ランダムウォークへの挑戦⑩ | Pythonで資産推移とドローダウンを分析する(2026/8/1)

未分類

はじめに

前回は、零号機の連勝・連敗を分析しました。

ランダムウォークへの挑戦⑨ | Pythonで取引の連勝・連敗を分析する(2026/7/28)
熊本で震度7の地震がありました。今年、西南戦争を巡る旅として熊本に旅行に行ったばかりなので、他人事とは思えず、大変驚いています。復旧作業中だった熊本城の石垣も一部崩れてしまったとのことで悲しい気持ちです。熊本でお世話になった郷土料理のお店や…

最大連勝・最大連敗だけでなく、「何連勝・何連敗がどのくらいの頻度で発生しているのか」を調べることで、零号機の取引結果の連続性を見ることができました。

しかし、運用時のリスクを考えるうえでは、連敗数だけでは十分ではありません。

例えば同じ5連敗でも、

-100円
-200円
-150円
-100円
-200円

と、

-2,000円
-3,000円
-2,500円
-3,000円
-2,000円

では、資産への影響がまったく違います。

そこで今回は、ドローダウンを分析します。

ドローダウンとは、資産がそれまでの最高地点からどの程度下落したのかを表すものです。

単に「最大でいくら負けたか」だけでなく、

  • どこから下落が始まったのか
  • どこで底をつけたのか
  • その後、元の最高値まで回復したのか
  • 回復まで何回の取引が必要だったのか

まで調べてみます。


今回確認したいこと

今回は、零号機について次の項目を確認します。

  • 取引ごとの累積損益
  • それまでの最高損益
  • 最高地点からの下落額
  • 最大ドローダウン
  • 最大ドローダウンの開始地点
  • 最大ドローダウンの底
  • 最高値を回復するまでの取引数・期間

また、分析結果をCSVとして保存するとともに、

「累積損益と最高損益」

「ドローダウンの推移」

をグラフにします。


ドローダウン分析の考え方

まず、今回どのようにドローダウンを求めるのか整理します。

例えば、取引による累積損益が次のように推移したとします。

10,000
15,000
20,000
18,000
12,000
16,000
21,000

3回目の取引で累積損益が20,000円になっています。

その後12,000円まで下落した場合、

12,000 - 20,000 = -8,000

なので、この時点のドローダウンは-8,000円です。

その後、累積損益が21,000円になれば、それまでの最高値20,000円を超えるため、ドローダウンから回復したことになります。

今回は、この処理をすべての取引についてPythonで計算します。


前回まで使用しているanalyze.pyに、ドローダウン分析を追加します。

① 決済時間順に並べる

drawdown_trades = (
    trades
    .sort_values("決済時間")
    .reset_index(drop=True)
    .copy()
)

ドローダウンは資産が時間とともにどう変化したかを見る分析なので、取引の順番が重要です。

そこで、

.sort_values("決済時間")

によって決済時間の古い順に並べます。

さらに、

.reset_index(drop=True)

で並べ替え前のindexを捨て、0から連番を振り直します。


② 取引順を付ける

drawdown_trades["取引順"] = range(
    1,
    len(drawdown_trades) + 1
)

各取引に、

1
2
3
4
...

という取引順を付けます。

これによって後から、

何回目の取引で最大ドローダウンが始まったのか

などを調べられるようになります。


③ 累積損益を計算する

drawdown_trades["累積損益"] = (
    drawdown_trades["損益"].cumsum()
)

.cumsum()は累積和(cumulative sum)を計算する機能です。

例えば損益が、

+100
+200
-50
+300

なら、

100
300
250
550

となります。

つまり、取引を重ねるごとに利益がどのように積み上がったのかを表します。


④ その時点までの最高値を求める

drawdown_trades["過去最高損益"] = (
    drawdown_trades["累積損益"].cummax()
)

.cummax()は、その行までに登場した最大値を取得します。

例えば、

累積損益
100
300
250
550
400

なら、

過去最高損益
100
300
300
550
550

となります。

250円まで下がっても、それ以前に300円を記録しているので過去最高値は300円のままです。


⑤ ドローダウンを計算する

drawdown_trades["ドローダウン"] = (
    drawdown_trades["累積損益"]
    - drawdown_trades["過去最高損益"]
)

現在の累積損益から、その時点までの最高損益を引きます。

例えば、

累積損益       250
過去最高損益   300

なら、

250 - 300 = -50

となります。

最高値を更新しているときは、

550 - 550 = 0

