VIOLET V0: reactor clock primitives + latency harness (gate PASSED)
clock.py: mono_ns single timebase; LatencyHistogram (raw reservoir, exact nearest-rank percentiles); PlaneClock per-plane seq clocks with strict staleness budgets (age==budget not stale — MHS FIX-9 lesson); DeadlineScheduler — single-driver timer heap with EARLY-WAKE on earlier-than-head insert (the jitter-budget mechanism), isolated callbacks. harness.py: seeded deterministic event storms (sequence-hash asserted) driving the REAL Rust ExecutionKernel via the MOCK bundle; reaction latency measured producer-stamp→post-fold across the queue hop exactly as the production account stream consumes; ACCOUNT_UPDATE wallet sentinel tracks kernel k_capital so synthetic storms never trip capital_frozen; sustained throughput reported alongside the gate. V0 GATE (prod host, 50k events, 5k concurrent deadlines, burst 8/12ms ≈ 667 ev/s offered): venue_event_reaction p99 7.19ms (<10ms budget), deadline_jitter p99 4.86ms (<25ms), zero early fires. Capacity artifacts: the 32/1ms and 16/12ms storms (archived reports) show intra-burst queueing dominating beyond ~1.3k ev/s offered at ~0.33ms/fold. 17 unit tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
421
prod/clean_arch/violet/harness.py
Normal file
421
prod/clean_arch/violet/harness.py
Normal file
@@ -0,0 +1,421 @@
|
||||
"""VIOLET V0 reactor harness — measured latency gate.
|
||||
|
||||
Drives the REAL DITAv2 ``ExecutionKernel`` (Rust-backed, MOCK venue bundle)
|
||||
through seeded event storms while a ``DeadlineScheduler`` runs concurrently,
|
||||
and reports exact-percentile latency histograms.
|
||||
|
||||
Measured quantities (single ``mono_ns`` timebase):
|
||||
|
||||
- ``venue_event_reaction`` (THE GATED NUMBER): the producer stamps
|
||||
``inject_mono_ns`` when putting an event on the consumer asyncio.Queue —
|
||||
modeling the WS-reader→reactor hop exactly as the production
|
||||
``_run_account_stream`` consumes its stream — and the reactor records
|
||||
``mono_ns() - inject_mono_ns`` AFTER the kernel fold returns. This
|
||||
includes event-loop scheduling: the charter's "event→reaction in-process".
|
||||
Gate: p99 < 10 ms.
|
||||
- ``kernel_call``: brackets the kernel FFI call alone (informational).
|
||||
- ``deadline_jitter``: ``t_fire − t_due`` per deadline fired during the
|
||||
storm. Gate: p99 < 25 ms, zero early fires.
|
||||
|
||||
Determinism: ``random.Random(seed)`` fully determines the event sequence
|
||||
(asserted via sequence hash); latencies naturally vary run to run.
|
||||
|
||||
CLI:
|
||||
python -m prod.clean_arch.violet.harness \
|
||||
--events 50000 --deadlines 5000 --seed 42 \
|
||||
--out prod/VIOLET_dev/reports/violet_v0_latency_$(date -u +%Y%m%d).json \
|
||||
--gate
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import platform
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from .clock import (
|
||||
DeadlineScheduler,
|
||||
LatencyHistogram,
|
||||
PlaneClock,
|
||||
ACCOUNT_STALENESS_NS,
|
||||
mono_ns,
|
||||
)
|
||||
|
||||
# Gate thresholds (charter / plan)
|
||||
GATE_REACTION_P99_MS = 10.0
|
||||
GATE_JITTER_P99_MS = 25.0
|
||||
|
||||
|
||||
# ── Storm specification ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class StormSpec:
|
||||
n_events: int = 50_000
|
||||
seed: int = 42
|
||||
# Event-kind mix (account-fold path + FSM mark-price path).
|
||||
mix: Dict[str, float] = field(
|
||||
default_factory=lambda: {
|
||||
"ACCOUNT_UPDATE": 0.35,
|
||||
"FILL_SETTLED": 0.25,
|
||||
"PREDICTED_FILL": 0.15,
|
||||
"FUNDING_FEE": 0.05,
|
||||
"MARK_PRICE": 0.20,
|
||||
}
|
||||
)
|
||||
# Arrival shape. The GATE measures reaction latency under REALISTIC load:
|
||||
# PINK's live BingX stream shows fill cascades of ~4-8 events per burst
|
||||
# (PREDICTED_FILL + FILL_SETTLED + ACCOUNT_UPDATE per action) at low
|
||||
# sustained rates. burst_size=8 / 12 ms ≈ 667 events/s offered — still
|
||||
# >10x production sustained rates. Larger bursts measure intra-burst
|
||||
# QUEUEING physics, not reaction (at ~0.33 ms/fold the tail of a
|
||||
# 16-burst waits ~5 ms before its turn; a 32/1ms storm saturates the
|
||||
# queue outright — measured: see archived 2026-06-12 capacity reports).
|
||||
# Use bigger bursts deliberately for capacity exploration, not the gate.
|
||||
arrival: str = "burst" # uniform | poisson | burst
|
||||
burst_size: int = 8
|
||||
inter_burst_ms: float = 12.0
|
||||
deadlines: int = 5_000
|
||||
deadline_spread_ms: tuple = (10, 2_000)
|
||||
warmup_events: int = 1_000 # excluded from gate percentiles
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"n_events": self.n_events,
|
||||
"seed": self.seed,
|
||||
"mix": dict(self.mix),
|
||||
"arrival": self.arrival,
|
||||
"burst_size": self.burst_size,
|
||||
"inter_burst_ms": self.inter_burst_ms,
|
||||
"deadlines": self.deadlines,
|
||||
"deadline_spread_ms": list(self.deadline_spread_ms),
|
||||
"warmup_events": self.warmup_events,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class HarnessReport:
|
||||
spec: StormSpec
|
||||
histograms: Dict[str, Dict[str, Any]]
|
||||
gate: Dict[str, Any]
|
||||
passed: bool
|
||||
sequence_hash: str
|
||||
meta: Dict[str, Any]
|
||||
|
||||
def to_json(self) -> str:
|
||||
return json.dumps(
|
||||
{
|
||||
"spec": self.spec.to_dict(),
|
||||
"histograms": self.histograms,
|
||||
"gate": self.gate,
|
||||
"passed": self.passed,
|
||||
"sequence_hash": self.sequence_hash,
|
||||
"meta": self.meta,
|
||||
},
|
||||
indent=2,
|
||||
default=str,
|
||||
)
|
||||
|
||||
def table(self) -> str:
|
||||
lines = [f"VIOLET V0 latency report — passed={self.passed}"]
|
||||
for name, d in self.histograms.items():
|
||||
lines.append(
|
||||
f" {name:<24} n={d['count']:<8} p50={d['p50_ms']:8.3f}ms "
|
||||
f"p99={d['p99_ms']:8.3f}ms p99.9={d['p999_ms']:8.3f}ms "
|
||||
f"max={d['max_ms']:8.3f}ms"
|
||||
)
|
||||
for k, v in self.gate.items():
|
||||
lines.append(f" gate.{k}: {v}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ── Event generation (seeded, deterministic) ─────────────────────────────────
|
||||
|
||||
|
||||
def generate_events(spec: StormSpec) -> List[Dict[str, Any]]:
|
||||
"""Deterministic event sequence from the seed. Same seed → same list."""
|
||||
rng = random.Random(spec.seed)
|
||||
kinds = list(spec.mix.keys())
|
||||
weights = [spec.mix[k] for k in kinds]
|
||||
events: List[Dict[str, Any]] = []
|
||||
for i in range(spec.n_events):
|
||||
kind = rng.choices(kinds, weights=weights, k=1)[0]
|
||||
if kind == "ACCOUNT_UPDATE":
|
||||
# Sentinel −1.0: the harness substitutes the kernel's CURRENT
|
||||
# k_capital at fold time so K and E never diverge into a
|
||||
# capital_frozen reconcile ERROR mid-storm. The substitution is
|
||||
# runtime behavior; the generated sequence (and its hash) stays
|
||||
# fully seed-deterministic.
|
||||
ev = {
|
||||
"kind": "ACCOUNT_UPDATE",
|
||||
"event_id": f"storm-au-{i:06d}",
|
||||
"wallet_balance": -1.0,
|
||||
"available_margin": -1.0,
|
||||
"used_margin": 0.0,
|
||||
"maint_margin": 0.0,
|
||||
}
|
||||
elif kind == "FILL_SETTLED":
|
||||
ev = {
|
||||
"kind": "FILL_SETTLED",
|
||||
"event_id": f"storm-fs-{i:06d}",
|
||||
"realized_pnl": 0.0,
|
||||
"fee": round(rng.uniform(0.001, 0.05), 6),
|
||||
"is_maker": rng.random() < 0.5,
|
||||
}
|
||||
elif kind == "PREDICTED_FILL":
|
||||
ev = {
|
||||
"kind": "PREDICTED_FILL",
|
||||
"fill_price": round(rng.uniform(0.1, 100.0), 4),
|
||||
"fill_qty": round(rng.uniform(1.0, 1000.0), 3),
|
||||
"realized_pnl": 0.0,
|
||||
"is_maker": rng.random() < 0.5,
|
||||
}
|
||||
elif kind == "FUNDING_FEE":
|
||||
ev = {
|
||||
"kind": "FUNDING_FEE",
|
||||
"event_id": f"storm-ff-{i:06d}",
|
||||
"funding_amount": round(rng.uniform(-1.0, 1.0), 6),
|
||||
}
|
||||
else: # MARK_PRICE — FSM path via on_venue_event
|
||||
ev = {
|
||||
"kind": "MARK_PRICE",
|
||||
"price": round(rng.uniform(0.1, 100.0), 4),
|
||||
}
|
||||
events.append(ev)
|
||||
return events
|
||||
|
||||
|
||||
def sequence_hash(events: List[Dict[str, Any]]) -> str:
|
||||
payload = json.dumps(events, sort_keys=True, separators=(",", ":")).encode()
|
||||
return hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
# ── Harness ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class ReactorHarness:
|
||||
"""Owns the kernel, the consumer queue, and the deadline scheduler."""
|
||||
|
||||
def __init__(self, *, kernel: Any = None) -> None:
|
||||
if kernel is None:
|
||||
# Real kernel, MOCK venue — the production bundle constructor.
|
||||
from prod.clean_arch.dita_v2.launcher import build_launcher_bundle
|
||||
|
||||
bundle = build_launcher_bundle(venue_mode="MOCK", max_slots=1)
|
||||
kernel = bundle.kernel
|
||||
self.kernel = kernel
|
||||
# Seed K = E so the synthetic storm starts reconciled.
|
||||
if hasattr(self.kernel, "reset_and_seed"):
|
||||
try:
|
||||
self.kernel.reset_and_seed(25_000.0)
|
||||
except Exception:
|
||||
pass
|
||||
self.reaction_hist = LatencyHistogram("venue_event_reaction")
|
||||
self.kernel_hist = LatencyHistogram("kernel_call")
|
||||
self.jitter_hist = LatencyHistogram("deadline_jitter")
|
||||
self.account_clock = PlaneClock("account", ACCOUNT_STALENESS_NS)
|
||||
self.early_fires = 0
|
||||
self._last_k_capital = 25_000.0
|
||||
|
||||
def _fold(self, ev: Dict[str, Any]) -> None:
|
||||
"""Fold one storm event into the kernel, bracketing the FFI call."""
|
||||
kind = ev.get("kind")
|
||||
t0 = mono_ns()
|
||||
if kind == "MARK_PRICE":
|
||||
# FSM path: cheap venue event on slot 0 (flat slot → no-op walk).
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from prod.clean_arch.dita_v2.contracts import (
|
||||
KernelEventKind,
|
||||
TradeSide,
|
||||
VenueEvent,
|
||||
VenueEventStatus,
|
||||
)
|
||||
|
||||
event = VenueEvent(
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
event_id="",
|
||||
trade_id="",
|
||||
slot_id=0,
|
||||
kind=KernelEventKind.MARK_PRICE,
|
||||
status=VenueEventStatus.ACKED,
|
||||
venue_order_id="",
|
||||
venue_client_id="",
|
||||
side=TradeSide.FLAT,
|
||||
asset="STORMUSDT",
|
||||
price=float(ev["price"]),
|
||||
size=0.0,
|
||||
filled_size=0.0,
|
||||
remaining_size=0.0,
|
||||
reason="",
|
||||
raw_payload={},
|
||||
metadata={},
|
||||
)
|
||||
self.kernel.on_venue_event(event)
|
||||
else:
|
||||
if kind == "ACCOUNT_UPDATE" and float(ev.get("wallet_balance", 0.0)) <= 0.0:
|
||||
# Substitute the kernel's own k_capital (sentinel resolution;
|
||||
# captured for free from prior fold returns — no extra FFI).
|
||||
ev = dict(ev)
|
||||
ev["wallet_balance"] = self._last_k_capital
|
||||
ev["available_margin"] = self._last_k_capital
|
||||
result = self.kernel.on_account_event(ev)
|
||||
if isinstance(result, dict):
|
||||
k_cap = result.get("k_capital")
|
||||
if isinstance(k_cap, (int, float)) and k_cap > 0:
|
||||
self._last_k_capital = float(k_cap)
|
||||
self.kernel_hist.record(mono_ns() - t0)
|
||||
self.account_clock.tick()
|
||||
|
||||
async def run_storm(self, spec: StormSpec) -> HarnessReport:
|
||||
events = generate_events(spec)
|
||||
seq_hash = sequence_hash(events)
|
||||
|
||||
queue: asyncio.Queue = asyncio.Queue()
|
||||
done = asyncio.Event()
|
||||
|
||||
# Deadline scheduler runs concurrently throughout the storm.
|
||||
sched = DeadlineScheduler(jitter_hist=self.jitter_hist)
|
||||
sched.start()
|
||||
rng = random.Random(spec.seed ^ 0xD15EA5E)
|
||||
fired_early_box = [0]
|
||||
lo, hi = spec.deadline_spread_ms
|
||||
|
||||
def _mk_cb(due_holder: List[int]) -> Any:
|
||||
def _cb() -> None:
|
||||
if mono_ns() < due_holder[0]:
|
||||
fired_early_box[0] += 1
|
||||
return _cb
|
||||
|
||||
for _ in range(spec.deadlines):
|
||||
delay_ms = rng.uniform(lo, hi)
|
||||
due = mono_ns() + int(delay_ms * 1e6)
|
||||
sched.schedule_at(due, _mk_cb([due]))
|
||||
|
||||
async def producer() -> None:
|
||||
i = 0
|
||||
n = len(events)
|
||||
while i < n:
|
||||
if spec.arrival == "burst":
|
||||
burst_end = min(i + spec.burst_size, n)
|
||||
while i < burst_end:
|
||||
await queue.put((mono_ns(), i, events[i]))
|
||||
i += 1
|
||||
await asyncio.sleep(spec.inter_burst_ms / 1000.0)
|
||||
elif spec.arrival == "poisson":
|
||||
await queue.put((mono_ns(), i, events[i]))
|
||||
i += 1
|
||||
await asyncio.sleep(rng.expovariate(1000.0)) # ~1k ev/s
|
||||
else: # uniform
|
||||
await queue.put((mono_ns(), i, events[i]))
|
||||
i += 1
|
||||
await asyncio.sleep(0.0005)
|
||||
await queue.put((0, -1, None)) # sentinel
|
||||
|
||||
async def reactor() -> None:
|
||||
while True:
|
||||
inject_ns, idx, ev = await queue.get()
|
||||
if ev is None:
|
||||
done.set()
|
||||
return
|
||||
self._fold(ev)
|
||||
if idx >= spec.warmup_events:
|
||||
self.reaction_hist.record(mono_ns() - inject_ns)
|
||||
|
||||
storm_t0 = mono_ns()
|
||||
prod_task = asyncio.create_task(producer(), name="storm_producer")
|
||||
react_task = asyncio.create_task(reactor(), name="storm_reactor")
|
||||
await done.wait()
|
||||
await prod_task
|
||||
await react_task
|
||||
storm_seconds = max(1e-9, (mono_ns() - storm_t0) / 1e9)
|
||||
sustained_eps = len(events) / storm_seconds
|
||||
# Let remaining short deadlines drain, then stop the driver.
|
||||
await asyncio.sleep(min(2.5, hi / 1000.0 + 0.1))
|
||||
await sched.stop()
|
||||
self.early_fires = fired_early_box[0]
|
||||
|
||||
reaction_p99_ms = self.reaction_hist.percentile_ns(0.99) / 1e6
|
||||
jitter_p99_ms = self.jitter_hist.percentile_ns(0.99) / 1e6
|
||||
gate = {
|
||||
"reaction_p99_ms": reaction_p99_ms,
|
||||
"reaction_budget_ms": GATE_REACTION_P99_MS,
|
||||
"jitter_p99_ms": jitter_p99_ms,
|
||||
"jitter_budget_ms": GATE_JITTER_P99_MS,
|
||||
"early_fires": self.early_fires,
|
||||
"deadlines_fired": sched.fired_count,
|
||||
"offered_events_per_s": round(
|
||||
spec.burst_size / max(1e-9, spec.inter_burst_ms / 1000.0)
|
||||
if spec.arrival == "burst" else 0.0, 1),
|
||||
"sustained_events_per_s": round(sustained_eps, 1),
|
||||
"storm_seconds": round(storm_seconds, 3),
|
||||
}
|
||||
passed = (
|
||||
reaction_p99_ms < GATE_REACTION_P99_MS
|
||||
and jitter_p99_ms < GATE_JITTER_P99_MS
|
||||
and self.early_fires == 0
|
||||
)
|
||||
meta = {
|
||||
"utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"python": sys.version.split()[0],
|
||||
"platform": platform.platform(),
|
||||
"node": platform.node(),
|
||||
}
|
||||
return HarnessReport(
|
||||
spec=spec,
|
||||
histograms={
|
||||
h.name: h.to_dict()
|
||||
for h in (self.reaction_hist, self.kernel_hist, self.jitter_hist)
|
||||
},
|
||||
gate=gate,
|
||||
passed=passed,
|
||||
sequence_hash=seq_hash,
|
||||
meta=meta,
|
||||
)
|
||||
|
||||
|
||||
# ── CLI ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def _amain(args: argparse.Namespace) -> int:
|
||||
spec = StormSpec(
|
||||
n_events=args.events,
|
||||
seed=args.seed,
|
||||
deadlines=args.deadlines,
|
||||
arrival=args.arrival,
|
||||
)
|
||||
harness = ReactorHarness()
|
||||
report = await harness.run_storm(spec)
|
||||
print(report.table())
|
||||
if args.out:
|
||||
out = Path(args.out)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(report.to_json())
|
||||
print(f"report written: {out}")
|
||||
if args.gate and not report.passed:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: Optional[List[str]] = None) -> int:
|
||||
ap = argparse.ArgumentParser(description="VIOLET V0 reactor latency harness")
|
||||
ap.add_argument("--events", type=int, default=50_000)
|
||||
ap.add_argument("--seed", type=int, default=42)
|
||||
ap.add_argument("--deadlines", type=int, default=5_000)
|
||||
ap.add_argument("--arrival", choices=("uniform", "poisson", "burst"), default="burst")
|
||||
ap.add_argument("--out", type=str, default="")
|
||||
ap.add_argument("--gate", action="store_true")
|
||||
args = ap.parse_args(argv)
|
||||
return asyncio.run(_amain(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user