Pythonで最大ドローダウンを自動計算するコード|資産を守るリスク管理

自動化・運用

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

バックテストで好成績を出した売買ロジックを、いざ実運用に投入しようとしている段階ではないでしょうか。勝率やリターンの数値に目を奪われがちですが、その裏で「最大いくら負けるのか」を把握していなければ、資産運用は成り立ちません。

📘 外部参考Backtesting.py(公式)Backtrader 公式

最大ドローダウン(Maximum Drawdown)は、資産曲線のピークから谷底までの最大下落率を示す指標です。この数値を知ることで、自分が耐えられるリスクの限界を事前に設定できます。

📘 外部参考Drawdown(Investopedia)

📘 外部参考Maximum Drawdown(Investopedia)

しかし、多くの個人投資家はリターンだけを見てロジックの優劣を判断しています。「年利30%」という成績の裏に「一時的に資産が50%減った期間がある」と知れば、そのロジックの印象は大きく変わるはずです。

原因は、ドローダウンの計算方法を知らないか、手作業での算出が面倒で後回しにしていることにあります。Excelでの累積最大値の追跡は煩雑で、銘柄や期間を変えるたびに手間が発生します。

本記事では、Pythonでドローダウンを自動計算し、最大ドローダウンの発生期間まで特定するコードを提供します。さらに、複数銘柄のドローダウンを一括比較する応用コードも解説します。

コードはすべてコピペで動作します。SBI証券で運用する銘柄のリスク評価ツールとして、売買ルールの設計に活用してください。

最大ドローダウンの基本概念とリスク管理上の位置づけ

最大ドローダウンが示すもの

最大ドローダウン(Maximum Drawdown、以下MDD)は、ある期間における資産の累積最高値からの最大下落幅を百分率で表した指標です。例えば、資産が100万円から60万円まで減少した場合、MDDは-40%になります。

MDDは「最悪のシナリオで資産がどれだけ減るか」を示します。リターンが高くてもMDDが大きいロジックは、途中で精神的に耐えられず損切りしてしまうリスクが高いと判断してください。

リスク管理における判断基準

一般的に、MDDが-20%を超えるロジックは個人投資家にとって高リスクとされます。プロのファンドでも-30%を超えると運用停止を検討する水準です。

MDDとリターンの比率である「カルマーレシオ(Calmar Ratio)」も重要です。年間リターン÷MDDの絶対値で算出し、1.0以上であれば「リスクに見合ったリターン」と評価されます。

【コピペOK】最大ドローダウンを算出・可視化するメインコード

まず、必要なライブラリをインストールしてください。


pip install yfinance pandas matplotlib japanize-matplotlib

以下がメインコードです。


import datetime
from typing import Optional

import pandas as pd
import yfinance as yf
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import japanize_matplotlib  # noqa: F401 – 日本語フォント有効化

# ==============================
# 設定エリア
# ==============================
TICKER: str = "7203.T"            # 分析対象の銘柄コード(例:トヨタ自動車)
START_DATE: str = "2020-01-01"    # データ取得開始日
END_DATE: str = "2025-12-31"      # データ取得終了日
INITIAL_CAPITAL: float = 1_000_000.0  # 初期資金(円)
OUTPUT_CHART: str = "drawdown_chart.png"


# ==============================
# データ取得
# ==============================
def fetch_close_data(
    ticker: str,
    start: str,
    end: str,
) -> pd.Series:
    ""銘柄の終値Seriesを取得して返す""
    df: pd.DataFrame = yf.download(
        ticker,
        start=start,
        end=end,
        auto_adjust=True,
        progress=False,
    )
    if df.empty:
        raise ValueError(f"{ticker} のデータ取得に失敗しました。ティッカーを確認してください。")
    close: pd.Series = df["Close"].squeeze()
    close.name = ticker
    return close


# ==============================
# 資産曲線の構築
# ==============================
def build_equity_curve(
    close: pd.Series,
    initial_capital: float,
) -> pd.Series:
    ""終値ベースの資産曲線を構築する(買い持ち前提)""
    normalized: pd.Series = close / close.iloc[0]
    equity: pd.Series = normalized * initial_capital
    equity.name = "equity"
    return equity


