はじめに
前回は、エントリー時のATRとボリンジャーバンド幅を組み合わせ、零号機の取引を16種類の相場環境に分けて分析しました。

その結果、
- 低ATR × 狭いBB幅では成績が低下する
- 中程度のボラティリティでは比較的良好な成績となる
- ATRとBB幅には強い相関があるものの、完全に同じ情報ではない
ということが見えてきました。
ここまで調べてきたのは、
「エントリーしたとき、相場がどのような状態だったのか」
という相場環境です。
今回は少し視点を変えます。
零号機ではエントリー時に、
entry_band_distance_pips
という値も記録しています。
これは、エントリーした瞬間に価格がボリンジャーバンドからどれだけ離れていたのかを示す値です。
同じボリンジャーバンドのブレイクでも、
バンドを少しだけ抜けたところでエントリーした場合
と、
バンドから大きく離れたところでエントリーした場合
では、その後の値動きが違うかもしれません。
今回は、この「バンドからの距離」と取引成績の関係を調べてみます。
BAND_DISTANCEとは
零号機では、エントリー時の価格とボリンジャーバンドとの距離をpips単位で記録しています。
例えば、
entry_band_distance_pips = 1.5
であれば、
エントリー時点で価格がバンドから1.5pips離れていた
ということになります。
この値が小さければ、バンドを抜けた直後に近いエントリー。
反対に値が大きければ、すでに価格がバンドから大きく離れた状態でのエントリーです。
ここで考えられる仮説は2つあります。
仮説① 大きく抜けた方が強い
価格がバンドを大きく突き抜けているということは、それだけ強い値動きが発生している可能性があります。
その場合、
BAND_DISTANCEが大きいほど、その後もトレンドが継続しやすい
かもしれません。
仮説② 大きく抜けすぎると逆に弱い
一方で、バンドから大きく離れた時点では、
すでに価格が動きすぎている
可能性もあります。
その場合、高値掴み・安値掴みとなり、その後反転しやすくなるかもしれません。
どちらなのかを実際の取引データから確認します。
まず分布を確認する
これまでのATRやBB幅と同じように、いきなり取引成績を分析するのではなく、まずBAND_DISTANCEがどのように分布しているのか確認します。
今回使用するのは、
results/trade_base.csv
です。
必要なのは、
entry_band_distance_pips
と、
profit
です。
まずデータを読み込みます。
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)
)
BAND_DISTANCEが記録されている取引だけを抽出します。
band_df = df[
df["entry_band_distance_pips"].notna()
].copy()
print(
"\nBAND_DISTANCE 分析対象件数:",
len(band_df)
)
基本統計量を見る
まず、
print(
"\nBAND_DISTANCE 基本統計量"
)
print(
band_df["entry_band_distance_pips"].describe()
)
として、
- 平均値
- 標準偏差
- 最小値
- 25%点
- 中央値
- 75%点
- 最大値
を確認します。
これによって、
零号機は普段、バンドからどの程度離れた位置でエントリーしているのか
が分かります。
ヒストグラムで分布を見る
数値だけでは分かりにくいため、ヒストグラムも作成します。
fig, ax = plt.subplots(
figsize=(9, 6)
)
ax.hist(
band_df["entry_band_distance_pips"],
bins=40
)
ax.set_xlabel(
"Band Distance (pips)"
)
ax.set_ylabel(
"Trades"
)
ax.set_title(
"Distribution of Entry Band Distance"
)
plt.tight_layout()
plt.savefig(
"results/band_distance_histogram.png",
dpi=150
)
plt.close()
これで、零号機のエントリーがどの距離帯に集中しているのかを視覚的に確認できます。
BAND_DISTANCEを四分位に分ける
続いて、ATRやBB幅と同じように、BAND_DISTANCEを四分位に分けます。
band_df["band_distance_quartile_no"] = pd.qcut(
band_df["entry_band_distance_pips"],
q=4,
labels=False,
duplicates="drop"
)
さらに、
band_df["band_distance_group"] = (
band_df["band_distance_quartile_no"]
.apply(
lambda x:
f"Q{int(x) + 1}"
if pd.notna(x)
else np.nan
)
)
とします。
これで、
Q1 → バンドからの距離が小さい25%
Q2 → 次の25%
Q3 → 次の25%
Q4 → バンドからの距離が大きい25%
となります。
各グループの距離を確認する
四分位の実際の範囲も確認しておきます。
distance_range = (
band_df
.groupby(
"band_distance_group",
observed=True
)
["entry_band_distance_pips"]
.agg(
[
"min",
"max",
"mean",
"median",
"count"
]
)
)
print(
"\nBAND_DISTANCE 四分位"
)
print(
distance_range
)
これで、
Q1が実際には何pipsまでなのか
Q4では何pips程度離れているのか
を確認できます。
四分位ごとの取引成績を計算する
ここから本題です。
各グループについて、
- 取引回数
- 勝率
- 総損益
- 平均損益
- PF
を計算します。
まず基本成績です。
summary = (
band_df
.groupby(
"band_distance_group",
observed=True
)
.agg(
trades=(
"profit",
"count"
),
total_profit=(
"profit",
"sum"
),
average_profit=(
"profit",
"mean"
)
)
)
勝率を計算する
win_rate = (
band_df
.assign(
win=band_df["profit"] > 0
)
.groupby(
"band_distance_group",
observed=True
)
["win"]
.mean()
* 100
)
summary["win_rate"] = win_rate
PFを計算する
総利益を計算します。
gross_profit = (
band_df[
band_df["profit"] > 0
]
.groupby(
"band_distance_group",
observed=True
)
["profit"]
.sum()
)
続いて総損失です。
gross_loss = (
band_df[
band_df["profit"] < 0
]
.groupby(
"band_distance_group",
observed=True
)
["profit"]
.sum()
.abs()
)
PFを計算します。
summary["profit_factor"] = (
gross_profit
/ gross_loss
)
最後に列順を整えます。
summary = summary[
[
"trades",
"win_rate",
"total_profit",
"average_profit",
"profit_factor"
]
]
summary = summary.reindex(
[
"Q1",
"Q2",
"Q3",
"Q4"
]
)
print(
"\nBAND_DISTANCE別 取引成績"
)
print(
summary
)
CSVに保存する
分析結果は、
distance_range.to_csv(
"results/band_distance_ranges.csv",
encoding="utf-8-sig"
)
summary.to_csv(
"results/band_distance_analysis.csv",
encoding="utf-8-sig"
)
として保存します。
PFをグラフにする
今回最も注目したいPFを棒グラフにします。
fig, ax = plt.subplots(
figsize=(8, 6)
)
summary["profit_factor"].plot(
kind="bar",
ax=ax
)
ax.axhline(
1.0,
linestyle="--"
)
ax.set_xlabel(
"Band Distance Quartile"
)
ax.set_ylabel(
"Profit Factor"
)
ax.set_title(
"Profit Factor by Entry Band Distance"
)
ax.tick_params(
axis="x",
rotation=0
)
plt.tight_layout()
plt.savefig(
"results/band_distance_pf.png",
dpi=150
)
plt.close()
PF=1のラインも入れておきます。
これによって、
バンドからの距離が大きくなるほどPFが上がるのか
あるいは、
ある距離を超えるとPFが低下するのか
を直感的に確認できます。
平均損益も確認する
PFだけではなく、1取引あたりの平均損益もグラフにします。
fig, ax = plt.subplots(
figsize=(8, 6)
)
summary["average_profit"].plot(
kind="bar",
ax=ax
)
ax.axhline(
0,
linestyle="--"
)
ax.set_xlabel(
"Band Distance Quartile"
)
ax.set_ylabel(
"Average Profit"
)
ax.set_title(
"Average Profit by Entry Band Distance"
)
ax.tick_params(
axis="x",
rotation=0
)
plt.tight_layout()
plt.savefig(
"results/band_distance_average_profit.png",
dpi=150
)
plt.close()
出力されるファイル
今回の分析では、
results/
├─ band_distance_analysis.csv
├─ band_distance_ranges.csv
├─ band_distance_histogram.png
├─ band_distance_pf.png
└─ band_distance_average_profit.png
の5ファイルが作成されます。
分析結果
BAND_DISTANCEを四分位に分けて取引成績を比較した結果、以下のようになりました。
| グループ | BAND_DISTANCE | 取引数 | 勝率 | 総損益 | 平均損益 | PF |
|---|---|---|---|---|---|---|
| Q1 | 0.002~1.264 pips | 432 | 74.07% | 33,462.91 | 77.46 | 1.310 |
| Q2 | 1.266~3.561 pips | 431 | 67.52% | 22,559.94 | 52.34 | 1.161 |
| Q3 | 3.575~7.218 pips | 431 | 56.61% | 46,160.75 | 107.10 | 1.298 |
| Q4 | 7.219~75.473 pips | 431 | 48.96% | 111,106.01 | 257.79 | 1.425 |
まず目につくのが、BAND_DISTANCEが大きくなるにつれて勝率が大きく低下していることです。
Q1では勝率74.07%だったのに対し、
- Q2:67.52%
- Q3:56.61%
- Q4:48.96%
となりました。
最もバンドから離れてエントリーしたQ4では、ついに勝率が50%を下回っています。
この数字だけを見ると、
バンドから離れてからエントリーするほど成績が悪くなる
ようにも見えます。
しかし、PFと平均損益を見ると、まったく違う姿が現れました。
勝率と収益性が逆方向に動いている

