ランダムウォークへの挑戦㉓|最高PFは本当に強いのか。Relative Band Distanceを時間分割で比較する(2026/8/30)

未分類

はじめに

前回は、Relative Band Distanceについて、四分位という区切りを一度捨て、

境界値を少し動かしても優位性が残るのか

を調べました。

これまで有力だったQ3は、

0.44 ~ 0.88

付近でした。

この条件のProfit Factorは約、

PF 1.40

でした。

しかし、第22回で下限と上限を細かく動かしたところ、興味深い結果が出ました。

特に目立ったのが、

Upper Threshold = 1.00

付近です。

例えば、

0.44 ~ 1.00
PF 約1.64

となり、元のQ3よりPFが大きく上昇しました。

さらに探索範囲の中で最もPFが高かったのは、

0.60 ~ 1.00

でした。

PFは約、

1.86

です。

数字だけを見ると、

では0.60~1.00を零号機へ入れればいい

となりそうです。

しかし、ここで一度止まります。


「最高PF」と「強い条件」は同じではない

バックテストを繰り返していると、どうしても、

一番PFの高い条件

を採用したくなります。

例えば、

A:0.44~0.88 → PF 1.40
B:0.44~1.00 → PF 1.64
C:0.50~1.00 → PF 1.71
D:0.60~1.00 → PF 1.86

なら、

D:0.60~1.00

を選びたくなります。

しかし、これは過去データ全体を使って計算した結果です。

もし0.60~1.00が、

前半では非常に強い
後半では弱い

という条件だった場合、

全期間で計算したPF1.86という数字だけを見るのは危険です。

一方、

PFは1.5程度

でも、

どの期間でもPF1を超えている

条件があるのであれば、そちらの方が将来使える可能性があります。

今回は、

最高PFを探す

のではなく、

時間が変わっても成績が残る条件を探す

ことにします。


今回比較する4つの条件

今回は、第19回から第22回までの結果を踏まえて、次の4条件を比較します。

A:0.44 ~ 0.88
B:0.44 ~ 1.00
C:0.50 ~ 1.00
D:0.60 ~ 1.00

それぞれ意味があります。


A|0.44~0.88

0.44 ~ 0.88

は、これまで分析してきたQ3相当の条件です。

いわば今回の、

基準

です。

第19回ではQ3のPFが最も高く、その後、

  • 大勝ち依存
  • 時間安定性
  • 年別成績

なども調べてきました。

したがって、新しい候補が本当に優秀なのかを見るための比較対象として残します。


B|0.44~1.00

0.44 ~ 1.00

は、第22回で特に注目した条件です。

Q3の下限はそのまま残しつつ、

0.88
↓
1.00

まで上限を広げています。

前回の結果では、

取引数を増やしながらPFも上昇する

というかなり興味深い動きを見せました。

また、

Relative Band Distance = 1.00

は、

バンドからの距離がATR約1個分

という意味になります。

単なる四分位点ではなく、相場の値動きとしても比較的解釈しやすい数字です。


C|0.50~1.00

3つ目は、

0.50 ~ 1.00

です。

下限を少し引き上げます。

考え方としては、

バンドをわずかに抜けただけのエントリーを除外する

というものです。

0.44と0.50には大きな違いはありません。

だからこそ、0.50に変更しても成績が大きく変わらないのであれば、

0.44という細かな境界値に依存していない

とも考えられます。

EAへ実装する場合にも、

0.50

という単純な値の方が扱いやすいです。


D|0.60~1.00

最後が、

0.60 ~ 1.00

です。

第22回の探索範囲で最高PFとなった条件です。

全期間では、

PF 約1.86

となりました。

かなり魅力的な数字です。

しかし逆に、今回もっとも疑うべき条件でもあります。

なぜなら、

過去データを探索した結果として見つかった最高値

だからです。

0.60~1.00が本当に強いのか。

それとも、

過去データにもっともきれいに当てはまっただけ

なのか。

今回の中心テーマになります。


時間を4分割する

まず、全取引をエントリー日時順に並べます。

そしてデータ全体を、

Period1
Period2
Period3
Period4

の4つに分割します。

イメージとしては、

過去
│
├─ Period1
├─ Period2
├─ Period3
└─ Period4
              現在

です。

重要なのは、

各候補ごとに別々の期間を作るのではない

という点です。

まず元データ全体を同じ時間軸で4分割し、その同じ期間の中で4候補を比較します。

これによって、

Period1ではAとBどちらが強いか
Period2ではどうか
Period3ではどうか
Period4ではどうか

を同じ条件で比較できます。


前半・後半でも比較する

4分割だけでなく、

First Half
Second Half

という2分割でも調べます。

4期間にすると細かな変化を見ることができますが、1期間あたりの取引数は減ります。

一方、前半と後半だけならサンプル数を多く確保できます。

そこで今回は、

4期間分析
+
前半・後半分析

の両方を行います。


年別でも確認する

さらに、これまでと同様に年別分析も行います。

