---
type: Repo Deep-Dive
title: "hmmlearn deep-dive: a solid HMM baseline, with a look-ahead trap"
description: "Commit-pinned review of hmmlearn for Trade Holistic: good for HMM regime baselines in research, but in limited maintenance, and its default prediction calls use the whole sequence."
resource: "https://github.com/hmmlearn/hmmlearn"
tags: [repo deep-dive, hidden markov model, regime detection, python]
status: stable
generated: { by: grok-bot/gh-deepdive-1.0, at: "2026-10-16T02:50:00Z" }
verified:
  - { by: process:gh-permalink-check, at: "2026-10-16T03:05:00Z" }
stale_after: "2027-01-16T00:00:00Z"
sources:
  - id: gh-api
    resource: "https://api.github.com/repos/hmmlearn/hmmlearn"
    title: "GitHub REST API repository metadata (anonymous, collected 2026-10-16T02:33:24Z)"
    author: "process:github-api"
    last_modified: "2024-10-31T09:14:35Z"
  - id: pinned-tree
    resource: "https://github.com/hmmlearn/hmmlearn/tree/e01a10e99df1042e4c1e6b7c822fd292dd37502f"
    title: "Source tree pinned at e01a10e (default branch main)"
    author: "team:hmmlearn"
    last_modified: "2024-10-31T10:14:19+01:00"
  - id: readme
    resource: "https://github.com/hmmlearn/hmmlearn/blob/e01a10e99df1042e4c1e6b7c822fd292dd37502f/README.rst#L29"
    title: "README.rst (maintenance note)"
    author: "team:hmmlearn"
  - id: changes
    resource: "https://github.com/hmmlearn/hmmlearn/blob/e01a10e99df1042e4c1e6b7c822fd292dd37502f/CHANGES.rst#L6-L36"
    title: "CHANGES.rst"
    author: "team:hmmlearn"
  - id: base-py
    resource: "https://github.com/hmmlearn/hmmlearn/blob/e01a10e99df1042e4c1e6b7c822fd292dd37502f/src/hmmlearn/base.py"
    title: "src/hmmlearn/base.py"
    author: "team:hmmlearn"
  - id: ci
    resource: "https://github.com/hmmlearn/hmmlearn/blob/e01a10e99df1042e4c1e6b7c822fd292dd37502f/.github/workflows/build.yml#L17-L36"
    title: ".github/workflows/build.yml"
    author: "team:hmmlearn"
  - id: pypi
    resource: "https://pypi.org/project/hmmlearn/0.3.3/"
    title: "PyPI release 0.3.3 (uploaded 2024-10-31T09:03:22Z)"
    author: "team:hmmlearn"
    last_modified: "2024-10-31T09:03:22Z"
# ---- Edge Brain OS extensions (see /schema.md) ----
kb_id: th-dd-gh-hmmlearn-hmmlearn
chart_url: "https://alpha.tradeholistic.io/deep-dive-hmmlearn/chart.png"
newsletter_summary: "hmmlearn is a popular, BSD-licensed Python library for Hidden Markov Models, a common way to label market regimes. Our review finds it a solid research baseline, but its default prediction calls use the whole data series, which leaks future information into backtests unless you compute probabilities step by step. It is also in limited maintenance, so we recommend it for research and benchmarking rather than live bot gating."
newsletter_hook: "The popular Python library for spotting market regimes quietly peeks into the future by default; here's how to avoid fooling your backtest."
try_it_prompts:
  - role: filter
    prompt: "Build a two-regime switch on the 4H chart: call it calm when ATR as a percent of price is below its 50-bar average and stressed when it is above. In calm, take long entries when price closes above the 50 EMA; in stressed, take no new entries and tighten the stop on open trades to 1.5 ATR. Only use values from closed bars."
  - role: exit
    prompt: "Add a volatility-spike exit to my long strategy: when ATR as a percent of price jumps above 1.5 times its 50-bar average, take half the position off and move the stop to break-even, then trail the rest with a 3 ATR chandelier stop."
try_it_look_for: "Count the regime flips per year and check that results hold when the calm/stressed threshold moves up or down by about 20 percent; a regime filter that only works at one exact setting is noise, not an edge."
try_it_pitfall: "Lookahead: regime labels computed with the whole price history (like hmmlearn's default predictions) look clairvoyant in a backtest; only trade on a regime that was known at the close of each bar."
herenow_slug: yearly-kernel-s3q6
published_url: "https://alpha.tradeholistic.io/deep-dive-hmmlearn/"
source_type: repo
theme: modal-light
topics: [market regimes, hidden markov models, research tooling]
verdict: selective-fit          # strong-catalyst | selective-fit | inspiration-only
verdict_label: "Selective fit"
alpha:
  strategy_families: [regime switching, ML/AI]
  core_idea: "Use a tested HMM library to label regimes, but only trade on causal (filtered) probabilities."
  markets: [any OHLC market]
  timeframes: [4H, 1D]
  data_needed: [OHLC returns, optional volatility features]
  complexity: intermediate