なので、ドローダウンは0です。

そのため、ドローダウンは基本的に、

0
または
マイナス

になります。


⑥ 最大ドローダウンを取得する

max_drawdown = (
    drawdown_trades["ドローダウン"].min()
)

ドローダウンはマイナスで表現しているため、最も小さい値が最も大きな下落です。

さらに、

max_drawdown_row = (
    drawdown_trades.loc[
        drawdown_trades["ドローダウン"].idxmin()
    ]
)

で、その最大ドローダウンが発生した行を取得します。

ここでは、

.min()

最小値そのもの

.idxmin()

最小値が存在するindex

を取得するという違いがあります。

また、drawdown_trades.loc は、DataFrameから「行や列をラベルで指定して取り出す」ための機能です。


⑦ ドローダウンの底を取得する

drawdown_bottom_order = int(
    max_drawdown_row["取引順"]
)

drawdown_bottom_time = (
    max_drawdown_row["決済時間"]
)

最大ドローダウンが発生した行から、

  • 何回目の取引だったのか
  • いつ発生したのか

を取得します。

ここが、今回分析する最大ドローダウンの「底」です。


⑧ ドローダウンの開始地点を探す

最大ドローダウンの底が分かったら、その下落がどの最高値から始まったのかを探します。

before_bottom = drawdown_trades[
    drawdown_trades["取引順"]
    <= drawdown_bottom_order
].copy()

まず、底に到達するまでの取引だけを抽出します。

そして、

drawdown_peak_value = (
    max_drawdown_row["過去最高損益"]
)

で、最大ドローダウンを計算する基準となった最高損益を取得します。

続いて、

peak_rows = before_bottom[
    before_bottom["累積損益"]
    == drawdown_peak_value
]

で、その最高値を記録した行を探します。

最後に、

drawdown_peak_row = peak_rows.iloc[-1]

とします。

.iloc[-1]最後の行という意味です。

同じ最高値を複数回記録している可能性があるため、その中で底に到達する前の最後の最高値をドローダウン開始地点とします。


⑨ 最高値まで回復した地点を探す

次は、底をつけた後に元の最高値まで戻ったかを調べます。

after_bottom = drawdown_trades[
    drawdown_trades["取引順"]
    > drawdown_bottom_order
].copy()

底をつけた後の取引だけを抽出します。

そして、

recovery_rows = after_bottom[
    after_bottom["累積損益"]
    >= drawdown_peak_value
]

によって、

ドローダウン開始前の最高損益以上になった取引

を探します。

複数該当する場合、

recovery_row = recovery_rows.iloc[0]

として最初の行を取得します。

つまり、最初に元の最高値まで回復した取引です。


⑩ 回復までの取引数と期間を計算する

recovery_trade_count = (
    recovery_order
    - drawdown_bottom_order
)

total_drawdown_trade_count = (
    recovery_order
    - drawdown_peak_order
)

drawdown_duration = (
    recovery_time
    - drawdown_peak_time
)

ここでは2種類の取引数を計算しています。

最高値
 ↓
 ↓ 下落
 ↓
底
 ↓
 ↓ 回復
 ↓
元の最高値

recovery_trade_countは、

底 → 回復

までの取引数です。

total_drawdown_trade_countは、

最高値 → 下落 → 底 → 回復

というドローダウン全体の取引数です。


CSVとグラフを保存する

分析結果は、

drawdown_trades.to_csv(
    "results/drawdown_trades.csv",
    index=False,
    encoding="utf-8-sig"
)

でCSVに保存します。

また、

cumulative_profit_drawdown.png
drawdown.png

という2つのグラフも保存します。

前者では累積損益・過去最高損益・最大ドローダウンの開始点・底・回復点を確認できます。

後者では、ドローダウンそのものが時間とともにどのように変化したのかを確認します。

出力結果

出力結果
出力結果

分析結果

実際に零号機のドローダウンを可視化してみました。

まず、累積損益と過去最高損益を重ねたグラフを見てみます。

青線が累積損益、オレンジの点線がその時点までの過去最高損益です。

全体を見ると、累積損益は上下を繰り返しながらも、約1,700回の取引を通して右肩上がりに推移しています。

一方で、常に最高値を更新し続けているわけではありません。

利益が伸びた後に一定期間停滞したり、大きく下落したりする局面が何度も確認できます。

その中で最大のドローダウンが発生したのは、355回目の取引から始まった下落局面でした。

