#!/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, ultimo_errore # noqa: E402 from src.live.venue_watch import (PERSIST_HOURS, THRESHOLD_BPS, # noqa: E402 run_once) 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() 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 if __name__ == "__main__": raise SystemExit(main())