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

Or explicitly:

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

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).
Each Fill includes an is_maker field indicating whether the order was a maker or taker:
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:

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.