synthetic_intents.py: seeded IntentScriptSpec -> CycleSpec scripts (script_hash + outcomes_hash determinism, V0 discipline); per-scenario router ExecConfig constructed directly (no env mutation); cycle executor runs full ENTER->terminal->flatten lifecycles with per-scenario terminal predicates and cycle-end invariants (working registry empty, driver drained, slot flat). exec_harness.py: composition root — production bundle (MOCK, injected ScriptedVenue), ExecDeadlineDriver ports wired, pump = venue.reconcile() -> kernel + driver.on_fill forwarding (the production seam), gate report via the ExecGateReport schema, archive next to V0 reports. scripted_venue.py amendment: MARKET orders never rest (venue realism — directives are keyed by trade_id and the R1 MARKET fallback shares the position's trade_id). Matrix green through the REAL kernel at 100ms TTL: immediate fill, rest-then-fill (deadline cancelled), fill-races-cancel (no retry), rest-expire-retry (-r1 opens), retry-exhaust skip|market, exit-expire -> MARKET same trade_id, post-only reject, cancel-reject (no strand). Two runs same seed -> identical outcomes_hash. Router 77 green; shared clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
375 lines
16 KiB
Python
375 lines
16 KiB
Python
"""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
|