FX / EA Python

FXのボリンジャーバンドでおすすめの設定は?期間・σを5通貨で徹底検証

本ページにはプロモーションが含まれています。

FXでボリンジャーバンドを使う際、

  • 期間は20でいいの?
  • ±2σが本当におすすめなの?
  • 通貨によって設定は変えた方がいいの?

と疑問に思う方も多いのではないでしょうか。

ボリンジャーバンドでは、期間20・±2σが定番設定としてよく使われます。

ただし、すべての通貨や売買方向でこの設定が最適とは限りません。

そこで本記事では、USDJPY・EURJPY・GBPJPY・EURUSD・AUDUSDの5通貨を対象に、15分足を使ってボリンジャーバンドの設定を検証しました。

検証するのは、

  • 期間11種類
  • σ 5種類
  • TP/SL 10種類
  • LONG・SHORT

を組み合わせた合計5,500ケースです。

なお、5,500ケースには条件上シグナルが発生せず、取引回数が0となったケースも含まれます。

単純に合計pipsだけを見るのではなく、PF・最大ドローダウン・平均損益・取引回数なども確認しながら、成績が良かった設定を比較します。

この記事を読めば、期間20・±2σが本当に使いやすい設定なのか、通貨やLONG・SHORTによってどのような違いがあるのかが分かります。

先に結果を確認したい方は、「5通貨のボリンジャーバンド検証結果」からご覧ください。

今回のバックテストでは、GMOコインFX APIから取得した15分足のCSVデータを使用しています。

ローソク足データの取得方法は、以下の記事で詳しく紹介しています。

また、今回使用したバックテストスクリプトは以下です

run_bollinger_band_backtest.py
"""5通貨・LONG/SHORTでボリンジャーバンド逆張りを一括検証する。

終値がいったんバンドの外へ出た後、バンド内へ戻った足でシグナルを確定する。
次の15分足の始値で約定し、固定pipsのTPまたはSLで決済する。
"""

import csv
import math
import time
from collections import deque
from datetime import date, datetime, timedelta
from pathlib import Path


# 使い方:
# 1. fetch_gmo_candles.pyで、5通貨の15分足を取得します。
# 2. GMO-APIフォルダで次のコマンドを実行します。
#    python backtest_research/bollinger_band/run_bollinger_band_backtest.py
# 3. 結果は、このファイルと同じフォルダにあるresultsへ保存されます。

# ボリンジャーバンドの計算、シグナル判定、売買には15分足だけを使用します。
ENTRY_TIMEFRAME = "15min"
ENTRY_TIMEFRAME_DURATION = timedelta(minutes=15)

# 既存のローソク足CSVに合わせ、ASK価格を読み込みます。
# BID価格、スプレッド、手数料、スリッページは考慮しません。
PRICE_TYPE = "ASK"

# CSV内のtimestamp_jstの日付が、この開始日から終了日までの足を検証します。
# 開始日と終了日のローソク足も検証対象に含みます。
BACKTEST_START_DATE = date(2024, 1, 1)
BACKTEST_END_DATE = date(2026, 7, 31)

# 検証する5つの通貨ペアと、買い・売りの2方向です。
SYMBOLS = ("USD_JPY", "EUR_JPY", "GBP_JPY", "EUR_USD", "AUD_USD")
SIDES = ("LONG", "SHORT")

# 「期間」は移動平均と標準偏差を計算するローソク足の本数です。
# 「σ」は移動平均から上下バンドまでの標準偏差の倍率です。
BB_PERIODS = (5, 10, 15, 20, 21, 25, 30, 40, 50, 75, 100)
BB_SIGMAS = (1.0, 1.5, 2.0, 2.5, 3.0)
# 検証開始時点から最長期間のバンドを計算できるよう、開始前の足を使います。
WARMUP_BARS = max(BB_PERIODS)
# 各要素は「(利益確定幅, 損切り幅)」で、単位はpipsです。
# 例:(30, 10)なら、30pipsの利益または10pipsの損失で決済します。
TP_SL_PAIRS = (
    (20, 10),
    (30, 10),
    (30, 20),
    (50, 10),
    (50, 20),
    (50, 30),
    (80, 20),
    (80, 30),
    (100, 30),
    (100, 50),
)

# __file__を基準にするため、どのフォルダから実行しても入出力先は同じです。
SCRIPT_DIR = Path(__file__).resolve().parent
GMO_API_DIR = SCRIPT_DIR.parents[1]
DEFAULT_DATA_DIR = GMO_API_DIR / "data" / "candles"
DEFAULT_OUTPUT_DIR = SCRIPT_DIR / "results"

