#!/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.notifier import notify # noqa: E402 from src.live.venue_watch import (PERSIST_HOURS, THRESHOLD_BPS, # noqa: E402 run_once) def main() -> int: quiet = "--quiet" in sys.argv rep = run_once() 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() 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"]: # ⚠️ 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 il titolo 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 — ed e' l'unico giorno che conta. if rep.get("severity") == "warn": titolo = "⚠️ VENUE WATCH — Deribit non operativa, ma spiegata. Nessuna azione" else: titolo = ("🚨 VENUE WATCH — possibile stress su Deribit. " "PRIMO PASSO: prelievo di prova") notify(titolo, {f"alert {i+1}": a for i, a in enumerate(rep["alerts"])}) for a in rep["alerts"]: print(f" ALERT: {a}") return 2 if rep.get("severity") == "alert" else 0 return 0 if __name__ == "__main__": raise SystemExit(main())