ボリンジャーバンドをPythonで実装!±2σで逆張りシグナルを検知する方法

Python実装・コード

※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。

株価が「買われすぎ」なのか「売られすぎ」なのかを統計的に判断できるテクニカル指標、それがボリンジャーバンドです。

移動平均線と標準偏差を組み合わせたこの指標は、逆張り戦略の根幹として世界中のトレーダーに活用されています。

しかし、証券会社のチャートツールに表示されるボリンジャーバンドを眺めるだけでは、複数銘柄の一括監視や独自ルールによる自動シグナル検知は実現できません。

この記事では、Pythonとpandasだけでボリンジャーバンドの全ライン(±1σ、±2σ、±3σ)を計算し、バンドブレイクの売買シグナルを自動検出するコードを提供します。

すべてのコードはコピペでそのまま動作しますので、統計やプログラミングの初心者でも安心して取り組めます。


ボリンジャーバンドの基本理論

コードを書く前に、ボリンジャーバンドが「何を計算し、何を意味しているのか」を正確に理解しておくことが重要です。

ボリンジャーバンドを構成する5本のライン

ボリンジャーバンドは、中心の移動平均線を基準に、標準偏差(σ:シグマ)を加減した複数のラインで構成されます。

ライン名 計算式 意味
+2σ(上限バンド) SMA + 標準偏差 × 2 統計的な上限ゾーン
+1σ SMA + 標準偏差 × 1 やや高めのゾーン
ミドルバンド(SMA) 終値のN日単純移動平均 基準値
−1σ SMA − 標準偏差 × 1 やや安めのゾーン
−2σ(下限バンド) SMA − 標準偏差 × 2 統計的な下限ゾーン

一般的に使用されるデフォルトパラメータは「期間20日・±2σ」です。

標準偏差と確率の関係

ボリンジャーバンドの理論的基盤は「正規分布」です。

株価が正規分布に従うと仮定した場合、各バンド内に価格が収まる確率は以下の通りです。

  • ±1σ以内: 約68.3%
  • ±2σ以内: 約95.4%
  • ±3σ以内: 約99.7%

つまり、株価が±2σの外に出る確率は統計的に約4.6%しかないということになります。

この「異常値」を検知して逆張りエントリーを行うのが、ボリンジャーバンドを使った基本戦略です。

ただし、株価は厳密には正規分布に従いません。トレンドが強い局面ではバンドウォーク(バンドに沿って一方向に動き続ける現象)が発生するため、逆張りだけに頼るのは危険です。必ず他の指標と併用してください。


【コピペOK】ボリンジャーバンドの計算関数を実装する

ここからは、pandasを使ってボリンジャーバンドの全ラインを計算する関数を実装します。

汎用的なボリンジャーバンド計算関数

以下のコードは、DataFrameに「Close」列があれば、どんなデータソースにも適用できる汎用関数です。


import pandas as pd

def calculate_bollinger_bands(
    df: pd.DataFrame,
    period: int = 20,
    num_std: float = 2.0
) -> pd.DataFrame:
    """
    ボリンジャーバンドを計算してDataFrameに追加する。

    Parameters
    ----------
    df : pd.DataFrame
        'Close' 列を含むDataFrame
    period : int
        移動平均の期間(デフォルト20日)
    num_std : float
        標準偏差の倍率(デフォルト2.0)

    Returns
    -------
    pd.DataFrame
        BB_Middle, BB_Upper, BB_Lower, BB_Width, BB_Percent 列が追加されたDataFrame
    """
    # ミドルバンド(単純移動平均)
    df["BB_Middle"] = df["Close"].rolling(window=period).mean()

    # 標準偏差
    df["BB_Std"] = df["Close"].rolling(window=period).std()

    # アッパーバンド / ロワーバンド
    df["BB_Upper"] = df["BB_Middle"] + (df["BB_Std"] * num_std)
    df["BB_Lower"] = df["BB_Middle"] - (df["BB_Std"] * num_std)

    # ±1σのラインも追加
    df["BB_Upper_1s"] = df["BB_Middle"] + (df["BB_Std"] * 1)
    df["BB_Lower_1s"] = df["BB_Middle"] - (df["BB_Std"] * 1)

    # バンド幅(ボラティリティの大きさを数値化)
    df["BB_Width"] = (df["BB_Upper"] - df["BB_Lower"]) / df["BB_Middle"] * 100

    # %B(現在価格がバンド内のどの位置にあるか)
    df["BB_Percent"] = (df["Close"] - df["BB_Lower"]) / (df["BB_Upper"] - df["BB_Lower"])

    return df

