PINK: E2E trace analysis — Pass 17 unsafe review/dead code/build/protocols (T1-T14)
Seventeenth pass: catch_unwind + AssertUnwindSafe partially mutated state no
rollback (T1 High), HazelcastRowWriter bare json.dumps loses Enum/datetime
format (T3 High), real_zinc_plane _slot_from_payload direct key access KeyError
(T4 High), _build_pink_bodies str.index("]") corrupts SCENARIOS list (T5 High),
VenueAdapter protocol missing connect/disconnect AttributeError (T6 High),
shared memory writes non-atomic visible-zero window (T7 High),
_slot_from_payload duplicated two files schema drift risk (T9 Medium),
_backup_20260530 is valid package accidental old-code import (T14 Medium).
319 total flaws across 17 passes.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
970
prod/clean_arch/dita_v2/test_flaws.py
Normal file
970
prod/clean_arch/dita_v2/test_flaws.py
Normal file
@@ -0,0 +1,970 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user