A 3-state Gaussian Hidden Markov Model with a full covariance matrix per state, trained via the Baum-Welch (Expectation-Maximization) algorithm using hmmlearn's GaussianHMM implementation. Hyperparameters:
n_components=3 — three hidden regimescovariance_type="full" — each state has its own full covariance matrixn_iter=200, tol=1e-4 — convergence criteriarandom_state=42 — for reproducibility; same data + same seed always produces an identical modelThree states is the right count for a regime indicator: more states tend to overfit on daily data (the model finds spurious sub-regimes that don't generalize), and fewer collapses the meaningful bull/bear distinction.
Each trading day is represented by up to four features. Features are z-scored (standardized to mean=0, std=1) before feeding into the HMM so no single feature dominates.
| Feature | Formula | Why it matters |
|---|---|---|
log_return |
ln(P[t] / P[t-1]) |
Day-over-day directional move. Captures bull/bear drift. |
realized_vol |
std(log_return, window=10) |
Backward-looking 10-day volatility. The primary axis separating regimes. |
fx_change |
ln(FX[t] / FX[t-1]) |
Local currency move vs USD (or DXY for SPX). Currency stress often leads equity stress. |
vix |
raw level (India VIX, CBOE VIX) | Forward-looking implied vol from options. Available only for Nifty + S&P. |
KOSPI and Shanghai use 3 features (no VIX). For those indices, realized_vol carries the vol signal alone — and is more than sufficient: see the regime tables on the dashboard.
The HMM returns three states numbered 0, 1, 2 — but the numbering is arbitrary and can flip between training runs. To make labels stable, after each fit we compute a composite score for each state:
score[i] = -z(mean_realized_vol[i]) + 0.25 * z(mean_log_return[i])
Where z(.) is z-scoring across the three states. States are then sorted by score:
Why vol-dominant? Returns alone barely separate regimes in long-bull markets like India — all three states tend to have positive mean returns. Volatility is the actionable axis: when vol is high, be defensive, regardless of price direction. The 0.25 weight on returns acts as a tiebreaker for ambiguous cases.
The single most important methodological choice in a live dashboard is using filtered probabilities, not smoothed ones.
P(state[t] | observations[1..t]). At each day t, the model uses only data up through that day. This is what you'd actually have if you ran the model in real time.P(state[t] | observations[1..T]). The forward-backward algorithm uses future observations to refine past probabilities. Looks great in backtests, but cheats — you can't have it live.This dashboard always shows filtered probabilities. We compute them via a direct forward-pass implementation of the log-alpha recursion:
log_alpha[t, j] = log_emission[t, j]
+ logsumexp_i( log_alpha[t-1, i] + log_transmat[i, j] )
filtered[t] = exp(log_alpha[t] - logsumexp(log_alpha[t]))
This means: today's probabilities will be revised tomorrow as new data arrives, but past displayed probabilities are never revised retroactively using future data.
Daily OHLC data is pulled from Yahoo Finance via the yfinance library:
| Index | Price ticker | FX ticker | VIX ticker | Range |
|---|---|---|---|---|
| Nifty 50 | ^NSEI | INR=X | ^INDIAVIX | Jan 2010 – today |
| S&P 500 | ^GSPC | DX-Y.NYB (DXY) | ^VIX | Jan 2010 – today |
| KOSPI | ^KS11 | KRW=X | — | Jan 2010 – today |
| Shanghai | 000001.SS | CNY=X | — | Jan 2010 – today |
All indices have ~4,000 trading days of data. Yahoo Finance is a free, unofficial data source — adequate for personal research, but commercial deployments should use a paid feed (e.g., EOD Historical Data, Alpha Vantage). The DataSource abstraction in the code makes the swap a one-file change.
| Job | When | What it does |
|---|---|---|
daily |
Mon–Fri 16:30 IST (after NSE close) | Refetches the latest day's prices/FX/VIX, recomputes features, runs filtered inference, writes today's probability row to SQLite. |
weekly |
Sundays | Retrains all four HMMs on the full dataset including the just-finished week. Saves a timestamped model backup. Rebuilds the full probability history. |
api |
Always | FastAPI server serves the dashboard, with auto-restart if it ever crashes. |
A comprehensive audit was performed across seven layers — raw data accuracy, alignment and gaps, feature math, HMM training internals, label permutation, filtered probability correctness, and database parity. All 100+ assertions passed; the math is fully reproducible and the probabilities are provably causal (verified by truncating data and confirming byte-identical past values).
Known data-quality concerns: 4 days of suspect USD/INR ticks in Jan-Feb 2012 and 2 days of suspect USD/CNY ticks in Jul 2011 from Yahoo Finance. Impact is bounded to ~2 weeks of localized regime classification near those dates and does not affect any high-confidence historical calls (COVID, 2017–18 bull, 2022 bear, etc.).