"""VIOLET V4 execution runner. This is the live-capable VIOLET runner boundary: NG7 scan -> BLUE-faithful VIOLET decision -> ExecIntent -> DITAv2 KernelIntent -> DITAv2 BingX VST venue execution Safety is explicit rather than castrated: the module can submit real orders via ``ExecutionKernel.process_intent_async`` when the operator runs the V4 launcher with VST keys and arming gates green. Unit tests inject fake kernels and never touch a venue. """ from __future__ import annotations import argparse import asyncio import gc import json import logging import math import os import sys import threading import time import uuid from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path from typing import Any, Callable, Mapping, Optional from prod.clean_arch.dita_v2.contracts import ( KernelCommandType, KernelIntent, KernelOutcome, TradeSide, ) from .contracts_v3 import ExecIntent from .decision_engine import ShadowDecision from .exec_intent import to_exec_intent RUNNER_CONTRACT_VERSION = "violet-v4-exec-runner-contracts-20260626" LOGGER = logging.getLogger("violet.v4_execution_runner") _ASEX_SRC = Path("/mnt/dolphinng5_predict/ASEx/src") if _ASEX_SRC.exists() and str(_ASEX_SRC) not in sys.path: sys.path.insert(0, str(_ASEX_SRC)) try: # pragma: no cover - exercised when ASEx package is installed/available. from asex.guarded import ASExGuardedState from asex.worker import ASExWorker except Exception: # pragma: no cover - fallback is covered by unit tests. ASExGuardedState = None ASExWorker = None def _finite_positive(value: float, name: str) -> float: out = float(value) if not math.isfinite(out) or out <= 0.0: raise ValueError(f"{name} must be finite and > 0") return out def _non_empty(value: str, name: str) -> str: out = str(value or "").strip() if not out: raise ValueError(f"{name} must be non-empty") return out def _trade_side(side: str) -> TradeSide: normalized = str(side or "").strip().upper() if normalized == "LONG": return TradeSide.LONG if normalized == "SHORT": return TradeSide.SHORT raise ValueError(f"unsupported VIOLET side: {side!r}") def _action_from_intent(intent: ExecIntent, explicit: Optional[KernelCommandType]) -> KernelCommandType: if explicit is not None: return explicit if intent.reason == "ENTRY": return KernelCommandType.ENTER if intent.reason == "EXIT": return KernelCommandType.EXIT raise ValueError(f"unsupported VIOLET intent reason: {intent.reason!r}") def exec_intent_to_kernel_intent( intent: ExecIntent, *, reference_price: float, trade_id: str, slot_id: int = 0, intent_id: Optional[str] = None, action: Optional[KernelCommandType] = None, order_type: str = "MARKET", limit_price: float = 0.0, metadata: Optional[Mapping[str, Any]] = None, timestamp: Optional[datetime] = None, ) -> KernelIntent: """Convert a VIOLET ``ExecIntent`` into a DITAv2 ``KernelIntent``. The function is intentionally pure and strict. It does not fetch prices, allocate state outside the returned object, or contact a venue. Live runner code must pass a fresh reference price with provenance in ``metadata``. """ price = _finite_positive(reference_price, "reference_price") qty = _finite_positive(float(intent.qty), "intent.qty") leverage = _finite_positive(float(intent.exchange_leverage), "intent.exchange_leverage") tid = _non_empty(trade_id, "trade_id") oid = _non_empty(intent_id or f"violet-v4-{uuid.uuid4().hex}", "intent_id") order = _non_empty(order_type, "order_type").upper() if order == "LIMIT": _finite_positive(limit_price, "limit_price") elif float(limit_price or 0.0) < 0.0: raise ValueError("limit_price must be >= 0") merged_metadata: dict[str, Any] = dict(metadata or {}) merged_metadata.update( { "violet_contract_version": RUNNER_CONTRACT_VERSION, "violet_ts_ns": int(intent.ts_ns), "violet_maker_policy": intent.maker_policy, "violet_target_notional": float(intent.target_notional), "violet_reason": intent.reason, } ) return KernelIntent( timestamp=timestamp or datetime.now(timezone.utc), intent_id=oid, trade_id=tid, slot_id=int(slot_id), asset=_non_empty(intent.asset, "intent.asset"), side=_trade_side(intent.side), action=_action_from_intent(intent, action), reference_price=price, target_size=qty, leverage=leverage, reason=f"violet_v4:{intent.reason.lower()}", metadata=merged_metadata, order_type=order, limit_price=float(limit_price or 0.0), ) @dataclass(frozen=True) class VioletV4RunnerConfig: """Operator/runtime knobs for the V4 execution runner.""" session_id: str capital: float = 69_000.0 slot_id: int = 0 maker_policy: str = "maker_both" order_type: str = "MARKET" limit_price: float = 0.0 poll_interval_s: float = 0.25 queue_maxsize: int = 4096 max_submissions: int = 0 max_notional_usdt: float = 0.0 assume_vol_ok: bool = False require_arming: bool = True report_dir: str = "/mnt/vp-VIOLET_main/prod/VIOLET_dev/reports" hz_cluster: str = "dolphin" hz_host: str = "localhost:5701" hz_map: str = "DOLPHIN_FEATURES" hz_key: str = "latest_eigen_scan" snapshot_symbol: str = "BTCUSDT" @classmethod def from_env(cls) -> "VioletV4RunnerConfig": return cls( session_id=os.environ.get("DOLPHIN_VIOLET_SESSION_ID", uuid.uuid4().hex), capital=float(os.environ.get("DOLPHIN_VIOLET_CAPITAL", "69000")), slot_id=int(os.environ.get("DOLPHIN_VIOLET_SLOT_ID", "0")), maker_policy=os.environ.get("DOLPHIN_VIOLET_MAKER_POLICY", "maker_both"), order_type=os.environ.get("DOLPHIN_VIOLET_ORDER_TYPE", "MARKET"), limit_price=float(os.environ.get("DOLPHIN_VIOLET_LIMIT_PRICE", "0")), poll_interval_s=float(os.environ.get("DOLPHIN_VIOLET_POLL_INTERVAL_SEC", "0.25")), queue_maxsize=int(os.environ.get("DOLPHIN_VIOLET_SCAN_QUEUE_MAX", "4096")), max_submissions=int(os.environ.get("DOLPHIN_VIOLET_MAX_SUBMISSIONS", "0")), max_notional_usdt=float(os.environ.get("DOLPHIN_VIOLET_MAX_NOTIONAL_USDT", "0")), assume_vol_ok=str(os.environ.get("DOLPHIN_VIOLET_ASSUME_VOL_OK", "0")).lower() in {"1", "true", "yes", "on"}, require_arming=str(os.environ.get("DOLPHIN_VIOLET_REQUIRE_ARMING", "1")).lower() not in {"0", "false", "no", "off"}, report_dir=os.environ.get( "DOLPHIN_VIOLET_REPORT_DIR", "/mnt/vp-VIOLET_main/prod/VIOLET_dev/reports", ), hz_cluster=os.environ.get("HZ_CLUSTER", "dolphin"), hz_host=os.environ.get("HZ_HOST", "localhost:5701"), hz_map=os.environ.get("DOLPHIN_VIOLET_SCAN_MAP", "DOLPHIN_FEATURES"), hz_key=os.environ.get("DOLPHIN_VIOLET_SCAN_KEY", "latest_eigen_scan"), snapshot_symbol=os.environ.get("DOLPHIN_VIOLET_SNAPSHOT_SYMBOL", "BTCUSDT"), ) class _LocalGuardedState: """Tiny ASEx-compatible fallback for tests or missing ASEx install.""" def __init__(self) -> None: self._lock = threading.Lock() self.last_scan_number = -1 self.scans_seen = 0 self.decisions = 0 self.submissions = 0 self.outcomes = 0 self.errors = 0 self.last_trade_id = "" self.last_intent_id = "" def mutate(self, mutation: Mapping[str, Any]) -> dict[str, Any]: with self._lock: return self._apply(dict(mutation)) def _apply(self, mutation: dict[str, Any]) -> dict[str, Any]: op = str(mutation.get("op") or "") if op == "scan": sn = int(mutation.get("scan_number") or 0) if sn <= self.last_scan_number: return {"accepted": False, "reason": "duplicate_scan", "last_scan_number": self.last_scan_number} self.last_scan_number = sn self.scans_seen += 1 return {"accepted": True, "last_scan_number": self.last_scan_number} if op == "decision": self.decisions += 1 return {"accepted": True, "decisions": self.decisions} if op == "submission": self.submissions += 1 self.last_trade_id = str(mutation.get("trade_id") or "") self.last_intent_id = str(mutation.get("intent_id") or "") return {"accepted": True, "submissions": self.submissions} if op == "outcome": self.outcomes += 1 return {"accepted": True, "outcomes": self.outcomes} if op == "error": self.errors += 1 return {"accepted": True, "errors": self.errors} return {"accepted": False, "reason": f"unknown_op:{op}"} if ASExGuardedState is not None: class VioletRunnerGuardedState(ASExGuardedState[dict[str, Any], dict[str, Any]]): """ASEx single-writer state for scan/order lifecycle counters.""" def __init__(self) -> None: super().__init__() self._state = _LocalGuardedState() def _validate(self, mutation: dict[str, Any]) -> bool: return str(mutation.get("op") or "") in { "scan", "decision", "submission", "outcome", "error", } def _apply(self, mutation: dict[str, Any]) -> dict[str, Any]: return self._state._apply(dict(mutation)) else: VioletRunnerGuardedState = _LocalGuardedState # type: ignore[assignment] class SerialRunnerState: """Small wrapper around ASExWorker with a deterministic local fallback.""" def __init__(self) -> None: self.backend = VioletRunnerGuardedState() self.worker = ASExWorker(self.backend) if ASExWorker is not None else None def mutate(self, mutation: Mapping[str, Any], *, timeout: float = 5.0) -> dict[str, Any]: payload = dict(mutation) if self.worker is not None: return self.worker.mutate(payload).result(timeout=timeout) return self.backend.mutate(payload) # type: ignore[attr-defined] def close(self) -> None: if self.worker is not None: self.worker.close(timeout=2.0) def _parse_scan_payload(raw: Any) -> dict[str, Any]: if raw is None: return {} if isinstance(raw, str): data = json.loads(raw) elif isinstance(raw, Mapping): data = dict(raw) else: return {} if isinstance(data, dict) and data.get("version") == "NG7": try: from prod.clean_arch.adapters.eigen_scan_normalizer import normalize_ng7_scan data = normalize_ng7_scan(data) except Exception: LOGGER.debug("NG7 normalize failed; using raw scan", exc_info=True) return data if isinstance(data, dict) else {} def reference_price_from_scan(payload: Mapping[str, Any], asset: str) -> float: """Extract the decision asset price from an NG7/universe payload.""" symbol = str(asset or "").upper() assets = payload.get("assets") or [] prices = payload.get("asset_prices") or [] if isinstance(assets, list) and isinstance(prices, list): for raw_asset, raw_price in zip(assets, prices): if str(raw_asset).upper() != symbol: continue price = _finite_positive(float(raw_price), "reference_price") return price result = payload.get("result") if isinstance(payload, Mapping) else None if isinstance(result, Mapping): result_asset = str(result.get("asset") or payload.get("target_asset") or "").upper() if result_asset == symbol: return _finite_positive(float(result.get("price") or payload.get("price")), "reference_price") payload_asset = str(payload.get("asset") or payload.get("target_asset") or "").upper() if payload_asset == symbol and payload.get("price") is not None: return _finite_positive(float(payload.get("price")), "reference_price") raise ValueError(f"scan payload has no fresh price for {symbol}") def _decision_from_shadow( shadow: dict[str, Any], payload: dict[str, Any], *, scan_number: int, now_ns: int, vel_div: float, vol_ok: bool, ) -> Optional[ShadowDecision]: """Run the existing BLUE-faithful VIOLET shadow path and return the decision.""" from .shadow_live_factors import _ensure_ob_engine shadow["engine"].observe(payload, scan_number) live_source = shadow.get("live_source") if live_source is None: return None ob_engine = _ensure_ob_engine(shadow, payload) live_result = live_source( shadow["client"], scan_history=shadow["scan_history"], selector=shadow["selector"], ob_engine=ob_engine, bar_idx=shadow.get("ob_bar_idx", 0), prior_boost_beta=shadow.get("prior_boost_beta"), ) shadow["last_live_source"] = live_result shadow["ob_bar_idx"] = shadow.get("ob_bar_idx", 0) + 1 ab = getattr(live_result, "acb_boost", None) bb = getattr(live_result, "acb_beta", None) if ab is not None and bb is not None: shadow["prior_boost_beta"] = (ab, bb) decision = shadow["engine"].decide( now_ns=now_ns, scan_number=scan_number, capital=shadow["capital"], vel_div=vel_div, vol_ok=vol_ok, factors=live_result.factors, ) if decision is None: return None journal = shadow.get("journal") if journal is not None: journal.journal(decision, mono_ns=now_ns) return decision class HazelcastNG7ScanSource: """Event-listener first, polling fallback source for NG7 scans.""" def __init__(self, config: VioletV4RunnerConfig) -> None: self.config = config self.client = None self.map = None self.blocking_map = None self.queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue(maxsize=config.queue_maxsize) self._loop: Optional[asyncio.AbstractEventLoop] = None self._last_polled_scan = -1 async def connect(self) -> None: import hazelcast self._loop = asyncio.get_running_loop() self.client = hazelcast.HazelcastClient( cluster_name=self.config.hz_cluster, cluster_members=[self.config.hz_host], ) self.map = self.client.get_map(self.config.hz_map) self.blocking_map = self.map.blocking() def on_entry(event: Any) -> None: try: payload = _parse_scan_payload(getattr(event, "value", None)) if not payload: return loop = self._loop if loop is None: return loop.call_soon_threadsafe(self._offer_payload, payload) except Exception: LOGGER.debug("scan listener callback failed", exc_info=True) self.map.add_entry_listener( include_value=True, key=self.config.hz_key, updated_func=on_entry, added_func=on_entry, ) def _offer_payload(self, payload: dict[str, Any]) -> None: try: self.queue.put_nowait(payload) except asyncio.QueueFull: try: self.queue.get_nowait() except asyncio.QueueEmpty: pass self.queue.put_nowait(payload) async def next_payload(self) -> dict[str, Any]: try: return await asyncio.wait_for(self.queue.get(), timeout=self.config.poll_interval_s) except asyncio.TimeoutError: raw = self.blocking_map.get(self.config.hz_key) payload = _parse_scan_payload(raw) sn = int(payload.get("scan_number") or 0) if sn <= self._last_polled_scan: return {} self._last_polled_scan = sn return payload async def close(self) -> None: if self.client is not None: self.client.shutdown() class VioletV4ExecutionRunner: """Live-capable runner: decision path to actual DITAv2 execution.""" def __init__( self, *, bundle: Any, shadow: dict[str, Any], config: VioletV4RunnerConfig, state: Optional[SerialRunnerState] = None, decision_step: Optional[ Callable[[dict[str, Any], dict[str, Any], int, int, float, bool], Optional[ShadowDecision]] ] = None, ) -> None: self.bundle = bundle self.shadow = shadow self.config = config self.state = state or SerialRunnerState() self.decision_step = decision_step or ( lambda sh, payload, sn, now_ns, vd, vol_ok: _decision_from_shadow( sh, payload, scan_number=sn, now_ns=now_ns, vel_div=vd, vol_ok=vol_ok, ) ) self._closed = False async def connect(self) -> None: connect = getattr(self.bundle.venue, "connect", None) if connect is not None: result = connect() if asyncio.iscoroutine(result): await result def close(self) -> None: if self._closed: return self._closed = True try: close = getattr(self.bundle, "close", None) if close is not None: close() finally: self.state.close() def _capital(self) -> float: try: cap = float(self.bundle.kernel.account.snapshot.capital) if math.isfinite(cap) and cap > 0.0: return cap except Exception: pass return float(self.config.capital) def _maybe_cap_exec_intent(self, intent: ExecIntent, reference_price: float) -> ExecIntent: cap = float(self.config.max_notional_usdt or 0.0) if cap <= 0.0 or float(intent.target_notional) <= cap: return intent return ExecIntent( asset=intent.asset, side=intent.side, qty=cap / reference_price, exchange_leverage=intent.exchange_leverage, maker_policy=intent.maker_policy, target_notional=cap, ts_ns=intent.ts_ns, reason=intent.reason, ) async def process_scan_payload(self, raw_payload: Any) -> Optional[KernelOutcome]: payload = _parse_scan_payload(raw_payload) if not payload: return None scan_number = int(payload.get("scan_number") or 0) if scan_number <= 0: return None scan_state = self.state.mutate({"op": "scan", "scan_number": scan_number}) if not scan_state.get("accepted"): return None vel_div = float(payload.get("vel_div") or 0.0) if not math.isfinite(vel_div): vel_div = 0.0 vol_ok = bool(payload.get("vol_ok", True)) if self.config.assume_vol_ok: vol_ok = True now_ns = int(time.monotonic_ns()) decision = self.decision_step(self.shadow, payload, scan_number, now_ns, vel_div, vol_ok) if decision is None: return None self.state.mutate({"op": "decision", "scan_number": scan_number, "asset": decision.asset}) reference_price = reference_price_from_scan(payload, decision.asset) exec_intent = to_exec_intent( decision, capital=self._capital(), reference_price=reference_price, maker_policy=self.config.maker_policy, ) exec_intent = self._maybe_cap_exec_intent(exec_intent, reference_price) trade_id = f"VIOLET-{self.config.session_id[:8]}-{scan_number}" intent_id = f"violet-v4-{self.config.session_id[:8]}-{scan_number}" kernel_intent = exec_intent_to_kernel_intent( exec_intent, reference_price=reference_price, trade_id=trade_id, slot_id=self.config.slot_id, intent_id=intent_id, order_type=self.config.order_type, limit_price=self.config.limit_price, metadata={ "violet_session_id": self.config.session_id, "scan_number": scan_number, "price_source": "ng7_scan", "raw_notional_before_runner_cap": float(decision.target_exposure), }, ) submission = self.state.mutate( {"op": "submission", "scan_number": scan_number, "trade_id": trade_id, "intent_id": intent_id} ) if self.config.max_submissions and int(submission.get("submissions") or 0) > self.config.max_submissions: LOGGER.critical("max submissions reached; refusing scan=%s trade_id=%s", scan_number, trade_id) return None LOGGER.warning( "VIOLET V4 LIVE SUBMIT scan=%s asset=%s side=%s qty=%.8f lev=%.2f ref=%.8f trade=%s", scan_number, kernel_intent.asset, kernel_intent.side.value, kernel_intent.target_size, kernel_intent.leverage, kernel_intent.reference_price, trade_id, ) try: outcome = await self.bundle.kernel.process_intent_async(kernel_intent) self.state.mutate( { "op": "outcome", "scan_number": scan_number, "trade_id": trade_id, "accepted": bool(getattr(outcome, "accepted", False)), "diagnostic": str(getattr(getattr(outcome, "diagnostic_code", ""), "value", "")), } ) return outcome except Exception as exc: self.state.mutate({"op": "error", "scan_number": scan_number, "error": str(exc)}) raise def _apply_runtime_optimizations() -> None: if gc.isenabled(): gc.disable() def _build_shadow(config: VioletV4RunnerConfig) -> dict[str, Any]: from prod.ch_writer import ch_put_violet from .decision_engine import VioletDecisionEngine from .shadow_journal import VioletDecisionJournal from .shadow_live_factors import build_shadow_live_source thr = float(os.environ.get("DOLPHIN_VIOLET_ENTRY_VEL_DIV_THRESHOLD", "-0.02")) return { "engine": VioletDecisionEngine(entry_vel_div_threshold=thr), "journal": VioletDecisionJournal(sink=ch_put_violet, session_id=config.session_id), "capital": float(config.capital), **build_shadow_live_source(), } def _build_real_bundle(config: VioletV4RunnerConfig) -> Any: from prod.clean_arch.dita_v2.launcher import build_launcher_bundle from prod.launch_dolphin_violet import ( DDL_APPLY_CMD, _apply_violet_env, _preflight_clickhouse, _violet_table_present, build_bingx_exec_client_config, ) from .v4_arming import assess_v4_arming, write_v4_arming_report _apply_violet_env() missing = _preflight_clickhouse() if missing: raise RuntimeError(f"dolphin_violet tables missing: {missing}; run {DDL_APPLY_CMD}") if not _violet_table_present("violet_decisions"): raise RuntimeError(f"dolphin_violet table missing: violet_decisions; run {DDL_APPLY_CMD}") report = assess_v4_arming(report_dir=config.report_dir, launcher_mode="execution") write_v4_arming_report(report, report_dir=config.report_dir) if config.require_arming and not report.can_arm: raise RuntimeError(f"VIOLET V4 arming refused: {report.reasons}") return build_launcher_bundle( venue_mode="BINGX", max_slots=1, prefix="violet", bingx_config=build_bingx_exec_client_config(), ) async def run_live( config: Optional[VioletV4RunnerConfig] = None, *, stop_after_first_submission: bool = False, ) -> None: """Run the actual VIOLET V4 BingX VST execution loop.""" cfg = config or VioletV4RunnerConfig.from_env() _apply_runtime_optimizations() bundle = _build_real_bundle(cfg) shadow = _build_shadow(cfg) source = HazelcastNG7ScanSource(cfg) runner = VioletV4ExecutionRunner(bundle=bundle, shadow=shadow, config=cfg) await runner.connect() await source.connect() LOGGER.critical( "VIOLET V4 EXECUTION RUNNER ARMED: session=%s venue=BINGX_VST hz=%s[%s]", cfg.session_id, cfg.hz_map, cfg.hz_key, ) try: while True: payload = await source.next_payload() if payload: outcome = await runner.process_scan_payload(payload) if stop_after_first_submission and outcome is not None: return await asyncio.sleep(0) finally: await source.close() runner.close() def main(argv: Optional[list[str]] = None) -> int: parser = argparse.ArgumentParser(description="VIOLET V4 live BingX VST execution runner") parser.add_argument("--once", action="store_true", help="process one queued/polled scan then exit") args = parser.parse_args(argv) config = VioletV4RunnerConfig.from_env() if args.once: config = dataclass_replace(config, max_submissions=1) asyncio.run(run_live(config, stop_after_first_submission=args.once)) return 0 def dataclass_replace(config: VioletV4RunnerConfig, **updates: Any) -> VioletV4RunnerConfig: data = config.__dict__.copy() data.update(updates) return VioletV4RunnerConfig(**data) __all__ = [ "HazelcastNG7ScanSource", "RUNNER_CONTRACT_VERSION", "SerialRunnerState", "VioletV4ExecutionRunner", "VioletV4RunnerConfig", "exec_intent_to_kernel_intent", "main", "reference_price_from_scan", "run_live", ] if __name__ == "__main__": raise SystemExit(main())