%B(パーセントB)の読み方

上記コードで計算している BB_Percent(%B)は、現在の株価がバンド内のどの位置にあるかを0〜1の範囲で示す指標です。

  • %B > 1.0: 株価が+2σを上に突き抜けている(買われすぎ)
  • %B = 0.5: 株価がミドルバンド上にある
  • %B < 0.0: 株価が−2σを下に突き抜けている(売られすぎ)

この%Bを使えば、異なる株価水準の銘柄同士でも「バンドに対する相対位置」を統一基準で比較できます。


【コピペOK】yfinanceで実データに適用する

計算関数が完成したら、yfinanceで取得した実際の株価データにボリンジャーバンドを適用します。

環境準備

以下のコマンドで必要なライブラリをインストールしてください。


pip install yfinance pandas

日本株のボリンジャーバンドを一気通貫で算出するコード


import yfinance as yf
import pandas as pd

# ==============================
# 設定エリア(★ここを編集してください)
# ==============================
SYMBOL = "9984.T"       # ソフトバンクグループ
PERIOD = "1y"           # 取得期間
BB_PERIOD = 20          # 移動平均の期間
BB_STD = 2.0            # 標準偏差の倍率

# ==============================
# ボリンジャーバンド計算関数
# ==============================
def calculate_bollinger_bands(df: pd.DataFrame, period: int, num_std: float) -> pd.DataFrame:
    """ボリンジャーバンドの全ラインを計算する"""
    df["BB_Middle"] = df["Close"].rolling(window=period).mean()
    df["BB_Std"] = df["Close"].rolling(window=period).std()
    df["BB_Upper"] = df["BB_Middle"] + (df["BB_Std"] * num_std)
    df["BB_Lower"] = df["BB_Middle"] - (df["BB_Std"] * num_std)
    df["BB_Upper_1s"] = df["BB_Middle"] + (df["BB_Std"] * 1)
    df["BB_Lower_1s"] = df["BB_Middle"] - (df["BB_Std"] * 1)
    df["BB_Width"] = (df["BB_Upper"] - df["BB_Lower"]) / df["BB_Middle"] * 100
    df["BB_Percent"] = (df["Close"] - df["BB_Lower"]) / (df["BB_Upper"] - df["BB_Lower"])
    return df

# ==============================
# メイン実行
# ==============================
def main():
    print(f"=== {SYMBOL} のボリンジャーバンドを計算します ===")

    ticker = yf.Ticker(SYMBOL)
    df = ticker.history(period=PERIOD)

    if df.empty:
        print("エラー: データが取得できませんでした。銘柄コードを確認してください。")
        return

    print(f"取得件数: {len(df)}件")

    df = calculate_bollinger_bands(df, BB_PERIOD, BB_STD)

    # 結果表示
    display_cols = ["Close", "BB_Upper", "BB_Middle", "BB_Lower", "BB_Width", "BB_Percent"]
    print("n--- 直近10日間 ---")
    print(df[display_cols].tail(10).round(2))

    # CSV保存
    filename = f"{SYMBOL.replace('.', '_')}_bollinger.csv"
    df.to_csv(filename)
    print(f"n保存完了: {filename}")

if __name__ == "__main__":
    main()

実行すると、以下のような出力が得られます。


--- 直近10日間 ---
              Close  BB_Upper  BB_Middle  BB_Lower  BB_Width  BB_Percent
2025-02-10   9250.0   9580.32    9120.50   8660.68      10.1        0.64
2025-02-12   8950.0   9560.18    9100.25   8640.32       9.9        0.34
...

BB_Percent が1.0を超えていれば+2σブレイク、0.0を下回っていれば−2σブレイクです。この列を監視するだけでシグナル検知が可能となります。


【コピペOK】逆張りシグナルを自動検知する