今回のCSVから確認すると、最大ドローダウンは次のようになりました。

  • ドローダウン開始:355回目(2017年4月24日)
  • 開始時の累積損益:約73,643円
  • ドローダウンの底:401回目(2017年8月11日)
  • 底での累積損益:約43,902円
  • 最大ドローダウン:約-29,742円
  • 最高値を回復:507回目(2018年5月17日)
  • 開始から回復まで:152取引
  • 開始から回復まで:約388日

つまり、累積損益約73,643円まで到達した後、約29,742円を失い、約43,902円まで落ち込んだことになります。

金額だけを見ると約3万円ですが、当時の累積利益約7.4万円に対して考えると、それまで積み上げた利益の約40%を一時的に失ったことになります。

さらに興味深いのが、回復までの時間です。

最大ドローダウンの底そのものは、開始から約109日後に記録しています。

しかし、そこから元の最高値まで戻るにはさらに約279日かかり、最終的に最高値を更新したのはドローダウン開始から約388日後でした。

つまり、最大の問題は「約3万円負けたこと」だけではありません。

一度最高値をつけてから、約1年1か月にわたってその最高値を更新できなかったことも、零号機を実際に運用するうえでは重要な特徴だと思います。

ドローダウンの推移を見る

次に、ドローダウンだけを取り出したグラフを見てみます。

0に近いほど過去最高値付近にいて、下方向に大きくなるほど最高値から資産が減少していることを表します。

こちらを見ると、零号機は最大ドローダウンの1回だけが突出しているわけではありません。

序盤には-20,000円を超える下落があり、400回目前後には今回の最大ドローダウンとなる約-29,742円を記録しています。

その後も、1,100回台や1,500回台などで比較的大きなドローダウンが発生しています。

一方で、その都度最高値付近まで戻り、その後さらに利益を伸ばしていることも確認できます。

このことから零号機は、

「一直線に利益を積み上げるシステム」ではなく、「一定のドローダウンを何度も経験しながら長期的に利益を積み上げるシステム

と考えた方が実態に近そうです。

また、グラフ終盤にも注目したいところです。

過去最高の累積損益を記録した後、最後の部分では再びドローダウン状態に入っています。

つまり、バックテスト終了時点では、直近の最高値をまだ回復していません

最大ドローダウンだけを見て「最大でも約3万円の下落だから大丈夫」と判断するのではなく、

  • ドローダウンがどの程度の頻度で発生するのか
  • どのくらい長く続くのか
  • 運用資金に対してどの程度の割合になるのか
  • どのような相場で大きなドローダウンが発生するのか

まで考える必要がありそうです。

今回分かったこと

今回の分析で、これまでとは少し違う角度から零号機の性格が見えてきました。

これまで時間帯、曜日、保有時間、連勝・連敗などを調べてきましたが、それらは基本的に個々の取引の特徴を見る分析でした。

今回は取引を時系列につなげることで、利益がどのような道筋で積み上がってきたのかを見ることができました。

零号機は長期的には利益を積み上げています。

しかし、その過程では約29,742円の最大ドローダウンが発生し、元の最高値を更新するまで約388日を要していました。

バックテストの最終利益だけを見ていると、

「最終的に利益が出ているから問題ない」

と考えてしまいます。

しかし実際に運用していたとすれば、約1年間最高値を更新できない期間でもシステムを止めずに運用し続けなければなりません。

これは、バックテスト結果を見るだけでは分かりにくい、実運用上かなり重要なリスクだと思います。

そして、ここで新しい疑問が出てきました。

なぜ、このタイミングで大きなドローダウンが発生したのでしょうか。

単なる偶然なのか。

特定の時間帯や曜日、保有時間が関係しているのか。

それとも、零号機が苦手とする相場環境が存在するのでしょうか。

ここは今後、さらに調べていきたいと思います。

ランダムウォークへの挑戦⑪ | 零号機の性格診断──Chapter1「観測」 完結(2026/8/1)
はじめにここまでPythonを使って、零号機をさまざまな角度から分析してきました。第4回から第10回にわたり、基本成績月別損益エントリー・決済時間帯エントリー・決済曜日保有時間連勝・連敗ドローダウンと、一つずつ零号機を観測してきました。最初…

今回追加したコード

今回追加したコード
# ==============================
# ドローダウン分析
# ==============================

drawdown_trades = (
    trades
    .sort_values("決済時間")
    .reset_index(drop=True)
    .copy()
)


# 取引順を追加
drawdown_trades["取引順"] = range(
    1,
    len(drawdown_trades) + 1
)


