Skip to main content
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

# Paper is the default, just omit exchange
hz.run(
    name="backtest",
    markets=["test-market"],
    pipeline=[fair_value, quoter],
    mode="paper",
)
Or explicitly:
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:
1

Identify fills

Buy orders with price >= mid_price and sell orders with price <= mid_price are identified as matchable.
2

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

Update orders

Order status is updated (filled or partially filled). Terminal orders are tracked for later eviction.

Configuration

ParameterDefaultDescription
paper_fee_rate0.001Fee rate per fill (0.1%), applied to both maker and taker unless overridden
paper_maker_fee_rateNoneMaker fee rate (overrides paper_fee_rate for maker fills)
paper_taker_fee_rateNoneTaker fee rate (overrides paper_fee_rate for taker fills)
paper_partial_fill_ratio1.0Fraction 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).
# 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:
fills = engine.recent_fills()
for f in fills:
    print(f"{f.fill_id}: {'maker' if f.is_maker else 'taker'} fee={f.fee:.4f}")
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().

Partial fills

Set paper_partial_fill_ratio to simulate partial fills:
# 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
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.

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.