ランダムウォークへの挑戦㉑|Q3の優位性は時間が変わっても残るのか(2026/8/29)

未分類

はじめに

前回は、Relative Band Distanceごとの利益分布を分析しました。

ランダムウォークへの挑戦⑳|零号機の利益は「一部の大勝ち」に支えられているのか(2026/8/20)
はじめに前回は、BAND_DISTANCE ÷ ATRによって、バンドからの距離をそのときのボラティリティで正規化した、Relative Band Distanceを分析しました。結果は次のようになりました。グループ勝率平均損益中央値PFQ…

その結果、特にQ3とQ4で利益の作り方がかなり違うことが分かりました。

Q3は、

  • PF:1.412
  • 中央値:+271.15
  • 上位5%の勝ち取引を除外してもPF:1.120
  • 上位10%を除外してもPF:0.968

となりました。

一方Q4は、

  • PF:1.297
  • 中央値:-189.61
  • 上位5%除外後PF:0.942
  • 上位10%除外後PF:0.793

でした。

つまりQ4は、

少数の大きな勝ちによって利益を作るホームラン型

であるのに対し、Q3は、

比較的広い取引から利益を積み上げている

可能性が見えてきました。

現時点では、Relative Band Distance Q3が最も興味深いゾーンです。

しかし、まだ一つ大きな問題が残っています。


全期間で強いだけでは意味がない

これまでの分析は、基本的にバックテスト期間全体をまとめて行ってきました。

例えばQ3のPFが1.412だったとしても、

前半:PF 2.0
後半:PF 0.9

という結果なら、

Q3には安定した優位性がある

とは言いにくいです。

たまたま一部の期間で大きく稼いだ結果、全体のPFが高く見えている可能性があるからです。

逆に、

期間1:PF 1.3
期間2:PF 1.4
期間3:PF 1.5
期間4:PF 1.3

のように複数期間で同じ傾向が再現しているなら、

Q3の優位性は特定期間だけの偶然ではない

可能性が高まります。

そこで今回は、これまで分析してきたRelative Band Distanceに、

時間軸

を追加します。


今回調べること

今回は3つの分析を行います。

① バックテスト期間を4分割する

全取引を時系列順に並べ、

Period 1
Period 2
Period 3
Period 4

の4期間に分割します。

各期間についてQ1~Q4の、

  • 取引数
  • 勝率
  • 総損益
  • 平均損益
  • PF

を計算します。


② 年別に分析する

取引日時が取得できる場合は、年ごとの成績も確認します。

例えば、

2018
2019
2020
2021
...

のように分け、

Q3が特定の年だけ強かったのか

を確認します。


③ 累積損益を見る

さらに、Q1~Q4それぞれについて累積損益を計算します。

これによって、

長期間にわたって少しずつ利益を積み上げているのか

それとも、

一部の期間だけ急激に利益が増えているのか

を確認できます。

PFだけでは見えない時間的な特徴を、累積損益曲線から見てみます。


時系列分析で注意すること

今回重要なのは、

ランダムに4分割しない

ことです。

過去の取引をシャッフルして4グループに分けてしまうと、時間的な安定性は調べられません。

必ず取引日時で並べた上で、

古い
↓
新しい

という順番を維持して分割します。


まずRelative Band Distanceを作る

今回も前回までと同じように、

Relative Band Distance
=
BAND_DISTANCE ÷ ATR

を使用します。

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回、第20回と同じ基準を再現します。


時系列順に4期間へ分割する

まず取引日時で並び替えます。

今回は entry_time を使用します。

analysis_df = analysis_df.sort_values(
    "entry_time"
).reset_index(drop=True)

その後、

analysis_df["time_period"] = pd.qcut(
    analysis_df.index,
    q=4,
    labels=[
        "Period1",
        "Period2",
        "Period3",
        "Period4"
    ]
)

とします。

これによって、取引数がほぼ同数になるように時系列で4分割できます。


期間ごとにPFを確認する

各期間とRelative Band Distanceグループを組み合わせ、

Period1 × Q1
Period1 × Q2
Period1 × Q3
Period1 × Q4

Period2 × Q1
...

という形で成績を計算します。

ここで特に注目したいのはQ3です。

もし、

Period1  Q3 → PF 1以上
Period2  Q3 → PF 1以上
Period3  Q3 → PF 1以上
Period4  Q3 → PF 1以上

