Skip to content

Repository files navigation

vol-regime-classifier

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

What's here

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

Why this exists

Two ideas every quant researcher should be able to implement from first principles:

  1. 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.
  2. 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.


Methodology

Data

  • SPX (^GSPC) and VIX (^VIX) daily closes via yfinance, 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.py falls back to a synthetic 2-state mixture so the project always runs end-to-end.

2-state Gaussian HMM

  • States: low-vol (0) and high-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.

Regime overlay

  • 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).

Quick start

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, <60s

The script will:

  1. Fetch SPX + VIX via yfinance (or fall back to synthetic).
  2. Compute realized vol, z-score, split 70/30.
  3. Fit the HMM via Baum-Welch.
  4. Decode the full series via Viterbi.
  5. Run the regime-tilted overlay backtest.
  6. Print the headline metric and write results/results.json, results/regime_daily.csv, results/figure.png, and results/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%

Use as a library

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"])

Results snapshot

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.

Honest scope

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.
  • yfinance data 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.


Related repos in this portfolio

  • validation-gate-stack — the SEMANTICS of how senior researchers think about a candidate; this project is the regime-detection input to gates like g13_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.

About

From-scratch 2-state Gaussian HMM on 16y SPX realized vol — regime-tilted overlay lifts Sharpe 0.75->1.08 (+43.9%) and cuts max DD -33.9% -> -19.4%. No hmmlearn/sklearn; pure numpy forward-backward + Baum-Welch.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages