몬테카를로 포트폴리오 리스크 분석

몬테카를로 시뮬레이션이란?

몬테카를로 시뮬레이션(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-분포(팻테일)를 사용하면 극단적 손실을 현실적으로 반영할 수 있습니다
  • 스트레스 테스트를 결합하면 블랙스완 이벤트에 대한 포트폴리오 내구성을 검증할 수 있습니다
  • 시뮬레이션 결과를 켈리 기준 자금 관리와 결합하면 최적 포지션 사이징까지 도출할 수 있습니다

단, 몬테카를로 시뮬레이션은 입력 파라미터(평균 수익률, 변동성, 상관관계)의 품질에 크게 좌우됩니다. 백테스트 데이터 전처리를 철저히 수행하고, 정기적으로 파라미터를 업데이트하는 것이 실전 운용의 핵심입니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux