ランダムウォークへの挑戦⑲|BAND_DISTANCEをATRで割って「相対的なブレイクの強さ」を分析する(2026/8/19)

未分類

はじめに

前回は、エントリー時に価格がボリンジャーバンドからどれだけ離れていたのかを示す、

entry_band_distance_pips

と取引成績の関係を分析しました。

ランダムウォークへの挑戦⑱|バンドからの距離で零号機のエントリー位置を分析する(2026/8/19)
はじめに前回は、エントリー時のATRとボリンジャーバンド幅を組み合わせ、零号機の取引を16種類の相場環境に分けて分析しました。その結果、低ATR × 狭いBB幅では成績が低下する中程度のボラティリティでは比較的良好な成績となるATRとBB幅…

BAND_DISTANCEを四分位に分けたところ、結果は次のようになりました。

グループBAND_DISTANCE勝率平均損益PF
Q10.002~1.264 pips74.07%77.461.310
Q21.266~3.561 pips67.52%52.341.161
Q33.575~7.218 pips56.61%107.101.298
Q47.219~75.473 pips48.96%257.791.425

特に興味深かったのがQ4です。

最もバンドから離れてエントリーしたQ4は、勝率が48.96%と最も低かったにもかかわらず、

  • PF:1.425
  • 平均損益:257.79
  • 総損益:111,106.01

と、収益性では最も良い結果となりました。

この結果から、

バンドを大きく抜けた局面ほど、大きなトレンドを捉えやすいのではないか

という仮説が浮かびました。

しかし、ここには一つ問題があります。

BAND_DISTANCEは「pips」という絶対的な距離だからです。


同じ10pipsでも意味は同じなのか

例えば、

BAND_DISTANCE = 10pips

だったとします。

一見すると、どちらも同じだけバンドを抜けています。

しかし、そのときのATRが、

ATR = 5pips

だった場合、10pipsという距離はATRの2倍です。

一方、

ATR = 20pips

なら10pipsはATRの0.5倍にすぎません。

つまり、

同じ10pipsでも、そのときの相場のボラティリティによって「ブレイクの大きさ」の意味が変わる

ことになります。

そこで今回は、BAND_DISTANCEをATRで割った新しい指標を作ります。


Relative Band Distanceを作る

今回、

relative_band_distance

という値を作ります。

計算式は単純です。

relative_band_distance
=
entry_band_distance_pips
÷
entry_atr14_pips

例えば、

BAND_DISTANCE = 10pips
ATR = 5pips

なら、

relative_band_distance = 2.0

です。

これは、

バンドからの距離がATRの2倍

という意味になります。

一方、

BAND_DISTANCE = 10pips
ATR = 20pips

なら、

relative_band_distance = 0.5

です。

こちらは、

バンドからの距離はATRの半分

となります。

これなら単純なpipsではなく、

そのときの相場の値動きに対して、どの程度大きくバンドを突破していたのか

を比較できます。


今回調べたいこと

今回の目的は大きく二つあります。

まず、

Relative Band Distanceが大きいほど取引成績が良くなるのか

を確認します。

そしてもう一つ重要なのが、

前回BAND_DISTANCEのQ4が強かったのは、本当に「大きなブレイク」が強かったからなのか

という点です。

もしBAND_DISTANCEとATRに強い関係があるなら、

Q4には単に高ATRの相場が多かった

だけかもしれません。

その場合、前回確認したQ4の優位性をそのままエントリー条件に利用するのは危険です。

今回はこの点も確認します。


データを読み込む

今回も、

results/trade_base.csv

を使用します。

import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv(
    "results/trade_base.csv"
)

print(
    "読み込み件数:",
    len(df)
)

今回必要になるのは、

entry_band_distance_pips
entry_atr14_pips
profit

の3つです。


Relative Band Distanceを計算する

まず欠損値を除外します。

analysis_df = df[
    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"]
)

として新しい変数を作ります。


まずBAND_DISTANCEとATRの関係を見る

前回のQ4が高ATR相場の影響を受けているか考えるため、

entry_band_distance_pips

と、

entry_atr14_pips

の相関係数を確認します。

