from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime, timezone import random from typing import Any, Callable, Iterable, Optional, Sequence import pytest from prod.clean_arch.dita_v2 import ( BingxVenueAdapter, BackendMode, ControlUpdate, ExecutionKernel, InMemoryControlPlane, InMemoryZincPlane, KernelCommandType, KernelControlSnapshot, KernelDiagnosticCode, KernelEventKind, KernelIntent, KernelMode, KernelVerbosity, TradeSide, TradeStage, VenueEvent, VenueEventStatus, VenueOrder, VenueOrderStatus, ) from prod.clean_arch.ports.execution import ExchangeStateSnapshot, ExecutionReceipt def _norm_symbol(symbol: str) -> str: return str(symbol or "").replace("-", "").replace("_", "").upper() def _snapshot( *, capital: float = 25_000.0, positions: list[dict[str, Any]] | None = None, open_orders: list[dict[str, Any]] | None = None, all_orders: list[dict[str, Any]] | None = None, all_fills: list[dict[str, Any]] | None = None, source: str = "bingx", recovered: bool = False, ) -> ExchangeStateSnapshot: position_map = { _norm_symbol(str(row.get("symbol", ""))): dict(row) for row in (positions or []) if _norm_symbol(str(row.get("symbol", ""))) } return ExchangeStateSnapshot( timestamp=datetime.now(timezone.utc), capital=capital, equity=capital, open_positions=position_map, open_orders=[dict(row) for row in (open_orders or [])], all_orders=[dict(row) for row in (all_orders or [])], all_fills=[dict(row) for row in (all_fills or [])], account={"balances": [{"asset": "USDT", "total": capital}]}, open_notional=0.0, source=source, recovered=recovered, ) def _sign(side: TradeSide) -> int: return -1 if side == TradeSide.SHORT else 1 def _position_row(asset: str, side: TradeSide, qty: float, price: float) -> dict[str, Any]: signed_qty = _sign(side) * abs(float(qty)) return { "symbol": asset, "positionSide": side.value, "positionAmt": f"{signed_qty}", "avgPrice": f"{price}", "markPrice": f"{price}", "leverage": "2", } @dataclass(frozen=True) class VenueScriptStep: name: str submit_kind: str fill_ratio: float = 0.0 cancel_kind: str = "cancel_ack" submit_advances: bool = True cancel_advances: bool = True reject_reason: str = "MOCK_REJECT" cancel_reason: str = "MOCK_CANCEL" @dataclass(frozen=True) class SignalAction: kind: str price: float target_size: float = 0.0 fill_ratio: float = 1.0 reason: str = "" require_close: bool = False class ScriptedVenueAdapter: """Deterministic venue adapter that plays scripted submit/cancel outcomes.""" def __init__(self, steps: Sequence[VenueScriptStep]) -> None: self.steps = list(steps) self._step_index = 0 self._active_step_index = 0 self._order_seq = 1 self._event_seq = 1 self._open_orders: dict[str, VenueOrder] = {} self._open_positions: dict[str, dict[str, Any]] = {} self.calls: list[tuple[str, Any]] = [] def _next_step(self) -> VenueScriptStep: if self._step_index < len(self.steps): step = self.steps[self._step_index] self._step_index += 1 return step return VenueScriptStep(name="default", submit_kind="ack_only") def submit(self, intent: KernelIntent) -> list[VenueEvent]: self.calls.append(("submit", intent.action.value, intent.trade_id, intent.slot_id)) step = self._next_step() self._active_step_index = max(0, self._step_index - 1) order_id = f"MOCK-{self._order_seq:08d}" self._order_seq += 1 client_id = f"{intent.trade_id}:{intent.intent_id}" order = VenueOrder( internal_trade_id=intent.trade_id, venue_order_id=order_id, venue_client_id=client_id, side=intent.side, intended_size=float(intent.target_size), filled_size=0.0, average_fill_price=float(intent.reference_price or 0.0), status=VenueOrderStatus.NEW, metadata={"slot_id": intent.slot_id, "asset": intent.asset, "action": intent.action.value}, ) if step.submit_kind == "entry_reject": return [ self._event( intent=intent, order=order, kind=KernelEventKind.ORDER_REJECT, status=VenueEventStatus.REJECTED, reason=step.reject_reason, ) ] ack = self._event( intent=intent, order=order, kind=KernelEventKind.ORDER_ACK, status=VenueEventStatus.ACKED, ) self._open_orders[order_id] = order events = [ack] if step.submit_kind in {"entry_partial", "exit_partial", "entry_full", "exit_full"}: fill_ratio = max(0.0, min(1.0, float(step.fill_ratio or 0.0))) if fill_ratio <= 0.0: fill_ratio = 1.0 if step.submit_kind.endswith("full") else 0.5 fill_size = float(intent.target_size) * fill_ratio fill_kind = KernelEventKind.FULL_FILL if fill_ratio >= 1.0 else KernelEventKind.PARTIAL_FILL fill_status = VenueEventStatus.FILLED if fill_kind == KernelEventKind.FULL_FILL else VenueEventStatus.PARTIALLY_FILLED events.append( self._event( intent=intent, order=order, kind=fill_kind, status=fill_status, price=float(intent.reference_price or 0.0), filled_size=fill_size, remaining_size=max(0.0, float(intent.target_size) - fill_size), ) ) self._apply_fill(intent, fill_size, fill_kind == KernelEventKind.FULL_FILL) if fill_kind == KernelEventKind.FULL_FILL: self._open_orders.pop(order_id, None) return events def cancel(self, order: VenueOrder, *, reason: str = "") -> list[VenueEvent]: self.calls.append(("cancel", order.venue_order_id, reason)) step = self.steps[min(self._active_step_index, len(self.steps) - 1)] if self.steps else VenueScriptStep(name="default", submit_kind="ack_only") if step.cancel_kind == "cancel_reject": return [ self._event( intent=self._intent_from_order(order), order=order, kind=KernelEventKind.CANCEL_REJECT, status=VenueEventStatus.CANCELED_REJECTED, reason=step.cancel_reason, ) ] self._open_orders.pop(order.venue_order_id, None) if step.cancel_advances: self._step_index = max(self._step_index, self._active_step_index + 1) return [ self._event( intent=self._intent_from_order(order), order=order, kind=KernelEventKind.CANCEL_ACK, status=VenueEventStatus.CANCELED, reason=reason or step.cancel_reason, ) ] def open_orders(self) -> list[VenueOrder]: return list(self._open_orders.values()) def open_positions(self) -> list[dict[str, Any]]: return list(self._open_positions.values()) def reconcile(self) -> list[VenueEvent]: events: list[VenueEvent] = [] for order in self._open_orders.values(): events.append( self._event( intent=self._intent_from_order(order), order=order, kind=KernelEventKind.ORDER_ACK, status=VenueEventStatus.ACKED, reason="RECONCILE", ) ) for row in self._open_positions.values(): events.append( VenueEvent( timestamp=datetime.now(timezone.utc), event_id=f"EV-{self._event_seq:08d}", trade_id=str(row.get("trade_id", "")), slot_id=int(row.get("slot_id", 0)), kind=KernelEventKind.RECONCILE, status=VenueEventStatus.ACKED, venue_order_id=str(row.get("venue_order_id", "")), venue_client_id=str(row.get("venue_client_id", "")), side=TradeSide(str(row.get("side", TradeSide.FLAT.value))), asset=str(row.get("symbol", "")), price=float(row.get("avgPrice", 0.0)), size=abs(float(row.get("positionAmt", 0.0))), filled_size=abs(float(row.get("positionAmt", 0.0))), remaining_size=0.0, reason="RECONCILE", raw_payload=dict(row), metadata={"source": "mock"}, ) ) self._event_seq += 1 return events def _event( self, *, intent: KernelIntent, order: VenueOrder, kind: KernelEventKind, status: VenueEventStatus, price: float | None = None, filled_size: float = 0.0, remaining_size: float = 0.0, reason: str = "", ) -> VenueEvent: event = VenueEvent( timestamp=datetime.now(timezone.utc), event_id=f"EV-{self._event_seq:08d}", trade_id=intent.trade_id, slot_id=intent.slot_id, kind=kind, status=status, venue_order_id=order.venue_order_id, venue_client_id=order.venue_client_id, side=order.side, asset=intent.asset, price=float(price if price is not None else intent.reference_price or 0.0), size=float(intent.target_size or 0.0), filled_size=float(filled_size), remaining_size=float(remaining_size), reason=reason, raw_payload={ "status": status.value, "orderId": order.venue_order_id, "clientOrderId": order.venue_client_id, "symbol": intent.asset, "side": order.side.value, "action": intent.action.value, }, metadata={"intent_id": intent.intent_id, "action": intent.action.value}, ) self._event_seq += 1 return event def _intent_from_order(self, order: VenueOrder) -> KernelIntent: return KernelIntent( timestamp=datetime.now(timezone.utc), intent_id=order.venue_client_id, trade_id=order.internal_trade_id, slot_id=int(order.metadata.get("slot_id", 0)), asset=str(order.metadata.get("asset", "")), side=order.side, action=KernelCommandType.EXIT if order.metadata.get("action") == "EXIT" else KernelCommandType.ENTER, reference_price=float(order.average_fill_price or 0.0), target_size=float(order.intended_size or 0.0), leverage=2.0, reason=str(order.metadata.get("action", "")), ) def _apply_fill(self, intent: KernelIntent, filled_size: float, full: bool) -> None: signed = _sign(intent.side) * abs(float(filled_size)) row = self._open_positions.get(intent.asset) if intent.action == KernelCommandType.ENTER: self._open_positions[intent.asset] = { "symbol": intent.asset, "trade_id": intent.trade_id, "slot_id": intent.slot_id, "side": intent.side.value, "positionSide": intent.side.value, "positionAmt": f"{signed}", "avgPrice": f"{intent.reference_price}", "markPrice": f"{intent.reference_price}", "venue_order_id": f"MOCK-{self._order_seq - 1:08d}", "venue_client_id": f"{intent.trade_id}:{intent.intent_id}", } return if row is None: return current = abs(float(row.get("positionAmt", 0.0))) new_qty = max(0.0, current - abs(float(filled_size))) if new_qty <= 1e-12 or full: self._open_positions.pop(intent.asset, None) return row["positionAmt"] = f"{_sign(intent.side) * new_qty}" self._open_positions[intent.asset] = row @dataclass(frozen=True) class BingxE2EStep: name: str submit_kind: str submit_fill_ratio: float before_snapshot: ExchangeStateSnapshot after_snapshot: ExchangeStateSnapshot receipt: ExecutionReceipt submit_advances: bool = True cancel_kind: str = "cancel_ack" cancel_advances: bool = True cancel_before_snapshot: ExchangeStateSnapshot | None = None cancel_after_snapshot: ExchangeStateSnapshot | None = None class BingxE2EBackend: """Stateful fake backend that drives the real BingxVenueAdapter.""" def __init__(self, steps: Sequence[BingxE2EStep]) -> None: self.steps = list(steps) self.index = 0 self.calls: list[tuple[str, Any]] = [] self.connected = False self._operation: str | None = None self._active_index = 0 async def connect(self) -> bool: self.connected = True self.calls.append(("connect", None)) return True async def disconnect(self) -> None: self.connected = False self.calls.append(("disconnect", None)) async def refresh_state(self, symbol: str | None = None, *, include_history: bool = False) -> ExchangeStateSnapshot: self.calls.append(("refresh_state", symbol, include_history, self.index, self._operation)) step = self.steps[min(self._active_index, len(self.steps) - 1)] if self._operation == "submit": snapshot = step.after_snapshot if step.submit_advances: self.index = min(self.index + 1, len(self.steps) - 1) self._operation = None return snapshot if self._operation == "cancel": snapshot = step.cancel_after_snapshot or step.after_snapshot if step.cancel_advances: self.index = min(self.index + 1, len(self.steps) - 1) self._operation = None return snapshot return step.before_snapshot async def submit_intent(self, legacy_intent: Any) -> ExecutionReceipt: self.calls.append(("submit_intent", legacy_intent.trade_id, legacy_intent.action.value)) self._active_index = min(self.index, len(self.steps) - 1) step = self.steps[self._active_index] self._operation = "submit" if step.submit_kind == "reject": return ExecutionReceipt( timestamp=datetime.now(timezone.utc), status="REJECTED", symbol=legacy_intent.asset, side=legacy_intent.side.value, action=legacy_intent.action.value, quantity=float(legacy_intent.target_size), price=float(legacy_intent.reference_price), client_order_id=step.receipt.client_order_id, order_id=step.receipt.order_id, raw_ack={"status": "REJECTED", "msg": "E2E_REJECT"}, raw_state={}, ) return step.receipt async def cancel_order(self, order: VenueOrder, *, reason: str = "") -> dict[str, Any]: self.calls.append(("cancel_order", order.venue_order_id, reason)) self._operation = "cancel" step = self.steps[min(self._active_index, len(self.steps) - 1)] if step.cancel_kind == "cancel_reject": return {"status": "CANCEL_REJECTED", "msg": reason or "E2E_CANCEL_REJECT"} return {"status": "CANCELED", "msg": reason or "E2E_CANCEL_ACK"} def _kernel(venue: Any, *, zinc: Any | None = None) -> ExecutionKernel: return ExecutionKernel( max_slots=1, control_plane=InMemoryControlPlane( KernelControlSnapshot( mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE, backend_mode=BackendMode.MOCK if not isinstance(venue, BingxVenueAdapter) else BackendMode.BINGX, trace_transitions=True, debug_clickhouse_enabled=True, mirror_to_hazelcast=True, ) ), venue=venue, zinc_plane=zinc or InMemoryZincPlane(), ) def _intent( *, action: KernelCommandType, trade_id: str, side: TradeSide, slot_id: int = 0, target_size: float = 1.0, price: float = 100.0, exit_leg_ratios: Sequence[float] = (1.0,), reason: str = "E2E", ) -> KernelIntent: return KernelIntent( timestamp=datetime.now(timezone.utc), intent_id=f"{trade_id}:{action.value}:{slot_id}:{reason}", trade_id=trade_id, slot_id=slot_id, asset="BTCUSDT", side=side, action=action, reference_price=price, target_size=target_size, leverage=2.0, exit_leg_ratios=tuple(exit_leg_ratios), reason=reason, ) def _entry_event(trade_id: str, slot_id: int, side: TradeSide, target_size: float, price: float, *, partial: bool = False, ratio: float = 1.0) -> list[VenueEvent]: order = VenueOrder( internal_trade_id=trade_id, venue_order_id=f"{trade_id}-entry-oid", venue_client_id=f"{trade_id}:entry", side=side, intended_size=target_size, filled_size=0.0, average_fill_price=price, status=VenueOrderStatus.NEW, metadata={"slot_id": slot_id, "asset": "BTCUSDT", "action": "ENTER"}, ) events = [ VenueEvent( timestamp=datetime.now(timezone.utc), event_id=f"{trade_id}-ack", trade_id=trade_id, slot_id=slot_id, kind=KernelEventKind.ORDER_ACK, status=VenueEventStatus.ACKED, venue_order_id=order.venue_order_id, venue_client_id=order.venue_client_id, side=side, asset="BTCUSDT", price=price, size=target_size, filled_size=0.0, remaining_size=target_size, ) ] if partial: fill_size = target_size * ratio events.append( VenueEvent( timestamp=datetime.now(timezone.utc), event_id=f"{trade_id}-fill", trade_id=trade_id, slot_id=slot_id, kind=KernelEventKind.PARTIAL_FILL, status=VenueEventStatus.PARTIALLY_FILLED, venue_order_id=order.venue_order_id, venue_client_id=order.venue_client_id, side=side, asset="BTCUSDT", price=price, size=target_size, filled_size=fill_size, remaining_size=max(0.0, target_size - fill_size), ) ) else: events.append( VenueEvent( timestamp=datetime.now(timezone.utc), event_id=f"{trade_id}-fill", trade_id=trade_id, slot_id=slot_id, kind=KernelEventKind.FULL_FILL, status=VenueEventStatus.FILLED, venue_order_id=order.venue_order_id, venue_client_id=order.venue_client_id, side=side, asset="BTCUSDT", price=price, size=target_size, filled_size=target_size, remaining_size=0.0, ) ) return events def _close_and_mark(kernel: ExecutionKernel, *, trade_id: str, side: TradeSide, exit_size: float, price: float, reason: str) -> None: kernel.process_intent(_intent(action=KernelCommandType.EXIT, trade_id=trade_id, side=side, target_size=exit_size, price=price, exit_leg_ratios=(0.5, 0.5), reason=reason)) def _assert_full_cycle(kernel: ExecutionKernel, *, side: TradeSide, trade_id: str, expect_closed: bool = True) -> None: slot = kernel.slot(0) assert slot.trade_id == trade_id if expect_closed: assert slot.closed is True assert slot.fsm_state in {TradeStage.CLOSED, TradeStage.IDLE} assert kernel.account.snapshot.open_positions in {0, 1} def _bingx_steps_for_cycle(side: TradeSide, *, hung_exit: bool = False, cancel_reject: bool = False) -> list[BingxE2EStep]: entry_receipt = ExecutionReceipt( timestamp=datetime.now(timezone.utc), status="NEW", symbol="BTC-USDT", side=side.value, action="ENTER", quantity=1.0, price=75_000.0, client_order_id="cid-entry", order_id="oid-entry", raw_ack={ "orderId": "oid-entry", "clientOrderId": "cid-entry", "status": "NEW", "symbol": "BTC-USDT", "executedQty": "0", }, raw_state={}, ) exit_ack_status = "NEW" if hung_exit or cancel_reject else "FILLED" exit_filled_qty = 0.0 if hung_exit or cancel_reject else 0.5 exit_receipt = ExecutionReceipt( timestamp=datetime.now(timezone.utc), status=exit_ack_status, symbol="BTC-USDT", side=("SELL" if side == TradeSide.SHORT else "BUY"), action="EXIT", quantity=0.5, price=74_900.0 if side == TradeSide.SHORT else 75_100.0, client_order_id="cid-exit-1", order_id="oid-exit-1", raw_ack={ "orderId": "oid-exit-1", "clientOrderId": "cid-exit-1", "status": exit_ack_status, "symbol": "BTC-USDT", "executedQty": f"{exit_filled_qty}", "cumFilledQty": f"{exit_filled_qty}", "avgPrice": "74900" if side == TradeSide.SHORT else "75100", }, raw_state={}, ) final_receipt = ExecutionReceipt( timestamp=datetime.now(timezone.utc), status="FILLED", symbol="BTC-USDT", side=("SELL" if side == TradeSide.SHORT else "BUY"), action="EXIT", quantity=0.5, price=74_850.0 if side == TradeSide.SHORT else 75_150.0, client_order_id="cid-exit-2", order_id="oid-exit-2", raw_ack={ "orderId": "oid-exit-2", "clientOrderId": "cid-exit-2", "status": "FILLED", "symbol": "BTC-USDT", "executedQty": "0.5", "cumFilledQty": "0.5", "avgPrice": "74850" if side == TradeSide.SHORT else "75150", }, raw_state={}, ) entry_before = _snapshot() entry_after = _snapshot( positions=[_position_row("BTC-USDT", side, 1.0, 75_000.0)], open_orders=[ { "symbol": "BTC-USDT", "clientOrderId": "cid-entry", "clientOrderID": "cid-entry", "orderId": "oid-entry", "status": "FILLED", "origQty": "1", "executedQty": "1", "avgPrice": "75000", } ], all_orders=[{"symbol": "BTC-USDT", "clientOrderId": "cid-entry", "clientOrderID": "cid-entry", "orderId": "oid-entry", "status": "FILLED"}], all_fills=[{"symbol": "BTC-USDT", "clientOrderId": "cid-entry", "clientOrderID": "cid-entry", "orderId": "oid-entry", "status": "FILLED", "executedQty": "1", "lastFilledQty": "1", "lastFillPrice": "75000"}], ) exit_before = entry_after cancel_open_positions = [_position_row("BTC-USDT", side, 1.0, 75_000.0)] if (hung_exit or cancel_reject) else [] cancel_open_orders = [ { "symbol": "BTC-USDT", "clientOrderId": "cid-exit-1", "clientOrderID": "cid-exit-1", "orderId": "oid-exit-1", "status": exit_ack_status, "origQty": "0.5", "executedQty": "0", "avgPrice": "74900", } ] if (hung_exit or cancel_reject) else [] exit_after = _snapshot( positions=cancel_open_positions, open_orders=cancel_open_orders, all_orders=[{"symbol": "BTC-USDT", "clientOrderId": "cid-exit-1", "clientOrderID": "cid-exit-1", "orderId": "oid-exit-1", "status": exit_ack_status}], all_fills=[{"symbol": "BTC-USDT", "clientOrderId": "cid-exit-1", "clientOrderID": "cid-exit-1", "orderId": "oid-exit-1", "status": exit_ack_status, "executedQty": f"{exit_filled_qty}", "lastFilledQty": f"{exit_filled_qty}", "lastFillPrice": "74900"}] if exit_filled_qty > 0 else [], ) cancel_after = _snapshot( positions=cancel_open_positions, open_orders=[], all_orders=[{"symbol": "BTC-USDT", "clientOrderId": "cid-exit-1", "clientOrderID": "cid-exit-1", "orderId": "oid-exit-1", "status": "CANCELED"}], all_fills=[], ) cancel_before = exit_after cancel_kind = "cancel_reject" if cancel_reject else "cancel_ack" final_before = cancel_after if (hung_exit or cancel_reject) else exit_after final_after = _snapshot( positions=[_position_row("BTC-USDT", side, 0.5, 74_900.0 if side == TradeSide.SHORT else 75_100.0)] if (hung_exit or cancel_reject) else [], open_orders=[], all_orders=[{"symbol": "BTC-USDT", "clientOrderId": "cid-exit-2", "clientOrderID": "cid-exit-2", "orderId": "oid-exit-2", "status": "FILLED"}], all_fills=[{"symbol": "BTC-USDT", "clientOrderId": "cid-exit-2", "clientOrderID": "cid-exit-2", "orderId": "oid-exit-2", "status": "FILLED", "executedQty": "0.5", "lastFilledQty": "0.5", "lastFillPrice": "74850" if side == TradeSide.SHORT else "75150"}] if (hung_exit or cancel_reject) else [], ) return [ BingxE2EStep("entry", "fill", 1.0, entry_before, entry_after, entry_receipt), BingxE2EStep( "exit_hang" if hung_exit else "exit_1", "fill" if not hung_exit else "ack_only", 0.5 if not hung_exit else 0.0, exit_before, exit_after, exit_receipt, submit_advances=not (hung_exit or cancel_reject), cancel_kind=cancel_kind, cancel_before_snapshot=cancel_before, cancel_after_snapshot=cancel_after, cancel_advances=True, ), BingxE2EStep("exit_2", "fill", 1.0, final_before, final_after, final_receipt), ] def _run_signal_plan(kernel: ExecutionKernel, side: TradeSide, plan: Sequence[SignalAction]) -> ExecutionKernel: trade_id = f"signal-{side.value.lower()}" for step in plan: if step.kind == "entry": kernel.process_intent(_intent(action=KernelCommandType.ENTER, trade_id=trade_id, side=side, target_size=1.0, price=75_000.0, reason=step.reason or "ENTRY")) elif step.kind == "mark": kernel.process_intent(_intent(action=KernelCommandType.MARK_PRICE, trade_id=trade_id, side=side, target_size=1.0, price=step.price, reason=step.reason or "MARK")) elif step.kind == "exit": kernel.process_intent(_intent(action=KernelCommandType.EXIT, trade_id=trade_id, side=side, target_size=step.target_size, price=step.price, exit_leg_ratios=(0.5, 0.5), reason=step.reason or "EXIT")) elif step.kind == "cancel": slot = kernel.slot(0) if step.require_close: active_order = slot.active_exit_order if active_order is None: fallback_client_id = f"{trade_id}:{step.reason or 'CANCEL'}:{slot.slot_id}" active_order = VenueOrder( internal_trade_id=slot.trade_id or trade_id, venue_order_id=str(slot.active_entry_order.venue_order_id if slot.active_entry_order else fallback_client_id), venue_client_id=str(slot.active_entry_order.venue_client_id if slot.active_entry_order else fallback_client_id), side=slot.side, intended_size=float(slot.active_exit_order.intended_size if slot.active_exit_order else max(slot.size, step.target_size or slot.size or 0.0)), filled_size=0.0, average_fill_price=float(step.price), status=VenueOrderStatus.NEW, metadata={"slot_id": slot.slot_id, "asset": slot.asset, "action": "EXIT"}, ) emitted = kernel.venue.cancel(active_order, reason=step.reason or "CANCEL") for event in emitted: kernel.on_venue_event(event) elif step.kind == "reconcile": kernel.process_intent(_intent(action=KernelCommandType.RECONCILE, trade_id=trade_id, side=side, target_size=1.0, price=step.price, reason=step.reason or "RECONCILE")) else: raise AssertionError(step.kind) return kernel MOCK_SIGNAL_CASES = [ ( "short_full_gamut", TradeSide.SHORT, [ VenueScriptStep("entry", "entry_full"), VenueScriptStep("exit_tp1", "exit_partial", fill_ratio=0.5), VenueScriptStep("exit_tp2", "exit_full", fill_ratio=1.0), ], [ SignalAction("entry", 75_000.0, reason="ENTRY"), SignalAction("mark", 74_200.0, reason="PUMP_BREAK"), SignalAction("exit", 74_900.0, target_size=0.5, reason="TP1"), SignalAction("mark", 74_100.0, reason="TRAIL"), SignalAction("exit", 74_800.0, target_size=0.5, reason="TP2"), ], ), ( "long_full_gamut", TradeSide.LONG, [ VenueScriptStep("entry", "entry_full"), VenueScriptStep("exit_tp1", "exit_partial", fill_ratio=0.5), VenueScriptStep("exit_tp2", "exit_full", fill_ratio=1.0), ], [ SignalAction("entry", 75_000.0, reason="ENTRY"), SignalAction("mark", 75_800.0, reason="RALLY"), SignalAction("exit", 75_100.0, target_size=0.5, reason="TP1"), SignalAction("mark", 75_900.0, reason="TRAIL"), SignalAction("exit", 75_200.0, target_size=0.5, reason="TP2"), ], ), ( "hung_exit_then_cancel", TradeSide.SHORT, [ VenueScriptStep("entry", "entry_full"), VenueScriptStep("hung_exit", "ack_only", submit_advances=False, cancel_kind="cancel_ack", cancel_advances=True), VenueScriptStep("exit_after_cancel", "exit_full", fill_ratio=1.0), ], [ SignalAction("entry", 75_000.0, reason="ENTRY"), SignalAction("mark", 74_300.0, reason="HANG"), SignalAction("exit", 74_950.0, target_size=0.5, reason="HUNG_TP"), SignalAction("cancel", 74_950.0, reason="CANCEL_HUNG", require_close=True), SignalAction("exit", 74_700.0, target_size=0.5, reason="RESUME_TP"), ], ), ( "cancel_reject_then_fill", TradeSide.SHORT, [ VenueScriptStep("entry", "entry_full"), VenueScriptStep("hung_exit", "ack_only", submit_advances=False, cancel_kind="cancel_reject", cancel_advances=False), VenueScriptStep("exit_after_reject", "exit_full", fill_ratio=1.0), ], [ SignalAction("entry", 75_000.0, reason="ENTRY"), SignalAction("mark", 74_250.0, reason="HANG"), SignalAction("exit", 74_950.0, target_size=0.5, reason="HUNG_TP"), SignalAction("cancel", 74_950.0, reason="CANCEL_REJECT", require_close=True), SignalAction("exit", 74_650.0, target_size=0.5, reason="FINAL_TP"), ], ), ] @pytest.mark.parametrize("name,side,steps,plan", MOCK_SIGNAL_CASES, ids=[case[0] for case in MOCK_SIGNAL_CASES]) def test_mock_signal_gamut_e2e_matrix(name: str, side: TradeSide, steps: Sequence[VenueScriptStep], plan: Sequence[SignalAction]) -> None: venue = ScriptedVenueAdapter(steps) kernel = _kernel(venue) kernel.update_control(ControlUpdate(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)) _run_signal_plan(kernel, side, plan) slot = kernel.slot(0) assert slot.trade_id == f"signal-{side.value.lower()}" assert venue.calls[0][0] == "submit" expected_cancel = any(step.kind == "cancel" and step.require_close for step in plan) assert any(call[0] == "cancel" for call in venue.calls) == expected_cancel assert kernel.snapshot()["control"]["mode"] == KernelMode.DEBUG.value if name in {"short_full_gamut", "long_full_gamut", "hung_exit_then_cancel", "cancel_reject_then_fill"}: assert slot.fsm_state in {TradeStage.CLOSED, TradeStage.POSITION_OPEN, TradeStage.EXIT_WORKING} if name == "hung_exit_then_cancel": assert any(call[0] == "cancel" for call in venue.calls) assert slot.closed is True or slot.fsm_state == TradeStage.POSITION_OPEN def _bingx_backend_for_plan(side: TradeSide, *, hung_exit: bool = False, cancel_reject: bool = False) -> BingxE2EBackend: return BingxE2EBackend(_bingx_steps_for_cycle(side, hung_exit=hung_exit, cancel_reject=cancel_reject)) @pytest.mark.parametrize( "side,hung_exit,cancel_reject", [ (TradeSide.SHORT, False, False), (TradeSide.LONG, False, False), (TradeSide.SHORT, True, False), (TradeSide.SHORT, True, True), ], ids=["short_full", "long_full", "short_hung", "short_cancel_reject"], ) def test_bingx_basic_e2e_matrix(side: TradeSide, hung_exit: bool, cancel_reject: bool) -> None: backend = _bingx_backend_for_plan(side, hung_exit=hung_exit, cancel_reject=cancel_reject) venue = BingxVenueAdapter(backend=backend) kernel = _kernel(venue) kernel.update_control(ControlUpdate(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE, backend_mode=BackendMode.BINGX)) _run_signal_plan( kernel, side, [ SignalAction("entry", 75_000.0, reason="ENTRY"), SignalAction("mark", 74_200.0 if side == TradeSide.SHORT else 75_800.0, reason="MARK"), SignalAction("exit", 74_900.0 if side == TradeSide.SHORT else 75_100.0, target_size=0.5, reason="TP1"), SignalAction("cancel", 74_900.0 if side == TradeSide.SHORT else 75_100.0, reason="CANCEL" if hung_exit or cancel_reject else "NO_CANCEL", require_close=hung_exit or cancel_reject), SignalAction("exit", 74_850.0 if side == TradeSide.SHORT else 75_150.0, target_size=0.5, reason="TP2"), ], ) slot = kernel.slot(0) assert backend.connected is False assert any(call[0] == "submit_intent" for call in backend.calls) assert slot.trade_id.startswith("signal-") assert slot.fsm_state in {TradeStage.CLOSED, TradeStage.POSITION_OPEN, TradeStage.EXIT_WORKING} if not hung_exit: assert slot.closed is True else: assert any(call[0] == "cancel_order" for call in backend.calls) FUZZ_SEEDS = tuple(range(12)) FUZZ_VENUES = ("mock", "bingx") @pytest.mark.parametrize("seed", FUZZ_SEEDS, ids=lambda seed: f"seed-{seed}") @pytest.mark.parametrize("venue_kind", FUZZ_VENUES, ids=lambda venue_kind: f"venue-{venue_kind}") def test_e2e_chaos_fuzz_matrix(seed: int, venue_kind: str) -> None: rng = random.Random(20260527 + seed) side = rng.choice([TradeSide.SHORT, TradeSide.LONG]) if venue_kind == "mock": steps = [ VenueScriptStep("entry", "entry_full" if rng.random() > 0.2 else "entry_partial", fill_ratio=1.0 if rng.random() > 0.5 else 0.5), VenueScriptStep("exit", "ack_only" if rng.random() > 0.35 else "exit_partial", fill_ratio=0.5, cancel_kind="cancel_reject" if rng.random() > 0.75 else "cancel_ack", submit_advances=False if rng.random() > 0.35 else True), VenueScriptStep("exit2", "exit_full", fill_ratio=1.0), ] venue = ScriptedVenueAdapter(steps) kernel = _kernel(venue) else: backend = _bingx_backend_for_plan(side, hung_exit=rng.random() > 0.4, cancel_reject=rng.random() > 0.7) venue = BingxVenueAdapter(backend=backend) kernel = _kernel(venue) kernel.update_control(ControlUpdate(backend_mode=BackendMode.BINGX)) trade_id = f"fuzz-{venue_kind}-{seed}" kernel.process_intent(_intent(action=KernelCommandType.ENTER, trade_id=trade_id, side=side, target_size=1.0, price=75_000.0, reason="ENTER")) for idx in range(rng.randint(2, 5)): op = rng.choice(["mark", "exit", "cancel", "reconcile"]) slot = kernel.slot(0) if op == "mark": kernel.process_intent(_intent(action=KernelCommandType.MARK_PRICE, trade_id=trade_id, side=side, target_size=1.0, price=74_000.0 if side == TradeSide.SHORT else 76_000.0, reason=f"MARK-{idx}")) elif op == "exit" and slot.fsm_state in {TradeStage.POSITION_OPEN, TradeStage.EXIT_WORKING}: kernel.process_intent(_intent(action=KernelCommandType.EXIT, trade_id=trade_id, side=side, target_size=max(0.1, slot.size or 0.5), price=74_900.0 if side == TradeSide.SHORT else 75_100.0, exit_leg_ratios=(0.5, 0.5), reason=f"EXIT-{idx}")) elif op == "cancel" and slot.active_exit_order is not None: kernel.process_intent(_intent(action=KernelCommandType.CANCEL, trade_id=trade_id, side=side, target_size=slot.active_exit_order.intended_size, price=74_900.0 if side == TradeSide.SHORT else 75_100.0, reason=f"CANCEL-{idx}")) elif op == "reconcile": kernel.process_intent(_intent(action=KernelCommandType.RECONCILE, trade_id=trade_id, side=side, target_size=1.0, price=75_000.0, reason=f"RECONCILE-{idx}")) final_slot = kernel.slot(0) assert final_slot.trade_id == trade_id assert final_slot.fsm_state in { TradeStage.ENTRY_WORKING, TradeStage.POSITION_OPEN, TradeStage.EXIT_WORKING, TradeStage.CLOSED, TradeStage.STALE_STATE_RECONCILING, } assert kernel.account.snapshot.equity == pytest.approx(kernel.account.snapshot.capital + kernel.account.snapshot.unrealized_pnl, abs=1e-9) assert kernel.snapshot()["control"]["runtime_namespace"] == "dita_v2" if final_slot.closed: assert final_slot.size == pytest.approx(0.0, abs=1e-9) else: assert final_slot.fsm_state in { TradeStage.ENTRY_WORKING, TradeStage.POSITION_OPEN, TradeStage.EXIT_WORKING, TradeStage.STALE_STATE_RECONCILING, }