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