Files
siloqy/prod/tests/test_pink_clickhouse_persistence.py

327 lines
11 KiB
Python
Raw Normal View History

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