ランダムウォークへの挑戦⑯|ボリンジャーバンドの幅で零号機の得意な相場を探る(2026/8/11)

ランダムウォーク 未分類

前回は、エントリー時のATRと取引成績の関係を分析しました。

ランダムウォークへの挑戦⑮|零号機は「荒れた相場」に強いのか ― ATRと取引成績の関係をPythonで分析する(2026/8/9)
はじめに前回は、零号機がMQL4から出力した trade_log.csv をPythonへ読み込み、trade_base.csvという分析用データへ変換しました。ここまでのChapter2では、零号機が取引する ↓エントリー時の相場環境を記…

ATRとは、簡単にいえば「そのときの相場がどれくらい動いているか」を表す指標です。

分析の結果、最もATRが低いQ1では勝率自体はそれほど悪くないものの、PF(プロフィットファクター)が低下していることが分かりました。

つまり、

「低ATRでは勝てない」のではなく、「勝っても利益を伸ばしにくいのではないか」

という仮説が見えてきました。

しかし、相場環境を表す指標はATRだけではありません。

零号機では、エントリー時のボリンジャーバンドの幅も、

entry_bb_width_pips

として記録しています。

そこで今回は、

「ボリンジャーバンドが狭い相場と広い相場では、零号機の成績に違いがあるのか?」

をPythonで分析してみます。


ボリンジャーバンドの「幅」とは

ボリンジャーバンドは、移動平均線を中心として、その上下に標準偏差を利用したバンドを表示するテクニカル指標です。

相場の値動きが小さいとバンドは狭くなり、値動きが大きくなるとバンドも広がる傾向があります。

今回記録している、

entry_bb_width_pips

は、零号機がエントリーした瞬間のボリンジャーバンドの幅をpipsで表したものです。

イメージとしては、

BB幅が狭い
──────── +2σ
───────  移動平均
──────── -2σ

        ↓

BB幅が広い
──────── +2σ


──────── 移動平均


─────────-2σ

という違いです。

今回はこのBB幅を使って、

零号機が「バンド収縮時」と「バンド拡大時」のどちらを得意としているのか

を調べます。


今回の分析方針

前回のATR分析と比較できるように、基本的には同じ方法を使います。

BB幅を小さい順に並べ、

グループ意味
Q1BB幅が狭い下位25%
Q225~50%
Q350~75%
Q4BB幅が広い上位25%

の4グループに分割します。

そして、それぞれについて、

取引回数・勝率・総損益・平均損益・PF

を比較します。


Pythonで分析する

まず必要なライブラリを読み込みます。

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

分析結果を保存するため、resultsフォルダがなければ作成します。

os.makedirs(
    "results",
    exist_ok=True
)

BB幅のデータを確認する

まず、

print(
    df["entry_bb_width_pips"].describe()
)

を実行します。

describe()を使うことで、

count
mean
std
min
25%
50%
75%
max

を確認できます。

これによって、零号機が実際にどの程度のBB幅でエントリーしているのかを把握します。

ここで特に重要なのが、

25%
50%
75%

です。

今回は、この境界を利用して4グループに分類します。


分析対象データを作成する

元のDataFrameを直接変更せず、BB幅分析用のDataFrameを作ります。

bb_df = df[
    df["entry_bb_width_pips"].notna()
].copy()

notna()によって、BB幅が記録されている取引だけを抽出します。

そして、

.copy()

によって分析専用のDataFrameを作っています。


BB幅を四分位に分類する

次に、pd.qcut()を使います。

bb_df["bb_quartile_no"] = pd.qcut(
    bb_df["entry_bb_width_pips"],
    q=4,
    labels=False,
    duplicates="drop"
)

q=4なので、取引数がほぼ同じになるように4グループへ分割されます。

ただ、この状態では、

0
1
2
3

となっているため、分かりやすく、

Q1
Q2
Q3
Q4

へ変換します。

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

これで、

Q1 → BB幅が最も狭い25%
Q2 → 次の25%
Q3 → 次の25%
Q4 → BB幅が最も広い25%

という分類が完成しました。


各グループのBB幅を確認する

