Skip to content
Published on

ポートフォリオリスク管理:Pythonで実装するVaRとモンテカルロシミュレーション実践ガイド

Authors
  • Name
    Twitter
Portfolio VaR Monte Carlo

1. はじめに:リスクを数値で語る方法

「このポートフォリオのリスクはどのくらいですか?」この質問に対して「やや危険です」や「ボラティリティが高いです」と答えても、実務では意味がない。リスクマネージャー、規制当局、投資家が求めるのは具体的な数値だ。「95%信頼水準で1日の最大損失は2億3千万ウォンです。」これがValue at Risk(VaR)が存在する理由である。

VaRは1990年代初頭にJP MorganのRiskMetricsシステムで大衆化されて以来、バーゼル規制体系(Basel II/III/IV)の中核指標として定着した。しかし、2008年の金融危機でVaRの限界が如実に露呈し、単にVaRの数値一つだけを見るのではなく、CVaR(Expected Shortfall)、ストレステスティング、バックテスティングを併せて実施する統合リスクフレームワークが必須となった。

本稿では、VaRの3つの主要計算方法論であるパラメトリック(分散共分散)、ヒストリカルシミュレーション、モンテカルロシミュレーションのそれぞれをPythonコードで実装し、バックテスティングでモデルを検証し、プロダクション環境でリスクシステムを運用する際に必ず知っておくべき注意事項と失敗事例を詳細に扱う。

対象読者はPythonの基本文法とpandas/numpyに慣れた開発者またはジュニアクオンツである。金融ドメイン知識は可能な限り最初から説明するので、背景知識がなくても追いつける。

2. VaRの基本概念と3つの方法論

VaRとは何か

VaR(Value at Risk)は、所与の信頼水準(confidence level)と保有期間(holding period)の下で、ポートフォリオが被りうる最大予想損失額を意味する。数学的に表現すると以下の通りである。

VaR_α = -inf{x : P(L > x) <= 1 - α}

ここでαは信頼水準(通常95%または99%)、Lは損失分布、xは損失金額である。直感的に言えば、「95% VaR = 1億円」とは「今後1日間で95%の確率で損失が1億円を超えないであろう」ことを意味する。逆に言えば、5%の確率で1億円以上の損失が生じる可能性があるということでもある。

VaRの3つの主要パラメータ

  • 信頼水準(Confidence Level):95%、99%、99.5%など。バーゼル規制では99%を基本とする。
  • 保有期間(Holding Period):1日、10日、1ヶ月など。トレーディングブックは通常1日、投資ポートフォリオは10日〜1ヶ月を使用する。
  • 観測期間(Observation Window):VaR計算に使用する過去データの期間。通常250日(1年)〜500日(2年)を使用する。

3つの計算方法論の概要

VaRを計算する方法は大きく3つに分類される。

パラメトリック(Parametric/Variance-Covariance)法:収益率が正規分布に従うと仮定し、平均と標準偏差(または共分散行列)のみでVaRを計算する。高速だがファットテール(fat tail)現象を捉えられない。

ヒストリカルシミュレーション(Historical Simulation)法:過去の収益率データを直接使用して損失分布を構成する。分布の仮定が不要だが、過去になかったシナリオは反映できない。

モンテカルロシミュレーション(Monte Carlo Simulation)法:確率モデル(例:幾何ブラウン運動)から数万〜数十万個のランダムシナリオを生成して損失分布を推定する。最も柔軟だが計算コストが高い。

3. パラメトリックVaRの実装

パラメトリックVaRは最もシンプルで高速な方法である。ポートフォリオ収益率が正規分布N(μ, σ^2)に従うと仮定すれば、VaRは以下のように計算される。

VaR = -(μ + z*α * σ) _ Portfolio_Value

ここでz_αは標準正規分布の分位数(例:95%信頼水準ならz = -1.645)である。

import numpy as np
import pandas as pd
from scipy import stats
import yfinance as yf
from datetime import datetime, timedelta

# ポートフォリオ銘柄とウェイト設定
tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'NVDA']
weights = np.array([0.25, 0.20, 0.20, 0.20, 0.15])
portfolio_value = 1_000_000_000  # 10億ウォン

# 株価データ収集(2年分)
end_date = datetime(2026, 3, 1)
start_date = end_date - timedelta(days=730)
prices = yf.download(tickers, start=start_date, end=end_date)['Adj Close']

# 日次対数収益率の計算
log_returns = np.log(prices / prices.shift(1)).dropna()

# ポートフォリオ収益率
portfolio_returns = log_returns.dot(weights)

# パラメトリックVaR計算
confidence_levels = [0.90, 0.95, 0.99]
mean_return = portfolio_returns.mean()
std_return = portfolio_returns.std()

print("=" * 60)
print("パラメトリックVaR(分散共分散法)")
print("=" * 60)
print(f"日次平均収益率: {mean_return:.6f}")
print(f"日次標準偏差:    {std_return:.6f}")
print(f"ポートフォリオ価値:  {portfolio_value:,.0f} ウォン")
print("-" * 60)

