ランダムウォークへの挑戦⑳|零号機の利益は「一部の大勝ち」に支えられているのか(2026/8/20)

未分類

はじめに

前回は、

BAND_DISTANCE ÷ ATR

によって、バンドからの距離をそのときのボラティリティで正規化した、

Relative Band Distance

を分析しました。

ランダムウォークへの挑戦⑲|BAND_DISTANCEをATRで割って「相対的なブレイクの強さ」を分析する(2026/8/19)
はじめに前回は、エントリー時に価格がボリンジャーバンドからどれだけ離れていたのかを示す、entry_band_distance_pipsと取引成績の関係を分析しました。BAND_DISTANCEを四分位に分けたところ、結果は次のようになりま…

結果は次のようになりました。

グループ勝率平均損益中央値PF
Q173.84%80.25258.521.283
Q269.37%103.63321.651.300
Q360.09%142.33271.151.412
Q443.85%168.47-189.611.297

最もPFが高かったのはQ3でした。

一方で気になったのがQ4です。

Q4は勝率43.85%で、中央値も-189.61。

つまり、半分以上の取引が負けています。

それにもかかわらず、

  • 平均損益:+168.47
  • 総損益:+72,612.18
  • PF:1.297

と、全体としては利益が残りました。

この結果から、

ごく一部の大きな利益が、多数の損失を補っているのではないか

という疑問が出てきます。

今回は、PFや平均損益だけではなく、利益の分布そのものを調べてみます。


なぜ平均値だけでは足りないのか

例えば、5回の取引結果が、

-100
-100
-100
-100
+1000

だったとします。

この場合、平均損益は、

+120

です。

平均値だけを見ると利益が出ています。

しかし実際には、5回中4回は負けています。

このように、

少数の非常に大きな利益

があると、平均値は大きく引っ張られます。

トレンドフォローでは、このような分布自体は珍しくありません。

小さな損失を繰り返しながら、大きなトレンドを捕まえたときに利益を伸ばすからです。

ただし、

どの程度大勝ちに依存しているのか

は確認しておく必要があります。


今回調べたいこと

今回はRelative Band DistanceのQ1~Q4について、次の項目を調べます。

  • 取引数
  • 平均損益
  • 中央値
  • 最大利益
  • 最大損失
  • 25%点
  • 75%点
  • 90%点
  • 95%点
  • 通常のPF
  • 上位10%の勝ち取引が総利益の何%を占めるか
  • 上位5%の勝ち取引が総利益の何%を占めるか
  • 上位10%の勝ち取引を除外した場合のPF
  • 上位5%の勝ち取引を除外した場合のPF

特に注目するのはQ3とQ4です。

前回、

Q3はPFが最高

Q4は平均損益が最高だが中央値がマイナス

という違いがありました。

もしQ3が上位の大勝ちを除いてもPF1付近を維持する一方、Q4だけPFが大きく崩れるなら、

Q3の方が比較的安定した優位性を持ち、Q4はホームラン依存型

という可能性があります。


データを準備する

今回も、

results/trade_base.csv

を使用します。

まず前回と同じRelative Band Distanceを作ります。

analysis_df["relative_band_distance"] = (
    analysis_df["entry_band_distance_pips"]
    /
    analysis_df["entry_atr14_pips"]
)

そして四分位に分けます。

analysis_df["relative_band_group"] = pd.qcut(
    analysis_df["relative_band_distance"],
    q=4,
    labels=[
        "Q1",
        "Q2",
        "Q3",
        "Q4"
    ]
)

これで、第19回と同じ条件で比較できます。


損益分布を見る

まず各グループについて、

  • 最小値
  • 25%点
  • 中央値
  • 75%点
  • 90%点
  • 95%点
  • 最大値

を計算します。

これまで平均値を中心に見てきましたが、今回は、

取引結果が実際にどの範囲へ分布しているのか

を確認します。

特に、

平均値 >> 中央値