# 期間・σ・TP/SLごとの集計結果へ保存する列と、その並び順です。
RESULT_COLUMNS = (
    "通貨ペア", "売買方向", "期間", "σ", "TP(pips)", "SL(pips)",
    "取引回数", "勝ち取引数", "負け取引数", "勝率(%)",
    "合計損益(pips)", "平均損益(pips)", "プロフィットファクター",
    "最大ドローダウン(pips)", "平均保有時間(分)", "最終足決済数",
)


def load_candles(path):
    """検証開始前のウォームアップ足と検証期間内のOHLCを読み込む。"""
    # deque(maxlen=100)は、新しい足を追加すると古い足を自動的に捨てます。
    # そのため、検証開始前の全履歴ではなく直近100本だけを保持できます。
    warmup = deque(maxlen=WARMUP_BARS)
    backtest_candles = []
    # utf-8-sigを使うと、Excel向けのBOM付きCSVも読み込めます。
    with path.open("r", encoding="utf-8-sig", newline="") as file:
        for row in csv.DictReader(file):
            timestamp = datetime.fromisoformat(row["timestamp_jst"])
            # 終了日より後の足は指標計算にも売買にも使用しません。
            if timestamp.date() > BACKTEST_END_DATE:
                continue
            # CSVの文字列を、日時と計算可能なfloatへ変換します。
            candle = {
                "open_time": timestamp,
                "close_time": timestamp + ENTRY_TIMEFRAME_DURATION,
                "open": float(row["open"]),
                "high": float(row["high"]),
                "low": float(row["low"]),
                "close": float(row["close"]),
            }
            if timestamp.date() < BACKTEST_START_DATE:
                # 全履歴ではなく、指標計算に必要な直近本数だけを保持します。
                warmup.append(candle)
            else:
                backtest_candles.append(candle)
    return list(warmup) + backtest_candles, len(warmup), len(backtest_candles)


def calculate_bollinger_bands(closes, period, sigma):
    """終値のSMAと母標準偏差(ddof=0)から上下バンドを計算する。"""
    if period < 2:
        raise ValueError("ボリンジャーバンド期間は2以上にしてください")
    if sigma <= 0:
        raise ValueError("σは0より大きくしてください")

    # 必要本数が揃う前は計算できないため、結果をいったんNoneで埋めます。
    middle = [None] * len(closes)
    upper = [None] * len(closes)
    lower = [None] * len(closes)
    window = deque()
    window_sum = 0.0
    window_sum_squares = 0.0

    # 終値の合計と二乗和を更新しながら計算すると、各位置で期間分を
    # 毎回足し直すより高速に全設定を検証できます。
    for index, value in enumerate(closes):
        window.append(value)
        window_sum += value
        window_sum_squares += value * value
        if len(window) > period:
            removed = window.popleft()
            window_sum -= removed
            window_sum_squares -= removed * removed
        if len(window) < period:
            continue

        # middleは単純移動平均線(SMA)です。
        mean = window_sum / period
        # 丸め誤差でごく小さい負数になる場合があるため0を下限にします。
        variance = max(window_sum_squares / period - mean * mean, 0.0)
        standard_deviation = math.sqrt(variance)
        # 上側バンド = SMA + σ × 標準偏差
        # 下側バンド = SMA - σ × 標準偏差
        middle[index] = mean
        upper[index] = mean + sigma * standard_deviation
        lower[index] = mean - sigma * standard_deviation

    return middle, upper, lower


def make_trade(position, exit_time, exit_price, reason, side, pip_size):
    """決済した1取引の保有時間、pips損益、決済理由を作る。"""
    # LONGは値上がり、SHORTは値下がりしたときに損益をプラスにします。
    direction = 1.0 if side == "LONG" else -1.0
    return {
        # 日時差なので、土日やデータ欠損をまたぐ時間も含みます。
        "保有時間(分)": (exit_time - position["time"]).total_seconds() / 60,
        # 円通貨は1pips=0.01円、それ以外は1pips=0.0001で換算します。
        "損益(pips)": direction * (exit_price - position["price"]) / pip_size,
        "決済理由": reason,
    }


