---
title: "Kalman Filters"
description: "Linear and nonlinear state estimation for real-time tracking, hedge ratio estimation, and spread trading in prediction markets."
icon: "filter"
---

<Note>
  **Pro Feature.** Requires a Pro or Ultra subscription. [Get started at api.mathematicalcompany.com](https://api.mathematicalcompany.com)
</Note>

<Tip>
  **What is this?** A Kalman filter tracks a hidden 'true' value (like fair price) from noisy observations. It smooths out market noise in real time, giving you a cleaner signal than raw prices. Use it for spread trading, hedge ratio estimation, or any time you need to filter noise from a live feed.
</Tip>

# Kalman Filters

Horizon provides two Kalman filter implementations in Rust: a standard linear `KalmanFilter` and an `UnscentedKF` for nonlinear dynamics. Both support online single-step updates for live trading and batch smoothing for offline analysis.

<CardGroup cols={2}>
  <Card title="Linear Kalman Filter" icon="chart-line">
    Standard Kalman filter for linear state-space models. Optimal for Gaussian noise.
  </Card>
  <Card title="Unscented Kalman Filter" icon="wave-square">
    Sigma-point filter for nonlinear dynamics. No Jacobians required.
  </Card>
  <Card title="Hedge Ratio Tracker" icon="scale-balanced">
    Online hedge ratio estimation via time-varying regression coefficients.
  </Card>
  <Card title="Spread Trading" icon="arrows-left-right-to-line">
    Kalman-filtered spread for pairs/stat-arb strategies on correlated markets.
  </Card>
</CardGroup>

---

## KalmanFilter

The linear Kalman filter tracks a hidden state vector through noisy observations. It is optimal (minimum variance) when the system is linear and noise is Gaussian.

### Constructor

```python
import horizon as hz

kf = hz.KalmanFilter(
    state_dim=2,        # Dimension of the state vector
    obs_dim=1,          # Dimension of the observation vector
)
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `state_dim` | `int` | Dimension of the hidden state vector |
| `obs_dim` | `int` | Dimension of the observation vector |

### Configuration Methods

Set the system matrices before running the filter. All matrices are provided as flat row-major lists.

```python
# State transition: x_{t+1} = F * x_t
kf.set_transition([[1.0, 1.0], [0.0, 1.0]])

# Observation model: z_t = H * x_t
kf.set_observation([[1.0, 0.0]])

# Process noise covariance Q
kf.set_process_noise([[0.01, 0.0], [0.0, 0.01]])

# Measurement noise covariance R
kf.set_measurement_noise([[0.1]])
```

| Method | Parameter | Type | Description |
|--------|-----------|------|-------------|
| `set_transition` | `matrix` | `list[list[float]]` | State transition matrix F (state_dim x state_dim) |
| `set_observation` | `matrix` | `list[list[float]]` | Observation matrix H (obs_dim x state_dim) |
| `set_process_noise` | `matrix` | `list[list[float]]` | Process noise Q (state_dim x state_dim) |
| `set_measurement_noise` | `matrix` | `list[list[float]]` | Measurement noise R (obs_dim x obs_dim) |

### predict()

Propagate the state forward one time step using the transition model.

```python
kf.predict()
```

### update()

Incorporate a new observation and correct the state estimate.

```python
kf.update([0.55])  # observation vector
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `observation` | `list[float]` | Observation vector (length obs_dim) |

### State Access

```python
state = kf.state()           # Current state estimate [x1, x2, ...]
cov = kf.covariance()        # State covariance matrix
ll = kf.log_likelihood()     # Cumulative log-likelihood of observations
```

| Method | Returns | Description |
|--------|---------|-------------|
| `state()` | `list[float]` | Current state estimate vector |
| `covariance()` | `list[list[float]]` | State covariance matrix P |
| `log_likelihood()` | `float` | Cumulative log-likelihood of all processed observations |

---

## UnscentedKF

The Unscented Kalman Filter handles nonlinear state transitions and observation models using sigma-point propagation. No Jacobian computation is needed.

### Constructor

```python
ukf = hz.UnscentedKF(
    state_dim=2,
    obs_dim=1,
    alpha=1e-3,       # Sigma point spread (default: 1e-3)
    beta=2.0,         # Prior distribution parameter (default: 2.0)
    kappa=0.0,        # Secondary scaling parameter (default: 0.0)
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `state_dim` | `int` | required | Dimension of the state vector |
| `obs_dim` | `int` | required | Dimension of the observation vector |
| `alpha` | `float` | `1e-3` | Controls spread of sigma points around the mean |
| `beta` | `float` | `2.0` | Incorporates prior distribution knowledge (2.0 is optimal for Gaussian) |
| `kappa` | `float` | `0.0` | Secondary scaling parameter |

The UKF supports the same `set_process_noise`, `set_measurement_noise`, `predict`, `update`, `state`, `covariance`, and `log_likelihood` methods as `KalmanFilter`. The transition and observation models are specified as nonlinear functions during construction or via configuration.

---

## Pipeline Integration

### hz.kalman_tracker

Tracks a filtered price estimate using a constant-velocity Kalman model. Injects the smoothed state into `ctx.params`.

```python
import horizon as hz

def spread_strategy(ctx):
    kf_state = ctx.params.get("kalman")
    if kf_state is None:
        return []
    filtered_price = kf_state["price"]
    velocity = kf_state["velocity"]
    raw = ctx.feed.price

    # Trade mean reversion when price deviates from filter
    deviation = raw - filtered_price
    if abs(deviation) > 0.03:
        side = "sell" if deviation > 0 else "buy"
        return hz.order(side=side, price=raw, size=10)
    return []

hz.run(
    name="kalman_mm",
    markets=["election-winner"],
    feeds={"book": hz.PolymarketBook("election-winner")},
    pipeline=[
        hz.kalman_tracker(feed="book", process_noise=0.001, measurement_noise=0.01),
        spread_strategy,
    ],
    risk=hz.Risk(max_position=100),
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `feed` | `str` | `None` | Feed name to read prices from |
| `process_noise` | `float` | `0.001` | Process noise variance Q |
| `measurement_noise` | `float` | `0.01` | Measurement noise variance R |
| `param_name` | `str` | `"kalman"` | Key in ctx.params |

### hz.kalman_hedge_ratio

Estimates a time-varying hedge ratio between two feeds using a Kalman regression model. The state tracks the intercept and slope (hedge ratio) of the linear relationship.

```python
hz.run(
    name="pairs_trader",
    markets=["market-a", "market-b"],
    feeds={
        "a": hz.PolymarketBook("token-a"),
        "b": hz.PolymarketBook("token-b"),
    },
    pipeline=[
        hz.kalman_hedge_ratio(feed_x="a", feed_y="b", process_noise=1e-4),
        pairs_strategy,  # ctx.params["hedge_ratio"], ctx.params["hedge_intercept"]
    ],
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `feed_x` | `str` | required | Feed name for the independent variable |
| `feed_y` | `str` | required | Feed name for the dependent variable |
| `process_noise` | `float` | `1e-4` | Process noise for coefficient drift |
| `measurement_noise` | `float` | `0.01` | Observation noise variance |

### hz.kalman_spread

Computes a Kalman-filtered spread between two markets and injects spread statistics for stat-arb strategies.

```python
import horizon as hz

def stat_arb(ctx):
    spread = ctx.params.get("kalman_spread")
    if spread is None:
        return []
    z_score = spread["z_score"]
    if z_score > 2.0:
        return hz.order(side="sell", price=ctx.feed.price, size=5)
    elif z_score < -2.0:
        return hz.order(side="buy", price=ctx.feed.price, size=5)
    return []

hz.run(
    name="spread_arb",
    markets=["market-a", "market-b"],
    feeds={
        "a": hz.PolymarketBook("token-a"),
        "b": hz.PolymarketBook("token-b"),
    },
    pipeline=[
        hz.kalman_spread(feed_x="a", feed_y="b", lookback=200),
        stat_arb,
    ],
)
```

| Key | Type | Description |
|-----|------|-------------|
| `ctx.params["kalman_spread"]["spread"]` | `float` | Current filtered spread value |
| `ctx.params["kalman_spread"]["z_score"]` | `float` | Z-score of the spread relative to its filtered mean and variance |
| `ctx.params["kalman_spread"]["hedge_ratio"]` | `float` | Current hedge ratio estimate |

---

## Example: Hedge Ratio Spread Trading

```python
import horizon as hz

model = hz.KalmanFilter(state_dim=2, obs_dim=1)
model.set_transition([[1.0, 0.0], [0.0, 1.0]])   # random walk coefficients
model.set_observation([[1.0, 0.0]])                 # observe intercept + beta * x
model.set_process_noise([[1e-5, 0.0], [0.0, 1e-5]])
model.set_measurement_noise([[1e-2]])

# Online hedge ratio estimation
for price_x, price_y in zip(series_x, series_y):
    model.set_observation([[1.0, price_x]])
    model.predict()
    model.update([price_y])
    intercept, beta = model.state()
    spread = price_y - beta * price_x - intercept
    print(f"beta={beta:.4f}, spread={spread:.4f}")
```

---

## Mathematical Background

<AccordionGroup>
  <Accordion title="Kalman Filter">
    The Kalman filter recursively estimates the state of a linear system:

   - **Predict**: x_hat = F * x + B * u, P = F * P * F' + Q
   - **Update**: K = P * H' * (H * P * H' + R)^(-1), x = x + K * (z - H * x), P = (I - K * H) * P

    where F is the transition matrix, H the observation matrix, Q the process noise, R the measurement noise, and K the Kalman gain.
  </Accordion>
  <Accordion title="Unscented Transform">
    The UKF propagates 2n+1 sigma points (where n is the state dimension) through the nonlinear function, then recovers the mean and covariance from the transformed points. This avoids computing Jacobians and provides second-order accuracy for Gaussian inputs.
  </Accordion>
  <Accordion title="Time-Varying Hedge Ratio">
    Modeling the hedge ratio as a Kalman state allows it to drift over time, adapting to structural changes in the relationship between two markets. This is superior to rolling OLS because the Kalman filter optimally weights old vs. new information based on the noise model.
  </Accordion>
</AccordionGroup>
