Saturday, March 28, 2026
HomeAI ToolsPython Mean Reversion Alerts: Build an RSI + Bollinger Band Signal Generator

Python Mean Reversion Alerts: Build an RSI + Bollinger Band Signal Generator

Build time: ~20 min
📊 Data source: yfinance (free)
🔧 Difficulty: Intermediate
📈 Output: Daily buy/sell alert signals + CSV export

Python Investor’s Toolkit: Part 1: Momentum · Part 2: Monte Carlo · Part 3: Mean Reversion · Part 4: Earnings Quality · Part 5: Macro Regime

Part 3 of 5 — The Python Investor’s Toolkit: Practical quant tools built with free data.

Momentum (Part 1) tells you which stocks are trending. But trends don’t go straight — they oscillate. Mean reversion strategies exploit those oscillations: when a stock gets statistically oversold within an ongoing uptrend, it tends to snap back. That snap-back is where a mean reversion trader earns their edge.

This scanner combines two classic technical indicators — RSI (Relative Strength Index) and Bollinger Bands — to identify stocks that are simultaneously oversold on both measures. A single-indicator signal has too many false positives; the dual confirmation cuts noise significantly.

What You’ll Need

pip install yfinance pandas numpy

How the Two Indicators Work Together

Indicator What It Measures Oversold Level Overbought Level
RSI (14-day) Speed/magnitude of recent price moves < 30 > 70
%B (Bollinger) Price position within its volatility band < 0.05 (below lower band) > 0.95 (above upper band)
Combined Signal Both indicators agree on extremity Strong Buy (mean reversion) Strong Sell (take profit)

💡 Why %B Instead of Just Price vs. Band: %B normalizes a stock’s position within its Bollinger Bands to a 0–1 scale. A %B of 0 means the price is exactly at the lower band; a %B of 1 means it’s at the upper band. Values below 0 or above 1 indicate the price has broken through the band — a rare and significant signal when combined with an extreme RSI reading.

The Code

import yfinance as yf
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

# -------------------------------------------------------
# Indicator functions
# -------------------------------------------------------

def calculate_rsi(prices, period=14):
    """Wilder's RSI — the standard implementation used by most platforms."""
    delta = prices.diff()
    gain  = delta.clip(lower=0)
    loss  = -delta.clip(upper=0)

    # Wilder smoothing (exponential, not simple moving average)
    avg_gain = gain.ewm(alpha=1/period, min_periods=period, adjust=False).mean()
    avg_loss = loss.ewm(alpha=1/period, min_periods=period, adjust=False).mean()

    rs = avg_gain / avg_loss
    return 100 - (100 / (1 + rs))

def calculate_bollinger_bands(prices, period=20, std_dev=2):
    """Standard 20-period Bollinger Bands with 2 standard deviation bands."""
    sma   = prices.rolling(window=period).mean()
    std   = prices.rolling(window=period).std()
    upper = sma + (std * std_dev)
    lower = sma - (std * std_dev)
    return upper, sma, lower

def get_signals(ticker, lookback_days=200):
    """
    Calculate RSI and Bollinger Band signals for a single ticker.
    Returns signal strength: +3 (strong buy) to -3 (strong sell), 0 = neutral.
    """
    end   = datetime.today()
    start = end - timedelta(days=lookback_days + 60)

    data = yf.download(ticker, start=start, end=end, progress=False)
    if data.empty:
        return None

    close = data['Adj Close']
    rsi   = calculate_rsi(close)
    bb_upper, bb_mid, bb_lower = calculate_bollinger_bands(close)

    # %B: where is price within the bands? (0 = lower band, 1 = upper band)
    pct_b = (close - bb_lower) / (bb_upper - bb_lower)

    current_price = float(close.iloc[-1])
    current_rsi   = float(rsi.iloc[-1])
    current_pct_b = float(pct_b.iloc[-1])

    # Classify signal
    if   current_rsi < 30  and current_pct_b < 0.05:  signal, strength = "STRONG BUY",   3
    elif current_rsi < 40  and current_pct_b < 0.20:  signal, strength = "BUY",           2
    elif current_rsi < 45  and current_pct_b < 0.30:  signal, strength = "WEAK BUY",      1
    elif current_rsi > 70  and current_pct_b > 0.95:  signal, strength = "STRONG SELL",  -3
    elif current_rsi > 60  and current_pct_b > 0.80:  signal, strength = "SELL",          -2
    elif current_rsi > 55  and current_pct_b > 0.70:  signal, strength = "WEAK SELL",     -1
    else:                                                signal, strength = "NEUTRAL",       0

    return {
        'ticker'   : ticker,
        'price'    : round(current_price, 2),
        'rsi'      : round(current_rsi, 1),
        'pct_b'    : round(current_pct_b, 3),
        'bb_lower' : round(float(bb_lower.iloc[-1]), 2),
        'bb_upper' : round(float(bb_upper.iloc[-1]), 2),
        'signal'   : signal,
        'strength' : strength
    }