例えば、

2014
2015
2016
...
2025

について各候補のPFを計算します。

ここでは、

最高PFだった年

より、

PF1を超えた年が何年あるか

を重視します。

例えば、

条件A
12年中10年 PF1以上

条件D
12年中7年 PF1以上

だった場合、

全期間ではDのPFが高くても、Aの方が時間的には安定している可能性があります。


今回見る数字

各条件について、次の数字を計算します。

取引数
勝率
総損益
平均損益
中央値
総利益
総損失
Profit Factor

さらに、4期間分析から、

期間平均PF
最低PF
最高PF
PFの標準偏差
PF1以上だった期間数

も計算します。

この中で特に注目するのは、

最低PF

です。


最高値より「最悪時」を見る

例えば、

条件A

Period1  1.40
Period2  1.35
Period3  1.50
Period4  1.30

だったとします。

平均PFは、

約1.39

です。

一方、

条件D

Period1  2.50
Period2  2.10
Period3  1.40
Period4  0.80

だった場合、平均PFは高くなる可能性があります。

しかし、

Period4 PF 0.80

です。

つまり、ある期間では期待値がマイナスになっています。

実際にEAを運用する立場からすると、

過去最高のPF

より、

環境が悪いときにどこまで崩れるのか

の方が重要かもしれません。

そこで今回は、

Minimum Period PF

も比較します。


PFの標準偏差も計算する

さらに、

PFの標準偏差

も計算します。

例えば、

1.40
1.42
1.38
1.44

なら変動は小さいです。

一方、

2.30
0.90
2.00
0.80

なら平均値が同程度だったとしても、かなり不安定です。

したがって、

PFが高い

だけでなく、

PFのばらつきが小さい

条件も評価します。


今回の理想形

今回もっとも期待しているのは、

全期間PFが高い

かつ

Period1~4すべてPF1以上

かつ

最低PFも高い

かつ

PF標準偏差が小さい

という条件です。

例えば、

0.50~1.00

全期間PF      1.65
平均期間PF    1.60
最低期間PF    1.35
PF1以上       4/4
標準偏差      小さい

という結果なら、かなり面白いです。

反対に、

0.60~1.00

全期間PF      1.86
最低期間PF    0.85
PF1以上       3/4
標準偏差      大きい

なら、

最高PFという理由だけで0.60~1.00を採用するのは危険

ということになります。


これはまだアウト・オブ・サンプルではない

ここで一つ注意があります。

今回時間を分割するとはいえ、

完全なアウト・オブ・サンプル検証ではありません。

今回比較する、

0.44~0.88
0.44~1.00
0.50~1.00
0.60~1.00

という候補自体が、これまでのデータ分析をもとに選ばれているからです。

つまり、同じデータの情報をある程度使っています。

今回確認できるのは、

発見した条件が過去の特定期間だけに依存していないか

という時間安定性です。

本当に未知のデータに対する性能を調べるには、その先に、

アウト・オブ・サンプル
ウォークフォワード
フォワードテスト

などが必要になります。

それはもう少し先でやります。

今は、一つずつ怪しい条件を落としていきます。


分析方法

今回のPythonコードでは、まずRelative Band Distanceを、

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

として計算します。

候補条件は、

CANDIDATES = {
    "A_0.44_0.88": (0.44, 0.88),
    "B_0.44_1.00": (0.44, 1.00),
    "C_0.50_1.00": (0.50, 1.00),
    "D_0.60_1.00": (0.60, 1.00),
}

とします。


期間は元データ全体で決める

全データをエントリー時間順に並べ、

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

とします。

その上で、

Period1
Period2
Period3
Period4

を割り当てます。

これによって、どの候補も同じ時期について比較できます。


出力するグラフ

今回は主に4つのグラフを作ります。

① 4期間のPF

candidate_period_pf.png

4つの条件についてPeriod1~4のPFを折れ線グラフにします。

これが今回もっとも重要なグラフです。


② 前半・後半のPF

candidate_half_pf.png

前半と後半で成績が大きく変わっていないかを確認します。


③ 年別PF

candidate_year_pf.png

各条件について年ごとのPFを表示します。


④ 最低PFと平均PF

candidate_stability_pf.png

各候補について、

平均期間PF
最低期間PF

を比較します。

全期間PFだけでは分からない、

最悪の期間でもどれくらい耐えているか

を見るためのグラフです。


分析結果

今回比較した条件は、

A:0.44 ~ 0.88
B:0.44 ~ 1.00
C:0.50 ~ 1.00
D:0.60 ~ 1.00

の4つです。

まず全期間の成績を確認すると、

A:取引数441 総損益 +60,876円 PF 1.397
B:取引数534 総損益 +117,305円 PF 1.639
C:取引数452 総損益 +108,243円 PF 1.708
D:取引数344 総損益  +94,287円 PF 1.861

となりました。

全期間だけを見れば、やはり、

D:0.60 ~ 1.00

がPF1.861でトップです。