実際にどの範囲で分割されたのかも確認します。

bb_ranges = (
    bb_df
    .groupby(
        "bb_group",
        observed=True
    )
    ["entry_bb_width_pips"]
    .agg(
        [
            "min",
            "max",
            "mean"
        ]
    )
)

print(bb_ranges)

結果は、

          min     max     mean
bb_group
Q1         ...
Q2         ...
Q3         ...
Q4         ...

という形になります。

これによって、

Q1は○○~○○pips
Q2は○○~○○pips

という具体的な相場環境を確認できます。


BB幅別の取引成績を計算する

ここから本題です。

まず各グループの基本成績を計算します。

bb_summary = (
    bb_df
    .groupby(
        "bb_group",
        observed=True
    )
    .agg(
        trades=(
            "profit",
            "count"
        ),
        total_profit=(
            "profit",
            "sum"
        ),
        average_profit=(
            "profit",
            "mean"
        )
    )
)

続いて勝率です。

win_rate = (
    bb_df
    .assign(
        win=bb_df["profit"] > 0
    )
    .groupby(
        "bb_group",
        observed=True
    )
    ["win"]
    .mean()
    * 100
)

bb_summary["win_rate"] = win_rate

これで、

取引回数
総損益
平均損益
勝率

が計算できました。


PFを計算する

次にPF(Profit Factor)を計算します。

PFは、

総利益 ÷ 総損失の絶対値

です。

まず勝ちトレードだけを集計します。

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

次に負けトレードです。

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

そして、

bb_summary["profit_factor"] = (
    gross_profit
    / gross_loss
)

とします。


表示順を整える

最後に、

bb_summary = (
    bb_summary
    .reindex(
        [
            "Q1",
            "Q2",
            "Q3",
            "Q4"
        ]
    )
)

としてQ1~Q4の順番に並べます。

表示する列も整理します。

bb_summary = bb_summary[
    [
        "trades",
        "win_rate",
        "total_profit",
        "average_profit",
        "profit_factor"
    ]
]

結果を確認します。

print(bb_summary)

さらにCSVとして保存します。

bb_summary.to_csv(
    "results/bb_width_analysis.csv",
    encoding="utf-8-sig"
)

これで、

results/
    bb_width_analysis.csv

が作成されます。


グラフにしてみる

数値だけでは分かりにくいため、PFをグラフ化します。

plt.figure(
    figsize=(8, 5)
)

plt.bar(
    bb_summary.index,
    bb_summary["profit_factor"]
)

plt.axhline(
    y=1.0,
    linestyle="--"
)

plt.xlabel(
    "BB Width Quartile"
)

plt.ylabel(
    "Profit Factor"
)

plt.title(
    "Profit Factor by BB Width"
)

plt.tight_layout()

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

plt.close()

PF=1のところに横線を引いているため、

どのBB幅から利益が出やすくなっているのか

を視覚的に確認できます。


ATRとBB幅は同じものを見ているのか?

ここで、もう一つ調べてみます。

前回分析したATRも、今回分析したBB幅も、ともに相場のボラティリティと関係する指標です。

そこで疑問が生まれます。

「ATRが高いときは、単純にBB幅も広くなっているだけなのではないか?」

もし両者がほぼ同じ動きをしているのであれば、ATRとBB幅を別々に分析する意味は小さくなります。

そこで相関係数を計算します。

atr_bb_df = df[
    [
        "entry_atr14_pips",
        "entry_bb_width_pips"
    ]
].dropna()

そして、

atr_bb_correlation = (
    atr_bb_df[
        "entry_atr14_pips"
    ]
    .corr(
        atr_bb_df[
            "entry_bb_width_pips"
        ]
    )
)

print(
    "ATRとBB幅の相関係数:",
    atr_bb_correlation
)

相関係数は、

+1に近い → 強い正の相関
 0に近い → あまり関係がない
-1に近い → 強い負の相関

という見方ができます。


ATRとBB幅を散布図にする

さらに視覚的にも確認します。

plt.figure(
    figsize=(8, 5)
)

plt.scatter(
    atr_bb_df[
        "entry_atr14_pips"
    ],
    atr_bb_df[
        "entry_bb_width_pips"
    ],
    alpha=0.4
)

