리스크 패리티(Risk Parity)란?
리스크 패리티는 각 자산이 포트폴리오 전체 리스크에 동일한 비중으로 기여하도록 자금을 배분하는 전략입니다. 1996년 브리지워터의 레이 달리오가 ‘올웨더(All Weather)’ 포트폴리오로 대중화했으며, 현재 전 세계 기관 투자자들이 가장 많이 사용하는 자산 배분 프레임워크 중 하나입니다.
전통적인 60/40 포트폴리오(주식 60%, 채권 40%)는 금액 기준으로는 분산되어 보이지만, 리스크의 90% 이상이 주식에 집중됩니다. 리스크 패리티는 이 문제를 해결합니다.
리스크 기여도(Risk Contribution) 이해하기
리스크 패리티의 핵심 개념은 한계 리스크 기여도(Marginal Risk Contribution)입니다:
포트폴리오 변동성: σ_p = √(w^T × Σ × w)
자산 i의 한계 리스크 기여도(MRC_i):
MRC_i = (Σ × w)_i / σ_p
자산 i의 총 리스크 기여도(TRC_i):
TRC_i = w_i × MRC_i
리스크 패리티 조건:
TRC_1 = TRC_2 = ... = TRC_n = σ_p / n
즉, 모든 자산의 총 리스크 기여도가 동일해야 합니다. 변동성이 높은 자산은 비중을 줄이고, 낮은 자산은 비중을 늘립니다.
파이썬으로 리스크 패리티 구현
scipy 최적화를 사용한 리스크 패리티 포트폴리오 구현입니다:
import numpy as np
import pandas as pd
from scipy.optimize import minimize
def risk_parity_weights(cov_matrix: np.ndarray) -> np.ndarray:
"""
리스크 패리티 최적 비중 계산
목적함수: 각 자산의 리스크 기여도 차이를 최소화
"""
n = cov_matrix.shape[0]
target_risk = 1.0 / n # 각 자산의 목표 리스크 비중
def objective(w):
port_vol = np.sqrt(w @ cov_matrix @ w)
mrc = (cov_matrix @ w) / port_vol
trc = w * mrc
risk_contrib = trc / port_vol
# 각 자산의 리스크 기여도와 목표의 차이 제곱합
return np.sum((risk_contrib - target_risk) ** 2)
constraints = {"type": "eq", "fun": lambda w: np.sum(w) - 1}
bounds = [(0.01, 1.0)] * n
x0 = np.ones(n) / n
result = minimize(objective, x0, method="SLSQP",
bounds=bounds, constraints=constraints)
return result.x
def analyze_risk_contribution(weights, cov_matrix, asset_names):
"""리스크 기여도 분석"""
port_vol = np.sqrt(weights @ cov_matrix @ weights)
mrc = (cov_matrix @ weights) / port_vol
trc = weights * mrc
risk_pct = trc / port_vol
df = pd.DataFrame({
"자산": asset_names,
"비중": [f"{w:.1%}" for w in weights],
"한계리스크기여도": [f"{m:.4f}" for m in mrc],
"리스크기여비중": [f"{r:.1%}" for r in risk_pct],
})
return df, port_vol
# 4자산 포트폴리오 예시 (주식, 채권, 원자재, 금)
asset_names = ["주식", "채권", "원자재", "금"]
annual_vol = np.array([0.18, 0.05, 0.22, 0.15])
correlation = np.array([
[1.0, -0.2, 0.3, 0.0],
[-0.2, 1.0, -0.1, 0.3],
[0.3, -0.1, 1.0, 0.1],
[0.0, 0.3, 0.1, 1.0],
])
cov_matrix = np.outer(annual_vol, annual_vol) * correlation
# 리스크 패리티 비중 계산
rp_weights = risk_parity_weights(cov_matrix)
df, port_vol = analyze_risk_contribution(rp_weights, cov_matrix, asset_names)
print("=== 리스크 패리티 포트폴리오 ===")
print(df.to_string(index=False))
print(f"n포트폴리오 연간 변동성: {port_vol:.2%}")
60/40 vs 리스크 패리티 비교
전통적 60/40 포트폴리오와 리스크 패리티의 차이를 비교합니다:
| 항목 | 60/40 포트폴리오 | 리스크 패리티 |
|---|---|---|
| 배분 기준 | 금액 비중 | 리스크 기여도 |
| 주식 리스크 비중 | ~90% | ~25% |
| 분산 효과 | 낮음 (주식에 편중) | 높음 (균등 분산) |
| 하락장 방어 | 취약 | 상대적 우수 |
| 구현 복잡도 | 단순 | 최적화 필요 |
| 리밸런싱 | 분기/반기 | 월간 권장 |
롤링 리밸런싱 백테스트
리스크 패리티를 월간 리밸런싱으로 백테스트하는 코드입니다:
def backtest_risk_parity(returns_df: pd.DataFrame,
lookback: int = 252,
rebalance_freq: str = "M") -> pd.DataFrame:
"""
롤링 리스크 패리티 백테스트
매월 말 과거 252일 데이터로 공분산 추정 후 리밸런싱
"""
rebalance_dates = returns_df.resample(rebalance_freq).last().index
weights_history = {}
portfolio_returns = []
current_weights = np.ones(len(returns_df.columns)) / len(returns_df.columns)
for date in returns_df.index:
if date in rebalance_dates:
loc = returns_df.index.get_loc(date)
if loc >= lookback:
window = returns_df.iloc[loc - lookback:loc]
cov = window.cov().values * 252 # 연율화
try:
current_weights = risk_parity_weights(cov)
except Exception:
pass # 최적화 실패 시 이전 비중 유지
weights_history[date] = current_weights
daily_ret = returns_df.loc[date].values
port_ret = np.sum(current_weights * daily_ret)
portfolio_returns.append({"date": date, "return": port_ret})
result = pd.DataFrame(portfolio_returns).set_index("date")
result["cumulative"] = (1 + result["return"]).cumprod()
return result
# 사용 예시 (가상 데이터)
np.random.seed(42)
dates = pd.date_range("2020-01-01", "2025-12-31", freq="B")
returns = pd.DataFrame({
"주식": np.random.normal(0.0004, 0.012, len(dates)),
"채권": np.random.normal(0.0001, 0.003, len(dates)),
"원자재": np.random.normal(0.0002, 0.015, len(dates)),
"금": np.random.normal(0.0002, 0.010, len(dates)),
}, index=dates)
result = backtest_risk_parity(returns, lookback=252)
total_return = result["cumulative"].iloc[-1] - 1
annual_return = (1 + total_return) ** (252 / len(result)) - 1
max_dd = (result["cumulative"] / result["cumulative"].cummax() - 1).min()
print(f"누적 수익률: {total_return:.2%}")
print(f"연평균 수익률: {annual_return:.2%}")
print(f"최대 낙폭: {max_dd:.2%}")
실전 적용 시 고려사항
- 공분산 추정 안정성: 짧은 윈도우는 노이즈가 많고, 긴 윈도우는 최신 시장 상황을 반영하지 못합니다. 지수가중 공분산(EWMA)을 사용하면 최근 데이터에 더 높은 가중치를 줄 수 있습니다.
- 레버리지 활용: 순수 리스크 패리티는 채권 비중이 높아 기대 수익률이 낮을 수 있습니다. 기관에서는 레버리지를 통해 목표 수익률을 맞추지만, 개인 투자자는 켈리 기준을 참고해 적정 레버리지 수준을 판단하세요.
- 거래 비용: 월간 리밸런싱은 거래 비용이 발생합니다. 슬리피지 최적화를 적용하고, 비중 변화가 임계값(예: 2%) 이상일 때만 리밸런싱하는 밴드 방식을 고려하세요.
- 위기 상황: 금융 위기 시 상관관계가 급등하면 리스크 패리티의 분산 효과가 약해집니다. 레짐 필터를 추가해 고변동성 장세에서는 전체 노출을 줄이는 것이 좋습니다.
- 자산 유니버스 선택: 최소 3~4개 자산 클래스를 포함해야 합니다. 상관관계가 낮은 자산일수록 리스크 패리티의 효과가 극대화됩니다.
리스크 패리티 변형 전략
def hierarchical_risk_parity(returns_df: pd.DataFrame) -> np.ndarray:
"""
계층적 리스크 패리티(HRP) - 간소화 버전
상관 행렬 기반 클러스터링 후 하향식 배분
"""
from scipy.cluster.hierarchy import linkage, leaves_list
from scipy.spatial.distance import squareform
corr = returns_df.corr().values
dist = np.sqrt(0.5 * (1 - corr))
np.fill_diagonal(dist, 0)
# 계층적 클러스터링
condensed = squareform(dist)
link = linkage(condensed, method="ward")
order = leaves_list(link)
# 역분산 가중 (정렬된 순서)
n = len(order)
vol = returns_df.std().values
inv_vol = 1.0 / vol
weights = np.zeros(n)
for i, idx in enumerate(order):
weights[idx] = inv_vol[idx]
weights /= weights.sum()
return weights
# HRP는 공분산 역행렬이 불안정한 경우의 대안
hrp_w = hierarchical_risk_parity(returns)
print("HRP 비중:")
for name, w in zip(returns.columns, hrp_w):
print(f" {name}: {w:.1%}")
핵심 정리
리스크 패리티는 금액이 아닌 리스크 기여도를 균등 배분하는 포트폴리오 전략입니다. 전통적 60/40 대비 진정한 분산 투자를 실현하며, 파이썬으로 공분산 추정부터 최적화, 백테스트까지 자동화할 수 있습니다. 몬테카를로 포트폴리오 최적화와 결합하면 더욱 정교한 자산 배분이 가능합니다.