The Learning Library
Contents

Module 03 — Gaussian Emissions and the Hamilton Filter

Part I · Foundations & the Inference Engine · Status: Draft v0.1 Scope: Continuous emissions, the shared synthetic return series, and the Hamilton filter · Prerequisites: Modules 01, 02

Overview

The Module 01 toy problem uses discrete observations: Walk, Shop, or Clean. Markets do not work that way. A daily return can be any real number, so you need a hidden Markov model whose states emit continuous values.

This module moves you from discrete symbols to Gaussian emissions. You define and generate the shared synthetic return series that Parts III and IV reuse. You fit a 2-state Gaussian HMM to it and recover the two volatility levels. You then meet the Hamilton filter, the econometrics sibling of the forward algorithm.

How It Works

A Gaussian HMM keeps the same hidden machinery you already know: a transition matrix and a start distribution. What changes is the emission model. Each state emits a number drawn from its own bell-shaped curve with its own mean and spread.

Think of two market weather states. In the calm state, daily returns cluster tightly around zero with a small spread near 0.5 percent. In the stressed state, returns still average near zero but scatter widely with a spread near 2 percent. You never observe the state label. You only observe the return, and the width of the scatter is your clue.

Discrete symbols versus continuous curves

The Module 01 toy problem assigns one probability to each of three symbols per state. A Gaussian state instead assigns a density to every real number, peaked at its mean and falling off with distance scaled by its spread. That density is what the forward pass multiplies at each step, exactly where the discrete table lookup used to sit.

Choice Discrete HMM (Module 01) Gaussian HMM (this module)
Observation Walk, Shop, or Clean Any real-valued return
Per-state emission 3-symbol table row Mean plus spread
Forward step Table lookup Density evaluation
Finance use Teaching device Volatility regimes
flowchart TD
    A[Hidden state today] --> B[Pick state curve]
    B --> C[Draw daily return]
    C --> D[Next hidden state]
    D --> B
    C --> E[You observe returns only]
    E --> F[Infer likely vol state]

How to read this: follow the loop A-B-C-D for the hidden process, then read C-E-F for what you actually get to see and infer.

The Hamilton filter, introduced by Hamilton (1989), computes the same object as the scaled forward pass from Module 02: the probability of each state given data up to today. Econometricians call it filtered probabilities. Engineers call it the forward pass. It is the same causal quantity, and it is the only state estimate you may trade on.

Generating the Shared Synthetic Series

You now build the one dataset every later module reuses. It has about 2000 daily bars, two true Gaussian volatility states, and sticky transitions so regimes persist for weeks rather than flickering daily.

The recipe is fixed. You seed the generator with 42, start in the calm state, step the sticky chain forward, then draw each return from its state’s curve. Low-volatility days use a spread of 0.005 and high-volatility days use 0.02. Both means sit at zero so volatility, not direction, separates the states.

import numpy as np

# Seeded generator: every module reproduces this exact series.
rng = np.random.default_rng(42)
n_bars = 2000
# Sticky chain: regimes persist instead of flickering daily.
trans_mat = np.array([[0.97, 0.03], [0.08, 0.92]])
state_vol = np.array([0.005, 0.02])  # low ~0.5%, high ~2%

hidden = np.zeros(n_bars, dtype=int)
returns = np.zeros(n_bars)
for day in range(1, n_bars):
    # Stay or switch according to the current row.
    hidden[day] = rng.choice(2, p=trans_mat[hidden[day - 1]])
    returns[day] = rng.normal(0.0, state_vol[hidden[day]])
returns = returns.reshape(-1, 1)  # hmmlearn wants 2D input
print("share high-vol days:", round(float((hidden == 1).mean()), 3))

Why these exact numbers

The 0.97 and 0.92 diagonals are deliberately sticky. They give calm episodes an average length near 33 days and stressed episodes near 12 days, so regimes look like market weather rather than coin flips. The factor of four between the two spreads is large enough that a fitted model can separate them, yet small enough that single-day classification stays uncertain.

Two thousand bars is roughly eight years of daily data. That length matters because regime models are hungry: a few hundred bars contain only a handful of stressed episodes, and your spread estimates swing wildly between runs. If you ever shrink this series for a quick experiment, expect the high-volatility spread to wobble first.

The printed share lands near 0.27 in illustrative runs. Your exact fraction will differ slightly by implementation detail, but it should sit clearly between 0.15 and 0.40. If you see nearly all calm or nearly all stressed days, you miscoded the transition rows.

