"""UnifiedExecutor composition tests — the initial-submit pipeline (spec §2/§6/§15.2). Each asserts a composition ruling that is NOT re-derivable from the parts: * S2+ pain-fence refuses whole (mutation: let S2 through -> RED) * placer-declines → cross iff urgency crosses; ACQUIRE abandons (mutation: cross ACQUIRE -> RED) * TTL derived from the urgency's discipline Async via asyncio.run (no pytest-asyncio needed). Run: /home/dolphin/siloqy_env/bin/python3 -m pytest prod/exec_unified/test_executor.py -q """ from __future__ import annotations import asyncio from decimal import Decimal from prod.exec_unified.contract import ExecutionRequest, SGrade, Side, UrgencyClass from prod.exec_unified.drive_loop import Action, DriveLoop, SlotView from prod.exec_unified.executor import ExecuteStatus, UnifiedExecutor from prod.exec_unified.placer import MarketSnapshot from prod.exec_unified.router import ExecutionMethod from prod.exec_unified.working import WorkingRegistry NARROW = MarketSnapshot(best_bid=Decimal("100"), best_ask=Decimal("100.05"), spread_bps=Decimal("5"), tick=Decimal("0.01"), step=Decimal("0.001")) WIDE = MarketSnapshot(best_bid=Decimal("100"), best_ask=Decimal("100.5"), spread_bps=Decimal("20"), tick=Decimal("0.01"), step=Decimal("0.001")) FLAT = SlotView(trade_id="", stage="ACKED", size=Decimal(0)) # An exit is submitted against an OPEN position — the slot still shows it (size > 0) until # the maker close fills. A flat slot at exit-submit time is nonsensical. OPEN = SlotView(trade_id="pos-1", stage="POSITION_OPEN", size=Decimal("1")) class FakeClock: def __init__(self, t=1000.0): self.t = t def __call__(self): return self.t class FakePort: def __init__(self, clock, slot=FLAT): self._c = clock self.slot = slot self.submits = [] self.pumps = 0 def clock(self): return self._c() def slot_view(self): return self.slot def last_own_fill_at(self): return -1e9 async def pump(self): self.pumps += 1 async def cancel(self, wo): pass async def open_positions(self): return [] async def submit(self, plan): self.submits.append(plan) def _exec(slot=FLAT): clock = FakeClock() port = FakePort(clock, slot) reg = WorkingRegistry(clock) return UnifiedExecutor(DriveLoop(port, reg)), port, reg def _req(urgency, side=Side.BUY, **kw): base = dict(request_id="req", asset="BTCUSDT", side=side, size=Decimal("0.001"), urgency=urgency) base.update(kw) return ExecutionRequest(**base) def _run(coro): return asyncio.run(coro) # ── S2+ pain-fence ─────────────────────────────────────────────────────────── def test_s2_refused_whole_no_submit(): # Mutation: drop the fence -> this order would submit and RED here. ex, port, _ = _exec() r = _run(ex.execute(_req(UrgencyClass.ACQUIRE, s_grade=SGrade.S2), NARROW)) assert r.status is ExecuteStatus.REFUSED_S2 assert port.submits == [] # nothing hit the venue assert "REFUSED" in r.reason def test_s3_also_refused(): ex, port, _ = _exec() r = _run(ex.execute(_req(UrgencyClass.HARVEST, side=Side.SELL, s_grade=SGrade.S3), NARROW)) assert r.status is ExecuteStatus.REFUSED_S2 assert port.submits == [] def test_s0_default_not_refused(): ex, port, _ = _exec() r = _run(ex.execute(_req(UrgencyClass.ACQUIRE), NARROW)) # default S0 assert r.status is not ExecuteStatus.REFUSED_S2 # ── routing → submit ───────────────────────────────────────────────────────── def test_catastrophic_crosses_taker(): ex, port, _ = _exec() r = _run(ex.execute(_req(UrgencyClass.CATASTROPHIC, side=Side.SELL), NARROW)) assert r.status is ExecuteStatus.CROSSED_TAKER assert len(port.submits) == 1 assert port.submits[0].method is ExecutionMethod.TAKER assert port.submits[0].reduce_only is True # exit reduces assert port.submits[0].request_id == "req-0" # per-attempt id (H4) def test_acquire_narrow_spread_rests_maker(): ex, port, reg = _exec() r = _run(ex.execute(_req(UrgencyClass.ACQUIRE), NARROW)) assert r.status is ExecuteStatus.RESTING_MAKER assert port.submits[0].method is ExecutionMethod.MAKER assert port.submits[0].limit_price == Decimal("100") # placed at bid (touch), quantized assert reg.working("req-0") is not None # drive loop now owns it def test_acquire_wide_spread_abandons_never_crosses(): # THE key composition ruling: a missed entry is free (§4-1). Mutation: cross here -> RED. ex, port, _ = _exec() r = _run(ex.execute(_req(UrgencyClass.ACQUIRE), WIDE)) assert r.status is ExecuteStatus.ABANDONED assert port.submits == [] # NOT crossed def test_protect_wide_spread_crosses(): # Contrast with ACQUIRE: PROTECT crosses when maker is impossible (cross_on_expiry). ex, port, _ = _exec() r = _run(ex.execute(_req(UrgencyClass.PROTECT, side=Side.SELL), WIDE)) assert r.status is ExecuteStatus.CROSSED_TAKER assert port.submits[0].method is ExecutionMethod.TAKER def test_harvest_narrow_rests_maker(): ex, port, reg = _exec(slot=OPEN) r = _run(ex.execute(_req(UrgencyClass.HARVEST, side=Side.SELL), NARROW)) assert r.status is ExecuteStatus.RESTING_MAKER assert port.submits[0].limit_price == Decimal("100.05") # SELL at ask (touch) def test_maker_filled_on_submit(): ex, port, reg = _exec(slot=SlotView(trade_id="req-0", stage="POSITION_OPEN", size=Decimal("1"))) r = _run(ex.execute(_req(UrgencyClass.ACQUIRE), NARROW)) assert r.status is ExecuteStatus.FILLED_ON_SUBMIT assert reg.working("req-0") is None # filled → not left working def test_post_only_rejected_registers_for_instant_resolve(): ex, port, reg = _exec(slot=SlotView(trade_id="", stage="ORDER_REJECTED", size=Decimal(0))) r = _run(ex.execute(_req(UrgencyClass.ACQUIRE), NARROW)) assert r.status is ExecuteStatus.REJECTED_RESTING wo = reg.working("req-0") assert wo is not None assert reg.expired() == [wo] # deadline pulled to now # ── TTL derivation from urgency discipline ─────────────────────────────────── def test_ttl_protect_bounded_2s(): ex, port, reg = _exec(slot=OPEN) _run(ex.execute(_req(UrgencyClass.PROTECT, side=Side.SELL), NARROW)) wo = reg.working("req-0") assert wo.deadline - wo.created == 2.0 # BOUNDED_MS = 2000 ms def test_ttl_rotate_from_deadline_ms(): ex, port, reg = _exec(slot=OPEN) _run(ex.execute(_req(UrgencyClass.ROTATE, side=Side.SELL, deadline_ms=30000), NARROW)) wo = reg.working("req-0") assert wo.deadline - wo.created == 30.0 # caller's deadline def test_ttl_acquire_unbounded_uses_quote_lifetime(): from prod.exec_unified import _constants as K ex, port, reg = _exec() _run(ex.execute(_req(UrgencyClass.ACQUIRE), NARROW)) wo = reg.working("req-0") assert wo.deadline - wo.created == K.MAKER_QUOTE_TTL_S # bounded by chases, not clock