for cl in confidence_levels:
    z_score = stats.norm.ppf(1 - cl)
    var_pct = -(mean_return + z_score * std_return)
    var_amount = var_pct * portfolio_value
    print(f"{cl*100:.0f}% VaR: {var_pct:.4%} = {var_amount:,.0f} ウォン")

# 10日VaR(バーゼル規制基準): sqrt(10)スケーリング
print("\n10日VaR(sqrt(T)スケーリング):")
for cl in confidence_levels:
    z_score = stats.norm.ppf(1 - cl)
    var_1d = -(mean_return + z_score * std_return)
    var_10d = var_1d * np.sqrt(10)
    var_10d_amount = var_10d * portfolio_value
    print(f"{cl*100:.0f}% 10日VaR: {var_10d:.4%} = {var_10d_amount:,.0f} ウォン")

上記コードでいくつかの核心ポイントを押さえよう。第一に、対数収益率(log return)を使用する。対数収益率は時間に対して加法的(additive)であるため多期間の収益率を合算でき、正規分布の仮定ともよりフィットする。第二に、10日VaRへのスケーリングにsqrt(10)ルールを適用する。これは収益率が互いに独立で同一分布(i.i.d.)であるという仮定の下でのみ正確である。実際の金融時系列は自己相関とボラティリティクラスタリング(volatility clustering)があるため、このスケーリングは近似に過ぎない。

パラメトリックVaRの多資産拡張:共分散行列の活用

単一ポートフォリオ収益率の標準偏差を直接計算する代わりに、個別資産の共分散行列を活用すればリスク寄与度(risk contribution)分析が可能になる。

# 共分散行列ベースのポートフォリオVaR
cov_matrix = log_returns.cov()

# ポートフォリオ分散 = w^T * Σ * w
portfolio_variance = weights @ cov_matrix.values @ weights
portfolio_std = np.sqrt(portfolio_variance)

# 個別資産のVaR寄与度(Component VaR)
marginal_var = cov_matrix.values @ weights / portfolio_std
component_var = weights * marginal_var

print("\n" + "=" * 60)
print("コンポーネントVaR分析(99%信頼水準)")
print("=" * 60)
z_99 = stats.norm.ppf(0.99)

for i, ticker in enumerate(tickers):
    cvar_pct = component_var[i] * z_99
    cvar_amount = cvar_pct * portfolio_value
    contribution_pct = component_var[i] / portfolio_std * 100
    print(f"{ticker}: ウェイト {weights[i]*100:.0f}% | "
          f"リスク寄与度 {contribution_pct:.1f}% | "
          f"Component VaR {cvar_amount:,.0f} ウォン")

# 分散効果の確認
individual_vars = np.array([
    weights[i] * np.sqrt(cov_matrix.values[i, i]) * z_99
    for i in range(len(tickers))
])
undiversified_var = individual_vars.sum() * portfolio_value
diversified_var = portfolio_std * z_99 * portfolio_value
diversification_benefit = undiversified_var - diversified_var

print(f"\n非分散VaR(合算):   {undiversified_var:,.0f} ウォン")
print(f"分散VaR(ポートフォリオ): {diversified_var:,.0f} ウォン")
print(f"分散効果:            {diversification_benefit:,.0f} ウォン "
      f"({diversification_benefit/undiversified_var*100:.1f}% 削減)")

この分析の核心は分散効果(diversification benefit)である。個別銘柄のVaRを単純合算した値よりポートフォリオ全体のVaRが小さく、この差が分散投資のリスク削減効果である。相関関係が低いほど分散効果は大きくなり、相関係数が1に近づくと(例:危機時の相関収束)分散効果は急激に縮小する。

4. ヒストリカルシミュレーションVaR

ヒストリカルシミュレーションは、分布の仮定を置かないという点でパラメトリック法より現実的である。過去の収益率データを直接損失分布として使用するため、ファットテールや非対称性(skewness)が自然に反映される。

def historical_var(returns, confidence_level=0.95, portfolio_value=1_000_000_000):
    """
    ヒストリカルシミュレーション方式でVaRを計算する。
    """
    sorted_returns = returns.sort_values()
    n = len(sorted_returns)

    percentile_index = int(np.floor((1 - confidence_level) * n))
    var_pct = -sorted_returns.iloc[percentile_index]

    var_pct_interp = -np.percentile(returns, (1 - confidence_level) * 100)

    return {
        'var_pct': var_pct,
        'var_pct_interp': var_pct_interp,
        'var_amount': var_pct * portfolio_value,
        'var_amount_interp': var_pct_interp * portfolio_value,
        'n_observations': n,
        'worst_loss': -sorted_returns.iloc[0],
        'worst_loss_date': sorted_returns.index[0],
    }

results = {}
for cl in [0.90, 0.95, 0.99]:
    result = historical_var(portfolio_returns, cl)
    results[cl] = result
    print(f"\n{cl*100:.0f}% ヒストリカルVaR:")
    print(f"  パーセンタイル方式: {result['var_pct']:.4%} = {result['var_amount']:,.0f} ウォン")
    print(f"  補間法方式:   {result['var_pct_interp']:.4%} = {result['var_amount_interp']:,.0f} ウォン")

