Pythonで株価のボラティリティ(変動率)から適切な購入枚数を計算する方法

自動化・運用

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

「この銘柄、値動きが激しいけど何株買えばいいのか」——感覚で枚数を決めてしまい、想定以上の損失を出した経験かもしれません。

ボラティリティ(Volatility:変動率)は、銘柄の「荒れ具合」を数値化した指標です。この数値を使えば、1トレードあたりのリスク金額を一定に保つポジションサイジング(Position Sizing)が実現できます。

📘 外部参考Volatility(Investopedia)ボラティリティ(Wikipedia 日本語)

しかし実際には「標準偏差の計算方法がわからない」「年率換算と日次の違いが曖昧」「算出した数値をどう枚数に落とし込むかが不明」といった壁にぶつかる人が大半です。

原因は、ボラティリティの数学的な定義と、それを実トレードの資金管理に接続するロジックが別々に解説されていることにあります。両者をつなぐ具体的な計算式が欠けているのです。

ということで、この記事では、Pythonで株価のヒストリカルボラティリティ(Historical Volatility)を算出し、その値から「口座資金の何%をリスクにさらすか」に基づいて適切な購入枚数を自動計算するコードを提供します。

📘 外部参考Python 公式Python 公式ドキュメント(日本語)

コードはすべてコピペで動作します。銘柄コードやリスク許容率を自分の運用ルールに合わせて変更し、すぐに実践へ移してください。

ボラティリティとポジションサイジングの基礎

標準偏差で測る「荒れ具合」の正体

ボラティリティとは、株価のリターン(日次変動率)のばらつき度合いを示す統計量です。具体的には、日次対数リターンの標準偏差(Standard Deviation)として算出します。

標準偏差が大きい銘柄ほど、1日の値動き幅が大きくなります。たとえば日次ボラティリティが2%の銘柄は、約68%の確率で前日終値から±2%の範囲に収まることを意味します。

年率ボラティリティに換算する場合は、日次ボラティリティに√250(年間営業日数の平方根)を掛けます。年率20%と年率40%の銘柄では、同じ金額を投じた場合のリスクが2倍異なる点を押さえてください。

ポジションサイジングの基本公式

ポジションサイジングとは、1トレードあたりの購入枚数をリスク許容額から逆算する手法です。基本公式は以下のとおりです。

項目 計算式
1トレードのリスク許容額 口座資金 × リスク許容率(例:2%)
1株あたりのリスク金額 現在株価 × 日次ボラティリティ × ボラティリティ倍率
購入株数 リスク許容額 ÷ 1株あたりのリスク金額

リスク許容率は1〜3%が一般的な目安です。5%を超える設定は、連敗時に口座資金が急速に減少するため危険です。

【コピペOK】ボラティリティ算出と枚数計算のメインコード


pip install yfinance pandas numpy matplotlib japanize-matplotlib

# ==============================
# ボラティリティ算出 & ポジションサイジング計算
# ==============================

import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
import japanize_matplotlib  # noqa: F401

# ==============================
# 設定エリア
# ==============================
TICKER: str = "7203.T"            # 銘柄コード(トヨタ自動車)
PERIOD: str = "1y"                # データ取得期間(1年)
VOL_WINDOW: int = 20              # ボラティリティ算出の移動窓(営業日)
ANNUAL_TRADING_DAYS: int = 250    # 年間営業日数
ACCOUNT_BALANCE: float = 1_000_000.0  # 口座資金(円)
RISK_PER_TRADE: float = 0.02     # 1トレードあたりのリスク許容率(2%)
VOL_MULTIPLIER: float = 2.0      # ボラティリティ倍率(損切り幅の目安)
LOT_UNIT: int = 100              # 売買単位(日本株は通常100株)