平均損益についても、

A:+138円
B:+220円
C:+239円
D:+274円

となり、条件を狭めるほど高くなっています。

ここだけを見ると、

やはり0.60~1.00が一番良い

と考えたくなります。

しかし、時間軸で分解すると景色がかなり変わりました。


Aは4期間を通じてほとんど成績が変わらない

まず、これまでのQ3相当である、

A:0.44 ~ 0.88

です。

4期間のPFは、

Period1:1.280
Period2:1.440
Period3:1.458
Period4:1.458

となりました。

すべてPF1を上回っています。

それだけではなく、

平均PF:1.409
最低PF:1.280
最高PF:1.458
標準偏差:0.075

です。

今回の4候補の中で、

PFのばらつきが圧倒的に小さい

結果となりました。

グラフを見ても、Period1からPeriod4までほぼ横ばいです。

第21回でもQ3の時間安定性を確認しましたが、今回ほかの候補と直接比較したことで、

Aの特徴はPFの高さではなく、時間的な安定性にある

ことがよりはっきりしました。


BはPFを高めながら取引数も増えた

次に、

B:0.44 ~ 1.00

です。

4期間では、

Period1:1.787
Period2:1.768
Period3:1.729
Period4:1.221

となりました。

Period1~3までは非常に安定しています。

しかもPF1.7台です。

Aより明らかに高い水準を維持しています。

一方で、Period4になると、

PF 1.221

まで低下しました。

それでもPF1は割っていません。

また全期間では、

取引数:534
PF:1.639
総損益:+117,305円

です。

Aより取引数が約21%多いにもかかわらず、PFも高くなっています。

つまりBは、

取引機会を増やしながら期待値も高めた

というかなり魅力的な条件です。

ただし、

標準偏差:0.235

であり、Aの0.075に比べると時間変動は大きくなりました。


Cは平均PFがさらに高い

C:0.50 ~ 1.00

では、

Period1:1.820
Period2:2.211
Period3:1.649
Period4:1.274

となりました。

平均期間PFは、

1.738

で、Bよりさらに高くなっています。

最低PFも、

1.274

です。

これは面白い結果です。

Aの最低PFが、

1.280

なので、最低値だけを見るとほぼ同じです。

しかしCは、全期間PFが、

1.708

あります。

つまり、

最悪期のPFはAとほぼ同じなのに、全期間の収益性はかなり高い

ということになります。

ただし、標準偏差は、

0.337

まで上昇します。

Period2のPF2.211など、良い期間の恩恵をかなり受けています。


Dは最高PFだったが、もっとも不安定だった

そして、第22回で最もPFが高かった、

D:0.60 ~ 1.00

です。

結果は、

Period1:2.554
Period2:2.041
Period3:1.646
Period4:1.091

となりました。

全期間PFは、

1.861

です。

数字だけなら4候補で最も高いです。

しかしPeriod1からPeriod4へ、

2.554
↓
2.041
↓
1.646
↓
1.091

と、ほぼ一貫して低下しています。

これが今回もっとも気になった結果です。

標準偏差も、

0.536

で4候補中最大です。

さらに最低PFは、

1.091

まで落ちています。

PF1は辛うじて維持していますが、

全期間PF1.86という数字から想像するほど頑丈ではない

ことが分かりました。


前半・後半に分けると傾向はさらに明確

次に、データ全体を前半・後半に分けました。

結果は、

A
前半:1.339
後半:1.458

B
前半:1.780
後半:1.492

C
前半:1.944
後半:1.473

D
前半:2.368
後半:1.370

です。

非常に分かりやすい結果になりました。

Aだけ、

1.339
→
1.458

と後半でむしろ改善しています。

一方、

B
1.780 → 1.492

C
1.944 → 1.473

D
2.368 → 1.370

と、上限1.00を採用した3条件はいずれも後半でPFが低下しました。

特にDは、

2.368
→
1.370

と大きく落ちています。

つまり、

0.60~1.00の高PFは、データ前半の非常に強い成績にかなり支えられている

可能性があります。


DのPF1.86には「昔の強さ」がかなり含まれている

Period1を見ると、

D:PF 2.554

です。

さらに年別では、2015年に、

PF 8.510

という極端に高い値が出ています。

2015年のDは、

取引数:29
総損益:+54,452円
PF:8.510

です。

全期間の総損益94,287円に対して、2015年だけで約54,000円を稼いでいます。

つまり単純計算では、

Dの全期間利益の半分以上が2015年だけで発生している

ことになります。

これはかなり重要です。

DのPF1.86という数字をそのまま現在の期待値と考えるのは危険そうです。


近年になるほど条件を狭めるメリットが消えている

年別を見ると、さらに興味深いことが分かります。

2022年は、

A:1.108
B:1.445
C:1.194
D:1.092

2023年は、

A:0.893
B:0.966
C:0.925
D:0.928

2024年は、

A:1.316
B:1.264
C:1.241
D:1.166

2025年は、