print(f"\n観測期間内最大損失: {results[0.95]['worst_loss']:.4%} "
      f"({results[0.95]['worst_loss_date'].strftime('%Y-%m-%d')})")

rolling_window = 250
rolling_var_95 = portfolio_returns.rolling(rolling_window).apply(
    lambda x: -np.percentile(x, 5), raw=True
)

print(f"\nローリングVaR(250日ウィンドウ)統計:")
print(f"  平均 95% VaR: {rolling_var_95.mean():.4%}")
print(f"  最大 95% VaR: {rolling_var_95.max():.4%}(最もリスクが高い時期)")
print(f"  最小 95% VaR: {rolling_var_95.min():.4%}(最もリスクが低い時期)")

ヒストリカルシミュレーションの最大の弱点は「ゴースト効果(ghost effect)」である。観測ウィンドウに極端なイベントが含まれるとVaRが高く維持され、そのイベントがウィンドウから外れる日にVaRが急激に低下する。例えば250日ウィンドウを使用する場合、ちょうど250日前に大きな急落があったとすると、その急落がウィンドウの外に出る翌日にVaRが突然減少する。これは実際のリスクが変わっていないにもかかわらず測定値が急変するアーティファクトである。

この問題の解決策として、指数加重移動平均(EWMA)方式の加重ヒストリカルシミュレーションがある。直近の観測値により高い重みを付与してゴースト効果を緩和する。

5. モンテカルロシミュレーションVaR

モンテカルロシミュレーションはVaR計算において最も柔軟で強力な方法である。基本的なアイデアは、確率モデル(通常は幾何ブラウン運動、GBM)から数千〜数万個の価格パスをランダムに生成し、各パスでのポートフォリオ損益を計算して損失分布を構成することである。

幾何ブラウン運動(GBM)ベースのシミュレーション

幾何ブラウン運動において株価Sの変化は以下の確率微分方程式に従う。

dS = μ _ S _ dt + σ _ S _ dW

ここでμはドリフト(期待収益率)、σはボラティリティ、dWはウィーナー過程の増分である。離散化すると以下となる。

S(t+dt) = S(t) _ exp((μ - σ^2/2) _ dt + σ _ sqrt(dt) _ Z)

ここでZは標準正規分布に従う乱数である。

def monte_carlo_var(
    returns_df,
    weights,
    portfolio_value,
    n_simulations=50_000,
    n_days=1,
    confidence_level=0.95,
    seed=42
):
    """
    モンテカルロシミュレーションでポートフォリオVaRを計算する。

    幾何ブラウン運動(GBM)ベースで相関関係が反映された
    多変量正規分布からシナリオを生成する。
    """
    np.random.seed(seed)

    mean_returns = returns_df.mean().values
    cov_matrix = returns_df.cov().values
    n_assets = len(weights)

    L = np.linalg.cholesky(cov_matrix)

    portfolio_sim_returns = np.zeros(n_simulations)

    for sim in range(n_simulations):
        cumulative_return = 0
        for day in range(n_days):
            Z = np.random.standard_normal(n_assets)
            correlated_returns = mean_returns + L @ Z
            daily_port_return = weights @ correlated_returns
            cumulative_return += daily_port_return

        portfolio_sim_returns[sim] = cumulative_return

    sim_pnl = portfolio_sim_returns * portfolio_value

    var_pct = -np.percentile(portfolio_sim_returns, (1 - confidence_level) * 100)
    var_amount = var_pct * portfolio_value

    var_threshold = np.percentile(portfolio_sim_returns, (1 - confidence_level) * 100)
    tail_losses = portfolio_sim_returns[portfolio_sim_returns <= var_threshold]
    cvar_pct = -tail_losses.mean()
    cvar_amount = cvar_pct * portfolio_value

    return {
        'var_pct': var_pct,
        'var_amount': var_amount,
        'cvar_pct': cvar_pct,
        'cvar_amount': cvar_amount,
        'sim_returns': portfolio_sim_returns,
        'sim_pnl': sim_pnl,
        'n_simulations': n_simulations,
        'n_days': n_days,
        'confidence_level': confidence_level,
    }

mc_results = {}
for cl in [0.90, 0.95, 0.99]:
    result = monte_carlo_var(
        log_returns, weights, portfolio_value,
        n_simulations=100_000,
        n_days=1,
        confidence_level=cl,
        seed=42
    )
    mc_results[cl] = result
    print(f"\n{cl*100:.0f}% モンテカルロVaR(100,000シミュレーション):")
    print(f"  VaR:  {result['var_pct']:.4%} = {result['var_amount']:,.0f} ウォン")
    print(f"  CVaR: {result['cvar_pct']:.4%} = {result['cvar_amount']:,.0f} ウォン")

