正規分布でVaR計算してたら暴落で全然当たらなかった話→ファットテールを知る

基礎知識・戦略

「99%の確率で、1日の損失は最大でも◯円に収まります」——VaR(バリュー・アット・リスク)を初めて計算したとき、僕はこの数字にすっかり安心してしまいました。自分のポートフォリオのリスクが、たった1つの金額で表現できてしまう。これは便利だ、と。

ところが実際に相場が荒れた日、損失はVaRの計算値をあっさり超えていきました。しかも1回だけではありません。「100日に1回しか起きないはず」の水準を、数か月のうちに何度も突破したのです。

調べていくうちに、原因が自分の計算方法にあると分かりました。正規分布を仮定してVaRを計算していたことです。株価のリターンは正規分布に従いません。この記事では、その事実をPythonで実際に確認し、ファットテール(裾の厚さ)を踏まえたリスク計算に切り替えるまでの過程をまとめます。

※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。

VaRとは何を計算しているのか

VaRは「ある確率のもとで、一定期間に発生しうる最大損失額」を表します。たとえば「1日・信頼水準99%のVaRが50万円」なら、次のような意味になります。

  • 100日のうち99日は、1日の損失が50万円以内に収まる
  • 残る1日は50万円を超える損失が出る
  • ただし、その1日にいくら損するかは何も言っていない

3つめが決定的に重要です。僕が最初に見落としていたのもここでした。VaRは「どこまでが平常運転か」の線引きをしているだけで、その線を超えたときの被害規模はまったく教えてくれません。

たとえるなら、堤防の高さだけを決めて、越水したときにどれだけ浸水するかを考えていない状態です。堤防の高さは大事ですが、それだけでは防災計画になりません。

VaRを計算するには、大きく3つの方法があります。どれを選ぶかで結果がかなり変わるので、先に整理しておきます。

手法やり方前提
分散共分散法平均と標準偏差から計算正規分布を仮定する
ヒストリカル法過去データの分位点を取る分布の仮定なし
モンテカルロ法乱数で将来を大量に生成生成モデルの仮定が必要

この記事では最初の2つを扱います。僕が使っていたのは1つめで、失敗の原因もそこにありました。モンテカルロ法についてはモンテカルロシミュレーションで株式投資のリスクを測る方法で別途扱っています。

📘 外部参考Value at Risk(Investopedia)ファット・テール(野村證券 証券用語解説集)

正規分布を仮定したVaRを計算する

まずは、僕が最初にやっていた方法から。分散共分散法(パラメトリックVaR)と呼ばれるもので、平均と標準偏差さえあれば計算できます。

pip install yfinance pandas numpy scipy matplotlib
import yfinance as yf
import numpy as np
import pandas as pd
from scipy.stats import norm

PORTFOLIO = 5_000_000   # 運用資金

px = yf.download("^N225", period="10y", auto_adjust=True,
                 progress=False)["Close"].dropna()
ret = px.pct_change().dropna()

mu, sigma = float(ret.mean()), float(ret.std())

print(f"日次リターン 平均 {mu * 100:.4f}% / 標準偏差 {sigma * 100:.4f}%\n")
for conf in (0.95, 0.99, 0.999):
    z = norm.ppf(1 - conf)                 # 下側の分位点(負の値)
    var_pct = mu + z * sigma
    print(f"信頼水準 {conf * 100:>5.1f}% → VaR {abs(var_pct) * 100:>5.2f} % "
          f"= {abs(var_pct) * PORTFOLIO:>10,.0f} 円")

数行で計算できてしまいます。この手軽さが罠でした。出てきた数字がもっともらしく見えるので、前提を疑わずに使ってしまうのです。

ここで使っている norm.ppf() は、正規分布を前提に「下位◯%の位置」を返す関数です。つまりこの計算は、「株価のリターンは正規分布に従う」という仮定の上に完全に乗っかっています

検証:VaRは実際に何回破られたか

理論が正しいかは、過去データで数えれば分かります。バックテスティングと呼ばれる検証で、金融機関でも規制上求められている手続きです。

n = len(ret)