# ==============================
# ドローダウン系列の算出
# ==============================
def calc_drawdown_series(equity: pd.Series) -> pd.DataFrame:
    ""資産曲線からドローダウン率の時系列を算出する""
    cumulative_max: pd.Series = equity.cummax()
    drawdown: pd.Series = (equity - cumulative_max) / cumulative_max
    result: pd.DataFrame = pd.DataFrame({
        "equity": equity,
        "cumulative_max": cumulative_max,
        "drawdown": drawdown,
    })
    return result


# ==============================
# 最大ドローダウンの特定
# ==============================
def find_max_drawdown(dd_df: pd.DataFrame) -> dict:
    ""最大ドローダウンの値・開始日・底値日を辞書で返す""
    trough_idx = dd_df["drawdown"].idxmin()
    trough_date: datetime.date = trough_idx.date()
    mdd_value: float = round(dd_df.loc[trough_idx, "drawdown"] * 100, 2)

    # ピーク日を特定(底値日以前で資産が最大だった日)
    pre_trough: pd.DataFrame = dd_df.loc[:trough_idx]
    peak_idx = pre_trough["equity"].idxmax()
    peak_date: datetime.date = peak_idx.date()

    # 回復日を特定(底値日以降でピーク時の資産を回復した日)
    peak_equity: float = dd_df.loc[peak_idx, "equity"]
    post_trough: pd.DataFrame = dd_df.loc[trough_idx:]
    recovery: pd.DataFrame = post_trough[post_trough["equity"] >= peak_equity]
    recovery_date: Optional[datetime.date] = (
        recovery.index[0].date() if not recovery.empty else None
    )

    return {
        "mdd_pct": mdd_value,
        "peak_date": peak_date,
        "trough_date": trough_date,
        "recovery_date": recovery_date,
    }


# ==============================
# 可視化
# ==============================
def plot_drawdown(
    dd_df: pd.DataFrame,
    mdd_info: dict,
    ticker: str,
    filepath: str,
) -> None:
    ""資産曲線とドローダウンを上下2段チャートで保存する""
    fig, (ax1, ax2) = plt.subplots(
        nrows=2, ncols=1, figsize=(10, 7), sharex=True,
        gridspec_kw={"height_ratios": [2, 1]},
    )

    # 上段:資産曲線
    ax1.plot(dd_df.index, dd_df["equity"], linewidth=1.0, label="資産曲線")
    ax1.plot(dd_df.index, dd_df["cumulative_max"], linewidth=0.7,
             linestyle="--", color="gray", label="累積最高値")
    ax1.set_ylabel("資産額(円)")
    ax1.set_title(f"{ticker}  最大ドローダウン: {mdd_info['mdd_pct']}%")
    ax1.legend(loc="upper left")

    # 下段:ドローダウン率
    ax2.fill_between(dd_df.index, dd_df["drawdown"] * 100, 0,
                     color="red", alpha=0.3)
    ax2.plot(dd_df.index, dd_df["drawdown"] * 100, color="red", linewidth=0.8)
    ax2.set_ylabel("ドローダウン(%)")
    ax2.set_xlabel("日付")
    ax2.axhline(mdd_info["mdd_pct"], color="darkred", linestyle="--",
                linewidth=0.7, label=f"MDD {mdd_info['mdd_pct']}%")
    ax2.legend(loc="lower left")

    fig.tight_layout()
    fig.savefig(filepath, dpi=150)
    plt.close(fig)
    print(f"チャートを保存しました: {filepath}")


# ==============================
# 結果のコンソール出力
# ==============================
def print_summary(mdd_info: dict, ticker: str) -> None:
    ""最大ドローダウンの要約をコンソール出力する""
    print("=" * 40)
    print(f"銘柄: {ticker}")
    print(f"最大ドローダウン: {mdd_info['mdd_pct']}%")
    print(f"ピーク日: {mdd_info['peak_date']}")
    print(f"底値日:   {mdd_info['trough_date']}")
    recovery: str = str(mdd_info["recovery_date"]) if mdd_info["recovery_date"] else "未回復"
    print(f"回復日:   {recovery}")
    print("=" * 40)