ボリンジャーバンドの計算ができたら、次は株価がバンドを突き抜けたタイミングを自動で検知するロジックを実装します。

逆張り戦略のシグナルルール

基本的な逆張り戦略は以下のルールで運用します。

  • 買いシグナル: 株価が−2σを下に突き抜けた後、バンド内に戻ったとき(反転上昇の兆候)
  • 売りシグナル: 株価が+2σを上に突き抜けた後、バンド内に戻ったとき(反転下落の兆候)

単純に「−2σを下回ったら即買い」とするのではなく、「突き抜けた後に戻る」動きを確認するのがポイントです。

これにより、バンドウォーク中の早すぎるエントリーを回避できます。

シグナル種別 前日の条件 当日の条件 判定
買い(反転検知) 終値 < −2σ 終値 ≥ −2σ バンド内回帰 → BUY
売り(反転検知) 終値 > +2σ 終値 ≤ +2σ バンド内回帰 → SELL

バンドブレイク&回帰シグナルの完全コード


import yfinance as yf
import pandas as pd

# ==============================
# 設定エリア
# ==============================
SYMBOL = "9984.T"
PERIOD = "1y"
BB_PERIOD = 20
BB_STD = 2.0

# ==============================
# ボリンジャーバンド計算
# ==============================
def calculate_bollinger_bands(df: pd.DataFrame) -> pd.DataFrame:
    """ボリンジャーバンドを追加する"""
    df["BB_Middle"] = df["Close"].rolling(window=BB_PERIOD).mean()
    bb_std = df["Close"].rolling(window=BB_PERIOD).std()
    df["BB_Upper"] = df["BB_Middle"] + (bb_std * BB_STD)
    df["BB_Lower"] = df["BB_Middle"] - (bb_std * BB_STD)
    df["BB_Percent"] = (df["Close"] - df["BB_Lower"]) / (df["BB_Upper"] - df["BB_Lower"])
    return df

# ==============================
# シグナル検知
# ==============================
def detect_bb_signals(df: pd.DataFrame) -> pd.DataFrame:
    """バンドブレイク後の回帰シグナルを検出する"""
    df["Signal"] = ""

    for i in range(1, len(df)):
        prev_close = df["Close"].iloc[i - 1]
        curr_close = df["Close"].iloc[i]
        prev_upper = df["BB_Upper"].iloc[i - 1]
        prev_lower = df["BB_Lower"].iloc[i - 1]
        curr_upper = df["BB_Upper"].iloc[i]
        curr_lower = df["BB_Lower"].iloc[i]

        # NaNチェック
        if pd.isna(prev_upper) or pd.isna(curr_upper):
            continue

        # 買いシグナル:前日に-2σ以下 → 当日に-2σ以上に回帰
        if prev_close < prev_lower and curr_close >= curr_lower:
            df.iloc[i, df.columns.get_loc("Signal")] = "BUY"

        # 売りシグナル:前日に+2σ以上 → 当日に+2σ以下に回帰
        elif prev_close > prev_upper and curr_close <= curr_upper:
            df.iloc[i, df.columns.get_loc("Signal")] = "SELL"

    return df

# ==============================
# メイン実行
# ==============================
def main():
    print(f"=== {SYMBOL} のボリンジャーバンドシグナルを検出します ===n")

    ticker = yf.Ticker(SYMBOL)
    df = ticker.history(period=PERIOD)

    if df.empty:
        print("データ取得に失敗しました。銘柄コードを確認してください。")
        return

    df = calculate_bollinger_bands(df)
    df = detect_bb_signals(df)

    # シグナルが出た日だけ抽出
    signals = df[df["Signal"] != ""]
    display_cols = ["Close", "BB_Upper", "BB_Middle", "BB_Lower", "BB_Percent", "Signal"]

    if signals.empty:
        print("指定期間内にシグナルは検出されませんでした。")
    else:
        print(f"検出されたシグナル数: {len(signals)}件n")
        print(signals[display_cols].round(2).to_string())

    # CSV保存
    filename = f"{SYMBOL.replace('.', '_')}_bb_signals.csv"
    df.to_csv(filename)
    print(f"n保存完了: {filename}")

