몬테카를로 VaR 리스크 측정

VaR이란 무엇인가

VaR(Value at Risk)은 특정 신뢰수준에서 일정 기간 동안 발생할 수 있는 최대 예상 손실액을 하나의 숫자로 표현하는 리스크 지표입니다. 예를 들어 “95% VaR = 500만 원”이라면, 95% 확률로 하루 손실이 500만 원을 넘지 않는다는 의미입니다. 1990년대 JP모건의 RiskMetrics 이후 금융 업계의 표준 리스크 측정 도구로 자리 잡았으며, 바젤 규제에서도 핵심 지표로 사용됩니다.

개인 퀀트 트레이더에게도 VaR은 필수입니다. 포지션 사이징, 레버리지 한도 설정, 전략별 리스크 배분 등 체계적 리스크 관리의 출발점이 바로 VaR이기 때문입니다.

VaR 계산의 세 가지 방법

VaR을 계산하는 대표적인 방법 세 가지를 비교합니다.

  • 역사적 시뮬레이션(Historical Simulation) — 과거 수익률 분포에서 직접 백분위를 추출합니다. 모델 가정이 없어 단순하지만, 과거 데이터에 의존하므로 미래 레짐 변화에 취약합니다.
  • 분산-공분산법(Parametric) — 수익률이 정규분포를 따른다고 가정하고 평균·표준편차로 계산합니다. 빠르지만 팻테일을 과소평가합니다.
  • 몬테카를로 시뮬레이션 — 확률 모델에서 수만~수십만 개의 시나리오를 생성하여 손실 분포를 구합니다. 가장 유연하며 비선형 포지션(옵션 등)에도 적용 가능합니다.

이 글에서는 가장 실전적인 몬테카를로 VaR에 집중합니다.

파이썬 몬테카를로 VaR 구현

기하 브라운 운동(GBM) 기반의 기본 몬테카를로 VaR 코드입니다.

import numpy as np
import pandas as pd

def monte_carlo_var(returns: pd.Series, 
                    portfolio_value: float,
                    confidence: float = 0.95,
                    horizon: int = 1,
                    n_simulations: int = 100000,
                    seed: int = 42):
    """
    몬테카를로 시뮬레이션 기반 VaR 계산
    
    Parameters:
        returns: 일일 수익률 시계열
        portfolio_value: 포트폴리오 현재 가치
        confidence: 신뢰수준 (0.95 = 95%)
        horizon: 보유 기간 (일)
        n_simulations: 시뮬레이션 횟수
    
    Returns:
        var_amount: VaR 금액
        cvar_amount: CVaR(ES) 금액
        simulated_returns: 시뮬레이션 수익률 배열
    """
    np.random.seed(seed)
    
    mu = returns.mean()
    sigma = returns.std()
    
    # GBM 시뮬레이션
    daily_returns = np.random.normal(
        mu, sigma, (n_simulations, horizon)
    )
    
    # 다기간 누적 수익률
    cumulative = np.prod(1 + daily_returns, axis=1) - 1
    
    # 손실 분포 (음수 = 손실)
    simulated_pnl = portfolio_value * cumulative
    
    # VaR: 하위 (1-confidence) 백분위
    var_percentile = np.percentile(
        simulated_pnl, (1 - confidence) * 100
    )
    var_amount = abs(var_percentile)
    
    # CVaR (Expected Shortfall): VaR 초과 손실의 평균
    tail_losses = simulated_pnl[simulated_pnl <= var_percentile]
    cvar_amount = abs(tail_losses.mean())
    
    return var_amount, cvar_amount, cumulative

# 실행 예시
df = pd.read_csv("btc_daily.csv", parse_dates=["date"])
returns = df["close"].pct_change().dropna()

var_95, cvar_95, sims = monte_carlo_var(
    returns, 
    portfolio_value=100_000_000,  # 1억 원
    confidence=0.95,
    horizon=1,
    n_simulations=200000
)

print(f"95% 1일 VaR: {var_95:,.0f}원")
print(f"95% 1일 CVaR: {cvar_95:,.0f}원")

