2026-02-18 20:30:15 +01:00
# API Reference: Hidden Markov Model (HMM)
The `HMM` class provides a complete implementation of Hidden Markov Models with Gaussian emissions for regime detection, time series modeling, and state inference.
## Quick Start
2026-02-09 16:15:41 +01:00
```python
from optimizr import HMM
2026-02-18 20:30:15 +01:00
import numpy as np
2026-02-09 16:15:41 +01:00
2026-02-18 20:30:15 +01:00
# Create model with 2 hidden states (e.g., bull/bear market)
2026-02-09 16:15:41 +01:00
model = HMM ( n_states = 2 )
2026-02-18 20:30:15 +01:00
# Train on returns data
returns = np . random . randn ( 1000 , 1 ) # Should be 2D: (n_samples, n_features)
model . fit ( returns , n_iterations = 100 , tolerance = 1e-6 )
# Decode most likely state sequence (Viterbi)
states = model . predict ( returns )
# Compute log-likelihood (for model comparison)
logp = model . score ( returns )
print ( f "Log-likelihood: { logp : .2f } " )
print ( f "Decoded states: { states [: 10 ] } " )
```
## Constructor
### `HMM(n_states: int)`
Creates a new Hidden Markov Model with Gaussian emissions.
**Parameters:**
- `n_states` (int): Number of hidden states/regimes. Common choices:
- `n_states=2` : Binary regime (e.g., bull/bear, high/low volatility)
- `n_states=3` : Three-regime model (e.g., bull/sideways/bear)
- `n_states>3` : Fine-grained regime detection (requires more data)
**Returns:**
- `HMM` object with random initialization
**Initialization:**
- Transition matrix $A$: Uniform with slight self-transition bias
- Initial state distribution $\pi$: Uniform
- Emission parameters (means $\mu_i$, covariances $\Sigma_i$): From K-means clustering
**Example:**
```python
# Binary regime model
hmm_2 = HMM ( n_states = 2 )
# Three-regime model for more nuanced detection
hmm_3 = HMM ( n_states = 3 )
```
**When to use:**
- `n_states=2` : Most common, sufficient for many applications
- Higher `n_states` : When you have strong prior belief in multiple regimes and sufficient data (>1000 samples per state)
## Methods
### `fit(X, n_iterations=100, tolerance=1e-6, n_init=1, random_state=None)`
Trains the HMM on observed data using the Baum-Welch (Expectation-Maximization) algorithm.
**Parameters:**
- `X` (np.ndarray): Training data of shape `(n_samples, n_features)`
- For univariate time series: reshape to `(n, 1)` with `X.reshape(-1, 1)`
- For multivariate: pass directly as `(n, d)` where `d` is feature dimension
- `n_iterations` (int, default=100): Maximum number of EM iterations
- Typical range: 50-200
- More iterations → better convergence but slower training
- `tolerance` (float, default=1e-6): Convergence threshold
- Algorithm stops when log-likelihood improvement < `tolerance`
- Typical range: 1e-8 to 1e-4
- Smaller values → tighter convergence but more iterations
- `n_init` (int, default=1): Number of random initializations
- The best model (highest log-likelihood) is kept
- Recommended: 5-10 for production models (helps avoid local minima)
- `random_state` (int, optional): Random seed for reproducibility
**Returns:**
- `self` : The fitted HMM object (for method chaining)
**Algorithm: Baum-Welch (EM for HMMs)**
The Baum-Welch algorithm iteratively refines model parameters:
1. **E-step** : Compute state occupation probabilities
- Forward pass: $\alpha_t(i) = P(O_1, \ldots, O_t, S_t = i \mid \lambda)$
- Backward pass: $\beta_t(i) = P(O_{t+1}, \ldots, O_T \mid S_t = i, \lambda)$
- State probabilities: $\gamma_t(i) = \frac{\alpha_t(i)\beta_t(i)}{\sum_j \alpha_t(j)\beta_t(j)}$
- Transition probabilities: $\xi_t(i,j) = \frac{\alpha_t(i)a_{ij}b_j(O_{t+1})\beta_{t+1}(j)}{\sum_{i,j}\alpha_t(i)a_{ij}b_j(O_{t+1})\beta_{t+1}(j)}$
2. **M-step** : Update model parameters
- Initial probabilities: $\pi_i = \gamma_1(i)$
- Transition matrix: $a_{ij} = \frac{\sum_{t=1}^{T-1}\xi_t(i,j)}{\sum_{t=1}^{T-1}\gamma_t(i)}$
- Emission means: $\mu_i = \frac{\sum_{t=1}^T \gamma_t(i) O_t}{\sum_{t=1}^T \gamma_t(i)}$
- Emission covariances: $\Sigma_i = \frac{\sum_{t=1}^T \gamma_t(i)(O_t - \mu_i)(O_t - \mu_i)^T}{\sum_{t=1}^T \gamma_t(i)}$
3. **Convergence** : Repeat until log-likelihood change < tolerance
**Example:**
```python
import numpy as np
from optimizr import HMM
# Simulate two-regime data
np . random . seed ( 42 )
n = 2000
# Regime 1: low volatility (first 1000 samples)
regime1 = np . random . normal ( 0.0 , 0.5 , 1000 )
# Regime 2: high volatility (last 1000 samples)
regime2 = np . random . normal ( 0.0 , 2.0 , 1000 )
data = np . concatenate ([ regime1 , regime2 ]) . reshape ( - 1 , 1 )
# Train HMM
hmm = HMM ( n_states = 2 )
hmm . fit ( data , n_iterations = 200 , tolerance = 1e-6 , n_init = 5 )
print ( "Training complete" )
```
**Convergence diagnostics:**
```python
# Plot log-likelihood over iterations (requires storing history)
# Check if converged before max_iter
# Verify parameters make sense (e.g., distinct means for each state)
```
**Typical training time:**
- 1000 samples, 2 states, 100 iterations: ~50-100ms
- 10000 samples, 3 states, 200 iterations: ~500ms-1s
### `predict(X)`
Decodes the most likely sequence of hidden states using the Viterbi algorithm.
**Parameters:**
- `X` (np.ndarray): Observation sequence of shape `(n_samples, n_features)`
- Must match feature dimension used in `fit()`
**Returns:**
- `states` (np.ndarray): Most likely state sequence of shape `(n_samples,)`
- Values are integers in range `[0, n_states-1]`
**Algorithm: Viterbi**
The Viterbi algorithm finds the globally optimal state sequence:
1. **Initialization** : $\delta_1(i) = \pi_i \cdot b_i(O_1)$
2. **Recursion** : $\delta_t(j) = \max_i[\delta_{t-1}(i) \cdot a_{ij}] \cdot b_j(O_t)$
3. **Termination** : $P^* = \max_i[\delta_T(i)]$
4. **Backtracking** : Trace back from $\arg\max_i[\delta_T(i)]$ to recover state sequence
**Complexity:** $O(T \cdot K^2)$ where $T$ is sequence length, $K$ is number of states
**Example:**
```python
# After training (see fit() example)
states = hmm . predict ( data )
# Analyze regime distribution
unique , counts = np . unique ( states , return_counts = True )
for state , count in zip ( unique , counts ):
print ( f "State { state } : { count } samples ( { count / len ( states ) * 100 : .1f } %)" )
# Identify regime switches
switches = np . where ( np . diff ( states ) != 0 )[ 0 ]
print ( f "Number of regime switches: { len ( switches ) } " )
# Use for trading: buy in regime 0, sell in regime 1
current_state = states [ - 1 ]
if current_state == 0 :
print ( "Signal: BUY (low volatility regime)" )
else :
print ( "Signal: SELL (high volatility regime)" )
```
**Use cases:**
- **Regime detection**: Identify market states (bull/bear, high/low vol)
- **Trading signals**: Generate buy/sell signals based on regime
- **Risk management**: Adjust position size based on estimated regime
- **Anomaly detection**: Flag unusual regime transitions
### `score(X)`
Computes the log-likelihood of the observation sequence under the fitted model.
**Parameters:**
- `X` (np.ndarray): Observation sequence of shape `(n_samples, n_features)`
**Returns:**
- `logp` (float): Log-likelihood $\log P(O \mid \lambda)$
**Algorithm: Forward Algorithm**
The forward algorithm efficiently computes the likelihood:
1. **Initialization** : $\alpha_1(i) = \pi_i \cdot b_i(O_1)$
2. **Induction** : $\alpha_t(j) = \left[\sum_{i=1}^K \alpha_{t-1}(i) \cdot a_{ij}\right] \cdot b_j(O_t)$
3. **Termination** : $P(O \mid \lambda) = \sum_{i=1}^K \alpha_T(i)$
**Numerical stability:** Uses log-space computation with scaling to avoid underflow.
**Example:**
```python
# Model comparison: which number of states fits best?
logp_scores = {}
for n_states in [ 2 , 3 , 4 ]:
hmm = HMM ( n_states = n_states )
hmm . fit ( data , n_iterations = 100 )
logp = hmm . score ( data )
logp_scores [ n_states ] = logp
print ( f " { n_states } states: log-likelihood = { logp : .2f } " )
# Higher log-likelihood is better (but watch for overfitting)
best_k = max ( logp_scores , key = logp_scores . get )
print ( f "Best model: { best_k } states" )
# Use BIC for model selection (penalizes complexity)
def bic ( logp , n_params , n_samples ):
return - 2 * logp + n_params * np . log ( n_samples )
n_samples = len ( data )
for n_states in [ 2 , 3 , 4 ]:
n_params = n_states ** 2 + 2 * n_states # Approx: A, pi, means, variances
bic_score = bic ( logp_scores [ n_states ], n_params , n_samples )
print ( f " { n_states } states: BIC = { bic_score : .2f } " )
```
**Use cases:**
- **Model selection**: Compare models with different `n_states` using BIC/AIC
- **Convergence monitoring**: Track log-likelihood during training
- **Outlier detection**: Low likelihood → data doesn't match model
- **Model reliability**: Higher likelihood → better fit (but watch overfitting)
## Complete Example: Market Regime Detection
Here's a complete workflow for detecting market regimes in financial data:
```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from optimizr import HMM
# 1. Load financial data (example: S&P 500 returns)
# In practice, load from your data source
np . random . seed ( 42 )
n_samples = 2000
# Simulate returns with regime changes
returns = []
for i in range ( n_samples ):
if i < 500 : # Bull market
returns . append ( np . random . normal ( 0.001 , 0.01 ))
elif i < 1000 : # Correction
returns . append ( np . random . normal ( - 0.002 , 0.02 ))
elif i < 1500 : # Recovery
returns . append ( np . random . normal ( 0.001 , 0.015 ))
else : # Bear market
returns . append ( np . random . normal ( - 0.001 , 0.025 ))
returns = np . array ( returns ) . reshape ( - 1 , 1 )
# 2. Train HMM with multiple initializations
print ( "Training HMM..." )
hmm = HMM ( n_states = 3 ) # 3 regimes: bull, neutral, bear
hmm . fit ( returns , n_iterations = 200 , tolerance = 1e-6 , n_init = 10 )
# 3. Decode regimes
states = hmm . predict ( returns )
# 4. Analyze regimes
print ( " \n Regime Statistics:" )
for state_id in range ( 3 ):
mask = ( states == state_id )
state_returns = returns [ mask ]
mean_ret = np . mean ( state_returns )
std_ret = np . std ( state_returns )
count = np . sum ( mask )
print ( f "State { state_id } :" )
print ( f " Count: { count } ( { count / len ( returns ) * 100 : .1f } %)" )
print ( f " Mean return: { mean_ret : .4f } " )
print ( f " Volatility: { std_ret : .4f } " )
print ( f " Sharpe (annualized): { mean_ret / std_ret * np . sqrt ( 252 ) : .2f } " )
# 5. Identify regime switches
switches = np . where ( np . diff ( states ) != 0 )[ 0 ] + 1
print ( f " \n Regime switches: { len ( switches ) } " )
print ( f "Average regime duration: { len ( returns ) / len ( switches ) : .1f } days" )
# 6. Visualize regimes
plt . figure ( figsize = ( 14 , 8 ))
# Plot returns with regime colors
plt . subplot ( 3 , 1 , 1 )
colors = [ 'green' , 'yellow' , 'red' ]
for state_id in range ( 3 ):
mask = ( states == state_id )
plt . scatter ( np . where ( mask )[ 0 ], returns [ mask ],
c = colors [ state_id ], alpha = 0.5 , s = 10 ,
label = f 'State { state_id } ' )
plt . ylabel ( 'Returns' )
plt . title ( 'Returns colored by HMM regime' )
plt . legend ()
plt . grid ( True , alpha = 0.3 )
# Plot cumulative returns per regime
plt . subplot ( 3 , 1 , 2 )
cumulative = np . cumsum ( returns . flatten ())
plt . plot ( cumulative , color = 'black' , linewidth = 1 )
for switch in switches :
plt . axvline ( switch , color = 'red' , alpha = 0.3 , linestyle = '--' )
plt . ylabel ( 'Cumulative Returns' )
plt . title ( 'Cumulative returns with regime switches' )
plt . grid ( True , alpha = 0.3 )
# Plot state sequence
plt . subplot ( 3 , 1 , 3 )
plt . plot ( states , linewidth = 0.5 )
plt . ylabel ( 'State' )
plt . xlabel ( 'Time' )
plt . title ( 'Decoded state sequence' )
plt . yticks ( range ( 3 ))
plt . grid ( True , alpha = 0.3 )
plt . tight_layout ()
plt . savefig ( 'hmm_regime_detection.png' , dpi = 150 )
print ( " \n Plot saved to hmm_regime_detection.png" )
# 7. Generate trading signals
current_state = states [ - 1 ]
state_returns = returns [ states == current_state ]
expected_return = np . mean ( state_returns )
expected_vol = np . std ( state_returns )
print ( f " \n Current regime: State { current_state } " )
print ( f "Expected return: { expected_return : .4f } " )
print ( f "Expected volatility: { expected_vol : .4f } " )
if expected_return > 0.0005 :
signal = "BUY"
position_size = 1.0
elif expected_return < - 0.0005 :
signal = "SELL"
position_size = 0.0
else :
signal = "HOLD"
position_size = 0.5
print ( f "Trading signal: { signal } " )
print ( f "Recommended position size: { position_size * 100 : .0f } %" )
```
## Advanced Usage
### Model Selection with BIC
Choose the optimal number of states using Bayesian Information Criterion:
```python
from optimizr import HMM
import numpy as np
def bic_score ( hmm , X ):
"""Compute BIC for HMM: BIC = -2*log(L) + k*log(n)"""
logp = hmm . score ( X )
n_states = hmm . n_states # Assuming this attribute exists
n_features = X . shape [ 1 ]
# Parameters: transition matrix + initial prob + means + covariances
k = n_states ** 2 + n_states + n_states * n_features + n_states * n_features ** 2
n = X . shape [ 0 ]
return - 2 * logp + k * np . log ( n )
# Test different numbers of states
results = []
for n_states in range ( 2 , 6 ):
hmm = HMM ( n_states = n_states )
hmm . fit ( data , n_iterations = 100 , n_init = 5 )
bic = bic_score ( hmm , data )
logp = hmm . score ( data )
results . append (( n_states , logp , bic ))
print ( f " { n_states } states: log-likelihood= { logp : .2f } , BIC= { bic : .2f } " )
# Best model has lowest BIC
best_n_states = min ( results , key = lambda x : x [ 2 ])[ 0 ]
print ( f " \n Best model: { best_n_states } states" )
```
### Integration with Optimal Control
Combine HMM regime detection with regime-specific optimal control:
```python
from optimizr import HMM , estimate_ou_params_py , solve_hjb_py
# 1. Detect regimes with HMM
returns = np . diff ( spread )
hmm = HMM ( n_states = 2 )
hmm . fit ( returns . reshape ( - 1 , 1 ), n_iterations = 100 )
regimes = hmm . predict ( returns . reshape ( - 1 , 1 ))
# 2. Estimate OU parameters per regime
thresholds = {}
for regime_id in range ( 2 ):
mask = ( regimes == regime_id )
spread_regime = spread [ 1 :][ mask ] # Align with returns
# Estimate OU parameters
kappa , theta , sigma , half_life = estimate_ou_params_py (
spread_regime , dt = 1 / 252
)
# Solve HJB for regime-specific thresholds
lower , upper , _ , _ = solve_hjb_py (
kappa = kappa , theta = theta , sigma = sigma ,
rho = 0.04 , transaction_cost = 0.001
)
thresholds [ regime_id ] = ( lower , upper )
print ( f "Regime { regime_id } : κ= { kappa : .2f } , thresholds=( { lower : .3f } , { upper : .3f } )" )
# 3. Apply regime-aware trading
current_regime = regimes [ - 1 ]
lower , upper = thresholds [ current_regime ]
current_spread = spread [ - 1 ]
if current_spread < lower :
action = "BUY"
elif current_spread > upper :
action = "SELL"
else :
action = "HOLD"
print ( f " \n Current regime: { current_regime } " )
print ( f "Current spread: { current_spread : .3f } " )
print ( f "Thresholds: ( { lower : .3f } , { upper : .3f } )" )
print ( f "Action: { action } " )
2026-02-09 16:15:41 +01:00
```
2026-02-18 20:30:15 +01:00
### Multivariate HMM
For multiple features (e.g., returns + volume + volatility):
```python
# Prepare multivariate data
returns = np . random . randn ( 1000 , 1 )
volume = np . random . randn ( 1000 , 1 )
volatility = np . random . randn ( 1000 , 1 )
# Stack features
X = np . hstack ([ returns , volume , volatility ]) # Shape: (1000, 3)
# Train multivariate HMM
hmm = HMM ( n_states = 3 )
hmm . fit ( X , n_iterations = 150 )
# Decode regimes based on all features
states = hmm . predict ( X )
# Each state now captures joint patterns in returns, volume, and volatility
```
## Best Practices
### Data Preparation
1. **Scaling** : Standardize features to similar scales
```python
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
` ``
2. **Stationarity**: Ensure time series is stationary (use returns, not prices)
` ``python
returns = np.diff(np.log(prices)) # Log returns
` ``
3. **Outlier handling**: Winsorize extreme values
` ``python
from scipy.stats import mstats
X_winsorized = mstats.winsorize(X, limits=[0.01, 0.01])
` ``
### Model Training
1. **Multiple initializations**: Use ` n_init=5-10` to avoid local minima
2. **Convergence**: Monitor log-likelihood, ensure convergence before ` max_iter`
3. **Validation**: Use held-out data to verify generalization
### Parameter Selection
1. **Number of states**: Start with 2-3, increase if necessary
2. **Iterations**: 100-200 typically sufficient
3. **Tolerance**: 1e-6 for production, 1e-4 for quick experimentation
### Practical Tips
1. **Minimum data**: Use at least 500 samples per state (1000+ for 2-state model)
2. **Regime persistence**: Check average regime duration is meaningful (not too short)
3. **Physical interpretation**: Verify decoded regimes make sense (e.g., high-vol state has higher variance)
4. **Robustness**: Test on multiple time periods, verify stability
## Troubleshooting
### Model not converging
- **Symptom**: Log-likelihood oscillating or not improving
- **Fix**: Increase ` n_iterations`; try different ` n_init`; check data scaling
### All samples assigned to one state
- **Symptom**: ` predict()` returns all 0s or all 1s
- **Fix**: Reduce ` n_states`; check data has sufficient variation; verify stationarity
### Unrealistic regime switches
- **Symptom**: State changes every few samples
- **Fix**: Add transition probability constraints (requires model extension); increase minimum regime duration
### Poor out-of-sample performance
- **Symptom**: High in-sample log-likelihood but poor predictions on new data
- **Fix**: Reduce ` n_states` (overfitting); use cross-validation; add regularization
## Performance Characteristics
### Computational Complexity
- **Training (Baum-Welch)**: $O(I \cdot T \cdot K^2)$
- $I$: number of iterations (~100-200)
- $T$: sequence length
- $K$: number of states
- **Prediction (Viterbi)**: $O(T \cdot K^2)$
- **Scoring (Forward)**: $O(T \cdot K^2)$
### Memory Requirements
- Model parameters: $O(K^2 + K \cdot d^2)$ where $d$ is feature dimension
- Forward/backward matrices: $O(T \cdot K)$
### Typical Runtimes (on modern CPU)
- Train (1000 samples, 2 states, 100 iter): ~50-100ms
- Train (10000 samples, 3 states, 200 iter): ~500ms-1s
- Predict (1000 samples, 2 states): ~5-10ms
- Score (1000 samples, 2 states): ~5-10ms
## References
### Hidden Markov Models
- **Rabiner, L. R.** (1989). A tutorial on hidden Markov models and selected applications in speech recognition. *Proceedings of the IEEE* , 77(2), 257-286.
- **Murphy, K. P.** (2012). *Machine Learning: A Probabilistic Perspective* . MIT Press. (Chapter 17: Markov and hidden Markov models)
### Financial Applications
- **Guidolin, M., & Timmermann, A.** (2008). International asset allocation under regime switching, skew, and kurtosis preferences. *The Review of Financial Studies* , 21(2), 889-935.
- **Nystrup, P., Madsen, H., & Lindström, E.** (2015). Stylised facts of financial time series and hidden Markov models in continuous time. *Quantitative Finance* , 15(9), 1531-1541.
- **Ang, A., & Bekaert, G.** (2002). Regime switches in interest rates. *Journal of Business & Economic Statistics* , 20(2), 163-182.
### Algorithms
- **Forney, G. D.** (1973). The Viterbi algorithm. *Proceedings of the IEEE* , 61(3), 268-278.
- **Baum, L. E., Petrie, T., Soules, G., & Weiss, N.** (1970). A maximization technique occurring in the statistical analysis of probabilistic functions of Markov chains. *The Annals of Mathematical Statistics* , 41(1), 164-171.
## See Also
- [HMM Algorithms ](../algorithms/hmm.md ) - Detailed mathematical foundations (Forward-Backward, Viterbi, Baum-Welch)
- [Optimal Control ](../algorithms/optimal_control.md ) - Integrate HMM regimes with optimal control
- [Optimal Control API ](optimal_control.md ) - API reference for control algorithms