- はじめに
- Q3は「0.44~0.88」だった
- 「0.44」という数字に意味はあるのか
- 「山」ではなく「台地」を探す
- 今回調べること
- ただし「最高PFランキング」だけは見ない
- 最低取引数を設定する
- Relative Band Distanceを再計算する
- 下限と上限を総当たりする
- Profit Factorを計算する
- ヒートマップで見る
- 元のQ3も比較対象として表示する
- さらに「下限固定」「上限固定」でも見る
- 今回特に見たいポイント
- 分析結果
- 下限0.44には強く依存していない
- 一方で、上限側にはかなり面白い変化があった
- 本当に面白いのは「1.00」という境界かもしれない
- 「1.00」には意味があるのか
- 最高PFは0.60~1.00だった
- Q3仮説を少し修正する必要がありそう
- ただし、ここでフィルターを採用してはいけない
- 発見した「1.00」は未来でも通用するのか
- 次回予告
- 今回のコード全文
- 出力されるファイル
- 関連記事
はじめに
前回は、Relative Band DistanceのQ3について、
時間が変わっても優位性が残っているのか
を検証しました。

その結果、Q3のPFは、
Period1 1.273
Period2 1.659
Period3 1.435
Period4 1.401
となり、4期間すべてでPF1を上回りました。
さらに年別でも、
12年間中10年間でPF1以上
となりました。
これまでの分析を整理すると、
第19回
Q3のPFが最も高かった
↓
第20回
大勝ちを除外してもQ3は比較的強かった
↓
第21回
時間を分割してもQ3の優位性が残った
というところまで来ました。
Relative Band Distance Q3は、かなり面白い条件になってきています。
しかし、まだこのまま零号機へ組み込むことはできません。
理由は、
Q3という区切り自体が、過去データから作られたものだから
です。
今回は、いよいよこの問題を調べます。
Q3は「0.44~0.88」だった
これまで使用してきたRelative Band Distanceは、
Relative Band Distance
=
BAND_DISTANCE ÷ ATR
です。
つまり、
ボリンジャーバンドからどれくらい離れてエントリーしたか
を、そのときのボラティリティで正規化しています。
第19回で全取引を四分位に分けたところ、Q3はおおよそ、
0.44 ~ 0.88
となりました。
そして、この範囲のPFが、
1.412
と最も高くなりました。
一見すると、
ではRelative Band Distanceが0.44~0.88のときだけエントリーすればいい
となります。
しかし、これは少し危険です。
「0.44」という数字に意味はあるのか
例えば、本当に相場の構造としてQ3付近に優位性があるのであれば、
0.40 ~ 0.84
0.42 ~ 0.86
0.44 ~ 0.88
0.46 ~ 0.90
0.48 ~ 0.92
のように条件を少し動かしても、ある程度似た成績になるはずです。
一方で、
0.43 ~ 0.87 → PF 1.05
0.44 ~ 0.88 → PF 1.41
0.45 ~ 0.89 → PF 0.98
のように、わずかな違いだけで成績が急変するのであれば、
0.44~0.88という条件を過去データに合わせ込んだだけ
かもしれません。
これはバックテストでは非常に重要な問題です。
条件を細かく調整していけば、過去データ上だけ成績の良いポイントはいくらでも見つかります。
しかし、その一点だけが突出している条件は、未来でも再現するとは限りません。
今回探したいのは、
最高PFになる一点
ではありません。
少し条件を変えても比較的良い成績が続く範囲
です。
「山」ではなく「台地」を探す
イメージとしては、
PF
1.5 ▲
/ \
1.4 / \
1.3 _________/ \_________
Relative Band Distance
という鋭い山よりも、
PF
1.5
1.4 ┌────────┐
1.3 _______│ │_______
└────────┘
Relative Band Distance
という形の方が好ましいです。
前者は、
ある特定のパラメータだけが偶然良かった
可能性があります。
後者は、
その周辺一帯に何らかの相場的な特徴が存在する
可能性があります。
自動売買のパラメータ探索では、最高値そのものより、
良い成績が広い範囲に分布しているか
を見る方が重要だと考えています。
今回はこの「台地」が存在するかを調べます。
今回調べること
今回は、Relative Band Distanceの下限と上限を少しずつ変化させます。
基準となるQ3は、
下限 0.44
上限 0.88
です。
ここから下限を、
0.30
0.32
0.34
...
0.60
と動かします。
上限についても、
0.70
0.72
0.74
...
1.10
と動かします。
そして、それぞれの組み合わせについて、
- 取引数
- 勝率
- 総損益
- 平均損益
- 中央値
- Profit Factor
を計算します。
例えば、
0.40 ~ 0.80
0.40 ~ 0.82
0.40 ~ 0.84
...
0.42 ~ 0.80
0.42 ~ 0.82
0.42 ~ 0.84
...
という大量の組み合わせを一気に調べます。
ただし「最高PFランキング」だけは見ない
今回注意したいことがあります。
すべての組み合わせを計算すると、
PFが一番高い条件はどこか
を見たくなります。
しかし、これだけでは意味がありません。
例えば、
0.52~0.74
取引数:47
PF:2.30
という条件があったとしても、
取引数が少ないために偶然PFが高くなった可能性があります。
一方、
0.40~0.92
取引数:380
PF:1.35
であれば、PF自体は低くてもこちらの方が信頼できるかもしれません。
そこで今回は、
PFと取引数を同時に見る
ことにします。
最低取引数を設定する
今回は、
MIN_TRADES = 100
として、
100回未満の組み合わせは主要な評価対象から外す
ことにしました。
100回という数字自体に絶対的な意味があるわけではありません。
ただ、数十回しかない条件のPF2やPF3を追いかけ始めると、過剰最適化へ向かいやすくなります。
今回は、
ある程度のサンプル数を確保した上で、広い範囲に優位性が存在するか
を優先します。
Relative Band Distanceを再計算する
今回も、
analysis_df["relative_band_distance"] = (
analysis_df["entry_band_distance_pips"]
/
analysis_df["entry_atr14_pips"]
)
としてRelative Band Distanceを計算します。
ただし、今回は四分位には分けません。
前回までは、
pd.qcut()
を使ってQ1~Q4を作っていました。
今回はこれを使わず、
Relative Band Distanceを連続値のまま扱う
のがポイントです。
下限と上限を総当たりする
下限候補を、
LOWER_VALUES = np.arange(
0.30,
0.62,
0.02
)
上限候補を、
UPPER_VALUES = np.arange(
0.70,
1.12,
0.02
)
としました。
その組み合わせについて、
lower <= relative_band_distance <= upper
を満たす取引だけを抽出します。
例えば、
mask = (
(analysis_df["relative_band_distance"] >= lower)
&
(analysis_df["relative_band_distance"] <= upper)
)
です。
これをすべての組み合わせについて繰り返します。
Profit Factorを計算する
Profit Factorはこれまでと同じく、
総利益 ÷ 総損失
です。
コードでは、
gross_profit = selected.loc[
selected["profit"] > 0,
"profit"
].sum()
gross_loss = -selected.loc[
selected["profit"] < 0,
"profit"
].sum()
profit_factor = (
gross_profit / gross_loss
if gross_loss > 0
else np.nan
)
とします。
これで、
下限
上限
取引数
勝率
総損益
平均損益
中央値
PF
が1行ずつ記録されます。
ヒートマップで見る
大量の数字をCSVで見るだけでは分かりにくいため、今回はヒートマップも作ります。
横軸を、
上限
縦軸を、
下限
色を、
Profit Factor
とします。
これによって、
Q3周辺にPFの高い領域がまとまって存在するか
を見ることができます。
今回もっとも見たいグラフです。
もし、
0.42~0.84
0.42~0.86
0.42~0.88
0.44~0.84
0.44~0.86
0.44~0.88
0.46~0.86
0.46~0.88
など広い範囲でPF1.3~1.4程度が維持されているなら、
Q3は偶然切り取られた区間ではなく、その周辺に比較的広い優位性がある
という仮説が強くなります。
逆に、
0.44~0.88
だけが突然突出していれば、かなり警戒すべきです。
元のQ3も比較対象として表示する
今回のコードでは、
BASE_LOWER = 0.44
BASE_UPPER = 0.88
として、元のQ3付近の条件も別途計算します。
これによって、
Q3相当条件
取引数
勝率
総損益
平均損益
中央値
PF
を確認できます。
ただし、第19回で使った0.44~0.88は四分位点を丸めて表記した数字なので、
今回の0.44~0.88と第19回Q3の取引集合が完全に一致するとは限らない
点には注意が必要です。
今回の目的はPF1.412を完全再現することではありません。
その周辺に成績の良い範囲が存在するか
を見ることです。
さらに「下限固定」「上限固定」でも見る
二次元のヒートマップだけでは少し分かりにくいため、もう2つグラフを作ります。
まず、
上限を0.88に固定
して、下限だけを変化させます。
0.30~0.88
0.32~0.88
0.34~0.88
...
という分析です。
逆に、
下限を0.44に固定
して、
0.44~0.70
0.44~0.72
0.44~0.74
...
と上限だけを動かします。
これによって、
どちら側の境界に成績が敏感なのか
も確認できます。
これは実際にEAへフィルターを実装するときにも重要です。
今回特に見たいポイント
今回、最高PFそのものより注目したいのは次の3点です。
① Q3周辺でPF1以上が広く続いているか
0.44~0.88だけではなく、その周辺にもPF1以上の組み合わせが多ければプラス材料です。
② PF1.3前後の領域がまとまって存在するか
一つだけPF1.5より、
1.32
1.37
1.40
1.35
1.31
のような領域が続いている方が、今回の目的には合っています。
③ 取引数が十分残るか
フィルターを厳しくしてPFを上げても、
1,600取引
↓
100取引
となれば、零号機の性格そのものが変わります。
PFだけでなく、
どれくらいの取引機会を捨てることになるのか
も見ていきます。
分析結果
まず、元のQ3に相当する、
0.44 ~ 0.88
の条件を確認しました。
結果は、
取引数:441
勝率:59.86%
総損益:+60,876円
平均損益:+138円
中央値:+271円
PF:1.397
となりました。
第19回で四分位として計算したQ3のPF1.412とほぼ同じ水準です。
丸めた境界値を直接指定しているため完全には一致しませんが、
0.44~0.88付近に優位性が存在する
というこれまでの分析結果は再確認できました。
しかし、今回さらに興味深かったのはここからです。
Q3だけが突出していたわけではなかった
全組み合わせをヒートマップにすると、Relative Band Distanceの条件を多少変えても、広い範囲でPF1を超えていることが分かりました。

