"""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) # Venue realism: MARKET orders never rest — directives (keyed by # trade_id) only govern LIMIT quotes, so a MARKET fallback that # shares the trade_id of a resting maker quote always fills. if (script is None or script.directive == Directive.IMMEDIATE_FILL or str(intent.order_type) == "MARKET"): 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