#!/usr/bin/env python3 """ DOLPHIN Nautilus Event-Driven Trader """ import sys import json import math import os import time import signal import threading import urllib.request from typing import Optional from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timezone from pathlib import Path from collections import deque # Stablecoins / pegged assets that must never be traded _STABLECOIN_SYMBOLS = frozenset({ 'USDCUSDT', 'BUSDUSDT', 'FDUSDUSDT', 'USDTUSDT', 'TUSDUSDT', 'DAIUSDT', 'FRAXUSDT', 'USDDUSDT', 'USTCUSDT', 'EURUSDT', }) sys.path.insert(0, '/mnt/dolphinng5_predict') sys.path.insert(0, '/mnt/dolphinng5_predict/nautilus_dolphin') from nautilus_dolphin.nautilus.proxy_boost_engine import create_d_liq_engine from nautilus_dolphin.nautilus.esf_alpha_orchestrator import NDPosition from nautilus_dolphin.nautilus.adaptive_circuit_breaker import AdaptiveCircuitBreaker from nautilus_dolphin.nautilus.ob_features import OBFeatureEngine from nautilus_dolphin.nautilus.ob_provider import MockOBProvider from nautilus_dolphin.nautilus.esof_size_gate import parse_esof_payload, esof_gate_from_payload try: from adaptive_exit.market_state_runtime import MarketStateRuntime except Exception: MarketStateRuntime = None try: from adaptive_exit.sc_threshold_advisor import SCThresholdAdvisor except Exception: SCThresholdAdvisor = None try: from adaptive_exit.sc_gauge_advisor import SCGaugeAdvisor, build_obf_snapshot_from_engine except Exception: SCGaugeAdvisor = None build_obf_snapshot_from_engine = None try: from adaptive_exit.bounce_advisor import BounceAdvisor except Exception: BounceAdvisor = None try: from nautilus_dolphin.nautilus.alpha_exit_v7_engine import AlphaExitEngineV7, TradeContextV7 except Exception: AlphaExitEngineV7 = None TradeContextV7 = None BLUE_CH_DB = "dolphin" try: from prod.ch_writer import ch_put, ts_us as _ch_ts_us except ImportError: def ch_put(*a, **kw): pass def _ch_ts_us(): return 0 try: from announcement_router import build_announcement_center except ImportError: from prod.announcement_router import build_announcement_center sys.path.insert(0, '/mnt/dolphinng5_predict/prod') from dolphin_exit_handler import install_exit_handler install_exit_handler("nautilus_trader") HZ_CLUSTER = "dolphin" HZ_HOST = "127.0.0.1:5701" EIGEN_DIR = Path('/mnt/dolphinng6_data/eigenvalues') CAPITAL_DISK_CHECKPOINT = Path("/tmp/dolphin_capital_checkpoint.json") ANNOUNCEMENT_CONFIG = Path("/mnt/dolphinng5_predict/prod/configs/position_notifications_blue.json") ANNOUNCEMENT_RUNTIME_ENV = Path("/mnt/dolphin_training/observability_notifications_blue.runtime.json") ENGINE_KWARGS = dict( initial_capital=25000.0, vel_div_threshold=-0.02, vel_div_extreme=-0.05, min_leverage=0.5, max_leverage=8.0, # note: create_d_liq_engine overrides to D_LIQ_SOFT_CAP=8.0 leverage_convexity=3.0, fraction=0.20, fixed_tp_pct=0.0095, stop_pct=1.0, max_hold_bars=250, # gold spec: 250 use_direction_confirm=True, dc_lookback_bars=7, dc_min_magnitude_bps=0.75, dc_skip_contradicts=True, dc_leverage_boost=1.0, dc_leverage_reduce=0.5, use_asset_selection=True, min_irp_alignment=0.0, # gold spec: no IRP filter use_sp_fees=True, use_sp_slippage=True, sp_maker_entry_rate=0.62, sp_maker_exit_rate=0.50, use_ob_edge=True, ob_edge_bps=5.0, ob_confirm_rate=0.40, lookback=100, use_alpha_layers=True, use_dynamic_leverage=True, seed=42, allow_subday_acb_exit=False, ) def _env_bool(name: str, default: bool) -> bool: raw = os.environ.get(name) if raw is None: return default return str(raw).strip().lower() in {"1", "true", "yes", "on"} def _direction_from_env(value: Optional[str] = None) -> int: raw = os.environ.get("DOLPHIN_DIRECTION", "short_only") if value is None else value text = str(raw or "short_only").strip().lower() if text in {"short", "short_only", "sell", "-1"}: return -1 if text in {"long", "long_only", "buy", "+1", "1"}: return 1 raise ValueError( f"Unsupported DOLPHIN_DIRECTION={raw!r}; use short_only or long_only" ) def _direction_label(direction: int) -> str: return "LONG" if int(direction) == 1 else "SHORT" def _safe_float(value, default: float = 0.0) -> float: try: out = float(value) except (TypeError, ValueError): return default return out if math.isfinite(out) else default def _flatten_env_payload(payload, prefix: str = "") -> dict: flat = {} if not isinstance(payload, dict): return flat for key, value in payload.items(): if not isinstance(key, str) or not key.strip(): continue full_key = f"{prefix}_{key}" if prefix else key if isinstance(value, dict): flat.update(_flatten_env_payload(value, full_key)) else: flat[full_key.upper()] = value return flat def _seed_runtime_env(path: Path) -> None: if not path.exists(): return try: payload = json.loads(path.read_text()) except Exception: return for key, value in _flatten_env_payload(payload).items(): if key not in os.environ and value not in (None, "", "__CHANGE_ME__", "__REPLACE_ME__"): os.environ[key] = str(value) BTC_VOL_WINDOW = 50 # Per-bucket SL % used when HIBERNATE fires while a position is open. # Instead of immediate HIBERNATE_HALT, we arm TP (existing fixed_tp_pct) + # a per-bucket stop-loss so the position exits cleanly rather than being # force-closed at whatever price the halt fires at. # Values derived from AE shadow data + bucket trade analysis (2026-04-19). # B3 wide: shadow shows mae_norm 5-5.1 before FIXED_TP; 3.5×ATR fires on noise. # B4 tight: 34.8% WR, 0.80 R:R — cut fast, no recovery value. # B6 widest: extreme vol (vol_daily_pct 760-864); normal ATR excursions are large. _BUCKET_SL_PCT: dict = { 0: 0.015, # Low-vol high-corr nano-cap 1: 0.012, # Med-vol low-corr mid-price (XRP/XLM class) 2: 0.015, # Mega-cap BTC/ETH — default (not traded) 3: 0.025, # High-vol mid-corr STAR bucket (ENJ/ADA/DOGE) — needs room 4: 0.008, # Worst bucket (BNB/LTC/LINK) — cut fast 5: 0.018, # High-vol low-corr micro-price (ATOM/TRX class) 6: 0.030, # Extreme-vol mid-corr (FET/ZRX) — widest 'default': 0.015, } # Gold-calibrated from full 5-year BTC history: 0.00026414 (stricter, ~2.7x tighter). # 2026-04-07: switched to 56-day gold window value (0.00009868) — the exact threshold # used in the T=2155 ROI=+189% backtest. More permissive; paper trading to gather data. VOL_P60_THRESHOLD = 0.00009868 # Algorithm Versioning # v1_shakedown: v50-v150 (noise bug), loose vol gate # v2_gold_fix: CORRECTED v50-v750 macro divergence (matches parquet backtest) ALGO_VERSION = "v2_gold_fix_v50-v750" # Persistent, version-tagged trade log (survives reboots; sorts by date) _LOG_DIR = "/mnt/dolphinng5_predict/prod/logs" os.makedirs(_LOG_DIR, exist_ok=True) _LOG_DATE = datetime.now(timezone.utc).strftime("%Y%m%d") TRADE_LOG = f"{_LOG_DIR}/nautilus_trader_{_LOG_DATE}_{ALGO_VERSION}.log" running = True def log(msg): ts = datetime.now(timezone.utc).isoformat() line = f"[{ts}] {msg}" print(line, flush=True) with open(TRADE_LOG, 'a') as f: f.write(line + '\n') class DolphinLiveTrader: def __init__(self): self.eng = None self.hz_client = None self.features_map = None self.safety_map = None self.pnl_map = None self.state_map = None self.heartbeat_map = None self.eng_lock = threading.Lock() self._dedup_lock = threading.Lock() # guards atomic check-and-set on last_scan_number self._scan_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="scan") self.last_scan_number = -1 self.last_file_mtime = 0 self.bar_idx = 0 self.current_day = None self.trades_executed = 0 self.scans_processed = 0 self.btc_prices = deque(maxlen=BTC_VOL_WINDOW + 2) self.cached_posture = "APEX" self.posture_cache_time = 0 self.ob_assets = [] self.ob_eng = None self.acb = None self.last_w750_vel = None self._pending_entries: dict = {} # trade_id → entry snapshot (for CH trade_events) self._last_exf: dict = {} self._exf_log_time = 0.0 # throttle for on_exf_update logging self._ae = None # AdaptiveExitEngine shadow (parallel, never real exits) self._v7_exit_engine = None # AlphaExitEngineV7 live BLUE exit control + journal self._v7_contexts: dict = {} # trade_id → TradeContextV7 self._v7_decisions: dict = {} # trade_id → latest v7 decision self._v7_decision_seq: dict = {} # trade_id → monotonic eval sequence self._v7_journal_enabled: bool = _env_bool("DOLPHIN_ENABLE_V7_JOURNAL", True) self._v7_journal_db: str = BLUE_CH_DB self._v7_journal_table: str = "v7_decision_events" self._v7_live_exit_enabled: bool = False self._sc_advisor = None # SC threshold advisor (shadow-only) self._sc_advisor_last_log = 0.0 self._sc_gauge = None # SC bucket gauge advisor (shadow-only) self._sc_gauge_last_log = 0.0 self._bounce_advisor = None # inverse-ARS bounce advisor (shadow-only) self._bounce_advisor_last_log = 0.0 self._bounce_price_history: dict[str, deque] = {} self._market_state_runtime = MarketStateRuntime() if MarketStateRuntime is not None else None self._hibernate_protect_active: str | None = None # trade_id being protected self._bucket_assignments: dict = {} # asset → KMeans bucket_id (loaded from pkl) self._last_esof_size_mult: float = 1.0 self.trade_direction: int = _direction_from_env() self._trade_announcement_center = None _seed_runtime_env(ANNOUNCEMENT_RUNTIME_ENV) if ANNOUNCEMENT_CONFIG.exists(): try: self._trade_announcement_center = build_announcement_center( ANNOUNCEMENT_CONFIG, hz_getter=self._get_hz, logger=None, ) log(" Position announcements: loaded") except Exception as e: log(f" Position announcements: {e}") self._trade_announcement_center = None def _build_engine(self): log("Building NDAlphaEngine...") engine_kwargs = dict(ENGINE_KWARGS) engine_kwargs["allow_subday_acb_exit"] = _env_bool( "DOLPHIN_ALLOW_ACB_SUBDAY_EXIT", bool(engine_kwargs.get("allow_subday_acb_exit", False)), ) self.eng = create_d_liq_engine(**engine_kwargs) log(f" Engine: {type(self.eng).__name__}") log(f" Direction: {_direction_label(self.trade_direction)} ({self.trade_direction:+d})") log(f" ACB subday exits: {'ON' if engine_kwargs['allow_subday_acb_exit'] else 'OFF'}") log(f" Leverage: soft={self.eng.base_max_leverage}x abs={self.eng.abs_max_leverage}x") if EIGEN_DIR.exists(): try: date_strings = sorted([d.name for d in EIGEN_DIR.iterdir() if d.is_dir()]) self.acb = AdaptiveCircuitBreaker() self.acb.preload_w750(date_strings) self.eng.set_acb(self.acb) log(" ACBv6: loaded") except Exception as e: log(f" ACBv6: {e}") else: self.acb = AdaptiveCircuitBreaker() self.eng.set_acb(self.acb) log(" ACBv6: loaded (no preload dates)") self.eng.set_esoteric_hazard_multiplier(0.0) # gold spec: init guard, MUST precede set_mc_forewarner log(f" Hazard: set_esoteric_hazard_multiplier(0.0) — soft={self.eng.base_max_leverage}x") MC_MODELS_DIR = '/mnt/dolphinng5_predict/nautilus_dolphin/mc_results/models' MC_BASE_CFG = { 'trial_id': 0, 'vel_div_threshold': -0.020, 'vel_div_extreme': -0.050, 'use_direction_confirm': True, 'dc_lookback_bars': 7, 'dc_min_magnitude_bps': 0.75, 'dc_skip_contradicts': True, 'dc_leverage_boost': 1.00, 'dc_leverage_reduce': 0.50, 'vd_trend_lookback': 10, 'min_leverage': 0.50, 'max_leverage': 8.00, # gold spec 'leverage_convexity': 3.00, 'fraction': 0.20, 'use_alpha_layers': True, 'use_dynamic_leverage': True, 'fixed_tp_pct': 0.0095, 'stop_pct': 1.00, 'max_hold_bars': 250, 'use_sp_fees': True, 'use_sp_slippage': True, # gold spec 'sp_maker_entry_rate': 0.62, 'sp_maker_exit_rate': 0.50, 'use_ob_edge': True, 'ob_edge_bps': 5.00, 'ob_confirm_rate': 0.40, 'ob_imbalance_bias': -0.09, 'ob_depth_scale': 1.00, 'use_asset_selection': True, 'min_irp_alignment': 0.0, 'asset_selector_lookback': 10, 'lookback': 100, # gold spec 'acb_beta_high': 0.80, 'acb_beta_low': 0.20, 'acb_w750_threshold_pct': 60, } if Path(MC_MODELS_DIR).exists(): try: from mc.mc_ml import DolphinForewarner forewarner = DolphinForewarner(models_dir=MC_MODELS_DIR) self.eng.set_mc_forewarner(forewarner, MC_BASE_CFG) log(" MC-Forewarner: wired") except Exception as e: log(f" MC-Forewarner: {e}") try: from adaptive_exit.adaptive_exit_engine import AdaptiveExitEngine self._ae = AdaptiveExitEngine.load() log(" AdaptiveExitEngine: loaded (shadow mode — no real exits)") except Exception as e: log(f" AdaptiveExitEngine: {e} — shadow disabled") if AlphaExitEngineV7 is not None and self._v7_journal_enabled: try: self._v7_exit_engine = AlphaExitEngineV7(bar_duration_sec=11.0) self._ensure_v7_journal_table() log(" AlphaExitEngineV7: loaded (live BLUE exit control + journal)") except Exception as e: log(f" AlphaExitEngineV7: {e} — shadow disabled") self._v7_exit_engine = None self._v7_live_exit_enabled = self._v7_exit_engine is not None if self.eng is not None: self.eng.exit_decision_provider = self._v7_live_exit_decision if self._v7_live_exit_enabled else None self._load_bucket_assignments() if SCThresholdAdvisor is not None: try: self._sc_advisor = SCThresholdAdvisor.load( strategy="blue", shadow_db=BLUE_CH_DB, ) log(" SCThresholdAdvisor: loaded (shadow mode — no sizing changes)") except Exception as e: log(f" SCThresholdAdvisor: {e} — shadow disabled") self._sc_advisor = None if SCGaugeAdvisor is not None: try: self._sc_gauge = SCGaugeAdvisor.load( strategy="blue", shadow_db=BLUE_CH_DB, ) log(" SCGaugeAdvisor: loaded (shadow mode — no sizing changes)") except Exception as e: log(f" SCGaugeAdvisor: {e} — shadow disabled") self._sc_gauge = None if BounceAdvisor is not None: try: self._bounce_advisor = BounceAdvisor.load( strategy="blue", shadow_db=BLUE_CH_DB, ) log(" BounceAdvisor: loaded (shadow mode — no execution changes)") except Exception as e: log(f" BounceAdvisor: {e} — shadow disabled") self._bounce_advisor = None def _load_bucket_assignments(self): """Load KMeans asset→bucket_id mapping for hibernate protection SL levels.""" try: import pickle pkl_path = Path('/mnt/dolphinng5_predict/adaptive_exit/models/bucket_assignments.pkl') with open(pkl_path, 'rb') as f: data = pickle.load(f) self._bucket_assignments = data.get('assignments', {}) log(f" BucketAssignments: {len(self._bucket_assignments)} assets loaded for hibernate protection") except Exception as e: log(f" BucketAssignments: {e} — hibernate protect will use default SL={_BUCKET_SL_PCT['default']*100:.1f}%") def _announce_position_event( self, *, kind: str, severity: str, title: str, message: str, metadata: dict | None = None, ) -> None: center = getattr(self, "_trade_announcement_center", None) if center is None: return try: center.note_event( kind=kind, severity=severity, title=title, message=message, metadata=metadata or {}, ) except Exception as e: log(f" Position announcement failed: {e}") def _read_esof_payload(self) -> dict | None: """Read the freshest EsoF advisory payload from HZ, if available.""" if not self.features_map: return None for key in ("esof_latest", "esof_advisor_latest"): try: raw = self.features_map.blocking().get(key) except Exception: continue payload = parse_esof_payload(raw) if payload: return payload return None def _sync_esof_size_gate(self) -> None: """Update the shared engine with the current continuous EsoF size multiplier.""" payload = self._read_esof_payload() score, mult = esof_gate_from_payload(payload) with self.eng_lock: if hasattr(self.eng, "set_esof_advisory_score"): self.eng.set_esof_advisory_score(score) if mult != self._last_esof_size_mult: self._last_esof_size_mult = mult if score is None: log(f"EsoF size gate: neutral mult={mult:.2f} (no fresh score)") else: log(f"EsoF size gate: sc={score:+.3f} mult={mult:.2f}") def _sync_sc_threshold_advisor(self, scan_number: int, vel_div: float) -> None: """Shadow-only advisory layer for tracking / future threshold learning.""" if self._sc_advisor is None: return try: payload = self._read_esof_payload() trade_history = getattr(self.eng, "trade_history", []) open_tid = next(iter(self._pending_entries.keys()), "") pending = self._pending_entries.get(open_tid, {}) if open_tid else {} rec = self._sc_advisor.evaluate( trade_id=str(open_tid or ""), asset=str(pending.get("asset", "")), sc=_safe_float(payload.get("advisory_score", payload.get("score", 0.0)) if payload else None), vel_div=float(vel_div or 0.0), exf_snapshot=getattr(self, "_last_exf", {}) or {}, trade_history=trade_history, current_mult=float(self._last_esof_size_mult or 1.0), esof_payload=payload, scan_number=int(scan_number or 0), bar_idx=int(self.bar_idx), strategy="blue", log_shadow=True, ) if open_tid: pending["sc_threshold_advisor"] = rec pending["sc_exec_mult"] = float(self._last_esof_size_mult or 1.0) self._pending_entries[open_tid] = pending now = time.time() if now - self._sc_advisor_last_log >= 300: self._sc_advisor_last_log = now log( f"SC_ADVISOR: sc={rec['sc']:+.3f} cur={rec['current_mult']:.2f} " f"rec={rec['recommended_mult']:.2f} cut={rec['recommended_sc_cut']:+.2f} " f"conf={rec['confidence']:.2f} src={rec['decision_source']}" ) except Exception as e: log(f"SC_ADVISOR error: {e}") def _current_obf_snapshot(self, asset: str, bar_idx: int) -> dict[str, dict]: if build_obf_snapshot_from_engine is None or self.ob_eng is None or not asset: return {} try: return build_obf_snapshot_from_engine(self.ob_eng, asset, bar_idx) except Exception: return {} def _record_bounce_prices(self, prices_dict: dict[str, float]) -> None: """Maintain rolling price histories for the bounce advisor.""" if not prices_dict: return for asset, px in prices_dict.items(): try: price = float(px) except Exception: continue if not math.isfinite(price) or price <= 0.0: continue hist = self._bounce_price_history.get(asset) if hist is None: hist = deque(maxlen=512) self._bounce_price_history[asset] = hist hist.append(price) def _bounce_price_path(self, asset: str) -> list[float]: hist = self._bounce_price_history.get(asset) if not hist: return [] return [float(px) for px in hist if math.isfinite(float(px))] def _bounce_eval( self, *, trade_id: str, asset: str, side: str, source: str, scan_number: int, entry_ts: datetime | None, current_price: float, entry_price: float, quantity: float, notional: float, leverage: float, vel_div: float, current_mult: float, bars_held: int, log_shadow: bool = True, ) -> dict | None: """Evaluate the bounce advisor on a rolling price path and persist the row.""" if self._bounce_advisor is None or not trade_id or not asset: return None price_path = self._bounce_price_path(asset) if len(price_path) < 3: return None rec = self._bounce_advisor.evaluate( trade_id=str(trade_id), asset=str(asset), side=str(side or "SHORT"), price_path=price_path, entry_ts=entry_ts or datetime.now(timezone.utc), entry_price=float(entry_price or 0.0), current_price=float(current_price or 0.0), quantity=float(quantity or 0.0), notional=float(notional or 0.0), leverage=float(leverage or 0.0), current_mult=float(current_mult or 1.0), vel_div=float(vel_div or 0.0), scan_number=int(scan_number or 0), bar_idx=int(self.bar_idx), bars_held=int(max(0, bars_held)), source=str(source or "entry"), obf_snapshot=self._current_obf_snapshot(asset, self.bar_idx), log_shadow=log_shadow, use_ta=True, use_obf=True, ) if rec: rec["price_path"] = price_path[-128:] return rec def _ensure_v7_journal_table(self) -> None: """Create the V7 decision journal if it does not already exist.""" ddl = f""" CREATE TABLE IF NOT EXISTS {self._v7_journal_db}.{self._v7_journal_table} ( ts DateTime64(6, 'UTC'), ts_day Date MATERIALIZED toDate(ts), strategy LowCardinality(String), source LowCardinality(String), trade_id String, asset LowCardinality(String), side LowCardinality(String), entry_price Float64, current_price Float64, quantity Float64, notional Float64, leverage Float32, bar_idx UInt32, decision_seq UInt32, bars_held UInt16, action LowCardinality(String), reason LowCardinality(String), pnl_pct Float32, mfe Float32, mae Float32, mfe_risk Float32, mae_risk Float32, exit_pressure Float32, rv_comp Float32, mae_thresh1 Float32, bounce_score Float32, bounce_risk Float32, ob_imbalance Float32, vel_div_entry Float32, vel_div_now Float32, v50_vel Float32, v750_vel Float32, exf_funding Float32, exf_dvol Float32, exf_fear_greed Float32, exf_taker Float32, posture LowCardinality(String) ) ENGINE = MergeTree PARTITION BY toYYYYMM(ts) ORDER BY (ts_day, trade_id, decision_seq, ts) TTL ts_day + toIntervalDay(180) """ try: req = urllib.request.Request( "http://localhost:8123/", data=ddl.encode(), method="POST", ) req.add_header("X-ClickHouse-User", "dolphin") req.add_header("X-ClickHouse-Key", "dolphin_ch_2026") urllib.request.urlopen(req, timeout=5).close() except Exception as exc: log(f"[V7_JOURNAL] table ensure failed: {exc}") def _record_v7_decision( self, *, trade_id: str, asset: str, side: str, decision: dict, current_price: float, ob_imbalance: float, vel_div_now: float, v50_vel: float, v750_vel: float, source: str = "scan_eval", bar_idx: int | None = None, ) -> None: """Persist a V7 evaluation for observability and offline comparison.""" if not self._v7_journal_enabled or self._v7_exit_engine is None: return pending = self._pending_entries.get(trade_id, {}) seq = int(self._v7_decision_seq.get(trade_id, 0)) + 1 self._v7_decision_seq[trade_id] = seq entry_price = float(pending.get("entry_price", 0.0) or 0.0) quantity = float(pending.get("quantity", 0.0) or 0.0) row = { "ts": _ch_ts_us(), "strategy": "blue", "source": source, "trade_id": str(trade_id or ""), "asset": str(asset or pending.get("asset", "")), "side": str(side or pending.get("side", "")), "entry_price": entry_price, "current_price": float(current_price or 0.0), "quantity": quantity, "notional": float(quantity * entry_price), "leverage": float(pending.get("leverage", 0.0) or 0.0), "bar_idx": int(max(0, self.bar_idx - 1 if bar_idx is None else bar_idx)), "decision_seq": seq, "bars_held": int(decision.get("bars_held", 0) or 0), "action": str(decision.get("action", "UNKNOWN") or "UNKNOWN"), "reason": str(decision.get("reason") or ""), "pnl_pct": float(decision.get("pnl_pct", 0.0) or 0.0), "mfe": float(decision.get("mfe", 0.0) or 0.0), "mae": float(decision.get("mae", 0.0) or 0.0), "mfe_risk": float(decision.get("mfe_risk", 0.0) or 0.0), "mae_risk": float(decision.get("mae_risk", 0.0) or 0.0), "exit_pressure": float(decision.get("exit_pressure", 0.0) or 0.0), "rv_comp": float(decision.get("rv_comp", 0.0) or 0.0), "mae_thresh1": float(decision.get("mae_thresh1", 0.0) or 0.0), "bounce_score": float(decision.get("bounce_score", 0.0) or 0.0), "bounce_risk": float(decision.get("bounce_risk", 0.0) or 0.0), "ob_imbalance": float(ob_imbalance or 0.0), "vel_div_entry": float(pending.get("vel_div_entry", 0.0) or 0.0), "vel_div_now": float(vel_div_now or 0.0), "v50_vel": float(v50_vel or 0.0), "v750_vel": float(v750_vel or 0.0), "exf_funding": float(self._last_exf.get("funding", 0.0) or 0.0), "exf_dvol": float(self._last_exf.get("dvol", 0.0) or 0.0), "exf_fear_greed": float(self._last_exf.get("fear_greed", 0.0) or 0.0), "exf_taker": float(self._last_exf.get("taker", 0.0) or 0.0), "posture": str(pending.get("posture", self.cached_posture) or ""), } try: ch_put(self._v7_journal_table, row) except Exception as exc: log(f"[V7_JOURNAL] write failed: {exc}") def _v7_live_exit_decision( self, *, pos, bar_idx: int, prices: dict, vel_div: float, v50_vel: float, v750_vel: float, ) -> dict | None: """Live BLUE exit hook backed by AlphaExitEngineV7. The orchestrator calls this before falling back to the base exit manager. Returns a V7 decision dict or None if the trade cannot yet be evaluated. """ if self._v7_exit_engine is None or pos is None: return None trade_id = str(getattr(pos, "trade_id", "") or "") asset = str(getattr(pos, "asset", "") or "") if not trade_id or not asset: return None pending = self._pending_entries.get(trade_id, {}) ctx_v7 = self._v7_contexts.get(trade_id) eval_bar = max(0, int(bar_idx) - 1) if ctx_v7 is None: try: ctx_v7 = self._v7_exit_engine.make_context( entry_price=float( pending.get("entry_price", getattr(pos, "entry_price", 0.0)) or getattr(pos, "entry_price", 0.0) or 0.0 ), entry_bar=int(pending.get("entry_bar", eval_bar) or eval_bar), side=1 if str(pending.get("side", "SHORT") or "SHORT") == "SHORT" else 0, ) if self._last_exf: ctx_v7.set_exf( funding=float(self._last_exf.get("funding", 0.0) or 0.0), dvol=float(self._last_exf.get("dvol", 0.0) or 0.0), fear_greed=float(self._last_exf.get("fear_greed", 0.0) or 0.0), taker=float(self._last_exf.get("taker", 0.0) or 0.0), ) self._v7_contexts[trade_id] = ctx_v7 self._v7_decision_seq.setdefault(trade_id, 0) except Exception as exc: log(f" V7 live context init failed for {trade_id}: {exc}") return None elif self._last_exf: try: ctx_v7.set_exf( funding=float(self._last_exf.get("funding", 0.0) or 0.0), dvol=float(self._last_exf.get("dvol", 0.0) or 0.0), fear_greed=float(self._last_exf.get("fear_greed", 0.0) or 0.0), taker=float(self._last_exf.get("taker", 0.0) or 0.0), ) except Exception: pass ob_imb = 0.0 if self.ob_eng is not None: try: ob_sig = self.ob_eng.get_signal(asset, float(eval_bar)) ob_imb = float(getattr(ob_sig, "imbalance_ma5", 0.0) or 0.0) except Exception as exc: log(f" V7 live OB signal failed for {trade_id}: {exc}") cur_px = float( prices.get(asset, getattr(pos, "current_price", 0.0)) or getattr(pos, "current_price", 0.0) or 0.0 ) if cur_px <= 0.0: return None decision = self._v7_exit_engine.evaluate( ctx_v7, cur_px, eval_bar, ob_imb, asset=asset, ) self._v7_decisions[trade_id] = decision self._record_v7_decision( trade_id=trade_id, asset=asset, side=str(pending.get("side", "SHORT") or "SHORT"), decision=decision, current_price=cur_px, ob_imbalance=ob_imb, vel_div_now=vel_div, v50_vel=v50_vel, v750_vel=v750_vel, source="live_exit", bar_idx=eval_bar, ) action = str(decision.get("action", "HOLD") or "HOLD") if action != "HOLD": log( " V7 live decision: " f"{trade_id} {asset} action={action} reason={decision.get('reason', '')} " f"pressure={float(decision.get('exit_pressure', 0.0) or 0.0):+.3f} " f"pnl_pct={float(decision.get('pnl_pct', 0.0) or 0.0):+.3f}" ) return decision def _sync_sc_gauge_advisor(self, scan_number: int, vel_div: float) -> None: """Shadow-only bucket gauge advisory surface.""" if self._sc_gauge is None: return try: payload = self._read_esof_payload() trade_history = getattr(self.eng, "trade_history", []) open_tid = next(iter(self._pending_entries.keys()), "") pending = self._pending_entries.get(open_tid, {}) if open_tid else {} asset = str(pending.get("asset", "")) rec = self._sc_gauge.evaluate( trade_id=str(open_tid or ""), asset=asset, sc=_safe_float(payload.get("advisory_score", payload.get("score", 0.0)) if payload else None), vel_div=float(vel_div or 0.0), exf_snapshot=getattr(self, "_last_exf", {}) or {}, obf_snapshot=self._current_obf_snapshot(asset, self.bar_idx), trade_history=trade_history, current_mult=float(self._last_esof_size_mult or 1.0), esof_payload=payload, scan_number=int(scan_number or 0), bar_idx=int(self.bar_idx), strategy="blue", log_shadow=True, ) if open_tid: pending["sc_bucket_gauge"] = rec pending["sc_bucket_gauge_exec_mult"] = float(self._last_esof_size_mult or 1.0) self._pending_entries[open_tid] = pending now = time.time() if now - self._sc_gauge_last_log >= 300: self._sc_gauge_last_log = now log( f"SC_GAUGE: sc={rec['sc']:+.3f} bucket={rec['bucket_id']} " f"cur={rec['current_mult']:.2f} rec={rec['recommended_size_mult']:.2f} " f"tp={rec['recommended_tp_mult']:.2f} hold={rec['recommended_hold_mult']:.2f} " f"cut={rec['recommended_sc_cut']:+.2f} conf={rec['confidence']:.2f}" ) except Exception as e: log(f"SC_GAUGE error: {e}") # ── CH position-state persistence ───────────────────────────────────────── def _ps_write_open(self, tid: str, entry: dict): """Persist OPEN row to position_state on entry. Fire-and-forget via ch_put.""" try: ch_put("position_state", { "ts": entry['entry_ts'], "trade_id": tid, "asset": entry['asset'], "direction": -1 if entry['side'] == 'SHORT' else 1, "entry_price": entry['entry_price'], "quantity": entry['quantity'], "notional": round(entry['quantity'] * entry['entry_price'], 4), "leverage": entry['leverage'], "bucket_id": int(self._bucket_assignments.get(entry['asset'], -1)), "entry_bar": self.bar_idx, "status": "OPEN", "exit_reason": "", "pnl": 0.0, "bars_held": 0, }) except Exception as e: log(f" position_state OPEN write failed: {e}") def _ps_write_closed(self, tid: str, pending: dict, x: dict): """Persist CLOSED row to position_state on exit (supersedes OPEN row via ReplacingMergeTree).""" try: ch_put("position_state", { "ts": _ch_ts_us(), "trade_id": tid, "asset": pending.get('asset', ''), "direction": -1 if pending.get('side') == 'SHORT' else 1, "entry_price": pending.get('entry_price', 0.0), "quantity": pending.get('quantity', 0.0), "notional": round(pending.get('quantity', 0.0) * pending.get('entry_price', 0.0), 4), "leverage": pending.get('leverage', 0.0), "bucket_id": int(self._bucket_assignments.get(pending.get('asset', ''), -1)), "entry_bar": 0, "status": "CLOSED", "exit_reason": str(x.get('reason', 'UNKNOWN')), "pnl": float(x.get('net_pnl', 0) or 0), "bars_held": int(x.get('bars_held', 0) or 0), }) except Exception as e: log(f" position_state CLOSED write failed: {e}") def _restore_position_state(self): """On startup: check CH for an OPEN position and restore engine state.""" try: import urllib.request, base64 as _b64 sql = ("SELECT trade_id, asset, direction, entry_price, quantity, " "notional, leverage, bucket_id, bars_held " "FROM dolphin.position_state FINAL " "WHERE status = 'OPEN' " "ORDER BY ts DESC LIMIT 1 FORMAT TabSeparated") req = urllib.request.Request( "http://localhost:8123/?database=dolphin", data=sql.encode(), headers={"Authorization": "Basic " + _b64.b64encode(b"dolphin:dolphin_ch_2026").decode()}) with urllib.request.urlopen(req, timeout=5) as r: row = r.read().decode().strip() if not row: log(" position_state: no open position to restore") return cols = row.split('\t') if len(cols) < 9: log(f" position_state: unexpected row format: {row}") return trade_id = cols[0] asset = cols[1] direction = int(cols[2]) entry_price = float(cols[3]) quantity = float(cols[4]) notional = float(cols[5]) leverage = float(cols[6]) bucket_id = int(cols[7]) stored_bars = int(cols[8]) # Estimate entry_bar so MAX_HOLD countdown continues from where it left off restored_entry_bar = max(0, self.bar_idx - stored_bars) pos = NDPosition( trade_id = trade_id, asset = asset, direction = direction, entry_price = entry_price, entry_bar = restored_entry_bar, notional = notional, leverage = leverage, fraction = notional / max(self.eng.capital * leverage, 1.0), entry_vel_div = 0.0, bucket_idx = 0, # signal-strength bucket (not KMeans); 0=safe default current_price = entry_price, ) with self.eng_lock: self.eng.position = pos self.eng.exit_manager.setup_position( trade_id, entry_price, direction, restored_entry_bar, ) # NOTE: do NOT arm hibernate protect here. # _day_posture starts as 'APEX' — the posture sync block on the # first incoming scan will detect the APEX→HIBERNATE transition # and call _hibernate_protect_position() at the right moment. # Rebuild _pending_entries so the exit CH write fires correctly side = 'SHORT' if direction == -1 else 'LONG' self._pending_entries[trade_id] = { 'asset': asset, 'side': side, 'entry_price': entry_price, 'quantity': quantity, 'leverage': leverage, 'vel_div_entry': 0.0, 'boost_at_entry': 1.0, 'beta_at_entry': 1.0, 'posture': 'RESTORED', 'entry_ts': _ch_ts_us(), 'entry_date': (self.current_day or ''), } if self._v7_exit_engine is not None: try: ctx = self._v7_exit_engine.make_context( entry_price=entry_price, entry_bar=restored_entry_bar, side=1 if direction == -1 else 0, ) self._v7_contexts[trade_id] = ctx self._v7_decision_seq[trade_id] = 0 except Exception as e: log(f" V7 live restore context failed: {e}") log(f" position_state RESTORED: {asset} {side} entry={entry_price} " f"notional={notional:.0f} bars_held≈{stored_bars} trade={trade_id}") except Exception as e: log(f" position_state restore error: {e}") def _hibernate_protect_position(self): """Arm per-bucket TP+SL instead of immediate HIBERNATE_HALT. Must be called under eng_lock with an open position. Sets stop_pct_override on the live exit_manager state so the position exits via FIXED_TP or STOP_LOSS rather than being force-closed. Records trade_id in _hibernate_protect_active so the exit path can re-label the reason and finalize posture once the position closes. """ pos = self.eng.position if pos is None: return bucket = self._bucket_assignments.get(pos.asset, 'default') sl_pct = _BUCKET_SL_PCT.get(bucket, _BUCKET_SL_PCT['default']) tp_pct = self.eng.exit_manager.fixed_tp_pct # Patch the live exit_manager state for this trade_id em_state = self.eng.exit_manager._positions.get(pos.trade_id) if em_state is not None: em_state['stop_pct_override'] = sl_pct else: # Position not registered in exit_manager (shouldn't happen, but be safe) log(f" HIBERNATE_PROTECT: trade {pos.trade_id} not in exit_manager — arming anyway via re-setup") self.eng.exit_manager.setup_position( pos.trade_id, pos.entry_price, pos.direction, pos.entry_bar, stop_pct_override=sl_pct, ) self._hibernate_protect_active = pos.trade_id log(f"HIBERNATE_PROTECT armed: {pos.asset} B{bucket} " f"SL={sl_pct*100:.2f}% TP={tp_pct*100:.2f}% trade={pos.trade_id}") def _connect_hz(self): log("Connecting to Hazelcast...") import hazelcast self.hz_client = hazelcast.HazelcastClient(cluster_name=HZ_CLUSTER, cluster_members=[HZ_HOST]) self.features_map = self.hz_client.get_map("DOLPHIN_FEATURES") self.safety_map = self.hz_client.get_map("DOLPHIN_SAFETY") self.pnl_map = self.hz_client.get_map("DOLPHIN_PNL_BLUE") self.state_map = self.hz_client.get_map("DOLPHIN_STATE_BLUE") self.heartbeat_map = self.hz_client.get_map("DOLPHIN_HEARTBEAT") # Immediate heartbeat — prevents Cat1=0 during startup gap try: self.heartbeat_map.blocking().put('nautilus_flow_heartbeat', json.dumps({ 'ts': time.time(), 'iso': datetime.now(timezone.utc).isoformat(), 'phase': 'starting', 'flow': 'nautilus_event_trader', })) except Exception: pass log(" Hz connected") def _read_posture(self): now = time.time() if now - self.posture_cache_time < 10: return self.cached_posture try: posture_raw = self.safety_map.blocking().get("latest") or self.safety_map.blocking().get("posture") if posture_raw: if isinstance(posture_raw, str): try: parsed = json.loads(posture_raw) self.cached_posture = parsed.get("posture", posture_raw) except (json.JSONDecodeError, AttributeError): self.cached_posture = posture_raw else: self.cached_posture = posture_raw.get("posture", "APEX") self.posture_cache_time = now except: pass return self.cached_posture def _rollover_day(self): today = datetime.now(timezone.utc).strftime('%Y-%m-%d') if today == self.current_day: return posture = self._read_posture() with self.eng_lock: if today != self.current_day: # double-checked: only one thread calls begin_day if getattr(self, 'acb', None): try: exf_raw = self.features_map.blocking().get('exf_latest') if self.features_map else None es_raw = self.features_map.blocking().get('latest_eigen_scan') if self.features_map else None exf_snapshot = json.loads(exf_raw) if isinstance(exf_raw, str) else (exf_raw or {}) eigen_scan = json.loads(es_raw) if isinstance(es_raw, str) else (es_raw or {}) w750_vel = eigen_scan.get('w750_velocity', 0.0) if exf_snapshot: self.acb.get_dynamic_boost_from_hz( date_str=today, exf_snapshot=exf_snapshot, w750_velocity=float(w750_vel) if w750_vel else None, direction=self.trade_direction, ) log(f"ACB: Pre-warmed cache for {today} from HZ") except Exception as e: log(f"ACB Rollover Error: {e}") self.eng.begin_day(today, posture=posture, direction=self.trade_direction) self.bar_idx = 0 self.current_day = today log( f"begin_day({today}) called with posture={posture} " f"direction={_direction_label(self.trade_direction)}" ) def _compute_vol_ok(self, scan): assets = scan.get('assets', []) prices = scan.get('asset_prices', []) if not assets or not prices: return True prices_dict = dict(zip(assets, prices)) btc_price = prices_dict.get('BTCUSDT') if btc_price is None: return True self.btc_prices.append(float(btc_price)) if len(self.btc_prices) < BTC_VOL_WINDOW: return True import numpy as np arr = np.array(self.btc_prices) dvol = float(np.std(np.diff(arr) / arr[:-1])) return dvol > VOL_P60_THRESHOLD @staticmethod def _normalize_ng7(scan: dict) -> dict: """ Promote NG7-format scan to NG5-compatible flat dict. NG7 embeds eigenvalue windows and prices inside result{} — the engine expects flat top-level fields. Mapping derived from continuous_convert.py: vel_div = w50_velocity − w750_velocity (fast minus slow eigenvalue velocity) w50_velocity = multi_window_results["50"].tracking_data.lambda_max_velocity w750_velocity = multi_window_results["750"].tracking_data.lambda_max_velocity assets = sorted(current_prices.keys()), BTCUSDT always last """ result = scan.get('result') or {} mw = result.get('multi_window_results') or {} def _vel(win): v = (mw.get(str(win)) or {}).get('tracking_data', {}).get('lambda_max_velocity') try: f = float(v) return f if math.isfinite(f) else 0.0 except (TypeError, ValueError): return 0.0 v50 = _vel(50) v150 = _vel(150) v750 = _vel(750) cp = (result.get('pricing_data') or {}).get('current_prices') or {} assets = [a for a in cp if a != 'BTCUSDT'] if 'BTCUSDT' in cp: assets.append('BTCUSDT') # BTC always last — matches NG5/Arrow convention prices = [float(cp[a]) for a in assets] instability = float((result.get('regime_prediction') or {}) .get('instability_score') or 0.0) return { **scan, 'vel_div': v50 - v750, 'w50_velocity': v50, 'w750_velocity': v750, 'assets': assets, 'asset_prices': prices, 'instability_50': instability, } def on_scan(self, event): """Reactor-thread entry point — dispatches immediately to worker thread.""" if not event.value: return listener_time = time.time() self._scan_executor.submit(self._process_scan, event, listener_time) def _process_scan(self, event, listener_time): try: if not event.value: return scan = json.loads(event.value) if isinstance(event.value, str) else event.value # Normalise NG7 format → NG5-compatible flat dict before any field access if scan.get('version') == 'NG7': scan = self._normalize_ng7(scan) scan_number = int(scan.get('scan_number') or 0) # Dedup: scan_number is authoritative (monotonically increasing). # file_mtime / timestamp are unreliable across NG7 restart probes. with self._dedup_lock: if scan_number > 0 and scan_number <= self.last_scan_number: return self.last_scan_number = scan_number self.scans_processed += 1 self._rollover_day() assets = scan.get('assets') or [] if assets and not self.ob_assets: self._wire_obf(assets) prices = scan.get('asset_prices') or [] if assets and prices and len(assets) != len(prices): log(f"WARN scan #{scan_number}: assets/prices mismatch " f"({len(assets)}≠{len(prices)}) — dropped") return prices_dict = dict(zip(assets, prices)) if assets and prices else {} # Remove stablecoins — they should never be selected as a trade asset for sym in _STABLECOIN_SYMBOLS: prices_dict.pop(sym, None) self._record_bounce_prices(prices_dict) vol_ok = self._compute_vol_ok(scan) vel_div = float(scan.get('vel_div') or 0.0) if not math.isfinite(vel_div): log(f"WARN scan #{scan_number}: non-finite vel_div={vel_div} — clamped to 0.0") vel_div = 0.0 v50_vel = float(scan.get('w50_velocity') or 0.0) v750_vel = float(scan.get('w750_velocity') or 0.0) if not math.isfinite(v50_vel): v50_vel = 0.0 if not math.isfinite(v750_vel): v750_vel = 0.0 self.last_w750_vel = v750_vel # Feed live OB data into OBF engine for this bar (AGENT_SPEC_OBF_LIVE_SWITCHOVER) if self.ob_eng is not None and self.ob_assets: self.ob_eng.step_live(self.ob_assets, self.bar_idx) # Live posture sync — update engine posture + regime_dd_halt together posture_now = self._read_posture() with self.eng_lock: prev_posture = getattr(self.eng, '_day_posture', 'APEX') if posture_now != prev_posture: if posture_now in ('TURTLE', 'HIBERNATE'): self.eng.regime_dd_halt = True # always block new entries if (posture_now == 'HIBERNATE' and self.eng.position is not None and not self._hibernate_protect_active): # Position in flight: arm TP+SL instead of letting # _manage_position() fire HIBERNATE_HALT next bar. # _day_posture stays at prev value — no HALT fires. self._hibernate_protect_position() else: self.eng._day_posture = posture_now log(f"POSTURE_SYNC: {posture_now} — halt set") else: self.eng._day_posture = posture_now self.eng.regime_dd_halt = False if self._hibernate_protect_active: log(f"POSTURE_SYNC: {posture_now} — posture recovered, clearing protect mode") self._hibernate_protect_active = None else: log(f"POSTURE_SYNC: {posture_now} — halt lifted") # EsoF value gate — exposure only, no alpha or selection changes. self._sync_esof_size_gate() self._sync_sc_threshold_advisor(scan_number=scan_number, vel_div=vel_div) self._sync_sc_gauge_advisor(scan_number=scan_number, vel_div=vel_div) if self._market_state_runtime is not None: try: self._market_state_runtime.update_scan_state( scan_payload=scan, prices_dict=prices_dict, scan_number=scan_number, vel_div=vel_div, v50_vel=v50_vel, v750_vel=v750_vel, vol_ok=vol_ok, posture=posture_now, exf_snapshot=getattr(self, "_last_exf", {}) or {}, esof_payload=self._read_esof_payload(), top_k_assets=5, ) except Exception as e: log(f" MarketStateRuntime scan update failed: {e}") step_start = time.time() with self.eng_lock: result = self.eng.step_bar( bar_idx=self.bar_idx, vel_div=vel_div, prices=prices_dict, vol_regime_ok=vol_ok, v50_vel=v50_vel, v750_vel=v750_vel ) self.bar_idx += 1 scan_to_fill_ms = (time.time() - listener_time) * 1000 step_bar_ms = (time.time() - step_start) * 1000 log(f"LATENCY scan #{scan_number}: scan→fill={scan_to_fill_ms:.1f}ms step_bar={step_bar_ms:.1f}ms vel_div={vel_div:.5f}") ch_put("eigen_scans", { "ts": _ch_ts_us(), "scan_number": scan_number, "scan_uuid": str(scan.get("scan_uuid") or ""), "vel_div": vel_div, "w50_velocity": v50_vel, "w750_velocity": v750_vel, "instability_50": float(scan.get("instability_50") or 0.0), "scan_to_fill_ms": scan_to_fill_ms, "step_bar_ms": step_bar_ms, }) if result.get('entry'): self.trades_executed += 1 e = result['entry'] log(f"ENTRY: {e} [{ALGO_VERSION}]") # Cache entry fields for CH trade_events on exit tid = e.get('trade_id') if tid: self._pending_entries[tid] = { 'asset': e.get('asset', ''), 'side': 'SHORT' if e.get('direction', -1) == -1 else 'LONG', 'entry_price': float(e.get('entry_price', 0) or 0), 'quantity': round(float(e.get('notional', 0) or 0) / float(e.get('entry_price', 1) or 1), 6), 'leverage': float(e.get('leverage', 0) or 0), 'vel_div_entry': float(e.get('vel_div', 0) or 0), 'boost_at_entry': float(getattr(getattr(self, 'eng', None), 'acb_boost', 1.0) or 1.0), 'beta_at_entry': float(getattr(getattr(self, 'eng', None), 'acb_beta', 1.0) or 1.0), 'posture': posture_now, 'entry_ts': _ch_ts_us(), 'entry_date': (self.current_day or ''), 'entry_bar': self.bar_idx, } # Persist position to CH so restarts can recover it self._ps_write_open(tid, self._pending_entries[tid]) self._announce_position_event( kind="trade_entry", severity="info", title=f"[BLUE] ENTRY {e.get('asset', '')} {self._pending_entries[tid]['side']}", message=( f"entry={float(e.get('entry_price', 0) or 0):.6f} " f"qty={self._pending_entries[tid]['quantity']:.6f} " f"lev={self._pending_entries[tid]['leverage']:.2f}x" ), metadata={ "trade_id": tid, "asset": self._pending_entries[tid]["asset"], "side": self._pending_entries[tid]["side"], "entry_price": self._pending_entries[tid]["entry_price"], "quantity": self._pending_entries[tid]["quantity"], "leverage": self._pending_entries[tid]["leverage"], "vel_div_entry": self._pending_entries[tid]["vel_div_entry"], "boost_at_entry": self._pending_entries[tid]["boost_at_entry"], "beta_at_entry": self._pending_entries[tid]["beta_at_entry"], "posture": self._pending_entries[tid]["posture"], "entry_ts": self._pending_entries[tid]["entry_ts"], }, ) if self._v7_exit_engine is not None: try: side = 1 if e.get('direction', -1) == -1 else 0 ctx = self._v7_exit_engine.make_context( entry_price=float(e.get('entry_price', 0) or 0), entry_bar=max(0, self.bar_idx - 1), side=side, ) if self._last_exf: ctx.set_exf( funding=float(self._last_exf.get('funding', 0.0) or 0.0), dvol=float(self._last_exf.get('dvol', 0.0) or 0.0), fear_greed=float(self._last_exf.get('fear_greed', 0.0) or 0.0), taker=float(self._last_exf.get('taker', 0.0) or 0.0), ) self._v7_contexts[tid] = ctx self._v7_decisions.pop(tid, None) self._v7_decision_seq[tid] = 0 except Exception as e: log(f" V7 live context init failed for {tid}: {e}") # Shadow AE: notify of entry (vel_div at entry bar is in scope) if self._ae is not None: try: self._ae.on_entry( trade_id=tid, asset=e.get('asset', ''), direction=int(e.get('direction', -1)), entry_price=float(e.get('entry_price', 0) or 0), vel_div_entry=vel_div, ) except Exception: pass if self._sc_advisor is not None: try: payload = self._read_esof_payload() rec = self._sc_advisor.evaluate( trade_id=tid, asset=e.get('asset', ''), sc=_safe_float(payload.get('advisory_score', payload.get('score', 0.0)) if payload else None), vel_div=vel_div, exf_snapshot=getattr(self, "_last_exf", {}) or {}, trade_history=getattr(self.eng, 'trade_history', []), current_mult=float(self._last_esof_size_mult or 1.0), esof_payload=payload, scan_number=scan_number, bar_idx=self.bar_idx, strategy="blue", log_shadow=True, ) self._pending_entries[tid]['sc_threshold_advisor'] = rec self._pending_entries[tid]['sc_exec_mult'] = float(self._last_esof_size_mult or 1.0) except Exception: pass if self._sc_gauge is not None: try: payload = self._read_esof_payload() rec = self._sc_gauge.evaluate( trade_id=tid, asset=e.get('asset', ''), sc=_safe_float(payload.get('advisory_score', payload.get('score', 0.0)) if payload else None), vel_div=vel_div, exf_snapshot=getattr(self, "_last_exf", {}) or {}, obf_snapshot=self._current_obf_snapshot(e.get('asset', ''), self.bar_idx), trade_history=getattr(self.eng, 'trade_history', []), current_mult=float(self._last_esof_size_mult or 1.0), esof_payload=payload, scan_number=scan_number, bar_idx=self.bar_idx, strategy="blue", log_shadow=True, ) self._pending_entries[tid]['sc_bucket_gauge'] = rec self._pending_entries[tid]['sc_bucket_gauge_exec_mult'] = float(self._last_esof_size_mult or 1.0) except Exception: pass if self._bounce_advisor is not None: try: entry_ts_val = float(self._pending_entries[tid].get('entry_ts', 0) or 0) entry_ts_dt = datetime.fromtimestamp(entry_ts_val / 1_000_000, tz=timezone.utc) if entry_ts_val else None bounce_rec = self._bounce_eval( trade_id=tid, asset=str(e.get('asset', '')), side=self._pending_entries[tid]['side'], source="entry", scan_number=scan_number, entry_ts=entry_ts_dt, current_price=float(prices_dict.get(e.get('asset', ''), e.get('entry_price', 0)) or e.get('entry_price', 0) or 0), entry_price=float(e.get('entry_price', 0) or 0), quantity=float(self._pending_entries[tid].get('quantity', 0) or 0), notional=float(e.get('notional', 0) or 0), leverage=float(e.get('leverage', 0) or 0), vel_div=vel_div, current_mult=float(self._last_esof_size_mult or 1.0), bars_held=0, log_shadow=True, ) if bounce_rec: self._pending_entries[tid]['bounce_advisor_entry'] = bounce_rec self._pending_entries[tid]['bounce_advisor_latest'] = bounce_rec except Exception as e: log(f" BounceAdvisor entry eval failed for {tid}: {e}") if (self._v7_exit_engine is not None and self.eng is not None and getattr(self.eng, 'position', None) is not None and not self._v7_live_exit_enabled): pos = self.eng.position tid_v7 = getattr(pos, 'trade_id', '') pending_v7 = self._pending_entries.get(tid_v7, {}) ctx_v7 = self._v7_contexts.get(tid_v7) if ctx_v7 is None and pending_v7: try: ctx_v7 = self._v7_exit_engine.make_context( entry_price=float(pending_v7.get('entry_price', pos.entry_price) or pos.entry_price or 0.0), entry_bar=int(pending_v7.get('entry_bar', max(0, self.bar_idx - 1)) or max(0, self.bar_idx - 1)), side=1 if pending_v7.get('side', 'SHORT') == 'SHORT' else 0, ) if self._last_exf: ctx_v7.set_exf( funding=float(self._last_exf.get('funding', 0.0) or 0.0), dvol=float(self._last_exf.get('dvol', 0.0) or 0.0), fear_greed=float(self._last_exf.get('fear_greed', 0.0) or 0.0), taker=float(self._last_exf.get('taker', 0.0) or 0.0), ) self._v7_contexts[tid_v7] = ctx_v7 self._v7_decision_seq.setdefault(tid_v7, 0) except Exception as e: log(f" V7 live context restore failed for {tid_v7}: {e}") ctx_v7 = None if ctx_v7 is not None and pending_v7: try: if self.ob_eng is not None: ob_sig = self.ob_eng.get_signal(pos.asset, float(max(0, self.bar_idx - 1))) ob_imb = float(getattr(ob_sig, 'imbalance_ma5', 0.0) or 0.0) else: ob_imb = 0.0 cur_px = float(prices_dict.get(pos.asset, pos.current_price) or pos.current_price or 0.0) if cur_px > 0.0: v7dec = self._v7_exit_engine.evaluate( ctx_v7, cur_px, max(0, self.bar_idx - 1), ob_imb, asset=pos.asset, ) self._v7_decisions[tid_v7] = v7dec self._record_v7_decision( trade_id=tid_v7, asset=pos.asset, side=pending_v7.get('side', 'SHORT'), decision=v7dec, current_price=cur_px, ob_imbalance=ob_imb, vel_div_now=vel_div, v50_vel=v50_vel, v750_vel=v750_vel, bar_idx=max(0, self.bar_idx - 1), ) if self._bounce_advisor is not None: try: entry_ts_val = float(pending_v7.get('entry_ts', 0) or 0) entry_ts_dt = datetime.fromtimestamp(entry_ts_val / 1_000_000, tz=timezone.utc) if entry_ts_val else None bounce_rec = self._bounce_eval( trade_id=tid_v7, asset=pos.asset, side=pending_v7.get('side', 'SHORT'), source="open_scan", scan_number=scan_number, entry_ts=entry_ts_dt, current_price=cur_px, entry_price=float(pending_v7.get('entry_price', pos.entry_price) or pos.entry_price or 0.0), quantity=float(pending_v7.get('quantity', getattr(pos, 'quantity', 0.0)) or getattr(pos, 'quantity', 0.0) or 0.0), notional=float(pending_v7.get('notional', getattr(pos, 'notional', 0.0)) or getattr(pos, 'notional', 0.0) or 0.0), leverage=float(pending_v7.get('leverage', getattr(pos, 'leverage', 0.0)) or getattr(pos, 'leverage', 0.0) or 0.0), vel_div=vel_div, current_mult=float(self._last_esof_size_mult or 1.0), bars_held=max(0, int(self.bar_idx - int(pending_v7.get('entry_bar', max(0, self.bar_idx - 1)) or max(0, self.bar_idx - 1)))), log_shadow=True, ) if bounce_rec: pending_v7['bounce_advisor_latest'] = bounce_rec self._pending_entries[tid_v7] = pending_v7 except Exception as e: log(f" BounceAdvisor open-scan eval failed for {tid_v7}: {e}") except Exception as e: log(f" V7 live evaluate failed for {tid_v7}: {e}") if result.get('exit'): x = result['exit'] tid = x.get('trade_id') # Hibernate-protected exits: re-label reason, finalize posture if tid and self._hibernate_protect_active == tid: _orig = x.get('reason', '') _map = {'FIXED_TP': 'HIBERNATE_TP', 'STOP_LOSS': 'HIBERNATE_SL', 'MAX_HOLD': 'HIBERNATE_MAXHOLD'} x['reason'] = _map.get(_orig, f'HIBERNATE_{_orig}') self._hibernate_protect_active = None # Position closed — now safe to commit posture to HIBERNATE _cur_posture = self._read_posture() if _cur_posture == 'HIBERNATE': self.eng._day_posture = 'HIBERNATE' log(f"HIBERNATE_PROTECT: closed via {x['reason']} — posture finalized HIBERNATE") else: log(f"HIBERNATE_PROTECT: closed via {x['reason']} — posture recovered to {_cur_posture}") log(f"EXIT: {x} [{ALGO_VERSION}]") tid = x.get('trade_id') pending = self._pending_entries.pop(tid, {}) if tid else {} if tid: self._v7_contexts.pop(tid, None) self._v7_decisions.pop(tid, None) self._v7_decision_seq.pop(tid, None) if pending: # exact bar price the engine exited against — prices_dict is still in scope exit_price = float(prices_dict.get(pending['asset'], 0) or 0) if self._sc_advisor is not None: try: _rec = pending.get('sc_threshold_advisor') if _rec: self._sc_advisor.observe_outcome( _rec, executed_mult=float(pending.get('sc_exec_mult', self._last_esof_size_mult) or 1.0), pnl_pct=float(x.get('pnl_pct', 0) or 0), exit_reason=str(x.get('reason', 'UNKNOWN')), ) except Exception: pass if self._sc_gauge is not None: try: _rec = pending.get('sc_bucket_gauge') if _rec: self._sc_gauge.observe_outcome( _rec, executed_mult=float(pending.get('sc_bucket_gauge_exec_mult', self._last_esof_size_mult) or 1.0), pnl_pct=float(x.get('pnl_pct', 0) or 0), exit_reason=str(x.get('reason', 'UNKNOWN')), ) except Exception: pass if self._bounce_advisor is not None: try: _bounce_rec = pending.get('bounce_advisor_entry') if _bounce_rec: self._bounce_advisor.observe_outcome( _bounce_rec, pnl_pct=float(x.get('pnl_pct', 0) or 0), exit_reason=str(x.get('reason', 'UNKNOWN')), ) except Exception as e: log(f" BounceAdvisor outcome update failed for {tid}: {e}") if self._market_state_runtime is not None: try: self._market_state_runtime.online_update_from_trade( asset=str(pending.get("asset", "")), entry_price=float(pending.get("entry_price", 0) or 0), exit_price=float(exit_price), direction=-1 if str(pending.get("side", "SHORT")).upper() == "SHORT" else 1, pnl_pct=float(x.get("pnl_pct", 0) or 0), bars_held=int(x.get("bars_held", 0) or 0), exit_reason=str(x.get("reason", "UNKNOWN")), trade_id=str(tid or ""), leverage=float(pending.get("leverage", 1.0) or 1.0), ) except Exception as e: log(f" MarketStateRuntime outcome update failed for {tid}: {e}") ch_put("trade_events", { "ts": _ch_ts_us(), "date": pending['entry_date'], "strategy": "blue", "asset": pending['asset'], "side": pending['side'], "entry_price": pending['entry_price'], "exit_price": exit_price, "quantity": pending['quantity'], "pnl": float(x.get('net_pnl', 0) or 0), "pnl_pct": float(x.get('pnl_pct', 0) or 0), "exit_reason": str(x.get('reason', 'UNKNOWN')), "vel_div_entry": pending['vel_div_entry'], "boost_at_entry": pending['boost_at_entry'], "beta_at_entry": pending['beta_at_entry'], "posture": pending['posture'], "leverage": pending['leverage'], "bars_held": int(x.get('bars_held', 0) or 0), "regime_signal": 0, }) # Mark position closed in CH (supersedes OPEN row via ReplacingMergeTree) self._ps_write_closed(tid, pending, x) self._announce_position_event( kind="trade_exit", severity="info" if float(x.get("pnl_pct", 0) or 0) >= 0 else "warning", title=f"[BLUE] EXIT {pending.get('asset', '')} {pending.get('side', '')}", message=( f"reason={x.get('reason', 'UNKNOWN')} " f"pnl={float(x.get('net_pnl', 0) or 0):+.2f} " f"pnl_pct={float(x.get('pnl_pct', 0) or 0):+.3%}" ), metadata={ "trade_id": tid, "asset": pending.get("asset", ""), "side": pending.get("side", ""), "entry_price": pending.get("entry_price", 0), "exit_price": exit_price, "quantity": pending.get("quantity", 0), "pnl": float(x.get("net_pnl", 0) or 0), "pnl_pct": float(x.get("pnl_pct", 0) or 0), "exit_reason": str(x.get("reason", "UNKNOWN")), "bars_held": int(x.get("bars_held", 0) or 0), "posture": pending.get("posture", ""), }, ) # Shadow AE: record outcome for online update if self._ae is not None and tid: try: self._ae.on_exit( trade_id=tid, actual_exit_reason=str(x.get('reason', 'UNKNOWN')), pnl_pct=float(x.get('pnl_pct', 0) or 0), ) except Exception: pass # Shadow AE: per-bar evaluate for all open trades — daemon thread, zero hot-path impact if self._ae is not None and self._pending_entries: _ae_ref = self._ae _pending_snap = dict(self._pending_entries) # shallow copy under GIL _prices_snap = dict(prices_dict) _vel_now = vel_div _bar = self.bar_idx def _ae_eval(): for _tid, _p in _pending_snap.items(): try: _cur = _prices_snap.get(_p['asset'], 0) or 0 if not _cur: continue _entry_px = float(_p.get('entry_price', 0) or 0) _bars_held = max(0, int(_bar - int(_p.get('entry_bar', _bar)))) _shadow_pnl_pct = ((_entry_px - _cur) / _entry_px) if _entry_px > 0 else 0.0 _shadow = _ae_ref.evaluate( trade_id=_tid, asset=_p['asset'], direction=-1, entry_price=_entry_px, current_price=_cur, bars_held=_bars_held, vel_div_now=_vel_now, ) _ae_ref.log_shadow(_shadow, pnl_pct=_shadow_pnl_pct) except Exception: pass threading.Thread(target=_ae_eval, daemon=True).start() self._push_state(scan_number, vel_div, vol_ok, self._read_posture()) except Exception as e: log(f"ERROR in _process_scan: {e}") def on_exf_update(self, event): if not event.value: return snapshot = json.loads(event.value) if isinstance(event.value, str) else event.value if not self.current_day or not self.acb: return try: self._last_exf = { 'funding': float(snapshot.get('funding_btc', 0.0)), 'dvol': float(snapshot.get('dvol_btc', 50.0)), 'fear_greed': float(snapshot.get('fng', 50.0)), 'taker': float(snapshot.get('taker', 0.5)), } w750_vel = getattr(self, 'last_w750_vel', None) acb_info = self.acb.get_dynamic_boost_from_hz( date_str=self.current_day, exf_snapshot=snapshot, w750_velocity=float(w750_vel) if w750_vel else None, direction=self.trade_direction, ) with self.eng_lock: if hasattr(self.eng, 'update_acb_boost'): subday_exit = self.eng.update_acb_boost( boost=acb_info['boost'], beta=acb_info['beta'] ) if subday_exit is not None: log(f"SUBDAY_EXIT: {subday_exit} [{ALGO_VERSION}]") tid = subday_exit.get('trade_id') pending = {} if tid: pending = self._pending_entries.pop(tid, {}) if pending and self._sc_advisor is not None: try: _rec = pending.get('sc_threshold_advisor') if _rec: self._sc_advisor.observe_outcome( _rec, executed_mult=float(pending.get('sc_exec_mult', self._last_esof_size_mult) or 1.0), pnl_pct=float(subday_exit.get('pnl_pct', 0) or 0), exit_reason=str(subday_exit.get('reason', 'SUBDAY_ACB_NORMALIZATION')), ) except Exception: pass if pending and self._sc_gauge is not None: try: _rec_g = pending.get('sc_bucket_gauge') if _rec_g: self._sc_gauge.observe_outcome( _rec_g, executed_mult=float(pending.get('sc_bucket_gauge_exec_mult', self._last_esof_size_mult) or 1.0), pnl_pct=float(subday_exit.get('pnl_pct', 0) or 0), exit_reason=str(subday_exit.get('reason', 'SUBDAY_ACB_NORMALIZATION')), ) except Exception: pass if pending and self._bounce_advisor is not None: try: _bounce_rec = pending.get('bounce_advisor_entry') if _bounce_rec: self._bounce_advisor.observe_outcome( _bounce_rec, pnl_pct=float(subday_exit.get('pnl_pct', 0) or 0), exit_reason=str(subday_exit.get('reason', 'SUBDAY_ACB_NORMALIZATION')), ) except Exception as e: log(f" BounceAdvisor outcome update failed for {tid}: {e}") if self._market_state_runtime is not None: try: self._market_state_runtime.online_update_from_trade( asset=str(pending.get("asset", "")), entry_price=float(pending.get("entry_price", 0) or 0), exit_price=float(subday_exit.get("exit_price", 0) or 0), direction=-1 if str(pending.get("side", "SHORT")).upper() == "SHORT" else 1, pnl_pct=float(subday_exit.get("pnl_pct", 0) or 0), bars_held=int(subday_exit.get("bars_held", 0) or 0), exit_reason=str(subday_exit.get("reason", "SUBDAY_ACB_NORMALIZATION")), trade_id=str(tid or ""), leverage=float(pending.get("leverage", 1.0) or 1.0), ) except Exception as e: log(f" MarketStateRuntime outcome update failed for {tid}: {e}") ch_put("trade_events", { "ts": _ch_ts_us(), "date": self.current_day or '', "strategy": "blue", "asset": pending.get('asset', subday_exit.get('asset', '')), "side": pending.get('side', 'SHORT'), "entry_price": pending.get('entry_price', 0), "exit_price": float(subday_exit.get('exit_price', 0) or 0), "quantity": round(float(pending.get('notional', 0) or 0) / max(float(pending.get('entry_price', 1) or 1), 1e-12), 6), "pnl": float(subday_exit.get('net_pnl', 0) or 0), "pnl_pct": float(subday_exit.get('pnl_pct', 0) or 0), "exit_reason": str(subday_exit.get('reason', 'SUBDAY_ACB_NORMALIZATION')), "vel_div_entry": float(pending.get('vel_div_entry', 0) or 0), "boost_at_entry": float(pending.get('boost_at_entry', 0) or 0), "beta_at_entry": float(pending.get('beta_at_entry', 0) or 0), "posture": pending.get('posture', ''), "leverage": float(pending.get('leverage', 0) or 0), "bars_held": int(subday_exit.get('bars_held', 0) or 0), "regime_signal": 0, }) self._announce_position_event( kind="trade_exit", severity="info" if float(subday_exit.get("pnl_pct", 0) or 0) >= 0 else "warning", title=f"[BLUE] EXIT {pending.get('asset', '')} {pending.get('side', '')}", message=( f"reason={subday_exit.get('reason', 'SUBDAY_ACB_NORMALIZATION')} " f"pnl={float(subday_exit.get('net_pnl', 0) or 0):+.2f} " f"pnl_pct={float(subday_exit.get('pnl_pct', 0) or 0):+.3%}" ), metadata={ "trade_id": tid, "asset": pending.get("asset", subday_exit.get("asset", "")), "side": pending.get("side", "SHORT"), "entry_price": pending.get("entry_price", 0), "exit_price": float(subday_exit.get("exit_price", 0) or 0), "quantity": round(float(pending.get("notional", 0) or 0) / max(float(pending.get("entry_price", 1) or 1), 1e-12), 6), "pnl": float(subday_exit.get("net_pnl", 0) or 0), "pnl_pct": float(subday_exit.get("pnl_pct", 0) or 0), "exit_reason": str(subday_exit.get("reason", "SUBDAY_ACB_NORMALIZATION")), "bars_held": int(subday_exit.get("bars_held", 0) or 0), "posture": pending.get("posture", ""), }, ) now = time.time() if now - self._exf_log_time >= 300: self._exf_log_time = now log(f"ACB subday: boost={acb_info['boost']:.4f} beta={acb_info['beta']:.4f} " f"signals={acb_info['signals']:.1f} src={acb_info.get('source','?')}") # ACB_EXIT disabled: update_acb_boost() called to keep boost/beta current # (ACBv6 intact), but SUBDAY_ACB_NORMALIZATION exits are suppressed. except ValueError as e: log(f"ACB Stale Data Fallback: {e}") except Exception as e: log(f"on_exf_update Error: {e}") def _wire_obf(self, assets): if not assets or self.ob_assets: return self.ob_assets = assets from nautilus_dolphin.nautilus.hz_ob_provider import HZOBProvider live_ob = HZOBProvider( hz_cluster=HZ_CLUSTER, hz_host=HZ_HOST, assets=assets, ) self.ob_eng = OBFeatureEngine(live_ob) # No preload_date() call — live mode uses step_live() per scan self.eng.set_ob_engine(self.ob_eng) log(f" OBF wired: HZOBProvider, {len(assets)} assets (LIVE mode)") def _save_capital(self): """Persist capital to HZ (primary) and disk (fallback) so restarts survive HZ loss.""" capital = getattr(self.eng, 'capital', None) if capital is None or not math.isfinite(capital) or capital < 1.0: return payload = json.dumps({'capital': capital, 'ts': time.time()}) # Primary: Hazelcast try: self.state_map.blocking().put('capital_checkpoint', payload) except Exception as e: log(f" capital HZ save failed: {e}") # Secondary: local disk (survives HZ restart) try: CAPITAL_DISK_CHECKPOINT.write_text(payload) except Exception as e: log(f" capital disk save failed: {e}") def _restore_capital(self): """On startup, restore capital from HZ or disk checkpoint.""" def _try_load(raw, source): if not raw: return False try: data = json.loads(raw) saved = float(data.get('capital', 0)) age_h = (time.time() - data.get('ts', 0)) / 3600 if saved >= 1.0 and math.isfinite(saved) and age_h < 72: self.eng.capital = saved log(f" Capital restored from {source}: ${saved:,.2f} (age {age_h:.1f}h)") return True except Exception: pass return False # Primary: Hazelcast try: raw = self.state_map.blocking().get('capital_checkpoint') if _try_load(raw, 'HZ'): return except Exception as e: log(f" capital HZ restore failed: {e}") # Secondary: disk fallback try: if CAPITAL_DISK_CHECKPOINT.exists(): raw = CAPITAL_DISK_CHECKPOINT.read_text() if _try_load(raw, 'disk'): return except Exception as e: log(f" capital disk restore failed: {e}") log(" Capital: no valid checkpoint — starting at initial_capital") def _push_state(self, scan_number, vel_div, vol_ok, posture): try: with self.eng_lock: capital = getattr(self.eng, 'capital', 25000.0) # Engine uses a single NDPosition object, not a list pos = getattr(self.eng, 'position', None) if pos is not None: open_notional = float(getattr(pos, 'notional', 0) or 0) open_positions_list = [{ 'asset': pos.asset, 'side': 'SHORT' if pos.direction == -1 else 'LONG', 'entry_price': pos.entry_price, 'quantity': round(open_notional / pos.entry_price, 6) if pos.entry_price else 0, 'notional': open_notional, 'leverage': float(getattr(pos, 'leverage', 0) or 0), 'unrealized_pnl': round(pos.pnl_pct * open_notional, 2), }] else: open_notional = 0.0 open_positions_list = [] cur_leverage = (open_notional / capital) if capital and capital > 0 and math.isfinite(capital) else 0.0 snapshot = { 'capital': capital if math.isfinite(capital) else None, 'open_positions': open_positions_list, 'algo_version': ALGO_VERSION, 'last_scan_number': scan_number, 'last_vel_div': vel_div, 'vol_ok': vol_ok, 'posture': posture, 'scans_processed': self.scans_processed, 'trades_executed': self.trades_executed, 'bar_idx': self.bar_idx, 'timestamp': datetime.now(timezone.utc).isoformat(), # Leverage envelope — for TUI slider 'leverage_soft_cap': getattr(self.eng, 'base_max_leverage', 8.0), 'leverage_abs_cap': getattr(self.eng, 'abs_max_leverage', 9.0), 'open_notional': round(open_notional, 2), 'current_leverage': round(cur_leverage, 4), } future = self.state_map.put('engine_snapshot', json.dumps(snapshot)) future.add_done_callback(lambda f: None) # Heartbeat — MHS checks age < 30s; we run every scan (~11s) if self.heartbeat_map is not None: hb = json.dumps({ 'ts': time.time(), 'iso': datetime.now(timezone.utc).isoformat(), 'run_date': self.current_day, 'phase': 'trading', 'flow': 'nautilus_event_trader', }) self.heartbeat_map.put('nautilus_flow_heartbeat', hb) # Persist capital so next restart resumes from here if capital is not None and math.isfinite(capital) and capital >= 1.0: self._save_capital() except Exception as e: log(f" Failed to push state: {e}") def run(self): global running log("=" * 70) log("🐬 DOLPHIN Nautilus Event-Driven Trader Starting") log("=" * 70) self._build_engine() self._connect_hz() self._restore_capital() self._rollover_day() self._restore_position_state() def listener(event): self.on_scan(event) self.features_map.add_entry_listener( key='latest_eigen_scan', include_value=True, updated_func=listener, added_func=listener ) def exf_listener(event): self.on_exf_update(event) self.features_map.add_entry_listener( key='exf_latest', include_value=True, updated_func=exf_listener, added_func=exf_listener ) log("✅ Hz listener registered") log(f"🏷️ ALGO_VERSION: {ALGO_VERSION}") log("⏳ Waiting for scans...") try: while running: time.sleep(1) except KeyboardInterrupt: log("Interrupted") finally: self.shutdown() def shutdown(self): log("Shutting down...") self._scan_executor.shutdown(wait=False) if self.eng and self.current_day: try: with self.eng_lock: summary = self.eng.end_day() log(f"end_day: {summary}") except Exception as e: log(f"end_day failed: {e}") if self._market_state_runtime is not None: try: self._market_state_runtime.save() except Exception: pass if self.hz_client: try: self.hz_client.shutdown() log("Hz disconnected") except: pass log(f"🛑 Stopped. Scans: {self.scans_processed}, Trades: {self.trades_executed}") def signal_handler(signum, frame): global running log(f"Signal {signum} received") running = False def main(): signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) trader = DolphinLiveTrader() trader.run() if __name__ == '__main__': main()