"""VIOLET V2: seeded synthetic intent scripts + the cycle executor. Each cycle is one full trade lifecycle (ENTER → terminal → flatten) driven through the REAL kernel + ScriptedVenue + ExecDeadlineDriver, with the scenario controlling venue behavior and router policy. Scripts are seeded and hashed (same discipline as the V0 storm): same seed ⇒ same scenario sequence, prices, trade ids — and the per-cycle outcome tuples are part of the determinism check in the V2 gate. """ from __future__ import annotations import asyncio import hashlib import json import random from dataclasses import dataclass, field from datetime import datetime, timezone from enum import Enum from typing import Any, Dict, List, Optional from prod.clean_arch.dita_v2.contracts import ( KernelCommandType, KernelEventKind, KernelIntent, TradeSide, TradeStage, ) from prod.clean_arch.dita_v2.exec_router import ExecConfig from .clock import mono_ns from .exec_driver import SLOT_OPENISH from .scripted_venue import Directive EXIT_URGENT_REASON = "CATASTROPHIC" # always MARKET per router policy class Scenario(str, Enum): IMMEDIATE_FILL = "immediate_fill" REST_THEN_FILL = "rest_then_fill" FILL_RACES_CANCEL = "fill_races_cancel" REST_EXPIRE_RETRY = "rest_expire_retry" RETRY_EXHAUST_SKIP = "retry_exhaust_skip" RETRY_EXHAUST_MARKET = "retry_exhaust_market" EXIT_EXPIRE_MARKET = "exit_expire_market" POST_ONLY_REJECT = "post_only_reject" CANCEL_REJECT = "cancel_reject" def router_config_for(scenario: Scenario) -> ExecConfig: """Constructed DIRECTLY (no env mutation) per the V2 plan.""" if scenario == Scenario.REST_EXPIRE_RETRY: return ExecConfig(style="maker_both", entry_miss="retry", entry_retries=1, retry_exhaust="skip") if scenario == Scenario.RETRY_EXHAUST_SKIP: return ExecConfig(style="maker_both", entry_miss="retry", entry_retries=1, retry_exhaust="skip") if scenario == Scenario.RETRY_EXHAUST_MARKET: return ExecConfig(style="maker_both", entry_miss="retry", entry_retries=1, retry_exhaust="market") return ExecConfig(style="maker_both", entry_miss="skip") @dataclass class IntentScriptSpec: n_cycles: int = 18 # 2 × the 9-scenario matrix seed: int = 7 asset: str = "STORMUSDT" base_price: float = 100.0 walk_bps: float = 8.0 scenarios: Optional[List[Scenario]] = None # None → round-robin all def to_dict(self) -> Dict[str, Any]: return { "n_cycles": self.n_cycles, "seed": self.seed, "asset": self.asset, "base_price": self.base_price, "walk_bps": self.walk_bps, "scenarios": [s.value for s in (self.scenarios or list(Scenario))], } @dataclass(frozen=True) class CycleSpec: idx: int scenario: Scenario trade_id: str price: float @dataclass class CycleOutcome: idx: int scenario: str ok: bool entry_path: str # base | r1 | m | none exit_path: str # maker | market_fallback | urgent_market | none detail: str = "" resolution_ms: float = 0.0 def key(self) -> Dict[str, Any]: """Determinism-relevant projection (latency excluded).""" return {"idx": self.idx, "scenario": self.scenario, "ok": self.ok, "entry_path": self.entry_path, "exit_path": self.exit_path} def generate_script(spec: IntentScriptSpec) -> List[CycleSpec]: rng = random.Random(spec.seed) order = spec.scenarios or list(Scenario) px = spec.base_price out: List[CycleSpec] = [] for i in range(spec.n_cycles): px = max(0.01, px * (1.0 + rng.uniform(-1, 1) * spec.walk_bps / 1e4)) out.append(CycleSpec(idx=i, scenario=order[i % len(order)], trade_id=f"vsyn-{spec.seed}-{i:05d}", price=round(px, 6))) return out def script_hash(cycles: List[CycleSpec]) -> str: payload = json.dumps( [{"idx": c.idx, "scenario": c.scenario.value, "trade_id": c.trade_id, "price": c.price} for c in cycles], sort_keys=True, separators=(",", ":")).encode() return hashlib.sha256(payload).hexdigest() def outcomes_hash(outcomes: List[CycleOutcome]) -> str: payload = json.dumps([o.key() for o in outcomes], sort_keys=True, separators=(",", ":")).encode() return hashlib.sha256(payload).hexdigest() # ── intent builders (mirror pink_direct's plan→intent mapping) ──────────────── def build_enter_intent(cycle: CycleSpec, plan: Any, asset: str) -> KernelIntent: return KernelIntent( timestamp=datetime.now(timezone.utc), intent_id=cycle.trade_id, trade_id=cycle.trade_id, slot_id=0, asset=asset, side=TradeSide.SHORT, action=KernelCommandType.ENTER, reference_price=cycle.price, target_size=1.0, leverage=1.0, exit_leg_ratios=(1.0,), reason=f"vsyn:{cycle.scenario.value}", metadata={"_time_in_force": ("PostOnly" if plan.post_only else "GTC"), "_exec_reason": plan.reason}, stage=TradeStage.INTENT_CREATED, order_type=plan.order_type, limit_price=float(plan.limit_price or 0.0), ) def build_exit_intent(trade_id: str, plan: Any, asset: str, *, size: float, price: float) -> KernelIntent: return KernelIntent( timestamp=datetime.now(timezone.utc), intent_id=f"{trade_id}-x", trade_id=trade_id, slot_id=0, asset=asset, side=TradeSide.SHORT, action=KernelCommandType.EXIT, reference_price=price, target_size=float(size), leverage=1.0, exit_leg_ratios=(1.0,), reason=plan.reason, metadata={"_time_in_force": ("PostOnly" if plan.post_only else "GTC"), "_exec_reason": plan.reason}, stage=TradeStage.INTENT_CREATED, order_type=plan.order_type, limit_price=float(plan.limit_price or 0.0), ) # ── cycle executor ──────────────────────────────────────────────────────────── class SyntheticIntentDriver: """Runs scripted cycles against (kernel, venue, exec driver, router). The owner (ExecStormHarness) provides ``pump`` — which MUST forward working-order fills to ``driver.on_fill`` — plus slot_view (same shape as the exec driver's port). """ def __init__(self, *, kernel: Any, venue: Any, driver: Any, pump, slot_view, ttl_ms: float, logger: Any = None) -> None: self.kernel = kernel self.venue = venue self.driver = driver self.pump = pump self.slot_view = slot_view self.ttl_ms = float(ttl_ms) self.logger = logger # — helpers — def _slot_flat(self) -> bool: _tid, stage, size = self.slot_view() return size <= 0.0 and stage not in SLOT_OPENISH and stage not in ( "ORDER_SENT", "ORDER_ACKED", "ENTRY_WORKING", "ORDER_REQUESTED", "EXIT_REQUESTED") async def _await_terminal(self, pred, timeout_s: float) -> bool: deadline = mono_ns() + int(timeout_s * 1e9) while mono_ns() < deadline: await self.pump() if pred(): return True await asyncio.sleep(0.005) return False def _arm_venue(self, cycle: CycleSpec) -> None: tid, s = cycle.trade_id, cycle.scenario ttl = self.ttl_ms if s == Scenario.IMMEDIATE_FILL: self.venue.set_directive(tid, Directive.IMMEDIATE_FILL) elif s == Scenario.REST_THEN_FILL: self.venue.set_directive(tid, Directive.REST_THEN_FILL, fill_delay_ms=ttl * 0.3) elif s == Scenario.FILL_RACES_CANCEL: self.venue.set_directive(tid, Directive.FILL_RACES_CANCEL) elif s == Scenario.REST_EXPIRE_RETRY: self.venue.set_directive(tid, Directive.REST_THEN_EXPIRE) self.venue.set_directive(f"{tid}-r1", Directive.IMMEDIATE_FILL) elif s in (Scenario.RETRY_EXHAUST_SKIP,): self.venue.set_directive(tid, Directive.REST_THEN_EXPIRE) elif s == Scenario.RETRY_EXHAUST_MARKET: self.venue.set_directive(tid, Directive.REST_THEN_EXPIRE) # -m fallback is MARKET → fills via venue realism, no directive. elif s == Scenario.EXIT_EXPIRE_MARKET: self.venue.set_directive(tid, Directive.IMMEDIATE_FILL) # re-armed to REST_THEN_EXPIRE after the entry fills (same tid). elif s == Scenario.POST_ONLY_REJECT: self.venue.set_directive(tid, Directive.POST_ONLY_REJECT) elif s == Scenario.CANCEL_REJECT: self.venue.set_directive(tid, Directive.CANCEL_REJECT) async def _flatten(self, cycle: CycleSpec, outcome: CycleOutcome) -> bool: """End-of-cycle: close any open position (urgent MARKET) and clear any stuck working slot (direct CANCEL — operator-style cleanup for the CANCEL_REJECT scenario).""" slot_tid, stage, size = self.slot_view() if size > 0.0 and slot_tid: router = self.driver.ports.router plan = router.plan_exit(trade_id=slot_tid, asset="STORMUSDT", position_side="SHORT", reference_price=cycle.price, reason=EXIT_URGENT_REASON) intent = build_exit_intent(slot_tid, plan, "STORMUSDT", size=size, price=cycle.price) await self.kernel.process_intent_async(intent) if outcome.exit_path == "none": outcome.exit_path = "urgent_market" ok = await self._await_terminal(self._slot_flat, 2.0) if not ok: return False elif not self._slot_flat() and slot_tid: # stuck working entry (cancel-reject path): direct CANCEL, the # venue accepts the second attempt (directive consumed realism # is NOT modeled — clear it explicitly instead). self.venue._scripts.pop(slot_tid, None) cancel = KernelIntent( timestamp=datetime.now(timezone.utc), intent_id=f"{slot_tid}-opcxl", trade_id=slot_tid, slot_id=0, asset="STORMUSDT", side=TradeSide.SHORT, action=KernelCommandType.CANCEL, reference_price=cycle.price, target_size=1.0, leverage=1.0, exit_leg_ratios=(1.0,), reason="vsyn:cleanup", metadata={}, stage=TradeStage.INTENT_CREATED, ) await self.kernel.process_intent_async(cancel) if not await self._await_terminal(self._slot_flat, 2.0): return False return True # — the cycle — async def run_cycle(self, cycle: CycleSpec) -> CycleOutcome: out = CycleOutcome(idx=cycle.idx, scenario=cycle.scenario.value, ok=False, entry_path="none", exit_path="none") router = self.driver.ports.router # Per-scenario policy swap — safe at cycle boundaries because the # working registry is empty between cycles (asserted at cycle end). router.config = router_config_for(cycle.scenario) t0 = mono_ns() self._arm_venue(cycle) plan = router.plan_entry(trade_id=cycle.trade_id, asset="STORMUSDT", position_side="SHORT", reference_price=cycle.price) if plan.suppress: out.detail = f"entry suppressed at quiescence: {plan.reason}" return out intent = build_enter_intent(cycle, plan, "STORMUSDT") await self.kernel.process_intent_async(intent) if plan.is_maker: self.driver.on_submit(plan, intent) budget_s = self.ttl_ms / 1000.0 + 1.0 s = cycle.scenario def _slot_open_with(tid: str) -> bool: slot_tid, stage, size = self.slot_view() return slot_tid == tid and size > 0.0 and stage in SLOT_OPENISH if s in (Scenario.IMMEDIATE_FILL, Scenario.REST_THEN_FILL, Scenario.FILL_RACES_CANCEL): if not await self._await_terminal( lambda: _slot_open_with(cycle.trade_id), budget_s): out.detail = "entry never opened" return out out.entry_path = "base" elif s == Scenario.REST_EXPIRE_RETRY: if not await self._await_terminal( lambda: _slot_open_with(f"{cycle.trade_id}-r1"), budget_s + self.ttl_ms / 1000.0): out.detail = "retry never opened" return out out.entry_path = "r1" elif s == Scenario.RETRY_EXHAUST_MARKET: if not await self._await_terminal( lambda: _slot_open_with(f"{cycle.trade_id}-m"), budget_s + 2 * self.ttl_ms / 1000.0): out.detail = "market fallback never opened" return out out.entry_path = "m" elif s in (Scenario.RETRY_EXHAUST_SKIP, Scenario.POST_ONLY_REJECT, Scenario.CANCEL_REJECT): # Terminal = no position, working registry cleared. def _resolved() -> bool: _tid, _stage, size = self.slot_view() return (size <= 0.0 and not router.working_orders() and not self.driver._resolving) if not await self._await_terminal( _resolved, budget_s + 2 * self.ttl_ms / 1000.0): out.detail = "miss path never resolved" return out out.entry_path = "none" # EXIT phase for every cycle that holds a position. slot_tid, _stage, size = self.slot_view() if size > 0.0: if s == Scenario.EXIT_EXPIRE_MARKET: out.entry_path = "base" # immediate-fill entry above self.venue.set_directive(slot_tid, Directive.REST_THEN_EXPIRE) xplan = router.plan_exit(trade_id=slot_tid, asset="STORMUSDT", position_side="SHORT", reference_price=cycle.price, reason="TAKE_PROFIT") if not xplan.is_maker: out.detail = f"expected maker exit, got {xplan.reason}" return out xintent = build_exit_intent(slot_tid, xplan, "STORMUSDT", size=size, price=cycle.price) await self.kernel.process_intent_async(xintent) self.driver.on_submit(xplan, xintent) if not await self._await_terminal(self._slot_flat, budget_s + 1.0): out.detail = "exit market fallback never closed" return out out.exit_path = "market_fallback" # all other scenarios flatten below if not await self._flatten(cycle, out): out.detail = "flatten failed" return out # Cycle-end invariants — the matrix's hard correctness core. if router.working_orders(): out.detail = f"stuck working orders: {router.working_orders()}" return out if not await self.driver.drain(1.0): out.detail = f"driver did not drain: {self.driver.snapshot()}" return out out.resolution_ms = (mono_ns() - t0) / 1e6 out.ok = True return out async def run(self, spec: IntentScriptSpec) -> List[CycleOutcome]: outcomes = [] for cycle in generate_script(spec): outcomes.append(await self.run_cycle(cycle)) return outcomes