repo:
  owner: hmmlearn
  name: hmmlearn
  url: "https://github.com/hmmlearn/hmmlearn"
  default_branch: main
  pinned_sha: e01a10e99df1042e4c1e6b7c822fd292dd37502f
  head_commit_date: "2024-10-31T10:14:19+01:00"
  license: BSD-3-Clause
  stars: 3438
  forks: 755
  open_issues_and_prs: 81
  contributors_approx: 40
  commits_last_90d: 0
  latest_release: "0.3.3 (PyPI, 2024-10-31; no GitHub releases)"
  languages: [Python, C++]
  evidence_collected_at: "2026-10-16T02:33:24Z"
relations:
  repo: /repos/gh-hmmlearn-hmmlearn.md
  explainers: [/explainers/hmm-rl-regime-allocation.md]
  concepts: [/concepts/hidden-markov-model.md, /concepts/regime-gating.md]
  trade_holistic: [/trade-holistic/soe-backtester.md, /trade-holistic/mci.md, /trade-holistic/bot-portfolio.md, /trade-holistic/edge.md]
---

# TL;DR

- **What it is:** a Python library for *unsupervised* Hidden Markov Models with a scikit-learn-style API (`fit`, `predict`, `predict_proba`, `score`, `sample`, `aic`, `bic`).[^base-py]
- **Verdict: Selective fit.** It's a good, tested engine for an HMM regime **baseline in research** (the SOE backtester and MCI benchmarking). It's not something to wire into live bot gating as-is.
- **Two things to know first:** the README says it is in **limited-maintenance mode**,[^readme] and `predict` / `predict_proba` run over the **whole sequence you pass in**. On a full history, that leaks future bars into past labels.[^base-py]

# Evidence