plt.xlabel(
    "Entry ATR14 (pips)"
)

plt.ylabel(
    "Entry BB Width (pips)"
)

plt.title(
    "ATR vs BB Width"
)

plt.tight_layout()

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

plt.close()

これで、

results/
├─ bb_width_analysis.csv
├─ bb_width_pf.png
└─ atr_bb_scatter.png

という3つの分析結果ができます。

出力結果

分析結果

BB幅を四分位に分け、それぞれの取引成績を比較した結果がこちらです。

BB幅取引数勝率総損益平均損益PF
Q1(5.546~17.914)43359.35%+18,517.15+42.761.16
Q2(17.986~25.882)43064.19%+40,483.17+94.151.29
Q3(25.893~38.165)43161.72%+73,654.01+170.891.48
Q4(38.219~246.406)43161.95%+80,635.28+187.091.32

まず目につくのが、BB幅が最も狭いQ1の成績の弱さです。

Q1の勝率は59.35%、PFは1.16。

利益自体は出ているものの、4グループの中では最も低いPFとなりました。

一方、BB幅が広くなるにつれて、

Q1:1.16
Q2:1.29
Q3:1.48

とPFが上昇しています。

この結果だけを見ると、零号機はボリンジャーバンドが収縮した相場よりも、ある程度バンドが広がり、値動きが生じている相場を得意としているように見えます。


最もPFが高かったのはQ3

今回特に注目したいのがQ3です。

BB幅25.893~38.165pipsでは、

勝率:61.72%
平均損益:+170.89
PF:1.48

となりました。

興味深いのは、勝率だけならQ2の64.19%の方が高いことです。

それにもかかわらず、PFではQ3が大きく上回っています。

つまり、零号機にとって重要なのは単純に「勝ちやすい相場」ではなく、勝ったときの利益と負けたときの損失を含めて、最終的に利益を残しやすい相場かどうかということになります。

これは前回のATR分析でも見えてきた特徴です。

勝率が高いことと、EAとして効率よく利益を残せることは必ずしも同じではありません。


BB幅は広ければ広いほど良いわけではない

もう一つ興味深いのがQ4です。

BB幅が最も広いQ4では、

総損益:+80,635.28
平均損益:+187.09

となり、どちらも4グループ中で最大でした。

ところがPFを見ると、

Q3:1.48
Q4:1.32

と、Q3より低下しています。

したがって、

「BB幅が広ければ広いほど零号機が強くなる」

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

ある程度BB幅が広がるまでは収益効率が改善するものの、極端に広がった相場ではPFが再び低下しています。

現時点では、

狭すぎる相場は苦手。
しかし、激しく動けば動くほど良いわけでもない。

という零号機の特徴が見えてきました。

なお、Q4だけは38.219~246.406pipsと非常に広い範囲になっています。

今回使用したqcut()は「値の幅」を均等にするのではなく、「取引数」がほぼ均等になるように分類するためです。

Q4には200pipsを超えるような極端な相場も含まれているため、将来的にはQ4の内部をさらに分解してみても面白そうです。


ATRとBB幅には強い関係があった

続いて、前回分析したATRと今回のBB幅との関係も調べました。

その結果、相関係数は、

0.836

となりました。

相関係数は+1に近いほど、2つの数値が同じ方向に動く傾向が強いことを意味します。

0.836という値から、ATRとBB幅にはかなり強い正の相関があることが分かります。

散布図を見ても、その傾向ははっきりしています。

ATRが大きくなるにつれてBB幅も広くなり、逆にATRが小さいところではBB幅も狭くなる傾向があります。

どちらも相場のボラティリティと関係する指標であることを考えれば、納得できる結果です。


前回と今回で「同じ現象」を見ている?

ここで、一つ重要な疑問が生まれました。

前回のATR分析では、ATRが最も低いQ1でPFが低下していました。

そして今回のBB幅分析でも、BB幅が最も狭いQ1のPFが1.16と最低でした。

これだけを見ると、

「低ATRが苦手」
「狭いBBも苦手」