となるなら、かなり興味深い結果です。

一方、

Period1 → 0.8
Period2 → 0.9
Period3 → 2.5
Period4 → 0.9

なら、全期間PF1.412という結果は、一部期間に強く依存していたことになります。


年別分析も行う

4期間分割だけでは、期間の区切り方によって結果が変わる可能性があります。

そこで、取引データに複数年が含まれている場合には年別分析も行います。

例えば、

2020年 Q3 PF
2021年 Q3 PF
2022年 Q3 PF
...

を確認します。

4分割分析と年別分析の両方でQ3が安定していれば、

Q3にはある程度時間を超えて再現する特徴がある

可能性が高まります。


累積損益曲線を見る

最後にQ1~Q4ごとの累積損益を作ります。

analysis_df["cumulative_profit"] = (
    analysis_df
    .groupby(
        "relative_band_group",
        observed=True
    )
    ["profit"]
    .cumsum()
)

これをグラフ化します。

例えばQ3が、

バックテスト期間全体を通じて右肩上がり

ならかなり理想的です。

逆に、

一度大きく利益を伸ばした後、長期間横ばいまたは下落

であれば、PFだけでは見えなかった不安定性があります。


分析結果

今回は、これまで注目してきたRelative Band Distance Q3の優位性が、

特定の期間だけに依存したものではないか

を確認するため、バックテスト期間に時間軸を加えて分析しました。

まず全取引を時系列順に並べ、取引数がおおむね均等になるよう4期間に分割しました。

その結果、各グループのPFは次のようになりました。

期間Q1Q2Q3Q4
Period11.3921.1581.2731.309
Period21.5511.6061.6591.370
Period30.7562.2681.4351.488
Period41.8390.6901.4011.022

ここで非常に興味深い結果が出ました。

Q3だけが4期間すべてでPF1を上回りました。


Q3は4期間すべてで利益を残した

Q3だけを取り出すと、

Period1  PF 1.273
Period2  PF 1.659
Period3  PF 1.435
Period4  PF 1.401

となりました。

最も低いPeriod1でもPF1.273です。

さらに各期間の総損益も、

Period1  +13,569.19
Period2  +16,516.05
Period3  +18,851.95
Period4  +12,409.11

と、4期間すべてでプラスでした。

これは今回かなり重要な結果だと思います。

これまでQ3は、全期間でPF1.412と最も高い結果になっていました。

しかしそれだけでは、

ある一時期だけ大きく稼いだため、全体のPFが高くなっている

可能性を否定できませんでした。

今回4期間すべてでPF1を上回ったことで、

Q3の優位性は特定期間だけの一発ではない

可能性がかなり高まりました。


Period2だけが突出しているわけでもない

Q3の最高PFはPeriod2の1.659でした。

確かにこの期間は強いです。

しかし、

1.273
1.659
1.435
1.401

という結果を見ると、

Period2だけが極端に高く、それ以外はほぼPF1

という形ではありません。

Period3、Period4でも1.4前後を維持しています。

これは、個人的にはかなり好ましい結果です。

過去データから良い条件を探していると、

0.9
0.8
2.8
1.0

のように、一つの期間だけ突出して全体成績を押し上げているケースがあります。

今回のQ3ではそのような形ではありませんでした。


他のグループは期間によって大きく崩れた

Q3と比較すると、他のグループの不安定さがよく分かります。

Q1

Period1  1.392
Period2  1.551
Period3  0.756
Period4  1.839

Period3でPF0.756まで低下しています。

最終的には全期間で利益が残っていますが、

特定の相場環境ではかなり機能しなくなる

可能性があります。


Q2

Q2はさらに極端です。

Period1  1.158
Period2  1.606
Period3  2.268
Period4  0.690

Period3ではPF2.268と非常に強い一方、直近側のPeriod4ではPF0.690まで低下しました。

累積損益グラフでも、2024年頃をピークに、その後大きく利益を減らしています。

つまりQ2は、

強い時期にはかなり強いが、相場環境が変わると急激に機能しなくなる

タイプに見えます。


Q4

Q4は、

Period1  1.309
Period2  1.370
Period3  1.488
Period4  1.022

でした。

4期間すべてPF1以上ではありますが、Period4では1.022まで低下しています。

前回までの分析でQ4は、

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

ことが分かっています。

今回の累積損益を見ても、Q4は途中で大きく跳ねる局面が多く、Q3よりかなり凸凹しています。