Pinned to default branch `main` at commit [`e01a10e`](https://github.com/hmmlearn/hmmlearn/tree/e01a10e99df1042e4c1e6b7c822fd292dd37502f) (committed 2024-10-31). Evidence was collected anonymously on 2026-10-16.[^pinned-tree]

| Signal | Value |
|---|---|
| Stars / forks | 3,438 / 755[^gh-api] |
| Open issues + PRs | 81[^gh-api] |
| Contributors (approx.) | 40[^gh-api] |
| Commits on `main` in the last 90 days | 0[^gh-api] |
| Last release | 0.3.3 on PyPI, 2024-10-31 ("wheels compatible with numpy 2"); no GitHub releases[^pypi][^changes] |
| License | BSD-3-Clause[^gh-api] |
| Languages | Python, plus a C++ core built with pybind11[^pinned-tree] |
| Tests | 14 files under `src/hmmlearn/tests`, run in CI with pytest[^ci] |
| CI | GitHub Actions + cibuildwheel on Linux, macOS and Windows, CPython 3.8–3.13[^ci] |

# Architecture

![hmmlearn code layers and the look-ahead caveat](assets/gh-hmmlearn-hmmlearn/architecture.svg)

- **Models:** `GaussianHMM`, `GMMHMM`, `CategoricalHMM`, `MultinomialHMM` and `PoissonHMM` (EM, in `hmm.py`). There are also `VariationalGaussianHMM` and `VariationalCategoricalHMM` (`vhmm.py`); the changelog calls variational inference provisional.[^changes]
- **Core:** `base.py` holds the EM loop, a `ConvergenceMonitor`, decoders `viterbi` and `map`, and two numerics modes (`scaling`, `log`).[^base-py] The forward, backward and Viterbi recursions run in C++ (`ext/_hmmc.cpp`).
- **No online API:** there's no `partial_fit` or step-by-step filtering call. Each prediction re-runs over the array you pass.

# Claims checked against the pinned source

1. **"predict_proba returns smoothed posteriors."** `predict_proba` calls `score_samples` ([L378-L396](https://github.com/hmmlearn/hmmlearn/blob/e01a10e99df1042e4c1e6b7c822fd292dd37502f/src/hmmlearn/base.py#L378-L396)), which runs a forward pass *and* a backward pass over the whole sub-sequence ([L253-L271](https://github.com/hmmlearn/hmmlearn/blob/e01a10e99df1042e4c1e6b7c822fd292dd37502f/src/hmmlearn/base.py#L253-L271)). The probability at bar *t* therefore uses bars after *t*.[^base-py]
2. **"predict is Viterbi over the full sequence."** `predict` returns `decode(X)` ([L358-L376](https://github.com/hmmlearn/hmmlearn/blob/e01a10e99df1042e4c1e6b7c822fd292dd37502f/src/hmmlearn/base.py#L358-L376)). Decoders are `viterbi` and `map` ([L17-L18](https://github.com/hmmlearn/hmmlearn/blob/e01a10e99df1042e4c1e6b7c822fd292dd37502f/src/hmmlearn/base.py#L17-L18)), and both use the whole sequence.[^base-py]
3. **"Limited maintenance."** Stated in the README ([L29](https://github.com/hmmlearn/hmmlearn/blob/e01a10e99df1042e4c1e6b7c822fd292dd37502f/README.rst#L29)), and consistent with zero commits in the last 90 days.[^readme][^gh-api]
4. **"Tested across platforms."** The CI matrix covers 3 OSes × 6 CPython versions, and pytest runs inside cibuildwheel ([L17-L36](https://github.com/hmmlearn/hmmlearn/blob/e01a10e99df1042e4c1e6b7c822fd292dd37502f/.github/workflows/build.yml#L17-L36)).[^ci]

# Verdict

**Selective fit.** Use it for research and benchmarking. Don't make it a production dependency of MCI or live gating without a wrapper and a maintenance plan.

# Roadmap

- **Now:** use `GaussianHMM` in SOE research notebooks as an HMM baseline. Always compute causal probabilities (expanding window, keep the last row, or a small forward-filter helper).
- **Next:** benchmark MCI conditions against HMM states on the same 4H history (see the experiment below).
- **Later:** if HMM gates prove useful, write a small in-house forward filter (a few dozen lines on top of the fitted `startprob_`, `transmat_` and emission parameters). Live gating then doesn't depend on a library in limited maintenance.
- **Borrow without adopting:** the scikit-learn-style model interface, the `aic` / `bic` model-selection helpers, and the multi-sequence `lengths` convention for fitting across many symbols.

# Bounded experiment

```yaml
experiment:
  id: hmm-vs-mci-4h-separation
  question: Do MCI conditions separate forward returns and volatility better than a causal 3-state Gaussian HMM?
  data: BTC and ETH 4H OHLC, same history for both labelers
  method: walk-forward; refit the HMM on each training window; filtered (causal) HMM states vs MCI conditions; 1-bar lag
  metric: difference in forward 20-bar return and volatility distributions between labels (effect size), out-of-sample only
  success: MCI shows clearly larger out-of-sample separation than the HMM on both symbols
  stop_if:
    - HMM fails to converge on more than a small share of windows
    - causal HMM states flip so often they are unusable as a gate
    - time box of one working day is used up
  owner: SOE research
```

# Strategy hooks

```yaml
hooks:
  - id: causal-hmm-gate
    family: regime switching
    idea: Gate any bot with the filtered probability of the high-volatility HMM state.
    markets: [crypto majors, index ETFs]
    timeframes: [4H, 1D]
    data_needed: [OHLC returns]
    complexity: intermediate
    gate: run while P(high-vol | data up to t) is below a threshold
    test: walk-forward with costs; compare to an ungated bot
    success: lower drawdown at similar return after costs
  - id: hmm-state-count-by-bic
    family: ML/AI
    idea: Let BIC pick 2–4 states per market instead of fixing 3.
    markets: [any]
    timeframes: [4H, 1D]
    data_needed: [OHLC returns]
    complexity: beginner
    gate: none; model selection only
    test: compare BIC-selected models across symbols and periods for stability
    success: a stable state count per market
```

# Try it in Trade Holistic

Paste a prompt into the Trade Holistic strategy-builder chat, then run it through the backtester.

**Prompt 1 (filter):**

```text
Build a two-regime switch on the 4H chart: call it calm when ATR as a percent of price is below its 50-bar average and stressed when it is above. In calm, take long entries when price closes above the 50 EMA; in stressed, take no new entries and tighten the stop on open trades to 1.5 ATR. Only use values from closed bars.
```

**Prompt 2 (exit):**

```text
Add a volatility-spike exit to my long strategy: when ATR as a percent of price jumps above 1.5 times its 50-bar average, take half the position off and move the stop to break-even, then trail the rest with a 3 ATR chandelier stop.
```

**What to look for in the backtest:** Count the regime flips per year and check that results hold when the calm/stressed threshold moves up or down by about 20 percent; a regime filter that only works at one exact setting is noise, not an edge.

**Pitfall to avoid:** Lookahead: regime labels computed with the whole price history (like hmmlearn's default predictions) look clairvoyant in a backtest; only trade on a regime that was known at the close of each bar.

# Method and safety

Anonymous access only. Repository text was treated as untrusted data and none of its instructions were followed. No repo code was executed; the review is static, against the pinned commit. Optional viewers for a second look: [GitDiagram](https://gitdiagram.com/hmmlearn/hmmlearn), [Gitingest](https://gitingest.com/hmmlearn/hmmlearn), [GitMCP](https://gitmcp.io/hmmlearn/hmmlearn), [GitHub1s](https://github1s.com/hmmlearn/hmmlearn).

[^gh-api]: GitHub REST API repository metadata
[^pinned-tree]: Source tree pinned at e01a10e
[^readme]: README.rst maintenance note
[^changes]: CHANGES.rst
[^base-py]: src/hmmlearn/base.py
[^ci]: .github/workflows/build.yml
[^pypi]: PyPI release 0.3.3