となっている場合は、右側に大きな利益が存在している可能性が高くなります。


上位10%の勝ち取引は総利益の何%を作っているのか

次に、各グループの勝ち取引だけを取り出します。

その中から利益額の大きい順に並べ、

上位10%の取引が、そのグループの総利益の何%を作っているのか

を計算します。

例えば、総利益100万円のうち、上位10%の取引だけで70万円を稼いでいた場合、

上位10%利益寄与率 = 70%

です。

この値が高いほど、

少数の大勝ちへの依存度が高い

と考えられます。

同様に上位5%についても確認します。


大勝ちを除いたらPFはどうなるか

さらに踏み込んで、

上位10%の勝ち取引を除外した場合

のPFも再計算します。

これはかなり厳しいテストです。

トレンドフォロー戦略では大きな利益を取ること自体が戦略の一部なので、

大勝ちを除外してPF1を割ったから戦略として悪い

という意味ではありません。

むしろ、

収益構造がどれだけ大勝ちに依存しているのか

を見るためのストレステストです。

上位10%を除いてもPFが1付近に残るのであれば、比較的広い取引から利益を得ています。

一方、PFが大幅に低下するのであれば、

少数の大きなトレンドを捕まえることが収益の中心

であることが分かります。


箱ひげ図でも確認する

数値だけでは分布が分かりにくいため、箱ひげ図も作ります。

箱ひげ図では、

  • 中央値
  • 25%点
  • 75%点
  • 外れ値

を視覚的に確認できます。

今回は特にQ4について、

多数のマイナス取引の右側に、巨大なプラスの外れ値が存在しているのか

を見てみます。

ただし極端な利益が存在するとグラフ全体が潰れてしまう可能性があります。

そのため、

  • 全取引版
  • 上下1%を除いた拡大版

の2種類を出力します。


今回の分析で分かること

ここまでの分析では、

どの条件でPFが高いか

を中心に見てきました。

しかしPFが同じ1.4であっても、

タイプA
多くの取引で少しずつ利益

タイプB
多数の小さな損失
+
数回の巨大な利益

では意味がまったく違います。

今回の分析では、

零号機の利益がどのような形で作られているのか

まで踏み込みます。

これによって、単なる条件別成績ではなく、

零号機という戦略そのものの性格

が少しずつ見えてくるはずです。


分析結果

今回は、Relative Band DistanceのQ1~Q4について、単純なPFや平均損益だけではなく、

「利益がどのような分布から生まれているのか」

を調べました。

まず、各グループの損益分布は次のようになりました。

グループ勝率平均損益中央値25%点75%点95%点最大利益最大損失PF
Q173.84%80.25258.52-27.11529.961,079.143,320.04-6,217.991.283
Q269.37%103.63321.65-151.78665.681,413.704,821.95-12,814.891.300
Q360.09%142.33271.15-332.32678.301,678.827,261.58-6,104.371.412
Q443.85%168.47-189.61-659.86902.323,158.4025,936.77-17,965.371.297

ここでも前回と同じように、Q4だけかなり異質な分布になっています。


Q1~Q3は中央値がプラス、Q4だけマイナス

まず中央値を見ると、

Q1  +258.52
Q2  +321.65
Q3  +271.15
Q4  -189.61

となりました。

Q1~Q3では、「典型的な取引」はプラスです。

一方Q4だけは中央値がマイナスになっています。

つまりQ4では、

半分以上の取引が損失側にある

にもかかわらず、平均損益は4グループで最も高いという状態です。

これは平均値が一部の大きな利益に強く引っ張られていることを示しています。


Q4は利益側の裾が非常に長い

最大利益を見ると、

Q1   3,320.04
Q2   4,821.95
Q3   7,261.58
Q4  25,936.77

となりました。

Q4の最大利益は、Q3の約3.6倍です。

95%点についても、

Q1  1,079.14
Q2  1,413.70
Q3  1,678.82
Q4  3,158.40

となっており、Q4だけかなり大きくなっています。