A:3.411
B:3.069
C:4.247
D:3.132

2026年は、

A:1.027
B:0.523
C:0.446
D:0.265

となっています。

特に2026年はかなり特徴的です。

Relative Band Distanceの下限を高くするほど、

A:1.027
B:0.523
C:0.446
D:0.265

と悪化しています。

2026年はまだ途中であり取引数も少ないため、それだけで結論を出すことはできません。

ただ、

最近の相場では0.60~1.00という狭い条件がむしろ弱くなっている

可能性は意識しておく必要があります。


年単位ではAが最も安定

PF1を超えた年数を見ると、

A:10 / 12年
B: 8 / 12年
C: 8 / 12年
D: 9 / 12年

でした。

割合にすると、

A:83.3%
B:66.7%
C:66.7%
D:75.0%

です。

最低年PFについても、

A:0.893
B:0.523
C:0.446
D:0.265

となりました。

この数字はかなり印象的です。

条件を狭めるほど全期間PFは上がりますが、

最悪の年の成績は逆に悪化する

傾向が見えています。


「PFを上げるほど安定性が下がる」というトレードオフ

今回の4条件を並べると、

             全期間PF   最低期間PF   PF標準偏差
A 0.44-0.88    1.397      1.280       0.075
B 0.44-1.00    1.639      1.221       0.235
C 0.50-1.00    1.708      1.274       0.337
D 0.60-1.00    1.861      1.091       0.536

となります。

きれいなトレードオフが見えます。

条件を狭める

↓

全期間PFは高くなる

↓

しかし時間によるPFの変動も大きくなる

という関係です。

Dはもっとも稼げた条件ですが、もっとも不安定です。

AはもっともPFが低いですが、もっとも安定しています。


では、どの条件が一番良いのか

今回の結果だけで一つ選ぶなら、

D:0.60~1.00

ではないと思います。

Dは確かに最高PFですが、

前半PF:2.368
後半PF:1.370

であり、近年になるほど成績が低下しています。

一方Aは、

前半PF:1.339
後半PF:1.458

で、時間的な安定性は非常に高いです。

ただ、Aには、

全期間PF:1.397
総損益:約6.1万円

という弱点があります。

BやCに比べると収益性をかなり捨てています。

そこで個人的に今回もっとも気になるのは、

C:0.50~1.00

です。

Cは、

全期間PF:1.708
総損益:+108,243円
最低期間PF:1.274
4期間すべてPF1以上

です。

最低期間PFはAとほぼ同じなのに、全期間PFはかなり高くなっています。

ただし、

年別PF1以上:8/12年
2026年PF:0.446

という弱点もあります。

したがって、

Aは安定性重視

Cは収益性と安定性のバランス型

Dは高収益だが過去依存の疑いが強い

という整理が現時点では最もしっくりきます。


今回分かったこと

第22回では、

Relative Band Distance 1.00付近に強い領域がある

ことを発見しました。

そして今回は、

その強さが時間を変えても残るか

を調べました。

結果として、

0.60~1.00

は確かに全期間では非常に強いものの、その強さは時間的に一定ではありませんでした。

一方、従来のQ3相当である、

0.44~0.88

はPFこそ低いものの、4期間を通じて非常に安定しています。

つまり今回得られたのは、

「最適な境界値」

ではなく、

収益性と頑丈さの間にトレードオフが存在する

という結果でした。

これは零号機にルールを追加する上で、かなり重要な発見だと思います。


次回

ここまでで、

A:安定性が高い
C:収益性とのバランスが良い
D:最高PFだが時間依存が大きい

という特徴が分かってきました。

しかし、まだ重要な指標を見ていません。

それが、

ドローダウン

です。

これまで主にProfit Factorや総損益を使って条件を比較してきました。

しかし実際にEAを運用する場合、

PFが高い

ことと、

安心して運用できる

ことは同じではありません。

例えば、

条件A
PF 1.40
最大DD 10%

条件C
PF 1.71
最大DD 25%

条件D
PF 1.86
最大DD 40%

なら、単純にDを選ぶべきとは言えません。

逆にCやDで、

PF上昇
+
最大DD低下

が同時に起きるなら、Relative Band Distanceフィルターを採用する価値はかなり高くなります。


第24回では4条件を「資産曲線」で比較する

次回は、

フィルターなし
A:0.44~0.88
B:0.44~1.00
C:0.50~1.00
D:0.60~1.00

について、同じ初期資金から資産曲線を作ります。

そして、

総損益
PF
最大ドローダウン額
最大ドローダウン率
取引数
平均損益

を比較します。

特に注目するのは、

Relative Band Distanceを絞ることで、負けトレードを効率よく排除できているのか

です。

今までは、

残した取引だけ

を見てきました。

しかし零号機へフィルターを追加するということは、

条件外の取引をすべて捨てる

ということでもあります。

そこで次回は、

元の零号機
↓
各Relative Band Distanceフィルター

という形で、EA全体の運用成績として比較します。