最低100取引以上ある351通りの組み合わせについて見ると、
PF 1.0以上:349通り
PF 1.2以上:338通り
PF 1.3以上:318通り
PF 1.4以上:225通り
PF 1.5以上:137通り
PF 1.6以上:56通り
となりました。
割合にすると、
PF1.0以上:約99.4%
PF1.2以上:約96.3%
PF1.3以上:約90.6%
PF1.4以上:約64.1%
PF1.5以上:約39.0%
PF1.6以上:約16.0%
です。
これはかなり重要です。
仮に0.44~0.88という一点だけがPF1.4で、その周囲がPF1前後まで急落していれば、
Q3という区切りに過剰適合していた
可能性を疑う必要がありました。
しかし今回の結果はむしろ逆でした。
広い範囲にわたってPF1.3~1.6程度の領域が続いています。
つまり、今回探していた、
「尖った山」ではなく「台地」
にかなり近い結果です。
下限0.44には強く依存していない
次に、上限を0.88に固定して、下限だけを動かしました。

結果を見ると、
下限0.30 → PF 1.397
0.32 → 1.510
0.34 → 1.508
0.36 → 1.394
0.38 → 1.356
0.40 → 1.387
0.42 → 1.394
0.44 → 1.397
0.46 → 1.454
0.48 → 1.430
0.50 → 1.422
0.52 → 1.386
となっています。
0.54だけPF1.193まで一時的に落ちていますが、それ以外は概ねPF1.35~1.50付近です。
ここから分かるのは、
0.44という下限値自体に特別な意味があるわけではなさそう
ということです。
例えば、
0.40~0.88
0.42~0.88
0.44~0.88
0.46~0.88
0.48~0.88
0.50~0.88
と多少条件を変えても、PFは大きく崩れていません。
むしろ0.46~0.88ではPF1.454となっています。
したがって、
Q3の下限0.44を数百分の一単位で最適化する必要はなさそう
です。
これは過剰最適化を避けるという意味では良い結果です。
一方で、上限側にはかなり面白い変化があった
さらに興味深いのが、下限を0.44に固定して上限を動かした結果です。