箱ひげ図でも、Q4には1万を大きく超える利益や2万を超える利益が存在しています。

つまりQ4では、

普段の取引はそれほど強くない一方、ごく一部で巨大な利益が発生する

という特徴がかなり明確です。


一方でQ4は損失側も大きい

ただし、Q4は利益側だけが大きいわけではありません。

最大損失を見ると、

Q1   -6,217.99
Q2  -12,814.89
Q3   -6,104.37
Q4  -17,965.37

でした。

Q4は最大利益が最も大きい一方、最大損失も最も大きくなっています。

さらに25%点も、

Q1   -27.11
Q2  -151.78
Q3  -332.32
Q4  -659.86

と、Relative Band Distanceが大きくなるほど損失側へ広がっています。

つまりQ4は、

大きく勝つ可能性もあるが、大きく負ける可能性もある

非常に振れ幅の大きな取引群です。


上位10%の勝ち取引がどれだけ利益を作っているか

次に、各グループの勝ち取引について、

利益額上位10%の取引が、総利益の何%を占めているのか

を計算しました。

結果は、

Q1  28.95%
Q2  30.06%
Q3  31.46%
Q4  38.85%

となりました。

Q1~Q3では、おおむね30%前後です。

一方Q4では、

上位10%の勝ち取引だけで総利益の38.85%

を作っています。

さらに上位5%だけを見ると、

Q1  17.88%
Q2  18.80%
Q3  20.68%
Q4  27.38%

でした。

Q4では、わずか10件の大きな勝ち取引だけで、総利益の27.38%を占めています。

この結果からも、

Q4は他のグループより明らかに少数の大勝ちへの依存度が高い

ことが分かります。


「大勝ちを取り除く」ストレステスト

ここからが今回最も重要な分析です。

各グループについて、利益額上位5%の勝ち取引を削除し、PFを再計算しました。

結果は、

グループ通常PF上位5%除外上位10%除外
Q11.2831.0540.912
Q21.3001.0560.909
Q31.4121.1200.968
Q41.2970.9420.793

かなりはっきりと違いが出ました。


Q4は上位5%を除いただけでPF1を割った

Q4は通常状態では、

PF 1.297

でした。

しかし上位5%の勝ち取引を除くと、

PF 0.942

まで低下しました。

さらに上位10%を除くと、

PF 0.793

です。

総損益も、

通常
+72,612.18

上位5%除外
-14,123.82

上位10%除外
-50,471.73

となりました。

つまりQ4は、

上位5%程度の大きな勝ちが存在しなければ、そもそも利益が残らない

構造になっています。

第19回で、

Q4はホームラン型なのではないか

と考えましたが、今回の分析ではその傾向がかなり明確になりました。


ただし「ホームラン依存=悪い」ではない

ここは注意が必要です。

Q4が一部の大勝ちに依存しているからといって、

Q4は悪い条件

とは言えません。

零号機はトレンドフォロー型です。

トレンドフォロー戦略では、

多数の小さな負けを受け入れながら、ごく少数の大きなトレンドを取り切る

という利益構造そのものが戦略の特徴です。

その大勝ちを意図的に削除すれば、PFが大きく低下するのはある意味当然とも言えます。

今回のストレステストは、

大勝ちを除いた方が良いか

を見るためではありません。

利益がどの程度大勝ちに依存しているのか

を測るための分析です。

その意味で、Q4はかなり強い「ホームラン依存型」であることが分かりました。


ではQ3はどうか

一方、かなり興味深いのがQ3です。

Q3はもともと、

PF 1.412

と4グループ中最高でした。

上位5%の勝ち取引を除いても、

PF 1.120

を維持しています。

総損益も、

+17,844.97

です。

つまりQ3は、

最上位の大勝ちをいくつか取り除いても、まだ利益が残っている

唯一のグループではありませんが、その耐性は4グループ中最も高くなりました。