print("\nシミュレーション数による95% VaRの収束:")
for n_sim in [1_000, 5_000, 10_000, 50_000, 100_000, 500_000]:
    result = monte_carlo_var(
        log_returns, weights, portfolio_value,
        n_simulations=n_sim, confidence_level=0.95, seed=42
    )
    print(f"  N={n_sim:>7,}: VaR = {result['var_pct']:.6%}")

シミュレーション数の決定と収束性

モンテカルロVaRにおけるシミュレーション回数(N)は精度に直接影響する。VaR推定値の標準誤差は概ね1/sqrt(N)に比例するため、シミュレーション数を4倍にすると標準誤差は半分になる。実務では最低10,000回、プロダクションでは50,000〜100,000回が推奨される。99% VaRのような極端な信頼水準ではテール領域のサンプルが少ないため、より多くのシミュレーションが必要である。

コレスキー分解(Cholesky decomposition)は共分散行列を下三角行列Lに分解してL * L^T = Σの関係を満たす方法である。独立な標準正規乱数ベクトルZにLを掛ければ、元の共分散構造を持つ相関した乱数ベクトルが得られる。共分散行列が正定値(positive definite)でなければコレスキー分解は失敗するが、これはデータ期間が資産数より短い場合や線形従属関係がある場合に発生する。この場合は固有値分解(eigendecomposition)を使用するか、最も近い正定値行列に近似するHighamアルゴリズムを適用する必要がある。

6. CVaR(Expected Shortfall)拡張

VaRの最大の限界は「VaRを超える損失がどれほど大きくなりうるか」について何の情報も与えないことである。95% VaRが2%だとしても、残りの5%に該当するケースで3%損失が出る場合も、30%損失が出る場合もある。CVaR(Conditional VaR)、またはExpected Shortfall(ES)はVaRを超過する損失の平均として定義され、テールリスクをはるかに良く捉える。

数学的定義は以下の通りである。

CVaR_α = E[L | L > VaR_α]

CVaRはVaRと異なり、劣加法性(subadditivity)を満たすコヒーレントなリスク尺度(coherent risk measure)である。劣加法性とは、2つのポートフォリオを合わせた時のリスクが個別リスクの合計以下であるという性質である。VaRはこの性質を満たさないため、VaR基準で最適化すると分散投資の効果を過小評価する可能性がある。

バーゼルIII以降の規制体系では、VaRの代わりにExpected Shortfallを市場リスクの基本尺度として採用している(FRTB: Fundamental Review of the Trading Book)。

def comprehensive_risk_metrics(returns, portfolio_value, confidence_level=0.95):
    """
    ポートフォリオの総合リスク指標を計算する。

    VaR、CVaR以外にも最大ドローダウン(MDD)、ボラティリティ、
    歪度、尖度などを合わせて計算し、リスクの全体像を提供する。
    """
    sorted_returns = returns.sort_values()
    n = len(sorted_returns)

    var_pct = -np.percentile(returns, (1 - confidence_level) * 100)

    var_threshold = np.percentile(returns, (1 - confidence_level) * 100)
    tail_returns = returns[returns <= var_threshold]
    cvar_pct = -tail_returns.mean()

    daily_vol = returns.std()
    annual_vol = daily_vol * np.sqrt(252)

    skewness = returns.skew()
    kurtosis = returns.kurtosis()

    cumulative = (1 + returns).cumprod()
    running_max = cumulative.cummax()
    drawdown = (cumulative - running_max) / running_max
    max_drawdown = drawdown.min()

    parametric_var = -(returns.mean() + stats.norm.ppf(1 - confidence_level) * returns.std())
    tail_ratio = var_pct / parametric_var if parametric_var > 0 else float('inf')

    results = {
        'VaR': var_pct,
        'CVaR': cvar_pct,
        'VaR(金額)': var_pct * portfolio_value,
        'CVaR(金額)': cvar_pct * portfolio_value,
        'CVaR/VaR比率': cvar_pct / var_pct if var_pct > 0 else 0,
        '日次ボラティリティ': daily_vol,
        '年率ボラティリティ': annual_vol,
        '歪度': skewness,
        '超過尖度': kurtosis,
        '最大ドローダウン': max_drawdown,
        'テール比率(実際/パラメトリック)': tail_ratio,
    }

    print("=" * 60)
    print(f"総合リスク分析({confidence_level*100:.0f}%信頼水準)")
    print("=" * 60)
    for key, value in results.items():
        if '金額' in key:
            print(f"  {key}: {value:,.0f} ウォン")
        elif '比率' in key or 'ボラティリティ' in key or 'ドローダウン' in key:
            print(f"  {key}: {value:.4%}")
        else:
            print(f"  {key}: {value:.6f}")

    print("\n[解釈ガイド]")
    if kurtosis > 1:
        print(f"  - 超過尖度 {kurtosis:.2f}: 正規分布よりテールが厚い"
              f"(ファットテール)。パラメトリックVaRがリスクを過小評価する可能性が高い。")
    if skewness < -0.5:
        print(f"  - 歪度 {skewness:.2f}: 負の非対称性。"
              f"大きな損失が大きな利益よりも頻繁に/大きく発生する傾向。")
    if tail_ratio > 1.2:
        print(f"  - テール比率 {tail_ratio:.2f}: 実際のテールリスクが"
              f"正規分布仮定対比 {(tail_ratio-1)*100:.0f}% 大きい。")

    return results

