SPB Git

spb/wp27_uqo

Public
5.0 KB · 105 lines python
1import numpy as np2import pytest34from market_agents.exchange.accounts import Account5from market_agents.exchange.engine import AssetSpec, Exchange, ExchangeConfig6from market_agents.exchange.orders import Action, CancelIntent, OrderIntent, OrderStatus, OrderType, Side789def act(*intents):10    return Action(orders=list(intents))111213def test_fill_moves_cash_and_shares_with_fees(exchange):14    ex = exchange15    ex.submit(0, act(OrderIntent(0, Side.SELL, OrderType.LIMIT, 10, 20000)), 0.0)16    ex.submit(1, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 10, 20000)), 0.0)17    ex.process_until(1.0)18    a0, a1 = ex.ledger[0], ex.ledger[1]19    notional = 20000 * 1020    fee = round(notional * 2 / 10000)21    assert a0.cash == 1_000_000 + notional and a0.positions[0] == 90           # maker: no fee22    assert a1.cash == 1_000_000 - notional - fee and a1.positions[0] == 110    # taker pays 2 bp23    assert ex.ledger.exchange_fees == fee24    assert a1.reserved_cash == 0 and a0.reserved_qty[0] == 025    ex.ledger.check_conservation()26    assert len(ex.trades) == 1 and ex.trades[0].aggressor is Side.BUY272829def test_latency_orders_arrivals(exchange):30    ex = exchange31    # agent 3 (latency 0.4) submits first but agent 0 (latency 0.1) arrives first32    ex.submit(3, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 1, 19000)), 0.0)33    ex.submit(0, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 1, 19000)), 0.0)34    ex.process_until(1.0)35    q = ex.books[0].bids[-19000]36    assert [o.agent_id for o in q] == [0, 3]373839def test_rejections(exchange):40    ex = exchange41    ex.submit(0, act(OrderIntent(0, Side.SELL, OrderType.LIMIT, 1000, 20000)), 0.0)   # only 100 held -> clipped to 10042    ex.submit(1, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 1, 5000)), 0.0)        # outside price band43    ex.submit(2, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 10_000, 20000)), 0.0)  # insufficient cash44    ex.submit(3, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 0, 20000)), 0.0)       # bad qty45    ex.process_until(1.0)46    reasons = [e.payload["reason"] for e in ex.events if e.kind == "reject"]47    assert reasons == ["price_band", "insufficient_cash", "bad_qty"]48    assert ex.orders[1].qty == 100 and ex.orders[1].status is OrderStatus.OPEN49    ex.ledger.check_conservation()505152def test_cancel_releases_reservation(exchange):53    ex = exchange54    ids = ex.submit(1, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 10, 19000)), 0.0)55    ex.process_until(1.0)56    assert ex.ledger[1].reserved_cash == 19000 * 10 + round(19000 * 10 * 2 / 10000) + 10   # notional + worst-case fee + 1c/lot57    ex.submit(1, Action(cancels=[CancelIntent(ids[0])]), 1.0)58    ex.process_until(2.0)59    assert ex.ledger[1].reserved_cash == 0 and ex.orders[ids[0]].status is OrderStatus.CANCELLED60    ex.ledger.check_conservation()616263def test_market_buy_capped_by_cash():64    ex = Exchange([AssetSpec(asset=0, name="A0")], ExchangeConfig(taker_fee_bps=0.0), np.random.default_rng(0), {0: 100})65    ex.register(Account(agent_id=0, cash=10_000, positions={0: 0}), 0.1)66    ex.register(Account(agent_id=1, cash=0, positions={0: 1000}), 0.1)67    ex.submit(1, act(OrderIntent(0, Side.SELL, OrderType.LIMIT, 1000, 100)), 0.0)68    ex.submit(0, act(OrderIntent(0, Side.BUY, OrderType.MARKET, 500, None)), 0.5)69    ex.process_until(2.0)70    assert ex.ledger[0].positions[0] == 100 and ex.ledger[0].cash == 071    ex.ledger.check_conservation()727374def test_dividend_and_conservation(exchange):75    ex = exchange76    ex.pay_dividends(0, 50)77    assert all(a.cash == 1_000_000 + 5000 for a in ex.ledger.accounts.values())78    ex.ledger.check_conservation()798081def test_halt_rejects_new_orders():82    ex = Exchange([AssetSpec(asset=0, name="A0")], ExchangeConfig(halt_threshold=0.05, halt_window=2, halt_duration=5),83                  np.random.default_rng(0), {0: 10000})84    ex.register(Account(agent_id=0, cash=10_000_000, positions={0: 1000}), 0.1)85    ex.register(Account(agent_id=1, cash=10_000_000, positions={0: 1000}), 0.1)86    for t in range(3):87        ex.end_of_tick(t)88    ex.submit(0, act(OrderIntent(0, Side.SELL, OrderType.LIMIT, 10, 9000)), 3.0)89    ex.submit(1, act(OrderIntent(0, Side.BUY, OrderType.MARKET, 10, None)), 3.0)90    ex.process_until(4.0)91    assert ex.state[0].n_halts == 1 and ex.is_halted(0)92    ex.submit(1, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 1, 9000)), 4.0)93    ex.process_until(5.0)94    assert [e.payload["reason"] for e in ex.events if e.kind == "reject"] == ["halted"]959697def test_margin_call_generates_liquidation():98    ex = Exchange([AssetSpec(asset=0, name="A0")], ExchangeConfig(margin_check=True), np.random.default_rng(0), {0: 10000})99    ex.register(Account(agent_id=0, cash=-900_000, positions={0: 100}, leverage_cap=5.0, maintenance_margin=0.25), 0.1)100    ex.register(Account(agent_id=1, cash=5_000_000, positions={0: 100}), 0.1)101    # equity = -900,000 + 100*10000 = 100,000 ; gross = 1,000,000 ; ratio 0.10 < 0.25 -> margin call102    actions = ex.margin_check()103    assert len(actions) == 1 and actions[0][0] == 0 and actions[0][1].orders[0].tag == "liquidation"104    assert any(e.kind == "margin_call" for e in ex.events)105
Files · d09ab0c71293588caf435dcaebf6e4bc03a129ff
Menu

Theme