- Authors
- Name
- はじめに
- ボラティリティの定義とスタイライズドファクト
- ARCHモデルの基礎
- GARCH(1,1)の詳細
- GJR-GARCHとレバレッジ効果
- EGARCHモデル
- Python実践実装
- モデル診断と選択
- VaRとCVaRの計算
- バックテストと検証
- トラブルシューティング
- 運用チェックリスト
- 失敗事例と教訓
- 発展的トピック:ボラティリティ予測の次のステップ
- 参考文献
- クイズ

はじめに
金融市場におけるリターンの予測は極めて困難です。しかし、ボラティリティの予測は比較的実現可能です。この違いこそがリスク管理の出発点です。明日の株価が上がるか下がるかはわかりませんが、今日の市場状況から明日の価格変動の大きさは合理的に推測できます。
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)の計算、バックテストまで、実務で必要なワークフロー全体をカバーしています。数学的背景が十分でない開発者でも、順を追って理解し実データに適用できる構成になっています。
ボラティリティの定義とスタイライズドファクト
ボラティリティとは何か
ボラティリティとは、資産リターンの分散または標準偏差を指します。実務では大きく3つに分類されます。
- ヒストリカルボラティリティ:過去のリターンデータから算出した標準偏差。最もシンプルだが後ろ向きの指標。
- インプライドボラティリティ:オプション価格から逆算した市場の予想ボラティリティ。VIX指数が代表的。
- 条件付きボラティリティ:過去の情報を条件として将来のボラティリティを予測した期待値。まさにGARCHモデルが推定する対象。
年率換算ボラティリティは、日次ボラティリティに営業日数の平方根を掛けて算出します。韓国市場の年間営業日数約250日を適用すると、日次標準偏差1.2%は年率換算で約19%のボラティリティに相当します。
金融リターンのスタイライズドファクト
実際の金融データには、正規分布と一定の分散を仮定する単純な統計モデルでは説明できない特徴があります。これらはスタイライズドファクトと呼ばれ、GARCHモデルはまさにこれらの特徴を捉えるために設計されています。
| スタイライズドファクト | 説明 | GARCHとの関連性 |
|---|---|---|
| ボラティリティクラスタリング | 大きな変動の後には大きな変動が、小さな変動の後には小さな変動が続く傾向 | GARCHの中核的な仮定およびモデリング対象 |
| ファットテール | リターン分布の裾が正規分布より厚い(尖度が3超) | Student-t分布、GED分布などの非正規分布で捕捉 |
| レバレッジ効果 | 負のショックは正のショックよりもボラティリティを大きく増加させる | EGARCH、GJR-GARCHによる非対称性のモデリング |
| 平均回帰性 | ボラティリティは長期的な平均水準に戻る傾向がある | GARCHパラメータの定常性条件に反映 |
| 長期記憶性 | ボラティリティの自己相関の減衰が非常に遅い | 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 # パーセントリターン
# スタイライズドファクトの検証
print(f"Mean: {returns.mean():.4f}%")
print(f"Std Dev: {returns.std():.4f}%")
print(f"Skewness: {returns.skew():.4f}")
print(f"Kurtosis: {returns.kurtosis():.4f}") # 超過尖度(正規分布=0)
# Jarque-Bera正規性検定
jb_stat, jb_pval = stats.jarque_bera(returns)
print(f"Jarque-Bera statistic: {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"\nReturns Ljung-Box p-value (lag=10): {lb_returns['lb_pvalue'].iloc[-1]:.4f}")
print(f"Returns^2 Ljung-Box p-value (lag=10): {lb_squared['lb_pvalue'].iloc[-1]:.6f}")
# 二乗リターンのp値が非常に小さければボラティリティクラスタリングの存在を示す
上記のコードを実行すると、通常は尖度が3を大きく超え(ファットテール)、二乗リターンの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)の3つのパラメータそれぞれの意味:
omega:長期分散への寄与。値が大きいほど分散の下限が高くなる。alpha:ニュースショック係数。昨日の市場ショックが今日のボラティリティに与える影響の大きさ。beta:分散持続係数。昨日のボラティリティが今日にどれだけ引き継がれるか。
定常性条件とパラメータ解釈
GARCH(1,1)が定常であるためには、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 | ボラティリティの持続性 |
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 # パーセントリターン
# Student-t分布によるGARCH(1,1)
garch11 = arch_model(
returns,
vol='Garch',
p=1, # GARCH次数(過去の分散)
q=1, # ARCH次数(過去のショック)
mean='Constant',
dist='t' # Student-t分布(ファットテールを捕捉)
)
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"Long-run annualized volatility: {annual_vol:.2%}")
# 条件付きボラティリティ時系列
cond_vol = result.conditional_volatility
print(f"\nConditional volatility - last 5 days:")
print(cond_vol.tail())
GJR-GARCHとレバレッジ効果
レバレッジ効果とは何か
株式市場では、同じ大きさのショックであっても、負のショックの方が正のショックよりもボラティリティを大きく増加させます。この現象はBlack(1976年)が最初に発見し、レバレッジ効果と呼ばれています。経済学的な説明としては、株価が下落すると負債比率(レバレッジ比率)が上昇し、企業のリスクが増大するためです。
標準的なGARCH(1,1)はショックの符号に関係なくepsilon^2のみを反映するため、この非対称性を捕捉できません。
GJR-GARCHモデル
Glosten、Jagannathan、Runkle(1993年)が提唱したGJR-GARCHは、指示関数を導入して非対称性をモデル化します:
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はレバレッジ効果の存在を示す
# Student-t分布によるGJR-GARCH(1,1,1)
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"Positive shock impact: {alpha_gjr:.4f}")
print(f"Negative shock impact: {alpha_gjr + gamma:.4f}")
print(f"Asymmetry ratio: {(alpha_gjr + gamma) / alpha_gjr:.2f}x")
gammaが正で統計的に有意であれば、レバレッジ効果が確認されます。実証研究では、株式市場のgammaは一般的に0.05~0.15の範囲に収まります。
EGARCHモデル
NelsonのEGARCH(1991年)
EGARCH(Exponential GARCH)は対数ボラティリティをモデル化し、2つの利点を提供します:
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が負であれば負のショックがボラティリティをより増加させる。
- 乗法的構造:対数空間の線形モデルであるため、ショック効果は既存のボラティリティ水準に比例する。
# 歪みStudent-t分布によるEGARCH(1,1)
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 (asymmetry): {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 | 正規、t、Skewt、GED | 多段階予測対応 | 最も包括的なGARCH実装 | APIの学習コスト |
| statsmodels | 基本的なGARCH | 限定的 | 正規分布のみ | 基本レベル | 統合統計パッケージ | 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値が0.05未満(定常)+ ARCH LM p値が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) - 歪み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) - 正規分布(比較用)
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("\nModel comparison (sorted by BIC):")
print(comparison.to_string())
モデル診断と選択
残差診断
フィッティングしたGARCHモデルがデータを十分に説明しているかを検証するには、標準化残差がiidであり、選択した分布に従うかどうかを検定する必要があります。
def diagnose_model(result, model_name: str = ""):
"""GARCHモデルのフィット品質を診断"""
std_resid = result.std_resid
print(f"=== {model_name} Diagnostics ===")
# 1. 標準化残差の記述統計
print(f"Mean: {std_resid.mean():.4f} (expected: 0)")
print(f"Std Dev: {std_resid.std():.4f} (expected: 1)")
print(f"Skewness: {std_resid.skew():.4f}")
print(f"Kurtosis: {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 (residuals, lag=10) p-value: "
f"{lb_resid['lb_pvalue'].iloc[-1]:.4f}")
print(f"Ljung-Box (residuals^2, lag=10) p-value: "
f"{lb_sq['lb_pvalue'].iloc[-1]:.4f}")
# p値 > 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 (residual effect) p-value: {lm_pval:.4f}")
# p値 > 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 | モデルの簡潔さ重視 | 低いほど良い | 保守的、シンプルなモデルを選好 |
| 対数尤度 | 絶対的なフィット水準 | 高いほど良い | 単独では使用不可(パラメータ数が反映されない) |
| Ljung-Box(残差) | 平均方程式のフィット | p > 0.05 | 残差に自己相関が残ってはならない |
| Ljung-Box(二乗残差) | 分散方程式のフィット | p > 0.05 | 残差にARCH効果が残ってはならない |
| ARCH-LM | 分散方程式の完全性 | p > 0.05 | Ljung-Boxとのクロスチェック推奨 |
| アウトオブサンプルRMSE | 実務的予測力 | 低いほど良い | 最も信頼性の高い比較基準 |
実務的な推奨:BICで候補を2~3つに絞り、アウトオブサンプル性能(ローリングウィンドウ予測)で最終決定します。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)の計算
"""
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:
if 'nu' in params.index:
nu = params['nu']
q = stats.t.ppf(alpha, df=nu)
else:
q = stats.norm.ppf(alpha)
var_pct = -(mu + cond_vol * q) / 100
var_amount = var_pct * investment
if 'nu' in params.index:
nu = params['nu']
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_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(期待ショートフォール)の方が、実際のテールリスクをより正確に反映します。
バックテストと検証
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:
"""
ローリングウィンドウVaRバックテストの実行
"""
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)
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)
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
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値: {kupiec_pval:.4f}")
print(f" モデル採択: {'はい' if result['model_accepted'] else 'いいえ'}")
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値が0.05以上であれば、モデルのVaR予測は統計的に正確と判定されます。p値が0.05未満の場合、VaR超過頻度が理論的水準と有意に異なるため、モデルの見直しが必要です。
バックテスト解釈ガイド
| 状況 | 意味 | 対応策 |
|---|---|---|
| 実際の超過率が期待より高い | VaRの過小推定(リスク過小評価) | より保守的な分布(t分布の自由度を下げる)または非対称モデル |
| 実際の超過率が期待より低い | VaRの過大推定(資本の無駄遣い) | 正規分布またはt分布の自由度を上げる |
| 超過がクラスター状に発生 | 条件付きモデルがボラティリティレジーム変化を捕捉できていない | マルコフレジームスイッチングモデルまたはウィンドウの縮小 |
| Kupiec合格、Christoffersen不合格 | 全体頻度は正しいが時間的独立性が違反 | 平均方程式または分散方程式の再構成 |
トラブルシューティング
よくある問題と解決策
問題1:最適化の収束失敗
ConvergenceWarning: Maximum number of iterations has been exceeded.
原因は通常、初期値の不良またはデータのスケール問題です。
解決策:
# 1. リターンのスケールを確認(パーセンテージであるべき、小数ではない)
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以上
非定常GARCHは長期非条件付き分散が未定義であることを意味します。原因は通常、データに構造変化が含まれていることです。
解決策:
# 推定期間を短縮(構造変化後のデータのみ使用)
# 例:COVID後のデータのみ使用
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値が0.05未満)
- ARCH効果の存在を確認(Engle LM検定p値が0.05未満)
- リターン分布の尖度と歪度を確認(正規性の評価)
- 分布の選択:高尖度ならStudent-t、歪みがあればSkewed-t
- 2~3つのモデル候補をフィッティング(GARCH、GJR-GARCH、EGARCH)
- 情報量基準(AIC/BIC)で候補をソート
- 最適モデルの残差診断を実施(Ljung-Box、ARCH-LM)
予測・応用フェーズ
- アウトオブサンプル予測力の検証(ローリングウィンドウ)
- VaRバックテスト実施(Kupiec検定の合否)
- 予測ボラティリティの年率換算単位変換を確認
- ポジションサイズと保有期間をVaR金額算出に反映
- CVaR(期待ショートフォール)を併記
運用・モニタリングフェーズ
- モデル再推定頻度の決定(週次/月次)
- データパイプラインの欠損値・外れ値処理ロジックの確認
- 極端な市場環境下でのモデル挙動のシミュレーション
- 構造変化検出ロジックの実装
- VaR超過イベントのアラートシステム構築
- 四半期ごとのモデルパフォーマンスレビューレポートの作成
失敗事例と教訓
事例1:正規分布仮定によるVaR過小推定
あるリスク管理チームがGARCH(1,1)-正規分布で99% VaRを算出しました。バックテストの結果、VaR超過率は期待される1%ではなく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:日次GARCHをイントラデイデータに適用
標準的なGARCH(1,1)を5分足データに適用しましたが、日中パターン(寄り付き後のボラティリティ急上昇、昼休みの低下、引け前の上昇)が残差に完全に残りました。すべてのLjung-Box検定が棄却され、モデルは無意味でした。
教訓:イントラデイデータからはまずイントラデイの周期性を除去する必要があります。Andersen and Bollerslev(1997年)のFFF(Flexible Fourier Form)フィルターでイントラデイパターンを除去してからGARCHを適用するか、実現ボラティリティを直接モデル化するHAR-RVモデルを使用してください。
事例5:多変量ポートフォリオに単変量GARCHを個別適用
5資産ポートフォリオのリスク測定で、各資産に個別にGARCHをフィッティングし、固定相関でポートフォリオ分散を算出しました。2020年3月のCOVIDショック時、実際の損失がVaRの4倍を超えました。
教訓:危機時には資産間の相関が急激に上昇します。単変量GARCHモデルの合計ではこの動的相関を捕捉できません。DCC-GARCH(Dynamic Conditional Correlation)またはCCC-GARCHを使用して時変相関を一緒にモデル化する必要があります。Python環境ではarchパッケージの多変量モデルサポートはまだ限定的であるため、Rのrmgarchパッケージを並行使用するか、独自実装が必要です。
発展的トピック:ボラティリティ予測の次のステップ
実現ボラティリティとHAR-RVモデル
GARCHがリターンに対してパラメトリックな構造を仮定するのに対し、実現ボラティリティはイントラデイの高頻度データからボラティリティをノンパラメトリックに測定します。Corsi(2009年)のHAR-RVモデルは、日次・週次・月次の実現ボラティリティの自己回帰構造を組み合わせ、優れた予測力を発揮しています。
機械学習との融合
近年の研究では、GARCHの条件付き分散をLSTMやTransformerなどの深層学習モデルの特徴量として使用するハイブリッドアプローチに注目が集まっています。GARCHが線形構造から捕捉する情報と、ニューラルネットワークが捕捉する非線形パターンを組み合わせることで、単独使用よりも予測性能を向上させることができます。ただし、金融データは信号対雑音比(SNR)が低いため、過学習のリスクが高く、厳密なクロスバリデーションが不可欠です。
レジームスイッチングGARCH(マルコフスイッチングGARCH)
このモデルはボラティリティが高ボラティリティ・低ボラティリティの2つ(またはそれ以上)のレジーム間で切り替わると仮定します。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)が驚くほどロバストであると結論した研究。
クイズ
Q1: 「時系列ボラティリティ予測実践ガイド:GARCHモデルファミリーとPython実装」の主なトピックは何ですか?
ARCH/GARCHからEGARCH、GJR-GARCH、DCC-GARCHに至るボラティリティモデルの数学的原理、Python archパッケージによる実装、モデル選択と診断、バックテスト、そして実務リスク管理への応用までを体系的に解説します。
Q2: ボラティリティの定義とスタイライズドファクトとは何ですか?
ボラティリティとは何か ボラティリティとは、資産リターンの分散または標準偏差を指します。実務では大きく3つに分類されます。 ヒストリカルボラティリティ:過去のリターンデータから算出した標準偏差。最もシンプルだが後ろ向きの指標。 インプライドボラティリティ:オプション価格から逆算した市場の予想ボラティリティ。VIX指数が代表的。 条件付きボラティリティ:過去の情報を条件として将来のボラティリティを予測した期待値。まさにGARCHモデルが推定する対象。 年率換算ボラティリティは、日次ボラティリティに営業日数の平方根を掛けて算出します。
Q3: ARCHモデルの基礎の核心的な概念を説明してください。
EngleのARCH(q)モデル ARCH(AutoRegressive Conditional Heteroskedasticity)モデルは、条件付き分散が過去の二乗誤差項に依存すると仮定するモデルです。 リターン過程は次のように定義されます: ここで条件付き分散sigma_t^2は: omega > 0:長期分散の基底水準 alpha_i >= 0:過去のショックの影響係数 q:ARCH次数(何期前までのショックを反映するか) ARCH(1)の場合、昨日大きなショック(正負問わず)が発生すれば、今日の条件付き分散が増加します。
Q4: GARCH(1,1)の詳細の主な特徴は何ですか?
モデル構造 Bollerslev(1986年)が提唱したGARCH(p,q)は、条件付き分散方程式に過去の分散自体を追加したモデルです: GARCH(1,1)の3つのパラメータそれぞれの意味: omega:長期分散への寄与。値が大きいほど分散の下限が高くなる。 alpha:ニュースショック係数。昨日の市場ショックが今日のボラティリティに与える影響の大きさ。 beta:分散持続係数。昨日のボラティリティが今日にどれだけ引き継がれるか。
Q5: GJR-GARCHとレバレッジ効果はどのように機能しますか?
レバレッジ効果とは何か 株式市場では、同じ大きさのショックであっても、負のショックの方が正のショックよりもボラティリティを大きく増加させます。この現象はBlack(1976年)が最初に発見し、レバレッジ効果と呼ばれています。経済学的な説明としては、株価が下落すると負債比率(レバレッジ比率)が上昇し、企業のリスクが増大するためです。 標準的なGARCH(1,1)はショックの符号に関係なくepsilon^2のみを反映するため、この非対称性を捕捉できません。