band_atr_corr = (
    analysis_df[
        [
            "entry_band_distance_pips",
            "entry_atr14_pips"
        ]
    ]
    .corr()
    .iloc[0, 1]
)

print(
    "BAND_DISTANCE と ATR の相関:",
    band_atr_corr
)

ここで強い正の相関が確認された場合、

BAND_DISTANCEが大きい取引には、もともとATRが大きい取引が多い

可能性があります。

逆に相関が弱ければ、BAND_DISTANCEにはATRとは別の情報が含まれていることになります。


Relative Band Distanceの分布を見る

次に、新しく作った指標の基本統計量を確認します。

print(
    analysis_df[
        "relative_band_distance"
    ]
    .describe()
)

さらにヒストグラムも作成します。

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

ax.hist(
    analysis_df[
        "relative_band_distance"
    ],
    bins=40
)

ax.set_xlabel(
    "Relative Band Distance"
)

ax.set_ylabel(
    "Trades"
)

ax.set_title(
    "Distribution of Relative Band Distance"
)

plt.tight_layout()

plt.savefig(
    "results/relative_band_distance_histogram.png",
    dpi=150
)

plt.close()

絶対値だった前回のBAND_DISTANCEと比べて、分布がどう変化するのかも確認します。


Relative Band Distanceを四分位にする

これまでと同じように、Relative Band Distanceを四分位に分けます。

analysis_df["relative_band_quartile_no"] = pd.qcut(
    analysis_df["relative_band_distance"],
    q=4,
    labels=False,
    duplicates="drop"
)

さらに、

analysis_df["relative_band_group"] = (
    analysis_df["relative_band_quartile_no"]
    .apply(
        lambda x:
            f"Q{int(x) + 1}"
            if pd.notna(x)
            else np.nan
    )
)

とします。

これによって、

Q1
→ ATRに対してバンドからの距離が小さい25%

Q2
→ 次の25%

Q3
→ 次の25%

Q4
→ ATRに対してバンドからの距離が大きい25%

となります。

前回のQ1~Q4とは意味が違うことに注意が必要です。

今回は単純なpipsではなく、

ボラティリティに対して、どれだけ大きくバンドを突破したか

による分類です。


各四分位の取引成績を比較する

今回も、

  • 取引数
  • 勝率
  • 総損益
  • 平均損益
  • 中央値
  • 総利益
  • 総損失
  • PF

を計算します。

前回と同じ指標を使うことで、

絶対距離で分類した場合

と、

ATRで正規化して分類した場合

を比較できるようにします。


PFを比較する

特に注目したいのがPFです。

前回、絶対的なBAND_DISTANCEでは、

Q1  1.310
Q2  1.161
Q3  1.298
Q4  1.425

となりました。

今回もQ4が最も高くなるのであれば、

相場のボラティリティに対して大きくバンドを突破した場面ほど、零号機が得意としている

可能性が高まります。

一方、正規化したことでQ4の優位性が消えるのであれば、

前回のQ4の強さは、BAND_DISTANCEそのものというより高ボラティリティ相場の影響だった

可能性が出てきます。

ここは今回最も注目したいポイントです。


前回のBAND_DISTANCE Q4の中身も調べる

さらに、前回の分析結果をもう一段掘り下げます。

BAND_DISTANCEをもう一度四分位に分け、

同時にATRも四分位に分けます。

analysis_df["band_group"] = pd.qcut(
    analysis_df["entry_band_distance_pips"],
    q=4,
    labels=[
        "Q1",
        "Q2",
        "Q3",
        "Q4"
    ]
)

analysis_df["atr_group"] = pd.qcut(
    analysis_df["entry_atr14_pips"],
    q=4,
    labels=[
        "Q1",
        "Q2",
        "Q3",
        "Q4"
    ]
)

これによって、

BAND_DISTANCE Q4の取引が、ATRのどのグループに多く存在するのか

を調べます。

もしBAND_DISTANCE Q4の大半がATR Q4に集中していた場合、

前回の結果にはATRの影響がかなり含まれている可能性があります。

逆にATR Q1~Q4に広く分布しているのであれば、

BAND_DISTANCE自体が独立した情報を持っている可能性が高まります。


