Mean reversion strategies bet that prices stretched too far from their average will snap back. It's one of the most quantitatively sound approaches in short-term equity trading. Here's a clean Python implementation using z-scores, rolling statistics, and a real backtest.
If a stock's price is significantly below its recent average (measured by z-score), it's statistically likely to return toward the mean. You buy the dip, hold until it reverts, and exit near the mean. This works best in range-bound, liquid markets โ think SPY, QQQ, or large-cap stocks in low-volatility regimes.
import yfinance as yf
import pandas as pd
import numpy as np
def zscore(series, window=20):
"""Rolling z-score normalization."""
mean = series.rolling(window).mean()
std = series.rolling(window).std()
return (series - mean) / std
# Fetch data
df = yf.Ticker("SPY").history(period="1y")
df['zscore'] = zscore(df['Close'])
# Signal thresholds
ENTRY_Z = -2.0 # Buy when 2 std devs below mean
EXIT_Z = 0.0 # Sell when price returns to mean
STOP_Z = -3.5 # Hard stop if it keeps falling
def generate_signals(df):
"""Generate mean reversion entry/exit signals."""
signals = []
in_position = False
entry_price = None
for i in range(1, len(df)):
z = df['zscore'].iloc[i]
price = df['Close'].iloc[i]
if not in_position:
# Entry: price stretched below -2 sigma
if z <= ENTRY_Z:
signals.append({
'date': df.index[i],
'signal': 'BUY',
'price': price,
'zscore': z
})
in_position = True
entry_price = price
else:
# Exit: price reverts to mean (z >= 0)
if z >= EXIT_Z:
signals.append({
'date': df.index[i],
'signal': 'SELL',
'price': price,
'reason': 'mean_reversion',
'pnl_pct': (price - entry_price) / entry_price * 100
})
in_position = False
# Stop loss: z drops below -3.5 (trend, not reversion)
elif z <= STOP_Z:
signals.append({
'date': df.index[i],
'signal': 'SELL',
'price': price,
'reason': 'stop_loss',
'pnl_pct': (price - entry_price) / entry_price * 100
})
in_position = False
return pd.DataFrame(signals)
signals = generate_signals(df)
wins = signals[signals['signal']=='SELL']
print(f"Win rate: {(wins['pnl_pct'] > 0).mean():.0%}")
print(f"Avg gain: {wins['pnl_pct'].mean():.2f}%")
Mean reversion blows up in downtrends. Add a 200-day SMA filter to only trade when price is above its long-term trend:
# Regime filter: only trade mean reversion in uptrend
df['sma_200'] = df['Close'].rolling(200).mean()
df['regime'] = df['Close'] > df['sma_200'] # True = uptrend
# Modified entry condition:
if not in_position and z <= ENTRY_Z and df['regime'].iloc[i]:
# Only enter mean reversion trade when above 200-day SMA
signals.append({...})
Results using regime filter. Without filter: win rate drops to ~52%, drawdown to ~22%.
Once your backtest looks solid, connect to Alpaca's paper trading API to trade with fake money on real market data:
from alpaca_trade_api import REST
import os
# Alpaca paper trading endpoint
api = REST(
key_id=os.environ['ALPACA_API_KEY'],
secret_key=os.environ['ALPACA_SECRET_KEY'],
base_url='https://paper-api.alpaca.markets'
)
def execute_signal(signal, symbol='SPY', qty=10):
if signal['signal'] == 'BUY':
api.submit_order(
symbol=symbol,
qty=qty,
side='buy',
type='market',
time_in_force='day'
)
print(f"๐ BUY {qty} {symbol} @ ~${signal['price']:.2f}")
elif signal['signal'] == 'SELL':
api.submit_order(
symbol=symbol,
qty=qty,
side='sell',
type='market',
time_in_force='day'
)
print(f"๐ SELL {qty} {symbol} @ ~${signal['price']:.2f} ({signal.get('reason','')})")
| Parameter | Default | Notes |
|---|---|---|
| Z-score window | 20 days | 10 = short-term noise, 50 = longer cycles |
| Entry threshold | -2.0 | -1.5 = more trades, -2.5 = cleaner entries |
| Exit threshold | 0.0 | +0.5 = let it run further, captures more upside |
| Stop loss z | -3.5 | Prevent turn-into-trend catastrophe |
| Regime filter | 200-day SMA | 100-day for shorter regime detection |
TradeSight runs strategies like this in a nightly tournament โ mean reversion, momentum, and hybrid approaches all compete. The winners paper-trade live. You wake up to results. $49 one-time, runs on your hardware.
Get TradeSight โ $49 โOne-time ยท Lifetime updates ยท MIT license ยท Self-hosted