def collect_trades(
    candles, upper, lower, side, tp_pips, sl_pips, pip_size,
    entry_start_index,
):
    """バンド外から内側へ復帰した次足で入り、固定TP/SLで決済する。"""
    # positionがNoneならポジションを持っていません。
    # pending_entryは、現在足で確定した注文を次足始値で実行する予約です。
    position = None
    pending_entry = False
    trades = []

    # 前足と比較するため、2本目(index=1)から順番に処理します。
    for index in range(1, len(candles)):
        # 前足の終値で復帰を確認した注文を、現在足の始値で約定させます。
        if (
            position is None
            and pending_entry
            and index >= entry_start_index
        ):
            # シグナル足の終値ではなく次足始値を使い、未来参照を防ぎます。
            position = {
                "time": candles[index]["open_time"],
                "price": candles[index]["open"],
            }
        pending_entry = False

        # ポジション保有中は、現在足の高値・安値でTP/SL到達を確認します。
        if position is not None:
            tp_distance = tp_pips * pip_size
            sl_distance = sl_pips * pip_size
            if side == "LONG":
                # LONGは上昇でTP、下落でSLへ到達します。
                tp_price = position["price"] + tp_distance
                sl_price = position["price"] - sl_distance
                tp_hit = candles[index]["high"] >= tp_price
                sl_hit = candles[index]["low"] <= sl_price
            else:
                # SHORTは下落でTP、上昇でSLへ到達します。
                tp_price = position["price"] - tp_distance
                sl_price = position["price"] + sl_distance
                tp_hit = candles[index]["low"] <= tp_price
                sl_hit = candles[index]["high"] >= sl_price

            # 15分足内の到達順を判別できない場合は、保守的にSLを優先します。
            if sl_hit:
                exit_price, reason = sl_price, "SL"
            elif tp_hit:
                exit_price, reason = tp_price, "TP"
            else:
                exit_price = reason = None
            if exit_price is not None:
                trades.append(make_trade(
                    position, candles[index]["close_time"], exit_price,
                    reason, side, pip_size,
                ))
                position = None

        # 前足と現在足の終値・バンドを比較し、バンド内へ戻った瞬間を探します。
        current_close = candles[index]["close"]
        previous_close = candles[index - 1]["close"]
        if side == "LONG":
            current_band = lower[index]
            previous_band = lower[index - 1]
            signal = (
                previous_band is not None
                and current_band is not None
                and previous_close < previous_band
                and current_close >= current_band
            )
        else:
            current_band = upper[index]
            previous_band = upper[index - 1]
            signal = (
                previous_band is not None
                and current_band is not None
                and previous_close > previous_band
                and current_close <= current_band
            )
        # ポジションがないときだけ、次足の注文を予約します。
        if position is None and signal:
            pending_entry = True

    # データ終了時にポジションが残っていれば、最終足終値で強制決済します。
    if position is not None:
        trades.append(make_trade(
            position, candles[-1]["close_time"], candles[-1]["close"],
            "データ終了", side, pip_size,
        ))
    return trades


def summarize(symbol, side, period, sigma, tp, sl, trades):
    """取引ごとのpips損益から、1設定の成績をまとめる。"""
    profits = [trade["損益(pips)"] for trade in trades]
    # 0pipsは勝ちにも負けにも含めませんが、取引回数には含めます。
    wins = [value for value in profits if value > 0]
    losses = [value for value in profits if value < 0]
    # 累積損益の過去最高値から、最大で何pips下落したかを計算します。
    equity = peak = max_drawdown = 0.0
    for profit in profits:
        equity += profit
        peak = max(peak, equity)
        max_drawdown = max(max_drawdown, peak - equity)
    count = len(profits)
    # PF(プロフィットファクター)= 総利益 ÷ 総損失です。
    gross_loss = abs(sum(losses))
    durations = [trade["保有時間(分)"] for trade in trades]
    return {
        "通貨ペア": symbol,
        "売買方向": side,
        "期間": period,
        "σ": sigma,
        "TP(pips)": tp,
        "SL(pips)": sl,
        "取引回数": count,
        "勝ち取引数": len(wins),
        "負け取引数": len(losses),
        "勝率(%)": round(len(wins) / count * 100, 2) if count else 0.0,
        "合計損益(pips)": round(sum(profits), 2),
        "平均損益(pips)": round(sum(profits) / count, 2) if count else 0.0,
        "プロフィットファクター": round(sum(wins) / gross_loss, 4) if gross_loss else "",
        "最大ドローダウン(pips)": round(max_drawdown, 2),
        "平均保有時間(分)": round(sum(durations) / count, 2) if count else 0.0,
        "最終足決済数": sum(trade["決済理由"] == "データ終了" for trade in trades),
    }


def write_csv(path, rows):
    """集計結果をExcelでも開きやすいUTF-8 BOM付きCSVへ保存する。"""
    # resultsや通貨・方向別フォルダがなければ自動的に作成します。
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8-sig", newline="") as file:
        writer = csv.DictWriter(file, fieldnames=RESULT_COLUMNS)
        writer.writeheader()
        writer.writerows(rows)


