exec(unified): UnifiedExecutor — initial-submit pipeline + S2 pain-fence

executor.py composes the built parts into one execute(request, snapshot): S-grade fence →
router.decide → placer.pre_submit → submit → register working. The initial-submit half that
complements DriveLoop's expiry half (analogue of pink_direct.py:1645-1700).

Composition rulings encoded + tested:
- S2/S3 pain-fence (§15.2): refused WHOLE, loudly, below T14 — never silently sliced.
- placer declines (spread gate) → cross ONLY if urgency crosses on expiry; ACQUIRE ABANDONS
  (a missed entry is free, §4-1). Both mutation-verified RED.
- TTL from urgency discipline: PROTECT 2s / ROTATE deadline_ms / ACQUIRE quote-lifetime.

contract.py: SGrade enum (S0-S3) + s_grade/parent_request_id fields. _constants: MAKER_QUOTE_TTL_S.

FIX (real bug, not just test): drive_loop._is_resolved EXIT was trade_id-based (a PINK
artifact — PINK reused the position's trade_id for exits). The agnostic layer never gets
the position id, so exit-done is now SIZE-based. Kept ENTER on clientOrderId match.

Full exec_unified suite: 94 green, mutation-litmus verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-07-15 02:30:30 +02:00
parent a310c92977
commit 21419b03d0
6 changed files with 376 additions and 3 deletions

View File

@@ -0,0 +1,176 @@
"""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