CVaR이 VaR보다 나은 이유

VaR은 "최대 손실이 얼마인가"만 알려주지만, CVaR(Conditional VaR, Expected Shortfall)은 "VaR을 초과하는 손실의 평균"을 알려줍니다. 이 차이가 실전에서 매우 중요합니다.

  • 테일 리스크 반영 — VaR은 95번째 백분위의 한 점만 보지만, CVaR은 하위 5% 전체 평균을 봅니다. 꼬리 위험을 더 정확히 포착합니다.
  • 일관성(Coherence) — VaR은 분산 불가능할 수 있지만(두 포트폴리오 VaR 합 < 결합 VaR), CVaR은 항상 하위 가산성(subadditivity)을 만족합니다.
  • 바젤 III 전환 — 규제도 VaR에서 CVaR(ES)로 이동하고 있습니다. 97.5% ES가 새로운 표준입니다.

다자산 포트폴리오 몬테카를로 VaR

여러 자산으로 구성된 포트폴리오의 VaR을 계산할 때는 자산 간 상관관계를 반영해야 합니다. 촐레스키 분해(Cholesky Decomposition)를 사용합니다.

def portfolio_monte_carlo_var(returns_df: pd.DataFrame,
                               weights: np.ndarray,
                               portfolio_value: float,
                               confidence: float = 0.95,
                               horizon: int = 1,
                               n_sims: int = 100000):
    """
    다자산 상관관계 반영 몬테카를로 VaR
    
    Parameters:
        returns_df: 자산별 일일 수익률 DataFrame
        weights: 자산 비중 배열 (합계 = 1)
    """
    mu = returns_df.mean().values
    cov = returns_df.cov().values
    n_assets = len(mu)
    
    # 촐레스키 분해로 상관 구조 반영
    L = np.linalg.cholesky(cov)
    
    portfolio_returns = np.zeros(n_sims)
    
    for sim in range(n_sims):
        cum_return = np.zeros(n_assets)
        for d in range(horizon):
            z = np.random.standard_normal(n_assets)
            correlated = L @ z
            daily = mu + correlated
            cum_return += daily
        
        portfolio_returns[sim] = weights @ cum_return
    
    pnl = portfolio_value * portfolio_returns
    var_val = abs(np.percentile(pnl, (1 - confidence) * 100))
    cvar_val = abs(pnl[pnl <= -var_val].mean()) if np.any(pnl <= -var_val) else var_val
    
    return var_val, cvar_val

# 예시: BTC 50%, ETH 30%, SOL 20% 포트폴리오
weights = np.array([0.5, 0.3, 0.2])
var_p, cvar_p = portfolio_monte_carlo_var(
    returns_df, weights, 
    portfolio_value=100_000_000,
    confidence=0.99,
    horizon=5  # 5일 VaR
)
print(f"99% 5일 포트폴리오 VaR: {var_p:,.0f}원")
print(f"99% 5일 포트폴리오 CVaR: {cvar_p:,.0f}원")

GARCH 결합 몬테카를로 VaR

기본 GBM은 변동성을 상수로 가정하지만, 실제 시장의 변동성은 시간에 따라 변합니다. GARCH 모델을 결합하면 현재 변동성 상태를 반영한 더 정확한 VaR을 계산할 수 있습니다.

from arch import arch_model

def garch_monte_carlo_var(returns: pd.Series,
                          portfolio_value: float,
                          confidence: float = 0.95,
                          horizon: int = 5,
                          n_sims: int = 50000):
    """
    GARCH(1,1) 기반 몬테카를로 VaR
    시변 변동성을 반영한 정교한 리스크 측정
    """
    # GARCH(1,1) 피팅
    am = arch_model(returns * 100, vol="Garch", p=1, q=1)
    res = am.fit(disp="off")
    
    omega = res.params["omega"]
    alpha = res.params["alpha[1]"]
    beta = res.params["beta[1]"]
    last_var = res.conditional_volatility.iloc[-1]**2
    last_resid = res.resid.iloc[-1]
    
    sim_returns = np.zeros((n_sims, horizon))
    
    for i in range(n_sims):
        h = last_var
        for t in range(horizon):
            z = np.random.standard_normal()
            r = np.sqrt(h) * z / 100  # 스케일 복원
            sim_returns[i, t] = r
            h = omega + alpha * (z * np.sqrt(h))**2 + beta * h
    
    cum_returns = np.prod(1 + sim_returns, axis=1) - 1
    pnl = portfolio_value * cum_returns
    
    var_val = abs(np.percentile(pnl, (1 - confidence) * 100))
    tail = pnl[pnl <= np.percentile(pnl, (1 - confidence) * 100)]
    cvar_val = abs(tail.mean())
    
    return var_val, cvar_val, res