if __name__ == "__main__":
    main()

【コピペOK】複数銘柄の一括スクリーニング

ウォッチリストの全銘柄を一括でスクリーニングし、直近日にシグナルが発生した銘柄だけを抽出するコードです。

一括スクリーニングコード


import yfinance as yf
import pandas as pd
import time

# ==============================
# 設定エリア
# ==============================
WATCHLIST = [
    ("7203.T", "トヨタ自動車"),
    ("9984.T", "ソフトバンクG"),
    ("6758.T", "ソニーG"),
    ("8306.T", "三菱UFJ"),
    ("4063.T", "信越化学"),
    ("6861.T", "キーエンス"),
]
PERIOD = "6mo"
BB_PERIOD = 20
BB_STD = 2.0

# ==============================
# 計算 + 直近シグナル判定
# ==============================
def analyze_single_stock(symbol: str) -> dict:
    """1銘柄のボリンジャーバンドを計算し直近シグナルを返す"""
    ticker = yf.Ticker(symbol)
    df = ticker.history(period=PERIOD)

    if df.empty or len(df) < BB_PERIOD + 1:
        return {"signal": "取得失敗", "close": "-", "percent_b": "-"}

    df["BB_Middle"] = df["Close"].rolling(window=BB_PERIOD).mean()
    bb_std = df["Close"].rolling(window=BB_PERIOD).std()
    df["BB_Upper"] = df["BB_Middle"] + (bb_std * BB_STD)
    df["BB_Lower"] = df["BB_Middle"] - (bb_std * BB_STD)
    df["BB_Percent"] = (df["Close"] - df["BB_Lower"]) / (df["BB_Upper"] - df["BB_Lower"])

    prev = df.iloc[-2]
    curr = df.iloc[-1]

    signal = "— なし"
    if prev["Close"] < prev["BB_Lower"] and curr["Close"] >= curr["BB_Lower"]:
        signal = "🔵 BUY(-2σ回帰)"
    elif prev["Close"] > prev["BB_Upper"] and curr["Close"] <= curr["BB_Upper"]:
        signal = "🔴 SELL(+2σ回帰)"
    elif curr["Close"] < curr["BB_Lower"]:
        signal = "⚠️ -2σブレイク中"
    elif curr["Close"] > curr["BB_Upper"]:
        signal = "⚠️ +2σブレイク中"

    return {
        "signal": signal,
        "close": round(curr["Close"], 1),
        "percent_b": round(curr["BB_Percent"], 3),
    }

# ==============================
# スクリーニング実行
# ==============================
def screening():
    print("=== ボリンジャーバンド スクリーニング開始 ===n")
    results = []

    for symbol, name in WATCHLIST:
        print(f"処理中: {name}({symbol})")
        result = analyze_single_stock(symbol)
        results.append({
            "銘柄": name,
            "コード": symbol,
            "終値": result["close"],
            "%B": result["percent_b"],
            "シグナル": result["signal"],
        })
        time.sleep(1)

    result_df = pd.DataFrame(results)
    print("n=== スクリーニング結果 ===n")
    print(result_df.to_string(index=False))

if __name__ == "__main__":
    screening()

スクリーニング結果の読み方

出力される一覧表には、各銘柄の「%B」と「シグナル」が表示されます。

  • %B < 0: −2σの外にいる(売られすぎゾーン)
  • %B > 1: +2σの外にいる(買われすぎゾーン)
  • %B = 0.5付近: ミドルバンド近辺で安定している状態

「🔵 BUY」「🔴 SELL」が表示された銘柄は、直近でバンド回帰が発生しています。

「⚠️ ブレイク中」は現在進行形でバンド外にあり、まだ回帰していない状態です。

バンドウォーク(トレンドに沿ってバンド外を推移し続ける状態)では逆張りが連続で損失を出します。出来高の急増や決算発表などファンダメンタル要因も確認してからエントリー判断を行ってください。


ボリンジャーバンドの応用テクニック

基本のバンドブレイク戦略に加え、実戦で使える応用テクニックを2つ紹介します。

スクイーズとエクスパンション

