613 lines
25 KiB
Python
613 lines
25 KiB
Python
|
|
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)
|