Markets: A Rigorous Course

13.5 Regime switching: the Hamilton filter

Advanced about 20 min

Motivation

Markets alternate between calm and stressed periods with different means and volatilities. A two-state model gives a probability that you are in the stressed regime today, which you can use to scale risk. The same machinery shows how easily a regime model leaks the future into a backtest.

Setup#

st∈{0,1}s_t\in\{0,1\} is a hidden Markov chain with transition matrix Pij=Pr⁡(st=j∣st−1=i)P_{ij} = \Pr(s_t = j\mid s_{t-1} = i), rows summing to 1. Given st=js_t = j, yt∼N(μj,σj2)y_t\sim N(\mu_j,\sigma_j^2) independently of the past (Hamilton 1989 used an autoregression; the i.i.d.-within-regime case keeps the algebra clean). Let Yt=(y1,…,yt)Y_t = (y_1,\dots,y_t), fj(y)=ϕ((y−μj)/σj)/σjf_j(y) = \phi((y-\mu_j)/\sigma_j)/\sigma_j, and define the vectors

  • predicted probabilities ξt∣t−1(j)=Pr⁡(st=j∣Yt−1)\xi_{t\mid t-1}(j) = \Pr(s_t = j\mid Y_{t-1});
  • filtered probabilities ξt∣t(j)=Pr⁡(st=j∣Yt)\xi_{t\mid t}(j) = \Pr(s_t = j\mid Y_t);
  • smoothed probabilities ξt∣n(j)=Pr⁡(st=j∣Yn)\xi_{t\mid n}(j) = \Pr(s_t = j\mid Y_n).

Proposition 13.15Hamilton filter

Starting from ξ1∣0=π\xi_{1\mid0} = \pi (an initial distribution), for t=1,…,nt = 1,\dots,n:

ξt∣t(j)=ξt∣t−1(j) fj(yt)∑kξt∣t−1(k) fk(yt),(13.23)\xi_{t\mid t}(j) = \frac{\xi_{t\mid t-1}(j)\,f_j(y_t)}{\sum_k \xi_{t\mid t-1}(k)\,f_k(y_t)}, \tag{13.23}
ξt+1∣t=P⊤ξt∣t,(13.24)\xi_{t+1\mid t} = P^\top\xi_{t\mid t}, \tag{13.24}
ℓ=∑t=1nln⁡(∑kξt∣t−1(k) fk(yt)).(13.25)\ell = \sum_{t=1}^n\ln\Big(\sum_k\xi_{t\mid t-1}(k)\,f_k(y_t)\Big). \tag{13.25}
Proof

(13.23) is Bayes' rule: Pr⁡(st=j∣Yt)∝Pr⁡(st=j∣Yt−1) p(yt∣st=j,Yt−1)\Pr(s_t=j\mid Y_t) \propto \Pr(s_t=j\mid Y_{t-1})\,p(y_t\mid s_t=j, Y_{t-1}), and p(yt∣st=j,Yt−1)=fj(yt)p(y_t\mid s_t=j,Y_{t-1}) = f_j(y_t) by conditional independence. The normaliser is p(yt∣Yt−1)p(y_t\mid Y_{t-1}). For (13.24), Pr⁡(st+1=j∣Yt)=∑iPr⁡(st+1=j∣st=i,Yt)Pr⁡(st=i∣Yt)=∑iPijξt∣t(i)\Pr(s_{t+1}=j\mid Y_t) = \sum_i\Pr(s_{t+1}=j\mid s_t=i, Y_t)\Pr(s_t=i\mid Y_t) = \sum_i P_{ij}\xi_{t\mid t}(i), using the Markov property (st+1s_{t+1} depends on YtY_t only through sts_t). (13.25) is the prediction-error decomposition p(Yn)=∏tp(yt∣Yt−1)p(Y_n) = \prod_t p(y_t\mid Y_{t-1}) with the normaliser of (13.23).∎

The usual initial distribution is the ergodic one, π⊤P=π⊤\pi^\top P = \pi^\top. For two states,

π0=1−p112−p00−p11,π1=1−p002−p00−p11,(13.26)\pi_0 = \frac{1-p_{11}}{2-p_{00}-p_{11}}, \qquad \pi_1 = \frac{1-p_{00}}{2-p_{00}-p_{11}}, \tag{13.26}

and the expected duration of a stay in state jj is 1/(1−pjj)1/(1-p_{jj}) (the stay length is geometric with success probability 1−pjj1-p_{jj}).

Proposition 13.16Kim smoother

With ξn∣n\xi_{n\mid n} from the filter, for t=n−1,…,1t = n-1,\dots,1:

ξt∣n(i)=ξt∣t(i)∑jPij ξt+1∣n(j)ξt+1∣t(j).(13.27)\xi_{t\mid n}(i) = \xi_{t\mid t}(i)\sum_j P_{ij}\,\frac{\xi_{t+1\mid n}(j)}{\xi_{t+1\mid t}(j)}. \tag{13.27}
Proof

