Kalman Pair
Pair trading with a Kalman filter that learns the hedge ratio as the market shifts
Risk
Low
Holding Period
Days to weeks
Best For
Pairs whose β visibly drifts over time
How it works
The classic OLS pair trade fits a fixed β once and trades the spread forever. But when the true relationship drifts — central bank regime shifts, currency pegs changing, balance-sheet rebalancing — that static β becomes stale and the 'spread' systematically diverges. Kalman pair treats (α, β) as hidden state following a random walk and re-estimates it every bar. The innovation (y − α − β·x) is the tradable spread.
Mathematical Foundation
[α_t, β_t] = [α_{t−1}, β_{t−1}] + w_t, w_t ~ N(0, Q=δ/(1−δ)·I) z = e_t / std_window(e)Signal Generation Logic
- 1For every asset pair, initialize the state [α=0, β=1] with large initial uncertainty (P₀ = 10³·I)
- 2At each bar t, run one Kalman update: predict, compute innovation e_t = y_t − α_t − β_t · x_t, update gain, update state
- 3Compute a z-score of the innovation series using a 60-bar rolling std — this is the 'normalized spread'
- 4Enter long spread when z < −2 (spread is cheap relative to recent innovations)
- 5Enter short spread when z > +2
- 6Exit when |z| < 0.5 — spread has reverted through the filter's live estimate
- 7Skip the first 60 bars (burn-in) so the filter's state has time to converge
Parameters Explained
deltaKalman process-noise scaling. Larger delta = β adapts faster to regime changes but becomes noisier; Chan's book default is 1e-5. Try 1e-3 for more responsive tracking.
Default
1e-5v_epsilonObservation-noise variance. Larger = trust observations less, filter stays closer to its prior. Default is adequate for log-price-scale pairs.
Default
1e-3entry_zAbsolute z-score threshold to open a position. Same semantics as the static pair_trading strategy.
Default
2exit_zAbsolute z-score threshold to close. Tighter than entry_z for faster mean-reversion harvest.
Default
0.5z_windowRolling window (bars) for computing the innovation std — this is the denominator of the trading z-score.
Default
60burn_inBars skipped at the start so the filter's state converges before signals fire.
Default
60When It Works
Pairs whose true hedge ratio drifts over time — central bank regime transitions, currency-peg adjustments, balance-sheet rebalancing. Kalman keeps the β fresh where static OLS would become stale.
When It Fails
Pairs without true cointegration — the filter will happily track spurious β paths and generate garbage signals. Also sensitive to δ tuning: too large and the filter chases noise; too small and it's slow. Burn-in period means no signals in the first ~3 months.
Risks & Limitations
- Parameter sensitivity: δ and v_epsilon require tuning per asset class; default (1e-5 / 1e-3) is a starting point, not a free lunch
- Spurious cointegration: the filter doesn't reject the pair — it will trade nonsense if you feed it nonsense
- Same-bar-β issue: asset_label carries the β observed at signal time, but the spread rehedges continuously; a backtest engine that doesn't support time-varying β will approximate performance
- No automatic re-entry after stop-out — if |z| > stop_z you should manually reset the filter state or wait for a clean signal
Implementation
Plain 2D Kalman filter in pure numpy (no external library) — ~30 lines. State is [alpha, beta]; observation is y_t = H · state + noise with H = [1, x_t]. Random-walk dynamics: state_{t+1} = state_t + N(0, Q). Initial P = 10³·I so the first few observations move freely; the filter then settles down.
Model parameters
Entry Z
|z| threshold to open a pair position
Exit Z
|z| threshold to close once spread normalizes
δ (delta)
Process-noise scaling — smaller = slower β adaptation
z Window
Rolling window for innovation z-score denominator
Burn-in
Bars skipped at start for filter convergence
Academic background
Academic Basis
Based on Chan (2009) 'Algorithmic Trading: Winning Strategies and Their Rationale', Chapter 3 on Kalman-filter hedge ratio estimation
Backtest this strategy
Run the exact model on your selected assets and date range. See trade-by-trade performance.
Backtest This →