BAND_DISTANCE × ATRの構成比を見る

クロス集計を作ります。

cross_count = pd.crosstab(
    analysis_df["band_group"],
    analysis_df["atr_group"]
)

print(
    cross_count
)

さらに、各BAND_DISTANCEグループの中でATRが何%を占めているかも計算します。

cross_ratio = (
    pd.crosstab(
        analysis_df["band_group"],
        analysis_df["atr_group"],
        normalize="index"
    )
    * 100
)

これによって、

前回Q4だった取引の何%がATR Q4だったのか

を直接確認できます。


分析結果

今回は、前回使用したBAND_DISTANCEをATRで割り、

Relative Band Distance
=
BAND_DISTANCE ÷ ATR

という指標を作りました。

これによって単純なpipsではなく、

そのときの相場の値動きに対して、どの程度大きくバンドを突破していたのか

を比較します。

Relative Band Distanceを四分位に分けた結果は、次のようになりました。

グループRelative Band Distance取引数勝率総損益平均損益中央値PF
Q10.0003~0.184143273.84%34,666.0280.25258.521.283
Q20.1842~0.441443169.37%44,665.11103.63321.651.300
Q30.4424~0.875943160.09%61,346.30142.33271.151.412
Q40.8766~3.851943143.85%72,612.18168.47-189.611.297

前回とは少し違う結果になりました。


平均損益はQ1からQ4まで一貫して上昇した

まず平均損益を見ると、

  • Q1:80.25
  • Q2:103.63
  • Q3:142.33
  • Q4:168.47

となりました。

Relative Band Distanceが大きくなるにつれて、平均損益は一貫して上昇しています。

つまり、

ATRに対して大きくバンドを突破した取引ほど、1回あたりの利益期待額は大きくなる

傾向が確認できました。

これは前回のBAND_DISTANCE分析とも共通しています。

絶対距離で見ても、ATRで正規化しても、

強くバンドを抜けた取引ほど、大きな値動きを捉えやすい

という性質自体は残っているようです。


一方、勝率は大きく低下した

しかし、勝率を見ると正反対の動きをしています。

Q1  73.84%
Q2  69.37%
Q3  60.09%
Q4  43.85%

Q1では約74%あった勝率が、Q4では43.85%まで低下しました。

つまり、

Relative Band Distanceが大きくなるほど勝ちやすくなる

わけではありません。

むしろ、

大きくバンドを突破した取引ほど負ける回数は増えるが、勝ったときの利益も大きくなる

という構造が見えてきました。

これは前回にも見られた特徴ですが、ATRで正規化した後も残りました。

零号機は、大きなブレイク局面では「高勝率型」ではなく、よりトレンドフォローらしい損益構造になっている可能性があります。


最もPFが高かったのはQ4ではなくQ3だった

今回、最も重要なのがPFです。

結果は、

Q1  1.283
Q2  1.300
Q3  1.412
Q4  1.297

となりました。

最高だったのはQ4ではなく、

Q3の1.412

です。

これは前回の結果と大きく違います。

前回、絶対的なBAND_DISTANCEではQ4がPF1.425で最高でした。

そのため、

バンドから大きく離れるほど有利なのではないか

と考えました。

しかしATRで正規化すると、

最も極端にバンドを突破したQ4より、その一段手前のQ3の方がPFが高い

という結果になりました。

つまり、

「大きく抜ければ大きく抜けるほど良い」

という単純な関係ではなさそうです。


「ほどよく強いブレイク」が最も効率的なのか

Q3のRelative Band Distanceは、

0.4424~0.8759

でした。

つまり、バンドからの距離がATRのおよそ0.44~0.88倍に相当する取引です。

一方、Q4は、

0.8766~3.8519

です。

ATRと同程度、あるいはATRの数倍までバンドから離れている非常に強いブレイクが含まれます。

今回のPFを見る限り、

ATRに対してある程度大きくバンドを抜けたところまでは収益性が向上するが、極端に離れすぎると効率が低下する

可能性があります。

言い換えると、

弱すぎるブレイクでもなく、極端に伸び切ったブレイクでもない「中~強程度の突破」

