18. 確率過程とリスク指標
テーマ: 幾何ブラウン運動による株価シミュレーションと VaR / 期待ショートフォール
学習点: NumPy の累積積, 分位点, ヒストグラムのテキスト描画, リスク指標の定義
依存: NumPy / 難易度: 中級
実行方法
uv run 18_random_walk_var.py
スクリプト冒頭の PEP 723 メタデータ(# /// script)により、必要なライブラリは
uv が自動的に仮想環境へ導入します。事前の pip install は不要です。
解説
何をするプログラムか
株価を幾何ブラウン運動(GBM)でモデル化し、5 万通りの 1 年間(252 営業日)の値動きをシミュレートして、投資のリスクを多面的に測るプログラムです。銀行や運用会社が日常的に使うリスク指標 VaR(バリュー・アット・リスク)と期待ショートフォール(ES)を、1 億円のポジションに対して計算します。
さらに、期末のリターンだけでは見えない「途中でどれだけ沈んだか」(最大ドローダウン)、および「長く持てば安全か」という時間分散効果の論点まで検証し、リスクを 1 つの数字で語ることの危うさを体感させる構成になっています。
コードの読みどころ
simulate_gbm()はrng.standard_normal((n_paths, days))で 5 万 × 252 の乱数行列を一括生成し、日次対数収益率をnp.cumsum(logret, axis=1)で累積してからnp.exp()に通します。価格の累積積を対数の累積和に置き換える、数値計算の定石です。histogram()はnp.histogramで度数を求め、'█' * int(c / mx * width)で棒を描く、外部ライブラリ不要のテキスト描画です。- VaR は
-np.quantile(daily, 1 - a) * positionと分位点そのもので定義され、ES はdaily[daily <= np.quantile(...)]というブールインデックスで裾のデータだけを取り出し、その条件付き平均として計算しています。定義とコードが 1 対 1 に対応しています。 - 最大ドローダウンは
np.maximum.accumulate(paths, axis=1)で各経路の「過去最高値」の系列を作り、そこからの下落率の最小値を取るだけで求まります。累積系のユニバーサル関数の好例です。
理論的背景
GBM では対数収益率が i.i.d. の正規分布に従い、価格は対数正規分布になります。VaR_α は損失分布の α 分位点、ES_α(CVaR)は VaR を超えた場合の損失の条件付き期待値です。VaR は「その先」の損失の大きさを無視するうえ、一般に劣加法性(分散投資でリスク指標が増えない性質)を満たしません。ES は劣加法性を含む整合的リスク尺度の公理を満たすことが知られています(Artzner et al. 1999)。バーゼル規制が VaR から ES へ移行した背景でもあります。
実行結果の見方
まず 1 年後価格の平均 3,183.2・中央値 3,094.1 が理論値(3,185.5 / 3,095.1)とほぼ一致し、平均>中央値という対数正規分布の歪みが出ている点を確認してください。「期待収益率 6%」は典型的な結果ではなく、分布の右裾に引っ張られた値です。
リスク指標の表では、どの信頼水準でも ES が VaR を上回ること(例: 99% で VaR 351 万円、ES 402 万円)が重要です。最大ドローダウンでは、期待収益率が正でも 58.5% の経路が途中 20% 以上の下落を経験します。最後の表は、保有期間を延ばすと元本割れ確率は 46%→28% に低下する一方、年率リターンの標準偏差の縮小と金額ベースの損失拡大が併存するという、時間分散効果論争(Samuelson 1963)の要点を示しています。
ソースコード
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "numpy",
# ]
# ///
"""18: 確率過程とリスク指標 -----------------------------------------------
テーマ: 幾何ブラウン運動による株価シミュレーションと VaR / 期待ショートフォール
学習点: NumPy の累積積, 分位点, ヒストグラムのテキスト描画, リスク指標の定義
根拠: GBM では対数収益率が i.i.d. 正規に従う。
VaR_α は損失分布の α 分位点、ES_α(期待ショートフォール, CVaR)は
VaR を超える損失の条件付き期待値。ES は劣加法性を満たす整合的
リスク尺度である一方、VaR は一般に満たさない(Artzner et al. 1999)。
"""
import numpy as np
rng = np.random.default_rng(2024)
def simulate_gbm(S0, mu, sigma, days, n_paths):
dt = 1 / 252
z = rng.standard_normal((n_paths, days))
logret = (mu - sigma ** 2 / 2) * dt + sigma * np.sqrt(dt) * z
return S0 * np.exp(np.cumsum(logret, axis=1)), logret
def histogram(data, bins=25, width=50):
counts, edges = np.histogram(data, bins=bins)
mx = counts.max()
for c, lo, hi in zip(counts, edges[:-1], edges[1:]):
print(f" {lo:>8.2%}〜{hi:>8.2%} |{'█' * int(c / mx * width)} {c}")
def main() -> None:
S0, mu, sigma, days, n = 3000.0, 0.06, 0.24, 252, 50_000
paths, logret = simulate_gbm(S0, mu, sigma, days, n)
final = paths[:, -1]
total_ret = final / S0 - 1
print(f"初期価格 {S0:,.0f} / 期待収益率 {mu:.1%} / ボラティリティ {sigma:.1%}")
print(f"シミュレーション {n:,} 本 × {days} 営業日\n")
print(f"1年後価格: 平均 {final.mean():,.1f} / 中央値 {np.median(final):,.1f}")
print(f" 理論平均 S0·e^(μT) = {S0 * np.exp(mu):,.1f}")
print(f" 理論中央値 S0·e^((μ-σ²/2)T) = {S0 * np.exp(mu - sigma**2/2):,.1f}")
print(" ※ 対数正規分布は右に歪むため 平均 > 中央値。"
"「期待収益率」を中央的な結果と誤読しない。\n")
print("[1年後リターンの分布]")
histogram(total_ret)
print("\n[リスク指標(保有期間1日, ポジション1億円)]")
position = 100_000_000
daily = logret[:, 0]
for a in [0.90, 0.95, 0.99, 0.999]:
var = -np.quantile(daily, 1 - a) * position
tail = daily[daily <= np.quantile(daily, 1 - a)]
es = -tail.mean() * position
print(f" 信頼水準 {a:>6.1%}: VaR = {var:>13,.0f}円 / "
f"ES = {es:>13,.0f}円")
print(" ※ ES は VaR を必ず上回り、裾の深さを反映する。")
print("\n[最大ドローダウン(経路依存リスク)]")
running_max = np.maximum.accumulate(paths, axis=1)
dd = (paths - running_max) / running_max
mdd = dd.min(axis=1)
print(f" 平均MDD = {mdd.mean():.2%} / 中央値 {np.median(mdd):.2%}")
for q in [0.05, 0.25, 0.50]:
print(f" 下位{q:>5.0%}のMDD = {np.quantile(mdd, q):.2%}")
print(f" 20%以上下落を経験した経路の割合 = {(mdd <= -0.20).mean():.1%}")
print(" ※ 期末リターンが正でも途中で大きく沈む経路が多数ある。")
print("\n[時間分散効果の検証] 保有期間とマイナスで終わる確率")
for years in [1, 3, 5, 10, 20]:
p, _ = simulate_gbm(S0, mu, sigma, 252 * years, 5_000)
print(f" {years:>2}年: 元本割れ確率 = {(p[:, -1] < S0).mean():>6.2%} / "
f"年率換算リターンの標準偏差 = "
f"{np.std(np.log(p[:, -1] / S0) / years):.3%}")
print(" ※ 年率リターンのばらつきは縮むが、金額ベースの損失幅は拡大する。")
print(" これが「時間分散効果」をめぐる論争(Samuelson 1963)の要点。")
if __name__ == "__main__":
main()
実行結果
初期価格 3,000 / 期待収益率 6.0% / ボラティリティ 24.0%
シミュレーション 50,000 本 × 252 営業日
1年後価格: 平均 3,183.2 / 中央値 3,094.1
理論平均 S0·e^(μT) = 3,185.5
理論中央値 S0·e^((μ-σ²/2)T) = 3,095.1
※ 対数正規分布は右に歪むため 平均 > 中央値。「期待収益率」を中央的な結果と誤読しない。
[1年後リターンの分布]
-61.04%〜 -50.11% | 55
-50.11%〜 -39.19% |███ 645
-39.19%〜 -28.27% |██████████████ 2651
-28.27%〜 -17.34% |███████████████████████████████ 5569
-17.34%〜 -6.42% |██████████████████████████████████████████████ 8281
-6.42%〜 4.50% |██████████████████████████████████████████████████ 8856
4.50%〜 15.43% |████████████████████████████████████████████ 7894
15.43%〜 26.35% |██████████████████████████████████ 6157
26.35%〜 37.27% |██████████████████████ 4061
37.27%〜 48.19% |██████████████ 2565
48.19%〜 59.12% |████████ 1516
59.12%〜 70.04% |████ 821
70.04%〜 80.96% |██ 469
80.96%〜 91.89% |█ 226
91.89%〜 102.81% | 121
102.81%〜 113.73% | 58
113.73%〜 124.65% | 31
124.65%〜 135.58% | 12
135.58%〜 146.50% | 6
146.50%〜 157.42% | 2
157.42%〜 168.35% | 1
168.35%〜 179.27% | 2
179.27%〜 190.19% | 0
190.19%〜 201.12% | 0
201.12%〜 212.04% | 1
[リスク指標(保有期間1日, ポジション1億円)]
信頼水準 90.0%: VaR = 1,929,181円 / ES = 2,639,445円
信頼水準 95.0%: VaR = 2,473,210円 / ES = 3,101,749円
信頼水準 99.0%: VaR = 3,514,002円 / ES = 4,022,414円
信頼水準 99.9%: VaR = 4,642,763円 / ES = 5,027,057円
※ ES は VaR を必ず上回り、裾の深さを反映する。
[最大ドローダウン(経路依存リスク)]
平均MDD = -23.22% / 中央値 -21.83%
下位 5%のMDD = -39.30%
下位 25%のMDD = -28.44%
下位 50%のMDD = -21.83%
20%以上下落を経験した経路の割合 = 58.5%
※ 期末リターンが正でも途中で大きく沈む経路が多数ある。
[時間分散効果の検証] 保有期間とマイナスで終わる確率
1年: 元本割れ確率 = 46.18% / 年率換算リターンの標準偏差 = 24.025%
3年: 元本割れ確率 = 40.70% / 年率換算リターンの標準偏差 = 14.011%
5年: 元本割れ確率 = 37.68% / 年率換算リターンの標準偏差 = 10.750%
10年: 元本割れ確率 = 34.30% / 年率換算リターンの標準偏差 = 7.474%
20年: 元本割れ確率 = 28.48% / 年率換算リターンの標準偏差 = 5.341%
※ 年率リターンのばらつきは縮むが、金額ベースの損失幅は拡大する。
これが「時間分散効果」をめぐる論争(Samuelson 1963)の要点。