165 lines
7.3 KiB
Python
165 lines
7.3 KiB
Python
|
|
"""VIOLET V2: ScriptedVenue — per-order directives over the MOCK venue.
|
||
|
|
|
||
|
|
Subclasses ``MockVenueAdapter`` (zero edits to the shared module) to give
|
||
|
|
tests deterministic, per-trade control over whether a maker quote fills
|
||
|
|
immediately, rests then fills, rests until TTL expiry, rejects post-only,
|
||
|
|
rejects the cancel, or fills in the race window between TTL fire and the
|
||
|
|
CANCEL round-trip (the late-WS-fill case production sees).
|
||
|
|
|
||
|
|
Deferred fills are released through ``reconcile()`` — the exact seam the
|
||
|
|
production runtime drains via ``pump_venue_events`` — NEVER through the
|
||
|
|
parent's ``subscribe()`` 50 ms poll, which would put an artificial floor
|
||
|
|
under the V2 latency gate.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from enum import Enum
|
||
|
|
from typing import Dict, List, Optional, Tuple
|
||
|
|
|
||
|
|
from prod.clean_arch.dita_v2.contracts import (
|
||
|
|
KernelEventKind,
|
||
|
|
KernelIntent,
|
||
|
|
VenueEvent,
|
||
|
|
VenueEventStatus,
|
||
|
|
VenueOrder,
|
||
|
|
VenueOrderStatus,
|
||
|
|
)
|
||
|
|
from prod.clean_arch.dita_v2.mock_venue import MockVenueAdapter, MockVenueScenario
|
||
|
|
|
||
|
|
from .clock import mono_ns
|
||
|
|
from .domain import typed
|
||
|
|
|
||
|
|
|
||
|
|
class Directive(str, Enum):
|
||
|
|
IMMEDIATE_FILL = "immediate_fill"
|
||
|
|
REST_THEN_FILL = "rest_then_fill"
|
||
|
|
REST_THEN_EXPIRE = "rest_then_expire"
|
||
|
|
POST_ONLY_REJECT = "post_only_reject"
|
||
|
|
CANCEL_REJECT = "cancel_reject" # rests; cancel is rejected
|
||
|
|
FILL_RACES_CANCEL = "fill_races_cancel" # rests; fill lands during cancel
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class _Script:
|
||
|
|
directive: Directive
|
||
|
|
fill_delay_ms: float = 0.0
|
||
|
|
|
||
|
|
|
||
|
|
class ScriptedVenue(MockVenueAdapter):
|
||
|
|
"""MockVenueAdapter + per-trade_id directives.
|
||
|
|
|
||
|
|
Directive lookup is longest-prefix: a directive set for ``T1`` also
|
||
|
|
governs ``T1-r1``/``T1-m`` unless those register their own — retry
|
||
|
|
chains can change behavior mid-scenario.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, scenario: Optional[MockVenueScenario] = None):
|
||
|
|
super().__init__(scenario)
|
||
|
|
self._scripts: Dict[str, _Script] = {}
|
||
|
|
self._intents: Dict[str, KernelIntent] = {} # venue_order_id → intent
|
||
|
|
self._pending_fills: List[Tuple[int, str]] = [] # (due_mono_ns, venue_order_id)
|
||
|
|
self.submits: List[str] = []
|
||
|
|
self.cancels: List[str] = []
|
||
|
|
|
||
|
|
# ── scripting API ─────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
@typed
|
||
|
|
def set_directive(self, trade_id_prefix: str, directive: Directive,
|
||
|
|
*, fill_delay_ms: float = 0.0) -> None:
|
||
|
|
self._scripts[trade_id_prefix] = _Script(directive, fill_delay_ms)
|
||
|
|
|
||
|
|
def _script_for(self, trade_id: str) -> Optional[_Script]:
|
||
|
|
if trade_id in self._scripts:
|
||
|
|
return self._scripts[trade_id]
|
||
|
|
best = None
|
||
|
|
for prefix in self._scripts:
|
||
|
|
if trade_id.startswith(prefix):
|
||
|
|
if best is None or len(prefix) > len(best):
|
||
|
|
best = prefix
|
||
|
|
return self._scripts.get(best) if best else None
|
||
|
|
|
||
|
|
# ── venue surface ─────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
def submit(self, intent: KernelIntent) -> List[VenueEvent]:
|
||
|
|
self.submits.append(intent.trade_id)
|
||
|
|
script = self._script_for(intent.trade_id)
|
||
|
|
if script is None or script.directive == Directive.IMMEDIATE_FILL:
|
||
|
|
return super().submit(intent) # parent default: ACK + FULL_FILL
|
||
|
|
|
||
|
|
order_id = f"V-{next(self._order_seq):08d}"
|
||
|
|
order = VenueOrder(
|
||
|
|
internal_trade_id=intent.trade_id,
|
||
|
|
venue_order_id=order_id,
|
||
|
|
venue_client_id=f"{intent.trade_id}:{intent.intent_id}",
|
||
|
|
side=intent.side,
|
||
|
|
intended_size=float(intent.target_size),
|
||
|
|
status=VenueOrderStatus.NEW,
|
||
|
|
metadata={"intent_id": intent.intent_id, "action": intent.action.value,
|
||
|
|
"slot_id": intent.slot_id, "asset": intent.asset},
|
||
|
|
)
|
||
|
|
if script.directive == Directive.POST_ONLY_REJECT:
|
||
|
|
return [self._event_from_order(
|
||
|
|
intent, order, KernelEventKind.ORDER_REJECT,
|
||
|
|
VenueEventStatus.REJECTED, reason="POST_ONLY_WOULD_CROSS")]
|
||
|
|
|
||
|
|
# All REST_* directives: the quote rests — ACK only.
|
||
|
|
self._open_orders[order_id] = order
|
||
|
|
self._intents[order_id] = intent
|
||
|
|
if script.directive == Directive.REST_THEN_FILL:
|
||
|
|
due = mono_ns() + int(script.fill_delay_ms * 1_000_000)
|
||
|
|
self._pending_fills.append((due, order_id))
|
||
|
|
return [self._event_from_order(
|
||
|
|
intent, order, KernelEventKind.ORDER_ACK, VenueEventStatus.ACKED)]
|
||
|
|
|
||
|
|
def cancel(self, order: VenueOrder, *, reason: str = "") -> List[VenueEvent]:
|
||
|
|
self.cancels.append(order.internal_trade_id)
|
||
|
|
script = self._script_for(order.internal_trade_id)
|
||
|
|
if script is not None and script.directive == Directive.FILL_RACES_CANCEL:
|
||
|
|
# The fill beat the cancel on the wire: surface it on the next
|
||
|
|
# reconcile (= production pump), reject this cancel.
|
||
|
|
self._pending_fills.append((0, order.venue_order_id))
|
||
|
|
return [self._event_from_order(
|
||
|
|
self._dummy_intent(order), order, KernelEventKind.CANCEL_REJECT,
|
||
|
|
VenueEventStatus.CANCELED_REJECTED, reason="ORDER_ALREADY_FILLED")]
|
||
|
|
if script is not None and script.directive == Directive.CANCEL_REJECT:
|
||
|
|
return [self._event_from_order(
|
||
|
|
self._dummy_intent(order), order, KernelEventKind.CANCEL_REJECT,
|
||
|
|
VenueEventStatus.CANCELED_REJECTED, reason="MOCK_CANCEL_REJECT")]
|
||
|
|
return super().cancel(order, reason=reason)
|
||
|
|
|
||
|
|
def reconcile(self) -> List[VenueEvent]:
|
||
|
|
"""Release pending fills whose due time has passed — the production
|
||
|
|
pump path. Fill price = limit price for LIMIT orders, else reference."""
|
||
|
|
now = mono_ns()
|
||
|
|
out: List[VenueEvent] = []
|
||
|
|
keep: List[Tuple[int, str]] = []
|
||
|
|
for due, order_id in self._pending_fills:
|
||
|
|
if now < due:
|
||
|
|
keep.append((due, order_id))
|
||
|
|
continue
|
||
|
|
order = self._open_orders.get(order_id)
|
||
|
|
intent = self._intents.get(order_id)
|
||
|
|
if order is None or intent is None:
|
||
|
|
continue # cancelled before the fill landed
|
||
|
|
px = (float(intent.limit_price) if intent.order_type == "LIMIT"
|
||
|
|
and float(intent.limit_price or 0.0) > 0.0
|
||
|
|
else float(intent.reference_price or 0.0))
|
||
|
|
out.append(self._event_from_order(
|
||
|
|
intent, order, KernelEventKind.FULL_FILL, VenueEventStatus.FILLED,
|
||
|
|
price=px, fill_size=float(intent.target_size), remaining_size=0.0))
|
||
|
|
self._open_orders[order_id] = VenueOrder(
|
||
|
|
internal_trade_id=order.internal_trade_id,
|
||
|
|
venue_order_id=order.venue_order_id,
|
||
|
|
venue_client_id=order.venue_client_id,
|
||
|
|
side=order.side,
|
||
|
|
intended_size=order.intended_size,
|
||
|
|
filled_size=float(intent.target_size),
|
||
|
|
average_fill_price=px,
|
||
|
|
status=VenueOrderStatus.FILLED,
|
||
|
|
metadata=dict(order.metadata),
|
||
|
|
)
|
||
|
|
self._pending_fills = keep
|
||
|
|
return out
|