Pr⁡(st=i∣Yn)=∑jPr⁡(st=i∣st+1=j,Yn)Pr⁡(st+1=j∣Yn)\Pr(s_t=i\mid Y_n) = \sum_j\Pr(s_t=i\mid s_{t+1}=j, Y_n)\Pr(s_{t+1}=j\mid Y_n). The key step: given st+1s_{t+1}, the future observations yt+1,…,yny_{t+1},\dots,y_n carry no further information about sts_t, because they depend on sts_t only through st+1s_{t+1}. So Pr⁡(st=i∣st+1=j,Yn)=Pr⁡(st=i∣st+1=j,Yt)=Pr⁡(st+1=j∣st=i)Pr⁡(st=i∣Yt)/Pr⁡(st+1=j∣Yt)=Pij ξt∣t(i)/ξt+1∣t(j)\Pr(s_t=i\mid s_{t+1}=j, Y_n) = \Pr(s_t=i\mid s_{t+1}=j, Y_t) = \Pr(s_{t+1}=j\mid s_t=i)\Pr(s_t=i\mid Y_t)/\Pr(s_{t+1}=j\mid Y_t) = P_{ij}\,\xi_{t\mid t}(i)/\xi_{t+1\mid t}(j) by Bayes. Substitute. Kim (1994) gives the general version.∎

Filtered versus smoothed: a look-ahead trap#

ξt∣t\xi_{t\mid t} is a function of YtY_t only: you could have computed it at the close of day tt. ξt∣n\xi_{t\mid n} uses all data to nn. Smoothed probabilities are sharper and look better on a chart, so a backtest that trades on ξt∣n\xi_{t\mid n} shows an excellent regime-timing strategy that could never have been run. The test test_filtered_probs_use_only_past_data rewrites yt+1,…,yny_{t+1},\dots,y_n and asserts that ξs∣s\xi_{s\mid s} for s≤ts\le t is bit-for-bit unchanged while ξs∣n\xi_{s\mid n} changes.

There is a second, subtler leak. Even ξt∣t\xi_{t\mid t} is computed with parameters estimated on the full sample. If the stressed regime's volatility was estimated using March 2020, then the filter "knew" in 2019 how bad a stressed regime looks. A clean backtest re-estimates (μ,σ,P)(\mu,\sigma,P) on data up to tt (expanding or rolling windows) and only then filters.

Worked example: one filter step#

Daily Nifty-like returns in percent: calm regime μ0=0.05\mu_0 = 0.05, σ0=0.7\sigma_0 = 0.7; stressed regime μ1=−0.10\mu_1 = -0.10, σ1=2.0\sigma_1 = 2.0; p00=0.98p_{00} = 0.98, p11=0.95p_{11} = 0.95 (illustrative). Expected durations: 50 days calm, 20 days stressed. Ergodic probabilities from (13.26): π0=0.05/0.07=0.714\pi_0 = 0.05/0.07 = 0.714, π1=0.286\pi_1 = 0.286.

Yesterday's filtered probabilities were (0.9,0.1)(0.9, 0.1). Predicted for today: ξt∣t−1=P⊤(0.9,0.1)⊤=(0.9×0.98+0.1×0.05, 0.9×0.02+0.1×0.95)=(0.887,0.113)\xi_{t\mid t-1} = P^\top(0.9,0.1)^\top = (0.9\times0.98 + 0.1\times0.05,\ 0.9\times0.02 + 0.1\times0.95) = (0.887, 0.113).

Today the index falls 2.5%. The densities are f0(−2.5)=ϕ(−3.643)/0.7=7.48×10−4f_0(-2.5) = \phi(-3.643)/0.7 = 7.48\times10^{-4} and f1(−2.5)=ϕ(−1.2)/2.0=0.0971f_1(-2.5) = \phi(-1.2)/2.0 = 0.0971. Joint terms: 0.887×7.48×10−4=6.64×10−40.887\times7.48\times10^{-4} = 6.64\times10^{-4} and 0.113×0.0971=0.010970.113\times0.0971 = 0.01097. Their sum, p(yt∣Yt−1)=0.01164p(y_t\mid Y_{t-1}) = 0.01164, is today's likelihood contribution. Filtered: Pr⁡(stressed∣Yt)=0.01097/0.01164=0.943\Pr(\text{stressed}\mid Y_t) = 0.01097/0.01164 = 0.943. One bad day moves the probability from 11% to 94%, because a 2.5% fall is a 3.6σ event in the calm regime and a 1.2σ event in the stressed one. Had the index risen 0.3% instead, the filtered stress probability would fall to 0.045.

Practitioner notes

  • The likelihood is invariant to relabelling the states, so constrain σ0<σ1\sigma_0<\sigma_1 and start optimisers with distinct volatilities.
  • Regime models fit in-sample turning points beautifully and detect them in real time with a lag of several days; size the lag before trusting a "regime filter" on a strategy.
  • statsmodels.tsa.regime_switching.MarkovRegression(..., switching_variance=True) implements this model; its log-likelihood matches (13.25) with the ergodic start (checked in the tests). Note that statsmodels stores the transition matrix with columns, not rows, summing to 1.

Create a free account

Reading is open to everyone. An account keeps your progress, answers, notes and code drafts, and gives you the study plan, the journals and the simulator. Running code needs an account approved for it.