はじめに
前回は、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
です。
コメント