特に2025年頃には累積損益が一気に大きく上昇しており、前回確認した「ホームラン型」という性質とも整合します。


累積損益を見るとQ3の特徴がより分かりやすい

Q3単独の累積損益を見ると、2015年当初には一時、

約-1万円

まで落ち込んでいます。

しかしその後回復し、2016年頃からプラス圏へ入りました。

以降は上下を繰り返しながらも、

長期的にはかなり明確な右肩上がり

となっています。

特に2018年頃から2022年頃にかけては、累積利益が、

約1万円
↓
約6万円

まで大きく伸びました。

一方、2022年後半以降は少し様子が変わっています。

一度約6.4万円まで上昇した後、

約4.7万円付近までドローダウン

しています。

その後は回復し、2026年には再び約6.2万円まで上昇しました。

つまり、

一本調子で右肩上がり

ではありません。

しかし、

利益が特定の1回だけで作られているわけでもない

ことはグラフからかなり明確です。

複数の時期に利益を積み上げています。


Q3にも停滞期はある

ここは重要なので、良い結果だけを見るのは避けます。

Q3は安定しているとはいっても、

常に利益を出し続けている

わけではありません。

例えば2022年後半から2024年頃にかけては、大きなドローダウンと横ばい期間があります。

累積損益は一時約6.4万円まで伸びた後、約4.7万円まで低下しています。

その差はおよそ1.7万円です。

その後回復していますが、

Q3を使えばドローダウンがなくなる

ということではありません。

今回分かったのは、

長期的に見ると、Q3は他のグループより時間的な再現性が高そう

ということです。


年別で見るとさらに細かい違いが見える

年別PFも確認しました。

Q3は、

PF
20151.540
20161.105
20170.909
20182.168
20192.520
20201.501
20212.964
20221.110
20230.830
20241.329
20253.152
20261.027

となりました。

12年間のうち、

10年でPF1を上回っています。

PF1を下回ったのは、

2017年  0.909
2023年  0.830

の2年だけでした。

4期間分割より細かく見ると、当然ながらマイナスになる年も存在します。

しかし12年中10年でPF1以上という結果は、少なくとも、

数年間だけ偶然機能した条件

とは考えにくそうです。


ただし年別PFには注意が必要

一方で、年別PFをそのまま比較しすぎるのも危険です。

1年間のQ3取引数は数十回程度しかありません。

サンプル数が少なくなるほど、

数回の大きな勝ち負けでPFが大きく動く

からです。

実際、2025年のQ3はPF3.152ですが、こうした高い値を、

Q3は2025年に非常に強かった

と単純に解釈するのは慎重にした方がよさそうです。

そのため今回の主判断は、

4期間分割

を中心にし、年別分析は補助的な確認と考えます。


ここまででQ3の仮説はかなり強くなった

Q3について、ここ数回の結果を整理すると、

第19回

Relative Band Distanceを分析。

Q3のPF1.412が最高。

第20回

大勝ち依存度を分析。

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

PF1.120

と4グループ中最も高い耐性。

第21回

時間安定性を分析。

Period1  1.273
Period2  1.659
Period3  1.435
Period4  1.401

と、

4期間すべてでPF1以上。

年別でも、

12年中10年でPF1以上。

ここまで来ると、

Relative Band Distance Q3には何らかの優位性がある

という仮説は、かなり強くなってきたと思います。


ただし、まだ「Q3を採用」はしない

ここで、

ではQ3だけエントリーすればいい

と考えたくなります。

しかし、まだ一つ大きな問題があります。

Q3という区分は、

今回使用した過去データを四分位に分けた結果

として作られています。

前回のQ3は、

Relative Band Distance
約0.44~0.88

でした。

しかし、

0.44以上なら突然優位性が発生する

0.88を超えた瞬間に突然悪化する

とは考えにくいです。

もし、

0.44~0.88

という狭い範囲だけが偶然高成績で、

0.40~0.84

や、

0.48~0.92

にすると成績が崩れるのであれば、

それは典型的な過剰最適化の可能性があります。

次に調べるべきなのは、

Q3という四分位そのもの

ではありません。

Relative Band Distanceの値を少しずつ動かしたとき、成績がどう変化するか

です。

ここからは、フィルター候補として実際に使えるのかを検証していきます。


今回のまとめ

今回の分析では、Relative Band Distance Q3の時間的な安定性を調べました。

