はじめに
前回は、Relative Band Distanceの候補、
A:0.44 ~ 0.88
B:0.44 ~ 1.00
C:0.50 ~ 1.00
D:0.60 ~ 1.00
について、資産曲線とドローダウンを比較しました。
その結果、かなり意外なことが分かりました。
Dは全期間で、
PF:1.861
平均損益:274円
最大DD率:1.122%
となり、
PF 1位
平均損益 1位
最大DD 1位
という非常に強い結果を残しました。
当初は、
PFを高くする代わりに、ドローダウンも大きくなっているのではないか
という可能性も考えていました。
しかし実際には逆でした。
Relative Band Distanceを、
0.60 ~ 1.00
まで絞り込んだDは、最も高いPFを記録しながら、最大DDは最も小さくなりました。
それだけを見るなら、
Dをそのまま零号機へ採用すればよいのではないか
とも思えます。
Dには「時間劣化」という問題がある
第23回でDを期間ごとに分割すると、PFは、
Period1:2.554
Period2:2.041
Period3:1.646
Period4:1.091
でした。
きれいな右肩下がりです。
さらに前半・後半で比較すると、
前半:2.368
後半:1.370
でした。
第24回で作った資産曲線でも、この特徴は確認できました。
2015年頃から2021年頃までは順調に資産を伸ばしています。
しかし2022年頃からは、Dの資産曲線がほぼ横ばいになっています。
つまりDは、
大きなドローダウンを起こして崩壊した
わけではありません。
むしろ、
負けてはいないが、以前ほど稼げなくなった
という状態です。
ここが非常に気になります。
「PFが低下した」だけでは原因は分からない
Profit Factorは、
総利益 ÷ 総損失
です。
したがってPFが、
2.5
↓
1.1
まで低下したとしても、その原因はいくつも考えられます。
例えば、
① 勝率が低下した
② 1回あたりの利益が小さくなった
③ 1回あたりの損失が大きくなった
④ 大きな利益を取れるトレードが減った
⑤ Dに該当する取引そのものが減った
などです。
これらはすべて、最終的にはPF低下として現れます。
しかし原因はまったく違います。
もし、
勝率
60%
↓
45%
となっているのであれば、
エントリー条件そのものが機能しなくなっている
可能性があります。
一方、
勝率はほぼ同じ
平均利益だけ低下
しているのであれば、
トレンドが伸びにくくなった
可能性があります。
また、
平均利益は同じ
平均損失が拡大
しているのであれば、
Dが苦手とする値動きが増えた
可能性があります。
そこで今回は、PFを構成する要素を分解します。
今回はDを徹底的に分解する
今回の主役は、
D:0.60 ~ 1.00
です。
Dに該当するトレードだけを抽出し、時間ごとに、
取引数
勝ち数
負け数
勝率
総利益
総損失
PF
平均利益
平均損失
平均損益
最大利益
最大損失
を計算します。
これによって、
DのPFがなぜ低下しているのか
を調べます。
4期間分析をもう一度使う
まず、第23回と同じ考え方で、Dの全期間を時系列順に4分割します。
Period1
Period2
Period3
Period4
です。
今回はPFだけではなく、その内部を分解します。
例えば、
P1 P2 P3 P4
勝率 62% 60% 58% 50%
平均利益 500 480 450 420
平均損失 -300 -310 -320 -330
PF 2.55 2.04 1.65 1.09
のようになれば、
勝率低下がPF悪化の主因
と考えられます。
逆に、
勝率 60% 61% 60% 59%
平均利益 600 520 430 350
となれば、
勝つ確率ではなく、勝ったときに伸びなくなった
可能性が高くなります。
年別でも確認する
4期間分析だけでは、大きな流れしか分かりません。
そこで今回は、
年別分析
も行います。
各年について、
trades
win_rate
gross_profit
gross_loss
profit_factor
avg_win
avg_loss
avg_profit
total_profit
を計算します。
これによって、
2015
2016
2017
...
2024
2025
2026
と、Dの性質がどのように変化してきたのかを確認します。
特に注目するのは、
2022年以降
です。
第24回の資産曲線を見る限り、このあたりからDの成長が明らかに鈍っています。
取引数そのものが減っていないか
もう一つ重要なのが、
取引数
です。
例えばDが非常に強い条件だったとしても、
2016:40回
2017:38回
2018:35回
2019:32回
2020:30回
2021:28回
2022:15回
2023:12回
2024:10回
2025:8回
のようになっていれば、
Relative Band Distance 0.60~1.00という相場環境そのものが発生しにくくなった
可能性があります。
これは、
Dの優位性が消えた
のとは少し意味が違います。
優位性そのものは残っていても、
優位性を発揮できる場面が減った
だけかもしれません。
この違いは、今後のシステム設計を考える上でかなり重要です。
勝率と平均損益を別々に見る
今回は、
yearly_win_rate.png
(各年の取引のうち、何%が勝ちトレードだったかを示すグラフ)
と、
yearly_avg_profit.png
(各年の1取引あたりの平均損益を示すグラフ)
も作ります。
PFだけを見ると、
2.5 → 1.1
という結果しか分かりません。
しかし、
勝率
と、
1取引平均損益
を別々にグラフ化すれば、
エントリー精度が落ちたのか
それとも、
1回のトレードから得られる利益が減ったのか
を視覚的に確認できます。
平均利益と平均損失も分離する
さらに、
yearly_avg_win_loss.png
を作ります。
ここでは、
平均利益
平均損失の絶対値
を年別に比較します。
例えば勝率が変わっていなくても、
平均利益
500円
↓
300円
となればPFは悪化します。
逆に、
平均利益:ほぼ一定
平均損失
300円
↓
450円
となっている可能性もあります。
このグラフによって、
勝ったトレードが弱くなったのか
負けたトレードが悪化したのか
を分離します。
Dだけでは判断できない問題
ただし、Dだけを分析すると一つ問題があります。
例えば、
DのPFが2022年以降低下
していたとしても、
それが、
D固有の問題
なのか、
零号機全体の問題
なのか分かりません。
そこで今回は比較対象として、
BASE
C
D
の3条件も年別で比較します。
Cを入れる理由は、現時点で最もバランスの取れた候補だからです。
もし、
BASE:低下
C:低下
D:低下
なら、
2022年以降の相場そのものが零号機に不向きになった
可能性があります。
一方、
BASE:維持
C:維持
D:低下
なら、
D固有の優位性が失われている
可能性が高くなります。
今回の核心
今回知りたいことを一言で表すなら、
Dは「勝てなくなった」のか、それとも「稼げる場面が減った」のか
です。
この2つは似ていますが、システム設計上は大きく違います。
前者なら、
0.60 ~ 1.00
という条件自体を疑う必要があります。
後者なら、
D条件
+
別の相場環境フィルター
という方向に発展できる可能性があります。
例えば、
ATR
ボラティリティ
時間帯
トレンド強度
BB幅
などとの組み合わせによって、
Dが機能する環境だけをさらに判別する
という次の研究につながります。
そのためにも、まずDの時間劣化の中身を確認します。
出力されるファイル
今回のコードでは、3つのCSVを出力します。
d_time_decay_period_analysis.csv
d_time_decay_year_analysis.csv
relative_band_year_comparison.csv
それぞれ、
d_time_decay_period_analysis.csv
→ Dを4期間に分割した詳細分析
d_time_decay_year_analysis.csv
→ Dの年別詳細分析
relative_band_year_comparison.csv
→ BASE・C・Dの年別比較
です。
さらに5枚のグラフを出力します。
yearly_pf_comparison.png
yearly_trade_count.png
yearly_win_rate.png
yearly_avg_profit.png
yearly_avg_win_loss.png
分析結果
今回の目的は、
D(Relative Band Distance 0.60~1.00)の優位性は、なぜ時間とともに低下したのか
を調べることでした。
結果として、かなり重要なことが分かりました。
まずDの344取引を時系列順に4等分した結果です。
| 期間 | 取引数 | 勝率 | PF | 平均利益 | 平均損失 | 1取引平均損益 | 総損益 |
|---|---|---|---|---|---|---|---|
| Period1 | 86 | 53.49% | 3.027 | 1,808円 | -687円 | +648円 | +55,697円 |
| Period2 | 86 | 58.14% | 1.863 | 883円 | -658円 | +238円 | +20,449円 |
| Period3 | 86 | 50.00% | 1.473 | 1,077円 | -731円 | +173円 | +14,881円 |
| Period4 | 86 | 52.33% | 1.121 | 671円 | -657円 | +38円 | +3,260円 |
PFだけを見ると、
3.027
↓
1.863
↓
1.473
↓
1.121
と、明確に低下しています。
しかし今回重要なのは、その中身です。
Dは「勝てなくなった」わけではない
まず勝率を見ます。
Period1:53.49%
Period2:58.14%
Period3:50.00%
Period4:52.33%
意外なことに、長期的な勝率には明確な右肩下がりがありません。
Period1が53.49%なのに対して、直近のPeriod4も52.33%です。
つまり、
DのPF低下を、単純な勝率低下だけでは説明できない
ことが分かりました。
これは今回の分析で非常に重要なポイントです。
Dは時間とともに「エントリーすると負けるようになった」という単純な劣化ではありません。
最大の変化は「勝ったときに伸びなくなった」こと
次に平均利益を見ます。
Period1:1,808円
Period2: 883円
Period3:1,077円
Period4: 671円
Period1とPeriod4を比較すると、
1,808円
↓
671円
まで低下しています。
約63%の減少です。
一方、平均損失は、
Period1:-687円
Period2:-658円
Period3:-731円
Period4:-657円
です。
こちらは驚くほど変わっていません。
つまりDで起きていることは、
昔
勝ち:大きい
負け:一定
↓
最近
勝ち:小さい
負け:ほぼ同じ
という変化です。
これは非常に重要です。
Dは負けが大きくなったのではなく、勝ったときの利益が伸びなくなっている。
これがPF低下の主因と考えられます。
1取引あたりの期待値はほぼ消滅した
その結果、平均損益は、
Period1:+647.64円
Period2:+237.78円
Period3:+173.04円
Period4: +37.90円
まで低下しました。
Period1では1回取引するごとに平均約648円を得られていました。
それがPeriod4では、
約38円
です。
約94%減っています。
総損益も、
Period1:+55,697円
Period2:+20,449円
Period3:+14,881円
Period4: +3,260円
となっています。
同じ86取引ずつ比較しているにもかかわらず、Period1とPeriod4では利益に17倍以上の差があります。
したがって、
Dの資産曲線が最近横ばいになったのは、取引回数が減ったからだけではなく、1回あたりの優位性そのものがほぼ失われたから
と考えるのが妥当です。
年別では、さらに激しい変化が見える
4期間分析では大きな流れを確認しました。
次に年別で見ると、Dの不安定さがより鮮明になります。
PFは、
2015:8.510
2016:1.061
2017:0.892
2018:2.455
2019:2.421
2020:2.077
2021:2.633
2022:1.092
2023:0.928
2024:1.166
2025:3.132
2026:0.265
でした。
一直線に悪化しているわけではありません。
2018~2021年は非常に強く、2025年にもPF3.132まで復活しています。
したがって、
Dという条件が完全に死んだ
とはまだ言えません。
むしろ、
非常に強い年
↓
ほぼ優位性がない年
↓
再び強い年
が存在します。
ここから見えてくるのは、
Dの成績は、相場環境によって大きく変わる
という可能性です。
Dだけが壊れたわけではない
ここでBASE・C・Dを比較すると、さらに重要なことが分かります。

