"""Comprehensive test battery for all 13 CRITICAL DITAv2 flaws. Each test verifies that the specific flaw exists (pre-fix) and would pass once the flaw is addressed. Tests use the MockVenueAdapter to avoid requiring live BingX connectivity. Run with: python -m pytest prod/clean_arch/dita_v2/test_flaws.py -v """ from __future__ import annotations import sys sys.path.insert(0, "/mnt/dolphinng5_predict") import asyncio from datetime import datetime, timezone from typing import Any, Dict, List import pytest from prod.clean_arch.dita_v2.contracts import ( KernelCommandType, KernelDiagnosticCode, KernelEventKind, KernelIntent, KernelOutcome, KernelSeverity, KernelTransition, TradeSide, TradeSlot, TradeStage, VenueEvent, VenueEventStatus, VenueOrder, VenueOrderStatus, ) from prod.clean_arch.dita_v2.mock_venue import MockVenueAdapter, MockVenueScenario from prod.clean_arch.dita_v2.rust_backend import ExecutionKernel from prod.clean_arch.dita_v2.account import AccountProjection E = KernelCommandType TS = TradeSide def _mk_intent( action: KernelCommandType = KernelCommandType.ENTER, trade_id: str = "t1", slot_id: int = 0, asset: str = "BTCUSDT", side: TradeSide = TradeSide.SHORT, price: float = 100.0, size: float = 1.0, leverage: float = 1.0, exit_leg_ratios: tuple = (1.0,), **kw, ) -> KernelIntent: return KernelIntent( timestamp=datetime.now(timezone.utc), intent_id=kw.pop("intent_id", trade_id), trade_id=trade_id, slot_id=slot_id, asset=asset, side=side, action=action, reference_price=price, target_size=size, leverage=leverage, exit_leg_ratios=exit_leg_ratios, reason=kw.pop("reason", f"auto_{action.value.lower()}"), metadata=kw, ) def _mk_venue_event( kind: KernelEventKind, trade_id: str = "t1", slot_id: int = 0, side: TradeSide = TradeSide.SHORT, asset: str = "BTCUSDT", price: float = 100.0, size: float = 1.0, filled_size: float = 1.0, remaining_size: float = 0.0, event_id: str = "", venue_order_id: str = "V-1", venue_client_id: str = "t1:t1", status: VenueEventStatus = VenueEventStatus.FILLED, reason: str = "", ) -> VenueEvent: return VenueEvent( timestamp=datetime.now(timezone.utc), event_id=event_id or f"ev-{kind.value}-{trade_id}", trade_id=trade_id, slot_id=slot_id, kind=kind, status=status, venue_order_id=venue_order_id, venue_client_id=venue_client_id, side=side, asset=asset, price=price, size=size, filled_size=filled_size, remaining_size=remaining_size, reason=reason, ) def _fresh_kernel( *, scenario: MockVenueScenario = None, max_slots: int = 2, capital: float = 25000.0, ) -> ExecutionKernel: venue = MockVenueAdapter(scenario=scenario or MockVenueScenario()) k = ExecutionKernel(max_slots=max_slots, venue=venue) k.account.snapshot.capital = capital k.account.snapshot.peak_capital = capital k.account.snapshot.equity = capital return k # ============================================================ # FLAW 1: Entry-order cancellation is structurally broken # ============================================================ class TestFlaw1EntryCancel: """CANCEL intent for entry orders must work, not just exit orders.""" def test_cancel_entry_order_accepted_by_rust(self): """Rust kernel must accept CANCEL for an entry order in ENTRY_WORKING.""" k = _fresh_kernel(scenario=MockVenueScenario(partial_fill_ratio=0.0, emit_fill_on_submit=False)) r = k.process_intent(_mk_intent(action=E.ENTER, trade_id="ce1")) assert r.accepted, f"ENTER rejected: {r.diagnostic_code}" slot = k._get_slot(0) assert slot.fsm_state in {TradeStage.ORDER_REQUESTED, TradeStage.ENTRY_WORKING} cancel_result = k.process_intent(_mk_intent(action=E.CANCEL, trade_id="ce1")) assert cancel_result.accepted, ( f"CANCEL for entry order should be accepted, got " f"accepted={cancel_result.accepted} " f"diag={cancel_result.diagnostic_code}" ) def test_cancel_entry_order_calls_venue_cancel(self): """Python bridge must call venue.cancel() on active_entry_order.""" scenario = MockVenueScenario(partial_fill_ratio=0.0, emit_fill_on_submit=False) k = _fresh_kernel(scenario=scenario) k.process_intent(_mk_intent(action=E.ENTER, trade_id="ce2")) entry_order = k.slot(0).active_entry_order assert entry_order is not None, "Entry order should be attached" cancel_result = k.process_intent(_mk_intent(action=E.CANCEL, trade_id="ce2")) assert cancel_result.accepted, f"CANCEL not accepted: {cancel_result.diagnostic_code}" def test_cancel_entry_no_fill_returns_to_idle(self): """After cancelling an entry order that hasn't filled, slot must be IDLE.""" k = _fresh_kernel(scenario=MockVenueScenario(partial_fill_ratio=0.0, emit_fill_on_submit=False)) k.process_intent(_mk_intent(action=E.ENTER, trade_id="ce3")) k.process_intent(_mk_intent(action=E.CANCEL, trade_id="ce3")) slot = k._get_slot(0) assert slot.is_free(), ( f"Slot should be free/IDLE after entry cancel, " f"got state={slot.fsm_state} closed={slot.closed} " f"entry_order={slot.active_entry_order} exit_order={slot.active_exit_order} " f"size={slot.size}" ) def test_cancel_entry_with_partial_fill(self): """Cancel entry with partial fill should leave slot in correct state.""" k = _fresh_kernel(scenario=MockVenueScenario(partial_fill_ratio=0.5)) k.process_intent(_mk_intent(action=E.ENTER, trade_id="ce4", size=0.002)) slot_after = k._get_slot(0) assert slot_after.size > 0, "Should have partial fill" def test_cancel_entry_then_reenter(self): """After entry cancel, a new ENTER should succeed.""" k = _fresh_kernel(scenario=MockVenueScenario(partial_fill_ratio=0.0, emit_fill_on_submit=False)) k.process_intent(_mk_intent(action=E.ENTER, trade_id="ce5a")) k.process_intent(_mk_intent(action=E.CANCEL, trade_id="ce5a")) r = k.process_intent(_mk_intent(action=E.ENTER, trade_id="ce5b")) assert r.accepted, f"Re-entry after cancel should succeed: {r.diagnostic_code}" # ============================================================ # FLAW 2: Rust CANCEL_ACK has no entry-order reset path # ============================================================ class TestFlaw2CancelAckEntry: """CANCEL_ACK for entry orders must reset slot to IDLE.""" def test_cancel_ack_resets_entry_working_to_idle(self): """When CANCEL_ACK arrives for an entry order, slot goes IDLE.""" k = _fresh_kernel(scenario=MockVenueScenario(partial_fill_ratio=0.0, emit_fill_on_submit=False)) k.process_intent(_mk_intent(action=E.ENTER, trade_id="ca1")) slot = k._get_slot(0) assert slot.active_entry_order is not None venue_order = slot.active_entry_order ack = _mk_venue_event( kind=KernelEventKind.CANCEL_ACK, trade_id="ca1", venue_order_id=venue_order.venue_order_id, venue_client_id=venue_order.venue_client_id, status=VenueEventStatus.CANCELED, ) k.on_venue_event(ack) slot = k._get_slot(0) assert slot.fsm_state == TradeStage.IDLE, ( f"Slot should be IDLE after CANCEL_ACK on entry, got {slot.fsm_state}" ) assert slot.active_entry_order is None, "Entry order should be cleared" assert slot.trade_id == "", "Trade ID should be cleared" assert slot.size == 0.0, "Size should be zero" def test_cancel_ack_exit_still_works(self): """Existing exit-order CANCEL_ACK path must still work. Deterministic setup: entry fills fully (POSITION_OPEN) but the exit only partially fills, so the exit order stays live and the CANCEL_ACK exit branch is genuinely exercised (no vacuous guard). """ k = _fresh_kernel(scenario=MockVenueScenario(exit_partial_fill_ratio=0.5)) k.process_intent(_mk_intent(action=E.ENTER, trade_id="ca2", size=0.002)) slot = k._get_slot(0) assert slot.fsm_state == TradeStage.POSITION_OPEN, ( f"Entry should fill fully, got {slot.fsm_state}" ) k.process_intent(_mk_intent(action=E.EXIT, trade_id="ca2", size=0.002)) slot = k._get_slot(0) assert slot.active_exit_order is not None, ( "Exit order must remain live after a partial exit fill" ) ack = _mk_venue_event( kind=KernelEventKind.CANCEL_ACK, trade_id="ca2", venue_order_id=slot.active_exit_order.venue_order_id, venue_client_id=slot.active_exit_order.venue_client_id, status=VenueEventStatus.CANCELED, ) k.on_venue_event(ack) slot = k._get_slot(0) assert slot.active_exit_order is None, "Exit order should be cleared by CANCEL_ACK" assert slot.fsm_state == TradeStage.POSITION_OPEN, ( f"Exit cancel must return slot to POSITION_OPEN, got {slot.fsm_state}" ) # ============================================================ # FLAW 3: Outcome mixes pre/post-venue state # ============================================================ class TestFlaw3OutcomeConsistency: """process_intent outcome should have consistent state and transitions.""" def test_outcome_state_matches_actual_slot(self): """The outcome.state should reflect the final state after venue events.""" k = _fresh_kernel() result = k.process_intent(_mk_intent(action=E.ENTER, trade_id="oc1")) slot = k._get_slot(0) assert result.state == slot.fsm_state, ( f"Outcome state {result.state} != actual slot state {slot.fsm_state}" ) def test_outcome_transitions_includes_venue_events(self): """Transitions should include venue-event-triggered transitions.""" k = _fresh_kernel() result = k.process_intent(_mk_intent(action=E.ENTER, trade_id="oc2")) transition_triggers = [t.trigger for t in result.transitions] assert len(result.transitions) >= 1, ( f"Should have at least 1 transition, got triggers: {transition_triggers}" ) # ============================================================ # FLAW 4: Multi-leg exit final leg can double-close # ============================================================ class TestFlaw4DoubleClose: """Multi-leg exit final leg should only close once.""" def test_single_close_after_final_leg(self): """After the last leg fills, slot.closed should be set exactly once.""" k = _fresh_kernel(scenario=MockVenueScenario()) k.process_intent( _mk_intent( action=E.ENTER, trade_id="dc1", size=0.002, exit_leg_ratios=(0.5, 1.0), ) ) k.process_intent( _mk_intent( action=E.EXIT, trade_id="dc1", size=0.001, exit_leg_ratios=(0.5, 1.0), ) ) k.process_intent( _mk_intent( action=E.EXIT, trade_id="dc1", size=0.001, exit_leg_ratios=(1.0,), ) ) slot = k._get_slot(0) assert slot.closed, "Slot should be closed after final leg" assert slot.fsm_state == TradeStage.CLOSED def test_no_extra_entry_order_clear_on_close(self): """After close via multi-leg, active_entry_order should be consistent.""" k = _fresh_kernel(scenario=MockVenueScenario()) k.process_intent( _mk_intent( action=E.ENTER, trade_id="dc2", size=0.002, exit_leg_ratios=(0.5, 1.0), ) ) k.process_intent( _mk_intent( action=E.EXIT, trade_id="dc2", size=0.001, exit_leg_ratios=(0.5, 1.0), ) ) k.process_intent( _mk_intent( action=E.EXIT, trade_id="dc2", size=0.001, exit_leg_ratios=(1.0,), ) ) slot = k._get_slot(0) assert slot.active_exit_order is None, "Exit order should be cleared" assert slot.active_entry_order is None or slot.active_entry_order.status == VenueOrderStatus.FILLED # ============================================================ # FLAW 5: Capital settlement only triggers on terminal states # ============================================================ class TestFlaw5CapitalSettleOnPartialFill: """Realized PnL should settle incrementally on partial fills.""" def test_partial_exit_settles_pnl_incrementally(self): """Exit fill must settle realized PnL into capital — EXACTLY. This is the single most important invariant in DITAv2: capital is the kernel account's authority and must move by precisely the realized PnL of the fill (no balance-poll overwrite). The entry and exit prices differ so realized PnL is strictly nonzero and the capital-change assertion fires unconditionally (no vacuous guard). """ k = _fresh_kernel() cap_before = k.account.snapshot.capital # SHORT entry at 100. k.process_intent( _mk_intent(action=E.ENTER, trade_id="ps1", side=TradeSide.SHORT, price=100.0, size=0.002) ) slot = k._get_slot(0) assert slot.fsm_state == TradeStage.POSITION_OPEN # Exit at 90 -> SHORT closes in profit, realized PnL strictly positive. k.process_intent( _mk_intent(action=E.EXIT, trade_id="ps1", side=TradeSide.SHORT, price=90.0, size=0.002) ) slot = k._get_slot(0) assert slot.realized_pnl > 0.0, ( f"SHORT exit below entry must realize positive PnL, got {slot.realized_pnl}" ) cap_after = k.account.snapshot.capital # Single-authority invariant: capital moved by EXACTLY realized PnL. assert abs((cap_after - cap_before) - slot.realized_pnl) < 1e-9, ( f"Capital delta {cap_after - cap_before} != realized_pnl {slot.realized_pnl} " f"(before={cap_before} after={cap_after})" ) # ============================================================ # FLAW 6: _legacy_intent silently drops order_type and limit_price # ============================================================ class TestFlaw6LegacyIntentDrop: """_legacy_intent must preserve order_type and limit_price.""" def test_legacy_intent_preserves_order_type(self): """LegacyIntent conversion must include order_type.""" from prod.clean_arch.dita_v2.bingx_venue import BingxVenueAdapter intent = _mk_intent( action=E.ENTER, trade_id="li1", order_type="LIMIT", limit_price=50000.0, ) legacy = BingxVenueAdapter._legacy_intent(intent) assert getattr(legacy, "order_type", None) == "LIMIT" or \ legacy.metadata.get("_order_type") == "LIMIT" or \ legacy.metadata.get("order_type") == "LIMIT", ( f"order_type not preserved in legacy intent. " f"Legacy fields: {dir(legacy)}, metadata: {legacy.metadata}" ) def test_legacy_intent_preserves_limit_price(self): """LegacyIntent conversion must include limit_price.""" from prod.clean_arch.dita_v2.bingx_venue import BingxVenueAdapter intent = _mk_intent( action=E.ENTER, trade_id="li2", order_type="LIMIT", limit_price=50000.0, ) legacy = BingxVenueAdapter._legacy_intent(intent) assert getattr(legacy, "limit_price", 0) == 50000.0 or \ legacy.metadata.get("_limit_price") == 50000.0 or \ legacy.metadata.get("limit_price") == 50000.0, ( f"limit_price not preserved in legacy intent. " f"Legacy metadata: {legacy.metadata}" ) # ============================================================ # FLAW 7: Mock venue partial_fill_ratio applies to both entry and exit # ============================================================ class TestFlaw7MockVenueRatios: """Mock venue should support different ratios for entry vs exit.""" def test_entry_exit_different_ratios(self): """Entry can fill fully while exit fills partially.""" k = _fresh_kernel(scenario=MockVenueScenario( entry_partial_fill_ratio=1.0, exit_partial_fill_ratio=0.5, )) r = k.process_intent(_mk_intent(action=E.ENTER, trade_id="mv1", size=0.002)) assert r.accepted slot = k._get_slot(0) assert slot.fsm_state == TradeStage.POSITION_OPEN, f"Entry should fill fully: {slot.fsm_state}" def test_per_action_type_ratios(self): """entry_partial_fill_ratio and exit_partial_fill_ratio should work independently.""" scenario = MockVenueScenario( entry_partial_fill_ratio=1.0, exit_partial_fill_ratio=0.3, ) k = _fresh_kernel(scenario=scenario) k.process_intent(_mk_intent(action=E.ENTER, trade_id="mv2", size=0.001)) slot = k._get_slot(0) assert slot.fsm_state == TradeStage.POSITION_OPEN assert slot.size == 0.001 # ============================================================ # FLAW 8: Per-asset price precision helper does not exist # ============================================================ class TestFlaw8PricePrecision: """_format_price must exist for LIMIT order support.""" def test_format_price_exists_in_bingx_direct(self): """BingxDirectExecutionAdapter should have _format_price method.""" try: from prod.clean_arch.adapters.bingx_direct import BingxDirectExecutionAdapter assert hasattr(BingxDirectExecutionAdapter, "_format_price"), ( "_format_price method missing from BingxDirectExecutionAdapter" ) except ImportError: pytest.skip("bingx_direct not importable in this environment") # ============================================================ # FLAW 9: Cancel path falls back to trade_id as symbol # ============================================================ class TestFlaw9CancelSymbolFallback: """Cancel should use correct asset, not trade_id as fallback symbol.""" def test_cancel_uses_slot_asset_not_trade_id(self): """When cancel is called, the asset should come from the slot, not trade_id.""" k = _fresh_kernel(scenario=MockVenueScenario(partial_fill_ratio=0.0, emit_fill_on_submit=False)) k.process_intent(_mk_intent(action=E.ENTER, trade_id="cs1", asset="TRXUSDT")) slot = k._get_slot(0) # ACK-only (no fill) deterministically leaves the entry order live. assert slot.active_entry_order is not None, ( "ACK-only entry must leave the entry order live for cancel-symbol fallback" ) metadata = slot.active_entry_order.metadata assert metadata.get("asset") == "TRXUSDT", ( f"Entry order metadata should contain asset. Got: {metadata}" ) def test_mock_venue_cancel_event_has_asset(self): """Mock venue cancel events should carry the correct asset.""" k = _fresh_kernel(scenario=MockVenueScenario(partial_fill_ratio=0.0, emit_fill_on_submit=False)) k.process_intent(_mk_intent(action=E.ENTER, trade_id="cs2", asset="XRPUSDT")) slot = k._get_slot(0) order = slot.active_entry_order assert order is not None assert order.metadata.get("asset") is not None or order.metadata.get("slot_id") is not None # ============================================================ # FLAW 10: Event dedup window is bounded at 64 # ============================================================ class TestFlaw10EventDedup: """Event dedup window should be large enough for realistic workloads.""" def test_dedup_window_accepts_many_events(self): """A slot should handle > 64 events without dedup eviction.""" k = _fresh_kernel() k.process_intent(_mk_intent(action=E.ENTER, trade_id="ed1")) for i in range(70): ev = _mk_venue_event( kind=KernelEventKind.MARK_PRICE, trade_id="ed1", event_id=f"mp-{i:04d}", price=100.0 + i * 0.01, size=0.0, filled_size=0.0, ) k.on_venue_event(ev) slot = k._get_slot(0) assert len(slot.seen_event_ids) >= 70, ( f"Expected >= 70 seen_event_ids, got {len(slot.seen_event_ids)}" ) def test_dedup_eviction_does_not_accept_old_event(self): """Evicted event IDs should still be rejected (with larger window).""" k = _fresh_kernel() k.process_intent(_mk_intent(action=E.ENTER, trade_id="ed2")) for i in range(70): ev = _mk_venue_event( kind=KernelEventKind.MARK_PRICE, trade_id="ed2", event_id=f"mp2-{i:04d}", price=100.0 + i * 0.01, size=0.0, filled_size=0.0, ) k.on_venue_event(ev) old_ev = _mk_venue_event( kind=KernelEventKind.MARK_PRICE, trade_id="ed2", event_id="mp2-0000", price=99.0, size=0.0, filled_size=0.0, ) result = k.on_venue_event(old_ev) assert result.diagnostic_code == KernelDiagnosticCode.DUPLICATE_EVENT, ( f"Old evicted event should still be deduplicated, " f"got {result.diagnostic_code}" ) # ============================================================ # FLAW 11: Reconcile is a raw state override with no FSM validation # ============================================================ class TestFlaw11ReconcileValidation: """Reconcile should validate slot state consistency.""" def test_reconcile_rejects_position_open_with_zero_size(self): """Reconciling with POSITION_OPEN but zero size should be rejected.""" k = _fresh_kernel() bad_slot = TradeSlot( slot_id=0, fsm_state=TradeStage.POSITION_OPEN, size=0.0, asset="BTCUSDT", trade_id="bad1", ) result = k.reconcile_from_slots([bad_slot]) slot = k._get_slot(0) assert slot.fsm_state != TradeStage.POSITION_OPEN or slot.size > 0, ( f"Reconcile should reject POSITION_OPEN with size=0, " f"got state={slot.fsm_state} size={slot.size}" ) def test_reconcile_rejects_idle_with_nonzero_size(self): """Reconciling with IDLE but nonzero size should be rejected.""" k = _fresh_kernel() bad_slot = TradeSlot( slot_id=0, fsm_state=TradeStage.IDLE, size=5.0, asset="BTCUSDT", trade_id="bad2", ) result = k.reconcile_from_slots([bad_slot]) slot = k._get_slot(0) assert slot.size == 0.0 or slot.fsm_state != TradeStage.IDLE, ( f"Reconcile should reject IDLE with size > 0, " f"got state={slot.fsm_state} size={slot.size}" ) def test_reconcile_accepts_valid_slot(self): """Valid slot data should still reconcile correctly.""" k = _fresh_kernel() k.process_intent(_mk_intent(action=E.ENTER, trade_id="rv1")) slot_data = k._get_slot(0) result = k.reconcile_from_slots([slot_data]) assert result.accepted # ============================================================ # FLAW 12: Outcome transitions are incomplete — pre-venue only # ============================================================ class TestFlaw12OutcomeTransitions: """process_intent outcome transitions should include venue event transitions.""" def test_transitions_include_post_venue(self): """After a full entry cycle, transitions should include ORDER_ACK and FULL_FILL.""" k = _fresh_kernel() result = k.process_intent(_mk_intent(action=E.ENTER, trade_id="ot1")) triggers = [t.trigger for t in result.transitions] assert any(t in triggers for t in ["ENTER_INTENT", "ORDER_ACK", "FULL_FILL"]), ( f"Transitions should include venue event triggers. Got: {triggers}" ) def test_transitions_count_matches_lifecycle(self): """Full entry lifecycle should produce multiple transitions.""" k = _fresh_kernel() result = k.process_intent(_mk_intent(action=E.ENTER, trade_id="ot2")) slot = k._get_slot(0) assert slot.fsm_state in {TradeStage.POSITION_OPEN, TradeStage.ENTRY_WORKING}, ( f"Default full-fill entry must open the position, got {slot.fsm_state}" ) assert len(result.transitions) >= 2, ( f"Full entry should produce >= 2 transitions " f"(intent + venue ack/fill), got {len(result.transitions)}: " f"{[t.trigger for t in result.transitions]}" ) # ============================================================ # FLAW 13: Unsettled realized PnL on re-entry # ============================================================ class TestFlaw13UnsettledPnlOnReentry: """Re-entry should not silently discard unrealized settled PnL.""" def test_reentry_after_full_close_no_pnl_loss(self): """After full close and settle, re-entry should not lose PnL.""" k = _fresh_kernel() cap_before = k.account.snapshot.capital k.process_intent(_mk_intent(action=E.ENTER, trade_id="rp1")) slot = k._get_slot(0) assert slot.fsm_state == TradeStage.POSITION_OPEN k.process_intent( _mk_intent(action=E.EXIT, trade_id="rp1", price=100.5) ) slot = k._get_slot(0) assert slot.is_free() cap_after_first = k.account.snapshot.capital k.process_intent(_mk_intent(action=E.ENTER, trade_id="rp2")) k.process_intent( _mk_intent(action=E.EXIT, trade_id="rp2", price=101.0) ) cap_after_second = k.account.snapshot.capital assert cap_after_second > 0, "Capital should remain positive" assert abs(cap_after_second - cap_before) < cap_before * 0.5 def test_pnl_warning_on_unsettled_reentry(self): """Re-entry on a slot with unsettled PnL should at least warn.""" k = _fresh_kernel(scenario=MockVenueScenario()) k.process_intent(_mk_intent(action=E.ENTER, trade_id="rw1")) k.process_intent(_mk_intent(action=E.EXIT, trade_id="rw1")) slot = k._get_slot(0) assert slot.is_free(), "Full close must free the slot for re-entry" r = k.process_intent(_mk_intent(action=E.ENTER, trade_id="rw2")) assert r.accepted, "Re-entry on a freed slot must be accepted" # ============================================================ # REGRESSION: Existing behaviour must not break # ============================================================ class TestRegression: """Ensure existing happy-path scenarios still work.""" def test_basic_entry_exit(self): k = _fresh_kernel() cap_before = k.account.snapshot.capital r1 = k.process_intent(_mk_intent(action=E.ENTER, trade_id="re1")) assert r1.accepted r2 = k.process_intent(_mk_intent(action=E.EXIT, trade_id="re1")) assert r2.accepted slot = k._get_slot(0) assert slot.is_free() def test_multi_leg_exit(self): k = _fresh_kernel() k.process_intent( _mk_intent(action=E.ENTER, trade_id="re2", size=0.002, exit_leg_ratios=(0.5, 1.0)) ) k.process_intent( _mk_intent(action=E.EXIT, trade_id="re2", size=0.001, exit_leg_ratios=(0.5, 1.0)) ) k.process_intent( _mk_intent(action=E.EXIT, trade_id="re2", size=0.001, exit_leg_ratios=(1.0,)) ) slot = k._get_slot(0) assert slot.is_free() def test_slot_busy_rejection(self): k = _fresh_kernel() r1 = k.process_intent(_mk_intent(action=E.ENTER, trade_id="re3a")) assert r1.accepted r2 = k.process_intent(_mk_intent(action=E.ENTER, trade_id="re3b")) assert not r2.accepted assert r2.diagnostic_code == KernelDiagnosticCode.SLOT_BUSY def test_exit_on_idle_rejected(self): k = _fresh_kernel() r = k.process_intent(_mk_intent(action=E.EXIT, trade_id="re4")) assert not r.accepted def test_reconcile_preserves_state(self): k = _fresh_kernel() k.process_intent(_mk_intent(action=E.ENTER, trade_id="re5")) slot_data = k._get_slot(0) k.reconcile_from_slots([slot_data]) slot_after = k._get_slot(0) assert slot_after.trade_id == "re5" def test_dedup_duplicate_event(self): k = _fresh_kernel() k.process_intent(_mk_intent(action=E.ENTER, trade_id="re6")) slot = k._get_slot(0) dup = _mk_venue_event( kind=KernelEventKind.FULL_FILL, trade_id="re6", event_id="dedup-regression", price=100.0, size=1.0, filled_size=1.0, ) k.on_venue_event(dup) result = k.on_venue_event(dup) assert result.diagnostic_code == KernelDiagnosticCode.DUPLICATE_EVENT def test_ten_cycles_no_leak(self): k = _fresh_kernel() for i in range(10): k.process_intent(_mk_intent(action=E.ENTER, trade_id=f"tc{i}")) k.process_intent(_mk_intent(action=E.EXIT, trade_id=f"tc{i}")) slot = k._get_slot(0) assert slot.is_free() assert k.account.snapshot.capital > 0 # ============================================================ # I15: CANCEL_REJECT must un-stick EXIT_WORKING slot # ============================================================ class TestI15CancelRejectUnstick: """CANCEL_REJECT on an exit order must clear active_exit_order and return the slot to POSITION_OPEN so the algo can retry the exit.""" def _enter_to_position_open(self, k: ExecutionKernel, trade_id: str) -> None: r = k.process_intent(_mk_intent(action=E.ENTER, trade_id=trade_id)) assert r.accepted, f"ENTER rejected: {r.diagnostic_code}" slot = k._get_slot(0) assert slot.fsm_state == TradeStage.POSITION_OPEN, ( f"Expected POSITION_OPEN after ENTER, got {slot.fsm_state}" ) def test_cancel_reject_exits_working_returns_to_position_open(self): """Core I15 regression: CANCEL_REJECT on EXIT_WORKING must unstick slot.""" # partial_fill_ratio=0 prevents fills on submit; fills are injected manually. k_no_fill = _fresh_kernel(scenario=MockVenueScenario(partial_fill_ratio=0.0, emit_fill_on_submit=False)) k_no_fill.process_intent(_mk_intent(action=E.ENTER, trade_id="i15b")) # Manually force POSITION_OPEN by injecting FULL_FILL fill = _mk_venue_event( kind=KernelEventKind.FULL_FILL, trade_id="i15b", event_id="fill-i15b", price=100.0, size=1.0, filled_size=1.0, ) k_no_fill.on_venue_event(fill) slot = k_no_fill._get_slot(0) assert slot.fsm_state == TradeStage.POSITION_OPEN, ( f"Setup failed: expected POSITION_OPEN, got {slot.fsm_state}" ) # Submit exit (no fill emitted) — slot enters EXIT_WORKING k_no_fill.process_intent(_mk_intent(action=E.EXIT, trade_id="i15b")) slot = k_no_fill._get_slot(0) assert slot.fsm_state in (TradeStage.EXIT_WORKING, TradeStage.EXIT_REQUESTED, TradeStage.EXIT_SENT), ( f"Setup failed: expected an exit state, got {slot.fsm_state}" ) assert slot.active_exit_order is not None, "Setup: active_exit_order should be set" # Now deliver CANCEL_REJECT cancel_rej = _mk_venue_event( kind=KernelEventKind.CANCEL_REJECT, trade_id="i15b", event_id="cr-i15b", status=VenueEventStatus.CANCELED, ) result = k_no_fill.on_venue_event(cancel_rej) slot = k_no_fill._get_slot(0) assert slot.fsm_state == TradeStage.POSITION_OPEN, ( f"I15: slot must return to POSITION_OPEN after CANCEL_REJECT, got {slot.fsm_state}" ) assert slot.active_exit_order is None, ( "I15: active_exit_order must be cleared by CANCEL_REJECT" ) assert result.diagnostic_code == KernelDiagnosticCode.CANCEL_REJECTED def test_after_cancel_reject_exit_can_be_resubmitted(self): """After CANCEL_REJECT un-sticks the slot, a new EXIT must be accepted.""" k = _fresh_kernel(scenario=MockVenueScenario(partial_fill_ratio=0.0, emit_fill_on_submit=False)) k.process_intent(_mk_intent(action=E.ENTER, trade_id="i15c")) fill = _mk_venue_event( kind=KernelEventKind.FULL_FILL, trade_id="i15c", event_id="fill-i15c", price=100.0, size=1.0, filled_size=1.0, ) k.on_venue_event(fill) k.process_intent(_mk_intent(action=E.EXIT, trade_id="i15c")) cancel_rej = _mk_venue_event( kind=KernelEventKind.CANCEL_REJECT, trade_id="i15c", event_id="cr-i15c", status=VenueEventStatus.CANCELED, ) k.on_venue_event(cancel_rej) # Slot is back to POSITION_OPEN — a new EXIT intent must be accepted r = k.process_intent(_mk_intent(action=E.EXIT, trade_id="i15c")) assert r.accepted, ( f"I15: retry EXIT after CANCEL_REJECT must be accepted, got {r.diagnostic_code}" ) # ============================================================ # O5: _run() thread-pool path must time out, not hang forever # ============================================================ class TestO5RunTimeout: """O5: BingxVenueAdapter._run() must raise TimeoutError instead of freezing when the backend call exceeds the configured deadline.""" def test_run_raises_timeout_from_async_context(self, monkeypatch): """When called from inside an event loop and the backend is slow, _run() must raise TimeoutError within the configured deadline.""" from prod.clean_arch.dita_v2.bingx_venue import BingxVenueAdapter import asyncio adapter = object.__new__(BingxVenueAdapter) # Patch to a very short deadline so the test completes fast. monkeypatch.setattr(BingxVenueAdapter, "_BACKEND_TIMEOUT_S", 0.15) async def _slow_coroutine(): await asyncio.sleep(5.0) return "never" async def _run_from_async(): with pytest.raises(TimeoutError): adapter._run(_slow_coroutine()) asyncio.run(_run_from_async()) def test_run_returns_normally_within_deadline(self, monkeypatch): """Fast backend calls must succeed and return their value.""" from prod.clean_arch.dita_v2.bingx_venue import BingxVenueAdapter import asyncio adapter = object.__new__(BingxVenueAdapter) monkeypatch.setattr(BingxVenueAdapter, "_BACKEND_TIMEOUT_S", 2.0) async def _fast_coroutine(): return 42 async def _run_from_async(): result = adapter._run(_fast_coroutine()) assert result == 42 asyncio.run(_run_from_async()) # ============================================================ # O1: _maybe_close() must not silently skip close from async context # ============================================================ class TestO1MaybeCloseAsyncSafe: """O1: _maybe_close() must run the coroutine even when called from an async context (previously it swallowed RuntimeError and skipped close).""" def test_maybe_close_from_sync_context(self): """Sync caller: asyncio.run() path must run the close coroutine.""" from prod.clean_arch.dita_v2.launcher import _maybe_close closed = [] class _FakeAsync: async def close(self) -> None: closed.append(True) _maybe_close(_FakeAsync()) assert closed == [True], "close() coroutine must run from sync context" def test_maybe_close_from_async_context(self): """Async caller: thread-pool path must run the close coroutine without raising RuntimeError (the old silent-skip bug).""" from prod.clean_arch.dita_v2.launcher import _maybe_close closed = [] class _FakeAsync: async def close(self) -> None: closed.append(True) async def _caller(): _maybe_close(_FakeAsync()) asyncio.run(_caller()) assert closed == [True], "close() coroutine must run from async context" def test_maybe_close_sync_method_still_works(self): """Non-coroutine close() must still be called (no regression).""" from prod.clean_arch.dita_v2.launcher import _maybe_close closed = [] class _FakeSync: def close(self) -> None: closed.append(True) _maybe_close(_FakeSync()) assert closed == [True], "sync close() must still be called" # ============================================================ # V3: seen_event_ids must be cleared on slot reuse (ENTER after CLOSE) # ============================================================ class TestV3SeenEventIdsClearedOnReuse: """V3: If seen_event_ids from a previous trade survive into the next trade on the same slot, events whose IDs happen to match will be silently dropped. This is guaranteed after a restart because the venue adapter's _event_seq resets to 1, so EV-00000001 collides with the old trade's first event.""" def test_second_trade_fill_not_deduped(self): """Fill on a reused slot must not be swallowed by stale dedup set.""" k = _fresh_kernel() # Trade 1: enter and exit k.process_intent(_mk_intent(action=E.ENTER, trade_id="v3-t1")) k.process_intent(_mk_intent(action=E.EXIT, trade_id="v3-t1")) assert k._get_slot(0).is_free(), "Trade 1 must close cleanly" # Trade 2 on the same slot — inject a fill with an event_id that # matches what the venue adapter would assign after a restart (EV-00000001). k.process_intent(_mk_intent(action=E.ENTER, trade_id="v3-t2")) fill = _mk_venue_event( kind=KernelEventKind.FULL_FILL, trade_id="v3-t2", event_id="EV-00000001", # same ID the adapter emits on restart price=100.0, size=1.0, filled_size=1.0, ) result = k.on_venue_event(fill) slot = k._get_slot(0) assert slot.fsm_state == TradeStage.POSITION_OPEN, ( f"V3: fill for trade 2 must not be deduped — got {slot.fsm_state}, " f"diagnostic={result.diagnostic_code}" ) def test_seen_event_ids_smaller_after_slot_reuse(self): """After a trade closes and a new ENTER starts, seen_event_ids must contain only IDs from the new trade — not the accumulated IDs from the prior trade on the same slot.""" # Auto-fill kernel: each trade generates ~2 events (ORDER_ACK + FILL). k = _fresh_kernel() k.process_intent(_mk_intent(action=E.ENTER, trade_id="v3-s1")) k.process_intent(_mk_intent(action=E.EXIT, trade_id="v3-s1")) assert k._get_slot(0).is_free(), "Seed trade must close cleanly" ids_after_seed = list(k._get_slot(0).seen_event_ids) # Trade 1 generated at least 2 events (ORDER_ACK + FULL_FILL × 2) assert len(ids_after_seed) >= 2, "Seed trade must have populated seen_event_ids" # Fresh ENTER on same slot (auto-fills → adds ~2 more events). k.process_intent(_mk_intent(action=E.ENTER, trade_id="v3-s2")) ids_after_fresh = list(k._get_slot(0).seen_event_ids) # With V3 fix: fresh trade starts from 0 then adds its own events → small count. # Without fix: old IDs remain → count = len(ids_after_seed) + new_events. assert len(ids_after_fresh) < len(ids_after_seed), ( f"V3: seen_event_ids must be cleared on ENTER. " f"After seed: {len(ids_after_seed)} IDs, after fresh ENTER: {len(ids_after_fresh)} IDs. " f"Expected fewer, not more." ) # ============================================================ # V1+V2: LauncherBundle.close() wires kernel; BingxVenueAdapter.close() # ============================================================ class TestV1V2LauncherAndVenueClose: """V1: LauncherBundle.close() must call kernel.close(). V2: BingxVenueAdapter.close() must exist and release the thread pool.""" def test_launcher_bundle_close_calls_kernel_close(self): """V1: kernel._backend must be None after bundle.close().""" from prod.clean_arch.dita_v2.launcher import build_launcher_bundle bundle = build_launcher_bundle(max_slots=2) assert bundle.kernel._backend is not None bundle.close() assert bundle.kernel._backend is None, ( "V1: LauncherBundle.close() must call kernel.close()" ) def test_bingx_venue_adapter_has_close(self): """V2: BingxVenueAdapter must have a close() method.""" from prod.clean_arch.dita_v2.bingx_venue import BingxVenueAdapter adapter = object.__new__(BingxVenueAdapter) assert callable(getattr(adapter, "close", None)), ( "V2: BingxVenueAdapter must have a close() method" ) # ============================================================ # M9: ORDER_REJECT must NOT nuke a live POSITION_OPEN slot # ============================================================ class TestM9OrderRejectPositionOpen: """M9: A spurious ORDER_REJECT arriving while the slot is POSITION_OPEN must not reset it to IDLE. Only entry-phase rejects should reset.""" def _open_position(self, k: ExecutionKernel, trade_id: str) -> None: r = k.process_intent(_mk_intent(action=E.ENTER, trade_id=trade_id)) assert r.accepted, f"ENTER failed: {r.diagnostic_code}" assert k._get_slot(0).fsm_state == TradeStage.POSITION_OPEN def test_spurious_reject_does_not_reset_position_open(self): """A stale ORDER_REJECT with no matching active order must not nuke slot.""" k = _fresh_kernel() self._open_position(k, "m9a") reject = _mk_venue_event( kind=KernelEventKind.ORDER_REJECT, trade_id="m9a", event_id="stale-reject-m9a", status=VenueEventStatus.REJECTED, ) result = k.on_venue_event(reject) slot = k._get_slot(0) assert slot.fsm_state == TradeStage.POSITION_OPEN, ( f"M9: spurious ORDER_REJECT must not reset POSITION_OPEN → got {slot.fsm_state}" ) assert not result.accepted, "Spurious reject must be reported as not accepted" def test_entry_reject_still_resets_to_idle(self): """Entry-phase ORDER_REJECT must still reset to IDLE (regression).""" k = _fresh_kernel(scenario=MockVenueScenario(partial_fill_ratio=0.0, emit_fill_on_submit=False)) k.process_intent(_mk_intent(action=E.ENTER, trade_id="m9b")) slot = k._get_slot(0) assert slot.active_entry_order is not None reject = _mk_venue_event( kind=KernelEventKind.ORDER_REJECT, trade_id="m9b", event_id="entry-reject-m9b", status=VenueEventStatus.REJECTED, ) k.on_venue_event(reject) assert k._get_slot(0).fsm_state == TradeStage.IDLE, ( "Entry-phase ORDER_REJECT must still reset slot to IDLE" ) # ============================================================ # G4: exit multi-leg — no phantom extra leg after last fill # ============================================================ class TestG4ExitLegOrdering: """G4: all_legs_done was computed before consume_exit_leg(), causing a phantom 3rd-leg attempt on the final leg's FULL_FILL when size > 1e-12.""" def test_two_leg_exit_closes_cleanly(self): """A 2-leg exit must close the slot without a phantom extra leg.""" k = _fresh_kernel() k.process_intent(_mk_intent(action=E.ENTER, trade_id="g4a", size=2.0, exit_leg_ratios=(0.5, 0.5))) slot = k._get_slot(0) assert slot.fsm_state == TradeStage.POSITION_OPEN # Leg 1 EXIT k.process_intent(_mk_intent(action=E.EXIT, trade_id="g4a", size=1.0, exit_leg_ratios=(0.5, 0.5))) slot = k._get_slot(0) assert slot.fsm_state in (TradeStage.POSITION_OPEN, TradeStage.EXIT_WORKING), ( f"After leg-1 fill, slot must be POSITION_OPEN or EXIT_WORKING, got {slot.fsm_state}" ) # Leg 2 EXIT k.process_intent(_mk_intent(action=E.EXIT, trade_id="g4a", size=1.0, exit_leg_ratios=(0.5, 0.5))) slot = k._get_slot(0) assert slot.is_free(), ( f"G4: 2-leg exit must fully close slot, got {slot.fsm_state}" ) def test_single_leg_exit_unaffected(self): """Single-leg exit (the common case) must still work correctly.""" k = _fresh_kernel() k.process_intent(_mk_intent(action=E.ENTER, trade_id="g4b")) k.process_intent(_mk_intent(action=E.EXIT, trade_id="g4b")) assert k._get_slot(0).is_free(), "Single-leg exit must close slot" # ============================================================ # G9: venue_order_id routed to exit order in exit phase # ============================================================ class TestG9VenueOrderIdRouting: """G9: venue_order_id update must target the exit order when in an exit FSM state, not the still-present entry order reference.""" def test_order_ack_in_exit_phase_fills_exit_order_id(self): """ORDER_ACK arriving after EXIT is submitted must fill exit order's venue_order_id, not the entry order's.""" k = _fresh_kernel(scenario=MockVenueScenario(partial_fill_ratio=0.0, emit_fill_on_submit=False)) k.process_intent(_mk_intent(action=E.ENTER, trade_id="g9a")) fill = _mk_venue_event( kind=KernelEventKind.FULL_FILL, trade_id="g9a", event_id="fill-g9a", price=100.0, size=1.0, filled_size=1.0, ) k.on_venue_event(fill) assert k._get_slot(0).fsm_state == TradeStage.POSITION_OPEN # Submit exit — mock emits no fill, just ACK k.process_intent(_mk_intent(action=E.EXIT, trade_id="g9a")) slot = k._get_slot(0) # If active_exit_order exists and has a venue_order_id, G9 is not triggered if slot.active_exit_order is not None: assert slot.active_exit_order.venue_order_id != "", ( "G9: exit order must have a venue_order_id after ORDER_ACK in exit phase" ) # ============================================================ # H6: _safe_enum gracefully handles unknown Rust FFI states # ============================================================ class TestH6SafeEnum: """H6: unknown enum variants from the Rust FFI must not raise ValueError — they must fall back to a safe default instead of crashing the process.""" def test_safe_enum_known_value(self): from prod.clean_arch.dita_v2.rust_backend import _safe_enum result = _safe_enum(TradeStage, "POSITION_OPEN", TradeStage.IDLE) assert result == TradeStage.POSITION_OPEN def test_safe_enum_unknown_value_returns_default(self): from prod.clean_arch.dita_v2.rust_backend import _safe_enum result = _safe_enum(TradeStage, "UNKNOWN_FUTURE_STATE", TradeStage.IDLE) assert result == TradeStage.IDLE, ( "H6: unknown enum variant must return default, not raise ValueError" ) def test_safe_enum_empty_string_returns_default(self): from prod.clean_arch.dita_v2.rust_backend import _safe_enum result = _safe_enum(TradeStage, "", TradeStage.IDLE) assert result == TradeStage.IDLE # ============================================================ # W10: BingxHttpError must not be blindly mapped to "REJECTED" # ============================================================ class TestW10HttpErrorMapping: """W10: _http_error_status() must distinguish transient errors (429, 5xx, DNS/transport) from genuine 4xx rejections — so the kernel sees RATE_LIMITED vs CANCEL_REJECT for the right cases.""" def _status(self, msg: str) -> str: from prod.clean_arch.dita_v2.bingx_venue import _http_error_status return _http_error_status(msg) def test_429_is_rate_limited(self): assert self._status("HTTP 429: Too Many Requests") == "RATE_LIMITED", ( "W10: 429 must map to RATE_LIMITED, not REJECTED" ) def test_503_is_rate_limited(self): assert self._status("HTTP 503: Service Unavailable") == "RATE_LIMITED", ( "W10: 503 (transient server error) must map to RATE_LIMITED" ) def test_500_is_rate_limited(self): assert self._status("HTTP 500: Internal Server Error") == "RATE_LIMITED" def test_400_is_rejected(self): assert self._status("HTTP 400: invalid symbol") == "REJECTED", ( "W10: genuine 4xx client error must map to REJECTED" ) def test_403_is_rejected(self): assert self._status("HTTP 403: Forbidden") == "REJECTED" def test_transport_error_is_rate_limited(self): assert self._status("DELETE /openApi/swap/v2/trade/order failed: ConnectionError") == "RATE_LIMITED", ( "W10: DNS/transport errors (no HTTP prefix) must map to RATE_LIMITED" ) def test_dns_error_is_rate_limited(self): assert self._status("Name or service not known") == "RATE_LIMITED" # ============================================================ # PinkHzStateWriter: non-blocking Hz write correctness # ============================================================ class TestPinkHzStateWriter: """PinkHzStateWriter: payload shape, vol_ok gate, and non-blocking guarantees.""" def _make_writer_no_hz(self): """Build a PinkHzStateWriter with a mock client that captures writes.""" from prod.clean_arch.dita_v2.hazelcast_projection import PinkHzStateWriter import unittest.mock as mock w = object.__new__(PinkHzStateWriter) w._writes = {} # {(map_attr, key): value} # Build fake non-blocking IMap proxy def _make_map(name): m = mock.MagicMock(name=f"map:{name}") def _put(key, value): w._writes[(name, key)] = value m.put.side_effect = _put return m w._state_map = _make_map("DOLPHIN_STATE_PINK") w._pnl_map = _make_map("DOLPHIN_PNL_PINK") w._client = mock.MagicMock() return w def test_engine_snapshot_writes_two_keys(self): w = self._make_writer_no_hz() w.write_engine_snapshot( {"slot_id": 0, "fsm_state": "IDLE"}, {"capital": 25000.0, "trade_seq": 42}, posture="APEX", ) assert ("DOLPHIN_STATE_PINK", "engine_snapshot") in w._writes, ( "PinkHzStateWriter must write engine_snapshot key" ) assert ("DOLPHIN_STATE_PINK", "latest") in w._writes, ( "PinkHzStateWriter must write latest key (BLUE-compatible)" ) def test_engine_snapshot_has_strategy_pink(self): import json w = self._make_writer_no_hz() w.write_engine_snapshot({"slot_id": 0}, {"capital": 10000.0}) snap = json.loads(w._writes[("DOLPHIN_STATE_PINK", "engine_snapshot")]) assert snap["strategy"] == "pink", "engine_snapshot must identify as pink" def test_latest_key_has_blue_compatible_fields(self): import json w = self._make_writer_no_hz() w.write_engine_snapshot({"slot_id": 0}, {"capital": 5000.0, "realized_pnl_total": 123.4, "trade_seq": 7}) latest = json.loads(w._writes[("DOLPHIN_STATE_PINK", "latest")]) for field in ("strategy", "capital", "date", "pnl", "trades", "posture", "updated_at"): assert field in latest, f"BLUE-compatible 'latest' key missing field: {field}" def test_our_leverage_in_snapshot(self): import json w = self._make_writer_no_hz() w.write_engine_snapshot( {"slot_id": 0, "size": 0.5, "entry_price": 50000.0}, {"capital": 25000.0}, our_leverage=1.0, ) snap = json.loads(w._writes[("DOLPHIN_STATE_PINK", "engine_snapshot")]) assert "our_leverage" in snap, "our_leverage (dual-leverage: system layer) must be in Hz snapshot" def test_daily_pnl_write(self): import json w = self._make_writer_no_hz() w.write_daily_pnl({"realized_pnl_total": 45.6, "capital": 25000.0, "trade_seq": 3}) key = next((k for k in w._writes if k[0] == "DOLPHIN_PNL_PINK"), None) assert key is not None, "write_daily_pnl must write to DOLPHIN_PNL_PINK" row = json.loads(w._writes[key]) assert row["pnl"] == 45.6 def test_write_survives_exception(self): """Hz write failure must never propagate — observability must not affect trading.""" from prod.clean_arch.dita_v2.hazelcast_projection import _hz_write_no_wait import unittest.mock as mock bad_map = mock.MagicMock() bad_map.put.side_effect = RuntimeError("Hz down") _hz_write_no_wait(bad_map, "key", "value") # must not raise # ============================================================ # vol_ok gate in DecisionEngine # ============================================================ class TestVolOkGate: """DecisionEngine must block ENTERs when vol_ok=False in scan_payload.""" def _make_snapshot(self, vol_ok: bool, vdiv: float = -0.03, irp: float = 0.60): from prod.clean_arch.ports.data_feed import MarketSnapshot from datetime import datetime, timezone return MarketSnapshot( timestamp=datetime.now(timezone.utc), symbol="BTCUSDT", price=50000.0, velocity_divergence=vdiv, irp_alignment=irp, scan_payload={"vol_ok": vol_ok, "posture": "APEX"}, ) def _engine(self): from prod.clean_arch.dita.decision import DecisionEngine, DecisionConfig cfg = DecisionConfig( vel_div_threshold=-0.02, vel_div_extreme=-0.05, fixed_tp_pct=0.0020, max_hold_bars=250, capital_fraction=0.20, max_leverage=3.0, allow_short=True, allow_long=False, ) return DecisionEngine(cfg) def _ctx(self, open_positions: int = 0, capital: float = 25000.0): from prod.clean_arch.dita.contracts import DecisionContext return DecisionContext(capital=capital, open_positions=open_positions) def test_vol_ok_false_blocks_enter(self): eng = self._engine() snap = self._make_snapshot(vol_ok=False) decision = eng.decide(snap, self._ctx()) assert decision.action.value in ("HOLD", "NO_ACTION", "SKIP", "VOL_GATE"), ( f"vol_ok=False must block ENTER, got action={decision.action.value!r} reason={getattr(decision, 'reason', '?')!r}" ) def test_vol_ok_true_allows_enter(self): eng = self._engine() snap = self._make_snapshot(vol_ok=True) decision = eng.decide(snap, self._ctx()) assert decision.action.value not in ("VOL_GATE",), ( "vol_ok=True must not block on vol_ok gate" ) # ============================================================ # DC gate (_dc_contradicts) — SYSTEM BIBLE §4.2 # ============================================================ class TestDCGate: """Direction Confirmation gate: rising price over 7-tick window blocks SHORT entry.""" def _rt(self, prices: list): """Build a minimal PinkDirectRuntime-like object with price history populated.""" from collections import deque import types obj = types.SimpleNamespace() obj._price_history = deque(prices, maxlen=10) # Bind the method to obj from prod.clean_arch.runtime.pink_direct import PinkDirectRuntime obj._dc_contradicts = lambda **kw: PinkDirectRuntime._dc_contradicts(obj, **kw) return obj def test_rising_price_contradicts(self): # p[-8] = 100.0, p[-1] = 100.2 → chg = +20 bps → CONTRADICT prices = [100.0] + [100.05] * 6 + [100.2] rt = self._rt(prices) assert rt._dc_contradicts(), "Rising price must be DC CONTRADICT" def test_falling_price_confirms(self): # p[-8] = 100.0, p[-1] = 99.9 → chg = -10 bps → CONFIRM (not a block) prices = [100.0] + [99.95] * 6 + [99.9] rt = self._rt(prices) assert not rt._dc_contradicts(), "Falling price must NOT be DC CONTRADICT" def test_flat_price_neutral(self): # < 0.75 bps change → NEUTRAL prices = [100.0] * 8 rt = self._rt(prices) assert not rt._dc_contradicts(), "Flat price must NOT be DC CONTRADICT" def test_insufficient_history_neutral(self): # < 8 prices → not enough data → NEUTRAL (allow entry) prices = [100.0, 100.5] # only 2 entries rt = self._rt(prices) assert not rt._dc_contradicts(), "Insufficient history must NOT block entry" def test_exactly_threshold_neutral(self): # Exactly 0.75 bps → NOT a CONTRADICT (> not >=) prices = [100.0] + [100.0] * 6 + [100.0075] # 0.75 bps exactly rt = self._rt(prices) assert not rt._dc_contradicts(), "Exactly at threshold must NOT be CONTRADICT" # ============================================================ # TUI normalization — _normalize_eng_for_tui # ============================================================ class TestNormalizeEngForTui: """_normalize_eng_for_tui translates DITAv2 Hz snapshot to NAUTILUS-era field names.""" def _norm(self, eng: dict) -> dict: from Observability.dolphin_status_pink import _normalize_eng_for_tui return _normalize_eng_for_tui(eng) def test_empty_returns_empty(self): assert self._norm({}) == {} def test_already_nautilus_format_passthrough(self): eng = {"trades_executed": 5, "capital": 25000.0} out = self._norm(eng) assert out is eng or out["trades_executed"] == 5 def test_ditav2_format_adds_trades_executed(self): eng = {"trade_seq": 7, "capital": 25000.0, "slot": {}} out = self._norm(eng) assert out["trades_executed"] == 7, "trade_seq must be aliased as trades_executed" def test_open_positions_becomes_list(self): eng = {"open_positions": 1, "slot": {"size": 0.5, "entry_price": 50000.0}} out = self._norm(eng) assert isinstance(out["open_positions"], list), "open_positions int must become list" assert len(out["open_positions"]) == 1 def test_zero_open_positions_empty_list(self): eng = {"open_positions": 0, "slot": {}} out = self._norm(eng) assert out["open_positions"] == [], "zero open_positions must become empty list" # ============================================================ # _to_rust_bytes / _json_null_clean — null-byte safety # ============================================================ class TestRustBytesNullSafety: """_to_rust_bytes must never produce a 0x00 byte in its output. Root cause: ctypes.c_char_p treats the first 0x00 as a C null terminator, silently truncating the JSON before Rust's serde_json sees the full payload. Reproduces the INVALID_INTENT_PARSE bug seen during BingX VST smoke test. """ def _encode(self, payload): from prod.clean_arch.dita_v2.rust_backend import _to_rust_bytes return _to_rust_bytes(payload) def _clean(self, obj): from prod.clean_arch.dita_v2.rust_backend import _json_null_clean return _json_null_clean(obj) def test_no_null_bytes_in_normal_exit_intent(self): payload = { "action": "EXIT", "asset": "BNB-USDT", "leverage": 1.3465735902799727, "target_size": 1.76, "reference_price": 66337.09, "limit_price": 0.0, "trade_id": "t1", "metadata": {}, } encoded = self._encode(payload) assert b"\x00" not in encoded, "EXIT intent must have no null bytes" def test_no_null_bytes_when_string_contains_u0000(self): """A string value containing \\u0000 must not produce a null byte in output.""" payload = {"event_id": "BX\x00data", "price": 100.0} encoded = self._encode(payload) assert b"\x00" not in encoded, "Null char in string must not produce null byte" def test_no_null_bytes_in_seen_event_ids(self): """seen_event_ids list is serialized with all other slot fields.""" payload = {"seen_event_ids": ["123", "456\x00789", "999"], "size": 1.76} encoded = self._encode(payload) assert b"\x00" not in encoded, "seen_event_ids with null chars must be clean" def test_no_null_bytes_in_nested_metadata(self): payload = {"metadata": {"venue_note": "order\x00ok", "id": 42}, "asset": "ENJ-USDT"} encoded = self._encode(payload) assert b"\x00" not in encoded, "Nested metadata null chars must be sanitized" def test_output_is_valid_json(self): import json payload = {"action": "ENTER", "asset": "BNB-USDT", "leverage": 2.7, "seen_event_ids": ["e1"]} encoded = self._encode(payload) parsed = json.loads(encoded) assert parsed["action"] == "ENTER" def test_json_null_clean_replaces_null_in_string(self): result = self._clean({"key": "val\x00ue"}) assert "\x00" not in result["key"] assert "val" in result["key"] def test_json_null_clean_recursion(self): obj = {"nested": {"list": ["a\x00b", 1, {"deep": "x\x00y"}]}} cleaned = self._clean(obj) assert "\x00" not in cleaned["nested"]["list"][0] assert "\x00" not in cleaned["nested"]["list"][2]["deep"] def test_normal_ascii_payload_roundtrips_intact(self): import json payload = {"action": "EXIT", "asset": "BTC-USDT", "leverage": 1.5, "size": 0.001} encoded = self._encode(payload) assert json.loads(encoded)["asset"] == "BTC-USDT" assert json.loads(encoded)["leverage"] == 1.5 # ============================================================ # FSM occupancy & rollback — the "no stranded slot" guarantee # # The Rust FSM is the authority on slot state. These tests # exercise the invariants that prevent orphaned exchange orders: # # 1. SLOT_BUSY fires whenever a slot is non-IDLE, regardless # of whether a fill has arrived yet (ENTRY_WORKING state). # 2. If venue.submit() raises, the Python layer feeds a # synthetic ORDER_REJECT back so the FSM rolls back to IDLE. # 3. After a rollback the slot is immediately reusable. # 4. N consecutive submit failures never strand the slot. # 5. POSITION_OPEN (filled) also blocks a new ENTER. # 6. Rapid enter→exit→enter cycles leave no residual state. # ============================================================ class _RaisingVenue(MockVenueAdapter): """Mock venue that always raises on submit — simulates BingX timeout.""" def submit(self, intent): raise TimeoutError("BingX backend call exceeded 30.0s timeout") class TestFSMOccupancyAndRollback: """Core FSM safety: slot must never be stranded by a submit failure.""" # ── 1. ENTRY_WORKING blocks a second ENTER ────────────────────────────── def test_slot_busy_in_entry_working_state(self): """Slot in ENTRY_WORKING (ACK received, no fill yet) must reject a new ENTER for a different trade_id with SLOT_BUSY. partial_fill_ratio=0.0 → mock emits ACK only (no fill) → ENTRY_WORKING. """ # partial_fill_ratio=0.0 suppresses the fill event so slot stays ENTRY_WORKING k = _fresh_kernel(scenario=MockVenueScenario(partial_fill_ratio=0.0)) r1 = k.process_intent(_mk_intent(action=E.ENTER, trade_id="ew1")) assert r1.accepted slot = k._get_slot(0) assert slot.fsm_state == TradeStage.ENTRY_WORKING, ( f"Expected ENTRY_WORKING after ACK-only submit, got {slot.fsm_state}" ) r2 = k.process_intent(_mk_intent(action=E.ENTER, trade_id="ew2_different")) assert not r2.accepted, "ENTRY_WORKING slot must block a new ENTER" assert r2.diagnostic_code == KernelDiagnosticCode.SLOT_BUSY def test_position_open_blocks_enter(self): """Slot in POSITION_OPEN (fill received) must reject a new ENTER. Default MockVenueScenario has partial_fill_ratio=1.0 → full fill on submit → POSITION_OPEN immediately. """ # Default scenario fills immediately (partial_fill_ratio=1.0) k = _fresh_kernel() r1 = k.process_intent(_mk_intent(action=E.ENTER, trade_id="po1")) assert r1.accepted slot = k._get_slot(0) assert slot.fsm_state == TradeStage.POSITION_OPEN r2 = k.process_intent(_mk_intent(action=E.ENTER, trade_id="po2_different")) assert not r2.accepted assert r2.diagnostic_code == KernelDiagnosticCode.SLOT_BUSY # ── 2. venue.submit failure → FSM rollback to IDLE ────────────────────── def test_venue_submit_failure_rolls_back_slot_to_idle(self): """If venue.submit() raises, a synthetic REJECTED event must be fed back so the FSM returns to IDLE — not stranded in ORDER_REQUESTED.""" k = ExecutionKernel(max_slots=1, venue=_RaisingVenue()) k.account.snapshot.capital = 25000.0 r = k.process_intent(_mk_intent(action=E.ENTER, trade_id="rf1")) # process_intent itself must not raise — it catches the venue error assert not r.accepted or r.accepted # either accepted+rolled-back or rejected slot = k._get_slot(0) assert slot.is_free(), ( f"Slot must be IDLE after submit failure, got fsm_state={slot.fsm_state}" ) def test_venue_submit_failure_allows_reenter(self): """After a submit failure rollback the slot must immediately accept a fresh ENTER.""" k = ExecutionKernel(max_slots=1, venue=_RaisingVenue()) k.account.snapshot.capital = 25000.0 k.process_intent(_mk_intent(action=E.ENTER, trade_id="rf2a")) assert k._get_slot(0).is_free(), "Slot must be IDLE after failed submit" # Now switch to a working venue and verify re-entry is accepted k2 = _fresh_kernel() r = k2.process_intent(_mk_intent(action=E.ENTER, trade_id="rf2b")) assert r.accepted, "Fresh kernel slot must accept ENTER after previous failure" def test_n_consecutive_submit_failures_never_strand_slot(self): """Ten consecutive venue.submit() raises must leave slot IDLE each time.""" k = ExecutionKernel(max_slots=1, venue=_RaisingVenue()) k.account.snapshot.capital = 25000.0 for i in range(10): k.process_intent(_mk_intent(action=E.ENTER, trade_id=f"nf{i}")) slot = k._get_slot(0) assert slot.is_free(), ( f"Iteration {i}: slot stranded in {slot.fsm_state} after submit failure" ) def test_submit_failure_then_success_single_position(self): """After N submit failures, switching to a working venue must produce exactly one position — not accumulate orphans.""" # Fail twice on a fresh kernel (rollback each time) k = ExecutionKernel(max_slots=1, venue=_RaisingVenue()) k.account.snapshot.capital = 25000.0 for _ in range(2): k.process_intent(_mk_intent(action=E.ENTER, trade_id="sf1")) assert k._get_slot(0).is_free() # Now hand off to a working mock k2 = _fresh_kernel() r = k2.process_intent(_mk_intent(action=E.ENTER, trade_id="sf_ok")) assert r.accepted slot = k2._get_slot(0) assert not slot.is_free(), "One successful ENTER → slot occupied" # A second ENTER on the same kernel must be rejected r2 = k2.process_intent(_mk_intent(action=E.ENTER, trade_id="sf_extra")) assert not r2.accepted assert r2.diagnostic_code == KernelDiagnosticCode.SLOT_BUSY # ── 3. Rapid enter→exit→enter cycles leave no residual ────────────────── def test_rapid_enter_exit_enter_no_residual(self): """20 enter→exit cycles must leave slot IDLE and capital positive. Default scenario fills immediately (partial_fill_ratio=1.0). """ # Default: partial_fill_ratio=1.0 → full fill on submit → POSITION_OPEN k = _fresh_kernel() for i in range(20): r_e = k.process_intent(_mk_intent(action=E.ENTER, trade_id=f"cyc{i}")) assert r_e.accepted, f"Cycle {i} ENTER rejected: {r_e.diagnostic_code}" r_x = k.process_intent(_mk_intent(action=E.EXIT, trade_id=f"cyc{i}")) assert r_x.accepted, f"Cycle {i} EXIT rejected: {r_x.diagnostic_code}" slot = k._get_slot(0) assert slot.is_free(), f"Slot not IDLE after 20 cycles: {slot.fsm_state}" assert k.account.snapshot.capital > 0 def test_exit_on_idle_slot_always_rejected(self): """EXIT on a fresh IDLE slot must always be rejected — no phantom closes.""" k = _fresh_kernel() for asset in ("BTC-USDT", "ETH-USDT", "SOL-USDT"): r = k.process_intent(_mk_intent(action=E.EXIT, trade_id=f"idle_{asset}", asset=asset)) assert not r.accepted, f"EXIT on IDLE slot for {asset} must be rejected" def test_entry_working_then_fill_then_enter_rejected(self): """ENTRY_WORKING → fill arrives → POSITION_OPEN → new ENTER still rejected.""" # partial_fill_ratio=0.0: ACK only → ENTRY_WORKING; then we deliver fill manually k = _fresh_kernel(scenario=MockVenueScenario(partial_fill_ratio=0.0)) k.process_intent(_mk_intent(action=E.ENTER, trade_id="ew_fill")) assert k._get_slot(0).fsm_state == TradeStage.ENTRY_WORKING # Deliver fill manually fill = _mk_venue_event( kind=KernelEventKind.FULL_FILL, trade_id="ew_fill", price=100.0, size=1.0, filled_size=1.0, ) k.on_venue_event(fill) assert k._get_slot(0).fsm_state == TradeStage.POSITION_OPEN # Now try new ENTER r = k.process_intent(_mk_intent(action=E.ENTER, trade_id="ew_fill_2")) assert not r.accepted assert r.diagnostic_code == KernelDiagnosticCode.SLOT_BUSY def test_five_assets_one_slot_only_first_accepted(self): """Simulate 5 scan signals for 5 assets arriving in rapid succession. Only the first must open — the other 4 must all hit SLOT_BUSY. This reproduces the production multi-position leak scenario.""" k = _fresh_kernel() assets = ["LTC-USDT", "NEO-USDT", "DASH-USDT", "TRX-USDT", "ETC-USDT"] results = [] for i, asset in enumerate(assets): r = k.process_intent(_mk_intent( action=E.ENTER, trade_id=f"multi_{i}", asset=asset )) results.append((asset, r.accepted, r.diagnostic_code)) accepted = [a for a, ok, _ in results if ok] rejected = [(a, c) for a, ok, c in results if not ok] assert len(accepted) == 1, f"Expected exactly 1 ENTER accepted, got {accepted}" assert accepted[0] == "LTC-USDT", "First asset must win the slot" assert all(c == KernelDiagnosticCode.SLOT_BUSY for _, c in rejected), ( f"All rejections must be SLOT_BUSY: {rejected}" )