# ==============================
# メイン処理
# ==============================
if __name__ == "__main__":
    # 1. データ取得
    close: pd.Series = fetch_close_data(TICKER, START_DATE, END_DATE)

    # 2. 資産曲線構築
    equity: pd.Series = build_equity_curve(close, INITIAL_CAPITAL)

    # 3. ドローダウン算出
    dd_df: pd.DataFrame = calc_drawdown_series(equity)

    # 4. 最大ドローダウン特定
    mdd_info: dict = find_max_drawdown(dd_df)

    # 5. 結果出力
    print_summary(mdd_info, TICKER)

    # 6. 可視化
    plot_drawdown(dd_df, mdd_info, TICKER, OUTPUT_CHART)

コードの処理フロー解説

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

* ステップ1 データ取得yf.download()で指定銘柄の終値を取得する。auto_adjust=Trueにより株式分割・配当を調整済みの価格が返される

* ステップ2 資産曲線構築:初期資金を基準に、買い持ち(Buy and Hold)した場合の資産推移を算出する

* ステップ3 ドローダウン算出cummax()で累積最高値を追跡し、各時点での下落率を計算する。この処理がドローダウン分析の核心部分である

* ステップ4 最大ドローダウン特定idxmin()で最大下落率の日付を取得し、ピーク日・底値日・回復日を逆算する

* ステップ5 結果出力:MDD値、ピーク日、底値日、回復日をコンソールに表示する

* ステップ6 可視化:上段に資産曲線と累積最高値、下段にドローダウン率を描画し、PNGで保存する

TICKER"^N225""9984.T"に変更すれば、任意の銘柄・指数に対して同じ分析を即座に実行できます。

出力結果の読み解き方と損切りルールへの応用

MDD値から損切りラインを設定する

算出されたMDDが-35%だった場合、「過去にこの銘柄は最大35%下落した実績がある」ことを意味します。実運用では、この値の1.2〜1.5倍を想定最大損失として見積もってください。

SBI証券の逆指値注文機能を使い、エントリー価格から想定最大損失の半分(例:-17.5%)で損切り注文を入れておくのが実践的です。MDDの半分で切ることで、最悪期を丸ごと被る事態を避けられます。

回復日の有無が示すリスク

回復日が「未回復」と表示された場合、ピーク時の資産にまだ戻っていないことを意味します。これは非常に重要な警告サインです。

回復に要した日数も確認してください。回復まで1年以上かかった銘柄は、資金拘束リスクが高いと判断するべきです。短期〜中期運用を前提とするなら、回復期間が6か月以内の銘柄を優先的に選定してください。

【コピペOK】複数銘柄のドローダウンを一括比較する応用コード


import pandas as pd
import yfinance as yf

# ==============================
# 設定エリア
# ==============================
TICKERS: list[str] = [
    "7203.T",   # トヨタ自動車
    "9984.T",   # ソフトバンクグループ
    "6758.T",   # ソニーグループ
    "8306.T",   # 三菱UFJフィナンシャル・グループ
]
START_DATE: str = "2020-01-01"
END_DATE: str = "2025-12-31"


# ==============================
# 単一銘柄のMDD算出
# ==============================
def calc_mdd_for_ticker(
    ticker: str,
    start: str,
    end: str,
) -> dict:
    ""銘柄のMDD・ピーク日・底値日を辞書で返す""
    df: pd.DataFrame = yf.download(
        ticker, start=start, end=end, auto_adjust=True, progress=False,
    )
    if df.empty:
        return {"銘柄": ticker, "MDD(%)": None, "ピーク日": None, "底値日": None}

    close: pd.Series = df["Close"].squeeze()
    cummax: pd.Series = close.cummax()
    drawdown: pd.Series = (close - cummax) / cummax

    trough_idx = drawdown.idxmin()
    mdd_pct: float = round(drawdown.loc[trough_idx] * 100, 2)
    peak_idx = close.loc[:trough_idx].idxmax()

    return {
        "銘柄": ticker,
        "MDD(%)": mdd_pct,
        "ピーク日": peak_idx.date(),
        "底値日": trough_idx.date(),
    }


