Pythonでボリンジャーバンド・ブレイクアウト戦略を実装する【日本株・製造メーカー向け】

日立や三菱電機みたいな製造メーカーを長年見てきて、「決算発表の後にバン!って動く動きがあるよな」とずっと思ってた。でも手動でチャートを眺めながら「これはブレイクアウトか?」を判断するのは、育児中の今はもう無理。だからPythonでボリンジャーバンドのブレイクアウトを自動検出できるようにした。思ったより精度が良くてびっくりしてる。

ボリンジャーバンドとは(簡単おさらい)

ボリンジャーバンドは移動平均線を中心に、上下に「標準偏差×倍率」の帯を引いたもの。一般的な設定は20日移動平均 ± 2σ。株価がバンドの外側に飛び出すと「統計的に異常な動き」として注目される。

使い方は主に2つあって、「バンドタッチで反転を狙う(逆張り)」と「バンドを突き抜けたらトレンド継続(順張り)」。今回は後者のブレイクアウト戦略。製造メーカー株は一度トレンドが出ると続きやすい印象があって、順張りのほうが僕の体感に合ってる。

Pythonで実装する

まずライブラリのインストール。

pip install yfinance pandas numpy matplotlib

次にボリンジャーバンドの計算とブレイクアウト検出。

import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
matplotlib.rcParams['font.family'] = 'IPAGothic'  # 日本語フォント(環境に応じて変更)

def get_bollinger_bands(ticker, period=20, std_dev=2, start="2023-01-01", end="2025-12-31"):
    """ボリンジャーバンドを計算して返す"""
    df = yf.download(ticker, start=start, end=end)
    df = df[["Close"]].dropna()

    df["MA"] = df["Close"].rolling(period).mean()
    df["STD"] = df["Close"].rolling(period).std()
    df["Upper"] = df["MA"] + std_dev * df["STD"]
    df["Lower"] = df["MA"] - std_dev * df["STD"]
    df = df.dropna()
    return df

def detect_breakout(df):
    """ブレイクアウトシグナルを検出する"""
    df = df.copy()

    # 上抜けブレイクアウト(前日はバンド内 → 当日はバンド外)
    df["prev_close"] = df["Close"].shift(1)
    df["prev_upper"] = df["Upper"].shift(1)
    df["prev_lower"] = df["Lower"].shift(1)

    df["breakout_up"] = (df["Close"] > df["Upper"]) & (df["prev_close"] <= df["prev_upper"])
    df["breakout_down"] = (df["Close"] < df["Lower"]) & (df["prev_close"] >= df["prev_lower"])

    return df

# 日立(6501.T)で試す
ticker = "6501.T"
df = get_bollinger_bands(ticker)
df = detect_breakout(df)

# ブレイクアウト日を確認
up_breakouts = df[df["breakout_up"]]
down_breakouts = df[df["breakout_down"]]

print(f"上抜けブレイクアウト: {len(up_breakouts)}回")
print(f"下抜けブレイクアウト: {len(down_breakouts)}回")
print("\n直近の上抜けブレイクアウト:")
print(up_breakouts[["Close", "Upper", "Lower"]].tail(5))

ブレイクアウト戦略のバックテスト

シグナルが出た翌日に買い、一定日数後に手仕舞いするシンプルな戦略でバックテスト。

def backtest_breakout(df, hold_days=5):
    """
    ブレイクアウト翌日に買い、hold_days後に売るバックテスト
    """
    trades = []

    for i in range(len(df) - hold_days - 1):
        if df["breakout_up"].iloc[i]:
            # 翌日の始値で買い(終値で近似)
            entry_price = df["Close"].iloc[i + 1]
            exit_price = df["Close"].iloc[i + 1 + hold_days]
            return_pct = (exit_price - entry_price) / entry_price * 100
            trades.append({
                "entry_date": df.index[i + 1],
                "exit_date": df.index[i + 1 + hold_days],
                "entry_price": entry_price,
                "exit_price": exit_price,
                "return_pct": return_pct
            })

    trades_df = pd.DataFrame(trades)
    if trades_df.empty:
        print("トレードなし")
        return trades_df

    print(f"総トレード数: {len(trades_df)}")
    print(f"勝率: {(trades_df['return_pct'] > 0).mean() * 100:.1f}%")
    print(f"平均リターン: {trades_df['return_pct'].mean():.2f}%")
    print(f"最大利益: {trades_df['return_pct'].max():.2f}%")
    print(f"最大損失: {trades_df['return_pct'].min():.2f}%")
    return trades_df