4期間に分割したところ、

1.273
1.659
1.435
1.401

と、4期間すべてでPF1を上回りました。

また年別でも、12年間のうち10年でPF1以上となりました。

さらに累積損益を見ると、途中に大きなドローダウンや停滞期はあるものの、

長期間にわたって複数回利益を積み上げながら、最終的には右肩上がり

となっています。

これまで、

PFが高い

だけだったQ3に、

大勝ちだけに依存していない

時間を分けても利益が残る

という特徴が加わりました。

Relative Band Distance Q3は、現時点では零号機のエントリーフィルター候補として最も興味深い条件です。

しかし、まだ採用はしません。

次は四分位という区切りを一度捨てて、Relative Band Distanceそのものを連続的に動かしてみます。


今回のコード全文

ファイル名:

relative_band_time_analysis.py

コード全文を見る
import os
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_PERIOD_CSV = os.path.join(
    OUTPUT_DIR,
    "relative_band_period_analysis.csv"
)

OUTPUT_PERIOD_PF_CSV = os.path.join(
    OUTPUT_DIR,
    "relative_band_period_pf.csv"
)

OUTPUT_YEARLY_CSV = os.path.join(
    OUTPUT_DIR,
    "relative_band_yearly_analysis.csv"
)

OUTPUT_YEARLY_PF_CSV = os.path.join(
    OUTPUT_DIR,
    "relative_band_yearly_pf.csv"
)

OUTPUT_PERIOD_PF_GRAPH = os.path.join(
    OUTPUT_DIR,
    "relative_band_period_pf.png"
)

OUTPUT_CUMULATIVE_GRAPH = os.path.join(
    OUTPUT_DIR,
    "relative_band_cumulative_profit.png"
)

OUTPUT_Q3_PERIOD_GRAPH = os.path.join(
    OUTPUT_DIR,
    "q3_period_profit_factor.png"
)

OUTPUT_Q3_CUMULATIVE_GRAPH = os.path.join(
    OUTPUT_DIR,
    "q3_cumulative_profit.png"
)


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

df = pd.read_csv(
    INPUT_FILE
)


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

print(
    "RELATIVE BAND TIME ANALYSIS"
)

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

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


# ============================================================
# 日時列の判定
# ============================================================

time_candidates = [
    "entry_time",
    "entry_datetime",
    "open_time",
    "EntryTime",
    "ENTRY_TIME"
]


time_column = None


for candidate in time_candidates:

    if candidate in df.columns:

        time_column = candidate

        break


if time_column is None:

    raise ValueError(
        "取引日時の列が見つかりません。"
        "entry_time等の列名を確認してください。"
    )


print(
    f"使用する日時列: {time_column}"
)


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

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


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 [
    "entry_band_distance_pips",
    "entry_atr14_pips",
    "profit"
]:

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


# ============================================================
# 日時変換
# ============================================================

df[time_column] = pd.to_datetime(
    df[time_column],
    errors="coerce"
)


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

analysis_df = df[
    df[
        "entry_band_distance_pips"
    ].notna()
    &
    df[
        "entry_atr14_pips"
    ].notna()
    &
    df[
        "profit"
    ].notna()
    &
    df[
        time_column
    ].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()


# ============================================================
# 時系列順に並び替え
# ============================================================

analysis_df = (
    analysis_df
    .sort_values(
        time_column
    )
    .reset_index(
        drop=True
    )
)


# ============================================================
# 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
)


# ============================================================
# 年
# ============================================================

analysis_df[
    "year"
] = (
    analysis_df[
        time_column
    ]
    .dt
    .year
)


# ============================================================
# 取引順による4期間分割
# ============================================================

number_of_trades = len(
    analysis_df
)


analysis_df[
    "trade_order"
] = np.arange(
    number_of_trades
)


analysis_df[
    "time_period"
] = pd.qcut(
    analysis_df[
        "trade_order"
    ],
    q=4,
    labels=[
        "Period1",
        "Period2",
        "Period3",
        "Period4"
    ]
)


period_order = [
    "Period1",
    "Period2",
    "Period3",
    "Period4"
]


# ============================================================
# 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
    )


# ============================================================
# 成績計算関数
# ============================================================

