Files
DOLPHIN/Observability/TUI/dolphin_tui_v3.py

1054 lines
52 KiB
Python
Raw Normal View History

#!/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: 1x5x GREEN (MC champion ref) 5x8x YELLOW (soft cap) 8x9x 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 0abs_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()