261 lines
8.8 KiB
Python
261 lines
8.8 KiB
Python
|
|
"""V2b: ExecDeadlineDriver — TTL resolution semantics over fake-kernel ports.
|
||
|
|
|
||
|
|
The real ExecutionRouter and the real DeadlineScheduler are used; only the
|
||
|
|
kernel/venue side is faked so each path (skip, retry, exhaust, market,
|
||
|
|
exit escalation, fill races, rejected-instant, fail-safe gate) can be
|
||
|
|
forced deterministically. Kernel-integration runs live in V2c.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
import sys
|
||
|
|
|
||
|
|
sys.path.insert(0, "/mnt/dolphinng5_predict")
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from prod.clean_arch.dita_v2.exec_router import ExecConfig, ExecutionRouter
|
||
|
|
from prod.clean_arch.violet.clock import DeadlineScheduler, LatencyHistogram
|
||
|
|
from prod.clean_arch.violet.exec_driver import (
|
||
|
|
ExecDeadlineDriver,
|
||
|
|
ExecDriverPorts,
|
||
|
|
)
|
||
|
|
from prod.clean_arch.violet.domain import ExecDriverSettings
|
||
|
|
from prod.clean_arch.violet.test_violet_scripted_venue import _intent
|
||
|
|
|
||
|
|
TTL_MS = 30.0
|
||
|
|
|
||
|
|
|
||
|
|
class _FakeRuntime:
|
||
|
|
"""slot_view + submit + pump fakes the driver ports plug into."""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.slot = ("", "IDLE", 0.0)
|
||
|
|
self.submitted = [] # KernelIntents the driver sent
|
||
|
|
self.pump_hook = None # optional callable run on each pump
|
||
|
|
self.flat = True
|
||
|
|
self.last_fill_ns = 0
|
||
|
|
|
||
|
|
async def submit_intent(self, intent):
|
||
|
|
self.submitted.append(intent)
|
||
|
|
return None
|
||
|
|
|
||
|
|
async def pump_events(self):
|
||
|
|
if self.pump_hook is not None:
|
||
|
|
self.pump_hook()
|
||
|
|
return 0
|
||
|
|
|
||
|
|
def ports(self, router):
|
||
|
|
return ExecDriverPorts(
|
||
|
|
router=router,
|
||
|
|
submit_intent=self.submit_intent,
|
||
|
|
pump_events=self.pump_events,
|
||
|
|
slot_view=lambda: self.slot,
|
||
|
|
venue_flat=lambda: self.flat,
|
||
|
|
last_own_fill_mono_ns=lambda: self.last_fill_ns,
|
||
|
|
reference_price=lambda asset: 100.0,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _setup(style="maker_both", miss="skip", retries=1, exhaust="skip"):
|
||
|
|
rt = _FakeRuntime()
|
||
|
|
router = ExecutionRouter(ExecConfig(
|
||
|
|
style=style, entry_miss=miss, entry_retries=retries,
|
||
|
|
retry_exhaust=exhaust))
|
||
|
|
sched = DeadlineScheduler(jitter_hist=LatencyHistogram("jit"))
|
||
|
|
driver = ExecDeadlineDriver(
|
||
|
|
rt.ports(router), sched,
|
||
|
|
settings=ExecDriverSettings(ttl_override_ms=TTL_MS))
|
||
|
|
return rt, router, sched, driver
|
||
|
|
|
||
|
|
|
||
|
|
def _enter_plan(router, tid, px=100.0):
|
||
|
|
return router.plan_entry(trade_id=tid, asset="BTCUSDT",
|
||
|
|
position_side="SHORT", reference_price=px)
|
||
|
|
|
||
|
|
|
||
|
|
def _exit_plan(router, tid, px=100.0):
|
||
|
|
return router.plan_exit(trade_id=tid, asset="BTCUSDT",
|
||
|
|
position_side="SHORT", reference_price=px,
|
||
|
|
reason="TAKE_PROFIT")
|
||
|
|
|
||
|
|
|
||
|
|
async def _drive(driver, sched, body):
|
||
|
|
sched.start()
|
||
|
|
try:
|
||
|
|
await body()
|
||
|
|
assert await driver.drain(2.0), driver.snapshot()
|
||
|
|
finally:
|
||
|
|
await sched.stop()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_expire_skip_cancels_then_clears():
|
||
|
|
rt, router, sched, driver = _setup(miss="skip")
|
||
|
|
|
||
|
|
async def body():
|
||
|
|
plan = _enter_plan(router, "E1")
|
||
|
|
driver.on_submit(plan, _intent("E1", limit_price=plan.limit_price))
|
||
|
|
assert router.working("E1") is not None
|
||
|
|
await asyncio.sleep(TTL_MS / 1000 + 0.1)
|
||
|
|
|
||
|
|
await _drive(driver, sched, body)
|
||
|
|
cancels = [i for i in rt.submitted if i.intent_id == "E1-ttlcxl"]
|
||
|
|
assert len(cancels) == 1 and cancels[0].action.value == "CANCEL"
|
||
|
|
assert router.working_orders() == []
|
||
|
|
assert driver.counters["entry_skips"] == 1
|
||
|
|
assert driver.ttl_resolution_hist.count == 1
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_retry_chain_then_exhaust_skip():
|
||
|
|
rt, router, sched, driver = _setup(miss="retry", retries=1, exhaust="skip")
|
||
|
|
|
||
|
|
async def body():
|
||
|
|
plan = _enter_plan(router, "E2")
|
||
|
|
driver.on_submit(plan, _intent("E2", limit_price=plan.limit_price))
|
||
|
|
await asyncio.sleep(2 * (TTL_MS / 1000) + 0.3) # two expiries
|
||
|
|
|
||
|
|
await _drive(driver, sched, body)
|
||
|
|
tids = [i.trade_id for i in rt.submitted if i.action.value == "ENTER"]
|
||
|
|
assert tids == ["E2-r1"] # exactly one retry
|
||
|
|
r1 = next(i for i in rt.submitted if i.trade_id == "E2-r1")
|
||
|
|
assert r1.order_type == "LIMIT"
|
||
|
|
assert r1.metadata["_time_in_force"] == "PostOnly"
|
||
|
|
assert driver.counters["entry_retries"] == 1
|
||
|
|
assert driver.counters["entry_skips"] == 1 # the exhaust
|
||
|
|
assert router.working_orders() == []
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_exhaust_market_submits_market_enter():
|
||
|
|
rt, router, sched, driver = _setup(miss="retry", retries=0, exhaust="market")
|
||
|
|
|
||
|
|
async def body():
|
||
|
|
plan = _enter_plan(router, "E3")
|
||
|
|
driver.on_submit(plan, _intent("E3", limit_price=plan.limit_price))
|
||
|
|
await asyncio.sleep(TTL_MS / 1000 + 0.15)
|
||
|
|
|
||
|
|
await _drive(driver, sched, body)
|
||
|
|
markets = [i for i in rt.submitted if i.trade_id == "E3-m"]
|
||
|
|
assert len(markets) == 1
|
||
|
|
assert markets[0].order_type == "MARKET"
|
||
|
|
assert driver.counters["entry_markets"] == 1
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_exit_expiry_escalates_market_same_trade_id():
|
||
|
|
rt, router, sched, driver = _setup()
|
||
|
|
rt.slot = ("X1", "POSITION_OPEN", 1.0) # position held by X1
|
||
|
|
|
||
|
|
async def body():
|
||
|
|
plan = _exit_plan(router, "X1")
|
||
|
|
assert plan.is_maker
|
||
|
|
driver.on_submit(plan, _intent("X1", limit_price=plan.limit_price))
|
||
|
|
await asyncio.sleep(TTL_MS / 1000 + 0.15)
|
||
|
|
|
||
|
|
await _drive(driver, sched, body)
|
||
|
|
mkt = [i for i in rt.submitted if i.intent_id == "X1-mkt"]
|
||
|
|
assert len(mkt) == 1
|
||
|
|
assert mkt[0].trade_id == "X1" # R1: SAME trade lifecycle
|
||
|
|
assert mkt[0].order_type == "MARKET"
|
||
|
|
assert mkt[0].metadata["_time_in_force"] == "GTC"
|
||
|
|
assert driver.counters["exit_market_fallbacks"] == 1
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_fill_racing_cancel_is_noted_not_retried():
|
||
|
|
rt, router, sched, driver = _setup(miss="retry", retries=3)
|
||
|
|
fired = {"n": 0}
|
||
|
|
|
||
|
|
def pump_hook():
|
||
|
|
# The SECOND pump (after the cancel) reveals the fill won the race.
|
||
|
|
fired["n"] += 1
|
||
|
|
if fired["n"] >= 2:
|
||
|
|
rt.slot = ("E4", "POSITION_OPEN", 1.0)
|
||
|
|
|
||
|
|
rt.pump_hook = pump_hook
|
||
|
|
|
||
|
|
async def body():
|
||
|
|
plan = _enter_plan(router, "E4")
|
||
|
|
driver.on_submit(plan, _intent("E4", limit_price=plan.limit_price))
|
||
|
|
await asyncio.sleep(TTL_MS / 1000 + 0.15)
|
||
|
|
|
||
|
|
await _drive(driver, sched, body)
|
||
|
|
assert driver.counters["fills_after_ttl"] == 1
|
||
|
|
assert driver.counters["entry_retries"] == 0 # no re-quote after a fill
|
||
|
|
assert [i.trade_id for i in rt.submitted
|
||
|
|
if i.action.value == "ENTER"] == []
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_on_fill_cancels_deadline_before_expiry():
|
||
|
|
rt, router, sched, driver = _setup()
|
||
|
|
|
||
|
|
async def body():
|
||
|
|
plan = _enter_plan(router, "E5")
|
||
|
|
driver.on_submit(plan, _intent("E5", limit_price=plan.limit_price))
|
||
|
|
driver.on_fill("E5") # WS fill notification
|
||
|
|
await asyncio.sleep(TTL_MS / 1000 + 0.1)
|
||
|
|
|
||
|
|
await _drive(driver, sched, body)
|
||
|
|
assert rt.submitted == [] # no cancel ever sent
|
||
|
|
assert driver.counters["deadline_fires"] == 0
|
||
|
|
assert router.working_orders() == []
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_post_only_reject_resolves_instantly():
|
||
|
|
rt, router, sched, driver = _setup(miss="skip")
|
||
|
|
rt.slot = ("E6", "ORDER_REJECTED", 0.0)
|
||
|
|
|
||
|
|
async def body():
|
||
|
|
plan = _enter_plan(router, "E6")
|
||
|
|
driver.on_submit(plan, _intent("E6", limit_price=plan.limit_price))
|
||
|
|
await asyncio.sleep(0.05) # far below TTL
|
||
|
|
|
||
|
|
await _drive(driver, sched, body)
|
||
|
|
assert driver.counters["deadline_fires"] == 1 # schedule_in(0) fired now
|
||
|
|
assert any(i.intent_id == "E6-ttlcxl" for i in rt.submitted)
|
||
|
|
assert router.working_orders() == []
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_requote_gate_fails_safe_on_probe_error():
|
||
|
|
rt, router, sched, driver = _setup(miss="retry", retries=2)
|
||
|
|
|
||
|
|
def boom():
|
||
|
|
raise RuntimeError("venue probe down")
|
||
|
|
|
||
|
|
driver.ports.venue_flat = boom
|
||
|
|
|
||
|
|
async def body():
|
||
|
|
plan = _enter_plan(router, "E7")
|
||
|
|
driver.on_submit(plan, _intent("E7", limit_price=plan.limit_price))
|
||
|
|
await asyncio.sleep(TTL_MS / 1000 + 0.15)
|
||
|
|
|
||
|
|
await _drive(driver, sched, body)
|
||
|
|
assert driver.counters["requote_blocked"] == 1
|
||
|
|
assert driver.counters["entry_retries"] == 0 # ambiguity ⇒ skip, never quote
|
||
|
|
assert router.working_orders() == []
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_immediate_fill_never_registers():
|
||
|
|
rt, router, sched, driver = _setup()
|
||
|
|
rt.slot = ("E8", "POSITION_OPENED", 1.0)
|
||
|
|
|
||
|
|
async def body():
|
||
|
|
plan = _enter_plan(router, "E8")
|
||
|
|
driver.on_submit(plan, _intent("E8", limit_price=plan.limit_price))
|
||
|
|
|
||
|
|
await _drive(driver, sched, body)
|
||
|
|
assert driver.counters["immediate_fills"] == 1
|
||
|
|
assert router.working_orders() == []
|
||
|
|
assert driver.snapshot()["pending_deadlines"] == 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(pytest.main([__file__, "-v"]))
|