上位10%を除くとPF0.968となり1をわずかに下回りますが、Q1の0.912、Q2の0.909、Q4の0.793より高い値です。

この結果を見る限り、

Q3はQ4よりも広い範囲の取引から利益を得ている

可能性があります。


Q3とQ4は同じ「トレンドフォロー型」でも性格が違う

ここまでを見ると、Q3とQ4の違いがかなり見えてきました。

Q3

  • 勝率60.09%
  • 中央値+271.15
  • PF1.412
  • 上位5%除外後もPF1.120
  • 上位10%除外後もPF0.968

つまり、

大きな利益も存在するが、それだけに依存せず比較的広い取引で利益を積み上げている

タイプです。

一方Q4は、

  • 勝率43.85%
  • 中央値-189.61
  • 最大利益25,936.77
  • 上位10%が総利益の38.85%
  • 上位5%除外だけでPF0.942

となりました。

こちらは、

普段は負けることの方が多いが、たまに発生する非常に大きなトレンドで利益を作る

タイプです。

同じ零号機の中でも、Relative Band Distanceによって利益の作り方そのものが変わっている可能性があります。


Q3は今のところ最も興味深いゾーン

ここ数回の分析を振り返ると、Q3はかなり興味深い位置にあります。

Relative Band Distance分析では、

PF1.412

と最高でした。

今回のストレステストでも、

上位5%の勝ちを除いてPF1.120

と最も高い耐性を示しました。

一方で、Q4ほど極端な外れ値には依存していません。

つまり現時点では、

「相場のボラティリティに対して、ある程度強くバンドを抜けているが、極端には伸び切っていない」

領域が、零号機にとって比較的安定した条件なのではないか、という仮説が強くなってきました。

ただし、まだこれをエントリーフィルターに使う段階ではありません。


今回のまとめ

今回、零号機の利益分布を分析したことで、PFだけを見ていたときには分からなかった違いが見えてきました。

特に重要なのは、

Q3とQ4では利益の作り方がかなり違う

という点です。

Q4は、

多数の負け + 少数の巨大な勝ち

という典型的なホームラン型でした。

上位5%の勝ちを除くだけでPFは1を割りました。

一方Q3は、

大勝ちも存在するが、比較的広い取引から利益を得ている

構造でした。

上位5%を除いてもPF1.120を維持しています。

ここまでの結果から、

Q4は「当たれば大きい」

Q3は「比較的安定して利益を作る」

という違いがありそうです。

しかし、このQ3の優位性が過去データ全体にわたって安定して存在していたのかは、まだ分かりません。

たまたま一部の期間だけQ3が強かった可能性もあります。

そこで次は、時間軸を加えてみます。

出力されるファイル

今回のコードでは、

results/
├─ profit_distribution_analysis.csv
├─ profit_contribution_analysis.csv
├─ profit_stress_test.csv
├─ profit_distribution_boxplot.png
├─ profit_distribution_boxplot_trimmed.png
├─ top_profit_contribution.png
└─ profit_factor_stress_test.png

を出力します。

特に重要なのは、

profit_distribution_analysis.csv
profit_contribution_analysis.csv
profit_stress_test.csv

の3つです。

この3ファイルを見ることで、

Q3とQ4ではどちらが大勝ちに依存しているのか

零号機の利益は上位5%・10%の取引にどれくらい集中しているのか

大勝ちを取り除いた場合に各グループのPFがどこまで崩れるのか

を比較できます。

次回は、この結果を使って零号機の「利益の作り方」そのものを分析します。

今回のコード全文

ファイル名:

profit_distribution_analysis.py

コード全文を見る
import os
import math
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt


# ============================================================
# 設定
# ============================================================

INPUT_FILE = "results/trade_base.csv"

OUTPUT_DIR = "results"

os.makedirs(
    OUTPUT_DIR,
    exist_ok=True
)


OUTPUT_SUMMARY_CSV = os.path.join(
    OUTPUT_DIR,
    "profit_distribution_analysis.csv"
)

