#!/usr/bin/env python """r0821_venue_refs.py — la taratura del tripwire di venue, RI-MISURATA a tre referenze. DEBITO CHIUSO QUI (dichiarato nel diario 2026-08-19 §4 e nel commento di `src/live/venue_watch.py`): il 19/08 e' stata aggiunta **Kraken** come terza referenza, perche' Coinbase ha comprato Deribit e una referenza che e' la casa madre non misura piu' se Deribit scolla dal mondo. THRESHOLD_BPS e PERSIST_HOURS non furono toccati, ma la frase «zero falsi allarmi in 8 anni» era stata misurata con l'insieme di referenze di allora — e **quel numero non e' stato ri-misurato**. Il diario dichiarava solo la DIREZIONE dell'errore («allerta di meno», perche' la mediana e' robusta e lo spread max-min si allarga -> piu' BLIND, che e' lo stato morbido). COSA MISURA (riusa il nucleo di r0726_venue_tripwire: `dislocation`, `episodes`, `zero_fp_frontier` sono IMPORTATE, non riscritte — c'e' un test d'identita' in tests/): 1. la PROFONDITA' REALE di ogni referenza, che e' il fatto che struttura tutto il resto; 2. il limite di Kraken verificato OGGI sulla rete, non creduto da un commento del 26/07; 3. il confronto APPAIATO 2 referenze vs 3 sulle STESSE ore (mai due campioni diversi: lezione del 26/07, la statistica e' la differenza appaiata, non la differenza fra due mediane) su: quota di ore utilizzabili (BLIND), distribuzione di |scarto|, frontiera a zero falsi allarmi; 4. il controllo POSITIVO (Bitfinex 2018-19) sotto la regola nuova: un rilevatore tarato per NON segnalare e' indistinguibile da uno rotto finche' non gli si mette davanti un caso vero. uv run python scripts/research/r0821_venue_refs.py [--refresh-kraken] """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from scripts.research.r0726_venue_tripwire import ( # noqa: E402 _fetch_1h, dislocation, episodes, load_refs, zero_fp_frontier, ) from src.data.downloader import load_data # noqa: E402 from src.live.venue_watch import PERSIST_HOURS, THRESHOLD_BPS # noqa: E402 ASSETS = ("BTC", "ETH") # insiemi di consenso da confrontare. 'LIVE-pre' e 'LIVE-post' sono le due configurazioni # REALI del sorvegliante, prima e dopo il 19/08. SETS = { "LIVE-pre (CB+BS)": ("coinbase", "bitstamp"), "CALIB (CB+BS+BF)": ("coinbase", "bitstamp", "bitfinex"), "LIVE-post (CB+BS+KR)": ("coinbase", "bitstamp", "kraken"), } def _ts(idx) -> pd.DatetimeIndex: return pd.to_datetime(idx, unit="ms", utc=True) def profondita(refs: dict) -> None: print("\n [1/5] PROFONDITA' REALE delle referenze (e' il fatto che struttura tutto il resto)") print(f"\n {'asset':<6}{'venue':<11}{'barre':>9} {'da':>12} {'a':>12}") for a in ASSETS: for eid in ("coinbase", "bitstamp", "bitfinex", "kraken"): s = refs.get((a, eid), pd.Series(dtype=float)) if len(s): i = _ts(s.index) print(f" {a:<6}{eid:<11}{len(s):>9,} {str(i[0].date()):>12} {str(i[-1].date()):>12}") else: print(f" {a:<6}{eid:<11}{0:>9} {'—':>12} {'—':>12} <-- VUOTA") print("\n ⚠️ Da leggere prima di ogni numero sotto: su ETH `bitfinex` e' VUOTA e `kraken` copre") print(" un mese -> sulla storia lunga il consenso di ETH e' SEMPRE a due referenze; su BTC") print(" e' a tre solo fino al 2022-05 (fine della serie bitfinex).") def limite_kraken(refresh: bool) -> None: print("\n [2/5] IL LIMITE DI KRAKEN, verificato OGGI sulla rete (non creduto da un commento)") if not refresh: print(" saltato (--refresh-kraken per misurarlo); resta il dato in cache del 26/07: 702 barre") return d = load_data("BTC", "1h") s_ms, e_ms = int(d["timestamp"].iloc[0]), int(d["timestamp"].iloc[-1]) s = _fetch_1h("kraken", "BTC/USD", s_ms, e_ms, 720) if not len(s): print(" kraken: nessuna barra (fetch fallito) — nessuna conclusione") return i = _ts(s.index) attese = (e_ms - s_ms) // 3_600_000 print(f" chieste {attese:,} ore dal {str(_ts([s_ms])[0].date())}; ricevute {len(s):,} barre " f"({i[0].date()} -> {i[-1].date()}) = {len(s)/attese*100:.2f}% del richiesto") print(" -> il tetto ~700 candele e' CONFERMATO: la storia lunga con Kraken non e' ottenibile,") print(" e questo non e' un problema per il LIVE (gli servono le ultime ore), lo e' per la TARATURA.") def _frame(a: str, refs: dict, venues: tuple[str, ...]) -> pd.DataFrame: d = load_data(a, "1h") der = pd.Series(d["close"].astype(float).values, index=d["timestamp"].astype(int).values) rr = [refs[(a, e)] for e in venues if len(refs.get((a, e), []))] return dislocation(der, rr) if len(rr) >= 2 else pd.DataFrame() def confronto_appaiato(refs: dict) -> None: """2 referenze vs 3, sulle STESSE ore. Mai due campioni diversi.""" print("\n [3/5] CONFRONTO APPAIATO 2 referenze vs 3 — sulle stesse ore, mai due campioni diversi") for a in ASSETS: for nome3, v3 in (("BF (2018-2022)", ("coinbase", "bitstamp", "bitfinex")), ("KR (1 mese)", ("coinbase", "bitstamp", "kraken"))): f2 = _frame(a, refs, ("coinbase", "bitstamp")) f3 = _frame(a, refs, v3) terza = v3[2] s3 = refs.get((a, terza), pd.Series(dtype=float)) if not len(f3) or not len(s3): print(f"\n {a} + {nome3}: terza referenza assente -> confronto NON possibile") continue # ore in cui la TERZA referenza esiste davvero: e' li' che il confronto ha senso comune = f2.index.intersection(f3.index).intersection(s3.index) if len(comune) < 100: print(f"\n {a} + {nome3}: solo {len(comune)} ore in comune -> non si conclude") continue A, B = f2.loc[comune], f3.loc[comune] u2, u3 = A["usable"].mean(), B["usable"].mean() ent = A["usable"] & B["usable"] d_bps = (B.loc[ent, "bps"].abs() - A.loc[ent, "bps"].abs()) print(f"\n {a} + {nome3} ore in comune {len(comune):,} " f"({_ts(comune)[0].date()} -> {_ts(comune)[-1].date()})") print(f" utilizzabili (non-BLIND) 2 ref {u2*100:6.2f}% 3 ref {u3*100:6.2f}%" f" -> {(u3-u2)*100:+.2f} pp") print(f" |scarto| mediano 2 ref {A.loc[ent,'bps'].abs().median():6.2f} " f"3 ref {B.loc[ent,'bps'].abs().median():6.2f} bps" f" -> differenza APPAIATA mediana {d_bps.median():+.2f} bps") print(f" |scarto| p99 2 ref {A.loc[ent,'bps'].abs().quantile(.99):6.2f} " f"3 ref {B.loc[ent,'bps'].abs().quantile(.99):6.2f} bps") for et, F in (("2 ref", A), ("3 ref", B)): ep = episodes(F["bps"], F["usable"], THRESHOLD_BPS, PERSIST_HOURS) print(f" episodi a ({THRESHOLD_BPS:.0f} bps, {PERSIST_HOURS}h) {et}: {len(ep)}") def frontiera(refs: dict) -> None: print(f"\n [4/5] FRONTIERA A ZERO FALSI ALLARMI, per insieme di referenze " f"(punto in produzione: {THRESHOLD_BPS:.0f} bps / {PERSIST_HOURS}h)") for nome, v in SETS.items(): print(f"\n {nome}") for a in ASSETS: f = _frame(a, refs, v) if not len(f) or f["usable"].sum() < 100: print(f" {a}: campione insufficiente ({0 if not len(f) else int(f['usable'].sum())} ore utili)") continue i = _ts(f.index) front = dict((h, b) for b, h in zero_fp_frontier(f["bps"], f["usable"])) ep = episodes(f["bps"], f["usable"], THRESHOLD_BPS, PERSIST_HOURS) b4 = front.get(PERSIST_HOURS) print(f" {a}: {int(f['usable'].sum()):,} ore utili " f"({i[0].date()}->{i[-1].date()}, {f['usable'].mean()*100:.1f}% non-BLIND) " f"soglia minima a {PERSIST_HOURS}h = {b4 if b4 else 'nessuna'} bps " f"falsi allarmi a ({THRESHOLD_BPS:.0f},{PERSIST_HOURS}h) = {len(ep)}") def controllo_positivo(refs: dict) -> None: print("\n [5/5] CONTROLLO POSITIVO — Bitfinex 2018-19 come BERSAGLIO (consenso CB+BS)") print(" Un rilevatore tarato per non segnalare e' indistinguibile da uno rotto.") a = "BTC" tgt = refs.get((a, "bitfinex"), pd.Series(dtype=float)) base = [refs[(a, e)] for e in ("coinbase", "bitstamp") if len(refs.get((a, e), []))] if not len(tgt) or len(base) < 2: print(" serie insufficienti -> controllo NON eseguito (e questo e' un fallimento, non un ok)") return f = dislocation(tgt, base) ep = episodes(f["bps"], f["usable"], THRESHOLD_BPS, PERSIST_HOURS) if not ep: print(" ✋ ZERO episodi su Bitfinex: il rilevatore NON vede un caso noto -> taratura da rivedere") return dur = sorted((e["hours"] for e in ep), reverse=True) pk = max(abs(e["peak_bps"]) for e in ep) print(f" {len(ep)} episodi · piu' lungo {dur[0]:,} ore · picco {pk:,.0f} bps " f"= {pk/THRESHOLD_BPS:.1f}x la soglia") print(f" durate delle prime 5: {dur[:5]}") def main() -> None: refresh = "--refresh-kraken" in sys.argv print("=" * 100) print(" r0821 — TARATURA DEL TRIPWIRE DI VENUE, RI-MISURATA A TRE REFERENZE") print("=" * 100) print(f" regola in produzione: |scarto| > {THRESHOLD_BPS:.0f} bps persistente {PERSIST_HOURS}h " "a segno costante, consenso = MEDIANA delle referenze concordi (>=2).") refs = load_refs() profondita(refs) limite_kraken(refresh) confronto_appaiato(refs) frontiera(refs) controllo_positivo(refs) print("\n" + "=" * 100) if __name__ == "__main__": main()