# ==============================
# 関数定義: データ取得
# ==============================
def fetch_stock_data(ticker: str, period: str) -> pd.DataFrame:
    '株価データを取得しDataFrameで返す'
    df: pd.DataFrame = yf.download(ticker, period=period, interval="1d", progress=False)
    if df.empty:
        raise ValueError(f"データを取得できませんでした: {ticker}")
    if isinstance(df.columns, pd.MultiIndex):
        df.columns = df.columns.get_level_values(0)
    return df


# ==============================
# 関数定義: 対数リターン算出
# ==============================
def calc_log_returns(df: pd.DataFrame) -> pd.Series:
    '日次対数リターンを算出する'
    log_returns: pd.Series = np.log(df["Close"] / df["Close"].shift(1))
    return log_returns


# ==============================
# 関数定義: ヒストリカルボラティリティ算出
# ==============================
def calc_historical_volatility(
    log_returns: pd.Series,
    window: int,
    annual_days: int,
) -> dict[str, pd.Series | float]:
    '日次・年率のヒストリカルボラティリティを算出する'
    daily_vol: pd.Series = log_returns.rolling(window=window).std()
    annual_vol: pd.Series = daily_vol * np.sqrt(annual_days)
    latest_daily: float = float(daily_vol.dropna().iloc[-1])
    latest_annual: float = float(annual_vol.dropna().iloc[-1])
    return {
        "daily_vol_series": daily_vol,
        "annual_vol_series": annual_vol,
        "latest_daily": latest_daily,
        "latest_annual": latest_annual,
    }


# ==============================
# 関数定義: ポジションサイジング
# ==============================
def calc_position_size(
    account_balance: float,
    risk_per_trade: float,
    current_price: float,
    daily_volatility: float,
    vol_multiplier: float,
    lot_unit: int,
) -> dict[str, float | int]:
    'ボラティリティに基づく購入株数を算出する'
    risk_amount: float = account_balance * risk_per_trade
    risk_per_share: float = current_price * daily_volatility * vol_multiplier
    if risk_per_share <= 0:
        raise ValueError("1株あたりリスク金額が0以下です。パラメータを確認してください。")
    raw_shares: float = risk_amount / risk_per_share
    lot_shares: int = int(raw_shares // lot_unit) * lot_unit
    actual_risk: float = lot_shares * risk_per_share
    actual_risk_pct: float = (actual_risk / account_balance) * 100
    investment_amount: float = lot_shares * current_price
    return {
        "risk_amount": risk_amount,
        "risk_per_share": risk_per_share,
        "raw_shares": raw_shares,
        "lot_shares": lot_shares,
        "actual_risk": actual_risk,
        "actual_risk_pct": actual_risk_pct,
        "investment_amount": investment_amount,
    }


# ==============================
# 関数定義: 結果表示
# ==============================
def print_result(
    ticker: str,
    current_price: float,
    vol_data: dict,
    pos_data: dict,
) -> None:
    '算出結果をコンソールに表示する'
    print("=" * 50)
    print(f"銘柄: {ticker}")
    print(f"現在株価: {current_price:,.2f} 円")
    print(f"日次ボラティリティ: {vol_data['latest_daily'] * 100:.3f}%")
    print(f"年率ボラティリティ: {vol_data['latest_annual'] * 100:.2f}%")
    print("-" * 50)
    print(f"口座資金: {pos_data['risk_amount'] / (RISK_PER_TRADE):,.0f} 円")
    print(f"リスク許容額 ({RISK_PER_TRADE*100:.1f}%): {pos_data['risk_amount']:,.0f} 円")
    print(f"1株あたりリスク: {pos_data['risk_per_share']:,.2f} 円")
    print(f"理論株数: {pos_data['raw_shares']:.1f} 株")
    print(f"購入株数 (単元調整後): {pos_data['lot_shares']:,} 株")
    print(f"必要投資額: {pos_data['investment_amount']:,.0f} 円")
    print(f"実効リスク額: {pos_data['actual_risk']:,.0f} 円 ({pos_data['actual_risk_pct']:.2f}%)")
    print("=" * 50)


# ==============================
# 関数定義: チャート描画
# ==============================
def plot_volatility(
    df: pd.DataFrame,
    vol_data: dict,
    ticker: str,
    window: int,
) -> None:
    '株価とボラティリティの推移をグラフ表示する'
    fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True)

    axes[0].plot(df.index, df["Close"], color="black", linewidth=1.0)
    axes[0].set_title(f"{ticker} 終値推移")
    axes[0].set_ylabel("株価(円)")
    axes[0].grid(True, alpha=0.3)

    axes[1].plot(
        df.index,
        vol_data["daily_vol_series"] * 100,
        color="steelblue",
        linewidth=1.0,
    )
    axes[1].set_title(f"日次ボラティリティ({window}日移動窓)")
    axes[1].set_ylabel("ボラティリティ(%)")
    axes[1].grid(True, alpha=0.3)

    axes[2].plot(
        df.index,
        vol_data["annual_vol_series"] * 100,
        color="crimson",
        linewidth=1.0,
    )
    axes[2].set_title(f"年率ボラティリティ({window}日移動窓)")
    axes[2].set_ylabel("ボラティリティ(%)")
    axes[2].grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()