OUTPUT_CONTRIBUTION_CSV = os.path.join(
    OUTPUT_DIR,
    "profit_contribution_analysis.csv"
)

OUTPUT_STRESS_CSV = os.path.join(
    OUTPUT_DIR,
    "profit_stress_test.csv"
)

OUTPUT_BOXPLOT = os.path.join(
    OUTPUT_DIR,
    "profit_distribution_boxplot.png"
)

OUTPUT_BOXPLOT_TRIMMED = os.path.join(
    OUTPUT_DIR,
    "profit_distribution_boxplot_trimmed.png"
)

OUTPUT_CONTRIBUTION_GRAPH = os.path.join(
    OUTPUT_DIR,
    "top_profit_contribution.png"
)

OUTPUT_STRESS_GRAPH = os.path.join(
    OUTPUT_DIR,
    "profit_factor_stress_test.png"
)


# ============================================================
# データ読み込み
# ============================================================

df = pd.read_csv(
    INPUT_FILE
)


print(
    "========================================"
)

print(
    "PROFIT DISTRIBUTION ANALYSIS"
)

print(
    "========================================"
)

print(
    f"\n読み込み件数: {len(df)}"
)


# ============================================================
# 必要列確認
# ============================================================

required_columns = [
    "entry_band_distance_pips",
    "entry_atr14_pips",
    "profit"
]


missing_columns = [
    column
    for column in required_columns
    if column not in df.columns
]


if missing_columns:

    raise ValueError(
        "必要な列がありません: "
        + ", ".join(missing_columns)
    )


# ============================================================
# 数値型へ変換
# ============================================================

for column in required_columns:

    df[column] = pd.to_numeric(
        df[column],
        errors="coerce"
    )


# ============================================================
# 分析対象
# ============================================================

analysis_df = df[
    df["entry_band_distance_pips"].notna()
    &
    df["entry_atr14_pips"].notna()
    &
    df["profit"].notna()
    &
    (
        df["entry_atr14_pips"]
        > 0
    )
].copy()


if analysis_df.empty:

    raise ValueError(
        "分析対象データがありません。"
    )


print(
    f"分析対象件数: {len(analysis_df)}"
)


# ============================================================
# Relative Band Distance
# ============================================================

analysis_df[
    "relative_band_distance"
] = (
    analysis_df[
        "entry_band_distance_pips"
    ]
    /
    analysis_df[
        "entry_atr14_pips"
    ]
)


analysis_df[
    "relative_band_distance"
] = (
    analysis_df[
        "relative_band_distance"
    ]
    .replace(
        [
            np.inf,
            -np.inf
        ],
        np.nan
    )
)


analysis_df = analysis_df[
    analysis_df[
        "relative_band_distance"
    ]
    .notna()
].copy()


# ============================================================
# Relative Band Distanceを四分位化
# ============================================================

quartile_order = [
    "Q1",
    "Q2",
    "Q3",
    "Q4"
]


analysis_df[
    "relative_band_group"
] = pd.qcut(
    analysis_df[
        "relative_band_distance"
    ],
    q=4,
    labels=quartile_order
)


# ============================================================
# PF計算関数
# ============================================================

def calculate_pf(data):

    gross_profit = (
        data.loc[
            data["profit"] > 0,
            "profit"
        ]
        .sum()
    )

    gross_loss = abs(
        data.loc[
            data["profit"] < 0,
            "profit"
        ]
        .sum()
    )

    if gross_loss == 0:

        return np.nan

    return (
        gross_profit
        /
        gross_loss
    )


# ============================================================
# 分布統計
# ============================================================

summary_rows = []


