---
title: "Markov Regime Detection"
description: "Real-time market regime classification using a Rust Hidden Markov Model (HMM). Baum-Welch training, Viterbi decoding, and O(N^2) online forward filter."
icon: "wave-square"
---

# Markov Regime Detection

Horizon includes a Hidden Markov Model (HMM) with Gaussian emissions, implemented entirely in Rust. It classifies the current market into regimes (e.g., calm vs. volatile) in real time, with a per-tick cost of O(N^2) where N is the number of states (typically 2-3).

<Note>
  Regime detection lets your strategy adapt its behavior to market conditions. Widen spreads in volatile regimes, reduce size in crisis regimes, or disable quoting entirely when the model signals a regime change.
</Note>

## Overview

<CardGroup cols={2}>
  <Card title="Rust HMM" icon="bolt">
    Full Baum-Welch EM training, Viterbi decoding, forward-backward smoothing. All in Rust with zero Python overhead.
  </Card>
  <Card title="O(N^2) Online Filter" icon="gauge-high">
    Per-tick forward filter costs ~9 multiplies for 3 states. Effectively zero latency added to your pipeline.
  </Card>
  <Card title="Auto-Train Mode" icon="graduation-cap">
    No pre-trained model? Collect prices during warmup and train inline. Works in both live and backtest.
  </Card>
  <Card title="Pipeline Integration" icon="diagram-project">
    Drop `hz.markov_regime()` into any pipeline. Injects regime info into `ctx.params` for downstream use.
  </Card>
</CardGroup>

---

## Quick Start

### Pre-Trained Model

Train offline on historical returns, then use in live trading:

```python
import horizon as hz

# 1. Train on historical log returns
model = hz.MarkovRegimeModel(n_states=2)
model.fit(historical_returns, max_iters=100)

# 2. Use in pipeline
hz.run(
    name="regime_mm",
    markets=["election-winner"],
    feeds={"book": hz.PolymarketBook("election-winner")},
    pipeline=[
        hz.markov_regime(model=model, feed="book"),
        my_strategy,  # ctx.params["regime"] = 0 (calm) or 1 (volatile)
    ],
)
```

### Auto-Train Mode

No historical data? Train inline during warmup:

```python
hz.run(
    pipeline=[
        hz.markov_regime(n_states=2, warmup=200, feed="book"),
        my_strategy,
    ],
    ...
)
```

The model collects 200 price ticks, computes log returns, trains the HMM, then starts classifying. Before training completes, `ctx.params["regime"]` is not set.

---

## MarkovRegimeModel (Rust)

The core HMM class. Use this directly for offline analysis, or pass it to `hz.markov_regime()` for pipeline use.

### Constructor

```python
model = hz.MarkovRegimeModel(n_states=2)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `n_states` | `int` | required | Number of hidden states (2-8) |

### fit()

Train the model using Baum-Welch EM on a series of observations (log returns).

```python
log_likelihood = model.fit(data, max_iters=100, tol=1e-6)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `data` | `list[float]` | required | Observation sequence (log returns). Min 10 observations. |
| `max_iters` | `int` | `100` | Maximum EM iterations |
| `tol` | `float` | `1e-6` | Convergence tolerance on log-likelihood |

Returns the final log-likelihood. States are automatically sorted by variance after training (state 0 = lowest variance = calmest regime).

### decode()

Find the most likely state sequence using the Viterbi algorithm.

```python
states = model.decode(data)
# [0, 0, 0, 1, 1, 1, 0, 0, ...]
```

Returns a list of state indices (0 to n_states-1) for each observation.

### filter_step()

Online forward filter. Process one observation and update state probabilities. This is the hot path for live trading.

```python
probs = model.filter_step(observation)
# [0.85, 0.15] -> 85% probability of state 0
```

Returns a list of state probabilities (sums to 1.0).

### predict()

One-step-ahead prediction: given the current filtered state, what are the probabilities for the next time step?

```python
next_probs = model.predict()
# [0.78, 0.22]
```

### smooth()

Full forward-backward smoothing on a batch of observations. More accurate than filtering alone.

```python
smoothed = model.smooth(data)
# [[0.9, 0.1], [0.85, 0.15], ...]  # one row per observation
```

### Other Methods

| Method | Returns | Description |
|--------|---------|-------------|
| `transition_matrix()` | `list[list[float]]` | N x N state transition probabilities |
| `emission_params()` | `list[tuple[float, float]]` | (mean, variance) for each state's Gaussian |
| `current_regime()` | `int` | Most likely current state from the filter |
| `filtered_probs()` | `list[float]` | Current filtered state probabilities |
| `reset_filter()` | `None` | Reset filter to initial state (for re-processing) |
| `n_states()` | `int` | Number of states |
| `is_trained()` | `bool` | Whether fit() has been called |

---

## hz.prices_to_returns

Convenience function to convert a price series to log returns.

```python
prices = [100.0, 101.0, 99.0, 102.0]
returns = hz.prices_to_returns(prices)
# [0.00995, -0.02005, 0.02985]
```

Handles zero prices gracefully (returns 0.0 for that period). Requires at least 2 prices.

---

## hz.markov_regime() Pipeline Factory

Creates a pipeline function that classifies the current regime on each tick.

