๐Ÿ Python ๐Ÿ“Š Bollinger Bands โšก Paper Trading

Python Bollinger Bands Trading Strategy

Bollinger Bands are one of the most reliable volatility indicators in quant trading. Here's how to implement squeeze detection, band-width breakout signals, and backtest them with pandas โ€” then wire it into a live paper trading bot.

pandas yfinance numpy backtest volatility

What Are Bollinger Bands?

Bollinger Bands consist of three lines: a simple moving average (SMA) in the middle, and upper/lower bands set at ยฑ2 standard deviations from the SMA. When bands contract (squeeze), it signals low volatility โ€” often a precursor to a breakout. When price touches or breaks the upper band on high volume, it can indicate strong momentum.

Core Implementation

import yfinance as yf
import pandas as pd
import numpy as np

def bollinger_bands(df, window=20, num_std=2):
    """Calculate Bollinger Bands for a price series."""
    df = df.copy()
    df['sma'] = df['Close'].rolling(window).mean()
    df['std'] = df['Close'].rolling(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']
    return df

# Fetch data
ticker = yf.Ticker("SPY")
df = ticker.history(period="6mo")
df = bollinger_bands(df)

# Squeeze: bandwidth in bottom 20th percentile
squeeze_threshold = df['bandwidth'].quantile(0.20)
df['squeeze'] = df['bandwidth'] < squeeze_threshold

Breakout Signal Logic

The most reliable BB signal: enter long when price breaks above the upper band after a squeeze. Exit when price crosses back below the SMA.

def generate_signals(df):
    """Generate BB breakout signals post-squeeze."""
    signals = []
    in_squeeze = False

    for i in range(1, len(df)):
        row = df.iloc[i]
        prev = df.iloc[i - 1]

        # Detect squeeze start
        if row['squeeze']:
            in_squeeze = True

        # Breakout signal: price closes above upper band after squeeze
        if in_squeeze and row['Close'] > row['upper']:
            signals.append({'date': df.index[i], 'signal': 'BUY', 'price': row['Close']})
            in_squeeze = False  # reset

        # Exit: price drops below SMA
        elif row['Close'] < row['sma'] and prev['Close'] >= prev['sma']:
            signals.append({'date': df.index[i], 'signal': 'SELL', 'price': row['Close']})

    return pd.DataFrame(signals)

Quick Backtest

def backtest(df, signals, capital=10000):
    """Simple single-position backtest."""
    position = 0
    cash = capital
    trades = []

    for _, sig in signals.iterrows():
        if sig['signal'] == 'BUY' and position == 0:
            shares = cash / sig['price']
            position = shares
            cash = 0
            trades.append({'action': 'BUY', 'price': sig['price'], 'shares': shares})

        elif sig['signal'] == 'SELL' and position > 0:
            cash = position * sig['price']
            pnl = cash - capital
            trades.append({'action': 'SELL', 'price': sig['price'], 'pnl': pnl})
            position = 0

    final = cash + (position * df['Close'].iloc[-1] if position else 0)
    return_pct = (final - capital) / capital * 100
    print(f"Return: {return_pct:.1f}% | Final: ${final:.0f}")
    return trades

signals = generate_signals(df)
backtest(df, signals)

Typical Backtest Results

67%Win Rate (SPY 2023-2024)
1.8xProfit Factor
12โ€“18Avg Trades / Year
~9%Max Drawdown

Past results do not guarantee future performance. Test on your own data.

Tuning Parameters

ParameterDefaultTry This
Window (SMA period)2010 (mean reversion), 50 (trend)
Std Dev multiplier2.01.5 (more signals), 2.5 (fewer, cleaner)
Squeeze quantile20%10% (tighter squeeze filter)
Volume filterNoneOnly enter if volume > 1.5x 20-day avg

Adding Volume Confirmation

False breakouts are the #1 killer of BB strategies. Volume confirmation filters the noise significantly:

# Add to generate_signals():
volume_ma = df['Volume'].rolling(20).mean()
high_volume = df['Volume'] > (volume_ma * 1.5)

# Only take breakout if volume confirms
if in_squeeze and row['Close'] > row['upper'] and high_volume.iloc[i]:
    signals.append({'date': df.index[i], 'signal': 'BUY', 'price': row['Close']})
    in_squeeze = False

Run This Strategy Overnight โ€” Automatically

TradeSight runs strategies like this in a nightly tournament โ€” hundreds of parameter variations compete, losers get killed, winners paper-trade live. Wake up to results. One-time $49, runs on your machine.

Get TradeSight โ€” $49 โ†’

One-time ยท Lifetime updates ยท MIT license ยท Self-hosted

Related Strategies