# 累積損益
drawdown_trades["累積損益"] = (
    drawdown_trades["損益"].cumsum()
)


# その時点までの最高損益
drawdown_trades["過去最高損益"] = (
    drawdown_trades["累積損益"].cummax()
)


# ドローダウン
drawdown_trades["ドローダウン"] = (
    drawdown_trades["累積損益"]
    - drawdown_trades["過去最高損益"]
)


# 最大ドローダウン
max_drawdown = (
    drawdown_trades["ドローダウン"].min()
)


# 最大ドローダウンを記録した行
max_drawdown_row = (
    drawdown_trades.loc[
        drawdown_trades["ドローダウン"].idxmin()
    ]
)


# ドローダウンの底
drawdown_bottom_order = int(
    max_drawdown_row["取引順"]
)

drawdown_bottom_time = (
    max_drawdown_row["決済時間"]
)


# ==============================
# ドローダウン開始地点
# ==============================

before_bottom = drawdown_trades[
    drawdown_trades["取引順"]
    <= drawdown_bottom_order
].copy()


drawdown_peak_value = (
    max_drawdown_row["過去最高損益"]
)


peak_rows = before_bottom[
    before_bottom["累積損益"]
    == drawdown_peak_value
]


drawdown_peak_row = (
    peak_rows.iloc[-1]
)


drawdown_peak_order = int(
    drawdown_peak_row["取引順"]
)

drawdown_peak_time = (
    drawdown_peak_row["決済時間"]
)


# ==============================
# 回復地点
# ==============================

after_bottom = drawdown_trades[
    drawdown_trades["取引順"]
    > drawdown_bottom_order
].copy()


recovery_rows = after_bottom[
    after_bottom["累積損益"]
    >= drawdown_peak_value
]


if not recovery_rows.empty:

    recovery_row = recovery_rows.iloc[0]

    recovery_order = int(
        recovery_row["取引順"]
    )

    recovery_time = (
        recovery_row["決済時間"]
    )

    recovery_trade_count = (
        recovery_order
        - drawdown_bottom_order
    )

    total_drawdown_trade_count = (
        recovery_order
        - drawdown_peak_order
    )

    drawdown_duration = (
        recovery_time
        - drawdown_peak_time
    )

    recovery_status = "回復済み"

else:

    recovery_order = None
    recovery_time = None
    recovery_trade_count = None
    total_drawdown_trade_count = None
    drawdown_duration = None
    recovery_status = "未回復"


# ==============================
# 結果表示
# ==============================

print(
    "\n===== ドローダウン分析 ====="
)

print(
    f"最大ドローダウン: "
    f"{max_drawdown:.0f}"
)

print(
    f"ドローダウン開始時の累積損益: "
    f"{drawdown_peak_value:.0f}"
)

print(
    f"ドローダウン開始取引: "
    f"{drawdown_peak_order}回目"
)

print(
    f"ドローダウン開始時間: "
    f"{drawdown_peak_time}"
)

print(
    f"ドローダウンの底: "
    f"{drawdown_bottom_order}回目"
)

print(
    f"底を記録した時間: "
    f"{drawdown_bottom_time}"
)

print(
    f"回復状況: "
    f"{recovery_status}"
)


if recovery_status == "回復済み":

    print(
        f"回復した取引: "
        f"{recovery_order}回目"
    )

    print(
        f"回復した時間: "
        f"{recovery_time}"
    )

    print(
        f"底から回復までの取引数: "
        f"{recovery_trade_count}回"
    )

    print(
        f"開始から回復までの取引数: "
        f"{total_drawdown_trade_count}回"
    )

    print(
        f"開始から回復までの期間: "
        f"{drawdown_duration}"
    )


# ==============================
# CSV保存
# ==============================

drawdown_trades_output = (
    drawdown_trades.copy()
)


drawdown_trades_output[
    [
        "損益",
        "累積損益",
        "過去最高損益",
        "ドローダウン"
    ]
] = (
    drawdown_trades_output[
        [
            "損益",
            "累積損益",
            "過去最高損益",
            "ドローダウン"
        ]
    ].round(0)
)


drawdown_trades_output.to_csv(
    "results/drawdown_trades.csv",
    index=False,
    encoding="utf-8-sig"
)


# ==============================
# 累積損益グラフ
# ==============================

plt.figure(
    figsize=(14, 7)
)


plt.plot(
    drawdown_trades["取引順"],
    drawdown_trades["累積損益"],
    label="Cumulative Profit"
)


