Automated Market Making
Horizon includes a complete Avellaneda-Stoikov market making engine. All math is implemented in Rust for maximum performance and exposed to Python via PyO3. Thehz.market_maker() pipeline factory handles everything from feed ingestion to multi-level quote generation in a single call.
Market making in prediction markets involves continuously quoting bid and ask prices. The Avellaneda-Stoikov model adjusts quotes based on inventory risk, volatility, and competitive dynamics. Horizon’s implementation adds competitive spread blending and multi-level quoting on top of the base model.
Overview
Reservation Price
hz.reservation_price() computes the inventory-skewed fair value using Avellaneda-Stoikov.Optimal Spread
hz.optimal_spread() computes the theoretically optimal bid-ask width.Competitive Spread
hz.competitive_spread() blends model spread with live orderbook spread.Inventory-Aware Sizing
hz.mm_size() skews bid/ask sizes based on current inventory.Core Functions
hz.reservation_price
Compute the inventory-skewed fair value (Avellaneda-Stoikov reservation price).| Parameter | Type | Description |
|---|---|---|
mid | float | Current mid price |
inventory | float | Net inventory (positive = long, negative = short) |
gamma | float | Risk aversion (higher = more inventory penalty) |
volatility | float | Estimated price volatility |
time_horizon | float | Time horizon (1.0 = full period) |
r = mid - inventory * gamma * volatility^2 * time_horizon
- Long inventory skews fair value down (encourages selling)
- Short inventory skews fair value up (encourages buying)
- NaN/Inf inventory defaults to zero (returns mid)
hz.optimal_spread
Compute the theoretically optimal bid-ask spread.| Parameter | Type | Description |
|---|---|---|
volatility | float | Estimated price volatility |
inventory | float | Current inventory |
gamma | float | Risk aversion parameter |
kappa | float | Order arrival intensity (higher = more competitive) |
time_horizon | float | Time horizon |
hz.competitive_spread
Blend the model-optimal spread with the live orderbook spread.| Parameter | Type | Description |
|---|---|---|
base_spread | float | Theoretical optimal spread |
book_spread | float | Current orderbook spread |
book_imbalance | float | Orderbook imbalance (not used in current version) |
aggression | float | 0.0 = pure model, 1.0 = pure orderbook |
hz.mm_size
Compute inventory-skewed bid and ask sizes.| Parameter | Type | Description |
|---|---|---|
base_size | float | Base order size for each side |
inventory | float | Current inventory |
max_position | float | Maximum position limit |
fill_rate | float | Historical fill rate (0 to 1) |
skew_factor | float | Size skew intensity (0 = no skew, 1 = aggressive) |
(bid_size, ask_size):
- Long inventory → smaller bids, larger asks (reduces inventory)
- Short inventory → larger bids, smaller asks (builds inventory)
- Returns
(0.0, 0.0)if base_size is zero or NaN
hz.estimate_volatility
Estimate price volatility from a series of prices using log-return standard deviation.Pipeline Factory: hz.market_maker
Themarket_maker() factory returns a pipeline function that generates multi-level quotes automatically.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
base_spread | float | 0.04 | Fallback spread |
gamma | float | 0.5 | Risk aversion |
kappa | float | 1.5 | Order arrival intensity |
max_position | float | 100.0 | Max position for sizing |
num_levels | int | 1 | Quote levels to generate |
level_spacing | float | 0.01 | Spacing between levels |
aggression | float | 0.5 | Book vs model spread blend |
volatility_window | int | 50 | Rolling vol window |
feed_name | str or None | None | Feed key |
size | float | 5.0 | Base order size |
skew_factor | float | 0.5 | Size skew factor |
time_horizon | float | 1.0 | Time horizon |
How It Works
On each cycle, the market maker:- Gets the mid price from the feed (bid/ask midpoint or price field)
- Estimates volatility from the rolling price history
- Computes
reservation_price()with current inventory - Computes
optimal_spread()based on volatility and risk aversion - Blends with live spread via
competitive_spread()if bid/ask data is available - Computes inventory-skewed
mm_size() - Generates
num_levelsquote levels with 0.8x size decay per level - Clamps all prices to [0.01, 0.99] and skips crossed quotes
list[Quote] compatible with the pipeline system.
Signal Chaining
When placed afterhz.signal_combiner() in a pipeline, the market maker receives the combined signal value as its fair value estimate. This replaces the feed mid price for the reservation price calculation:
Examples
Single-Level Market Maker
Multi-Level with Aggressive Spread
Combined with Signal Combiner
Mathematical Background
Avellaneda-Stoikov Model
Avellaneda-Stoikov Model
The model computes the dealer’s reservation price as:
r = s - q * gamma * sigma^2 * TWhere s is the mid price, q is inventory, gamma is risk aversion, sigma is volatility, and T is the time horizon. This skews the fair value against inventory to encourage mean reversion.Optimal Spread
Optimal Spread
The optimal spread balances adverse selection risk against the probability of getting filled. Higher volatility and lower order arrival rates lead to wider spreads.
Why Competitive Blending?
Why Competitive Blending?
Pure model spreads can be too wide or too narrow relative to the live orderbook. The
aggression parameter lets you blend toward the market spread when you want to be competitive, or toward the model spread when you want to protect against adverse selection.