という2つの弱点を発見したように見えます。

しかし、ATRとBB幅の相関係数は0.836です。

これだけ強く関連しているのであれば、実は別々の弱点ではなく、

同じ「低ボラティリティ相場」を、ATRとBB幅という2つの指標から観測しているだけ

という可能性があります。

つまり現時点では、

低ATRだから成績が悪いのか。
BB幅が狭いから成績が悪いのか。
それとも低ボラティリティ相場そのものが苦手なのか。

まだ切り分けることができません。

むしろ、この疑問が生まれたこと自体が今回の分析で得られた大きな成果です。


それでもATRとBB幅は完全に同じではない

一方で、相関係数は0.836であって1.0ではありません。

散布図を見ても完全な一直線ではなく、同じようなATRでもBB幅にはばらつきがあります。

つまり、ATRとBB幅には強い関連があるものの、完全に同じ情報を持っているわけではありません。

例えば、

ATRは低いが、BB幅は比較的広い相場

あるいは、

ATRは高いが、BB幅は比較的狭い相場

では、零号機の成績はどうなるのでしょうか。

ここを調べることで、単独の指標だけを見ていては分からなかった零号機の特徴が見えてくるかもしれません。


今回のまとめ

今回は、エントリー時のBB幅と零号機の取引成績の関係を分析しました。

BB幅別のPFは、

Q1:1.16
Q2:1.29
Q3:1.48
Q4:1.32

となり、BB幅が狭いQ1で収益効率が最も低く、Q3で最も高くなりました。

一方で、最もBB幅の広いQ4ではPFが再び低下しています。

この結果から、

零号機は低ボラティリティ相場を苦手としている可能性があるものの、ボラティリティが高ければ高いほど強いわけではない

という仮説が見えてきました。

さらにATRとBB幅の相関係数を調べると、

0.836

という強い正の相関が確認されました。

前回発見した「低ATRでの成績低下」と、今回発見した「狭いBB幅での成績低下」。

これは別々の弱点ではなく、同じ低ボラティリティ環境を異なる角度から見ているだけなのかもしれません。

では、ATRとBB幅を組み合わせたらどうなるのでしょうか。


次回|ATR × BB幅 ― 16種類の相場環境へ

これまで、

第15回ではATR

第16回ではBB幅

をそれぞれ単独で分析してきました。

次回は、この2つを初めて組み合わせます。

ATRをQ1~Q4、BB幅もQ1~Q4に分類すると、理論上は、

4 × 4 = 16種類

の相場環境を作ることができます。

例えば、

ATR Q1 × BB Q1
ATR Q1 × BB Q2
ATR Q1 × BB Q3
ATR Q1 × BB Q4

……

ATR Q4 × BB Q4

です。

そして、それぞれについて、

取引数・勝率・平均損益・PF

を比較します。

今回の分析によって、次に確認したいことも明確になりました。

低ATRだから弱いのか?

BB幅が狭いから弱いのか?

それとも「低ATR × 狭いBB」という組み合わせが弱いのか?

ここを切り分けていきます。

ただし、ATRとBB幅には0.836という強い相関があります。

そのため、16種類すべてに同じくらいの取引が存在するとは限りません。

例えば、

「ATRは最低水準なのに、BB幅は最大水準」

という相場は、ほとんど存在しない可能性があります。

そこで次回はPFだけを見るのではなく、各グループの取引数も必ず確認します。

取引数が極端に少ないグループで高いPFが出ても、それだけで「零号機が得意な相場」と判断することはできないからです。

単一の特徴量を見る段階から、複数の特徴量の関係を見る段階へ進めます。

今回のコード全文

コード全文
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
)


# ==========================================
# BB幅の基本統計
# ==========================================

print(
    "\nBB幅の基本統計"
)

print(
    df["entry_bb_width_pips"].describe()
)


# ==========================================
# 分析対象データ作成
# ==========================================

bb_df = df[
    df["entry_bb_width_pips"].notna()
].copy()


print(
    "\nBB幅分析対象件数:",
    len(bb_df)
)


# ==========================================
# BB幅を四分位に分類
# ==========================================

