OU Spread Modeling
Ornstein-Uhlenbeck — the mathematics of rubber bands snapping back
Learning Objectives
- •Model spreads as Ornstein-Uhlenbeck (OU) processes
- •Estimate mean-reversion speed, equilibrium, and volatility from data
- •Derive optimal entry/exit thresholds from OU parameters
Explain Like I'm 5
A mean-reverting spread can be modeled as an Ornstein-Uhlenbeck process — basically a spring that pulls back to center with random kicks. The OU model gives you three numbers: how fast it reverts (kappa), what level it reverts to (mu), and how volatile it is (sigma). From these, you can calculate mathematically optimal entry and exit levels instead of guessing at z-score thresholds.
Think of It This Way
Imagine a rubber ball on a spring. The spring (mean-reversion force) pulls it to the center. Random kicks (market noise) push it away. The OU model describes exactly this: spring strength (kappa), center position (mu), and kick strength (sigma). Stronger spring = faster reversion = better pairs trade.
1The OU Process
2Estimating OU Parameters From Data
3From OU Parameters to Optimal Thresholds
OU Spread with Optimal Trading Bands
4OU in Practice: What Textbooks Miss
Key Formulas
OU Process
The Ornstein-Uhlenbeck process: spread pulled toward mu with force kappa, disturbed by noise sigma. The standard model for mean-reverting processes.
OU Half-Life
Time for the spread to revert halfway to its mean. Shorter half-life = more tradeable pair.
Hands-On Code
OU Parameter Estimation
import numpy as np
from sklearn.linear_model import LinearRegression
def estimate_ou_params(spread):
"""Estimate OU process parameters from spread time series."""
S = spread[:-1]
dS = np.diff(spread)
model = LinearRegression()
model.fit(S.reshape(-1, 1), dS)
b = model.coef_[0]
a = model.intercept_
residuals = dS - model.predict(S.reshape(-1, 1))
if b >= 0:
print("[WARN] Spread is NOT mean-reverting (b >= 0)")
return None
kappa = -b
mu = -a / b
sigma = np.std(residuals) * np.sqrt(2 * kappa)
half_life = np.log(2) / kappa
stationary_std = sigma / np.sqrt(2 * kappa)
print(f"=== OU PARAMETERS ===")
print(f"kappa (reversion speed): {kappa:.4f}")
print(f"mu (equilibrium): {mu:.4f}")
print(f"sigma (volatility): {sigma:.4f}")
print(f"Half-life: {half_life:.1f} periods")
print(f"Stationary std: {stationary_std:.4f}")
print(f"\n=== OPTIMAL THRESHOLDS ===")
print(f"Entry: +/-{2*stationary_std:.4f} from mu")
print(f"Exit: +/-{0.5*stationary_std:.4f} from mu")
print(f"Stop: +/-{4*stationary_std:.4f} from mu")
return {'kappa': kappa, 'mu': mu, 'sigma': sigma, 'half_life': half_life}Estimates the three OU parameters (kappa, mu, sigma) from a spread time series using AR(1) regression, then computes optimal entry/exit and stop-loss thresholds.
Knowledge Check
Q1.Two pairs have the same spread volatility. Pair A has kappa=0.1 (half-life 7 days). Pair B has kappa=0.01 (half-life 69 days). Which is better for short-term pairs trading?
Assignment
Estimate OU parameters for your best cointegrated pair. Compute optimal entry/exit thresholds. Backtest using OU-optimal thresholds vs. simple z-score thresholds. Which performs better?