Files
PythagorasGoal/scripts/live/venue_watch.py
T
Adriano 1a7f670966 sei sorveglianti registrano i giri e stanno in monitor_health (Fixes #16)
src/live/giri.registra_giro: una riga jsonl {ts ms, campi} dopo una misura completata,
mai un'eccezione. edge_watch, fee_watch, scale_watch, vrp_f_watch, venue_news (cron_daily)
e venue_watch (cron_book, orario) scrivono data/<dir>/runs.jsonl; niente riga senza misura.
monitor_health: sei MonitorSpec (30h i giornalieri, 3h venue_watch), open_labeled=False.
Stessa riparazione di cc01 (#10, #12), generalizzata. Primo giro notturno: NUOVO, non allerta.
Test: incrocio P1 percorso/RUNS, misura -> una riga, non-misura -> nessuna. Suite: 1098 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 08:48:07 +00:00

117 lines
5.7 KiB
Python

#!/usr/bin/env python
"""venue_watch.py — runner ORARIO del tripwire di venue. Allerta su Telegram, NON blocca nulla.
Vedi `src/live/venue_watch.py` per la taratura (100 bps / 4h a segno costante, zero falsi allarmi
su 8 anni, controllo positivo su Bitfinex 2018-19) e per il RUNBOOK a un allarme.
uv run python scripts/live/venue_watch.py # un giro, stampa il report
uv run python scripts/live/venue_watch.py --quiet # solo in caso di allarme (per il cron)
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live.deribit import check_specs # noqa: E402
from src.live.giri import registra_giro # noqa: E402
from src.live.notifier import notify, ultimo_errore # noqa: E402
from src.live.venue_watch import (PERSIST_HOURS, THRESHOLD_BPS, # noqa: E402
run_once)
RUNS = ROOT / "data" / "venue_watch" / "runs.jsonl"
def _titolo(rep: dict) -> str:
"""⚠️ Il titolo dice cosa FARE, non solo cosa e' successo: chi lo legge sul telefono deve
sapere il primo passo senza aprire il repo (runbook completo in src/live/venue_watch.py).
⚠️ Dal 19/08 DIPENDE dalla gravita'. Prima era sempre il 🚨 col prelievo di prova, anche per
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") == "warn":
return "⚠️ VENUE WATCH — Deribit non operativa, ma spiegata. Nessuna azione"
return "🚨 VENUE WATCH — possibile stress su Deribit. PRIMO PASSO: prelievo di prova"
def _manda(rep: dict) -> tuple[bool, str]:
"""Il sender iniettato in `run_once`: e' lui a decidere se i marcatori 'gia' detto' si
scrivono. `tentativi=3` perche' su questo percorso il 6,9% degli invii si perde, e qui
perderne uno significa perdere l'episodio intero."""
ok = notify(_titolo(rep), {f"alert {i+1}": a for i, a in enumerate(rep["alerts"])},
tentativi=3)
if ok:
return True, "ok"
return False, (ultimo_errore() or "motivo non registrato")
def main() -> int:
quiet = "--quiet" in sys.argv
rep = run_once(sender=_manda)
if not quiet:
print("=" * 78)
print(" VENUE WATCH — scarto Deribit vs consenso USD indipendente")
print("=" * 78)
print(f" soglia: {THRESHOLD_BPS:.0f} bps persistenti {PERSIST_HOURS}h a segno costante")
lock = rep["platform_locked"]
print(f" public/status locked: {rep.get('locked_raw') or 'non leggibile'}"
+ (f" (da {rep['lock_hours']}h"
+ (", manutenzione dichiarata" if rep.get("maintenance") else "") + ")"
if lock else ""))
for a, lvl in rep["levels"].items():
d = rep["detail"][a]
bps = f"{d['bps']:+7.1f} bps" if d["bps"] is not None else " n/d "
print(f" {a:<4} {lvl:<6} {bps} ({d['n_refs']} referenze, "
f"spread {d['ref_spread_bps']:.1f} bps)" if d["ref_spread_bps"] is not None
else f" {a:<4} {lvl:<6} {bps} ({d['n_refs']} referenze)")
# --- specifiche contratto: la tabella dichiarata si e' scostata dal venue?
# ⚠️ Gira qui e non nel book: e' un controllo di SOLA LETTURA e non deve stare sul percorso
# che manda ordini. Il 18/08 Deribit ha cambiato tick e size dei perpetual lineari USDC e
# niente se n'era accorto: e' andata bene perche' i cambi erano riduzioni.
spec = check_specs()
registra_giro(RUNS, livelli=rep["levels"])
if spec["divergenze"]:
rifiuto = [d for d in spec["divergenze"] if d["rischio"] == "rifiuto"]
righe = {f"{d['strumento']} {d['campo']}": f"dichiarato {d['dichiarato']} / "
f"venue {d['venue']} -> {d['rischio']}"
for d in spec["divergenze"]}
if rifiuto:
righe["azione"] = ("il dichiarato e' PIU' FINE del venue: gli ordini verranno "
"RIFIUTATI. Correggi _CONTRACT in src/live/deribit.py adesso.")
notify("🚨 SPEC CONTRATTO — Deribit rifiutera' gli ordini", righe)
else:
righe["azione"] = ("il dichiarato e' piu' GROSSO del venue: gli ordini restano "
"conformi, si perde solo granularita'. Aggiorna _CONTRACT "
"in src/live/deribit.py.")
notify("⚠️ SPEC CONTRATTO — la tabella dichiarata e' vecchia", righe)
for k, v in righe.items():
print(f" SPEC: {k}: {v}")
if rep["alerts"]:
# L'invio l'ha gia' fatto `run_once` attraverso `_manda`: qui si stampa e si riporta
# l'ESITO. Un invio fallito si vede nel log del cron invece di sparire — e lo stato su
# disco e' stato disfatto, quindi l'ora prossima ci riprova da solo.
for a in rep["alerts"]:
print(f" ALERT: {a}")
print(f" telegram: {rep.get('invio')}")
return 2 if rep.get("severity") == "alert" else 0
return 0
USO = """uso: venue_watch.py [--quiet]
venue_watch.py — runner ORARIO del tripwire di venue. Allerta su Telegram, NON blocca nulla.
uv run python scripts/live/venue_watch.py # un giro, stampa il report
uv run python scripts/live/venue_watch.py --quiet # solo in caso di allarme (per il cron)
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("venue_watch.py", USO, flag=("--quiet",), con_valore=())
raise SystemExit(main())