print(f"{'信頼水準':>8s} {'VaR':>8s} {'理論上の超過':>12s} {'実際の超過':>10s} {'倍率':>7s}")
for conf in (0.95, 0.99, 0.995, 0.999):
    z = norm.ppf(1 - conf)
    threshold = mu + z * sigma
    actual = int((ret < threshold).sum())
    expected = (1 - conf) * n
    ratio = actual / expected if expected else float("inf")
    print(f"{conf * 100:>7.1f}% {abs(threshold) * 100:>7.2f}% "
          f"{expected:>12.1f} {actual:>10d} {ratio:>6.2f}倍")

実行すると、信頼水準が高くなるほど実際の超過回数が理論値を大きく上回ることが分かります。95%ではそこそこ合っていても、99.9%になると理論の数倍から10倍以上に膨れ上がります。

これが「安全側を見ているつもりで、いちばん危ない領域を最も過小評価していた」という状態です。気にしているのは極端な損失なのに、極端になるほど計算が外れる。これでは何のためのリスク管理か分かりません。

なぜ外れるのか——分布の形を見る

原因を確かめるため、実際のリターン分布と正規分布を重ねてみます。

from scipy import stats

skew = float(stats.skew(ret))
kurt = float(stats.kurtosis(ret))        # 超過尖度(正規分布なら0)
jb_stat, jb_p = stats.jarque_bera(ret)

print(f"歪度 (skewness)  : {skew:>8.3f}   (正規分布なら 0)")
print(f"尖度 (kurtosis)  : {kurt:>8.3f}   (正規分布なら 0)")
print(f"Jarque-Bera検定 p: {jb_p:>8.2e}")
print("→ p値が極めて小さい = 正規分布であるという仮説は棄却される"
      if jb_p < 0.05 else "→ 正規分布と矛盾しない")

日経平均で計算すると、尖度は5〜10程度という値になります。正規分布なら0のはずが、大きく上回っている。これが「裾が厚い」=ファットテールということです。

指標正規分布実際の株価意味すること
歪度0マイナス急落のほうが急騰より激しい
尖度(超過)0大きくプラス極端な日が理論より頻繁に起きる

歪度と尖度の詳しい読み方は歪度・尖度で「暴落しやすさ」を数値化する方法にまとめています。

ヒストリカルVaR:分布を仮定しない

解決策の1つめは単純です。正規分布を仮定するのをやめ、実際に起きたデータをそのまま並べて分位点を取る。これがヒストリカルVaRです。

def historical_var(returns, confidence=0.99, portfolio=1.0):
    """過去データの分位点をそのまま使うVaR"""
    q = float(pd.Series(returns).quantile(1 - confidence))
    return abs(q) * portfolio


def parametric_var(returns, confidence=0.99, portfolio=1.0):
    """正規分布を仮定したVaR"""
    r = pd.Series(returns)
    z = norm.ppf(1 - confidence)
    return abs(float(r.mean()) + z * float(r.std())) * portfolio


print(f"{'信頼水準':>8s} {'正規分布':>12s} {'ヒストリカル':>14s} {'差':>12s}")
for conf in (0.95, 0.99, 0.995, 0.999):
    p = parametric_var(ret, conf, PORTFOLIO)
    h = historical_var(ret, conf, PORTFOLIO)
    print(f"{conf * 100:>7.1f}% {p:>12,.0f} {h:>14,.0f} "
          f"{h - p:>+12,.0f}")

信頼水準を上げるほどヒストリカルVaRのほうが大きな損失を見積もることが確認できます。実際に起きたことをそのまま反映しているので当然です。

ただしこの方法にも弱点があります。過去に起きなかったことは予測できないという点です。10年分のデータで計算すれば、その10年で最悪だった日が上限になります。それを超える事態は想定に入りません。

期待ショートフォール:超えた先を見る

VaRの最大の弱点は、冒頭に書いたとおり「線を超えた後」を何も語らないことでした。これを補うのが期待ショートフォール(ES、条件付きVaR)です。

考え方はシンプルで、VaRを超えた日だけを集めて、その平均損失を出すだけです。

def expected_shortfall(returns, confidence=0.99, portfolio=1.0):
    """VaRを超えた場合の平均損失(テールの重心)"""
    r = pd.Series(returns)
    threshold = r.quantile(1 - confidence)
    tail = r[r <= threshold]
    return abs(float(tail.mean())) * portfolio if len(tail) else 0.0


