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:
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user