Skip to content
Published on

時系列ボラティリティ予測実践ガイド:GARCHモデルファミリーとPython実装

Authors
  • Name
    Twitter
GARCHボラティリティ予測

はじめに

金融市場におけるリターンの予測は極めて困難です。しかし、ボラティリティの予測は比較的実現可能です。この違いこそがリスク管理の出発点です。明日の株価が上がるか下がるかはわかりませんが、今日の市場状況から明日の価格変動の大きさは合理的に推測できます。

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)をフィッティングすると、通常次のような結果が得られます:

パラメータ典型的な範囲解釈
omega0.01 - 0.05長期分散の基底水準
alpha0.05 - 0.15ニュースショックのボラティリティへの反映速度
beta0.80 - 0.95ボラティリティの持続性
alpha + beta0.95 - 0.991に近いほどショックの効果が長く持続

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の利点:

  1. パラメータ制約が不要:対数を取ることでsigma_t^2は自動的に正になる。alphaとbetaに非負制約が不要。
  2. 非対称効果の直接モデリング:gamma項がzの符号を直接反映。gammaが負であれば負のショックがボラティリティをより増加させる。
  3. 乗法的構造:対数空間の線形モデルであるため、ショック効果は既存のボラティリティ水準に比例する。
# 歪み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サポート非対称モデル分布オプション予測機能強み弱み
archGARCH、EGARCH、HARCHGJR、EGARCH、APARCH正規、t、Skewt、GED多段階予測対応最も包括的なGARCH実装APIの学習コスト
statsmodels基本的なGARCH限定的正規分布のみ基本レベル統合統計パッケージGARCH機能が限定的
rugarch (R)非常に広範完全サポート10以上の分布高度な予測機能学術的標準R環境が必要
rmgarch (R)DCC-GARCH完全な多変量各種相関予測多変量GARCHR環境が必要

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.05Ljung-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のような極端な市場レジーム転換を捕捉するのに有利です。ただし、パラメータ推定の複雑さとレジーム数の選択の恣意性という欠点があります。

参考文献

  1. archパッケージ公式ドキュメント - PythonでのGARCHモデリングの標準ライブラリ。APIリファレンスとサンプルコードが充実。 https://arch.readthedocs.io/en/latest/

  2. Machine Learning Mastery - ARCH and GARCH Models - ARCH/GARCHの概念とPython実装をステップバイステップで解説する入門ガイド。 https://machinelearningmastery.com/develop-arch-and-garch-models-for-time-series-forecasting-in-python/

  3. QuantInsti - GARCH and GJR-GARCH Volatility Forecasting - GJR-GARCHを用いたボラティリティ予測とトレーディング応用の実践ガイド。 https://blog.quantinsti.com/garch-gjr-garch-volatility-forecasting-python/

  4. Forecastegy - Volatility Forecasting in Python - 各種ボラティリティ予測手法の比較とPython実装。 https://forecastegy.com/posts/volatility-forecasting-in-python/

  5. DataCamp - GARCH Models in Python - GARCHモデルの体系的学習コース。概念から実践応用まで構造化されたカリキュラム。 https://www.datacamp.com/courses/garch-models-in-python

  6. Bollerslev, T. (1986) - "Generalized Autoregressive Conditional Heteroskedasticity." Journal of Econometrics, 31(3), 307-327. GARCHの原論文。

  7. Engle, R.F. (1982) - "Autoregressive Conditional Heteroscedasticity with Estimates of the Variance of United Kingdom Inflation." Econometrica, 50(4), 987-1007. ARCHの原論文でありノーベル賞受賞研究。

  8. 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のみを反映するため、この非対称性を捕捉できません。