0.70~0.88付近では、
0.44~0.70 → PF 1.325
0.44~0.74 → PF 1.390
0.44~0.80 → PF 1.365
0.44~0.86 → PF 1.387
0.44~0.88 → PF 1.397
と、PF1.3~1.4程度でした。
ところが、0.88を超えたあたりからPFが上昇し始めます。
0.44~0.90 → PF 1.448
0.44~0.94 → PF 1.443
0.44~0.96 → PF 1.470
0.44~0.98 → PF 1.496
0.44~1.00 → PF 1.639
特に、
0.44~1.00
では、
取引数:534
総損益:+117,305円
平均損益:+220円
中央値:+262円
PF:1.639
となりました。
これは元のQ3、
0.44~0.88
PF 1.397
総損益 +60,876円
と比較するとかなり大きな違いです。
上限を0.88から1.00までわずかに広げただけで、
取引数
441 → 534
総損益
+60,876円 → +117,305円
PF
1.397 → 1.639
となりました。
取引数を減らしてPFだけを高くしたわけではありません。
むしろ、
取引数を増やしながらPFも上昇している
のがポイントです。
本当に面白いのは「1.00」という境界かもしれない
ヒートマップを見ると、特に目立つのが、
Upper Threshold = 1.00
付近です。
この縦方向にPFの高い領域が広く形成されています。
例えばPF上位条件を見ると、
0.60~1.00
取引数344
PF 1.861
0.58~1.00
取引数361
PF 1.778
0.56~1.00
取引数381
PF 1.722
0.50~1.00
取引数452
PF 1.708
0.48~1.00
取引数481
PF 1.695
0.46~1.00
取引数503
PF 1.708
となっています。
さらに興味深いのが、
0.34~1.00
取引数675
PF 1.691
です。
かなり条件を広げてもPF1.69を維持しています。
これは、
ある極端に狭い範囲だけが強い
という結果ではありません。
むしろ、
Relative Band Distanceの上限1.00付近を境に、かなり広い下限設定で良好な成績が出ている
ように見えます。
「1.00」には意味があるのか
Relative Band Distanceは、
Band Distance ÷ ATR
です。
したがって、
Relative Band Distance = 1.00
とは、
ボリンジャーバンドからの距離が、おおよそATR1個分
という意味になります。
これは少し面白い結果です。
これまでQ3という統計的な区切りを追っていましたが、今回初めて、
ATR1個分
という比較的解釈しやすい水準が候補として浮かび上がりました。
つまり零号機では、
バンドを抜けているものの、ATR1個分以上離れすぎていない位置
が比較的良い可能性があります。
もちろん、今回だけで、
1.00が相場の真の境界
と結論づけることはできません。
1.00というきれいな数字が見えたからこそ、逆に慎重になる必要があります。
最高PFは0.60~1.00だった
今回の探索範囲の中で、最もPFが高かった条件は、
0.60~1.00
でした。
結果は、
取引数:344
勝率:53.49%
総損益:+94,287円
平均損益:+274円
中央値:+117円
PF:1.861
です。
PFだけを見ると非常に魅力的です。
ただし、これをそのまま、
最適値は0.60~1.00
とするつもりはありません。
その理由は今回ずっと見てきたとおり、
最高値を採用すること自体が過剰最適化につながる
からです。
むしろ興味深いのは、
0.46~1.00
0.48~1.00
0.50~1.00
0.56~1.00
0.58~1.00
0.60~1.00
など、周囲にもPF1.7前後の条件が多数存在することです。
こちらの方が今回の分析では重要です。
Q3仮説を少し修正する必要がありそう
これまで私は、
Relative Band DistanceのQ3が強い
と考えてきました。
しかし、今回の結果を見ると、少し表現を変えた方が良さそうです。
もしかすると本当に存在しているのは、
Q3という区間
ではなく、
バンドからある程度離れているが、ATR1個分を大きく超えていない領域
の優位性なのかもしれません。
つまり今回の分析によって、
Q3が強い
という統計上の発見から、
Relative Band Distance 1.0付近に
何らかの構造が存在するのではないか
という、もう少し具体的な仮説へ進むことができました。
これはかなり大きな変化です。
ただし、ここでフィルターを採用してはいけない
結果だけを見ると、
Relative Band Distance
0.4~1.0
あたりを零号機に入れたくなります。
しかし、まだ一つ大きな問題があります。
今回も、
同じ過去データを使って条件を探索している
からです。
0.30~0.60、
0.70~1.12、
という大量の組み合わせを試した結果として、
1.00付近が良い
と発見しています。
つまり、1.00という数字もまた、
過去データを見たから発見できた数字
です。
これをそのまま同じデータで評価して、
PF1.64だから強い
としてしまうと、探索と評価を同じデータで行うことになります。
これは避けなければなりません。
次回
発見した「1.00」は未来でも通用するのか
次回は、今回見つかった、
Relative Band Distanceの上限1.00付近
について、
時間的に分割しても優位性が残るのか
を改めて検証します。
第21回では、
0.44~0.88
というQ3について時間安定性を調べました。
しかし今回、
0.44~1.00
や、
0.46~1.00
0.50~1.00
0.60~1.00
など、より強い候補が見つかりました。
そこで次回は、例えばデータを時系列で、
前半50%
後半50%
または、
Period1
Period2
Period3
Period4
に分割します。
そして、
0.44~0.88
0.44~1.00
0.50~1.00
0.60~1.00
などの条件について、
取引数
PF
総損益
平均損益
中央値
を比較します。
特に確認したいのは、
0.60~1.00という最高PF条件が、すべての期間で強いのか
です。
もし、
Period1 PF 2.5
Period2 PF 2.2
Period3 PF 0.9
Period4 PF 0.8
のような結果であれば、全期間PF1.86という数字は一部期間に依存している可能性があります。
一方で、
Period1 PF 1.6
Period2 PF 1.9
Period3 PF 1.7
Period4 PF 1.5
のように安定していれば、
Relative Band Distance 0.6~1.0付近
はかなり有力なフィルター候補になります。
さらに、
0.44~1.00
のような広めの条件と、
0.60~1.00
という狭い条件を比較することで、
PFを最大化する狭い条件
と、
多少PFは下がっても取引数と安定性を確保できる条件
のどちらを採用するべきかも考えたいと思います。
次回予告
今回の分析では、
Q3の境界は偶然ではなさそう
というところから、さらに、
Relative Band Distance 1.00付近に興味深い境界が存在する
という新しい仮説が見えてきました。
しかしこれは、過去データを探索して見つけた数字です。
次回は、
「0.44~1.00」「0.60~1.00」は時代が変わっても強いのか
を検証します。
最高PFを追いかけるのではなく、
時間が変わっても残る頑丈な条件
を探します。
今回のコード全文
ファイル名:
relative_band_threshold_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_CSV = os.path.join(
OUTPUT_DIR,
"relative_band_threshold_analysis.csv"
)
OUTPUT_FILTERED_CSV = os.path.join(
OUTPUT_DIR,
"relative_band_threshold_filtered.csv"
)
OUTPUT_HEATMAP = os.path.join(
OUTPUT_DIR,
"relative_band_pf_heatmap.png"
)
OUTPUT_LOWER_GRAPH = os.path.join(
OUTPUT_DIR,
"relative_band_lower_sensitivity.png"
)
OUTPUT_UPPER_GRAPH = os.path.join(
OUTPUT_DIR,
"relative_band_upper_sensitivity.png"
)
OUTPUT_TOP_CSV = os.path.join(
OUTPUT_DIR,
"relative_band_threshold_top.csv"
)
# 元Q3のおおよその境界
BASE_LOWER = 0.44
BASE_UPPER = 0.88
# 探索範囲
LOWER_VALUES = np.round(
np.arange(
0.30,
0.62,
0.02
),
2
)
UPPER_VALUES = np.round(
np.arange(
0.70,
1.12,
0.02
),
2
)
# 最低取引数
MIN_TRADES = 100
# ============================================================
# データ読み込み
# ============================================================
df = pd.read_csv(
INPUT_FILE
)
print(
"========================================"
)
print(
"RELATIVE BAND THRESHOLD ANALYSIS"
)
print(
"========================================"
)
print(
f"\n読み込み件数: {len(df)}"
)
# ============================================================
# 必要列確認
# ============================================================
required_columns = [
"entry_band_distance_pips",
"entry_atr14_pips",
"profit"
]
missing_columns = [
column
for column in required_columns
if column not in df.columns
]
if missing_columns:
raise ValueError(
"必要な列がありません: "
+ ", ".join(missing_columns)
)
# ============================================================
# 数値変換
# ============================================================
for column in required_columns:
df[column] = pd.to_numeric(
df[column],
errors="coerce"
)
# ============================================================
# 分析対象データ
# ============================================================
analysis_df = df[
df[
"entry_band_distance_pips"
].notna()
&
df[
"entry_atr14_pips"
].notna()
&
df[
"profit"
].notna()
&
(
df[
"entry_atr14_pips"
]
> 0
)
].copy()
if analysis_df.empty:
raise ValueError(
"分析対象データがありません。"
)
# ============================================================
# 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()
print(
f"分析対象件数: {len(analysis_df)}"
)
print(
"\nRelative Band Distance 基本統計量"
)
print(
analysis_df[
"relative_band_distance"
].describe()
)
# ============================================================
# 集計関数
# ============================================================
def calculate_metrics(
selected,
lower,
upper
):
trade_count = len(
selected
)
if trade_count == 0:
return {
"lower": lower,
"upper": upper,
"trade_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()
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 {
"lower": lower,
"upper": upper,
"trade_count": trade_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
}
# ============================================================
# 全組み合わせを分析
# ============================================================
results = []
for lower in LOWER_VALUES:
for upper in UPPER_VALUES:
if lower >= upper:
continue
mask = (
(
analysis_df[
"relative_band_distance"
]
>= lower
)
&
(
analysis_df[
"relative_band_distance"
]
<= upper
)
)
selected = analysis_df[
mask
]
metrics = calculate_metrics(
selected,
lower,
upper
)
results.append(
metrics
)
result_df = pd.DataFrame(
results
)
# ============================================================
# CSV保存
# ============================================================
result_df.to_csv(
OUTPUT_CSV,
index=False,
encoding="utf-8-sig"
)
print(
f"\n保存: {OUTPUT_CSV}"
)
# ============================================================
# 最低取引数以上に限定
# ============================================================
filtered_df = result_df[
result_df[
"trade_count"
]
>= MIN_TRADES
].copy()
filtered_df.to_csv(
OUTPUT_FILTERED_CSV,
index=False,
encoding="utf-8-sig"
)
print(
f"保存: {OUTPUT_FILTERED_CSV}"
)
# ============================================================
# PF上位条件
# ============================================================
top_df = (
filtered_df
.sort_values(
"profit_factor",
ascending=False
)
.head(
30
)
.copy()
)
top_df.to_csv(
OUTPUT_TOP_CSV,
index=False,
encoding="utf-8-sig"
)
print(
f"保存: {OUTPUT_TOP_CSV}"
)
print(
"\n========================================"
)
print(
f"PF上位条件"
f"(取引数{MIN_TRADES}以上)"
)
print(
"========================================"
)
print(
top_df[
[
"lower",
"upper",
"trade_count",
"win_rate",
"total_profit",
"median_profit",
"profit_factor"
]
]
.head(
20
)
.to_string(
index=False
)
)
# ============================================================
# 元Q3相当条件
# ============================================================
base_mask = (
(
analysis_df[
"relative_band_distance"
]
>= BASE_LOWER
)
&
(
analysis_df[
"relative_band_distance"
]
<= BASE_UPPER
)
)
base_df = analysis_df[
base_mask
]
base_metrics = calculate_metrics(
base_df,
BASE_LOWER,
BASE_UPPER
)
print(
"\n========================================"
)
print(
"元Q3相当条件"
)
print(
"========================================"
)
for key, value in base_metrics.items():
print(
f"{key}: {value}"
)
# ============================================================
# PF 1以上 / 1.2以上 / 1.3以上の条件数
# ============================================================
print(
"\n========================================"
)
print(
"安定領域確認"
)
print(
"========================================"
)
valid_count = len(
filtered_df
)
pf_100_count = (
filtered_df[
"profit_factor"
]
>= 1.0
).sum()
pf_120_count = (
filtered_df[
"profit_factor"
]
>= 1.2
).sum()
pf_130_count = (
filtered_df[
"profit_factor"
]
>= 1.3
).sum()
print(
f"評価対象組み合わせ数: "
f"{valid_count}"
)
print(
f"PF >= 1.0: "
f"{pf_100_count}"
)
print(
f"PF >= 1.2: "
f"{pf_120_count}"
)
print(
f"PF >= 1.3: "
f"{pf_130_count}"
)
if valid_count > 0:
print(
f"PF1以上割合: "
f"{pf_100_count / valid_count * 100:.1f}%"
)
print(
f"PF1.2以上割合: "
f"{pf_120_count / valid_count * 100:.1f}%"
)
print(
f"PF1.3以上割合: "
f"{pf_130_count / valid_count * 100:.1f}%"
)
# ============================================================
# Q3近傍だけを確認
# ============================================================
near_q3_df = filtered_df[
(
filtered_df[
"lower"
]
>= 0.38
)
&
(
filtered_df[
"lower"
]
<= 0.50
)
&
(
filtered_df[
"upper"
]
>= 0.82
)
&
(
filtered_df[
"upper"
]
<= 0.94
)
].copy()
print(
"\n========================================"
)
print(
"Q3近傍"
)
print(
"========================================"
)
print(
near_q3_df[
[
"lower",
"upper",
"trade_count",
"total_profit",
"median_profit",
"profit_factor"
]
]
.sort_values(
[
"lower",
"upper"
]
)
.to_string(
index=False
)
)
# ============================================================
# ヒートマップ用データ
# ============================================================
heatmap_df = (
filtered_df
.pivot(
index="lower",
columns="upper",
values="profit_factor"
)
.sort_index(
ascending=False
)
)
# ============================================================
# PFヒートマップ
# ============================================================
fig, ax = plt.subplots(
figsize=(
14,
9
)
)
image = ax.imshow(
heatmap_df.values,
aspect="auto",
interpolation="nearest"
)
ax.set_title(
"Profit Factor Heatmap\n"
"Relative Band Distance Threshold"
)
ax.set_xlabel(
"Upper Threshold"
)
ax.set_ylabel(
"Lower Threshold"
)
ax.set_xticks(
np.arange(
len(
heatmap_df.columns
)
)
)
ax.set_xticklabels(
[
f"{value:.2f}"
for value
in heatmap_df.columns
],
rotation=90
)
ax.set_yticks(
np.arange(
len(
heatmap_df.index
)
)
)
ax.set_yticklabels(
[
f"{value:.2f}"
for value
in heatmap_df.index
]
)
colorbar = fig.colorbar(
image,
ax=ax
)
colorbar.set_label(
"Profit Factor"
)
plt.tight_layout()
plt.savefig(
OUTPUT_HEATMAP,
dpi=150
)
plt.close()
print(
f"\n保存: {OUTPUT_HEATMAP}"
)
# ============================================================
# 上限を元Q3付近に固定し下限を動かす
# ============================================================
fixed_upper_candidates = result_df[
np.isclose(
result_df[
"upper"
],
BASE_UPPER
)
].copy()
fixed_upper_candidates = (
fixed_upper_candidates
.sort_values(
"lower"
)
)
plt.figure(
figsize=(
11,
6
)
)
plt.plot(
fixed_upper_candidates[
"lower"
],
fixed_upper_candidates[
"profit_factor"
],
marker="o"
)
plt.axhline(
1.0,
linestyle="--"
)
plt.axvline(
BASE_LOWER,
linestyle="--"
)
plt.title(
f"Lower Threshold Sensitivity "
f"(Upper = {BASE_UPPER:.2f})"
)
plt.xlabel(
"Lower Threshold"
)
plt.ylabel(
"Profit Factor"
)
plt.grid()
plt.tight_layout()
plt.savefig(
OUTPUT_LOWER_GRAPH,
dpi=150
)
plt.close()
print(
f"保存: {OUTPUT_LOWER_GRAPH}"
)
# ============================================================
# 下限を元Q3付近に固定し上限を動かす
# ============================================================
fixed_lower_candidates = result_df[
np.isclose(
result_df[
"lower"
],
BASE_LOWER
)
].copy()
fixed_lower_candidates = (
fixed_lower_candidates
.sort_values(
"upper"
)
)
plt.figure(
figsize=(
11,
6
)
)
plt.plot(
fixed_lower_candidates[
"upper"
],
fixed_lower_candidates[
"profit_factor"
],
marker="o"
)
plt.axhline(
1.0,
linestyle="--"
)
plt.axvline(
BASE_UPPER,
linestyle="--"
)
plt.title(
f"Upper Threshold Sensitivity "
f"(Lower = {BASE_LOWER:.2f})"
)
plt.xlabel(
"Upper Threshold"
)
plt.ylabel(
"Profit Factor"
)
plt.grid()
plt.tight_layout()
plt.savefig(
OUTPUT_UPPER_GRAPH,
dpi=150
)
plt.close()
print(
f"保存: {OUTPUT_UPPER_GRAPH}"
)
# ============================================================
# 最終表示
# ============================================================
print(
"\n========================================"
)
print(
"分析完了"
)
print(
"========================================"
)
print(
"\n出力ファイル:"
)
print(
OUTPUT_CSV
)
print(
OUTPUT_FILTERED_CSV
)
print(
OUTPUT_TOP_CSV
)
print(
OUTPUT_HEATMAP
)
print(
OUTPUT_LOWER_GRAPH
)
print(
OUTPUT_UPPER_GRAPH
)
出力されるファイル
今回のコードを実行すると、resultsフォルダに次のファイルが出力されます。
relative_band_threshold_analysis.csv
relative_band_threshold_filtered.csv
relative_band_threshold_top.csv
relative_band_pf_heatmap.png
relative_band_lower_sensitivity.png
relative_band_upper_sensitivity.png
特に次回確認するのは、
relative_band_threshold_analysis.csv
relative_band_pf_heatmap.png
relative_band_lower_sensitivity.png
relative_band_upper_sensitivity.png
です。
ここから、
Q3が本当に広い優位性を持つゾーンなのか
を判断していきます。
コメント