def run_experiment(candles, symbol, side, entry_start_index, output_dir):
    """1通貨・1売買方向の期間・σ・TP/SL候補を検証する。"""
    # 同じ期間・σのバンドをTP/SLごとに再計算しないよう先に作ります。
    closes = [candle["close"] for candle in candles]
    indicators = {
        (period, sigma): calculate_bollinger_bands(closes, period, sigma)[1:]
        for period in BB_PERIODS
        for sigma in BB_SIGMAS
    }
    # USDJPY・EURJPY・GBPJPYは1pips=0.01円、それ以外は0.0001です。
    pip_size = 0.01 if symbol.endswith("_JPY") else 0.0001
    rows = []
    for (period, sigma), (upper, lower) in indicators.items():
        for tp, sl in TP_SL_PAIRS:
            trades = collect_trades(
                candles, upper, lower, side, tp, sl, pip_size,
                entry_start_index,
            )
            rows.append(summarize(
                symbol, side, period, sigma, tp, sl, trades,
            ))
    # 1通貨・1方向の550ケースを1つのresult.csvへ保存します。
    write_csv(output_dir / symbol / side / "result.csv", rows)
    return rows


def main():
    """全5,500ケースを実行し、通貨別結果と全件結果を保存する。"""
    # スクリプト全体の実行時間を測定します。
    started = time.perf_counter()
    if BACKTEST_START_DATE > BACKTEST_END_DATE:
        raise SystemExit("検証開始日は検証終了日以前に設定してください")
    # 設定数から実際のケース数を計算するため、候補を変えても表示が追従します。
    total_cases = (
        len(BB_PERIODS) * len(BB_SIGMAS) * len(TP_SL_PAIRS)
        * len(SYMBOLS) * len(SIDES)
    )
    print(
        f"期間{len(BB_PERIODS)}種類 × σ {len(BB_SIGMAS)}種類 "
        f"× TP/SL {len(TP_SL_PAIRS)}組 × 5通貨 × 2方向 "
        f"= {total_cases}ケース"
    )
    print(f"検証期間: {BACKTEST_START_DATE} ~ {BACKTEST_END_DATE}")

    # 同じ通貨のCSVをLONGとSHORTで2回読まないよう、先に1回だけ読み込みます。
    candles_by_symbol = {}
    entry_start_indexes = {}
    for symbol in SYMBOLS:
        path = DEFAULT_DATA_DIR / symbol / f"{ENTRY_TIMEFRAME}_{PRICE_TYPE}.csv"
        if not path.is_file():
            raise SystemExit(f"入力CSVが見つかりません: {path}")
        candles, warmup_count, backtest_count = load_candles(path)
        if backtest_count == 0:
            raise SystemExit(f"{symbol}の検証期間内データがありません")
        if warmup_count >= WARMUP_BARS:
            # 検証開始前の最後の足で確定したシグナルは、検証期間の最初の足から
            # エントリーできます。
            entry_start_index = warmup_count
            warmup_description = f"検証開始前{warmup_count}本"
        else:
            # CSVに検証開始前の足がない場合は、検証期間内の先頭100本を全設定で
            # 共通のウォームアップにします。短い期間だけ先に取引しないためです。
            if len(candles) <= WARMUP_BARS:
                raise SystemExit(
                    f"{symbol}のデータが不足しています: "
                    f"{len(candles)}本(ウォームアップに必要{WARMUP_BARS}本)"
                )
            entry_start_index = WARMUP_BARS
            warmup_description = f"CSV先頭{WARMUP_BARS}本"
            first_entry_time = candles[entry_start_index]["open_time"]
            print(
                f"[WARN] {symbol}: 検証開始前の足が{warmup_count}本のため、"
                f"{warmup_description}をウォームアップに使用します。"
                f"エントリー開始: {first_entry_time.isoformat()}"
            )
        candles_by_symbol[symbol] = candles
        entry_start_indexes[symbol] = entry_start_index
        print(
            f"[LOAD] {symbol}: 検証{backtest_count}本 "
            f"/ ウォームアップ: {warmup_description}"
        )

    # 通貨・方向別に検証しながら、全結果もall_rowsへ集めます。
    all_rows = []
    cases_per_experiment = len(BB_PERIODS) * len(BB_SIGMAS) * len(TP_SL_PAIRS)
    for symbol in SYMBOLS:
        for side in SIDES:
            print(f"[RUN] {symbol} {side}: {cases_per_experiment}ケース")
            all_rows.extend(run_experiment(
                candles_by_symbol[symbol], symbol, side,
                entry_start_indexes[symbol], DEFAULT_OUTPUT_DIR,
            ))

    # 個別CSVに加え、5,500ケースを一括比較できるCSVも保存します。
    write_csv(DEFAULT_OUTPUT_DIR / "all_results.csv", all_rows)
    print(f"完了: {DEFAULT_OUTPUT_DIR}")
    print(f"実行時間: {time.perf_counter() - started:.2f}秒")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

