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>
580 lines
31 KiB
Python
580 lines
31 KiB
Python
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
|