Mean Reversion Strategies
Mean reversion is one of the oldest ideas in trading: what goes up too far tends to come back down, and what drops too far tends to bounce back. This guide shows you how to quantify βtoo farβ and build strategies around it.
Mean Reversion in One Sentence
Prices tend to return to their average over time. If a stock is 2 standard deviations above its mean, it is statistically likely to fall back. Mean reversion strategies profit from this tendency.
Z-Score: Measuring βHow Far from Normalβ
The Z-score tells you how many standard deviations the current price is from its moving average. A Z-score of +2 means the price is unusually high. A Z-score of -2 means unusually low.
import yfinance as yf import pandas as pd import numpy as np import matplotlib.pyplot as plt # Download data ticker = "AAPL" df = yf.download(ticker, start="2022-01-01", end="2024-01-01") # Calculate Z-score using 20-day rolling window window = 20 df["MA"] = df["Close"].rolling(window).mean() df["STD"] = df["Close"].rolling(window).std() df["Z_Score"] = (df["Close"] - df["MA"]) / df["STD"] # Plot Z-score plt.figure(figsize=(12, 5)) plt.plot(df.index, df["Z_Score"], color="teal", linewidth=1) plt.axhline(y=2, color="red", linestyle="--", alpha=0.7, label="Overbought (+2)") plt.axhline(y=-2, color="green", linestyle="--", alpha=0.7, label="Oversold (-2)") plt.axhline(y=0, color="white", linewidth=0.5, alpha=0.3) plt.fill_between(df.index, df["Z_Score"], 2, where=(df["Z_Score"] > 2), color="red", alpha=0.2) plt.fill_between(df.index, df["Z_Score"], -2, where=(df["Z_Score"] < -2), color="green", alpha=0.2) plt.title(f"{{ticker}} Z-Score ({{window}}-day)") plt.ylabel("Z-Score") plt.legend() plt.grid(True, alpha=0.3) plt.tight_layout() plt.show()Bollinger Band Strategy
# Bollinger Bands: MA +/- 2 standard deviations df["Upper_Band"] = df["MA"] + 2 * df["STD"] df["Lower_Band"] = df["MA"] - 2 * df["STD"] # Strategy: buy when price touches lower band, sell when it touches upper band df["Signal"] = 0 df.loc[df["Close"] < df["Lower_Band"], "Signal"] = 1 # Buy signal df.loc[df["Close"] > df["Upper_Band"], "Signal"] = -1 # Sell signal # Forward-fill the signal (stay in position until next signal) df["Position"] = df["Signal"].replace(0, np.nan).ffill().fillna(0) # Calculate returns df["Strategy_Return"] = df["Close"].pct_change() * df["Position"].shift(1) df["Cumulative"] = (1 + df["Strategy_Return"]).cumprod() # Plot plt.figure(figsize=(12, 6)) plt.plot(df.index, df["Close"], label="Price", alpha=0.7) plt.plot(df.index, df["MA"], label="MA(20)", linewidth=1) plt.fill_between(df.index, df["Upper_Band"], df["Lower_Band"], alpha=0.1, color="teal", label="Bollinger Bands") plt.title(f"{{ticker}} with Bollinger Bands") plt.legend() plt.grid(True, alpha=0.3) plt.tight_layout() plt.show()Pairs Trading: The Classic Mean Reversion Strategy
Pairs trading finds two stocks that move together and trades the spread between them. When the spread widens, you bet it will narrow. When it narrows too much, you bet it will widen.
# Pairs trading: Coca-Cola vs PepsiCo data = yf.download(["KO", "PEP"], start="2022-01-01", end="2024-01-01")["Close"] # Calculate the spread (ratio between the two prices) spread = data["KO"] / data["PEP"] # Z-score of the spread spread_mean = spread.rolling(60).mean() spread_std = spread.rolling(60).std() spread_z = (spread - spread_mean) / spread_std # Trading rules: # Z > 2: Short KO, Long PEP (spread is too wide, expect it to narrow) # Z < -2: Long KO, Short PEP (spread is too narrow, expect it to widen) # Z near 0: Close all positions (spread has reverted) signal = pd.Series(0, index=spread_z.index) signal[spread_z > 2] = -1 # Short the spread signal[spread_z < -2] = 1 # Long the spread # Spread return when we are in a position spread_return = spread.pct_change() strategy_return = spread_return * signal.shift(1) cumulative = (1 + strategy_return).cumprod() print(f"Pairs trading cumulative return: {{cumulative.iloc[-1] - 1:.2%}}") # Plot the Z-score with entry/exit zones plt.figure(figsize=(12, 5)) plt.plot(spread_z.index, spread_z, color="teal", linewidth=1) plt.axhline(y=2, color="red", linestyle="--", label="Short spread") plt.axhline(y=-2, color="green", linestyle="--", label="Long spread") plt.axhline(y=0, color="white", linewidth=0.5, alpha=0.3) plt.title("KO/PEP Spread Z-Score (Pairs Trading)") plt.legend() plt.grid(True, alpha=0.3) plt.tight_layout() plt.show()Cointegration vs Correlation
Correlation means two stocks move in the same direction. Cointegration means the SPREAD between them is stable over time. You can have highly correlated stocks that are NOT cointegrated (they drift apart). For pairs trading, you need cointegration, not just correlation.
When Mean Reversion Fails
- Trending markets β In a strong trend, prices do NOT revert. Mean reversion strategies get destroyed in strong bull or bear markets.
- Regime changes β The relationship between two stocks can break permanently (e.g., one gets acquired, changes industry, or goes bankrupt).
- Black swan events β COVID crashed everything together. Your βhedgedβ pairs trade lost money on both sides.
- Crowded trades β If everyone is trading the same pairs, the edge disappears.