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** |
| — | **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** |
| 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
@@ -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'
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.
### 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).
+317 -223
View File
@@ -3,53 +3,57 @@
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
(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
BTC_USDC. Il progetto non l'ha mai misurata.
molto piu' piccoli, e contando l'open interest sembra che su SOL/XRP/HYPE/AVAX sia molto piu'
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
geometria di VRP01: vendi ~-0.28, compri ~-0.10) eseguibile con qualche centinaio di dollari,
con spread e liquidita' che non se la mangino?
geometria di VRP01: vendi ~-0.28, compri ~-0.10) eseguibile con qualche centinaio di dollari, con
spread e liquidita' che non se la mangino?
COSA MISURA, in ordine, e si FERMA appena un vincolo la uccide:
1. i fatti del venue letti dall'API pubblica (min_trade_amount, tick_size, contract_size,
costo in $ di UN lotto), e la LIQUIDITA' vera: spread relativo (ask-bid)/mid ai delta che
servono davvero, non sull'ATM, + profondita' in cima al book;
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.
IPOTESI DICHIARATA PRIMA DI MISURARE: che il vincolo binding fosse la TAGLIA DEL LOTTO (e' cosi'
che il progetto ha sempre bocciato le opzioni: "BTC min 0.1 = $6.210/lotto -> FUORI a $3.000") e
che le famiglie alt, con lotti da poche decine di dollari, la superassero. Sbagliata due volte:
il lotto non e' il vincolo, e la classifica di liquidita' su cui poggia la domanda si ROVESCIA.
COSA NON PUO' FARE, e va detto prima dei numeri: **non esiste un backtest possibile**. Il nostro
collettore raccoglie solo le inverse BTC/ETH e l'archivio ereditato pure. Questo file misura le
condizioni di UN ISTANTE. Il massimo verdetto ottenibile e' `LEAD` = "vale la pena raccogliere il
dato", con il costo di raccolta stimato in chiamate/ora sul giro esistente.
COSA MISURA, in ordine, fermandosi appena un vincolo la uccide:
0. l'UNITA' dei contratti — due test empirici, perche' leggerla male sposta ogni cifra in
dollari di un fattore 10-1000 (errore commesso nella prima stesura di questo file);
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:
(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
STRESS. Un istante di calma non e' un campione.
(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
dichiara che un meccanismo diverso la riapre. Un put credit spread non e' direzionale nel
senso di quella decisione. Ma il guardrail sul DATO vale lo stesso: nessun file nuovo in
`data/raw/`, e i prezzi del sottostante qui vengono da `underlying_price` della catena,
mai da `load_data`.
(c) il gate IV-rank>0.30 e' l'unico alpha misurato di VRP01, e su questi sottostanti NON e'
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 in
quel senso. Ma il guardrail sul DATO vale identico: nessun file nuovo in `data/raw/`, e i
prezzi del sottostante qui vengono da `underlying_price` della catena, 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'
calcolabile: non abbiamo un giorno di storia di vol implicita. Senza quel gate la struttura
non e' VRP01, e' vendere vol a caso.
CONVENZIONE DI UNITA' (la trappola di questo file): le opzioni INVERSE si quotano in CRIPTO per
contratto, le USDC-LINEARI in USDC per unita' di sottostante. Confrontare i due premi senza
convertire darebbe numeri sbagliati di un fattore ~10^4-10^5. Qui ogni importo in dollari passa
per `prem_usd()`, e le colonne unit-free (spread relativo, costo/credito) sono le uniche
direttamente confrontabili fra le due famiglie.
CONVENZIONE DI UNITA' (la trappola di questo file, verificata al §0): le opzioni INVERSE si
quotano in CRIPTO per contratto, le USDC-LINEARI in USDC per UNITA' DI SOTTOSTANTE; e
`min_trade_amount` e' in unita' di sottostante, NON in contratti — quindi non va moltiplicato per
`contract_size` (farlo gonfia XRP_USDC da $1.469 a $1.469.000 per lotto).
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'
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 --cache # riusa
nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py --cache
"""
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")
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
OI_MIN = 100.0 # la stessa soglia di collect_chain.py
DTE_LO, DTE_HI = 4.0, 45.0 # dal settimanale di VRP01 (4-10) a ~1.5 mesi
OI_MIN = 100.0 # la soglia che usa collect_chain.py, qui sotto esame
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
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
FAMS = LINEAR + INVERSE
# ---------------------------------------------------------------- rete (parca, sola lettura)
@@ -132,11 +138,6 @@ def fetch_all(b: Budget) -> dict:
{"currency": cur, "kind": "option", "expired": "false"}, b) or []
out[f"summ_{cur}"] = get("get_book_summary_by_currency",
{"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
@@ -153,37 +154,40 @@ def put_delta(S: float, K: float, T: float, sig: float) -> float:
# ---------------------------------------------------------------- unita'
def is_inverse(fam: str) -> bool:
def is_inv(fam: str) -> bool:
return fam in INVERSE
def prem_usd(price: float, r: dict) -> float:
"""Premio in DOLLARI di `min_trade_amount` contratti quotati a `price`.
def usd(price: float | None, r: dict) -> float:
"""Prezzo di UN LOTTO MINIMO, in dollari.
Lineare: il prezzo e' gia' in USDC per unita' di sottostante.
Inverse: il prezzo e' in CRIPTO per contratto -> si moltiplica per il sottostante.
Lineare: il prezzo e' gia' in USDC per unita' di sottostante -> x min_trade_amount.
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"]
return price * k * (r["S"] if is_inverse(r["fam"]) else 1.0)
if price is None:
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:
"""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
peggiore e' a S=Kl e vale (Ks-Kl)/Kl cripto = (Ks-Kl) dollari a quel prezzo -> stessa
formula, ma APPROSSIMATA (il valore in dollari della perdita dipende da dove finisce S).
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 formula,
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)
def im_short_put_usd(S: float, K: float, mark_usd: float, qty: float) -> float:
"""Initial margin di una short put, in USDC, secondo la formula PUBBLICATA da Deribit.
def im_short_put(S: float, K: float, mark_lot: float, r: dict) -> float:
"""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
da qui: si conferma con `private/get_margins`, che richiede una chiave (lettura, ma non
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):
return float("nan")
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 per_contratto * qty
return max(max(0.15 - otm, 0.10), 0.075) * S * r["minamt"] + max(0.0, mark_lot)
# ---------------------------------------------------------------- costruzione della vista
# ---------------------------------------------------------------- vista
def build(raw: dict) -> list[dict]:
@@ -205,8 +208,7 @@ def build(raw: dict) -> list[dict]:
inst[i["instrument_name"]] = i
for s in raw.get(f"summ_{cur}") or []:
summ[s["instrument_name"]] = s
rows = []
now_ms = datetime.now(UTC).timestamp() * 1000.0
rows, now_ms = [], datetime.now(UTC).timestamp() * 1000.0
for name, i in inst.items():
s = summ.get(name)
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"
ask = float(ask) if ask else None
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"
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(
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),
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),
tick=float(i.get("tick_size") or 0.0), steps=i.get("tick_size_steps") or [],
minamt=float(i.get("min_trade_amount") or 0.0),
vol_usd=float(s.get("volume_usd") 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),
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
@@ -242,217 +244,310 @@ def rel_spread(r: dict) -> float:
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:
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(f"{'famiglia':<10} {'strum':>6} {'OI>=100':>8} {'min_amt':>9} {'c_size':>7} "
f"{'tick':>9} {'sottost.$':>11} {'$/lotto':>10} {'taker':>8} {'settle':>7}")
print(f"{'famiglia':<10} {'strum':>6} {'min_amt':>9} {'c_size':>7} {'tick':>9} "
f"{'sottost.$':>11} {'$/lotto':>9} {'taker':>7} {'settle':>7}")
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]
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]))
mins = {r["minamt"] for r in sub}
cs = {r["csize"] for r in sub}
a = sub[0]
lot = a["minamt"] * a["csize"] * S
tab[f] = dict(n=len(sub), nliq=len(liq), minamt=a["minamt"], csize=a["csize"],
tick=a["tick"], S=S, lot=lot, taker=a["taker"])
if f in LINEAR or f in INVERSE:
avv = "" if (len(mins) == 1 and len(cs) == 1) else " <- min/c_size NON uniformi"
print(f"{f:<10} {len(sub):>6} {len(liq):>8} {a['minamt']:>9.4g} {a['csize']:>7.4g} "
f"{a['tick']:>9.5g} {S:>11.2f} {lot:>10.2f} {str(a['taker']):>8} "
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.")
tab[f] = dict(n=len(sub), minamt=a["minamt"], csize=a["csize"], tick=a["tick"], S=S,
lot=a["minamt"] * S, taker=a["taker"],
nliq=sum(1 for r in sub if r["oi"] >= OI_MIN))
print(f"{f:<10} {len(sub):>6} {a['minamt']:>9.4g} {a['csize']:>7.4g} {a['tick']:>9.5g} "
f"{S:>11.2f} {tab[f]['lot']:>9.2f} {a['taker']:>7.4g} {str(a['settle']):>7}")
print("\n `$/lotto` = min_trade_amount x sottostante = il NOZIONALE di un lotto (NON il premio,")
print(" 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(" 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
def replica(tab: dict) -> None:
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', due modi
# ---------------------------------------------------------------- 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"
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 = []
for e in sorted({round(r["dte"], 4) for r in puts}):
g = [r for r in puts if round(r["dte"], 4) == e]
s = min(g, key=lambda r: abs(r["delta_bs"] - SHORT_DELTA))
lo = min(g, key=lambda r: abs(r["delta_bs"] - LONG_DELTA))
gs = [r for r in g if r["bid"] is not None] if eseguibile else g
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"]:
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
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(f"2. LA LIQUIDITA' AI DELTA CHE SERVONO ({SHORT_DELTA} corta / {LONG_DELTA} lunga),")
print(f" NON sull'ATM. Finestra {DTE_LO:.0f}-{DTE_HI:.0f} giorni. Delta = BS su mark_iv, r=0.")
print(f"3. LA STRUTTURA — put credit spread {SHORT_DELTA}/{LONG_DELTA}, finestra "
f"{DTE_LO:.0f}-{DTE_HI:.0f} giorni, per UN LOTTO MINIMO")
print("=" * 104)
print(f"{'famiglia':<10} {'scad':>5} {'2lati':>6} {'d corta':>8} {'d lunga':>8} "
f"{'spr% corta':>11} {'spr% lunga':>11} {'credito$':>9} {'costo/cred':>11} "
print(" `f_venue` = (bid_corta - ask_ala) / (mark_corta - mark_ala) = quanta parte del")
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}")
res = {}
for fam in list(LINEAR) + list(INVERSE):
gg = gambe(rows, fam)
for f in FAMS:
gg = scegli(rows, f, eseguibile=True)
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
ss, ll, ds, dl, cred, cost, fee, ml = [], [], [], [], [], [], [], []
due = neg = 0
acc = {k: [] for k in ("ds", "dl", "ss", "sl", "cm", "ce", "fee", "ml")}
for g in gg:
s, lo = g["short"], g["long"]
rs, rl = rel_spread(s), rel_spread(lo)
if not (np.isfinite(rs) and np.isfinite(rl)):
cm = usd(s["mark"], s) - usd(lo["mark"], lo)
ce = usd(s["bid"], s) - usd(lo["ask"], lo)
if cm <= 0:
continue
due += 1
c_mid = prem_usd(s["mid"], s) - prem_usd(lo["mid"], lo)
c_exe = prem_usd(s["bid"], s) - prem_usd(lo["ask"], lo)
if c_mid <= 0:
neg += 1
acc["ds"].append(s["delta"]); acc["dl"].append(lo["delta"])
acc["ss"].append(rel_spread(s)); acc["sl"].append(rel_spread(lo))
acc["cm"].append(cm); acc["ce"].append(ce)
acc["fee"].append(2.0 * (fee_lotto(s) + fee_lotto(lo)) / cm)
acc["ml"].append(loss_usd(s["K"], lo["K"], s) - ce)
if not acc["cm"]:
print(f"{f:<10} {len(gg):>4} nessuna struttura con credito positivo al mark")
continue
# fee taker Deribit: min(0.03% del sottostante, 12.5% del premio) per gamba, x2 gambe,
# 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)")
m = {k: float(np.nanmedian(v)) if v else float("nan") for k, v in acc.items()}
m["fv"] = float(np.median([e / c for e, c in zip(acc["ce"], acc["cm"])]))
m["n"] = len(acc["cm"])
res[f] = m
print(f"{f:<10} {m['n']:>4} {m['ds']:>8.3f} {m['dl']:>7.3f} {m['ss']*100:>10.1f}% "
f"{m['sl']*100:>8.1f}% {m['cm']:>11.2f} {m['ce']:>11.2f} {m['fv']:>8.3f} "
f"{m['fee']*100:>8.1f}% {m['ml']:>9.2f}")
print("\n ⚠️ `spr%` puo' essere NaN quando la gamba ha un lato solo: e' negoziabile per")
print(" aprire (il lato che serve c'e') ma non ha un mid, quindi non ha uno spread definito.")
print(" `fee/cred` = 2 gambe x (apertura + chiusura) col cap 12.5%. VRP01 tiene FINO A")
print(" SCADENZA (nessuna gestione infra-settimana, il profit-take e' REFUTED il 30/07),")
print(" quindi il costo realistico e' ~la META' di questa colonna + la delivery fee.")
print(f"\n --- controllo: gli STESSI strike scelti fra TUTTI gli strike (non solo quotati) ---")
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
r = dict(n=len(gg), due=due, neg=neg, s=float(np.median(ss)), l=float(np.median(ll)),
ds=float(np.median(ds)), dl=float(np.median(dl)), cred=float(np.median(cred)),
cost=float(np.median(cost)), fee=float(np.median(fee)), ml=float(np.median(ml)))
res[fam] = r
print(f"{fam:<10} {r['n']:>5} {due:>6} {r['ds']:>8.3f} {r['dl']:>8.3f} "
f"{r['s']*100:>10.1f}% {r['l']*100:>10.1f}% {r['cred']:>9.2f} "
f"{r['cost']*100:>10.1f}% {r['fee']*100:>8.1f}% {r['ml']:>9.2f}")
print("\n `2lati` = strutture in cui ENTRAMBE le gambe hanno bid E ask. Una riga presente non")
print(" e' un dato presente: una gamba con un solo lato non e' negoziabile a un prezzo noto.")
print(" `d corta`/`d lunga` = delta REALIZZATI (non i target): se la griglia degli strike e'")
print(" grossolana la struttura che compreresti non e' quella che hai chiesto.")
print(" `costo/cred` = quanto dell'incasso a mid resta sul tavolo attraversando lo spread su")
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(" `credito$` e `maxloss$` sono per UN LOTTO MINIMO.")
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
def profondita(rows: list[dict], fams: list[str], b: Budget) -> None:
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(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}")
for fam in fams:
gg = gambe(rows, fam)[:4] # max 4 scadenze/famiglia: budget di rete
for lato, key in (("corta", "short"), ("lunga", "long")):
lots, prem, dd, tk = [], [], [], []
f"{'prezzo/tick':>12} {'max|d venue-BS|':>17}")
for f in fams:
gg = scegli(rows, f, eseguibile=True)[:4] # max 4 scadenze: budget di rete
for lato, key, side in (("corta", "short", "bids"), ("ala", "long", "asks")):
lots, prem, tk, dd = [], [], [], []
for g in gg:
r = g[key]
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
side = ob.get("bids") if key == "short" else ob.get("asks")
if not side:
continue
px, sz = float(side[0][0]), float(side[0][1])
px, sz = float(ob[side][0][0]), float(ob[side][0][1])
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"]:
tk.append(px / r["tick"])
gv = (ob.get("greeks") or {}).get("delta")
if gv is not None and np.isfinite(r["delta_bs"]):
dd.append(abs(float(gv) - r["delta_bs"]))
if gv is not None and np.isfinite(r["delta"]):
dd.append(abs(float(gv) - r["delta"]))
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
print(f"{fam:<10} {lato:<6} {len(lots):>3} {np.median(lots):>14.1f} "
f"{np.median(prem):>17.2f} "
f"{(np.median(tk) if tk else float('nan')):>15.1f} "
f"{(max(dd) if dd else float('nan')):>21.4f}")
print(f"{f:<10} {lato:<6} {len(lots):>3} {np.median(lots):>14.1f} "
f"{np.median(prem):>17.2f} {(np.median(tk) if tk else float('nan')):>12.1f} "
f"{(max(dd) if dd else float('nan')):>17.4f}")
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(" d'artefatto del 30/07: un'ala a 1-2 tick e' un prezzo di griglia, non un prezzo.")
print(" `max|delta venue-BS|` valida il MIO calcolo del delta contro le greche del venue: se")
print(" non e' piccolo, tutta la selezione degli strike qui sopra e' sbagliata.")
print(" entrano senza muovere il prezzo. `prezzo/tick` e' il controllo d'artefatto del")
print(" 30/07: un prezzo a 1-2 tick e' una griglia, non un prezzo.")
print(" `max|d venue-BS|` valida il MIO delta contro le greche del venue: se non e' piccolo,")
print(" tutta la selezione degli strike qui sopra e' sbagliata.")
# ---------------------------------------------------------------- 3. collaterale
# ---------------------------------------------------------------- §4 collaterale e capacita'
def collaterale(rows: list[dict], res: dict) -> None:
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(f"{'famiglia':<10} {'IM corta+ala $':>15} {'max-loss $':>11} {'IM/conto':>9} "
f"{'strutture @635 (IM)':>20} {'(max-loss)':>12}")
for fam in res:
gg = gambe(rows, fam)
ims, mls = [], []
print(f"{'famiglia':<10} {'IM corta+ala$':>14} {'max-loss$':>10} {'n @IM':>7} "
f"{'n @maxloss':>11} {'n a budget 20%':>15} {'credito lordo$':>15}")
for f, m in res.items():
gg = scegli(rows, f, eseguibile=True)
ims = []
for g in gg:
s, lo = g["short"], g["long"]
if s["mid"] is None or lo["ask"] is None:
if lo["ask"] is None:
continue
qty = s["csize"] * s["minamt"]
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))
ims.append(im_short_put(s["S"], s["K"], usd(s["mark"], s), s) + usd(lo["ask"], lo))
if not ims:
continue
im, ml = float(np.median(ims)), float(np.median(mls))
print(f"{fam:<10} {im:>15.2f} {ml:>11.2f} {im/CAPITAL:>8.1%} "
f"{CAPITAL/im:>20.1f} {CAPITAL/ml:>12.1f}")
im, ml = float(np.median(ims)), m["ml"]
n_budget = math.floor(RISK_BUDGET * CAPITAL / ml) if ml > 0 else 0
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(" venue, FONTE SECONDARIA) + il premio dell'ala, che si paga in contanti. E' il")
print(" fabbisogno se il venue NON netta le gambe.")
print(" `max-loss` = perdita massima strutturale = il fabbisogno se il venue LE NETTA.")
print(" ⚠️ La differenza fra i due numeri e' l'unica cosa importante di questa tabella e NON")
print(" E' LEGGIBILE dall'API pubblica: si chiude con `private/get_margins` (lettura, ma")
print(" autenticata -> fuori dal perimetro dichiarato di questo file).")
print(" venue, FONTE SECONDARIA) + il premio dell'ala, che si paga in contanti = il")
print(" fabbisogno se il venue NON netta le gambe. `max-loss` = il fabbisogno se LE NETTA.")
print(" ⚠️ Quale dei due valga NON e' leggibile dall'API pubblica: si chiude con")
print(" `private/get_margins` (lettura autenticata, fuori dal perimetro di questo file).")
print(" Ma la conclusione NON dipende da quale sia: il collaterale non e' il vincolo in")
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("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(" `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:
t = tab.get(f)
if t:
n = t["nliq"] + 2 # + get_instruments + get_book_summary
print(f"{f:<10} {n:>14.0f} {n/4.0:>11.1f} {n/650*100:>11.0f}%")
sub = [r for r in rows if r["fam"] == f]
two = sum(1 for r in sub if r["bid"] is not None and r["ask"] is not None)
print(f"{f:<10} {tab[f]['nliq']:>10} {two:>9} {(two+2)/4.0:>19.1f} "
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(" il 29/07 (fallback silenzioso del feed 5m di SKH01). Ogni famiglia aggiunta allunga")
print(" la finestra in cui il giro occupa l'IP.")
@@ -471,7 +566,7 @@ def main() -> int:
print("=" * 104)
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():
raw = json.loads(CACHE.read_text())
print(f"\n[cache] {CACHE}")
@@ -481,8 +576,7 @@ def main() -> int:
raw = fetch_all(b)
CACHE.parent.mkdir(parents=True, exist_ok=True)
CACHE.write_text(json.dumps(raw))
print(f"\n[rete] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429, "
f"{time.time()-t0:.0f}s")
print(f"\n[rete] {b.calls} chiamate, {b.err} errori, {b.r429} 429, {time.time()-t0:.0f}s")
rows = build(raw)
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.")
return 1
unita(rows, b, rete)
tab = famiglie(rows)
replica(tab)
res = liquidita(rows)
liquidita(rows, tab)
res = struttura(rows)
vive = [f for f in res if res[f]["due"] >= 2]
if a.cache:
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:
vive = [f for f in res if res[f]["n"] >= 2]
if rete and vive:
guardia_finestra()
profondita(rows, vive, b)
else:
print("\n3b. PROFONDITA': non girata (--cache, oppure nessuna famiglia con >=2 strutture).")
collaterale(rows, res)
costo_raccolta(tab)
costo_raccolta(rows, tab)
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(" * Non c'e' STORIA: il collettore raccoglie BTC/ETH inverse, l'archivio ereditato pure.")
print(" Zero giorni di catena su SOL/XRP/HYPE/AVAX/*_USDC -> nessun backtest, nessun")
print(" hold-out, nessun deflated-Sharpe. Il massimo verdetto ottenibile e' LEAD.")
print(" * Non c'e' STORIA: il collettore raccoglie BTC/ETH inverse, l'archivio ereditato")
print(" pure. Zero giorni di catena su queste famiglie -> nessun backtest, nessun hold-out,")
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(" 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(" * 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(" * 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(" * E' UN ISTANTE: uno spread relativo mediano misurato a un'ora del sabato non e' la")
print(" sua distribuzione. Servirebbe almeno un ciclo settimanale per sapere se e' tipico.")
print(f"[rete, totale] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429")
print(" * E' UN ISTANTE, per giunta di SABATO pomeriggio: uno spread relativo mediano a")
print(" un'ora del fine settimana non e' la sua distribuzione. Un book alt puo' essere")
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
+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)")
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)")
# 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):")
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)
if nm == lead:
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("-" * L)
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}"
f"{'pos/n':>8}")
f"{'pos/n':>8}{'barre att.':>12}")
wf, wi, wh = {}, {}, {}
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,
@@ -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]),
offs, metric=sh)
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}"
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_is = max(WGRID, key=lambda w: wi[w])
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}")
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(" -> se e' ~0 o negativa, la scelta di w in-sample NON predice l'hold-out: l'asse")
print(" e' rumore e il picco a w=90 e' un max-of-k.")
print(" LETTURA (attacco 4 FALLITO, e il motivo e' informativo): la scelta di w")
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
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)")
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")
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("=" * L)