print(f"{'信頼水準':>8s} {'VaR':>12s} {'ES':>12s} {'ES/VaR':>9s}")
for conf in (0.95, 0.99, 0.995):
    v = historical_var(ret, conf, PORTFOLIO)
    e = expected_shortfall(ret, conf, PORTFOLIO)
    print(f"{conf * 100:>7.1f}% {v:>12,.0f} {e:>12,.0f} {e / v:>8.2f}倍")

ESはVaRの1.2〜1.5倍程度になります。つまり「VaRを超える日が来たら、平均してVaRの1.3倍くらいの損失を覚悟する必要がある」ということです。

この数字を知ってから、僕はリスク管理の考え方を変えました。VaRは「いつもの範囲」を知るための指標、ESは「悪い日に何が起きるか」を知るための指標。両方を並べて見るようになってから、想定外の損失で慌てることが減りました。

ちなみに国際的な銀行規制でも、リスク指標の主役はVaRからESへ移行しています。実務の世界でも「VaRだけでは足りない」という結論に至ったわけです。

t分布でテールを厚くする

もう1つの改善策として、正規分布の代わりに裾の厚い分布を当てはめる方法があります。よく使われるのがt分布です。

from scipy.stats import t as t_dist

# 実データにt分布を当てはめる
params = t_dist.fit(ret)
df_param, loc, scale = params

print(f"推定された自由度 : {df_param:.2f}")
print("(自由度が小さいほど裾が厚い。30以上ならほぼ正規分布)\n")


def student_t_var(returns, confidence=0.99, portfolio=1.0):
    """t分布を仮定したVaR"""
    nu, loc, scale = t_dist.fit(returns)
    q = t_dist.ppf(1 - confidence, nu, loc=loc, scale=scale)
    return abs(float(q)) * portfolio


print(f"{'信頼水準':>8s} {'正規':>12s} {'t分布':>12s} {'ヒストリカル':>14s}")
for conf in (0.95, 0.99, 0.995, 0.999):
    print(f"{conf * 100:>7.1f}% "
          f"{parametric_var(ret, conf, PORTFOLIO):>12,.0f} "
          f"{student_t_var(ret, conf, PORTFOLIO):>12,.0f} "
          f"{historical_var(ret, conf, PORTFOLIO):>14,.0f}")

自由度が3〜6程度と推定されるはずです。この値が小さいほど、正規分布から遠い=裾が厚いことを意味します。t分布を使うと、正規分布より現実に近い(大きめの)VaRが得られます。

手法長所短所
正規分布計算が簡単。式が閉じているテールを大幅に過小評価
ヒストリカル仮定不要。実データに忠実過去にない事態は扱えない
t分布裾の厚さを反映できる分布の形は仮定したまま
期待ショートフォール超えた先の被害が分かるデータが少ないと不安定

実務ではヒストリカルVaRとESの組み合わせが扱いやすいと感じています。分布の形を仮定しなくて済み、かつテールの重心も分かるからです。

ポートフォリオ全体のVaRを出す

実際に管理したいのは、1銘柄ではなく保有資産全体のリスクです。各銘柄のVaRを単純に足すと、実態より大きくなります。値動きのタイミングがずれる分、打ち消し合うためです。

HOLDINGS = {"7203.T": 0.30, "6758.T": 0.20,
            "9432.T": 0.30, "8306.T": 0.20}
CONF = 0.99

prices = yf.download(list(HOLDINGS), period="5y", auto_adjust=True,
                     progress=False)["Close"].dropna()
rets = prices.pct_change().dropna()
weights = pd.Series(HOLDINGS)

# 1) 各銘柄を単独で見た場合のVaRを足し合わせる
sum_var = sum(historical_var(rets[c], CONF, PORTFOLIO * w)
              for c, w in HOLDINGS.items())

# 2) ポートフォリオとしての損益系列から直接求める
port_ret = (rets * weights).sum(axis=1)
port_var = historical_var(port_ret, CONF, PORTFOLIO)
port_es = expected_shortfall(port_ret, CONF, PORTFOLIO)

print(f"単純合計のVaR : {sum_var:>12,.0f} 円")
print(f"実際のVaR     : {port_var:>12,.0f} 円")
print(f"分散効果      : {sum_var - port_var:>12,.0f} 円 "
      f"({(1 - port_var / sum_var) * 100:.1f}% 削減)")
