Every pairs trade hides a regression inside it: the hedge ratio that turns two prices into one spread. Estimate that ratio once and you have quietly assumed the relationship never moves — over fifteen years of EWA and EWC it moves from 0.71 to 1.49. We build a Kalman filter from scratch in NumPy that treats the hedge ratio as a moving target, then run an honest experiment: identical trading rules on the Kalman spread and the static spread, with costs, and let the data pick the winner.

  1. 01Load EWA & EWC adjusted closes, 2010–2024 (the classic Chan pair)
  2. 02Fit the two OLS baselines: full-sample beta and rolling 252d beta (statsmodels)
  3. 03Build the Kalman filter in NumPy — state [β, α], random-walk transition
  4. 04Compare the three beta paths on one chart
  5. 05Trade the spread: |z| > 2 entry, z-crosses-0 exit, 10 bp per leg
  6. 06Read the honest scoreboard: gross vs net, Kalman vs static

1.Why static hedge ratios die

EWA (Australia) and EWC (Canada) are the canonical cointegration pair — two commodity-heavy developed markets whose daily returns correlate at 0.83. Regress EWC on EWA over the full sample and you get one hedge ratio for fifteen years: , with an ADF p-value of 0.038 on the residual spread — cointegrated, by the book (Engle–Granger: regress one price on the other, then unit-root-test the residual). The problem is the word one. A hedge ratio is an estimate of an economic relationship — commodity mix, currency betas, index composition — and every one of those drifted between 2010 and 2024. A rolling window is the standard fix, and it limps: every observation inside the window carries equal weight, so year-old data moves today's estimate as much as yesterday's, and each point falling out of the window jerks the estimate — the window cliff.

0.811.21.41.61.82'10'12'14'16'18'20'22'24
EWA & EWC, normalized to 1.0 at 2010-01-04 — related, not identicalEWCEWA

2.Beta as a state, not a constant

The Kalman filter starts from a different premise: the regression coefficients are unobserved statesthat evolve through time, and each day's prices are a noisy measurement of them. Two equations define the model. The state equation says the hedge ratio and intercept follow a random walk — — tomorrow's relationship is today's, plus noise. The observation equation says . Two variances close the model: the state noise with (the standard parameterization from Chan), and observation noise .

Delta is the single real knob, and it replaces the window size entirely: it is a forgetting rate. Larger δ lets the states wander faster (adaptive but noisy); smaller δ pins them down (smooth but laggy); δ = 0 collapses to recursive least squares — a static beta refined forever. Both values here are textbook defaults, deliberately not tuned on this sample: tune δ to the backtest and you are optimizing the strategy through the back door.

3.The filter in five lines of NumPy

No library, no black box — the whole filter is a predict step and an update step, looped over the sample. Predict: with a random-walk transition the state estimate is unchanged and its covariance grows by Q (uncertainty leaks in). Update: compare the observed EWC to the prediction, and shift the states toward the error in proportion to the Kalman gain — the ratio of state uncertainty to total uncertainty.

kalman.py
def kalman_hedge(x, y, delta=1e-5, r_obs=1e-3):
    q = (delta / (1 - delta)) * np.eye(2)     # trans_cov
    state, p_cov = np.zeros(2), np.eye(2)     # [beta, alpha], diffuse start
    betas, alphas = np.zeros(len(x)), np.zeros(len(x))
    for t in range(len(x)):
        h = np.array([x[t], 1.0])             # observation map
        p_cov = p_cov + q                     # predict   (F = I)
        e = y[t] - h @ state                  # innovation
        s = h @ p_cov @ h + r_obs             # innovation variance
        k = p_cov @ h / s                     # Kalman gain
        state = state + k * e                 # update
        p_cov = p_cov - np.outer(k, h @ p_cov)
        betas[t], alphas[t] = state
    return betas, alphas

The gain is the elegance. When the filter is uncertain (large p_cov), it learns aggressively from each observation; once confident, new data barely moves it — unless Q keeps injecting doubt, which is exactly what lets β keep adapting forever. An exponentially-weighted regression, derived from first principles rather than picked from a menu of window sizes.

4.Three betas, one pair

