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} is a hidden Markov chain with transition matrix Pij=Pr(st=j∣st−1=i), rows summing to 1. Given st=j, yt∼N(μj,σj2) independently of the past (Hamilton 1989 used an autoregression; the i.i.d.-within-regime case keeps the algebra clean). Let Yt=(y1,…,yt), fj(y)=ϕ((y−μj)/σj)/σj, and define the vectors
- predicted probabilities ξt∣t−1(j)=Pr(st=j∣Yt−1);
- filtered probabilities ξt∣t(j)=Pr(st=j∣Yt);
- smoothed probabilities ξt∣n(j)=Pr(st=j∣Yn).
Proposition 13.15Hamilton filter
Starting from ξ1∣0=π (an initial distribution), for t=1,…,n:
ξt∣t(j)=∑kξt∣t−1(k)fk(yt)ξt∣t−1(j)fj(yt),(13.23)
ξt+1∣t=P⊤ξt∣t,(13.24)
ℓ=t=1∑nln(k∑ξt∣t−1(k)fk(yt)).(13.25)
Proof
(13.23) is Bayes' rule: Pr(st=j∣Yt)∝Pr(st=j∣Yt−1)p(yt∣st=j,Yt−1), and p(yt∣st=j,Yt−1)=fj(yt) by conditional independence. The normaliser is p(yt∣Yt−1). For (13.24), Pr(st+1=j∣Yt)=∑iPr(st+1=j∣st=i,Yt)Pr(st=i∣Yt)=∑iPijξt∣t(i), using the Markov property (st+1 depends on Yt only through st). (13.25) is the prediction-error decomposition p(Yn)=∏tp(yt∣Yt−1) with the normaliser of (13.23).∎
The usual initial distribution is the ergodic one, π⊤P=π⊤. For two states,
π0=2−p00−p111−p11,π1=2−p00−p111−p00,(13.26)
and the expected duration of a stay in state j is 1/(1−pjj) (the stay length is geometric with success probability 1−pjj).
Proposition 13.16Kim smoother
With ξn∣n from the filter, for t=n−1,…,1:
ξt∣n(i)=ξt∣t(i)j∑Pijξt+1∣t(j)ξt+1∣n(j).(13.27)
Proof
Pr(st=i∣Yn)=∑jPr(st=i∣st+1=j,Yn)Pr(st+1=j∣Yn). The key step: given st+1, the future observations yt+1,…,yn carry no further information about st, because they depend on st only through st+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) by Bayes. Substitute. Kim (1994) gives the general version.∎
Filtered versus smoothed: a look-ahead trap#
ξt∣t is a function of Yt only: you could have computed it at the close of day t. ξt∣n uses all data to n. Smoothed probabilities are sharper and look better on a chart, so a backtest that trades on ξt∣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,…,yn and asserts that ξs∣s for s≤t is bit-for-bit unchanged while ξs∣n changes.
There is a second, subtler leak. Even ξt∣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) on data up to t (expanding or rolling windows) and only then filters.
Worked example: one filter step#
Daily Nifty-like returns in percent: calm regime μ0=0.05, σ0=0.7; stressed regime μ1=−0.10, σ1=2.0; p00=0.98, p11=0.95 (illustrative). Expected durations: 50 days calm, 20 days stressed. Ergodic probabilities from (13.26): π0=0.05/0.07=0.714, π1=0.286.
Yesterday's filtered probabilities were (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).
Today the index falls 2.5%. The densities are f0(−2.5)=ϕ(−3.643)/0.7=7.48×10−4 and f1(−2.5)=ϕ(−1.2)/2.0=0.0971. Joint terms: 0.887×7.48×10−4=6.64×10−4 and 0.113×0.0971=0.01097. Their sum, p(yt∣Yt−1)=0.01164, is today's likelihood contribution. Filtered: Pr(stressed∣Yt)=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 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.