同じようにボリンジャーバンドの設定を検証してみたい方は、参考にしてみてください。

FXのボリンジャーバンドとは

ボリンジャーバンドとは、移動平均線を中心に、価格のばらつきを帯(バンド)で表示するテクニカル指標です。

一般的には、移動平均線の上下に±1σ・±2σ・±3σを表示し、価格が平均からどの程度離れているかや、相場の値動きの大きさを確認するために使われます。

ボリンジャーバンドで分かること

ボリンジャーバンドでは、主に以下のようなことを確認できます。

  • 価格が移動平均線からどれくらい離れているか
  • 値動きが大きくなっているか、小さくなっているか
  • トレンドが強くなっているか

価格だけでなく、バンドの幅を見ることで相場のボラティリティも判断しやすくなります。

移動平均線と±1σ・±2σ・±3σの見方

ボリンジャーバンドは、中央の移動平均線と、その上下にあるσ(シグマ)のラインで構成されます。

  • 中央線:移動平均線
  • ±1σ:標準偏差1つ分
  • ±2σ:標準偏差2つ分
  • ±3σ:標準偏差3つ分

価格が±2σや±3σに近づくほど、移動平均線から大きく離れている状態です。

ただし、±2σに到達したから必ず反転するわけではありません。

強いトレンドでは、±2σ付近を維持しながら価格が動き続けることもあります。

スクイーズ・エクスパンション・バンドウォークとは

ボリンジャーバンドでは、バンドの形にも特徴があります。

  • スクイーズ:バンド幅が狭くなった状態
  • エクスパンション:バンド幅が広がった状態
  • バンドウォーク:価格が±2σ付近に沿って動く状態

スクイーズは値動きが小さい状態、エクスパンションは値動きが大きくなっている状態を表します。

また、強いトレンドではバンドウォークが発生することがあるため、バンドへの到達だけで逆張りを判断しないことが重要です。

今回のボリンジャーバンド検証条件

ここからは、ボリンジャーバンドの期間やσによって成績がどのように変わるのか、実際の15分足データを使って検証します。

今回は、5通貨・LONG/SHORT・11種類の期間・5種類のσ・10種類のTP/SLを組み合わせてバックテストします。

検証する5通貨・時間足・期間

今回検証する通貨ペアは、以下の5つです。

  • USDJPY
  • EURJPY
  • GBPJPY
  • EURUSD
  • AUDUSD

すべて15分足を使用し、検証期間は2024年1月1日〜2026年7月31日とします。

価格データにはASKを使用し、スプレッドやスリッページなどの取引コストは今回の検証には含めません。

また、検証開始時点から最長期間のボリンジャーバンドを計算できるように、2024年1月1日より前のローソク足を最大100本、計算用データとして使用します。

検証するボリンジャーバンドの期間・σ

ボリンジャーバンドの期間は、以下の11種類を比較します。

  • 5・10・15・20・21・25・30・40・50・75・100

σは、以下の5種類です。

  • 1.0σ
  • 1.5σ
  • 2.0σ
  • 2.5σ
  • 3.0σ

定番の期間20・2σだけでなく、短い期間から長い期間まで幅広く比較します。

検証するTP・SL

エントリー後の決済には、固定TP・SLを使用します。

検証する組み合わせは以下の10種類です。

TPSL
20pips10pips
30pips10pips
30pips20pips
50pips10pips
50pips20pips
50pips30pips
80pips20pips
80pips30pips
100pips30pips
100pips50pips

ボリンジャーバンドの設定だけでなく、TP・SLとの組み合わせによって成績がどのように変わるのかも確認します。

LONG・SHORTのエントリー条件

今回は、価格がボリンジャーバンドの外側に出たあと、再びバンド内へ戻ったタイミングをシグナルとします。

LONGのエントリー条件

  • 前の15分足終値が下側バンドより下
  • 現在の15分足終値が下側バンド以上まで戻る
  • 次の15分足の始値でLONG

SHORTのエントリー条件

  • 前の15分足終値が上側バンドより上
  • 現在の15分足終値が上側バンド以下まで戻る
  • 次の15分足の始値でSHORT

シグナルが確定した足ではなく、次の15分足始値でエントリーすることで未来参照を避けています。

TP・SLによる決済条件

エントリー後は、あらかじめ設定したTP・SLのどちらかに到達するまでポジションを保有します。

LONGでは、

  • 価格がTPまで上昇 → 利確
  • 価格がSLまで下落 → 損切り

SHORTでは、

  • 価格がTPまで下落 → 利確
  • 価格がSLまで上昇 → 損切り

とします。