今回のコード全文

ファイル名は、

relative_band_time_stability.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
)


CANDIDATES = {
    "A_0.44_0.88": (
        0.44,
        0.88
    ),
    "B_0.44_1.00": (
        0.44,
        1.00
    ),
    "C_0.50_1.00": (
        0.50,
        1.00
    ),
    "D_0.60_1.00": (
        0.60,
        1.00
    ),
}


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


HALF_LABELS = [
    "First Half",
    "Second Half"
]


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

OVERALL_CSV = os.path.join(
    OUTPUT_DIR,
    "candidate_overall_analysis.csv"
)

PERIOD_CSV = os.path.join(
    OUTPUT_DIR,
    "candidate_period_analysis.csv"
)

HALF_CSV = os.path.join(
    OUTPUT_DIR,
    "candidate_half_analysis.csv"
)

YEAR_CSV = os.path.join(
    OUTPUT_DIR,
    "candidate_year_analysis.csv"
)

STABILITY_CSV = os.path.join(
    OUTPUT_DIR,
    "candidate_time_stability_summary.csv"
)

PERIOD_PF_GRAPH = os.path.join(
    OUTPUT_DIR,
    "candidate_period_pf.png"
)

HALF_PF_GRAPH = os.path.join(
    OUTPUT_DIR,
    "candidate_half_pf.png"
)

YEAR_PF_GRAPH = os.path.join(
    OUTPUT_DIR,
    "candidate_year_pf.png"
)

STABILITY_GRAPH = os.path.join(
    OUTPUT_DIR,
    "candidate_stability_pf.png"
)


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

df = pd.read_csv(
    INPUT_FILE
)


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

print(
    "RELATIVE BAND TIME STABILITY ANALYSIS"
)

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

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


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

required_columns = [
    "entry_time",
    "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
        )
    )


# ============================================================
# 型変換
# ============================================================

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


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


for column in numeric_columns:

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


# ============================================================
# 分析対象データ
# ============================================================

analysis_df = df[
    df[
        "entry_time"
    ].notna()
    &
    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(
        "分析対象データがありません。"
    )


# ============================================================
# 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(
        "entry_time"
    )
    .reset_index(
        drop=True
    )
)


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


print(
    f"分析開始日: "
    f"{analysis_df['entry_time'].min()}"
)

print(
    f"分析終了日: "
    f"{analysis_df['entry_time'].max()}"
)


# ============================================================
# Period1~4を作成
# ============================================================

period_numbers = pd.qcut(
    np.arange(
        len(
            analysis_df
        )
    ),
    q=4,
    labels=PERIOD_LABELS
)


analysis_df[
    "period"
] = period_numbers


# ============================================================
# 前半・後半を作成
# ============================================================

half_numbers = pd.qcut(
    np.arange(
        len(
            analysis_df
        )
    ),
    q=2,
    labels=HALF_LABELS
)


analysis_df[
    "half"
] = half_numbers


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

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


# ============================================================
# 期間情報表示
# ============================================================

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

print(
    "4期間の日時範囲"
)

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


for period in PERIOD_LABELS:

    period_df = analysis_df[
        analysis_df[
            "period"
        ]
        == period
    ]

    print(
        f"\n{period}"
    )

    print(
        f"開始: "
        f"{period_df['entry_time'].min()}"
    )

    print(
        f"終了: "
        f"{period_df['entry_time'].max()}"
    )

    print(
        f"元データ件数: "
        f"{len(period_df)}"
    )


# ============================================================
# 指標計算関数
# ============================================================

def calculate_metrics(
    selected
):

    trade_count = len(
        selected
    )


    if trade_count == 0:

        return {
            "trade_count": 0,
            "win_count": 0,
            "loss_count": 0,
            "win_rate": np.nan,
            "total_profit": np.nan,
            "average_profit": np.nan,
            "median_profit": np.nan,
            "gross_profit": np.nan,
            "gross_loss": np.nan,
            "profit_factor": np.nan
        }


    win_count = (
        selected[
            "profit"
        ]
        > 0
    ).sum()


    loss_count = (
        selected[
            "profit"
        ]
        < 0
    ).sum()


    win_rate = (
        win_count
        /
        trade_count
        *
        100
    )


    total_profit = (
        selected[
            "profit"
        ]
        .sum()
    )


    average_profit = (
        selected[
            "profit"
        ]
        .mean()
    )


    median_profit = (
        selected[
            "profit"
        ]
        .median()
    )


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


    gross_loss = -(
        selected.loc[
            selected[
                "profit"
            ]
            < 0,
            "profit"
        ]
        .sum()
    )


    if gross_loss > 0:

        profit_factor = (
            gross_profit
            /
            gross_loss
        )

    else:

        profit_factor = np.nan


    return {
        "trade_count": trade_count,
        "win_count": win_count,
        "loss_count": loss_count,
        "win_rate": win_rate,
        "total_profit": total_profit,
        "average_profit": average_profit,
        "median_profit": median_profit,
        "gross_profit": gross_profit,
        "gross_loss": gross_loss,
        "profit_factor": profit_factor
    }


