Paper Venue

In-memory exchange with tick-based matching, fees, and slippage

Paper is Horizon’s in-memory exchange. It supports limit and market orders, matches resting limits on tick crosses, applies fees and slippage, and tracks positions and cash. everything you need for backtest and paper-mode testing.

Import

python
from horizon.venues import Paper

Signature

python
Paper(
    initial_cash_usd: float = 100_000.0,
    supported_classes: list[AssetClass] | None = None,
    fee_bps: float = 0.0,
    slippage_bps: float = 0.0,
    bar_fill_mode: str = "cross",     # "cross" | "touch"
)
initial_cash_usdfloat
Starting cash balance.
supported_classeslist | None
Asset classes this venue accepts orders for. Defaults to all.
fee_bpsfloat
Per-fill fee in basis points. `fee = abs(qty × price) × fee_bps / 10000`.
slippage_bpsfloat
Slippage on market orders. Buys fill at `last × (1 + bps/10000)`, sells at `last × (1 - bps/10000)`.
bar_fill_modestr
How limit orders match. `"cross"` fills when price crosses the limit on a new tick. `"touch"` fills when the bar touches the limit (requires high/low data).

Tick-based matching

The critical method is tick(market_id, price, timestamp):

python
paper.tick("AAPL", 99.0, datetime(2024, 1, 1))

What happens:

  1. _last_prices["AAPL"] = 99.0
  2. For every resting limit order on AAPL:
    • If buy limit AND price ≤ limit → fill at limit
    • If sell limit AND price ≥ limit → fill at limit
  3. Returns a list of new VenueFill events

Market orders fill immediately at submit time using the last known price plus slippage. Limit orders rest until a crossing tick arrives.

Usage

python
from horizon.venues import Paper
from horizon.types import OrderAction, Urgency

paper = Paper(initial_cash_usd=100_000, fee_bps=10, slippage_bps=5)
paper.connect()

# Submit a limit order
order = paper.submit(OrderAction.place(
    market_id="AAPL",
    side="buy",
    quantity=10,
    price=180.0,
    order_type="limit",
))
print(order.status)   # "new". resting

# Tick with a crossing price
fills = paper.tick("AAPL", 179.5, datetime.now())
print(len(fills))     # 1. filled at 180.0 (the limit)

# Check state
print(paper.cash())            # 100_000 - 10*180 - fee
print(paper.positions())       # [VenuePosition(AAPL, qty=10, avg_cost=180)]
print(paper.balance())         # VenueCapital with equity, cash, buying_power

Market orders

python
# Establish a last price first
paper.tick("AAPL", 180.0)

order = paper.submit(OrderAction.place(
    market_id="AAPL",
    side="buy",
    quantity=10,
    order_type="market",
    urgency=Urgency.Immediate,
))
# Fills immediately at 180 + slippage
print(order.status)   # "filled"

Market orders with no prior tick are rejected cleanly (status="rejected").

Cancel + amend

python
order = paper.submit(OrderAction.place(market_id="AAPL", side="buy", quantity=10, price=180, order_type="limit"))

# Cancel
paper.cancel(order.id)

# Or amend before filling
updated = paper.amend(order.id, new_quantity=20, new_price=181.0)

# Cancel all on a market
paper.cancel_all("AAPL")

# Cancel all on the venue
paper.cancel_all()

Fills

Use drain_fills() to retrieve fills since the last drain call:

python
fills = paper.drain_fills()   # returns list[VenueFill], empties the queue
for f in fills:
    print(f.market_id, f.side, f.quantity, f.price, f.fee)

The backtest loop calls drain_fills() implicitly after every tick to route fills into the ledger.

Cash + P&L tracking

The Paper venue maintains its own cash balance internally:

python
paper.cash()              # current cash
paper.realized_pnl()      # cumulative realized P&L
paper.balance()           # full VenueCapital with equity, buying power, used BP

Tests

16 tests in tests/test_paper_venue.py:

python
def test_limit_fills_when_price_crosses(self):
    p = Paper(initial_cash_usd=100_000)
    p.connect()
    p.submit(_buy_limit(qty=10, price=100))
    fills = p.tick("AAPL", 99.0, datetime(2024, 1, 1))
    assert len(fills) == 1
    assert p.positions()[0].quantity == 10

def test_limit_does_not_fill_above_for_buy(self):
    p = Paper(initial_cash_usd=100_000)
    p.connect()
    p.submit(_buy_limit(qty=10, price=100))
    p.tick("AAPL", 101.0)   # above limit. no fill
    assert len(p.open_orders()) == 1

def test_market_order_fills_at_last_price(self):
    p = Paper(initial_cash_usd=100_000)
    p.connect()
    p.tick("AAPL", 100.0)
    order = p.submit(_buy_market(qty=10))
    assert order.status == "filled"

Covers limit matching on cross, market fills, rejects without prior tick, slippage, cancel, amend, cash tracking, fee application, and drain semantics.

Source

horizon/venues/paper.py. ~280 lines. Pure Python, no dependencies.

Next