Riskfolio-Lib 리뷰 — 37개 리스크 지표가 필요한 사람, 과한 사람
코스피 ETF와 미국 지수 ETF, 금과 국채를 섞은 12개 자산 바스켓을 Riskfolio-Lib으로 두 번 최적화했습니다. 표준편차 기준에서 단기 국채 ETF 하나가 41%를 가져갔는데, CVaR로 바꾸자 그 자리가 23%로 내려가고 원자재 비중이 올라왔습니다. 데이터도 기간도 그대로였고, 바뀐 건 리스크를 재는 자 하나뿐이었죠.
추천부터 박아둘게요. 자산 10개 안팎을 월 1회 리밸런싱하는 개인이라면 HRP 한 줄로 시작하고 나머지 지표는 손대지 마세요. 전략 여럿을 합성하거나 낙폭 관리가 운용 조건에 들어가면 CDaR·EDaR 계열 때문에라도 이 라이브러리를 고를 이유가 생깁니다. 반면 파이썬을 이제 막 잡은 분께는 과합니다. CVXPY 위에 얹힌 도구라, 해가 안 풀릴 때 읽어야 하는 메시지가 금융 언어가 아니라 최적화 언어예요.
백테스트를 다 돌린 뒤에야 만난 진짜 병목
전략 하나의 성적을 재는 일과 전략·자산 여럿을 섞는 일은 푸는 문제가 다릅니다. 저는 신호는 멀쩡한데 비중을 눈대중으로 주다가 한 자산에 절반이 쏠린 계좌를 몇 달 들고 있었습니다.
이번 리뷰의 기준은 셋입니다. 리스크 측정치 커버리지, 해가 안정적으로 나오는가, 결과를 남에게 설명할 수 있는가. 환경은 파이썬 3.11 노트북 CPU였고, 백테스트 조건은 2021년 1월부터 2025년 12월까지 일봉, 월말 1회 리밸런싱, 왕복 수수료 0.03%, 슬리피지 5bp였어요. 수익률 이야기는 하지 않습니다. 배분 도구 리뷰지 성과 자랑이 아니니까요.
37개라는 숫자는 정확히 어디에 붙어 있을까요?
공식 저장소 설명을 읽으면 숫자가 세 번 나옵니다. 평균-리스크 최적화에 쓰이는 볼록 리스크 측정치가 26개, 리스크 패리티 쪽이 22개, 계층적 군집 기반인 HRP와 HERC가 37개예요. 37은 평균-분산 자리에 붙은 숫자가 아니라 계층 구조 쪽 숫자입니다.
이 차이를 모르면 헛다리를 짚습니다. 저도 처음엔 37개를 전부 볼록최적화로 직접 최소화할 수 있다고 믿었어요. 실제로는 VaR처럼 볼록하지 않은 지표가 계층 쪽 목록에만 들어 있습니다. 그나마 분산형·하방형·낙폭형 세 계열로 분류가 정리돼 있어 길을 잃지는 않았고요. 위 분류와 개수는 Riskfolio-Lib 공식 저장소 설명 기준입니다.
같은 12자산으로 MVO·CVaR·HRP를 나란히 돌려봤습니다
일간 수익률 데이터프레임을 Portfolio 객체에 넘긴 뒤 리스크 측정치 문자열만 바꿔 호출하면 끝입니다. 판다스 자료구조와 붙어 있어 준비 단계가 짧아요.
import riskfolio as rp
port = rp.Portfolio(returns=rets) # rets: 일간 수익률 DataFrame
port.assets_stats(method_mu="hist", method_cov="hist")
w_mv = port.optimization(model="Classic", rm="MV", obj="MinRisk")
w_cvar = port.optimization(model="Classic", rm="CVaR", obj="MinRisk")
rm 인자 하나로 표준편차에서 CVaR로 넘어갑니다. 걸린 건 그다음이었죠. 낙폭이 궁금해 CDaR로 바꿨더니 비중이 또 흔들렸고, EVaR를 시도하자 솔버가 발목을 잡았습니다. 지수 원뿔을 다루는 문제라 기본 조합으로는 수렴이 늦어요. 제 노트북에서 자산 12개 CVaR 최소화는 1초 안쪽이었는데 EVaR는 6초대였습니다. 월 1회 리밸런싱이면 상관없지만, 파라미터를 바꿔가며 200번 도는 검증 루프에서는 체감이 완전히 달라집니다.
HRP는 성격이 아예 다릅니다. 공분산을 뒤집지 않고 상관 구조로 자산을 묶어 내려가서, 자산 수가 늘고 표본이 짧아도 해가 나와요.
hc = rp.HCPortfolio(returns=rets)
w_hrp = hc.optimization(model="HRP", rm="MV", linkage="single")
제 실험에서 HRP 비중의 최대값은 19%였습니다. 같은 데이터로 MVO가 41%를 한 곳에 몰아준 것과 나란히 놓으면 성격 차이가 분명하죠. 대신 군집 트리를 그려 보여주지 않으면 왜 이 비중인지 상대를 납득시키기 어렵습니다.
어떤 사람에게 이 라이브러리가 과할까요?
안 맞는 경우부터 말할게요. 자산이 5개 이하라면 37개든 26개든 의미가 없습니다. 제가 4자산으로 줄여 확인했더니 MVO와 CVaR 결과 차이가 비중 기준 2%포인트 안쪽이었습니다. 데이터가 2년도 안 되면 비슷하죠. 꼬리를 재는 CVaR·EVaR는 극단 구간 표본을 먹고 사는데, 표본이 없으면 숫자만 그럴듯해지거든요.
두 번째 함정은 선택지 자체입니다. 지표가 많으니 검증 성적이 제일 잘 나오는 쪽을 고르고 싶어져요. 그건 최적화가 아니라 사후 고르기죠. 저는 지표를 먼저 확정하고 검증하는 순서로 스스로를 묶어뒀습니다.
한계도 분명해요. 기대수익 추정을 이 도구가 대신 풀어주지는 않습니다. 과거 평균을 그대로 넣으면 MVO는 여전히 과거를 향해 쏠려요. 거래비용과 회전율 제약도 제 쪽에서 따로 얹어야 했고, 문서가 노트북 예제 중심이라 레퍼런스만으로는 감을 잡기 어렵습니다.
직접 짠 numpy 스크립트를 버린 날
두 달 동안 제가 짠 60줄짜리 numpy 평균-분산 코드를 고집했어요. 그걸 버린 계기는 지인의 질문 하나였습니다. 최대 낙폭이 일정 수준을 넘지 않게 묶을 수 있느냐는 것. 낙폭은 경로에 의존하는 값이라 공분산 한 장으로는 안 되고, 직접 구현하려다 며칠을 태웠죠. Riskfolio-Lib에서는 rm 인자를 CDaR로 바꾸는 것으로 끝났습니다. 그때 제가 아껴야 할 시간이 최적화 구현이 아니라 데이터 정합성과 리밸런싱 규칙 쪽에 있다는 걸 알았어요.
가벼운 대안도 있어요. 평균-분산과 블랙-리터만 정도면 PyPortfolioOpt가 더 짧게 끝나고, scikit-learn 문법이 편하면 skfolio 쪽이 손에 붙습니다. 저는 낙폭 계열이 필요해 남았고요. 지표 개수는 자랑거리가 아니라 분기점입니다. 솔버 조합과 버전은 제가 확인한 시점 기준이라 바뀌면 추후 업데이트하겠습니다.
자주 묻는 질문
Q. HRP를 쓰면 공분산 추정이 필요 없나요?
A. 상관·거리 행렬은 여전히 필요합니다. 다만 역행렬을 만들지 않아서 자산 수가 표본 길이에 가까워질 때 수치가 덜 튀어요. 12자산을 1년치 데이터로 줄여봤을 때 MVO는 해가 불안정했지만 HRP는 나왔습니다.
Q. 리밸런싱 주기는 어떻게 잡았나요?
A. 월말 1회로 고정했습니다. 왕복 수수료 0.03%·슬리피지 5bp 가정에서 주간 리밸런싱은 회전율이 세 배 넘게 뛰었어요.
Q. 최소 몇 개 자산부터 의미가 있을까요?
A. 제 실험 범위에서는 8개 근처가 분기점이었어요. 그 아래에선 지표를 바꿔도 비중 변화가 작았고, 12개를 넘기면서 계열별 차이가 눈에 보이기 시작했습니다.