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>
280 lines
14 KiB
Python
280 lines
14 KiB
Python
"""The drive loop — PINK's beating heart, ported (spec §7).
|
|
|
|
This is a faithful transcription of ``pink_direct.py``'s working-order driver
|
|
(``_exec_after_submit`` L975, ``_handle_expired_working`` L1089, ``_exec_safe_to_requote``
|
|
L1049) against INJECTED seams (C1: clock, venue+kernel I/O behind one ``ExecPort``) so it
|
|
carries no ambient state and could run venue-side someday. **Every branch is a headstone —
|
|
transcribed warts-and-all, not tidied** (operator ruling: "we paid dearly for pink").
|
|
|
|
What this loop is NOT (contract §2 / audit): it never sees capital, leverage, posture, or
|
|
picks an asset — those are the kernel/brain SEAM, reached only through ``ExecPort``. Where a
|
|
PINK guard lived in that SEAM, its lesson is preserved here as a referenced comment (the
|
|
zero-silent-suppression rule), not silently dropped.
|
|
|
|
Ported: table rows #1-8,11-13,15-16 of PINK_DRIVE_LOOP_PORT_INVENTORY.md.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from decimal import Decimal
|
|
from enum import Enum
|
|
from typing import Protocol
|
|
|
|
from . import _constants as K
|
|
from .contract import Side
|
|
from .router import ExecutionMethod, RoutingDecision
|
|
from .working import Action, WorkingOrder, WorkingRegistry
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
# Kernel FSM stages that mean "a position exists" (incl. partials + exit-in-flight).
|
|
# pink_direct.py:936 `_SLOT_OPENISH`. Strings because they come from the kernel FSM seam.
|
|
OPENISH = frozenset({
|
|
"PARTIAL_FILL", "POSITION_OPENED", "POSITION_OPEN", "EXIT_REQUESTED", "EXIT_SENT",
|
|
"EXIT_ACKED", "EXIT_WORKING", "POSITION_PARTIALLY_CLOSED",
|
|
})
|
|
# Stages that mean the position is gone. pink_direct.py:1106.
|
|
CLOSED_STAGES = frozenset({"POSITION_CLOSED", "CLOSED", "TRADE_TERMINAL_WRITTEN", "IDLE"})
|
|
# Stages that mean the venue rejected our order (post-only would cross, etc.).
|
|
# pink_direct.py:1000. A rejected maker quote still registers → resolves via the TTL path.
|
|
REJECTED_STAGES = frozenset({"ORDER_REJECTED", "EXIT_REJECTED"})
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SlotView:
|
|
"""Kernel-truth snapshot of the single slot (pink_direct.py:940 `_exec_slot_view`).
|
|
``size`` is Decimal — audit H1: PINK used float, the port does not."""
|
|
|
|
trade_id: str
|
|
stage: str
|
|
size: Decimal
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ResubmitPlan:
|
|
"""A rebuilt order the loop hands back to the venue (retry / market / exit-escalation).
|
|
Venue-agnostic: the dialect turns method+limit_price into a venue payload."""
|
|
|
|
request_id: str # this attempt's id — unique per attempt (audit H4)
|
|
base_request_id: str
|
|
asset: str
|
|
side: Side
|
|
action: Action
|
|
method: ExecutionMethod
|
|
limit_price: Decimal # ignored when method is TAKER
|
|
attempt: int
|
|
reduce_only: bool
|
|
size: Decimal = Decimal("0") # base qty for the venue (threaded from the request)
|
|
|
|
|
|
# The plan the venue receives is the same shape whether it is an initial submit (executor,
|
|
# attempt 0) or a rebuilt retry/escalation (drive loop). "Resubmit" is the origin name;
|
|
# OrderPlan is the role name the executor uses.
|
|
OrderPlan = ResubmitPlan
|
|
|
|
|
|
class ExecPort(Protocol):
|
|
"""The single seam to kernel + venue. Fakeable; the loop touches the world ONLY here."""
|
|
|
|
def clock(self) -> float: ...
|
|
def slot_view(self) -> SlotView: ...
|
|
def last_own_fill_at(self) -> float: ...
|
|
async def pump(self) -> None: ... # drain venue events → kernel
|
|
async def cancel(self, wo: WorkingOrder) -> None: ... # idempotent on the venue
|
|
async def open_positions(self) -> list[dict]: ... # venue-truth flat probe
|
|
async def submit(self, plan: ResubmitPlan) -> None: ... # resubmit rebuilt order
|
|
|
|
|
|
class MissAction(Enum):
|
|
RETRY = "retry" # re-quote maker (chase budget remains)
|
|
MARKET = "market" # cross now (cross_on_expiry urgencies)
|
|
ABANDON = "abandon" # ACQUIRE: a missed entry is free — never chase, never cross (§4-1)
|
|
|
|
|
|
class Resolved(Enum):
|
|
ALREADY_RESOLVED = "already_resolved" # fill/cancel raced the sweep
|
|
FILLED_AFTER_TTL = "filled_after_ttl" # fill surfaced during cancel round-trip
|
|
ESCALATED_MARKET = "escalated_market" # EXIT never strands / entry cross
|
|
RETRIED = "retried"
|
|
ABANDONED = "abandoned" # ACQUIRE miss
|
|
SKIPPED_SLOT_BUSY = "skipped_slot_busy" # raced remainder fill — never double-enter
|
|
REQUOTE_BLOCKED = "requote_blocked" # venue not provably flat — fail safe
|
|
|
|
|
|
class DriveLoop:
|
|
"""Orchestrates working-order resolution. One instance per exec instance."""
|
|
|
|
def __init__(self, port: ExecPort, registry: WorkingRegistry,
|
|
logger: logging.Logger = LOGGER) -> None:
|
|
self.port = port
|
|
self.registry = registry
|
|
self.log = logger
|
|
|
|
# ── classification ───────────────────────────────────────────────────────
|
|
def _is_resolved(self, wo: WorkingOrder, slot: SlotView) -> bool:
|
|
"""Entry filled or exit done, per kernel truth (pink _entry_filled/_exit_done, L1099).
|
|
|
|
ENTER: our entry filled — the slot carries OUR clientOrderId (the kernel tags the new
|
|
position with it; clientOrderId echo is the best-practice fill key, audit H5) and shows
|
|
size + an open stage.
|
|
|
|
EXIT: the position is gone — size drained or a closed stage. **Deliberately SIZE-based,
|
|
not trade_id-based**: pink_direct.py:1105 could compare `slot_tid != wo.trade_id` only
|
|
because it REUSED the position's trade_id for the exit intent. This layer is agnostic
|
|
(contract §2) — the caller mints a fresh request_id for the exit and never hands us the
|
|
position's id — so an exit is "done" iff the position closed, which is the size signal.
|
|
"""
|
|
if wo.action is Action.ENTER:
|
|
return slot.trade_id == wo.request_id and slot.size > 0 and slot.stage in OPENISH
|
|
return slot.size <= 0 or slot.stage in CLOSED_STAGES
|
|
|
|
def after_submit(self, wo: WorkingOrder, *, rejected: bool) -> str:
|
|
"""Classify a maker submit: filled-now / working / rejected. pink L975.
|
|
|
|
A rejected post-only quote registers with an already-expired deadline so the TTL
|
|
sweep resolves it through the ONE shared miss/escalation path — do not build a
|
|
second path (pink L1010-1011)."""
|
|
slot = self.port.slot_view()
|
|
if self._is_resolved(wo, slot):
|
|
return "immediate_fill" # filled on submit — do not register
|
|
self.registry.register(wo)
|
|
if rejected:
|
|
self.registry.expire_now(wo.request_id)
|
|
return "working_reject"
|
|
return "working"
|
|
|
|
# ── the TTL sweep ────────────────────────────────────────────────────────
|
|
async def resolve_expired(self) -> list[Resolved]:
|
|
"""One sweep over expired quotes. Caller drives cadence (injected — no literal 1 s;
|
|
pink L1070 hardcoded 1.0, amended per spec NFR). An expiry-handler exception drops
|
|
THAT order to a logged error and continues — never let one wedge the sweep."""
|
|
out: list[Resolved] = []
|
|
for wo in self.registry.expired():
|
|
try:
|
|
out.append(await self.handle_expired(wo))
|
|
except Exception as exc: # BI-3: log, never bare-swallow (pink's silent-demon)
|
|
self.log.error("drive_loop: expiry handler failed for %s: %r",
|
|
wo.request_id, exc, exc_info=True)
|
|
return out
|
|
|
|
async def handle_expired(self, wo: WorkingOrder) -> Resolved:
|
|
"""The heart — pink_direct.py:1089, 10-step sequence, order preserved exactly."""
|
|
# 1. already resolved (a fill/cancel notification raced this sweep)
|
|
if self.registry.working(wo.request_id) is None:
|
|
return Resolved.ALREADY_RESOLVED
|
|
# 2. drain late venue events FIRST — the quote may already be filled
|
|
await self.port.pump()
|
|
if self.registry.working(wo.request_id) is None:
|
|
return Resolved.ALREADY_RESOLVED
|
|
# 3. cancel the quote (idempotent; CANCEL_REJECT on a filled order is harmless;
|
|
# on a partial entry this cancels the remainder). BI-3: log failures, don't swallow.
|
|
try:
|
|
await self.port.cancel(wo)
|
|
except Exception as exc:
|
|
self.log.warning("drive_loop: ttl-cancel %s failed: %r", wo.request_id, exc)
|
|
# 4. pump again — the cancel round-trip may have surfaced a fill
|
|
await self.port.pump()
|
|
# 5. re-classify AFTER the cancel (fill may have raced the cancel — the core race)
|
|
slot = self.port.slot_view()
|
|
if self._is_resolved(wo, slot):
|
|
self.registry.note_fill(wo.request_id)
|
|
return Resolved.FILLED_AFTER_TTL
|
|
self.registry.note_cancel(wo.request_id)
|
|
|
|
# 6. EXIT never strands a position → escalate to MARKET, same base id (pink L1147)
|
|
if wo.action is Action.EXIT:
|
|
await self.port.submit(self._resubmit(wo, ExecutionMethod.TAKER, reduce_only=True))
|
|
await self.port.pump()
|
|
self.log.warning("drive_loop: exit TTL → MARKET fallback %s", wo.request_id)
|
|
return Resolved.ESCALATED_MARKET
|
|
|
|
# 7. ENTER miss policy: retry(bounded) | market | abandon
|
|
miss = self._entry_miss_action(wo)
|
|
# 8. slot-busy guard: never double-enter on a raced remainder fill (pink L1173)
|
|
slot = self.port.slot_view()
|
|
if slot.size > 0 or slot.stage in OPENISH:
|
|
self.log.warning("drive_loop: entry miss %s: slot busy (%s) — skip",
|
|
wo.request_id, slot.stage)
|
|
return Resolved.SKIPPED_SLOT_BUSY
|
|
if miss is MissAction.ABANDON:
|
|
# ACQUIRE: a missed entry is free, a chased/crossed entry is not (§4-1). Abandon.
|
|
self.log.info("drive_loop: entry miss %s → abandon", wo.request_id)
|
|
return Resolved.ABANDONED
|
|
# 9. venue-truth requote gate: re-quote ONLY when provably flat. Ambiguity → skip;
|
|
# "a skipped entry is always safe, a doubled position is not" (pink L1180-1190).
|
|
if not await self.safe_to_requote():
|
|
self.log.warning("drive_loop: entry miss %s: venue not provably flat — skip",
|
|
wo.request_id)
|
|
return Resolved.REQUOTE_BLOCKED
|
|
# 10. resubmit retry(maker) | market(taker)
|
|
method = ExecutionMethod.MAKER if miss is MissAction.RETRY else ExecutionMethod.TAKER
|
|
await self.port.submit(self._resubmit(wo, method, reduce_only=False))
|
|
await self.port.pump()
|
|
return Resolved.RETRIED if miss is MissAction.RETRY else Resolved.ESCALATED_MARKET
|
|
|
|
# ── gates & helpers ──────────────────────────────────────────────────────
|
|
async def safe_to_requote(self) -> bool:
|
|
"""True only when the venue is PROVABLY flat. Fails SAFE on: recent own fill
|
|
(REST reconcile lags WS by seconds), any live position, or any probe error
|
|
(pink_direct.py:1049)."""
|
|
if self.port.clock() - (self.port.last_own_fill_at() or 0.0) < K.REQUOTE_HOT_WINDOW_S:
|
|
return False
|
|
try:
|
|
rows = await self.port.open_positions()
|
|
except Exception as exc:
|
|
self.log.warning("drive_loop: requote probe failed (%r) — fail safe", exc)
|
|
return False
|
|
for row in rows or []:
|
|
qty = abs(_dec(row.get("positionAmt") or row.get("positionQty")
|
|
or row.get("qty") or 0))
|
|
if qty > Decimal("1e-9"):
|
|
return False
|
|
return True
|
|
|
|
def _entry_miss_action(self, wo: WorkingOrder) -> MissAction:
|
|
"""Chase budget from the routing decision (stored on the working order at register).
|
|
ACQUIRE: max_reprices=0, cross_on_expiry=False → ABANDON. pink miss policy L1166."""
|
|
max_reprices = int(wo.meta.get("max_reprices", 0))
|
|
cross_on_expiry = bool(wo.meta.get("cross_on_expiry", False))
|
|
if wo.attempt < max_reprices:
|
|
return MissAction.RETRY
|
|
if cross_on_expiry:
|
|
return MissAction.MARKET
|
|
return MissAction.ABANDON
|
|
|
|
def _resubmit(self, wo: WorkingOrder, method: ExecutionMethod, *,
|
|
reduce_only: bool) -> ResubmitPlan:
|
|
"""Rebuild the order for a retry/market/escalation with a FRESH per-attempt id
|
|
(audit H4 / FIX OrigClOrdID: retries must not reuse a clientOrderId)."""
|
|
nxt = wo.attempt + 1
|
|
base = wo.base_request_id
|
|
return ResubmitPlan(
|
|
request_id=f"{base}-{nxt}", base_request_id=base, asset=wo.asset,
|
|
side=wo.side, action=wo.action, method=method,
|
|
limit_price=wo.limit_price, attempt=nxt, reduce_only=reduce_only,
|
|
size=wo.size,
|
|
)
|
|
|
|
|
|
def build_working_order(*, request_id: str, base_request_id: str, asset: str, side: Side,
|
|
action: Action, limit_price: Decimal, decision: RoutingDecision,
|
|
clock: float, ttl_s: float, size: Decimal = Decimal("0"),
|
|
attempt: int = 0) -> WorkingOrder:
|
|
"""Construct a WorkingOrder, stamping the policy bits the drive loop needs at expiry
|
|
(max_reprices / cross_on_expiry) so the loop never imports the brain's decision path."""
|
|
return WorkingOrder(
|
|
request_id=request_id, base_request_id=base_request_id, asset=asset, side=side,
|
|
action=action, limit_price=limit_price, deadline=clock + ttl_s, created=clock,
|
|
size=size, attempt=attempt,
|
|
meta={"max_reprices": decision.max_reprices,
|
|
"cross_on_expiry": decision.cross_on_expiry},
|
|
)
|
|
|
|
|
|
def _dec(v: object) -> Decimal:
|
|
try:
|
|
return Decimal(str(v))
|
|
except Exception:
|
|
return Decimal(0)
|