Skip to content

Commit 4bf2ba4

Browse files
Robin DENISclaude
andcommitted
feat(jupyter): I.1 Jupyter rich displays + notebook gallery
Phase I.1 of the hybrid distribution strategy (ADR-0012) — hmm-studio becomes Jupyter-native by default. Every major class now renders as a styled HTML view inline in Jupyter / IPython / VS Code notebooks / Colab. Surface added : - src/hmm_core/_jupyter.py — shared HTML rendering helpers (pure HTML + inline CSS, zero JavaScript, zero external deps) • render_matrix_heatmap (with forbidden-mask × overlay) • render_stats_table, render_sequence_strip, render_chip_list • wrap_html (CSS injection + .hmm-studio container) - _repr_html_ added to 7 classes : • Topology — mask heatmap + stats • FittedModel — fitted transmat heatmap + log-lik/BIC/AIC/converged • NHMMFittedModel — A_t mean + A_t std (covariate variability) • GMMNHMMFittedModel — A_t mean + sub-modes per regime table • FactorialNHMMFittedModel — per-chain A_t heatmaps • Pipeline — steps table with params + output split chips • PreparedResult — shape, NaN count, columns, 5-row preview Tests : 17 new in tests/test_jupyter_repr.py, all passing. Total main suite : 198/198 ✓ (was 181, +17). Notebooks gallery (notebooks/) — runnable examples for distribution : - 01_quickstart.ipynb : 30-sec tour, ergodic + left-right comparison - 02_nhmm_crypto.ipynb : covariate-driven regime detection - 03_data_prep_recipes.ipynb : bundled recipes + Python builder + sidecar - README.md : gallery philosophy + run instructions README updated : new "Quickstart in Jupyter (recommended)" section placed before the CLI tour. Positions Jupyter as the primary surface per ADR-0012. What this unlocks : - Pip-install + import + rich display, no environment switch - Slot into existing researcher workflows (Kaggle, Colab, JupyterLab) - Builds toward I.2 (sklearn-compatible API) which will leverage these HTML displays in sklearn Pipelines too Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 9aa77cb commit 4bf2ba4

13 files changed

Lines changed: 1440 additions & 0 deletions

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,42 @@ docker compose down -v # wipe volume (clears DB, uploads, results)
5656
**Desktop shortcut**: right-click `start.bat` → "Send to" → "Desktop (create
5757
shortcut)". Rename to "hmm-studio".
5858

59+
## Quickstart in Jupyter (recommended)
60+
61+
`hmm-studio` is **Jupyter-native** : every object renders as a rich HTML view
62+
inline (heatmaps, statistics tables, sequence strips). The fastest way to
63+
get started :
64+
65+
```python
66+
from hmm_core.topology import Topology, EmissionSpec, FitSpec, InitSpec
67+
from hmm_core.fit import fit
68+
import numpy as np
69+
70+
# 1. Build a topology (renders inline as HTML in Jupyter)
71+
topo = Topology(
72+
name="quickstart",
73+
n_states=3,
74+
state_names=["low", "mid", "high"],
75+
emission=EmissionSpec(type="gaussian", covariance_type="diag", n_features=1),
76+
allowed_transitions=None, # ergodic
77+
startprob="uniform",
78+
init=InitSpec(strategy="kmeans", seed=42),
79+
fit=FitSpec(algorithm="baum_welch", n_iter=100, tol=1e-4),
80+
)
81+
topo # rich HTML view
82+
83+
# 2. Fit on data (FittedModel renders heatmap + stats)
84+
X = np.random.default_rng(42).normal(size=(200, 1))
85+
result = fit(topo, X, seed=42)
86+
result # rich HTML view
87+
88+
# 3. Decode
89+
viterbi_states = result.model.predict(X)
90+
```
91+
92+
See the [notebook gallery](notebooks/) for full examples : quickstart,
93+
NHMM regime detection, data preprocessing recipes, and more.
94+
5995
## 30-second tour
6096

6197
### CLI