bb_df["bb_quartile_no"] = pd.qcut(
    bb_df["entry_bb_width_pips"],
    q=4,
    labels=False,
    duplicates="drop"
)


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


# ==========================================
# 各グループのBB幅
# ==========================================

bb_ranges = (
    bb_df
    .groupby(
        "bb_group",
        observed=True
    )
    ["entry_bb_width_pips"]
    .agg(
        [
            "min",
            "max",
            "mean"
        ]
    )
)


bb_ranges = bb_ranges.reindex(
    [
        "Q1",
        "Q2",
        "Q3",
        "Q4"
    ]
)


print(
    "\nBB幅の範囲"
)

print(
    bb_ranges
)


# ==========================================
# BB幅別成績
# ==========================================

bb_summary = (
    bb_df
    .groupby(
        "bb_group",
        observed=True
    )
    .agg(
        trades=(
            "profit",
            "count"
        ),
        total_profit=(
            "profit",
            "sum"
        ),
        average_profit=(
            "profit",
            "mean"
        )
    )
)


# ==========================================
# 勝率
# ==========================================

win_rate = (
    bb_df
    .assign(
        win=bb_df["profit"] > 0
    )
    .groupby(
        "bb_group",
        observed=True
    )
    ["win"]
    .mean()
    * 100
)


bb_summary["win_rate"] = win_rate


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

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


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

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


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

bb_summary["profit_factor"] = (
    gross_profit
    / gross_loss
)


# ==========================================
# Q1~Q4の順番に並べる
# ==========================================

bb_summary = (
    bb_summary
    .reindex(
        [
            "Q1",
            "Q2",
            "Q3",
            "Q4"
        ]
    )
)


# ==========================================
# 列順を整理
# ==========================================

bb_summary = bb_summary[
    [
        "trades",
        "win_rate",
        "total_profit",
        "average_profit",
        "profit_factor"
    ]
]


print(
    "\nBB幅別取引成績"
)

print(
    bb_summary
)


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

bb_summary.to_csv(
    "results/bb_width_analysis.csv",
    encoding="utf-8-sig"
)


bb_ranges.to_csv(
    "results/bb_width_ranges.csv",
    encoding="utf-8-sig"
)


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

plt.figure(
    figsize=(8, 5)
)


plt.bar(
    bb_summary.index,
    bb_summary["profit_factor"]
)


plt.axhline(
    y=1.0,
    linestyle="--"
)


plt.xlabel(
    "BB Width Quartile"
)


plt.ylabel(
    "Profit Factor"
)


plt.title(
    "Profit Factor by BB Width"
)


plt.tight_layout()


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


plt.close()


# ==========================================
# ATRとBB幅の関係
# ==========================================

atr_bb_df = df[
    [
        "entry_atr14_pips",
        "entry_bb_width_pips"
    ]
].dropna()


atr_bb_correlation = (
    atr_bb_df[
        "entry_atr14_pips"
    ]
    .corr(
        atr_bb_df[
            "entry_bb_width_pips"
        ]
    )
)


print(
    "\nATRとBB幅の相関係数:",
    atr_bb_correlation
)


# ==========================================
# ATR × BB幅 散布図
# ==========================================

plt.figure(
    figsize=(8, 5)
)


plt.scatter(
    atr_bb_df[
        "entry_atr14_pips"
    ],
    atr_bb_df[
        "entry_bb_width_pips"
    ],
    alpha=0.4
)


plt.xlabel(
    "Entry ATR14 (pips)"
)


plt.ylabel(
    "Entry BB Width (pips)"
)


plt.title(
    "ATR vs BB Width"
)


plt.tight_layout()


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


plt.close()


# ==========================================
# 完了表示
# ==========================================

print(
    "\nBB幅分析が完了しました。"
)

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

print(
    "results/bb_width_analysis.csv"
)

print(
    "results/bb_width_ranges.csv"
)

print(
    "results/bb_width_pf.png"
)

print(
    "results/atr_bb_scatter.png"
)

関連記事

ランダムウォークへの挑戦⑯|ボリンジャーバンドの幅で零号機の得意な相場を探る(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をコピーしました