---
title: "Horizon Paper Exchange"
description: "Local simulated exchange with tick-based order matching for Horizon strategy development."
icon: "file"
---

Paper trading runs a local simulated exchange with tick-based order matching. No network calls, no credentials required. This is the default exchange for development and backtesting.

## Usage

```python
# Paper is the default, just omit exchange
hz.run(
    name="backtest",
    markets=["test-market"],
    pipeline=[fair_value, quoter],
    mode="paper",
)
```

Or explicitly:

```python
engine = Engine()  # Default is paper
engine = Engine(paper_fee_rate=0.001)  # Custom fee rate
engine = Engine(paper_partial_fill_ratio=0.5)  # 50% partial fills
```

## How Matching Works

Each cycle, the strategy loop calls `engine.tick(market_id, mid_price)`. The paper exchange uses a **3-phase tick**:

<Steps>
  <Step title="Identify fills">
    Buy orders with `price >= mid_price` and sell orders with `price <= mid_price` are identified as matchable.
  </Step>
  <Step title="Create fills">
    `Fill` objects are created at the order's limit price (not the mid price). The fill size respects the `partial_fill_ratio` setting.
  </Step>
  <Step title="Update orders">
    Order status is updated (filled or partially filled). Terminal orders are tracked for later eviction.
  </Step>
</Steps>

## Configuration

| Parameter | Default | Description |
|-----------|---------|-------------|
| `paper_fee_rate` | `0.001` | Fee rate per fill (0.1%), applied to both maker and taker unless overridden |
| `paper_maker_fee_rate` | `None` | Maker fee rate (overrides `paper_fee_rate` for maker fills) |
| `paper_taker_fee_rate` | `None` | Taker fee rate (overrides `paper_fee_rate` for taker fills) |
| `paper_partial_fill_ratio` | `1.0` | Fraction of order filled per tick (1.0 = full fill) |

### Maker/taker fees

Split fees by liquidity role. Makers add liquidity (limit orders resting below/above mid), takers remove it (market orders or limit orders that cross the spread).

```python
# Flat fee (backward compatible)
engine = Engine(paper_fee_rate=0.001)

# Split maker/taker fees
engine = Engine(
    paper_maker_fee_rate=0.0002,  # 2 bps for makers
    paper_taker_fee_rate=0.002,   # 20 bps for takers
)

# Override just one side (taker), maker falls back to paper_fee_rate
engine = Engine(paper_fee_rate=0.001, paper_taker_fee_rate=0.003)
```

Each `Fill` includes an `is_maker` field indicating whether the order was a maker or taker:

```python
fills = engine.recent_fills()
for f in fills:
    print(f"{f.fill_id}: {'maker' if f.is_maker else 'taker'} fee={f.fee:.4f}")
```

<Note>
  In the paper exchange, maker/taker is determined by comparing the order price to the mid price at fill time. Market orders are always takers. For more realistic maker/taker simulation, use the BookSim exchange with L2 orderbook data via `hz.backtest()`.
</Note>

### Partial fills

Set `paper_partial_fill_ratio` to simulate partial fills:

```python
# Each tick fills 50% of the remaining order size
engine = Engine(paper_partial_fill_ratio=0.5)
```

## Mid Price Source

The `tick()` function needs a mid price to determine which orders fill. The strategy loop uses this priority:

1. **Feed mid price**: `(feed.bid + feed.ask) / 2` from the first feed with data
2. **Feed last price**: `feed.price` if bid/ask aren't available
3. **Quote mid**: average of all quote mid prices as a fallback

<Note>
  In paper mode, the mid price drives order matching. If you have no feeds configured, the paper exchange uses your quote mid prices, which means your own quotes determine fills. Useful for basic testing but not realistic simulation.
</Note>

## Order Eviction

Terminal orders (filled, canceled, rejected) are evicted periodically to prevent memory growth. The strategy loop calls `engine.evict_stale_orders(300.0)` every 100 cycles, removing terminal orders older than 5 minutes.
