몬테카를로 시뮬레이션이란?
몬테카를로 시뮬레이션(Monte Carlo Simulation)은 무작위 샘플링을 반복하여 확률적 결과를 추정하는 통계 기법입니다. 퀀트 투자에서는 포트폴리오의 미래 수익률 분포를 시뮬레이션하여 리스크를 정량적으로 측정하는 데 핵심적으로 활용됩니다.
전통적인 분석 방법이 정규분포를 가정하는 반면, 몬테카를로 시뮬레이션은 팻테일(fat tail)이나 비대칭 분포 같은 현실적인 시장 특성을 반영할 수 있어 더 정확한 리스크 추정이 가능합니다.
포트폴리오 리스크 분석에 왜 필요한가
단순히 과거 수익률의 표준편차만으로는 포트폴리오의 실질적인 위험을 파악하기 어렵습니다. 몬테카를로 시뮬레이션을 활용하면 다음과 같은 핵심 리스크 지표를 산출할 수 있습니다.
- VaR(Value at Risk): 특정 신뢰수준에서 최대 손실 추정
- CVaR(Conditional VaR): VaR을 초과하는 손실의 평균값
- 최대 낙폭(Max Drawdown) 분포: 최악의 시나리오에서 자산 하락 폭
- 파산 확률(Probability of Ruin): 원금 대비 특정 비율 이하로 떨어질 확률
이러한 지표들은 샤프 비율만으로는 포착하기 어려운 꼬리 위험(tail risk)을 정량화하는 데 필수적입니다.
파이썬 구현: 기본 몬테카를로 시뮬레이션
NumPy를 활용한 기본적인 포트폴리오 몬테카를로 시뮬레이션 구현입니다. 일별 수익률이 정규분포를 따른다고 가정합니다.
import numpy as np
import pandas as pd
def monte_carlo_portfolio(
mean_returns: np.ndarray,
cov_matrix: np.ndarray,
weights: np.ndarray,
initial_value: float = 10_000_000,
days: int = 252,
simulations: int = 10_000
) -> np.ndarray:
"""
포트폴리오 몬테카를로 시뮬레이션
Returns: (simulations, days+1) 형태의 포트폴리오 가치 배열
"""
port_mean = np.dot(weights, mean_returns)
port_std = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
# 일별 랜덤 수익률 생성
daily_returns = np.random.normal(port_mean, port_std, (simulations, days))
# 누적 수익률로 포트폴리오 가치 계산
price_paths = np.zeros((simulations, days + 1))
price_paths[:, 0] = initial_value
for t in range(1, days + 1):
price_paths[:, t] = price_paths[:, t-1] * (1 + daily_returns[:, t-1])
return price_paths
# 사용 예시: 3종목 포트폴리오
mean_returns = np.array([0.0004, 0.0006, 0.0003]) # 일별 평균 수익률
cov_matrix = np.array([
[0.0004, 0.0001, 0.00005],
[0.0001, 0.0006, 0.0002],
[0.00005, 0.0002, 0.0003]
])
weights = np.array([0.4, 0.35, 0.25])
paths = monte_carlo_portfolio(mean_returns, cov_matrix, weights)
print(f"시뮬레이션 경로 수: {paths.shape[0]}")
print(f"1년 후 평균 포트폴리오 가치: {paths[:, -1].mean():,.0f}원")
VaR과 CVaR 계산
시뮬레이션 결과로부터 VaR과 CVaR을 계산하는 방법입니다. 이 두 지표는 자동매매 리스크 관리에서도 핵심적으로 활용됩니다.
def calculate_risk_metrics(price_paths: np.ndarray, confidence: float = 0.95):
"""VaR, CVaR, 최대 낙폭 계산"""
initial = price_paths[:, 0]
final = price_paths[:, -1]
returns = (final - initial) / initial
# VaR: 하위 (1-confidence)% 수익률
var_percentile = np.percentile(returns, (1 - confidence) * 100)
var_amount = initial[0] * abs(var_percentile)
# CVaR: VaR 이하 수익률의 평균
cvar_returns = returns[returns <= var_percentile]
cvar_amount = initial[0] * abs(cvar_returns.mean())
# 최대 낙폭 분포
max_drawdowns = []
for path in price_paths:
running_max = np.maximum.accumulate(path)
drawdowns = (path - running_max) / running_max
max_drawdowns.append(drawdowns.min())
return {
"VaR_95": f"{abs(var_percentile)*100:.2f}%",
"VaR_금액": f"{var_amount:,.0f}원",
"CVaR_95": f"{abs(cvar_returns.mean())*100:.2f}%",
"CVaR_금액": f"{cvar_amount:,.0f}원",
"평균_최대낙폭": f"{abs(np.mean(max_drawdowns))*100:.2f}%",
"최악_최대낙폭": f"{abs(np.min(max_drawdowns))*100:.2f}%",
"파산확률_50%손실": f"{(returns < -0.5).mean()*100:.2f}%"
}
metrics = calculate_risk_metrics(paths)
for k, v in metrics.items():
print(f"{k}: {v}")
고급 기법: GBM과 팻테일 분포
현실의 금융 시장은 정규분포보다 꼬리가 두꺼운 분포를 보입니다. 기하 브라운 운동(GBM)과 t-분포를 결합하면 더 현실적인 시뮬레이션이 가능합니다.
from scipy import stats
def monte_carlo_gbm_fat_tail(
mu: float,
sigma: float,
initial_value: float,
days: int = 252,
simulations: int = 10_000,
df: int = 5 # t-분포 자유도 (낮을수록 꼬리 두꺼움)
) -> np.ndarray:
"""GBM + t-분포 기반 시뮬레이션"""
dt = 1 / 252
paths = np.zeros((simulations, days + 1))
paths[:, 0] = initial_value
for t in range(1, days + 1):
# t-분포에서 랜덤 샘플링 (팻테일 반영)
z = stats.t.rvs(df=df, size=simulations)
# 분산 보정: t-분포의 분산은 df/(df-2)
z = z / np.sqrt(df / (df - 2))
# GBM 공식 적용
paths[:, t] = paths[:, t-1] * np.exp(
(mu - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * z
)
return paths
# 연간 기대수익률 10%, 변동성 25%
paths_fat = monte_carlo_gbm_fat_tail(mu=0.10, sigma=0.25, initial_value=10_000_000)
metrics_fat = calculate_risk_metrics(paths_fat)
print("=== 팻테일 모델 리스크 지표 ===")
for k, v in metrics_fat.items():
print(f"{k}: {v}")
자유도(df)를 낮출수록 극단적 사건의 빈도가 증가합니다. 일반적으로 주식 시장은 자유도 4~6 정도의 t-분포가 잘 맞는 것으로 알려져 있습니다.
시나리오 스트레스 테스트
몬테카를로 시뮬레이션에 역사적 위기 시나리오를 결합하면 스트레스 테스트가 가능합니다.
def stress_test_scenarios(
base_paths: np.ndarray,
crash_probability: float = 0.02,
crash_magnitude: float = -0.15
) -> np.ndarray:
"""시나리오 기반 스트레스 테스트"""
stressed_paths = base_paths.copy()
n_sims, n_days = stressed_paths.shape
for sim in range(n_sims):
for day in range(1, n_days):
# 일정 확률로 급락 이벤트 발생
if np.random.random() < crash_probability / 252:
shock = np.random.uniform(crash_magnitude * 1.5, crash_magnitude)
stressed_paths[sim, day:] *= (1 + shock)
return stressed_paths
stressed = stress_test_scenarios(paths)
print("=== 스트레스 테스트 결과 ===")
stress_metrics = calculate_risk_metrics(stressed)
for k, v in stress_metrics.items():
print(f"{k}: {v}")
실전 적용 팁
몬테카를로 시뮬레이션을 실제 자동매매 시스템에 적용할 때 주의할 점을 정리합니다.
| 항목 | 권장 설정 | 비고 |
|---|---|---|
| 시뮬레이션 횟수 | 10,000회 이상 | 안정적 VaR 추정 기준 |
| 데이터 기간 | 최소 3년 | 위기 구간 포함 필수 |
| 리밸런싱 반영 | 월별 또는 분기별 | 고정 비중 가정 위험 |
| 거래비용 | 편도 0.1~0.3% | 슬리피지 포함 |
| 분포 모델 | t-분포 (df=5) | 정규분포 과소추정 주의 |
핵심 정리
몬테카를로 시뮬레이션은 퀀트 포트폴리오의 리스크를 확률적으로 평가하는 가장 강력한 도구입니다. 핵심 포인트를 요약하면 다음과 같습니다.
- VaR/CVaR로 손실 위험을 정량화하고, 최대 낙폭 분포로 최악의 시나리오를 대비합니다
- 정규분포 대신 t-분포(팻테일)를 사용하면 극단적 손실을 현실적으로 반영할 수 있습니다
- 스트레스 테스트를 결합하면 블랙스완 이벤트에 대한 포트폴리오 내구성을 검증할 수 있습니다
- 시뮬레이션 결과를 켈리 기준 자금 관리와 결합하면 최적 포지션 사이징까지 도출할 수 있습니다
단, 몬테카를로 시뮬레이션은 입력 파라미터(평균 수익률, 변동성, 상관관계)의 품질에 크게 좌우됩니다. 백테스트 데이터 전처리를 철저히 수행하고, 정기적으로 파라미터를 업데이트하는 것이 실전 운용의 핵심입니다.