risk_metrics = comprehensive_risk_metrics(portfolio_returns, portfolio_value, 0.95)

CVaR/VaR比率はテールリスクの深刻度を示す有用な指標である。正規分布の下で95%信頼水準のCVaR/VaR比率は約1.25である。この比率が1.5を超えると極端なテールリスクが存在するシグナルであり、ストレステスティングを強化しポートフォリオ調整を検討すべきである。

7. ポートフォリオ最適化とリスク統合

VaRとCVaRを単に測定するだけでなく、ポートフォリオ構成に反映すればリスクベースの最適化が可能になる。PyPortfolioOptライブラリは平均分散最適化(Markowitz)、最小CVaR最適化、階層的リスクパリティ(HRP)など多様な方法を提供する。

from pypfopt import EfficientFrontier, risk_models, expected_returns
from pypfopt import HRPOpt
from pypfopt.efficient_frontier import EfficientCVaR

mu = expected_returns.mean_historical_return(prices)
S = risk_models.sample_cov(prices)

ef_minvol = EfficientFrontier(mu, S)
ef_minvol.min_volatility()
w_minvol = ef_minvol.clean_weights()
perf_minvol = ef_minvol.portfolio_performance(verbose=False)

ef_sharpe = EfficientFrontier(mu, S)
ef_sharpe.max_sharpe(risk_free_rate=0.035)
w_sharpe = ef_sharpe.clean_weights()
perf_sharpe = ef_sharpe.portfolio_performance(verbose=False)

hrp = HRPOpt(log_returns)
hrp.optimize()
w_hrp = hrp.clean_weights()
perf_hrp = hrp.portfolio_performance(verbose=False)

ef_cvar = EfficientCVaR(mu, log_returns)
ef_cvar.min_cvar()
w_cvar = ef_cvar.clean_weights()

print("=" * 70)
print("ポートフォリオ最適化結果比較")
print("=" * 70)
print(f"{'戦略':<20} {'期待収益率':>10} {'ボラティリティ':>10} {'シャープ':>8}")
print("-" * 70)
print(f"{'最小ボラティリティ':<20} {perf_minvol[0]:>10.2%} {perf_minvol[1]:>10.2%} "
      f"{perf_minvol[2]:>8.3f}")
print(f"{'最大シャープ':<20} {perf_sharpe[0]:>10.2%} {perf_sharpe[1]:>10.2%} "
      f"{perf_sharpe[2]:>8.3f}")
print(f"{'HRP':<20} {perf_hrp[0]:>10.2%} {perf_hrp[1]:>10.2%} "
      f"{perf_hrp[2]:>8.3f}")

print("\nウェイト比較:")
for ticker in tickers:
    print(f"  {ticker}: 最小ボラ {w_minvol.get(ticker,0):.1%} | "
          f"最大シャープ {w_sharpe.get(ticker,0):.1%} | "
          f"HRP {w_hrp.get(ticker,0):.1%} | "
          f"最小CVaR {w_cvar.get(ticker,0):.1%}")

最小ボラティリティポートフォリオは全体リスクを最小化するが期待収益率も低くなりうる。最大シャープポートフォリオはリスク対比リターンが最も高いが特定銘柄にウェイトが集中する傾向がある。HRP(Hierarchical Risk Parity)は共分散行列の逆行列を求めないため推定誤差に対してロバストで、資産数が多い場合や共分散行列が不安定な場合に頑健な結果を提供する。

実務では単一の最適化手法に依存するのではなく、複数の手法の結果をアンサンブルするか、ブラック・リッターマンモデルで投資家のビューを組み合わせるのが一般的である。

8. VaR方法論比較表

3つのVaR方法論の特性を体系的に比較すると以下の通りである。

基準パラメトリックVaRヒストリカルVaRモンテカルロVaR
分布仮定正規分布(またはt分布)なし(ノンパラメトリック)モデルによる(GBM等)
計算速度非常に速い(ミリ秒)速い(ミリ秒〜秒)遅い(秒〜分)
ファットテール反映不可(t分布使用時は部分的)過去のファットテールを自動反映モデルにより反映可能
非線形商品不可(デルタ・ガンマ近似必要)可能(フルリバリュエーション)可能(フルリバリュエーション)
新シナリオ生成可能不可(過去データに限定)生成可能
実装難易度低い中程度高い
データ要件平均、分散、共分散のみ必要十分な過去データが必要モデルパラメータ + 乱数生成
規制認定バーゼル:条件付き認定バーゼル:認定バーゼル:認定
主な弱点正規分布仮定の違反ゴースト効果、将来シナリオ不在モデルリスク、計算コスト
適した用途高速な日中リスクモニタリング規制報告、一般ポートフォリオデリバティブ、複合ポートフォリオ
拡張性資産数増加に二乗コスト線形コスト高い固定コスト