def calculate_summary(data):

    trades = len(
        data
    )


    if trades == 0:

        return {
            "trades": 0,
            "wins": 0,
            "losses": 0,
            "win_rate": np.nan,
            "total_profit": np.nan,
            "average_profit": np.nan,
            "median_profit": np.nan,
            "profit_factor": np.nan
        }


    wins = (
        data[
            "profit"
        ]
        > 0
    ).sum()


    losses = (
        data[
            "profit"
        ]
        < 0
    ).sum()


    win_rate = (
        wins
        /
        trades
        * 100
    )


    return {
        "trades": trades,

        "wins": wins,

        "losses": losses,

        "win_rate": win_rate,

        "total_profit": (
            data[
                "profit"
            ]
            .sum()
        ),

        "average_profit": (
            data[
                "profit"
            ]
            .mean()
        ),

        "median_profit": (
            data[
                "profit"
            ]
            .median()
        ),

        "profit_factor": (
            calculate_pf(
                data
            )
        )
    }


# ============================================================
# 4期間 × Q1~Q4 分析
# ============================================================

period_rows = []


for period in period_order:

    for group in quartile_order:

        group_df = analysis_df[
            (
                analysis_df[
                    "time_period"
                ]
                == period
            )
            &
            (
                analysis_df[
                    "relative_band_group"
                ]
                == group
            )
        ].copy()


        stats = calculate_summary(
            group_df
        )


        period_rows.append(
            {
                "period": period,

                "group": group,

                **stats
            }
        )


period_df = pd.DataFrame(
    period_rows
)


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

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


for column in [
    "total_profit",
    "average_profit",
    "median_profit"
]:

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


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


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

print(
    "4期間 × Relative Band Distance"
)

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


print(
    period_df
)


period_df.to_csv(
    OUTPUT_PERIOD_CSV,
    index=False,
    encoding="utf-8-sig"
)


# ============================================================
# 期間別PF表
# ============================================================

period_pf = (
    period_df
    .pivot(
        index="period",
        columns="group",
        values="profit_factor"
    )
)


period_pf = period_pf.reindex(
    index=period_order,
    columns=quartile_order
)


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

print(
    "期間別PF"
)

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


print(
    period_pf
)


period_pf.to_csv(
    OUTPUT_PERIOD_PF_CSV,
    encoding="utf-8-sig"
)


# ============================================================
# 年別分析
# ============================================================

yearly_rows = []


years = sorted(
    analysis_df[
        "year"
    ]
    .dropna()
    .unique()
)


for year in years:

    for group in quartile_order:

        group_df = analysis_df[
            (
                analysis_df[
                    "year"
                ]
                == year
            )
            &
            (
                analysis_df[
                    "relative_band_group"
                ]
                == group
            )
        ].copy()


        stats = calculate_summary(
            group_df
        )


        yearly_rows.append(
            {
                "year": int(
                    year
                ),

                "group": group,

                **stats
            }
        )


yearly_df = pd.DataFrame(
    yearly_rows
)


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


for column in [
    "total_profit",
    "average_profit",
    "median_profit"
]:

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


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


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

print(
    "年別成績"
)

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


print(
    yearly_df
)


yearly_df.to_csv(
    OUTPUT_YEARLY_CSV,
    index=False,
    encoding="utf-8-sig"
)


# ============================================================
# 年別PF
# ============================================================

yearly_pf = (
    yearly_df
    .pivot(
        index="year",
        columns="group",
        values="profit_factor"
    )
)


yearly_pf = yearly_pf.reindex(
    columns=quartile_order
)


yearly_pf.to_csv(
    OUTPUT_YEARLY_PF_CSV,
    encoding="utf-8-sig"
)


# ============================================================
# 期間別PFグラフ
# ============================================================

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


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


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


ax.set_xlabel(
    "Time Period"
)


ax.set_ylabel(
    "Profit Factor"
)


ax.set_title(
    "Profit Factor by Time Period and Relative Band Distance"
)


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


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


plt.tight_layout()


plt.savefig(
    OUTPUT_PERIOD_PF_GRAPH,
    dpi=150
)


plt.close()


# ============================================================
# Q3のみ期間別PF
# ============================================================

q3_period = (
    period_pf[
        "Q3"
    ]
)


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


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


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


ax.set_xlabel(
    "Time Period"
)


ax.set_ylabel(
    "Profit Factor"
)


ax.set_title(
    "Q3 Profit Factor by Time Period"
)


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


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


plt.tight_layout()


plt.savefig(
    OUTPUT_Q3_PERIOD_GRAPH,
    dpi=150
)