が、零号機にとって最も効率の良い領域なのかもしれません。


Q4は平均プラスなのに中央値がマイナス

さらに興味深いのがQ4です。

Q4は、

  • 平均損益:+168.47
  • 中央値:-189.61

となりました。

平均値は大きくプラスなのに、「真ん中の取引」を表す中央値はマイナスです。

しかも勝率も43.85%なので、242敗に対して189勝となっています。

それでも、

  • 総損益:+72,612.18
  • PF:1.297

と、最終的には利益が残っています。

これは、

多くの取引では小さく負ける一方、ごく一部の大きな勝ちが全体の利益を押し上げている

可能性をかなり強く示しています。

Q4にはトレンドフォロー戦略らしい損益分布が現れているように見えます。

ただし、現時点ではまだ「一部の大勝ちがどれくらい全体を支えているか」までは確認できていません。

ここは次回詳しく調べる必要があります。


BAND_DISTANCEとATRには中程度の相関があった

続いて、前回の分析で残った疑問を確認します。

BAND_DISTANCEとATRの相関係数を計算したところ、

0.516

となりました。

完全に同じ指標ではありませんが、無視できない程度の正の相関があります。

つまり、

ATRが大きい相場ほど、BAND_DISTANCEも大きくなりやすい

傾向があります。

これは重要です。

前回、BAND_DISTANCE Q4のPFが最も高かったため、

バンドから大きく離れること自体に優位性がある

可能性を考えました。

しかし、そのQ4にはそもそも高ATR相場が多く含まれていた可能性があります。

そこでBAND_DISTANCE四分位とATR四分位をクロス集計してみました。


BAND_DISTANCE Q4の半分以上がATR Q4だった

各BAND_DISTANCEグループのATR構成比を見ると、次のようになりました。

BAND_DISTANCEATR Q1ATR Q2ATR Q3ATR Q4
Q142.13%28.47%19.91%9.49%
Q231.55%30.39%21.81%16.24%
Q322.04%26.22%28.77%22.97%
Q44.87%14.39%29.47%51.28%

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

BAND_DISTANCE Q1ではATR Q1が42.13%を占めています。

一方、BAND_DISTANCE Q4では、

ATR Q4だけで51.28%

を占めています。

実際の取引数でも、BAND_DISTANCE Q4の431件中、

221件

がATR Q4でした。

逆にATR Q1は21件しかありません。

つまり、

大きなBAND_DISTANCEは、高ボラティリティ相場で発生しやすい

ことがはっきり確認できました。


前回の「Q4が強い」を少し修正する必要がある

第18回では、BAND_DISTANCE Q4のPFが1.425と最も高かったため、

バンドを大きく抜けた取引ほど強い可能性がある

と考えました。

今回の結果を踏まえると、この解釈は少し修正する必要がありそうです。

BAND_DISTANCE Q4には高ATR相場がかなり集中しています。

さらにATRで正規化すると、

Q1  PF 1.283
Q2  PF 1.300
Q3  PF 1.412
Q4  PF 1.297

となり、Q4の優位性は消えました。

したがって、第18回で確認したQ4の強さの一部は、

「バンドから遠く離れたから強かった」

のではなく、

「値動きの大きな相場だったため、BAND_DISTANCEも大きくなり、その相場環境自体が大きな利益を生みやすかった」

ことによる可能性があります。

この切り分けができたことは、今回の分析の大きな成果です。


単純な相関係数だけでは分からない

今回、損益との相関係数も確認しました。

BAND_DISTANCE × profit           -0.0478
Relative Band Distance × profit  -0.0221
ATR × profit                      0.0014

いずれもほぼゼロです。

一見すると、

どの指標も利益とは関係がない

ように見えます。

しかし四分位に分けてみると、Relative Band Distance Q3のPFが1.412になるなど、明らかな差が確認できました。

これは、

相関係数は「値が大きくなるほど利益も一直線に増える」という線形関係を見る指標

だからです。

今回のように、

Q1 → 普通
Q2 → 少し改善
Q3 → 最も良い
Q4 → 再び低下

という非線形な関係では、相関係数はほぼゼロでもおかしくありません。

