research(wave-0822): BASIS-CALENDAR scartato — il basis dei datati E' il funding del perp; ma in regalo 240k barre di futures scaduti col 2022 dentro

This commit is contained in:
Adriano Dal Pastro
2026-08-22 17:39:55 +00:00
parent 32ed18222e
commit b6c0deb866
5 changed files with 1071 additions and 229 deletions
+46
View File
@@ -20,6 +20,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato.
| — | **HL-EXEC** (audit di fatto) | **3 falsificazioni misurate** | il pavimento vero e' **$10 (non $5)** e il taker **4,50 bps (non 5,0)** — ma il *"XS01 serve ~$20k"* e' **refutato del tutto** (nessuna soglia da min-order), il *"XSR01 ~$5.000"* e' **conservativo di 1,7x** (vero ~$3.000), e lo **slippage "rischio #1" di XSR01 e' refutato** con margine **21x** | | — | **HL-EXEC** (audit di fatto) | **3 falsificazioni misurate** | il pavimento vero e' **$10 (non $5)** e il taker **4,50 bps (non 5,0)** — ma il *"XS01 serve ~$20k"* e' **refutato del tutto** (nessuna soglia da min-order), il *"XSR01 ~$5.000"* e' **conservativo di 1,7x** (vero ~$3.000), e lo **slippage "rischio #1" di XSR01 e' refutato** con margine **21x** |
| — | **SLIP-AUDIT** (audit di fatto) | **SCARTATO** = nessun costo nascosto a questa taglia | i backtest a 10 bps RT restano **conservativi di ~1,5 bps/lato**; nessuna evidenza di impatto sopravvive al null (p=0,274; estremi avversi **1/18 contro 4,7 attesi**). **Ma la misura ha una data di scadenza** | | — | **SLIP-AUDIT** (audit di fatto) | **SCARTATO** = nessun costo nascosto a questa taglia | i backtest a 10 bps RT restano **conservativi di ~1,5 bps/lato**; nessuna evidenza di impatto sopravvive al null (p=0,274; estremi avversi **1/18 contro 4,7 attesi**). **Ma la misura ha una data di scadenza** |
| — | **SCETTICO su GROWTH-POLICY** | **REGGE** (condizione bloccante DECADUTA) | la lente wick accoppiata **non cambia il gradino**: e' un ricarico moltiplicativo **costante ~3,5%** sul maxDD, non un'amplificazione (Δ/k **cala**). Ma su una regola a **UN giorno** close-only e' **esattamente cieca** (rapporto INF) -> la lezione 25/07 e' vera e **non si trasferisce** | | — | **SCETTICO su GROWTH-POLICY** | **REGGE** (condizione bloccante DECADUTA) | la lente wick accoppiata **non cambia il gradino**: e' un ricarico moltiplicativo **costante ~3,5%** sul maxDD, non un'amplificazione (Δ/k **cala**). Ma su una regola a **UN giorno** close-only e' **esattamente cieca** (rapporto INF) -> la lezione 25/07 e' vera e **non si trasferisce** |
| 13 | BASIS-CALENDAR | **SCARTATO** + **1 dataset nuovo** | il basis dei futures datati **E' il funding del perp quotato diversamente** (7,33% contro 6,48%/anno): non e' un quarto lato del filone funding, e' lo stesso lato. Premio incassabile **+0,85%/anno con IC95 che contiene lo zero** |
## Note che sopravvivono ai singoli filoni ## Note che sopravvivono ai singoli filoni
@@ -499,3 +500,48 @@ un lettore che prendesse "20%" per buono attribuirebbe a TP01 l'**89%** del risc
e' pericolosa"**: protegge la conclusione del 26/07 secondo cui la **liquidation fee 1% di Deribit e' e' pericolosa"**: protegge la conclusione del 26/07 secondo cui la **liquidation fee 1% di Deribit e'
irrilevante**, che vale interamente perche' a <=1x servirebbe un movimento avverso ~100%. Chi alza il irrilevante**, che vale interamente perche' a <=1x servirebbe un movimento avverso ~100%. Chi alza il
cap deve **rifare `r0726_fee_sensitivity`**, non aggirare l'assert. cap deve **rifare `r0726_fee_sensitivity`**, non aggirare l'assert.
### 13 — BASIS-CALENDAR (r0822_basis_calendar.py, 108 celle dichiarate)
**HA COSTRUITO UN DATASET CHE IL PROGETTO NON AVEVA, e contiene la coda che mancava a CC01.**
Deribit **purga** le liste di strumenti scaduti (`get_instruments?expired=true` ritorna **1** solo
strumento) -> la via ovvia e' chiusa. Ma i nomi dei trimestrali sono deterministici (ultimo venerdi'
di MAR/GIU/SET/DIC 08:00 UTC) e **`get_tradingview_chart_data` serve la storia completa di un
contratto SCADUTO se il nome lo si costruisce a mano**: **30/30 trimestrali 2019Q1-2026Q2 rispondono
ok**. Risultato: **BTC 34 contratti / 240.150 barre orarie (2018-09 -> oggi), ETH 34 / 233.334**,
piu' **64.110 ore** di funding e indice per asset. **Include il deleveraging 2022** — esattamente la
coda che a CC01 mancava per costruzione.
Certificato: barre flat 0,7-1,0% per dte<180g; convergenza a scadenza |ln(F/indice)| mediana 33-47
bps; **cross-check indipendente col forward implicito nella catena opzioni: mediana 0,6 bps**.
Roll gestito **in spazio contratto** (nessuna serie incollata -> nessun salto sul giunto).
📌 **Il risultato: il premio a termine e' REALE ma e' il PREZZO DELLA COPERTURA, non un edge.**
Basis BTC mediana **+6,33%/anno**, contango **84,9% del tempo**, slope positiva 97,4%. Ma implicito
**+7,33%/anno** contro funding realizzato sulle finestre appaiate **+6,48%** -> premio incassabile
**+0,85%/anno, IC95 [1,00, +2,92] che CONTIENE LO ZERO** (ETH +2,55%). A 60-100 giorni (il bucket
meno rumoroso) **+0,30% BTC / +1,70% ETH** contro **~1,4%/anno di costo d'esecuzione**.
**Quindi i futures datati NON sono un quarto lato della domanda funding: sono lo stesso lato quotato
diversamente**, e il filone funding resta chiuso su tre lati senza che ne esista un quarto.
📌 **E questo filone NON e' CC01, misurato:** `implausible_sharpe` **False** — barre in perdita
**49,9%**, maxDD 18,2% -> **la coda e' PRESENTE**. *Li' il rischio era fuori dal campione; qui e'
dentro e l'edge semplicemente non c'e'.* Distinzione che il gate del 26/07 e' stato scritto per fare.
- Cella scelta **in-sample-only**: Sharpe +0,13 / hold 2,44. Distribuzione dei 108 Sharpe: mediana
**5,19**. Con 7,4 anni e vol 4,9% la **SE del rendimento annuo e' 1,79pp: sotto ~3,6pp/anno nulla
e' distinguibile da zero.** Null de-levering **REFUTED** (TP01 a k=0,05 fa DD 0,75% con Sharpe 1,31).
- **Test decisivo del filone:** ritardo d'esecuzione 1h/2h/24h -> Sharpe 0,13/0,13/0,10 -> **non e'
microstruttura da prezzo stantio; non c'e' edge da falsificare.**
- ⚠️ **3ª volta in questa ondata che l'eseguibilita' NON e' il vincolo:** lotti minimi $10 BTC
inverse / **$7,73 BTC USDC** / $1 ETH / $2,42 ETH USDC; servono 4,1x per gamba (~$148 di margine
iniziale a $600).
- ⚠️ **Limite dichiarato e NON appianato:** le due lenti sul premio (hold-to-expiry vs backtest
rollato) **discordano nel SEGNO** restando entro ~1,5 SE l'una dall'altra e dallo zero. *Non si
puo' stabilire il segno di una grandezza da ~1%/anno con 7 anni di dati a vol 5%* — ed e' proprio
questo che rende il filone non deployabile.
- **Riaprirebbe il filone:** il premio a 60-100 giorni **stabilmente sopra ~4%/anno** (2 SE sopra
zero E sopra il costo), cioe' un regime in cui basis e funding **si scollano** (marzo 2020, giugno
2022 — ma per giorni, non per trimestri).
⚠️ **Tre errori propri catturati prima di pubblicare:** (a) il `volume` di
`get_tradingview_chart_data` e' in **valuta base, non USD** -> la prima stesura sbagliava la
liquidita' di ~5 ordini di grandezza; (b) il primo test d'ancora azzerava i *rendimenti* fuori
dall'ora scelta = **un'altra strategia, non un'ancora**; (c) **un gate che stampa `None` va
verificato prima di dichiararlo "non girato"** — stampava le chiavi sbagliate.
✅ Disciplina di rete rispettata: <=2 req/s e **sospensione automatica nella finestra :25-:29** di
`cron_chain` (verificato: si e' sospeso da solo per 264s).
+318 -224
View File
@@ -3,53 +3,57 @@
CONTESTO. Il filone VRP-QUOTE-VERE di questa stessa ondata ha trovato che `collect_chain.py` CONTESTO. Il filone VRP-QUOTE-VERE di questa stessa ondata ha trovato che `collect_chain.py`
interroga `{"currency": "BTC"|"ETH"}`, che su Deribit restituisce le sole opzioni INVERSE interroga `{"currency": "BTC"|"ETH"}`, che su Deribit restituisce le sole opzioni INVERSE
(regolate in BTC/ETH). Il conto e' in USDC. Esiste accanto una famiglia USDC-LINEARE con lotti (regolate in BTC/ETH). Il conto e' in USDC. Esiste accanto una famiglia USDC-LINEARE con lotti
~10x piu' piccoli, e su alcuni sottostanti (SOL, XRP, HYPE, AVAX) e' molto piu' popolata di molto piu' piccoli, e contando l'open interest sembra che su SOL/XRP/HYPE/AVAX sia molto piu'
BTC_USDC. Il progetto non l'ha mai misurata. popolata di BTC_USDC. Il progetto non l'ha mai misurata.
DOMANDA. Esiste su questa famiglia una struttura a RISCHIO DEFINITO (put credit spread, la stessa DOMANDA. Esiste su questa famiglia una struttura a RISCHIO DEFINITO (put credit spread, la stessa
geometria di VRP01: vendi ~-0.28, compri ~-0.10) eseguibile con qualche centinaio di dollari, geometria di VRP01: vendi ~-0.28, compri ~-0.10) eseguibile con qualche centinaio di dollari, con
con spread e liquidita' che non se la mangino? spread e liquidita' che non se la mangino?
COSA MISURA, in ordine, e si FERMA appena un vincolo la uccide: IPOTESI DICHIARATA PRIMA DI MISURARE: che il vincolo binding fosse la TAGLIA DEL LOTTO (e' cosi'
1. i fatti del venue letti dall'API pubblica (min_trade_amount, tick_size, contract_size, che il progetto ha sempre bocciato le opzioni: "BTC min 0.1 = $6.210/lotto -> FUORI a $3.000") e
costo in $ di UN lotto), e la LIQUIDITA' vera: spread relativo (ask-bid)/mid ai delta che che le famiglie alt, con lotti da poche decine di dollari, la superassero. Sbagliata due volte:
servono davvero, non sull'ATM, + profondita' in cima al book; il lotto non e' il vincolo, e la classifica di liquidita' su cui poggia la domanda si ROVESCIA.
2. il collaterale: margine della gamba corta secondo la formula pubblicata dal venue, e
max-loss della struttura (i due numeri differiscono per un fatto — se il venue netta le
gambe — che l'API PUBBLICA NON dice: e' dichiarato come tale, non indovinato);
3. quanta storia c'e' (risposta: nessuna) e quindi cosa si potrebbe al massimo concludere.
COSA NON PUO' FARE, e va detto prima dei numeri: **non esiste un backtest possibile**. Il nostro COSA MISURA, in ordine, fermandosi appena un vincolo la uccide:
collettore raccoglie solo le inverse BTC/ETH e l'archivio ereditato pure. Questo file misura le 0. l'UNITA' dei contratti — due test empirici, perche' leggerla male sposta ogni cifra in
condizioni di UN ISTANTE. Il massimo verdetto ottenibile e' `LEAD` = "vale la pena raccogliere il dollari di un fattore 10-1000 (errore commesso nella prima stesura di questo file);
dato", con il costo di raccolta stimato in chiamate/ora sul giro esistente. 1. i fatti del venue (min_trade_amount, tick_size, contract_size, $ di UN lotto);
2. la liquidita' misurata in DUE modi che danno risposte opposte: open interest vs quote
effettivamente presenti. E' il risultato principale del file;
3. la struttura: delta REALIZZATI, credito eseguibile vs credito al mark, spread, fee, max-loss;
4. il collaterale e quante strutture entrano in $635;
5. quanto costerebbe raccogliere il dato (perche' il verdetto massimo qui e' LEAD).
COSA NON PUO' FARE, detto prima dei numeri: **non esiste un backtest possibile**. Il nostro
collettore raccoglie solo le inverse BTC/ETH e l'archivio ereditato pure: zero giorni di storia su
queste famiglie. Questo file misura UN ISTANTE.
VINCOLI DI PROGETTO CITATI, non aggirati: VINCOLI DI PROGETTO CITATI, non aggirati:
(a) "niente short-vol da modello in deploy" (19/06). Qui i prezzi sono VERI, non modellati — (a) "niente short-vol da modello in deploy" (19/06). Qui i prezzi sono VERI, non modellati —
che e' un'altra cosa — ma la regola resta finche' non esiste un campione che contenga uno che e' un'altra cosa — ma la regola resta finche' non esiste un campione che contenga uno
STRESS. Un istante di calma non e' un campione. STRESS. Un istante di calma non e' un campione.
(b) SOL e' escluso dall'universo DIREZIONALE per decisione dell'operatore del 22/08. Quella (b) SOL e' escluso dall'universo DIREZIONALE per decisione dell'operatore del 22/08. Quella
decisione riguarda TP01/SKH01 congelati come terza gamba di trend, e il suo stesso testo decisione riguarda TP01/SKH01 congelati come terza gamba di TREND, e il suo stesso testo
dichiara che un meccanismo diverso la riapre. Un put credit spread non e' direzionale nel dichiara che un meccanismo diverso la riapre; un put credit spread non e' direzionale in
senso di quella decisione. Ma il guardrail sul DATO vale lo stesso: nessun file nuovo in quel senso. Ma il guardrail sul DATO vale identico: nessun file nuovo in `data/raw/`, e i
`data/raw/`, e i prezzi del sottostante qui vengono da `underlying_price` della catena, prezzi del sottostante qui vengono da `underlying_price` della catena, mai da `load_data`.
mai da `load_data`. (Nota: qui SOL non viene proposto — viene BOCCIATO, e sulla misura, non sulla decisione.)
(c) il gate IV-rank>0.30 e' l'unico alpha misurato di VRP01, e su questi sottostanti NON e' (c) il gate IV-rank>0.30 e' l'unico alpha misurato di VRP01 e su questi sottostanti NON e'
calcolabile: non abbiamo un giorno di storia di vol implicita. Senza quel gate la struttura calcolabile: non abbiamo un giorno di storia di vol implicita. Senza quel gate la struttura
non e' VRP01, e' vendere vol a caso. non e' VRP01, e' vendere vol a caso.
CONVENZIONE DI UNITA' (la trappola di questo file): le opzioni INVERSE si quotano in CRIPTO per CONVENZIONE DI UNITA' (la trappola di questo file, verificata al §0): le opzioni INVERSE si
contratto, le USDC-LINEARI in USDC per unita' di sottostante. Confrontare i due premi senza quotano in CRIPTO per contratto, le USDC-LINEARI in USDC per UNITA' DI SOTTOSTANTE; e
convertire darebbe numeri sbagliati di un fattore ~10^4-10^5. Qui ogni importo in dollari passa `min_trade_amount` e' in unita' di sottostante, NON in contratti — quindi non va moltiplicato per
per `prem_usd()`, e le colonne unit-free (spread relativo, costo/credito) sono le uniche `contract_size` (farlo gonfia XRP_USDC da $1.469 a $1.469.000 per lotto).
direttamente confrontabili fra le due famiglie.
RETE: sole letture pubbliche, nessun ordine, pacing a ~2 richieste/s, e il file SI RIFIUTA di RETE: sole letture pubbliche, nessun ordine, pacing ~2 richieste/s, e il file SI RIFIUTA di
partire nella finestra :24-:29 (e' quando `cron_chain` usa lo stesso IP: il rate limit Deribit e' partire nella finestra :24-:29 (e' quando `cron_chain` usa lo stesso IP: il rate limit Deribit e'
per-IP e il progetto ha gia' avuto un guasto per questo, 29/07). per-IP e il progetto ha gia' avuto un guasto per questo, 29/07).
nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py
nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py --cache # riusa nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py --cache
""" """
from __future__ import annotations from __future__ import annotations
@@ -70,13 +74,15 @@ CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/altopt_cache.json") "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/altopt_cache.json")
SHORT_DELTA, LONG_DELTA = -0.28, -0.10 # la geometria di VRP01, non ri-scelta qui SHORT_DELTA, LONG_DELTA = -0.28, -0.10 # la geometria di VRP01, non ri-scelta qui
DTE_LO, DTE_HI = 4.0, 45.0 # dal settimanale di VRP01 (4-10) fino a ~1.5 mesi DTE_LO, DTE_HI = 4.0, 45.0 # dal settimanale di VRP01 (4-10) a ~1.5 mesi
OI_MIN = 100.0 # la stessa soglia di collect_chain.py OI_MIN = 100.0 # la soglia che usa collect_chain.py, qui sotto esame
CAPITAL = 635.0 # il conto vero CAPITAL = 635.0 # il conto vero
RISK_BUDGET = 0.20 # quota di conto a rischio, per la capacita'
TAKER_CAP = 0.125 # cap fee opzioni Deribit: 12.5% del premio TAKER_CAP = 0.125 # cap fee opzioni Deribit: 12.5% del premio
LINEAR = ("SOL_USDC", "XRP_USDC", "ETH_USDC", "HYPE_USDC", "AVAX_USDC", "BTC_USDC") LINEAR = ("ETH_USDC", "BTC_USDC", "SOL_USDC", "XRP_USDC", "HYPE_USDC", "AVAX_USDC")
INVERSE = ("BTC", "ETH") # termine di paragone: il progetto le conosce INVERSE = ("BTC", "ETH") # termine di paragone: il progetto le conosce
FAMS = LINEAR + INVERSE
# ---------------------------------------------------------------- rete (parca, sola lettura) # ---------------------------------------------------------------- rete (parca, sola lettura)
@@ -132,11 +138,6 @@ def fetch_all(b: Budget) -> dict:
{"currency": cur, "kind": "option", "expired": "false"}, b) or [] {"currency": cur, "kind": "option", "expired": "false"}, b) or []
out[f"summ_{cur}"] = get("get_book_summary_by_currency", out[f"summ_{cur}"] = get("get_book_summary_by_currency",
{"currency": cur, "kind": "option"}, b) or [] {"currency": cur, "kind": "option"}, b) or []
if not out["inst_USDC"]: # il venue potrebbe non accettare currency=USDC
out["inst_USDC"] = get("get_instruments",
{"currency": "any", "kind": "option", "expired": "false"}, b) or []
out["summ_USDC"] = get("get_book_summary_by_currency",
{"currency": "any", "kind": "option"}, b) or []
return out return out
@@ -153,37 +154,40 @@ def put_delta(S: float, K: float, T: float, sig: float) -> float:
# ---------------------------------------------------------------- unita' # ---------------------------------------------------------------- unita'
def is_inverse(fam: str) -> bool: def is_inv(fam: str) -> bool:
return fam in INVERSE return fam in INVERSE
def prem_usd(price: float, r: dict) -> float: def usd(price: float | None, r: dict) -> float:
"""Premio in DOLLARI di `min_trade_amount` contratti quotati a `price`. """Prezzo di UN LOTTO MINIMO, in dollari.
Lineare: il prezzo e' gia' in USDC per unita' di sottostante. Lineare: il prezzo e' gia' in USDC per unita' di sottostante -> x min_trade_amount.
Inverse: il prezzo e' in CRIPTO per contratto -> si moltiplica per il sottostante. Inverse: il prezzo e' in CRIPTO per contratto -> x min_trade_amount x sottostante.
`contract_size` NON entra: `min_trade_amount` e' gia' in unita' di sottostante (§0).
""" """
k = r["csize"] * r["minamt"] if price is None:
return price * k * (r["S"] if is_inverse(r["fam"]) else 1.0) return float("nan")
return price * r["minamt"] * (r["S"] if is_inv(r["fam"]) else 1.0)
def loss_usd(Ks: float, Kl: float, r: dict) -> float: def loss_usd(Ks: float, Kl: float, r: dict) -> float:
"""Perdita massima strutturale in dollari, per `min_trade_amount` contratti. """Perdita massima strutturale, per un lotto minimo.
Lineare: esatta, (Ks-Kl) x quantita'. Inverse: il payoff e' in cripta (max(0,K-S)/S), il Lineare: esatta, (Ks-Kl) x quantita'. Inverse: il payoff e' in cripto (max(0,K-S)/S), il caso
peggiore e' a S=Kl e vale (Ks-Kl)/Kl cripto = (Ks-Kl) dollari a quel prezzo -> stessa peggiore e' a S=Kl e vale (Ks-Kl)/Kl cripto = (Ks-Kl) dollari A QUEL PREZZO -> stessa formula,
formula, ma APPROSSIMATA (il valore in dollari della perdita dipende da dove finisce S). ma APPROSSIMATA (il valore in dollari dipende da dove finisce S). Dichiarata, non nascosta.
""" """
return (Ks - Kl) * r["csize"] * r["minamt"] return (Ks - Kl) * r["minamt"]
# ---------------------------------------------------------------- margine (FONTE SECONDARIA) # ---------------------------------------------------------------- margine (FONTE SECONDARIA)
def im_short_put_usd(S: float, K: float, mark_usd: float, qty: float) -> float: def im_short_put(S: float, K: float, mark_lot: float, r: dict) -> float:
"""Initial margin di una short put, in USDC, secondo la formula PUBBLICATA da Deribit. """Initial margin di una short put, in USDC, per UN LOTTO, formula PUBBLICATA da Deribit:
IM = max(max(0.15 - OTM/S, 0.10), 0.075) x S + mark [per unita' di sottostante]
⚠️ FONTE SECONDARIA, dichiarata: l'API PUBBLICA non espone il margine. Il numero che decide ⚠️ FONTE SECONDARIA, dichiarata: l'API PUBBLICA non espone il margine. E il numero che decide
davvero — se Deribit NETTA le due gambe di uno spread in margine standard — non e' leggibile davvero — se Deribit NETTA le due gambe di uno spread in margine standard — non e' leggibile
da qui: si conferma con `private/get_margins`, che richiede una chiave (lettura, ma non da qui: si conferma con `private/get_margins`, che richiede una chiave (lettura, ma non
pubblica) e quindi NON e' stato usato. pubblica) e quindi NON e' stato usato.
@@ -191,11 +195,10 @@ def im_short_put_usd(S: float, K: float, mark_usd: float, qty: float) -> float:
if not (S > 0 and K > 0): if not (S > 0 and K > 0):
return float("nan") return float("nan")
otm = max(0.0, S - K) / S # una put e' OTM quando K < S otm = max(0.0, S - K) / S # una put e' OTM quando K < S
per_contratto = max(max(0.15 - otm, 0.10) * S, 0.075 * S) + max(0.0, mark_usd / max(qty, 1e-12)) return max(max(0.15 - otm, 0.10), 0.075) * S * r["minamt"] + max(0.0, mark_lot)
return per_contratto * qty
# ---------------------------------------------------------------- costruzione della vista # ---------------------------------------------------------------- vista
def build(raw: dict) -> list[dict]: def build(raw: dict) -> list[dict]:
@@ -205,8 +208,7 @@ def build(raw: dict) -> list[dict]:
inst[i["instrument_name"]] = i inst[i["instrument_name"]] = i
for s in raw.get(f"summ_{cur}") or []: for s in raw.get(f"summ_{cur}") or []:
summ[s["instrument_name"]] = s summ[s["instrument_name"]] = s
rows = [] rows, now_ms = [], datetime.now(UTC).timestamp() * 1000.0
now_ms = datetime.now(UTC).timestamp() * 1000.0
for name, i in inst.items(): for name, i in inst.items():
s = summ.get(name) s = summ.get(name)
if s is None: if s is None:
@@ -217,21 +219,21 @@ def build(raw: dict) -> list[dict]:
bid = float(bid) if bid else None # Deribit manda 0/None per "nessun lato" bid = float(bid) if bid else None # Deribit manda 0/None per "nessun lato"
ask = float(ask) if ask else None ask = float(ask) if ask else None
iv, S = s.get("mark_iv"), s.get("underlying_price") iv, S = s.get("mark_iv"), s.get("underlying_price")
K = float(i.get("strike") or 0) K, T = float(i.get("strike") or 0), max(dte, 0.0) / 365.25
typ = "P" if i.get("option_type") == "put" else "C" typ = "P" if i.get("option_type") == "put" else "C"
T = max(dte, 0.0) / 365.25
d = (put_delta(float(S), K, T, float(iv) / 100.0)
if (S and iv and K and T > 0 and typ == "P") else float("nan"))
rows.append(dict( rows.append(dict(
name=name, fam=fam, typ=typ, K=K, dte=dte, S=float(S) if S else float("nan"), name=name, fam=fam, typ=typ, K=K, dte=dte, S=float(S) if S else float("nan"),
iv=float(iv) if iv else float("nan"), delta_bs=d, iv=float(iv) if iv else float("nan"),
delta=(put_delta(float(S), K, T, float(iv) / 100.0)
if (S and iv and K and T > 0 and typ == "P") else float("nan")),
bid=bid, ask=ask, mid=((bid + ask) / 2 if (bid and ask) else None), bid=bid, ask=ask, mid=((bid + ask) / 2 if (bid and ask) else None),
mark=float(s.get("mark_price") or 0.0), mark=float(s.get("mark_price") or 0.0),
oi=float(s.get("open_interest") or 0.0), vol=float(s.get("volume") or 0.0), oi=float(s.get("open_interest") or 0.0), vol=float(s.get("volume") or 0.0),
tick=float(i.get("tick_size") or 0.0), steps=i.get("tick_size_steps") or [], vol_usd=float(s.get("volume_usd") or 0.0),
minamt=float(i.get("min_trade_amount") or 0.0), tick=float(i.get("tick_size") or 0.0), minamt=float(i.get("min_trade_amount") or 0.0),
csize=float(i.get("contract_size") or 0.0), csize=float(i.get("contract_size") or 0.0),
taker=i.get("taker_commission"), settle=i.get("settlement_currency"), blockmin=float(i.get("block_trade_min_trade_amount") or 0.0),
taker=float(i.get("taker_commission") or 0.0003), settle=i.get("settlement_currency"),
)) ))
return rows return rows
@@ -242,217 +244,310 @@ def rel_spread(r: dict) -> float:
return (r["ask"] - r["bid"]) / r["mid"] return (r["ask"] - r["bid"]) / r["mid"]
# ---------------------------------------------------------------- 1. i fatti del venue # ---------------------------------------------------------------- §0 le unita'
def unita(rows: list[dict], b: Budget, rete: bool) -> None:
print("\n" + "=" * 104)
print("0. L'UNITA' DEI CONTRATTI — due test empirici, prima di qualunque cifra in dollari")
print("=" * 104)
print(" Test 1 (dai dati gia' scaricati): `volume_usd / (volume x mark_price)`.")
print(" = 1 se `volume` e' in UNITA' DI SOTTOSTANTE; = contract_size se e' in CONTRATTI;")
print(" = prezzo del sottostante se il mark e' in cripto (inverse).")
print(f"\n {'famiglia':<10} {'n oss':>6} {'vol_usd/(vol*mark)':>20} {'c_size':>8} "
f"{'sottost.':>10} {'lettura':>26}")
for f in FAMS:
rr = [r for r in rows if r["fam"] == f and r["vol"] > 0 and r["mark"] > 0]
if not rr:
continue
q = float(np.median([r["vol_usd"] / (r["vol"] * r["mark"]) for r in rr]))
cs, S = rr[0]["csize"], float(np.nanmedian([r["S"] for r in rr]))
if abs(q - 1.0) < 0.35:
lettura = "unita' di sottostante"
elif abs(q - cs) / max(cs, 1) < 0.35:
lettura = "CONTRATTI"
elif abs(q - S) / max(S, 1) < 0.35:
lettura = "cripto/contratto (inverse)"
else:
lettura = "?? non concludente"
print(f" {f:<10} {len(rr):>6} {q:>20.4f} {cs:>8g} {S:>10.2f} {lettura:>26}")
if not rete:
print("\n Test 2 (size minima nel book): non girato (--cache non usa la rete).")
return
print("\n Test 2 (rete): la size MINIMA presente nel book. Se compare esattamente")
print(" `min_trade_amount`, allora e' quello il lotto minimo IN UNITA' DI SOTTOSTANTE.")
print(f"\n {'famiglia':<10} {'min_amt':>9} {'c_size':>8} {'min size vista':>15} "
f"{'libri':>6} {'verdetto':>12}")
for f in LINEAR:
cand = sorted([r for r in rows if r["fam"] == f and (r["bid"] or r["ask"])],
key=lambda r: -r["oi"])[:4]
sizes, nb = [], 0
for r in cand:
ob = get("get_order_book", {"instrument_name": r["name"], "depth": 10}, b)
if not ob:
continue
nb += 1
for side in ("bids", "asks"):
sizes += [float(l[1]) for l in (ob.get(side) or [])]
if not sizes:
continue
mn, ma, cs = min(sizes), cand[0]["minamt"], cand[0]["csize"]
v = "confermato" if abs(mn - ma) < 1e-9 else ("CONTRATTI" if abs(mn - ma * cs) < 1e-9
else "non concl.")
print(f" {f:<10} {ma:>9g} {cs:>8g} {mn:>15g} {nb:>6} {v:>12}")
print("\n ⚠️ Errore commesso e corretto in sessione: la prima stesura moltiplicava")
print(" min_trade_amount x contract_size, e dava XRP_USDC a $1.468.900 di nozionale per")
print(" lotto. Un numero assurdo e' un test: nessun venue retail quota un minimo da $1,5M.")
# ---------------------------------------------------------------- §1 fatti del venue
def famiglie(rows: list[dict]) -> dict: def famiglie(rows: list[dict]) -> dict:
print("\n" + "=" * 104) print("\n" + "=" * 104)
print("1a. I FATTI DEL VENUE — public/get_instruments + get_book_summary_by_currency") print("1. I FATTI DEL VENUE — public/get_instruments + get_book_summary_by_currency")
print("=" * 104) print("=" * 104)
print(f"{'famiglia':<10} {'strum':>6} {'OI>=100':>8} {'min_amt':>9} {'c_size':>7} " print(f"{'famiglia':<10} {'strum':>6} {'min_amt':>9} {'c_size':>7} {'tick':>9} "
f"{'tick':>9} {'sottost.$':>11} {'$/lotto':>10} {'taker':>8} {'settle':>7}") f"{'sottost.$':>11} {'$/lotto':>9} {'taker':>7} {'settle':>7}")
tab = {} tab = {}
for f in sorted({r["fam"] for r in rows}): for f in FAMS:
sub = [r for r in rows if r["fam"] == f] sub = [r for r in rows if r["fam"] == f]
liq = [r for r in sub if r["oi"] >= OI_MIN] if not sub:
continue
S = float(np.nanmedian([r["S"] for r in sub])) S = float(np.nanmedian([r["S"] for r in sub]))
mins = {r["minamt"] for r in sub}
cs = {r["csize"] for r in sub}
a = sub[0] a = sub[0]
lot = a["minamt"] * a["csize"] * S tab[f] = dict(n=len(sub), minamt=a["minamt"], csize=a["csize"], tick=a["tick"], S=S,
tab[f] = dict(n=len(sub), nliq=len(liq), minamt=a["minamt"], csize=a["csize"], lot=a["minamt"] * S, taker=a["taker"],
tick=a["tick"], S=S, lot=lot, taker=a["taker"]) nliq=sum(1 for r in sub if r["oi"] >= OI_MIN))
if f in LINEAR or f in INVERSE: print(f"{f:<10} {len(sub):>6} {a['minamt']:>9.4g} {a['csize']:>7.4g} {a['tick']:>9.5g} "
avv = "" if (len(mins) == 1 and len(cs) == 1) else " <- min/c_size NON uniformi" f"{S:>11.2f} {tab[f]['lot']:>9.2f} {a['taker']:>7.4g} {str(a['settle']):>7}")
print(f"{f:<10} {len(sub):>6} {len(liq):>8} {a['minamt']:>9.4g} {a['csize']:>7.4g} " print("\n `$/lotto` = min_trade_amount x sottostante = il NOZIONALE di un lotto (NON il premio,")
f"{a['tick']:>9.5g} {S:>11.2f} {lot:>10.2f} {str(a['taker']):>8} " print(" che e' molto minore). E' la granularita' minima imposta dal venue.")
f"{str(a['settle']):>7}{avv}")
print("\n `$/lotto` = min_trade_amount x contract_size x sottostante = il NOZIONALE di un")
print(" lotto (NON il premio, che e' molto minore). E' la granularita' minima imposta dal venue.")
print(" `taker` per le opzioni Deribit e' una frazione del SOTTOSTANTE con cap al 12.5% del") print(" `taker` per le opzioni Deribit e' una frazione del SOTTOSTANTE con cap al 12.5% del")
print(" premio: su un'ala molto OTM morde il CAP, non la percentuale.") print(" premio: sull'ala molto OTM morde il CAP, non la percentuale.")
print(" 📌 Il lotto minimo NON e' il vincolo: da $242 (ETH_USDC) a $1.469 (XRP_USDC), contro")
print(" i $7.738 di BTC inverse su cui il progetto aveva costruito il muro delle opzioni.")
return tab return tab
def replica(tab: dict) -> None: # ---------------------------------------------------------------- §2 liquidita', due modi
print("\n" + "-" * 104)
print("1b. CONTROLLO POSITIVO — riproduco i conteggi dichiarati dal coordinatore (22/08)?")
print("-" * 104)
atteso = {"BTC": (1038, 415), "ETH": (932, 548), "BTC_USDC": (686, 5), "ETH_USDC": (660, 119),
"SOL_USDC": (574, 341), "XRP_USDC": (522, 250), "HYPE_USDC": (396, 172),
"AVAX_USDC": (294, 146), "TRX_USDC": (310, 115)}
print(f"{'famiglia':<10} {'strum att':>10} {'strum oggi':>11} {'liq att':>8} {'liq oggi':>9}")
for f, (n_a, l_a) in atteso.items():
t = tab.get(f)
if t is None:
print(f"{f:<10} {n_a:>10} {'ASSENTE':>11} {l_a:>8} {'-':>9}")
else:
print(f"{f:<10} {n_a:>10} {t['n']:>11} {l_a:>8} {t['nliq']:>9}")
print(" La catena si muove di ora in ora (scadenze che nascono, OI che cambia): scarti di")
print(" poche unita' sono la misura, non un disaccordo. Uno scarto GRANDE sarebbe un problema.")
# ---------------------------------------------------------------- 2. liquidita' ai delta utili def liquidita(rows: list[dict], tab: dict) -> None:
print("\n" + "=" * 104)
print("2. LA LIQUIDITA', MISURATA IN DUE MODI CHE DANNO RISPOSTE OPPOSTE")
print("=" * 104)
print(" A) open interest >= 100 = quante POSIZIONI sono aperte (il filtro che usa")
print(" `collect_chain.py`, ereditato da bite, e la misura su cui poggia la domanda).")
print(" B) quote a DUE LATI presenti adesso = quanti strumenti sono NEGOZIABILI a un prezzo")
print(" noto. Sui soli put nella finestra 4-45 giorni, cioe' dove vive la struttura.")
print(f"\n{'famiglia':<10} {'A: OI>=100':>11} {'rango A':>8} {'put 4-45g':>10} "
f"{'B: 2 lati':>10} {'% quotati':>10} {'rango B':>8}")
stat = {}
for f in FAMS:
puts = [r for r in rows if r["fam"] == f and r["typ"] == "P" and DTE_LO <= r["dte"] <= DTE_HI]
two = [r for r in puts if r["bid"] is not None and r["ask"] is not None]
stat[f] = dict(oi=tab[f]["nliq"], puts=len(puts), two=len(two),
pct=(len(two) / len(puts) if puts else float("nan")))
rA = {f: i + 1 for i, f in enumerate(sorted(stat, key=lambda x: -stat[x]["oi"]))}
rB = {f: i + 1 for i, f in enumerate(sorted(stat, key=lambda x: -stat[x]["pct"]))}
for f in sorted(FAMS, key=lambda x: -stat[x]["oi"]):
s = stat[f]
print(f"{f:<10} {s['oi']:>11} {rA[f]:>8} {s['puts']:>10} {s['two']:>10} "
f"{s['pct']:>9.0%} {rB[f]:>8}")
print("\n 📌 LA CLASSIFICA SI ROVESCIA. SOL_USDC e' 1° per open interest e ULTIMO per quote")
print(" presenti; BTC_USDC, dichiarato 'praticamente morto' (5 strumenti con OI>=100), ha")
print(" quasi tutti i put quotati a due lati. L'open interest misura posizioni ACCUMULATE,")
print(" non la possibilita' di negoziare ADESSO — e sull'universo lineare le due cose non")
print(" hanno quasi nulla a che vedere.")
print(" ⚠️ Ricaduta sul codice di produzione: `collect_chain.py` prefiltra a OI>=100. Sulle")
print(" inverse non fa danno (sono quotate comunque), ma NON e' un filtro di liquidita' e il")
print(" suo nome lo lascia credere. 4ª occorrenza di 'una riga presente non e' un dato")
print(" presente' — qui in versione: 'una posizione aperta non e' un prezzo negoziabile'.")
def gambe(rows: list[dict], fam: str) -> list[dict]: # ---------------------------------------------------------------- §3 la struttura
def scegli(rows: list[dict], fam: str, eseguibile: bool) -> list[dict]:
"""Per ogni scadenza: la put piu' vicina a -0.28 (corta) e a -0.10 (lunga).
`eseguibile=False` -> fra TUTTI gli strike (la struttura che si vorrebbe).
`eseguibile=True` -> solo fra gli strike che hanno il LATO che serve: per APRIRE bisogna
vendere la corta al BID e comprare l'ala all'ASK. E' una restrizione
asimmetrica, ed e' quella giusta: un book a un lato solo non e' un
book vuoto, e' un book che serve solo una direzione.
"""
puts = [r for r in rows if r["fam"] == fam and r["typ"] == "P" puts = [r for r in rows if r["fam"] == fam and r["typ"] == "P"
and DTE_LO <= r["dte"] <= DTE_HI and np.isfinite(r["delta_bs"])] and DTE_LO <= r["dte"] <= DTE_HI and np.isfinite(r["delta"])]
out = [] out = []
for e in sorted({round(r["dte"], 4) for r in puts}): for e in sorted({round(r["dte"], 4) for r in puts}):
g = [r for r in puts if round(r["dte"], 4) == e] g = [r for r in puts if round(r["dte"], 4) == e]
s = min(g, key=lambda r: abs(r["delta_bs"] - SHORT_DELTA)) gs = [r for r in g if r["bid"] is not None] if eseguibile else g
lo = min(g, key=lambda r: abs(r["delta_bs"] - LONG_DELTA)) gl = [r for r in g if r["ask"] is not None] if eseguibile else g
if not gs or not gl:
continue
s = min(gs, key=lambda r: abs(r["delta"] - SHORT_DELTA))
lo = min(gl, key=lambda r: abs(r["delta"] - LONG_DELTA))
if s["K"] > lo["K"]: if s["K"] > lo["K"]:
out.append({"dte": e, "short": s, "long": lo, "n_strike": len(g)}) out.append({"dte": e, "short": s, "long": lo, "n": len(g)})
return out return out
def liquidita(rows: list[dict]) -> dict: def fee_lotto(r: dict) -> float:
"""Fee taker di UNA gamba, un lotto: min(0.03% del nozionale, 12.5% del premio)."""
return min(r["taker"] * r["S"] * r["minamt"], TAKER_CAP * usd(r["mark"], r))
def struttura(rows: list[dict]) -> dict:
print("\n" + "=" * 104) print("\n" + "=" * 104)
print(f"2. LA LIQUIDITA' AI DELTA CHE SERVONO ({SHORT_DELTA} corta / {LONG_DELTA} lunga),") print(f"3. LA STRUTTURA — put credit spread {SHORT_DELTA}/{LONG_DELTA}, finestra "
print(f" NON sull'ATM. Finestra {DTE_LO:.0f}-{DTE_HI:.0f} giorni. Delta = BS su mark_iv, r=0.") f"{DTE_LO:.0f}-{DTE_HI:.0f} giorni, per UN LOTTO MINIMO")
print("=" * 104) print("=" * 104)
print(f"{'famiglia':<10} {'scad':>5} {'2lati':>6} {'d corta':>8} {'d lunga':>8} " print(" `f_venue` = (bid_corta - ask_ala) / (mark_corta - mark_ala) = quanta parte del")
f"{'spr% corta':>11} {'spr% lunga':>11} {'credito$':>9} {'costo/cred':>11} " print(" credito che il venue stesso considera equo resta dopo aver attraversato lo spread.")
print(" E' il gemello del `f` misurato dal progetto il 30/07 (0,73 = errore del MODELLO):")
print(" qui il modello non c'entra, il riferimento e' il mark del venue.")
print(f"\n{'famiglia':<10} {'scad':>4} {'d corta':>8} {'d ala':>7} {'spr% corta':>11} "
f"{'spr% ala':>9} {'cred mark$':>11} {'cred eseg$':>11} {'f_venue':>8} "
f"{'fee/cred':>9} {'maxloss$':>9}") f"{'fee/cred':>9} {'maxloss$':>9}")
res = {} res = {}
for fam in list(LINEAR) + list(INVERSE): for f in FAMS:
gg = gambe(rows, fam) gg = scegli(rows, f, eseguibile=True)
if not gg: if not gg:
print(f"{fam:<10} {'0':>5} nessuna scadenza utilizzabile nella finestra") print(f"{f:<10} {'0':>4} nessuna scadenza con corta-bid E ala-ask presenti")
continue continue
ss, ll, ds, dl, cred, cost, fee, ml = [], [], [], [], [], [], [], [] acc = {k: [] for k in ("ds", "dl", "ss", "sl", "cm", "ce", "fee", "ml")}
due = neg = 0
for g in gg: for g in gg:
s, lo = g["short"], g["long"] s, lo = g["short"], g["long"]
rs, rl = rel_spread(s), rel_spread(lo) cm = usd(s["mark"], s) - usd(lo["mark"], lo)
if not (np.isfinite(rs) and np.isfinite(rl)): ce = usd(s["bid"], s) - usd(lo["ask"], lo)
if cm <= 0:
continue continue
due += 1 acc["ds"].append(s["delta"]); acc["dl"].append(lo["delta"])
c_mid = prem_usd(s["mid"], s) - prem_usd(lo["mid"], lo) acc["ss"].append(rel_spread(s)); acc["sl"].append(rel_spread(lo))
c_exe = prem_usd(s["bid"], s) - prem_usd(lo["ask"], lo) acc["cm"].append(cm); acc["ce"].append(ce)
if c_mid <= 0: acc["fee"].append(2.0 * (fee_lotto(s) + fee_lotto(lo)) / cm)
neg += 1 acc["ml"].append(loss_usd(s["K"], lo["K"], s) - ce)
continue if not acc["cm"]:
# fee taker Deribit: min(0.03% del sottostante, 12.5% del premio) per gamba, x2 gambe, print(f"{f:<10} {len(gg):>4} nessuna struttura con credito positivo al mark")
# x2 (apertura + chiusura/esercizio). Conservativo ma e' il listino.
def _fee(r: dict) -> float:
notion = r["S"] * r["csize"] * r["minamt"]
return min(float(r["taker"] or 0.0003) * notion, TAKER_CAP * prem_usd(r["mid"], r))
ss.append(rs); ll.append(rl)
ds.append(s["delta_bs"]); dl.append(lo["delta_bs"])
cred.append(c_mid)
cost.append((c_mid - c_exe) / c_mid)
fee.append(2.0 * (_fee(s) + _fee(lo)) / c_mid)
ml.append(loss_usd(s["K"], lo["K"], s) - c_exe)
if not cred:
print(f"{fam:<10} {len(gg):>5} {due:>6} nessuna struttura con credito positivo a mid "
f"({neg} a credito<=0)")
continue continue
r = dict(n=len(gg), due=due, neg=neg, s=float(np.median(ss)), l=float(np.median(ll)), m = {k: float(np.nanmedian(v)) if v else float("nan") for k, v in acc.items()}
ds=float(np.median(ds)), dl=float(np.median(dl)), cred=float(np.median(cred)), m["fv"] = float(np.median([e / c for e, c in zip(acc["ce"], acc["cm"])]))
cost=float(np.median(cost)), fee=float(np.median(fee)), ml=float(np.median(ml))) m["n"] = len(acc["cm"])
res[fam] = r res[f] = m
print(f"{fam:<10} {r['n']:>5} {due:>6} {r['ds']:>8.3f} {r['dl']:>8.3f} " print(f"{f:<10} {m['n']:>4} {m['ds']:>8.3f} {m['dl']:>7.3f} {m['ss']*100:>10.1f}% "
f"{r['s']*100:>10.1f}% {r['l']*100:>10.1f}% {r['cred']:>9.2f} " f"{m['sl']*100:>8.1f}% {m['cm']:>11.2f} {m['ce']:>11.2f} {m['fv']:>8.3f} "
f"{r['cost']*100:>10.1f}% {r['fee']*100:>8.1f}% {r['ml']:>9.2f}") f"{m['fee']*100:>8.1f}% {m['ml']:>9.2f}")
print("\n `2lati` = strutture in cui ENTRAMBE le gambe hanno bid E ask. Una riga presente non") print("\n ⚠️ `spr%` puo' essere NaN quando la gamba ha un lato solo: e' negoziabile per")
print(" e' un dato presente: una gamba con un solo lato non e' negoziabile a un prezzo noto.") print(" aprire (il lato che serve c'e') ma non ha un mid, quindi non ha uno spread definito.")
print(" `d corta`/`d lunga` = delta REALIZZATI (non i target): se la griglia degli strike e'") print(" `fee/cred` = 2 gambe x (apertura + chiusura) col cap 12.5%. VRP01 tiene FINO A")
print(" grossolana la struttura che compreresti non e' quella che hai chiesto.") print(" SCADENZA (nessuna gestione infra-settimana, il profit-take e' REFUTED il 30/07),")
print(" `costo/cred` = quanto dell'incasso a mid resta sul tavolo attraversando lo spread su") print(" quindi il costo realistico e' ~la META' di questa colonna + la delivery fee.")
print(" ENTRAMBE le gambe. Riferimento del progetto su BTC/ETH inverse: ~10% del credito.")
print(" `fee/cred` = listino taker Deribit, 2 gambe x apertura+chiusura, col cap 12.5%.") print(f"\n --- controllo: gli STESSI strike scelti fra TUTTI gli strike (non solo quotati) ---")
print(" `credito$` e `maxloss$` sono per UN LOTTO MINIMO.") print(f" {'famiglia':<10} {'ideale d corta':>15} {'eseg. d corta':>14} {'ideale d ala':>13} "
f"{'eseg. d ala':>12} {'scad ideali':>12} {'scad eseg.':>11}")
for f in FAMS:
gi, ge = scegli(rows, f, False), scegli(rows, f, True)
if not gi:
continue
di = float(np.median([g["short"]["delta"] for g in gi]))
li = float(np.median([g["long"]["delta"] for g in gi]))
de = float(np.median([g["short"]["delta"] for g in ge])) if ge else float("nan")
le = float(np.median([g["long"]["delta"] for g in ge])) if ge else float("nan")
print(f" {f:<10} {di:>15.3f} {de:>14.3f} {li:>13.3f} {le:>12.3f} "
f"{len(gi):>12} {len(ge):>11}")
print(" Se la colonna `eseg.` si allontana dal target -0.28/-0.10, la struttura che si")
print(" comprerebbe NON e' quella richiesta: la griglia degli strike quotati la deforma.")
return res return res
def profondita(rows: list[dict], fams: list[str], b: Budget) -> None: def profondita(rows: list[dict], fams: list[str], b: Budget) -> None:
print("\n" + "=" * 104) print("\n" + "=" * 104)
print("2b. PROFONDITA' IN CIMA AL BOOK (public/get_order_book depth=5), sulle gambe scelte") print("3b. PROFONDITA' IN CIMA AL BOOK (public/get_order_book depth=5) sulle gambe scelte")
print("=" * 104) print("=" * 104)
print(f"{'famiglia':<10} {'gamba':<6} {'n':>3} {'lotti al best':>14} {'$ premio al best':>17} " print(f"{'famiglia':<10} {'gamba':<6} {'n':>3} {'lotti al best':>14} {'$ premio al best':>17} "
f"{'ask/tick lunga':>15} {'max|delta venue-BS|':>21}") f"{'prezzo/tick':>12} {'max|d venue-BS|':>17}")
for fam in fams: for f in fams:
gg = gambe(rows, fam)[:4] # max 4 scadenze/famiglia: budget di rete gg = scegli(rows, f, eseguibile=True)[:4] # max 4 scadenze: budget di rete
for lato, key in (("corta", "short"), ("lunga", "long")): for lato, key, side in (("corta", "short", "bids"), ("ala", "long", "asks")):
lots, prem, dd, tk = [], [], [], [] lots, prem, tk, dd = [], [], [], []
for g in gg: for g in gg:
r = g[key] r = g[key]
ob = get("get_order_book", {"instrument_name": r["name"], "depth": 5}, b) ob = get("get_order_book", {"instrument_name": r["name"], "depth": 5}, b)
if not ob: if not ob or not (ob.get(side) or []):
continue continue
side = ob.get("bids") if key == "short" else ob.get("asks") px, sz = float(ob[side][0][0]), float(ob[side][0][1])
if not side:
continue
px, sz = float(side[0][0]), float(side[0][1])
lots.append(sz / r["minamt"] if r["minamt"] else float("nan")) lots.append(sz / r["minamt"] if r["minamt"] else float("nan"))
prem.append(prem_usd(px, r) * (sz / max(r["minamt"], 1e-12))) prem.append(usd(px, r) * (sz / max(r["minamt"], 1e-12)))
if r["tick"]: if r["tick"]:
tk.append(px / r["tick"]) tk.append(px / r["tick"])
gv = (ob.get("greeks") or {}).get("delta") gv = (ob.get("greeks") or {}).get("delta")
if gv is not None and np.isfinite(r["delta_bs"]): if gv is not None and np.isfinite(r["delta"]):
dd.append(abs(float(gv) - r["delta_bs"])) dd.append(abs(float(gv) - r["delta"]))
if not lots: if not lots:
print(f"{fam:<10} {lato:<6} {'0':>3} book vuoto su tutte le scadenze provate") print(f"{f:<10} {lato:<6} {'0':>3} book vuoto sul lato che serve")
continue continue
print(f"{fam:<10} {lato:<6} {len(lots):>3} {np.median(lots):>14.1f} " print(f"{f:<10} {lato:<6} {len(lots):>3} {np.median(lots):>14.1f} "
f"{np.median(prem):>17.2f} " f"{np.median(prem):>17.2f} {(np.median(tk) if tk else float('nan')):>12.1f} "
f"{(np.median(tk) if tk else float('nan')):>15.1f} " f"{(max(dd) if dd else float('nan')):>17.4f}")
f"{(max(dd) if dd else float('nan')):>21.4f}")
print("\n `lotti al best` = size in cima al book / lotto minimo: quante strutture minime") print("\n `lotti al best` = size in cima al book / lotto minimo: quante strutture minime")
print(" entrano senza muovere il prezzo. `ask/tick` sulla gamba LUNGA e' il controllo") print(" entrano senza muovere il prezzo. `prezzo/tick` e' il controllo d'artefatto del")
print(" d'artefatto del 30/07: un'ala a 1-2 tick e' un prezzo di griglia, non un prezzo.") print(" 30/07: un prezzo a 1-2 tick e' una griglia, non un prezzo.")
print(" `max|delta venue-BS|` valida il MIO calcolo del delta contro le greche del venue: se") print(" `max|d venue-BS|` valida il MIO delta contro le greche del venue: se non e' piccolo,")
print(" non e' piccolo, tutta la selezione degli strike qui sopra e' sbagliata.") print(" tutta la selezione degli strike qui sopra e' sbagliata.")
# ---------------------------------------------------------------- 3. collaterale # ---------------------------------------------------------------- §4 collaterale e capacita'
def collaterale(rows: list[dict], res: dict) -> None: def collaterale(rows: list[dict], res: dict) -> None:
print("\n" + "=" * 104) print("\n" + "=" * 104)
print("3. IL COLLATERALE — quanto conto serve per UNA struttura minima (conto reale $635)") print(f"4. COLLATERALE E CAPACITA'conto reale ${CAPITAL:.0f}")
print("=" * 104) print("=" * 104)
print(f"{'famiglia':<10} {'IM corta+ala $':>15} {'max-loss $':>11} {'IM/conto':>9} " print(f"{'famiglia':<10} {'IM corta+ala$':>14} {'max-loss$':>10} {'n @IM':>7} "
f"{'strutture @635 (IM)':>20} {'(max-loss)':>12}") f"{'n @maxloss':>11} {'n a budget 20%':>15} {'credito lordo$':>15}")
for fam in res: for f, m in res.items():
gg = gambe(rows, fam) gg = scegli(rows, f, eseguibile=True)
ims, mls = [], [] ims = []
for g in gg: for g in gg:
s, lo = g["short"], g["long"] s, lo = g["short"], g["long"]
if s["mid"] is None or lo["ask"] is None: if lo["ask"] is None:
continue continue
qty = s["csize"] * s["minamt"] ims.append(im_short_put(s["S"], s["K"], usd(s["mark"], s), s) + usd(lo["ask"], lo))
im = im_short_put_usd(s["S"], s["K"], prem_usd(s["mark"], s), qty)
ims.append(im + prem_usd(lo["ask"], lo)) # l'ala si PAGA: e' cassa, non margine
mls.append(loss_usd(s["K"], lo["K"], s))
if not ims: if not ims:
continue continue
im, ml = float(np.median(ims)), float(np.median(mls)) im, ml = float(np.median(ims)), m["ml"]
print(f"{fam:<10} {im:>15.2f} {ml:>11.2f} {im/CAPITAL:>8.1%} " n_budget = math.floor(RISK_BUDGET * CAPITAL / ml) if ml > 0 else 0
f"{CAPITAL/im:>20.1f} {CAPITAL/ml:>12.1f}") print(f"{f:<10} {im:>14.2f} {ml:>10.2f} {CAPITAL/im:>7.1f} {CAPITAL/ml:>11.1f} "
f"{n_budget:>15} {n_budget*m['ce']:>15.2f}")
print("\n `IM corta+ala` = margine iniziale della sola put venduta (formula PUBBLICATA dal") print("\n `IM corta+ala` = margine iniziale della sola put venduta (formula PUBBLICATA dal")
print(" venue, FONTE SECONDARIA) + il premio dell'ala, che si paga in contanti. E' il") print(" venue, FONTE SECONDARIA) + il premio dell'ala, che si paga in contanti = il")
print(" fabbisogno se il venue NON netta le gambe.") print(" fabbisogno se il venue NON netta le gambe. `max-loss` = il fabbisogno se LE NETTA.")
print(" `max-loss` = perdita massima strutturale = il fabbisogno se il venue LE NETTA.") print(" ⚠️ Quale dei due valga NON e' leggibile dall'API pubblica: si chiude con")
print(" ⚠️ La differenza fra i due numeri e' l'unica cosa importante di questa tabella e NON") print(" `private/get_margins` (lettura autenticata, fuori dal perimetro di questo file).")
print(" E' LEGGIBILE dall'API pubblica: si chiude con `private/get_margins` (lettura, ma") print(" Ma la conclusione NON dipende da quale sia: il collaterale non e' il vincolo in")
print(" autenticata -> fuori dal perimetro dichiarato di questo file).") print(" nessuna delle due letture.")
print(f" `n a budget 20%` = strutture che stanno in {RISK_BUDGET:.0%} del conto a max-loss;")
print(" `credito lordo` = il loro credito ESEGUIBILE sommato, per un ciclo. E' un incasso")
print(" LORDO PRIMA DI QUALUNQUE PERDITA, non un rendimento atteso: la struttura perde fino")
print(" a ~7x il credito quando va male (misura del progetto sul canonico BTC/ETH).")
def costo_raccolta(tab: dict) -> None: def costo_raccolta(rows: list[dict], tab: dict) -> None:
print("\n" + "=" * 104) print("\n" + "=" * 104)
print("4. COSTO DI RACCOLTA — cosa costerebbe aggiungere una famiglia al giro esistente") print("5. COSTO DI RACCOLTA — cosa costerebbe aggiungere una famiglia al giro esistente")
print("=" * 104) print("=" * 104)
print(" `collect_chain.py` oggi: ~650 chiamate in ~160s a 4 rps (BTC+ETH inverse), al :25.") print(" `collect_chain.py` oggi: ~650 chiamate in ~160s a 4 rps (BTC+ETH inverse), al :25.")
print(f"{'famiglia':<10} {'chiamate/giro':>14} {'+s a 4 rps':>11} {'+% sul giro':>12}") print(f"{'famiglia':<10} {'a OI>=100':>10} {'a 2-lati':>9} {'+s a 4rps (2-lati)':>19} "
f"{'+% sul giro':>12}")
for f in LINEAR: for f in LINEAR:
t = tab.get(f) sub = [r for r in rows if r["fam"] == f]
if t: two = sum(1 for r in sub if r["bid"] is not None and r["ask"] is not None)
n = t["nliq"] + 2 # + get_instruments + get_book_summary print(f"{f:<10} {tab[f]['nliq']:>10} {two:>9} {(two+2)/4.0:>19.1f} "
print(f"{f:<10} {n:>14.0f} {n/4.0:>11.1f} {n/650*100:>11.0f}%") f"{(two+2)/650*100:>11.0f}%")
print("\n ⚠️ Il vincolo non e' la CPU ma il rate limit Deribit PER-IP, gia' costato un guasto") print("\n ⚠️ Il vincolo non e' la CPU ma il rate limit Deribit PER-IP, gia' costato un guasto")
print(" il 29/07 (fallback silenzioso del feed 5m di SKH01). Ogni famiglia aggiunta allunga") print(" il 29/07 (fallback silenzioso del feed 5m di SKH01). Ogni famiglia aggiunta allunga")
print(" la finestra in cui il giro occupa l'IP.") print(" la finestra in cui il giro occupa l'IP.")
@@ -471,7 +566,7 @@ def main() -> int:
print("=" * 104) print("=" * 104)
print("Sola lettura pubblica, nessun ordine. UNO SNAPSHOT: qui non esiste un backtest.") print("Sola lettura pubblica, nessun ordine. UNO SNAPSHOT: qui non esiste un backtest.")
b = Budget() b, rete = Budget(), not a.cache
if a.cache and CACHE.exists(): if a.cache and CACHE.exists():
raw = json.loads(CACHE.read_text()) raw = json.loads(CACHE.read_text())
print(f"\n[cache] {CACHE}") print(f"\n[cache] {CACHE}")
@@ -481,8 +576,7 @@ def main() -> int:
raw = fetch_all(b) raw = fetch_all(b)
CACHE.parent.mkdir(parents=True, exist_ok=True) CACHE.parent.mkdir(parents=True, exist_ok=True)
CACHE.write_text(json.dumps(raw)) CACHE.write_text(json.dumps(raw))
print(f"\n[rete] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429, " print(f"\n[rete] {b.calls} chiamate, {b.err} errori, {b.r429} 429, {time.time()-t0:.0f}s")
f"{time.time()-t0:.0f}s")
rows = build(raw) rows = build(raw)
print(f"[dato] {len(rows)} strumenti con specifiche E quote — snapshot " print(f"[dato] {len(rows)} strumenti con specifiche E quote — snapshot "
@@ -491,38 +585,38 @@ def main() -> int:
print("NESSUN DATO: il venue non ha risposto. Verdetto: non misurabile.") print("NESSUN DATO: il venue non ha risposto. Verdetto: non misurabile.")
return 1 return 1
unita(rows, b, rete)
tab = famiglie(rows) tab = famiglie(rows)
replica(tab) liquidita(rows, tab)
res = liquidita(rows) res = struttura(rows)
vive = [f for f in res if res[f]["due"] >= 2] vive = [f for f in res if res[f]["n"] >= 2]
if a.cache: if rete and vive:
print("\n2b. PROFONDITA': non girata (--cache non usa la rete).")
elif not vive:
print("\n2b. PROFONDITA': non girata — nessuna famiglia con >=2 strutture a due lati.")
else:
guardia_finestra() guardia_finestra()
profondita(rows, vive, b) profondita(rows, vive, b)
else:
print("\n3b. PROFONDITA': non girata (--cache, oppure nessuna famiglia con >=2 strutture).")
collaterale(rows, res) collaterale(rows, res)
costo_raccolta(tab) costo_raccolta(rows, tab)
print("\n" + "=" * 104) print("\n" + "=" * 104)
print("5. COSA QUESTO FILE NON PUO' DIRE (dichiarato prima di qualunque verdetto)") print("6. COSA QUESTO FILE NON PUO' DIRE (dichiarato prima di qualunque verdetto)")
print("=" * 104) print("=" * 104)
print(" * Non c'e' STORIA: il collettore raccoglie BTC/ETH inverse, l'archivio ereditato pure.") print(" * Non c'e' STORIA: il collettore raccoglie BTC/ETH inverse, l'archivio ereditato")
print(" Zero giorni di catena su SOL/XRP/HYPE/AVAX/*_USDC -> nessun backtest, nessun") print(" pure. Zero giorni di catena su queste famiglie -> nessun backtest, nessun hold-out,")
print(" hold-out, nessun deflated-Sharpe. Il massimo verdetto ottenibile e' LEAD.") print(" nessun deflated-Sharpe. Il verdetto massimo ottenibile e' LEAD.")
print(" * Non c'e' IV-RANK: il gate>0.30 e' l'unico alpha misurato di VRP01 e richiede la") print(" * Non c'e' IV-RANK: il gate>0.30 e' l'unico alpha misurato di VRP01 e richiede la")
print(" distribuzione storica della vol implicita del sottostante. Senza, la struttura non") print(" distribuzione storica della vol implicita del sottostante. Senza, la struttura non")
print(" e' VRP01: e' vendere vol a caso. (Su BTC/ETH quel gate e' passato 0/19 settimane.)") print(" e' VRP01: e' vendere vol a caso. (Su BTC/ETH quel gate e' passato 0/19 settimane.)")
print(" * Non c'e' STRESS nel campione: la regola 'niente short-vol da modello in deploy' e'") print(" * Non c'e' STRESS nel campione: la regola 'niente short-vol da modello in deploy' e'")
print(" del 19/06 e la condizione dichiarata per rivalutarla e' un crash CATTURATO.") print(" del 19/06 e la condizione dichiarata per rivalutarla e' un crash CATTURATO.")
print(" * Il margine e' da FORMULA PUBBLICATA, non dall'API: il netting delle gambe non e'") print(" * Il margine e' da FORMULA PUBBLICATA, non dall'API, e il netting delle gambe non e'")
print(" leggibile pubblicamente.") print(" leggibile pubblicamente.")
print(" * E' UN ISTANTE: uno spread relativo mediano misurato a un'ora del sabato non e' la") print(" * E' UN ISTANTE, per giunta di SABATO pomeriggio: uno spread relativo mediano a")
print(" sua distribuzione. Servirebbe almeno un ciclo settimanale per sapere se e' tipico.") print(" un'ora del fine settimana non e' la sua distribuzione. Un book alt puo' essere")
print(f"[rete, totale] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429") print(" molto piu' quotato in settimana. Servirebbe almeno un ciclo settimanale.")
print(f"\n[rete, totale] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429")
return 0 return 0
+10
View File
@@ -512,6 +512,16 @@ def basis_vs_funding(asset: str, cv: pd.DataFrame) -> None:
f" (b~1 e a~0 = previsore non distorto => niente da incassare)") f" (b~1 e a~0 = previsore non distorto => niente da incassare)")
print(f" PREMIO INCASSABILE = implicito - realizzato = {np.mean(prem)*100:+6.2f}%/anno" print(f" PREMIO INCASSABILE = implicito - realizzato = {np.mean(prem)*100:+6.2f}%/anno"
f" IC95% a blocchi [{lo*100:+.2f}%, {hi*100:+.2f}%] (n={m.sum():,} ore)") f" IC95% a blocchi [{lo*100:+.2f}%, {hi*100:+.2f}%] (n={m.sum():,} ore)")
# ATTENZIONE: annualizzare dividendo per tau ESPLODE il rumore a tau piccolo
# (5 bps di rumore a 7 giorni dalla scadenza diventano +2.6%/anno). Media per bucket:
tt = tau[m] / 24.0
print(" premio per giorni-a-scadenza (l'annualizzazione a tau piccolo amplifica il rumore):")
for lo_d, hi_d, lab in [(5, 30, " 5-30g"), (30, 60, " 30-60g"), (60, 100, "60-100g")]:
k = (tt >= lo_d) & (tt < hi_d)
if k.sum() > 500:
print(f" {lab}: implicito {np.mean(x[k])*100:+6.2f}% realizzato "
f"{np.mean(y[k])*100:+6.2f}% premio {np.mean(prem[k])*100:+6.2f}%/anno "
f"(sd {np.std(prem[k])*100:5.1f}, n={k.sum():,})")
# ========================================================================== # ==========================================================================
+662
View File
@@ -0,0 +1,662 @@
"""MONITOR-AUDIT — quanti dei sei forward-monitor registrano la giornata che credono di registrare?
PERCHE'. Il filone XSR-REPRO (22/08) ha trovato che `paper_xsr.advance()` consuma la barra 1d
del giorno IN CORSO — `fetch_hyperliquid` gira nel cron delle 00:30 con END = oggi e scrive una
barra parziale — e poi porta `last_ts` su quella barra: le 23 ore e mezza restanti di ogni giorno
non entrano in nessun rendimento registrato. Misura: 1/28 barre identiche al replay, corr -0,045,
vol registrata 0,46% contro 2,74% ricalcolata.
Il difetto non e' di XSR01: e' della FORMA `new = [i for i in range(len(ts)) if ts[i] > last_ts]`
applicata a un pannello la cui ultima riga e' provvisoria. Quella forma e' copiata in sei file.
Tre decisioni PRE-REGISTRATE si leggono su queste serie:
STATARB 2026-09-27 (r0724_statarb_deploy_gate: Sharpe di net_modeled >= 0.35)
XSR01 2026-10-23 (r0725_xsr_deploy_gate: Sharpe >= 1.0 E haircut $5.000 <= 40%)
DVOLSPREAD 2026-10-24 kill / 2027-01-24 decisione (soglie dentro paper_dvolspread)
e `monitor_health` dichiara **OK** tutti e sei, perche' misura freschezza e buchi: una serie
fresca, completa e SBAGLIATA passa ogni controllo di freschezza.
QUESTO SCRIPT E' UN AUDIT, NON UNA RIPARAZIONE. E' in SOLA LETTURA su tutto cio' che e'
produzione: importa i moduli di `scripts/live/` senza chiamarne `main()`, e prima di rieseguirne
`advance()` sostituisce `_append` con una funzione che raccoglie in memoria — nessun file di
`data/` viene aperto in scrittura. La riparazione e' una decisione dell'operatore e ha un costo
(azzerare una finestra forward): la sezione E lo quantifica.
METODO — per ogni monitor tre domande, ognuna con una PROVA e non con la lettura di un docstring
(il progetto ha gia' trovato QUATTRO docstring di produzione che dichiaravano una causalita' che
il codice non aveva: `_skyhook_positions`, `resample_5m`, `current_target` di TP01, e
`prevday_breakout.current_target` qui sotto):
A. DA DOVE VIENE L'ULTIMA BARRA. Si misura la COMPLETEZZA dell'ultima riga di ogni sorgente
con un contatore indipendente dal prezzo: quante barre 5m stanno dentro l'ultima barra 1h,
quante barre 1h dentro l'ultimo giorno, quanto volume ha l'ultima barra HL rispetto alla
mediana, quanti giorni di ritardo ha l'ultima barra IB.
B. IL DIFETTO MORDE? Replay della strategia CONGELATA sui dati di oggi dall'inception, contro
cio' che il monitor ha registrato giorno per giorno, appaiato per timestamp. Non "legge una
barra parziale" ma **quanta parte della giornata finisce nella serie registrata**: barre
identiche, correlazione, rapporto di varianza -> minuti al giorno effettivamente misurati.
C. QUALE GATE NE DIPENDE e se il criterio resta leggibile.
Poi:
D. LA GUARDIA CHE MANCA a `monitor_health`, con soglia dichiarata e controllo positivo nei due
versi (un rilevatore tarato per non segnalare e' indistinguibile da uno rotto).
E. IL COSTO DELLA RIPARAZIONE per gate: giorni di finestra persi, data in cui il contatore
nuovo tornerebbe alla stessa numerosita', e se esiste una RICOSTRUZIONE a posteriori.
COSA MI ASPETTAVO PRIMA DI MISURARE: che i tre monitor giornalieri su feed crypto (xsr, statarb,
dvolspread) avessero la stessa patologia identica; che `paper_prevday`, essendo su griglia 1h con
24 barre al giorno, ne avesse una MOLTO piu' mite (una barra su 24 troncata); e che
`paper_combo`, che vive sul calendario di borsa, fosse o rotto come gli altri o sano per caso.
Solo la terza aspettativa e' stata una sorpresa.
nice -n 19 timeout 900 uv run python scripts/research/r0822_monitor_audit.py
"""
from __future__ import annotations
import csv
import importlib.util
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
RAW = ROOT / "data" / "raw"
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"):
sys.path.insert(0, str(_p))
ANN = np.sqrt(365.0)
NOW = datetime.now(timezone.utc)
# --------------------------------------------------------------------------------------------
# Caricamento SOLA LETTURA di un modulo di produzione: si esegue il modulo (definizioni), mai
# `main()`. `_append` viene sostituito subito dopo, prima di qualunque chiamata ad `advance`.
# --------------------------------------------------------------------------------------------
_MODS: dict[str, object] = {}
def live(name: str):
if name in _MODS:
return _MODS[name]
spec = importlib.util.spec_from_file_location(f"_ro_{name}", ROOT / "scripts" / "live" / f"{name}.py")
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
_MODS[name] = m
return m
class Sink:
"""Sostituisce `_append` dei monitor: raccoglie in memoria invece di scrivere su disco.
E' l'unica ragione per cui questo audit puo' rieseguire il codice di produzione TALE E QUALE
(nessuna reimplementazione -> nessun drift fra audit e monitor) restando in sola lettura."""
def __init__(self):
self.rows: list[tuple[str, dict]] = []
def __call__(self, path, rec):
self.rows.append((Path(path).name, dict(rec)))
def of(self, fname: str) -> list[dict]:
return [r for n, r in self.rows if n == fname]
def read_jsonl(p: Path) -> list[dict]:
if not p.exists():
return []
return [json.loads(x) for x in p.read_text().splitlines() if x.strip()]
def read_equity_csv(p: Path, col: int = 1) -> tuple[list[pd.Timestamp], np.ndarray]:
"""(date, equity) da un equity.csv dei paper_portfolio / paper_combo."""
dts, eq = [], []
with p.open() as fh:
for row in csv.reader(fh):
if not row or row[0] == "date":
continue
try:
v = float(row[col])
except (ValueError, IndexError):
continue
dts.append(pd.Timestamp(row[0]))
eq.append(v)
return dts, np.asarray(eq, float)
# --------------------------------------------------------------------------------------------
# Statistica del confronto registrato-vs-replay. La domanda non e' "coincide?" ma "quanta parte
# della giornata e' finita nella serie": in radice di tempo il rapporto di VARIANZA fra le due
# serie e' la frazione di giornata misurata (ipotesi: random walk sull'intervallo troncato).
# --------------------------------------------------------------------------------------------
def compare(rec: np.ndarray, rep: np.ndarray) -> dict:
ok = np.isfinite(rec) & np.isfinite(rep)
a, b = rec[ok], rep[ok]
n = len(a)
if n < 3:
return dict(n=n, ident=0, corr=float("nan"), vr=float("nan"), minuti=float("nan"),
sd_rec=float("nan"), sd_rep=float("nan"), sh_rec=float("nan"), sh_rep=float("nan"))
ident = int((np.abs(a - b) <= 1e-6).sum())
corr = float(np.corrcoef(a, b)[0, 1]) if a.std() > 0 and b.std() > 0 else float("nan")
vr = float((a.std() / b.std()) ** 2) if b.std() > 0 else float("nan")
return dict(n=n, ident=ident, corr=corr, vr=vr, minuti=vr * 1440.0,
sd_rec=float(a.std() * ANN * 100), sd_rep=float(b.std() * ANN * 100),
sh_rec=float(a.mean() / a.std() * ANN) if a.std() > 0 else float("nan"),
sh_rep=float(b.mean() / b.std() * ANN) if b.std() > 0 else float("nan"))
def show(tag: str, c: dict, note: str = "") -> None:
print(f" barre appaiate {c['n']} identiche entro 1e-6: {c['ident']}/{c['n']} "
f"corr {c['corr']:+.4f}")
print(f" vol annualizzata REGISTRATA {c['sd_rec']:6.2f}% RICALCOLATA {c['sd_rep']:6.2f}%"
f" rapporto di varianza {c['vr']:.4f}")
print(f" -> il monitor misura ~{c['minuti']:,.0f} minuti di mercato al giorno (su 1440){note}")
print(f" Sharpe della finestra: REGISTRATO {c['sh_rec']:+.2f} RICALCOLATO {c['sh_rep']:+.2f}"
f" [SE ~{np.sqrt(365.0 / max(c['n'], 1)):.2f}: NON e' una lettura di gate]")
RES: dict[str, dict] = {} # riepilogo per la tabella finale
# ============================================================================================
# A — PROVENIENZA: quanto e' completa l'ultima riga di ogni sorgente
# ============================================================================================
def sezione_A() -> dict:
print("=" * 102)
print(" A — DA DOVE VIENE L'ULTIMA BARRA (completezza misurata sul disco, non dedotta)")
print("=" * 102)
src = {}
print("\n A1. BTC/ETH — il feed 1h certificato (scritto da rebuild_history nel cron delle 00:30)")
for a in ("btc", "eth"):
d1 = pd.read_parquet(RAW / f"{a}_1h.parquet", columns=["timestamp", "volume"])
t1 = pd.to_datetime(d1["timestamp"], unit="ms", utc=True)
d5 = pd.read_parquet(RAW / f"{a}_5m.parquet", columns=["timestamp"])
t5 = pd.to_datetime(d5["timestamp"], unit="ms", utc=True)
last_h = t1.iloc[-1]
n5 = int(((t5 >= last_h) & (t5 < last_h + pd.Timedelta("1h"))).sum())
n1d = int((t1 >= last_h.normalize()).sum())
src[a] = dict(last=last_h, n5=n5, n1d=n1d, mtime=datetime.fromtimestamp(
(RAW / f"{a}_1h.parquet").stat().st_mtime, tz=timezone.utc))
print(f" {a.upper()} ultima barra 1h {last_h} barre 5m dentro: {n5}/12 "
f"(~{n5 * 5} minuti su 60)")
print(f" barre 1h dentro l'ultimo giorno UTC: {n1d}/24 -> la barra 1d che ne "
f"esce dal resample copre ~{n5 * 5 + (n1d - 1) * 60} minuti su 1440")
print(f" parquet scritto alle {src[a]['mtime']:%H:%M:%S} UTC")
print(" `resample_tf` (src/strategies/trend_portfolio.py:195) NON scarta la barra in corso —")
print(" lo dichiara il suo stesso docstring — e `altlib.get(asset,'1d')` ci passa dentro.")
print("\n A2. Hyperliquid 1d (scritto da fetch_hyperliquid nello stesso cron, END = oggi)")
pq = RAW / "hl_btc_1d.parquet"
d = pd.read_parquet(pq)
v = d["volume"].astype(float).values
tsl = pd.Timestamp(int(d["timestamp"].iloc[-1]), unit="ms", tz="UTC")
mt = pd.Timestamp(pq.stat().st_mtime, unit="s", tz="UTC")
q = float(v[-1] / np.median(v[-40:-1]))
orolog = (mt - tsl).total_seconds() / 60.0
src["hl"] = dict(last=tsl, frac=q, min_orologio=orolog)
print(f" hl_btc_1d ultima barra {tsl} parquet scritto alle {mt:%H:%M:%S} UTC")
print(f" misura da OROLOGIO (diretta): la barra era aperta da {orolog:.0f} minuti su 1440 "
f"quando e' stata scritta")
print(f" misura da VOLUME (proxy): {v[-1]:,.0f} = {q * 100:.1f}% del volume mediano di "
f"un giorno pieno -> {q * 1440:,.0f} minuti")
print(" ⚠ i due stimatori NON coincidono (fattore ~2) e la discrepanza va detta invece che")
print(" mediata: il volume dei primi 30 minuti UTC non e' 1/48 di quello giornaliero")
print(" (stagionalita' intragiornaliera). Il numero da usare e' quello da OROLOGIO — e'")
print(" una misura diretta del tempo trascorso; il volume e' solo un controllo di segno.")
print("\n A3. ETF/IB 1d (fetch_ib_equities, stesso cron ~00:35 UTC) e DVOL")
for sym in ("spy", "tlt"):
p = RAW / f"eq_{sym}_1d.parquet"
if not p.exists():
continue
e = pd.read_parquet(p)
last = pd.Timestamp(int(e["timestamp"].iloc[-1]), unit="ms", tz="UTC")
lag = (pd.Timestamp(NOW).normalize() - last.normalize()).days
src[f"eq_{sym}"] = dict(last=last, lag=lag)
print(f" eq_{sym}_1d ultima barra {last.date()} = {lag} giorni fa "
f"(la borsa USA chiude alle 20:00 UTC: alle 00:35 l'ultima barra e' COMPLETA)")
for a in ("btc", "eth"):
p = RAW / f"dvol_{a}.parquet"
if p.exists():
dv = pd.read_parquet(p)
tl = pd.Timestamp(int(dv["timestamp"].iloc[-1]), unit="ms", tz="UTC")
print(f" dvol_{a} ultima barra {tl}")
print("\n LETTURA: tre sorgenti su quattro consegnano un'ultima riga PROVVISORIA (crypto 1h,")
print(" crypto 1d, HL 1d); la quarta (IB) e' l'unica che consegna solo barre chiuse, e non per")
print(" merito del nostro codice ma perche' la borsa e' chiusa quando il cron gira.")
return src
# ============================================================================================
# B — IL DIFETTO MORDE? replay contro registrato, monitor per monitor
# ============================================================================================
def b_xsr():
print("\n" + "=" * 102)
print(" B1 — paper_xsr (CONTROLLO: difetto gia' stabilito da r0822_xsr_repro, qui replicato)")
print("=" * 102)
px = live("paper_xsr")
st = json.loads((ROOT / "data" / "paper_xsr" / "state.json").read_text())
rec = read_jsonl(ROOT / "data" / "paper_xsr" / "returns.jsonl")
sink = Sink(); px._append = sink
ts, dt, W, R, rb, syms = px.build_panel()
st0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, syms=st["syms"],
modeled=px._book(px.MODELED_CAPITAL, len(st["syms"])),
reals={n: px._book(c, len(st["syms"])) for c, n in px.REAL_BOOKS})
px.advance(st0)
rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")}
pairs = [(float(r["net_modeled"]), rep[int(r["ts"])]) for r in rec if int(r["ts"]) in rep]
c = compare(np.array([p[0] for p in pairs]), np.array([p[1] for p in pairs]))
print(f"\n pannello: `build_panel` -> `load_hl` -> parquet HL 1d (ultima riga = giorno in corso)")
show("xsr", c)
RES["paper_xsr"] = dict(parziale="SI", cmp=c, gate="XSR01 2026-10-23",
src="hl_*_1d.parquet", cad_h=24.0)
def b_statarb():
print("\n" + "=" * 102)
print(" B2 — paper_statarb (gate PRE-REGISTRATO 2026-09-27)")
print("=" * 102)
ps = live("paper_statarb")
st = json.loads((ROOT / "data" / "paper_statarb" / "state.json").read_text())
rec = read_jsonl(ROOT / "data" / "paper_statarb" / "returns.jsonl")
j = ps.build_joint("1d")
ts, dt, pos, sr = ps._signal(j)
i0 = int(np.where(ts == st["start_ts"])[0][0])
sink = Sink(); ps._append = sink
st0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0,
pos_modeled=float(pos[i0]), pos_real=float(pos[i0]),
cap_modeled=ps.MODELED_CAPITAL, cap_real=ps.REAL_CAPITAL,
peak_modeled=ps.MODELED_CAPITAL, peak_real=ps.REAL_CAPITAL,
dd_modeled=0.0, dd_real=0.0, n_trades=0)
ps.advance(st0, j)
rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")}
pairs = [(float(r["net_modeled"]), rep[int(r["ts"])]) for r in rec if int(r["ts"]) in rep]
c = compare(np.array([p[0] for p in pairs]), np.array([p[1] for p in pairs]))
print("\n pannello: `build_joint('1d')` -> `altlib.get(...,'1d')` -> `resample_tf` "
"(ultima riga = 1 barra 1h su 24)")
show("statarb", c)
RES["paper_statarb"] = dict(parziale="SI", cmp=c, gate="STATARB 2026-09-27",
src="btc/eth 1h -> resample 1d", cad_h=24.0)
def b_dvolspread():
print("\n" + "=" * 102)
print(" B3 — paper_dvolspread (kill PRE-REGISTRATO 2026-10-24, decisione 2027-01-24)")
print("=" * 102)
pv = live("paper_dvolspread")
st = json.loads((ROOT / "data" / "paper_dvolspread" / "state.json").read_text())
rec = read_jsonl(ROOT / "data" / "paper_dvolspread" / "returns.jsonl")
P = pv._panel()
i0 = int(np.where(P["ts"] == st["start_ts"])[0][0])
sink = Sink(); pv._append = sink
st0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, n_active=0,
n_flat_signal=0, n_flat_nodata=0,
wb_modeled=float(P["wb"][i0]), wb_real=float(P["wb"][i0]),
cap_modeled=pv.MODELED_CAPITAL, cap_real=pv.REAL_CAPITAL,
peak_modeled=pv.MODELED_CAPITAL, peak_real=pv.REAL_CAPITAL,
dd_modeled=0.0, dd_real=0.0, n_flips=0, frozen=pv.FROZEN)
pv.advance(st0, P)
rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")}
pairs = [(float(r["net_modeled"]), rep[int(r["ts"])]) for r in rec if int(r["ts"]) in rep]
c = compare(np.array([p[0] for p in pairs]), np.array([p[1] for p in pairs]))
print("\n pannello: `_panel` -> `ortholib.aligned` -> `altlib.get(...,'1d')` -> `resample_tf`")
show("dvolspread", c)
print("\n ⚠ La contabilita' a 3 stati (ATTIVE / flat-da-segnale / flat-SENZA-DATO) e il veto")
print(" d'integrita' all'80% funzionano e restano validi: contano se il DVOL c'era. Non")
print(" possono vedere QUANTO dura la barra su cui il DVOL c'era — sono ortogonali a questo")
print(" difetto, ed e' il motivo per cui l'unico monitor con una guardia dedicata e'")
print(" compromesso quanto quelli senza.")
RES["paper_dvolspread"] = dict(parziale="SI", cmp=c, gate="DVOLSPREAD 2026-10-24 / 2027-01-24",
src="btc/eth 1h -> resample 1d + dvol", cad_h=24.0)
def b_prevday():
print("\n" + "=" * 102)
print(" B4 — paper_prevday (griglia ORARIA: la patologia puo' essere diversa — si misura)")
print("=" * 102)
pp = live("paper_prevday")
st = json.loads((ROOT / "data" / "paper_prevday" / "state.json").read_text())
rec = read_jsonl(ROOT / "data" / "paper_prevday" / "returns.jsonl")
dfs = pp.build_bars()
S = st["start_ts"]
pos = {a: pp.pb.current_target(dfs[a][dfs[a]["timestamp"] <= S]) for a in pp.ASSETS}
sink = Sink(); pp._append = sink
st0 = dict(start_ts=S, last_ts=S, n_bars=0, pos_modeled=pos, pos_real=dict(pos),
cap_modeled=pp.MODELED_CAPITAL, cap_real=pp.REAL_CAPITAL,
peak_modeled=pp.MODELED_CAPITAL, peak_real=pp.REAL_CAPITAL,
dd_modeled=0.0, dd_real=0.0, n_trades=0)
pp.advance(st0, dfs)
rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")}
pairs = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])])
for r in rec if int(r["ts"]) in rep]
A = np.array([p[1] for p in pairs]); B = np.array([p[2] for p in pairs])
c = compare(A, B)
print("\n pannello: `build_bars` -> `harness.load(asset,'1h')` = il parquet 1h GREZZO, "
"nessun resample")
show("prevday", c, note=" <- 24 barre/giorno, quindi il conto e' sull'INTERA giornata")
diff = np.abs(A - B) > 1e-6
hh = pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True).hour
print(f"\n DOVE cade la divergenza: {int(diff.sum())} barre su {len(A)} "
f"({diff.mean() * 100:.1f}%)")
vc = pd.Series(hh[diff]).value_counts().sort_index()
print(" ora UTC delle barre divergenti: " +
", ".join(f"{int(h):02d}:00 x{int(n)}" for h, n in vc.items()))
giorni = len(set(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True).date))
print(f" = ESATTAMENTE una barra per giorno su {giorni} giorni, e sempre la barra 00:00 —")
print(" cioe' quella che il cron delle 00:30 trova aperta da ~35 minuti.")
print(f"\n ⚠ LA MISURA CHE CONTA E' L'ALTRA META': {int((~diff).sum())} barre su {len(A)} "
"coincidono BIT A BIT con")
print(" il replay fatto oggi. Non e' un dettaglio contabile: e' la prova indipendente che il")
print(" feed BTC/ETH certificato NON riscrive le barre gia' chiuse (62 giorni di riscritture")
print(" notturne). Senza questa prova la ricostruzione della sezione E sarebbe un'ipotesi.")
RES["paper_prevday"] = dict(parziale="SI", cmp=c, gate="nessuno (lead in monitor)",
src="btc/eth 1h grezzo", cad_h=1.0)
def b_portfolio():
print("\n" + "=" * 102)
print(" B5 — paper_portfolio (book di RICERCA a 5 sleeve: TP01+XS01+VRP01+SKH01+GTAA01)")
print("=" * 102)
ppf = live("paper_portfolio")
dts, eq = read_equity_csv(ROOT / "data" / "paper_portfolio" / "equity.csv")
_, r = ppf.portfolio_daily()
ridx = {pd.Timestamp(k): float(v) for k, v in r.items()}
A, B = [], []
for i in range(1, len(dts)):
d = dts[i]
if d in ridx:
A.append(eq[i] / eq[i - 1] - 1.0)
B.append(ridx[d])
c = compare(np.array(A), np.array(B))
print("\n serie: `StrategyPortfolio(active_sleeves()).combined_daily()`; i rendimenti "
"registrati si\n ricavano da equity.csv (il monitor salva l'equity, non i ritorni)")
show("portfolio", c)
print("\n ⚠ ULTIMA RIGA ESCLUSA DAL GIUDIZIO? No, e' inclusa, e vale la pena dirlo: la barra di")
print(" OGGI coincide per forza (il replay legge lo stesso disco che il cron ha scritto alle")
print(" 00:30 e da allora nessuno lo ha riscritto). Un audit che girasse SOLO sull'ultima")
print(" barra concluderebbe 'tutto a posto'. La potenza sta nelle barre di IERI e prima.")
RES["paper_portfolio"] = dict(parziale="SI", cmp=c, gate="nessuno (dashboard/book di ricerca)",
src="5 sleeve, il piu' recente su 1h->1d e HL 1d", cad_h=24.0)
def b_combo():
print("\n" + "=" * 102)
print(" B6 — paper_combo (calendario di BORSA: TP01 Deribit + GTAA IB)")
print("=" * 102)
pc = live("paper_combo")
dts, eq = read_equity_csv(ROOT / "data" / "paper_combo" / "equity.csv", col=1)
naked, _ = pc.both_daily()
ridx = {pd.Timestamp(k): float(v) for k, v in naked.items()}
A, B = [], []
for i in range(1, len(dts)):
d = dts[i]
if d in ridx:
A.append(eq[i] / eq[i - 1] - 1.0)
B.append(ridx[d])
c = compare(np.array(A), np.array(B))
print("\n serie: `combo_daily()` = TP01 compoundato sulla griglia dei GIORNI DI BORSA + GTAA")
show("combo", c)
tp_last = pd.Timestamp(list(ridx.keys())[-1])
print(f"\n PERCHE' QUESTO E' DIVERSO. La griglia e' `eq.index` (i giorni di borsa di IB),")
print(f" non il calendario crypto: l'ultima riga della serie e' {tp_last.date()}, mentre il")
print(f" feed crypto su disco arriva a oggi. Alle 00:35 UTC l'ultima barra IB e' quella di")
print(" IERI ed e' CHIUSA -> il monitor si ferma li', e il TP01 compoundato fra due giorni di")
print(" borsa consecutivi usa due chiusure crypto COMPLETE (il weekend finisce dentro la")
print(" barra del lunedi', che e' la ragione per cui il rapporto qui sopra non e' 2/7).")
print(" ⚠ E' un caso sano PER CASO, non per progetto: nessuna riga di codice chiede la barra")
print(" chiusa. Se domani GTAA01 venisse allineato al calendario crypto (o se si aggiungesse")
print(" una gamba 24/7 alla griglia), il monitor diventerebbe rotto come gli altri cinque")
print(" senza che nulla lo segnali.")
RES["paper_combo"] = dict(parziale="NO", cmp=c, gate="nessuno (paper cross-venue)",
src="TP01 1h->1d su griglia IB", cad_h=24.0)
# ============================================================================================
# C — TABELLA DI SINTESI
# ============================================================================================
GATE_INFO = {
"paper_xsr": ("XSR01 2026-10-23", "Sharpe>=1.0 E haircut $5.000<=40%"),
"paper_statarb": ("STATARB 2026-09-27", "Sharpe di net_modeled >= 0.35"),
"paper_dvolspread": ("DVOLSPREAD 24/10 kill", "Sharpe<-0.50 -> ritiro; 27/01 decisione piena"),
"paper_prevday": ("-", "lead senza data"),
"paper_portfolio": ("-", "dashboard"),
"paper_combo": ("-", "paper cross-venue"),
}
def sezione_C():
print("\n" + "=" * 102)
print(" C — TABELLA: una riga per monitor")
print("=" * 102)
print(f"\n {'monitor':<19}{'parz.':>6}{'ident./tot':>13}{'corr':>9}{'min/g':>9}"
f"{'ricostr.':>10} gate dipendente")
for k, v in RES.items():
c = v["cmp"]
ratio = "{}/{}".format(c["ident"], c["n"])
print(f" {k:<19}{v['parziale']:>6}{ratio:>13}{c['corr']:>+9.3f}"
f"{c['minuti']:>9,.0f}{'SI':>10} {GATE_INFO[k][0]}")
print("\n 'ricostr.' = la serie CORRETTA sulla stessa finestra forward si puo' ricalcolare oggi")
print(" dai feed su disco: e' letteralmente la colonna 'replay' di questo script (sezione E).")
# ============================================================================================
# D — LA GUARDIA CHE MANCA A monitor_health
# ============================================================================================
def guardia_barra_chiusa(last_bar_ms: float, cadenza_h: float, scritta_a: float,
grazia_min: float = 5.0) -> bool:
"""VERO se la barra consumata per ultima era GIA' CHIUSA quando e' stata registrata.
`last_bar_ms` timestamp (open-labeled) dell'ultima barra registrata, in ms epoch;
`cadenza_h` passo nominale della serie (24 per i giornalieri, 1 per prevday);
`scritta_a` quando quella riga e' stata scritta (mtime del file di serie), in secondi epoch.
E' O(1), non ricalcola nessuna strategia, e usa solo cio' che il monitor gia' scrive.
La `grazia` esiste per lo skew d'orologio, non per ammorbidire il giudizio: le violazioni
misurate valgono ore, non minuti."""
chiusura = last_bar_ms / 1000.0 + cadenza_h * 3600.0
return chiusura <= scritta_a + grazia_min * 60.0
def _ultima_barra(dirname: str, series: str) -> tuple[float, float]:
p = ROOT / "data" / dirname / series
mt = p.stat().st_mtime
if series.endswith(".jsonl"):
rows = read_jsonl(p)
return float(rows[-1]["ts"]), mt
dts, _ = read_equity_csv(p)
return float(pd.Timestamp(dts[-1]).timestamp() * 1000), mt
def sezione_D():
print("\n" + "=" * 102)
print(" D — LA GUARDIA CHE MANCA (proposta, NON implementata: tocca il cron di produzione)")
print("=" * 102)
print("""
PERCHE' `monitor_health` NON LO VEDE. Misura due guasti — coda (eta' dell'ultima barra) e buchi
interni (copertura fra prima e ultima barra) — e questo difetto non e' ne' l'uno ne' l'altro:
la serie e' FRESCA (scritta stanotte) e COMPLETA (una barra per giorno, zero buchi). Entrambe
le misure guardano i TIMESTAMP e nessuna guarda cosa c'e' dentro la barra. Aggiungere una terza
misura della stessa famiglia non aiuterebbe: serve una misura che confronti la barra letta con
il momento in cui e' stata letta.
DUE CANDIDATI, e non sono equivalenti.""")
# ---- candidato 1: vol registrata vs vol ricalcolata --------------------------------------
print("\n D1. CANDIDATO 'RAPPORTO DI VOLATILITA'' (vol registrata / vol ricalcolata).")
print(f" {'monitor':<19}{'vol reg.':>10}{'vol ric.':>10}{'rapporto':>10} verdetto a "
"soglia [0.70, 1.40]")
lo, hi = 0.70, 1.40
miss = []
for k, v in RES.items():
c = v["cmp"]
rr = c["sd_rec"] / c["sd_rep"] if c["sd_rep"] else float("nan")
seg = "SEGNALA" if not (lo <= rr <= hi) else "tace"
if v["parziale"] == "SI" and seg == "tace":
miss.append(k)
print(f" {k:<19}{c['sd_rec']:>9.2f}%{c['sd_rep']:>9.2f}%{rr:>10.3f} {seg}")
print(f" -> MANCA {len(miss)} monitor rotti su {sum(1 for v in RES.values() if v['parziale'] == 'SI')}"
f": {', '.join(miss) if miss else '-'}")
print(" Motivo strutturale, non taratura: su una griglia oraria il difetto tocca 1 barra")
print(" su 24 e sposta la vol dell'1-2%, cioe' DENTRO il rumore di stima di una vol su")
print(" poche settimane. Abbassare la soglia per prenderlo genererebbe falsi allarmi ogni")
print(" volta che la vol di mercato cambia regime. E costa: richiede di RIESEGUIRE ogni")
print(" strategia a ogni giro (qui ~6 s per il solo book a 5 sleeve).")
print(" Resta utile per un guasto DIVERSO — il drift fra monitor e strategia — che la D2")
print(" non vede. Va aggiunta DOPO, non al posto della D2.")
# ---- candidato 2: barra chiusa ------------------------------------------------------------
print("\n D2. CANDIDATO 'BARRA CHIUSA' — RACCOMANDATO.")
print(" REGOLA: ts_ultima_barra + cadenza <= quando quella riga e' stata SCRITTA")
print(" (il 'quando' e' l'mtime del file di serie: `_append` scrive solo se c'e' una")
print(" barra nuova, quindi l'mtime E' l'ora dell'ultima registrazione).")
print(" Soglia: grazia 5 minuti per lo skew d'orologio. La soglia esatta e' irrilevante —")
print(" si vede sotto che le violazioni valgono 23,4 h e 0,4 h, non minuti.")
specs = [("paper_xsr", "returns.jsonl", 24.0), ("paper_statarb", "returns.jsonl", 24.0),
("paper_dvolspread", "returns.jsonl", 24.0), ("paper_prevday", "returns.jsonl", 1.0),
("paper_portfolio", "equity.csv", 24.0), ("paper_combo", "equity.csv", 24.0)]
print(f"\n {'monitor':<19}{'ultima barra':>22}{'scritta alle':>22}"
f"{'mancavano':>12} verdetto")
esiti = {}
for name, ser, cad in specs:
lb, mt = _ultima_barra(name, ser)
ok = guardia_barra_chiusa(lb, cad, mt)
manca = (lb / 1000.0 + cad * 3600.0 - mt) / 3600.0
esiti[name] = ok
print(f" {name:<19}"
f"{str(pd.Timestamp(lb, unit='ms', tz='UTC'))[:19]:>22}"
f"{str(pd.Timestamp(mt, unit='s', tz='UTC'))[:19]:>22}"
f"{max(manca, 0):>11.1f}h {'OK' if ok else '*** LEGGE-IN-CORSO ***'}")
print("\n -> segnala 5 monitor su 6 e TACE sull'unico sano. Non e' un rilevatore tarato")
print(" per segnalare: `paper_combo` e' un controllo positivo REALE, con dati veri,")
print(" che la guardia distingue dagli altri cinque senza nessuna eccezione cablata.")
# ---- controllo positivo sintetico, nei due versi ------------------------------------------
print("\n D3. CONTROLLO POSITIVO SINTETICO (obbligatorio: una guardia che segnala tutto o")
print(" niente e' inutile quanto una rotta). Casi costruiti, nei DUE versi:")
base = pd.Timestamp("2026-08-21 00:00:00", tz="UTC")
casi = [
("sano: barra 1d di ieri, scritta stanotte alle 00:30",
base, 24.0, pd.Timestamp("2026-08-22 00:30", tz="UTC"), True),
("ROTTO: barra 1d di OGGI, scritta oggi alle 00:30 (= i cinque monitor)",
pd.Timestamp("2026-08-22 00:00", tz="UTC"), 24.0,
pd.Timestamp("2026-08-22 00:30", tz="UTC"), False),
("sano: barra 1h delle 23:00, scritta a mezzanotte e mezza",
pd.Timestamp("2026-08-21 23:00", tz="UTC"), 1.0,
pd.Timestamp("2026-08-22 00:30", tz="UTC"), True),
("ROTTO: barra 1h delle 00:00, scritta alle 00:30 (= paper_prevday)",
pd.Timestamp("2026-08-22 00:00", tz="UTC"), 1.0,
pd.Timestamp("2026-08-22 00:30", tz="UTC"), False),
("sano: monitor in ritardo di 3 giorni (fermo, ma non legge una barra aperta)",
pd.Timestamp("2026-08-18 00:00", tz="UTC"), 24.0,
pd.Timestamp("2026-08-22 00:30", tz="UTC"), True),
("limite: barra chiusa 4 minuti DOPO la scrittura -> assorbita dalla grazia",
pd.Timestamp("2026-08-21 00:04", tz="UTC"), 24.0,
pd.Timestamp("2026-08-22 00:00", tz="UTC"), True),
]
tutti = True
for desc, bar, cad, w, atteso in casi:
got = guardia_barra_chiusa(bar.timestamp() * 1000, cad, w.timestamp())
tutti &= (got == atteso)
print(f" [{'ok ' if got == atteso else 'FAIL'}] atteso {'OK ' if atteso else 'ALLARME'}"
f" -> ottenuto {'OK ' if got else 'ALLARME'} {desc}")
print(f" -> controllo positivo {'SUPERATO' if tutti else '*** FALLITO ***'} "
"(la guardia distingue 'fermo' da 'legge una barra aperta': sono guasti diversi")
print(" e vanno riparati in modi diversi — uno e' il cron, l'altro e' `advance()`).")
print("\n D4. COSA NON COPRE, dichiarato. La guardia vede la barra CONSUMATA, non la barra")
print(" USATA PER IL SEGNALE: un monitor che si fermasse alla barra chiusa ma calcolasse")
print(" il target su un pannello che include quella in corso passerebbe. Per quello serve")
print(" la D1 (o un test di identita' contro il backtest), ed e' il motivo per cui la")
print(" raccomandazione e' 'prima la D2, poi la D1', non 'una delle due'.")
# ============================================================================================
# E — COSTO DELLA RIPARAZIONE
# ============================================================================================
def sezione_E():
print("\n" + "=" * 102)
print(" E — LA DOMANDA OPERATIVA: cosa costa riparare `advance()`")
print("=" * 102)
oggi = pd.Timestamp(NOW).normalize().tz_localize(None)
righe = [
("paper_statarb", "STATARB", pd.Timestamp("2026-09-27"), "returns.jsonl"),
("paper_xsr", "XSR01", pd.Timestamp("2026-10-23"), "returns.jsonl"),
("paper_dvolspread", "DVOLSPREAD (kill)", pd.Timestamp("2026-10-24"), "returns.jsonl"),
("paper_dvolspread", "DVOLSPREAD (decisione)", pd.Timestamp("2027-01-24"), "returns.jsonl"),
]
print(f"\n {'monitor / gate':<34}{'barre oggi':>12}{'data gate':>13}{'se AZZERI':>12}"
f"{'ritardo':>10}")
for mon, nome, gate, ser in righe:
n = len(read_jsonl(ROOT / "data" / mon / ser))
# con una barra al giorno, tornare a n barre richiede n giorni dal reset
rip = oggi + pd.Timedelta(days=n)
rit = (rip - gate).days
print(f" {f'{mon} / {nome}':<34}{n:>12}{str(gate.date()):>13}"
f"{str(rip.date()):>12}{rit:>+9}g")
print("\n Cosi' letta, la riparazione sposta i tre gate di 1-2 mesi. MA la colonna giusta e'")
print(" un'altra, e cambia la decisione:")
print("""
✅ LA FINESTRA FORWARD **NON VA PERSA**: e' RICOSTRUIBILE, per tutti e sei i monitor.
Le ore mancanti non sono mai state registrate, ma non e' li' che vivono: vivono nei feed
ARCHIVIATI su disco, che contengono le barre CHIUSE di ogni giorno della finestra. Il
"replay" della sezione B *e'* la serie corretta sulla stessa identica finestra
[inception -> oggi], calcolata con la config congelata e lo stesso codice di produzione.
Ricostruire non e' rifare un backtest su una finestra scelta dopo: inception, parametri e
universo sono congelati e datati PRIMA — cambia solo che ogni barra dura 24 ore invece di 35
minuti.
LE DUE CONDIZIONI, ed e' giusto chiedere che siano verificate e non assunte:
(1) il feed non riscrive le barre gia' chiuse. **MISURATO qui**: `paper_prevday` ha
1426 barre su 1488 identiche BIT A BIT al replay dopo 62 giorni di riscritture
notturne del parquet BTC/ETH (sezione B4). Per Hyperliquid la stessa cosa e' misurata
in `r0822_xsr_repro` T2 (Sharpe 2024 e 2025 riprodotti al centesimo su 731 barre).
(2) la config e' congelata e verificabile: `paper_dvolspread` lo controlla da solo
(`frozen` nello stato, esce 1 se diverge); per gli altri il congelamento e' nel
sorgente ed e' in git.
⚠ COSA **NON** E' RICOSTRUIBILE, e va detto perche' e' l'eccezione che rende la regola utile:
una serie che dipende da uno stato non archiviato. Nel progetto ce n'e' una — la catena
opzioni (`data/raw/cb_chain`, un'ora persa e' persa per sempre: Deribit non serve book
storici) — e infatti `monitor_health` le da' una soglia d'eta' di 3 ore invece di 48. I sei
monitor di questo audit NON sono in quella categoria: leggono tutti feed che il cron
ri-scarica per intero ogni notte.
QUINDI LA DECISIONE DELL'OPERATORE NON E' "riparare e perdere la finestra" ma:
(a) riparare `advance()` perche' si fermi all'ultima barra CHIUSA;
(b) RIGENERARE `returns.jsonl` / `equity.csv` dall'inception con lo stesso codice — cio' che
questo script ha gia' calcolato in memoria — invece di `--reset`;
(c) dichiarare nel diario che le serie sono state rigenerate e da quale commit, perche' una
serie forward rigenerata e' credibile solo se la data di inception e la config sono
verificabili in git (lo sono).
Il costo vero e' (c), non i giorni: nessuna delle tre date di gate si sposta.
⚠ UNA COSA CHE LA RIGENERAZIONE CAMBIA DAVVERO, e va scritta nel diario: i libri REAL saltano i
ribilanci sotto il min-order, e quella decisione dipende dal capitale corrente, quindi il
percorso REAL rigenerato non e' il percorso REAL registrato — non e' "lo stesso numero
calcolato meglio", e' un secondo percorso deterministico. Per XSR01 conta: il gate del 23/10
legge net_REAL-$5000 per l'haircut.""")
def main() -> None:
print("\n" + "#" * 102)
print("# r0822_monitor_audit — SOLA LETTURA su scripts/live, src/live, data, config, cron")
print(f"# ora UTC {NOW:%Y-%m-%d %H:%M:%S} (il cron_daily di oggi ha girato alle 00:30)")
print("#" * 102 + "\n")
sezione_A()
b_xsr()
b_statarb()
b_dvolspread()
b_prevday()
b_portfolio()
b_combo()
sezione_C()
sezione_D()
sezione_E()
print("\n" + "=" * 102)
print(" FINE — nessun file di produzione e' stato modificato da questo script.")
print("=" * 102 + "\n")
if __name__ == "__main__":
main()
+35 -5
View File
@@ -511,7 +511,7 @@ def main() -> None:
print("\n per anno (ISO dSh, mediana appaiata sulle ancore):") print("\n per anno (ISO dSh, mediana appaiata sulle ancore):")
yrs = None yrs = None
for nm in [lead, "BIN L>=1 (0/1)", "DROP attivita' >p75", "BIN L<1 (INVERSO)"]: for nm in [lead, "BIN L>=med (0/1)", "canale FREQUENZA cont.", "BIN L<1 (INVERSO)"]:
yrs, cells = per_year(BK2, CT2, nm, offs) yrs, cells = per_year(BK2, CT2, nm, offs)
if nm == lead: if nm == lead:
print(" " + f"{'':<26}" + "".join(f"{y:>8}" for y in yrs)) print(" " + f"{'':<26}" + "".join(f"{y:>8}" for y in yrs))
@@ -524,8 +524,9 @@ def main() -> None:
print(" 4. L'ASSE DELLA FINESTRA — picco o plateau? + scelta di w IN-SAMPLE-ONLY") print(" 4. L'ASSE DELLA FINESTRA — picco o plateau? + scelta di w IN-SAMPLE-ONLY")
print("-" * L) print("-" * L)
wnames = [(w, f"VTL tv20 w{w}" if w != 90 else lead) for w in WGRID] wnames = [(w, f"VTL tv20 w{w}" if w != 90 else lead) for w in WGRID]
bd0 = EXV2["BTC"]["base_daily"]
print(f" {'w':>6}{'ISO dShFULL':>14}{'pos/n':>8}{'ISO dShIS':>12}{'ISO dShHOLD':>13}" print(f" {'w':>6}{'ISO dShFULL':>14}{'pos/n':>8}{'ISO dShIS':>12}{'ISO dShHOLD':>13}"
f"{'pos/n':>8}") f"{'pos/n':>8}{'barre att.':>12}")
wf, wi, wh = {}, {}, {} wf, wi, wh = {}, {}, {}
for w, nm in wnames: for w, nm in wnames:
dF = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o, n=nm: CT2[n][o], offs, dF = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o, n=nm: CT2[n][o], offs,
@@ -535,8 +536,9 @@ def main() -> None:
dH = A.anchor_luck_delta(lambda o, n=nm: hold(BK2[n][o]), lambda o, n=nm: hold(CT2[n][o]), dH = A.anchor_luck_delta(lambda o, n=nm: hold(BK2[n][o]), lambda o, n=nm: hold(CT2[n][o]),
offs, metric=sh) offs, metric=sh)
wf[w], wi[w], wh[w] = dF["median_paired"], dI["median_paired"], dH["median_paired"] wf[w], wi[w], wh[w] = dF["median_paired"], dI["median_paired"], dH["median_paired"]
nact = float(np.median((bd0 != 0.0).astype(float).rolling(w).sum().dropna().values))
print(f" {w:>6}{wf[w]:>+14.3f}{dF['n_positive']:>5}/{len(offs):<3}{wi[w]:>+12.3f}" print(f" {w:>6}{wf[w]:>+14.3f}{dF['n_positive']:>5}/{len(offs):<3}{wi[w]:>+12.3f}"
f"{wh[w]:>+13.3f}{dH['n_positive']:>5}/{len(offs):<3}") f"{wh[w]:>+13.3f}{dH['n_positive']:>5}/{len(offs):<3}{nact:>12.0f}")
wbest_full = max(WGRID, key=lambda w: wf[w]) wbest_full = max(WGRID, key=lambda w: wf[w])
wbest_is = max(WGRID, key=lambda w: wi[w]) wbest_is = max(WGRID, key=lambda w: wi[w])
print(f"\n argmax su FULL (cio' che l'agente ha pubblicato): w = {wbest_full} " print(f"\n argmax su FULL (cio' che l'agente ha pubblicato): w = {wbest_full} "
@@ -561,8 +563,14 @@ def main() -> None:
f"{wf[wbest_full]:+.3f} / {wh[wbest_full]:+.3f}") f"{wf[wbest_full]:+.3f} / {wh[wbest_full]:+.3f}")
rho = float(np.corrcoef([wi[w] for w in WGRID], [wh[w] for w in WGRID])[0, 1]) rho = float(np.corrcoef([wi[w] for w in WGRID], [wh[w] for w in WGRID])[0, 1])
print(f" corr(ISO dSh in-sample, ISO dSh hold-out) fra le {len(WGRID)} finestre: {rho:+.2f}") print(f" corr(ISO dSh in-sample, ISO dSh hold-out) fra le {len(WGRID)} finestre: {rho:+.2f}")
print(" -> se e' ~0 o negativa, la scelta di w in-sample NON predice l'hold-out: l'asse") print(" LETTURA (attacco 4 FALLITO, e il motivo e' informativo): la scelta di w")
print(" e' rumore e il picco a w=90 e' un max-of-k.") print(" in-sample-only cade sulla cella pubblicata, e la corr IS/HOLD e' POSITIVA -> l'asse")
print(" non e' rumore. E non e' nemmeno un PICCO: e' una SOGLIA. Sotto w=45 la finestra")
print(" contiene troppo poche barre ATTIVE per misurare qualsiasi cosa (colonna a destra) e")
print(" il segno si rovescia; da 45 a 270 e' un plateau, 5 celle su 5 positive con 19-23")
print(" ancore su 23. La descrizione 'picco, non plateau' del bersaglio e' PIU' SEVERA del")
print(" vero — ed e' proprio cio' che si aspetta da un FILTRO DI FREQUENZA: serve una")
print(" finestra abbastanza lunga da contare i trade.")
# ------------------------------------------------------------------ §5 sintesi # ------------------------------------------------------------------ §5 sintesi
print("\n" + "-" * L) print("\n" + "-" * L)
@@ -606,6 +614,28 @@ def main() -> None:
f"{RES['BIN L>=med (0/1)']['dH']:+.3f} HOLD ({RES['BIN L>=med (0/1)']['nF']}/{len(offs)} ancore)") f"{RES['BIN L>=med (0/1)']['dH']:+.3f} HOLD ({RES['BIN L>=med (0/1)']['nF']}/{len(offs)} ancore)")
print(f" controllo di segno (INVERSO): {RES['BIN L<1 (INVERSO)']['dF']:+.3f} FULL " print(f" controllo di segno (INVERSO): {RES['BIN L<1 (INVERSO)']['dF']:+.3f} FULL "
f"({RES['BIN L<1 (INVERSO)']['nF']}/{len(offs)}) -> il filtro e' reale, non un artefatto") f"({RES['BIN L<1 (INVERSO)']['nF']}/{len(offs)}) -> il filtro e' reale, non un artefatto")
print("\n" + "=" * L)
print(" VERDETTO DELLO SCETTICO")
print("=" * L)
print(" A1 test PRE-REGISTRATO (IV su V1): previsione |V1| < |V2-DD| VERIFICATA IN "
f"{ok}/{tot} CELLE.")
print(" L'effetto e' PIU' GRANDE dove lo stop e' gia' in ATR (V1 mediana "
f"{np.median(np.abs(e1s)):.3f} vs V2-DD {np.median(np.abs(e2s)):.3f}), e il test NON e'")
print(" senza potenza (su V2-DD la famiglia arriva a +0.067 con 23/23). -> la")
print(" SPIEGAZIONE 'lo stop a percentuale fissa lascia variare il rischio' e' REFUTATA.")
print(" A2 contabilita': la premessa del difetto e' FALSA (P&L al giorno d'INGRESSO, "
"99.3%);")
print(" la 'riparazione' e' un no-op bit-exact; il '+0,04 fantasma' e' [L combinata] -")
print(" [L per asset]. Nessun look-ahead c'era, e la variante scartata era la MIGLIORE.")
print(" A3 un filtro BINARIO 0/1 sulla mediana della stessa L riproduce il "
f"{RES['BIN L>=med (0/1)']['dF']/base_eff*100:.0f}% del")
print(" vantaggio senza alcun vol-target -> la FORMULA DI SIZING E' ORNAMENTO. Il canale")
print(" FREQUENZA da solo fa +0.051, la MAGNITUDINE +0.003 (replica esatta del bersaglio).")
print(" A4 l'asse w NON e' un picco ne' un max-of-k: e' una soglia sul numero di barre")
print(" attive nella finestra, con plateau 45-270. Attacco fallito.")
print(" --> il NUMERO regge (+0.070 riprodotto esattamente), la SPIEGAZIONE cade due volte,")
print(" e il gate pre-registrato per il 22/12/2026 e' GIA' STATO FALLITO oggi.")
print(f"\n fatto in {time.time()-t0:.0f}s") print(f"\n fatto in {time.time()-t0:.0f}s")
print("=" * L) print("=" * L)