1054 lines
52 KiB
Python
1054 lines
52 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
DOLPHIN TUI v3 — Live, event-driven observability
|
|||
|
|
==================================================
|
|||
|
|
All panels driven by real data. Zero additional load on origin systems:
|
|||
|
|
• HZ maps → entry listeners (push model; callbacks fire on change only)
|
|||
|
|
• Prefect → polled via SDK every 60 s
|
|||
|
|
• Capital / meta-health → HZ listener + disk fallback
|
|||
|
|
|
|||
|
|
Run: python3 dolphin_tui_v3.py
|
|||
|
|
Keys: q=quit r=force-refresh l=toggle log t=toggle test footer
|
|||
|
|
|
|||
|
|
HZ maps consumed:
|
|||
|
|
DOLPHIN_FEATURES / latest_eigen_scan
|
|||
|
|
DOLPHIN_FEATURES / exf_latest
|
|||
|
|
DOLPHIN_FEATURES / acb_boost
|
|||
|
|
DOLPHIN_FEATURES / obf_universe_latest
|
|||
|
|
DOLPHIN_FEATURES / mc_forewarner_latest
|
|||
|
|
DOLPHIN_META_HEALTH / latest
|
|||
|
|
DOLPHIN_SAFETY / latest
|
|||
|
|
DOLPHIN_STATE_BLUE / capital_checkpoint
|
|||
|
|
DOLPHIN_HEARTBEAT / nautilus_flow_heartbeat
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# ── stdlib ────────────────────────────────────────────────────────────────────
|
|||
|
|
import asyncio
|
|||
|
|
import json
|
|||
|
|
import math
|
|||
|
|
import threading
|
|||
|
|
import time
|
|||
|
|
from collections import deque
|
|||
|
|
from datetime import datetime, timezone
|
|||
|
|
from pathlib import Path
|
|||
|
|
from typing import Any, Dict, Optional
|
|||
|
|
|
|||
|
|
# ── third-party ───────────────────────────────────────────────────────────────
|
|||
|
|
try:
|
|||
|
|
from textual.app import App, ComposeResult
|
|||
|
|
from textual.containers import Horizontal, Vertical
|
|||
|
|
from textual.widgets import Digits, ProgressBar, Sparkline, Static
|
|||
|
|
except ImportError as e:
|
|||
|
|
raise SystemExit(f"textual not found — activate siloqy_env: {e}")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
import hazelcast
|
|||
|
|
_HZ_OK = True
|
|||
|
|
except ImportError:
|
|||
|
|
_HZ_OK = False
|
|||
|
|
|
|||
|
|
# ── paths ─────────────────────────────────────────────────────────────────────
|
|||
|
|
_META_JSON = Path("/mnt/dolphinng5_predict/run_logs/meta_health.json")
|
|||
|
|
_CAPITAL_JSON = Path("/tmp/dolphin_capital_checkpoint.json")
|
|||
|
|
_TEST_JSON = Path("/mnt/dolphinng5_predict/run_logs/test_results_latest.json")
|
|||
|
|
_PREFECT_BASE = "http://localhost:4200"
|
|||
|
|
|
|||
|
|
# ── symbols watched in OBF panel ──────────────────────────────────────────────
|
|||
|
|
_OBF_SYMS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT"]
|
|||
|
|
|
|||
|
|
# ── color maps ────────────────────────────────────────────────────────────────
|
|||
|
|
_PC = {"APEX": "green", "STALKER": "yellow", "TURTLE": "dark_orange", "HIBERNATE": "red"}
|
|||
|
|
_MC = {"GREEN": "green", "ORANGE": "dark_orange", "RED": "red"}
|
|||
|
|
_SC = {"GREEN": "green", "DEGRADED": "yellow", "CRITICAL": "dark_orange", "DEAD": "red"}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
# Thread-safe state store
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
class _State:
|
|||
|
|
"""Lock-guarded dict shared between HZ listener threads and the Textual loop."""
|
|||
|
|
def __init__(self):
|
|||
|
|
self._l = threading.Lock()
|
|||
|
|
self._d: Dict[str, Any] = {}
|
|||
|
|
def put(self, k: str, v: Any):
|
|||
|
|
with self._l: self._d[k] = v
|
|||
|
|
def get(self, k: str, default=None):
|
|||
|
|
with self._l: return self._d.get(k, default)
|
|||
|
|
def update(self, mapping: dict):
|
|||
|
|
with self._l: self._d.update(mapping)
|
|||
|
|
|
|||
|
|
|
|||
|
|
_S = _State()
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
# HZ entry-listener adapter (runs in a daemon thread, calls back to Textual)
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
def _ingest(key: str, raw: Optional[str]):
|
|||
|
|
"""Parse JSON payload and store in shared state."""
|
|||
|
|
if not raw:
|
|||
|
|
return
|
|||
|
|
try:
|
|||
|
|
_S.update({f"hz.{key}": json.loads(raw), f"hz.{key}._t": time.time()})
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
def start_hz_listener(on_scan=None):
|
|||
|
|
"""
|
|||
|
|
Daemon thread: connect, initial-fetch all keys, attach entry listeners.
|
|||
|
|
on_scan: callable invoked (from HZ thread) when latest_eigen_scan arrives.
|
|||
|
|
Reconnects automatically on disconnect.
|
|||
|
|
"""
|
|||
|
|
if not _HZ_OK:
|
|||
|
|
_S.put("hz_up", False)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
def _run():
|
|||
|
|
while True:
|
|||
|
|
try:
|
|||
|
|
_S.put("hz_up", False)
|
|||
|
|
client = hazelcast.HazelcastClient(
|
|||
|
|
cluster_name="dolphin",
|
|||
|
|
cluster_members=["localhost:5701"],
|
|||
|
|
connection_timeout=5.0,
|
|||
|
|
)
|
|||
|
|
_S.put("hz_up", True)
|
|||
|
|
|
|||
|
|
# ── DOLPHIN_FEATURES ─────────────────────────────────────
|
|||
|
|
fm = client.get_map("DOLPHIN_FEATURES").blocking()
|
|||
|
|
for k in ("latest_eigen_scan", "exf_latest", "acb_boost",
|
|||
|
|
"obf_universe_latest", "mc_forewarner_latest"):
|
|||
|
|
_ingest(k, fm.get(k))
|
|||
|
|
|
|||
|
|
def _f(e):
|
|||
|
|
_ingest(e.key, e.value)
|
|||
|
|
if e.key == "latest_eigen_scan" and on_scan:
|
|||
|
|
try: on_scan()
|
|||
|
|
except Exception: pass
|
|||
|
|
fm.add_entry_listener(include_value=True, updated=_f, added=_f)
|
|||
|
|
|
|||
|
|
# ── DOLPHIN_META_HEALTH ──────────────────────────────────
|
|||
|
|
mhm = client.get_map("DOLPHIN_META_HEALTH").blocking()
|
|||
|
|
_ingest("meta_health", mhm.get("latest"))
|
|||
|
|
def _mh(e):
|
|||
|
|
if e.key == "latest": _ingest("meta_health", e.value)
|
|||
|
|
mhm.add_entry_listener(include_value=True, updated=_mh, added=_mh)
|
|||
|
|
|
|||
|
|
# ── DOLPHIN_SAFETY ───────────────────────────────────────
|
|||
|
|
sm = client.get_map("DOLPHIN_SAFETY").blocking()
|
|||
|
|
_ingest("safety", sm.get("latest"))
|
|||
|
|
def _sf(e):
|
|||
|
|
if e.key == "latest": _ingest("safety", e.value)
|
|||
|
|
sm.add_entry_listener(include_value=True, updated=_sf, added=_sf)
|
|||
|
|
|
|||
|
|
# ── DOLPHIN_STATE_BLUE ───────────────────────────────────
|
|||
|
|
stm = client.get_map("DOLPHIN_STATE_BLUE").blocking()
|
|||
|
|
_ingest("capital", stm.get("capital_checkpoint"))
|
|||
|
|
_ingest("engine_snapshot", stm.get("engine_snapshot"))
|
|||
|
|
def _cap(e):
|
|||
|
|
if e.key == "capital_checkpoint": _ingest("capital", e.value)
|
|||
|
|
elif e.key == "engine_snapshot": _ingest("engine_snapshot", e.value)
|
|||
|
|
stm.add_entry_listener(include_value=True, updated=_cap, added=_cap)
|
|||
|
|
|
|||
|
|
# ── DOLPHIN_PNL_BLUE (live trade performance) ────────────
|
|||
|
|
try:
|
|||
|
|
pnl_m = client.get_map("DOLPHIN_PNL_BLUE").blocking()
|
|||
|
|
_ingest("pnl_blue", pnl_m.get("session_perf"))
|
|||
|
|
def _pnl(e):
|
|||
|
|
if e.key == "session_perf": _ingest("pnl_blue", e.value)
|
|||
|
|
pnl_m.add_entry_listener(include_value=True, updated=_pnl, added=_pnl)
|
|||
|
|
except Exception:
|
|||
|
|
pass # map may not exist yet — silent until event trader wires it
|
|||
|
|
|
|||
|
|
# ── DOLPHIN_HEARTBEAT ────────────────────────────────────
|
|||
|
|
hbm = client.get_map("DOLPHIN_HEARTBEAT").blocking()
|
|||
|
|
_ingest("heartbeat", hbm.get("nautilus_flow_heartbeat"))
|
|||
|
|
def _hb(e):
|
|||
|
|
if e.key == "nautilus_flow_heartbeat": _ingest("heartbeat", e.value)
|
|||
|
|
hbm.add_entry_listener(include_value=True, updated=_hb, added=_hb)
|
|||
|
|
|
|||
|
|
# Block until disconnected
|
|||
|
|
while True:
|
|||
|
|
time.sleep(5)
|
|||
|
|
if not getattr(client.lifecycle_service, "is_running",
|
|||
|
|
lambda: True)():
|
|||
|
|
break
|
|||
|
|
except Exception as e:
|
|||
|
|
_S.put("hz_up", False)
|
|||
|
|
_S.put("hz_err", str(e))
|
|||
|
|
time.sleep(10)
|
|||
|
|
|
|||
|
|
threading.Thread(target=_run, daemon=True, name="hz-listener").start()
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
# Prefect poll (asyncio task, 60s interval)
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
async def prefect_poll_loop():
|
|||
|
|
"""Poll Prefect SDK for latest flow-run status per flow. 60s interval."""
|
|||
|
|
while True:
|
|||
|
|
try:
|
|||
|
|
from prefect.client.orchestration import get_client
|
|||
|
|
from prefect.client.schemas.sorting import FlowRunSort
|
|||
|
|
async with get_client() as pc:
|
|||
|
|
runs = await pc.read_flow_runs(
|
|||
|
|
limit=20, sort=FlowRunSort.START_TIME_DESC)
|
|||
|
|
seen: Dict[str, Any] = {}
|
|||
|
|
fid_to_name: Dict[str, str] = {}
|
|||
|
|
for r in runs:
|
|||
|
|
fid = str(r.flow_id)
|
|||
|
|
if fid not in seen:
|
|||
|
|
seen[fid] = r
|
|||
|
|
rows = []
|
|||
|
|
for fid, r in seen.items():
|
|||
|
|
if fid not in fid_to_name:
|
|||
|
|
try:
|
|||
|
|
f = await pc.read_flow(r.flow_id)
|
|||
|
|
fid_to_name[fid] = f.name
|
|||
|
|
except Exception:
|
|||
|
|
fid_to_name[fid] = fid[:8]
|
|||
|
|
rows.append({
|
|||
|
|
"name": fid_to_name[fid],
|
|||
|
|
"state": r.state_name or "?",
|
|||
|
|
"ts": r.start_time.strftime("%m-%d %H:%M")
|
|||
|
|
if r.start_time else "--",
|
|||
|
|
})
|
|||
|
|
_S.put("prefect_flows", rows[:8])
|
|||
|
|
_S.put("prefect_ok", True)
|
|||
|
|
except Exception as e:
|
|||
|
|
_S.put("prefect_ok", False)
|
|||
|
|
_S.put("prefect_err", str(e)[:60])
|
|||
|
|
await asyncio.sleep(60)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
# Helper utilities
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
def _age(ts: float) -> str:
|
|||
|
|
"""Render age of a Unix timestamp as compact string."""
|
|||
|
|
if not ts:
|
|||
|
|
return "?"
|
|||
|
|
s = time.time() - ts
|
|||
|
|
if s < 0: return "0s"
|
|||
|
|
if s < 60: return f"{s:.0f}s"
|
|||
|
|
if s < 3600: return f"{s/60:.0f}m"
|
|||
|
|
return f"{s/3600:.1f}h"
|
|||
|
|
|
|||
|
|
def _age_col(ts: float, warn=15, dead=60) -> str:
|
|||
|
|
s = time.time() - ts if ts else 9999
|
|||
|
|
if s > dead: return "red"
|
|||
|
|
if s > warn: return "yellow"
|
|||
|
|
return "green"
|
|||
|
|
|
|||
|
|
def _bar(v: float, width=12) -> str:
|
|||
|
|
"""ASCII fill bar 0-1."""
|
|||
|
|
v = max(0.0, min(1.0, v))
|
|||
|
|
f = round(v * width)
|
|||
|
|
return "█" * f + "░" * (width - f)
|
|||
|
|
|
|||
|
|
def _fmt_vel(v) -> str:
|
|||
|
|
if v is None: return "---"
|
|||
|
|
return f"{float(v):+.5f}"
|
|||
|
|
|
|||
|
|
def _dot(state: str) -> str:
|
|||
|
|
s = (state or "").upper()
|
|||
|
|
if s == "COMPLETED": return "[green]●[/green]"
|
|||
|
|
if s == "RUNNING": return "[cyan]●[/cyan]"
|
|||
|
|
if s in ("FAILED","CRASHED","TIMEDOUT"): return "[red]●[/red]"
|
|||
|
|
if s == "CANCELLED": return "[dim]●[/dim]"
|
|||
|
|
if s == "PENDING": return "[yellow]●[/yellow]"
|
|||
|
|
return "[dim]◌[/dim]"
|
|||
|
|
|
|||
|
|
def _posture_markup(p: str) -> str:
|
|||
|
|
c = _PC.get(p, "dim")
|
|||
|
|
return f"[{c}]{p}[/{c}]"
|
|||
|
|
|
|||
|
|
def _col(v, c): return f"[{c}]{v}[/{c}]"
|
|||
|
|
|
|||
|
|
def _eigen_from_scan(scan: dict):
|
|||
|
|
"""Extract normalised eigen fields from both NG7 (nested result) and NG8 (flat) HZ formats."""
|
|||
|
|
if not scan: return {}
|
|||
|
|
# NG7 wraps in "result"
|
|||
|
|
r = scan.get("result", scan)
|
|||
|
|
mwr_raw = r.get("multi_window_results", {})
|
|||
|
|
# Keys may be ints or strings
|
|||
|
|
def _td(w):
|
|||
|
|
return (mwr_raw.get(w) or mwr_raw.get(str(w)) or {}).get("tracking_data", {})
|
|||
|
|
def _rs(w):
|
|||
|
|
return (mwr_raw.get(w) or mwr_raw.get(str(w)) or {}).get("regime_signals", {})
|
|||
|
|
# NG8 flat format also stores w50_velocity etc.
|
|||
|
|
v50 = _td(50).get("lambda_max_velocity") or scan.get("w50_velocity", 0.0)
|
|||
|
|
v150 = _td(150).get("lambda_max_velocity") or scan.get("w150_velocity", 0.0)
|
|||
|
|
v300 = _td(300).get("lambda_max_velocity") or scan.get("w300_velocity", 0.0)
|
|||
|
|
v750 = _td(750).get("lambda_max_velocity") or scan.get("w750_velocity", 0.0)
|
|||
|
|
vel_div = scan.get("vel_div", float(v50 or 0) - float(v150 or 0))
|
|||
|
|
inst_50 = _rs(50).get("instability_score", 0.0)
|
|||
|
|
inst_avg = sum(_rs(w).get("instability_score", 0.0) for w in (50,150,300,750)) / 4
|
|||
|
|
bt_price = (r.get("pricing_data", {}) or {}).get("current_prices", {}).get("BTCUSDT")
|
|||
|
|
return {
|
|||
|
|
"scan_number": scan.get("scan_number", 0),
|
|||
|
|
"timestamp": scan.get("timestamp", 0),
|
|||
|
|
"vel_div": float(vel_div or 0),
|
|||
|
|
"v50": float(v50 or 0), "v150": float(v150 or 0),
|
|||
|
|
"v300": float(v300 or 0), "v750": float(v750 or 0),
|
|||
|
|
"inst50": float(inst_50 or 0),
|
|||
|
|
"inst_avg": float(inst_avg or 0),
|
|||
|
|
"btc_price": float(bt_price) if bt_price else None,
|
|||
|
|
"regime": r.get("regime", r.get("sentiment", "?")),
|
|||
|
|
"version": scan.get("version", "?"),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
# CSS
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
_CSS = """
|
|||
|
|
Screen { background: #0a0a0a; color: #d4d4d4; }
|
|||
|
|
#header { height: 2; background: #111; border-bottom: solid #333; padding: 0 1; }
|
|||
|
|
#trader_row { height: 5; }
|
|||
|
|
#top_row { height: 9; }
|
|||
|
|
#mid_row { height: 9; }
|
|||
|
|
#bot_row { height: 7; }
|
|||
|
|
#log_row { height: 5; display: none; }
|
|||
|
|
#mc_outer { height: 16; border: solid #224; background: #060616; }
|
|||
|
|
#mc_title { height: 1; padding: 0 1; }
|
|||
|
|
#mc_body { height: 15; }
|
|||
|
|
#mc_left { width: 18; padding: 0 1; }
|
|||
|
|
#mc_center { width: 1fr; padding: 0 1; }
|
|||
|
|
#mc_right { width: 30; padding: 0 1; }
|
|||
|
|
/* Tier 1: envelope assessment */
|
|||
|
|
#mc_prob_label { height: 1; }
|
|||
|
|
#mc_prob_bar { height: 1; }
|
|||
|
|
#mc_env_label { height: 1; }
|
|||
|
|
#mc_env_bar { height: 1; }
|
|||
|
|
#mc_champ_label { height: 1; }
|
|||
|
|
#mc_champ_bar { height: 1; }
|
|||
|
|
/* Tier 2: live perf (ROI/DD/WR + MAE rows + leverage slider) */
|
|||
|
|
#mc_live { height: 8; }
|
|||
|
|
/* Right column */
|
|||
|
|
#mc_spark_lbl { height: 1; }
|
|||
|
|
#mc_spark { height: 2; }
|
|||
|
|
#mc_mae_spark { height: 2; }
|
|||
|
|
#mc_mae_lbl { height: 1; }
|
|||
|
|
#mc_digits { height: 3; }
|
|||
|
|
#mc_status { height: 3; }
|
|||
|
|
#mc_legend { height: 6; }
|
|||
|
|
#test_footer { height: 3; background: #101010; border-top: solid #2a2a2a; padding: 0 1; }
|
|||
|
|
|
|||
|
|
Static.panel { border: solid #333; padding: 0 1; height: 100%; }
|
|||
|
|
#p_trader { width: 1fr; border: solid #006650; }
|
|||
|
|
#p_health { width: 1fr; }
|
|||
|
|
#p_alpha { width: 1fr; }
|
|||
|
|
#p_scan { width: 1fr; }
|
|||
|
|
#p_extf { width: 1fr; }
|
|||
|
|
#p_obf { width: 1fr; }
|
|||
|
|
#p_capital { width: 1fr; }
|
|||
|
|
#p_prefect { width: 1fr; }
|
|||
|
|
#p_acb { width: 1fr; }
|
|||
|
|
#p_log { width: 1fr; border: solid #333; padding: 0 1; }
|
|||
|
|
|
|||
|
|
ProgressBar > .bar--bar { color: $success; }
|
|||
|
|
ProgressBar > .bar--complete { color: $success; }
|
|||
|
|
ProgressBar.-danger > .bar--bar { color: $error; }
|
|||
|
|
ProgressBar.-warning > .bar--bar { color: $warning; }
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
# Textual App
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
class DolphinTUI(App):
|
|||
|
|
CSS = _CSS
|
|||
|
|
BINDINGS = [
|
|||
|
|
("q", "quit", "Quit"),
|
|||
|
|
("r", "force_refresh","Refresh"),
|
|||
|
|
("l", "toggle_log", "Log"),
|
|||
|
|
("t", "toggle_tests", "Tests"),
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
_log_vis = False
|
|||
|
|
_test_vis = True
|
|||
|
|
_prob_hist: deque
|
|||
|
|
|
|||
|
|
# ── Layout ───────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def compose(self) -> ComposeResult:
|
|||
|
|
yield Static("", id="header")
|
|||
|
|
|
|||
|
|
with Horizontal(id="trader_row"):
|
|||
|
|
yield Static("", classes="panel", id="p_trader")
|
|||
|
|
|
|||
|
|
with Horizontal(id="top_row"):
|
|||
|
|
yield Static("", classes="panel", id="p_health")
|
|||
|
|
yield Static("", classes="panel", id="p_alpha")
|
|||
|
|
yield Static("", classes="panel", id="p_scan")
|
|||
|
|
|
|||
|
|
with Horizontal(id="mid_row"):
|
|||
|
|
yield Static("", classes="panel", id="p_extf")
|
|||
|
|
yield Static("", classes="panel", id="p_obf")
|
|||
|
|
yield Static("", classes="panel", id="p_capital")
|
|||
|
|
|
|||
|
|
with Horizontal(id="bot_row"):
|
|||
|
|
yield Static("", classes="panel", id="p_prefect")
|
|||
|
|
yield Static("", classes="panel", id="p_acb")
|
|||
|
|
|
|||
|
|
with Vertical(id="mc_outer"):
|
|||
|
|
yield Static("", id="mc_title")
|
|||
|
|
with Horizontal(id="mc_body"):
|
|||
|
|
with Vertical(id="mc_left"):
|
|||
|
|
yield Digits("0.000", id="mc_digits")
|
|||
|
|
yield Static("", id="mc_status")
|
|||
|
|
with Vertical(id="mc_center"):
|
|||
|
|
# ── Tier 1: envelope assessment (4h) ──
|
|||
|
|
yield Static("", id="mc_prob_label")
|
|||
|
|
yield ProgressBar(total=100, show_eta=False,
|
|||
|
|
show_percentage=False, id="mc_prob_bar")
|
|||
|
|
yield Static("", id="mc_env_label")
|
|||
|
|
yield ProgressBar(total=100, show_eta=False,
|
|||
|
|
show_percentage=False, id="mc_env_bar")
|
|||
|
|
yield Static("", id="mc_champ_label")
|
|||
|
|
yield ProgressBar(total=100, show_eta=False,
|
|||
|
|
show_percentage=False, id="mc_champ_bar")
|
|||
|
|
# ── Tier 2: live performance vs MC bounds ──
|
|||
|
|
yield Static("", id="mc_live")
|
|||
|
|
with Vertical(id="mc_right"):
|
|||
|
|
yield Static("", id="mc_spark_lbl")
|
|||
|
|
yield Sparkline([], id="mc_spark")
|
|||
|
|
yield Static("", id="mc_mae_lbl")
|
|||
|
|
yield Sparkline([], id="mc_mae_spark")
|
|||
|
|
yield Static("", id="mc_legend")
|
|||
|
|
|
|||
|
|
yield Static("", id="test_footer")
|
|||
|
|
|
|||
|
|
with Horizontal(id="log_row"):
|
|||
|
|
yield Static("", classes="panel", id="p_log")
|
|||
|
|
|
|||
|
|
def on_mount(self) -> None:
|
|||
|
|
self._prob_hist = deque([0.0] * 40, maxlen=40)
|
|||
|
|
self._session_start_cap: Optional[float] = None # first capital seen → ROI denominator
|
|||
|
|
self._cap_peak: Optional[float] = None # peak capital → live DD numerator
|
|||
|
|
self._mae_deque: deque = deque(maxlen=500) # per-trade MAE values, newest last
|
|||
|
|
# Event-driven callback: fires from HZ listener thread
|
|||
|
|
start_hz_listener(on_scan=lambda: self.call_from_thread(self._update))
|
|||
|
|
# Prefect async poll
|
|||
|
|
self.run_worker(prefect_poll_loop(), name="prefect-poll", exclusive=True)
|
|||
|
|
# Periodic fallback refresh (catches non-HZ sources + any missed events)
|
|||
|
|
self.set_interval(1.0, self._update)
|
|||
|
|
self._update()
|
|||
|
|
|
|||
|
|
# ── Master render ────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _update(self) -> None:
|
|||
|
|
now_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
|||
|
|
hz_up = _S.get("hz_up", False)
|
|||
|
|
|
|||
|
|
# Pull all live data from state
|
|||
|
|
mh = _S.get("hz.meta_health") or self._read_json(_META_JSON)
|
|||
|
|
safe = _S.get("hz.safety", {})
|
|||
|
|
scan = _S.get("hz.latest_eigen_scan", {})
|
|||
|
|
exf = _S.get("hz.exf_latest", {})
|
|||
|
|
acb = _S.get("hz.acb_boost", {})
|
|||
|
|
obf_u = _S.get("hz.obf_universe_latest", {})
|
|||
|
|
mc = _S.get("hz.mc_forewarner_latest", {})
|
|||
|
|
cap = _S.get("hz.capital") or self._read_json(_CAPITAL_JSON) or {}
|
|||
|
|
hb = _S.get("hz.heartbeat", {})
|
|||
|
|
eigen = _eigen_from_scan(scan)
|
|||
|
|
|
|||
|
|
# ── HEADER ───────────────────────────────────────────────────────────
|
|||
|
|
rm_m = mh.get("rm_meta", 0.0) if mh else 0.0
|
|||
|
|
mhs_st = mh.get("status", "?") if mh else "?"
|
|||
|
|
sc_mhs = _SC.get(mhs_st, "dim")
|
|||
|
|
posture= (safe or {}).get("posture", "?")
|
|||
|
|
pc_col = _PC.get(posture, "dim")
|
|||
|
|
hz_tag = "[green][HZ✓][/green]" if hz_up else "[red][HZ✗][/red]"
|
|||
|
|
mc_st = (mc or {}).get("status", "N/A")
|
|||
|
|
mc_col = _MC.get(mc_st, "dim")
|
|||
|
|
self._w("#header").update(
|
|||
|
|
f"[bold cyan]🐬 DOLPHIN-NAUTILUS[/bold cyan] {now_str}"
|
|||
|
|
f" {hz_tag} [{sc_mhs}]MHS:{mhs_st} {rm_m:.3f}[/{sc_mhs}]"
|
|||
|
|
f" [{pc_col}]◈{posture}[/{pc_col}]"
|
|||
|
|
f" [{mc_col}]MC:{mc_st}[/{mc_col}]\n"
|
|||
|
|
f"[dim] localhost:5701 q=quit r=refresh l=log t=tests[/dim]"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ── TRADER ───────────────────────────────────────────────────────────
|
|||
|
|
cap_val = float(cap.get("capital", 0)) if cap else 0.0
|
|||
|
|
hb_phase = hb.get("phase", "?") if hb else "N/A"
|
|||
|
|
hb_ts = hb.get("ts") if hb else None
|
|||
|
|
hb_age = _age(hb_ts) if hb_ts else "?"
|
|||
|
|
hb_col = _age_col(hb_ts, 30, 120) if hb_ts else "red"
|
|||
|
|
vel_div = eigen.get("vel_div", 0.0)
|
|||
|
|
vc = "green" if vel_div > 0 else ("red" if vel_div < -0.02 else "yellow")
|
|||
|
|
scan_no = eigen.get("scan_number", 0)
|
|||
|
|
scan_ts = eigen.get("timestamp", 0)
|
|||
|
|
vol_ok_s = "[green]VOL✓[/green]" # placeholder — would need vol calc
|
|||
|
|
btc_p = eigen.get("btc_price")
|
|||
|
|
btc_str = f"BTC:[cyan]${btc_p:,.0f}[/cyan] " if btc_p else ""
|
|||
|
|
regime = eigen.get("regime", "?")
|
|||
|
|
self._w("#p_trader").update(
|
|||
|
|
f"[bold cyan]TRADER[/bold cyan] {_posture_markup(posture)}"
|
|||
|
|
f" phase:[{hb_col}]{hb_phase}[/{hb_col}] hb:{_col(hb_age, hb_col)}"
|
|||
|
|
f" scan:[dim]#{scan_no}[/dim] {btc_str}"
|
|||
|
|
f" regime:[yellow]{regime}[/yellow] {vol_ok_s}\n"
|
|||
|
|
f" vel_div:[{vc}]{vel_div:+.5f}[/{vc}] thr:[dim]-0.02000[/dim]"
|
|||
|
|
f" cap:[cyan]${cap_val:,.0f}[/cyan]\n"
|
|||
|
|
f" open-pos: [dim]read DOLPHIN_PNL_BLUE (not yet wired)[/dim]"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ── SYS HEALTH ───────────────────────────────────────────────────────
|
|||
|
|
if mh:
|
|||
|
|
svc = mh.get("service_status", {})
|
|||
|
|
hz_ks = mh.get("hz_key_status", {})
|
|||
|
|
def _svc_dot(nm):
|
|||
|
|
st = svc.get(nm, "?")
|
|||
|
|
return "[green]●[/green]" if st=="RUNNING" else "[red]●[/red]"
|
|||
|
|
def _hz_dot(nm):
|
|||
|
|
info = hz_ks.get(nm, {})
|
|||
|
|
sc = info.get("score", 0)
|
|||
|
|
return "[green]●[/green]" if sc >= 0.9 else ("[yellow]●[/yellow]" if sc >= 0.5 else "[red]●[/red]")
|
|||
|
|
self._w("#p_health").update(
|
|||
|
|
f"[bold]SYS HEALTH[/bold] [{sc_mhs}]{mhs_st}[/{sc_mhs}]\n"
|
|||
|
|
f"rm:[{sc_mhs}]{rm_m:.3f}[/{sc_mhs}]"
|
|||
|
|
f" m4:{mh['m4_control_plane']:.2f}"
|
|||
|
|
f" m1d:{mh['m1_data_infra']:.2f}"
|
|||
|
|
f" m3:{mh['m3_data_freshness']:.2f}"
|
|||
|
|
f" m5:{mh['m5_coherence']:.2f}\n"
|
|||
|
|
f"exf:{_svc_dot('dolphin_data:exf_fetcher')}"
|
|||
|
|
f" acb:{_svc_dot('dolphin_data:acb_processor')}"
|
|||
|
|
f" obf:{_svc_dot('dolphin_data:obf_universe')}\n"
|
|||
|
|
f"tdr:{_svc_dot('dolphin:nautilus_trader')}"
|
|||
|
|
f" scb:{_svc_dot('dolphin:scan_bridge')}\n"
|
|||
|
|
f"[dim]hz: exf{_hz_dot('exf_latest')}"
|
|||
|
|
f" scan{_hz_dot('latest_eigen_scan')}"
|
|||
|
|
f" obf{_hz_dot('obf_universe')}[/dim]"
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
self._w("#p_health").update("[bold]SYS HEALTH[/bold]\n[dim]awaiting MHS…[/dim]")
|
|||
|
|
|
|||
|
|
# ── ALPHA ENGINE (SurvivalStack) ──────────────────────────────────────
|
|||
|
|
rm_s = float((safe or {}).get("Rm", 0.0))
|
|||
|
|
bd = (safe or {}).get("breakdown", {})
|
|||
|
|
safe_ts = _S.get("hz.safety._t")
|
|||
|
|
safe_age = _age(safe_ts) if safe_ts else "?"
|
|||
|
|
safe_ac = _age_col(safe_ts, 30, 120) if safe_ts else "red"
|
|||
|
|
def _cat(n):
|
|||
|
|
v = bd.get(f"Cat{n}", 0.0)
|
|||
|
|
c = "green" if v >= 0.9 else ("yellow" if v >= 0.6 else "red")
|
|||
|
|
return f"[{c}]{v:.2f}[/{c}]"
|
|||
|
|
self._w("#p_alpha").update(
|
|||
|
|
f"[bold]ALPHA ENGINE[/bold] {_posture_markup(posture)}\n"
|
|||
|
|
f"Rm:[{_PC.get(posture,'dim')}]{_bar(rm_s,14)}[/{_PC.get(posture,'dim')}]{rm_s:.3f}\n"
|
|||
|
|
f"C1:{_cat(1)} C2:{_cat(2)} C3:{_cat(3)}\n"
|
|||
|
|
f"C4:{_cat(4)} C5:{_cat(5)}"
|
|||
|
|
f" fenv:{bd.get('f_env',0):.2f} fex:{bd.get('f_exe',0):.2f}\n"
|
|||
|
|
f"[dim]age:{_col(safe_age, safe_ac)}[/dim]"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ── SCAN / NG8 ────────────────────────────────────────────────────────
|
|||
|
|
scan_age = _age(eigen.get("timestamp")) if eigen.get("timestamp") else "?"
|
|||
|
|
scan_ac = _age_col(eigen.get("timestamp", 0), 15, 60)
|
|||
|
|
vi = eigen.get("inst_avg", 0)
|
|||
|
|
self._w("#p_scan").update(
|
|||
|
|
f"[bold]SCAN {eigen.get('version','?')}[/bold]"
|
|||
|
|
f" [dim]#{scan_no}[/dim] age:[{scan_ac}]{scan_age}[/{scan_ac}]\n"
|
|||
|
|
f"vel_div:[{vc}]{vel_div:+.5f}[/{vc}]\n"
|
|||
|
|
f"w50:[yellow]{_fmt_vel(eigen.get('v50'))}[/yellow]"
|
|||
|
|
f" w150:[dim]{_fmt_vel(eigen.get('v150'))}[/dim]\n"
|
|||
|
|
f"w300:[dim]{_fmt_vel(eigen.get('v300'))}[/dim]"
|
|||
|
|
f" w750:[dim]{_fmt_vel(eigen.get('v750'))}[/dim]\n"
|
|||
|
|
f"inst:{vi:.4f}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ── ExtF ──────────────────────────────────────────────────────────────
|
|||
|
|
exf_t = _S.get("hz.exf_latest._t")
|
|||
|
|
exf_age = _age(exf_t) if exf_t else "?"
|
|||
|
|
exf_ac = _age_col(exf_t, 30, 120) if exf_t else "red"
|
|||
|
|
f_btc = exf.get("funding_btc")
|
|||
|
|
dvol = exf.get("dvol_btc")
|
|||
|
|
fng = exf.get("fng")
|
|||
|
|
taker = exf.get("taker")
|
|||
|
|
ls_btc = exf.get("ls_btc")
|
|||
|
|
vix = exf.get("vix")
|
|||
|
|
ok_cnt = exf.get("_ok_count", 0)
|
|||
|
|
dvol_c = "red" if dvol and dvol > 70 else ("yellow" if dvol and dvol > 50 else "green")
|
|||
|
|
fng_c = "red" if fng and fng < 25 else ("yellow" if fng and fng < 45 else "green")
|
|||
|
|
def _exf_str():
|
|||
|
|
if not exf:
|
|||
|
|
return "[bold]ExtF[/bold]\n[dim]no data[/dim]"
|
|||
|
|
_f = f"{f_btc:.5f}" if f_btc is not None else "?"
|
|||
|
|
_dv = f"{dvol:.1f}" if dvol is not None else "?"
|
|||
|
|
_fg = f"{int(fng)}" if fng is not None else "?"
|
|||
|
|
_tk = f"{taker:.3f}" if taker is not None else "?"
|
|||
|
|
_ls = f"{ls_btc:.3f}"if ls_btc is not None else "?"
|
|||
|
|
_vx = f"{vix:.1f}" if vix is not None else "?"
|
|||
|
|
return (
|
|||
|
|
f"[bold]ExtF[/bold] [{exf_ac}]{ok_cnt}/9 {exf_age}[/{exf_ac}]\n"
|
|||
|
|
f"fund:[cyan]{_f}[/cyan] dvol:[{dvol_c}]{_dv}[/{dvol_c}]\n"
|
|||
|
|
f"fng:[{fng_c}]{_fg}[/{fng_c}] taker:[yellow]{_tk}[/yellow]\n"
|
|||
|
|
f"ls_btc:{_ls} vix:{_vx}\n"
|
|||
|
|
f"acb✓:{exf.get('_acb_ready','?')}"
|
|||
|
|
)
|
|||
|
|
self._w("#p_extf").update(_exf_str())
|
|||
|
|
|
|||
|
|
# ── OBF ───────────────────────────────────────────────────────────────
|
|||
|
|
obf_t = _S.get("hz.obf_universe_latest._t")
|
|||
|
|
obf_age = _age(obf_t) if obf_t else "?"
|
|||
|
|
obf_ac = _age_col(obf_t, 30, 120) if obf_t else "red"
|
|||
|
|
n_assets = obf_u.get("_n_assets", 0) if obf_u else 0
|
|||
|
|
lines = [f"[bold]OBF[/bold] [{obf_ac}]n={n_assets} {obf_age}[/{obf_ac}]"]
|
|||
|
|
for sym in _OBF_SYMS:
|
|||
|
|
if not obf_u: break
|
|||
|
|
a = obf_u.get(sym)
|
|||
|
|
if not a: continue
|
|||
|
|
imb = float(a.get("imbalance", 0))
|
|||
|
|
fp = float(a.get("fill_probability", 0))
|
|||
|
|
dq = float(a.get("depth_quality", 0))
|
|||
|
|
imb_c = "green" if imb > 0.1 else ("red" if imb < -0.1 else "yellow")
|
|||
|
|
lines.append(
|
|||
|
|
f"{sym[:3]} [{imb_c}]{imb:+.2f}[/{imb_c}]"
|
|||
|
|
f" fp:{fp:.2f} dq:{dq:.2f}"
|
|||
|
|
)
|
|||
|
|
self._w("#p_obf").update("\n".join(lines[:6]))
|
|||
|
|
|
|||
|
|
# ── CAPITAL ───────────────────────────────────────────────────────────
|
|||
|
|
cap_t = _S.get("hz.capital._t")
|
|||
|
|
cap_ac = _age_col(cap_t, 60, 300) if cap_t else "dim"
|
|||
|
|
cap_age= _age(cap_t) if cap_t else "?"
|
|||
|
|
# Drawdown from safety Cat5 (≈1 at <5%DD, ≈0.5 at 12%)
|
|||
|
|
c5 = bd.get("Cat5", 1.0) if bd else 1.0
|
|||
|
|
# Invert sigmoid: DD_approx = 0.12 + ln((1/c5)-1)/30
|
|||
|
|
try:
|
|||
|
|
dd_est = 0.12 + math.log(1.0/c5 - 1.0) / 30.0 if 0 < c5 < 1 else 0.0
|
|||
|
|
except Exception:
|
|||
|
|
dd_est = 0.0
|
|||
|
|
dd_c = "red" if dd_est > 0.15 else ("yellow" if dd_est > 0.08 else "green")
|
|||
|
|
self._w("#p_capital").update(
|
|||
|
|
f"[bold]CAPITAL[/bold] [{cap_ac}]{cap_age}[/{cap_ac}]\n"
|
|||
|
|
f"Cap:[cyan]${cap_val:,.0f}[/cyan]\n"
|
|||
|
|
f"DD≈:[{dd_c}]{dd_est*100:.1f}%[/{dd_c}] C5:{c5:.3f}\n"
|
|||
|
|
f"Pos:{_posture_markup(posture)}\n"
|
|||
|
|
f"[dim]pnl/trades: DOLPHIN_PNL_BLUE[/dim]"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ── PREFECT ───────────────────────────────────────────────────────────
|
|||
|
|
flows = _S.get("prefect_flows") or []
|
|||
|
|
pf_ok = _S.get("prefect_ok", False)
|
|||
|
|
pf_hdr = "[green]✓[/green]" if pf_ok else "[red]✗[/red]"
|
|||
|
|
flines = "\n".join(
|
|||
|
|
f"{_dot(f['state'])} {f['name'][:22]:<22} {f['ts']}"
|
|||
|
|
for f in flows[:5]
|
|||
|
|
)
|
|||
|
|
self._w("#p_prefect").update(
|
|||
|
|
f"[bold]PREFECT[/bold] {pf_hdr}\n"
|
|||
|
|
+ (flines or "[dim]polling…[/dim]")
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ── ACB ───────────────────────────────────────────────────────────────
|
|||
|
|
acb_t = _S.get("hz.acb_boost._t")
|
|||
|
|
acb_age = _age(acb_t) if acb_t else "?"
|
|||
|
|
acb_ac = _age_col(acb_t, 3600, 86400) if acb_t else "dim"
|
|||
|
|
boost = acb.get("boost", 1.0) if acb else 1.0
|
|||
|
|
beta = acb.get("beta", 0.8) if acb else 0.8
|
|||
|
|
cut = acb.get("cut", 0.0) if acb else 0.0
|
|||
|
|
w750_thr = acb.get("w750_threshold", 0.0) if acb else 0.0
|
|||
|
|
acb_date = acb.get("date", "?") if acb else "?"
|
|||
|
|
boost_c = "green" if boost >= 1.5 else ("yellow" if boost >= 1.0 else "red")
|
|||
|
|
cut_c = "red" if cut > 0 else "dim"
|
|||
|
|
self._w("#p_acb").update(
|
|||
|
|
f"[bold]ACB[/bold] [{acb_ac}]{acb_date}[/{acb_ac}]\n"
|
|||
|
|
f"boost:[{boost_c}]{boost:.2f}x[/{boost_c}]"
|
|||
|
|
f" β={beta:.2f}"
|
|||
|
|
f" cut:[{cut_c}]{cut:.2f}[/{cut_c}]\n"
|
|||
|
|
f"w750_thr:{w750_thr:.4f}\n"
|
|||
|
|
f"[dim]factors: dvol={acb.get('factors',{}).get('dvol_btc','?') if acb else '?'}"
|
|||
|
|
f" fng={acb.get('factors',{}).get('fng','?') if acb else '?'}[/dim]\n"
|
|||
|
|
f"[dim]age:{acb_age} cfg:{acb.get('config_used','?') if acb else '?'}[/dim]"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ── MC FOREWARNER FOOTER ─────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
# --- Session capital tracking (for live ROI + DD) ---
|
|||
|
|
cur_cap = float((cap or {}).get("capital", 0.0)) if cap else 0.0
|
|||
|
|
if cur_cap > 0:
|
|||
|
|
if self._session_start_cap is None:
|
|||
|
|
self._session_start_cap = cur_cap
|
|||
|
|
if self._cap_peak is None or cur_cap > self._cap_peak:
|
|||
|
|
self._cap_peak = cur_cap
|
|||
|
|
|
|||
|
|
eng_snap = _S.get("hz.engine_snapshot", {}) or {}
|
|||
|
|
trades_ex = eng_snap.get("trades_executed", None)
|
|||
|
|
|
|||
|
|
# Live ROI and session DD (computable from capital alone)
|
|||
|
|
live_roi: Optional[float] = None
|
|||
|
|
live_dd: Optional[float] = None
|
|||
|
|
if cur_cap > 0 and self._session_start_cap and self._session_start_cap > 0:
|
|||
|
|
live_roi = (cur_cap - self._session_start_cap) / self._session_start_cap
|
|||
|
|
if cur_cap > 0 and self._cap_peak and self._cap_peak > 0 and cur_cap < self._cap_peak:
|
|||
|
|
live_dd = (self._cap_peak - cur_cap) / self._cap_peak
|
|||
|
|
|
|||
|
|
# MC envelope fields
|
|||
|
|
prob = float((mc or {}).get("catastrophic_prob", 0.0))
|
|||
|
|
env = float((mc or {}).get("envelope_score", 0.0))
|
|||
|
|
champ_p = (mc or {}).get("champion_probability", None)
|
|||
|
|
pred_roi = (mc or {}).get("predicted_roi", None)
|
|||
|
|
pred_roi_lo= (mc or {}).get("predicted_roi_p10", None)
|
|||
|
|
pred_roi_hi= (mc or {}).get("predicted_roi_p90", None)
|
|||
|
|
pred_dd = (mc or {}).get("predicted_max_dd", None)
|
|||
|
|
mc_warns = (mc or {}).get("warnings", [])
|
|||
|
|
mc_ts = (mc or {}).get("timestamp", None)
|
|||
|
|
sc = _MC.get(mc_st, "dim")
|
|||
|
|
self._prob_hist.append(prob)
|
|||
|
|
|
|||
|
|
# Staleness: time since last 4h MC run
|
|||
|
|
mc_age_str = "—"
|
|||
|
|
if mc_ts:
|
|||
|
|
try:
|
|||
|
|
mc_dt = datetime.fromisoformat(mc_ts.replace("Z", "+00:00"))
|
|||
|
|
age_s = (datetime.now(timezone.utc) - mc_dt).total_seconds()
|
|||
|
|
age_m = int(age_s // 60)
|
|||
|
|
mc_age_str = f"{age_m//60}h{age_m%60:02d}m ago" if age_m >= 60 else f"{age_m}m ago"
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# Title row
|
|||
|
|
self._w("#mc_title").update(
|
|||
|
|
f"[bold cyan]⚡ MC-FOREWARNER RISK MANIFOLD[/bold cyan]"
|
|||
|
|
f" [{sc}]▶ {mc_st}[/{sc}]"
|
|||
|
|
f" [dim]src:{(mc or {}).get('source','N/A')} assessed:{mc_age_str} next:~4h cadence[/dim]"
|
|||
|
|
if mc else
|
|||
|
|
"[bold cyan]⚡ MC-FOREWARNER RISK MANIFOLD[/bold cyan]"
|
|||
|
|
" [yellow]awaiting HZ data (Prefect 4h schedule)[/yellow]"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Left: big cat.prob digits + status summary
|
|||
|
|
self._w("#mc_digits", Digits).update(f"{prob:.3f}")
|
|||
|
|
status_str = {"GREEN": "🟢 SAFE", "ORANGE": "🟡 CAUTION", "RED": "🔴 DANGER"}.get(mc_st, "⚪ N/A")
|
|||
|
|
champ_str = f"champ:{champ_p*100:.0f}%" if champ_p is not None else "champ:—"
|
|||
|
|
self._w("#mc_status").update(
|
|||
|
|
f"[{sc}]{status_str}[/{sc}]\n"
|
|||
|
|
f"[dim]cat.prob {champ_str}[/dim]\n"
|
|||
|
|
f"[dim]env:{env:.3f}[/dim]"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Center tier 1: catastrophic_prob bar
|
|||
|
|
pb = self._w("#mc_prob_bar", ProgressBar)
|
|||
|
|
pb.progress = int(prob * 100)
|
|||
|
|
pb.remove_class("-danger", "-warning")
|
|||
|
|
if prob >= 0.30: pb.add_class("-danger")
|
|||
|
|
elif prob >= 0.10: pb.add_class("-warning")
|
|||
|
|
self._w("#mc_prob_label").update(
|
|||
|
|
f"[dim]cat.prob[/dim] [{sc}]{prob:.4f}[/{sc}]"
|
|||
|
|
f" [green]<0.10 OK[/green] [yellow]<0.30 WARN[/yellow] [red]≥0.30 CRIT[/red]"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# envelope_score bar (inverted: low = danger)
|
|||
|
|
eb = self._w("#mc_env_bar", ProgressBar)
|
|||
|
|
eb.progress = int(env * 100)
|
|||
|
|
eb.remove_class("-danger", "-warning")
|
|||
|
|
if env < 0.40: eb.add_class("-danger")
|
|||
|
|
elif env < 0.70: eb.add_class("-warning")
|
|||
|
|
self._w("#mc_env_label").update(
|
|||
|
|
f"[dim]env.score[/dim] [green]{env:.4f}[/green]"
|
|||
|
|
f" [red]<0.40 DANGER[/red] [yellow]<0.70 CAUTION[/yellow] [green]≥0.70 SAFE[/green]"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# champion_probability bar
|
|||
|
|
chp_val = champ_p if champ_p is not None else 0.0
|
|||
|
|
cb = self._w("#mc_champ_bar", ProgressBar)
|
|||
|
|
cb.progress = int(chp_val * 100)
|
|||
|
|
cb.remove_class("-danger", "-warning")
|
|||
|
|
if chp_val < 0.30: cb.add_class("-danger")
|
|||
|
|
elif chp_val < 0.60: cb.add_class("-warning")
|
|||
|
|
self._w("#mc_champ_label").update(
|
|||
|
|
f"[dim]champ.prob[/dim] "
|
|||
|
|
+ (f"[green]{champ_p*100:.1f}%[/green]" if champ_p is not None else "[dim]—[/dim]")
|
|||
|
|
+ " [green]>60% GOOD[/green] [yellow]>30% MARGINAL[/yellow] [red]<30% RISK[/red]"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Center tier 2: live performance vs MC predicted bounds
|
|||
|
|
def _pct(v):
|
|||
|
|
return f"{v*100:+.1f}%" if v is not None else "—"
|
|||
|
|
def _edge_bar(live, lo, hi, width=18, invert=False):
|
|||
|
|
"""ASCII distance-to-edge bar. live/lo/hi all same units (fraction)."""
|
|||
|
|
if live is None or lo is None or hi is None:
|
|||
|
|
return "[dim]" + "·" * width + "[/dim]"
|
|||
|
|
span = hi - lo
|
|||
|
|
if span <= 0:
|
|||
|
|
return "[dim]?[/dim]"
|
|||
|
|
pos = max(0.0, min(1.0, (live - lo) / span))
|
|||
|
|
if invert: # for DD: high = closer to danger
|
|||
|
|
pos = 1.0 - pos
|
|||
|
|
idx = int(pos * (width - 1))
|
|||
|
|
bar = "·" * idx + "|" + "·" * (width - 1 - idx)
|
|||
|
|
col = "green" if pos > 0.40 else ("yellow" if pos > 0.15 else "red")
|
|||
|
|
return f"[{col}]{bar}[/{col}]"
|
|||
|
|
|
|||
|
|
# ROI line — MC hard thresholds from mc_metrics.py:
|
|||
|
|
# strongly_profitable (champion gate) : ROI > +30%
|
|||
|
|
# L_catastrophic : ROI < -30%
|
|||
|
|
# Bar spans -30% → +30% (champion gate); position = distance from catastrophic edge
|
|||
|
|
_ROI_CRIT = -0.30 # catastrophic lower bound
|
|||
|
|
_ROI_CHAMP = 0.30 # champion gate (strongly_profitable)
|
|||
|
|
roi_bar = _edge_bar(live_roi, _ROI_CRIT, _ROI_CHAMP) if live_roi is not None else _edge_bar(None, None, None)
|
|||
|
|
roi_live = _pct(live_roi)
|
|||
|
|
roi_mc = (f"MC p10-p90:[{_pct(pred_roi_lo)},{_pct(pred_roi_hi)}]"
|
|||
|
|
if pred_roi_lo is not None else "MC:—")
|
|||
|
|
roi_line = f"[dim]ROI [/dim]{roi_live:>8} {roi_bar} [dim]{roi_mc} champ:>{_pct(_ROI_CHAMP)}[/dim]"
|
|||
|
|
|
|||
|
|
# DD line — MC hard thresholds from mc_metrics.py:
|
|||
|
|
# drawdown_ok (champion gate) : DD < 20% ← crossing here kills champion_prob
|
|||
|
|
# L_catastrophic : DD > 40% ← crossing here spikes catastrophic_prob
|
|||
|
|
# Bar spans 0% → 40% (catastrophic boundary); 20% marker = champion gate
|
|||
|
|
_DD_CHAMP = 0.20 # champion region gate
|
|||
|
|
_DD_CRIT = 0.40 # catastrophic label threshold
|
|||
|
|
dd_bar = _edge_bar(live_dd, 0.0, _DD_CRIT, invert=True) if live_dd is not None else _edge_bar(None, None, None)
|
|||
|
|
dd_live = f"{live_dd*100:.1f}%" if live_dd is not None else "—"
|
|||
|
|
dd_champ_dist = f"edge:{(_DD_CHAMP - live_dd)*100:.1f}%rm" if live_dd is not None else ""
|
|||
|
|
dd_pred_str = f"champ gate:<{_DD_CHAMP*100:.0f}% crit:>{_DD_CRIT*100:.0f}% {dd_champ_dist}"
|
|||
|
|
dd_line = f"[dim]DD [/dim]{dd_live:>8} {dd_bar} [dim]{dd_pred_str}[/dim]"
|
|||
|
|
|
|||
|
|
# ── WR / PF / Sharpe / Calmar — from DOLPHIN_PNL_BLUE when wired ──────
|
|||
|
|
pnl_blue = _S.get("hz.pnl_blue") or {}
|
|||
|
|
def _lm(key, fmt="{:.3f}"):
|
|||
|
|
v = pnl_blue.get(key)
|
|||
|
|
return fmt.format(v) if v is not None else "—"
|
|||
|
|
|
|||
|
|
if pnl_blue:
|
|||
|
|
# Ingest per-trade MAE list if present (newest-first list)
|
|||
|
|
raw_mae_list = pnl_blue.get("trade_mae_history") or []
|
|||
|
|
if raw_mae_list:
|
|||
|
|
# Only extend with values not yet in deque (check by length delta)
|
|||
|
|
new_count = max(0, len(raw_mae_list) - len(self._mae_deque))
|
|||
|
|
for v in raw_mae_list[-new_count:]:
|
|||
|
|
try: self._mae_deque.append(float(v))
|
|||
|
|
except Exception: pass
|
|||
|
|
|
|||
|
|
wr_line = (
|
|||
|
|
f"[dim]WR[/dim] {_lm('win_rate','{:.1%}'):>7} "
|
|||
|
|
f"[dim]PF:{_lm('profit_factor','{:.2f}')} "
|
|||
|
|
f"Sh:{_lm('sharpe','{:.2f}')} "
|
|||
|
|
f"Cal:{_lm('calmar','{:.2f}')}[/dim]"
|
|||
|
|
) if pnl_blue else (
|
|||
|
|
f"[dim]WR/PF/Sharpe/Calmar — awaiting DOLPHIN_PNL_BLUE[/dim]"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ── MAE rolling windows ──────────────────────────────────────────────
|
|||
|
|
# MAE = max adverse excursion per trade (for SHORTs: max price rise above entry)
|
|||
|
|
# Windows: last 1 / 5 / 10 / 25 / 75 / 100 / 250 / 500 trades
|
|||
|
|
_MAE_WINDOWS = (1, 5, 10, 25, 75, 100, 250, 500)
|
|||
|
|
mae_list = list(self._mae_deque) # O(n) copy, done once per render
|
|||
|
|
def _mae_window(n):
|
|||
|
|
sl = mae_list[-n:] if len(mae_list) >= n else mae_list
|
|||
|
|
return (max(sl) if sl else None)
|
|||
|
|
|
|||
|
|
if mae_list:
|
|||
|
|
parts = []
|
|||
|
|
for w in _MAE_WINDOWS:
|
|||
|
|
v = _mae_window(w)
|
|||
|
|
if v is None:
|
|||
|
|
parts.append(f"[dim]{w}:—[/dim]")
|
|||
|
|
else:
|
|||
|
|
col = "green" if v < 0.005 else ("yellow" if v < 0.015 else "red")
|
|||
|
|
parts.append(f"[{col}]{w}:{v*100:.2f}%[/{col}]")
|
|||
|
|
mae_line1 = "[dim]MAE max/window [/dim]" + " ".join(parts[:4])
|
|||
|
|
mae_line2 = " " + " ".join(parts[4:])
|
|||
|
|
else:
|
|||
|
|
src = "DOLPHIN_PNL_BLUE" if not pnl_blue else "trade_mae_history"
|
|||
|
|
mae_line1 = f"[dim]MAE — awaiting {src}[/dim]"
|
|||
|
|
mae_line2 = ""
|
|||
|
|
|
|||
|
|
# ── Leverage slider ──────────────────────────────────────────────────
|
|||
|
|
# Sources: engine_snapshot (leverage_soft_cap, leverage_abs_cap, current_leverage)
|
|||
|
|
# ACB boost (scales effective ceiling)
|
|||
|
|
# Zones: 1x–5x GREEN (MC champion ref) 5x–8x YELLOW (soft cap) 8x–9x RED (abs cap)
|
|||
|
|
_LEV_CHAMP = 5.0 # MC champion reference leverage (mc_sampler CHAMPION max_leverage)
|
|||
|
|
_LEV_SOFT = float(eng_snap.get("leverage_soft_cap", 8.0))
|
|||
|
|
_LEV_ABS = float(eng_snap.get("leverage_abs_cap", 9.0))
|
|||
|
|
cur_lev = float(eng_snap.get("current_leverage", 0.0))
|
|||
|
|
acb_boost_v= float((acb or {}).get("boost", 1.0))
|
|||
|
|
# Effective ceiling = base soft cap × ACB boost, clamped to abs cap
|
|||
|
|
eff_ceil = min(_LEV_SOFT * acb_boost_v, _LEV_ABS)
|
|||
|
|
|
|||
|
|
# Build the slider bar (width=40 characters, range 0→_LEV_ABS)
|
|||
|
|
_W = 40
|
|||
|
|
def _lev_bar(cur, ceil, soft, champ, abs_cap, width=_W):
|
|||
|
|
"""
|
|||
|
|
Zones (characters proportional to leverage range 0→abs_cap):
|
|||
|
|
0 ──── champ: green fill / green dot
|
|||
|
|
champ ─ soft: yellow fill / yellow dot
|
|||
|
|
soft ── abs: red fill / red dot
|
|||
|
|
Current position: ◈ (bold white)
|
|||
|
|
Ceiling: ┤ marker
|
|||
|
|
"""
|
|||
|
|
scale = width / abs_cap
|
|||
|
|
# Build char array
|
|||
|
|
chars = []
|
|||
|
|
for i in range(width):
|
|||
|
|
lev_at = (i + 0.5) / scale
|
|||
|
|
if lev_at <= champ: chars.append(("█", "green"))
|
|||
|
|
elif lev_at <= soft: chars.append(("▓", "yellow"))
|
|||
|
|
else: chars.append(("░", "red"))
|
|||
|
|
|
|||
|
|
# Place ceiling marker
|
|||
|
|
ceil_idx = min(width - 1, int(ceil * scale))
|
|||
|
|
c, _ = chars[ceil_idx]
|
|||
|
|
chars[ceil_idx] = ("┤", "bold white")
|
|||
|
|
|
|||
|
|
# Place current leverage cursor (if > 0)
|
|||
|
|
if cur > 0:
|
|||
|
|
cur_idx = min(width - 1, int(cur * scale))
|
|||
|
|
chars[cur_idx] = ("◈", "bold cyan")
|
|||
|
|
|
|||
|
|
# Render using run-length encoding for performance
|
|||
|
|
out = ""
|
|||
|
|
prev_col = None
|
|||
|
|
seg = ""
|
|||
|
|
for ch, col in chars:
|
|||
|
|
if col != prev_col:
|
|||
|
|
if seg and prev_col:
|
|||
|
|
out += f"[{prev_col}]{seg}[/{prev_col}]"
|
|||
|
|
seg = ch
|
|||
|
|
prev_col = col
|
|||
|
|
else:
|
|||
|
|
seg += ch
|
|||
|
|
if seg and prev_col:
|
|||
|
|
out += f"[{prev_col}]{seg}[/{prev_col}]"
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
lev_bar_str = _lev_bar(cur_lev, eff_ceil, _LEV_SOFT, _LEV_CHAMP, _LEV_ABS)
|
|||
|
|
cur_lev_str = f"[bold cyan]{cur_lev:.2f}x[/bold cyan]" if cur_lev > 0 else "[dim]—[/dim]"
|
|||
|
|
ceil_col = "green" if eff_ceil <= _LEV_CHAMP else ("yellow" if eff_ceil <= _LEV_SOFT else "red")
|
|||
|
|
lev_line1 = (
|
|||
|
|
f"[dim]LEV[/dim] [{ceil_col}]{eff_ceil:.1f}x[/{ceil_col}][dim]┤[/dim] "
|
|||
|
|
f"{lev_bar_str} "
|
|||
|
|
f"[dim]|{_LEV_ABS:.0f}x[/dim] cur:{cur_lev_str} acb:×{acb_boost_v:.3f}"
|
|||
|
|
)
|
|||
|
|
lev_line2 = (
|
|||
|
|
f"[dim] 1x{'─'*int((_LEV_CHAMP-1)/_LEV_ABS*_W-2)}"
|
|||
|
|
f"5x(champ){'─'*int((_LEV_SOFT-_LEV_CHAMP)/_LEV_ABS*_W-9)}"
|
|||
|
|
f"8x(soft){'─'*int((_LEV_ABS-_LEV_SOFT)/_LEV_ABS*_W-8)}9x(abs)[/dim]"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
trades_line = (
|
|||
|
|
f"[dim]trades:{trades_ex if trades_ex is not None else '—'} "
|
|||
|
|
f"start:${self._session_start_cap:,.0f} "
|
|||
|
|
f"cur:${cur_cap:,.0f}[/dim]"
|
|||
|
|
) if cur_cap > 0 else "[dim]awaiting capital[/dim]"
|
|||
|
|
|
|||
|
|
self._w("#mc_live").update(
|
|||
|
|
f"{roi_line}\n{dd_line}\n{wr_line}\n"
|
|||
|
|
f"{mae_line1}\n{mae_line2}\n"
|
|||
|
|
f"{lev_line1}\n{lev_line2}\n"
|
|||
|
|
f"{trades_line}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Right column: cat.prob sparkline + MAE sparkline + legend
|
|||
|
|
self._w("#mc_spark_lbl").update(
|
|||
|
|
f"[dim]cat.prob [{min(self._prob_hist):.3f}–{max(self._prob_hist):.3f}][/dim]"
|
|||
|
|
)
|
|||
|
|
self._w("#mc_spark", Sparkline).data = list(self._prob_hist)
|
|||
|
|
self._w("#mc_mae_lbl").update(
|
|||
|
|
f"[dim]MAE hist (n={len(mae_list)})[/dim]"
|
|||
|
|
)
|
|||
|
|
self._w("#mc_mae_spark", Sparkline).data = mae_list[-40:] if mae_list else [0.0]
|
|||
|
|
warn_str = ("\n[yellow]⚠ " + mc_warns[0] + "[/yellow]") if mc_warns else ""
|
|||
|
|
self._w("#mc_legend").update(
|
|||
|
|
"[bold]MC THRESHOLDS[/bold]\n"
|
|||
|
|
"[green]GREEN[/green] cat < 0.10\n"
|
|||
|
|
"[yellow]ORANGE[/yellow] cat < 0.30\n"
|
|||
|
|
"[red]RED[/red] cat ≥ 0.30\n"
|
|||
|
|
f"[dim]DD gate: <20%[/dim]\n"
|
|||
|
|
f"[dim]DD crit: >40%[/dim]"
|
|||
|
|
+ warn_str
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ── TEST RESULTS FOOTER ──────────────────────────────────────────────
|
|||
|
|
if self._test_vis:
|
|||
|
|
tr = self._read_json(_TEST_JSON) or {}
|
|||
|
|
def _tr_badge(cat):
|
|||
|
|
info = tr.get(cat, {})
|
|||
|
|
if not info: return f"[dim]{cat[:12]}:n/a[/dim]"
|
|||
|
|
p, f = info.get("passed",0), info.get("failed",0)
|
|||
|
|
t = p + f
|
|||
|
|
c = "green" if f == 0 else ("yellow" if f <= 2 else "red")
|
|||
|
|
ts = info.get("ts", "?")[:10]
|
|||
|
|
return f"[{c}]{cat[:10]}:{p}/{t}[/{c}][dim]@{ts}[/dim]"
|
|||
|
|
cats = ["data_integrity","finance_fuzz","signal_fill","degradation","actor"]
|
|||
|
|
badges = " ".join(_tr_badge(c) for c in cats)
|
|||
|
|
last_run = tr.get("_run_at", "never")
|
|||
|
|
self._w("#test_footer").update(
|
|||
|
|
f"[bold dim]TESTS[/bold dim] [dim]last:{last_run}[/dim]\n"
|
|||
|
|
f"{badges}\n"
|
|||
|
|
f"[dim]update: prod/run_logs/test_results_latest.json[/dim]"
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
self._w("#test_footer").update("")
|
|||
|
|
|
|||
|
|
# ── LOG (hidden by default) ───────────────────────────────────────────
|
|||
|
|
if self._log_vis:
|
|||
|
|
hz_err = _S.get("hz_err", "")
|
|||
|
|
pf_err = _S.get("prefect_err", "")
|
|||
|
|
self._w("#p_log").update(
|
|||
|
|
f"[bold]LOG[/bold] (l=hide)\n"
|
|||
|
|
f"[dim]{now_str}[/dim] scan=#{scan_no} vel={vel_div:+.5f}\n"
|
|||
|
|
f"hz_err:{hz_err or 'none'} pf_err:{pf_err or 'none'}\n"
|
|||
|
|
f"[dim]state keys: {len(_S._d)}[/dim]"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ── Actions ──────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def action_force_refresh(self) -> None: self._update()
|
|||
|
|
|
|||
|
|
def action_toggle_log(self) -> None:
|
|||
|
|
self._log_vis = not self._log_vis
|
|||
|
|
self.query_one("#log_row").display = self._log_vis
|
|||
|
|
|
|||
|
|
def action_toggle_tests(self) -> None:
|
|||
|
|
self._test_vis = not self._test_vis
|
|||
|
|
self._update()
|
|||
|
|
|
|||
|
|
# ── Helpers ──────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _w(self, selector: str, widget_type=Static):
|
|||
|
|
return self.query_one(selector, widget_type)
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _read_json(path: Path) -> Optional[dict]:
|
|||
|
|
try:
|
|||
|
|
return json.loads(path.read_text())
|
|||
|
|
except Exception:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
# Test-result writer (called by CI / manual test scripts)
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
def write_test_results(results: dict):
|
|||
|
|
"""
|
|||
|
|
Called by test scripts to update the TUI test footer.
|
|||
|
|
results = {
|
|||
|
|
"data_integrity": {"passed": 15, "failed": 0, "ts": "2026-04-05T10:00"},
|
|||
|
|
"degradation": {"passed": 12, "failed": 0, "ts": "2026-04-05T10:01"},
|
|||
|
|
...
|
|||
|
|
"_run_at": "2026-04-05T10:01:42Z"
|
|||
|
|
}
|
|||
|
|
"""
|
|||
|
|
_TEST_JSON.parent.mkdir(parents=True, exist_ok=True)
|
|||
|
|
_TEST_JSON.write_text(json.dumps(results, indent=2))
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
# Entry point
|
|||
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
DolphinTUI().run()
|