# ==============================
# メイン処理
# ==============================
if __name__ == "__main__":
    records: list[dict] = []
    for t in TICKERS:
        result: dict = calc_mdd_for_ticker(t, START_DATE, END_DATE)
        records.append(result)

    comparison: pd.DataFrame = pd.DataFrame(records)
    comparison = comparison.sort_values("MDD(%)", ascending=True)
    print(comparison.to_string(index=False))

コードの処理フロー解説

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

* ステップ1 銘柄リスト定義TICKERSリストに比較したい銘柄コードを列挙する

* ステップ2 ループ処理:各銘柄について終値取得→累積最高値追跡→MDD算出を繰り返す

* ステップ3 比較テーブル出力:MDDの降順(下落が大きい順)でソートし、一覧表として出力する

出力イメージは以下のとおりです。

銘柄 MDD(%) ピーク日 底値日
9984.T -45.12 2021-02-16 2022-06-20
6758.T -32.87 2021-11-22 2022-10-13
7203.T -28.45 2024-03-22 2024-08-05
8306.T -22.31 2020-01-20 2020-03-16

TICKERSにETFコード(1321.Tなど)を追加すれば、ポートフォリオ全体のリスク比較にも応用できます。

よくあるエラーと対処法

IndexError: index 0 is out of bounds

データが空のSeriesに対してiloc[0]を実行した場合に発生します。指定した期間にデータが存在しない銘柄や、上場廃止銘柄でこのエラーが起こります。

以下を試してください。

* START_DATEを上場日以降に設定する

* df.emptyのチェックをfetch_close_data()内で行い、空の場合は明示的にエラーを出す

* 複数銘柄の一括処理ではtry-exceptで囲み、失敗した銘柄をスキップする

チャートのドローダウンが常に0%と表示される

データが1行しか取得できていないか、終値が全期間で同一値になっている可能性があります。auto_adjust=Trueの挙動がyfinanceのバージョンによって異なることが原因です。

📘 外部参考yfinance 公式GitHub

以下を試してください。

* print(close.head(10))でデータの中身を確認する

* yfinanceを最新版に更新する(pip install --upgrade yfinance

* auto_adjust=Falseに変更し、df["Adj Close"]カラムを使用する

SettingWithCopyWarningが表示される

pandasがデータのコピーと参照を区別できない場合の警告です。動作自体には影響しませんが、放置すると将来のバージョンでエラーに昇格する可能性があります。

📘 外部参考pandas User Guide(公式)

対処法として、代入先のDataFrameに.copy()を明示的に付与してください。例えばpre_trough = dd_df.loc[:trough_idx]pre_trough = dd_df.loc[:trough_idx].copy()に変更すれば警告は消えます。

まとめ

この記事では、Pythonを使って最大ドローダウン(MDD)を自動計算し、リスク管理に活かす方法を解説しました。

要点を整理します。

* MDDは資産の累積最高値からの最大下落率であり、cummax()を使えば数行で算出できる

* MDD-20%超は個人投資家にとって高リスク。損切りラインはMDDの半分を目安に設定する

* ピーク日・底値日・回復日の3点セットで分析すると、リスクの時間的な広がりを把握できる

* 複数銘柄のMDDを一覧比較することで、ポートフォリオ全体のリスク偏りを可視化できる

* カルマーレシオ(年間リターン÷MDD絶対値)が1.0以上のロジックを優先的に採用する

次のステップとして、自分のバックテスト結果の損益データをCSVで読み込み、買い持ちではなく実際の売買ロジックに基づいたMDDを算出してください。build_equity_curve()関数を、CSVの累積損益カラムを受け取る形に差し替えるだけで対応できます。

さらに、MDDが一定の閾値(例:-15%)を超えた時点で自動的にポジションサイズを縮小する「ドローダウン制御ロジック」を組み込めば、実運用レベルのリスク管理システムへと発展させられます。

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