Mean Reversion in Python
A strategy paradigm based on the statistical tendency of asset prices to return to their long-run historical average after deviating from it.
Definition
Mean Reversion is one of the two foundational paradigms of quantitative trading (alongside Trend Following), rooted in the statistical concept of stationarity. It posits that asset prices, spreads, volatility, or other financial time series that are stationary will inevitably revert toward their long-run mean after experiencing transient deviations. The theoretical foundation draws from Ornstein-Uhlenbeck processes in stochastic calculus and pairs trading in statistical arbitrage. Mean reversion strategies profit from identifying when a series has deviated 'too far' from equilibrium and positioning for the return journey, typically using z-score thresholds as entry and exit signals.
Quantitative Formula
This is the Ornstein-Uhlenbeck (OU) stochastic differential equation. is the asset price or spread at time , is the long-run mean, is the mean-reversion speed (how quickly the series reverts), is the diffusion coefficient (volatility), and is a Wiener process increment. The half-life of mean reversion is — the expected time for a deviation to decay by 50%.
Why It Matters in Backtesting
The half-life of mean reversion is the single most important parameter for a mean-reversion backtest. A half-life of 2 days demands intraday execution infrastructure; a half-life of 30 days is compatible with daily bar strategies. Critically, mean-reversion strategies are highly sensitive to transaction costs — they generate many trades with small individual edges that slippage can easily erase. A rigorous backtest must verify stationarity via ADF or KPSS tests before assuming mean reversion exists, and must apply realistic slippage to avoid overstating net returns.
Python Implementation
import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import adfuller
def mean_reversion_strategy(prices: pd.Series, entry_z: float = 2.0,
exit_z: float = 0.5, lookback: int = 60) -> dict:
"""
Implements a z-score based mean reversion strategy with stationarity testing.
Generates long/short signals when price deviates beyond entry_z standard deviations.
"""
# Test for stationarity (prerequisite for mean reversion)
adf_result = adfuller(prices.dropna(), autolag="AIC")
rolling_mean = prices.rolling(lookback).mean()
rolling_std = prices.rolling(lookback).std()
z_score = (prices - rolling_mean) / rolling_std
# Half-life estimation via AR(1) regression
lag_prices = prices.shift(1).dropna()
delta_prices = prices.diff().dropna()
aligned = pd.concat([delta_prices, lag_prices], axis=1).dropna()
beta = np.polyfit(aligned.iloc[:, 1], aligned.iloc[:, 0], 1)[0]
half_life = -np.log(2) / beta if beta < 0 else np.inf
signals = pd.Series(0, index=prices.index)
signals[z_score < -entry_z] = 1 # Buy when oversold
signals[z_score > entry_z] = -1 # Sell when overbought
signals[(z_score > -exit_z) & (z_score < exit_z)] = 0 # Exit at mean
return {
"signals": signals,
"z_score": z_score,
"half_life_days": half_life,
"is_stationary": adf_result[1] < 0.05, # p-value < 0.05 rejects unit root
"adf_p_value": adf_result[1]
}Test this in a live environment
Stop running Jupyter notebooks locally. Paste this Mean Reversion code directly into Valetha's Strategy Lab and run a full historical backtest in seconds.
Open the Python Strategy Lab