Skip to main content
Pro Feature. Requires a Pro or Ultra subscription. Get started at api.mathematicalcompany.com
What is this? Prediction market contracts are binary options. Standard Black-Scholes pricing ignores volatility smiles, jumps, and fat tails. These three models (Heston, Merton, Variance Gamma) capture those features and produce more accurate theoretical prices. Use them to identify mispriced contracts, extract implied volatility surfaces, and build pricing-based trading signals.

Characteristic Function Pricing

Prediction market contracts are binary options: they pay 1 dollar if an event occurs and 0 otherwise. Standard Black-Scholes assumes log-normal returns, which ignores volatility smiles, jumps, and fat tails. Horizon implements three stochastic models that capture these features via characteristic function inversion: Heston (stochastic volatility), Merton (jump-diffusion), and Variance Gamma (pure-jump). All numerical integration and root-finding runs in Rust.

Heston Model

Stochastic volatility with mean reversion. Captures volatility smiles and term structure.

Merton Jump-Diffusion

Diffusion plus Poisson jumps. Captures sudden event-driven moves.

Variance Gamma

Pure-jump process with no diffusion component. Captures skewness and excess kurtosis.

Implied Volatility

hz.implied_vol_from_binary() backs out the implied volatility from an observed binary price.

Model Parameters

HestonParams

Parameters for the Heston stochastic volatility model.
import horizon as hz

params = hz.HestonParams(
    v0=0.04,       # initial variance
    kappa=2.0,     # mean reversion speed
    theta=0.04,    # long-run variance
    sigma=0.3,     # vol of vol
    rho=-0.7,      # correlation between price and vol
)
FieldTypeDescription
v0floatInitial instantaneous variance
kappafloatMean reversion speed of variance (must be positive)
thetafloatLong-run variance level (must be positive)
sigmafloatVolatility of variance (vol of vol, must be positive)
rhofloatCorrelation between asset returns and variance (-1 to 1)
The Feller condition 2 * kappa * theta > sigma^2 ensures variance stays positive. If violated, the variance process can hit zero, which is handled numerically but may produce less reliable prices.

MertonParams

Parameters for the Merton jump-diffusion model.
params = hz.MertonParams(
    sigma=0.20,       # diffusion volatility
    jump_intensity=1.0, # expected jumps per year (Poisson lambda)
    jump_mean=-0.05,   # mean log jump size
    jump_vol=0.10,     # std dev of log jump size
)
FieldTypeDescription
sigmafloatDiffusion volatility (continuous component)
jump_intensityfloatPoisson intensity (expected number of jumps per year)
jump_meanfloatMean of log-normal jump size distribution
jump_volfloatStandard deviation of log-normal jump size distribution

VGParams

Parameters for the Variance Gamma model.
params = hz.VGParams(
    sigma=0.20,   # volatility of the Brownian component
    theta=-0.10,  # drift of the Brownian component (controls skewness)
    nu=0.25,      # variance rate of the gamma time change (controls kurtosis)
)
FieldTypeDescription
sigmafloatVolatility of the subordinated Brownian motion
thetafloatDrift of the subordinated Brownian motion (negative = left skew)
nufloatVariance rate of the gamma subordinator (larger = fatter tails)

Pricing Functions

hz.binary_price_heston

Price a binary option under the Heston stochastic volatility model.
import horizon as hz

params = hz.HestonParams(v0=0.04, kappa=2.0, theta=0.04, sigma=0.3, rho=-0.7)

price = hz.binary_price_heston(
    params=params,
    s0=0.55,        # current underlying price
    strike=0.50,    # binary strike (pays $1 if S_T >= strike)
    t=30.0,         # time to expiry in days
    r=0.0,          # risk-free rate (0 for prediction markets)
)
print(f"Binary price (Heston): {price.price:.4f}")
print(f"Delta: {price.delta:.4f}")
print(f"Vega: {price.vega:.6f}")

hz.binary_price_merton

Price a binary option under the Merton jump-diffusion model.
params = hz.MertonParams(sigma=0.20, jump_intensity=1.0, jump_mean=-0.05, jump_vol=0.10)

price = hz.binary_price_merton(
    params=params,
    s0=0.55,
    strike=0.50,
    t=30.0,
    r=0.0,
)
print(f"Binary price (Merton): {price.price:.4f}")

hz.binary_price_vg

Price a binary option under the Variance Gamma model.
params = hz.VGParams(sigma=0.20, theta=-0.10, nu=0.25)

price = hz.binary_price_vg(
    params=params,
    s0=0.55,
    strike=0.50,
    t=30.0,
    r=0.0,
)
print(f"Binary price (VG): {price.price:.4f}")

Common Parameters

All three pricing functions share these parameters:
ParameterTypeDescription
paramsmodel paramsHestonParams, MertonParams, or VGParams
s0floatCurrent underlying price or probability
strikefloatBinary strike level (contract pays 1 if S_T >= strike)
tfloatTime to expiry in days
rfloatRisk-free rate (annualized). Use 0.0 for prediction markets

BinaryPrice Type

FieldTypeDescription
pricefloatFair value of the binary option (0 to 1)
deltafloatSensitivity to underlying price (dPrice/dS)
vegafloatSensitivity to volatility (dPrice/dSigma)
modelstrModel name: "heston", "merton", or "vg"

