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 syntheticFilledACKs. A partially crossing limit that rests getsNewfor 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¶
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.