plt.close()


# ============================================================
# Q1~Q4累積損益
# ============================================================

fig, ax = plt.subplots(
    figsize=(11, 7)
)


for group in quartile_order:

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


    group_df = (
        group_df
        .sort_values(
            time_column
        )
    )


    group_df[
        "cumulative_profit"
    ] = (
        group_df[
            "profit"
        ]
        .cumsum()
    )


    ax.plot(
        group_df[
            time_column
        ],
        group_df[
            "cumulative_profit"
        ],
        label=group
    )


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


ax.set_xlabel(
    "Time"
)


ax.set_ylabel(
    "Cumulative Profit"
)


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


ax.legend()


ax.grid(
    alpha=0.3
)


plt.tight_layout()


plt.savefig(
    OUTPUT_CUMULATIVE_GRAPH,
    dpi=150
)


plt.close()


# ============================================================
# Q3のみ累積損益
# ============================================================

q3_df = analysis_df[
    analysis_df[
        "relative_band_group"
    ]
    == "Q3"
].copy()


q3_df = (
    q3_df
    .sort_values(
        time_column
    )
)


q3_df[
    "cumulative_profit"
] = (
    q3_df[
        "profit"
    ]
    .cumsum()
)


fig, ax = plt.subplots(
    figsize=(11, 7)
)


ax.plot(
    q3_df[
        time_column
    ],
    q3_df[
        "cumulative_profit"
    ]
)


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


ax.set_xlabel(
    "Time"
)


ax.set_ylabel(
    "Cumulative Profit"
)


ax.set_title(
    "Q3 Cumulative Profit"
)


ax.grid(
    alpha=0.3
)


plt.tight_layout()


plt.savefig(
    OUTPUT_Q3_CUMULATIVE_GRAPH,
    dpi=150
)


plt.close()


# ============================================================
# Q3の期間安定性を表示
# ============================================================

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

print(
    "Q3 PERIOD STABILITY"
)

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


q3_stability = period_df[
    period_df[
        "group"
    ]
    == "Q3"
][
    [
        "period",
        "trades",
        "win_rate",
        "total_profit",
        "average_profit",
        "profit_factor"
    ]
]


print(
    q3_stability
)


# ============================================================
# Q3 PF > 1 の期間数
# ============================================================

q3_positive_periods = (
    q3_stability[
        "profit_factor"
    ]
    > 1
).sum()


print(
    "\nQ3 PF > 1 の期間数:",
    f"{q3_positive_periods} / 4"
)


# ============================================================
# Q3 PFの平均・標準偏差
# ============================================================

q3_pf_mean = (
    q3_stability[
        "profit_factor"
    ]
    .mean()
)


q3_pf_std = (
    q3_stability[
        "profit_factor"
    ]
    .std()
)


print(
    f"Q3 期間別PF平均: {q3_pf_mean:.3f}"
)


print(
    f"Q3 期間別PF標準偏差: {q3_pf_std:.3f}"
)


# ============================================================
# 出力ファイル
# ============================================================

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

print(
    "分析完了"
)

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


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


print(
    OUTPUT_PERIOD_CSV
)


print(
    OUTPUT_PERIOD_PF_CSV
)


print(
    OUTPUT_YEARLY_CSV
)


print(
    OUTPUT_YEARLY_PF_CSV
)


print(
    OUTPUT_PERIOD_PF_GRAPH
)


print(
    OUTPUT_CUMULATIVE_GRAPH
)


print(
    OUTPUT_Q3_PERIOD_GRAPH
)


print(
    OUTPUT_Q3_CUMULATIVE_GRAPH
)

出力されるファイル

results/
├─ relative_band_period_analysis.csv
├─ relative_band_period_pf.csv
├─ relative_band_yearly_analysis.csv
├─ relative_band_yearly_pf.csv
├─ relative_band_period_pf.png
├─ relative_band_cumulative_profit.png
├─ q3_period_profit_factor.png
└─ q3_cumulative_profit.png

今回特に重要なのは、

relative_band_period_analysis.csv
relative_band_period_pf.csv
relative_band_yearly_pf.csv
relative_band_cumulative_profit.png
q3_cumulative_profit.png

です。

この結果を見れば、

Q3が本当に時間を超えて安定しているのか

かなり判断しやすくなるはずです。

関連記事

ランダムウォークへの挑戦㉑|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をコピーしました