trades = backtest_breakout(df, hold_days=5)

チャートで確認する

def plot_bollinger_with_signals(df, ticker_name):
    """ボリンジャーバンドとシグナルをプロット"""
    fig, ax = plt.subplots(figsize=(14, 6))

    # ボリンジャーバンド
    ax.plot(df.index, df["Close"], label="終値", color="black", linewidth=1)
    ax.plot(df.index, df["MA"], label="20日MA", color="blue", linewidth=1, linestyle="--")
    ax.plot(df.index, df["Upper"], label="上限バンド(+2σ)", color="red", linewidth=1)
    ax.plot(df.index, df["Lower"], label="下限バンド(-2σ)", color="green", linewidth=1)
    ax.fill_between(df.index, df["Upper"], df["Lower"], alpha=0.05, color="gray")

    # ブレイクアウトシグナル
    up_signals = df[df["breakout_up"]]
    down_signals = df[df["breakout_down"]]
    ax.scatter(up_signals.index, up_signals["Close"], marker="^", color="red", s=100, zorder=5, label="上抜けシグナル")
    ax.scatter(down_signals.index, down_signals["Close"], marker="v", color="green", s=100, zorder=5, label="下抜けシグナル")

    ax.set_title(f"{ticker_name} ボリンジャーバンド・ブレイクアウト戦略")
    ax.legend(loc="upper left")
    ax.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig(f"{ticker_name}_bollinger.png", dpi=150)
    plt.show()

plot_bollinger_with_signals(df, "6501_日立")

複数の製造メーカー銘柄でスクリーニング

「今日ブレイクアウトしている銘柄はどれか?」を一括チェックできるスクリーナーも作った。

# 製造メーカー銘柄リスト
manufacturer_tickers = {
    "7203.T": "トヨタ",
    "6501.T": "日立",
    "6752.T": "パナソニック",
    "6702.T": "富士通",
    "6861.T": "キーエンス",
    "7267.T": "ホンダ",
    "6758.T": "ソニー",
    "6902.T": "デンソー",
}

today_breakouts = []

for ticker, name in manufacturer_tickers.items():
    try:
        df = get_bollinger_bands(ticker, start="2024-01-01")
        df = detect_breakout(df)

        # 直近5日以内のブレイクアウト
        recent = df.tail(5)
        if recent["breakout_up"].any():
            last_signal = recent[recent["breakout_up"]].index[-1]
            today_breakouts.append({
                "銘柄": name,
                "コード": ticker,
                "シグナル日": last_signal.strftime("%Y-%m-%d"),
                "終値": df["Close"].iloc[-1],
                "上限バンド": round(df["Upper"].iloc[-1], 0)
            })
    except Exception as e:
        print(f"{name}: データ取得エラー ({e})")

if today_breakouts:
    print("直近5日のブレイクアウト銘柄:")
    print(pd.DataFrame(today_breakouts).to_string(index=False))
else:
    print("直近5日のブレイクアウト銘柄なし")

実際にやってみての気づき

日立(6501.T)で3年分バックテストしてみたら、勝率は55〜60%、平均リターン1〜2%くらいだった。悪くはないけど「必勝法」でもない。特に、バンドが収縮している状態(スクイーズ)でのブレイクアウトは精度が高く、反対に「もともとボラが高い局面」でのシグナルは機能しにくい印象。

あと製造メーカーは決算前後の動きが大きいので、決算期(3月・9月)を外してバックテストすると数値が変わる。こういう「季節性」も考慮するのが次のステップかなと思ってる。

まとめ

ボリンジャーバンドのブレイクアウト戦略をPythonで実装すれば、「毎日チャートを眺める」作業を自動化できる。製造メーカー中心のポートフォリオを持っている人には相性がいい手法だと思う。

個人的に、スクリーナーを朝の通勤中にスマホで確認できるよう通知機能を追加したい。次は「スクイーズ検出 → ブレイクアウト待ち」の組み合わせにも挑戦してみる。

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