"""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")