for group_name in quartile_order:

    group_df = analysis_df[
        analysis_df[
            "relative_band_group"
        ]
        == group_name
    ].copy()


    profit = group_df[
        "profit"
    ]


    summary_rows.append(
        {
            "group": group_name,

            "trades": len(
                group_df
            ),

            "wins": (
                profit > 0
            ).sum(),

            "losses": (
                profit < 0
            ).sum(),

            "win_rate": (
                (
                    profit > 0
                ).mean()
                * 100
            ),

            "total_profit": (
                profit.sum()
            ),

            "average_profit": (
                profit.mean()
            ),

            "minimum_profit": (
                profit.min()
            ),

            "q25_profit": (
                profit.quantile(
                    0.25
                )
            ),

            "median_profit": (
                profit.median()
            ),

            "q75_profit": (
                profit.quantile(
                    0.75
                )
            ),

            "q90_profit": (
                profit.quantile(
                    0.90
                )
            ),

            "q95_profit": (
                profit.quantile(
                    0.95
                )
            ),

            "maximum_profit": (
                profit.max()
            ),

            "profit_factor": (
                calculate_pf(
                    group_df
                )
            )
        }
    )


summary_df = pd.DataFrame(
    summary_rows
)


summary_df = (
    summary_df
    .set_index(
        "group"
    )
)


# ============================================================
# 小数点整理
# ============================================================

summary_df[
    "win_rate"
] = (
    summary_df[
        "win_rate"
    ]
    .round(2)
)


round_columns = [
    "total_profit",
    "average_profit",
    "minimum_profit",
    "q25_profit",
    "median_profit",
    "q75_profit",
    "q90_profit",
    "q95_profit",
    "maximum_profit"
]


for column in round_columns:

    summary_df[
        column
    ] = (
        summary_df[
            column
        ]
        .round(2)
    )


summary_df[
    "profit_factor"
] = (
    summary_df[
        "profit_factor"
    ]
    .round(3)
)


print(
    "\n========================================"
)

print(
    "損益分布"
)

print(
    "========================================"
)


print(
    summary_df
)


summary_df.to_csv(
    OUTPUT_SUMMARY_CSV,
    encoding="utf-8-sig"
)


# ============================================================
# 上位利益寄与率
# ============================================================

contribution_rows = []


for group_name in quartile_order:

    group_df = analysis_df[
        analysis_df[
            "relative_band_group"
        ]
        == group_name
    ].copy()


    winning_df = group_df[
        group_df[
            "profit"
        ]
        > 0
    ].copy()


    winning_df = winning_df.sort_values(
        by="profit",
        ascending=False
    )


    gross_profit = (
        winning_df[
            "profit"
        ]
        .sum()
    )


    win_count = len(
        winning_df
    )


    top_10_count = max(
        1,
        math.ceil(
            win_count
            * 0.10
        )
    )


    top_5_count = max(
        1,
        math.ceil(
            win_count
            * 0.05
        )
    )


    top_10_profit = (
        winning_df
        .head(
            top_10_count
        )
        [
            "profit"
        ]
        .sum()
    )


    top_5_profit = (
        winning_df
        .head(
            top_5_count
        )
        [
            "profit"
        ]
        .sum()
    )


    if gross_profit > 0:

        top_10_share = (
            top_10_profit
            /
            gross_profit
            * 100
        )

        top_5_share = (
            top_5_profit
            /
            gross_profit
            * 100
        )

    else:

        top_10_share = np.nan
        top_5_share = np.nan


    contribution_rows.append(
        {
            "group": group_name,

            "winning_trades": win_count,

            "gross_profit": gross_profit,

            "top_10_count": top_10_count,

            "top_10_profit": top_10_profit,

            "top_10_profit_share": top_10_share,

            "top_5_count": top_5_count,

            "top_5_profit": top_5_profit,

            "top_5_profit_share": top_5_share
        }
    )


contribution_df = pd.DataFrame(
    contribution_rows
)


contribution_df = (
    contribution_df
    .set_index(
        "group"
    )
)


for column in [
    "gross_profit",
    "top_10_profit",
    "top_5_profit"
]:

    contribution_df[
        column
    ] = (
        contribution_df[
            column
        ]
        .round(2)
    )