# ============================================================
# 候補抽出関数
# ============================================================

def select_candidate(
    source_df,
    lower,
    upper
):

    selected = source_df[
        (
            source_df[
                "relative_band_distance"
            ]
            >= lower
        )
        &
        (
            source_df[
                "relative_band_distance"
            ]
            <= upper
        )
    ].copy()


    return selected


# ============================================================
# 全期間分析
# ============================================================

overall_results = []


for candidate_name, thresholds in CANDIDATES.items():

    lower = thresholds[0]
    upper = thresholds[1]


    selected = select_candidate(
        analysis_df,
        lower,
        upper
    )


    metrics = calculate_metrics(
        selected
    )


    row = {
        "candidate": candidate_name,
        "lower": lower,
        "upper": upper
    }


    row.update(
        metrics
    )


    overall_results.append(
        row
    )


overall_df = pd.DataFrame(
    overall_results
)


overall_df.to_csv(
    OVERALL_CSV,
    index=False,
    encoding="utf-8-sig"
)


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

print(
    "全期間分析"
)

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


print(
    overall_df[
        [
            "candidate",
            "lower",
            "upper",
            "trade_count",
            "win_rate",
            "total_profit",
            "average_profit",
            "median_profit",
            "profit_factor"
        ]
    ]
    .to_string(
        index=False
    )
)


# ============================================================
# 4期間分析
# ============================================================

period_results = []


for candidate_name, thresholds in CANDIDATES.items():

    lower = thresholds[0]
    upper = thresholds[1]


    for period in PERIOD_LABELS:

        source_df = analysis_df[
            analysis_df[
                "period"
            ]
            == period
        ]


        selected = select_candidate(
            source_df,
            lower,
            upper
        )


        metrics = calculate_metrics(
            selected
        )


        row = {
            "candidate": candidate_name,
            "lower": lower,
            "upper": upper,
            "period": period,
            "period_start": (
                source_df[
                    "entry_time"
                ].min()
            ),
            "period_end": (
                source_df[
                    "entry_time"
                ].max()
            )
        }


        row.update(
            metrics
        )


        period_results.append(
            row
        )


period_df = pd.DataFrame(
    period_results
)


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


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

print(
    "4期間分析"
)

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


print(
    period_df[
        [
            "candidate",
            "period",
            "trade_count",
            "win_rate",
            "total_profit",
            "average_profit",
            "median_profit",
            "profit_factor"
        ]
    ]
    .to_string(
        index=False
    )
)


# ============================================================
# 前半・後半分析
# ============================================================

half_results = []


for candidate_name, thresholds in CANDIDATES.items():

    lower = thresholds[0]
    upper = thresholds[1]


    for half in HALF_LABELS:

        source_df = analysis_df[
            analysis_df[
                "half"
            ]
            == half
        ]


        selected = select_candidate(
            source_df,
            lower,
            upper
        )


        metrics = calculate_metrics(
            selected
        )


        row = {
            "candidate": candidate_name,
            "lower": lower,
            "upper": upper,
            "half": half,
            "period_start": (
                source_df[
                    "entry_time"
                ].min()
            ),
            "period_end": (
                source_df[
                    "entry_time"
                ].max()
            )
        }


        row.update(
            metrics
        )


        half_results.append(
            row
        )


half_df = pd.DataFrame(
    half_results
)


half_df.to_csv(
    HALF_CSV,
    index=False,
    encoding="utf-8-sig"
)


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

print(
    "前半・後半分析"
)

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


print(
    half_df[
        [
            "candidate",
            "half",
            "trade_count",
            "win_rate",
            "total_profit",
            "average_profit",
            "median_profit",
            "profit_factor"
        ]
    ]
    .to_string(
        index=False
    )
)


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

year_results = []


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


for candidate_name, thresholds in CANDIDATES.items():

    lower = thresholds[0]
    upper = thresholds[1]


    for year in years:

        source_df = analysis_df[
            analysis_df[
                "year"
            ]
            == year
        ]


        selected = select_candidate(
            source_df,
            lower,
            upper
        )


        metrics = calculate_metrics(
            selected
        )


        row = {
            "candidate": candidate_name,
            "lower": lower,
            "upper": upper,
            "year": int(
                year
            )
        }


        row.update(
            metrics
        )


        year_results.append(
            row
        )


year_df = pd.DataFrame(
    year_results
)


year_df.to_csv(
    YEAR_CSV,
    index=False,
    encoding="utf-8-sig"
)


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

print(
    "年別PF"
)

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


year_pf_table = (
    year_df
    .pivot(
        index="year",
        columns="candidate",
        values="profit_factor"
    )
)


print(
    year_pf_table
    .round(
        3
    )
    .to_string()
)


# ============================================================
# 時間安定性サマリー
# ============================================================

stability_results = []