# ==============================
# メイン処理
# ==============================
if __name__ == "__main__":
    df: pd.DataFrame = fetch_stock_data(TICKER, PERIOD)
    log_returns: pd.Series = calc_log_returns(df)
    vol_data: dict = calc_historical_volatility(log_returns, VOL_WINDOW, ANNUAL_TRADING_DAYS)
    current_price: float = float(df["Close"].iloc[-1])
    pos_data: dict = calc_position_size(
        ACCOUNT_BALANCE, RISK_PER_TRADE, current_price,
        vol_data["latest_daily"], VOL_MULTIPLIER, LOT_UNIT,
    )
    print_result(TICKER, current_price, vol_data, pos_data)
    plot_volatility(df, vol_data, TICKER, VOL_WINDOW)

コードの処理フロー解説

上記のコードは、以下の6ステップで構成されています。

* fetch_stock_data関数でyfinance経由の株価データを1年分取得する

* calc_log_returns関数で日次の対数リターン(自然対数の差分)を算出する

* calc_historical_volatility関数で20日移動窓の標準偏差を求め、日次・年率のボラティリティ時系列と最新値を返す

* calc_position_size関数で口座資金×リスク許容率からリスク許容額を算出し、1株あたりリスク金額で割ることで理論株数を得る。さらに売買単位(100株)で切り捨て調整する

* print_result関数で計算結果を一覧形式でコンソールに出力する

* plot_volatility関数で終値・日次ボラティリティ・年率ボラティリティの3段チャートを描画する

VOL_MULTIPLIERを1.0に下げれば損切り幅が狭くなり、株数が増えます。逆に3.0に上げればより保守的な枚数になります。自分の損切りルールに合わせて調整してください。

算出結果の読み解き方と実践的な判断基準

ボラティリティの水準別リスク評価

算出された年率ボラティリティの水準によって、銘柄のリスク特性を分類できます。

年率ボラティリティ リスク評価 銘柄の傾向
15%未満 低リスク 大型安定株・公益セクター
15〜30% 中リスク 主要指数構成銘柄
30〜50% 高リスク グロース株・中小型株
50%超 超高リスク バイオ・新興テーマ株

年率ボラティリティ50%を超える銘柄は、1日で5%以上動くことが珍しくありません。このような銘柄で枚数管理を怠ると、数日の連敗で口座資金の大半を失います。

算出された株数が0株になるケース

ボラティリティが高い銘柄、または口座資金に対して株価が高い銘柄では、単元調整後の購入株数が0株になることがあります。

これは「今の資金規模ではリスク管理上トレードすべきでない」というシグナルです。0株と表示された場合は、その銘柄への投資を見送るか、口座資金を増やしてから再検討してください。無理に枚数を1単元にして購入すると、許容リスクを超えた取引になるため危険です。