for column in [
    "top_10_profit_share",
    "top_5_profit_share"
]:

    contribution_df[
        column
    ] = (
        contribution_df[
            column
        ]
        .round(2)
    )


print(
    "\n========================================"
)

print(
    "上位利益取引の寄与率"
)

print(
    "========================================"
)


print(
    contribution_df
)


contribution_df.to_csv(
    OUTPUT_CONTRIBUTION_CSV,
    encoding="utf-8-sig"
)


# ============================================================
# 上位勝ち取引除外ストレステスト
# ============================================================

stress_rows = []


for group_name in quartile_order:

    group_df = analysis_df[
        analysis_df[
            "relative_band_group"
        ]
        == group_name
    ].copy()


    original_pf = (
        calculate_pf(
            group_df
        )
    )


    winning_df = group_df[
        group_df[
            "profit"
        ]
        > 0
    ].sort_values(
        by="profit",
        ascending=False
    )


    win_count = len(
        winning_df
    )


    top_10_count = max(
        1,
        math.ceil(
            win_count
            * 0.10
        )
    )


    top_5_count = max(
        1,
        math.ceil(
            win_count
            * 0.05
        )
    )


    top_10_indices = (
        winning_df
        .head(
            top_10_count
        )
        .index
    )


    top_5_indices = (
        winning_df
        .head(
            top_5_count
        )
        .index
    )


    without_top_10 = (
        group_df
        .drop(
            index=top_10_indices
        )
    )


    without_top_5 = (
        group_df
        .drop(
            index=top_5_indices
        )
    )


    pf_without_top_10 = (
        calculate_pf(
            without_top_10
        )
    )


    pf_without_top_5 = (
        calculate_pf(
            without_top_5
        )
    )


    total_profit_without_top_10 = (
        without_top_10[
            "profit"
        ]
        .sum()
    )


    total_profit_without_top_5 = (
        without_top_5[
            "profit"
        ]
        .sum()
    )


    stress_rows.append(
        {
            "group": group_name,

            "original_pf": (
                original_pf
            ),

            "pf_without_top_5_percent_winners": (
                pf_without_top_5
            ),

            "pf_without_top_10_percent_winners": (
                pf_without_top_10
            ),

            "total_profit_without_top_5_percent_winners": (
                total_profit_without_top_5
            ),

            "total_profit_without_top_10_percent_winners": (
                total_profit_without_top_10
            )
        }
    )


stress_df = pd.DataFrame(
    stress_rows
)


stress_df = (
    stress_df
    .set_index(
        "group"
    )
)


for column in [
    "original_pf",
    "pf_without_top_5_percent_winners",
    "pf_without_top_10_percent_winners"
]:

    stress_df[
        column
    ] = (
        stress_df[
            column
        ]
        .round(3)
    )


for column in [
    "total_profit_without_top_5_percent_winners",
    "total_profit_without_top_10_percent_winners"
]:

    stress_df[
        column
    ] = (
        stress_df[
            column
        ]
        .round(2)
    )


print(
    "\n========================================"
)

print(
    "大勝ち除外ストレステスト"
)

print(
    "========================================"
)


print(
    stress_df
)


stress_df.to_csv(
    OUTPUT_STRESS_CSV,
    encoding="utf-8-sig"
)


# ============================================================
# 箱ひげ図
# ============================================================

boxplot_data = [
    analysis_df.loc[
        analysis_df[
            "relative_band_group"
        ]
        == group,
        "profit"
    ]
    for group in quartile_order
]


fig, ax = plt.subplots(
    figsize=(9, 6)
)


ax.boxplot(
    boxplot_data,
    tick_labels=quartile_order
)


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


ax.set_xlabel(
    "Relative Band Distance Quartile"
)


ax.set_ylabel(
    "Profit"
)


ax.set_title(
    "Profit Distribution by Relative Band Distance"
)


ax.grid(
    axis="y",
    alpha=0.3
)


plt.tight_layout()


