ボラティリティ(volatility、価格変動の大きさ)は、株式分析でリスクを表す中心的な指標です。本記事では、日次リターンから ヒストリカルボラティリティ(historical volatility) を計算し、年率に換算するまでの流れを Python で整理します。

目次

  1. ヒストリカルボラティリティの定義
  2. サンプルデータの準備
  3. 全期間ボラティリティ
  4. ローリング(移動)ボラティリティ
  5. 可視化
  6. 窓幅の選び方
  7. EWMA(指数加重移動平均)による拡張

ヒストリカルボラティリティの定義

ヒストリカルボラティリティとは、過去のリターンの 標準偏差 を、年率に換算したものです。

σannual=σdaily×252\sigma_{\text{annual}} = \sigma_{\text{daily}} \times \sqrt{252}
  • σdaily\sigma_{\text{daily}} : 日次リターン(対数リターンが慣例)の標準偏差
  • 252: 1 年あたりの営業日数(米株・日本株とも目安)

「標準偏差そのもの」と「年率換算後の値」は別物です。論文・実務レポートで「ボラティリティ 20%」と書かれていれば、ほぼ常に 年率換算した値 を指します。

なぜ 252\sqrt{252} で掛けるのか

リターンが独立同分布(i.i.d.)に近いと仮定すると、TT 期間の合計リターンの分散は単期間の分散の TT 倍になります。標準偏差は分散の平方根なので、T\sqrt{T} で掛ける形になります。

Var(1+2++T)=TVar(t)\text{Var}(\ell_1 + \ell_2 + \cdots + \ell_T) = T \cdot \text{Var}(\ell_t)

実際のリターンには自己相関やボラティリティクラスタリングがあるため、この近似は完全ではありません。それでも、業界標準の出発点として広く使われます。

サンプルデータの準備

裾の厚い t 分布から、約 4 年分の日次対数リターンを生成します。実データを使う場合は J-Quants 経由(#6-5「日次株価四本値を取得する (/equities/bars/daily)」)で取得した対数リターン列に置き換えてください。

import numpy as np
import pandas as pd
rng = np.random.default_rng(seed=7)
n_days = 1000
returns = pd.Series(
rng.standard_t(df=6, size=n_days) * 0.012,
index=pd.date_range("2022-01-04", periods=n_days, freq="B"),
name="log_return",
)

全期間ボラティリティ

まず期間全体の標準偏差をとり、年率換算します。

TRADING_DAYS = 252 # 1 年あたりの営業日数の慣例
daily_vol = returns.std(ddof=0)
annual_vol = daily_vol * np.sqrt(TRADING_DAYS)
print(f"日次ボラ: {daily_vol:.6f}")
print(f"年率ボラ: {annual_vol:.4f} ({annual_vol * 100:.2f}%)")

ddof=0 は母標準偏差(分母 nn)、ddof=1 は不偏標準偏差(分母 n1n-1)です。サンプルサイズが大きいときの差はわずかですが、慣例として ddof=0 を採用する文献と ddof=1 を採用する文献が混在します。本記事では母標準偏差で統一します。

ローリング(移動)ボラティリティ

直近の窓幅 ww 営業日だけを使った標準偏差を、日々ローリングで計算するのが ローリングボラティリティ です。市場のレジーム変化を捉えやすくなります。

WINDOW = 60 # 約 3 か月
rolling_vol_daily = returns.rolling(WINDOW).std(ddof=0)
rolling_vol_annual = rolling_vol_daily * np.sqrt(TRADING_DAYS)
print(rolling_vol_annual.tail())

最初の w1w-1 日は窓が埋まらないので NaN になります。これを「過去のデータ不足」として扱い、可視化や指標計算では dropna() で落とすのが安全です。

可視化

ローリング年率ボラを折れ線で確認します。

import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(rolling_vol_annual.index, rolling_vol_annual, color="tab:blue")
ax.axhline(annual_vol, color="black", linestyle="--", linewidth=0.8, label="full-period")
ax.set_title(f"Rolling annualized volatility (window={WINDOW})")
ax.set_ylabel("annualized vol")
ax.set_xlabel("Date")
ax.grid(alpha=0.3)
ax.legend()
plt.tight_layout()
plt.savefig("rolling_vol.png", dpi=120)
plt.close(fig)
60日窓のローリング年率ボラティリティ推移

線が水平線(全期間の年率ボラ)から大きく外れる時期は、市場が「いつもと違う」状態にあった期間と解釈できます。

窓幅の選び方

窓幅 ww は次のトレードオフで決めます。

窓幅特徴向いている用途
20 日(約 1 か月)反応が速い・ノイズが多い直近の急変を捉える
60 日(約 3 か月)中庸。標準的月次リバランス・銘柄比較
252 日(約 1 年)滑らか・反応が遅い長期的なリスク水準の把握

短すぎる窓は「たまたまの数日」に振り回され、長すぎる窓は「もう関係ない過去」を引きずります。1 つの窓幅に固定せず、複数の窓幅を重ねて確認するのが実務上は無難です。

EWMA(指数加重移動平均)による拡張

直近のリターンに重みを大きく置く方法として、EWMA(exponentially weighted moving average)があります。

LAMBDA = 0.94 # RiskMetrics で広く使われる係数
ewma_var = returns.ewm(alpha=1 - LAMBDA, adjust=False).var(bias=True)
ewma_vol_annual = np.sqrt(ewma_var) * np.sqrt(TRADING_DAYS)
print(ewma_vol_annual.tail())

alpha = 1 - λ という変換に注意します。λ = 0.94 は実効的な窓幅が約 25 日に相当し、市場のレジーム変化に追従しやすい設定です。

注意点

  • 対数リターンを使うのが慣例: 単純リターンでも近似的には同じ値になりますが、合算性の良い対数リターンが標準です
  • 異常値の扱い: 株式分割やストップ高/安の影響が混じると、極端な値で標準偏差が膨らみます。調整済み終値の利用を確認します
  • 年率換算は近似: 自己相関・クラスタリングがある実際のリターンでは、252\sqrt{252} ルールはあくまで出発点です
  • 過去は将来を保証しない: 過去のボラティリティが小さくても、将来も小さい保証はありません

生成AI へのプロンプト例

複数銘柄に対してボラティリティ表をまとめて出したいときの例です。

入力 DataFrame に次の列があります:
- date (datetime64)
- ticker (str)
- log_return (float)
各 ticker について、次の列を持つ DataFrame を返す関数
volatility_table(df, window=60) を書いてください。
戻り値の列:
- ticker
- vol_full_annual: 全期間の年率ボラ
- vol_rolling_last: 直近の window 日ローリング年率ボラ
- vol_ewma_last: λ=0.94 の EWMA 年率ボラの最終値
要件:
- pandas 2.2 / numpy 系
- 営業日数は 252 で年率換算
- ddof=0 で標準偏差を取る
- docstring を日本語で書く

まとめ

  • ヒストリカルボラティリティは「日次標準偏差 × 252\sqrt{252}」で年率に換算する
  • 全期間とローリングを併用し、レジーム変化を確認する
  • 窓幅は 20 / 60 / 252 を併用するのが実務的
  • EWMA は直近の動きに敏感で、反応性と滑らかさのバランスが良い
  • T\sqrt{T} ルールは i.i.d. 仮定の近似であり、限界を理解して使う