Files
siloqy/prod/tests/test_dita_v2_e2e_functional.py
Codex 3d7b00e28d Snapshot PINK DITAv2 system + Sprint 0 flaw-fix verification
First commit of the previously-untracked PINK-on-DITAv2 migration system
(execution moves to the Rust kernel; policy stays on legacy DITA, so Alpha
Engine algorithmic integrity is preserved). BLUE is untouched.

Sprint 0 (safety snapshot + flaw-fix verification, MARKET single-leg scope):
- Verified Rust FSM fixes (flaws 2,4,10,11,13) by source read of lib.rs.
- Hardened 5 vacuous/guarded assertions in test_flaws.py so each flaw test
  genuinely exercises its fix. Most important: Flaw 5 now asserts capital
  moves by EXACTLY realized PnL (was entering/exiting at the same price).
- Offline suites: 533 passed, 0 failed (35 flaws + 402 kernel/accounting/
  bridge + 96 runtime/persistence/multi-exit/restart/seams).
- GATE PASS: MARKET-path-critical flaws 1,2,5 confirmed fixed + green.
- Added SPRINT0_FLAW_VERIFICATION.md report and _rust_kernel/.gitignore
  (excludes Rust target/ build artifacts).

LIMIT/partial-fill remain explicitly out of scope (MARKET-only bring-up).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 18:26:43 +02:00

908 lines
38 KiB
Python

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,
}