Skip to main content
Ultra Feature. Requires an Ultra subscription. Get started at api.mathematicalcompany.com
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.

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.

Streaming EM

Online EM separates diffusion and jump components without batch processing. Updates in O(1) per tick.

Noise Filtering

Bid-ask spread is treated as microstructure noise, producing cleaner vol estimates.

Jump Detection

Automatically estimates the probability that each price move was a jump vs. diffusion.

Vol Surface

Interpolated volatility surface across time-to-resolution horizons.

API

BeliefVolSurface

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
)
ParameterTypeDefaultDescription
decayfloat0.99EWMA decay factor. Higher = more memory, smoother estimates
min_observationsint20Ticks before switching from simple EWMA to full EM

surface.update

Process a new price observation. Returns the current belief vol estimate.
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

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

surface.jump_probability

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

surface.surface_point

Interpolated vol at a given time-to-resolution.
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.
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.
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

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)],
)