Portfolio Optimization Under a Mean-Reverting SDE

The geometric Brownian motion from the previous post is a reasonable model for a single trending asset, but pairs of cointegrated assets — think a stock and its future, or two closely related equities — are often better described by an Ornstein-Uhlenbeck process on their spread:

$$ dX_t = \theta(\mu - X_t), dt + \sigma, dW_t $$

Here $\theta$ controls the speed of mean reversion, $\mu$ is the long-run mean of the spread, and $\sigma$ is the diffusion coefficient. Unlike GBM, the OU process has a well-defined stationary distribution, which is precisely what makes a pairs-trading strategy tractable: you can compute the half-life of a deviation directly from $\theta$,

$$ t_{1/2} = \frac{\ln 2}{\theta} $$

and size a mean-reversion trade around it. Estimating $\theta$, $\mu$, and $\sigma$ from data is usually done with a discretized AR(1) regression:

ou_fit.py
import numpy as np

def fit_ou(spread: np.ndarray, dt: float = 1.0) -> tuple[float, float, float]:
    x, y = spread[:-1], spread[1:]
    beta, alpha = np.polyfit(x, y, 1)
    theta = -np.log(beta) / dt
    mu = alpha / (1 - beta)
    resid = y - (alpha + beta * x)
    sigma = resid.std() * np.sqrt(2 * theta / (1 - beta ** 2))
    return theta, mu, sigma

Typical fitted half-lives for liquid equity pairs:

PairθHalf-life
XOM / CVX spread0.0848.3 days
KO / PEP spread0.04116.9 days

Full position-sizing and Kelly-fraction derivation is a longer post on its own — this one is mostly here to set up the SDE so the fitting code above has somewhere to live.