# -------------------------------------------------------
# Scanner: run across a watchlist
# -------------------------------------------------------

def scan_watchlist(tickers):
    """Scan a list of tickers and print actionable signals."""
    print(f"Scanning {len(tickers)} tickers...")
    results = [r for t in tickers if (r := get_signals(t)) is not None]

    df = pd.DataFrame(results).sort_values('strength', ascending=False)

    buys  = df[df['strength'] > 0]
    sells = df[df['strength'] < 0]

    if not buys.empty:
        print("n=== BUY SIGNALS (Oversold) ===")
        print(buys[['ticker','price','rsi','pct_b','signal']].to_string(index=False))

    if not sells.empty:
        print("n=== SELL SIGNALS (Overbought) ===")
        print(sells[['ticker','price','rsi','pct_b','signal']].to_string(index=False))

    if buys.empty and sells.empty:
        print("nNo actionable signals today — all stocks are in neutral territory.")

    df.to_csv('mean_reversion_signals.csv', index=False)
    print(f"nAll results saved to mean_reversion_signals.csv")
    return df

# -------------------------------------------------------
# Run it — replace with your watchlist
# -------------------------------------------------------
watchlist = [
    'AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META', 'NVDA', 'TSLA',
    'JPM', 'BAC', 'GS', 'WMT', 'JNJ', 'XOM', 'CVX',
    'SPY', 'QQQ', 'IWM', 'GLD', 'TLT'
]

results = scan_watchlist(watchlist)

Running It Daily (Automation Tip)

The real value of this tool is consistency — running it every day before market open and acting on signals systematically. On Windows, schedule it with Task Scheduler to run at 8:30 AM. On macOS/Linux, add a cron job:

# Run the scanner Monday–Friday at 8:30 AM
30 8 * * 1-5 /usr/bin/python3 /path/to/mean_reversion_scanner.py >> /path/to/scanner.log 2>&1

📈 Key Insight: Mean reversion works best on high-quality stocks within established uptrends — not on broken businesses or stocks in genuine freefall. Before acting on a STRONG BUY signal, check that the stock is above its 200-day moving average. If it’s below, the “oversold” reading may simply reflect a deteriorating business, not a temporary dip.

⚠️ Watch Out: Mean reversion signals have higher hit rates in range-bound, low-volatility markets. During strong trending markets or high-volatility regimes (VIX > 30), oversold conditions can persist or deepen significantly. Check the current VIX level before sizing into a mean reversion trade — reduce position size by 50% when VIX is above 25.

📊 Portfolio Takeaway

Only act on STRONG BUY signals (strength +3, RSI < 30 and %B < 0.05) — and only when the stock is above its 200-day moving average. Keep position sizes small (1–2% of portfolio per signal), since mean reversion can take weeks and may deepen before recovering. When VIX is above 25, skip mean reversion trades entirely or cut size by half — oversold conditions in high-volatility regimes can persist far longer than the model expects.

What’s Next

Parts 1–3 of this series have focused on price-based signals. Part 4 goes deeper: using SEC EDGAR’s free API to analyze earnings quality directly from financial statements — identifying companies where reported earnings are backed by real cash flow vs. accounting accruals.

Series: The Python Investor’s Toolkit
Part 1: Momentum Scanner
Part 2: Monte Carlo Portfolio Stress-Test
Part 3: Mean Reversion Alert System (this post)
Part 4: Earnings Quality Analyzer (SEC EDGAR)
Part 5: Macro Regime Detector (FRED API)

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here