"""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