Q4の勝率は48.96%で4グループ中最低でした。
ところがPFは、
1.425
と4グループ中最高です。

平均損益についても、
- Q1:77.46
- Q2:52.34
- Q3:107.10
- Q4:257.79
となっており、Q4は突出しています。
つまり、
バンドから大きく離れてエントリーした取引は負ける回数そのものは多いが、勝ったときの利益が大きい
という構造になっている可能性があります。
これは零号機の売買ロジックを考える上で非常に興味深い結果です。
単純に、
勝率が高い条件=良い条件
とは言えないことが改めて確認できました。
Q4の総損益は11万を超えた
総損益を見ると、その差はさらに明確です。
Q1は約3.3万、Q2は約2.3万、Q3は約4.6万だったのに対し、Q4は、
111,106.01
となっています。
全4グループの中で圧倒的に大きい値です。
取引数は各グループほぼ同じなので、単純にQ4の取引数が多いためではありません。
1取引あたりの平均損益も257.79と大きいため、
バンドから大きく離れるほど、一部の強いトレンドを捕まえた際の利益が大きくなる
可能性が考えられます。
零号機はトレンドフォロー型なので、この結果はロジックの性格ともある程度整合します。
Q4は「負け越している」のに最も稼いでいる
今回特に面白いのがQ4です。
431回の取引に対して、
- 勝ち:211回
- 負け:220回
となっており、負けの方が多くなりました。
さらに中央値も、
-33.18
です。
つまりQ4では「典型的な1回の取引」を見ると、むしろわずかに損失側にあります。
それにもかかわらず、
- 総損益:111,106.01
- 平均損益:257.79
- PF:1.425
となっています。
これは平均値を一部の大きな利益が押し上げている可能性を示しています。
言い換えると、
BAND_DISTANCEが大きい取引では、小さな負けを繰り返しながら、ときどき発生する大きなトレンドを捕まえて利益を稼いでいる
という構造かもしれません。
この点は零号機の戦略を理解する上でかなり重要です。
「大きく抜けたら遅すぎる」という仮説とは逆だった
分析前には、
バンドから大きく離れてからエントリーすると、すでに価格が動きすぎており、高値掴み・安値掴みになるのではないか
という仮説も考えていました。
しかし今回の結果を見る限り、少なくとも単純なBAND_DISTANCEだけで見ると、その傾向は確認できませんでした。
むしろQ4のPFが最も高かったことから、
バンドを大きく抜けるほど、強いトレンドが発生している可能性が高い
という仮説の方が有力になりました。
ただし、ここで注意が必要です。
Q4の範囲はかなり広い

