Files
sentiment-engine/prod/exec_unified/kernel_port.py
Codex ec95bd6663 exec(unified): sync local build — kernel_port + dialect §11 + friction §12 + size threading
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>
2026-07-15 08:05:26 +02:00

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