```python
fn = hz.markov_regime(
    model=None,           # Pre-trained model (or None for auto-train)
    n_states=2,           # States for auto-train (ignored if model provided)
    warmup=100,           # Ticks before auto-training
    feed="book",          # Feed name to read prices from
    param_name="regime",  # Key in ctx.params
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `model` | `MarkovRegimeModel` | `None` | Pre-trained model. If None, auto-trains after warmup. |
| `n_states` | `int` | `2` | Number of states (only used if model is None) |
| `warmup` | `int` | `100` | Ticks to collect before auto-training |
| `feed` | `str` | `None` | Feed name to read price from. None = first available. |
| `param_name` | `str` | `"regime"` | Key prefix in ctx.params |

### Injected Parameters

After training and sufficient data, the function injects:

| Key | Type | Description |
|-----|------|-------------|
| `ctx.params["regime"]` | `int` | Most likely state (0 = calm, highest = volatile) |
| `ctx.params["regime_probs"]` | `list[float]` | State probabilities |
| `ctx.params["regime_vol_state"]` | `float` | P(highest-volatility state) |

---

## Examples

### Regime-Adaptive Market Maker

Widen spreads in volatile regimes:

```python
import horizon as hz

# Train on historical data
model = hz.MarkovRegimeModel(n_states=2)
model.fit(historical_returns, max_iters=100)

def adaptive_quoter(ctx):
    regime = ctx.params.get("regime", 0)
    vol_prob = ctx.params.get("regime_vol_state", 0.0)

    # Base spread widens with volatility
    base_spread = 0.04
    spread = base_spread + vol_prob * 0.06  # 4-10 cents

    # Reduce size in volatile regime
    size = 10.0 if regime == 0 else 3.0

    fair = ctx.feed.price
    return hz.quotes(fair=fair, spread=spread, size=size)

hz.run(
    name="regime_mm",
    markets=["election-winner"],
    feeds={"book": hz.PolymarketBook("election-winner")},
    pipeline=[
        hz.markov_regime(model=model, feed="book"),
        adaptive_quoter,
    ],
    risk=hz.Risk(max_position=100),
)
```

### Regime-Gated Trading

Only trade in calm regimes:

```python
def regime_gate(ctx):
    regime = ctx.params.get("regime", 0)
    if regime != 0:
        return None  # Skip quoting in volatile regime
    return True  # Pass through to next pipeline stage

hz.run(
    pipeline=[
        hz.markov_regime(n_states=3, warmup=200, feed="book"),
        regime_gate,
        my_quoter,
    ],
    ...
)
```

### Backtest with Regime Detection

```python
import horizon as hz

# Train model on historical returns
model = hz.MarkovRegimeModel(n_states=2)
model.fit(historical_returns, max_iters=100)

def regime_quoter(ctx):
    regime = ctx.params.get("regime", 0)
    spread = 0.06 if regime == 1 else 0.04
    return hz.quotes(fair=ctx.feed.price, spread=spread, size=5)

result = hz.backtest(
    data=tick_data,
    pipeline=[hz.markov_regime(model=model), regime_quoter],
)
print(result.summary())
```

### Offline Analysis

Use the HMM directly for research without a pipeline:

```python
import horizon as hz

# Fit model
model = hz.MarkovRegimeModel(n_states=3)
model.fit(returns, max_iters=200)

# Decode most likely state sequence
states = model.decode(returns)
print(f"Regime sequence: {states[:20]}")

# Inspect learned parameters
for i, (mean, var) in enumerate(model.emission_params()):
    print(f"State {i}: mean={mean:.6f}, std={var**0.5:.6f}")

# Transition matrix
tm = model.transition_matrix()
for row in tm:
    print([f"{p:.3f}" for p in row])

# Smooth for full posterior
smoothed = model.smooth(returns)
# smoothed[t] = [P(state_0|all_data), P(state_1|all_data), ...]
```

---

## Mathematical Background

<AccordionGroup>
  <Accordion title="Hidden Markov Model">
    An HMM models a system that transitions between N hidden states. At each time step:
    1. The system transitions from state i to state j with probability A[i][j] (transition matrix)
    2. In state j, it emits an observation from a Gaussian distribution N(mu_j, sigma^2_j)

    The model parameters are: initial state probabilities (pi), transition matrix (A), and emission parameters (mu, sigma^2) for each state.
  </Accordion>
  <Accordion title="Baum-Welch Training">
    The Baum-Welch algorithm (a special case of EM) iteratively:
    1. **E-step**: Run forward-backward to compute state occupation probabilities given current parameters
    2. **M-step**: Re-estimate parameters (A, mu, sigma^2) from the occupation probabilities

    Converges to a local maximum of the observation likelihood. Horizon uses quantile-based initialization to improve convergence.
  </Accordion>
  <Accordion title="Viterbi Decoding">
    The Viterbi algorithm finds the single most likely state sequence using dynamic programming. Runs in O(T * N^2) time where T is the sequence length and N is the number of states.
  </Accordion>
  <Accordion title="Online Forward Filter">
    The forward filter recursively computes P(state_t | observations_1..t):

    ```
    alpha_t(j) = sum_i [alpha_{t-1}(i) * A[i][j]] * B(j, o_t)
    ```

    Then normalize: `P(state_t = j) = alpha_t(j) / sum(alpha_t)`. This is O(N^2) per time step. For N=3 (typical), that's 9 multiplies plus normalization.
  </Accordion>
</AccordionGroup>

<Warning>
  HMMs assume stationary dynamics. If market regimes shift structurally (e.g., new regulation), retrain the model periodically. Use the auto-train mode with a reasonable warmup for adaptive behavior.
</Warning>