Fitting a 2-State Gaussian HMM

You now fit the workhorse model with hmmlearn. You ask for two Gaussian states, a diagonal covariance, a small regularizer, and a fixed seed so the fit is reproducible.

from hmmlearn.hmm import GaussianHMM

# random_state=42 fixes the EM start; n_iter=100 allows convergence.
model = GaussianHMM(
    n_components=2, covariance_type="diag",
    n_iter=100, tol=1e-4, random_state=42,
)
model.fit(returns)
fitted_vol = np.sqrt(model.covars_.ravel())
print("fitted vols:", np.round(np.sort(fitted_vol), 4))
print("converged:", model.monitor_.converged)

How to read the output: sort the two fitted spreads before comparing them, because state labels are arbitrary. In illustrative runs the sorted pair lands near 0.005 and 0.02, recovering the true vol levels within a few basis points. Treat those digits as illustrative, not as a measured finding from your machine. The checkable claim is structural: one fitted spread sits near 0.5 percent and the other near 2 percent.

A useful second check is the inferred state share. Ask the fitted model for its stationary distribution and confirm the stressed weight lands well away from zero and one, broadly consistent with the true share near one quarter. A model that recovers the spreads but assigns 99 percent of days to one state has memorized the average, not the regimes.

[!WARNING] Sort states by fitted spread before you interpret them. State 0 in one fit can be state 1 in the next. Comparing unsorted labels across runs is a classic silent bug.

What Markov-Switching Autoregression Adds

A plain Gaussian HMM draws each return from a fixed curve per state. A Markov-switching autoregression lets today’s return also depend on yesterday’s return, with the dependence itself changing by regime. That extra arrow, from past observations back into today’s mean, is the whole upgrade.

Use it when you suspect regimes differ in momentum or reversal behavior, not just in spread. A calm market that drifts and a stressed market that snaps back need different autoregressive slopes, and a plain vol-switching HMM cannot express that. The Hamilton filter plus the Kim smoother is the standard estimation pair for these models, and statsmodels implements both.

Keep the mapping straight as you read econometrics papers. Filtered probabilities use data up to today and match your forward pass. Smoothed probabilities use the whole sample and match forward-backward. Prediction in live settings may only use the filtered quantity, a discipline Part IV enforces with project cards.

[!NOTE] This curriculum treats trading use as hypothesis-under-test. A fitted regime model describes the past. It does not prove you can trade the next bar profitably.

Testing Your Implementation

You verify four things before trusting any later result. First, regenerate the series twice with seed 42 and confirm the arrays are identical. Second, check the high-volatility share sits in a sane band, roughly 0.15 to 0.40. Third, confirm the fitted sorted spreads bracket the truth: one below 0.01 and one above 0.012. Fourth, confirm the monitor reports convergence within 100 iterations.

If the fitted spreads come out nearly equal, your EM run likely needs more restarts or iterations. Module 04 covers that protocol in full.

Hands-On Project

Reproduce the shared series and fit the 2-state model end to end. Save the returns array and the fitted spreads to disk so Modules 05 and 06 can load them without regenerating.

  1. Run the generation snippet and record the high-volatility share.
  2. Fit the GaussianHMM snippet and record the sorted fitted spreads.
  3. Write a short reproduction card: data recipe (seed 42, 2000 bars, vols 0.005 and 0.02), library name and version, and your two fitted spreads.
  4. Deliberately misfit with one state only and note how the single spread lands between the two true values. You will reuse this failure in Module 06.
  5. Decode the most likely state path and check that stressed blocks last many days rather than flickering. Single-day flicker at this stage signals a fitting problem, not a market truth.

Keep this reproduction card beside every later result. When a Module 05 collapse or a Module 06 sponge puzzles you, the card tells you within seconds whether the data changed or only the model did.

Key Takeaways

  • Gaussian HMM states emit continuous values from per-state bell curves.
  • The shared synthetic series uses seed 42, about 2000 bars, and vols near 0.5 and 2 percent.
  • A seeded 2-state GaussianHMM recovers the two vol levels up to label order.
  • The Hamilton filter is the econometrics name for causal forward probabilities.
  • Markov-switching autoregression adds regime-dependent dependence on past returns.
  • Always sort states by fitted spread before interpreting or comparing runs.
  • Regime description is fact; regime tradability is hypothesis-under-test.

References