An open-source, from-scratch implementation of a 2-state Gaussian Hidden Markov Model on SPX realized volatility, with a regime-aware position-sizing overlay on a long-only SPX allocation.
| Headline metric | Regime-tilted overlay: Sharpe 0.75 → 1.08 (+43.9%), max DD reduced from −33.9% to −19.4%. The HMM finds ~50/50 low-vol / high-vol days from 16 years of SPX realized vol, and a simple leverage-tilt overlay (1.5× in low-vol, 0.5× in high-vol) lifts risk-adjusted return and roughly halves drawdown. |
| Run it | python3 run.py (numpy + pandas + matplotlib + yfinance; no hmmlearn, no sklearn) |
| Headline output | results/results.json, results/figure.png, results/regime_daily.csv, results/demo_output.txt |
| Author / contact | @christianmacion26 |
vol-regime-classifier/
├── README.md ← you are here
├── memo.md ← design rationale + honest-scope notes
├── hmm.py ← from-scratch 2-state Gaussian HMM (forward / backward / Baum-Welch / Viterbi)
├── regime_overlay.py ← regime-tilted portfolio construction (1.5× / 0.5×)
├── data_loader.py ← yfinance SPX + VIX fetcher with synthetic fallback
├── plot_results.py ← 3-panel matplotlib figure
├── run.py ← single CLI entry point
├── results/ ← populated by run.py
│ ├── figure.png
│ ├── results.json
│ ├── regime_daily.csv
│ └── demo_output.txt
└── requirements.txt
Two ideas every quant researcher should be able to implement from first principles:
- Vol regimes are real and they're useful. SPX doesn't have one volatility — it has at least two. A simple 2-state HMM, fit on z-scored log realized vol, recovers sticky regimes whose transitions match what we'd eyeball in a chart.
- Even a toy regime overlay improves risk-adjusted return. Once you have a regime label per day, the dumbest possible portfolio tilt — 1.5× in low-vol, 0.5× in high-vol — improves Sharpe and roughly halves max drawdown on a 16-year out-of-sample-ish window.
The HMM itself is built from scratch on NumPy: forward-backward with scaling, Baum-Welch for parameter estimation, Viterbi for decoding. No hmmlearn, no sklearn, no scipy. The math is the deliverable.
- SPX (
^GSPC) and VIX (^VIX) daily closes viayfinance, 2010-01-01 → 2026-01-01 (or the most recent available). - Realized vol = 21-day rolling σ of SPX log returns × √252.
- Observation for the HMM = z-scored log realized vol (1-D Gaussian emissions).
- Train/test split: chronological 70/30, no shuffling.
- If yfinance fails (network, rate limit, etc.),
data_loader.pyfalls back to a synthetic 2-state mixture so the project always runs end-to-end.
- States:
low-vol(0) andhigh-vol(1), relabeled at fit-time so state 0 has the smaller emission mean. - Initial parameters per spec:
π = [0.5, 0.5]A = [[0.95, 0.05], [0.05, 0.95]]μ = [-1, +1](z-units)σ² = [1, 1]
- EM (Baum-Welch) for up to 100 iterations, stops when
|Δ log-lik| < 1e-6. - Forward / backward scaled per-time-step to avoid underflow on 4,000-step series.
- Viterbi in log-space for the most-likely state sequence.
- Baseline: 100% SPX buy-and-hold.
- Tilted: 1.5× SPX on low-vol days, 0.5× SPX on high-vol days.
- Reported: annualized Sharpe, max drawdown, total return, and hit rate (fraction of days where the tilted series outperforms the baseline).
git clone https://github.com/christianmacion26/vol-regime-classifier
cd vol-regime-classifier
python3 -m pip install -r requirements.txt # numpy, pandas, matplotlib, yfinance
python3 run.py # end-to-end, <60sThe script will:
- Fetch SPX + VIX via
yfinance(or fall back to synthetic). - Compute realized vol, z-score, split 70/30.
- Fit the HMM via Baum-Welch.
- Decode the full series via Viterbi.
- Run the regime-tilted overlay backtest.
- Print the headline metric and write
results/results.json,results/regime_daily.csv,results/figure.png, andresults/demo_output.txt.
Sample output:
data source : yfinance
train/test split : 2,802 / 1,201 (chronological 70/30)
HMM converged : True (iterations=30)
pi [low, high] : [0.0000, 1.0000]
A [from 0 -> 0,1] : [0.9812, 0.0188]
A [from 1 -> 0,1] : [0.0207, 0.9793]
low-vol days : 1,972 (49.3%)
high-vol days : 2,031 (50.7%)
HEADLINE: Regime-tilted overlay: Sharpe 0.75 -> 1.08 (+43.9%), max DD reduced from -33.9% to -19.4%
import numpy as np
import hmm as hmm_mod
# Fit on your own z-scored log-vol series
x = np.loadtxt("my_log_realized_vol.csv")
fit = hmm_mod.fit(x, n_iter=100, tol=1e-6)
states = hmm_mod.viterbi(x, fit["pi"], fit["A"], fit["mu"], fit["var"])| Baseline (SPY B&H) | Tilted (1.5× / 0.5×) | |
|---|---|---|
| Total return | +520.45% | +707.40% |
| Annualized Sharpe | 0.75 | 1.08 |
| Max drawdown | −33.92% | −19.38% |
| Hit rate (vs baseline) | — | 50.6% |
Decoded regimes:
- 49.3% low-vol days, 50.7% high-vol days
- Empirical transition matrix from decoded states ≈
[[0.98, 0.02], [0.02, 0.98]]— highly sticky, as expected.
This is a small, IP-clean re-derivation on public market data (SPX + VIX via yfinance). It demonstrates vol-regime literacy and HMM fluency; it does not claim any of the following:
- It does not implement the full Baum-Welch derivation (e.g. multiple restarts, regularization, Bayesian priors over
A). A single random seed is used. - The 1-D Gaussian emission model is intentionally simple. Real vol regimes are better fit with skewed / Student-t emissions or a multi-factor structure.
- The overlay is a toy tilt, not a strategy. There is no transaction-cost model, no slippage, no borrow, no leverage cost. The hit-rate is close to 50% because the lift comes from the asymmetry of the tilt (overweight good days, underweight bad days), not from prediction accuracy.
- The 70/30 chronological split is a sanity check, not a proper walk-forward or nested cross-validation. There's a risk that the post-2020 high-vol regime (COVID, 2022 inflation) is over-represented in the test window.
yfinancedata is end-of-day and may have survivorship issues in the broader sense; here we use index data, so this is moot for SPX itself.
What it does claim:
Given daily SPX realized vol, this project's from-scratch HMM recovers sticky low-vol / high-vol regimes; a 1.5× / 0.5× overlay on those regimes lifts Sharpe and roughly halves max drawdown over the available window.
validation-gate-stack— the SEMANTICS of how senior researchers think about a candidate; this project is the regime-detection input to gates likeg13_regime_robustness.multiple-testing-deflated-sharpe— headline application of multiple-testing corrections to Sharpe.bias-audit— the look-ahead bias shift test.
Built July 2026 by Christian Macion.