notebooks/01_quickstart.ipynb

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# hmm-studio — 30-second quickstart\n",
8+
"\n",
9+
"Fit a constrained Hidden Markov Model from 4 lines of Python.\n",
10+
"\n",
11+
"Every object below renders as a rich HTML view in Jupyter — heatmaps, statistics tables, color-coded sequences.\n",
12+
"\n",
13+
"**Install** : `pip install hmm-studio`\n"
14+
]
15+
},
16+
{
17+
"cell_type": "markdown",
18+
"metadata": {},
19+
"source": [
20+
"## 1. Generate some synthetic time-series data\n",
21+
"\n",
22+
"We simulate 3 latent regimes with well-separated Gaussian observations."
23+
]
24+
},
25+
{
26+
"cell_type": "code",
27+
"execution_count": null,
28+
"metadata": {},
29+
"outputs": [],
30+
"source": [
31+
"import numpy as np\n",
32+
"\n",
33+
"rng = np.random.default_rng(42)\n",
34+
"X = np.concatenate([\n",
35+
" rng.normal(0.0, 1.0, (60, 1)), # regime A\n",
36+
" rng.normal(5.0, 1.0, (60, 1)), # regime B\n",
37+
" rng.normal(-3.0, 1.0, (60, 1)), # regime C\n",
38+
"])\n",
39+
"X.shape"
40+
]
41+
},
42+
{
43+
"cell_type": "markdown",
44+
"metadata": {},
45+
"source": [
46+
"## 2. Declare a topology\n",
47+
"\n",
48+
"`Topology` describes the structure of the model : how many states, what transitions are allowed, what kind of emissions."
49+
]
50+
},
51+
{
52+
"cell_type": "code",
53+
"execution_count": null,
54+
"metadata": {},
55+
"outputs": [],
56+
"source": [
57+
"from hmm_core.topology import Topology, EmissionSpec, FitSpec, InitSpec\n",
58+
"\n",
59+
"topo = Topology(\n",
60+
" name=\"quickstart_3state\",\n",
61+
" n_states=3,\n",
62+
" state_names=[\"low\", \"mid\", \"high\"],\n",
63+
" emission=EmissionSpec(type=\"gaussian\", covariance_type=\"diag\", n_features=1),\n",
64+
" allowed_transitions=None, # ergodic — every transition allowed\n",
65+
" startprob=\"uniform\",\n",
66+
" init=InitSpec(strategy=\"kmeans\", seed=42),\n",
67+
" fit=FitSpec(algorithm=\"baum_welch\", n_iter=100, tol=1e-4),\n",
68+
")\n",
69+
"topo # rich HTML view inline below"
70+
]
71+
},
72+
{
73+
"cell_type": "markdown",
74+
"metadata": {},
75+
"source": [
76+
"## 3. Fit\n",
77+
"\n",
78+
"Constrained Baum-Welch on the data. The result is a `FittedModel` with log-likelihood, BIC/AIC, convergence info, and the fitted transition matrix."
79+
]
80+
},
81+
{
82+
"cell_type": "code",
83+
"execution_count": null,
84+
"metadata": {},
85+
"outputs": [],
86+
"source": [
87+
"from hmm_core.fit import fit\n",
88+
"\n",
89+
"result = fit(topo, X, seed=42)\n",
90+
"result"
91+
]
92+
},
93+
{
94+
"cell_type": "markdown",
95+
"metadata": {},
96+
"source": [
97+
"## 4. Decode\n",
98+
"\n",
99+
"Viterbi gives the most likely state sequence."
100+
]
101+
},
102+
{
103+
"cell_type": "code",
104+
"execution_count": null,
105+
"metadata": {},
106+
"outputs": [],
107+
"source": [
108+
"viterbi_states = result.model.predict(X)\n",
109+
"print(\"First 30 states:\", viterbi_states[:30])"
110+
]
111+
},
112+
{
113+
"cell_type": "markdown",
114+
"metadata": {},
115+
"source": [
116+
"## Try a left-right constrained topology\n",
117+
"\n",
118+
"Force progression : state must go `low → mid → high`, no back-transitions."
119+
]
120+
},
121+
{
122+
"cell_type": "code",
123+
"execution_count": null,
124+
"metadata": {},
125+
"outputs": [],
126+
"source": [
127+
"left_right = Topology(\n",
128+
" name=\"quickstart_left_right\",\n",
129+
" n_states=3,\n",
130+
" state_names=[\"low\", \"mid\", \"high\"],\n",
131+
" emission=EmissionSpec(type=\"gaussian\", covariance_type=\"diag\", n_features=1),\n",
132+
" allowed_transitions=[\n",
133+
" (\"low\", \"low\"), (\"low\", \"mid\"),\n",
134+
" (\"mid\", \"mid\"), (\"mid\", \"high\"),\n",
135+
" (\"high\", \"high\"),\n",
136+
" ],\n",
137+
" startprob=\"first_state\",\n",
138+
" init=InitSpec(strategy=\"kmeans\", seed=42),\n",
139+
" fit=FitSpec(algorithm=\"baum_welch\", n_iter=100, tol=1e-4),\n",
140+
")\n",
141+
"left_right # note the forbidden cells marked with × in the mask"
142+
]
143+
},
144+
{
145+
"cell_type": "markdown",
146+
"metadata": {},
147+
"source": [
148+
"## Next steps\n",
149+
"\n",
150+
"- **NHMM** (covariate-dependent transitions) — see `02_nhmm_crypto.ipynb`\n",
151+
"- **GMM-NHMM** (multi-modal regimes) — `src/hmm_core/gmm_nhmm.py`\n",
152+
"- **Factorial NHMM** (independent dimensions) — `src/hmm_core/factorial_nhmm.py`\n",
153+
"- **Data prep recipes** — `from hmm_core.prep import Pipeline`\n",
154+
"\n",
155+
"Full documentation : [docs/roadmap.md](../docs/roadmap.md) for the architecture and strategy."
156+
]
157+
}
158+
],
159+
"metadata": {
160+
"kernelspec": {
161+
"display_name": "Python 3",
162+
"language": "python",
163+
"name": "python3"
164+
},
165+
"language_info": {
166+
"name": "python",
167+
"version": "3.12"
168+
}
169+
},
170+
"nbformat": 4,
171+
"nbformat_minor": 5
172+
}