var_g, cvar_g, garch_res = garch_monte_carlo_var(
    returns, 100_000_000, confidence=0.99, horizon=10
)
print(f"GARCH 99% 10일 VaR: {var_g:,.0f}원")
print(f"GARCH 99% 10일 CVaR: {cvar_g:,.0f}원")

VaR 기반 포지션 사이징

VaR을 단순히 측정에 그치지 않고 실제 매매에 통합하는 방법입니다.

def var_position_sizing(capital: float,
                        max_var_pct: float,
                        asset_daily_vol: float,
                        confidence: float = 0.95):
    """
    VaR 한도 기반 최대 포지션 크기 결정
    
    Parameters:
        capital: 총 자본
        max_var_pct: 허용 VaR 비율 (예: 0.02 = 자본의 2%)
        asset_daily_vol: 자산의 일일 변동성
        confidence: 신뢰수준
    """
    from scipy.stats import norm
    z = norm.ppf(confidence)
    
    max_var = capital * max_var_pct
    max_position = max_var / (z * asset_daily_vol)
    
    leverage = max_position / capital
    
    return {
        "max_position": max_position,
        "leverage": leverage,
        "daily_var": max_var,
        "var_pct": max_var_pct
    }

# 일일 변동성 4%인 자산, 자본의 2% VaR 한도
sizing = var_position_sizing(
    capital=100_000_000,
    max_var_pct=0.02,
    asset_daily_vol=0.04,
    confidence=0.95
)
print(f"최대 포지션: {sizing['max_position']:,.0f}원")
print(f"레버리지: {sizing['leverage']:.2f}x")

이 접근법의 핵심은 리스크 예산(Risk Budget) 개념입니다. 전체 포트폴리오의 VaR 한도를 정하고, 각 전략·자산에 VaR을 배분하면 체계적인 리스크 관리가 가능합니다.

실전 적용 시 주의사항

  • 시뮬레이션 수 — 최소 10만 회 이상을 권장합니다. 99% VaR처럼 극단 백분위를 추정할수록 더 많은 시뮬레이션이 필요합니다.
  • 분포 가정 — 정규분포 대신 t-분포나 역사적 부트스트래핑을 사용하면 팻테일을 더 잘 반영합니다.
  • 백테스트 검증 — VaR 모델이 정확한지 확인하려면 실제 손실이 VaR을 초과한 횟수(VaR violation)를 추적합니다. 95% VaR이면 약 5%의 초과가 정상입니다.
  • 스트레스 테스트 병행 — VaR은 "정상적" 시장 상황의 리스크입니다. 2020년 코로나 폭락이나 2022년 LUNA 사태 같은 극단 시나리오는 별도 스트레스 테스트로 보완하세요.
  • 갱신 주기 — 변동성 레짐이 바뀌면 VaR도 크게 변합니다. 최소 일 1회, 변동성 급등 시에는 실시간 재계산이 필요합니다.

마무리 — VaR은 시작이다

몬테카를로 VaR은 퀀트 리스크 관리의 기본 인프라입니다. 단일 숫자로 리스크를 요약하되, CVaR로 테일 리스크를 보완하고, GARCH로 시변 변동성을 반영하며, 포지션 사이징에 직접 연결하는 것이 실전 활용의 핵심입니다. 리스크를 측정할 수 없으면 관리할 수 없고, 관리할 수 없으면 수익도 지속 불가능합니다.

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