Pairs Trading and Cointegration

Finance

Pairs trading is the canonical statistical-arbitrage strategy. The idea: find two securities whose prices are linked by some economic relationship — same industry, same supply chain, share class duals, ETF and its constituents — so that their RATIO or LINEAR COMBINATION is approximately stationary even though each individual price is a random walk. When the spread deviates from its long-term mean, short the rich one, long the cheap one; close the position when the spread mean-reverts. The mathematical machinery is COINTEGRATION (Engle and Granger, 1987 — Granger's share of the 2003 Nobel cited it), and the trading rule is mean-reversion in the spread.

Pairs trading is also the cleanest entry point to a broader family of STATISTICAL ARBITRAGE strategies that look for short-term mean reversion in mispriced relationships. Many of these strategies were extremely profitable in the 1990s and early 2000s (Bamberger / D.E. Shaw / Renaissance), and have since been crowded by quant funds running similar logic. The basic methodology survives — the alpha has migrated to subtler signals, faster timescales, and more complex multi-asset combinations.

Stationarity and cointegration

A time series is STATIONARY if its statistical properties (mean, variance, autocorrelation) are time-invariant. Asset prices are typically NOT stationary — they have unit roots, meaning with no mean-reverting force; the variance of grows linearly with . Stationary series are tradeable in a mean-reversion sense; non-stationary series are not.

Two non-stationary series and are COINTEGRATED if there exists a coefficient such that IS stationary. Both and can wander freely, but their linear combination is anchored. Economically, this happens when the two assets are linked by an equilibrium relationship (no-arbitrage, hedging, common factor) that arbitrageurs enforce. The spread can deviate temporarily but ALWAYS comes back.

The Engle-Granger test

The classical procedure (Engle and Granger 1987):

  1. Regress on : . The coefficient is the COINTEGRATING VECTOR.
  2. Form the residuals .
  3. Apply an Augmented Dickey-Fuller (ADF) unit-root test to the residuals. The null hypothesis is that has a unit root (non-stationary, no cointegration). The test regresses on (with optional lags); the -statistic of the lagged-level coefficient is compared to the DICKEY-FULLER critical values (NOT the usual Student t critical values, because under the null the regressor is non-stationary).

Standard Dickey-Fuller critical values (no constant, no trend, large samples): 1% = -2.58, 5% = -1.95, 10% = -1.62. If the observed is BELOW the critical value (more negative), reject the null and conclude COINTEGRATION. Practitioners typically use the Johansen test (1991) instead in higher dimensions; for pair-wise tests Engle-Granger is fine.

The spread as Ornstein-Uhlenbeck process

Once cointegration is established, model the spread as a mean-reverting Ornstein-Uhlenbeck process:

The PARAMETER is the rate of mean reversion (inverse of the characteristic timescale); is the long-term mean; sets the volatility. Estimate by maximum likelihood or by regression of on . The CHARACTERISTIC HALF-LIFE of mean reversion is ; for a pair to be tradeable you typically want half-life in days to weeks, not months.

Trading the spread

Standardize the spread by its rolling mean and standard deviation:

where are computed over a trailing window (e.g. 60 days). The classical rule:

The thresholds (2 in, 0.5 out, 4 stop) are tunable parameters. Optimal values depend on the spread's mean-reversion speed and transaction costs; this is empirical work, and overfitting is the standard hazard.

Code

# Pairs trading on a synthetic cointegrated pair:
#   1. Generate two series sharing a common stochastic trend.
#   2. Engle-Granger test: regress one on the other, ADF the residuals.
#   3. Build the spread, compute rolling z-score, and run a simple
#      mean-reversion strategy that enters at |z|>2 and exits at |z|<0.5.

import numpy as np

rng = np.random.default_rng(123)
n = 1000

# Two cointegrated series: shared random-walk trend plus idiosyncratic noise.
common  = np.cumsum(rng.normal(0, 1, n))
beta_true = 1.5
A = common + rng.normal(0, 0.5, n) + 10
B = beta_true * common + rng.normal(0, 0.5, n) + 5

# ─── Engle-Granger cointegration test ──────────────────────────────────
# Step 1: regress A on B (or vice versa).
b_est, a_est = np.polyfit(B, A, 1)
spread = A - (b_est * B + a_est)
print(f"Engle-Granger regression A_t = a + b * B_t + e_t:")
print(f"  b_hat = {b_est:.4f}  (true hedge ratio = 1 / beta_true = {1/beta_true:.4f})")

# Step 2: ADF unit-root test on the residuals.
# Manual ADF: regress dx_t on x_{t-1} and check t-statistic vs DF critical values.
def adf_t_statistic(x):
    dx = np.diff(x)
    x_lag = x[:-1]
    X = x_lag - np.mean(x_lag)
    Y = dx - np.mean(dx)
    rho = np.sum(X * Y) / np.sum(X * X)
    e   = Y - rho * X
    se  = np.sqrt(np.sum(e**2) / (len(e) - 1) / np.sum(X * X))
    return rho / se

t_stat = adf_t_statistic(spread)
print(f"  ADF t-statistic on spread: {t_stat:.4f}")
print(f"  Critical values: 1% = -2.58, 5% = -1.95")
print(f"  Spread is " +
      ('STATIONARY (cointegrated)' if t_stat < -1.95 else 'non-stationary'))

# ─── Mean-reversion trading strategy ──────────────────────────────────
window = 60
def rolling(x, w, fn):
    return np.array([fn(x[max(0, i-w):i+1]) for i in range(len(x))])
mu_s = rolling(spread, window, np.mean)
sd_s = rolling(spread, window, np.std)
z    = (spread - mu_s) / np.maximum(sd_s, 1e-8)

position = 0
trades = 0
pnl = 0.0
for t in range(window, n - 1):
    if position == 0:
        if   z[t] >  2: position = -1; entry = spread[t]; trades += 1
        elif z[t] < -2: position =  1; entry = spread[t]; trades += 1
    else:
        if abs(z[t]) < 0.5:
            pnl += position * (spread[t] - entry)
            position = 0

print(f"\nMean-reversion strategy on the spread (|z|>2 entry, |z|<0.5 exit):")
print(f"  Number of round-trip trades: {trades}")
print(f"  Total PnL on the spread: {pnl:.2f}")

Output:

Engle-Granger regression A_t = a + b * B_t + e_t:
  b_hat = 0.6642  (true hedge ratio = 1 / beta_true = 0.6667)
  ADF t-statistic on spread: -30.8160
  Critical values: 1% = -2.58, 5% = -1.95
  Spread is STATIONARY (cointegrated)

Mean-reversion strategy on the spread (|z|>2 entry, |z|<0.5 exit):
  Number of round-trip trades: 41
  Total PnL on the spread: 59.00

Three things to read off. (1) The Engle-Granger regression recovers the true hedge ratio very close to the analytical value (the regression coefficient is the ratio that makes the residuals stationary). (2) The ADF -statistic is — WAY beyond the 1% critical value of . Strong rejection of the unit-root null; cointegration is conclusively present (as designed). (3) The simple entry / exit rule generates 41 round-trip trades over the 1000-day sample with $59 of P&L on the spread — modest but positive. In real applications the spread would be normalized to dollar P&L per unit position, and you'd benchmark the Sharpe ratio rather than absolute P&L.

The Kalman filter extension

A static hedge ratio works when the relationship between and is stable. In practice the relationship can drift — corporate actions, structural breaks, evolving fundamentals. The KALMAN FILTER tracks a TIME-VARYING hedge ratio:

The state-space form is:

with . The Kalman filter gives the optimal recursive estimate of from the observations, and the FILTERED residuals are the trading spread. This is widely used in practice — many "pairs trading" production systems are really Kalman filters on cointegrating relationships.

What goes wrong in practice

Related