Files
siloqy/prod/clean_arch/dita_v2/test_pink_persistence.py
Codex b30205ceb6 PINK: fix persistence layer — exit_price, entry_bar, recovery, external exits, NaN tracing
G21/E23/A13 — exit_price used entry_price (every trade had exit_price==entry_price):
  _write_trade_event: exit_price = fill_price_hint > intent.reference_price > decision.reference_price
  _write_trade_exit_leg: same priority chain via fill_price_hint parameter
  persist_result: extracts fill_price_hint from FULL_FILL/PARTIAL_FILL events in outcome
  persist_fill_events: intent.reference_price = actual fill price → propagates correctly

A14 — entry_bar was active_leg_index (exit leg counter, not bar count):
  _write_position_state: entry_bar = intent.bars_held (0 when intent is None)

A15 — persist_recovery_state used acc_dict as slot_dict (trade_id always ""):
  Now reads kernel.slot(0).to_dict() when kernel is wired; trade_id from real slot

External-position exit_qty=0 fix:
  _write_trade_exit_leg: when prev_size<=0 (no prior ENTER tracked), falls back to
  initial_size or intent.target_size so exit legs for reconcile-detected positions are meaningful

exit_qty field added to trade_exit_legs rows (was computed but not emitted)

NaN tracing (_checked_float):
  Introduces _checked_float() wrapper that logs WARNING + writes anomaly_events spool
  row on NaN/inf in financial fields; applied to realized_pnl in exit paths

29 new persistence unit tests (mocked) + chaos/fuzz suite:
  exit_price correctness, capital ordering, pnl_leg incremental, entry_bar,
  recovery trade_id, external position exits, multi-leg, restart-mid-trade, NaN/None fields
  164/164 total (97 flaws + 25 kernel reliability + 29 persistence + 13 phase4) green

FLAWS doc: pass 6 — G21/E23/A13/A14/A15 closed; 26 total fixed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 09:30:30 +02:00

837 lines
36 KiB
Python

"""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']}"
)