First commit of the previously-untracked PINK-on-DITAv2 migration system (execution moves to the Rust kernel; policy stays on legacy DITA, so Alpha Engine algorithmic integrity is preserved). BLUE is untouched. Sprint 0 (safety snapshot + flaw-fix verification, MARKET single-leg scope): - Verified Rust FSM fixes (flaws 2,4,10,11,13) by source read of lib.rs. - Hardened 5 vacuous/guarded assertions in test_flaws.py so each flaw test genuinely exercises its fix. Most important: Flaw 5 now asserts capital moves by EXACTLY realized PnL (was entering/exiting at the same price). - Offline suites: 533 passed, 0 failed (35 flaws + 402 kernel/accounting/ bridge + 96 runtime/persistence/multi-exit/restart/seams). - GATE PASS: MARKET-path-critical flaws 1,2,5 confirmed fixed + green. - Added SPRINT0_FLAW_VERIFICATION.md report and _rust_kernel/.gitignore (excludes Rust target/ build artifacts). LIMIT/partial-fill remain explicitly out of scope (MARKET-only bring-up). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
327 lines
11 KiB
Python
327 lines
11 KiB
Python
"""PINK ClickHouse persistence tests — DITAv2 outcome + slot_dict API."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from types import SimpleNamespace
|
|
|
|
|
|
from prod.clean_arch.dita_v2.contracts import TradeStage as DitaTradeStage
|
|
from prod.clean_arch.dita import (
|
|
AccountProjection,
|
|
AccountSnapshot,
|
|
Decision,
|
|
DecisionAction,
|
|
Intent,
|
|
TradeSide,
|
|
TradeStage,
|
|
)
|
|
from prod.clean_arch.dita_v2.contracts import (
|
|
KernelDiagnosticCode,
|
|
KernelOutcome,
|
|
KernelSeverity,
|
|
KernelTransition,
|
|
)
|
|
from prod.clean_arch.persistence.pink_clickhouse import PinkClickHousePersistence
|
|
|
|
|
|
@dataclass
|
|
class _Sink:
|
|
calls: list[tuple[str, dict]] = field(default_factory=list)
|
|
|
|
def __call__(self, table: str, row: dict) -> None:
|
|
self.calls.append((table, row))
|
|
|
|
|
|
def _make_snapshot():
|
|
return SimpleNamespace(
|
|
timestamp=datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc),
|
|
symbol="BTCUSDT",
|
|
price=100.0,
|
|
)
|
|
|
|
|
|
def _make_decision(action: DecisionAction) -> Decision:
|
|
return Decision(
|
|
timestamp=datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc),
|
|
decision_id="BTCUSDT-D-000000000001",
|
|
asset="BTCUSDT",
|
|
action=action,
|
|
side=TradeSide.SHORT,
|
|
reason="STRUCTURAL_DISLOCATION" if action == DecisionAction.ENTER else "TAKE_PROFIT",
|
|
confidence=0.9,
|
|
velocity_divergence=-0.12,
|
|
irp_alignment=0.8,
|
|
reference_price=100.0,
|
|
target_size=1.0,
|
|
leverage=2.0,
|
|
bars_held=0,
|
|
stage=TradeStage.ORDER_REQUESTED,
|
|
metadata={},
|
|
)
|
|
|
|
|
|
def _make_intent(action: DecisionAction) -> Intent:
|
|
return Intent(
|
|
timestamp=datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc),
|
|
trade_id="BTCUSDT-T-000000000001",
|
|
decision_id="BTCUSDT-D-000000000001",
|
|
asset="BTCUSDT",
|
|
action=action,
|
|
side=TradeSide.SHORT,
|
|
reason="STRUCTURAL_DISLOCATION" if action == DecisionAction.ENTER else "TAKE_PROFIT",
|
|
target_size=1.0,
|
|
leverage=2.0,
|
|
reference_price=100.0,
|
|
confidence=0.9,
|
|
bars_held=0,
|
|
exit_leg_ratios=(0.5, 1.0),
|
|
metadata={"exit_ratio": 0.5},
|
|
)
|
|
|
|
|
|
def _make_account() -> AccountProjection:
|
|
return AccountProjection(
|
|
runtime_namespace="pink",
|
|
strategy_namespace="pink",
|
|
event_namespace="pink",
|
|
actor_name="PinkDirectRuntime",
|
|
exec_venue="bingx",
|
|
data_venue="binance",
|
|
ledger_authority="exchange",
|
|
snapshot=AccountSnapshot(capital=25_000.0, equity=25_000.0),
|
|
)
|
|
|
|
|
|
def _make_outcome(
|
|
accepted: bool = True,
|
|
code: KernelDiagnosticCode = KernelDiagnosticCode.OK,
|
|
) -> KernelOutcome:
|
|
return KernelOutcome(
|
|
accepted=accepted,
|
|
slot_id=0,
|
|
trade_id="BTCUSDT-T-000000000001",
|
|
state=DitaTradeStage.POSITION_OPEN,
|
|
diagnostic_code=code,
|
|
severity=KernelSeverity.INFO,
|
|
transitions=(),
|
|
emitted_events=(),
|
|
details={},
|
|
)
|
|
|
|
|
|
def _make_slot_dict(
|
|
closed: bool = False,
|
|
size: float = 1.0,
|
|
pnl: float = 0.0,
|
|
) -> dict:
|
|
return {
|
|
"slot_id": 0,
|
|
"trade_id": "BTCUSDT-T-000000000001",
|
|
"asset": "BTCUSDT",
|
|
"side": "SHORT",
|
|
"entry_price": 100.0,
|
|
"size": size,
|
|
"initial_size": 1.0,
|
|
"leverage": 2.0,
|
|
"realized_pnl": pnl,
|
|
"unrealized_pnl": 0.0,
|
|
"closed": closed,
|
|
"close_reason": "TAKE_PROFIT" if closed else "",
|
|
"fsm_state": "CLOSED" if closed else "POSITION_OPEN",
|
|
"exit_leg_ratios": [0.5, 1.0],
|
|
"active_leg_index": 0,
|
|
"active_exit_order": None,
|
|
"active_entry_order": None,
|
|
}
|
|
|
|
|
|
def _make_acc_dict(capital: float = 25120.0) -> dict:
|
|
return {
|
|
"capital": capital,
|
|
"equity": capital,
|
|
"realized_pnl": 120.0,
|
|
"unrealized_pnl": 0.0,
|
|
"open_positions": 0,
|
|
"open_notional": 0.0,
|
|
"leverage": 0.0,
|
|
}
|
|
|
|
|
|
def test_persistence_mirrors_policy_account_and_position_rows() -> None:
|
|
"""ENTER phase: policy_events, account_events, position_state, trade_reconstruction."""
|
|
sink = _Sink()
|
|
account = _make_account()
|
|
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
|
|
snapshot = _make_snapshot()
|
|
decision = _make_decision(DecisionAction.ENTER)
|
|
intent = _make_intent(DecisionAction.ENTER)
|
|
outcome = _make_outcome()
|
|
slot_dict = _make_slot_dict(closed=False, size=1.0)
|
|
acc_dict = _make_acc_dict(25000.0)
|
|
market_state = {
|
|
"market_fingerprint_choppiness_strength": 0.2,
|
|
"market_fingerprint_trend_persistence": 0.4,
|
|
"market_state_top_asset_target": "ETHUSDT",
|
|
}
|
|
|
|
persistence.persist_step(
|
|
snapshot=snapshot,
|
|
decision=decision,
|
|
intent=intent,
|
|
outcome=outcome,
|
|
slot_dict=slot_dict,
|
|
acc_dict=acc_dict,
|
|
phase="execution",
|
|
market_state=market_state,
|
|
)
|
|
|
|
tables = [t for t, _ in sink.calls]
|
|
assert "policy_events" in tables, f"Missing policy_events, got {tables}"
|
|
assert "v7_decision_events" in tables
|
|
assert "account_events" in tables
|
|
assert "position_state" in tables
|
|
assert "status_snapshots" in tables
|
|
assert "trade_reconstruction" in tables
|
|
assert "trade_events" not in tables, "No trade_events on ENTER"
|
|
|
|
policy = next(row for t, row in sink.calls if t == "policy_events")
|
|
v7 = next(row for t, row in sink.calls if t == "v7_decision_events")
|
|
position_row = next(row for t, row in sink.calls if t == "position_state")
|
|
recon_row = next(row for t, row in sink.calls if t == "trade_reconstruction")
|
|
|
|
assert policy["trade_id"] == intent.trade_id
|
|
assert policy["action"] == "ENTER"
|
|
assert policy == v7
|
|
assert "market_state_bundle_json" in position_row
|
|
assert position_row["tp_base_pct"] == 0.0
|
|
assert recon_row["market_state_bundle_json"]
|
|
assert "market_fingerprint_choppiness_strength" in recon_row["market_state_bundle_json"]
|
|
|
|
|
|
def test_persistence_writes_anomaly_for_diagnostic() -> None:
|
|
"""Non-OK diagnostic_code emits anomaly_events row."""
|
|
sink = _Sink()
|
|
account = _make_account()
|
|
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
|
|
snapshot = _make_snapshot()
|
|
decision = _make_decision(DecisionAction.ENTER)
|
|
intent = _make_intent(DecisionAction.ENTER)
|
|
outcome = _make_outcome(accepted=False, code=KernelDiagnosticCode.ORDER_REJECTED)
|
|
slot_dict = _make_slot_dict(closed=False, size=0.0)
|
|
acc_dict = _make_acc_dict(25000.0)
|
|
|
|
persistence.persist_step(
|
|
snapshot=snapshot,
|
|
decision=decision,
|
|
intent=intent,
|
|
outcome=outcome,
|
|
slot_dict=slot_dict,
|
|
acc_dict=acc_dict,
|
|
phase="execution",
|
|
)
|
|
|
|
tables = [t for t, _ in sink.calls]
|
|
assert "anomaly_events" in tables, f"Missing anomaly_events, got {tables}"
|
|
anomaly = next(row for t, row in sink.calls if t == "anomaly_events")
|
|
assert anomaly["anomaly"] == "ORDER_REJECTED"
|
|
|
|
|
|
def test_persistence_writes_terminal_trade_event_on_close() -> None:
|
|
"""EXIT with slot_dict.closed=True writes trade_events."""
|
|
sink = _Sink()
|
|
account = _make_account()
|
|
account.snapshot.capital = 25120.0
|
|
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
|
|
snapshot = _make_snapshot()
|
|
decision = _make_decision(DecisionAction.EXIT)
|
|
intent = _make_intent(DecisionAction.EXIT)
|
|
outcome = _make_outcome()
|
|
slot_dict = _make_slot_dict(closed=True, size=0.0, pnl=120.0)
|
|
acc_dict = _make_acc_dict(25120.0)
|
|
market_state = {"market_fingerprint_mean_reversion_strength": 0.3}
|
|
|
|
persistence.persist_step(
|
|
snapshot=snapshot,
|
|
decision=decision,
|
|
intent=intent,
|
|
outcome=outcome,
|
|
slot_dict=slot_dict,
|
|
acc_dict=acc_dict,
|
|
phase="execution",
|
|
market_state=market_state,
|
|
)
|
|
|
|
tables = [t for t, _ in sink.calls]
|
|
assert "trade_events" in tables, f"Missing trade_events, got {tables}"
|
|
trade = next(row for t, row in sink.calls if t == "trade_events")
|
|
assert trade["exit_reason"] == "TAKE_PROFIT"
|
|
assert trade["trade_id"] == intent.trade_id
|
|
assert "market_state_bundle_json" in trade
|
|
assert "market_fingerprint_mean_reversion_strength" in trade["market_state_bundle_json"]
|
|
|
|
|
|
def test_persistence_writes_anomaly_and_recovery_rows() -> None:
|
|
"""record_anomaly() + persist_recovery_state() write correct rows."""
|
|
sink = _Sink()
|
|
account = _make_account()
|
|
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
|
|
snapshot = _make_snapshot()
|
|
decision = _make_decision(DecisionAction.HOLD)
|
|
intent = _make_intent(DecisionAction.HOLD)
|
|
|
|
persistence.record_anomaly(
|
|
snapshot=snapshot,
|
|
decision=decision,
|
|
intent=intent,
|
|
anomaly="hung_exit",
|
|
origin="injected",
|
|
sensor="m8_execution_integrity",
|
|
detail="forced drop",
|
|
rm_meta=0.42,
|
|
)
|
|
persistence.persist_recovery_state(
|
|
snapshot=snapshot,
|
|
acc_dict={},
|
|
market_state={"market_fingerprint_dd_pressure": 0.2},
|
|
)
|
|
|
|
tables = [t for t, _ in sink.calls]
|
|
assert "anomaly_events" in tables
|
|
anomaly = next(row for t, row in sink.calls if t == "anomaly_events")
|
|
assert anomaly["anomaly"] == "hung_exit"
|
|
assert anomaly["sensor"] == "m8_execution_integrity"
|
|
assert "status_snapshots" in tables
|
|
assert "account_events" in tables
|
|
assert "position_state" in tables
|
|
|
|
|
|
def test_persistence_writes_account_reconcile_rows() -> None:
|
|
"""persist_recovery_state with account_reconcile phase writes correct rows."""
|
|
sink = _Sink()
|
|
account = _make_account()
|
|
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
|
|
snapshot = _make_snapshot()
|
|
|
|
persistence.persist_recovery_state(
|
|
snapshot=snapshot,
|
|
acc_dict={},
|
|
phase="account_reconcile",
|
|
event_type="ACCOUNT_RECONCILE",
|
|
market_state={"market_fingerprint_return_entropy": 0.1},
|
|
)
|
|
|
|
tables = [t for t, _ in sink.calls]
|
|
assert "status_snapshots" in tables
|
|
assert "account_events" in tables
|
|
assert "position_state" in tables
|
|
assert "trade_reconstruction" in tables
|
|
account_row = next(row for t, row in sink.calls if t == "account_events")
|
|
status_row = next(row for t, row in sink.calls if t == "status_snapshots")
|
|
recon_row = next(row for t, row in sink.calls if t == "trade_reconstruction")
|
|
assert account_row["event_type"] == "ACCOUNT_RECONCILE"
|
|
assert status_row["phase"] == "account_reconcile"
|
|
assert recon_row["event_type"] == "ACCOUNT_RECONCILE"
|
|
assert "market_state_bundle_json" in recon_row
|