Slice off the first year (rolling-OLS warm-up, Kalman burn-in) and compare the three estimators of the same quantity. The static line says the answer is 1.57, forever. The rolling OLS swings between 0.19 and 2.75 — whipping around every regime change a full window late. The Kalman path covers 0.71 to 1.49, moving early and smoothly: no cliff, because no window.

0.511.522.5'11'13'15'17'19'21'23static 1.57
The EWC~EWA hedge ratio, three ways — Kalman filter vs rolling 252d OLS vs full-sample OLSKalman βrolling OLSstatic OLS

Note what the rolling estimator does around 2020–2021: the COVID shock enters the window, distorts the regression for exactly 252 trading days, then falls out and the estimate jumps again — two artefacts from one event. The filter digests the same shock in weeks and moves on.

5.Trading the spread

The spread is , z-scored on a trailing 60-day window. Rules, identical for both variants: enter long the spread (long EWC, short β·EWA) when , short when , exit when z crosses zero. Positions are sized to $1 gross notional at entry, the Kalman variant re-hedges the EWA leg to the current β daily, and every unit of traded notional pays 10 bp. Signals use the close and P&L starts the next day — no lookahead in the rule. The static beta itself, of course, is one giant lookahead: it was fit on all fifteen years, including the future of every trade it takes.

signal.py
spread = ewc - beta * ewa - alpha              # beta_t, alpha_t from the filter
z = (spread - spread.rolling(60).mean()) / spread.rolling(60).std()

# enter |z| > 2, exit when z crosses 0, hold in between
if p == 0:
    p = 1 if z[t] < -2 else (-1 if z[t] > 2 else 0)
elif p == 1 and z[t] >= 0: p = 0               # long leg reverted
elif p == -1 and z[t] <= 0: p = 0              # short leg reverted
-3-2-1012'11'13'15'17'19'21'23+2σ−2σ
The Kalman-spread z-score with ±2σ entry bands — every excursion is a candidate tradez-score±2σ entry

6.What the numbers actually say

Here is the honest scoreboard, and it is more interesting than a clean win. Gross of costs, the Kalman spread is the better signal on every risk-adjusted axis: Sharpe 0.55 vs 0.47, volatility 2.4% vs 5.0%, max drawdown -6.0% vs -12.3% — half the risk, more reward per unit of it. But the filtered spread mean-reverts fast, so it trades 152 round trips to the static variant's 57 — and at 10 bp per unit of traded notional, that turnover consumes the entire edge and then some.

0.90.9511.051.11.151.21.25'11'13'15'17'19'21'23
Strategy equity, net of 10 bp costs — the cost drag on 2.7× turnover flips the rankingKalman βstatic β
VariantAnn ret (net)Ann volSharpe (net)Sharpe (gross)Max DD (net)Trades
Kalman β (adaptive)-0.9%2.3%-0.380.55-12.6%152
Static β (full-sample)1.5%5.0%0.310.47-13.3%57

So did the Kalman beta “improve” the strategy? As an estimator, unambiguously — better gross Sharpe, half the drawdown, and no lookahead, against a static baseline that was handed the answer key. As a net P&L line at retail costs, no: the same adaptivity that tracks the relationship also generates signals faster than 10 bp round trips can pay for. Halve the cost and the gap halves; at institutional frictions of 1–2 bp the Kalman variant pulls level and ahead. Estimation quality and implementability are different axes, and a backtest that reports only one is hiding the other.

References

  1. 1.Kalman, R. E. (1960). A New Approach to Linear Filtering and Prediction Problems. Journal of Basic Engineering, 82(1).
  2. 2.Engle, R. F. & Granger, C. W. J. (1987). Co-integration and Error Correction: Representation, Estimation, and Testing. Econometrica, 55(2), 251–276.
  3. 3.Chan, E. (2013). Algorithmic Trading: Winning Strategies and Their Rationale. Wiley — ch. 3, the EWA/EWC Kalman example and the δ/(1−δ) parameterization.
  4. 4.Harvey, A. C. (1989). Forecasting, Structural Time Series Models and the Kalman Filter. Cambridge University Press.
  5. 5.Companion notebook: kalman-filter-hedge-ratios.ipynb — reproduces every figure from raw data; fully deterministic, no RNG.