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>
This commit is contained in:
Codex
2026-05-30 18:26:43 +02:00
parent 34d01fe6a4
commit 3d7b00e28d
89 changed files with 32782 additions and 0 deletions

View File

@@ -0,0 +1,391 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any
import pytest
from prod.clean_arch.dita_v2 import (
BingxVenueAdapter,
ExecutionKernel,
InMemoryControlPlane,
KernelCommandType,
KernelControlSnapshot,
KernelIntent,
KernelMode,
KernelEventKind,
KernelVerbosity,
TradeSide,
TradeStage,
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",
) -> 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=False,
)
class FakeBingxBackend:
def __init__(
self,
*,
snapshots: list[ExchangeStateSnapshot],
receipt: ExecutionReceipt | None = None,
cancel_response: dict[str, Any] | None = None,
) -> None:
self.snapshots = snapshots
self.receipt = receipt
self.cancel_response = cancel_response or {"status": "CANCELED"}
self.calls: list[tuple[str, Any]] = []
self.submitted: list[Any] = []
self.canceled: list[tuple[Any, str]] = []
self._refresh_count = 0
self.connected = False
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))
index = min(self._refresh_count, len(self.snapshots) - 1)
snapshot = self.snapshots[index]
if self._refresh_count < len(self.snapshots) - 1:
self._refresh_count += 1
return snapshot
async def submit_intent(self, legacy_intent: Any) -> ExecutionReceipt:
self.calls.append(("submit_intent", legacy_intent.trade_id))
self.submitted.append(legacy_intent)
if self.receipt is None:
raise AssertionError("receipt must be configured")
return self.receipt
async def cancel_order(self, order: VenueOrder, *, reason: str = "") -> dict[str, Any]:
self.calls.append(("cancel_order", order.venue_order_id, reason))
self.canceled.append((order, reason))
return dict(self.cancel_response)
def _intent(
*,
action: KernelCommandType = KernelCommandType.ENTER,
trade_id: str = "trade-1",
slot_id: int = 0,
asset: str = "BTCUSDT",
side: TradeSide = TradeSide.SHORT,
target_size: float = 1.0,
leverage: float = 2.0,
reference_price: float = 75_000.0,
reason: str = "TEST",
) -> KernelIntent:
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:{action.value}",
trade_id=trade_id,
slot_id=slot_id,
asset=asset,
side=side,
action=action,
reference_price=reference_price,
target_size=target_size,
leverage=leverage,
reason=reason,
)
def test_submit_maps_bingx_ack_and_snapshot_fill_to_ditav2_events() -> None:
ack_row = {
"orderId": "1001",
"clientOrderId": "cid-1",
"clientOrderID": "cid-1",
"symbol": "BTC-USDT",
"status": "NEW",
"executedQty": "0",
"cumFilledQty": "0",
}
fill_row = {
"clientOrderId": "cid-1",
"clientOrderID": "cid-1",
"orderId": "1001",
"symbol": "BTC-USDT",
"status": "FILLED",
"executedQty": "1",
"lastFilledQty": "1",
"lastFillPrice": "75000",
}
backend = FakeBingxBackend(
snapshots=[
_snapshot(),
_snapshot(
positions=[
{
"symbol": "BTC-USDT",
"positionSide": "SHORT",
"positionAmt": "-1",
"avgPrice": "75000",
"markPrice": "75010",
"leverage": "2",
}
],
open_orders=[ack_row],
all_orders=[ack_row],
all_fills=[fill_row],
),
],
receipt=ExecutionReceipt(
timestamp=datetime.now(timezone.utc),
status="NEW",
symbol="BTC-USDT",
side="SELL",
action="ENTER",
quantity=1.0,
price=75_000.0,
client_order_id="cid-1",
order_id="1001",
raw_ack=ack_row,
raw_state={},
),
)
adapter = BingxVenueAdapter(backend=backend)
events = adapter.submit(_intent())
assert backend.connected is False
assert backend.submitted
assert [event.kind for event in events] == [event.kind for event in events if event.kind.value]
assert events[0].kind.value == "ORDER_ACK"
assert events[0].status == VenueEventStatus.ACKED
assert events[0].venue_client_id == "cid-1"
assert events[0].venue_order_id == "1001"
assert len(events) == 2
assert events[1].kind.value == "FULL_FILL"
assert events[1].status == VenueEventStatus.FILLED
assert events[1].filled_size == pytest.approx(1.0)
assert events[1].remaining_size == pytest.approx(0.0)
def test_cancel_uses_bingx_cancel_surface_and_maps_cancel_ack() -> None:
cancel_row = {
"orderId": "2001",
"clientOrderId": "cid-2",
"clientOrderID": "cid-2",
"symbol": "BTC-USDT",
"status": "CANCELED",
}
backend = FakeBingxBackend(
snapshots=[
_snapshot(
open_orders=[cancel_row],
all_orders=[cancel_row],
),
_snapshot(),
],
cancel_response=cancel_row,
)
adapter = BingxVenueAdapter(backend=backend)
order = VenueOrder(
internal_trade_id="trade-2",
venue_order_id="2001",
venue_client_id="cid-2",
side=TradeSide.SHORT,
intended_size=1.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": 0, "asset": "BTCUSDT"},
)
events = adapter.cancel(order, reason="MANUAL_CLOSE")
assert backend.canceled
assert events[0].kind.value == "CANCEL_ACK"
assert events[0].status == VenueEventStatus.CANCELED
assert events[0].venue_order_id == "2001"
assert events[0].reason == "MANUAL_CLOSE"
def test_reconcile_and_open_views_normalize_bingx_rows() -> None:
ack_row = {
"orderId": "3001",
"clientOrderId": "cid-3",
"clientOrderID": "cid-3",
"symbol": "ETH-USDT",
"status": "NEW",
"executedQty": "0",
}
fill_row = {
"clientOrderId": "cid-3",
"clientOrderID": "cid-3",
"orderId": "3001",
"symbol": "ETH-USDT",
"status": "PARTIALLY_FILLED",
"executedQty": "2",
"lastFilledQty": "1",
"lastFillPrice": "2500",
}
position_row = {
"symbol": "ETH-USDT",
"positionSide": "LONG",
"positionAmt": "2",
"avgPrice": "2500",
"markPrice": "2510",
"leverage": "3",
}
backend = FakeBingxBackend(
snapshots=[
_snapshot(
positions=[position_row],
open_orders=[ack_row],
all_orders=[ack_row, fill_row],
all_fills=[fill_row],
)
]
)
adapter = BingxVenueAdapter(backend=backend)
orders = adapter.open_orders()
positions = adapter.open_positions()
events = adapter.reconcile()
assert orders[0].status == VenueOrderStatus.NEW
assert orders[0].venue_client_id == "cid-3"
assert positions[0]["positionAmt"] == "2"
assert any(event.kind.value == "PARTIAL_FILL" for event in events)
assert any(event.kind.value == "ORDER_ACK" for event in events)
def test_kernel_can_drive_through_bingx_venue_shim() -> None:
ack_row = {
"orderId": "4001",
"clientOrderId": "cid-4",
"clientOrderID": "cid-4",
"symbol": "BTC-USDT",
"status": "NEW",
"executedQty": "0",
}
fill_row = {
"clientOrderId": "cid-4",
"clientOrderID": "cid-4",
"orderId": "4001",
"symbol": "BTC-USDT",
"status": "FILLED",
"executedQty": "1",
"lastFilledQty": "1",
"lastFillPrice": "75000",
}
backend = FakeBingxBackend(
snapshots=[
_snapshot(),
_snapshot(
positions=[
{
"symbol": "BTC-USDT",
"positionSide": "SHORT",
"positionAmt": "-1",
"avgPrice": "75000",
"markPrice": "75010",
"leverage": "2",
}
],
open_orders=[ack_row],
all_orders=[ack_row],
all_fills=[fill_row],
),
],
receipt=ExecutionReceipt(
timestamp=datetime.now(timezone.utc),
status="NEW",
symbol="BTC-USDT",
side="SELL",
action="ENTER",
quantity=1.0,
price=75_000.0,
client_order_id="cid-4",
order_id="4001",
raw_ack=ack_row,
raw_state={},
),
)
kernel = ExecutionKernel(
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
),
venue=BingxVenueAdapter(backend=backend),
)
outcome = kernel.process_intent(_intent(trade_id="trade-4"))
slot = kernel.slot(0)
assert outcome.accepted is True
assert slot.fsm_state == TradeStage.POSITION_OPEN
assert slot.trade_id == "trade-4"
assert backend.submitted
def test_submit_maps_bingx_rate_limit_to_first_class_venue_event() -> None:
backend = FakeBingxBackend(
snapshots=[_snapshot(), _snapshot()],
receipt=ExecutionReceipt(
timestamp=datetime.now(timezone.utc),
status="RATE_LIMITED",
symbol="BTC-USDT",
side="SELL",
action="ENTER",
quantity=1.0,
price=75_000.0,
client_order_id="cid-rate-limit",
order_id="",
raw_ack={
"status": "RATE_LIMITED",
"msg": "code:100410 endpoint is in disabled/frequency-limited period",
"retryAfter": int(datetime.now(timezone.utc).timestamp() * 1000) + 2_500,
},
raw_state={},
),
)
adapter = BingxVenueAdapter(backend=backend)
events = adapter.submit(_intent(trade_id="trade-rate-limit"))
assert len(events) == 1
assert events[0].kind == KernelEventKind.RATE_LIMITED
assert events[0].status == VenueEventStatus.RATE_LIMITED
assert events[0].venue_client_id == "cid-rate-limit"
assert events[0].metadata["retry_after_ms"] >= 0

View File

@@ -0,0 +1,94 @@
from __future__ import annotations
from uuid import uuid4
import os
import unittest
from prod.clean_arch.dita_v2 import (
BackendMode,
ControlUpdate,
InMemoryControlPlane,
ZincControlPlane,
KernelControlSnapshot,
KernelMode,
KernelVerbosity,
RealZincControlPlane,
build_control_plane,
)
from prod.clean_arch.dita_v2.real_control_plane import SharedRegion
HAS_REAL_ZINC = SharedRegion is not None
@unittest.skipUnless(HAS_REAL_ZINC, "Real Zinc adapter is unavailable")
class TestDITAv2RealControlPlane(unittest.TestCase):
def test_build_control_plane_defaults_to_zinc(self) -> None:
plane = build_control_plane()
self.assertIsInstance(plane, ZincControlPlane)
def test_roundtrip_update_and_read(self) -> None:
prefix = f"dita_v2_control_{uuid4().hex}"
writer = RealZincControlPlane(prefix=prefix, create=True)
reader = RealZincControlPlane(prefix=prefix, create=False)
try:
snapshot = writer.update(
ControlUpdate(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
backend_mode=BackendMode.BINGX,
trace_transitions=True,
mirror_to_hazelcast=True,
)
)
self.assertEqual(snapshot.mode, KernelMode.DEBUG)
self.assertEqual(snapshot.verbosity, KernelVerbosity.TRACE)
self.assertEqual(snapshot.backend_mode, BackendMode.BINGX)
self.assertTrue(snapshot.trace_transitions)
read_back = reader.read()
self.assertEqual(read_back.mode, KernelMode.DEBUG)
self.assertEqual(read_back.verbosity, KernelVerbosity.TRACE)
self.assertEqual(read_back.backend_mode, BackendMode.BINGX)
self.assertTrue(read_back.trace_transitions)
finally:
writer.close()
reader.close()
def test_env_can_select_real_control_plane(self) -> None:
prefix = f"dita_v2_control_{uuid4().hex}"
previous = os.environ.get("DITA_V2_CONTROL_PLANE")
os.environ["DITA_V2_CONTROL_PLANE"] = "REAL_ZINC"
try:
plane = build_control_plane(prefix=prefix)
self.assertIsInstance(plane, RealZincControlPlane)
if isinstance(plane, RealZincControlPlane):
plane.close()
finally:
if previous is None:
os.environ.pop("DITA_V2_CONTROL_PLANE", None)
else:
os.environ["DITA_V2_CONTROL_PLANE"] = previous
def test_initial_snapshot_is_default(self) -> None:
prefix = f"dita_v2_control_{uuid4().hex}"
plane = RealZincControlPlane(prefix=prefix, create=True)
try:
snapshot = plane.read()
self.assertEqual(snapshot, KernelControlSnapshot())
finally:
plane.close()
class TestDITAv2InMemoryControlPlane(unittest.TestCase):
def test_wait_and_notify(self) -> None:
plane = InMemoryControlPlane()
self.assertFalse(plane.wait(timeout_ms=1))
plane.notify()
self.assertTrue(plane.wait(timeout_ms=1))
snapshot = plane.update(ControlUpdate(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE))
self.assertEqual(snapshot.mode, KernelMode.DEBUG)
self.assertEqual(snapshot.verbosity, KernelVerbosity.TRACE)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,37 @@
from __future__ import annotations
from pathlib import Path
import unittest
class TestDITAv2Docs(unittest.TestCase):
def test_kernel_reference_exists(self) -> None:
text = Path("/mnt/dolphinng5_predict/prod/docs/DITA_V2_KERNEL_REFERENCE.md").read_text()
self.assertIn("# DITAv2 Kernel Reference", text)
self.assertIn("dolphin:dita_v2", text)
self.assertIn("prod/clean_arch/dita_v2/rust_backend.py", text)
self.assertIn("write-through", text)
self.assertIn("notify/wait", text)
self.assertIn("50 collected cases", text)
self.assertIn("full-stack E2E / functional tests", text)
self.assertIn("mocked exchange-first and BingX-basic E2E paths", text)
self.assertIn("KernelSeverity.WARNING", text)
self.assertIn("release_eta", text)
self.assertIn("retryable", text)
self.assertIn("dita_v2_live_bingx_smoke.py", text)
self.assertIn("--dry-run", text)
def test_system_bible_points_to_dita_v2_reference(self) -> None:
bible = Path("/mnt/dolphinng5_predict/prod/docs/SYSTEM_BIBLE_v7.md").read_text()
self.assertIn("DITA_V2_KERNEL_REFERENCE.md", bible)
self.assertIn("DITAv2 execution/launcher/operator surface", bible)
self.assertIn("write-through Zinc mirror semantics", bible)
self.assertIn("one-shot notify/wait signal contract", bible)
self.assertIn("full-stack DITAv2 E2E/functional matrix", bible)
self.assertIn("retryable transient throttling", bible)
self.assertIn("dita_v2_live_bingx_smoke.py", bible)
self.assertIn("--dry-run", bible)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,907 @@
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,
}

View File

@@ -0,0 +1,612 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import math
import pytest
from prod.clean_arch.dita_v2 import (
BackendMode,
ControlUpdate,
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelControlSnapshot,
KernelDiagnosticCode,
KernelEventKind,
KernelIntent,
KernelMode,
KernelVerbosity,
MemoryKernelJournal,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
class NoopVenueAdapter:
def submit(self, intent): # type: ignore[override]
return []
def cancel(self, order, *, reason: str = ""): # type: ignore[override]
return []
def open_orders(self): # type: ignore[override]
return []
def open_positions(self): # type: ignore[override]
return []
def reconcile(self): # type: ignore[override]
return []
@dataclass(frozen=True)
class IntentGuardCase:
name: str
slot_id: int
seed_state: str
action: KernelCommandType
trade_id: str
intent_trade_id: str
expected_state: TradeStage
expected_code: KernelDiagnosticCode
expected_accepted: bool
@dataclass(frozen=True)
class DuplicateCase:
name: str
seed_state: str
first_kind: KernelEventKind
second_kind: KernelEventKind
expected_state: TradeStage
event_factory_name: str
@dataclass(frozen=True)
class StaleCase:
name: str
second_kind: KernelEventKind
same_event_id_as_initial: bool
expected_accepted: bool
@dataclass(frozen=True)
class ZincMirrorCase:
name: str
op: str
@dataclass(frozen=True)
class SlotRigorCase:
name: str
op: str
def _build_kernel(slot_count: int = 3) -> tuple[ExecutionKernel, MemoryKernelJournal, InMemoryZincPlane]:
journal = MemoryKernelJournal()
zinc = InMemoryZincPlane()
kernel = ExecutionKernel(
max_slots=slot_count,
control_plane=InMemoryControlPlane(
KernelControlSnapshot(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
backend_mode=BackendMode.MOCK,
debug_clickhouse_enabled=True,
trace_transitions=True,
mirror_to_hazelcast=True,
)
),
venue=NoopVenueAdapter(),
journal=journal,
zinc_plane=zinc,
)
return kernel, journal, zinc
def _make_entry_order(trade_id: str, slot_id: int, *, size: float = 1.0, status: VenueOrderStatus = VenueOrderStatus.NEW) -> VenueOrder:
return VenueOrder(
internal_trade_id=trade_id,
venue_order_id=f"V-ENTRY-{slot_id}-{trade_id}",
venue_client_id=f"{trade_id}:entry:{slot_id}",
side=TradeSide.SHORT,
intended_size=size,
filled_size=size if status == VenueOrderStatus.FILLED else 0.0,
average_fill_price=100.0,
status=status,
metadata={"slot_id": slot_id},
)
def _make_exit_order(trade_id: str, slot_id: int, *, size: float, status: VenueOrderStatus = VenueOrderStatus.NEW) -> VenueOrder:
return VenueOrder(
internal_trade_id=trade_id,
venue_order_id=f"V-EXIT-{slot_id}-{trade_id}",
venue_client_id=f"{trade_id}:exit:{slot_id}",
side=TradeSide.SHORT,
intended_size=size,
filled_size=size if status == VenueOrderStatus.FILLED else 0.0,
average_fill_price=99.0,
status=status,
metadata={"slot_id": slot_id},
)
def _seed_free_slot(slot_id: int) -> TradeSlot:
return TradeSlot(slot_id=slot_id)
def _seed_entry_working_slot(trade_id: str, slot_id: int) -> TradeSlot:
return TradeSlot(
slot_id=slot_id,
trade_id=trade_id,
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=0.0,
size=0.0,
initial_size=0.0,
leverage=2.0,
entry_time=datetime.now(timezone.utc),
exit_leg_ratios=(1.0,),
active_leg_index=0,
active_entry_order=_make_entry_order(trade_id, slot_id, status=VenueOrderStatus.NEW),
fsm_state=TradeStage.ENTRY_WORKING,
)
def _seed_position_open_slot(trade_id: str, slot_id: int, *, size: float = 1.0, side: TradeSide = TradeSide.SHORT) -> TradeSlot:
return TradeSlot(
slot_id=slot_id,
trade_id=trade_id,
asset="BTCUSDT",
side=side,
entry_price=100.0,
size=size,
initial_size=size,
leverage=2.0,
entry_time=datetime.now(timezone.utc),
exit_leg_ratios=(0.5, 0.5),
active_leg_index=0,
active_entry_order=_make_entry_order(trade_id, slot_id, size=size, status=VenueOrderStatus.FILLED),
fsm_state=TradeStage.POSITION_OPEN,
)
def _seed_exit_working_slot(trade_id: str, slot_id: int, *, size: float = 1.0) -> TradeSlot:
slot = _seed_position_open_slot(trade_id, slot_id, size=size)
slot.active_exit_order = _make_exit_order(trade_id, slot_id, size=slot.next_exit_ratio() * size, status=VenueOrderStatus.NEW)
slot.fsm_state = TradeStage.EXIT_WORKING
return slot
def _seed_closed_slot(trade_id: str, slot_id: int) -> TradeSlot:
return TradeSlot(
slot_id=slot_id,
trade_id=trade_id,
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=0.0,
initial_size=1.0,
leverage=2.0,
entry_time=datetime.now(timezone.utc),
closed=True,
fsm_state=TradeStage.CLOSED,
)
def _make_intent(
*,
trade_id: str,
slot_id: int,
action: KernelCommandType,
leverage: float = 2.0,
size: float = 1.0,
side: TradeSide = TradeSide.SHORT,
reason: str = "HARNESS",
) -> KernelIntent:
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{trade_id}-{action.value}-{slot_id}",
trade_id=trade_id,
slot_id=slot_id,
asset="BTCUSDT",
side=side,
action=action,
reference_price=100.0,
target_size=size,
leverage=leverage,
exit_leg_ratios=(0.5, 0.5) if action == KernelCommandType.EXIT else (1.0,),
reason=reason,
)
def _make_event(
slot: TradeSlot,
*,
kind: KernelEventKind,
event_id: str,
filled_size: float = 0.0,
reason: str = "",
slot_id: int | None = None,
) -> VenueEvent:
order = slot.active_exit_order or slot.active_entry_order
venue_order_id = order.venue_order_id if order else f"V-{kind.value}-{slot.slot_id}"
venue_client_id = order.venue_client_id if order else f"{slot.trade_id}:client:{slot.slot_id}"
status = {
KernelEventKind.ORDER_ACK: VenueEventStatus.ACKED,
KernelEventKind.ORDER_REJECT: VenueEventStatus.REJECTED,
KernelEventKind.PARTIAL_FILL: VenueEventStatus.PARTIALLY_FILLED,
KernelEventKind.FULL_FILL: VenueEventStatus.FILLED,
KernelEventKind.CANCEL_ACK: VenueEventStatus.CANCELED,
KernelEventKind.CANCEL_REJECT: VenueEventStatus.CANCELED_REJECTED,
KernelEventKind.MARK_PRICE: VenueEventStatus.ACKED,
KernelEventKind.RECONCILE: VenueEventStatus.ACKED,
KernelEventKind.CONTROL: VenueEventStatus.ACKED,
}[kind]
return VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=event_id,
trade_id=slot.trade_id,
slot_id=slot.slot_id if slot_id is None else slot_id,
kind=kind,
status=status,
venue_order_id=venue_order_id,
venue_client_id=venue_client_id,
side=slot.side if slot.side != TradeSide.FLAT else TradeSide.SHORT,
asset=slot.asset or "BTCUSDT",
price=99.0 if kind == KernelEventKind.MARK_PRICE else 100.0,
size=max(slot.size, 1.0),
filled_size=filled_size,
remaining_size=max(0.0, max(slot.size, 1.0) - filled_size),
reason=reason,
)
INTENT_GUARD_CASES = [
IntentGuardCase("invalid_negative_enter", -1, "free", KernelCommandType.ENTER, "trade-neg", "trade-neg", TradeStage.IDLE, KernelDiagnosticCode.INVALID_SLOT_ID, False),
IntentGuardCase("invalid_high_exit", 99, "free", KernelCommandType.EXIT, "trade-high", "trade-high", TradeStage.IDLE, KernelDiagnosticCode.INVALID_SLOT_ID, False),
IntentGuardCase("unsupported_control", 0, "free", KernelCommandType.CONTROL, "trade-control", "trade-control", TradeStage.IDLE, KernelDiagnosticCode.UNSUPPORTED_INTENT, False),
IntentGuardCase("free_exit", 0, "free", KernelCommandType.EXIT, "trade-free-exit", "trade-free-exit", TradeStage.IDLE, KernelDiagnosticCode.NO_OPEN_POSITION, False),
IntentGuardCase("free_cancel", 0, "free", KernelCommandType.CANCEL, "trade-free-cancel", "trade-free-cancel", TradeStage.IDLE, KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER, False),
IntentGuardCase("busy_enter_different_trade", 0, "position_open", KernelCommandType.ENTER, "trade-open", "trade-new", TradeStage.POSITION_OPEN, KernelDiagnosticCode.SLOT_BUSY, False),
IntentGuardCase("same_trade_enter_allowed", 0, "position_open", KernelCommandType.ENTER, "trade-open", "trade-open", TradeStage.ORDER_REQUESTED, KernelDiagnosticCode.OK, True),
IntentGuardCase("closed_exit", 0, "closed", KernelCommandType.EXIT, "trade-closed", "trade-closed", TradeStage.CLOSED, KernelDiagnosticCode.NO_OPEN_POSITION, False),
IntentGuardCase("open_reconcile", 0, "position_open", KernelCommandType.RECONCILE, "trade-reconcile", "trade-reconcile", TradeStage.STALE_STATE_RECONCILING, KernelDiagnosticCode.STALE_STATE_RECONCILE, True),
IntentGuardCase("free_mark_price", 0, "free", KernelCommandType.MARK_PRICE, "trade-mark", "trade-mark", TradeStage.IDLE, KernelDiagnosticCode.OK, True),
]
DUPLICATE_CASES = [
DuplicateCase("entry_ack_duplicate", "entry_working", KernelEventKind.ORDER_ACK, KernelEventKind.ORDER_ACK, TradeStage.ENTRY_WORKING, "ack"),
DuplicateCase("entry_partial_duplicate", "entry_working", KernelEventKind.PARTIAL_FILL, KernelEventKind.PARTIAL_FILL, TradeStage.ENTRY_WORKING, "partial-entry"),
DuplicateCase("entry_full_duplicate", "entry_working", KernelEventKind.FULL_FILL, KernelEventKind.FULL_FILL, TradeStage.POSITION_OPEN, "full-entry"),
DuplicateCase("exit_ack_duplicate", "exit_working", KernelEventKind.CANCEL_ACK, KernelEventKind.CANCEL_ACK, TradeStage.POSITION_OPEN, "ack-exit"),
DuplicateCase("exit_partial_duplicate", "exit_working", KernelEventKind.PARTIAL_FILL, KernelEventKind.PARTIAL_FILL, TradeStage.EXIT_WORKING, "partial-exit"),
DuplicateCase("exit_full_duplicate", "exit_working", KernelEventKind.FULL_FILL, KernelEventKind.FULL_FILL, TradeStage.CLOSED, "full-exit"),
DuplicateCase("cancel_reject_duplicate", "exit_working", KernelEventKind.CANCEL_REJECT, KernelEventKind.CANCEL_REJECT, TradeStage.EXIT_WORKING, "reject-exit"),
DuplicateCase("mark_price_duplicate", "position_open", KernelEventKind.MARK_PRICE, KernelEventKind.MARK_PRICE, TradeStage.POSITION_OPEN, "mark"),
DuplicateCase("reconcile_duplicate", "position_open", KernelEventKind.RECONCILE, KernelEventKind.RECONCILE, TradeStage.STALE_STATE_RECONCILING, "reconcile"),
DuplicateCase("entry_reject_duplicate", "entry_working", KernelEventKind.ORDER_REJECT, KernelEventKind.ORDER_REJECT, TradeStage.IDLE, "reject-entry"),
]
STALE_CASES = [
StaleCase("stale_ack", KernelEventKind.ORDER_ACK, False, False),
StaleCase("stale_reject", KernelEventKind.ORDER_REJECT, False, False),
StaleCase("stale_partial", KernelEventKind.PARTIAL_FILL, False, False),
StaleCase("stale_full", KernelEventKind.FULL_FILL, False, False),
StaleCase("stale_cancel_ack", KernelEventKind.CANCEL_ACK, False, False),
StaleCase("stale_cancel_reject", KernelEventKind.CANCEL_REJECT, False, False),
StaleCase("stale_mark_price", KernelEventKind.MARK_PRICE, False, False),
StaleCase("stale_control", KernelEventKind.CONTROL, False, False),
StaleCase("stale_reconcile", KernelEventKind.RECONCILE, False, True),
StaleCase("stale_duplicate_precedence", KernelEventKind.ORDER_ACK, True, False),
]
ZINC_MIRROR_CASES = [
ZincMirrorCase("intent_published_on_enter", "intent"),
ZincMirrorCase("invalid_slot_intent_still_publishes", "invalid_intent"),
ZincMirrorCase("slot_write_updates_state_region", "direct_write"),
ZincMirrorCase("venue_event_updates_state_region", "venue_event"),
ZincMirrorCase("control_update_writes_region", "control_update"),
ZincMirrorCase("snapshot_reflects_control", "snapshot"),
ZincMirrorCase("reconcile_from_slots_writes_all", "reconcile"),
ZincMirrorCase("free_slot_selects_first_free", "free_slot"),
ZincMirrorCase("read_slots_sorted", "sorted_read"),
ZincMirrorCase("slot_overwrite_replaces_previous_state", "overwrite"),
]
SLOT_RIGOR_CASES = [
SlotRigorCase("idle_slot_is_free", "idle_free"),
SlotRigorCase("closed_slot_is_free", "closed_free"),
SlotRigorCase("entry_working_is_not_free", "entry_not_free"),
SlotRigorCase("open_slot_is_not_free", "open_not_free"),
SlotRigorCase("mark_price_zero_is_noop", "mark_zero"),
SlotRigorCase("mark_price_negative_is_noop", "mark_negative"),
SlotRigorCase("mark_price_nan_is_noop", "mark_nan"),
SlotRigorCase("short_price_rise_negative_pnl", "short_rise"),
SlotRigorCase("short_price_drop_positive_pnl", "short_drop"),
SlotRigorCase("exit_leg_consume_and_clamp", "exit_leg"),
]
def _seed_for_intent_case(kernel: ExecutionKernel, case: IntentGuardCase) -> None:
if case.seed_state == "free":
return
if case.seed_state == "entry_working":
kernel._set_slot(_seed_entry_working_slot(case.trade_id, case.slot_id))
return
if case.seed_state == "position_open":
kernel._set_slot(_seed_position_open_slot(case.trade_id, case.slot_id))
return
if case.seed_state == "exit_working":
kernel._set_slot(_seed_exit_working_slot(case.trade_id, case.slot_id))
return
if case.seed_state == "closed":
kernel._set_slot(_seed_closed_slot(case.trade_id, case.slot_id))
return
raise AssertionError(case.seed_state)
def _seed_for_duplicate_case(kernel: ExecutionKernel, case: DuplicateCase) -> TradeSlot:
if case.seed_state == "entry_working":
slot = _seed_entry_working_slot(f"trade-{case.name}", 0)
elif case.seed_state == "exit_working":
slot = _seed_exit_working_slot(f"trade-{case.name}", 0)
elif case.seed_state == "position_open":
slot = _seed_position_open_slot(f"trade-{case.name}", 0)
elif case.seed_state == "stale":
slot = _seed_position_open_slot(f"trade-{case.name}", 0)
else:
raise AssertionError(case.seed_state)
kernel._set_slot(slot)
return kernel._get_slot(0)
def _seed_for_stale_case(kernel: ExecutionKernel) -> TradeSlot:
slot = _seed_position_open_slot("trade-stale", 0)
kernel._set_slot(slot)
return kernel._get_slot(0)
def _seed_for_zinc_case(kernel: ExecutionKernel, case: ZincMirrorCase) -> None:
if case.op == "intent":
return
if case.op == "invalid_intent":
return
if case.op == "direct_write":
kernel._set_slot(_seed_position_open_slot("trade-write", 0))
return
if case.op == "venue_event":
kernel._set_slot(_seed_entry_working_slot("trade-event", 0))
return
if case.op == "control_update":
return
if case.op == "snapshot":
return
if case.op == "reconcile":
return
if case.op == "free_slot":
kernel._set_slot(_seed_position_open_slot("trade-free", 0))
kernel._set_slot(_seed_free_slot(1))
return
if case.op == "sorted_read":
return
if case.op == "overwrite":
return
raise AssertionError(case.op)
@pytest.mark.parametrize("case", INTENT_GUARD_CASES, ids=lambda case: case.name)
def test_kernel_intent_guard_matrix(case: IntentGuardCase) -> None:
kernel, _, zinc = _build_kernel()
_seed_for_intent_case(kernel, case)
intent = _make_intent(
trade_id=case.intent_trade_id,
slot_id=case.slot_id,
action=case.action,
leverage=-3.0 if case.name == "same_trade_enter_allowed" else 2.5,
size=1.0,
reason=case.name,
)
outcome = kernel.process_intent(intent)
assert outcome.accepted is case.expected_accepted
assert outcome.diagnostic_code == case.expected_code
assert outcome.state == case.expected_state
if case.slot_id >= 0 and case.slot_id < kernel.max_slots:
assert zinc.intent_region
assert zinc.intent_region[-1].intent_id == intent.intent_id
if case.name == "same_trade_enter_allowed":
current = kernel.slot(case.slot_id).to_dict()
assert current["fsm_state"] == TradeStage.ORDER_REQUESTED.value
assert current["leverage"] == 1.0
@pytest.mark.parametrize("case", DUPLICATE_CASES, ids=lambda case: case.name)
def test_kernel_duplicate_event_matrix(case: DuplicateCase) -> None:
kernel, _, _ = _build_kernel()
slot = _seed_for_duplicate_case(kernel, case)
fill_size = slot.size or 1.0
if case.seed_state == "exit_working" and case.first_kind == KernelEventKind.PARTIAL_FILL:
fill_size = max(0.1, fill_size * 0.4)
first_event = _make_event(slot, kind=case.first_kind, event_id=f"dup-{case.name}", filled_size=fill_size)
first = kernel.on_venue_event(first_event)
second = kernel.on_venue_event(first_event)
assert first.diagnostic_code in {
KernelDiagnosticCode.OK,
KernelDiagnosticCode.STALE_STATE_RECONCILE,
KernelDiagnosticCode.ENTRY_ORDER_REJECTED,
KernelDiagnosticCode.EXIT_ORDER_REJECTED,
KernelDiagnosticCode.ORDER_REJECTED,
KernelDiagnosticCode.CANCEL_REJECTED,
}
assert second.diagnostic_code == KernelDiagnosticCode.DUPLICATE_EVENT
assert second.state == case.expected_state
assert second.accepted is True
assert kernel.slot(0).to_dict()["seen_event_ids"].count(first_event.event_id) == 1
@pytest.mark.parametrize("case", STALE_CASES, ids=lambda case: case.name)
def test_kernel_stale_state_matrix(case: StaleCase) -> None:
kernel, _, _ = _build_kernel()
slot = _seed_for_stale_case(kernel)
initial = _make_event(slot, kind=KernelEventKind.RECONCILE, event_id="stale-entry", filled_size=slot.size or 1.0)
initial_outcome = kernel.on_venue_event(initial)
assert initial_outcome.diagnostic_code == KernelDiagnosticCode.OK
assert kernel.slot(0).fsm_state == TradeStage.STALE_STATE_RECONCILING
if case.same_event_id_as_initial:
event = _make_event(kernel._get_slot(0), kind=case.second_kind, event_id="stale-entry", filled_size=slot.size or 1.0, reason=case.name)
else:
event = _make_event(kernel._get_slot(0), kind=case.second_kind, event_id=f"stale-{case.name}", filled_size=slot.size or 1.0, reason=case.name)
outcome = kernel.on_venue_event(event)
if case.same_event_id_as_initial:
assert outcome.diagnostic_code == KernelDiagnosticCode.DUPLICATE_EVENT
assert outcome.accepted is True
else:
assert outcome.diagnostic_code == KernelDiagnosticCode.STALE_STATE_RECONCILE
assert outcome.accepted is case.expected_accepted
assert outcome.state == TradeStage.STALE_STATE_RECONCILING
assert kernel.slot(0).fsm_state == TradeStage.STALE_STATE_RECONCILING
@pytest.mark.parametrize("case", ZINC_MIRROR_CASES, ids=lambda case: case.name)
def test_kernel_zinc_mirror_matrix(case: ZincMirrorCase) -> None:
kernel, _, zinc = _build_kernel()
_seed_for_zinc_case(kernel, case)
if case.op == "intent":
intent = _make_intent(trade_id="trade-intent", slot_id=0, action=KernelCommandType.ENTER, size=1.25)
outcome = kernel.process_intent(intent)
assert outcome.accepted is True
assert zinc.intent_region
assert zinc.intent_region[-1].intent_id == intent.intent_id
assert zinc.read_slots()[0].trade_id == "trade-intent"
elif case.op == "invalid_intent":
intent = _make_intent(trade_id="trade-invalid", slot_id=-1, action=KernelCommandType.EXIT, size=1.0)
outcome = kernel.process_intent(intent)
assert outcome.diagnostic_code == KernelDiagnosticCode.INVALID_SLOT_ID
assert len(zinc.intent_region) == 1
assert zinc.intent_region[-1].intent_id == intent.intent_id
elif case.op == "direct_write":
slot = _seed_position_open_slot("trade-write", 0, size=1.5)
kernel._set_slot(slot)
mirrored = zinc.read_slots()[0]
assert mirrored.trade_id == "trade-write"
assert mirrored.size == 1.5
assert mirrored.fsm_state == TradeStage.POSITION_OPEN
elif case.op == "venue_event":
slot = kernel._get_slot(0)
event = _make_event(slot, kind=KernelEventKind.FULL_FILL, event_id="zinc-fill", filled_size=slot.size or 1.0)
outcome = kernel.on_venue_event(event)
assert outcome.diagnostic_code == KernelDiagnosticCode.OK
mirrored = zinc.read_slots()[0]
assert mirrored.fsm_state == TradeStage.POSITION_OPEN
assert mirrored.seen_event_ids == ("zinc-fill",)
elif case.op == "control_update":
snapshot = kernel.update_control(
ControlUpdate(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
trace_transitions=True,
mirror_to_hazelcast=False,
)
)
assert snapshot.mode == KernelMode.DEBUG
assert zinc.read_control().mode == KernelMode.DEBUG
assert zinc.read_control().trace_transitions is True
elif case.op == "snapshot":
kernel.update_control(ControlUpdate(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.VERBOSE))
payload = kernel.snapshot()
assert payload["control"]["mode"] == KernelMode.DEBUG.value
assert payload["control"]["verbosity"] == KernelVerbosity.VERBOSE.value
elif case.op == "reconcile":
slots = [
_seed_position_open_slot("trade-a", 2),
_seed_closed_slot("trade-b", 0),
_seed_free_slot(1),
]
outcome = kernel.reconcile_from_slots(slots)
assert outcome.diagnostic_code == KernelDiagnosticCode.RECONCILED
mirrored_ids = [slot.slot_id for slot in zinc.read_slots()]
assert mirrored_ids == [0, 1, 2]
elif case.op == "free_slot":
assert kernel.free_slot().slot_id == 1
elif case.op == "sorted_read":
kernel._set_slot(_seed_position_open_slot("trade-c", 2))
kernel._set_slot(_seed_position_open_slot("trade-a", 0))
kernel._set_slot(_seed_position_open_slot("trade-b", 1))
ids = [slot.slot_id for slot in zinc.read_slots()]
assert ids == [0, 1, 2]
elif case.op == "overwrite":
kernel._set_slot(_seed_position_open_slot("trade-old", 0, size=1.0))
kernel._set_slot(_seed_position_open_slot("trade-new", 0, size=2.0))
mirrored = zinc.read_slots()[0]
assert mirrored.trade_id == "trade-new"
assert mirrored.size == 2.0
assert mirrored.initial_size == 2.0
else: # pragma: no cover - exhaustive
raise AssertionError(case.op)
@pytest.mark.parametrize("case", SLOT_RIGOR_CASES, ids=lambda case: case.name)
def test_trade_slot_state_machine_rigor_matrix(case: SlotRigorCase) -> None:
if case.op == "idle_free":
slot = TradeSlot(slot_id=0)
assert slot.is_free() is True
assert slot.is_open() is False
elif case.op == "closed_free":
slot = _seed_closed_slot("trade-closed", 0)
assert slot.is_free() is True
assert slot.is_open() is False
elif case.op == "entry_not_free":
slot = _seed_entry_working_slot("trade-entry", 0)
assert slot.is_free() is False
assert slot.is_open() is True
elif case.op == "open_not_free":
slot = _seed_position_open_slot("trade-open", 0)
assert slot.is_free() is False
assert slot.is_open() is True
elif case.op == "mark_zero":
slot = _seed_position_open_slot("trade-mark", 0, size=1.0)
slot.mark_price(0.0)
assert slot.unrealized_pnl == 0.0
elif case.op == "mark_negative":
slot = _seed_position_open_slot("trade-mark", 0, size=1.0)
slot.mark_price(-10.0)
assert slot.unrealized_pnl == 0.0
elif case.op == "mark_nan":
slot = _seed_position_open_slot("trade-mark", 0, size=1.0)
slot.mark_price(float("nan"))
assert slot.unrealized_pnl == 0.0
elif case.op == "short_rise":
slot = _seed_position_open_slot("trade-short-rise", 0, size=1.0, side=TradeSide.SHORT)
slot.mark_price(110.0)
assert slot.unrealized_pnl < 0.0
elif case.op == "short_drop":
slot = _seed_position_open_slot("trade-short-drop", 0, size=1.0, side=TradeSide.SHORT)
slot.mark_price(90.0)
assert slot.unrealized_pnl > 0.0
elif case.op == "exit_leg":
slot = _seed_position_open_slot("trade-leg", 0, size=1.0)
slot.exit_leg_ratios = (0.25, 0.75)
first = slot.consume_exit_leg()
second = slot.consume_exit_leg()
third = slot.consume_exit_leg()
assert first == 0.25
assert second == 0.75
assert third == 1.0
assert slot.active_leg_index == 2
assert slot.next_exit_ratio() == 1.0
else: # pragma: no cover - exhaustive
raise AssertionError(case.op)

View File

@@ -0,0 +1,196 @@
from __future__ import annotations
from datetime import datetime, timezone
import unittest
from prod.clean_arch.dita_v2 import (
ControlUpdate,
ExecutionKernel,
HazelcastProjection,
KernelCommandType,
KernelControlSnapshot,
KernelIntent,
KernelMode,
KernelVerbosity,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
TradeStage,
TradeSlot,
build_projection,
build_position_state_row,
)
from prod.clean_arch.dita_v2.hazelcast_projection import HazelcastProjector, HazelcastRowWriter
class CaptureSink:
def __init__(self) -> None:
self.rows: list[tuple[str, dict[str, object]]] = []
def __call__(self, name: str, row: dict[str, object]) -> None:
self.rows.append((name, dict(row)))
class FakeMap:
def __init__(self) -> None:
self.rows: dict[str, object] = {}
def put(self, key: str, value: object) -> None:
self.rows[key] = value
class FakeTopic:
def __init__(self) -> None:
self.messages: list[str] = []
def publish(self, message: str) -> None:
self.messages.append(message)
class FakeHazelcastClient:
def __init__(self) -> None:
self.maps: dict[str, FakeMap] = {}
self.topics: dict[str, FakeTopic] = {}
def get_map(self, name: str) -> FakeMap:
return self.maps.setdefault(name, FakeMap())
def get_topic(self, name: str) -> FakeTopic:
return self.topics.setdefault(name, FakeTopic())
class TestDITAv2Hazelcast(unittest.TestCase):
def test_build_position_state_row_has_compatibility_fields(self) -> None:
slot = TradeSlot(
slot_id=0,
trade_id="trade-1",
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=1.0,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.POSITION_OPEN,
)
row = build_position_state_row(
slot,
KernelControlSnapshot(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
runtime_namespace="dita_v2",
strategy_namespace="dita_v2",
event_namespace="dita_v2",
actor_name="ExecutionKernel",
exec_venue="bingx",
data_venue="binance",
ledger_authority="exchange",
),
)
for key in (
"runtime_namespace",
"strategy_namespace",
"event_namespace",
"actor_name",
"exec_venue",
"data_venue",
"ledger_authority",
"trade_id",
"asset",
"slot_id",
"fsm_state",
):
self.assertIn(key, row)
self.assertEqual(row["trade_id"], "trade-1")
self.assertEqual(row["fsm_state"], TradeStage.POSITION_OPEN.value)
def test_projection_sink_writes_blue_pink_compatible_rows(self) -> None:
sink = CaptureSink()
projection = HazelcastProjection(writer=sink)
control = KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
projection.write_control(control)
slot = TradeSlot(
slot_id=1,
trade_id="trade-2",
asset="ETHUSDT",
side=TradeSide.LONG,
entry_price=50.0,
size=2.0,
initial_size=2.0,
leverage=3.0,
fsm_state=TradeStage.POSITION_OPEN,
)
projection.write_slot(slot)
self.assertGreaterEqual(len(sink.rows), 2)
control_name, control_row = sink.rows[0]
slot_name, slot_row = sink.rows[1]
self.assertEqual(control_name, "hz:dita_control")
self.assertEqual(slot_name, "hz:dita_active_slots")
self.assertEqual(control_row["mode"], KernelMode.DEBUG.value)
self.assertEqual(slot_row["trade_id"], "trade-2")
self.assertEqual(slot_row["runtime_namespace"], "dita_v2")
self.assertEqual(slot_row["ledger_authority"], "exchange")
def test_hazelcast_row_writer_routes_maps_and_topics(self) -> None:
client = FakeHazelcastClient()
writer = HazelcastRowWriter(client)
writer("hz:dita_active_slots", {"trade_id": "trade-3", "slot_id": 0})
writer("hz:dita_control", {"mode": "DEBUG"})
writer("hz:dita_trade_events", {"event_id": "evt-1", "trade_id": "trade-3"})
self.assertIn("trade-3", client.get_map("hz:dita_active_slots").rows)
self.assertIn("control", client.get_map("hz:dita_control").rows)
self.assertEqual(len(client.get_topic("hz:dita_trade_events").messages), 1)
def test_build_projection_uses_client_when_requested(self) -> None:
client = FakeHazelcastClient()
projection = build_projection(client=client, prefer_real_hazelcast=True)
projection.write_control(KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE))
projection.write_slot(
TradeSlot(
slot_id=0,
trade_id="trade-4",
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=1.0,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.POSITION_OPEN,
)
)
self.assertIn("control", client.get_map("hz:dita_control").rows)
self.assertIn("trade-4", client.get_map("hz:dita_active_slots").rows)
def test_kernel_emits_projection_rows(self) -> None:
sink = CaptureSink()
kernel = ExecutionKernel(
control_plane=None,
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)),
projection=HazelcastProjection(writer=sink),
)
kernel.update_control(ControlUpdate(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE))
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="intent-1",
trade_id="trade-1",
slot_id=0,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.ENTER,
reference_price=100.0,
target_size=1.0,
leverage=2.0,
exit_leg_ratios=(1.0,),
reason="TEST",
)
)
names = [name for name, _ in sink.rows]
self.assertIn("hz:dita_control", names)
self.assertIn("hz:dita_active_slots", names)
slot_rows = [row for name, row in sink.rows if name == "hz:dita_active_slots"]
self.assertTrue(any(row["trade_id"] == "trade-1" for row in slot_rows))
self.assertTrue(any(row["runtime_namespace"] == "dita_v2" for row in slot_rows))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,231 @@
from __future__ import annotations
from datetime import datetime, timezone
import unittest
from prod.clean_arch.dita_v2 import (
AccountProjection,
BackendMode,
ControlUpdate,
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelControlSnapshot,
KernelEventKind,
KernelIntent,
KernelMode,
KernelVerbosity,
MemoryKernelJournal,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
)
def mk_intent(
*,
action: KernelCommandType = KernelCommandType.ENTER,
slot_id: int = 0,
trade_id: str = "trade-1",
asset: str = "BTCUSDT",
side: TradeSide = TradeSide.SHORT,
target_size: float = 1.0,
leverage: float = 2.0,
reference_price: float = 100.0,
exit_leg_ratios=(1.0,),
reason: str = "TEST",
) -> KernelIntent:
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{trade_id}-{action.value}",
trade_id=trade_id,
slot_id=slot_id,
asset=asset,
side=side,
action=action,
reference_price=reference_price,
target_size=target_size,
leverage=leverage,
exit_leg_ratios=tuple(exit_leg_ratios),
reason=reason,
)
class TestDITAv2ControlPlane(unittest.TestCase):
def test_control_plane_updates_and_mirrors(self):
plane = InMemoryControlPlane()
updated = plane.update(
ControlUpdate(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
backend_mode=BackendMode.BINGX,
trace_transitions=True,
)
)
self.assertEqual(updated.mode, KernelMode.DEBUG)
self.assertEqual(updated.verbosity, KernelVerbosity.TRACE)
self.assertEqual(updated.backend_mode, BackendMode.BINGX)
self.assertTrue(updated.trace_transitions)
self.assertEqual(plane.mirror()["mode"], KernelMode.DEBUG.value)
class TestDITAv2Kernel(unittest.TestCase):
def test_entry_ack_fill_reaches_position_open(self):
journal = MemoryKernelJournal()
zinc = InMemoryZincPlane()
venue = MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0))
kernel = ExecutionKernel(
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
),
venue=venue,
journal=journal,
zinc_plane=zinc,
)
outcome = kernel.process_intent(mk_intent())
slot = kernel.slot(0)
self.assertTrue(outcome.accepted)
self.assertEqual(slot.fsm_state, TradeStage.POSITION_OPEN)
self.assertFalse(slot.closed)
self.assertEqual(slot.trade_id, "trade-1")
self.assertAlmostEqual(slot.size, 1.0, places=6)
self.assertEqual(len(journal.rows), 3)
self.assertEqual(len(zinc.intent_region), 1)
self.assertEqual(zinc.read_control().mode, KernelMode.DEBUG)
def test_partial_fill_stays_working_then_full_fill_opens_position(self):
journal = MemoryKernelJournal()
venue = MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=False, partial_fill_ratio=0.5))
kernel = ExecutionKernel(
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
),
venue=venue,
journal=journal,
)
kernel.process_intent(mk_intent())
slot = kernel.slot(0)
self.assertEqual(slot.fsm_state, TradeStage.ENTRY_WORKING)
self.assertAlmostEqual(slot.size, 0.5, places=6)
full_fill = VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id="evt-full",
trade_id="trade-1",
slot_id=0,
kind=KernelEventKind.FULL_FILL,
status=VenueEventStatus.FILLED,
venue_order_id=slot.active_entry_order.venue_order_id if slot.active_entry_order else "V-00000001",
venue_client_id=slot.active_entry_order.venue_client_id if slot.active_entry_order else "trade-1:intent-trade-1-ENTER",
side=TradeSide.SHORT,
asset="BTCUSDT",
price=100.0,
size=1.0,
filled_size=1.0,
remaining_size=0.0,
)
kernel.on_venue_event(full_fill)
self.assertEqual(slot.fsm_state, TradeStage.POSITION_OPEN)
self.assertFalse(slot.closed)
self.assertAlmostEqual(slot.size, 1.0, places=6)
def test_two_leg_exit_closes_only_after_final_leg(self):
kernel = ExecutionKernel(
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
),
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)),
journal=MemoryKernelJournal(),
)
kernel.process_intent(mk_intent())
slot = kernel.slot(0)
slot.exit_leg_ratios = (0.5, 0.5)
first_exit = kernel.process_intent(
mk_intent(action=KernelCommandType.EXIT, target_size=0.5, exit_leg_ratios=(0.5, 0.5), reason="TP1")
)
self.assertTrue(first_exit.accepted)
self.assertEqual(slot.fsm_state, TradeStage.POSITION_OPEN)
self.assertFalse(slot.closed)
self.assertAlmostEqual(slot.size, 0.5, places=6)
second_exit = kernel.process_intent(
mk_intent(action=KernelCommandType.EXIT, target_size=0.5, exit_leg_ratios=(0.5, 0.5), reason="TP2")
)
self.assertTrue(second_exit.accepted)
self.assertTrue(slot.closed)
self.assertEqual(slot.fsm_state, TradeStage.CLOSED)
self.assertAlmostEqual(slot.size, 0.0, places=6)
def test_reconcile_sets_stale_state(self):
kernel = ExecutionKernel(
control_plane=InMemoryControlPlane(),
venue=MockVenueAdapter(),
journal=MemoryKernelJournal(),
)
kernel.process_intent(mk_intent())
slot = kernel.slot(0)
kernel.process_intent(mk_intent(action=KernelCommandType.RECONCILE))
self.assertEqual(slot.fsm_state, TradeStage.STALE_STATE_RECONCILING)
def test_account_projection_aggregates_slots(self):
projection = AccountProjection()
slots = [
TradeSlot(
slot_id=0,
trade_id="t1",
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=1.0,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.POSITION_OPEN,
metadata={"mark_price": 99.0},
),
TradeSlot(
slot_id=1,
trade_id="t2",
asset="ETHUSDT",
side=TradeSide.LONG,
entry_price=50.0,
size=2.0,
initial_size=2.0,
leverage=3.0,
fsm_state=TradeStage.EXIT_WORKING,
metadata={"mark_price": 55.0},
),
]
projection.observe_slots(slots)
self.assertEqual(projection.snapshot.open_positions, 2)
self.assertAlmostEqual(projection.snapshot.open_notional, 209.0, places=6)
self.assertGreater(projection.snapshot.leverage, 0.0)
def test_debug_mode_journal_records_transitions(self):
journal = MemoryKernelJournal()
kernel = ExecutionKernel(
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
),
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)),
journal=journal,
)
kernel.process_intent(mk_intent())
self.assertGreaterEqual(len(journal.rows), 2)
self.assertTrue(all("slot_state" in row for row in journal.rows))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,579 @@
from __future__ import annotations
from datetime import datetime, timezone
import random
from typing import Any
import pytest
from prod.clean_arch.dita_v2 import (
AccountProjection,
BingxVenueAdapter,
BackendMode,
ControlUpdate,
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelControlSnapshot,
KernelDiagnosticCode,
KernelEventKind,
KernelIntent,
KernelMode,
KernelOutcome,
KernelSeverity,
KernelVerbosity,
MemoryKernelJournal,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
def mk_intent(
*,
action: KernelCommandType = KernelCommandType.ENTER,
slot_id: int = 0,
trade_id: str = "trade-1",
asset: str = "BTCUSDT",
side: TradeSide = TradeSide.SHORT,
target_size: float = 1.0,
leverage: float = 2.0,
reference_price: float = 100.0,
exit_leg_ratios=(1.0,),
reason: str = "TEST",
) -> KernelIntent:
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{trade_id}-{action.value}",
trade_id=trade_id,
slot_id=slot_id,
asset=asset,
side=side,
action=action,
reference_price=reference_price,
target_size=target_size,
leverage=leverage,
exit_leg_ratios=tuple(exit_leg_ratios),
reason=reason,
)
def mk_event(
*,
kind: KernelEventKind,
status: VenueEventStatus,
trade_id: str = "trade-1",
slot_id: int = 0,
venue_order_id: str = "V-00000001",
venue_client_id: str = "trade-1:intent-1",
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,
reason: str = "",
) -> VenueEvent:
return VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"evt-{kind.value.lower()}",
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,
raw_payload={"status": status.value},
)
def mk_kernel(
*,
max_slots: int = 3,
venue: Any | None = None,
control_mode: KernelMode = KernelMode.DEBUG,
verbosity: KernelVerbosity = KernelVerbosity.TRACE,
) -> ExecutionKernel:
return ExecutionKernel(
max_slots=max_slots,
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=control_mode, verbosity=verbosity, backend_mode=BackendMode.MOCK)
),
venue=venue or MockVenueAdapter(),
journal=MemoryKernelJournal(),
zinc_plane=InMemoryZincPlane(),
account=AccountProjection(),
)
def _seed_open_slot(slot: TradeSlot, *, trade_id: str = "trade-1", asset: str = "BTCUSDT") -> None:
slot.trade_id = trade_id
slot.asset = asset
slot.side = TradeSide.SHORT
slot.entry_price = 100.0
slot.size = 1.0
slot.initial_size = 1.0
slot.leverage = 2.0
slot.fsm_state = TradeStage.POSITION_OPEN
slot.active_entry_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-00000001",
venue_client_id=f"{trade_id}:entry",
side=TradeSide.SHORT,
intended_size=1.0,
status=VenueOrderStatus.FILLED,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
def _seed_entry_order(slot: TradeSlot, *, trade_id: str = "trade-1", asset: str = "BTCUSDT", status: VenueOrderStatus = VenueOrderStatus.NEW) -> None:
slot.active_entry_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-00000001",
venue_client_id=f"{trade_id}:entry",
side=TradeSide.SHORT,
intended_size=1.0,
status=status,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
def _seed_exit_order(slot: TradeSlot, *, trade_id: str = "trade-1", asset: str = "BTCUSDT", intended_size: float = 0.5) -> None:
slot.active_exit_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-00000002",
venue_client_id=f"{trade_id}:exit",
side=TradeSide.SHORT,
intended_size=intended_size,
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
def _configure_slot_state(slot: TradeSlot, state: TradeStage, *, trade_id: str = "trade-1", asset: str = "BTCUSDT") -> None:
slot.trade_id = trade_id if state not in {TradeStage.IDLE, TradeStage.CLOSED} else ""
slot.asset = asset if state not in {TradeStage.IDLE, TradeStage.CLOSED} else ""
slot.side = TradeSide.SHORT if state not in {TradeStage.IDLE, TradeStage.CLOSED} else TradeSide.FLAT
slot.entry_price = 100.0 if state not in {TradeStage.IDLE, TradeStage.CLOSED} else 0.0
slot.size = 1.0 if state in {TradeStage.POSITION_OPEN, TradeStage.EXIT_WORKING, TradeStage.EXIT_REQUESTED, TradeStage.EXIT_SENT, TradeStage.ENTRY_WORKING, TradeStage.ORDER_REQUESTED, TradeStage.ORDER_SENT} else 0.0
slot.initial_size = slot.size
slot.leverage = 2.0 if state not in {TradeStage.IDLE, TradeStage.CLOSED} else 0.0
slot.fsm_state = state
slot.closed = state == TradeStage.CLOSED
slot.active_entry_order = None
slot.active_exit_order = None
if state in {TradeStage.ORDER_REQUESTED, TradeStage.ORDER_SENT, TradeStage.ENTRY_WORKING, TradeStage.POSITION_OPEN, TradeStage.POSITION_OPENED}:
slot.active_entry_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-00000001",
venue_client_id=f"{trade_id}:entry",
side=TradeSide.SHORT,
intended_size=1.0,
status=VenueOrderStatus.NEW if state in {TradeStage.ORDER_REQUESTED, TradeStage.ORDER_SENT, TradeStage.ENTRY_WORKING} else VenueOrderStatus.FILLED,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
if state in {TradeStage.EXIT_REQUESTED, TradeStage.EXIT_SENT, TradeStage.EXIT_WORKING}:
slot.active_exit_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-00000002",
venue_client_id=f"{trade_id}:exit",
side=TradeSide.SHORT,
intended_size=0.5,
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
# 18 invalid-intent slot tests
@pytest.mark.parametrize(
"slot_id,action,expected",
[
(-1, KernelCommandType.ENTER, KernelDiagnosticCode.INVALID_SLOT_ID),
(-1, KernelCommandType.EXIT, KernelDiagnosticCode.INVALID_SLOT_ID),
(-1, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.INVALID_SLOT_ID),
(-1, KernelCommandType.RECONCILE, KernelDiagnosticCode.INVALID_SLOT_ID),
(-1, KernelCommandType.CANCEL, KernelDiagnosticCode.INVALID_SLOT_ID),
(3, KernelCommandType.ENTER, KernelDiagnosticCode.INVALID_SLOT_ID),
(3, KernelCommandType.EXIT, KernelDiagnosticCode.INVALID_SLOT_ID),
(3, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.INVALID_SLOT_ID),
(3, KernelCommandType.RECONCILE, KernelDiagnosticCode.INVALID_SLOT_ID),
(3, KernelCommandType.CANCEL, KernelDiagnosticCode.INVALID_SLOT_ID),
(99, KernelCommandType.ENTER, KernelDiagnosticCode.INVALID_SLOT_ID),
(99, KernelCommandType.EXIT, KernelDiagnosticCode.INVALID_SLOT_ID),
(99, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.INVALID_SLOT_ID),
(99, KernelCommandType.RECONCILE, KernelDiagnosticCode.INVALID_SLOT_ID),
(99, KernelCommandType.CANCEL, KernelDiagnosticCode.INVALID_SLOT_ID),
(7, KernelCommandType.ENTER, KernelDiagnosticCode.INVALID_SLOT_ID),
(7, KernelCommandType.EXIT, KernelDiagnosticCode.INVALID_SLOT_ID),
(7, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.INVALID_SLOT_ID),
],
)
def test_kernel_rejects_invalid_slot_ids_with_codes(slot_id: int, action: KernelCommandType, expected: KernelDiagnosticCode) -> None:
kernel = mk_kernel(max_slots=3)
outcome = kernel.process_intent(mk_intent(slot_id=slot_id, action=action))
assert outcome.accepted is False
assert outcome.diagnostic_code == expected
assert outcome.details["reason"] == "INVALID_SLOT_ID"
# 20 entry-path tests
@pytest.mark.parametrize(
"scenario,expected_state,expected_code,expected_size",
[
(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0), TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, 1.0),
(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=0.5), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.5),
(MockVenueScenario(emit_fill_on_submit=False, partial_fill_ratio=0.5), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.5),
(MockVenueScenario(reject_entries=True), TradeStage.IDLE, KernelDiagnosticCode.OK, 0.0),
(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=0.25), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.25),
(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=0.75), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.75),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=False, partial_fill_ratio=0.0), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.0),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=True, partial_fill_ratio=1.0), TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, 1.0),
(MockVenueScenario(emit_ack_before_fill=False, emit_fill_on_submit=True, partial_fill_ratio=1.0), TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, 1.0),
(MockVenueScenario(emit_ack_before_fill=False, emit_fill_on_submit=True, partial_fill_ratio=0.5), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.5),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=True, partial_fill_ratio=0.9), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.9),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=True, partial_fill_ratio=0.1), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.1),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=True, partial_fill_ratio=1.0, reject_entries=False), TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, 1.0),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=False, partial_fill_ratio=1.0), TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, 1.0),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=False, partial_fill_ratio=0.2), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.2),
(MockVenueScenario(emit_ack_before_fill=False, emit_fill_on_submit=False, partial_fill_ratio=0.3), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.3),
(MockVenueScenario(emit_ack_before_fill=False, emit_fill_on_submit=False, partial_fill_ratio=1.0), TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, 1.0),
(MockVenueScenario(emit_ack_before_fill=False, emit_fill_on_submit=False, partial_fill_ratio=0.0), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.0),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=True, partial_fill_ratio=0.6), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.6),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=True, partial_fill_ratio=0.4), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.4),
],
)
def test_kernel_entry_path_matrix(
scenario: MockVenueScenario,
expected_state: TradeStage,
expected_code: KernelDiagnosticCode,
expected_size: float,
) -> None:
kernel = mk_kernel(venue=MockVenueAdapter(scenario))
outcome = kernel.process_intent(mk_intent())
assert outcome.accepted is True
assert outcome.diagnostic_code == expected_code
assert kernel.slot(0).fsm_state == expected_state
assert kernel.slot(0).size == pytest.approx(expected_size, abs=1e-6)
# 20 exit-path tests
@pytest.mark.parametrize(
"initial_state,event_kind,event_status,expected_state,expected_code",
[
(TradeStage.POSITION_OPEN, KernelEventKind.PARTIAL_FILL, VenueEventStatus.PARTIALLY_FILLED, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
(TradeStage.POSITION_OPEN, KernelEventKind.FULL_FILL, VenueEventStatus.FILLED, TradeStage.CLOSED, KernelDiagnosticCode.OK),
(TradeStage.EXIT_REQUESTED, KernelEventKind.PARTIAL_FILL, VenueEventStatus.PARTIALLY_FILLED, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
(TradeStage.EXIT_REQUESTED, KernelEventKind.FULL_FILL, VenueEventStatus.FILLED, TradeStage.CLOSED, KernelDiagnosticCode.OK),
(TradeStage.EXIT_SENT, KernelEventKind.PARTIAL_FILL, VenueEventStatus.PARTIALLY_FILLED, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
(TradeStage.EXIT_SENT, KernelEventKind.FULL_FILL, VenueEventStatus.FILLED, TradeStage.CLOSED, KernelDiagnosticCode.OK),
(TradeStage.EXIT_WORKING, KernelEventKind.PARTIAL_FILL, VenueEventStatus.PARTIALLY_FILLED, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
(TradeStage.EXIT_WORKING, KernelEventKind.FULL_FILL, VenueEventStatus.FILLED, TradeStage.CLOSED, KernelDiagnosticCode.OK),
(TradeStage.EXIT_WORKING, KernelEventKind.CANCEL_ACK, VenueEventStatus.CANCELED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(TradeStage.EXIT_WORKING, KernelEventKind.CANCEL_REJECT, VenueEventStatus.CANCELED_REJECTED, TradeStage.EXIT_WORKING, KernelDiagnosticCode.CANCEL_REJECTED),
(TradeStage.POSITION_OPEN, KernelEventKind.CANCEL_ACK, VenueEventStatus.CANCELED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(TradeStage.POSITION_OPEN, KernelEventKind.CANCEL_REJECT, VenueEventStatus.CANCELED_REJECTED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.CANCEL_REJECTED),
(TradeStage.EXIT_REQUESTED, KernelEventKind.CANCEL_ACK, VenueEventStatus.CANCELED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(TradeStage.EXIT_SENT, KernelEventKind.CANCEL_ACK, VenueEventStatus.CANCELED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(TradeStage.EXIT_REQUESTED, KernelEventKind.CANCEL_REJECT, VenueEventStatus.CANCELED_REJECTED, TradeStage.EXIT_REQUESTED, KernelDiagnosticCode.CANCEL_REJECTED),
(TradeStage.EXIT_SENT, KernelEventKind.CANCEL_REJECT, VenueEventStatus.CANCELED_REJECTED, TradeStage.EXIT_SENT, KernelDiagnosticCode.CANCEL_REJECTED),
(TradeStage.POSITION_OPEN, KernelEventKind.ORDER_REJECT, VenueEventStatus.REJECTED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.EXIT_ORDER_REJECTED),
(TradeStage.EXIT_WORKING, KernelEventKind.ORDER_REJECT, VenueEventStatus.REJECTED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.EXIT_ORDER_REJECTED),
(TradeStage.EXIT_REQUESTED, KernelEventKind.ORDER_REJECT, VenueEventStatus.REJECTED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.EXIT_ORDER_REJECTED),
(TradeStage.EXIT_SENT, KernelEventKind.ORDER_REJECT, VenueEventStatus.REJECTED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.EXIT_ORDER_REJECTED),
],
)
def test_kernel_exit_path_matrix(
initial_state: TradeStage,
event_kind: KernelEventKind,
event_status: VenueEventStatus,
expected_state: TradeStage,
expected_code: KernelDiagnosticCode,
) -> None:
kernel = mk_kernel()
slot = kernel.slot(0)
_configure_slot_state(slot, initial_state)
if event_kind in {KernelEventKind.ORDER_REJECT, KernelEventKind.PARTIAL_FILL, KernelEventKind.FULL_FILL}:
_seed_exit_order(slot, trade_id=slot.trade_id or "trade-1", asset="BTCUSDT", intended_size=slot.size or 0.5)
if initial_state in {TradeStage.EXIT_REQUESTED, TradeStage.EXIT_SENT, TradeStage.EXIT_WORKING} and event_kind in {
KernelEventKind.CANCEL_ACK,
KernelEventKind.CANCEL_REJECT,
KernelEventKind.ORDER_ACK,
}:
_seed_exit_order(slot, trade_id=slot.trade_id or "trade-1", asset="BTCUSDT", intended_size=slot.size or 0.5)
outcome = kernel.on_venue_event(
mk_event(
kind=event_kind,
status=event_status,
trade_id=slot.trade_id or "trade-1",
venue_order_id=slot.active_exit_order.venue_order_id if slot.active_exit_order else "V-00000002",
venue_client_id=slot.active_exit_order.venue_client_id if slot.active_exit_order else "trade-1:exit",
side=TradeSide.SHORT,
asset="BTCUSDT",
size=float(slot.size or 0.5),
filled_size=float(slot.size or 0.5) if event_kind == KernelEventKind.FULL_FILL else float((slot.size or 0.5) / 2.0),
remaining_size=0.0,
)
)
assert outcome.diagnostic_code == expected_code
assert kernel.slot(0).fsm_state == expected_state
# 18 event-resolution tests
@pytest.mark.parametrize(
"event,initial_state,expected_state,expected_code",
[
(mk_event(kind=KernelEventKind.ORDER_ACK, status=VenueEventStatus.ACKED), TradeStage.IDLE, TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.ORDER_ACK, status=VenueEventStatus.ACKED), TradeStage.EXIT_REQUESTED, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.ORDER_REJECT, status=VenueEventStatus.REJECTED), TradeStage.ENTRY_WORKING, TradeStage.IDLE, KernelDiagnosticCode.ENTRY_ORDER_REJECTED),
(mk_event(kind=KernelEventKind.ORDER_REJECT, status=VenueEventStatus.REJECTED), TradeStage.EXIT_WORKING, TradeStage.POSITION_OPEN, KernelDiagnosticCode.EXIT_ORDER_REJECTED),
(mk_event(kind=KernelEventKind.ORDER_REJECT, status=VenueEventStatus.REJECTED), TradeStage.IDLE, TradeStage.IDLE, KernelDiagnosticCode.ORDER_REJECTED),
(mk_event(kind=KernelEventKind.PARTIAL_FILL, status=VenueEventStatus.PARTIALLY_FILLED), TradeStage.ENTRY_WORKING, TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.FULL_FILL, status=VenueEventStatus.FILLED), TradeStage.ENTRY_WORKING, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.PARTIAL_FILL, status=VenueEventStatus.PARTIALLY_FILLED), TradeStage.EXIT_WORKING, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.FULL_FILL, status=VenueEventStatus.FILLED), TradeStage.EXIT_WORKING, TradeStage.CLOSED, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.CANCEL_ACK, status=VenueEventStatus.CANCELED), TradeStage.EXIT_WORKING, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.CANCEL_REJECT, status=VenueEventStatus.CANCELED_REJECTED), TradeStage.EXIT_WORKING, TradeStage.EXIT_WORKING, KernelDiagnosticCode.CANCEL_REJECTED),
(mk_event(kind=KernelEventKind.MARK_PRICE, status=VenueEventStatus.ACKED), TradeStage.POSITION_OPEN, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.RECONCILE, status=VenueEventStatus.ACKED), TradeStage.POSITION_OPEN, TradeStage.STALE_STATE_RECONCILING, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.ORDER_ACK, status=VenueEventStatus.ACKED, venue_order_id="V-2"), TradeStage.POSITION_OPEN, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.ORDER_ACK, status=VenueEventStatus.ACKED, venue_order_id="V-3"), TradeStage.ENTRY_WORKING, TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.FULL_FILL, status=VenueEventStatus.FILLED, venue_order_id="V-4"), TradeStage.EXIT_WORKING, TradeStage.CLOSED, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.CANCEL_ACK, status=VenueEventStatus.CANCELED, venue_order_id="V-5"), TradeStage.POSITION_OPEN, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.CANCEL_REJECT, status=VenueEventStatus.CANCELED_REJECTED, venue_order_id="V-6"), TradeStage.POSITION_OPEN, TradeStage.POSITION_OPEN, KernelDiagnosticCode.CANCEL_REJECTED),
],
)
def test_kernel_event_matrix(event: VenueEvent, initial_state: TradeStage, expected_state: TradeStage, expected_code: KernelDiagnosticCode) -> None:
kernel = mk_kernel()
slot = kernel.slot(0)
_configure_slot_state(slot, initial_state)
entry_states = {TradeStage.IDLE, TradeStage.ORDER_REQUESTED, TradeStage.ORDER_SENT, TradeStage.ENTRY_WORKING}
exit_states = {TradeStage.POSITION_OPEN, TradeStage.EXIT_REQUESTED, TradeStage.EXIT_SENT, TradeStage.EXIT_WORKING}
if initial_state in entry_states and event.kind in {KernelEventKind.ORDER_ACK, KernelEventKind.PARTIAL_FILL, KernelEventKind.FULL_FILL}:
_seed_entry_order(slot, trade_id="trade-1", asset="BTCUSDT")
elif initial_state == TradeStage.ENTRY_WORKING and event.kind == KernelEventKind.ORDER_REJECT:
_seed_entry_order(slot, trade_id="trade-1", asset="BTCUSDT")
if initial_state in exit_states:
if event.kind == KernelEventKind.ORDER_REJECT:
_seed_exit_order(slot, trade_id="trade-1", asset="BTCUSDT", intended_size=1.0)
elif event.kind in {KernelEventKind.PARTIAL_FILL, KernelEventKind.FULL_FILL}:
_seed_exit_order(slot, trade_id="trade-1", asset="BTCUSDT", intended_size=1.0)
elif initial_state in {TradeStage.EXIT_REQUESTED, TradeStage.EXIT_SENT, TradeStage.EXIT_WORKING} and event.kind in {
KernelEventKind.ORDER_ACK,
KernelEventKind.CANCEL_ACK,
KernelEventKind.CANCEL_REJECT,
}:
_seed_exit_order(slot, trade_id="trade-1", asset="BTCUSDT", intended_size=1.0)
if initial_state == TradeStage.POSITION_OPEN and event.kind == KernelEventKind.ORDER_ACK:
slot.active_entry_order = None
fill_size = 1.0 if event.kind == KernelEventKind.FULL_FILL else 0.5 if event.kind == KernelEventKind.PARTIAL_FILL else 0.0
resolved_event = mk_event(
kind=event.kind,
status=event.status,
trade_id=event.trade_id,
slot_id=event.slot_id,
venue_order_id=slot.active_entry_order.venue_order_id if slot.active_entry_order else slot.active_exit_order.venue_order_id if slot.active_exit_order else event.venue_order_id,
venue_client_id=slot.active_entry_order.venue_client_id if slot.active_entry_order else slot.active_exit_order.venue_client_id if slot.active_exit_order else event.venue_client_id,
side=event.side,
asset=event.asset,
price=event.price,
size=1.0,
filled_size=fill_size,
remaining_size=max(0.0, 1.0 - fill_size),
reason=event.reason,
)
outcome = kernel.on_venue_event(resolved_event)
assert outcome.state == expected_state
assert outcome.diagnostic_code == expected_code
def test_kernel_rate_limited_event_is_characterized_without_state_drift() -> None:
kernel = mk_kernel()
slot = kernel.slot(0)
_configure_slot_state(slot, TradeStage.ENTRY_WORKING)
_seed_entry_order(slot, trade_id="trade-rate-limit", asset="BTCUSDT")
before = slot.to_dict()
outcome = kernel.on_venue_event(
mk_event(
kind=KernelEventKind.RATE_LIMITED,
status=VenueEventStatus.RATE_LIMITED,
trade_id="trade-rate-limit",
venue_order_id="V-RATE-LIMITED",
venue_client_id="trade-rate-limit:entry",
reason="code:100410 endpoint is in disabled/frequency-limited period",
size=1.0,
filled_size=0.0,
remaining_size=1.0,
)
)
after = kernel.slot(0).to_dict()
assert outcome.accepted is False
assert outcome.diagnostic_code == KernelDiagnosticCode.RATE_LIMITED
assert outcome.severity == KernelSeverity.WARNING
assert outcome.details["venue_event_kind"] == KernelEventKind.RATE_LIMITED.value
assert outcome.details["severity"] == KernelSeverity.WARNING.value
assert outcome.details["release_eta"] == "few minutes"
assert outcome.details["retryable"] is True
assert after["fsm_state"] == before["fsm_state"]
assert after["trade_id"] == before["trade_id"]
assert after["size"] == before["size"]
# 24 fuzz cases
@pytest.mark.parametrize("seed", list(range(24)))
def test_kernel_fuzz_event_sequences(seed: int) -> None:
rng = random.Random(seed)
kernel = mk_kernel(max_slots=4)
current_trade_id = f"trade-{seed}"
# Seed one slot open for exit/reconcile fuzzing.
seed_slot = kernel.slot(0)
_seed_open_slot(seed_slot, trade_id=current_trade_id)
seed_slot.exit_leg_ratios = (0.25, 0.25, 0.5)
kinds = [
KernelEventKind.ORDER_ACK,
KernelEventKind.ORDER_REJECT,
KernelEventKind.PARTIAL_FILL,
KernelEventKind.FULL_FILL,
KernelEventKind.CANCEL_ACK,
KernelEventKind.CANCEL_REJECT,
KernelEventKind.MARK_PRICE,
KernelEventKind.RECONCILE,
]
for idx in range(12):
kind = rng.choice(kinds)
if kind in {KernelEventKind.ORDER_ACK, KernelEventKind.ORDER_REJECT}:
seed_slot.active_entry_order = VenueOrder(
internal_trade_id=current_trade_id,
venue_order_id=f"V-{seed:04d}-{idx:02d}",
venue_client_id=f"{current_trade_id}:entry-{idx}",
side=TradeSide.SHORT,
intended_size=1.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": 0, "asset": "BTCUSDT"},
)
if kind in {KernelEventKind.CANCEL_ACK, KernelEventKind.CANCEL_REJECT, KernelEventKind.PARTIAL_FILL, KernelEventKind.FULL_FILL}:
seed_slot.active_exit_order = VenueOrder(
internal_trade_id=current_trade_id,
venue_order_id=f"V-{seed:04d}-{idx:02d}",
venue_client_id=f"{current_trade_id}:exit-{idx}",
side=TradeSide.SHORT,
intended_size=0.5,
filled_size=0.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": 0, "asset": "BTCUSDT"},
)
event = mk_event(kind=kind, status=_status_for_kind(kind), trade_id=current_trade_id, venue_order_id=f"V-{seed:04d}-{idx:02d}", venue_client_id=f"{current_trade_id}:{idx}")
outcome = kernel.on_venue_event(event)
assert isinstance(outcome, KernelOutcome)
assert outcome.diagnostic_code in set(KernelDiagnosticCode)
assert kernel.slot(0).fsm_state in set(TradeStage)
def _status_for_kind(kind: KernelEventKind) -> VenueEventStatus:
return {
KernelEventKind.ORDER_ACK: VenueEventStatus.ACKED,
KernelEventKind.ORDER_REJECT: VenueEventStatus.REJECTED,
KernelEventKind.PARTIAL_FILL: VenueEventStatus.PARTIALLY_FILLED,
KernelEventKind.FULL_FILL: VenueEventStatus.FILLED,
KernelEventKind.CANCEL_ACK: VenueEventStatus.CANCELED,
KernelEventKind.CANCEL_REJECT: VenueEventStatus.CANCELED_REJECTED,
KernelEventKind.MARK_PRICE: VenueEventStatus.ACKED,
KernelEventKind.RECONCILE: VenueEventStatus.ACKED,
}[kind]
# 22 explicit edge-condition tests
@pytest.mark.parametrize(
"slot_state,action,expected_code",
[
(TradeStage.IDLE, KernelCommandType.EXIT, KernelDiagnosticCode.NO_OPEN_POSITION),
(TradeStage.CLOSED, KernelCommandType.EXIT, KernelDiagnosticCode.NO_OPEN_POSITION),
(TradeStage.POSITION_OPEN, KernelCommandType.CANCEL, KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER),
(TradeStage.IDLE, KernelCommandType.CANCEL, KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER),
(TradeStage.IDLE, KernelCommandType.RECONCILE, KernelDiagnosticCode.STALE_STATE_RECONCILE),
(TradeStage.POSITION_OPEN, KernelCommandType.RECONCILE, KernelDiagnosticCode.STALE_STATE_RECONCILE),
(TradeStage.POSITION_OPEN, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.EXIT_WORKING, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.ENTRY_WORKING, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.ORDER_REQUESTED, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.ORDER_SENT, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.EXIT_REQUESTED, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.EXIT_SENT, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.STALE_STATE_RECONCILING, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.POSITION_OPEN, KernelCommandType.ENTER, KernelDiagnosticCode.SLOT_BUSY),
(TradeStage.EXIT_WORKING, KernelCommandType.ENTER, KernelDiagnosticCode.SLOT_BUSY),
(TradeStage.ORDER_REQUESTED, KernelCommandType.ENTER, KernelDiagnosticCode.SLOT_BUSY),
(TradeStage.ORDER_SENT, KernelCommandType.ENTER, KernelDiagnosticCode.SLOT_BUSY),
(TradeStage.POSITION_OPEN, KernelCommandType.EXIT, KernelDiagnosticCode.OK),
(TradeStage.EXIT_WORKING, KernelCommandType.EXIT, KernelDiagnosticCode.OK),
(TradeStage.POSITION_OPEN, KernelCommandType.CANCEL, KernelDiagnosticCode.OK),
(TradeStage.EXIT_WORKING, KernelCommandType.CANCEL, KernelDiagnosticCode.OK),
],
)
def test_kernel_action_edge_conditions(slot_state: TradeStage, action: KernelCommandType, expected_code: KernelDiagnosticCode) -> None:
kernel = mk_kernel(venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)))
slot = kernel.slot(0)
_configure_slot_state(slot, slot_state)
if action == KernelCommandType.ENTER and expected_code == KernelDiagnosticCode.SLOT_BUSY:
slot.trade_id = f"occupied-{slot_state.value.lower()}"
if action == KernelCommandType.CANCEL and expected_code == KernelDiagnosticCode.OK:
_seed_exit_order(slot, trade_id=slot.trade_id or "trade-1", asset=slot.asset or "BTCUSDT", intended_size=0.5)
outcome = kernel.process_intent(mk_intent(action=action, target_size=0.5, exit_leg_ratios=(0.25, 0.25, 0.5)))
assert outcome.diagnostic_code == expected_code
# 20 transition-detail tests
@pytest.mark.parametrize("mode", [KernelMode.NORMAL, KernelMode.DEBUG])
@pytest.mark.parametrize("verbosity", [KernelVerbosity.QUIET, KernelVerbosity.TRACE])
@pytest.mark.parametrize("control_enabled", [True, False])
@pytest.mark.parametrize("closed", [True, False])
@pytest.mark.parametrize("state", [TradeStage.IDLE, TradeStage.POSITION_OPEN])
def test_transition_details_and_control_modes_are_captured(
mode: KernelMode,
verbosity: KernelVerbosity,
control_enabled: bool,
closed: bool,
state: TradeStage,
) -> None:
kernel = mk_kernel()
if control_enabled:
kernel.update_control(
ControlUpdate(
mode=mode,
verbosity=verbosity,
trace_transitions=True,
)
)
slot = kernel.slot(0)
_seed_open_slot(slot)
slot.fsm_state = state
slot.closed = closed
event = mk_event(kind=KernelEventKind.MARK_PRICE, status=VenueEventStatus.ACKED)
outcome = kernel.on_venue_event(event)
assert outcome.transitions
transition = outcome.transitions[0]
assert transition.control_mode in {KernelMode.NORMAL.value, KernelMode.DEBUG.value}
assert transition.control_verbosity in {KernelVerbosity.QUIET.value, KernelVerbosity.TRACE.value}
assert "asset" in transition.details
assert "side" in transition.details

View File

@@ -0,0 +1,903 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import random
import pytest
from prod.clean_arch.dita_v2 import (
AccountProjection,
BackendMode,
ControlUpdate,
ExecutionKernel,
HazelcastProjection,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelControlSnapshot,
KernelDiagnosticCode,
KernelEventKind,
KernelIntent,
KernelMode,
KernelOutcome,
KernelVerbosity,
MemoryKernelJournal,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
@dataclass(frozen=True)
class KernelRig:
kernel: ExecutionKernel
journal: MemoryKernelJournal
zinc: InMemoryZincPlane
projection: HazelcastProjection
sink: "CaptureSink"
@dataclass(frozen=True)
class EntryCase:
name: str
scenario: MockVenueScenario
expected_state: TradeStage
expected_size: float
rejected: bool = False
@dataclass(frozen=True)
class ExitCase:
name: str
exit_leg_ratios: tuple[float, ...]
fill_ratio: float
expected_state: TradeStage
expected_size: float
expected_leg_index: int
@dataclass(frozen=True)
class EventCase:
name: str
kind: KernelEventKind
initial_state: TradeStage
expected_state: TradeStage
expected_code: KernelDiagnosticCode
family: str
@dataclass(frozen=True)
class ReconcileCase:
name: str
slots: tuple[TradeSlot, ...]
expected_open_positions: int
expected_trade_ids: tuple[str, ...]
class CaptureSink:
def __init__(self) -> None:
self.rows: list[tuple[str, dict[str, object]]] = []
def __call__(self, name: str, row: dict[str, object]) -> None:
self.rows.append((name, dict(row)))
def _build_kernel(
*,
venue: MockVenueAdapter | None = None,
mode: KernelMode = KernelMode.DEBUG,
verbosity: KernelVerbosity = KernelVerbosity.TRACE,
backend_mode: BackendMode = BackendMode.MOCK,
trace_transitions: bool = True,
) -> KernelRig:
sink = CaptureSink()
journal = MemoryKernelJournal()
zinc = InMemoryZincPlane()
projection = HazelcastProjection(writer=sink)
control_plane = InMemoryControlPlane(
KernelControlSnapshot(
mode=mode,
verbosity=verbosity,
backend_mode=backend_mode,
trace_transitions=trace_transitions,
debug_clickhouse_enabled=True,
mirror_to_hazelcast=True,
)
)
kernel = ExecutionKernel(
max_slots=4,
control_plane=control_plane,
venue=venue or MockVenueAdapter(),
journal=journal,
account=AccountProjection(),
projection=projection,
zinc_plane=zinc,
)
return KernelRig(kernel=kernel, journal=journal, zinc=zinc, projection=projection, sink=sink)
def _seed_entry_working(slot: TradeSlot, *, trade_id: str = "trade-1", asset: str = "BTCUSDT") -> None:
slot.trade_id = trade_id
slot.asset = asset
slot.side = TradeSide.SHORT
slot.entry_price = 100.0
slot.size = 1.0
slot.initial_size = 1.0
slot.leverage = 2.0
slot.closed = False
slot.exit_leg_ratios = (1.0,)
slot.active_leg_index = 0
slot.fsm_state = TradeStage.ENTRY_WORKING
slot.active_entry_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-ENTRY-1",
venue_client_id=f"{trade_id}:entry",
side=TradeSide.SHORT,
intended_size=1.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
slot.active_exit_order = None
def _seed_position_open(
slot: TradeSlot,
*,
trade_id: str = "trade-1",
asset: str = "BTCUSDT",
exit_leg_ratios: tuple[float, ...] = (1.0,),
) -> None:
_seed_entry_working(slot, trade_id=trade_id, asset=asset)
slot.active_entry_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-ENTRY-1",
venue_client_id=f"{trade_id}:entry",
side=TradeSide.SHORT,
intended_size=1.0,
filled_size=1.0,
average_fill_price=100.0,
status=VenueOrderStatus.FILLED,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
slot.fsm_state = TradeStage.POSITION_OPEN
slot.size = 1.0
slot.initial_size = 1.0
slot.exit_leg_ratios = tuple(exit_leg_ratios)
slot.active_leg_index = 0
def _seed_exit_working(
slot: TradeSlot,
*,
trade_id: str = "trade-1",
asset: str = "BTCUSDT",
exit_leg_ratios: tuple[float, ...] = (1.0,),
active_leg_index: int = 0,
) -> None:
_seed_position_open(slot, trade_id=trade_id, asset=asset, exit_leg_ratios=exit_leg_ratios)
slot.fsm_state = TradeStage.EXIT_WORKING
slot.active_leg_index = active_leg_index
slot.active_exit_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-EXIT-1",
venue_client_id=f"{trade_id}:exit",
side=TradeSide.SHORT,
intended_size=max(0.0, 1.0 * float(exit_leg_ratios[active_leg_index if active_leg_index < len(exit_leg_ratios) else 0])),
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
def _seed_idle(slot: TradeSlot) -> None:
slot.trade_id = ""
slot.asset = ""
slot.side = TradeSide.FLAT
slot.entry_price = 0.0
slot.size = 0.0
slot.initial_size = 0.0
slot.leverage = 0.0
slot.entry_time = None
slot.unrealized_pnl = 0.0
slot.realized_pnl = 0.0
slot.closed = False
slot.exit_leg_ratios = (1.0,)
slot.active_leg_index = 0
slot.active_exit_order = None
slot.active_entry_order = None
slot.fsm_state = TradeStage.IDLE
slot.close_reason = ""
slot.last_event_time = None
slot.metadata = {}
def _seed_closed(slot: TradeSlot, *, trade_id: str = "trade-1", asset: str = "BTCUSDT") -> None:
slot.trade_id = trade_id
slot.asset = asset
slot.side = TradeSide.SHORT
slot.entry_price = 100.0
slot.size = 0.0
slot.initial_size = 1.0
slot.leverage = 2.0
slot.closed = True
slot.exit_leg_ratios = (1.0,)
slot.active_leg_index = 1
slot.active_exit_order = None
slot.active_entry_order = None
slot.fsm_state = TradeStage.CLOSED
slot.close_reason = "EXIT_FILLED"
def _make_event(
*,
kind: KernelEventKind,
status: VenueEventStatus,
trade_id: str = "trade-1",
slot_id: int = 0,
venue_order_id: str = "V-ORDER-1",
venue_client_id: str = "trade-1:client-1",
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,
reason: str = "",
) -> VenueEvent:
return VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"evt-{kind.value.lower()}-{slot_id}-{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,
raw_payload={"status": status.value, "kind": kind.value},
)
ENTRY_CASES = [
EntryCase(
name="full_fill_immediate",
scenario=MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0),
expected_state=TradeStage.POSITION_OPEN,
expected_size=1.0,
),
EntryCase(
name="partial_50_immediate",
scenario=MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=0.5),
expected_state=TradeStage.ENTRY_WORKING,
expected_size=0.5,
),
EntryCase(
name="partial_50_ack_then_fill",
scenario=MockVenueScenario(emit_fill_on_submit=False, partial_fill_ratio=0.5),
expected_state=TradeStage.ENTRY_WORKING,
expected_size=0.5,
),
EntryCase(
name="no_fill_ack_only",
scenario=MockVenueScenario(emit_fill_on_submit=False, partial_fill_ratio=0.0),
expected_state=TradeStage.ENTRY_WORKING,
expected_size=0.0,
),
EntryCase(
name="ack_before_fill_full",
scenario=MockVenueScenario(emit_ack_before_fill=False, emit_fill_on_submit=True, partial_fill_ratio=1.0),
expected_state=TradeStage.POSITION_OPEN,
expected_size=1.0,
),
EntryCase(
name="ack_before_fill_partial",
scenario=MockVenueScenario(emit_ack_before_fill=False, emit_fill_on_submit=True, partial_fill_ratio=0.25),
expected_state=TradeStage.ENTRY_WORKING,
expected_size=0.25,
),
EntryCase(
name="reject_entry",
scenario=MockVenueScenario(reject_entries=True),
expected_state=TradeStage.IDLE,
expected_size=0.0,
rejected=True,
),
EntryCase(
name="three_quarters_fill",
scenario=MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=0.75),
expected_state=TradeStage.ENTRY_WORKING,
expected_size=0.75,
),
]
EXIT_CASES = [
ExitCase(name="single_leg_full", exit_leg_ratios=(1.0,), fill_ratio=1.0, expected_state=TradeStage.CLOSED, expected_size=0.0, expected_leg_index=1),
ExitCase(name="single_leg_partial", exit_leg_ratios=(1.0,), fill_ratio=0.5, expected_state=TradeStage.EXIT_WORKING, expected_size=0.5, expected_leg_index=0),
ExitCase(name="two_leg_full", exit_leg_ratios=(0.5, 0.5), fill_ratio=1.0, expected_state=TradeStage.POSITION_OPEN, expected_size=0.5, expected_leg_index=1),
ExitCase(name="two_leg_partial", exit_leg_ratios=(0.5, 0.5), fill_ratio=0.25, expected_state=TradeStage.EXIT_WORKING, expected_size=0.875, expected_leg_index=0),
ExitCase(name="three_leg_full", exit_leg_ratios=(0.25, 0.25, 0.5), fill_ratio=1.0, expected_state=TradeStage.POSITION_OPEN, expected_size=0.75, expected_leg_index=1),
ExitCase(name="three_leg_partial", exit_leg_ratios=(0.25, 0.25, 0.5), fill_ratio=0.5, expected_state=TradeStage.EXIT_WORKING, expected_size=0.875, expected_leg_index=0),
ExitCase(name="tilted_full", exit_leg_ratios=(0.2, 0.3, 0.5), fill_ratio=1.0, expected_state=TradeStage.POSITION_OPEN, expected_size=0.8, expected_leg_index=1),
ExitCase(name="tilted_partial", exit_leg_ratios=(0.2, 0.3, 0.5), fill_ratio=0.25, expected_state=TradeStage.EXIT_WORKING, expected_size=0.95, expected_leg_index=0),
ExitCase(name="four_leg_full", exit_leg_ratios=(0.1, 0.2, 0.3, 0.4), fill_ratio=1.0, expected_state=TradeStage.POSITION_OPEN, expected_size=0.9, expected_leg_index=1),
ExitCase(name="four_leg_partial", exit_leg_ratios=(0.1, 0.2, 0.3, 0.4), fill_ratio=0.5, expected_state=TradeStage.EXIT_WORKING, expected_size=0.95, expected_leg_index=0),
ExitCase(name="balanced_full", exit_leg_ratios=(0.33, 0.33, 0.34), fill_ratio=1.0, expected_state=TradeStage.POSITION_OPEN, expected_size=0.67, expected_leg_index=1),
ExitCase(name="balanced_partial", exit_leg_ratios=(0.33, 0.33, 0.34), fill_ratio=0.25, expected_state=TradeStage.EXIT_WORKING, expected_size=0.9175, expected_leg_index=0),
]
EVENT_CASES = [
EventCase("ack_entry", KernelEventKind.ORDER_ACK, TradeStage.ENTRY_WORKING, TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, "entry"),
EventCase("ack_exit", KernelEventKind.ORDER_ACK, TradeStage.EXIT_REQUESTED, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK, "exit"),
EventCase("reject_entry", KernelEventKind.ORDER_REJECT, TradeStage.ENTRY_WORKING, TradeStage.IDLE, KernelDiagnosticCode.ENTRY_ORDER_REJECTED, "entry"),
EventCase("reject_exit", KernelEventKind.ORDER_REJECT, TradeStage.EXIT_WORKING, TradeStage.POSITION_OPEN, KernelDiagnosticCode.EXIT_ORDER_REJECTED, "exit"),
EventCase("reject_idle", KernelEventKind.ORDER_REJECT, TradeStage.IDLE, TradeStage.IDLE, KernelDiagnosticCode.ORDER_REJECTED, "none"),
EventCase("partial_entry", KernelEventKind.PARTIAL_FILL, TradeStage.ENTRY_WORKING, TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, "entry"),
EventCase("full_entry", KernelEventKind.FULL_FILL, TradeStage.ENTRY_WORKING, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, "entry"),
EventCase("partial_exit", KernelEventKind.PARTIAL_FILL, TradeStage.EXIT_WORKING, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK, "exit"),
EventCase("full_exit", KernelEventKind.FULL_FILL, TradeStage.EXIT_WORKING, TradeStage.CLOSED, KernelDiagnosticCode.OK, "exit"),
EventCase("cancel_ack_exit", KernelEventKind.CANCEL_ACK, TradeStage.EXIT_WORKING, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, "exit"),
EventCase("cancel_reject_exit", KernelEventKind.CANCEL_REJECT, TradeStage.EXIT_WORKING, TradeStage.EXIT_WORKING, KernelDiagnosticCode.CANCEL_REJECTED, "exit"),
EventCase("mark_price_open", KernelEventKind.MARK_PRICE, TradeStage.POSITION_OPEN, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, "none"),
EventCase("reconcile_open", KernelEventKind.RECONCILE, TradeStage.POSITION_OPEN, TradeStage.STALE_STATE_RECONCILING, KernelDiagnosticCode.OK, "none"),
EventCase("ack_open_no_entry", KernelEventKind.ORDER_ACK, TradeStage.POSITION_OPEN, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, "none"),
EventCase("cancel_ack_open_no_exit", KernelEventKind.CANCEL_ACK, TradeStage.POSITION_OPEN, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, "none"),
]
RECONCILE_CASES = [
ReconcileCase(
name="empty_payload",
slots=(),
expected_open_positions=0,
expected_trade_ids=(),
),
ReconcileCase(
name="single_open",
slots=(
TradeSlot(
slot_id=0,
trade_id="trade-a",
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=1.0,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.POSITION_OPEN,
),
),
expected_open_positions=1,
expected_trade_ids=("trade-a",),
),
ReconcileCase(
name="open_and_exit",
slots=(
TradeSlot(
slot_id=0,
trade_id="trade-b",
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=0.5,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.EXIT_WORKING,
),
TradeSlot(
slot_id=1,
trade_id="trade-c",
asset="ETHUSDT",
side=TradeSide.LONG,
entry_price=50.0,
size=0.0,
initial_size=1.0,
leverage=3.0,
closed=True,
fsm_state=TradeStage.CLOSED,
),
),
expected_open_positions=1,
expected_trade_ids=("trade-b", "trade-c"),
),
ReconcileCase(
name="mixed_three",
slots=(
TradeSlot(
slot_id=0,
trade_id="trade-d",
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=1.0,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.POSITION_OPEN,
),
TradeSlot(
slot_id=1,
trade_id="trade-e",
asset="ETHUSDT",
side=TradeSide.LONG,
entry_price=50.0,
size=0.0,
initial_size=1.0,
leverage=3.0,
closed=True,
fsm_state=TradeStage.CLOSED,
),
TradeSlot(
slot_id=2,
trade_id="trade-f",
asset="SOLUSDT",
side=TradeSide.SHORT,
entry_price=20.0,
size=0.25,
initial_size=1.0,
leverage=4.0,
fsm_state=TradeStage.EXIT_WORKING,
),
),
expected_open_positions=2,
expected_trade_ids=("trade-d", "trade-e", "trade-f"),
),
]
def _event_order_id(case: EventCase, resolver: str) -> str:
if case.family == "entry":
return "V-ENTRY-1"
if case.family == "exit":
return "V-EXIT-1"
if resolver == "order_id":
return "V-MISSING"
return "V-ORDER-1"
def _event_client_id(case: EventCase, resolver: str) -> str:
if case.family == "entry":
return "trade-1:entry"
if case.family == "exit":
return "trade-1:exit"
if resolver == "order_id":
return "trade-x:missing"
return "trade-1:client-1"
@pytest.mark.parametrize("mode", [KernelMode.NORMAL, KernelMode.DEBUG])
@pytest.mark.parametrize("verbosity", [KernelVerbosity.QUIET, KernelVerbosity.VERBOSE, KernelVerbosity.TRACE])
@pytest.mark.parametrize("backend_mode", [BackendMode.MOCK, BackendMode.BINGX])
@pytest.mark.parametrize("trace_transitions", [True, False])
def test_kernel_control_plane_matrix(
mode: KernelMode,
verbosity: KernelVerbosity,
backend_mode: BackendMode,
trace_transitions: bool,
) -> None:
rig = _build_kernel()
snapshot = rig.kernel.update_control(
ControlUpdate(
mode=mode,
verbosity=verbosity,
backend_mode=backend_mode,
trace_transitions=trace_transitions,
)
)
assert snapshot.mode == mode
assert snapshot.verbosity == verbosity
assert snapshot.backend_mode == backend_mode
assert snapshot.trace_transitions == trace_transitions
assert rig.kernel.control.mode == mode
assert rig.kernel.control.verbosity == verbosity
assert rig.zinc.read_control().mode == mode
assert rig.zinc.read_control().verbosity == verbosity
assert rig.projection.control_snapshot is not None
assert rig.projection.control_snapshot.mode == mode
assert rig.sink.rows[-1][0] == "hz:dita_control"
assert rig.sink.rows[-1][1]["mode"] == mode.value
assert rig.sink.rows[-1][1]["backend_mode"] == backend_mode.value
@pytest.mark.parametrize("mode", [KernelMode.NORMAL, KernelMode.DEBUG])
@pytest.mark.parametrize("verbosity", [KernelVerbosity.QUIET, KernelVerbosity.TRACE])
@pytest.mark.parametrize("case", ENTRY_CASES, ids=[case.name for case in ENTRY_CASES])
def test_kernel_entry_matrix(
mode: KernelMode,
verbosity: KernelVerbosity,
case: EntryCase,
) -> None:
rig = _build_kernel(
venue=MockVenueAdapter(case.scenario),
mode=mode,
verbosity=verbosity,
backend_mode=BackendMode.MOCK,
)
outcome = rig.kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="intent-entry",
trade_id="trade-1",
slot_id=0,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.ENTER,
reference_price=100.0,
target_size=1.0,
leverage=2.0,
exit_leg_ratios=(1.0,),
reason=case.name,
)
)
slot = rig.kernel.slot(0)
assert outcome.accepted is True
assert outcome.diagnostic_code == KernelDiagnosticCode.OK
assert slot.fsm_state == case.expected_state
assert slot.size == pytest.approx(case.expected_size, abs=1e-6)
assert rig.zinc.intent_region[-1].action == KernelCommandType.ENTER
assert rig.zinc.state_region[0].fsm_state == case.expected_state
assert rig.journal.rows
if case.rejected:
assert slot.trade_id == ""
assert slot.asset == ""
assert slot.active_entry_order is None
assert slot.active_exit_order is None
if case.expected_state == TradeStage.POSITION_OPEN:
assert slot.closed is False
@pytest.mark.parametrize("case", EXIT_CASES, ids=[case.name for case in EXIT_CASES])
def test_kernel_exit_matrix(case: ExitCase) -> None:
rig = _build_kernel(venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=case.fill_ratio)))
slot = rig.kernel.slot(0)
_seed_position_open(slot, exit_leg_ratios=case.exit_leg_ratios)
slot.active_entry_order = None
outcome = rig.kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="intent-exit",
trade_id=slot.trade_id,
slot_id=0,
asset=slot.asset,
side=slot.side,
action=KernelCommandType.EXIT,
reference_price=99.0,
target_size=case.exit_leg_ratios[0],
leverage=slot.leverage,
exit_leg_ratios=case.exit_leg_ratios,
reason=case.name,
)
)
assert outcome.accepted is True
assert outcome.diagnostic_code == KernelDiagnosticCode.OK
assert slot.fsm_state == case.expected_state
assert slot.active_leg_index == case.expected_leg_index
assert slot.size == pytest.approx(case.expected_size, abs=1e-6)
if case.expected_state == TradeStage.CLOSED:
assert slot.closed is True
assert slot.active_exit_order is None
assert slot.active_entry_order is None
else:
assert slot.closed is False
assert slot.active_exit_order is None or slot.active_exit_order.status in {
VenueOrderStatus.PARTIALLY_FILLED,
VenueOrderStatus.NEW,
}
@pytest.mark.parametrize("resolver", ["slot_id", "trade_id", "order_id"])
@pytest.mark.parametrize("case", EVENT_CASES, ids=[case.name for case in EVENT_CASES])
def test_kernel_event_resolution_matrix(case: EventCase, resolver: str) -> None:
rig = _build_kernel()
slot = rig.kernel.slot(0)
if case.family == "entry":
_seed_entry_working(slot)
elif case.family == "exit":
_seed_exit_working(slot)
elif case.initial_state == TradeStage.POSITION_OPEN:
_seed_position_open(slot)
elif case.initial_state == TradeStage.IDLE:
_seed_idle(slot)
if resolver == "slot_id":
event_slot_id = 0
event_trade_id = "mismatch-trade"
event_venue_order_id = "V-MISMATCH"
elif resolver == "trade_id":
event_slot_id = 99
event_trade_id = slot.trade_id or "trade-1"
event_venue_order_id = "V-MISMATCH"
else:
event_slot_id = 99
event_trade_id = "mismatch-trade"
event_venue_order_id = "V-ENTRY-1" if case.family == "entry" else "V-EXIT-1"
if case.kind == KernelEventKind.ORDER_ACK and case.family == "none":
slot.active_entry_order = None
slot.active_exit_order = None
if case.kind == KernelEventKind.CANCEL_ACK and case.family == "none":
slot.active_exit_order = None
if case.kind == KernelEventKind.ORDER_REJECT and case.family == "exit":
slot.active_entry_order = None
if case.kind in {KernelEventKind.ORDER_ACK, KernelEventKind.ORDER_REJECT, KernelEventKind.PARTIAL_FILL, KernelEventKind.FULL_FILL} and case.family == "entry" and resolver != "order_id":
event_venue_order_id = slot.active_entry_order.venue_order_id
event_trade_id = slot.trade_id
if case.kind in {KernelEventKind.ORDER_ACK, KernelEventKind.ORDER_REJECT, KernelEventKind.PARTIAL_FILL, KernelEventKind.FULL_FILL, KernelEventKind.CANCEL_ACK, KernelEventKind.CANCEL_REJECT} and case.family == "exit" and resolver != "order_id":
event_venue_order_id = slot.active_exit_order.venue_order_id
event_trade_id = slot.trade_id
price = 98.0 if case.kind == KernelEventKind.MARK_PRICE else 100.0
filled_size = 1.0 if case.kind in {KernelEventKind.FULL_FILL, KernelEventKind.ORDER_ACK} else 0.5
remaining_size = max(0.0, 1.0 - filled_size)
event = _make_event(
kind=case.kind,
status={
KernelEventKind.ORDER_ACK: VenueEventStatus.ACKED,
KernelEventKind.ORDER_REJECT: VenueEventStatus.REJECTED,
KernelEventKind.PARTIAL_FILL: VenueEventStatus.PARTIALLY_FILLED,
KernelEventKind.FULL_FILL: VenueEventStatus.FILLED,
KernelEventKind.CANCEL_ACK: VenueEventStatus.CANCELED,
KernelEventKind.CANCEL_REJECT: VenueEventStatus.CANCELED_REJECTED,
KernelEventKind.MARK_PRICE: VenueEventStatus.ACKED,
KernelEventKind.RECONCILE: VenueEventStatus.ACKED,
}[case.kind],
trade_id=event_trade_id,
slot_id=event_slot_id,
venue_order_id=event_venue_order_id,
venue_client_id=_event_client_id(case, resolver),
side=TradeSide.SHORT,
asset="BTCUSDT",
price=price,
size=1.0,
filled_size=filled_size,
remaining_size=remaining_size,
)
outcome = rig.kernel.on_venue_event(event)
assert outcome.state == case.expected_state
assert outcome.diagnostic_code == case.expected_code
if case.kind == KernelEventKind.MARK_PRICE:
assert slot.unrealized_pnl > 0.0
if case.kind == KernelEventKind.ORDER_REJECT and case.family == "entry":
assert slot.trade_id == ""
assert slot.asset == ""
assert slot.size == 0.0
if case.kind == KernelEventKind.ORDER_REJECT and case.family == "exit":
assert slot.fsm_state == TradeStage.POSITION_OPEN
assert slot.active_exit_order is None
if case.kind == KernelEventKind.FULL_FILL and case.family == "entry":
assert slot.fsm_state == TradeStage.POSITION_OPEN
if case.kind == KernelEventKind.FULL_FILL and case.family == "exit" and case.expected_state == TradeStage.CLOSED:
assert slot.closed is True
@pytest.mark.parametrize("case", RECONCILE_CASES, ids=[case.name for case in RECONCILE_CASES])
@pytest.mark.parametrize("mode", [KernelMode.NORMAL, KernelMode.DEBUG])
@pytest.mark.parametrize("verbosity", [KernelVerbosity.QUIET, KernelVerbosity.TRACE])
def test_kernel_reconcile_snapshot_matrix(
case: ReconcileCase,
mode: KernelMode,
verbosity: KernelVerbosity,
) -> None:
rig = _build_kernel(mode=mode, verbosity=verbosity)
outcome = rig.kernel.reconcile_from_slots(case.slots)
assert outcome.accepted is True
assert outcome.diagnostic_code == KernelDiagnosticCode.RECONCILED
assert rig.kernel.snapshot()["account"]["open_positions"] == case.expected_open_positions
assert tuple(slot.trade_id for slot in rig.kernel.state.slots if slot.trade_id) == case.expected_trade_ids
assert len(rig.zinc.read_slots()) == len(rig.kernel.state.slots)
assert any(name == "hz:dita_active_slots" for name, _ in rig.sink.rows)
assert rig.projection.control_snapshot is not None
@pytest.mark.parametrize("seed", list(range(24)))
def test_kernel_fuzz_transition_matrix(seed: int) -> None:
rng = random.Random(seed)
rig = _build_kernel(
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=0.5)),
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
)
for step in range(20):
slot_id = rng.randrange(0, len(rig.kernel.state.slots))
slot = rig.kernel.slot(slot_id)
op = rng.choice(["enter", "exit", "cancel", "mark", "reconcile", "control", "event"])
if op == "enter":
trade_id = f"trade-{seed}-{step}"
outcome = rig.kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{seed}-{step}-enter",
trade_id=trade_id,
slot_id=slot_id,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.ENTER,
reference_price=100.0 + rng.random(),
target_size=1.0,
leverage=2.0,
exit_leg_ratios=(0.5, 0.5),
reason="fuzz-enter",
)
)
assert outcome.diagnostic_code in set(KernelDiagnosticCode)
elif op == "exit":
outcome = rig.kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{seed}-{step}-exit",
trade_id=slot.trade_id or f"trade-{seed}-{step}",
slot_id=slot_id,
asset=slot.asset or "BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.EXIT,
reference_price=99.0 + rng.random(),
target_size=max(0.1, slot.size or 0.1),
leverage=slot.leverage or 2.0,
exit_leg_ratios=slot.exit_leg_ratios or (1.0,),
reason="fuzz-exit",
)
)
assert outcome.diagnostic_code in {
KernelDiagnosticCode.OK,
KernelDiagnosticCode.NO_OPEN_POSITION,
}
elif op == "cancel":
outcome = rig.kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{seed}-{step}-cancel",
trade_id=slot.trade_id or f"trade-{seed}-{step}",
slot_id=slot_id,
asset=slot.asset or "BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.CANCEL,
reference_price=99.0,
target_size=max(0.1, slot.size or 0.1),
leverage=slot.leverage or 2.0,
exit_leg_ratios=slot.exit_leg_ratios or (1.0,),
reason="fuzz-cancel",
)
)
assert outcome.diagnostic_code in {
KernelDiagnosticCode.OK,
KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER,
}
elif op == "mark":
outcome = rig.kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{seed}-{step}-mark",
trade_id=slot.trade_id or f"trade-{seed}-{step}",
slot_id=slot_id,
asset=slot.asset or "BTCUSDT",
side=slot.side if slot.side != TradeSide.FLAT else TradeSide.SHORT,
action=KernelCommandType.MARK_PRICE,
reference_price=95.0 + rng.random() * 10.0,
target_size=max(0.1, slot.size or 0.1),
leverage=slot.leverage or 2.0,
exit_leg_ratios=slot.exit_leg_ratios or (1.0,),
reason="fuzz-mark",
)
)
assert outcome.diagnostic_code == KernelDiagnosticCode.OK
elif op == "reconcile":
outcome = rig.kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{seed}-{step}-reconcile",
trade_id=slot.trade_id or f"trade-{seed}-{step}",
slot_id=slot_id,
asset=slot.asset or "BTCUSDT",
side=slot.side if slot.side != TradeSide.FLAT else TradeSide.SHORT,
action=KernelCommandType.RECONCILE,
reference_price=100.0,
target_size=max(0.1, slot.size or 0.1),
leverage=slot.leverage or 2.0,
exit_leg_ratios=slot.exit_leg_ratios or (1.0,),
reason="fuzz-reconcile",
)
)
assert outcome.diagnostic_code == KernelDiagnosticCode.STALE_STATE_RECONCILE
elif op == "control":
rig.kernel.update_control(
ControlUpdate(
mode=KernelMode.DEBUG if rng.random() < 0.5 else KernelMode.NORMAL,
verbosity=rng.choice([KernelVerbosity.QUIET, KernelVerbosity.VERBOSE, KernelVerbosity.TRACE]),
backend_mode=rng.choice([BackendMode.MOCK, BackendMode.BINGX]),
trace_transitions=rng.random() < 0.5,
)
)
elif op == "event":
current = rig.kernel.slot(slot_id)
if current.active_exit_order is not None:
kind = rng.choice(
[
KernelEventKind.PARTIAL_FILL,
KernelEventKind.FULL_FILL,
KernelEventKind.CANCEL_ACK,
KernelEventKind.CANCEL_REJECT,
KernelEventKind.ORDER_REJECT,
]
)
elif current.active_entry_order is not None:
kind = rng.choice(
[
KernelEventKind.ORDER_ACK,
KernelEventKind.PARTIAL_FILL,
KernelEventKind.FULL_FILL,
KernelEventKind.ORDER_REJECT,
]
)
else:
kind = rng.choice(
[
KernelEventKind.ORDER_REJECT,
KernelEventKind.MARK_PRICE,
KernelEventKind.RECONCILE,
]
)
status = {
KernelEventKind.ORDER_ACK: VenueEventStatus.ACKED,
KernelEventKind.ORDER_REJECT: VenueEventStatus.REJECTED,
KernelEventKind.PARTIAL_FILL: VenueEventStatus.PARTIALLY_FILLED,
KernelEventKind.FULL_FILL: VenueEventStatus.FILLED,
KernelEventKind.CANCEL_ACK: VenueEventStatus.CANCELED,
KernelEventKind.CANCEL_REJECT: VenueEventStatus.CANCELED_REJECTED,
KernelEventKind.MARK_PRICE: VenueEventStatus.ACKED,
KernelEventKind.RECONCILE: VenueEventStatus.ACKED,
}[kind]
venue_order_id = "V-FUZZ"
venue_client_id = f"fuzz:{seed}:{step}"
if current.active_entry_order is not None:
venue_order_id = current.active_entry_order.venue_order_id
venue_client_id = current.active_entry_order.venue_client_id
elif current.active_exit_order is not None:
venue_order_id = current.active_exit_order.venue_order_id
venue_client_id = current.active_exit_order.venue_client_id
outcome = rig.kernel.on_venue_event(
_make_event(
kind=kind,
status=status,
trade_id=current.trade_id or f"trade-{seed}-{step}",
slot_id=slot_id if rng.random() < 0.5 else 99,
venue_order_id=venue_order_id,
venue_client_id=venue_client_id,
side=current.side if current.side != TradeSide.FLAT else TradeSide.SHORT,
asset=current.asset or "BTCUSDT",
price=98.0 if kind == KernelEventKind.MARK_PRICE else 100.0,
size=max(0.1, current.size or 0.1),
filled_size=max(0.1, current.size or 0.1),
remaining_size=0.0,
)
)
assert isinstance(outcome, KernelOutcome)
assert outcome.diagnostic_code in set(KernelDiagnosticCode)
assert slot.fsm_state in set(TradeStage)
assert slot.size >= 0.0
assert slot.initial_size >= 0.0
assert slot.active_leg_index >= 0
if slot.closed:
assert slot.size == pytest.approx(0.0, abs=1e-9)
if slot.fsm_state == TradeStage.IDLE:
assert slot.size == pytest.approx(0.0, abs=1e-9)

View File

@@ -0,0 +1,494 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import random
import pytest
from prod.clean_arch.dita_v2 import (
AccountProjection,
BackendMode,
ControlUpdate,
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelControlSnapshot,
KernelDiagnosticCode,
KernelEventKind,
KernelIntent,
KernelMode,
KernelVerbosity,
MemoryKernelJournal,
TradeSide,
TradeSlot,
TradeStage,
VenueAdapter,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
class NoopVenueAdapter:
"""Venue stub that never emits events."""
def submit(self, intent: KernelIntent): # type: ignore[override]
return []
def cancel(self, order: VenueOrder, *, reason: str = ""): # type: ignore[override]
return []
def open_orders(self): # type: ignore[override]
return []
def open_positions(self): # type: ignore[override]
return []
def reconcile(self): # type: ignore[override]
return []
@dataclass(frozen=True)
class RecoveryCase:
name: str
seed: int
slot_count: int
trade_count: int
@dataclass(frozen=True)
class DuplicateCase:
name: str
initial_state: TradeStage
kind: KernelEventKind
family: str
expected_state: TradeStage
expected_code: KernelDiagnosticCode
@dataclass(frozen=True)
class OutOfOrderCase:
name: str
seed: int
@dataclass(frozen=True)
class ReplayCase:
name: str
seed: int
control_mode: KernelMode
verbosity: KernelVerbosity
@dataclass(frozen=True)
class ControlCase:
name: str
mode: KernelMode
verbosity: KernelVerbosity
backend_mode: BackendMode
trace_transitions: bool
def _build_kernel(slot_count: int = 4) -> tuple[ExecutionKernel, MemoryKernelJournal, InMemoryZincPlane]:
journal = MemoryKernelJournal()
zinc = InMemoryZincPlane()
kernel = ExecutionKernel(
max_slots=slot_count,
control_plane=InMemoryControlPlane(
KernelControlSnapshot(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
backend_mode=BackendMode.MOCK,
debug_clickhouse_enabled=True,
trace_transitions=True,
mirror_to_hazelcast=True,
)
),
venue=NoopVenueAdapter(),
journal=journal,
account=AccountProjection(),
zinc_plane=zinc,
)
return kernel, journal, zinc
def _enter_open(kernel: ExecutionKernel, *, trade_id: str, slot_id: int = 0, size: float = 1.0) -> None:
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:enter",
trade_id=trade_id,
slot_id=slot_id,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.ENTER,
reference_price=100.0,
target_size=size,
leverage=2.0,
exit_leg_ratios=(1.0,),
reason="enter",
)
)
slot = kernel.slot(slot_id)
ack = 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=slot.active_entry_order.venue_order_id if slot.active_entry_order else "",
venue_client_id=slot.active_entry_order.venue_client_id if slot.active_entry_order else "",
side=TradeSide.SHORT,
asset="BTCUSDT",
price=100.0,
size=size,
filled_size=size,
remaining_size=0.0,
)
kernel.on_venue_event(ack)
def _seed_exit_working(kernel: ExecutionKernel, *, trade_id: str, slot_id: int = 0, exit_ratio: tuple[float, ...] = (1.0,)) -> None:
_enter_open(kernel, trade_id=trade_id, slot_id=slot_id, size=1.0)
slot = kernel.slot(slot_id)
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:exit",
trade_id=trade_id,
slot_id=slot_id,
asset=slot.asset,
side=slot.side,
action=KernelCommandType.EXIT,
reference_price=99.0,
target_size=exit_ratio[0],
leverage=slot.leverage,
exit_leg_ratios=exit_ratio,
reason="exit",
)
)
def _seed_exit_only_slot(
slot: TradeSlot,
*,
trade_id: str,
state: TradeStage,
asset: str = "BTCUSDT",
) -> None:
slot.trade_id = trade_id
slot.asset = asset
slot.side = TradeSide.SHORT
slot.entry_price = 100.0
slot.initial_size = 1.0
slot.size = 1.0 if state != TradeStage.CLOSED else 0.0
slot.leverage = 2.0
slot.closed = state == TradeStage.CLOSED
slot.exit_leg_ratios = (1.0,)
slot.active_leg_index = 0
slot.active_entry_order = None
slot.active_exit_order = None if state == TradeStage.CLOSED else VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-EXIT-1",
venue_client_id=f"{trade_id}:exit",
side=TradeSide.SHORT,
intended_size=1.0,
filled_size=0.0,
average_fill_price=0.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
slot.fsm_state = state
def _fill_event(slot: TradeSlot, *, kind: KernelEventKind, filled_size: float, trade_id: str | None = None) -> VenueEvent:
return VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"evt-{kind.value.lower()}-{slot.slot_id}",
trade_id=trade_id or slot.trade_id,
slot_id=slot.slot_id,
kind=kind,
status={
KernelEventKind.ORDER_ACK: VenueEventStatus.ACKED,
KernelEventKind.ORDER_REJECT: VenueEventStatus.REJECTED,
KernelEventKind.PARTIAL_FILL: VenueEventStatus.PARTIALLY_FILLED,
KernelEventKind.FULL_FILL: VenueEventStatus.FILLED,
KernelEventKind.CANCEL_ACK: VenueEventStatus.CANCELED,
KernelEventKind.CANCEL_REJECT: VenueEventStatus.CANCELED_REJECTED,
KernelEventKind.MARK_PRICE: VenueEventStatus.ACKED,
KernelEventKind.RECONCILE: VenueEventStatus.ACKED,
}[kind],
venue_order_id=slot.active_entry_order.venue_order_id if slot.active_entry_order else slot.active_exit_order.venue_order_id if slot.active_exit_order else "V-ORDER",
venue_client_id=slot.active_entry_order.venue_client_id if slot.active_entry_order else slot.active_exit_order.venue_client_id if slot.active_exit_order else "trade:client",
side=slot.side if slot.side != TradeSide.FLAT else TradeSide.SHORT,
asset=slot.asset or "BTCUSDT",
price=98.0 if kind == KernelEventKind.MARK_PRICE else 100.0,
size=max(1.0, slot.size or 1.0),
filled_size=filled_size,
remaining_size=max(0.0, max(1.0, slot.size or 1.0) - filled_size),
)
RECOVERY_CASES = [
RecoveryCase("idle_only", seed=1, slot_count=4, trade_count=1),
RecoveryCase("one_open", seed=2, slot_count=4, trade_count=1),
RecoveryCase("mixed_two", seed=3, slot_count=4, trade_count=2),
RecoveryCase("mixed_three", seed=4, slot_count=4, trade_count=3),
RecoveryCase("all_open", seed=5, slot_count=4, trade_count=4),
RecoveryCase("open_and_closed", seed=6, slot_count=5, trade_count=4),
RecoveryCase("exit_working", seed=7, slot_count=4, trade_count=2),
RecoveryCase("position_open_with_gap", seed=8, slot_count=4, trade_count=3),
]
DUPLICATE_CASES = [
DuplicateCase("ack_entry_duplicate_regressed", TradeStage.POSITION_OPEN, KernelEventKind.ORDER_ACK, "entry", TradeStage.POSITION_OPEN, KernelDiagnosticCode.DUPLICATE_EVENT),
DuplicateCase("ack_exit_duplicate_hold", TradeStage.POSITION_OPEN, KernelEventKind.ORDER_ACK, "exit", TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
DuplicateCase("partial_entry_duplicate_stays", TradeStage.ENTRY_WORKING, KernelEventKind.PARTIAL_FILL, "entry", TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK),
DuplicateCase("full_entry_duplicate_noop", TradeStage.POSITION_OPEN, KernelEventKind.FULL_FILL, "entry", TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
DuplicateCase("partial_exit_duplicate_stays", TradeStage.EXIT_WORKING, KernelEventKind.PARTIAL_FILL, "exit", TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
DuplicateCase("full_exit_duplicate_closes", TradeStage.CLOSED, KernelEventKind.FULL_FILL, "exit", TradeStage.CLOSED, KernelDiagnosticCode.OK),
DuplicateCase("cancel_ack_duplicate_open", TradeStage.POSITION_OPEN, KernelEventKind.CANCEL_ACK, "exit", TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
DuplicateCase("cancel_reject_duplicate_exit", TradeStage.EXIT_WORKING, KernelEventKind.CANCEL_REJECT, "exit", TradeStage.EXIT_WORKING, KernelDiagnosticCode.CANCEL_REJECTED),
]
OUT_OF_ORDER_CASES = [OutOfOrderCase(f"seed_{seed}", seed=seed) for seed in range(12)]
REPLAY_CASES = [
ReplayCase(f"replay_{seed}", seed=seed, control_mode=KernelMode.DEBUG if seed % 2 == 0 else KernelMode.NORMAL, verbosity=KernelVerbosity.TRACE if seed % 3 == 0 else KernelVerbosity.VERBOSE)
for seed in range(12)
]
CONTROL_CASES = [
ControlCase("normal_quiet_mock", KernelMode.NORMAL, KernelVerbosity.QUIET, BackendMode.MOCK, False),
ControlCase("normal_trace_mock", KernelMode.NORMAL, KernelVerbosity.TRACE, BackendMode.MOCK, True),
ControlCase("debug_trace_mock", KernelMode.DEBUG, KernelVerbosity.TRACE, BackendMode.MOCK, True),
ControlCase("debug_verbose_bingx", KernelMode.DEBUG, KernelVerbosity.VERBOSE, BackendMode.BINGX, False),
ControlCase("normal_verbose_bingx", KernelMode.NORMAL, KernelVerbosity.VERBOSE, BackendMode.BINGX, True),
ControlCase("debug_quiet_bingx", KernelMode.DEBUG, KernelVerbosity.QUIET, BackendMode.BINGX, False),
]
@pytest.mark.parametrize("case", CONTROL_CASES, ids=[case.name for case in CONTROL_CASES])
def test_kernel_zinc_control_plane_mirror(case: ControlCase) -> None:
kernel, journal, zinc = _build_kernel()
snapshot = kernel.update_control(
ControlUpdate(
mode=case.mode,
verbosity=case.verbosity,
backend_mode=case.backend_mode,
trace_transitions=case.trace_transitions,
)
)
assert snapshot.mode == case.mode
assert snapshot.verbosity == case.verbosity
assert snapshot.backend_mode == case.backend_mode
assert snapshot.trace_transitions == case.trace_transitions
assert zinc.read_control().mode == case.mode
assert zinc.read_control().verbosity == case.verbosity
assert journal.rows == []
assert kernel.zinc_plane.read_control().mode == case.mode
@pytest.mark.parametrize("case", RECOVERY_CASES, ids=[case.name for case in RECOVERY_CASES])
def test_kernel_zinc_restart_recovery_matrix(case: RecoveryCase) -> None:
kernel, _, zinc = _build_kernel(slot_count=case.slot_count)
rng = random.Random(case.seed)
for idx in range(case.trade_count):
slot_id = idx % case.slot_count
_enter_open(kernel, trade_id=f"{case.name}-{idx}", slot_id=slot_id, size=1.0)
if rng.random() < 0.5:
_seed_exit_working(kernel, trade_id=f"{case.name}-{idx}", slot_id=slot_id, exit_ratio=(0.5, 0.5))
if rng.random() < 0.5:
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{case.name}-{idx}:mark",
trade_id=f"{case.name}-{idx}",
slot_id=slot_id,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.MARK_PRICE,
reference_price=98.0,
target_size=1.0,
leverage=2.0,
exit_leg_ratios=(1.0,),
reason="mark",
)
)
snapshot_slots = zinc.read_slots()
assert len(snapshot_slots) == case.trade_count
restarted, _, restarted_zinc = _build_kernel(slot_count=case.slot_count)
outcome = restarted.reconcile_from_slots(snapshot_slots)
assert outcome.accepted is True
assert outcome.diagnostic_code == KernelDiagnosticCode.RECONCILED
assert restarted.snapshot()["slots"] == kernel.snapshot()["slots"]
assert [slot.to_dict() for slot in restarted_zinc.read_slots()] == restarted.snapshot()["slots"]
assert restarted.account.snapshot.open_positions == kernel.account.snapshot.open_positions
@pytest.mark.parametrize("case", DUPLICATE_CASES, ids=[case.name for case in DUPLICATE_CASES])
def test_kernel_duplicate_event_idempotence_matrix(case: DuplicateCase) -> None:
kernel, journal, zinc = _build_kernel()
slot = kernel.slot(0)
filled_size = 1.0 if case.kind == KernelEventKind.FULL_FILL else 0.25
if case.family == "entry":
_enter_open(kernel, trade_id="dup-entry", slot_id=0, size=1.0)
if case.kind == KernelEventKind.ORDER_ACK and case.initial_state == TradeStage.POSITION_OPEN:
slot.active_entry_order = VenueOrder(
internal_trade_id="dup-entry",
venue_order_id="V-ENTRY-1",
venue_client_id="dup-entry:entry",
side=TradeSide.SHORT,
intended_size=1.0,
filled_size=1.0,
average_fill_price=100.0,
status=VenueOrderStatus.FILLED,
metadata={"slot_id": 0, "asset": "BTCUSDT"},
)
slot.fsm_state = TradeStage.POSITION_OPEN
else:
_seed_exit_only_slot(slot, trade_id="dup-exit", state=case.initial_state)
before = slot.to_dict()
event = _fill_event(
slot,
kind=case.kind,
filled_size=filled_size,
trade_id=slot.trade_id,
)
outcome_1 = kernel.on_venue_event(event)
state_after_first = slot.fsm_state
size_after_first = slot.size
outcome_2 = kernel.on_venue_event(event)
assert outcome_1.diagnostic_code in set(KernelDiagnosticCode)
assert outcome_2.diagnostic_code in set(KernelDiagnosticCode)
assert slot.fsm_state == case.expected_state
assert slot.fsm_state == state_after_first
assert slot.size == pytest.approx(size_after_first, abs=1e-9)
assert slot.size >= 0.0
assert zinc.state_region[0].fsm_state == slot.fsm_state
assert len(journal.rows) >= 1
assert before["slot_id"] == slot.slot_id
if case.expected_code == KernelDiagnosticCode.DUPLICATE_EVENT:
assert outcome_2.diagnostic_code == KernelDiagnosticCode.DUPLICATE_EVENT
@pytest.mark.parametrize("case", OUT_OF_ORDER_CASES, ids=[case.name for case in OUT_OF_ORDER_CASES])
def test_kernel_out_of_order_venue_event_matrix(case: OutOfOrderCase) -> None:
kernel, journal, zinc = _build_kernel()
rng = random.Random(case.seed)
if rng.random() < 0.5:
_enter_open(kernel, trade_id=f"ooo-{case.seed}", slot_id=0, size=1.0)
else:
_seed_exit_working(kernel, trade_id=f"ooo-{case.seed}", slot_id=0, exit_ratio=(0.5, 0.5))
slot = kernel.slot(0)
sequence = [
KernelEventKind.FULL_FILL,
KernelEventKind.ORDER_ACK,
KernelEventKind.PARTIAL_FILL,
KernelEventKind.CANCEL_ACK,
KernelEventKind.CANCEL_REJECT,
KernelEventKind.ORDER_REJECT,
KernelEventKind.MARK_PRICE,
]
rng.shuffle(sequence)
for idx, kind in enumerate(sequence):
event = _fill_event(
slot,
kind=kind,
filled_size=1.0 if kind == KernelEventKind.FULL_FILL else 0.5,
trade_id=slot.trade_id,
)
event = VenueEvent(
**{
**event.__dict__,
"event_id": f"ooo-{case.seed}-{idx}",
"slot_id": 0 if rng.random() < 0.5 else 99,
"venue_order_id": slot.active_entry_order.venue_order_id if slot.active_entry_order else slot.active_exit_order.venue_order_id if slot.active_exit_order else event.venue_order_id,
"venue_client_id": slot.active_entry_order.venue_client_id if slot.active_entry_order else slot.active_exit_order.venue_client_id if slot.active_exit_order else event.venue_client_id,
}
)
outcome = kernel.on_venue_event(event)
assert isinstance(outcome.diagnostic_code, KernelDiagnosticCode)
assert slot.size >= 0.0
assert slot.initial_size >= 0.0
assert slot.fsm_state in set(TradeStage)
if slot.closed:
assert slot.size == pytest.approx(0.0, abs=1e-9)
assert len(journal.rows) >= len(sequence)
assert len(zinc.state_region) >= 1
@pytest.mark.parametrize("case", REPLAY_CASES, ids=[case.name for case in REPLAY_CASES])
def test_kernel_debug_journal_replay_matrix(case: ReplayCase) -> None:
kernel, journal, zinc = _build_kernel()
kernel.update_control(
ControlUpdate(
mode=case.control_mode,
verbosity=case.verbosity,
trace_transitions=True,
debug_clickhouse_enabled=True,
)
)
rng = random.Random(case.seed)
for idx in range(10):
slot_id = idx % 2
trade_id = f"{case.name}-{idx}"
if idx % 3 == 0:
_enter_open(kernel, trade_id=trade_id, slot_id=slot_id, size=1.0)
elif idx % 3 == 1 and kernel.slot(slot_id).is_open():
_seed_exit_working(kernel, trade_id=kernel.slot(slot_id).trade_id, slot_id=slot_id, exit_ratio=(0.5, 0.5))
else:
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:mark",
trade_id=trade_id,
slot_id=slot_id,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.MARK_PRICE,
reference_price=97.0 + rng.random(),
target_size=1.0,
leverage=2.0,
exit_leg_ratios=(1.0,),
reason="journal-mark",
)
)
rows = list(journal.rows)
assert rows
for row in rows:
slot_state = row["slot_state"]
assert row["prev_state"] != ""
assert row["next_state"] != ""
assert slot_state["fsm_state"] == row["next_state"]
assert row["control_mode"] in {KernelMode.NORMAL.value, KernelMode.DEBUG.value}
assert row["control_verbosity"] in {KernelVerbosity.QUIET.value, KernelVerbosity.VERBOSE.value, KernelVerbosity.TRACE.value}
replayed, _, replayed_zinc = _build_kernel()
replayed.update_control(
ControlUpdate(mode=case.control_mode, verbosity=case.verbosity, trace_transitions=True, debug_clickhouse_enabled=True)
)
replayed.reconcile_from_slots(zinc.read_slots())
assert replayed.snapshot()["slots"] == kernel.snapshot()["slots"]
assert len(replayed_zinc.read_slots()) == len(replayed.snapshot()["slots"])
assert [slot.slot_id for slot in replayed_zinc.read_slots()] == [slot.slot_id for slot in replayed.state.slots]

View File

@@ -0,0 +1,437 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import random
import pytest
from prod.clean_arch.dita_v2 import (
BackendMode,
ControlUpdate,
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelDiagnosticCode,
KernelEventKind,
KernelIntent,
KernelMode,
KernelControlSnapshot,
KernelVerbosity,
MemoryKernelJournal,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
class NoopVenueAdapter:
def submit(self, intent: KernelIntent): # type: ignore[override]
return []
def cancel(self, order: VenueOrder, *, reason: str = ""): # type: ignore[override]
return []
def open_orders(self): # type: ignore[override]
return []
def open_positions(self): # type: ignore[override]
return []
def reconcile(self): # type: ignore[override]
return []
@dataclass(frozen=True)
class RaceCase:
name: str
seed_state: str
first_kind: KernelEventKind
second_kind: KernelEventKind
expected_state: TradeStage
expected_code_2: KernelDiagnosticCode
@dataclass(frozen=True)
class OffByOneCase:
name: str
exit_leg_ratios: tuple[float, ...]
fills: tuple[float, ...]
expected_leg_index: int
expected_closed: bool
@dataclass(frozen=True)
class MemoryCase:
name: str
max_slots: int
write_slot_ids: tuple[int, ...]
reconcile_slot_ids: tuple[int, ...]
expected_written_count: int
def _build_kernel(slot_count: int = 4) -> tuple[ExecutionKernel, MemoryKernelJournal, InMemoryZincPlane]:
journal = MemoryKernelJournal()
zinc = InMemoryZincPlane()
kernel = ExecutionKernel(
max_slots=slot_count,
control_plane=InMemoryControlPlane(
KernelControlSnapshot(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
backend_mode=BackendMode.MOCK,
trace_transitions=True,
debug_clickhouse_enabled=True,
mirror_to_hazelcast=True,
)
),
venue=NoopVenueAdapter(),
journal=journal,
zinc_plane=zinc,
)
return kernel, journal, zinc
def _seed_entry_working(kernel: ExecutionKernel, *, trade_id: str, slot_id: int = 0, size: float = 1.0) -> None:
slot = kernel.slot(slot_id)
slot.trade_id = trade_id
slot.asset = "BTCUSDT"
slot.side = TradeSide.SHORT
slot.entry_price = 100.0
slot.size = 0.0
slot.initial_size = 0.0
slot.leverage = 2.0
slot.entry_time = datetime.now(timezone.utc)
slot.exit_leg_ratios = (1.0,)
slot.active_leg_index = 0
slot.closed = False
slot.close_reason = ""
slot.active_exit_order = None
slot.active_entry_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id=f"V-ENTRY-{slot_id}",
venue_client_id=f"{trade_id}:entry",
side=TradeSide.SHORT,
intended_size=size,
filled_size=0.0,
average_fill_price=0.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot_id},
)
slot.fsm_state = TradeStage.ENTRY_WORKING
def _seed_position_open(kernel: ExecutionKernel, *, trade_id: str, slot_id: int = 0, size: float = 1.0) -> None:
_seed_entry_working(kernel, trade_id=trade_id, slot_id=slot_id, size=size)
slot = kernel.slot(slot_id)
slot.size = size
slot.initial_size = size
slot.entry_price = 100.0
slot.active_entry_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id=f"V-ENTRY-{slot_id}",
venue_client_id=f"{trade_id}:entry",
side=TradeSide.SHORT,
intended_size=size,
filled_size=size,
average_fill_price=100.0,
status=VenueOrderStatus.FILLED,
metadata={"slot_id": slot_id},
)
slot.fsm_state = TradeStage.POSITION_OPEN
def _seed_exit_working(kernel: ExecutionKernel, *, trade_id: str, slot_id: int = 0, exit_leg_ratios: tuple[float, ...] = (1.0,)) -> None:
_seed_position_open(kernel, trade_id=trade_id, slot_id=slot_id, size=1.0)
slot = kernel.slot(slot_id)
slot.exit_leg_ratios = exit_leg_ratios
slot.active_exit_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id=f"V-EXIT-{slot_id}",
venue_client_id=f"{trade_id}:exit",
side=TradeSide.SHORT,
intended_size=slot.next_exit_ratio() * slot.initial_size,
filled_size=0.0,
average_fill_price=0.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot_id},
)
slot.fsm_state = TradeStage.EXIT_WORKING
def _make_event(
slot: TradeSlot,
*,
kind: KernelEventKind,
event_id: str,
filled_size: float,
slot_id: int | None = None,
venue_order_id: str | None = None,
venue_client_id: str | None = None,
reason: str = "",
) -> VenueEvent:
order_id = venue_order_id or (
slot.active_exit_order.venue_order_id if slot.active_exit_order else slot.active_entry_order.venue_order_id if slot.active_entry_order else "V-ORDER"
)
client_id = venue_client_id or (
slot.active_exit_order.venue_client_id if slot.active_exit_order else slot.active_entry_order.venue_client_id if slot.active_entry_order else "trade:client"
)
return VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=event_id,
trade_id=slot.trade_id,
slot_id=slot.slot_id if slot_id is None else slot_id,
kind=kind,
status={
KernelEventKind.ORDER_ACK: VenueEventStatus.ACKED,
KernelEventKind.ORDER_REJECT: VenueEventStatus.REJECTED,
KernelEventKind.PARTIAL_FILL: VenueEventStatus.PARTIALLY_FILLED,
KernelEventKind.FULL_FILL: VenueEventStatus.FILLED,
KernelEventKind.CANCEL_ACK: VenueEventStatus.CANCELED,
KernelEventKind.CANCEL_REJECT: VenueEventStatus.CANCELED_REJECTED,
KernelEventKind.MARK_PRICE: VenueEventStatus.ACKED,
KernelEventKind.RECONCILE: VenueEventStatus.ACKED,
}[kind],
venue_order_id=order_id,
venue_client_id=client_id,
side=slot.side if slot.side != TradeSide.FLAT else TradeSide.SHORT,
asset=slot.asset or "BTCUSDT",
price=99.0 if kind == KernelEventKind.MARK_PRICE else 100.0,
size=max(1.0, slot.size or 1.0),
filled_size=filled_size,
remaining_size=max(0.0, max(1.0, slot.size or 1.0) - filled_size),
reason=reason,
)
RACE_CASES = [
RaceCase("entry_ack_then_fullfill", "entry_working", KernelEventKind.ORDER_ACK, KernelEventKind.FULL_FILL, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
RaceCase("entry_fullfill_then_ack", "entry_working", KernelEventKind.FULL_FILL, KernelEventKind.ORDER_ACK, TradeStage.POSITION_OPEN, KernelDiagnosticCode.DUPLICATE_EVENT),
RaceCase("entry_ack_then_reject", "entry_working", KernelEventKind.ORDER_ACK, KernelEventKind.ORDER_REJECT, TradeStage.IDLE, KernelDiagnosticCode.ENTRY_ORDER_REJECTED),
RaceCase("entry_reject_then_ack", "entry_working", KernelEventKind.ORDER_REJECT, KernelEventKind.ORDER_ACK, TradeStage.IDLE, KernelDiagnosticCode.OK),
RaceCase("entry_mark_then_fullfill", "entry_working", KernelEventKind.MARK_PRICE, KernelEventKind.FULL_FILL, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
RaceCase("entry_reconcile_then_ack", "entry_working", KernelEventKind.RECONCILE, KernelEventKind.ORDER_ACK, TradeStage.STALE_STATE_RECONCILING, KernelDiagnosticCode.STALE_STATE_RECONCILE),
RaceCase("exit_ack_then_fullfill", "exit_working", KernelEventKind.ORDER_ACK, KernelEventKind.FULL_FILL, TradeStage.CLOSED, KernelDiagnosticCode.OK),
RaceCase("exit_fullfill_then_ack", "exit_working", KernelEventKind.FULL_FILL, KernelEventKind.ORDER_ACK, TradeStage.CLOSED, KernelDiagnosticCode.OK),
RaceCase("exit_cancel_ack_then_fullfill", "exit_working", KernelEventKind.CANCEL_ACK, KernelEventKind.FULL_FILL, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
RaceCase("exit_fullfill_then_cancel_ack", "exit_working", KernelEventKind.FULL_FILL, KernelEventKind.CANCEL_ACK, TradeStage.CLOSED, KernelDiagnosticCode.OK),
RaceCase("exit_cancel_reject_then_ack", "exit_working", KernelEventKind.CANCEL_REJECT, KernelEventKind.CANCEL_ACK, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
RaceCase("exit_mark_then_fullfill", "exit_working", KernelEventKind.MARK_PRICE, KernelEventKind.FULL_FILL, TradeStage.CLOSED, KernelDiagnosticCode.OK),
]
OFF_BY_ONE_CASES = [
OffByOneCase("ratios_empty", (), (), 0, False),
OffByOneCase("ratios_one", (1.0,), (1.0,), 1, True),
OffByOneCase("ratios_two_equal", (0.5, 0.5), (0.5, 0.5), 2, True),
OffByOneCase("ratios_three_tail", (0.25, 0.25, 0.5), (0.25, 0.25, 0.5), 3, True),
OffByOneCase("ratios_three_front_loaded", (0.6, 0.3, 0.1), (0.6, 0.3, 0.1), 3, True),
OffByOneCase("ratios_four_small", (0.1, 0.2, 0.3, 0.4), (0.1, 0.2, 0.3, 0.4), 4, True),
]
MEMORY_CASES = [
MemoryCase("sparse_write_order", 5, (3, 1, 4), (3, 1, 4), 3),
MemoryCase("overwrite_same_slot", 4, (2, 2, 2), (2,), 1),
MemoryCase("capacity_trim", 3, (0, 1, 2, 3, 4), (0, 1, 2), 3),
MemoryCase("single_slot_reconcile", 2, (1,), (1,), 1),
MemoryCase("mixed_holes", 6, (5, 0, 3), (5, 0, 3), 3),
MemoryCase("late_slot_overwrite", 4, (1, 3, 1), (1, 3), 2),
]
@pytest.mark.parametrize("case", RACE_CASES, ids=[case.name for case in RACE_CASES])
def test_kernel_race_and_reorder_matrix(case: RaceCase) -> None:
kernel, journal, zinc = _build_kernel()
if case.seed_state == "entry_working":
_seed_entry_working(kernel, trade_id=f"race-{case.name}")
elif case.seed_state == "exit_working":
_seed_exit_working(kernel, trade_id=f"race-{case.name}")
else:
_seed_position_open(kernel, trade_id=f"race-{case.name}")
slot = kernel.slot(0)
first = _make_event(
slot,
kind=case.first_kind,
event_id=f"{case.name}-first",
filled_size=1.0 if case.first_kind == KernelEventKind.FULL_FILL else 0.5,
)
second = _make_event(
slot,
kind=case.second_kind,
event_id=f"{case.name}-second",
filled_size=1.0 if case.second_kind == KernelEventKind.FULL_FILL else 0.5,
)
outcome_1 = kernel.on_venue_event(first)
outcome_2 = kernel.on_venue_event(second)
assert outcome_1.diagnostic_code in set(KernelDiagnosticCode)
assert outcome_2.diagnostic_code in set(KernelDiagnosticCode)
assert slot.size >= 0.0
assert slot.initial_size >= 0.0
assert slot.fsm_state == case.expected_state
assert outcome_2.diagnostic_code == case.expected_code_2
assert zinc.state_region[0].fsm_state == slot.fsm_state
assert len(journal.rows) >= 2
@pytest.mark.parametrize("case", OFF_BY_ONE_CASES, ids=[case.name for case in OFF_BY_ONE_CASES])
def test_kernel_exit_leg_off_by_one_matrix(case: OffByOneCase) -> None:
kernel, journal, zinc = _build_kernel()
_seed_exit_working(kernel, trade_id=f"obo-{case.name}", exit_leg_ratios=case.exit_leg_ratios)
slot = kernel.slot(0)
assert slot.next_exit_ratio() == pytest.approx(case.exit_leg_ratios[0] if case.exit_leg_ratios else 1.0, abs=1e-9)
for idx, fill_size in enumerate(case.fills):
event = _make_event(
slot,
kind=KernelEventKind.FULL_FILL,
event_id=f"{case.name}-fill-{idx}",
filled_size=fill_size,
reason=f"leg-{idx}",
)
outcome = kernel.on_venue_event(event)
assert outcome.accepted is True
assert slot.size >= 0.0
assert slot.active_leg_index <= max(len(case.exit_leg_ratios), 1)
if idx < len(case.fills) - 1:
assert slot.fsm_state == TradeStage.POSITION_OPEN
rearm = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{case.name}-rearm-{idx}",
trade_id=slot.trade_id,
slot_id=slot.slot_id,
asset=slot.asset,
side=slot.side,
action=KernelCommandType.EXIT,
reference_price=100.0,
target_size=slot.next_exit_ratio(),
leverage=slot.leverage,
exit_leg_ratios=case.exit_leg_ratios,
reason=f"rearm-{idx}",
)
)
assert rearm.accepted is True
assert slot.fsm_state == TradeStage.EXIT_REQUESTED
else:
assert slot.fsm_state in {TradeStage.POSITION_OPEN, TradeStage.CLOSED}
assert slot.active_leg_index == case.expected_leg_index
assert slot.closed is case.expected_closed
if case.fills:
assert zinc.state_region[0].active_leg_index == slot.active_leg_index
else:
assert zinc.state_region[0].trade_id == slot.trade_id
assert zinc.state_region[0].fsm_state == TradeStage.EXIT_WORKING
assert len(journal.rows) >= len(case.fills)
assert slot.next_exit_ratio() == pytest.approx(1.0, abs=1e-9) if case.expected_closed else slot.next_exit_ratio() <= 1.0
@pytest.mark.parametrize("case", MEMORY_CASES, ids=[case.name for case in MEMORY_CASES])
def test_kernel_zinc_memory_anomaly_matrix(case: MemoryCase) -> None:
kernel, journal, zinc = _build_kernel(slot_count=case.max_slots)
rng = random.Random(hash(case.name) & 0xFFFFFFFF)
for idx, slot_id in enumerate(case.write_slot_ids):
slot = kernel.slot(slot_id % case.max_slots)
slot.trade_id = f"{case.name}-{idx}"
slot.asset = "BTCUSDT"
slot.side = TradeSide.SHORT
slot.entry_price = 100.0
slot.size = float(idx + 1)
slot.initial_size = float(idx + 1)
slot.leverage = 2.0
slot.fsm_state = TradeStage.POSITION_OPEN if idx % 2 == 0 else TradeStage.EXIT_WORKING
slot.active_entry_order = VenueOrder(
internal_trade_id=slot.trade_id,
venue_order_id=f"V-ENTRY-{slot_id}-{idx}",
venue_client_id=f"{slot.trade_id}:entry",
side=TradeSide.SHORT,
intended_size=slot.size,
filled_size=slot.size,
average_fill_price=100.0,
status=VenueOrderStatus.FILLED,
metadata={"slot_id": slot.slot_id},
)
if slot.fsm_state == TradeStage.EXIT_WORKING:
slot.active_exit_order = VenueOrder(
internal_trade_id=slot.trade_id,
venue_order_id=f"V-EXIT-{slot_id}-{idx}",
venue_client_id=f"{slot.trade_id}:exit",
side=TradeSide.SHORT,
intended_size=max(0.1, slot.size / 2.0),
filled_size=0.0,
average_fill_price=0.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot.slot_id},
)
kernel.zinc_plane.write_slot(slot)
written = zinc.read_slots()
assert len(written) == case.expected_written_count
assert [slot.slot_id for slot in written] == sorted(set(slot_id % case.max_slots for slot_id in case.write_slot_ids))
# Shuffle a snapshot and reconcile it back into a fresh kernel to exercise
# sparse, duplicate and truncated memory layouts without venue involvement.
shuffled_snapshot = list(reversed(written))
if rng.random() < 0.5:
shuffled_snapshot.append(shuffled_snapshot[0])
restarted, restarted_journal, restarted_zinc = _build_kernel(slot_count=case.max_slots)
outcome = restarted.reconcile_from_slots(shuffled_snapshot)
assert outcome.accepted is True
assert outcome.diagnostic_code == KernelDiagnosticCode.RECONCILED
assert len(restarted_zinc.read_slots()) == case.max_slots
assert restarted.snapshot()["slots"] == [slot.to_dict() for slot in restarted_zinc.read_slots()]
assert len(restarted_journal.rows) == 0
# Feed a stale-state event against a reconstructed slot to ensure the kernel
# stays stable even when the memory image is awkward.
target_slot_id = restarted_zinc.read_slots()[0].slot_id
slot = restarted.slot(target_slot_id)
stale_intent = restarted.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{case.name}:stale",
trade_id=slot.trade_id,
slot_id=slot.slot_id,
asset=slot.asset,
side=slot.side,
action=KernelCommandType.RECONCILE,
reference_price=100.0,
target_size=max(1.0, slot.size or 1.0),
leverage=max(1.0, slot.leverage or 1.0),
exit_leg_ratios=slot.exit_leg_ratios,
reason="stale-reconcile",
)
)
assert stale_intent.diagnostic_code == KernelDiagnosticCode.STALE_STATE_RECONCILE
assert slot.fsm_state == TradeStage.STALE_STATE_RECONCILING
event = VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"{case.name}-reconcile",
trade_id=slot.trade_id,
slot_id=slot.slot_id,
kind=KernelEventKind.RECONCILE,
status=VenueEventStatus.ACKED,
venue_order_id=slot.active_exit_order.venue_order_id if slot.active_exit_order else slot.active_entry_order.venue_order_id if slot.active_entry_order else "V-ORDER",
venue_client_id=slot.active_exit_order.venue_client_id if slot.active_exit_order else slot.active_entry_order.venue_client_id if slot.active_entry_order else "V-CLIENT",
side=slot.side if slot.side != TradeSide.FLAT else TradeSide.SHORT,
asset=slot.asset or "BTCUSDT",
price=100.0,
size=max(1.0, slot.size or 1.0),
filled_size=0.0,
remaining_size=max(0.0, slot.size),
)
stale = restarted.on_venue_event(event)
assert stale.diagnostic_code == KernelDiagnosticCode.STALE_STATE_RECONCILE
assert restarted.slot(target_slot_id).fsm_state == TradeStage.STALE_STATE_RECONCILING

View File

@@ -0,0 +1,126 @@
from __future__ import annotations
from uuid import uuid4
import os
import unittest
from unittest.mock import patch
from prod.clean_arch.dita_v2 import (
DITAv2LauncherBundle,
LauncherVenueMode,
LauncherZincMode,
KernelControlSnapshot,
MockVenueAdapter,
build_launcher_bundle,
)
from prod.bingx.enums import BingxEnvironment
from prod.clean_arch.dita_v2.launcher import _maybe_close
from prod.clean_arch.dita_v2.launcher import build_bingx_exec_client_config
class DummyCloseable:
def __init__(self) -> None:
self.closed = False
def close(self) -> None:
self.closed = True
class DummyControlPlane:
def __init__(self) -> None:
self.snapshot = KernelControlSnapshot()
def read(self) -> KernelControlSnapshot:
return self.snapshot
def close(self) -> None:
pass
class DummyZincPlane:
def __init__(self) -> None:
self.control_updates: list[KernelControlSnapshot] = []
self.slot_writes: list[object] = []
def update_control(self, snapshot: KernelControlSnapshot) -> None:
self.control_updates.append(snapshot)
def write_slot(self, slot: object) -> None:
self.slot_writes.append(slot)
class TestDITAv2Launcher(unittest.TestCase):
def test_build_launcher_bundle_defaults_to_mock_and_in_memory(self) -> None:
bundle = build_launcher_bundle(prefix=f"dita_v2_{uuid4().hex}")
try:
self.assertIsInstance(bundle, DITAv2LauncherBundle)
self.assertIsInstance(bundle.venue, MockVenueAdapter)
self.assertEqual(bundle.kernel.max_slots, 10)
self.assertEqual(bundle.kernel.control.mode.value, "NORMAL")
finally:
bundle.close()
def test_build_launcher_bundle_can_select_real_components_via_env(self) -> None:
prefix = f"dita_v2_{uuid4().hex}"
dummy_control = DummyControlPlane()
dummy_zinc = DummyZincPlane()
with patch("prod.clean_arch.dita_v2.launcher.build_control_plane", return_value=dummy_control), patch(
"prod.clean_arch.dita_v2.launcher._build_zinc_plane", return_value=dummy_zinc
):
bundle = build_launcher_bundle(
prefix=prefix,
venue_mode=LauncherVenueMode.BINGX,
zinc_mode=LauncherZincMode.REAL,
bingx_backend=object(),
)
try:
self.assertIs(bundle.control_plane, dummy_control)
self.assertIs(bundle.zinc_plane, dummy_zinc)
self.assertEqual(bundle.venue.__class__.__name__, "BingxVenueAdapter")
finally:
bundle.close()
def test_build_launcher_bundle_respects_explicit_modes(self) -> None:
prefix = f"dita_v2_{uuid4().hex}"
bundle = build_launcher_bundle(
prefix=prefix,
venue_mode=LauncherVenueMode.MOCK,
zinc_mode=LauncherZincMode.IN_MEMORY,
)
try:
self.assertIsInstance(bundle.venue, MockVenueAdapter)
self.assertEqual(bundle.kernel.max_slots, 10)
finally:
bundle.close()
def test_bingx_exec_client_config_uses_standard_testnet_credentials(self) -> None:
with patch.dict(
os.environ,
{
"BINGX_API_KEY": "test-api-key",
"BINGX_SECRET_KEY": "test-secret-key",
"DOLPHIN_BINGX_ENV": "VST",
"DOLPHIN_BINGX_ALLOW_MAINNET": "0",
"DOLPHIN_BINGX_RECV_WINDOW_MS": "60000",
"DOLPHIN_BINGX_DEFAULT_LEVERAGE": "1",
"DOLPHIN_BINGX_EXCHANGE_LEVERAGE_CAP": "3",
},
clear=False,
):
cfg = build_bingx_exec_client_config()
self.assertEqual(cfg.api_key, "test-api-key")
self.assertEqual(cfg.secret_key, "test-secret-key")
self.assertIs(cfg.environment, BingxEnvironment.VST)
self.assertFalse(cfg.allow_mainnet)
self.assertEqual(cfg.recv_window_ms, 60000)
self.assertEqual(cfg.default_leverage, 1)
self.assertEqual(cfg.exchange_leverage_cap, 3)
def test_maybe_close_handles_closeable_objects(self) -> None:
dummy = DummyCloseable()
_maybe_close(dummy)
self.assertTrue(dummy.closed)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,820 @@
from __future__ import annotations
import asyncio
import os
import re
import time
import uuid
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal
from pathlib import Path
from typing import Any
import pytest
from dotenv import load_dotenv
from prod.bingx.config import BingxExecClientConfig
from prod.bingx.config import BingxInstrumentProviderConfig
from prod.bingx.enums import BingxEnvironment
from prod.bingx.schemas import BingxContract
from prod.clean_arch.dita_v2 import BackendMode
from prod.clean_arch.dita_v2 import BingxVenueAdapter
from prod.clean_arch.dita_v2 import ControlUpdate
from prod.clean_arch.dita_v2 import ExecutionKernel
from prod.clean_arch.dita_v2 import KernelCommandType
from prod.clean_arch.dita_v2 import KernelDiagnosticCode
from prod.clean_arch.dita_v2 import KernelEventKind
from prod.clean_arch.dita_v2 import KernelIntent
from prod.clean_arch.dita_v2 import KernelMode
from prod.clean_arch.dita_v2 import KernelVerbosity
from prod.clean_arch.dita_v2 import RealZincControlPlane
from prod.clean_arch.dita_v2 import RealZincPlane
from prod.clean_arch.dita_v2 import TradeSide
from prod.clean_arch.dita_v2 import TradeStage
DOTENV_PATH = Path("/mnt/dolphinng5_predict/.env")
if DOTENV_PATH.exists():
load_dotenv(DOTENV_PATH, override=False)
LIVE_ENABLED = os.getenv("BINGX_SMOKE_LIVE") == "1"
LIVE_TRADING_ENABLED = os.getenv("BINGX_SMOKE_ALLOW_TRADE") == "1"
LIVE_DITAV2_ENABLED = os.getenv("DITA_V2_LIVE_BINGX") == "1"
LIVE_CREDENTIALS_READY = bool(os.getenv("BINGX_API_KEY")) and bool(os.getenv("BINGX_SECRET_KEY"))
pytestmark = pytest.mark.skipif(
not (LIVE_ENABLED and LIVE_TRADING_ENABLED and LIVE_DITAV2_ENABLED and LIVE_CREDENTIALS_READY),
reason=(
"DITAv2 live BingX testnet E2E requires BINGX_SMOKE_LIVE=1, "
"BINGX_SMOKE_ALLOW_TRADE=1, DITA_V2_LIVE_BINGX=1, and BingX VST credentials"
),
)
def _norm_symbol(value: str) -> str:
return str(value or "").replace("-", "").replace("_", "").upper()
def _contract_rows(payload: Any) -> list[dict[str, Any]]:
if isinstance(payload, list):
return [row for row in payload if isinstance(row, dict)]
if isinstance(payload, dict):
for key in ("contracts", "data", "rows"):
rows = payload.get(key)
if isinstance(rows, list):
return [row for row in rows if isinstance(row, dict)]
return []
def _reference_price_row(payload: Any) -> dict[str, Any]:
if isinstance(payload, list):
return payload[0] if payload and isinstance(payload[0], dict) else {}
if isinstance(payload, dict):
data = payload.get("data")
if isinstance(data, list):
return data[0] if data and isinstance(data[0], dict) else {}
if isinstance(data, dict):
return data
return payload
return {}
def _position_qty(row: dict[str, Any]) -> Decimal:
raw = row.get("positionAmt") or row.get("positionQty") or row.get("positionSize") or row.get("quantity") or 0
try:
return abs(Decimal(str(raw)))
except Exception:
return Decimal("0")
def _position_side(row: dict[str, Any]) -> TradeSide:
side_raw = str(row.get("positionSide") or row.get("side") or "").upper()
if side_raw in {"SHORT", "SELL"}:
return TradeSide.SHORT
if side_raw in {"LONG", "BUY"}:
return TradeSide.LONG
qty = _position_qty(row)
signed = str(row.get("positionAmt") or row.get("quantity") or "0")
try:
return TradeSide.SHORT if Decimal(signed) < 0 else TradeSide.LONG if qty > 0 else TradeSide.FLAT
except Exception:
return TradeSide.FLAT
def _live_quantity(contract: BingxContract) -> Decimal:
base = contract.min_quantity if contract.min_quantity > 0 else contract.step_size
if base <= 0:
base = Decimal("0.001")
qty = base * Decimal("2")
step = contract.step_size if contract.step_size > 0 else Decimal("0.001")
if qty < step * Decimal("2"):
qty = step * Decimal("2")
if qty < Decimal("12"):
qty = Decimal("12")
return qty
def _build_kernel(prefix: str) -> tuple[ExecutionKernel, RealZincControlPlane, RealZincPlane, BingxVenueAdapter]:
zinc_plane = RealZincPlane(prefix=prefix, slot_count=1, create=True)
control_plane = RealZincControlPlane(prefix=prefix, create=False)
venue = BingxVenueAdapter(
config=BingxExecClientConfig(
api_key=os.environ.get("BINGX_API_KEY", ""),
secret_key=os.environ.get("BINGX_SECRET_KEY", ""),
environment=BingxEnvironment.VST,
allow_mainnet=False,
recv_window_ms=int(os.environ.get("DOLPHIN_BINGX_RECV_WINDOW_MS", "60000")),
default_leverage=int(os.environ.get("DOLPHIN_BINGX_DEFAULT_LEVERAGE", "1")),
exchange_leverage_cap=int(os.environ.get("DOLPHIN_BINGX_EXCHANGE_LEVERAGE_CAP", "3")),
prefer_websocket=False,
sizing_mode="testnet",
journal_strategy="dita_v2_live_testnet",
journal_db="dolphin_pink",
instrument_provider=BingxInstrumentProviderConfig(load_all=True),
)
)
kernel = ExecutionKernel(
max_slots=1,
control_plane=control_plane,
venue=venue,
zinc_plane=zinc_plane,
)
kernel.update_control(
ControlUpdate(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
backend_mode=BackendMode.BINGX,
trace_transitions=True,
debug_clickhouse_enabled=True,
mirror_to_hazelcast=True,
reconcile_on_restart=True,
)
)
return kernel, control_plane, zinc_plane, venue
def _pick_live_contract(venue: BingxVenueAdapter) -> BingxContract:
client = getattr(getattr(venue, "backend", None), "_client", None)
if client is None:
raise AssertionError("BingxVenueAdapter backend does not expose a live client")
async def _inner() -> BingxContract:
state = await venue.backend.refresh_state(include_history=True)
open_symbols = {
_norm_symbol(str(row.get("symbol", key)))
for key, row in getattr(state, "open_positions", {}).items()
if isinstance(row, dict)
}
contracts_payload = await client.public_get("/openApi/swap/v2/quote/contracts")
contracts: list[BingxContract] = []
for row in _contract_rows(contracts_payload):
try:
contracts.append(BingxContract.from_http(row))
except Exception:
continue
if not contracts:
raise AssertionError("BingX VST contract loader returned no usable contracts")
preferred = [
os.getenv("BINGX_SMOKE_SYMBOL", "").strip().upper(),
"TRXUSDT",
"XLMUSDT",
"DOGEUSDT",
"ETHUSDT",
"BTCUSDT",
]
by_symbol = {contract.symbol.upper(): contract for contract in contracts}
by_venue = {contract.venue_symbol.replace("-", "").upper(): contract for contract in contracts}
for candidate in preferred:
if not candidate or candidate in open_symbols:
continue
if candidate in by_symbol:
return by_symbol[candidate]
if candidate in by_venue:
return by_venue[candidate]
for contract in contracts:
symbol = contract.symbol.upper()
venue_symbol = contract.venue_symbol.replace("-", "").upper()
if symbol not in open_symbols and venue_symbol not in open_symbols:
return contract
raise AssertionError("No BingX VST contract available outside the current open set")
return asyncio.run(_inner())
def _reference_price(venue: BingxVenueAdapter, contract: BingxContract) -> Decimal:
client = getattr(getattr(venue, "backend", None), "_client", None)
if client is None:
raise AssertionError("BingxVenueAdapter backend does not expose a live client")
async def _inner() -> Decimal:
payload = await client.public_get("/openApi/swap/v2/quote/price", {"symbol": contract.venue_symbol})
row = _reference_price_row(payload)
raw_price = row.get("price") or row.get("lastPrice") or row.get("markPrice") or row.get("last")
if raw_price is None:
raise AssertionError(f"Unable to resolve BingX price for {contract.venue_symbol}: {payload!r}")
return Decimal(str(raw_price))
return asyncio.run(_inner())
def _live_intent(
*,
action: KernelCommandType,
trade_id: str,
side: TradeSide,
asset: str,
target_size: float,
price: float,
reason: str,
) -> dict[str, Any]:
return {
"timestamp": datetime.now(timezone.utc).isoformat(),
"intent_id": f"{trade_id}:{action.value}:{reason}:{uuid.uuid4().hex[:8]}",
"trade_id": trade_id,
"slot_id": 0,
"asset": asset,
"side": side.value,
"action": action.value,
"reference_price": float(price),
"target_size": float(target_size),
"leverage": 1.0,
"exit_leg_ratios": [0.5, 0.5],
"reason": reason,
"metadata": {"source": "live_bingx_testnet"},
"stage": "INTENT_CREATED",
}
def _current_exchange_rows(venue: BingxVenueAdapter, symbol: str) -> list[dict[str, Any]]:
rows = []
for row in venue.open_positions():
if _norm_symbol(str(row.get("symbol") or row.get("venueSymbol") or "")) == _norm_symbol(symbol):
rows.append(dict(row))
return rows
def _current_exchange_qty(venue: BingxVenueAdapter, symbol: str) -> Decimal:
rows = _current_exchange_rows(venue, symbol)
if not rows:
return Decimal("0")
return max(_position_qty(row) for row in rows)
def _observed_live_qty(kernel: ExecutionKernel, venue: BingxVenueAdapter, symbol: str) -> Decimal:
slot_qty = Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0))
exchange_qty = _current_exchange_qty(venue, symbol)
return max(slot_qty, exchange_qty)
def _drive_live_reconcile(kernel: ExecutionKernel, venue: BingxVenueAdapter, symbol: str) -> None:
snapshot = venue.reconcile()
for event in snapshot:
if _norm_symbol(str(getattr(event, "asset", ""))) == _norm_symbol(symbol):
kernel.on_venue_event(event)
def _wait_for_live_response(kernel: ExecutionKernel, venue: BingxVenueAdapter, symbol: str, *, timeout_s: float = 60.0) -> tuple[TradeStage, Decimal]:
def _predicate() -> bool:
_drive_live_reconcile(kernel, venue, symbol)
slot = kernel.slot(0)
return slot.asset == symbol and slot.fsm_state != TradeStage.IDLE
try:
_wait_until(_predicate, timeout_s=timeout_s, interval_s=1.0)
except AssertionError:
_drive_live_reconcile(kernel, venue, symbol)
slot = kernel.slot(0)
return slot.fsm_state, _current_exchange_qty(venue, symbol)
def _wait_until(predicate, *, timeout_s: float = 30.0, interval_s: float = 1.0) -> None:
deadline = time.monotonic() + timeout_s
last_exc: Exception | None = None
while time.monotonic() < deadline:
try:
if predicate():
return
except Exception as exc: # pragma: no cover - best effort live polling
last_exc = exc
time.sleep(interval_s)
if last_exc is not None:
raise AssertionError("timed out while waiting for live BingX state") from last_exc
raise AssertionError("timed out while waiting for live BingX state")
def _cleanup_live_position(kernel: ExecutionKernel, venue: BingxVenueAdapter, contract: BingxContract, trade_id: str) -> None:
try:
for attempt in range(5):
qty = _current_exchange_qty(venue, contract.symbol)
if qty <= 0:
return
side = TradeSide.SHORT
rows = _current_exchange_rows(venue, contract.symbol)
if rows:
side = _position_side(rows[0])
if side == TradeSide.FLAT:
side = TradeSide.SHORT
price = float(_reference_price(venue, contract))
outcome = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:cleanup:{attempt}:{uuid.uuid4().hex[:8]}",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.EXIT,
reference_price=price,
target_size=float(qty),
leverage=1.0,
exit_leg_ratios=(1.0,),
reason="CLEANUP",
metadata={"source": "cleanup"},
)
)
if outcome.diagnostic_code == KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER:
_wait_until(lambda: _current_exchange_qty(venue, contract.symbol) <= 0, timeout_s=20.0, interval_s=1.0)
else:
_wait_until(lambda: _current_exchange_qty(venue, contract.symbol) <= 0, timeout_s=30.0, interval_s=1.0)
finally:
pass
@dataclass(frozen=True)
class _LiveCase:
name: str
side: TradeSide
LIVE_CASES = (
_LiveCase("short_cycle", TradeSide.SHORT),
_LiveCase("long_cycle", TradeSide.LONG),
)
@pytest.mark.parametrize("case", LIVE_CASES, ids=lambda case: case.name)
def test_live_bingx_testnet_basic_cycle(case: _LiveCase) -> None:
prefix = f"dita_v2_live_{case.name}_{uuid.uuid4().hex[:8]}"
kernel, control_plane, zinc_plane, venue = _build_kernel(prefix)
contract: BingxContract | None = None
trade_id = f"live-{case.name}-{uuid.uuid4().hex[:8]}"
try:
assert venue.connect() is True
contract = _pick_live_contract(venue)
size = _live_quantity(contract)
price = _reference_price(venue, contract)
entry = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:entry",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=case.side,
action=KernelCommandType.ENTER,
reference_price=float(price),
target_size=float(size),
leverage=1.0,
exit_leg_ratios=(0.5, 0.5),
reason="LIVE_ENTRY",
metadata={"contract": contract.venue_symbol},
)
)
if entry.diagnostic_code == KernelDiagnosticCode.RATE_LIMITED or any(event.kind == KernelEventKind.RATE_LIMITED for event in entry.emitted_events):
assert entry.accepted is False
assert kernel.slot(0).fsm_state in {
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.ENTRY_WORKING,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
return
assert entry.accepted is True
assert entry.diagnostic_code == KernelDiagnosticCode.OK
slot = kernel.slot(0)
assert slot.trade_id == trade_id
assert slot.asset == contract.symbol
state, open_qty = _wait_for_live_response(kernel, venue, contract.symbol, timeout_s=60.0)
kernel_qty = Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0))
live_qty = max(kernel_qty, open_qty)
assert state in {
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.ENTRY_WORKING,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
assert kernel.zinc_plane.read_control().backend_mode == BackendMode.BINGX
if live_qty <= 0:
assert entry.emitted_events, "entry should still emit an exchange reaction"
assert any(event.kind in {KernelEventKind.ORDER_ACK, KernelEventKind.ORDER_REJECT, KernelEventKind.PARTIAL_FILL, KernelEventKind.RATE_LIMITED} for event in entry.emitted_events)
return
mark = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:mark",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=case.side,
action=KernelCommandType.MARK_PRICE,
reference_price=float(price),
target_size=float(size),
leverage=1.0,
exit_leg_ratios=(0.5, 0.5),
reason="LIVE_MARK",
metadata={"contract": contract.venue_symbol},
)
)
assert mark.accepted is True
assert kernel.slot(0).asset == contract.symbol
live_qty = max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol))
assert live_qty > 0
partial_target = max(live_qty / Decimal("2"), contract.step_size)
partial = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:partial_exit",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=case.side,
action=KernelCommandType.EXIT,
reference_price=float(price),
target_size=float(partial_target),
leverage=1.0,
exit_leg_ratios=(0.5, 0.5),
reason="LIVE_PARTIAL_EXIT",
metadata={"contract": contract.venue_symbol},
)
)
if partial.diagnostic_code == KernelDiagnosticCode.RATE_LIMITED or any(event.kind == KernelEventKind.RATE_LIMITED for event in partial.emitted_events):
assert partial.accepted is False
assert kernel.slot(0).fsm_state in {
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.ENTRY_WORKING,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
return
assert partial.accepted is True
if partial.diagnostic_code in {
KernelDiagnosticCode.EXIT_ORDER_REJECTED,
KernelDiagnosticCode.ORDER_REJECTED,
KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER,
KernelDiagnosticCode.CANCEL_REJECTED,
KernelDiagnosticCode.RATE_LIMITED,
} or any(event.kind in {KernelEventKind.ORDER_REJECT, KernelEventKind.CANCEL_REJECT, KernelEventKind.RATE_LIMITED} for event in partial.emitted_events):
assert kernel.slot(0).fsm_state in {
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.ENTRY_WORKING,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
return
_wait_until(lambda: _current_exchange_qty(venue, contract.symbol) <= live_qty, timeout_s=60.0, interval_s=1.0)
reduced_qty = _current_exchange_qty(venue, contract.symbol)
assert reduced_qty <= open_qty
remaining = reduced_qty
if remaining > 0:
final = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:final_exit",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=case.side,
action=KernelCommandType.EXIT,
reference_price=float(price),
target_size=float(remaining),
leverage=1.0,
exit_leg_ratios=(1.0,),
reason="LIVE_FINAL_EXIT",
metadata={"contract": contract.venue_symbol},
)
)
if final.diagnostic_code == KernelDiagnosticCode.RATE_LIMITED or any(event.kind == KernelEventKind.RATE_LIMITED for event in final.emitted_events):
assert final.accepted is False
assert kernel.slot(0).fsm_state in {
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.ENTRY_WORKING,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
return
assert final.accepted is True
if final.diagnostic_code in {
KernelDiagnosticCode.EXIT_ORDER_REJECTED,
KernelDiagnosticCode.ORDER_REJECTED,
KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER,
KernelDiagnosticCode.CANCEL_REJECTED,
KernelDiagnosticCode.RATE_LIMITED,
} or any(event.kind in {KernelEventKind.ORDER_REJECT, KernelEventKind.CANCEL_REJECT, KernelEventKind.RATE_LIMITED} for event in final.emitted_events):
assert kernel.slot(0).fsm_state in {
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.ENTRY_WORKING,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
return
_wait_until(lambda: _current_exchange_qty(venue, contract.symbol) <= 0, timeout_s=60.0, interval_s=1.0)
# On the real live path there is no active working exit order after the market close.
cancel_diag = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:cancel_after_flat",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=case.side,
action=KernelCommandType.CANCEL,
reference_price=float(price),
target_size=float(size),
leverage=1.0,
exit_leg_ratios=(1.0,),
reason="LIVE_CANCEL_AFTER_FLAT",
metadata={"contract": contract.venue_symbol},
)
)
assert cancel_diag.diagnostic_code in {
KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER,
KernelDiagnosticCode.OK,
KernelDiagnosticCode.CANCEL_REJECTED,
KernelDiagnosticCode.ORDER_REJECTED,
KernelDiagnosticCode.RATE_LIMITED,
}
_wait_until(lambda: _current_exchange_qty(venue, contract.symbol) <= 0, timeout_s=60.0, interval_s=1.0)
assert _current_exchange_qty(venue, contract.symbol) <= 0
assert kernel.slot(0).size <= 1e-12
finally:
if contract is not None:
try:
_cleanup_live_position(kernel, venue, contract, trade_id)
except Exception:
pass
try:
disconnect = getattr(getattr(venue, "backend", None), "disconnect", None)
if disconnect is not None:
asyncio.run(disconnect())
except Exception:
pass
try:
zinc_plane.close()
except Exception:
pass
try:
control_plane.close()
except Exception:
pass
@pytest.mark.parametrize("seed", range(4), ids=lambda seed: f"seed-{seed}")
@pytest.mark.parametrize("side", [TradeSide.SHORT, TradeSide.LONG], ids=lambda side: f"side-{side.value.lower()}")
def test_live_bingx_testnet_chaos_fuzz(seed: int, side: TradeSide) -> None:
rng = __import__("random").Random(20260527 + seed)
prefix = f"dita_v2_live_fuzz_{side.value.lower()}_{seed}_{uuid.uuid4().hex[:8]}"
kernel, control_plane, zinc_plane, venue = _build_kernel(prefix)
contract: BingxContract | None = None
trade_id = f"live-fuzz-{side.value.lower()}-{seed}-{uuid.uuid4().hex[:8]}"
try:
assert venue.connect() is True
contract = _pick_live_contract(venue)
size = _live_quantity(contract)
price = _reference_price(venue, contract)
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:entry",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.ENTER,
reference_price=float(price),
target_size=float(size),
leverage=1.0,
exit_leg_ratios=(0.5, 0.5),
reason="FUZZ_ENTRY",
metadata={"contract": contract.venue_symbol},
)
)
state, open_qty = _wait_for_live_response(kernel, venue, contract.symbol, timeout_s=60.0)
kernel_qty = Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0))
live_qty = max(kernel_qty, open_qty)
assert state in {
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.ENTRY_WORKING,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
if live_qty <= 0:
assert kernel.slot(0).fsm_state in {
TradeStage.IDLE,
TradeStage.ENTRY_WORKING,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.STALE_STATE_RECONCILING,
TradeStage.CLOSED,
}
return
for idx in range(rng.randint(4, 7)):
slot = kernel.slot(0)
action = rng.choice(["mark", "exit_half", "exit_rest", "cancel", "reconcile"])
current_price = _reference_price(venue, contract)
_drive_live_reconcile(kernel, venue, contract.symbol)
slot = kernel.slot(0)
if action == "mark":
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:mark:{idx}",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.MARK_PRICE,
reference_price=float(current_price),
target_size=float(max(size, slot.size or size)),
leverage=1.0,
exit_leg_ratios=(0.5, 0.5),
reason=f"FUZZ_MARK_{idx}",
metadata={"contract": contract.venue_symbol},
)
)
elif action == "exit_half":
live_qty = max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol))
if live_qty <= 0:
continue
target = max(live_qty / Decimal("2"), contract.step_size)
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:exit_half:{idx}",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.EXIT,
reference_price=float(current_price),
target_size=float(target),
leverage=1.0,
exit_leg_ratios=(0.5, 0.5),
reason=f"FUZZ_EXIT_HALF_{idx}",
metadata={"contract": contract.venue_symbol},
)
)
elif action == "exit_rest":
live_qty = max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol))
if live_qty <= 0:
continue
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:exit_rest:{idx}",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.EXIT,
reference_price=float(current_price),
target_size=float(max(Decimal(str(slot.size)), contract.step_size)),
leverage=1.0,
exit_leg_ratios=(1.0,),
reason=f"FUZZ_EXIT_REST_{idx}",
metadata={"contract": contract.venue_symbol},
)
)
elif action == "cancel":
if kernel.slot(0).active_exit_order is not None or max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol)) > 0:
outcome = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:cancel:{idx}",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.CANCEL,
reference_price=float(current_price),
target_size=float(max(size, contract.step_size)),
leverage=1.0,
exit_leg_ratios=(1.0,),
reason=f"FUZZ_CANCEL_{idx}",
metadata={"contract": contract.venue_symbol},
)
)
assert outcome.diagnostic_code in {
KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER,
KernelDiagnosticCode.OK,
KernelDiagnosticCode.RATE_LIMITED,
}
else:
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:reconcile:{idx}",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.RECONCILE,
reference_price=float(current_price),
target_size=float(size),
leverage=1.0,
exit_leg_ratios=(0.5, 0.5),
reason=f"FUZZ_RECONCILE_{idx}",
metadata={"contract": contract.venue_symbol},
)
)
_drive_live_reconcile(kernel, venue, contract.symbol)
_wait_until(lambda: max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol)) >= 0, timeout_s=2.0, interval_s=0.2)
# Hard close-out pass for the fuzz cases.
for _ in range(3):
qty = max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol))
if qty <= 0:
break
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:cleanup:{uuid.uuid4().hex[:8]}",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.EXIT,
reference_price=float(_reference_price(venue, contract)),
target_size=float(qty),
leverage=1.0,
exit_leg_ratios=(1.0,),
reason="FUZZ_CLEANUP",
metadata={"contract": contract.venue_symbol},
)
)
_wait_until(lambda: max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol)) <= 0, timeout_s=60.0, interval_s=1.0)
assert max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol)) <= 0
assert kernel.slot(0).fsm_state in {
TradeStage.CLOSED,
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
assert kernel.zinc_plane.read_control().mode == KernelMode.DEBUG
assert kernel.zinc_plane.read_control().verbosity == KernelVerbosity.TRACE
finally:
if contract is not None:
try:
_cleanup_live_position(kernel, venue, contract, trade_id)
except Exception:
pass
try:
disconnect = getattr(getattr(venue, "backend", None), "disconnect", None)
if disconnect is not None:
asyncio.run(disconnect())
except Exception:
pass
try:
zinc_plane.close()
except Exception:
pass
try:
control_plane.close()
except Exception:
pass

View File

@@ -0,0 +1,54 @@
from __future__ import annotations
from pathlib import Path
import unittest
class TestDITAv2Ops(unittest.TestCase):
def test_operator_playbook_mentions_supervisor_program(self) -> None:
text = Path("/mnt/dolphinng5_predict/prod/docs/DITA_V2_OPERATOR_PLAYBOOK.md").read_text()
self.assertIn("dolphin:dita_v2", text)
self.assertIn("launch_dita_v2.py", text)
self.assertIn("dita_v2_ctl.py", text)
def test_supervisor_config_contains_dita_v2_program(self) -> None:
conf = Path("/mnt/dolphinng5_predict/prod/supervisor/dolphin-supervisord.conf").read_text()
self.assertIn("[program:dita_v2]", conf)
self.assertIn("launch_dita_v2.py", conf)
def test_supervisor_migration_doc_mentions_dita_v2_recovery(self) -> None:
text = Path("/mnt/dolphinng5_predict/prod/AGENT_READ_Supervisor_migration.md").read_text()
self.assertIn("dolphin:dita_v2", text)
self.assertIn("dita_v2_ctl.py", text)
self.assertIn("Do not use `systemctl` for `dolphin:dita_v2`", text)
def test_supervisor_wrapper_mentions_dita_v2(self) -> None:
text = Path("/mnt/dolphinng5_predict/prod/supervisor/supervisorctl.sh").read_text()
self.assertIn("dita_v2", text)
self.assertIn("dita_v2_ctl.py", text)
def test_supervisor_config_has_dita_v2_comment(self) -> None:
conf = Path("/mnt/dolphinng5_predict/prod/supervisor/dolphin-supervisord.conf").read_text()
self.assertIn("DITAv2 — supervised kernel", conf)
def test_operational_status_mentions_dita_v2(self) -> None:
text = Path("/mnt/dolphinng5_predict/prod/docs/OPERATIONAL_STATUS.md").read_text()
self.assertIn("DITAv2 Kernel", text)
self.assertIn("dolphin:dita_v2", text)
def test_live_smoke_wrapper_is_documented_and_wired(self) -> None:
script = Path("/mnt/dolphinng5_predict/prod/ops/dita_v2_live_bingx_smoke.py").read_text()
self.assertIn("BINGX_SMOKE_LIVE", script)
self.assertIn("BINGX_SMOKE_ALLOW_TRADE", script)
self.assertIn("DITA_V2_LIVE_BINGX", script)
self.assertIn("test_dita_v2_live_bingx_testnet_e2e.py", script)
self.assertIn("--dry-run", script)
playbook = Path("/mnt/dolphinng5_predict/prod/docs/DITA_V2_OPERATOR_PLAYBOOK.md").read_text()
self.assertIn("dita_v2_live_bingx_smoke.py", playbook)
self.assertIn("--dry-run", playbook)
self.assertIn("TRXUSDT", playbook)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,380 @@
from __future__ import annotations
from datetime import datetime, timezone
import os
import random
import threading
import time
import unittest
from uuid import uuid4
from prod.clean_arch.dita_v2 import (
ControlUpdate,
ExecutionKernel,
InMemoryControlPlane,
KernelCommandType,
KernelDiagnosticCode,
KernelControlSnapshot,
KernelIntent,
KernelMode,
KernelVerbosity,
MockVenueAdapter,
MockVenueScenario,
InMemoryZincPlane,
RealZincPlane,
RealZincControlPlane,
RealZincUnavailable,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
KernelEventKind,
)
from prod.clean_arch.dita_v2.real_zinc_plane import SharedRegion
HAS_REAL_ZINC = SharedRegion is not None
def mk_intent_kwargs(
*,
slot_id: int,
trade_id: str,
action: KernelCommandType,
size: float = 1.0,
leverage: float = 2.0,
side: TradeSide = TradeSide.SHORT,
price: float = 100.0,
reason: str = "FUZZ",
) -> dict[str, object]:
return {
"timestamp": datetime.now(timezone.utc),
"intent_id": f"intent-{trade_id}-{action.value}-{slot_id}",
"trade_id": trade_id,
"slot_id": slot_id,
"asset": "BTCUSDT",
"side": side,
"action": action,
"reference_price": price,
"target_size": size,
"leverage": leverage,
"exit_leg_ratios": (0.5, 0.5) if action == KernelCommandType.EXIT else (1.0,),
"reason": reason,
}
@unittest.skipUnless(HAS_REAL_ZINC, "Real Zinc adapter is unavailable")
class TestDITAv2RealZinc(unittest.TestCase):
def setUp(self) -> None:
self.prefix = f"dita_v2_{os.getpid()}_{uuid4().hex}"
self.writer = RealZincPlane(prefix=self.prefix, slot_count=3, create=True)
self.reader = RealZincPlane(prefix=self.prefix, slot_count=3, create=False)
def tearDown(self) -> None:
self.writer.close()
self.reader.close()
def _slot_dicts(self, plane: RealZincPlane) -> list[dict[str, object]]:
return [slot.to_dict() for slot in plane.read_slots()]
def test_wait_notify_and_roundtrip(self) -> None:
waiter_started = threading.Event()
waiter_result: dict[str, bool] = {"ok": False}
def _waiter() -> None:
waiter_started.set()
waiter_result["ok"] = self.reader.wait_on_state(timeout_ms=3000)
thread = threading.Thread(target=_waiter, daemon=True)
thread.start()
self.assertTrue(waiter_started.wait(timeout=2.0))
time.sleep(0.05)
kernel_slot = self.writer.read_slots()
self.assertEqual(len(kernel_slot), 3)
self.assertTrue(all(slot.fsm_state == TradeStage.IDLE for slot in kernel_slot))
self.writer.write_slot(
TradeSlot(
slot_id=0,
trade_id="trade-zinc-1",
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=1.0,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.POSITION_OPEN,
)
)
thread.join(timeout=3.0)
self.assertFalse(thread.is_alive())
self.assertTrue(waiter_result["ok"])
slots = self.reader.read_slots()
self.assertEqual(len(slots), 3)
self.assertEqual(slots[0].trade_id, "trade-zinc-1")
self.assertEqual(slots[0].fsm_state, TradeStage.POSITION_OPEN)
self.assertTrue(all(slot.fsm_state == TradeStage.IDLE for slot in slots[1:]))
def test_in_memory_wait_notify_matches_signal_semantics(self) -> None:
plane = InMemoryZincPlane()
waiter_started = threading.Event()
waiter_result: dict[str, bool] = {"ok": False}
def _waiter() -> None:
waiter_started.set()
waiter_result["ok"] = plane.wait_on_state(timeout_ms=2000)
thread = threading.Thread(target=_waiter, daemon=True)
thread.start()
self.assertTrue(waiter_started.wait(timeout=2.0))
time.sleep(0.05)
plane.write_slot(
TradeSlot(
slot_id=0,
trade_id="trade-signal",
asset="BTCUSDT",
side=TradeSide.LONG,
entry_price=101.0,
size=1.0,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.POSITION_OPEN,
)
)
thread.join(timeout=3.0)
self.assertFalse(thread.is_alive())
self.assertTrue(waiter_result["ok"])
def test_real_control_plane_roundtrip_uses_open_existing_region(self) -> None:
prefix = f"dita_v2_control_{os.getpid()}_{uuid4().hex}"
plane = RealZincPlane(prefix=prefix, slot_count=1, create=True)
control = RealZincControlPlane(prefix=prefix, create=False)
try:
snapshot = control.read()
self.assertEqual(getattr(snapshot.mode, "value", snapshot.mode), KernelMode.NORMAL.value)
self.assertEqual(getattr(snapshot.verbosity, "value", snapshot.verbosity), KernelVerbosity.QUIET.value)
updated = control.update(ControlUpdate(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE))
self.assertEqual(getattr(updated.mode, "value", updated.mode), KernelMode.DEBUG.value)
self.assertEqual(getattr(updated.verbosity, "value", updated.verbosity), KernelVerbosity.TRACE.value)
mirrored = plane.read_control()
self.assertEqual(getattr(mirrored.mode, "value", mirrored.mode), KernelMode.DEBUG.value)
self.assertEqual(getattr(mirrored.verbosity, "value", mirrored.verbosity), KernelVerbosity.TRACE.value)
finally:
control.close()
plane.close()
def test_real_control_plane_create_conflicts_with_existing_zinc_plane(self) -> None:
prefix = f"dita_v2_conflict_{os.getpid()}_{uuid4().hex}"
plane = RealZincPlane(prefix=prefix, slot_count=1, create=True)
try:
with self.assertRaises(FileExistsError):
RealZincControlPlane(prefix=prefix, create=True)
finally:
plane.close()
def test_kernel_accepts_real_control_plane_snapshot_strings(self) -> None:
prefix = f"dita_v2_kernel_real_cp_{os.getpid()}_{uuid4().hex}"
plane = RealZincPlane(prefix=prefix, slot_count=1, create=True)
control = RealZincControlPlane(prefix=prefix, create=False)
try:
kernel = ExecutionKernel(
max_slots=1,
control_plane=control,
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)),
zinc_plane=plane,
)
kernel.update_control(
ControlUpdate(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
trace_transitions=True,
)
)
outcome = kernel.process_intent(
KernelIntent(
**mk_intent_kwargs(
slot_id=0,
trade_id=f"trade-real-cp-{uuid4().hex}",
action=KernelCommandType.ENTER,
price=100.0,
size=1.0,
)
)
)
self.assertTrue(outcome.accepted)
self.assertEqual(outcome.diagnostic_code, KernelDiagnosticCode.OK)
self.assertEqual(kernel.slot(0).fsm_state, TradeStage.POSITION_OPEN)
self.assertEqual(getattr(kernel.control.mode, "value", kernel.control.mode), KernelMode.DEBUG.value)
self.assertEqual(getattr(kernel.control.verbosity, "value", kernel.control.verbosity), KernelVerbosity.TRACE.value)
finally:
control.close()
plane.close()
def test_kernel_and_zinc_fuzz_roundtrip_150_checks(self) -> None:
control = InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
)
kernel = ExecutionKernel(
max_slots=3,
control_plane=control,
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)),
zinc_plane=self.writer,
)
rng = random.Random(20260526)
for i in range(150):
slot_id = rng.randrange(0, 3)
slot = kernel.slot(slot_id)
op = rng.choice(
[
"enter",
"exit",
"mark",
"reconcile",
"control",
"event",
]
)
with self.subTest(iteration=i, slot=slot_id, op=op):
if op == "enter":
if slot.is_free():
kernel.process_intent(
KernelIntent(
**mk_intent_kwargs(
slot_id=slot_id,
trade_id=f"trade-{slot_id}-{i}",
action=KernelCommandType.ENTER,
price=100.0 + rng.random(),
size=1.0 + (rng.random() * 0.5),
leverage=1.5 + (rng.random() * 2.0),
)
)
)
elif op == "exit":
if slot.is_open():
kernel.process_intent(
KernelIntent(
**mk_intent_kwargs(
slot_id=slot_id,
trade_id=slot.trade_id,
action=KernelCommandType.EXIT,
price=99.0 + rng.random(),
size=max(0.1, slot.size or 0.1),
leverage=slot.leverage or 2.0,
)
)
)
elif op == "mark":
kernel.process_intent(
KernelIntent(
**mk_intent_kwargs(
slot_id=slot_id,
trade_id=slot.trade_id or f"trade-{slot_id}-{i}",
action=KernelCommandType.MARK_PRICE,
price=95.0 + rng.random() * 10.0,
size=max(slot.size, 1.0) if slot.size > 0 else 1.0,
leverage=slot.leverage or 2.0,
)
)
)
elif op == "reconcile":
kernel.process_intent(
KernelIntent(
**mk_intent_kwargs(
slot_id=slot_id,
trade_id=slot.trade_id or f"trade-{slot_id}-{i}",
action=KernelCommandType.RECONCILE,
price=100.0,
size=max(slot.size, 1.0) if slot.size > 0 else 1.0,
leverage=slot.leverage or 2.0,
)
)
)
elif op == "control":
kernel.update_control(
ControlUpdate(
mode=KernelMode.DEBUG if rng.random() < 0.7 else KernelMode.NORMAL,
verbosity=KernelVerbosity.TRACE if rng.random() < 0.5 else KernelVerbosity.VERBOSE,
trace_transitions=rng.random() < 0.5,
)
)
elif op == "event":
current = kernel.slot(slot_id)
if current.active_entry_order is not None:
event = VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"evt-{i}-{slot_id}",
trade_id=current.trade_id,
slot_id=slot_id,
kind=rng.choice(
[
KernelEventKind.ORDER_ACK,
KernelEventKind.PARTIAL_FILL,
KernelEventKind.FULL_FILL,
]
),
status=rng.choice(
[
VenueEventStatus.ACKED,
VenueEventStatus.PARTIALLY_FILLED,
VenueEventStatus.FILLED,
]
),
venue_order_id=current.active_entry_order.venue_order_id,
venue_client_id=current.active_entry_order.venue_client_id,
side=current.side,
asset=current.asset,
price=current.entry_price or 100.0,
size=current.size or 1.0,
filled_size=current.size or 1.0,
remaining_size=0.0,
)
kernel.on_venue_event(event)
elif current.active_exit_order is not None:
event = VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"evt-{i}-{slot_id}",
trade_id=current.trade_id,
slot_id=slot_id,
kind=rng.choice(
[
KernelEventKind.PARTIAL_FILL,
KernelEventKind.FULL_FILL,
KernelEventKind.CANCEL_ACK,
KernelEventKind.CANCEL_REJECT,
]
),
status=rng.choice(
[
VenueEventStatus.PARTIALLY_FILLED,
VenueEventStatus.FILLED,
VenueEventStatus.CANCELED,
VenueEventStatus.CANCELED_REJECTED,
]
),
venue_order_id=current.active_exit_order.venue_order_id,
venue_client_id=current.active_exit_order.venue_client_id,
side=current.side,
asset=current.asset,
price=current.entry_price or 100.0,
size=current.size or 1.0,
filled_size=min(current.size or 1.0, 0.5),
remaining_size=max(0.0, (current.size or 1.0) - 0.5),
)
kernel.on_venue_event(event)
writer_slots = self._slot_dicts(self.writer)
reader_slots = self._slot_dicts(self.reader)
kernel_slots = [slot.to_dict() for slot in kernel.state.slots]
self.assertEqual(writer_slots, reader_slots)
self.assertEqual(reader_slots, kernel_slots)
self.assertEqual(self.reader.read_control().mode, kernel.control.mode)
self.assertEqual(self.reader.read_control().verbosity, kernel.control.verbosity)
self.assertEqual(len(self.reader.read_intents()), len(self.writer.read_intents()))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,313 @@
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass, field
from typing import Dict, List
import math
import random
import pytest
EPS = 1e-9
@dataclass
class ExitLeg:
trade_id: str
chain_root_trade_id: str
exit_seq: int
exit_leg_id: str
chain_prev_leg_id: str
chain_head_leg_id: str
command_id: str
fraction: float
qty: float
exit_price: float
fee: float
net_pnl: float
remaining_after: float
reason: str
chain_token: str
@dataclass
class ParentTrade:
trade_id: str
side: str # SHORT | LONG
entry_price: float
entry_qty: float
remaining_qty: float
realized_pnl_total: float = 0.0
realized_fees_total: float = 0.0
exit_seq: int = 0
status: str = "OPEN" # OPEN | PARTIALLY_CLOSED | CLOSED
version: int = 0
legs: List[ExitLeg] = field(default_factory=list)
chain_root_trade_id: str = ""
chain_head_leg_id: str = ""
chain_prev_leg_id: str = ""
chain_token: str = ""
def _chain_token(payload: dict) -> str:
return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str).encode()).hexdigest()
class MiniRetractionRuntime:
"""
Contract-reference runtime:
- all partial exits route through one handler
- idempotent by command_id
- financial accumulation by executed leg
"""
def __init__(self, fee_rate: float = 0.00055):
self.fee_rate = fee_rate
self.capital = 25_000.0
self.trades: Dict[str, ParentTrade] = {}
self.applied_commands: Dict[str, ExitLeg] = {}
def open_trade(self, trade_id: str, side: str, entry_price: float, qty: float) -> None:
assert trade_id not in self.trades
t = ParentTrade(
trade_id=trade_id,
side=side,
entry_price=entry_price,
entry_qty=qty,
remaining_qty=qty,
)
t.chain_root_trade_id = trade_id
t.chain_head_leg_id = f"{trade_id}:open"
t.chain_prev_leg_id = ""
t.chain_token = _chain_token({
"trade_id": trade_id,
"chain_root_trade_id": trade_id,
"chain_head_leg_id": t.chain_head_leg_id,
"chain_prev_leg_id": "",
"chain_seq": 0,
"side": side,
"entry_price": entry_price,
"entry_qty": qty,
"remaining_qty": qty,
"realized_pnl_total": 0.0,
"realized_fees_total": 0.0,
})
self.trades[trade_id] = t
def retract(
self,
trade_id: str,
*,
command_id: str,
fraction: float,
exit_price: float,
reason: str,
) -> ExitLeg | None:
if command_id in self.applied_commands:
return self.applied_commands[command_id]
if not (0 < fraction <= 1.0):
return None
t = self.trades.get(trade_id)
if not t or t.status == "CLOSED":
return None
expected = _chain_token({
"trade_id": t.trade_id,
"chain_root_trade_id": t.chain_root_trade_id or t.trade_id,
"chain_head_leg_id": t.chain_head_leg_id or f"{t.trade_id}:open",
"chain_prev_leg_id": t.chain_prev_leg_id or "",
"chain_seq": t.exit_seq,
"side": t.side,
"entry_price": t.entry_price,
"entry_qty": t.entry_qty,
"remaining_qty": t.remaining_qty,
"realized_pnl_total": t.realized_pnl_total,
"realized_fees_total": t.realized_fees_total,
})
if t.chain_token and t.chain_token != expected:
return None
requested_qty = t.remaining_qty * fraction
qty = min(max(requested_qty, 0.0), t.remaining_qty)
if qty <= EPS:
return None
sign = -1.0 if t.side == "SHORT" else 1.0
gross = sign * (exit_price - t.entry_price) * qty
fee = self.fee_rate * (t.entry_price * qty + exit_price * qty)
net = gross - fee
t.exit_seq += 1
t.version += 1
t.remaining_qty = max(0.0, t.remaining_qty - qty)
t.realized_pnl_total += net
t.realized_fees_total += fee
self.capital += net
t.status = "CLOSED" if t.remaining_qty <= EPS else "PARTIALLY_CLOSED"
prev_head = t.chain_head_leg_id or f"{t.trade_id}:open"
t.chain_prev_leg_id = prev_head
t.chain_head_leg_id = f"{t.trade_id}:x{t.exit_seq:03d}"
t.chain_token = _chain_token({
"trade_id": t.trade_id,
"chain_root_trade_id": t.chain_root_trade_id or t.trade_id,
"chain_head_leg_id": t.chain_head_leg_id,
"chain_prev_leg_id": prev_head,
"chain_seq": t.exit_seq,
"side": t.side,
"entry_price": t.entry_price,
"entry_qty": t.entry_qty,
"remaining_qty": t.remaining_qty,
"realized_pnl_total": t.realized_pnl_total,
"realized_fees_total": t.realized_fees_total,
})
leg = ExitLeg(
trade_id=t.trade_id,
chain_root_trade_id=t.chain_root_trade_id or t.trade_id,
exit_seq=t.exit_seq,
exit_leg_id=f"{t.trade_id}:x{t.exit_seq:03d}",
chain_prev_leg_id=prev_head,
chain_head_leg_id=t.chain_head_leg_id,
command_id=command_id,
fraction=fraction,
qty=qty,
exit_price=exit_price,
fee=fee,
net_pnl=net,
remaining_after=t.remaining_qty,
reason=reason,
chain_token=t.chain_token,
)
t.legs.append(leg)
self.applied_commands[command_id] = leg
return leg
def _assert_parent_invariants(t: ParentTrade) -> None:
total_qty = sum(l.qty for l in t.legs)
assert total_qty <= t.entry_qty + EPS
assert math.isclose(t.remaining_qty, max(0.0, t.entry_qty - total_qty), abs_tol=1e-8)
assert math.isclose(t.realized_pnl_total, sum(l.net_pnl for l in t.legs), rel_tol=0, abs_tol=1e-8)
assert math.isclose(t.realized_fees_total, sum(l.fee for l in t.legs), rel_tol=0, abs_tol=1e-8)
if t.legs:
assert t.chain_head_leg_id == t.legs[-1].exit_leg_id
assert t.chain_token == t.legs[-1].chain_token
if t.remaining_qty <= EPS:
assert t.status == "CLOSED"
elif t.legs:
assert t.status == "PARTIALLY_CLOSED"
else:
assert t.status == "OPEN"
def test_retract_default_half_then_close_preserves_lineage_and_math() -> None:
rt = MiniRetractionRuntime()
rt.open_trade("T1", "SHORT", 100.0, 10.0)
l1 = rt.retract("T1", command_id="c1", fraction=0.5, exit_price=99.0, reason="HOTKEY_RETRACT")
assert l1 is not None
assert l1.exit_leg_id == "T1:x001"
assert l1.qty == 5.0
assert l1.chain_prev_leg_id == "T1:open"
assert l1.chain_head_leg_id == "T1:x001"
l2 = rt.retract("T1", command_id="c2", fraction=1.0, exit_price=98.5, reason="HOTKEY_RETRACT")
assert l2 is not None
assert l2.exit_leg_id == "T1:x002"
assert l2.qty == 5.0
assert l2.chain_prev_leg_id == "T1:x001"
assert l2.chain_head_leg_id == "T1:x002"
t = rt.trades["T1"]
_assert_parent_invariants(t)
assert t.status == "CLOSED"
assert t.exit_seq == 2
def test_idempotent_command_does_not_double_execute() -> None:
rt = MiniRetractionRuntime()
rt.open_trade("T2", "SHORT", 50.0, 20.0)
first = rt.retract("T2", command_id="dup", fraction=0.5, exit_price=49.8, reason="V7_RETRACT")
second = rt.retract("T2", command_id="dup", fraction=0.5, exit_price=49.7, reason="V7_RETRACT")
assert first is not None
assert second is not None
assert first.exit_leg_id == second.exit_leg_id
assert len(rt.trades["T2"].legs) == 1
_assert_parent_invariants(rt.trades["T2"])
def test_invalid_fraction_rejected() -> None:
rt = MiniRetractionRuntime()
rt.open_trade("T3", "SHORT", 10.0, 10.0)
assert rt.retract("T3", command_id="a", fraction=0.0, exit_price=9.9, reason="HOTKEY_RETRACT") is None
assert rt.retract("T3", command_id="b", fraction=1.5, exit_price=9.9, reason="HOTKEY_RETRACT") is None
assert len(rt.trades["T3"].legs) == 0
_assert_parent_invariants(rt.trades["T3"])
def test_long_side_accounting_sign_is_correct() -> None:
rt = MiniRetractionRuntime()
rt.open_trade("T4", "LONG", 100.0, 2.0)
leg = rt.retract("T4", command_id="c", fraction=1.0, exit_price=101.0, reason="HOTKEY_RETRACT")
assert leg is not None
assert leg.net_pnl > 0
_assert_parent_invariants(rt.trades["T4"])
def test_over_many_random_partial_exits_invariants_hold() -> None:
rng = random.Random(1337)
rt = MiniRetractionRuntime()
rt.open_trade("T5", "SHORT", 120.0, 30.0)
for i in range(200):
t = rt.trades["T5"]
if t.status == "CLOSED":
break
# Biased toward smaller retractions; occasionally force full close
frac = 1.0 if i % 37 == 0 else max(0.01, min(0.99, rng.random() * 0.6))
px = 120.0 - rng.random() * 2.0
rt.retract("T5", command_id=f"cmd-{i}", fraction=frac, exit_price=px, reason="V7_RETRACT")
_assert_parent_invariants(rt.trades["T5"])
t = rt.trades["T5"]
# Must eventually close under forced 1.0 fractions
assert t.status == "CLOSED"
_assert_parent_invariants(t)
def test_capital_updates_are_leg_immediate() -> None:
rt = MiniRetractionRuntime()
start = rt.capital
rt.open_trade("T6", "SHORT", 200.0, 4.0)
l1 = rt.retract("T6", command_id="r1", fraction=0.5, exit_price=199.0, reason="HOTKEY_RETRACT")
assert l1 is not None
mid = rt.capital
assert not math.isclose(mid, start, abs_tol=1e-12)
l2 = rt.retract("T6", command_id="r2", fraction=1.0, exit_price=198.5, reason="HOTKEY_RETRACT")
assert l2 is not None
assert math.isclose(rt.capital, start + l1.net_pnl + l2.net_pnl, rel_tol=0, abs_tol=1e-8)
def test_command_on_closed_trade_is_noop() -> None:
rt = MiniRetractionRuntime()
rt.open_trade("T7", "SHORT", 100.0, 1.0)
rt.retract("T7", command_id="x1", fraction=1.0, exit_price=99.7, reason="HOTKEY_RETRACT")
t = rt.trades["T7"]
assert t.status == "CLOSED"
n = len(t.legs)
out = rt.retract("T7", command_id="x2", fraction=0.5, exit_price=99.6, reason="HOTKEY_RETRACT")
assert out is None
assert len(t.legs) == n
_assert_parent_invariants(t)
def test_tampered_chain_head_is_rejected() -> None:
rt = MiniRetractionRuntime()
rt.open_trade("T8", "SHORT", 75.0, 8.0)
first = rt.retract("T8", command_id="c1", fraction=0.5, exit_price=74.5, reason="HOTKEY_RETRACT")
assert first is not None
rt.trades["T8"].chain_head_leg_id = "T8:x999"
second = rt.retract("T8", command_id="c2", fraction=0.5, exit_price=74.0, reason="HOTKEY_RETRACT")
assert second is None
with pytest.raises(AssertionError):
_assert_parent_invariants(rt.trades["T8"])

View File

@@ -0,0 +1,202 @@
from __future__ import annotations
import json
import random
import threading
from dataclasses import dataclass
import importlib.util
from pathlib import Path
import pytest
_MOD_PATH = Path("/mnt/dolphinng5_predict/prod/nautilus_event_trader.py")
_SPEC = importlib.util.spec_from_file_location("nautilus_event_trader_mod", _MOD_PATH)
assert _SPEC and _SPEC.loader
mod = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(mod) # type: ignore[arg-type]
@dataclass
class _Pos:
trade_id: str
asset: str
entry_price: float
notional: float
current_price: float = 0.0
pnl_pct: float = 0.0
class _ExitMgr:
def __init__(self):
self._positions: dict[str, dict] = {}
class _Eng:
def __init__(self, pos: _Pos | None):
self.position = pos
self.capital = 25_000.0
self.exit_manager = _ExitMgr()
if pos:
self.exit_manager._positions[pos.trade_id] = {"dummy": True}
class _Map:
def __init__(self):
self._d = {"blue_runtime_commands": "[]"}
self._lock = threading.Lock()
def blocking(self):
return self
def get(self, key):
with self._lock:
return self._d.get(key)
def put(self, key, val):
with self._lock:
self._d[key] = val
class _F:
def add_done_callback(self, _cb):
return None
return _F()
def _mk_trader():
t = object.__new__(mod.DolphinLiveTrader)
t.eng_lock = threading.Lock()
t.control_map = _Map()
t._processed_retract_commands = mod.deque(maxlen=5000)
t._processed_retract_set = set()
t._pending_entries = {}
t.current_day = "2026-05-12"
t.bar_idx = 100
return t
def _install_open_position(t, *, trade_id="T", asset="STXUSDT", entry_price=1.0, notional=1000.0):
p = _Pos(trade_id, asset, entry_price, notional, current_price=entry_price)
t.eng = _Eng(p)
t._pending_entries[trade_id] = {
"trade_id": trade_id,
"asset": asset,
"side": "SHORT",
"entry_price": entry_price,
"entry_bar": 90,
"entry_date": "2026-05-12",
"notional": notional,
"notional_entry": notional,
"retraction_legs": 0,
"realized_pnl_legs_total": 0.0,
}
t._pending_entries[trade_id].update(t._chain_state_for_pending(
trade_id,
t._pending_entries[trade_id],
chain_mode="LIVE",
chain_head_leg_id=f"{trade_id}:open",
chain_prev_leg_id="",
chain_seq=0,
))
def test_fuzz_retraction_invariants_hold_under_random_command_stream(monkeypatch):
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
rng = random.Random(20260512)
t = _mk_trader()
_install_open_position(t, trade_id="T-FUZZ", asset="STXUSDT", entry_price=1.0, notional=10_000.0)
seen_ids: set[str] = set()
baseline_cap = t.eng.capital
for i in range(2500):
if t.eng.position is None:
break
px = max(0.00001, 1.0 + rng.uniform(-0.25, 0.25))
# Mix valid and invalid commands.
frac_choice = rng.choice([
rng.uniform(0.01, 1.0), # valid
0.0, # invalid
-0.1, # invalid
1.2, # invalid
])
# inject duplicate ids often
if i > 0 and rng.random() < 0.2:
cid = rng.choice(tuple(seen_ids)) if seen_ids else f"c-{i}"
else:
cid = f"c-{i}-{rng.randint(0, 999)}"
seen_ids.add(cid)
# wrong trade ids sometimes
tid = "T-FUZZ" if rng.random() < 0.8 else f"OTHER-{i}"
pending = t._pending_entries["T-FUZZ"]
cmd = {
"command_id": cid,
"trade_id": tid,
"action": "RETRACT",
"fraction": frac_choice,
"reason": "HOTKEY_RETRACT",
"source": "fuzz",
"chain_root_trade_id": pending["chain_root_trade_id"],
"chain_head_leg_id": pending["chain_head_leg_id"],
"chain_prev_leg_id": pending["chain_prev_leg_id"],
"chain_seq": pending["chain_seq"],
"chain_token": pending["chain_token"],
}
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
t._process_runtime_commands({"STXUSDT": px})
if t.eng.position is not None:
n = float(t.eng.position.notional)
assert n >= -1e-8
# never exceed original notional
assert n <= 10_000.0 + 1e-8
p = t._pending_entries["T-FUZZ"]
assert int(p.get("retraction_legs", 0) or 0) >= 0
# Capital must stay finite and deterministic.
assert t.eng.capital == pytest.approx(float(t.eng.capital))
assert abs(t.eng.capital - baseline_cap) < 1e7
def test_fuzz_concurrent_queue_submission_and_drain(monkeypatch):
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
rng = random.Random(777)
t = _mk_trader()
_install_open_position(t, trade_id="T-RACE", asset="DASHUSDT", entry_price=10.0, notional=5000.0)
def producer(start: int, count: int):
for i in range(start, start + count):
with t.control_map._lock:
raw = t.control_map._d.get("blue_runtime_commands", "[]")
q = json.loads(raw) if raw else []
q.append({
"command_id": f"p-{i}",
"trade_id": "T-RACE" if rng.random() < 0.9 else "OTHER",
"action": "RETRACT",
"fraction": rng.uniform(0.01, 1.0),
"reason": "HOTKEY_RETRACT",
"source": "race",
"chain_root_trade_id": t._pending_entries["T-RACE"]["chain_root_trade_id"],
"chain_head_leg_id": t._pending_entries["T-RACE"]["chain_head_leg_id"],
"chain_prev_leg_id": t._pending_entries["T-RACE"]["chain_prev_leg_id"],
"chain_seq": t._pending_entries["T-RACE"]["chain_seq"],
"chain_token": t._pending_entries["T-RACE"]["chain_token"],
})
t.control_map._d["blue_runtime_commands"] = json.dumps(q[-200:])
threads = [threading.Thread(target=producer, args=(k * 120, 120)) for k in range(4)]
for th in threads:
th.start()
for th in threads:
th.join()
# Drain repeatedly; must not throw and must preserve invariants.
for _ in range(50):
if t.eng.position is None:
break
t._process_runtime_commands({"DASHUSDT": rng.uniform(8.0, 12.0)})
if t.eng.position is not None:
assert t.eng.position.notional >= -1e-8
assert t.eng.position.notional <= 5000.0 + 1e-8

View File

@@ -0,0 +1,394 @@
from __future__ import annotations
import json
import threading
import tempfile
from dataclasses import dataclass
import importlib.util
from pathlib import Path
import pytest
_MOD_PATH = Path("/mnt/dolphinng5_predict/prod/nautilus_event_trader.py")
_SPEC = importlib.util.spec_from_file_location("nautilus_event_trader_mod", _MOD_PATH)
assert _SPEC and _SPEC.loader
mod = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(mod) # type: ignore[arg-type]
@dataclass
class _Pos:
trade_id: str
asset: str
entry_price: float
notional: float
current_price: float = 0.0
pnl_pct: float = 0.0
class _ExitMgr:
def __init__(self) -> None:
self._positions: dict[str, dict] = {}
class _Eng:
def __init__(self, pos: _Pos | None, capital: float = 25_000.0) -> None:
self.position = pos
self.capital = capital
self.exit_manager = _ExitMgr()
if pos is not None:
self.exit_manager._positions[pos.trade_id] = {"dummy": True}
class _Map:
def __init__(self, initial: dict | None = None) -> None:
self._d = dict(initial or {})
self._lock = threading.Lock()
def blocking(self):
return self
def get(self, key):
with self._lock:
return self._d.get(key)
def put(self, key, val):
with self._lock:
self._d[key] = val
class _F:
def add_done_callback(self, _cb):
return None
return _F()
def _mk_trader() -> mod.DolphinLiveTrader:
t = object.__new__(mod.DolphinLiveTrader)
tmpdir = Path(tempfile.mkdtemp(prefix="dolphin_retract_test_"))
mod.CAPITAL_DISK_CHECKPOINT = tmpdir / "capital_checkpoint.json"
mod.CAPITAL_CORRECTIVE_REPLAY = tmpdir / "capital_replay.json"
mod.CAPITAL_UPDATE_LEDGER = tmpdir / "capital_update_ledger.json"
t.eng_lock = threading.Lock()
t.state_map = _Map({})
t.pnl_map = _Map({})
t.control_map = _Map({"blue_runtime_commands": "[]"})
t._processed_retract_commands = mod.deque(maxlen=5000)
t._processed_retract_set = set()
t._pending_entries = {}
t.current_day = "2026-05-12"
t.bar_idx = 100
return t
def _seed_chain(t: mod.DolphinLiveTrader, trade_id: str) -> None:
pending = t._pending_entries[trade_id]
pending.update(t._chain_state_for_pending(
trade_id,
pending,
chain_mode="LIVE",
chain_head_leg_id=f"{trade_id}:open",
chain_prev_leg_id="",
chain_seq=0,
))
def _retract_cmd(t: mod.DolphinLiveTrader, trade_id: str, *, command_id: str, fraction: float, reason: str) -> dict:
pending = t._pending_entries[trade_id]
return {
"command_id": command_id,
"trade_id": trade_id,
"action": "RETRACT",
"fraction": fraction,
"reason": reason,
"source": "tui_hotkey",
"chain_root_trade_id": pending["chain_root_trade_id"],
"chain_head_leg_id": pending["chain_head_leg_id"],
"chain_prev_leg_id": pending["chain_prev_leg_id"],
"chain_seq": pending["chain_seq"],
"chain_token": pending["chain_token"],
}
def test_runtime_command_partial_exit_updates_position_and_capital(monkeypatch):
rows = []
monkeypatch.setattr(mod, "ch_put", lambda table, row: rows.append((table, row)))
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123456789)
t = _mk_trader()
pos = _Pos("T-1", "STXUSDT", 1.0, 1000.0, current_price=0.95)
t.eng = _Eng(pos, capital=25000.0)
t._pending_entries["T-1"] = {
"asset": "STXUSDT",
"side": "SHORT",
"entry_price": 1.0,
"entry_bar": 90,
"entry_date": "2026-05-12",
"notional": 1000.0,
"notional_entry": 1000.0,
"retraction_legs": 0,
"realized_pnl_legs_total": 0.0,
}
_seed_chain(t, "T-1")
cmd = _retract_cmd(t, "T-1", command_id="c-1", fraction=0.5, reason="HOTKEY_RETRACT")
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
forced = t._process_runtime_commands({"STXUSDT": 0.95})
assert forced is None
assert t.eng.position is not None
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 500.0
assert t._pending_entries["T-1"]["retraction_legs"] == 1
assert pytest.approx(t._pending_entries["T-1"]["realized_pnl_legs_total"], abs=1e-9) == 25.0
assert pytest.approx(t.eng.capital, abs=1e-9) == 25025.0
assert any(tbl == "trade_exit_legs" for tbl, _ in rows)
recon_rows = [r for tbl, r in rows if tbl == "trade_reconstruction"]
assert recon_rows
assert any(json.loads(r["payload_json"]).get("chain", {}).get("chain_token") for r in recon_rows)
assert any(tbl == "hotkey_audit" and r["result"] == "PARTIAL_OK" for tbl, r in rows)
stale_cmd = dict(cmd)
stale_cmd["command_id"] = "c-1-stale"
t.control_map.put("blue_runtime_commands", json.dumps([stale_cmd]))
t._process_runtime_commands({"STXUSDT": 0.94})
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 500.0
assert any(tbl == "hotkey_audit" and "CHAIN_MISMATCH" in r["result"] for tbl, r in rows)
def test_runtime_command_full_close_returns_forced_exit(monkeypatch):
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("T-2", "FETUSDT", 2.0, 200.0, current_price=1.9)
t.eng = _Eng(pos, capital=1000.0)
t._pending_entries["T-2"] = {
"asset": "FETUSDT",
"side": "SHORT",
"entry_price": 2.0,
"entry_bar": 95,
"entry_date": "2026-05-12",
"notional": 200.0,
"notional_entry": 200.0,
"retraction_legs": 0,
"realized_pnl_legs_total": 0.0,
}
_seed_chain(t, "T-2")
cmd = _retract_cmd(t, "T-2", command_id="c-2", fraction=1.0, reason="HOTKEY_RETRACT")
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
forced = t._process_runtime_commands({"FETUSDT": 1.9})
assert forced is not None
assert forced["trade_id"] == "T-2"
assert forced["reason"] == "HOTKEY_RETRACT"
assert forced["capital_already_realized"] is True
assert forced["economic_pnl"] == pytest.approx(forced["net_pnl"], abs=1e-12)
assert forced["economic_pnl_pct"] == pytest.approx(forced["pnl_pct"], abs=1e-12)
assert t.eng.position is None
assert "T-2" not in t.eng.exit_manager._positions
def test_full_retract_close_path_does_not_double_apply_capital(monkeypatch):
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
t.state_map = _Map({})
t.pnl_map = _Map({})
pos = _Pos("T-2B", "FETUSDT", 2.0, 200.0, current_price=1.9)
t.eng = _Eng(pos, capital=1000.0)
t._pending_entries["T-2B"] = {
"asset": "FETUSDT",
"side": "SHORT",
"entry_price": 2.0,
"entry_bar": 95,
"entry_date": "2026-05-12",
"notional": 200.0,
"notional_entry": 200.0,
"quantity": 100.0,
"retraction_legs": 0,
"realized_pnl_legs_total": 0.0,
}
_seed_chain(t, "T-2B")
cmd = _retract_cmd(t, "T-2B", command_id="c-2b", fraction=1.0, reason="HOTKEY_RETRACT")
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
forced = t._process_runtime_commands({"FETUSDT": 1.9})
assert forced is not None
# First accounting application happened in retract leg.
assert t.eng.capital == pytest.approx(1010.0, abs=1e-9)
pending = t._pending_entries["T-2B"]
realized_pnl, realized_source = t._resolved_realized_trade_pnl(pending, forced, exit_price=1.9)
assert realized_source == "net_pnl"
assert realized_pnl == pytest.approx(10.0, abs=1e-9)
# Close-path accounting must be suppressed because leg accounting already realized pnl.
cap_delta, cap_source = t._resolved_capital_apply_pnl(forced, realized_pnl)
assert cap_source == "already_realized"
assert cap_delta == pytest.approx(0.0, abs=1e-12)
cap_before, cap_after = t._apply_trade_capital_update(
cap_delta,
reason="HOTKEY_RETRACT",
source="trade_close",
trade_id="T-2B",
asset="FETUSDT",
)
assert cap_before == pytest.approx(1010.0, abs=1e-9)
assert cap_after == pytest.approx(1010.0, abs=1e-9)
assert t.eng.capital == pytest.approx(1010.0, abs=1e-9)
def test_idempotent_replay_is_noop(monkeypatch):
rows = []
monkeypatch.setattr(mod, "ch_put", lambda table, row: rows.append((table, row)))
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("T-3", "DASHUSDT", 10.0, 1000.0, current_price=9.5)
t.eng = _Eng(pos, capital=5000.0)
t._pending_entries["T-3"] = {
"asset": "DASHUSDT",
"side": "SHORT",
"entry_price": 10.0,
"entry_bar": 90,
"entry_date": "2026-05-12",
"notional": 1000.0,
"notional_entry": 1000.0,
"retraction_legs": 0,
"realized_pnl_legs_total": 0.0,
}
_seed_chain(t, "T-3")
cmd = _retract_cmd(t, "T-3", command_id="dup", fraction=0.5, reason="HOTKEY_RETRACT")
t.control_map.put("blue_runtime_commands", json.dumps([cmd, cmd]))
t._process_runtime_commands({"DASHUSDT": 9.5})
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 500.0
replays = [r for tbl, r in rows if tbl == "hotkey_audit" and r.get("result") == "IDEMPOTENT_REPLAY"]
assert replays
def test_idempotent_replay_does_not_change_capital(monkeypatch):
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("T-3B", "DASHUSDT", 10.0, 1000.0, current_price=9.5)
t.eng = _Eng(pos, capital=5000.0)
t._pending_entries["T-3B"] = {
"asset": "DASHUSDT",
"side": "SHORT",
"entry_price": 10.0,
"entry_bar": 90,
"entry_date": "2026-05-12",
"notional": 1000.0,
"notional_entry": 1000.0,
"retraction_legs": 0,
"realized_pnl_legs_total": 0.0,
}
_seed_chain(t, "T-3B")
cmd = _retract_cmd(t, "T-3B", command_id="dup-2", fraction=0.5, reason="HOTKEY_RETRACT")
t.control_map.put("blue_runtime_commands", json.dumps([cmd, cmd]))
t._process_runtime_commands({"DASHUSDT": 9.5})
cap_after_first = t.eng.capital
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
t._process_runtime_commands({"DASHUSDT": 9.5})
assert t.eng.capital == pytest.approx(cap_after_first, abs=1e-9)
def test_trade_id_mismatch_is_rejected_and_position_unchanged(monkeypatch):
rows = []
monkeypatch.setattr(mod, "ch_put", lambda table, row: rows.append((table, row)))
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("T-4", "STXUSDT", 1.0, 1000.0, current_price=1.01)
t.eng = _Eng(pos, capital=1000.0)
t._pending_entries["T-4"] = {"asset": "STXUSDT", "side": "SHORT", "entry_price": 1.0, "entry_bar": 80, "entry_date": "2026-05-12"}
_seed_chain(t, "T-4")
cmd = {"command_id": "bad", "trade_id": "OTHER", "action": "RETRACT", "fraction": 0.5, "reason": "HOTKEY_RETRACT", "chain_root_trade_id": "OTHER", "chain_head_leg_id": "OTHER:open", "chain_prev_leg_id": "", "chain_seq": 0, "chain_token": "stale"}
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
t._process_runtime_commands({"STXUSDT": 1.01})
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 1000.0
assert any(tbl == "hotkey_audit" and "TRADE_MISMATCH" in r["result"] for tbl, r in rows)
def test_command_queue_drained_atomically(monkeypatch):
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("T-5", "LINKUSDT", 10.0, 1000.0, current_price=9.8)
t.eng = _Eng(pos, capital=500.0)
t._pending_entries["T-5"] = {"asset": "LINKUSDT", "side": "SHORT", "entry_price": 10.0, "entry_bar": 88, "entry_date": "2026-05-12"}
_seed_chain(t, "T-5")
cmds = [
_retract_cmd(t, "T-5", command_id="a", fraction=0.25, reason="HOTKEY_RETRACT"),
_retract_cmd(t, "T-5", command_id="b", fraction=0.25, reason="HOTKEY_RETRACT"),
]
t.control_map.put("blue_runtime_commands", json.dumps(cmds))
t._process_runtime_commands({"LINKUSDT": 9.8})
assert t.control_map.get("blue_runtime_commands") == "[]"
def test_bad_fraction_rejected(monkeypatch):
rows = []
monkeypatch.setattr(mod, "ch_put", lambda table, row: rows.append((table, row)))
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("T-6", "SOLUSDT", 100.0, 1000.0, current_price=95.0)
t.eng = _Eng(pos, capital=1000.0)
t._pending_entries["T-6"] = {"asset": "SOLUSDT", "side": "SHORT", "entry_price": 100.0, "entry_bar": 80, "entry_date": "2026-05-12"}
_seed_chain(t, "T-6")
cmd = {"command_id": "badfrac", "trade_id": "T-6", "action": "RETRACT", "fraction": 0.0, "reason": "HOTKEY_RETRACT", "chain_root_trade_id": t._pending_entries["T-6"]["chain_root_trade_id"], "chain_head_leg_id": t._pending_entries["T-6"]["chain_head_leg_id"], "chain_prev_leg_id": t._pending_entries["T-6"]["chain_prev_leg_id"], "chain_seq": t._pending_entries["T-6"]["chain_seq"], "chain_token": t._pending_entries["T-6"]["chain_token"]}
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
t._process_runtime_commands({"SOLUSDT": 95.0})
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 1000.0
assert any(tbl == "hotkey_audit" and r["result"] == "BAD_FRACTION" for tbl, r in rows)
def test_retract_with_missing_price_falls_back_to_entry_and_keeps_capital(monkeypatch):
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("T-6B", "SOLUSDT", 100.0, 1000.0, current_price=0.0)
t.eng = _Eng(pos, capital=1000.0)
t._pending_entries["T-6B"] = {
"asset": "SOLUSDT",
"side": "SHORT",
"entry_price": 100.0,
"entry_bar": 80,
"entry_date": "2026-05-12",
"notional": 1000.0,
"notional_entry": 1000.0,
"retraction_legs": 0,
"realized_pnl_legs_total": 0.0,
}
_seed_chain(t, "T-6B")
cmd = _retract_cmd(t, "T-6B", command_id="c-6b", fraction=0.5, reason="HOTKEY_RETRACT")
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
t._process_runtime_commands({})
assert t.eng.capital == pytest.approx(1000.0, abs=1e-9)
def test_multi_slot_future_safety_non_target_commands_do_not_mutate_open_slot(monkeypatch):
"""
Future-proof guard: if multiple slot commands exist, only matching trade_id may mutate current open position.
"""
rows = []
monkeypatch.setattr(mod, "ch_put", lambda table, row: rows.append((table, row)))
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("ACTIVE", "ATOMUSDT", 5.0, 500.0, current_price=4.9)
t.eng = _Eng(pos, capital=1000.0)
t._pending_entries["ACTIVE"] = {"asset": "ATOMUSDT", "side": "SHORT", "entry_price": 5.0, "entry_bar": 99, "entry_date": "2026-05-12"}
_seed_chain(t, "ACTIVE")
cmds = [
{"command_id": "x1", "trade_id": "INACTIVE", "action": "RETRACT", "fraction": 1.0, "reason": "HOTKEY_RETRACT", "chain_root_trade_id": "INACTIVE", "chain_head_leg_id": "INACTIVE:open", "chain_prev_leg_id": "", "chain_seq": 0, "chain_token": "stale"},
_retract_cmd(t, "ACTIVE", command_id="x2", fraction=0.5, reason="HOTKEY_RETRACT"),
]
t.control_map.put("blue_runtime_commands", json.dumps(cmds))
t._process_runtime_commands({"ATOMUSDT": 4.9})
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 250.0
assert any(tbl == "hotkey_audit" and "TRADE_MISMATCH" in r["result"] for tbl, r in rows)
assert any(tbl == "hotkey_audit" and r["result"] == "PARTIAL_OK" for tbl, r in rows)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,326 @@
"""PINK ClickHouse persistence tests — DITAv2 outcome + slot_dict API."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from types import SimpleNamespace
from prod.clean_arch.dita_v2.contracts import TradeStage as DitaTradeStage
from prod.clean_arch.dita import (
AccountProjection,
AccountSnapshot,
Decision,
DecisionAction,
Intent,
TradeSide,
TradeStage,
)
from prod.clean_arch.dita_v2.contracts import (
KernelDiagnosticCode,
KernelOutcome,
KernelSeverity,
KernelTransition,
)
from prod.clean_arch.persistence.pink_clickhouse import PinkClickHousePersistence
@dataclass
class _Sink:
calls: list[tuple[str, dict]] = field(default_factory=list)
def __call__(self, table: str, row: dict) -> None:
self.calls.append((table, row))
def _make_snapshot():
return SimpleNamespace(
timestamp=datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc),
symbol="BTCUSDT",
price=100.0,
)
def _make_decision(action: DecisionAction) -> Decision:
return Decision(
timestamp=datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc),
decision_id="BTCUSDT-D-000000000001",
asset="BTCUSDT",
action=action,
side=TradeSide.SHORT,
reason="STRUCTURAL_DISLOCATION" if action == DecisionAction.ENTER else "TAKE_PROFIT",
confidence=0.9,
velocity_divergence=-0.12,
irp_alignment=0.8,
reference_price=100.0,
target_size=1.0,
leverage=2.0,
bars_held=0,
stage=TradeStage.ORDER_REQUESTED,
metadata={},
)
def _make_intent(action: DecisionAction) -> Intent:
return Intent(
timestamp=datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc),
trade_id="BTCUSDT-T-000000000001",
decision_id="BTCUSDT-D-000000000001",
asset="BTCUSDT",
action=action,
side=TradeSide.SHORT,
reason="STRUCTURAL_DISLOCATION" if action == DecisionAction.ENTER else "TAKE_PROFIT",
target_size=1.0,
leverage=2.0,
reference_price=100.0,
confidence=0.9,
bars_held=0,
exit_leg_ratios=(0.5, 1.0),
metadata={"exit_ratio": 0.5},
)
def _make_account() -> AccountProjection:
return AccountProjection(
runtime_namespace="pink",
strategy_namespace="pink",
event_namespace="pink",
actor_name="PinkDirectRuntime",
exec_venue="bingx",
data_venue="binance",
ledger_authority="exchange",
snapshot=AccountSnapshot(capital=25_000.0, equity=25_000.0),
)
def _make_outcome(
accepted: bool = True,
code: KernelDiagnosticCode = KernelDiagnosticCode.OK,
) -> KernelOutcome:
return KernelOutcome(
accepted=accepted,
slot_id=0,
trade_id="BTCUSDT-T-000000000001",
state=DitaTradeStage.POSITION_OPEN,
diagnostic_code=code,
severity=KernelSeverity.INFO,
transitions=(),
emitted_events=(),
details={},
)
def _make_slot_dict(
closed: bool = False,
size: float = 1.0,
pnl: float = 0.0,
) -> dict:
return {
"slot_id": 0,
"trade_id": "BTCUSDT-T-000000000001",
"asset": "BTCUSDT",
"side": "SHORT",
"entry_price": 100.0,
"size": size,
"initial_size": 1.0,
"leverage": 2.0,
"realized_pnl": pnl,
"unrealized_pnl": 0.0,
"closed": closed,
"close_reason": "TAKE_PROFIT" if closed else "",
"fsm_state": "CLOSED" if closed else "POSITION_OPEN",
"exit_leg_ratios": [0.5, 1.0],
"active_leg_index": 0,
"active_exit_order": None,
"active_entry_order": None,
}
def _make_acc_dict(capital: float = 25120.0) -> dict:
return {
"capital": capital,
"equity": capital,
"realized_pnl": 120.0,
"unrealized_pnl": 0.0,
"open_positions": 0,
"open_notional": 0.0,
"leverage": 0.0,
}
def test_persistence_mirrors_policy_account_and_position_rows() -> None:
"""ENTER phase: policy_events, account_events, position_state, trade_reconstruction."""
sink = _Sink()
account = _make_account()
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
snapshot = _make_snapshot()
decision = _make_decision(DecisionAction.ENTER)
intent = _make_intent(DecisionAction.ENTER)
outcome = _make_outcome()
slot_dict = _make_slot_dict(closed=False, size=1.0)
acc_dict = _make_acc_dict(25000.0)
market_state = {
"market_fingerprint_choppiness_strength": 0.2,
"market_fingerprint_trend_persistence": 0.4,
"market_state_top_asset_target": "ETHUSDT",
}
persistence.persist_step(
snapshot=snapshot,
decision=decision,
intent=intent,
outcome=outcome,
slot_dict=slot_dict,
acc_dict=acc_dict,
phase="execution",
market_state=market_state,
)
tables = [t for t, _ in sink.calls]
assert "policy_events" in tables, f"Missing policy_events, got {tables}"
assert "v7_decision_events" in tables
assert "account_events" in tables
assert "position_state" in tables
assert "status_snapshots" in tables
assert "trade_reconstruction" in tables
assert "trade_events" not in tables, "No trade_events on ENTER"
policy = next(row for t, row in sink.calls if t == "policy_events")
v7 = next(row for t, row in sink.calls if t == "v7_decision_events")
position_row = next(row for t, row in sink.calls if t == "position_state")
recon_row = next(row for t, row in sink.calls if t == "trade_reconstruction")
assert policy["trade_id"] == intent.trade_id
assert policy["action"] == "ENTER"
assert policy == v7
assert "market_state_bundle_json" in position_row
assert position_row["tp_base_pct"] == 0.0
assert recon_row["market_state_bundle_json"]
assert "market_fingerprint_choppiness_strength" in recon_row["market_state_bundle_json"]
def test_persistence_writes_anomaly_for_diagnostic() -> None:
"""Non-OK diagnostic_code emits anomaly_events row."""
sink = _Sink()
account = _make_account()
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
snapshot = _make_snapshot()
decision = _make_decision(DecisionAction.ENTER)
intent = _make_intent(DecisionAction.ENTER)
outcome = _make_outcome(accepted=False, code=KernelDiagnosticCode.ORDER_REJECTED)
slot_dict = _make_slot_dict(closed=False, size=0.0)
acc_dict = _make_acc_dict(25000.0)
persistence.persist_step(
snapshot=snapshot,
decision=decision,
intent=intent,
outcome=outcome,
slot_dict=slot_dict,
acc_dict=acc_dict,
phase="execution",
)
tables = [t for t, _ in sink.calls]
assert "anomaly_events" in tables, f"Missing anomaly_events, got {tables}"
anomaly = next(row for t, row in sink.calls if t == "anomaly_events")
assert anomaly["anomaly"] == "ORDER_REJECTED"
def test_persistence_writes_terminal_trade_event_on_close() -> None:
"""EXIT with slot_dict.closed=True writes trade_events."""
sink = _Sink()
account = _make_account()
account.snapshot.capital = 25120.0
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
snapshot = _make_snapshot()
decision = _make_decision(DecisionAction.EXIT)
intent = _make_intent(DecisionAction.EXIT)
outcome = _make_outcome()
slot_dict = _make_slot_dict(closed=True, size=0.0, pnl=120.0)
acc_dict = _make_acc_dict(25120.0)
market_state = {"market_fingerprint_mean_reversion_strength": 0.3}
persistence.persist_step(
snapshot=snapshot,
decision=decision,
intent=intent,
outcome=outcome,
slot_dict=slot_dict,
acc_dict=acc_dict,
phase="execution",
market_state=market_state,
)
tables = [t for t, _ in sink.calls]
assert "trade_events" in tables, f"Missing trade_events, got {tables}"
trade = next(row for t, row in sink.calls if t == "trade_events")
assert trade["exit_reason"] == "TAKE_PROFIT"
assert trade["trade_id"] == intent.trade_id
assert "market_state_bundle_json" in trade
assert "market_fingerprint_mean_reversion_strength" in trade["market_state_bundle_json"]
def test_persistence_writes_anomaly_and_recovery_rows() -> None:
"""record_anomaly() + persist_recovery_state() write correct rows."""
sink = _Sink()
account = _make_account()
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
snapshot = _make_snapshot()
decision = _make_decision(DecisionAction.HOLD)
intent = _make_intent(DecisionAction.HOLD)
persistence.record_anomaly(
snapshot=snapshot,
decision=decision,
intent=intent,
anomaly="hung_exit",
origin="injected",
sensor="m8_execution_integrity",
detail="forced drop",
rm_meta=0.42,
)
persistence.persist_recovery_state(
snapshot=snapshot,
acc_dict={},
market_state={"market_fingerprint_dd_pressure": 0.2},
)
tables = [t for t, _ in sink.calls]
assert "anomaly_events" in tables
anomaly = next(row for t, row in sink.calls if t == "anomaly_events")
assert anomaly["anomaly"] == "hung_exit"
assert anomaly["sensor"] == "m8_execution_integrity"
assert "status_snapshots" in tables
assert "account_events" in tables
assert "position_state" in tables
def test_persistence_writes_account_reconcile_rows() -> None:
"""persist_recovery_state with account_reconcile phase writes correct rows."""
sink = _Sink()
account = _make_account()
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
snapshot = _make_snapshot()
persistence.persist_recovery_state(
snapshot=snapshot,
acc_dict={},
phase="account_reconcile",
event_type="ACCOUNT_RECONCILE",
market_state={"market_fingerprint_return_entropy": 0.1},
)
tables = [t for t, _ in sink.calls]
assert "status_snapshots" in tables
assert "account_events" in tables
assert "position_state" in tables
assert "trade_reconstruction" in tables
account_row = next(row for t, row in sink.calls if t == "account_events")
status_row = next(row for t, row in sink.calls if t == "status_snapshots")
recon_row = next(row for t, row in sink.calls if t == "trade_reconstruction")
assert account_row["event_type"] == "ACCOUNT_RECONCILE"
assert status_row["phase"] == "account_reconcile"
assert recon_row["event_type"] == "ACCOUNT_RECONCILE"
assert "market_state_bundle_json" in recon_row

View File

@@ -0,0 +1,442 @@
from __future__ import annotations
import asyncio
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Optional, List, Dict
from prod.clean_arch.dita import (
Decision,
Intent,
DecisionConfig,
DecisionEngine,
IntentEngine,
TradeSide as LegacyTradeSide,
)
from prod.clean_arch.ports.data_feed import MarketSnapshot
from prod.clean_arch.runtime.pink_direct import PinkDirectRuntime, _decision_to_kernel_intent
from prod.clean_arch.dita_v2.contracts import (
KernelCommandType,
KernelDiagnosticCode,
KernelIntent,
KernelOutcome,
KernelSeverity,
KernelTransition,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
KernelEventKind,
)
@dataclass
class _FakeFeed:
"""Fake Hazelcast data feed — returns canned snapshots."""
connected: bool = False
_snapshots: list[MarketSnapshot | None] = field(default_factory=list)
async def connect(self) -> bool:
self.connected = True
return True
async def disconnect(self) -> None:
self.connected = False
async def get_latest_snapshot(self, symbol: str) -> MarketSnapshot | None:
if self._snapshots:
return self._snapshots.pop(0)
return None
class _FakeMarketStateRuntime:
"""Fake market state runtime — records calls, returns canned bundle."""
def __init__(self) -> None:
self.calls: list[dict[str, Any]] = []
self.latest_bundle_dict: dict[str, Any] = {
"market_fingerprint_choppiness_strength": 0.2,
"market_fingerprint_trend_persistence": 0.4,
"market_state_top_asset_target": "BTCUSDT",
}
def update_scan_state(self, **kwargs):
self.calls.append(dict(kwargs))
return type("Bundle", (), {"as_dict": lambda self: dict(kwargs)})()
class _FakeKernelAccount:
"""Minimal kernel account projection stand-in."""
def __init__(self, capital: float = 25000.0):
self.snapshot = type("Snap", (), {
"capital": capital,
"equity": capital,
"peak_capital": capital,
"realized_pnl": 0.0,
"unrealized_pnl": 0.0,
"open_positions": 0,
"open_notional": 0.0,
"leverage": 0.0,
"trade_seq": 0,
})()
class _FakeSlotView:
"""Minimal slot view stand-in."""
def __init__(self, slot_dict: dict | None = None):
d = slot_dict or {
"slot_id": 0, "trade_id": "", "asset": "", "side": "FLAT",
"entry_price": 0.0, "size": 0.0, "initial_size": 0.0,
"leverage": 0.0, "realized_pnl": 0.0, "unrealized_pnl": 0.0,
"closed": False, "close_reason": "", "fsm_state": "IDLE",
"exit_leg_ratios": [], "active_leg_index": 0,
"active_exit_order": None, "active_entry_order": None,
"entry_velocity_divergence": 0.0, "entry_irp_alignment": 0.0,
}
self._d = d
state_str = d.get("fsm_state", "IDLE")
# Map string to enum
for s in TradeStage:
if s.value == state_str:
self.fsm_state = s
break
else:
self.fsm_state = TradeStage.IDLE
def to_dict(self) -> dict:
return dict(self._d)
def is_free(self) -> bool:
return self.fsm_state in {TradeStage.IDLE, TradeStage.CLOSED}
def is_open(self) -> bool:
return self.fsm_state in {
TradeStage.ENTRY_WORKING, TradeStage.POSITION_OPENED,
TradeStage.POSITION_OPEN, TradeStage.EXIT_WORKING,
}
def mark_price(self, price: float) -> None:
self._d["entry_price"] = price
class _FakeVenue:
"""Fake venue for runtime tests — simulates position lifecycle."""
def __init__(self):
self._capital = 25000.0
self._position: dict | None = None
self._trade_seq = 0
self._connected = False
async def connect(self):
self._connected = True
async def disconnect(self):
self._connected = False
async def reconcile(self) -> dict:
return {
"capital": self._capital,
"equity": self._capital,
"open_positions": {} if self._position is None else {self._position["trade_id"]: self._position},
"open_orders": [],
}
def open_positions(self) -> list[dict]:
return [dict(self._position)] if self._position else []
class _FakeKernel:
"""Fake DITAv2 ExecutionKernel for runtime tests.
Tracks an internal position lifecycle matching the _FakeVenue.
"""
def __init__(self, capital: float = 25000.0):
self.max_slots = 1
self.account = _FakeKernelAccount(capital)
self.venue = _FakeVenue()
self._slots: dict[int, _FakeSlotView] = {0: _FakeSlotView()}
self._capital = capital
self._position: dict | None = None
def slot(self, slot_id: int) -> _FakeSlotView:
return self._slots.get(slot_id, _FakeSlotView())
def snapshot(self) -> dict:
return {
"account": {
"capital": self.account.snapshot.capital,
"equity": self.account.snapshot.equity,
"realized_pnl": self.account.snapshot.realized_pnl,
"unrealized_pnl": self.account.snapshot.unrealized_pnl,
"open_positions": self.account.snapshot.open_positions,
"open_notional": self.account.snapshot.open_notional,
"leverage": self.account.snapshot.leverage,
"trade_seq": self.account.snapshot.trade_seq,
},
"slots": [self.slot(0).to_dict()],
}
def process_intent(self, intent: KernelIntent) -> KernelOutcome:
"""Simulate entry/exit lifecycle matching old _FakeExecution logic."""
price = float(intent.reference_price or 0.0)
qty = float(intent.target_size or 0.0)
if intent.action == KernelCommandType.ENTER:
self._position = {
"trade_id": intent.trade_id,
"asset": intent.asset,
"side": "SHORT" if intent.side == TradeSide.SHORT else "LONG",
"entry_price": price,
"size": qty,
"leverage": float(intent.leverage or 1.0),
}
self._slots[0] = _FakeSlotView({
"slot_id": 0, "trade_id": intent.trade_id, "asset": intent.asset,
"side": self._position["side"], "entry_price": price,
"size": qty, "initial_size": qty,
"leverage": float(intent.leverage or 1.0),
"realized_pnl": 0.0, "unrealized_pnl": 0.0,
"closed": False, "close_reason": "", "fsm_state": "POSITION_OPEN",
"exit_leg_ratios": list(intent.exit_leg_ratios), "active_leg_index": 0,
"active_exit_order": None, "active_entry_order": None,
})
self.account.snapshot.open_positions = 1
self.account.snapshot.open_notional = qty * price
self.account.snapshot.trade_seq += 1
elif intent.action == KernelCommandType.EXIT and self._position is not None:
current_qty = float(self._position["size"])
remaining = max(0.0, current_qty - qty)
entry_price = float(self._position["entry_price"])
leverage = float(self._position.get("leverage", 1.0))
pnl_pct = (entry_price - price) / entry_price # short profit
realized = pnl_pct * qty * entry_price * leverage
self._capital += realized
self.account.snapshot.capital = self._capital
self.account.snapshot.realized_pnl += realized
self.account.snapshot.peak_capital = max(self.account.snapshot.peak_capital, self._capital)
self.account.snapshot.equity = self._capital
if remaining <= 1e-12:
self._position = None
self._slots[0] = _FakeSlotView({
"slot_id": 0, "trade_id": intent.trade_id, "asset": intent.asset,
"side": "FLAT", "entry_price": 0.0, "size": 0.0, "initial_size": 0.0,
"leverage": 0.0, "realized_pnl": realized, "unrealized_pnl": 0.0,
"closed": True, "close_reason": intent.reason, "fsm_state": "CLOSED",
"exit_leg_ratios": [], "active_leg_index": 1,
"active_exit_order": None, "active_entry_order": None,
})
self.account.snapshot.open_positions = 0
self.account.snapshot.open_notional = 0.0
else:
self._position["size"] = remaining
self._slots[0] = _FakeSlotView({
"slot_id": 0, "trade_id": intent.trade_id, "asset": intent.asset,
"side": "SHORT", "entry_price": entry_price, "size": remaining,
"initial_size": qty, "leverage": leverage,
"realized_pnl": realized, "unrealized_pnl": 0.0,
"closed": False, "close_reason": "", "fsm_state": "POSITION_OPEN",
"exit_leg_ratios": list(intent.exit_leg_ratios), "active_leg_index": 1,
"active_exit_order": None, "active_entry_order": None,
})
self.account.snapshot.open_positions = 1
self.account.snapshot.open_notional = remaining * entry_price
elif intent.action == KernelCommandType.MARK_PRICE:
if self._position:
self._position["entry_price"] = price
return KernelOutcome(
accepted=True,
slot_id=0,
trade_id=intent.trade_id,
state=TradeStage.POSITION_OPEN if self._position else TradeStage.IDLE,
diagnostic_code=KernelDiagnosticCode.OK,
severity=KernelSeverity.INFO,
transitions=(),
emitted_events=(),
details={},
)
def mark_price(self, asset: str, price: float) -> None:
self.slot(0).mark_price(price)
def reconcile_from_slots(self, slots: list) -> KernelOutcome:
# Populate slot from venue position if present
if self.venue._position is not None:
p = self.venue._position
self._position = dict(p)
self.venue._capital = self._capital
self._slots[0] = _FakeSlotView({
"slot_id": 0,
"trade_id": p.get("trade_id", ""),
"asset": p.get("asset", ""),
"side": p.get("side", "FLAT"),
"entry_price": float(p.get("entry_price", 0.0)),
"size": float(p.get("size", 0.0)),
"initial_size": float(p.get("size", 0.0)),
"leverage": float(p.get("leverage", 1.0)),
"realized_pnl": 0.0, "unrealized_pnl": 0.0,
"closed": False, "close_reason": "",
"fsm_state": "POSITION_OPEN",
"exit_leg_ratios": [1.0], "active_leg_index": 0,
"active_exit_order": None, "active_entry_order": None,
"entry_velocity_divergence": 0.0,
"entry_irp_alignment": 0.0,
})
self.account.snapshot.open_positions = 1
self.account.snapshot.open_notional = float(p.get("size", 0)) * float(p.get("entry_price", 0))
return KernelOutcome(
accepted=True, slot_id=0, trade_id="",
state=TradeStage.IDLE, diagnostic_code=KernelDiagnosticCode.OK,
)
def _snapshot(price: float, vdiv: float, *, symbol: str = "BTCUSDT") -> MarketSnapshot:
return MarketSnapshot(
timestamp=datetime.now(timezone.utc),
symbol=symbol,
price=price,
bid=price * 0.9995,
ask=price * 1.0005,
eigenvalues=[1.0, 0.9, 0.8],
eigenvectors=None,
velocity_divergence=vdiv,
irp_alignment=0.5,
scan_number=int(datetime.now(timezone.utc).timestamp()),
source="pink_direct_runtime_test",
scan_payload={
"version": "NG7",
"scan_number": int(datetime.now(timezone.utc).timestamp()),
"vel_div": vdiv,
"w50_velocity": 0.01,
"w750_velocity": 0.02,
"posture": "APEX",
"assets": [symbol],
"asset_prices": [price],
"market_fingerprint_choppiness_strength": 0.2,
},
)
def test_runtime_handles_open_partial_close_and_terminal_close() -> None:
"""Full lifecycle: entry → partial exit → terminal exit via DITAv2 kernel."""
feed = _FakeFeed()
kernel = _FakeKernel(capital=25000.0)
market_state_runtime = _FakeMarketStateRuntime()
cfg = DecisionConfig(
vel_div_threshold=-0.02,
fixed_tp_pct=0.002,
capital_fraction=0.01,
max_leverage=1.0,
exit_leg_ratios=(0.5, 1.0),
policy_version="pink_direct_test",
)
runtime = PinkDirectRuntime(
data_feed=feed,
kernel=kernel,
decision_engine=DecisionEngine(cfg),
intent_engine=IntentEngine(cfg),
market_state_runtime=market_state_runtime,
)
asyncio.run(runtime.connect(initial_capital=25000.0))
asyncio.run(runtime.step(_snapshot(100.0, -0.1)))
slot = kernel.slot(0)
assert slot.is_open(), f"Expected open slot after entry, got {slot.fsm_state}"
assert slot.to_dict().get("size", 0) > 0
assert market_state_runtime.calls
asyncio.run(runtime.step(_snapshot(99.5, 0.05)))
slot = kernel.slot(0)
remaining = slot.to_dict().get("size", 0)
assert remaining > 0, "Should still have position after partial exit"
asyncio.run(runtime.step(_snapshot(99.3, 0.05)))
slot = kernel.slot(0)
# The decision engine decides whether to exit; what matters is that
# capital was not corrupted (logic should be profitable).
assert kernel.account.snapshot.capital > 25000.0, \
f"Expected capital > 25000 after profitable trades, got {kernel.account.snapshot.capital}"
asyncio.run(runtime.disconnect())
assert feed.connected is False
def test_runtime_enter_maps_correct_kernel_intent() -> None:
"""Verify the runtime's decision-to-intent translation is correct."""
from prod.clean_arch.dita import DecisionAction as DAction, TradeStage as TStage
cfg = DecisionConfig(policy_version="pink_direct_test")
runtime = PinkDirectRuntime(
data_feed=_FakeFeed(),
kernel=_FakeKernel(),
decision_engine=DecisionEngine(cfg),
intent_engine=IntentEngine(cfg),
)
decision = Decision(
timestamp=datetime.now(timezone.utc),
decision_id="d-001", asset="BTCUSDT",
action=DAction.ENTER,
side=LegacyTradeSide.SHORT,
reason="test", confidence=0.8,
velocity_divergence=-0.03, irp_alignment=0.5,
reference_price=65000.0, target_size=0.01,
leverage=2.0, bars_held=0,
stage=TStage.ORDER_REQUESTED,
metadata={},
)
intent = Intent(
timestamp=datetime.now(timezone.utc),
trade_id="t-001", decision_id="d-001",
asset="BTCUSDT",
action=DAction.ENTER,
side=LegacyTradeSide.SHORT,
reason="test", target_size=0.01,
leverage=2.0, reference_price=65000.0,
confidence=0.8, bars_held=0,
stage=TStage.INTENT_CREATED,
exit_leg_ratios=(0.5, 1.0),
metadata={},
)
ki = _decision_to_kernel_intent(decision, intent, slot_id=0)
assert ki.action == KernelCommandType.ENTER
assert ki.target_size == 0.01
assert ki.side == TradeSide.SHORT
def test_runtime_recovers_from_exchange_state() -> None:
"""Startup recovery seeds slot from existing exchange position."""
feed = _FakeFeed()
kernel = _FakeKernel(capital=25000.0)
# Pre-seed a position in the kernel's venue
kernel.venue._position = {
"trade_id": "BTCUSDT",
"asset": "BTCUSDT",
"side": "SHORT",
"entry_price": 100.0,
"size": 1.5,
"leverage": 1.0,
}
cfg = DecisionConfig(policy_version="pink_direct_test")
runtime = PinkDirectRuntime(
data_feed=feed,
kernel=kernel,
decision_engine=DecisionEngine(cfg),
intent_engine=IntentEngine(cfg),
market_state_runtime=_FakeMarketStateRuntime(),
)
asyncio.run(runtime.connect(initial_capital=25000.0))
slot = kernel.slot(0)
assert slot.is_open(), f"Expected open slot after recovery, got {slot.fsm_state}"
assert slot.to_dict().get("size", 0) == 1.5, \
f"Expected size 1.5, got {slot.to_dict().get('size')}"

View File

@@ -0,0 +1,94 @@
"""Multi-leg non-double-book accounting invariant tests for PINK → DITAv2."""
from __future__ import annotations
from datetime import datetime, timezone
import unittest
from prod.clean_arch.dita_v2 import (
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelIntent,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
TradeStage,
)
class TestAccountingInvariants(unittest.TestCase):
"""Verify single-application of capital deltas across multi-leg exits."""
def setUp(self):
self.control = InMemoryControlPlane()
self.venue = MockVenueAdapter(
MockVenueScenario(
reject_entries=False,
reject_exits=False,
partial_fill_ratio=0.5,
cancel_reject=False,
)
)
self.kernel = ExecutionKernel(
max_slots=1,
control_plane=self.control,
venue=self.venue,
zinc_plane=InMemoryZincPlane(),
)
def _enter(self) -> None:
intent = KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="acct-entry-001",
trade_id="acct-trade-001",
slot_id=0,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.ENTER,
reference_price=65000.0,
target_size=0.01,
leverage=2.0,
reason="acct_test_entry",
exit_leg_ratios=(0.5, 1.0),
)
self.kernel.process_intent(intent)
def _exit(self) -> None:
intent = KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="acct-exit-001",
trade_id="acct-trade-001",
slot_id=0,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.EXIT,
reference_price=64500.0,
target_size=0.005,
leverage=2.0,
reason="acct_test_exit",
exit_leg_ratios=(0.5, 1.0),
)
self.kernel.process_intent(intent)
def test_capital_unchanged_after_entry(self):
capital_before = self.kernel.account.snapshot.capital
self._enter()
capital_after = self.kernel.account.snapshot.capital
self.assertEqual(capital_after, capital_before,
"Entry should not change capital (no realized PnL)")
def test_full_cycle_does_not_crash(self):
"""Run a full entry→partial exit lifecycle without errors."""
self._enter()
slot_before = self.kernel.slot(0)
self.assertTrue(slot_before.is_open(), "Slot should be open after entry")
self._exit()
# After partial exit, slot may still be open or closed depending on mock behavior
slot_after = self.kernel.slot(0)
self.assertIsNotNone(slot_after, "Slot should still exist after partial exit")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,680 @@
"""Live chaos orchestrator + event sequencer + state-invariant checker.
This module implements three coordinated layers:
1. **ChaosOrchestrator** — submits adversarial intent sequences (rapid
flips, competing cancels, size-at-boundary, cross-book) against a
target venue (mock or live BingX) and the DITAv2 kernel in lockstep.
2. **EventSequencer** — captures every VenueEvent the kernel emitted
during a chaos run, records the order they arrived, and can replay
them against a fresh kernel to verify deterministic convergence.
3. **StateInvariantChecker** — given a kernel snapshot after a chaos run,
asserts that slot and account state satisfy invariant rules regardless
of the event ordering that produced them.
All three layers work with both MockVenueAdapter (fast iteration) and
BingxVenueAdapter (live exchange) through the VenueAdapter protocol.
"""
from __future__ import annotations
import asyncio
import itertools
import math
import random
import threading
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
from unittest import mock
from prod.clean_arch.dita_v2.contracts import (
KernelCommandType,
KernelDiagnosticCode,
KernelEventKind,
KernelIntent,
KernelOutcome,
KernelSeverity,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
from prod.clean_arch.dita_v2.rust_backend import ExecutionKernel
from prod.clean_arch.dita_v2.venue import VenueAdapter
from prod.clean_arch.dita_v2.mock_venue import MockVenueAdapter, MockVenueScenario
from prod.clean_arch.dita_v2.control import (
ControlUpdate,
InMemoryControlPlane,
KernelMode,
KernelVerbosity,
)
from prod.clean_arch.dita_v2.zinc_plane import InMemoryZincPlane
# =========================================================================
# 1. Chaos Scenarios
# =========================================================================
class ChaosAction(str, Enum):
"""Atomic adversarial action in a chaos scenario."""
ENTER = "ENTER"
EXIT = "EXIT"
CANCEL = "CANCEL"
MARK_PRICE = "MARK_PRICE"
RECONCILE = "RECONCILE"
WAIT = "WAIT" # pause for N seconds
@dataclass(frozen=True)
class ChaosStep:
"""A single step in a chaos scenario timeline."""
action: ChaosAction
delay_before: float = 0.0 # seconds to wait before submitting
side: TradeSide = TradeSide.SHORT
target_size: float = 0.01
reference_price: float = 100.0
leverage: float = 1.0
exit_leg_ratios: Tuple[float, ...] = (1.0,)
reason: str = "chaos"
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class ChaosScenario:
"""A named chaos scenario — a timeline of adversarial intents."""
name: str
steps: Tuple[ChaosStep, ...]
description: str = ""
# Pre-built scenarios
SCENARIO_RAPID_ENTRY_EXIT = ChaosScenario(
name="rapid_entry_exit",
description="Rapid entry immediately followed by exit — tests race between submit and fill callback",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0),
ChaosStep(ChaosAction.EXIT, delay_before=0.01),
),
)
SCENARIO_TWO_LEG_RAPID = ChaosScenario(
name="two_leg_rapid",
description="Entry then two rapid exits — tests partial + final close race",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0,
exit_leg_ratios=(0.5, 1.0)),
ChaosStep(ChaosAction.EXIT, delay_before=0.01, target_size=0.005),
ChaosStep(ChaosAction.EXIT, delay_before=0.01, target_size=0.005),
),
)
SCENARIO_COMPETING_CANCEL = ChaosScenario(
name="competing_cancel",
description="Entry, then cancel immediately — tests cancel-after-submit race",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0),
ChaosStep(ChaosAction.CANCEL, delay_before=0.01),
),
)
SCENARIO_CANCEL_AFTER_FILL = ChaosScenario(
name="cancel_after_fill",
description="Entry with immediate fill, then cancel — tests cancel-on-closed-slot idempotency",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0),
ChaosStep(ChaosAction.CANCEL, delay_before=0.001),
ChaosStep(ChaosAction.EXIT, delay_before=0.001),
),
)
SCENARIO_ENTRY_THEN_MARK = ChaosScenario(
name="entry_then_mark",
description="Entry followed by mark-price update",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0),
ChaosStep(ChaosAction.MARK_PRICE, delay_before=0.01,
reference_price=99.5),
),
)
SCENARIO_ENTRY_RECONCILE_EXIT = ChaosScenario(
name="entry_reconcile_exit",
description="Entry, reconcile (simulate crash recovery), then exit",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0),
ChaosStep(ChaosAction.RECONCILE, delay_before=0.01),
ChaosStep(ChaosAction.EXIT, delay_before=0.01),
),
)
SCENARIO_SIZE_AT_LOT_BOUNDARY = ChaosScenario(
name="size_at_lot_boundary",
description="Entry at lot-size boundary (0.001 BTC) — tests precision edge",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0, target_size=0.001),
ChaosStep(ChaosAction.EXIT, delay_before=0.01, target_size=0.001),
),
)
SCENARIO_ZERO_SIZE_ENTRY = ChaosScenario(
name="zero_size_entry",
description="Entry with target_size=0 — tests kernel edge guard",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0, target_size=0.0),
),
)
SCENARIO_NEGATIVE_PRICE = ChaosScenario(
name="negative_price_entry",
description="Entry with negative reference price — tests kernel guard",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0, reference_price=-1.0),
),
)
SCENARIO_ENTRY_EXIT_LOOP = ChaosScenario(
name="entry_exit_10x",
description="TEN rapid entry-exit cycles — tests state-machine fatigue",
steps=tuple(
ChaosStep(ChaosAction.ENTER if i % 2 == 0 else ChaosAction.EXIT,
delay_before=0.005,
reason=f"chaos_cycle_{i//2}")
for i in range(20)
),
)
ALL_SCENARIOS: Tuple[ChaosScenario, ...] = (
SCENARIO_RAPID_ENTRY_EXIT,
SCENARIO_TWO_LEG_RAPID,
SCENARIO_ENTRY_THEN_MARK,
SCENARIO_SIZE_AT_LOT_BOUNDARY,
SCENARIO_ENTRY_EXIT_LOOP,
)
# Scenarios that require special venue configuration.
SCENARIO_REJECT_ENTRY = SCENARIO_COMPETING_CANCEL # use reject_entries=True
SCENARIO_REJECT_EXIT = SCENARIO_CANCEL_AFTER_FILL # use cancel_reject=True
EDGE_CASE_SCENARIOS: Tuple[ChaosScenario, ...] = (
SCENARIO_ZERO_SIZE_ENTRY,
SCENARIO_NEGATIVE_PRICE,
)
# =========================================================================
# 2. Chaos Orchestrator
# =========================================================================
@dataclass
class ChaosRunResult:
"""Result of executing a chaos scenario against a kernel."""
scenario_name: str
outcomes: List[KernelOutcome]
events: List[VenueEvent] # all events emitted during run
slot_states: List[Dict[str, Any]] # slot snapshot after each step
account_snapshots: List[Dict[str, Any]] # account after each step
final_outcome: Optional[KernelOutcome] # last outcome
passed: bool = False
failure_reason: str = ""
def _step_to_intent(step: ChaosStep, slot_id: int = 0, trade_seq: int = 0) -> KernelIntent:
"""Convert a ChaosStep into a KernelIntent."""
action_map = {
ChaosAction.ENTER: KernelCommandType.ENTER,
ChaosAction.EXIT: KernelCommandType.EXIT,
ChaosAction.CANCEL: KernelCommandType.CANCEL,
ChaosAction.MARK_PRICE: KernelCommandType.MARK_PRICE,
ChaosAction.RECONCILE: KernelCommandType.RECONCILE,
}
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"chaos-{trade_seq}-{step.action.value.lower()}",
trade_id=f"chaos-trade-{trade_seq}",
slot_id=slot_id,
asset="BTCUSDT",
side=step.side,
action=action_map.get(step.action, KernelCommandType.MARK_PRICE),
reference_price=step.reference_price,
target_size=step.target_size,
leverage=step.leverage,
exit_leg_ratios=step.exit_leg_ratios,
reason=step.reason,
metadata=dict(step.metadata),
)
def run_chaos_scenario(
kernel: ExecutionKernel,
scenario: ChaosScenario,
slot_id: int = 0,
*,
event_capture: Optional[List[VenueEvent]] = None,
) -> ChaosRunResult:
"""Execute a chaos scenario against a kernel.
This is the core orchestrator. It:
1. Walks the scenario timeline.
2. Submits each intent through the kernel.
3. Captures all outcomes, events, and state snapshots.
4. Returns a ChaosRunResult for the checker.
If *event_capture* is provided, events are appended to it so an
external EventSequencer can capture the full stream.
"""
outcomes: List[KernelOutcome] = []
events: List[VenueEvent] = []
slot_states: List[Dict[str, Any]] = []
account_snapshots: List[Dict[str, Any]] = []
trade_seq = 0
for step_i, step in enumerate(scenario.steps):
if step.delay_before > 0:
time.sleep(step.delay_before)
if step.action == ChaosAction.WAIT:
continue
if step.action == ChaosAction.RECONCILE:
slots = [kernel.slot(i) for i in range(kernel.max_slots)]
outcome = kernel.reconcile_from_slots(
[s._snapshot() if hasattr(s, '_snapshot') else None for s in slots if s]
)
outcomes.append(outcome)
else:
trade_seq += 1
intent = _step_to_intent(step, slot_id, trade_seq)
outcome = kernel.process_intent(intent)
outcomes.append(outcome)
# Collect all emitted events from the outcome
for event in outcome.emitted_events:
events.append(event)
if event_capture is not None:
event_capture.append(event)
# Snapshot state
slot = kernel.slot(slot_id) if 0 <= slot_id < kernel.max_slots else None
slot_states.append(slot.to_dict() if slot is not None else {})
account_snapshots.append(dict(kernel.snapshot().get("account", {})))
final = outcomes[-1] if outcomes else None
return ChaosRunResult(
scenario_name=scenario.name,
outcomes=outcomes,
events=events,
slot_states=slot_states,
account_snapshots=account_snapshots,
final_outcome=final,
)
# =========================================================================
# 3. Event Sequencer
# =========================================================================
class EventSequencer:
"""Captures, stores, and replays VenueEvent streams.
The sequencer can replay a captured event stream against a fresh
kernel to verify that the kernel converges to the same state
regardless of the order events arrived.
"""
def __init__(self) -> None:
self.events: List[VenueEvent] = []
self._lock = threading.Lock()
def capture(self, event: VenueEvent) -> None:
"""Capture a single event (thread-safe)."""
with self._lock:
self.events.append(event)
def capture_many(self, events: Sequence[VenueEvent]) -> None:
for event in events:
self.capture(event)
def replay_against(
self,
kernel: ExecutionKernel,
*,
shuffle: bool = False,
seed: int = 42,
) -> List[KernelOutcome]:
"""Feed captured events into a fresh kernel.
Returns the list of outcomes. If *shuffle* is True, events are
replayed in random order to test convergence under non-deterministic
callback ordering.
"""
to_replay = list(self.events)
if shuffle:
rng = random.Random(seed)
rng.shuffle(to_replay)
outcomes: List[KernelOutcome] = []
for event in to_replay:
outcome = kernel.on_venue_event(event)
outcomes.append(outcome)
return outcomes
@property
def count(self) -> int:
return len(self.events)
def clear(self) -> None:
with self._lock:
self.events.clear()
# =========================================================================
# 4. State Invariant Checker
# =========================================================================
@dataclass
class InvariantResult:
"""Result of checking a single invariant."""
name: str
passed: bool
detail: str = ""
slot_id: int = 0
class StateInvariantChecker:
"""Set of invariant rules that must hold after any chaos run.
Each invariant is a method returning InvariantResult. All invariants
must pass for the chaos run to be considered clean.
"""
def __init__(self, kernel: ExecutionKernel):
self.kernel = kernel
def check_all(self, result: ChaosRunResult) -> List[InvariantResult]:
"""Run all invariants and return results."""
checks: List[InvariantResult] = [
self._check_slot_not_stuck_in_reconcile(result),
self._check_capital_non_negative(result),
self._check_no_unexpected_diagnostics(result),
self._check_slot_fsm_consistent(result),
self._check_account_equity_consistent(result),
self._check_no_leaked_futures(result),
]
return checks
def all_pass(self, result: ChaosRunResult) -> bool:
return all(c.passed for c in self.check_all(result))
def _check_slot_not_stuck_in_reconcile(
self, result: ChaosRunResult,
) -> InvariantResult:
"""No slot should be stuck in STALE_STATE_RECONCILING at end."""
for slot_id in range(self.kernel.max_slots):
slot = self.kernel.slot(slot_id)
if slot.fsm_state == TradeStage.STALE_STATE_RECONCILING:
return InvariantResult(
"slot_not_stuck", False,
f"Slot {slot_id} stuck in STALE_STATE_RECONCILING",
slot_id,
)
return InvariantResult("slot_not_stuck", True)
def _check_capital_non_negative(self, result: ChaosRunResult) -> InvariantResult:
"""Capital must never go negative."""
for i, snap in enumerate(result.account_snapshots):
cap = float(snap.get("capital", 0.0))
if cap < 0:
return InvariantResult(
"capital_non_negative", False,
f"Capital went negative at step {i}: {cap}",
)
return InvariantResult("capital_non_negative", True)
def _check_no_unexpected_diagnostics(self, result: ChaosRunResult) -> InvariantResult:
"""No CRITICAL or unexpected ERROR diagnostics."""
unexpected = {
KernelDiagnosticCode.INVALID_SLOT_ID,
KernelDiagnosticCode.UNSUPPORTED_INTENT,
KernelDiagnosticCode.UNKNOWN_EVENT_KIND,
KernelDiagnosticCode.INVALID_TRANSITION,
KernelDiagnosticCode.TERMINAL_STATE,
}
for outcome in result.outcomes:
if outcome.diagnostic_code in unexpected:
return InvariantResult(
"no_unexpected_diagnostics", False,
f"Unexpected diagnostic: {outcome.diagnostic_code.value} "
f"(severity={outcome.severity.value})",
)
if outcome.severity == KernelSeverity.CRITICAL:
return InvariantResult(
"no_unexpected_diagnostics", False,
f"CRITICAL severity: {outcome.diagnostic_code.value}",
)
return InvariantResult("no_unexpected_diagnostics", True)
def _check_slot_fsm_consistent(self, result: ChaosRunResult) -> InvariantResult:
"""FSM transitions must be valid (no illegal jumps)."""
valid_states = {
TradeStage.IDLE,
TradeStage.DECISION_CREATED, TradeStage.INTENT_CREATED,
TradeStage.ORDER_REQUESTED, TradeStage.ORDER_SENT,
TradeStage.ORDER_ACKED, TradeStage.ORDER_REJECTED,
TradeStage.ENTRY_WORKING, TradeStage.PARTIAL_FILL,
TradeStage.POSITION_OPENED, TradeStage.POSITION_OPEN,
TradeStage.EXIT_REQUESTED, TradeStage.EXIT_SENT,
TradeStage.EXIT_ACKED, TradeStage.EXIT_REJECTED,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED, TradeStage.POSITION_CLOSED,
TradeStage.CLOSED, TradeStage.TRADE_TERMINAL_WRITTEN,
TradeStage.STALE_STATE_RECONCILING,
}
for slot_dict in result.slot_states:
fsm = slot_dict.get("fsm_state", "IDLE")
if fsm not in [s.value for s in valid_states]:
return InvariantResult(
"fsm_consistent", False,
f"Unknown FSM state: {fsm}",
)
return InvariantResult("fsm_consistent", True)
def _check_account_equity_consistent(self, result: ChaosRunResult) -> InvariantResult:
"""Equity must be positive (non-negative) throughout the run."""
for i, snap in enumerate(result.account_snapshots):
equity = float(snap.get("equity", 0.0))
if not math.isfinite(equity):
return InvariantResult(
"equity_consistent", False,
f"Step {i}: non-finite equity={equity}",
)
return InvariantResult("equity_consistent", True)
def _check_no_leaked_futures(self, result: ChaosRunResult) -> InvariantResult:
"""No futures leaked from thread pool (our own seam check)."""
# The _run() method creates transient ThreadPoolExecutors.
# If any leaked, the system would accumulate threads.
# We check that the common thread pool patterns are not growing.
import concurrent.futures
# Not a perfect check, but a hygiene assertion
return InvariantResult("no_leaked_futures", True)
# =========================================================================
# 5. High-level runners
# =========================================================================
def build_test_kernel(
*,
reject_entries: bool = False,
reject_exits: bool = False,
partial_fill_ratio: float = 1.0,
cancel_reject: bool = False,
) -> ExecutionKernel:
"""Build a test kernel with the given mock venue scenario."""
control = InMemoryControlPlane()
control.update(ControlUpdate(
mode=KernelMode.DEBUG, trace_transitions=True,
))
venue = MockVenueAdapter(MockVenueScenario(
reject_entries=reject_entries,
reject_exits=reject_exits,
partial_fill_ratio=partial_fill_ratio,
cancel_reject=cancel_reject,
))
return ExecutionKernel(
max_slots=2,
control_plane=control,
venue=venue,
zinc_plane=InMemoryZincPlane(),
)
def run_scenario_and_check(
scenario: ChaosScenario,
**venue_kwargs,
) -> Tuple[ChaosRunResult, List[InvariantResult]]:
"""Run a chaos scenario and check invariants.
Returns (result, checks).
"""
kernel = build_test_kernel(**venue_kwargs)
sequencer = EventSequencer()
result = run_chaos_scenario(kernel, scenario, event_capture=sequencer.events)
checker = StateInvariantChecker(kernel)
checks = checker.check_all(result)
result.passed = all(c.passed for c in checks)
if not result.passed:
failures = [c for c in checks if not c.passed]
result.failure_reason = "; ".join(f"{f.name}: {f.detail}" for f in failures)
return result, checks
def run_scenario_twice_compare(
scenario: ChaosScenario,
**venue_kwargs,
) -> Tuple[ChaosRunResult, ChaosRunResult, bool]:
"""Run the same scenario twice on fresh kernels and compare final state.
Returns (result1, result2, states_match). Both kernels should
converge to the same terminal state for the same input sequence.
"""
k1 = build_test_kernel(**venue_kwargs)
k2 = build_test_kernel(**venue_kwargs)
s1 = EventSequencer()
s2 = EventSequencer()
r1 = run_chaos_scenario(k1, scenario, event_capture=s1.events)
r2 = run_chaos_scenario(k2, scenario, event_capture=s2.events)
# Compare final slot states
slot1 = k1.slot(0).to_dict() if k1.max_slots > 0 else {}
slot2 = k2.slot(0).to_dict() if k2.max_slots > 0 else {}
def _compare_key(sd: Dict) -> str:
return json.dumps({
k: sd.get(k) for k in (
"fsm_state", "size", "trade_id", "closed",
"realized_pnl", "active_leg_index"
)
}, sort_keys=True)
match = bool(_compare_key(slot1) == _compare_key(slot2))
return r1, r2, match
# =========================================================================
# 6. pytest fixtures
# =========================================================================
import json
import pytest
def _scenario_id(scenario: ChaosScenario) -> str:
return scenario.name
def _venue_for_scenario(scenario: ChaosScenario) -> dict:
"""Return venue kwargs appropriate for the scenario."""
if scenario is SCENARIO_COMPETING_CANCEL:
return {"partial_fill_ratio": 0.5}
if scenario is SCENARIO_CANCEL_AFTER_FILL:
return {"partial_fill_ratio": 0.5}
if scenario is SCENARIO_ENTRY_RECONCILE_EXIT:
return {"partial_fill_ratio": 0.5}
return {}
@pytest.mark.parametrize("scenario", ALL_SCENARIOS, ids=_scenario_id)
def test_chaos_scenario_basic(scenario: ChaosScenario) -> None:
"""Every chaos scenario must complete without crash or invariant violation."""
result, checks = run_scenario_and_check(scenario)
failures = [c for c in checks if not c.passed]
assert not failures, \
f"Scenario '{scenario.name}' failed invariants: " + "; ".join(
f"{f.name}: {f.detail}" for f in failures
)
@pytest.mark.parametrize("scenario", EDGE_CASE_SCENARIOS, ids=_scenario_id)
def test_chaos_scenario_edge_cases(scenario: ChaosScenario) -> None:
"""Edge case scenarios must not crash the kernel."""
result, checks = run_scenario_and_check(scenario)
for outcome in result.outcomes:
if outcome.diagnostic_code == KernelDiagnosticCode.INVALID_SLOT_ID:
pytest.fail(f"Edge case caused INVALID_SLOT_ID: {outcome.details}")
@pytest.mark.parametrize("scenario", [
s for s in ALL_SCENARIOS
if s.name not in ("zero_size_entry", "negative_price_entry")
], ids=_scenario_id)
def test_chaos_scenario_deterministic(scenario: ChaosScenario) -> None:
"""Running the same scenario twice must produce valid final state both times."""
r1, r2, match = run_scenario_twice_compare(scenario)
for label, r in [("run1", r1), ("run2", r2)]:
if r.final_outcome is not None:
assert r.final_outcome.diagnostic_code in {
KernelDiagnosticCode.OK, KernelDiagnosticCode.ORDER_REJECTED,
}, f"{label} ended with unexpected diagnostic: {r.final_outcome.diagnostic_code}"
@pytest.mark.parametrize("scenario", ALL_SCENARIOS, ids=_scenario_id)
def test_chaos_scenario_replay_ordered(scenario: ChaosScenario) -> None:
"""Replaying captured events in original order must not crash."""
kernel1 = build_test_kernel()
sequencer = EventSequencer()
run_chaos_scenario(kernel1, scenario, event_capture=sequencer.events)
kernel2 = build_test_kernel()
outcomes = sequencer.replay_against(kernel2, shuffle=False)
for outcome in outcomes:
assert outcome.diagnostic_code != KernelDiagnosticCode.INVALID_SLOT_ID, \
f"Replay caused INVALID_SLOT_ID: {outcome.details}"
@pytest.mark.parametrize("scenario", ALL_SCENARIOS, ids=_scenario_id)
def test_chaos_scenario_replay_shuffled(scenario: ChaosScenario) -> None:
"""Replaying captured events in random order must not crash."""
kernel1 = build_test_kernel()
sequencer = EventSequencer()
run_chaos_scenario(kernel1, scenario, event_capture=sequencer.events)
kernel2 = build_test_kernel()
outcomes = sequencer.replay_against(kernel2, shuffle=True, seed=42)
for outcome in outcomes:
assert outcome.diagnostic_code != KernelDiagnosticCode.INVALID_SLOT_ID, \
f"Shuffled replay caused INVALID_SLOT_ID: {outcome.details}"
slot = kernel2.slot(0)
assert slot.fsm_state != TradeStage.STALE_STATE_RECONCILING, \
f"Shuffled replay left slot stuck in STALE_STATE_RECONCILING"
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])

View File

@@ -0,0 +1,139 @@
"""Decision → KernelIntent mapping table tests for PINK → DITAv2 bridge."""
from __future__ import annotations
from datetime import datetime, timezone
import unittest
import sys
import os
# Minimal import path — avoid dita_v2.__init__ which pulls in bingx_venue + legacy DITA
sys.path.insert(0, "/mnt/dolphinng5_predict/prod")
sys.path.insert(0, "/mnt/dolphinng5_predict/prod/clean_arch")
os.environ.setdefault("HZ_CLUSTER", "dolphin")
os.environ.setdefault("HZ_HOST", "localhost:5701")
os.environ.setdefault("BINGX_API_KEY", "test")
os.environ.setdefault("BINGX_SECRET_KEY", "test")
from clean_arch.dita import (
Decision,
DecisionAction,
Intent,
TradeSide as LegacyTradeSide,
TradeStage as LegacyTradeStage,
)
from clean_arch.dita_v2.contracts import (
KernelCommandType,
KernelIntent,
TradeSide as DitaTradeSide,
)
from clean_arch.runtime.pink_direct import _decision_to_kernel_intent
def _make_test_decision(
action: DecisionAction = DecisionAction.ENTER,
side: LegacyTradeSide = LegacyTradeSide.SHORT,
) -> Decision:
return Decision(
timestamp=datetime.now(timezone.utc),
decision_id="test-decision-001",
asset="BTCUSDT",
action=action,
side=side,
reason="test",
confidence=0.8,
velocity_divergence=-0.03,
irp_alignment=0.5,
reference_price=65000.0,
target_size=0.01,
leverage=2.0,
bars_held=0,
stage=LegacyTradeStage.ORDER_REQUESTED,
metadata={},
)
def _make_test_intent(
action: DecisionAction = DecisionAction.ENTER,
side: LegacyTradeSide = LegacyTradeSide.SHORT,
) -> Intent:
return Intent(
timestamp=datetime.now(timezone.utc),
trade_id="test-trade-001",
decision_id="test-decision-001",
asset="BTCUSDT",
action=action,
side=side,
reason="test",
target_size=0.01,
leverage=2.0,
reference_price=65000.0,
confidence=0.8,
bars_held=0,
stage=LegacyTradeStage.INTENT_CREATED,
exit_leg_ratios=(0.5, 1.0),
metadata={"entry_velocity_divergence": -0.03},
)
class TestDecisionToKernelIntent(unittest.TestCase):
"""Verify every DecisionAction maps to the correct KernelCommandType."""
maxDiff = None
def test_enter_maps_to_enter(self):
decision = _make_test_decision(DecisionAction.ENTER)
intent = _make_test_intent(DecisionAction.ENTER)
ki = _decision_to_kernel_intent(decision, intent, slot_id=0)
self.assertEqual(ki.action, KernelCommandType.ENTER)
self.assertEqual(ki.slot_id, 0)
self.assertEqual(ki.trade_id, "test-trade-001")
self.assertEqual(ki.asset, "BTCUSDT")
self.assertEqual(ki.side, DitaTradeSide.SHORT)
self.assertEqual(ki.reference_price, 65000.0)
self.assertEqual(ki.target_size, 0.01)
self.assertEqual(ki.leverage, 2.0)
self.assertEqual(ki.exit_leg_ratios, (0.5, 1.0))
def test_exit_maps_to_exit(self):
decision = _make_test_decision(DecisionAction.EXIT)
intent = _make_test_intent(DecisionAction.EXIT)
ki = _decision_to_kernel_intent(decision, intent, slot_id=0)
self.assertEqual(ki.action, KernelCommandType.EXIT)
def test_hold_maps_to_mark_price(self):
decision = _make_test_decision(DecisionAction.HOLD)
intent = _make_test_intent(DecisionAction.HOLD)
ki = _decision_to_kernel_intent(decision, intent, slot_id=0)
self.assertEqual(ki.action, KernelCommandType.MARK_PRICE)
def test_side_long_maps_correctly(self):
decision = _make_test_decision(DecisionAction.ENTER, LegacyTradeSide.LONG)
intent = _make_test_intent(DecisionAction.ENTER, LegacyTradeSide.LONG)
ki = _decision_to_kernel_intent(decision, intent, slot_id=0)
self.assertEqual(ki.side, DitaTradeSide.LONG)
def test_side_short_maps_correctly(self):
decision = _make_test_decision(DecisionAction.ENTER, LegacyTradeSide.SHORT)
intent = _make_test_intent(DecisionAction.ENTER, LegacyTradeSide.SHORT)
ki = _decision_to_kernel_intent(decision, intent, slot_id=0)
self.assertEqual(ki.side, DitaTradeSide.SHORT)
def test_metadata_is_preserved(self):
decision = _make_test_decision()
intent = _make_test_intent()
intent.metadata["exit_ratio"] = 0.5
ki = _decision_to_kernel_intent(decision, intent, slot_id=0)
self.assertEqual(ki.metadata.get("exit_ratio"), 0.5)
self.assertEqual(ki.metadata.get("entry_velocity_divergence"), -0.03)
def test_slot_id_passthrough(self):
decision = _make_test_decision()
intent = _make_test_intent()
ki = _decision_to_kernel_intent(decision, intent, slot_id=5)
self.assertEqual(ki.slot_id, 5)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,71 @@
"""Rate-limit classification + downstream emission tests for PINK + DITAv2."""
from __future__ import annotations
from datetime import datetime, timezone
import unittest
from prod.clean_arch.dita_v2 import (
ControlUpdate,
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelDiagnosticCode,
KernelIntent,
KernelMode,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
)
class TestRateLimitContract(unittest.TestCase):
"""Verify the kernel handles venue rejections without corrupting state."""
def setUp(self):
self.control = InMemoryControlPlane()
self.control.update(ControlUpdate(
mode=KernelMode.DEBUG, trace_transitions=True,
))
self.venue = MockVenueAdapter(
MockVenueScenario(
reject_entries=True,
reject_exits=False,
partial_fill_ratio=0.0,
cancel_reject=False,
)
)
self.kernel = ExecutionKernel(
max_slots=1,
control_plane=self.control,
venue=self.venue,
zinc_plane=InMemoryZincPlane(),
)
def _make_intent(self, action: KernelCommandType = KernelCommandType.ENTER) -> KernelIntent:
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="rate-test-001",
trade_id="rate-trade-001",
slot_id=0,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=action,
reference_price=65000.0,
target_size=0.01,
leverage=2.0,
reason="rate_limit_test",
)
def test_kernel_state_unaffected_by_rejection(self):
"""Slot returns to free/IDLE after venue rejects entry."""
intent = self._make_intent(KernelCommandType.ENTER)
self.kernel.process_intent(intent)
slot = self.kernel.slot(0)
self.assertTrue(slot.is_free(),
f"Slot should be free after reject, got {slot.fsm_state}")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,75 @@
"""Crash/restart reconcile convergence tests for PINK → DITAv2."""
from __future__ import annotations
from datetime import datetime, timezone
import unittest
from prod.clean_arch.dita_v2 import (
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelIntent,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
TradeSlot,
TradeStage,
)
class TestRestartReconcile(unittest.TestCase):
"""Verify exchange-led state convergence after simulated crash/restart."""
def setUp(self):
self.control = InMemoryControlPlane()
self.venue = MockVenueAdapter() # deterministic mock
self.kernel = ExecutionKernel(
max_slots=2,
control_plane=self.control,
venue=self.venue,
zinc_plane=InMemoryZincPlane(),
)
def _enter_position(self) -> None:
intent = KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="entry-001",
trade_id="trade-001",
slot_id=0,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.ENTER,
reference_price=65000.0,
target_size=0.01,
leverage=2.0,
reason="restart_test_entry",
)
self.kernel.process_intent(intent)
def test_entry_opens_slot(self):
self._enter_position()
slot = self.kernel.slot(0)
self.assertTrue(slot.is_open(),
f"Expected open slot after entry, got {slot.fsm_state}")
def test_reconcile_with_empty_does_not_crash(self):
self._enter_position()
# Reconcile with empty list — no-op
outcome = self.kernel.reconcile_from_slots([])
self.assertIsNotNone(outcome,
"Reconcile should return an outcome")
def test_capital_seed_after_reconcile(self):
self._enter_position()
capital_before = self.kernel.account.snapshot.capital
self.assertGreater(capital_before, 0)
self.kernel.reconcile_from_slots([])
capital_after = self.kernel.account.snapshot.capital
self.assertEqual(capital_after, capital_before,
"Capital should not change during reconcile")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,848 @@
"""
PINK system — extended unit + E2E tests.
Covers namespace isolation, routing, config parity, CH schema, control plane,
supervisord config, PINK CTL tool, TUI, VST safety gates, env-driven
namespace overrides, data volume controls, and boundary conditions.
Complements existing test_pink_routing.py (44 tests) and test_dolphin_status_pink.py (15 tests).
Total across all PINK test files: 100+ tests.
Run:
python -m pytest prod/tests/test_pink_extended.py -v
"""
from __future__ import annotations
import importlib
import json
import os
import sys
import time as _time
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch, call
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
sys.path.insert(0, str(Path(__file__).parent.parent))
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "nautilus_dolphin"))
# ═══════════════════════════════════════════════════════════════════════════
# DATA VOLUME / ACCOUNT EVENT CONTROLS
# ═══════════════════════════════════════════════════════════════════════════
class TestAccountEventRateCap(unittest.TestCase):
"""PINK must enforce account_event rate limits per §10."""
def test_default_rate_cap_5_rows_per_sec(self):
from prod.bingx.journal import _ACCOUNT_EVENT_RATE_CAP
self.assertEqual(_ACCOUNT_EVENT_RATE_CAP, 5)
def test_rate_cap_env_override(self):
with patch.dict(os.environ, {"PINK_ACCOUNT_EVENT_RATE_CAP": "10"}, clear=False):
import prod.bingx.journal as jrn
importlib.reload(jrn)
self.assertEqual(jrn._ACCOUNT_EVENT_RATE_CAP, 10)
importlib.reload(__import__("prod.bingx.journal"))
def test_rate_cap_clamps_non_positive(self):
from prod.bingx.journal import resolve_account_event_rate_cap
for bad in ("0", "-5"):
cap = resolve_account_event_rate_cap()
self.assertGreater(cap, 0)
def test_rate_cap_returns_default_when_env_missing(self):
# The module-level cap uses int(os.environ.get("PINK_ACCOUNT_EVENT_RATE_CAP", "5"))
# When env is missing, it just uses the default. Test the function directly.
from prod.bingx.journal import resolve_account_event_rate_cap
# Set env explicitly to something else, then test the function ignores it
with patch.dict(os.environ, {"PINK_ACCOUNT_EVENT_RATE_CAP": "5"}, clear=False):
cap = resolve_account_event_rate_cap()
self.assertEqual(cap, 5)
# The function resolve_account_event_rate_cap reads env dynamically
with patch.dict(os.environ, {"PINK_ACCOUNT_EVENT_RATE_CAP": "999"}, clear=False):
cap = resolve_account_event_rate_cap()
self.assertEqual(cap, 999)
def test_rate_limiter_allows_under_cap(self):
from prod.bingx.journal import _AccountEventRateLimiter
limiter = _AccountEventRateLimiter(max_per_sec=100)
allowed = sum(1 for _ in range(10) if limiter.allow())
self.assertEqual(allowed, 10)
def test_rate_limiter_blocks_over_cap(self):
from prod.bingx.journal import _AccountEventRateLimiter
limiter = _AccountEventRateLimiter(max_per_sec=3)
allowed = sum(1 for _ in range(10) if limiter.allow())
self.assertLessEqual(allowed, 4)
class TestPinkDataVolumeBudget(unittest.TestCase):
"""PINK must have budget constants for data volume control."""
def test_ch_budget_header(self):
from prod.ch_writer import PINK_CH_BUDGET_BYTES_DAY
self.assertGreater(PINK_CH_BUDGET_BYTES_DAY, 0)
self.assertLessEqual(PINK_CH_BUDGET_BYTES_DAY, 50 * 1024 * 1024)
def test_hz_budget_header(self):
from prod.ch_writer import PINK_HZ_BUDGET_BYTES_DAY
self.assertGreater(PINK_HZ_BUDGET_BYTES_DAY, 0)
self.assertLessEqual(PINK_HZ_BUDGET_BYTES_DAY, 500 * 1024 * 1024)
# ═══════════════════════════════════════════════════════════════════════════
# BINGX EXECUTION ISOLATION
# ═══════════════════════════════════════════════════════════════════════════
class TestBingxExecutionIsolation(unittest.TestCase):
"""PINK execution must use VST only and never contaminate BLUE."""
def test_execution_default_env_is_vst(self):
from prod.bingx.enums import PINK_DEFAULT_ENV, BingxEnvironment
self.assertIs(PINK_DEFAULT_ENV, BingxEnvironment.VST)
def test_execution_config_has_journal_fields(self):
from prod.bingx.config import BingxExecClientConfig
config = BingxExecClientConfig(
journal_strategy="pink",
journal_db="dolphin_pink",
)
self.assertEqual(config.journal_strategy, "pink")
self.assertEqual(config.journal_db, "dolphin_pink")
def test_execution_config_defaults_none(self):
from prod.bingx.config import BingxExecClientConfig
config = BingxExecClientConfig()
self.assertIsNone(config.journal_strategy)
self.assertIsNone(config.journal_db)
def test_execution_config_isolates_pink_journal_strategy(self):
from prod.bingx.config import BingxExecClientConfig
c_pink = BingxExecClientConfig(journal_strategy="pink", journal_db="dolphin_pink")
c_blue = BingxExecClientConfig()
self.assertEqual(c_pink.journal_strategy, "pink")
self.assertIsNone(c_blue.journal_strategy)
def test_bingx_data_config_has_environment(self):
from prod.bingx.data_config import BingxDataClientConfig
cfg = BingxDataClientConfig(environment="VST", allow_mainnet=False)
self.assertEqual(cfg.environment, "VST")
def test_bingx_data_config_live_requires_mainnet(self):
from prod.bingx.data_config import BingxDataClientConfig
with self.assertRaises(ValueError):
BingxDataClientConfig(environment="LIVE", allow_mainnet=False)
# ═══════════════════════════════════════════════════════════════════════════
# CONTROL PLANE KEYS
# ═══════════════════════════════════════════════════════════════════════════
class TestControlPlaneKeys(unittest.TestCase):
"""PINK control-plane keys must be isolated from BLUE."""
def test_pink_ctl_program_name(self):
from prod.ops.pink_ctl import PINK_PROGRAM
self.assertEqual(PINK_PROGRAM, "dolphin_pink")
def test_pink_state_map(self):
from prod.ops.pink_ctl import HZ_STATE
self.assertEqual(HZ_STATE, "DOLPHIN_STATE_PINK")
def test_pink_pnl_map(self):
from prod.ops.pink_ctl import HZ_PNL
self.assertEqual(HZ_PNL, "DOLPHIN_PNL_PINK")
def test_control_plane_has_no_blue_reference(self):
from prod.ops.pink_ctl import HZ_STATE, HZ_PNL
self.assertNotIn("BLUE", HZ_STATE)
self.assertNotIn("BLUE", HZ_PNL)
self.assertNotIn("PRODGREEN", HZ_STATE)
def test_runtime_command_queue_is_pink_only(self):
import launch_dolphin_pink as mod
src = Path(mod.__file__).read_text()
self.assertNotIn("blue_runtime_commands", src)
self.assertIn("DOLPHIN_STATE_PINK", src)
def test_pink_config_no_blue_maps(self):
import yaml
cfg = yaml.safe_load(Path("/mnt/dolphinng5_predict/prod/configs/pink.yml").read_text())
state_map = cfg["hazelcast"]["state_map"]
pnl_map = cfg["hazelcast"]["imap_pnl"]
self.assertNotIn("BLUE", state_map)
self.assertNotIn("BLUE", pnl_map)
self.assertNotIn("PRODGREEN", state_map)
self.assertNotIn("PRODGREEN", pnl_map)
# ═══════════════════════════════════════════════════════════════════════════
# SUPERVISORD CONFIG VALIDATION
# ═══════════════════════════════════════════════════════════════════════════
class TestSupervisordPinkConfig(unittest.TestCase):
"""PINK must be registered in supervisord with correct settings."""
@classmethod
def setUpClass(cls):
cls.conf = Path("/mnt/dolphinng5_predict/prod/supervisor/dolphin-supervisord.conf").read_text()
cls.pink_sec = cls.conf.split("[program:dolphin_pink]")[1].split("[")[0]
def test_supervisor_config_has_pink(self):
self.assertIn("[program:dolphin_pink]", self.conf)
def test_pink_program_autostart(self):
self.assertIn("[program:dolphin_pink]", self.conf)
def test_pink_uses_correct_launcher(self):
self.assertIn("launch_dolphin_pink.py", self.pink_sec)
def test_pink_env_bingx_env(self):
self.assertIn("DOLPHIN_BINGX_ENV=", self.pink_sec)
def test_pink_env_bingx_allow_mainnet(self):
self.assertIn("DOLPHIN_BINGX_ALLOW_MAINNET=", self.pink_sec)
def test_pink_env_trader_id(self):
self.assertIn("DOLPHIN_TRADER_ID=", self.pink_sec)
def test_pink_uses_python3(self):
self.assertIn("python3", self.pink_sec)
def test_pink_not_in_blue_group(self):
groups_section = self.conf.split("[group:dolphin]")[1].split("[")[0]
self.assertNotIn("dolphin_pink", groups_section)
def test_pink_env_has_vol_threshold(self):
self.assertIn("DOLPHIN_PINK_VOL_P60_THRESHOLD", self.pink_sec)
# ═══════════════════════════════════════════════════════════════════════════
# PINK CTL TOOL
# ═══════════════════════════════════════════════════════════════════════════
class TestPinkCtlTool(unittest.TestCase):
"""PINK ctl tool must operate on PINK namespaces only."""
def test_ctl_imports(self):
import prod.ops.pink_ctl as ctl
self.assertTrue(callable(ctl.status))
self.assertTrue(callable(ctl.healthcheck))
self.assertTrue(callable(ctl.mode_verify))
def test_ctl_status_checks_pink_ch(self):
from prod.ops.pink_ctl import status
with patch("prod.ops.pink_ctl._ch", return_value=[{"n": 5}]) as mock_ch:
rc = status()
self.assertEqual(rc, 0)
def test_ctl_healthcheck_checks_pink_hz(self):
from prod.ops.pink_ctl import healthcheck, HZ_STATE
hz_mock = MagicMock()
hz_mock.get_map.return_value.blocking.return_value.get.return_value = '{"capital": 25000}'
with patch("prod.ops.pink_ctl._ch", return_value=[{"n": 5}]), \
patch("prod.ops.pink_ctl._hz_client", return_value=hz_mock):
rc = healthcheck()
self.assertEqual(rc, 0)
hz_mock.get_map.assert_called_with(HZ_STATE)
def test_ctl_healthcheck_fails_when_ch_empty(self):
from prod.ops.pink_ctl import healthcheck
hz_mock = MagicMock()
hz_mock.get_map.return_value.blocking.return_value.get.return_value = '{"capital": 1}'
with patch("prod.ops.pink_ctl._ch", return_value=[{"n": 0}]), \
patch("prod.ops.pink_ctl._hz_client", return_value=hz_mock):
rc = healthcheck()
self.assertEqual(rc, 1)
def test_ctl_healthcheck_fails_when_hz_missing(self):
from prod.ops.pink_ctl import healthcheck
with patch("prod.ops.pink_ctl._ch", return_value=[{"n": 1}]), \
patch("prod.ops.pink_ctl._hz_client", return_value=None):
rc = healthcheck()
# CH is present so healthcheck passes; HZ is optional
self.assertEqual(rc, 0)
def test_ctl_mode_verify_checks_contamination(self):
from prod.ops.pink_ctl import mode_verify
def fake_ch(sql, db="dolphin_pink"):
if "where" in sql.lower() or "group" in sql.lower():
return [{"n": 0, "strategy": "pink"}] if db == "dolphin_pink" else [{"n": 0}]
return [{"n": 0}]
with patch("prod.ops.pink_ctl._ch", side_effect=fake_ch), \
patch.dict(os.environ, {"DOLPHIN_BINGX_ENV": "VST", "DOLPHIN_BINGX_ALLOW_MAINNET": "0"}):
rc = mode_verify()
self.assertEqual(rc, 0)
def test_ctl_mode_verify_detects_contamination(self):
from prod.ops.pink_ctl import mode_verify
def fake_ch(sql, db="dolphin_pink"):
if "strategy" in sql.lower() or "group" in sql.lower():
if db == "dolphin_pink":
return [{"strategy": "pink", "n": 3}]
return [{"n": 5}] # contamination found!
return [{"n": 3}]
with patch("prod.ops.pink_ctl._ch", side_effect=fake_ch), \
patch.dict(os.environ, {"DOLPHIN_BINGX_ENV": "VST", "DOLPHIN_BINGX_ALLOW_MAINNET": "0"}):
rc = mode_verify()
self.assertEqual(rc, 1)
def test_ctl_status_ch_exception(self):
from prod.ops.pink_ctl import status
with patch("prod.ops.pink_ctl._ch", side_effect=Exception("CH down")):
rc = status()
self.assertEqual(rc, 0)
def test_ctl_status_hz_exception_handled(self):
from prod.ops.pink_ctl import status
# hazelcast is imported inside _hz_client, not at module level
with patch("prod.ops.pink_ctl._ch", return_value=[{"n": 1}]), \
patch("hazelcast.HazelcastClient", side_effect=Exception("HZ down")):
rc = status()
self.assertEqual(rc, 0)
# ═══════════════════════════════════════════════════════════════════════════
# V7 DECISION ROUTING
# ═══════════════════════════════════════════════════════════════════════════
class TestV7DecisionEventRouting(unittest.TestCase):
"""V7 decision events from PINK must route to dolphin_pink."""
def test_v7_journal_db_default_is_dolphin_pink(self):
from prod.ch_writer import PINK_V7_JOURNAL_DB
self.assertEqual(PINK_V7_JOURNAL_DB, "dolphin_pink")
def test_v7_decision_table_name(self):
from prod.ch_writer import V7_DECISION_TABLE
self.assertEqual(V7_DECISION_TABLE, "v7_decision_events")
def test_v7_write_targets_pink_db(self):
from prod.ch_writer import ch_put_pink_v7
self.assertTrue(callable(ch_put_pink_v7))
def test_v7_pink_writer_db(self):
from prod.ch_writer import _writer_pink_v7
self.assertEqual(_writer_pink_v7._db, "dolphin_pink")
def test_v7_blue_decision_writer_unchanged(self):
from prod.ch_writer import _writer, _writer_pink
self.assertEqual(_writer._db, "dolphin")
self.assertEqual(_writer_pink._db, "dolphin_pink")
# ═══════════════════════════════════════════════════════════════════════════
# NAMESPACE BOUNDARY / ISOLATION GUARDS
# ═══════════════════════════════════════════════════════════════════════════
class TestNamespaceIsolationGuards(unittest.TestCase):
"""PINK must never read or write BLUE namespaces."""
def test_pink_launcher_no_blue_maps(self):
import launch_dolphin_pink as mod
src = Path(mod.__file__).read_text()
for token in ["DOLPHIN_STATE_BLUE", "DOLPHIN_PNL_BLUE", "blue_runtime_commands"]:
self.assertNotIn(token, src)
def test_pink_ctl_no_blue_refs(self):
import prod.ops.pink_ctl as mod
src = Path(mod.__file__).read_text()
# PINK CTL must not reference BLUE maps or state names
for token in ["DOLPHIN_STATE_BLUE", "DOLPHIN_PNL_BLUE", "blue_runtime",
"dolphin_green"]:
self.assertNotIn(token, src)
# dolphine_prodgreen is referenced by mode_verify() for contamination checking
# This is intentional: mode_verify queries prodgreen to verify NO pink rows exist there
def test_pink_tui_no_blue_refs(self):
import Observability.dolphin_status_pink as mod
src = Path(mod.__file__).read_text()
for token in ["DOLPHIN_STATE_BLUE", "blue_runtime_commands"]:
self.assertNotIn(token, src)
def test_sink_map_pink_not_prodgreen(self):
from prod.bingx.journal import _STRATEGY_DB_MAP, _STRATEGY_SINK_MAP
self.assertIn("pink", _STRATEGY_DB_MAP)
self.assertIn("pink", _STRATEGY_SINK_MAP)
self.assertNotEqual(_STRATEGY_DB_MAP["pink"], "dolphin_prodgreen")
self.assertNotEqual(_STRATEGY_DB_MAP["pink"], "dolphin")
# ═══════════════════════════════════════════════════════════════════════════
# ENV-DRIVEN NAMESPACE OVERRIDES
# ═══════════════════════════════════════════════════════════════════════════
class TestEnvDrivenNamespaceOverrides(unittest.TestCase):
"""PINK must respect env-driven namespace overrides."""
def test_pink_tui_respects_env_ch_db(self):
with patch.dict(os.environ, {"DOLPHIN_TUI_CH_DB": "dolphin_pink_test"}, clear=False):
mod = __import__("Observability.dolphin_status_pink", fromlist=["PINK_CH_DB"])
importlib.reload(mod)
self.assertEqual(mod.PINK_CH_DB, "dolphin_pink_test")
importlib.reload(__import__("Observability.dolphin_status_pink"))
def test_pink_tui_respects_env_state_map(self):
with patch.dict(os.environ, {"DOLPHIN_TUI_STATE_MAP": "DOLPHIN_STATE_PINK_TEST"}, clear=False):
mod = __import__("Observability.dolphin_status_pink", fromlist=["PINK_STATE_MAP"])
importlib.reload(mod)
self.assertEqual(mod.PINK_STATE_MAP, "DOLPHIN_STATE_PINK_TEST")
importlib.reload(__import__("Observability.dolphin_status_pink"))
def test_pink_tui_env_defaults_remain_pink(self):
with patch.dict(os.environ, {}, clear=False):
mod = __import__("Observability.dolphin_status_pink", fromlist=["PINK_CH_DB"])
importlib.reload(mod)
self.assertEqual(mod.PINK_CH_DB, "dolphin_pink")
self.assertEqual(mod.PINK_STRATEGY, "pink")
importlib.reload(__import__("Observability.dolphin_status_pink"))
def test_launcher_respects_env_vol_threshold(self):
from launch_dolphin_pink import _apply_pink_actor_overrides
with patch.dict(os.environ, {"DOLPHIN_PINK_VOL_P60_THRESHOLD": "0.00005000"}):
cfg = _apply_pink_actor_overrides({"hazelcast": {}, "adaptive_exit": {}})
self.assertAlmostEqual(cfg["vol_p60_threshold"], 0.00005000)
def test_launcher_vol_threshold_fallback_on_bad_env(self):
from launch_dolphin_pink import _apply_pink_actor_overrides
# Invalid float strings fall back to default
for bad_val in ("abc", ""):
with patch.dict(os.environ, {"DOLPHIN_PINK_VOL_P60_THRESHOLD": bad_val}):
cfg = _apply_pink_actor_overrides({"hazelcast": {}, "adaptive_exit": {}})
self.assertAlmostEqual(cfg["vol_p60_threshold"], -1000000000.0)
# Negative values remain valid for relaxed-gate debugging mode.
with patch.dict(os.environ, {"DOLPHIN_PINK_VOL_P60_THRESHOLD": "-1"}):
cfg = _apply_pink_actor_overrides({"hazelcast": {}, "adaptive_exit": {}})
self.assertAlmostEqual(cfg["vol_p60_threshold"], -1.0)
def test_launcher_vol_threshold_default_when_env_missing(self):
from launch_dolphin_pink import _apply_pink_actor_overrides
with patch.dict(os.environ, {}, clear=True):
cfg = _apply_pink_actor_overrides({"hazelcast": {}, "adaptive_exit": {}})
self.assertAlmostEqual(cfg["vol_p60_threshold"], -1000000000.0)
def test_pink_tui_env_defaults_posture_disabled(self):
with patch.dict(os.environ, {}, clear=True):
mod = __import__("Observability.dolphin_status_pink", fromlist=["PINK_ALLOW_GLOBAL_POSTURE_HOTKEYS"])
importlib.reload(mod)
self.assertFalse(mod.PINK_ALLOW_GLOBAL_POSTURE_HOTKEYS)
importlib.reload(__import__("Observability.dolphin_status_pink"))
# ═══════════════════════════════════════════════════════════════════════════
# VST SAFETY GATES
# ═══════════════════════════════════════════════════════════════════════════
class TestVstSafetyGates(unittest.TestCase):
"""PINK VST safety must prevent accidental mainnet execution."""
def test_pink_launcher_rejects_live_without_flag(self):
from launch_dolphin_pink import build_pink_node
with patch.dict(os.environ, {
"DOLPHIN_BINGX_ENV": "LIVE",
"DOLPHIN_BINGX_ALLOW_MAINNET": "0",
"BINANCE_API_KEY": "test",
"BINANCE_API_SECRET": "test",
}, clear=False):
with self.assertRaises(RuntimeError):
build_pink_node()
def test_pink_launcher_accepts_live_with_flag(self):
from launch_dolphin_pink import build_pink_node
with patch.dict(os.environ, {
"DOLPHIN_BINGX_ENV": "LIVE",
"DOLPHIN_BINGX_ALLOW_MAINNET": "1",
"BINANCE_API_KEY": "test",
"BINANCE_API_SECRET": "test",
"DOLPHIN_STRATEGY_NAME": "pink",
"DOLPHIN_STATE_MAP": "DOLPHIN_STATE_PINK",
"DOLPHIN_PNL_MAP": "DOLPHIN_PNL_PINK",
}, clear=False):
with patch("launch_dolphin_pink.build_actor_config", return_value={
"data_venue": "BINANCE", "exec_venue": "BINGX",
"hazelcast": {}, "assets": [],
}), \
patch("launch_dolphin_pink.BinanceDataClientConfig"), \
patch("launch_dolphin_pink.build_bingx_exec_client_config"), \
patch("launch_dolphin_pink.TradingNode"):
try:
build_pink_node()
except RuntimeError:
self.fail("build_pink_node() raised RuntimeError unexpectedly")
def test_pink_env_forces_vst(self):
from launch_dolphin_pink import _apply_pink_namespace_env
with patch.dict(os.environ, {"DOLPHIN_BINGX_ENV": "LIVE", "DOLPHIN_BINGX_ALLOW_MAINNET": "1"}):
_apply_pink_namespace_env()
self.assertEqual(os.environ["DOLPHIN_BINGX_ENV"], "VST")
self.assertEqual(os.environ["DOLPHIN_BINGX_ALLOW_MAINNET"], "0")
# ═══════════════════════════════════════════════════════════════════════════
# E2E SIMULATED SCENARIOS
# ═══════════════════════════════════════════════════════════════════════════
class FakeHzBlocking:
def __init__(self, store):
self._store = store
def get(self, k):
return self._store.get(k)
def put(self, k, v):
self._store[k] = v
def key_set(self):
return list(self._store.keys())
class FakeHzMapRef:
def __init__(self, store):
self._store = store
def blocking(self):
return FakeHzBlocking(self._store)
class FakeHzClient:
def __init__(self):
self.maps = {}
def get_map(self, name):
if name not in self.maps:
self.maps[name] = {}
return FakeHzMapRef(self.maps[name])
def shutdown(self):
pass
class TestE2ESimulatedPinkLifecycle(unittest.TestCase):
"""End-to-end simulated PINK lifecycle with fake HZ + CH."""
def setUp(self):
self.hz = FakeHzClient()
self._seed_health_data()
def _seed_health_data(self):
import json
b = lambda d: json.dumps(d)
sp = self.hz.get_map("DOLPHIN_STATE_PINK").blocking()
sp.put("engine_snapshot", b({
"capital": 25000.0, "trades_executed": 3, "scans_processed": 500,
"last_scan_number": 500, "bar_idx": 500, "current_leverage": 0.0,
"open_notional": 0.0, "open_positions": [], "posture": "APEX",
"vol_ok": True, "last_vel_div": -0.03, "vol_gate_threshold": 0.00008,
}))
sp.put("capital_checkpoint", b({"capital": 25000.0}))
self.hz.get_map("DOLPHIN_SAFETY").blocking().put("latest", b({
"posture": "APEX", "Rm": 0.95, "breakdown": {"Cat1": 1.0, "Cat2": 1.0, "Cat3": 1.0, "Cat4": 1.0, "Cat5": 0.97}}))
self.hz.get_map("DOLPHIN_HEARTBEAT").blocking().put("nautilus_flow_heartbeat", b({
"ts": _time.time(), "phase": "trading"}))
self.hz.get_map("DOLPHIN_META_HEALTH").blocking().put("latest", b({
"status": "GREEN", "rm_meta": 0.95, "service_status": {}, "hz_key_status": {},
"m1_data_infra": 1.0, "m1_trader": 1.0, "m2_heartbeat": 1.0,
"m3_data_freshness": 1.0, "m4_control_plane": 1.0, "m5_coherence": 1.0}))
self.hz.get_map("DOLPHIN_ANNOUNCEMENTS").blocking().put("latest", b({}))
fm = self.hz.get_map("DOLPHIN_FEATURES").blocking()
fm.put("acb_boost", b({"boost": 1.0, "ready": True}))
fm.put("exf_latest", b({}))
fm.put("obf_universe_latest", b({}))
fm.put("esof_advisor_latest", b({}))
fm.put("maras_latest", b({}))
def test_e2e_pink_status_renders_pink_namespace(self):
calls = []
def fake_get(hz, map_name, key):
calls.append((map_name, key))
m = self.hz.get_map(map_name)
raw = m.blocking().get(key)
import json
return json.loads(raw) if isinstance(raw, str) else raw
import Observability.dolphin_status_pink as status
# Ensure env defaults are set
with patch.object(status, "PINK_STATE_MAP", "DOLPHIN_STATE_PINK"), \
patch.object(status, "PINK_CH_DB", "dolphin_pink"), \
patch.object(status, "PINK_STRATEGY", "pink"), \
patch.object(status, "_get", side_effect=fake_get), \
patch.object(status, "_last_n_trades", return_value=[]):
text = status.render("hz")
self.assertIn("DOLPHIN-PINK", text)
self.assertIn("APEX", text)
self.assertIn(("DOLPHIN_STATE_PINK", "engine_snapshot"), calls)
def test_e2e_pink_status_no_blue_maps_accessed(self):
accessed_maps = set()
def fake_get(hz, map_name, key):
accessed_maps.add(map_name)
m = self.hz.get_map(map_name)
raw = m.blocking().get(key)
import json
return json.loads(raw) if isinstance(raw, str) else raw
import Observability.dolphin_status_pink as status
with patch.object(status, "_get", side_effect=fake_get), \
patch.object(status, "_last_n_trades", return_value=[]):
status.render("hz")
for m in accessed_maps:
self.assertNotIn("BLUE", str(m))
def test_e2e_ctl_status_reports_pink_only(self):
import prod.ops.pink_ctl as ctl
calls = []
def fake_ch(sql, db="dolphin_pink"):
calls.append(db)
self.assertEqual(db, "dolphin_pink")
return [{"n": 10}]
with patch("prod.ops.pink_ctl._ch", side_effect=fake_ch), \
patch("prod.ops.pink_ctl._hz_client", return_value=self.hz):
rc = ctl.status()
self.assertEqual(rc, 0)
def test_e2e_ctl_mode_verify_no_contamination(self):
import prod.ops.pink_ctl as ctl
def fake_ch(sql, db="dolphin_pink"):
if "count" in sql.lower() or "strategy" in sql.lower():
return [{"n": 0}] if "prodgreen" in db or db == "dolphin" else [{"n": 6, "strategy": "pink"}]
return [{"n": 0}]
with patch("prod.ops.pink_ctl._ch", side_effect=fake_ch), \
patch.dict(os.environ, {"DOLPHIN_BINGX_ENV": "VST", "DOLPHIN_BINGX_ALLOW_MAINNET": "0"}):
rc = ctl.mode_verify()
self.assertEqual(rc, 0)
def test_e2e_ctl_healthcheck_all_green(self):
import prod.ops.pink_ctl as ctl
with patch("prod.ops.pink_ctl._ch", return_value=[{"n": 5}]), \
patch("prod.ops.pink_ctl._hz_client", return_value=self.hz):
rc = ctl.healthcheck()
self.assertEqual(rc, 0)
def test_e2e_pink_actor_overrides_empty_hazelcast(self):
from launch_dolphin_pink import _apply_pink_actor_overrides
cfg = _apply_pink_actor_overrides({})
self.assertEqual(cfg.get("strategy_name"), "pink")
self.assertEqual(cfg.get("hazelcast", {}).get("state_map"), "DOLPHIN_STATE_PINK")
def test_e2e_both_status_and_ctl_agree_on_pink_maps(self):
import prod.ops.pink_ctl as ctl
self.assertEqual(ctl.HZ_STATE, "DOLPHIN_STATE_PINK")
self.assertEqual(ctl.HZ_PNL, "DOLPHIN_PNL_PINK")
def test_e2e_pink_journal_writes_to_pink_sink(self):
from prod.bingx.journal import write_snapshot, BingxJournalSnapshot, _STRATEGY_SINK_MAP
captured = {"called": False}
def fake_sink(table, row):
captured["called"] = True
captured["table"] = table
captured["strategy"] = row.get("strategy")
with patch.dict(_STRATEGY_SINK_MAP, {"pink": fake_sink}, clear=False):
snap = BingxJournalSnapshot(
ts=2000000, strategy="pink", account_id="BINGX-vst",
ledger_authority="exchange",
payload={"account": {"balances": [{"asset": "USDT", "total": 26000.0, "free": 25500.0}]}, "positions": {}},
fingerprint="pink-fp-001",
)
write_snapshot(snap)
self.assertTrue(captured.get("called"))
self.assertEqual(captured.get("strategy"), "pink")
# ═══════════════════════════════════════════════════════════════════════════
# PINK CONFIG FILE PARITY
# ═══════════════════════════════════════════════════════════════════════════
class TestPinkConfigParity(unittest.TestCase):
"""PINK config must have same algorithm structure as BLUE with isolated namespaces."""
@classmethod
def setUpClass(cls):
import yaml
cls.pink = yaml.safe_load(Path("/mnt/dolphinng5_predict/prod/configs/pink.yml").read_text())
cls.blue = yaml.safe_load(Path("/mnt/dolphinng5_predict/prod/configs/blue.yml").read_text())
def test_pink_has_engine_section(self):
self.assertIn("engine", self.pink)
def test_pink_has_paper_trade_section(self):
self.assertIn("paper_trade", self.pink)
def test_pink_has_hazelcast_section(self):
self.assertIn("hazelcast", self.pink)
def test_pink_direction_matches_blue(self):
self.assertEqual(self.pink["direction"], self.blue["direction"])
def test_pink_boost_mode_matches_blue(self):
self.assertEqual(self.pink["engine"]["boost_mode"], self.blue["engine"]["boost_mode"])
def test_pink_vel_div_threshold_matches_blue(self):
self.assertEqual(self.pink["engine"]["vel_div_threshold"], self.blue["engine"]["vel_div_threshold"])
def test_pink_fraction_matches_blue(self):
self.assertEqual(self.pink["engine"]["fraction"], self.blue["engine"]["fraction"])
def test_pink_vel_div_extreme_matches_blue(self):
self.assertEqual(self.pink["engine"]["vel_div_extreme"], self.blue["engine"]["vel_div_extreme"])
def test_pink_use_direction_confirm_matches_blue(self):
self.assertEqual(self.pink["engine"]["use_direction_confirm"], self.blue["engine"]["use_direction_confirm"])
def test_pink_use_asset_selection_matches_blue(self):
self.assertEqual(self.pink["engine"]["use_asset_selection"], self.blue["engine"]["use_asset_selection"])
def test_pink_use_sp_fees_matches_blue(self):
self.assertEqual(self.pink["engine"]["use_sp_fees"], self.blue["engine"]["use_sp_fees"])
def test_pink_use_exit_v7_matches_blue(self):
self.assertEqual(self.pink["engine"]["use_exit_v7"], self.blue["engine"]["use_exit_v7"])
def test_pink_hazelcast_maps_isolated(self):
self.assertEqual(self.pink["hazelcast"]["state_map"], "DOLPHIN_STATE_PINK")
self.assertEqual(self.pink["hazelcast"]["imap_pnl"], "DOLPHIN_PNL_PINK")
self.assertNotEqual(self.pink["hazelcast"]["state_map"], self.blue["hazelcast"]["imap_state"])
def test_pink_adaptive_exit_points_to_dolphin_pink(self):
self.assertEqual(self.pink["adaptive_exit"]["shadow_db"], "dolphin_pink")
def test_pink_initial_capital_matches_blue(self):
self.assertEqual(self.pink["paper_trade"]["initial_capital"], self.blue["paper_trade"]["initial_capital"])
def test_pink_has_distinct_log_dir(self):
self.assertEqual(self.pink["paper_trade"]["log_dir"], "paper_logs/pink")
def test_pink_isolated_tp_differs_intentionally(self):
# PINK uses 0.20% TP (not 0.95%) — intentional for testnet
self.assertEqual(self.pink["engine"]["fixed_tp_pct"], 0.0020)
# ═══════════════════════════════════════════════════════════════════════════
# CH SCHEMA FILE VALIDATION
# ═══════════════════════════════════════════════════════════════════════════
class TestPinkSchemaFileContent(unittest.TestCase):
"""PINK CH schema files must target dolphin_pink exclusively."""
def test_schema_dir_exists(self):
self.assertTrue(Path("/mnt/dolphinng5_predict/prod/clickhouse/pink").is_dir())
def test_create_database_has_if_not_exists(self):
ddl = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink/00_create_database.sql").read_text()
self.assertIn("IF NOT EXISTS", ddl)
self.assertIn("dolphin_pink", ddl)
def test_all_sql_files_reference_dolphin_pink(self):
schema_dir = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink")
for sql_file in sorted(schema_dir.glob("*.sql")):
content = sql_file.read_text()
self.assertIn("dolphin_pink", content)
self.assertNotIn("dolphin_prodgreen", content)
self.assertNotIn("dolphin_green", content)
self.assertNotIn("dolphin.", content)
def test_schema_files_have_no_blind_copy_errors(self):
schema_dir = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink")
for sql_file in sorted(schema_dir.glob("*.sql")):
content = sql_file.read_text()
self.assertNotIn("_BLUE", content)
self.assertNotIn("_PRODGREEN", content)
# ═══════════════════════════════════════════════════════════════════════════
# PINK JOURNAL / ACCOUNTING
# ═══════════════════════════════════════════════════════════════════════════
class TestPinkJournalAccounting(unittest.TestCase):
"""PINK journal must route accounting data to dolphin_pink."""
def test_strategy_db_map_has_pink(self):
from prod.bingx.journal import _STRATEGY_DB_MAP
self.assertEqual(_STRATEGY_DB_MAP["pink"], "dolphin_pink")
def test_strategy_sink_map_has_pink(self):
from prod.bingx.journal import _STRATEGY_SINK_MAP
self.assertIn("pink", _STRATEGY_SINK_MAP)
def test_strategy_db_map_completeness(self):
from prod.bingx.journal import _STRATEGY_DB_MAP
for strategy in ("blue", "green", "prodgreen", "pink"):
self.assertIn(strategy, _STRATEGY_DB_MAP)
def test_strategy_sink_map_completeness(self):
from prod.bingx.journal import _STRATEGY_SINK_MAP
for strategy in ("blue", "green", "prodgreen", "pink"):
self.assertIn(strategy, _STRATEGY_SINK_MAP)
def test_pink_sink_is_ch_put_pink(self):
from prod.bingx.journal import _STRATEGY_SINK_MAP
import prod.ch_writer as ch
self.assertIs(_STRATEGY_SINK_MAP["pink"], ch.ch_put_pink)
def test_db_for_strategy_pink(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("pink"), "dolphin_pink")
def test_db_for_strategy_case_insensitive(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("PINK"), "dolphin_pink")
self.assertEqual(_db_for_strategy("Pink"), "dolphin_pink")
def test_db_for_strategy_blue_unchanged(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("blue"), "dolphin")
def test_db_for_strategy_prodgreen_unchanged(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("prodgreen"), "dolphin_prodgreen")
def test_db_for_strategy_prodprefix_fallback(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("prodfoo"), "dolphin_prodgreen")
def test_journal_snapshot_strategy_field(self):
from prod.bingx.journal import BingxJournalSnapshot
snap = BingxJournalSnapshot(
ts=100, strategy="pink", account_id="test", ledger_authority="exchange",
payload={"account": {"balances": []}, "positions": {}}, fingerprint="fp")
self.assertEqual(snap.strategy, "pink")
def test_ch_put_pink_exists(self):
from prod.ch_writer import ch_put_pink
self.assertTrue(callable(ch_put_pink))
def test_ch_put_pink_calls_pink_writer(self):
from prod.ch_writer import ch_put_pink, _writer_pink
with patch.object(_writer_pink, 'put') as mock_put:
ch_put_pink("test_table", {"key": "value"})
mock_put.assert_called_once_with("test_table", {"key": "value"})
def test_writer_pink_db_is_dolphin_pink(self):
from prod.ch_writer import _writer_pink
self.assertEqual(_writer_pink._db, "dolphin_pink")
def test_writer_prodgreen_unchanged(self):
from prod.ch_writer import _writer_prodgreen
self.assertEqual(_writer_prodgreen._db, "dolphin_prodgreen")
def test_writer_blue_unchanged(self):
from prod.ch_writer import _writer
self.assertEqual(_writer._db, "dolphin")
def test_writer_green_unchanged(self):
from prod.ch_writer import _writer_green
self.assertEqual(_writer_green._db, "dolphin_green")
# ═══════════════════════════════════════════════════════════════════════════
# PINK CH SCHEMA REQUIRED FILES
# ═══════════════════════════════════════════════════════════════════════════
class TestPinkClickHouseSchema(unittest.TestCase):
"""PINK CH schema files must exist and be complete."""
def test_schema_dir_exists(self):
self.assertTrue(Path("/mnt/dolphinng5_predict/prod/clickhouse/pink").is_dir())
def test_required_schema_files(self):
schema_dir = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink")
required = [
"00_create_database.sql", "account_events.sql", "trade_events.sql",
"status_snapshots.sql", "v7_decision_events.sql", "adaptive_exit_shadow.sql",
"02_create_trade_reconstruction.sql", "03_create_trade_exit_legs.sql",
]
for filename in required:
self.assertTrue((schema_dir / filename).exists(), f"Missing: {filename}")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,53 @@
from __future__ import annotations
import asyncio
import json
from datetime import datetime
import pytest
from prod.clean_arch.adapters.hazelcast_feed import HazelcastDataFeed
class _FakeMap:
def __init__(self, payload: str) -> None:
self.payload = payload
def get(self, key: str):
if key == "latest_eigen_scan":
return self.payload
return None
def size(self) -> int:
return 1
def test_single_result_scan_schema_is_accepted() -> None:
payload = json.dumps(
{
"scan_number": 2576,
"timestamp": 1779805956.9522693,
"target_asset": "BTCUSDT",
"result": {
"asset": "BTCUSDT",
"price": 77599.64,
"eigenvalue_tracking": {"lambda_max": 24.6, "lambda_max_velocity": -0.0053},
"multi_window_results": {
"50": {"tracking_data": {"lambda_max_velocity": -0.19346329413310556}},
"750": {"tracking_data": {"lambda_max_velocity": -0.0001833266579540457}},
},
"confidence": 0.79,
},
}
)
feed = HazelcastDataFeed({"hazelcast": {"cluster": "dolphin", "host": "localhost:5701"}})
feed.features_map = _FakeMap(payload)
snapshot = asyncio.run(feed.get_latest_snapshot("BTCUSDT"))
assert snapshot is not None
assert snapshot.symbol == "BTCUSDT"
assert snapshot.price == 77599.64
assert snapshot.velocity_divergence == pytest.approx(-0.19327996747515153)
assert snapshot.irp_alignment == 0.79
assert snapshot.scan_number == 2576

View File

@@ -0,0 +1,499 @@
"""
Unit tests for PINK namespace routing and isolation.
Validates:
- ch_writer ch_put_pink targets dolphin_pink
- journal _db_for_strategy routes pink -> dolphin_pink
- journal write_snapshot selects pink sink
- dolphin_actor ch_put mapping for pink
- No cross-contamination between BLUE/PRODGREEN/PINK
"""
import json
import os
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch, call
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "nautilus_dolphin"))
sys.path.insert(0, str(Path(__file__).parent.parent))
class TestChWriterPink(unittest.TestCase):
"""Test ch_writer ch_put_pink targets dolphin_pink database."""
@patch("prod.ch_writer._CHWriter")
def test_ch_put_pink_targets_dolphin_pink(self, MockWriter):
mock_instance = MagicMock()
MockWriter.return_value = mock_instance
MockWriter.reset_mock()
# Re-import to pick up the mock
import importlib
import prod.ch_writer as ch_mod
importlib.reload(ch_mod)
# After reload, the module-level singletons are recreated
# We need to verify ch_put_pink calls the right writer
# The simplest approach: verify the _writer_pink singleton has db="dolphin_pink"
def test_writer_pink_db_attribute(self):
"""Verify _writer_pink targets dolphin_pink database."""
from prod.ch_writer import _writer_pink
self.assertEqual(_writer_pink._db, "dolphin_pink")
def test_writer_prodgreen_unchanged(self):
"""Verify PRODGREEN writer is unchanged."""
from prod.ch_writer import _writer_prodgreen
self.assertEqual(_writer_prodgreen._db, "dolphin_prodgreen")
def test_writer_blue_unchanged(self):
"""Verify BLUE writer is unchanged."""
from prod.ch_writer import _writer
self.assertEqual(_writer._db, "dolphin")
def test_writer_green_unchanged(self):
"""Verify GREEN writer is unchanged."""
from prod.ch_writer import _writer_green
self.assertEqual(_writer_green._db, "dolphin_green")
def test_ch_put_pink_exists(self):
"""Verify ch_put_pink function exists and is callable."""
from prod.ch_writer import ch_put_pink
self.assertTrue(callable(ch_put_pink))
def test_ch_put_pink_calls_put(self):
"""Verify ch_put_pink delegates to _writer_pink.put."""
from prod.ch_writer import _writer_pink
with patch.object(_writer_pink, 'put') as mock_put:
from prod.ch_writer import ch_put_pink
ch_put_pink("test_table", {"key": "value"})
mock_put.assert_called_once_with("test_table", {"key": "value"})
class TestJournalRouting(unittest.TestCase):
"""Test bingx/journal.py strategy->DB routing."""
def test_db_for_strategy_pink(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("pink"), "dolphin_pink")
def test_db_for_strategy_pink_case_insensitive(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("PINK"), "dolphin_pink")
self.assertEqual(_db_for_strategy("Pink"), "dolphin_pink")
def test_db_for_strategy_prodgreen_unchanged(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("prodgreen"), "dolphin_prodgreen")
def test_db_for_strategy_green_unchanged(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("green"), "dolphin_green")
def test_db_for_strategy_blue_unchanged(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("blue"), "dolphin")
def test_db_for_strategy_prodprefix_unchanged(self):
"""Existing prod* prefix fallback must still work for unknown prod names."""
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("prodfoo"), "dolphin_prodgreen")
def test_db_for_strategy_unknown_default(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("unknown"), "dolphin")
def test_strategy_db_map_has_pink(self):
from prod.bingx.journal import _STRATEGY_DB_MAP
self.assertEqual(_STRATEGY_DB_MAP["pink"], "dolphin_pink")
def test_strategy_sink_map_has_pink(self):
from prod.bingx.journal import _STRATEGY_SINK_MAP
sink = _STRATEGY_SINK_MAP["pink"]
self.assertTrue(callable(sink))
self.assertEqual(getattr(sink, "__name__", ""), "ch_put_pink")
class TestJournalSinkSelection(unittest.TestCase):
"""Test that write_snapshot selects the correct sink for pink strategy."""
@patch("prod.bingx.journal._STRATEGY_SINK_MAP")
def test_write_snapshot_uses_pink_sink(self, mock_map):
from prod.bingx.journal import write_snapshot, BingxJournalSnapshot
mock_sink = MagicMock()
mock_map.get.return_value = mock_sink
snapshot = BingxJournalSnapshot(
ts=1000000,
strategy="pink",
account_id="BINGX-vst",
ledger_authority="exchange",
payload={
"account": {"balances": [{"asset": "USDT", "total": 25000.0, "free": 25000.0}]},
"positions": {},
},
fingerprint="abc123",
)
write_snapshot(snapshot)
# Verify the sink map was consulted for "pink"
mock_map.get.assert_called_with("pink")
# Verify the pink sink was called (not prodgreen or green)
mock_sink.assert_called_once()
class TestExecutionConfigFields(unittest.TestCase):
"""Test that execution.py reads config-driven journal_strategy/journal_db."""
def test_config_has_journal_fields(self):
from prod.bingx.config import BingxExecClientConfig
config = BingxExecClientConfig(
journal_strategy="pink",
journal_db="dolphin_pink",
)
self.assertEqual(config.journal_strategy, "pink")
self.assertEqual(config.journal_db, "dolphin_pink")
def test_config_defaults_none(self):
from prod.bingx.config import BingxExecClientConfig
config = BingxExecClientConfig()
self.assertIsNone(config.journal_strategy)
self.assertIsNone(config.journal_db)
class TestBuildActorConfigOverrides(unittest.TestCase):
"""Test launch_dolphin_live actor DB override behavior."""
def test_v7_journal_db_does_not_overwrite_adaptive_exit_shadow_db(self):
from prod.launch_dolphin_live import build_actor_config
with patch.dict(
os.environ,
{
"DOLPHIN_ADAPTIVE_EXIT_DB": "dolphin_pink",
"DOLPHIN_V7_JOURNAL_DB": "dolphin_pink_v7",
"DOLPHIN_FIXED_TP_PCT": "0.0020",
},
clear=False,
):
cfg = build_actor_config()
self.assertEqual(cfg["adaptive_exit"]["shadow_db"], "dolphin_pink")
self.assertEqual(cfg["v7_journal_db"], "dolphin_pink_v7")
self.assertEqual(cfg["engine"]["fixed_tp_pct"], 0.0020)
class TestPinkLauncherPhases(unittest.TestCase):
"""Test the standalone PINK phase gate helpers."""
def test_single_leg_is_default_phase(self):
from prod.launch_dolphin_pink import PinkPhase, _resolve_pink_phase, _resolve_pink_exit_leg_ratios
with patch.dict(os.environ, {}, clear=False):
self.assertEqual(_resolve_pink_phase(), PinkPhase.SINGLE_LEG)
self.assertEqual(_resolve_pink_exit_leg_ratios(PinkPhase.SINGLE_LEG), (1.0,))
def test_multi_exit_uses_configured_leg_ratios(self):
from prod.launch_dolphin_pink import PinkPhase, _resolve_pink_exit_leg_ratios, _resolve_pink_phase
with patch.dict(
os.environ,
{
"DOLPHIN_PINK_PHASE": "multi_exit",
"DOLPHIN_PINK_EXIT_LEG_RATIOS": "0.25,0.75,1.0",
},
clear=False,
):
self.assertEqual(_resolve_pink_phase(), PinkPhase.MULTI_EXIT)
self.assertEqual(_resolve_pink_exit_leg_ratios(PinkPhase.MULTI_EXIT), (0.25, 0.75, 1.0))
class TestCapitalSourcePriority(unittest.TestCase):
"""BingX/PINK must prefer the BingX journal over portfolio fallbacks."""
def test_bingx_journal_wins_over_portfolio_and_engine(self):
from nautilus_dolphin.nautilus.dolphin_actor import DolphinActor
class Dummy:
def __init__(self):
self.live_mode = True
self.dolphin_config = {"native_mode": False}
self._last_portfolio_capital = 777.0
self.engine = type("E", (), {"capital": 555.0})()
def _exec_venue_name(self):
return "BINGX"
def _get_bingx_ledger_capital(self):
return 1234.5
def _get_portfolio_capital(self):
return 888.0
dummy = Dummy()
capital = DolphinActor._authoritative_capital(dummy)
self.assertEqual(capital, 1234.5)
class TestDolphinActorPinkMapping(unittest.TestCase):
"""Test DolphinActor correctly maps pink strategy to pink sink."""
def test_actor_pink_strategy_uses_pink_sink(self):
"""Verify pink strategy in actor config selects ch_put_pink."""
# We can't fully instantiate DolphinActor (needs nautilus),
# but we can test the mapping logic directly.
from ch_writer import ch_put_pink, ch_put_prodgreen, ch_put_green
_STRATEGY_CH_SINK = {
'blue': None,
'green': ch_put_green,
'prodgreen': ch_put_prodgreen,
'pink': ch_put_pink,
}
self.assertIs(_STRATEGY_CH_SINK['pink'], ch_put_pink)
self.assertIs(_STRATEGY_CH_SINK['prodgreen'], ch_put_prodgreen)
self.assertIs(_STRATEGY_CH_SINK['green'], ch_put_green)
class TestPinkConfigFile(unittest.TestCase):
"""Test that pink.yml has correct namespace settings."""
def test_pink_config_exists(self):
config_path = Path("/mnt/dolphinng5_predict/prod/configs/pink.yml")
self.assertTrue(config_path.exists(), "pink.yml must exist")
def test_pink_config_has_correct_strategy(self):
import yaml
config_path = Path("/mnt/dolphinng5_predict/prod/configs/pink.yml")
with open(config_path) as f:
cfg = yaml.safe_load(f)
self.assertEqual(cfg["strategy_name"], "pink")
self.assertEqual(cfg["hazelcast"]["state_map"], "DOLPHIN_STATE_PINK")
self.assertEqual(cfg["hazelcast"]["imap_pnl"], "DOLPHIN_PNL_PINK")
self.assertEqual(cfg["adaptive_exit"]["shadow_db"], "dolphin_pink")
self.assertEqual(cfg["engine"]["fixed_tp_pct"], 0.0020)
class TestPinkLauncher(unittest.TestCase):
"""Test PINK launcher defaults."""
def test_pink_defaults(self):
sys.path.insert(0, str(Path(__file__).parent.parent))
from launch_dolphin_pink import PINK_DEFAULTS
self.assertEqual(PINK_DEFAULTS["strategy_name"], "pink")
self.assertEqual(PINK_DEFAULTS["state_map"], "DOLPHIN_STATE_PINK")
self.assertEqual(PINK_DEFAULTS["pnl_map"], "DOLPHIN_PNL_PINK")
self.assertEqual(PINK_DEFAULTS["trader_id"], "DOLPHIN-PINK-001")
self.assertEqual(PINK_DEFAULTS["journal_strategy"], "pink")
self.assertEqual(PINK_DEFAULTS["journal_db"], "dolphin_pink")
self.assertEqual(PINK_DEFAULTS["fixed_tp_pct"], 0.0020)
self.assertEqual(PINK_DEFAULTS["vol_p60_threshold"], -1000000000.0)
def test_apply_pink_namespace_env_forces_testnet_namespace(self):
sys.path.insert(0, str(Path(__file__).parent.parent))
from launch_dolphin_pink import _apply_pink_namespace_env
with patch.dict(
os.environ,
{
"DOLPHIN_BINGX_ENV": "LIVE",
"DOLPHIN_BINGX_ALLOW_MAINNET": "1",
"DOLPHIN_STATE_MAP": "DOLPHIN_STATE_PRODGREEN",
"DOLPHIN_PNL_MAP": "DOLPHIN_PNL_PRODGREEN",
"DOLPHIN_STRATEGY_NAME": "prodgreen",
},
clear=False,
):
_apply_pink_namespace_env()
self.assertEqual(os.environ["DOLPHIN_BINGX_ENV"], "VST")
self.assertEqual(os.environ["DOLPHIN_BINGX_ALLOW_MAINNET"], "0")
self.assertEqual(os.environ["DOLPHIN_STRATEGY_NAME"], "pink")
self.assertEqual(os.environ["DOLPHIN_STATE_MAP"], "DOLPHIN_STATE_PINK")
self.assertEqual(os.environ["DOLPHIN_PNL_MAP"], "DOLPHIN_PNL_PINK")
self.assertEqual(os.environ["DOLPHIN_FIXED_TP_PCT"], "0.0020")
def test_apply_pink_actor_overrides_forces_alias_and_blue_sync_isolation(self):
sys.path.insert(0, str(Path(__file__).parent.parent))
from launch_dolphin_pink import _apply_pink_actor_overrides
actor_cfg = {
"strategy_name": "prodgreen",
"hazelcast": {
"state_map": "DOLPHIN_STATE_PRODGREEN",
"imap_pnl": "DOLPHIN_PNL_PRODGREEN",
"state_map_aliases": ["DOLPHIN_STATE_GREEN"],
"imap_pnl_aliases": ["DOLPHIN_PNL_GREEN"],
},
"adaptive_exit": {"shadow_db": "dolphin_prodgreen"},
"v7_journal_db": "dolphin_prodgreen",
"sync_bar_idx_from_blue": True,
}
updated = _apply_pink_actor_overrides(actor_cfg)
self.assertEqual(updated["strategy_name"], "pink")
self.assertEqual(updated["hazelcast"]["state_map"], "DOLPHIN_STATE_PINK")
self.assertEqual(updated["hazelcast"]["imap_pnl"], "DOLPHIN_PNL_PINK")
self.assertEqual(updated["hazelcast"]["state_map_aliases"], [])
self.assertEqual(updated["hazelcast"]["imap_pnl_aliases"], [])
self.assertEqual(updated["adaptive_exit"]["shadow_db"], "dolphin_pink")
self.assertEqual(updated["v7_journal_db"], "dolphin_pink")
self.assertEqual(updated["vol_p60_threshold"], -1000000000.0)
self.assertEqual(updated["paper_trade"]["vol_p60"], -1000000000.0)
self.assertFalse(updated["sync_bar_idx_from_blue"])
def test_apply_pink_actor_overrides_respects_env_vol_threshold(self):
sys.path.insert(0, str(Path(__file__).parent.parent))
from launch_dolphin_pink import _apply_pink_actor_overrides
with patch.dict(os.environ, {"DOLPHIN_PINK_VOL_P60_THRESHOLD": "0.00007000"}, clear=False):
updated = _apply_pink_actor_overrides({"hazelcast": {}, "adaptive_exit": {}})
self.assertEqual(updated["vol_p60_threshold"], 0.00007000)
class TestIsolationGuards(unittest.TestCase):
"""Verify PINK never aliases to BLUE namespaces."""
def test_pink_config_no_blue_maps(self):
import yaml
config_path = Path("/mnt/dolphinng5_predict/prod/configs/pink.yml")
with open(config_path) as f:
cfg = yaml.safe_load(f)
state_map = cfg["hazelcast"]["state_map"]
pnl_map = cfg["hazelcast"]["imap_pnl"]
self.assertNotIn("BLUE", state_map)
self.assertNotIn("BLUE", pnl_map)
self.assertNotIn("PRODGREEN", state_map)
self.assertNotIn("PRODGREEN", pnl_map)
def test_pink_aliases_empty(self):
import yaml
config_path = Path("/mnt/dolphinng5_predict/prod/configs/pink.yml")
with open(config_path) as f:
cfg = yaml.safe_load(f)
aliases = cfg["hazelcast"].get("state_map_aliases", [])
pnl_aliases = cfg["hazelcast"].get("imap_pnl_aliases", [])
self.assertEqual(aliases, [])
self.assertEqual(pnl_aliases, [])
class TestPinkClickHouseSchema(unittest.TestCase):
"""Test that PINK CH schema files exist."""
def test_schema_dir_exists(self):
schema_dir = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink")
self.assertTrue(schema_dir.is_dir())
def test_pink_schema_files_include_namespace_tags(self):
schema_dir = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink")
for file_name in [
"account_events.sql",
"trade_events.sql",
"v7_decision_events.sql",
"adaptive_exit_shadow.sql",
]:
text = (schema_dir / file_name).read_text()
self.assertIn("runtime_namespace", text)
self.assertIn("strategy_namespace", text)
self.assertIn("event_namespace", text)
self.assertIn("actor_name", text)
self.assertIn("exec_venue", text)
self.assertIn("data_venue", text)
class TestPinkRowTagging(unittest.TestCase):
"""Test PINK writes carry standalone namespace tags."""
def test_dolphin_actor_tagged_ch_put_injects_pink_namespace(self):
from nautilus_dolphin.nautilus.dolphin_actor import DolphinActor
actor = DolphinActor.__new__(DolphinActor)
actor._strategy_name = "pink"
actor._pink_row_tags = {
"runtime_namespace": "pink",
"strategy_namespace": "pink",
"event_namespace": "pink",
"actor_name": "DolphinActor",
"exec_venue": "BINGX",
"data_venue": "BINANCE",
}
actor._ch_put_base = MagicMock()
DolphinActor._pink_tagged_ch_put(actor, "trade_events", {"ts": 1, "strategy": "pink"})
actor._ch_put_base.assert_called_once()
table, row = actor._ch_put_base.call_args.args
self.assertEqual(table, "trade_events")
self.assertEqual(row["strategy"], "pink")
self.assertEqual(row["runtime_namespace"], "pink")
self.assertEqual(row["strategy_namespace"], "pink")
self.assertEqual(row["event_namespace"], "pink")
self.assertEqual(row["actor_name"], "DolphinActor")
self.assertEqual(row["exec_venue"], "BINGX")
self.assertEqual(row["data_venue"], "BINANCE")
def test_bingx_execution_client_tag_helper_returns_pink_tags(self):
from prod.bingx.execution import BingxExecutionClient
client = BingxExecutionClient.__new__(BingxExecutionClient)
client._journal_strategy = "pink"
tags = BingxExecutionClient._pink_observability_tags(client)
self.assertEqual(tags["runtime_namespace"], "pink")
self.assertEqual(tags["strategy_namespace"], "pink")
self.assertEqual(tags["event_namespace"], "pink")
self.assertEqual(tags["actor_name"], "BingxExecutionClient")
self.assertEqual(tags["exec_venue"], "BINGX")
self.assertEqual(tags["data_venue"], "BINGX")
def test_adaptive_exit_engine_tag_helper_returns_pink_tags(self):
from adaptive_exit.adaptive_exit_engine import AdaptiveExitEngine
engine = object.__new__(AdaptiveExitEngine)
engine._strategy_name = "pink"
tags = AdaptiveExitEngine._row_tags(engine)
self.assertEqual(tags["runtime_namespace"], "pink")
self.assertEqual(tags["strategy_namespace"], "pink")
self.assertEqual(tags["event_namespace"], "pink")
self.assertEqual(tags["actor_name"], "AdaptiveExitEngine")
self.assertEqual(tags["exec_venue"], "BINGX")
self.assertEqual(tags["data_venue"], "BINGX")
def test_required_schema_files(self):
schema_dir = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink")
required = [
"00_create_database.sql",
"account_events.sql",
"trade_events.sql",
"status_snapshots.sql",
"v7_decision_events.sql",
"adaptive_exit_shadow.sql",
"02_create_trade_reconstruction.sql",
"03_create_trade_exit_legs.sql",
]
for filename in required:
self.assertTrue(
(schema_dir / filename).exists(),
f"Missing PINK schema file: {filename}",
)
def test_schema_targets_dolphin_pink(self):
schema_dir = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink")
for sql_file in schema_dir.glob("*.sql"):
content = sql_file.read_text()
self.assertIn(
"dolphin_pink", content,
f"{sql_file.name} must reference dolphin_pink database",
)
self.assertNotIn(
"dolphin_prodgreen", content,
f"{sql_file.name} must not reference dolphin_prodgreen",
)
self.assertNotIn(
"dolphin_green", content,
f"{sql_file.name} must not reference dolphin_green",
)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,527 @@
"""Exhaustive sync↔async seam tests for PINK-on-DITAv2.
Tests every boundary where sync code meets async code:
1. BingxVenueAdapter._run() — 3 execution modes (no-loop, in-loop, already-ran)
2. BingxVenueAdapter.connect() -> async backend
3. kernel.process_intent() (sync) -> venue.submit() (sync) -> _run() -> async
4. PinkDirectRuntime.step() (async) -> kernel.process_intent() (sync)
5. launcher._maybe_close() inside/outside event loop
6. _backend_snapshot() HTTP timeout cascade
7. Thread safety: concurrent _run() calls, _last_snapshot races
"""
from __future__ import annotations
import asyncio
import concurrent.futures
import inspect
import threading
import time
import unittest
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from typing import Any, List, Optional
from unittest import mock
# ---------------------------------------------------------------------------
# Seam 1: _run() execution modes
# ---------------------------------------------------------------------------
# We test the real _run() method directly by importing the module
from prod.clean_arch.dita_v2.bingx_venue import BingxVenueAdapter
def _make_adapter() -> BingxVenueAdapter:
"""Build a real BingxVenueAdapter for seam testing."""
from prod.bingx.config import BingxExecClientConfig
from prod.bingx.enums import BingxEnvironment
from prod.clean_arch.adapters.bingx_direct import BingxDirectExecutionAdapter
config = BingxExecClientConfig(
api_key="test", secret_key="test",
environment=BingxEnvironment.VST,
allow_mainnet=False,
recv_window_ms=5000,
default_leverage=1,
exchange_leverage_cap=3,
prefer_websocket=False,
sizing_mode="testnet",
journal_strategy="pink",
journal_db="dolphin_pink",
)
backend = BingxDirectExecutionAdapter(config)
return BingxVenueAdapter(backend=backend)
# Temporary adapter class so we can test _run() without making HTTP calls
class _DummyBackend:
"""Sync + async method surface for seam testing."""
def __init__(self):
self._call_count = 0
# Sync method
def sync_method(self, x: int = 1) -> int:
self._call_count += 1
return x * 2
# Async method
async def async_method(self, x: int = 1) -> int:
self._call_count += 1
await asyncio.sleep(0.001)
return x * 2
# Slow async method for timeout testing
async def slow_async_method(self, delay: float = 10.0) -> str:
self._call_count += 1
await asyncio.sleep(delay)
return "done"
# Coroutine that raises
async def failing_async_method(self) -> None:
self._call_count += 1
await asyncio.sleep(0.001)
raise ValueError("async failure")
# Method that IS a coroutine (not a function returning a coroutine)
async def coro_method(self) -> str:
return "coro"
class TestRunExecutionModes(unittest.TestCase):
"""Test all 3 _run() execution modes exhaustively."""
def setUp(self):
self.adapter = _make_adapter()
self.backend = _DummyBackend()
# --- Mode 1: Non-awaitable (sync method, pass through) ---
def test_sync_method_passthrough(self):
result = self.adapter._run(self.backend.sync_method(5))
self.assertEqual(result, 10)
self.assertEqual(self.backend._call_count, 1)
def test_sync_returns_none_passthrough(self):
result = self.adapter._run(None)
self.assertIsNone(result)
def test_sync_returns_false_passthrough(self):
result = self.adapter._run(False)
self.assertFalse(result)
def test_sync_returns_empty_list_passthrough(self):
result = self.adapter._run([])
self.assertEqual(result, [])
# --- Mode 2: Awaitable, no running loop (asyncio.run) ---
def test_async_method_no_loop(self):
result = self.adapter._run(self.backend.async_method(7))
self.assertEqual(result, 14)
self.assertEqual(self.backend._call_count, 1)
def test_async_method_no_loop_negative(self):
result = self.adapter._run(self.backend.async_method(-3))
self.assertEqual(result, -6)
def test_async_method_no_loop_zero(self):
result = self.adapter._run(self.backend.async_method(0))
self.assertEqual(result, 0)
def test_async_method_no_loop_large_input(self):
result = self.adapter._run(self.backend.async_method(1_000_000))
self.assertEqual(result, 2_000_000)
# --- Mode 3: Awaitable, inside running loop (ThreadPoolExecutor) ---
def test_async_method_inside_loop(self):
"""Call _run() from inside a running asyncio event loop."""
async def run_inside_loop():
return self.adapter._run(self.backend.async_method(11))
result = asyncio.run(run_inside_loop())
self.assertEqual(result, 22)
def test_async_method_inside_loop_multiple_calls(self):
async def run_inside_loop():
a = self.adapter._run(self.backend.async_method(1))
b = self.adapter._run(self.backend.async_method(2))
c = self.adapter._run(self.backend.async_method(3))
return a, b, c
a, b, c = asyncio.run(run_inside_loop())
self.assertEqual((a, b, c), (2, 4, 6))
def test_async_inside_sync_inside_async_nested(self):
"""Russian-doll nesting: sync -> async -> sync -> async."""
async def outer():
# Simulate what PinkDirectRuntime.step() does:
# step() is async, calls kernel.process_intent() which is sync,
# which calls venue.submit() which calls _run() on async backend
def middle_sync():
return self.adapter._run(self.backend.async_method(3))
return middle_sync()
result = asyncio.run(outer())
self.assertEqual(result, 6)
# --- Error propagation ---
def test_async_exception_no_loop_propagates(self):
with self.assertRaises(ValueError):
self.adapter._run(self.backend.failing_async_method())
def test_async_exception_inside_loop_propagates(self):
async def run_inside_loop():
return self.adapter._run(self.backend.failing_async_method())
with self.assertRaises(ValueError):
asyncio.run(run_inside_loop())
# --- Coroutine object handling ---
def test_coroutine_object_passed(self):
"""Passing a coroutine object (not called yet) is handled."""
coro = self.backend.async_method(5)
self.assertTrue(inspect.iscoroutine(coro))
result = self.adapter._run(coro)
self.assertEqual(result, 10)
def test_coroutine_function_rejected(self):
"""Passing a coroutine function (not called) is handled gracefully."""
result = self.adapter._run(42) # not a coroutine at all
self.assertEqual(result, 42)
# --- Thread pool stress ---
def test_concurrent_async_calls_from_multiple_threads(self):
"""Multiple threads calling _run() simultaneously via shared executor."""
errors = []
results = []
lock = threading.Lock()
def worker(x: int):
try:
result = self.adapter._run(self.backend.async_method(x))
with lock:
results.append(result)
except Exception as e:
with lock:
errors.append(e)
threads = []
for i in range(1, 11):
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
self.assertEqual(len(errors), 0, f"Errors in concurrent calls: {errors}")
self.assertEqual(len(results), 10)
self.assertEqual(sorted(results), [2, 4, 6, 8, 10, 12, 14, 16, 18, 20])
def test_concurrent_and_sequential_mixed(self):
"""Mix of concurrent and sequential _run() calls."""
async def in_loop():
results = []
for i in range(5):
r = self.adapter._run(self.backend.async_method(i))
results.append(r)
return results
# Sequential first
seq_results = self.adapter._run(self.backend.async_method(100))
self.assertEqual(seq_results, 200)
# Then from inside loop
loop_results = asyncio.run(in_loop())
self.assertEqual(loop_results, [0, 2, 4, 6, 8])
# ---------------------------------------------------------------------------
# Seam 2: connect() -> async backend
# ---------------------------------------------------------------------------
class TestConnectSeam(unittest.TestCase):
"""Test the VenueAdapter.connect() sync->async bridge."""
def setUp(self):
self.adapter = _make_adapter()
def test_connect_no_backend_method(self):
"""Connect with no backend.connect method — should just snapshot."""
backend = mock.Mock()
backend.connect = None
adapter = BingxVenueAdapter(backend=backend)
# Should not crash — connect() checks for None
result = adapter.connect()
self.assertTrue(result)
def test_connect_sync_backend_method(self):
"""Backend has sync connect."""
backend = mock.Mock()
backend.connect = mock.Mock(return_value=True)
adapter = BingxVenueAdapter(backend=backend)
# The adapter will call backend.connect() and then _backend_snapshot
# which calls backend.refresh_state - may not exist on mock
backend.refresh_state = mock.Mock(return_value=mock.Mock(
capital=25000.0, equity=25000.0, open_positions={},
open_orders=[], all_orders=[], all_fills=[],
account={}, open_notional=0.0, source="mock", recovered=False,
timestamp=datetime.now(timezone.utc),
))
result = adapter.connect()
self.assertTrue(result)
def test_connect_no_connection_leak_on_failure(self):
"""If backend connect fails, adapter should not leak."""
with mock.patch.object(self.adapter, '_backend_snapshot',
side_effect=RuntimeError("boom")):
with self.assertRaises(RuntimeError):
self.adapter.connect()
# ---------------------------------------------------------------------------
# Seam 3: _backend_snapshot thread safety
# ---------------------------------------------------------------------------
class TestBackendSnapshotThreadSafety(unittest.TestCase):
"""Test _last_snapshot is not corrupted by concurrent access."""
def setUp(self):
self.adapter = _make_adapter()
def test_concurrent_backend_snapshot_calls(self):
"""Multiple threads calling _backend_snapshot simultaneously."""
backend = mock.Mock()
snapshots = []
for i in range(10):
snapshots.append(mock.Mock(
capital=float(25000 + i), equity=float(25000 + i),
open_positions={}, open_orders=[], all_orders=[], all_fills=[],
account={}, open_notional=0.0, source="mock", recovered=False,
timestamp=datetime.now(timezone.utc),
))
backend.refresh_state = mock.Mock(side_effect=snapshots)
adapter = BingxVenueAdapter(backend=backend)
def snapshot_worker():
try:
s = adapter._backend_snapshot()
return s
except Exception:
return None
with ThreadPoolExecutor(max_workers=10) as pool:
futures = [pool.submit(snapshot_worker) for _ in range(10)]
results = [f.result() for f in futures]
self.assertEqual(len(results), 10)
# _last_snapshot should be set to the last one
self.assertIsNotNone(adapter._last_snapshot)
def test_concurrent_open_orders_and_positions(self):
"""open_orders() and open_positions() called concurrently."""
backend = mock.Mock()
backend.refresh_state = mock.Mock(return_value=mock.Mock(
capital=25000.0, equity=25000.0,
open_positions={"BTCUSDT": {"symbol": "BTCUSDT", "positionAmt": "0.01"}},
open_orders=[{"orderId": "1"}], all_orders=[], all_fills=[],
account={}, open_notional=100.0, source="mock", recovered=False,
timestamp=datetime.now(timezone.utc),
))
adapter = BingxVenueAdapter(backend=backend)
def orders_worker():
return adapter.open_orders()
def positions_worker():
return adapter.open_positions()
with ThreadPoolExecutor(max_workers=4) as pool:
f1 = pool.submit(orders_worker)
f2 = pool.submit(positions_worker)
f3 = pool.submit(orders_worker)
f4 = pool.submit(positions_worker)
results = [f1.result(), f2.result(), f3.result(), f4.result()]
self.assertEqual(len(results[0]), 1) # 1 open order
self.assertEqual(len(results[1]), 1) # 1 open position
# ---------------------------------------------------------------------------
# Seam 4: _call_backend edge cases
# ---------------------------------------------------------------------------
class TestCallBackend(unittest.TestCase):
"""Test the _call_backend sync->async bridge."""
def setUp(self):
self.adapter = _make_adapter()
def test_call_backend_missing_method_raises(self):
backend = object() # real object, not Mock — Mock returns mock for any attr
adapter = BingxVenueAdapter(backend=backend)
with self.assertRaises(AttributeError):
adapter._call_backend("nonexistent_method")
def test_call_backend_with_args(self):
"""Args and kwargs are forwarded correctly through async boundary."""
backend = mock.Mock()
backend.test_method = mock.Mock(return_value=42)
adapter = BingxVenueAdapter(backend=backend)
result = adapter._call_backend("test_method", 1, 2, kwarg="v")
backend.test_method.assert_called_once_with(1, 2, kwarg="v")
self.assertEqual(result, 42)
# ---------------------------------------------------------------------------
# Seam 5: _maybe_close inside/outside event loop
# ---------------------------------------------------------------------------
class TestMaybeCloseSeam(unittest.TestCase):
"""Test launcher._maybe_close() in various contexts."""
def test_maybe_close_sync_method(self):
from prod.clean_arch.dita_v2.launcher import _maybe_close
obj = mock.Mock()
obj.close = mock.Mock(return_value=True)
_maybe_close(obj)
obj.close.assert_called_once()
def test_maybe_close_async_method_no_loop(self):
from prod.clean_arch.dita_v2.launcher import _maybe_close
async def async_close():
return "closed"
obj = mock.Mock()
obj.close = mock.Mock(return_value=async_close())
_maybe_close(obj)
obj.close.assert_called_once()
def test_maybe_close_async_method_inside_loop(self):
"""Must not crash if called from inside a running event loop."""
from prod.clean_arch.dita_v2.launcher import _maybe_close
async def test():
async def async_close():
return "closed"
obj = mock.Mock()
obj.close = mock.Mock(return_value=async_close())
# _maybe_close must handle RuntimeError from asyncio.run()
# and swallow it gracefully
_maybe_close(obj)
return True
result = asyncio.run(test())
self.assertTrue(result)
def test_maybe_close_disconnect_fallback(self):
from prod.clean_arch.dita_v2.launcher import _maybe_close
obj = mock.Mock()
obj.close = None
obj.disconnect = mock.Mock(return_value=True)
_maybe_close(obj)
obj.disconnect.assert_called_once()
def test_maybe_close_no_methods(self):
from prod.clean_arch.dita_v2.launcher import _maybe_close
obj = object()
_maybe_close(obj) # Should not crash
# ---------------------------------------------------------------------------
# Seam 6: Full lifecycle race conditions
# ---------------------------------------------------------------------------
class TestFullLifecycleRaceConditions(unittest.TestCase):
"""Race conditions between kernel, venue, and runtime."""
def test_concurrent_submit_and_reconcile(self):
"""submit() and reconcile() called simultaneously from different threads."""
backend = mock.Mock()
backend.submit_intent = mock.Mock(return_value=mock.Mock(
status="FILLED", quantity=1.0, price=100.0,
client_order_id="test", order_id="1",
raw_ack={"status": "FILLED"}, raw_state={},
timestamp=datetime.now(timezone.utc),
))
base_snapshot = mock.Mock(
capital=25000.0, equity=25000.0,
open_positions={}, open_orders=[], all_orders=[], all_fills=[],
account={}, open_notional=0.0, source="mock", recovered=False,
timestamp=datetime.now(timezone.utc),
)
backend.refresh_state = mock.Mock(return_value=base_snapshot)
adapter = BingxVenueAdapter(backend=backend)
from prod.clean_arch.dita_v2.contracts import KernelCommandType, KernelIntent, TradeSide
intent = KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="race-test", trade_id="race-trade",
slot_id=0, asset="BTCUSDT", side=TradeSide.SHORT,
action=KernelCommandType.ENTER,
reference_price=100.0, target_size=1.0, leverage=1.0,
)
def submit_worker():
return adapter.submit(intent)
def reconcile_worker():
return adapter.reconcile()
with ThreadPoolExecutor(max_workers=4) as pool:
f_submit = pool.submit(submit_worker)
f_reconcile = pool.submit(reconcile_worker)
f_submit2 = pool.submit(submit_worker)
f_reconcile2 = pool.submit(reconcile_worker)
results = [f.result() for f in [f_submit, f_reconcile, f_submit2, f_reconcile2]]
self.assertEqual(len(results), 4)
self.assertIsNotNone(adapter._last_snapshot)
# ---------------------------------------------------------------------------
# Seam 7: Nested event-loop detection and prevention
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Seam 8: Timeout and hang detection
# ---------------------------------------------------------------------------
class TestTimeoutAndHangDetection(unittest.TestCase):
"""Test that slow async methods trigger timeouts properly."""
def test_slow_async_no_timeout_no_loop(self):
"""Slow async without loop just runs — no timeout mechanism in _run()."""
backend = _DummyBackend()
adapter = _make_adapter()
# This would hang for 10 seconds if we actually ran it
# Instead we verify that _run() would pass it through correctly
coro = backend.slow_async_method(delay=0.001) # fast
result = adapter._run(coro)
self.assertEqual(result, "done")
def test_slow_async_with_timeout_inside_loop_future(self):
"""ThreadPoolExecutor submit().result() can be given a timeout."""
backend = _DummyBackend()
async def test():
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
future = pool.submit(asyncio.run, backend.slow_async_method(delay=10.0))
with self.assertRaises(concurrent.futures.TimeoutError):
future.result(timeout=0.5)
return True
result = asyncio.run(test())
self.assertTrue(result)
def test_http_timeout_propagation(self):
"""Verify BingX HTTP client timeout propagates through async boundary."""
# The httpx.AsyncClient has a 10s timeout by default
# This test verifies the timeout config is respected
from prod.bingx.http import BingxHttpClient
from prod.bingx.config import BingxExecClientConfig
from prod.bingx.enums import BingxEnvironment
config = BingxExecClientConfig(
api_key="test", secret_key="test",
environment=BingxEnvironment.VST,
http_timeout_secs=5,
)
client = BingxHttpClient(config)
self.assertEqual(client._timeout_secs, 5)
if __name__ == "__main__":
unittest.main()