PINK DITAv2 L0-L2: two-phase persistence + async-fill pump + LIMIT wiring

Execution-infra only (policy stays MARKET; algorithmic integrity untouched).

L0 — two-phase (request->result) persistence (pink_clickhouse.py):
- Split persist_step into persist_request (policy_events + trade_reconstruction
  ORDER_REQUESTED) and persist_result (state snapshot + per-fill lifecycle rows).
- Lifecycle rows (ENTRY_FILLED/EXIT/trade_events/trade_exit_legs) gated on
  evidence of an actual fill (FULL/PARTIAL_FILL event, closed slot, or size drop
  vs _leg_state) -> a resting LIMIT (ACK only) emits no terminal rows.
- Add persist_fill_events: synthesizes a minimal decision/intent from slot+event
  for async fills and routes through persist_result.

L1 — async-fill pump (pink_direct.py):
- PinkDirectRuntime.pump_venue_events(): venue.reconcile() -> kernel.on_venue_event
  (capital settles, FSM advances), persists applied fills; kernel dedups
  duplicates (no double-settle). Called at the start of step().

L2 — LIMIT placement (bingx_direct.py):
- submit_intent now honors _order_type/_limit_price from intent metadata
  (was hardcoded MARKET): LIMIT -> type=LIMIT + price + GTC; MARKET default;
  invalid limit price falls back to MARKET.

Offline: 63 passed (persistence/groundwork/pump/limit-payload/runtime/accounting/
flaws/kernel). MARKET path unchanged; resting LIMIT now correct end-to-end offline.
Live VST validation (L3) pending. BLUE untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-05-31 03:23:44 +02:00
parent 4651cc71d6
commit 55ed6902d8
6 changed files with 1025 additions and 28 deletions

View File

@@ -30,6 +30,7 @@ from prod.clean_arch.dita import (
)
from prod.clean_arch.dita_v2.contracts import (
KernelCommandType,
KernelDiagnosticCode,
KernelIntent,
TradeSide as DitaTradeSide,
TradeStage,
@@ -306,9 +307,68 @@ class PinkDirectRuntime:
except Exception:
return {}
async def pump_venue_events(
self, snapshot: Any | None = None, *, market_state: Any = None
) -> int:
"""Drain late (async) venue fills into the kernel and persist the result.
Resting LIMIT and partial fills arrive *after* the submitting
``process_intent`` returns. This calls ``venue.reconcile()`` and feeds
each event to ``kernel.on_venue_event`` so capital settles and the FSM
advances; the kernel dedups duplicates via ``seen_event_ids`` /
``_last_settled_pnl`` (no double-settle). Only events the kernel actually
applied (accepted, not DUPLICATE_EVENT) are persisted, via the two-phase
result-logger. Capital authority stays ``kernel.account``.
Returns the number of applied events.
"""
venue = self.kernel.venue
reconcile = getattr(venue, "reconcile", None)
if reconcile is None:
return 0
try:
events = reconcile()
if inspect.isawaitable(events):
events = await events
except Exception as exc:
self.logger.warning("Venue reconcile failed: %s", exc)
return 0
events = list(events or [])
if not events:
return 0
applied: list[Any] = []
for event in events:
try:
outcome = self.kernel.on_venue_event(event)
except Exception as exc:
self.logger.warning("on_venue_event failed: %s", exc)
continue
if getattr(outcome, "accepted", False) and getattr(
outcome, "diagnostic_code", None
) != KernelDiagnosticCode.DUPLICATE_EVENT:
applied.append(event)
if applied and self.persistence is not None:
slot_dict = self.kernel.slot(0).to_dict() if self.kernel.max_slots > 0 else {}
persist_snapshot = snapshot
if persist_snapshot is None:
persist_snapshot = SimpleNamespace(
timestamp=datetime.now(timezone.utc),
symbol=str(slot_dict.get("asset", "")),
)
self.persistence.persist_fill_events(
snapshot=persist_snapshot,
events=applied,
slot_dict=slot_dict,
market_state=market_state or {},
)
return len(applied)
async def step(self, snapshot: MarketSnapshot) -> Decision:
"""Single policy + execution cycle.
0. Pump late (async) venue fills into the kernel (LIMIT/partial settle)
1. Update market state
2. Decide (policy layer)
3. Plan (intent layer)
@@ -317,6 +377,9 @@ class PinkDirectRuntime:
6. Persist
"""
market_state = self._update_market_state_runtime(snapshot)
# Drain any late fills BEFORE the policy reads slot/account state, so a
# resting LIMIT that filled since the last cycle is reflected.
await self.pump_venue_events(snapshot, market_state=market_state)
acc = self.kernel.snapshot()["account"]
slot_view = self.kernel.slot(0) if self.kernel.max_slots > 0 else None
slot_dict = slot_view.to_dict() if slot_view is not None else {}