バンド幅(BB_Width)の変化を監視することで、ボラティリティの急変を事前に察知できます。

  • スクイーズ: バンド幅が極端に狭くなっている状態。大きな値動きが近いことを示唆します
  • エクスパンション: バンド幅が急拡大した状態。トレンド発生の初動を捉えるチャンスです

スクイーズの検出には、以下のようにBB_Widthの直近最小値を監視する方法が有効です。


# 直近60日間のバンド幅の最小値を算出
df["BB_Width_Min_60"] = df["BB_Width"].rolling(window=60).min()

# スクイーズ判定:現在のバンド幅が直近最小値と等しければスクイーズ
df["Is_Squeeze"] = df["BB_Width"] == df["BB_Width_Min_60"]

ダブルボリンジャーバンド戦略

±2σだけでなく±1σのラインも活用する手法です。

以下のゾーン分類でトレンドの強さを判断します。

ゾーン 条件 解釈
強い上昇トレンド 終値が+1σ〜+2σの間 買い継続
弱い上昇 / 中立 終値が−1σ〜+1σの間 様子見
強い下降トレンド 終値が−1σ〜−2σの間 売り継続

前述のコードでは BB_Upper_1sBB_Lower_1s も計算済みのため、ゾーン分類のロジックを追加するだけでダブルボリンジャーバンド戦略を実装できます。


よくあるエラーと対処法

ボリンジャーバンド実装時に初心者がつまずきやすいポイントをQ&A形式で解説します。

最初の数十行がNaNになります

ボリンジャーバンドは移動平均と標準偏差の計算にN日分のデータが必要です。

期間20日の場合、最初の19行はNaNとなります。

取得期間を「6mo」や「1y」に設定すれば、十分なデータが確保されます。

シグナル検出のループ内では、NaNチェックを必ず入れてください。

rolling().std() の計算結果が他のツールと微妙に異なります

pandasの rolling().std() はデフォルトで不偏標準偏差(ddof=1)を使用します。

一方、一部のチャートツールでは母標準偏差(ddof=0)を使用しています。

証券会社のチャートと完全一致させたい場合は、以下のように ddof=0 を指定してください。


df["BB_Std"] = df["Close"].rolling(window=20).std(ddof=0)

バンドウォークでシグナルが連発してしまいます

強いトレンドが発生すると、株価がバンド外を推移しながら一方向に動き続ける「バンドウォーク」が起こります。

この状態では、回帰シグナルが短期間に何度も発生し、逆張りの連続損失につながります。

対策として、以下のフィルターを追加することを推奨します。

  • 出来高フィルター: 出来高が20日平均の1.5倍以上のときはシグナルを無視します
  • 連続シグナル抑制: 前回のシグナルから5営業日以内の再シグナルを無視します
  • トレンドフィルター: ミドルバンド(SMA)の傾きが急な場合はシグナルを無視します

yfinanceでデータが取得できません

銘柄コードの末尾に「.T」が付いていない、または上場廃止銘柄を指定している可能性があります。

Yahoo Financeのウェブサイトで銘柄コードが有効であることを確認してください。

ネットワーク環境が不安定な場合は、time.sleep(2) でリクエスト間隔を広げることも有効です。


まとめ

ボリンジャーバンドをPythonで計算し、逆張りシグナルを自動検知するまでの流れを整理します。

  1. 理論の理解: 移動平均と標準偏差で構成される5本のラインと、%Bの意味を把握します
  2. 計算関数の実装: pandasの rolling()std() で全ラインを算出します
  3. 実データへの適用: yfinanceで日本株の株価を取得し、ボリンジャーバンドを計算します
  4. シグナル検知: バンドブレイク後の回帰タイミングを自動判定します
  5. 複数銘柄スクリーニング: ウォッチリストを一括処理し、シグナル発生銘柄を一覧表示します

ボリンジャーバンドは逆張りに強い一方、トレンド相場ではバンドウォークに巻き込まれるリスクがあります。

MACDやRSIなどのトレンドフォロー型指標と組み合わせて使うことで、ダマシを大幅に減らせます。

まずは今回のコードをそのまま実行してバンドの動きを体感し、その後スクイーズ検出やダブルボリンジャーバンド戦略の実装に進んでください。

タイトルとURLをコピーしました