---
title: "Belief-Volatility Surface"
description: "Streaming belief-volatility estimation with online EM decomposition into diffusion and jump components. The prediction market analog of implied vol."
icon: "wave-pulse"
---

<Warning>
  **Ultra Feature.** Requires an Ultra subscription. [Get started at api.mathematicalcompany.com](https://api.mathematicalcompany.com)
</Warning>

<Tip>
  **What is this?** In options markets, implied volatility tells you how uncertain the market is. Prediction markets don't have this - until now. The belief-volatility surface estimates a "belief vol" parameter from streaming prediction market prices, decomposing price changes into gradual drift (diffusion) and sudden jumps. Use it to calibrate logit-space market making, detect regime changes, and price the uncertainty of uncertainty.
</Tip>

# Belief-Volatility Surface

Horizon's `BeliefVolSurface` is a streaming estimator that processes tick-by-tick prediction market data and produces a calibrated belief-volatility estimate. It uses online Expectation-Maximization (EM) to separate price changes into:

- **Diffusion**: gradual belief shifts (estimated as "belief vol")
- **Jumps**: sudden information shocks (estimated as jump probability)

The estimator operates in logit space and accounts for bid-ask spread as microstructure noise.

<CardGroup cols={2}>
  <Card title="Streaming EM" icon="bolt">
    Online EM separates diffusion and jump components without batch processing. Updates in O(1) per tick.
  </Card>
  <Card title="Noise Filtering" icon="filter">
    Bid-ask spread is treated as microstructure noise, producing cleaner vol estimates.
  </Card>
  <Card title="Jump Detection" icon="triangle-exclamation">
    Automatically estimates the probability that each price move was a jump vs. diffusion.
  </Card>
  <Card title="Vol Surface" icon="mountain">
    Interpolated volatility surface across time-to-resolution horizons.
  </Card>
</CardGroup>

---

## API

### BeliefVolSurface

```python
import horizon as hz

surface = hz.BeliefVolSurface(
    decay=0.99,            # EWMA decay factor (0.5-0.9999)
    min_observations=20,   # Minimum ticks before EM estimation
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `decay` | `float` | `0.99` | EWMA decay factor. Higher = more memory, smoother estimates |
| `min_observations` | `int` | `20` | Ticks before switching from simple EWMA to full EM |

### surface.update

Process a new price observation. Returns the current belief vol estimate.

```python
vol = surface.update(
    price=0.55,        # Current market price (0-1)
    timestamp=1234.5,  # Timestamp (any monotonic float)
    bid=0.54,          # Best bid (optional, for noise estimation)
    ask=0.56,          # Best ask (optional, for noise estimation)
    volume=100.0,      # Trade volume (optional, reserved for future use)
)
print(f"Current belief vol: {vol:.4f}")
```

The algorithm:
1. Converts price to logit space
2. Estimates microstructure noise from bid-ask spread: `noise_var = (logit(ask) - logit(bid))^2 / 12`
3. Computes observed variance: `delta_x^2 - noise_var`
4. Runs online EM to separate diffusion and jump components
5. Returns `sqrt(diffusion_var)` as belief vol

### surface.current_vol

```python
vol = surface.current_vol()  # Latest belief vol estimate
```

### surface.jump_probability

```python
jp = surface.jump_probability()  # EM-estimated probability of a jump
```

### surface.surface_point

Interpolated vol at a given time-to-resolution.

```python
vol_1h = surface.surface_point(time_to_resolution=1.0)    # 1 hour
vol_24h = surface.surface_point(time_to_resolution=24.0)  # 24 hours
# Scales as vol * sqrt(T / T_ref)
```

### surface.reset

Clear all state and return to initial values.

```python
surface.reset()
```

---

## Online EM Algorithm

The surface decomposes each price change into two regimes:

**E-step** (per observation):
```
P(jump | delta_x) = jump_prior * N(dx; 0, diff_var + jump_var) /
                     [jump_prior * N(...) + (1-jump_prior) * N(dx; 0, diff_var)]
```

**M-step** (EWMA update):
```
diff_var  = decay * diff_var  + (1-decay) * (1-gamma_jump) * observed_var
jump_var  = decay * jump_var  + (1-decay) * gamma_jump * observed_var
jump_prob = decay * jump_prob + (1-decay) * gamma_jump
```

This produces three outputs:
- **`belief_vol`** = `sqrt(diff_var)` - the smooth component
- **`jump_var`** - the sudden component
- **`jump_prob`** - estimated probability that any given move is a jump

---

## Pipeline Integration

### hz.belief_vol_tracker

Automatically maintains per-market `BeliefVolSurface` instances and injects estimates into `ctx.params`.

```python
hz.run(
    pipeline=[
        hz.belief_vol_tracker(feed_name="polymarket", decay=0.99, min_observations=20),
        hz.logit_market_maker(),  # Reads ctx.params["belief_vol"] automatically
    ],
    markets=[market],
    exchange=exchange,
    feeds=[hz.PolymarketBook("polymarket", market.condition_id)],
)
```

Injects into `ctx.params`:
- `"belief_vol"`: Current belief volatility estimate
- `"jump_prob"`: Estimated jump probability

The logit market maker reads `"belief_vol"` from params if present, so these two pipeline functions compose naturally.

---

## Example: Full Logit + Belief Vol Pipeline

```python
import horizon as hz

market = hz.Market(condition_id="btc-100k", ...)
exchange = hz.Polymarket(...)

hz.run(
    pipeline=[
        # 1. Estimate belief vol from live data
        hz.belief_vol_tracker("book", decay=0.99),

        # 2. Compute logit Greeks
        hz.logit_greeks_pipeline("book", t_hours=24.0),

        # 3. Market make in logit space (auto-reads belief_vol)
        hz.logit_market_maker(
            gamma=0.3,
            kappa=1.5,
            use_vpin=True,
        ),
    ],
    markets=[market],
    exchange=exchange,
    feeds=[hz.PolymarketBook("book", market.condition_id)],
)
```