plt.plot(
    drawdown_trades["取引順"],
    drawdown_trades["過去最高損益"],
    linestyle="--",
    label="Running Maximum"
)


plt.scatter(
    drawdown_peak_order,
    drawdown_peak_value,
    label="Drawdown Peak"
)


plt.scatter(
    drawdown_bottom_order,
    max_drawdown_row["累積損益"],
    label="Drawdown Bottom"
)


if recovery_status == "回復済み":

    plt.scatter(
        recovery_order,
        recovery_row["累積損益"],
        label="Recovery"
    )


plt.title(
    "Cumulative Profit and Maximum Drawdown"
)

plt.xlabel(
    "Trade Number"
)

plt.ylabel(
    "Cumulative Profit"
)

plt.legend()

plt.grid()

plt.tight_layout()


plt.savefig(
    "results/cumulative_profit_drawdown.png",
    dpi=150
)


plt.close()


# ==============================
# ドローダウン推移グラフ
# ==============================

plt.figure(
    figsize=(14, 7)
)


plt.plot(
    drawdown_trades["取引順"],
    drawdown_trades["ドローダウン"]
)


plt.axhline(
    y=0,
    linestyle="--"
)


plt.scatter(
    drawdown_bottom_order,
    max_drawdown,
    label="Maximum Drawdown"
)


plt.title(
    "Drawdown"
)

plt.xlabel(
    "Trade Number"
)

plt.ylabel(
    "Drawdown"
)

plt.legend()

plt.grid()

plt.tight_layout()


plt.savefig(
    "results/drawdown.png",
    dpi=150
)


plt.close()

関連記事

ランダムウォークへの挑戦㉓|最高PFは本当に強いのか。Relative Band Distanceを時間分割で比較する(2026/8/30)
ランダムウォークへの挑戦㉒|Q3の境界をずらしても優位性は残るのか(2026/8/30)
ランダムウォークへの挑戦㉑|Q3の優位性は時間が変わっても残るのか(2026/8/29)
ランダムウォークへの挑戦⑳|零号機の利益は「一部の大勝ち」に支えられているのか(2026/8/20)
ランダムウォークへの挑戦⑲|BAND_DISTANCEをATRで割って「相対的なブレイクの強さ」を分析する(2026/8/19)
ランダムウォークへの挑戦⑱|バンドからの距離で零号機のエントリー位置を分析する(2026/8/19)
ランダムウォークへの挑戦⑰|ATR×BB幅で零号機の「得意な相場」を16種類に分解する(2026/8/14)
ランダムウォークへの挑戦⑯|ボリンジャーバンドの幅で零号機の得意な相場を探る(2026/8/11)
ランダムウォークへの挑戦⑮|零号機は「荒れた相場」に強いのか ― ATRと取引成績の関係をPythonで分析する(2026/8/9)
ランダムウォークへの挑戦⑭ | 零号機の「記憶」をPythonへ渡す ― trade_log.csvを分析用データに整える(2026/8/5)
ランダムウォークへの挑戦⑬ | 零号機に「記憶」を与える ― mql4でエントリーから決済までの相場環境を記録する(2026/8/2)
ランダムウォークへの挑戦⑫ | Chapter2「学習」始動 ー零号機は何を学ぶべきなのかー(2026/8/2)
ランダムウォークへの挑戦⑪ | 零号機の性格診断──Chapter1「観測」 完結(2026/8/1)
ランダムウォークへの挑戦⑩ | Pythonで資産推移とドローダウンを分析する(2026/8/1)
ランダムウォークへの挑戦⑨ | Pythonで取引の連勝・連敗を分析する(2026/7/28)
ランダムウォークへの挑戦⑧ | Pythonでポジションの保有時間別に成績を分析する(2026/7/26)
ランダムウォークへの挑戦⑦ | Pythonでエントリー曜日と決済曜日から零号機を分析する(2026/7/23)
ランダムウォークへの挑戦⑥ | Pythonでエントリー時間と決済時間から損益を分析する(2026/7/22)
ランダムウォークへの挑戦⑤ | Pythonで月別損益をグラフ化する(2026/7/5)
ランダムウォークへの挑戦④ |Pythonで「零号機」の基本成績を分析する(2026/6/11)
ランダムウォークへの挑戦③ | 環境設定(2026/5/26)
ランダムウォークへの挑戦➁ | 小目標3つ設定(2026/5/25)
ランダムウォークへの挑戦① | はじめに(2026/5/25)

コメント

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