venue: la taratura ri-misurata — «zero falsi allarmi in 8 anni» non descriveva la produzione

B1 (memoria) + B2 (misura). Il 19/08 stava solo in un diario e in un commit: la
sessione che ha cambiato il tripwire di venue, la tabella _CONTRACT e le referenze
non era in CLAUDE.md — 2a occorrenza dopo edge_watch, e riguarda di nuovo qualcosa
che gira con soldi veri. Ora c'e', insieme al debito che dichiarava.

B2: il debito era «la taratura non e' ri-misurata a tre referenze». Ri-misurandola
(scripts/research/r0821_venue_refs.py, riusa dislocation/episodes/zero_fp_frontier
di r0726) il problema non era dove ci si aspettava.

1. «Zero falsi allarmi in 8 anni» veniva dal consenso Coinbase+Bitstamp+BITFINEX
   dello script di ricerca; il sorvegliante live gira su Coinbase+Bitstamp. Due
   liste di referenze in due posti diversi, e nessun test poteva accorgersene.
   Sull'insieme reale i falsi allarmi sono 1: 2020-03-13 07:00-10:00 UTC, 4 ore a
   -418 bps di picco su BTC = il crash COVID, cioe' proprio l'evento che CLAUDE.md
   elencava come esempio di cio' su cui NON scattava. La firma che lo dimostra: le
   "65.043 ore BTC" citate in memoria sono esattamente le ore utili del set con
   bitfinex; sul set reale sono 69.633.

2. Kraken non lo ripara: su 8 anni porta un mese. Il tetto ~700 candele
   dell'endpoint pubblico e' stato ri-verificato oggi sulla rete (704 barre su
   70.286 richieste = 1,00%), non creduto da un commento del 26/07.

3. Perche' spariva a 3 referenze, ed e' il punto trasferibile: con bitfinex dentro
   1 delle 4 ore diventa BLIND, lo streak si azzera e l'episodio non esiste — ma la
   dislocazione e' ancora li' (mediana -343 bps). Lo zero non veniva da un consenso
   piu' accurato, veniva da un'ora buttata (ore utilizzabili 93,4% vs 100,0%).

4. La direzione dichiarata il 19/08 ("piu' BLIND, allerta di meno") e' confermata ma
   la sua taglia dipende dal venue, non dal numero tre: confronto appaiato sulle
   stesse ore, con bitfinex -13,91 pp di ore utilizzabili, con kraken +0,00 pp.

5. Controllo positivo intatto: Bitfinex 2018-19, 22 episodi, il piu' lungo 2.324h,
   picco 1.136 bps = 11,4x la soglia.

DECISIONE: (100 bps, 4h) NON si tocca, ma la giustificazione cambia. 1 falso allarme
in 8 anni costa 0,031%/anno di equity attesa contro il 100% che un vero positivo
evita (ogni p plausibile e' 16-160x sopra il break-even); alzare a 150 bps per
ripristinare lo zero porterebbe il margine su FTX da 3,0x a 2,0x, sotto la gamba (b)
del criterio dichiarato prima di guardare i dati. La memoria si contraddiceva da
sola — «zero falsi allarmi» al punto (2), «a 1 ogni 8 anni serve p > 0,031%» al
punto (4) — e la meta' giusta era quella dell'economia.

Congelato il MECCANISMO, non il numero: tests/test_venue_watch.py 35 -> 37, due test
puri (lo spread max-min e' monotono nel numero di referenze; una sola ora BLIND
spezza lo streak, con un caso di controllo che DEVE scattare).

Soglie, book, pesi, config, cron: INVARIATI. Suite 621 verdi, 1 rosso noto
(test_gtaa_band_gate, altro asse). Diario: docs/diary/2026-08-21-venue-taratura-*.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-08-21 17:55:16 +00:00
parent e69cc5bd81
commit fac9978d87
4 changed files with 493 additions and 10 deletions
+192
View File
@@ -0,0 +1,192 @@
#!/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()