print(f"期待ショートフォール: {port_es:>8,.0f} 円")

分散効果がはっきり数字で出ます。同じ資金を1銘柄に集中させた場合と比べて、リスクがどれだけ下がっているかが分かるわけです。

ただし注意点があります。暴落局面では銘柄間の相関が1に近づく——つまり全部まとめて下がるため、分散効果は最も必要なときに消えます。この現象を確認する方法は相関係数を計算して銘柄間の関係を数値化する方法で扱っています。

実際の運用にどう落とし込むか

日次で監視して記録する

計算して終わりにせず、毎日回して記録を残します。VaRが急拡大したときは相場環境が変わった合図なので、ポジションを見直すきっかけになります。

# 直近250日を基準に、VaRの推移を追う
rolling_var = port_ret.rolling(250).quantile(0.01).abs() * PORTFOLIO
rolling_var = rolling_var.dropna()

print(f"現在のVaR   : {float(rolling_var.iloc[-1]):>10,.0f} 円")
print(f"過去1年平均 : {float(rolling_var.tail(250).mean()):>10,.0f} 円")

ratio = float(rolling_var.iloc[-1] / rolling_var.tail(250).mean())
print(f"平均比      : {ratio:.2f} 倍")
if ratio > 1.3:
    print("⚠ リスクが平常時より3割以上拡大しています")

VaRを資金管理の上限に使う

「1日のVaRが資金の2%を超えないようにする」といったルールを決めておくと、ポジションサイズの上限が自動的に決まります。

MAX_VAR_PCT = 0.02   # 資金の2%まで

current = historical_var(port_ret, 0.99)     # 比率で取得
allowed = MAX_VAR_PCT / current if current else 0

print(f"現在のVaR比率 : {current * 100:.2f} %")
print(f"許容できる建玉 : 資金の {allowed * 100:.0f} % まで")
if allowed < 1.0:
    print(f"→ 現在のポジションは過大です。{(1 - allowed) * 100:.0f}% 縮小を検討してください")

この考え方は自作システムで株を運用する際の絶対ルールと資金管理法とも共通します。リスク量を先に決めて、そこから建玉を逆算するという発想です。

VaRを信じすぎない

最後に、いちばん大事なことを書いておきます。ここまで手法を改善してきましたが、どの方法を使ってもVaRは「過去の延長線上」でしか語れません

  • 市場が閉まって売りたくても売れない状況は計算に入らない
  • ストップ安に張り付いて損切りが執行されないケースも織り込めない
  • 過去に前例のない出来事は、定義上どうやっても予測できない

だからこそ、VaRとESに加えて「全部なくなっても生活が壊れない金額か」という素朴な問いを最後に置いています。統計は平常時の管理には有効ですが、最後の防波堤にはなりません。

まとめ

正規分布を仮定したVaRで痛い目に遭い、ファットテールを踏まえた計算に切り替えるまでの流れをまとめました。

  • VaRは「平常の範囲」を示すだけで、超えたときの被害額は語らない
  • 正規分布を仮定すると、信頼水準が高いほど過小評価になる。最も知りたい領域が最も外れる
  • 株価リターンの尖度は5〜10(正規分布は0)。裾が明確に厚い
  • 対策はヒストリカルVaR(仮定なし)かt分布(裾の厚さを反映)
  • 期待ショートフォールを併用すると、超えた先の平均損失が分かる。VaRの1.2〜1.5倍
  • 分散効果は数字で確認できるが、暴落時には相関が1に近づいて消える
  • どの手法も過去の延長。最後は「全額失っても大丈夫か」で判断する

いちばんの学びは、「便利な数字ほど前提を確認すべき」ということでした。VaRは5行で計算できてしまうがゆえに、その裏にある「正規分布の仮定」を疑わないまま使ってしまう。数字が出ること自体が、思考停止の入口になり得るわけです。

まずは自分のポートフォリオで、正規分布版とヒストリカル版のVaRを両方計算して並べてみてください。信頼水準99.5%あたりで両者の差を見ると、自分がどれだけリスクを過小評価していたかが一目で分かります。

タイトルとURLをコピーしました