同じ15分足の中でTPとSLの両方に到達した場合は、どちらが先だったのか判断できないため、SLを優先します。

また、検証期間終了時にポジションが残っている場合は、最終足の終値で決済します。

検証する組み合わせ数

今回の組み合わせは、

  • 5通貨 × LONG・SHORT × 期間11種類 × σ5種類 × TP/SL10種類

です。

つまり、

  • 5 × 2 × 11 × 5 × 10 = 5,500ケース

を検証します。

なお、期間とσの組み合わせによってはシグナルが発生せず、取引回数が0となるケースもあります。

成績が良かった設定の選定基準

5,500ケースの結果は、合計pipsだけでは判断しません。

主に、

  • 合計損益(pips)
  • プロフィットファクター(PF)
  • 最大ドローダウン
  • 1取引あたりの平均損益
  • 取引回数

を確認しながら比較します。

利益が大きくても最大ドローダウンが大きすぎる設定や、取引回数が少なすぎる設定は慎重に判断します。

この結果をもとに、次の章では5通貨それぞれで成績が良かったボリンジャーバンド設定を確認していきます。

5通貨のボリンジャーバンド検証結果

ここからは、5,500ケースのバックテスト結果から、成績が良かったボリンジャーバンド設定を通貨別に確認します。

今回はLONG・SHORTを分けて評価し、

  • 取引回数100回以上
  • 合計pipsがプラス
  • PF1.05以上

を満たした設定を対象とします。

そのうえで、PFの高い順に並べ、合計pips・最大ドローダウン・平均損益もあわせて確認します。

なお、今回の結果にはスプレッド・手数料・スリッページなどの取引コストは含めていません。

特に1取引あたりの平均損益が小さい設定は、取引コストを加味すると成績が大きく悪化する可能性があります。

また、基準を満たす設定が5つ未満の場合は、無理にTOP5を作らず、基準を満たした設定のみ掲載します。

USDJPYで成績が良かったボリンジャーバンド設定

USDJPY LONG:TOP5

順位期間σTP/SL取引回数合計pipsPF最大DD
1213.050/10362+1,4201.5108180
2403.080/20363+2,5401.4792460
3203.050/10330+1,2001.4706250
4403.050/10392+1,3601.4474250
5503.0100/30296+2,6901.4332370

1位は期間21・3σ・TP50・SL10でした。

362回の取引で+1,420pips、PF1.5108、最大DD180pipsとなっています。

また、PF上位5設定はすべて3σでした。

少なくともPF上位では、2σより外側まで価格が動いたあとにバンド内へ戻る設定が目立つ結果となっています。

USDJPY SHORT:基準を満たした4設定

順位期間σTP/SL取引回数合計pipsPF最大DD
1152.030/101,912+9201.0652670
2102.030/101,918+8201.0578730
3152.050/101,779+8101.0551890
4102.020/101,994+7001.0536560

USDJPY SHORTでは、基準を満たしたのは4設定だけでした。

1位は期間15・2σ・TP30・SL10ですが、PFは1.0652とLONG上位より低く、1取引あたりの平均損益も小さい結果です。

今回の検証期間・ルールでは、USDJPYはSHORTよりLONGの方が相性が良い結果となりました。

特にSHORTは取引回数が多いため、スプレッドなどの取引コストを加味した場合は注意が必要です。

EURJPYで成績が良かったボリンジャーバンド設定

EURJPY LONG:TOP5

順位期間σTP/SL取引回数合計pipsPF最大DD
1153.080/20200+1,2001.4054380
2203.050/10351+1,1101.4051290
3203.080/20335+2,0001.4032340
4403.050/10393+1,1101.3592240
5403.080/30326+2,2101.33951,170

1位は期間15・3σ・TP80・SL20です。

200回の取引で+1,200pips、PF1.4054となりました。

また、PF上位5設定はすべてです。

期間は15・20・40と分かれていますが、PF上位では3σの設定が目立つ結果となりました。

EURJPY SHORT:基準を満たした3設定

順位期間σTP/SL取引回数合計pipsPF最大DD
1303.050/10315+2701.1047250
2303.030/10318+1801.0769250
3253.030/10298+1401.0636260

EURJPY SHORTでは、基準を満たしたのは3設定だけでした。

1位は期間30・3σ・TP50・SL10で、315回の取引で+270pips、PF1.1047です。

LONGと比べると基準を満たす設定が少なく、PFや合計pipsも低くなっています。

そのため、今回の検証期間全体では、EURJPYはLONG側の方が良い結果となりました。

GBPJPYで成績が良かったボリンジャーバンド設定

GBPJPY LONG:TOP5