【コピペOK】複数銘柄の一括比較とリスク配分の最適化


# ==============================
# 複数銘柄ボラティリティ一括比較
# ==============================

import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
import japanize_matplotlib  # noqa: F401

# ==============================
# 設定エリア
# ==============================
TICKER_LIST: list[str] = ["7203.T", "9984.T", "6758.T", "8306.T", "4502.T"]
COMPARE_PERIOD: str = "1y"
COMPARE_VOL_WINDOW: int = 20
COMPARE_ANNUAL_DAYS: int = 250
COMPARE_ACCOUNT: float = 3_000_000.0
COMPARE_RISK_PCT: float = 0.02
COMPARE_VOL_MULT: float = 2.0
COMPARE_LOT: int = 100


# ==============================
# 関数定義: 一括分析
# ==============================
def batch_volatility_analysis(
    tickers: list[str],
    period: str,
    window: int,
    annual_days: int,
    account: float,
    risk_pct: float,
    vol_mult: float,
    lot_unit: int,
) -> pd.DataFrame:
    '複数銘柄のボラティリティと推奨枚数を一括算出する'
    rows: list[dict] = []
    for ticker in tickers:
        try:
            df: pd.DataFrame = yf.download(ticker, period=period, interval="1d", progress=False)
            if df.empty:
                continue
            if isinstance(df.columns, pd.MultiIndex):
                df.columns = df.columns.get_level_values(0)
            log_ret: pd.Series = np.log(df["Close"] / df["Close"].shift(1))
            daily_vol: float = float(log_ret.rolling(window=window).std().dropna().iloc[-1])
            annual_vol: float = daily_vol * np.sqrt(annual_days)
            price: float = float(df["Close"].iloc[-1])
            risk_amount: float = account * risk_pct
            risk_per_share: float = price * daily_vol * vol_mult
            raw_shares: float = risk_amount / risk_per_share if risk_per_share > 0 else 0
            lot_shares: int = int(raw_shares // lot_unit) * lot_unit
            invest: float = lot_shares * price
            rows.append({
                "銘柄": ticker,
                "株価": round(price, 2),
                "日次Vol(%)": round(daily_vol * 100, 3),
                "年率Vol(%)": round(annual_vol * 100, 2),
                "推奨株数": lot_shares,
                "投資額": round(invest, 0),
                "リスク額": round(lot_shares * risk_per_share, 0),
            })
        except Exception as e:
            print(f"エラー ({ticker}): {e}")
    return pd.DataFrame(rows)


# ==============================
# 関数定義: 比較チャート描画
# ==============================
def plot_comparison(result_df: pd.DataFrame) -> None:
    '銘柄別のボラティリティと推奨株数を棒グラフで比較する'
    fig, axes = plt.subplots(1, 2, figsize=(14, 5))

    axes[0].barh(result_df["銘柄"], result_df["年率Vol(%)"], color="steelblue")
    axes[0].set_xlabel("年率ボラティリティ(%)")
    axes[0].set_title("銘柄別 年率ボラティリティ比較")
    axes[0].grid(True, alpha=0.3, axis="x")

    axes[1].barh(result_df["銘柄"], result_df["推奨株数"], color="darkorange")
    axes[1].set_xlabel("推奨購入株数")
    axes[1].set_title("銘柄別 推奨株数(リスク2%基準)")
    axes[1].grid(True, alpha=0.3, axis="x")

    plt.tight_layout()
    plt.show()


# ==============================
# メイン処理
# ==============================
if __name__ == "__main__":
    result: pd.DataFrame = batch_volatility_analysis(
        TICKER_LIST, COMPARE_PERIOD, COMPARE_VOL_WINDOW,
        COMPARE_ANNUAL_DAYS, COMPARE_ACCOUNT, COMPARE_RISK_PCT,
        COMPARE_VOL_MULT, COMPARE_LOT,
    )
    print(result.to_string(index=False))
    print(f"n合計投資額: {result['投資額'].sum():,.0f} 円")
    print(f"合計リスク額: {result['リスク額'].sum():,.0f} 円")
    print(f"口座資金に対するリスク比率: {result['リスク額'].sum() / COMPARE_ACCOUNT * 100:.2f}%")
    plot_comparison(result)

コードの処理フロー解説

上記のコードは、以下の3ステップで構成されています。

* batch_volatility_analysis関数で5銘柄のデータを順次取得し、各銘柄のボラティリティ・推奨株数・投資額・リスク額を一括算出してDataFrameにまとめる

* メイン処理で一覧表を出力し、ポートフォリオ全体の合計投資額・合計リスク額・口座資金に対するリスク比率を表示する

* plot_comparison関数で年率ボラティリティと推奨株数の横棒グラフを並べて描画し、銘柄間の比較を視覚化する

合計リスク比率が10%を大きく超える場合は、銘柄数を減らすかCOMPARE_RISK_PCTを下げてポートフォリオ全体のリスクを調整してください。

よくあるエラーと対処法

ValueError: データを取得できませんでした

yfinanceがYahoo Financeからデータを取得できなかった場合に発生します。主な原因は銘柄コードの記述ミス、または市場が休場中でデータが空で返されることです。

以下を試してください。

* 東証銘柄は7203.Tのようにサフィックス.Tを付けているか確認する

* Yahoo Financeのサイトで当該銘柄コードが存在するか直接検索して確認する

* ネットワーク接続に問題がないか確認し、時間をおいて再実行する

ZeroDivisionErrorが枚数計算で発生する

risk_per_share(1株あたりリスク金額)が0になった場合に発生します。原因は、取得データの日数がVOL_WINDOW(20日)に満たず、ボラティリティがNaNとして算出されることです。

以下を試してください。

* PERIOD"6mo"以上に設定し、十分な日数のデータを確保する

* VOL_WINDOWの値をデータ件数より小さい値に設定する

* 上場直後の銘柄などデータが少ない場合は分析対象から除外する

グラフの日本語が文字化け(□□□)になる

japanize_matplotlibのインストール漏れ、またはフォントキャッシュの不整合が原因です。Matplotlib(マットプロットリブ)はデフォルトで日本語フォントを含まないため、明示的にフォント設定が必要です。

以下を試してください。

* pip install japanize-matplotlibを再実行し、ライブラリが正しくインストールされていることを確認する

* Matplotlibのフォントキャッシュをrm -rf ~/.cache/matplotlib(Mac/Linux)で削除して再起動する

* それでも解消しない場合はplt.rcParams["font.family"] = "IPAexGothic"のようにフォント名を直接指定する

まとめ

この記事では、Pythonでヒストリカルボラティリティを算出し、リスク許容率に基づいて適切な購入株数を自動計算する方法を解説しました。

実際に使ってみた要点をまとめます。

* ボラティリティは日次対数リターンの標準偏差で算出し、√250を掛けて年率に換算する

* ポジションサイジングの基本公式は「リスク許容額 ÷ 1株あたりリスク金額 = 購入株数」である

* VOL_MULTIPLIER(ボラティリティ倍率)を調整することで、損切り幅に応じた枚数制御ができる

* 算出株数が0株になった場合は「トレードすべきでない」というリスク管理上のシグナルとして受け取る

* 複数銘柄を一括比較する際は、ポートフォリオ全体のリスク比率が10%を超えないよう管理する

また、ATR(Average True Range:真の値幅の平均)をボラティリティ指標に置き換えたポジションサイジングを試してみるといいかと思います。ATRはギャップ(窓開け)を考慮した値幅指標であり、ヒストリカルボラティリティとは異なる角度からリスクを評価できます。

また、今回のコードを定期的に実行してCSVに記録すれば、ボラティリティの変化に応じて枚数を動的に調整する運用ルールを構築できます。固定枚数から数値ベースの管理に切り替えると、1トレードあたりのリスクが安定するかと思います。

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