方法論選択ガイドライン

  • 単純な株式/債券ポートフォリオ:ヒストリカルVaRから始め、パラメトリックVaRをベンチマークとして比較
  • オプション/デリバティブを含む場合:必ずモンテカルロVaRを使用(非線形ペイオフの反映が必須)
  • リアルタイムリスクモニタリング:パラメトリックVaR(速度優先)
  • 規制報告目的:ヒストリカルまたはモンテカルロVaR + 必ずバックテスティングを伴う
  • リスクリミット設定:CVaRベース推奨(VaRのみではテールリスクを反映しない)

9. バックテスティングとモデル検証

VaRモデルを構築したら必ずバックテスティング(backtesting)でモデルの正確性を検証しなければならない。バックテスティングとは、過去データでVaRを計算した後、実際の損失がVaRを超過した回数(VaR breach)が理論的期待と一致するかを統計的に検定する手続きである。

例えば95% VaRを250日間バックテスティングすると、期待される超過回数は約12.5回(250 × 5%)である。超過回数がこれより有意に多ければモデルがリスクを過小評価しており、少なければ過大評価している。

Kupiec検定(POF Test)

Kupiec検定は超過比率(proportion of failures)が理論的比率と一致するかを尤度比検定(likelihood ratio test)で確認する。

Christoffersen検定(Conditional Coverage Test)

Christoffersen検定は超過の独立性(independence)まで検定する。VaR超過が連続で発生する場合(clustering)、モデルがボラティリティクラスタリング現象を適切に反映していない証拠である。

def backtest_var(returns, var_series, confidence_level=0.95, method_name=""):
    """
    VaRバックテスティング:Kupiec検定の実施
    """
    breaches = returns < -var_series
    n_breaches = breaches.sum()
    n_total = len(returns)
    breach_rate = n_breaches / n_total
    expected_rate = 1 - confidence_level
    expected_breaches = expected_rate * n_total

    print(f"\n{'=' * 60}")
    print(f"VaRバックテスティング結果: {method_name}")
    print(f"{'=' * 60}")
    print(f"  観測期間:       {n_total} 営業日")
    print(f"  VaR超過回数:   {n_breaches} 回")
    print(f"  期待超過回数:  {expected_breaches:.1f} 回")
    print(f"  実際超過比率:  {breach_rate:.2%}")
    print(f"  期待超過比率:  {expected_rate:.2%}")

    p = expected_rate
    p_hat = breach_rate if breach_rate > 0 else 1e-10

    if n_breaches == 0:
        lr_stat = -2 * (n_total * np.log(1 - p) - n_total * np.log(1 - p_hat))
    elif n_breaches == n_total:
        lr_stat = -2 * (n_total * np.log(p) - n_total * np.log(p_hat))
    else:
        lr_stat = -2 * (
            n_breaches * np.log(p) +
            (n_total - n_breaches) * np.log(1 - p) -
            n_breaches * np.log(p_hat) -
            (n_total - n_breaches) * np.log(1 - p_hat)
        )

    p_value = 1 - stats.chi2.cdf(lr_stat, df=1)

    print(f"\n  Kupiec LR統計量: {lr_stat:.4f}")
    print(f"  p値:          {p_value:.4f}")

    if p_value > 0.05:
        verdict = "合格(モデル適合)"
    else:
        verdict = "不合格(モデル不適合 - 再検討必要)"
    print(f"  判定(5%有意水準): {verdict}")

    if n_total >= 250:
        scale_factor = n_total / 250
        scaled_breaches = n_breaches / scale_factor
        if scaled_breaches <= 4:
            zone = "グリーン(Green Zone) - 良好"
        elif scaled_breaches <= 9:
            zone = "イエロー(Yellow Zone) - 注意必要"
        else:
            zone = "レッド(Red Zone) - モデル修正必須"
        print(f"  バーゼル信号灯:      {zone}")

    if n_breaches > 0:
        consecutive = 0
        max_consecutive = 0
        for b in breaches:
            if b:
                consecutive += 1
                max_consecutive = max(max_consecutive, consecutive)
            else:
                consecutive = 0
        print(f"\n  最大連続超過:   {max_consecutive} 営業日")
        if max_consecutive >= 3:
            print(f"  警告: 連続超過 {max_consecutive}回発生。"
                  f"ボラティリティクラスタリング未反映の可能性。")

    return {
        'n_breaches': n_breaches,
        'breach_rate': breach_rate,
        'lr_stat': lr_stat,
        'p_value': p_value,
        'pass': p_value > 0.05,
    }

window = 250
test_returns = portfolio_returns.iloc[window:]

rolling_mean = portfolio_returns.rolling(window).mean().iloc[window:]
rolling_std = portfolio_returns.rolling(window).std().iloc[window:]
z_95 = stats.norm.ppf(0.95)
parametric_var_series = -(rolling_mean - z_95 * rolling_std)

historical_var_series = portfolio_returns.rolling(window).apply(
    lambda x: -np.percentile(x, 5), raw=True
).iloc[window:]

