---
title: "Characteristic Function Pricing"
description: "Heston, Merton Jump-Diffusion, and Variance Gamma models for binary option pricing via characteristic function inversion. Rust-native with Python pipeline integration."
icon: "calculator"
---

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

<Tip>
  **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.
</Tip>

# 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.

<CardGroup cols={2}>
  <Card title="Heston Model" icon="chart-mixed">
    Stochastic volatility with mean reversion. Captures volatility smiles and term structure.
  </Card>
  <Card title="Merton Jump-Diffusion" icon="bolt">
    Diffusion plus Poisson jumps. Captures sudden event-driven moves.
  </Card>
  <Card title="Variance Gamma" icon="wave-pulse">
    Pure-jump process with no diffusion component. Captures skewness and excess kurtosis.
  </Card>
  <Card title="Implied Volatility" icon="magnifying-glass">
    `hz.implied_vol_from_binary()` backs out the implied volatility from an observed binary price.
  </Card>
</CardGroup>

---

## Model Parameters

### HestonParams

Parameters for the Heston stochastic volatility model.

```python
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
)
```

| Field | Type | Description |
|-------|------|-------------|
| `v0` | `float` | Initial instantaneous variance |
| `kappa` | `float` | Mean reversion speed of variance (must be positive) |
| `theta` | `float` | Long-run variance level (must be positive) |
| `sigma` | `float` | Volatility of variance (vol of vol, must be positive) |
| `rho` | `float` | Correlation between asset returns and variance (-1 to 1) |

<Note>
  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.
</Note>

### MertonParams

Parameters for the Merton jump-diffusion model.

```python
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
)
```

| Field | Type | Description |
|-------|------|-------------|
| `sigma` | `float` | Diffusion volatility (continuous component) |
| `jump_intensity` | `float` | Poisson intensity (expected number of jumps per year) |
| `jump_mean` | `float` | Mean of log-normal jump size distribution |
| `jump_vol` | `float` | Standard deviation of log-normal jump size distribution |

### VGParams

Parameters for the Variance Gamma model.

```python
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)
)
```

| Field | Type | Description |
|-------|------|-------------|
| `sigma` | `float` | Volatility of the subordinated Brownian motion |
| `theta` | `float` | Drift of the subordinated Brownian motion (negative = left skew) |
| `nu` | `float` | Variance rate of the gamma subordinator (larger = fatter tails) |

---

## Pricing Functions

### hz.binary_price_heston

Price a binary option under the Heston stochastic volatility model.

```python
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.

```python
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.

```python
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:

| Parameter | Type | Description |
|-----------|------|-------------|
| `params` | model params | `HestonParams`, `MertonParams`, or `VGParams` |
| `s0` | `float` | Current underlying price or probability |
| `strike` | `float` | Binary strike level (contract pays 1 if `S_T >= strike`) |
| `t` | `float` | Time to expiry in days |
| `r` | `float` | Risk-free rate (annualized). Use 0.0 for prediction markets |

### BinaryPrice Type

| Field | Type | Description |
|-------|------|-------------|
| `price` | `float` | Fair value of the binary option (0 to 1) |
| `delta` | `float` | Sensitivity to underlying price (dPrice/dS) |
| `vega` | `float` | Sensitivity to volatility (dPrice/dSigma) |
| `model` | `str` | Model 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.

```python
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%}")
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `observed_price` | `float` | Observed market price of the binary option |
| `s0` | `float` | Current underlying price |
| `strike` | `float` | Binary strike level |
| `t` | `float` | Time to expiry in days |

Returns `float`: the annualized implied volatility. Returns `NaN` if root-finding fails to converge.

---

## Comparing Models

```python
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"]`.

```python
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

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `feed` | `str` | required | Feed name to read the current price from |
| `model` | `str` | `"heston"` | Pricing model: `"heston"`, `"merton"`, or `"vg"` |
| `params` | model params | required | Model parameters (`HestonParams`, `MertonParams`, or `VGParams`) |
| `strike` | `float` | `0.50` | Binary strike level |
| `horizon_days` | `float` | `30.0` | Time to expiry in days |

---

## Mathematical Background

<AccordionGroup>
  <Accordion title="Characteristic Function Inversion">
    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)] du

    Each model provides a closed-form characteristic function, and the integral is evaluated numerically using adaptive Gauss-Kronrod quadrature in Rust.
  </Accordion>
  <Accordion title="Heston Model">
    The Heston (1993) model specifies:

    dS/S = mu*dt + sqrt(V)*dW_1
    dV = kappa*(theta - V)*dt + sigma*sqrt(V)*dW_2
    corr(dW_1, dW_2) = rho

    The 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.
  </Accordion>
  <Accordion title="Merton Jump-Diffusion">
    The Merton (1976) model adds compound Poisson jumps to geometric Brownian motion:

    dS/S = mu*dt + sigma*dW + (e^J - 1)*dN

    where 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).
  </Accordion>
  <Accordion title="Variance Gamma">
    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) = theta*G(t) + sigma*W(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.
  </Accordion>
</AccordionGroup>

<Warning>
  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.
</Warning>
