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.

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-lifeInterpretationPractical action
< 1 dayVery fast reversionOften microstructure noise; costs dominate
1–10 daysSweet spotClassic stat-arb spread
10–60 daysSlow but tradeableWider stops, smaller size
> 100 daysBarely revertingLikely 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 + σdW is 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.

pairs trading z-score rules from heuristics into model-based signals.

stationarity first, and always cap holding time and use a stop.

#ornstein-uhlenbeck #mean reversion #stochastic process #pairs trading #half-life