hz.implied_vol_from_binary

Back out the implied volatility from an observed binary option price using Brent’s root-finding method.
import horizon as hz

iv = hz.implied_vol_from_binary(
    observed_price=0.62,  # market price of the binary
    s0=0.55,              # current underlying
    strike=0.50,          # binary strike
    t=30.0,               # days to expiry
)
print(f"Implied volatility: {iv:.2%}")
ParameterTypeDescription
observed_pricefloatObserved market price of the binary option
s0floatCurrent underlying price
strikefloatBinary strike level
tfloatTime to expiry in days
Returns float: the annualized implied volatility. Returns NaN if root-finding fails to converge.

Comparing Models

import horizon as hz

s0 = 0.55
strike = 0.50
t = 30.0

heston = hz.binary_price_heston(
    hz.HestonParams(v0=0.04, kappa=2.0, theta=0.04, sigma=0.3, rho=-0.7),
    s0=s0, strike=strike, t=t, r=0.0,
)
merton = hz.binary_price_merton(
    hz.MertonParams(sigma=0.20, jump_intensity=1.0, jump_mean=-0.05, jump_vol=0.10),
    s0=s0, strike=strike, t=t, r=0.0,
)
vg = hz.binary_price_vg(
    hz.VGParams(sigma=0.20, theta=-0.10, nu=0.25),
    s0=s0, strike=strike, t=t, r=0.0,
)

print(f"Heston: {heston.price:.4f}  delta={heston.delta:.4f}")
print(f"Merton: {merton.price:.4f}  delta={merton.delta:.4f}")
print(f"VG:     {vg.price:.4f}  delta={vg.delta:.4f}")

Pipeline Integration

The hz.pricing_signal() pipeline function computes model-implied fair values each cycle and injects them into ctx.params["pricing"].
import horizon as hz

def model(ctx):
    pricing = ctx.params.get("pricing")
    if pricing is None:
        return []

    # Compare model fair value to market price
    market_price = ctx.feeds["poly"].price
    model_fair = pricing.price
    edge = model_fair - market_price

    if abs(edge) < 0.02:
        return []  # not enough edge

    return hz.quotes(fair=model_fair, spread=0.04, size=10)

hz.run(
    name="char-function-mm",
    markets=["election"],
    pipeline=[
        hz.pricing_signal(
            feed="poly",
            model="heston",
            params=hz.HestonParams(v0=0.04, kappa=2.0, theta=0.04, sigma=0.3, rho=-0.7),
            strike=0.50,
            horizon_days=30.0,
        ),
        model,
    ],
    feeds={"poly": hz.PolymarketBook(token_id="0x123...")},
    interval=5.0,
)

Parameters

ParameterTypeDefaultDescription
feedstrrequiredFeed name to read the current price from
modelstr"heston"Pricing model: "heston", "merton", or "vg"
paramsmodel paramsrequiredModel parameters (HestonParams, MertonParams, or VGParams)
strikefloat0.50Binary strike level
horizon_daysfloat30.0Time to expiry in days

Mathematical Background

A binary option price is P(S_T >= K) under the risk-neutral measure. This probability can be recovered from the characteristic function phi(u) of log(S_T) via the Gil-Pelaez inversion formula:P(S_T >= K) = 0.5 + (1/pi) * integral_0^inf Re[exp(-iu*ln(K)) * phi(u) / (iu)] duEach model provides a closed-form characteristic function, and the integral is evaluated numerically using adaptive Gauss-Kronrod quadrature in Rust.
The Heston (1993) model specifies:dS/S = mu*dt + sqrt(V)dW_1 dV = kappa(theta - V)dt + sigmasqrt(V)*dW_2 corr(dW_1, dW_2) = rhoThe characteristic function has a known closed form involving complex logarithms. Negative rho (the typical case) produces a left-skewed return distribution, matching the empirical observation that large downward moves are accompanied by volatility spikes.
The Merton (1976) model adds compound Poisson jumps to geometric Brownian motion:dS/S = mudt + sigmadW + (e^J - 1)*dNwhere N is a Poisson process with intensity lambda, and J ~ Normal(mu_J, sigma_J^2). The characteristic function decomposes into a diffusion part and a jump part, each with a closed form. This model is useful when prediction markets experience sudden, discrete price moves (e.g., breaking news).
The Variance Gamma (Madan, Carr, Chang 1998) model is a pure-jump process constructed by subordinating a Brownian motion with drift by a gamma process:X(t) = thetaG(t) + sigmaW(G(t))where G(t) is a gamma process with unit mean rate and variance rate nu. The VG model has three parameters controlling volatility (sigma), skewness (theta), and kurtosis (nu). It produces return distributions with heavier tails than normal and can capture the empirical observation that prediction market returns are leptokurtic.
Characteristic function pricing assumes the underlying follows the specified stochastic process. Prediction market prices are bounded in [0, 1] and may not follow any of these models exactly. Use model prices as signals (fair value estimates) rather than exact arbitrage bounds. Calibrate parameters to recent market data using hz.implied_vol_from_binary() as a starting point.