feat(dashboard): banner alert operativi del book live (SKH/posizione/equity)
Il dashboard non mostrava nessuno dei tre segnali di health introdotti nei commit31369b3(skh_error) e5670469(pos_error/eq_fallback): finivano solo su Telegram + log cron, invisibili a chi guarda il monitor. pos_error/eq_fallback erano gia' in shadow_report (usato dal dashboard) ma ignorati dal template; skh_error non era nemmeno recuperato (il dashboard usa shadow_report, non book_report). Fix: - _alerts_banner(): helper puro (verde "nessun alert" se pulito, rosso con una riga per errore) che rispecchia i tre alert di book_execute. - build(): raccoglie pos_error/eq_fallback dallo shadow gia' fetchato + skh_error da book_report(offline=True) (feed certificato, nessuna rete extra). - html(): renderizza il banner in cima alla sezione ① LIVE. - tests/test_dashboard.py: +4 (verde, 3 alert, parziale, chiave-ignota). Suite 160/160. Render end-to-end verde (conto sano $598.06 flat). Chiude la copertura: i 3 pattern di errore silenzioso ora visibili su log + Telegram + dashboard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+37
-1
@@ -71,6 +71,20 @@ def build():
|
|||||||
freq = book_trade_frequency() # trade/anno contati (SKH01 round-trip + TP01 turnover)
|
freq = book_trade_frequency() # trade/anno contati (SKH01 round-trip + TP01 turnover)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
freq = {"error": f"{type(e).__name__}: {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(
|
data = dict(
|
||||||
version=APP_VERSION,
|
version=APP_VERSION,
|
||||||
last_data=str(idx[-1].date()),
|
last_data=str(idx[-1].date()),
|
||||||
@@ -78,7 +92,7 @@ def build():
|
|||||||
per_sleeve=bt["per_sleeve"], yearly=bt["yearly"],
|
per_sleeve=bt["per_sleeve"], yearly=bt["yearly"],
|
||||||
positions=pf.current_positions(), spark=spark, paper=paper, prevday=prevday,
|
positions=pf.current_positions(), spark=spark, paper=paper, prevday=prevday,
|
||||||
combo=combo, statarb=statarb, gtaa_weights=gtaa_w, deribit=deribit,
|
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)
|
_CACHE.update(t=time.time(), data=data)
|
||||||
return data
|
return data
|
||||||
@@ -97,6 +111,27 @@ def svg_spark(spark, w=900, h=220):
|
|||||||
f'<polyline fill="none" stroke="#2ecc71" stroke-width="2" points="{" ".join(pts)}"/></svg>')
|
f'<polyline fill="none" stroke="#2ecc71" stroke-width="2" points="{" ".join(pts)}"/></svg>')
|
||||||
|
|
||||||
|
|
||||||
|
_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 = "<br>".join(f"<b>{_ALERT_LABELS.get(k, k)}:</b> <span class=warn>{v}</span>"
|
||||||
|
for k, v in book_alerts.items())
|
||||||
|
return (f"<div class=box style='border-color:#e74c3c;background:#241416'>"
|
||||||
|
f"<b class=r>⚠ ALERT OPERATIVI BOOK LIVE</b><br>{rows}</div>")
|
||||||
|
return ("<div class=box style='border-color:#1f7a3f'>"
|
||||||
|
"<span class=g>✓ Book live: nessun alert</span> "
|
||||||
|
"<span style='color:#8a93a0;font-size:12px'>(feed SKH ok · posizione leggibile · equity reale ok)</span></div>")
|
||||||
|
|
||||||
|
|
||||||
def html():
|
def html():
|
||||||
d = build()
|
d = build()
|
||||||
f, ho = d["full"], d["holdout"]
|
f, ho = d["full"], d["holdout"]
|
||||||
@@ -269,6 +304,7 @@ th{{color:#8a93a0;font-weight:500}}.y{{display:inline-block;background:#161b22;b
|
|||||||
<div class=sub>monitor · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE non attiva (solo micro-test) · ordine: LIVE → trades fatti → storico</div>
|
<div class=sub>monitor · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE non attiva (solo micro-test) · ordine: LIVE → trades fatti → storico</div>
|
||||||
|
|
||||||
<div class="section live">① LIVE — Deribit mainnet (conto reale, sola lettura)</div>
|
<div class="section live">① LIVE — Deribit mainnet (conto reale, sola lettura)</div>
|
||||||
|
{_alerts_banner(d.get("book_alerts") or {})}
|
||||||
<div class=box><b>Shadow TP01</b> (cosa farebbe ORA sul conto reale, nessun ordine inviato):<br>{shadow_html}</div>
|
<div class=box><b>Shadow TP01</b> (cosa farebbe ORA sul conto reale, nessun ordine inviato):<br>{shadow_html}</div>
|
||||||
<p class=warn>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.</p>
|
<p class=warn>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.</p>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user