CodeForFinance
Python

Bollinger Bands Strategy in Python

Bollinger Bands are a volatility indicator created by John Bollinger. They consist of a middle band (SMA) and upper/lower bands set at 2 standard deviations above and below. When bands squeeze, a breakout is coming. When price touches the outer bands, a reversal may follow. In this tutorial, we build Bollinger Bands from scratch and create a mean-reversion trading strategy.

Prerequisites

  • Basic Python and pandas
  • Understanding of standard deviation

Step 1.Get market data

We use SPY (S&P 500 ETF) as it exhibits clear mean-reversion behaviour.

import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt

df = yf.download("SPY", start="2021-01-01", end="2024-06-01")
df = df[["Close"]].copy()
df.columns = ["close"]

Step 2.Calculate Bollinger Bands

Bandwidth measures how wide the bands are relative to price. Low bandwidth means a squeeze.

WINDOW = 20
NUM_STD = 2

df["sma"] = df["close"].rolling(window=WINDOW).mean()
df["std"] = df["close"].rolling(window=WINDOW).std()
df["upper"] = df["sma"] + (NUM_STD * df["std"])
df["lower"] = df["sma"] - (NUM_STD * df["std"])
df["bandwidth"] = (df["upper"] - df["lower"]) / df["sma"]

print(df[["close", "sma", "upper", "lower", "bandwidth"]].tail())

Step 3.Generate mean-reversion signals

The mean-reversion idea: price touching the lower band is oversold, upper band is overbought.

df["signal"] = 0
df.loc[df["close"] < df["lower"], "signal"] = 1    # Buy when price below lower band
df.loc[df["close"] > df["upper"], "signal"] = -1   # Sell when price above upper band

buys = df[df["signal"] == 1]
sells = df[df["signal"] == -1]
print(f"Buy signals: {len(buys)}")
print(f"Sell signals: {len(sells)}")

Step 4.Detect volatility squeezes

A squeeze often precedes a big move. Traders watch for the bands to expand after a squeeze.

squeeze_threshold = df["bandwidth"].quantile(0.1)
df["squeeze"] = df["bandwidth"] < squeeze_threshold
squeezes = df[df["squeeze"]]
print(f"Squeeze days (lowest 10% bandwidth): {len(squeezes)}")

Step 5.Plot everything

Upper chart shows price with Bollinger Bands and buy/sell signals. Lower chart shows bandwidth with squeeze detection.

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10))

ax1.plot(df.index, df["close"], label="Price", linewidth=1)
ax1.plot(df.index, df["sma"], label="SMA 20", alpha=0.7)
ax1.fill_between(df.index, df["upper"], df["lower"], alpha=0.15, color="cyan")
ax1.scatter(buys.index, buys["close"], marker="^", color="lime", s=60)
ax1.scatter(sells.index, sells["close"], marker="v", color="red", s=60)
ax1.set_title("Bollinger Bands")
ax1.legend()

ax2.plot(df.index, df["bandwidth"], color="orange")
ax2.axhline(squeeze_threshold, color="red", linestyle="--", alpha=0.5, label="Squeeze threshold")
ax2.set_title("Bandwidth (Squeeze Detector)")
ax2.legend()

plt.tight_layout()
plt.savefig("bollinger.png", dpi=150)
plt.show()

Expected Output

Buy signals: 42
Sell signals: 38
Squeeze days (lowest 10% bandwidth): 87

Next Steps

  • Combine Bollinger Bands with RSI for confirmation
  • Backtest the mean-reversion strategy with transaction costs
  • Try different window and standard deviation settings

Recommended Reading

Bollinger on Bollinger Bands

As an Amazon Associate we may earn from qualifying purchases.

Browse All Tutorials →

Developer Essentials

As an Amazon Associate we may earn from qualifying purchases.