順位期間σTP/SL取引回数合計pipsPF最大DD
11003.050/20223+1,5601.5693230
21003.050/30211+1,8301.5596300
31003.0100/50184+2,5001.4717500
4403.050/10379+1,3701.4676190
51003.050/10238+8601.4674240

1位は期間100・3σ・TP50・SL20でした。

223回の取引で+1,560pips、PF1.5693、最大DD230pipsとなっています。

このPF1.5693は、今回基準を満たした設定の中で最も高い値です。

また、TOP5のうち4設定が期間100・3σとなっており、GBPJPY LONGでは長めの期間がPF上位に多く入りました。

GBPJPY SHORT:TOP5

順位期間σTP/SL取引回数合計pipsPF最大DD
1102.550/30364+1,1601.1815320
2102.580/20369+9201.1608580
3102.580/30350+1,1601.1585680
4102.5100/30338+1,0401.1376810
5153.080/30134+3801.1348470

GBPJPY SHORTでは、USDJPY・EURJPYよりも多くの設定が基準を満たしました。

1位は期間10・2.5σ・TP50・SL30で、364回の取引で+1,160pips、PF1.1815です。

TOP4はすべて期間10・2.5σとなっており、LONGの期間100・3σとは大きく異なります。

今回の結果からも、LONGとSHORTは別々に設定を検証する必要があることが分かります。

EURUSDで成績が良かったボリンジャーバンド設定

EURUSD LONG:TOP5

順位期間σTP/SL取引回数合計pipsPF最大DD
1153.030/20176+630.01.3387200
21002.5100/50162+1,574.91.3247600
3153.030/10182+340.01.2656150
4753.030/10365+670.01.2607170
51003.030/10266+460.01.2447160

1位は期間15・3σ・TP30・SL20でした。

176回の取引で+630pips、PF1.3387、最大DD200pipsです。

一方、2位の期間100・2.5σ・TP100・SL50は、PFでは1位を下回るものの、合計+1,574.9pipsとなっています。

EURUSD LONGでは、期間15だけでなく75・100といった長い期間もPF上位に入りました。

EURUSD SHORT:TOP5

順位期間σTP/SL取引回数合計pipsPF最大DD
1213.050/10295+1,0701.4693180
2213.030/10304+8801.4272160
3102.530/10420+1,1601.4056150
4102.550/10395+1,2101.3916280
5153.030/10184+4401.3465150

EURUSD SHORTでは、複数の設定が高いPFとなりました。

1位は期間21・3σ・TP50・SL10で、295回の取引で+1,070pips、PF1.4693、最大DD180pipsです。

また、期間10・2.5σも3位・4位に入っています。

今回の検証では、JPY系のUSDJPY・EURJPYとは異なり、EURUSDではSHORT側にもPFの高い設定が複数見つかりました。

AUDUSDで成績が良かったボリンジャーバンド設定

AUDUSD LONG:TOP5

順位期間σTP/SL取引回数合計pipsPF最大DD
11003.030/20207+749.51.3438280
21003.030/10252+429.51.2413170
31003.0100/30131+592.81.2058410
41003.050/30158+509.51.1846330
51003.080/30139+499.51.1734300

1位は期間100・3σ・TP30・SL20です。

207回の取引で+749.5pips、PF1.3438、最大DD280pipsとなりました。

AUDUSD LONGでは、PF上位5設定がすべて期間100・3σです。

少なくとも今回のPF上位では、短い期間よりも長い期間を使った設定が目立つ結果となりました。

AUDUSD SHORT:TOP5

順位期間σTP/SL取引回数合計pipsPF最大DD
1203.050/30197+1,155.51.3561340
2253.050/30215+1,255.51.3542210
3213.050/30207+1,175.51.3433220
4203.050/10253+650.01.3250310
5153.050/30150+805.51.3229480

AUDUSD SHORTでは、期間15〜25・3σの設定がPF上位に入りました。

1位は期間20・3σ・TP50・SL30で、197回の取引で+1,155.5pips、PF1.3561です。

一方、2位の期間25・3σ・TP50・SL30はPF1.3542とほぼ同水準で、合計+1,255.5pips、最大DD210pipsとなっています。

そのため、AUDUSD SHORTでは期間20だけでなく、21・25付近でも近い成績の設定が見つかりました。

今回掲載したTOP5は、2024年1月1日〜2026年7月31日の同一期間内で設定を探索・比較した結果です。

未知期間を使ったOOS検証は行っていないため、今回の上位設定が将来も同じ成績になることを保証するものではありません。

5通貨のボリンジャーバンド検証結果を比較

5通貨の結果を比較すると、すべての通貨で共通する最適なボリンジャーバンド設定はありませんでした。

各通貨・売買方向でPFが最も高かった設定は以下の通りです。

