1"""Property-based tests: random order streams never violate book, reservation or conservation invariants,2and the event log replays to the same final state."""3import numpy as np4from hypothesis import given, settings, strategies as st56from market_agents.exchange.accounts import Account7from market_agents.exchange.engine import AssetSpec, Exchange, ExchangeConfig8from market_agents.exchange.orders import Action, CancelIntent, OrderIntent, OrderType, Side91011def build(seed, n_agents=6, allow_short=False):12 ex = Exchange([AssetSpec(asset=0, name="A0", price_band_pct=0.9)], ExchangeConfig(taker_fee_bps=3.0, maker_fee_bps=-1.0,13 allow_short=allow_short, max_short_lots=50, order_ttl=7), np.random.default_rng(seed), {0: 1000})14 for i in range(n_agents):15 ex.register(Account(agent_id=i, cash=200_000 * (i + 1), positions={0: 50 * (i + 1)}, short_allowed=allow_short),16 latency=0.05 + 0.3 * (i % 3))17 return ex181920intent = st.tuples(st.integers(0, 5), st.sampled_from([Side.BUY, Side.SELL]), st.sampled_from([OrderType.LIMIT, OrderType.MARKET]),21 st.integers(1, 80), st.integers(500, 1500), st.booleans())222324@settings(max_examples=60, deadline=None)25@given(st.lists(intent, min_size=1, max_size=120), st.integers(0, 10_000))26def test_random_streams_keep_invariants(stream, seed):27 ex = build(seed)28 t = 029 for k, (agent, side, otype, qty, price, cancel) in enumerate(stream):30 if cancel and ex.orders:31 oid = max(ex.orders)32 ex.submit(ex.orders[oid].agent_id, Action(cancels=[CancelIntent(oid)]), float(t))33 ex.submit(agent, Action(orders=[OrderIntent(0, side, otype, qty, None if otype is OrderType.MARKET else price)]), float(t))34 if k % 3 == 2:35 ex.process_until(float(t + 1))36 ex.end_of_tick(t)37 t += 138 ex.books[0].check_invariants()39 ex.ledger.check_conservation()40 ex.process_until(float(t + 5))41 ex.books[0].check_invariants()42 ex.ledger.check_conservation()43 # reservations equal the sum over resting orders44 for acc in ex.ledger.accounts.values():45 resting = ex.books[0].resting_for(acc.agent_id)46 assert acc.reserved_cash == sum(o.reserved_cash for o in resting)47 assert acc.reserved_qty.get(0, 0) == sum(o.reserved_qty for o in resting)48 assert acc.positions[0] >= 0495051@settings(max_examples=25, deadline=None)52@given(st.lists(intent, min_size=5, max_size=80), st.integers(0, 1000))53def test_determinism_and_replay(stream, seed):54 def run():55 ex = build(seed)56 for k, (agent, side, otype, qty, price, cancel) in enumerate(stream):57 ex.submit(agent, Action(orders=[OrderIntent(0, side, otype, qty, None if otype is OrderType.MARKET else price)]), float(k // 4))58 if k % 4 == 3:59 ex.process_until(float(k // 4 + 1))60 ex.process_until(1e9)61 return ex62 a, b = run(), run()63 assert a.snapshot() == b.snapshot()64 assert [(e.kind, e.payload) for e in a.events] == [(e.kind, e.payload) for e in b.events]65