Compare commits
2 Commits
e69cc5bd81
...
8cfe15cbd5
| Author | SHA1 | Date | |
|---|---|---|---|
| 8cfe15cbd5 | |||
| fac9978d87 |
@@ -1120,25 +1120,31 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
decisione appena presa (100% Deribit fino a $20k): se non si puo' ridurre l'ESPOSIZIONE, l'unica
|
decisione appena presa (100% Deribit fino a $20k): se non si puo' ridurre l'ESPOSIZIONE, l'unica
|
||||||
leva e' il **TEMPO**. Script `r0726_venue_tripwire.py` (segnale) + `r0726_venue_response.py`
|
leva e' il **TEMPO**. Script `r0726_venue_tripwire.py` (segnale) + `r0726_venue_response.py`
|
||||||
(costo della risposta); produzione `src/live/venue_watch.py` + `scripts/live/venue_watch.py` in
|
(costo della risposta); produzione `src/live/venue_watch.py` + `scripts/live/venue_watch.py` in
|
||||||
`cron_book.sh`; test `tests/test_venue_watch.py` (23); diario `2026-07-26-venue-tripwire.md`.
|
`cron_book.sh`; test `tests/test_venue_watch.py` (**37**); diari `2026-07-26-venue-tripwire.md`,
|
||||||
|
`2026-08-19-venue-watch-disciplina-allarmi.md`, `2026-08-21-venue-taratura-tre-referenze.md`.
|
||||||
**Book, pesi, config INVARIATI.**
|
**Book, pesi, config INVARIATI.**
|
||||||
(1) **Il segnale:** un venue che gata i prelievi **rompe l'arbitraggio** → il suo prezzo si stacca
|
(1) **Il segnale:** un venue che gata i prelievi **rompe l'arbitraggio** → il suo prezzo si stacca
|
||||||
dal consenso e ci RESTA. Il segnale e' **|scarto|, non il segno** (Mt.Gox andava a *premio*, un
|
dal consenso e ci RESTA. Il segnale e' **|scarto|, non il segno** (Mt.Gox andava a *premio*, un
|
||||||
venue in fuga a *sconto*: dicono la stessa cosa). Consenso = venue **USD indipendenti**
|
venue in fuga a *sconto*: dicono la stessa cosa). Consenso = **mediana** di venue **USD
|
||||||
(Coinbase, Bitstamp); mai USDT (depeg 2022 → falsi allarmi giganti). Deribit sta a **3 bps** dal
|
indipendenti** (Coinbase, Bitstamp, **e Kraken dal 19/08** — vedi sotto); mai USDT (depeg 2022 →
|
||||||
consenso in mediana su 8 anni (65.043 ore BTC + 64.541 ETH) — fondo di rumore bassissimo, ed e'
|
falsi allarmi giganti). Deribit sta a **3 bps** dal consenso in mediana su 8 anni — fondo di
|
||||||
cio' che rende possibile una soglia con margine.
|
rumore bassissimo, ed e' cio' che rende possibile una soglia con margine. ⚠️ Le "65.043 ore BTC"
|
||||||
|
citate qui fino al 21/08 erano le ore del consenso **con bitfinex dentro**, che la produzione non
|
||||||
|
ha mai avuto: sull'insieme reale sono **69.633** (ETH 64.541 invariato).
|
||||||
(2) **Taratura CONGELATA = 100 bps persistenti 4h a segno costante.** Criterio **dichiarato
|
(2) **Taratura CONGELATA = 100 bps persistenti 4h a segno costante.** Criterio **dichiarato
|
||||||
prima**, perche' i due ovvi sbagliano in versi opposti (provati entrambi): *minimi bps* → 25bps/24h
|
prima**, perche' i due ovvi sbagliano in versi opposti (provati entrambi): *minimi bps* → 25bps/24h
|
||||||
consuma 24 delle ~72h che diede FTX; *minime ore* → 500bps/2h **manca FTX** (margine 0.6x).
|
consuma 24 delle ~72h che diede FTX; *minime ore* → 500bps/2h **manca FTX** (margine 0.6x).
|
||||||
Regola adottata: (a) zero falsi allarmi su entrambi gli asset in 8 anni; (b) margine ≥3x sul caso
|
Regola adottata: (a) zero falsi allarmi su entrambi gli asset in 8 anni; (b) margine ≥3x sul caso
|
||||||
storico **piu' debole** (FTX ~300bps) → soglia ≤100bps; (c) a quei vincoli, minima latenza.
|
storico **piu' debole** (FTX ~300bps) → soglia ≤100bps; (c) a quei vincoli, minima latenza.
|
||||||
Margine finale **3x FTX / 5x QuadrigaCX / 10-20x Mt.Gox**, con **zero falsi allarmi** inclusi
|
Margine finale **3x FTX / 5x QuadrigaCX / 10-20x Mt.Gox**.
|
||||||
crash COVID 2020-03, maggio 2021, LUNA e novembre 2022.
|
⚠️ **CORREZIONE 2026-08-21 — la gamba (a) NON e' soddisfatta dalla configurazione che gira, e la
|
||||||
|
frase "zero falsi allarmi inclusi crash COVID 2020-03" era FALSA: il falso allarme E' il COVID.**
|
||||||
|
Vedi il blocco **(8)** in fondo. Il punto (100 bps, 4h) **resta**, per economia e per la gamba (b).
|
||||||
(3) ✅ **CONTROLLO POSITIVO superato** (obbligatorio: un rilevatore tarato per non segnalare e'
|
(3) ✅ **CONTROLLO POSITIVO superato** (obbligatorio: un rilevatore tarato per non segnalare e'
|
||||||
indistinguibile da uno rotto). Puntato su **Bitfinex 2018-19** (problemi bancari/Tether):
|
indistinguibile da uno rotto). Puntato su **Bitfinex 2018-19** (problemi bancari/Tether):
|
||||||
**22 episodi**, il piu' lungo **2.324 ore consecutive** a +447bps di picco, altri a 1.151h/+663bps
|
**22 episodi**, il piu' lungo **2.324 ore consecutive** a +447bps di picco, altri a 1.151h/+663bps
|
||||||
e 496h/+1136bps. **22 scatti dove il problema c'era, 0 su Deribit in 8 anni.** E la DURATA
|
e 496h/+1136bps. **22 scatti dove il problema c'era, 1 solo su Deribit in 8 anni**
|
||||||
|
(era dichiarato 0 — vedi **(8)**). E la DURATA
|
||||||
risponde alla domanda vera: un venue gated resta dislocato per **settimane** → 4h di latenza
|
risponde alla domanda vera: un venue gated resta dislocato per **settimane** → 4h di latenza
|
||||||
costano una frazione trascurabile del preavviso.
|
costano una frazione trascurabile del preavviso.
|
||||||
(4) **Economia della risposta:** costo ATTESO di un falso allarme (flat 3 giorni, misurato sul
|
(4) **Economia della risposta:** costo ATTESO di un falso allarme (flat 3 giorni, misurato sul
|
||||||
@@ -1168,6 +1174,73 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
**l'outer-join con referenze di lunghezza diversa e' una trappola ricorrente**); (f) un controllo
|
**l'outer-join con referenze di lunghezza diversa e' una trappola ricorrente**); (f) un controllo
|
||||||
positivo finito dentro il ramo `else` **non gira mai** (trovato in sessione: era esattamente il
|
positivo finito dentro il ramo `else` **non gira mai** (trovato in sessione: era esattamente il
|
||||||
difetto che doveva prevenire).
|
difetto che doveva prevenire).
|
||||||
|
✅ **(7) DISCIPLINA DEGLI ALLARMI + TERZA REFERENZA + SPECIFICHE CONTRATTO (2026-08-19)** —
|
||||||
|
*(bullet scritto il 21/08: la sessione era in un diario e in un commit e NON in memoria operativa,
|
||||||
|
2ª occorrenza dopo `edge_watch`)*. Nato da 4 🚨 identici il 18/08 per una **manutenzione Deribit
|
||||||
|
annunciata** (14/08 per il 18/08 09:00 UTC, downtime dichiarato 15-30 min) che ha **sforato fino
|
||||||
|
alle 14:07 UTC**. **Il book si era comportato bene** (astensione alle 09:07 e 10:07, *"conto non
|
||||||
|
leggibile → non eseguo a cieco"*): il difetto era negli allarmi. (a) Il blocco piattaforma era un
|
||||||
|
`if` secco **senza memoria**, accanto a un rilevatore che allerta una volta per streak → ora passa
|
||||||
|
da **`lock_step()`** pura: manutenzione entro `MAINT_GRACE_HOURS`=2 → ⚠️ **una volta**; che sfora
|
||||||
|
→ 🚨 una volta («ha SFORATO»); blocco **senza** manutenzione dichiarata → 🚨 subito; rientro
|
||||||
|
annunciato una volta; **`public/status` illeggibile → NIENTE** (dichiarare «e' rientrato» perche'
|
||||||
|
non si e' riusciti a guardare sarebbe la bugia peggiore). *Un allarme massimo speso per un evento
|
||||||
|
atteso e' un allarme che non verra' letto il giorno che e' vero.* (b) Il messaggio stampava
|
||||||
|
`locked=true` **cablato** mentre il parser accetta anche `partial` → dichiarava un valore che non
|
||||||
|
aveva letto, e il runbook manda a controllare **proprio quel campo**; ora stampa e **salva** il
|
||||||
|
grezzo. (c) ⚠️ **Il difetto che poteva costare:** il 18/08 Deribit ha cambiato **tick e size dei
|
||||||
|
perpetual lineari USDC** (BTC tick 0.5→**0.1**, ETH 0.05→**0.01**, ETH min/step 0.001→**0.0001**)
|
||||||
|
e la tabella `_CONTRACT` di `deribit.py`, **cablata a mano**, non se n'e' accorta. Nessun ordine
|
||||||
|
rifiutato **e non per merito nostro**: erano *riduzioni*, e un valore piu' grosso resta conforme
|
||||||
|
(costo effettivo: granularita', incremento minimo ETH $1.92 invece di $0.19). **Il giorno che
|
||||||
|
Deribit ALZA un minimo la stessa cecita' fa rifiutare gli ordini.** Cablato **`check_specs()`**
|
||||||
|
nel venue_watch orario, **fuori dal percorso ordini**: la tabella dichiarata resta l'AUTORITA'
|
||||||
|
per costruire un ordine, il venue e' il **controllore** (prendere i valori dall'API dentro
|
||||||
|
l'esecuzione renderebbe l'ordine dipendente da come ha risposto una GET = non ricostruibile).
|
||||||
|
Verdetti: `granularita'` (dichiarato piu' grosso → conforme, ⚠️) vs `rifiuto` (dichiarato piu'
|
||||||
|
fine → 🚨). Uno strumento **non letto** finisce in `non_letti`, non conta come «combacia».
|
||||||
|
(d) Aggiunta **Kraken** come terza referenza: **Coinbase ha comprato Deribit** e una referenza
|
||||||
|
che e' la casa madre non misura piu' se Deribit scolla dal mondo.
|
||||||
|
⚠️ **(8) LA TARATURA RI-MISURATA (2026-08-21) — «zero falsi allarmi in 8 anni» non ha mai
|
||||||
|
descritto la produzione, e il falso allarme e' il COVID.** Script `r0821_venue_refs.py`, diario
|
||||||
|
`2026-08-21-venue-taratura-tre-referenze.md`. **Soglie, book, config INVARIATI.**
|
||||||
|
**(i)** Il numero pubblicato veniva dal consenso **Coinbase+Bitstamp+Bitfinex** dello script di
|
||||||
|
ricerca; il sorvegliante live girava su **Coinbase+Bitstamp**. Due liste di referenze in due
|
||||||
|
posti diversi, e **nessun test poteva accorgersene**. Sull'insieme reale: **1 falso allarme in
|
||||||
|
8 anni**, il **2020-03-13 07:00-10:00 UTC**, 4 ore a **−418 bps** di picco su BTC — cioe' proprio
|
||||||
|
l'evento che questo bullet elencava come esempio di cio' su cui NON scattava. La soglia minima a
|
||||||
|
zero falsi allarmi a 4h e' **150 bps** (BTC), non 100.
|
||||||
|
**(ii) Kraken non lo ripara:** su 8 anni porta **un mese** (tetto ~700 candele dell'endpoint
|
||||||
|
pubblico, **ri-verificato oggi**: 704 barre su 70.286 richieste = 1,00%) → LIVE-post ≡ LIVE-pre
|
||||||
|
sulla storia lunga, numeri **identici**.
|
||||||
|
**(iii) 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% contro 100,0%).
|
||||||
|
**(iv) La direzione dichiarata il 19/08 e' confermata, la sua TAGLIA dipende dal venue** (confronto
|
||||||
|
appaiato, stesse ore): con **bitfinex** le ore non-BLIND passano 99,95% → 86,04% = **−13,91 pp**;
|
||||||
|
con **kraken** **+0,00 pp** (|scarto| mediano appaiato −0,04 bps). Non e' una proprieta' del
|
||||||
|
numero tre: e' **quanto la terza referenza e' d'accordo con le altre**.
|
||||||
|
**(v) Controllo positivo intatto:** Bitfinex 2018-19 → 22 episodi, il piu' lungo 2.324h, picco
|
||||||
|
**1.136 bps = 11,4x** la soglia.
|
||||||
|
**(vi) DECISIONE: (100 bps, 4h) NON si tocca, ma la giustificazione cambia.** 1 falso allarme in
|
||||||
|
8 anni = 0,125/anno × 0,248% = **0,031%/anno** di equity attesa contro il **100%** che un vero
|
||||||
|
positivo evita, e ogni `p` plausibile (0,5-5%) e' **16-160x sopra** il break-even; alzare a 150 bps
|
||||||
|
porterebbe il margine su FTX da **3,0x a 2,0x**, sotto la gamba (b) dichiarata prima di guardare i
|
||||||
|
dati. **Il numero da citare e' «1 in 8 anni»** — che e' esattamente l'esempio gia' usato al punto
|
||||||
|
(4): **la memoria si contraddiceva da sola e la meta' giusta era quella dell'economia.**
|
||||||
|
**REGOLE NUOVE:** (g) **un numero di taratura si etichetta con la CONFIGURAZIONE, non solo con la
|
||||||
|
finestra** — «zero falsi allarmi in 8 anni» era vero e inutile perche' descriveva un consenso che
|
||||||
|
la produzione non ha mai avuto; (h) **uno "zero" si legge accanto alla quota di campione
|
||||||
|
utilizzabile**: meno falsi allarmi perche' si vede meglio e meno perche' si vede di meno hanno lo
|
||||||
|
stesso valore stampato e valore opposto; (i) **quando due affermazioni della stessa memoria si
|
||||||
|
contraddicono, la contraddizione e' informazione** — una delle due e' stata scritta guardando i
|
||||||
|
dati; (j) **una nota che dichiara un debito puo' sottodimensionarlo**: il 19/08 diceva «il numero
|
||||||
|
non e' ri-misurato», e il numero era sbagliato **prima** della modifica che lo aveva fatto
|
||||||
|
dichiarare. **RESTA APERTO:** il Rulebook Deribit del 12/08 (ADL, perdita socializzata, *emergency
|
||||||
|
powers*, conti dormienti) non lo sorveglia nessuno; `MAINT_GRACE_HOURS`=2 **presume** gli annunci
|
||||||
|
invece di leggerli (due `locked=true` in 4 giorni: 18/08 ~4h, 21/08 ~1h); la terza referenza non
|
||||||
|
e' validabile sulla storia.
|
||||||
- ⚖️ **RIVALUTAZIONE DELLA STRATEGIA (2026-07-26, fine giornata) — 0 cambi, e il peso 75/25
|
- ⚖️ **RIVALUTAZIONE DELLA STRATEGIA (2026-07-26, fine giornata) — 0 cambi, e il peso 75/25
|
||||||
confermato per la TERZA volta.** Script `r0726_reeval_live_weight.py`, test
|
confermato per la TERZA volta.** Script `r0726_reeval_live_weight.py`, test
|
||||||
`tests/test_reeval_live_weight.py` (6), diario `2026-07-26-reeval-strategia.md`.
|
`tests/test_reeval_live_weight.py` (6), diario `2026-07-26-reeval-strategia.md`.
|
||||||
@@ -1581,6 +1654,30 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
bps**, applica la regola congelata (≤5bps nulla · >10bps rivedere il peso SKH01) e allerta su
|
bps**, applica la regola congelata (≤5bps nulla · >10bps rivedere il peso SKH01) e allerta su
|
||||||
**qualsiasi** cambiamento dei tre. `test_baseline_e_quella_dei_backtest` lega la soglia al
|
**qualsiasi** cambiamento dei tre. `test_baseline_e_quella_dei_backtest` lega la soglia al
|
||||||
default `fee_rt=0.001` di `backtest_signals`: se divergono, il test lo dice.
|
default `fee_rt=0.001` di `backtest_signals`: se divergono, il test lo dice.
|
||||||
|
⚠️ **CORREZIONE 2026-08-21 — sorvegliava lo STRUMENTO SBAGLIATO (test 13 → 17).** `INSTRUMENTS`
|
||||||
|
era la tupla cablata `("BTC-PERPETUAL","ETH-PERPETUAL")` = i perpetual **INVERSE** (regolati in
|
||||||
|
BTC/ETH), mentre il book esegue sui **LINEARI USDC** (`BTC_USDC-PERPETUAL`). Due conseguenze:
|
||||||
|
(a) il tier sorvegliato era di un prodotto **mai tradato** — oggi identico per caso (3.50/1.50 su
|
||||||
|
entrambe le linee) ma **la prova che si muovono in modo indipendente e' nel progetto**: il cambio
|
||||||
|
del 18/08 tocco' i SOLI lineari (inverse ancora tick 0.5/min 10.0, lineari 0.1/0.0001) → un
|
||||||
|
aumento sulla sola linea lineare sarebbe stato invisibile e la regola ≤5/>10 bps applicata al
|
||||||
|
numero sbagliato; (b) **il cross-check sui trade reali non poteva misurare nulla per
|
||||||
|
costruzione** — chiedeva `trade_history` di uno strumento con **0 fill** e stampava «NON
|
||||||
|
MISURATO» anche nei giorni con 4 esecuzioni (verificato: inverse 0 trade, `_USDC` 3+1). Ora
|
||||||
|
`INSTRUMENTS` si **DERIVA da `src.live.book.INSTRUMENT`**: la divergenza non e' un rischio da
|
||||||
|
ricordare, e' impossibile. ⚠️ **E non era un rename:** le due famiglie hanno unita' DIVERSE —
|
||||||
|
inverse `amount`=nozionale USD e `fee` in valuta base; lineare `amount`=quantita' BASE e `fee`
|
||||||
|
gia' in USDC. Ripuntare senza correggerle avrebbe dato, su un fill vero (0.001 BTC @ 74.305,80,
|
||||||
|
fee 0,02600703), **~2,6e8 bps invece di 3,50** — e **senza sollevare nulla**. Cablate
|
||||||
|
`convenzione()` (lineare/inverse/**ignota**: una famiglia non nota non si indovina) e
|
||||||
|
`fee_bps_di_un_fill()` pure; il cross-check ora gira e da' **3,50 bps effettivi = il tier
|
||||||
|
esatto**, e il report stampa lo scarto effettivo−tier con ⚠️ sopra 1 bps.
|
||||||
|
**REGOLA: un sorvegliante deve DERIVARE il proprio bersaglio dal codice sorvegliato, mai
|
||||||
|
ridichiararlo** — due liste in due file divergono in silenzio, e il giorno che divergono il
|
||||||
|
sorvegliante continua a dire OK. **3ª occorrenza in un giorno della stessa forma** (test di
|
||||||
|
`book_live` senza potenza a libro flat, taratura di `venue_watch` misurata con bitfinex mentre
|
||||||
|
il live girava senza): *un controllo puntato su una configurazione diversa da quella che gira
|
||||||
|
passa sempre, e non sta controllando niente.*
|
||||||
(2) **`src/live/monitor_health.py` + `scripts/live/monitor_health.py`** (test 16): **tre gate
|
(2) **`src/live/monitor_health.py` + `scripts/live/monitor_health.py`** (test 16): **tre gate
|
||||||
pre-registrati** (STATARB 27/09, XSR01 23/10, DVOLSPREAD 24/10) si decidono su serie forward di
|
pre-registrati** (STATARB 27/09, XSR01 23/10, DVOLSPREAD 24/10) si decidono su serie forward di
|
||||||
cui **una sola** aveva una guardia d'integrita'. Un monitor fermo produce silenzio, e **il
|
cui **una sola** aveva una guardia d'integrita'. Un monitor fermo produce silenzio, e **il
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# 2026-08-21 — «Zero falsi allarmi in 8 anni» non descriveva il sistema che gira
|
||||||
|
|
||||||
|
**Richiesta:** *"analizza e fai B1 e B2"* — B1: portare in memoria operativa la sessione del 19/08,
|
||||||
|
che stava solo nel diario e nel commit. B2: ri-misurare la taratura del tripwire di venue dopo
|
||||||
|
l'aggiunta della terza referenza, debito dichiarato quel giorno e mai chiuso.
|
||||||
|
|
||||||
|
**Toccati:** `scripts/research/r0821_venue_refs.py` (nuovo), `tests/test_venue_watch.py` (35 → 37),
|
||||||
|
`CLAUDE.md`. **Book, pesi, `config/live.json`, `THRESHOLD_BPS`, `PERSIST_HOURS`: INVARIATI.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Il debito, come era stato dichiarato
|
||||||
|
|
||||||
|
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. Il diario fu onesto
|
||||||
|
sul limite: *«"Zero falsi allarmi in 8 anni" e' stato misurato con l'insieme di referenze di allora
|
||||||
|
… quel numero non e' stato ri-misurato»*, e dichiarava la sola **direzione** dell'errore — con la
|
||||||
|
mediana di tre il consenso e' piu' robusto e lo spread max-min si allarga, quindi si va piu' spesso
|
||||||
|
in BLIND, che e' lo stato morbido: il modo di sbagliare e' «allerta di meno».
|
||||||
|
|
||||||
|
La direzione era giusta. Ma ri-misurando salta fuori un fatto piu' grosso, che non era nel verso
|
||||||
|
previsto perche' non riguardava la modifica del 19/08 **ma la taratura originale**.
|
||||||
|
|
||||||
|
## 1. Il fatto che struttura tutto: la profondita' delle referenze
|
||||||
|
|
||||||
|
| asset | venue | barre | da | a |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| BTC | coinbase | 69.643 | 2018-08-14 | 2026-07-26 |
|
||||||
|
| BTC | bitstamp | 69.663 | 2018-08-14 | 2026-07-26 |
|
||||||
|
| BTC | **bitfinex** | 33.000 | 2018-08-14 | **2022-05-21** |
|
||||||
|
| BTC | **kraken** | **702** | 2026-06-26 | 2026-07-26 |
|
||||||
|
| ETH | coinbase | 64.554 | 2019-03-14 | 2026-07-26 |
|
||||||
|
| ETH | bitstamp | 64.572 | 2019-03-14 | 2026-07-26 |
|
||||||
|
| ETH | **bitfinex** | **0** | — | — |
|
||||||
|
| ETH | kraken | 702 | 2026-06-26 | 2026-07-26 |
|
||||||
|
|
||||||
|
Il tetto di Kraken e' stato **ri-verificato oggi sulla rete**, non creduto da un commento del 26/07:
|
||||||
|
chieste 70.286 ore, ricevute **704** (l'1,00% del richiesto). L'endpoint OHLC pubblico ritorna le
|
||||||
|
ultime ~700 candele qualunque `since`.
|
||||||
|
|
||||||
|
**Conseguenza: la taratura a 8 anni con Kraken dentro non e' ottenibile.** Non per pigrizia — per
|
||||||
|
struttura della fonte. E non e' un problema per il sorvegliante live, a cui servono le ultime ore.
|
||||||
|
|
||||||
|
## 2. Il falso allarme che c'era, e che nessuno aveva visto
|
||||||
|
|
||||||
|
Frontiera a zero falsi allarmi, per insieme di referenze, punto in produzione (100 bps, 4h):
|
||||||
|
|
||||||
|
| insieme | asset | ore utili | non-BLIND | soglia minima a 4h | falsi allarmi a (100, 4h) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **LIVE-pre (CB+BS)** | BTC | 69.633 | 100,0% | **150 bps** | **1** |
|
||||||
|
| | ETH | 64.541 | 100,0% | 100 bps | 0 |
|
||||||
|
| CALIB (CB+BS+BF) | BTC | **65.043** | 93,4% | 75 bps | **0** |
|
||||||
|
| | ETH | 64.541 | 100,0% | 100 bps | 0 |
|
||||||
|
| **LIVE-post (CB+BS+KR)** | BTC | 69.633 | 100,0% | **150 bps** | **1** |
|
||||||
|
| | ETH | 64.541 | 100,0% | 100 bps | 0 |
|
||||||
|
|
||||||
|
Due cose in questa tabella.
|
||||||
|
|
||||||
|
**(a) «Zero falsi allarmi in 8 anni» non ha mai descritto la configurazione in produzione.** Quel
|
||||||
|
numero viene dalla riga CALIB, che ha `bitfinex` nel consenso — e il sorvegliante live non l'ha mai
|
||||||
|
avuta. Il 65.043 e' la firma: e' esattamente il numero di ore che CLAUDE.md cita al punto (1) del
|
||||||
|
bullet VENUE WATCH. Sul consenso reale le ore sono 69.633 e i falsi allarmi sono **1**.
|
||||||
|
|
||||||
|
**(b) Aggiungere Kraken non lo ripara**, perche' su 8 anni Kraken porta un mese: LIVE-post e
|
||||||
|
LIVE-pre danno numeri **identici** sulla storia lunga.
|
||||||
|
|
||||||
|
Il falso allarme e':
|
||||||
|
|
||||||
|
```
|
||||||
|
2020-03-13 07:00 -> 10:00 UTC 4 ore picco -418,7 bps segno -1
|
||||||
|
```
|
||||||
|
|
||||||
|
Il **crash COVID**. Cioe' proprio l'evento che la memoria elencava come esempio di cio' su cui il
|
||||||
|
tripwire *non* scattava: *«zero falsi allarmi inclusi crash COVID 2020-03, maggio 2021, LUNA e
|
||||||
|
novembre 2022»*.
|
||||||
|
|
||||||
|
## 3. Perche' spariva a tre referenze — e perche' non e' una buona notizia
|
||||||
|
|
||||||
|
Nella stessa finestra, con `bitfinex` nel consenso, **1 delle 4 ore diventa BLIND** (utilizzabili
|
||||||
|
75%) → il run si spezza, non arriva mai a 4 ore consecutive, l'episodio non esiste. Ma la
|
||||||
|
dislocazione **e' ancora li'**: mediana **−343 bps** su quelle ore.
|
||||||
|
|
||||||
|
⚠️ **Lo zero non veniva da un consenso piu' accurato, veniva da un'ora buttata.** «Zero falsi
|
||||||
|
allarmi» puo' voler dire *piu' preciso* oppure *piu' cieco*, e le due si distinguono solo guardando
|
||||||
|
la quota di ore utilizzabili — che nella riga CALIB e' 93,4% contro il 100,0% delle altre.
|
||||||
|
|
||||||
|
Congelato in due test puri (`tests/test_venue_watch.py`), perche' e' il meccanismo e non il numero
|
||||||
|
a essere trasferibile:
|
||||||
|
|
||||||
|
- `test_una_terza_referenza_non_puo_aumentare_le_ore_utilizzabili` — lo spread max-min e' monotono
|
||||||
|
nel numero di referenze, quindi «piu' referenze» non e' gratis: sposta verso BLIND;
|
||||||
|
- `test_una_sola_ora_BLIND_spezza_lo_streak_e_impedisce_l_allarme` — con un caso di controllo che
|
||||||
|
DEVE scattare, altrimenti il test non proverebbe nulla.
|
||||||
|
|
||||||
|
## 4. La direzione dichiarata il 19/08 e' confermata, e la sua TAGLIA dipende dal venue
|
||||||
|
|
||||||
|
Confronto **appaiato** (stesse ore, mai due campioni diversi):
|
||||||
|
|
||||||
|
| | ore | non-BLIND 2 ref | 3 ref | Δ | \|scarto\| mediano, differenza appaiata |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| BTC + bitfinex | 33.000 | 99,95% | 86,04% | **−13,91 pp** | −0,04 bps |
|
||||||
|
| BTC + kraken | 702 | 100,00% | 100,00% | **+0,00 pp** | −0,04 bps |
|
||||||
|
| ETH + kraken | 702 | 100,00% | 100,00% | +0,00 pp | +0,03 bps |
|
||||||
|
|
||||||
|
«Piu' referenze → piu' BLIND» e' vero, ma con Bitfinex costa **14 punti** di ore utilizzabili e con
|
||||||
|
Kraken **zero**. Non e' una proprieta' del numero tre: e' una proprieta' di **quanto la terza
|
||||||
|
referenza e' d'accordo con le altre**. Bitfinex nel 2018-2022 era il venue con i problemi bancari —
|
||||||
|
metterlo nel consenso e' metterci dentro il rumore che il controllo positivo usa come *segnale*.
|
||||||
|
|
||||||
|
## 5. Controllo positivo: intatto
|
||||||
|
|
||||||
|
Bitfinex 2018-19 come bersaglio, consenso Coinbase+Bitstamp: **22 episodi**, il piu' lungo **2.324
|
||||||
|
ore**, picco **1.136 bps = 11,4x** la soglia. Il rilevatore vede i casi veri.
|
||||||
|
|
||||||
|
## 6. Decisione: la soglia NON si tocca, ma la giustificazione cambia
|
||||||
|
|
||||||
|
Il criterio dichiarato in anticipo il 26/07 aveva tre gambe: **(a) zero falsi allarmi in 8 anni**,
|
||||||
|
(b) margine ≥3x sul caso storico piu' debole (FTX ~300 bps) → soglia ≤100 bps, (c) minima latenza.
|
||||||
|
**La gamba (a) non e' soddisfatta** dalla configurazione reale. Le altre due reggono, e l'economia
|
||||||
|
del 26/07 risolve da sola:
|
||||||
|
|
||||||
|
- 1 falso allarme in 8 anni = 0,125/anno × 0,248% di costo = **0,031%/anno** di equity attesa,
|
||||||
|
contro il **100%** che un vero positivo evita → break-even a `p > 0,031%`, e ogni `p` plausibile
|
||||||
|
(0,5-5%) e' 16-160 volte sopra;
|
||||||
|
- alzare a 150 bps per ripristinare lo zero porterebbe il margine su FTX da **3,0x a 2,0x**, sotto
|
||||||
|
il vincolo (b) dichiarato prima di guardare i dati.
|
||||||
|
|
||||||
|
**Quindi (100 bps, 4h) resta**, ma perche' **un falso allarme ogni 8 anni e' economico**, non
|
||||||
|
perche' non ce ne siano. E il numero da citare e' **1 in 8 anni** — che e', ironicamente, proprio
|
||||||
|
l'esempio che il punto (4) del bullet usava per il break-even: la memoria si contraddiceva da sola
|
||||||
|
e la meta' giusta era quella dell'economia.
|
||||||
|
|
||||||
|
## 7. Cio' che resta aperto
|
||||||
|
|
||||||
|
- **La terza referenza non e' validabile sulla storia.** Kraken e' verificata su un mese e su quel
|
||||||
|
mese non cambia nulla. Se serve una validazione lunga, va cercata una fonte che pagini davvero
|
||||||
|
(il tetto ~700 e' dell'endpoint pubblico, non di ccxt).
|
||||||
|
- **Il Rulebook Deribit del 12/08 non lo sorveglia nessuno** (ADL, perdita socializzata, *emergency
|
||||||
|
powers*, conti dormienti) — invariato dal 19/08.
|
||||||
|
- **`MAINT_GRACE_HOURS = 2` presume gli annunci invece di leggerli.** Non e' teorico: due
|
||||||
|
`locked=true` in quattro giorni (18/08 ~4h, 21/08 ~1h).
|
||||||
|
|
||||||
|
## REGOLE
|
||||||
|
|
||||||
|
- **Un numero di taratura va etichettato con la CONFIGURAZIONE su cui e' stato misurato**, non solo
|
||||||
|
con la finestra. «Zero falsi allarmi in 8 anni» era vero e inutile: descriveva un consenso che la
|
||||||
|
produzione non ha mai avuto, e nessun test poteva accorgersene perche' i due percorsi (ricerca e
|
||||||
|
live) tenevano la lista delle referenze in due posti diversi.
|
||||||
|
- **Uno "zero" si legge sempre accanto alla quota di campione utilizzabile.** Meno falsi allarmi
|
||||||
|
perche' si vede meglio e meno perche' si vede di meno hanno lo stesso valore stampato e valore
|
||||||
|
opposto.
|
||||||
|
- **Quando due affermazioni della stessa memoria si contraddicono** (qui «zero falsi allarmi» e «a
|
||||||
|
1 ogni 8 anni serve p > 0,031%»), la contraddizione e' un'informazione: una delle due e' stata
|
||||||
|
scritta guardando i dati.
|
||||||
|
- **Il debito dichiarato il 19/08 era corretto ma sottodimensionato:** diceva «il numero non e'
|
||||||
|
ri-misurato» e il numero era sbagliato **prima** della modifica che lo aveva fatto dichiarare.
|
||||||
+69
-13
@@ -48,12 +48,28 @@ import requests
|
|||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
sys.path.insert(0, str(ROOT))
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from src.live.book import INSTRUMENT as BOOK_INSTRUMENT # noqa: E402
|
||||||
from src.live.notifier import notify # noqa: E402
|
from src.live.notifier import notify # noqa: E402
|
||||||
|
|
||||||
STATE = ROOT / "data" / "fee_watch" / "state.json"
|
STATE = ROOT / "data" / "fee_watch" / "state.json"
|
||||||
INSTRUMENTS = ("BTC-PERPETUAL", "ETH-PERPETUAL")
|
# ⚠️ CORREZIONE 2026-08-21. Qui c'era la tupla CABLATA ("BTC-PERPETUAL", "ETH-PERPETUAL") — gli
|
||||||
|
# INVERSE, regolati in BTC/ETH — mentre il book esegue sui LINEARI USDC (BTC_USDC-PERPETUAL).
|
||||||
|
# Sono due linee di prodotto distinte e Deribit le cambia in modo indipendente: il 18/08 ha
|
||||||
|
# toccato tick e size dei SOLI lineari (inverse ancora tick 0.5 / min 10.0, lineari 0.1 / 0.0001).
|
||||||
|
# Conseguenze del difetto: (a) il tier sorvegliato era di un prodotto che il book non tratta —
|
||||||
|
# oggi identico per caso, domani no; (b) il cross-check sui trade reali chiedeva la storia di uno
|
||||||
|
# strumento mai tradato e stampava "NON MISURATO" anche nei giorni con 4 fill.
|
||||||
|
# La lista si DERIVA dal book: cosi' la divergenza non e' un rischio da ricordare, e' impossibile.
|
||||||
|
INSTRUMENTS = tuple(BOOK_INSTRUMENT[a] for a in sorted(BOOK_INSTRUMENT))
|
||||||
API = "https://www.deribit.com/api/v2/public/get_instrument"
|
API = "https://www.deribit.com/api/v2/public/get_instrument"
|
||||||
|
|
||||||
|
# Convenzione di unita' dei trade, che NON e' la stessa nelle due famiglie:
|
||||||
|
# inverse (BTC-PERPETUAL) amount = nozionale in USD, fee in valuta base (BTC)
|
||||||
|
# lineare (BTC_USDC-PERPETUAL) amount = quantita' in BASE, fee gia' in USDC
|
||||||
|
# Applicare la convenzione sbagliata non da' un errore: da' un numero. Su un fill vero
|
||||||
|
# (0.001 BTC @ 74.305,8, fee 0,026 USDC) quella inverse darebbe ~2.6e8 bps invece di 3,50.
|
||||||
|
SUFFISSO_LINEARE = "_USDC-PERPETUAL"
|
||||||
|
|
||||||
# --- riferimenti CONGELATI (cambiarli invalida la curva di r0726_fee_sensitivity.py) ---
|
# --- riferimenti CONGELATI (cambiarli invalida la curva di r0726_fee_sensitivity.py) ---
|
||||||
BASELINE_TAKER_BPS = 5.0 # 0.10% RT: l'assunzione di OGNI backtest del progetto
|
BASELINE_TAKER_BPS = 5.0 # 0.10% RT: l'assunzione di OGNI backtest del progetto
|
||||||
BASELINE_MAKER_BPS = 0.0
|
BASELINE_MAKER_BPS = 0.0
|
||||||
@@ -88,9 +104,47 @@ def verdict(taker_bps: float) -> tuple[str, str]:
|
|||||||
f"(4x piu' fee-sensibile di TP01) via weights_tilt_null")
|
f"(4x piu' fee-sensibile di TP01) via weights_tilt_null")
|
||||||
|
|
||||||
|
|
||||||
|
def convenzione(instrument: str) -> str:
|
||||||
|
"""Famiglia di unita' dell'istrumento: 'lineare' | 'inverse' | 'ignota'. PURA.
|
||||||
|
|
||||||
|
'ignota' NON e' un caso da indovinare: un fill misurato con la convenzione sbagliata produce
|
||||||
|
un numero plausibile-o-assurdo ma sempre SILENZIOSO. Meglio dichiarare di non sapere.
|
||||||
|
"""
|
||||||
|
if instrument.endswith(SUFFISSO_LINEARE):
|
||||||
|
return "lineare"
|
||||||
|
if instrument.endswith("-PERPETUAL"):
|
||||||
|
return "inverse"
|
||||||
|
return "ignota"
|
||||||
|
|
||||||
|
|
||||||
|
def fee_bps_di_un_fill(instrument: str, amount: float, price: float, fee: float) -> float | None:
|
||||||
|
"""bps di nozionale pagati su UN fill, con la convenzione della sua famiglia. PURA.
|
||||||
|
|
||||||
|
Ritorna None se non e' calcolabile (dati mancanti o famiglia ignota): None significa
|
||||||
|
'non misurata', mai 'zero'.
|
||||||
|
"""
|
||||||
|
fam = convenzione(instrument)
|
||||||
|
try:
|
||||||
|
amount, price, fee = abs(float(amount)), float(price), abs(float(fee))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
if amount <= 0 or price <= 0:
|
||||||
|
return None
|
||||||
|
if fam == "lineare":
|
||||||
|
notional_usd, fee_usd = amount * price, fee # amount in BASE, fee gia' in USDC
|
||||||
|
elif fam == "inverse":
|
||||||
|
notional_usd, fee_usd = amount, fee * price # amount in USD, fee in valuta base
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return fee_usd / notional_usd * 1e4 if notional_usd > 0 else None
|
||||||
|
|
||||||
|
|
||||||
def realized_fee_bps(limit: int = 20) -> dict:
|
def realized_fee_bps(limit: int = 20) -> dict:
|
||||||
"""Fee REALMENTE pagata sui trade del conto, in bps di nozionale. Fonte autorevole, ma
|
"""Fee REALMENTE pagata sui trade del conto, in bps di nozionale (media pesata sul nozionale).
|
||||||
disponibile solo se il book ha tradato di recente: {} non e' 'zero', e' 'non misurata'."""
|
|
||||||
|
Fonte autorevole, ma disponibile solo se il book ha tradato di recente: {} non e' 'zero',
|
||||||
|
e' 'non misurata'.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
from src.live.deribit import DeribitRead
|
from src.live.deribit import DeribitRead
|
||||||
d = DeribitRead()
|
d = DeribitRead()
|
||||||
@@ -104,16 +158,13 @@ def realized_fee_bps(limit: int = 20) -> dict:
|
|||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
for t in trades:
|
for t in trades:
|
||||||
try:
|
bps = fee_bps_di_un_fill(ins, t.get("amount"), t.get("price"), t.get("fee"))
|
||||||
notional = abs(float(t.get("amount") or 0.0)) # perp: nozionale in USD
|
if bps is None:
|
||||||
price = float(t.get("price") or 0.0)
|
|
||||||
fee = abs(float(t.get("fee") or 0.0)) # in valuta di settlement
|
|
||||||
if notional <= 0 or price <= 0:
|
|
||||||
continue
|
|
||||||
tot_fee_usd += fee * price
|
|
||||||
tot_notional += notional
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
continue
|
continue
|
||||||
|
notional = abs(float(t["amount"])) * float(t["price"]) \
|
||||||
|
if convenzione(ins) == "lineare" else abs(float(t["amount"]))
|
||||||
|
tot_fee_usd += bps / 1e4 * notional
|
||||||
|
tot_notional += notional
|
||||||
if tot_notional > 0:
|
if tot_notional > 0:
|
||||||
out[ins] = tot_fee_usd / tot_notional * 1e4
|
out[ins] = tot_fee_usd / tot_notional * 1e4
|
||||||
return out
|
return out
|
||||||
@@ -183,7 +234,12 @@ def main() -> int:
|
|||||||
if real:
|
if real:
|
||||||
print("\n cross-check sui trade REALI del conto (fonte autorevole):")
|
print("\n cross-check sui trade REALI del conto (fonte autorevole):")
|
||||||
for ins, bps in real.items():
|
for ins, bps in real.items():
|
||||||
print(f" {ins:<16}{bps:>9.2f} bps/lato effettivi")
|
tier = r["current"].get(ins, {}).get("taker_bps")
|
||||||
|
nota = ""
|
||||||
|
if tier is not None:
|
||||||
|
d = bps - tier
|
||||||
|
nota = f" (tier taker {tier:.2f} → {d:+.2f})" + (" ⚠️ DIVERGE" if abs(d) > 1.0 else "")
|
||||||
|
print(f" {ins:<22}{bps:>9.2f} bps/lato effettivi{nota}")
|
||||||
else:
|
else:
|
||||||
print("\n cross-check sui trade reali: NON MISURATO (nessun trade recente leggibile)")
|
print("\n cross-check sui trade reali: NON MISURATO (nessun trade recente leggibile)")
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -108,3 +108,57 @@ def test_sorveglia_il_tier_base_e_lo_dichiara():
|
|||||||
def test_fee_reali_assenti_non_sono_fee_zero():
|
def test_fee_reali_assenti_non_sono_fee_zero():
|
||||||
"""{} = 'non misurata'. Un dict vuoto letto come 'zero' direbbe che il book trada gratis."""
|
"""{} = 'non misurata'. Un dict vuoto letto come 'zero' direbbe che il book trada gratis."""
|
||||||
assert FW.realized_fee_bps.__doc__ and "non misurata" in FW.realized_fee_bps.__doc__
|
assert FW.realized_fee_bps.__doc__ and "non misurata" in FW.realized_fee_bps.__doc__
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# STRUMENTI E UNITA' — il difetto trovato il 2026-08-21.
|
||||||
|
# `INSTRUMENTS` era la tupla cablata ("BTC-PERPETUAL", "ETH-PERPETUAL"), cioe' gli INVERSE,
|
||||||
|
# mentre il book esegue sui LINEARI USDC. Il tier sorvegliato era di un prodotto mai tradato
|
||||||
|
# e il cross-check sui trade reali non poteva misurare nulla per costruzione.
|
||||||
|
# ===========================================================================
|
||||||
|
def test_sorveglia_esattamente_gli_strumenti_DEL_BOOK():
|
||||||
|
"""La divergenza non dev'essere un rischio da ricordare: dev'essere impossibile.
|
||||||
|
|
||||||
|
Il 18/08 Deribit ha cambiato tick e size dei SOLI perpetual lineari USDC lasciando intatti
|
||||||
|
gli inverse: le due linee di prodotto si muovono in modo indipendente, quindi sorvegliare
|
||||||
|
l'una mentre si trada l'altra e' un controllo che non controlla.
|
||||||
|
"""
|
||||||
|
from src.live.book import INSTRUMENT as BOOK
|
||||||
|
assert set(FW.INSTRUMENTS) == set(BOOK.values()), \
|
||||||
|
"gli strumenti sorvegliati devono essere quelli su cui il book esegue"
|
||||||
|
assert all(i.endswith("_USDC-PERPETUAL") for i in FW.INSTRUMENTS)
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_convenzione_di_unita_e_dichiarata_e_non_indovinata():
|
||||||
|
assert FW.convenzione("BTC_USDC-PERPETUAL") == "lineare"
|
||||||
|
assert FW.convenzione("BTC-PERPETUAL") == "inverse"
|
||||||
|
# una famiglia che non conosciamo NON si indovina: un fill misurato con la convenzione
|
||||||
|
# sbagliata non da' un errore, da' un numero.
|
||||||
|
assert FW.convenzione("BTC-28MAR27-100000-C") == "ignota"
|
||||||
|
assert FW.fee_bps_di_un_fill("BTC-28MAR27-100000-C", 1.0, 100.0, 0.01) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_le_due_famiglie_hanno_unita_DIVERSE_e_scambiarle_non_fa_rumore():
|
||||||
|
"""⚠️ Il punto che rende il difetto pericoloso: applicare la convenzione sbagliata non
|
||||||
|
solleva niente. Su un fill VERO del 21/08 (0.001 BTC @ 74.305,80, fee 0,02600703 USDC) la
|
||||||
|
formula giusta da' il tier taker esatto; quella inverse da' un numero enorme e muto.
|
||||||
|
"""
|
||||||
|
amount, price, fee = 0.001, 74305.8, 0.02600703
|
||||||
|
giusto = FW.fee_bps_di_un_fill("BTC_USDC-PERPETUAL", amount, price, fee)
|
||||||
|
assert giusto is not None and abs(giusto - 3.50) < 0.01, f"atteso ~3.50 bps, ottenuto {giusto}"
|
||||||
|
|
||||||
|
# la stessa riga letta con la convenzione inverse: nessuna eccezione, solo un numero falso
|
||||||
|
sbagliato = FW.fee_bps_di_un_fill("BTC-PERPETUAL", amount, price, fee)
|
||||||
|
assert sbagliato is not None and sbagliato > 1e6, \
|
||||||
|
"il controllo positivo deve mostrare che la convenzione sbagliata NON fallisce, mente"
|
||||||
|
|
||||||
|
# e su un fill inverse la convenzione inverse e' quella giusta (amount in USD, fee in BTC)
|
||||||
|
inv = FW.fee_bps_di_un_fill("BTC-PERPETUAL", 74.3058, 74305.8, 74.3058 * 3.5e-4 / 74305.8)
|
||||||
|
assert inv is not None and abs(inv - 3.50) < 0.01
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_fill_non_misurabile_e_None_non_zero():
|
||||||
|
for bad in ((0.0, 100.0, 1.0), (1.0, 0.0, 1.0), (None, 100.0, 1.0), ("x", 100.0, 1.0)):
|
||||||
|
assert FW.fee_bps_di_un_fill("BTC_USDC-PERPETUAL", *bad) is None
|
||||||
|
# fee nulla e' invece una misura legittima (ordine maker a rebate zero)
|
||||||
|
assert FW.fee_bps_di_un_fill("BTC_USDC-PERPETUAL", 1.0, 100.0, 0.0) == 0.0
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
"""Test del tripwire di venue (src/live/venue_watch.py + scripts/research/r0726_venue_tripwire.py).
|
"""Test del tripwire di venue (src/live/venue_watch.py + scripts/research/r0726_venue_tripwire.py).
|
||||||
|
|
||||||
Il test che conta di piu' e' `test_controllo_positivo_*`: un rilevatore che non segnala mai nulla
|
Il test che conta di piu' e' `test_controllo_positivo_*`: un rilevatore che non segnala mai nulla
|
||||||
e' indistinguibile da uno rotto, e questo qui e' TARATO per non segnalare (zero falsi allarmi su
|
e' indistinguibile da uno rotto, e questo qui e' TARATO per non segnalare. Senza un controllo
|
||||||
8 anni). Senza un controllo positivo, "non e' mai scattato" non e' una buona notizia.
|
positivo, "non e' mai scattato" non e' una buona notizia.
|
||||||
|
|
||||||
|
⚠️ CORREZIONE 2026-08-21 (`scripts/research/r0821_venue_refs.py`): la taratura NON e' "zero falsi
|
||||||
|
allarmi in 8 anni" sull'insieme di referenze che gira in PRODUZIONE. Quel numero fu misurato con
|
||||||
|
`bitfinex` nel consenso, che il sorvegliante live non ha mai avuto; sul consenso reale (Coinbase +
|
||||||
|
Bitstamp, e da oggi + Kraken) i falsi allarmi in 8 anni sono **1**, il 2020-03-13 (crash COVID,
|
||||||
|
4 ore a -418 bps di picco su BTC) — cioe' proprio l'evento che la memoria citava come esempio di
|
||||||
|
cio' su cui NON scattava. Il punto (100 bps, 4h) resta invariato per economia, non per assenza di
|
||||||
|
falsi allarmi: 1 ogni 8 anni costa ~0.031%/anno di equity attesa contro il 100% che evita.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -354,3 +362,57 @@ def test_la_tabella_dichiarata_combacia_col_venue_oggi():
|
|||||||
"BTC-PERPETUAL": {"tick_size": 0.5, "min_trade_amount": 10.0, "contract_size": 10.0},
|
"BTC-PERPETUAL": {"tick_size": 0.5, "min_trade_amount": 10.0, "contract_size": 10.0},
|
||||||
"ETH-PERPETUAL": {"tick_size": 0.05, "min_trade_amount": 1.0, "contract_size": 1.0},
|
"ETH-PERPETUAL": {"tick_size": 0.05, "min_trade_amount": 1.0, "contract_size": 1.0},
|
||||||
}) == []
|
}) == []
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# MECCANICA scoperta il 2026-08-21 ri-misurando la taratura a tre referenze.
|
||||||
|
# Sono i due fatti PURI che spiegano i numeri di r0821_venue_refs.py; i numeri
|
||||||
|
# empirici stanno nel diario, riprodotti da quello script.
|
||||||
|
# ===========================================================================
|
||||||
|
def test_una_terza_referenza_non_puo_aumentare_le_ore_utilizzabili():
|
||||||
|
"""Aggiungere una referenza puo' solo ALLARGARE lo spread max-min, mai stringerlo ->
|
||||||
|
la quota di ore utilizzabili e' monotona NON crescente nel numero di referenze.
|
||||||
|
|
||||||
|
E' la ragione strutturale per cui "piu' referenze" non e' gratis: la modifica del 19/08
|
||||||
|
sposta il sistema verso BLIND, che e' lo stato morbido (allerta di MENO, non di piu').
|
||||||
|
"""
|
||||||
|
base = [100.0, 100.4] # spread 40 bps: concordi
|
||||||
|
ok, n, spread = dislocation_bps(100.2, base)
|
||||||
|
assert ok is not None and n == 2 and spread < THRESHOLD_BPS
|
||||||
|
|
||||||
|
# una terza referenza lontana allarga lo spread oltre la soglia -> BLIND, non allarme
|
||||||
|
fuori, n3, spread3 = dislocation_bps(100.2, base + [102.0])
|
||||||
|
assert n3 == 3 and spread3 > spread, "lo spread deve essere monotono nel numero di referenze"
|
||||||
|
assert fuori is None, "referenze in disaccordo -> si SCARTA il campione, non si allerta"
|
||||||
|
|
||||||
|
# e una terza referenza CONCORDE non toglie nulla: il caso Kraken misurato il 21/08
|
||||||
|
dentro, n3b, spread3b = dislocation_bps(100.2, base + [100.2])
|
||||||
|
assert dentro is not None and n3b == 3 and spread3b >= spread
|
||||||
|
|
||||||
|
|
||||||
|
def test_una_sola_ora_BLIND_spezza_lo_streak_e_impedisce_l_allarme():
|
||||||
|
"""⚠️ Il fatto che rende ambiguo un "zero falsi allarmi" misurato con piu' referenze.
|
||||||
|
|
||||||
|
Lo streak si AZZERA su un'ora BLIND (non si accumula evidenza su dati che non parlano).
|
||||||
|
Quindi un insieme di referenze che manda in BLIND una sola ora dentro una finestra di
|
||||||
|
dislocazione fa SPARIRE l'episodio senza che la dislocazione sia diminuita: e' esattamente
|
||||||
|
cio' che e' successo al falso allarme del 2020-03-13, presente a 2 referenze e assente a 3
|
||||||
|
perche' 1 delle 4 ore diventava BLIND (dislocazione mediana ancora -343 bps).
|
||||||
|
|
||||||
|
Morale: "zero falsi allarmi" puo' voler dire "piu' accurato" oppure "piu' cieco", e le due
|
||||||
|
si distinguono solo guardando la quota di ore utilizzabili.
|
||||||
|
"""
|
||||||
|
st = AssetState()
|
||||||
|
# 4 ore consecutive oltre soglia a segno costante -> ALERT alla quarta
|
||||||
|
for _ in range(PERSIST_HOURS - 1):
|
||||||
|
st, lvl = step(st, -400.0)
|
||||||
|
assert lvl == "WATCH"
|
||||||
|
st_alert, lvl = step(st, -400.0)
|
||||||
|
assert lvl == "ALERT", "il caso di controllo deve scattare, altrimenti il test non prova nulla"
|
||||||
|
|
||||||
|
# stessa dislocazione, ma con UNA ora cieca in mezzo -> nessun allarme
|
||||||
|
st2 = AssetState()
|
||||||
|
for bps in (-400.0, -400.0, None, -400.0):
|
||||||
|
st2, lvl2 = step(st2, bps)
|
||||||
|
assert lvl2 != "ALERT"
|
||||||
|
assert st2.streak_hours < PERSIST_HOURS, "l'ora BLIND deve azzerare lo streak"
|
||||||
|
|||||||
Reference in New Issue
Block a user