notebooks/02_nhmm_crypto.ipynb

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# NHMM for crypto regime detection\n",
8+
"\n",
9+
"Non-homogeneous HMM : transitions depend on external covariates. Useful when you have features (volatility, funding rate, macro indicators) that drive regime changes.\n",
10+
"\n",
11+
"**This notebook** : simulate BTC-like returns with vol-driven regime switching, fit an NHMM, inspect A_t variability across time.\n"
12+
]
13+
},
14+
{
15+
"cell_type": "markdown",
16+
"metadata": {},
17+
"source": [
18+
"## 1. Simulate regime-switching returns\n",
19+
"\n",
20+
"Two regimes (bull / bear), transition probability depends on realized volatility."
21+
]
22+
},
23+
{
24+
"cell_type": "code",
25+
"execution_count": null,
26+
"metadata": {},
27+
"outputs": [],
28+
"source": [
29+
"import numpy as np\n",
30+
"import pandas as pd\n",
31+
"\n",
32+
"rng = np.random.default_rng(0)\n",
33+
"T = 1500\n",
34+
"\n",
35+
"# Synthetic realized vol — slow-moving covariate\n",
36+
"realized_vol = rng.gamma(2, 0.5, T).cumsum() / np.arange(1, T + 1)\n",
37+
"realized_vol = (realized_vol - realized_vol.mean()) / realized_vol.std()\n",
38+
"\n",
39+
"# Generate regime + observations\n",
40+
"regime = np.zeros(T, dtype=int)\n",
41+
"for t in range(1, T):\n",
42+
" p_stay = 0.95 if realized_vol[t] < 0 else 0.7\n",
43+
" regime[t] = regime[t - 1] if rng.random() < p_stay else 1 - regime[t - 1]\n",
44+
"\n",
45+
"X = np.array([rng.normal(0.5 if r == 0 else -0.8, 1.0) for r in regime]).reshape(-1, 1)\n",
46+
"Z = realized_vol.reshape(-1, 1)\n",
47+
"\n",
48+
"print(f\"T = {T}, regime 0 share = {(regime == 0).mean():.2%}\")"
49+
]
50+
},
51+
{
52+
"cell_type": "markdown",
53+
"metadata": {},
54+
"source": [
55+
"## 2. Declare topology and fit NHMM"
56+
]
57+
},
58+
{
59+
"cell_type": "code",
60+
"execution_count": null,
61+
"metadata": {},
62+
"outputs": [],
63+
"source": [
64+
"from hmm_core.topology import Topology, EmissionSpec, FitSpec, InitSpec\n",
65+
"from hmm_core.nhmm import fit_nhmm\n",
66+
"\n",
67+
"topo = Topology(\n",
68+
" name=\"crypto_2regime_nhmm\",\n",
69+
" n_states=2,\n",
70+
" state_names=[\"bull\", \"bear\"],\n",
71+
" emission=EmissionSpec(type=\"gaussian\", covariance_type=\"diag\", n_features=1),\n",
72+
" allowed_transitions=None,\n",
73+
" startprob=\"uniform\",\n",
74+
" init=InitSpec(strategy=\"kmeans\", seed=42),\n",
75+
" fit=FitSpec(algorithm=\"baum_welch\", n_iter=100, tol=1e-4),\n",
76+
")\n",
77+
"\n",
78+
"result = fit_nhmm(topo, X, Z, covariate_names=[\"realized_vol\"], seed=42)\n",
79+
"result"
80+
]
81+
},
82+
{
83+
"cell_type": "markdown",
84+
"metadata": {},
85+
"source": [
86+
"## 3. Inspect time-varying transition matrix\n",
87+
"\n",
88+
"The HTML display above shows :\n",
89+
"- **A_t averaged over T** : the homogeneous-equivalent transition matrix\n",
90+
"- **A_t variability across t (std)** : where the covariate creates the most variation\n",
91+
"\n",
92+
"Now let's look at specific timesteps."
93+
]
94+
},
95+
{
96+
"cell_type": "code",
97+
"execution_count": null,
98+
"metadata": {},
99+
"outputs": [],
100+
"source": [
101+
"# Pick low-vol and high-vol timesteps\n",
102+
"low_vol_t = int(np.argmin(Z[:, 0]))\n",
103+
"high_vol_t = int(np.argmax(Z[:, 0]))\n",
104+
"\n",
105+
"A_low = result.A_at(low_vol_t)\n",
106+
"A_high = result.A_at(high_vol_t)\n",
107+
"\n",
108+
"print(f\"At t={low_vol_t} (low vol, z={Z[low_vol_t, 0]:.2f}) :\")\n",
109+
"print(A_low)\n",
110+
"print()\n",
111+
"print(f\"At t={high_vol_t} (high vol, z={Z[high_vol_t, 0]:.2f}) :\")\n",
112+
"print(A_high)"
113+
]
114+
},
115+
{
116+
"cell_type": "markdown",
117+
"metadata": {},
118+
"source": [
119+
"## 4. Decode the regime path"
120+
]
121+
},
122+
{
123+
"cell_type": "code",
124+
"execution_count": null,
125+
"metadata": {},
126+
"outputs": [],
127+
"source": [
128+
"decoded = result.base.model.predict(X)\n",
129+
"accuracy = (decoded == regime).mean()\n",
130+
"swap_accuracy = (decoded == 1 - regime).mean()\n",
131+
"print(f\"Decoded vs true regime accuracy : {max(accuracy, swap_accuracy):.2%}\")"
132+
]
133+
},
134+
{
135+
"cell_type": "markdown",
136+
"metadata": {},
137+
"source": [
138+
"## Next : GMM-NHMM for multi-modal regimes\n",
139+
"\n",
140+
"If each regime has internal sub-modes (e.g. bull-smooth vs bull-explosive), use `fit_gmm_nhmm` to model them with GMM emissions per state.\n",
141+
"\n",
142+
"If you have multiple independent regime dimensions (trend × volatility × macro), use `fit_factorial_nhmm` to model them as parallel chains."
143+
]
144+
}
145+
],
146+
"metadata": {
147+
"kernelspec": {
148+
"display_name": "Python 3",
149+
"language": "python",
150+
"name": "python3"
151+
},
152+
"language_info": {
153+
"name": "python",
154+
"version": "3.12"
155+
}
156+
},
157+
"nbformat": 4,
158+
"nbformat_minor": 5
159+
}

0 commit comments

Comments
 (0)