venue_watch: BLIND ha una severity propria, il titolo non chiede piu' il prelievo (#31)

Con un asset BLIND e nessun ALERT, severity restava None e il titolo del
runner cadeva sul default piu' forte ("PRIMO PASSO: prelievo di prova") per
un episodio che e' "non vedo", non "stress misurato". run_once ora assegna
severity="blind" quando c'e' un BLIND senza ALERT (ALERT resta piu' forte, P9);
_titolo distingue il caso con "venue_watch CIECO — referenze non leggibili,
nessun giudizio sul venue". La ripetizione oraria di BLIND non e' toccata.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 13:38:35 +00:00
parent dda5d41f38
commit 5acc31e390
3 changed files with 88 additions and 0 deletions
+2
View File
@@ -31,6 +31,8 @@ def _titolo(rep: dict) -> str:
un blocco da manutenzione annunciata: il 18/08 sono usciti quattro messaggi identici che
chiedevano di prelevare, tutti dopo che il book aveva gia' ripreso. Un allarme massimo speso
per un evento atteso e' un allarme che non verra' letto il giorno che e' vero."""
if rep.get("severity") == "blind":
return "⚠️ venue_watch CIECO — referenze non leggibili, nessun giudizio sul venue"
if rep.get("severity") == "warn":
return "⚠️ VENUE WATCH — Deribit non operativa, ma spiegata. Nessuna azione"
return "🚨 VENUE WATCH — possibile stress su Deribit. PRIMO PASSO: prelievo di prova"
+5
View File
@@ -346,6 +346,11 @@ def run_once(state_path: Path = STATE_PATH, sender=None) -> dict:
f"({o['n_refs']} referenze, spread {o['ref_spread_bps']})")
if report["levels"] and any(l == "ALERT" for l in report["levels"].values()):
report["severity"] = "alert"
# issue #31: senza ALERT, un asset BLIND lasciava severity=None — il titolo del runner
# cadeva sul default piu' forte ("PRIMO PASSO: prelievo di prova") per un episodio che e'
# "non vedo", non "stress misurato". BLIND e' lo stato MORBIDO: cede il passo ad ALERT.
elif report["levels"] and any(l == "BLIND" for l in report["levels"].values()):
report["severity"] = "blind"
# --- blocco piattaforma: adesso passa dalla stessa disciplina degli altri stati
lock_new, lock_lvl = lock_step(st.lock, obs["platform_locked"], obs.get("maintenance", False))
+81
View File
@@ -499,3 +499,84 @@ def test_senza_sender_il_report_lo_DICE(monkeypatch, tmp_path):
"""'non tentato' non e' 'inviato': il silenzio non deve leggersi come un invio riuscito."""
sp, rep = _porta_ad_alert(monkeypatch, tmp_path, None)
assert "non tentato" in rep["invio"]
# =============================================================================================
# issue #31 — BLIND senza ALERT lasciava severity=None: il titolo del runner cadeva sul default
# piu' forte ("PRIMO PASSO: prelievo di prova") anche per un episodio che e' "non vedo", non
# "stress misurato". BLIND e' lo stato MORBIDO (docstring del modulo).
# =============================================================================================
def test_severity_blind_quando_un_asset_e_cieco_senza_alert(monkeypatch, tmp_path):
sp = tmp_path / "vw.json"
def fake():
return {"ts": 1, "platform_locked": False, "locked_raw": "false", "maintenance": False,
"assets": {a: dict(deribit=1.0, n_refs=1, ref_spread_bps=None, bps=None)
for a in VW.ASSETS},
"deribit_errors": []}
monkeypatch.setattr(VW, "observe", fake)
rep = None
for _ in range(VW.BLIND_ALERT_HOURS):
rep = VW.run_once(state_path=sp, sender=lambda r: (True, "ok"))
assert any(l == "BLIND" for l in rep["levels"].values())
assert not any(l == "ALERT" for l in rep["levels"].values())
assert rep["severity"] == "blind"
def test_severity_alert_prevale_su_blind(monkeypatch, tmp_path):
"""Se un asset e' cieco ma l'altro e' in ALERT vero, la gravita' massima vince (P9).
BTC resta senza referenze per tutto il giro (arriva a BLIND all'ora `BLIND_ALERT_HOURS`);
ETH parte harmless e passa a un vero streak sopra soglia SOLO negli ultimi `PERSIST_HOURS`
round, cosi' che il round in cui ETH scatta ALERT sia lo stesso in cui BTC e' gia' BLIND."""
sp = tmp_path / "vw.json"
letture = {"n": 0}
soglia_eth = VW.BLIND_ALERT_HOURS - VW.PERSIST_HOURS # da qui in poi ETH costruisce lo streak
def fake():
letture["n"] += 1
eth_bps = VW.THRESHOLD_BPS * 3 if letture["n"] > soglia_eth else 1.0
return {"ts": letture["n"], "platform_locked": False, "locked_raw": "false",
"maintenance": False,
"assets": {"BTC": dict(deribit=1.0, n_refs=1, ref_spread_bps=None, bps=None),
"ETH": dict(deribit=1.0, n_refs=3, ref_spread_bps=1.0, bps=eth_bps)},
"deribit_errors": []}
monkeypatch.setattr(VW, "observe", fake)
rep = None
for _ in range(VW.BLIND_ALERT_HOURS):
rep = VW.run_once(state_path=sp, sender=lambda r: (True, "ok"))
assert rep["levels"]["BTC"] == "BLIND"
assert rep["levels"]["ETH"] == "ALERT"
assert rep["severity"] == "alert"
def _carica_script_venue_watch():
"""scripts/live/venue_watch.py (il runner) — via importlib, per non collidere col nome
del modulo `src.live.venue_watch` gia' importato sopra come `VW`."""
import importlib.util
sp = importlib.util.spec_from_file_location(
"venue_watch_script", ROOT / "scripts" / "live" / "venue_watch.py")
m = importlib.util.module_from_spec(sp)
sp.loader.exec_module(m)
return m
def test_titolo_blind_non_dice_prelievo():
SVW = _carica_script_venue_watch()
t = SVW._titolo({"severity": "blind"})
assert "prelievo" not in t.lower()
assert "CIECO" in t
def test_titolo_alert_dice_ancora_prelievo():
SVW = _carica_script_venue_watch()
assert "prelievo" in SVW._titolo({"severity": "alert"}).lower()
assert "prelievo" in SVW._titolo({"severity": None}).lower() # default: nessuna gravita' letta
def test_titolo_warn_resta_quello_di_prima():
SVW = _carica_script_venue_watch()
assert "Nessuna azione" in SVW._titolo({"severity": "warn"})