---
title: "Automated Market Making"
description: "Avellaneda-Stoikov market making with inventory skew, competitive spread blending, and multi-level quoting. Rust-native for maximum performance."
icon: "chart-mixed"
---

# 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. The `hz.market_maker()` pipeline factory handles everything from feed ingestion to multi-level quote generation in a single call.

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

## Overview

<CardGroup cols={2}>
  <Card title="Reservation Price" icon="crosshairs">
    `hz.reservation_price()` computes the inventory-skewed fair value using Avellaneda-Stoikov.
  </Card>
  <Card title="Optimal Spread" icon="arrows-left-right">
    `hz.optimal_spread()` computes the theoretically optimal bid-ask width.
  </Card>
  <Card title="Competitive Spread" icon="gauge">
    `hz.competitive_spread()` blends model spread with live orderbook spread.
  </Card>
  <Card title="Inventory-Aware Sizing" icon="scale-balanced">
    `hz.mm_size()` skews bid/ask sizes based on current inventory.
  </Card>
</CardGroup>

---

## Core Functions

### hz.reservation_price

Compute the inventory-skewed fair value (Avellaneda-Stoikov reservation price).

```python
import horizon as hz

r = hz.reservation_price(
    mid=0.50,           # Current mid price
    inventory=10.0,     # Current inventory (positive = long)
    gamma=0.5,          # Risk aversion parameter
    volatility=0.02,    # Estimated volatility
    time_horizon=1.0,   # Time horizon (normalized)
)
print(f"Reservation price: {r:.4f}")  # 0.4980 (skewed down for long inventory)
```

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

Formula: `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.

```python
spread = hz.optimal_spread(
    volatility=0.02,    # Estimated volatility
    inventory=10.0,     # Current inventory
    gamma=0.5,          # Risk aversion
    kappa=1.5,          # Order arrival intensity
    time_horizon=1.0,   # Time horizon
)
print(f"Optimal spread: {spread:.4f}")
```

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

```python
spread = hz.competitive_spread(
    base_spread=0.04,      # From optimal_spread()
    book_spread=0.02,      # Live orderbook spread
    book_imbalance=0.0,    # Orderbook imbalance (-1 to 1)
    aggression=0.5,        # 0 = pure model, 1 = pure book
)
print(f"Competitive spread: {spread:.4f}")  # 0.03 (blend)
```

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

The result is clamped to [0.001, 1.0].

### hz.mm_size

Compute inventory-skewed bid and ask sizes.

```python
bid_size, ask_size = hz.mm_size(
    base_size=5.0,       # Base order size
    inventory=50.0,      # Current inventory
    max_position=100.0,  # Maximum position limit
    fill_rate=0.3,       # Historical fill rate
    skew_factor=0.5,     # How aggressively to skew sizes
)
print(f"Bid: {bid_size:.1f}, Ask: {ask_size:.1f}")
```

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

Returns `(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.

```python
vol = hz.estimate_volatility([0.50, 0.51, 0.49, 0.52, 0.48, 0.51])
print(f"Volatility: {vol:.4f}")
```

Returns 0.0 for fewer than 2 prices or constant prices.

---

## Pipeline Factory: hz.market_maker

The `market_maker()` factory returns a pipeline function that generates multi-level quotes automatically.

```python
import horizon as hz

hz.run(
    name="mm_strategy",
    markets=["will-btc-hit-100k"],
    feeds={"btc": hz.BinanceWS("btcusdt")},
    pipeline=[hz.market_maker(base_spread=0.04, gamma=0.3, size=5.0)],
    risk=hz.Risk(max_position=100),
)
```

### Parameters

```python
mm = hz.market_maker(
    base_spread=0.04,         # Base spread when no book data available
    gamma=0.5,                # Risk aversion (inventory penalty)
    kappa=1.5,                # Order arrival intensity
    max_position=100.0,       # Maximum position for sizing
    num_levels=1,             # Number of quote levels
    level_spacing=0.01,       # Price spacing between levels
    aggression=0.5,           # Spread blend: 0=model, 1=book
    volatility_window=50,     # Price history window for vol estimation
    feed_name=None,           # Feed key (None = use first available)
    size=5.0,                 # Base order size
    skew_factor=0.5,          # Inventory size skew intensity
    time_horizon=1.0,         # Time horizon parameter
)
```

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

1. Gets the mid price from the feed (bid/ask midpoint or price field)
2. Estimates volatility from the rolling price history
3. Computes `reservation_price()` with current inventory
4. Computes `optimal_spread()` based on volatility and risk aversion
5. Blends with live spread via `competitive_spread()` if bid/ask data is available
6. Computes inventory-skewed `mm_size()`
7. Generates `num_levels` quote levels with 0.8x size decay per level
8. Clamps all prices to [0.01, 0.99] and skips crossed quotes

Returns `list[Quote]` compatible with the pipeline system.

### Signal Chaining

When placed after `hz.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:

```python
pipeline=[
    hz.signal_combiner([...]),       # returns float (e.g., 0.55)
    hz.market_maker(feed_name="book"),  # uses 0.55 as fair value
]
```

The signal value is only used when it falls in the valid range (0, 1). Otherwise the feed mid price is used as the fallback.

---

## Examples

### Single-Level Market Maker

```python
import horizon as hz

hz.run(
    name="simple_mm",
    markets=["election-winner"],
    feeds={"poly": hz.PolymarketBook("election-winner")},
    pipeline=[hz.market_maker(
        feed_name="poly",
        base_spread=0.06,
        gamma=0.3,
        size=10.0,
    )],
    risk=hz.Risk(max_position=200, max_drawdown_pct=5),
)
```

### Multi-Level with Aggressive Spread

```python
mm = hz.market_maker(
    feed_name="book",
    base_spread=0.02,
    gamma=0.8,           # High risk aversion
    aggression=0.7,      # Lean toward book spread
    size=5.0,
    num_levels=3,        # Three levels
    level_spacing=0.02,  # 2 cents apart
)
```

### Combined with Signal Combiner

```python
import horizon as hz

hz.run(
    name="signal_mm",
    markets=["election-winner"],
    feeds={"book": hz.PolymarketBook("election-winner")},
    pipeline=[
        hz.signal_combiner([
            hz.price_signal("book", weight=0.5),
            hz.spread_signal("book", weight=0.3),
            hz.momentum_signal("book", lookback=20, weight=0.2),
        ]),
        hz.market_maker(feed_name="book", gamma=0.5, size=5.0),
    ],
    risk=hz.Risk(max_position=100),
)
```

---

## Mathematical Background

<AccordionGroup>
  <Accordion title="Avellaneda-Stoikov Model">
    The model computes the dealer's reservation price as:

    `r = s - q * gamma * sigma^2 * T`

    Where `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.
  </Accordion>
  <Accordion title="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.
  </Accordion>
  <Accordion title="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.
  </Accordion>
</AccordionGroup>

<Warning>
  Market making in prediction markets carries inventory risk. Always use risk limits (`max_position`, `max_drawdown_pct`) and start with paper trading before deploying live. The `gamma` parameter controls how aggressively the model penalizes inventory, start with higher values (0.5-1.0) to be conservative.
</Warning>
