- Authors
- Name
- 들어가며
- 변동성의 정의와 Stylized Facts
- ARCH 모델 기초
- GARCH(1,1) 심화
- GJR-GARCH와 레버리지 효과
- EGARCH 모델
- Python 실전 구현
- 모델 진단과 선택
- VaR, CVaR 계산
- 백테스팅과 검증
- 트러블슈팅
- 운영 체크리스트
- 실패 사례와 교훈
- 고급 주제: 변동성 예측의 다음 단계
- 참고자료

들어가며
금융 시장에서 수익률을 예측하는 것은 극히 어렵다. 그러나 변동성(volatility)을 예측하는 것은 상대적으로 가능하다. 이 차이가 리스크 관리의 출발점이다. 내일 주가가 오를지 내릴지는 모르지만, 내일의 가격 변동 폭이 어느 정도일지는 오늘의 시장 상태로부터 상당 부분 추론할 수 있다.
GARCH(Generalized Autoregressive Conditional Heteroskedasticity) 모델 패밀리는 이 변동성 예측의 핵심 도구다. 1982년 Robert Engle이 ARCH 모델을 제안한 이후, Tim Bollerslev의 GARCH(1986), Nelson의 EGARCH(1991), Glosten-Jagannathan-Runkle의 GJR-GARCH(1993) 등으로 진화하면서, 금융 변동성 모델링의 표준 프레임워크로 자리잡았다. Engle은 ARCH 모델로 2003년 노벨 경제학상을 수상했다.
이 글은 변동성 모델의 수학적 원리를 설명하되, 수식을 나열하는 데 그치지 않는다. Python arch 패키지를 활용한 실전 구현, 모델 선택과 진단, VaR(Value at Risk) 계산, 백테스팅까지 실무에서 필요한 전체 워크플로우를 다룬다. 수학적 배경이 약한 개발자도 코드를 따라가며 실제 데이터에 적용할 수 있도록 구성했다.
변동성의 정의와 Stylized Facts
변동성이란 무엇인가
변동성은 자산 수익률의 분산 또는 표준편차를 의미한다. 실무에서는 크게 세 가지로 구분한다.
- 역사적 변동성(Historical Volatility): 과거 수익률 데이터에서 계산한 표준편차. 가장 단순하지만 후행적이다.
- 내재 변동성(Implied Volatility): 옵션 가격으로부터 역산한 시장의 기대 변동성. VIX 지수가 대표적이다.
- 조건부 변동성(Conditional Volatility): 과거 정보를 조건으로 한 미래 변동성의 기댓값. GARCH 모델이 추정하는 대상이 바로 이것이다.
연율화 변동성은 일간 변동성에 거래일 수의 제곱근을 곱하여 계산한다. 한국 시장 기준 연간 거래일 약 250일을 적용하면, 일간 표준편차 1.2%는 연율화 변동성 약 19%에 해당한다.
금융 수익률의 Stylized Facts
실제 금융 데이터는 정규분포와 동일 분산을 가정하는 단순 통계 모형으로는 설명이 안 되는 특성들을 가진다. 이를 Stylized Facts라 부르며, GARCH 모델은 이러한 특성을 포착하기 위해 설계되었다.
| Stylized Fact | 설명 | GARCH 관련성 |
|---|---|---|
| 변동성 군집(Volatility Clustering) | 큰 변동 뒤에 큰 변동, 작은 변동 뒤에 작은 변동이 이어지는 경향 | GARCH의 핵심 가정이자 모델링 대상 |
| 두꺼운 꼬리(Fat Tails) | 수익률 분포의 꼬리가 정규분포보다 두꺼움 (첨도 3 초과) | Student-t, GED 등 비정규 분포 적용으로 포착 |
| 레버리지 효과(Leverage Effect) | 하락 충격이 상승 충격보다 변동성을 더 크게 증가시킴 | EGARCH, GJR-GARCH로 비대칭성 모델링 |
| 평균 회귀(Mean Reversion) | 변동성이 장기 평균 수준으로 되돌아가는 경향 | GARCH 모수의 정상성 조건으로 반영 |
| 장기 기억(Long Memory) | 변동성의 자기상관이 매우 천천히 감소 | FIGARCH, IGARCH 등 분수 적분 모델로 포착 |
import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
from scipy import stats
# KOSPI 200 ETF (KODEX 200) 또는 S&P 500 데이터 수집
spy = yf.download('SPY', start='2020-01-01', end='2026-03-01', auto_adjust=True)
returns = spy['Close'].pct_change().dropna() * 100 # 백분율 수익률
# Stylized Facts 확인
print(f"평균: {returns.mean():.4f}%")
print(f"표준편차: {returns.std():.4f}%")
print(f"왜도(Skewness): {returns.skew():.4f}")
print(f"첨도(Kurtosis): {returns.kurtosis():.4f}") # 정규분포 = 0 (excess kurtosis)
# Jarque-Bera 정규성 검정
jb_stat, jb_pval = stats.jarque_bera(returns)
print(f"Jarque-Bera 통계량: {jb_stat:.2f}, p-value: {jb_pval:.6f}")
# Ljung-Box 검정: 수익률 자체 vs 수익률 제곱
from statsmodels.stats.diagnostic import acorr_ljungbox
lb_returns = acorr_ljungbox(returns, lags=10, return_df=True)
lb_squared = acorr_ljungbox(returns**2, lags=10, return_df=True)
print(f"\n수익률 Ljung-Box p-value (lag=10): {lb_returns['lb_pvalue'].iloc[-1]:.4f}")
print(f"수익률^2 Ljung-Box p-value (lag=10): {lb_squared['lb_pvalue'].iloc[-1]:.6f}")
# 수익률 제곱의 p-value가 매우 작으면 변동성 군집 존재
위 코드를 실행하면 일반적으로 첨도가 3을 크게 초과하고(fat tails), 수익률 제곱의 Ljung-Box 검정이 유의미하게 나온다(변동성 군집). 이것이 GARCH 모델을 적용해야 하는 통계적 근거다.
ARCH 모델 기초
Engle의 ARCH(q) 모델
ARCH(AutoRegressive Conditional Heteroskedasticity) 모델은 조건부 분산이 과거 오차항의 제곱에 의존한다고 가정한다.
수익률 과정을 다음과 같이 정의한다.
r_t = mu + epsilon_t
epsilon_t = sigma_t * z_t, z_t ~ N(0,1)
여기서 조건부 분산 sigma_t^2는 다음과 같다.
sigma_t^2 = omega + alpha_1 * epsilon_{t-1}^2 + ... + alpha_q * epsilon_{t-q}^2
omega > 0: 장기 분산의 기저 수준alpha_i >= 0: 과거 충격의 영향 계수q: ARCH 차수 (과거 몇 시점의 충격을 반영할 것인가)
ARCH(1) 모델의 경우, 어제 큰 충격(양수든 음수든)이 발생하면 오늘의 조건부 분산이 커진다. 이것이 변동성 군집을 설명하는 메커니즘이다.
ARCH 모델의 한계
실무에서 ARCH 모델을 직접 쓰는 경우는 드물다. 변동성 군집을 제대로 포착하려면 q가 매우 커야 하는데, 이는 추정해야 할 모수가 많아진다는 뜻이다. ARCH(20) 이상을 설정해야 적합도가 나오는 경우가 흔하며, 이는 과적합 위험과 추정 불안정성을 초래한다. 이 한계를 극복한 것이 GARCH다.
GARCH(1,1) 심화
모델 구조
Bollerslev(1986)가 제안한 GARCH(p,q)는 조건부 분산에 과거 분산 자체를 추가한다.
sigma_t^2 = omega + alpha * epsilon_{t-1}^2 + beta * sigma_{t-1}^2
GARCH(1,1)의 세 가지 모수가 각각 의미하는 바는 다음과 같다.
omega: 장기 분산의 기여분. 값이 크면 분산의 하한이 높다.alpha: 뉴스 충격 계수. 어제 시장에서 발생한 충격이 오늘 변동성에 미치는 영향의 크기.beta: 분산 지속 계수. 어제의 변동성이 오늘로 얼마나 이어지는가.
정상성 조건과 모수 해석
GARCH(1,1)이 정상성(stationarity)을 가지려면 alpha + beta < 1이어야 한다. 이 값이 1에 가까울수록 변동성 충격이 오래 지속된다.
무조건 분산(장기 평균 분산)은 다음과 같이 계산한다.
sigma_long^2 = omega / (1 - alpha - beta)
실무에서 주식 수익률에 GARCH(1,1)을 적합하면 일반적으로 다음과 같은 결과를 얻는다.
| 모수 | 전형적 범위 | 해석 |
|---|---|---|
omega | 0.01 ~ 0.05 | 장기 분산의 기저 수준 |
alpha | 0.05 ~ 0.15 | 뉴스 충격이 변동성에 빠르게 반영되는 정도 |
beta | 0.80 ~ 0.95 | 변동성의 관성(persistence) |
alpha + beta | 0.95 ~ 0.99 | 1에 가까울수록 충격의 효과가 오래 지속 |
alpha가 크고 beta가 작으면 시장이 새 정보에 빠르게 반응하고 빠르게 잊는다. alpha가 작고 beta가 크면 변동성 수준이 한번 바뀌면 오래 유지된다.
Python 구현: GARCH(1,1) 적합
from arch import arch_model
import yfinance as yf
import pandas as pd
import numpy as np
# 데이터 준비
spy = yf.download('SPY', start='2018-01-01', end='2026-03-01', auto_adjust=True)
returns = spy['Close'].pct_change().dropna() * 100 # 백분율 수익률
# GARCH(1,1) with Student-t 분포
garch11 = arch_model(
returns,
vol='Garch',
p=1, # GARCH 차수 (과거 분산)
q=1, # ARCH 차수 (과거 충격)
mean='Constant',
dist='t' # Student-t 분포 (fat tails 포착)
)
result = garch11.fit(disp='off')
print(result.summary())
# 모수 추출
omega = result.params['omega']
alpha = result.params['alpha[1]']
beta = result.params['beta[1]']
persistence = alpha + beta
print(f"\nomega: {omega:.6f}")
print(f"alpha: {alpha:.4f}")
print(f"beta: {beta:.4f}")
print(f"persistence (alpha+beta): {persistence:.4f}")
# 장기 무조건 분산과 연율화 변동성
long_run_var = omega / (1 - alpha - beta)
annual_vol = np.sqrt(long_run_var * 252) / 100 # 소수점 비율로 변환
print(f"장기 연율화 변동성: {annual_vol:.2%}")
# 조건부 변동성 시계열
cond_vol = result.conditional_volatility
print(f"\n조건부 변동성 - 최근 5일:")
print(cond_vol.tail())
GJR-GARCH와 레버리지 효과
레버리지 효과란
주식 시장에서 동일한 크기의 충격이라도 하락 충격이 상승 충격보다 변동성을 더 크게 증가시킨다. Black(1976)이 처음 관찰한 이 현상을 레버리지 효과라 부른다. 주가가 하락하면 부채/자본 비율(leverage ratio)이 상승하여 기업의 위험이 증가한다는 것이 경제학적 설명이다.
일반 GARCH(1,1)은 충격의 부호와 무관하게 epsilon^2만 반영하므로 이 비대칭성을 포착하지 못한다.
GJR-GARCH 모델
Glosten, Jagannathan, Runkle(1993)이 제안한 GJR-GARCH는 지시 함수(indicator function)를 도입하여 비대칭성을 모델링한다.
sigma_t^2 = omega + (alpha + gamma * I_{t-1}) * epsilon_{t-1}^2 + beta * sigma_{t-1}^2
여기서 I_{t-1}은 epsilon_{t-1} < 0이면 1, 아니면 0인 지시 함수다.
- 양의 충격(상승): 분산에
alpha만큼 기여 - 음의 충격(하락): 분산에
alpha + gamma만큼 기여 gamma > 0이면 레버리지 효과가 존재
# GJR-GARCH(1,1,1) with Student-t
gjr = arch_model(
returns,
vol='Garch',
p=1, o=1, q=1, # o=1이 GJR의 비대칭 항
mean='Constant',
dist='t'
)
gjr_result = gjr.fit(disp='off')
print(gjr_result.summary())
gamma = gjr_result.params['gamma[1]']
alpha_gjr = gjr_result.params['alpha[1]']
print(f"\nalpha: {alpha_gjr:.4f}")
print(f"gamma: {gamma:.4f}")
print(f"양의 충격 영향: {alpha_gjr:.4f}")
print(f"음의 충격 영향: {alpha_gjr + gamma:.4f}")
print(f"비대칭 비율: {(alpha_gjr + gamma) / alpha_gjr:.2f}x")
gamma가 양수이고 통계적으로 유의미하면 레버리지 효과가 확인된 것이다. 실증 연구에서 주식 시장의 gamma는 대체로 0.05~0.15 범위에 위치한다.
EGARCH 모델
Nelson(1991)의 EGARCH
EGARCH(Exponential GARCH)는 로그 변동성을 모델링하여 두 가지 이점을 제공한다.
ln(sigma_t^2) = omega + alpha * (|z_{t-1}| - E|z_{t-1}|) + gamma * z_{t-1} + beta * ln(sigma_{t-1}^2)
여기서 z_{t-1} = epsilon_{t-1} / sigma_{t-1}은 표준화 잔차다.
EGARCH의 장점은 다음과 같다.
- 모수 제약 불필요: 로그를 취하므로 sigma_t^2는 자동으로 양수. alpha, beta에 비음수 제약을 걸 필요가 없다.
- 비대칭 효과의 직접 모델링: gamma 항이 z의 부호를 직접 반영. gamma가 음수이면 음의 충격이 변동성을 더 키운다.
- 승법적 구조: 로그 공간에서의 선형 모델이므로 충격 효과가 기존 변동성 수준에 비례한다.
# EGARCH(1,1) with Skewed Student-t
egarch = arch_model(
returns,
vol='EGARCH',
p=1, q=1,
mean='Constant',
dist='skewt' # 비대칭 Student-t 분포
)
egarch_result = egarch.fit(disp='off')
print(egarch_result.summary())
# EGARCH에서의 비대칭 파라미터 확인
# gamma < 0 이면 음의 충격이 변동성을 더 높임
gamma_egarch = egarch_result.params['gamma[1]']
print(f"\ngamma (비대칭): {gamma_egarch:.4f}")
if gamma_egarch < 0:
print("레버리지 효과 확인: 하락 충격이 변동성을 더 크게 증가시킴")
GARCH 변형 모델 비교
| 모델 | 비대칭 효과 | 모수 제약 | 장기 기억 | 실무 사용도 | 적합 상황 |
|---|---|---|---|---|---|
| GARCH(1,1) | 없음 | omega, alpha, beta 비음수 | 없음 | 매우 높음 | 기본 벤치마크, 대부분의 자산 |
| GJR-GARCH | 지시 함수 기반 | omega, alpha, beta, gamma 비음수 | 없음 | 높음 | 주식 시장 (레버리지 효과) |
| EGARCH | 연속적 비대칭 | 제약 없음 | 없음 | 높음 | 옵션 가격결정, 외환 시장 |
| TGARCH | 절대값 기반 | 비음수 제약 | 없음 | 중간 | GARCH(1,1) 대안 |
| IGARCH | 없음 | alpha+beta=1 | 단위근 | 중간 | 고빈도 데이터 |
| FIGARCH | 없음 | 추가 d 파라미터 | 분수 적분 | 낮음 | 장기 변동성 의존성 연구 |
Python 실전 구현
환경 설정
# 핵심 패키지 설치
pip install arch yfinance pandas numpy scipy matplotlib statsmodels
# 선택 패키지
pip install seaborn quantstats
변동성 모델링 Python 라이브러리 비교
| 라이브러리 | GARCH 지원 | 비대칭 모델 | 분포 선택 | 예측 | 장점 | 단점 |
|---|---|---|---|---|---|---|
| arch | GARCH, EGARCH, HARCH | GJR, EGARCH, APARCH | Normal, t, Skewt, GED | 다단계 예측 지원 | 가장 포괄적인 GARCH 구현 | API 학습 곡선 |
| statsmodels | 기본 GARCH | 제한적 | Normal | 기본 수준 | 통합 통계 패키지 | GARCH 기능 제한적 |
| rugarch (R) | 매우 광범위 | 전체 지원 | 10+ 분포 | 고급 예측 | 학술 표준 | R 환경 필요 |
| rmgarch (R) | DCC-GARCH | 다변량 전체 | 다양 | 상관관계 예측 | 다변량 GARCH | R 환경 필요 |
Python 환경에서는 arch 패키지가 사실상 표준이다. Kevin Sheppard가 개발하고 유지 관리하며, 학술 연구와 실무 양쪽에서 검증되었다.
전체 워크플로우: 데이터 수집부터 예측까지
import numpy as np
import pandas as pd
import yfinance as yf
from arch import arch_model
from arch.unitroot import ADF, KPSS
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
# --------------------------------------------------
# 1단계: 데이터 수집 및 전처리
# --------------------------------------------------
def prepare_returns(ticker: str, start: str, end: str) -> pd.Series:
"""수익률 데이터를 수집하고 전처리한다."""
data = yf.download(ticker, start=start, end=end, auto_adjust=True)
prices = data['Close']
log_returns = np.log(prices / prices.shift(1)).dropna() * 100
return log_returns
returns = prepare_returns('SPY', '2018-01-01', '2026-03-01')
# --------------------------------------------------
# 2단계: 사전 검정
# --------------------------------------------------
def pre_tests(returns: pd.Series) -> dict:
"""GARCH 모델 적용 전 필수 검정을 수행한다."""
results = {}
# ADF 검정: 수익률이 정상 과정인지 확인
adf = ADF(returns, lags=5)
results['ADF_stat'] = adf.stat
results['ADF_pvalue'] = adf.pvalue
results['is_stationary'] = adf.pvalue < 0.05
# ARCH 효과 검정 (Engle의 LM 검정)
from statsmodels.stats.diagnostic import het_arch
lm_stat, lm_pvalue, _, _ = het_arch(returns, nlags=10)
results['ARCH_LM_stat'] = lm_stat
results['ARCH_LM_pvalue'] = lm_pvalue
results['has_arch_effect'] = lm_pvalue < 0.05
return results
tests = pre_tests(returns)
for key, val in tests.items():
print(f"{key}: {val}")
# ADF p-value < 0.05 (정상) + ARCH LM p-value < 0.05 (ARCH 효과 존재)
# 두 조건 모두 충족해야 GARCH 적합이 의미 있음
# --------------------------------------------------
# 3단계: 복수 모델 적합
# --------------------------------------------------
def fit_models(returns: pd.Series) -> dict:
"""여러 GARCH 변형을 적합하고 결과를 반환한다."""
models = {}
# 1) GARCH(1,1) - Student-t
am = arch_model(returns, vol='Garch', p=1, q=1,
mean='Constant', dist='t')
models['GARCH(1,1)-t'] = am.fit(disp='off')
# 2) GJR-GARCH(1,1,1) - Student-t
am = arch_model(returns, vol='Garch', p=1, o=1, q=1,
mean='Constant', dist='t')
models['GJR-GARCH-t'] = am.fit(disp='off')
# 3) EGARCH(1,1) - Skewed Student-t
am = arch_model(returns, vol='EGARCH', p=1, q=1,
mean='Constant', dist='skewt')
models['EGARCH-skewt'] = am.fit(disp='off')
# 4) GARCH(1,1) - Normal (비교용)
am = arch_model(returns, vol='Garch', p=1, q=1,
mean='Constant', dist='normal')
models['GARCH(1,1)-N'] = am.fit(disp='off')
return models
models = fit_models(returns)
# --------------------------------------------------
# 4단계: 모델 비교 (정보 기준)
# --------------------------------------------------
comparison = pd.DataFrame({
name: {
'AIC': res.aic,
'BIC': res.bic,
'Log-Likelihood': res.loglikelihood,
'Params': res.num_params
}
for name, res in models.items()
}).T
comparison = comparison.sort_values('BIC')
print("\n모델 비교 (BIC 기준 정렬):")
print(comparison.to_string())
모델 진단과 선택
잔차 진단
적합된 GARCH 모델이 데이터를 잘 설명하는지 확인하려면, 표준화 잔차(standardized residuals)가 iid이고 선택한 분포를 따르는지 검정해야 한다.
def diagnose_model(result, model_name: str = ""):
"""GARCH 모델의 적합도를 진단한다."""
std_resid = result.std_resid
print(f"=== {model_name} 진단 ===")
# 1. 표준화 잔차의 기술 통계
print(f"평균: {std_resid.mean():.4f} (기대값: 0)")
print(f"표준편차: {std_resid.std():.4f} (기대값: 1)")
print(f"왜도: {std_resid.skew():.4f}")
print(f"첨도: {std_resid.kurtosis():.4f}")
# 2. Ljung-Box 검정: 잔차 자기상관
from statsmodels.stats.diagnostic import acorr_ljungbox
lb_resid = acorr_ljungbox(std_resid, lags=10, return_df=True)
lb_sq = acorr_ljungbox(std_resid**2, lags=10, return_df=True)
print(f"\nLjung-Box (잔차, lag=10) p-value: "
f"{lb_resid['lb_pvalue'].iloc[-1]:.4f}")
print(f"Ljung-Box (잔차^2, lag=10) p-value: "
f"{lb_sq['lb_pvalue'].iloc[-1]:.4f}")
# p-value > 0.05 이면 자기상관 없음 = 모델이 구조를 잘 포착함
# 3. ARCH-LM 검정: 잔여 ARCH 효과
from statsmodels.stats.diagnostic import het_arch
lm_stat, lm_pval, _, _ = het_arch(std_resid, nlags=10)
print(f"ARCH-LM (잔여 효과) p-value: {lm_pval:.4f}")
# p-value > 0.05 이면 잔여 ARCH 효과 없음 = 변동성 구조 충분히 포착
# 4. 정규성 vs t-분포 적합도
jb_stat, jb_pval = stats.jarque_bera(std_resid)
print(f"Jarque-Bera p-value: {jb_pval:.6f}")
print()
return {
'lb_resid_pval': lb_resid['lb_pvalue'].iloc[-1],
'lb_sq_pval': lb_sq['lb_pvalue'].iloc[-1],
'arch_lm_pval': lm_pval,
'jb_pval': jb_pval
}
# 최적 모델 진단
best_model_name = comparison.index[0] # BIC 최소 모델
best_result = models[best_model_name]
diag = diagnose_model(best_result, best_model_name)
모델 선택 기준 정리
| 기준 | 사용 시점 | 선호 방향 | 주의사항 |
|---|---|---|---|
| AIC | 예측 성능 중심 | 낮을수록 좋음 | 과적합 경향, 모수 페널티 약함 |
| BIC | 모델 간소성 중심 | 낮을수록 좋음 | 보수적, 단순 모델 선호 |
| Log-Likelihood | 모델 적합도 절대 수준 | 높을수록 좋음 | 단독 사용 불가 (모수 수 미반영) |
| Ljung-Box (잔차) | 평균 방정식 적합도 | p > 0.05 | 잔차에 남은 자기상관 없어야 함 |
| Ljung-Box (잔차^2) | 분산 방정식 적합도 | p > 0.05 | 잔여 ARCH 효과 없어야 함 |
| ARCH-LM | 분산 방정식 완전성 | p > 0.05 | Ljung-Box와 교차 확인 권장 |
| Out-of-Sample RMSE | 실전 예측력 | 낮을수록 좋음 | 가장 신뢰도 높은 비교 기준 |
실무적 권고: BIC로 후보를 2~3개 좁히고, out-of-sample 성능(rolling window 예측)으로 최종 결정한다. AIC와 BIC가 다른 모델을 지목할 경우, 예측이 목적이면 AIC, 해석이 목적이면 BIC를 따른다.
VaR, CVaR 계산
GARCH 기반 VaR
VaR(Value at Risk)는 특정 신뢰 수준에서 일정 기간 동안 발생할 수 있는 최대 손실 금액이다. GARCH 모델을 사용하면 시간 변동 VaR를 계산할 수 있다.
def calculate_garch_var(
returns: pd.Series,
model_result,
confidence_levels: list = [0.01, 0.05],
investment: float = 1_000_000
) -> pd.DataFrame:
"""
GARCH 모델 기반 VaR와 CVaR(ES)를 계산한다.
Parameters
----------
returns : pd.Series
수익률 시계열 (백분율)
model_result : arch result
적합된 GARCH 모델 결과
confidence_levels : list
VaR 신뢰 수준 (0.01 = 99%, 0.05 = 95%)
investment : float
투자 금액 (원 단위)
Returns
-------
pd.DataFrame
날짜별 VaR, CVaR 값
"""
cond_vol = model_result.conditional_volatility
mu = model_result.params.get('mu', 0)
# 모델에서 추정된 분포 파라미터 사용
dist = model_result.model.distribution
params = model_result.params
records = []
for alpha in confidence_levels:
# 분포의 분위수 (quantile)
# Student-t의 경우 자유도를 반영
if 'nu' in params.index:
nu = params['nu']
q = stats.t.ppf(alpha, df=nu)
else:
q = stats.norm.ppf(alpha)
# VaR = -(mu + sigma * quantile)
var_pct = -(mu + cond_vol * q) / 100
var_amount = var_pct * investment
# CVaR (Conditional VaR / Expected Shortfall)
# E[X | X < VaR] 계산
if 'nu' in params.index:
nu = params['nu']
# Student-t의 CVaR 공식
pdf_q = stats.t.pdf(q, df=nu)
cvar_factor = -(
(nu + q**2) / (nu - 1)
) * pdf_q / alpha
else:
pdf_q = stats.norm.pdf(q)
cvar_factor = -pdf_q / alpha
cvar_pct = -(mu + cond_vol * cvar_factor) / 100
cvar_amount = cvar_pct * investment
for date in cond_vol.index:
records.append({
'date': date,
'alpha': alpha,
'confidence': f"{(1-alpha)*100:.0f}%",
'VaR_pct': var_pct.loc[date],
'VaR_amount': var_amount.loc[date],
'CVaR_pct': cvar_pct.loc[date],
'CVaR_amount': cvar_amount.loc[date],
})
return pd.DataFrame(records)
# VaR 계산 실행
var_df = calculate_garch_var(returns, best_result)
# 최근 결과 확인
recent = var_df[var_df['alpha'] == 0.01].tail(5)
print("최근 5일 99% VaR (투자금 100만원 기준):")
print(recent[['date', 'confidence', 'VaR_pct', 'VaR_amount',
'CVaR_pct', 'CVaR_amount']].to_string(index=False))
VaR 해석 주의사항
99% 1일 VaR가 2.5%라는 것은 "내일 수익률이 -2.5% 이하로 떨어질 확률이 1%"라는 뜻이다. 100만 원 투자 기준 약 2.5만 원 이상의 손실이 발생할 확률이 1%이다. 그러나 VaR가 2.5%라고 해서 최대 손실이 2.5%라는 뜻이 아니다. VaR를 초과하는 경우의 평균 손실인 CVaR(Expected Shortfall)이 실제 테일 리스크를 더 정확하게 반영한다.
백테스팅과 검증
VaR 백테스팅: Kupiec 검정
VaR 모델의 정확성을 검증하는 가장 기본적인 방법은 실제 VaR 초과 빈도가 이론적 빈도와 일치하는지 검정하는 것이다. Kupiec(1995)의 POF(Proportion of Failures) 검정을 사용한다.
def backtest_var(
returns: pd.Series,
model_type: str = 'Garch',
p: int = 1,
o: int = 0,
q: int = 1,
dist: str = 't',
window: int = 1000,
alpha: float = 0.01
) -> dict:
"""
Rolling window 방식의 VaR 백테스팅을 수행한다.
Parameters
----------
returns : pd.Series
수익률 시계열 (백분율)
window : int
추정 윈도우 크기 (거래일)
alpha : float
VaR 신뢰 수준
Returns
-------
dict
백테스팅 결과 (초과 횟수, 비율, Kupiec 검정 등)
"""
violations = []
var_series = []
actual_series = []
n_forecasts = len(returns) - window
print(f"백테스팅 시작: {n_forecasts}일 예측")
for i in range(window, len(returns)):
train = returns.iloc[i-window:i]
actual = returns.iloc[i]
try:
am = arch_model(train, vol=model_type,
p=p, o=o, q=q,
mean='Constant', dist=dist)
res = am.fit(disp='off', show_warning=False)
# 1일 앞 예측
forecast = res.forecast(horizon=1)
mean_f = forecast.mean.iloc[-1, 0]
var_f = forecast.variance.iloc[-1, 0]
vol_f = np.sqrt(var_f)
# VaR 계산
if 'nu' in res.params.index:
nu = res.params['nu']
quantile = stats.t.ppf(alpha, df=nu)
else:
quantile = stats.norm.ppf(alpha)
var_value = -(mean_f + vol_f * quantile)
# 초과 여부 판단
is_violation = actual < -var_value
violations.append(is_violation)
var_series.append(var_value)
actual_series.append(actual)
except Exception:
violations.append(False)
var_series.append(np.nan)
actual_series.append(actual)
violations = np.array(violations)
n_violations = violations.sum()
violation_rate = n_violations / len(violations)
expected_rate = alpha
# Kupiec POF 검정 (우도비 검정)
n = len(violations)
x = n_violations
if x == 0 or x == n:
kupiec_stat = np.nan
kupiec_pval = np.nan
else:
lr = -2 * (
np.log((1 - alpha)**(n - x) * alpha**x)
- np.log((1 - x/n)**(n - x) * (x/n)**x)
)
kupiec_pval = 1 - stats.chi2.cdf(lr, df=1)
kupiec_stat = lr
result = {
'n_forecasts': n,
'n_violations': int(n_violations),
'violation_rate': violation_rate,
'expected_rate': expected_rate,
'kupiec_stat': kupiec_stat,
'kupiec_pval': kupiec_pval,
'model_accepted': kupiec_pval > 0.05 if not np.isnan(kupiec_pval) else None,
}
print(f"\n백테스팅 결과:")
print(f" 예상 초과율: {expected_rate:.2%}")
print(f" 실제 초과율: {violation_rate:.2%}")
print(f" 초과 횟수: {n_violations}/{n}")
print(f" Kupiec LR 통계량: {kupiec_stat:.4f}")
print(f" Kupiec p-value: {kupiec_pval:.4f}")
print(f" 모델 수용: {'Yes' if result['model_accepted'] else 'No'}")
return result
# 백테스팅 실행 (시간이 오래 걸릴 수 있음)
bt_result = backtest_var(
returns,
model_type='Garch',
p=1, o=1, q=1, # GJR-GARCH
dist='t',
window=1000,
alpha=0.01
)
Kupiec 검정의 p-value가 0.05 이상이면 모델의 VaR 예측이 통계적으로 정확하다고 판단한다. p-value가 0.05 미만이면 VaR 초과 빈도가 이론적 수준과 유의미하게 다르다는 뜻이므로 모델을 재검토해야 한다.
백테스팅 해석 가이드
| 상황 | 의미 | 대응 |
|---|---|---|
| 실제 초과율이 기대 초과율보다 높음 | VaR를 과소 추정 (위험 과소평가) | 더 보수적인 분포(t 자유도 낮추기) 또는 비대칭 모델 적용 |
| 실제 초과율이 기대 초과율보다 낮음 | VaR를 과대 추정 (자본 낭비) | 정규분포 또는 자유도 높은 t-분포 시도 |
| 초과가 군집되어 발생 | 조건부 모델이 변동성 체제 전환을 포착하지 못함 | 마르코프 체제 전환 모델 또는 윈도우 축소 |
| Kupiec 통과, Christoffersen 실패 | 전체 빈도는 맞지만 시간적 독립성 위반 | 모델 평균 방정식 또는 분산 방정식 재설정 |
트러블슈팅
자주 발생하는 문제와 해결 방법
문제 1: 최적화 수렴 실패 (Convergence Warning)
ConvergenceWarning: Maximum number of iterations has been exceeded.
원인은 대부분 초기값 설정 불량이거나 데이터 스케일 문제다.
해결 방법:
# 1. 수익률 스케일 확인 (0.01 단위가 아닌 백분율로)
returns_pct = returns * 100 # 소수점 -> 백분율
# 2. 다른 최적화 방법 시도
result = model.fit(
disp='off',
options={'maxiter': 500},
starting_values=np.array([0.01, 0.05, 0.90]) # omega, alpha, beta
)
문제 2: 분산이 음수로 추정됨
GARCH(1,1)에서는 모수 제약으로 인해 드물지만, 비표준 모델에서 발생할 수 있다. EGARCH로 전환하면 로그 분산을 모델링하므로 이 문제가 자동으로 해결된다.
문제 3: alpha + beta가 1 이상
비정상(non-stationary) GARCH는 장기 무조건 분산이 정의되지 않는다. 원인은 구조적 변동(structural break)이 데이터에 포함된 경우가 대부분이다.
해결 방법:
# 추정 기간 축소 (구조적 변동 이후 데이터만 사용)
# 예: COVID-19 이후 데이터만 사용
returns_post_covid = returns['2020-07-01':]
am = arch_model(returns_post_covid, vol='Garch', p=1, q=1,
mean='Constant', dist='t')
result = am.fit(disp='off')
persistence = result.params['alpha[1]'] + result.params['beta[1]']
print(f"Persistence: {persistence:.4f}")
문제 4: 예측 변동성이 일정한 값으로 수렴
GARCH 다기간 예측은 장기 무조건 분산으로 빠르게 수렴하는 것이 정상 동작이다. 수렴 속도는 alpha + beta에 의해 결정된다. 장기 예측이 필요하면 GARCH보다 지수 가중 이동 평균(EWMA)이나 내재 변동성 기반 접근을 고려한다.
문제 5: Student-t 자유도 추정이 불안정
# 자유도를 고정하여 추정
am = arch_model(returns, vol='Garch', p=1, q=1,
mean='Constant', dist='t')
# 자유도 고정 (예: nu=5)
constraints = {'nu': 5.0}
result = am.fit(disp='off')
# 또는 GED 분포를 대안으로 사용
am_ged = arch_model(returns, vol='Garch', p=1, q=1,
mean='Constant', dist='ged')
result_ged = am_ged.fit(disp='off')
운영 체크리스트
모델 구축 단계
- 수익률 데이터가 정상 과정인지 확인 (ADF 검정 p-value 0.05 미만)
- ARCH 효과가 존재하는지 확인 (Engle LM 검정 p-value 0.05 미만)
- 수익률 분포의 첨도와 왜도 확인 (정규분포 적합성 판단)
- 분포 선택: 첨도가 높으면 Student-t, 왜도도 있으면 Skewed-t
- 모델 후보 2~3개 적합 (GARCH, GJR-GARCH, EGARCH)
- 정보 기준(AIC/BIC)으로 후보 정렬
- 최적 모델의 잔차 진단 수행 (Ljung-Box, ARCH-LM)
예측 및 적용 단계
- Out-of-sample 예측력 검증 (rolling window)
- VaR 백테스팅 수행 (Kupiec 검정 통과 여부)
- 예측 변동성의 연율화 단위 변환 확인
- VaR 금액 계산 시 포지션 크기와 보유 기간 반영
- CVaR(Expected Shortfall)도 함께 보고
운영 및 모니터링 단계
- 모델 재추정 주기 결정 (주간/월간)
- 데이터 파이프라인의 결측치, 이상치 처리 로직 확인
- 극단적 시장 상황에서의 모델 행동 시뮬레이션
- 구조적 변동(structural break) 감지 로직 구현
- VaR 초과 발생 시 알림 체계 구축
- 분기별 모델 성능 리뷰 보고서 생성
실패 사례와 교훈
사례 1: 정규분포 가정으로 인한 VaR 과소 추정
한 리스크 관리 팀이 GARCH(1,1)-Normal로 99% VaR를 계산했다. 백테스팅 결과, 1%여야 할 VaR 초과율이 3.2%로 나왔다. 원인은 정규분포의 꼬리가 실제 수익률 분포보다 얇기 때문이었다.
교훈: 금융 수익률에 GARCH를 적합할 때 정규분포를 기본으로 쓰지 말 것. Student-t 분포를 기본값으로 하고, BIC로 정규분포와 비교하여 선택한다. 실증적으로 거의 항상 Student-t가 우세하다.
# 나쁜 예: 정규분포 가정
bad_model = arch_model(returns, vol='Garch', p=1, q=1,
mean='Constant', dist='normal')
# 좋은 예: Student-t 분포 기본
good_model = arch_model(returns, vol='Garch', p=1, q=1,
mean='Constant', dist='t')
사례 2: 구조적 변동을 무시한 장기 데이터 사용
2008년 금융위기부터 2024년까지의 16년 데이터로 GARCH를 적합했더니 alpha + beta가 0.999에 달했다. 이는 IGARCH(분산에 단위근이 있는 모델)에 가까운 결과로, 변동성 예측이 사실상 "현재 수준이 영원히 지속된다"가 되어 무의미했다.
교훈: 구조적 변동(금융위기, 팬데믹 등)이 포함된 지나치게 긴 데이터는 모수 추정을 왜곡한다. 3~5년 윈도우를 기본으로 하되, 구조적 변동 이후 데이터만 사용하는 것이 실무적으로 안전하다. Bai-Perron 또는 CUSUM 검정으로 구조적 변동 시점을 감지할 수 있다.
사례 3: 변동성 예측과 수익률 예측의 혼동
퀀트 트레이더가 GARCH의 조건부 분산이 높아질 때 매도, 낮아질 때 매수하는 전략을 운용했다. 결과는 시장 대비 지속적인 언더퍼포먼스였다.
교훈: GARCH는 변동성(리스크)을 예측하는 모델이지, 수익률(방향)을 예측하는 모델이 아니다. 높은 변동성은 큰 상승의 가능성도 동시에 의미한다. GARCH 출력을 포지션 사이징(변동성이 높을 때 포지션 축소)이나 리스크 관리에 사용하는 것이 올바른 적용 방법이다.
사례 4: 일중(Intraday) 데이터에 일간 GARCH 적용
5분봉 데이터에 표준 GARCH(1,1)을 적용했더니 일중 패턴(장 시작 후 변동성 급등, 점심 시간 하락, 장 마감 전 상승)이 잔차에 고스란히 남았다. Ljung-Box 검정이 모두 기각되어 모델이 무의미했다.
교훈: 일중 데이터에는 일중 주기성(intraday periodicity)을 먼저 제거해야 한다. Andersen과 Bollerslev(1997)의 FFF(Flexible Fourier Form) 필터로 일중 패턴을 제거한 후 GARCH를 적용하거나, 실현 변동성(Realized Volatility)을 직접 모델링하는 HAR-RV 모델을 사용한다.
사례 5: 다변량 포트폴리오에 단변량 GARCH 각각 적용
5개 자산 포트폴리오의 리스크를 측정할 때, 각 자산에 개별 GARCH를 적합하고 고정 상관관계로 포트폴리오 분산을 계산했다. 2020년 COVID 폭락 시 실제 손실이 VaR의 4배를 초과했다.
교훈: 위기 시 자산 간 상관관계는 급격히 상승한다. 단변량 GARCH의 합으로는 이 동적 상관관계를 포착할 수 없다. DCC-GARCH(Dynamic Conditional Correlation)나 CCC-GARCH를 사용하여 상관관계의 시간 변동성을 함께 모델링해야 한다. Python에서는 arch 패키지에 아직 다변량 모델 지원이 제한적이므로, R의 rmgarch 패키지를 병행하거나 직접 구현해야 한다.
고급 주제: 변동성 예측의 다음 단계
실현 변동성과 HAR-RV 모델
GARCH가 수익률의 모수적 구조를 가정하는 반면, 실현 변동성(Realized Volatility)은 일중 고빈도 데이터로부터 비모수적으로 변동성을 직접 측정한다. Corsi(2009)의 HAR-RV 모델은 일간, 주간, 월간 실현 변동성의 자기회귀 구조를 결합하여 뛰어난 예측력을 보인다.
머신러닝과의 결합
최근 연구에서는 GARCH의 조건부 분산을 LSTM, Transformer 등 딥러닝 모델의 피처로 활용하는 하이브리드 접근이 주목받고 있다. GARCH가 선형 구조에서 포착하는 정보와 신경망이 비선형 패턴에서 포착하는 정보를 결합하면, 단독 사용보다 예측 성능이 향상될 수 있다. 다만 금융 데이터의 낮은 신호 대 잡음비(SNR) 때문에 과적합 위험이 높으므로 엄격한 교차 검증이 필수다.
체제 전환 GARCH (Markov-Switching GARCH)
변동성이 고변동성/저변동성 두 가지(또는 그 이상) 체제 사이를 전환한다고 가정하는 모델이다. 2008년 금융위기, 2020년 COVID와 같은 극단적 시장 국면 전환을 포착하는 데 유리하다. 그러나 모수 추정이 복잡하고 체제 수 선택에 임의성이 있다는 단점이 있다.
참고자료
arch 패키지 공식 문서 - Python GARCH 모델링의 표준 라이브러리. API 레퍼런스와 예제 코드가 충실하다. https://arch.readthedocs.io/en/latest/
Machine Learning Mastery - ARCH and GARCH Models - ARCH/GARCH의 개념과 Python 구현을 단계별로 설명하는 입문 가이드. https://machinelearningmastery.com/develop-arch-and-garch-models-for-time-series-forecasting-in-python/
QuantInsti - GARCH and GJR-GARCH Volatility Forecasting - GJR-GARCH를 활용한 변동성 예측과 트레이딩 적용을 다룬 실무 가이드. https://blog.quantinsti.com/garch-gjr-garch-volatility-forecasting-python/
Forecastegy - Volatility Forecasting in Python - 다양한 변동성 예측 방법론의 비교와 Python 구현. https://forecastegy.com/posts/volatility-forecasting-in-python/
DataCamp - GARCH Models in Python - GARCH 모델의 체계적인 학습 과정. 개념부터 실전 적용까지 커리큘럼이 잘 구성되어 있다. https://www.datacamp.com/courses/garch-models-in-python
Bollerslev, T. (1986) - "Generalized Autoregressive Conditional Heteroskedasticity." Journal of Econometrics, 31(3), 307-327. GARCH 모델의 원 논문.
Engle, R.F. (1982) - "Autoregressive Conditional Heteroscedasticity with Estimates of the Variance of United Kingdom Inflation." Econometrica, 50(4), 987-1007. ARCH 모델의 원 논문이자 노벨상 수상 연구.
Hansen, P.R. and Lunde, A. (2005) - "A Forecast Comparison of Volatility Models: Does Anything Beat a GARCH(1,1)?" Journal of Applied Econometrics. 수백 개의 변동성 모델을 비교한 결과, GARCH(1,1)이 놀라울 만큼 강건하다는 결론을 내린 연구.