戦略をやめる基準の決め方:ドローダウンは「想定内」か「崩壊」か


戦略が負けているとき、私たちは2つの間違いのどちらかを犯します。

  • 早すぎる撤退:正常な調整局面で怖くなってやめる → 回復を取り逃す
  • 遅すぎる撤退:明らかに壊れているのに「いつか戻る」と持ち続ける → 傷が広がる

この2つを分けるのは根性でも直感でもなく、事前に決めた数値基準です。この記事では、撤退ラインの決め方をPythonで解説します。

⚠️ 本記事は教育目的の情報共有です。投資は自己責任で行ってください。

目次

  1. 「想定内の負け」を先に知っておく
  2. モンテカルロで最悪シナリオを計算する
  3. 撤退トリガーの3類型
  4. 実装:運用中の戦略を毎月チェックする
  5. 撤退基準を守るための仕組み
  6. まとめ

1. 「想定内の負け」を先に知っておく

そもそも、あなたの戦略はどれくらい負けることが正常なのでしょうか。

これを知らずに運用を始めると、正常な範囲の下落でも「壊れた!」と感じます。逆に、異常な下落を「まだ想定内のはず」と正当化してしまうこともあります。

だから運用開始前に、こう決めておきます。

この戦略は、正常に機能していても最大で X% のドローダウンと、Yヶ月 の低迷が起こりうる。それを超えたら「壊れた可能性」を疑う。

XとYを勘で決めないために、モンテカルロを使います。

2. モンテカルロで最悪シナリオを計算する

バックテストの結果は「過去に実際に起きた1本の道筋」にすぎません。同じ戦略でも、リターンの並び順が変わればドローダウンは大きく変わります。

そこで、過去のリターンをシャッフルして「あり得た別の未来」を大量に作ります。

import numpy as np
import pandas as pd

def monte_carlo_drawdown(returns: pd.Series, n_sims: int = 5000,
                         horizon: int = 252) -> pd.Series:
    """リターンをブートストラップして最大DDの分布を得る

    returns: 戦略の日次リターン系列
    horizon: シミュレーションする営業日数(252 = 1年)
    """
    r = returns.dropna().values
    rng = np.random.default_rng(42)
    results = []
    for _ in range(n_sims):
        sample = rng.choice(r, size=horizon, replace=True)
        cum = np.cumprod(1 + sample)
        peak = np.maximum.accumulate(cum)
        dd = ((cum - peak) / peak).min()
        results.append(dd * 100)
    return pd.Series(results)

# 使い方
dd_dist = monte_carlo_drawdown(strategy_returns)
print(f"中央値      : {dd_dist.median():.1f}%")
print(f"下位5%点    : {dd_dist.quantile(0.05):.1f}%")   # 20回に1回はここまで
print(f"下位1%点    : {dd_dist.quantile(0.01):.1f}%")   # 100回に1回

出力例:

中央値      : -18.3%
下位5%点    : -34.7%
下位1%点    : -46.2%

この場合、-35%程度のドローダウンは「20年に1回」ではなく「20回のうち1回」起こりうる、正常範囲の出来事です。ここで慌てて撤退するのは早すぎます。

一方、-50%を超えたら「分布の外」=戦略が想定した性質を失った可能性が高い、と判断できます。

💡 ブートストラップは順序をシャッフルするため、トレンドの継続性(自己相関)を壊します。モメンタム系では最悪DDを楽観的に見積もる傾向があるので、出た数字は少し厳しめに解釈するのが安全です。

3. 撤退トリガーの3類型

数値基準は、性質の違う3つを併用すると漏れが減ります。

① 深さ(ドローダウン)

最大DDがモンテカルロの下位1%点を超えたら停止

一撃の大きさを見る基準。分かりやすく、実装も簡単。

② 長さ(低迷期間)

過去高値を N ヶ月以上更新できなければ再検証

「じわじわ死んでいく」タイプの劣化を捉えます。深さより長さの方が、戦略の陳腐化を早く検知できることが多いです。

def months_underwater(equity: pd.Series) -> int:
    """直近の高値からの経過月数"""
    peak_date = equity.idxmax()
    return (equity.index[-1] - peak_date).days // 30

③ 前提の崩壊(定性)

戦略が依存している前提そのものが変わったか

例:

  • 手数料無料化で、コスト前提が変わった
  • 対象市場の流動性が構造的に落ちた
  • 同じ戦略が広く知られ、優位性が消えた(アルファの減衰)

数値化できませんが、①②が発火したときに必ず点検する項目として持っておきます。

4. 実装:運用中の戦略を毎月チェックする

def health_check(equity: pd.Series, dd_limit: float,
                 max_underwater_months: int = 18) -> dict:
    """月次で回す健康診断"""
    cum = equity / equity.iloc[0]
    peak = cum.cummax()
    current_dd = ((cum.iloc[-1] - peak.iloc[-1]) / peak.iloc[-1]) * 100
    uw = months_underwater(equity)

    alerts = []
    if current_dd < dd_limit:
        alerts.append(f"DD {current_dd:.1f}% が撤退ライン {dd_limit:.1f}% を突破")
    if uw >= max_underwater_months:
        alerts.append(f"高値未更新 {uw}ヶ月(基準 {max_underwater_months}ヶ月)")

    return {
        "現在DD(%)": round(current_dd, 1),
        "高値未更新(月)": uw,
        "判定": "要再検証" if alerts else "継続",
        "警告": alerts,
    }

重要なのは、この閾値を運用開始前に決めて記録しておくことです。下落の最中に決めた基準は、必ず自分に甘くなります。

5. 撤退基準を守るための仕組み

基準を決めても、守れなければ意味がありません。人間は含み損を抱えると判断が歪みます。

  • 紙に書いて日付を入れる:「2026年X月、DD-45%または高値未更新18ヶ月で再検証」と運用開始前に記録
  • 判定は月次の決まった日に:毎日眺めると、下落中の判断を毎日迫られることになります
  • 「撤退」=「全否定」ではない:ロットを半分にする、レジームフィルタを追加する、ペーパーに戻す、など段階的な選択肢を用意しておく
  • 痛みの直後にルールを変えない:損失の直後の変更は、たいてい過剰最適化の入り口です。「改善候補」としてメモし、次の検証区切りで判断する

私自身、運用中の戦略が想定外に沈んだときも、その場でパラメータを触らないことを最優先しています。検証中に手を入れたら、それまでの検証データが無価値になるからです。

6. まとめ

  • 「耐えるか撤退か」は根性ではなく事前に決めた数値で判断する
  • モンテカルロで「正常な負け方」の分布を先に知る(下位1%点=分布の外)
  • トリガーは深さ(DD)・長さ(高値未更新)・前提の崩壊の3類型
  • 閾値は運用開始前に決めて記録する(下落中に決めると必ず甘くなる)
  • 撤退は全否定ではない。ロット縮小・フィルタ追加・ペーパー復帰という段階がある

戦略を作るときに一番書き忘れるのが「やめる条件」です。最初に決めておけば、下落局面で一番貴重なもの——冷静さ——を守れます。

関連記事 → ウォークフォワード検証とは? / 投資botは「作った後」が本番


質問や感想は X (@QuantNobu) までどうぞ。

⚠️ 免責:本記事は教育目的の情報共有であり、投資助言ではありません。実際の投資判断はご自身の責任で行ってください。