したがってEAの分析では、

相関が低い=意味のない変数

と単純に判断しない方がよさそうです。


今回見えてきたこと

今回の結果から、少なくとも三つのことが分かりました。

まず、

BAND_DISTANCEとATRは完全に独立ではない

ということです。

相関係数は0.516で、BAND_DISTANCE Q4の半分以上をATR Q4が占めていました。

次に、

ATRで正規化しても、ブレイクの大きさによって取引成績に違いがある

ことです。

特にRelative Band Distance Q3ではPF1.412となり、一定以上の強いブレイクには意味がありそうです。

そして最後に、

極端に大きいブレイクが必ずしも最も効率的ではない

ことです。

平均損益はQ4が最大でしたが、PFはQ3が最大でした。

零号機にとって重要なのは単純な「大きなブレイク」ではなく、

相場のボラティリティに対して適度に強いブレイク

なのかもしれません。


今回のまとめ

これまで、

ATR
↓
相場の値動き

BB幅
↓
ボリンジャーバンドの広がり

ATR × BB幅
↓
相場環境

BAND_DISTANCE
↓
絶対的なブレイク距離

BAND_DISTANCE ÷ ATR
↓
相場環境に対する相対的なブレイク距離

と分析を進めてきました。

今回、ATRでBAND_DISTANCEを正規化したことで、第18回では見えなかった部分がかなりはっきりしてきました。

前回はBAND_DISTANCE Q4が最も高PFでしたが、正規化後の最高PFはQ3でした。

つまり、

「大きく抜ければ抜けるほど良い」

わけではありません。

また、BAND_DISTANCE Q4の51.28%をATR Q4が占めていたため、前回見えたQ4の強さには高ボラティリティ相場の影響もかなり含まれていそうです。

一方でRelative Band Distance Q3がPF1.412だったことから、

ボラティリティに対してある程度強くバンドを突破した局面

には、独自の優位性が存在する可能性も残りました。

しかし、まだエントリー条件には使いません。

特に今回のQ4では、

平均損益 +168.47
中央値 -189.61

という大きな乖離がありました。

これは、少数の大きな利益が平均値を押し上げている可能性があります。

次はこの「利益の中身」を調べてみます。

出力されるファイル

今回のコードを実行すると、

results/
├─ relative_band_distance_analysis.csv
├─ relative_band_distance_ranges.csv
├─ band_atr_correlation.csv
├─ band_atr_cross_count.csv
├─ band_atr_cross_ratio.csv
├─ relative_band_distance_histogram.png
├─ relative_band_distance_pf.png
└─ relative_band_distance_average_profit.png

が出力されます。

このうち特に重要なのは、

relative_band_distance_analysis.csv
relative_band_distance_ranges.csv
band_atr_correlation.csv
band_atr_cross_ratio.csv

です。

これらを使って、

前回見つかった「BAND_DISTANCE Q4の強さ」が正規化後も残るのか

を確認していきます。

今回のコード全文

ファイル名:

relative_band_distance_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_ANALYSIS_CSV = os.path.join(
    OUTPUT_DIR,
    "relative_band_distance_analysis.csv"
)

OUTPUT_RANGES_CSV = os.path.join(
    OUTPUT_DIR,
    "relative_band_distance_ranges.csv"
)

OUTPUT_CORRELATION_CSV = os.path.join(
    OUTPUT_DIR,
    "band_atr_correlation.csv"
)

OUTPUT_CROSS_COUNT_CSV = os.path.join(
    OUTPUT_DIR,
    "band_atr_cross_count.csv"
)

OUTPUT_CROSS_RATIO_CSV = os.path.join(
    OUTPUT_DIR,
    "band_atr_cross_ratio.csv"
)

OUTPUT_HISTOGRAM = os.path.join(
    OUTPUT_DIR,
    "relative_band_distance_histogram.png"
)

OUTPUT_PF_GRAPH = os.path.join(
    OUTPUT_DIR,
    "relative_band_distance_pf.png"
)

OUTPUT_AVG_PROFIT_GRAPH = os.path.join(
    OUTPUT_DIR,
    "relative_band_distance_average_profit.png"
)


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

