Files
DOLPHIN/Observability/TUI/dolphin_tui_v5.py

741 lines
38 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
DOLPHIN TUI v4
==============
Fixes vs v3:
SYS HEALTH: tdr/scb labels expanded to full service names
ALPHA ENGINE: falls back to engine_snapshot when DOLPHIN_SAFETY is empty
MC-FOREWARNER: graceful "not yet run" display; shows last-run age; no crash on absent key
Version number shown in header after MC status
Run: source /home/dolphin/siloqy_env/bin/activate && python dolphin_tui_v4.py
Keys: q=quit r=force-refresh l=toggle log t=toggle test footer
"""
# ── stdlib ────────────────────────────────────────────────────────────────────
import asyncio, json, math, threading, 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
TUI_VERSION = "TUI v5"
_META_JSON = Path("/mnt/dolphinng5_predict/run_logs/meta_health.json")
_CAPITAL_JSON = Path("/tmp/dolphin_capital_checkpoint.json")
# Path relative to this file: Observability/TUI/ → ../../run_logs/
_TEST_JSON = Path(__file__).parent.parent.parent / "run_logs" / "test_results_latest.json"
_OBF_SYMS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT"]
_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 ─────────────────────────────────────────────────────────
class _State:
def __init__(self):
self._l = threading.Lock(); self._d: Dict[str, Any] = {}
def put(self, k, v):
with self._l: self._d[k] = v
def get(self, k, default=None):
with self._l: return self._d.get(k, default)
def update(self, m):
with self._l: self._d.update(m)
_S = _State()
def _ingest(key, raw):
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):
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)
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)
for map_name, key, state_key in [
("DOLPHIN_META_HEALTH", "latest", "meta_health"),
("DOLPHIN_SAFETY", "latest", "safety"),
("DOLPHIN_HEARTBEAT", "nautilus_flow_heartbeat", "heartbeat"),
]:
m = client.get_map(map_name).blocking()
_ingest(state_key, m.get(key))
def _cb(e, sk=state_key, ek=key):
if e.key == ek: _ingest(sk, e.value)
m.add_entry_listener(include_value=True, updated=_cb, added=_cb)
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)
try:
pm = client.get_map("DOLPHIN_PNL_BLUE").blocking()
_ingest("pnl_blue", pm.get("session_perf"))
def _pnl(e):
if e.key == "session_perf": _ingest("pnl_blue", e.value)
pm.add_entry_listener(include_value=True, updated=_pnl, added=_pnl)
except Exception: pass
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()
async def prefect_poll_loop():
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)
# ── Helpers ───────────────────────────────────────────────────────────────────
def _age(ts):
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, warn=15, dead=60):
s = time.time() - ts if ts else 9999
return "red" if s > dead else ("yellow" if s > warn else "green")
def _bar(v, width=12):
v = max(0.0, min(1.0, v))
f = round(v * width)
return "" * f + "" * (width - f)
def _fmt_vel(v):
return "---" if v is None else f"{float(v):+.5f}"
def _dot(state):
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):
c = _PC.get(p, "dim")
return f"[{c}]{p}[/{c}]"
def _col(v, c): return f"[{c}]{v}[/{c}]"
def _eigen_from_scan(scan):
if not scan: return {}
r = scan.get("result", scan)
mwr_raw = r.get("multi_window_results", {})
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", {})
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_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),
"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; }
#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; }
#mc_live { height: 8; }
#mc_spark_lbl { height: 1; }
#mc_spark { height: 2; }
#mc_mae_lbl { height: 1; }
#mc_mae_spark { height: 2; }
#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; }
"""
# ── 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; _mae_deque: deque
_session_start_cap: Optional[float] = None
_cap_peak: Optional[float] = None
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"):
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")
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._mae_deque = deque(maxlen=500)
start_hz_listener(on_scan=lambda: self.call_from_thread(self._update))
self.run_worker(prefect_poll_loop(), name="prefect-poll", exclusive=True)
self.set_interval(1.0, self._update)
self._update()
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)
mh = _S.get("hz.meta_health") or self._read_json(_META_JSON)
safe = _S.get("hz.safety") or {}
scan = _S.get("hz.latest_eigen_scan") or {}
exf = _S.get("hz.exf_latest") or {}
acb = _S.get("hz.acb_boost") or {}
obf_u = _S.get("hz.obf_universe_latest") or {}
mc = _S.get("hz.mc_forewarner_latest") or {}
cap = _S.get("hz.capital") or self._read_json(_CAPITAL_JSON) or {}
hb = _S.get("hz.heartbeat") or {}
eng = _S.get("hz.engine_snapshot") or {}
eigen = _eigen_from_scan(scan)
# ── posture: DOLPHIN_SAFETY first, fall back to engine_snapshot ───────
posture = safe.get("posture") or eng.get("posture") or "?"
# ── Rm / breakdown: DOLPHIN_SAFETY first, fall back to zeros ─────────
rm_s = float(safe.get("Rm", 0.0))
bd = safe.get("breakdown") or {}
# If DOLPHIN_SAFETY is empty but engine_snapshot has posture, show that
safety_live = bool(safe.get("posture") or safe.get("Rm"))
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")
pc_col = _PC.get(posture, "dim")
hz_tag = "[green][HZ✓][/green]" if hz_up else "[red][HZ✗][/red]"
mc_st = mc.get("status", "N/A") if mc else "N/A"
mc_col = _MC.get(mc_st, "dim")
# ── HEADER ────────────────────────────────────────────────────────────
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}]"
f" [dim]{TUI_VERSION}[/dim]\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)
btc_p = eigen.get("btc_price")
btc_str = f"BTC:[cyan]${btc_p:,.0f}[/cyan] " if btc_p else ""
trades_ex= eng.get("trades_executed")
last_vd = eng.get("last_vel_div")
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}\n"
f" vel_div:[{vc}]{vel_div:+.5f}[/{vc}] thr:[dim]-0.02000[/dim]"
f" cap:[cyan]${cap_val:,.0f}[/cyan]"
f" trades:{trades_ex if trades_ex is not None else ''}\n"
f" last_vel_div:[dim]{last_vd:+.5f}[/dim] open-pos:[dim]DOLPHIN_PNL_BLUE[/dim]"
if last_vd is not None else
f" vel_div:[{vc}]{vel_div:+.5f}[/{vc}] thr:[dim]-0.02000[/dim]"
f" cap:[cyan]${cap_val:,.0f}[/cyan]"
f" trades:{trades_ex if trades_ex is not None else ''}\n"
f" open-pos:[dim]DOLPHIN_PNL_BLUE (not yet wired)[/dim]"
)
# ── SYS HEALTH — FIX: expand tdr/scb labels ──────────────────────────
if mh:
svc = mh.get("service_status", {})
hz_ks= mh.get("hz_key_status", {})
def _svc(nm, label):
st = svc.get(nm, "?")
dot = "[green]●[/green]" if st == "RUNNING" else "[red]●[/red]"
return f"{dot}[dim]{label}[/dim]"
def _hz_dot(nm):
sc = hz_ks.get(nm, {}).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.get('m4_control_plane',0):.2f}"
f" m3:{mh.get('m3_data_freshness',0):.2f}"
f" m5:{mh.get('m5_coherence',0):.2f}\n"
f"{_svc('dolphin_data:exf_fetcher','exf')}"
f" {_svc('dolphin_data:acb_processor','acb')}"
f" {_svc('dolphin_data:obf_universe','obf')}\n"
f"{_svc('dolphin:nautilus_trader','trader')}"
f" {_svc('dolphin:scan_bridge','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 — FIX: fall back to engine_snapshot when safety empty ─
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}]"
if safety_live:
self._w("#p_alpha").update(
f"[bold]ALPHA ENGINE[/bold] {_posture_markup(posture)}\n"
f"Rm:[{pc_col}]{_bar(rm_s,14)}[/{pc_col}]{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]"
)
else:
# DOLPHIN_SAFETY empty — show what we have from engine_snapshot
bars_idx = eng.get("bar_idx", "?")
scans_p = eng.get("scans_processed", "?")
self._w("#p_alpha").update(
f"[bold]ALPHA ENGINE[/bold] {_posture_markup(posture)}\n"
f"[yellow]DOLPHIN_SAFETY empty[/yellow]\n"
f"posture from engine_snapshot\n"
f"bar:{bars_idx} scans:{scans_p}\n"
f"[dim]Rm/Cat1-5: awaiting DOLPHIN_SAFETY[/dim]"
)
# ── SCAN ──────────────────────────────────────────────────────────────
scan_ac = _age_col(eigen.get("timestamp", 0), 15, 60)
scan_age= _age(eigen.get("timestamp")) if eigen.get("timestamp") else "?"
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")
if exf:
self._w("#p_extf").update(
f"[bold]ExtF[/bold] [{exf_ac}]{ok_cnt}/9 {exf_age}[/{exf_ac}]\n"
f"fund:[cyan]{f_btc:.5f}[/cyan] dvol:[{dvol_c}]{dvol:.1f}[/{dvol_c}]\n"
f"fng:[{fng_c}]{int(fng) if fng else '?'}[/{fng_c}] taker:[yellow]{taker:.3f}[/yellow]\n"
f"ls_btc:{ls_btc:.3f} vix:{vix:.1f}\n"
f"acb✓:{exf.get('_acb_ready','?')}"
)
else:
self._w("#p_extf").update("[bold]ExtF[/bold]\n[dim]no data[/dim]")
# ── 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}] 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 "?"
c5 = bd.get("Cat5", 1.0) if bd else 1.0
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
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.get('date','?') if acb else '?'}[/{acb_ac}]\n"
f"boost:[{boost_c}]{boost:.2f}x[/{boost_c}] β={beta:.2f}"
f" cut:[{cut_c}]{cut:.2f}[/{cut_c}]\n"
f"w750_thr:{acb.get('w750_threshold',0.0) if acb else 0.0:.4f}\n"
f"[dim]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 — FIX: graceful when absent ─────────────────────────
prob = float(mc.get("catastrophic_prob", 0.0)) if mc else 0.0
env = float(mc.get("envelope_score", 0.0)) if mc else 0.0
champ_p = mc.get("champion_probability") if mc else None
mc_ts = mc.get("timestamp") if mc else None
mc_warns = mc.get("warnings", []) if mc else []
sc = _MC.get(mc_st, "dim")
self._prob_hist.append(prob)
# Age since last 4h run
mc_age_str = "never run"
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
mc_present = bool(mc)
self._w("#mc_title").update(
f"[bold cyan]⚡ MC-FOREWARNER RISK MANIFOLD[/bold cyan] {TUI_VERSION}"
+ (f" [{sc}]▶ {mc_st}[/{sc}] [dim]assessed:{mc_age_str} cadence:4h[/dim]"
if mc_present else
" [yellow]⚠ no data yet — Prefect 4h schedule not yet run[/yellow]"
" [dim](mc_forewarner_flow runs every 4h)[/dim]")
)
# Left: digits + status
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[dim]cat.prob {champ_str}[/dim]\n[dim]env:{env:.3f}[/dim]")
if mc_present else
"[yellow]awaiting[/yellow]\n[dim]first run[/dim]\n[dim]in ~4h[/dim]"
)
# Center bars
for bar_id, val, lo_thr, hi_thr, lo_cls, hi_cls, label, fmt in [
("mc_prob_bar", prob, 0.10, 0.30, "-warning", "-danger",
f"[dim]cat.prob[/dim] [{sc}]{prob:.4f}[/{sc}] [green]<0.10 OK[/green] [yellow]<0.30 WARN[/yellow] [red]≥0.30 CRIT[/red]",
int(prob * 100)),
("mc_env_bar", env, 0.40, 0.70, "-danger", "-warning",
f"[dim]env.score[/dim] [green]{env:.4f}[/green] [red]<0.40 DANGER[/red] [yellow]<0.70 CAUTION[/yellow] [green]≥0.70 SAFE[/green]",
int(env * 100)),
]:
pb = self._w(f"#{bar_id}", ProgressBar)
pb.progress = fmt
pb.remove_class("-danger", "-warning")
if val < lo_thr: pb.add_class(lo_cls)
elif val < hi_thr: pb.add_class(hi_cls)
self._w(f"#{bar_id.replace('_bar','_label')}").update(label)
# 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]"
)
# Live performance tier
cur_cap = float(cap.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
live_roi = ((cur_cap - self._session_start_cap) / self._session_start_cap
if cur_cap > 0 and self._session_start_cap else None)
live_dd = ((self._cap_peak - cur_cap) / self._cap_peak
if cur_cap > 0 and self._cap_peak and cur_cap < self._cap_peak else None)
pnl_blue = _S.get("hz.pnl_blue") or {}
def _pct(v): return f"{v*100:+.1f}%" if v is not None else ""
def _lm(k, fmt="{:.3f}"):
v = pnl_blue.get(k); return fmt.format(v) if v is not None else ""
roi_c = "green" if live_roi and live_roi > 0 else ("red" if live_roi and live_roi < -0.10 else "yellow")
dd_c2 = "red" if live_dd and live_dd > 0.20 else ("yellow" if live_dd and live_dd > 0.08 else "green")
self._w("#mc_live").update(
f"[dim]ROI[/dim] [{roi_c}]{_pct(live_roi):>8}[/{roi_c}]"
f" [dim]champ gate:>+30% crit:<-30%[/dim]\n"
f"[dim]DD [/dim] [{dd_c2}]{_pct(live_dd):>8}[/{dd_c2}]"
f" [dim]champ gate:<20% crit:>40%[/dim]\n"
f"[dim]WR:{_lm('win_rate','{:.1%}')} PF:{_lm('profit_factor','{:.2f}')}"
f" Sh:{_lm('sharpe','{:.2f}')} Cal:{_lm('calmar','{:.2f}')}[/dim]\n"
f"[dim]cap:${cur_cap:,.0f} start:${self._session_start_cap:,.0f} "
f"trades:{eng.get('trades_executed','')}[/dim]"
if cur_cap > 0 and self._session_start_cap else
"[dim]awaiting capital data…[/dim]"
)
# Right: sparklines + legend
self._w("#mc_spark_lbl").update(
f"[dim]cat.prob history [{min(self._prob_hist):.3f}{max(self._prob_hist):.3f}][/dim]")
self._w("#mc_spark", Sparkline).data = list(self._prob_hist)
mae_list = list(self._mae_deque)
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"
"[dim]DD gate: <20%[/dim]\n"
"[dim]DD crit: >40%[/dim]" + warn_str
)
# ── TEST FOOTER — schema: {passed, total, status: PASS|FAIL|N/A} ────────
if self._test_vis:
tr = self._read_json(_TEST_JSON) or {}
run_at = tr.get("_run_at", "never")
cats = [
("data_integrity", "data"),
("finance_fuzz", "fuzz"),
("signal_fill", "signal"),
("degradation", "degrad"),
("actor", "actor"),
]
def _badge(key, short):
info = tr.get(key, {})
if not info:
return f"[dim]{short}:n/a[/dim]"
status = info.get("status", "N/A")
passed = info.get("passed")
total = info.get("total")
if status == "N/A" or passed is None:
return f"[dim]{short}:N/A[/dim]"
col = "green" if status == "PASS" else "red"
return f"[{col}]{short}:{passed}/{total}[/{col}]"
badges = " ".join(_badge(k, s) for k, s in cats)
self._w("#test_footer").update(
f"[bold dim]TESTS[/bold dim] [dim]last run: {run_at}[/dim]"
f" [dim]t=toggle r=reload[/dim]\n"
f"{badges}\n"
f"[dim]file: run_logs/test_results_latest.json "
f"API: write_test_results() in dolphin_tui_v5.py[/dim]"
)
else:
self._w("#test_footer").update("")
# ── LOG ───────────────────────────────────────────────────────────────
if self._log_vis:
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:{_S.get('hz_err','none')} pf_err:{_S.get('prefect_err','none')}\n"
f"[dim]state keys:{len(_S._d)} safety_live:{safety_live}[/dim]"
)
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()
def _w(self, selector, widget_type=Static):
return self.query_one(selector, widget_type)
@staticmethod
def _read_json(path):
try: return json.loads(path.read_text())
except Exception: return None
def write_test_results(results: dict):
"""
Update the TUI test footer. Called by test scripts / CI / conftest.py.
Schema:
{
"_run_at": "auto-injected",
"data_integrity": {"passed": 15, "total": 15, "status": "PASS"},
"finance_fuzz": {"passed": null, "total": null, "status": "N/A"},
...
}
status: "PASS" | "FAIL" | "N/A"
Example (conftest.py):
import sys; sys.path.insert(0, "/mnt/dolphinng5_predict/Observability/TUI")
from dolphin_tui_v5 import write_test_results
write_test_results({"data_integrity": {"passed": 15, "total": 15, "status": "PASS"}})
"""
_TEST_JSON.parent.mkdir(parents=True, exist_ok=True)
# Merge with existing file so missing categories are preserved
existing = {}
try:
existing = json.loads(_TEST_JSON.read_text())
except Exception:
pass
existing.update(results)
existing["_run_at"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
_TEST_JSON.write_text(json.dumps(existing, indent=2))
if __name__ == "__main__":
DolphinTUI().run()