通貨売買方向期間σTP/SLPF
USDJPYLONG213.050/101.5108
USDJPYSHORT152.030/101.0652
EURJPYLONG153.080/201.4054
EURJPYSHORT303.050/101.1047
GBPJPYLONG1003.050/201.5693
GBPJPYSHORT102.550/301.1815
EURUSDLONG153.030/201.3387
EURUSDSHORT213.050/101.4693
AUDUSDLONG1003.030/201.3438
AUDUSDSHORT203.050/301.3561

今回特に目立ったのは、PF1位の10設定中8設定が3σだったことです。

また、USDJPY・EURJPYではLONGのPFがSHORTを大きく上回る一方、EURUSD・AUDUSDではSHORTにも比較的高いPFの設定が見つかりました。

GBPJPYでは、LONGが期間100・3σ、SHORTが期間10・2.5σと、売買方向によって上位設定が大きく異なっています。

今回最もPFが高かったのは、GBPJPY LONGの期間100・3σ・TP50/SL20でPF1.5693でした。

この結果から、ボリンジャーバンドでは定番設定だけを見るのではなく、通貨・LONG/SHORT・期間・σ・TP/SLを分けて検証することが重要だと分かります。

ボリンジャーバンドをFXで使う際の注意点

今回の検証では成績の良い設定がいくつか見つかりましたが、実際に使う場合は以下の点に注意が必要です。

強いトレンドではバンドウォークに注意する

強いトレンドでは、価格が±2σや±3σ付近に沿って動き続けるバンドウォークが発生することがあります。

今回のような逆張りでは、バンド外から内側へ戻ったあとでも、そのままトレンドが継続する可能性があります。

そのため、実運用では強いトレンド中ではないかを確認することが重要です。

実運用では取引コスト・OOS検証も確認する

今回のバックテストでは、スプレッド・スリッページ・手数料などの取引コストを含めていません。

特に平均損益が小さい設定は、取引コストを加味すると成績が大きく悪化する可能性があります。

また、今回の上位設定は2024年1月1日〜2026年7月31日の同一期間内で比較した結果です。

実際に使用する場合は、取引コストを含めた再検証や、別期間を使ったOOS検証を行ってから判断することが重要です。

ボリンジャーバンドの設定に関するよくある質問

ボリンジャーバンドのおすすめ期間は?

一般的には期間20がよく使われます。

ただし、今回の検証では期間15・21・100などもPF上位に入っており、すべての通貨で期間20が最適とは限りませんでした。

そのため、まずは期間20を基準にしつつ、通貨や売買方向ごとに他の期間も比較するのがおすすめです。

ボリンジャーバンドは何σがおすすめ?

一般的には**±2σ**がよく使われます。

一方、今回の検証では各通貨・売買方向のPF1位10設定のうち、8設定が3σでした。

ただし、3σが常に優れているという意味ではありません。

今回のように、2σを基準にしつつ2.5σ・3σも比較し、PFや取引回数を確認するのがよいでしょう。

ボリンジャーバンドは逆張りと順張りのどちらで使う?

ボリンジャーバンドは、逆張り・順張りのどちらにも使われます。

今回の記事では、価格がバンド外へ出たあと、再びバンド内へ戻るタイミングを狙う逆張りルールを検証しました。

一方で、スクイーズ後の値動きやバンドウォークを利用して、トレンド方向へエントリーする使い方もあります。

逆張りと順張りでは条件が大きく異なるため、別の戦略として検証することが重要です。

まとめ

今回は、USDJPY・EURJPY・GBPJPY・EURUSD・AUDUSDの5通貨を対象に、ボリンジャーバンドの期間・σ・TP/SL・LONG/SHORTを組み合わせた5,500ケースを検証しました。

今回の結果から分かった主なポイントは、以下の通りです。

  • 期間20・2σだけが最適とは限らない
  • PF上位では3σの設定が多かった
  • USDJPY・EURJPYはLONGの方が良い結果となった
  • EURUSD・AUDUSDはSHORTにも有力な設定が見つかった
  • 同じ通貨でもLONGとSHORTで適した設定が異なる
  • 通貨ごとに期間・σ・TP/SLを分けて検証することが重要

今回最も高いPFとなったのは、GBPJPY LONGの期間100・3σ・TP50/SL20でPF1.5693でした。

一方で、今回のバックテストにはスプレッド・スリッページ・手数料などの取引コストを含めていません。

また、2024年1月1日〜2026年7月31日の同一期間内で設定を比較した結果のため、上位設定が将来も同じように機能するとは限りません。

実際に使用する場合は、今回の結果をそのまま採用するのではなく、取引コストを含めた再検証やOOS検証を行ったうえで判断してください。

-FX / EA, Python