df = pd.read_csv(
    INPUT_FILE
)

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

print(
    "RELATIVE BAND DISTANCE 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()


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


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(
    "Relative Band Distance計算後:",
    len(analysis_df)
)


# ============================================================
# 基本統計量
# ============================================================

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

print(
    "RELATIVE BAND DISTANCE 基本統計量"
)

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


relative_description = (
    analysis_df[
        "relative_band_distance"
    ]
    .describe()
)


print(
    relative_description
)


# ============================================================
# BAND_DISTANCEとATRの相関
# ============================================================

band_atr_corr = (
    analysis_df[
        [
            "entry_band_distance_pips",
            "entry_atr14_pips"
        ]
    ]
    .corr()
    .iloc[
        0,
        1
    ]
)


# ============================================================
# Relative Band Distanceとprofitの相関
# ============================================================

relative_profit_corr = (
    analysis_df[
        [
            "relative_band_distance",
            "profit"
        ]
    ]
    .corr()
    .iloc[
        0,
        1
    ]
)


# ============================================================
# BAND_DISTANCEとprofit
# ============================================================

band_profit_corr = (
    analysis_df[
        [
            "entry_band_distance_pips",
            "profit"
        ]
    ]
    .corr()
    .iloc[
        0,
        1
    ]
)


# ============================================================
# ATRとprofit
# ============================================================

atr_profit_corr = (
    analysis_df[
        [
            "entry_atr14_pips",
            "profit"
        ]
    ]
    .corr()
    .iloc[
        0,
        1
    ]
)


# ============================================================
# 相関結果
# ============================================================

correlation_df = pd.DataFrame(
    {
        "correlation": [
            band_atr_corr,
            relative_profit_corr,
            band_profit_corr,
            atr_profit_corr
        ]
    },
    index=[
        "band_distance_vs_atr",
        "relative_band_distance_vs_profit",
        "band_distance_vs_profit",
        "atr_vs_profit"
    ]
)


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

print(
    "相関係数"
)

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

print(
    correlation_df.round(6)
)


correlation_df.to_csv(
    OUTPUT_CORRELATION_CSV,
    encoding="utf-8-sig"
)


# ============================================================
# ヒストグラム
# ============================================================

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


ax.hist(
    analysis_df[
        "relative_band_distance"
    ],
    bins=40
)


ax.set_xlabel(
    "Relative Band Distance"
)


ax.set_ylabel(
    "Number of Trades"
)


ax.set_title(
    "Distribution of Relative Band Distance"
)


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


plt.tight_layout()


plt.savefig(
    OUTPUT_HISTOGRAM,
    dpi=150
)


plt.close()


# ============================================================
# 四分位に分類
# ============================================================

analysis_df[
    "relative_band_quartile_no"
] = pd.qcut(
    analysis_df[
        "relative_band_distance"
    ],
    q=4,
    labels=False,
    duplicates="drop"
)


analysis_df[
    "relative_band_group"
] = (
    analysis_df[
        "relative_band_quartile_no"
    ]
    .apply(
        lambda x:
            f"Q{int(x) + 1}"
            if pd.notna(x)
            else np.nan
    )
)


quartile_order = [
    "Q1",
    "Q2",
    "Q3",
    "Q4"
]


# ============================================================
# 四分位ごとの範囲
# ============================================================

relative_range = (
    analysis_df
    .groupby(
        "relative_band_group",
        observed=True
    )
    [
        "relative_band_distance"
    ]
    .agg(
        min_relative_distance="min",
        max_relative_distance="max",
        mean_relative_distance="mean",
        median_relative_distance="median",
        trades="count"
    )
)


relative_range = (
    relative_range
    .reindex(
        quartile_order
    )
)


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

print(
    "RELATIVE BAND DISTANCE 四分位"
)

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


print(
    relative_range
)


# ============================================================
# 勝敗判定
# ============================================================

analysis_df[
    "is_win"
] = (
    analysis_df[
        "profit"
    ]
    > 0
)


# ============================================================
# 基本成績
# ============================================================

summary = (
    analysis_df
    .groupby(
        "relative_band_group",
        observed=True
    )
    .agg(
        trades=(
            "profit",
            "count"
        ),
        total_profit=(
            "profit",
            "sum"
        ),
        average_profit=(
            "profit",
            "mean"
        ),
        median_profit=(
            "profit",
            "median"
        ),
        win_rate=(
            "is_win",
            "mean"
        )
    )
)


summary[
    "win_rate"
] = (
    summary[
        "win_rate"
    ]
    * 100
)


# ============================================================
# 勝ち数
# ============================================================

wins = (
    analysis_df[
        analysis_df[
            "profit"
        ]
        > 0
    ]
    .groupby(
        "relative_band_group",
        observed=True
    )
    .size()
)


# ============================================================
# 負け数
# ============================================================

losses = (
    analysis_df[
        analysis_df[
            "profit"
        ]
        < 0
    ]
    .groupby(
        "relative_band_group",
        observed=True
    )
    .size()
)


summary[
    "wins"
] = wins


summary[
    "losses"
] = losses


summary[
    "wins"
] = (
    summary[
        "wins"
    ]
    .fillna(0)
    .astype(int)
)


summary[
    "losses"
] = (
    summary[
        "losses"
    ]
    .fillna(0)
    .astype(int)
)


# ============================================================
# 総利益
# ============================================================

gross_profit = (
    analysis_df[
        analysis_df[
            "profit"
        ]
        > 0
    ]
    .groupby(
        "relative_band_group",
        observed=True
    )
    [
        "profit"
    ]
    .sum()
)


# ============================================================
# 総損失
# ============================================================

gross_loss = (
    analysis_df[
        analysis_df[
            "profit"
        ]
        < 0
    ]
    .groupby(
        "relative_band_group",
        observed=True
    )
    [
        "profit"
    ]
    .sum()
    .abs()
)


summary[
    "gross_profit"
] = (
    gross_profit
)


summary[
    "gross_loss"
] = (
    gross_loss
)


summary[
    "gross_profit"
] = (
    summary[
        "gross_profit"
    ]
    .fillna(0)
)


summary[
    "gross_loss"
] = (
    summary[
        "gross_loss"
    ]
    .fillna(0)
)


# ============================================================
# PF
# ============================================================

summary[
    "profit_factor"
] = np.where(
    summary[
        "gross_loss"
    ]
    > 0,
    summary[
        "gross_profit"
    ]
    /
    summary[
        "gross_loss"
    ],
    np.nan
)


# ============================================================
# Q1~Q4順に整理
# ============================================================

summary = (
    summary
    .reindex(
        quartile_order
    )
)


summary = summary[
    [
        "trades",
        "wins",
        "losses",
        "win_rate",
        "total_profit",
        "average_profit",
        "median_profit",
        "gross_profit",
        "gross_loss",
        "profit_factor"
    ]
]


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

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


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

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


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


for column in [
    "min_relative_distance",
    "max_relative_distance",
    "mean_relative_distance",
    "median_relative_distance"
]:

    relative_range[column] = (
        relative_range[column]
        .round(4)
    )


# ============================================================
# 結果表示
# ============================================================

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

print(
    "RELATIVE BAND DISTANCE別 取引成績"
)

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


print(
    summary
)


# ============================================================
# CSV保存
# ============================================================

relative_range.to_csv(
    OUTPUT_RANGES_CSV,
    encoding="utf-8-sig"
)


summary.to_csv(
    OUTPUT_ANALYSIS_CSV,
    encoding="utf-8-sig"
)


# ============================================================
# PFグラフ
# ============================================================

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


summary[
    "profit_factor"
].plot(
    kind="bar",
    ax=ax
)


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


ax.set_xlabel(
    "Relative Band Distance Quartile"
)


ax.set_ylabel(
    "Profit Factor"
)


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


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


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


plt.tight_layout()


plt.savefig(
    OUTPUT_PF_GRAPH,
    dpi=150
)


plt.close()


# ============================================================
# 平均損益グラフ
# ============================================================

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


summary[
    "average_profit"
].plot(
    kind="bar",
    ax=ax
)


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


ax.set_xlabel(
    "Relative Band Distance Quartile"
)


ax.set_ylabel(
    "Average Profit"
)


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


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


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


plt.tight_layout()


plt.savefig(
    OUTPUT_AVG_PROFIT_GRAPH,
    dpi=150
)


plt.close()


# ============================================================
# BAND_DISTANCEを四分位化
# ============================================================

analysis_df[
    "band_group"
] = pd.qcut(
    analysis_df[
        "entry_band_distance_pips"
    ],
    q=4,
    labels=quartile_order
)


# ============================================================
# ATRを四分位化
# ============================================================

analysis_df[
    "atr_group"
] = pd.qcut(
    analysis_df[
        "entry_atr14_pips"
    ],
    q=4,
    labels=quartile_order
)


# ============================================================
# BAND_DISTANCE × ATR 件数
# ============================================================

cross_count = pd.crosstab(
    analysis_df[
        "band_group"
    ],
    analysis_df[
        "atr_group"
    ]
)


cross_count = (
    cross_count
    .reindex(
        index=quartile_order,
        columns=quartile_order
    )
)


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

print(
    "BAND_DISTANCE × ATR 取引件数"
)

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


print(
    cross_count
)


# ============================================================
# BAND_DISTANCE × ATR 構成比
# ============================================================

cross_ratio = (
    pd.crosstab(
        analysis_df[
            "band_group"
        ],
        analysis_df[
            "atr_group"
        ],
        normalize="index"
    )
    * 100
)


cross_ratio = (
    cross_ratio
    .reindex(
        index=quartile_order,
        columns=quartile_order
    )
)


cross_ratio = (
    cross_ratio
    .round(2)
)


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

print(
    "BAND_DISTANCE × ATR 構成比(%)"
)

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


print(
    cross_ratio
)


cross_count.to_csv(
    OUTPUT_CROSS_COUNT_CSV,
    encoding="utf-8-sig"
)


cross_ratio.to_csv(
    OUTPUT_CROSS_RATIO_CSV,
    encoding="utf-8-sig"
)


# ============================================================
# 特にBAND_DISTANCE Q4のATR構成を表示
# ============================================================

if "Q4" in cross_ratio.index:

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

    print(
        "BAND_DISTANCE Q4 のATR構成"
    )

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

    print(
        cross_ratio.loc[
            "Q4"
        ]
    )


# ============================================================
# 全体成績
# ============================================================

total_trades = len(
    analysis_df
)


total_wins = (
    analysis_df[
        "profit"
    ]
    > 0
).sum()


total_losses = (
    analysis_df[
        "profit"
    ]
    < 0
).sum()


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


overall_win_rate = (
    total_wins
    /
    total_trades
    * 100
)


overall_gross_profit = (
    analysis_df.loc[
        analysis_df[
            "profit"
        ]
        > 0,
        "profit"
    ]
    .sum()
)


overall_gross_loss = abs(
    analysis_df.loc[
        analysis_df[
            "profit"
        ]
        < 0,
        "profit"
    ]
    .sum()
)


if overall_gross_loss > 0:

    overall_pf = (
        overall_gross_profit
        /
        overall_gross_loss
    )

else:

    overall_pf = np.nan


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

print(
    "分析対象取引 全体成績"
)

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


print(
    f"取引回数 : {total_trades}"
)


print(
    f"勝ち     : {total_wins}"
)


print(
    f"負け     : {total_losses}"
)


print(
    f"勝率     : {overall_win_rate:.2f}%"
)


print(
    f"総損益   : {total_profit:.2f}"
)


print(
    f"PF       : {overall_pf:.3f}"
)


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

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

print(
    "分析完了"
)

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


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


print(
    OUTPUT_ANALYSIS_CSV
)


print(
    OUTPUT_RANGES_CSV
)


print(
    OUTPUT_CORRELATION_CSV
)


print(
    OUTPUT_CROSS_COUNT_CSV
)


print(
    OUTPUT_CROSS_RATIO_CSV
)


print(
    OUTPUT_HISTOGRAM
)


print(
    OUTPUT_PF_GRAPH
)


print(
    OUTPUT_AVG_PROFIT_GRAPH
)

関連記事

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