Lands the /root/dev local increments (share was ENOSPC; now writable) into the canonical repo:
- kernel_port.py: real ExecPort over the DITAv2 kernel (duck-typed; injected to_kernel_intent).
- dialect.py §11: BingX boundary — clientOrderId(H4)/dash/quantize/payload (PostOnly=timeInForce).
- friction.py §12: effective-bps + naive-baseline savings; side-lane journal that can't raise
(b46ebd2); BingX commission-sign flip. DDL ships with code (register in applier verify-set).
- drive_loop/executor/working: Decimal size threaded through plan/working types (exit size cap).
All pure stdlib+Decimal, mutation-litmus RED on the two load-bearing asserts. 132 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
178 lines
7.6 KiB
Python
178 lines
7.6 KiB
Python
"""End-to-end: UnifiedExecutor → DriveLoop → KernelExecPort → (fake) DITAv2 kernel.
|
|
|
|
Proves the pure engine drives a kernel-shaped backend correctly — translation (side/action/
|
|
TIF/order_type), exit-size capping, maker-rest→fill, exit-escalation, taker-cross. The fake
|
|
kernel mimics ExecutionKernel's surface (slot()/process_intent_async/on_venue_event/venue).
|
|
The injected translator is identity, so the fake reads IntentSpec directly and we can assert
|
|
exactly what the port produced.
|
|
|
|
Run: /home/dolphin/siloqy_env/bin/python3 -m pytest prod/exec_unified/test_kernel_port.py -q
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from decimal import Decimal
|
|
from types import SimpleNamespace
|
|
|
|
from prod.exec_unified.contract import ExecutionRequest, Side, UrgencyClass
|
|
from prod.exec_unified.drive_loop import Action, DriveLoop
|
|
from prod.exec_unified.executor import ExecuteStatus, UnifiedExecutor
|
|
from prod.exec_unified.kernel_port import IntentSpec, KernelExecPort
|
|
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"))
|
|
|
|
|
|
def _slot(trade_id, stage, size):
|
|
return SimpleNamespace(trade_id=trade_id, fsm_state=SimpleNamespace(value=stage),
|
|
size=size, asset="BTCUSDT")
|
|
|
|
|
|
class FakeVenue:
|
|
def __init__(self):
|
|
self.events = []
|
|
self.positions = []
|
|
def reconcile(self):
|
|
ev, self.events = self.events, []
|
|
return ev
|
|
def open_positions(self):
|
|
return self.positions
|
|
|
|
|
|
class FakeKernel:
|
|
"""Minimal ExecutionKernel surface. process_intent_async mutates slot 0 like the real
|
|
single-slot FSM: MARKET fills now; LIMIT rests (slot unchanged); CANCEL is a no-op."""
|
|
def __init__(self, slot):
|
|
self._slot = slot
|
|
self.venue = FakeVenue()
|
|
self.max_slots = 1
|
|
self.processed: list[IntentSpec] = []
|
|
def slot(self, i):
|
|
return self._slot
|
|
async def process_intent_async(self, spec: IntentSpec):
|
|
self.processed.append(spec)
|
|
if spec.action_kind == "ENTER" and spec.order_type == "MARKET":
|
|
self._slot = _slot(spec.request_id, "POSITION_OPEN", spec.size)
|
|
elif spec.action_kind == "EXIT" and spec.order_type == "MARKET":
|
|
self._slot = _slot("", "IDLE", Decimal(0))
|
|
return SimpleNamespace(accepted=True)
|
|
def on_venue_event(self, e):
|
|
fn = getattr(e, "apply", None)
|
|
if fn:
|
|
fn(self)
|
|
# test helpers
|
|
def force_open(self, tid, size="1"):
|
|
self._slot = _slot(tid, "POSITION_OPEN", Decimal(size))
|
|
|
|
|
|
def _ident(spec): # local translator: kernel reads the normalized spec directly
|
|
return spec
|
|
|
|
|
|
class Clock:
|
|
def __init__(self, t=1000.0): self.t = t
|
|
def __call__(self): return self.t
|
|
|
|
|
|
def _stack(slot):
|
|
clock = Clock()
|
|
kernel = FakeKernel(slot)
|
|
port = KernelExecPort(kernel, _ident, clock=clock)
|
|
reg = WorkingRegistry(port.clock)
|
|
dl = DriveLoop(port, reg)
|
|
return UnifiedExecutor(dl), dl, kernel, reg
|
|
|
|
|
|
def _req(urgency, side=Side.BUY, size="0.001", **kw):
|
|
return ExecutionRequest(request_id="req", asset="BTCUSDT", side=side,
|
|
size=Decimal(size), urgency=urgency, **kw)
|
|
|
|
|
|
def _run(c): return asyncio.run(c)
|
|
|
|
|
|
# ── translation correctness ──────────────────────────────────────────────────
|
|
|
|
def test_maker_entry_translated_to_limit_postonly():
|
|
ex, dl, kernel, reg = _stack(_slot("", "IDLE", Decimal(0)))
|
|
r = _run(ex.execute(_req(UrgencyClass.ACQUIRE), NARROW))
|
|
assert r.status is ExecuteStatus.RESTING_MAKER
|
|
spec = kernel.processed[-1]
|
|
assert spec.action_kind == "ENTER"
|
|
assert spec.order_type == "LIMIT" and spec.tif == "PostOnly" # maker
|
|
assert spec.side is Side.BUY
|
|
assert spec.limit_price == Decimal("100") and spec.size == Decimal("0.001")
|
|
assert reg.working("req-0") is not None
|
|
|
|
|
|
def test_catastrophic_translated_to_market_gtc():
|
|
ex, dl, kernel, reg = _stack(_slot("pos", "POSITION_OPEN", Decimal("1")))
|
|
r = _run(ex.execute(_req(UrgencyClass.CATASTROPHIC, side=Side.SELL, size="1"), NARROW))
|
|
assert r.status is ExecuteStatus.CROSSED_TAKER
|
|
spec = kernel.processed[-1]
|
|
assert spec.action_kind == "EXIT" and spec.order_type == "MARKET" and spec.tif == "GTC"
|
|
assert spec.reduce_only is True
|
|
# MARKET exit closed the slot in the fake kernel
|
|
assert kernel.slot(0).size == Decimal(0)
|
|
|
|
|
|
# ── exit size capping (pink _exit_intent_from_slot) ──────────────────────────
|
|
|
|
def test_exit_size_capped_to_slot():
|
|
# Caller asks to close 10 but the slot holds 0.5 — the port caps to 0.5 (never overshoot).
|
|
ex, dl, kernel, reg = _stack(_slot("pos", "POSITION_OPEN", Decimal("0.5")))
|
|
_run(ex.execute(_req(UrgencyClass.PROTECT, side=Side.SELL, size="10"), NARROW))
|
|
spec = kernel.processed[-1]
|
|
assert spec.action_kind == "EXIT"
|
|
assert spec.size == Decimal("0.5") # capped to the authoritative remaining size
|
|
|
|
|
|
# ── full lifecycle: maker entry rests, then fills after TTL ──────────────────
|
|
|
|
def test_entry_rests_then_fills_after_ttl():
|
|
ex, dl, kernel, reg = _stack(_slot("", "IDLE", Decimal(0)))
|
|
r = _run(ex.execute(_req(UrgencyClass.ACQUIRE), NARROW))
|
|
assert r.status is ExecuteStatus.RESTING_MAKER
|
|
wo = reg.working("req-0")
|
|
assert wo is not None
|
|
# the maker quote fills — kernel slot now shows our position (trade_id == our rid0)
|
|
kernel.force_open("req-0")
|
|
res = _run(dl.handle_expired(wo))
|
|
from prod.exec_unified.drive_loop import Resolved
|
|
assert res is Resolved.FILLED_AFTER_TTL
|
|
assert reg.working("req-0") is None # cleared, not retried
|
|
|
|
|
|
# ── full lifecycle: maker exit never strands → escalates to MARKET ───────────
|
|
|
|
def test_maker_exit_escalates_to_market_and_closes():
|
|
ex, dl, kernel, reg = _stack(_slot("pos", "POSITION_OPEN", Decimal("1")))
|
|
r = _run(ex.execute(_req(UrgencyClass.PROTECT, side=Side.SELL, size="1"), NARROW))
|
|
assert r.status is ExecuteStatus.RESTING_MAKER # maker exit resting
|
|
wo = reg.working("req-0")
|
|
# TTL fires, position still open → escalate to MARKET (never strand)
|
|
from prod.exec_unified.drive_loop import Resolved
|
|
res = _run(dl.handle_expired(wo))
|
|
assert res is Resolved.ESCALATED_MARKET
|
|
assert kernel.processed[-1].order_type == "MARKET" # crossed
|
|
assert kernel.slot(0).size == Decimal(0) # position closed
|
|
|
|
|
|
# ── requote gate reads real venue positions ──────────────────────────────────
|
|
|
|
def test_requote_gate_blocks_when_venue_holds_position():
|
|
ex, dl, kernel, reg = _stack(_slot("", "IDLE", Decimal(0)))
|
|
# entry with a chase budget, so a miss would try to requote
|
|
_run(ex.execute(_req(UrgencyClass.ACQUIRE), NARROW))
|
|
wo = reg.working("req-0")
|
|
wo.meta["max_reprices"] = 1 # allow a retry attempt
|
|
kernel.venue.positions = [{"positionAmt": "0.3"}] # venue is NOT flat
|
|
from prod.exec_unified.drive_loop import Resolved
|
|
res = _run(dl.handle_expired(wo))
|
|
assert res is Resolved.REQUOTE_BLOCKED # port.open_positions() saw the position
|
|
# no new order submitted after the blocked requote
|
|
assert all(s.action_kind != "ENTER" or s.request_id == "req-0" for s in kernel.processed)
|