Skip to content

SimExchange — local financial execution

SimExchange executes funded order-book interactions locally in Rust. It needs no server or network. Its synchronous interface is useful for tests and controlled strategies; MarlSimEnv supplies chronological computation, arrival, publication and delivery for training experiments.

Funded example

from fastboxex import SimExchange, SimConnection, SimMarket

exchange = SimExchange()
exchange.add_asset(1)
seller = SimConnection(exchange, exchange.add_participant("seller", inventory=1000))
buyer = SimConnection(exchange, exchange.add_participant("buyer", cash=1_000_000_000))
for phase in ("Suspended", "Auction", "Trade"):
    exchange.transition_to(phase)
seller.submit_limit(1, 1, 1_000_000, 100, client_tag=77)
seller.drain_acks()  # New resting order
buyer.submit_market(1, 0, 100, client_tag=88)
assert buyer.drain_acks() == []  # Executions are delivered as fills
print(buyer.drain_fills())
exchange.validate()

Prices/quantities are raw protocol integers. Notional is price × quantity. Display conversion is separate. Ordinary orders are rounded down to tick/lot grids before economic validation. Malformed controls or unknown identities raise errors; economic rejects produce final rejection events.

Contract

  • Price-time continuous matching at the resting price.
  • Limit remainders rest; IOC remainders cancel; market orders never rest.
  • Delta prices use opposite BBO plus/minus an explicitly signed tick count. Sign is not inverted by side. The sentinel delta represents a market order.
  • A resting order gets New. Full executions do not create synthetic Filled ACKs. A partially crossing limit that rests gets New for the remainder.
  • Fills carry actual remaining quantities and maker/taker fee components in Rust. The quantity is the residual at that execution. A market/IOC residual need not remain live: use terminal ACKs and standing-order updates to establish that.
  • Ordinary cancel ACK price is zero; IOC remainder ACK price is the resolved price.
  • Cancel/modify require BookOnly or Trade. Failed operations preserve FIFO.
  • Successful modifications lose continuous queue priority; fills accumulated before modification remain distinct from a change in requested quantity.
  • Auction clearing maximizes executable volume, minimizes imbalance and applies the existing pressure tie-break. Allocation is price/order-ID priority.
  • Self-crossing is permitted, as in the reference engine; both fee legs settle.
  • Trade collars use opposite BBO with reference fallback; modifications also use opposite BBO during BookOnly, as in the reference. Auctions update the reference. Circuit breakers retain the opening anchor and suspend after fills. Zero disables the breaker; 10000 represents a finite 100% band.

The local indexed book does not model the live engine's finite ring-price-slot collisions. See conformance/README.md in the source repository for the supported comparison domain and named upstream exceptions.

Fees and reservations

exchange = SimExchange(fees={
    "maker": {"bps": 2, "direction": "Rebate"},
    "taker": {"bps": 8, "direction": "Pay"},
    "exchange_fee_bps": 3,
})

Default fees are zero. Charges round up; rebates round down; all settle in cash. Auction sides use maker fees. The venue account balances fee transfers and may pay rebates. fill_fee follows the existing protocol's unsigned platform-plus- exchange magnitude; its direction describes the platform component. For rebate schedules it is not a signed net fee. Use account changes for economic P&L.

Reservations are tracked exactly per order. Conservative per-lot cash reserves cover either role and fragmentation. On normal 100-tick/100-lot grids and usual maker/taker schedules these match the reference engine; small-unit research grids deliberately avoid its rounding defects. Budget-limited buys remain lot aligned. Tiny-notional sells reserve any rounded-fee deficit.

validate() checks linked FIFO indexes, cached price quantities, owner indexes, order-backed reservations and cash/inventory conservation, including the venue. It is an explicit diagnostic rather than an expensive check after every hot event.

Multiple local assets

Configure assets before registering participants. Funding is per asset:

exchange = SimExchange()
exchange.add_asset(1)
exchange.add_asset(2, config={"asset_id": 2, "tick_size": 200,
                            "lot_size": 100, "reference_price": 2_000_000})
pid = exchange.add_participant("cross-asset", endowments={
    1: {"cash": 10_000_000_000, "inventory": 1000},
    2: {"cash": 20_000_000_000, "inventory": 500},
})
print(exchange.account(pid, 2))

Scalar cash/inventory initialization is accepted only for a single asset. Assets have separate books, configurations, lifecycle and ledger state. Commands always include an asset ID; multi-asset lifecycle transitions select asset_id explicitly.

Lifecycle

BookOnly → Suspended → Auction → Trade
Trade → Suspended → Auction → Trade or Closed

Entering BookOnly clears an existing book. Auction executes on entry. A tripped local breaker can transition directly to Closed, an explicitly documented fix for the live guard/matrix conflict. The normal transition matrix remains strict.

Synchronous views versus delivered observations

SimMarket.get_latest(asset_id) is an immediate controller-side snapshot. advance_time, advance_ticks and advance_to change its local clock; they do not pretend to simulate a transport. force_snapshot advances the local snapshot sequence. Use MARL delivered views for latency-sensitive agent decisions.

LOB tuples retain (price, remaining_quantity, order_id, client_tag) ordering. The snapshot includes all 18 public flow counters; Rust provides direct existing- protocol projections for snapshots, fills and acknowledgements.

Diagnostic mode

mode="diagnostic" supports seed_book for market-generation diagnostics and bypasses funded accounting. It must not be used as evidence of economic wealth or risk correctness. Funded/parity runs reject seed_book; initialize them with backed orders in BookOnly. Invalid mode names are errors, not silent fallbacks.

See v4 migration and MARL for training, checkpoints, observation history and research examples.