plt.savefig(
    OUTPUT_BOXPLOT,
    dpi=150
)


plt.close()


# ============================================================
# 上下1%を除いた箱ひげ図
# ============================================================

lower_limit = (
    analysis_df[
        "profit"
    ]
    .quantile(
        0.01
    )
)


upper_limit = (
    analysis_df[
        "profit"
    ]
    .quantile(
        0.99
    )
)


trimmed_df = analysis_df[
    (
        analysis_df[
            "profit"
        ]
        >= lower_limit
    )
    &
    (
        analysis_df[
            "profit"
        ]
        <= upper_limit
    )
].copy()


trimmed_boxplot_data = [
    trimmed_df.loc[
        trimmed_df[
            "relative_band_group"
        ]
        == group,
        "profit"
    ]
    for group in quartile_order
]


fig, ax = plt.subplots(
    figsize=(9, 6)
)


ax.boxplot(
    trimmed_boxplot_data,
    tick_labels=quartile_order
)


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


ax.set_xlabel(
    "Relative Band Distance Quartile"
)


ax.set_ylabel(
    "Profit"
)


ax.set_title(
    "Profit Distribution by Relative Band Distance (1%-99%)"
)


ax.grid(
    axis="y",
    alpha=0.3
)


plt.tight_layout()


plt.savefig(
    OUTPUT_BOXPLOT_TRIMMED,
    dpi=150
)


plt.close()


# ============================================================
# 上位利益寄与率グラフ
# ============================================================

contribution_plot_df = (
    contribution_df[
        [
            "top_5_profit_share",
            "top_10_profit_share"
        ]
    ]
)


fig, ax = plt.subplots(
    figsize=(9, 6)
)


contribution_plot_df.plot(
    kind="bar",
    ax=ax
)


ax.set_xlabel(
    "Relative Band Distance Quartile"
)


ax.set_ylabel(
    "Share of Gross Profit (%)"
)


ax.set_title(
    "Contribution of Top Winning Trades"
)


ax.tick_params(
    axis="x",
    rotation=0
)


ax.grid(
    axis="y",
    alpha=0.3
)


plt.tight_layout()


plt.savefig(
    OUTPUT_CONTRIBUTION_GRAPH,
    dpi=150
)


plt.close()


# ============================================================
# PFストレステストグラフ
# ============================================================

stress_plot_df = (
    stress_df[
        [
            "original_pf",
            "pf_without_top_5_percent_winners",
            "pf_without_top_10_percent_winners"
        ]
    ]
)


fig, ax = plt.subplots(
    figsize=(9, 6)
)


stress_plot_df.plot(
    kind="bar",
    ax=ax
)


ax.axhline(
    y=1.0,
    linestyle="--"
)


ax.set_xlabel(
    "Relative Band Distance Quartile"
)


ax.set_ylabel(
    "Profit Factor"
)


ax.set_title(
    "Profit Factor Stress Test"
)


ax.tick_params(
    axis="x",
    rotation=0
)


ax.grid(
    axis="y",
    alpha=0.3
)


plt.tight_layout()


plt.savefig(
    OUTPUT_STRESS_GRAPH,
    dpi=150
)


plt.close()


# ============================================================
# 全体結果
# ============================================================

print(
    "\n========================================"
)

print(
    "分析完了"
)

print(
    "========================================"
)


print(
    "\n出力ファイル:"
)


print(
    OUTPUT_SUMMARY_CSV
)


print(
    OUTPUT_CONTRIBUTION_CSV
)


print(
    OUTPUT_STRESS_CSV
)


print(
    OUTPUT_BOXPLOT
)


print(
    OUTPUT_BOXPLOT_TRIMMED
)


print(
    OUTPUT_CONTRIBUTION_GRAPH
)


print(
    OUTPUT_STRESS_GRAPH
)

関連記事

ランダムウォークへの挑戦⑳|零号機の利益は「一部の大勝ち」に支えられているのか(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をコピーしました