for candidate_name in CANDIDATES.keys():

    candidate_period_df = period_df[
        period_df[
            "candidate"
        ]
        == candidate_name
    ].copy()


    valid_pf = (
        candidate_period_df[
            "profit_factor"
        ]
        .dropna()
    )


    if len(
        valid_pf
    ) > 0:

        mean_period_pf = (
            valid_pf
            .mean()
        )

        median_period_pf = (
            valid_pf
            .median()
        )

        min_period_pf = (
            valid_pf
            .min()
        )

        max_period_pf = (
            valid_pf
            .max()
        )

        period_pf_std = (
            valid_pf
            .std(
                ddof=0
            )
        )

        periods_above_1 = (
            valid_pf
            > 1.0
        ).sum()

    else:

        mean_period_pf = np.nan
        median_period_pf = np.nan
        min_period_pf = np.nan
        max_period_pf = np.nan
        period_pf_std = np.nan
        periods_above_1 = 0


    candidate_year_df = year_df[
        year_df[
            "candidate"
        ]
        == candidate_name
    ].copy()


    valid_year_pf = (
        candidate_year_df[
            "profit_factor"
        ]
        .dropna()
    )


    years_with_trades = (
        candidate_year_df[
            "trade_count"
        ]
        > 0
    ).sum()


    years_above_1 = (
        candidate_year_df[
            "profit_factor"
        ]
        > 1.0
    ).sum()


    if years_with_trades > 0:

        year_pf_above_1_rate = (
            years_above_1
            /
            years_with_trades
            *
            100
        )

    else:

        year_pf_above_1_rate = np.nan


    if len(
        valid_year_pf
    ) > 0:

        min_year_pf = (
            valid_year_pf
            .min()
        )

        mean_year_pf = (
            valid_year_pf
            .mean()
        )

        year_pf_std = (
            valid_year_pf
            .std(
                ddof=0
            )
        )

    else:

        min_year_pf = np.nan
        mean_year_pf = np.nan
        year_pf_std = np.nan


    overall_row = overall_df[
        overall_df[
            "candidate"
        ]
        == candidate_name
    ].iloc[0]


    stability_results.append(
        {
            "candidate": candidate_name,
            "lower": overall_row[
                "lower"
            ],
            "upper": overall_row[
                "upper"
            ],
            "overall_trade_count": overall_row[
                "trade_count"
            ],
            "overall_total_profit": overall_row[
                "total_profit"
            ],
            "overall_profit_factor": overall_row[
                "profit_factor"
            ],
            "mean_period_pf": mean_period_pf,
            "median_period_pf": median_period_pf,
            "min_period_pf": min_period_pf,
            "max_period_pf": max_period_pf,
            "period_pf_std": period_pf_std,
            "periods_above_1": periods_above_1,
            "period_count": len(
                valid_pf
            ),
            "years_with_trades": years_with_trades,
            "years_above_1": years_above_1,
            "year_pf_above_1_rate": (
                year_pf_above_1_rate
            ),
            "mean_year_pf": mean_year_pf,
            "min_year_pf": min_year_pf,
            "year_pf_std": year_pf_std
        }
    )


stability_df = pd.DataFrame(
    stability_results
)


stability_df.to_csv(
    STABILITY_CSV,
    index=False,
    encoding="utf-8-sig"
)


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

print(
    "時間安定性サマリー"
)

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


print(
    stability_df[
        [
            "candidate",
            "overall_trade_count",
            "overall_total_profit",
            "overall_profit_factor",
            "mean_period_pf",
            "min_period_pf",
            "period_pf_std",
            "periods_above_1",
            "years_above_1",
            "years_with_trades",
            "year_pf_above_1_rate",
            "min_year_pf"
        ]
    ]
    .round(
        3
    )
    .to_string(
        index=False
    )
)


# ============================================================
# グラフ1
# 4期間PF
# ============================================================

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


for candidate_name in CANDIDATES.keys():

    candidate_df = period_df[
        period_df[
            "candidate"
        ]
        == candidate_name
    ].copy()


    candidate_df[
        "period"
    ] = pd.Categorical(
        candidate_df[
            "period"
        ],
        categories=PERIOD_LABELS,
        ordered=True
    )


    candidate_df = (
        candidate_df
        .sort_values(
            "period"
        )
    )


    plt.plot(
        candidate_df[
            "period"
        ],
        candidate_df[
            "profit_factor"
        ],
        marker="o",
        label=candidate_name
    )


plt.axhline(
    1.0,
    linestyle="--"
)


plt.title(
    "Profit Factor by Time Period"
)

plt.xlabel(
    "Time Period"
)

plt.ylabel(
    "Profit Factor"
)

plt.legend()

plt.grid()

plt.tight_layout()


plt.savefig(
    PERIOD_PF_GRAPH,
    dpi=150
)


plt.close()


# ============================================================
# グラフ2
# 前半・後半PF
# ============================================================

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


