diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 384f49d..e685821 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -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). diff --git a/scripts/research/r0822_alt_options.py b/scripts/research/r0822_alt_options.py index 2cad5af..1a433e3 100644 --- a/scripts/research/r0822_alt_options.py +++ b/scripts/research/r0822_alt_options.py @@ -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 - 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)") + 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 - 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.") + 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 + 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 diff --git a/scripts/research/r0822_basis_calendar.py b/scripts/research/r0822_basis_calendar.py index 22d2ddc..cd7227a 100644 --- a/scripts/research/r0822_basis_calendar.py +++ b/scripts/research/r0822_basis_calendar.py @@ -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():,})") # ========================================================================== diff --git a/scripts/research/r0822_monitor_audit.py b/scripts/research/r0822_monitor_audit.py new file mode 100644 index 0000000..a73f1c3 --- /dev/null +++ b/scripts/research/r0822_monitor_audit.py @@ -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() diff --git a/scripts/research/r0822_volsize_skeptic.py b/scripts/research/r0822_volsize_skeptic.py index 95d9c8d..324327d 100644 --- a/scripts/research/r0822_volsize_skeptic.py +++ b/scripts/research/r0822_volsize_skeptic.py @@ -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)