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

@@ -19,10 +19,14 @@ from prod.clean_arch.dita import (
)
from prod.clean_arch.dita_v2.contracts import (
KernelDiagnosticCode,
KernelEventKind,
KernelOutcome,
KernelSeverity,
KernelTransition,
VenueEvent,
VenueEventStatus,
)
from prod.clean_arch.dita_v2.contracts import TradeSide as DitaTradeSide
from prod.clean_arch.persistence.pink_clickhouse import PinkClickHousePersistence
@@ -324,3 +328,96 @@ def test_persistence_writes_account_reconcile_rows() -> None:
assert status_row["phase"] == "account_reconcile"
assert recon_row["event_type"] == "ACCOUNT_RECONCILE"
assert "market_state_bundle_json" in recon_row
# ---------------------------------------------------------------------------
# L0 — two-phase (request -> result) persistence
# ---------------------------------------------------------------------------
def _fill_event(kind: KernelEventKind, *, filled: float, remaining: float, price: float = 100.0) -> VenueEvent:
return VenueEvent(
timestamp=datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc),
event_id=f"EV-{kind.value}",
trade_id="BTCUSDT-T-000000000001",
slot_id=0,
kind=kind,
status=VenueEventStatus.FILLED if kind == KernelEventKind.FULL_FILL else VenueEventStatus.ACKED,
side=DitaTradeSide.SHORT,
asset="BTCUSDT",
price=price,
size=1.0,
filled_size=filled,
remaining_size=remaining,
)
def _outcome_with_events(*events: VenueEvent) -> KernelOutcome:
return KernelOutcome(
accepted=True, slot_id=0, trade_id="BTCUSDT-T-000000000001",
state=DitaTradeStage.POSITION_OPEN, diagnostic_code=KernelDiagnosticCode.OK,
severity=KernelSeverity.INFO, transitions=(), emitted_events=tuple(events), details={},
)
def test_request_row_precedes_result_rows_on_filled_entry() -> None:
"""ENTER with a FULL_FILL event: ORDER_REQUESTED is logged before ENTRY_FILLED."""
sink = _Sink()
persistence = PinkClickHousePersistence(_make_account(), sink=sink, v7_sink=sink)
persistence.persist_step(
snapshot=_make_snapshot(),
decision=_make_decision(DecisionAction.ENTER),
intent=_make_intent(DecisionAction.ENTER),
outcome=_outcome_with_events(_fill_event(KernelEventKind.FULL_FILL, filled=1.0, remaining=0.0)),
slot_dict=_make_slot_dict(closed=False, size=1.0),
phase="execution",
)
recon_types = [r["event_type"] for t, r in sink.calls if t == "trade_reconstruction"]
assert "ORDER_REQUESTED" in recon_types, recon_types
assert "ENTRY_FILLED" in recon_types, recon_types
assert recon_types.index("ORDER_REQUESTED") < recon_types.index("ENTRY_FILLED")
def test_resting_limit_entry_logs_request_but_no_fill() -> None:
"""ACK-only LIMIT entry (slot still working, size 0) -> request row, NO ENTRY_FILLED."""
sink = _Sink()
persistence = PinkClickHousePersistence(_make_account(), sink=sink, v7_sink=sink)
# Working entry order: slot not closed, size 0 (nothing filled yet).
working_slot = _make_slot_dict(closed=False, size=0.0)
working_slot["fsm_state"] = "ENTRY_WORKING"
persistence.persist_step(
snapshot=_make_snapshot(),
decision=_make_decision(DecisionAction.ENTER),
intent=_make_intent(DecisionAction.ENTER),
outcome=_outcome_with_events(_fill_event(KernelEventKind.ORDER_ACK, filled=0.0, remaining=1.0)),
slot_dict=working_slot,
phase="execution",
)
recon_types = [r["event_type"] for t, r in sink.calls if t == "trade_reconstruction"]
assert "ORDER_REQUESTED" in recon_types, recon_types
assert "ENTRY_FILLED" not in recon_types, f"resting LIMIT must not log a fill: {recon_types}"
# State snapshot rows still written (observability).
tables = [t for t, _ in sink.calls]
assert "account_events" in tables and "position_state" in tables
def test_resting_limit_exit_emits_no_terminal_rows() -> None:
"""An exit intent whose order rests (size unchanged) -> no trade_exit_legs / trade_events."""
sink = _Sink()
account = _make_account()
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
# Seed leg state as if a 1.0 position is open (prev_size = 1.0).
persistence._leg_state["BTCUSDT-T-000000000001"] = {"prev_realized": 0.0, "prev_size": 1.0, "prev_leg_id": ""}
# Exit order resting: slot still open at full size, ACK only, no fill.
resting = _make_slot_dict(closed=False, size=1.0)
persistence.persist_step(
snapshot=_make_snapshot(),
decision=_make_decision(DecisionAction.EXIT),
intent=_make_intent(DecisionAction.EXIT),
outcome=_outcome_with_events(_fill_event(KernelEventKind.ORDER_ACK, filled=0.0, remaining=1.0)),
slot_dict=resting,
phase="execution",
)
tables = [t for t, _ in sink.calls]
assert "trade_exit_legs" not in tables, "resting exit must not emit a leg row"
assert "trade_events" not in tables
assert "ORDER_REQUESTED" in [r["event_type"] for t, r in sink.calls if t == "trade_reconstruction"]