> ## Documentation Index
> Fetch the complete documentation index at: https://mathematicalcompany.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Automated Market Making

> Avellaneda-Stoikov market making with inventory skew, competitive spread blending, and multi-level quoting. Rust-native for maximum performance.

# 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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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>