例えば2023年は、
BASE:1.093
C :0.925
D :0.928
でした。
2026年は、
BASE:0.628
C :0.446
D :0.265
です。
つまり最近の悪化はDだけに起きているわけではありません。
特に2026年は、
BASE
C
D
のすべてがPF1を割っています。
逆に2025年は、
BASE:2.323
C :4.247
D :3.132
と、3条件すべてが非常に強い年でした。
これはかなり示唆的です。
もしDというRelative Band Distance条件だけが劣化しているのであれば、
BASE:正常
C:正常
D:悪化
となるはずです。
しかし実際には、
BASE・C・Dが同じ年に強くなったり、弱くなったりする傾向
があります。
したがって今回の結果からは、
Relative Band Distanceだけではなく、零号機そのものが特定の相場環境で強く、別の相場環境では弱い
可能性が浮かび上がってきました。
2022年は特に興味深い
ただし、すべてが完全に同じ動きをしているわけでもありません。
2022年を見ると、
BASE:PF 1.589
C :PF 1.194
D :PF 1.092
でした。
BASEはまだ十分利益を出しています。
しかしRelative Band Distanceで絞り込むほどPFが低下しています。
これは、
2022年には、これまで有効だったRelative Band Distanceの高い領域がむしろ機能しにくかった
可能性を示しています。
つまり、
零号機全体の相場依存性
+
Relative Band Distance固有の相場依存性
の両方が存在している可能性があります。
取引機会も最近は減っている
次にDの年別取引数を見ると、
2016:40
2017:37
2018:30
2019:29
2020:31
2021:25
2022:28
2023:33
2024:26
2025:18
2026:18
となっています。

