diff --git a/src/live/dashboard.py b/src/live/dashboard.py index 7fbf77d..9b4ff69 100644 --- a/src/live/dashboard.py +++ b/src/live/dashboard.py @@ -71,6 +71,20 @@ def build(): freq = book_trade_frequency() # trade/anno contati (SKH01 round-trip + TP01 turnover) except Exception as e: freq = {"error": f"{type(e).__name__}: {e}"} + # ALERT operativi del book live (health): pos/equity dallo shadow gia' fetchato, feed SKH da book_report. + book_alerts = {} + if shadow and "error" not in shadow: + if shadow.get("pos_error"): + book_alerts["pos_error"] = shadow["pos_error"] + if shadow.get("eq_fallback"): + book_alerts["eq_fallback"] = shadow["eq_fallback"] + try: + from src.live.book import book_report + skh_err = book_report(offline=True).get("skh_error") # feed certificato (deterministico), no rete extra + if skh_err: + book_alerts["skh_error"] = skh_err + except Exception as e: + book_alerts["book_report"] = f"{type(e).__name__}: {e}" data = dict( version=APP_VERSION, last_data=str(idx[-1].date()), @@ -78,7 +92,7 @@ def build(): per_sleeve=bt["per_sleeve"], yearly=bt["yearly"], positions=pf.current_positions(), spark=spark, paper=paper, prevday=prevday, combo=combo, statarb=statarb, gtaa_weights=gtaa_w, deribit=deribit, - shadow=shadow, trades=trades, freq=freq, bh=None, + shadow=shadow, trades=trades, freq=freq, bh=None, book_alerts=book_alerts, ) _CACHE.update(t=time.time(), data=data) return data @@ -97,6 +111,27 @@ def svg_spark(spark, w=900, h=220): f'') +_ALERT_LABELS = { + "skh_error": "⚠️ Feed SKH01 fallito (sleeve forzato flat)", + "pos_error": "🛑 Posizione reale non leggibile (esecuzione BLOCCATA)", + "eq_fallback": "⚠️ Equity reale non leggibile (sizing su paper_cap)", + "book_report": "🛑 Book report non calcolabile", +} + + +def _alerts_banner(book_alerts: dict) -> str: + """Banner HTML degli alert operativi del book live (health). Verde se nessuno. + Rispecchia esattamente i tre segnali che book_execute manda su Telegram/cron.""" + if book_alerts: + rows = "
".join(f"{_ALERT_LABELS.get(k, k)}: {v}" + for k, v in book_alerts.items()) + return (f"
" + f"⚠ ALERT OPERATIVI BOOK LIVE
{rows}
") + return ("
" + "✓ Book live: nessun alert " + "(feed SKH ok · posizione leggibile · equity reale ok)
") + + def html(): d = build() f, ho = d["full"], d["holdout"] @@ -269,6 +304,7 @@ th{{color:#8a93a0;font-weight:500}}.y{{display:inline-block;background:#161b22;b
monitor · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE non attiva (solo micro-test) · ordine: LIVE → trades fatti → storico
① LIVE — Deribit mainnet (conto reale, sola lettura)
+{_alerts_banner(d.get("book_alerts") or {})}
Shadow TP01 (cosa farebbe ORA sul conto reale, nessun ordine inviato):
{shadow_html}

Solo TP01 e' armato live (cap $300/asset · disaster-SL on-book −30% · capitale reale ≈ $600). SKH01/XS01/VRP01 = paper/stat-mode. Lo "Shadow" mostra il target reale ma NON invia ordini.

diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py new file mode 100644 index 0000000..3b60d75 --- /dev/null +++ b/tests/test_dashboard.py @@ -0,0 +1,44 @@ +"""Test del dashboard (src/live/dashboard.py) — solo la logica pura, niente rete/HTTP. + +Copre il banner degli ALERT operativi del book live: i tre segnali di health (feed SKH, posizione +non leggibile, equity fallback) che book_execute manda su Telegram devono comparire anche sul +dashboard. Verde quando pulito, rosso con le righe quando in errore. +""" +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(PROJECT_ROOT)) + +from src.live.dashboard import _alerts_banner + + +def test_banner_green_when_no_alerts(): + h = _alerts_banner({}) + assert "nessun alert" in h + assert "class=g" in h # stile verde + assert "ALERT OPERATIVI" not in h # nessun banner rosso + + +def test_banner_shows_all_three_alerts(): + h = _alerts_banner({ + "skh_error": "RuntimeError: feed 5m giu", + "pos_error": "posizione non leggibile, assunta FLAT: BTC", + "eq_fallback": "sizing su paper_cap $2,000", + }) + assert "ALERT OPERATIVI BOOK LIVE" in h and "class=r" in h # banner rosso + # ogni alert: label descrittiva + messaggio grezzo + assert "Feed SKH01 fallito" in h and "feed 5m giu" in h + assert "Posizione reale non leggibile" in h and "assunta FLAT: BTC" in h + assert "Equity reale non leggibile" in h and "paper_cap $2,000" in h + + +def test_banner_partial_alert_only_shows_present(): + h = _alerts_banner({"pos_error": "BTC non leggibile"}) + assert "Posizione reale non leggibile" in h and "BTC non leggibile" in h + assert "Feed SKH01" not in h and "Equity reale" not in h # solo quello presente + + +def test_banner_unknown_key_falls_back_to_key_name(): + h = _alerts_banner({"book_report": "ImportError: boom"}) + assert "Book report non calcolabile" in h and "boom" in h