Skip to main content

Volatility Suite

Horizon provides six volatility estimators implemented in Rust, plus a hz.volatility() pipeline function that computes all of them per cycle and injects a VolatilitySnapshot into your strategy context.
Prediction markets trade 24/7 on a 365-day calendar. All estimators use sqrt(365) for annualization by default, which differs from the sqrt(252) convention used in equity markets.

Overview

Close-to-Close

hz.estimate_volatility() - standard deviation of log returns. Simple, widely used.

Parkinson

hz.parkinson_vol() - high/low range estimator. ~5x more efficient than close-to-close.

Garman-Klass

hz.garman_klass_vol() - OHLC estimator. Most efficient standard estimator.

Yang-Zhang

hz.yang_zhang_vol() - combines overnight jump + Rogers-Satchell. Handles opening gaps.

EWMA

hz.ewma_vol() - exponentially weighted, reactive to recent changes.

Rolling

hz.rolling_vol() - windowed realized vol series for time-varying analysis.

Rust Functions

All functions return 0.0 on insufficient or invalid data. All accept an annualize parameter.

hz.estimate_volatility

Standard deviation of log returns (close-to-close).
import horizon as hz

vol = hz.estimate_volatility(
    prices=[0.50, 0.52, 0.48, 0.51, 0.49],
    annualize=True,  # default: False (backward compatible)
)

hz.parkinson_vol

Parkinson high/low range estimator. More efficient than close-to-close because it uses intra-period range information.
vol = hz.parkinson_vol(
    highs=[0.55, 0.56, 0.54],
    lows=[0.45, 0.44, 0.46],
    annualize=True,  # default: True
)

hz.garman_klass_vol

Garman-Klass OHLC estimator. The most statistically efficient standard estimator using open, high, low, close data.
vol = hz.garman_klass_vol(
    opens=[0.50, 0.52, 0.48],
    highs=[0.55, 0.56, 0.54],
    lows=[0.45, 0.44, 0.46],
    closes=[0.52, 0.48, 0.51],
    annualize=True,  # default: True
)

hz.yang_zhang_vol

Yang-Zhang estimator combining overnight jump variance, open-to-close variance, and Rogers-Satchell range variance. The most robust OHLC estimator, especially when opening gaps are present.
vol = hz.yang_zhang_vol(
    opens=[0.50, 0.52, 0.48, 0.51],
    highs=[0.55, 0.56, 0.54, 0.57],
    lows=[0.45, 0.44, 0.46, 0.43],
    closes=[0.52, 0.48, 0.51, 0.53],
    annualize=True,  # default: True
)
Yang-Zhang requires at least 2 bars (for overnight returns). Returns 0.0 with fewer.

hz.ewma_vol

Exponentially weighted moving average volatility. More reactive to recent price changes than equal-weighted estimators.
vol = hz.ewma_vol(
    prices=[0.50, 0.52, 0.48, 0.51, 0.49, 0.53],
    span=20,          # EWMA span (default: 20)
    annualize=True,   # default: True
)

hz.rolling_vol

Rolling windowed realized volatility series. Returns a list of vol values, one per window position.
vols = hz.rolling_vol(
    prices=[0.50, 0.52, 0.48, 0.51, 0.49, 0.53, 0.47],
    window=3,          # rolling window size (default: 20)
    annualize=True,    # default: True
)
# vols is a list of floats, one per window position

Pipeline Function

The hz.volatility() pipeline function computes all estimators per cycle from a feed and injects a VolatilitySnapshot into ctx.params["vol"].
import horizon as hz

def model(ctx):
    vol = ctx.params.get("vol")
    if vol and vol.best > 0:
        spread = vol.best * 2  # scale spread by vol
        return hz.quotes(fair=ctx.feeds["poly"].price, spread=spread)
    return []

hz.run(
    pipeline=[
        hz.volatility("poly", lookback=100, ewma_span=20),
        model,
    ],
    feeds={"poly": hz.PolymarketBook(token_id="0x123...")},
    markets=[hz.Market(id="0x123...", name="Example")],
)

Parameters

ParameterTypeDefaultDescription
feedstrrequiredFeed name to read prices from
lookbackint100Maximum price history length per market
ewma_spanint20EWMA span parameter
rolling_windowint20Rolling vol window size
annualizeboolTrueMultiply by sqrt(365)

VolatilitySnapshot

The snapshot injected into ctx.params["vol"] is a frozen dataclass:
from horizon import VolatilitySnapshot

snap = VolatilitySnapshot(
    realized=0.15,
    parkinson=0.12,
    garman_klass=0.11,
    yang_zhang=0.13,
    ewma=0.14,
    rolling=0.12,
)

snap.best       # 0.13 (yang_zhang - highest priority non-zero)
snap.as_dict()  # dict with all fields + "best"
Best priority order: yang_zhang > garman_klass > parkinson > ewma > realized

Choosing an Estimator

EstimatorData NeededEfficiencyBest For
Close-to-closePrices only1xTick data, simple strategies
ParkinsonHigh/Low~5xRange-based analysis
Garman-KlassOHLC~8xWhen you have proper OHLC bars
Yang-ZhangOHLC~14xMarkets with opening gaps
EWMAPricesAdaptiveReactive volatility tracking
RollingPrices1x/windowTime-varying vol analysis
The pipeline’s best property automatically selects the highest-quality available estimate.