特に2025年以降は、
18回
18回
まで減少しています。
ただし2026年は8月7日までのデータなので、18回を単純に年間値として過去年と比較することはできません。
一方、2025年の18回は過去と比較して明らかに少ないです。
したがって、
Dに該当する取引機会そのものが減っている可能性
もあります。
しかし、これだけではDの劣化を説明できません。
なぜなら2025年は取引数が18回しかないにもかかわらず、
PF:3.132
平均損益:+374円
だからです。
つまり、
取引回数が少ない=優位性が低い
ではありません。
「取引機会の減少」と「1取引あたりの期待値低下」は別々に考える必要があります。
勝率はかなり振れる
年別勝率を見ると、
2015:65.5%
2016:45.0%
2017:56.8%
2018:60.0%
2019:58.6%
2020:54.8%
2021:52.0%
2022:50.0%
2023:51.5%
2024:53.8%
2025:61.1%
2026:27.8%
となりました。

2018~2025年を見ると、おおむね50~60%程度を維持しています。
特に2025年は、
61.1%
まで上昇しています。
したがって長期的なDの劣化を、
勝率が徐々に下がったから
と説明するのは難しそうです。
ただし2026年は、
27.8%
まで急落しています。
18取引しかないため現段階ではサンプル数が少ないものの、これは無視できない数字です。
2026年については、DだけではなくBASE・CもPF1を割っているため、
零号機にとってかなり苦手な相場環境になっている
可能性があります。
期待値低下の正体は「利幅」にある
今回最も重要なのが平均利益と平均損失です。