BAND_DISTANCEの分布を見ると、かなり右側に長い裾を持っています。
四分位の範囲は、
- Q1:0.002~1.264 pips
- Q2:1.266~3.561 pips
- Q3:3.575~7.218 pips
- Q4:7.219~75.473 pips
でした。
Q1~Q3までは比較的狭い範囲ですが、Q4だけは、
7.219~75.473pips
と非常に広くなっています。
Q4の平均値は14.986pips、中央値は11.448pipsなので、50pipsや70pipsといった極端なエントリーは少数と考えられます。
つまり、
Q4が強いのは7pips以上なら何でも強いからなのか
それとも、
20pips、30pips、あるいはそれ以上離れた少数の取引が大きな利益を出しているだけなのか
は、今回の分析だけではまだ分かりません。
ここは今後さらに分解する価値があります。
ヒストグラムから分かること
BAND_DISTANCEの分布は左右対称ではなく、明確な右裾の長い分布となりました。
多くの取引は0~10pips程度に集中しており、距離が大きくなるほど取引数は急速に少なくなっています。
一方で、
- 20pips
- 30pips
- 40pips
- 50pips以上
といった取引も少数ながら存在しています。
このためBAND_DISTANCEについては、単純な平均値だけで判断すると実態を捉えにくそうです。
今回、四分位で分析したこと自体は妥当だったと考えられます。
Q2が最も弱かった
もう一つ気になるのがQ2です。
Q2は、
- 勝率:67.52%
- PF:1.161
- 平均損益:52.34
- 総損益:22,559.94
となり、4グループの中で最も収益性が低くなりました。
つまり、
BAND_DISTANCEが小さいほど良い
わけでも、
BAND_DISTANCEが大きいほど単調に良くなる
わけでもありません。
PFは、
Q1 1.310 → Q2 1.161 → Q3 1.298 → Q4 1.425
となっており、一度Q2で落ち込んだ後、Q3・Q4で上昇しています。
このことからも、BAND_DISTANCEと収益性の関係は単純な直線関係ではなさそうです。
今回見えてきた零号機の特徴
今回の結果から、零号機には次のような特徴がある可能性があります。
バンドをわずかに抜けた場面
→ 勝率が高い。
バンドを大きく抜けた場面
→ 勝率は低下する。
しかし、
バンドを大きく抜けた場面
→ 当たったときの利益が大きく、最終的なPFと平均損益は高い。
つまり零号機には、
小さなブレイクでは高勝率型、大きなブレイクでは低勝率・高リターン型
という、異なる2つの顔が存在している可能性があります。
これはかなり面白い発見です。
ただし、まだ「Q4だけ取引する」とはしない
今回の結果だけを見ると、
7.2pips以上離れたときだけエントリーすればいいのではないか
と考えたくなります。
しかし、ここで売買条件に組み込むのはまだ早いと考えます。
なぜなら、BAND_DISTANCEは相場のボラティリティと無関係ではないからです。
例えば10pipsのBAND_DISTANCEでも、
ATRが5pipsしかない相場なら非常に大きなブレイクですが、
ATRが30pipsある相場なら、それほど大きなブレイクではありません。
つまり、
BAND_DISTANCEという絶対値だけでは、本当の意味での「ブレイクの強さ」を表せていない
可能性があります。
今回の分析で見えてきたQ4の強さが、
BAND_DISTANCEそのものによる効果なのか
それとも、
そもそもATRが大きい相場がQ4に多いだけなのか
を切り分ける必要があります。
ここが次の分析テーマになります。
今回のコード全文
コード全文を見る
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)
)
# ==============================
# resultsフォルダ作成
# ==============================
os.makedirs(
"results",
exist_ok=True
)
# ==============================
# 分析対象データ
# ==============================
band_df = df[
df["entry_band_distance_pips"].notna()
].copy()
print(
"\nBAND_DISTANCE 分析対象件数:",
len(band_df)
)
# ==============================
# 基本統計量
# ==============================
print(
"\nBAND_DISTANCE 基本統計量"
)
print(
band_df["entry_band_distance_pips"].describe()
)
# ==============================
# ヒストグラム
# ==============================
fig, ax = plt.subplots(
figsize=(9, 6)
)
ax.hist(
band_df["entry_band_distance_pips"],
bins=40
)
ax.set_xlabel(
"Band Distance (pips)"
)
ax.set_ylabel(
"Trades"
)
ax.set_title(
"Distribution of Entry Band Distance"
)
plt.tight_layout()
plt.savefig(
"results/band_distance_histogram.png",
dpi=150
)
plt.close()
# ==============================
# 四分位に分類
# ==============================
band_df["band_distance_quartile_no"] = pd.qcut(
band_df["entry_band_distance_pips"],
q=4,
labels=False,
duplicates="drop"
)
band_df["band_distance_group"] = (
band_df["band_distance_quartile_no"]
.apply(
lambda x:
f"Q{int(x) + 1}"
if pd.notna(x)
else np.nan
)
)
# ==============================
# 各四分位の距離範囲
# ==============================
distance_range = (
band_df
.groupby(
"band_distance_group",
observed=True
)
["entry_band_distance_pips"]
.agg(
[
"min",
"max",
"mean",
"median",
"count"
]
)
)
distance_range = distance_range.reindex(
[
"Q1",
"Q2",
"Q3",
"Q4"
]
)
print(
"\nBAND_DISTANCE 四分位"
)
print(
distance_range
)
# ==============================
# 基本成績
# ==============================
summary = (
band_df
.groupby(
"band_distance_group",
observed=True
)
.agg(
trades=(
"profit",
"count"
),
total_profit=(
"profit",
"sum"
),
average_profit=(
"profit",
"mean"
)
)
)
# ==============================
# 勝率
# ==============================
win_rate = (
band_df
.assign(
win=band_df["profit"] > 0
)
.groupby(
"band_distance_group",
observed=True
)
["win"]
.mean()
* 100
)
summary["win_rate"] = win_rate
# ==============================
# 総利益
# ==============================
gross_profit = (
band_df[
band_df["profit"] > 0
]
.groupby(
"band_distance_group",
observed=True
)
["profit"]
.sum()
)
# ==============================
# 総損失
# ==============================
gross_loss = (
band_df[
band_df["profit"] < 0
]
.groupby(
"band_distance_group",
observed=True
)
["profit"]
.sum()
.abs()
)
# ==============================
# PF
# ==============================
summary["profit_factor"] = (
gross_profit
/ gross_loss
)
# ==============================
# 列順・行順
# ==============================
summary = summary[
[
"trades",
"win_rate",
"total_profit",
"average_profit",
"profit_factor"
]
]
summary = summary.reindex(
[
"Q1",
"Q2",
"Q3",
"Q4"
]
)
print(
"\nBAND_DISTANCE別 取引成績"
)
print(
summary
)
# ==============================
# CSV保存
# ==============================
distance_range.to_csv(
"results/band_distance_ranges.csv",
encoding="utf-8-sig"
)
summary.to_csv(
"results/band_distance_analysis.csv",
encoding="utf-8-sig"
)
# ==============================
# PFグラフ
# ==============================
fig, ax = plt.subplots(
figsize=(8, 6)
)
summary["profit_factor"].plot(
kind="bar",
ax=ax
)
ax.axhline(
1.0,
linestyle="--"
)
ax.set_xlabel(
"Band Distance Quartile"
)
ax.set_ylabel(
"Profit Factor"
)
ax.set_title(
"Profit Factor by Entry Band Distance"
)
ax.tick_params(
axis="x",
rotation=0
)
plt.tight_layout()
plt.savefig(
"results/band_distance_pf.png",
dpi=150
)
plt.close()
# ==============================
# 平均損益グラフ
# ==============================
fig, ax = plt.subplots(
figsize=(8, 6)
)
summary["average_profit"].plot(
kind="bar",
ax=ax
)
ax.axhline(
0,
linestyle="--"
)
ax.set_xlabel(
"Band Distance Quartile"
)
ax.set_ylabel(
"Average Profit"
)
ax.set_title(
"Average Profit by Entry Band Distance"
)
ax.tick_params(
axis="x",
rotation=0
)
plt.tight_layout()
plt.savefig(
"results/band_distance_average_profit.png",
dpi=150
)
plt.close()
print(
"\n分析完了"
)
print(
"出力ファイル:"
)
print(
"results/band_distance_analysis.csv"
)
print(
"results/band_distance_ranges.csv"
)
print(
"results/band_distance_histogram.png"
)
print(
"results/band_distance_pf.png"
)
print(
"results/band_distance_average_profit.png"
)
コメント