for candidate_name in CANDIDATES.keys():

    candidate_df = half_df[
        half_df[
            "candidate"
        ]
        == candidate_name
    ].copy()


    candidate_df[
        "half"
    ] = pd.Categorical(
        candidate_df[
            "half"
        ],
        categories=HALF_LABELS,
        ordered=True
    )


    candidate_df = (
        candidate_df
        .sort_values(
            "half"
        )
    )


    plt.plot(
        candidate_df[
            "half"
        ],
        candidate_df[
            "profit_factor"
        ],
        marker="o",
        label=candidate_name
    )


plt.axhline(
    1.0,
    linestyle="--"
)


plt.title(
    "Profit Factor: First Half vs Second Half"
)

plt.xlabel(
    "Period"
)

plt.ylabel(
    "Profit Factor"
)

plt.legend()

plt.grid()

plt.tight_layout()


plt.savefig(
    HALF_PF_GRAPH,
    dpi=150
)


plt.close()


# ============================================================
# グラフ3
# 年別PF
# ============================================================

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


for candidate_name in CANDIDATES.keys():

    candidate_df = year_df[
        year_df[
            "candidate"
        ]
        == candidate_name
    ].copy()


    candidate_df = (
        candidate_df
        .sort_values(
            "year"
        )
    )


    plt.plot(
        candidate_df[
            "year"
        ],
        candidate_df[
            "profit_factor"
        ],
        marker="o",
        label=candidate_name
    )


plt.axhline(
    1.0,
    linestyle="--"
)


plt.title(
    "Yearly Profit Factor by Candidate"
)

plt.xlabel(
    "Year"
)

plt.ylabel(
    "Profit Factor"
)

plt.legend()

plt.grid()

plt.tight_layout()


plt.savefig(
    YEAR_PF_GRAPH,
    dpi=150
)


plt.close()


# ============================================================
# グラフ4
# 平均期間PFと最低期間PF
# ============================================================

plot_df = (
    stability_df
    .set_index(
        "candidate"
    )[
        [
            "mean_period_pf",
            "min_period_pf"
        ]
    ]
)


ax = plot_df.plot(
    kind="bar",
    figsize=(
        12,
        7
    )
)


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


ax.set_title(
    "Mean and Minimum Profit Factor "
    "Across Time Periods"
)

ax.set_xlabel(
    "Candidate"
)

ax.set_ylabel(
    "Profit Factor"
)


plt.xticks(
    rotation=0
)

plt.grid(
    axis="y"
)

plt.tight_layout()


plt.savefig(
    STABILITY_GRAPH,
    dpi=150
)


plt.close()


# ============================================================
# 各候補の期間別詳細
# ============================================================

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

print(
    "候補別詳細"
)

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


for candidate_name in CANDIDATES.keys():

    print(
        "\n----------------------------------------"
    )

    print(
        candidate_name
    )

    print(
        "----------------------------------------"
    )


    candidate_overall = overall_df[
        overall_df[
            "candidate"
        ]
        == candidate_name
    ]


    print(
        "\n全期間"
    )


    print(
        candidate_overall[
            [
                "trade_count",
                "win_rate",
                "total_profit",
                "average_profit",
                "median_profit",
                "profit_factor"
            ]
        ]
        .round(
            3
        )
        .to_string(
            index=False
        )
    )


    candidate_period = period_df[
        period_df[
            "candidate"
        ]
        == candidate_name
    ]


    print(
        "\n4期間"
    )


    print(
        candidate_period[
            [
                "period",
                "trade_count",
                "win_rate",
                "total_profit",
                "average_profit",
                "median_profit",
                "profit_factor"
            ]
        ]
        .round(
            3
        )
        .to_string(
            index=False
        )
    )


    candidate_half = half_df[
        half_df[
            "candidate"
        ]
        == candidate_name
    ]


    print(
        "\n前半・後半"
    )


    print(
        candidate_half[
            [
                "half",
                "trade_count",
                "win_rate",
                "total_profit",
                "average_profit",
                "median_profit",
                "profit_factor"
            ]
        ]
        .round(
            3
        )
        .to_string(
            index=False
        )
    )


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

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

print(
    "分析完了"
)

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


print(
    "\nCSV:"
)

print(
    OVERALL_CSV
)

print(
    PERIOD_CSV
)

print(
    HALF_CSV
)

print(
    YEAR_CSV
)

print(
    STABILITY_CSV
)


print(
    "\nグラフ:"
)

print(
    PERIOD_PF_GRAPH
)

print(
    HALF_PF_GRAPH
)

print(
    YEAR_PF_GRAPH
)

print(
    STABILITY_GRAPH
)

出力されるファイル

今回のコードでは、次のCSVを出力します。

candidate_overall_analysis.csv
candidate_period_analysis.csv
candidate_half_analysis.csv
candidate_year_analysis.csv
candidate_time_stability_summary.csv

グラフは、

candidate_period_pf.png
candidate_half_pf.png
candidate_year_pf.png
candidate_stability_pf.png

です。

関連記事

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