bt_param = backtest_var(test_returns, parametric_var_series, 0.95, "パラメトリックVaR")
bt_hist = backtest_var(test_returns, historical_var_series, 0.95, "ヒストリカルVaR")

バックテスティングでモデルが失敗した場合、直ちに原因を分析しなければならない。一般的な原因と対応は以下の通りである。

  • 超過回数が多すぎる(リスク過小評価):観測ウィンドウを拡大するか、ストレス期間を含むようにデータを拡張。GARCHモデルで時変ボラティリティを反映。または信頼水準を引き上げて保守的に運用。
  • 超過回数が少なすぎる(リスク過大評価):資本効率が低下するため、モデルパラメータを精緻化するかボラティリティ推定手法を改善。
  • 超過がクラスタリングで発生:GARCH/EGARCHなどのボラティリティクラスタリングモデルをVaRフレームワークに統合。

10. 運用上の注意事項

データ品質の問題

VaRモデルの正確性は入力データの品質に全面的に依存する。実務で頻繁に遭遇するデータの問題は以下の通りである。

  • 欠損価格データ:特定の営業日に価格がなければ収益率計算が歪む。単純な前日価格代入(forward fill)はボラティリティを過小評価しうる。欠損比率が5%を超える場合、当該銘柄のVaR信頼性を再検討すべきである。
  • 配当/株式分割未調整:修正株価(adjusted price)を使用しないと、配当落ち日や株式分割日に人為的な急落が観測されVaRを過大評価する。
  • 生存者バイアス(Survivorship Bias):上場廃止銘柄がデータから外れるとリスクを過小評価する。ヒストリカルVaRで特に深刻である。
  • ステール価格(Stale Prices):流動性が低い資産は最終約定価格が現在価値を反映していない可能性がある。これによりボラティリティと相関関係の両方が歪む。

モデルの限界と仮定の違反

仮定現実影響対応
収益率は正規分布ファットテール、負の歪度が存在パラメトリックVaR過小評価t分布使用またはヒストリカル/MC併用
収益率間は独立(i.i.d.)ボラティリティクラスタリングsqrt(T)スケーリング不正確GARCHで時変ボラティリティモデリング
相関関係は一定危機時に相関が収束分散効果の過大評価ストレス相関、コピュラモデル
市場流動性は十分急落時に流動性消失清算不可で損失拡大流動性調整VaR(LVaR)
過去が未来を代表構造変化(regime change)全VaR方法論に影響レジームスイッチングモデル、ストレスシナリオ

規制上の考慮事項

  • バーゼルIII/IV:市場リスク資本は97.5% ES(Expected Shortfall)基準で計算。内部モデルを使用するには独立したバックテスティングの通過が必須。
  • FRTB(Fundamental Review of the Trading Book):従来のVaRベースからESベースへの移行。ストレス期間のESの使用を要求。
  • 韓国金融監督院:内部モデル承認を受けた金融機関は自社VaRモデルを使用できるが、定期的なバックテスティング報告書の提出義務がある。

プロダクション運用チェックポイント

  • 毎日のVaR計算結果を自動記録し、前日比で急激な変化(例:30%以上の変動)があればアラートを発生させる。
  • 月次でバックテスティングを実施しモデルの正確性を継続的にモニタリングする。
  • 四半期ごとにモデルパラメータ(観測ウィンドウ、ボラティリティモデル等)をレビューし、市場環境の変化に合わせて調整する。
  • ストレステストシナリオを最低年1回更新する。新たな危機事例(例:コロナパンデミック、SVB事件)をシナリオに追加する。

11. 失敗事例と復旧手順

事例1:2008年金融危機とVaRの限界

2008年のグローバル金融危機はVaRベースのリスク管理の根本的限界を示した歴史的事件である。危機前、ほとんどの投資銀行は95%または99% VaRをリスクリミットとして使用していたが、実際の損失はVaRを数十倍超過した。

原因分析:

  • 仮定の違反:サブプライムモーゲージ関連の仕組み商品(CDO等)の原資産の相関が危機時に急激に1に収束した。VaRモデルは平時の低い相関に基づいていたため、分散効果を大きく過大評価した。
  • 流動性リスクの未反映:VaRは資産を正常に清算できると仮定するが、危機時に市場流動性が完全に消失した。売り気配のない市場では理論的VaR自体が無意味であった。
  • モデルリスク(Model Risk):CDO価格決定に使用されたガウシアンコピュラ(Gaussian copula)モデルがテール依存性(tail dependence)を過小評価した。

教訓:

  • VaRはリスクの上限ではなく「正常な市場環境での損失推定値」に過ぎない。
  • VaR単独使用は危険であり、CVaR + ストレステスティング + 流動性リスク評価を必ず併用すべきである。
  • 相関関係の仮定のロバスト性をストレスシナリオで必ず点検すべきである。

事例2:モデル実装エラーによるリスク過小評価

実務ではVaRモデルの失敗は理論的限界よりも実装バグで頻繁に発生する。以下は実際に起こりやすいエラーパターンである。

