The Ornstein-Uhlenbeck Process: Modeling Mean Reversion
The Ornstein-Uhlenbeck (OU) process is the cleanest mathematical model of mean reversion — a quantity that gets pulled back toward a long-run average whenever it strays. Where geometric Brownian motion wanders without bound, the OU process is anchored. That makes it the natural model for the things systematic traders actually trade: interest rates, volatility, and above all the spread in a pairs trade. This guide derives the OU equation, explains the all-important half-life of mean reversion, shows two ways to estimate the parameters, and turns the model into concrete entry and exit rules with Python.
The OU stochastic differential equation
The OU process X(t) is defined by the SDE:
dX = θ (μ − X) dt + σ dW
Read each piece literally:
μis the long-run mean — the level the process is pulled toward.θ(theta) is the speed of mean reversion — how hard the pull is. Largerθ
means a faster, more aggressive snap back.
σis the volatility of the random shocks.dWis a Brownian motion
increment.
The drift term θ(μ − X) is the whole story: when X is above μ the term is negative and pushes it down; when X is below μ it pushes up. The further from the mean, the stronger the restoring force. It is a spring with random buffeting.
Stationary distribution
Unlike a random walk, the OU process has a well-defined stationary distribution. Left to run, it settles into a normal distribution:
X(∞) ~ Normal( mean = μ, variance = σ² / (2θ) )
This is exactly the stationarity property you test for in time-series analysis. A higher reversion speed θ tightens the equilibrium spread; a higher σ widens it. The ratio σ²/(2θ) is the natural scale of the fluctuations you will be trading around.
The half-life of mean reversion
The single most useful number you extract from an OU fit is the half-life — the expected time for a deviation from the mean to shrink by half. It follows directly from θ:
half_life = ln(2) / θ
The half-life sets your trading clock. If the half-life is 3 days, deviations resolve in days, and you should use rolling windows, holding periods, and stops measured in days. If it is 200 days, the "edge" reverts so slowly that financing and drift risk will likely swamp it. As a rule of thumb, a tradeable spread has a half-life short relative to your holding horizon but long enough to clear costs.
Estimating OU parameters by OLS
Discretize the SDE over a fixed step and it becomes an AR(1) regression. Regress the change ΔX on the level X:
X(t+1) − X(t) = a + b·X(t) + ε
Match coefficients to the discretized OU and recover the parameters:
θ = −b / Δtμ = −a / bσ = std(ε) · sqrt( 2θ / (1 − exp(−2θΔt)) )
import numpy as np
import statsmodels.api as sm
def fit_ou(x, dt=1.0):
x = np.asarray(x, dtype=float)
dx = np.diff(x)
lag = x[:-1]
model = sm.OLS(dx, sm.add_constant(lag)).fit()
a, b = model.params
theta = -b / dt
mu = -a / b
resid_std = np.std(model.resid, ddof=2)
sigma = resid_std * np.sqrt(2 * theta / (1 - np.exp(-2 * theta * dt)))
half_life = np.log(2) / theta
return {"theta": theta, "mu": mu, "sigma": sigma,
"half_life": half_life}
# spread = priceA - beta * priceB (a cointegrated pairs spread)
params = fit_ou(spread, dt=1.0)
print(f"theta={params['theta']:.3f} mu={params['mu']:.3f} "
f"half-life={params['half_life']:.1f} bars")
The slope b must be negative for genuine mean reversion (θ > 0). A non-negative slope means no reversion — do not trade it as an OU spread.
A note on maximum likelihood
OLS on the discretized equation is fast and usually good enough, but the exact OU transition density is known in closed form (it is Gaussian with explicit conditional mean and variance), so you can fit by maximum likelihood for slightly better small-sample behavior and proper standard errors. In practice the OLS and MLE estimates of θ agree closely when the data is reasonably long and evenly sampled; reach for MLE when steps are irregular or you need confidence intervals on the half-life.
Applying OU to a pairs spread
The OU process is the rigorous justification for the z-score rules used in mean-reversion and pairs trading. Once you have a cointegrated spread, fit OU to it, then standardize:
def ou_zscore(spread, mu, sigma_eq):
# sigma_eq is the *equilibrium* std = sqrt(sigma**2 / (2*theta))
return (spread - mu) / sigma_eq
theta, mu, sigma = params["theta"], params["mu"], params["sigma"]
sigma_eq = sigma / np.sqrt(2 * theta)
z = ou_zscore(spread, mu, sigma_eq)
entry, exit_, stop = 2.0, 0.0, 3.5
position = 0
for zt in z:
if position == 0 and abs(zt) > entry:
position = -np.sign(zt) # fade the deviation
elif position != 0 and abs(zt) < exit_ + 0.25:
position = 0 # spread reverted
elif position != 0 and abs(zt) > stop:
position = 0 # spread broke; bail
The OU fit gives the z-score a principled scale (the equilibrium standard deviation, not a noisy rolling estimate) and the half-life gives you a maximum holding time: if the spread has not reverted within roughly two or three half-lives, treat the relationship as broken and exit.
Choosing thresholds with the half-life
| Half-life | Interpretation | Practical action |
|---|---|---|
| < 1 day | Very fast reversion | Often microstructure noise; costs dominate |
| 1–10 days | Sweet spot | Classic stat-arb spread |
| 10–60 days | Slow but tradeable | Wider stops, smaller size |
| > 100 days | Barely reverting | Likely spurious; avoid |
Pair these with risk-based position sizing so a spread that fails to revert cannot sink the book.
Common mistakes
- Trading a non-stationary "spread." If the OLS slope is not significantly
negative, there is no reversion; you are trading a random walk and will bleed.
- Estimating
θon too little data. Half-life estimates are noisy in short
samples; use enough history and re-estimate on a rolling basis.
- Fixing parameters forever. Real spreads drift;
μ,θ, and the hedge ratio all
evolve, so refit periodically.
- Ignoring the holding-time cap. Without a max-hold tied to the half-life, a broken
spread ties up capital indefinitely while moving against you.
- Confusing equilibrium std with rolling std. Use the OU equilibrium scale for a
clean z-score; mixing scales distorts your thresholds.
Key takeaways
- The OU process
dX = θ(μ − X)dt + σdWis the canonical model of
mean reversion, with an explicit pull toward μ at speed θ.
- Its half-life,
ln(2)/θ, is the most actionable output and sets your holding
horizon and stops.
- Estimate parameters quickly by OLS of changes on levels (an AR(1) regression);
use MLE for irregular sampling or confidence intervals.
- Fit OU to a cointegrated spread to convert
pairs trading z-score rules from heuristics into model-based signals.
- Confirm the Hurst exponent and
stationarity first, and always cap holding time and use a stop.
