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.8 KiB
Python
178 lines
7.8 KiB
Python
"""KernelExecPort — the real ExecPort over the DITAv2 kernel + venue (spec §4, §7).
|
|
|
|
This is the wiring that turns the pure engine (UnifiedExecutor + DriveLoop) into something
|
|
that drives a real venue. It implements the ``ExecPort`` seam by calling the DITAv2
|
|
``ExecutionKernel`` exactly the way ``pink_direct.py`` does — process_intent_async, slot(),
|
|
on_venue_event, venue.reconcile/open_positions — WITHOUT importing or modifying the kernel.
|
|
|
|
Two design choices that matter:
|
|
* The kernel's concrete intent type (``KernelIntent``, from the vendored dita_v2) is reached
|
|
through an INJECTED ``to_kernel_intent`` translator. So this module imports nothing from
|
|
dita_v2 — it stays testable against a fake kernel, and the real wiring supplies the real
|
|
translator (side/action/TIF mapping) from a module that may import dita_v2. Clean seam.
|
|
* Everything time/order-truth-based reads the kernel/venue, never ambient state.
|
|
|
|
SEAM lessons preserved as referenced comments (zero-suppression — "we paid dearly for pink"):
|
|
* K≈E capital reconcile + enter-freeze live INSIDE the kernel (pink_direct.py:717-763); the
|
|
port only feeds venue events in via on_venue_event and never reimplements accounting.
|
|
* Fill ownership on a shared account (pink _fill_is_ours L588) is a venue-adapter concern;
|
|
the port trusts the kernel/venue's own-fill attribution and only latches the timestamp.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
import logging
|
|
import time
|
|
from dataclasses import dataclass
|
|
from decimal import Decimal
|
|
from typing import Any, Callable
|
|
|
|
from .contract import Side
|
|
from .drive_loop import Action, ExecutionMethod, OrderPlan, SlotView
|
|
from .working import WorkingOrder
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class IntentSpec:
|
|
"""Normalized, venue-agnostic intent. The injected translator turns this into the
|
|
kernel's concrete KernelIntent (side→LONG/SHORT, action→KernelCommandType, TIF→metadata).
|
|
Everything the kernel needs, nothing about the caller's world."""
|
|
|
|
request_id: str
|
|
base_request_id: str
|
|
asset: str
|
|
side: Side
|
|
action_kind: str # "ENTER" | "EXIT" | "CANCEL"
|
|
order_type: str # "MARKET" | "LIMIT"
|
|
limit_price: Decimal # 0 for MARKET
|
|
size: Decimal
|
|
tif: str # "PostOnly" | "GTC"
|
|
reduce_only: bool
|
|
reason: str
|
|
|
|
|
|
ToKernelIntent = Callable[[IntentSpec], Any]
|
|
|
|
|
|
class KernelExecPort:
|
|
"""Implements the ExecPort protocol against a DITAv2 ExecutionKernel (duck-typed)."""
|
|
|
|
def __init__(self, kernel: Any, to_kernel_intent: ToKernelIntent, *,
|
|
clock: Callable[[], float] = time.monotonic,
|
|
logger: logging.Logger = LOGGER) -> None:
|
|
self.kernel = kernel
|
|
self._to_ki = to_kernel_intent
|
|
self._clock = clock
|
|
self.log = logger
|
|
self._last_own_fill: float = 0.0
|
|
|
|
# ── read seams ───────────────────────────────────────────────────────────
|
|
def clock(self) -> float:
|
|
return self._clock()
|
|
|
|
def last_own_fill_at(self) -> float:
|
|
return self._last_own_fill
|
|
|
|
def slot_view(self) -> SlotView:
|
|
"""Kernel-truth snapshot of slot 0. Tolerant of partial mocks (pink _exec_slot_view L940)."""
|
|
try:
|
|
slot = self.kernel.slot(0)
|
|
except Exception:
|
|
return SlotView(trade_id="", stage="", size=Decimal(0))
|
|
stage = getattr(slot, "fsm_state", None)
|
|
stage_name = getattr(stage, "value", None) or str(stage or "")
|
|
return SlotView(
|
|
trade_id=str(getattr(slot, "trade_id", "") or ""),
|
|
stage=str(stage_name),
|
|
size=_dec(getattr(slot, "size", 0)),
|
|
)
|
|
|
|
async def open_positions(self) -> list[dict]:
|
|
"""Venue-truth flat probe (pink _exec_safe_to_requote L1059). Fail-safe is the
|
|
CALLER's job (drive_loop treats a raise as 'not provably flat')."""
|
|
venue = self.kernel.venue
|
|
rows = venue.open_positions() if hasattr(venue, "open_positions") else []
|
|
if inspect.isawaitable(rows):
|
|
rows = await rows
|
|
return list(rows or [])
|
|
|
|
# ── write seams ──────────────────────────────────────────────────────────
|
|
async def pump(self) -> None:
|
|
"""Drain late venue events into the kernel (pink pump_venue_events L1228). The kernel
|
|
dedups (seen_event_ids) and owns capital settlement — we only feed events and latch
|
|
the own-fill timestamp so the requote hot-window can fail safe."""
|
|
venue = self.kernel.venue
|
|
reconcile = getattr(venue, "reconcile", None)
|
|
if reconcile is None:
|
|
return
|
|
try:
|
|
events = reconcile()
|
|
if inspect.isawaitable(events):
|
|
events = await events
|
|
except Exception as exc:
|
|
self.log.warning("kernel_port.pump: venue reconcile failed: %r", exc)
|
|
return
|
|
for event in list(events or []):
|
|
try:
|
|
self.kernel.on_venue_event(event)
|
|
except Exception as exc:
|
|
self.log.warning("kernel_port.pump: on_venue_event failed: %r", exc)
|
|
continue
|
|
if _is_fill(event):
|
|
# REST reconcile lags WS fills by seconds — latch so the requote gate
|
|
# fails safe inside the hot window (pink_direct.py:648, :1055).
|
|
self._last_own_fill = self._clock()
|
|
|
|
async def cancel(self, wo: WorkingOrder) -> None:
|
|
"""Cancel a working quote via the kernel (idempotent on the venue; CANCEL_REJECT on a
|
|
filled order is harmless — pink _exec_cancel_working L1021)."""
|
|
spec = IntentSpec(
|
|
request_id=f"{wo.request_id}-cxl", base_request_id=wo.base_request_id,
|
|
asset=wo.asset, side=wo.side, action_kind="CANCEL", order_type="MARKET",
|
|
limit_price=Decimal(0), size=wo.size, tif="GTC", reduce_only=False,
|
|
reason="exec_unified:ttl_cancel",
|
|
)
|
|
await self._process(spec)
|
|
|
|
async def submit(self, plan: OrderPlan) -> None:
|
|
"""Translate an OrderPlan → kernel intent and dispatch it. Exits cap their size to
|
|
the kernel's authoritative remaining position (pink _exit_intent_from_slot L1358) so a
|
|
stale caller size can neither strand nor overshoot a close."""
|
|
size = plan.size
|
|
if plan.action is Action.EXIT:
|
|
slot_size = self.slot_view().size
|
|
if slot_size > 0:
|
|
size = min(plan.size, slot_size) if plan.size > 0 else slot_size
|
|
is_maker = plan.method is ExecutionMethod.MAKER
|
|
spec = IntentSpec(
|
|
request_id=plan.request_id, base_request_id=plan.base_request_id,
|
|
asset=plan.asset, side=plan.side, action_kind=plan.action.value,
|
|
order_type="LIMIT" if is_maker else "MARKET",
|
|
limit_price=plan.limit_price if is_maker else Decimal(0),
|
|
size=size, tif="PostOnly" if is_maker else "GTC",
|
|
reduce_only=plan.reduce_only, reason="exec_unified:submit",
|
|
)
|
|
await self._process(spec)
|
|
|
|
async def _process(self, spec: IntentSpec) -> Any:
|
|
ki = self._to_ki(spec)
|
|
result = self.kernel.process_intent_async(ki)
|
|
if inspect.isawaitable(result):
|
|
result = await result
|
|
return result
|
|
|
|
|
|
def _dec(v: object) -> Decimal:
|
|
try:
|
|
return Decimal(str(v or 0))
|
|
except Exception:
|
|
return Decimal(0)
|
|
|
|
|
|
def _is_fill(event: Any) -> bool:
|
|
kind = getattr(getattr(event, "kind", None), "value", "") or str(getattr(event, "kind", "") or "")
|
|
status = getattr(getattr(event, "status", None), "value", "") or str(getattr(event, "status", "") or "")
|
|
return "FILL" in kind.upper() or status.upper() in ("FILLED", "PARTIALLY_FILLED")
|