2015年は平均利益が、
3,247円
もありました。
一方、平均損失は、
725円
です。
非常に大きなリスクリワード差があります。
その後を見ると、平均損失は年によって変動しながらも、おおむね数百円~1,000円程度です。
ところが平均利益は、
2018:1,265円
2020:1,298円
2021:1,058円
2022:1,136円
2023: 637円
2024: 539円
2025: 899円
2026: 502円
と、近年かなり小さくなっています。
特に2026年は、
平均利益:502円
平均損失:730円
となり、ついに平均損失の方が大きくなりました。
Dはもともと、
勝ったときに大きく伸ばす
ことで高いPFを実現していた可能性があります。
その「大きく伸ばす」部分が弱くなったことで、Dの優位性が失われていると考えられます。
1取引平均損益を見ると変化はさらに明確

Dの平均損益は、
2018:+450円
2019:+201円
2020:+369円
2021:+341円
2022: +48円
2023: -25円
2024: +41円
2025:+374円
2026:-387円
でした。
2022年以降を見ると、2025年を除いてほとんどゼロ付近です。
これは第24回で見た、
2022年頃からDの資産曲線が横ばいになった
という現象ときれいに一致します。
つまり資産曲線の横ばいは偶然ではありません。
1トレードあたりの期待値がほぼゼロまで低下していることが原因です。
ただし2025年の復活を無視してはいけない
ここで非常に面白いのが2025年です。
2025年のDは、
取引数:18
勝率:61.1%
PF:3.132
平均利益:899円
平均損失:-451円
平均損益:+374円
でした。
かなり強いです。
つまり、
Dは昔は機能した
↓
時間とともに完全に壊れた
という単純な話ではありません。
むしろ、
Dが非常に機能する相場
Dがほとんど機能しない相場
が存在していると考えた方が自然です。
これは次の研究方向を大きく変えます。
「時間」そのものが原因ではない可能性
ここまで第23回では、
Dは時間とともに劣化している
と考えてきました。
しかし今回の年別分析を見ると、少し表現を修正する必要があります。
確かに4期間で見れば、
PF
3.027
↓
1.863
↓
1.473
↓
1.121
と低下しています。
しかし年別では、
2021:2.633
2022:1.092
2023:0.928
2024:1.166
2025:3.132
2026:0.265
です。
2025年に再び大きく復活しています。
したがって、
時間の経過そのものによってDの優位性が消えた
というより、
近年、Dが機能しにくい相場環境の割合が増えている
可能性を考えた方がよさそうです。
今回の結論
今回の分析で、Dの時間劣化についてかなり中身が見えてきました。
① DのPFは長期的には低下している
② しかし勝率には明確な長期低下がない
③ 平均損失も大きく悪化していない
④ 最も大きく変化したのは平均利益
⑤ その結果、1取引あたりの期待値が大幅に低下した
⑥ 最近はDに該当する取引数も減っている
⑦ ただし2025年にはDが再び非常に強く機能した
⑧ BASE・C・Dが同時に強い/弱い年も多い
したがって現時点では、
Dの優位性が単純に時間とともに消滅した
とは考えません。
むしろ、
零号機の利益を大きく伸ばせる相場環境と、伸ばせない相場環境が存在する
という仮説が強くなりました。
Relative Band Distanceは、その中から有利なエントリーを選別する能力を持っています。
しかしRelative Band Distanceだけでは、
今がその戦略に適した相場環境なのか
までは判定できない可能性があります。
ここまで来ると、次に探すべきものはRelative Band Distanceのさらに細かい閾値ではありません。
「相場環境」を表す別の変数です。
次回
次回は、
Dが機能する年と機能しない年では、相場環境に何が違うのか
を調べます。
今回、特に対照的だったのが、
強い年
2018:PF 2.455
2019:PF 2.421
2020:PF 2.077
2021:PF 2.633
2025:PF 3.132
に対して、
弱い年
2017:PF 0.892
2022:PF 1.092
2023:PF 0.928
2024:PF 1.166
2026:PF 0.265
です。
なぜ同じ、
Relative Band Distance
0.60 ~ 1.00
という条件なのに、これほど結果が変わるのでしょうか。
次回はまず、
ATR
BB幅
など、すでにmarket_masterに存在する相場環境指標を使って、
Dが勝ったときと負けたときの市場状態
を比較します。
特に今回、
勝率低下よりも「勝ったときの利益が伸びなくなった」ことが問題
と分かりました。
そのため、
ボラティリティが低くて伸びないのか
ボラティリティが高すぎてノイズに負けるのか
BBの拡大・収縮状態が関係しているのか
を調べる価値があります。
ここでDの利益を左右する市場状態が見つかれば、
Relative Band Distance
+
相場環境フィルター
という次の段階へ進めます。
つまり、
「良いエントリー」を探す研究から、「システムを動かすべき相場」を判定する研究へ
進みたいと思います。
今回のコード
ファイル名は、
relative_band_d_time_decay.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)
D_LOWER = 0.60
D_UPPER = 1.00
C_LOWER = 0.50
C_UPPER = 1.00
# ============================================================
# 出力ファイル
# ============================================================
PERIOD_CSV = os.path.join(
OUTPUT_DIR,
"d_time_decay_period_analysis.csv"
)
YEAR_CSV = os.path.join(
OUTPUT_DIR,
"d_time_decay_year_analysis.csv"
)
COMPARE_CSV = os.path.join(
OUTPUT_DIR,
"relative_band_year_comparison.csv"
)
PF_GRAPH = os.path.join(
OUTPUT_DIR,
"yearly_pf_comparison.png"
)
TRADES_GRAPH = os.path.join(
OUTPUT_DIR,
"yearly_trade_count.png"
)
WIN_RATE_GRAPH = os.path.join(
OUTPUT_DIR,
"yearly_win_rate.png"
)
AVG_PROFIT_GRAPH = os.path.join(
OUTPUT_DIR,
"yearly_avg_profit.png"
)
AVG_WIN_LOSS_GRAPH = os.path.join(
OUTPUT_DIR,
"yearly_avg_win_loss.png"
)
# ============================================================
# データ読み込み
# ============================================================
df = pd.read_csv(INPUT_FILE)
print("=" * 60)
print("RELATIVE BAND DISTANCE - D TIME DECAY ANALYSIS")
print("=" * 60)
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()
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)
)
analysis_df["year"] = (
analysis_df["entry_time"].dt.year
)
print(f"分析対象件数: {len(analysis_df)}")
print(
"期間:",
analysis_df["entry_time"].min(),
"~",
analysis_df["entry_time"].max()
)
# ============================================================
# 条件抽出
# ============================================================
base_df = analysis_df.copy()
c_df = analysis_df[
analysis_df["relative_band_distance"]
.between(
C_LOWER,
C_UPPER,
inclusive="both"
)
].copy()
d_df = analysis_df[
analysis_df["relative_band_distance"]
.between(
D_LOWER,
D_UPPER,
inclusive="both"
)
].copy()
print(f"\nBASE取引数: {len(base_df)}")
print(f"C取引数: {len(c_df)}")
print(f"D取引数: {len(d_df)}")
# ============================================================
# 統計計算
# ============================================================
def calculate_stats(source_df):
profits = source_df["profit"]
trades = len(profits)
if trades == 0:
return {
"trades": 0,
"wins": 0,
"losses": 0,
"win_rate": np.nan,
"gross_profit": 0,
"gross_loss": 0,
"profit_factor": np.nan,
"avg_win": np.nan,
"avg_loss": np.nan,
"avg_loss_abs": np.nan,
"avg_profit": np.nan,
"total_profit": 0,
"max_win": np.nan,
"max_loss": np.nan,
}
wins = profits[profits > 0]
losses = profits[profits < 0]
win_count = len(wins)
loss_count = len(losses)
win_rate = (
win_count
/
trades
*
100
)
gross_profit = wins.sum()
gross_loss = -losses.sum()
if gross_loss > 0:
profit_factor = (
gross_profit
/
gross_loss
)
elif gross_profit > 0:
profit_factor = np.inf
else:
profit_factor = np.nan
avg_win = (
wins.mean()
if win_count > 0
else np.nan
)
avg_loss = (
losses.mean()
if loss_count > 0
else np.nan
)
avg_loss_abs = (
abs(avg_loss)
if pd.notna(avg_loss)
else np.nan
)
avg_profit = profits.mean()
total_profit = profits.sum()
max_win = (
wins.max()
if win_count > 0
else np.nan
)
max_loss = (
losses.min()
if loss_count > 0
else np.nan
)
return {
"trades": trades,
"wins": win_count,
"losses": loss_count,
"win_rate": win_rate,
"gross_profit": gross_profit,
"gross_loss": gross_loss,
"profit_factor": profit_factor,
"avg_win": avg_win,
"avg_loss": avg_loss,
"avg_loss_abs": avg_loss_abs,
"avg_profit": avg_profit,
"total_profit": total_profit,
"max_win": max_win,
"max_loss": max_loss,
}
# ============================================================
# Dを時系列で4分割
# ============================================================
d_sorted = (
d_df
.sort_values("entry_time")
.reset_index(drop=True)
)
# DataFrameそのものではなく、
# 行番号を4分割する
period_indices = np.array_split(
np.arange(len(d_sorted)),
4
)
period_rows = []
for i, indices in enumerate(
period_indices,
start=1
):
# 行番号を使ってDataFrameとして抽出
period_df = (
d_sorted
.iloc[indices]
.copy()
.reset_index(drop=True)
)
stats = calculate_stats(
period_df
)
stats["period"] = (
f"Period{i}"
)
stats["start_date"] = (
period_df["entry_time"].min()
)
stats["end_date"] = (
period_df["entry_time"].max()
)
period_rows.append(stats)
period_result_df = pd.DataFrame(
period_rows
)
period_columns = [
"period",
"start_date",
"end_date",
"trades",
"wins",
"losses",
"win_rate",
"gross_profit",
"gross_loss",
"profit_factor",
"avg_win",
"avg_loss",
"avg_loss_abs",
"avg_profit",
"total_profit",
"max_win",
"max_loss",
]
period_result_df = period_result_df[
period_columns
]
period_result_df.to_csv(
PERIOD_CSV,
index=False
)
# ============================================================
# D 年別分析
# ============================================================
year_rows = []
for year, year_df in d_df.groupby("year"):
stats = calculate_stats(
year_df
)
stats["year"] = year
year_rows.append(stats)
year_result_df = pd.DataFrame(
year_rows
)
year_columns = [
"year",
"trades",
"wins",
"losses",
"win_rate",
"gross_profit",
"gross_loss",
"profit_factor",
"avg_win",
"avg_loss",
"avg_loss_abs",
"avg_profit",
"total_profit",
"max_win",
"max_loss",
]
year_result_df = (
year_result_df[
year_columns
]
.sort_values("year")
)
year_result_df.to_csv(
YEAR_CSV,
index=False
)
# ============================================================
# BASE / C / D 年別比較
# ============================================================
comparison_rows = []
comparison_sets = {
"BASE": base_df,
"C_0.50_1.00": c_df,
"D_0.60_1.00": d_df,
}
for candidate, candidate_df in comparison_sets.items():
for year, year_df in candidate_df.groupby("year"):
stats = calculate_stats(
year_df
)
stats["candidate"] = candidate
stats["year"] = year
comparison_rows.append(stats)
comparison_df = pd.DataFrame(
comparison_rows
)
comparison_columns = [
"candidate",
"year",
"trades",
"wins",
"losses",
"win_rate",
"gross_profit",
"gross_loss",
"profit_factor",
"avg_win",
"avg_loss",
"avg_loss_abs",
"avg_profit",
"total_profit",
]
comparison_df = (
comparison_df[
comparison_columns
]
.sort_values(
[
"year",
"candidate"
]
)
)
comparison_df.to_csv(
COMPARE_CSV,
index=False
)
# ============================================================
# グラフ1:BASE / C / D 年別PF
# ============================================================
plt.figure(
figsize=(12, 7)
)
for candidate in comparison_sets.keys():
plot_df = comparison_df[
comparison_df["candidate"]
==
candidate
]
plt.plot(
plot_df["year"],
plot_df["profit_factor"],
marker="o",
label=candidate
)
plt.axhline(
1.0,
linestyle="--",
linewidth=1
)
plt.title(
"Yearly Profit Factor Comparison"
)
plt.xlabel("Year")
plt.ylabel("Profit Factor")
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(
PF_GRAPH,
dpi=150
)
plt.close()
# ============================================================
# グラフ2:D 年別取引数
# ============================================================
plt.figure(
figsize=(12, 7)
)
plt.bar(
year_result_df["year"],
year_result_df["trades"]
)
plt.title(
"D: Yearly Trade Count"
)
plt.xlabel("Year")
plt.ylabel("Trades")
plt.grid(
axis="y",
alpha=0.3
)
plt.tight_layout()
plt.savefig(
TRADES_GRAPH,
dpi=150
)
plt.close()
# ============================================================
# グラフ3:D 年別勝率
# ============================================================
plt.figure(
figsize=(12, 7)
)
plt.plot(
year_result_df["year"],
year_result_df["win_rate"],
marker="o"
)
plt.title(
"D: Yearly Win Rate"
)
plt.xlabel("Year")
plt.ylabel("Win Rate (%)")
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(
WIN_RATE_GRAPH,
dpi=150
)
plt.close()
# ============================================================
# グラフ4:D 年別平均損益
# ============================================================
plt.figure(
figsize=(12, 7)
)
plt.plot(
year_result_df["year"],
year_result_df["avg_profit"],
marker="o"
)
plt.axhline(
0,
linestyle="--",
linewidth=1
)
plt.title(
"D: Yearly Average Profit per Trade"
)
plt.xlabel("Year")
plt.ylabel("Average Profit")
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(
AVG_PROFIT_GRAPH,
dpi=150
)
plt.close()
# ============================================================
# グラフ5:D 平均利益 / 平均損失
# ============================================================
plt.figure(
figsize=(12, 7)
)
plt.plot(
year_result_df["year"],
year_result_df["avg_win"],
marker="o",
label="Average Win"
)
plt.plot(
year_result_df["year"],
year_result_df["avg_loss_abs"],
marker="o",
label="Average Loss (Absolute)"
)
plt.title(
"D: Yearly Average Win / Loss"
)
plt.xlabel("Year")
plt.ylabel("Profit / Loss")
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(
AVG_WIN_LOSS_GRAPH,
dpi=150
)
plt.close()
# ============================================================
# コンソール表示
# ============================================================
print("\n")
print("=" * 60)
print("D PERIOD ANALYSIS")
print("=" * 60)
print(
period_result_df.to_string(
index=False
)
)
print("\n")
print("=" * 60)
print("D YEAR ANALYSIS")
print("=" * 60)
print(
year_result_df.to_string(
index=False
)
)
print("\n")
print("=" * 60)
print("OUTPUT FILES")
print("=" * 60)
print(PERIOD_CSV)
print(YEAR_CSV)
print(COMPARE_CSV)
print(PF_GRAPH)
print(TRADES_GRAPH)
print(WIN_RATE_GRAPH)
print(AVG_PROFIT_GRAPH)
print(AVG_WIN_LOSS_GRAPH)
コメント