import numpy as np import pytest from market_agents.exchange.accounts import Account from market_agents.exchange.engine import AssetSpec, Exchange, ExchangeConfig from market_agents.exchange.orders import Action, CancelIntent, OrderIntent, OrderStatus, OrderType, Side def act(*intents): return Action(orders=list(intents)) def test_fill_moves_cash_and_shares_with_fees(exchange): ex = exchange ex.submit(0, act(OrderIntent(0, Side.SELL, OrderType.LIMIT, 10, 20000)), 0.0) ex.submit(1, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 10, 20000)), 0.0) ex.process_until(1.0) a0, a1 = ex.ledger[0], ex.ledger[1] notional = 20000 * 10 fee = round(notional * 2 / 10000) assert a0.cash == 1_000_000 + notional and a0.positions[0] == 90 # maker: no fee assert a1.cash == 1_000_000 - notional - fee and a1.positions[0] == 110 # taker pays 2 bp assert ex.ledger.exchange_fees == fee assert a1.reserved_cash == 0 and a0.reserved_qty[0] == 0 ex.ledger.check_conservation() assert len(ex.trades) == 1 and ex.trades[0].aggressor is Side.BUY def test_latency_orders_arrivals(exchange): ex = exchange # agent 3 (latency 0.4) submits first but agent 0 (latency 0.1) arrives first ex.submit(3, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 1, 19000)), 0.0) ex.submit(0, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 1, 19000)), 0.0) ex.process_until(1.0) q = ex.books[0].bids[-19000] assert [o.agent_id for o in q] == [0, 3] def test_rejections(exchange): ex = exchange ex.submit(0, act(OrderIntent(0, Side.SELL, OrderType.LIMIT, 1000, 20000)), 0.0) # only 100 held -> clipped to 100 ex.submit(1, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 1, 5000)), 0.0) # outside price band ex.submit(2, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 10_000, 20000)), 0.0) # insufficient cash ex.submit(3, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 0, 20000)), 0.0) # bad qty ex.process_until(1.0) reasons = [e.payload["reason"] for e in ex.events if e.kind == "reject"] assert reasons == ["price_band", "insufficient_cash", "bad_qty"] assert ex.orders[1].qty == 100 and ex.orders[1].status is OrderStatus.OPEN ex.ledger.check_conservation() def test_cancel_releases_reservation(exchange): ex = exchange ids = ex.submit(1, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 10, 19000)), 0.0) ex.process_until(1.0) assert ex.ledger[1].reserved_cash == 19000 * 10 + round(19000 * 10 * 2 / 10000) + 10 # notional + worst-case fee + 1c/lot ex.submit(1, Action(cancels=[CancelIntent(ids[0])]), 1.0) ex.process_until(2.0) assert ex.ledger[1].reserved_cash == 0 and ex.orders[ids[0]].status is OrderStatus.CANCELLED ex.ledger.check_conservation() def test_market_buy_capped_by_cash(): ex = Exchange([AssetSpec(asset=0, name="A0")], ExchangeConfig(taker_fee_bps=0.0), np.random.default_rng(0), {0: 100}) ex.register(Account(agent_id=0, cash=10_000, positions={0: 0}), 0.1) ex.register(Account(agent_id=1, cash=0, positions={0: 1000}), 0.1) ex.submit(1, act(OrderIntent(0, Side.SELL, OrderType.LIMIT, 1000, 100)), 0.0) ex.submit(0, act(OrderIntent(0, Side.BUY, OrderType.MARKET, 500, None)), 0.5) ex.process_until(2.0) assert ex.ledger[0].positions[0] == 100 and ex.ledger[0].cash == 0 ex.ledger.check_conservation() def test_dividend_and_conservation(exchange): ex = exchange ex.pay_dividends(0, 50) assert all(a.cash == 1_000_000 + 5000 for a in ex.ledger.accounts.values()) ex.ledger.check_conservation() def test_halt_rejects_new_orders(): ex = Exchange([AssetSpec(asset=0, name="A0")], ExchangeConfig(halt_threshold=0.05, halt_window=2, halt_duration=5), np.random.default_rng(0), {0: 10000}) ex.register(Account(agent_id=0, cash=10_000_000, positions={0: 1000}), 0.1) ex.register(Account(agent_id=1, cash=10_000_000, positions={0: 1000}), 0.1) for t in range(3): ex.end_of_tick(t) ex.submit(0, act(OrderIntent(0, Side.SELL, OrderType.LIMIT, 10, 9000)), 3.0) ex.submit(1, act(OrderIntent(0, Side.BUY, OrderType.MARKET, 10, None)), 3.0) ex.process_until(4.0) assert ex.state[0].n_halts == 1 and ex.is_halted(0) ex.submit(1, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 1, 9000)), 4.0) ex.process_until(5.0) assert [e.payload["reason"] for e in ex.events if e.kind == "reject"] == ["halted"] def test_margin_call_generates_liquidation(): ex = Exchange([AssetSpec(asset=0, name="A0")], ExchangeConfig(margin_check=True), np.random.default_rng(0), {0: 10000}) ex.register(Account(agent_id=0, cash=-900_000, positions={0: 100}, leverage_cap=5.0, maintenance_margin=0.25), 0.1) ex.register(Account(agent_id=1, cash=5_000_000, positions={0: 100}), 0.1) # equity = -900,000 + 100*10000 = 100,000 ; gross = 1,000,000 ; ratio 0.10 < 0.25 -> margin call actions = ex.margin_check() assert len(actions) == 1 and actions[0][0] == 0 and actions[0][1].orders[0].tag == "liquidation" assert any(e.kind == "margin_call" for e in ex.events)