"""Comprehensive persistence layer tests — all mocked, no CH/BingX I/O. Covers: Unit: exit_price, capital_before/after, pnl_leg, entry_bar, recovery trade_id, external-position exit_qty, persist_fill_events price propagation. Chaos: None fields, zero prices, NaN pnl, missing slot keys, multi-leg exits, restart-mid-trade recovery, concurrent fills, leg ratio mismatches. """ from __future__ import annotations import math import sys sys.path.insert(0, "/mnt/dolphinng5_predict") import pytest from dataclasses import replace from datetime import datetime, timezone from types import SimpleNamespace from unittest.mock import MagicMock from prod.clean_arch.dita_v2.account import AccountProjection from prod.clean_arch.dita_v2.contracts import ( KernelDiagnosticCode, KernelEventKind, KernelOutcome, KernelSeverity, TradeStage as KernelStage, VenueEvent, VenueEventStatus, TradeSide as KTradeSide, ) from prod.clean_arch.dita import ( Decision, DecisionAction, Intent, TradeSide, TradeStage, ) from prod.clean_arch.persistence.pink_clickhouse import ( PinkClickHousePersistence, PinkClickHousePersistenceConfig, ) # --------------------------------------------------------------------------- # Fixtures / factories # --------------------------------------------------------------------------- _TS = datetime(2026, 6, 5, 10, 0, 0, tzinfo=timezone.utc) def _snap(price: float = 1.1683, symbol: str = "XRP-USDT") -> SimpleNamespace: return SimpleNamespace(timestamp=_TS, price=price, symbol=symbol) def _account(capital: float = 25_000.0, pnl: float = 0.0) -> AccountProjection: acc = AccountProjection() acc.snapshot.capital = capital acc.snapshot.peak_capital = capital acc.snapshot.equity = capital acc.snapshot.realized_pnl = pnl return acc def _decision( action: DecisionAction = DecisionAction.ENTER, asset: str = "XRP-USDT", side: TradeSide = TradeSide.SHORT, price: float = 1.1683, size: float = 5027.0, bars_held: int = 3, reason: str = "test_reason", trade_id: str = "", # ignored — Decision has no trade_id; use for _intent pairing only ) -> Decision: return Decision( timestamp=_TS, decision_id="d-001", asset=asset, action=action, side=side, reason=reason, confidence=0.8, velocity_divergence=0.02, irp_alignment=0.5, reference_price=price, target_size=size, leverage=1.0, bars_held=bars_held, stage=TradeStage.ORDER_REQUESTED, metadata={}, ) def _intent( action: DecisionAction = DecisionAction.ENTER, asset: str = "XRP-USDT", side: TradeSide = TradeSide.SHORT, price: float = 1.1683, size: float = 5027.0, bars_held: int = 3, trade_id: str = "XRP-USDT", ) -> Intent: return Intent( timestamp=_TS, trade_id=trade_id, decision_id="d-001", asset=asset, action=action, side=side, reason="test_reason", target_size=size, leverage=1.0, reference_price=price, confidence=0.8, exit_leg_ratios=(1.0,), bars_held=bars_held, stage=TradeStage.ORDER_REQUESTED, metadata={}, ) def _slot( trade_id: str = "XRP-USDT", asset: str = "XRP-USDT", side: str = "SHORT", entry_price: float = 1.1683, size: float = 5027.0, initial_size: float = 5027.0, realized_pnl: float = 0.0, closed: bool = False, active_leg_index: int = 0, leverage: float = 1.0, close_reason: str = "", exit_leg_ratios: tuple = (1.0,), ) -> dict: return { "trade_id": trade_id, "asset": asset, "side": side, "entry_price": entry_price, "size": size, "initial_size": initial_size, "realized_pnl": realized_pnl, "closed": closed, "active_leg_index": active_leg_index, "leverage": leverage, "close_reason": close_reason, "exit_leg_ratios": list(exit_leg_ratios), "slot_id": 0, } def _outcome( accepted: bool = True, trade_id: str = "XRP-USDT", state: KernelStage = KernelStage.POSITION_OPEN, events: tuple = (), ) -> KernelOutcome: return KernelOutcome( accepted=accepted, slot_id=0, trade_id=trade_id, state=state, diagnostic_code=KernelDiagnosticCode.OK, severity=KernelSeverity.INFO, transitions=(), emitted_events=events, details={}, ) def _fill_event( price: float = 1.1700, size: float = 5027.0, kind: KernelEventKind = KernelEventKind.FULL_FILL, trade_id: str = "XRP-USDT", asset: str = "XRP-USDT", ) -> VenueEvent: return VenueEvent( timestamp=_TS, event_id="ev-001", trade_id=trade_id, slot_id=0, kind=kind, status=VenueEventStatus.FILLED, venue_order_id="ord-001", venue_client_id="cid-001", side=KTradeSide.SHORT, asset=asset, price=price, size=size, filled_size=size, remaining_size=0.0, reason="", raw_payload={}, metadata={}, ) def _sink_and_persist(capital: float = 25_000.0, pnl: float = 0.0, kernel=None): rows: list[tuple[str, dict]] = [] acc = _account(capital, pnl) cfg = PinkClickHousePersistenceConfig( strategy="pink", runtime_namespace="dita_v2", strategy_namespace="dita_v2", event_namespace="dita_v2", initial_capital=25_000.0, ) p = PinkClickHousePersistence( account=acc, config=cfg, sink=lambda t, r: rows.append((t, r)), v7_sink=lambda t, r: rows.append((t, r)), kernel=kernel, ) return p, rows, acc def _rows_of(rows, table: str) -> list[dict]: return [r for t, r in rows if t == table] # =========================================================================== # 1. EXIT_PRICE — must not equal entry_price # =========================================================================== class TestExitPrice: """G21: exit_price in trade_events must reflect the fill/order price, not entry.""" def test_exit_price_differs_from_entry_price(self): """Entry=1.1683, exit order at 1.1700 — trade_events.exit_price must be 1.1700.""" entry = 1.1683 exit_ref = 1.1700 pnl = 5027.0 * (entry - exit_ref) # SHORT: profit when price falls, loss here p, rows, acc = _sink_and_persist(capital=25_000.0 + pnl, pnl=pnl) # Seed leg state (simulating a prior ENTER) p._leg_state["XRP-USDT"] = { "prev_realized": 0.0, "prev_size": 5027.0, "prev_leg_id": "", } closed_slot = _slot( entry_price=entry, size=0.0, initial_size=5027.0, realized_pnl=pnl, closed=True, active_leg_index=1, ) dec = _decision(action=DecisionAction.EXIT, price=exit_ref) intent = _intent(action=DecisionAction.EXIT, price=exit_ref) fill_ev = _fill_event(price=exit_ref, size=5027.0) outcome = _outcome(state=KernelStage.CLOSED, events=(fill_ev,)) p.persist_result( snapshot=_snap(), decision=dec, intent=intent, outcome=outcome, slot_dict=closed_slot, phase="execution", ) te = _rows_of(rows, "trade_events") assert te, "trade_events row must be written on closed slot" assert te[0]["exit_price"] != entry, ( f"exit_price ({te[0]['exit_price']}) must NOT equal entry_price ({entry})" ) assert abs(te[0]["exit_price"] - exit_ref) < 1e-9, ( f"exit_price ({te[0]['exit_price']}) should be intent.reference_price={exit_ref}" ) def test_exit_price_fallback_to_decision_price_when_intent_zero(self): """If intent.reference_price=0, fall back to decision.reference_price.""" entry = 1.1683 exit_ref = 1.1700 pnl = -85.46 p, rows, acc = _sink_and_persist(capital=25_000.0 + pnl, pnl=pnl) p._leg_state["XRP-USDT"] = {"prev_realized": 0.0, "prev_size": 5027.0, "prev_leg_id": ""} closed_slot = _slot(entry_price=entry, size=0.0, realized_pnl=pnl, closed=True, active_leg_index=1) dec = _decision(action=DecisionAction.EXIT, price=exit_ref) intent = _intent(action=DecisionAction.EXIT, price=0.0) # zero intent price outcome = _outcome(state=KernelStage.CLOSED) p.persist_result( snapshot=_snap(), decision=dec, intent=intent, outcome=outcome, slot_dict=closed_slot, phase="execution", ) te = _rows_of(rows, "trade_events") assert te, "trade_events must be written" assert te[0]["exit_price"] > 0, "exit_price must be > 0 (should fall back to decision price)" assert te[0]["exit_price"] != entry, "exit_price must not be entry_price" def test_persist_fill_events_exit_price_uses_event_price(self): """persist_fill_events must propagate the venue fill price to trade_events.exit_price.""" entry = 1.1683 fill_price = 1.1650 # actual fill from exchange pnl = 5027.0 * (entry - fill_price) # SHORT profit p, rows, acc = _sink_and_persist(capital=25_000.0 + pnl, pnl=pnl) p._leg_state["XRP-USDT"] = {"prev_realized": 0.0, "prev_size": 5027.0, "prev_leg_id": ""} closed_slot = _slot(entry_price=entry, size=0.0, realized_pnl=pnl, closed=True, active_leg_index=1) fill_ev = _fill_event(price=fill_price, size=5027.0, kind=KernelEventKind.FULL_FILL) p.persist_fill_events( snapshot=_snap(), events=[fill_ev], slot_dict=closed_slot, ) te = _rows_of(rows, "trade_events") assert te, "trade_events must be written for closed fill" assert abs(te[0]["exit_price"] - fill_price) < 1e-9, ( f"exit_price ({te[0]['exit_price']}) must match fill price {fill_price}" ) # =========================================================================== # 2. CAPITAL_BEFORE / CAPITAL_AFTER # =========================================================================== class TestCapitalFields: """capital_before and capital_after must be finite, ordered, and non-null.""" def test_capital_after_equals_account_capital(self): cap_after = 25_147.32 pnl = 147.32 p, rows, acc = _sink_and_persist(capital=cap_after, pnl=pnl) p._leg_state["XRP-USDT"] = {"prev_realized": 0.0, "prev_size": 5027.0, "prev_leg_id": ""} closed_slot = _slot(size=0.0, realized_pnl=pnl, closed=True, active_leg_index=1) dec = _decision(action=DecisionAction.EXIT, price=1.16) intent = _intent(action=DecisionAction.EXIT, price=1.16) outcome = _outcome(state=KernelStage.CLOSED) p.persist_result(snapshot=_snap(), decision=dec, intent=intent, outcome=outcome, slot_dict=closed_slot) te = _rows_of(rows, "trade_events") assert te assert abs(te[0]["capital_after"] - cap_after) < 1e-6 assert abs(te[0]["capital_before"] - (cap_after - pnl)) < 1e-6 def test_capital_before_lt_after_on_profitable_trade(self): """Profitable SHORT: capital_after > capital_before.""" entry = 1.1683 exit_p = 1.1500 pnl = 5027.0 * (entry - exit_p) # positive PnL for SHORT p, rows, acc = _sink_and_persist(capital=25_000.0 + pnl, pnl=pnl) p._leg_state["XRP-USDT"] = {"prev_realized": 0.0, "prev_size": 5027.0, "prev_leg_id": ""} closed_slot = _slot(size=0.0, realized_pnl=pnl, closed=True, active_leg_index=1) p.persist_result( snapshot=_snap(), decision=_decision(action=DecisionAction.EXIT, price=exit_p), intent=_intent(action=DecisionAction.EXIT, price=exit_p), outcome=_outcome(state=KernelStage.CLOSED), slot_dict=closed_slot, ) te = _rows_of(rows, "trade_events") assert te assert te[0]["capital_after"] > te[0]["capital_before"] def test_capital_before_gt_after_on_losing_trade(self): """Losing SHORT: capital_after < capital_before.""" entry = 1.1683 exit_p = 1.1900 pnl = 5027.0 * (entry - exit_p) # negative for SHORT when price rises p, rows, acc = _sink_and_persist(capital=25_000.0 + pnl, pnl=pnl) p._leg_state["XRP-USDT"] = {"prev_realized": 0.0, "prev_size": 5027.0, "prev_leg_id": ""} closed_slot = _slot(size=0.0, realized_pnl=pnl, closed=True, active_leg_index=1) p.persist_result( snapshot=_snap(), decision=_decision(action=DecisionAction.EXIT, price=exit_p), intent=_intent(action=DecisionAction.EXIT, price=exit_p), outcome=_outcome(state=KernelStage.CLOSED), slot_dict=closed_slot, ) te = _rows_of(rows, "trade_events") assert te assert te[0]["capital_after"] < te[0]["capital_before"] def test_exit_leg_capital_fields_populated(self): """trade_exit_legs capital_before and capital_after must be finite floats.""" pnl = 120.0 p, rows, acc = _sink_and_persist(capital=25_000.0 + pnl, pnl=pnl) p._leg_state["XRP-USDT"] = {"prev_realized": 0.0, "prev_size": 5027.0, "prev_leg_id": ""} closed_slot = _slot(size=0.0, realized_pnl=pnl, closed=True, active_leg_index=1) p.persist_result( snapshot=_snap(), decision=_decision(action=DecisionAction.EXIT, price=1.16), intent=_intent(action=DecisionAction.EXIT, price=1.16), outcome=_outcome(state=KernelStage.CLOSED), slot_dict=closed_slot, ) legs = _rows_of(rows, "trade_exit_legs") assert legs, "trade_exit_legs must be written" assert math.isfinite(legs[0]["capital_before"]) assert math.isfinite(legs[0]["capital_after"]) assert legs[0]["capital_before"] > 0 assert legs[0]["capital_after"] > 0 # =========================================================================== # 3. PNL_LEG — exit quantity correctness # =========================================================================== class TestPnlLeg: def test_pnl_leg_correct_single_leg(self): """Single-leg exit: pnl_leg must equal total realized_pnl.""" pnl = 200.0 p, rows, acc = _sink_and_persist(capital=25_200.0, pnl=pnl) p._leg_state["XRP-USDT"] = {"prev_realized": 0.0, "prev_size": 5027.0, "prev_leg_id": ""} closed_slot = _slot(size=0.0, realized_pnl=pnl, closed=True, active_leg_index=1) p.persist_result( snapshot=_snap(), decision=_decision(action=DecisionAction.EXIT), intent=_intent(action=DecisionAction.EXIT), outcome=_outcome(state=KernelStage.CLOSED), slot_dict=closed_slot, ) legs = _rows_of(rows, "trade_exit_legs") assert legs assert abs(legs[0]["pnl_leg"] - pnl) < 1e-9 def test_exit_qty_nonzero_when_leg_state_tracked(self): """When leg_state has prev_size=5027, exit_qty must be 5027 for full close.""" p, rows, acc = _sink_and_persist(capital=25_000.0) p._leg_state["XRP-USDT"] = {"prev_realized": 0.0, "prev_size": 5027.0, "prev_leg_id": ""} closed_slot = _slot(size=0.0, initial_size=5027.0, realized_pnl=0.0, closed=True, active_leg_index=1) p.persist_result( snapshot=_snap(), decision=_decision(action=DecisionAction.EXIT), intent=_intent(action=DecisionAction.EXIT, size=5027.0), outcome=_outcome(state=KernelStage.CLOSED), slot_dict=closed_slot, ) legs = _rows_of(rows, "trade_exit_legs") assert legs assert legs[0]["exit_qty"] > 0, "exit_qty must be positive when leg was tracked" def test_exit_qty_nonzero_for_external_position(self): """When leg_state missing (external position), exit_qty must still be > 0.""" # XRP position detected via reconcile — no prior ENTER persist p, rows, acc = _sink_and_persist(capital=25_000.0, pnl=150.0) # _leg_state["XRP-USDT"] intentionally NOT set closed_slot = _slot(size=0.0, initial_size=5027.0, realized_pnl=150.0, closed=True, active_leg_index=1) p.persist_result( snapshot=_snap(), decision=_decision(action=DecisionAction.EXIT), intent=_intent(action=DecisionAction.EXIT, size=5027.0), outcome=_outcome(state=KernelStage.CLOSED), slot_dict=closed_slot, ) legs = _rows_of(rows, "trade_exit_legs") assert legs, "trade_exit_legs must be written even for external positions" assert legs[0]["exit_qty"] > 0, ( f"exit_qty ({legs[0]['exit_qty']}) must be > 0 for external position close" ) def test_multi_leg_pnl_leg_incremental(self): """Two-leg exit: each leg's pnl_leg must be the delta, not cumulative.""" p, rows, acc = _sink_and_persist(capital=25_000.0, pnl=0.0) p._leg_state["T1"] = {"prev_realized": 0.0, "prev_size": 1000.0, "prev_leg_id": ""} # Leg 1: partial exit, 400 qty, pnl = 50 slot_leg1 = _slot("T1", size=600.0, initial_size=1000.0, realized_pnl=50.0, closed=False, active_leg_index=1, exit_leg_ratios=(0.4, 1.0)) acc.snapshot.capital = 25_050.0 acc.snapshot.realized_pnl = 50.0 p.persist_result( snapshot=_snap(), decision=_decision(action=DecisionAction.EXIT, trade_id="T1"), intent=_intent(action=DecisionAction.EXIT, trade_id="T1", size=400.0), outcome=_outcome(state=KernelStage.POSITION_OPEN), slot_dict=slot_leg1, ) # Leg 2: final exit, 600 qty, additional pnl = 80 slot_leg2 = _slot("T1", size=0.0, initial_size=1000.0, realized_pnl=130.0, closed=True, active_leg_index=2, exit_leg_ratios=(0.4, 1.0)) acc.snapshot.capital = 25_130.0 acc.snapshot.realized_pnl = 130.0 p.persist_result( snapshot=_snap(), decision=_decision(action=DecisionAction.EXIT, trade_id="T1"), intent=_intent(action=DecisionAction.EXIT, trade_id="T1", size=600.0), outcome=_outcome(state=KernelStage.CLOSED), slot_dict=slot_leg2, ) legs = _rows_of(rows, "trade_exit_legs") assert len(legs) == 2, f"Expected 2 exit leg rows, got {len(legs)}" assert abs(legs[0]["pnl_leg"] - 50.0) < 1e-9, f"Leg 1 pnl_leg={legs[0]['pnl_leg']}, want 50" assert abs(legs[1]["pnl_leg"] - 80.0) < 1e-9, f"Leg 2 pnl_leg={legs[1]['pnl_leg']}, want 80" # =========================================================================== # 4. ENTRY_BAR — must not map active_leg_index # =========================================================================== class TestEntryBar: def test_entry_bar_uses_bars_held_not_leg_index(self): """entry_bar in position_state must reflect bars_held, not active_leg_index.""" bars = 7 p, rows, acc = _sink_and_persist() slot = _slot(active_leg_index=3) # leg_index=3, bars_held=7 — must not use 3 dec = _decision(bars_held=bars) intent = _intent(bars_held=bars) p.persist_result( snapshot=_snap(), decision=dec, intent=intent, outcome=_outcome(), slot_dict=slot, ) ps = _rows_of(rows, "position_state") assert ps assert ps[0]["entry_bar"] != 3, "entry_bar must not be active_leg_index (3)" assert ps[0]["entry_bar"] == bars, f"entry_bar should be bars_held={bars}" def test_entry_bar_zero_when_hold(self): """HOLD: entry_bar must be 0 (or bars_held=0), not active_leg_index.""" p, rows, acc = _sink_and_persist() slot = _slot(active_leg_index=5) dec = _decision(action=DecisionAction.HOLD, bars_held=0) intent = _intent(action=DecisionAction.HOLD, bars_held=0) p.persist_result(snapshot=_snap(), decision=dec, intent=intent, outcome=None, slot_dict=slot) ps = _rows_of(rows, "position_state") assert ps assert ps[0]["entry_bar"] != 5, "entry_bar must not be active_leg_index" # =========================================================================== # 5. RECOVERY — trade_id must come from slot, not account dict # =========================================================================== class TestRecoveryPersist: def test_recovery_trade_id_from_kernel_slot(self): """persist_recovery_state must read trade_id from kernel.slot(0), not acc_dict.""" slot_view = MagicMock() slot_view.to_dict.return_value = { "trade_id": "XRP-USDT", "asset": "XRP-USDT", "side": "SHORT", "entry_price": 1.1683, "size": 5027.0, "initial_size": 5027.0, "realized_pnl": 0.0, "closed": False, "active_leg_index": 0, "leverage": 1.0, "close_reason": "", "exit_leg_ratios": [1.0], } kernel = MagicMock() kernel.slot.return_value = slot_view kernel.snapshot.return_value = {"account": {}} kernel.max_slots = 1 p, rows, acc = _sink_and_persist(kernel=kernel) acc_dict = {"capital": 103747.0, "equity": 103747.0, "realized_pnl": 0.0} # no trade_id! p.persist_recovery_state( snapshot=_snap(), acc_dict=acc_dict, phase="recovery", ) recs = _rows_of(rows, "trade_reconstruction") assert recs, "trade_reconstruction must be written for recovery" assert recs[0]["trade_id"] == "XRP-USDT", ( f"trade_id must come from kernel slot, got '{recs[0]['trade_id']}'" ) def test_recovery_trade_id_empty_when_kernel_not_wired(self): """Without kernel, recovery trade_id is empty (not crash).""" p, rows, acc = _sink_and_persist() acc_dict = {"capital": 25000.0} p.persist_recovery_state(snapshot=_snap(), acc_dict=acc_dict) recs = _rows_of(rows, "trade_reconstruction") assert recs # row is written, just with empty trade_id def test_recovery_with_open_position_in_kernel(self): """Recovery when kernel has open slot writes OPEN status.""" slot_view = MagicMock() slot_view.to_dict.return_value = _slot( trade_id="ATOM-T1", asset="ATOM-USDT", size=200.0, closed=False, ) kernel = MagicMock() kernel.slot.return_value = slot_view kernel.snapshot.return_value = {"account": {}} kernel.max_slots = 1 p, rows, acc = _sink_and_persist(kernel=kernel) p.persist_recovery_state(snapshot=_snap(), acc_dict={"capital": 25000.0}) ps = _rows_of(rows, "position_state") assert ps assert ps[0]["status"] in ("OPEN", "RECOVERED_OPEN", "FLAT") # =========================================================================== # 6. FULL ENTER → EXIT lifecycle — all fields non-null # =========================================================================== class TestFullLifecycle: def test_full_enter_exit_no_null_financial_fields(self): """Complete ENTER + EXIT: trade_events must have no null financial fields.""" entry_price = 1.1683 exit_price = 1.1500 qty = 5027.0 pnl = qty * (entry_price - exit_price) # SHORT profit p, rows, acc = _sink_and_persist(capital=25_000.0) # --- ENTER --- snap = _snap(entry_price) enter_slot = _slot(entry_price=entry_price, size=qty, initial_size=qty) enter_out = _outcome(state=KernelStage.POSITION_OPEN, events=(_fill_event(entry_price, qty, KernelEventKind.FULL_FILL),)) p.persist_result( snapshot=snap, decision=_decision(DecisionAction.ENTER, price=entry_price, size=qty), intent=_intent(DecisionAction.ENTER, price=entry_price, size=qty), outcome=enter_out, slot_dict=enter_slot, phase="execution", ) # --- EXIT --- acc.snapshot.capital = 25_000.0 + pnl acc.snapshot.realized_pnl = pnl exit_snap = _snap(exit_price) exit_slot = _slot( entry_price=entry_price, size=0.0, initial_size=qty, realized_pnl=pnl, closed=True, active_leg_index=1, ) exit_fill = _fill_event(exit_price, qty, KernelEventKind.FULL_FILL) exit_out = _outcome(state=KernelStage.CLOSED, events=(exit_fill,)) p.persist_result( snapshot=exit_snap, decision=_decision(DecisionAction.EXIT, price=exit_price, size=qty), intent=_intent(DecisionAction.EXIT, price=exit_price, size=qty), outcome=exit_out, slot_dict=exit_slot, phase="execution", ) te = _rows_of(rows, "trade_events") assert te, "trade_events must be written on close" row = te[0] # Critical financial fields must be non-null and finite for field in ("pnl", "capital_before", "capital_after", "entry_price", "exit_price"): assert row[field] is not None, f"trade_events.{field} must not be None" assert math.isfinite(float(row[field])), f"trade_events.{field} must be finite" assert row["exit_price"] != row["entry_price"], "exit_price must differ from entry_price" assert row["capital_after"] > row["capital_before"], "profitable trade: cap_after > cap_before" def test_enter_writes_entry_filled_reconstruction(self): """ENTER fill must write ENTRY_FILLED to trade_reconstruction.""" p, rows, acc = _sink_and_persist() enter_slot = _slot(size=5027.0, initial_size=5027.0) fill = _fill_event(1.1683, 5027.0, KernelEventKind.FULL_FILL) out = _outcome(state=KernelStage.POSITION_OPEN, events=(fill,)) p.persist_result( snapshot=_snap(), decision=_decision(DecisionAction.ENTER), intent=_intent(DecisionAction.ENTER), outcome=out, slot_dict=enter_slot, ) recs = _rows_of(rows, "trade_reconstruction") types = [r["event_type"] for r in recs] assert "ENTRY_FILLED" in types, f"Expected ENTRY_FILLED, got: {types}" def test_exit_writes_trade_event_and_exit_leg(self): """Exit close must write both trade_events and trade_exit_legs.""" p, rows, acc = _sink_and_persist(capital=25_000.0, pnl=100.0) p._leg_state["XRP-USDT"] = {"prev_realized": 0.0, "prev_size": 5027.0, "prev_leg_id": ""} acc.snapshot.capital = 25_100.0 closed_slot = _slot(size=0.0, realized_pnl=100.0, closed=True, active_leg_index=1) fill = _fill_event(1.16, 5027.0) out = _outcome(state=KernelStage.CLOSED, events=(fill,)) p.persist_result( snapshot=_snap(), decision=_decision(DecisionAction.EXIT), intent=_intent(DecisionAction.EXIT, price=1.16), outcome=out, slot_dict=closed_slot, ) assert _rows_of(rows, "trade_events"), "trade_events must be written" assert _rows_of(rows, "trade_exit_legs"), "trade_exit_legs must be written" # =========================================================================== # 7. CHAOS / FUZZ TESTS # =========================================================================== class TestChaosPersistence: """Resilience: None fields, NaN, zero prices, missing keys — must not crash.""" def test_none_entry_price_in_slot(self): """slot_dict.entry_price=None must not crash or produce NaN.""" p, rows, acc = _sink_and_persist(capital=25_000.0, pnl=50.0) p._leg_state["T1"] = {"prev_realized": 0.0, "prev_size": 100.0, "prev_leg_id": ""} bad_slot = _slot("T1", entry_price=None, size=0.0, realized_pnl=50.0, closed=True) bad_slot["entry_price"] = None # override to None p.persist_result( snapshot=_snap(), decision=_decision(DecisionAction.EXIT, price=1.16), intent=_intent(DecisionAction.EXIT, price=1.16, trade_id="T1"), outcome=_outcome(state=KernelStage.CLOSED), slot_dict=bad_slot, ) te = _rows_of(rows, "trade_events") assert te, "Must write trade_events even with None entry_price" assert math.isfinite(te[0]["capital_after"]) def test_nan_realized_pnl_in_slot(self): """NaN realized_pnl must be sanitized to 0.0, not propagated.""" p, rows, acc = _sink_and_persist() p._leg_state["T1"] = {"prev_realized": 0.0, "prev_size": 100.0, "prev_leg_id": ""} bad_slot = _slot("T1", size=0.0, realized_pnl=float("nan"), closed=True) p.persist_result( snapshot=_snap(), decision=_decision(DecisionAction.EXIT, trade_id="T1"), intent=_intent(DecisionAction.EXIT, trade_id="T1"), outcome=_outcome(state=KernelStage.CLOSED), slot_dict=bad_slot, ) te = _rows_of(rows, "trade_events") if te: assert math.isfinite(te[0]["pnl"]), "NaN pnl must be sanitized" def test_zero_exit_price_does_not_crash(self): """zero reference_price for exit must not produce NaN/inf or crash.""" p, rows, acc = _sink_and_persist() p._leg_state["T1"] = {"prev_realized": 0.0, "prev_size": 100.0, "prev_leg_id": ""} closed_slot = _slot("T1", size=0.0, closed=True) p.persist_result( snapshot=_snap(), decision=_decision(DecisionAction.EXIT, price=0.0, trade_id="T1"), intent=_intent(DecisionAction.EXIT, price=0.0, trade_id="T1"), outcome=_outcome(state=KernelStage.CLOSED), slot_dict=closed_slot, ) # Must not raise; rows may be sparse but no crash def test_missing_slot_keys_gracefully_handled(self): """Minimally-populated slot_dict must not crash persistence.""" p, rows, acc = _sink_and_persist() minimal_slot = {"closed": True} # only closed key p.persist_result( snapshot=_snap(), decision=_decision(DecisionAction.EXIT), intent=_intent(DecisionAction.EXIT), outcome=_outcome(state=KernelStage.CLOSED), slot_dict=minimal_slot, ) # Must not raise def test_hold_decision_writes_status_snapshot(self): """HOLD must still emit status_snapshots, position_state, account_events.""" p, rows, acc = _sink_and_persist() p.persist_result( snapshot=_snap(), decision=_decision(action=DecisionAction.HOLD), intent=_intent(action=DecisionAction.HOLD), outcome=None, slot_dict=_slot(), ) assert _rows_of(rows, "status_snapshots"), "HOLD must write status_snapshots" assert _rows_of(rows, "account_events"), "HOLD must write account_events" assert _rows_of(rows, "position_state"), "HOLD must write position_state" def test_infinite_capital_does_not_propagate(self): """If account capital is somehow inf, output rows must still be finite.""" p, rows, acc = _sink_and_persist() acc.snapshot.capital = float("inf") # corrupt p.persist_result( snapshot=_snap(), decision=_decision(), intent=_intent(), outcome=_outcome(), slot_dict=_slot(), ) # Rows should still be written; capital fields may be 0 or clamped ss = _rows_of(rows, "status_snapshots") assert ss def test_empty_events_tuple_in_outcome(self): """Empty emitted_events must not cause ENTER to log ENTRY_FILLED.""" p, rows, acc = _sink_and_persist() open_slot = _slot(size=5027.0) out = _outcome(events=()) # no fill events p.persist_result( snapshot=_snap(), decision=_decision(DecisionAction.ENTER), intent=_intent(DecisionAction.ENTER), outcome=out, slot_dict=open_slot, ) recs = _rows_of(rows, "trade_reconstruction") types = [r["event_type"] for r in recs] # slot_open=True so ENTRY_FILLED IS expected (size>0) # This is the current design — verify no crash assert recs is not None def test_partial_exit_followed_by_final_exit(self): """Two-leg exit must produce two trade_exit_legs rows and one trade_events.""" p, rows, acc = _sink_and_persist(capital=25_000.0, pnl=0.0) p._leg_state["T2"] = {"prev_realized": 0.0, "prev_size": 1000.0, "prev_leg_id": ""} # Partial exit slot_p = _slot("T2", size=600.0, initial_size=1000.0, realized_pnl=50.0, active_leg_index=1, exit_leg_ratios=(0.4, 1.0)) acc.snapshot.capital = 25_050.0 acc.snapshot.realized_pnl = 50.0 partial_fill = _fill_event(trade_id="T2", price=1.16, size=400.0, kind=KernelEventKind.PARTIAL_FILL) p.persist_result( snapshot=_snap(), decision=_decision(DecisionAction.EXIT, trade_id="T2"), intent=_intent(DecisionAction.EXIT, trade_id="T2", size=400.0), outcome=_outcome(state=KernelStage.POSITION_OPEN, events=(partial_fill,)), slot_dict=slot_p, ) # Final exit slot_f = _slot("T2", size=0.0, initial_size=1000.0, realized_pnl=130.0, closed=True, active_leg_index=2, exit_leg_ratios=(0.4, 1.0)) acc.snapshot.capital = 25_130.0 acc.snapshot.realized_pnl = 130.0 final_fill = _fill_event(trade_id="T2", price=1.15, size=600.0) p.persist_result( snapshot=_snap(), decision=_decision(DecisionAction.EXIT, trade_id="T2"), intent=_intent(DecisionAction.EXIT, trade_id="T2", size=600.0), outcome=_outcome(state=KernelStage.CLOSED, events=(final_fill,)), slot_dict=slot_f, ) legs = _rows_of(rows, "trade_exit_legs") te = _rows_of(rows, "trade_events") assert len(legs) == 2, f"Expected 2 exit legs, got {len(legs)}" assert len(te) == 1, f"Expected 1 trade_event, got {len(te)}" assert all(math.isfinite(l["pnl_leg"]) for l in legs), "All pnl_leg must be finite" def test_restart_mid_trade_recovery_then_exit(self): """Simulates restart: kernel has open position, recovery runs, then exit.""" slot_view = MagicMock() slot_view.to_dict.return_value = _slot("ATOM-T1", "ATOM-USDT", "SHORT", entry_price=1.802, size=26152.81, initial_size=26152.81) kernel = MagicMock() kernel.slot.return_value = slot_view kernel.snapshot.return_value = {"account": {}} kernel.max_slots = 1 p, rows, acc = _sink_and_persist(capital=25_000.0, kernel=kernel) # Recovery after restart p.persist_recovery_state( snapshot=_snap(), acc_dict={"capital": 25_000.0}, phase="recovery", ) # Now exit (position detected via reconcile) acc.snapshot.capital = 25_100.0 acc.snapshot.realized_pnl = 100.0 closed_slot = _slot("ATOM-T1", "ATOM-USDT", "SHORT", entry_price=1.802, size=0.0, initial_size=26152.81, realized_pnl=100.0, closed=True) fill_ev = _fill_event(trade_id="ATOM-T1", asset="ATOM-USDT", price=1.798, size=26152.81) p.persist_result( snapshot=_snap(), decision=_decision(DecisionAction.EXIT, asset="ATOM-USDT", trade_id="ATOM-T1"), intent=_intent(DecisionAction.EXIT, asset="ATOM-USDT", trade_id="ATOM-T1", size=26152.81), outcome=_outcome(state=KernelStage.CLOSED, events=(fill_ev,)), slot_dict=closed_slot, ) te = _rows_of(rows, "trade_events") assert te, "trade_events must be written for exit after recovery" assert math.isfinite(te[0]["capital_after"]) def test_persist_fill_events_partial_fill_updates_leg_state(self): """PARTIAL_FILL via pump must update leg_state for next leg.""" p, rows, acc = _sink_and_persist(capital=25_000.0, pnl=0.0) p._leg_state["T3"] = {"prev_realized": 0.0, "prev_size": 1000.0, "prev_leg_id": ""} partial_slot = _slot("T3", size=600.0, initial_size=1000.0, realized_pnl=40.0) partial_fill = _fill_event(trade_id="T3", price=1.16, size=400.0, kind=KernelEventKind.PARTIAL_FILL) acc.snapshot.capital = 25_040.0 p.persist_fill_events(snapshot=_snap(), events=[partial_fill], slot_dict=partial_slot) # leg_state should now have prev_size=600 assert "T3" in p._leg_state assert abs(p._leg_state["T3"]["prev_size"] - 600.0) < 1e-9, ( f"leg_state.prev_size should be 600 after partial fill, got {p._leg_state['T3']['prev_size']}" )