よくあるバグ:

  • 収益率計算の方向エラー(P_t - P_{t-1}) / P_{t-1} の代わりに (P_{t-1} - P_t) / P_t で計算すると符号と大きさの両方が間違う。
  • 年率化の間違い:日次ボラティリティにsqrt(252)ではなく252を掛けるとボラティリティが約16倍過大評価される。
  • 配列インデックスエラー:VaRの分位数計算で昇順/降順を混同すると、最小損失をVaRとして報告する深刻なエラーが発生する。
  • タイムゾーン(timezone)の不一致:米国株式と欧州株式の終値時点が異なるのに無視すると相関関係が歪む。

復旧手順:

  1. VaR急変時に直ちにデータパイプラインを点検(欠損、異常値、重複の有無)
  2. 単一資産について手動計算とコード結果を対照検証
  3. 既知の分布(例:標準正規分布)から生成した合成データでコード単体テスト
  4. 過去の特定日付のVaRを再現して時点別の一貫性を確認
  5. 独立した第二の実装体(またはベンダーシステム)との交差検証

事例3:ボラティリティレジーム転換の未検知

2020年3月のコロナパンデミック初期にはVIX指数が80を超え、2018〜2019年のデータで学習したVaRモデルが一日で無力化された。観測ウィンドウ250日(1年)内にこのような極端なボラティリティが含まれていなかったためである。

対応策:

  • GARCH(1,1)モデルを適用して直近数日間のボラティリティ急増を即座にVaRに反映する。
  • 長期ウィンドウ(500日、750日)を併用して2008年などの過去の危機データを含める。
  • VaRモデルにレジームスイッチング(マルコフレジームスイッチング)機能を追加し、現在が高ボラティリティレジームか低ボラティリティレジームかを自動検知する。
  • VIX等の外部ボラティリティ指標をモニタリングし、急騰時にリスクリミットを先制的に調整する。

12. チェックリスト

VaRベースのリスク管理システムを構築する際に以下の項目を点検する。

モデル構築段階

  • 収益率計算方法の確認(対数収益率 vs 算術収益率、用途に合わせて選択)
  • データ品質検証(欠損、異常値、配当/分割調整の有無)
  • 最低2種類以上のVaR方法論を並行計算(パラメトリック + ヒストリカル、またはヒストリカル + モンテカルロ)
  • CVaR(Expected Shortfall)を併せて計算しテールリスクを把握
  • 観測ウィンドウ長の感度分析実施(250日、500日、750日を比較)
  • 共分散行列の正定値性確認(コレスキー分解可能性テスト)

バックテスティング段階

  • Kupiec検定(POF test)通過確認
  • 超過(breach)パターンのクラスタリング分析(Christoffersen独立性検定)
  • バーゼル信号灯体系基準グリーンゾーン維持の確認
  • 最低250営業日以上のバックテスティング期間確保

プロダクション運用段階

  • 日次VaR自動計算および記録パイプライン構築
  • VaR急変時の自動アラートメカニズム設定(前日比閾値超過時)
  • 月次バックテスティングレポート自動生成
  • 四半期ごとのモデルパラメータレビュー日程確立
  • 年次ストレステストシナリオ更新
  • コード変更時の単体テスト(合成データベースのVaR再現テスト)
  • 第二システムまたはベンダーとの定期交差検証

ガバナンス段階

  • リスクリミット(limit)体系とVaR連動方式の文書化
  • VaR超過時のエスカレーション手順確立
  • モデル変更管理(model change management)プロセス確立
  • モデルリスクインベントリにVaRモデルを登録し定期検討

13. 参考資料

  • Investopedia: Value at Risk (VaR) Explained - VaR概念の基礎を固めるのに適した入門資料。信頼水準、保有期間等の核心パラメータについて直感的な説明を提供する。
  • Risk Engineering: Value at Risk - VaRの数学的定義と3つの方法論に関する学術レベルの資料。限界点と代替案についての議論も含まれている。
  • Interactive Brokers: Risk Metrics in Python - VaR and CVaR Guide - PythonでVaRとCVaRを実装する実践ガイド。yfinanceを活用したデータ収集からバックテスティングまでコード例を提供する。
  • PyPortfolioOpt GitHub Repository - 平均分散最適化、HRP、ブラック・リッターマン等の多様なポートフォリオ最適化アルゴリズムのPython実装体。リスク統合最適化に活用可能である。
  • PyQuant News: Quickly Compute Value at Risk with Monte Carlo - モンテカルロシミュレーションベースのVaR計算の核心を簡潔にまとめたニュースレター。コレスキー分解とシミュレーション収束に関する実用的なアドバイスを含む。
  • Hull, J.C. (2022). Options, Futures, and Other Derivatives. 11th Edition. Pearson. - 金融工学の標準的教科書で、VaRの章で3つの方法論の数学的基礎を厳密に扱う。
  • Jorion, P. (2006). Value at Risk: The New Benchmark for Managing Financial Risk. 3rd Edition. McGraw-Hill. - VaRに関する最も包括的な単行本。方法論、バックテスティング、規制適用まで全範囲を扱う。