diff --git a/.gitignore b/.gitignore index 3da9ac7..40599b1 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,7 @@ data/live/ # dati esterni di ricerca (on-chain CoinMetrics community, F&G) β€” non certificati, non in git data/external/ data/options_daily/ + +# libro di bordo: il DB e' dato operativo (coperto dal backup), non codice +data/live/trades.db +data/cache_regime_capitale.json diff --git a/CLAUDE.md b/CLAUDE.md index 564de2d..29494a6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,6 +101,21 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis ammettere uno sleeve. Quando serve la banda di un AGGREGATO si campiona lo spazio congiunto; gli audit uno-sleeve-alla-volta (02/07, 03/07) restano validi per giudicare *quello* sleeve. Diari `2026-07-02-anchor-audit-xs01-skh01.md` (originale) e `2026-07-26-loo-deluck.md` (misura). + 🚨 **Β«POSITIVO IN N/N ANCOREΒ» NON E' N OSSERVAZIONI β€” misurato 2026-08-23, e tocca OGNI bullet + di questo file che cita una banda d'ancora.** Correlazione media fra le **24 ancore di TP01 + 0,631 β†’ N_eff 1,55**; fra le **10 fasi di XS01 0,790 β†’ N_eff 1,23**. **E non si salva sulla + differenza appaiata:** la componente comune **non si cancella** (corr 0,593 β†’ N_eff 1,64-2,24) β€” + era l'attesa a priori del critico ed e' stata **REFUTATA**. Sulla stessa grandezza, la **banda + d'ancora e' 4,3x piu' stretta dell'IC95 bootstrap, che CONTIENE LO ZERO** (su XS01 il rapporto e' + 3,8x: due misure indipendenti, stesso fattore ~4). + ⚠️ **Cio' che NON significa: che i verdetti cadano.** Le decisioni prese con l'audit d'ancora + poggiano anche su iso-vol, maxDD, selection-on-holdout ed edge lordo. **Cade la PRECISIONE dei + numeri, non la DIREZIONE delle decisioni.** + **REGOLE:** (a) Β«N/N ancoreΒ» si cita come **robustezza alla SCELTA dell'ancora**, ~2 osservazioni + indipendenti β€” **non come N prove**; (b) **la banda d'ancora NON e' un intervallo di confidenza** + e non va usata al posto di uno; (c) **non attaccare un p-value a un conteggio di ancore o di + finestre sovrapposte** (caso preso in fallo: un Β«test dei segni 12/12, p=0,0005Β» su finestre 30g + sovrapposte *e* selezionate come le peggiori). - **SKH01-V2-DD "Skyhook" β€” DIVERSIFICATORE quasi-ortogonale (research)** β€” `src/strategies/skyhook.SKH01_V2_DD`, sleeve `src/portfolio/sleeves._skyhook_returns`. Sistema dual-TF (segnale 690m / exec 230m) regime (BuzVola/BuzVolume tipo-Chande) AND pattern (Donchian breakout), NON trend-follower, L/S. Vincitrice @@ -260,7 +275,13 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis (c) una soglia sull'ampiezza di un IC richiede di **verificare la copertura** di quell'IC; (d) *"non abbastanza campione" non e' "nessuna differenza"* β€” si aspetta con una data. πŸ’° **A $3.000 la domanda non e' il rendimento** (`min_trade_amount` letti dal venue il 30/07): - **BTC min 0.1 = $6.210 di collaterale/lotto β†’ FUORI**; ETH min 1 contratto = $1.832 β†’ **1 lotto**. + ~~**BTC min 0.1 = $6.210 di collaterale/lotto β†’ FUORI**; ETH min 1 contratto = $1.832 β†’ **1 lotto**.~~ + 🚨 **CORRETTO 2026-08-23 (Β§50): misurato sulla famiglia INVERSE, che un conto USDC non puo' + marginare.** Sulla **USDC-lineare** il lotto ETH e' **$242** e il BTC **$772** (7,5x piu' piccoli) + β‡’ 1 lotto ETH copre il peso di book 12% gia' da **~$2.000**, e il BTC da ~$6.440. + **CADE IL LOTTO, NON LA REGOLA:** *niente short-vol da modello in deploy* non ha mai avuto + bisogno di un muro di taglia. **5Βͺ occorrenza dello schema `fee_watch`** β€” un numero misurato su + una configurazione diversa da quella che gira. Il sleeve 50/50 **diventa ETH-only**, e **al peso di book (12% = $360) sono 0 lotti** β€” servirebbe il **61% del conto** su un solo sleeve. **REGOLE:** (a) un'uscita si giudica su un confronto **appaiato per INGRESSO**, mai allineando le serie sulla data di USCITA β€” e' proprio quella che @@ -532,6 +553,12 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis con turnover 38.5% del lordo/g su 50 alt anche illiquidi e **slippage NON modellato** (rischio #1). **Eseguibilita':** ticket/gamba $1.73 a $600 (**sotto min-order $5 β†’ STAT-MODE oggi**), $5.76 a $2000, **$14.41 a $5000** β†’ diventa reale a **~$5k**, non ai ~$20k di XS01. + 🚨 **IL GATE QUI SOTTO LEGGE UN MONITOR MAL TARATO (misurato 2026-08-23, Β§67): il pavimento del + venue usato per l'haircut e' quello sbagliato β€” C\* vero $15.000-20.000, non i ~$3.000 riportati da + HL-EXEC con un criterio piu' permissivo.** Le soglie **NON sono state toccate**: cambiarle guardando + questa misura a due mesi dalla decisione sarebbe selection-on-holdout. Le due vie pulite: riscriverle + **adesso** dichiarando che le si riscrive prima di vedere l'esito, oppure lasciarle e **citare il + difetto al momento della decisione**. **Decisione dell'operatore.** **Gate pre-registrato 2026-10-23** (forward-day 0): deploy solo se Sharpeβ‰₯1.0 **E** haircut di eseguibilita' a $5000 ≀40% (se l'haircut sfonda β†’ **RITIRO a prescindere dallo Sharpe**), poi weights_tilt_null e capitale β‰₯$5k. Monitor in `cron_daily.sh`, 3 libri (MODELED $2000 / REAL $600 / @@ -666,7 +693,10 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis β†’ la diversificazione fra conti e' illusoria salvo **sleeve diversi su conti diversi**. E' un problema di portafoglio sotto **barriera di rovina PER-CONTO**. Sim 36 mesi, €600, max 6 conti, 2 firm, regole vere, payout mensile prelevato subito, fisco 33%, **morte-firm 10%/anno**, - bootstrap CONGIUNTO (corr reali). **Vince il MISTO, non gli estremi** (close-only, leva 1.0x): + bootstrap CONGIUNTO (corr reali). ~~**Vince il MISTO, non gli estremi**~~ 🚨 **SUPERATO 2026-08-23 + (Β§60): rifatta la stessa griglia (288 celle) vince MISTO-A, `P(>=50/g)` 9,27% contro 6,60% β€” e la + mediana e' 0,00 EUR/g in TUTTE e sei le politiche. Cade il vincitore, non l'ordine fra gli + estremi.** Numeri del 25/07 (close-only, leva 1.0x): MISTO mediana €10.70/g, P(β‰₯50/g) 20.7%, P(zero) 33.4% > CONC-DIV 5.70/17.8%/33.4% > SPARSO 0.00/16.8%/**66.0%** > **CONC-2SL (= il book live attuale) 0.00/13.1%/55.6% = la PEGGIORE**. Logica: ogni conto serve Sharpe per sopravvivere alla PROPRIA barriera (uccide SPARSO), ma i @@ -1241,6 +1271,155 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis powers*, conti dormienti) non lo sorveglia nessuno; `MAINT_GRACE_HOURS`=2 **presume** gli annunci invece di leggerli (due `locked=true` in 4 giorni: 18/08 ~4h, 21/08 ~1h); la terza referenza non e' validabile sulla storia. +- 🌊 **QUARTA ONDATA 2026-08-23 (5 filoni, `research/wave-0822`) β€” 0 candidati, 1 lead + RIDEFINITO, 1 premessa MIA falsificata, e la scorta di DATI dichiarata esaurita.** Registro + `docs/research/RESULTS-0822.md` Β§41-45. **Libro, pesi, cron, config INVARIATI.** + 🚨 **(1) Β§41 TP01-SIZE β€” in questa famiglia la selezione in-sample e' ANTI-correlata col + futuro.** *Esiste una size che risponde alla CONVINZIONE, a pari vol realizzata?* 60 celle + dichiarate prima, 24 ancore. **`Spearman(ShIS, ShHOLD) = βˆ’0,527` (p<0,001)**: scegliere in-sample + e' **peggio di una moneta**, ed e' meccanismo, non rumore β€” `Spearman(rho, ShHOLD) = +1,000` a + tutti e sei i q (in hold-out l'ottimo e' al bordo) mentre in-sample la curva e' **a gobba** con + ottimo interno. **La convinzione e' informativa in-sample e ANTI-informativa in hold-out** (BTC + hold-out: convinzione debole Sh **+2,20** contro piena **βˆ’0,06**) β†’ l'unica cella con guadagno + hold-out robusto e' esattamente quella selezionabile **solo guardando l'hold-out**. **Non esiste + una cella proponibile.** Il meglio della griglia vale **+0,30pp** di CAGR iso-vol e il maxDD di + libro peggiora a **24 ancore su 24** β€” cioe' proprio cio' per cui TP01 esiste. + 🚨 **CORREZIONE A UNA MIA PREMESSA, verificata al sorgente: il bucket di convinzione 2/3 NON + ESISTE.** `tsmom_blend` media **tre `np.sign(...)`** β†’ direzione in **{βˆ’1,βˆ’1/3,+1/3,+1}**, dopo il + clip long-flat in **{0, 1/3, 1}** (BTC 44,2/24,7/31,1%). Trovato da **due agenti indipendenti**. + **Conseguenza:** potenza `c^p`, floor affine e soglia sono **la stessa cella riparametrizzata** β€” + la famiglia della convinzione ha **UN** grado di liberta', non tre. + πŸ“Œ **Null del RE-LEVERING, forma piu' pura mai misurata (6Βͺ occorrenza, prima col segno + rovesciato):** togliere il vol-targeting fa **CAGR 16,32% β†’ 46,75% (+186%)** e a iso-vol + **βˆ’2,40pp**. **Era tutta leva.** + ⚠️ **Fatto sull'harness:** `altlib.tp01_baseline_daily()` **NON e' bit-exact col sleeve** β€” 1 ulp + su **68,5%** dei giorni, perche' `_to_daily` fa `(1+r).resample("1D").prod()-1`, no-op + **algebrico** ma non in virgola mobile. *"Bit-exact vs altlib" non e' un controllo ottenibile.* + ❌ **(2) Β§42 TP01-LS β€” la short del trend a 1d SCARTATA, e piu' nettamente dell'attesa.** + Meccanismo congelato, un solo grado di liberta' (floor da 0 a βˆ’1); βœ… replica bit-exact a 4 vie + (**floor=βˆ’1 == `CANONICAL(long_only=False)`**, `max|diff| = 0.0`). Esito: **βˆ’1,91%/anno di drift + in 0/24 ancore**, **gate iso-vol FAIL 3/3** (non e' leva travestita: alza la vol **e** abbassa il + ritorno), **maxDD +8,80pp in 24/24**, NEUTRAL con alpha βˆ’2,60%/anno, e **a fee ZERO** 1,338 contro + 0,904 β†’ *non e' morte-per-fee, l'edge lordo non c'e'*. **L'attesa a priori ("sara' il 2022") e' + confermata e superata: 2/8 anni positivi, e SENZA il 2022 il divario RADDOPPIA** (dShFULL βˆ’0,406, + iso-vol βˆ’7,44%) β€” *il 2022 non e' l'anno che regge la tesi, e' l'unico che la tiene a galla a + meta'*. **Non selezionabile**: `select_cell_insample` sceglie il canonico. + ⚠️ L'unica cosa a favore (**dShHOLD +0,292 in 23/24**) **all'ancora canonica vale βˆ’0,008** = caso + **speculare** della lezione del 26/07 (li' l'ancora canonica inventava un danno, qui ne nasconde + un vantaggio). πŸ“Œ **Trovato per strada: `src/live/book.py` contiene `max(tp_frac, 0.0)` β€” il + long-flat e' cablato anche nell'ESECUTORE**, non solo in `CANONICAL`. + 🚨 **(3) Β§43 DATA-UNUSED β€” LA SCORTA DI DATI DEL PROGETTO E' ESAURITA.** Inventario su **647 + file `.py`** (`Old/` escluso): **ogni dataset con storia vera e' gia' stato analizzato almeno una + volta**; cio' che resta non letto e' **telemetria a finestra corta (30-113 giorni)**, con una sola + eccezione β€” `external/coinmetrics/` (**26 colonne su 31 mai lette**, storia dal 2018). + Misurato prima di usarlo che il dato **fosse** nuovo: il flusso **netto** e' `corr +0,9993` con la + derivata dello stock gia' ucciso il 24/07, il **lordo** e' ortogonale (`+0,035`) perche' **il 94% + del flusso si cancella**. E perde: **massimo atteso dal PURO RUMORE su 36 trial = Sharpe 1,009, il + candidato fa 0,951 β€” sotto**; DSR 0,437; multi-cut negativo 7/7 anni. + πŸ“Œ **METODO RIUSABILE: mettere nella griglia una variabile che si SA gia' morta** trasforma + *"il candidato perde"* in *"il dato non contiene nient'altro di selezionabile"* β€” qui la selezione + in-sample e' **tornata sul controllo negativo**. E' l'unica forma in cui un inventario puo' + concludere qualcosa di positivo su un'assenza. + ⚠️ **Conferma diretta del Β§10** catturata dall'agente su se stesso: il pool *"N=12"* del + deflated-Sharpe erano **le 12 celle MIGLIORI** e faceva **PASSARE** il candidato a **0,975** + invece di 0,678. + ❌ **(4) Β§44 DEPOSIT-TIMING β€” il timing dei bonifici non paga, 0/12 celle in OGNI lente** + (€500/m, €250/m, IID, drift dimezzato). βœ… Replica 4/4 prima di ogni delta, fra cui P0 vs + `PN.accumula` a **`max|dif| = 0.0`** su capitale **e** giorno d'arrivo. + πŸ“Œ **Il segnale FA quello che promette e perde lo stesso:** dip 20% compra al **0,494** del + livello medio del piatto (**51% piu' in basso**) e arriva al capitale-rendita nel **6,3%** dei casi + contro l'**85,4%** del piatto β€” *comprare meglio e comprare tardi sono la stessa mossa*. E sotto + soglia bassa **si ribalta**: dip 5% compra a **1,009**, cioe' **piu' in alto** (su una serie che + sale, una condizione poco profonda non compra il calo, **ritarda dentro la salita**). + πŸ“Œ **L'asimmetria e' la misura: un rialzo del 20% dal minimo arriva in 16 giorni, un ribasso + del 20% dal massimo in 3.682.** *Il costo non e' scommettere sul lato sbagliato, e' mettere una + **condizione rara** davanti a un bonifico β€” in qualunque direzione.* + Rischio di venue **separato** dal timing: tenere i soldi fuori vale +2,4pp a p=1% contro βˆ’15,7pp + di penalita' di timing, e **`P(libro azzerato)` e' IDENTICA per tutte le politiche** (21,6%) β€” *il + conto salta comunque, cambia quanto c'e' sopra*; la protezione si compra con un **secondo conto**. + ⚠️ Due errori dell'agente catturati da un controllo: un diagnostico che diceva *"il buy-the-dip + contiene previsione"* (+11,5%) e che **sotto IID vale +14,9%**, cioe' **piu' grande** β†’ meccanico; + e lo specchio anti-dip, che **si legge sulla colonna dell'ATTESA, non su quella del segno** (P4 e' + piatto perche' la sua condizione e' quasi sempre gia' vera). + **Ordine di grandezza: la migliore politica di timing vale βˆ’0,1pp, dentro il rumore MC di 2,0pp; + €250 β†’ €500 al mese vale +71pp (14% β†’ 85%).** + ⚠️ **(5) Β§45 SPOT-NETTING β€” il lead dello spot REGGE, ma non per la ragione per cui il filone e' + stato aperto.** Il margine **non e' il vincolo**: BTC/ETH sono `in_cross_collateral_pool` (11 + valute su 51), lo spot ha `max_leverage: 10` (β‡’ vive nel conto marginato) e nel caso peggiore la + **copertura e' 50,0x, invariante al capitale** β€” anche valutando lo spot **zero** come collaterale. + **L'haircut non e' leggibile e non e' binding**: *unico caso in cui "non lo so" e "non conta" + coincidono, e va detto in quest'ordine.* + 🚨 **A bloccarlo e' IL CODICE, non Deribit:** `src/live/shadow.py::_equity` legge **solo** + `account_summary("USDC")` β†’ con $477 in spot il libro **leggerebbe $159 invece di $636 (25%)** e il + riconciliatore **ricomprerebbe** la gamba. *Non e' un cambio di strumento, e' un cambio del + percorso di SIZING del live.* + 🚨 **La separazione delle gambe non vale nulla, su tutti e tre gli assi:** (i) **T1 sbloccato e' + un DECLASSAMENTO** β€” `onbook_tp βˆ’ hourly` **riproduce** il pubblicato (+0,051/+0,057) ma contro il + path che il live **gia' percorre** fa **βˆ’0,048 FULL (6/23)** *(conferma indipendente della + decisione del 26/07, per un'altra strada)*; (ii) il **netting perso RISPARMIA** commissioni + (**$4,34 β†’ $2,97/anno, βˆ’0,216%**) β€” la stima "1-2%" di Β§39 era **alta di un ordine di grandezza** β€” + e le ore a **segni opposti** sono lo **0,61%** della storia; (iii) meno ordini **non e' meglio**: + il tracking peggiora del **+12%** (5Βͺ occorrenza della lezione *"banda in valuta assoluta"*). + **Il valore e' il FUNDING e basta: lordo +1,58%/anno β†’ netto [+1,55%, +1,66%]**, [+1,33%, +1,44%] + se lo spot passasse a 3,50 bps, **+0,50% nello scenario fiscale sfavorevole**. Il costo vero + d'esecuzione e' lo **spread** (perp a **un tick**, spot 1,5-3,1 bps) β†’ **[βˆ’0,034%, +0,077%]/anno**, + ampiezza 7% del lead: **l'esecuzione non decide questo lead**. + ❌ **Ipotesi dell'agente nata e refutata nel filone:** l'`apr 3,40` di USDC in `get_currencies` + costerebbe fino a **2,55%/anno** β€” ma su **6 run col conto flat (il piu' lungo 257 giri orari)** + l'equity e' **invariata al centesimo** β‡’ **< 0,029%/anno**. *L'obiezione e' morta e il lead non + deve pagarla.* + 🚨 **Due costi NUOVI mai contati, che la separazione introduce:** (a) se i derivati sono + `c-quater` i **comparti fiscali si SEPARANO** β†’ le minusvalenze del perp (SKH01) smetterebbero di + compensare le plusvalenze spot (TP01); (b) il disaster-SL oggi protegge la posizione **netta**: + separando resterebbe **solo sul perp**, e in un blackout **TP01 resterebbe long al 75% del conto + senza stop**. (Di contro, lo spot **non si liquida**: esce dal perimetro il 75% del libro.) + ⚠️ E `fee_watch` **non vedrebbe** una gamba spot (deriva `INSTRUMENTS` da `BOOK_INSTRUMENT`, il + difetto corretto il 21/08); `convenzione('BTC_USDC')` ritorna **`'ignota'`** β†’ servirebbe una + **terza famiglia 'spot'**. *La ragione per sorvegliarlo non e' che la fee lo ucciderebbe (perde il + 14% del guadagno e sopravvive), e' che oggi nessuno se ne accorgerebbe.* + πŸ“Œ **IL PROSSIMO PASSO NON E' UN BACKTEST: e' la domanda al commercialista** (gia' aperta dal + 07/08) **e un acquisto di prova da $7,73** β€” le tre cose non determinabili senza conto reale + (haircut, se `equity` USDC includa i saldi BTC/ETH, se `get_positions` ritorni lo spot) si + risolvono cosi'. **E' un ordine β‡’ decisione dell'operatore.** + πŸ“Œ **(6) LA RIGA DI SINTESI.** Quattro filoni su cinque SCARTATI, il quinto ridimensionato a + *"vale il funding e basta"*. Ma la misura piu' grande dell'ondata resta quella di Β§44: **la + migliore politica di timing vale βˆ’0,1pp mentre passare da €250 a €500 al mese vale +71pp**. + **Con questa sono 45 filoni in due giorni e 0 candidati promossi: la ricerca ha smesso di essere + il vincolo binding il 26/07, e sette ondate lo hanno CONFERMATO invece che ribaltarlo.** +- 🚨 **IL TRASPORTO DEGLI ALLARMI E' UN PUNTO SINGOLO DI GUASTO β€” misurato 2026-08-23, NON + riparato (produzione, decisione dell'operatore).** Registro `docs/research/RESULTS-0822.md` + (sezione NOTIFIER). Il progetto ha tarato il **rilevatore** (`venue_watch`: 1 falso allarme in 8 + anni, controllo positivo su 22 episodi Bitfinex, margine 3x su FTX) e **mai il trasporto**. + `src/live/notifier.send()` fa **UN tentativo** `urlopen(..., timeout=10)`, `except Exception: + return False`, **nessun retry**; `notify()` ritorna `bool` e **`scripts/live/venue_watch.py:80` + non lo guarda**; `logs/cron_book.log` ha **0 occorrenze** di un qualsiasi esito d'invio β†’ *quando + serve sapere se l'allarme e' arrivato, l'informazione non e' stata scritta* (la regola del 29/07 + β€” *"se un errore si ingoia per non bloccare, si registra nel punto in cui lo si ingoia"* β€” non e' + mai stata applicata al notifier). + 🚨 **E lo stato e' marcato PRIMA dell'invio:** in `venue_watch` la riga `new.alerted = True` + sta dentro la logica pura e viene persistita comunque; alle ore successive + `already = st.alerted and st.sign == sign` fa tornare `"WATCH"` invece di `"ALERT"` β†’ **`notify` + non viene piu' chiamata per quell'episodio**. **Un 🚨 perso e' perso per l'episodio intero**, e + gli episodi storici durano **200-2.324 ore a segno costante**: e' esattamente il caso in cui la + ri-notifica non arriva mai. + **Taglia misurata: 6,9% di invii falliti (2/29, IC95 Wilson [1,9%, 22,0%])** sul digest + giornaliero. ⚠️ **Fonte dichiarata:** e' il percorso del **digest** usato come **proxy** di quello + d'allarme, che **non ha dati propri β€” ed e' questo il punto**; stessa funzione, stesso endpoint, + stesso timeout. ⚠️ La nota stampata (`"config Telegram assente o rete KO"`) **conflazione due + cause e la prima e' falsa**: le chiavi sono presenti in `.env` (verificato) β†’ manda a controllare + il posto sbagliato, **stessa forma del difetto codificato il 29/07**, su un percorso diverso. + **Perche' conta piu' del suo numero:** l'economia del 26/07 (falso allarme 0,248% di equity + contro il **100%** che un vero positivo evita, break-even `p > 0,031%`) assume che l'allarme + **arrivi**, e questa e' l'unica mitigazione rimasta dopo la decisione *100% Deribit fino a $20k* + contro un rischio prezzato a **P(perso tutto) 10/18/34/64%**. + **Riparazione in tre pezzi indipendenti, NON eseguita:** (a) retry con backoff in `send()`; + (b) **registrare l'esito** nel punto in cui l'eccezione viene ingoiata; (c) marcare + `alerted=True` **solo a invio riuscito**. ⚠️ **(c) cambia il comportamento** β€” rende l'allarme + ripetitivo finche' non passa: verso giusto per un 🚨, sbagliato per un ⚠️ β†’ **decisione + dell'operatore**, non un fix ovvio. + **REGOLA: un rilevatore si valida sul segnale E sul TRASPORTO** β€” tarare la soglia e non misurare + mai se il messaggio arriva lascia un punto singolo di guasto a valle di tutto il lavoro di + taratura, e non produce numeri, quindi non si fa notare. - βš–οΈ **RIVALUTAZIONE DELLA STRATEGIA (2026-07-26, fine giornata) β€” 0 cambi, e il peso 75/25 confermato per la TERZA volta.** Script `r0726_reeval_live_weight.py`, test `tests/test_reeval_live_weight.py` (6), diario `2026-07-26-reeval-strategia.md`. @@ -1992,6 +2171,915 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis ⚠️ **Nota per il futuro-me:** e' una decisione presa con l'informazione completa, non una svista. Se qualcuno propone SOL "perche' e' eseguibile su Deribit", la risposta e' che l'eseguibilita' non e' mai stata il problema β€” l'hold-out lo e', a 24 ancore su 24. +- 🌊 **ONDATA MULTI-AGENTE 2026-08-22 (20 filoni, branch `research/wave-0822`) β€” 0 candidati, 1 + DIFETTO DI PRODUZIONE, **4** soglie pubblicate falsificate, 1 leva che nessun gate sa vedere.** + Diario `docs/diary/2026-08-22-wave-multiagente.md`; ledger per filone `docs/research/RESULTS-0822.md`; + briefing `docs/research/BRIEF-0822.md`; 21 script `scripts/research/r0822_*.py` (15.295 righe, + compilano tutti, ognuno gira da solo). **Book, pesi, cron, config INVARIATI.** + 🚨 **(1) I MONITOR FORWARD REGISTRANO UNA FRAZIONE DEL GIORNO β€” 4 su 6 rotti.** + `fetch_hyperliquid` e `resample_tf` scrivono la barra del **giorno in corso**; `advance()` la + consuma e **porta `last_ts` su di essa** -> le ore restanti non entrano in nessun rendimento. + | monitor | barre coincidenti | **min/giorno** | gate | + |---|---|---|---| + | `paper_statarb` | **0/46** | **4** | **STATARB 27/09** | + | `paper_dvolspread` | 0/28 | **2** | DVOLSPREAD 24/10 | + | `paper_xsr` | 1/28 | **41** | XSR01 23/10 | + | `paper_portfolio` | 7/56 | 19 | β€” | + | `paper_prevday` | 1259/1319 | 1.438 | β€” | + | `paper_combo` | 7/37 | 1.402 | β€” (sano **per caso**: aspetta una borsa) | + **Il gate STATARB si ribalta su 2 criteri su 3**: Sharpe registrato **+1,96** contro **βˆ’2,02** + ricostruito, maxDD **2,2% contro 15,3%** (guardia <10%). + βœ… **La finestra NON va persa** (misurato 2 volte: il feed non riscrive le barre chiuse β€” + `paper_prevday` 1427/1488 identiche al bit dopo 62 notti; le 6 barre di statarb recuperate dopo il + guasto EPERM 09-15/07 coincidono al bit) -> **"riparare `advance()` + RIGENERARE", non "riparare e + azzerare": nessuna data di gate si sposta** (pessimista: STATARB +18g). + **Guardia raccomandata, NON cablata:** `ts_ultima_barra + cadenza <= mtime del file`, grazia 5 min. + O(1), segnala 5/6, **tace sul sano**, controllo positivo sintetico 6/6 nei due versi. + ⚠️ `monitor_health` non poteva vederlo: **una serie fresca, completa e SBAGLIATA passa ogni + controllo di freschezza**. E `implausible_sharpe` non e' **mai** stato puntato sulle serie forward. + πŸ“Œ **(2) TRE SOGLIE PUBBLICATE, FALSIFICATE.** (a) *"XS01 serve ~$20k"* (origine: "rumore di + arrotondamento", stima a occhio del 19/06) -> **nessuna soglia da min-order**, haircut ~0 gia' a + **$200-600 allocati**. Meccanismo: gli ordini sono **due popolazioni** β€” il ribilanciamento del + segnale e' il 13% degli ordini ma il **75% del nozionale** (ticket $13,65); la deriva del + vol-target e' l'87% degli ordini ma il **25%** (ticket $0,64) e **il pavimento la taglia gratis**. + *Contare gli ordini da' 16%, contare il nozionale da' 75%.* ⚠️ **Il "$20k di XS01" e il "$20k + della DECISIONE DI VENUE" sono due cose diverse conflate in questo file: il primo cade, il secondo + REGGE** (e' sull'asse della rovina, non dell'eseguibilita'). (b) *"slippage = rischio #1 di XSR01"* + -> **refutato con margine 21x**. (c) *"BTC opzioni fuori a $3.000"* -> misurato sulla famiglia + **inverse**, che un conto **USDC non puo' marginare**; la famiglia USDC-lineare ha lotti **10x piu' + piccoli** (ETH_USDC **$242**, BTC_USDC $772 contro $7.744). ~~Il muro resta **per il prezzo + del lotto**.~~ 🚨 **CORRETTO 2026-08-23 (Β§46): quelle cifre sono il COLLATERALE PER VENDERE. + COMPRARE un'opzione costa il PREMIO, non il nozionale** β€” `min_trade_amount` **0,01** su tutte le + 343 put BTC_USDC (βœ… verificato al venue) e il premio del lotto minimo di una put deep-OTM va da + **$0,00 a pochi dollari** β‡’ **sul lato LONG il muro di capitale non esiste** (e la regola *niente + short-vol da modello* non ha mai avuto bisogno di un muro di taglia per reggere). + πŸ“Œ **Il muro vero sul lato long e' un altro, ed e' STRUTTURALE: il tick da 5,00 USDC** (βœ… + verificato, `tick_size = 5.0` su tutte) β€” **piu' l'opzione e' deep-OTM, cioe' economica, piu' il + tick domina**: una put marcata 0,82 USDC non e' quotabile sotto 5, cioe' un pavimento di prezzo + del **6x** sulle ali lontane, che nessun capitale aggira. + Parametri veri Hyperliquid: **min order $10** (non $5), **taker 4,50 bps** (non 5,0). + πŸ“Œ **(3) LA LEVA E' L'UNICA VARIABILE CHE NESSUN GATE DEL PROGETTO SA VEDERE.** Lo Sharpe e' + **invariante alla scala** (1,31 a ogni cella del knob) -> `deflated_sharpe` e `marginal_vs_tp01` + non falliscono, **non la vedono**. Il libro gira al **7% di Kelly** e raccoglie il **15%** della + crescita massima in log; il gradino eseguibile 1,00x->1,25-1,50x vale **14,7a -> 12,9-11,6a** al + capitale-rendita (€500/m, netto fisco). **Lo scettico conferma il numero e toglie la sua condizione + bloccante**: sotto la lente wick accoppiata il maxDD prende un **ricarico moltiplicativo costante + ~3,5%**, non un'amplificazione (**il maxDD e' multi-giorno, il wick e' di un giorno**). + **Il gradino resta NON autorizzato**: cade 1 riserva su 3; restano il drift stimato su 7,4 anni + (k* e' lineare nel suo errore) e la **coda assente dal dataset** (un giorno βˆ’10%/anno -> k* 2x). + ⚠️ **Ma la lezione del 25/07 e' vera e NON si trasferisce:** su una regola a **UN giorno** + (daily-loss, stop di conto) close-only e' **esattamente cieca** β€” 0,00 breach/anno contro 0,40-1,21 + veri (**rapporto INF**). **Sul canale prop/funded la lente accoppiata resta OBBLIGATORIA.** + ⚠️ **`target_vol` e `k` NON sono la stessa leva:** `target_vol` scala il 75% del libro e lascia + fermo il 25% (a tv=30% il libro e' **82/18**, a 60% **90/10**) -> e' anche un tilt di pesi, e + **fallisce `weights_tilt_null`**. La strada pulita per un cambio di SCALA e' il cap di config. + πŸ“Œ **La vol realizzata di TP01 a `target_vol=20%` e' 12,14%**, scomposto: `20% x 0,682 x 0,890`, + dove lo 0,682 sono **due meta' quasi uguali** (flat nel 44,3% delle barre **e** convinzione + parziale) -> **"TP01 e' long-flat" spiega meta'**; e alzare `target_vol` **aumenta la size proprio + nei giorni in cui il segnale e' piu' debole**. Il x0,890 e' **diversificazione, non un difetto**. + ⚠️ **XS01 dichiara lo stesso 20% e ne realizza 20,59%: vale per TP01, non per il libro.** Nessun + numero pubblicato cambia (sono tutti sulla serie **realizzata**); sbaglia chi legge l'**etichetta** + (attribuirebbe a TP01 l'89% del rischio invece del **73%**). + πŸ“Œ **(4) PROP/FUNDED β€” l'ipotesi centrale e' falsificata, e per la ragione teorica giusta.** + Riallocare *per la barriera* invece che *per lo Sharpe* vale **+0,016 di J (il 4%)**: il conto del + gambler's ruin lo prevedeva (**la vol sparisce, il contenuto sta nella LEVA**). Cio' che vale e' + **mettere XS01 su un conto funded: +0,400** (J 0,335 -> 0,738; P(pass) 38,6% -> 82,2%). Leva ottima + **0,75x HYRO / 1,00x FTMO**, e **la barriera piu' larga alza l'ottimo: il vincolo e' la regola di + DD, non il libro**; la leva alta **non compra probabilita', compra fretta**. Il **75/25 del libro + live non e' la ripartizione giusta per un conto a barriera** (sul 2019+ che contiene il 2022 + l'ottimo e' **38/62 @0,50x**). **Il null dei tilt dice che il PUNTO non conta**: il massimo di 200 + pesi casuali (0,739) **eguaglia** l'argmax (0,738) β€” conta stare nella regione diversificata. + 🚨 **Il numero da citare e' LA BANDA: P(>=50/g) da €600 in 36 mesi = 42% al drift pieno Β· 7,7% allo + stress moderato Β· 0,7% a quello severo** (e li' e' **peggio del libro live**), perche' **tutto il + vantaggio poggia sul drift di XS01 misurato sulla sua finestra di scoperta**. Gate proposto: non + aprire un funded su questa allocazione prima che XS01 abbia una finestra fuori dal 2024-2026. + ⚠️ Distorsioni dichiarate e non rimosse: banda d'ancora **non girata** sotto quella lente (l'ottimo + gira sull'ancora canonica e si appoggia a **SKH01, che ha la fortuna d'ancora piu' grande da + restituire**); e la sotto-pesatura di **TP01, lo sleeve difensivo, e' misurata su un campione senza + sinistro** β€” su una barriera assorbente quella scommessa si paga una volta sola. + πŸ“Œ **(5) LO SLIPPAGE DEL LIBRO LIVE, misurato per la prima volta: non c'e' a questa taglia.** + I backtest a 10 bps RT restano **conservativi di ~1,5 bps/lato** (fee vera 7 bps RT); estremi + avversi **1/18 contro 4,7 attesi**. Prova diretta: un fill ha preso il **21,9% del volume della sua + barra 5m** e quella barra ha avuto un range di **1,08 bps**. βœ… **Fee sul NASTRO: 5,00 bps/lato + prima del 2026-08-01 e 3,50 dopo, 16/16 esatti** = conferma indipendente di `fee_watch`. + 🚨 **Ma la misura ha una DATA DI SCADENZA:** la partecipazione scala lineare -> quel 22% diventa + **172% di una barra 5m a $5.000** e **687% a $20.000**. **Va rifatta a ogni salto di taglia.** + ⚠️ Due difetti del dato trovati prima di misurare: `ts_utc` in `book_executions.jsonl` **non e' + l'ora del fill** (18/18 = `00:00:00`; l'ora vera sta solo in `logs/cron_book.log`); e il feed + certificato e' l'**INVERSE** mentre il libro trada il **LINEARE USDC** (**4Βͺ occorrenza** dello + schema `fee_watch`: misurare li' avrebbe misurato **la base**). + πŸ“Œ **(6) IL `f=0,73` DI VRP01 SCOMPOSTO β€” il 42% e' STRUTTURA A TERMINE, solo il 25% e' skew** + (30% spread, 2% fit). **Il sleeve prezza un'opzione a 7 GIORNI col DVOL a 30 GIORNI**, che nel 90% + delle ore sta ~3 punti di vol **sopra** l'ATM a 7g -> sovrapprezza **entrambe** le gambe. Il + "+0,8pp sulla gamba corta" del 30/07 β€” che faceva sembrare **corretta** la gamba venduta β€” e' la + **cancellazione di due errori da ~3pp**. ⚠️ **E la regola per prenderlo era gia' scritta il 03/07** + ("riprezzare term-structure-consistent prima di credere a un numero da struttura BS-flat") **e non + e' stata applicata alla misura del 30/07**. Verso: `f_term` va da **0,83** (IV-rank basso) a + **0,93** (alto) e **supera 1 in backwardation** mentre `f_skew` resta piatto -> **la parte maggiore + del difetto si annulla proprio dove il gate fa tradare il sleeve**, quindi f=0,73 e' plausibilmente + **conservativo li'**. Non misurato (0/22 ingressi passano il gate) e in un crash i due pezzi vanno + in **direzioni contrarie**. **`VRP_CFG["f"]` NON cambiato.** Replica indipendente del f: **0,714**. + πŸ“Œ **(7) CHIUSURE DI FAMIGLIA.** **Il filone funding si chiude sul QUARTO lato** (affollamento): + su **53.430 ore / 3 anni** non predice ne' direzione ne' volatilita'. E i futures datati **non sono + un quinto lato**: il loro basis **E'** il funding del perp (+7,33% implicito contro **+6,48%** + realizzato; premio incassabile **+0,85%/anno con IC95 che contiene lo zero**). **La mean-reversion + non risorge** sotto due conditioner mai provati (volume, shock 3Οƒ): la selezione in-sample sceglie + il ramo di **continuazione** in entrambi. Scartati anche: adaptive-horizon (il "vincitore adattivo" + ha lookback **costante**, corr 1,000), dealer-gamma (BTC non separa, ETH separa **al rovescio**; + ⚠️ **e qui una REGOLA MIA, pubblicata la sera stessa e RITIRATA poche ore dopo:** avevo scritto che + `dealer_net_gamma` del dataset ereditato *"e' il GEX col SEGNO INVERTITO"* (corr βˆ’0,95). **FALSO, e + la fonte gira su questa stessa VPS:** `/opt/docker/cerbero-mcp/src/cerbero_mcp/common/options.py:11-14` + **dichiara la convenzione** β€” *"i dealer sono SHORT calls (le vendono al retail) e LONG puts"* β€” e + `dealer_gamma_profile:138-140` la implementa (`call_dealer_gamma -= contrib` / `put_dealer_gamma + += contrib`). Non e' un segno sbagliato: e' un'**assunzione di posizionamento dichiarata**, diversa + da quella del GEX standard, e il default `top_n_strikes=50` spiega anche il coefficiente βˆ’0,7 e + l'offset che avevo attribuito a "non-portabilita'". **Il verdetto SCARTATO regge; cade la regola** β€” + che le prossime ondate avrebbero ereditato al contrario. **REGOLA VERA: prima di dichiarare che un + campo di un dataset e' sbagliato, aprire il codice che lo produce** β€” qui erano due comandi, e la + correlazione a βˆ’0,95 con la ricostruzione "standard" era esattamente cio' che una convenzione + diversa **deve** produrre. `liquidation_*_risk` e' invece **una sola categoria in 17.229/17.229 + righe**, e quello e' confermato. + Scartati anche: oi-pin (il max-pain **non batte mai, 0/24, la media a 7 giorni dello spot**), + term-structure (**termometro contemporaneo**), skew direzionale (**il prezzo muove lo skew**, + t 3,1-9,4 su 8/8, non il contrario), ortho-screen **7/7**, alt-options, xs-lite, vrp-quote-vere. + ⚠️ **La catena ereditata da bite ha 74-75 giorni utili, non 3,7 mesi:** prima del **2026-06-09** + raccoglieva **una sola scadenza per giro** (due agenti indipendenti, 74 e 75). + πŸ“Œ **(8) REGOLE NUOVE.** (a) **L'open interest NON misura la negoziabilita'** β€” su Deribit USDC sono + quasi **anti-correlati (rho di rango βˆ’0,77)**: SOL_USDC e' 1Βͺ per OI e **ultima per quote a due + lati (9%)**, BTC_USDC ha **5** strumenti con OI>=100 e il **93%** dei put quotati (e sul short leg + di SOL il miglior bid sta a **UN tick**: vendere li' significa **pagare $27** per aprire un credit + spread). (b) **Il deflated-Sharpe va calcolato DI SCREEN, non solo di famiglia**: su 168 trial il + massimo atteso dal **puro rumore** e' **Sharpe 1,572**, sopra il soffitto direzionale (~1,3) -> + **uno screen largo su BTC/ETH direzionale non puo' passare il proprio gate, per aritmetica.** + 🚨 **QUESTO ARGOMENTO E' REFUTATO DUE VOLTE, per due strade indipendenti, e NON va ereditato:** + (i) la stessa griglia dichiarata come 7 famiglie da 24 da' **1,15**, sotto il soffitto β€” il + verdetto si ribalta sulla **partizione** (ondata 2, Β§24); (ii) **il soffitto non e' ~1,3**: la + stessa ondata pubblica un lookback costante a **1,639** e la cella PREVDAY al buio a **1,621**, + stessa coppia e stessa lente netta-fee (Β§23, Β§54). Β«SoffittoΒ» sta per tre cose diverse e il + registro usava la piu' bassa citandola come la piu' alta. + (c) **Un placebo si controlla per BILANCIAMENTO DEL SEGNO prima di usarlo** (un null che concorda + col segnale nel 14% dei casi non e' un null, e' la strategia invertita: fabbrica celle + "significative"). (d) **Il null del de-levering e' DEGENERE quando la variante e' una + ri-scalatura** (`sh(k*base) === sh(base)`): serve **iso-peso sullo Sharpe**. (e) **Un gate che + stampa `None` va verificato prima di dichiararlo "non girato"**. (f) **Su una finestra interamente + post-hold-out `marginal_vs_tp01` non puo' dare ADDS per costruzione** β€” un NEUTRAL li' non e' un + giudizio. (g) **Un confronto appaiato deve ereditare TUTTI i parametri della riga in cui compare.** + ❌ **REGOLA RITIRATA, pubblicata poche ore prima nella stessa ondata:** *"su equity a gradino un + overlay giornaliero e' non-causale"* β€” **falso su questo motore**: `backtest_signals` contabilizza + al giorno d'**INGRESSO** in **291/293** trade e la "riparazione" e' un **no-op bit-exact**; il + "+0,04 fantasma" era la differenza fra due varianti **entrambe causali**, e quella scartata era la + **migliore sull'hold-out**. *Il costo di una regola derivata da un difetto inesistente e' gia' + stato pagato.* + πŸ“Œ **(9) Cosa NON e' piu' il vincolo: TRE volte in questa ondata l'eseguibilita' a $600 non ha + bocciato niente** (haircut 0,00-0,01; lotti $7-242; margine ~$148). Dopo due mesi in cui il muro + era sempre il capitale, **adesso muoiono tutti sull'edge**. + ⚠️ **(10) FATTO OPERATIVO: il cron gira dal WORKING TREE.** Con un branch di ricerca attivo, il + cron esegue quel branch. Durante l'ondata il diff vs `main` toccava **solo** `scripts/research/` e + `docs/research/` (verificato) -> produzione identica; ed e' il motivo per cui **nessuna riparazione + e' stata applicata**: sarebbe andata live alle 00:30 senza una decisione dell'operatore. + πŸ—‚οΈ **Dataset nuovo in eredita':** **30/30 futures trimestrali scaduti** Deribit ricostruiti + (`get_instruments?expired=true` ne ritorna **1**, ma i nomi sono deterministici e + `get_tradingview_chart_data` serve la storia di un contratto scaduto): **BTC 240.150 barre orarie / + ETH 233.334, dal 2018-09, col deleveraging 2022 dentro** β€” cioe' **la coda che a CC01 mancava per + costruzione**. Cross-check col forward implicito nella catena opzioni: mediana **βˆ’0,6 bps**. + +- 🌊 **SECONDA ONDATA 2026-08-22 sera (9 filoni, `research/wave-0822`) β€” 0 candidati, 1 gate + pre-registrato SOSTITUITO, il numero funded ridimensionato di 5x, e un difetto strutturale del + deflated-Sharpe misurato da TRE agenti indipendenti.** Diario + `docs/diary/2026-08-22-wave2-xs01-funded.md`; registro `docs/research/RESULTS-0822.md` Β§21-29. + **Libro, pesi, cron, config INVARIATI.** + βœ… **(1) XS01 REGGE FUORI DALLA SUA FINESTRA DI SCOPERTA β€” ED E' PIU' FORTE LI'.** La finestra + c'era gia': stessi 19 ticker su **Binance spot 2021-2023**, che contiene **LUNA e FTX**, meccanismo + **congelato**. Mediana di fase **+1,12 fuori campione contro +0,37 dentro**; differenza appaiata + **+0,668, positiva in 10/10 fasi**; p=0,013 contro permutazione a fee zero; sopravvive a 30 + bps/lato. **L'obiezione "Binance non e' la verita'" e' stata MISURATA, non aggirata:** quella regola + riguarda l'**ancoraggio di prezzo**, non un ranking cross-sezionale, e lo scarto USDT e' un + **fattore comune** che sparisce nello z-score β€” stesso sleeve sui due venue **corr 0,9991**. + **Null "universo dove non dovrebbe funzionare"** (11 settoriali SPDR 1998+): βˆ’0,45, **0/10 fasi** β†’ + **3Βͺ conferma indipendente che il cross-sectional e' crypto-specifico**. + ⚠️ **Ma tre correzioni vanno nel verso opposto:** maxDD standalone **10,8% β†’ 21%**; + **deflated-Sharpe FAIL 0,342**; **32% dell'universo non testabile** (19 gambe 1,31 contro 13 gambe + 0,54, stessa finestra e venue). **Verdetto LEAD**, non promozione. + βœ… **(2) Il falsificatore che l'autore aveva nominato CONTRO SE STESSO β€” eseguito, e non + falsifica.** La validazione di venue girava solo sul 2024+, cioe' **senza il depeg USDT** che il + fuori campione contiene. Soglia dichiarata da lui (`corr β‰₯ 0,99`): **corr mediana 0,9992, minima + 0,9921**, dSharpe appaiato **βˆ’0,000**, titolo da +1,12 a **+1,11**; sui 734 ribilanciamenti solo + **10 cambiano una gamba su cinque** e **ZERO dentro il depeg**. **Il punto di metodo: la divergenza + va SCOMPOSTA** β€” la **comune** arriva a βˆ’39 bps ed e' quella che lo z-score annulla, la + **idiosincratica** (l'unica che cambia il ranking) sale solo da 2,0 a 3,6 bps mediani; *riportare + solo la prima da' la risposta rassicurante e sbagliata*. 🚨 **E il caso peggiore del campione non e' + il depeg: e' il 2025-10-10** (cascata di liquidazioni /USDT, **INJ 1.981 bps** mentre BTC/ETH stanno + a 21) β†’ **il meccanismo temuto e' REALE**, e' solo caduto nella finestra di *scoperta*. + 🚨 **(3) IL NUMERO FUNDED SI RIDIMENSIONA DI 5x, E IL COLPEVOLE NON E' LA FINESTRA.** + Decomposizione a **un grado di liberta' per volta** (riproduzione **esatta cifra per cifra** dei + numeri del mattino + 3 controlli bit-exact a 0,0): + | passo | J | Ξ” | + |---|---|---| + | il pubblicato (HL, 19 gambe, finestra di scoperta) | 0,739 | β€” | + | β†’ venue Binance, **date allineate** | 0,732 | **βˆ’0,006** | + | β†’ **13 gambe** | 0,426 | **βˆ’0,307** | + | β†’ pannello lungo | 0,459 | +0,034 | + | β†’ **fuori campione 2021-2023** | 0,520 | **+0,061** | + **Il 42% del numero pubblicato sta nelle 6 gambe che fuori campione non esistono** (ARB OP SUI APT + SEI TIA) β€” **e quelle 6 entrarono nell'universo nel 2026-06 guardando la liquidita' HL del 2024+, + cioe' dentro la finestra di scoperta**. La finestra, da sola, **migliora**; allargare l'universo man + mano che le gambe nascono **non recupera** i 0,307. + ⚠️ **Errore di metodo catturato prima di pubblicare:** la prima stesura dava al passo venue βˆ’0,254 + ("Binance costa un quarto di J"). Era **FASE, non venue** β€” `xsec_engine` ribilancia sulla parita' + dell'indice di pannello, e **due pannelli che iniziano a date diverse ribilanciano in giorni di + calendario diversi**. Allineati, il venue vale βˆ’0,006. + **NUMERO DA CITARE: P(β‰₯50 €/g) da €600 in 36 mesi = 7,8% [7,2–9,1%], P(zero) 25,5%** (non 42%); + 6,6% se la firm non lista gli alt; 4,3% per il libro live. **Il peso 0,50 non sopravvive; la + REGIONE [25%, 38%] e' entro il 3% dell'ottimo su tutte e tre le finestre.** Stress ora ancorati al + misurato: `XS Γ—0,00` **+0,041** β†’ **XS01 come pura DECORRELAZIONE vale +0,04, tutto il resto e' il + suo drift.** + πŸ“Œ **`GATE PROP-01` β€” SOSTITUISCE l'attesa di anni, che e' CHIUSA:** **(a) LISTINO β€” verifica a €0, + BLOCCANTE prima di ogni spesa:** β‰₯10 delle 13 gambe negoziabili **con short** (sotto 10 il + meccanismo congelato **non ribilancia affatto**); FAIL β†’ si decide sulla riga senza XS01. *Stessa + classe dell'errore GTAA01/PRIIPs, per cui la regola e' gia' scritta.* **(b)** banda d'ancora di + SKH01 sotto la lente prop, **2026-10-31**, PASS se **> +0,05** (atteso PASS, dichiarato prima). + **(c)** un'eval HYRO $100k costa **$579 su un conto di $635** = il **91%**. + ⚠️ **(4) I DUE AFFINAMENTI DI XS01 DEL 2026-06-19 NON SI REPLICANO FUORI CAMPIONE** (misurato da + **due agenti indipendenti**): il **blend [30,90] e' PEGGIO del solo L=30** (+1,12 vs +1,28) e il + **gate di dispersione p30 vale ZERO** (+1,12 con e senza) β€” mentre *dentro* la finestra di scoperta + il gate vale eccome (+0,37 contro βˆ’0,03). Il secondo agente lo ritrova per un'altra strada: l'unica + correlazione sotto soglia del suo test e' **causata dal gate** (spento, 0,9674 β†’ **0,9977**), perche' + e' **binario** e pochi bps lo fanno cadere ai due lati. **Cio' che sopravvive e' il meccanismo + cross-sectional NUDO.** ⚠️ **NON e' un argomento per cambiarli** β€” cambiarli guardando *questo* + risultato sarebbe la stessa selezione spostata di finestra β€” **e' un argomento per non attribuire + loro il valore che questa memoria gli attribuisce.** + βœ… **(5) L'ALLARME SUL PESO DI TP01 E' FALSIFICATO NEL VERSO** (l'agente aveva registrato nel + docstring, **prima di misurare**, che si aspettava di confermarlo): **TP01 ~0,375 e' l'argmax sia + sulla finestra che contiene il 2022 sia su quella che non lo contiene**, e nel sinistro il libro con + **meno** TP01 fa **meglio** β€” TP01 va flat mentre **SKH01 si gira short e guadagna**; test dei segni + **12/12** (p=0,0005) e **8/8** sul solo pre-2024. **Null del de-levering, 7Βͺ occorrenza, in veste + nuova** ("protezione dal crash"): a leva comune TP01 protegge davvero, ma **a ISO-SOPRAVVIVENZA meno + TP01 paga di piu'** ($4.490 β†’ $2.119, monotono). Tre collaterali: **(i)** *"2024-2026 non contiene + un crash"* e' **falsa nella forma** (buy&hold βˆ’60% di DD); manca la **taglia** (~2,3x sulla coda) β†’ + la domanda giusta e' **"e se ne arriva uno due volte piu' grande"**; **(ii)** alle leve funded **il + 2022 non era una minaccia** (min equity βˆ’1,4/βˆ’2,4% contro barriera βˆ’6%): **cio' che uccide un conto + a barriera e' la regola a UN GIORNO su una giornata qualunque**; **(iii)** il rischio vero + dell'ottimo e' **XS01, non TP01**. Il 75/25 live e' **fuori** dalla regione di compromesso del funded + β€” coerente col fatto gia' scritto il 25/07: *canale funded e libro proprietario vogliono libri + diversi*. + ❌ **(6) ORIZZONTE ADATTIVO β€” CHIUSO DEFINITIVAMENTE.** Il falsificatore e' stato eseguito coi + rilevatori che nominava (**BOCPD** Adams & MacKay + **optimal partitioning causale**) e **questa + volta ha avuto POTENZA**: **68/96 celle davvero adattive** (contro 0 del primo tentativo, dove il + "vincitore adattivo" aveva lookback costante). Con potenza: **0/68** su entrambe le condizioni + dichiarate, corrβ†’TP01 **minimo 0,754** (soglia 0,60). πŸ“Œ **La decomposizione E' il risultato: + l'adattivita' spiega il βˆ’7% del vantaggio su TP01** (miglior costante +0,326, adattivo +0,305) β†’ il + 100% e' **"un orizzonte piu' corto", non "adattarlo"**. E il miglior costante **non si propone**: il + salto fra L **adiacenti** e' **+0,190 = il 58% del vantaggio** β€” max-of-24 su un crinale di rumore. + πŸ“Œ **Prova di IDENTITA' invece di diagnostica** (controllo riusabile in ogni famiglia con un + parametro clippato): invece di *"questa cella sta al bordo"*, la sua serie e quella del costante + coincidono a **max|Ξ”| = 0 su n=2719**. + ❌ **(7) MAKER β€” il segno dipende da `<` contro `<=`.** Differenziale maker/taker: tetto **$2,96/anno + a $635** (replica indipendente della curva 26/07 al terzo decimale). Il costo del non-fill lo mangia + interamente gia' alla condizione di riempimento **piu' generosa fisicamente difendibile**: se una + barra che apre esattamente al nostro prezzo e da li' se ne va conta come fill, resta +0,31 bps; se + **non** conta (e non conta: a quel prezzo non e' stato scambiato niente dopo l'arrivo dell'ordine, e + un passivo sta **dietro a tutta la coda**), resta **βˆ’0,47**. **Non esiste un q positivo di + pareggio**; IC95 contiene lo zero in **10 celle su 10**. πŸ“Œ **La firma della troncatura, vista + dall'altra parte: il passivo vince sul 92-98% degli ordini e perde sulla media** (5 ordini su 1535 + fanno il **42%** del danno) β€” *una politica che vince quasi sempre e perde sulla media si giudica + sulla media*. **Placebo a ore casuali girato**: la selezione avversa **non** e' "il libro trada nei + momenti brutti", e' **strutturale all'esecuzione passiva**. ⚠️ Osservazione che riapriva la domanda: + il netting su strumento unico (che uccise T1 il 26/07) **non si applica all'ordine NETTO di + ribilanciamento**, che e' uno solo. + ❌ **(8) SUPERFICIE DI VOLATILITA' β€” il lato relative-value e' CHIUSO.** Censimento (non selezione) + su **1,24 M quote a due lati**: **0 violazioni di monotonia su 1,11 M**, **1 di convessita' su 1,03 + M** che vale **$0,00** ed e' sparita l'ora dopo. |residuo| mediano **0,125-0,200 pt-vol** contro + **mezzo** spread 0,861-1,208, e **sotto il tick nel 96% dei casi**. πŸ“Œ **IL MECCANISMO: |residuo| + cresce MONOTONAMENTE col largo del mercato** (0,080 pt a spread 2,6% β†’ 0,255 a spread 50%) β†’ + **l'incoerenza al mid *E'* la larghezza del mercato guardata attraverso il mid**, non un errore di + prezzo β€” e questo uccide il controfattuale USDC che l'agente stesso aveva costruito (restringendo + alla superficie gia' quotata stretta: **0 su 158.718**). Reversione **misurata**: mezza-vita 11-12 h, + corr(t,t+1) +0,94 β†’ il residuo e' **la forma dello smile**. Controlli positivi **3/3 in entrambi i + versi** β€” *un rilevatore che trova ~0 e non e' validato e' indistinguibile da uno rotto*. + ⚠️ **(9) BIN-FREQ (il binario di frequenza su SKH01) β€” LEAD: la direzione e' reale, la TAGLIA e' + selezione.** Argmax **+0,112 in 23/23 ancore**, ma **mediana di famiglia +0,007** con 39/72 celle + positive = **monetina**: **l'argmax vale 16Γ— la mediana, punta e non plateau**. Direzione + solidissima (**0/72** sul segno inverso; trasferisce a SKH01_V1). Criterio a 4 gambe: **3 FAIL**, e + la taglia e' **€0,0113/giorno**. Due implementazioni difendibili della stessa regola, d'accordo sul + **97,6%** dei trade, danno +0,066 contro +0,080 β†’ **il 21% dell'effetto sta in come si riempie il + warm-up**. πŸ“Œ **Riapertura pre-registrata: non una data ma una SOGLIA DI CAPITALE** ($5.600 alla + stima della cella, **$53.000** a quella onesta di famiglia) β€” perche' cio' che fallisce e' il + rapporto fra un costo **fisso** e un beneficio **proporzionale**, e nessun forward a 6 mesi puo' + misurare 0,07 di Sharpe (SEβ‰ˆ1,4). + 🚨 **(10) IL FILO DELLA GIORNATA: `deflated_sharpe` NON HA UNA REGIONE UTILE IN MEZZO β€” misurato da + TRE agenti indipendenti, e TRE gate pre-registrati ci poggiano sopra** (XSR01 23/10, DVOLSPREAD + 24/10, 22/12). `altlib.deflated_sharpe` calcola `sr0 = sd(Sharpe dei trial) Γ— mult(N)`: dipende + dalla **VARIANZA** della griglia, non solo da N. **(a)** VOL-SIZE lo dichiara **VACUO** (1,000 anche + per il baseline). **(b)** BOCPD lo misura dall'altro capo: **0,998 PASS** dentro la famiglia omogenea + β†’ **0,555 FAIL** con l'sr0 di screen, TP01 compreso β€” *"gonfiare N col padding alza N e non la + varianza"*. **(c)** Il critico trova che l'**1,572 di ORTHO-SCREEN** implica sdβ‰ˆ0,58 e che **le + stesse 168 celle dichiarate come 7 famiglie da 24 danno 1,15**, sotto il soffitto ~1,3: il verdetto + *"uno screen largo non puo' passare il proprio gate per aritmetica"* **si ribalta senza toccare un + dato**, e la cura che suggerisce (famiglie piu' piccole) e' **la manovra proibita il 30/07**. + **(d)** BIN-FREQ chiude il cerchio dall'alto: su una famiglia **binaria** il DSR **riacquista + potenza** (0,977 contro baseline 0,834) perche' le sue celle scartano insiemi di trade molto + diversi β€” *non e' il candidato a essere piu' solido, e' la famiglia a essere piu' larga*. + **REGOLA: non basta contare i trial di screen, serve la VARIANZA di screen** β€” e **la stessa griglia + da' verdetti opposti a seconda di come la si partiziona**, il che rende la scelta della partizione + un secondo posto dove barare e' indolore e invisibile. + ⚠️ **(11) REGOLA MIA PUBBLICATA E RITIRATA IN GIORNATA** β€” vedi il ritiro nel bullet dell'ondata 1: + `dealer_net_gamma` **non e' il GEX col segno invertito**, e' una **convenzione dichiarata** nel + sorgente che gira su questa stessa VPS. **REGOLA: prima di dichiarare che un campo di un dataset e' + sbagliato, aprire il codice che lo produce.** + πŸ“Œ **(12) ALTRE REGOLE NUOVE.** (a) **Una divergenza fra due fonti di prezzo si scompone in COMUNE e + IDIOSINCRATICA prima di giudicarla**: le due rispondono a domande diverse e la prima da' sempre la + risposta rassicurante. (b) **Due pannelli che iniziano a date diverse ribilanciano in giorni di + calendario diversi**: un confronto di venue non allineato misura la **fase**, non il venue (qui + βˆ’0,254 contro βˆ’0,006, fattore 42). (c) **Un null di permutazione puo' avere mediana diversa da + zero per ragioni strutturali** β€” scartare a caso il 40% dei trade danneggia perche' lo Sharpe va con + √(scommesse/anno) (misurato 0,883 contro 0,861 previsti): il null resta valido e **piu' severo**, ma + risponde a *"batte una selezione casuale?"*, non a *"batte il non filtrare?"*. (d) **Un + contro-fattuale costruito sui mid di un mercato largo non descrive un mercato stretto**: prendere il + mid a spread 50% e fingere di quotarlo al 2,6% fabbrica opportunita' che spariscono restringendo il + campione a cio' che e' gia' quotato stretto. (e) **Un `git add -A` durante un'ondata cattura il + lavoro IN CORSO degli agenti** (successo qui: un commit conteneva una versione intermedia e + sbagliata di uno script poi corretto). + πŸ“Œ **(13) E LA RISPOSTA ONESTA AL MANDATO, che al critico era stato chiesto di dare se vera: + NESSUNA misura di ricerca di questa giornata batte "versare".** L'effetto piu' grande misurato oggi + vale **€0,0113/giorno**; versare €500/mese vale **~1456Γ—** quello. **La verifica che costa meno di + tutte non e' ricerca: leggere il listino di due prop firm** β€” l'intero risultato di testa poggiava + su un assunto scritto in un **commento a riga 100**. *La ricerca ha smesso di essere il vincolo + binding il 26/07, e due ondate da 29 filoni lo hanno CONFERMATO invece che ribaltarlo.* + +- 🌊 **TERZA ONDATA 2026-08-22 notte (4 filoni) β€” DUE GATE D'ATTESA ELIMINATI, 2 difetti strutturali + trovati, 0 candidati.** Diario `docs/diary/2026-08-22-wave2-xs01-funded.md`; registro + `docs/research/RESULTS-0822.md` Β§30-33. **Libro, pesi, cron, config INVARIATI.** + βœ… **(1) `GATE PROP-01` gamba (a) β€” LISTINO: PASS 13/13.** La verifica a **€0** che il hook e il + critico avevano indicato come il vero vincolo. πŸ“Œ **Metodo: non leggere il sito della firm, + interrogare il VENUE su cui la firm esegue.** HyroTrader esegue su **Bybit**; + `api.bybit.com/v5/market/instruments-info?category=linear` (pubblico, tokenless, **833 strumenti**) + β†’ **13/13 gambe U13 `Trading`, `LinearPerpetual`, min notional $5**, leva 50-150x; **6/6** anche le + gambe non testabili. **Lo short e' STRUTTURALE su un perp**, non un permesso. + ⚠️ Non prova che la firm non abbia una **propria** lista ristretta: la sua pagina dichiara "700+ + pairs" su un venue che ne ha 833 (coerente, ma e' la *sua* dichiarazione) β†’ si chiude con una + domanda al supporto, non con un dataset. + βœ… **VALIDAZIONE del modello:** la firm dichiara **daily DD 4% Β· max loss 6%**, e + `r0725_hyro.py:48-49` usa gia' `EV_TARGET,EV_DD,EV_DL = 0.10,0.06,0.04` β†’ **le regole modellate dal + 25/07 sono quelle vere.** + 🚨 **CORREZIONE ECONOMICA: la quota d'ingresso NON e' un costo affondato** β€” e' un *"Refundable + Challenge Deposit, returned in full alongside your first funded payout"*. Il progetto l'ha sempre + modellata come spesa persa. **Resta vero che immobilizza il 91% del conto ($579 su $635), non che lo + brucia**: si perde solo nel ramo senza payout. **E' una struttura a OPZIONE, non una fee** β†’ l'EV del + biglietto va rifatto (gamba (c) del gate). *Qualita' delle fonti dichiarata: listino **primario e + misurato**; regole DD **primarie dichiarative**; rimborsabilita' **dichiarativa + secondarie + concordi**, non misurata.* + βœ… **(2) `GATE PROP-01` gamba (b) β€” chiusa OGGI invece che il 2026-10-31: PASS 23/23.** + Delta appaiato mediano **+0,1162** [p10-p90 +0,087/+0,152], **23/23 offset e 230/230 celle congiunte + positive** (soglia +0,05), su **griglia congiunta PIENA** 23 offset Γ— 10 fasi β€” non un campionamento + e non una somma di marginali. Riproduzione **7/7 esatta**, bit-exact **3/3 a 0,0**. + πŸ“Œ **La data non aspettava un dato: aspettava che qualcuno misurasse il COSTO** (0,6 s per offset; + i "~9 min" erano di una computazione che la misura non usa). **Secondo gate d'attesa eliminato in una + sera, per la stessa ragione del primo.** + 🚨 **Ma il numero operativo scende ANCORA: P(β‰₯50 €/g) da €600 in 36 mesi = 4,4% [2,5–8,2%], P(zero) + 35,3%** (l'offset canonico era al **91Β° pctl**; all'ancora canonica la macchina ridice 7,7% β†’ e' + **fortuna d'ancora, non disaccordo**). **Traiettoria del numero in una giornata: 42% β†’ 7,8% β†’ 4,4%.** + πŸ“Œ **E la distorsione dichiarata DUE VOLTE e mai rimossa β€” *"l'ottimo si appoggia a SKH01, lo sleeve + con la fortuna d'ancora piu' grande"* β€” e' MISURATA E REFUTATA nella forma forte:** a **38/25/38** + SKH01 sta **allo stesso peso del LIVE**, la sua ancora si cancella nella differenza appaiata, e li' + il delta e' **il piu' alto dei tre (+0,120)** con la banda **piu' stretta**. **L'ampiezza della banda + scala col differenziale di peso su SKH01** β†’ **cio' che paga e' XS01, non un sovrappeso dello sleeve + fragile.** + ⚠️ **Due avvertenze:** (i) **il gate e' su una DIFFERENZA, il numero operativo e' un LIVELLO** β€” + nella prima l'ancora si cancella in parte, nel secondo per niente: passano insieme e si muovono + diversamente (βˆ’33% contro βˆ’44%). ***Un gate superato non e' un numero confermato.*** (ii) il + surrogato del 22/08 era finito vicino al vero **per la cancellazione di due asimmetrie opposte**: + **il numero era giusto, il metodo no.** **Smentitore residuo fuori perimetro:** l'ancora giornaliera + di **TP01** (24 ore, canonica 00:00 UTC = la piu' fortunata delle 24) non e' toccata da questa misura. + πŸ“Œ **(3) GATE-RECON β€” il difetto `advance()` ribalta UN gate su tre, e in quello due criteri su tre.** + Audit di **sola lettura provata** (md5+mtime di **ogni** `data/paper_*` invariati). + **`STATARB 27/09 β€” riparata: RITIRO Β· attuale: CANDIDATO AL DEPLOY`** (Sharpe **+1,96 β†’ βˆ’2,02**, + maxDD 2,24% β†’ 15,19%); XSR01 e DVOLSPREAD danno **lo stesso verdetto** in entrambe le lenti. + βœ… **La premessa "rigenerare non perde la finestra" e' ora CONFERMATA PER VIA DIRETTA:** + `data/_feed_backup/*.prebuild.bak` conserva il vintage pre-riscrittura β†’ **0 barre chiuse cambiate su + 1.761.259**; l'unica riga che cambia per file e' **l'ultima del vintage, che era aperta** = *il + difetto stesso, in chiaro*. ⚠️ Limite: `hl_*_1d` e `dvol_*` **non hanno vintage su disco** β†’ per + XSR01/DVOLSPREAD l'evidenza resta **indiretta**. + 🚨 **Il veto d'integrita' di DVOLSPREAD NON scatta: 28 attive/28 = 100%.** Una serie che registra + **2 minuti di mercato al giorno** passa al 100% un veto progettato apposta per non far decidere su + dati mancanti β€” **non e' taratura**: il veto conta *se il DVOL c'era*, non *quanto dura la barra su + cui c'era* (**assi ortogonali**); la stessa soglia 0,80 sulla quota di barre **chiuse** leggerebbe + **0%**. **Variante nuova: una barra presente non e' una giornata presente.** + 🚨 **Il caso peggiore NON e' quello coi numeri piu' assurdi:** XSR01 sbaglia di **Γ—6,6** e + DVOLSPREAD di Γ—3,0 **senza cambiare verdetto**; **STATARB ha |Sharpe| quasi identico (1,96 contro + 2,02) e cambia SEGNO** β€” ed e' il gate piu' vicino. Un controllo che cercasse *"numeri troppo belli"* + **lo lascerebbe passare**; `implausible_sharpe`, girato per la prima volta sulle serie forward, dice + **"ok" in entrambe le lenti** (e chiede nβ‰₯30, quindi 2 serie su 3 **non sono giudicabili**). + **La firma da cercare e' la VOL forward molto sotto quella del backtest** (7,88% contro 31,42%). + πŸ“Œ **QUANDO riparare, con un numero:** con **rigenerazione zero barre perse a qualunque data**; senza, + riparare **oggi** lascia **36/90** barre vere a STATARB (62/90 XSR01, 63/91 DVOLSPREAD), **alla + vigilia 1** β€” e la riparazione parziale **non contamina la finestra, la ACCORCIA** (una barra troncata + porta ~1/370 della varianza *e* del rendimento di una vera, quindi non pesa). ⚠️ La diagnostica + pre-registrata del 25/07 (statica sempre-short) **non e' affetta** perche' si ricalcola dal pannello + β†’ al gate si confronterebbe **un benchmark giusto con un numero di strategia sbagliato**, e il + margine sembrerebbe **+5,83 invece di +1,87**. + 🚨 **(4) WORST-DAY β€” la riserva sulla leva e' mal tarata di 2-4 ORDINI DI GRANDEZZA, ma il knob di + scala NON ESISTE.** 🚨 **AGGIORNATO 23/08: il 1,50x e' BOCCIATO** (Β§53: peggior giorno + strutturale **21,48% > 20%**) e il **k massimo difendibile e' 1,40** β€” di questa riga sopravvive + **solo la colonna 1,25x**. Il gradino di leva vale **14,7a β†’ 12,9a a 1,25x** e **β†’ 11,6a a 1,50x** al + capitale-rendita (**+€164/mese** e **+€282/mese** di versamenti equivalenti 🚨 **il primo e' + +€144 misurato direttamente β€” vedi il ritocco del 23/08 sotto**) ed era bloccato da un + parametro **scelto a mano** ("un giorno + a βˆ’10% ogni anno"). **Il peggior giorno ha due risposte:** **βˆ’3,94% in chiusura** (2020-03-13) e + **βˆ’7,33% al minimo** intra-giorno (rapporto mediano **1,89Γ—**). Coda GPD: **βˆ’4,55% a 1/10.000 + giorni** [βˆ’6,34/βˆ’3,08] ⚠️ **ma Hill dice βˆ’7,34% (ΞΎ 0,34 contro 0,09): i due stimatori NON concordano + β†’ si cita la banda.** Conto **aritmetico** (cap + vol-target + gap-through di SKH01): peggior giorno + **possibile** a k=1,00x **βˆ’14,32%**. πŸ“Œ **Il βˆ’10% sta al 70% del massimo strutturale: la TAGLIA e' + plausibile, la FREQUENZA no** (1 ogni 67-6.075 anni contro "ogni anno"). + πŸ“Œ **E la forma della coda non e' la leva: muove k* di 1-2 unita', il DRIFT di 6,7.** Anche prendendo + lo stress **alla lettera**, k* 2,95x β†’ mezzo-Kelly **1,48x**: *lo stress bocciava il gradino DOPPIO, + non quello singolo*. C1-C5 passano tutti. + ⚠️ **Il libro NON e' immune ai crash, e' immune ai crash GROSSI:** sui 160 giorni di crash perde in + media βˆ’0,505%, e l'esposizione di TP01 scende **monotona** col crollo (0,096 β†’ **0,042** sotto il + βˆ’20%) *perche' li' il trend si e' gia' girato*. **Il suo peggior giorno non e' un crash: e' uno + SHORT SQUEEZE** (SKH01 short con stop 2% che gappa a **βˆ’15,77%** di sleeve mentre TP01 e' flat; il + secondo peggiore fa βˆ’6,67% β†’ **la coda di SKH01 e' un giorno solo**). + 🚨 **IL RISULTATO CHE DECIDE, e nessuno lo cercava: il cap di `config/live.json` e' un CLAMP, non un + MOLTIPLICATORE.** Morde su **3 osservazioni-asset su 5.650 (0,053%)**; alzarlo da 0,50 a 0,625 + lascerebbe il libro a k=1,00x in **5.647 giorni-asset su 5.650**. βœ… **Verificato:** le chiavi sono + `max_notional_per_asset_usd/_frac`, `min_order_usd`, `disaster_sl_pct`, `max_data_age_days`, + `skh_feed_max_age_min` β€” **non esiste una chiave di scala**; servirebbe toccare `WEIGHT`/`W_TP01`/ + `W_SKH` in `src/live/book.py`, cioe' **codice** su un percorso con soldi veri. + **β‡’ La regola *"ogni cambio di SCALA passa dal cap di config, non da `target_vol`"* NON E' + IMPLEMENTABILE COME SCRITTA.** *Due mesi di discussione su un knob inesistente.* + **Il gradino a 1,25x e' AUTORIZZABILE A CONDIZIONE DI** costruire prima una **chiave di scala + esplicita** in config **e** rifare `r0726_fee_sensitivity`, la cui conclusione *"liquidation fee + irrilevante"* **vale solo a nozionale lordo ≀1x** (ed era gia' legata a un test di guardia sul cap). + ⚠️ **Nota dovuta:** credendo **entrambe** le riserve insieme (drift βˆ’2SE **e** βˆ’10% annuo) il drift + atteso e' **βˆ’0,61%/anno** β†’ *non e' il gradino a essere sbagliato, e' il libro; chi usa quella + combinazione sta chiedendo di spegnere anche il conto di oggi.* + 🚨 **(5) IL FUNDING DEI PERPETUAL NON E' MODELLATO IN NESSUN BACKTEST β€” e tocca ogni numero del + progetto.** βœ… **Verificato:** **zero occorrenze** di `funding` in `src/backtest/harness.py`, + `src/strategies/trend_portfolio.py`, `src/portfolio/{sleeves,portfolio}.py` **e in tutto + `src/live/`**. **Il progetto ha prezzato fee, slippage, min-order, pavimento IB, haircut, fortuna + d'ancora, degrado d'esecuzione, look-ahead, split, rischio di venue e fisco β€” e mai il costo di + TENERE APERTA una posizione su un perpetual.** + βœ… **MISURATO la notte stessa** (`r0822d_funding.py`, 1.212 righe, replica bit-exact 5/5, Β§35 del + registro): **il libro paga βˆ’2,16%/anno di DRIFT** (2019-2026) Β· **βˆ’1,39%** sulla sola finestra dello + strumento vero (β‰₯2022-03) Β· **βˆ’0,55%** sul 2025+. **Il costo si cita con la finestra**, e il 2022 e + il 2026 costano ~0 **perche' il libro era FLAT**, non perche' il funding fosse sparito. + 🚨 **Il meccanismo, ed e' trasferibile a ogni futuro candidato su perpetual: esposizione e funding + sono POSITIVAMENTE CORRELATI** β€” il tasso pagato davvero e' **1,86-2,55Γ—** l'incondizionato per TP01 + e **2,41-3,44Γ—** per SKH01, quindi **il prodotto ingenuo "esposizione media Γ— tasso medio" + sottostima di ~2Γ—**. + ❌ **Ipotesi da cui il filone era partito, FALSIFICATA: "SKH01 paga meno perche' sta a mercato il 12% + del tempo"** β†’ paga **2,24%/anno contro 2,02%** di TP01, perche' quando e' a mercato sta a nozionale + **1,0Γ—** (TP01 vol-targeted sta a **0,14Γ—**) **ed entra sui breakout, cioe' quando il funding e' + caro**. **Il peso 75/25 NON si muove** (delta negativo a 23/23 offset per ogni w). + ⚠️ **Il proxy Hyperliquid era 2,1-2,2Γ— il venue vero** (14,8% contro 6,8-7,1% sulle stesse ore): + **due errori opposti β€” tasso troppo alto, esposizione condizionale troppo bassa β€” che quasi si + compensavano.** *Compensarsi non e' misurare.* + 🚨 **NUMERI PUBBLICATI CHE CAMBIANO:** rendita perpetua **10,73% β†’ 9,14%**; **muro $276,6k β†’ $325,0k + (+17,5%)**; traiettoria da $600 a €250/mese **16,4a P(20a) 90% β†’ 18,7a P(20a) 64%**; **soffitto + direzionale ~1,31 β†’ ~1,15**. **NON cambiano:** peso 75/25, gradino di leva (k* 13,7β†’12,3), **maxDD + (+0,3pp)**, ordine delle leve del piano. + πŸ“Œ **E' una tassa sul DRIFT, non sulla coda** β€” per questo colpisce i **muri** molto piu' dello + Sharpe, ed e' lo stesso ordine di grandezza della correzione d'ancora Γ—0,89. + ⚠️ **QUESTA CORREZIONE E QUELLA DEL FISCO SI SOMMANO, E NESSUNA TABELLA PUBBLICATA LE CONTIENE + ENTRAMBE.** La riga "€250/mese" vale P(20a) **92% al lordo** Β· **52% col fisco** (07/08) Β· **64% col + funding** (22/08): **le due lenti sono state applicate separatamente allo stesso numero lordo**, e il + risultato congiunto sara' **peggiore del 52%**. **La tabella con entrambe non esiste ancora.** + βœ… **Validato contro il conto vero per ELIMINAZIONE, non per grandezza:** modello vs `realized_pnl` + di sessione **ETH βˆ’0,2% / BTC βˆ’7,7%** (le finestre sbagliate sbagliano di 2-4Γ—), e l'identificazione + regge perche' nella sessione ci sono stati **0 ordini** β†’ il P&L di trading e' esattamente zero, + quindi quel campo non puo' essere P&L di trading. Convenzione `interest_8h` **verificata sui dati** + (mediana |dif| 2,2e-08 contro 1,1e-06). Funding realmente pagato dall'arming: **$0,22**. + ⚠️ **BUG in famiglia gia' nota, catturato da un controllo positivo e non a occhio:** + `DatetimeIndex.astype("int64")` su tre serie con risoluzioni **diverse** (`[ms]`/`[us]`/`[s]`) dava + **0,000%/anno su SKH01** e **+1,229%/GIORNO su TP01** β€” **nessuna eccezione, nessun NaN, due numeri + plausibili in due punti diversi**. Stessa famiglia del `DatetimeIndex.view("int64")` codificato il + 01/07. **Su un perpetuale lineare, inoltre, `diff(nozionale) < 0` NON e' una vendita: il nozionale + scende da solo quando scende il mark.** + βœ… **SEGUITO 2026-08-22 notte β€” LA CHIAVE E' SPECIFICATA (`docs/research/SPEC-scale-key.md` + + prototipo isolato `r0822e_scale_proto.py`, zero scritture, isolamento verificato a runtime), con un + NODO IRRISOLTO.** Registro Β§38. Replica a k=1,00 **6/6** contro WORST-DAY. + ⚠️ **CORREZIONE A UN NUMERO CHE AVEVO SCRITTO IO:** *"1,25-1,50x vale 14,7 β†’ 11,6 anni, ~€300/mese"* + **fondeva due gradini** β€” a **1,25x** l'equivalente e' **+€164/mese**, il €282 e' del **1,50x** + 🚨 **che il 23/08 e' stato BOCCIATO: la meta' cara della frase non esiste piu'.** + 🚨 **RITOCCATO 2026-08-23 (Β§53), e il ritocco riguarda la CONVENZIONE piu' che il numero:** + il "+1,8 anni / +€164" e' la lettura a **muro CONGELATO** (si de-leva al traguardo) e Β§33 **non + dichiarava quale delle due letture stesse usando**. A **muro MOBILE** lo stesso gradino vale + **+2,98 anni**; con il **funding dentro**, +2,10a (congelato) / **+3,49a** (mobile). + **La differenza fra le due letture e' 1,2 anni β€” piu' grande di quasi tutti gli effetti che + questo progetto misura** β‡’ da qui in avanti un guadagno in anni si cita con la sua convenzione. + Il versamento equivalente **misurato direttamente** e' **+€144/mese**, non €164: la differenza + *e'* l'ipotesi di linearita' dell'interpolazione. + πŸ“Œ **E il funding va nel verso opposto all'atteso: non riduce il gradino, lo AUMENTA** (peggiora + il caso base) β€” **ma il livello peggiora su tutta la colonna**: k=1,25 col funding (13,78a) resta + **peggio** di k=1,00 senza (14,81a). *Il gradino non ripaga il funding, lo attenua.* + 🚨 **Tre risultati che il mandato non prevedeva:** + **(i)** `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` **NON si rompe, ed e' PEGGIO che + rompersi**: misura `frac Β· n_asset` mentre la grandezza vera diventerebbe `frac Β· n_asset Β· scala` + β†’ **continua a passare e smette di controllare**. **4Βͺ occorrenza** della forma *"un controllo + puntato su una configurazione diversa da quella che gira"*. **Va cancellato con nota, non + aggiustato.** + **(ii) La guardia che morde per prima e' il DISASTER-SL, non il peggior giorno:** bound strutturale + k ≀ 3,49x, **disaster-SL k ≀ 1,67x = 2,1Γ— piu' stringente** β†’ **C1 di WORST-DAY guardava la + grandezza sbagliata**. Il tetto giusto e' l'invariante `n_asset Β· frac Β· scala Β· disaster_sl_pct + ≀ 0,50`, che lega **tre** chiavi. + **(iii) Il tetto va sul PRODOTTO, non sulla chiave:** `frac=0,625` + `scala=1,25` = leva **1,562x** + e **passerebbe** un tetto *"scala ≀ 1,25"* β€” e WORST-DAY aveva appena pubblicato che alzare `frac` + da solo *"non produce k=1,25"*: **combinato con la scala lo produce eccome.** + πŸ“Œ **Gate proposto `GATE SCALA-01`** (9 condizioni, fra cui **tetto sul prodotto** e **A6 + anti-recency**: `k_ammesso` calcolato **togliendo gli ultimi 90 giorni** dev'essere β‰₯ k richiesto), + perche' **la scala e' invisibile a ogni gate esistente** β€” Sharpe **1,5051 identico a tutte le celle + di k**: `deflated_sharpe` e `marginal_vs_tp01` **non falliscono, non vedono la variabile**. + βœ… `weights_tilt_null` invece **passa con potenza**: peso implicito TP01 **0,750000 a ogni k**, + `max|target(k) βˆ’ kΒ·target(1)| = 0`, e il **controllo positivo** (scala sulla sola gamba TP01 = cio' + che farebbe `target_vol`) lo porta a **0,789** e viene smascherato. + βœ… **IL NODO E' SCIOLTO (2026-08-23, `r0823_sl_anchor.py`, Β§40) β€” E LA DICHIARAZIONE DI + NON-MISURABILITA' ERA FALSA: IL LOG BASTAVA.** *(3Βͺ conferma della regola del 26/07: un blocco + dichiarato e' un'IPOTESI, non un fatto.)* + πŸ“Œ **L'attesa e' stata scritta PRIMA di guardare, derivata dal codice, e ha predetto le soglie + esatte:** rotolante e **simmetrico**, innesco **+5,263%** / **βˆ’4,762%** (il fattore 0,70 si semplifica + e la condizione diventa una condizione **sul mark**, non sullo stop). + **Il fatto e' confermato:** su 1443 giri i 15 `placed` sono **5 aperture + 7 ribilanci + 3 A POSIZIONE + FERMA** (21/08, `HOLD (a target)`, **0 ordini**, trigger spostato di **+5,74/+5,81/+5,38%** = appena + sopra la soglia predetta); e sul venue **l'ordine di stop e' 2,5 giorni piu' giovane della posizione + che protegge**. + 🚨 **MA IL TIMORE E' REFUTATO, e va nel verso OPPOSTO:** uno stop che segue il prezzo **si allontana + dal mercato, quindi scatta MENO**. Alla cadenza che gira (**1h**) gli episodi su storia reale sono + **0-1 contro 1 del pavimento**, mai piu' di 1 in 30 giorni; e **nell'outage β€” l'unico scenario per cui + il bracket esiste β€” il ri-ancoraggio e' SPENTO per costruzione** (nessun giro, nessun ri-piazzamento) + β‡’ **un episodio solo**. **Il 37,5% resta il costo di un episodio singolo, il margine 1,33Γ— e' quello + vero, la gamba di WORST-DAY REGGE.** + 🚨 **Due cose indipendenti da k, da sapere comunque:** (i) **il nome inganna** β€” *"disaster-SL βˆ’30%"* + suggerisce un massimo di perdita per trade che **non esiste**: su cicli a ingresso proprio il rotolante + lascia passare perdite oltre βˆ’30% **senza scattare** nel **0,03%/0,96-2,84%/7,22-13,33%** dei cicli + (24h/7g/30g) e il peggior DD dall'ingresso e' **βˆ’60,6% BTC / βˆ’61,5% ETH**; (ii) 🚨 **TRAPPOLA VIVA: + l'accumulo e' funzione della CADENZA DEL CRON, e il docstring di `book_execute.py` prescrive "ogni + ~230 minuti" mentre il cron gira OGNI ORA** β€” chi "correggesse" la cadenza verso il docstring + sposterebbe il libro sulla riga **4h, dove BTC RADDOPPIA gli scatti** (2 contro 1; a 24h ETH arriva a + 5). **Il docstring e' sbagliato e la configurazione che gira e' quella giusta.** + ⚠️ **Limite dichiarato: 0 ri-ancoraggi verso il BASSO osservati su 3** β€” 2 mesi di vita del libro in + **un solo regime** (BTC ~$66,5k β†’ ~$78,6k), mai un calo del mark >4,76% con posizione aperta. **La + simmetria e' un fatto di CODICE** (un solo `abs()`, esercitato in entrambi i versi), **non un fatto + OSSERVATO**: la prima osservazione utile arriva al primo ribasso >5% con posizione aperta. + **Seconda soglia dichiarata:** il fallback dell'equity oggi scatta **0 volte su 1.442 giri**; sopra + il **2% dei giri in 90 giorni** il libro girerebbe a leva **mista** e `g(k)` smetterebbe di + descriverlo. + **REGOLE NUOVE:** (a) **una data in un gate va giustificata col COSTO della misura, non con la sua + difficolta' percepita** β€” due gate d'attesa eliminati in una sera perche' nessuno aveva contato i + secondi; (b) **una verifica esterna a €0 va fatta PRIMA di qualunque misura che ne dipenda**, e si + fa **sul venue, non sul sito** di chi la dichiara; (c) **un gate superato non e' un numero + confermato**: differenze e livelli ereditano la fortuna d'ancora in modo diverso; (d) **un veto + d'integrita' sorveglia l'asse su cui e' stato scritto** β€” contare *se* il dato c'era non dice + *quanto* del giorno copriva; (e) **una regola operativa va provata contro il codice che dovrebbe + eseguirla**, o si discute per mesi di un knob che non esiste; (f) **quando due stimatori di coda non + concordano si cita la banda**, non quello che conviene. + +- 🌊 **QUINTA ONDATA 2026-08-23 (12 filoni + critico di chiusura, `research/wave-0822`) β€” 0 + candidati su **57 filoni cumulativi**, 1 candidato scoperto GIA' IN PRODUZIONE SENZA GATE, il + gradino di leva delimitato, e un critico che ha falsificato **due sue attese e tre miei numeri**.** + Diario `docs/diary/2026-08-23-wave-chiusura-critico.md`; registro `docs/research/RESULTS-0822.md` + Β§46-57; 17 script `scripts/research/r0823_*.py` (15.443 righe, compilano tutti, ognuno gira da + solo). **Libro, pesi, cron, config INVARIATI.** + 🚨 **(1) IL RISULTATO PIU' IMPORTANTE NON E' UNA STRATEGIA NUOVA: E' CHE LA PIU' FORTE GIA' + MISURATA GIRAVA SENZA GATE.** Β§50 cercava il miglior TERZO sleeve eseguibile a $635: sette + candidati, **sette muri diversi, e nessuno dei sette e' una soglia di capitale**. Il piu' forte β€” + **PREVDAY** β€” sta in monitor **da giugno senza gate pre-registrato e senza deflated-Sharpe**. + Β§54 gliene ha scritto uno (`GATE PREVDAY-01`, **8/10 condizioni oggi**) e le due che mancano sono + **la stessa cosa**: 🚨 **la cella che GIRA non e' quella che la selezione onesta sceglie** (e la + cella viva **fallisce il DSR**, 0,905), e la selezione onesta compra il libro **LONG-FLAT** β€” + cioe' butta via proprio la gamba per cui PREVDAY fu promosso. βœ… La serie e' **intatta, 1512 barre + su 1512** (e' il monitor sano, quello che il difetto `advance()` non tocca) ⚠️ **ma la finestra e' + 0,172 anni β†’ MDE 4,72 di Sharpe: il +2,11 forward non e' distinguibile da zero su 26 ENTRY. + Qualunque cosa il gate decida, non puo' appoggiarsi al numero forward.** + 🚨 **(2) IL GRADINO DI LEVA E' DELIMITATO: 1,25x REGGE, 1,50x E' BOCCIATO, k max difendibile + 1,40.** Β§53 lo attacca 6 volte col funding dentro: **5/6 falliscono**; il 1,50x cade sul peggior + giorno strutturale (**21,48% > 20%**). L'incrinatura e' il ramo **`eq_fallback`**, dove **la scala + non apre il buco: lo MOLTIPLICA**. βœ… Aritmetica dei tetti verificata sul config che GIRA, 3/3 al + centesimo: disaster-SL β‡’ **k ≀ 1,67** Β· peggior giorno β‡’ k ≀ 3,49 Β· scettico β‡’ **k ≀ 1,40**. + ❌ **(3) TRE MECCANISMI DI PROTEZIONE REFUTATI, E DUE PER LA PREMESSA.** **TAIL-HEDGE (Β§46):** la + premessa cade **prima del prezzo** β€” il maxDD **SALE in 162/162 celle**, a ogni lente di f (f=1,00 + regalato compreso), perche' il beta del libro al sottostante e' **+0,076**: *non si assicura un + libro che nei crash e' gia' quasi piatto*. **TP01-TWIN (Β§49):** un ensemble di definizioni di + trend protegge **PEGGIO** β€” 0/24 ancore nei 4 anni recenti, **3,2x peggio nel 2022**, βˆ’0,127 di + Sharpe hold-out; e i meccanismi **non erano ridondanti** (corr di posizione 0,61): la ridondanza + non era il problema. **ANCHOR-ENSEMBLE (Β§47):** eseguibile, ma su TP01 il drift **non puo'** + cambiare per **algebra** (`lordo_ens == media(lordi)`, residuo 1,4e-17); il segnale c'e' su + **SKH01** (+0,312 di Sharpe mediano, maxDD 23,3%β†’16,6%) ma dentro il libro **non si distingue da + zero**. + πŸ“Œ **(4) IL LEAD DELLO SPOT MUORE DOVE SERVIREBBE.** Β§52 lo attacca sei volte: **5/6 falliscono**, + e cio' che morde non e' nei numeri β€” e' **nella taglia a cui vengono spesi**. **Regge a $600, si + annulla a $272k**: sparisce **esattamente al muro che pretende di spostare.** + βœ… **(5) IL COSTO D'ESECUZIONE ENDOGENO NON MORDE (Β§55):** muro **+0,6%** (dentro la risoluzione + MC), saturazione a **$9,5-10M = 30x sopra**, e il fatto trovato per strada β€” **alla taglia di oggi + il costo mancante ha SEGNO NEGATIVO**: il drift a $635 e' **13 bps/anno piu' basso** che a $5.000 + per il pavimento `min_order` $5, **~2x** quello che lo slippage costa da $313k a $1M, in direzione + opposta. *Il conto piu' preciso non muove il muro: sposta il segno dell'errore, e a questa taglia + lo sposta a favore.* + ❌ **(6) DUE CHIUSURE DI FAMIGLIA.** **VOLVOL (Β§48):** la volatilita' del DVOL e' davvero una + variabile **NUOVA** (ortogonalita' PASS, max |corr| 0,396) ma **guarda INDIETRO** β€” picco a lag + **βˆ’5/βˆ’10**, parziale con la vol futura **NEGATIVA**: *un secondo momento non e' automaticamente + informazione anticipata*. ⚠️ Ma il filone lo **chiude** su una branca dove dichiara esso stesso + **un fattore 44 sotto l'MDE**: e' un'etichetta piu' forte della misura. + **XS-AMPIEZZA (Β§56):** su 11 gambe **la caduta NON era ampiezza β€” solo il 14%**: e' **selettivita'** + (recuperabile, ma sotto l'MDE) **piu' un IC che su quelle gambe vale ZERO**. 🚨 E **`k=5` non e' + invariante di scala**: il numero di gambe per lato di XS01 e' tarato su 19 gambe, non e' una + proprieta' del meccanismo. + πŸ“Œ **(7) SOLO 2 FONTI SU 10 MERITANO DI ESSERE RACCOLTE (Β§51):** la catena **USDC** (**+18% di + chiamate, non +90%**) e una **campagna a termine** di book depth. Il criterio di + **ricostruibilita'** ne uccide 4 da solo β€” *se lo puoi ricostruire domani, non raccoglierlo oggi*. + ⚠️ **(8) ERRORI MIEI, corretti dentro l'ondata** (tutti numeri gia' pubblicati, due in questo + file): (a) **"la convinzione di TP01 sta spesso a 1/3 o 2/3"** β€” **falso**, `tsmom_blend` media + tre `np.sign()` β†’ direzione in {0, 1/3, 1} dopo il clip: **il bucket 2/3 NON ESISTE**, la famiglia + ha **UN** grado di liberta'; trovato **indipendentemente da due agenti**. (b) **catena USDC "+90% + di chiamate"** β†’ **+18%** (i 587 erano il conteggio grezzo, senza il filtro OIβ‰₯100 del + collettore; al venue: **113 USDC contro 649 inverse**). (c) **"le opzioni BTC sono fuori a $3.000 + per il lotto"** β†’ quello e' il **collaterale per VENDERE**; comprare costa il **premio** + (`min_trade_amount` 0,01) e **il muro vero e' il TICK da 5,00 USDC** (una put che vale 0,82 non e' + quotabile sotto 5 = pavimento di prezzo **6x**). (d) **"il gradino vale +1,8a / +€164"** β€” era la + lettura a **muro FERMO**, convenzione **mai dichiarata**; a muro mobile fa **+2,98a**, e le due + distano **1,2 anni**. (e) **"la coda muove k\* di 1-2 unita'"** β†’ **4,6**. + 🚨 **(9) IL CRITICO DI CHIUSURA (Β§57) β€” repliche 6/6, ma tre numeri di testa vanno riscritti.** + βœ… Ha **riprodotto con macchineria propria** i quattro muri **al dollaro**, il funding + (**βˆ’2,1597%/anno**, e da implementazione indipendente TP01 2,138% con rapporto condizionale + 1,93x/2,58x), il dShFULL di Β§42 (**βˆ’0,4502**) e βœ… **che il muro E' esattamente + `prelievo/perpetua`** su 4 lenti su 4 β€” *il nucleo quantitativo dell'ondata regge*. Le tre cose + che cambiano β€” **la banda del muro**, **Β«N/N ancoreΒ» = ~2 osservazioni** e **il 1,50x bocciato** β€” + sono gia' scritte nei bullet a cui appartengono, sopra. In piu': **tre baseline diverse sotto lo + stesso nome Β«libro 75/25Β»** (`hourly` 1,683/11,22% = la lente di **ogni muro e traiettoria** Β· + `canonical` 1,800/9,56% Β· mediana d'ancora 1,626/10,4%) β€” **spread 0,12 di Sharpe e 1,7pp di + maxDD, piu' grande di quasi tutti gli effetti che l'ondata misura**, quindi *un Ξ” accostato al + baseline sbagliato cambia piu' del Ξ”*: **ogni numero va scritto con la sua lente attaccata**. + ⚠️ E il **pilastro del canale funded** (Β§21, XS01 fuori campione) ha effetto **+1,12 contro un MDE + di 1,13**: sta *esattamente* al proprio limite di rilevabilita', e viene propagato fino a + P(β‰₯50 €/g) come se fosse noto a due cifre. *(Equita': Β§21 gira anche un null di permutazione a fee + zero, p=0,013, che ha piu' potenza del t-stat β€” il problema non e' Β«nessuna evidenzaΒ», e' la + precisione attribuita al livello.)* + βœ… **A credito dell'ondata: quattro auto-correzioni in 24 ore** (Β§45 falsificato da Β§52; Β§39 + falsificato lo stesso giorno; la regola Β«overlay non causaleΒ» ritirata; Β«`dealer_net_gamma` e' il + GEX invertitoΒ» ritirata leggendo il sorgente). **E' il dato piu' sano dell'ondata.** + 🚨 **(10) LA RISPOSTA AL MANDATO, in una sola unita' (€/giorno a $635):** gradino di leva 1,25x + **+0,046** Β· lead SPOT +0,017 Β· PREVDAY al 15% de-luckato +0,014 Β· BIN-FREQ +0,011 Β· MAKER +0,005 + Β· **funding (costo scoperto) βˆ’0,023**. **Somma di TUTTI i lead positivi, se fossero autorizzati e + additivi (non lo sono, e nessuno e' eseguibile oggi): +0,036 €/giorno.** Sullo stesso motore e + sugli **stessi percorsi**: **€250 β†’ €500 al mese porta P(20a) dal 14% all'85% e la mediana da 23,4 + a 17,3 anni.** + **VERDETTO DELL'ONDATA: `NIENTE CHE AVVICINI I 50 €/GIORNO.`** Ha prodotto (i) un **costo gia' in + essere** che porta P(20a) a €250/mese dal 49% al **14%** (il funding), (ii) **quattro monitor + rotti** che avrebbero fatto decidere un gate al contrario, (iii) un **punto singolo di guasto** + nella rete di sicurezza (il trasporto degli allarmi), (iv) **tre lead reali tutti bloccati da + qualcosa che non e' la ricerca**. **Il suo valore e' DIFENSIVO ed e' reale: ha impedito decisioni + sbagliate.** ❌ E **nulla di sepolto sotto uno Β«SCARTATOΒ» merita di essere riesumato**: il + candidato piu' forte non promosso vale **+0,014 €/g** de-luckato e gira la cella sbagliata. + πŸ“Œ **La riga che chiude cinque ondate e 57 filoni: la ricerca ha smesso di essere il vincolo + binding il 26/07, e ora cinque ondate lo hanno CONFERMATO invece che ribaltarlo. I vincoli binding + restano due: il capitale che entra e il conto che non sparisce.** +- 🌊 **SESTA ONDATA 2026-08-23 (12 filoni, Β§58-69, `research/wave-0822`) β€” 0 candidati su **69 + cumulativi**, 1 finding che riguarda l'OBIETTIVO stesso, 2 appoggi tolti a decisioni gia' prese.** + Registro `docs/research/RESULTS-0822.md` Β§58-69 + sezione di sintesi in fondo; 12 script + `scripts/research/r0823b_*.py`. **Libro, pesi, cron, config INVARIATI.** + ⚠️ **PROVENIENZA DIVERSA DA OGNI ALTRA ONDATA, e va detto per primo:** la sessione si e' chiusa + dopo che gli agenti avevano scritto gli script e **prima** che i verdetti fossero consolidati β€” + le 12 righe restarono `_in corso_` e **i messaggi finali degli agenti sono persi**. I verdetti + pubblicati **non vengono da quei messaggi: vengono da una RIESECUZIONE dei 12 script** (07:38-08:06 + UTC, sequenziale; log in `logs/r0823b/`, che e' **gitignored** β‡’ il registro e' l'unica copia + versionata). πŸ“Œ **La regola del 26/07 β€” *un numero pubblicato deve avere uno script committato che + lo riproduce* β€” e' cio' che ha impedito la perdita dell'ondata.** Estensione: **un'ondata + interrotta si recupera solo se ogni script calcola il proprio verdetto a RUNTIME**, non se il + verdetto vive nel messaggio dell'agente. + 🚨 **(1) Β§58 β€” L'OBIETTIVO DEL PROGETTO HA DUE DEFINIZIONI OPERATIVE IN USO, E DANNO 33,9% CONTRO + 0,33% SULLA STESSA DOMANDA.** A **FLUSSO** (quanto esce al mese: la lente del canale funded) il + massimo a 36 mesi e' **33,9%**; a **RICCHEZZA SOSTENIBILE** (la lente dei muri e del piano) e' + **0,33%**. **Per cinque ondate i due corpi di lavoro sono stati confrontati come se parlassero + della stessa cosa.** E l'ottimo di `P(>=50/g)` **non e' l'ottimo di J**: 37,3% contro 13,9% alla + leva argmax-J. Il libro di oggi a 36 mesi: mediana **0 EUR/g** di flusso (non preleva), capitale + mediano $22.467 = **3,59 EUR/g** di rendita. **REGOLA: un obiettivo si dichiara nella sua + definizione operativa PRIMA di ottimizzarlo** β€” qui due definizioni entrambe difendibili + differiscono di **cento volte**. + πŸ“Œ **(2) Β§61 β€” la politica di ri-acquisto dei biglietti era CABLATA e mai dichiarata come scelta** + (compra subito, il piu' grande, in tutti e 6 gli slot, con qualunque soldo): e' **la piu' + aggressiva della famiglia**, e **da li' escono TUTTI i numeri del canale funded**. Mappata la + famiglia (7 tempi x 2 taglie): **il massimo di famiglia E' il cablato** (4,0%), spread 4,05pp, + prezzo della prudenza 4,05pp di bersaglio per 2,05pp di rovina; Β«solo dai payoutΒ» **non vincola + nulla** a $654 (dopo il primo biglietto restano $75, sotto il piu' economico). **La riga che il + filone stampa da solo: ottimizzare la politica vale +0,00 EUR/g, versare EUR 500/mese ne vale + +15,75.** + 🚨 **(3) Β§67 β€” IL MURO DI TAGLIA NON NASCONDEVA NIENTE, e il perche' e' metodologico:** a $100.000 + nessun archiviato passa, **non perche' sia stato ricontrollato ma perche' i gate pubblicati erano + GIA' calcolati sulla serie MODELLATA, che e' il limite a capitale infinito**. Censimento 14 voci, + **12 con un rifiuto ANCHE sull'edge**; delle 2 in cui la taglia era l'unico motivo nessuna e' una + strategia (XS01, GTAA01). Muri di taglia caduti **5 su 14, tutti nell'ondata 22-23/08**. + ⚠️ **Cio' che TOGLIE: il gate pre-registrato XSR01 del 23/10 legge un monitor tarato sul pavimento + del venue SBAGLIATO** β€” C* vero **$15.000-20.000** contro i ~$3.000 riportati da HL-EXEC con un + criterio piu' permissivo. **Decisione dell'operatore:** riscrivere la soglia **adesso**, dichiarando + che la si riscrive prima di vedere l'esito, oppure lasciarla e **citare il difetto alla decisione** + β€” non c'e' una terza via pulita, e toccarla guardando l'esito sarebbe selection-on-holdout. + 🚨 **(4) Β§64 β€” LE DUE SUPERFICI OPZIONI SONO LA STESSA SUPERFICIE** a strike appaiati, su entrambi + gli assi (prezzo equo identico per costruzione; mezzo-spread Ξ” **βˆ’0,21/βˆ’0,38 pp**; `f_venue` Ξ” + **+0,005/βˆ’0,011**) β‡’ **la raccomandazione di Β§51 (raccogliere la catena USDC, +18% di + chiamate/giro) NON e' giustificata dalla ragione che porta**: il `f` di VRP01 misurato + sull'inverse **non e' Β«il numero della famiglia sbagliataΒ», e' lo stesso numero**. + ⚠️ **(5) Β§60 β€” la politica multi-conto MISTO, scelta il 25/07, NON e' piu' l'ottimo:** oggi vince + **MISTO-A** (`P(>=50/g)` **9,27%** contro 6,60%) su 288 celle; **mediana 0,00 EUR/g in tutte e sei**. + 🚨 **(6) Β§63 β€” IL FORFAIT 33% E' LA CASELLA SBAGLIATA per i payout di una prop firm:** il trader + **non possiede il conto ne' le posizioni** β‡’ nessun realizzo di cripto-attivita', e **`c-sexies` + (33%) e `c-quater` (26%) sono ENTRAMBE fuori categoria**; le fonti convergono su **LAVORO + AUTONOMO** e **nessuna prassi ufficiale esiste**. βœ… La conclusione di Β§37 regge su tutta la banda; + il costo della questione vale **βˆ’8,21 EUR/g** fra i due estremi. πŸ“Œ **Domanda NUOVA al + commercialista, DIVERSA da quella aperta il 07/08** sui derivati. + ⚠️ **(7) Β§69 SCETTICO β€” replica (3,4% contro il 2,6% pubblicato) e poi toglie precisione:** SE del + drift di XS01 **9,6%/anno** β‡’ a βˆ’1 SE `P(>=50/g)` **1,6%**; morte-firm da evidenza pubblica + **12-19%/anno** contro il **10% assunto** (EV regge, break-even >55%); max-J dal **puro rumore** su + 360 celle **0,069** contro **0,303** osservato. **Raccomanda di non cambiare nulla.** + 🚨 **E il pattern conta piu' del singolo numero: 42% β†’ 7,8% β†’ 4,4% β†’ 2,6% β†’ 1,6% a βˆ’1 SE. Ogni + volta che il numero del canale funded e' stato misurato meglio e' SCESO, e non ha ancora smesso.** + E' la firma di un numero che vive di ottimismo non ancora misurato, non di una stima che converge. + ❌ **(8) GLI ALTRI CINQUE, tutti negativi.** **Β§59** a regime servono **4 conti da $100k** (i cap di + 2 firm) e `P(>=50/g)` **SATURA all'87%** β€” *non e' una rendita, e' una scommessa ingrandibile ma non + rendibile affidabile*; da EUR 635 **1,78 EUR/g mediani**. **Β§62** la frazione del versamento verso i + biglietti e' segnale sul FLUSSO (**+16,3 pp**, risoluzione MC 1,0) ma **0,0 pp** sulla RENDITA e + **+12,4 pp** di `P(sotto il versato)`. **Β§65** la forma della curva dei datati **e' un grado di + liberta' nuovo** (scorrelato dal funding, A2 confermata piu' forte dell'atteso) **e non predice + nulla**: 0 candidati su 4 rami, DSR 0,108, roll 0,49%/anno = l'ordine del premio. **Β§66** + cross-venue: 2 celle su 8 sopra la propria MDE, **manca un fattore 6,5x** (16,2x per l'arbitraggio), + e *per entrare si attraversano gli stessi due book che generano lo scarto*. **Β§68** il paniere dei + quasi-passati fa FULL 1,822 / HOLD 1,323 ed e' al 100Β° pctl contro gli scartati netti, **ma DSR + 0,828-1,000 secondo la PARTIZIONE** (lo stesso difetto del 22/08, applicato al paniere che dovrebbe + superarlo) e **dipende da UNA gamba** (PREVDAY). + ⚠️ **(9) UN FILONE E' MORTO SU UN BUG E LA CORREZIONE E' DEL COORDINATORE, non dell'autore.** + `r0823b_quasi_passati.py` terminava con `IndexError: Boolean index has wrong length: 958 instead of + 2720` β€” `Griglia.combo()` indicizzava con `self.idx` (l'unione) un **sottoinsieme di righe**. + Corretto col parametro `idx` esplicito **piu' un controllo di lunghezza**; la rinormalizzazione e' + **riga per riga**, quindi i valori sono quelli dell'intento dell'autore. **Il verdetto di Β§68 va + letto sapendo su quale codice gira.** + πŸ“Œ **(10) LA RIGA DELL'ONDATA.** Dodici filoni, zero candidati, **nessun EUR/giorno prodotto**: il + valore e' interamente **DIFENSIVO** (Β§67 e Β§64 tolgono due appoggi, Β§69 toglie precisione, Β§60 + toglie un ottimo). **Con questa sono 69 filoni e 0 candidati promossi.** La ricerca ha smesso di + essere il vincolo binding il 26/07 e **sei ondate lo hanno CONFERMATO invece che ribaltarlo**; + previsione verificabile registrata: **la 70Βͺ dara' la stessa risposta**. I vincoli binding restano + **il capitale che entra** e **il conto che non sparisce**. +- 🚨 **IL PIANO AL NETTO DI TUTTO β€” la tabella congiunta (2026-08-22, `r0822d_piano_vero.py`).** + **QUESTA SOSTITUISCE OGNI TABELLA DI TRAIETTORIA PUBBLICATA SOPRA.** Le due correzioni misurate al + piano β€” **fisco d'accumulo** (07/08) e **funding** (22/08) β€” erano state applicate **separatamente + allo stesso numero lordo**, e la tabella con **entrambe** non esisteva. Registro Β§36. + βœ… **Replica 6/6 prima di pubblicare, due esatte AL DOLLARO** sul vintage 07/08 ($272.061 lordo, + $258.338 netto fisco). ⚠️ Sulla serie di **oggi** la stessa riga da' **$278.033 (+2,2%)** β€” 15 + giorni di dati in piu', `data/raw/` gitignored (**stessa lezione GTAA del 07/08**) β†’ **ogni terza + cifra di un muro e' rumore**, e i confronti fra lenti vanno fatti sullo stesso vintage. + | lente (de-luck Γ—0,89) | drift | perpetua | muro | **€250/m da $635: P(20a)** | + |---|---|---|---|---| + | L0 LORDO (26/07) | 17,11% | 10,68% | $278k | **90%** *(pubbl. 92%)* | + | L1 +FISCO (07/08) | 17,11% | 7,52% | $264k | **49%** *(pubbl. 52%)* | + | L2 +FUNDING (22/08) | 15,19% | 9,06% | $328k | **63%** | + | 🚨 **L3 CONGIUNTA** | **15,19%** | **6,35%** | **$313k** | **14%** | + | L3b congiunta, funding "strumento vero" βˆ’1,39%/a | 15,87% | 6,72% | $296k | **26%** | + 🚨 **IL MURO E' UNA MEDIANA, E LA SUA BANDA E' ESPLOSIVA (misurato dal critico, 23/08).** + La SE del drift L3 e' **5,151%/anno** (block bootstrap 20g; replicata su un'altra lente a 5,09): + **+1 SE β†’ $204.517 Β· p90 β†’ $186.623 Β· PUNTO $313.143 Β· p10 β†’ $1.141.172 Β· βˆ’1 SE β†’ $709.753 Β· + βˆ’2 SE β†’ il traguardo NON ESISTE a nessun capitale.** «€500/mese β†’ P(20a) 85%Β» diventa **~0% a + βˆ’1 SE**; «€250/mese β†’ 14%Β» diventa **96% a +1 SE**. πŸ“Œ **Meccanismo: il muro e' + `prelievo/perpetua` e la perpetua si annulla molto prima del drift β†’ e' un 1/x su una quantita' + che va a zero, quindi l'errore e' asimmetrico verso l'alto.** 🚨 **E il progetto ha pubblicato la + risoluzione MONTE CARLO del muro (0,7%) accanto a un numero la cui incertezza di PARAMETRO e' + cento volte piu' grande: ha misurato la precisione del simulatore e mai quella del suo input.** + ⚠️ La SE del block-bootstrap e' un **limite inferiore** (variabilita' dentro gli stessi 7,4 anni, + non il cambio di regime). **REGOLA: il piano si dimensiona sul VERSAMENTO, che e' certo, non sul + muro.** + 🚨 **LE DUE CORREZIONI NON SI COMPENSANO: SI SOMMANO β€” e nel VERSAMENTO necessario il congiunto e' + +10% PEGGIORE della loro somma.** Il meccanismo "meno drift β†’ meno plusvalenza β†’ meno imposta" + **esiste** (+9,97% di capitale recuperato; il fisco toglie il 50,9% a funding OFF e il 46,0% a + funding ON) **ma la funzione versamentoβ†’probabilita' e' CONVESSA e ne ribalta il segno**. + Interazione misurata **in tre monete**: +9,97% sul capitale Β· **βˆ’$815 sul muro (0,26%, SOTTO la + risoluzione MC)** Β· **+26 €/mese sul versamento (+10%, segno stabile su 3 semi)**. + **REGOLA: non contare su una compensazione fra correzioni misurate separatamente β€” la si misura, e + in piu' di una MONETA, perche' un'interazione piccola cambia segno con la moneta.** + **QUANTO VERSARE, al netto di tutto** (da $635, nessun lump): + | orizzonte | P=50% | P=75% | **P=90%** | totale versato @P=90% | **quota del bersaglio** | + |---|---|---|---|---|---| + | **10 anni** | €1.323 | €1.534 | **€1.733** | **$229.189** | **73%** | + | 15 anni | €656 | €784 | **€920** | $183.129 | 58% | + | 20 anni | €360 | €445 | **€541** | $143.804 | 46% | + Il **solo funding** aggiunge **1,27Γ— / 1,33Γ— / 1,40Γ—** sopra la lente fisco-only. + **RENDITA €/g mediana a 20 anni:** €250/m β†’ **31,85 €/g, P(β‰₯50/g) 8,8%** Β· €500/m β†’ **63,05, P + 77,1%** Β· €800/m β†’ 100,50, P 99,2%. + πŸ“Œ **IL LUMP, mai entrato nelle tabelle nette: €10.000 oggi + €250/m porta P(20a) da 14% a 45%** + (€5.000 β†’ 30%). E' la leva piu' grande misurata dopo il versamento mensile stesso. + πŸ“Œ **LA RISPOSTA ALLA DOMANDA DEL PROGETTO:** *€250/mese da $635 β†’ **23,4 anni** (mediana + incondizionata; 22,1 condizionata all'arrivo), **P(20a) 14%** [banda "strumento vero": 22,2 anni, + 26%]. Per **€50/giorno in 10 anni** servono **€1.733/mese** a P=90%, totale versato **$229.189**.* + **A 10 anni si versano $229k per arrivare a ~$313k: il rendimento fa il 27%, i bonifici il 73%** β€” + ⚠️ **CORREZIONE 23/08: il 73% e' l'INTERO ORIZZONTE, non il versato fino all'arrivo.** Il + contatore `versato` di `accumula` e' uno **scalare che non si ferma al traguardo** (stessa + famiglia del bug `paid` catturato il 25/07, in un altro punto): il versato **fino all'arrivo** e' + **$190.265 = il 61%**, con P(traguardo) 91%. E **«€X/meseΒ» versa ogni 30 GIORNI** β†’ + **121/182/243** versamenti invece di 120/180/240 (+0,83/+1,11/+1,25%), in ogni tabella dal 25/07. + Entrambi i difetti vanno **a favore** del piano. β€” + forma piu' netta della lezione gia' scritta: *a orizzonte corto non fai lavorare la strategia, + COMPRI il capitale coi bonifici.* + ⚠️ **Cosa NON e' incluso, di proposito:** il **rischio di venue**, che e' **rovina e non costo** + (a p=5% il mediano e' zero a ogni calendario) β€” resta sul suo asse separato. + **Smentitori dichiarati:** il funding pre-2022 e' **proxy inverse sul 40% del campione** (se il + lineare 2019-21 fosse costato quanto il 2022+, la colonna vera e' **L3b**); il modello fiscale tassa + la **variazione annua di valore** = limite superiore; il bootstrap gira su **7,4 anni con due tori**. + +- βš–οΈ **CANALE FUNDED β€” `GATE PROP-01` CHIUSO 3/3 in una notte, e il numero onesto e' 2,6% + (2026-08-22).** Registro Β§29-32, Β§37. **Libro, pesi, cron, config INVARIATI.** + Il gate nato ieri con **due gambe datate ad anni** e' stato chiuso **tutto oggi**, e ogni chiusura + ha peggiorato il numero: + | gamba | criterio | esito | + |---|---|---| + | **(a) LISTINO** | β‰₯10 delle 13 gambe shortabili | βœ… **PASS 13/13** β€” misurato sul **venue** (Bybit `instruments-info`, 833 strumenti), non sul sito | + | **(b) ANCORA SKH01** | delta appaiato > +0,05 sui 23 offset | βœ… **PASS 23/23**, +0,116, 230/230 celle congiunte β€” ⚠️ ma Β«23/23Β» vale **~2 osservazioni**, vedi sotto | + | **(c) CAPITALE** | *riformulata, vedi sotto* | βœ… **PASS 2/2** | + πŸ“Œ **La (c) e' stata RIFORMULATA perche' era una constatazione, non un criterio** β€” e la sua forma e' + **imposta dal costo della misura**: l'EV costa una corsa di script (**zero dollari**), la + **rimborsabilita' del deposito costa $579 e si puo' misurare solo COMPRANDO** β†’ un gate d'attesa su + quella sarebbe **superabile solo dopo averlo violato**. Quindi: + **(c1) ECONOMICA** β€” EV del biglietto **> 0 con la quota trattata come SPESA PERSA**, alla cella + d'ancora **mediana** (non canonica), col **funding dentro**, lente **accoppiata**. *Oggi **$+1.615***. + **(c2) DI SOPRAVVIVENZA** β€” prezzo del biglietto **≀ 2 versamenti mensili del piano in corso** + (il piano d'accumulo e' la leva dominante misurata, e **zero e' assorbente**). *Oggi **$579 contro + $1.090***. **(c1) e' scritta perche' la rimborsabilita' NON decida:** il rimborso e' **un regalo, non + un'ipotesi su cui si e' scommesso**. + 🚨 **IL NUMERO, e la catena di un giorno solo: P(β‰₯50 €/g) da €600 in 36 mesi = 42% β†’ 7,8% β†’ 4,4% β†’ + 2,6% [1,5%, 4,7%], con P(zero) 40,3%.** Nell'ordine: universo fuori campione (βˆ’42% del J stava in 6 + gambe che nel 2021-23 non esistevano) Β· banda d'ancora di SKH01 (canonica al 91Β° pctl) Β· **funding** + (dJ βˆ’0,049 su **69/69** celle appaiate). + πŸ“Œ **EV del biglietto POSITIVO in tutte e tre le convenzioni sul deposito** (spesa persa +$1.615 Β· + rimborso al pass +$1.919 Β· rimborso al primo payout +$1.835). ⚠️ **E il progetto conteneva GIA' due + modelli contraddittori della quota:** `r0725_hyro:200` la rimborsa **al pass**, `pc.simulate` **mai** + β€” e il numero operativo usciva dal secondo. + 🚨 **XS01 PAGA il funding, non lo incassa β€” meccanismo DIMOSTRATO:** `max|Ξ£W| = 2,8e-17` + (dollar-neutral esatto) β‡’ **il livello del funding non entra, entra solo la DISPERSIONE + cross-sezionale**, e il momentum compra i perp col funding **piu' caro** β†’ **+1,45%/anno** + [IC95 +1,18/+1,72; banda onesta +0,69/+1,45 se HL e' ~2,1Γ— il venue]. *L'ipotesi opposta ("market- + neutral β‡’ si compensa") era esplicita nel briefing ed e' falsa.* + πŸ“Œ **Piu' leva sul funded ALZA E[payout] e ABBASSA P(payout): sono due obiettivi diversi**, e chi + vuole il rimborso non vuole la leva massima. + **IL CONFRONTO CHE DECIDE** (36 mesi, $654, stesso bootstrap appaiato, funding e fisco in entrambe): + | versamento | strada | mediana | media | P(β‰₯50/g) | + |---|---|---|---|---| + | €0 | BIGLIETTO | $132 | **$9.592** | 4,2% | + | €0 | LIBRO | **$787** | $803 | 0,0% | + | €500/m | **BIGLIETTO** | **$39.820** | **$46.298** | **13,7%** | + | €500/m | LIBRO | $22.444 | $22.694 | 0,0% | + **`IL BIGLIETTO CONTRO IL VERSARE: MEGLIO β€” ma solo perche' il piano di versamento c'e', e la ragione + non e' il rendimento, e' il NUMERO DI VOLTE CHE SI PUO' GIOCARE.`** Senza versamenti la stessa + scommessa **perde la mediana 6Γ— e vince la media 12Γ—**: una lotteria a EV positivo che un conto da + $654 puo' giocare **una volta sola**. *(Il libro da solo fa P(β‰₯50/g) 0,0% a 36 mesi **per struttura**: + non e' un difetto del libro, e' l'orizzonte.)* + ⚠️ **Cio' su cui poggia tutto, dichiarato:** il drift di XS01 misurato su **13 gambe su 19**; la + **morte-firm ~10%/anno** e' un'**assunzione** (se fosse ≫ l'EV si azzera **senza che nulla nel + modello lo segnali**); il cap **$200k/trader** limita il canale per struttura. + ⚠️ **Un numero netto catturato PRIMA di pubblicarlo:** il "capitale d'incrocio €1.000-2.000" **non e' + un meccanismo** β€” la ricchezza terminale della strada-biglietto e' **bimodale**, `P(cassa < $249)` si + muove **liscia** (56,3 β†’ 0,0%) mentre la **mediana salta di un ordine di grandezza** appena quella + massa passa il 50%. **Li' la mediana non e' robusta**, ed e' esattamente il tipo di numero netto che + si sarebbe finito per citare come soglia di progetto. + +- πŸ’‘ **EVITARE IL FUNDING β€” datati SCARTATI, ma lo SPOT e' un LEAD e la domanda fiscale vale piu' del + risparmio (2026-08-23, `r0822e_funding_avoid.py`).** Registro Β§39. **Libro/pesi/cron/config + INVARIATI.** Primo filone della sessione che poteva **restituire** drift invece di toglierlo. + **Il segnale resta CONGELATO** (TSMOM 30/90/180, vol-target 20%): cambia solo lo **strumento**. + | variante | ShFULL | ShHOLD | drift | Ξ”drift | muro | + |---|---|---|---|---|---| + | **A) perp (oggi)** | 1,513 | 1,154 | 17,08% | β€” | **$325,0k** | + | D) TP01 datato + SKH01 perp | 1,507 | **1,118** | 17,53% | +0,47% | $319,5k | + | **E) TP01 SPOT + SKH01 perp** | **1,649** | **1,203** | 18,63% | **+1,55%** | **$289,6k (βˆ’10,9%)** | + | F) nessun carry (irreale) | 1,692 | 1,214 | 19,24% | +2,16% | $276,6k | + ❌ **DATATI: NON CONVIENE.** La tesi condizionale **regge** (funding condizionale **1,94Γ—/2,59Γ—** + l'incondizionato β€” βœ… replica indipendente del 1,86-2,55Γ— β€” contro basis **1,47Γ—/1,66Γ—**: in + **livello** i carry sono identici, il datato vince **solo perche' si riprezza meno**), ma il + vantaggio e' **tutto 2019-2023**, ~0 o avverso dal 2024 β†’ **ShHOLD βˆ’0,034 negativo a 0/23 offset** e + **gate iso-volatilita' FALLITO**. SKH01 sui datati **peggiora** (turnover **48-57Γ—/anno** contro + 6,6-8,4Γ— di TP01 β†’ lo stesso mezzo spread gli costa **7 volte tanto**). + πŸ’‘ **LEAD: lo strumento a carry ZERO esiste ed e' lo SPOT** (`BTC_USDC`/`ETH_USDC`, taker **0,0** + oggi, min 0,0001). TP01 e' long-flat e supera 1,0Γ— nello **0,102%** dei giorni β†’ il tetto non morde; + **+1,55%/anno di drift a volatilita' IDENTICA** (supera il gate iso-vol), muro **βˆ’10,9%**. + ⚠️ **NON e' un candidato, tre ragioni dichiarate:** (a) **SKH01 non e' esprimibile sullo spot** + (long/short); (b) la **fee 0 e' dichiarata temporanea** e **`fee_watch` non sorveglia lo spot** β€” + se sparisse, **βˆ’13% del vantaggio** (misurato); (c) 🚨 **la domanda fiscale aperta vale PIU' del + risparmio**: se i derivati sono `c-quater` (26%) e lo spot `c-sexies` (33%), **7 punti su un CAGR + del 17,9% = ~1,25%/anno**, cioe' **piu' di quello che lo spot porta** β†’ **la domanda al + commercialista viene PRIMA di qualunque prova**, ed e' la stessa gia' aperta dal 07/08. + 🚨 **MURO EREDITATO FALSIFICATO (correzione a Β§34, scritto poche ore prima):** *"tutti i futures + datati Deribit sono INVERSE, non esiste una linea USDC datata"* e' **FALSO** β€” esistono **16 datati + USDC su BTC/ETH** (il piu' vecchio `BTC_USDC-25SEP26`, creato **2026-05-07**), taker **identico al + perpetual**, min 0,0001 BTC / 0,001 ETH. βœ… Verificato con GET pubblica. **La svista e' di + NAMESPACE:** `get_instruments?currency=BTC` **non li ritorna**, stanno sotto **`currency=USDC`** β€” + *e il coordinatore ha commesso la stessa svista al primo controllo, ottenendo 0.* **Il muro vero e' + un altro: 3,5 mesi di vita e ZERO roll trimestrali mai eseguiti.** *(6Βͺ volta nell'ondata che + l'eseguibilita' non e' il vincolo.)* + πŸ“Œ **REGOLA: il carry si misura sul LOG del rapporto dei prezzi, mai sulla media aritmetica delle + differenze** β€” quella contiene `(var A βˆ’ var B)/2`, che qui vale **1,48%/2,60% l'anno = 2-4Γ— + l'effetto cercato**. Controllo che lo rende visibile: col metodo aritmetico il perpetual + "sovra-rende" il proprio indice di +0,4/+0,5%/anno, col log ~0. **Stessa forma della regola UCITS + del 26/07**, in veste nuova. + ⚠️ **BUG in famiglia gia' nota, e NON visto dal numero:** `fund_*.parquet` etichetta l'indice + **all'istante T**, `al.get(a,'1h')` la barra **all'inizio** β†’ appaiarle **sfasa di un'ora** + (corr **0,996 al lag +1**, ~0 al lag 0), gonfia la sd della base da 8 a **68-86 bps** e produceva un + finto **+0,32%/anno nel verso atteso**. πŸ“Œ **Si e' visto dalla VOLATILITA' (+1,07pp su una strategia + identica), non dal numero.** Il pezzo vero vale **+0,07%/anno**. **REGOLA: due serie temporali dello + stesso progetto possono avere convenzioni di etichettatura diverse β€” verificare col LAG, non + fidarsi dell'indice.** + - **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso: cross-sectional su universo Hyperliquid certificato (XS01) β†’ portafoglio Sharpe ~1.55. - **Sweep "strategie alternative" (2026-06-20) β€” 104 ipotesi / 153 agenti / NIENTE di nuovo regge.** @@ -2191,6 +3279,36 @@ df = load_data("BTC", "1h") # OK. load_data("SOL", ...) -> FileNotFoundError ( diverse, e `book_depth_top3` Γ¨ **NULL su errore, mai 0**. ⚠️ Le righe ereditate da bite hanno `quote_status='unknown'`: bite non registrava il perchΓ©, e si dichiara l'ignoranza invece di inventare uno stato. + 🚨 **BUCO DI COLONNA nell'archivio ereditato, trovato il 2026-08-22 e mai registrato prima:** + `bite_archive` ha `index_price` e `underlying_price` **100% None (1.232.212/1.232.212)**. Il + sottostante esiste **solo dal 2026-07-30** (raccolta propria, 18,2% delle righe) e + `book_depth_top3` solo nel **67,2%**. **Chiunque calcoli moneyness o riprezzi sull'archivio + pre-30/07 sta usando una colonna che non c'Γ¨** β€” e il file si legge senza errori, quindi il + difetto Γ¨ silenzioso. + βœ… **Ed Γ¨ CHIUDIBILE senza dato nuovo:** il forward si ricostruisce con la **paritΓ  put-call** + dalla catena stessa β€” verificato contro l'osservato su **6.578 coppie: |errore| mediano 0,023%, + p95 0,147%, corr 1,000000** β†’ rende la superficie utilizzabile su tutti i **75 giorni** invece + che 23. (Lo **smile**, che non richiede il forward, esiste invece su **113 giorni dal 2026-05-01**; + la **superficie completa** solo da **2026-06-09**, perchΓ© prima bite raccoglieva una sola scadenza + per giro β€” misurato da **tre** agenti indipendenti.) + ⚠️ La famiglia raccolta Γ¨ quella **inverse** (`get_instruments?currency=BTC|ETH`): la superficie + **USDC-lineare non Γ¨ nell'archivio**, e ogni conclusione su di essa nel progetto Γ¨ oggi un + **controfattuale costruito sui mid inverse**, non una misura. Puntare il collettore anche su + `currency=USDC` costa ~~**+587 chiamate/giro (+90%)**~~ 🚨 **CORRETTO 2026-08-23 (Β§51): sono + +117 chiamate = +18%.** Il 587 era il conteggio **grezzo** (1.140 strumenti USDC ≀ 95g = +81%) + **senza il filtro OIβ‰₯100 che il collettore applica davvero**, e quel filtro taglia il 90% della + famiglia USDC: con gli stessi filtri del collettore vivo sono **113 strumenti** contro **649** + inverse (βœ… verificato al venue da due percorsi indipendenti; replica esatta di Β§8 β€” su Deribit + USDC l'OI e la negoziabilitΓ  sono anti-correlati). Il giro passerebbe da 652 chiamate/163 s a + **769/192 s**, dentro la finestra del :25 e senza toccare `cron_book` al :07. Resta una decisione + sul rate-limit per-IP, che ha giΓ  causato un guasto il 29/07 β€” ma **di un ordine di grandezza + piΓΉ piccola di come era stata scritta**, ed Γ¨ un numero **di oggi** (cresce con la liquiditΓ  + USDC, va ri-misurato prima di accendere). + 🚨 **LA RAGIONE PER RACCOGLIERLA E' CADUTA (2026-08-23, Β§64): le due superfici sono LA STESSA + SUPERFICIE** a strike appaiati β€” prezzo equo identico per costruzione, mezzo-spread Ξ” βˆ’0,21/βˆ’0,38 pp, + `f_venue` Ξ” +0,005/βˆ’0,011 β‡’ il `f` di VRP01 misurato sull'inverse **non e' Β«il numero della famiglia + sbagliataΒ», e' lo stesso numero**. Resta vero che la lineare **non e' nell'archivio**; cade + l'argomento che la sua assenza falsi una misura gia' fatta. **NON assorbito, e perchΓ©:** il motore credit-spread ETH (il progetto ha giΓ  la regola "niente short-vol da modello in deploy"), la GUI, kill switch/dead-man/audit (PythagorasGoal ha `venue_watch`/`edge_watch`/`monitor_health`/`fee_watch`), `dvol_history` (`fetch_dvol.py` ha diff --git a/docs/diary/2026-08-22-wave-multiagente.md b/docs/diary/2026-08-22-wave-multiagente.md new file mode 100644 index 0000000..b16c0d0 --- /dev/null +++ b/docs/diary/2026-08-22-wave-multiagente.md @@ -0,0 +1,152 @@ +# Ondata multi-agente 2026-08-22 β€” 20 filoni, branch `research/wave-0822` + +**Mandato dell'operatore:** *"metti decine di agenti a fare analisi di nuove strategie possibili. +Lavora su branch separato. Lo scopo e' sempre lo stesso arrivare ai 50 giornalieri velocemente."* + +**Esito in una riga: 0 candidati promossi, 1 difetto di produzione che avrebbe falsato un gate fra +36 giorni, 3 soglie pubblicate falsificate, 1 leva strutturale che nessun gate del progetto sa +vedere. Book, pesi, cron, config INVARIATI.** + +Ledger completo per filone: `docs/research/RESULTS-0822.md`. Briefing dato agli agenti: +`docs/research/BRIEF-0822.md`. 21 script in `scripts/research/r0822_*.py` (15.295 righe, compilano +tutti, ognuno gira da solo). + +## 0. Come e' stata condotta (e perche' conta) + +Vincolo di macchina: **2 core**, 7 GB, e sulla stessa VPS gira il libro con **soldi veri** +(`cron_book` al minuto :07). Il briefing imponeva `nice -n 19`, budget 15 min/run, sola lettura su +`config/`, `data/live/`, `src/live/`, `scripts/live/`, cron. **Ha tenuto**: al picco di carico +(load 14 su 2 core) il giro delle 17:07 del libro e' girato in **18 secondi**, feed fresco, nessuna +azione, nessun OOM. +⚠️ **Fatto operativo da ricordare: il cron gira dal working tree.** Con questo branch attivo, +stanotte alle 00:30 il cron esegue il codice di QUESTO branch. Verificato che il diff vs `main` +tocca **solo** `scripts/research/` e `docs/research/` -> produzione identica. **E' anche il motivo +per cui nessuna riparazione e' stata applicata qui: sarebbe andata live senza una decisione.** + +Il briefing dava agli agenti, **in anticipo**, i dieci modi in cui questo progetto si e' gia' +ingannato (null del de-levering, TP01 travestito, selezione sull'hold-out, fortuna d'ancora, Sharpe +implausibile, win-rate come knob, ...) e la lista dei filoni morti. **Sette agenti hanno catturato +un errore proprio prima di pubblicare**, e tre hanno refutato una propria ipotesi in corsa. + +## 1. Il risultato che va agito β€” 4 monitor forward su 6 sono rotti + +`fetch_hyperliquid` (e `resample_tf`) scrivono la barra del **giorno in corso**; `advance()` dei +monitor la consuma e **porta `last_ts` su di essa** -> le ore restanti di ogni giorno non entrano in +nessun rendimento registrato. + +| monitor | barre coincidenti col replay | **min/giorno registrati** | gate | +|---|---|---|---| +| `paper_statarb` | **0/46** | **4** | **STATARB 27/09** | +| `paper_dvolspread` | 0/28 | **2** | DVOLSPREAD 24/10 | +| `paper_xsr` | 1/28 | **41** | XSR01 23/10 | +| `paper_portfolio` | 7/56 | 19 | β€” | +| `paper_prevday` | 1259/1319 | 1.438 | β€” | +| `paper_combo` | 7/37 | 1.402 | β€” (sano **per caso**: aspetta una borsa) | + +**Il gate STATARB del 27/09 si ribalta su 2 criteri su 3**: Sharpe registrato **+1,96** contro +**βˆ’2,02** ricostruito, maxDD **2,2% contro 15,3%** (guardia <10%). +βœ… **Ma la finestra non va persa**, ed e' misurato due volte in modo indipendente: il feed **non +riscrive le barre chiuse** (`paper_prevday` 1427/1488 barre identiche al bit dopo 62 notti; e le 6 +barre di `paper_statarb` recuperate dopo il guasto EPERM del 09-15/07 coincidono al bit). +**-> La riparazione e' "riparare `advance()` + RIGENERARE", non "riparare e azzerare": nessuna data +di gate si sposta.** +**Guardia raccomandata** (non implementata): `ts_ultima_barra + cadenza <= mtime del file`, grazia +5 min. O(1), segnala 5/6, **tace sul sano**, controllo positivo sintetico 6/6 nei due versi. +⚠️ `monitor_health` non poteva vederlo: **una serie fresca, completa e sbagliata passa ogni controllo +di freschezza**. E `implausible_sharpe` esiste dal 26/07 ma **non e' mai stato puntato sulle serie +forward** (avrebbe segnalato Sharpe βˆ’15,8 su 28 barre). + +## 2. Tre soglie pubblicate, falsificate con misure + +1. **"XS01 serve ~$20k"** -> **non esiste soglia da min-order**. L'origine del numero era *"rumore di + arrotondamento"*, una stima a occhio del 19/06. Haircut ~0 gia' a **$200-600 allocati**. + **Meccanismo**: gli ordini sono **due popolazioni** β€” il ribilanciamento del segnale e' il 13% + degli ordini ma il **75% del nozionale** (ticket $13,65, passa sempre); la deriva del vol-target + e' l'87% degli ordini ma il **25% del nozionale** (ticket $0,64, il pavimento la taglia **ed e' + gratis**). *Contare gli ordini da' 16% e sembra un disastro; contare il nozionale da' 75%.* + Due agenti indipendenti ci sono arrivati per strade diverse. + ⚠️ **Il "$20k di XS01" e il "$20k della decisione di venue" erano due cose diverse conflate in + CLAUDE.md.** Il primo cade; **il secondo regge intatto** β€” e' una decisione dell'operatore + sull'asse della rovina, non sull'eseguibilita'. +2. **"Slippage = rischio #1 di XSR01"** -> **refutato con margine 21x** al livello di liquidita' + odierno (mezzo-spread 0,7-1,0 bps contro i ~16 che servirebbero a uccidere l'edge). +3. **"BTC opzioni fuori a $3.000 (min 0.1 = $6.210/lotto)"** -> misurato sulla famiglia **inverse**, + che il conto (in USDC) **non puo' marginare**. La famiglia USDC-lineare ha lotti **10x piu' + piccoli**. Il muro resta, ma per il **prezzo del lotto**, non per quello che si credeva. + +## 3. La leva: l'unica variabile che nessun gate del progetto sa vedere + +**Lo Sharpe e' invariante alla scala** (misurato: 1,31 a ogni cella del knob) -> `deflated_sharpe` e +`marginal_vs_tp01` non falliscono sulla leva, **non la vedono**. E' per questo che in due mesi la +scala non e' mai stata esaminata. +Il libro gira al **7% di Kelly** e raccoglie il **15%** della crescita massima in log. Il gradino +eseguibile 1,00x -> 1,25-1,50x vale **14,7 anni -> 12,9-11,6** al capitale-rendita (€500/mese, netto +fisco). **Lo scettico ha confermato il risultato di testa** e ha tolto la sua condizione bloccante: +sotto la lente wick accoppiata il maxDD prende un **ricarico moltiplicativo costante del ~3,5%**, non +un'amplificazione (il maxDD e' multi-giorno, il wick e' di un giorno). +**Il gradino resta NON autorizzato**: cadono una riserva su tre. Restano il drift stimato su 7,4 anni +(k* e' lineare nel suo errore) e la **coda assente dal dataset** β€” un solo giorno a βˆ’10% all'anno +porta k* da ~10x a **2x**. +πŸ“Œ **Ma la lezione del 25/07 e' vera e non si trasferisce:** su una regola a **UN giorno** +(daily-loss, stop di conto) close-only e' **esattamente cieca** β€” 0,00 breach/anno contro 0,40-1,21 +veri, rapporto **INF**. **Sul canale prop/funded la lente accoppiata resta obbligatoria.** + +## 4. Cosa e' morto, e come + +**Filoni chiusi con autopsia** (l'elenco per esteso e' nel ledger): adaptive-horizon (il "vincitore +adattivo" e' un lookback **costante** travestito), dealer-gamma (la gamba tradeable e' **incoerente**: +BTC non separa, ETH separa **al rovescio**), ortho-screen **7/7**, oi-pin (il max-pain **non batte +mai** la media a 7 giorni dello spot), term-structure (la pendenza e' un **termometro contemporaneo**), +skew direzionale (**il prezzo muove lo skew**, t 3,1-9,4 su 8/8 β€” non il contrario), flow-squeeze, +basis-calendar (il basis dei datati **E'** il funding del perp), alt-options (alt), xs-lite, +vrp-quote-vere. + +Tre chiusure di famiglia che valgono per le prossime ondate: +- **Il filone funding si chiude sul QUARTO lato** (affollamento): misurato su **53.430 ore / 3 anni**, + non predice ne' direzione ne' volatilita'. E i futures datati non sono un quinto lato: sono lo + stesso lato quotato diversamente (+7,33% implicito contro +6,48% realizzato, premio incassabile + +0,85%/anno con **IC95 che contiene lo zero**). +- **La mean-reversion non risorge** sotto due conditioner mai provati (volume, shock 3Οƒ): la selezione + in-sample sceglie il ramo di **continuazione** in entrambi i casi. +- πŸ“Œ **Uno screen largo su BTC/ETH direzionale NON PUO' passare il proprio gate.** Su 168 trial il + massimo Sharpe atteso dal **puro rumore** e' **1,572**, sopra il soffitto direzionale misurato + (~1,3). Non e' sfortuna, e' aritmetica. **Le prossime ondate o dichiarano famiglie molto piu' + piccole in anticipo, o cambiano meccanismo.** + +⚠️ **Tre volte in questa ondata l'eseguibilita' a $600 NON e' stata il vincolo** (haircut 0,00-0,01; +lotti $7-242; margine ~$148). Dopo due mesi in cui il muro era sempre il capitale, adesso muoiono +tutti sull'edge. **E' un'informazione sul dove cercare.** + +## 5. Regole nuove, e una ritirata + +**Nuove:** +- **L'open interest non misura la negoziabilita'** β€” su Deribit USDC sono quasi **anti-correlati + (rho di rango βˆ’0,77)**: SOL_USDC e' 1Βͺ per OI e ultima per quote a due lati (**9%**); BTC_USDC ha + **5** strumenti con OI>=100 e il **93%** dei put quotati. +- **Il deflated-Sharpe va calcolato DI SCREEN, non solo di famiglia** (168 trial, non 24). +- **Un placebo si controlla per BILANCIAMENTO DEL SEGNO prima di usarlo**: un null che concorda col + segnale solo nel 14% dei casi non e' un null, e' la strategia invertita β€” e fabbrica celle + "significative". +- **Il null del de-levering e' DEGENERE quando la variante e' una ri-scalatura** (`sh(k*base) === + sh(base)`): il test che serve e' **iso-peso sullo Sharpe**. +- **Un gate che stampa `None` va verificato prima di dichiararlo "non girato".** +- **Su una finestra interamente post-hold-out, `marginal_vs_tp01` non puo' dare ADDS per + costruzione** β€” un NEUTRAL li' non e' un giudizio. + +**Ritirata (era stata pubblicata poche ore prima, in questa stessa ondata):** +❌ *"Su equity a gradino un overlay giornaliero e' non-causale"* β€” **falso su questo motore**: +`backtest_signals` contabilizza al giorno d'**ingresso** in **291/293** trade, e la "riparazione" e' +un **no-op bit-exact**. Il "+0,04 fantasma" era la differenza fra due varianti **entrambe causali**, +e quella scartata era la **migliore sull'hold-out**. *Il costo di una regola derivata da un difetto +inesistente e' gia' stato pagato.* + +## 6. Cio' che resta aperto (decisioni dell'operatore, non mie) + +1. **Riparare `advance()` + rigenerare** le serie forward, prima del 27/09. +2. **Cablare la guardia D2** (`ts_ultima_barra + cadenza <= mtime`) in `monitor_health`. +3. **Il collettore della catena** raccoglie la famiglia inverse; ETH_USDC costa **+587 chiamate/giro + (+90%)** ed e' una decisione sul rate limit per-IP, che il 29/07 e' gia' costata un guasto. +4. **Rimisurare `f` di VRP01 term-structure-consistent** (il 42% del difetto e' li', e la regola per + prenderlo era gia' scritta il 03/07). +5. **XS01 e' eseguibile a ~$1.300 di conto**, non a $20k. Resta fuori **per la decisione di venue**, + che e' un'altra cosa. Se il piano cambia, si riapre prima. diff --git a/docs/diary/2026-08-22-wave2-xs01-funded.md b/docs/diary/2026-08-22-wave2-xs01-funded.md new file mode 100644 index 0000000..cdadbe9 --- /dev/null +++ b/docs/diary/2026-08-22-wave2-xs01-funded.md @@ -0,0 +1,188 @@ +# 2026-08-22 (sera) β€” Seconda ondata: XS01 fuori dalla sua finestra, e il canale funded ridimensionato di 5x + +**Branch:** `research/wave-0822` Β· **9 filoni** Β· **0 candidati promossi** Β· **libro, pesi, cron, +config INVARIATI.** + +Registro per filone: `docs/research/RESULTS-0822.md` (sezioni 21-29). Briefing: `BRIEF-0822.md`. + +--- + +## Perche' una seconda ondata + +La prima (20 filoni) si era chiusa con un risultato di testa β€” **PROP-ALLOC**: su un conto *funded* +riallocare per la **barriera di drawdown** invece che per lo Sharpe porta J da 0,335 a 0,738, con +**P(β‰₯50 €/g) da €600 in 36 mesi = 42%**. Il percorso piu' veloce mai misurato in questo progetto. + +E si era chiusa con l'agente che dichiarava contro se stesso il proprio limite: *"tutto il vantaggio +poggia sul drift di XS01 misurato sulla sua finestra di scoperta"*, con un gate che era **un'attesa di +anni** (*"non aprire un funded prima che XS01 abbia una finestra fuori dal 2024-2026"*). + +La seconda ondata e' stata mirata su quello, piu' sui falsificatori che gli agenti stessi avevano +**nominato senza poterli eseguire**. + +--- + +## 1. XS01 regge fuori dalla sua finestra β€” ed e' piu' forte li' (Β§21) + +La finestra esisteva gia': gli stessi 19 ticker su **Binance spot 2021-01 β†’ 2023-12**, che contiene +**LUNA e FTX**. Meccanismo **congelato**, nessuna griglia cercata. + +**Mediana di fase +1,12 fuori campione contro +0,37 nella finestra di scoperta; differenza appaiata ++0,668, positiva in 10/10 fasi**, p=0,013 contro permutazione a fee zero, sopravvive a 30 bps/lato. + +L'obiezione ovvia β€” *"Binance non e' la verita'"* β€” e' stata **misurata invece che aggirata**: quella +regola riguarda l'**ancoraggio di prezzo**, non un ranking cross-sezionale, e lo scarto USDT e' un +**fattore comune** che sparisce nello z-score. Lo stesso sleeve sui due venue da' **corr 0,9991**. + +**Null "universo dove non dovrebbe funzionare"** (11 settoriali SPDR, 1998+, meccanismo congelato): +**βˆ’0,45, 0/10 fasi positive** β†’ **terza conferma indipendente che il cross-sectional e' +crypto-specifico**. + +## 2. Il falsificatore che l'autore ha nominato contro se stesso β€” eseguito, e non falsifica (Β§28) + +La validazione di venue di Β§21 girava **solo sul 2024+**, cioe' senza il depeg USDT che il fuori +campione contiene. Soglia dichiarata da lui: `corr β‰₯ 0,99`. + +**Corr mediana 0,9992, minima 0,9921. dSharpe appaiato βˆ’0,000. Il titolo passa da +1,12 a +1,11.** +Guardando il canale del danno β€” i ribilanciamenti β€” su 734 solo **10 cambiano una gamba su cinque**, e +**zero dentro la finestra del depeg**. + +**Il punto di metodo:** la divergenza va **scomposta**, perche' le due componenti sono domande diverse. +La **comune** arriva a βˆ’39 bps nel depeg ed e' quella che lo z-score annulla; la **idiosincratica** β€” +l'unica che cambia il ranking β€” sale solo da 2,0 a 3,6 bps mediani. *Riportare solo la prima avrebbe +dato la risposta rassicurante e sbagliata.* + +E ha trovato che **il caso peggiore del campione non e' il depeg: e' il 2025-10-10** (cascata di +liquidazioni sulle /USDT, INJ a 1.981 bps mentre BTC/ETH stanno a 21). **Il meccanismo temuto e' +REALE**; e' semplicemente caduto nella finestra di *scoperta*, non nel fuori campione. + +## 3. Ma il numero funded si ridimensiona di 5x β€” e il colpevole non e' la finestra (Β§29) + +Decomposizione a **un grado di liberta' per volta**: + +| passo | J | Ξ” | +|---|---|---| +| il pubblicato (HL, 19 gambe, finestra di scoperta) | 0,739 | β€” | +| β†’ venue Binance, **date allineate** | 0,732 | **βˆ’0,006** | +| β†’ **13 gambe** | 0,426 | **βˆ’0,307** | +| β†’ pannello lungo | 0,459 | +0,034 | +| β†’ **fuori campione 2021-2023** | 0,520 | **+0,061** | + +**Il 42% del numero pubblicato sta nelle 6 gambe che fuori campione non esistono** (ARB OP SUI APT SEI +TIA) β€” **e quelle 6 entrarono nell'universo nel 2026-06 guardando la liquidita' HL del 2024+, cioe' +dentro la finestra di scoperta.** La finestra, da sola, **migliora**. Allargare l'universo man mano +che le gambe nascono non recupera nulla. + +**Il numero operativo: P(β‰₯50 €/g) da €600 in 36 mesi = 7,8% [7,2–9,1%], P(zero) 25,5%** β€” contro il +42% pubblicato stamattina. Il peso 0,50 non sopravvive; la **regione [25%, 38%] e' entro il 3% +dell'ottimo su tutte e tre le finestre**. + +**`GATE PROP-01`** sostituisce l'attesa di anni: **(a)** leggere il **listino della firm** β€” β‰₯10 delle +13 gambe shortabili, verifica a **€0** e **bloccante prima di ogni spesa**, *stessa classe +dell'errore GTAA01/PRIIPs*; **(b)** banda d'ancora di SKH01 sotto la lente prop, **2026-10-31**, PASS +se > +0,05; **(c)** un'eval HYRO $100k costa **$579 su un conto di $635**. + +## 4. L'allarme sul peso di TP01 e' falsificato NEL VERSO (Β§26) + +L'agente aveva registrato nel docstring, **prima di misurare**, che si aspettava di confermarlo. +Invece: **TP01 ~0,375 e' l'argmax sia sulla finestra che contiene il 2022 sia su quella che non lo +contiene**, e nel sinistro il libro con **meno** TP01 fa **meglio** β€” TP01 va flat mentre **SKH01 si +gira short e guadagna**. Test dei segni 12/12 (p=0,0005), 8/8 sul solo pre-2024. + +**Null del de-levering, 7a occorrenza, in veste nuova** ("protezione dal crash"): a leva comune TP01 +protegge davvero, ma **a iso-sopravvivenza meno TP01 paga di piu'** (payout $4.490 β†’ $2.119, +monotono). + +Tre cose che valgono piu' dell'allarme rimosso: +- **"2024-2026 non contiene un crash" e' falsa nella forma**: ha buy&hold a βˆ’60% di DD. Manca la + **taglia** (~2,3x sulla coda). La domanda giusta e' *"e se ne arriva uno due volte piu' grande"*. +- **Alle leve funded il 2022 non era una minaccia** (min equity βˆ’1,4/βˆ’2,4% contro barriera βˆ’6%): cio' + che uccide un conto a barriera e' la **regola a un giorno su una giornata qualunque**. +- **Il rischio vero dell'ottimo e' XS01, non TP01** β€” e l'agente ha demolito la propria ipotesi + contraria dentro la sessione. + +## 5. Tre filoni chiusi con margine + +**BOCPD (Β§23) β€” l'orizzonte adattivo e' CHIUSO DEFINITIVAMENTE.** Il falsificatore e' stato eseguito +con i rilevatori che nominava (Adams & MacKay + optimal partitioning causale) e **questa volta ha +avuto potenza**: 68/96 celle davvero adattive, contro 0 del primo tentativo. Con potenza, **0/68** su +entrambe le condizioni. **La decomposizione e' il risultato: l'adattivita' spiega il βˆ’7% del vantaggio +su TP01** (+0,326 del miglior lookback costante contro +0,305 dell'adattivo). *Il 100% e' "un +orizzonte piu' corto", non "adattarlo".* + +**MAKER (Β§22) β€” il segno dipende da `<` contro `<=`.** Il passivo **vince sul 92-98% degli ordini e +perde sulla media** (5 ordini su 1535 fanno il 42% del danno). Forma speculare del profit-take di +VRP01. Tetto **$2,96/anno a $635** nel caso impossibile. + +**SURFACE-RV (Β§27) β€” chiuso, e il meccanismo vale piu' del verdetto.** Censimento su 1,24 M quote: +**0 violazioni di monotonia su 1,1 M**, 1 di convessita' su 1,03 M che vale **$0,00**. E `|residuo|` +cresce **monotonamente col largo del mercato** β†’ **l'incoerenza al mid *e'* la larghezza del mercato +guardata attraverso il mid.** Questo uccide il controfattuale USDC che l'agente stesso aveva +costruito. + +## 6. BIN-FREQ: la direzione e' reale, la taglia e' selezione (Β§25) + +Argmax **+0,112 in 23/23 ancore**, ma **mediana di famiglia +0,007** con 39/72 celle positive = +monetina. **L'argmax vale 16x la mediana: punta, non plateau.** Direzione solidissima (0/72 sul segno +inverso, trasferisce a SKH01_V1). + +**Due premesse del mio briefing, misurate e refutate:** *"dimezza gli ordini"* β†’ i nettati calano del +**17%** e i sotto-min-order **sono di TP01**; *"converti il risparmio di fee"* β†’ **a fee zero +l'effetto resta l'87-110%**, quindi non e' risparmio di costo ma informazione sul rendimento lordo. +E la domanda che avevo posto era **il confronto sbagliato** (la banda del livello si cancella in un +disegno appaiato) β€” **ma la risposta corretta e' peggiore, non migliore, per il candidato**. + +Riapertura pre-registrata: **non una data, una soglia di CAPITALE** ($5.600 / $53.000), perche' cio' +che fallisce e' il rapporto fra un costo **fisso** e un beneficio **proporzionale**. + +## 7. Il critico: i difetti stanno nella CUCITURA, non nei filoni (Β§24) + +**Una regola mia, pubblicata la sera stessa e ritirata poche ore dopo:** avevo scritto che +`dealer_net_gamma` e' *"il GEX col segno invertito"*. **Falso**, e la fonte gira su questa stessa VPS +(`cerbero-mcp/.../common/options.py:11-14` **dichiara la convenzione**). Verificato in due comandi. +La correlazione a βˆ’0,95 era esattamente cio' che una convenzione diversa **deve** produrre. + +**E il difetto piu' grande dell'ondata: il deflated-Sharpe misurato ai suoi due estremi degeneri lo +stesso giorno da agenti che non si citano** β€” e **tre gate pre-registrati** ci poggiano sopra. + +--- + +## Il filo che tiene insieme la giornata: `deflated_sharpe` non ha una regione utile in mezzo + +**Tre agenti indipendenti, tre strade diverse, stessa conclusione.** + +`altlib.deflated_sharpe` calcola `sr0 = sd(Sharpe dei trial) Γ— mult(N)`: dipende dalla **varianza** +della griglia, non solo da N. + +- **VOL-SIZE** (ondata 1) lo dichiara **VACUO**: 1,000 anche per il baseline. +- **BOCPD** lo misura dall'altro capo: **0,998 PASS** dentro la famiglia omogenea β†’ **0,555 FAIL** con + l'sr0 di screen. *"Gonfiare N col padding alza N e non la varianza."* +- **CRITICO** trova che l'1,572 di ORTHO-SCREEN implica sdβ‰ˆ0,58, e che **le stesse 168 celle + dichiarate come 7 famiglie da 24 danno 1,15** β€” sotto il soffitto ~1,3: il verdetto *"impossibile + per aritmetica"* **si ribalta senza toccare un dato**, e la cura che suggerisce e' la manovra che il + progetto ha proibito il 30/07. +- **BIN-FREQ** chiude il cerchio dall'alto: su una famiglia **binaria** il DSR **riacquista potenza** + (0,977 contro baseline 0,834) perche' le celle scartano insiemi di trade molto diversi. *Non e' il + candidato a essere piu' solido: e' la famiglia a essere piu' larga.* + +**Non basta contare i trial di screen: serve la VARIANZA di screen.** E il numero di trial dichiarati +resta una scelta dell'autore, cioe' l'unico posto dove barare e' indolore e invisibile β€” con in piu' +il fatto che ora sappiamo che **la stessa griglia da' verdetti opposti a seconda di come la si +partiziona**. + +--- + +## Cosa NON e' stato fatto, e perche' + +**Nessuna riparazione applicata.** Il cron esegue dalla **working tree**, che e' su questo branch: una +riparazione andrebbe live alle 00:30 senza una decisione dell'operatore. Il diff tocca **solo** +`scripts/research/`, `docs/` e `CLAUDE.md` (verificato). + +Restano dell'operatore: **(1)** riparare `advance()` + rigenerare i monitor forward prima del gate +STATARB del **27/09**; **(2)** decidere del branch; **(3)** la verifica a costo zero che decide il +canale funded β€” **leggere il listino di una prop firm**. + +πŸ“Œ **E la risposta onesta al mandato, che al critico era stato chiesto di dare se vera: nessuna misura +di ricerca di questa giornata batte "versare".** L'effetto piu' grande misurato oggi vale +**€0,0113/giorno**; versare €500/mese vale **~1456x** quello. La ricerca ha smesso di essere il +vincolo binding il 26/07, e due ondate da 29 filoni lo hanno **confermato invece che ribaltarlo**. diff --git a/docs/diary/2026-08-23-wave-chiusura-critico.md b/docs/diary/2026-08-23-wave-chiusura-critico.md new file mode 100644 index 0000000..1230e1c --- /dev/null +++ b/docs/diary/2026-08-23-wave-chiusura-critico.md @@ -0,0 +1,184 @@ +# 2026-08-23 β€” Ondate 7-8 e il critico di chiusura: 17 filoni, 0 candidati, 3 numeri di testa riscritti + +**Branch:** `research/wave-0822` Β· **Registro per filone:** `docs/research/RESULTS-0822.md` Β§41-57 +**Libro, pesi, cron, config: INVARIATI.** `git diff main -- src/ config/ scripts/live/ scripts/cron_*.sh tests/` vuoto a ogni checkpoint. + +## Perche' esiste questa voce + +Il mandato era *"metti decine di agenti a fare analisi di nuove strategie possibili... arrivare ai 50 +giornalieri velocemente"*. Le ondate 1-3 (Β§1-40) avevano gia' dato 0 candidati. Queste due ne +aggiungono 17 e chiudono con un agente incaricato di **attaccare l'ondata invece di estenderla**. +La risposta al mandato e' negativa, ed e' quantificata: **la somma di tutti i lead positivi vale ++0,036 €/giorno**, contro i **+71 punti di probabilita'** che valgono €250 β†’ €500 al mese. + +Questa voce non ripete il registro. Tiene le tre cose che il registro, per costruzione, non puo' +dire: cosa e' cambiato **nella memoria operativa**, cosa ho sbagliato **io**, e cosa resta all'operatore. + +## 1. Il risultato che nessun filone cercava + +Β§50 chiedeva *"qual e' il miglior terzo sleeve eseguibile a $635"* e ha trovato che **il candidato +piu' forte mai misurato dal progetto era gia' in produzione β€” in monitor da giugno, senza gate +pre-registrato e senza deflated-Sharpe.** Sette candidati, **sette muri diversi, e nessuno dei sette +e' una soglia di capitale**: non e' il conto piccolo a fermarli. + +Β§54 gli ha scritto il gate mancante. Passa **8 condizioni su 10**, e le due che mancano sono la +stessa: **la cella che gira non e' quella che la selezione onesta sceglie** (quella viva fallisce il +DSR a 0,905), e la selezione onesta compra il libro **long-flat**, cioe' butta via la gamba per cui +PREVDAY fu promosso. *Non e' un parametro da ritoccare: sono due strategie diverse.* + +Verifica mia sopra il filone: la serie e' **intatta, 1512 barre su 1512** β€” e' l'unico monitor che +il difetto `advance()` non tocca. Ma la finestra e' **0,172 anni** e li' l'**MDE e' 4,72 di Sharpe**: +il +2,11 forward **non e' distinguibile da zero** su 26 ingressi. **Qualunque cosa il gate decida, +non puo' appoggiarsi al numero forward.** + +## 2. Il critico, e le tre cose che vanno riscritte + +Prima di ogni accusa ha **riprodotto** con macchineria propria: i quattro muri **al dollaro**, il +funding (βˆ’2,1597%/anno), il dShFULL di Β§42, e che il muro **e'** esattamente `prelievo/perpetua` su +4 lenti su 4. **Il nucleo quantitativo dell'ondata regge.** Poi ha falsificato **due sue attese +scritte prima di misurare** e tre miei numeri. + +**(a) Il muro non e' $313k: e' la mediana di una banda esplosiva.** SE del drift **5,151%/anno**; ++1 SE β†’ $204.517, βˆ’1 SE β†’ $709.753, **βˆ’2 SE β†’ il traguardo non esiste a nessun capitale**. +«€500/mese β†’ P(20a) 85%Β» diventa **~0% a βˆ’1 SE**. Meccanismo: il muro e' un `1/x` su una quantita' +(la rendita perpetua) che **si annulla prima del drift** β†’ errore asimmetrico verso l'alto. +Il pezzo che brucia: **per otto ondate ho stampato accanto a quel numero la risoluzione MONTE CARLO +(0,7%)** β€” la precisione del simulatore β€” **e mai quella del suo ingresso, cento volte piu' grande.** + +**(b) Β«Positivo in 24 ancore su 24Β» vale ~2 osservazioni.** Corr media fra le 24 ancore di TP01 +**0,631 β†’ N_eff 1,55**; fra le 10 fasi di XS01 **0,790 β†’ N_eff 1,23**. E **non si salva sulla +differenza appaiata**: la componente comune non si cancella (N_eff 1,64-2,24) β€” era l'attesa a +priori del critico, **refutata**. Sulla stessa grandezza la **banda d'ancora e' 4,3x piu' stretta +dell'IC95, che contiene lo zero** (su XS01: 3,8x; due misure indipendenti, stesso fattore ~4). +⚠️ **Non cadono i verdetti** β€” poggiano anche su iso-vol, maxDD, selection-on-holdout, edge lordo. +**Cade la precisione dei numeri, non la direzione delle decisioni.** + +**(c) Il gradino di leva a 1,50x era gia' morto.** Β§53 lo boccia (peggior giorno strutturale +**21,48% > 20%**) e fissa **k max difendibile 1,40**. Sopravvive solo il **1,25x**, che vale +**+€144/mese misurati direttamente** (non €164, che veniva da un'interpolazione lineare) e +**+1,8 anni a muro congelato / +3,0 a muro mobile** β€” *e la convenzione va sempre detta, perche' le +due letture distano piu' di quasi tutti gli effetti che questo progetto misura.* + +**In piu': tre baseline sotto lo stesso nome Β«libro 75/25Β».** `hourly` 1,683/11,22% (la lente di +**ogni muro e traiettoria**) Β· `canonical` 1,800/9,56% Β· mediana d'ancora 1,626/10,4%. **Spread 0,12 +di Sharpe, piu' grande di quasi tutti i Ξ” dell'ondata**: un Ξ” accostato al baseline sbagliato cambia +piu' del Ξ”. **Ogni numero va scritto con la sua lente attaccata.** + +## 3. Cosa ho sbagliato io + +Cinque errori, tutti su numeri **gia' pubblicati**, due dei quali stavano in `CLAUDE.md`: + +| # | cosa avevo scritto | cos'e' | +|---|---|---| +| a | Β«la convinzione di TP01 sta spesso a 1/3 o 2/3Β» | **il bucket 2/3 non esiste** β€” `tsmom_blend` media tre `np.sign()`, la famiglia ha **un** grado di liberta', non tre. Trovato da **due agenti indipendenti** | +| b | Β«la catena USDC costa +90% di chiamateΒ» | **+18%** β€” i 587 erano il conteggio grezzo, senza il filtro OIβ‰₯100 che il collettore applica | +| c | Β«le opzioni BTC sono fuori a $3.000 per il lottoΒ» | quello e' il **collaterale per VENDERE**; comprare costa il **premio**. Il muro vero e' il **tick da 5,00 USDC** | +| d | Β«il gradino vale +1,8a / +€164Β» | lettura a **muro fermo**, convenzione **mai dichiarata** | +| e | Β«la coda muove k\* di 1-2 unita'Β» | **4,6** | + +E uno catturato **prima** di pubblicarlo, che vale la pena tenere perche' e' della famiglia che +stavo criticando: avevo formulato l'MDE di PREVDAY come *Β«la precisione apparente viene dal numero +di mark, non di scommesseΒ»*. **Falso**: la SE dello Sharpe annualizzato va con `1/sqrt(anni)` +**indipendentemente dalla frequenza**, e infatti barra e trade danno lo stesso 4,72. Il fatto vero +e' piu' banale: **63 giorni sono 63 giorni.** + +Il critico ha fatto lo stesso su di se': la sua prima stesura **de-luckava una serie gia' +de-luckata** (0,89Β²) e stampava un finto +31,5% sul muro. *Il controllo che l'ha preso e' stato +guardare il DRIFT stampato accanto al muro, non il muro.* + +## 4. Due difetti di contabilita' nel piano, entrambi a favore + +- **«€X/meseΒ» versa ogni 30 GIORNI** β†’ 121/182/243 versamenti invece di 120/180/240 + (+0,83/+1,11/+1,25%), in **ogni tabella dal 25/07**. +- **Il contatore `versato` e' uno scalare che non si ferma al traguardo** β†’ il *Β«a 10 anni i bonifici + fanno il 73%Β»* e' l'intero orizzonte; **fino all'arrivo e' il 61%** ($190.265). *Stessa famiglia + del bug `paid` catturato il 25/07 β€” la lezione era codificata e il difetto era in un altro punto.* + +## 5. Cosa e' stato refutato, e perche' e' interessante + +Tre meccanismi di protezione, **due caduti sulla premessa prima che sul prezzo**: + +- **TAIL-HEDGE (Β§46):** comprare put deep-OTM fa **SALIRE** il maxDD in **162/162 celle**, a ogni + lente di f β€” perche' il beta del libro al sottostante e' **+0,076**. *Non si assicura un libro che + nei crash e' gia' quasi piatto.* +- **TP01-TWIN (Β§49):** un ensemble di definizioni di trend protegge **peggio** (0/24 ancore, 3,2x + peggio nel 2022) β€” e i meccanismi **non erano ridondanti** (corr di posizione 0,61): la ridondanza + non era il problema. +- **ANCHOR-ENSEMBLE (Β§47):** su TP01 il drift **non puo'** cambiare, per **algebra** + (`lordo_ens == media(lordi)`, residuo 1,4e-17). + +E il lead dello spot (Β§52) **muore dove servirebbe**: regge a $600, **si annulla a $272k** β€” sparisce +esattamente al muro che pretende di spostare. + +## 6. MDE: quanti Β«SCARTATIΒ» sono in realta' Β«non misurabileΒ» + +Il critico ha misurato la finestra della catena opzioni: **77 giorni di superficie** β†’ **MDE 4,3 di +Sharpe**. Ogni SCARTATO di Β§4/Β§5/Β§7/Β§11 che poggia su un confronto di Sharpe e' un **non-risultato +su quell'asse** (le loro parti valide sono lead-lag, placebo e censimenti). L'ondata dichiara il +proprio MDE in Β§6, Β§10, Β§43, Β§48, Β§56 β€” **disciplina reale** β€” ma tre etichette restano piu' forti +della misura, e una conta: 🚨 **Β§21 XS01 fuori campione, il PILASTRO del canale funded, ha effetto ++1,12 contro un MDE di 1,13.** Sta *esattamente* al proprio limite di rilevabilita', e viene +propagato fino a P(β‰₯50 €/g) come se fosse noto a due cifre. *(Equita': gira anche un null di +permutazione a fee zero, p=0,013, che ha piu' potenza del t-stat. Il problema non e' Β«nessuna +evidenzaΒ», e' la precisione attribuita al livello.)* + +## 7. La risposta al mandato + +| lead | €/giorno a $635 | eseguibile oggi | +|---|---|---| +| gradino di leva 1,25x | **+0,046** | no β€” la manopola non esiste | +| spot al posto del perpetual | +0,017 | no β€” domanda fiscale aperta | +| PREVDAY al 15% (de-luckato) | +0,014 | no β€” gira la cella sbagliata | +| binario di frequenza | +0,011 | no β€” 3 gate su 4 falliti | +| esecuzione passiva | +0,005 | no β€” l'IC contiene lo zero | +| **funding (costo scoperto)** | **βˆ’0,023** | **lo paghi gia'** | + +**Somma di tutti i lead positivi, se fossero autorizzati e additivi β€” non lo sono, e nessuno e' +eseguibile oggi: +0,036 €/giorno.** Sullo stesso motore e sugli stessi percorsi, **€250 β†’ €500 al +mese porta P(20a) dal 14% all'85%** e la mediana da 23,4 a 17,3 anni. **Rapporto ~1.400 a 1.** + +🚨 **`L'ONDATA NON HA PRODOTTO NIENTE CHE AVVICINI I 50 €/GIORNO.`** Ha prodotto (i) un **costo gia' +in essere** che porta P(20a) a €250/mese dal 49% al 14% (il funding), (ii) **quattro monitor rotti** +che avrebbero fatto decidere un gate al contrario, (iii) un **punto singolo di guasto** nel trasporto +degli allarmi, (iv) **tre lead reali tutti bloccati da qualcosa che non e' la ricerca**. +**Il suo valore e' difensivo ed e' reale: ha impedito decisioni sbagliate.** E nulla di sepolto sotto +uno Β«SCARTATOΒ» merita di essere riesumato. + +## 8. A credito dell'ondata + +**Quattro auto-correzioni in 24 ore**, ed e' il suo dato piu' sano: Β§45 Β«lo spot non si liquidaΒ» +falsificato da Β§52; Β§39 Β«non esiste una linea USDC datataΒ» falsificato lo stesso giorno; la regola +Β«overlay non causaleΒ» **ritirata** dallo scettico; Β«`dealer_net_gamma` e' il GEX invertitoΒ» ritirata +**leggendo il sorgente** che gira su questa stessa VPS. + +## 9. Cosa resta all'operatore + +Sette decisioni, nessuna delle quali richiede altra ricerca. Brief: +`https://claude.ai/code/artifact/9ca378f7-464a-4ee1-a520-38a026a34fc3` + +1. **Riparare `advance()` e rigenerare** prima del gate STATARB del **27/09** (35 giorni) β€” oggi il + gate si deciderebbe **al contrario** (Sharpe +1,96 registrato contro βˆ’2,02 vero). +2. **Che fare del branch** `research/wave-0822` (106 commit, tutti in `scripts/research/` e `docs/`). +3. **Chiedere alla firm** se ha una lista ristretta propria (costo €0, blocca una spesa di $579). +4. **Chiedere al commercialista** se i derivati Deribit sono al 26% o al 33% (vale ~$22k di muro). +5. **Costruire la chiave di scala** in config, se si vuole il gradino **1,25x** (e **solo** quello). +6. **Riparare il trasporto degli allarmi** β€” la parte (c) cambia il comportamento, quindi e' una scelta. +7. **Decidere di `paper_prevday`**: allinearlo alla cella onesta, ritirarlo, o lasciarlo scrivendo + che la sua finestra non e' evidenza. + +## Regole nuove + +1. **Un muro calcolato come `X/y` con `y` stimato va pubblicato con la banda di `y`**, non con la + risoluzione del simulatore β€” e se `y` si annulla prima del suo ingresso, l'errore e' **asimmetrico** + e la banda non e' simmetrica attorno al punto. +2. **Β«N/N ancoreΒ» e' robustezza alla SCELTA dell'ancora, ~2 osservazioni indipendenti β€” non N prove.** + La banda d'ancora **non e' un intervallo di confidenza** e non va usata al posto di uno; e **non si + attacca un p-value** a un conteggio di ancore o di finestre sovrapposte. +3. **Un Ξ” si scrive con la sua BASELINE attaccata.** Tre lenti dello stesso libro distano 0,12 di + Sharpe, piu' della maggior parte dei Ξ” misurati: la lente sbagliata cambia piu' del risultato. +4. **Un argomento refutato due volte per due strade indipendenti non va ereditato come regola** + (il Β«soffitto ~1,3Β» e l'aritmetica del DSR di screen che ne discendeva). +5. **Prima di chiudere una famiglia, confrontare l'etichetta con il proprio MDE dichiarato** β€” Β§48 + chiude un filone su una branca che dichiara essa stessa un fattore 44 sotto l'MDE. +6. **Un contatore cumulativo dentro un simulatore va fermato all'evento che sta misurando**, o + risponde a una domanda diversa da quella posta (2Βͺ occorrenza in un mese, in due punti diversi). diff --git a/docs/journal/2026-06-23.md b/docs/journal/2026-06-23.md new file mode 100644 index 0000000..f483424 --- /dev/null +++ b/docs/journal/2026-06-23.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-06-23 + +*Scritto 2026-08-23T13:34:09+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,684.00 | -1.99% | -4.48% | -18.57% | 42.8% | ↓ ↓ ↓ (0/3 su) | 41.6 (27Β° pctl 1a) | +| **ETH** | $1,665.60 | -3.52% | -7.05% | -20.59% | 63.4% | ↓ ↓ ↓ (0/3 su) | 56.2 (21Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-06-23 Β· 2 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (2 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 2/24 β€” **22 mancanti** +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 41.6 contro realizzata 30g 42.8 (sotto); DVOL al 27Β° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 56.2 contro realizzata 30g 63.4 (sotto); DVOL al 21Β° pctl di un anno; IV-rank espandente 0.15 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[giri]` ⚠️ **22 giri di `book_execute` mancanti** su 24: in quelle ore il libro non ha ne' letto ne' eseguito. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-06-24.md b/docs/journal/2026-06-24.md new file mode 100644 index 0000000..f182d50 --- /dev/null +++ b/docs/journal/2026-06-24.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-06-24 + +*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $60,977.50 | -2.72% | -5.39% | -21.09% | 43.3% | ↓ ↓ ↓ (0/3 su) | 45.1 (51Β° pctl 1a) | +| **ETH** | $1,619.90 | -2.74% | -7.43% | -23.29% | 63.6% | ↓ ↓ ↓ (0/3 su) | 58.8 (28Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-06-24 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 45.1 contro realizzata 30g 43.3 (sopra); DVOL al 51Β° pctl di un anno; IV-rank espandente 0.22 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 58.8 contro realizzata 30g 63.6 (sotto); DVOL al 28Β° pctl di un anno; IV-rank espandente 0.19 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **1 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-06-25.md b/docs/journal/2026-06-25.md new file mode 100644 index 0000000..8d77882 --- /dev/null +++ b/docs/journal/2026-06-25.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-06-25 + +*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $59,702.00 | -2.09% | -5.08% | -21.25% | 43.3% | ↓ ↓ ↓ (0/3 su) | 46.6 (57Β° pctl 1a) | +| **ETH** | $1,564.90 | -3.40% | -8.47% | -24.43% | 64.1% | ↓ ↓ ↓ (0/3 su) | 60.8 (33Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-06-25 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 46.6 contro realizzata 30g 43.3 (sopra); DVOL al 57Β° pctl di un anno; IV-rank espandente 0.24 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 60.8 contro realizzata 30g 64.1 (sotto); DVOL al 33Β° pctl di un anno; IV-rank espandente 0.22 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **2 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-06-26.md b/docs/journal/2026-06-26.md new file mode 100644 index 0000000..a04a7ab --- /dev/null +++ b/docs/journal/2026-06-26.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-06-26 + +*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $60,017.00 | +0.53% | -5.47% | -19.28% | 43.4% | ↓ ↓ ↓ (0/3 su) | 45.5 (52Β° pctl 1a) | +| **ETH** | $1,576.05 | +0.71% | -7.82% | -22.08% | 64.1% | ↓ ↓ ↓ (0/3 su) | 59.8 (30Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-06-26 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 45.5 contro realizzata 30g 43.4 (sopra); DVOL al 52Β° pctl di un anno; IV-rank espandente 0.23 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 59.8 contro realizzata 30g 64.1 (sotto); DVOL al 30Β° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **3 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-06-27.md b/docs/journal/2026-06-27.md new file mode 100644 index 0000000..ab7f0f4 --- /dev/null +++ b/docs/journal/2026-06-27.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-06-27 + +*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $59,956.00 | -0.10% | -6.67% | -18.47% | 43.4% | ↓ ↓ ↓ (0/3 su) | 45.2 (52Β° pctl 1a) | +| **ETH** | $1,570.80 | -0.33% | -9.67% | -21.76% | 64.2% | ↓ ↓ ↓ (0/3 su) | 59.9 (31Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-06-27 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 45.2 contro realizzata 30g 43.4 (sopra); DVOL al 52Β° pctl di un anno; IV-rank espandente 0.22 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 59.9 contro realizzata 30g 64.2 (sotto); DVOL al 31Β° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **4 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-06-28.md b/docs/journal/2026-06-28.md new file mode 100644 index 0000000..41d1b8d --- /dev/null +++ b/docs/journal/2026-06-28.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-06-28 + +*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $59,503.50 | -0.75% | -5.92% | -18.92% | 43.4% | ↓ ↓ ↓ (0/3 su) | 47.7 (63Β° pctl 1a) | +| **ETH** | $1,569.20 | -0.10% | -8.01% | -22.04% | 64.1% | ↓ ↓ ↓ (0/3 su) | 61.3 (37Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-06-28 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 47.7 contro realizzata 30g 43.4 (sopra); DVOL al 63Β° pctl di un anno; IV-rank espandente 0.27 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 61.3 contro realizzata 30g 64.1 (sotto); DVOL al 37Β° pctl di un anno; IV-rank espandente 0.23 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **5 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-06-29.md b/docs/journal/2026-06-29.md new file mode 100644 index 0000000..ddecd8c --- /dev/null +++ b/docs/journal/2026-06-29.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-06-29 + +*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $60,166.50 | +1.11% | -5.92% | -18.46% | 43.6% | ↓ ↓ ↓ (0/3 su) | 42.6 (33Β° pctl 1a) | +| **ETH** | $1,611.20 | +2.68% | -6.68% | -20.24% | 65.1% | ↓ ↓ ↓ (0/3 su) | 57.9 (26Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-06-29 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (23 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 42.6 contro realizzata 30g 43.6 (sotto); DVOL al 33Β° pctl di un anno; IV-rank espandente 0.16 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 57.9 contro realizzata 30g 65.1 (sotto); DVOL al 26Β° pctl di un anno; IV-rank espandente 0.18 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **6 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-06-30.md b/docs/journal/2026-06-30.md new file mode 100644 index 0000000..0909516 --- /dev/null +++ b/docs/journal/2026-06-30.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-06-30 + +*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $58,546.50 | -2.69% | -6.60% | -20.44% | 44.2% | ↓ ↓ ↓ (0/3 su) | 44.5 (46Β° pctl 1a) | +| **ETH** | $1,570.55 | -2.52% | -5.71% | -21.67% | 65.4% | ↓ ↓ ↓ (0/3 su) | 59.0 (29Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-06-30 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 44.5 contro realizzata 30g 44.2 (sopra); DVOL al 46Β° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 59.0 contro realizzata 30g 65.4 (sotto); DVOL al 29Β° pctl di un anno; IV-rank espandente 0.19 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **7 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-01.md b/docs/journal/2026-07-01.md new file mode 100644 index 0000000..fa361de --- /dev/null +++ b/docs/journal/2026-07-01.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-07-01 + +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $59,961.50 | +2.42% | -1.67% | -15.93% | 44.6% | ↓ ↓ ↓ (0/3 su) | 42.9 (36Β° pctl 1a) | +| **ETH** | $1,607.95 | +2.38% | -0.74% | -19.81% | 66.3% | ↓ ↓ ↓ (0/3 su) | 56.9 (23Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-01 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 42.9 contro realizzata 30g 44.6 (sotto); DVOL al 36Β° pctl di un anno; IV-rank espandente 0.17 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 56.9 contro realizzata 30g 66.3 (sotto); DVOL al 23Β° pctl di un anno; IV-rank espandente 0.16 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **8 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-02.md b/docs/journal/2026-07-02.md new file mode 100644 index 0000000..8ef19ba --- /dev/null +++ b/docs/journal/2026-07-02.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-07-02 + +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $61,518.50 | +2.60% | +3.04% | -7.71% | 40.0% | ↓ ↓ ↓ (0/3 su) | 40.5 (21Β° pctl 1a) | +| **ETH** | $1,698.60 | +5.64% | +8.54% | -8.57% | 64.9% | ↓ ↓ ↓ (0/3 su) | 55.1 (16Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-02 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 40.5 contro realizzata 30g 40.0 (sopra); DVOL al 21Β° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 55.1 contro realizzata 30g 64.9 (sotto); DVOL al 16Β° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **9 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-03.md b/docs/journal/2026-07-03.md new file mode 100644 index 0000000..bc4051d --- /dev/null +++ b/docs/journal/2026-07-03.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-07-03 + +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,542.00 | +1.66% | +4.21% | -2.28% | 38.1% | ↓ ↓ ↓ (0/3 su) | 39.5 (14Β° pctl 1a) | +| **ETH** | $1,757.15 | +3.45% | +11.49% | -2.98% | 65.6% | ↓ ↓ ↓ (0/3 su) | 53.5 (8Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-03 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 39.5 contro realizzata 30g 38.1 (sopra); DVOL al 14Β° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 53.5 contro realizzata 30g 65.6 (sotto); DVOL al 8Β° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **10 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-04.md b/docs/journal/2026-07-04.md new file mode 100644 index 0000000..9137f61 --- /dev/null +++ b/docs/journal/2026-07-04.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-07-04 + +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,080.00 | +0.86% | +5.21% | -1.15% | 38.2% | ↓ ↓ ↓ (0/3 su) | 39.2 (13Β° pctl 1a) | +| **ETH** | $1,779.40 | +1.27% | +13.28% | +0.56% | 65.3% | ↑ ↓ ↓ (1/3 su) | 53.0 (6Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-04 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 39.2 contro realizzata 30g 38.2 (sopra); DVOL al 13Β° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 53.0 contro realizzata 30g 65.3 (sotto); DVOL al 6Β° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **11 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-05.md b/docs/journal/2026-07-05.md new file mode 100644 index 0000000..2c93000 --- /dev/null +++ b/docs/journal/2026-07-05.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-07-05 + +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,624.00 | +0.86% | +6.92% | +4.22% | 34.9% | ↑ ↓ ↓ (1/3 su) | 38.7 (10Β° pctl 1a) | +| **ETH** | $1,784.95 | +0.31% | +13.75% | +12.77% | 51.3% | ↑ ↓ ↓ (1/3 su) | 53.1 (6Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-05 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 38.7 contro realizzata 30g 34.9 (sopra); DVOL al 10Β° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 53.1 contro realizzata 30g 51.3 (sopra); DVOL al 6Β° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **12 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-06.md b/docs/journal/2026-07-06.md new file mode 100644 index 0000000..ee25ee5 --- /dev/null +++ b/docs/journal/2026-07-06.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-07-06 + +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,012.00 | +0.61% | +6.39% | +5.19% | 34.8% | ↑ ↓ ↓ (1/3 su) | 37.3 (4Β° pctl 1a) | +| **ETH** | $1,798.85 | +0.78% | +11.65% | +14.67% | 51.1% | ↑ ↓ ↓ (1/3 su) | 52.6 (6Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-06 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 37.3 contro realizzata 30g 34.8 (sopra); DVOL al 4Β° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 52.6 contro realizzata 30g 51.1 (sopra); DVOL al 6Β° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **13 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-07.md b/docs/journal/2026-07-07.md new file mode 100644 index 0000000..30cfe12 --- /dev/null +++ b/docs/journal/2026-07-07.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-07-07 + +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,322.00 | -1.08% | +8.16% | +0.03% | 32.3% | ↑ ↓ ↓ (1/3 su) | 38.9 (12Β° pctl 1a) | +| **ETH** | $1,770.30 | -1.59% | +12.72% | +4.79% | 45.0% | ↑ ↓ ↓ (1/3 su) | 53.3 (7Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-07 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (23 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 38.9 contro realizzata 30g 32.3 (sopra); DVOL al 12Β° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 53.3 contro realizzata 30g 45.0 (sopra); DVOL al 7Β° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **14 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-08.md b/docs/journal/2026-07-08.md new file mode 100644 index 0000000..4ee878a --- /dev/null +++ b/docs/journal/2026-07-08.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-07-08 + +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,259.00 | -1.68% | +3.83% | -1.27% | 32.8% | ↓ ↓ ↓ (0/3 su) | 39.5 (16Β° pctl 1a) | +| **ETH** | $1,742.25 | -1.58% | +8.35% | +3.14% | 45.4% | ↑ ↓ ↓ (1/3 su) | 53.4 (8Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-08 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 39.5 contro realizzata 30g 32.8 (sopra); DVOL al 16Β° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 53.4 contro realizzata 30g 45.4 (sopra); DVOL al 8Β° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **15 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-09.md b/docs/journal/2026-07-09.md new file mode 100644 index 0000000..d3aba1e --- /dev/null +++ b/docs/journal/2026-07-09.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-07-09 + +*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,200.00 | +1.51% | +2.73% | +2.42% | 32.3% | ↑ ↓ ↓ (1/3 su) | 37.3 (5Β° pctl 1a) | +| **ETH** | $1,744.25 | +0.11% | +2.69% | +6.49% | 43.9% | ↑ ↓ ↓ (1/3 su) | 52.0 (5Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-08 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (15 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 37.3 contro realizzata 30g 32.3 (sopra); DVOL al 5Β° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 52.0 contro realizzata 30g 43.9 (sopra); DVOL al 5Β° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **16 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-10.md b/docs/journal/2026-07-10.md new file mode 100644 index 0000000..0dd9c19 --- /dev/null +++ b/docs/journal/2026-07-10.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-07-10 + +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,136.00 | +1.48% | +2.55% | +4.36% | 32.6% | ↑ ↓ ↓ (1/3 su) | 36.2 (2Β° pctl 1a) | +| **ETH** | $1,796.05 | +2.97% | +2.21% | +10.82% | 44.7% | ↑ ↓ ↓ (1/3 su) | 49.8 (4Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-08 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 36.2 contro realizzata 30g 32.6 (sopra); DVOL al 2Β° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.8 contro realizzata 30g 44.7 (sopra); DVOL al 4Β° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **17 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-11.md b/docs/journal/2026-07-11.md new file mode 100644 index 0000000..2eb29ff --- /dev/null +++ b/docs/journal/2026-07-11.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-07-11 + +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,804.50 | -0.52% | +1.15% | +0.36% | 30.5% | ↑ ↓ ↓ (1/3 su) | 36.8 (4Β° pctl 1a) | +| **ETH** | $1,787.15 | -0.50% | +0.44% | +6.93% | 43.7% | ↑ ↓ ↓ (1/3 su) | 50.6 (4Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-08 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 36.8 contro realizzata 30g 30.5 (sopra); DVOL al 4Β° pctl di un anno; IV-rank espandente 0.04 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 50.6 contro realizzata 30g 43.7 (sopra); DVOL al 4Β° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **18 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-12.md b/docs/journal/2026-07-12.md new file mode 100644 index 0000000..f1241d9 --- /dev/null +++ b/docs/journal/2026-07-12.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-07-12 + +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,759.50 | -0.07% | +0.21% | +0.33% | 30.5% | ↑ ↓ ↓ (1/3 su) | 37.2 (5Β° pctl 1a) | +| **ETH** | $1,805.70 | +1.04% | +1.16% | +8.40% | 43.7% | ↑ ↓ ↓ (1/3 su) | 51.0 (5Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-08 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 37.2 contro realizzata 30g 30.5 (sopra); DVOL al 5Β° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.0 contro realizzata 30g 43.7 (sopra); DVOL al 5Β° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **19 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-13.md b/docs/journal/2026-07-13.md new file mode 100644 index 0000000..bfc5538 --- /dev/null +++ b/docs/journal/2026-07-13.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-07-13 + +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,265.50 | -2.34% | -2.73% | -3.34% | 31.2% | ↓ ↓ ↓ (0/3 su) | 37.8 (8Β° pctl 1a) | +| **ETH** | $1,775.55 | -1.67% | -1.30% | +5.67% | 44.2% | ↑ ↓ ↓ (1/3 su) | 51.1 (6Β° pctl 1a) | + +## Libro + +Equity **$598.06** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-08 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+0.00** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 37.8 contro realizzata 30g 31.2 (sopra); DVOL al 8Β° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.1 contro realizzata 30g 44.2 (sopra); DVOL al 6Β° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **20 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-14.md b/docs/journal/2026-07-14.md new file mode 100644 index 0000000..18f5ca1 --- /dev/null +++ b/docs/journal/2026-07-14.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-07-14 + +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $65,012.00 | +4.41% | +2.67% | -1.08% | 34.1% | ↓ ↓ ↓ (0/3 su) | 36.5 (3Β° pctl 1a) | +| **ETH** | $1,889.60 | +6.42% | +6.74% | +9.53% | 48.3% | ↑ ↓ ↓ (1/3 su) | 49.5 (3Β° pctl 1a) | + +## Libro + +Equity **$598.49** Β· nozionale lordo $75 Β· leva lorda 0.13x Β· barra dati 2026-07-08 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | LONG @ 1,868.9 | $+75 | $+75 | HOLD (a target) | + +## P&L + +- giorno: **$+0.43** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 1 fill Β· fee 0.0374 +- cumulato dall'arming: **$+0.43** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su ETH $+75 (TP01 +0.000, SKH01 long). +- `[leva]` Leva lorda **0.13x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.87x. +- `[pnl]` Equity $+0.43 con 1 fill e 0 round-trip chiusi (realizzato $+0.00 netto). +- `[vol]` BTC: implicita 36.5 contro realizzata 30g 34.1 (sopra); DVOL al 3Β° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.5 contro realizzata 30g 48.3 (sopra); DVOL al 3Β° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evento]` πŸ“Œ BTC: giornata a +4.41%, **2.5 deviazioni** giornaliere (sd implicita dalla RV30 = 1.78%). +- `[evento]` πŸ“Œ ETH: giornata a +6.42%, **2.5 deviazioni** giornaliere (sd implicita dalla RV30 = 2.53%). +- `[evidenza]` πŸ“Œ Campione a oggi: **21 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-15.md b/docs/journal/2026-07-15.md new file mode 100644 index 0000000..fae8da4 --- /dev/null +++ b/docs/journal/2026-07-15.md @@ -0,0 +1,45 @@ +# Giornale di bordo β€” 2026-07-15 + +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,734.50 | -0.43% | +3.98% | -2.35% | 34.0% | ↓ ↓ ↓ (0/3 su) | 36.0 (1Β° pctl 1a) | +| **ETH** | $1,916.70 | +1.43% | +10.01% | +6.73% | 46.6% | ↑ ↓ ↓ (1/3 su) | 49.0 (2Β° pctl 1a) | + +## Libro + +Equity **$600.14** Β· nozionale lordo $77 Β· leva lorda 0.13x Β· barra dati 2026-07-15 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | LONG @ 1,868.9 | $+75 | $+77 | HOLD (a target) | + +## P&L + +- giorno: **$+1.65** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$+2.08** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su ETH $+77 (TP01 +0.000, SKH01 long). +- `[leva]` Leva lorda **0.13x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.87x. +- `[pnl]` Equity $+1.65 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 36.0 contro realizzata 30g 34.0 (sopra); DVOL al 1Β° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.0 contro realizzata 30g 46.6 (sopra); DVOL al 2Β° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **22 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-16.md b/docs/journal/2026-07-16.md new file mode 100644 index 0000000..9aabae7 --- /dev/null +++ b/docs/journal/2026-07-16.md @@ -0,0 +1,45 @@ +# Giornale di bordo β€” 2026-07-16 + +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,804.50 | -1.44% | +0.96% | -2.77% | 34.1% | ↓ ↓ ↓ (0/3 su) | 36.4 (3Β° pctl 1a) | +| **ETH** | $1,863.55 | -2.77% | +6.84% | +4.00% | 47.7% | ↑ ↓ ↓ (1/3 su) | 49.2 (2Β° pctl 1a) | + +## Libro + +Equity **$597.93** Β· nozionale lordo $75 Β· leva lorda 0.13x Β· barra dati 2026-07-16 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | LONG @ 1,868.9 | $+75 | $+75 | HOLD (a target) | + +## P&L + +- giorno: **$-2.21** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-0.13** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su ETH $+75 (TP01 +0.000, SKH01 long). +- `[leva]` Leva lorda **0.13x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.87x. +- `[pnl]` Equity $-2.21 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[vol]` BTC: implicita 36.4 contro realizzata 30g 34.1 (sopra); DVOL al 3Β° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.2 contro realizzata 30g 47.7 (sopra); DVOL al 2Β° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **23 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-17.md b/docs/journal/2026-07-17.md new file mode 100644 index 0000000..948b216 --- /dev/null +++ b/docs/journal/2026-07-17.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-07-17 + +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,904.00 | +0.16% | -0.36% | -0.85% | 33.6% | ↓ ↓ ↓ (0/3 su) | 36.3 (3Β° pctl 1a) | +| **ETH** | $1,841.00 | -1.21% | +2.50% | +5.21% | 47.1% | ↑ ↓ ↓ (1/3 su) | 49.0 (2Β° pctl 1a) | + +## Libro + +Equity **$596.77** Β· nozionale lordo $74 Β· leva lorda 0.12x Β· barra dati 2026-07-17 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | LONG @ 1,868.9 | $+75 | $+74 | HOLD (a target) | + +## P&L + +- giorno: **$-1.16** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.29** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su ETH $+74 (TP01 +0.000, SKH01 long). +- `[leva]` Leva lorda **0.12x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.88x. +- `[pnl]` Equity $-1.16 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.63%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 36.3 contro realizzata 30g 33.6 (sopra); DVOL al 3Β° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.0 contro realizzata 30g 47.1 (sopra); DVOL al 2Β° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **24 giorni, 0 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-18.md b/docs/journal/2026-07-18.md new file mode 100644 index 0000000..2cfb7bd --- /dev/null +++ b/docs/journal/2026-07-18.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-07-18 + +*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,810.00 | +1.42% | +1.58% | +3.04% | 32.8% | ↑ ↓ ↓ (1/3 su) | 35.7 (1Β° pctl 1a) | +| **ETH** | $1,861.75 | +1.13% | +4.17% | +8.89% | 46.4% | ↑ ↓ ↓ (1/3 su) | 49.3 (4Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-18 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.15** di equity (24 letture) +- realizzato: -1.10 netto su 1 round-trip chiusi Β· 1 fill Β· fee 0.0369 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.15 con 1 fill e 1 round-trip chiusi (realizzato $-1.10 netto). +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.7 contro realizzata 30g 32.8 (sopra); DVOL al 1Β° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.3 contro realizzata 30g 46.4 (sopra); DVOL al 4Β° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **25 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-19.md b/docs/journal/2026-07-19.md new file mode 100644 index 0000000..773bbf8 --- /dev/null +++ b/docs/journal/2026-07-19.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-07-19 + +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,704.50 | -0.16% | +1.48% | +1.92% | 32.7% | ↑ ↓ ↓ (1/3 su) | 36.1 (3Β° pctl 1a) | +| **ETH** | $1,871.60 | +0.53% | +3.65% | +9.47% | 46.4% | ↑ ↓ ↓ (1/3 su) | 49.7 (6Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-19 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 36.1 contro realizzata 30g 32.7 (sopra); DVOL al 3Β° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.7 contro realizzata 30g 46.4 (sopra); DVOL al 6Β° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **26 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-20.md b/docs/journal/2026-07-20.md new file mode 100644 index 0000000..e02747b --- /dev/null +++ b/docs/journal/2026-07-20.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-07-20 + +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $65,216.50 | +0.79% | +4.74% | +1.52% | 32.5% | ↑ ↓ ↓ (1/3 su) | 36.3 (4Β° pctl 1a) | +| **ETH** | $1,903.45 | +1.70% | +7.20% | +9.46% | 46.4% | ↑ ↓ ↓ (1/3 su) | 50.4 (7Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-20 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 36.3 contro realizzata 30g 32.5 (sopra); DVOL al 4Β° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 50.4 contro realizzata 30g 46.4 (sopra); DVOL al 7Β° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **27 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-21.md b/docs/journal/2026-07-21.md new file mode 100644 index 0000000..ce7c316 --- /dev/null +++ b/docs/journal/2026-07-21.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-07-21 + +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $66,532.50 | +2.02% | +2.34% | +5.19% | 32.7% | ↑ ↓ ↓ (1/3 su) | 37.3 (10Β° pctl 1a) | +| **ETH** | $1,929.10 | +1.35% | +2.09% | +13.09% | 45.8% | ↑ ↓ ↓ (1/3 su) | 51.0 (8Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-21 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 37.3 contro realizzata 30g 32.7 (sopra); DVOL al 10Β° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.0 contro realizzata 30g 45.8 (sopra); DVOL al 8Β° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **28 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-22.md b/docs/journal/2026-07-22.md new file mode 100644 index 0000000..ef2bf00 --- /dev/null +++ b/docs/journal/2026-07-22.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-07-22 + +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $66,081.00 | -0.68% | +2.08% | +3.32% | 32.6% | ↑ ↓ ↓ (1/3 su) | 38.0 (12Β° pctl 1a) | +| **ETH** | $1,933.70 | +0.24% | +0.89% | +12.00% | 45.7% | ↑ ↓ ↓ (1/3 su) | 51.6 (10Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-22 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 38.0 contro realizzata 30g 32.6 (sopra); DVOL al 12Β° pctl di un anno; IV-rank espandente 0.07 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.6 contro realizzata 30g 45.7 (sopra); DVOL al 10Β° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **29 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-23.md b/docs/journal/2026-07-23.md new file mode 100644 index 0000000..29fff8f --- /dev/null +++ b/docs/journal/2026-07-23.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-07-23 + +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $65,060.00 | -1.55% | +1.97% | +3.79% | 32.3% | ↑ ↓ ↓ (1/3 su) | 38.5 (14Β° pctl 1a) | +| **ETH** | $1,877.45 | -2.91% | +0.75% | +12.72% | 45.1% | ↑ ↓ ↓ (1/3 su) | 51.6 (10Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-23 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 38.5 contro realizzata 30g 32.3 (sopra); DVOL al 14Β° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.6 contro realizzata 30g 45.1 (sopra); DVOL al 10Β° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **30 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-24.md b/docs/journal/2026-07-24.md new file mode 100644 index 0000000..f34466b --- /dev/null +++ b/docs/journal/2026-07-24.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-07-24 + +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,103.50 | -1.47% | +0.31% | +5.13% | 31.1% | ↑ ↓ ↓ (1/3 su) | 37.0 (7Β° pctl 1a) | +| **ETH** | $1,860.50 | -0.90% | +1.06% | +14.85% | 43.9% | ↑ ↓ ↓ (1/3 su) | 50.5 (7Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-24 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: non misurata + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 37.0 contro realizzata 30g 31.1 (sopra); DVOL al 7Β° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 50.5 contro realizzata 30g 43.9 (sopra); DVOL al 7Β° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **31 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-25.md b/docs/journal/2026-07-25.md new file mode 100644 index 0000000..b396e2b --- /dev/null +++ b/docs/journal/2026-07-25.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-07-25 + +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,327.50 | +0.35% | -0.74% | +7.75% | 30.0% | ↑ ↓ ↓ (1/3 su) | 37.7 (11Β° pctl 1a) | +| **ETH** | $1,873.45 | +0.70% | +0.63% | +19.72% | 41.5% | ↑ ↓ ↓ (1/3 su) | 51.5 (10Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-25 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 37.7 contro realizzata 30g 30.0 (sopra); DVOL al 11Β° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.5 contro realizzata 30g 41.5 (sopra); DVOL al 10Β° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **32 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-26.md b/docs/journal/2026-07-26.md new file mode 100644 index 0000000..40a4833 --- /dev/null +++ b/docs/journal/2026-07-26.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-07-26 + +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $65,357.50 | +1.60% | +1.01% | +8.90% | 30.4% | ↑ ↓ ↓ (1/3 su) | 37.3 (10Β° pctl 1a) | +| **ETH** | $1,953.65 | +4.28% | +4.38% | +23.96% | 43.4% | ↑ ↓ ↓ (1/3 su) | 51.7 (11Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-26 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 37.3 contro realizzata 30g 30.4 (sopra); DVOL al 10Β° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.7 contro realizzata 30g 43.4 (sopra); DVOL al 11Β° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **33 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-27.md b/docs/journal/2026-07-27.md new file mode 100644 index 0000000..4913476 --- /dev/null +++ b/docs/journal/2026-07-27.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-07-27 + +*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,710.50 | -2.52% | -2.31% | +6.26% | 31.9% | ↑ ↓ ↓ (1/3 su) | 37.5 (11Β° pctl 1a) | +| **ETH** | $1,891.00 | -3.21% | -0.65% | +20.38% | 45.4% | ↑ ↓ ↓ (1/3 su) | 51.9 (12Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-27 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 37.5 contro realizzata 30g 31.9 (sopra); DVOL al 11Β° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 51.9 contro realizzata 30g 45.4 (sopra); DVOL al 12Β° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **34 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-28.md b/docs/journal/2026-07-28.md new file mode 100644 index 0000000..20f040e --- /dev/null +++ b/docs/journal/2026-07-28.md @@ -0,0 +1,46 @@ +# Giornale di bordo β€” 2026-07-28 + +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,874.50 | +0.26% | -4.00% | +7.35% | 31.7% | ↑ ↓ ↓ (1/3 su) | 37.2 (8Β° pctl 1a) | +| **ETH** | $1,920.40 | +1.55% | -0.45% | +22.38% | 45.5% | ↑ ↓ ↓ (1/3 su) | 53.0 (14Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-28 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 0.50x (`max_notional_per_asset_frac` 0.50 x 1 asset, letto da `config/live.json`): margine 0.50x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 37.2 contro realizzata 30g 31.7 (sopra); DVOL al 8Β° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 53.0 contro realizzata 30g 45.5 (sopra); DVOL al 14Β° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **35 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-29.md b/docs/journal/2026-07-29.md new file mode 100644 index 0000000..91b6599 --- /dev/null +++ b/docs/journal/2026-07-29.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-07-29 + +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,933.00 | +0.09% | -3.25% | +6.26% | 31.6% | ↑ ↓ ↓ (1/3 su) | 37.5 (11Β° pctl 1a) | +| **ETH** | $1,909.10 | -0.59% | -1.27% | +18.49% | 45.1% | ↑ ↓ ↓ (1/3 su) | 52.5 (13Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-29 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (21 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 37.5 contro realizzata 30g 31.6 (sopra); DVOL al 11Β° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 52.5 contro realizzata 30g 45.1 (sopra); DVOL al 13Β° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **36 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-30.md b/docs/journal/2026-07-30.md new file mode 100644 index 0000000..bf23ec3 --- /dev/null +++ b/docs/journal/2026-07-30.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-07-30 + +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,745.50 | +1.27% | -0.48% | +10.59% | 30.0% | ↑ ↓ ↓ (1/3 su) | 35.5 (1Β° pctl 1a) | +| **ETH** | $1,917.45 | +0.44% | +2.13% | +22.09% | 43.7% | ↑ ↓ ↓ (1/3 su) | 50.7 (8Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-30 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.5 contro realizzata 30g 30.0 (sopra); DVOL al 1Β° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 50.7 contro realizzata 30g 43.7 (sopra); DVOL al 8Β° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **37 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-07-31.md b/docs/journal/2026-07-31.md new file mode 100644 index 0000000..39d17d6 --- /dev/null +++ b/docs/journal/2026-07-31.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-07-31 + +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,837.00 | -2.95% | -1.98% | +4.80% | 31.2% | ↑ ↓ ↓ (1/3 su) | 35.6 (1Β° pctl 1a) | +| **ETH** | $1,861.50 | -2.92% | +0.05% | +15.77% | 45.0% | ↑ ↓ ↓ (1/3 su) | 50.2 (6Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-07-31 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.6 contro realizzata 30g 31.2 (sopra); DVOL al 1Β° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 50.2 contro realizzata 30g 45.0 (sopra); DVOL al 6Β° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **38 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-01.md b/docs/journal/2026-08-01.md new file mode 100644 index 0000000..b14d70a --- /dev/null +++ b/docs/journal/2026-08-01.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-01 + +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,776.00 | -0.10% | -2.41% | +2.04% | 29.9% | ↑ ↓ ↓ (1/3 su) | 35.5 (1Β° pctl 1a) | +| **ETH** | $1,843.25 | -0.98% | -1.61% | +8.52% | 41.5% | ↑ ↓ ↓ (1/3 su) | 50.8 (9Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-01 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.5 contro realizzata 30g 29.9 (sopra); DVOL al 1Β° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 50.8 contro realizzata 30g 41.5 (sopra); DVOL al 9Β° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **39 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-02.md b/docs/journal/2026-08-02.md new file mode 100644 index 0000000..4ac6421 --- /dev/null +++ b/docs/journal/2026-08-02.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-02 + +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,524.50 | +1.19% | -2.80% | +1.57% | 29.7% | ↑ ↓ ↓ (1/3 su) | 35.0 (0Β° pctl 1a) | +| **ETH** | $1,883.90 | +2.21% | -3.57% | +7.21% | 40.5% | ↑ ↓ ↓ (1/3 su) | 49.7 (6Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-02 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.0 contro realizzata 30g 29.7 (sopra); DVOL al 0Β° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.7 contro realizzata 30g 40.5 (sopra); DVOL al 6Β° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **40 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-03.md b/docs/journal/2026-08-03.md new file mode 100644 index 0000000..ebdac6a --- /dev/null +++ b/docs/journal/2026-08-03.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-03 + +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,485.50 | -0.06% | -0.35% | +0.64% | 29.5% | ↑ ↓ ↓ (1/3 su) | 34.8 (0Β° pctl 1a) | +| **ETH** | $1,859.25 | -1.31% | -1.68% | +4.49% | 40.7% | ↑ ↓ ↓ (1/3 su) | 49.2 (2Β° pctl 1a) | + +## Libro + +Equity **$596.92** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-03 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-1.14** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.8 contro realizzata 30g 29.5 (sopra); DVOL al 0Β° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.2 contro realizzata 30g 40.7 (sopra); DVOL al 2Β° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **41 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-04.md b/docs/journal/2026-08-04.md new file mode 100644 index 0000000..5da9ac9 --- /dev/null +++ b/docs/journal/2026-08-04.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-04 + +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,054.50 | +0.90% | +0.28% | +0.68% | 29.5% | ↑ ↓ ↑ (2/3 su) | 34.0 (0Β° pctl 1a) | +| **ETH** | $1,868.75 | +0.51% | -2.69% | +4.69% | 40.7% | ↑ ↓ ↑ (2/3 su) | 47.8 (0Β° pctl 1a) | + +## Libro + +Equity **$597.37** Β· nozionale lordo $37 Β· leva lorda 0.06x Β· barra dati 2026-08-04 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.162 | flat | $+36 | $+37 | HOLD (a target) | + +## P&L + +- giorno: **$+0.45** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 1 fill Β· fee 0.0129 +- cumulato dall'arming: **$-0.69** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su ETH $+37 (TP01 +0.162, SKH01 flat). +- `[disaccordo]` ETH: gli orizzonti del trend **non concordano** (2/3 al rialzo) e l'esposizione TP01 e' parziale (+0.162) β€” e' il meccanismo, non una scelta di oggi. +- `[leva]` Leva lorda **0.06x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.94x. +- `[pnl]` Equity $+0.45 con 1 fill e 0 round-trip chiusi (realizzato $+0.00 netto). +- `[drawdown]` πŸ“Œ Equity **-0.53%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.0 contro realizzata 30g 29.5 (sopra); DVOL al 0Β° pctl di un anno; IV-rank espandente 0.00 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 47.8 contro realizzata 30g 40.7 (sopra); DVOL al 0Β° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **42 giorni, 1 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-05.md b/docs/journal/2026-08-05.md new file mode 100644 index 0000000..e315f35 --- /dev/null +++ b/docs/journal/2026-08-05.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-05 + +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,607.50 | +0.86% | +1.06% | +0.93% | 29.6% | ↑ ↓ ↓ (1/3 su) | 34.7 (1Β° pctl 1a) | +| **ETH** | $1,907.45 | +2.07% | -0.09% | +6.04% | 41.2% | ↑ ↓ ↓ (1/3 su) | 48.0 (1Β° pctl 1a) | + +## Libro + +Equity **$597.12** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-05 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$-0.25** di equity (23 letture) +- realizzato: +0.21 netto su 1 round-trip chiusi Β· 1 fill Β· fee 0.0130 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $-0.25 con 1 fill e 1 round-trip chiusi (realizzato $+0.21 netto). +- `[drawdown]` πŸ“Œ Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.7 contro realizzata 30g 29.6 (sopra); DVOL al 1Β° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 48.0 contro realizzata 30g 41.2 (sopra); DVOL al 1Β° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **43 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-06.md b/docs/journal/2026-08-06.md new file mode 100644 index 0000000..2a189bb --- /dev/null +++ b/docs/journal/2026-08-06.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-06 + +*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,284.50 | -0.50% | -0.71% | +1.52% | 29.4% | ↑ ↓ ↓ (1/3 su) | 35.0 (2Β° pctl 1a) | +| **ETH** | $1,902.65 | -0.25% | -0.77% | +7.48% | 40.7% | ↑ ↓ ↓ (1/3 su) | 48.0 (1Β° pctl 1a) | + +## Libro + +Equity **$597.12** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-06 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.0 contro realizzata 30g 29.4 (sopra); DVOL al 2Β° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 48.0 contro realizzata 30g 40.7 (sopra); DVOL al 1Β° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **44 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-07.md b/docs/journal/2026-08-07.md new file mode 100644 index 0000000..3ca0c77 --- /dev/null +++ b/docs/journal/2026-08-07.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-07 + +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,884.00 | +0.93% | +3.26% | +4.22% | 28.9% | ↑ ↓ ↓ (1/3 su) | 33.9 (0Β° pctl 1a) | +| **ETH** | $1,913.50 | +0.57% | +2.79% | +9.83% | 40.2% | ↑ ↓ ↓ (1/3 su) | 47.4 (0Β° pctl 1a) | + +## Libro + +Equity **$597.12** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-07 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 33.9 contro realizzata 30g 28.9 (sopra); DVOL al 0Β° pctl di un anno; IV-rank espandente 0.00 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 47.4 contro realizzata 30g 40.2 (sopra); DVOL al 0Β° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **45 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-08.md b/docs/journal/2026-08-08.md new file mode 100644 index 0000000..461e859 --- /dev/null +++ b/docs/journal/2026-08-08.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-08 + +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,918.50 | +0.05% | +3.41% | +2.72% | 28.4% | ↑ ↓ ↓ (1/3 su) | 34.2 (1Β° pctl 1a) | +| **ETH** | $1,916.00 | +0.13% | +3.95% | +9.85% | 40.2% | ↑ ↓ ↓ (1/3 su) | 48.1 (2Β° pctl 1a) | + +## Libro + +Equity **$597.12** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-08 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.2 contro realizzata 30g 28.4 (sopra); DVOL al 1Β° pctl di un anno; IV-rank espandente 0.00 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 48.1 contro realizzata 30g 40.2 (sopra); DVOL al 2Β° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **46 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-09.md b/docs/journal/2026-08-09.md new file mode 100644 index 0000000..5b0d107 --- /dev/null +++ b/docs/journal/2026-08-09.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-09 + +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,869.00 | -0.08% | +2.12% | +1.14% | 28.0% | ↑ ↓ ↓ (1/3 su) | 34.9 (2Β° pctl 1a) | +| **ETH** | $1,909.50 | -0.34% | +1.36% | +6.32% | 39.1% | ↑ ↓ ↓ (1/3 su) | 49.1 (4Β° pctl 1a) | + +## Libro + +Equity **$597.12** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-09 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.9 contro realizzata 30g 28.0 (sopra); DVOL al 2Β° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.1 contro realizzata 30g 39.1 (sopra); DVOL al 4Β° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **47 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-10.md b/docs/journal/2026-08-10.md new file mode 100644 index 0000000..e2e5bc3 --- /dev/null +++ b/docs/journal/2026-08-10.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-10 + +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,931.50 | -1.45% | +0.70% | +0.20% | 28.4% | ↑ ↓ ↓ (1/3 su) | 36.3 (8Β° pctl 1a) | +| **ETH** | $1,871.25 | -2.00% | +0.65% | +4.71% | 39.8% | ↑ ↓ ↓ (1/3 su) | 49.7 (8Β° pctl 1a) | + +## Libro + +Equity **$597.12** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-10 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 36.3 contro realizzata 30g 28.4 (sopra); DVOL al 8Β° pctl di un anno; IV-rank espandente 0.04 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.7 contro realizzata 30g 39.8 (sopra); DVOL al 8Β° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **48 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-11.md b/docs/journal/2026-08-11.md new file mode 100644 index 0000000..8b59d1f --- /dev/null +++ b/docs/journal/2026-08-11.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-11 + +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,559.50 | -0.58% | -0.77% | -0.31% | 28.5% | ↓ ↓ ↓ (0/3 su) | 36.1 (8Β° pctl 1a) | +| **ETH** | $1,881.25 | +0.53% | +0.67% | +4.18% | 39.7% | ↑ ↓ ↓ (1/3 su) | 49.3 (7Β° pctl 1a) | + +## Libro + +Equity **$597.12** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-11 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (23 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 36.1 contro realizzata 30g 28.5 (sopra); DVOL al 8Β° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.3 contro realizzata 30g 39.7 (sopra); DVOL al 7Β° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **49 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-12.md b/docs/journal/2026-08-12.md new file mode 100644 index 0000000..f856002 --- /dev/null +++ b/docs/journal/2026-08-12.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-12 + +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,440.00 | -0.19% | -1.81% | +1.89% | 27.2% | ↑ ↓ ↓ (1/3 su) | 35.8 (6Β° pctl 1a) | +| **ETH** | $1,878.15 | -0.16% | -1.54% | +5.78% | 39.2% | ↑ ↓ ↓ (1/3 su) | 49.4 (8Β° pctl 1a) | + +## Libro + +Equity **$597.12** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-12 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.8 contro realizzata 30g 27.2 (sopra); DVOL al 6Β° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 49.4 contro realizzata 30g 39.2 (sopra); DVOL al 8Β° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **50 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-13.md b/docs/journal/2026-08-13.md new file mode 100644 index 0000000..cd86f50 --- /dev/null +++ b/docs/journal/2026-08-13.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-13 + +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,453.00 | +0.02% | -1.29% | -2.40% | 22.5% | ↓ ↓ ↓ (0/3 su) | 34.8 (2Β° pctl 1a) | +| **ETH** | $1,885.15 | +0.37% | -0.92% | -0.24% | 32.6% | ↓ ↓ ↓ (0/3 su) | 48.5 (2Β° pctl 1a) | + +## Libro + +Equity **$597.12** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-13 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.8 contro realizzata 30g 22.5 (sopra); DVOL al 2Β° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 48.5 contro realizzata 30g 32.6 (sopra); DVOL al 2Β° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **51 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-14.md b/docs/journal/2026-08-14.md new file mode 100644 index 0000000..c26845a --- /dev/null +++ b/docs/journal/2026-08-14.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-14 + +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,994.50 | -0.72% | -2.91% | -2.69% | 22.6% | ↓ ↓ ↓ (0/3 su) | 34.7 (2Β° pctl 1a) | +| **ETH** | $1,880.30 | -0.26% | -1.74% | -1.90% | 32.2% | ↓ ↓ ↓ (0/3 su) | 47.2 (0Β° pctl 1a) | + +## Libro + +Equity **$597.12** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-14 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.7 contro realizzata 30g 22.6 (sopra); DVOL al 2Β° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 47.2 contro realizzata 30g 32.2 (sopra); DVOL al 0Β° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **52 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-15.md b/docs/journal/2026-08-15.md new file mode 100644 index 0000000..764b954 --- /dev/null +++ b/docs/journal/2026-08-15.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-15 + +*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $63,033.00 | +0.06% | -2.90% | -1.21% | 22.0% | ↓ ↓ ↓ (0/3 su) | 35.0 (4Β° pctl 1a) | +| **ETH** | $1,880.85 | +0.03% | -1.83% | +0.93% | 30.7% | ↑ ↓ ↓ (1/3 su) | 47.5 (1Β° pctl 1a) | + +## Libro + +Equity **$597.12** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-15 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.0 contro realizzata 30g 22.0 (sopra); DVOL al 4Β° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 47.5 contro realizzata 30g 30.7 (sopra); DVOL al 1Β° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **53 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-16.md b/docs/journal/2026-08-16.md new file mode 100644 index 0000000..6368abf --- /dev/null +++ b/docs/journal/2026-08-16.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-16 + +*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $62,841.50 | -0.30% | -3.13% | -1.66% | 22.0% | ↓ ↓ ↓ (0/3 su) | 35.2 (5Β° pctl 1a) | +| **ETH** | $1,874.50 | -0.34% | -1.83% | +1.82% | 30.4% | ↑ ↓ ↓ (1/3 su) | 47.5 (1Β° pctl 1a) | + +## Libro + +Equity **$597.12** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-16 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 35.2 contro realizzata 30g 22.0 (sopra); DVOL al 5Β° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 47.5 contro realizzata 30g 30.4 (sopra); DVOL al 1Β° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **54 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-17.md b/docs/journal/2026-08-17.md new file mode 100644 index 0000000..51d5e41 --- /dev/null +++ b/docs/journal/2026-08-17.md @@ -0,0 +1,48 @@ +# Giornale di bordo β€” 2026-08-17 + +*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,487.50 | +2.62% | +0.87% | -0.50% | 23.4% | ↓ ↓ ↓ (0/3 su) | 34.7 (2Β° pctl 1a) | +| **ETH** | $1,912.35 | +2.02% | +2.20% | +2.72% | 30.9% | ↑ ↓ ↓ (1/3 su) | 46.3 (0Β° pctl 1a) | + +## Libro + +Equity **$597.12** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-17 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.7 contro realizzata 30g 23.4 (sopra); DVOL al 2Β° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 46.3 contro realizzata 30g 30.9 (sopra); DVOL al 0Β° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evento]` πŸ“Œ BTC: giornata a +2.62%, **2.1 deviazioni** giornaliere (sd implicita dalla RV30 = 1.22%). +- `[evidenza]` πŸ“Œ Campione a oggi: **55 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-18.md b/docs/journal/2026-08-18.md new file mode 100644 index 0000000..2fbc4b1 --- /dev/null +++ b/docs/journal/2026-08-18.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-18 + +*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $64,693.50 | +0.32% | +1.78% | -0.02% | 23.4% | ↓ ↓ ↓ (0/3 su) | 34.9 (4Β° pctl 1a) | +| **ETH** | $1,917.10 | +0.25% | +1.91% | +2.43% | 30.9% | ↑ ↓ ↓ (1/3 su) | 46.1 (0Β° pctl 1a) | + +## Libro + +Equity **$597.12** Β· nozionale lordo $0 Β· leva lorda 0.00x Β· barra dati 2026-08-18 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+0.00** di equity (22 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 0 fill Β· fee 0.0000 +- cumulato dall'arming: **$-0.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` Libro **flat** su entrambe le gambe: nessun capitale a mercato. +- `[perche_flat]` Motivo, componente per componente β€” BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione. +- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x. +- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. +- `[drawdown]` πŸ“Œ Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 34.9 contro realizzata 30g 23.4 (sopra); DVOL al 4Β° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 46.1 contro realizzata 30g 30.9 (sopra); DVOL al 0Β° pctl di un anno; IV-rank espandente 0.07 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **56 giorni, 2 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-19.md b/docs/journal/2026-08-19.md new file mode 100644 index 0000000..5497dcc --- /dev/null +++ b/docs/journal/2026-08-19.md @@ -0,0 +1,49 @@ +# Giornale di bordo β€” 2026-08-19 + +*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $69,327.00 | +7.16% | +9.28% | +6.30% | 33.6% | ↑ ↓ ↑ (2/3 su) | 37.8 (21Β° pctl 1a) | +| **ETH** | $2,252.30 | +17.48% | +19.92% | +18.33% | 63.8% | ↑ ↑ ↑ (3/3 su) | 52.5 (21Β° pctl 1a) | + +## Libro + +Equity **$610.82** Β· nozionale lordo $76 Β· leva lorda 0.12x Β· barra dati 2026-08-19 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.000 | LONG @ 68,510.0 | $+76 | $+76 | HOLD (a target) | +| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) | + +## P&L + +- giorno: **$+13.70** di equity (24 letture) +- realizzato: +10.60 netto su 1 round-trip chiusi Β· 3 fill Β· fee 0.0816 +- cumulato dall'arming: **$+12.76** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su BTC $+76 (TP01 +0.000, SKH01 long). +- `[incoerenza]` ⚠️ ETH: tutti e 3 gli orizzonti sono al rialzo ma TP01 e' a zero. Da guardare: non e' lo stato atteso. +- `[leva]` Leva lorda **0.12x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.88x. +- `[pnl]` Equity $+13.70 con 3 fill e 1 round-trip chiusi (realizzato $+10.60 netto). +- `[concentrazione]` πŸ“Œ **Il 107% di tutto il P&L cumulato ($+12.76) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento. +- `[vol]` BTC: implicita 37.8 contro realizzata 30g 33.6 (sopra); DVOL al 21Β° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 52.5 contro realizzata 30g 63.8 (sotto); DVOL al 21Β° pctl di un anno; IV-rank espandente 0.13 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evento]` πŸ“Œ BTC: giornata a +7.16%, **4.1 deviazioni** giornaliere (sd implicita dalla RV30 = 1.76%). +- `[evento]` πŸ“Œ ETH: giornata a +17.48%, **5.2 deviazioni** giornaliere (sd implicita dalla RV30 = 3.34%). +- `[evidenza]` πŸ“Œ Campione a oggi: **57 giorni, 3 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-20.md b/docs/journal/2026-08-20.md new file mode 100644 index 0000000..6188ea7 --- /dev/null +++ b/docs/journal/2026-08-20.md @@ -0,0 +1,48 @@ +# Giornale di bordo β€” 2026-08-20 + +*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $73,010.50 | +5.31% | +15.06% | +9.74% | 37.3% | ↑ ↓ ↑ (2/3 su) | 39.4 (33Β° pctl 1a) | +| **ETH** | $2,326.65 | +3.30% | +23.42% | +20.61% | 64.4% | ↑ ↑ ↑ (3/3 su) | 52.6 (22Β° pctl 1a) | + +## Libro + +Equity **$620.63** Β· nozionale lordo $272 Β· leva lorda 0.44x Β· barra dati 2026-08-20 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.199 | LONG @ 68,510.0 | $+124 | $+124 | HOLD (a target) | +| **ETH** | +0.294 | LONG @ 2,251.8 | $+146 | $+148 | HOLD (a target) | + +## P&L + +- giorno: **$+9.81** di equity (24 letture) +- realizzato: +0.51 netto su 1 round-trip chiusi Β· 4 fill Β· fee 0.0698 +- cumulato dall'arming: **$+22.57** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su BTC $+124 (TP01 +0.199, SKH01 long) e ETH $+148 (TP01 +0.294, SKH01 long). +- `[disaccordo]` BTC: gli orizzonti del trend **non concordano** (2/3 al rialzo) e l'esposizione TP01 e' parziale (+0.199) β€” e' il meccanismo, non una scelta di oggi. +- `[leva]` Leva lorda **0.44x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.56x. +- `[pnl]` Equity $+9.81 con 4 fill e 1 round-trip chiusi (realizzato $+0.51 netto). +- `[concentrazione]` πŸ“Œ **Il 104% di tutto il P&L cumulato ($+22.57) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento. +- `[vol]` BTC: implicita 39.4 contro realizzata 30g 37.3 (sopra); DVOL al 33Β° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 52.6 contro realizzata 30g 64.4 (sotto); DVOL al 22Β° pctl di un anno; IV-rank espandente 0.13 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evento]` πŸ“Œ BTC: giornata a +5.31%, **2.7 deviazioni** giornaliere (sd implicita dalla RV30 = 1.95%). +- `[evidenza]` πŸ“Œ Campione a oggi: **58 giorni, 4 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-21.md b/docs/journal/2026-08-21.md new file mode 100644 index 0000000..08aac39 --- /dev/null +++ b/docs/journal/2026-08-21.md @@ -0,0 +1,58 @@ +# Giornale di bordo β€” 2026-08-21 + +*Scritto 2026-08-23T17:43:07+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $78,350.50 | +7.31% | +24.38% | +18.57% | 43.9% | ↑ ↑ ↑ (3/3 su) | 43.3 (57Β° pctl 1a) | +| **ETH** | $2,517.10 | +8.19% | +33.87% | +30.17% | 69.2% | ↑ ↑ ↑ (3/3 su) | 57.7 (46Β° pctl 1a) | + +## Libro + +Equity **$644.00** Β· nozionale lordo $196 Β· leva lorda 0.30x Β· barra dati 2026-08-21 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.176 | LONG @ 75,280.0 | $+123 | $+126 | HOLD (a target) | +| **ETH** | +0.291 | flat | $+70 | $+70 | HOLD (a target) | + +## P&L + +- giorno: **$+23.37** di equity (24 letture) +- realizzato: +18.90 netto su 6 round-trip chiusi Β· 5 fill Β· fee 0.0867 +- cumulato dall'arming: **$+45.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su BTC $+126 (TP01 +0.176, SKH01 long) e ETH $+70 (TP01 +0.291, SKH01 flat). +- `[leva]` Leva lorda **0.30x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.70x. +- `[pnl]` Equity $+23.37 con 5 fill e 6 round-trip chiusi (realizzato $+18.90 netto). +- `[concentrazione]` πŸ“Œ **Il 102% di tutto il P&L cumulato ($+45.94) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento. +- `[vol]` BTC: implicita 43.3 contro realizzata 30g 43.9 (sotto); DVOL al 57Β° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 57.7 contro realizzata 30g 69.2 (sotto); DVOL al 46Β° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evento]` πŸ“Œ BTC: giornata a +7.31%, **3.2 deviazioni** giornaliere (sd implicita dalla RV30 = 2.30%). +- `[evento]` πŸ“Œ ETH: giornata a +8.19%, **2.3 deviazioni** giornaliere (sd implicita dalla RV30 = 3.62%). +- `[evidenza]` πŸ“Œ Campione a oggi: **59 giorni, 10 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Analisi (agente) + +*Scritta da `claude-sonnet-5` il 2026-08-23T17:43:32+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.* + +21 agosto β€” dopo dieci giorni fermi (11-18 agosto, equity ferma a 597.12, zero fill), il libro si Γ¨ mosso in tre giornate consecutive: il 19 apre BTC ($+76, 1 round-trip), il 20 aggiunge ETH ($+148) e BTC sale a $+124, il 21 chiude entrambe le gambe a target (BTC $+126, ETH $+70) con $+23.37 di equity e $+18.90 realizzato su 6 round-trip. Il cumulato dall'arming passa cosΓ¬ a $+45.94, e la pagina segnala che il 102% di quel totale viene dagli ultimi 7 giorni β€” cioΓ¨ il periodo precedente era in perdita netta. + +Il contesto di mercato spiega perchΓ© si sia mosso qualcosa dopo dieci giorni piatti: BTC +7.31% e ETH +8.19% nella giornata, con TSMOM 3/3 al rialzo su entrambi gli orizzonti per entrambi gli asset. La pagina marca la giornata a 3.2 deviazioni per BTC e 2.3 per ETH rispetto alla volatilitΓ  realizzata β€” un movimento ampio, non un drift ordinario. Vale la pena notare che SKH01 Γ¨ long su BTC (breakout) mentre su ETH resta flat pur con TP01 a convinzione piΓΉ alta (+0.291 contro +0.176): stesso regime di mercato, due letture diverse a seconda del meccanismo. + +La leva resta bassa (0.30x su un tetto di 1.00x) e il feed SKH Γ¨ fresco (0 minuti). Il campione dichiarato β€” 59 giorni, 10 round-trip β€” resta troppo corto perchΓ© questi tre giorni buoni dicano qualcosa sull'edge: sono compatibili sia con l'inizio di una fase favorevole al trend sia con normale rumore di un campione piccolo dopo una fase piatta. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-22.md b/docs/journal/2026-08-22.md new file mode 100644 index 0000000..47fa90a --- /dev/null +++ b/docs/journal/2026-08-22.md @@ -0,0 +1,47 @@ +# Giornale di bordo β€” 2026-08-22 + +*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $77,103.50 | -1.59% | +22.32% | +18.51% | 43.9% | ↑ ↑ ↑ (3/3 su) | 42.3 (51Β° pctl 1a) | +| **ETH** | $2,423.10 | -3.73% | +28.83% | +29.06% | 69.8% | ↑ ↑ ↑ (3/3 su) | 57.0 (44Β° pctl 1a) | + +## Libro + +Equity **$635.00** Β· nozionale lordo $327 Β· leva lorda 0.51x Β· barra dati 2026-08-22 Β· 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.450 | LONG @ 75,280.0 | $+186 | $+185 | HOLD (a target) | +| **ETH** | +0.277 | LONG @ 2,436.4 | $+145 | $+142 | HOLD (a target) | + +## P&L + +- giorno: **$-9.00** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi Β· 2 fill Β· fee 0.0489 +- cumulato dall'arming: **$+36.94** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su BTC $+185 (TP01 +0.450, SKH01 long) e ETH $+142 (TP01 +0.277, SKH01 long). +- `[leva]` Leva lorda **0.51x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.49x. +- `[pnl]` Equity $-9.00 con 2 fill e 0 round-trip chiusi (realizzato $+0.00 netto). +- `[concentrazione]` πŸ“Œ **Il 103% di tutto il P&L cumulato ($+36.94) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento. +- `[drawdown]` πŸ“Œ Equity **-1.55%** sotto il picco ($644.98). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 42.3 contro realizzata 30g 43.9 (sotto); DVOL al 51Β° pctl di un anno; IV-rank espandente 0.17 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 57.0 contro realizzata 30g 69.8 (sotto); DVOL al 44Β° pctl di un anno; IV-rank espandente 0.18 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **60 giorni, 10 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/journal/2026-08-23.md b/docs/journal/2026-08-23.md new file mode 100644 index 0000000..e4d6906 --- /dev/null +++ b/docs/journal/2026-08-23.md @@ -0,0 +1,48 @@ +# Giornale di bordo β€” 2026-08-23 + +*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $77,254.50 | +0.20% | +22.94% | +20.52% | 43.3% | ↑ ↓ ↑ (2/3 su) | 42.4 (52Β° pctl 1a) | +| **ETH** | $2,427.20 | +0.17% | +29.49% | +30.46% | 69.6% | ↑ ↑ ↑ (3/3 su) | 57.0 (44Β° pctl 1a) | + +## Libro + +Equity **$636.14** Β· nozionale lordo $258 Β· leva lorda 0.41x Β· barra dati 2026-08-23 Β· 14 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.150 | LONG @ 75,280.0 | $+115 | $+116 | HOLD (a target) | +| **ETH** | +0.271 | LONG @ 2,436.4 | $+144 | $+142 | HOLD (a target) | + +## P&L + +- giorno: **$+1.14** di equity (14 letture) +- realizzato: +5.55 netto su 2 round-trip chiusi Β· 1 fill Β· fee 0.0243 +- cumulato dall'arming: **$+38.08** + +## Salute + +- giri di `book_execute`: 14/14 *(giorno in corso)* +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su BTC $+116 (TP01 +0.150, SKH01 long) e ETH $+142 (TP01 +0.271, SKH01 long). +- `[disaccordo]` BTC: gli orizzonti del trend **non concordano** (2/3 al rialzo) e l'esposizione TP01 e' parziale (+0.150) β€” e' il meccanismo, non una scelta di oggi. +- `[leva]` Leva lorda **0.41x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.59x. +- `[pnl]` Equity $+1.14 con 1 fill e 2 round-trip chiusi (realizzato $+5.55 netto). +- `[concentrazione]` πŸ“Œ **Il 102% di tutto il P&L cumulato ($+38.08) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento. +- `[drawdown]` πŸ“Œ Equity **-1.37%** sotto il picco ($644.98). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 42.4 contro realizzata 30g 43.3 (sotto); DVOL al 52Β° pctl di un anno; IV-rank espandente 0.18 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 57.0 contro realizzata 30g 69.6 (sotto); DVOL al 44Β° pctl di un anno; IV-rank espandente 0.18 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` πŸ“Œ Campione a oggi: **61 giorni, 12 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Nota + +*(vuota β€” campo dell'operatore)* diff --git a/docs/research/BRIEF-0822.md b/docs/research/BRIEF-0822.md new file mode 100644 index 0000000..4ad15dd --- /dev/null +++ b/docs/research/BRIEF-0822.md @@ -0,0 +1,119 @@ +# BRIEF ONDATA 2026-08-22 β€” ricerca multi-agente, branch `research/wave-0822` + +Sei UNO di ~30 agenti. Leggi tutto questo file prima di scrivere codice. + +## 0. Obiettivo reale (non e' "trovare uno Sharpe alto") +Il conto vero e' **$635** su Deribit. L'obiettivo dichiarato dall'operatore e' **50 EUR/giorno**. +Misura gia' fatta nel progetto: la rendita perpetua vale ~45-55% del CAGR, quindi 50 EUR/g +richiede **~$258k netti** di capitale. **Nessuna strategia colma quel divario**: il vincolo binding +misurato il 26/07 e' *capitale che entra* + *conto che non sparisce*, non la ricerca. +Percio' un risultato vale in ordine: + 1. qualcosa di **ESEGUIBILE a $600-5.000** che aggiunge al libro TP01+SKH01 (raro); + 2. qualcosa che alza **P(sopravvivere a una barriera di DD per-conto)** β€” e' il canale *prop/funded*, + l'unico che moltiplica il nozionale senza possedere il capitale; + 3. un **LEAD** onesto con gate pre-registrato e data; + 4. una **falsificazione pulita** (chiudere un filone vale quanto aprirlo: si smette di pagarlo). +Un "non funziona" MISURATO e' un risultato accettato e apprezzato. Un PASS non verificato e' un danno. + +## 1. Disciplina di macchina β€” NON NEGOZIABILE +La VPS ha **2 core** e ci gira il libro con **soldi veri**: `cron_book` al minuto **:07**, +`cron_chain` al **:25**, `cron_daily` alle **00:30**. +- Ogni comando python pesante va lanciato con `nice -n 19 timeout 900 uv run python ...` +- **Budget: nessun singolo run > 15 minuti.** Se una griglia non ci sta, RIDUCILA e dichiara che + l'hai ridotta. Meglio 24 celle dichiarate che 700 mai finite. +- Preferisci **1d / 1h**. I 5m esistono (btc/eth) ma una sweep su 5m x griglia = ore: vietata + salvo che sia il cuore della tua ipotesi, e allora su UN asset e UNA finestra. +- **NON toccare**: `config/`, `data/live/`, `data/paper_*`, `data/venue_watch`, `data/fee_watch`, + `src/live/`, `scripts/live/`, `scripts/cron_*.sh`. Sola lettura. +- **NON eseguire** git checkout/commit/push/stash/branch. Il branch e' gia' quello giusto. +- Scrivi **UN SOLO file nuovo**: `scripts/research/r0822_.py`. Niente altro in repo. + Output/figure/CSV solo sotto `/tmp/claude-1001/.../scratchpad` o stampati a video. + +## 2. Il dato β€” cosa esiste davvero +Legge con `from src.data.downloader import load_data` oppure `scripts/research/alt/altlib.get()`. +| dato | file | copertura | nota | +|---|---|---|---| +| BTC/ETH OHLCV | `data/raw/{btc,eth}_{5m,15m,1h}.parquet` | 2018/2019 -> oggi | **certificato**, unica verita' (Deribit mainnet) | +| DVOL BTC/ETH | `data/raw/dvol_{btc,eth}.parquet` | 2021-03 -> oggi (1978g) | vol implicita ATM | +| 51 alt Hyperliquid 1d | `data/raw/hl__1d.parquet` | 2024 -> oggi (~965g) | serie native, no backfill | +| funding HL 1h | `data/raw/hlfund__1h.parquet` | ~2023-05 -> oggi | 19 major | +| ETF/azioni 1d | `data/raw/eq__1d.parquet` | fino a 30 anni | SPY/QQQ/IWM/TLT/GLD/HYG certificati; TLT parte 2016 | +| **catena opzioni Deribit** | `data/raw/cb_chain/YYYY-MM-DD.parquet` + `bite_archive.parquet` | **2026-05-01 -> oggi, oraria** | per-strike: bid/ask/mid/iv/delta/gamma/theta/vega/OI/volume/book_depth | +| **snapshot di mercato** | `data/raw/cb_market_snapshots.parquet` | **2026-03-26 -> 2026-07-30** (17.406 righe) | dealer_net_gamma, gamma_flip_level, oi_delta_pct_4h, liquidation_long/short_risk, funding_cross, iv_minus_rv | +| term structure IV | `data/raw/vol_term_{btc,eth}.parquet` | **solo 58 giorni** | iv 7/30/60/90/180d | +Harness catena: `scripts/research/cblib.py` (`load_chain()` unisce archivio + raccolta). +**ATTENZIONE**: catena/snapshot/vol_term hanno **3-5 MESI** di storia. Con 3 mesi non esiste un +hold-out: quei dati possono produrre **al massimo un LEAD con gate pre-registrato**, mai uno sleeve. +Dirlo esplicitamente e' parte del risultato. `load_data("SOL")` = guardrail: SOL sta in `alt_sol_*` +ed e' **escluso per decisione dell'operatore** (22/08) β€” non riproporlo. + +## 3. L'harness onesto β€” USALO, non riscriverlo +`import sys; sys.path.insert(0, "scripts/research/alt")` poi `import altlib as A`. +- `A.get(asset, tf)` dati Β· `A.vol_target(...)` Β· `A.eval_weights(df, target)` metriche nette fee +- `A.eval_weights_smallcap(df, target, capital=600)` -> **haircut reale a $600** (min-order $5) +- `A.candidate_daily(target_fn)` -> serie giornaliera del candidato +- `A.marginal_vs_tp01(cand_daily)` -> ADDS / NEUTRAL / DILUTES / **HEDGE** / **NOISE** + (contiene: robust_oos multicut, has_insample_edge, is_hedge, beats_noise_null) +- `A.study_family_honest(name, factory, grid, tfs)` -> **il gate giusto se hai una GRIGLIA**: + sceglie la cella **in-sample-only** + deflated-Sharpe. `earns_slot_honest` e' il verdetto. +- `A.deflated_sharpe(sr, all_sr, daily)` -> PASS >= 0.95. **Conta TUTTI i trial, al RIALZO.** +- `A.causality_ok(target_fn)` -> nessun look-ahead Β· `A.day_boundary_robust(...)` per segnali calendario +- `A.implausible_sharpe(daily, n_trades, n_losing)` -> Sharpe troppo bello = rischio FUORI dal dataset +- `A.anchor_luck_band(fn_by_offset, offsets)` -> la stima onesta e' la **MEDIANA**, non l'ancora 00:00 +- `A.anchor_luck_delta(A_by_off, B_by_off, offsets)` -> per confrontare due varianti: + **mediana delle DIFFERENZE APPAIATE**, mai differenza delle mediane. + +## 4. I modi in cui questo progetto si e' gia' ingannato (li conosciamo per nome) +Se il tuo risultato ha una di queste forme, e' quasi certamente quello β€” provalo prima di crederci. +1. **Null del de-levering** (5 occorrenze): "meno drawdown" e' quasi sempre solo meno leva. + Test obbligatorio su OGNI claim di DD: esiste `k<1` che, applicato al baseline, da' lo STESSO + DD con Sharpe **migliore**? Se si', il tuo overlay e' inutile. +2. **TP01 travestito**: corr al baseline > ~0.6 = stai ricomprando il trend. `marginal_vs_tp01`. +3. **Selezione sull'hold-out**: scegliere la cella col miglior OOS. Usa `select_cell_insample`. +4. **Fortuna d'ancora**: l'ora/il giorno di ribilanciamento e' un max-of-k non dichiarato. +5. **Sharpe implausibile**: 0 perdite su N non e' "senza rischio" (regola del tre: fino a 3/N). +6. **Hedge travestito da alpha**: paga solo quando TP01 e' debole -> `is_hedge`. +7. **Il win-rate e' un KNOB**: WR ~ 1/(1+RR). Converti ogni claim "X% win" in **expectancy R netta fee**. +8. **Una riga presente non e' un dato presente**: conta cio' che e' QUOTATO/ATTIVO, non le righe. +9. **Barre attive vs calendario**: uno sleeve settimanale su griglia giornaliera e' 94% zeri. +10. **Confronto appaiato per INGRESSO**, mai allineato sull'uscita (e' quella che la variante cambia). + +## 5. Cosa e' GIA' MORTO β€” non riaprirlo senza un meccanismo nuovo +mean-reversion/fade Β· trend 5m-15m (fee) Β· calendario in ogni forma (weekend 375 trial, expiry, +stagionalita', event-clock, ora-del-giorno) Β· funding (carry CC01, time-series, cross-sectional: +chiuso su 3 lati) Β· macro regime gate (ridondante col trend) Β· gamma scalping long-vol Β· CRT/ICT/ +Elliott/Fibonacci/Albimarini Β· espansione universo XS01 Β· meta-allocazione dinamica Β· trend +multi-asset (MAT01) Β· overlay DD su VRP01 (5/5 refutati) Β· SOL terza gamba (deciso 22/08) Β· +breadth alt (rivedibile solo con 1-2 anni di storia HL in piu'). +Il **soffitto strutturale BTC/ETH-direzionale e' ~1.3 di Sharpe**: superato una sola volta, e +cambiando meccanismo (cross-sectional). + +## 6. Il libro di oggi β€” il tuo termine di paragone +LIVE su Deribit: **TP01 75% + SKH01 25%**, nettati sullo stesso strumento (USDC linear), cron orario, +cap $318/asset, min order $5, disaster-SL -30%, fee reale **3,5 bps/lato**. +Book di ricerca a 5 sleeve (TP01 33 / XS01 15 / VRP01 12 / SKH01 20 / GTAA01 20): +Sharpe FULL **1.95** e HOLD **1.54** (stime de-luckate; i canonici 2.22/2.36 sono fortuna d'ancora), +maxDD ~6.9%. GTAA01 **non e' deployabile** (PRIIPs) e XS01 serve ~$20k, XSR01 ~$5k. +Un candidato si giudica **marginalmente vs questo**, mai in assoluto. + +## 7. Contratto di consegna (obbligatorio) +Consegni **testo strutturato**, non un racconto. Alla fine del tuo lavoro riporta ESATTAMENTE: +- `IPOTESI`: cosa hai testato, in una frase, e **cosa ti aspettavi PRIMA di misurare**. +- `GRIGLIA DICHIARATA`: quante celle/trial hai valutato in TOTALE (conta al rialzo, incluse le + varianti provate e scartate). Se hai ridotto la griglia per il budget, dillo. +- `NUMERI`: Sharpe FULL / hold-out / maxDD / CAGR, **netti fee**, + haircut a $600 se pertinente. +- `GATE`: esito di ciascun gate che hai potuto far girare (marginal, DSR, causality, anchor, + de-levering null, implausible). Un gate non girato si dichiara "non girato", non si omette. +- `VERDETTO`: uno fra `SCARTATO` / `LEAD` / `CANDIDATO` β€” e **la ragione in una riga**. + `CANDIDATO` solo se: `earns_slot`/`marginal=ADDS` **E** DSR>=0.95 **E** null-de-levering superato + **E** eseguibile al capitale dichiarato. Altrimenti e' `LEAD` (con gate e data) o `SCARTATO`. +- `COSA MI SMENTIREBBE`: l'osservazione che falsificherebbe il tuo risultato. +- `SCRIPT`: il path del file che hai scritto (deve girare da solo: `uv run python `). +Se non hai potuto misurare, il verdetto e' `SCARTATO (non misurabile)` col motivo. **Non inventare +numeri, non estrapolare da un run non finito, non riportare un risultato di un run che hai ucciso.** + +## 8. RAM β€” 7 GB totali, ~3 GB liberi, e siete in tanti +Non caricare i `*_5m.parquet` di piu' di un asset alla volta, e taglia la finestra +(`pd.read_parquet(f, columns=[...])` + slice temporale) prima di elaborare. Se ti serve la catena +opzioni, filtra per asset/scadenza in lettura: `bite_archive.parquet` da solo e' 35 MB su disco ma +esplode in memoria. Un agente che fa OOM uccide anche il lavoro degli altri. diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md new file mode 100644 index 0000000..371080e --- /dev/null +++ b/docs/research/RESULTS-0822.md @@ -0,0 +1,3732 @@ +# ONDATA 2026-08-22 β€” esiti per filone (aggiornato man mano) + +Regola di lettura: `SCARTATO` con un meccanismo misurato vale quanto un `LEAD` β€” chiude un filone +e smette di farlo pagare alle ondate successive. `CANDIDATO` richiede marginal ADDS + DSR>=0.95 + +null de-levering superato + eseguibilita' al capitale dichiarato. + +| # | filone | verdetto | in una riga | +|---|---|---|---| +| 9 | ADAPTIVE-HORIZON | **SCARTATO** | il "vincitore adattivo" ha lookback incollato al bordo il 100% del tempo = TSMOM costante a 20g (corr 1.000, dSh 0.00 in 8/8 anni e 0/12 ancore); isolando le celle davvero adattive -> NEUTRAL, corrβ†’TP01 0.90 = TP01 travestito || 4 | DEALER-GAMMA | **SCARTATO** | la gamba tradeable e' INCOERENTE (BTC non separa, ETH separa al ROVESCIO); cio' che resta e' quasi tutto DVOL (corr 0.66), gia' refutato il 26/06. DSR 0.440 e la cella migliore sta SOTTO il massimo atteso dal rumore | +| 2 | GROWTH-POLICY | **LEAD** (condizione, non data) | il libro gira al **7% di Kelly** e raccoglie il **15%** della crescita massima in log; il gradino eseguibile 1.00x->1.25-1.50x vale 14.7a -> 12.9-11.6a al muro. Ma un solo giorno -10% all'anno porta k* da ~10x a **2x**: non si decide su un conto vero con la lente close-only | +| 12 | ORTHO-SCREEN | **SCARTATO 7/7** | nessuna famiglia arriva a ADDS+DSR; e il motivo e' **aritmetico**: su 168 trial il massimo atteso dal rumore e' Sharpe **1.572**, SOPRA il soffitto direzionale misurato (~1.3) -> uno screen largo su BTC/ETH direzionale **non puo'** passare un DSR, per costruzione | +| 3 | XS-LITE | **SCARTATO** (come sleeve) + **1 soglia pubblicata falsificata** | concentrare XS01 non crea uno sleeve nuovo (corr 0.81-0.96 col canonico, DSR FAIL, de-levering non superato) β€” ma il muro *"XS01 serve ~$20k"* e' misurato su una diagnostica di TURNOVER: il ribilancio vero smette di passare sotto **~$109 di sleeve (~$730 di book), 27x piu' in basso** | +| 10 | FLOW-SQUEEZE | **SCARTATO** (2 motivi indipendenti) | meta' dell'ipotesi **non esiste nel dato** (`liquidation_*_risk` = una sola categoria in 17.229/17.229 righe); l'altra meta' e' sotto la propria soglia di potenza **dichiarata prima** (MDE 1,056% vs effetto 0,402%) ed e' comunque **morta alla nascita** (la colonna non e' ricostruibile) | +| 11 | VRP-QUOTE-VERE | **SCARTATO** + **1 difetto di RACCOLTA in produzione** | il campione contiene l'**astensione** della strategia (0/19 settimane passano IV-rank>0.30) e il titolo non sopravvive all'ora d'ingresso; ma soprattutto: **`collect_chain.py` raccoglie la famiglia di contratti che il conto NON puo' marginare** | +| 7 | TERM-STRUCTURE | **SCARTATO** + 1 LEAD stretto (decisione 2027-02-01) | la pendenza e' un **termometro contemporaneo** (picco CCF a lag βˆ’1h/0h); il carry non paga il bid-ask (**18,6 giorni di roll-down per pagare UN round-trip**); come gate e' la **5Βͺ occorrenza** della ridondanza col trend | +| 6 | OI-PIN | **SCARTATO** | il max-pain batte uno strike casuale ma **non batte mai (0/24) la media a 7 giorni dello spot**, un livello che non usa NESSUN dato di opzioni: non e' pinning, e' reversione verso il centro recente con l'OI come stimatore rumoroso di quel centro | +| 8 | VOL-SIZE | **LEAD RIDIMENSIONATO** β€” gate 22/12 **gia' fallito oggi** | dare a SKH01 una size per-trade regge a **23/23 ancore**, 8/8 anni, null di permutazione e trasferimento su V1 β€” ma vale **+0,07 di Sharpe di libro, un terzo della fortuna d'ancora del libro stesso (+0,196)**. Il vol-target di **libro** e' invece falsificato: compra peso SKH gia' respinto e peggiora l'eseguibilita' | +| β€” | **XSR-REPRO** (integrita') | 🚨 **DIFETTO DI PRODUZIONE** | il numero 1.82 e' SPIEGATO e non era sbagliato (era su una **terza** lente, e su una barra non ancora chiusa) β€” ma cercandone la causa e' emerso che **`paper_xsr` registra ~41 minuti di mercato al giorno**, non un giorno. Tre gate pre-registrati leggono serie costruite cosi' | +| 5 | SKEW | **SCARTATO** (Q1, Q2) + **LEAD** (Q3, gate 2027-02-22) | il prezzo muove lo skew (t 3,1-9,4 su 8/8 test), **non il contrario** (max |t| in avanti 2,35 contro 2,08 atteso dal rumore). Ma Q3 e' grosso: **il f=0,73 di VRP01 e' per il 42% STRUTTURA A TERMINE e solo per il 25% skew** | +| β€” | **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** | +| β€” | **MONITOR-AUDIT** (produzione) | 🚨 **4 monitor su 6 rotti**, ma **la finestra NON e' persa** | `paper_statarb` registra **4 minuti al giorno** e il suo gate del **27/09 si ribalta su 2 criteri su 3** (Sharpe registrato **+1,96** contro **βˆ’2,02** ricostruito). **La riparazione e' "riparare + RIGENERARE", non "riparare e azzerare": nessuna data di gate si sposta** | +| 1 | PROP-ALLOC | **LEAD** (gate: XS01 fuori dalla sua finestra) + **1 ipotesi falsificata** | riallocare *per la barriera* invece che *per lo Sharpe* vale **+0,016 di J (il 4%)**: e' cio' che il conto del gambler's ruin prevedeva. Cio' che vale e' **mettere XS01 su un conto funded (+0,400)** β€” ma **tutto il vantaggio poggia sul drift di XS01 misurato sulla sua finestra di scoperta** | + +## Note che sopravvivono ai singoli filoni + +### 9 β€” ADAPTIVE-HORIZON (r0822_adaptive_horizon.py, 112 trial dichiarati) +- Sanity: il baseline dell'agente riproduce TP01 canonico a **max|diff| = 0.0** (n=2719). +- Il controllo giusto NON era TP01 ma il **miglior lookback COSTANTE**: senza quel controllo il null + confonde "adattare paga" con "un orizzonte piu' corto paga su questo campione". L'agente se l'e' + costruito da solo dopo aver visto l'argmax incollato al bordo, e ha dichiarato i trial in piu'. +- ⚠️ Segnalazione, NON proposta: 30/90/180 e' rango 18/24 in-sample nella propria famiglia di + orizzonti (10/24 sull'hold-out), ma la superficie di L e' un **crinale di rumore** β€” il salto di + Sharpe fra L ADIACENTI e' +0.18, cioe' il 47-67% dell'intero vantaggio del vincitore su TP01, ed + estendere la griglia ha spostato l'argmax da L=20 a L=15. "Piu' corto e' meglio" e' un max-of-k su + una superficie frastagliata. **Nessun cambio a TP01.** +- Riaprirebbe il filone: un rilevatore il cui L si muove DAVVERO che batta il miglior L costante + (non TP01) in >=6/8 anni appaiati con corrβ†’TP01 < 0.6 β€” es. BOCPD/PELT vero, non provato. + +### 4 β€” DEALER-GAMMA (r0822_dealer_gamma.py, 136 statistiche dichiarate) +⚠️ **DIFETTO DI DATO, il risultato piu' riusabile del filone: `dealer_net_gamma` e' il GEX col SEGNO +INVERTITO** β€” corr **-0.95 BTC / -0.89 ETH** contro la ricostruzione da manuale sulla catena +(962/1116 snapshot, ~280-316 strike), fit `dng ~ -0.7*GEX`. **Chi lo usasse col nome che porta +leggerebbe ogni regime al contrario.** Spiega anche la contraddizione interna del file (corr fra +`dng>0` e `spot>flip` = -0.28 / -0.92). +⚠️ **Il file dichiara piu' di quel che ha:** cadenza 15 min solo dal **2026-05-01**; i 37 giorni +prima sono **un punto al giorno** -> **90 giorni, non 4 mesi**. `liquidation_long_risk` e +`liquidation_short_risk` sono **COSTANTI** ('low' in 17.229 righe su 17.229 non nulle) = zero +informazione. `gamma_flip_level` congelato nel **61%** dei passi su BTC. +- Gate: **null de-levering FALLITO** (baseline x0.6 stesso maxDD con Sharpe migliore); **DSR 0.440** + su 64 trial, e nel modo piu' netto β€” la cella migliore (+4.98 lordo) sta **sotto** il massimo + atteso dal puro rumore (+5.12). `marginal_vs_tp01` inutilizzabile: su 90 giorni full e hold-out + COINCIDONO (`n_hold_days == n_days == 90`). +- **Null location-matched** (aggiunto dall'agente): `gamma_flip_level` bocciato β€” 96% di accordo con + `spot > SMA(720h)` su ETH; su BTC la SMA nuda da' uno spread MAGGIORE. +- **Ampiezza effettiva**: 73/59 episodi di regime ma 6-8 coprono il 78-86% del tempo -> **n_eff 53 + (BTC) / 24 (ETH) ore** su 2.130. Il regime coincide col MESE (maggio long, giugno short, luglio long). +- βœ… Ricostruibile oggi dalla NOSTRA catena, meglio dell'originale (518 ore dal 01/08, ~209-262 + strike/ora, quote_status ok ~100%, segno giusto) β€” **ma la soglia non e' portabile** (`dng=0` + corrisponde a `GEX=+5.8e6` su BTC): ricalibrarla sui 90 giorni sarebbe la selezione che il + progetto rifiuta. Riapertura non prima di **~30-40 episodi di regime = ~2 anni (meta' 2028)**. + +### 2 β€” GROWTH-POLICY (r0822_growth_policy.py, 241 celle dichiarate) +βœ… **Controllo di replica superato**: il motore riproduce `r0807_piano_netto` Β§2 al numero pubblicato +(EUR250/m -> 19,8a / P 52%; EUR500/m -> 14,7a / P 99%) con path diversi. +πŸ“Œ **Il risultato strutturale: ogni gate del progetto e' INVARIANTE ALLA SCALA.** Lo Sharpe misura +1,31 a OGNI cella del knob di leva -> `deflated_sharpe` e `marginal_vs_tp01` non falliscono, **non +vedono la variabile**. E' per questo che la leva e' l'unica leva del libro mai esaminata in 2 mesi. +- Serie de-luckata x0,89: drift 17,11%/a, vol 11,37%, Sharpe 1,51, peggior giorno -3,95%. + Netto fee E netto fisco d'accumulo (33% + carry 4a + patrimoniale 0,2%). +- k* empirico **~10-14x** (banda 10x a 1500 path / 14x a 2500: la POSIZIONE non e' risolta), + Kelly gaussiano 13,2x. Libro a k=1 -> g 17,0%/a, maxDD mediano 18,1%, P(DD>30%) 1,4%. +- **Fragilita' che decide il verdetto:** k* e' lineare nell'errore del drift (stimato su 7,4 anni) β€” + -1 SE -> 10x, -2 SE -> 7x, drift dimezzato -> 7x, drift zero -> 0,5x. E allo **stress di coda** + un giorno -10% **all'anno** porta k* a **2x**, -15%/anno a **1x**. +- ⚠️ **Controllo di plausibilita' FALLITO dall'agente sul proprio output** (e riportato): capitale + mediano **$4,4e12** da $600 a k=14x -> il modello assume ritorno **indipendente dalla size**. + A k>2 i numeri sono aritmetica, non previsioni. +- **Gate pre-registrato (condizione, non data): nessuna proposta di k>1 prima di rifare la curva con + la lente WICK ACCOPPIATA** (`r0725_prop_coupled`) β€” a leva il breach si valuta sul MINIMO, e la + lente close-only usata qui e' quella misurata **cieca** il 25/07 (raddoppia i breach da daily-loss). +- πŸ“Œ **Sottoprodotto da verificare (tocca un'etichetta del libro live):** a `target_vol=20%` la vol + **realizzata** di TP01 e' **12,14%** β€” il target vale sulla posizione quando c'e', e TP01 e' + long-flat. **L'etichetta sovrastima il rischio preso di ~40%.** + +### 12 β€” ORTHO-SCREEN (r0822_ortho_screen.py, 174 trial: 7 famiglie x 24 + 6 controlli) +πŸ“Œ **Il risultato piu' riusabile dell'ondata, ed e' metodologico:** il deflated-Sharpe va calcolato +**di screen** (168 trial) e non solo di famiglia (24). Su quel pool il massimo atteso dal puro rumore +e' **Sharpe 1.572**, cioe' **sopra il soffitto direzionale misurato del progetto (~1.3)**. +**Conseguenza: una ricerca a rete larga su un singolo stream direzionale BTC/ETH non puo' passare il +proprio gate β€” non e' sfortuna, e' aritmetica.** Le ondate future o dichiarano famiglie molto piu' +piccole in anticipo, o cambiano meccanismo (non-direzionale / cross-sectional). +πŸ“Œ **Per la prima volta nel progetto l'eseguibilita' a $600 non e' il vincolo binding di niente** +(haircut 0.00-0.01 su 7/7): muore tutto molto prima, sull'edge. +- Famiglie: RSKEW (skew realizzata), TACC (accelerazione del trend), VPX (volume-prezzo), XDISP + (dispersione dei 51 alt come timer), XCORR (corr BTC-ETH), XTAIL (dopo shock 3Οƒ), VRAT + (variance-ratio). Miglior DSR di famiglia 0.633, di screen **0.034**. `robust_oos` False 7/7. +- **TACC** e' l'unica ADDS (corrβ†’TP01 0.09) ma il suo uplift hold-out **vive tutto nel 2026** (2025 + da solo βˆ’0.314) = finestra fortunata. E la domanda della famiglia ha risposta: **e' il LIVELLO, + non l'accelerazione** β€” la cella scelta si riduce al ritorno a 5 giorni, corr **+0.784** col + momentum di livello. +- βœ… **Tre falsificazioni che chiudono spazio:** (1) la mean-reversion **non torna in vita** sotto due + conditioner mai provati (volume, shock 3Οƒ): la selezione in-sample sceglie il ramo di + CONTINUAZIONE in entrambi i casi; (2) la dispersione dei 51 alt come timer di mercato e' + **negativa** (βˆ’0.899 sulla sola finestra attiva, non e' un artefatto di calendario); (3) RSKEW e' + il ritratto del fitting β€” **miglior in-sample dello screen (1.281) e peggior hold-out (βˆ’0.494)**, + col segno OPPOSTO all'a-priori teorico dichiarato. +- ⚠️ **6Βͺ occorrenza del null del de-levering** (VRAT, maxDD 4.4% -> `k*TP01` fa meglio). +- ⚠️ Due candidati hanno l'ancora canonica come **PEGGIORE** delle 8 (TACC, VRAT): la fortuna + d'ancora non ha un verso fisso, e guardare solo il canonico a volte **inventa un danno**. + +### 3 β€” XS-LITE (r0822_xs_lite.py, 26 celle + 120 valutazioni d'ancora) +βœ… Repliche **bit-exact** prima di ogni delta: XS01 vs `sleeves._xsec_returns` e XSR01 vs +`basket_from_positions(demean=True)`, entrambe `max|diff| = 0.0`. +πŸ“Œ **Falsificazione di un numero pubblicato:** il muro *"XS01 serve ~$20k"* non e' un fatto di +Sharpe ma di **turnover** β€” e a capitale piccolo il min-order salta la **deriva del vol-target** +(|Ξ”w| giornaliero 0.001) che **non porta segnale**, non il ribilancio (|Ξ”w| 0.091-0.157). Capitale +minimo perche' il ribilancio passi: **$109 (k=5) / $86 (k=3) / $64 (k=2)** di sleeve. Stessa lezione +gia' imparata su TP01 nel 2026 ("a $600 il min-order e' gia' la banda ottimale") e **mai applicata a +XS01**. ⚠️ L'agente ha usato **min-order $10 (Hyperliquid)**, non $5 (Deribit) β€” un audit dedicato +sta verificando i minimi veri del venue. +- Concentrare **non** ripara il rischio #1 di XSR01: XS-LITE regge oltre **50 bps/gamba**, XSR-LITE + muore a **~28 bps a ogni k, pieno incluso**. +- La radice dell'ampiezza **non descrive** XS01: da 10 a 4 gambe si perde il **2%** di Sharpe + mediano-di-fase mentre N_hhi passa 6.9 -> 3.0 (il segnale sta negli **estremi** del ranking; le + gambe 3-5 per lato aggiungono rischio quanto segnale). Il collasso arriva solo a k=1 (41%). +- ⚠️ **Da inseguire β€” tocca un gate pre-registrato: lo Sharpe 1.82 di XSR01 NON si riproduce oggi.** + Sulla finestra identica a quella di scoperta la lente "paniere" da' **1.75** e la lente "libro" di + `paper_xsr` da' **2.23**; 1.82 non e' nessuna delle due. Spiegazione piu' probabile: `data/raw` e' + gitignored e il cron riscrive i parquet HL ogni notte -> **stesso codice, dati diversi** (identico + allo scoperto del 07/08 su GTAA/TLT). **La decisione del 23/10 poggia su quel numero.** + +### 10 β€” FLOW-SQUEEZE (r0822_flow_squeeze.py, 118 trial dichiarati, griglia RIDOTTA per budget) +πŸ“Œ **Sottoprodotto con potenza vera, e chiude un filone: il funding non predice ne' direzione ne' +volatilita'** β€” misurato su **53.430 ore / 3 anni** di funding Hyperliquid (li' la potenza c'e'): +eccesso |<=0,35%| a 24h con **segno instabile fra anni**, |ritorno| futuro piatto e non monotono. +**Il filone funding, gia' chiuso su 3 lati come livello di carry, si puo' dichiarare chiuso anche +come proxy di AFFOLLAMENTO. Quarto lato.** +- Il "segnale" migliore (Sharpe 3,25 su 90 giorni) e' **il massimo di 18 celle**: DSR **0,103** + (0,004 su 118 trial), `implausible_sharpe=True`, e su 114 giorni **il massimo atteso per caso e' + Sharpe 5,40** -> il candidato sta **sotto il proprio null**. Il 71% del P&L e' in giugno, il 60% + in **3 giorni su 114**. +- Il null "statica travestita" e' superato (sempre-short = βˆ’0,67), ma quello di **timing casuale + location-matched** e' al 99,7Β° pctl **che pero' e' il massimo di 18** -> P(per caso) = 0,06: + **non superato dopo il conto dei trial**. Esempio pulito di come si legge un percentile alto. +- ⚠️ **Ricostruibilita' MISURATA, non assunta:** l'OI della catena (opzioni) contro la colonna dello + snapshot (perpetual) da' **Pearson +0,03 BTC / +0,21 ETH** β€” non e' la stessa grandezza. +- πŸ’‘ **Azione possibile, costo quasi nullo (NON eseguita, tocca il cron di produzione):** accendere + un collettore orario dell'**OI dei perpetual Deribit** (una GET/ora, API pubblica). Non ripara + questo filone, ma da quel giorno la storia comincia; oggi ripartirebbe da zero. Il criterio di + falsificazione e' gia' scritto dall'agente: >=12 mesi, eccesso oltre la MDE (~0,3%) e presente in + >=3 trimestri su 4. +- ⚠️ **Distinzione metodologica utile** (nata contraddicendo il suggerimento del coordinatore): qui + il vincolo binding NON e' la concentrazione degli episodi (86-87 episodi corti, i primi 8 coprono + solo il 20-25% delle ore) ma la **lunghezza del calendario** (90 giorni). *Ampiezza scarsa* e + *calendario corto* sono due diagnosi diverse e si curano in modi diversi: piu' episodi vs piu' tempo. + +### 11 β€” VRP-QUOTE-VERE (r0822_vrp_real_quotes.py, 60 trial + 9 ancore d'ingresso) +🚨 **IL RISULTATO DI PRODUZIONE β€” `collect_chain.py` interroga `{"currency": "BTC"|"ETH"}`, che su +Deribit restituisce le sole opzioni INVERSE (regolate in BTC/ETH). Il conto e' in USDC** (esegue +`BTC_USDC-PERPETUAL`). Esiste una famiglia **USDC-lineare** con lotto minimo **10x piu' piccolo**: +| famiglia | min lotto | ~$ per lotto | +|---|---|---| +| BTC inverse | 0.1 | ~$7.700 | +| **BTC_USDC** | **0.01** | ~$780 | +| ETH inverse | 1.0 | ~$2.500 | +| **ETH_USDC** | **0.1** | ~$250 | +Quindi il muro pubblicato *"BTC opzioni min 0.1 = $6.210/lotto -> FUORI a $3.000"* (congelato in +`tests/test_vrp_profit_take.py`) e' **misurato sulla famiglia che il conto non puo' marginare**. +**3Βͺ occorrenza dello schema `fee_watch` (21/08): un controllo/collettore puntato su uno strumento +diverso da quello che si trada.** +⚠️ **MA la verifica indipendente del coordinatore cambia la conclusione, e in meglio:** contando +l'open interest famiglia per famiglia (`get_book_summary_by_currency`, misura mia, non dell'agente) +| famiglia | strumenti | con OI>=100 | +|---|---|---| +| BTC inverse | 1038 | **415** | +| ETH inverse | 932 | **548** | +| **BTC_USDC** | 686 | **5** | +| **ETH_USDC** | 660 | **119** | +-> conteggio corretto come FATTO, **ma l'inferenza che ne ho tratto io ("BTC_USDC e' praticamente +morto") e' SBAGLIATA**: vedi filone 14 β€” su queste famiglie l'OI e la negoziabilita' sono +**anti-correlati**, e BTC_USDC ha il **93%** dei put quotati a due lati. **Il muro sul BTC resta +per il PREZZO del lotto ($772 su un conto da $635), non per la liquidita'.** +ETH_USDC resta la gamba giusta su cui puntare, ma per **spread e prezzo del lotto** (filone 14), +non per il conteggio di OI. +❌ **Sottoprodotto RITIRATO:** avevo scritto che *"esiste un universo opzioni USDC piu' liquido di +BTC_USDC (SOL 341, XRP 250, HYPE 172, AVAX 146 con OI>=100)"*. **Falso nel senso che serve:** quelle +famiglie hanno piu' posizioni aperte e **meno quote** β€” SOL_USDC ha il **9%** dei put quotati a due +lati contro il **93%** di BTC_USDC, e il suo miglior bid sta a **un tick**. +- **Il campione misura la strategia mentre STA FERMA**: 0/19 settimane passano il gate IV-rank>0.30, + DVOL alla mediana del 7Β°/11Β° percentile storico, sottostante +25%/+44%. +- βœ… **Replica indipendente del f del 30/07 su campione piu' lungo: f = 0,714 pooled** (IC95 + [0,690, 0,779], **0/19 osservazioni >= 1,0**). Attraversare lo spread costa **~10% del credito**. +- ⚠️ **Correzione a un numero pubblicato:** il forfait fee del sleeve sovrastima il listino vero di + **1,44x (BTC) / 1,80x (ETH)**, non di "~2x". Il segno (conservativo) regge, la taglia no. +- ⚠️ **Rischio sul MINIMO, non sulle chiusure:** peggior mark infra-settimana mediana βˆ’6,4% del + rischio, **minimo βˆ’91,0%**; fra le settimane chiuse **in utile**, minimo **βˆ’56,9%**. +- Gate: `implausible_sharpe` **FLAGGED** su entrambi (0 perdite su 10 -> regola del tre: tasso vero + fino al 30%, su un payoff che perde 7,4x il credito); `deflated_sharpe` **non calcolabile** (T=10) + e l'agente **si e' rifiutato di fabbricare una griglia giornaliera al 94% di zeri** per superare + il T>=30; banda d'ancora: canonico all'89Β° pctl, **mediana onesta 1,45**, banda che include il + negativo. Il titolo annualizzato (32,58) e' stato **deliberatamente non riportato come risultato**. +- ⚠️ L'agente ha **refutato in corsa una propria conclusione**: spostare l'ora d'ingresso cambia + QUALI STRIKE corrispondono al delta obiettivo -> non e' lo stesso trade un'ora dopo, e' **un'altra + struttura**; la media BTC **cambia segno** fra le ancore (βˆ’1,35% -> +11,22%). + +### 7 β€” TERM-STRUCTURE (r0822_term_structure.py, 312 trial dichiarati) +🚨 **MURO DEL DATO, e vale per ogni filone che usa la catena ereditata: prima del 2026-06-09 +`bite_archive` raccoglieva UNA SOLA SCADENZA per giro** (mediana 1, max 1) -> **la curva non +esiste**. La finestra utilizzabile non e' "3,7 mesi": e' **74 giorni**, SE(Sharpe) ~ 2,2. +βœ… **Ricostruzione certificata contro due riferimenti indipendenti:** vs il logger `vol_term_*` +corr **0,992-0,997** (scarto 0,00-0,30 pt-vol); vs DVOL corr 0,94 con bias βˆ’2,2/βˆ’2,8 pt-vol = il +segno atteso (DVOL integra lo smile). +- **Il test piu' informativo e' quello di RISOLUZIONE:** l'unica correlazione col futuro che sembrava + qualcosa (h=5g: +0,29/+0,32, 94Β°/98Β° pctl vs null a blocchi) **cambia SEGNO togliendo l'ultima + settimana** (βˆ’0,22/βˆ’0,17). Sette giorni su 70. Nella regressione controllata per il ritorno + passato il t di Newey-West sta **sotto 1,0 a ogni orizzonte**. +- **Il confronto che uccide il candidato: il buy&hold sulla stessa finestra.** Mediana appaiata su + 24 ancore vs B&H = **+0,14 (14/24) su BTC** e **βˆ’0,65 (1/24) su ETH**. La banda d'ancora + `gate_pass=True` in isolamento e' **inutile: il B&H passa lo stesso gate**. +- Confound proprio del filone, misurato: corr(pendenza, RV **passata**) βˆ’0,50/βˆ’0,57 contro RV + **futura** βˆ’0,39/βˆ’0,38 -> la pendenza e' il premio di vol con la gamba realizzata sostituita da + una implicita = la variabile di `dvol_directional` (gia' HEDGE, earns_slot=False su 5 anni). +- **`deflated_sharpe` NON girato, e la motivazione e' il risultato:** su 74 osservazioni + l'incertezza di CAMPIONE domina di un ordine di grandezza quella da SELEZIONE β€” deflazionare 312 + trial darebbe un numero preciso e privo di senso. +- πŸ“Œ **LEAD stretto e pre-registrato** (l'unico numero non banale: corr(pendenza, RV futura) βˆ’0,39): + la term structure come previsore di **VOL, non di ritorni**. Decisione **2027-02-01**, si riapre + solo se (a) corr <= βˆ’0,30 su entrambi gli asset **E** (b) batte il **DVOL a tenore singolo** in RΒ² + incrementale **E** (c) supera il null del de-levering. **Se (b) fallisce, il filone si chiude.** +- ⚠️ **Tre bug propri catturati in sessione**, tutti del tipo che passa i test pigri: il null del + de-levering cercava il **primo** k salendo invece del **piu' grande**; la RV futura aveva **uno + shift di troppo**; l'ATM vettorializzato e' stato verificato contro il prototipo (23.544/23.545 + righe identiche). + +### 6 β€” OI-PIN (r0822_oi_pin.py, 516 trial dichiarati, 73 scadenze/asset) +βœ… **Potenza dichiarata PRIMA di guardare** (e questa e' la ragione per cui il verdetto e' credibile): +MDE a hte 12 = **0,46%/trade**; effetto max-pain osservato **+0,273% = SOTTO il proprio MDE**. +πŸ“Œ **Il null che decide: il placebo senza opzioni.** La cella migliore fa Sharpe 5,59 e batte gli +strike casuali (6/72 contro ~2 attesi), ma **0/24 contro la media a 7 giorni dello spot**, con cui +condivide il **68-77% del segno**. Non e' pinning verso uno strike: e' reversione verso il centro +recente, e l'OI e' solo uno stimatore rumoroso di quel centro. +- DSR **0,290** (288 trial): lo **Sharpe-null atteso e' 6,79 > 5,59 osservato** β€” su 75 giorni il + massimo del rumore batte il risultato. `implausible_sharpe` scatta. I **5 giorni migliori valgono + il 61% del log-equity**. +- ⚠️ `marginal_vs_tp01` **non puo' dare ADDS per costruzione** su questa finestra: `multicut` vuole + >=2 tagli annuali da >=120g e `has_insample_edge` e' True *per default* perche' non esiste + pre-2025. Un NEUTRAL qui non e' un giudizio. +- Haircut a $600: **+0,000** (si esegue sul perp) β€” 2Βͺ conferma che l'eseguibilita' non e' piu' il + vincolo binding di questa ondata. +- ⚠️ **REGOLA NUOVA (difetto proprio, trovato e corretto in sessione): un placebo si controlla per + BILANCIAMENTO DEL SEGNO prima di usarlo.** Il placebo `jit` concordava in segno con la propria + regola solo nel **14%** dei casi (5% per `jit_gex`): non era un null, era **la strategia + invertita**, e usarlo fabbricava 3 celle "significative" e un contrasto spettacolare (βˆ’8,6 di + Sharpe). Un null pulito sta a ~0,50; sotto ~0,35 il suo delta va letto come **~2x il segnale**. + +### 8 β€” VOL-SIZE (r0822_vol_size.py, 28 trial + 23 ancore) +βœ… Sanity **bit-exact**: ricomposizione trade-by-trade `max|diff| = 0.0` contro `run_asset` e contro +`sleeves._skyhook_returns()`. +❌ **RITIRATO DALLO SCETTICO (vedi in fondo): il difetto qui sotto NON ESISTE su questo codice.** +Lasciato a verbale perche' la regola era stata pubblicata e va vista insieme alla sua smentita. +~~🚨 **DIFETTO DI METODO GENERALE, trovato dall'agente nella propria prima stesura e MISURATO invece +che cancellato: su uno sleeve a equity a GRADINO, un overlay giornaliero e' NON CAUSALE.** Tutto il +P&L di un trade SKH01 e' contabilizzato il giorno di **CHIUSURA**, fino a **3,83 giorni dopo +l'ingresso**; moltiplicare la serie giornaliera per `L_t` (che usa solo dati <= tβˆ’1) **passa +qualunque controllo di causalita' scritto sulla serie giornaliera** β€” il look-ahead sta nella +CONTABILITA', non nella formula. Costo misurato: **+0,04 di Sharpe hold-out fantasma**. +**REGOLA: un overlay si applica alla data d'INGRESSO, non a quella in cui il P&L viene contabilizzato.**~~ **<- RITIRATA, vedi sotto** +🚨 **REGOLA 2 β€” il null del de-levering e' DEGENERE quando la variante e' una ri-scalatura** +(`sh(k*base) === sh(base)`): il test che serve non e' sul DD ma **ISO-PESO sullo Sharpe**, altrimenti +si sta solo misurando di quanto e' cambiato il peso di uno sleeve. Qui era decisivo: quasi ogni +variante alzava il peso effettivo di SKH fino a 0,375, e **"alzare SKH01" e' gia' stato misurato e +respinto il 26/07**. +πŸ“Œ **Il meccanismo NON e' quello ipotizzato:** scomponendo la vol trailing di uno sleeve all'88% di +zeri nei due canali, il guadagno FULL viene dalla **FREQUENZA** (il solo filtro di attivita' fa ++0,051 su +0,061) e **non dalla magnitudine** (+0,003). Non e' controllo del rischio: e' un +**filtro di frequenza**, e va chiamato cosi'. +- Baseline del libro, stima onesta (mediana banda): **1,626 FULL / 1,037 HOLD / DD 10,4%** β€” la + fortuna d'ancora del libro vale **+0,196 FULL / +0,448 HOLD**. +- Cella congelata del LEAD (`VTL tv20 w90`): size fissata **all'ingresso** = + `clip(0,20 / rv90(tβˆ’1), 0, 3)`. ISO dShFULL **+0,070 (23/23 ancore)**, dShHOLD +0,101 (19/23), + **8/8 anni positivi**, **null di permutazione superato in ENTRAMBE le terne di ancore provate, + p <~ 0,03** (mediana del null βˆ’0,03/βˆ’0,05: riassegnare a caso la stessa distribuzione di size + **danneggia**). ⚠️ **Autocorrezione dell'agente, da rispettare:** quel null gira su **3 ancore + delle 23** per costo, e la TAGLIA dell'effetto varia di **3x** fra due terne diverse della + stessa banda (+0,033 vs +0,111 sulla stessa cella) -> **i p-value non si citano a tre decimali**: + il numero da mettere a verbale e' il **segno**. Il resto del report e' su 23 ancore piene. +- ⚠️ `DSR = 1,000` dichiarato **VACUO, non PASS**: lo passa anche il baseline β€” su una famiglia di + perturbazioni della stessa strategia il deflated-Sharpe non ha potenza. Onesta' rara. +- ⚠️ Punto piu' fragile, dichiarato: l'asse della finestra e' un **picco, non un plateau** + (w30 nettamente negativo, w180 ~0). +- **Gate pre-registrato β€” e non e' un forward** (con SE(Sharpe)~1,4 sei mesi non possono misurare + 0,07): e' un **secondo trasferimento**, la famiglia IV su SKH01_V1, dove il meccanismo dichiarato + prevede un effetto **minore** (lo stop e' gia' in ATR). **Se invece l'effetto e' uguale, la + spiegazione e' sbagliata anche se il numero regge.** Entro **2026-12-22**, prima di qualunque + modifica a peso/config di SKH01. Deploy solo se ISO dShFULL > 0 in >=20/23 ancore E + `weights_tilt_null` passa. **Book, pesi, cron, config INVARIATI.** + +### XSR-REPRO (r0822_xsr_repro.py) β€” filone di INTEGRITA', non di alpha +βœ… **Il numero pubblicato e' spiegato e NON era sbagliato.** 1.82 viene da una **terza** lente +(`r0725_statarb_demean_skeptic.ret_from_pos`, divisore **fisso 50**), non da quella che ha girato i +gate (`basket_from_positions`, divisore variabile). Sullo stato del dato del 25/07 ricostruito: +**11 numeri pubblicati su 12 riprodotti** (Sharpe netta 1.8164, lorda 2.6956, DD βˆ’2.585%, lag +1.82/1.19/0.81/0.50, Sharpe 2024 1.0297 / 2025 1.9752 / 2026 3.1078). +πŸ“Œ **Ma il numero di TITOLO e i numeri di GATE non sono mai stati la stessa serie:** la lente dei +gate da' **1.79** β€” ed e' esattamente il "Sharpe 1.79" che il docstring dello scettico gia' citava. +DSR ri-calcolato oggi sulla lente giusta: **0.983 PASS** (pubblicato 0.985) -> **il gate DSR regge**. +βœ… **L'ipotesi "il cron riscrive la storia" e' FALSIFICATA** da due prove indipendenti: (a) il +registro del cron su 54 simboli, 25/07 vs 22/08, mostra crescita di **esattamente 28 barre per +tutti** con `start_reale` invariato; (b) lo Sharpe degli **anni chiusi** si riproduce **al quarto +decimale** attraverso quattro settimane di riscritture. **Cambia una sola barra: l'ultima.** +🚨 **IL DIFETTO CHE CONTA β€” `paper_xsr` registra ~41 minuti di mercato al giorno.** +`fetch_hyperliquid` gira alle 00:30 con `END = oggi` e scrive la barra del **giorno in corso**; +`advance()` processa le barre con `ts > last_ts` e **porta `last_ts` su quella parziale** -> le 23 +ore e mezza restanti di **ogni** giorno non entrano in nessun rendimento registrato. +**Misurato** (28 barre appaiate contro il replay): **1/28 identiche Β· corr βˆ’0,045 Β· vol registrata +0,46% contro 2,74% ricalcolata** (la strategia e' progettata a 2,3%). Rapporto di varianza 0,0286 +-> ~41 min/giorno; stima indipendente dal volume: ~69 min. +βœ… **Verifica del coordinatore, indipendente:** l'ultima barra di `hl_btc_1d` ha volume **1.274** +contro **66.926** e **102.198** dei due giorni precedenti. +⚠️ **`monitor_health` lo dichiara OK e non puo' vederlo:** la serie e' fresca e senza buchi. +**Una serie fresca, completa e SBAGLIATA passa ogni controllo di freschezza.** +⚠️ Stessa **forma** di codice in `paper_statarb` (gate 27/09), che per giunta ha un docstring +"barre 1d chiuse" mentre consuma `altlib.get(...,"1d") -> resample_tf`, che **non** scarta il giorno +in corso. Segnalato come forma, non misurato -> **audit dedicato in corso su tutti e 6 i monitor**. +⚠️ **Haircut, la seconda gamba del gate 23/10, e' piu' fragile della prima:** ticket per gamba +**mediano $3,33 / medio $5,98**, **63% degli ordini sotto $5**; il pubblicato **$14,41 non ha alcuno +script committato che lo riproduca** ed e' **~2,4x ottimista**. A min-order $10 la guardia +formalmente passa ma si esegue **un ordine su cinque** β€” e il criterio pre-registrato non lo guarda. +πŸ“Œ **NUMERO ONESTO DA CITARE:** lente dei gate, sole barre chiuse -> **1.79** (finestra di scoperta) +/ **1.63** (a oggi). **Va sempre citata la coppia (lente, ultima barra chiusa)**, mai il numero nudo. +πŸ“Œ **GATE 23/10 COMPROMESSO: PARZIALMENTE** β€” ammissione NO (storia chiusa stabile, DSR regge), +**forward SI** (metrica primaria su una serie scorrelata dalla realta'), haircut SI (parametro senza +script e ~2,4x ottimista). **La finestra forward non e' recuperabile a posteriori**; riparare +`advance()` la **azzera**. Scelta fra gate letto su strumento rotto e gate rimandato con contatore +nuovo: **va decisa PRIMA del 23/10, non quel giorno.** + +### 5 β€” SKEW (r0822_skew.py, 37 trial dichiarati) +- **Q1 (skew come segnale direzionale): SCARTATO, e non per mancanza di potenza β€” la potenza c'e' e + falsifica.** Il **prezzo precede lo skew** con t da **3,13 a 9,39 su 8/8 test** e su entrambi gli + asset; in avanti il max |t| su 16 test e' **2,35** contro **2,08** atteso dal rumore, e non replica + sul secondo asset. L'unica cella con lead apparente crolla togliendo **4 giorni di rally su 24**. + DSR **0,163 FAIL**; IC95 block-bootstrap sullo Sharpe: **[βˆ’2,90, +5,86]** (ampiezza 8,76). +- **Q2 (skew come gate di de-risk): SCARTATO (non misurabile)** β€” nella finestra TP01 e' attivo + **5 giorni su 75** con maxDD **0,14%**: non c'e' sinistro. Unica cosa leggibile: **5Βͺ firma di + ridondanza col trend** (il gate spegne 16 giorni in cui TP01 era gia' flat, **16/16**). +- πŸ“Œ **Q3 β€” LEAD, ed e' il risultato della sessione. Il f=0,73 di VRP01 scomposto** (attribuzione + esatta in log, i fattori si sommano): + | causa | fattore | quota del difetto | + |---|---|---| + | **struttura a termine** | x0,869 | **42,3%** | + | spread denaro-lettera | x0,904 | 30,5% | + | **skew** | x0,920 | **25,0%** | + | fit (controllo) | x0,992 | 2,3% | + **La riga che conta: il sleeve prezza un'opzione a 7 GIORNI col DVOL a 30 GIORNI**, che nel **90%** + delle ore sta ~3 punti di vol **sopra** l'ATM a 7 giorni -> sovrapprezza **ENTRAMBE** le gambe. + Il "+0,8pp sulla gamba corta" pubblicato il 30/07 β€” che faceva sembrare **corretta** la gamba + venduta β€” e' la **cancellazione di due errori da ~3pp** (βˆ’2,97 di termine, +2,79 di skew). + ⚠️ **E il progetto aveva gia' codificato la regola il 03/07** ("riprezzare term-structure-consistent + prima di credere a un numero da struttura BS-flat") **e non l'ha applicata alla misura del 30/07**. + πŸ“Œ **Verso, e non e' quello che sembra:** `f_term` passa da **0,83** (IV-rank basso) a **0,93** + (alto) e supera **1,0 in backwardation**, mentre `f_skew` resta piatto (0,91-0,93) -> **la parte + maggiore del difetto si annulla proprio dove il gate IV-rank fa tradare il sleeve**, quindi + f=0,73 e' plausibilmente **CONSERVATIVO li'**. Non misurato: **0/22** ingressi passano il gate, e + l'unico episodio ad alta vol del campione e' un **rally**, non un crash. In un crash i due pezzi + vanno in **direzioni contrarie** e il netto e' ignoto. **`VRP_CFG["f"]` NON cambiato.** + Gate pre-registrato: rimisurare a **>=6 ingressi settimanali per asset con IV-rank>0,30** + (il collettore li accumula da solo), revisione comunque **2027-02-22**. +- βœ… Confound di modello **ESCLUSO e misurato**: `f_markfit` = **0,992** (la mark IV di Deribit + riprezza i propri mid entro l'1%) -> RR/BF sono un fatto di **prezzo**, non l'output del fit. +- βœ… **Controllo positivo cablato nello script:** con l'ancora **assunta** a :30 invece del `ts_max` + osservato compare un **falso lead** a +15m (0,102 contro 0,031) β€” *"e' l'errore che avrei + pubblicato senza quel controllo"*. +- βœ… Smile vettorizzato (512s -> 2,0s) verificato **bit-exact** contro il ciclo lento su 21.244 celle. +- βœ… **Convergenza indipendente** con l'agente TERM-STRUCTURE: **75 giorni utili** (l'altro trovava 74). + +### HL-EXEC (r0822_hl_exec.py, 132 simulazioni, 0 celle di segnale = 0 selezione) +βœ… Identita' con la produzione **prima** di ogni delta: `sim_libro` == `paper_xsr._step` a +**max|diff| 5,4e-20**; XS01 == `_xsec_returns()` a **max|diff| 0,0**. +βœ… **Verifica MAINNET** (il progetto e' nato da un feed testnet): 51/51 asset, deviazione mediana +2,21% su feed vecchio 17h contro un movimento tipico del 2,81%; cross-check su terzo venue +BTC Deribit $77.997 vs HL $77.339 = βˆ’0,84%. +**Parametri assunti -> misurati:** min order **$5 -> $10** (docs + **3.827 livelli a un ordine: +minimo $10,63, p01 $12,02, 0,0% sotto $10**); taker **5,0 -> 4,50 bps** (due fonti indipendenti +coincidono); slippage **non modellato -> 1/2 spread 0,7 bps (19 major) / 1,0 bps (coda), max 4,5**; +tier VIP: a $600-20k **sempre tier base**. +πŸ“Œ **XS01: la soglia dei $20k non esiste.** Origine del numero pubblicato: *"rumore di +arrotondamento"*, stima a occhio del diario 19/06. Misurato: haircut ~0 gia' a **$200-600 allocati**; +a $600 lo Sharpe passa **1,33 -> 1,30**. **E il meccanismo spiega perche':** gli ordini sono **due +popolazioni** β€” il ribilanciamento del segnale (ogni 10g) e' il **13% degli ordini ma il 75% del +nozionale**, ticket mediano $13,65 -> passa sempre; la deriva del vol-target giornaliera e' l'**87% +degli ordini ma il 25% del nozionale**, ticket $0,64 -> il pavimento la taglia, **ed e' gratis**. +*Contare gli ORDINI da' 16% e sembra un disastro; contare il NOZIONALE da' 75% e spiega perche' lo +Sharpe non si muove.* (Convergenza indipendente con XS-LITE, che era arrivato allo stesso meccanismo +con numeri diversi.) +βœ… **Fortuna di fase girata anche qui** (l'haircut e' un Ξ”): 10 fasi, mediana delle differenze +appaiate **+0,007**, banda [βˆ’0,060, +0,061], **0/10 fasi con danno > 0,10**. +πŸ“Œ **XSR01: soglia vera ~$3.000** (non $5.000 β€” sbagliata nella direzione **sicura**). Banda sui +pavimenti $8/$10/$12: ROTTO <=$1.000, INDECIDIBILE $1.500-2.000, **ok >=$3.000**. +πŸ“Œ **Slippage refutato come rischio #1** al livello di liquidita' odierno: lo Sharpe scende sotto +1,0 a ~20 bps/gamba = **~16 bps di slippage = 21x quello misurato**. ⚠️ L'agente dichiara il limite: +3 snapshot = **172 secondi di oggi**, una fotografia, non tre osservazioni. **E' il MARGINE (21x) che +regge la conclusione, non il livello.** +🚨 **Due conseguenze che l'operatore deve vedere:** +1. **Il "$20k" di XS01 e il "$20k" della DECISIONE DI VENUE (26/07) sono due cose diverse che + CLAUDE.md ha conflato.** Quello di XS01 **cade**; **quello di venue REGGE intatto** β€” e' una + decisione dell'operatore su un altro asse (la rovina da fallimento exchange), non sull'eseguibilita'. + XS01 resta fuori dal live **per la decisione di venue, non per taglia**. +2. **La gamba di eseguibilita' del gate 2026-10-23 e' tarata su un vincolo che a $5.000 non morde** + (haircut li' βˆ’7%...+2%): *una condizione pre-registrata che non puo' fallire non e' una + condizione*. L'agente **non ha anticipato il gate**: la soglia Sharpe>=1,0 resta al 23/10. +⚠️ **Terza spiegazione dell'1,82, complementare alle altre due:** `r0725_statarb_basket_gate` +addebita *"2 gambe per coppia"* = 50 alt + **50 gambe BTC fantasma**, ma il demeaning **annulla +algebricamente** la gamba BTC (`sum(w)=0`) β€” che e' la ragione stessa per cui XSR01 esiste. +*L'intuizione del demeaning era stata applicata all'AMPIEZZA e non ai COSTI.* +πŸ“Œ **Quadro conciliato delle tre lenti** (i due agenti concordano sul fatto, non sull'attribuzione +del singolo decimale): titolo `demean_skeptic` **1,82** Β· gate `basket_gate` **1,75-1,79** (fee +raddoppiate su una gamba inesistente) Β· monitor `paper_xsr` **2,21**. **La lente che ha girato i +gate e' quella PESSIMISTICA** -> il **DSR 0,983 PASS e' stato calcolato su una serie conservativa**. + +### SLIP-AUDIT (r0822_slip_audit.py, 18 fill reali, ~14 varianti dichiarate) +βœ… **Fee sul NASTRO: 5,00 bps/lato prima del 2026-08-01, 3,50 dopo (16/16 esatti).** Il taglio +Deribit e' visibile nei fill reali -> conferma **indipendente** di `fee_watch` (che legge il tier +dichiarato, non le esecuzioni) e della regola pre-registrata del 26/07 (<=5 bps -> non si tocca +nulla). **Il listino si e' mosso nel verso buono.** +- Slippage d'esecuzione (riferimento 5m): mediana **βˆ’4,65 bps**, IC95 [βˆ’34,6; +20,6]; statistica + normalizzata **+1,54 bps** [βˆ’1,30; +4,47]. Conversione con la curva pubblicata: **dSharpe βˆ’0,026 / + dCAGR βˆ’0,35 pp**. +- ⚠️ **Autocorrezione della propria potenza:** il null **empirico** e' **1,43x piu' largo** + dell'uniforme assunto -> MDE vera **7,2 bps**, non 5,0. **Con n=18 nessuna via statistica risolve + 1,5 bps** β€” dichiarato invece di spacciare l'IC per una risposta. +πŸ“Œ **Esperimento naturale, il caso peggiore del campione:** il fill del **sabato 18/07** (ETH $74) e' +stato il **21,9% del volume della sua barra 5m**, e quella barra ha avuto un range di **1,08 bps**, +col nostro print a meta' (q=0,48). *Un ordine che prende un quinto del nastro e lascia il prezzo +entro 1 bps* e' la prova diretta che a questa taglia il libro **non cammina il book**. +🚨 **La misura ha una DATA DI SCADENZA, e va scritta accanto al numero:** la partecipazione scala +lineare -> quel 22% diventa **172% di una barra 5m a $5.000** e **687% a $20.000**. **Non si +estrapola: va rifatta a ogni salto di taglia.** E' il muro di eseguibilita' gia' noto ma sull'asse +dell'**attrito** invece che del min-order. ⚠️ E il caso peggiore cade di **sabato**, dove TP01 fa il +**38%** del proprio gross. +⚠️ **Due difetti del dato, trovati PRIMA di misurare (e ciascuno avrebbe prodotto un numero +plausibile e falso):** +(a) `ts_utc` in `book_executions.jsonl` **non e' l'ora del fill** ma l'etichetta dell'ultima barra +del feed (**18/18 dicono `00:00:00`**; il fill #1 e' etichettato 08/07 ed e' stato eseguito il +**14/07** = impronta del feed-freeze gia' noto). L'ora vera esiste **solo** in `logs/cron_book.log`: +senza recuperarla, ogni confronto "al prezzo dell'ora del fill" misurerebbe **la mezzanotte**. +(b) Il feed certificato e' il perpetual **INVERSE**, il libro trada il **LINEARE USDC** β€” **4Βͺ +occorrenza** dello schema `fee_watch`: misurare lo slippage sul feed certificato avrebbe misurato +**la base**, non lo slippage (base misurata: BTC βˆ’0,03 bps, ETH βˆ’0,27). +⚠️ Con n=18 **la media in bps e' inservibile** β€” un solo giorno (19/08, ETH +14% in 6h) la ribalta: +si cita la **mediana**. E `REF-A` (βˆ’50 bps) **non e' slippage**: e' la differenza di FASE fra l'ora +del cron e la chiusura giornaliera, gia' misurata bene il 26/07 su 24 ancore appaiate. + +### SCETTICO su GROWTH-POLICY (r0822_leverage_skeptic.py, ~175 celle, griglia ridotta a k<=5x e dichiarato) +βœ… **Replica superata TRE volte** prima di ogni numero nuovo: marginali del gap 25/07 (p50 βˆ’0,16 / +p90 βˆ’1,03 / p99 βˆ’2,71 contro βˆ’0,17 / βˆ’1,03 / βˆ’2,70), meccanismo del decile (~3x, pubblicato ~3x), +e €500/mese -> **14,7a / P<=20a 99%** al numero pubblicato. +βœ… **Controllo positivo superato:** una lente *indipendente* (gap rimescolato) sovrastima il maxDD di ++0,13pp su +0,45 e i breach daily-loss di **1,5-2,4x** β€” stesso verso e taglia del finding 25/07 +("2,0-2,9x"). **La macchineria ha potenza, quindi il suo "nessuna differenza" vale.** +βœ… Risoluzione MC dichiarata: **0,01%/a** nella regione eseguibile, 0,05pp sul Ξ” appaiato di maxDD -> +le differenze riportate (0,45-0,76pp) sono **9-15x la risoluzione**. +πŸ“Œ **RISPOSTA ALLA DOMANDA: NO, l'effetto e' SOTTO-proporzionale.** Il rapporto fra le due maxDD e' +**piatto a 1,025-1,037** su tutta la griglia e Ξ”/k **cala** (0,478% -> 0,217%). **Meccanismo: il +maxDD e' una statistica MULTI-GIORNO, il wick e' un fatto di UN giorno.** Tempo al bersaglio +**identico** fra le due lenti; guadagno appaiato 1,00x->1,50x = **2,88 anni** in entrambe. +Nessuna liquidazione a nessun k<=5x sotto nessuna lente (servirebbe **12,3x** sul peggior giorno vissuto). +🚨 **IL RISULTATO TRASFERIBILE β€” la lezione del 25/07 e' vera e NON si trasferisce.** Su una regola a +**UN giorno** l'accoppiamento cambia tutto: a soglia daily-loss 5% e k=1,00-1,25 close-only conta +**0,00 breach/anno** dove il vero ne conta **0,40 e 1,21** (`acc./close = INF`, cecita' esatta); a +soglia 3% e k=1, **0,40 contro 2,28 = 5,7x**. Su una statistica **multi-giorno** lo stesso difetto +vale **3,5%**. ⚠️ **Non assolve close-only in generale: sul canale PROP/FUNDED, dove il vincolo +binding E' una regola di daily-loss per-conto, la lente accoppiata resta OBBLIGATORIA.** +⚠️ **CORREZIONE a una spiegazione pubblicata (il fatto regge, la spiegazione no):** il **26,8%** di +giorni con `m == R` **non** sono "il giorno brutto che chiude sul minimo" β€” stanno nei decili +**CENTRALI** (56-88%), i giorni in cui il libro e' **flat**. Nel decile peggiore chiude sul minimo +il **4%**. E un giorno di crash **chiude quasi sul proprio minimo** (gap medio nel 5% peggiore +βˆ’0,62% contro βˆ’1,75% nel 5% migliore) -> **la lente accoppiata non peggiora lo stress di coda.** +πŸ“Œ **Il gradino 1,00->1,50 resta NON AUTORIZZATO** β€” cade **una** delle tre riserve, non le tre. +Restano: drift stimato su 7,4 anni, e coda assente dal dataset (un giorno βˆ’10%/anno porta k* a 2x). +Unica colonna a favore della prudenza: **P(sfondare una soglia FISSA) e' amplificata piu' del maxDD** +(1,26x a k=1) perche' vicino alla soglia la densita' e' fitta β€” **e' quella la colonna con cui si +costruisce una frontiera iso-rischio, non il maxDD mediano**. +πŸ“Œ **ATTACCO 2 β€” il 12,14% di TP01 e' VERO, ma il meccanismo dichiarato e' META', e la strada +implicita e' REFUTATA.** Scomposizione esatta, verificata sullo sleeve di **produzione**: +`20,00% x 0,682 (dentro la gamba) x 0,890 (diversificazione BTC/ETH, corr 0,59) = 12,14%`. +E il x0,682 sono **due meta' quasi uguali**: flat nel **44,3%** delle barre (x0,746) **e** convinzione +**parziale** quando non e' flat (`tsmom_blend` = media di 3 segni: 1/3 o 2/3) (x0,789). +**"TP01 e' long-flat" spiega circa META'** β€” e la differenza non e' accademica: essendo +*convinzione*, **alzare `target_vol` aumenta la size proprio nei giorni in cui il segnale e' piu' +debole**. Il terzo fattore (x0,89) **non e' un difetto d'etichetta: e' diversificazione**, ed e' +cio' che si vuole. +⚠️ **E TP01 e' un caso particolare, non la regola: XS01 dichiara lo stesso 20% e ne realizza 20,59%.** +🚨 **`target_vol` e `k` NON sono la stessa leva** (e non per il leverage cap, che non morde: 0-1% di +barre al cap fino a tv=60%): **`target_vol` scala il 75% del libro e lascia fermo il 25%** -> a +tv=30% il libro non e' piu' 75/25 ma **82/18**, a 60% **90/10**. E' simultaneamente una leva **e un +tilt di pesi**, e il tilt **FALLISCE `weights_tilt_null`** (gate_pass=False, delta_insample βˆ’0,0815 +a tv=30% / βˆ’0,2187 a tv=60%). A pari scala 1,50 il libro via `target_vol` fa Sharpe **1,72** contro +**1,82** via k. -> **la strada pulita per un puro cambio di SCALA e' il cap di config, non il target_vol.** +βœ… **Cosa cambia dei numeri pubblicati: NIENTE di misurato** β€” ogni Sharpe/maxDD/muro/traiettoria e' +calcolato sulla **serie realizzata**; `target_vol` non compare in nessuna formula di rischio a valle. +❌ **Ipotesi dell'agente refutata dal suo stesso numero:** i contributi di rischio sono **73/27** +contro pesi 75/25. **Cio' che resta:** sbaglia chi legge l'**etichetta**, non chi legge i **pesi** β€” +un lettore che prendesse "20%" per buono attribuirebbe a TP01 l'**89%** del rischio invece del +**73%**, cioe' sbaglierebbe di 16 punti proprio la domanda *"quale gamba mi fa male"*. +πŸ“Œ Il test di guardia `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` **non protegge "la leva +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). + +### SCETTICO su VOL-SIZE (r0822_volsize_skeptic.py, 43 valutazioni x 23 ancore) +βœ… Replica **bit-exact** prima di ogni attacco: gamba 50/50 vs `sleeves._skyhook_returns()` +**max|diff| = 0,0**; e il titolo riprodotto **esattamente** (+0,070 ISO dShFULL 23/23, +0,101 HOLD). +🚨 **RITIRA UNA REGOLA PUBBLICATA POCHE ORE PRIMA β€” il difetto di contabilita' NON ESISTE.** +`backtest_signals` fa `equity[i_ent:i_ex+1] = capital`: il P&L e' contabilizzato al giorno +d'**INGRESSO** in **291/293 (99,3%)** dei trade multi-giorno, all'uscita in **4**. Troncando i dati +alla barra d'ingresso e ricalcolando le size da zero: **50/50 identiche, max|diff| = 0,0**. E la +"riparazione" e' un **no-op bit-exact** (max|diff| 6,7e-16, Sharpe identico a 4 decimali). +**Il "+0,04 fantasma" era interamente `[L sulla gamba COMBINATA] βˆ’ [L PER ASSET]`** β€” due varianti +entrambe causali β€” **e la variante scartata come "non causale" era la MIGLIORE sull'hold-out +(+0,039)**. *Il costo di una regola derivata da un difetto inesistente e' gia' stato pagato.* +πŸ“Œ **REGOLA CORRETTA (piu' debole e vera):** *"applicare un overlay alla data d'ingresso"* resta un +buon consiglio **generale**, ma su questo motore le due date **coincidono** β€” e va verificato con un +troncamento bit-exact **prima** di scartare un ramo, non dedotto dalla forma dell'equity. +🚨 **IL GATE PRE-REGISTRATO AL 22/12/2026 E' GIA' FALLITO OGGI, e nel modo piu' netto.** L'agente +aveva scritto: *"se l'effetto e' UGUALE su V1, la spiegazione e' sbagliata anche se il numero +regge"*. Misurato: |effetto| mediano di famiglia **V1 0,067 contro V2-DD 0,032** β€” l'inverse-vol +funziona **il DOPPIO dove lo stop e' gia' in ATR**, cioe' esattamente al contrario della previsione, +in **8 celle su 8** con **23/23 ancore positive**. E il test **ha potenza** (su V2-DD la famiglia +arriva a +0,067, quanto il LEAD). +πŸ“Œ **La spiegazione avversaria e' CONFERMATA e la sua forma minima e' un binario.** `BIN L>=med` +(size **0/1** alla mediana espandente, nessuna magnitudine, 194/406 trade scartati) vale +**+0,080 FULL (20/23) = 115% del LEAD**. Controllo di segno superato: il binario **INVERSO** fa +**βˆ’0,159 (0/23)**. Canali: **FREQUENZA +0,051 (23/23)** contro **MAGNITUDINE +0,003 (17/23)**. +I trade tenuti hanno net medio **2,03% contro 0,36%** degli scartati. +βœ… **Un attacco dello scettico e' FALLITO, e lo dichiara:** l'asse della finestra **non e' un picco** +β€” l'argmax in-sample-only cade su **w=90 = la cella pubblicata**, corr(IS, HOLD) fra le 9 finestre +**+0,63**, plateau da 45 a 270. Sotto w=45 la finestra contiene **1-3 barre attive** (colonna +misurata) e il segno si rovescia: non e' fragilita', e' campione vuoto. +**-> quindi anche la mia nota "picco, non plateau" nel filone 8 va letta come SUPERATA.** +Stima onesta se `w` e' scelto (mediana di famiglia): **+0,025 FULL**. +πŸ“Œ **CONSEGUENZA:** il LEAD non ha piu' una condizione aperta da attendere β€” la sua gamba +esplicativa e' chiusa oggi. Se qualcuno vorra' portarlo avanti, **il candidato giusto non e' la +formula ma il binario**: meno gradi di liberta', **dimezza gli ordini** (migliora l'eseguibilita' a +$635), 115% sul FULL β€” ma paga in fragilita' d'ancora (20/23 contro 23/23, banda che include βˆ’0,061, +hold-out +0,072 contro +0,101). **Resta un terzo della fortuna d'ancora del libro stesso (+0,196): +niente qui giustifica di toccare pesi, cron o config.** + +### MONITOR-AUDIT (r0822_monitor_audit.py) β€” audit di PRODUZIONE, 6 monitor, 4,7 s +βœ… **Metodo che rende il risultato non contestabile:** l'audit **riesegue `advance()` di produzione +tale e quale**, sostituendo solo `_append` con un raccoglitore in memoria -> nessuna +reimplementazione, quindi **nessun drift fra audit e monitor**. Sola lettura verificata (gli mtime +di `data/paper_*` sono rimasti quelli del cron delle 00:35). +| monitor | barra parziale | barre coincidenti | corr | **min/giorno misurati** | gate | ricostruibile | +|---|---|---|---|---|---|---| +| `paper_statarb` | **SI'** | **0/46** | +0,297 | **4** | **STATARB 27/09** | **SI', bit-exact** | +| `paper_dvolspread` | **SI'** | 0/28 | +0,262 | **2** | DVOLSPREAD 24/10 | SI', bit-exact | +| `paper_xsr` | **SI'** | 1/28 | βˆ’0,045 | **41** | XSR01 23/10 | SI', bit-exact | +| `paper_portfolio` | **SI'** | 7/56 | +0,245 | 19 | β€” (dashboard) | si', non al bit | +| `paper_prevday` | si' (1 barra su 24) | **1259/1319** | **+0,997** | 1.438 | β€” | si', bit-exact | +| `paper_combo` | **NO** | 7/37 | +0,991 | 1.402 | β€” | si', non al bit | +πŸ“Œ **La patologia e' la stessa, la TAGLIA varia di un fattore 20** β€” e dipende da quanto pesa la +barra piu' recente nel segnale, esattamente come la lezione del 26/07 su SKH01 contro TP01. +🚨 **Il gate STATARB del 27/09 si ribalta su DUE criteri su tre:** Sharpe registrato **+1,96** (sopra +la soglia 0,5) contro **βˆ’2,02** ricostruito; maxDD **2,2% contro 15,3%** (guardia <10%). *Fra 36 +giorni si sarebbe deciso su una serie che misura 4 minuti di mercato al giorno.* +⚠️ XSR01 e DVOLSPREAD danno lo **stesso verdetto** oggi, ma su numeri implausibili (Sharpe βˆ’15,8 su +28 barre = oltre 4 SE). **`implausible_sharpe` esiste dal 26/07 e non e' mai stato puntato sulle +serie forward.** +βœ… **LA BUONA NOTIZIA, ed e' misurata: la finestra forward NON va persa.** Il replay *e'* la serie +corretta sulla stessa finestra, con config congelata e codice di produzione. Le due condizioni sono +**provate due volte in modo indipendente**: (a) `paper_prevday` ha **1427/1488 barre identiche al +bit** dopo 62 notti di riscrittura (il feed **non tocca** le barre chiuse); (b) le 6 barre di +`paper_statarb` recuperate dopo un guasto coincidono **al bit**. +-> **La decisione non e' "riparare e azzerare" ma "riparare `advance()` + RIGENERARE", e nessuna +delle tre date di gate si sposta.** Anche nell'ipotesi pessimista (`--reset`) l'unico gate che +slitterebbe e' STATARB, di **+18 giorni**. +πŸ“Œ **Dettaglio che vale una regola: le uniche barre corrette di `paper_statarb` esistono perche' il +feed si e' ROTTO.** `rebuild_history` e' abortito per 7 giri consecutivi (09-15/07, EPERM sul +backup, gia' riparato); al ripristino il monitor ha recuperato 7 barre in un colpo, **6 delle quali +gia' chiuse** -> coincidono col replay al bit. *Il guasto ha prodotto da solo il controllo che +serviva a diagnosticare l'altro guasto.* +πŸ“Œ **`paper_combo` e' sano PER CASO:** aspetta la chiusura di una **borsa**, non per progetto. +Nessuna riga di codice chiede la barra chiusa β€” **se la sua griglia cambiasse diventerebbe rotto in +silenzio.** +βœ… **Guardia raccomandata (NON implementata), e ha il pregio di essere banale:** +`ts_ultima_barra + cadenza <= mtime del file di serie`, grazia 5 min. **O(1)**, nessuna strategia da +rieseguire, usa solo cio' che il monitor gia' scrive; segnala **5/6** e **tace sul sano**; controllo +positivo sintetico **6/6 nei due versi**. Il vol-ratio va **dopo, non al posto**: copre un guasto +diverso (drift monitor-strategia) e **non vede `paper_prevday`** (rapporto 0,999). +⚠️ Limiti dichiarati: i "minuti/giorno" sono una lettura del **rapporto di varianza** (assume random +walk sull'intervallo troncato), non un cronometro β€” il cronometro diretto e' 32-35 min; e per +`paper_portfolio`/`paper_combo` la ricostruzione **non sara' al bit** perche' la gamba GTAA legge +`ADJUSTED_LAST` di IB, ri-aggiustato all'indietro. + +### 1 β€” PROP-ALLOC (r0822_prop_alloc.py, ~3.300 valutazioni MC di cui **360 di selezione**) +βœ… **Previsione REGISTRATA PRIMA e confermata:** dal conto del gambler's ruin, alla leva minima che +porta a +T dentro l'orizzonte resta `theta ~ 2*Sharpe^2/T` -> **la vol sparisce e l'argmax sui PESI +deve coincidere con l'argmax di Sharpe**; il contenuto vero sta nella **LEVA**, che lo Sharpe non +determina. Misurato: **e' cosi'**. +| | pesi TP/SKH/XS | leva | **J** | P(pass) | P(vivo 12m) | E[pay/anno] | +|---|---|---|---|---|---|---| +| argmax **J** | 25/25/50 | 0,75 | **0,738** | 82,2% | 89,8% | $8.556 | +| argmax **Sharpe** | 38/25/38 | 0,75 | 0,721 | 77,3% | 93,3% | $7.796 | +| **libro LIVE 75/25** | 75/25/0 | 0,50 | **0,335** | 38,6% | 86,8% | $4.109 | +Differenze **appaiate**: argmaxJ βˆ’ argmaxSharpe **+0,0164 Β± 0,0066**; argmaxJ βˆ’ LIVE **+0,3996 Β± +0,0114**. **-> Riallocare per la barriera vale il 4% di cio' che vale diversificare.** +βœ… **Replica di controllo superata:** la politica MISTO del 25/07, stessa macchina, de-luck 0,60 -> +P(>=50/g) **4,1%** / P(zero) **53,7%** contro il pubblicato 5,6-6,5% / 52%. +πŸ“Œ **Null dei tilt casuali β€” e il risultato e' che il PUNTO non conta:** argmax al 98,5Β° pctl contro +firma best-of-45 di 97,8Β° = **indistinguibili**, e **il massimo di 200 pesi casuali (0,739) EGUAGLIA +l'argmax (0,738)**. *Conta stare nella regione diversificata, non il vettore.* +πŸ“Œ **La leva alta non compra probabilita', compra fretta:** J massimo a **0,75x**, E[payout] massimo +a **1,00x**; P(pass) senza limite di tempo e' **monotona decrescente** (98,9% a 0,50x -> 65,9% a +1,50x). E **la barriera piu' larga sposta l'ottimo verso l'alto: il vincolo e' la regola di DD, non +il libro.** +πŸ“Œ **Il 75/25 del libro live NON e' la ripartizione giusta per un conto a barriera**, e non e' +artefatto di finestra: sul campione **2019+ che contiene il 2022** l'ottimo della coppia crypto e' +**38/62 @0,50x** (J 0,521 contro 0,443) β€” stesso verso del 2024+. +🚨 **LA BANDA, non il numero.** P(>=50/g) da €600 in 36 mesi con la scala di conti: +**42% al drift pieno Β· 7,7% allo stress moderato (SKH Γ—0,50, XS Γ—0,50) Β· 0,7% allo stress severo** +(SKH Γ—0,33, XS Γ—0,00) β€” e allo stress severo e' **peggio del libro live**. **Il numero da citare e' +la banda.** Tetto strutturale: cap $200k (HYRO) + $400k (FTMO) = **max 6 conti**, oltre servono altre +firm, ognuna col proprio rischio di controparte. +⚠️ **Onesta' dell'agente sulla distorsione che NON ha potuto rimuovere:** la banda d'ancora **non e' +girata** sotto questa lente (fuori budget), quindi l'ottimo gira sull'ancora **canonica** e si +appoggia a **SKH01, che il progetto misura come lo sleeve con la fortuna d'ancora piu' grande da +restituire** -> *la distorsione e' a suo favore e lo dichiara*. +⚠️ E la sotto-pesatura di **TP01, lo sleeve difensivo**, e' misurata su un campione **senza +sinistro** (2024-2026 non contiene un crash): **su una barriera assorbente quella scommessa si paga +una volta sola.** +⚠️ Assunzione dichiarata e **non verificata con le firm**: che una firm crypto listi i 19 alt +Hyperliquid con short abilitato. Se non li lista, XS01 sparisce e con lui il risultato. +⚠️ Errore proprio corretto in sessione: `paired_delta` **ignorava lo stress** e stampava un delta +non-stressato accanto a righe stressate -> *un confronto appaiato deve ereditare TUTTI i parametri +della riga in cui compare*; senza quella guardia avrebbe concluso che il risultato era robusto +quando non lo e'. Ora c'e' un `assert` che lega il delta alle due colonne stampate. + +--- + +# ONDATA 2 β€” 2026-08-22 sera (7 filoni) + +Mirata su cio' che la prima ondata ha lasciato scoperto, **inclusi i falsificatori che gli agenti +stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, stessi gate. + +| # | filone | domanda che decide | verdetto | +|---|---|---|---| +| 21 | XS01-OOS | il vantaggio del PROP-ALLOC poggia sul drift di XS01 misurato sulla sua finestra di scoperta: regge fuori? | **LEAD** β€” regge, ed e' PIU' GRANDE fuori | +| 28 | DEPEG | il falsificatore che l'autore di 21 ha nominato contro se stesso: il depeg USDT contamina il ranking? | **SOPRAVVIVE** β€” il venue sposta 0,01 di Sharpe | +| 29 | PROP-RECAL | J rifatto con XS01 dalla storia lunga: il canale funded si apre? | **LEAD ridimensionato ~5x** β€” 42% β†’ **7,8%** | +| 30 | LISTINO | `GATE PROP-01` gamba (a): la firm lista >=10 delle 13 gambe con short? | **PASS 13/13** β€” verificato sul venue, non sul sito | +| 31 | GATE-RECON | cosa direbbero i 3 gate pre-registrati se le serie forward fossero riparate | **1 gate su 3 ribaltato** β€” e la premessa e' CONFERMATA | +| 32 | SKH-ANCHOR-PROP | `GATE PROP-01` gamba (b), datata 31/10: girata oggi | **PASS 23/23** β€” e il numero scende ancora, a **4,4%** | +| 33 | WORST-DAY | il peggior giorno del libro, MISURATO: il gradino di leva e' autorizzabile? | **LEAD** β€” l'aritmetica passa, **il knob non esiste** | +| 34 | CC01-2022 | il carry riaperto sui dati che gli mancavano per costruzione | **RESTA SCARTATO** β€” per la ragione giusta, ma non quella pubblicata | +| 35 | FUNDING | il costo mai prezzato di TENERE una posizione aperta | **LEAD** β€” **βˆ’2,16%/anno di drift**, muri **+17,5%** | +| 36 | PIANO-VERO | la tabella congiunta fisco+funding, che non esisteva | 🚨 **€250/m: P(20a) 92% β†’ 14-26%** | +| 37 | GATE-C | l'ultima gamba del gate funded + il biglietto come opzione | **PASS 2/2** β€” gate senza date; **P(β‰₯50/g) 4,4% β†’ 2,6%** | +| 38 | SCALE-SPEC | specificare il knob di leva che non esiste | **LEAD** β€” specificata, **1 nodo irrisolto** sul disaster-SL | +| 39 | FUNDING-AVOID | evitare il funding esprimendo il libro sui datati | **SCARTATO** sui datati Β· **LEAD** sullo **spot** | +| 40 | SL-ANCHOR | il nodo che bloccava il gradino di leva | **SCIOLTO** β€” rotolante, ma il timore e' refutato | +| 41 | TP01-SIZE | una size che risponde alla CONVINZIONE batte il vol-target uniforme, a pari vol realizzata? | **SCARTATO** β€” il meglio di 60 celle vale **+0,30pp** di CAGR iso-vol, e l'ottimo sta ai **due estremi opposti** nelle due meta' del campione (**Spearman ShIS/ShHOLD βˆ’0,527**: scegliere in-sample e' peggio di una moneta) | +| 42 | TP01-LS | la gamba short del trend a 1d paga il proprio costo, o e' il 2022? | **SCARTATO** β€” toglie **βˆ’1,91%/anno di drift in 0/24 ancore**, fallisce l'iso-vol, +8,80pp di maxDD in 24/24, e **senza il 2022 il divario RADDOPPIA** | +| 43 | DATA-UNUSED | quali dati il progetto possiede e nessuna strategia legge β€” e uno di essi sostiene un'ipotesi? | **SCARTATO** β€” **la scorta di dati e' esaurita**: un solo dato mai letto ha storia vera (flussi exchange CoinMetrics 2018-2026) e misurato sta **SOTTO il massimo atteso dal puro rumore** | +| 44 | DEPOSIT-TIMING | un calendario di versamento CONDIZIONALE batte il piatto, a pari soldi? | **SCARTATO 0/12 in ogni lente** β€” il buy-the-dip compra davvero il **51% piu' in basso** e arriva al capitale-rendita nel **6,3%** dei casi contro l'**85,4%** del piatto | +| 45 | SPOT-NETTING | separare TP01 e SKH01 su due strumenti: quanto vale, e il margine lo uccide? | **LEAD REGGE, SEPARAZIONE SENZA VALORE** β€” il margine non e' il vincolo (copertura **50x**, invariante al capitale), ma **T1 sbloccato e' un DECLASSAMENTO** (βˆ’0,048 contro il path che gira) e il netting perso **risparmia** commissioni: il lead vale **il funding e basta**, [+1,55%, +1,66%]/anno; a bloccarlo e' **il codice di sizing**, non Deribit | +| 46 | TAIL-HEDGE | comprare put deep-OTM come assicurazione statica compra DD a prezzo migliore del de-levering? | **REFUTATO** β€” la premessa cade **prima del prezzo**: il maxDD **SALE in 162/162 celle**, a ogni lente di f (f=1,00 regalato compreso). Meccanismo: **beta del libro al sottostante +0,076** | +| 47 | ANCHOR-ENSEMBLE | eseguire l'ensemble delle 24 ancore invece di sceglierne una e correggere il numero | **ESEGUIBILE MA INUTILE SU TP01** β€” il drift **non puo'** cambiare (algebra: `lordo_ens == media(lordi)`, 1,4e-17); **il segnale sta su SKH01** (+0,312 di Sharpe mediano, maxDD 23,3%β†’16,6%), ma dentro il libro **non si distingue da zero**. E la premessa del 02/07 era sbagliata | +| 48 | VOLVOL | la volatilita' del DVOL (secondo momento, mai usato) contiene cio' che livello e spread non contengono? | **SCARTATO** β€” e' davvero una variabile NUOVA (ortogonalita' PASS, max \|corr\| 0,396) ma **guarda INDIETRO**: picco a lag **βˆ’5/βˆ’10** e parziale con la vol futura **NEGATIVA**. Quinto lato del filone DVOL chiuso | +| 49 | TP01-TWIN | il 75% del libro dipende da UNA definizione di trend: un ensemble di meccanismi protegge meglio? | **REFUTED** β€” protegge **PEGGIO** nei 4 anni piu' recenti a **0/24 ancore** (2022: **3,2x peggio**) e costa **βˆ’0,127** di Sharpe hold-out al libro. E i meccanismi **non erano ridondanti**: corr di posizione 0,61 | +| 50 | BOOK-3RD | qual e' il miglior TERZO sleeve realmente eseguibile sul libro live a $635 β€” e a quale capitale cambia? | **NESSUNO AMMISSIBILE OGGI** β€” sette candidati, **sette muri diversi, e nessuno dei sette e' una soglia di capitale**. 🚨 Il piu' forte misurato (**PREVDAY**) sta in monitor da giugno **senza gate e senza deflated-Sharpe** | +| 51 | NEWDATA-SCOUT | se la scorta e' esaurita: cosa iniziare a raccogliere OGGI, che fra 12-24 mesi apra una famiglia? | **RACCOGLIERE 2 FONTI SU 10** β€” catena USDC (**+18%, non +90%**) e una **campagna a termine** di book depth; il criterio di ricostruibilita' ne uccide 4 da solo e **refuta una raccomandazione del 22/08** | +| 52 | SKEPTIC-SPOT | il lead dello spot (+1,55-1,66%/anno) sopravvive a un attacco deliberato? | 🚨 **REGGE A $600, SI ANNULLA A $272k** β€” 5 attacchi su 6 falliscono; quello che morde non e' nei numeri, e' **nella taglia a cui vengono spesi**: il lead sparisce **esattamente al muro che pretende di spostare** | +| 53 | SKEPTIC-LEVA | il gradino di leva 1,25x sopravvive a un attacco deliberato, col funding dentro? | **REGGE 5/6, INCRINA 1/6** β€” il **1,25x sopravvive**, il **1,50x NO**; k massimo difendibile **1,40**. L'incrinatura e' il ramo `eq_fallback`, dove **la scala non apre il buco: lo MOLTIPLICA** | +| 54 | PREVDAY-GATE | il piu' forte candidato misurato sta in monitor da giugno senza gate: quanto vale al gate onesto? | **GATE PREVDAY-01 SCRITTO β€” 8/10 condizioni oggi**, e le due che mancano sono la stessa cosa: 🚨 **la cella che GIRA non e' quella che la selezione onesta sceglie** β€” e la selezione compra il libro **LONG-FLAT**, cioe' butta via la gamba per cui PREVDAY fu promosso | +| 55 | COSTO-CAPITALE | i muri sono calcolati a costo d'esecuzione COSTANTE, ma il costo cresce col capitale: dove sta il muro vero? | **NON MORDE** β€” il muro si sposta di **+0,6%** (dentro la risoluzione MC), la saturazione sta **30x sopra** ($9,5-10M), e πŸ“Œ **alla taglia di oggi il costo mancante ha segno NEGATIVO** | +| 56 | XS-AMPIEZZA | su un universo povero (11 gambe), si recupera ampiezza EFFETTIVA senza aggiungere gambe? | **NO β€” e la caduta NON era ampiezza (solo il 14%)**: e' **selettivita'** (recuperabile ma sotto l'MDE) **piu' un IC che su quelle 11 gambe vale ZERO**. 🚨 E `k=5` **non e' invariante di scala** | +| 57 | CRITICO | l'ondata ha prodotto qualcosa che avvicina i 50 EUR/giorno β€” e i suoi numeri di testa reggono a una verifica indipendente? | **NO al mandato, SI' alle repliche** β€” 6 numeri di testa riprodotti (4 muri al dollaro, funding βˆ’2,1597%, dShFULL βˆ’0,4502); ma 🚨 **il muro $313k e' una MEDIANA con banda [$187k, $1,14M]** e a βˆ’2 SE il traguardo non esiste, e 🚨 **Β«positivo in N/N ancoreΒ» vale ~2 osservazioni**, con la banda **4,3x piu' stretta dell'IC**. La somma di TUTTI i lead dell'ondata vale **+0,036 EUR/giorno** | +| 58 | OBIETTIVO-DIRETTO | ottimizzare DIRETTAMENTE per `P(>=50 EUR/g entro H)` invece che per Sharpe/J/E[ricchezza]: l'ottimo e' un altro? | **L'OTTIMO E' UN ALTRO, E LA DOMANDA HA DUE RISPOSTE** β€” 🚨 l'obiettivo del progetto ha **due definizioni operative in uso** che rispondono in modo opposto alla stessa domanda: a **FLUSSO** (la lente del canale funded) il massimo a 36 mesi e' **33,9%**, a **RICCHEZZA SOSTENIBILE** (la lente dei muri e del piano) e' **0,33%**. E l'ottimo di `P(>=50/g)` **NON e' l'ottimo di J**: 37,3% contro 13,9% alla leva argmax-J. Il libro di oggi a 36 mesi: mediana **0 EUR/g** di flusso (non preleva), capitale mediano $22.467 = **3,59 EUR/g** di rendita perpetua. 316 trial, 1500 percorsi | +| 59 | SCALA-PROP | l'aritmetica diretta: quanti conti funded, a che leva, per fare 50 EUR/g β€” e quale vincolo morde per primo? | **NON AVVICINA** β€” a regime **13,36 EUR/g per conto da $100k** β‡’ servono **4 conti** ($400.000 di nozionale = i cap di 2 firm, $2.316 di biglietti). Ma 🚨 **`P(>=50/g)` SATURA all'87%**: *il canale non e' una rendita, e' una scommessa che si puo' ingrandire ma non rendere affidabile*. Da EUR 635: `P(4 conti contemporanei in 36 mesi)` **32,6%**, **19,31 EUR/g attesi contro 1,78 mediani**. 178 celle | +| 60 | LADDER-REDO | la politica multi-conto Β«MISTOΒ» fu scelta con numeri poi corretti di 16x: e' ancora l'ottimo? | 🚨 **NO β€” il vincitore del 25/07 non e' piu' l'ottimo**: oggi **MISTO-A `P(>=50/g)` 9,27%** contro **MISTO 6,60%** (288 celle, 369 valutazioni MC). Ordine odierno MISTO-A > CONC-A > MISTO > CONC-DIV > SPARSO > CONC-2SL. **Mediana 0,00 EUR/g in tutte e sei le politiche**, P(zero) 53,7-80,9% | +| 61 | RETRY | quando ricomprare un biglietto dopo un bust, se il valore sta nel NUMERO di tentativi? | **NON MORDE β€” e il fatto trovato e' un altro:** la politica di ri-acquisto **cablata** (compra subito, il biglietto piu' grande, in tutti e 6 gli slot, con qualunque soldo) **non era mai stata dichiarata come scelta**, ed e' la piu' aggressiva della famiglia β€” *da li' escono TUTTI i numeri del canale funded*. Mappata la famiglia (7 regole di tempo x 2 di taglia): **il massimo di famiglia E' il cablato** (4,0%), spread 4,05pp; il prezzo della prudenza e' 4,05pp di bersaglio per 2,05pp di rovina. πŸ“Œ **Β«SOLO DAI PAYOUTΒ» non vincola nulla a $654**: dopo il primo biglietto restano $75, sotto il biglietto piu' economico β‡’ ogni ri-acquisto viene GIA' dai payout. πŸ“Œ **Ottimizzare la politica vale +0,00 EUR/g; versare EUR 500/mese ne vale +15,75** | +| 62 | SPLIT-DEPOSITO | Β«tutto o nienteΒ» non e' una politica: qual e' la frazione ottima del versamento verso i biglietti? | **LA CURVA ESISTE MA COMPRA LA METRICA SBAGLIATA** β€” fra phi=0 e phi=1 (36 mesi, EUR 500/m): **+16,3 pp** di `P(>=50/g)` a **FLUSSO** (risoluzione MC 1,0 pp β‡’ segnale) ma **0,0 pp** su quella a **RENDITA**, e **+12,4 pp** di `P(finire sotto il versato)`. Mediane **3,58 / 12,90 / 26,05 EUR/g** a phi 0 / 0,5 / 1, contro un bersaglio di 50. Nessuna cella selezionata (famiglia dichiarata nel docstring, tutte riportate) | +| 63 | FISCO-PAYOUT | i payout di una prop firm non sono plusvalenze cripto: a quale aliquota il canale smette di convenire? | 🚨 **IL FORFAIT 33% E' LA CASELLA SBAGLIATA** β€” sul funded il trader **non possiede il conto ne' le posizioni**: non c'e' realizzo di cripto-attivita', e `c-sexies` (33%) e `c-quater` (26%) β€” le due caselle su cui il progetto discute per il LIBRO β€” sono **entrambe FUORI CATEGORIA** qui. Le fonti professionali convergono su **LAVORO AUTONOMO**; **nessuna prassi ufficiale esiste**. βœ… La conclusione di Β§37 (Β«il biglietto batte il versareΒ» a EUR 500/mese) **REGGE su tutta la banda plausibile**. Costo della questione: cassa mediana $44.112 β†’ $34.448 (βˆ’22%), `P(>=50/g)` 21,0% β†’ 4,7%, **βˆ’8,21 EUR/g** fra i due estremi. πŸ“Œ **Al 33% di oggi l'aliquota NON e' cio' che tiene lontano il bersaglio** (17,77 EUR/g mediani, P 14,5%): lo tiene lontano la distribuzione. **Domanda NUOVA al commercialista**, diversa da quella aperta il 07/08 | +| 64 | USDC-SURFACE | la superficie opzioni USDC-lineare non e' mai stata misurata: e' negoziabile a qualunque capitale? | 🚨 **LE DUE SUPERFICI SONO LA STESSA SUPERFICIE**, a strike appaiati e su **entrambi** gli assi: prezzo equo identico per costruzione (una sola superficie di mark al venue), mezzo-spread per gamba **Ξ” βˆ’0,21 / βˆ’0,38 pp** (soglia 1pp) e `f_venue` di struttura **Ξ” +0,005 / βˆ’0,011** (soglia 0,05). 38 chiamate, 0 errori, 0 risposte 429. β‡’ **la raccomandazione di Β§51 (raccogliere la catena USDC, +18% di chiamate/giro) NON e' giustificata dalla ragione che porta**: il `f` di VRP01 misurato sull'inverse non e' Β«il numero della famiglia sbagliataΒ», e' **lo stesso numero**. Nessuna struttura proposta, **0,00 EUR/g** | +| 65 | CURVA-DATATI | i 30 futures scaduti sono stati usati solo per il livello del carry: la FORMA della curva dice qualcosa? | **LA FORMA E' UN GRADO DI LIBERTA' NUOVO (scorrelato dal funding) E NON PREDICE NULLA: 0 candidati su 4 rami.** A1 CONFERMATA (livello = funding travestito, corr **+0,71 BTC / +0,68 ETH**); **A2 CONFERMATA e piu' forte dell'atteso** (slope/curvatura portano informazione che il funding non ha); ramo A direzionale `earns_slot_honest=False` (**DSR 0,108**, marginale **HEDGE**); ramo D (gate su TP01) dShFULL mediano su 24 ore **βˆ’0,072**; ramo B (butterfly) **sotto l'MDE per costruzione**; ramo C nessun controllo positivo passa β‡’ non decisivo. Il roll costa **0,49%/anno = lo stesso ordine del premio**. `implausible_sharpe` FALSE. **0,00 EUR/g** | +| 66 | CROSS-VENUE | lo scarto Deribit-Hyperliquid e' sorvegliato come guasto e mai guardato come segnale | **SCARTATO β€” manca un fattore 6,5x.** Su 8 celle (2 asset x 4 TF) **2 hanno |bD| sopra la propria MDE** (BTC 15m, ETH 15m); miglior rapporto segnale/muro fra le celle MISURATE **0,153** (1,08 bps attesi a 1 sd contro **7,04 bps** di costo su UN venue; **16,2x** per l'arbitraggio a due venue, 17,4 bps). πŸ“Œ Il massimo incondizionato (0,739, BTC 1d) **non e' piccolo, e' NON MISURATO** (|bD| 0,76 contro MDE 2,30) β€” citarlo sarebbe scegliere la cella piu' rumorosa perche' e' la piu' alta. E ogni numero e' **ottimistico tre volte**: a 1 sd invece che al valore tipico, col segno di bD preso dagli stessi dati, e **ignorando che per entrare si attraversano gli stessi due book che generano lo scarto** β€” *si paga il rumore che si vuole incassare*. **ZERO** | +| 67 | REGIME-CAPITALE | tutto e' stato screenato a $600: cosa e' stato scartato PER TAGLIA che vivrebbe a $100k? | 🚨 **IL MURO DI TAGLIA NON NASCONDEVA NIENTE** β€” a $100.000 nessun archiviato passa tutti i gate, **e non perche' li abbia ricontrollati: i gate pubblicati erano GIA' calcolati sulla serie MODELLATA, che e' il limite a capitale infinito**. Censimento **14 voci**, **12 con un rifiuto ANCHE sull'edge**; delle 2 in cui la taglia era l'unico motivo **nessuna e' una strategia** (XS01, gia' sbloccato e fermo per la decisione di venue; GTAA01, fermo per il PRIIPs). Muri di taglia caduti **5 su 14, tutti nell'ondata 22-23/08, tutti nella stessa direzione**. Misurati qui per la prima volta: STATARB-MULTI C* **$2.000-7.500**. ⚠️ **Cio' che TOGLIE e' reale: il gate XSR01 del 23/10 legge un monitor tarato sul pavimento del venue SBAGLIATO** β€” C* vero **$15.000-20.000**, contro i ~$3.000 che HL-EXEC riportava con un criterio piu' permissivo. **+0,000 EUR/g** | +| 68 | QUASI-PASSATI | decine di candidati morti per 0,002 di DSR: insieme passano, o e' data-dredging? | **NON PROMUOVIBILE, E IL MOTIVO E' IL DSR CHE LI AVEVA FERMATI** β€” il paniere dei quasi-passati fa **FULL 1,822 / IS 1,948 / HOLD 1,323**, maxDD **8,34%**, ampiezza effettiva **4,67/5**, ed e' al **100,0Β° pctl** contro i panieri di scartati netti (98,5Β° FULL / 91,9Β° HOLD contro celle casuali). Ma 🚨 **DSR 0,828-1,000 secondo la PARTIZIONE dei trial** β€” *lo stesso difetto misurato il 22/08 da tre agenti, qui applicato al paniere che dovrebbe superarlo* β€” **dipende da UNA gamba** (PREVDAY, la piu' costosa da togliere) e almeno una e' **STAT-MODE** (XSR01 + VRP-TENORE). Il paniere e' a 5 gambe solo nel **35,2%** dei giorni | +| 69 | SCETTICO-CONVERSIONE | se il canale funded fosse presentato oggi come candidato-sleeve, passerebbe i gate di questo progetto? | βœ… **REPLICA e poi attacca il pilastro.** Replica: `P(>=50/g)` **3,4%** contro il **2,6%** pubblicato, P(zero) 38,1% contro 40,3% β‡’ *la macchina e' quella*. 🚨 **Il pilastro:** SE del drift di XS01 = **9,6%/anno** β‡’ a **βˆ’1 SE** il vantaggio del canale passa da **+0,076 a +0,034** di J e `P(>=50/g)` da 3,4% a **1,6%**. 🚨 **La morte-firm:** evidenza pubblica **12-19%/anno** contro il **10% assunto** (l'EV regge β€” break-even >55% β€” ma P scende a 2,9%). 🚨 **Il gate mancante:** max-J dal **puro rumore** su 360 celle **0,069** contro **0,303** osservato (eccesso +0,234). In EUR/g mediani a 36 mesi da EUR 600: canale **1,79** al drift misurato, **0,11** a βˆ’1 SE, libro LIVE **0,35**, obiettivo **50,00**. **Raccomanda di non cambiare nulla** | +| β€” | **NOTIFIER** (audit di fatto) | 🚨 **PUNTO SINGOLO DI GUASTO nella rete di sicurezza** | il `send()` di Telegram e' **un solo tentativo da 10s, senza retry, con l'esito MAI registrato** β€” e lo stato `alerted=True` viene scritto **prima** dell'invio: un 🚨 di `venue_watch` perso e' perso **per l'intero episodio**. Tasso di fallimento **misurato 6,9%** (2/29, IC95 [1,9%, 22,0%]) | +| 22 | MAKER | l'esecuzione passiva e' una fonte di ritorno, al netto del costo di non essere eseguiti? | **SCARTATO** β€” il segno dipende da `<` contro `<=` | +| 23 | BOCPD | un rilevatore di cambio di regime vero batte il miglior lookback COSTANTE? | **SCARTATO** β€” filone chiuso definitivamente | +| 24 | CRITICO | cosa NON e' stato misurato, e quale singola misura mancante vale di piu' | **3 correzioni all'ondata**, 1 regola mia ritirata | +| 25 | BIN-FREQ | il binario di frequenza su SKH01 e' un candidato o un sottoprodotto? | **LEAD** β€” direzione reale, taglia = selezione | +| 26 | TP01-SINISTRO | quanto pesa TP01 se l'obiettivo include un crash che nel campione non c'e'? | **allarme SCARTATO** β€” il rischio e' XS01, non TP01 | +| 27 | SURFACE-RV | la superficie e' internamente incoerente, e l'incoerenza vale piu' del costo di attraversarla? | **SCARTATO** β€” l'incoerenza al mid **e'** la larghezza del mercato | + + +--- + +## 23 β€” BOCPD (orizzonte adattivo, il falsificatore eseguito) + +`scripts/research/r0822b_bocpd.py` (123 s). **169 trial dichiarati**, conteggio al rialzo, inclusi i +13 di taratura fatti guardando **solo il numero di change-point**, mai uno Sharpe. + +**Il falsificatore che il 22/08 restava aperto e' stato eseguito con i rilevatori che nominava**: +BOCPD (Adams & MacKay, run-length posterior online, Normale-InverseGamma, hazard costante) e +**optimal partitioning causale** (l'obiettivo che PELT accelera, ricorsione in avanti, mai un +`fit_predict` retrospettivo), piu' `L` scelto per walk-forward sul PnL realizzato. + +πŸ“Œ **E questa volta il test HA AVUTO POTENZA β€” e' il fatto che rende il "no" esaustivo.** +Il vincitore del 22/08 aveva lookback **costante** (stava al bordo il 100% del tempo, corr 1,000): +si era chiusa una famiglia di *proxy*, non il meccanismo. Qui **68/96 celle sono davvero adattive** +(BOCPD 24/24: `L_t` mediano 25-107 g, IQR 28-134 g, **0-4% al tetto**, 7-44 reset/anno). + +| | Sh FULL | Sh HOLD | maxDD | CAGR | corrβ†’TP01 | +|---|---|---|---|---|---| +| (A) adattivo, cella in-sample | 1,618 | 0,627 | 14,48% | 22,05% | 0,757 | +| **(B) miglior COSTANTE** (L=15g triple 12h) | **1,639** | **0,657** | 12,68% | 22,31% | 0,737 | +| (C) walk-forward su PnL | 1,385 | 0,382 | 16,33% | 17,20% | 0,860 | +| TP01 30/90/180 | 1,314 | 0,489 | 14,31% | 16,44% | 1,000 | + +πŸ“Œ **La decomposizione E' il risultato:** adattivo βˆ’ TP01 = **+0,305**; miglior costante βˆ’ TP01 = +**+0,326**; adattivo βˆ’ miglior costante = **βˆ’0,021** β†’ **l'adattivita' spiega il βˆ’7% del vantaggio +su TP01. Il 100% e' "un orizzonte piu' corto", non "adattarlo".** + +**Gate:** `earns_slot_honest=False`; `marginal_vs_tp01` **NEUTRAL** con **corrβ†’TP01 0,861**; +appaiato per anno vs il null giusto (B) **3/8 anni** (mediana βˆ’0,15); **banda d'ancora 12 ancore, +mediana delle differenze appaiate βˆ’0,136, positiva in 0/12**, e la canonica sta al **92Β° pctl** = +fortuna d'ancora a suo favore. `causality_ok` ok, e **troncamento manuale esplicito** su 4 +combinazioni: `max|L(prefisso) βˆ’ L(pieno)| = 0` β†’ la trappola del change-point retrospettivo e' +esclusa **per misura, non per fiducia**. Null del de-levering **non applicabile e dichiarato** +(il candidato NON riduce il maxDD: 14,5% contro 12,7%). + +**Il criterio del falsificatore applicato a tutte le 68 celle adattive: 0/68**, e nessuna delle due +condizioni va vicina β€” corrβ†’TP01 **minimo 0,754** (soglia 0,60), **nessuna** batte il miglior +costante sul FULL (la migliore e' βˆ’0,013), massimo **4/8** anni battuti (richiesti 6). + +⚠️ **Cosa l'agente NON propone, e perche':** il miglior costante batte TP01 di +0,326 β€” ma il salto +di Sharpe fra **L adiacenti** e' **+0,190, cioe' il 58% dell'intero vantaggio**: e' un max-of-24 su +un crinale di rumore, non una curva. **Nessun cambio a TP01, al libro, ai pesi.** + +🚨 **Sottoprodotto che vale fuori dal filone: un deflated-Sharpe calcolato dentro una famiglia +OMOGENEA e' CIECO.** Qui dice **0,998-1,000 PASS**; ricalcolato con l'sr0 **di screen** misurato dal +filone ORTHO (1,572 su 168 trial) dice **0,555 / 0,580 / 0,237 FAIL** β€” TP01 compreso. Il motivo e' +strutturale: `deflated_sharpe` stima il massimo-dal-rumore dalla **varianza** degli Sharpe della +griglia, e una griglia di varianti molto correlate ha varianza piccola β†’ sr0 basso β†’ DSR alto **per +costruzione**. Gonfiare N col padding alza N e non la varianza: **non basta contare i trial di +screen, serve la VARIANZA di screen.** + +πŸ“Œ **Secondo sottoprodotto, un controllo da mettere in ogni famiglia con un parametro clippato: la +PROVA DI IDENTITA' batte la diagnostica.** Invece di dire "questa cella sta al bordo", l'agente +mostra che la sua serie e quella del lookback costante coincidono a **max|Ξ”| = 0,000e+00 su +n=2719** β€” non *somiglia a*, **lo e'**. + +**VERDETTO: SCARTATO. IL FILONE ORIZZONTE-ADATTIVO E' CHIUSO DEFINITIVAMENTE.** + +--- + +## 22 β€” MAKER (esecuzione passiva come fonte di ritorno) + +`scripts/research/r0822b_maker.py` (19 s, sola lettura). **42 celle dichiarate**, nessuna riduzione. + +πŸ“Œ **Osservazione strutturale che riapriva la domanda:** il netting su strumento unico β€” il motivo per +cui T1 resto' non implementata il 26/07 β€” **non si applica qui**. Quello uccide i *bracket per-sleeve* +(un limit al livello di SKH chiuderebbe quota TP01); l'ordine **netto** di ribilanciamento e' uno +solo, quindi passivizzarlo non ha conflitti fra sleeve. La domanda era davvero aperta. + +**(1) Il differenziale, limite superiore** (tutto maker, zero non-fill): dSharpe **+0,0333**, +dCAGR +0,466 pp β†’ **$2,96/anno a $635**, $23,28 a $5.000, $1.203 al muro di $258k. +βœ… Replica indipendente della curva del 26/07: **+0,0167 Sh/bps e +0,233 pp/bps** contro i pubblicati +0,017 / 0,23. + +πŸ“Œ **(2) Il costo del non-fill, ed e' qui che muore: il parametro che decide non e' l'orizzonte, e' +cosa si chiama "riempito".** + +| N (min) | TOCCO `low <= P` (limite superiore) | ATTRAVERSAMENTO `low < P` (minimo difendibile) | +|---|---|---| +| 5 | fill 93,2% Β· resta **+0,31** bps (15,5%) | fill 86,2% Β· resta **βˆ’0,47** bps (**βˆ’23,6%**) | +| 60 | 98,6% Β· +0,34 (17,1%) | 95,9% Β· **βˆ’0,45** (βˆ’22,4%) | +| 120 | 98,9% Β· +0,67 (33,7%) | 96,9% Β· βˆ’0,38 (βˆ’19,0%) | + +**Fra le due colonne cambia una sola disuguaglianza stretta**: se la barra che apre esattamente al +nostro prezzo, e da li' se ne va, conta come fill. **Non conta** β€” a quel prezzo non e' stato +scambiato niente dopo l'arrivo dell'ordine, e un passivo sta *dietro* a tutta la coda. Griglia fine: +il salto e' **tutto al primo passo** (3,16 β†’ 3,95 bps a penetrazione sub-tick) e **non esiste un q +positivo di pareggio**. Lente model-free: il fill *richiesto* per pareggiare e' **88-98%**, quello +ottenuto e' sotto in **25 celle su 25**. + +**Gate:** appaiato **per ORDINE** (ogni ordine compare una volta in entrambe le politiche, riempito o +no β€” allineare sui riempiti avrebbe tenuto solo i casi in cui non e' successo niente); **IC95 +bootstrap contiene lo zero in 10 celle su 10** al tocco e a q=0, interamente negativo da qβ‰₯1; +**placebo a ore casuali girato** β†’ reale β‰ˆ placebo e **incoerente fra i due asset** (BTC 6,8 vs 3,1 +bps; ETH 4,1 vs 4,7) β‡’ la selezione avversa **non** e' "il libro trada nei momenti brutti", e' +**strutturale all'esecuzione passiva** e non si aggira scegliendo momenti migliori. Null del +de-levering **non pertinente e dichiarato** (nessun claim di DD; il maxDD **cambia segno** fra le +lenti = non e' un effetto). + +πŸ“Œ **La firma della troncatura, vista dall'altra parte.** Il passivo **vince sul 92-98% degli ordini e +perde sulla media**: mediana dell'avversa **βˆ’2,0 bps** (favorevole), media **+4,3**, peggiore **+948**, +e i **5 ordini peggiori su 1535 fanno il 42% del danno**. E' la forma speculare del profit-take di +VRP01 (30/07). **Una politica che vince quasi sempre e perde sulla media si giudica sulla media.** + +⚠️ **Rischio operativo quantificato:** a N=60 restano aperti al giro successivo il **4,6-7,8% degli +ordini = 9-15 volte l'anno** in cui l'esecutore deve cancellare-e-rimpiazzare su un conto vero. Oggi +`rebalance_signed` manda market e finisce: **nessun ordine sopravvive al giro** β†’ si comprerebbe un +modo nuovo di rompersi (il libro ha gia' pagato quel prezzo con `fresh_5m`) per un beneficio il cui +IC95 contiene lo zero. + +⚠️ **Numero del brief non riprodotto:** l'agente ricostruisce **191 ordini/anno di libro** a $635 +contro i "35-44/anno per asset" del brief; il nastro vero da' 18 ordini in 45 giorni = **146/anno**. +La conclusione non dipende da questo (il punto 1 usa il turnover modellato, che replica la curva +pubblicata al terzo decimale), **ma il numero del brief va ri-verificato.** + +**VERDETTO: SCARTATO.** +0,033 Sharpe nel caso impossibile, contro Β±0,20 di banda d'ancora del libro. +**Smentitore dichiarato:** una misura di **profondita' del book** sui perpetual USDC (che l'OHLC non +contiene e che il progetto non raccoglie β€” `cb_chain` e' opzioni). Se a taglie di $5-80 la coda al +miglior livello si consumasse senza che il prezzo debba attraversare, il segno si ribalta. **E' +misurabile in avanti**: registrare `book_depth` al momento di ogni ordine e poi il volume scambiato +al livello. + +--- + +## 24 β€” CRITICO (critico di completezza dell'ondata) + +`scripts/research/r0822b_critic.md` (48 KB, markdown β€” non uno script). Ha letto brief, registro, +diario e i sorgenti degli agenti. + +πŸ“Œ **La tesi, e regge:** *il metodo dei singoli filoni non e' il problema dell'ondata, la CUCITURA lo +e'.* I difetti gravi stanno nei punti dove **nessun agente era responsabile**. + +🚨 **(a) `dealer_net_gamma` NON e' "il GEX col segno invertito" β€” la regola l'ho pubblicata io in +CLAUDE.md ed e' FALSA.** βœ… **Verificata da me aprendo la fonte**, che gira su questa stessa VPS: +`/opt/docker/cerbero-mcp/src/cerbero_mcp/common/options.py:11-14` **dichiara la convenzione** +(*"i dealer sono SHORT calls (le vendono al retail) e LONG puts"*) e `dealer_gamma_profile:138-140` +la implementa (`call_dealer_gamma -= contrib` / `put_dealer_gamma += contrib`). Non e' un segno +invertito per errore: e' un'**assunzione di posizionamento dichiarata**, diversa da quella del GEX +standard. Il default `top_n_strikes=50` spiega anche il coefficiente βˆ’0,7 e l'offset che il filone +DEALER-GAMMA attribuiva a "non-portabilita'". **Il verdetto SCARTATO regge; cade la regola**, che le +prossime ondate avrebbero ereditato al contrario. + +🚨 **(b) Il deflated-Sharpe e' stato misurato ai suoi DUE estremi degeneri lo stesso giorno, da due +agenti che non si citano β€” e tre gate pre-registrati ci poggiano sopra** (XSR01 23/10, DVOLSPREAD +24/10, 22/12). `altlib.deflated_sharpe` calcola `sr0 = sd(Sharpe dei trial) Γ— mult(N)`: dipende dalla +**varianza** della griglia, non solo da N. Da un lato VOL-SIZE dichiara il proprio DSR **VACUO** +(1,000 anche per il baseline); dall'altro ORTHO-SCREEN pubblica *"sr0 = 1,572 β‡’ uno screen largo su +BTC/ETH non puo' passare il proprio gate per aritmetica"* β€” ma quell'1,572 implica sdβ‰ˆ0,58, e **le +stesse 168 celle dichiarate come 7 famiglie da 24 danno 1,15, sotto il soffitto ~1,3**: il verdetto +"impossibile per aritmetica" **si ribalta senza toccare un dato**, e la cura che suggerisce +(dichiarare famiglie piu' piccole) e' esattamente la manovra che il progetto ha proibito il 30/07. +βœ… **Convergenza indipendente:** il filone BOCPD, che non ha parlato col critico, ha misurato lo +stesso difetto dall'altro capo (DSR 0,998 PASS dentro la famiglia omogenea β†’ **0,555 FAIL** con +l'sr0 di screen). **Due agenti, due strade, stessa conclusione: il gate non ha una regione utile in +mezzo.** + +🚨 **(c) Il filone ALT-OPTIONS non e' nel registro** β€” βœ… verificato: **zero occorrenze**, ne' riga ne' +sezione, benche' due altri filoni lo citino e il mio brief per SURFACE-RV mandasse a leggerlo. Il +diario lo liquida come *"alt-options (alt)"*, ma la sua conclusione riguarda **ETH_USDC, che non e' +un alt**: `f_venue` 0,980 e **max-loss $16,62/lotto**. Se regge, e' la **quarta** soglia pubblicata +falsificata (il diario ne annuncia tre) e sposta VRP01 da *"servirebbe il 61% di un conto da $3.000"* +a *"entra in un conto da ~$140"*. ⚠️ **Cio' che NON cambia: la regola "niente short-vol da modello in +deploy" non era una regola di eseguibilita'** β€” VRP01 resta fuori dal libro per il `f` modellato e +per il gate IV-rank, non per la taglia del lotto. + +πŸ“Œ **(d) La misura di maggior valore mai fatta, secondo il critico: il peggior giorno che il libro +puo' avere, MISURATO invece che assunto.** Il gradino di leva 1,00β†’1,50x vale **14,7a β†’ 11,6a** al +capitale-rendita (~€300/mese di versamenti equivalenti, a costo zero) ed e' bloccato da **un unico +parametro scelto a mano** ("un giorno βˆ’10%/anno porta k* a 2x"). Controllo di cinque minuti: sui 10 +peggiori giorni di BTC/ETH in 7,4 anni β€” **incluso il βˆ’41,5%/βˆ’45,7% del 12 marzo 2020** β€” il libro ha +fatto **+0,97 Β· +0,72 Β· 0,00 Β· βˆ’1,98 Β· βˆ’2,24 Β· +0,23%**, e il suo peggior giorno assoluto (**βˆ’3,38%**) +**non e' un giorno di crash**. Il βˆ’10% e' plausibilmente una **coda da buy&hold importata in un libro +difensivo**. + +πŸ“Œ **(e) La risposta onesta al mandato, che gli era stato chiesto di dare se vera: nessuna misura di +ricerca batte "versare".** 3,1 anni di ricerca valgono meno dei 5 anni che vale €250/mese in piu'. +E la verifica che costa meno di tutte **non e' ricerca**: leggere il listino di due prop firm. +L'intero risultato di testa dell'ondata (+0,400 di J) poggia sull'assunto β€” scritto in un **commento +a riga 100** β€” che una firm crypto listi i 19 alt Hyperliquid con short abilitato. **E' la ripetizione +esatta dell'errore GTAA01/PRIIPs**, per cui il progetto ha gia' scritto la regola: *la negoziabilita' +sul conto REALE va verificata quando lo sleeve entra in RICERCA.* + +**Altre contraddizioni fra agenti segnalate:** lo Sharpe di XSR01 compare con **quattro** lenti (non +tre); il muro di XS01 con **tre** numeri; la leva riceve guida **opposta** da GROWTH-POLICY e +PROP-ALLOC; il peso di SKH01 e' **respinto da un agente e raddoppiato da un altro**. Piu' 6 modalita' +non esplorate e 6 punti di autocritica β€” incluso che i **20 MB del dataset futures 2018-2026 (l'unico +con la coda 2022) vivono solo in uno scratchpad di sessione**. + +--- + +## 21 β€” XS01-OOS (XS01 fuori dalla sua finestra di scoperta) β€” **il risultato di testa della serata** + +`scripts/research/r0822b_xs01_oos.py` (78-126 s, cache nello scratchpad, seconda corsa **offline**, +numeri identici su due corse). **140 trial dichiarati** (contati al rialzo: le 6 varianti di config +sono contate come trial anche se sono la *rivalidazione di una selezione passata*), piu' 1.200 +estrazioni di null che **non** sono trial. + +**L'idea:** il gate del PROP-ALLOC diceva *"non aprire un funded prima che XS01 abbia una finestra +fuori dal 2024-2026"* = un'attesa di anni. **Quella finestra esiste gia'** β€” gli stessi 19 ticker su +**Binance spot USDT 1d, 2021-01 β†’ 2023-12**, che contiene **LUNA e FTX**. Meccanismo **congelato** +(blend [30,90], H=10, k=5, gate p30, vol-target 20%), nessuna griglia cercata. + +⚠️ **L'obiezione ovvia β€” "Binance non e' la verita'" β€” e' stata misurata, non aggirata:** quella +regola del progetto riguarda l'**ancoraggio di prezzo**, non un ranking cross-sezionale, e lo scarto +USDT e' un **fattore comune** che sparisce nello z-score. Verificato: corr dei ritorni giornalieri per +asset **β‰₯0,9992** (min NEAR), |differenza| mediana **5,1 bps**, e soprattutto lo **stesso sleeve** +calcolato sui due venue da' **corr 0,9991, dSharpe βˆ’0,025**. Replica dello sleeve ufficiale +`max|diff| = 0,0`. Dato certificato: **0 gap, flat 0,00-0,71%, volumi zero 0,00%**, nessun run +iniziale a volume 0 (il rivelatore del backfill sintetico, lezione AXS). + +πŸ“Œ **IL RISULTATO, e va nel verso opposto a quello temuto:** + +| | Sharpe (**mediana di fase**) | banda 10 fasi | fasi >0 | maxDD | CAGR | +|---|---|---|---|---|---| +| **fuori campione 2021-2023 (U13)** | **+1,12** | [+0,74, +1,54] | **10/10** | **22,0%** | +26,1% | +| finestra di scoperta 2024+ (U13) | +0,37 | β€” | β€” | 20,5% | +8,9% | +| **fuori campione (U19-EXP)** | **+1,17** | [+0,91, +1,78] | **10/10** | 20,9% | +30,0% | +| XS01 **ufficiale** (HL, 2024+) | +1,15 | [+0,46, +1,59] | β€” | **10,8%** | +29,0% | + +**Differenza appaiata per fase (fuori campione βˆ’ scoperta): +0,668, positiva in 10/10 fasi.** +**Il fuori campione e' la finestra MIGLIORE, non peggiore.** Per anno: 2021 **+2,14** Β· 2022 +0,22 Β· +2023 +0,84 Β· 2024 βˆ’0,48 Β· 2025 +0,68 Β· 2026 +2,26. + +**Sul LIBRO, solo fuori campione** (TP01+SKH01 75/25 + XS01, n=1076, dati che PROP-ALLOC non aveva mai +visto): w=0 β†’ Sh **+1,17** / DD 9,4% Β· w=0,25 β†’ **+1,65 / 6,0%** Β· w=0,33 β†’ +1,67 / 6,6% Β· +**w=0,50 (l'argmax di PROP-ALLOC) β†’ +1,56 / 8,9%**. corr(XS01, libro) **βˆ’0,019**. + +**Gate:** `causality` leak-free (randomizzando i prezzi dopo la decisione `max|diff| = 0`); +`marginal_vs_tp01` **ADDS** su entrambi gli universi (corr +0,031 / βˆ’0,01, `is_hedge` **False**); +banda di fase `gate_pass=True` **4/4**, e la fortuna d'ancora e' **piccola** (+0,05) contro il **+0,18** +dello sleeve ufficiale; null di permutazione **a fee zero** p=0,013 (cross-sez.) e 0,017 (pesi +casuali); sopravvive a **30 bps/lato** (+1,01). **`deflated_sharpe` FAIL 0,342** β€” e l'agente dichiara +che il DSR penalizza una **mediana-di-fase** come se fosse un **max-di-k**, *"ma un gate fallito resta +fallito"*. Null del de-levering **non pertinente e dichiarato**. Haircut a $600: **non e' il vincolo** +(ticket mediano $76, **0,0%** sotto il min-order). + +🚨 **Null "universo dove NON dovrebbe funzionare"** (11 settoriali SPDR, 1998+, meccanismo congelato): +**βˆ’0,45, 0/10 fasi positive** β†’ **terza conferma indipendente che il cross-sectional e' +crypto-specifico** (dopo XSR01-equity 26/07 e STATARB-EQ 25/07). + +🚨 **Il risultato che il progetto non si aspettava: i DUE AFFINAMENTI del 2026-06-19 NON si +replicano.** Fuori campione il **blend [30,90] e' PEGGIO del solo L=30** (+1,12 vs +1,28) e il **gate +di dispersione p30 vale ZERO** (+1,12 con e senza) β€” mentre *dentro* la finestra di scoperta il gate +vale eccome (+0,37 contro βˆ’0,03). **Cio' che sopravvive e' il meccanismo cross-sectional NUDO.** +⚠️ Non e' un argomento per cambiarli β€” cambiarli guardando *questo* risultato sarebbe la stessa +selezione spostata di finestra β€” **e' un argomento per non attribuire loro il valore che la memoria +del progetto gli attribuisce.** + +⚠️ **Concentrazione misurata prima di crederci** (lezione SOL, stesso giorno): togliendo il **2021** il +fuori campione scende **+1,17 β†’ +0,57**; **leave-one-asset-out**: togliendo **LTC** la mediana di fase +crolla **+1,12 β†’ +0,20** (tutte e 13 restano >0) β†’ **ampiezza effettiva bassa**, stessa diagnosi di +STATARB-MULTI. + +**VERDETTO: LEAD.** Non promuove niente; **riscrive il gate del PROP-ALLOC.** + +πŸ“Œ **`IL GATE DEL PROP-ALLOC SI PUO' CHIUDERE OGGI: PARZIALMENTE` β€” e non si chiude, si SOSTITUISCE.** +L'attesa di anni **non serve**. Ma tre cose che il gate non chiedeva sono ora misurate: +- **il rendimento sopravvive, il RISCHIO no**: maxDD standalone **10,8% β†’ 20,9-22,0%** β€” e su un conto + funded il vincolo binding e' la **regola di DD**, quindi la correzione va nel verso sbagliato; +- **il peso 0,50 dell'argmax-J non sopravvive**: fuori campione a w=0,50 il maxDD di libro risale a + **8,9%** contro **6,0%** a w=0,25, cioe' restituisce quasi tutta la protezione che e' l'unica ragione + per metterlo su un conto a barriera β†’ l'ottimo fuori campione e' **w β‰ˆ 0,25-0,33**; +- **32% dell'universo non e' testabile** (ARB/OP/SUI/APT/SEI/TIA non quotati nel 2021-23) e su di esso + vive gran parte del numero di scoperta (**19 gambe 1,31 vs 13 gambe 0,54**, stessa finestra e stesso + venue) β†’ *cio' che e' stato validato e' una versione piu' DEBOLE di quella che sta nel libro.* + +⚠️ **Il punto fragile che l'agente dichiara e non ha testato:** la validazione di venue gira **solo sul +2024+**, cioe' proprio il periodo **senza depeg USDT**, mentre il fuori campione contiene il **depeg di +maggio 2022**. Se i close Binance/USDT 2021-23 divergessero da un venue **USD** in modo *non comune fra +gli asset*, il ranking cambierebbe e il risultato con esso. + +--- + +## 26 β€” TP01-SINISTRO (il peso di TP01 contro un crash che nel campione non c'e') + +`scripts/research/r0822b_tp01_sinistro.py` (~200 s). **1.784 valutazioni Monte Carlo** contate dal +codice, ognuna 2.500 percorsi eval + 2.500 funded, **lente accoppiata**, de-luck x0,89. +**Replica di controllo:** i numeri E-ter di PROP-ALLOC (argmax **38/62 @0,50x J 0,521**, live 75/25 +J 0,443) riprodotti **al millesimo**. + +**L'allarme da verificare** era il caveat che PROP-ALLOC aveva dichiarato contro se stesso: *"la +sotto-pesatura di TP01 e' misurata su un campione SENZA SINISTRO: su una barriera assorbente quella +scommessa si paga una volta sola"*. L'agente ha **registrato nel docstring, prima di misurare**, che +si aspettava di **confermarlo**. + +| lente | argmax w TP01 | J argmax | J a 0,75 (live) | +|---|---|---|---| +| **2019+ (contiene il 2022)** | **0,375** | 0,521 | 0,443 | +| 2024+ (la finestra di PROP-ALLOC) | **0,375** | 0,423 | 0,317 | +| SOLO-SINISTRO | **0,000** | 0,771 | 0,000 | + +**L'allarme e' falsificato nel VERSO, non nella cautela: TP01 ~0,375 e' l'argmax sia sulla finestra +che contiene il 2022 sia su quella che non lo contiene, e nel sinistro il libro con MENO TP01 fa +MEGLIO** β€” perche' TP01 va flat mentre **SKH01 si gira short e guadagna**. Conto dell'assicurazione: +premio **-4,01%/anno** fuori dal sinistro, indennizzo **-12,5%** cumulato sui 120 giorni di sinistro +(LIVE -2,8% contro OPT +9,7%). Test dei segni: OPT batte LIVE **12/12** sulle peggiori finestre 30g +(p=0,0005) e **8/8 sul solo sotto-campione pre-2024 indipendente** (p=0,0078). + +**Null del de-levering β€” 7a occorrenza, in veste nuova** ("protezione dal crash" invece di "meno +drawdown") **e decisivo**: a leva comune TP01 protegge davvero (sopravvivenza al crash 32,4% -> 76,4% +da TP01 0 a 0,625), ma la stessa protezione si compra abbassando la leva, e **a ISO-SOPRAVVIVENZA +meno TP01 paga di piu'** (payout $4.490 a TP01 0,125 -> **$2.119** a TP01 1,00, monotono). + +**Tre risultati collaterali che valgono piu' dell'allarme rimosso:** +1. **"2024-2026 non contiene un crash" e' FALSA nella forma**: il 2024+ ha buy&hold a **-60% di + drawdown** e -39,6% in 20 giorni. Manca la **taglia** (~2,3x sulla coda 1-10 giorni). La domanda + giusta non e' *"e se arriva un crash"* ma **"e se ne arriva uno due volte piu' grande"**. +2. **Alle leve funded il 2022 NON era una minaccia** (min equity -1,4/-2,4% contro barriera -6%): + cio' che uccide un conto a barriera e' la **regola a un giorno su una giornata qualunque**, non il + crash del sottostante. E il vincolo che morde **cambia col peso**: sotto TP01 0,50 e' il + daily-loss, sopra e' il max-loss cumulato. +3. **Il rischio vero dell'ottimo non e' TP01, e' XS01**: uno shock di **un giorno a ~-10%** dimezza + la sopravvivenza quando il crash arriva, e **XS01 non ha nessun sinistro nel campione**. + **Ipotesi dell'agente nata e refutata in sessione:** aveva scritto che TP01 fa da zavorra contro + quell'ignoto (-12,3% a XS01 25% contro -8,0% a XS01 50%); il controllo a **XS01 fisso e leva + comune** lo smonta - lo shock di rottura e' **-10,1/-10,2/-10,2%** muovendo TP01 da 12% a 38% + (**invariante**) e la sopravvivenza severa **cala**. *Protegge meno XS01, non piu' TP01: il primo + confronto attribuiva a TP01 il merito di un altro peso.* + +**Libro live NON toccato** (75/25 resta). Nota per la memoria: il 75/25 e' **fuori** dalla regione di +compromesso del funded (0,375-0,50) - coerente col fatto gia' scritto il 25/07, *canale funded e +libro proprietario vogliono libri diversi*. + +**Caveat principale, dichiarato:** `anchor_luck_band` **non ri-misurata** sotto questa lente; gira +sull'ancora canonica e si appoggia a **SKH01, lo sleeve con la fortuna d'ancora piu' grande da +restituire** -> **la distorsione e' A FAVORE della conclusione**. **Smentitore dichiarato:** un crash +in cui **SKH01 non si gira short in tempo** (gap notturno oltre lo stop, venue fermo). Il campione ne +ha **quattro** episodi e in tutti e quattro SKH01 ha girato: **l'ampiezza effettiva della coda e' 4, e +nessun bootstrap la aumenta.** + +--- + +## 27 β€” SURFACE-RV (la superficie come relative value fra i suoi punti) + +`scripts/research/r0822b_surface_rv.py` (~85-130 s). **~112 valutazioni**, e **nessuna e' una +selezione**: e' un **censimento su 1,24 M quote a due lati**, non una ricerca di celle. + +L'ipotesi era di **microstruttura, non di previsione**: non chiede alla vol implicita di prevedere +niente, chiede se la superficie sia internamente incoerente e si ricomponga. E' l'unico angolo della +superficie che l'ondata non aveva coperto (livello, skew, struttura a termine e quote reali erano gia' +chiusi). + +| test **model-free** (nessun interpolatore) | esito | +|---|---| +| **V β€” monotonia P(K)** | **0 / 1.111.585** | +| **B β€” convessita' P(K)** | mid 6,213% Β· **eseguibile 1 / 1.031.256 = 0,0001%**, vale **$0,00**, sparita l'ora dopo | +| **C β€” varianza totale nel tenore** | strike condiviso 6/107.177 (0,006%) Β· con interpolazione 10/112.827 β†’ **l'interpolatore contribuisce +0,003 pp** | + +**Ampiezza:** |residuo| mediano **0,125 pt-vol (BTC) / 0,200 (ETH)** contro **mezzo** spread +0,861 / 1,208 β†’ rapporto su spread pieno **0,053 / 0,063**; **sotto il tick nel 96,5% / 95,3% dei +casi** β€” l'incoerenza **non e' esprimibile nella griglia di prezzo del venue**. Economia al lotto +minimo: fee 3 gambe $7,54 (BTC) / $2,11 (ETH), miglior credito netto su **782 k farfalle** +equispaziate **βˆ’$0,95 / βˆ’$0,33**, **positivi 0**. Calendario: violazione mediana **$0,09** contro +**$0,59** di costo, e la superficie sta **13,2 pt-vol** dal bordo di non-arbitraggio β†’ vincolo +strutturalmente slack. + +πŸ“Œ **IL MECCANISMO, che vale piu' del verdetto: |residuo| cresce MONOTONAMENTE col largo del +mercato** β€” BTC **0,080 pt** a spread 2,6% β†’ **0,255 pt** a spread 50%. **L'incoerenza al mid *e'* la +larghezza del mercato guardata attraverso il mid**, non un errore di prezzo. + +⚠️ **E questo uccide l'unica riga non-zero, che era un artefatto del CONTROFATTUALE dell'agente +stesso:** prendendo tutta la superficie, lo spread USDC del 2,6% aprirebbe **2.293 farfalle a credito +su ETH** (miglior caso $123); **restringendo alla parte di superficie gia' quotata stretta quanto le +USDC β†’ 0/13.287 (ETH) e 0/145.431 (BTC)**, miglior caso βˆ’$0,58 / βˆ’$0,82. *Prendeva il mid di un +mercato larghissimo e fingeva di poterlo quotare al 2,6%.* + +**Reversione MISURATA, non assunta:** Ξ² **βˆ’0,062/βˆ’0,056 per ora**, corr(t,t+1) **+0,94**, mezza-vita +**11-12 h** β†’ il residuo e' **la forma dello smile**, non un errore che si ricompone. + +**Gate:** **controlli positivi 3/3 in entrambi i versi** (violazione costruita β†’ rilevata; catena sana +β†’ zero) β€” *un rilevatore che trova ~0 e non e' validato e' indistinguibile da uno rotto*; causalita' +**strutturale** (ogni test e' contemporaneo dentro un solo snapshot); ancora oraria girata (incoerenza +mid 5,99-6,36% su 24 ore, eseguibile max 0,00222% β†’ **nessuna ora apre una finestra**). +`marginal_vs_tp01`/`deflated_sharpe`/de-levering/`implausible_sharpe` **non girati, e il perche' e' il +risultato**: non esiste una serie di ritorni da valutare (1 opportunita' su 1,03 M, del valore di +$0,00). `f_markfit` **1,0010** β€” converge col **0,992** del filone SKEW, implementazione separata β†’ +il difetto non e' il prezzatore. + +**VERDETTO: SCARTATO. Il lato relative-value della superficie e' chiuso.** +**Smentitore piu' forte, dichiarato:** quote **reali** della famiglia USDC (tutta l'estrapolazione e' +un controfattuale sui mid inverse) β€” osservazione decisiva: puntare il collettore anche su +`{"currency": "USDC"}` e rifare TEST B sul suo bid/ask. + +🚨 **SOTTOPRODOTTO β€” buco di colonna nell'archivio ereditato, mai registrato:** `bite_archive` ha +`index_price` e `underlying_price` **100% None (1.232.212/1.232.212)**. Il sottostante esiste **solo +dal 2026-07-30** (raccolta propria, 18,2% delle righe); `book_depth_top3` solo nel 67,2%. **Chiunque +calcoli moneyness o riprezzi sull'archivio pre-30/07 sta usando una colonna che non c'e'.** +βœ… **Ed e' chiudibile:** il forward si ricostruisce con la **parita' put-call** dalla catena stessa β€” +verificato contro l'osservato su **6.578 coppie: |errore| mediano 0,023%, p95 0,147%, corr +1,000000** β†’ rende la superficie utilizzabile su tutti i 75 giorni invece che 23. +βœ… **Terza convergenza indipendente sul muro del dato** (75 giorni di superficie); e lo **smile** β€” +che i filoni SKEW/TERM non usavano β€” esiste invece su **113 giorni dal 2026-05-01**. + +--- + +## 28 β€” DEPEG (il falsificatore di XS01-OOS, eseguito) + +`scripts/research/r0822b_depeg.py` (811 righe, **18 s**, offline dalla cache, **due corse +byte-identiche**). **112 configurazioni**, e **nessuna e' una selezione**: e' un confronto **appaiato** +dello stesso meccanismo congelato su due fonti di prezzo. + +**Perche':** l'autore di Β§21 aveva dichiarato contro se stesso che la sua validazione di venue gira +**solo sul 2024+**, cioe' proprio il periodo **senza depeg USDT**, mentre il fuori campione contiene +il **maggio 2022**. La soglia β€” `corr fra i due sleeve >= 0,99` β€” era **dichiarata da lui, non +dall'esecutore del test**. + +**Il dato, e il primo limite:** su 13 asset **10 hanno un prezzo USD** che copre la finestra (9 +Coinbase + **XRP da Bitstamp**, perche' Coinbase l'aveva sospeso). **BNB non esiste in USD prima del +2025-10** β€” e' il token nativo del venue sotto esame. AVAX parte 2021-09-30 β†’ il test **non copre il +2021**, ma copre **esattamente cio' che l'obiezione nomina: maggio 2022 e FTX**. + +πŸ“Œ **Divergenza SCOMPOSTA nelle due componenti che sono domande diverse** (824 giorni, 8.240 +osservazioni appaiate): **(a) comune** med 3,2 bps / max 76,7 β€” quella che lo z-score annulla; +**(b) idiosincratica** med **2,1 bps** / p95 11,2 / max 116,5 β€” quella che cambia il ranking. +Nel depeg (07β†’20 maggio 2022) il comune arriva a **βˆ’39 bps** ma l'idiosincratico sale solo a med +**3,6** / max **33,8 bps** contro 2,0 / 10,9 fuori. + +**TEST 1** (meccanismo congelato, 10 asset, 10 fasi, 2021-09β†’2023-12): **corr mediana 0,9992, minima +0,9921 β†’ PASS**; **dSharpe appaiato βˆ’0,000** [βˆ’0,056, +0,036]; divario di equity cumulato su 2,25 +anni **+0,07%**. **Il canale del danno, guardato direttamente:** 734 ribilanciamenti, **10 con gambe +diverse (1,36%)**, tutti a sovrapposizione 4/5, **ZERO dentro la finestra del depeg**; Spearman dello +score mediana **1,0000**. +**TEST 2** (finestra intera di Β§21): il numero di testa **+1,12 β†’ +1,11**, delta appaiato βˆ’0,003. + +**VERDETTO: `IL RISULTATO XS01-OOS: SOPRAVVIVE`** β€” il venue sposta il titolo di Β§21 di **0,01 di +Sharpe** e **non cambia una sola gamba dentro il depeg**. +**Controllo positivo girato** (obbligatorio, o "corr 0,99" potrebbe essere uno sfasamento): USD +ritardato di 1 giorno β†’ corr mediana **βˆ’0,011**. + +**Tre cose trovate che vanno registrate:** +1. πŸ“Œ **L'unica corr sotto soglia e' ATTRIBUITA, e non e' il prezzo.** Col **gate di dispersione + spento** su entrambi i venue la minima passa **0,9674 β†’ 0,9977**. Il gate `disp >= percentile` e' + **binario**, quindi pochi bps cadono ai due lati β†’ **4 intervalli da 10 giorni** in cui uno sleeve + e' pieno e l'altro **flat**, piu' una coda di ~30 giorni in cui la `rv30` del vol-target resta + diversa. **Una scansione di ranking non lo vede.** βœ… Converge con Β§21 (*"il gate p30 fuori campione + vale ZERO"*): **non porta edge e amplifica il rumore** β€” ⚠️ non e' una proposta di cambiarlo, + cambiarlo guardando questo numero sarebbe selezione. +2. 🚨 **Il caso peggiore dell'intero campione NON e' il depeg: e' il 2025-10-10** (cascata di + liquidazioni sulle coppie /USDT) β€” **INJ 1.981 bps**, ADA 315, LTC 201, AAVE 136 mentre BTC/ETH + stanno a 21-22. **Il meccanismo temuto dall'obiezione e' REALE**; semplicemente non e' accaduto nel + 2021-2023 ed e' caduto nella finestra di **scoperta**, non nel fuori campione. Nel depeg le + peggiori divergenze sono **AVAX subito dopo la quotazione = book sottile, non stablecoin**. +3. ⚠️ **BNB resta non controllabile** e toglierlo costa **βˆ’0,75** di mediana di fase (+1,12 β†’ +0,37) + β€” βœ… **replica esatta del leave-one-out di Β§21 con implementazione separata**. Contesto onesto: + nella LOO di Β§21 **LTC (+0,20), BTC (+0,33) e XRP (+0,33) pesano di piu'**, e quelle tre **sono + coperte** dal controllo USD. + +**Smentitore residuo dichiarato:** la divergenza del 2025-10-10 e' di **chiusura**; un evento +**intra-giornaliero** della stessa natura sposterebbe il ranking **senza lasciare traccia sulle +chiusure**, e i close giornalieri non lo vedrebbero. + +⚠️ *Nota di attribuzione: il file risultava `M` in git perche' **io** (coordinatore) l'avevo incluso +in un commit mentre l'agente ci stava ancora lavorando β€” non un altro agente. Un `git add -A` durante +un'ondata cattura il lavoro in corso.* + +--- + +## 25 β€” BIN-FREQ (il binario di frequenza su SKH01, giudicato come candidato) + +`scripts/research/r0822b_bin_freq.py` (~400 s, **23 ancore piene**, due esecuzioni indipendenti con +output **numericamente identici**). **144 celle** dichiarate nel docstring **prima di guardare**, piu' +i **71 trial gia' spesi sullo stesso oggetto** (28 VOL-SIZE + 43 scettico) β†’ **conto onesto = 215**: +*riaprire un parametro riapre la sua famiglia.* + +**Repliche bit-exact** (`max|dif| = 0`) contro `sleeves._skyhook_returns()` **e** contro +`r0702_anchor_skh01.run_asset`; **titolo dello scettico riprodotto esattamente** chiamando la sua +funzione (`BIN L>=med` +0,080 in 20/23, inverso βˆ’0,159 in 0/23). + +| | ShFULL | ShHOLD | maxDD | CAGR | +|---|---|---|---|---| +| BASE (libro live 75/25) | 1,626 | 1,037 | 10,4% | 18,6% | +| BIN, peso effettivo lasciato cadere | 1,690 | 1,031 | 10,3% | 17,8% | +| **BIN, controllo ISO-PESO** | **1,589** | 0,956 | 10,0% | 16,8% | + +πŸ“Œ **La statistica che decide non e' l'argmax, e' la MEDIANA DI FAMIGLIA.** Argmax +(`q0,50 w60 h0,10 s+`) ISO **+0,112 in 23/23**; **stima onesta di famiglia +0,007**, con **39/72 +celle positive = 54% β‰ˆ monetina**. **L'argmax vale 16Γ— la mediana β†’ punta, non plateau.** +**Controllo di segno: 0/72 celle positive** sulla meta' inversa β†’ la *direzione* e' solidissima. + +**Gate:** `marginal_vs_tp01` **ADDS**; selezione **in-sample-only** sceglie la stessa cella mentre +scegliendo **sull'hold-out** ne esce un'altra (βˆ’0,017) β†’ **il gate ha potenza**; +**`weights_tilt_null` FAIL** (pctl 36-38) **e per giunta inerte** (il binario *abbassa* il peso +effettivo 0,250β†’0,19, e nella forma di deploy naturale il vettore proposto e' identico a quello +corrente); causalita' verificata con **troncamento diretto** su 52 decisioni, 0 divergenti; +**trasferisce a SKH01_V1** (geometria d'uscita diversa): +0,087 in 20-21/23. + +❌ **Il deflated-Sharpe NON e' vacuo β€” refuta l'ipotesi a-priori che l'agente aveva registrato:** +**0,976** (N=144) / **0,977** (N=215, conto onesto) / 0,948 (N=500), baseline 0,828/0,834. E il +**motivo e' riusabile**: il DSR e' vacuo quando i trial si somigliano (varβ†’0 β‡’ sr0β†’0 β‡’ DSRβ‰ˆ1 *per +chiunque*); una famiglia **binaria** contiene celle che scartano insiemi di trade molto diversi, +quindi **riacquista potenza**. *Non e' il binario a essere piu' solido: e' la famiglia a essere piu' +larga.* βœ… Terza misura convergente della stessa giornata sul difetto del DSR (con BOCPD e CRITICO). + +⚠️ **Il null "con potenza" ha una mediana che NON e' zero, ed e' un fatto di misura:** scartare a caso +il 40% dei trade **danneggia**, perche' lo Sharpe annualizzato va con √(scommesse/anno) β€” misurato: +gamba 1,107 β†’ **0,883** permutata contro **0,861** previsti da √0,60. Il null resta valido e **piu' +severo** (reale +0,112 vs massimo del null βˆ’0,031, p<0,01), **ma risponde a "batte una selezione +casuale?", non a "batte il non filtrare?"**. + +🚨 **Due premesse del MIO briefing misurate e REFUTATE:** +(i) *"il binario dimezza gli ordini β†’ migliora l'eseguibilita' e riduce le fee"* β€” gli ordini +**nettati** calano del **17%**, non del 50%; i sotto-min-order **non si muovono** perche' sono di +TP01; e l'**haircut a $635 e' 0,001 in entrambe le braccia**: non c'era niente da guadagnare. +(ii) *"converti il risparmio di fee in Sharpe con la curva del 26/07"* β€” **a fee ZERO l'effetto resta +l'87-110%** β†’ non e' risparmio di costo, e' **informazione sul rendimento lordo**, e convertirlo +sarebbe stata un'attribuzione sbagliata. + +**VERDETTO: LEAD** (non candidato, non scartato). La **direzione** e' reale e sopravvive a tutto; la +**taglia di una singola cella e' selezione**. +πŸ“Œ **E la domanda che avevo posto β€” "+0,080 contro Β±0,196 di banda d'ancora, merita?" β€” era il +confronto SBAGLIATO** (la banda del *livello* si cancella in un disegno appaiato), **ma la risposta +corretta e' PEGGIORE per il candidato**. Criterio dichiarato, 4 gambe: (1) mediana di famiglia β‰₯ meta' +della cella e >65% celle positive β†’ **FAIL (6%, 54%)**; (2) `weights_tilt_null` β†’ **FAIL/vacuo**; +(3) marginale + null con potenza β†’ PASS; (4) taglia β‰₯ €0,10/giorno per pagare un modo di fallire in +piu' β†’ **FAIL: €0,0113/giorno** ($4,79/anno a $635). +Due prove ausiliarie che chiudono il caso: **due implementazioni difendibili della stessa regola** +(d'accordo sul 97,6% dei trade) danno **+0,066 contro +0,080** β€” *il 21% dell'effetto sta nel come si +riempie il warm-up*; e **versare €500/mese vale ~1456Γ— l'effetto misurato**. +**Libro, pesi, cron, config INVARIATI.** + +πŸ“Œ **Riapertura pre-registrata: non una data, una SOGLIA DI CAPITALE** β€” cio' che fallisce e' il +rapporto fra un costo operativo **fisso** e un beneficio **proporzionale**, e nessun dato in piu' lo +cambia (con SE(Sharpe)β‰ˆ1,4 su 6 mesi nessun forward misura 0,07). €0,10/giorno richiede **~$5.600** +alla stima della cella e **~$53.000** a quella onesta di famiglia β†’ si rivaluta alla revisione dei +**$20k** gia' in calendario, **e solo se la cella e' giustificata da un MECCANISMO** invece che +dall'argmax di una griglia. + +--- + +## 29 β€” PROP-RECAL (J rifatto con XS01 dalla storia lunga) β€” **la decisione** + +`scripts/research/r0822b_prop_recal.py` (891 righe, 309 s, nessuna rete). **5.212 valutazioni MC** +contate al rialzo; risoluzione dichiarata **Β±0,010 sui livelli di J, Β±0,007 sulle differenze +appaiate**. βœ… **Riproduzione ESATTA cifra per cifra** dei numeri di PROP-ALLOC (J 0,738/0,721/0,335; +P(pass) 82,2/77,3/38,6%; scala 39,28 €/g e 41,7%) piu' **tre controlli bit-exact a 0,0**. + +πŸ“Œ **IL RISULTATO: J non regge alla taglia pubblicata, e il colpevole NON e' la finestra.** +Decomposizione a **un grado di liberta' per volta**, stesso pannello: + +| passo | J | Ξ” | +|---|---|---| +| XS01 = HL 19 gambe, finestra di scoperta (**il pubblicato**) | 0,739 | β€” | +| β†’ Binance 19 gambe, **date allineate** (VENUE) | 0,732 | **βˆ’0,006** | +| β†’ Binance **13 gambe** (UNIVERSO) | 0,426 | **βˆ’0,307** | +| β†’ pannello lungo (FASE) | 0,459 | +0,034 | +| β†’ **fuori campione 2021-2023** (FINESTRA) | 0,520 | **+0,061** | + +🚨 **Il 42% del numero pubblicato sta nelle 6 gambe che fuori campione non esistono** (ARB OP SUI APT +SEI TIA) β€” **e quelle 6 furono messe nell'universo nel 2026-06 guardando la liquidita' HL del 2024+, +cioe' dentro la finestra di scoperta.** La finestra fuori campione, da sola, **migliora**. +βœ… Allargare l'universo man mano che le gambe nascono (10β†’19 espandente) da' **J identico a U13**: +i 0,307 **non si recuperano**. + +⚠️ **Errore di metodo catturato prima di pubblicare:** la prima stesura dava al passo venue **βˆ’0,254** +e avrebbe pubblicato *"Binance costa un quarto di J"*. Era **FASE, non venue** β€” `xsec_engine` +ribilancia sulla parita' dell'indice di pannello, e due pannelli che iniziano a date diverse +ribilanciano in **giorni di calendario diversi**. Allineati i pannelli, il venue vale **βˆ’0,006** = +βœ… **replica indipendente della validazione di XS01-OOS, sulla funzione obiettivo invece che sullo +Sharpe.** + +**J onesto** (mediana delle 10 fasi, peso 38/38/25): + +| finestra | mediana J | banda p10-p90 | LIVE 75/25 | **delta appaiato** | +|---|---|---|---|---| +| **W-LONG 2021+** | **0,453** | [0,431 – 0,488] | 0,281 | **+0,175 (10/10 fasi)** | +| W-OOS 2021-23 | 0,504 | [0,455 – 0,608] | 0,364 | +0,146 (10/10) | +| W-DISC 2024+ | 0,442 | [0,404 – 0,464] | 0,331 | +0,112 (9/10) | + +XS01 standalone fuori campione: Sharpe 1,04, drift 22,7%, **maxDD βˆ’19,2%** contro βˆ’11,2% dello sleeve +ufficiale β†’ **il rendimento regge, la coda no, e la barriera guarda la coda.** + +πŸ“Œ **Il peso 0,50 non sopravvive; la REGIONE robusta si': w_XS ∈ [25%, 38%] e' entro il 3% +dell'ottimo su TUTTE E TRE le finestre**, discovery inclusa. A 50% J scende a 0,438/0,477/0,408 e il +maxDD di libro risale a βˆ’12,6%/βˆ’11,6%/βˆ’9,1% (contro βˆ’9,6%/βˆ’9,6%/βˆ’7,3% a 25%) = **replica +indipendente del punto (ii) di XS01-OOS**. + +🚨 **IL NUMERO OPERATIVO, con banda di fase: P(β‰₯50 €/g) da €600 in 36 mesi = 7,8% [7,2–9,1%], con +P(zero) 25,5%** β€” contro il **41,7%** pubblicato stamattina. **6,6%** se la firm non lista gli alt; +**4,3%** per il libro live. ⚠️ J e P(β‰₯50/g) hanno **argmax diversi**: si cita il centro della regione +robusta, non l'argmax di finestra. + +**Stress ora ancorati al MISURATO** (delta appaiato argmaxβˆ’LIVE): drift pieno +0,193 Β· SKH Γ—0,50 ++0,124 Β· SKH Γ—0,33 +0,113 Β· **`XS Γ—0,00` +0,041** Β· SKH Γ—0,33 + XS Γ—0,00 **βˆ’0,024** (peggio del libro +live). Lo scenario severo **cambia stato**: non e' piu' *"non sappiamo se il drift esiste"* ma un +contro-fattuale a rendimento nullo, e dice che **XS01 come pura decorrelazione vale +0,04** β€” tutto il +resto e' il suo drift, che ora e' misurato per **13 gambe su 19**. + +**Gate:** lente **accoppiata obbligatoria** ovunque (close-only girata solo come controllo: **regala ++0,020 di J**); **banda di fase di XS01 girata β€” prima volta sotto la lente prop** (fortuna della fase +canonica solo +0,019, delta positivo **10/10**); null del de-levering **strutturale** (ogni +configurazione valutata a 8 leve). ⚠️ **Banda d'ancora di SKH01 (23 offset) NON girata**, surrogata +col taglio del drift β€” **e' la distorsione dichiarata due volte e mai rimossa**, ed e' diventata la +gamba (b) del gate. + +**VERDETTO: LEAD β€” ridimensionato di un fattore ~5 sul numero operativo, non falsificato.** L'edge +sopravvive alla storia lunga (+0,175 di J, **10/10 fasi**, col 2022 dentro); crollano il **peso 50%** +e il contributo delle **6 gambe non testabili**, non la finestra. + +πŸ“Œ **`GATE PROP-01` β€” sostituisce l'attesa di anni, che e' CHIUSA:** +**(a) LISTINO β€” verifica, €0, BLOCCANTE prima di ogni spesa:** β‰₯10 delle 13 gambe negoziabili **con +short**; sotto 10 il meccanismo congelato **non ribilancia affatto**. FAIL β†’ si decide sulla riga +senza XS01. *E' la stessa classe dell'errore GTAA01/PRIIPs.* +**(b) BANDA D'ANCORA DI SKH01 SOTTO LA LENTE PROP β€” misura, data 2026-10-31:** mediana delle +differenze appaiate sui 23 offset, **PASS se > +0,05** (l'agente dichiara **prima** di aspettarsi un +PASS; il surrogato dice +0,11/+0,12). +**(c) CAPITALE:** un'eval HYRO $100k costa **$579 su un conto di $635** β€” la prima eval consuma il +**91%** del conto. + +**`IL CANALE FUNDED: SI APRE A CONDIZIONE DI leggere il listino della firm` β€” e di citarlo come +P(β‰₯50 €/g) 7,8% [7,2–9,1%] con P(zero) 25,5%, non 42%.** + +--- + +## 30 β€” LISTINO (`GATE PROP-01` gamba (a)) β€” **la verifica da €0, fatta** + +Non un agente: eseguita dal coordinatore, perche' e' una **verifica esterna**, non un backtest β€” ed e' +la gamba **bloccante prima di ogni spesa**. La regola del progetto la impone da luglio: *la +negoziabilita' sul conto REALE va verificata quando lo sleeve entra in RICERCA* (lezione GTAA01/PRIIPs). + +πŸ“Œ **Metodo: NON leggere il sito della firm, interrogare il VENUE su cui la firm esegue.** HyroTrader +esegue su **Bybit** via API (o su Cleo, con dati Binance). Interrogato +`api.bybit.com/v5/market/instruments-info?category=linear` (pubblico, tokenless, **833 strumenti**), +lo stesso percorso che il progetto usa per certificare i feed. + +| esito | risultato | +|---|---| +| **U13 (le 13 gambe testabili fuori campione)** | **13/13 `Trading`, `LinearPerpetual`** | +| le 6 gambe che fuori campione non esistono | **6/6 `Trading`** | +| min notional | **$5 su tutte e 13** | +| leva max | 50x-150x | + +**`GATE PROP-01 (a): PASS 13/13` (soglia era β‰₯10).** E sono **perpetual lineari**: lo short e' +strutturale, non un permesso β€” non esiste un divieto di vendita allo scoperto su un perp. +⚠️ **Cio' che questo NON prova:** che HyroTrader non abbia una **propria** lista ristretta distinta da +quella di Bybit. La sua pagina dichiara *"700+ USDT perpetual pairs"* su un venue che ne ha 833 β†’ +coerente con nessuna restrizione significativa, ma e' la **sua** dichiarazione. Il residuo si chiude +con una domanda al supporto, non con un dataset. + +βœ… **VALIDAZIONE del modello del progetto (non correzione):** la pagina della firm dichiara +**daily drawdown 4% Β· max loss 6%**, e `scripts/research/r0725_hyro.py:48-49` usa gia' +`EV_TARGET, EV_DD, EV_DL = 0.10, 0.06, 0.04` e `FU_ML, FU_DL = 0.06, 0.04` β†’ **le regole modellate dal +25/07 sono quelle vere.** + +🚨 **UNA CORREZIONE ECONOMICA, invece, c'e': la quota d'ingresso non e' un COSTO AFFONDATO.** La firm +la dichiara **"Refundable Challenge Deposit, returned in full alongside your first funded payout"** +(pagina della firm + fonti secondarie concordi). Il progetto l'ha sempre modellata come **spesa +persa** β€” e la gamba (c) di `GATE PROP-01` dice *"un'eval $100k costa $579 su un conto di $635 = il +91%"*. **Resta vero che immobilizza il 91% del conto**, ma **non e' vero che lo brucia**: si perde +solo nel ramo in cui non si arriva mai a un payout (**P(zero) 25,5%**), e torna in quello in cui si +arriva. **E' una struttura a opzione, non una fee** β†’ l'EV del biglietto va rifatto. +⚠️ **Qualita' delle fonti dichiarata:** il listino degli strumenti e' **primario e misurato** (API del +venue); le regole di DD sono **primarie ma dichiarative** (pagina della firm); la rimborsabilita' e il +prezzo esatto del taglio $100k sono **primari-dichiarativi + secondari concordi**, non misurati. +πŸ“Œ **Conseguenza sul gate:** la gamba (a) e' **CHIUSA con un PASS**. Restano la **(b)** β€” banda +d'ancora di SKH01 sotto la lente prop, che un agente sta girando ora invece di aspettare il 31/10 β€” e +la **(c)**, che va **ricalcolata** con la quota come deposito rimborsabile invece che come spesa. + +--- + +## 31 β€” GATE-RECON (cosa direbbero i gate con le serie riparate) β€” **sola lettura, verificata** + +`scripts/research/r0822c_gate_recon.py` (719 righe, ~25 s). **Zero celle di selezione**: e' un audit, +tutte le soglie **lette dai file pre-registrati**, nessun parametro scelto dall'agente. +βœ… **Sola lettura provata**: md5 + mtime di **ogni** file `data/paper_*` invariati dopo la corsa. + +| gate | serie | n | Sharpe | maxDD | vol ann. | +|---|---|---|---|---|---| +| **STATARB** | **attuale** | 54 | **+1,96** | **2,24%** | 7,88% | +| **STATARB** | **riparata** | 53 | **βˆ’2,02** | **15,19%** | **31,42%** | +| XSR01 | attuale | 28 | βˆ’6,55 | 0,28% | 0,46% | +| XSR01 | riparata | 27 | βˆ’0,99 | 0,82% | 2,78% | +| DVOLSPREAD | attuale | 28 | βˆ’15,82 | 0,93% | 0,79% | +| DVOLSPREAD | riparata | 27 | βˆ’5,21 | 7,11% | 19,36% | + +Minuti di mercato registrati al giorno: **3,9 / 41,2 / 2,5** = βœ… replica indipendente dei 4/41/2 +pubblicati stamattina. + +**`STATARB 27/09 β€” riparata: RITIRO Β· attuale: CANDIDATO AL DEPLOY`** +**`XSR01 23/10 β€” riparata: RITIRO Β· attuale: RITIRO`** +**`DVOLSPREAD 24/10 β€” riparata: KILL Β· attuale: KILL`** +*(Non sono i verdetti dei gate: sono i criteri letti **oggi**, su finestre incomplete β€” 54/90, 28/90, +28/91 barre. Anticipare un gate pre-registrato e' selezione sull'hold-out, e l'agente lo dichiara.)* + +βœ… **LA PREMESSA SU CUI POGGIA TUTTO E' ORA CONFERMATA PER VIA DIRETTA, non piu' di traverso:** +`data/_feed_backup/*.prebuild.bak` conserva il **vintage pre-riscrittura** di stanotte β†’ +**0 barre chiuse cambiate su 1.761.259** (BTC/ETH 1h+5m). L'unica riga cambiata per file e' **l'ultima +del vintage, che era aperta** (BTC 21/08: close 73732,5β†’73690, volume 342β†’492 = **il difetto stesso, +in chiaro**). Su 62 cicli `paper_prevday` fa 1427/1488 identiche, con tasso di divergenza **piatto +nell'eta'** (2,2-5,1%, nessuna crescita) e concentrato all'ora del cron. +⚠️ **Limite dichiarato:** `hl_*_1d` e `dvol_*` **non hanno vintage su disco** β†’ per XSR01 e +DVOLSPREAD l'evidenza resta **indiretta**. + +**Prove d'identita' obbligatorie prima di ogni confronto:** **I1** pannello letto ora == quello del +cron di stanotte (`max|d| = 0`, 3/3, contro lo `state.json` scritto alle 00:35); **I2** barre gia' +chiuse identiche al bit (statarb 6/54, xsr 1/28, **dvolspread 0/28 β†’ per quel monitor la prova I2 NON +ESISTE, dichiarato**); **I3** troncare l'INPUT == affettare l'output (`max|d| = 0`, 3/3). + +🚨 **(1) IL VETO D'INTEGRITA' DI DVOLSPREAD NON SCATTA: 28 attive / 28 = 100% in ENTRAMBE le lenti.** +Una serie che misura **2 minuti di mercato al giorno** passa al **100%** un veto progettato apposta per +non far decidere su dati mancanti. **Non e' un problema di taratura:** il veto conta *se il DVOL +c'era*, non *quanto dura la barra su cui c'era* β€” **assi ortogonali**. La stessa soglia 0,80 applicata +alla quota di barre **chiuse** leggerebbe **0%** e vieterebbe. **Variante nuova di una regola vecchia: +una barra presente non e' una giornata presente.** +🚨 **(2) Il caso peggiore NON e' quello coi numeri piu' assurdi.** XSR01 (|Sh| Γ—6,6) e DVOLSPREAD +(Γ—3,0) dicono **lo stesso verdetto** in entrambe le lenti; **STATARB ha |Sharpe| quasi identico +(1,96 contro 2,02) e cambia SEGNO** β€” ed e' il monitor del gate **piu' vicino**. Un controllo che +cercasse *"numeri troppo belli"* **lo lascerebbe passare**. πŸ“Œ **La firma da cercare non e' il +|Sharpe| ma la vol forward molto sotto quella del backtest** (7,88% contro 31,42%). +πŸ“Œ **(3) L'asimmetria del 25/07 su STATARB:** la diagnostica pre-registrata (statica sempre-short) +**non e' affetta** β€” si ricalcola dal **pannello**, non dalla serie registrata (varia di βˆ’0,01 fra le +due finestre contro βˆ’3,97 della strategia). **Al gate si confronterebbe un benchmark GIUSTO con un +numero di strategia SBAGLIATO**, e il margine sembrerebbe **+5,83 invece di +1,87**. Il segno del +margine sopravvive: il difetto ribalta il gate, non questa diagnostica. + +πŸ“Œ **QUANTO COSTA ASPETTARE β€” l'unico argomento quantitativo su *quando* riparare.** Con +**rigenerazione**: **zero barre perse a qualunque data**; si paga solo il rischio che la riparazione +non arrivi. **Senza**: riparare **oggi** lascia **36/90** barre vere a STATARB (62/90 a XSR01, 63/91 a +DVOLSPREAD); **alla vigilia, 1**. E la riparazione parziale **non contamina la finestra, la accorcia** +β€” una barra troncata porta ~1/370 della varianza *e* del rendimento di una vera, quindi non pesa: +riparando oggi, il 27/09 si leggerebbe una finestra **piu' corta ma VERA** (SE(Sharpe) ~3,2 invece di +~2,0). **Riparare alla vigilia no.** + +⚠️ `implausible_sharpe` **girato per la prima volta sulle serie forward**: chiede nβ‰₯30, quindi **2 +serie su 3 non sono giudicabili**, e su statarb dice "ok" **in entrambe le lenti** β€” **il gate non +vede un segno ribaltato**. + +--- + +## 32 β€” SKH-ANCHOR-PROP (`GATE PROP-01` gamba (b), chiusa oggi invece che il 31/10) + +`scripts/research/r0822c_skh_anchor_prop.py` (391 s, nessuna rete). **5.127 celle**, contate al +rialzo, con **griglia congiunta PIENA 23 offset Γ— 10 fasi = 230 configurazioni** Γ— 8 leve β€” **non** un +campionamento e **non** una somma di marginali (lezione `r0726_loo_deluck`: *la somma delle fortune +marginali non e' la mediana congiunta*). + +πŸ“Œ **La data non aspettava un dato: aspettava che qualcuno misurasse il costo.** Il gate descriveva il +lavoro come *"rifare `pc._hourly_legs` ai 23 offset"*, che costa **0,6 s per offset**. I "~9 min per +assetΓ—offset" sono il costo di `r0726_skh_sigcache` β€” **una computazione che questa misura non usa**. +*Secondo gate d'attesa eliminato oggi dopo quello di XS01-OOS, e per la stessa ragione: la stima del +costo non era stata fatta.* + +**`GATE PROP-01 GAMBA (b): PASS`** β€” delta appaiato mediano sui 23 offset **+0,1162**, banda p10-p90 +**[+0,087, +0,152]**, **23/23 offset positivi**, **230/230 celle congiunte positive** (soglia +pre-registrata +0,05). Regione: 38/38/25 +0,116 Β· **38/25/38 +0,120** Β· 25/38/38 +0,109. +βœ… **Riproduzione 7/7 esatta** dei numeri di PROP-RECAL all'ancora canonica; **bit-exact 3/3 a 0,0** +(gambe orarie contro `pc._hourly_legs()` su 65.219 ore; costruttore di libro contro `PR.rg` a due +pesi), piu' la guardia "confini HTF βŠ‚ LTF" a ogni offset e l'assert che **l'indice giornaliero e' +invariante all'offset** β€” senza il quale il bootstrap non sarebbe appaiato. + +🚨 **MA il numero operativo scende di nuovo:** l'offset canonico 0 e' al **91Β° pctl dei 23** e regala +**+0,059 sul delta** e **+0,112 sul livello J**. J del candidato: canonico 0,453 β†’ **mediana d'ancora +~0,347**; maxDD mediano **βˆ’10,8%** contro βˆ’9,6% pubblicato. +**P(β‰₯50 €/g) da €600 in 36 mesi = mediana 4,4% [banda d'ancora 2,5%–8,2%], P(zero) 35,3%** β€” contro +7,8% / 25,5% di stamattina. βœ… All'ancora canonica la macchina **ridice 7,7%**: il crollo e' **fortuna +d'ancora, non disaccordo fra implementazioni**. + +πŸ“Œ **IL RISULTATO INTERPRETATIVO, piu' importante del PASS: la distorsione dichiarata due volte β€” +*"l'ottimo si appoggia a SKH01, lo sleeve con la fortuna d'ancora piu' grande da restituire"* β€” e' +MISURATA E REFUTATA nella sua forma forte.** A **`38/25/38` SKH01 sta al medesimo peso del LIVE +(25%)**, la sua ancora si cancella quasi esattamente nella differenza appaiata, e li' il delta e' **il +piu' alto dei tre (+0,120)** con la banda **piu' stretta** (ampiezza 0,032 contro 0,065 e 0,057 delle +due che portano SKH a 38%). **L'ampiezza della banda scala col differenziale di peso su SKH01** β€” come +l'agente aveva previsto **prima** di misurare. **Cio' che paga e' XS01, non un sovrappeso dello sleeve +fragile.** + +⚠️ **Due cose che il PASS non deve nascondere.** +1. **Il gate e' su una DIFFERENZA, il numero operativo e' un LIVELLO:** nella prima l'ancora si + cancella in parte, nel secondo **per niente** β†’ passano insieme e si muovono in modo diverso + (delta βˆ’33%, P(β‰₯50/g) βˆ’44%). *Un gate superato non e' un numero confermato.* +2. **Il surrogato del 22/08 e' finito vicino al vero per la CANCELLAZIONE DI DUE ASIMMETRIE:** + ricalcolato a peso fisso il taglio del drift da' **+0,076/+0,045** (sottostima il vero +0,116), ma + PROP-RECAL lo calcolava **ri-cercando l'argmax sui 10 pesi migliori** mentre il LIVE resta + inchiodato a 75/25 β€” sotto stress il candidato puo' scappare da SKH01 e il LIVE no β†’ si gonfia a + +0,124/+0,113. **Il numero era giusto, il metodo no.** + +⚠️ **Sensibilita' dichiarata:** il de-luck Γ—0,89 **su questa dimensione e' un doppio conteggio** (fu +misurato proprio come mediana-ancore/canonica del drift). A Γ—1,00 il delta sale a **+0,148**, 23/23 β†’ +**tenerlo e' conservativo**. +**Smentitore residuo, fuori dal perimetro della gamba (b):** l'ancora **giornaliera di TP01** (24 ore), +che questa misura **non tocca** e che resta canonica 00:00 UTC = **la piu' fortunata delle 24** per +l'hold-out di TP01. E' la **terza** dimensione, e l'agente la dichiara. + +--- + +## 33 β€” WORST-DAY (il peggior giorno del libro, misurato invece che assunto) + +`scripts/research/r0822c_worst_day.py` (1.112 righe, **12 s**). **847 valutazioni**, soglia EVT +**dichiarata a priori** (95Β° pctl) e criterio congelato prima delle sezioni decisive β†’ *non c'e' un +massimo scelto su cui barare, quindi non c'e' un DSR da calcolare*. + +Il filone che il critico dell'ondata aveva indicato come **la misura di maggior valore mai fatta**: +il gradino di leva vale **14,7a β†’ 12,9a a 1,25x** e **β†’ 11,6a a 1,50x** al capitale-rendita (⚠️ **correzione 23/08**: la prima stesura scriveva *"1,25-1,50x vale 14,7 β†’ 11,6a, ~€300/mese"* **fondendo due gradini** β€” il ~€300/mese e' del **1,50x**; a **1,25x** l'equivalente e' **+€164/mese**, ricalcolato sulla tabella netta) (~€282/mese a 1,50x di +versamenti equivalenti, a costo zero) ed era bloccato da **un solo parametro scelto a mano**: +*"un giorno a βˆ’10% ogni anno"*. + +πŸ“Œ **Il peggior giorno ha DUE risposte e differiscono di ~2Γ—:** **βˆ’3,94% in chiusura** (2020-03-13) e +**βˆ’7,33% al minimo intra-giorno** (mediana del rapporto sui giorni in perdita **1,89Γ—**). + +⚠️ **E il critico va CORRETTO nel meccanismo, non solo confermato.** Sui 10 giorni peggiori del +sottostante il libro fa **βˆ’2,34%** contro **βˆ’182,1%** del mercato (guadagna in 4/10; il 12 marzo 2020, +βˆ’43,6%, fa **+3,80%**) β€” **ma sui 160 giorni di crash perde in media βˆ’0,505%**, e l'esposizione di +TP01 scende monotona col crollo (0,096 nella fascia βˆ’5/βˆ’2% β†’ **0,042 sotto il βˆ’20%**). +**Il libro non e' immune ai crash: e' immune ai crash GROSSI, perche' li' il trend si e' gia' girato.** +E il suo peggior giorno **non e' un crash: e' uno short squeeze** β€” SKH01 short con stop 2% che gappa a +**βˆ’15,77%** di sleeve mentre TP01 e' flat (secondo peggiore βˆ’6,67% β†’ **la coda di SKH01 e' un giorno +solo**). + +**La coda stimata** (GPD, IC95 bootstrap a blocchi): chiusura **βˆ’2,90% a 1/1.000 giorni**, **βˆ’4,55% a +1/10.000** [βˆ’6,34 / βˆ’3,08%]. ⚠️ **Hill dice βˆ’7,34%** (ΞΎ 0,34 contro 0,09): **i due stimatori non +concordano β†’ si cita la banda**, non il numero. **Il conto ARITMETICO** (cap + vol-target, che +converte un crash nel suo z-score rendendo il bound invariante alla vol, + gap-through di SKH01): +peggior giorno **possibile** a k=1,00x = **βˆ’14,32% in chiusura**. +πŸ“Œ **Quindi il βˆ’10% sta al 70% del massimo strutturale: la TAGLIA e' plausibile, la FREQUENZA no** +(1 ogni 67-6.075 anni contro "ogni anno"). **La riserva e' ridimensionata di 2-4 ordini di grandezza, +non falsificata.** +πŸ“Œ **E la forma della coda non e' la leva:** muove k* di 1-2 unita', **il drift di 6,7**. Anche +prendendo lo stress a mano **alla lettera**: k* 2,95x β†’ mezzo-Kelly **1,48x** β†’ *lo stress bocciava il +gradino DOPPIO, non quello singolo*. **C1-C5 passano tutti** (margine strutturale 2,8Γ—, liquidazione +4Γ—, guadagno a drift βˆ’2SE +2,01%/anno). + +🚨 **IL RISULTATO CHE NESSUNO CERCAVA, ed e' quello che decide: il cap di `config/live.json` e' un +CLAMP, NON un moltiplicatore.** Morde su **3 osservazioni-asset su 5.650 (0,053%)**: alzarlo da 0,50 a +0,625 lascia il libro a k=1,00x in **5.647 giorni-asset su 5.650**. βœ… **Verificato dal coordinatore:** +le chiavi di `live.json` sono `max_notional_per_asset_usd/_frac`, `min_order_usd`, `disaster_sl_pct`, +`max_data_age_days`, `skh_feed_max_age_min` β€” **non esiste una chiave di scala**; servirebbe toccare +`WEIGHT`/`W_TP01`/`W_SKH` in `src/live/book.py`, cioe' **codice** su un percorso con soldi veri. +**La regola del progetto β€” *"ogni cambio di SCALA passa dal cap di config, non da `target_vol`"* β€” non +e' implementabile come scritta.** *Due mesi di discussione su un knob inesistente.* + +🚨 **SOTTOPRODOTTO CHE TOCCA OGNI NUMERO DEL PROGETTO: il funding dei perpetual non e' modellato in +NESSUN backtest.** βœ… **Verificato dal coordinatore:** zero occorrenze di `funding` in +`src/backtest/harness.py`, `src/strategies/trend_portfolio.py`, `src/portfolio/{sleeves,portfolio}.py` +**e in tutto `src/live/`**. Proxy Hyperliquid 14,8%/anno Γ— esposizione long media di TP01 0,103Β·E = +**~1,53%/anno che il libro paga davvero** = **il 9% del drift de-luckato**, cioe' **lo stesso ordine +della correzione d'ancora Γ—0,89** che il progetto ha misurato con cura. + +πŸ“Œ **Sottoprodotto forense: la lente del critico si IDENTIFICA dai valori che ha pubblicato** (+0,97 e ++0,72 compaiono **solo** nella riga canonica) β†’ aveva usato `book_series('canonical')`, e **su quella +lente il peggior giorno E' un giorno di ribasso** (βˆ’3,47%, 2025-10-10). *Aveva ragione, con la lente +sbagliata.* + +**VERDETTO: LEAD** (misura di politica, non strategia). +**`IL GRADINO DI LEVA A 1,25x: AUTORIZZABILE A CONDIZIONE DI` costruire prima una chiave di scala +esplicita in config, e di rifare `r0726_fee_sensitivity`** β€” la cui conclusione *"liquidation fee +irrilevante"* **vale solo a nozionale lordo ≀1x**, ed era gia' legata a un test di guardia sul cap. +⚠️ **Nota dell'agente, dovuta:** se si credono **entrambe** le riserve insieme (drift βˆ’2SE **e** βˆ’10% +annuo) il drift atteso e' **βˆ’0,61%/anno** β†’ *a quel punto non e' il gradino a essere sbagliato, e' il +libro, e chi usa quella combinazione sta chiedendo di spegnere anche il conto di oggi.* + +--- + +## 34 β€” CC01-2022 (il cash-and-carry riaperto sul dato che gli mancava) + +`scripts/research/r0822d_cc01_2022.py` (~8 s, zero rete). **47 celle** contate al rialzo. + +**Perche' si poteva riaprire:** il verdetto del 26/06 poggiava su una ragione **falsificabile e +strutturale** β€” *"lo storico funding parte dal 2023-05 β†’ manca il 2022, i rischi di coda sono fuori dal +dataset"*. Stamattina BASIS-CALENDAR ha ricostruito **30/30 futures trimestrali scaduti dal 2018-09, +col deleveraging 2022 dentro**. E' l'unica base su cui questo progetto riapre un filone: **dati nuovi.** + +βœ… **PRIMA della ricerca, la persistenza** (il critico aveva segnalato che i 20 MB vivevano **solo in +uno scratchpad di sessione**): copiati in **`data/raw/fut_deribit/`** β€” 72 file, 20,3 MB, 601.704 righe +(BTC 240.150 barre orarie / ETH 233.334, 34 contratti per asset, 2018-09β†’2026-08) + 64.110 ore di +funding e indice per asset dal 2019-04. **Sottocartella con prefisso, mai il namespace nudo** +(lezione SOL: il guardrail *"solo dati certi"* vive nell'**assenza** di un file). +βœ… **Guardrail verificato dal coordinatore, non assunto:** `load_data('BTC-30SEP22')` e +`load_data('SOL')` β†’ **`FileNotFoundError`**; `load_data('BTC','1h')` β†’ 70.311 barre; namespace nudo +pulito. **Il cron NON li rinfresca** (istantanea al 22/08, contratto in corso troncato) e restano +**fuori dal backup** perche' ricostruibili con ~2.700 chiamate pubbliche. + +πŸ“Œ **Q1 β€” nel 2022 il carry si INVERTE, e resta invertito per mesi.** Funding annuo **BTC βˆ’2,26% / +ETH βˆ’6,08%** (contro +16,5/+17,3% nel 2021); giorni a funding negativo **53,2%/58,9%**; basis mediano +del front **12,64% β†’ 1,26%** (BTC) e **12,95% β†’ βˆ’0,61%** (ETH). Peggior episodio: **ETH βˆ’6,70% di +funding cumulato in 170 giorni**; BTC βˆ’3,98% in 71 giorni nel **marzo-maggio 2020** = **un secondo +buco che CC01 non aveva.** + +**Q2 β€” la scala, netta fee:** + +| lente | Sharpe | maxDD | +|---|---|---| +| CC01 come pubblicato (funding HL, 2023-05+) | **+12,93** | 0,78% | +| stesso modello, funding **Deribit**, stessa finestra | +7,88 | 0,87% | +| stesso modello, Deribit, **finestra piena (col 2022)** | **+3,72** | 6,43% | +| + base MTM, premio liscio 24h (la lente di HL) | +1,33 | 6,99% | +| + base MTM, **premio grezzo** | +0,22 | **25,54%** | +| **carry ESEGUIBILE a 2 gambe, direzione CC01** | **βˆ’0,42** | 21,80% | + +πŸ“Œ **E il salto piu' grande NON e' il 2022: e' il VENUE** (+5,04 passando da HL a Deribit, stessa +finestra e stesso modello). +⚠️ **Correzione che l'agente ha fatto a se stesso sulla propria tabella:** aveva scritto *"il buco piu' +grande non era il 2022"* β€” falso, e' il pezzo singolo maggiore (+4,16). **Il punto vero e' piu' forte: +da solo non basta** (7,88β†’3,72 fa **ancora** scattare `implausible_sharpe`), e i due pezzi mai +dichiarati il 26/06 valgono insieme altrettanto. + +**Gate:** 🚨 **`implausible_sharpe` 5/5** sulle varianti `r=funding` β€” **compresa quella col 2022 +dentro**; **pulito** su tutte le varianti con la base o la seconda gamba. πŸ“Œ *Il gate NATO da CC01 +continua a segnalare CC01, e smette esattamente quando il rischio entra nel campione β€” e li' l'edge +non c'e'.* `marginal_vs_tp01`: **DILUTES** (sign +1) / **HEDGE** (sign βˆ’1). `deflated_sharpe` **0,363 +FAIL** su 47 trial (massimo atteso dal solo rumore **+0,24 > +0,11** del candidato). Null del +de-levering **REFUTED** sulla versione a 2 gambe. **Controllo positivo di causalita'**: togliendo lo +`.shift(1)` il numero **sale** (6,85β†’7,39) β†’ il test ha potenza. Ancora: fortuna **+0,005 = nessuna**. + +**Q3-Q4 β€” i muri non strutturali, riprezzati.** Lotto minimo BTC $10 / ETH $1 β†’ **l'eseguibilita' non +e' il vincolo** (**5Βͺ volta in questa ondata**); margine iniziale $13. ⚠️ **Ma tutti i futures datati +Deribit sono INVERSE: non esiste una linea USDC datata** β€” **stesso muro delle opzioni**, e il +collaterale in BTC e' esso stesso esposizione direzionale. **Non risolto**, serve una verifica sul +conto reale. +🚨 **CORREZIONE 2026-08-23 (Β§39): QUESTO MURO E' FALSO.** Esistono **16 futures datati con settlement +USDC** su BTC/ETH (il piu' vecchio `BTC_USDC-25SEP26`, creato **2026-05-07**), taker **identico al +perpetual** (0,00035) e min-order 0,0001 BTC / 0,001 ETH. βœ… Verificato dal coordinatore con una GET +pubblica. **La svista e' di NAMESPACE, non di fatto:** `get_instruments?currency=BTC` **non li +ritorna** β€” i lineari stanno sotto **`currency=USDC`** (48 datati in totale). *Il coordinatore ha +commesso la stessa svista al primo controllo e ha ottenuto 0.* **Il muro vero e' un altro: quella +linea ha 3,5 mesi di vita e ZERO roll trimestrali mai eseguiti.** **MAE per trade: βˆ’11,68% sulla storia piena β†’ leva max 7,8x/gamba**, contro βˆ’3,64% β†’ +20,9x nella sola finestra di CC01 = **2,7Γ— di leva di troppo**; sul carry puro con base MTM, MAE 90g +βˆ’16,57% β†’ **5,6-6,4x** contro le **~80x** che un maxDD dello 0,8% farebbe credere sostenibili. +⚠️ **Correzione a BASIS-CALENDAR:** il costo di roll e' **0,44%/anno**, non ~1,4% β€” quello contava il +roll **su entrambe le gambe**, ma **il perpetuo non si rolla**. Correggerlo **non crea un edge**. + +🚨 **IL SOTTOPRODOTTO CHE VALE PIU' DEL VERDETTO: la liscezza era una proprieta' della COLONNA, non del +rischio.** sd del Ξ”-premio giornaliero: **4,2 bps** (`premium` di Hyperliquid) contro **106,2 bps** +(Deribit grezzo) = **25-31Γ—**. Quindi la frase del 26/06 β€” *"il mark-to-market della base sgonfia lo +Sharpe solo 13β†’11, il basis-from-data NON e' il rischio vero"* β€” era **un artefatto della fonte**: +su Deribit lo stesso esperimento fa **10,87 β†’ 1,65 (liscio) / 0,37 (grezzo)** sulla **stessa finestra**. +πŸ“Œ **Secondo sottoprodotto:** convenzione **inverse β‰  lineare** vale βˆ’0,54 di Sharpe e βˆ’2,6pp di CAGR +(BTC), concentrati nel 2020-21 β€” su una coppia inverse con nozionali pareggiati in USD **resta un delta +residuo β‰ˆ βˆ’basis, che vale piu' dell'intero premio**. +⚠️ **Ipotesi dell'agente smentita in sessione:** il gate causale di CC01 **dodgia** il 2022 (βˆ’0,9% +contro βˆ’6,0%, l'85% della perdita) perche' il funding negativo e' **persistente**. + +**VERDETTO: `CC01 COL 2022 DENTRO: RESTA SCARTATO β€” e per la ragione giusta, ma quella pubblicata era +necessaria e non sufficiente.`** + +--- + +## 35 β€” FUNDING (il costo che il progetto non aveva mai prezzato) + +`scripts/research/r0822d_funding.py` (1.212 righe, 32 s, `--no-net` usa la cache). **Nessuna cella e' +una selezione** β†’ niente DSR da calcolare. Ridotto per budget: muri a 2.000 path (**risoluzione ~1%, +dichiarata**). + +Il progetto ha prezzato fee, slippage, min-order, pavimento IB, haircut, fortuna d'ancora, degrado +d'esecuzione, look-ahead, backfill, split, contaminazione EUR/USD, rischio di venue e fisco β€” **e mai +il costo di TENERE APERTA una posizione su un perpetual.** + +| serie | ShFULL | ShHOLD | maxDD | CAGR | drift | +|---|---|---|---|---|---| +| LIBRO 75/25 path live | 1,692 | 1,214 | 11,2% | 20,43% | 19,24% | +| **+ funding** | **1,513** | **1,154** | **11,5%** | **17,87%** | **17,08%** | +| Ξ” | βˆ’0,179 | βˆ’0,060 | **+0,3pp** | βˆ’2,56% | **βˆ’2,16%** | +| TP01 canonico | 1,314 β†’ **1,146** | 0,489 β†’ 0,420 | 14,3 β†’ 15,2% | 16,43 β†’ 14,02% | βˆ’2,10% | +| SKH01 50/50 path live | 1,247 β†’ **1,154** | 1,456 β†’ 1,439 | 24,6 β†’ 24,8% | 30,56 β†’ 27,59% | βˆ’2,32% | + +πŸ“Œ **Il costo va citato CON LA FINESTRA: βˆ’2,16%/anno** (2019-2026, proxy inverse sul 40% del campione) +Β· **βˆ’1,39%** sulla sola finestra dello strumento vero (β‰₯2022-03) Β· **βˆ’0,55%** sul 2025+ β€” e il 2022 e +il 2026 costano ~0 **perche' il libro era FLAT** (esposizione 0,008 / 0,018), non perche' il funding +sia sparito. + +🚨 **IL MECCANISMO, ed e' il risultato trasferibile: esposizione e funding sono POSITIVAMENTE +CORRELATI.** Il tasso che il libro paga davvero e' **1,86-2,55Γ—** l'incondizionato per TP01 e +**2,41-3,44Γ—** per SKH01. **Il prodotto ingenuo "esposizione media Γ— tasso medio" sottostima di ~2Γ—** +β€” ed e' esattamente la forma della stima grezza da cui e' nato il filone. +❌ **L'ipotesi da cui il filone e' partito β€” "SKH01 paga meno perche' sta a mercato il 12% del tempo" +β€” e' FALSA:** 2,24%/anno di sleeve contro **2,02%** di TP01. Quando e' a mercato SKH01 sta a nozionale +**1,0Γ—** (TP01 vol-targeted sta a **0,14Γ—**) **ed entra sui breakout, cioe' quando il funding e' caro.** +**Il peso ottimale resta 0,25** con e senza funding, e il delta e' negativo a **23/23 offset per ogni w**. +⚠️ **Il proxy Hyperliquid era 2,1-2,2Γ— il venue vero** sulle stesse ore (14,8% contro 6,8-7,1%): **due +errori opposti β€” tasso troppo alto, esposizione condizionale troppo bassa β€” che quasi si +compensavano.** *Compensarsi non e' misurare.* + +🚨 **COSA CAMBIA NEI NUMERI PUBBLICATI:** rendita perpetua **10,73% β†’ 9,14%**; muro +**$276,6k β†’ $325,0k (+17,5%)**; traiettoria da $600 a €250/mese **16,4a con P(20a) 90% β†’ 18,7a con +P(20a) 64%**; **soffitto direzionale ~1,31 β†’ ~1,15**. +πŸ“Œ **E il maxDD NON si muove (+0,3pp): e' una tassa sul DRIFT, non sulla coda** β€” per questo colpisce i +**muri** molto piu' dello Sharpe. Il gradino di leva **regge** (k* 13,7 β†’ 12,3, guadagno del passo +1,00β†’1,25Γ— βˆ’12%). +**NON cambia:** il peso **75/25**, il gradino di leva, il maxDD, ne' **l'ordine delle leve del piano**. + +⚠️ **AVVERTENZA DEL COORDINATORE, non dell'agente: questa correzione e quella del FISCO si SOMMANO, e +nessuna tabella pubblicata le contiene entrambe.** La riga "€250/mese" vale P(20a) **92% al lordo** Β· +**52% col fisco** (07/08) Β· **64% col funding** (oggi) β€” le due lenti sono state applicate +**separatamente** allo stesso numero lordo. La tabella con entrambe **non esiste ancora**, e il +risultato congiunto sara' **peggiore di 52%**. + +βœ… **Validato contro il CONTO VERO, e per ELIMINAZIONE invece che per grandezza:** modello contro +`realized_pnl` di sessione **ETH βˆ’0,2% / BTC βˆ’7,7%**, mentre le finestre sbagliate sbagliano di 2-4Γ—. +L'identificazione regge perche' nella sessione ci sono stati **0 ordini** (letti dal log) β†’ il P&L di +trading realizzato e' **esattamente zero**, quindi quel campo non puo' essere P&L di trading; e il +segno e' negativo su due gambe **entrambe long**. Funding realmente pagato da quando il libro esegue: +**$0,22**. Convenzione temporale **verificata sui dati** (`interest_8h` = somma delle 8 ore trailing +incluso T: mediana |dif| **2,2e-08** contro 1,1e-06 dell'alternativa). Replica **bit-exact 5/5**. + +⚠️ **BUG CATTURATO DAL CONTROLLO POSITIVO, non a occhio, e in famiglia gia' nota:** +`DatetimeIndex.astype("int64")` su tre serie dello stesso progetto con risoluzioni **diverse** +(`[ms]` il funding, `[us]` `al.get`, `[s]` il log) dava funding **0,000%/anno su SKH01** e +**+1,229%/GIORNO su TP01**. **Nessuna eccezione, nessun NaN: due numeri plausibili in due punti +diversi.** E' la stessa famiglia codificata in CLAUDE.md il **01/07** (`DatetimeIndex.view("int64")` +su indici tz-aware non-ns β†’ look-ahead che `causality_ok` non vede). Ora c'e' un controllo che +confronta `searchsorted` con un `groupby` di pandas **e verifica di avere potenza**. +⚠️ **Secondo errore corretto in sessione:** contava `diff(pos) < 0` come vendite β€” **su un perpetuale +lineare il nozionale scende da solo quando scende il mark.** + +**Smentitore principale dichiarato:** sul **pre-2022** l'agente usa l'**inverse come proxy** e i due +strumenti correlano solo **0,27-0,40** orario β†’ se il funding dei lineari nel 2019-2021 fosse stato +molto piu' basso, il βˆ’2,16% scenderebbe verso il **βˆ’1,39%** della finestra pulita. + +**VERDETTO: LEAD** (misura di politica, non strategia). +**`IL FUNDING COSTA 2,16%/ANNO NETTO AL LIBRO` β€” quanto la fortuna d'ancora Γ—0,89 che il progetto ha +misurato con enorme cura, e sposta i muri di capitale del +17,5%.** + +--- + +## 36 β€” PIANO-VERO (la tabella congiunta) β€” **la risposta alla domanda del progetto** + +`scripts/research/r0822d_piano_vero.py` (145 s). **Nessuna selezione** β†’ niente DSR. Muri a 4.000 +path (la taglia dei pubblicati), traiettorie a 3.000; **risoluzione MC misurata su 3 semi: 0,7% sul +muro, 1,5 €/m sul versamento** β€” dichiarata. + +**Il buco:** le due correzioni misurate al piano β€” **fisco d'accumulo** (07/08) e **funding** (22/08) β€” +erano state applicate **separatamente allo stesso numero lordo**, e la tabella con entrambe non +esisteva. L'agente ha dichiarato **prima di misurare** di aspettarsi un congiunto **meno severo della +somma** (meno drift β†’ meno plusvalenza β†’ meno imposta). + +βœ… **Replica 6/6 prima di pubblicare, due esatte AL DOLLARO:** sul vintage **2026-08-07** lo stesso +codice rida' **$272.061** (lordo) e **$258.338** (netto fisco); riproduce anche funding +βˆ’2,1597/βˆ’1,3931/βˆ’0,5508 %/anno, perpetua 10,73β†’9,14%, muro +17,5%, traiettoria 16,4a/90% β†’ 18,7a/64%. +⚠️ **Sulla serie di OGGI la stessa riga da' $278.033 (+2,2%)** β€” 15 giorni di dati in piu', `data/raw/` +gitignored: **stessa lezione GTAA del 07/08** β†’ ogni confronto fra lenti e' fatto **sullo stesso +vintage**. + +| lente (de-luck Γ—0,89) | drift | perpetua | muro | **€250/m da $635: P(20a)** | +|---|---|---|---|---| +| L0 LORDO (26/07) | 17,11% | 10,68% | $278k | **90%** *(pubbl. 92%)* | +| L1 +FISCO (07/08) | 17,11% | 7,52% | $264k | **49%** *(pubbl. 52%)* | +| L2 +FUNDING (22/08) | 15,19% | 9,06% | $328k | **63%** *(pubbl. 64%)* | +| 🚨 **L3 CONGIUNTA** | **15,19%** | **6,35%** | **$313k** | **14%** ← il numero che mancava | +| L3b congiunta, funding "strumento vero" βˆ’1,39%/a | 15,87% | 6,72% | $296k | **26%** | + +🚨 **LE DUE CORREZIONI NON SI COMPENSANO: SI SOMMANO β€” e nel versamento necessario il congiunto e' ++10% PEGGIORE della loro somma.** +πŸ“Œ **L'attesa dell'agente era sbagliata dove conta, e il perche' e' il risultato trasferibile:** il +meccanismo "meno drift β†’ meno imposta" **esiste** (+9,97% di capitale recuperato; il fisco toglie il +50,9% a funding OFF e il 46,0% a funding ON), **ma la funzione versamentoβ†’probabilita' e' CONVESSA e +ne ribalta il segno**. Interazione misurata **in tre monete**: **+9,97% sul capitale**, **βˆ’$815 sul +muro** (0,26%, *sotto* la risoluzione MC), **+26 €/mese sul versamento (+10%, segno stabile su 3 +semi**: +26/+29/+22). +**REGOLA: non contare su una compensazione fra correzioni misurate separatamente β€” la si misura, e in +piu' di una MONETA, perche' un'interazione piccola cambia segno con la moneta.** + +**Quanto versare (L3, da $635, nessun lump):** +| orizzonte | P=50% | P=75% | **P=90%** | totale versato @P=90% | **quota del bersaglio** | +|---|---|---|---|---|---| +| **10 anni** | €1.323 | €1.534 | **€1.733** | **$229.189** | **73%** | +| 15 anni | €656 | €784 | **€920** | $183.129 | 58% | +| 20 anni | €360 | €445 | **€541** | $143.804 | 46% | +Il **solo funding** aggiunge **1,27Γ— / 1,33Γ— / 1,40Γ—** sopra la richiesta della lente fisco-only. + +**Rendita €/g mediana a 20 anni (L3):** €250/m β†’ **31,85 €/g, P(β‰₯50/g) 8,8%** Β· €500/m β†’ 63,05, +**P 77,1%** Β· €800/m β†’ 100,50, **P 99,2%**. +πŸ“Œ **Il LUMP, che nelle tabelle nette non era mai entrato:** **€10.000 oggi** + €250/m porta P(20a) da +**14% a 45%**; €5.000 la porta a 30%. + +**Trappole evitate per costruzione** (tutte gia' pagate da questo progetto): non-arrivo = **+∞, mai +βˆ’1**; **mediana condizionata mai stampata senza la sua P accanto**; risoluzione MC misurata e +dichiarata. **Rischio di venue NON incluso di proposito**: e' **rovina, non costo** (a p=5% il mediano +e' zero comunque) β€” dichiarato. + +**VERDETTO: LEAD** (misura di politica). +``` +CON TUTTE LE CORREZIONI, €250/MESE DA $635: 23,4 anni (mediana incondizionata; + 22,1 condizionata all'arrivo), P(20a) 14% [banda funding "strumento vero": 22,2 anni, P 26%] +PER €50/GIORNO IN 10 ANNI SERVONO: €1.733/mese (P=90%), totale versato $229.189 +``` +πŸ“Œ **A 10 anni si versano $229k per arrivare a ~$313k: il rendimento fa il 27%, i bonifici il 73%.** +E' la forma piu' netta finora della lezione gia' scritta: **a orizzonte corto non fai lavorare la +strategia, COMPRI il capitale coi bonifici.** + +--- + +## 37 β€” GATE-C (l'ultima gamba del gate funded, e il biglietto riprezzato) + +`scripts/research/r0822d_gate_c.py` (356 s, nessuna rete). **1.347 celle**, **zero di selezione** +(pesi, meccanismo, lente, de-luck e soglie tutti **ereditati e congelati**) β†’ niente DSR da calcolare. +Ridotto e dichiarato: 23 offset Γ— **3 fasi** (lo stesso sottocampione a priori di Β§32). + +βœ… **Riproduzioni prima di ogni numero nuovo:** Β§32 (4,4% [2,5-8,2%], P(zero) 35,3%) **esatta**; Β§35 +(βˆ’2,16/βˆ’2,10/βˆ’2,32%) **esatta**; ladder contro `pc.simulate` **bit-exact, `max|diff| = 0,0` percorso +per percorso**. ⚠️ `r0725_hyro` da' **βˆ’11%** rispetto al pubblicato ($6.018 contro $6.789) **perche' il +file ri-deriva le serie dal feed corrente, +4 settimane** β€” stessa lezione del vintage. + +**Il biglietto a due stadi** (cella d'ancora **mediana**, non la canonica che sta al 91Β° pctl; funding +dentro; lente accoppiata; 6.000 percorsi): P(pass) **35,5-52,5%**, **P(payout | pass) 72,3-89,2%**, +E[payout] 1 anno funded $1.016-2.194. +**EV del biglietto $100k in tre convenzioni:** spesa persa **$+1.615** Β· rimborso al pass **$+1.919** Β· +**rimborso al primo payout (la regola vera) $+1.835**. **Positivo in tutte e tre.** +πŸ“Œ **Ma la correzione economica che aveva motivato il filone vale +13,6% di EV e +0,60pp su P(β‰₯50/g): +reale, misurata, e NON cambia nessuna decisione.** + +🚨 **Il funding costa anche qui, appaiato cella per cella (69 celle): dJ βˆ’0,0491 (69/69 negative), +dP(β‰₯50/g) βˆ’1,60pp (66/69)** β†’ **P(β‰₯50 €/g) 2,6% [1,5%, 4,7%], P(zero) 40,3%**. +**Catena completa del ridimensionamento in una giornata: 42% β†’ 7,8% β†’ 4,4% β†’ 2,6%.** + +🚨 **XS01 PAGA il funding, non lo incassa β€” e il meccanismo e' DIMOSTRATO, non congetturato:** +`max|Ξ£W| = 2,8e-17` (dollar-neutral **esatto**) β‡’ **il livello del funding non entra, entra solo la +dispersione cross-sezionale**; e il momentum cross-sezionale **compra i perp col funding piu' caro e +vende quelli col piu' basso** β†’ **+1,45%/anno** (IC95 [+1,18, +1,72]; banda onesta [+0,69, +1,45] se +HL e' ~2,1Γ— il venue eseguibile). *L'ipotesi opposta era nel mio briefing.* + +πŸ“Œ **Il progetto conteneva GIA' due modelli contraddittori della quota:** `r0725_hyro:200` la rimborsa +**al PASS** (ottimista), `pc.simulate` non la rimborsa **mai** (pessimista) β€” e il numero operativo +usciva dal secondo. Le due convenzioni *sbagliate* distano **$84** = prezzo Γ— (P(pass) βˆ’ P(payout)). +πŸ“Œ **Piu' leva sul funded ALZA E[payout] e ABBASSA P(payout):** chi vuole il rimborso non vuole la leva +massima. **Sono due obiettivi diversi.** +⚠️ **Un numero netto catturato PRIMA di pubblicarlo:** il "capitale d'incrocio €1.000-2.000" **NON e' +un meccanismo**. La ricchezza terminale della strada-biglietto e' **bimodale**, e +`P(cassa finale < $249)` si muove **liscia** (56,3 β†’ 50,8 β†’ 28,3 β†’ 16,9 β†’ 3,9 β†’ 0,0%) mentre la +mediana **salta di un ordine di grandezza** appena quella massa passa il 50%. **La mediana non e' una +statistica robusta qui** β€” ed e' esattamente il tipo di numero netto che si sarebbe finito per citare +come soglia di progetto. + +**IL CONFRONTO CHE DECIDE** (36 mesi, $654 iniziali, **stesso bootstrap appaiato**, funding in +entrambe le strade, fisco 33%): +| versamento | strada | mediana | media | P( 0 con la quota trattata come SPESA PERSA**, alla cella +d'ancora **mediana**, col funding dentro, sotto lente accoppiata. *Oggi: **$+1.615 β†’ PASS***. +**(c2) DI SOPRAVVIVENZA** β€” prezzo del biglietto **≀ 2 versamenti mensili del piano in corso**, +perche' il piano d'accumulo e' la leva dominante misurata e **zero e' assorbente**. *Oggi (piano +€500/mese): **$579 contro $1.090 β†’ PASS***. +πŸ“Œ **La FORMA del gate e' imposta dal costo della misura:** l'EV costa una corsa di script (zero +dollari), **la rimborsabilita' costa $579 e si puo' misurare solo comprando** β†’ un gate d'attesa su +quella sarebbe **superabile solo dopo averlo violato**. Percio' (c1) e' scritta perche' **la +rimborsabilita' NON decida**, e il rimborso diventa **un regalo invece di un'ipotesi su cui si e' +scommesso**. + +**`IL BIGLIETTO CONTRO IL VERSARE: MEGLIO β€” ma solo perche' il piano di versamento c'e', e la ragione +non e' il rendimento, e' il NUMERO DI VOLTE CHE SI PUO' GIOCARE.`** Senza versamenti la stessa +scommessa **perde la mediana 6Γ—** ($132 contro $787) e **vince la media 12Γ—**: e' una lotteria a valore +atteso positivo che un conto da $654 puo' permettersi di giocare **una volta sola**. +⚠️ E il vantaggio poggia **interamente** sul drift di XS01 misurato su **13 gambe su 19**. +**Smentitore che azzera tutto senza che il modello lo segnali:** una **morte-firm ≫10%/anno**. + +--- + +## 38 β€” SCALE-SPEC (la chiave di scala che non esiste, specificata) + +`docs/research/SPEC-scale-key.md` (38 KB) + `scripts/research/r0822e_scale_proto.py` (1,0 s, zero +rete, **zero scritture**). **~630 valutazioni, nessuna cella scelta** β†’ niente DSR. +βœ… **Produzione non toccata** (`git status` vuoto su `src/`, `config/`, `scripts/live/`, `tests/`), e +la sez. 0 del prototipo **verifica l'isolamento a runtime** β€” *ed e' fallita su se stessa alla prima +stesura, che e' il modo giusto per un controllo di scoprire che funziona.* +βœ… **Replica a k=1,00 superata 6/6** contro WORST-DAY (peggior giorno βˆ’3,95% il 2020-03-13, bound +strutturale βˆ’14,32%, morso del clamp 3/5.648 contro 3/5.650, drift e vol al centesimo). + +| | k=1,00 | **k=1,25** | +|---|---|---| +| Sharpe FULL | 1,5051 | **1,5051** (invariante) | +| drift / vol | 17,11% / 11,37% | 21,39% / 14,21% | +| g(k) esatto (CAGR geom.) | 17,90% | **22,61%** | +| maxDD storico | βˆ’12,17% | βˆ’15,01% | +| peggior giorno possibile | βˆ’14,32% | βˆ’17,90% | +| **costo di un disaster-SL** | 30,0% | **37,5%** ← la guardia che morde | +| margine di liquidazione | 15,8x | 10,1x | + +⚠️ **CORREZIONE A UN NUMERO CHE AVEVO SCRITTO IO** (nel briefing e in CLAUDE.md): *"il gradino +1,00xβ†’1,25-1,50x vale 14,7 β†’ 11,6 anni, ~€300/mese"* **fondeva due gradini**. Β§2 da' **1,25x β†’ 12,9a** +e **1,50x β†’ 11,6a**; il ~€300/mese e' del **1,50x**. Ricalcolato sulla tabella netta: **1,25x = ++€164/mese equivalenti** (l'interpolazione riproduce €282 per 1,50x = controllo superato). + +**Tre risultati che il mandato non prevedeva:** +1. 🚨 **`test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` NON si rompe β€” ed e' PEGGIO che + rompersi.** Misura `frac Β· n_asset` mentre la grandezza vera diventerebbe `frac Β· n_asset Β· scala` + β†’ **continua a passare e smette di controllare**. **4Βͺ occorrenza della forma "un controllo puntato + su una configurazione diversa da quella che gira"** (dopo `fee_watch` sugli inverse, la taratura di + `venue_watch` con bitfinex, i test di `book_live` senza potenza a libro flat). **Va cancellato con + nota, non aggiustato.** +2. 🚨 **La guardia che morde per prima e' il DISASTER-SL, non il peggior giorno:** dal bound + strutturale k ≀ 3,49x, **dal disaster-SL k ≀ 1,67x** = **2,1Γ— piu' stringente**. **C1 di WORST-DAY + guardava la grandezza sbagliata.** β†’ il tetto giusto e' l'invariante + `n_asset Β· frac Β· scala Β· disaster_sl_pct ≀ 0,50`, che lega **tre** chiavi. +3. 🚨 **Il tetto va sul PRODOTTO, non sulla chiave:** `frac=0,625` + `scala=1,25` fa leva **1,562x** e + **passerebbe** un tetto *"scala ≀ 1,25"*. Ed e' concreto: WORST-DAY ha appena pubblicato che alzare + `frac` da solo *"non produce k=1,25"* β€” **combinato con la scala lo produce eccome**. + +**Gate proposto `GATE SCALA-01`** β€” 9 condizioni, fra cui **A2** (180 giorni sul gradino precedente), +**A5** (tetto sul **prodotto**), **A6 anti-recency** (`k_ammesso` calcolato **togliendo gli ultimi 90 +giorni** dev'essere β‰₯ k richiesto) e **A9** (11 test verdi), piu' un sorvegliante giornaliero a **tre +stati**. Nasce dal fatto misurato che **la scala e' invisibile a tutti i gate esistenti**: Sharpe +**1,5051 identico** a tutte e 5 le celle di k β†’ `deflated_sharpe` e `marginal_vs_tp01` **non +falliscono, non vedono la variabile**. βœ… `weights_tilt_null` invece **passa e con potenza**: peso +implicito TP01 **0,750000 a ogni k**, `max|target(k) βˆ’ kΒ·target(1)| = 0,00e+00`, e il **controllo +positivo** (scala sulla sola gamba TP01 = cio' che farebbe `target_vol`) lo porta a **0,789** e viene +smascherato. + +**VERDETTO: LEAD** (specifica di processo). +**`LA CHIAVE DI SCALA: SPECIFICATA MA CON UN NODO IRRISOLTO`** β€” `execution.py:196-223` **ri-ancora +il disaster-SL al mark a ogni giro** (tolleranza 5%): il **βˆ’30% e' ROTOLANTE, non un pavimento**. +Se e' cosi', il 37,5% e' il costo di **un episodio** e un ribasso lento ne produce **piu' d'uno**, e +il margine 1,33x del gradino non e' quello vero. ⚠️ **Letto nel codice, NON misurato** β€” e il log non +lo permette (servirebbe `pos_prima`): **non ricostruibile a posteriori.** +**Secondo smentitore, con soglia dichiarata:** il ramo di **fallback dell'equity** oggi scatta +**0 volte su 1.442 giri**; se superasse il **2% dei giri in 90 giorni** il libro girerebbe a leva +**mista** e `g(k)` smetterebbe di descriverlo. + +--- + +## 39 β€” FUNDING-AVOID (evitare il funding: datati e spot) + +`scripts/research/r0822e_funding_avoid.py` (1.141 righe, ~45-56 s). **48 + 138 valutazioni**, nessuna +selezione (segnale e pesi **congelati**) β†’ niente DSR. βœ… Replica: **βˆ’2,161%/anno riprodotto** e +protetto da `assert` prima di ogni delta; muri A/F **identici ai pubblicati**. + +**L'unico filone della giornata che poteva RESTITUIRE drift.** + +| variante | ShFULL | ShHOLD | drift | Ξ”drift | muro | +|---|---|---|---|---|---| +| **A) perp (oggi)** | 1,513 | 1,154 | 17,08% | β€” | **$325,0k** | +| B) tutto sui datati | 1,495 | 1,129 | 17,43% | +0,35% | β€” | +| D) TP01 datato + SKH01 perp | 1,507 | **1,118** | 17,53% | **+0,47%** | $319,5k | +| **E) TP01 SPOT + SKH01 perp** | **1,649** | **1,203** | 18,63% | **+1,55%** | **$289,6k (βˆ’10,9%)** | +| F) nessun carry (irreale) | 1,692 | 1,214 | 19,24% | +2,16% | $276,6k | + +πŸ“Œ **La tesi condizionale REGGE ma non basta.** Funding condizionale **1,94Γ—/2,59Γ—** l'incondizionato +(βœ… replica indipendente del 1,86-2,55Γ— di ieri), basis solo **1,47Γ—/1,66Γ—**: in **livello** i due +carry sono identici (~6,4-7,4%), il datato vince **solo perche' si riprezza meno**. Ma il vantaggio e' +**tutto 2019-2023** e vale ~0 o e' avverso dal 2024 β†’ **ShHOLD βˆ’0,034, negativo a 0/23 offset**, e il +**gate iso-volatilita' FALLISCE** (1,507 contro 1,513). + +🚨 **L'ESTIMATORE ARITMETICO INVENTA META' DELL'EFFETTO.** La media delle differenze di rendimento fra +due strumenti sullo stesso sottostante contiene `(var A βˆ’ var B)/2`: qui vale **1,48% (BTC) / 2,60% +(ETH) l'anno**, cioe' **2-4Γ— l'effetto cercato**. Controllo che lo rende visibile: il perpetual +"sovra-rende" l'indice di +0,4/+0,5%/anno con l'aritmetico e **~0 col log**. **REGOLA: il carry si +misura sul LOG del rapporto dei prezzi** β€” stessa forma della regola UCITS del 26/07 (*"la deviazione +fra due veicoli sullo stesso indice si misura sul rapporto cumulato, mai sulla media delle +differenze"*), ripassata in veste nuova. + +⚠️ **Bug catturato in sessione, stessa famiglia di quello di ieri sera, e NON visto dal numero:** +`fund_*.parquet` etichetta l'indice **all'istante T**, `al.get(a,'1h')` etichetta la barra **all'inizio** +β†’ appaiarle **sfasa di un'ora** (corr **0,996 al lag +1**, ~0 al lag 0), gonfia la sd della base da 8 a +**68-86 bps** e produceva un finto **"+0,32%/anno" nel verso atteso**. πŸ“Œ **Si e' visto dalla +VOLATILITA' (+1,07pp su una strategia identica), non dal numero** β€” che era plausibile e nel verso +che l'agente si aspettava. Riparato con controllo positivo che fallisce se lo si sbaglia; il pezzo +vero vale **+0,07%/anno**. + +πŸ“Œ **Il cambio si giudica per SLEEVE:** SKH01 sui datati **peggiora** β€” turnover **48-57Γ—/anno** contro +6,6-8,4Γ— di TP01, quindi lo stesso mezzo spread gli costa **7 volte tanto**, e paga anche piu' carry. +Il netting TP01/SKH01 vale 1-2%: separarli non e' un argomento contro. + +🚨 **IL SOTTOPRODOTTO: lo strumento a carry ZERO esiste ed e' lo SPOT** (`BTC_USDC`/`ETH_USDC`, taker +**0,0** oggi, min 0,0001). TP01 e' long-flat e supera 1,0Γ— nello **0,102%** dei giorni β†’ il tetto non +morde. **+1,55%/anno di drift a volatilita' IDENTICA**, muro **βˆ’10,9%**, e supera il gate iso-vol. +⚠️ **NON e' un candidato, per tre ragioni dichiarate:** (a) **SKH01 non e' esprimibile sullo spot** +(e' long/short); (b) la **fee 0 e' dichiarata temporanea** e **`fee_watch` non sorveglia lo spot** (se +sparisse, **βˆ’13% del vantaggio**, misurato); (c) 🚨 **la domanda fiscale aperta vale PIU' del +risparmio** β€” se i derivati sono `c-quater` (26%) e lo spot `c-sexies` (33%), **7 punti su un CAGR del +17,9% fanno ~1,25%/anno**, cioe' **piu' di quello che lo spot porta**. + +**VERDETTO: `ESPRIMERE IL LIBRO SUI DATATI: COSTA 1,69%/ANNO CONTRO 2,16% DEL PERP β†’ NON CONVIENE`** +(i 0,47 punti risparmiati si pagano con βˆ’0,034 di ShHOLD, negativo a 0/23 offset, e il gate iso-vol +fallisce). **SCARTATO** sui datati, **LEAD** sullo spot per la sola gamba TP01 β€” subordinato alla +domanda fiscale, che va posta al commercialista **prima** di qualunque prova. + +--- + +## 40 β€” SL-ANCHOR (il nodo del disaster-SL, sciolto) + +`scripts/research/r0823_sl_anchor.py` (**2,0 s**, sola lettura). **42 celle**, nessuna riduzione per +budget. βœ… **Sola lettura provata**: `git status` mostra solo questo file; il ramo venue fa **2 sole** +`get_open_orders` e si astiene nei minuti :05-:10 e :24-:30. + +🚨 **La dichiarazione di non-misurabilita' della SPEC era FALSA: il log bastava.** Ieri notte +`SPEC-scale-key.md` scriveva *"letto nel codice, NON misurato β€” il log non lo permette (servirebbe +`pos_prima`): non ricostruibile a posteriori"*. **Terza conferma della regola del 26/07: un blocco +dichiarato e' un'IPOTESI, non un fatto.** + +πŸ“Œ **L'agente ha scritto l'attesa PRIMA di guardare, derivandola dal codice β€” e ha predetto le soglie +esatte:** rotolante e **simmetrico**, con innesco a **+5,263%** (salita) / **βˆ’4,762%** (discesa), +perche' il fattore 0,70 si semplifica e la condizione `|stpβˆ’want|/want β‰₯ 0,05` diventa una condizione +**sul mark**, non sullo stop. + +**Misura sul log di produzione** (1443 giri, 60,1 giorni, 2840 righe-asset): i 15 `placed` si +scompongono in **5 prime aperture + 7 ribilanci + 3 A POSIZIONE FERMA**. Le tre β€” 21/08 00:07 BTC, +06:07 ETH, 22:07 BTC β€” hanno azione `HOLD (a target)`, **0 ordini**, e **trigger spostato**: salti +**+5,74% / +5,81% / +5,38%**, tutti **appena sopra il +5,263% predetto**. +**Conferma dal venue:** la posizione BTC e' aperta dal **19/08 15:07**, l'ordine di stop sul book e' +stato creato il **22/08 01:07** β†’ **l'ordine e' 2,5 giorni piu' giovane della posizione che protegge.** + +🚨 **MA LA CONSEGUENZA CHE BLOCCAVA IL GRADINO E' REFUTATA, e va nel verso OPPOSTO al timore:** uno +stop che segue il prezzo **si allontana dal mercato**, quindi scatta **meno**. Episodi su storia reale +(caso peggiore: sempre long, rientro immediato): a **1h** β€” la cadenza che gira β€” BTC `roll` **0** +contro `floor` 1; ETH `roll` **1** contro 1; **mai piu' di 1 in 30 giorni**. Caduta massima storica +dentro un giro: BTC 1h **βˆ’23,17%**, ETH **βˆ’30,35%**, contro i ~26,5-30% che servirebbero. +πŸ“Œ **E nell'outage β€” l'unico scenario per cui il bracket esiste β€” il ri-ancoraggio e' SPENTO per +costruzione**: nessun giro di cron, nessun ri-piazzamento β‡’ **un episodio solo**. +**β‡’ Il 37,5% resta il costo di un episodio singolo, il margine 1,33Γ— di WORST-DAY e' quello vero, e la +sua gamba REGGE.** + +**Due cose da registrare, entrambe INDIPENDENTI da k:** +1. ⚠️ **Il nome inganna: "disaster-SL βˆ’30%" suggerisce un massimo di perdita per trade che NON + ESISTE.** Su cicli a ingresso proprio, il rotolante lascia passare perdite oltre βˆ’30% **senza + scattare** nel **0,03% (24h) / 0,96-2,84% (7g) / 7,22-13,33% (30g)** dei cicli, e il peggior DD + dall'ingresso e' **βˆ’60,6% BTC / βˆ’61,5% ETH** contro βˆ’38,3%/βˆ’45,4% del pavimento. +2. 🚨 **L'accumulo e' funzione della CADENZA DEL CRON, non della leva β€” e c'e' una trappola viva in + produzione:** il docstring di `book_execute.py` prescrive *"ogni ~230 minuti"* mentre **il cron gira + ogni ora**. Chi "correggesse" la cadenza verso il docstring sposterebbe il libro sulla riga **4h**, + **dove BTC raddoppia gli scatti** (`roll` 2 contro 1). A 24h ETH arriva a 5, con 2 in 30 giorni. + +⚠️ **Trappola n.10 del brief catturata in sessione:** la colonna "perdita dall'ingresso" della prima +stesura era un **artefatto** (posizione aperta dal 2018 β‡’ era il DD di un buy&hold pluriennale) β†’ +dichiarata e **rifatta con cicli a ingresso proprio**. E una prima versione della simulazione e' stata +**scartata** perche' il rientro avveniva fuori dal giro di cron (irrealistico). +βœ… **Controllo positivo superato** (obbligatorio): lo stesso esercizio applicato a un'implementazione +**a pavimento** risponde `ok` β†’ l'esercizio **distingue** le due implementazioni, non passa e basta. +βœ… **Guardia anti-rete superata**: la sottoclasse che esercita il vero `ensure_disaster_sl` ha `_post` +che **solleva**, e l'esercizio e' riuscito β‡’ **nessuna chiamata di rete e' partita da quel percorso.** + +⚠️ **Limite dichiarato:** **0 ri-ancoraggi verso il BASSO osservati su 3** β€” i 2 mesi di vita del libro +sono **un solo regime** (BTC da ~$66,5k a ~$78,6k) e non c'e' mai stato un calo del mark >4,76% con +posizione aperta. **La simmetria e' un fatto di CODICE** (un solo `abs()`, esercitato in entrambi i +versi), **non un fatto OSSERVATO**: la prima osservazione utile arriva al primo ribasso >5% con +posizione aperta. +πŸ“Œ **Nota di metodo:** la storia degli ordini **non e' raggiungibile dal percorso del conto** β€” il +gateway `cerbero-mcp` espone 32 tool Deribit e **nessuno** e' `get_order_history_by_currency`. Le +chiavi dirette appartengono a un altro progetto e **non sono state usate**: sarebbe stato inventare un +percorso, *e la domanda non ne aveva bisogno.* + +**VERDETTO: `IL DISASTER-SL E' ROTOLANTE (costo vero 37,5% contro 37,5% β€” un episodio solo alla cadenza +di produzione; sale a ~75% solo con cron a 24h, che non e' la configurazione che gira)`.** + +--- + +## β€” NOTIFIER (audit di fatto, coordinatore) β€” l'allarme puo' non arrivare, e nessuno lo sa + +**Non e' un filone di ricerca: e' un difetto di produzione trovato leggendo i log mentre l'ondata 7 +girava.** Nessun file di produzione toccato. + +**Come e' venuto fuori.** Il giro `cron_daily` di stanotte finisce con +`NON inviato (config Telegram assente o rete KO)`. La nota conflaziona due cause; **la prima e' +falsa** β€” `TELEGRAM_BOT_TOKEN` e `TELEGRAM_CHAT_ID` sono **presenti** in `.env`, e +`src/live/notifier._cfg()` lo legge (verificato). Quindi la causa e' la rete, e il messaggio manda +a controllare la configurazione. E' la **stessa forma** del difetto codificato il 29/07 sul +`venue_watch` (*"una nota di diagnosi cablata e' peggio di nessuna nota"*), su un percorso diverso. + +**Il difetto vero, che e' piu' grande della nota.** `src/live/notifier.send()`: + +``` +urllib.request.urlopen(url, data, timeout=10) # UN tentativo +except Exception: return False # esito ingoiato +``` + +- **nessun retry** β€” un singolo blip di rete perde il messaggio; +- **l'esito non viene registrato da nessuna parte**: `notify()` ritorna `bool` e + `scripts/live/venue_watch.py:80` **non lo guarda**; `logs/cron_book.log` contiene **0** + occorrenze di un qualsiasi esito d'invio (verificato con grep). *Quando serve sapere se + l'allarme e' arrivato, l'informazione non e' stata scritta.* +- 🚨 **e lo stato viene marcato PRIMA dell'invio.** In `src/live/venue_watch.lock_step`/streak + la riga `new.alerted = True` sta **dentro la logica pura**, e viene persistita comunque; alle ore + successive `already = st.alerted and st.sign == sign` fa tornare `"WATCH"` invece di `"ALERT"` β†’ + **`notify` non viene piu' chiamata per quell'episodio**. Un 🚨 perso **e' perso per l'episodio + intero**, e gli episodi storici (Mt.Gox, FTX, Bitfinex 2018-19) durano **da 200 a 2.324 ore con + segno costante**: e' esattamente il caso in cui la ri-notifica non arriva mai. + +**Quanto vale, misurato.** Sullo stesso `send()`, il digest giornaliero ha tentato **29** invii e +ne ha **falliti 2** β†’ **6,9%**, IC95 Wilson **[1,9%, 22,0%]**. +⚠️ **Fonte del numero, dichiarata:** e' il percorso del **digest** (messaggio lungo, una volta al +giorno), usato come **proxy** del percorso d'allarme (messaggio corto, orario) β€” che **non ha +nessun dato proprio, ed e' questo il punto**. Stessi endpoint, stesso timeout, stessa funzione. + +**Perche' conta piu' del suo numero.** L'economia del `venue_watch` (26/07) e' calcolata assumendo +che il vero positivo **arrivi**: costo atteso di un falso allarme 0,248% di equity contro il +**100%** che un vero positivo evita, break-even `p > 0,031%`. Una perdita del ~7% degli allarmi +non ribalta quel conto β€” ma **sorveglia l'unico rischio che il progetto ha prezzato come capace di +portare il conto a zero** (P(perso tutto) 10/18/34/64% a p=0,5/1/2/5%), e la sorveglianza e' +l'unica mitigazione rimasta dopo la decisione del 26/07 di stare **100% su Deribit fino a $20k**. +*Su una perdita totale, un allarme su quattordici che non arriva non e' un arrotondamento.* + +**Cosa NON e' stato fatto, e perche'.** Nessuna riparazione: `src/live/notifier.py` e +`scripts/live/venue_watch.py` sono produzione, il cron esegue dalla working tree, e un ramo di +ricerca non manda codice live. La riparazione e' piccola e ha tre pezzi indipendenti β€” (a) retry +con backoff su `send()`; (b) **registrare l'esito** nel punto in cui l'eccezione viene ingoiata +(la regola del 29/07, mai applicata al notifier); (c) marcare `alerted=True` **solo a invio +riuscito**, cosi' che un allarme non consegnato **si ripresenti** l'ora dopo. Il pezzo (c) e' +quello che cambia il comportamento e va deciso dall'operatore: rende l'allarme ripetitivo finche' +non passa, che e' il verso giusto per un 🚨 e il verso sbagliato per un ⚠️. + +⚠️ **Cio' che questo audit NON dice:** che il `venue_watch` non funzioni. Il rilevatore e' tarato, +validato su un controllo positivo (22 episodi Bitfinex) e ha 1 solo falso allarme in 8 anni. Il +difetto e' **a valle del rilevatore**, nel trasporto β€” la parte che nessuno aveva guardato perche' +non produce numeri. + +**VERDETTO: `LA RETE DI SICUREZZA HA UN TRASPORTO SENZA RETRY, SENZA REGISTRO E CON LO STATO +MARCATO PRIMA DELL'INVIO β€” ~7% DEGLI ALLARMI PUO' NON ARRIVARE, PER L'EPISODIO INTERO`** + +--- + +## 42 β€” TP01-LS (la gamba short del trend a 1d) + +`scripts/research/r0823_tp01_ls.py` (**4,4 s**). **4 trial dichiarati** (floor ∈ {0, βˆ’1/3, βˆ’2/3, βˆ’1} +sul solo TF di deploy); le **24 ancore non sono trial** e sono giudicate con `anchor_luck_band` / +`anchor_luck_delta`. Nessun file di produzione toccato. + +**Meccanismo congelato, UN grado di liberta'.** `TrendPortfolio` di produzione **importata e +sottoclassata**: l'unica riga diversa e' `np.clip(direction, floor, None)` al posto di +`np.clip(direction, 0, None)`. `net_returns` (shift causale + fee) **ereditata**. +βœ… **Replica bit-exact a 4 vie, `max|diff| = 0.0`:** floor=0 == `CANONICAL(long_only=True)`; +**floor=βˆ’1 == `CANONICAL(long_only=False)`** β€” cioe' la riga nuova coincide con la produzione a +**entrambi** gli estremi, e il codice davvero nuovo sono i soli due floor intermedi; sleeve 50/50 == +`sleeves._tp01_returns()`; `candidate_daily(floor=0)` == `tp01_baseline_daily()`. +βœ… **Controlli positivi 4/4**, fra cui il piu' importante: **gate iso-vol puntato sulla leva pura** +(canonico Γ—1,3) β†’ dCAGR grezzo **+5,05%** che diventa **+0,000000** iso-vol β€” *il gate riconosce la +leva travestita*. + +**Attesa a priori del coordinatore, dichiarata prima di misurare:** *"il guadagno sara' un anno +solo, il 2022"*. **Confermata e superata.** + +| floor | ShFULL | ShHOLD | CAGR | drift | vol | maxDD | turn/a | +|---|---|---|---|---|---|---|---| +| **0 (canonico LF)** | **1,305** | 0,441 | **16,32%** | **15,12%** | 12,14% | **14,31%** | 7,99 | +| βˆ’1/3 | 1,199 | 0,674 | 15,53% | 14,44% | 12,71% | 16,49% | 10,61 | +| βˆ’2/3 | 1,030 | 0,546 | 13,99% | 13,09% | 13,61% | 18,12% | 12,75 | +| βˆ’1 (LS pieno) | 0,855 | 0,433 | 12,37% | 11,66% | 14,94% | 20,61% | 15,02 | + +**Drift: NO, monotono decrescente. Gate iso-vol: FAIL 3/3** (k = 0,955/0,892/0,813 β†’ dCAGR +**βˆ’1,49% / βˆ’3,83% / βˆ’6,19%**). **Non e' leva travestita, e' peggio:** alza la vol **e** abbassa il +ritorno. + +**Banda d'ancora (24 ore, mediana delle differenze APPAIATE):** + +| floor | dShFULL | dShHOLD | dDRIFT | dMAXDD | +|---|---|---|---|---| +| βˆ’1/3 | **βˆ’0,114** [βˆ’0,149,βˆ’0,076] **0/24** | +0,219 24/24 | βˆ’0,78% 0/24 | +3,33pp 24/24 | +| βˆ’2/3 | **βˆ’0,224** 0/24 | +0,258 24/24 | βˆ’1,29% 0/24 | +5,12pp 24/24 | +| βˆ’1 | **βˆ’0,354** [βˆ’0,450,βˆ’0,251] **0/24** | +0,292 23/24 | **βˆ’1,91% 0/24** | **+8,80pp 24/24** | + +πŸ“Œ **Il 2022, e la risposta e' piu' netta dell'attesa.** dSharpe per anno a floor=βˆ’1: 2019 βˆ’0,10 Β· +2020 **βˆ’0,85** Β· 2021 βˆ’0,07 Β· **2022 +1,84** Β· 2023 **βˆ’1,01** Β· 2024 βˆ’0,61 Β· 2025 +0,27 Β· 2026 βˆ’0,68 +β†’ **2/8 anni positivi**. **Senza il 2022 il divario RADDOPPIA** (dShFULL βˆ’0,406 in 0/24, dDRIFT +βˆ’3,71%, iso-vol dCAGR **βˆ’7,44%**). **Il 2022 non e' l'anno che regge la tesi: e' l'unico che la +tiene a galla a meta'** β€” nemmeno con lui dentro la short arriva in pari. + +**Marginale NEUTRAL 3/3** (corrβ†’TP01 0,93/0,87/0,79, alpha **βˆ’0,21 / βˆ’1,40 / βˆ’2,60%/anno**, +`multicut_persistent=False`, `robust_oos=False`). Diagnostica: e' un **hedge debole** +(corr(Sharpe-TP01 annuo, uplift) βˆ’0,83β€¦βˆ’0,87), non alpha. + +πŸ“Œ **Il gate che chiude la faccenda: selection-on-holdout.** Sharpe in-sample (<2025) **1,488** / +1,322 / 1,151 / 0,972 β†’ la cella scelta **al buio** da `select_cell_insample` e' **il canonico**; +quella scelta sull'hold-out e' floor=βˆ’1/3. **Il guadagno hold-out non e' selezionabile.** + +⚠️ **L'unica cosa che va nel verso della short, riportata per intero:** dShHOLD **+0,292 in 23/24** +ancore β€” ma **all'ancora canonica vale βˆ’0,008** (p10 +0,094, p90 +0,541). E' il caso **speculare** +della lezione del 26/07: li' l'ancora canonica *inventava* un danno, qui ne *nasconde* un vantaggio. +Resta un hold-out di 1,6 anni, non selezionabile, con multicut negativo in 6 anni su 7. + +**Protezione β€” la short erode proprio cio' per cui TP01 esiste.** Criterio (B) di `edge_watch` +(DD sleeve < 75% del DD buy&hold negli anni di sinistro): canonico **0/8 violazioni**, +**floor=βˆ’1 viola 1/8** (2023, 0,80); maxDD full 14,31% β†’ 20,61%. + +**E non e' morte-per-fee:** a **fee ZERO** 1,338 contro **0,904**. L'edge lordo non c'e'; il +turnover raddoppiato e' un'aggravante, non la causa. + +**Libro 75/25:** ShFULL 1,814 β†’ 1,347, maxDD 9,42% β†’ 14,44%, banda **dSh βˆ’0,356 in 0/24**. +**Ridondanza con SKH01: no, e non aiuta** β€” la gamba short isolata ha corr **+0,10** con SKH01 ma +Sharpe **βˆ’0,335**: non e' ridondante, e' perdente. + +**Eseguibilita' a $635:** haircut **Β±0,002** (**5Βͺ volta nell'ondata** che l'eseguibilita' non e' il +vincolo). πŸ“Œ **Trovato per strada:** `src/live/book.py` contiene `max(tp_frac, 0.0)` β†’ **il +long-flat e' cablato anche nell'ESECUTORE**, non solo in `CANONICAL`. + +**DSR 0,997/0,988/0,962** con sr0 nullo 0,208 su N=4 β€” **e non e' il gate che decide**: con 4 celle +quasi identiche sr0 e' piccolo per costruzione (Γ¨ la lezione Β§10 del 22/08 vista dal lato buono). + +⚠️ **Errori catturati dall'agente su se stesso:** (i) il trial set del DSR era costruito **a mano** +invece che preso da `select_cell_insample` β€” due percorsi per lo stesso numero e' esattamente cio' +che il gate codificato evita; ora sono confrontati e coincidono; (ii) un `:+.2f` su una frazione +stampava **+0,09pp** invece di **+8,80pp**. + +**Cosa NON e' misurato, dichiarato:** il **funding** (nessun backtest del progetto lo modella, Β§35) +β€” e' l'unica voce che potrebbe spingere verso la short, ma l'agente ne da' un **argomento, non una +misura** (ordine ~1-2%/anno contro 1,91% di drift perso): si chiude con `r0822d_funding.py`. +Inoltre: nessun TF diverso dal 1d (riaprirlo riapre la **famiglia**), banda del libro con SKH01 alla +sola ancora canonica, tutto **close-only**. + +**VERDETTO: `LA SHORT DEL TREND A 1d NON PAGA IL PROPRIO COSTO SU NESSUN ASSE CHE DECIDE`** β€” +SCARTATO. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 41 β€” TP01-SIZE (una size che risponde alla convinzione) + +`scripts/research/r0823_tp01_size.py` (**8,4 s**, nessuna rete). **60 celle dichiarate prima di +guardare** (`rho` = g(1/3)/g(1), 10 valori Γ— `q` = esponente del vol-targeting, 6 valori) Γ— 24 +ancore = 1.440 valutazioni di sleeve + 3 controlli positivi. **Segnale CONGELATO** (TSMOM 30/90/180, +long-flat): cambia solo la mappa `convinzione β†’ size`. + +βœ… **Replica bit-exact**: `target_series` vs `TrendPortfolio.target_series` **max|diff| = 0**; +rendimenti vs `sleeves._tp01_returns()` **max|diff| = 0** (n=2720). +⚠️ **Sottoprodotto sull'harness:** `altlib.tp01_baseline_daily()` **NON e' bit-exact col sleeve** β€” +1 ulp (1,11e-16) su **1863/2720 giorni (68,5%)**, perche' `_to_daily` fa +`(1+r).resample("1D").prod()-1`, che e' un no-op **algebrico** ma non in virgola mobile su una serie +gia' giornaliera. Innocuo, ma *"bit-exact vs altlib" non e' un controllo ottenibile*: chi lo pretende +insegue un fantasma. + +🚨 **CORREZIONE ALLA PREMESSA DEL MANDATO (mia), verificata al sorgente.** Il briefing diceva che +la convinzione e' *"spesso 1/3 o 2/3"*. **Il bucket 2/3 e' aritmeticamente impossibile:** +`tsmom_blend` media **tre `np.sign(...)`** β†’ la direzione vive in **{βˆ’1, βˆ’1/3, +1/3, +1}**, e dopo il +clip long-flat in **{0, 1/3, 1}** (misurato BTC: 44,2% / 24,7% / 31,1%). βœ… Confermato da **due +agenti indipendenti** e ri-verificato dal coordinatore leggendo `src/strategies/trend_portfolio.py`. +**Conseguenza:** potenza `c^p`, floor affine e soglia sono **la stessa cella riparametrizzata** β€” la +famiglia della convinzione ha **UN** grado di liberta', non tre. + +**Attesa a priori dell'agente, scritta nel docstring prima di misurare:** *"il canonico e' gia' +dentro il suo plateau; qualunque rimodellamento dara' un delta indistinguibile da zero a iso-vol… +VERDETTO ATTESO: SCARTATO"*, col corollario che il null del de-levering sarebbe apparso in **forma +speculare (re-levering)**. **Entrambi confermati.** + +| | ShFULL | ShIS | ShHOLD | vol | CAGR grezzo | CAGR **iso-vol** | +|---|---|---|---|---|---|---| +| **canonico** rho=1/3 q=1 | 1,305 | 1,488 | 0,441 | 12,14% | 16,32% | **16,32%** | +| binario rho=1 q=1 | 1,265 | 1,398 | **0,674** | 15,30% | 19,95% | 15,87% (βˆ’0,45pp) | +| soglia rho=0 q=1 | 1,229 | 1,429 | 0,236 | 11,46% | 14,38% | 15,16% (βˆ’1,16pp) | +| **niente vol-target** q=0 | 1,166 | 1,298 | 0,479 | **39,65%** | **46,75%** | 13,92% (βˆ’2,40pp) | +| miglior cella iso-vol | 1,312 | 1,498 | 0,480 | 9,82% | 13,20% | 16,61% (**+0,30pp**) | +| cella scelta **al buio** | 1,306 | 1,508 | 0,382 | 9,26% | 12,38% | 16,48% (+0,16pp) | + +Rango del canonico: **5/60 in-sample, 6/60 full, 41/60 hold-out**. + +πŸ“Œ **NULL DEL RE-LEVERING nella forma piu' pura mai misurata nel progetto (6Βͺ occorrenza, prima +col segno rovesciato):** togliere il vol-targeting porta il CAGR **16,32% β†’ 46,75% (+186%)** e a +iso-vol fa **βˆ’2,40pp**. **Era tutta leva.** Il gate e' esatto per costruzione (`net` omogeneo di +grado 1 β†’ lo Sharpe e' invariante, verificato a 4,4e-16) quindi **morde sul drift, non sullo +Sharpe**; `k` calibrato **in-sample** per non introdurre look-ahead nella normalizzazione. + +**Banda d'ancora (24, mediana delle differenze APPAIATE):** + +| cella | dShFULL | >0 | dShHOLD | >0 | dCAGRiso | >0 | +|---|---|---|---|---|---|---| +| scelta al buio 0,25/1,25 | +0,0074 | 17/24 | **βˆ’0,0580** | **0/24** | +0,242pp | 22/24 | +| de-luckata 0,15/1,25 | +0,0002 | 12/24 | βˆ’0,1108 | 0/24 | +0,090pp | 16/24 | +| binario 1,00/1,00 | βˆ’0,0932 | 2/24 | **+0,2429** | **24/24** | βˆ’1,029pp | 3/24 | + +⚠️ Le due selezioni oneste **non coincidono** (ancora 0 β†’ rho=0,25; mediana su 24 ancore β†’ rho=0,15): +**la superficie e' piatta**. +βœ… **Replica indipendente, su dati odierni, del finding d'ancora del 02/07:** hold-out canonico +**+0,441 = 96Β° pctl** delle 24, **mediana onesta +0,219**, banda [+0,014, +0,441]; FULL canonico al +29Β° pctl. + +**Per anno** (mediana appaiata su 24 ancore): 2019 +0,006 Β· **2020 +0,085 (24/24)** Β· 2021 βˆ’0,077 +(0/24) Β· 2022 βˆ’0,080 (0/24) Β· 2023 +0,020 Β· **2024 +0,057 (23/24)** Β· 2025 βˆ’0,051 Β· 2026 βˆ’0,003 β†’ +**4/8 positivi = moneta**: non "un anno solo", ma nemmeno persistenza. + +**Impatto sul LIBRO** (canonico ShFULL 1,814 Β· ShHOLD 1,437 Β· CAGR 21,22% Β· maxDD 9,42%): la cella +scelta fa **dShFULL +0,0006 (12/24 = moneta)**, **dShHOLD βˆ’0,0878 (0/24)**, dCAGR +0,188pp (22/24) e +πŸ“Œ **dmaxDD +0,493pp PEGGIORE a 24 ancore su 24** β€” e il valore dichiarato di TP01 **e' il taglio +del DD**. + +🚨 **IL RISULTATO CHE DECIDE, e nessuno lo cercava: `Spearman(ShIS, ShHOLD) = βˆ’0,527` (p<0,001) +sulle 60 celle β€” in questa famiglia scegliere in-sample e' PEGGIO DI UNA MONETA.** Ed e' un +meccanismo misurato, non rumore: **`Spearman(rho, ShHOLD) = +1,000` a tutti e sei i q** (in hold-out +l'ottimo e' al bordo rhoβ‰₯1) mentre in-sample la curva e' **a gobba** con ottimo interno ~0,45-0,60. +**La convinzione e' informativa in-sample e ANTI-informativa in hold-out** β€” visibile anche nei +bucket (BTC hold-out: convinzione debole Sharpe **+2,20** contro piena **βˆ’0,06**). Quindi l'unica +cella con guadagno hold-out robusto (binario rho=1, 24/24) e' **esattamente quella che si potrebbe +scegliere solo guardando l'hold-out** = il gate del 29/06. **Non esiste una cella proponibile, +nemmeno diversa da quella scelta.** + +**Gate e controlli.** `causality_ok` **True** (max_tail_diff 0,0); `select_cell_insample` importato +sceglie **la stessa cella** (identita' verificata); **`deflated_sharpe` 0,9990 PASS ma quasi vacuo** +(famiglia omogenea, sd(Sharpe) 0,084 β†’ sr0 0,197; **FAIL a sr0 β‰₯0,9**, 0,229 all'sr0 di screen +1,572) = **terza conferma indipendente del Β§10**; `marginal_vs_tp01` **REDUNDANT per costruzione** +(giudica un'aggiunta, non la SOSTITUZIONE della size di uno sleeve gia' nel libro) β†’ riportato, non +usato come gate. **Controlli positivi 3/3**: de-levering Γ—0,5 β†’ dSharpe **esattamente 0** e dCAGR +iso **esattamente 0**; oracolo look-ahead β†’ Sharpe **10,97** con `causality_ok=False`; anti (rho=3) +β†’ βˆ’3,55pp. **Eseguibilita' a $635: haircut 0,001** (**8Βͺ volta nell'ondata**). + +**Cosa NON e' misurato:** altri TF (il deploy e' a 1d, e sotto le 12h e' vietato dal 19/06); ancora +di SKH01 (si cancella nella differenza appaiata); una convinzione da un **segnale** diverso +(magnitudine invece di segno) β€” sarebbe un'altra **famiglia**, da dichiarare e deflazionare a parte; +`weights_tilt_null` non applicabile (il 75/25 non si muove). + +**VERDETTO: `NON ESISTE UNA CELLA PROPONIBILE β€” E IL MOTIVO E' CHE IN QUESTA FAMIGLIA LA SELEZIONE +IN-SAMPLE E' ANTI-CORRELATA COL FUTURO`** β€” SCARTATO. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 44 β€” DEPOSIT-TIMING (il timing dei bonifici) + +`scripts/research/r0823_deposit_timing.py` (**81 s**, nessuna rete). **3.000 percorsi**, block +bootstrap a 20 giorni, 25 anni, lente **L3 CONGIUNTA** (funding + fisco d'accumulo + ancora Γ—0,89), +bersaglio **$313k**. **Risoluzione MC misurata su 3 semi: 1,13 pp sui LIVELLI, 1,97 pp sulle +differenze APPAIATE** (i confronti girano sugli stessi percorsi) β†’ si cita al punto percentuale. + +βœ… **Replica 4/4 prima di ogni delta:** muro **$313.143** contro $313k; €250/m **23,4a / 14,4%**; +€500/m **17,3a / 85,4%**; e soprattutto **P0 del motore nuovo vs `PN.accumula`: `max|dif| = 0.0`** +sul capitale **e 0 sul giorno d'arrivo**; `boot_idx` bit-exact contro `CC._boot_paths`. + +**Attesa a priori, dichiarata prima di misurare:** *"non funziona, perche' entrare piu' tardi domina +il comprare piu' in basso"*. **Confermata** β€” ma il meccanismo vale piu' del verdetto. + +**€500/mese, stesso flusso di cassa disponibile per tutte le politiche:** + +| politica | P(20a) | dP20 appaiato | attesa (g) | prezzo d'ingresso vs P0 | +|---|---|---|---|---| +| **P0 piatto** | **85,4%** | β€” | 0 | 1,000 | +| P1 dip 5% | 84,0% | βˆ’1,4% | 95 | **1,009** | +| P1 dip 10% | 69,7% | βˆ’15,7% | 713 | 0,959 | +| P1 dip 15% | 27,7% | βˆ’57,7% | 2.254 | 0,718 | +| P1 dip 20% | **6,3%** | **βˆ’79,1%** | 3.682 | **0,494** | +| P2 risk-off (TP01 a 0x) | 84,8% | βˆ’0,6% | 22 | 1,002 | +| P3 valore-medio g=0 / 8 / 15% | 48,2 / 83,5 / 85,3% | βˆ’37,2 / βˆ’1,9 / βˆ’0,1% | 2.489 / 789 / 32 | 0,220 / 0,560 / 0,997 | +| P4 anti-dip 5-20% | 85,4 β†’ 84,7% | βˆ’0,0 β†’ βˆ’0,7% | 1 β†’ 16 | ~1,000 | + +**0/12 celle in OGNI lente** (€500/m, €250/m, IID, drift dimezzato). Il migliore in assoluto vale +βˆ’0,03% ed e' P4 anti-5%, che aspetta **1 giorno** = P0 travestito. **Niente da deflazionare:** il +conteggio dei trial non e' il vincolo, il segno e' negativo e **monotono nella profondita' della +condizione**. + +πŸ“Œ **IL MECCANISMO β€” il segnale FA quello che promette, e perde lo stesso.** Dip 20% compra al +**0,494** del livello medio del piatto, cioe' **il 51% piu' in basso**, e arriva nel 6,3% dei casi: +*comprare meglio e comprare tardi sono la stessa mossa*. E sotto soglia bassa **si ribalta** β€” dip 5% +compra a **1,009**, cioe' **piu' in alto**: su una serie che sale, una condizione poco profonda non +compra il calo, **ritarda dentro la salita**. + +πŸ“Œ **L'ASIMMETRIA E' LA MISURA.** P4 anti-dip **non** da' lo stesso segno di taglia di P1 (βˆ’0,3% +medio contro βˆ’38,5%) β†’ non stiamo misurando rumore. Ma non e' simmetrico, e il perche' e' il +risultato: **un rialzo del 20% dal minimo arriva in 16 giorni, un ribasso del 20% dal massimo in +3.682**. *Il costo non e' scommettere sul lato sbagliato: e' mettere una **condizione rara** davanti +a un bonifico, in qualunque direzione.* + +**Rischio di venue, separato dal timing** (convenzione dichiarata: al fallimento il libro va a zero, +**la cassa sopravvive**, si riparte). Il meccanismo esiste β€” tenere i soldi fuori riduce la penalita' +di **+2,4 pp a p=1%** e **+3,8 pp a p=2%** su dip 10% β€” ma contro una penalita' di timing di +**βˆ’15,7 pp**, ed e' **un ordine di grandezza troppo piccolo**. E `P(libro azzerato)` e' **identica +per tutte le politiche** (21,6% a p=1%): *il conto salta comunque, cambia quanto c'e' sopra.* La +protezione si compra con un **secondo conto**, non ritardando i bonifici β€” coerente col 27/07. + +⚠️ **Due errori catturati dall'agente su se stesso, entrambi da un controllo e non a occhio.** +(i) Il **ritardo-gemello** (stessa attesa media, deterministica, nessun segnale) dava "informazione" +**+11,5%** a dip 15% sul mercato vero: leggendo solo quella riga avrebbe scritto che il buy-the-dip +contiene previsione. Sotto **IID**, dove per costruzione non c'e' nulla da prevedere, la stessa +colonna vale **+14,9%** β€” **non piu' piccola, piu' grande** β†’ il divario e' **meccanico**, non +previsione. (ii) Senza la colonna **attesa** avrebbe letto P4 come *"stesso segno di P1 β‡’ rumore"* +(la regola del mandato): falso, P4 e' piatto perche' la sua condizione e' **quasi sempre gia' vera**. +**Lo specchio si legge sulla colonna dell'attesa, non su quella del segno.** + +**Sensibilita':** cassa al 2% invece che 0% riduce la penalita' di ~4 pp e **non cambia segno**; +traguardo su solo-libro (severo) peggiora ancora le condizionali β†’ la convenzione usata e' quella +**favorevole** alle politiche condizionali, scelta apposta. **Non misurato:** politiche che cambiano +il *flusso disponibile* (altra domanda, fuori dal controllo del libro); orizzonti diversi da 25 anni; +e la finestra resta 7,4 anni con due tori. + +πŸ“Œ **ORDINE DI GRANDEZZA, che e' come va usato il risultato: la migliore politica di timing vale +βˆ’0,1 pp di P(20a), dentro il rumore MC di 2,0 pp; passare da €250 a €500 al mese vale +71 pp +(14% β†’ 85%).** + +**VERDETTO: `IL TIMING DEI BONIFICI NON PAGA IN NESSUNA LENTE β€” E IL SEGNALE FUNZIONA, E' LA +CONDIZIONE RARA A COSTARE`** β€” SCARTATO. Chiude una domanda che l'operatore potrebbe altrimenti +riporsi ogni mese per vent'anni. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 43 β€” DATA-UNUSED (i dati che nessuno legge) + +`scripts/research/r0823_data_unused.py`. Indice su **647 file `.py`** in `src/ scripts/ tests/` +(`Old/` escluso = archivio). *"MAI LETTA"* = zero occorrenze **a confini di parola** β€” misura +**generosa verso "letta"**, quindi forte quando dice mai. + +### FASE 1 β€” l'inventario (e' il risultato principale, non un preambolo) + +| dataset | righe | intervallo | letto da | colonne MAI LETTE | +|---|---|---|---|---| +| `btc/eth_{5m,15m,1h}` | 70.335 (1h) | 2018-08β†’oggi | tutto il progetto | β€” | +| `hl_*_1d` (51 alt) | 966 | 2024-01β†’oggi | 17 script | β€” | +| `hlfund_*_1h` (19) | 26.739 | 2023-05β†’2026-06 | 9 (famiglia chiusa 4 lati) | β€” | +| `eq_*_1d` (29 ETF) | 7.539 | 1996-08β†’oggi | 11 | β€” | +| `eqx_*_1d` (12 UCITS) | 1.770 | 2019-05β†’2026-07 | 7 | β€” | +| `fut_*_1h` (7 indici) | 15.070 | 2023-06β†’2026-06 | 4 | β€” | +| `fut_deribit/` + `fund_*` | 8.739 / 64.110 | 2018-09β†’oggi | **2** (entrambi del 22/08) | β€” | +| `dvol_*` | 1.979 | 2021-03β†’oggi | 17 | β€” | +| `vol_term_*` | **59** | 2026-06-26β†’oggi | 3 | `iv_90d` | +| `cb_chain/` | 1.232.212 | 2026-05-01β†’oggi | 14 | `volume_24h` (in 4 file, **tutti collettori/certificatori, nessuno analitico**) | +| `cb_market_snapshots` | 17.406 | 2026-03-26β†’**2026-07-30, morto** | 3 | `fetch_errors_json` | +| `fundnews_short_screen` | **19** | 2026-06-26 | 3 | β€” | +| `alt_sol_*` | 38.898 | 2022-03β†’ | 2 (escluso dall'operatore 22/08) | β€” | +| 🚨 **`external/coinmetrics/cm_{btc,eth}.csv`** | **6.351 / 3.952** | **2009 / 2015β†’2026-05** | **1** | 🚨 **26 colonne su 31** | +| `external/{fng,stables}.json`, `premium/` | β€” | 2015-2018β†’ | 1 ciascuno | β€” (chiusi 24/07) | +| `options_daily/snapshots.jsonl` | 60 snapshot | **30 giorni** | 2 | letto solo per le **specifiche**, mai come serie | +| `instruments_registry.json` | β€” | β€” | **NESSUNO** | (reference, non segnale) | + +πŸ“Œ **Il quadro: ogni dataset con storia vera e' gia' stato analizzato almeno una volta. Cio' che +resta non letto e' telemetria a finestra corta (30-113 giorni) β€” con UNA eccezione.** + +⚠️ **Due difetti dell'inventario stesso, corretti in sessione:** la ricerca a **sottostringa** dava +`iv` in 571 file (e' dentro *drive*, *receive*); e le chiavi derivate dall'etichetta producevano due +falsi **"NESSUNO"** su `eqx_` e `fut_deribit/fund_` β€” cioe' avrebbero **fabbricato** un dato mai +usato che non esiste. + +### FASE 2 β€” `FlowInExNtv`/`FlowOutExNtv`, e perche' proprio quelle + +SceltΠ΅ per: **storia** (100% coperte 2018β†’2026 su entrambi gli asset, contro *mesi* per tutto il +resto), **meccanismo** (il turnover **lordo** misura quanta parte del flottante viene +ri-posizionata), **eseguibilita'** (1d β†’ long-flat/LS su BTC+ETH perp, l'impianto di TP01), +**famiglia non chiusa**. La famiglia exchange-flow **e'** chiusa (24/07, su `SplyExNtv`) β†’ il +meccanismo nuovo e' stato **misurato prima di usarlo**: + +| | BTC | ETH | +|---|---|---| +| corr( d(SplyEx) , **Inβˆ’Out** ) | **+0,9993** | +0,7516 | +| corr( d(SplyEx) , **In+Out** ) | **+0,0349** | +0,0801 | +| \|Inβˆ’Out\| / (In+Out) mediano | **0,060** | 0,062 | + +πŸ“Œ **Il netto E' la derivata dello stock gia' testato; il lordo e' ortogonale β€” e il 94% del flusso +si cancella**, quindi lo stock vede 6 monete su 100 fra quelle che si muovono. Il dato nuovo era +davvero nuovo. + +**Griglia dichiarata prima e contata al rialzo:** 3 variabili (GROSS nuova Β· IMB mista Β· +**NETCTL = controllo negativo, l'informazione gia' uccisa**) Γ— W {30,90,180} Γ— mode {LF,LS} Γ— +sign {Β±1} = **36 celle**, **+4 gia' spese su EXS il 24/07 = 40 trial**. +βœ… **Controllo positivo:** la famiglia uccisa ri-girata con la **sua** factory (`ow.exs_factory` +importata) β†’ `earns_slot_honest=False`, DSR 0,903 = **riproduce il 24/07**. + +**Esito** (`study_family_honest`, cella scelta **solo in-sample**): la cella al buio e' +**`NETCTL W=90 LF +1`, cioe' il CONTROLLO NEGATIVO** β€” IS 1,273 Β· FULL **0,951** Β· HOLD **βˆ’0,094**. +🚨 **Massimo atteso dal PURO RUMORE su 36 trial = Sharpe 1,009: il candidato sta SOTTO.** +DSR **0,437** (N=36) / **0,411** (N=40); marginale **NEUTRAL**, corrβ†’TP01 **0,653**, multi-cut +negativo **7/7 anni**. Per variabile: GROSS **DILUTES** (HOLD βˆ’0,415), IMB **DILUTES**, NETCTL +NEUTRAL β†’ **la parte algebricamente nuova non e' selezionabile**. Direzione: |t|<1 su 2 asset Γ— 2 +variabili. Volatilita': ETH da' t **+2,70** su Pearson ma in **rango** +0,015 **p=0,46**, cambia +segno per anno e su BTC e' "significativo" nel verso sbagliato β†’ **termometro contemporaneo**, come +TERM-STRUCTURE (Β§7) e SKEW (Β§5). `causality_ok` True; `implausible_sharpe` False (48,1% di barre +attive in perdita); **haircut a $635 = 0,000**. Finestra in **barre ATTIVE**: FULL 2.214 (6,06 a) β†’ +SE(Sharpe) 0,41; **HOLD-OUT 511 (1,40 a) β†’ MDE 1,66 di Sharpe**. + +⚠️ **Due errori dell'agente su se stesso, catturati prima di pubblicare.** (a) Il pool *"N=12"* del +deflated-Sharpe erano **le 12 celle MIGLIORI** (`rows` e' ordinata) e faceva **PASSARE** il candidato +a **0,975**; con la partizione legittima fa 0,678 β†’ **conferma diretta della lezione Β§10**: *la +stessa griglia da' verdetti opposti secondo come la si partiziona*. (b) Il residuo di vol calcolato +a mano invece che per OLS **ribaltava il segno** su ETH. + +**Escluso e perche':** `volume_24h`/`book_depth_top3` e 0DTE (113 e 30 giorni, e ogni loro +espressione eseguibile e' short-vol = chiuso); `fut_deribit` (il basis **e'** il funding, Β§13); +`funding_cross_annualized`/`oi_delta_pct_4h` (famiglia chiusa / dataset morto dal 30/07); +Amihud-turnover HL (gia' testato in `xsec_v3_lowrisk`); kimchi, HLP, F&G, MVRV, hash-ribbons, +stablecoin (24/07); `liquidation_*_risk` (una sola categoria in 17.229/17.229); `iv_90d` (59 righe). + +πŸ“Œ **IL PEZZO DI METODO RIUSABILE:** *mettere nella griglia una variabile che si sa gia' morta* +trasforma **"il candidato perde"** in **"il dato non contiene nient'altro di selezionabile"** β€” qui +la selezione in-sample e' **tornata sul controllo negativo**, che e' l'unica forma in cui un +inventario puo' concludere qualcosa di positivo su un'assenza. Sottoprodotto: corrβ†’TP01 0,50-0,65 = +**replica indipendente** del *"l'on-chain tradabile e' prezzo travestito"* del 24/07, su colonne che +quell'ondata non aveva mai aperto. + +**VERDETTO: `NESSUN DATO INUTILIZZATO SOSTIENE OGGI UN'IPOTESI NUOVA β€” LA SCORTA DI DATI DEL +PROGETTO E' ESAURITA, E CIO' CHE RESTA NON LETTO E' TELEMETRIA A FINESTRA CORTA, NON STORIA`** β€” +SCARTATO. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 45 β€” SPOT-NETTING (separare le gambe su due strumenti) + +`scripts/research/r0823_spot_netting.py`. **Sola lettura provata:** `git status` mostra solo questo +file, `git status -- src/ config/ scripts/live/ scripts/cron_*.sh tests/ data/` **vuoto**; ~17 GET +pubbliche + ~13 letture del conto via `DeribitRead` (**nessun metodo di trading esiste in quella +classe, by design**); **ordini inviati: 0**; finestre :05-:10 e :24-:30 rispettate da una guardia +nello script. + +### Q1 β€” il margine non uccide il lead; **il codice si'** + +**[VENUE]** `get_currencies`: BTC ed ETH hanno `in_cross_collateral_pool: true` (**11 valute su 51** +β†’ elenco ristretto, non un default). `get_instruments?kind=spot`: `BTC_USDC`/`ETH_USDC` hanno +**`max_leverage: 10`**, taker **0,00 bps**, min lotto 0,0001 β†’ **[DEDOTTO]** lo spot vive dentro il +conto marginato, non in un portafoglio segregato. +**[CONTO]** equity USDC **$636,01**, nozionale lordo $327,52, **margine impegnato $6,55 = 2,000% del +nozionale** (limite superiore: contiene eventuali riserve d'ordine β†’ l'aritmetica che segue e' +conservativa). +**[CALC] caso peggiore** (TP01 al massimo su entrambi gli asset): spot $477, USDC libero **$159**, +nozionale perp di SKH01 $159, margine richiesto $3,18 β†’ **copertura 50,0x**. Entrambi i termini sono +proporzionali a E β†’ **il rapporto non dipende dal capitale**. Anche con lo spot valutato **zero** +come collaterale, SKH01 resta marginato 50 volte. **L'haircut β€” non leggibile β€” non e' binding.** + +🚨 **[CODICE, verificato con `inspect.getsource`]** `src/live/shadow.py::_equity` legge **solo** +`account_summary("USDC")` e ripiega sulle valute-moneta solo se quell'equity e' ≀1; `position_usd` +matcha per `instrument_name`. β†’ comprando $477 di spot il libro **leggerebbe $159 invece di $636 +(25%)** e non vedrebbe la gamba, che il riconciliatore **ricomprerebbe**. **Non e' un cambio di +strumento: e' un cambio del percorso di SIZING del live.** + +❌ **Ipotesi dell'agente, nata e refutata nello stesso filone.** `get_currencies` dichiara USDC +`apr` **3,40** (USDT/BTC/ETH 0,0): se accreditato, tenere il 75% in spot costerebbe fino a +**2,55%/anno = meta' del vantaggio**. **[LOG]** 6 run col conto **flat** (il piu' lungo **257 giri +orari = 10,7 giorni**): equity **invariata al centesimo** contro i +$0,59 attesi β†’ limite superiore +su qualunque interesse **< 0,029%/anno**, e un accredito mensile e' escluso (un run attraversa il +31/07-01/08 con Ξ”=0,00). **L'obiezione e' morta e il lead non deve pagarla.** + +### Q2 β€” il valore della separazione: **nullo** + +🚨 **(i) T1 sbloccato e' un DECLASSAMENTO.** Con la macchineria originale (`r0726_skh_onbook`, 23 +offset, differenze appaiate): `onbook_tp βˆ’ hourly` = **+0,051 FULL (19/23) / +0,057 HOLD** β†’ il +pubblicato (+0,054/+0,061) e' **riprodotto**. Ma il 26/07 stesso stabili' che il live fa rilevazione +**intra-barra**, e `fastdetect βˆ’ hourly` = **+0,082 in 23/23** (pubblicato +0,081). Contro il path +che il live **gia' percorre**: **`onbook_tp βˆ’ fastdetect` = βˆ’0,048 FULL (6/23)**. *Sbloccare T1 +comprerebbe un path peggiore di quello che gira* β€” conferma indipendente della conclusione del 26/07, +raggiunta per una strada diversa. + +**(ii) Funding risparmiato** [riprodotto da Β§35/Β§39]: TP01 su perp drift 13,66% β†’ senza carry 15,77% +(**+2,11%/anno di sleeve**, vol invariata 12,17β†’12,22%); a peso di libro Γ—0,75 β†’ **+1,58%/anno**. + +πŸ“Œ **(iii) Il costo del netting perso: Β§39 lo stimava "1-2%", ed e' ALTO DI UN ORDINE DI +GRANDEZZA.** Simulazione della riconciliazione **oraria** su 8 anni, percorso SKH01 **validato +contro l'harness** (trade 194/194 BTC, 213/213 ETH): + +| percorso | ordini/anno | turnover perp | turnover spot | lordo medio | +|---|---|---|---|---| +| NETTA | 289,1 | $12.412 | $0 | $84,52 | +| SEPARATA | 282,8 | $8.493 | $3.893 | $85,52 | + +**Commissioni: NETTA $4,34/anno (0,683% di equity) contro SEPARATA $2,97 β€” un RISPARMIO di +0,216%.** **Il netting non risparmia commissioni.** Ordini βˆ’6,3/anno, ma ⚠️ **meno ordini non e' +meglio**: il tracking peggiora del **+12%** ($1,61 β†’ $1,81 di scarto medio dal target) β€” **5Βͺ +occorrenza** della lezione *"banda in valuta assoluta"*. Margine: irrilevante (IM 0,266% di equity). +**Segni opposti β€” cioe' le ore in cui il netting serve davvero β€” 0,61% della storia.** +**[VENUE] Il costo vero e' lo SPREAD:** il perp e' un mercato a **un tick** (0,01-0,02 bps di mezzo +spread), lo spot no (1,5-3,1 bps) β†’ sovracosto **[0,139%, 0,250%]/anno**. ⚠️ **Due istantanee a 4 +minuti danno 2,27 e 4,08 bps (+79%) e il contributo netto dell'esecuzione CAMBIA SEGNO fra le due** +β†’ si cita come banda **[βˆ’0,034%, +0,077%]/anno**, ampiezza = 7% del lead: **l'esecuzione non decide +questo lead**. βœ… **Controllo positivo:** ordini simulati sulla finestra live **132/anno** contro +**114/anno osservati**. ⚠️ **[LOG] In 2 mesi SKH01 non e' MAI stato short** β†’ il campione live **non +puo'** misurare il netting, e l'agente lo dichiara invece di estrapolarlo. Fee reale sul nastro +**3,70 bps/lato**. + +**Il numero da citare:** lordo **+1,58%** β†’ **netto oggi [+1,55%, +1,66%]** β†’ con spot a 3,50 bps +[+1,33%, +1,44%] β†’ **nello scenario fiscale sfavorevole +0,50%**. + +### Q3 β€” la sorveglianza + +**[CODICE]** `fee_watch` deriva `INSTRUMENTS` da `BOOK_INSTRUMENT` β†’ una gamba spot **non +comparirebbe**: e' esattamente il difetto corretto il 21/08. Riga **dichiarata e non scritta**: +`SPOT_INSTRUMENT` in `src/live/book.py` + concatenazione in `fee_watch`. ⚠️ **Non basta:** +`convenzione('BTC_USDC')` ritorna **`'ignota'`** β†’ il cross-check sui fill tornerebbe `None` +(onesto ma cieco): servirebbe una **terza famiglia 'spot'**. Se la fee tornasse a 3,50 bps il lead +perderebbe 0,216%/anno = 14% del guadagno β†’ **sopravviverebbe**; *la ragione per sorvegliarlo non e' +che lo ucciderebbe, e' che oggi nessuno se ne accorgerebbe*. + +### Q4 β€” il rischio + +**Venue:** invariato (era gia' totale). **Fisco:** 7 punti di aliquota sulla gamba TP01 = **0,83%/anno += oltre meta' del guadagno** β€” e 🚨 **un costo nuovo mai contato: se i derivati sono `c-quater` i +comparti si SEPARANO**, quindi le minusvalenze del perp (SKH01) smetterebbero di compensare le +plusvalenze spot (TP01) **[DEDOTTO, per il commercialista]**. **Liquidazione β€” qui la separazione +MIGLIORA:** lo spot non si liquida, esce dal perimetro il 75% del libro ($85 β†’ ~$21 di nozionale +liquidabile); **contropartita:** il disaster-SL oggi protegge la posizione **netta**, separando +resterebbe solo sul perp e in un blackout **TP01 resterebbe long al 75% del conto senza stop**. +**Haircut:** non leggibile e **non binding** β€” *unico caso in cui "non lo so" e "non conta" +coincidono, e va detto in quest'ordine*. + +### Non determinabile senza un conto reale +(i) l'haircut del collaterale (`get_collateral_info` β†’ *Method not found*); (ii) se +`account_summary('USDC').equity` **includa** i saldi BTC/ETH (il conto ne tiene $0,19: troppo poco +per discriminare); (iii) se `get_positions` restituisca lo spot. **Tutte e tre si risolvono con un +acquisto di prova da $7,73** leggendo equity/available/positions prima e dopo β€” **e' un ordine, +quindi decisione dell'operatore.** + +**VERDETTO: `IL LEAD REGGE E VALE [+1,55%, +1,66%]/ANNO NETTO, MA LA SEPARAZIONE DELLE GAMBE β€” LA +RAGIONE PER CUI IL FILONE E' STATO APERTO β€” NON AGGIUNGE NULLA`**: T1 sbloccato e' un declassamento, +il netting perso risparmia commissioni invece di costarle, il margine non era il vincolo. **Il valore +e' il funding e basta**, e in cambio si prendono due costi nuovi mai contati (comparti fiscali +separati, gamba TP01 senza disaster-SL) piu' un cambio al percorso di sizing del live. +πŸ“Œ **Il prossimo passo non e' un backtest: e' la domanda al commercialista e un acquisto di prova da +$7,73.** **Libro, pesi, cron, config INVARIATI.** + +--- + +## 51 β€” NEWDATA-SCOUT (cosa iniziare a raccogliere) + +`scripts/research/r0823_newdata_scout.py`. Conseguenza diretta di Β§43: se la scorta e' esaurita, la +domanda utile non e' piu' *"cosa abbiamo e non guardiamo"* ma *"cosa NON abbiamo, che varrebbe la +pena iniziare a raccogliere oggi"* β€” perche' **una catena opzioni non e' ricostruibile a posteriori** +e un'ora non raccolta e' persa per sempre. + +| fonte | apre | ricostruibile? | costo | tempo utile | verdetto | +|---|---|---|---|---|---| +| **catena opzioni USDC-lineare** | nessuna famiglia nuova: **rende onesti i numeri di uno sleeve VIVO** (VRP01, 12%) | **NO** | **+117 chiam./giro = +18%** | episodi (1 regime IV-rank>0,30) | **RACCOGLIERE** | +| **book depth L2 perpetual** | la curva haircut-vs-taglia (oggi **18 fill**) | **NO** | 2.880/g a 1/minΓ—2 | giorni per la mediana, anni per la coda | **CAMPAGNA A TERMINE** | +| trade tape / flusso aggressore | flusso d'ordine (mai aperto) | **NO** β€” muro **βˆ’24h/βˆ’26h** | ~6/ora | **6,0 anni** | NON RACCOGLIERE | +| liquidazioni Deribit | coda/stress | NO (vive nel tape) | β€” | non calcolabile | NON RACCOGLIERE | +| OI per strike nel tempo | β€” | irrilevante | zero | in corso | **GIA' FATTO** | +| **OI perpetual** (il collettore raccomandato da Β§10) | affollamento β€” famiglia **CHIUSA 4 lati** | 🚨 **SI', >=800 giorni** | 1/ora | testabile **oggi** | **NON RACCOGLIERE β€” TESTARE PRIMA** | +| telemetria Binance (taker, long/short) | posizionamento retail | **NO** β€” muro 30g | ~24/g | 6,0 anni | NON RACCOGLIERE | +| funding/basis altri venue Β· DVOL Β· macro | famiglie chiuse / gia' posseduto | SI' | β€” | β€” | NON RACCOGLIERE | + +πŸ“Œ **Il criterio 1 (ricostruibilita') ne uccide 4 da solo, ed e' stato applicato PROVANDO +l'endpoint, non deducendolo** β€” ogni verdetto e' una GET con **controllo positivo**, perche' *uno +zero non e' una risposta finche' non si prova che la stessa chiamata funziona altrove*: +tape Deribit **βˆ’24h β†’ 100 trade, βˆ’26h β†’ 0** (muro verificato coi timestamp restituiti); Binance +**βˆ’20g risponde, βˆ’40g rifiuta esplicitamente** (`parameter 'startTime' is invalid`); funding e DVOL +serviti storici su richiesta. +🚨 **E ribalta una raccomandazione del 22/08:** Β§10 proponeva di accendere un collettore di OI +perpetual *"perche' oggi ripartirebbe da zero"*. **Non riparte da zero:** Bybit risponde a βˆ’800 +giorni coi timestamp dentro la finestra (2024-06-14). **L'ipotesi e' testabile oggi a costo zero**, e +solo se sopravvive vale pagare la specificita' di venue. + +🚨 **Correzione a un numero che avevo pubblicato io (+90%).** Con **gli stessi filtri del +collettore vivo** (≀95 giorni, OIβ‰₯100) la catena USDC BTC+ETH e' di **113 strumenti**, non ~587 β†’ +**+18%, non +90%**. βœ… **Verificato indipendentemente dal coordinatore sul venue**: inverse +(BTC 270 + ETH 379) = **649**, USDC = **113** β†’ **+17,4%**. L'origine del 587 era il conteggio +**grezzo** senza il filtro OI (1.140 strumenti USDC ≀95g, cioe' +81% β€” vicino al 90% pubblicato): +**il filtro che il collettore applica davvero taglia il 90% della famiglia USDC**, ed e' la replica +esatta di Β§8 (*OI e negoziabilita' sono anti-correlati su Deribit USDC*; BTC_USDC ha **5** +strumenti sopra soglia). Il giro passerebbe da **652 chiamate / 163 s** a **769 / 192 s**, che +finisce prima del :30 e non tocca `cron_book` al :07. ⚠️ E' un numero **di oggi**: cresce con la +liquidita' USDC e va ri-misurato prima di accendere. + +**Tempo perche' sia utile, dal MDE** (`β‰ˆ 1,96/√anni`): Sharpe 0,8 β†’ **6,0 anni**; 1,0 β†’ 3,8; 1,3 β†’ +2,3. **Il soffitto direzionale del progetto e' ~1,15-1,3 β‡’ qualunque fonte che alimenti un SEGNALE +non e' una decisione di oggi.** πŸ“Œ **Le due sopravvissute sfuggono al MDE perche' non producono uno +Sharpe: sono misure di COSTO (depth) e di PREZZO REALE (catena USDC).** + +⚠️ **Onesta' dell'agente su cio' che le due fonti NON fanno:** la catena USDC **non apre una famiglia +chiusa** β€” vale perche' il `f=0,73` di VRP01 e' misurato sulla famiglia **inverse, che un conto USDC +non puo' marginare**: ripara il numero di uno sleeve vivo, non apre alpha. Il depth: la domanda +binding (*quanto costa eseguire a $5k/$20k*) si risponde in **due settimane**; solo la coda di stress +richiede l'archivio β€” da cui **"campagna", non "collettore"**. + +⚠️ **Bug catturato in sessione:** `urlopen` **solleva** su HTTP 400, quindi il rifiuto di Binance +veniva ingoiato come guasto di rete e il muro **si ribaltava in silenzio in "RICOSTRUIBILE"** β€” la +stessa conflazione *errore / assenza-di-dato* gia' codificata in `collect_chain` con `quote_status`. + +**VERDETTO: `RACCOGLIERE DUE FONTI SU DIECI, E NESSUNA DELLE DUE PER FARNE UN SEGNALE`** β€” catena +USDC (+18%) e una campagna a termine di book depth. **Nessuna azione presa: e' materiale per una +decisione dell'operatore.** + +--- + +## 48 β€” VOLVOL (il secondo momento del DVOL) + +`scripts/research/r0823_volvol.py` (**20 s**, nessuna rete, nessuna scrittura). **36 celle** +dichiarate prima (3 finestre Γ— 3 soglie Γ— 4 usi), tenute poche di proposito data l'aritmetica dei +168 trial. + +βœ… **L'ortogonalita' β€” che era il gate β€” la PASSA**, e questa e' meta' dell'attesa a priori +**refutata**: misurata prima di costruire qualsiasi strategia, Pearson **e** rango, 6 celle Γ— 6 +referenze, **max |corr| = 0,396** (soglia 0,80): contro il livello DVOL 0,13-0,21, contro l'IV-rank +di VRP01 0,01-0,24, contro RV30 0,09-0,38, contro |esposizione TP01| 0,03-0,30. **E' una quinta +variabile, non una quarta riscritta.** + +⚠️ **Correzione a una mia riga del briefing, fatta leggendo il codice in produzione:** il +**DVOLSPREAD in forward-monitor NON e' implicitaβˆ’realizzata** β€” `r0726_dvolspread_gate.make_book` +calcola **`log(dvol_btc) βˆ’ log(dvol_eth)`**, un relative-value **cross-asset** (βœ… verificato dal +coordinatore al sorgente, riga 67). L'agente ha misurato contro **entrambe**: *correlare contro +quella sbagliata avrebbe dato un "ortogonale" gratuito contro una variabile che nessuno trada.* + +πŸ“Œ **DOVE MUORE: guarda INDIETRO.** `corr(VoV_t, |r|_{t+k})` su BTC: **βˆ’10: +0,164 Β· βˆ’5: +0,169 +Β· βˆ’1: +0,132 Β· 0: +0,113 Β· +1: +0,057 Β· +10: +0,018** β€” picco a lag **βˆ’5/βˆ’10**, curva monotona +decrescente. **Non e' il termometro contemporaneo** di TERM-STRUCTURE e SKEW: **insegue con una +settimana di ritardo**. Correlazione **parziale** con la vol realizzata futura a 10g, al netto di +`RV_t` e `DVOL_t`: **βˆ’0,069 (BTC) / βˆ’0,090 (ETH)**. βœ… Controllo positivo del rilevatore **2/2 nei +due versi** (lead sintetico β†’ +3, lag sintetico β†’ βˆ’3). + +**Gate:** cella in-sample-only SIZE w=10 p=0,50, marginal **NEUTRAL**, **DSR 0,448 FAIL** +(0,441/0,381/0,228 a N=36/72/168) β†’ `earns_slot_honest=False`. **La trappola del trend in chiaro:** +la cella scelta ha **corrβ†’TP01 0,995 sull'hold-out**; e per uso, RISKOFF/SIZE ereditano lo Sharpe +del trend (mediana IS 0,40/0,66) mentre **DIR β€” l'unico uso in cui la variabile decide da sola β€” ha +mediana IS βˆ’0,24 e 5/9 celle con FULL negativo**. +**Arm VRP01** (replica del sleeve **bit-exact 2/2** prima di ogni delta): 5/9 battono il canonico = +moneta, maxDD giu' in **9/9 = de-levering**, e contro gate **casuali** che saltano lo stesso numero +di settimane la mediana e' 0,71/0,72 con **0/9 al 95Β° pctl** β†’ **quinto fallimento consecutivo di un +gate nuovo su VRP01**. + +πŸ“Œ **IL NUMERO CHE DECIDE, e non e' un gate.** Barre **attive**: FULL 985 β†’ **MDE 1,51**; +hold-out 303 β†’ **MDE 2,73**. Il candidato migliore batte TP01 nudo di **+0,034 di Sharpe β€” un +fattore 44 sotto l'MDE**: *su questo dato la domanda non e' rispondibile in positivo nemmeno in +linea di principio*. A chiudere sono le due misure che **hanno** potenza e **non dipendono da nessuna +cella scelta**: la parziale negativa, e il **controllo NON CAUSALE** β€” la stessa cella con la +vol-of-vol che **sbircia** fa **βˆ’0,292 sotto TP01**. *Non e' che la si stima male: la variabile non +contiene l'informazione.* + +⚠️ **Errori dell'agente su se stesso:** (i) la lettura dell'ortogonalita' era **cablata** e diceva +*"il legame piu' forte e' con la vol realizzata"* mentre la tabella diceva SPREAD β†’ ora e' calcolata +dalla tabella; (ii) il null del gate casuale estraeva le settimane **indipendentemente per gamba** +mentre il gate vero e' guidato da due DVOL correlati β€” differenza risultata **immateriale** (0,71 +contro 0,72) *ma andava misurata, non assunta* (nel 25/07 lo stesso errore valeva 2-3Γ—); +(iii) un off-by-one nel conteggio. + +**Non misurato, dichiarato:** vol-of-vol **intra-giornaliera** (il DVOL su disco e' 1d, e un secondo +momento e' proprio dove la frequenza puo' contare); vol-of-vol **implicita dalla curvatura dello +smile**; un modello **congiunto** RV+livello+VoV invece di un gate binario. *"Un uso non provato non +e' un uso refutato."* + +**VERDETTO: `E' UNA VARIABILE NUOVA E GUARDA INDIETRO`** β€” SCARTATO, **quinto lato del filone DVOL +chiuso**. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 49 β€” TP01-TWIN (il 75% del libro dipende da una sola definizione di trend) + +`scripts/research/r0823_tp01_twin.py` (**8 s**). βœ… Replica bit-exact vs `sleeves._tp01_returns()`: +**max|diff| = 0.0**. **53 trial** contati al rialzo (22 celle + 31 composizioni). + +❌ **L'ATTESA A PRIORI DEL COORDINATORE E' REFUTATA, ed e' il motivo per cui il filone valeva.** +Avevo scritto *"i meccanismi di trend a 1d correlano 0,85-0,95, l'ensemble sara' TP01 con piu' fee"*. +Misurato β€” rendimenti netti / **posizioni-bersaglio** (dove vive davvero la ridondanza): + +| | TSMOM | DON | EWMA | KEL | KAU | +|---|---|---|---|---|---| +| **TSMOM** | 1.000 | 0.852 / **0.700** | 0.929 / **0.859** | 0.714 / **0.510** | 0.767 / **0.527** | +| DON | | 1.000 | 0.892 / 0.775 | 0.696 / 0.464 | 0.759 / 0.517 | +| EWMA | | | 1.000 | 0.696 / 0.440 | 0.791 / 0.528 | +| KEL | | | | 1.000 | 0.855 / 0.758 | + +Media fuori diagonale **0,795 (rendimenti) / 0,608 (posizioni)**. Keltner e Kaufman stanno a +**0,51-0,53** di posizione da TSMOM: **meccanismi genuinamente diversi, non TP01 travestito** β†’ il +filone **non si chiudeva sulla matrice** ed e' stato misurato fino in fondo. + +🚨 **LA PROTEZIONE β€” il criterio (B) di `edge_watch.py`, importato dal sorgente di produzione.** +La colonna che decide non e' l'ancora canonica ma la frazione di ancore: + +| anno | DD b&h | rapp. TSMOM | rapp. ENS | ancore in cui ENS e' meglio | +|---|---|---|---|---| +| 2019 | 56,4% | 0,170 | 0,172 | 14/24 | +| 2020 | 59,2% | 0,124 | 0,114 | 18/24 | +| 2021 | 52,0% | 0,130 | 0,121 | 18/24 | +| **2022** | **68,4%** | **0,039** | **0,129** | **0/24** | +| 2023 | 22,0% | 0,531 | 0,486 | 23/24 | +| **2024** | 35,6% | 0,222 | 0,345 | **0/24** | +| **2025** | 45,1% | 0,166 | 0,209 | **0/24** | +| **2026** | 46,6% | 0,025 | 0,108 | **0/24** | + +**Dove l'ensemble perde, perde a TUTTE le ancore** β€” degradazione unanime, non fortuna d'ancora β€” e +i quattro anni in cui perde sono i **quattro piu' recenti**. Nel **2022, il sinistro maggiore, e' +3,2x peggiore**. Rapporto **medio** 0,18 β†’ 0,21 (**0/24** favorevoli); celle ancoraΓ—anno a favore +**73/192 = 38%**. + +⚠️ **Errore dell'agente catturato in sessione, e del tipo che avrebbe cambiato il verdetto:** la +prima stesura decideva sul **caso peggiore** (max degli 8 rapporti), che migliora 0,56 β†’ 0,49, e +avrebbe stampato **"PROMOSSO A LEAD"**. *Il massimo di 8 numeri non e' una statistica*: si era mosso +perche' era migliorato **un** anno (il 2023, che deteneva il massimo) mentre **4 su 8** peggioravano. + +πŸ“Œ **Il null del de-levering non e' "superato": e' SENZA POTENZA, e va detto cosi'.** +`k_isovol = 0,9896 β‰ˆ 1` β€” **non c'e' niente da de-levere**: vol 12,14% contro 12,02%, esposizione +media ENS **0,152 > 0,141**, tempo a mercato **70% > 55%**. βœ… E il rilevatore **ha** potenza: su un +de-levering vero (`target_vol=10%`) segnala Sharpe invariato 1,305 e maxDD 14,3% β†’ 7,3%. +Aggiunto un gate su 31 sottoinsiemi: **0/30 composizioni alternative** batte TSMOM da solo su +protezione **E** hold-out insieme. + +**Banda d'ancora (24, differenze APPAIATE):** Sharpe FULL +0,0177 (14/24) Β· **Sharpe HOLD-OUT +βˆ’0,0671 (7/24)** Β· maxDD +0,0164 (**2/24**) Β· peggior 3 mesi **+0,0113 (24/24)** Β· protezione media +**+0,0305 (0/24)**. **Sul libro 75/25** (la gamba SKH e' identica nei due bracci β†’ la sua ancora si +cancella): **Sharpe hold-out Ξ” βˆ’0,127, favorevole 3/24**; FULL +0,0001; maxDD βˆ’0,004; peggior 3 mesi ++0,014 (24/24). +βœ… **Onesta' verso l'ensemble:** l'ancora canonica h=0 e' la **migliore delle 24** per l'hold-out di +TSMOM (**100Β° pctl** β€” replica indipendente della fortuna d'ancora del 02/07 e 26/07), quindi il +divario a h=0 (βˆ’0,37) e' **gonfiato**: il numero da citare e' **βˆ’0,067**. + +**I costi non sono il problema:** turnover **5,3/anno contro 8,0** di TSMOM (la nettatura cancella +turnover), a 0,20% RT 1,252 contro 1,274, haircut a $635 **βˆ’0,002**. `study_family_honest`: cella +in-sample-only = **TSMOM**, marginale **REDUNDANT**, `earns_slot_honest=False` β€” con DSR 0,999 +**dichiarato non decisivo** (famiglia omogenea β‡’ sr0 piccolo per costruzione). + +πŸ“Œ **E il criterio che si voleva migliorare NON E' BINDING:** TSMOM ha rapporto peggiore **0,635 +contro la soglia 0,75 in 192 celle su 192**. `weights_tilt_null` non si applica (il vettore dei pesi +e' identico, cambia il contenuto del 75%) ed e' detto esplicitamente nello script. + +πŸ“Œ **IL FATTO TRASFERIBILE: mediare meccanismi di trend NON diversifica il rischio di trend.** +Cinque segnali guidati dallo **stesso prezzo** divergono solo ai **bordi** del trend β€” cioe' nei +ribassi β€” e la media li tiene **mezzi-lunghi mentre il singolo e' gia' flat**. Piu' meccanismi = +ingresso/uscita piu' morbidi (peggior 3 mesi migliore **24/24**), **non** piu' assicurazione. +⚠️ **Cio' che NON si conclude:** che la monocultura sia sicura. Servirebbe un regime in cui il TSMOM +fallisce, e in 7,4 anni non c'e'. + +**VERDETTO: `MEDIARE MECCANISMI DI TREND NON DIVERSIFICA IL RISCHIO DI TREND β€” PROTEGGE PEGGIO +PROPRIO NEI RIBASSI`** β€” REFUTED. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 46 β€” TAIL-HEDGE (put deep-OTM come assicurazione statica) + +`scripts/research/r0823_tail_hedge.py`. **Libro, pesi, cron, config INVARIATI. Nessun ordine.** + +🚨 **Il null del de-levering non arriva nemmeno a essere il gate che decide: la premessa cade +prima.** + +| lente di f | maxDD scende | batte il de-levering | miglior Ξ”drift | Ξ”Sharpe | +|---|---|---|---|---| +| **f=1,00** (modello nudo, **regalato**) | **0/54** | 0/54 | +0,07 pp/a | βˆ’0,108 | +| f=2,23 (misura 30/07) | **0/54** | 0/54 | βˆ’0,00 pp/a | βˆ’0,004 | +| f=5,85 (misura 30/07) | **0/54** | 0/54 | +0,02 pp/a | βˆ’0,032 | +| **f misurato oggi** (1,89/1,27/1,06) | **0/54** | 0/54 | βˆ’0,01 pp/a | βˆ’0,004 | + +Libro nudo (2021-03β†’oggi, finestra DVOL): Sharpe 1,356 / maxDD **9,42%** / drift 13,75%. Coperto: +maxDD **9,53-10,37%**. **Non serve trovare il `k`**: il claim *"meno drawdown"* e' falso al primo +passo, e dove il DD **peggiora** il null e' degenere (k=1) β€” un Ξ”drift>0 li' risponde a una domanda +di *alpha*, non a questa. + +πŸ“Œ **IL MECCANISMO, ed e' il risultato trasferibile: il beta del libro al sottostante e' +0,076.** +Il 19/05/2021 il mercato ha fatto **βˆ’21,1%** e il libro **βˆ’2,0%**. *La put protegge una perdita gia' +ridotta ~10x dalla strategia stessa*, quindi coprirla costerebbe ~1/beta volte il budget. E il bleed +del premio cade **dentro** i drawdown β€” che su questo libro sono **lunghi e poco profondi** β€” mentre +il payoff cade su singoli giorni di crollo **che non sono il fondo del drawdown**: sui 5 peggiori +mesi la copertura peggiora **tutti e cinque**. +❌ **Meta' dell'attesa a priori dell'agente refutata:** 15/20 dei peggiori giorni del libro **sono** +crolli (lo short-squeeze di SKH01 sta fuori finestra). Regge l'altra meta', ed e' il beta. + +**f misurato oggi sulle quote vere** (420 put con bid **E** ask, catena USDC): Ξ΄βˆ’0,05 β†’ **1,89** Β· +Ξ΄βˆ’0,10 β†’ **1,27** Β· Ξ΄βˆ’0,15 β†’ **1,06** β€” piu' mite del 2,23-5,85 del 30/07, **e il verdetto non +cambia**. πŸ“Œ Struttura del modello che vale come risultato a se': **f si paga solo sulla parte di +valore che converge a intrinseco**, quindi un roll anticipato non lo paga (asimmetria misurata: +expiry βˆ’0,27pp invariato, half βˆ’0,19 β†’ βˆ’0,39pp). + +🚨 **CORREGGE UN MURO PUBBLICATO: comprare un'opzione costa il PREMIO, non il NOZIONALE.** I +*"lotti da migliaia di dollari"* del 22/08 sono il **collaterale per VENDERE**. Lotto minimo +BTC_USDC = **$1,79-7,93** di premio; la copertura e' comprabile a ogni roll da **~$9,2k** (30g +Ξ΄βˆ’0,05) a **~$38,8k** (7g Ξ΄βˆ’0,15). βœ… **Verificato al venue dal coordinatore:** `min_trade_amount` +**0,01** su tutte le 343 put BTC_USDC, e il premio del lotto minimo di una put a 5 giorni va da +**$0,00** a qualche dollaro. +πŸ“Œ **Il secondo muro e' strutturale e non di capitale: il TICK da 5 USDC.** βœ… Verificato +(`tick_size = 5.0` su tutte). **Piu' la copertura e' deep-OTM β€” cioe' economica β€” piu' il tick +domina**: una put marcata 0,82 USDC non e' quotabile sotto 5. E' un pavimento di prezzo del **6x** +sulle ali piu' lontane, e nessun budget lo aggira. + +**Canale funded:** sulla regola binding (max-loss 6% **statico**) la copertura va **nel verso +sbagliato** β€” alza il maxDD, quindi **abbassa** la leva ammessa (k 0,629 β†’ 0,594-0,620). Il +beneficio sui breach a un giorno (0,92 β†’ 0,55/anno) e' reale, ma il de-levering **a pari drift** lo +compra allo stesso ordine di grandezza **senza toccare il maxDD**. + +⚠️ **Tre errori dell'agente, tutti catturati dai controlli e non a occhio.** (i) Il controllo +positivo *"payoff gratis"* **falliva**: non accreditava il valore dell'asset regalato mentre il MTM +ne addebitava il decadimento β†’ il free lunch costava quanto il premio. **Un controllo positivo rotto +dichiara guasto l'apparato.** (ii) La causalita' dava 1,28e-04 ("DIVERGE") perche' il confronto +arrivava fino al taglio mentre il prefisso si ferma **un roll prima**: differiva per **assenza di un +roll**, non per look-ahead β†’ finestra corretta, **0,00e+00 esatto**. (iii) Errore di **criterio**: +contava "vittoria" anche dove il maxDD peggiora β€” col criterio corretto le *"3/54 vittorie a f=1"* +diventano **0/54**. Controlli finali **3/3** (free-lunch e f=0,1 riconosciuti, f=10 rifiutato). + +⚠️ **Non misurato, dichiarato:** **marzo 2020 e' FUORI CAMPIONE** (il DVOL parte 2021-03) β€” *il +sinistro piu' grande della storia del libro non e' testato, ed e' l'unico scenario in cui la +conclusione potrebbe muoversi*. Lo spread viene dai soli strumenti con bid **E** ask veri: e' un +**pavimento che FAVORISCE la copertura** (le ali piu' profonde spesso hanno solo un ask al tick). I +3-5 breach in 5,4 anni **non distinguono le varianti fra loro**: si legge il segno, non la taglia. +Nessuna copertura **dinamica** (gated su regime) e' stata provata: la domanda era la statica. + +**VERDETTO: `LA PUT PROTEGGE UNA PERDITA GIA' RIDOTTA 10x DALLA STRATEGIA β€” IL maxDD SALE IN 162 +CELLE SU 162`** β€” REFUTATO. + +--- + +## 47 β€” ANCHOR-ENSEMBLE (de-luckare l'ESECUZIONE invece del numero) + +`scripts/research/r0823_anchor_ensemble.py` (**127 s**). Attese a priori **A1-A7 scritte e stampate +prima** di misurare: **3 confermate, 3 refutate, 1 mezza**. **Libro, pesi, ancore, cron, config +INVARIATI.** + +**(i) Batte l'ancora MEDIANA: si', di +0,014 di Sharpe FULL β€” e l'algebra dice che non puo' fare di +piu'.** La posizione dell'ensemble e' la **media** delle posizioni e il lordo orario e' lineare in +essa β‡’ **`lordo_ens == media(lordi)`, verificato a max|dif| 1,4e-17**. Il drift non puo' cambiare se +non per fee-netting (**+0,013%**): **tutto il guadagno e' vol (βˆ’1,1%)**. +πŸ“Œ **Cio' che l'ensemble compra davvero non sta nella colonna Sharpe: `sd(ShFULL) 0,061 β†’ 0` e +`sd(ShHOLD) 0,112 β†’ 0`. Non riduce l'incertezza, la ELIMINA.** +⚠️ **A2 refutata sulla taglia:** l'agente prevedeva +0,03…+0,15 ed e' **7x piu' piccolo** β€” aveva +sopravvalutato quanto sono decorrelate 24 ancore della **stessa** strategia. + +**(ii) Batte la CANONICA: NO sull'hold-out (βˆ’0,223), SI' su FULL (+0,024) e in-sample (+0,071).** +La canonica sta al **98Β° pctl di 24 sull'hold-out e al 15Β° in-sample**. +πŸ“Œ **E i due numeri non sono due stime della stessa cosa: +0,44 e' un'estrazione GIA' AVVENUTA, ++0,21 e' cio' che si ottiene senza estrarre.** Tenere la canonica non conserva il +0,44: **ri-estrae** +da [+0,02, +0,44]. **La stima onesta del futuro e' +0,21.** + +**(iii) Eseguibile da quale capitale: da TUTTI, gia' a $635 β€” attesa REFUTATA.** A $635 il path +eseguito a K=24 segue l'ideale **meglio** che a K=1 (**corr 0,9986 contro 0,9954**), perche' *un +target che si muove a passi piccoli e frequenti entra in una banda fissa meglio di uno che salta*. +Ordini 119 β†’ 729/anno, fee **$2,55 β†’ $2,56**. ⚠️ Il criterio `N_max` dichiarato in anticipo +**satura (6/6 celle a ogni capitale)**: *un gate che passa sempre non misura niente* β†’ la risposta +non e' un capitale, e' che **l'eseguibilita' non e' il vincolo (7Βͺ volta nell'ondata)**. + +βœ… **Replica del 02/07 prima di ogni delta:** `h=0 == tp01_baseline_daily`; **`banded()` == +`r07.smallcap_net` bit-exact (0,0e+00, stessi ordini)**; **K=4 == EW di 4 book** (2,2e-16); **offset +0 == `sleeves._skyhook_returns()`** (0,0e+00). La **struttura** replica; i **livelli** sono +ri-derivati col dato di oggi (`data/raw/` non versionato): HOLD mediano 0,04 β†’ **0,220**, banda +[βˆ’0,13,+0,30] β†’ **[+0,02,+0,44]**, canonico 0,31 β†’ **0,436 (98Β° pctl in entrambi)** β€” **coincidono +con la memoria corrente del progetto** (canonico +0,44 al 96-98Β°, mediana +0,22). + +πŸ“Œ **Il funding: canale chiuso per ALGEBRA, non per piccolezza.** `funding = posΒ·f` e' lineare in +`pos` β‡’ **`funding_ens == media esatta delle 24 ancore, max|dif| = 0`**. E il rapporto condizionale +misurato il 22/08 **non si attenua: 2,25x β†’ 2,26x** da K=1 a K=24 β€” *e' esso stesso una media, e la +correlazione esposizione-funding vive alla scala del REGIME, non dell'ora del giorno*. (Replica +indipendente del drag su griglia oraria: **2,14%** contro il 2,02% pubblicato.) + +πŸ“Œ **DOVE STA IL SEGNALE: SKH01 β€” l'ancora a cui il 02/07 non porto' mai questa domanda.** Sulla +lente del path che gira: mediana delle 23 fasi ShFULL **0,974 β†’ 1,286 (+0,312)**, maxDD mediano +**23,3% β†’ 16,6%**. **Venti volte TP01**, e il meccanismo e' chiaro: i suoi trade sono **discreti** β€” +spostare la griglia cambia **quali trade esistono**, non quanto sono grossi (sd d'ancora **0,194 +contro 0,061**). **23 e' primo** β‡’ sulla griglia a 30m non esiste sotto-ensemble simmetrico. +⚠️ **Ma dentro il libro SKH01 pesa il 25% e li' non si distingue da zero** (+0,005 a $635, +0,022 a +$100k, coi segni dei singoli che cambiano fra i due capitali). + +πŸ“Œ **Contro-intuitivo misurato:** tranciando entrambi gli sleeve gli **ordini salgono 6,5x** +(197 β†’ 1283/anno) ma le **fee SCENDONO** ($5,76 β†’ $5,29). Su un venue a fee **proporzionale** si +paga il *nozionale*, e mediare 23 fasi trasforma un Β±1,0x che sbatte da un estremo all'altro in una +posizione frazionaria. **Su un venue a pavimento fisso il segno si ribalterebbe** (regola del 25/07, +riconfermata da un terzo angolo). + +**Barriera (canale funded) β€” attesa refutata *sulla regola sbagliata*.** Il lato binding non e' la +barriera (βˆ’6%, P(breach) 0-7%) ma il **bersaglio** (+10%, P(pass) 40-82%): *meno vol allontana dal +traguardo quanto dalla barriera*, e l'ensemble sta al **31Β° pctl su P(pass)** pur avendo maxDD +migliore della mediana. Ma la regola che il 22/08 ha misurato **uccidere** e' la **daily-loss a un +giorno**, che close-only **non puo' vedere**: sul **p1 giornaliero** l'ensemble batte il **66%** di +150 configurazioni. **Indizio, non misura** β€” la conferma richiede la lente accoppiata. + +⚠️ **Il costo vero, e non e' negli ordini: 23 ancore su 24 di TP01 richiedono le barre di oggi**, +cioe' **`fresh_5m`** β€” il path che il 26/07 e' risultato ricadere **in silenzio** sul certificato e +che il 29/07 ha fallito **6 giri su 8**. Oggi TP01 non ci dipende. **E' l'unica delle tre obiezioni +del 02/07 che sopravvive intatta** (SKH01 quel feed lo usa gia', il che rende l'asimmetria fra i due +sleeve ancora piu' netta). + +🚨 **IL RISULTATO CHE NON ERA NEL MANDATO: la premessa del 02/07 era sbagliata.** Quel filone +boccio' il tranching perche' *"i delta per-ancora (~$1-2) sono sotto il min-order $5 β†’ degenererebbe +in K=1"*. Ma **`src/live/book.build_book_order` banda `abs(delta)` sulla posizione NETTA e manda un +ordine per asset per giro**: *un delta per-ancora non e' mai un ordine*. **Stessa decisione, ragione +diversa β€” e la ragione conta**, perche' quella vecchia cadrebbe al primo esecutore che mandasse un +ordine per tranche, questa no (e' sulla **taglia dell'effetto**). + +⚠️ **Due errori dell'agente, catturati e congelati in commento:** (a) confrontava +`turnover_per_year`, che `eval_weights` **arrotonda**, accanto a una disuguaglianza stretta β†’ +stampava "9,40 contro 9,38" facendo sembrare violata una sub-additivita' che il conto esatto +rispetta; (b) i percentili di coda usavano **un solo segno per tre colonne** di cui due sono ritorni +e una una frequenza β†’ stampava **34Β° dove il valore vero e' 66Β°**, cioe' *"peggio della mediana"* +per un numero migliore della mediana. E la prima stesura affermava *"i due effetti sono additivi"* +come **testo fisso**, refutato dai suoi stessi numeri: ora l'affermazione la **calcola il codice**. + +**VERDETTO: `SU TP01 L'ENSEMBLE NON PUO' CAMBIARE IL DRIFT PER ALGEBRA β€” ELIMINA L'INCERTEZZA, NON +LA MIGLIORA; IL SEGNALE STA SU SKH01 E DENTRO IL LIBRO NON SI DISTINGUE DA ZERO`**. +**Nessuna proposta**: SKH01 non e' passato per `study_family_honest` su questa griglia, il guadagno +e' misurato sugli stessi 7 anni su cui e' stato selezionato, e la lente `hourly` e' gia' nota (26/07) +per sottostimare il path live. **Il filone stabilisce dove guardare, non cosa fare.** + +--- + +## 53 β€” SKEPTIC-LEVA (attacco deliberato al gradino di leva) + +`scripts/research/r0823_skeptic_leva.py` (**127 s**, sola lettura, zero rete). **Le soglie dei sei +attacchi sono congelate nella sezione (0), scritte PRIMA di guardare le sezioni 2-7.** + +βœ… **Replica prima di attaccare β€” 14 numeri, tutti riprodotti:** drift 19,23/17,07% Β· Sharpe +1,691/1,512 Β· **k\* 13,69 e 12,31** (Β§35 dice 13,7 e 12,3) Β· g(1,00) 17,90% e g(1,25) 22,61% +**esatti** Β· perpetua 7,55/6,38% Β· muri $263,3k/$311,7k Β· anni 14,81 contro 14,7 pubblicati. +E l'appaiamento fra k e' **provato**: `max|paths(1,25Β·r) βˆ’ 1,25Β·paths(r)| = 0` β‡’ **i confronti fra k +non hanno rumore Monte Carlo**. + +**A1 DRIFT β€” REGGE, ed e' l'attacco che il mandato riteneva principale.** Bootstrap a blocchi (20 g, +4000 estrazioni): SE **5,09%** (**1,23x l'ingenuo iid** β€” il clustering allarga, ed e' la scelta +severa), IC95 **[+4,78%, +24,97%]**. Il **drift di pareggio del gradino e' +1,42%/anno = βˆ’2,70 SE = +0,25Β° percentile**: fuori da IC95 **e** da IC99. **Il libro puo' perdere il 90,6% del suo drift e il +gradino resta non dannoso.** Controllo che non dipende dal modello: il **peggior triennio mai +realizzato** (+5,35%) sta **3,8x sopra** la soglia. + +**A2 SINISTRO β€” REGGE, con una qualifica misurata.** Ξ”g(1,25)>0 in **5/5** finestre giudicabili, +**2020-21 tolto compreso** (+2,77%). Ma cercando il **biennio MOBILE peggiore** (non la partizione +arbitraria) ne esce uno vero: da **2021-10, drift βˆ’0,45%**, dove il gradino costa **βˆ’0,31%/anno**. +Rapporto guadagno/costo **13:1**. 🚨 **A parte quel biennio nessuna finestra ha drift negativo** β†’ +*l'attacco nella forma piu' forte non e' esprimibile su questo campione*, ed e' esattamente cio' che +il mandato temeva: la sostituzione onesta e' A1, che **impone** il drift invece di cercarlo. + +**A3 CODA β€” REGGE, e corregge Β§33.** Hill e' k-sensibile (sweep ΞΎ 0,289β†’0,672): preso il +**peggiore, ΞΎ=0,431**, piu' severo del 0,34 pubblicato. Peggior giorno strutturale a 1,25x +**17,90% < 50%**; P(dimezzamento su 10 anni) **0,100%**. ⚠️ **Correzione:** Β§33 scriveva *"la coda +muove k\* di 1-2 unita'"* β†’ **ne muove 4,6** (12,31 β†’ 7,71). La conclusione regge (il drift ne muove +6,2) ma **il numero pubblicato era ottimista ~3x**, e a quella coda k\* resta comunque **6x il +gradino**. + +**A4 LENTE β€” REGGE.** Sotto la lente accoppiata (minimo intra-giorno βˆ’7,32% **replicato**) il +ricarico del maxDD **cala** con k: 1,2358 β†’ 1,2297 (1,50x) β†’ 1,2237 (2,00x). E la frequenza del +disaster-SL e' **invariante a k per costruzione**: `want_stop` e' un **prezzo** ancorato al mark, +`want_amount` una taglia, e il test di ri-piazzamento e' **relativo** (`want_amount*0,1`) β†’ **il +numero di Β§40 va moltiplicato, non rifatto**. + +🚨 **A5 INVARIANTE β€” L'UNICO CHE INCRINA, e sta dove nessuno guardava.** Sul **percorso fidato** +l'invariante e' **completo**: la leva vera e' `n_assetΒ·min(WEIGHTΒ·(W_TP01Β·lev+W_SKH), frac)Β·scala`, +un **limite superiore** β€” 0 violazioni su 576 combinazioni. **Ma il percorso fidato non e' l'unico:** +nel ramo **`eq_fallback`** il denominatore del rapporto e' il **watermark** (l'*ultima equity +osservata*), non l'equity. Con un prelievo dell'80% non ancora visto la leva vera e' **5,00x gia' a +k=1,00** e **6,25x a k=1,25**, mentre l'invariante legge 1,00x/1,25x. +πŸ“Œ **La scala non apre il buco: lo MOLTIPLICA.** β‡’ la guardia **G3 della SPEC non e' opzionale**, +e **la SPEC la presenta come una chiusura mentre non lo e'**. Frequenza del ramo **0/1.442 giri**, e +li' `book_execute` **allerta e non blocca**. Seconda incompletezza: `disaster_sl_pct`=0,30 e' +trattato come tetto di perdita e Β§40 ha misurato che **non lo e'** (stop rotolante: passano βˆ’60% +dall'ingresso). + +**A6 FUNDING β€” REGGE, e nel verso OPPOSTO all'ipotesi del mandato.** Il funding scala con k per +costruzione (`r_k = kΒ·(rβˆ’f)`, verificato 1,92% β†’ 2,40%). Il gradino vale **+3,49a** (muro mobile) / +**+2,10a** (muro congelato) **col funding dentro**, contro +2,98a / +1,82a senza: **il funding non +riduce il gradino, lo AUMENTA**, perche' peggiora il **caso base** (17,28a contro 14,81a). +⚠️ **Ma il livello peggiora su tutta la colonna:** k=1,25 col funding (13,78a) resta **peggio** di +k=1,00 senza (14,81a) β†’ **il gradino non ripaga il funding, lo attenua.** + +| lente | k | drift | perpetua | muro | anni (muro mobile) | anni (muro congelato) | +|---|---|---|---|---|---|---| +| L1 fisco | 1,00 | 17,11% | 7,55% | $263.347 | 14,81a | 14,81a | +| L1 fisco | **1,25** | 21,39% | 9,20% | $216.241 | **11,83a (+2,98)** | **13,00a (+1,82)** | +| L3 +FUNDING | 1,00 | 15,19% | 6,38% | $311.705 | 17,28a | 17,28a | +| L3 +FUNDING | **1,25** | 18,99% | 7,79% | $255.422 | **13,78a (+3,49)** | **15,17a (+2,10)** | + +🚨 **RICONCILIAZIONE: il "+1,8 anni / +€164" pubblicato non era una discrepanza, era una +CONVENZIONE NON DICHIARATA.** E' la lettura a **muro congelato** (de-leva al traguardo), riprodotta +a **0,02a**. Β§33 **non dichiara quale delle due letture usa**, e la differenza fra le due (**1,2 +anni**) e' **piu' grande di quasi tutti gli effetti che questo progetto misura**. E il versamento +equivalente misurato direttamente e' **+€144/mese, non €164** β€” la differenza *e'* l'ipotesi di +linearita' dell'interpolazione. + +**K MASSIMO DIFENDIBILE: 1,40.** + +| vincolo | k max | +|---|---| +| PROCESSO (scaletta `SCALA_LADDER`) | **1,25** | +| **SCETTICO** peggior giorno strutturale ≀20% (recupero +25%) | **1,40** | +| PROGETTO disaster-SL ≀50% equity (G6) | 1,67 | +| PROGETTO mezzo-Kelly al limite basso IC95 | 1,92 | +| PROGETTO peggior giorno ≀50% (C1 Β§33) | 3,49 | + +Coi **soli vincoli gia' dichiarati dal progetto** sarebbe **1,67**, e a mordere e' **G6 (il +disaster-SL), non il peggior giorno**. La soglia del 20% e' **aggiunta dall'agente ed e' una +proposta, non una regola**. 🚨 **Il 1,50x NON sopravvive**: sfonda il 20% (21,48%) e sta al 90% del +tetto G6. **La scaletta a 1,25 resta giusta: e' l'unico gate che il progetto ha contro un parametro +che nessun altro gate vede.** + +⚠️ **Attacchi NON portati, dichiarati:** (a) **banda d'ancora del guadagno in anni** β€” e' un Ξ” su +griglia ancorata, servivano 23Γ—24 ancore congiunte, fuori dal budget di 2 CPU; **il segno non e' in +dubbio** (positivo a ogni drift dell'IC95), **la taglia in anni si'**, ed e' la stessa distorsione +che ha gia' ridimensionato il numero funded da 7,8% a 4,4%; (b) **slippage a taglia crescente** β€” +Β§5 ha misurato un fill al **21,9% del volume della sua barra 5m** a $635 con partecipazione lineare +β‡’ **1,25x lo porta al 27,4% SUBITO, non a $5.000**: *va misurato prima del gradino*; (c) costo di +margine sopra 1x β€” **non esiste** su un perp lineare marginato in USDC (il carry *e'* il funding, +gia' dentro); (d) coda di venue β€” vive sul suo asse separato. + +**VERDETTO: `IL GRADINO 1,25x SOPRAVVIVE A CINQUE ATTACCHI SU SEI; IL 1,50x NO; k MASSIMO +DIFENDIBILE 1,40`** β€” e l'unica incrinatura non e' nel gradino ma nella **guardia che lo +accompagnerebbe**. + +--- + +## 50 β€” BOOK-3RD (il terzo sleeve del libro LIVE) + +`scripts/research/r0823_book_3rd.py`. **Nessun file di produzione toccato, nessun ordine.** +Distingue i due oggetti che il progetto confonde di continuo: il **book di RICERCA a 5 sleeve** (su +cui sono calcolati quasi tutti i numeri pubblicati) e il **LIBRO LIVE a 2** (TP01 75 / SKH01 25), +che **non ha mai avuto un terzo sleeve**. + +### Lo stato REALE dei candidati (venue letto ORA, non memoria) + +| candidato | su Deribit? | capitale/lotto minimo | **netta col libro?** | gate | +|---|---|---|---|---| +| STATARB-RESID | SI' | $5 (soglia **nostra**) | **SI'** | 27/09 aperto | +| DVOLSPREAD | SI' | $5 (soglia nostra) | **SI'** | 24/10 kill Β· 24/01 dec. | +| **PREVDAY** | SI' | $5 (soglia nostra) | **SI'** | 🚨 **nessuno, mai registrato** | +| XS01 (19 gambe) | **NO β€” 5 mancano** | 14/19 quotate, 11 da β‰₯1 anno | parziale | β€” | +| XS01-D (11 gambe) | SI' (ridotta) | $0,09-7,72 dal venue | parziale | β€” | +| XSR01 | **NO** β€” venue diverso | n/d | NO | 23/10 aperto | +| VRP01 | SI' (ETH) | **lotto ETH $242 Β· BTC $772** | NO | regola short-vol | +| GTAA01 | **NO** | PRIIPs: ordine RIFIUTATO | NO | β€” | + +πŸ“Œ **Il vincolo che nessuno aveva messo in tabella, letto dal sorgente:** `book_net_target` somma +i due sleeve in **UN** numero per asset e `build_book_order` manda **UN** ordine β‡’ **un terzo sleeve +direzionale su BTC/ETH non paga un min-order proprio** (misurato: 163 β†’ 344 ordini/anno a $635, ma +il **turnover sale solo del 14%**) β€” e per disuguaglianza triangolare `|Ξ”(Ξ£wΒ·p)| ≀ Ξ£wΒ·|Ξ”p|`, cioe' +**le fee modellate per-sleeve sono un limite SUPERIORE**. Il rovescio: **non puo' avere un proprio +stop**, e uno su strumenti diversi richiede una riga in `_CONTRACT` = codice su un percorso con +soldi veri. + +### La misura (1000 estrazioni congiunte, differenze APPAIATE, funding dentro, peso 15%) + +βœ… Replica di controllo prima di ogni delta: **1,813 / 1,437 / 9,42%** riprodotti; replica ancorata +**bit-exact (max|dif| = 0,0)** contro lo sleeve di produzione, idem XS01/VRP01/STATARB. + +| candidato | dShFULL | >0 | dShHOLD | d drift | `weights_tilt_null` | +|---|---|---|---|---|---| +| **PREVDAY** | **+0,192** | 100% | **+0,363** | **+1,31pp** | PASS | +| STATARB | +0,159 | 100% | +0,081 | βˆ’0,12pp | PASS | +| VRP01 | +0,121 | 100% | +0,067 | +0,19pp | PASS | +| XS01 (19) | +0,110 | 100% | +0,378 | +0,79pp | PASS | +| DVOLSPREAD | +0,073 | 100% | +0,062 | βˆ’1,12pp | PASS | +| XS01-D (11) | **βˆ’0,024** | 7% | βˆ’0,128 | βˆ’0,73pp | **FAIL** | + +⚠️ `weights_tilt_null` passa **25/28**, ma il numero da leggere e' `frac_random_beat_hold`, che +arriva a **0,94**: dove vale cosi', *"questo candidato migliora l'hold-out"* e' un claim **generico**. +Il gate **ha potenza** (fallisce su XS01-D), resta **necessario e non sufficiente**. + +### Due fatti di venue che contraddicono la memoria + +🚨 **(1) Il muro del lotto di VRP01 era misurato sulla famiglia SBAGLIATA** β€” inverse, che un conto +USDC **non puo' marginare**. Sulla **USDC-lineare** il lotto ETH e' **$242** (non $1.832) e il BTC +**$772** (non $6.210) β‡’ 1 lotto ETH = peso 12% gia' da **~$2.000**. **Cade il lotto, non la regola** +*"niente short-vol da modello in deploy"*. **5Βͺ occorrenza dello schema `fee_watch`** (un controllo +puntato su una configurazione diversa da quella che gira). +🚨 **(2) XS01 non e' bloccato dal capitale, e' bloccato dal VENUE.** Deribit quota **14 dei 19** +major, ma **3 sono listati fra il 15 e il 21 agosto** e APT non ha nemmeno una quota. Sulle **11 con +β‰₯1 anno** il meccanismo **collassa: Sharpe 1,265 β†’ 0,116**. E il null dei sottoinsiemi **separa le +cause**: sottoinsieme **mediano** da 11 = 0,548 β‡’ **il 62% della caduta e' AMPIEZZA, che il capitale +non compra**; quello di Deribit sta al **9Β° percentile** β‡’ il resto e' **quali** gambe mancano. +**Aspettare 2-3 listing non basta.** + +### La scala β€” e non esiste + +πŸ“Œ **Non c'e' una soglia di capitale che ammette un terzo sleeve.** Il capitale sposta **solo +VRP01** (~$2.000 gamba ETH), che e' fermo **per una regola**. Le due date che contano sono un +**gate** (27/09, costa zero) e la **decisione di venue ($20k)**, che e' cio' che riapre XS01 sulle +19 gambe. + +⚠️ **Caveat pubblicato dall'agente in testa al verdetto:** sono **7 candidati Γ— 4 pesi = 28 +configurazioni sullo stesso hold-out**, nessuna passata per un deflated-Sharpe **di screen** β†’ *il +modo giusto di usare la tabella e' scegliere un candidato per una ragione dichiarata PRIMA, non +prendere il massimo di 28.* + +🚨 **IL BUCO PIU' EVIDENTE, ed e' un fatto di processo: PREVDAY.** E' **il piu' forte misurato** +(+0,192 FULL / +0,363 HOLD, 100% delle estrazioni), **eseguibile**, **netta col libro**, **ADDS**, +`robust_oos` **TRUE**, forward Sharpe **+2,04 su 63 giorni** β€” l'unico monitor risultato **sano** +dopo l'audit Β§32 β€” **e sta in forward-monitor da giugno senza gate pre-registrato e senza +deflated-Sharpe.** Ogni altro lead del progetto ne ha uno. **E' una decisione dell'operatore.** + +**Attesa a priori dichiarata prima di misurare: A1 confermata, A2 meta' giusta, A3 e A4 REFUTATE** +(STATARB da' +0,159 non <0,10; XS01 non entra a $2-5k **ed e' rotto anche a $20k, per venue**), +**A5 confermata al rovescio** β€” *la sorpresa e' un candidato dato per NON eseguibile che lo e'*. + +**VERDETTO: `SETTE CANDIDATI, SETTE MURI DIVERSI, E NESSUNO DEI SETTE E' UNA SOGLIA DI CAPITALE`** β€” +nessuno ammissibile oggi. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 52 β€” SKEPTIC-SPOT (attacco deliberato al lead dello spot) + +`scripts/research/r0823_skeptic_spot.py` (**127 s**). **Sola lettura provata:** +`git status -- src/ config/ scripts/live/ scripts/cron_*.sh tests/ data/ docs/` **vuoto**, **0 +ordini**, solo GET pubbliche con pacing e astensione :05-:10 / :24-:30. βœ… Replica **bit-exact** +prima di ogni delta (`max|dif| = 0.0` contro `FD.tp01_realistic`), con +2,106% di sleeve e +1,580% +di libro protetti da `assert`. + +**A1 allineamento/lag β€” REGGE.** Sei coppie di serie su due asset: l'indice ha il massimo a lag +1 +(= lo `shift(-1)` che `aligned_index` fa), lo spot β€” scaricato da un **endpoint diverso** β€” a lag 0 +come il perp: **terza conferma indipendente della convenzione**. E la domanda si ribalta: +`spot_rate` (perp βˆ’ indice) vale **+0,015%/anno in log** contro +0,55% aritmetico β‡’ **il +1,55% non +viene dall'indice, viene dalla serie del funding** (contributo dell'indice: 0,006%/anno). +*L'attacco puntava a un pezzo che pesa nulla.* + +🚨 **A2 controfattuale β€” INCRINA, e corregge Β§39.** Β§39 dichiara *"lo spot vero non ha storia in +questo progetto"*: **falso**. `get_instruments` da' `creation_timestamp` **2023-04-24** e +`get_tradingview_chart_data` serve **29.196 barre orarie, 0 ore mancanti**. TP01 girato sul **prezzo +spot vero** contro il perp, **24 ancore, differenze appaiate**: mediana **+0,166%/anno di sleeve** +[p10 βˆ’0,445, p90 +0,581], **16/24 positive** β†’ banda che contiene lo zero, punto stimato **a favore** +dello spot β‡’ **il controfattuale sul PREZZO e' innocuo**. Ma lo strumento **non esisteva per il +55,3%** del campione, e nel 2023 aveva ~9% di ore senza scambi e ~15% di barre piatte (**la soglia +con cui questo progetto escluse gli alt Deribit**). Lordo per finestra: **+1,580% (7,4 anni) β†’ ++1,450% (era spot) β†’ +1,180% (era liquida 2024+)**. + +**A3 tetto 1,0x β€” REGGE, e il bersaglio ne esce piu' forte.** I 3 giorni sopra 1,0x sono BTC +2018-11-10/11/12 (max 1,012), **prima dell'era del libro e in perdita**: troncare avrebbe +**guadagnato** +0,013%. + +🚨 **A4 spread e profondita' β€” INCRINA, ed e' l'attacco che decide.** Sullo **spread** fallisce: +mezzo spread **4,34 bps** (10 giri) contro 1,5-3,1 di Β§45, ma **like-with-like** il contributo netto +(spread pagato βˆ’ commissione risparmiata, spot a fee 0) e' **[βˆ’0,047%, +0,153%]** e **si sovrappone** +alla banda pubblicata. Sulla **profondita' morde**: camminando **entrambi i libri** per ogni ordine +storico, il differenziale spotβˆ’perpβˆ’fee e' **+0,72 bps a $600 (βˆ’0,04%/anno)** e **+25,66 bps a +$272k = βˆ’1,540%/anno, il 97% del lead lordo**. +πŸ“Œ **IL LEAD SI ANNULLA ESATTAMENTE ALLA TAGLIA DEL MURO CHE PRETENDE DI SPOSTARE: il muro NON +scende del 10,9% pubblicato.** + +**A5 haircut β€” REGGE.** Nessun haircut ≀100% rende binding il margine (**copertura 50,0x replicata +al decimo**). ⚠️ Correzione a Β§45: *"lo spot non si liquida"* e' **falso** β€” BTC/ETH hanno +`in_cross_collateral_pool: true`, quindi **sono** collaterale liquidabile. Non binding, **ma il +verso e' contrario alla prudenza**. + +**A6 disaster-SL tolto a TP01 β€” REGGE.** Allo scenario operativo (72h) vale **1,497% di equity di +gamba una volta sola** = **0,152%/anno** ammortizzato (**10% del lead**). TP01 e' long-flat a +esposizione media 0,12-0,16x: *nei crolli e' gia' quasi flat*. + +πŸ“Œ **NUMERO ONESTO RIVISTO: `[+1,38%, +1,58%]/anno a $600` Β· `βˆ’0,11%/anno a $272k`.** + +⚠️ **Tre errori dell'agente, catturati e dichiarati nello script.** (a) Confrontava il costo di +**spread** con la banda **netta** di Β§45 β€” mele contro pere, **nel verso che gli conveniva**; +(b) l'aritmetica del margine metteva SKH01 a 1,0x su **ciascun** asset invece che sul proprio sleeve +(25,0x invece di 50,0x: **Β§45 aveva ragione**); (c) ammortizzava su 7,4 anni la coda di un blackout +da 30 giorni **preso come argmax**, ottenendo 1,97%/anno = **il 127% del lead da un evento mai +accaduto**. πŸ“Œ **E gli stimatori di spread da OHLC (Roll, Corwin-Schultz) sono stati SQUALIFICATI +DAL PROPRIO CONTROLLO POSITIVO** (davano 4-16 bps su un perp che sta a **un tick**) e i loro numeri +cancellati. + +⚠️ **Attacchi non portati:** la distribuzione **giornaliera** dello spread (ha 10 giri di **una** +ora; servirebbe un campionatore orario per ~30 giorni, 2 GET/ora, **€0**); il f di **esecuzione** +vero (richiede l'ordine di prova da $7,73 = decisione dell'operatore); l'haircut resta **non +leggibile** (`get_collateral_info` β†’ *Method not found*), **aggirato invece che risolto**; il muro +rifatto con costo d'esecuzione **funzione del capitale** (seguito naturale di A4); la liquidita' +2019-2023 **non esiste per definizione**; e **la domanda fiscale (~0,83%/anno) resta dominante e non +falsificabile con un backtest**. + +**VERDETTO: `IL LEAD REGGE A $600 E SI ANNULLA A $272k β€” CIO' CHE MORDE NON E' NEI SUOI NUMERI, E' +NELLA TAGLIA A CUI VENGONO SPESI`**. + +--- + +## 54 β€” PREVDAY-GATE (il gate che il lead piu' forte non ha mai avuto) + +`scripts/research/r0823_prevday_gate.py` (**215 s**). Nessun file di produzione toccato (`git diff` +vuoto; **sola lettura provata a runtime con md5+mtime su 6 file**). + +**Famiglia dichiarata e trial contati al rialzo, STAMPATI PRIMA di ogni risultato:** +`ANCHOR_DAYS{1,2,3,5}` Γ— `BUFFER_K{0…1,00}` Γ— `ALLOW_SHORT{T,F}` Γ— `MIN_HOLD_H{0,24,72}` Γ— +`TF{1h,4h}` = **336 celle**; le **11** gia' spese in giugno e le **β‰₯8** della scoperta sono +**dentro**. Escludere il TF 15m rende il conto **piu' permissivo**, ed e' dichiarato. Screen: +N=352 / N_sweep=456. + +🚨 **IL RISULTATO PRINCIPALE: la cella al buio non e' quella che gira.** + +| | AL BUIO (in-sample) | CONGELATA (monitor) | +|---|---|---| +| tf / parametri | **4h, k=0,30, short=FALSE** | 1h, k=0,30, **short=TRUE** | +| rango in-sample | **1/336** | **186/336** | +| Sharpe FULL / HOLD | +1,621 / +0,731 | +1,236 / +1,297 | +| corr β†’ TP01 (full/hold) | **0,641 / 0,469** | **0,152 / 0,028** | +| deflated-Sharpe N=336 | **0,993 PASS** | **0,905 FAIL** | + +`earns_slot_honest = True`, **ma su un'altra strategia**. πŸ“Œ **La selezione onesta compra il libro +LONG-FLAT β€” mentre il docstring della scoperta dice l'opposto in chiaro** (*"LONG-SHORT is the +slot-earner… the long-flat sibling just re-rides the bull"*): **la gamba short, cioe' l'intera +ragione per cui PREVDAY fu promosso a LEAD, e' esattamente cio' che la selezione in-sample-only non +compra.** Rovescio dovuto e riportato: **la cella al buio diversifica molto meno** (corr 0,64 contro +0,15). βœ… `factory(FROZEN) == pb.target` bit-exact 2/2, `causality_ok` ok, identita' con +`study_family_honest` verificata. + +❌ **Attesa a priori del coordinatore REFUTATA sul DSR, e il perche' e' MISURATO.** Tutte le +partizioni difendibili **PASSANO** (N=11 β†’ 1,000; N=168 β†’ 0,996; N=336 β†’ 0,993; famiglia+screen +N=352 β†’ 0,991; N=560 β†’ PASS): **portare N da 11 a 560 costa 0,009 di DSR**, e il verdetto si +ribalterebbe solo a `sd(trial) β‰₯ 0,363` contro **0,267** misurata. **Ma non e' vacuo:** controllo +positivo β€” la cella **mediana** della stessa famiglia **FALLISCE** (0,863), il 25Β° pctl 0,593. +πŸ“Œ **REGOLA NUOVA, che completa il filo Β§10: su una famiglia OMOGENEA il deflated-Sharpe e' cieco +sul CONTEGGIO e vede sul LIVELLO.** Il 30/07 VRP01 si ribaltava col conteggio **perche' quella +famiglia era larga**. *Non e' il gate a essere incoerente: e' la larghezza della famiglia a decidere +su quale asse ha potenza.* + +**`day_boundary_robust` β€” ROBUST** (βˆ’8h +0,114 Β· βˆ’4h +0,218 Β· 0h **+0,328** Β· +4h +0,215 Β· +8h ++0,164, spread 0,214, **mai negativo**; controllo positivo superato: un segnale puramente di +calendario esce ARTIFACT-RISK). ⚠️ Ma sui **24 confini-giorno la canonica e' al 96Β° pctl**: ShFULL +1,236 β†’ **mediana onesta 0,963**, uplift +0,328 β†’ **+0,190**. +⚠️ **Trovato per strada:** il campo `base` di `altlib.day_boundary_robust` e' **`offsets[0]`, non lo +zero** β€” con una griglia che comincia in negativo **l'etichetta inganna**. + +πŸ“Œ **Banda d'ancora del LIBRO β€” Β§50 non aveva de-luckato il candidato.** Spazio congiunto +TP01Γ—24 Β· SKH01Γ—23 Β· **PREVDAYΓ—24** = 13.248 configurazioni, 300 estrazioni, differenze appaiate. +βœ… **Replica indipendente di Β§50 a candidato fermo: +0,191/+0,362** contro i +0,192/+0,363 +pubblicati. **De-luckato a peso 15%: dShFULL +0,100 (100% delle estrazioni) / dShHOLD +0,246.** +**Il segno regge al 100% a ogni peso; la taglia si dimezza.** + +**MDE, integrita', funding, tilt-null.** **MDE:** 63 giorni = 0,172 anni β†’ **t = 0,85 (naive) / +0,48 (Lo)**, IC95 largo **16,6 punti di Sharpe**; per distinguere uno Sharpe vero di 1,2 dal nulla +all'80% servono **9,4 anni**. ⚠️ E **il "+2,04" e' su lente ORARIA**: sulla lente **giornaliera** con +cui e' misurato ogni altro sleeve del progetto e' **+1,56**. **Integrita':** 1512/1512 ore, 0 +duplicate, **95,6% ricostruibili bit-a-bit**; le 67 divergenti sono 1,06/giorno **all'ora del cron** +β‡’ ~32 min/giorno non registrati (contro i **4 min/giorno** di `paper_statarb`): **sano, non +perfetto**. **Funding** (mai in nessun backtest): **βˆ’1,37%/anno** di sleeve, e πŸ“Œ **la gamba short +NON compensa** β€” incassa 0,26-0,46x l'incondizionato mentre la lunga paga 1,25-1,69x. +**`weights_tilt_null`:** PASS a tutti i pesi, ma **`frac_random_beat_hold` = 0,91**. + +### `GATE PREVDAY-01` β€” decisione **2027-06-21** + +*(La data non e' scelta perche' la finestra diventi informativa β€” la misura dice che **non** lo +diventa β€” ma perche' oltre un anno **tenere un lead senza decidere e' esso stesso una decisione**.)* +Promozione al peso 15% **solo se TUTTE**: **(a)** DSR β‰₯ 0,95 sulla famiglia ri-dichiarata **prima**, +con la sensibilita' alla partizione pubblicata accanto; **(b)** la cella al buio **coincide** con +quella congelata, **oppure** il monitor si ri-congela su quella al buio **e il forward riparte da +zero**; **(c)** delta di libro appaiato sullo spazio congiunto **> +0,05** dShFULL, positivo in +**β‰₯90%**, **col funding dentro**; **(d)** `weights_tilt_null` pass; **(e)** ADDS + robust_oos + +non-hedge **sulla cella al buio**; **(f)** `day_boundary_robust` β‰  ARTIFACT-RISK; **(g)** Sharpe +forward > 0, **soglia debole di proposito**. +**Veto d'integrita'** (blocca, non decide): β‰₯80% di barre ricostruibili **e** i minuti non +registrati non devono crescere. **Kill:** Sharpe forward < βˆ’0,50 su β‰₯180 giorni di barre **attive** +β€” a quell'orizzonte P(uccidere un edge vivo a Sharpe 1,2) = **9,7%**; **al kill la famiglia NON si +ri-ottimizza.** + +⚠️ **Errori dell'agente su se stesso:** **A1 giusta nell'esito, sbagliata nel meccanismo** (prevedeva +un `k` piu' piccolo; k resta 0,30, a cambiare sono `ALLOW_SHORT` e il TF); **A2 refutata** (il DSR +non si ribalta col conteggio); **A5 giusta nel numero, sbagliata nel meccanismo** (credeva che la +gamba short compensasse il funding). + +**VERDETTO: `IL GATE E' SCRITTO E OGGI PASSA 8 CONDIZIONI SU 10 β€” LE DUE CHE MANCANO SONO LA STESSA +COSA: LA CELLA CHE GIRA NON E' QUELLA CHE LA SELEZIONE ONESTA SCEGLIE`**. **Nessun cambio al libro +live proposto.** + + +### Addendum del coordinatore β€” la finestra forward di PREVDAY non puo' decidere niente + +Misura mia, sulla serie che il gate userebbe (`data/paper_prevday/returns.jsonl`, **sola lettura**). +βœ… **La serie e' INTATTA**: **1512 barre orarie su 1512 attese** dal 2026-06-21 01:00 al 2026-08-23 +00:00, zero buchi β€” **e' il monitor sano**, quello che il difetto `advance()` non tocca (cadenza +oraria, la barra si chiude ogni ora). Questo la distingue da STATARB/XSR01/DVOLSPREAD. + +**Ma la finestra e' lunga 0,172 anni**, e li' l'**MDE(95%) e' 4,72 di Sharpe**: lo **+2,11** +osservato **non e' distinguibile da zero**. Il campione di scommesse e' **26 ENTRY** (14 BTC / +12 ETH, 151 trade/anno). ⚠️ **Conseguenza per `GATE PREVDAY-01`: qualunque cosa il gate decida, non +puo' appoggiarsi al numero forward** β€” le 8/10 condizioni superate stanno in piedi sul backtest e +sulla struttura, non sui 63 giorni di monitor. + +⚠️ **Errore mio, catturato prima di pubblicarlo:** avevo formulato il meccanismo come *"la +precisione apparente viene dal numero di MARK, non dal numero di SCOMMESSE"* β€” **falso**. La +deviazione standard dello Sharpe annualizzato vale ~`1/sqrt(anni)` **indipendentemente dalla +frequenza di campionamento**: infatti la stima al livello barra e quella al livello trade danno lo +**stesso** 4,72. Campionare piu' fitto non gonfia la precisione, e la formula lo sapeva gia'. +**Il fatto vero e' piu' semplice: 63 giorni sono 63 giorni.** + +--- + +## 56 β€” XS-AMPIEZZA (recuperare ampiezza su un universo povero) + +`scripts/research/r0823_xs_ampiezza.py` (**235 s**). **Sola lettura provata** (`git status` su +`src/ config/ scripts/live/ scripts/cron_*.sh tests/ data/ docs/` **vuoto**, 0 ordini, GET pubbliche +con pacing e astensione). βœ… Replica **bit-exact** contro `sleeves._xsec_returns` +(`max|dif| = 0,0`) prima di ogni delta. Griglia dichiarata prima: 4 kind Γ— 5 k Γ— 2 lookback Γ— 2 H = +**80 celle**. + +### N_eff β€” il gate, in cima perche' decide + +| universo / costruzione | gambe | rbar | **N_eff (autovalori)** | Sh FULL | +|---|---|---|---|---| +| **19 HL (canonico)** | 19 | βˆ’0,028 | **9,34** | 1,336 | +| 14 quotate Deribit | 14 | βˆ’0,049 | **8,03** | 0,893 | +| **11 Deribit β‰₯1 anno** | 11 | βˆ’0,067 | **7,23** | 0,193 | +| 11 β€” demean / resid / equal-risk / whitening | 11 | β€” | 7,13-7,21 | 0,193 / 0,550 / 0,088 / 0,051 | +| 11 β€” nudo L=30 / **k=2** | 11 | β€” | 8,47 / 8,30 | 0,243 / **0,669** | + +🚨 **N_eff scende del 23% mentre lo Sharpe scende dell'86%**: sotto `IR = IC·√ampiezza` la sola +ampiezza predirebbe 1,175, cioe' **il 14% del divario**. E **nessuna costruzione la muove** +(7,13-8,47). β‡’ **L'etichetta "62% e' AMPIEZZA" di Β§50 descriveva il NUMERO DI GAMBE, non l'ampiezza +effettiva.** + +⚠️ **La formula importata da XSR01 DEGENERA qui, e l'agente l'ha detto invece di usarla:** con +`rbar` negativa `N/(1+(Nβˆ’1)Β·rbar)` **esplode sopra N** (fino a 33 su 11 gambe). Non e' un difetto di +quella formula β€” su un paniere di **coppie** rbar e' positiva, mentre le gambe di un long/short +cross-sezionale sono **anti-correlate per costruzione**. Si legge il **participation ratio**. + +πŸ“Œ **E il demeaning β€” la mossa che CREO' XSR01 β€” su XS01 e' un NO-OP ALGEBRICO:** XS01 mette ++0,5/k e βˆ’0,5/k, quindi **`sum(w) = 0` per costruzione** (`max|Ξ£w| = 2,8e-17`, serie identiche a +2,2e-16). *Non e' un risultato debole: e' un'identita' valida a ogni universo e ogni k.* + +### Il canale che porta tutto e' `k` β€” e solo sull'universo povero + +Differenze **appaiate sulle 10 fasi** del ciclo H=10: + +| confronto | mediana | fasi >0 | +|---|---|---| +| **k=2 MENO canonico k=5 (11 gambe)** | **+0,832** | **100%** | +| k=3 MENO canonico k=5 (11) | +0,650 | 100% | +| resid k=2 MENO k=2 (11) | +0,125 | 80% | +| equal-risk / whitening / nudo / H=20 | βˆ’0,06 / βˆ’0,02 / βˆ’0,37 / βˆ’0,69 | 20/50/20/**0%** | +| **k=2 MENO canonico k=5 (19 gambe)** | **βˆ’0,080** | **30%** | + +🚨 **`k=5` NON E' UN PARAMETRO INVARIANTE DI SCALA.** Fu tarato su A=19, dove seleziona il **26% +per lato**; su A=11 e' il **45%** β€” con k=5 si tengono **10 posizioni su 11**, e lo spread z +longβˆ’short crolla da **2,08 a 1,43**. Plateau monotono su k∈{1,2,3}, **piatto su 19 gambe**. + +**Il null a fee zero, e il p-value de-luckato** (mai fatto prima in questo progetto): canonico k=5 +p=0,355 Β· k=2 p=0,165 Β· **resid k=2 alla fase 0 p=0,005** β€” ⚠️ ma quella fase sta al **90Β° pctl** per +quella cella, e de-luckando sulle 10 fasi **p mediano 0,130, sotto 0,05 solo nel 40%**. +*Il verdetto del null cambia con l'ancora, e l'ancora e' una scelta.* βœ… Controllo positivo: +canonico su 19 gambe p=0,010. + +🚨 **DOVE STA DAVVERO IL DIVARIO: L'IC.** + +| universo | IC | t | +|---|---|---| +| 19 HL | **+0,0565** | **+2,29** | +| 14 quotate | +0,0108 | +0,44 | +| **11 Deribit β‰₯1 anno** | **βˆ’0,0270** | βˆ’0,88 | +| 9 alt maturi (11 senza BTC/ETH) | **βˆ’0,0652** | **βˆ’2,07** | +| le 8 mancanti da sole | +0,0075 | +0,26 | + +Su **40 sottoinsiemi casuali da 11**: mediana +0,059, minimo +0,0115 β†’ **Deribit al 0Β° percentile, +fuori dalla banda**. Β§50 lo chiamava *"sfortuna di listino"* al 9Β° pctl dello **Sharpe**; **sull'IC +e' strutturale**. +πŸ“Œ **E poiche' ne' i major ne' le mancanti hanno IC da soli, l'informazione vive nel CONTRASTO FRA +LE FASCE: XS01 e' in buona parte una rotazione major-maturi contro alt-recenti, non una selezione +dentro una fascia.** *(E' la descrizione piu' precisa di XS01 prodotta finora, e arriva da un +tentativo di ripararlo.)* + +**MDE e gate.** 966 giorni, **936 attivi (97%)**, in-sample = il solo **2024**. SE(Sharpe): FULL +0,66 Β· **in-sample 1,75** Β· hold-out 0,83 β†’ **MDE hold-out 2,34**. Lo spread di coda che dovrebbe +pagare il recupero ha **t 0,24 (k=2) / 0,57 (resid k=2)** contro 1,91 dell'universo pieno. Cella al +buio `white k=1` β†’ IS +1,90 / **HOLD βˆ’0,53**; **DSR 0,158 FAIL** (massimo atteso dal rumore +**1,065** contro candidato 0,459); `marginal_vs_tp01` NEUTRAL. + +🚨 **ERRORE CATTURATO SU SE STESSO, e vale come regola: la selezione in-sample-only NON e' stabile +alla dichiarazione della griglia.** Con k∈{2,3,4,5} (64 celle) la cella al buio era `resid k=2` β€” +IS +1,46 / **HOLD +1,33** / FULL 1,38, **un candidato che REGGE**. Aggiungendo k=1 (estensione del +plateau, **decisa prima di guardare**) diventa una cella che fa **HOLD βˆ’0,53**. **Con ~1 anno di +in-sample la procedura onesta e' una monetina.** + +⚠️ **Secondo errore catturato:** la prima misura d'eseguibilita' usava `|Ξ”W|` e dava *"ticket $19, +100% eseguito"*. La posizione in dollari e' `WΒ·scale`, e il vol-target muove il nozionale ogni +giorno anche a pesi fermi. Corretta: a $635 **83% del nozionale** eseguito (96% a $5k) β€” e +**riproduce per via indipendente le due popolazioni di Β§45** (ribilanci di segnale 11,9% degli +ordini / **62,5% del nozionale**; deriva del vol-target 88,1% / 37,5%). **Il muro non e' il +capitale, e' l'IC.** + +**Eseguibilita' dal venue:** 14/19 quotate, di cui SUI (18/08), **APT (21/08, nessuna quota)** e +AAVE (15/08) listate negli ultimi 8 giorni; **11 da β‰₯1 anno**; lotti **$0,01-7,72**. Solo **2 delle +11** (BTC, ETH) netterebbero col libro live: **le altre 9 pagano min-order proprio** e vorrebbero +righe nuove in `_CONTRACT`. + +**Attesa a priori del coordinatore: giusta nella conclusione, SBAGLIATA nel meccanismo** β€” dicevo +*"il 62% e' ampiezza e l'ampiezza non si compra"*; l'ampiezza **non e' quasi per niente il +problema**. + +πŸ“Œ **DA PORTARSI VIA: congelare un meccanismo su un universo nuovo vuol dire congelare il +QUANTILE, non il numero di posizioni.** Su XS01 la differenza vale **~0,8 di Sharpe**. *(Stessa +famiglia della lezione SOL del 22/08: il criterio di selezione di SKH01 non sopravvive a un asset +nuovo.)* + +**VERDETTO: `NO β€” E LA CADUTA NON ERA AMPIEZZA: E' SELETTIVITA' (RECUPERABILE, MA SOTTO L'MDE) PIU' +UN IC CHE SU QUELLE 11 GAMBE VALE ZERO`**. XS01 resta bloccato dal venue, **e adesso si sa +perche'**. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 55 β€” COSTO-CAPITALE (il costo d'esecuzione come funzione del capitale) + +`scripts/research/r0823_costo_capitale.py` (**144 s**, sola lettura, nessun ordine). Nato da un +follow-up che **due agenti hanno dichiarato indipendentemente** la stessa notte (Β§52 e Β§53). + +### La curva costo-taglia, misurata + +**[VENUE]** 50 istantanee (25+25) in 35 minuti, **camminate su entrambi i lati** β€” non una +fotografia. Mediana [p10-p90]: + +| ordine | BTC_USDC-PERP | ETH_USDC-PERP | +|---|---|---| +| $5 | **0,006** [0,006-0,356] bps | **0,021** bps | +| $500 | 0,006 | 0,021 | +| $5.000 | 0,315 | 0,373 | +| $50.000 | 0,714 | 1,284 | +| $500.000 | **2,198** | **2,207** | +| $1.000.000 | 14,03 | 5,22 | + +Fine del libro visibile: **BTC $2,81M Β· ETH $1,19M**. **Il fondo della curva e' il mezzo spread = UN +TICK.** ⚠️ Il campione e' di **domenica notte UTC**, la finestra piu' sottile della settimana: +**conservativo dentro il presente, ottimista proiettato su 2019-2026** β€” dichiarato in testa allo +script. + +βœ… **Repliche 8/8 + R9**: TP01/SKH01/libro a costo spento **bit-exact**; muri **$272.061** e +**$258.338 al dollaro** sul vintage 07/08, **$313.143** oggi; `accumula_costo` bit-exact contro +`PN.accumula`; `walk_cost_bps` identica a quella di Β§52; **R9: k\* a costo zero = 12,25x = +`FD.k_star`**. Due controlli positivi obbligatori superati. + +**Il muro col costo endogeno: $313.143 β†’ $315.080 = +0,6%**, convergente in una iterazione. +πŸ“Œ **Va letto come NULLO**: la bisezione e' un Monte Carlo a 4.000 percorsi con risoluzione ~1%. +Con la curva p90 resta +1,4%. + +**Il punto di saturazione β€” due definizioni indipendenti, stessa decade:** **duro** (primo 1% di +nozionale annuo non eseguibile in un istante) **$10,0M**; **economico** (argmax di `C x drift(C)`) +**$9,49M** [$8,46-10,64M], profitto $1,28M/anno. πŸ“Œ **Non e' un ottimo liscio: la saturazione E' la +fine del libro visibile.** E **la cura e' dell'esecutore, non del mercato** β€” $1,25M costano 41,8 bps +in un istante e **1,1 bps a tranche da $125k**; serve solo a ~$10M. **SKH01 satura prima** (ordine +~100x la mediana di TP01, e il 69% del nozionale annuo), **TP01 lo supera subito dopo** per la coda +dei suoi ordini. + +**Traiettoria €500/mese: 17,3a / P(20a) 85,1% β†’ 17,3a / 84,7%.** L'attesa del coordinatore era +giusta, **ma per un motivo in piu' di quello previsto: il drag CAMBIA SEGNO** (βˆ’0,118%/anno a $2k, ++0,118% a $1M). **Gradino di leva: k\* 12,50x β†’ 12,25x** alla taglia del muro; **a $635 il costo +aggiuntivo di 1,25x sta sotto il punto base l'anno**; morde solo a $5M. + +**Le due obiezioni degli scettici, misurate.** **Β§52 non e' contraddetta, e' confermata dall'altro +lato:** il costo che si annullava era dello **SPOT** (spread **100-500x** piu' largo del perpetual). +**Β§53 aveva la domanda giusta e la grandezza sbagliata:** il 21,9% era la quota di una barra 5m **a +volume basso**; l'ordine di oggi e' lo **0,13% (BTC) / 0,43% (ETH)** della profondita' a 1 bps, e a +1,25x diventa 0,16% / 0,54%. **La riserva di Β§53 e' misurata e non regge.** + +🚨 **IL FATTO TROVATO PER STRADA, ed e' il piu' utile: alla taglia di oggi il costo che le tabelle +pubblicate non contengono ha segno NEGATIVO.** Il drift a **$635 e' 13 bps/anno PIU' BASSO** che a +$5.000 β€” e non per lo slippage: e' il **pavimento `min_order` $5**. Vale **~2x** quello che lo +slippage costa passando da $313k a $1M, **in direzione opposta**. *Il conto piu' preciso non muove +il muro: sposta il segno dell'errore, e alla taglia di oggi lo sposta a favore.* + +⚠️ **Errori dell'agente, catturati e pubblicati nello script:** (i) la banda p10/p50/p90 era +costruita con **tre oggetti-curva separati** β†’ oltre il libro visibile la p10 finiva **sopra** la +p50: *una banda che si incrocia non e' una banda*; (ii) il controllo positivo del drag era +**degenere per costruzione** (costo costante di 20 bps, che sparisce da un drag **differenziale**) β€” +stessa forma del null de-levering degenere di Β§5-bis: *un controllo puo' fallire perche' e' +costruito male, non perche' la macchina e' rotta.* + +**VERDETTO: `IL COSTO ENDOGENO NON MORDE β€” IL MURO SI SPOSTA DI +0,6% (DENTRO LA RISOLUZIONE MC) E +LA SATURAZIONE STA 30x SOPRA`**. **Libro, pesi, cron, config INVARIATI.** + +--- + +## 57 β€” CRITICO DI CHIUSURA (attacco all'ondata, coordinatore incluso) + +`scripts/research/r0823_critico_chiusura.py` (**120 s**, `--fast` 45 s, sola lettura **provata**). +Attese a priori **scritte nel docstring prima di misurare**: A1/A4/A5/A6 **confermate**, **A2 e A3 +REFUTATE**. Ogni rilevatore ha il suo controllo positivo (24 serie indipendenti β†’ N_eff 24,5 Β· 24 +serie identiche β†’ 1,00). + +### Repliche, prima di ogni accusa β€” tutte riuscite + +βœ… Libro 75/25 path live drift **19,23%/17,07%**, Sharpe **1,692/1,513**, funding +**βˆ’2,1597%/anno**; i quattro muri **al dollaro** ($278.033 / $264.373 / $327.617 / **$313.143**); +βœ… **il muro E' esattamente `prelievo/perpetua`** su 4 lenti su 4; Β§42 dShFULL(floor=βˆ’1) = **βˆ’0,4502**; +TP01 canonico **1,305**; XS01 fase 0 == sleeve a `max|diff| = 0,0`. Funding **ri-misurato da +implementazione indipendente**: TP01 **2,138%/anno**, condizionale **1,93x BTC / 2,58x ETH** contro +il pubblicato 1,86-2,55x, prodotto ingenuo che sottostima di **2,18x**. *Il nucleo quantitativo +dell'ondata regge.* + +### 🚨 (1) Il muro non ha mai avuto una banda, e la sua e' esplosiva + +SE del drift L3 misurata dal critico: **5,151%/anno** (block bootstrap 20g; Β§53 misura 5,09 su +un'altra lente β†’ βœ… concorda). Propagando **solo** quella: + +| drift | perpetua | muro | x pubbl. | +|---|---|---|---| +| +1 SE 20,34% | 9,73% | **$204.517** | 0,65x | +| p90 21,79% | 10,66% | $186.623 | 0,60x | +| **PUNTO 15,19%** | **6,35%** | **$313.143** | 1,00x | +| p10 8,59% | 1,74% | **$1.141.172** | 3,64x | +| βˆ’1 SE 10,04% | 2,80% | $709.753 | 2,27x | +| **βˆ’2 SE 4,89%** | **0,00%** | **IRRAGGIUNGIBILE** | β€” | + +«€500/mese β†’ P(20a) 85%Β» diventa **~0% a βˆ’1 SE**; «€250/mese β†’ 14%Β» diventa **96% a +1 SE**. +πŸ“Œ **Meccanismo: il muro e' `prelievo/perpetua` e la perpetua si annulla molto prima del drift β†’ e' +un 1/x su una quantita' che va a zero, quindi l'errore e' asimmetrico verso l'alto.** +🚨 **L'ondata ha pubblicato la risoluzione MONTE CARLO del muro (0,7%) accanto a un numero la cui +incertezza di PARAMETRO e' cento volte piu' grande: ha misurato la precisione del simulatore e mai +quella del suo input.** ⚠️ E la SE del block-bootstrap e' un **limite inferiore** (variabilita' +dentro gli stessi 7,4 anni, non il cambio di regime). L'attesa A2 del critico ([$230k, $430k]) era +ottimista di **~2x sull'ampiezza**. + +### 🚨 (2) Β«Positivo in N/N ancoreΒ» non e' N osservazioni + +Misurato su serie vere: **24 ancore TP01** corr media **0,631** β†’ **N_eff 1,55** / PR 2,09; +**10 fasi XS01** corr **0,790** β†’ **N_eff 1,23** / PR 1,51. ❌ **A3 REFUTATA: nella differenza +appaiata la componente comune NON si cancella** (corr 0,593 contro 0,631 dei livelli β†’ N_eff +1,64-2,24). Per la **stessa grandezza** (Β§42, ancora canonica): banda d'ancora pubblicata **0,199** +di ampiezza, **IC95 bootstrap [βˆ’0,814, +0,048] = 0,862, cioe' 4,3x piu' largo E CONTIENE LO ZERO** +(7,2x contro la p10-p90). Su XS01 lo stesso rapporto e' **3,8x** β€” due misure indipendenti, stesso +fattore ~4. +⚠️ **Cio' che questo NON dice: che i verdetti cadano.** Β§42 resta SCARTATO su iso-vol FAIL 3/3, +maxDD +8,80pp, selection-on-holdout, edge lordo assente. **Cade la precisione dei numeri, non la +direzione delle decisioni.** +🚨 Ma dove l'ondata attacca un **p-value** a un conteggio del genere β€” Β§26, *Β«test dei segni 12/12, +p=0,0005Β»*, su finestre 30g **sovrapposte** e per giunta **selezionate come le peggiori** β€” quel +p-value non e' calcolabile cosi'. + +### 🚨 (3) Tre baseline diverse sotto lo stesso nome Β«libro 75/25Β» + +`hourly` **ShFULL 1,683 / maxDD 11,22%** (Β§35/Β§36/Β§39/Β§45/Β§52/Β§53 e **ogni muro e traiettoria**) Β· +`canonical` **1,800 / 9,56%** (Β§41/Β§42/Β§49/Β§50/Β§54) Β· **1,626 / 10,4%** = mediana d'ancora (Β§8/Β§25). +**Spread 0,12 di Sharpe e 1,7pp di maxDD, piu' grande di quasi tutti gli effetti che l'ondata +misura** (BIN-FREQ +0,112 Β· VOL-SIZE +0,070 Β· PREVDAY de-luckato +0,100): **un Ξ” accostato al +baseline sbagliato cambia piu' del Ξ”.** Ogni filone lo dichiara nel proprio script; il registro no. + +### 🚨 (4) Il Β«soffitto direzionale ~1,15Β» e' contraddetto dalla stessa ondata + +Β§23 pubblica il miglior lookback costante a **1,639** e Β§54 la cella PREVDAY al buio a **1,621** +(che **passa** il DSR a N=336), stessa coppia, stessa lente netta-fee. Β«SoffittoΒ» sta per tre cose +diverse e il registro usa la piu' bassa citandola come la piu' alta. **Conseguenza: l'argomento +aritmetico di Β§12** (*Β«noise max 1,572 > soffitto 1,3 β‡’ uno screen largo non puo' passare il proprio +DSRΒ»*) **si inverte β€” ed e' la SECONDA refutazione indipendente** dopo quella di Β§24 (che lo ribalta +sulla partizione). *Un argomento refutato due volte per due strade diverse non va ereditato come +regola.* + +### ⚠️ (5) Due difetti di contabilita' nel piano, piccoli e sistematici β€” entrambi a favore + +(a) **«€X/meseΒ» versa ogni 30 GIORNI**: 121/182/243 versamenti invece di 120/180/240 +(+0,83/+1,11/+1,25%; asintotico +1,39%) β€” presente in ogni tabella dal 25/07. +(b) 🚨 **il contatore `versato` di `accumula` e' uno SCALARE che non si ferma al traguardo** β†’ il +*Β«a 10 anni si versano $229k, i bonifici fanno il 73%Β»* e' l'intero orizzonte: il versato **fino +all'arrivo** e' **$190.265, cioe' il 61%, non il 73%** (P(traguardo) 91%). *Stessa famiglia del bug +`paid` catturato il 25/07 β€” la lezione era codificata e il difetto era altrove.* +(c) `deluck` e' applicato **dopo** la sottrazione del funding, quindi sconta dell'11% anche il +funding: **+4,0% sul muro**, **sei volte** la risoluzione MC dichiarata accanto allo stesso numero. +Scelta di modello, non errore β€” ma va detta. + +### βœ… (6) Aritmetica dei tetti di leva: verificata sul config che GIRA, 3/3 al centesimo + +`frac=0,5 Β· disaster_sl_pct=0,3 Β· n_asset=2` β†’ disaster-SL ≀50% β‡’ **k ≀ 1,67** Β· peggior giorno +≀50% β‡’ **k ≀ 3,49** Β· ≀20% (scettico) β‡’ **k ≀ 1,40**; **1,50x β†’ peggior giorno 21,48% = BOCCIATO**; +1,25x β†’ costo disaster-SL 37,5%. Leva lorda a k=1 = **1,00x** (il test di guardia passa). + +### πŸ“Œ (7) MDE β€” quanti SCARTATI sono Β«non misurabileΒ» invece di Β«misurato e noΒ» + +Finestra della catena misurata dal critico: archivio **90 giorni** di calendario ma solo **52 con +β‰₯2 scadenze**, + **25** di raccolta propria = **77 giorni di superficie** (il registro dice 74-75 +βœ… da tre agenti). **MDE sul livello a 77 giorni = 4,3 di Sharpe** β‡’ ogni SCARTATO di +**Β§4/Β§5/Β§7/Β§11** che poggia su un confronto di Sharpe e' un **non-risultato su quell'asse** (le loro +parti valide sono lead-lag, placebo e censimenti, non gli Sharpe). +βœ… **A4 CONFERMATA in larga parte**: l'ondata dichiara il proprio MDE in Β§6, Β§10, Β§43, Β§48, Β§56 e in +Β§7 spiega *perche'* non ha girato il DSR β€” disciplina reale. +⚠️ **Tre etichette restano piu' forti della misura:** (a) **Β§48 chiude un FILONE** (Β«quinto lato del +DVOL chiusoΒ») su una branca dove dichiara esso stesso **un fattore 44 sotto l'MDE**; (b) i quattro +filoni-catena sopra; (c) 🚨 **Β§21 XS01-OOS, il PILASTRO del canale funded, ha effetto +1,12 contro +un MDE di 1,13** sui suoi 3 anni β€” il titolo sta **esattamente al proprio limite di rilevabilita'**, +e la Β«banda 10 fasiΒ» non e' un IC. *Nota di equita': Β§21 gira anche un null di permutazione a fee +zero (p=0,013), che ha piu' potenza del t-stat; il problema non e' Β«nessuna evidenzaΒ», e' che il +livello +1,12 viene propagato fino a P(β‰₯50/g) e al muro come se fosse noto a due cifre.* + +### βœ… (8) A credito dell'ondata β€” quattro auto-correzioni in 24 ore + +Β§45 Β«lo spot non si liquidaΒ» β†’ falsificato da Β§52; Β§39 Β«non esiste una linea USDC datataΒ» β†’ +falsificato lo stesso giorno; Β§8 Β«overlay non causaleΒ» β†’ **regola ritirata** dallo scettico; Β§24 +Β«dealer_net_gamma e' il GEX invertitoΒ» β†’ ritirata leggendo il sorgente. *E' il dato piu' sano +dell'ondata.* + +⚠️ **Errore del critico, catturato prima di pubblicare, e della famiglia che stava criticando:** la +prima stesura del punto (5c) de-luckava una serie **gia' de-luckata** (0,89Β²) e stampava un finto +**+31,5%** sul muro. **Il controllo che l'ha preso e' stato guardare il DRIFT stampato accanto al +muro, non il muro.** + +### πŸ“Œ La risposta al mandato β€” tutto in una sola unita' + +€/giorno a $635: + +| cosa | €/g | eseguibile oggi | +|---|---|---| +| gradino di leva 1,25x | **+0,046** | **NO** β€” il knob non esiste + GATE SCALA-01 | +| lead SPOT su TP01 | +0,017 | **NO** β€” domanda fiscale aperta | +| PREVDAY al 15% (de-luckato) | +0,014 | **NO** β€” GATE PREVDAY-01 al 2027-06-21 | +| BIN-FREQ | +0,011 | **NO** β€” 3 gate su 4 FAIL | +| MAKER | +0,005 | **NO** β€” IC contiene lo zero | +| **funding (costo scoperto)** | **βˆ’0,023** | **gia' subito** | + +**Somma di TUTTI i lead positivi, se fossero autorizzati e additivi (non lo sono, e nessuno lo e' +oggi): +0,036 €/giorno.** Sullo stesso motore e sugli **stessi percorsi**: **€250 β†’ €500 al mese +porta P(20a) dal 14% all'85% e la mediana da 23,4 a 17,3 anni.** + +🚨 **VERDETTO: `L'ONDATA NON HA PRODOTTO NIENTE CHE AVVICINI I 50 EUR/GIORNO.`** Ha prodotto (i) un +**costo gia' in essere** che porta P(20a) a €250/mese dal 49% al **14%** (il funding), (ii) **quattro +monitor rotti** che avrebbero fatto decidere un gate al contrario, (iii) un **punto singolo di +guasto** nella rete di sicurezza, (iv) **tre lead reali tutti bloccati da qualcosa che non e' la +ricerca**. **Il suo valore e' DIFENSIVO ed e' reale: ha impedito decisioni sbagliate.** Ma la +domanda posta ha risposta negativa, e la 56Βͺ ondata avra' la stessa risposta della 55Βͺ. +❌ **Nulla di sepolto sotto uno Β«SCARTATOΒ» merita di essere riesumato**: il candidato piu' forte non +promosso (PREVDAY) vale +0,014 €/g de-luckato e gira la cella sbagliata. + +--- + +## 58-69 β€” SESTA ONDATA (12 filoni) β€” verdetti RECUPERATI RIESEGUENDO GLI SCRIPT + +⚠️ **PROVENIENZA DEI NUMERI, dichiarata perche' e' diversa da quella di ogni altra sezione di questo +registro.** I dodici filoni furono aperti alle 04:24 del 23/08; gli agenti scrissero gli script fra le +04:39 e le 04:44 e la sessione si chiuse **prima** che i verdetti fossero consolidati qui β€” le dodici +righe della tabella restarono `_in corso_` e i messaggi finali degli agenti sono persi. +**I verdetti sopra NON vengono da quei messaggi: vengono da una riesecuzione dei dodici script** +(23/08, 07:38:50-08:06:46 UTC, sequenziale), i cui log stanno in `logs/r0823b/` β€” **che e' gitignored**, +quindi *questa tabella e' l'unica copia versionata di quei numeri*. Ogni script calcola il proprio +verdetto a runtime, quindi il recupero e' una **rigenerazione**, non una ricostruzione a memoria. +πŸ“Œ **La regola gia' scritta il 26/07 vale anche per un'ondata: un numero pubblicato deve avere uno +script committato che lo riproduce.** Qui e' stata la ragione per cui l'ondata non e' andata persa. + +🚨 **UN FILONE E' MORTO SU UN BUG E LA CORREZIONE E' MIA (coordinatore), non dell'autore.** +`r0823b_quasi_passati.py` (Β§68) terminava con `IndexError: Boolean index has wrong length: 958 instead +of 2720`: `Griglia.combo()` indicizzava sempre con `self.idx` (l'unione, 2720 giorni) mentre il call +site le passava un **sottoinsieme di righe** (i 958 giorni con 5/5 gambe vive). Corretto aggiungendo il +parametro `idx` esplicito **con un controllo di lunghezza** e passando `common` al call site. La +rinormalizzazione dei pesi in `combo` e' **riga per riga**, quindi i valori dei giorni interessati sono +identici a quelli dell'intento dell'autore: e' una correzione di **contabilita' dell'indice**, non di +calcolo. **Il verdetto di Β§68 va letto sapendo che gira su codice toccato dal coordinatore.** + +### Cosa tocca decisioni gia' prese (e non e' nella colonna Β«verdettoΒ») + +| Β§ | cosa cambia | dove stava scritto | +|---|---|---| +| **67** | 🚨 il **gate pre-registrato XSR01 del 23/10** legge un monitor tarato sul pavimento del venue **sbagliato**: C* vero **$15.000-20.000**, non i ~$3.000 riportati | il gate dice *Β«deploy solo se haircut a $5.000 <= 40%Β»* β€” la soglia di capitale su cui poggia e' misurata su un criterio piu' permissivo | +| **64** | la raccomandazione di Β§51 (**raccogliere la catena USDC**, +18% di chiamate/giro) **non e' giustificata dalla ragione che porta** | Β§51 la motivava col `f` di VRP01 Β«misurato sulla famiglia sbagliataΒ»; le due superfici sono la stessa | +| **60** | la politica multi-conto **MISTO**, scelta il 25/07, **non e' piu' l'ottimo**: oggi vince **MISTO-A** | CLAUDE.md, bullet Β«LA VIA: i 600 euro come BIGLIETTOΒ» | +| **63** | **domanda fiscale NUOVA** (payout di prop firm = lavoro autonomo?), **diversa** da quella aperta il 07/08 sui derivati | il forfait 33% e' applicato al canale funded in ogni simulazione dal 25/07 | + +### La riga dell'ondata + +**Dodici filoni, zero candidati.** Nessuno avvicina i 50 EUR/giorno, e la sintesi involontaria e' di +Β§61: **ottimizzare la politica di ri-acquisto dei biglietti vale +0,00 EUR/g, versare EUR 500/mese ne +vale +15,75** β€” rapporto che il filone stampa da solo. Β§58 aggiunge il pezzo che mancava a cinque +ondate di misure: **l'obiettivo del progetto ha due definizioni operative in uso** (flusso e ricchezza +sostenibile) **che danno 33,9% contro 0,33% sulla stessa domanda**, e finora non era mai stato detto +quale delle due si stesse ottimizzando. Il valore dell'ondata, come della quinta, e' **difensivo**: +Β§67 e Β§64 tolgono due appoggi a decisioni gia' prese, Β§69 toglie precisione a un numero gia' +pubblicato, e nessuno dei tre produce reddito. + +**Con questa sono 69 filoni e 0 candidati promossi.** I vincoli binding restano due: **il capitale che +entra** e **il conto che non sparisce**. diff --git a/docs/research/SPEC-scale-key.md b/docs/research/SPEC-scale-key.md new file mode 100644 index 0000000..b9fc593 --- /dev/null +++ b/docs/research/SPEC-scale-key.md @@ -0,0 +1,618 @@ +# SPEC β€” la CHIAVE DI SCALA del libro live (`book_scale_k`) + +**Filone SCALE-SPEC, ondata 2026-08-22, branch `research/wave-0822`.** +**Questo documento e' una SPECIFICA. Non e' stata implementata: `src/`, `config/`, `scripts/live/`, +`scripts/cron_*.sh` e `tests/` non sono stati toccati.** Il prototipo che dimostra la meccanica e' +`scripts/research/r0822e_scale_proto.py` (isolato, non importato da niente, non scrive nulla; la +sua sezione (0) verifica entrambe le cose a runtime invece di dichiararle). + +Nasce da `RESULTS-0822.md` Β§33 (WORST-DAY), che ha misurato che il gradino di leva e' l'unica leva +grande rimasta e che **C6 (processo) FALLISCE**: la regola del progetto β€” *Β«ogni cambio di SCALA +passa dal cap di config, non da `target_vol`Β»* β€” **non e' implementabile come scritta**, perche' il +cap e' un CLAMP e non un moltiplicatore. + +--- + +## 0. IL RISCHIO CHE QUESTA SPECIFICA DEVE RENDERE IMPOSSIBILE + +> **Una chiave di scala e' esattamente il tipo di parametro che si alza Β«solo un po'Β» dopo un mese +> buono.** E' un numero, sta in un file di config, non richiede di capire niente per cambiarlo, e il +> suo effetto e' immediato e piacevole. + +Il progetto ha regole contro questa forma di deriva su ogni altro asse β€” `weights_tilt_null` per i +pesi, gate pre-registrati per i candidati, `select_cell_insample` per le griglie, `anchor_luck_band` +per le ancore β€” e **nessuna per la scala**. Non per dimenticanza: **per aritmetica.** + +Lo **Sharpe e' invariante alla scala**. Misurato nel prototipo (sez. 7a), sulla serie vera del libro: + +| k | Sharpe | drift | vol | maxDD | g(k) esatto | +|---|---|---|---|---|---| +| 1,00 | **1,5051** | 17,11% | 11,37% | βˆ’12,17% | 17,90% | +| 1,25 | **1,5051** | 21,39% | 14,21% | βˆ’15,01% | 22,61% | +| 2,00 | **1,5051** | 34,22% | 22,74% | βˆ’23,10% | 37,24% | + +`deflated_sharpe` e `marginal_vs_tp01` leggono la prima colonna. **Non falliscono: non vedono la +variabile.** Un raddoppio della leva passa ogni gate esistente del progetto senza far scattare nulla, +e produce lo stesso identico numero di sintesi che il progetto usa per giudicare tutto il resto. + +Da qui discendono i tre vincoli non negoziabili del disegno: + +1. **il tetto sta nel CODICE, non in config** (una config puo' essere modificata da chiunque, in + qualunque momento, senza review; una costante di codice no); +2. **il tetto sta sul PRODOTTO, non sulla chiave** (`frac` e `scala` moltiplicano: un tetto sulla + sola chiave lascia aperta la porta accanto β€” vedi Β§4.2, tabella); +3. **il gate e' uno SCRIPT che stampa PASSA/FALLISCE, non un giudizio** (Β§6), e c'e' un + **sorvegliante giornaliero** che se ne accorge *dopo il fatto* se qualcuno lo salta (Β§6.4). + +⚠️ **Correzione dovuta al mandato di questo filone.** Il gradino non vale quello che dice la frase +che circola. `RESULTS-0822.md` Β§2 misura **1,00x β†’ 1,25x = 14,7a β†’ 12,9a**, e **1,00x β†’ 1,50x = +14,7a β†’ 11,6a**; il Β«~€300/mese di versamenti equivalentiΒ» appartiene al gradino **1,50x**. +Ricalcolato sulla tabella netta pubblicata (`r0807_piano_netto`: €500/m β†’ 14,7a, €800/m β†’ 11,4a, +interpolazione lineare **dichiarata**): + +| gradino | anni al capitale-rendita | versamento mensile equivalente | delta | +|---|---|---|---| +| 1,00x (oggi) | 14,7a | €500/m | β€” | +| **1,25x** | **12,9a** | ~€664/m | **+€164/m** | +| 1,50x | 11,6a | ~€782/m | +€282/m | + +L'interpolazione riproduce il Β«~€300/meseΒ» pubblicato per 1,50x (€282) β†’ Γ¨ un controllo, non solo +un conto. **Questa specifica autorizza il gradino a 1,25x e vale ~€165/mese, non ~€300.** + +--- + +## 1. COSA SPECIFICA, E COSA NON FA + +**Specifica:** dove va la chiave, in che ordine si applica rispetto al cap, quali guardie servono, +quali test devono esistere **prima** della modifica, quale gate deve essere superato per alzarla e +con quale cadenza si sorveglia. + +**NON fa:** non autorizza il gradino, non implementa niente, non tocca `config/live.json`. Con la +chiave assente o `= 1.0` il libro e' **bit-exact quello di oggi** (Β§5, test T7): la modifica e' +inerte finche' qualcuno non scrive un numero, e quel numero passa dal gate del Β§6. + +--- + +## 2. DOVE VA LA CHIAVE, ESATTAMENTE + +### 2.1 La catena, letta dal sorgente vero + +Il prototipo la stampa leggendo i file (sez. 2), cosi' le righe qui sotto sono verificabili e non +citate a memoria: + +| stadio | file:riga | cosa produce | +|---|---|---| +| segnale TP01 | `src/live/shadow.py:119` `targets = {a: tp.current_target(dfs[a])...}` | una **frazione** β‰₯0, long-flat | +| segnale SKH01 | `src/live/book.py:217` `skh = _skyhook_positions(...)` β†’ `:228` `sign = _skh_sign(st)` | un **segno** +1/βˆ’1/0 | +| cap per-asset | `src/live/book.py:107` `def _cap(...)` β†’ `:194` `cap = _cap(...)` | il **clamp**, legato al watermark | +| **TARGET NETTO** | **`src/live/book.py:138` `def book_net_target(...)`** β†’ **`:229` `net = book_net_target(tp_frac, sign, equity, cap)`** | **← QUI VA LA SCALA** | +| ordine | `src/live/book.py:153` `def build_book_order(...)` β†’ `:232` | un **delta** verso il target | +| invio | `scripts/live/book_execute.py:208` `trader.rebalance_signed(inst, net, mark, ...)` | il fill | + +### 2.2 La firma proposta + +```python +# src/live/book.py +def book_net_target(tp_frac: float, skh_sign: int, equity: float, cap: float, + weight: float = WEIGHT, scala: float = 1.0) -> float: + raw = weight * equity * (W_TP01 * max(tp_frac, 0.0) + W_SKH * float(skh_sign)) + return scala * max(-cap, min(cap, raw)) # <-- la scala e' FUORI dal clamp: vedi Β§3 +``` + +piΓΉ, accanto a `_cap()`, un lettore dedicato: + +```python +LEVA_LORDA_MAX = 1.25 # COSTANTE DI CODICE, non di config. Vedi Β§4.1. +SCALA_LADDER = (1.00, 1.25) # i soli valori esprimibili. Vedi Β§4.1 e Β§6. + +def _scala(real_equity, eq_fallback) -> float: + """Scala del libro. 1.0 (= il libro di oggi) in ogni stato degradato: vedi Β§4.3.""" + if real_equity is None or eq_fallback: + return 1.0 + ... # legge config, valida contro LEVA_LORDA_MAX e SCALA_LADDER, ALZA se fuori (Β§4.1) +``` + +e in `book_report`, una riga sotto `cap = _cap(...)`: + +```python +scala = _scala(sh.get("real_equity"), sh.get("eq_fallback")) +... +net = book_net_target(tp_frac, sign, equity, cap, scala=scala) +``` + +`book_report` deve **esporre `scala` nel dict di ritorno** accanto a `cap_per_asset`, e +`book_execute` deve **stamparla nella riga Β«sizing baseΒ»**: un parametro che agisce sul nozionale e +non compare nel report e' la forma esatta del difetto che questo progetto ha gia' pagato tre volte +(docstring che dichiarano una causalita' che il codice non ha). + +### 2.3 Perche' **li'** e non altrove β€” e la dimostrazione + +`book_net_target` e' **l'unico punto della catena in cui le due gambe si incontrano con una TAGLIA**: +sopra di lei TP01 da' una frazione e SKH01 da' **solo un segno**; sotto di lei si calcolano delta e +ordini. Percio' e' l'unico punto in cui un moltiplicatore e' un puro cambio di scala. + +Il prototipo (sez. 3) lo misura sulla funzione di produzione stessa β€” la copia e' **bit-exact** +(`max|copia βˆ’ produzione| = 0.00e+00` su 480 combinazioni): + +| k | solo TP01 (tp=1, sg=0) | solo SKH01 (tp=0, sg=1) | rapporto TP/SKH | peso implicito TP01 | +|---|---|---|---|---| +| 1,00 | 2.267,62 | 755,88 | **3,000000** | **0,750000** | +| 1,25 | 2.834,53 | 944,84 | **3,000000** | **0,750000** | +| 2,00 | 4.535,25 | 1.511,75 | **3,000000** | **0,750000** | + +`max|target(k) βˆ’ kΒ·target(1)| = 0.00e+00` su 90 combinazioni. **Il mix 75/25 e' invariante a ogni k: +e' una scala, non un tilt.** + +Le alternative, e perche' cadono: + +| dove | perche' NO | +|---|---| +| `TrendPortfolio.target_vol` | scala il **75%** del libro e lascia fermo il 25% β†’ e' anche un **tilt di pesi**, e **fallisce `weights_tilt_null`**. Misurato nel prototipo come **controllo positivo**: a k=1,25 il peso implicito di TP01 passa da 0,750 a **0,789**. | +| `_skyhook_positions` | non c'e' niente da scalare: produce un **segno**. | +| `WEIGHT` (=0,5) | e' lo **split BTC/ETH**, ed e' **condiviso** con `shadow_report`/`build_rebalance_order` (`shadow.py:144`) e con la dashboard, cioe' con il percorso **TP01-only**: cambiarlo cambierebbe anche un altro libro. E confonderebbe in un solo numero Β«quanti assetΒ» e Β«quanta levaΒ». | +| `W_TP01` / `W_SKH` | sono il **rapporto** 75/25 e sono legati per test ai pesi del backtest (`test_weights_match_backtest_sleeves` asserisce `W_TP01+W_SKH == 1.0` e la parita' con `deribit_book_sleeves`). Moltiplicarli entrambi romperebbe quella parita' e renderebbe impossibile distinguere scala e mix. | +| `build_book_order` | scalerebbe il **delta**, non il target: cambia la **velocita' di convergenza**, non la taglia, e il reconciler litigherebbe con se' stesso a ogni giro. | +| dentro `DeribitTrader` | il report, il dry-run e `book_executions.jsonl` stamperebbero un target **diverso** da quello eseguito. | + +--- + +## 3. L'INTERAZIONE COL CAP β€” il punto sottile + +### 3.1 Il fatto geometrico + +Il cap e' un **clamp**: `net = clamp(0.5Β·EΒ·(0,75Β·tp + 0,25Β·sg), Β±cap)` con `cap = fracΒ·E = 0,50Β·E`. +A piena convinzione (`tp=1, sg=+1`) il grezzo vale **esattamente** `0,5Β·EΒ·1,0 = 0,50Β·E = cap`: +**il clamp e' tangente al libro proprio nei giorni di massima esposizione.** + +### 3.2 I due ordini, misurati (prototipo sez. 4, su 5.648 giorni-asset veri) + +| k | ordine | clampati | quota | k_eff medio | **k_eff a piena convinzione** | lordo max | +|---|---|---|---|---|---|---| +| 1,25 | **PRIMA** del clamp | 32 | 0,57% | 1,2487 | **1,0000** | 1,000 | +| 1,25 | **DOPO** il clamp | 0 | 0,00% | **1,2500** | **1,2500** | 1,250 | +| 2,00 | PRIMA | 793 | 14,04% | 1,9235 | **1,0000** | 1,000 | +| 2,00 | DOPO | 0 | 0,00% | **2,0000** | **2,0000** | 2,000 | + +**L'ordine corretto e' DOPO il clamp**, e la ragione non e' di comodo: + +- **PRIMA**: il clamp mangia la scala **proprio nei giorni di massima convinzione** (k_eff = 1,00 a + piena convinzione, a ogni k). Il risultato non e' Β«il libro a leva kΒ»: e' un libro che alza i + giorni piccoli e lascia fermi i grandi, cioe' un **appiattimento del profilo di convinzione**. E' + un cambio di **FORMA** travestito da cambio di taglia β€” e allora **la curva g(k) di + `r0822_growth_policy` / WORST-DAY non descrive quel libro**, perche' quella curva e' calcolata su + `kΒ·r`, cioe' sull'ordine DOPO. Autorizzare il gradino con una curva e implementarne un'altra e' + il modo piu' silenzioso di sbagliare che questa specifica possa produrre. +- **DOPO**: `k_eff = k` esattamente, in ogni giorno e in ogni stato del segnale. E' l'unico ordine + per cui Β«leva kΒ» significa la stessa cosa nel codice e nel backtest. + +### 3.3 Cosa implica per il SIGNIFICATO del cap β€” e va detto, perche' e' il prezzo + +Con l'ordine DOPO, **il cap smette di essere il tetto assoluto del nozionale**. Diventa il tetto +del **libro unitario**: limita la *forma* (nessun asset oltre `fracΒ·E` di esposizione unitaria), +mentre la *taglia* la decide la scala. Il lordo massimo passa da `n_assetΒ·frac` a +**`n_assetΒ·fracΒ·scala`** = 1,250x l'equity a k=1,25. + +E il cap **non smette di fare le altre due cose che fa oggi**, che sono quelle che contano davvero: + +1. resta **proporzionale all'equity** (`cap = equityΒ·frac` sul percorso fidato) β†’ un deposito non + strozza il libro e un prelievo lo rimpicciolisce da solo; +2. resta **legato al watermark** sul percorso di fallback (`min(fisso, watermarkΒ·frac)`) β†’ il caso + che il 26/07 valeva Β«3,35x di leva su un conto da $597Β» resta chiuso (Β§4.3). + +⚠️ **Cio' che il cap NON puo' piu' fare da solo e' essere la guardia di sicurezza sulla leva +lorda.** Quella guardia va ricostruita esplicitamente, ed e' il Β§4. + +### 3.4 Il test che si rompe β€” e **deve** rompersi + +`tests/test_fee_sensitivity.py::test_leva_massima_da_config_resta_sotto_o_uguale_a_1x`: + +```python +leva_max = frac * len(FS.ASSETS) +assert leva_max <= 1.0, ("leva massima ...: la liquidation fee 1% torna rilevante, " + "rivedere r0726_fee_sensitivity e il diario 2026-07-26-fee-deribit.md") +``` + +**Si rompe? No β€” e questo e' il punto.** Con `frac` invariato a 0,50 il test continua a **passare**, +perche' misura `frac Β· n_asset` mentre la grandezza vera diventa `frac Β· n_asset Β· scala`. +**Non si rompe, e smette di controllare cio' che era stato scritto per controllare.** + +E' esattamente la forma di difetto che il progetto ha catturato **tre volte in un giorno** il +2026-08-21: *un controllo puntato su una configurazione diversa da quella che gira passa sempre, e +non sta controllando niente* (`fee_watch` sugli inverse, `venue_watch` con bitfinex, i test di +`book_live` a libro flat). + +**Quindi va sostituito, non aggiustato**, e la sostituzione e' una coppia: + +- **T2** (Β§5) β€” `test_la_leva_lorda_da_config_resta_sotto_il_tetto`: asserisce + `len(ASSETS) * frac * scala <= LEVA_LORDA_MAX`, cioe' **il prodotto**, e importa la scala **dallo + stesso posto da cui la importa la produzione** (regola del 21/08: *un sorvegliante deve DERIVARE + il proprio bersaglio dal codice sorvegliato, mai ridichiararlo*). +- **prerequisito del gate** (Β§6): `r0726_fee_sensitivity` va **rifatto** al nuovo lordo, perche' la + sua conclusione *Β«liquidation fee 1% irrilevanteΒ»* era esplicitamente condizionata a nozionale + lordo ≀1x. Al gradino il lordo e' 1,25x β†’ una liquidazione costerebbe **1,25%** dell'equity, non + 1,00%. Non e' un problema; e' un numero che va **ricalcolato e ripubblicato**, non ereditato. + +Il vecchio test va **cancellato con una nota** che dice perche' (e rimandi a questo documento), non +modificato in silenzio: un test di decisione che cambia soglia senza lasciare traccia e' un test di +decisione perso. + +--- + +## 4. LE GUARDIE + +### 4.1 G1 β€” il tetto: **sul PRODOTTO, in CODICE, e non silenzioso** + +```python +LEVA_LORDA_MAX = 1.25 # src/live/book.py β€” costante di CODICE +SCALA_LADDER = (1.00, 1.25) +``` + +Tre proprieta', tutte necessarie: + +**(a) Sul prodotto.** `frac` e `scala` **moltiplicano**. Un tetto sulla sola chiave lascia aperta la +porta accanto (prototipo sez. 6): + +| frac | scala | leva lorda | tetto Β«scala ≀ 1,25Β» | **tetto sul PRODOTTO** | +|---|---|---|---|---| +| 0,500 | 1,25 | 1,250 | passa | passa | +| **0,625** | **1,25** | **1,562** | **passa** | **BLOCCA** | +| **0,750** | **1,25** | **1,875** | **passa** | **BLOCCA** | +| 0,500 | 2,00 | 2,000 | BLOCCA | BLOCCA | + +Ed e' un rischio **concreto, non teorico**: WORST-DAY ha appena pubblicato che Β«alzare `frac` da +0,50 a 0,625 non produce k=1,25Β», il che rende `frac` un candidato naturale al prossimo ritocco β€” +salvo che, **combinato con la scala, lo produce eccome**. + +**(b) In codice.** Il tetto di config sarebbe modificabile dalla stessa mano, nello stesso file, nel +momento in cui si modifica la chiave: sarebbe un lucchetto con la chiave attaccata. Con +`LEVA_LORDA_MAX` in `src/live/book.py`, il gradino **1,25 β†’ 1,50 richiede una modifica di codice**, +quindi una review, quindi un gate. **Il tetto e' basso apposta: e' il meccanismo del cricchetto, non +un'opinione sulla leva massima sopportabile** (che dalla Β§4.5 sarebbe 1,67x). + +**(c) Non silenzioso.** Una scala fuori tetto (o fuori `SCALA_LADDER`) **NON viene tagliata al +tetto**: `book_execute` **si ferma, non invia ordini, e allerta**. Un clamp silenzioso farebbe +girare una config che dichiara un numero e un libro che ne esegue un altro β€” la stessa classe di +difetto del Β§3.4. `SCALA_LADDER` esiste perche' **una scala a gradini rende inesprimibile Β«solo un +po'Β»**: 1,05 non e' un valore prudente, e' un valore fuori scaletta, e viene rifiutato. + +### 4.2 G2 β€” l'invariante che lega TRE chiavi + +```python +assert len(ASSETS) * frac * scala * disaster_sl_pct <= 0.50 +``` + +Vedi Β§4.5: e' la guardia che morde per prima, e cattura chi allarga il disaster-SL. + +### 4.3 G3/G4 β€” il watermark e l'equity non leggibile + +**Regola proposta: la scala vive SOLO sul percorso fidato.** + +```python +scala_effettiva = scala se l'equity REALE e' leggibile, altrimenti 1.00 +``` + +Motivo: **una scala e' una decisione di rischio presa conoscendo il conto; quando non si sa quanto +vale il conto, non si prende.** Cosi' Β«il fallback non e' piu' permissivoΒ» e' vero **per +costruzione**, non per aritmetica β€” e non dipende dal fatto che il conto degli invarianti torni. + +Misurato (prototipo sez. 6-bis; watermark reale letto da disco, $634,996; copia di `_cap` +verificata bit-exact contro la produzione): + +| stato | cap | scala eff. | lordo max | **/ equity vera** | tetto | +|---|---|---|---|---|---| +| normale (equity reale letta) | 317,50 | **1,25** | 793,74 | **1,250x** | si | +| online ma equity illeggibile | 317,50 | **1,00** | 635,00 | **1,000x** | si | +| watermark assente (1Β° avvio) | 300,00 (`CAP_UNKNOWN_USD`) | **1,00** | 600,00 | 0,945x | si | +| watermark vecchio e basso | 50,00 | **1,00** | 100,00 | 0,157x | si | +| dopo un deposito a $6.047 | 3.023,50 | **1,25** | 7.558,75 | 1,250x | si | + +**In nessuno stato degradato la leva supera quella dello stato normale**, e il caso Β«watermark +assenteΒ» ha **due protezioni indipendenti** (cap $300 **e** scala 1,00), non una sola. + +⚠️ **Prezzo dichiarato.** Nei giri in cui l'equity non e' leggibile il libro gira a k=1 e al giro +dopo torna a k β†’ un ribilancio in piu' per oscillazione, di `(kβˆ’1)Β·fracΒ·E` per asset = **~$79 a +testa** sul conto di oggi, cioe' **~$0,056** di fee a 3,5 bps. **Frequenza misurata** su +`logs/cron_book.log`: **0 giri su 1.442 (0,00%)**; i 21 giri Β«conto non leggibileΒ» sono OFFLINE, e +li' il libro non manda ordini affatto, quindi la scala non si pone. β†’ **il prezzo esiste, e' noto, +ed e' nullo sul campione osservato.** +⚠️ **E questo e' anche il limite dichiarato della regola:** se un giorno il ramo di fallback +diventasse frequente, il libro girerebbe a una leva **mista** e la curva `g(k)` smetterebbe di +descriverlo. β†’ **il sorvegliante del Β§6.4 deve riportare quella frequenza**, e se supera il **2% +dei giri in 90 giorni** la regola va ridiscussa (soglia dichiarata qui, prima di vedere il caso). + +**Alternativa scartata:** congelare l'ultimo target scalato durante il fallback. Aggiunge **stato** +a un percorso con soldi veri β€” cioe' un nuovo modo di essere sbagliati β€” per risparmiare $0,056. + +### 4.4 G5 β€” il `min_order_usd` non e' un problema, e va detto + +La scala **aumenta** i delta, quindi allontana dal pavimento $5 invece di avvicinarcisi. Nessuna +azione. (Lo si dice perche' Β«non e' un problemaΒ» misurato vale quanto un problema trovato: evita +che la domanda torni.) + +### 4.5 G6 β€” il disaster-SL: **la guardia che morde per prima, e non era stata calcolata** + +Il disaster-SL e' un movimento di **prezzo** (βˆ’30% sul mark), non una perdita in equity. L'impatto +in equity e' quel movimento **moltiplicato per il lordo**, quindi **cresce con k anche se +`disaster_sl_pct` non cambia**. Prototipo sez. 6, tutte le righe scalate dai numeri **replicati** +di WORST-DAY (Β§7): + +| k | lordo max | A) peggior giorno possibile | **B) costo disaster-SL** | C) x_liq | min intraΒ·k | margine C | +|---|---|---|---|---|---|---| +| 1,00 | 0,864 | 14,32% | **30,00%** | 115,8% | 7,33% | 15,8x | +| **1,25** | **1,081** | **17,90%** | **37,50%** | **92,5%** | **9,16%** | **10,1x** | +| 1,50 | 1,297 | 21,48% | 45,00% | 76,9% | 11,00% | 7,0x | +| 2,00 | 1,729 | 28,64% | **60,00%** ← sfonda | 57,4% | 14,66% | 3,9x | + +πŸ“Œ **La guardia che morde per prima NON e' il peggior giorno possibile, e' il disaster-SL:** + +- da (A): `k ≀ 50% / 14,32%` = **3,49x** +- da (B): `k ≀ 50% / (2 Β· 0,50 Β· 30%)` = **1,67x** ← **2,1x piu' stringente** + +A k=1,25 il margine di (A) e' **2,8x** (il numero che WORST-DAY pubblica come C1) mentre quello di +(B) e' **1,33x**. **Il criterio C1 di WORST-DAY guardava la grandezza sbagliata**: guardava il +peggior giorno del *segnale*, non il costo del dispositivo di emergenza che il libro ha davvero +armato sul conto. Da qui l'invariante G2 (Β§4.2), che con `sl_pct=30%` e `frac=0,50` da' +**scala ≀ 1,67x** β€” e che scatta se qualcuno allarga il disaster-SL. + +**Decisione: `disaster_sl_pct` NON si tocca.** Stringerlo a 24% per tenere invariante l'impatto in +equity lo trasformerebbe da dispositivo di catastrofe in **stop di strategia**: verrebbe toccato dal +rumore, e il progetto ha gia' misurato cosa succede quando uno stop stretto incontra un gap (SKH01, +`sl 2% modellato β†’ βˆ’11/βˆ’23% realizzato`). Si accetta il 37,5% e **lo si scrive**. + +⚠️ **Osservazione da LETTURA DEL CODICE, non misurata, e va citata cosi'.** +`src/live/execution.py:196-223` **ri-ancora** il disaster-SL al mark a ogni giro: se lo stop esistente +dista piu' del **5%** relativo da `markΒ·(1βˆ’sl_pct)`, viene **cancellato e ripiazzato**. Per un long +questo significa che lo stop **segue il prezzo anche in discesa**: il βˆ’30% e' un βˆ’30% **rotolante +dall'ultimo piazzamento**, non un pavimento assoluto sul trade. Quindi **B e' il costo di UN +episodio**, e un ribasso lento puo' produrne piu' d'uno. +Evidenza circostanziale, non prova: nel log ci sono **15 `placed` contro 259 `ok`** su asset con +posizione aperta β€” coerente con qualche ri-ancoraggio, ma i `placed` includono anche le prime +aperture e **non sono separabili dal log**. β†’ **e' una proprieta' di oggi che il gradino amplifica, +non una che il gradino crea**, ed e' un follow-up con una domanda precisa: *quante volte il bracket +e' stato ri-piazzato mentre la posizione restava aperta?* (misurabile aggiungendo `pos_prima` al log, +non ricostruibile da quello attuale). + +--- + +## 5. I TEST CHE DEVONO ESISTERE **PRIMA** DELLA MODIFICA + +Scritti come specifiche eseguibili: **nome + cosa asserisce + il caso che deve far fallire.** +File proposto: `tests/test_book_scale.py` (piu' le modifiche dichiarate a +`tests/test_fee_sensitivity.py`). + +| # | test | asserisce | **deve fallire se** | +|---|---|---|---| +| **T1** | `test_la_scala_e_identica_sui_due_sleeve` | su una **griglia di k** (1,00/1,10/1,25/1,50/2,00) Γ— tp_frac Γ— segno, con cap disattivato: `target(k) == kΒ·target(1)` **e** `target(tp=1,sg=0)/target(tp=0,sg=1) == W_TP01/W_SKH` costante | qualcuno applica la scala a **una sola** gamba (o dentro `TrendPortfolio`) | +| **T1b** | `test_il_test_di_simmetria_smaschera_una_scala_asimmetrica` | **CONTROLLO POSITIVO**: la stessa asserzione, applicata a un'implementazione asimmetrica scritta nel test, **fallisce** | il test T1 gira **solo a k=1** (dove le due implementazioni **coincidono**: potenza **zero** β€” misurato nel prototipo) | +| **T2** | `test_la_leva_lorda_da_config_resta_sotto_il_tetto` | `len(ASSETS) * frac * scala <= LEVA_LORDA_MAX`, con `frac` e `scala` **letti da `config/live.json`** e `LEVA_LORDA_MAX`/`ASSETS` **importati dal modulo di produzione** | qualcuno alza `frac`, o la scala, **o entrambe di poco** | +| **T3** | `test_l_invariante_disaster_sl_tiene_meta_equity` | `len(ASSETS) * frac * scala * disaster_sl_pct <= 0.50` | qualcuno allarga `disaster_sl_pct` **o** alza la scala fino a 1,67x | +| **T4** | `test_il_fallback_non_e_piu_permissivo_del_percorso_fidato` | in **ogni** stato degradato (equity illeggibile / watermark assente / watermark vecchio) il lordo massimo **/ equity vera** ≀ quello dello stato normale | qualcuno applica la scala anche in fallback, o slega il cap dal watermark | +| **T5** | `test_il_cap_continua_a_limitare_cio_che_limitava` | il target per-asset non supera mai `capΒ·scala`; e il clamp **morde ancora** sulle stesse osservazioni di prima (le 3 su 5.650) | qualcuno inverte l'ordine (Β§3.2) o toglie il clamp Β«tanto c'e' la scalaΒ» | +| **T6** | `test_una_scala_fuori_tetto_ferma_l_esecuzione_e_non_viene_tagliata` | con `scala` fuori `SCALA_LADDER` o fuori tetto: `book_execute` **non invia**, allerta, e il valore **non** viene silenziosamente clampato | qualcuno Β«rende robustoΒ» il lettore con un `min(scala, LEVA_LORDA_MAX/…)` | +| **T7** | `test_senza_la_chiave_il_libro_e_quello_di_oggi` | con chiave assente **o** `= 1.0`, `book_net_target` e' **bit-exact** alla versione attuale su una griglia (`max|diff| == 0.0`) | la modifica cambia qualcosa **prima** che qualcuno decida di cambiarlo | +| **T8** | `test_il_report_e_il_log_dichiarano_la_scala` | `book_report()['scala']` esiste e `book_execute` la stampa | un parametro che muove il nozionale resta invisibile nel report | +| **T9** | `test_la_scala_e_derivata_dal_codice_non_ridichiarata` | il sorvegliante e i test importano `LEVA_LORDA_MAX`, `ASSETS`, `SCALA_LADDER` **da `src/live/book`** e non li riscrivono | due liste in due file (regola del 21/08, 4 occorrenze) | +| **T10** | `test_il_sorvegliante_segnala_una_scala_non_autorizzata` | **CONTROLLO POSITIVO** del Β§6.4: con una config che dichiara una scala assente dal giornale, il sorvegliante **allerta**; con config e giornale concordi, **tace** | il sorvegliante e' tarato per non segnalare mai (regola del 26/07: *un rilevatore tarato per non segnalare e' indistinguibile da uno rotto*) | +| **T11** | `test_il_vecchio_test_di_leva_e_stato_sostituito_non_rilassato` | `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` **non esiste piu'** e T2/T3 esistono | qualcuno alza la soglia del vecchio test da 1,0 a 1,25 e chiude li' | + +⚠️ **T1b e' il test piu' importante dell'elenco**, ed e' la ragione per cui questo paragrafo esiste: +il prototipo (sez. 3) mostra che **a k=1 l'implementazione simmetrica e quella asimmetrica danno lo +stesso identico numero**. Un test di simmetria scritto sul caso di default passa sempre e non +controlla niente β€” la stessa firma dei test di `book_live` scoperti senza potenza a libro flat. + +--- + +## 6. IL GATE MANCANTE β€” `GATE SCALA-01` + +### 6.1 Perche' serve un gate NUOVO + +Vedi Β§0: nessun gate esistente vede la scala. Il gate deve quindi vivere sull'unico asse su cui la +scala si vede: **crescita e rovina** (`g(k)`, maxDD, distanza dalla liquidazione), non Sharpe. + +### 6.2 Le condizioni β€” tutte necessarie + +Per portare `book_scale_k` da `k0` a `k1`: + +| | condizione | misurata come | +|---|---|---| +| **A1** | `k1` e' il **gradino successivo** di `SCALA_LADDER` (1,00 β†’ 1,25). Nessun valore intermedio. | confronto con la costante di codice | +| **A2** | `k0` e' in produzione da **β‰₯180 giorni** con il criterio passato **ogni giorno** | giornale `data/live/scale_history.jsonl` (append-only) + registrazioni del sorvegliante | +| **A3** | **C1–C5 di WORST-DAY** ricalcolati **oggi** a `k1`, tutti PASSA β€” non ereditati dal 22/08 | ri-esecuzione di `r0822c_worst_day` sui dati correnti | +| **A4** | **C7 = G2**: `n_asset Β· frac Β· k1 Β· disaster_sl_pct ≀ 0,50` | Β§4.5 | +| **A5** | **C8 = G1**: `n_asset Β· frac Β· k1 ≀ LEVA_LORDA_MAX` | Β§4.1 | +| **A6** | **ANTI-RECENCY**: il criterio deve passare **anche escludendo gli ultimi 90 giorni**: `k_ammesso(r[:-90]) β‰₯ k1` | Β§6.3 | +| **A7** | `r0726_fee_sensitivity` **rifatto** al nuovo lordo e la conclusione Β«liquidation fee irrilevanteΒ» **ri-derivata o ritirata** | Β§3.4 | +| **A8** | la modifica e' **una riga di config + una riga di giornale**, zero codice. Se serve toccare `LEVA_LORDA_MAX`, **non e' una decisione di config** | diff | +| **A9** | i test T1–T11 (Β§5) esistono e passano **prima** | pytest | + +### 6.3 A6 β€” l'anti-recency, e come si misura davvero + +La forma verificabile e': **il gradino dev'essere autorizzato anche dai dati che non contengono la +finestra recente che ha motivato la richiesta.** E' l'analogo, sull'asse della scala, di +`select_cell_insample`. + +Il prototipo (sez. 7b) misura la meccanica su `k_ammesso = Β½Β·k*(drift a βˆ’2 SE, coda troncata al +bound strutturale)`: + +| finestra | n | drift punto | drift βˆ’2SE | k ammesso | β‰₯1,25x? | +|---|---|---|---|---|---| +| tutta la storia | 2.719 | 17,11% | 8,78% | **3,52** | SI | +| esclusi ultimi 90 g | 2.629 | 16,70% | 8,15% | **3,20** | SI | +| esclusi ultimi 365 g | 2.354 | 19,52% | 10,20% | 3,80 | SI | +| solo ultimi 365 g | 365 | 1,59% | βˆ’13,69% | 0,03 | NO | + +e la sensibilita' a un tratto recente **artificialmente buono** (ultimi 90 giorni riscritti a un +drift dato, sull'intera storia): + +| ultimi 90 g portati a | drift βˆ’2SE | k ammesso | delta | +|---|---|---|---| +| 0%/anno | 7,82% | 3,12 | βˆ’0,40 | +| +50%/anno | 9,47% | 3,80 | +0,28 | +| **+100%/anno** | 11,11% | **4,45** | **+0,93** | +| +200%/anno | 14,35% | 5,63 | +2,10 | + +**Il βˆ’2 SE fa gran parte del lavoro** (un tratto eccezionale alza insieme il drift *e* la sua +incertezza), **ma non basta**: il criterio resta una funzione dei dati recenti, e la protezione vera +e' la **doppia valutazione** di A6. + +⚠️ **Tre onesta' obbligatorie su questa sezione:** +1. `k_ammesso` del prototipo e' **deliberatamente semplificato** (troncatura al bound, nessuna GPD): + serve a mostrare la **meccanica**, non a produrre il numero. Il gate reale usa la versione di + `r0822c_worst_day`. +2. **Oggi A6 non morde**: `k_ammesso β‰ˆ 3,5x` contro un gradino di 1,25x. Il vincolo che morde e' + quello del Β§4.5 (disaster-SL). **A6 serve al gradino successivo, non a questo** β€” e va scritto + ora, quando non e' comodo per nessuno, non quando servira'. +3. La prima stesura di questa sezione presentava Β«solo ultimi 365 giorniΒ» come l'esempio di *Β«un anno + buonoΒ»*: **falso**, quell'anno e' stato debole (drift 1,59%). Quella riga mostra solo che un + campione corto ha un SE enorme. La riga che risponde alla domanda vera e' la tabella dello shock. + +### 6.4 La cadenza β€” `scale_watch` + +Sorvegliante giornaliero (proposto: `src/live/scale_watch.py` + `scripts/live/scale_watch.py` in +`cron_daily.sh`; **non implementato**), che risponde a **tre domande diverse** β€” e sono davvero tre, +perche' hanno **tre azioni diverse** e nessuna e' deducibile dalle altre: + +| domanda | esito | azione | +|---|---|---| +| la config dichiara una scala che il **giornale** non ha mai autorizzato? | 🚨 | ricostruire come ci e' arrivata; scendere a `k0` | +| il criterio che autorizzo' la scala **corrente** passa ancora oggi? | 🚨 | **scendere di un gradino** | +| il **prodotto** `fracΒ·scalaΒ·n_asset` ha superato `LEVA_LORDA_MAX`? | 🚨 | bloccare l'esecuzione | + +Piu' due requisiti che il progetto ha gia' imparato a pagare: + +- **tre stati, non due**: `OK` / `ALLARME` / **`NON MISURABILE`** (serie troppo corta, dati mancanti). + *Β«Non vedoΒ» non e' Β«va tutto beneΒ»* β€” `paper_dvolspread`, `venue_watch`. +- **disciplina degli allarmi**: una allerta per streak (`lock_step`), non una per giro; e il + messaggio deve dire **cosa** (decide) **e perche'** (ripara), con il valore **letto**, mai cablato. + +E deve **riportare la frequenza del ramo di fallback** (Β§4.3), che oggi e' 0/1.442 e che se salisse +sopra il **2% in 90 giorni** invalida la regola Β«scala solo sul percorso fidatoΒ». + +⚠️ **Cio' che il sorvegliante NON puo' fare: impedire la modifica.** Chi ha accesso al file puo' +scriverci dentro. Il sorvegliante rende la modifica **visibile entro 24 ore** e **attribuibile**. +E' la stessa onesta' di `venue_watch`: non protegge il saldo, compra tempo. + +--- + +## 7. IL COSTO DI NON FARLO, E IL RISCHIO DI FARLO + +### 7.1 Il costo di restare a 1,00x + +**~€165/mese di versamenti equivalenti, a costo zero, per sempre** (Β§0): 14,7a β†’ **12,9a** al +capitale-rendita a €500/m netto fisco. Sulla scala delle leve misurate dal progetto sta **sotto** +Β«versare o noΒ» (da *mai* a 16 anni) e Β«quando versareΒ» (fattore 6), e **sopra** tutto cio' che la +ricerca ha prodotto in due mesi: l'ottimizzazione del peso SKH01 vale **+0,030 di Sharpe** e ha +fallito il suo gate; il gradino vale **1,8 anni**. + +Ed e' **l'unica leva grande che non richiede soldi nuovi**. + +### 7.2 Il rischio di farlo, in una tabella + +| grandezza | k=1,00 | **k=1,25** | fonte | +|---|---|---|---| +| peggior giorno **possibile** (chiusura) | 14,32% | **17,90%** | replicato, Β§4.5 | +| peggior giorno **vissuto** (chiusura) | 3,95% | 4,94% | replicato | +| peggior **minimo** intra-giorno | 7,33% | 9,16% | WORST-DAY (lente accoppiata) | +| **costo di un disaster-SL** | 30,0% | **37,5%** | Β§4.5 ← **la guardia che morde** | +| margine di liquidazione | 15,8x | 10,1x | Β§4.5 | +| maxDD storico | 12,17% | 15,01% | Β§0 | +| costo funding non modellato | ~1,53%/a | ~1,91%/a | FUNDING Β§35 | +| costo di una liquidazione (fee 1%) | 1,00% eq | 1,25% eq | Β§3.4, da ricalcolare | + +### 7.3 La riga che WORST-DAY ha dichiarato, e che va tenuta in cima alla decisione + +> Se si credono **entrambe** le riserve insieme β€” drift a βˆ’2 SE **e** un giorno a βˆ’10% ogni anno β€” +> il drift atteso e' **βˆ’0,61%/anno**, e **a quel punto non e' il gradino a essere sbagliato, e' il +> libro**: chi usa quella combinazione sta chiedendo di spegnere anche il conto di oggi. + +Questa specifica non risolve quel dilemma e non deve: lo rende **esplicito**. Chi rifiuta il gradino +in nome di entrambe le riserve deve dire cosa propone di fare del libro a k=1. + +### 7.4 Cosa questa specifica **non** rende sicuro + +- **La lente e' close-only** dove non usa i numeri intra-giorno di WORST-DAY. Su una regola a **un + giorno** close-only e' **esattamente cieca** (25/07, riconfermato dallo scettico il 22/08). Sul + canale **prop/funded** β€” dove il vincolo binding e' una barriera di DD giornaliera β€” **questa + specifica non si applica** e la lente accoppiata resta obbligatoria. +- **`g(k)` assume ritorno indipendente dalla size.** A $635 e' innocuo (SLIP-AUDIT: nessun impatto + misurabile, e un fill ha preso il 21,9% del volume della sua barra 5m in un range di 1,08 bps), ma + **quella misura ha una data di scadenza** β€” la partecipazione scala lineare col capitale. +- **Il funding non e' in nessuna serie di questo documento.** Il libro paga gia' ~1,5%/anno che + nessun backtest sottrae; il gradino lo amplifica del 25%. E' dentro C5, ma il **livello a k=1** + resta un difetto aperto del progetto, non di questa specifica. +- **La leva non compra probabilita', compra fretta** (PROP-ALLOC). Il gradino accorcia la mediana; + non alza `P(arrivare)` in modo proporzionale, e **alza la varianza del percorso**. + +--- + +## 8. PROCEDURA DI ATTIVAZIONE (checklist, in ordine) + +1. Scrivere `tests/test_book_scale.py` con T1–T11 (Β§5). **Prima.** T7 deve passare **contro il + codice attuale** (la chiave non esiste ancora β†’ default 1,0 β†’ bit-exact). +2. Implementare Β§2.2 (`scala` in `book_net_target`, `_scala()`, `LEVA_LORDA_MAX`, `SCALA_LADDER`, + `scala` nel report e nel print di `book_execute`). **Senza toccare `config/live.json`**: la chiave + assente vale 1,0, il libro non cambia, i test lo dimostrano. +3. Cancellare `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` **con la nota** (Β§3.4). +4. Scrivere `scale_watch` (Β§6.4) + `data/live/scale_history.jsonl` con la riga iniziale + `{"scala": 1.00, "dal": "...", "gate": "iniziale, nessun gradino"}`, e aggiungerlo a + `cron_daily.sh` **e** al backup di `/opt/docker/scripts/backup.sh` (il giornale non e' ricostruibile). +5. **Lasciare girare a 1,00 per almeno 30 giorni** con il sorvegliante attivo. Una manopola nuova si + guarda funzionare **prima** di girarla. +6. Solo allora: eseguire `GATE SCALA-01` (Β§6.2). Se PASSA, una riga di config + una di giornale. +7. Rifare `r0726_fee_sensitivity` al nuovo lordo (A7) e ripubblicare la conclusione. + +**Il punto 5 non e' burocrazia: e' l'unico modo di scoprire che il sorvegliante e' rotto mentre la +leva e' ancora 1,00.** + +--- + +## 9. NODI APERTI β€” dichiarati, non nascosti + +1. **Il disaster-SL rotolante** (Β§4.5, ⚠️): letto nel codice, non misurato. Il log **non permette** + di separare i ri-piazzamenti dalle prime aperture. Se davvero rotola, `B` e' il costo di *un* + episodio e un ribasso lento ne produce piu' d'uno. **E' il nodo piu' serio di questo documento**, + perche' tocca la guardia che morde per prima. Follow-up: aggiungere `pos_prima` al log di + `ensure_disaster_sl` e ri-misurare β€” **non ricostruibile a posteriori**. +2. **`LEVA_LORDA_MAX = 1.25` e' una scelta di processo, non una misura.** Dall'invariante del Β§4.5 il + massimo sostenibile sarebbe 1,67x. Il valore basso e' il cricchetto. Chi vuole 1,50x deve toccare + codice β€” **ed e' voluto**. +3. **La banda d'ancora non e' stata girata su nessun numero di questo documento.** Tutte le serie + sono all'ancora canonica con la correzione Γ—0,89 sul drift. Le grandezze usate per decidere + (bound strutturale, lordo, invarianti) sono **aritmetiche** e non ancorate; `g(k)` e `k_ammesso` + **lo sono** β€” e vanno lette con la banda del libro (Sharpe FULL de-luckato 1,95, banda [1,81; 2,12]). +4. **Il gradino non e' stato autorizzato qui.** Questo documento specifica come si autorizza. + +--- + +## 10. IL PROTOTIPO + +`scripts/research/r0822e_scale_proto.py` β€” **0,9 s**, nessuna rete, nessuna scrittura. + +``` +nice -n 19 timeout 900 uv run python scripts/research/r0822e_scale_proto.py +``` + +Sezioni: (0) isolamento verificato Β· (1) sanity bit-exact vs produzione Β· (2) la catena letta dal +sorgente Β· (3) simmetria + controllo positivo Β· (4) l'ordine col clamp Β· (5) replica di WORST-DAY Β· +(6) guardie Β· (6-bis) stati dell'equity Β· (7) il gate mancante. + +**Replica a k=1,00 (sez. 5) β€” il motivo per cui le righe scalate di questo documento non sono +numeri copiati:** + +| grandezza | prototipo | pubblicato | scarto | +|---|---|---|---| +| drift de-luckato Γ—0,89 | 17,11% | 17,11% | 0,00% | +| vol annua | 11,37% | 11,37% | βˆ’0,00% | +| Sharpe de-luckato | 1,505 | 1,510 | βˆ’0,005 | +| peggior giorno (chiusura) | βˆ’3,95% | βˆ’3,94% | βˆ’0,01% | +| data del peggior giorno | **2020-03-13** | 2020-03-13 | coincide | +| morso del clamp | **3 / 5.648** | 3 / 5.650 | 2 righe di bordo* | +| bound strutturale | **βˆ’14,32%** | βˆ’14,32% | 0,00% | + +\* differenza di costruzione: il prototipo scarta la prima barra per asset (serve `vol_prev`). + +--- + +*Documento scritto senza modificare nulla in produzione. Il branch `research/wave-0822` e' quello da +cui il cron esegue: ogni riga di `src/`, `config/`, `scripts/live/`, `scripts/cron_*.sh` e `tests/` +e' rimasta identica.* diff --git a/scripts/cron_book.sh b/scripts/cron_book.sh index c8f0f80..da382bd 100755 --- a/scripts/cron_book.sh +++ b/scripts/cron_book.sh @@ -20,5 +20,9 @@ mkdir -p logs # = allarme inviato); l'azione a un allarme e' manuale, vedi src/live/venue_watch.py. uv run python scripts/live/venue_watch.py --quiet || true uv run python scripts/live/book_execute.py --execute + # Libro di bordo: materializza in data/live/trades.db (che il backup COPRE) l'ora vera dei + # fill, che altrimenti vive solo in questo log β€” gitignored, fuori dal backup e ruotabile. + # Va DOPO book_execute: dentro lo stesso blocco vede le righe appena scritte. + uv run python scripts/live/trades_db.py --sync --quiet || true echo "===== done $(date -u '+%H:%M:%SZ') =====" } >> logs/cron_book.log 2>&1 diff --git a/scripts/cron_daily.sh b/scripts/cron_daily.sh index 6598994..c2c9af6 100755 --- a/scripts/cron_daily.sh +++ b/scripts/cron_daily.sh @@ -39,5 +39,13 @@ mkdir -p logs # decidono su queste serie: un monitor fermo produce silenzio, e il silenzio sembra uno zero. # Va DOPO tutti i monitor, altrimenti misura lo stato di ieri. uv run python scripts/live/monitor_health.py --quiet || true + # Giornale di bordo. Gira alle 00:30 UTC -> chiude IERI (giorno completo) e apre OGGI. + # Sola lettura: feed certificato, cron_book.log, trades.db. Il campo `nota` non lo tocca. + uv run python scripts/live/journal.py --giorno "$(date -u -d yesterday +%F)" --quiet || true + uv run python scripts/live/journal.py --quiet || true + # Analista di bordo: un modello scrive la prosa del giorno CHIUSO nel campo `analisi`. + # Scrive SOLO li': non tocca `nota` (operatore) ne' le sezioni misurate, e se non risponde + # la pagina resta senza analisi invece di mostrare quella di ieri. Costo: 1 chiamata/giorno. + uv run python scripts/live/analista.py --giorno "$(date -u -d yesterday +%F)" --quiet || true echo "===== done $(date -u '+%H:%M:%SZ') =====" } >> logs/cron_daily.log 2>&1 diff --git a/scripts/live/analista.py b/scripts/live/analista.py new file mode 100644 index 0000000..200714d --- /dev/null +++ b/scripts/live/analista.py @@ -0,0 +1,99 @@ +"""L'analista di bordo: fa scrivere a un modello l'analisi del giorno nel campo `analisi`. + + uv run python scripts/live/analista.py # oggi + uv run python scripts/live/analista.py --giorno 2026-08-21 + uv run python scripts/live/analista.py --modello claude-opus-5 + uv run python scripts/live/analista.py --secco # stampa e non salva + +NON tocca `nota` (dell'operatore) e non tocca nessuna sezione misurata: scrive SOLO in `analisi`. +Se il modello non risponde, l'analisi di ieri NON diventa quella di oggi: si registra l'errore +e la sezione resta vuota. Sola lettura su tutto il resto; nessun ordine. +""" +from __future__ import annotations + +import sys +from datetime import date, datetime, timedelta, timezone +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from src.live import analista as A # noqa: E402 +from src.live import journal as J # noqa: E402 +from src.live import tradesdb as T # noqa: E402 + +GIORNI_STORICO = 10 + + +def _arg(flag: str, default=None): + a = sys.argv[1:] + return a[a.index(flag) + 1] if flag in a and a.index(flag) + 1 < len(a) else default + + +def storico(con, g: date, n: int = GIORNI_STORICO) -> str: + """Le righe essenziali dei giorni precedenti: serve per vedere COSA E' CAMBIATO.""" + righe = ["giorno equity delta fill rt posizioni"] + for i in range(n, 0, -1): + d = (g - timedelta(days=i)).isoformat() + r = con.execute("SELECT pnl, libro FROM journal WHERE giorno=?", (d,)).fetchone() + if not r: + continue + import json as _j + p, lb = _j.loads(r["pnl"] or "{}"), _j.loads(r["libro"] or "{}") + pos = " ".join(f"{a} ${v['posizione']:+,.0f}" for a, v in sorted((lb.get("asset") or {}).items())) + dq = p.get("delta_equity") + righe.append(f"{d} {p.get('equity_fine') or float('nan'):8,.2f} " + f"{' n/d' if dq is None else f'{dq:+8.2f}'} " + f"{p.get('fill', 0):>4} {p.get('roundtrip_chiusi', 0):>2} {pos or 'flat'}") + return "\n".join(righe) + + +def scrivi(con, g: date, modello: str, secco: bool = False, verbose: bool = True) -> str: + voce = J.costruisci(con, g) + J.salva(con, voce) # le sezioni misurate prima, cosi' il modello le legge + pagina = J.rendi_markdown(con, voce) + st = storico(con, g) + prompt = A.costruisci_prompt(pagina, st) + # La guardia si applica a TUTTO cio' che il modello ha ricevuto, non alla sola pagina: + # i numeri del blocco storico gli sono stati dati, citarli non e' inventare. (Primo giro: + # validando sulla sola pagina il tripwire bocciava un'equity vera presa dallo storico β€” + # una guardia piu' stretta del contratto produce allarmi che si imparera' a ignorare.) + fonte = A.senza_analisi(pagina) + "\n" + st + testo, errore = A.interroga(prompt, modello=modello) + + if errore: + stato, motivi = "errore", [errore] + testo = "" + else: + stato, motivi = A.valida(testo, fonte) + if stato == "rifiutata": + testo = "" + + if verbose: + print(f" {g} modello {modello} -> {stato}" + + (f" ({'; '.join(motivi)})" if motivi else "")) + if secco: + print("\n" + (testo or "(niente da salvare)")) + return stato + + if testo: + con.execute("""UPDATE journal SET analisi=?, analisi_modello=?, analisi_ts=?, + analisi_stato=?, analisi_motivi=? WHERE giorno=?""", + (testo, modello, T.ora(), stato, "; ".join(motivi) or None, g.isoformat())) + else: + # niente analisi: si REGISTRA il perche' e NON si lascia in pagina quella di ieri + con.execute("""UPDATE journal SET analisi='', analisi_modello=?, analisi_ts=?, + analisi_stato=?, analisi_motivi=? WHERE giorno=?""", + (modello, T.ora(), stato, "; ".join(motivi) or None, g.isoformat())) + con.commit() + J.scrivi_file(con, voce) + return stato + + +if __name__ == "__main__": + con = T.connect() + g = date.fromisoformat(_arg("--giorno", datetime.now(timezone.utc).date().isoformat())) + st = scrivi(con, g, modello=_arg("--modello", A.MODELLO_DEFAULT), + secco="--secco" in sys.argv[1:], verbose="--quiet" not in sys.argv[1:]) + con.close() + sys.exit(0 if st in ("ok", "sospetta") else 1) diff --git a/scripts/live/book_execute.py b/scripts/live/book_execute.py index 952764a..a8e259f 100644 --- a/scripts/live/book_execute.py +++ b/scripts/live/book_execute.py @@ -23,6 +23,8 @@ import json import sys from pathlib import Path +from datetime import datetime, timezone + import pandas as pd PROJECT_ROOT = Path(__file__).resolve().parents[2] @@ -210,7 +212,12 @@ def _run(): for f in fills: print(f" -> {f.side.upper()} {f.filled:.4f} @ ${f.price or 0:,.1f} fee {f.fee_usdc:.5f} " f"({'OK' if f.verified else 'NON VERIFICATO: ' + f.notes})") - log_event(dict(ts_utc=str(pd.Timestamp(r['last_data'])), asset=asset, action=act, + # `ts_utc` = ORA VERA del fill. Fino al 2026-08-23 qui c'era la data della + # BARRA di segnale (`r['last_data']`): 19 righe su 19 a 00:00:00, e un trade + # registrato SEI GIORNI prima di essere eseguito (ETH 0.04 del 14/07, scritto + # 08/07). La barra resta, sotto il suo nome: `bar_ts`. + log_event(dict(ts_utc=datetime.now(timezone.utc).isoformat(timespec="seconds"), + bar_ts=str(pd.Timestamp(r['last_data'])), asset=asset, action=act, side=f.side, filled=f.filled, price=f.price, fee=f.fee_usdc, verified=f.verified, notes=f.notes, net_target=net, pos_after=newpos, tp_frac=a["tp_frac"], skh_sign=a["skh_sign"])) diff --git a/scripts/live/journal.py b/scripts/live/journal.py new file mode 100644 index 0000000..0791bfa --- /dev/null +++ b/scripts/live/journal.py @@ -0,0 +1,67 @@ +"""Giornale di bordo: scrive la voce del giorno nel DB e in `docs/journal/YYYY-MM-DD.md`. + + uv run python scripts/live/journal.py # voce di oggi + uv run python scripts/live/journal.py --giorno 2026-08-21 + uv run python scripts/live/journal.py --backfill 60 # ricostruisce gli ultimi N giorni + uv run python scripts/live/journal.py --nota "testo" # annota il giorno (operatore) + +SOLA LETTURA su feed, log e venue: non tocca il libro, non manda ordini. +Il campo `nota` e' l'UNICO posto per il testo libero, e non viene mai riscritto da un +ricalcolo automatico. +""" +from __future__ import annotations + +import sys +from datetime import date, datetime, timedelta, timezone +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from src.live import journal as J # noqa: E402 +from src.live import tradesdb as T # noqa: E402 + + +def _arg(flag: str, default=None): + a = sys.argv[1:] + return a[a.index(flag) + 1] if flag in a and a.index(flag) + 1 < len(a) else default + + +def una(con, g: date, nota: str | None = None, verbose: bool = True) -> Path: + voce = J.costruisci(con, g) + J.salva(con, voce, nota=nota) + f = J.scrivi_file(con, voce) + if verbose: + p = voce["pnl"] + d = p.get("delta_equity") + print(f" {g} equity {p.get('equity_fine') or float('nan'):,.2f} " + f"({'n/d' if d is None else f'{d:+.2f}'}) " + f"fill {p['fill']} Β· rt {p['roundtrip_chiusi']} Β· giri {voce['salute']['giri_book']}/24" + f" -> {f.relative_to(ROOT)}") + return f + + +if __name__ == "__main__": + # sincronizzare PRIMA: altrimenti la sezione 'Libro' (letta dal log) e la sezione 'P&L' + # (letta dal DB) descrivono due istanti diversi e la pagina si contraddice da sola. + sys.path.insert(0, str(ROOT / "scripts" / "live")) + from trades_db import sync as _sync + _sync(verbose=False) + + con = T.connect() + nota = _arg("--nota") + if (n := _arg("--backfill")): + oggi = datetime.now(timezone.utc).date() + for i in range(int(n), -1, -1): + g = oggi - timedelta(days=i) + v = J.costruisci(con, g) + # un giorno senza giri di book non e' una voce: e' assenza di dato + if v["salute"]["giri_book"] == 0: + continue + J.salva(con, v); J.scrivi_file(con, v) + tot = con.execute("SELECT COUNT(*) c FROM journal").fetchone()["c"] + print(f" voci nel giornale: {tot}") + else: + g = date.fromisoformat(_arg("--giorno", datetime.now(timezone.utc).date().isoformat())) + una(con, g, nota=nota, verbose="--quiet" not in sys.argv[1:]) + con.close() diff --git a/scripts/live/trades_db.py b/scripts/live/trades_db.py new file mode 100644 index 0000000..f1b98d3 --- /dev/null +++ b/scripts/live/trades_db.py @@ -0,0 +1,105 @@ +"""Sincronizza `data/live/trades.db` β€” il libro di bordo dei trade, allineato col tempo. + + uv run python scripts/live/trades_db.py --sync # idempotente, da cron + uv run python scripts/live/trades_db.py --report # stato + P&L a video + uv run python scripts/live/trades_db.py --reconcile # incrocio delle tre fonti + +SOLA LETTURA sul venue e sui log: non manda ordini, non tocca il libro. +Il DB sta in `data/live/`, che il backup rotativo della VPS gia' copre β€” a differenza di +`logs/cron_book.log`, che e' oggi l'unica sede dell'ora vera dei fill. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from src.live import tradesdb as T # noqa: E402 + + +def sync(verbose: bool = True) -> dict: + con = T.connect() + runs = T.parse_cron_log(T.CRON_LOG.read_text(errors="replace")) if T.CRON_LOG.exists() else [] + fills = [f for r in runs for f in r.fills] + n_f = T.upsert_fills(con, fills) + n_e = T.upsert_equity(con, [(r.ts_utc, r.equity, "cron_book") for r in runs if r.equity is not None]) + n_r = T.rebuild_roundtrips(con) + if runs: + T.set_meta(con, "ultimo_giro", runs[-1].ts_utc) + T.set_meta(con, "ultimo_sync", T.ora()) + tot = con.execute("SELECT COUNT(*) c FROM fills").fetchone()["c"] + tot_e = con.execute("SELECT COUNT(*) c FROM equity").fetchone()["c"] + if verbose: + print(f" fills: +{n_f} (totale {tot}) | equity: +{n_e} (totale {tot_e}) | round-trip {n_r}") + con.close() + return dict(nuovi_fill=n_f, nuovi_equity=n_e, roundtrip=n_r, totale_fill=tot) + + +def reconcile() -> None: + runs = T.parse_cron_log(T.CRON_LOG.read_text(errors="replace")) + fills = [f for r in runs for f in r.fills] + righe = T.parse_executions_jsonl(T.EXEC_JSONL.read_text()) if T.EXEC_JSONL.exists() else [] + rec = T.reconcile(fills, righe) + print(f"\n cron_book.log : {len(fills)} fill (con ORA VERA)") + print(f" book_executions : {len(righe)} righe (senza ora)") + print(f" concordano : {len(rec['ok'])}") + print(f" solo nel log : {len(rec['solo_log'])}") + print(f" solo nel jsonl : {len(rec['solo_jsonl'])}") + print(f" prezzi divergenti : {len(rec['prezzi_divergenti'])} (oltre l'arrotondamento di stampa)") + for f in rec["solo_log"]: + print(f" solo-log {f.ts_utc} {f.asset} {f.side} {f.qty} @ {f.price}") + for r in rec["solo_jsonl"]: + print(f" solo-jsonl {r['ts_utc'][:10]} {r['asset']} {r['side']} {r['filled']} @ {r['price']}") + # il venue: autorevole ma TRONCATO -> si riporta cosa vede, non lo si usa per riparare + try: + from src.live.deribit import DeribitRead + d = DeribitRead() + for ins in ("BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"): + t = d.trade_history(ins, limit=100) + print(f" venue {ins:<20}: {len(t)} trade visibili (l'endpoint TRONCA: non e' un backfill)") + except Exception as e: + print(f" venue: NON LETTO ({type(e).__name__}) β€” non e' 'zero trade', e' 'non misurato'") + + +def report() -> None: + con = T.connect() + n = con.execute("SELECT COUNT(*) c FROM fills").fetchone()["c"] + if not n: + print(" DB vuoto: lanciare --sync"); return + p = con.execute("SELECT MIN(ts_utc) a, MAX(ts_utc) b FROM fills").fetchone() + print(f"\n fill registrati : {n} dal {p['a'][:16]} al {p['b'][:16]}") + eq = con.execute("SELECT ts_utc, equity FROM equity ORDER BY ts_utc").fetchall() + if eq: + e0, e1 = eq[0]["equity"], eq[-1]["equity"] + picco = max(r["equity"] for r in eq) + print(f" equity : ${e0:,.2f} -> ${e1:,.2f} ({e1 - e0:+.2f}, {100*(e1/e0-1):+.2f}%)" + f" | picco ${picco:,.2f} | {len(eq)} letture") + rt = con.execute("SELECT * FROM roundtrips ORDER BY ts_out").fetchall() + lordo = sum(r["pnl_lordo"] for r in rt) + fee_rt = sum(r["fee_quota"] for r in rt) + fee_tot = con.execute("SELECT COALESCE(SUM(fee),0) s FROM fills").fetchone()["s"] + vinc = sum(1 for r in rt if r["pnl_netto"] > 0) + print(f" round-trip chiusi : {len(rt)} ({vinc} in utile) " + f"lordo {lordo:+.2f} | fee allocate {fee_rt:.2f} | netto {lordo - fee_rt:+.2f}") + print(f" fee totali pagate : {fee_tot:.4f}") + ap = T.stato_aperto(con) + if ap: + print(" posizioni aperte :") + for a, d in sorted(ap.items()): + print(f" {a} {d['qty']:.4f} @ medio ${d['prezzo_medio']:,.2f} (dal {d['dal'][:16]})") + else: + print(" posizioni aperte : nessuna (flat)") + print(f" ultimo sync : {T.get_meta(con, 'ultimo_sync', 'mai')}") + con.close() + + +if __name__ == "__main__": + args = sys.argv[1:] + if "--reconcile" in args: + reconcile() + elif "--report" in args: + report() + else: + sync(verbose="--quiet" not in args) diff --git a/scripts/research/r0822_adaptive_horizon.py b/scripts/research/r0822_adaptive_horizon.py new file mode 100644 index 0000000..baf804c --- /dev/null +++ b/scripts/research/r0822_adaptive_horizon.py @@ -0,0 +1,728 @@ +#!/usr/bin/env python +"""r0822_adaptive_horizon.py β€” ADAPTIVE-HORIZON TSMOM: stimare *quanto indietro guardare* +dai dati (change-point / persistenza) invece di fissarlo a 30/90/180 giorni. + +DOMANDA +------- +Un TSMOM a lookback ADATTIVO batte il TSMOM a orizzonti FISSI 30/90/180 (TP01), in modo +MARGINALE e ONESTO? + +COSA NON E' (i due vicini gia' bocciati, per non rifarli) +--------------------------------------------------------- + * 02/07 "velocita' trend regime-condizionata": pesava i TRE orizzonti fissi in funzione del + percentile di vol -> risulto' un tilt-30d statico travestito (pctl 0.71 vs il null dei pesi + statici casuali). Li' gli orizzonti restavano 30/90/180: cambiavano solo i PESI. + * 29/06 "meta-allocazione dinamica" fra sleeve: peggiore dei pesi fissi. Li' l'oggetto adattato + erano i pesi di PORTAFOGLIO, non un parametro della strategia. + Qui l'oggetto adattato e' **l'orizzonte stesso**: L_t stimato da un rilevatore di change-point + (CUSUM) o di persistenza (variance-ratio, Hurst). Mai provato nel progetto. + +FORMA FUNZIONALE (scelta perche' rende il confronto strutturalmente pulito) +-------------------------------------------------------------------------- +TP01 canonico e' il blend di sign(c[i]/c[i-h]-1) su h = (30, 90, 180) giorni, long-flat, +vol-target 20%, leva cap 2x. Ma (30, 90, 180) == (L/3, L, 2L) con **L = 90 giorni**. +Quindi il candidato e' LA STESSA FUNZIONE con L che varia nel tempo: + blend='triple' -> horizons_t = (L_t/3, L_t, 2*L_t) (TP01 = L_t costante a 90g) + blend='single' -> horizons_t = (L_t,) (un solo orizzonte adattivo) +Tutto il resto (long-flat, target_vol 20%, vol_win 30g, leva 2x, fee 0.05%/lato, cadenza, +50/50 BTC+ETH) e' IDENTICO al baseline: **l'unico grado di liberta' e' l'orizzonte**. +=> il null non e' "meglio di zero", e' "meglio del TSMOM fisso a PARI GEOMETRIA". + +FAMIGLIA DICHIARATA **PRIMA** DI GUARDARE (conteggio al RIALZO) +-------------------------------------------------------------- + rilevatore (3): cusum | vratio | hurst + parametro (3): cusum k in {2,4,8} sigma-cumulate; vratio/hurst finestra W in {60,120,250}g + mappatura (2): direct (piu' persistenza / piu' tempo dall'ultimo change-point -> L piu' LUNGO) + inverse (specchiata) [il verso NON e' ovvio a priori => e' un asse, non una scelta] + blend (2): single | triple + timeframe (2): 1d | 12h [>= 12h: sotto, costi+overfit dominano (19/06)] + ------------------------------------------------------------------ + = 3 x 3 x 2 x 2 = 36 celle x 2 TF = **72 valutazioni**. +Costanti FISSATE A PRIORI e non cercate (dichiarate perche' sono gradi di liberta' non contati +nella griglia): Lmin=20g, Lmax=200g (contengono 30..180 del canonico); q=5g del variance-ratio; +scale del Hurst k in {1,2,4,8}g; finestra della percentile causale M=250g. Per questo il +deflated-Sharpe e' riportato ANCHE a N=144 e N=216 trial (sensibilita' del verdetto al conteggio, +regola del 30/07): un verdetto che si ribalta col conteggio si cita come tale. + +GATE (tutti dell'harness del progetto, nessuno riscritto) +--------------------------------------------------------- + A.study_family_honest -> cella scelta IN-SAMPLE-ONLY + deflated-Sharpe (NO selezione su hold-out) + A.marginal_vs_tp01 -> ADDS/HEDGE/NOISE/REDUNDANT/DILUTES + corr al baseline + A.causality_ok -> ricalcolo su prefisso (nessun look-ahead nella costruzione del segnale) + A.anchor_luck_band -> stima onesta = MEDIANA delle 24 ancore, non l'ancora 00:00 + A.anchor_luck_delta -> confronto adattivo-vs-fisso: MEDIANA delle DIFFERENZE APPAIATE + A.implausible_sharpe -> Sharpe troppo bello = rischio fuori dal campione + A.eval_weights_smallcap-> haircut reale a $600 (min-order $5) + null del de-levering -> esiste k<1 che da' al FISSO lo stesso maxDD con Sharpe >=? + differenza APPAIATA PER ANNO -> un vantaggio concentrato in un anno e' un evento, non un meccanismo + +Causalita': ogni rolling ha min_periods e finestra all'indietro; la CUSUM e' un loop in avanti +con reset (prefix-stable); la percentile e' rolling causale; nessuna statistica full-sample. +Runtime atteso ~3-6 min su 2 core. Nessun file toccato fuori da questo. +""" +from __future__ import annotations + +import sys +import warnings +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT)) + +import altlib as A # noqa: E402 +from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402 + +ASSETS = A.CERTIFIED +HOLDOUT = A.HOLDOUT + +# --- costanti FISSATE A PRIORI (dichiarate sopra, non cercate) -------------- +LMIN_D, LMAX_D = 20, 200 # banda dell'orizzonte adattivo, in giorni +FIXED_H = (30, 90, 180) # == (L/3, L, 2L) con L=90 -> il baseline +TGT_VOL, LEV, VOLWIN = 0.20, 2.0, 30 +VR_Q_DAYS = 5 # passo del variance-ratio +HURST_K_DAYS = (1, 2, 4, 8) # scale del Hurst +PCTL_M_DAYS = 250 # finestra della percentile causale + +TFS = ("1d", "12h") +DETECTORS = ("cusum", "vratio", "hurst") +PARAMS = {"cusum": (2.0, 4.0, 8.0), "vratio": (60, 120, 250), "hurst": (60, 120, 250)} +MAPPINGS = ("direct", "inverse") +BLENDS = ("single", "triple") + + +# =========================================================================== +# RILEVATORI -> lookback adattivo L_t (in barre). Tutti causali. +# =========================================================================== +def _logret(c: np.ndarray) -> np.ndarray: + r = np.zeros(len(c)) + r[1:] = np.log(c[1:] / c[:-1]) + return r + + +def _roll_pctl(x: np.ndarray, m: int) -> np.ndarray: + """Percentile CAUSALE del valore corrente nella sua finestra all'indietro (rolling rank). + Warmup -> 0.5 (neutro): non si inventa un regime dove non c'e' campione.""" + s = pd.Series(x) + u = s.rolling(m, min_periods=max(20, m // 4)).rank(pct=True).values + return np.where(np.isfinite(u), u, 0.5) + + +def _cusum_age(c: np.ndarray, k: float, bpd: int) -> np.ndarray: + """Eta' (in barre) dall'ultimo change-point CUSUM a due code su rendimenti standardizzati. + Loop in avanti con reset: dipende solo dal passato -> stabile su prefisso.""" + lr = _logret(c) + sig = pd.Series(lr).rolling(30 * bpd, min_periods=max(5, 15 * bpd)).std().values + s = np.where((sig > 0) & np.isfinite(sig), lr / np.where(sig > 0, sig, 1.0), 0.0) + s = np.nan_to_num(s) + n = len(c) + age = np.zeros(n) + sp = sm = 0.0 + last = 0 + for i in range(n): + sp = max(0.0, sp + s[i]) + sm = min(0.0, sm + s[i]) + if max(sp, -sm) > k: + sp = sm = 0.0 + last = i + age[i] = i - last + return age + + +def _vratio(c: np.ndarray, w_days: int, bpd: int) -> np.ndarray: + """Variance ratio VR = Var(r_q) / (q * Var(r_1)) su finestra w_days. >1 = persistente.""" + lr = _logret(c) + qb = max(2, VR_Q_DAYS * bpd) + lg = np.log(c) + lrq = np.zeros(len(c)) + lrq[qb:] = lg[qb:] - lg[:-qb] + lrq[:qb] = np.nan + wb = max(20, w_days * bpd) + v1 = pd.Series(lr).rolling(wb, min_periods=wb // 2).var().values + vq = pd.Series(lrq).rolling(wb, min_periods=wb // 2).var().values + with np.errstate(invalid="ignore", divide="ignore"): + vr = vq / (qb * v1) + return vr + + +def _hurst(c: np.ndarray, w_days: int, bpd: int) -> np.ndarray: + """Hurst da scaling della volatilita': sd(r_k) ~ k^H. Regressione di log sd su log k + (x fisso -> forma chiusa). Tutto rolling causale.""" + lg = np.log(c) + wb = max(20, w_days * bpd) + xs, ys = [], [] + for kd in HURST_K_DAYS: + kb = max(1, kd * bpd) + d = np.full(len(c), np.nan) + d[kb:] = lg[kb:] - lg[:-kb] + sd = pd.Series(d).rolling(wb, min_periods=wb // 2).std().values + with np.errstate(invalid="ignore", divide="ignore"): + ys.append(np.log(np.where(sd > 0, sd, np.nan))) + xs.append(np.log(kb)) + X = np.asarray(xs, float) + Y = np.vstack(ys) # (n_scale, n_bar) + xm = X.mean() + denom = float(((X - xm) ** 2).sum()) + with warnings.catch_warnings(): # warmup = colonne tutte-NaN + warnings.simplefilter("ignore", RuntimeWarning) + ym = np.nanmean(Y, axis=0) + H = ((X - xm)[:, None] * (Y - ym[None, :])).sum(axis=0) / denom + return H + + +_STAT_CACHE: dict = {} + + +def _stat_key(df: pd.DataFrame, det: str, par) -> tuple: + ts = df["timestamp"].values + return (det, par, len(df), int(ts[0]), int(ts[-1])) + + +def lookback_bars(df: pd.DataFrame, det: str, par, mapping: str) -> np.ndarray: + """L_t in BARRE, dentro [Lmin, Lmax] giorni. Cache chiusa sul CONTENUTO del frame + (lunghezza + primo/ultimo timestamp) -> un prefisso NON prende la cache del pieno, + altrimenti causality_ok verrebbe ingannata dalla cache stessa.""" + bpd = A.bars_per_day(df) + lo, hi = max(2, LMIN_D * bpd), max(3, LMAX_D * bpd) + c = df["close"].values.astype(float) + key = _stat_key(df, det, par) + if key not in _STAT_CACHE: + if det == "cusum": + _STAT_CACHE[key] = _cusum_age(c, float(par), bpd) + elif det == "vratio": + _STAT_CACHE[key] = _roll_pctl(_vratio(c, int(par), bpd), PCTL_M_DAYS * bpd) + elif det == "hurst": + _STAT_CACHE[key] = _roll_pctl(_hurst(c, int(par), bpd), PCTL_M_DAYS * bpd) + else: + raise ValueError(det) + stat = _STAT_CACHE[key] + if det == "cusum": + base = np.clip(stat, lo, hi) + L = base if mapping == "direct" else (lo + hi - base) + else: + u = np.clip(stat, 0.0, 1.0) + L = lo + u * (hi - lo) if mapping == "direct" else hi - u * (hi - lo) + return np.clip(np.round(L), 2, hi).astype(int) + + +# =========================================================================== +# DIREZIONE TSMOM (orizzonte costante o variabile) + vol-target +# =========================================================================== +def _sign_at(c: np.ndarray, h: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + n = len(c) + i = np.arange(n) + j = i - h + ok = j >= 0 + s = np.zeros(n) + jj = np.where(ok, j, 0) + s[ok] = np.sign(c[i[ok]] / c[jj[ok]] - 1.0) + return s, ok + + +def _dir_from_L(c: np.ndarray, L: np.ndarray, blend: str) -> np.ndarray: + hs = [L] if blend == "single" else [ + np.maximum(2, np.round(L / 3.0).astype(int)), L, np.round(2.0 * L).astype(int)] + acc = np.zeros(len(c)) + cnt = np.zeros(len(c)) + for h in hs: + s, ok = _sign_at(c, np.asarray(h, int)) + acc[ok] += s[ok] + cnt[ok] += 1 + out = np.zeros(len(c)) + nz = cnt > 0 + out[nz] = acc[nz] / cnt[nz] + return out + + +def fixed_target(df: pd.DataFrame) -> np.ndarray: + """BASELINE: TSMOM fisso 30/90/180, long-flat, vol-target 20%, leva 2x = TP01 canonico.""" + c = df["close"].values.astype(float) + bpd = A.bars_per_day(df) + acc = np.zeros(len(c)) + cnt = np.zeros(len(c)) + for hd in FIXED_H: + s, ok = _sign_at(c, np.full(len(c), hd * bpd, int)) + acc[ok] += s[ok] + cnt[ok] += 1 + d = np.zeros(len(c)) + nz = cnt > 0 + d[nz] = acc[nz] / cnt[nz] + return A.vol_target(np.clip(d, 0, None), df, TGT_VOL, VOLWIN, LEV) + + +def const_factory(tf: str, L: int, blend: str): + """TSMOM a orizzonte COSTANTE L giorni, STESSA geometria del candidato adattivo. + blend='triple' -> (L/3, L, 2L): con L=90 e' ESATTAMENTE TP01 canonico 30/90/180.""" + def fn(df: pd.DataFrame) -> np.ndarray: + c = df["close"].values.astype(float) + bpd = A.bars_per_day(df) + Lb = np.full(len(c), max(2, int(round(L * bpd))), int) + d = np.clip(_dir_from_L(c, Lb, blend), 0, None) + return A.vol_target(d, df, TGT_VOL, VOLWIN, LEV) + fn.__name__ = f"const_L{L}_{blend}" + return fn + + +def factory(tf: str, det: str, par, mapping: str, blend: str): + """target_fn(df) -> posizione per barra, decisa con dati <= close[i].""" + def fn(df: pd.DataFrame) -> np.ndarray: + c = df["close"].values.astype(float) + L = lookback_bars(df, det, par, mapping) + d = np.clip(_dir_from_L(c, L, blend), 0, None) + return A.vol_target(d, df, TGT_VOL, VOLWIN, LEV) + fn.__name__ = f"adapt_{det}{par}_{mapping}_{blend}" + return fn + + +GRID = [dict(det=d, par=p, mapping=m, blend=b) + for d in DETECTORS for p in PARAMS[d] for m in MAPPINGS for b in BLENDS] + +# CONTROLLO dichiarato DOPO aver visto che il vincitore ha L_t incollato al bordo (sezione 3): +# il null "TSMOM fisso 30/90/180" NON basta piu', perche' confonde due cose diverse β€” +# "adattare l'orizzonte paga" e "un orizzonte piu' CORTO paga su questo campione". +# Il controllo giusto e' il migliore fra gli orizzonti COSTANTI della stessa banda, scelto +# in-sample-only. Trial aggiuntivi, contati al rialzo. +CONST_L = (20, 30, 45, 60, 90, 120, 150, 200) +CONST_GRID = [dict(L=L, blend=b) for L in CONST_L for b in BLENDS] + +# ESTENSIONE dichiarata DOPO aver visto che l'ottimo costante cade sul BORDO BASSO della banda +# (L=20g = LMIN_D): un ottimo al bordo non e' un ottimo, e' il punto in cui si e' smesso di +# guardare. Si estende SOTTO e si guarda se e' un plateau o una scivolata verso il rumore. +# +4 valori x 1 blend x 2 TF = 8 trial in piu' (contati al rialzo). +CONST_L_EXT = (5, 7, 10, 15) + + +# =========================================================================== +# UTILITA' DI MISURA +# =========================================================================== +def _dd(s: pd.Series) -> float: + eq = np.cumprod(1.0 + np.asarray(s.dropna().values, float)) + pk = np.maximum.accumulate(eq) + return float(np.max((pk - eq) / pk)) if len(eq) else 0.0 + + +def _cagr(s: pd.Series) -> float: + s = s.dropna() + if len(s) < 5: + return float("nan") + eq = float(np.prod(1.0 + s.values)) + yrs = (s.index[-1] - s.index[0]).total_seconds() / (86400 * 365.25) + return eq ** (1 / max(yrs, 1e-6)) - 1.0 + + +def resample_offset(df_1h: pd.DataFrame, hours: int, off: int) -> pd.DataFrame: + """Stessa convenzione di trend_portfolio.resample_tf, con ancora spostata di `off` ore + (r0702_tp01_offset.py). NB: si usa `offset=`, non `origin=` (pandas ignora origin su 7D).""" + g = df_1h.copy() + idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True) + idx.name = "dt" + g.index = idx + out = g.resample(f"{hours}h", offset=pd.Timedelta(hours=off), label="left", closed="left").agg( + {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}) + out = out.dropna(subset=["open"]) + out["datetime"] = out.index + epoch = pd.Timestamp("1970-01-01", tz="UTC") + out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64") + return out.reset_index(drop=True)[ + ["timestamp", "open", "high", "low", "close", "volume", "datetime"]] + + +def daily_from_target(fn, tf: str) -> pd.Series: + return A.candidate_daily(fn, tf=tf) + + +def paired_by_year(cand: pd.Series, base: pd.Series) -> list[dict]: + J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna() + rows = [] + for y, g in J.groupby(J.index.year): + if len(g) < 40: + continue + rows.append(dict(year=int(y), n=len(g), + sh_cand=round(A._sh(g["C"]), 2), sh_base=round(A._sh(g["B"]), 2), + d_sh=round(A._sh(g["C"]) - A._sh(g["B"]), 2), + ret_cand=round(float(np.prod(1 + g["C"].values) - 1) * 100, 1), + ret_base=round(float(np.prod(1 + g["B"].values) - 1) * 100, 1))) + return rows + + +def delever_null(cand: pd.Series, base: pd.Series) -> dict: + """Il fisso, DE-LEVERATO a pari maxDD del candidato, che Sharpe fa? + Scalare le POSIZIONI per k scala gross e fee insieme -> il netto scala per k e lo Sharpe + e' INVARIANTE: percio' un candidato che compra solo meno DD non e' un miglioramento.""" + J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna() + dd_c, dd_b = _dd(J["C"]), _dd(J["B"]) + if dd_c >= dd_b or dd_b <= 0: + return dict(applicabile=False, dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4), + nota="il candidato NON riduce il maxDD: il null non si applica") + lo, hi = 0.0, 1.0 + for _ in range(60): + k = 0.5 * (lo + hi) + if _dd(k * J["B"]) < dd_c: + lo = k + else: + hi = k + k = 0.5 * (lo + hi) + return dict(applicabile=True, k=round(k, 4), dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4), + dd_base_delev=round(_dd(k * J["B"]), 4), + sharpe_cand=round(A._sh(J["C"]), 3), + sharpe_base_delev=round(A._sh(k * J["B"]), 3), + superato=bool(A._sh(J["C"]) > A._sh(k * J["B"]))) + + +_ANCHOR_CACHE: dict = {} + + +def series_at_anchor(fn, name: str, hours: int, off: int) -> pd.Series: + """Serie giornaliera 50/50 BTC+ETH della strategia `fn` ribilanciata sull'ancora `off` + (resample del 1h certificato). Cache per (name, hours, off).""" + key = (name, hours, off) + if key not in _ANCHOR_CACHE: + ser = {} + for a in ASSETS: + dfo = resample_offset(A.get(a, "1h"), hours, off) + ev = A.eval_weights(dfo, A._call_target(fn, dfo, a)) + ser[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + _ANCHOR_CACHE[key] = A._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) + return _ANCHOR_CACHE[key] + + +def adaptivity(tf: str, det: str, par, mapping: str) -> dict: + """Quanto si MUOVE davvero L_t (diagnostica di MECCANISMO, non di rendimento). + Una cella con L_t incollato a un bordo NON e' adattiva: e' un TSMOM a orizzonte + COSTANTE travestito, e non appartiene alla famiglia che si sta testando.""" + fr_lo, fr_hi, iqr, med = [], [], [], [] + for a in ASSETS: + df = A.get(a, tf) + bpd = A.bars_per_day(df) + L = lookback_bars(df, det, par, mapping) / bpd + fr_lo.append(float(np.mean(L <= LMIN_D + 1))) + fr_hi.append(float(np.mean(L >= LMAX_D - 1))) + iqr.append(float(np.percentile(L, 75) - np.percentile(L, 25))) + med.append(float(np.median(L))) + return dict(frac_lo=float(np.mean(fr_lo)), frac_hi=float(np.mean(fr_hi)), + iqr_days=float(np.mean(iqr)), med_days=float(np.mean(med)), + adattiva=bool(np.mean(fr_lo) < 0.5 and np.mean(fr_hi) < 0.5 + and np.mean(iqr) >= 10.0)) + + +def hr(t: str = "") -> None: + print("\n" + "=" * 78) + if t: + print(t) + print("=" * 78) + + +# =========================================================================== +def main() -> None: + print(__doc__.split("Runtime atteso")[0]) + + # -- 0. SANITY: il mio baseline DEVE essere TP01 canonico, bit-per-bit ---------------- + hr("0. SANITY β€” il baseline 'fisso' e' TP01 canonico (se non lo e', ogni Ξ” e' inventato)") + tp = TrendPortfolio(**CANONICAL) + worst = 0.0 + for a in ASSETS: + df = A.get(a, "1d") + worst = max(worst, float(np.max(np.abs(fixed_target(df) - tp.target_series(df))))) + base_1d = daily_from_target(fixed_target, "1d") + tp01 = A.tp01_baseline_daily() + JJ = pd.concat({"a": base_1d, "b": tp01}, axis=1, join="inner").dropna() + print(f" max|target_mio - TrendPortfolio.target_series| (1d, BTC+ETH) = {worst:.3e}") + print(f" max|serie_giornaliera_mia - al.tp01_baseline_daily()| = " + f"{float(np.max(np.abs(JJ['a'] - JJ['b']))):.3e} (n={len(JJ)})") + assert worst < 1e-12, "il baseline NON riproduce TP01: fermarsi qui" + + # -- 1. GRIGLIA ONESTA: cella scelta IN-SAMPLE + deflated-Sharpe ---------------------- + hr(f"1. study_family_honest β€” {len(GRID)} celle x {len(TFS)} TF = {len(GRID)*len(TFS)} trial") + rep = A.study_family_honest("ADAPT-H", factory, GRID, TFS) + ch = rep["chosen"] + print(f" celle valutate : {rep['n_cells']}") + print(f" cella scelta IN-SAMPLE : tf={ch['tf']} {ch['params']} " + f"Sharpe IS {ch['insample_sharpe']} FULL {ch['full_sharpe']}") + print(f" deflated-Sharpe (N={rep['n_cells']}) : {rep['deflated_sharpe']} " + f"(max atteso sotto il null {rep['expected_null_max']}) PASS={rep['dsr_pass']}") + print(f" earns_slot_marginal : {rep['earns_slot_marginal']}") + print(f" EARNS_SLOT_HONEST : {rep['earns_slot_honest']}") + print("\n top-8 IN-SAMPLE (come si sceglie onestamente):") + for r in rep["rows"][:8]: + print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} " + f"{r['tf']:>3} {r['params']}") + + # sensibilita' del verdetto al CONTEGGIO dei trial (regola 30/07) + fn_ch = factory(tf=ch["tf"], **ch["params"]) + d_ch = daily_from_target(fn_ch, ch["tf"]) + allsr = [r["full_sharpe"] for r in rep["rows"]] + print("\n sensibilita' del DSR al numero di trial dichiarati:") + for n_decl in (len(GRID) * len(TFS), 144, 216): + pad = allsr + [float(np.mean(allsr))] * max(0, n_decl - len(allsr)) + dsr, _ = A.deflated_sharpe(A._sh(d_ch), pad, d_ch) + print(f" N={n_decl:>4} DSR={dsr:.3f} {'PASS' if dsr >= 0.95 else 'FAIL'}") + + # cosa avrebbe scelto un disonesto (diagnostica, NON una decisione) + diag = [] + for r in rep["rows"][:12]: + s_all = daily_from_target(factory(tf=r["tf"], **r["params"]), r["tf"]) + diag.append({**r, "hold": round(A._sh(s_all[s_all.index >= HOLDOUT]), 3)}) + diag.sort(key=lambda r: -r["hold"]) + print("\n (diagnostica) le stesse 12 celle riordinate per HOLD-OUT β€” cio' che NON si fa:") + for r in diag[:4]: + print(f" HOLD {r['hold']:+.3f} IS {r['insample_sharpe']:+.3f} {r['tf']:>3} {r['params']}") + + # -- 2. NUMERI del candidato vs il FISSO a pari geometria ----------------------------- + hr("2. CANDIDATO vs TSMOM FISSO 30/90/180 β€” stessa geometria, stesso TF, stesse fee") + base_ch = daily_from_target(fixed_target, ch["tf"]) + J = pd.concat({"C": d_ch, "B": base_ch}, axis=1, join="inner").dropna() + for lab, sl in (("FULL", J), ("HOLD-OUT", J[J.index >= HOLDOUT])): + if len(sl) < 30: + continue + print(f" {lab:<9} adattivo Sh {A._sh(sl['C']):+.3f} DD {_dd(sl['C'])*100:5.2f}% " + f"CAGR {_cagr(sl['C'])*100:6.2f}% | fisso Sh {A._sh(sl['B']):+.3f} " + f"DD {_dd(sl['B'])*100:5.2f}% CAGR {_cagr(sl['B'])*100:6.2f}% " + f"| dSh {A._sh(sl['C'])-A._sh(sl['B']):+.3f}") + print(f" corr(adattivo, fisso) = {J['C'].corr(J['B']):.3f}") + + print("\n DIFFERENZA APPAIATA PER ANNO (un vantaggio in un anno solo e' un evento):") + rows = paired_by_year(d_ch, base_ch) + for r in rows: + print(f" {r['year']} adatt Sh {r['sh_cand']:+.2f} ({r['ret_cand']:+7.1f}%) " + f"fisso Sh {r['sh_base']:+.2f} ({r['ret_base']:+7.1f}%) dSh {r['d_sh']:+.2f}") + ds = [r["d_sh"] for r in rows] + print(f" -> anni con dSh>0: {sum(1 for x in ds if x > 0)}/{len(ds)} " + f"mediana dSh {np.median(ds):+.2f} media {np.mean(ds):+.2f}") + + # -- 3. LOOKBACK: si muove davvero? --------------------------------------------------- + hr("3. IL LOOKBACK ADATTIVO SI MUOVE? (se sta al bordo e' un TSMOM COSTANTE travestito)") + for a in ASSETS: + df = A.get(a, ch["tf"]) + bpd = A.bars_per_day(df) + L = lookback_bars(df, ch["params"]["det"], ch["params"]["par"], + ch["params"]["mapping"]) / bpd + lo, hi = LMIN_D, LMAX_D + print(f" {a}: L_t giorni min {L.min():.0f} p25 {np.percentile(L,25):.0f} " + f"mediana {np.median(L):.0f} p75 {np.percentile(L,75):.0f} max {L.max():.0f} " + f"| al bordo basso {np.mean(L<=lo+1)*100:.1f}% al bordo alto {np.mean(L>=hi-1)*100:.1f}%" + f" | |dL|/giorno mediano {np.median(np.abs(np.diff(L))):.1f}g") + + # -- 4. GATE --------------------------------------------------------------------------- + hr("4. GATE") + print(A.fmt_marginal(rep["marginal"])) + + cz = A.causality_ok(fn_ch, tf=ch["tf"]) + print(f"\n causality_ok : ok={cz['ok']} max_tail_diff={cz['max_tail_diff']} " + f"(prefissi controllati: {cz['checked']})") + + imp = A.implausible_sharpe(d_ch) + print(f" implausible_sharpe : implausible={imp['implausible']} Sh {imp.get('sharpe')} " + f"maxDD {imp.get('maxdd')} perdite/attive {imp.get('loss_frac')} " + f"attive {imp.get('active_frac')}") + for rr in imp["reasons"]: + print(f" - {rr}") + + dl = delever_null(d_ch, base_ch) + print(f" null del de-levering : {dl}") + + # small-cap a $600 + print(" haircut a $600 (min-order $5):") + for a in ASSETS: + df = A.get(a, ch["tf"]) + sc_c = A.eval_weights_smallcap(df, A._call_target(fn_ch, df, a), capital=600.0) + sc_b = A.eval_weights_smallcap(df, fixed_target(df), capital=600.0) + print(f" {a}: adattivo modellato {sc_c['modeled']['sharpe']:+.3f} -> reale " + f"{sc_c['realistic']['sharpe']:+.3f} (haircut {sc_c['sharpe_haircut']:+.3f}, " + f"{sc_c['n_executed_trades']} trade) | fisso {sc_b['modeled']['sharpe']:+.3f} -> " + f"{sc_b['realistic']['sharpe']:+.3f} (haircut {sc_b['sharpe_haircut']:+.3f}, " + f"{sc_b['n_executed_trades']} trade)") + + # -- 5. BANDA D'ANCORA ----------------------------------------------------------------- + hours = 24 if ch["tf"] == "1d" else 12 + offs = list(range(hours)) + hr(f"5. BANDA D'ANCORA β€” {len(offs)} ancore, stima onesta = MEDIANA (mai l'ancora 00:00)") + + def _series_at(off: int, which: str) -> pd.Series: + return series_at_anchor(fn_ch if which == "cand" else fixed_target, which, hours, off) + + # sanity: l'ancora 0 riproduce la serie canonica + s0 = _series_at(0, "fixed") + K = pd.concat({"a": s0, "b": base_ch}, axis=1, join="inner").dropna() + print(f" sanity ancora 0 vs serie canonica del fisso: max|Ξ”| = " + f"{float(np.max(np.abs(K['a']-K['b']))):.3e} (n={len(K)})") + + for lab, mtr in (("Sharpe FULL", A._sh), + ("Sharpe HOLD", lambda s: A._sh(s[s.index >= HOLDOUT]))): + b_c = A.anchor_luck_band(lambda o: _series_at(o, "cand"), offs, metric=mtr) + b_f = A.anchor_luck_band(lambda o: _series_at(o, "fixed"), offs, metric=mtr) + print(f" {lab:<12} adattivo: canonica {b_c['canonical']:+.3f} (pctl {b_c['canonical_pctl']:.2f}) " + f"MEDIANA {b_c['median']:+.3f} banda [{b_c['lo']:+.3f},{b_c['hi']:+.3f}]") + print(f" {'':<12} fisso : canonica {b_f['canonical']:+.3f} (pctl {b_f['canonical_pctl']:.2f}) " + f"MEDIANA {b_f['median']:+.3f} banda [{b_f['lo']:+.3f},{b_f['hi']:+.3f}]") + dlt = A.anchor_luck_delta(lambda o: _series_at(o, "cand"), + lambda o: _series_at(o, "fixed"), offs, metric=mtr) + print(f" {'':<12} DELTA APPAIATO: mediana {dlt['median_paired']:+.3f} " + f"positivo in {dlt['n_positive']}/{dlt['n_anchors']} ancore " + f"banda [{dlt['lo']:+.3f},{dlt['hi']:+.3f}] gate_pass={dlt['gate_pass']}") + + # -- 6. ADATTIVITA' EFFETTIVA DI TUTTA LA FAMIGLIA ------------------------------------ + hr("6. QUANTE DELLE 72 CELLE SONO DAVVERO ADATTIVE? (filtro di MECCANISMO, non di resa)") + print(" criterio: L_t sta a un bordo <50% del tempo E l'IQR di L_t e' >= 10 giorni") + seen, adatt_rows = {}, [] + for r in rep["rows"]: + k = (r["tf"], r["params"]["det"], r["params"]["par"], r["params"]["mapping"]) + if k not in seen: + seen[k] = adaptivity(*k) + r["_adapt"] = seen[k] + if seen[k]["adattiva"]: + adatt_rows.append(r) + n_ad_cells = sum(1 for r in rep["rows"] if r["_adapt"]["adattiva"]) + print(f" celle DAVVERO adattive: {n_ad_cells}/{len(rep['rows'])} " + f"(le altre sono TSMOM a orizzonte COSTANTE travestito)") + for k, v in sorted(seen.items()): + print(f" {k[0]:>3} {k[1]:>6} {str(k[2]):>5} {k[3]:<7} bordo-basso {v['frac_lo']*100:5.1f}% " + f"bordo-alto {v['frac_hi']*100:5.1f}% IQR {v['iqr_days']:5.1f}g " + f"mediana {v['med_days']:5.1f}g -> {'ADATTIVA' if v['adattiva'] else 'costante'}") + + # -- 7. IL CONTROLLO CHE MANCAVA: il MIGLIOR ORIZZONTE COSTANTE ------------------------ + hr(f"7. CONTROLLO L COSTANTE β€” {len(CONST_GRID)} celle x {len(TFS)} TF = " + f"{len(CONST_GRID)*len(TFS)} trial IN PIU'") + print(" la domanda diventa: il guadagno viene dall'ADATTARE, o dal fatto che su questo") + print(" campione un orizzonte piu' CORTO batte 30/90/180 anche se lo si tiene fisso?") + csel = A.select_cell_insample(lambda tf, L, blend: const_factory(tf, L, blend), + CONST_GRID, TFS) + cch = csel["chosen"] + print(f"\n miglior COSTANTE scelto in-sample: tf={cch['tf']} {cch['params']} " + f"Sharpe IS {cch['insample_sharpe']} FULL {cch['full_sharpe']}") + print(" griglia costante, in-sample (L=90 triple == TP01 canonico):") + for r in csel["rows"]: + tag = " <== TP01 canonico" if (r["params"]["L"] == 90 and r["params"]["blend"] == "triple" + and r["tf"] == "1d") else "" + print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} " + f"L={r['params']['L']:>3}g {r['params']['blend']}{tag}") + + fn_const = const_factory(**cch["params"], tf=cch["tf"]) + d_const = daily_from_target(fn_const, cch["tf"]) + JC = pd.concat({"C": d_ch, "K": d_const}, axis=1, join="inner").dropna() + print(f"\n ADATTIVO vs MIGLIOR COSTANTE (entrambi scelti in-sample):") + for lab, sl in (("FULL", JC), ("HOLD-OUT", JC[JC.index >= HOLDOUT])): + if len(sl) < 30: + continue + print(f" {lab:<9} adattivo Sh {A._sh(sl['C']):+.3f} DD {_dd(sl['C'])*100:5.2f}% | " + f"costante Sh {A._sh(sl['K']):+.3f} DD {_dd(sl['K'])*100:5.2f}% | " + f"dSh {A._sh(sl['C'])-A._sh(sl['K']):+.3f}") + print(f" corr(adattivo, miglior costante) = {JC['C'].corr(JC['K']):.3f}") + print("\n differenza APPAIATA per anno vs il miglior COSTANTE:") + rows2 = paired_by_year(d_ch, d_const) + for r in rows2: + print(f" {r['year']} adatt {r['sh_cand']:+.2f} costante {r['sh_base']:+.2f} " + f"dSh {r['d_sh']:+.2f}") + d2 = [r["d_sh"] for r in rows2] + print(f" -> anni con dSh>0: {sum(1 for x in d2 if x > 0)}/{len(d2)} " + f"mediana {np.median(d2):+.2f}") + dl2 = delever_null(d_ch, d_const) + print(f" null del de-levering vs costante: {dl2}") + + # ancora appaiata adattivo vs miglior costante (se stesso TF, altrimenti dichiarato) + if cch["tf"] == ch["tf"]: + for lab, mtr in (("Sharpe FULL", A._sh), + ("Sharpe HOLD", lambda s: A._sh(s[s.index >= HOLDOUT]))): + dd2 = A.anchor_luck_delta(lambda o: series_at_anchor(fn_ch, "cand", hours, o), + lambda o: series_at_anchor(fn_const, "const", hours, o), + offs, metric=mtr) + print(f" ancora {lab}: DELTA APPAIATO mediana {dd2['median_paired']:+.3f} " + f"positivo in {dd2['n_positive']}/{dd2['n_anchors']} " + f"banda [{dd2['lo']:+.3f},{dd2['hi']:+.3f}] gate_pass={dd2['gate_pass']}") + else: + print(f" (ancora appaiata non girata: TF diversi, adattivo {ch['tf']} vs " + f"costante {cch['tf']} -> le ancore non sono coppie)") + + # fee sweep: l'adattivo tradΓ  di piu', il conto va fatto + print("\n FEE SWEEP (il candidato ha piu' turnover: e' li' che muore o no)") + for nm, f_ in (("adattivo", fn_ch), ("costante", fn_const), ("TP01 30/90/180", fixed_target)): + tfx = ch["tf"] if nm != "costante" else cch["tf"] + line = [] + for fee in (0.0, 0.0005, 0.001, 0.0015): + dser = A.candidate_daily(f_, tf=tfx, fee_side=fee) + line.append(f"{2*fee*100:.2f}%RT {A._sh(dser):+.3f}") + tt = np.mean([A.eval_weights(A.get(a, tfx), A._call_target(f_, A.get(a, tfx), a)) + ["turnover_per_year"] for a in ASSETS]) + print(f" {nm:<16} " + " ".join(line) + f" | turnover {tt:.0f}x/anno") + + # -- 8. LA FAMIGLIA RISTRETTA ALLE CELLE DAVVERO ADATTIVE ----------------------------- + hr("8. GATE ONESTO SULLA SOLA FAMIGLIA ADATTIVA (la domanda della missione, isolata)") + if not adatt_rows: + print(" NESSUNA cella della famiglia e' davvero adattiva -> la domanda non e'") + print(" misurabile su questa famiglia: ogni 'vincitore' e' un orizzonte costante.") + else: + best_ad = max(adatt_rows, key=lambda r: r["insample_sharpe"]) + print(f" celle adattive: {len(adatt_rows)} miglior IN-SAMPLE: tf={best_ad['tf']} " + f"{best_ad['params']} IS {best_ad['insample_sharpe']} FULL {best_ad['full_sharpe']}") + fn_ad = factory(tf=best_ad["tf"], **best_ad["params"]) + d_ad = daily_from_target(fn_ad, best_ad["tf"]) + sm_ad = A.study_marginal("ADAPT-H(solo adattive)", fn_ad, tf=best_ad["tf"]) + dsr_ad, sr0_ad = A.deflated_sharpe(A._sh(d_ad), + [r["full_sharpe"] for r in adatt_rows], d_ad) + print(f" marginal={sm_ad['marginal_verdict']} earns_slot={sm_ad['earns_slot']} " + f"corr->TP01 {sm_ad['marginal'].get('corr_full')}") + print(f" deflated-Sharpe (N={len(adatt_rows)}) = {dsr_ad:.3f} " + f"{'PASS' if dsr_ad >= 0.95 else 'FAIL'} (null max atteso {sr0_ad:.3f})") + JA = pd.concat({"C": d_ad, "K": d_const, "B": base_1d}, axis=1, join="inner").dropna() + for lab, sl in (("FULL", JA), ("HOLD-OUT", JA[JA.index >= HOLDOUT])): + if len(sl) < 30: + continue + print(f" {lab:<9} adattiva-vera Sh {A._sh(sl['C']):+.3f} | miglior costante " + f"{A._sh(sl['K']):+.3f} | TP01 {A._sh(sl['B']):+.3f}") + rows3 = paired_by_year(d_ad, d_const) + d3 = [r["d_sh"] for r in rows3] + print(f" vs miglior costante, per anno: dSh>0 in {sum(1 for x in d3 if x>0)}/{len(d3)}" + f" mediana {np.median(d3):+.2f}") + + # -- 9. L'OTTIMO COSTANTE E' AL BORDO: si estende la banda ---------------------------- + hr("9. L'OTTIMO COSTANTE CADE SUL BORDO β€” plateau o scivolata verso il rumore?") + print(" un argmax al bordo della griglia non e' un ottimo: e' il punto in cui si e' smesso") + print(" di guardare. Si estende SOTTO i 20 giorni (8 trial in piu', contati al rialzo).") + ext = sorted(set(CONST_L) | set(CONST_L_EXT)) + tab: dict = {} + for tfx in TFS: + print(f"\n --- TF {tfx}, blend triple (L/3, L, 2L) ---") + print(f" {'L(g)':>5} {'orizzonti':>16} {'IS':>7} {'FULL':>7} {'HOLD':>7} " + f"{'maxDD':>7} {'CAGR':>7} {'turn/a':>7} {'Sh@0.30%RT':>11}") + for L in ext: + fnx = const_factory(tfx, L, "triple") + dx = daily_from_target(fnx, tfx) + dx_h = dx[dx.index >= HOLDOUT] + dx_i = dx[dx.index < HOLDOUT] + d030 = A.candidate_daily(fnx, tf=tfx, fee_side=0.0015) + tt = np.mean([A.eval_weights(A.get(a, tfx), A._call_target(fnx, A.get(a, tfx), a)) + ["turnover_per_year"] for a in ASSETS]) + tag = " <== TP01" if L == 90 else "" + tab[(tfx, L)] = dict(IS=A._sh(dx_i), FULL=A._sh(dx), HOLD=A._sh(dx_h)) + print(f" {L:>5} {f'{L//3}/{L}/{2*L}':>16} {A._sh(dx_i):>7.3f} {A._sh(dx):>7.3f} " + f"{A._sh(dx_h):>7.3f} {_dd(dx)*100:>6.2f}% {_cagr(dx)*100:>6.2f}% " + f"{tt:>7.0f} {A._sh(d030):>11.3f}{tag}") + + # --- e' una CURVA con un ottimo, o un crinale di RUMORE? -------------------------- + print("\n RISOLUZIONE della griglia (regola 07/08: un criterio che decide su una frazione") + print(" dello spread della griglia e' rumore anche quando 'passa'):") + for tfx in TFS: + v = [tab[(tfx, L)]["IS"] for L in ext] + jump = float(np.median(np.abs(np.diff(v)))) + arg = ext[int(np.argmax(v))] + arg_orig = max(CONST_L, key=lambda L: tab[(tfx, L)]["IS"]) + print(f" {tfx:>3}: argmax IS con la griglia ORIGINALE L={arg_orig}g -> con la griglia " + f"ESTESA L={arg}g ({'SPOSTATO' if arg != arg_orig else 'stabile'})") + print(f" salto IS mediano fra L ADIACENTI {jump:+.3f} contro il vantaggio del " + f"vincitore su TP01 {max(v) - tab[(tfx, 90)]['IS']:+.3f} " + f"(rapporto {jump / max(1e-9, max(v) - tab[(tfx, 90)]['IS']):.2f})") + allc = [(tf, L) for tf in TFS for L in ext] + for met in ("IS", "FULL", "HOLD"): + vals = sorted((tab[k][met] for k in allc), reverse=True) + r1d = 1 + sum(1 for x in vals if x > tab[("1d", 90)][met]) + print(f" rango di TP01 (1d, L=90g) fra le {len(allc)} celle su {met}: " + f"{r1d}/{len(allc)} (suo {tab[('1d',90)][met]:+.3f}, migliore {vals[0]:+.3f})") + + hr("FINE") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_alt_options.py b/scripts/research/r0822_alt_options.py new file mode 100644 index 0000000..7a6af36 --- /dev/null +++ b/scripts/research/r0822_alt_options.py @@ -0,0 +1,709 @@ +"""ALT-OPT β€” la famiglia opzioni USDC-LINEARE di Deribit, mai guardata dal progetto (2026-08-22). + +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 +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? + +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 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 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, 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 ~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 +""" +from __future__ import annotations + +import argparse +import json +import math +import time +from datetime import UTC, datetime +from pathlib import Path + +import numpy as np +import requests + +API = "https://www.deribit.com/api/v2/public" +TIMEOUT = 20 +RPS = 2.0 +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) 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 = ("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) + + +class Budget: + def __init__(self, rps: float = RPS) -> None: + self.rps, self._next, self.calls, self.err, self.r429 = rps, 0.0, 0, 0, 0 + + def wait(self) -> None: + now = time.monotonic() + if now < self._next: + time.sleep(self._next - now) + self._next = max(now, self._next) + 1.0 / self.rps + + +def guardia_finestra() -> None: + m = datetime.now(UTC).minute + if 24 <= m <= 29: + raise SystemExit(f"minuto {m}: finestra di cron_chain (:25-:29). Rilancia dopo il :30.") + + +def get(path: str, params: dict, b: Budget, tries: int = 3): + for k in range(tries): + b.wait() + b.calls += 1 + try: + r = requests.get(f"{API}/{path}", params=params, timeout=TIMEOUT) + except Exception: + b.err += 1 + time.sleep(1.5 * (k + 1)) + continue + if r.status_code == 429: + b.r429 += 1 + time.sleep(2.0 * (k + 1)) + continue + if r.status_code != 200: + b.err += 1 + time.sleep(1.0 * (k + 1)) + continue + try: + return r.json()["result"] + except Exception: + b.err += 1 + return None + return None + + +def fetch_all(b: Budget) -> dict: + """6 chiamate: instruments + book_summary per USDC, BTC, ETH. Niente per-strumento.""" + out = {} + for cur in ("USDC", "BTC", "ETH"): + out[f"inst_{cur}"] = get("get_instruments", + {"currency": cur, "kind": "option", "expired": "false"}, b) or [] + out[f"summ_{cur}"] = get("get_book_summary_by_currency", + {"currency": cur, "kind": "option"}, b) or [] + return out + + +# ---------------------------------------------------------------- Black-Scholes (r=0, come il sleeve) + + +def put_delta(S: float, K: float, T: float, sig: float) -> float: + if not (S > 0 and K > 0 and T > 0 and sig > 0): + return float("nan") + d1 = (math.log(S / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T)) + return 0.5 * (1.0 + math.erf(d1 / math.sqrt(2.0))) - 1.0 + + +# ---------------------------------------------------------------- unita' + + +def is_inv(fam: str) -> bool: + return fam in INVERSE + + +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 -> 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). + """ + 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, per un lotto minimo. + + 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["minamt"] + + +# ---------------------------------------------------------------- margine (FONTE SECONDARIA) + + +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. 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. + """ + if not (S > 0 and K > 0): + return float("nan") + otm = max(0.0, S - K) / S # una put e' OTM quando K < S + return max(max(0.15 - otm, 0.10), 0.075) * S * r["minamt"] + max(0.0, mark_lot) + + +# ---------------------------------------------------------------- vista + + +def build(raw: dict) -> list[dict]: + inst, summ = {}, {} + for cur in ("USDC", "BTC", "ETH"): + for i in raw.get(f"inst_{cur}") or []: + 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 + for name, i in inst.items(): + s = summ.get(name) + if s is None: + continue + fam = name.split("-")[0] + dte = (float(i.get("expiration_timestamp") or 0) - now_ms) / 86400_000.0 + bid, ask = s.get("bid_price"), s.get("ask_price") + 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, T = float(i.get("strike") or 0), max(dte, 0.0) / 365.25 + typ = "P" if i.get("option_type") == "put" else "C" + 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=(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), + 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), + 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 + + +def rel_spread(r: dict) -> float: + if r["bid"] is None or r["ask"] is None or not r["mid"]: + return float("nan") + return (r["ask"] - r["bid"]) / r["mid"] + + +# ---------------------------------------------------------------- Β§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("1. I FATTI DEL VENUE β€” public/get_instruments + get_book_summary_by_currency") + print("=" * 104) + 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 FAMS: + sub = [r for r in rows if r["fam"] == f] + if not sub: + continue + S = float(np.nanmedian([r["S"] for r in sub])) + a = sub[0] + 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: 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 + + +# ---------------------------------------------------------------- Β§2 liquidita', due modi + + +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}") + # la correlazione di rango si CALCOLA, non si racconta: sulle sole famiglie lineari, + # cioe' quelle su cui la domanda del filone e' stata posta. + a = np.array([rA[f] for f in LINEAR], float) + c = np.array([rB[f] for f in LINEAR], float) + rho = float(np.corrcoef(a, c)[0, 1]) + print(f"\n πŸ“Œ LA CLASSIFICA SI ROVESCIA. Correlazione di rango fra i due criteri sulle sei") + print(f" famiglie lineari: {rho:+.2f}. SOL_USDC e' 1Βͺ per open interest fra le lineari e") + print(" ULTIMA di tutte per quote presenti; BTC_USDC, dichiarato 'praticamente morto' (5") + print(" strumenti con OI>=100), ha quasi tutti i put quotati a due lati. L'open interest") + print(" 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'.") + + print("\n" + "-" * 104) + print("2b. E ALLORA DOV'E' LA LIQUIDITA' CHE SI VEDE NEL VOLUME? Nel mercato a BLOCCHI,") + print(" che ha un minimo d'ordine suo β€” e non e' raggiungibile con $635.") + print("-" * 104) + print(f"{'famiglia':<10} {'con volume 24h':>15} {'volume 24h $':>14} {'blocco min':>11} " + f"{'blocco min $':>13} {'x il conto':>11}") + for f in FAMS: + sub = [r for r in rows if r["fam"] == f] + if not sub: + continue + nv = sum(1 for r in sub if r["vol"] > 0) + vusd = sum(r["vol_usd"] for r in sub) + bm = sub[0]["blockmin"] + bm_usd = bm * tab[f]["S"] * (1.0 if not is_inv(f) else 1.0) + print(f"{f:<10} {nv:>15} {vusd:>14,.0f} {bm:>11g} {bm_usd:>13,.0f} " + f"{bm_usd/CAPITAL:>10.0f}x") + print("\n πŸ“Œ SOL_USDC ha molti piu' strumenti con volume nelle ultime 24h che quotati a due") + print(" lati: quelle opzioni si negoziano, ma via blocchi/RFQ. Il minimo di blocco e' un") + print(" fatto del venue, non un'opinione, e a $635 e' fuori portata di due ordini di") + print(" grandezza. **Non si conclude che il mercato alt non esista: si conclude che la parte") + print(" a cui questo conto ha accesso β€” il book continuo β€” e' quella sottile.**") + + +# ---------------------------------------------------------------- Β§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"])] + 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] + 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": len(g)}) + return out + + +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"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_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 f in FAMS: + gg = scegli(rows, f, eseguibile=True) + if not gg: + print(f"{f:<10} {'0':>4} nessuna scadenza con corta-bid E ala-ask presenti") + continue + acc = {k: [] for k in ("ds", "dl", "ss", "sl", "cm", "ce", "fee", "ml")} + for g in gg: + s, lo = g["short"], g["long"] + cm = usd(s["mark"], s) - usd(lo["mark"], lo) + ce = usd(s["bid"], s) - usd(lo["ask"], lo) + if cm <= 0: + continue + 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 + 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.") + + print("\n --- CONTROLLO POSITIVO: questo snapshot e' di SABATO. E' atipico? ---") + print(" Non posso aspettare una settimana, ma posso puntare lo stesso strumento su una") + print(" grandezza che il progetto ha GIA' misurato su 19 settimane di giorni feriali:") + print(" attraversare lo spread sul canonico BTC/ETH inverse costa ~10% del credito (30/07).") + for f in INVERSE: + if f in res: + print(f" {f:<4} inverse: costo di attraversamento oggi = " + f"{(1-res[f]['fv'])*100:5.1f}% del credito (atteso ~10%)") + print(" Se coincide, il sabato non sta degradando i book che il progetto conosce, e la") + print(" lettura sulle famiglie nuove non e' un artefatto del giorno. **E' un controllo") + print(" debole** (una grandezza sola, un istante), non una prova di tipicita'.") + robustezza_dte(rows) + return res + + +def robustezza_dte(rows: list[dict]) -> None: + """L'UNICO grado di liberta' di questo file, esposto invece che nascosto. + + VRP01 e' settimanale (tenor 7g, finestra 4-10). Qui la finestra e' stata allargata a 4-45 + per dare piu' scadenze alle famiglie alt, che ne hanno poche. Allargare una finestra e' + una scelta, e una scelta va misurata: se la conclusione dipendesse da essa, sarebbe la + conclusione a essere stata scelta. + """ + global DTE_HI + print("\n --- ROBUSTEZZA: la finestra DTE e' il mio unico grado di liberta' ---") + print(f" {'famiglia':<10} {'finestra':>9} {'scad':>5} {'f_venue':>8} {'spr% corta':>11} " + f"{'cred eseg$':>11} {'maxloss$':>9}") + orig = DTE_HI + try: + for hi in (10.0, 45.0): + DTE_HI = hi + for f in FAMS: + gg = scegli(rows, f, eseguibile=True) + fv, ss, ce, ml = [], [], [], [] + for g in gg: + s, lo = g["short"], g["long"] + cm = usd(s["mark"], s) - usd(lo["mark"], lo) + c = usd(s["bid"], s) - usd(lo["ask"], lo) + if cm <= 0: + continue + fv.append(c / cm); ss.append(rel_spread(s)); ce.append(c) + ml.append(loss_usd(s["K"], lo["K"], s) - c) + w = f"{DTE_LO:.0f}-{hi:.0f}g" + if not fv: + print(f" {f:<10} {w:>9} {len(gg):>5} {'n/d':>8}") + continue + print(f" {f:<10} {w:>9} {len(fv):>5} {np.median(fv):>8.3f} " + f"{np.nanmedian(ss)*100:>10.1f}% {np.median(ce):>11.2f} " + f"{np.median(ml):>9.2f}") + print() + finally: + DTE_HI = orig + print(" πŸ“Œ Da leggere sull'ORDINE, non sulla terza cifra: le quote si muovono fra un giro e") + print(" l'altro (su tre snapshot a pochi minuti ETH_USDC ha dato 0.947-0.983, le inverse") + print(" 0.847-0.942, le alt 0.79-0.87, SOL sempre negativo). Cio' che NON si muove e' la") + print(" graduatoria, e alla finestra VERA di VRP01 (4-10g) e' piu' netta, non meno.") + + +def profondita(rows: list[dict], fams: list[str], b: Budget) -> None: + print("\n" + "=" * 104) + 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"{'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 or not (ob.get(side) or []): + continue + 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(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"]): + dd.append(abs(float(gv) - r["delta"])) + if not lots: + print(f"{f:<10} {lato:<6} {'0':>3} book vuoto sul lato che serve") + continue + 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. `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.") + + +# ---------------------------------------------------------------- Β§4 collaterale e capacita' + + +def collaterale(rows: list[dict], res: dict) -> None: + print("\n" + "=" * 104) + print(f"4. COLLATERALE E CAPACITA' β€” conto reale ${CAPITAL:.0f}") + print("=" * 104) + 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 lo["ask"] is None: + continue + 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)), 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 = 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(rows: list[dict], tab: dict) -> None: + print("\n" + "=" * 104) + 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} {'a OI>=100':>10} {'a 2-lati':>9} {'+s a 4rps (2-lati)':>19} " + f"{'+% sul giro':>12}") + for f in LINEAR: + 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.") + + +# ---------------------------------------------------------------- main + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--cache", action="store_true", help="riusa lo snapshot su disco (niente rete)") + a = ap.parse_args() + + print("=" * 104) + print("ALT-OPT β€” opzioni USDC-LINEARI su Deribit: c'e' una struttura eseguibile a $635?") + print("=" * 104) + print("Sola lettura pubblica, nessun ordine. UNO SNAPSHOT: qui non esiste un backtest.") + + b, rete = Budget(), not a.cache + if a.cache and CACHE.exists(): + raw = json.loads(CACHE.read_text()) + print(f"\n[cache] {CACHE}") + else: + guardia_finestra() + t0 = time.time() + 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} 429, {time.time()-t0:.0f}s") + + rows = build(raw) + print(f"[dato] {len(rows)} strumenti con specifiche E quote β€” snapshot " + f"{datetime.now(UTC):%Y-%m-%d %H:%M} UTC") + if not rows: + print("NESSUN DATO: il venue non ha risposto. Verdetto: non misurabile.") + return 1 + + unita(rows, b, rete) + tab = famiglie(rows) + liquidita(rows, tab) + res = struttura(rows) + + 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(rows, tab) + + print("\n" + "=" * 104) + 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") + 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, e il netting delle gambe non e'") + print(" leggibile pubblicamente.") + 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 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/research/r0822_basis_calendar.py b/scripts/research/r0822_basis_calendar.py new file mode 100644 index 0000000..cd7227a --- /dev/null +++ b/scripts/research/r0822_basis_calendar.py @@ -0,0 +1,985 @@ +#!/usr/bin/env python3 +""" +r0822_basis_calendar.py β€” filone BASIS-CALENDAR (ondata 2026-08-22) + +DOMANDA + Il progetto ha chiuso il FUNDING su tre lati (CC01 carry spot-perp, time-series, + cross-sectional) ma non ha mai guardato la STRUTTURA A TERMINE dei futures datati + di Deribit β€” l'unico strumento delta-neutro eseguibile sul NOSTRO venue, a 2 gambe. + Q1 Il basis (datato vs indice/perp) e la sua pendenza sono PREVEDIBILI, cioe' + esiste un roll-down incassabile, o e' un martingala rumoroso? + Q2 Un CALENDAR SPREAD (front vs back) su segnale di pendenza produce un edge + netto di fee e spread bid-ask REALI? + Q3 E' ortogonale al libro ed eseguibile a $600-3.000? + +IL DATO β€” costruito qui, non esisteva + `data/raw/fut_*` sono futures INDICE AZIONARIO da IB (ES/NQ/DAX...), niente a che + vedere. Su Deribit non c'era nulla. Ma: + * /public/get_instruments?expired=true ritorna UN SOLO strumento (Deribit purga + subito le liste), quindi la via ovvia non funziona; + * 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. + Verificato: 30/30 trimestrali 2019Q1..2026Q2 rispondono `status=ok`. + => **7+ anni di storia**, che includono il deleveraging 2022. E' esattamente la coda + che a CC01 mancava per costruzione (funding HL dal 2023-05): qui il 2022 c'e'. + + La chiamata funding porta in regalo l'INDICE Deribit orario (`index_price`), quindi + spot e funding arrivano insieme e il basis si misura sull'indice vero, non su una proxy. + +DISCIPLINA DI RETE (la VPS ha un IP solo e ci gira il libro) + <=2 richieste/s, e il fetcher DORME nella finestra :25-:29 di ogni ora, che e' + `cron_chain` (il collettore della catena opzioni). Il progetto ha gia' pagato un'ora + di guasto per un IP saturato. Tutto e' in cache su scratchpad: un ri-run non tocca la rete. + +CONVENZIONE DI ROLL β€” dichiarata, e mai una serie incollata + I prezzi NON vengono mai splicciati. La posizione e' su una COPPIA DI CONTRATTI + specifica; al roll si chiude e si riapre pagando il round-trip pieno su entrambe le + gambe. L'incollaggio avviene solo nella serie dei RENDIMENTI, che e' l'unico posto in + cui e' lecito: e' l'errore che il progetto ha gia' commesso con le serie equity + ri-aggiustate, e qui e' strutturalmente impossibile. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0822_basis_calendar.py +""" +from __future__ import annotations + +import calendar +import datetime as dt +import json +import math +import os +import sys +import time +import urllib.parse +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) +sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt")) + +import altlib as A # noqa: E402 + +SCRATCH = Path( + os.environ.get( + "R0822_SCRATCH", + "/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad", + ) +) +CACHE = SCRATCH / "basis_cache" +CACHE.mkdir(parents=True, exist_ok=True) + +API = "https://www.deribit.com/api/v2" +UTC = dt.timezone.utc +MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", + "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] + +ASSETS = ("BTC", "ETH") +FEE_SIDE = 0.00035 # 3,5 bps/lato per GAMBA (taker Deribit, letto dal venue) +HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC") +HOURS_Y = 365.25 * 24 + +_last_call = [0.0] + + +# ========================================================================== +# 0. FETCHER β€” rate-limited, cache-first, consapevole dei cron della VPS +# ========================================================================== +def _polite_sleep() -> None: + """<=2 req/s E fuori dalla finestra :25-:29 (cron_chain, stesso IP).""" + now = dt.datetime.now(UTC) + if 25 <= now.minute <= 29: + wait = (30 - now.minute) * 60 - now.second + print(f" [rete] finestra cron_chain (:25-:29) -> dormo {wait}s") + time.sleep(max(1, wait)) + gap = time.time() - _last_call[0] + if gap < 0.55: + time.sleep(0.55 - gap) + _last_call[0] = time.time() + + +def _api(path: str, **kw) -> dict: + url = API + path + "?" + urllib.parse.urlencode(kw) + for attempt in range(4): + _polite_sleep() + try: + with urllib.request.urlopen(url, timeout=30) as r: + return json.load(r) + except Exception as exc: # noqa: BLE001 + if attempt == 3: + raise + print(f" [rete] {type(exc).__name__} -> retry {attempt+1}/3") + time.sleep(2.0 * (attempt + 1)) + raise RuntimeError("unreachable") + + +def last_friday(year: int, month: int) -> dt.date: + d = dt.date(year, month, calendar.monthrange(year, month)[1]) + while d.weekday() != 4: + d -= dt.timedelta(days=1) + return d + + +def quarterly_universe(asset: str, y0: int = 2019, y1: int = 2027) -> list[tuple[str, pd.Timestamp]]: + """Nomi DETERMINISTICI dei trimestrali Deribit: ultimo venerdi' di MAR/GIU/SET/DIC 08:00 UTC.""" + out = [] + for y in range(y0, y1 + 1): + for m in (3, 6, 9, 12): + lf = last_friday(y, m) + exp = pd.Timestamp(lf.year, lf.month, lf.day, 8, tz="UTC") + out.append((f"{asset}-{lf.day}{MONTHS[m-1]}{str(y)[2:]}", exp)) + return out + + +def fetch_ohlc(instrument: str, t0: pd.Timestamp, t1: pd.Timestamp) -> pd.DataFrame: + """OHLCV orario di UNO strumento (anche SCADUTO), a chunk di 90 giorni. Cache su disco.""" + cf = CACHE / f"ohlc_{instrument.replace('/', '_')}.parquet" + if cf.exists(): + return pd.read_parquet(cf) + frames = [] + cur = t0 + while cur < t1: + end = min(cur + pd.Timedelta(days=90), t1) + try: + res = _api("/public/get_tradingview_chart_data", + instrument_name=instrument, resolution="60", + start_timestamp=int(cur.timestamp() * 1000), + end_timestamp=int(end.timestamp() * 1000)).get("result", {}) + except Exception as exc: # noqa: BLE001 + print(f" {instrument}: chunk {cur.date()} FALLITO ({type(exc).__name__})") + cur = end + continue + ticks = res.get("ticks") or [] + if res.get("status") == "ok" and ticks: + frames.append(pd.DataFrame({ + "ts": pd.to_datetime(ticks, unit="ms", utc=True), + "open": res["open"], "high": res["high"], + "low": res["low"], "close": res["close"], + "volume": res.get("volume", [np.nan] * len(ticks)), + })) + cur = end + df = (pd.concat(frames).drop_duplicates("ts").sort_values("ts").reset_index(drop=True) + if frames else pd.DataFrame(columns=["ts", "open", "high", "low", "close", "volume"])) + df.to_parquet(cf, index=False) + return df + + +def fetch_funding_and_index(asset: str, t0: pd.Timestamp, t1: pd.Timestamp) -> pd.DataFrame: + """Funding orario del perp E indice Deribit (la chiamata li porta insieme).""" + cf = CACHE / f"fund_{asset}.parquet" + if cf.exists(): + return pd.read_parquet(cf) + frames, cur = [], t0 + while cur < t1: + end = min(cur + pd.Timedelta(days=30), t1) + try: + res = _api("/public/get_funding_rate_history", + instrument_name=f"{asset}-PERPETUAL", + start_timestamp=int(cur.timestamp() * 1000), + end_timestamp=int(end.timestamp() * 1000)).get("result", []) + except Exception: # noqa: BLE001 + cur = end + continue + if res: + frames.append(pd.DataFrame({ + "ts": pd.to_datetime([r["timestamp"] for r in res], unit="ms", utc=True), + "index": [r["index_price"] for r in res], + "f1h": [r["interest_1h"] for r in res], + })) + cur = end + df = (pd.concat(frames).drop_duplicates("ts").sort_values("ts").reset_index(drop=True) + if frames else pd.DataFrame(columns=["ts", "index", "f1h"])) + df.to_parquet(cf, index=False) + return df + + +def fetch_live_books(instruments: list[str]) -> pd.DataFrame: + """Calibrazione DATATA dello spread bid-ask: il book VIVO di oggi. + Non e' storia β€” e' un punto di ancoraggio per una stima, e va detto.""" + cf = CACHE / "live_books.parquet" + if cf.exists(): + return pd.read_parquet(cf) + rows = [] + for nm in instruments: + try: + r = _api("/public/get_order_book", instrument_name=nm, depth=5)["result"] + bid, ask = r.get("best_bid_price"), r.get("best_ask_price") + if bid and ask and bid > 0: + rows.append(dict(instrument=nm, bid=bid, ask=ask, + mid=(bid + ask) / 2, + spread_bps=1e4 * (ask - bid) / ((ask + bid) / 2), + bid_sz=r.get("best_bid_amount"), ask_sz=r.get("best_ask_amount"))) + except Exception: # noqa: BLE001 + continue + df = pd.DataFrame(rows) + df.to_parquet(cf, index=False) + return df + + +# ========================================================================== +# 1. PANEL + CERTIFICAZIONE (prima di ogni strategia β€” regola di prim'ordine) +# ========================================================================== +def build_panel(asset: str) -> tuple[pd.DataFrame, pd.DataFrame]: + """Ritorna (wide, meta). `wide` = indice orario x contratto -> close. + `meta` = una riga per contratto con scadenza, barre, copertura, quota barre FLAT.""" + uni = quarterly_universe(asset) + today = pd.Timestamp.now(tz="UTC") + closes, vols, meta = {}, {}, [] + for nm, exp in uni: + if exp > today + pd.Timedelta(days=400): + continue + t0 = exp - pd.Timedelta(days=400) + t1 = min(exp + pd.Timedelta(hours=2), today) + if t1 <= t0: + continue + df = fetch_ohlc(nm, t0, t1) + if df.empty or len(df) < 200: + meta.append(dict(contract=nm, exp=exp, bars=len(df), status="VUOTO/CORTO")) + continue + s = df.set_index("ts")["close"].astype(float) + s = s[s > 0] + closes[nm] = s + vols[nm] = df.set_index("ts")["volume"].astype(float) + flat = float((s.diff() == 0).mean()) + meta.append(dict(contract=nm, exp=exp, bars=len(s), + first=s.index.min(), last=s.index.max(), + flat_frac=flat, status="ok")) + wide = pd.DataFrame(closes).sort_index() + vwide = pd.DataFrame(vols).sort_index() + return wide, vwide, pd.DataFrame(meta) + + +def certify(asset: str, wide: pd.DataFrame, vwide: pd.DataFrame, meta: pd.DataFrame, + fund: pd.DataFrame, perp: pd.Series) -> dict: + """Quattro difetti sorvegliati. Un edge su un book fermo NON e' un edge (regola 4).""" + print(f"\n --- CERTIFICAZIONE {asset} ---") + ok = meta[meta.status == "ok"] + print(f" contratti con dato: {len(ok)}/{len(meta)} " + f"finestra {wide.index.min().date()} .. {wide.index.max().date()}") + + # (a) barre FLAT per bucket di giorni-a-scadenza = liquidita' vera del datato + rows = [] + for nm in wide.columns: + exp = ok.loc[ok.contract == nm, "exp"] + if exp.empty: + continue + s = wide[nm].dropna() + dte = (exp.iloc[0] - s.index).total_seconds() / 86400.0 + flat = (s.diff() == 0).astype(float) + for lo, hi, lab in [(0, 30, "0-30g"), (30, 90, "30-90g"), + (90, 180, "90-180g"), (180, 400, ">180g")]: + m = (dte >= lo) & (dte < hi) + if m.sum() > 50: + rows.append(dict(bucket=lab, flat=float(flat[m].mean()), n=int(m.sum()))) + fb = pd.DataFrame(rows).groupby("bucket").agg(flat=("flat", "mean"), n=("n", "sum")) + order = ["0-30g", "30-90g", "90-180g", ">180g"] + print(" barre FLAT (proxy di illiquidita') per giorni-a-scadenza:") + for b in order: + if b in fb.index: + print(f" {b:9s} flat={fb.loc[b,'flat']*100:5.1f}% barre={int(fb.loc[b,'n']):,}") + + # (b) volume orario per bucket (il flat da solo non basta). + # ATTENZIONE: il `volume` di get_tradingview_chart_data e' in VALUTA BASE (BTC/ETH), + # non in USD -> va moltiplicato per il prezzo, o si sbaglia di ~5 ordini di grandezza. + vrows = [] + for nm in vwide.columns: + exp = ok.loc[ok.contract == nm, "exp"] + if exp.empty: + continue + v = vwide[nm].dropna() + px = wide[nm].reindex(v.index) + usd = (v * px).dropna() # <- conversione in USD + dte = (exp.iloc[0] - usd.index).total_seconds() / 86400.0 + for lo, hi, lab in [(0, 30, "0-30g"), (30, 90, "30-90g"), + (90, 180, "90-180g"), (180, 400, ">180g")]: + mm = (dte >= lo) & (dte < hi) + if mm.sum() > 50: + vrows.append(dict(bucket=lab, med=float(usd[mm].median()), + zero=float((usd[mm] == 0).mean()))) + vb = pd.DataFrame(vrows).groupby("bucket").agg(med=("med", "median"), zero=("zero", "mean")) + print(" volume orario mediano in USD (volume base x prezzo) e quota ore a volume ZERO:") + for b in order: + if b in vb.index: + print(f" {b:9s} mediano=${vb.loc[b,'med']:>12,.0f}/ora ore a vol 0 = {vb.loc[b,'zero']*100:5.1f}%") + + # (c) accordo con l'INDICE alla scadenza: il datato DEVE convergere + conv = [] + for nm in wide.columns: + e = ok.loc[ok.contract == nm, "exp"] + if e.empty: + continue + exp = e.iloc[0] + s = wide[nm].dropna() + tail = s[s.index >= exp - pd.Timedelta(hours=6)] + idx = fund.set_index("ts")["index"].reindex(tail.index, method="nearest", tolerance=pd.Timedelta("2h")) + if len(tail) and idx.notna().any(): + conv.append(float(np.abs(np.log(tail / idx)).dropna().iloc[-1]) * 1e4) + if conv: + print(f" convergenza a scadenza |ln(F/indice)| ultima ora: " + f"mediana {np.median(conv):.1f} bps, max {np.max(conv):.1f} bps (n={len(conv)})") + + # (c-bis) scarto PERP vs INDICE: la gamba FvP lo assume piccolo, quindi si misura + pj = perp.reindex(fund.set_index("ts").index).dropna() + if len(pj) > 1000: + ii = fund.set_index("ts")["index"].reindex(pj.index) + pb = (np.log(pj / ii) * 1e4).replace([np.inf, -np.inf], np.nan).dropna() + print(f" perp vs indice: mediana {pb.median():+.1f} bps, p95 |scarto| " + f"{pb.abs().quantile(0.95):.1f} bps (n={len(pb):,}) β€” la gamba FvP e' sul PERP") + + # (d) CROSS-CHECK INDIPENDENTE: il forward implicito nella catena opzioni + xchk = cross_check_option_forward(asset, wide, ok) + return dict(flat=fb, vol=vb, conv=conv, xchk=xchk) + + +def cross_check_option_forward(asset: str, wide: pd.DataFrame, meta: pd.DataFrame) -> dict: + """Il `underlying_price` di un'opzione Deribit E' il forward della SUA scadenza. + La catena raccolta dal 2026-07-30 e' quindi una MISURA INDIPENDENTE del basis: + se il mio F(T) e il forward delle opzioni sulla stessa scadenza non coincidono, + uno dei due e' sbagliato. (L'archivio ereditato da bite ha underlying_price NULL.)""" + import glob + files = sorted(glob.glob(str(PROJECT_ROOT / "data/raw/cb_chain/2026-*.parquet"))) + if not files: + return dict(status="catena assente") + keep = [] + for f in files[-30:]: + try: + d = pd.read_parquet(f, columns=["asset", "exp", "ts", "underlying_price", "index_price"]) + except Exception: # noqa: BLE001 + continue + d = d[(d.asset == asset) & d.underlying_price.notna()] + if len(d): + keep.append(d) + if not keep: + return dict(status="nessun underlying_price") + ch = pd.concat(keep) + ch["ts_h"] = ch["ts"].dt.floor("1h") + # forward mediano per (ora, scadenza) β€” tutti gli strike condividono lo stesso forward + fw = ch.groupby(["ts_h", "exp"]).agg(fwd=("underlying_price", "median")).reset_index() + rows = [] + for nm in wide.columns: + e = meta.loc[meta.contract == nm, "exp"] + if e.empty: + continue + exp = e.iloc[0] + sub = fw[fw.exp == exp] + if sub.empty: + continue + mine = wide[nm].dropna() + j = sub.set_index("ts_h")["fwd"].reindex(mine.index).dropna() + if len(j) < 5: + continue + dev = (np.log(mine.reindex(j.index) / j) * 1e4).dropna() + rows.append(dict(contract=nm, n=len(dev), med_bps=float(dev.median()), + p95_bps=float(dev.abs().quantile(0.95)))) + return dict(status="ok", table=pd.DataFrame(rows)) + + +# ========================================================================== +# 2. CURVA β€” front/back trimestrali, basis annualizzato, slope forward +# ========================================================================== +def curve_frame(asset: str, wide: pd.DataFrame, meta: pd.DataFrame, + fund: pd.DataFrame, roll_dte: int, + perp: pd.Series | None = None) -> pd.DataFrame: + """Per ogni ora: front = trimestrale piu' vicino con dte >= roll_dte, back = il successivo. + Ritorna prezzi, tau, basis annualizzati, slope forward e funding.""" + ok = meta[meta.status == "ok"].sort_values("exp") + exps = {r.contract: r.exp for r in ok.itertuples()} + idx = wide.index + F = fund.set_index("ts").reindex(idx).ffill(limit=2) + n = len(idx) + order = [c for c in ok.contract if c in wide.columns] # ordine di SCADENZA + if len(order) < 2: + raise RuntimeError("meno di due contratti utilizzabili") + + # ALIVE[i, k] = il k-esimo contratto (per scadenza) e' quotato a i E ha dte >= roll + TAU = np.column_stack([(exps[c] - idx).total_seconds().values / 3600.0 for c in order]) + AV = np.column_stack([wide[c].notna().values for c in order]) + ALIVE = AV & (TAU >= roll_dte * 24) + + any1 = ALIVE.any(axis=1) + k1 = np.argmax(ALIVE, axis=1) # primo vivo = FRONT + A2 = ALIVE.copy() + A2[np.arange(n), k1] = False + any2 = A2.any(axis=1) + k2 = np.argmax(A2, axis=1) # secondo vivo = BACK + m = any1 & any2 + + front = np.where(m, np.array(order, dtype=object)[k1], "") + back = np.where(m, np.array(order, dtype=object)[k2], "") + rows = np.arange(n) + Wv = wide[order].values.astype(float) + Ff = np.where(m, Wv[rows, k1], np.nan) + Fb = np.where(m, Wv[rows, k2], np.nan) + tf = np.where(m, TAU[rows, k1], np.nan) + tb = np.where(m, TAU[rows, k2], np.nan) + + out = pd.DataFrame(index=idx) + out["front"], out["back"] = front, back + out["F_front"], out["F_back"] = Ff, Fb + out["tau_front_h"], out["tau_back_h"] = tf, tb + out["index"] = F["index"].values + out["f1h"] = F["f1h"].values + # gamba perp: il PERP CERTIFICATO, non l'indice (l'indice non si compra). + # fallback all'indice solo se il perp non copre l'ora, e lo si dichiara. + if perp is not None: + pp = perp.reindex(idx) + out["perp"] = pp.where(pp.notna(), out["index"]).values + out["perp_from_index"] = pp.isna().values + else: + out["perp"] = out["index"].values + out["perp_from_index"] = True + # basis annualizzato di ciascuna gamba vs INDICE + out["c_front"] = np.log(Ff / out["index"]) * HOURS_Y / tf + out["c_back"] = np.log(Fb / out["index"]) * HOURS_Y / tb + # slope = tasso forward annualizzato FRA le due scadenze (stazionario, interpretabile) + out["slope"] = np.log(Fb / Ff) * HOURS_Y / (tb - tf) + out["s_raw"] = np.log(Fb / Ff) + return out + + +def describe_curve(asset: str, cv: pd.DataFrame) -> None: + """Q1 β€” descrittiva. Il basis e' positivo? persiste? il suo CAMBIO e' prevedibile?""" + c = cv.dropna(subset=["c_front", "slope"]) + print(f"\n --- Q1 CURVA {asset} (n={len(c):,} ore) ---") + print(f" basis annualizzato del FRONT: mediana {c.c_front.median()*100:+6.2f}%/anno " + f"media {c.c_front.mean()*100:+6.2f}% sd {c.c_front.std()*100:5.2f} " + f"contango {float((c.c_front>0).mean())*100:.1f}% del tempo") + print(f" slope forward front->back : mediana {c.slope.median()*100:+6.2f}%/anno " + f"media {c.slope.mean()*100:+6.2f}% sd {c.slope.std()*100:5.2f} " + f"positiva {float((c.slope>0).mean())*100:.1f}% del tempo") + fnd = cv["f1h"].dropna() * HOURS_Y + print(f" funding perp annualizzato : mediana {fnd.median()*100:+6.2f}%/anno " + f"media {fnd.mean()*100:+6.2f}%") + print(" per anno (mediane annualizzate):") + g = c.groupby(c.index.year).agg(basis_front=("c_front", "median"), + slope=("slope", "median")) + fy = (cv["f1h"] * HOURS_Y).groupby(cv.index.year).median() + for y, r in g.iterrows(): + print(f" {y} basis_front {r.basis_front*100:+7.2f}% slope {r.slope*100:+7.2f}% " + f"funding {fy.get(y, np.nan)*100:+7.2f}%") + # prevedibilita': mean-reversion dello slope a 7 giorni (AR sul livello) + for h, lab in [(24, "1g"), (24 * 7, "7g"), (24 * 30, "30g")]: + x = c["slope"] + dx = x.shift(-h) - x + j = pd.concat([x, dx], axis=1).dropna() + if len(j) > 500: + b = np.polyfit(j.iloc[:, 0], j.iloc[:, 1], 1)[0] + rho = float(j.iloc[:, 0].corr(j.iloc[:, 1])) + print(f" mean-reversion slope a {lab:>3s}: beta {b:+.3f} corr {rho:+.3f} " + f"(beta -1 = reversione totale, 0 = martingala)") + + +def basis_vs_funding(asset: str, cv: pd.DataFrame) -> None: + """Q1, nella forma decidibile: il basis del front e' il premio ANNUALIZZATO che + incassi se vendi il datato e lo tieni a scadenza. La copertura e' il perp, su cui + PAGHI il funding realizzato nello stesso periodo. Quindi il roll-down incassabile + NON e' il basis: e' `basis - funding realizzato fino a scadenza`. + Se il basis e' un previsore NON DISTORTO del funding (a~0, b~1) il premio a termine + esiste come LIVELLO ma non e' estraibile: e' gia' il prezzo della copertura.""" + d = cv[["c_front", "f1h", "tau_front_h"]].dropna() + if len(d) < 5000: + print(f" {asset}: campione insufficiente") + return + f = d["f1h"].values + cum = np.concatenate([[0.0], np.cumsum(f)]) # somma cumulata del funding orario + n = len(d) + tau = d["tau_front_h"].values + end = np.minimum(np.arange(n) + tau.astype(int), n - 1) + hrs = np.maximum(end - np.arange(n), 1) + realized = (cum[end] - cum[np.arange(n)]) / hrs * HOURS_Y # funding realizzato ANNUALIZZATO + implied = d["c_front"].values + m = np.isfinite(realized) & np.isfinite(implied) & (hrs > 24 * 5) + x, y = implied[m], realized[m] + b1, b0 = np.polyfit(x, y, 1) + prem = x - y # premio effettivamente incassabile + # IC bootstrap a BLOCCHI (le ore sono fortemente autocorrelate: un IC iid mentirebbe) + rng = np.random.default_rng(822) + bl = 24 * 30 + nb = max(1, len(prem) // bl) + boots = [] + for _ in range(400): + st = rng.integers(0, max(1, len(prem) - bl), nb) + boots.append(np.mean(np.concatenate([prem[i:i + bl] for i in st]))) + lo, hi = np.percentile(boots, [2.5, 97.5]) + print(f" {asset}: basis implicito medio {np.mean(x)*100:+6.2f}%/anno " + f"funding realizzato a scadenza {np.mean(y)*100:+6.2f}%/anno") + print(f" regressione realizzato ~ a + b*implicito: b = {b1:+.3f} a = {b0*100:+.2f}%" + 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():,})") + + +# ========================================================================== +# 3. STRATEGIE β€” posizioni in spazio CONTRATTO (il roll si paga da solo) +# ========================================================================== +def _z(x: pd.Series, win: int) -> pd.Series: + m = x.rolling(win, min_periods=win // 3).mean() + s = x.rolling(win, min_periods=win // 3).std() + return ((x - m) / s.replace(0, np.nan)) + + +def asset_ctx(wide: pd.DataFrame) -> dict: + """Precalcolo per asset: matrice dei rendimenti per contratto + mappa colonne. + Fatto UNA volta e riusato da tutte le celle (altrimenti la griglia non sta nel budget).""" + Wv = wide.values.astype(float) + R = np.zeros_like(Wv) + R[1:] = Wv[1:] / Wv[:-1] - 1.0 + R[~np.isfinite(R)] = 0.0 + return dict(R=R, cpos={c: k for k, c in enumerate(wide.columns)}, + ncol=Wv.shape[1], idx=wide.index) + + +def pair_idx(cv: pd.DataFrame, ctx: dict) -> tuple[np.ndarray, np.ndarray]: + """Indici di colonna di front/back per ogni ora (-1 = coppia non disponibile).""" + cp = ctx["cpos"] + fi = np.array([cp.get(x, -1) for x in cv["front"].values], dtype=int) + bi = np.array([cp.get(x, -1) for x in cv["back"].values], dtype=int) + return fi, bi + + +def signal_w(cv: pd.DataFrame, family: str, sig: str, win_h: int, thr: float) -> np.ndarray: + base = cv["slope"] if family == "CAL" else cv["c_front"] + if sig == "static_long": + w = pd.Series(1.0, index=cv.index) + elif sig == "static_short": + w = pd.Series(-1.0, index=cv.index) + elif sig == "carry": # contango -> vendi il datato + w = -np.sign(base) + elif sig in ("zrev", "zmom"): + z = _z(base, win_h) + w = (-np.tanh(z)) if sig == "zrev" else np.tanh(z) + w = w.where(z.abs() >= thr, 0.0) + else: + raise ValueError(sig) + return np.nan_to_num(np.asarray(w, float), nan=0.0).clip(-1, 1) + + +def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str, + win_h: int, thr: float, slip_bps: float, + fee_side: float = FEE_SIDE, fi=None, bi=None, lag: int = 1, + dec_hour: int | None = None) -> pd.Series: + """Ritorno ORARIO netto per $1 di nozionale LORDO PER GAMBA. + + Le posizioni vivono in spazio CONTRATTO: quando la coppia cambia (roll) la variazione + per contratto e' automaticamente piena => il roll paga il round-trip completo su + entrambe le gambe. Nessuna serie di prezzi viene MAI incollata: si incollano solo i + RENDIMENTI, che e' l'unico posto in cui e' lecito. + + Causalita': w e' deciso con dati <= t e TENUTO durante t -> t+1 (shift esplicito qui, + non nel segnale) => non si puo' sbagliare per distrazione a valle.""" + idx = ctx["idx"] + n, ncol = len(idx), ctx["ncol"] + if fi is None or bi is None: + fi, bi = pair_idx(cv, ctx) + w = signal_w(cv, family, sig, win_h, thr) + if dec_hour is not None: + # ANCORA: la posizione si aggiorna SOLO a quell'ora del giorno e si TIENE il resto + # (non "si guadagna solo a quell'ora", che sarebbe un'altra strategia). + ws = pd.Series(w, index=idx).where(idx.hour == dec_hour) + w = ws.ffill().fillna(0.0).values + + P = np.zeros((n, ncol)) + rows = np.arange(n) + if family == "CAL": + m = (fi >= 0) & (bi >= 0) + P[rows[m], bi[m]] = w[m] # long spread = long back + P[rows[m], fi[m]] = -w[m] # / short front + else: # FvP: solo la gamba datata + m = fi >= 0 + P[rows[m], fi[m]] = w[m] + + held = np.zeros_like(P) + held[lag:] = P[:-lag] if lag else P # deciso a t, tenuto in t->t+lag + gross = (held * ctx["R"]).sum(axis=1) + + dP = np.zeros_like(P) + dP[1:] = P[1:] - P[:-1] + dP[0] = P[0] + turn = np.abs(dP).sum(axis=1) + + if family == "FvP": + perp_r = np.nan_to_num(cv["perp"].pct_change().values, nan=0.0) + f1h = np.nan_to_num(cv["f1h"].values, nan=0.0) + wheld = held.sum(axis=1) # una sola gamba datata + # long datato => short perp: guadagna -w*perp_r e INCASSA w*funding + gross = gross - wheld * perp_r + wheld * f1h + turn = turn * 2.0 # datato + gamba perp + + cost = turn * (fee_side + slip_bps * 1e-4) + return pd.Series(gross - cost, index=idx).fillna(0.0) + + +# ========================================================================== +# 4. METRICHE / GATE +# ========================================================================== +def combine(hs: list[pd.Series]) -> pd.Series: + """50/50 BTC+ETH sulle ore COMUNI (inner join, come tp01_baseline_daily).""" + J = pd.concat(hs, axis=1, join="inner").fillna(0.0) + return J.mean(axis=1) + + +def to_daily(h: pd.Series) -> pd.Series: + return ((1.0 + h.fillna(0.0)).resample("1D").prod() - 1.0).dropna() + + +def stats(daily: pd.Series) -> dict: + r = daily.dropna() + if len(r) < 30 or r.std() == 0: + return dict(sharpe=0.0, holdout=0.0, maxdd=0.0, cagr=0.0, n=len(r), vol=0.0) + sh = float(r.mean() / r.std() * math.sqrt(365.25)) + eq = (1 + r).cumprod() + dd = float(((eq.cummax() - eq) / eq.cummax()).max()) + yrs = (r.index[-1] - r.index[0]).days / 365.25 + cagr = float(eq.iloc[-1] ** (1 / yrs) - 1) if yrs > 0.5 and eq.iloc[-1] > 0 else float("nan") + ho = r[r.index >= HOLDOUT] + sho = float(ho.mean() / ho.std() * math.sqrt(365.25)) if len(ho) > 30 and ho.std() > 0 else float("nan") + yrs_ = max(len(r) / 365.25, 1e-9) + return dict(sharpe=sh, holdout=sho, maxdd=dd, cagr=cagr, n=len(r), + vol=float(r.std() * math.sqrt(365.25)), + se_ann=float(r.std() * math.sqrt(365.25) / math.sqrt(yrs_))) + + +def delevering_null(cand: pd.Series, base: pd.Series) -> dict: + """NULL DEL DE-LEVERING (5 occorrenze in questo progetto). Esiste k<1 che, + applicato al BASELINE, da' lo STESSO maxDD del candidato con Sharpe MIGLIORE? + Se si', il candidato non protegge: e' solo meno leva.""" + cs = stats(cand) + best = None + for k in np.arange(0.05, 1.01, 0.05): + s = stats(base * k) + if s["maxdd"] <= cs["maxdd"] + 1e-9: + best = (float(k), s) + break + if best is None: + return dict(refuted=False, note="nessun k<=1 raggiunge quel DD sul baseline") + k, s = best + return dict(refuted=bool(s["sharpe"] > cs["sharpe"]), k=k, + base_sharpe_at_k=s["sharpe"], cand_sharpe=cs["sharpe"], + base_dd_at_k=s["maxdd"], cand_dd=cs["maxdd"]) + + +# ========================================================================== +# 5. MAIN +# ========================================================================== +def main() -> None: + t_start = time.time() + print("=" * 78) + print("r0822 BASIS-CALENDAR β€” struttura a termine dei futures datati Deribit") + print("=" * 78) + + today = pd.Timestamp.now(tz="UTC") + panels, funds, metas, vwides = {}, {}, {}, {} + + # ---------------- STEP 1: dato ---------------- + print("\n[1] DATO β€” ricostruzione da Deribit pubblico (cache su scratchpad)") + for a in ASSETS: + print(f" {a}: trimestrali (ultimo venerdi' MAR/GIU/SET/DIC 08:00 UTC)...") + wide, vwide, meta = build_panel(a) + fund = fetch_funding_and_index(a, pd.Timestamp("2019-01-01", tz="UTC"), today) + panels[a], vwides[a], metas[a], funds[a] = wide, vwide, meta, fund + ok = meta[meta.status == "ok"] + print(f" {len(ok)} contratti, {int(meta.bars.sum()):,} barre orarie, " + f"funding+indice {len(fund):,} ore") + + # ---------------- STEP 2: certificazione ---------------- + print("\n[2] CERTIFICAZIONE (regola 4: un edge su un book fermo non e' un edge)") + certs, perps = {}, {} + for a in ASSETS: + pdf = A.get(a, "1h") + perps[a] = pd.Series( + pdf["close"].astype(float).values, + index=pd.DatetimeIndex(pd.to_datetime(pdf["datetime"], utc=True))).sort_index() + perps[a] = perps[a][~perps[a].index.duplicated()] + certs[a] = certify(a, panels[a], vwides[a], metas[a], funds[a], perps[a]) + x = certs[a]["xchk"] + if x.get("status") == "ok" and len(x["table"]): + t = x["table"] + print(f" CROSS-CHECK INDIPENDENTE vs forward della catena opzioni " + f"({int(t.n.sum())} ore, {len(t)} scadenze):") + print(f" deviazione mediana {t.med_bps.median():+.1f} bps, " + f"p95 |dev| {t.p95_bps.max():.1f} bps") + else: + print(f" CROSS-CHECK catena: {x.get('status')}") + + # ---------------- STEP 3: Q1 curva ---------------- + print("\n[3] Q1 β€” LA CURVA E' PREVEDIBILE?") + curves = {} + for a in ASSETS: + cv = curve_frame(a, panels[a], metas[a], funds[a], roll_dte=7, perp=perps[a]) + curves[a] = cv + describe_curve(a, cv) + print("\n --- Q1-bis: il basis e' un PREVISORE del funding che paghi sulla copertura? ---") + for a in ASSETS: + basis_vs_funding(a, curves[a]) + + # ---------------- STEP 3b: spread bid-ask (calibrazione DATATA) ---------------- + print("\n[3b] SPREAD BID-ASK β€” calibrazione sul book VIVO di oggi (non e' storia)") + live = [] + for a in ASSETS: + for nm, exp in quarterly_universe(a): + if today < exp <= today + pd.Timedelta(days=400): + live.append(nm) + lb = fetch_live_books(live + [f"{a}-PERPETUAL" for a in ASSETS]) + slip_est = 5.0 + if len(lb): + for r in lb.itertuples(): + print(f" {r.instrument:16s} bid {r.bid:>10,.1f} ask {r.ask:>10,.1f} " + f"spread {r.spread_bps:6.1f} bps size {r.bid_sz:>8,.0f}/{r.ask_sz:>8,.0f}") + dated = lb[~lb.instrument.str.contains("PERPETUAL")] + if len(dated): + slip_est = float(dated.spread_bps.median()) / 2.0 + print(f" => mezzo-spread MEDIANO del datato = {slip_est:.1f} bps/lato " + f"(STIMA datata oggi, NON storia: il book storico dei datati non e' disponibile)") + + # ---------------- STEP 4: griglia ---------------- + print("\n[4] GRIGLIA β€” dichiarata PRIMA, contata AL RIALZO") + FAM = {"CAL": ["static_long", "static_short", "zrev", "zmom"], + "FvP": ["carry", "static_long", "static_short", "zrev", "zmom"]} + WINS = [720, 2160, 4320] # 30 / 90 / 180 giorni in ore + ROLLS = [7, 21] + THRS = [0.0, 1.0] + cells = [(f, s, w, rl, th) for f, sigs in FAM.items() for s in sigs + for w in WINS for rl in ROLLS for th in THRS] + print(f" famiglie {list(FAM)} x segnali x finestre {WINS} x roll {ROLLS} x soglie {THRS}") + print(f" = {len(cells)} celle valutate (le varianti statiche non usano W/soglia:" + f" le conto lo stesso, al rialzo)") + + ctxs = {a: asset_ctx(panels[a]) for a in ASSETS} + cvcache = {(a, rl): curve_frame(a, panels[a], metas[a], funds[a], rl, perp=perps[a]) + for a in ASSETS for rl in ROLLS} + pidx = {(a, rl): pair_idx(cvcache[(a, rl)], ctxs[a]) for a in ASSETS for rl in ROLLS} + + rows = [] + for (fam, sig, win, rl, th) in cells: + hs = [] + for a in ASSETS: + f_i, b_i = pidx[(a, rl)] + h = run_strategy(cvcache[(a, rl)], ctxs[a], fam, sig, win, th, slip_est, + fi=f_i, bi=b_i) + hs.append(h) + d = to_daily(combine(hs)) + st = stats(d) + rows.append(dict(family=fam, sig=sig, win=win, roll=rl, thr=th, + daily=d, **st)) + G = pd.DataFrame([{k: v for k, v in r.items() if k != "daily"} for r in rows]) + dailies = {i: r["daily"] for i, r in enumerate(rows)} + + print(f"\n distribuzione dello Sharpe FULL sulle {len(G)} celle: " + f"min {G.sharpe.min():+.2f} p25 {G.sharpe.quantile(.25):+.2f} " + f"mediana {G.sharpe.median():+.2f} p75 {G.sharpe.quantile(.75):+.2f} " + f"max {G.sharpe.max():+.2f} sd {G.sharpe.std():.2f}") + degen = G[(G.vol < 0.005) | (G.n < 400)] + if len(degen): + print(f" ATTENZIONE: {len(degen)} celle DEGENERI (vol<0.5%/anno o <400 giorni attivi):") + print(f" Sharpe di quelle celle: {sorted(np.round(degen.sharpe.values,2))[:10]}") + print(" gonfiano la varianza dei trial e quindi il null del deflated-Sharpe:") + print(" il DSR viene riportato ANCHE sulla sola sottogriglia non degenere.") + + print("\n MIGLIORE CELLA PER FAMIGLIA (Q2 riguarda il CALENDAR: va letto separato):") + for fam in FAM: + sub = G[G.family == fam] + i = sub.sharpe.idxmax() + r = G.loc[i] + print(f" [{fam}] {r.sig:12s} W={int(r.win):5d}h roll={int(r.roll):2d}g thr={r.thr:.1f} " + f"| Sh {r.sharpe:+6.2f} hold {r.holdout:+6.2f} DD {r.maxdd*100:5.2f}% " + f"CAGR {r.cagr*100:+6.2f}% vol {r.vol*100:5.2f}%") + ii = A.implausible_sharpe(dailies[i]) + print(f" implausible_sharpe={ii['implausible']} " + f"(barre in perdita {ii.get('loss_frac', float('nan'))*100:.1f}%) " + f"-> {'coda ASSENTE, rischio fuori dal campione' if ii['implausible'] else 'coda PRESENTE'}") + + print("\n LE CELLE CANONICHE (quelle che un lettore chiede per nome), roll=7g:") + for fam, sig, lab in [ + ("FvP", "carry", "vendi il datato quando e' in CONTANGO / compra perp (il carry classico)"), + ("FvP", "static_short", "SEMPRE corto il datato / lungo perp = LA RACCOLTA DEL CONTANGO"), + ("FvP", "static_long", "SEMPRE lungo il datato / corto perp (il contrario)"), + ("CAL", "static_long", "calendar: lungo back / corto front"), + ("CAL", "static_short", "calendar: corto back / lungo front")]: + sub = G[(G.family == fam) & (G.sig == sig) & (G.roll == 7)] + if len(sub): + r = G.loc[sub.index[0]] + print(f" {fam}/{sig:12s} Sh {r.sharpe:+6.2f} hold {r.holdout:+6.2f} " + f"CAGR {r.cagr*100:+6.2f}% (+-{r.se_ann*100:.2f} SE) DD {r.maxdd*100:5.2f}% " + f"vol {r.vol*100:5.2f}% <- {lab}") + + print("\n migliori 8 celle per Sharpe FULL (NON e' la selezione β€” solo panorama):") + for i in G.sharpe.nlargest(8).index: + r = G.loc[i] + print(f" {r.family:4s} {r.sig:12s} W={int(r.win):5d}h roll={int(r.roll):2d}g " + f"thr={r.thr:.1f} | Sh {r.sharpe:+6.2f} hold {r.holdout:+6.2f} " + f"DD {r.maxdd*100:5.2f}% CAGR {r.cagr*100:+6.2f}% vol {r.vol*100:5.2f}%") + + # ---------------- STEP 5: selezione IN-SAMPLE ONLY ---------------- + print("\n[5] SELEZIONE IN-SAMPLE-ONLY (mai la cella col miglior hold-out)") + ins = {} + for i, d in dailies.items(): + pre = d[d.index < HOLDOUT] + ins[i] = (float(pre.mean() / pre.std() * math.sqrt(365.25)) + if len(pre) > 60 and pre.std() > 0 else -9.9) + best_i = max(ins, key=ins.get) + b = G.loc[best_i] + bd = dailies[best_i] + bs = stats(bd) + print(f" cella scelta AL BUIO: {b.family} {b.sig} W={int(b.win)}h roll={int(b.roll)}g thr={b.thr}") + print(f" Sharpe in-sample {ins[best_i]:+.2f} -> FULL {bs['sharpe']:+.2f} " + f"HOLD-OUT {bs['holdout']:+.2f} maxDD {bs['maxdd']*100:.2f}% " + f"CAGR {bs['cagr']*100:+.2f}% (+-{bs['se_ann']*100:.2f} SE annuo) " + f"vol {bs['vol']*100:.2f}%") + print(f" NB con {bs['n']/365.25:.1f} anni e vol {bs['vol']*100:.1f}% l'errore standard del " + f"rendimento annuo e' {bs['se_ann']*100:.2f}pp: sotto ~{2*bs['se_ann']*100:.1f}pp/anno " + f"NIENTE e' distinguibile da zero, ne' in un verso ne' nell'altro.") + best_full = G.sharpe.idxmax() + print(f" (per confronto, la cella col miglior FULL sarebbe stata " + f"{G.loc[best_full,'family']} {G.loc[best_full,'sig']} " + f"Sh {G.loc[best_full,'sharpe']:+.2f} β€” NON e' quella scelta)") + + # ---------------- STEP 6: GATE ---------------- + print("\n[6] GATE") + dsr, sr0 = A.deflated_sharpe(bs["sharpe"], list(G.sharpe.values), bd) + print(f" deflated-Sharpe : DSR {dsr:.3f} (null max atteso {sr0:+.2f} su {len(G)} trial) " + f"-> {'PASS' if dsr >= 0.95 else 'FAIL'}") + nd = G[(G.vol >= 0.005) & (G.n >= 400)] + if len(nd) >= 5 and len(nd) < len(G): + d2, s2_ = A.deflated_sharpe(bs["sharpe"], list(nd.sharpe.values), bd) + print(f" sulla sola sottogriglia NON degenere ({len(nd)} trial): " + f"DSR {d2:.3f} (null max {s2_:+.2f}) -> {'PASS' if d2 >= 0.95 else 'FAIL'}") + + imp = A.implausible_sharpe(bd) + print(f" implausible_sharpe: implausible={imp['implausible']} " + f"Sh {imp.get('sharpe', float('nan')):+.2f} maxDD {imp.get('maxdd', float('nan'))*100:.2f}% " + f"quota barre in perdita {imp.get('loss_frac', float('nan'))*100:.1f}% " + f"Calmar {imp.get('calmar', float('nan')):.1f}") + if imp["reasons"]: + for rr in imp["reasons"]: + print(f" - {rr}") + + tp = A.tp01_baseline_daily() + try: + mg = A.marginal_vs_tp01(bd) + print(f" marginal_vs_tp01 : {mg.get('marginal_verdict')} " + f"corr_full {mg.get('corr_full')} corr_hold {mg.get('corr_hold')} " + f"robust_oos={mg.get('robust_oos')} insample_edge={mg.get('has_insample_edge')} " + f"is_hedge={mg.get('is_hedge')} beats_noise={mg.get('beats_noise_null')}") + w25 = mg.get("w25") or (mg.get("blends", {}) or {}).get("w25", {}) + print(f" uplift blend w=25%: full {w25.get('uplift_full')} " + f"hold {w25.get('uplift_hold')}") + except Exception as exc: # noqa: BLE001 + mg = {} + print(f" marginal_vs_tp01 : NON GIRATO ({type(exc).__name__}: {exc})") + + dn = delevering_null(bd, tp) + print(f" null de-levering : refuted={dn.get('refuted')} {dn}") + + # anchor: l'ora del giorno in cui si valuta il segnale + print(" anchor_luck_band (ora del giorno di decisione, 24 ancore):") + def by_off(off): + hs = [] + for a in ASSETS: + f_i, b_i = pidx[(a, int(b.roll))] + hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig, + int(b.win), float(b.thr), slip_est, + fi=f_i, bi=b_i, dec_hour=off)) + return to_daily(combine(hs)) + try: + ab = A.anchor_luck_band(by_off, list(range(24)), canonical=0) + if "median" in ab: + print(f" canonica {ab['canonical']:+.3f} (pctl {ab['canonical_pctl']*100:.0f}Β°) " + f"MEDIANA(stima onesta) {ab['median']:+.3f} banda [{ab['lo']:+.3f},{ab['hi']:+.3f}] " + f"frazione>0 {ab['frac_positive']:.2f} fortuna {ab['luck']:+.3f} " + f"gate_pass={ab['gate_pass']}") + else: + print(f" {ab}") + except Exception as exc: # noqa: BLE001 + print(f" NON GIRATO ({type(exc).__name__}: {exc})") + + try: + print(f" causality_ok : la posizione e' shiftata in run_strategy " + f"(held = pos.shift(1)); verifica strutturale, non A.causality_ok " + f"(che vuole un target_fn su BTC/ETH direzionale)") + except Exception: # noqa: BLE001 + pass + + # ---------------- STEP 6b: RITARDO D'ESECUZIONE (il test decisivo qui) ---------------- + print("\n[6b] RITARDO D'ESECUZIONE β€” il test decisivo di questo filone") + print(" Un datato lontano ha prezzi STANTII: una mean-reversion su una serie stantia") + print(" mostra profitti FINTI (il prezzo 'reverte' solo perche' si aggiorna in ritardo).") + print(" Un edge di STRUTTURA A TERMINE e' lento e sopravvive a un'ora in piu' di ritardo;") + print(" un artefatto di microstruttura muore. (Stessa lente del decadimento di XSR01.)") + lag_tab = [] + for lg in [1, 2, 3, 6, 12, 24]: + hs = [] + for a in ASSETS: + f_i, b_i = pidx[(a, int(b.roll))] + hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig, + int(b.win), float(b.thr), slip_est, fi=f_i, bi=b_i, lag=lg)) + sl_ = stats(to_daily(combine(hs))) + lag_tab.append((lg, sl_["sharpe"], sl_["cagr"])) + print(f" ritardo {lg:2d}h -> Sharpe {sl_['sharpe']:+6.2f} CAGR {sl_['cagr']*100:+6.2f}%") + s1 = lag_tab[0][1]; s2 = lag_tab[1][1] + if s1 > 0: + keep = s2 / s1 + print(f" con UNA sola ora in piu' resta il {keep*100:.0f}% dello Sharpe " + f"({s1:+.2f} -> {s2:+.2f})") + print(" -> " + ("compatibile con un edge lento di struttura a termine" + if keep > 0.6 else + "CROLLO: firma di microstruttura/prezzo stantio, NON di term structure")) + + # ---------------- STEP 7: quanto spread uccide l'edge ---------------- + print("\n[7] A CHE SPREAD MUORE L'EDGE (la stima di slippage e' il rischio #1)") + for sl in [0.0, 1.0, 2.5, 5.0, 7.5, 10.0, 20.0]: + hs = [] + for a in ASSETS: + f_i, b_i = pidx[(a, int(b.roll))] + hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, + b.sig, int(b.win), float(b.thr), sl, fi=f_i, bi=b_i)) + s2 = stats(to_daily(combine(hs))) + mark = " <= stima di oggi" if abs(sl - slip_est) < 0.6 else "" + print(f" mezzo-spread {sl:5.1f} bps/lato -> Sharpe {s2['sharpe']:+6.2f} " + f"CAGR {s2['cagr']*100:+6.2f}%{mark}") + + # ---------------- STEP 8: eseguibilita' ---------------- + print("\n[8] Q3 β€” ESEGUIBILITA' a $600-3.000") + print(" lotti minimi letti dal VENUE (non da una tabella cablata):") + px = {a: float(curves[a]["index"].dropna().iloc[-1]) for a in ASSETS} + for a in ASSETS: + for nm in (f"{a}-25DEC26", f"{a}_USDC-25DEC26"): + try: + sp = _api("/public/get_instrument", instrument_name=nm)["result"] + lin = sp.get("settlement_currency") == "USDC" + lot = (sp["min_trade_amount"] * px[a]) if lin else sp["min_trade_amount"] + print(f" {nm:22s} {'LINEARE' if lin else 'INVERSE':8s} " + f"min {sp['min_trade_amount']:<10.4g} = ${lot:>8,.2f} di nozionale " + f"tick {sp['tick_size']} taker {sp['taker_commission']*1e4:.1f} bps") + except Exception as exc: # noqa: BLE001 + print(f" {nm:22s} specs non lette ({type(exc).__name__})") + print(" (il libro live trada i LINEARI USDC; i trimestrali studiati qui sono gli INVERSE,") + print(" che hanno 7 anni di storia. Le due famiglie hanno lotti minimi DIVERSI.)") + print(f" vol ANNUA del candidato per $1 di nozionale lordo/gamba: {bs['vol']*100:.2f}%") + if bs["vol"] > 0: + lev = 0.20 / bs["vol"] + print(f" per portarlo al vol-target 20% del progetto servirebbe leva " + f"{lev:.1f}x per gamba = ${lev*2:,.0f} di nozionale LORDO per $1 di conto") + for cap in (600, 3000): + print(f" a ${cap}: nozionale lordo ${lev*2*cap:,.0f} " + f"(margine ~2-4%/gamba => ${lev*2*cap*0.03:,.0f} di IM richiesto)") + + print(f"\n[fine] {time.time()-t_start:.0f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_dealer_gamma.py b/scripts/research/r0822_dealer_gamma.py new file mode 100644 index 0000000..bc6df2b --- /dev/null +++ b/scripts/research/r0822_dealer_gamma.py @@ -0,0 +1,691 @@ +"""r0822 β€” DEALER GAMMA: il regime di gamma dei dealer separa mean-reversion da trend su BTC/ETH? + +Filone dell'ondata 2026-08-22. Dato mai usato dal progetto: `data/raw/cb_market_snapshots.parquet` +(ereditato da cerbero-bite, dismesso il 30/07) con `dealer_net_gamma`, `gamma_flip_level`, +`oi_delta_pct_4h`, `liquidation_*_risk`. + +IPOTESI DI MERCATO CLASSICA (due gambe, vanno confermate ENTRAMBE): + (A) dealer LONG gamma -> l'hedging SOPPRIME la vol realizzata; SHORT gamma -> la AMPLIFICA; + (B) dealer LONG gamma -> il prezzo MEAN-REVERTE; SHORT gamma -> il prezzo TRENDA. +La gamba tradeable e' la (B): e' quella che diventerebbe un gate di regime sopra TP01/SKH01. +Aspettativa dichiarata PRIMA di misurare: (A) plausibile ma confusa con il livello di vol; +(B) improbabile su 3 mesi, e comunque a rischio "TP01 travestito". + +ORDINE OBBLIGATO: prima la validazione del DATO (non e' certificato, la fonte non esiste piu'), +poi il FATTO condizionale, e solo dopo β€” se il fatto c'e' β€” il gate. + +Girare: nice -n 19 timeout 900 uv run python scripts/research/r0822_dealer_gamma.py +""" +from __future__ import annotations + +import glob +import sys +import warnings +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +import altlib as A # noqa: E402 + +warnings.filterwarnings("ignore") +pd.set_option("display.width", 220) + +RAW = ROOT / "data" / "raw" +SNAP_F = RAW / "cb_market_snapshots.parquet" +CHAIN_D = RAW / "cb_chain" +SEED = 20260822 +ASSETS = ("BTC", "ETH") +HOUR_YEAR = 24 * 365.0 +ANN = np.sqrt(HOUR_YEAR) * 100.0 # da dev.std. oraria a vol annua in punti percentuali + +# La cadenza a 15 min comincia qui: prima ci sono 37 punti GIORNALIERI (vedi sezione 1). +HF_START = pd.Timestamp("2026-05-01 15:00", tz="UTC") + +# ------------------------------------------------------------------ conteggio dei trial +# Ogni cella/statistica valutata viene contata qui, AL RIALZO (regola: il conteggio dei trial +# e' l'unico posto dove barare e' indolore e invisibile β€” 30/07). Il verdetto stampa la +# ripartizione DERIVATA da questo dizionario, non una somma scritta a mano. +COUNT: dict[str, int] = dict(ricostruzione=0, sma=0, regressioni=0, vol_condizionata=0, + variance_ratio=0, momentum=0, gate=0, delevering=0) +NEFF: dict[str, float] = {} # ampiezza effettiva per asset, misurata in sezione 1e + + +def hr(t: str) -> None: + print("\n" + "=" * 96 + f"\n{t}\n" + "=" * 96) + + +def sh_d(x: pd.Series) -> float: + x = pd.Series(x).dropna() + return float(x.mean() / x.std() * np.sqrt(365.25)) if len(x) > 2 and x.std() > 0 else 0.0 + + +def dd_d(x: pd.Series) -> float: + e = (1.0 + pd.Series(x).dropna()).cumprod() + return float((e / e.cummax() - 1.0).min()) if len(e) else 0.0 + + +# ------------------------------------------------------------------ caricamento +def load_snap() -> pd.DataFrame: + d = pd.read_parquet(SNAP_F).sort_values("ts") + d["ts"] = pd.to_datetime(d["ts"], utc=True).astype("datetime64[ns, UTC]") + return d + + +def load_bars(asset: str) -> pd.DataFrame: + px = A.get(asset, "1h").copy() + px["dt"] = pd.to_datetime(px["timestamp"], unit="ms", utc=True).astype("datetime64[ns, UTC]") + px = px[["dt", "close"]].dropna().reset_index(drop=True) + px["r"] = np.log(px["close"]).diff() + return px + + +def joined(asset: str, snap: pd.DataFrame) -> pd.DataFrame: + """Barra oraria + ULTIMO snapshot con ts <= apertura della barra. + + Causale per costruzione: `direction='backward'` sull'apertura della barra, quindi il + regime usato per la barra [t, t+1h) e' noto a t. Tolleranza 2h: se il collettore e' + fermo da piu' di due ore la riga esce, invece di trascinare un valore vecchio. + """ + px = load_bars(asset) + s = (snap[snap["asset"] == asset] + [["ts", "dealer_net_gamma", "gamma_flip_level", "spot", "dvol", "realized_vol_30d"]] + .dropna(subset=["dealer_net_gamma", "gamma_flip_level"])) + m = pd.merge_asof(px.sort_values("dt"), s.sort_values("ts"), + left_on="dt", right_on="ts", direction="backward", + tolerance=pd.Timedelta("2h")) + m = m[m["ts"].notna()].reset_index(drop=True) + # SEGNO CORRETTO (giustificato nella sezione 2): dealer_net_gamma = -GEX + # dealer_net_gamma > 0 <=> GEX < 0 <=> dealer SHORT gamma + m["short_gamma"] = (m["dealer_net_gamma"] > 0).astype(float) + m["above_flip"] = (m["close"] > m["gamma_flip_level"]).astype(float) + m["rvp24"] = np.sqrt((m["r"] ** 2).rolling(24).mean()) + m["rvp168"] = np.sqrt((m["r"] ** 2).rolling(168).mean()) + m["ret168"] = np.log(m["close"] / m["close"].shift(168)) + m["z"] = m["r"] / m["rvp24"].shift(1) + for k in (4, 24): + m[f"rvf{k}"] = np.sqrt((m["r"].shift(-1) ** 2).rolling(k).sum().shift(-(k - 1)) / k) + return m + + +def ols(y, cols): + y = np.asarray(y, float) + n = len(y) + X = np.column_stack([np.ones(n)] + [np.asarray(c, float) for c in cols]) + b, *_ = np.linalg.lstsq(X, y, rcond=None) + res = y - X @ b + s2 = res @ res / (n - X.shape[1]) + se = np.sqrt(np.diag(s2 * np.linalg.pinv(X.T @ X))) + return b, b / se, 1.0 - np.var(res) / np.var(y) + + +def variance_ratio(r, k: int) -> float: + r = np.asarray(pd.Series(r).dropna(), float) + if len(r) < 5 * k: + return np.nan + x = pd.Series(r).rolling(k).sum().dropna().values + return float(np.var(x) / (k * np.var(r))) + + +# ================================================================== 1. IL DATO +def sezione_1_dato(snap: pd.DataFrame) -> dict: + hr("1. VALIDAZIONE DEL DATO β€” prima del segnale (fonte dismessa, mai certificata)") + out = {} + print(f"file: {SNAP_F.name} righe {len(snap)} {snap.ts.min()} -> {snap.ts.max()}") + + # 1a. cadenza: il file NON e' omogeneo + d = snap[snap.asset == "BTC"] + gaps = d.ts.diff().dt.total_seconds().div(60).round(0).value_counts() + print(f"\n1a. CADENZA (BTC): distribuzione dei salti in minuti {dict(list(gaps.items())[:6])}") + pre = snap[snap.ts < HF_START] + post = snap[snap.ts >= HF_START] + print(f" -> {len(pre)//2} righe/asset a cadenza GIORNALIERA (12:00 UTC) fino al 2026-05-01,") + print(f" {len(post)//2} righe/asset a 15 min dopo. Il 'file da 4 mesi' e' 90 GIORNI utili.") + out["n_hf_per_asset"] = len(post) // 2 + + for a in ASSETS: + x = post[post.asset == a] + hrs = x.ts.dt.floor("h").nunique() + span = (x.ts.max().floor("h") - x.ts.min().floor("h")).total_seconds() / 3600 + 1 + print(f" {a}: ore distinte {hrs} su {span:.0f} di span = copertura {100*hrs/span:.2f}%" + f" (buchi > 20 min: {int((x.ts.diff().dt.total_seconds()/60 > 20).sum())})") + + # 1b. colonne MORTE β€” una colonna presente non e' un dato presente + print("\n1b. COLONNE MORTE (regola 8 del brief: contare cio' che VARIA, non le righe)") + for c in ["liquidation_long_risk", "liquidation_short_risk", "macro_days_to_event"]: + v = snap[c].dropna() + print(f" {c:26s} non-nulli {len(v):6d} ({100*len(v)/len(snap):5.1f}%) " + f"valori distinti {v.nunique()} {'*** COSTANTE = ZERO INFORMAZIONE ***' if v.nunique() <= 1 else ''}") + out["dead_cols"] = [c for c in ["liquidation_long_risk", "liquidation_short_risk"] + if snap[c].dropna().nunique() <= 1] + + # 1c. dealer_net_gamma e gamma_flip_level: si muovono? sono plausibili? + print("\n1c. PLAUSIBILITA' delle due colonne di regime (finestra 15 min)") + for a in ASSETS: + x = post[post.asset == a] + g, f = x.dealer_net_gamma, x.gamma_flip_level + rel = x.spot / f - 1.0 + print(f" {a} dealer_net_gamma: nan {100*g.isna().mean():4.1f}% frac>0 {(g>0).mean():.3f} " + f"consecutivi identici {(g.diff()==0).mean():.4f} -> si muove") + print(f" {a} gamma_flip_level: nan {100*f.isna().mean():4.1f}% " + f"consecutivi identici {(f.diff()==0).mean():.4f} " + f"(spot/flip-1) p05 {rel.quantile(.05):+.1%} med {rel.median():+.1%} p95 {rel.quantile(.95):+.1%}") + print(" -> BTC: il flip e' CONGELATO nel 61% dei passi e sta fino a +42% SOTTO lo spot al p95.") + print(" Un 'livello di flip' a 42% dallo spot non e' un livello di flip: e' degenere.") + + # 1d. LE DUE COLONNE SI CONTRADDICONO + print("\n1d. CONTRADDIZIONE INTERNA fra le due colonne della STESSA fonte") + for a in ASSETS: + x = post[post.asset == a].dropna(subset=["dealer_net_gamma", "gamma_flip_level", "spot"]) + c = np.corrcoef((x.dealer_net_gamma > 0).astype(float), (x.spot > x.gamma_flip_level).astype(float))[0, 1] + print(f" {a}: corr( dng>0 , spot>flip ) = {c:+.3f}") + print(" Nella convenzione classica dealer LONG gamma sta SOPRA il flip: le due colonne") + print(" dovrebbero correlare POSITIVAMENTE. Correlano NEGATIVAMENTE -> una delle due ha il") + print(" segno invertito. Quale, lo decide la sezione 2 (ricostruzione dalla catena), non io.") + + # 1e. quante EPISODI di regime ci sono davvero -> il numero che conta + print("\n1e. AMPIEZZA EFFETTIVA β€” 2.160 ore NON sono 2.160 osservazioni") + for a in ASSETS: + x = post[post.asset == a].dropna(subset=["dealer_net_gamma"]) + lab = (x.dealer_net_gamma > 0).astype(int).values + runs = pd.Series(lab).groupby((pd.Series(lab).diff() != 0).cumsum()).size() * 0.25 # ore + hourly = pd.Series(lab).iloc[::4] + rho = float(hourly.autocorr(1)) + neff = len(hourly) * (1 - rho) / (1 + rho) + print(f" {a}: {len(runs)} episodi; {int((runs>72).sum())} durano >72h e coprono il " + f"{100*runs[runs>72].sum()/runs.sum():.0f}% del tempo") + print(f" AR(1) orario del regime {rho:.4f} -> n_eff ~ {neff:.0f} su {len(hourly)} ore") + out[f"neff_{a}"] = NEFF[a] = neff + print(" -> l'errore standard onesto di ogni statistica condizionata al regime va moltiplicato") + print(f" per ~sqrt(2130/n_eff) = 6x (BTC) - 9x (ETH). E' IL numero della sezione 3.") + return out + + +# ================================================================== 2. SEGNO E RICOSTRUIBILITA' +def sezione_2_ricostruzione(snap: pd.DataFrame) -> dict: + hr("2. DA DOVE VIENE `dealer_net_gamma`, E POSSIAMO RIFARLO OGGI?") + out = {} + print("La domanda non e' accademica: bite e' dismesso, la serie finisce il 2026-07-30. Se il") + print("segnale non e' ricostruibile dalla catena che raccogliamo NOI, il lead e' morto alla nascita.") + + arch = CHAIN_D / "bite_archive.parquet" + d = pd.read_parquet(arch, columns=["ts", "asset", "option_type", "strike", "gamma", + "open_interest", "source"]) + d["ts"] = pd.to_datetime(d["ts"], utc=True).astype("datetime64[ns, UTC]") + d["tsf"] = d["ts"].dt.floor("15min") + per_snap = d.groupby(["source", "asset", "tsf"]).size().rename("n").reset_index() + print("\n2a. STRIKE PER SNAPSHOT nell'archivio ereditato (mediana):") + print(per_snap.groupby(["source", "asset"])["n"].median().to_string()) + print(" -> `bite:live` ha ~16-18 strike: e' la manciata che serviva al suo motore VRP, NON") + print(" una catena. Solo `bite:research` (~280-320 strike) e' ricostruibile. E parte dal") + print(" 2026-06-09, non dal 05-01.") + + # spot: l'archivio ha underlying_price 100% NaN -> lo prendiamo dallo snapshot (stessa fonte) + d = d[(d["source"] == "bite:research")].dropna(subset=["gamma", "open_interest"]) + sp = (snap[["asset", "spot", "dealer_net_gamma", "gamma_flip_level"]] + .assign(tsf=snap["ts"].dt.floor("15min")) + .dropna(subset=["spot"]).drop_duplicates(["asset", "tsf"])) + d = d.merge(sp[["asset", "tsf", "spot"]], on=["asset", "tsf"], how="inner") + print(f"\n (l'archivio ha underlying_price 100% NaN -> spot preso dallo snapshot, stessa fonte)") + + sgn = np.where(d["option_type"].values == "C", 1.0, -1.0) + S = d["spot"].values + G = d["gamma"].values * d["open_interest"].values + d["gex_std"] = G * S * S * 0.01 * sgn # GEX da manuale: dollar-gamma per 1%, call +, put - + d["gex_all"] = G * S * S * 0.01 # senza distinzione call/put + d["gex_nos"] = G * sgn # senza scala S^2 + d["gex_S1"] = G * S * sgn # scala lineare in S + agg = (d.groupby(["asset", "tsf"]) + .agg(**{c: (c, "sum") for c in ["gex_std", "gex_all", "gex_nos", "gex_S1"]}, + n=("gamma", "size")).reset_index()) + + print("\n2b. LA RICOSTRUZIONE (catena near-full `bite:research` vs la colonna di bite)") + for a in ASSETS: + j = (agg[agg.asset == a].merge(sp[sp.asset == a][["tsf", "dealer_net_gamma"]], on="tsf") + .dropna(subset=["dealer_net_gamma"])) + print(f" {a}: n={len(j)} snapshot, ~{j.n.median():.0f} strike ciascuno") + for c in ["gex_std", "gex_all", "gex_nos", "gex_S1"]: + COUNT["ricostruzione"] += 1 + rp = np.corrcoef(j[c], j.dealer_net_gamma)[0, 1] + rs = j[c].corr(j.dealer_net_gamma, method="spearman") + sa = float(np.mean((j[c] > 0) == (j.dealer_net_gamma > 0))) + sl, ic = np.polyfit(j[c], j.dealer_net_gamma, 1) + print(f" {c:8s} corr {rp:+.3f} rank {rs:+.3f} accordo-di-segno {sa:.3f}" + f" fit dng = {sl:+.3f}*GEX {ic:+.2e}") + best = j + out[f"corr_{a}"] = float(np.corrcoef(best.gex_std, best.dealer_net_gamma)[0, 1]) + out[f"slope_{a}"] = float(np.polyfit(best.gex_std, best.dealer_net_gamma, 1)[0]) + out[f"intc_{a}"] = float(np.polyfit(best.gex_std, best.dealer_net_gamma, 1)[1]) + print("\n RISULTATO: il GEX da manuale riproduce la colonna di bite a corr -0.95 (BTC) /") + print(" -0.89 (ETH), con accordo-di-segno del 10%/8%. Cioe':") + print(" dealer_net_gamma ~ -0.7 * GEX_standard -> LA COLONNA E' IL GEX COL SEGNO INVERTITO.") + print(" `dealer_net_gamma > 0` significa dealer SHORT gamma, non long. Il nome mente.") + print(" Questo CHIUDE la contraddizione della sezione 1d: e' la colonna dng ad avere il segno") + print(" girato, il gamma_flip_level e' nella convenzione classica. Da qui in poi il codice usa") + print(" `short_gamma = dealer_net_gamma > 0`.") + + print("\n2c. LA SOGLIA NON E' PORTABILE β€” e questo e' il punto che uccide il riuso diretto") + for a in ASSETS: + z = -out[f"intc_{a}"] / out[f"slope_{a}"] + print(f" {a}: dng=0 corrisponde a GEX = {z:+.3e}, non a GEX = 0.") + print(" Le due serie hanno zeri DIVERSI (universi diversi: il nostro collettore filtra OI>=100") + print(" e scadenze <=95g). Trasportare 'la soglia zero' da una all'altra e' un cambio di") + print(" definizione mascherato da continuita'. Ricalibrarla sui 90 giorni sarebbe selezione.") + + print("\n2d. IL SEGNALE E' VIVO OGGI? (raccolta nostra, `pyg`, oraria)") + fs = sorted(glob.glob(str(CHAIN_D / "2026-08-*.parquet"))) + if fs: + o = pd.concat([pd.read_parquet(f, columns=["ts", "asset", "option_type", "gamma", + "open_interest", "underlying_price", + "quote_status"]) for f in fs], ignore_index=True) + o["ts"] = pd.to_datetime(o["ts"], utc=True) + o = o.dropna(subset=["gamma", "open_interest", "underlying_price"]) + s2 = np.where(o["option_type"].values == "C", 1.0, -1.0) + o["gex"] = (o["gamma"].values * o["open_interest"].values + * o["underlying_price"].values ** 2 * 0.01 * s2) + g = (o.groupby(["asset", o["ts"].dt.floor("h")]) + .agg(gex=("gex", "sum"), n=("gamma", "size")).reset_index()) + for a in ASSETS: + x = g[g.asset == a] + print(f" {a}: {len(x)} ore dal {x.ts.min():%Y-%m-%d} al {x.ts.max():%Y-%m-%d %H:%M}, " + f"~{x.n.median():.0f} strike/ora, quote ok {100*(o[o.asset==a].quote_status=='ok').mean():.1f}%") + print(f" GEX medio {x.gex.mean():+.3e} frac GEX>0 (dealer LONG gamma) {(x.gex>0).mean():.3f}" + f" ultimo {x.gex.iloc[-1]:+.3e}") + print(" -> RICOSTRUIBILE, oraria, viva, con MEGLIO del dato originale (segno giusto e") + print(" `quote_status` esplicito). Il lead e' inseguibile: il dato non e' il vincolo.") + print(" -> ma nota: ad agosto il GEX e' >0 nell'88-93% delle ore. Il regime 'short gamma'") + print(" compare a sprazzi: la variabile non varia quasi mai, e questo e' un problema") + print(" di potenza che il tempo aggiusta LENTAMENTE.") + else: + print(" (nessun file 2026-08-* nella raccolta: salto)") + return out + + +# ================================================================== 3. IL FATTO (A): la vol +def sezione_3_fatto_vol(M: dict, snap: pd.DataFrame, rec: dict) -> dict: + hr("3. GAMBA (A) DELL'IPOTESI β€” short gamma amplifica la vol realizzata?") + out = {} + rng = np.random.default_rng(SEED) + print("Misura il FATTO condizionale, non una strategia. Se un effetto e' vero si vede qui.") + + print("\n3a. VOL REALIZZATA FORWARD, condizionata al regime (segno corretto in sezione 2)") + for a in ASSETS: + m = M[a] + for c in ["rvf4", "rvf24"]: + g = m.groupby("short_gamma")[c].mean() * ANN + COUNT["vol_condizionata"] += 1 + print(f" {a} {c}: LONG gamma {g.get(0.0, np.nan):5.1f}% SHORT gamma {g.get(1.0, np.nan):5.1f}%" + f" spread {g.get(1.0,0)-g.get(0.0,0):+5.1f} pp") + print(" Direzione CONFORME al manuale: short gamma -> vol forward piu' alta. Prima gamba OK.") + + print("\n3b. MA: il regime coincide quasi col MESE. Scomposizione (regola: un contributo si") + print(" scompone per periodo PRIMA di crederci β€” lezione SOL, 22/08)") + for a in ASSETS: + m = M[a].dropna(subset=["rvf24", "short_gamma"]).copy() + m["mo"] = m["dt"].dt.strftime("%Y-%m") + obs = (m.rvf24[m.short_gamma == 1].mean() - m.rvf24[m.short_gamma == 0].mean()) * ANN + print(f" {a} spread pieno {obs:+.1f} pp") + for mo, gg in m.groupby("mo"): + ns, nl = int(gg.short_gamma.sum()), int((1 - gg.short_gamma).sum()) + sp = (gg.rvf24[gg.short_gamma == 1].mean() - gg.rvf24[gg.short_gamma == 0].mean()) * ANN + print(f" {mo}: spread {sp:+6.1f} pp ore SHORT {ns:4d} / LONG {nl:4d}" + f" {'<-- mese quasi interamente in UN regime' if min(ns,nl) < 100 else ''}") + out[f"spread_{a}"] = obs + print(" -> Maggio e Luglio sono mesi LONG-gamma, Giugno e' un mese SHORT-gamma. Il confronto") + print(" 'short vs long' e', in pratica, 'giugno vs maggio+luglio': UN confronto, non 2.136.") + + print("\n3c. NULL a spostamento circolare (conserva l\'autocorrelazione di ENTRAMBE le serie,") + print(" rompe solo l\'allineamento) β€” e il suo LIMITE DI RISOLUZIONE") + for a in ASSETS: + m = M[a].dropna(subset=["rvf24", "short_gamma"]) + lab, y = m.short_gamma.values, m.rvf24.values + n = len(y) + obs = (y[lab == 1].mean() - y[lab == 0].mean()) * ANN + null = np.empty(2000) + for i in range(2000): + L = np.roll(lab, rng.integers(24, n - 24)) + null[i] = (y[L == 1].mean() - y[L == 0].mean()) * ANN + p = float((np.abs(null) >= abs(obs)).mean()) + runs = pd.Series(lab).groupby((pd.Series(lab).diff() != 0).cumsum()).size() + long_runs = runs[runs > 24] + n_align = n / max(long_runs.median(), 1.0) + print(f" {a}: osservato {obs:+.1f} pp | null media {null.mean():+.2f} sd {null.std():.2f}" + f" | p empirico (2 code) {p:.4f}") + print(f" MA lo spostamento circolare di una serie con episodi lunghi ~{long_runs.median():.0f}h") + print(f" produce solo ~{n_align:.0f} allineamenti DISTINTI: la risoluzione del p non e\'") + print(f" 1/2000, e\' ~1/{n_align:.0f}. Un p 'zero' su {n_align:.0f} configurazioni non e\' 1e-4.") + out[f"p_{a}"] = p + + print("\n3c-bis. LA PROVA CHE COSTA MENO DI TUTTE: si toglie GIUGNO (l\'unico mese short-gamma)") + for a in ASSETS: + m = M[a].dropna(subset=["rvf24", "short_gamma"]).copy() + m["mo"] = m["dt"].dt.strftime("%Y-%m") + for drop in (None, "2026-06"): + g = m if drop is None else m[m.mo != drop] + ns, nl = int(g.short_gamma.sum()), int((1 - g.short_gamma).sum()) + sp = (g.rvf24[g.short_gamma == 1].mean() - g.rvf24[g.short_gamma == 0].mean()) * ANN + tag = "campione pieno" if drop is None else "senza giugno" + print(f" {a} {tag:16s}: spread {sp:+6.1f} pp ore SHORT {ns:4d} / LONG {nl:4d}") + print(" -> tolto UN mese su tre restano ~40-130 ore di regime short su 1.400: lo 'spread'") + print(" diventa il confronto fra due manciate di ore. Non e\' un campione, e\' un episodio.") + + print("\n3d. NULL LOCATION-MATCHED per `gamma_flip_level`: livello di opzioni o media mobile?") + for a in ASSETS: + m = M[a] + g = m.groupby("above_flip")["rvf24"].mean() * ANN + sp_gamma = float(g.get(0.0, np.nan) - g.get(1.0, np.nan)) + print(f" {a} GAMMA spot>flip: sotto {g.get(0.0, np.nan):.1f}% sopra {g.get(1.0, np.nan):.1f}%" + f" spread {sp_gamma:+.1f} pp") + best = None + for nn in (6, 12, 24, 48, 72, 120, 168, 240, 336, 504, 720): + COUNT["sma"] += 1 + sma = m["close"].rolling(nn).mean() + ok = sma.notna() + agree = float((((m["close"] > m["gamma_flip_level"]) == (m["close"] > sma))[ok]).mean()) + gg = m.groupby((m["close"] > sma).astype(float))["rvf24"].mean() * ANN + spread = float(gg.get(0.0, np.nan) - gg.get(1.0, np.nan)) + if best is None or agree > best[1]: + best = (nn, agree, spread) + if best[2] > sp_gamma + 1.0: + verdetto = "il sosia da uno spread PIU GRANDE del livello di opzioni" + elif abs(best[2] - sp_gamma) <= 1.0: + verdetto = "il sosia da lo STESSO spread" + else: + verdetto = "il sosia da uno spread minore" + print(f" miglior sosia: spot>SMA({best[0]}h) -> accordo di regime {best[1]:.3f}, " + f"spread {best[2]:+.1f} pp ({verdetto})") + out[f"sma_agree_{a}"], out[f"sma_span_{a}"] = best[1], best[0] + out[f"sma_spread_{a}"], out[f"flip_spread_{a}"] = best[2], sp_gamma + print(" LETTURA, asset per asset (i due casi NON dicono la stessa cosa):") + print(" BTC: accordo 0.68, e la media mobile NUDA da' uno spread uguale o maggiore") + print(" (+8.8 contro +7.8 pp) -> il livello di opzioni non aggiunge nulla a una SMA.") + print(" ETH: accordo 0.96 -> `gamma_flip_level` E' una media mobile a 30 giorni con un") + print(" altro nome. Il suo spread e' piu' grande (+16.6 vs +10.1 pp), ma la") + print(" differenza vive tutta nel 4% di ore in cui i due regimi divergono: 4% di") + print(" 2.160 ore dentro un campione da 3 episodi non e' evidenza, e non e' testabile.") + print(" In nessuno dei due casi c'e' un contenuto 'opzioni' isolabile: e' la trappola 2 del") + print(" brief (TP01 travestito). `gamma_flip_level` non e' usabile come regime.") + + print("\n3e. IL REGIME AGGIUNGE SOPRA CIO' CHE GIA' SAPPIAMO? (log rv fwd 24h ~ vol passata + trend)") + print(" t_eff = t / sqrt(24) corregge la sovrapposizione della finestra a 24h. NON corregge") + print(" l'ampiezza effettiva (sezione 1e: n_eff 24-53 su 2.130): un t_eff di +2.3 su 6-11") + print(" episodi indipendenti resta un t che conta episodi come se fossero ore.") + for a in ASSETS: + m = M[a] + d = m.dropna(subset=["rvf24", "rvp24", "rvp168", "ret168", "dvol", "short_gamma", "above_flip"]) + d = d[d.rvf24 > 0] + y = np.log(d.rvf24) + base = [np.log(d.rvp24), np.log(d.rvp168), d.ret168] + d = d.assign(sma720=(d["close"] > d["close"].rolling(720).mean()).astype(float).fillna(0.0)) + print(f" {a} (n={len(d)}) corr(short_gamma, DVOL) = " + f"{np.corrcoef(d.short_gamma, d.dvol)[0,1]:+.3f}") + for lab, extra in [("base: vol+trend", []), + (" + spot>SMA720", [d.sma720]), + (" + spot>flip", [d.above_flip]), + (" + short_gamma", [d.short_gamma]), + (" + DVOL", [np.log(d.dvol)]), + (" + DVOL + short_gamma", [np.log(d.dvol), d.short_gamma])]: + COUNT["regressioni"] += 1 + b, t, r2 = ols(y, base + extra) + print(f" {lab:24s} R2 {r2:.3f} ultimo coef {b[-1]:+.3f} t {t[-1]:+6.2f}" + f" t_eff {t[-1]/np.sqrt(24):+5.2f}") + print(" -> BTC: `short_gamma` da solo spiega quanto DVOL da solo (R2 0.271 vs 0.272) e i due") + print(" insieme fanno 0.312 -> l'informazione e' in larga parte GIA' NEL DVOL, che il") + print(" progetto ha gia' provato come modulatore di TP01 il 26/06: era de-levering puro.") + print(" ETH: t_eff ~ +1.0 = rumore.") + return out + + +# ================================================================== 4. IL FATTO (B): MR vs trend +def sezione_4_fatto_mrtrend(M: dict) -> dict: + hr("4. GAMBA (B) DELL'IPOTESI β€” la gamba TRADEABILE: short gamma = trend, long gamma = MR?") + out = {} + print("Questa e' la gamba che diventerebbe un gate. La (A) e' solo il contorno.") + + print("\n4a. VARIANCE RATIO per regime. VR<1 = mean-reversion, VR>1 = trend.") + print(" La versione 'z' standardizza ogni ritorno per la vol delle 24h precedenti: senza,") + print(" il clustering di volatilita' DENTRO un regime gonfia il VR e si legge come trend.") + for a in ASSETS: + m = M[a] + for lab, col in [("grezzo", "r"), ("z-std", "z")]: + L = {k: variance_ratio(m[col][m.short_gamma == 0], k) for k in (2, 4, 12, 24)} + S = {k: variance_ratio(m[col][m.short_gamma == 1], k) for k in (2, 4, 12, 24)} + COUNT["variance_ratio"] += 4 + print(f" {a} {lab}: LONG " + " ".join(f"VR{k}={L[k]:.3f}" for k in (2, 4, 12, 24))) + print(f" {a} {lab}: SHORT " + " ".join(f"VR{k}={S[k]:.3f}" for k in (2, 4, 12, 24))) + if lab == "z-std": + out[f"vr24_long_{a}"], out[f"vr24_short_{a}"] = L[24], S[24] + print("\n LETTURA (versione z a 24h, l'unica scale-free):") + for a in ASSETS: + vl, vs = out[f"vr24_long_{a}"], out[f"vr24_short_{a}"] + if abs(vl - vs) < 0.05: + diag = "NESSUNA separazione (differenza < 0.05)" + elif vs > vl: + diag = "separazione NEL VERSO dell'ipotesi (short gamma piu' trendante)" + else: + diag = "separazione ROVESCIATA: e' il regime LONG gamma a trendare" + print(f" {a}: LONG {vl:.3f} vs SHORT {vs:.3f} -> {diag}") + print(" Due asset, due risposte diverse, e quella che separa lo fa al contrario. La gamba (B)") + print(" non e' 'debole': e' INCOERENTE. Con la gamba (A) confermata, questo e' il punto in cui") + print(" l'ipotesi si rompe β€” perche' e' la (B) che si traderebbe.") + + print("\n4b. La forma tradeable: payoff di momentum per regime, sgn(ritorno passato L) x ritorno") + print(" futuro H. Sharpe ANNUALIZZATO LORDO (nessuna fee: se non regge lordo e' finita).") + Ls, Hs = (1, 4, 12, 24), (1, 4, 12, 24) + cells = [] + for a in ASSETS: + m = M[a] + print(f" {a} " + " ".join(f"H={h:<2d}" for h in Hs) + " (L=long-gamma, S=short-gamma)") + for L in Ls: + past = np.log(m["close"] / m["close"].shift(L)) + row = [] + for H in Hs: + fwd = np.log(m["close"].shift(-H) / m["close"]) + pay = np.sign(past) * fwd + for lab, mask in [("L", m.short_gamma == 0), ("S", m.short_gamma == 1)]: + v = pay[mask].dropna() + s = float(v.mean() / v.std() * np.sqrt(HOUR_YEAR / H)) if len(v) > 30 and v.std() > 0 else np.nan + COUNT["momentum"] += 1 + cells.append((a, L, H, lab, s, v)) + row.append(f"{lab}{s:+.2f}") + print(f" L={L:2d}h " + " ".join(row)) + se = np.sqrt(HOUR_YEAR / len(m)) + neff = NEFF.get(a, len(m)) + print(f" [errore standard di UNA cella a H=1h: +/-{se:.2f} di Sharpe su {len(m)} ore.") + print(f" Con l'ampiezza effettiva della sezione 1e (n_eff={neff:.0f}) diventa" + f" +/-{se*np.sqrt(len(m)/neff):.1f}.") + print(" Ogni numero della tabella qui sopra e' dentro il proprio errore standard.]") + out["cells"] = cells + return out + + +# ================================================================== 5. IL GATE SUL LIBRO +def sezione_5_gate(snap: pd.DataFrame, fatti4: dict) -> dict: + hr("5. IL GATE SUL LIBRO β€” costruito e misurato, con la potenza dichiarata") + out = {} + print("La sezione 4 ha gia' refutato la gamba tradeable. Il gate si costruisce lo stesso, perche'") + print("un 'non funziona' misurato vale piu' di un 'non l'ho provato'.") + + B = A.tp01_baseline_daily() + reg = {} + for a in ASSETS: + s = (snap[(snap.asset == a) & (snap.ts >= HF_START)] + .dropna(subset=["dealer_net_gamma"])[["ts", "dealer_net_gamma"]] + .set_index("ts").resample("1D").last()) + # ultimo snapshot del giorno d -> usato per il giorno d+1: causale per costruzione + reg[a] = (s["dealer_net_gamma"] > 0).astype(float).shift(1) + R = pd.concat(reg, axis=1).mean(axis=1).dropna() + J = pd.concat({"B": B, "S": R}, axis=1, join="inner").dropna() + b = J["B"] + n = len(J) + se = np.sqrt(365.25 / n) + print(f"\n5a. FINESTRA: {J.index.min():%Y-%m-%d} -> {J.index.max():%Y-%m-%d}, {n} giorni.") + print(f" TP01 (baseline 50/50) su questa finestra: Sharpe {sh_d(b):+.2f} maxDD {dd_d(b):.2%}" + f" ritorno {(1+b).prod()-1:+.2%}") + print(f" ERRORE STANDARD DELLO SHARPE su {n} giorni: +/-{se:.2f}.") + print(" Cioe': su questa finestra TP01 e' indistinguibile da qualunque cosa fra -4 e 0. Il") + print(" libro non e' misurabile qui, e nessun gate lo sara'. E' il vincolo, non un dettaglio.") + print(f" (per giunta TP01 e' quasi FLAT: ritorno lordo {100*(1+b).prod()-100:+.2f}% in 3 mesi)") + out["n_days"], out["se_sharpe"] = n, se + + print("\n5b. QUATTRO gate dichiarati (2 polarita' x 2 intensita'), + il null del de-levering") + variants = { + "classico SHORT->1.5x LONG->0.5x": (0.5, 1.5), + "classico mite SHORT->1.25 LONG->0.75": (0.75, 1.25), + "INVERSO SHORT->0.5x LONG->1.5x": (1.5, 0.5), + "risk-off in SHORT (1.0 / 0.5)": (1.0, 0.5), + } + rows = [] + for name, (lo, hi) in variants.items(): + x = b * (lo + (hi - lo) * J["S"]) + COUNT["gate"] += 1 + rows.append((name, sh_d(x), dd_d(x))) + print(f" {name:38s} Sharpe {sh_d(x):+.2f} maxDD {dd_d(x):.2%}") + print("\n NULL DEL DE-LEVERING (obbligatorio su ogni claim di DD β€” 5 occorrenze nel progetto):") + for k in (0.5, 0.6, 0.7, 0.8, 0.9, 1.0): + COUNT["delevering"] += 1 + x = b * k + print(f" baseline x{k:.1f} Sharpe {sh_d(x):+.2f} maxDD {dd_d(x):.2%}") + best_dd = min(rows, key=lambda r: abs(r[2])) + print(f"\n -> il gate 'classico' porta il maxDD da {dd_d(b):.2%} a {rows[0][2]:.2%} con Sharpe" + f" {rows[0][1]:+.2f} (peggiore del baseline {sh_d(b):+.2f});") + print(f" il semplice `baseline x0.6` fa maxDD {dd_d(b*0.6):.2%} a Sharpe {sh_d(b*0.6):+.2f}.") + print(" NULL DE-LEVERING: **FALLITO**. Il gate e' meno leva con passaggi in piu'.") + out["null_delevering"] = "FALLITO" + + print("\n5c. `marginal_vs_tp01` sullo stream INCREMENTALE del gate (cio' che il gate aggiunge)") + ov = (b * (0.5 + 1.0 * J["S"])) - b + try: + rep = A.marginal_vs_tp01(ov) + for k in ("marginal_verdict", "n_days", "n_hold_days", "corr_full", "cand_full_sharpe", + "beta_to_tp01", "reason"): + if k in rep: + print(f" {k:20s} {rep[k]}") + bl = rep.get("blends", {}) + for w, v in bl.items(): + print(f" blend {w}: full {v.get('full')} uplift_full {v.get('uplift_full')}" + f" uplift_hold {v.get('uplift_hold')}") + out["marginal"] = rep.get("marginal_verdict") + except Exception as e: # pragma: no cover + out["marginal"] = f"errore: {e}" + print(f" non girato: {e}") + print(" ATTENZIONE alla lettura: `n_hold_days` == `n_days` β€” i 90 giorni cadono INTERAMENTE") + print(" dentro l'hold-out di altlib, quindi 'full' e 'hold' sono LA STESSA FINESTRA e i loro") + print(" uplift coincidono (si vede sopra: -0.066 e -0.066). I gate multi-cut e il jackknife") + print(" di `marginal_vs_tp01` girano su una finestra sola.") + print(" Un ADDS qui non sarebbe un ADDS: sarebbe la definizione di selezione") + print(" sull'hold-out. Si riporta il verdetto, non lo si usa per promuovere.") + + print("\n5d. DEFLATED SHARPE sulla cella migliore della griglia della sezione 4") + cells = [c for c in fatti4["cells"] if np.isfinite(c[4])] + best = max(cells, key=lambda c: c[4]) + a, L, H, lab, s_best, v = best + allsr = [c[4] for c in cells] + try: + dsr, null_max = A.deflated_sharpe(s_best, allsr, pd.Series(v.values).dropna().values, + dpy=HOUR_YEAR / H) + reg = "SHORT" if lab == "S" else "LONG" + print(f" cella migliore: {a} L={L}h H={H}h regime={reg} gamma -> Sharpe LORDO {s_best:+.2f}" + f" (su {len(allsr)} trial dichiarati)") + print(f" deflated Sharpe = {dsr:.3f} -> {'PASS' if dsr >= 0.95 else 'FAIL'} (soglia 0.95)") + print(f" massimo Sharpe ATTESO SOTTO IL NULLO con {len(allsr)} trial = {null_max:+.2f}") + if s_best < null_max: + print(f" -> la cella migliore ({s_best:+.2f}) sta SOTTO cio' che il puro rumore produce") + print(f" ({null_max:+.2f}) cercando {len(allsr)} volte. Non c'e' niente da deflazionare:") + print(" la griglia non ha prodotto nemmeno il massimo che il caso avrebbe prodotto.") + out["dsr"], out["dsr_nullmax"] = float(dsr), float(null_max) + except Exception as e: # pragma: no cover + out["dsr"] = f"errore: {e}" + print(f" non girato: {e}") + + print("\n5e. CAUSALITA'") + print(" Per COSTRUZIONE: ogni riga usa `merge_asof(direction='backward')` sull'APERTURA della") + print(" barra con tolleranza 2h, quindi il regime della barra [t,t+1h) e' noto a t; il gate") + print(" giornaliero usa l'ultimo snapshot del giorno d-1 (`.shift(1)`).") + print(" `A.causality_ok` non e' applicabile: perturba il futuro dei prezzi certificati, ma il") + print(" regime qui non viene dai prezzi β€” viene da un file esterno che copre 90 giorni su 8") + print(" anni. Girarlo darebbe un PASS privo di potenza sul 97% del campione (stessa forma del") + print(" test a potenza zero corretto il 21/08). Si dichiara l'argomento strutturale, non un") + print(" numero finto.") + out["causality"] = "per costruzione (merge_asof backward + shift(1)); A.causality_ok non applicabile" + return out + + +# ================================================================== main +def main() -> None: + hr("r0822 DEALER-GAMMA β€” il regime di gamma dei dealer separa MR da trend su BTC/ETH?") + print("IPOTESI: (A) short gamma amplifica la vol; (B) short gamma fa TRENDARE il prezzo, long") + print("gamma lo fa MEAN-REVERTERE -> gate di regime sopra TP01/SKH01.") + print("ATTESA DICHIARATA PRIMA DI MISURARE: (A) plausibile ma confusa col livello di vol;") + print("(B) improbabile su 3 mesi e a rischio 'TP01 travestito'.") + + snap = load_snap() + d1 = sezione_1_dato(snap) + d2 = sezione_2_ricostruzione(snap) + + hf = snap[snap.ts >= HF_START] + M = {a: joined(a, hf) for a in ASSETS} + for a in ASSETS: + print(f"\n[merge] {a}: {len(M[a])} barre orarie con regime, " + f"{M[a].dt.min():%Y-%m-%d} -> {M[a].dt.max():%Y-%m-%d}") + + d3 = sezione_3_fatto_vol(M, hf, d1) + d4 = sezione_4_fatto_mrtrend(M) + d5 = sezione_5_gate(snap, d4) + + hr("VERDETTO") + tot = sum(COUNT.values()) + print(f"GRIGLIA DICHIARATA: {tot} celle/statistiche valutate, contate al rialzo:") + for k, v in COUNT.items(): + print(f" {k:18s} {v:4d}") + print(" Nessuna griglia e' stata ridotta per budget: il vincolo qui e' il DATO, non la macchina.") + print(f" Le {COUNT['momentum']} celle di momentum sono quelle passate al deflated Sharpe (5d).") + print() + print("SCARTATO. La gamba (A) e' confermata nella direzione ma non nella potenza; la gamba (B),") + print("che e' quella che si traderebbe, e' INCOERENTE fra i due asset e rovesciata dove separa.") + print() + print(" 1. Il dato regge meno di quanto dichiara: 15 min solo dal 2026-05-01 (90 giorni, non 4") + print(" mesi); 2 colonne su 16 COSTANTI; `gamma_flip_level` congelato al 61% su BTC.") + print(" 2. `dealer_net_gamma` e' il GEX COL SEGNO INVERTITO (corr -0.95 BTC / -0.89 ETH contro") + print(" la ricostruzione dalla catena). Chi lo usasse col nome che porta leggerebbe ogni") + print(" regime al contrario. -> Questo e' il risultato riusabile di tutto il filone.") + print(" 3. `gamma_flip_level` e' una media mobile: 96% di accordo con spot>SMA(720h) su ETH; su") + print(" BTC (accordo 0.68) la SMA nuda da' uno spread di vol MAGGIORE del livello stesso.") + print(" Niente contenuto di opzioni isolabile. Trappola 2 del brief.") + print(" 4. Ampiezza effettiva: 73 (BTC) / 59 (ETH) episodi di regime, ma SEI e OTTO di essi") + print(" coprono il 78-86% del tempo; n_eff 53 e 24 ore su 2.130. Il regime coincide col") + print(" MESE (maggio LONG, giugno SHORT, luglio LONG): lo spread di vol e' un confronto") + print(" fra tre mesi, non fra 2.136 osservazioni. Tolto giugno cala del 46% (BTC) e del") + print(" 71% (ETH) e resta poggiato su 134 / 366 ore.") + print(" 5. Cio' che resta della gamba (A) e' quasi tutto DVOL (corr 0.66 su BTC), e il DVOL") + print(" come modulatore di TP01 e' gia' stato refutato il 26/06: era de-levering.") + print(" 6. Il gate misurato FALLISCE il null del de-levering (baseline x0.6 lo domina) su una") + print(" finestra dove SE(Sharpe) = +/-2.0 e TP01 e' praticamente flat.") + print(" 7. Deflated Sharpe 0.440 = FAIL, e nel modo piu' netto: la cella migliore delle 64") + print(" (+4.98 LORDO) sta SOTTO il massimo che 64 estrazioni di puro rumore producono") + print(" (+5.12). Non c'e' un candidato da deflazionare, c'e' una griglia sotto il rumore.") + print() + print("NON e' un LEAD: un lead richiede una soglia e una data che abbiano senso, e qui la soglia") + print("non e' nemmeno portabile fra la serie storica e quella che raccogliamo (zeri diversi,") + print("universi diversi). Ricalibrarla sui 90 giorni sarebbe la selezione che il progetto rifiuta.") + print() + print("COSA CONSERVARE (non e' una strategia, e' infrastruttura):") + print(" - il GEX si ricostruisce dalla NOSTRA catena, oraria, viva (518 ore dal 2026-08-01,") + print(" ~209-262 strike, quote ok ~100%), col segno GIUSTO e `quote_status` esplicito;") + print(" - se un giorno lo si riapre, si riapre su GEX ricostruito da noi, mai sulla colonna") + print(" ereditata, e non prima di avere ~30-40 EPISODI di regime (non 30-40 giorni):") + print(" al ritmo osservato (6-8 episodi / 90 giorni) sono ~2 anni, cioe' meta' 2028.") + print() + print("COSA MI SMENTIREBBE: una separazione del variance ratio z-standardizzato nello STESSO") + print("verso sui due asset (short gamma VR>1, long gamma VR<1) su GEX ricostruito da noi, con") + print("almeno 30 episodi di regime indipendenti e uno spread che sopravvive al controllo") + print("location-matched contro spot>SMA. Oggi ho l'opposto: BTC non separa, ETH separa al rovescio.") + print() + print(f"SCRIPT: scripts/research/r0822_dealer_gamma.py") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_flow_squeeze.py b/scripts/research/r0822_flow_squeeze.py new file mode 100644 index 0000000..f587a6e --- /dev/null +++ b/scripts/research/r0822_flow_squeeze.py @@ -0,0 +1,671 @@ +"""r0822_flow_squeeze.py β€” filone FLOW-SQUEEZE (ondata 2026-08-22). + +DOMANDA + Il posizionamento affollato (variazione rapida dell'open interest) ACCOPPIATO al rischio + di liquidazione produce un movimento prevedibile β€” squeeze o cascata β€” nelle ore successive? + E' l'unico angolo di "flusso" mai chiuso: il filone funding e' chiuso su 3 lati (carry CC01, + time-series, cross-sectional) ma sempre come LIVELLO del costo di carry, mai come PROXY DI + AFFOLLAMENTO accoppiato al rischio di liquidazione. + +PERIMETRO (dichiarato: un altro filone dell'ondata lavora su dealer_net_gamma/gamma_flip_level, + che qui NON si toccano) + data/raw/cb_market_snapshots.parquet -> oi_delta_pct_4h, liquidation_long_risk, + liquidation_short_risk, funding_perp_annualized, funding_cross_annualized, iv_minus_rv + data/raw/hlfund__1h.parquet -> funding orario Hyperliquid (BTC/ETH, ~3 anni) + data/raw/cb_chain/ -> open interest per strike (per la RICOSTRUIBILITA') + +IPOTESI A PRIORI, REGISTRATA PRIMA DI MISURARE + Attesa: NULLA di deployabile. (a) il dataset e' MORTO il 2026-07-30 (progetto esterno + dismesso) e ha ~3 mesi utili -> verdetto massimo LEAD; (b) il progetto ha gia' chiuso il + funding su 3 lati; (c) il sospetto principale e' che un eventuale effetto sull'estremo di + oi_delta sia "il prezzo e' appena crollato" travestito, cioe' momentum/mean-reversion di + prezzo gia' coperta. Cio' che NON mi aspettavo: che meta' dell'ipotesi (il rischio di + liquidazione) fosse **inesistente nel dato**. + +TEST DI POTENZA β€” DICHIARATO PRIMA DI LEGGERE I RISULTATI (sezione 2) + Il campione utile e' ~90 giorni x 2 asset. La MDE (minimum detectable effect a 2 SE) si + calcola sul numero di finestre NON sovrapposte, non sulle righe orarie. Un effetto sotto la + MDE NON e' un fatto stabilito, comunque venga il p-value di un bootstrap su dati sovrapposti. + +COME SI LEGGE IL RISULTATO + Le sezioni 1 e 5 (validazione e ricostruibilita') valgono piu' delle 2-4: se una colonna e' + costante non ha potenza, e se non sappiamo piu' produrla il segnale e' morto alla nascita. + +USO: nice -n 19 timeout 900 uv run python scripts/research/r0822_flow_squeeze.py +""" +from __future__ import annotations + +import bisect +import glob +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +import altlib as A # noqa: E402 + +RAW = ROOT / "data" / "raw" +SNAP_FILE = RAW / "cb_market_snapshots.parquet" +ASSETS = ("BTC", "ETH") +# finestra utile: prima del 01/05 gli snapshot sono ~1/giorno (vedi sezione 1) +W0 = pd.Timestamp("2026-05-01", tz="UTC") +HOURS = (4, 12, 24) + +# GRIGLIA DICHIARATA (conteggio al RIALZO, incluse le varianti scartate) +GRID_DECLARED = ( + 10 * 3 * 2 # 60 decili di oi_delta x 3 orizzonti x 2 lenti (demean per asset / per mese) + + 4 * 2 # 8 2x2 affollamento (OI alto/basso x funding alto/basso) x 2 orizzonti + + 4 * 2 # 8 specificazioni di regressione x 2 orizzonti + + 10 * 2 # 20 decili di funding HL x 2 orizzonti + + 3 * 3 * 2 # 18 griglia del segnale: soglia x orizzonte di holding x segno + + 2 # 2 null del segnale (sempre-short, timing casuale) + + 2 # 2 ricostruibilita' (2 asset) +) # = 118 + + +def hdr(t: str) -> None: + print("\n" + "=" * 78 + f"\n{t}\n" + "=" * 78) + + +# --------------------------------------------------------------------------- +# dati +# --------------------------------------------------------------------------- +def load_snapshots() -> pd.DataFrame: + s = pd.read_parquet(SNAP_FILE) + # epoca ESPLICITA in ms: .view("int64") su tz-aware non-ns e' un look-ahead noto + s["ts_ms"] = s["ts"].astype("datetime64[ms, UTC]").astype("int64") + return s.sort_values("ts_ms").reset_index(drop=True) + + +def snap_num(snap: pd.DataFrame, asset: str, cols: list[str]) -> pd.DataFrame: + d = snap[snap.asset == asset][["ts_ms"] + cols].copy() + for c in cols: + d[c] = pd.to_numeric(d[c], errors="coerce") + return d.sort_values("ts_ms").reset_index(drop=True) + + +def merge_causal(df: pd.DataFrame, right: pd.DataFrame) -> pd.DataFrame: + """Snapshot noto alla CHIUSURA della barra i (= timestamp[i] + 1h), backward, tolleranza 2h. + La posizione decisa a i e' poi tenuta durante i+1 (lo shift lo fa eval_weights).""" + left = pd.DataFrame({"ts_ms": (df["timestamp"].astype("int64") + 3_600_000).values}) + m = pd.merge_asof(left, right, on="ts_ms", direction="backward", + tolerance=2 * 3600 * 1000) + return m.drop(columns=["ts_ms"]) + + +def panel(snap: pd.DataFrame, cols: list[str], start=W0) -> pd.DataFrame: + out = [] + for a in ASSETS: + df = A.get(a, "1h") + df = df[df.datetime >= start - pd.Timedelta(days=7)].reset_index(drop=True) + df = pd.concat([df, merge_causal(df, snap_num(snap, a, cols))], axis=1) + cc = df["close"].values.astype(float) + for H in HOURS: + f = np.full(len(cc), np.nan) + f[:-H] = cc[H:] / cc[:-H] - 1.0 + df[f"f{H}"] = f + p4 = np.full(len(cc), np.nan) + p4[4:] = cc[4:] / cc[:-4] - 1.0 + df["p4"] = p4 + df["asset"] = a + out.append(df) + P = pd.concat(out, ignore_index=True) + return P[P.datetime >= start].reset_index(drop=True) + + +# --------------------------------------------------------------------------- +# 1. VALIDAZIONE DEL DATO +# --------------------------------------------------------------------------- +def sezione1(snap: pd.DataFrame) -> None: + hdr("1. VALIDAZIONE DEL DATO β€” le colonne vengono da un progetto esterno DISMESSO") + print(f"righe={len(snap)} ts {snap.ts.min()} -> {snap.ts.max()}") + cnt = snap[snap.asset == "BTC"].set_index("ts").resample("1D").size() + fasce = pd.cut(cnt, [-1, 0, 10, 50, 90, 400]).value_counts().sort_index() + print("\ngiorni per numero di snapshot/giorno (BTC):") + for k, v in fasce.items(): + print(f" {str(k):>14s} : {v:3d} giorni") + pieni = cnt[cnt >= 90] + print(f" -> primo giorno a cadenza piena (>=90/g): {pieni.index.min().date()}" + f" ; giorni pieni = {len(pieni)}") + print(" ATTENZIONE: la copertura dichiarata parte dal 2026-03-26 ma 36 giorni hanno <=10") + print(" snapshot: la finestra USABILE a risoluzione oraria e' 2026-05-01 -> 2026-07-30.") + + for a in ASSETS: + d = snap[snap.asset == a] + print(f"\n--- {a} (n={len(d)}) ---") + ok = d.fetch_ok.value_counts().to_dict() + print(f" fetch_ok: {ok} -> fallite {100*(1-d.fetch_ok.mean()):.1f}%") + for c in ("liquidation_long_risk", "liquidation_short_risk"): + vc = d[c].value_counts(dropna=False).to_dict() + nun = d[c].nunique() + flag = " <<< COSTANTE: ZERO POTENZA" if nun <= 1 else "" + print(f" {c:26s} categorie={nun} {vc}{flag}") + for c in ("oi_delta_pct_4h", "funding_perp_annualized", "funding_cross_annualized", + "iv_minus_rv", "macro_days_to_event"): + s = pd.to_numeric(d[c], errors="coerce") + mode_share = s.value_counts(normalize=True).iloc[0] if s.notna().any() else np.nan + print(f" {c:26s} nan={100*s.isna().mean():5.1f}% nuniq={s.nunique():5d} " + f"moda={s.mode().iloc[0] if s.notna().any() else float('nan'):+.4f} " + f"({100*mode_share:.1f}% delle righe) " + f"[{s.min():+.3f}, {s.median():+.3f}, {s.max():+.3f}]") + s = pd.to_numeric(d.oi_delta_pct_4h, errors="coerce").dropna() + same = (s.diff() == 0) + print(f" oi_delta_pct_4h: congelato {100*same.mean():.2f}% delle volte, " + f"autocorr(15m)={s.autocorr(1):+.3f} autocorr(4h)={s.autocorr(16):+.3f}") + print(" -> autocorr alta a 15m e ~0 a 4h = firma di una VERA finestra mobile a 4h " + "(non un valore inventato/congelato)") + + print("\n>>> ESITO SEZIONE 1") + print(" (a) liquidation_long_risk e liquidation_short_risk valgono 'low' nel 100% delle") + print(" righe non-nulle, su 17.406 righe, 4 mesi, entrambi gli asset: UNA sola") + print(" categoria. META' DELL'IPOTESI E' UNTESTABILE PER COSTRUZIONE β€” non 'debole',") + print(" proprio senza varianza. E' il risultato piu' importante del filone.") + print(" (b) funding_perp_annualized e' ANCORATO al tasso base (0.01%/8h = 0.1095/anno)") + print(" nel ~49% (BTC) / ~60% (ETH) delle righe: come proxy di affollamento e' muto") + print(" per meta' del campione.") + print(" (c) oi_delta_pct_4h e' l'unica colonna del perimetro che si comporta da serie vera.") + + +# --------------------------------------------------------------------------- +# 2. IL FATTO +# --------------------------------------------------------------------------- +def bboot_p(x: np.ndarray, nb: int = 2000, block: int = 24, seed: int = 7) -> float: + x = np.asarray(x, float) + x = x[np.isfinite(x)] + n = len(x) + if n < block * 3: + return float("nan") + rng = np.random.default_rng(seed) + k = int(np.ceil(n / block)) + starts = rng.integers(0, n - block, size=(nb, k)) + means = np.array([np.concatenate([x[s:s + block] for s in starts[i]])[:n].mean() + for i in range(nb)]) + return float((means >= 0).mean() if x.mean() < 0 else (means <= 0).mean()) + + +def ols_bboot(d: pd.DataFrame, y: str, cols: list[str], nb: int = 500, + blk: int = 48, seed: int = 11): + X = np.column_stack([np.ones(len(d))] + [d[c].values for c in cols]) + Y = d[y].values + b = np.linalg.lstsq(X, Y, rcond=None)[0] + rng = np.random.default_rng(seed) + n = len(Y) + k = int(np.ceil(n / blk)) + bs = [] + for _ in range(nb): + st = rng.integers(0, n - blk, size=k) + idx = np.concatenate([np.arange(s, s + blk) for s in st])[:n] + try: + bs.append(np.linalg.lstsq(X[idx], Y[idx], rcond=None)[0]) + except np.linalg.LinAlgError: + pass + se = np.array(bs).std(0) + return b, se + + +def sezione2(snap: pd.DataFrame) -> pd.DataFrame: + hdr("2. IL FATTO β€” ritorno futuro condizionato all'OI (potenza DICHIARATA PRIMA)") + P = panel(snap, ["oi_delta_pct_4h", "funding_perp_annualized", + "funding_cross_annualized", "iv_minus_rv"]) + d = P.dropna(subset=["oi_delta_pct_4h", "f24", "p4"]).copy() + print(f"campione: {len(d)} barre orarie ({d.datetime.min()} -> {d.datetime.max()})") + + print("\n--- TEST DI POTENZA (dichiarato prima di leggere qualunque media) ---") + mde = {} + for H in HOURS: + n_ind = len(d) / H + sd = d[f"f{H}"].std() + mde[H] = 2 * sd / np.sqrt(n_ind / 10) + print(f" H={H:2d}h finestre NON sovrapposte={n_ind:6.0f} sd={100*sd:.2f}% " + f"n/decile={n_ind/10:5.0f} MDE(2 SE)={100*mde[H]:.3f}%") + print(" REGOLA PRE-REGISTRATA: un effetto di decile sotto la propria MDE NON e' un fatto") + print(" stabilito, qualunque p-value dia un bootstrap su osservazioni sovrapposte.") + + for a in ASSETS: + m = d.asset == a + for H in HOURS: + d.loc[m, f"x{H}"] = d.loc[m, f"f{H}"] - d.loc[m, f"f{H}"].mean() + d["mese"] = d.datetime.dt.strftime("%Y-%m") + for H in HOURS: + d[f"y{H}"] = d[f"f{H}"] - d.groupby(["asset", "mese"])[f"f{H}"].transform("mean") + d["odec"] = d.groupby("asset").oi_delta_pct_4h.transform( + lambda s: pd.qcut(s, 10, labels=False)) + + drift = {a: {H: round(100 * d.loc[d.asset == a, f"f{H}"].mean(), 3) for H in HOURS} + for a in ASSETS} + print(f"\ndrift INCONDIZIONATO della finestra (%): {drift}") + print(" -> la finestra e' ribassista: ogni media condizionata va letta DEMEANATA,") + print(" altrimenti 'stare short' sembra un segnale.") + + print("\n--- eccesso di ritorno futuro (%) per decile di oi_delta_pct_4h ---") + print(" lente 1 = demean per ASSET ; lente 2 = demean per (ASSET,MESE)") + g = d.groupby("odec").agg(n=("x4", "size"), oi=("oi_delta_pct_4h", "mean"), + x4=("x4", "mean"), x24=("x24", "mean"), + y4=("y4", "mean"), y24=("y24", "mean"), + av4=("f4", lambda s: s.abs().mean()), + av24=("f24", lambda s: s.abs().mean())) + for c in ("x4", "x24", "y4", "y24", "av4", "av24"): + g[c] *= 100 + g["p_x4"] = [bboot_p(d[d.odec == k].x4.values) for k in g.index] + g["p_x24"] = [bboot_p(d[d.odec == k].x24.values, block=48) for k in g.index] + print(g.round(3).to_string()) + print(" av4/av24 = |ret| futuro medio: e' la gamba 'squeeze/cascata' dell'ipotesi") + print(" (piu' vol dopo un estremo di OI). E' QUASI PIATTA (0.59-0.75% a 4h, solo il decile 0") + print(" leggermente sopra): la 'cascata' prevedibile non c'e'.") + + print("\n--- CONTROLLO: e' solo 'il prezzo si e' appena mosso'? ---") + z = lambda s: (s - s.mean()) / s.std() # noqa: E731 + for c, n in [("oi_delta_pct_4h", "zoi"), ("p4", "zp4"), + ("funding_perp_annualized", "zfund"), ("iv_minus_rv", "ziv")]: + d[n] = d.groupby("asset")[c].transform(z) + dr = d.dropna(subset=["zoi", "zp4", "zfund", "ziv"]).reset_index(drop=True) + print(f" corr(oi_delta, ritorno 4h passato) = {dr.oi_delta_pct_4h.corr(dr.p4):+.3f}" + f" (con 24h passato = {dr.oi_delta_pct_4h.corr(dr.f24.shift(24)):+.3f})") + for y in ("x4", "x24"): + for cols in (["zoi"], ["zp4"], ["zoi", "zp4"], ["zoi", "zp4", "zfund", "ziv"]): + b, se = ols_bboot(dr, y, cols) + txt = " ".join(f"{c}={100*b[i+1]:+.4f}%(t={b[i+1]/max(se[i+1],1e-12):+.2f})" + for i, c in enumerate(cols)) + print(f" {y:4s} ~ {str(cols):36s} {txt}") + print(" -> la PENDENZA LINEARE di oi_delta e' nulla (|t|<1.3 in ogni specificazione).") + print(" Quel poco che c'e' vive SOLO nella coda estrema, e non e' momentum di prezzo") + print(" (corr con il ritorno passato ~0.02): e' un'altra cosa, ma e' una CODA.") + + print("\n--- il decile 0 (OI che CROLLA) β€” l'unica cella non piatta ---") + for H in (4, 24): + obs = d[d.odec == 0][f"y{H}"].mean() + rng = np.random.default_rng(3) + nulls = [] + for _ in range(2000): + sh = [] + for a in ASSETS: + sub = d[d.asset == a] + k = rng.integers(1, len(sub) - 1) + sh.append(sub[f"y{H}"].values[np.roll((sub.odec == 0).values, k)]) + nulls.append(np.concatenate(sh).mean()) + nulls = np.array(nulls) + print(f" H={H:2d}h osservato={100*obs:+.3f}% MDE={100*mde[H]:.3f}% " + f"null circolare: mediana={100*np.median(nulls):+.3f}% " + f"p5={100*np.percentile(nulls,5):+.3f}% p={float((nulls<=obs).mean()):.3f}") + for a in ASSETS: + sub = d[(d.asset == a) & (d.odec == 0)] + cond = (d[d.asset == a].odec == 0).values + # AMPIEZZA EFFETTIVA: il numero di ORE non e' la taglia campionaria se la + # condizione e' fatta di pochi episodi lunghi (lezione del filone gemello + # dealer-gamma della stessa ondata: 73 "episodi" di cui 6-8 all'80% del tempo). + lens, cur = [], 0 + for v in cond: + if v: + cur += 1 + elif cur: + lens.append(cur); cur = 0 + if cur: + lens.append(cur) + L = np.array(sorted(lens)[::-1]) + print(f" {a}: ore in decile0={len(sub)} episodi distinti={len(L)} " + f"durata mediana={np.median(L):.1f}h max={L.max()}h " + f"quota ore negli 8 episodi piu' lunghi={L[:8].sum()/L.sum():.2f} " + f"y4={100*sub.y4.mean():+.3f}% y24={100*sub.y24.mean():+.3f}%") + print(" -> QUI l'ampiezza NON e' la patologia: 86-87 episodi corti (mediana 2h) e i") + print(" primi 8 coprono solo il 20-25% delle ore. Il vincolo binding e' la") + print(" LUNGHEZZA DEL CALENDARIO (90 giorni), non la concentrazione degli episodi.") + print("\n scomposizione PER MESE (demean mensile) β€” il test che il progetto impone") + print(" dopo il caso SOL ('un contributo positivo si scompone prima di crederci'):") + tab = d[d.odec == 0].groupby("mese")[["y4", "y24"]].agg(["size", "mean"]) + for m_, r in tab.iterrows(): + print(f" {m_}: n={int(r[('y4','size')]):4d} " + f"y4={100*r[('y4','mean')]:+.3f}% y24={100*r[('y24','mean')]:+.3f}%") + + print("\n--- 2x2 AFFOLLAMENTO (l'ipotesi originale, col funding al posto della liquidazione) ---") + for c in ("oi_delta_pct_4h", "funding_perp_annualized"): + d[c + "_hi"] = d.groupby("asset")[c].transform(lambda s: s >= s.quantile(.8)) + d[c + "_lo"] = d.groupby("asset")[c].transform(lambda s: s <= s.quantile(.2)) + combos = [("OI su & funding ALTO (long affollati -> cascata?)", + d["oi_delta_pct_4h_hi"] & d["funding_perp_annualized_hi"]), + ("OI su & funding BASSO (short affollati -> squeeze?)", + d["oi_delta_pct_4h_hi"] & d["funding_perp_annualized_lo"]), + ("OI giu & funding ALTO ", + d["oi_delta_pct_4h_lo"] & d["funding_perp_annualized_hi"]), + ("OI giu & funding BASSO", + d["oi_delta_pct_4h_lo"] & d["funding_perp_annualized_lo"])] + base = 100 * d.x4.abs().mean() + for nm, mask in combos: + sub = d[mask] + if len(sub) < 20: + print(f" {nm:52s} n={len(sub)} TROPPO PICCOLO") + continue + print(f" {nm:52s} n={len(sub):5d} x4={100*sub.x4.mean():+.3f}%(p={bboot_p(sub.x4.values):.3f})" + f" x24={100*sub.x24.mean():+.3f}%(p={bboot_p(sub.x24.values,block=48):.3f})" + f" |x4|={100*sub.x4.abs().mean():.3f}%") + print(f" |x4| di riferimento su tutto il campione = {base:.3f}%") + print(" -> 4 celle, 8 test: la sola sotto 0.05 (OI su & funding basso, x24) e' UNA cella su") + print(" 8 con ~8 finestre 24h indipendenti. Non e' un fatto.") + return d + + +# --------------------------------------------------------------------------- +# 3. FUNDING HL β€” l'unica gamba del perimetro con storia lunga +# --------------------------------------------------------------------------- +def sezione3() -> None: + hdr("3. FUNDING HYPERLIQUID BTC/ETH β€” 3 ANNI: l'unica gamba del perimetro con POTENZA") + res = [] + for a in ASSETS: + f = RAW / f"hlfund_{a.lower()}_1h.parquet" + if not f.exists(): + print(f" {f} assente") + return + h = pd.read_parquet(f) + h.index = pd.DatetimeIndex(h.index).tz_convert("UTC").floor("h") + h = h[~h.index.duplicated()] + df = A.get(a, "1h") + df = df.set_index(pd.DatetimeIndex(df.datetime)) + j = df[["close"]].join(h[["funding", "premium"]], how="inner") + for H in (4, 24): + j[f"f{H}"] = j.close.shift(-H) / j.close - 1.0 + j["asset"] = a + res.append(j) + J = pd.concat(res).dropna(subset=["funding", "f24"]) + print(f"n={len(J)} barre orarie, {J.index.min().date()} -> {J.index.max().date()}") + print(" ATTENZIONE: il feed funding HL si ferma al 2026-06-22 (non e' aggiornato a oggi).") + base = J.funding.value_counts(normalize=True).iloc[0] + print(f" il funding e' ANCORATO al tasso base nel {100*base:.1f}% delle ore " + f"(valore {J.funding.mode().iloc[0]:.6f}/h) -> anche qui meta' campione e' muto.") + J["ye"] = J.index.year + J["fdec"] = J.groupby("asset").funding.transform( + lambda s: pd.qcut(s.rank(method="first"), 10, labels=False)) + for H in (4, 24): + J[f"y{H}"] = J[f"f{H}"] - J.groupby(["asset", "ye"])[f"f{H}"].transform("mean") + g = J.groupby("fdec").agg(n=("y4", "size"), fund=("funding", "mean"), + y4=("y4", "mean"), y24=("y24", "mean"), + av4=("f4", lambda s: s.abs().mean()), + av24=("f24", lambda s: s.abs().mean())) + g["fund"] *= 100 * 24 * 365 + for c in ("y4", "y24", "av4", "av24"): + g[c] *= 100 + print("\n--- eccesso su drift ANNUALE (%) e |ret| futuro per decile di funding HL ---") + print(g.round(3).to_string()) + print("\nestremi per ANNO (y24, %):") + t = J[J.fdec.isin([0, 9])].groupby(["fdec", "ye"]).y24.agg(["size", "mean"]) + for k, r in t.iterrows(): + print(f" dec={k[0]} anno={k[1]} n={int(r['size']):5d} y24={100*r['mean']:+.3f}%") + print("\n>>> ESITO SEZIONE 3: su 53.430 osservazioni e 3 anni β€” cioe' DOVE LA POTENZA C'E' β€”") + print(" il funding come proxy di affollamento non predice ne' la DIREZIONE (|eccesso|") + print(" <=0.35% a 24h, segno instabile fra anni) ne' la VOLATILITA' (|ret| futuro piatto") + print(" e non monotono). Il filone funding, gia' chiuso su 3 lati come livello di carry,") + print(" e' chiuso anche come proxy di affollamento.") + + +# --------------------------------------------------------------------------- +# 4. IL SEGNALE CAUSALE + I NULL + I GATE +# --------------------------------------------------------------------------- +def pct_expanding(x: np.ndarray, minobs: int = 200) -> np.ndarray: + """Percentile ESPANDENTE causale: al bar i usa solo i valori <= i-1.""" + out = np.full(len(x), np.nan) + seen: list[float] = [] + for i, v in enumerate(x): + if np.isfinite(v): + if len(seen) >= minobs: + out[i] = bisect.bisect_left(seen, v) / len(seen) + bisect.insort(seen, v) + return out + + +class Signal: + """SHORT (o LONG) quando il percentile causale di oi_delta_pct_4h e' sotto soglia, + tenuto H barre, vol-targeted 20%.""" + + def __init__(self, snap: pd.DataFrame): + self.SN = {a: snap_num(snap, a, ["oi_delta_pct_4h"]).dropna() for a in ASSETS} + + def oi(self, df, asset): + return merge_causal(df, self.SN[asset])["oi_delta_pct_4h"].values.astype(float) + + def make(self, thr: float, H: int, sign: int): + def fn(df, asset): + p = pct_expanding(self.oi(df, asset)) + trig = np.where(np.isfinite(p), (p <= thr).astype(float), 0.0) + held = pd.Series(trig).rolling(H, min_periods=1).max().values + return A.vol_target(sign * held, df, target_vol=0.20, leverage_cap=2.0) + return fn + + +def book_net(target_by_asset) -> pd.Series: + nets = [] + for a in ASSETS: + df = A.get(a, "1h") + m = (df.datetime >= W0).values + d2 = df[m].reset_index(drop=True) + ev = A.eval_weights(d2, np.asarray(target_by_asset(df, a))[m]) + nets.append(pd.Series(ev["net"], index=ev["idx"])) + J = pd.concat(nets, axis=1, join="inner").fillna(0.0) + return 0.5 * J.iloc[:, 0] + 0.5 * J.iloc[:, 1] + + +def stats(net: pd.Series) -> dict: + if net.std() == 0: + return dict(sharpe=0.0, dd=0.0, ret=0.0, cagr=0.0) + eq = (1 + net).cumprod() + dd = float(((eq.cummax() - eq) / eq.cummax()).max()) + yrs = (net.index[-1] - net.index[0]).total_seconds() / (365.25 * 86400) + return dict(sharpe=float(net.mean() / net.std() * np.sqrt(24 * 365.25)), + dd=dd, ret=float(eq.iloc[-1] - 1), + cagr=float(eq.iloc[-1] ** (1 / max(yrs, 1e-9)) - 1)) + + +def sezione4(snap: pd.DataFrame) -> None: + hdr("4. IL SEGNALE CAUSALE β€” griglia 18 celle, i null, i gate") + S = Signal(snap) + rows = [] + for thr in (0.10, 0.20, 0.30): + for H in (4, 12, 24): + for sign in (-1, +1): + st = stats(book_net(S.make(thr, H, sign))) + rows.append(dict(thr=thr, H=H, sign=sign, **st)) + print(f" thr={thr:.2f} H={H:2d} sign={sign:+d} " + f"Sharpe(90g)={st['sharpe']:+.2f} maxDD={100*st['dd']:5.2f}% " + f"ret={100*st['ret']:+6.2f}%") + R = pd.DataFrame(rows) + best = R.loc[R.sharpe.idxmax()] + print(f"\n miglior cella: thr={best.thr} H={int(best.H)} sign={int(best.sign):+d} " + f"-> Sharpe {best.sharpe:.2f}") + print(f" SE(Sharpe) su 90 giorni = sqrt(365/90) = {np.sqrt(365/90):.2f} -> ogni Sharpe") + print(" sotto ~4 e' indistinguibile da zero su questa finestra. Non esiste hold-out:") + print(" la cella e' scelta sull'UNICO campione (select_cell_insample non applicabile).") + + thr, H, sign = float(best.thr), int(best.H), int(best.sign) + tgt = S.make(thr, H, sign) + net = book_net(tgt) + st = stats(net) + + print("\n--- NULL 1: 'sei solo short in una finestra ribassista?' ---") + always = lambda df, a: A.vol_target( # noqa: E731 + sign * np.ones(len(df)), df, target_vol=0.20, leverage_cap=2.0) + st_a = stats(book_net(always)) + tim = np.mean([np.mean(np.asarray(tgt(A.get(a, "1h"), a))[ + (A.get(a, "1h").datetime >= W0).values] != 0) for a in ASSETS]) + print(f" candidato Sharpe={st['sharpe']:+.2f} (tempo a mercato {tim:.3f}) " + f"sempre-esposto Sharpe={st_a['sharpe']:+.2f}") + print(" -> il candidato NON e' la statica travestita: il timing porta qualcosa.") + + print("\n--- NULL 2: timing CASUALE a pari esposizione (shift circolare, 300 estrazioni) ---") + print(" e' il null LOCATION-MATCHED nella dimensione del tempo: stessa esposizione, stessa") + print(" struttura di episodi, stessa finestra β€” cambia SOLO QUANDO. Un null piu' ingenuo") + print(" (posizione casuale iid) direbbe di si' a qualunque cosa in una finestra con drift.") + base_trig = {} + for a in ASSETS: + df = A.get(a, "1h") + base_trig[a] = (np.asarray(tgt(df, a)), (df.datetime >= W0).values) + rng = np.random.default_rng(21) + nulls = [] + for _ in range(300): + shifted = {} + for a in ASSETS: + v, m = base_trig[a] + v2 = v.copy() + idx = np.where(m)[0] + v2[idx] = np.roll(v2[idx], rng.integers(1, len(idx) - 1)) + shifted[a] = v2 + nulls.append(stats(book_net(lambda df, a: shifted[a]))["sharpe"]) + nulls = np.array(nulls) + pctl = float((nulls < st["sharpe"]).mean()) + print(f" null: mediana={np.median(nulls):+.2f} p90={np.percentile(nulls,90):+.2f} " + f"p99={np.percentile(nulls,99):+.2f} -> candidato al {100*pctl:.1f}o pctl") + print(f" ma la cella e' il MASSIMO di 18: P(almeno una cella oltre quel pctl per caso) " + f"= 1-{pctl:.3f}^18 = {1-pctl**18:.2f} -> il null non e' superato dopo il conto dei trial.") + + print("\n--- GATE ---") + print(f" causality_ok : {A.causality_ok(tgt, tf='1h')}") + cd = A.candidate_daily(tgt, tf="1h") + cd = cd[cd.index >= W0] + sr = float(cd.mean() / cd.std() * np.sqrt(365.25)) + rep = A.marginal_vs_tp01(cd) + print(f" candidate_daily : n={len(cd)} giorni, Sharpe={sr:.2f}") + n_hold = int((cd.index >= A.HOLDOUT).sum()) + print(f" marginal_vs_tp01 : verdetto={rep.get('marginal_verdict')} " + f"beta_to_tp01={rep.get('beta_to_tp01')} has_insample_edge={rep.get('has_insample_edge')} " + f"is_hedge={rep.get('is_hedge')} robust_oos={rep.get('robust_oos')} " + f"beats_noise_null={rep.get('beats_noise_null')} null_pctl_full={rep.get('null_pctl_full')}") + print(f" ATTENZIONE: giorni totali={len(cd)}, giorni dopo HOLDOUT({A.HOLDOUT.date()})" + f"={n_hold} -> COINCIDONO") + print(f" (cand_full_sharpe={rep.get('cand_full_sharpe')} == " + f"cand_hold_sharpe={rep.get('cand_hold_sharpe')}). Il candidato vive INTERAMENTE") + print(" dentro l'hold-out del progetto: FULL e HOLD-OUT sono la STESSA finestra e la") + print(" meta' out-of-sample del gate e' strutturalmente INERTE (robust_oos=False non") + print(" giudica il candidato, dice che il multi-cut non esiste). Da leggere come") + print(" 'girato ma NON informativo sull'OOS', non come un NEUTRAL guadagnato sul campo.") + all_sr = list(R.sharpe.values) + d18 = A.deflated_sharpe(sr, all_sr, cd) + d_all = A.deflated_sharpe(sr, all_sr * 7, cd) + print(f" deflated_sharpe(18) : DSR={d18[0]:.3f} (Sharpe atteso del massimo sotto il null " + f"= {d18[1]:.2f}) -> {'PASS' if d18[0]>=0.95 else 'FAIL'}") + print(f" deflated_sharpe({GRID_DECLARED}) : DSR={d_all[0]:.3f} (null max {d_all[1]:.2f}) " + f"-> {'PASS' if d_all[0]>=0.95 else 'FAIL'}") + print(" con 114 giorni il MASSIMO atteso PER CASO su 18 celle e' Sharpe ~5.4:") + print(" il candidato (3.6) sta SOTTO il proprio null. Non serve altro.") + imp = A.implausible_sharpe(cd) + print(f" implausible_sharpe : {imp['implausible']} {imp['reasons']}") + print(" null de-levering : NON GIRATO β€” il candidato non fa alcun claim di riduzione") + print(" di drawdown (e' uno stream di ritorno, non un overlay).") + print(" anchor_luck_band : NON GIRATO β€” il segnale non e' ancorato a un'ora di") + print(" ribilanciamento (gira su ogni barra oraria).") + for a in ASSETS: + df = A.get(a, "1h") + m = (df.datetime >= W0).values + sc = A.eval_weights_smallcap(df[m].reset_index(drop=True), + np.asarray(tgt(df, a))[m], capital=600) + print(f" smallcap $600 {a} : haircut Sharpe={sc['sharpe_haircut']:+.3f} " + f"(reale {sc['realistic']['sharpe']:.2f} vs modellato {sc['modeled']['sharpe']:.2f}, " + f"{sc['n_executed_trades']} trade eseguiti)") + print(" -> l'ESEGUIBILITA' non e' il vincolo (2 gambe BTC/ETH perp, haircut ~0.02):") + print(" come per SOL, il candidato muore sull'evidenza, non sulla taglia del conto.") + + print("\n--- CONCENTRAZIONE (dove sta davvero il P&L) ---") + mm = cd.groupby(cd.index.strftime("%Y-%m")).agg(n="size", ret=lambda s: 100 * ((1 + s).prod() - 1)) + print(mm.round(2).to_string()) + tot = 100 * ((1 + cd).prod() - 1) + top3 = 100 * cd.sort_values().tail(3).sum() + print(f" somma dei 3 giorni migliori = {top3:.2f}% su un totale di {tot:.2f}% " + f"({100*top3/max(tot,1e-9):.0f}% del P&L in 3 giorni su 114)") + + +# --------------------------------------------------------------------------- +# 5. RICOSTRUIBILITA' +# --------------------------------------------------------------------------- +def sezione5(snap: pd.DataFrame) -> None: + hdr("5. RICOSTRUIBILITA' β€” il dato e' MORTO il 2026-07-30: sapremmo rifarlo oggi?") + cols = ["asset", "instrument_name", "open_interest", "ts"] + parts = [] + arch = RAW / "cb_chain" / "bite_archive.parquet" + if arch.exists(): + parts.append(pd.read_parquet(arch, columns=cols)) + for f in sorted(glob.glob(str(RAW / "cb_chain" / "2026-*.parquet"))): + parts.append(pd.read_parquet(f, columns=cols)) + C = pd.concat(parts, ignore_index=True) + del parts + C["ts"] = pd.to_datetime(C.ts, utc=True).dt.floor("h") + C = C.drop_duplicates(["ts", "instrument_name"]) + print(f"catena: {len(C)} righe {C.ts.min()} -> {C.ts.max()}") + sn = snap.copy() + sn["h"] = pd.to_datetime(sn.ts, utc=True).dt.floor("h") + for asset in ASSETS: + s = C[C.asset == asset] + piv = s.pivot_table(index="ts", columns="instrument_name", + values="open_interest", aggfunc="last") + piv = piv.reindex(pd.date_range(piv.index.min(), piv.index.max(), freq="h")) + V = piv.values + dl = np.full(len(piv), np.nan) + for i in range(4, len(piv)): + m = np.isfinite(V[i]) & np.isfinite(V[i - 4]) + if m.sum() < 30: + continue + b = V[i - 4][m].sum() + if b > 0: + dl[i] = 100 * (V[i][m].sum() / b - 1) + chain = pd.Series(dl, index=piv.index, name="chain") + ref = pd.to_numeric(sn[sn.asset == asset].groupby("h").oi_delta_pct_4h.last(), + errors="coerce").rename("snap") + j = pd.concat([ref, chain], axis=1).dropna() + print(f" {asset}: ore sovrapposte={len(j)} " + f"corr Pearson={j.snap.corr(j.chain):+.3f} " + f"Spearman={j.snap.corr(j.chain, method='spearman'):+.3f} " + f"(sd snap={j.snap.std():.3f} vs sd catena={j.chain.std():.3f})") + del piv, V + print("\n Nota: l'OI della catena e' l'OI delle OPZIONI a strumenti APPAIATI (stessi") + print(" strumenti a t e t-4h, cosi' le scadenze non creano salti). La correlazione con") + print(" oi_delta_pct_4h e' ~0 in rango: NON e' la stessa grandezza (lo snapshot misurava") + print(" l'OI dei PERPETUAL/futures via il progetto dismesso).") + print("\n>>> ESITO SEZIONE 5 β€” colonna per colonna, cosa sappiamo produrre OGGI:") + print(" oi_delta_pct_4h : NO. La catena da' OI di OPZIONI (rango ~0.05 con la") + print(" colonna). L'OI perp Deribit e' leggibile dall'API") + print(" pubblica ma NON lo raccogliamo: la storia parte da") + print(" zero il giorno che si accende un collettore.") + print(" liquidation_long/short : NO, e non importa: la colonna e' costante 'low'.") + print(" Nessuna fonte nostra la produce, e non c'e' niente") + print(" da produrre.") + print(" funding_perp/cross : SI ma degradato β€” funding HL c'e' (fermo al 22/06/2026),") + print(" funding Deribit non lo salviamo.") + print(" iv_minus_rv : SI, ricostruibile da dvol_*.parquet + barre certificate.") + + +# --------------------------------------------------------------------------- +def main() -> None: + print(__doc__) + print(f"GRIGLIA DICHIARATA (conteggio al rialzo) = {GRID_DECLARED} celle/trial") + snap = load_snapshots() + sezione1(snap) + sezione2(snap) + sezione3() + sezione4(snap) + sezione5(snap) + hdr("6. VERDETTO") + print("""SCARTATO. + 1. META' DELL'IPOTESI NON ESISTE NEL DATO: liquidation_long_risk e liquidation_short_risk + valgono 'low' nel 100% delle 17.229 righe non-nulle. Una variabile con UNA categoria non + ha potenza: l'accoppiamento affollamento x rischio-di-liquidazione e' untestabile, non + debole. (Ed e' esattamente la parte che rendeva l'angolo NUOVO rispetto al filone funding.) + 2. IL FATTO NON REGGE LA PROPRIA SOGLIA DI POTENZA, dichiarata prima di guardare: l'unico + effetto e' il decile piu' basso di oi_delta (-0.40% a 24h) e la MDE a 24h e' 1.06%. + La pendenza lineare e' nulla (|t|<1.3 in 4 specificazioni su 4); il |ritorno| futuro e' + quasi piatto fra i decili (0.59-0.75% a 4h, solo il decile 0 leggermente sopra) -> la + 'cascata' non c'e'; e il segno e' ASSENTE nel primo dei tre mesi (maggio y24=+0.02%, + giugno -0.64%, luglio -0.74%): 2 mesi su 3 non sono un plateau, sono due mesi. + 3. IL SEGNALE E' UN MASSIMO DI 18 CELLE SU 114 GIORNI: Sharpe 3.6 con SE(Sharpe)=2.0, + deflated-Sharpe 0.10 (18 celle) / 0.004 (118 trial) contro un massimo atteso PER CASO di + Sharpe 5.4; implausible_sharpe=True; e il P&L e' per il 71% di giugno e per il 60% + concentrato in 3 giorni su 114. marginal_vs_tp01 gira e dice NEUTRAL, ma va dichiarato + NON INFORMATIVO SULL'OOS: il candidato sta interamente dopo HOLDOUT(2025-01-01), quindi + full e hold-out sono la stessa finestra (Sharpe identico) e il multi-cut non esiste. + 4. ED E' COMUNQUE MORTO ALLA NASCITA: la colonna su cui poggia non e' ricostruibile con i dati + che raccogliamo (l'OI della catena e' quello delle OPZIONI, rango ~0.05 con la colonna) e + la sorgente e' stata dismessa il 2026-07-30. + Sottoprodotto con potenza vera (53.430 ore, 3 anni): il funding HL non predice ne' direzione + ne' volatilita' -> il filone funding, gia' chiuso su 3 lati come livello di carry, si puo' + dichiarare chiuso anche come PROXY DI AFFOLLAMENTO.""") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_growth_policy.py b/scripts/research/r0822_growth_policy.py new file mode 100644 index 0000000..edb3e91 --- /dev/null +++ b/scripts/research/r0822_growth_policy.py @@ -0,0 +1,564 @@ +#!/usr/bin/env python +"""r0822_growth_policy.py β€” LA POLITICA DI LEVA, giudicata sul TEMPO invece che sullo Sharpe. + +LA DOMANDA. `target_vol=20%` per TP01 e' canonico dal 2026-06-19 e non e' mai stato messo in +discussione **rispetto a un obiettivo di crescita**. E' stato scelto (e ri-confermato: peso 75/25 +tre volte, TP01xDVOL, tranching) sempre con una lente di **Sharpe** o di **drawdown**. Ma +l'operatore non massimizza lo Sharpe: vuole ~$258k netti il prima possibile. Sono due problemi +diversi β€” lo Sharpe e' invariante alla leva, il TEMPO no. + +Quindi: **quale leva k minimizza il tempo mediano al bersaglio, a parita' di rischio accettato?** +E cambia se il bersaglio e' $50k invece di $258k? + +COSA C'E' QUI + (0) Replica di controllo: il book live, il fattore d'ancora Γ—0.89 misurato, e i muri pubblicati. + (1) LA CURVA DI CRESCITA g(k) β€” dove sta il massimo (Kelly empirico) e da che parte sta il libro. + (2) SWEEP PRINCIPALE β€” per ogni k: tempo mediano al bersaglio (CONDIZIONATO, stampato accanto + alla sua probabilita'), P(entro 10/15/20a), P(rovina), P(DD>30%). Versamenti €0 e €500/mese, + bersagli $50k e il muro NETTO. Tutto **al netto del fisco d'accumulo** (33% + carry 4a + 0.2%). + (3) FRONTIERA ISO-ROVINA β€” la domanda del brief nella sua forma esatta. + (4) IL GATE CHE CONTA β€” il massimo di (1) e' calcolato su un campione che non contiene un solo + giorno peggiore di -3.9%. Stress esplicito: giorno di crash fuori campione + liquidazione + Deribit. Dove va il massimo quando il rischio che il dataset NON contiene entra nel conto. + (5) ESEGUIBILITA' β€” `config/live.json` oggi tiene il nozionale lordo <= 1.00x l'equity, e il + knob `target_vol` di TP01 NON scala linearmente (leverage cap 2x). Quanto di k e' teorico. + +ONESTA' (regole del progetto applicate qui): + Β· un non-arrivo e' **+infinito**, mai -1 (l'errore del 07/08: con -1 la mediana MIGLIORA quanto + peggio va la colonna); + Β· una mediana condizionata si stampa **sempre** accanto alla sua probabilita'; + Β· un Monte Carlo ha una **risoluzione**: la riga di testa e' rifatta con un secondo seme e la + banda e' dichiarata; + Β· la rovina non e' l'azzeramento aritmetico (con ritorni moltiplicativi il capitale non tocca + zero finche' k < 1/|peggior giorno| = 25.4): e' definita e dichiarata sotto. + + nice -n 19 timeout 900 uv run python scripts/research/r0822_growth_policy.py +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0725_capcurve as CC # noqa: E402 macchineria validata (book_series, _boot_paths, survival) +import r0726_deposits as DP # noqa: E402 deluck_returns() con il Γ—0.89 MISURATO +import r0727_tasse as TX # noqa: E402 CARRY_ANNI, PATRIMONIALE +from r0807_piano_netto import passo_fiscale # noqa: E402 il passo fiscale gia' validato + +# ------------------------------------------------------------------ parametri dichiarati +KS = (0.50, 0.75, 1.00, 1.25, 1.50, 2.00, 2.50, 3.50, 5.00, 7.00, + 10.00, 14.00, 18.00, 22.00) # 14 celle: la griglia stretta (<=8x) NON conteneva il + # massimo -> allargata finche' il massimo e' INTERNO +KS_STRESS = (1.00, 1.50, 2.00, 3.50, 5.00, 10.00, 18.00) # sottoinsieme per la (4) +DEPOSITI = (0, 500) # €/mese: nessuno, e il piano dichiarato +BERSAGLI = {"$50k": 50_000.0, "muro netto": 258_338.0} # r0807_piano_netto (fisco coerente) +ANNI = 25 # orizzonte di simulazione: 25 come r0726/r0807 (20 troncherebbe + # la coda della mediana condizionata). Le P si leggono a 10/15/20a. +N_PATHS = 2_500 +BLOCK = 20 +CHUNK = 500 # RAM: la VPS ha ~3.5 GB liberi e ci girano altri agenti +SEED = 20260822 +SEED_BIS = 8221 # secondo seme, solo per dichiarare la risoluzione MC +START = 600.0 +ALIQUOTA = CC.TAX_RATE # 0.33 +PATRIM = TX.PATRIMONIALE # 0.002 +RUIN_MULT = 0.20 # rovina = il moltiplicatore del PATH perde l'80% +DD_SOGLIA = 0.30 +WIPE = 0.90 # perdita giornaliera >= 90% dell'equity = liquidazione (margine+fee) +MURO_PUBBL_LORDO = 272_061.0 # r0726_capwall_refresh (convenzione pubblicata) + + +def sezione(t: str) -> None: + print("\n" + "=" * 104) + print(f" {t}") + print("=" * 104) + + +# =========================================================================== +# il motore: una passata, tutte le leve insieme (confronto APPAIATO sugli stessi path) +# =========================================================================== +def simula(r: np.ndarray, ks: tuple[float, ...], *, anni: int, dep_eur: float, + bersagli: dict[str, float], aliquota: float, patrimoniale: float, + n_paths: int = N_PATHS, seed: int = SEED, start: float = START, + crash_pct: float = 0.0, crash_p_anno: float = 0.0) -> dict: + """Accumulo a leva `k` per ogni k, sugli STESSI path (differenze appaiate). + + Stato per (path, k). Traccia in parallelo un GEMELLO SENZA VERSAMENTI: il drawdown e la + rovina si misurano su quello, cosi' sono confrontabili fra colonne con versamenti diversi + (un bonifico mensile maschera un drawdown senza ridurre il capitale distrutto). + + `crash_pct`/`crash_p_anno`: giorno di coda FUORI CAMPIONE (sezione 4). Il block bootstrap non + puo' generare un giorno peggiore del peggiore osservato: se lo si vuole, va messo a mano. + + Liquidazione: se la perdita del giorno a leva k supera `WIPE` dell'equity, il conto e' chiuso + dall'exchange -> capitale a 0 (i versamenti successivi ripartono da li'). + """ + kv = np.asarray(ks, float)[None, :] + nk = len(ks) + dep = dep_eur * CC.EURUSD + n_days = int(anni * 365) + rng = np.random.default_rng(seed) + nomi = list(bersagli) + soglie = np.array([bersagli[n] for n in nomi], float) + + hit = {n: [] for n in nomi} + fin, mult_fin, ruin, ddmax, versato_tot = [], [], [], [], 0.0 + + for c0 in range(0, n_paths, CHUNK): + m = min(CHUNK, n_paths - c0) + paths = CC._boot_paths(r, m, n_days, BLOCK, rng) + if crash_pct > 0 and crash_p_anno > 0: + # un giorno di crash con probabilita' `crash_p_anno`/anno, uniforme nel tempo + p_giorno = crash_p_anno / 365.0 + mask = rng.random((m, n_days)) < p_giorno + paths = np.where(mask, -abs(crash_pct), paths) + + cap = np.full((m, nk), start, float) + mult = np.ones((m, nk), float) # gemello senza versamenti + hwm = np.ones((m, nk), float) + dd = np.zeros((m, nk), float) + rovinato = np.zeros((m, nk), bool) + colpito = {n: np.full((m, nk), np.inf) for n in nomi} + carry = np.zeros((m * nk, TX.CARRY_ANNI)) + anno_start = cap.copy() + flusso = np.zeros((m, nk)) + versato = start + + for t in range(n_days): + rt = paths[:, t][:, None] * kv # (m, nk) + liq = rt <= -WIPE + g = np.where(liq, -1.0, rt) + cap *= (1.0 + g) + cap = np.maximum(cap, 0.0) + mult *= (1.0 + g) + mult = np.maximum(mult, 0.0) + hwm = np.maximum(hwm, mult) + dd = np.maximum(dd, 1.0 - mult / hwm) + rovinato |= mult <= RUIN_MULT + if dep > 0 and t % 30 == 0 and t > 0: + cap += dep + flusso += dep + versato += dep + for j, n in enumerate(nomi): + nuovi = np.isinf(colpito[n]) & (cap >= soglie[j]) + colpito[n][nuovi] = t + if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0): + passo_fiscale(cap.ravel(), anno_start.ravel(), flusso.ravel(), + carry, aliquota, patrimoniale) + anno_start = cap.copy() + flusso[:] = 0.0 + + for n in nomi: + hit[n].append(colpito[n]) + fin.append(cap) + mult_fin.append(mult) + ruin.append(rovinato) + ddmax.append(dd) + versato_tot = versato + + out = dict(versato=versato_tot, anni=anni, n_paths=n_paths, ks=ks) + out["cap"] = np.concatenate(fin, axis=0) + out["mult"] = np.concatenate(mult_fin, axis=0) + out["ruin"] = np.concatenate(ruin, axis=0) + out["dd"] = np.concatenate(ddmax, axis=0) + out["hit"] = {n: np.concatenate(hit[n], axis=0) for n in nomi} + return out + + +def anni_cond(hit_col: np.ndarray) -> tuple[float, float, float, float]: + """(anni mediani CONDIZIONATI all'arrivo, P<=10a, P<=15a, P<=20a). Non-arrivo = +inf.""" + arr = hit_col[np.isfinite(hit_col)] + med = float(np.median(arr)) / 365.0 if len(arr) >= 50 else float("nan") + p = lambda y: float((hit_col <= y * 365).mean()) # noqa: E731 + return med, p(10), p(15), p(20) + + +def fmt_anni(a: float) -> str: + return f"{a:.1f}a" if np.isfinite(a) else "mai" + + +# =========================================================================== +def main() -> None: + print("=" * 104) + print(" r0822 β€” GROWTH POLICY: quale leva minimizza il TEMPO al bersaglio, a pari rovina accettata") + print("=" * 104) + + # ------------------------------------------------------------------ (0) + sezione("(0) IL DATO β€” book LIVE (TP01 75 / SKH01 25) e replica di controllo") + base = CC.book_series("hourly") + r = DP.deluck_returns() + print(f"\n serie: {len(base)} giorni, {base.index.min().date()} -> {base.index.max().date()}") + print(f" lente 'hourly' = SKH01 sul PATH LIVE (fill al close 5m orario), non il backtest.") + print(f" canonico : drift {base.values.mean()*365:>7.2%} vol {base.values.std()*365**0.5:>6.2%}" + f" Sharpe {base.values.mean()/base.values.std()*365**0.5:>5.2f}") + print(f" de-luckato Γ—{DP.DELUCK}: drift {r.mean()*365:>7.2%} vol {r.std()*365**0.5:>6.2%}" + f" Sharpe {r.mean()/r.std()*365**0.5:>5.2f} <-- questa e' la serie usata ovunque sotto") + print(f" peggior giorno {r.min():+.2%} Β· miglior giorno {r.max():+.2%}") + print(f"\n ⚠️ Conseguenza aritmetica da tenere a mente per tutto il resto: con ritorni") + print(f" moltiplicativi il capitale NON tocca zero finche' k < 1/{abs(r.min()):.4f} = " + f"{1/abs(r.min()):.1f}x.") + print(f" Percio' 'rovina' qui e' DEFINITA: il moltiplicatore del path (gemello SENZA") + print(f" versamenti) scende sotto {RUIN_MULT:.0%}, cioe' 4 euro su 5 distrutti. E la") + print(f" liquidazione dell'exchange e' modellata a parte: perdita giornaliera >= {WIPE:.0%}.") + print(f"\n muri pubblicati riusati come bersagli:") + print(f" Β· ${MURO_PUBBL_LORDO:,.0f} (r0726_capwall_refresh, convenzione LORDA)") + print(f" Β· ${BERSAGLI['muro netto']:,.0f} (r0807_piano_netto, fisco COERENTE) <-- usato qui") + print(f" Β· $50.000 (il bersaglio 'corto' chiesto dal brief: ~€19/g di rendita perpetua)") + + print("\n CONTROLLO DI REPLICA β€” questo motore contro la tabella pubblicata il 07/08") + print(" (`r0807_piano_netto` Β§2: stesso fisco, stesso bersaglio, stessa convenzione di") + print(" mediana condizionata). I path sono diversi: si chiede accordo entro il rumore MC,") + print(" non l'identita'. Se questa riga non torna, nessun numero nuovo sarebbe") + print(" distinguibile da un bug.") + print(f"\n {'EUR/mese':>9}{'anni (pubbl.)':>16}{'P<=20a (pubbl.)':>18}{'anni (qui)':>13}{'P<=20a (qui)':>15}") + for dep, a_pub, p_pub in ((250, 19.8, 0.52), (500, 14.7, 0.99)): + sr = simula(r, (1.00,), anni=25, dep_eur=dep, bersagli={"muro": BERSAGLI["muro netto"]}, + aliquota=ALIQUOTA, patrimoniale=PATRIM, n_paths=3_000, seed=725) + a, _, _, p20 = anni_cond(sr["hit"]["muro"][:, 0]) + print(f" {dep:>9}{a_pub:>15.1f}a{p_pub:>18.0%}{fmt_anni(a):>13}{p20:>15.0%}") + + # ------------------------------------------------------------------ (1) + sezione("(1) LA CURVA DI CRESCITA g(k) β€” dove sta il massimo, e DI QUANTO il libro ne dista") + print(f"\n Crescita geometrica MEDIANA annua, orizzonte {ANNI} anni, ZERO versamenti,") + print(f" {N_PATHS} path, fisco d'accumulo DENTRO. Un vol-target e' esattamente questo:") + print(" uno scalare sui pesi. Griglia allargata a 22x perche' a 8x il massimo non era") + print(" ancora interno β€” una griglia che si ferma prima del massimo non lo misura, lo tronca.") + s0 = simula(r, KS, anni=ANNI, dep_eur=0, bersagli=BERSAGLI, + aliquota=ALIQUOTA, patrimoniale=PATRIM, seed=SEED) + s0b = simula(r, KS, anni=ANNI, dep_eur=0, bersagli=BERSAGLI, + aliquota=ALIQUOTA, patrimoniale=PATRIM, seed=SEED_BIS) + mu, sig = float(r.mean()), float(r.std()) + kelly_gauss = mu / sig ** 2 + + def g_curve(sim: dict) -> list[float]: + return [float(np.median(sim["mult"][:, j])) ** (1 / ANNI) - 1 for j in range(sim["mult"].shape[1])] + + g_med, g_medb = g_curve(s0), g_curve(s0b) + jstar = int(np.argmax(g_med)) + kbest, gbest = KS[jstar], g_med[jstar] + j1 = KS.index(1.00) + + lg = lambda g: float(np.log1p(g)) # noqa: E731 + print(f"\n {'k':>6}{'g mediana/anno':>17}{'(2Β° seme)':>12}{'ln(1+g)/ln(1+g*)':>19}" + f"{'maxDD mediano':>16}{'P(DD>30%)':>11}{'P(rovina)':>11}{'cap mediano a 25a':>20}") + for j, k in enumerate(KS): + marca = " <-- LIBRO" if j == j1 else (" <-- k*" if j == jstar else "") + print(f" {k:>6.2f}{g_med[j]:>17.2%}{g_medb[j]:>12.2%}{lg(g_med[j])/lg(gbest):>19.2f}" + f"{np.median(s0['dd'][:, j]):>16.1%}{(s0['dd'][:, j] > DD_SOGLIA).mean():>11.1%}" + f"{s0['ruin'][:, j].mean():>11.1%}{np.median(s0['cap'][:, j]):>20,.0f}{marca}") + esec = [j for j, k in enumerate(KS) if k <= 2.50] + ris = max(abs(a - b) for a, b in zip(g_med, g_medb)) + ris_e = max(abs(g_med[j] - g_medb[j]) for j in esec) + print(f"\n RISOLUZIONE MONTE CARLO (due semi indipendenti a {N_PATHS} path): scarto massimo") + print(f" {ris:.2%}/anno sull'intera griglia, ma solo {ris_e:.2%}/anno nella regione") + print(f" k<={KS[esec[-1]]:.2f} β€” il rumore vive tutto nelle celle a leva alta, dove la mediana e'") + print(" presa su una distribuzione enormemente dispersa. Conseguenza da tenere: la") + print(" POSIZIONE del massimo non e' risolvibile a questa taglia (vedi sotto), i confronti") + print(" nella regione eseguibile si'. Ogni confronto e' APPAIATO (stessi path per ogni k).") + + print(f"\n RISPOSTA ALLA DOMANDA 'da che parte, e di quanto':") + print(f" massimo EMPIRICO della crescita k* = {kbest:.2f}x (g = {gbest:.1%}/anno)") + print(f" massimo GAUSSIANO (Kelly, mu/sigmaΒ²) k = {kelly_gauss:.1f}x") + print(f" il LIBRO gira a k = 1.00x (g = {g_med[j1]:.1%}/anno)") + print(f" -> il libro sta a SINISTRA, a {1.0/kbest:.0%} di Kelly empirico, e raccoglie il") + print(f" {lg(g_med[j1])/lg(gbest):.0%} della crescita massima (in crescita LOGARITMICA, che e' la") + print(" grandezza che Kelly massimizza). NON e' un plateau: e' il ramo ripido.") + print(f"\n ⚠️ La POSIZIONE del massimo non e' risolta a questa taglia: la sezione (4) rifa'") + print(" la stessa curva con 1500 path e il massimo cade a 10x invece di 14x. La regione") + print(" 8-14x e' piatta entro il rumore -> si cita 'k* ~ 10-14x', non un numero.") + print(" Cio' che E' risolto, e con margine enorme, e' che 1.00x non ci sta vicino.") + print(f"\n ⚠️ PREVISIONE MIA, REFUTATA IN SESSIONE. Mi aspettavo che il massimo empirico") + print(f" cadesse molto a SINISTRA di quello gaussiano ({kelly_gauss:.1f}x), perche' il bootstrap a") + print(" blocchi conserva code e clustering che la formula ignora. Non succede: i due") + print(" coincidono entro il rumore. Il motivo e' informativo e prepara la sezione (4) β€”") + print(" la distribuzione delle CHIUSURE giornaliere di questo libro non ha code abbastanza") + print(" spesse da spostare Kelly. Il rischio che conta a leva non e' in quella") + print(" distribuzione, quindi non lo si trovera' ricampionandola meglio.") + print(f"\n ⚠️ Questo e' il risultato della lente, non una raccomandazione. Tre cose lo") + print(" ridimensionano, e sono misurate sotto: (a) k* dipende dal DRIFT, che e' stimato") + print(f" su {len(r)/365:.1f} anni (sez. 1-bis); (b) la coda che lo sostiene NON e' nel") + print(" campione (sez. 4); (c) meta' della griglia non e' eseguibile (sez. 5).") + print(f" ⚠️ E c'e' un controllo di plausibilita' che il modello fallisce da solo: a k={KS[jstar]:g}x") + print(f" il capitale mediano a 25 anni partendo da $600 e' ${np.median(s0['cap'][:, jstar]):,.0f}.") + print(" Un modello che promette piu' della capitalizzazione del mercato in cui trada ha") + print(" lasciato il dominio in cui vuol dire qualcosa: assume che il ritorno sia") + print(" indipendente dalla SIZE, il che e' falso ben prima di quella cifra.") + + # ------------------------------------------------------------------ (1-bis) + sezione("(1-bis) QUANTO E' SOLIDO k* β€” Kelly e' ipersensibile al drift, e il drift e' una STIMA") + se_drift = float(r.std() * 365 ** 0.5) / (len(r) / 365) ** 0.5 + print(f"\n drift stimato {mu*365:.2%}/anno su {len(r)/365:.1f} anni con vol {r.std()*365**0.5:.2%}") + print(f" -> errore standard della MEDIA = vol/sqrt(T) = {se_drift:.2%}/anno.") + print(" Kelly va come mu/sigmaΒ²: e' LINEARE nell'errore del drift, quindi l'incertezza") + print(" sul numeratore si trasferisce intera su k*. Stessa macchineria di `deluck_returns`") + print(" (si sottrae una costante al drift, la vol resta quella misurata).") + print(f"\n {'ipotesi sul drift':>34}{'drift/anno':>13}{'k* gaussiano':>15}{'k* empirico':>14}" + f"{'g(1x)':>9}{'g(k*)':>9}") + scen = [("punto (de-luckato Γ—0.89)", 0.0), + ("-1 errore standard", se_drift), + ("-2 errori standard", 2 * se_drift), + ("drift dimezzato (lente 07/08)", mu * 365 / 2), + ("drift a zero", mu * 365)] + for nome, taglio in scen: + rs = r - taglio / 365.0 + mus = float(rs.mean()) + kg = mus / sig ** 2 + ss = simula(rs, KS, anni=ANNI, dep_eur=0, bersagli=BERSAGLI, aliquota=ALIQUOTA, + patrimoniale=PATRIM, seed=SEED, n_paths=1_500) + gs = g_curve(ss) + js = int(np.argmax(gs)) + print(f" {nome:>34}{mus*365:>13.2%}{kg:>15.1f}{KS[js]:>13.2f}x" + f"{gs[j1]:>9.1%}{gs[js]:>9.1%}") + print("\n Lettura: un errore di stima di UN solo errore standard sul drift β€” che a 7 anni di") + print(" storia e' l'ordine di grandezza normale β€” sposta k* di parecchie unita'. E' la") + print(" ragione classica per cui si opera a una FRAZIONE di Kelly: non prudenza morale, ma") + print(" il fatto che k* e' un numero stimato con una barra d'errore larga quanto se stesso.") + + # ------------------------------------------------------------------ (2) + sezione(f"(2) SWEEP PRINCIPALE β€” tempo al bersaglio, orizzonte {ANNI} anni, AL NETTO del fisco") + print(f"\n da ${START:.0f} Β· {N_PATHS} path Β· blocchi {BLOCK}g Β· fisco {ALIQUOTA:.0%} + carry " + f"{TX.CARRY_ANNI}a + patrimoniale {PATRIM:.1%}") + print(" 'anni' = mediana CONDIZIONATA all'arrivo (non-arrivo = +infinito, MAI -1): si legge") + print(" sempre insieme a P<=20a accanto. 'p90' = il decile lento, cioe' il rischio di TEMPO.") + print(" ⚠️ Convenzione dichiarata: il fisco e' dentro il CAPITALE (colonne anni/P), NON dentro") + print(" il gemello di rischio (colonne DD/rovina). Un drawdown e' un fatto di mercato: metterci") + print(" l'imposta dentro sommerebbe due cose diverse in un numero solo.") + risultati = {} + for dep in DEPOSITI: + s = s0 if dep == 0 else simula(r, KS, anni=ANNI, dep_eur=dep, bersagli=BERSAGLI, + aliquota=ALIQUOTA, patrimoniale=PATRIM, seed=SEED) + risultati[dep] = s + for nome in BERSAGLI: + print(f"\n --- versamento €{dep}/mese Β· bersaglio {nome} (${BERSAGLI[nome]:,.0f}) Β· " + f"versato in {ANNI}a ${s['versato']:,.0f} ---") + print(f" {'k':>6}{'anni (p50)':>13}{'anni (p90)':>13}{'P<=10a':>9}{'P<=15a':>9}" + f"{'P<=20a':>9}{'P(rovina)':>11}{'P(DD>30%)':>11}{'P(DD>50%)':>11}") + for j, k in enumerate(KS): + h = s["hit"][nome][:, j] + a, p10, p15, p20 = anni_cond(h) + arr = h[np.isfinite(h)] + a90 = float(np.percentile(arr, 90)) / 365.0 if len(arr) >= 50 else float("nan") + print(f" {k:>6.2f}{fmt_anni(a):>13}{fmt_anni(a90):>13}{p10:>9.1%}{p15:>9.1%}" + f"{p20:>9.1%}{s['ruin'][:, j].mean():>11.1%}" + f"{(s['dd'][:, j] > DD_SOGLIA).mean():>11.1%}" + f"{(s['dd'][:, j] > 0.50).mean():>11.1%}") + + # ------------------------------------------------------------------ (3) + sezione("(3) FRONTIERA ISO-ROVINA β€” la domanda del brief nella sua forma esatta") + print("\n 'a parita' di probabilita' di rovina accettata': per ogni budget di rischio, la") + print(" leva PIU' ALTA che lo rispetta, e il tempo che quel budget compra. Il rischio si") + print(" misura sul gemello SENZA versamenti (deposit-independent: un bonifico mensile") + print(" maschera un drawdown senza ridurre il capitale distrutto), ed e' la stessa") + print(" grandezza su cui passa la regola di DD per-conto del canale funded.") + for dep in DEPOSITI: + s = risultati[dep] + print(f"\n --- versamento €{dep}/mese ---") + print(f" {'budget':>8}{'metrica':>13}{'k max':>9}" + + "".join(f"{'anni ' + n:>22}" for n in BERSAGLI)) + for metrica, col in (("P(DD>30%)", lambda j: (s["dd"][:, j] > DD_SOGLIA).mean()), + ("P(rovina)", lambda j: s["ruin"][:, j].mean())): + for budget in (0.01, 0.05, 0.10, 0.25, 0.50): + amm = [j for j in range(len(KS)) if col(j) <= budget] + if not amm: + print(f" {budget:>7.0%}{metrica:>13}{'nessuna':>9}") + continue + j = max(amm) + celle = [] + for n in BERSAGLI: + a, _, _, p20 = anni_cond(s["hit"][n][:, j]) + celle.append(f"{fmt_anni(a)} (P20 {p20:.0%})") + print(f" {budget:>7.0%}{metrica:>13}{KS[j]:>8.2f}x" + "".join(f"{c:>22}" for c in celle)) + print("\n ⚠️ Le due metriche danno risposte MOLTO diverse, e la ragione conta: P(rovina)") + print(" (perdere l'80% del moltiplicatore) e' quasi zero fino a leve altissime perche'") + print(" il libro non ha mai perso l'80% in nessuna finestra ricampionabile; P(DD>30%)") + print(" morde subito. Chi sceglie la leva sul solo azzeramento sceglie sulla metrica") + print(" che il dataset non puo' informare β€” vedi (4).") + + # ------------------------------------------------------------------ (4) + sezione("(4) IL GATE CHE CONTA β€” k* e' calcolato su un campione senza un solo giorno di crash") + print(f"\n Il block bootstrap ricampiona i giorni VISSUTI: non puo' produrre un giorno") + print(f" peggiore di {r.min():.2%}. Il libro ha attraversato il 2022 (LUNA/FTX) restando") + print(" difensivo β€” quindi per k=1 la coda del campione e' onesta. Ma a leva il rischio") + print(" cambia NATURA: entra la liquidazione, che su una chiusura giornaliera non si vede") + print(" (lezione della lente wick accoppiata, 25/07: 'close-only non e' conservativa, e'") + print(" CIECA'). Il rischio fuori campione non si stima dal campione: si mette a mano e si") + print(" dichiara. Ampiezze scelte a priori sull'unico precedente misurato nel progetto:") + print(" il gap-through-stop di SKH01 (sl 2% modellato -> -11/-23% realizzato).") + print(f"\n liquidazione se la perdita a leva supera {WIPE:.0%} dell'equity in un giorno.") + print(f"\n {'scenario':>22}" + "".join(f"{f'k={k:g}':>10}" for k in KS_STRESS) + f"{' k*':>8}") + s0s = simula(r, KS_STRESS, anni=ANNI, dep_eur=0, bersagli=BERSAGLI, aliquota=ALIQUOTA, + patrimoniale=PATRIM, seed=SEED, n_paths=1_500) + g0s = [float(np.median(s0s["mult"][:, j])) ** (1 / ANNI) - 1 for j in range(len(KS_STRESS))] + print(f" {'nessuno stress':>22}" + "".join(f"{g:>10.1%}" for g in g0s) + + f"{f'{KS_STRESS[int(np.argmax(g0s))]:g}x':>8}") + for cpct in (0.10, 0.15, 0.20): + for panno in (0.20, 1.00): + ss = simula(r, KS_STRESS, anni=ANNI, dep_eur=0, bersagli=BERSAGLI, aliquota=ALIQUOTA, + patrimoniale=PATRIM, seed=SEED, crash_pct=cpct, crash_p_anno=panno, + n_paths=1_500) + gg = [float(np.median(ss["mult"][:, j])) ** (1 / ANNI) - 1 for j in range(len(KS_STRESS))] + print(f" {f'-{cpct:.0%} ogni {1/panno:.0f}a':>22}" + + "".join(f"{g:>10.1%}" for g in gg) + + f"{f'{KS_STRESS[int(np.argmax(gg))]:g}x':>8}") + print("\n Lettura: basta UN giorno fuori campione all'anno perche' il massimo collassi da") + print(" due cifre a poche unita', e il ramo destro della curva e' il primo ad andare a") + print(" -100%. La crescita a leva alta e' comprata vendendo la coda che il dataset non") + print(" contiene: e' la stessa firma che il gate `implausible_sharpe` cerca (rischio FUORI") + print(" dal dataset), applicata alla politica invece che alla strategia.") + + # ------------------------------------------------------------------ (5) + sezione("(5) ESEGUIBILITA' β€” quanto di questa griglia esiste davvero sul conto") + import json + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac = cfg["max_notional_per_asset_frac"] + lordo_max = frac * 2 # 2 asset (BTC, ETH) + print(f"\n (a) IL CAP DI CONFIG. max_notional_per_asset_frac = {frac} su 2 asset") + print(f" -> nozionale LORDO massimo = {lordo_max:.2f}x l'equity. **k > {lordo_max:.2f}x NON e'") + print(" eseguibile senza toccare `config/live.json`**, e il 26/07 il progetto ha legato") + print(" una conclusione a quel parametro con un test apposta") + print(" (`test_leva_massima_da_config_resta_sotto_o_uguale_a_1x`): alzare il cap ROMPE") + print(" quel test, che e' esattamente il suo scopo. Quindi 11 delle 14 celle di questa") + print(" griglia sono TEORICHE per costruzione.") + print(f" min_order ${cfg['min_order_usd']} Β· disaster-SL {cfg['disaster_sl_pct']:.0%} Β· " + f"cap $ {cfg['max_notional_per_asset_usd']:,} (binding: min(quello, equityΓ—{frac}))") + + print("\n (b) IL KNOB `target_vol` DI TP01 β€” ipotesi mia: il leverage cap 2x lo strozza.") + from src.data.downloader import load_data # noqa: E402 + from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, resample_1d, simple_returns) + import pandas as pd # noqa: E402 + dfs = {a: resample_1d(load_data(a, "1h")) for a in ("BTC", "ETH")} + def tp01_knob(tv: float, lev: float) -> tuple[float, float, float]: + """(vol realizzata annua, Sharpe, quota di barre incollate al leverage cap) del solo TP01 + 50/50 BTC-ETH, netto fee, con `target_vol=tv` e `leverage=lev`.""" + serie, atcap = {}, [] + for a, df in dfs.items(): + tp = TrendPortfolio(**{**CANONICAL, "target_vol": tv, "leverage": lev}) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) + ret = simple_returns(df["close"].values.astype(float)) + pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1] + net = pos * ret - 0.0005 * np.abs(np.diff(pos, prepend=0.0)) + net[0] = 0.0 + serie[a] = pd.Series(net, index=pd.to_datetime(df["datetime"])) + atcap.append(float((np.abs(tgt) >= lev - 1e-9).mean())) + J = pd.concat(serie, axis=1, join="inner").fillna(0.0) # le due serie hanno lunghezze + x = (0.5 * J["BTC"] + 0.5 * J["ETH"]).values # diverse: si allineano sulle date + return (float(x.std() * 365 ** 0.5), float(x.mean() / x.std() * 365 ** 0.5), + float(np.mean(atcap))) + + rif = tp01_knob(0.20, 2.0)[0] # il canonico, calcolato PRIMA di usarlo come riferimento + print(f"\n {'target_vol':>11}{'lev cap':>9}{'x NOMINALE':>12}{'vol realizz.':>14}" + f"{'x OTTENUTO':>12}{'Sharpe':>9}{'quota barre al cap':>21}") + for lev in (2.0, 4.0): + for tv in (0.10, 0.20, 0.30, 0.40, 0.60): + v, sh, ac = tp01_knob(tv, lev) + print(f" {tv:>11.0%}{lev:>9.1f}{tv/0.20:>12.2f}{v:>14.2%}{v/rif:>12.2f}{sh:>9.2f}" + f"{ac:>21.1%}") + v20, sh20, _ = tp01_knob(0.20, 2.0) + print(f"\n ❌ IPOTESI REFUTATA, e la refutazione e' piu' utile dell'ipotesi. 'x OTTENUTO'") + print(" segue 'x NOMINALE' fino a target_vol 60% (3.00 nominale -> 2.98 ottenuto) e le") + print(" barre incollate al cap sono l'1.0%: **il leverage cap 2x NON e' binding**, quindi") + print(" il knob e' uno scalare lineare pulito. Lo Sharpe e' invariante a ogni cella") + print(f" (1.31 ovunque) β€” che e' esattamente il punto: la lente con cui `target_vol=20%`") + print(" fu scelto NON puo' distinguere queste righe, e infatti non le distingue.") + print(f"\n ⚠️ Sottoprodotto da registrare: a `target_vol=20%` la vol REALIZZATA di TP01 e'") + print(f" {v20:.2%}, non 20%. Il target vale sulla posizione quando c'e'; TP01 e' long-flat e") + print(" sta flat una parte del tempo, quindi l'etichetta sovrastima il rischio preso di") + print(" ~40%. Chi legge '20%' e lo confronta con un altro sistema sta confrontando un") + print(" parametro con una misura.") + print("\n Cio' che resta binding e' altro: (i) il cap di nozionale di config, punto (a);") + print(" (ii) SKH01 (25% del libro) NON e' vol-targeted affatto (misurato 26/07) β€” li'") + print(" l'unico modo di alzare la leva e' moltiplicare la size, cioe' margine e") + print(" liquidazione vere, non un parametro di sizing.") + + print("\n (c) COSA COSTA LA LEVA E NON E' NEL MODELLO SOPRA (tutti a sfavore di k alto):") + print(" Β· la fee scala LINEARMENTE con k (3,5 bps/lato oggi) e SKH01 e' ~4x piu'") + print(" fee-sensibile di TP01 (curva misurata il 26/07: -0.017 Sharpe/bps di book);") + print(" Β· il funding dei perp e il costo del margine oltre 1x non sono modellati;") + print(" Β· il min_order $5 su un conto da $635 e la granularita' che ne consegue;") + print(" Β· l'impatto: il modello assume ritorno indipendente dalla size (vedi sez. 1).") + print(" -> i numeri a k>1 sono un TETTO, non una stima.") + + # ------------------------------------------------------------------ verdetto + sezione("VERDETTO") + s500 = risultati[500] + jj = {k: KS.index(k) for k in (1.00, 1.50, 2.00)} + righe = [] + for nome in BERSAGLI: + for k, j in jj.items(): + a, _, _, p20 = anni_cond(s500["hit"][nome][:, j]) + righe.append((nome, k, a, p20, (s500["dd"][:, j] > DD_SOGLIA).mean())) + print(f"\n A €500/mese (il piano dichiarato), zona ESEGUIBILE e primo passo oltre:") + print(f" {'bersaglio':>14}{'k':>7}{'anni (p50)':>13}{'P<=20a':>9}{'P(DD>30%)':>11}") + for nome, k, a, p20, pdd in righe: + print(f" {nome:>14}{k:>6.2f}x{fmt_anni(a):>13}{p20:>9.0%}{pdd:>11.0%}") + print(f""" + GATE β€” cosa e' girato e cosa no (un gate non girato si dichiara, non si omette): + Β· `marginal_vs_tp01` NON APPLICABILE. Non c'e' un flusso di ritorni nuovo da giudicare + marginalmente: k e' uno SCALARE sul libro esistente, quindi la + correlazione col baseline e' 1.00 per costruzione. + Β· `deflated_sharpe` NON APPLICABILE, e vale la pena dire perche': lo Sharpe e' + INVARIANTE a k (misurato, sez. 5b: 1.31 a ogni cella del knob). + Un gate costruito sullo Sharpe da' lo stesso verdetto su tutta la + griglia β€” non e' che lo passi, e' che non vede la variabile. + Β· null del de-levering NON e' un gate da superare qui: e' L'OGGETTO della misura, percorso + in entrambi i versi (k<1 e k>1). E lo conferma: ogni 'meno DD' via + leva e' de-levering per definizione, ogni 'piu' crescita' e' leva. + Β· fortuna d'ancora GESTITA col fattore MISURATO Γ—{DP.DELUCK} sul drift (26/07), non con una + banda nuova: la leva non ha una propria ancora, moltiplica il drift. + Β· `implausible_sharpe` Non girato come funzione (darebbe lo stesso verdetto a ogni k, vedi + sopra); il controllo di plausibilita' e' stato fatto sull'OUTPUT ed + e' FALLITO dal modello a leva alta: capitale mediano ${np.median(s0['cap'][:, jstar]):,.0f} + da $600 in 25 anni. La firma 'rischio fuori dal dataset' c'e', ma + vive nella POLITICA, non nella strategia. + Β· causalita' Ereditata: la serie viene da `CC.book_series('hourly')`, macchineria + gia' validata; qui non si costruisce nessun segnale nuovo. + Β· risoluzione MC DICHIARATA: {ris_e:.2%}/anno nella regione eseguibile, {ris:.2%}/anno sulla + griglia intera. La posizione del massimo NON e' risolta. + + 1. IL LIBRO STA A SINISTRA DEL MASSIMO, E DI MOLTO: k* empirico ~10-14x (banda, non un + numero: a 1500 path cade a 10x, a 2500 a {kbest:.0f}x), il libro gira a 1x = {1.0/kbest:.0%} di Kelly + e raccoglie il {lg(g_med[j1])/lg(gbest):.0%} della crescita massima in log. Non e' un plateau + (come lo era il peso 75/25): e' il ramo ripido della curva. Sulla lente della CRESCITA + la scelta canonica `target_vol=20%` non e' ottimale β€” ed e' un fatto, perche' quella + scelta fu fatta su una lente di Sharpe, che alla leva e' INVARIANTE. + + 2. MA k* NON E' UN NUMERO SU CUI SI PUO' AGIRE, per tre ragioni misurate qui: + (a) e' lineare nell'errore del drift, stimato su {len(r)/365:.1f} anni (sez. 1-bis); + (b) e' comprato vendendo una coda che il dataset NON contiene β€” un giorno di crash + all'anno lo riporta a poche unita' (sez. 4); + (c) 11 celle su 14 non sono eseguibili: il cap di config tiene il nozionale lordo a + {lordo_max:.2f}x l'equity. (Il knob `target_vol` invece FUNZIONA β€” misurato, sez. 5b: il + leverage cap 2x non morde. L'ostacolo e' la config e SKH01, non il sizing di TP01.) + + 3. IL BERSAGLIO CAMBIA LA RISPOSTA, e nel verso opposto a quello che la domanda suggerisce. + Su $50k a €500/mese la leva quasi non serve: a k=1 ci si arriva in ~5 anni con P=100%, + perche' fino a quella taglia il capitale lo portano i BONIFICI, non il rendimento + (misura indipendente del 26/07: al traguardo l'80% viene dal rendimento, ma i primi + anni no). Sul muro netto la leva morde: e' proprio dove il rischio di rovina diventa + non-recuperabile, perche' zero e' assorbente e si ricomincia con lo stesso stipendio. + + 4. IL BUCO STRUTTURALE, che e' il risultato piu' trasferibile: **ogni gate di questo + progetto e' invariante alla leva**. `study_family_honest`, il deflated-Sharpe, + `marginal_vs_tp01`, `implausible_sharpe`, il null del de-levering, `weights_tilt_null` + β€” tutti giudicano forma della distribuzione, correlazioni o PESI RELATIVI, e nessuno + vede la SCALA. Per questo `target_vol=20%` e' sopravvissuto due mesi di scrutinio + senza mai essere esaminato: non c'era una lente che potesse bocciarlo. + + 5. COSA RESTA DI OPERATIVO: nulla da cambiare oggi. Il gradino ESEGUIBILE (k da 1.00x a + 1.25-1.50x, cioe' alzare il cap di config) esiste e non e' rumore MC, ma passa per una + modifica a un file di produzione su un conto con soldi veri, e nessun gate del progetto + lo autorizza: `weights_tilt_null` giudica i PESI, non la scala, e non esiste un gate + 'politica di leva'. Il candidato naturale sarebbe misurare la stessa curva sul canale + FUNDED, dove il vincolo binding e' gia' una regola di DD per-conto e il nozionale non + e' il proprio. + """) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_hl_exec.py b/scripts/research/r0822_hl_exec.py new file mode 100644 index 0000000..f31601a --- /dev/null +++ b/scripts/research/r0822_hl_exec.py @@ -0,0 +1,697 @@ +"""HL-EXEC (2026-08-22) β€” AUDIT DI FATTO: le regole VERE di Hyperliquid contro le soglie ASSUNTE. + +DOMANDA. Il progetto tiene due edge cross-sectional fuori dal libro **per taglia**: + XS01 "serve ~$20.000" (origine: diario 2026-06-19-hyperliquid-xsec, "rumore arrotondamento" β€” + una STIMA A OCCHIO, mai calcolata) + XSR01 "diventa reale a ~$5.000" + gate pre-registrato 2026-10-23 con soglia + "haircut di eseguibilita' a $5.000 <= 40%, altrimenti RITIRO" +Entrambe sono calcolate col pavimento **min_order $5**, che e' il minimo di **DERIBIT**. +XS01/XSR01 si eseguirebbero su **HYPERLIQUID**, che ha regole sue. Qui le regole si LEGGONO dal +venue invece di assumerle, e si dice quali conclusioni del progetto cambiano. + +NON manda ordini, non tocca il conto: sole letture PUBBLICHE (nessuna chiave, nessuna firma). + +GRADO DELLE FONTI (dichiarato per ogni numero, come chiede il brief): + [A] letto dall'API pubblica del venue api.hyperliquid.xyz/info (meta, metaAndAssetCtxs, + userFees, l2Book) + [B] derivato da [A] + verificato empiricamente sui book live + [C] documentazione ufficiale (hyperliquid.gitbook.io) + [D] assunto / ereditato dal progetto +La verifica MAINNET non e' un atto di fede: i mark price dell'API si incrociano con l'ultima +chiusura del feed CERTIFICATO su disco (data/raw/hl_*_1d.parquet). Il testnet non puo' superarlo. +(Il progetto ha gia' pagato il prezzo di un feed testnet creduto vero: e' la causa del reset v2.0.0.) + + nice -n 19 timeout 900 uv run python scripts/research/r0822_hl_exec.py + ... --no-net usa solo la cache su disco (nessuna chiamata di rete) + ... --snaps N quanti snapshot del book L2 (default 3, ~45s di distanza) +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) +sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research")) + +RAW = PROJECT_ROOT / "data" / "raw" +CACHE = Path(os.environ.get("HLEXEC_CACHE", "/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/hlexec")) +INFO_URL = "https://api.hyperliquid.xyz/info" # MAINNET (il testnet e' api.hyperliquid-testnet.xyz) + +# --- parametri sotto esame ---------------------------------------------------------------------- +MIN_ORDER_ASSUNTO = 5.0 # [D] pavimento DERIBIT usato da eval_weights_smallcap e da paper_xsr +MIN_ORDER_HL = 10.0 # [C] "Order must have minimum value of $10." (docs/error-responses) +FEE_LEG_MODELLO = 0.0005 # [D] 0.05%/gamba, config CONGELATA di XSR01 +XSR_SOGLIA_HAIRCUT = 0.40 # [D] soglia pre-registrata del gate 2026-10-23 +CAPITALI = (600.0, 1000.0, 1500.0, 2000.0, 3000.0, 4000.0, 5000.0, 7500.0, + 10000.0, 20000.0, 50000.0) +CAPITALI_XS = (100.0, 200.0, 300.0, 600.0, 1000.0, 2000.0, 3000.0, 5000.0, 10000.0) +TICKETS = (50.0, 100.0, 300.0) +ANN = np.sqrt(365.0) + + +# ================================================================================================== +# 0. RETE β€” letture pubbliche, con cache su disco (una corsa non deve ri-martellare il venue) +# ================================================================================================== +def _post(payload: dict, timeout: int = 30) -> object: + req = urllib.request.Request(INFO_URL, data=json.dumps(payload).encode(), + headers={"Content-Type": "application/json"}) + with urllib.request.urlopen(req, timeout=timeout) as r: + return json.loads(r.read().decode()) + + +def cached(name: str, payload: dict, no_net: bool, ttl_s: float = 6 * 3600) -> object: + CACHE.mkdir(parents=True, exist_ok=True) + f = CACHE / f"{name}.json" + if f.exists() and (no_net or (time.time() - f.stat().st_mtime) < ttl_s): + return json.loads(f.read_text()) + if no_net: + raise FileNotFoundError(f"cache assente per {name} e --no-net attivo") + d = _post(payload) + f.write_text(json.dumps(d)) + return d + + +def universo_certificato() -> list[str]: + """I 51 alt+BTC gia' CERTIFICATI su disco. Non si inventa un universo: si legge quello vero.""" + return sorted(p.stem.replace("hl_", "").replace("_1d", "").upper() + for p in RAW.glob("hl_*_1d.parquet")) + + +# ================================================================================================== +# 1. REGOLE DEL VENUE +# ================================================================================================== +def sig_figs_ok(px_str: str, sz_dec: int, max_dec: int = 6) -> bool: + """[C] regola tick perp: <=5 cifre significative E <= (MAX_DECIMALS - szDecimals) decimali; + i prezzi INTERI sono sempre ammessi. Qui serve per VERIFICARLA sui book veri -> [B].""" + s = px_str.lstrip("-") + dec = len(s.split(".")[1]) if "." in s else 0 + if dec == 0: + return True + digits = s.replace(".", "").lstrip("0") + return len(digits.rstrip("0")) <= 5 and dec <= (max_dec - sz_dec) + + +def leggi_regole(no_net: bool) -> tuple[pd.DataFrame, dict]: + meta_ctx = cached("metaAndAssetCtxs", {"type": "metaAndAssetCtxs"}, no_net) + fees = cached("userFees", {"type": "userFees", + "user": "0x0000000000000000000000000000000000000001"}, no_net) + uni, ctxs = meta_ctx[0]["universe"], meta_ctx[1] + rows = [] + for a, c in zip(uni, ctxs): + mark = float(c["markPx"]) if c.get("markPx") else np.nan + rows.append(dict(sym=a["name"], szDecimals=int(a["szDecimals"]), + maxLeverage=int(a["maxLeverage"]), + delisted=bool(a.get("isDelisted", False)), + markPx=mark, dayNtlVlm=float(c.get("dayNtlVlm") or np.nan), + openInterest=float(c.get("openInterest") or np.nan))) + df = pd.DataFrame(rows).set_index("sym") + df["lot_units"] = 10.0 ** (-df["szDecimals"]) # [A] passo di size in unita' + df["lot_usd"] = df["lot_units"] * df["markPx"] # [B] passo di size in dollari + df["px_decimals_max"] = (6 - df["szDecimals"]).clip(lower=0) # [C] + return df, fees["feeSchedule"] + + +def verifica_mainnet(df: pd.DataFrame, syms: list[str]) -> pd.DataFrame: + """Incrocia i mark dell'API con l'ULTIMA chiusura del feed certificato su disco. + Un feed testnet non puo' superare questo controllo: i suoi prezzi sono fantasia.""" + out = [] + for s in syms: + p = RAW / f"hl_{s.lower()}_1d.parquet" + if not p.exists() or s not in df.index: + continue + d = pd.read_parquet(p, columns=["timestamp", "close"]) + last_close = float(d["close"].iloc[-1]) + last_ts = pd.Timestamp(int(d["timestamp"].iloc[-1]), unit="ms", tz="UTC") + mk = float(df.loc[s, "markPx"]) + out.append(dict(sym=s, disco=last_close, api=mk, + dev_pct=100.0 * (mk - last_close) / last_close, + eta_h=(pd.Timestamp.now("UTC") - last_ts) / pd.Timedelta("1h"))) + return pd.DataFrame(out).set_index("sym") + + +# ================================================================================================== +# 2. BOOK L2 -> spread, profondita', costo di un ticket +# ================================================================================================== +def scarica_book(syms: list[str], snaps: int, no_net: bool, pause: float = 0.25, + gap_s: float = 45.0) -> dict[str, list[dict]]: + books: dict[str, list[dict]] = {s: [] for s in syms} + for k in range(snaps): + if k and not no_net: + time.sleep(gap_s) + for s in syms: + try: + b = cached(f"l2_{s}_{k}", {"type": "l2Book", "coin": s}, no_net) + except Exception as e: # un book mancante e' un dato, non un crash + print(f" [book] {s} snap{k}: {type(e).__name__}") + continue + books[s].append(b) + if not no_net: + time.sleep(pause) # pacing: il rate limit e' PER-IP e condiviso + return books + + +def costo_ticket(levels: list[dict], mid: float, notional: float, side: str) -> tuple[float, bool]: + """VWAP di un ordine marketable da `notional` dollari contro il book, in bps DA MID. + Ritorna (bps, book_esaurito). Per ticket piccoli converge al MEZZO SPREAD: e' giusto cosi', + a $14 non si 'cammina' il book, si paga il livello top.""" + resid, cost, filled = notional, 0.0, 0.0 + for lv in levels: + px, sz = float(lv["px"]), float(lv["sz"]) + cap = px * sz + take = min(resid, cap) + cost += take + filled += take / px + resid -= take + if resid <= 1e-9: + break + if filled <= 0: + return float("nan"), True + vwap = cost / filled + bps = (vwap - mid) / mid * 1e4 * (1.0 if side == "buy" else -1.0) + return bps, resid > 1e-6 + + +def misura_liquidita(books: dict[str, list[dict]], meta: pd.DataFrame) -> pd.DataFrame: + rows = [] + for s, snaps in books.items(): + if not snaps: + continue + rec: dict[str, list[float]] = {"spread": [], "d10": [], "viol": []} + for t in TICKETS: + rec[f"t{int(t)}"] = [] + for b in snaps: + bids, asks = b["levels"][0], b["levels"][1] + if not bids or not asks: + continue + bb, ba = float(bids[0]["px"]), float(asks[0]["px"]) + mid = 0.5 * (bb + ba) + rec["spread"].append((ba - bb) / mid * 1e4) + near = sum(float(l["px"]) * float(l["sz"]) for l in asks + if (float(l["px"]) - mid) / mid <= 0.0010) + near += sum(float(l["px"]) * float(l["sz"]) for l in bids + if (mid - float(l["px"])) / mid <= 0.0010) + rec["d10"].append(near) + sd = int(meta.loc[s, "szDecimals"]) if s in meta.index else 0 + rec["viol"].append(sum(0 if sig_figs_ok(l["px"], sd) else 1 for l in bids + asks)) + for t in TICKETS: + bb_, _ = costo_ticket(asks, mid, t, "buy") + sb_, _ = costo_ticket(bids, mid, t, "sell") + if np.isfinite(bb_) and np.isfinite(sb_): + rec[f"t{int(t)}"].append(0.5 * (bb_ + sb_)) + if not rec["spread"]: + continue + row = dict(sym=s, n_snap=len(rec["spread"]), + spread_bps=float(np.median(rec["spread"])), + half_spread_bps=float(np.median(rec["spread"])) / 2.0, + depth10bps_usd=float(np.median(rec["d10"])), + tick_viol=int(sum(rec["viol"]))) + for t in TICKETS: + v = rec[f"t{int(t)}"] + row[f"slip{int(t)}_bps"] = float(np.median(v)) if v else np.nan + rows.append(row) + return pd.DataFrame(rows).set_index("sym").sort_values("spread_bps") + + +def min_notional_osservato(books: dict[str, list[dict]]) -> dict: + """Controllo EMPIRICO del pavimento $10: i livelli con n==1 sono UN ordine solo. + Un ordine singolo sotto $10 non falsifica la regola (i fill parziali erodono un resto), + ma il PAVIMENTO della distribuzione dice dove il venue taglia.""" + vals = [] + for snaps in books.values(): + for b in snaps: + for side in b["levels"]: + for l in side: + if int(l.get("n", 0)) == 1: + vals.append(float(l["px"]) * float(l["sz"])) + v = np.array(vals, float) + if not len(v): + return {} + return dict(n=len(v), minimo=float(v.min()), p01=float(np.percentile(v, 1)), + p05=float(np.percentile(v, 5)), mediana=float(np.median(v)), + sotto10_pct=100.0 * float((v < 10.0).mean()), + sotto5_pct=100.0 * float((v < 5.0).mean())) + + +# ================================================================================================== +# 3. SIMULATORE DI LIBRO A N GAMBE (la contabilita' e' quella di paper_xsr._step, generalizzata) +# ================================================================================================== +def sim_libro(Weff: np.ndarray, R: np.ndarray, r_hedge: np.ndarray | None, cap0: float, + min_order: float | None, lot_usd: np.ndarray | None, px: np.ndarray | None, + fee_leg: float | np.ndarray) -> dict: + """Un passo per barra. Una gamba il cui |dw|*capitale sta sotto min_order NON si muove + (stessa convenzione di altlib.eval_weights_smallcap e di scripts/live/paper_xsr._step). + In piu': la size si arrotonda al LOTTO del venue -> se arrotonda a zero, non si esegue. + `fee_leg` puo' essere uno scalare o un vettore per-gamba (costo asset-specifico).""" + n, k = Weff.shape + w = np.zeros(k) + cap = cap0 + eq = np.empty(n) + nets = np.zeros(n) + n_fill = n_skip = n_lot0 = 0 + ntl_req = ntl_done = 0.0 + fee = np.broadcast_to(np.asarray(fee_leg, float).ravel(), (k,)) if np.ndim(fee_leg) else None + for i in range(n): + ret = float(np.dot(w, R[i])) + if r_hedge is not None: + ret -= float(w.sum()) * r_hedge[i] + w_t = Weff[i] + if min_order is None: + w_new = w_t.copy() + else: + move = np.abs(w_t - w) * cap >= min_order + if lot_usd is not None and px is not None: + # arrotondamento al lotto: la size eseguita e' un multiplo di lot_units + dn = np.abs(w_t - w) * cap + lots = np.floor(dn / np.maximum(lot_usd, 1e-12)) + zero = move & (lots < 1) + n_lot0 += int(zero.sum()) + move = move & (lots >= 1) + need = np.abs(w_t - w) > 1e-12 # una gamba gia' a posto NON e' un ordine saltato + ntl_req += float((np.abs(w_t - w) * cap).sum()) + ntl_done += float((np.abs(w_t - w) * cap)[move].sum()) + w_new = np.where(move, w_t, w) + n_fill += int((move & need).sum()) + n_skip += int(((~move) & need).sum()) + d = np.abs(w_new - w) + c = float((d * fee).sum()) if fee is not None else float(fee_leg) * float(d.sum()) + if r_hedge is not None: # la gamba di copertura paga anch'essa + dh = abs(float(w_new.sum()) - float(w.sum())) + c += (float(fee.mean()) if fee is not None else float(fee_leg)) * dh + net = ret - c + nets[i] = net + cap *= (1.0 + max(net, -0.99)) + eq[i] = cap + w = w_new + tot = n_fill + n_skip + return dict(net=nets, eq=eq, n_fill=n_fill, n_skip=n_skip, n_lot0=n_lot0, + pct_eseguite=(100.0 * n_fill / tot) if tot else np.nan, + pct_nozionale=(100.0 * ntl_done / ntl_req) if ntl_req > 0 else np.nan) + + +def metriche(net: np.ndarray, idx: pd.DatetimeIndex) -> dict: + x = np.asarray(net, float) + sd = x.std(ddof=1) + sh = float(x.mean() / sd * ANN) if sd > 0 else 0.0 + eq = np.cumprod(1.0 + np.clip(x, -0.99, None)) + dd = float((1.0 - eq / np.maximum.accumulate(eq)).max()) + yrs = max((idx[-1] - idx[0]) / pd.Timedelta("365.25D"), 1e-9) + cagr = float(eq[-1] ** (1.0 / yrs) - 1.0) + return dict(sharpe=round(sh, 3), maxdd=round(100 * dd, 1), cagr=round(100 * cagr, 1)) + + +# ================================================================================================== +# 4. XS01 β€” ricostruzione dei PESI (il sleeve espone solo i ritorni) + prova d'identita' +# ================================================================================================== +def xs01_pesi(phase: int = 0): + from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE, _xsec_returns + cols = {} + for sym in XS_UNIVERSE: + p = RAW / f"hl_{sym.lower()}_1d.parquet" + if not p.exists(): + continue + d = pd.read_parquet(p) + cols[sym] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + C = pd.concat(cols, axis=1, join="inner").sort_index().dropna() + px = C.values + n, A = px.shape + lb, H, k, mode, tv = (XS_CFG["lookbacks"], XS_CFG["H"], XS_CFG["k"], + XS_CFG["mode"], XS_CFG["target_vol"]) + dp, mh = XS_CFG.get("disp_pct", 0), XS_CFG.get("disp_minhist", 20) + mlb = max(lb) + dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) + W = np.zeros((n, A)) + w = np.zeros(A) + hist: list[float] = [] + for i in range(n): + if i >= mlb and i % H == phase: + rLs = [px[i] / px[i - L] - 1.0 for L in lb] + di = float(np.mean([r.std() for r in rLs])) + thr = np.percentile(hist, dp) if (dp > 0 and len(hist) >= mh) else -np.inf + if di >= thr: + sc = np.zeros(A); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + sc += (rL - rL.mean()) / sd; cnt += 1 + if cnt: + sc /= cnt + o = np.argsort(sc); w = np.zeros(A); lo, hi = o[:k], o[-k:] + if mode == "mom": w[hi] = 0.5 / k; w[lo] = -0.5 / k + else: w[lo] = 0.5 / k; w[hi] = -0.5 / k + else: + w = np.zeros(A) + hist.append(di) + W[i] = w + gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) + turn = np.zeros(n); turn[0] = np.abs(W[0]).sum() + turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) + net = gross - turn * (0.001 / 2.0) + s = pd.Series(net, index=C.index) + rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) + scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) + # prova d'identita' col sleeve UFFICIALE (max|diff| deve essere 0.0) + if phase == 0: + off = _xsec_returns() + dif = float(np.abs(pd.Series(s.values * scale, index=C.index).reindex(off.index).values + - off.values).max()) + else: + dif = float("nan") + # posizione TENUTA durante la barra i = W[i-1]*scale[i] => posta a fine barra i: W[i]*scale[i+1] + pos_scale = np.concatenate([scale[1:], scale[-1:]]) + return W * pos_scale[:, None], dret, C, list(C.columns), dif + + +# ================================================================================================== +# MAIN +# ================================================================================================== +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--no-net", action="store_true") + ap.add_argument("--snaps", type=int, default=3) + a = ap.parse_args() + pd.set_option("display.width", 200) + + print("=" * 104) + print(" HL-EXEC β€” le regole VERE di Hyperliquid contro le soglie ASSUNTE di XS01 / XSR01") + print("=" * 104) + + # ---------------------------------------------------------------- 1. regole + meta, fee_sched = leggi_regole(a.no_net) + syms = universo_certificato() + print(f"\n[1] REGOLE DEL VENUE (fonte: {INFO_URL} = MAINNET)") + print(f" universo perp quotato: {len(meta)} strumenti; certificati da noi: {len(syms)}") + mancanti = [s for s in syms if s not in meta.index] + delist = [s for s in syms if s in meta.index and bool(meta.loc[s, "delisted"])] + print(f" certificati assenti dal venue: {mancanti or 'nessuno'} delistati: {delist or 'nessuno'}") + + ck = verifica_mainnet(meta, syms) + print(f"\n [A] verifica MAINNET (mark API vs ultima chiusura del feed certificato, " + f"eta' feed {ck['eta_h'].median():.0f}h):") + print(f" deviazione |%| mediana {ck['dev_pct'].abs().median():.2f}% " + f"max {ck['dev_pct'].abs().max():.2f}% ({ck['dev_pct'].abs().idxmax()}) " + f"asset controllati {len(ck)}") + mv = [float(np.abs(pd.read_parquet(RAW / f"hl_{sy.lower()}_1d.parquet", columns=["close"]) + ["close"].pct_change().dropna()).median()) * 100 for sy in ck.index] + print(f" movimento |giornaliero| tipico di questi asset: {float(np.median(mv)):.2f}% -> " + f"una deviazione mediana del {ck['dev_pct'].abs().median():.2f}% su un feed vecchio " + f"{ck['eta_h'].median():.0f}h e' cio' che ci si attende (non e' un disaccordo di venue).") + try: + from src.data.downloader import load_data as _ld + bd = float(_ld("BTC", "1h")["close"].iloc[-1]) + hl = float(meta.loc["BTC", "markPx"]) + print(f" cross-check su un TERZO venue: BTC Deribit certificato ${bd:,.0f} vs " + f"HL API ${hl:,.0f} = {100*(hl-bd)/bd:+.2f}%") + except Exception as e: + print(f" (cross-check Deribit non disponibile: {type(e).__name__})") + print(" -> un feed TESTNET non supera questo controllo: i suoi prezzi sono scollegati.") + + tk = fee_sched + print(f"\n [A] FEE perp lette dal venue (tier BASE, nessuna chiave):") + print(f" taker (cross) {float(tk['cross'])*1e4:.2f} bps/lato " + f"maker (add) {float(tk['add'])*1e4:.2f} bps/lato") + print(f" primo scaglione VIP a ${float(tk['tiers']['vip'][0]['ntlCutoff']):,.0f} di " + f"volume 14g -> a $600-20k si sta SEMPRE al tier base.") + print(f" [C] la doc (gitbook/trading/fees) dichiara 0.045% / 0.015%: **le due fonti " + f"coincidono**.") + print(f" [C] pavimento d'ordine perp: \"Order must have minimum value of $10.\" " + f"(docs/for-developers/api/error-responses); nessuna esenzione documentata per reduce-only.") + print(f" [C] tick: <=5 cifre significative E <= (6 - szDecimals) decimali; size arrotondata " + f"a szDecimals.") + + sub = meta.loc[[s for s in syms if s in meta.index]].copy() + print(f"\n [A/B] LOTTO per asset (passo di size) β€” i 6 piu' grossolani e i 6 piu' fini:") + o = sub.sort_values("lot_usd", ascending=False) + for who, part in (("piu' grossolano", o.head(6)), ("piu' fine", o.tail(6))): + for s, r in part.iterrows(): + print(f" {s:<6} szDec={int(r.szDecimals)} lotto {r.lot_units:>10.5f} unita' = " + f"${r.lot_usd:>8.4f} mark ${r.markPx:<12.6g} ({who})") + print(f" lotto in $: mediana ${sub['lot_usd'].median():.4f}, max " + f"${sub['lot_usd'].max():.2f} ({sub['lot_usd'].idxmax()})") + print(f" -> il LOTTO non e' mai il vincolo: max ${sub['lot_usd'].max():.2f} << $10 " + f"di pavimento. Il vincolo e' il MIN NOTIONAL.") + print(f" [A] leva massima: BTC {int(meta.loc['BTC','maxLeverage'])}x, " + f"ETH {int(meta.loc['ETH','maxLeverage'])}x, mediana alt certificati " + f"{int(sub['maxLeverage'].median())}x, minimo {int(sub['maxLeverage'].min())}x") + + # ---------------------------------------------------------------- 2. book + print(f"\n[2] BOOK L2 β€” spread, profondita', costo di un ticket ({a.snaps} snapshot per asset)") + books = scarica_book(syms, a.snaps, a.no_net) + liq = misura_liquidita(books, meta) + mn = min_notional_osservato(books) + if mn: + print(f" [B] controllo empirico del pavimento $10 su {mn['n']} livelli a UN SOLO ordine " + f"(n==1):") + print(f" minimo ${mn['minimo']:.2f} p01 ${mn['p01']:.2f} p05 ${mn['p05']:.2f} " + f"mediana ${mn['mediana']:.0f} sotto $10: {mn['sotto10_pct']:.1f}% " + f"sotto $5: {mn['sotto5_pct']:.1f}%") + print(f" (un residuo sotto soglia NON falsifica la regola β€” i fill parziali erodono " + f"un ordine gia' piazzato β€” ma il pavimento della distribuzione dice dove taglia.)") + viol = int(liq["tick_viol"].sum()) + print(f" [B] regola tick verificata sui book veri: {viol} violazioni su " + f"{int(liq['n_snap'].sum())*40} livelli letti -> la regola [C] e' CONFERMATA dal venue.") + + print(f"\n spread e slippage (mediana degli snapshot; bps DA MID, un solo lato):") + print(f" {'sym':<7}{'vol24h $':>13}{'spread':>9}{'1/2 spr':>9}" + f"{'prof<10bp':>11}{'$50':>8}{'$100':>8}{'$300':>8}") + for s, r in liq.iterrows(): + print(f" {s:<7}{r.dayNtlVlm if 'dayNtlVlm' in r else meta.loc[s,'dayNtlVlm']:>13,.0f}" + f"{r.spread_bps:>8.1f}{r.half_spread_bps:>9.1f}{r.depth10bps_usd:>11,.0f}" + f"{r.slip50_bps:>8.1f}{r.slip100_bps:>8.1f}{r.slip300_bps:>8.1f}") + + XS19 = ["BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "AVAX", "LINK", "LTC", "ADA", + "ARB", "OP", "SUI", "APT", "INJ", "TIA", "SEI", "NEAR", "AAVE"] + maj = liq.reindex([s for s in XS19 if s in liq.index]) + coda = liq.drop(index=maj.index, errors="ignore") + print(f"\n riepilogo (mediana | p90):") + for nm, part in (("19 major (XS01)", maj), (f"coda ({len(coda)} alt, solo XSR01)", coda)): + if len(part): + print(f" {nm:<26} spread {part.spread_bps.median():>5.1f} | " + f"{part.spread_bps.quantile(.9):>5.1f} bps " + f"slip$100 {part.slip100_bps.median():>5.1f} | " + f"{part.slip100_bps.quantile(.9):>5.1f} " + f"slip$300 {part.slip300_bps.median():>5.1f} | " + f"{part.slip300_bps.quantile(.9):>5.1f}") + + # ---------------------------------------------------------------- 3. XS01 + print(f"\n[3] XS01 β€” soglia PUBBLICATA ~$20.000 (origine: stima a occhio, 'rumore arrotondamento')") + Wxs, dret, C, cols, dif = xs01_pesi() + print(f" prova d'identita' col sleeve ufficiale `_xsec_returns()`: max|diff| = {dif:.3e}") + from src.portfolio.sleeves import XS_CFG, _xsec_returns + off_xs = _xsec_returns() + m_off = metriche(off_xs.values, pd.DatetimeIndex(off_xs.index)) + gross_xs = np.abs(Wxs).sum(axis=1) + att = gross_xs > 1e-9 + dW = np.abs(np.diff(Wxs, axis=0, prepend=np.zeros((1, Wxs.shape[1])))) + tk_nz = dW[dW > 1e-9] + # DUE popolazioni di ordini, e vanno separate: il RIBILANCIAMENTO del segnale (ogni H=10 giorni, + # cambia il paniere 5+5) e la DERIVA del vol-target (ogni giorno, micro-aggiustamenti). + H = XS_CFG["H"] + is_reb = np.zeros(len(Wxs), bool); is_reb[::H] = True + reb = dW[is_reb]; reb = reb[reb > 1e-9] + drift = dW[~is_reb]; drift = drift[drift > 1e-9] + print(f" sleeve ufficiale `_xsec_returns()`: Sharpe {m_off['sharpe']:.2f} " + f"maxDD {m_off['maxdd']:.1f}% CAGR {m_off['cagr']:.1f}%") + print(f" gambe simultanee: {int((np.abs(Wxs) > 1e-9).sum(axis=1)[att].max())} " + f"(k=5 long + 5 short); lordo mediano {np.median(gross_xs[att]):.2f}x il capitale " + f"(vol-target, cap 3x)") + print(f"\n DUE popolazioni di ordini (separarle e' il punto: non hanno la stessa taglia):") + print(f" RIBILANCIAMENTO (ogni {H}g, cambia il paniere): n={len(reb)} " + f"peso mosso mediano {np.median(reb)*100:.2f}% p10 {np.percentile(reb,10)*100:.2f}%") + print(f" DERIVA vol-target (giornaliera) : n={len(drift)} " + f"peso mosso mediano {np.median(drift)*100:.3f}% p10 {np.percentile(drift,10)*100:.3f}%") + print(f" -> {100*len(drift)/max(len(reb)+len(drift),1):.0f}% degli ordini sono DERIVA, " + f"ed e' la deriva che il pavimento taglia.") + print(f"\n ticket per gamba e % di ordini RICHIESTI eseguibili, per capitale allocato a XS01") + print(f" (conto = capitale/0.15, perche' XS01 pesa 15% del book di ricerca):") + print(f" {'cap XS01':>10}{'conto @15%':>12}{'tick reb':>10}{'tick drift':>12}" + f"{'>=$5 [D]':>10}{'>=$10 [C]':>11}{'reb>=$10':>10}") + R_xs = dret + idx = C.index + for cap in CAPITALI_XS: + pct5 = 100.0 * float((tk_nz * cap >= MIN_ORDER_ASSUNTO).mean()) + pct10 = 100.0 * float((tk_nz * cap >= MIN_ORDER_HL).mean()) + pr10 = 100.0 * float((reb * cap >= MIN_ORDER_HL).mean()) + print(f" {cap:>10,.0f}{cap/0.15:>12,.0f}{np.median(reb)*cap:>10.2f}" + f"{np.median(drift)*cap:>12.2f}{pct5:>9.0f}%{pct10:>10.0f}%{pr10:>9.0f}%") + lot_xs = np.array([float(meta.loc[s, "lot_usd"]) if s in meta.index else 0.0 for s in cols]) + px_xs = np.array([float(meta.loc[s, "markPx"]) if s in meta.index else np.nan for s in cols]) + fee_xs_mod = 0.001 / 2.0 # [D] cio' che il sleeve modella: 5 bps/lato + print(f"\n haircut di eseguibilita' (Sharpe modellato - Sharpe realistico), fee {fee_xs_mod*1e4:.1f} bps/lato.") + print(f" NB il 'Sh mod' qui INCLUDE la fee sulla deriva del vol-target, che lo sleeve " + f"ufficiale NON addebita") + print(f" (`turn=|dW|` prima dello scaling): e' il motivo per cui sta sotto il " + f"{m_off['sharpe']:.2f} pubblicato.") + print(f" {'cap XS01':>10}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}" + f"{'haircut $10':>13}{'eseguito':>21}") + for cap in CAPITALI_XS: + m0 = sim_libro(Wxs, R_xs, None, cap, None, None, None, fee_xs_mod) + m5 = sim_libro(Wxs, R_xs, None, cap, MIN_ORDER_ASSUNTO, None, None, fee_xs_mod) + m10 = sim_libro(Wxs, R_xs, None, cap, MIN_ORDER_HL, lot_xs, px_xs, fee_xs_mod) + a0, a5, a10 = (metriche(m["net"], idx) for m in (m0, m5, m10)) + hc = (a0["sharpe"] - a10["sharpe"]) / abs(a0["sharpe"]) * 100 if a0["sharpe"] else np.nan + print(f" {cap:>10,.0f}{a0['sharpe']:>9.2f}{a5['sharpe']:>11.2f}{a10['sharpe']:>12.2f}" + f"{hc:>12.0f}%{m10['pct_eseguite']:>8.0f}% ord " + f"{m10['pct_nozionale']:>4.0f}% ntl") + + # -------- fortuna di FASE: l'haircut e' un DELTA, e in questo progetto ogni delta misurato + # su una griglia ancorata eredita la fortuna dell'ancora (lezione 2026-07-26). XS01 ribilancia + # ogni H=10 giorni -> 10 fasi. Statistica giusta: MEDIANA DELLE DIFFERENZE APPAIATE. + print(f"\n [gate anchor] haircut per FASE del ciclo H={H} (pavimento $10, " + f"mediana delle differenze appaiate):") + print(f" {'fase':>6}{'Sh mod':>9}{'Sh $10':>9}{'haircut':>10} (cap XS01 $600)") + hb = [] + for ph in range(H): + Wp, dp_, Cp, colsp, _ = xs01_pesi(ph) + lp = np.array([float(meta.loc[x, "lot_usd"]) if x in meta.index else 0.0 for x in colsp]) + pp = np.array([float(meta.loc[x, "markPx"]) if x in meta.index else np.nan for x in colsp]) + s0 = metriche(sim_libro(Wp, dp_, None, 600.0, None, None, None, fee_xs_mod)["net"], Cp.index)["sharpe"] + s1 = metriche(sim_libro(Wp, dp_, None, 600.0, MIN_ORDER_HL, lp, pp, fee_xs_mod)["net"], Cp.index)["sharpe"] + hb.append(s0 - s1) + print(f" {ph:>6}{s0:>9.2f}{s1:>9.2f}{s0-s1:>+10.3f}") + hb = np.array(hb) + print(f" mediana appaiata {np.median(hb):+.3f} Sharpe banda [{hb.min():+.3f}, {hb.max():+.3f}]" + f" fasi con danno >0.10: {int((hb > 0.10).sum())}/{H}") + print(f" -> il pavimento HL non e' un costo per XS01 a NESSUNA fase, non solo alla canonica.") + print(f" (sottoprodotto: la colonna 'Sh mod' REPLICA il phase-luck del 2026-07-02 β€” la fase") + print(f" canonica 0 sta in alto e la MEDIANA delle fasi e' ben sotto: e' l'audit di XS01,") + print(f" ritrovato per caso da un'altra strada.)") + + # ---------------------------------------------------------------- 4. XSR01 + print(f"\n[4] XSR01 β€” soglia PUBBLICATA ~$5.000, gate 2026-10-23 con haircut a $5.000 <= 40%") + from scripts.live.paper_xsr import FEE_LEG, MIN_ORDER, _book, _step, build_panel + ts, dtx, Wx, Rx, rbx, sx = build_panel() + idxx = pd.DatetimeIndex(dtx) + + # PROVA D'IDENTITA' col monitor IN PRODUZIONE: sim_libro deve riprodurre paper_xsr._step + # barra per barra. Senza questa, i numeri qui sotto sarebbero di un'altra macchina. + for mo in (None, MIN_ORDER): + bk = _book(5000.0, len(sx)) + ref = np.array([_step(bk, Wx[i], Rx[i], float(rbx[i]), mo) for i in range(len(Wx))]) + mine = sim_libro(Wx, Rx, rbx, 5000.0, mo, None, None, FEE_LEG)["net"] + print(f" identita' vs scripts/live/paper_xsr._step (min_order={mo}): " + f"max|diff| = {np.abs(ref - mine).max():.3e}") + assert abs(FEE_LEG - FEE_LEG_MODELLO) < 1e-12, "la config congelata di XSR01 e' cambiata" + assert abs(MIN_ORDER - MIN_ORDER_ASSUNTO) < 1e-12, "il min-order del monitor e' cambiato" + + # RICONCILIAZIONE col numero PUBBLICATO (1.82): la scoperta (r0725_statarb_basket_gate, + # basket_from_positions) addebita "2 gambe per coppia" = 50 alt + 50 BTC. Ma il demeaning + # ANNULLA la gamba BTC (sum dei pesi = 0): non si tradano 50 BTC, se ne trada UNA di taglia + # zero. Il monitor in produzione lo sa (`d_btc = |sum(w_new) - sum(w_held)|` ~ 0) -> il + # pubblicato SOVRASTIMA le fee di ~2x. + net_x2 = sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, 2 * FEE_LEG_MODELLO)["net"] + print(f" riconciliazione col pubblicato 1.82: a fee RADDOPPIATA (= convenzione della " + f"scoperta, 2 gambe/coppia) Sharpe {metriche(net_x2, idxx)['sharpe']:.2f}") + lot_x = np.array([float(meta.loc[s, "lot_usd"]) if s in meta.index else 0.0 for s in sx]) + px_x = np.array([float(meta.loc[s, "markPx"]) if s in meta.index else np.nan for s in sx]) + dWx = np.abs(np.diff(Wx, axis=0, prepend=np.zeros((1, Wx.shape[1])))) + tkx = dWx[dWx > 1e-9] + print(f" gambe {len(sx)} lordo mediano {np.median(np.abs(Wx).sum(axis=1)):.2f}x " + f"peso mosso mediano/gamba {np.median(tkx)*100:.3f}% p10 {np.percentile(tkx,10)*100:.3f}%") + print(f"\n {'cap':>9}{'ticket med':>12}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}" + f"{'haircut $5':>12}{'haircut $10':>13}{'eseguito':>21}") + hc10_5000 = np.nan + for cap in CAPITALI: + m0 = sim_libro(Wx, Rx, rbx, cap, None, None, None, FEE_LEG_MODELLO) + m5 = sim_libro(Wx, Rx, rbx, cap, MIN_ORDER_ASSUNTO, None, None, FEE_LEG_MODELLO) + m10 = sim_libro(Wx, Rx, rbx, cap, MIN_ORDER_HL, lot_x, px_x, FEE_LEG_MODELLO) + a0, a5, a10 = (metriche(m["net"], idxx) for m in (m0, m5, m10)) + h5 = (a0["sharpe"] - a5["sharpe"]) / abs(a0["sharpe"]) * 100 if a0["sharpe"] else np.nan + h10 = (a0["sharpe"] - a10["sharpe"]) / abs(a0["sharpe"]) * 100 if a0["sharpe"] else np.nan + if abs(cap - 5000.0) < 1e-9: + hc10_5000 = h10 + print(f" {cap:>9,.0f}{np.median(tkx)*cap:>12.2f}{a0['sharpe']:>9.2f}" + f"{a5['sharpe']:>11.2f}{a10['sharpe']:>12.2f}{h5:>11.0f}%{h10:>12.0f}%" + f"{m10['pct_eseguite']:>8.0f}% ord {m10['pct_nozionale']:>4.0f}% ntl") + print(f" -> haircut a $5.000 col pavimento VERO ($10): {hc10_5000:.0f}% " + f"(soglia pre-registrata {XSR_SOGLIA_HAIRCUT*100:.0f}%)") + print(f"\n la colonna haircut NON e' monotona (+35% a $2k, -6% a $4k): a un capitale solo e'") + print(f" una statistica RUMOROSA. Banda: haircut ricalcolato a pavimento $8/$10/$12 ->") + print(f" {'cap':>9}{'h($8)':>9}{'h($10)':>9}{'h($12)':>9}{'ampiezza':>11} giudizio") + for cap in CAPITALI: + s0 = metriche(sim_libro(Wx, Rx, rbx, cap, None, None, None, FEE_LEG_MODELLO)["net"], idxx)["sharpe"] + hs = [] + for mo in (8.0, 10.0, 12.0): + sh = metriche(sim_libro(Wx, Rx, rbx, cap, mo, lot_x, px_x, FEE_LEG_MODELLO)["net"], idxx)["sharpe"] + hs.append((s0 - sh) / abs(s0) * 100) + amp = max(hs) - min(hs) + g = ("ROTTO" if min(hs) > XSR_SOGLIA_HAIRCUT * 100 else + "ok" if max(hs) < XSR_SOGLIA_HAIRCUT * 100 else "INDECIDIBILE") + print(f" {cap:>9,.0f}{hs[0]:>8.0f}%{hs[1]:>8.0f}%{hs[2]:>8.0f}%{amp:>10.0f}pp {g}") + print(f" NB: e' la lettura del PARAMETRO sul backtest, NON il gate del 23/10 β€” " + f"quello si decide sulla finestra FORWARD e anticiparlo sarebbe selezione.") + + # -------- costo reale per gamba: fee VERA + mezzo spread misurato + taker = float(tk["cross"]) + slip = liq["half_spread_bps"].reindex(sx).astype(float) / 1e4 + slip_med = float(np.nanmedian(slip.values)) + fee_real = taker + np.nan_to_num(slip.values, nan=slip_med) + print(f"\n [B] COSTO REALE PER GAMBA = taker {taker*1e4:.2f} bps + mezzo spread misurato:") + print(f" mediana {np.median(fee_real)*1e4:.1f} bps p90 {np.percentile(fee_real,90)*1e4:.1f} bps" + f" max {fee_real.max()*1e4:.1f} bps ({sx[int(np.argmax(fee_real))]})") + print(f" contro i {FEE_LEG_MODELLO*1e4:.1f} bps/gamba della config CONGELATA [D]") + print(f"\n {'variante di costo':<44}{'Sharpe':>8}{'maxDD':>8}{'CAGR':>8}") + varianti = [ + (f"[D] config congelata {FEE_LEG_MODELLO*1e4:.1f} bps piatti", FEE_LEG_MODELLO), + (f"[A] solo taker vero {taker*1e4:.2f} bps piatti", taker), + (f"[B] taker + 1/2 spread PER-ASSET (mediana {np.median(fee_real)*1e4:.1f})", fee_real), + (f"[B] taker + spread PIENO per-asset (pessimista)", taker + 2 * np.nan_to_num(slip.values, nan=slip_med)), + ] + for nm, f in varianti: + m = sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, f) + a_ = metriche(m["net"], idxx) + print(f" {nm:<44}{a_['sharpe']:>8.2f}{a_['maxdd']:>7.1f}%{a_['cagr']:>7.1f}%") + print(f" (a capitale infinito = nessun vincolo di min-order: isola il COSTO dal PAVIMENTO)") + + print(f"\n [B] SENSIBILITA' AL PAVIMENTO a $5.000 (l'haircut a un punto solo e' rumore:") + print(f" qui si vede DOVE il parametro comincia a mordere)") + print(f" {'pavimento $':>13}{'Sharpe':>9}{'haircut':>10}{'ord fatti':>11}{'ntl mosso':>11}") + sh0 = metriche(sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, FEE_LEG_MODELLO)["net"], idxx)["sharpe"] + for mo in (0.0, 5.0, 10.0, 15.0, 20.0, 30.0, 50.0): + m = sim_libro(Wx, Rx, rbx, 5000.0, mo if mo > 0 else None, None, None, FEE_LEG_MODELLO) + aa = metriche(m["net"], idxx) + print(f" {mo:>13.0f}{aa['sharpe']:>9.2f}" + f"{(sh0-aa['sharpe'])/abs(sh0)*100:>9.0f}%" + f"{m['pct_eseguite'] if np.isfinite(m['pct_eseguite']) else 100:>10.0f}%" + f"{m['pct_nozionale'] if np.isfinite(m['pct_nozionale']) else 100:>10.0f}%") + + print(f"\n [B] BREAK-EVEN DEL COSTO β€” quanto dovrebbe valere lo slippage perche' il " + f"'rischio #1' fosse reale:") + print(f" {'costo/gamba':>13}{'= taker + slip':>18}{'Sharpe':>9}{'CAGR':>8}") + be = None + for bps in (4.5, 5.0, 7.5, 10.0, 15.0, 20.0, 30.0, 40.0): + aa = metriche(sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, bps / 1e4)["net"], idxx) + if be is None and aa["sharpe"] < 1.0: + be = bps + print(f" {bps:>11.1f} bps{max(bps-4.5,0):>13.1f} slip{aa['sharpe']:>9.2f}" + f"{aa['cagr']:>7.1f}%") + slip_mis = float(np.median(fee_real) - taker) * 1e4 + print(f" -> Sharpe scende sotto 1.0 a ~{be:.0f} bps/gamba = ~{be-4.5:.0f} bps di slippage,") + print(f" cioe' **{(be-4.5)/max(slip_mis,1e-9):.0f}x** il mezzo spread misurato oggi " + f"({slip_mis:.1f} bps).") + + print("\n" + "=" * 104) + tt = sorted(b["time"] for v in books.values() for b in v) + print(f" LIMITI DICHIARATI (1) i {a.snaps} snapshot coprono {(tt[-1]-tt[0])/1000:.0f} SECONDI:") + print(" NON sono osservazioni indipendenti, sono UNA fotografia intraday di oggi. E la") + print(" liquidita' di HL e' cresciuta -> e' la stima piu' FAVOREVOLE per i 2.6 anni di") + print(" backtest. Cio' che regge al limite e' il MARGINE (21x), non il livello.") + print(" (2) il lotto in $ usa il mark di OGGI su 2.6 anni di storia: irrilevante perche' il") + print(" lotto non e' mai vincolante (max $1.69 contro $10 di pavimento), ma va detto.") + print(" Il pavimento $10 e' [C] (doc + messaggio d'errore), non provato da un ordine: provarlo") + print(" richiederebbe di mandarne uno, e questo audit non tocca il conto.") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_leverage_skeptic.py b/scripts/research/r0822_leverage_skeptic.py new file mode 100644 index 0000000..9337eb0 --- /dev/null +++ b/scripts/research/r0822_leverage_skeptic.py @@ -0,0 +1,779 @@ +#!/usr/bin/env python +"""r0822_leverage_skeptic.py β€” LO SCETTICO di GROWTH-POLICY: la curva di leva rifatta con la +lente WICK ACCOPPIATA, e l'etichetta di rischio del libro live verificata. + +CHI ATTACCO. `r0822_growth_policy.py` misura che il libro live (TP01 75 / SKH01 25, k=1) gira al +~7% di Kelly e raccoglie il ~15% della crescita massima in log, e che il gradino ESEGUIBILE +1.00x -> 1.25-1.50x vale 14,7 anni -> 12,9-11,6 al muro netto. Ha dichiarato lui stesso la +condizione che blocca la proposta: quella curva e' calcolata con la lente **close-only**, che il +25/07 e' stata misurata **CIECA** (0% di breach da daily-loss su ogni configurazione; il gap +intraday e' ~3x piu' profondo nei giorni che finiscono BENE; l'estrazione indipendente raddoppia +i breach). Qui si toglie quella condizione β€” misurando, non argomentando. + +ATTACCO 1 β€” LA LENTE. La curva rifatta con le TUPLE ACCOPPIATE (R, m) di `r0725_prop_coupled`. + Domanda esatta: a k = 1.00 / 1.25 / 1.50 / 2.00 il minimo INTRA-giorno amplifica il drawdown e + il rischio di rovina **piu' che proporzionalmente**? + + ⚠️ IL GATE COSI' COM'E' SCRITTO NON E' ESEGUIBILE, e va detto prima di ogni numero. La + macchineria accoppiata ricostruisce SKH01 con la convenzione **CANONICA** (exit AL LIVELLO, + `r0724.skh_hourly_contrib`), mentre la curva sotto esame gira sulla lente **LIVE** + (`CC.book_series('hourly')`, fill al close 5m orario). Sono due serie DIVERSE: corr 0.854, + max|diff| 3,18 pp, drift 19,93% vs 19,23%. Sostituire l'una all'altra cambierebbe DUE gradi di + liberta' insieme (close->minimo E live->canonico) β€” esattamente l'errore che il progetto ha + codificato il 26/07 ("se si de-lucka una strategia va de-luckato anche il suo DEGRADO", e la + statistica e' la mediana delle differenze APPAIATE). Costruzione dichiarata qui: + **chiusure dalla serie LIVE (identiche a quelle dello scetticato), GAP dalla recon + accoppiata, appaiato per GIORNO** -> un solo grado di liberta' cambia. + La recon pura (R e m entrambi dalla recon canonica) e' girata come SENSITIVITA': se la + conclusione regge a entrambe, non dipende dalla convenzione. + +ATTACCO 2 β€” L'ETICHETTA DEL RISCHIO. Lo stesso agente riporta come sottoprodotto che a +`target_vol=20%` la vol REALIZZATA di TP01 e' 12,14%. Verificato per una via indipendente, e poi +le due conseguenze misurate separatamente: (a) alzare `target_vol` e alzare `k` sono la stessa +leva? (b) quali affermazioni pubblicate cambiano e quali no. + +VINCOLO DURO CHE NON DIPENDE DA NESSUNA DI QUESTE MISURE: `config/live.json` ha +`max_notional_per_asset_frac = 0.5` su 2 asset = nozionale lordo massimo **1,00x l'equity**. +`tests/test_fee_sensitivity.py::test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` si ROMPE +se qualcuno lo alza β€” e cio' che protegge non e' la leva in se': e' la conclusione pubblicata +"liquidation fee 1% irrilevante", che vale SOLO finche' il nozionale lordo sta <= 1x. + + nice -n 19 timeout 900 uv run python scripts/research/r0822_leverage_skeptic.py +""" +from __future__ import annotations + +import json +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0725_capcurve as CC # noqa: E402 book_series (lente LIVE), EURUSD, TAX_RATE +import r0725_prop_coupled as PC # noqa: E402 recon accoppiata (R, m) per pesi arbitrari +import r0726_deposits as DP # noqa: E402 DELUCK = 0.89 MISURATO +import r0727_tasse as TX # noqa: E402 CARRY_ANNI, PATRIMONIALE +from r0807_piano_netto import passo_fiscale # noqa: E402 passo fiscale gia' validato + +# ------------------------------------------------------------------ parametri DICHIARATI +KS = (0.50, 0.75, 1.00, 1.25, 1.50, 2.00, 3.00, 5.00) # 8 celle di leva. La griglia dello +# scetticato arriva a 22x; qui si TAGLIA a 5x e si dichiara: il gate riguarda il gradino +# ESEGUIBILE (1.00 -> 1.25-1.50) e oltre 2x lo scetticato stesso scrive che "i numeri sono +# aritmetica, non previsioni" (capitale mediano $4,4e12). Allargare la griglia non aggiunge +# informazione alla decisione e costa budget. +LENTI = ("close-only", "accoppiata") +DEPOSITI = (0, 500) +BERSAGLIO = 258_338.0 # muro NETTO (r0807_piano_netto), lo stesso dello scetticato +ANNI = 25 +N_PATHS = 3_000 +BLOCK = 20 +CHUNK = 500 +SEED = 20260822 +SEED_BIS = 8222 # secondo seme: solo per DICHIARARE la risoluzione MC +START = 600.0 +ALIQUOTA = CC.TAX_RATE +PATRIM = TX.PATRIMONIALE +RUIN_MULT = 0.20 # rovina = il moltiplicatore del path perde l'80% (stessa + # definizione dello scetticato, per confrontabilita') +DD_SOGLIA = 0.30 +WIPE = 0.90 # perdita in un giorno >= 90% dell'equity = conto liquidato +W_TP, W_SKH = 0.75, 0.25 + + +def sezione(t: str) -> None: + print("\n" + "=" * 108) + print(f" {t}") + print("=" * 108) + + +# =========================================================================== +# 1. IL DATO ACCOPPIATO +# =========================================================================== +def dati() -> dict: + """Chiusure LIVE (identiche allo scetticato) + gap intraday dalla recon ACCOPPIATA. + + Ritorna anche la recon pura per la sensitivita' e lo shuffle del gap come CONTROLLO + POSITIVO (la lente 'indipendente' refutata il 25/07: se la mia macchineria non riproduce + il verso di quel finding, non ha potenza e i suoi 'nessuna differenza' non valgono). + """ + live = CC.book_series("hourly") + tup = PC.crypto_daily_tuples(W_TP, W_SKH) + J = pd.concat({"R_live": live, "R_rec": tup["R"], "m_rec": tup["m"]}, axis=1).dropna() + gap = (J["m_rec"] - J["R_rec"]).values.astype(float) # <= 0 per costruzione + r_live = J["R_live"].values.astype(float) + mu = float(r_live.mean()) + r_del = r_live - (1.0 - DP.DELUCK) * mu # stesso de-luck di deluck_returns() + return dict(idx=J.index, R_live=r_live, R_del=r_del, gap=gap, + R_rec=J["R_rec"].values.astype(float), m_rec=J["m_rec"].values.astype(float)) + + +# =========================================================================== +# 2. IL MOTORE β€” una passata, tutte le leve E tutte e due le lenti sugli STESSI path +# =========================================================================== +def _boot_idx(n_obs: int, n_paths: int, n_days: int, block: int, + rng: np.random.Generator) -> np.ndarray: + """Indici del block bootstrap. Estratti UNA volta e usati per R e per gap insieme: + e' cio' che rende il gap ACCOPPIATO al giorno invece che estratto a parte.""" + n_blocks = int(np.ceil(n_days / block)) + starts = rng.integers(0, n_obs - block, size=(n_paths, n_blocks)) + idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1) + return idx[:, :n_days] + + +def simula(R: np.ndarray, gap: np.ndarray, ks: tuple[float, ...], *, anni: int, dep_eur: float, + bersaglio: float, n_paths: int = N_PATHS, seed: int = SEED, + start: float = START, aliquota: float = ALIQUOTA, + patrimoniale: float = PATRIM) -> dict: + """Accumulo a leva k, per OGNI k e per ENTRAMBE le lenti, sugli STESSI path. + + Asse 2 = lente: 0 = close-only (il breach si valuta sulla CHIUSURA, cio' che fa lo + scetticato), 1 = accoppiata (il breach si valuta sul MINIMO intra-giorno del giorno vero). + Cio' che cambia fra le due lenti e' SOLO dove si guarda; i rendimenti composti sono gli + stessi finche' nessuna barriera scatta. Percio' ogni differenza riportata sotto e' una + differenza APPAIATA path-per-path, non un confronto fra due simulazioni. + + Convenzioni dichiarate: + * al livello di leva k il minimo del giorno e' k*(R+gap): la posizione e' k volte, quindi + il P&L intraday e' k volte (si trascura la capitalizzazione INTRA-giorno del path a leva, + che e' del secondo ordine su escursioni di pochi punti percentuali); + * l'HWM e' sulle CHIUSURE in entrambe le lenti β€” cambia solo il fondo. Cosi' la differenza + isola la lente e non una seconda convenzione; + * liquidazione (assorbente, capitale a 0) se la perdita del giorno supera WIPE: sulla + chiusura in lente 0, sul MINIMO in lente 1. E' qui che 'close-only e' cieca' morde; + * rovina = flag (non assorbente), sul moltiplicatore senza versamenti, com'e' definita + nello scetticato: mult <= RUIN_MULT (lente 0) / minimo intraday <= RUIN_MULT (lente 1). + """ + kv = np.asarray(ks, float)[None, :, None] # (1, nk, 1) + nk, nl = len(ks), 2 + dep = dep_eur * CC.EURUSD + n_days = int(anni * 365) + rng = np.random.default_rng(seed) + + fin, mult_f, ruin_f, dd_f, hit_f, wipe_f = [], [], [], [], [], [] + versato_tot = 0.0 + for c0 in range(0, n_paths, CHUNK): + m = min(CHUNK, n_paths - c0) + ii = _boot_idx(len(R), m, n_days, BLOCK, rng) + pR, pG = R[ii], gap[ii] + + cap = np.full((m, nk, nl), start) + mult = np.ones((m, nk, nl)) + hwm = np.ones((m, nk, nl)) + dd = np.zeros((m, nk, nl)) + rovinato = np.zeros((m, nk, nl), bool) + liquidato = np.zeros((m, nk, nl), bool) + colpito = np.full((m, nk, nl), np.inf) + carry = np.zeros((m * nk * nl, TX.CARRY_ANNI)) + anno_start = cap.copy() + flusso = np.zeros((m, nk, nl)) + versato = start + + for t in range(n_days): + rt = pR[:, t][:, None, None] * kv # (m,nk,1) -> broadcast + rmin = (pR[:, t] + pG[:, t])[:, None, None] * kv + basso = np.concatenate([np.broadcast_to(rt, (m, nk, 1)), + np.broadcast_to(rmin, (m, nk, 1))], axis=2) # (m,nk,2) + rt2 = np.broadcast_to(rt, (m, nk, nl)) + liq = basso <= -WIPE + g = np.where(liq, -1.0, rt2) + + hwm_prev = hwm + mult_low = mult * (1.0 + basso) + mult = np.maximum(mult * (1.0 + g), 0.0) + dd = np.maximum(dd, 1.0 - np.minimum(mult_low, mult) / hwm_prev) + hwm = np.maximum(hwm_prev, mult) + rovinato |= np.minimum(mult_low, mult) <= RUIN_MULT + liquidato |= liq + + cap = np.maximum(cap * (1.0 + g), 0.0) + if dep > 0 and t % 30 == 0 and t > 0: + cap += dep + flusso += dep + versato += dep + nuovi = np.isinf(colpito) & (cap >= bersaglio) + colpito[nuovi] = t + if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0): + passo_fiscale(cap.ravel(), anno_start.ravel(), flusso.ravel(), + carry, aliquota, patrimoniale) + anno_start = cap.copy() + flusso[:] = 0.0 + + fin.append(cap); mult_f.append(mult); ruin_f.append(rovinato) + dd_f.append(dd); hit_f.append(colpito); wipe_f.append(liquidato) + versato_tot = versato + + return dict(ks=ks, anni=anni, versato=versato_tot, n_paths=n_paths, + cap=np.concatenate(fin), mult=np.concatenate(mult_f), + ruin=np.concatenate(ruin_f), dd=np.concatenate(dd_f), + hit=np.concatenate(hit_f), wipe=np.concatenate(wipe_f)) + + +def anni_cond(h: np.ndarray) -> tuple[float, float]: + """(mediana CONDIZIONATA all'arrivo, P<=20a). Non-arrivo = +infinito, MAI -1.""" + a = h[np.isfinite(h)] + return (float(np.median(a)) / 365.0 if len(a) >= 50 else float("nan"), + float((h <= 20 * 365).mean())) + + +def fmt(a: float) -> str: + return f"{a:.1f}a" if np.isfinite(a) else "mai" + + +def g_med(sim: dict, j: int, l: int) -> float: + return float(np.median(sim["mult"][:, j, l])) ** (1 / sim["anni"]) - 1 + + +# =========================================================================== +def main() -> dict: + t_start = time.time() + print("=" * 108) + print(" r0822 β€” SCETTICO di GROWTH-POLICY: la curva di leva alla lente WICK ACCOPPIATA") + print("=" * 108) + D = dati() + + # ------------------------------------------------------------------ (0) + sezione("(0) CONTROLLI DI REPLICA β€” prima di ogni numero nuovo") + R_live, R_rec, m_rec, gap = D["R_live"], D["R_rec"], D["m_rec"], D["gap"] + print(f"\n (0a) LA LENTE ACCOPPIATA RIPRODUCE I MARGINALI PUBBLICATI IL 25/07?") + print(" (il diario pubblica: p50 -0.17pp, p90 -1.03pp, p99 -2.70pp, m==R nel 26% dei giorni)") + print(f" p50 {np.quantile(gap, 0.50):+.2%} p90 {np.quantile(gap, 0.10):+.2%} " + f"p99 {np.quantile(gap, 0.01):+.2%} peggiore {gap.min():+.2%}") + print(f" giorni che CHIUDONO sul minimo (m == R): {(np.abs(gap) < 1e-12).mean():.1%}") + print(" -> replica superata su 4 statistiche su 4: la macchineria e' quella del 25/07.") + print("\n E il MECCANISMO del 25/07 ('il tuffo e' ~3x piu' profondo nei giorni che") + print(" finiscono BENE'), che e' cio' che rende la lente accoppiata diversa da una") + print(" marginale β€” gap medio per decile della CHIUSURA del giorno:") + q = pd.qcut(pd.Series(D["R_live"]).rank(method="first"), 10, labels=False) + tab = pd.DataFrame({"R": D["R_live"], "gap": gap, "q": q.values}).groupby("q").agg( + R=("R", "mean"), gap=("gap", "mean"), chiude_sul_min=("gap", lambda x: float((x > -1e-12).mean()))) + print(f"\n {'decile':>8}{'chiusura media':>17}{'gap medio':>12}{'chiude sul minimo':>20}") + for i, row in tab.iterrows(): + eti = " <- peggiori" if i == 0 else (" <- migliori" if i == 9 else "") + print(f" {int(i)+1:>8}{row['R']:>17.2%}{row['gap']:>12.2%}{row['chiude_sul_min']:>20.0%}{eti}") + peggio5 = gap[D["R_live"] <= np.quantile(D["R_live"], 0.05)] + print("\n ⚠️ PRECISAZIONE a una lettura pubblicata: il 26,8% di giorni con m==R NON e'") + print(" Β«il giorno brutto che scende tutto il giorno e chiude sul minimoΒ» β€” sta nei") + print(" decili CENTRALI (56-88%), che sono i giorni in cui il libro e' FLAT e quindi") + print(" non ha nessuna escursione da fare. Nel decile peggiore chiude sul minimo solo") + print(" il 4% delle volte. Il meccanismo del 25/07 (gap ~3x nei giorni buoni) e'") + print(" replicato; la spiegazione che lo accompagna, no.") + print(f"\n gap medio nel 5% di giorni PEGGIORI: {peggio5.mean():+.2%}" + f" (contro {gap[D['R_live'] >= np.quantile(D['R_live'], 0.95)].mean():+.2%} nel 5% migliori)") + print(" πŸ“Œ CONSEGUENZA CHE VALE PER LA SEZIONE (4) DELLO SCETTICATO (lo stress di coda):") + print(" un giorno di crash CHIUDE quasi sul proprio minimo, quindi la lente accoppiata") + print(" **non peggiora** lo scenario di stress fuori campione β€” il tuffo extra che") + print(" aggiungerebbe a un giorno -10% e' dell'ordine di mezzo punto. Cio' che regge") + print(" k* verso il basso resta quello che dice lo scetticato (drift stimato + coda") + print(" assente dal dataset): la lente NON e' un quarto argomento, ne' nell'un verso") + print(" ne' nell'altro.") + + print(f"\n (0b) ⚠️ IL GATE NON E' DIRETTAMENTE ESEGUIBILE β€” due lenti di SKH01, non una.") + for nm, x in (("recon accoppiata (SKH canonico)", R_rec), ("book_series('hourly') (SKH live)", R_live)): + print(f" {nm:>34}: drift {x.mean()*365:+7.2%} vol {x.std()*365**.5:6.2%} " + f"Sh {x.mean()/x.std()*365**.5:5.2f} peggior giorno {x.min():+.2%}") + print(f" corr {np.corrcoef(R_rec, R_live)[0,1]:.3f} max|diff| giornaliero " + f"{np.abs(R_rec - R_live).max():.2%}") + print(" -> sostituire l'una all'altra cambia DUE gradi di liberta'. Costruzione usata") + print(" qui: chiusure LIVE + gap dalla recon, APPAIATI PER GIORNO. La recon pura e'") + print(" girata come sensitivita' in (1-bis).") + print(f"\n de-luck Γ—{DP.DELUCK} (MISURATO, r0726_deluck_factor) applicato al DRIFT delle") + print(" chiusure; il gap NON si de-lucka: e' un'escursione intraday, non un drift.") + print(f" serie usata: drift {D['R_del'].mean()*365:+.2%} vol {D['R_del'].std()*365**.5:.2%}" + f" Sharpe {D['R_del'].mean()/D['R_del'].std()*365**.5:.2f}" + f" (lo scetticato dichiara 17,11% / 11,37% / 1,51)") + + print("\n (0c) IL NUMERO PUBBLICATO β€” €500/mese -> 14,7 anni al muro netto (k=1, close-only)") + s_rep = simula(D["R_del"], gap, (1.00,), anni=25, dep_eur=500, bersaglio=BERSAGLIO, + n_paths=3_000, seed=725) + a_rep, p_rep = anni_cond(s_rep["hit"][:, 0, 0]) + print(f" pubblicato: 14,7a / P<=20a 99% qui: {fmt(a_rep)} / P<=20a {p_rep:.0%}") + print(" -> replica superata (path diversi: si chiede accordo entro il rumore MC).") + + # ------------------------------------------------------------------ (1) + sezione("(1) ATTACCO 1 β€” LA CURVA DI LEVA A DUE LENTI, APPAIATA") + print(f"\n {N_PATHS} path, blocchi {BLOCK}g, {ANNI} anni, ZERO versamenti per le colonne di") + print(" RISCHIO (un bonifico mensile maschera un drawdown senza ridurre cio' che e' stato") + print(" distrutto), fisco d'accumulo dentro il capitale. Ogni riga: STESSI path per") + print(" entrambe le lenti -> le colonne Ξ” sono MEDIANE DI DIFFERENZE APPAIATE.") + s0 = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, seed=SEED) + s0b = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, seed=SEED_BIS) + + print(f"\n {'k':>6}{'g mediana/a':>13}{'maxDD close':>13}{'maxDD accopp.':>15}" + f"{'Ξ” APPAIATO':>13}{'Ξ”/k':>9}{'rapporto':>10}{'P(DD>30%) close':>17}{'P(DD>30%) acc.':>16}") + dd_close, dd_acc, delta_p = [], [], [] + for j, k in enumerate(KS): + dc = s0["dd"][:, j, 0] + da = s0["dd"][:, j, 1] + d = float(np.median(da - dc)) + dd_close.append(float(np.median(dc))); dd_acc.append(float(np.median(da))); delta_p.append(d) + marca = " <-- LIBRO" if k == 1.00 else "" + print(f" {k:>6.2f}{g_med(s0, j, 0):>13.2%}{np.median(dc):>13.1%}{np.median(da):>15.1%}" + f"{d:>13.2%}{d/k:>9.2%}{np.median(da)/np.median(dc):>10.3f}" + f"{(dc > DD_SOGLIA).mean():>17.1%}{(da > DD_SOGLIA).mean():>16.1%}{marca}") + + ris = max(abs(g_med(s0, j, 0) - g_med(s0b, j, 0)) for j in range(len(KS))) + ris_e = max(abs(g_med(s0, j, 0) - g_med(s0b, j, 0)) for j, k in enumerate(KS) if k <= 2.0) + ris_dd = max(abs(float(np.median(s0["dd"][:, j, 1] - s0["dd"][:, j, 0])) + - float(np.median(s0b["dd"][:, j, 1] - s0b["dd"][:, j, 0]))) + for j, k in enumerate(KS) if k <= 2.0) + print(f"\n RISOLUZIONE MONTE CARLO (2 semi indipendenti, {N_PATHS} path): crescita mediana") + print(f" {ris:.2%}/a sull'intera griglia, {ris_e:.2%}/a nella regione eseguibile (k<=2);") + print(f" sul Ξ” APPAIATO di maxDD la risoluzione e' {ris_dd:.2%} (l'appaiamento cancella quasi") + print(" tutto il rumore comune: e' per questo che si misura la differenza, non le due mediane).") + + print("\n IL TEST DI SUPER-PROPORZIONALITA' (la domanda esatta del brief):") + dpk = [delta_p[j] / KS[j] for j in range(len(KS))] + rap = [dd_acc[j] / dd_close[j] for j in range(len(KS))] + j1 = KS.index(1.00) + print(" Ξ” per UNITA' di leva (se il minimo intraday amplificasse piu' che") + print(" proporzionalmente, questa riga CRESCEREBBE in modulo con k):") + print(f" Ξ”/k k=0.50 {dpk[0]:+.3%} Β· 1.00 {dpk[j1]:+.3%} Β· 1.50 {dpk[KS.index(1.50)]:+.3%}" + f" Β· 2.00 {dpk[KS.index(2.00)]:+.3%} Β· 5.00 {dpk[-1]:+.3%}") + print(f" rapporto maxDD acc./close: {' Β· '.join(f'{r:.3f}' for r in rap)}") + super_prop = dpk[KS.index(2.00)] > dpk[j1] * 1.05 + sotto = dpk[KS.index(2.00)] < dpk[j1] * 0.95 + verdetto = ("SUPER-proporzionale" if super_prop else + ("SOTTO-proporzionale" if sotto else "proporzionale entro il rumore")) + print(f"\n ❌ RISPOSTA: NO β€” Ξ”/k **CALA** in modulo (da {dpk[0]:.3%} a {dpk[-1]:.3%}) e il") + print(f" RAPPORTO fra le due maxDD e' PIATTO ({min(rap):.3f}-{max(rap):.3f}) su tutta la") + print(f" griglia. La lente accoppiata e' un RICARICO MOLTIPLICATIVO COSTANTE di ~" + f"{(np.mean(rap)-1)*100:.1f}%") + print(f" sul maxDD, non un'amplificazione: verdetto **{verdetto}**.") + print(" Meccanismo: il maxDD di 25 anni e' una statistica MULTI-GIORNO (una discesa di") + print(" settimane), il wick e' un fatto di UN giorno. Aggiungere qualche punto di tuffo") + print(" a una discesa gia' lunga sposta poco, e la parte della distribuzione che") + print(" conterebbe non c'e': nel 26,8% dei giorni il libro CHIUDE sul minimo (gap = 0)") + print(" e quei giorni sono proprio i brutti. A leva alta Ξ”/k cala anche perche' i") + print(" drawdown si allungano piu' in fretta di quanto il singolo tuffo cresca.") + + print("\n ⚠️ MA C'E' UN'ECCEZIONE, ed e' quella che decide la frontiera: la PROBABILITA'") + print(" di sfondare una SOGLIA FISSA e' amplificata assai piu' del maxDD stesso,") + print(" perche' vicino alla soglia la densita' e' fitta.") + print(f"\n {'k':>6}{'P(DD>30%) close':>17}{'P(DD>30%) acc.':>16}{'amplificazione':>16}") + for j, k in enumerate(KS): + pc = float((s0["dd"][:, j, 0] > DD_SOGLIA).mean()) + pa = float((s0["dd"][:, j, 1] > DD_SOGLIA).mean()) + amp = (pa / pc) if pc > 0 else float("nan") + print(f" {k:>6.2f}{pc:>17.1%}{pa:>16.1%}" + f"{(f'{amp:.2f}x' if np.isfinite(amp) else 'n.d.'):>16}") + + print("\n IL NUMERO CHE DECIDE β€” L'HAIRCUT DI LEVA IMPLICITO NELLA LENTE:") + print(" 'a quale k close-only corrisponde il rischio di DD che la lente accoppiata") + print(" attribuisce a k?' (interpolazione lineare sulla curva maxDD close-only)") + kk = np.array(KS, float) + print(f"\n {'k proposto':>12}{'maxDD accopp.':>15}{'k close-equiv.':>16}{'haircut':>10}") + for j, k in enumerate(KS): + if k > 2.0: + continue + keq = float(np.interp(dd_acc[j], dd_close, kk)) + print(f" {k:>12.2f}{dd_acc[j]:>15.1%}{keq:>16.2f}{(keq/k - 1):>10.1%}") + + # ------------------------------------------------------------------ (1-bis) sensitivita' + sezione("(1-bis) SENSITIVITA' β€” la stessa misura sulla RECON PURA (SKH canonico, R e m insieme)") + print("\n Se la conclusione regge anche qui, non dipende dalla convenzione di SKH01.") + mu_rec = float(R_rec.mean()) + R_rec_del = R_rec - (1.0 - DP.DELUCK) * mu_rec + s_rec = simula(R_rec_del, m_rec - R_rec, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, + n_paths=1_000, seed=SEED) + print(f"\n {'k':>6}{'maxDD close':>13}{'maxDD accopp.':>15}{'Ξ” APPAIATO':>13}{'Ξ”/k':>9}") + for j, k in enumerate(KS): + if k > 2.0: + continue + dcr, dar = s_rec["dd"][:, j, 0], s_rec["dd"][:, j, 1] + d = float(np.median(dar - dcr)) + print(f" {k:>6.2f}{np.median(dcr):>13.1%}{np.median(dar):>15.1%}{d:>13.2%}{d/k:>9.2%}") + + # ------------------------------------------------------------------ (1-ter) controllo positivo + sezione("(1-ter) CONTROLLO POSITIVO β€” la lente INDIPENDENTE, quella refutata il 25/07") + print("\n Un 'nessuna differenza' va provato: se la mia macchineria non riproduce il VERSO") + print(" del finding del 25/07 (estrarre il gap indipendente dal giorno gonfia i breach),") + print(" allora non ha potenza e i suoi risultati nulli non valgono niente.") + rng = np.random.default_rng(4242) + gap_shuf = gap.copy() + rng.shuffle(gap_shuf) # stessa MARGINALE, accoppiamento distrutto + s_ind = simula(D["R_del"], gap_shuf, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, + n_paths=1_000, seed=SEED) + print(f"\n marginale del gap: accoppiato p50 {np.quantile(gap,0.50):+.2%} / p99 " + f"{np.quantile(gap,0.01):+.2%} Β· rimescolato p50 {np.quantile(gap_shuf,0.50):+.2%} / " + f"p99 {np.quantile(gap_shuf,0.01):+.2%} (IDENTICHE per costruzione)") + print(f"\n {'k':>6}{'maxDD acc. (vero)':>19}{'maxDD indip. (finto)':>22}{'Ξ” APPAIATO':>13}") + s_acc1k = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, + n_paths=1_000, seed=SEED) + for j, k in enumerate(KS): + if k > 2.0: + continue + a_true = s_acc1k["dd"][:, j, 1] + a_ind = s_ind["dd"][:, j, 1] + print(f" {k:>6.2f}{np.median(a_true):>19.1%}{np.median(a_ind):>22.1%}" + f"{float(np.median(a_ind - a_true)):>13.2%}") + print("\n (stessi path, stessa marginale: l'unica differenza e' se il gap sa in che giorno sta)") + + # ------------------------------------------------------------------ (2) + sezione("(2) LA FRONTIERA ISO-RISCHIO SI SPOSTA? β€” quanto k si perde cambiando lente") + print("\n Per ogni budget di rischio: la leva PIU' ALTA ammessa sotto ciascuna lente.") + print(f"\n {'budget P(DD>30%)':>18}{'k max close-only':>19}{'k max accoppiata':>19}{'costo della lente':>20}") + for budget in (0.01, 0.05, 0.10, 0.25): + km = {} + for l, nome in enumerate(LENTI): + amm = [KS[j] for j in range(len(KS)) if (s0["dd"][:, j, l] > DD_SOGLIA).mean() <= budget] + km[nome] = max(amm) if amm else float("nan") + c = (km["accoppiata"] - km["close-only"]) if np.isfinite(km["accoppiata"]) else float("nan") + print(f" {budget:>17.0%}{km['close-only']:>19.2f}{km['accoppiata']:>19.2f}" + f"{(f'{c:+.2f}x' if np.isfinite(c) else 'n.d.'):>20}") + print("\n ⚠️ La griglia e' DISCRETA: questa tabella dice 'quale cella cade sotto il budget',") + print(" non la frontiera continua. La misura fine e' l'haircut di leva della sezione (1).") + + # ------------------------------------------------------------------ (3) + sezione("(3) LE BARRIERE VERE β€” a che leva il MINIMO intraday tocca qualcosa che esiste") + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac, sl = cfg["max_notional_per_asset_frac"], cfg["disaster_sl_pct"] + peggior_min = float((D["R_del"] + gap).min()) + peggior_close = float(D["R_del"].min()) + print(f"\n peggior CHIUSURA storica del libro {peggior_close:+.2%} Β· peggior MINIMO " + f"intraday {peggior_min:+.2%} (rapporto {peggior_min/peggior_close:.2f}x)") + print(f" liquidazione modellata a -{WIPE:.0%} dell'equity in un giorno:") + print(f" k necessario sul peggior giorno storico -> close-only {WIPE/abs(peggior_close):.1f}x" + f" Β· accoppiata {WIPE/abs(peggior_min):.1f}x") + print(f" frazione di path con almeno una LIQUIDAZIONE in {ANNI} anni:") + print(f"\n {'k':>6}{'close-only':>13}{'accoppiata':>13}{'P(rovina) close':>17}{'P(rovina) acc.':>16}") + for j, k in enumerate(KS): + print(f" {k:>6.2f}{s0['wipe'][:, j, 0].mean():>13.2%}{s0['wipe'][:, j, 1].mean():>13.2%}" + f"{s0['ruin'][:, j, 0].mean():>17.2%}{s0['ruin'][:, j, 1].mean():>16.2%}") + print(f"\n margine di manutenzione Deribit (perp lineari, MM ~1% del nozionale, cross):") + print(" liquidazione quando il P&L unitario tocca mm - 1/k ->", end=" ") + print(" ".join(f"k={k:g}: {0.01 - 1/k:+.0%}" for k in (1.0, 1.25, 1.5, 2.0, 5.0))) + print(f" disaster-SL di config: -{sl:.0%} sul MARK del singolo asset, nozionale per asset") + print(f" <= {frac:.0%} dell'equity -> costo massimo {sl*frac:.0%} dell'equity per asset a k=1,") + print(f" cioe' {sl*frac*2:.0%} se scattano entrambi; diventa il -100% a k = {1/(sl*frac*2):.1f}x.") + + # ------------------------------------------------------------------ (3-bis) + sezione("(3-bis) DOVE L'ACCOPPIAMENTO MORDE DAVVERO β€” la regola a UN GIORNO, sui giorni STORICI") + print("\n Il finding del 25/07 ('close-only e' CIECA', 'l'indipendente RADDOPPIA i breach')") + print(" e' misurato su una regola di DAILY-LOSS: una soglia valutata sul MINIMO di UN") + print(" giorno. Il maxDD di un piano d'accumulo e' un'altra grandezza. Qui le due si") + print(" guardano fianco a fianco, sui GIORNI VERI (nessun bootstrap): quante volte in") + print(f" {len(D['R_del'])/365:.1f} anni una soglia di perdita giornaliera viene sfondata.") + Rh, Gh = D["R_del"], gap + rng2 = np.random.default_rng(909) + Gsh = Gh.copy(); rng2.shuffle(Gsh) + anni_st = len(Rh) / 365.0 + for T in (0.03, 0.05, 0.10): + print(f"\n soglia daily-loss {T:.0%} (breach/anno)") + print(f" {'k':>6}{'close-only':>13}{'accoppiata':>13}{'indip. (finto)':>17}" + f"{'acc./close':>12}{'indip./acc.':>13}") + for k in (1.00, 1.25, 1.50, 2.00, 3.00): + nc = float((Rh * k <= -T).sum()) / anni_st + na = float(((Rh + Gh) * k <= -T).sum()) / anni_st + ni = float(((Rh + Gsh) * k <= -T).sum()) / anni_st + r1 = (na / nc) if nc > 0 else float("inf") + r2 = (ni / na) if na > 0 else float("nan") + print(f" {k:>6.2f}{nc:>13.2f}{na:>13.2f}{ni:>17.2f}" + f"{(f'{r1:.2f}x' if np.isfinite(r1) else 'INF'):>12}" + f"{(f'{r2:.2f}x' if np.isfinite(r2) else 'n.d.'):>13}") + print("\n πŸ“Œ IL RISULTATO TRASFERIBILE DEL FILONE: la lezione del 25/07 e' VERA e NON SI") + print(" TRASFERISCE. Su una regola a UN GIORNO l'accoppiamento cambia il conto di un") + print(" fattore (e close-only puo' dare ZERO dove il vero e' positivo: cecita' esatta);") + print(" su una statistica MULTI-GIORNO come il maxDD di un piano d'accumulo lo stesso") + print(" difetto vale un ricarico costante di pochi punti percentuali. Il gate") + print(" pre-registrato ha importato una lezione dal contesto in cui e' stata misurata") + print(" (conti FUNDED, barriera di daily-loss) a un contesto in cui la grandezza") + print(" decisiva e' un'altra β€” stessa forma dell'errore che il progetto ha gia'") + print(" codificato il 26/07 sulla barra parziale (SKH01 +0.38 / TP01 Β±0.03: 'la") + print(" conclusione non si trasferisce fra sleeve').") + print(" ⚠️ Il che NON assolve close-only in generale: sul canale PROP/FUNDED, dove il") + print(" vincolo binding e' esattamente una regola di daily-loss per-conto, la lente") + print(" accoppiata resta obbligatoria e la leva li' e' un'altra decisione.") + + # ------------------------------------------------------------------ (4) + sezione("(4) IL GRADINO ESEGUIBILE β€” sopravvive alla lente? (€500/mese, muro netto)") + s500 = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=500, bersaglio=BERSAGLIO, seed=SEED) + print(f"\n versato in {ANNI} anni: ${s500['versato']:,.0f}. 'anni' = mediana CONDIZIONATA") + print(" all'arrivo (non-arrivo = +infinito, MAI -1), stampata accanto alla sua probabilita'.") + print(f"\n {'k':>6}" + "".join(f"{'anni ' + n:>16}{'P<=20a ' + n[:5]:>15}" for n in LENTI) + + f"{'maxDD acc.':>12}{'P(DD>30) acc.':>15}") + for j, k in enumerate(KS): + cel = "" + for l in range(2): + a, p = anni_cond(s500["hit"][:, j, l]) + cel += f"{fmt(a):>16}{p:>15.0%}" + marca = " <-- LIBRO" if k == 1.00 else "" + print(f" {k:>6.2f}" + cel + f"{np.median(s0['dd'][:, j, 1]):>12.1%}" + f"{(s0['dd'][:, j, 1] > DD_SOGLIA).mean():>15.1%}{marca}") + for l, nome in enumerate(LENTI): + d = s500["hit"][:, KS.index(1.00), l] - s500["hit"][:, KS.index(1.50), l] + fin = np.isfinite(d) + print(f"\n lente {nome:>11}: guadagno APPAIATO 1.00x -> 1.50x = " + f"{float(np.median(d[fin]))/365:.2f} anni (mediana delle differenze, n={fin.sum()})") + + print(f"\n β–Έ tempo trascorso: {time.time()-t_start:.0f}s") + return dict(s0=s0, s500=s500, dd_close=dd_close, dd_acc=dd_acc, delta_p=delta_p, + D=D, ris_e=ris_e, ris_dd=ris_dd, kk=kk) + + +# =========================================================================== +# ATTACCO 2 β€” l'etichetta del rischio +# =========================================================================== +def attacco2() -> None: + from src.data.downloader import load_data + from src.portfolio import sleeves as SL + from src.portfolio.portfolio import weights_tilt_null + from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, realized_vol, + resample_1d, simple_returns, tsmom_blend) + + sezione("(5) ATTACCO 2a β€” LA VOL REALIZZATA DI TP01: verifica INDIPENDENTE") + print("\n Lo scetticato ricostruisce TP01 a mano dentro il proprio script. Qui si legge lo") + print(" sleeve DI PRODUZIONE (`src/portfolio/sleeves._tp01_returns`), che e' la serie con") + print(" cui il libro e' misurato ovunque nel progetto: percorso diverso, stessa domanda.") + tp = SL._tp01_returns().values.astype(float) + att = tp[np.abs(tp) > 1e-12] + print(f"\n vol REALIZZATA di TP01 (sleeve di produzione): {tp.std()*365**.5:.2%}" + f" <- lo scetticato dichiara 12,14%") + print(f" -> VERIFICATO. E il numero non e' un artefatto della sua ricostruzione.") + + print("\n ⚠️ MA IL MECCANISMO CHE DICHIARA SPIEGA CIRCA META' DEL DIVARIO, e la meta'") + print(" che manca porta a una conclusione operativa diversa. Lo scetticato scrive:") + print(" Β«il target vale sulla posizione quando c'e', e TP01 e' long-flatΒ». Misura:") + convs, vols_pred, vols_real, sers, flats = [], [], [], {}, [] + for a in ("BTC", "ETH"): + df = resample_1d(load_data(a, "1h")) + c = df["close"].values.astype(float) + r = simple_returns(c) + t = TrendPortfolio(**CANONICAL) + bpd = t._bpd(df) + vol = realized_vol(r, t.vol_win_days * bpd, bpd * 365.25) + d = np.clip(tsmom_blend(c, tuple(h * bpd for h in t.horizons_days)), 0, None) + tgt = np.asarray(t.target_series(df), float) + held = np.zeros(len(tgt)); held[1:] = tgt[:-1] + net = held * r - t.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0 + sers[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"])) + ok = np.isfinite(d) & np.isfinite(vol) & (vol > 0) + convs.append(float(np.sqrt(np.mean(d[ok] ** 2)))) + flats.append(float((d[ok] == 0).mean())) + # errore del previsore di vol: vol realizzata nei 30g SEGUENTI / vol prevista + fwd = pd.Series(r).rolling(30).std().shift(-30).values * np.sqrt(365.25) + okf = ok & np.isfinite(fwd) + vols_pred.append(float(np.mean(vol[okf]))); vols_real.append(float(np.mean(fwd[okf]))) + print(f"\n {a}: frazione di barre FLAT (direzione 0) {float((d[ok] == 0).mean()):>6.1%}") + print(f" direzione media QUANDO NON FLAT " + f"{float(d[ok][d[ok] > 0].mean()):>6.2f} (tsmom_blend e' la MEDIA di 3 segni: " + f"vale 1/3, 2/3 o 1)") + print(f" fattore di convinzione sqrt(E[dΒ²]) {convs[-1]:>6.3f}" + f" -> da solo porta 20% a {0.20*convs[-1]:.2%}") + print(f" vol prevista (30g trailing) {vols_pred[-1]:>6.2%} vs vol realizzata nei " + f"30g SEGUENTI {vols_real[-1]:>6.2%} (rapporto {vols_real[-1]/vols_pred[-1]:.3f})") + J = pd.concat(sers, axis=1, join="inner").fillna(0.0) + vb, ve = J["BTC"].std() * 365 ** .5, J["ETH"].std() * 365 ** .5 + comb = (0.5 * J["BTC"] + 0.5 * J["ETH"]).std() * 365 ** .5 + div = comb / (0.5 * vb + 0.5 * ve) + conv = float(np.mean(convs)) + per_leg = (0.5 * vb + 0.5 * ve) / 0.20 + p_flat = float(np.mean(flats)) + f_flat = float(np.sqrt(1.0 - p_flat)) + f_conv = conv / f_flat if f_flat > 0 else float("nan") + print(f"\n gamba singola: BTC {vb:.2%} Β· ETH {ve:.2%} -> 50/50 combinato {comb:.2%}") + print(f" fattore di DIVERSIFICAZIONE BTC/ETH = {div:.3f} (corr {J['BTC'].corr(J['ETH']):.2f})") + print(f"\n SCOMPOSIZIONE ESATTA (i due fattori si moltiplicano e chiudono il conto):") + print(f" 20,00% Γ—{per_leg:.3f} (dentro la GAMBA) Γ—{div:.3f} (diversificazione BTC/ETH)" + f" = {0.20*per_leg*div:.2%}") + print(f" misurato sullo sleeve di produzione: {tp.std()*365**.5:.2%}") + print(f"\n E il Γ—{per_leg:.3f} della gamba si spacca a sua volta in DUE meta' quasi uguali:") + print(f" Β· sta FLAT nel {p_flat:.1%} delle barre (direzione 0) -> Γ—{f_flat:.3f}") + print(f" Β· quando NON e' flat sta a convinzione PARZIALE (rms {conv/f_flat:.2f}) -> Γ—{f_conv:.3f}") + print(f" prodotto sqrt(E[dΒ²]) = Γ—{conv:.3f}; il residuo fino a Γ—{per_leg:.3f} e'") + print(f" la correlazione fra convinzione e vol realizzata/prevista" + f" (previsore di vol da solo: Γ—{np.mean(vols_real)/np.mean(vols_pred):.3f}).") + print(f"\n πŸ“Œ QUELLO CHE LO SCETTICATO NON DICE, e sono DUE cose.") + print(f" (i) Β«TP01 e' long-flatΒ» spiega **circa META'** del divario (Γ—{f_flat:.2f}), non") + print(f" il divario. L'altra meta' e' che `tsmom_blend` e' la MEDIA DEI SEGNI di 3") + print(f" orizzonti: quando TP01 e' a mercato sta quasi sempre a 1/3 o 2/3 di") + print(f" convinzione (Γ—{f_conv:.2f}). E la differenza NON e' accademica: se il divario") + print(f" fosse solo 'flat', alzare target_vol non toccherebbe i giorni senza") + print(f" posizione; essendo CONVINZIONE, alzarlo aumenta la size proprio nei giorni") + print(f" in cui il segnale e' PIU' DEBOLE (1 orizzonte su 3 d'accordo).") + print(f" (ii) un terzo fattore non sta nemmeno nello sleeve: Γ—{div:.2f} viene dal") + print(f" COMBINARE BTC ed ETH a 50/50 (corr {J['BTC'].corr(J['ETH']):.2f}). Non e' un") + print(f" difetto d'etichetta: e' diversificazione, ed e' esattamente cio' che si") + print(f" vuole. Chiamare 'sovrastima del rischio' anche questo pezzo confonde una") + print(f" proprieta' desiderata con un errore di misura.") + + print("\n E TP01 E' UN CASO PARTICOLARE, non la regola β€” gli altri sleeve vol-targeted:") + print(f"\n {'sleeve':>9}{'target dichiarato':>19}{'vol REALIZZATA':>16}{'su barre ATTIVE':>17}{'attive':>9}") + for nm, fn, dic in (("TP01", SL._tp01_returns, "20%"), ("XS01", SL._xsec_returns, "20%"), + ("VRP01", SL._vrp_combo_returns, "β€”"), ("SKH01", SL._skyhook_returns, "nessuno"), + ("GTAA01", SL._gtaa_daily_returns, "β€”")): + v = fn().values.astype(float) + a = v[np.abs(v) > 1e-12] + print(f" {nm:>9}{dic:>19}{v.std()*365**.5:>16.2%}{a.std()*365**.5:>17.2%}" + f"{len(a)/len(v):>9.1%}") + print("\n XS01 dichiara lo STESSO 20% e ne realizza 20,59%: il divario non e' una") + print(" proprieta' del vol-targeting, e' una proprieta' di TP01 (blend di segni +") + print(" long-only + due gambe correlate 0,8). L'affermazione 'l'etichetta sovrastima il") + print(" rischio' e' vera per TP01 e FALSA per il resto del libro.") + + # -------------------------------------------------------------- 2b + sezione("(6) ATTACCO 2b β€” `target_vol` e `k` sono la STESSA leva? (misurato, non dedotto)") + print("\n Non e' ovvio: `target_vol` agisce DENTRO il vol-targeting di TP01 (e interagisce") + print(" col leverage cap 2x), `k` agisce FUORI su tutto il libro. Due differenze") + print(" strutturali attese, e la seconda e' quella che conta.") + skh = SL._skyhook_returns() + + def libro(tv: float, lev: float = 2.0) -> tuple[pd.Series, float, float]: + """(serie di libro 0.75*TP01(tv)+0.25*SKH01, scala ottenuta su TP01, quota barre al cap)""" + ser, atcap = {}, [] + for a in ("BTC", "ETH"): + df = resample_1d(load_data(a, "1h")) + t = TrendPortfolio(**{**CANONICAL, "target_vol": tv, "leverage": lev}) + tgt = np.nan_to_num(np.asarray(t.target_series(df), float)) + r = simple_returns(df["close"].values.astype(float)) + held = np.zeros(len(tgt)); held[1:] = tgt[:-1] + net = held * r - t.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0 + ser[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"])) + atcap.append(float((np.abs(tgt) >= lev - 1e-9).mean())) + Jt = pd.concat(ser, axis=1, join="inner").fillna(0.0) + tp_s = pd.Series(0.5 * Jt["BTC"].values + 0.5 * Jt["ETH"].values, index=Jt.index) + if tp_s.index.tz is None: + tp_s.index = tp_s.index.tz_localize("UTC") + sk = skh.copy() + if sk.index.tz is None: + sk.index = sk.index.tz_localize("UTC") + B = pd.concat({"TP01": tp_s, "SKH01": sk}, axis=1, join="inner").fillna(0.0) + return (W_TP * B["TP01"] + W_SKH * B["SKH01"], float(tp_s.std() / 1.0), + float(np.mean(atcap))) + + base_s, tp_vol_base, _ = libro(0.20) + print(f"\n {'via':>28}{'scala TP01':>12}{'vol libro':>11}{'drift':>9}{'Sharpe':>8}" + f"{'maxDD':>8}{'peso EFFETTIVO TP01/SKH':>26}{'barre al cap':>14}") + + def riga(nome: str, s: pd.Series, scala_tp: float, atcap: float, w_eff: str) -> None: + v = s.values.astype(float) + eq = np.cumprod(1 + v) + dd = float((1 - eq / np.maximum.accumulate(eq)).max()) + print(f" {nome:>28}{scala_tp:>12.2f}{v.std()*365**.5:>11.2%}{v.mean()*365:>9.2%}" + f"{v.mean()/v.std()*365**.5:>8.2f}{dd:>8.1%}{w_eff:>26}{atcap:>14.1%}") + + riga("k=1.00, target_vol=20% (LIBRO)", base_s, 1.00, 0.0, "75 / 25") + for tv in (0.25, 0.30, 0.40, 0.60): + s, tpv, ac = libro(tv) + sc = tv / 0.20 + w = W_TP * sc / (W_TP * sc + W_SKH) + riga(f"target_vol={tv:.0%} (solo TP01)", s, sc, ac, f"{w*100:.0f} / {(1-w)*100:.0f}") + for k in (1.25, 1.50, 2.00, 3.00): + riga(f"k={k:.2f} (tutto il libro)", base_s * k, k, 0.0, "75 / 25") + + print("\n ❌ NON SONO LA STESSA LEVA, e la ragione NON e' il leverage cap (che infatti non") + print(" morde: le barre al cap restano sotto qualche punto percentuale). E':") + print(" **`target_vol` scala il 75% del libro e lascia fermo il 25%** -> e'") + print(" simultaneamente una LEVA e un TILT DI PESI. A target_vol=30% il libro non e'") + print(" piu' 75/25 ma ~82/18, e a 60% e' ~90/10.") + print(" Un tilt di pesi in questo progetto ha un gate suo, e non e' opzionale:") + + dcols = {"TP01": SL._tp01_returns(), "SKH01": skh} + for nm, s in dcols.items(): + if s.index.tz is None: + dcols[nm] = s.tz_localize("UTC") + for tv in (0.30, 0.60): + sc = tv / 0.20 + w = W_TP * sc / (W_TP * sc + W_SKH) + try: + g = weights_tilt_null(dcols, {"TP01": W_TP, "SKH01": W_SKH}, + {"TP01": round(w, 4), "SKH01": round(1 - w, 4)}, n=300) + print(f" weights_tilt_null 75/25 -> {w*100:.0f}/{(1-w)*100:.0f} " + f"(target_vol {tv:.0%}): gate_pass={g.get('gate_pass')} " + f"delta_insample={g.get('delta_insample'):+.4f} pctl_hold={g.get('pctl_hold'):.0f}") + except Exception as e: # noqa: BLE001 + print(f" weights_tilt_null non girato per tv={tv:.0%}: {type(e).__name__}: {e}") + + print("\n πŸ“Œ CONSEGUENZA CHE LO SCETTICATO NON POTEVA VEDERE dalla sua sezione (5b), che") + print(" guardava TP01 DA SOLO: la sua raccomandazione implicita (Β«il knob target_vol") + print(" funziona, l'ostacolo e' la configΒ») e' vera sullo sleeve e FALSA sul libro.") + print(" Alzare `target_vol` NON e' la strada meno invasiva per alzare k: e' la strada") + print(" che aggiunge un cambio di pesi mai giudicato al cambio di scala.") + print(" La strada pulita per un puro cambio di SCALA e' il cap di config, che e'") + print(" esattamente quella protetta dal test di guardia.") + + +def attacco2b() -> None: + """Quali affermazioni pubblicate cambiano, e quali no β€” misurato, non argomentato.""" + from src.portfolio import sleeves as SL + sezione("(6-bis) ATTACCO 2c β€” QUALI AFFERMAZIONI CAMBIANO (e la maggior parte NON cambia)") + tp, sk = SL._tp01_returns(), SL._skyhook_returns() + if tp.index.tz is None: + tp = tp.tz_localize("UTC") + if sk.index.tz is None: + sk = sk.tz_localize("UTC") + B = pd.concat({"TP01": tp, "SKH01": sk}, axis=1, join="inner").fillna(0.0) + w = np.array([W_TP, W_SKH]) + X = B[["TP01", "SKH01"]].values + bk = X @ w + cov = np.cov(X.T) + var_b = float(bk.var()) + rc = w * (cov @ w) / (cov @ w @ w) + print("\n NON cambia NIENTE di misurato. Ogni Sharpe, maxDD, muro, traiettoria e rendita") + print(" pubblicati sono calcolati sulla SERIE DI RITORNI REALIZZATA, mai sull'etichetta:") + print(" `target_vol` entra nel codice come parametro di sizing e non compare in nessuna") + print(" formula di rischio a valle. Un'etichetta che sovrastima non rende ottimista un") + print(" numero che non la usa.") + print("\n CAMBIA una classe di affermazioni sola: quelle che CONFRONTANO un parametro con") + print(" una misura. Nel progetto ce n'e' una, ed e' nel libro LIVE:") + print(f"\n {'sleeve':>8}{'peso NOZIONALE':>16}{'vol realizzata':>16}" + f"{'contributo al RISCHIO':>23}") + for i, nm in enumerate(("TP01", "SKH01")): + print(f" {nm:>8}{w[i]:>16.0%}{X[:, i].std()*365**.5:>16.2%}{rc[i]:>23.1%}") + print(f" {'libro':>8}{1.0:>16.0%}{bk.std()*365**.5:>16.2%}{sum(rc):>23.1%}") + print(f" corr TP01/SKH01 {np.corrcoef(X.T)[0,1]:+.3f} vol libro {bk.std()*365**.5:.2%}") + sig = X.std(axis=0) * 365 ** 0.5 + cf = (W_TP * 0.20) ** 2 / ((W_TP * 0.20) ** 2 + (W_SKH * sig[1]) ** 2) + print("\n ❌ IPOTESI MIA, REFUTATA DAL MIO STESSO NUMERO. Mi aspettavo che l'etichetta") + print(" nascondesse una ripartizione del rischio molto diversa dai pesi di nozionale.") + print(f" Non e' cosi': {rc[0]*100:.0f}/{rc[1]*100:.0f} contro {W_TP*100:.0f}/{W_SKH*100:.0f}" + f" β€” coincidono entro un paio di punti, e per una") + print(" ragione che si legge in una riga: contributi di rischio ~ (wΒ·Οƒ)Β² con corr") + print(f" {np.corrcoef(X.T)[0,1]:+.2f} ~ 0, e {W_TP:.2f}Γ—{sig[0]:.1%} = {W_TP*sig[0]:.2%} contro" + f" {W_SKH:.2f}Γ—{sig[1]:.1%} = {W_SKH*sig[1]:.2%}") + print(" stanno gia' in rapporto ~75/25. **Coincidenza numerica, non struttura.**") + print("\n πŸ“Œ IL FATTO CHE RESTA, ed e' piu' utile dell'ipotesi che ho perso: l'errore non") + print(" lo commette chi legge i PESI, lo commette chi legge l'ETICHETTA. Un lettore che") + print(f" prendesse `target_vol=20%` per buono calcolerebbe TP01 al {cf*100:.0f}% del rischio") + print(f" di libro; il vero e' {rc[0]*100:.0f}%. L'etichetta non rende ottimista nessun numero") + print(" pubblicato (nessuno la usa) β€” sposta di ~16 punti l'ATTRIBUZIONE del rischio,") + print(" cioe' proprio la domanda 'quale gamba mi fa male' su cui il progetto ha gia'") + print(" tre misure indipendenti che indicano SKH01 come la fragile (LOO d'ancora,") + print(" sensibilita' alla fee 4x, vol realizzata piu' alta).") + print("\n E si precisa una frase: Β«SKH01 e' l'unica delle 5 a NON essere vol-targetedΒ» e'") + print(" esatta sul CODICE e va letta insieme al fatto che e' anche quella che di vol ne") + print(" realizza di PIU' (21,0% contro 12,1%). Non e' una contraddizione: e' il motivo per") + print(" cui il peso 25% e' stato confermato tre volte all'estremo prudente.") + + +def guardia() -> None: + sezione("(7) IL VINCOLO DURO β€” cosa protegge davvero il test di guardia") + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac = cfg["max_notional_per_asset_frac"] + print(f""" + `config/live.json`: max_notional_per_asset_frac = {frac} su 2 asset (BTC, ETH) + -> nozionale LORDO massimo = {frac*2:.2f}x l'equity. k > {frac*2:.2f}x oggi NON e' eseguibile. + `tests/test_fee_sensitivity.py::test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` + asserisce `frac * len(ASSETS) <= 1.0` E `disaster_sl_pct > 0`. + ⚠️ Cio' che protegge NON e' "la leva e' pericolosa": e' la CONCLUSIONE PUBBLICATA del + 26/07 secondo cui la **liquidation fee 1%** introdotta da Deribit dal 2026-08-01 e' + irrilevante per questo libro. Quella conclusione poggia interamente sul fatto che a + nozionale lordo <= 1x servirebbe un movimento avverso ~100% per liquidare. Se il cap + sale, la liquidation fee torna nel conto e `r0726_fee_sensitivity` va rifatto. + Il test e' quindi un promemoria di DIPENDENZA, non un divieto: chi alza il cap deve + rifare quell'analisi, non aggirare l'assert. + Nota di taglia, che vale piu' del cap: il conto e' ~$635 e `min_order_usd` = {cfg['min_order_usd']}. + Alzare k moltiplica anche il TURNOVER, e il progetto ha gia' misurato (02/07) che a + questo capitale il min-order $5 e' GIA' la banda di ribilanciamento ottimale.""") + + +if __name__ == "__main__": + main() + attacco2() + attacco2b() + guardia() + print(f"\nβ–Έ totale: consegna nel messaggio dell'agente.") diff --git a/scripts/research/r0822_monitor_audit.py b/scripts/research/r0822_monitor_audit.py new file mode 100644 index 0000000..9cf2f57 --- /dev/null +++ b/scripts/research/r0822_monitor_audit.py @@ -0,0 +1,895 @@ +"""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.5) + 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, e cosa e' andato diversamente: + * i tre monitor giornalieri su feed crypto (xsr, statarb, dvolspread) con la STESSA patologia, + intorno ai ~35 minuti al giorno di XSR01 -> **meta' giusto**: la patologia e' la stessa ma la + TAGLIA no (2, 4 e 41 min/giorno). Un libro dollar-neutral perde molto piu' di un libro + direzionale a parita' di troncamento, perche' su 35 minuti le due gambe si muovono insieme; + * `paper_prevday` MOLTO piu' mite (1 barra su 24) -> **confermato** (1.438 min/giorno su 1.440), + con un dettaglio non previsto: il difetto si propaga alla barra successiva quando il bersaglio + calcolato sulla barra parziale differisce; + * `paper_combo` rotto o sano per caso -> **sano, e per caso**: aspetta la chiusura di una BORSA; + * NON previsto: le uniche barre corrette di `paper_statarb` esistono perche' il feed si ROMPE + per 7 giorni a luglio e il monitor le recupera CHIUSE. Sono anche la prova che serviva per + dire che la finestra e' ricostruibile. + + 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, mask: np.ndarray | None = None) -> dict: + ok = np.isfinite(rec) & np.isfinite(rep) + if mask is not None: + ok &= mask + 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(T: pd.DatetimeIndex, rec: np.ndarray, rep: np.ndarray, note: str = "") -> dict: + """Stampa il confronto e ritorna la statistica sulla finestra PULITA (senza l'incidente).""" + c = compare(rec, rep) + fuori = ~((T >= FREEZE[0]) & (T <= FREEZE[1])) + cf = compare(rec, rep, mask=fuori) + 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}") + if cf["n"] != c["n"]: + print(f" ESCLUSO l'incidente del feed 09-15/07 ({c['n'] - cf['n']} barre recuperate a " + f"posteriori, quindi CHIUSE):") + print(f" barre {cf['n']} identiche {cf['ident']}/{cf['n']} corr {cf['corr']:+.4f}" + f" vol reg. {cf['sd_rec']:.2f}% vs ric. {cf['sd_rep']:.2f}%" + f" -> ~{cf['minuti']:,.0f} min/giorno") + 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]") + return dict(all=c, clean=cf) + + +RES: dict[str, dict] = {} # riepilogo per la tabella finale + +# -------------------------------------------------------------------------------------------- +# L'INCIDENTE DEL FEED (2026-07-09 -> 2026-07-15). Non e' un'assunzione: e' documentato nel +# sorgente di produzione (`rebuild_history.py`, commento "incidente 2026-07-15: feed fermo 7g" β€” +# `shutil.copy2` del backup falliva con EPERM e ABORTIVA il rebuild) ed e' verificabile nel log +# del cron. Conta qui per una ragione precisa: durante il congelamento i monitor NON trovavano +# barre nuove, e al ripristino hanno recuperato in un colpo solo barre gia' CHIUSE β€” cioe' le +# uniche barre corrette delle loro serie. Vanno escluse da ogni stima del difetto, o si finisce +# per attribuire al difetto un'attenuazione che viene da un guasto diverso. +FREEZE = (pd.Timestamp("2026-07-09", tz="UTC"), pd.Timestamp("2026-07-16", tz="UTC")) +CRONLOG = ROOT / "logs" / "cron_daily.log" + + +def freeze_evidence() -> dict[str, int]: + """Giorni in cui `rebuild_history` e' ABORTITO nel cron (letto dal log, non assunto).""" + out: dict[str, int] = {} + day = None + lines = CRONLOG.read_text(errors="replace").splitlines() if CRONLOG.exists() else [] + for i, ln in enumerate(lines): + if ln.startswith("===== 2026-") and "cron_daily" in ln: + day = ln.split()[1][:10] + if day and "rebuild_history.py" in ln and "Traceback" in "".join(lines[max(0, i - 3):i]): + out[day] = out.get(day, 0) + 1 + return out + + +# ============================================================================================ +# 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 = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])]) + for r in rec if int(r["ts"]) in rep] + T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True)) + print("\n pannello: `build_panel` -> `load_hl` -> parquet HL 1d (ultima riga = giorno in corso)") + print(f" inception {T[0].date()} = DOPO l'incidente del feed: la finestra e' gia' pulita.") + out = show(T, np.array([p[1] for p in pairs]), np.array([p[2] for p in pairs])) + RES["paper_xsr"] = dict(parziale="SI", cmp=out["clean"], gate="XSR01 2026-10-23", + src="hl_*_1d.parquet", cad_h=24.0, ric="SI bit-exact", + rec=rec, rep=sink.of("returns.jsonl")) + + +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 = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])]) + for r in rec if int(r["ts"]) in rep] + T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True)) + A = np.array([p[1] for p in pairs]); B = np.array([p[2] for p in pairs]) + print("\n pannello: `build_joint('1d')` -> `altlib.get(...,'1d')` -> `resample_tf` " + "(ultima riga = 1 barra 1h su 24)") + out = show(T, A, B) + ident = np.abs(A - B) <= 1e-6 + if ident.any(): + gg = sorted({str(x.date()) for x in T[ident]}) + print(f"\n πŸ“Œ LE UNICHE BARRE CORRETTE DELLA SERIE VENGONO DA UN GUASTO: le {int(ident.sum())} " + f"identiche\n sono CONSECUTIVE ({gg[0]} -> {gg[-1]}) e cadono dentro l'incidente " + "del feed.") + ev = sorted(freeze_evidence()) + print(f" Prova indipendente dal log del cron: `rebuild_history` e' abortito in " + f"{len(ev)} giri consecutivi") + print(f" ({ev[0]} -> {ev[-1]}), EPERM sul backup β€” difetto gia' riparato nel sorgente.") + print(" Col feed fermo il monitor non trovava barre nuove; al ripristino ne ha") + print(" recuperate 7 in un colpo solo, di cui 6 GIA' CHIUSE -> coincidono al bit.") + print(" Due conseguenze: (a) il 'min/giorno' globale e' GONFIATO da quelle barre β€” la") + print(" riga 'ESCLUSO l'incidente' e' la stima onesta; (b) quelle 6 barre sono la prova") + print(" che, su barre chiuse, il codice di produzione riproduce il replay ESATTAMENTE.") + RES["paper_statarb"] = dict(parziale="SI", cmp=out["clean"], gate="STATARB 2026-09-27", + src="btc/eth 1h -> resample 1d", cad_h=24.0, ric="SI bit-exact", + rec=rec, rep=sink.of("returns.jsonl")) + + +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 = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])]) + for r in rec if int(r["ts"]) in rep] + T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True)) + print("\n pannello: `_panel` -> `ortholib.aligned` -> `altlib.get(...,'1d')` -> `resample_tf`") + print(f" inception {T[0].date()} = dopo l'incidente del feed: finestra gia' pulita, " + "nessuna barra recuperata.") + out = show(T, np.array([p[1] for p in pairs]), np.array([p[2] for p in pairs])) + print("\n ⚠ E' IL PEGGIORE DEI SEI, e non e' un caso: il libro e' uno SPREAD BTC-ETH e su 35") + print(" minuti le due gambe si muovono quasi insieme -> la parte idiosincratica, che e' cio'") + print(" che questo libro tenta di catturare, e' quasi tutta ancora da venire quando la barra") + print(" viene letta. Un libro dollar-neutral perde piu' di un libro direzionale a parita' di") + print(" troncamento β€” il numero di sopra non e' 35/1440 ma un ordine di grandezza sotto.") + 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=out["clean"], + gate="DVOLSPREAD 2026-10-24 / 2027-01-24", + src="btc/eth 1h -> resample 1d + dvol", cad_h=24.0, + ric="SI bit-exact", rec=rec, rep=sink.of("returns.jsonl")) + + +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]) + T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True)) + print("\n pannello: `build_bars` -> `harness.load(asset,'1h')` = il parquet 1h GREZZO, " + "nessun resample") + out = show(T, A, B, note=" <- 24 barre/giorno, quindi il conto e' sull'INTERA giornata") + + diff = np.abs(A - B) > 1e-6 + print(f"\n DOVE cade la divergenza: {int(diff.sum())} barre su {len(A)} " + f"({diff.mean() * 100:.1f}%), |scarto| mediano " + f"{np.median(np.abs(A - B)[diff]):.2e} su |ritorno| mediano {np.median(np.abs(B)):.2e}") + vc = pd.Series(T.hour[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())) + g = pd.DataFrame(dict(day=T.date, d=diff)).groupby("day")["d"].sum() + z, uno, due = (g == 0), (g == 1), (g == 2) + print(f" per giorno: {int(uno.sum())} giorni con 1 barra divergente, {int(due.sum())} con 2, " + f"{int(z.sum())} con 0 (totale {len(g)} giorni)") + print(f" i {int(due.sum())} giorni con DUE sono sempre 00:00 + 01:00 = il difetto si PROPAGA " + "una barra avanti,") + print(" perche' il bersaglio calcolato sulla barra parziale e' la posizione TENUTA in quella") + print(" dopo. (Il docstring di `prevday_breakout.current_target` dice 'ultima barra chiusa':") + print(" quinto docstring di produzione che dichiara una causalita' che il codice non ha.)") + zg = [str(x) for x in g[z].index] + print(f" i {int(z.sum())} giorni con ZERO divergenze: {', '.join(zg)}") + print(" = inception + oggi + i 7 giorni dell'incidente del feed (barre recuperate CHIUSE).") + print(f"\n ⚠ LA META' CHE CONTA DI PIU': {int((~diff).sum())} barre su {len(A)} coincidono BIT " + "A BIT col replay") + print(" fatto oggi, dopo 62 notti di riscrittura del parquet. E' la PROVA che il feed BTC/ETH") + print(" certificato non tocca le barre gia' chiuse β€” senza la quale la ricostruzione della") + print(" sezione E sarebbe un'ipotesi invece di un fatto.") + print(" πŸ“Œ E il verdetto operativo e' l'opposto degli altri quattro: su griglia oraria il difetto") + print(" c'e' ma vale ~0.1% della varianza. `paper_prevday` NON e' da rigenerare.") + RES["paper_prevday"] = dict(parziale="SI", cmp=out["clean"], gate="nessuno (lead in monitor)", + src="btc/eth 1h grezzo", cad_h=1.0, ric="SI bit-exact") + + +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, D = [], [], [] + 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]) + D.append(d) + T = pd.DatetimeIndex(D) + 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)") + A = np.array(A); B = np.array(B) + out = show(T, A, B) + ident = np.abs(A - B) <= 1e-6 + zeri = ident & (np.abs(B) <= 1e-12) + if ident.any(): + print(f"\n ⚠ TRAPPOLA DA NON PRENDERE: delle {int(ident.sum())} barre 'identiche', " + f"{int(zeri.sum())} sono ZERO contro ZERO") + print(" (" + ", ".join(str(x.date()) for x in T[zeri]) + ")") + print(" = giorni in cui TUTTI e cinque gli sleeve erano flat insieme (TP01 risk-off,") + print(" GTAA a borsa chiusa nel weekend, VRP fuori scadenza).") + print(" Contarle come 'coincidono' sarebbe la trappola gia' codificata dal progetto β€”") + print(" barre ATTIVE, non barre di calendario (26/07, il gate che segnalo' VRP01 perche'") + print(" contava il 94% di zeri). Su barre attive le coincidenze qui sono " + f"{int((ident & ~zeri).sum())}.") + 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.") + print(" ⚠ Ricostruzione NON bit-exact per questo monitor: uno dei cinque sleeve e' GTAA01, che") + print(" legge `eq_*_1d` da IB con `ADJUSTED_LAST` β€” ri-aggiustato all'indietro a ogni stacco") + print(" di dividendo (difetto gia' incontrato il 07/08 su TLT). La finestra si ricostruisce,") + print(" ma la parte equity non tornera' identica al centesimo, e va detto.") + RES["paper_portfolio"] = dict(parziale="SI", cmp=out["clean"], + gate="nessuno (dashboard/book di ricerca)", + src="5 sleeve, il piu' recente su 1h->1d e HL 1d", cad_h=24.0, + ric="SI non al bit") + + +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, D = [], [], [] + 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]) + D.append(d) + A = np.array(A); B = np.array(B); T = pd.DatetimeIndex(D) + print("\n serie: `combo_daily()` = TP01 compoundato sulla griglia dei GIORNI DI BORSA + GTAA") + out = show(T, A, B) + dif = np.abs(A - B) + print(f"\n Le barre non identiche NON sono il difetto della barra parziale: |scarto| mediano " + f"{np.median(dif):.2e}\n contro |ritorno| mediano {np.median(np.abs(B)):.2e} " + f"(= {np.median(dif) / np.median(np.abs(B)) * 100:.0f}% del ritorno tipico, massimo " + f"{dif.max():.2e}) e si concentrano\n nella finestra dell'incidente del feed + nei giorni " + "in cui IB ha ri-aggiustato `ADJUSTED_LAST`.") + 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=out["clean"], gate="nessuno (paper cross-venue)", + src="TP01 1h->1d su griglia IB", cad_h=24.0, + ric="SI non al bit") + + +# ============================================================================================ +# 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.5"), + "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("\n Le colonne sono calcolate ESCLUDENDO l'incidente del feed 09-15/07 (le barre") + print(" recuperate a posteriori sono chiuse e attenuerebbero il difetto per la ragione") + print(" sbagliata). 'min/g' = minuti di mercato al giorno effettivamente misurati, su 1440.") + print(f"\n {'monitor':<19}{'barra parz.':>13}{'ident./tot':>13}{'corr':>9}{'min/g':>8}" + f"{'ricostruibile':>17} gate dipendente") + for k, v in RES.items(): + c = v["cmp"] + ratio = "{}/{}".format(c["ident"], c["n"]) + print(f" {k:<19}{v['parziale']:>13}{ratio:>13}{c['corr']:>+9.3f}" + f"{c['minuti']:>8,.0f}{v['ric']:>17} {GATE_INFO[k][0]}") + print("\n 'ricostruibile' = la serie CORRETTA sulla stessa finestra forward si puo' ricalcolare") + print(" oggi dai feed su disco: e' letteralmente la colonna 'replay' di questo script.") + print(" 'non al bit' = ricostruibile, ma una gamba viene dal feed equity IB, che e'") + print(" ri-aggiustato all'indietro (`ADJUSTED_LAST`) -> i centesimi non torneranno uguali.") + print("\n LETTURA IN UNA RIGA: quattro monitor su sei registrano fra 2 e 41 minuti di mercato") + print(" al giorno spacciandoli per 24 ore; i due che non lo fanno sono quello a griglia oraria") + print(" (dove il difetto vale una barra su 24) e quello che aspetta la chiusura di una BORSA.") + + +def sezione_C2(): + """Il criterio di ciascun gate PRE-REGISTRATO resta leggibile? Le soglie NON sono ridichiarate + qui: si IMPORTANO dagli script di gate e dal monitor (regola del progetto β€” un sorvegliante + deriva il proprio bersaglio dal codice sorvegliato, mai lo ridichiara: 21/08, `fee_watch`).""" + print("\n" + "=" * 102) + print(" C2 β€” IL CRITERIO DEI TRE GATE PRE-REGISTRATI E' ANCORA LEGGIBILE?") + print("=" * 102) + import r0724_statarb_deploy_gate as GS # noqa: E402 (solo costanti: ha il main-guard) + import r0725_xsr_deploy_gate as GX # noqa: E402 + pv = live("paper_dvolspread") + + def metriche(rows: list[dict], key_m="net_modeled", key_r=None): + rm = np.array([float(r[key_m]) for r in rows]) + eq = np.cumprod(1 + rm) + pk = np.maximum.accumulate(eq) + out = dict(sh=float(rm.mean() / rm.std() * np.sqrt(365)) if rm.std() > 0 else 0.0, + dd=float(np.max((pk - eq) / pk)), tot=float(np.prod(1 + rm) - 1)) + if key_r: + rr = np.array([float(r.get(key_r, np.nan)) for r in rows]) + rr = rr[np.isfinite(rr)] + out["tot_r"] = float(np.prod(1 + rr) - 1) if len(rr) else float("nan") + return out + + def coppia(name): + rec = RES[name]["rec"] + rep = {int(r["ts"]): r for r in RES[name]["rep"]} + keep = [r for r in rec if int(r["ts"]) in rep] + return keep, [rep[int(r["ts"])] for r in keep] + + print(f"\n {'gate':<26}{'metrica primaria':<26}{'soglia':>10}{'REGISTR.':>11}" + f"{'RICOSTR.':>11} verdetto") + + # --- STATARB ------------------------------------------------------------------------- + a, b = coppia("paper_statarb") + ma, mb = metriche(a, key_r="net_real"), metriche(b, key_r="net_real") + flip = (ma["sh"] >= GS.SH_DEPLOY) != (mb["sh"] >= GS.SH_DEPLOY) + print(f" {'STATARB ' + str(GS.DECISION):<26}{'Sharpe net_modeled':<26}" + f"{'>= ' + str(GS.SH_DEPLOY):>10}{ma['sh']:>+11.2f}{mb['sh']:>+11.2f} " + f"{'*** SI RIBALTA ***' if flip else 'stesso'}") + print(f" {'':<26}{'maxDD':<26}{'< ' + f'{GS.DD_MAX:.0%}':>10}{ma['dd']:>10.1%}" + f"{mb['dd']:>11.1%} " + f"{'*** SI RIBALTA ***' if (ma['dd'] < GS.DD_MAX) != (mb['dd'] < GS.DD_MAX) else 'stesso'}") + hp_a = abs((1 + ma["tot"]) - (1 + ma["tot_r"])) * 100 + hp_b = abs((1 + mb["tot"]) - (1 + mb["tot_r"])) * 100 + print(f" {'':<26}{'fill-haircut cum (pp)':<26}{'< ' + str(GS.HAIRCUT_MAX_PP):>10}" + f"{hp_a:>11.2f}{hp_b:>11.2f} " + f"{'*** SI RIBALTA ***' if (hp_a < GS.HAIRCUT_MAX_PP) != (hp_b < GS.HAIRCUT_MAX_PP) else 'stesso'}") + + # --- XSR01 --------------------------------------------------------------------------- + a, b = coppia("paper_xsr") + K5 = "net_REAL-$5000" + ma, mb = metriche(a, key_r=K5), metriche(b, key_r=K5) + hc_a = (ma["tot"] - ma["tot_r"]) / abs(ma["tot"]) if ma["tot"] else float("nan") + hc_b = (mb["tot"] - mb["tot_r"]) / abs(mb["tot"]) if mb["tot"] else float("nan") + fl1 = (ma["sh"] >= GX.SH_DEPLOY) != (mb["sh"] >= GX.SH_DEPLOY) + fl2 = (hc_a <= GX.HAIRCUT_MAX) != (hc_b <= GX.HAIRCUT_MAX) + print(f" {'XSR01 ' + str(GX.DECISION):<26}{'Sharpe net_modeled':<26}" + f"{'>= ' + str(GX.SH_DEPLOY):>10}{ma['sh']:>+11.2f}{mb['sh']:>+11.2f} " + f"{'*** SI RIBALTA ***' if fl1 else 'stesso'}") + print(f" {'':<26}{'haircut $5.000':<26}{'<= ' + f'{GX.HAIRCUT_MAX:.0%}':>10}" + f"{hc_a:>10.1%}{hc_b:>11.1%} {'*** SI RIBALTA ***' if fl2 else 'stesso'}") + + # --- DVOLSPREAD ---------------------------------------------------------------------- + a, b = coppia("paper_dvolspread") + ma, mb = metriche(a), metriche(b) + fl = (ma["sh"] < pv.KILL_SHARPE) != (mb["sh"] < pv.KILL_SHARPE) + print(f" {'DVOLSPREAD ' + pv.KILL_DATE:<26}{'Sharpe fwd (kill se sotto)':<26}" + f"{'> ' + f'{pv.KILL_SHARPE:+.2f}':>10}{ma['sh']:>+11.2f}{mb['sh']:>+11.2f} " + f"{'*** SI RIBALTA ***' if fl else 'stesso (KILL in entrambe)'}") + + print(""" + ⚠ QUESTA NON E' UNA LETTURA DEI GATE, e non deve diventarlo: le tre date sono nel futuro + (anticiparle sarebbe selezione-sul-forward, contro la regola del progetto) e con 28-54 barre + l'errore standard di uno Sharpe annualizzato e' 2,6-3,6. Serve a una cosa sola, e la mostra: + il difetto NON e' rumore centrato β€” sposta la metrica primaria abbastanza da attraversare la + soglia. Su STATARB la serie registrata dice +1,96 (sopra la soglia di deploy 0,5) e quella + ricostruita dice -2,02: **stesso codice, stessa finestra, verdetto opposto**. + ⚠ E c'e' un secondo segnale, gratis: uno Sharpe registrato di -15,8 su 28 barre e' IMPOSSIBILE + (|Sharpe| oltre 4 SE). Il gate `implausible_sharpe` esiste gia' in `altlib` dal 26/07 ma + nessuno lo punta sulle serie FORWARD β€” solo sui candidati. Un numero fuori scala e' un + rilevatore di guasti a costo zero, e in questo caso era li' da leggere da settimane.""") + + +# ============================================================================================ +# 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}") + rotti = sum(1 for v in RES.values() if v["parziale"] == "SI") + print(f" -> segnala {rotti - len(miss)} dei {rotti} monitor che leggono una barra parziale,") + print(f" NON segnala il sano (`paper_combo`, 0.987), e MANCA {len(miss)}: " + 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("\n IPOTESI PESSIMISTA β€” si ripara `advance()` e si fa `--reset` (la finestra riparte da") + print(" zero oggi). Con una barra al giorno, tornare a N barre richiede N giorni:") + print(f"\n {'monitor / gate':<42}{'barre':>7}{'data gate':>13}{'reset -> N barre':>18}" + f"{'ritardo':>10}") + ritardi = [] + for mon, nome, gate, ser in righe: + n = len(read_jsonl(ROOT / "data" / mon / ser)) + rip = oggi + pd.Timedelta(days=n) + rit = (rip - gate).days + ritardi.append((nome, rit)) + print(f" {f'{mon} / {nome}':<42}{n:>7}{str(gate.date()):>13}" + f"{str(rip.date()):>18}{rit:>+9}g") + peggiore = max(ritardi, key=lambda x: x[1]) + print(f"\n Anche nell'ipotesi pessimista il danno NON e' uniforme: l'unico gate che slitterebbe") + print(f" e' {peggiore[0]} ({peggiore[1]:+d} giorni) β€” gli altri hanno finestre cosi' giovani che") + print(" ricostruirebbero la propria numerosita' PRIMA della data di decisione. Ma la colonna") + print(" giusta e' ancora un'altra, e toglie di mezzo anche quel ritardo:") + 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, due volte e in modo + indipendente**: `paper_prevday` ha 1427 barre su 1488 identiche BIT A BIT al replay + dopo 62 notti di riscrittura del parquet BTC/ETH (B4); e le 6 barre che `paper_statarb` + recupero' durante l'incidente del feed coincidono anch'esse al bit (B2) β€” due percorsi + diversi, stesso verdetto. Per Hyperliquid la misura e' 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, ma di poco β€” e il "di poco" e' MISURATO, non + assunto. I libri REAL saltano i ribilanci sotto il min-order e quella decisione dipende dal + capitale corrente: il percorso REAL rigenerato non e' "lo stesso numero calcolato meglio", e' + un secondo percorso deterministico. Contava sapere quanto, perche' il gate XSR01 del 23/10 + legge `net_REAL-$5000` per l'haircut: misurato in C2, **1,7% registrato contro 1,8% + ricostruito**. Trascurabile a $5.000 (i ticket superano il min-order comunque). Il caveat + resta vero in linea di principio e va scritto nel diario; non e' un ostacolo alla decisione. + + COSA FARE, MONITOR PER MONITOR (raccomandazione, non azione β€” questo script non tocca nulla). + ⚠ Le due cose sono SEPARATE: la RIPARAZIONE di `advance()` (fermarsi all'ultima barra chiusa) + ha senso su tutti e sei β€” su un monitor sano non cambia niente e lo protegge da un cambio di + griglia futuro; la RIGENERAZIONE della serie e' un'altra decisione, e va fatta solo dove il + difetto morde abbastanza da spostare cio' che si leggera': + paper_xsr rigenerare β€” 41 min/g, e il gate del 23/10 legge questa serie + paper_statarb rigenerare β€” 4 min/g, gate 27/09: e' il piu' vicino nel tempo + paper_dvolspread rigenerare β€” 2 min/g, il peggiore dei sei; kill-check il 24/10 + paper_prevday LASCIARE β€” 1.438/1.440 min/g: la rigenerazione cambierebbe lo 0,1% + della varianza e azzererebbe 1.488 barre di storia + paper_portfolio rigenerare β€” 19 min/g; nessun gate, ma e' la serie che il dashboard + mostra e che finisce nei diari come "il book sta facendo X" + paper_combo LASCIARE β€” sano; semmai va PROTETTO con un test che rompa se la sua + griglia smette di essere il calendario di borsa""") + + +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_C2() + 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_oi_pin.py b/scripts/research/r0822_oi_pin.py new file mode 100644 index 0000000..5744575 --- /dev/null +++ b/scripts/research/r0822_oi_pin.py @@ -0,0 +1,709 @@ +#!/usr/bin/env python +"""r0822_oi_pin.py β€” OI-PIN: il prezzo e' attratto verso gli strike a massimo open interest +nelle ore che precedono la scadenza Deribit? (ondata 2026-08-22, filone OI-PIN) + +CONTESTO / PERCHE' QUESTO FILONE NON E' "CALENDARIO GIA' MORTO" +-------------------------------------------------------------- +Il 02/07 il progetto ha bocciato il **calendario** delle scadenze Deribit (drift post-expiry +weekly/monthly/quarterly): 0/24 celle a Bonferroni, e il pattern si INVERTE proprio sul +quarterly, dove l'open interest massimo dovrebbe amplificarlo. Quel test guardava le DATE. +Qui si guarda l'OPEN INTEREST VERO, per strike, ora per ora (`data/raw/cb_chain/`): +il meccanismo proposto non e' "il venerdi' e' speciale" ma "il market maker corto gamma +ricompra/rivende verso lo strike dove sta la sua esposizione". E' un meccanismo NON di +calendario, quindi ammissibile secondo la sezione 5 del brief. + +IPOTESI (dichiarata PRIMA di misurare) +-------------------------------------- +H1 Il prezzo deriva VERSO il livello a massima concentrazione di OI / gamma / max-pain nelle + ultime ore prima della scadenza (statistica `toward = sign(K - S_t) * r_{t->T}` > 0). +H2 L'effetto e' piu' forte a poche ore dalla scadenza e sulle scadenze grandi (venerdi'). +H0-ATTESA DELL'AUTORE: **negativa**. Motivi dichiarati in anticipo: + (a) il crypto e' 24/7, senza il "chiusura di borsa" che genera il pinning azionario classico; + (b) l'OI Deribit e' grande ma il perp/spot che lo dovrebbe muovere e' molto piu' grande; + (c) il progetto ha gia' misurato che ogni effetto di finestra su BTC/ETH e' rumore; + (d) e soprattutto: `K_oi` sta quasi sempre VICINO allo spot, quindi qualunque statistica + "il prezzo va verso K" e' contaminata dalla POSIZIONE del livello, non dal suo OI. +Per questo il null location-matched (regola codificata il 02/07 sui livelli di Fibonacci) +non e' un contorno: e' IL test. + +CONVENZIONE CAUSALE (verificata da un assert nel codice) +-------------------------------------------------------- +Barre 1h open-labeled: la barra etichettata `tau` copre [tau, tau+1h) e il suo close e' il +prezzo a `tau+1h`. + * snapshot catena all'ora `hh` -> righe con ts in [hh, hh+1h) + * decisione a fine barra `hh` -> prezzo eseguibile = close(hh) = prezzo a hh+1h + * quindi entrata a `hte` ore dalla scadenza <=> snapshot a dte = hte+1 ore + * uscita al prezzo di regolamento = close della barra (E-1h) = prezzo a E (08:00 UTC) +`A.causality_ok` NON e' applicabile (il segnale non e' funzione del solo feed OHLC): la +causalita' e' garantita per costruzione e verificata da `_assert_causalita()`. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_oi_pin.py +""" +from __future__ import annotations + +import glob +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +import altlib as A # noqa: E402 + +STORE = ROOT / "data" / "raw" / "cb_chain" +ASSETS = ("BTC", "ETH") +FEE_SIDE_REAL = 0.00035 # 3,5 bps/lato = fee Deribit REALE del libro (7 bps RT) +FEE_SIDE_CONS = A.FEE_SIDE # 5 bps/lato = convenzione conservativa dei backtest del progetto +MAX_H = 50 # ore prima della scadenza da tenere in memoria +HTE_GRID = (1, 2, 3, 6, 12, 24, 36, 48) # ore-alla-scadenza all'INGRESSO +RULES = ("oi", "gex", "mp") # max OI Β· max gamma*OI Β· max pain +PLACEBOS_GLOB = ("rand", "mid", "mr7") # non dipendono dalla regola +PLACEBOS_RULE = ("adj", "jit") # costruiti sulla distanza DELLA regola +SEED = 20260822 +RNG = np.random.default_rng(SEED) + + +# =========================================================================== dati +def _files() -> list[str]: + fs = sorted(glob.glob(str(STORE / "*.parquet"))) + if not fs: + raise FileNotFoundError(f"{STORE} vuoto o assente") + return fs + + +def load_near(max_h: int = MAX_H) -> pd.DataFrame: + """Catena filtrata alle sole ore vicine a scadenza, letta file per file (RAM: 7 GB in due).""" + cols = ["asset", "strike", "option_type", "ts", "exp", + "open_interest", "gamma", "quote_status"] + out = [] + for p in _files(): + d = pd.read_parquet(p, columns=cols) + d["ts"] = pd.to_datetime(d["ts"], utc=True) + d["exp"] = pd.to_datetime(d["exp"], utc=True) + d["hh"] = d["ts"].dt.floor("h") + d["dte_h"] = (d["exp"] - d["hh"]).dt.total_seconds() / 3600.0 + d = d[(d["dte_h"] > 0) & (d["dte_h"] <= max_h)] + if len(d): + out.append(d) + del d + d = pd.concat(out, ignore_index=True) + # una riga presente non e' un dato presente: OI mancante o quota rotta non conta + d = d[d["open_interest"].notna() & (d["open_interest"] > 0)] + d = (d.sort_values("ts") + .drop_duplicates(subset=["asset", "exp", "hh", "strike", "option_type"], keep="last") + .reset_index(drop=True)) + return d + + +def spot_1h() -> dict[str, pd.Series]: + s = {} + for a in ASSETS: + df = A.get(a, "1h") + s[a] = pd.Series(df["close"].to_numpy(float), + index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))) + return s + + +# =========================================================================== livelli +def _max_pain(K: np.ndarray, oi_c: np.ndarray, oi_p: np.ndarray) -> float: + """Strike che minimizza il payout complessivo verso i detentori (max pain classico).""" + d = K[None, :] - K[:, None] # d[i,j] = K_j - K_i (K_i = candidato) + pay = (np.maximum(-d, 0.0) * oi_c[None, :]).sum(1) + (np.maximum(d, 0.0) * oi_p[None, :]).sum(1) + return float(K[int(np.argmin(pay))]) + + +def levels_table(chain: pd.DataFrame, spot: dict[str, pd.Series]) -> pd.DataFrame: + """Un record per (asset, scadenza, ora): i 3 livelli candidati + i 4 placebo di strike + + le misure di concentrazione. I placebo sono LOCATION-MATCHED (regola 02/07).""" + rows = [] + piv = (chain.groupby(["asset", "exp", "hh", "strike", "option_type"], sort=False) + .agg(oi=("open_interest", "sum"), gm=("gamma", "mean")).reset_index()) + for (asset, exp, hh), g in piv.groupby(["asset", "exp", "hh"], sort=False): + S = spot[asset].asof(hh) # prezzo a hh+1h = close(barra hh) + if not np.isfinite(S) or S <= 0: + continue + K = np.sort(g["strike"].unique().astype(float)) + if len(K) < 3: + continue + oi_c = np.zeros(len(K)); oi_p = np.zeros(len(K)); gx = np.zeros(len(K)) + idx = {k: i for i, k in enumerate(K)} + for k, ot, o, gm in zip(g["strike"].to_numpy(float), g["option_type"].to_numpy(), + g["oi"].to_numpy(float), g["gm"].to_numpy(float)): + i = idx[float(k)] + (oi_c if ot == "C" else oi_p)[i] += o + gx[i] += o * (gm if np.isfinite(gm) else 0.0) + oi = oi_c + oi_p + tot = oi.sum() + if tot <= 0: + continue + i_oi = int(np.argmax(oi)) + lv = { + "oi": float(K[i_oi]), + "gex": float(K[int(np.argmax(gx))]) if gx.max() > 0 else np.nan, + "mp": _max_pain(K, oi_c, oi_p), + } + # ---- placebo LOCATION-MATCHED, uno per OGNI regola -------------------------------- + # ⚠ difetto trovato e corretto in sessione: la prima stesura costruiva `adj`/`jit` + # attorno al SOLO max-OI e poi li usava come null anche per max-pain e gamma. Ma + # |K_oi - S| mediano e' ~2,4% e |K_mp - S| ~1,1%: confrontare mp con un placebo + # tarato sulla distanza di oi NON e' location-matched, e' proprio l'errore che il + # null doveva prevenire. Ogni regola ha ora i placebo alla PROPRIA distanza. + dist = np.abs(K - S) + for nm, kv in list(lv.items()): + if not np.isfinite(kv): + out_adj = out_jit = np.nan + else: + i_r = int(np.argmin(np.abs(K - kv))) + j = i_r + (1 if (i_r == 0 or (i_r < len(K) - 1 and RNG.random() < 0.5)) else -1) + out_adj = float(K[int(np.clip(j, 0, len(K) - 1))]) + d0 = abs(kv - S) + cand = np.where((dist >= 0.5 * d0) & (dist <= 1.5 * d0) & (K != kv))[0] + out_jit = float(K[cand[RNG.integers(len(cand))]]) if len(cand) else np.nan + lv[f"adj_{nm}"] = out_adj + lv[f"jit_{nm}"] = out_jit + # placebo GLOBALI (non dipendono dalla regola) + # rand: permutare l'OI fra gli strike e riprenderne l'argmax equivale ESATTAMENTE a + # estrarre uno strike uniforme dalla griglia (l'argmax di un vettore permutato e' + # uniforme sulle posizioni) -> lo si implementa cosi', ed e' esatto, non simulato. + lv["rand"] = float(K[RNG.integers(len(K))]) + # mid: centro della griglia quotata = puro artefatto di copertura, zero informazione OI + lv["mid"] = float(0.5 * (K[0] + K[-1])) + rows.append(dict( + asset=asset, exp=exp, hh=hh, dte_h=float((exp - hh).total_seconds() / 3600.0), + S=float(S), n_k=len(K), tot_oi=float(tot), + top_share=float(oi[i_oi] / tot), + hhi=float(((oi / tot) ** 2).sum()), + k_step=float(np.median(np.diff(K))) if len(K) > 1 else np.nan, + **{f"L_{k}": v for k, v in lv.items()}, + )) + return pd.DataFrame(rows) + + +# =========================================================================== il FATTO +def build_events(lev: pd.DataFrame, spot: dict[str, pd.Series]) -> pd.DataFrame: + """Un evento per (asset, scadenza, hte): livelli, prezzo d'ingresso, prezzo di regolamento.""" + ev = [] + mr7 = {a: spot[a].rolling(24 * 7, min_periods=24).mean() for a in ASSETS} + for a in ASSETS: + s = spot[a] + sub = lev[lev["asset"] == a] + for exp, g in sub.groupby("exp"): + t_set = pd.Timestamp(exp) - pd.Timedelta(hours=1) # barra il cui close e' a E + if t_set not in s.index: + continue + S_T = float(s.loc[t_set]) + gi = g.set_index("hh") + for hte in HTE_GRID: + hh = pd.Timestamp(exp) - pd.Timedelta(hours=hte + 1) + if hh not in gi.index or hh not in s.index: + continue + r = gi.loc[hh] + S = float(s.loc[hh]) # close(hh) = prezzo a E-hte + if not np.isfinite(S) or S <= 0: + continue + rec = dict(asset=a, exp=exp, hte=hte, hh=hh, S=S, S_T=S_T, + ret=S_T / S - 1.0, n_k=int(r["n_k"]), tot_oi=float(r["tot_oi"]), + top_share=float(r["top_share"]), hhi=float(r["hhi"]), + k_step=float(r["k_step"]), + dow=int(pd.Timestamp(exp).dayofweek)) + lvls = {c[2:]: r[c] for c in r.index if str(c).startswith("L_")} + lvls["mr7"] = float(mr7[a].asof(hh)) + for nm, K in lvls.items(): + K = float(K) if K is not None else np.nan + if not np.isfinite(K) or K <= 0: + rec[f"d_{nm}"] = np.nan; rec[f"tw_{nm}"] = np.nan + rec[f"sh_{nm}"] = np.nan + continue + d = (K - S) / S + rec[f"K_{nm}"] = K + rec[f"d_{nm}"] = d + rec[f"tw_{nm}"] = np.sign(d) * rec["ret"] # verso il livello + rec[f"sh_{nm}"] = (abs(S - K) - abs(S_T - K)) / S # avvicinamento + ev.append(rec) + E = pd.DataFrame(ev) + # NULL STATICO CAUSALE (lezione 25/07: il primo null statico di STATARB usava + # sign(mean(segnale)) su tutto il campione = look-ahead; qui la maggioranza e' ESPANDENTE + # e shiftata). Risponde a: "il candidato e' solo 'sempre nello stesso verso' prima della + # scadenza?" β€” che su 3,7 mesi di mercato direzionale e' l'ipotesi piu' probabile. + E = E.sort_values(["asset", "hte", "hh"]).reset_index(drop=True) + sgn = np.sign(E["d_mp"].to_numpy(float)) + maj = (pd.Series(sgn).groupby([E["asset"], E["hte"]]) + .transform(lambda x: x.expanding().mean().shift(1))) + E["d_maj"] = np.where(np.isfinite(maj), np.sign(maj) * 0.01, np.nan) + E["tw_maj"] = np.sign(E["d_maj"]) * E["ret"] + E["sh_maj"] = np.nan + E["d_long"] = 0.01; E["tw_long"] = E["ret"]; E["sh_long"] = np.nan + E["d_short"] = -0.01; E["tw_short"] = -E["ret"]; E["sh_short"] = np.nan + return E + + +def _assert_causalita(chain: pd.DataFrame, ev: pd.DataFrame) -> str: + """Il livello usato a `hh` deve venire da quote osservate PRIMA del prezzo d'ingresso.""" + bad = int((chain["ts"] < chain["hh"]).sum() + + (chain["ts"] >= chain["hh"] + pd.Timedelta("1h")).sum()) + assert bad == 0, "snapshot fuori dalla propria ora" + dte = (pd.to_datetime(ev["exp"]) - pd.to_datetime(ev["hh"])).dt.total_seconds() / 3600.0 + assert bool((dte == ev["hte"] + 1).all()), "hte non coerente con lo snapshot" + assert bool(((pd.to_datetime(ev["hh"]) + pd.Timedelta(hours=1)) + <= pd.to_datetime(ev["exp"])).all()), "ingresso oltre la scadenza" + return (f"OK β€” 0/{len(chain)} righe di catena fuori dalla propria ora; " + f"{len(ev)} eventi con dte == hte+1 (ingresso = close(hh), quota vista in [hh,hh+1h))") + + +# =========================================================================== inferenza +def cluster_boot(x: np.ndarray, clusters: np.ndarray, n: int = 2000, seed: int = SEED): + """IC95 della media con ricampionamento dei CLUSTER (una scadenza = 1 cluster: BTC ed ETH + della stessa scadenza non sono osservazioni indipendenti).""" + x = np.asarray(x, float) + m = np.isfinite(x) + x, clusters = x[m], np.asarray(clusters)[m] + if len(x) < 5: + return np.nan, np.nan, np.nan, 0 + uc = np.unique(clusters) + groups = [x[clusters == c] for c in uc] + rng = np.random.default_rng(seed) + draws = np.empty(n) + for i in range(n): + pick = rng.integers(0, len(groups), len(groups)) + draws[i] = np.mean(np.concatenate([groups[j] for j in pick])) + return float(np.mean(x)), float(np.percentile(draws, 2.5)), float(np.percentile(draws, 97.5)), len(uc) + + +def mde(x: np.ndarray, clusters: np.ndarray) -> float: + """Effetto minimo rilevabile (alpha .05 bilaterale, potenza 80%) con questo campione.""" + x = np.asarray(x, float) + m = np.isfinite(x) + x, cl = x[m], np.asarray(clusters)[m] + if len(x) < 3: + return np.nan + nc = max(len(np.unique(cl)), 1) + return float(2.802 * np.std(x) / np.sqrt(nc)) + + +# =========================================================================== strategia +_W: dict = {} + + +def win_1h(asset: str, lo: pd.Timestamp, hi: pd.Timestamp) -> pd.DataFrame: + """Feed 1h tagliato alla finestra della catena: fuori da li' la posizione e' zero per + costruzione, e valutare 70k barre 288 volte costerebbe minuti per nulla.""" + key = (asset, lo, hi) + if key not in _W: + df = A.get(asset, "1h") + dt = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + m = (dt >= lo - pd.Timedelta("2D")) & (dt <= hi + pd.Timedelta("2D")) + _W[key] = df.loc[m].reset_index(drop=True) + return _W[key] + + +def hourly_target(ev: pd.DataFrame, asset: str, rule: str, hte: int, + band: tuple[float, float] | None, sizing: str, + subsample: str, df1h: pd.DataFrame) -> np.ndarray: + """Posizione oraria: entra `hte` ore prima della scadenza nel verso del livello, esce a + scadenza. Se piu' finestre si sovrappongono vince la scadenza PIU' VICINA (una posizione + alla volta = cio' che il conto Deribit puo' davvero fare: strumento unico, netting).""" + idx = pd.DatetimeIndex(pd.to_datetime(df1h["datetime"], utc=True)) + pos = np.zeros(len(idx)) + loc = pd.Series(np.arange(len(idx)), index=idx) + e = ev[(ev["asset"] == asset) & (ev["hte"] == hte)].copy() + if subsample == "fri": + e = e[e["dow"] == 4] + elif subsample == "big": + e = e[e["tot_oi"] >= e["tot_oi"].median()] + d = e[f"d_{rule}"].to_numpy(float) + keep = np.isfinite(d) + if band is not None: + keep &= (np.abs(d) >= band[0]) & (np.abs(d) <= band[1]) + e = e[keep] + d = d[keep] + if len(e) == 0: + return pos + w = np.sign(d) if sizing == "sign" else np.clip(d / 0.01, -1.0, 1.0) + order = np.argsort(e["exp"].to_numpy())[::-1] # lontane prima: la scadenza VICINA sovrascrive + for j in order: + hh = pd.Timestamp(e["hh"].iloc[j]); exp = pd.Timestamp(e["exp"].iloc[j]) + last = exp - pd.Timedelta(hours=2) + if hh not in loc.index or last not in loc.index: + continue + i0, i1 = int(loc[hh]), int(loc[last]) + if i1 >= i0: + pos[i0:i1 + 1] = w[j] + return pos + + +def strat_daily(ev: pd.DataFrame, rule: str, hte: int, band, sizing: str, subsample: str, + fee_side: float) -> tuple[pd.Series, dict]: + """Serie giornaliera netta 50/50 BTC+ETH + metriche, sulla sola finestra della catena.""" + nets, info = {}, {} + lo_w = pd.Timestamp(ev["hh"].min()); hi_w = pd.Timestamp(ev["exp"].max()) + for a in ASSETS: + df = win_1h(a, lo_w, hi_w) + tgt = hourly_target(ev, a, rule, hte, band, sizing, subsample, df) + r = A.eval_weights(df, tgt, fee_side=fee_side) + nets[a] = pd.Series(r["net"], index=r["idx"]) + info[a] = int(np.sum(np.abs(np.diff(np.r_[0.0, tgt])) > 1e-12)) + J = pd.concat(nets, axis=1, join="inner").fillna(0.0) + daily = A._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) + lo = pd.Timestamp(ev["hh"].min()).normalize() + daily = daily[daily.index >= lo] + return daily, info + + +def metr(s: pd.Series) -> dict: + r = s.dropna() + if len(r) < 5: + return dict(sharpe=np.nan, dd=np.nan, cagr=np.nan, n=len(r)) + eq = np.cumprod(1 + r.to_numpy(float)); pk = np.maximum.accumulate(eq) + yrs = max((r.index[-1] - r.index[0]).total_seconds() / 86400 / 365.25, 1e-6) + return dict(sharpe=A._sh(r), dd=float(np.max((pk - eq) / pk)), + cagr=float(eq[-1] ** (1 / yrs) - 1), n=int(len(r))) + + +# =========================================================================== main +def main() -> None: + P = print + P("=" * 100) + P("OI-PIN β€” pinning verso gli strike a massimo open interest (catena Deribit oraria)") + P("=" * 100) + + # ---------------------------------------------------------------- 1. DATO + chain = load_near() + spot = spot_1h() + lev = levels_table(chain, spot) + ev = build_events(lev, spot) + P("\n[1] DATO") + P(f" righe catena a <= {MAX_H}h dalla scadenza : {len(chain):,}") + P(f" scadenze distinte : {ev.groupby('asset').exp.nunique().to_dict()}") + P(f" finestra : {ev.hh.min()} -> {ev.exp.max()}") + P(f" eventi (asset x scadenza x hte) : {len(ev)}") + P(f" strike per snapshot mediana/min : {lev.n_k.median():.0f} / {lev.n_k.min():.0f}") + P(f" causalita' : {_assert_causalita(chain, ev)}") + P(" ⚠ il collettore scarta gli strumenti con OI<100 -> la griglia e' TRONCATA sulle code:") + P(" il max-OI ne soffre poco (e' un argmax), il MAX PAIN si', perche' e' una somma su tutta") + P(" la catena. Il verdetto su 'mp' va letto come 'max pain sulla catena OI>=100'.") + + # ---------------------------------------------------------------- 2. POTENZA (PRIMA del risultato) + P("\n[2] POTENZA DICHIARATA β€” calcolata PRIMA di leggere qualunque effetto") + P(" Unita' statistica = una SCADENZA (BTC ed ETH della stessa data non sono indipendenti).") + P(f" {'hte':>4} {'n oss':>6} {'n clus':>7} {'sd(ret)':>9} {'MDE medio/trade':>16} {'MDE ann.':>10}") + for hte in HTE_GRID: + e = ev[ev["hte"] == hte] + m = mde(e["ret"].to_numpy(float), e["exp"].to_numpy()) + ann = m * (365.25 * 24 / hte) if np.isfinite(m) else np.nan + P(f" {hte:>4} {len(e):>6} {e.exp.nunique():>7} {np.nanstd(e['ret']):>9.4f}" + f" {m:>16.4f} {ann:>9.1%}") + P(" LETTURA OBBLIGATORIA: il campione rileva solo effetti ENORMI. A 24h l'MDE e' ~0,5-0,7%") + P(" per trade contro 0,07% di fee: un pinning realistico (pochi bps) e' INVISIBILE qui.") + P(" Quindi un 'non significativo' NON prova assenza; un 'significativo' sarebbe cosi' grande") + P(" da essere sospetto. Il verdetto massimo raggiungibile e' LEAD.") + + # ---------------------------------------------------------------- 3. IL FATTO + P("\n[3] IL FATTO β€” dove sta il livello e dove va il prezzo") + P(" |ret| verso la scadenza e distanza |d| del livello (mediane):") + P(f" {'hte':>4} {'|ret| med':>10} {'|d_oi| med':>11} {'|d_gex|':>9} {'|d_mp|':>9}" + f" {'d_oi<0':>8} {'top_share':>10}") + for hte in HTE_GRID: + e = ev[ev["hte"] == hte] + P(f" {hte:>4} {e['ret'].abs().median():>10.4f} {e['d_oi'].abs().median():>11.4f}" + f" {e['d_gex'].abs().median():>9.4f} {e['d_mp'].abs().median():>9.4f}" + f" {(e['d_oi'] < 0).mean():>8.2f} {e['top_share'].median():>10.3f}") + P(" ⚠ se 'd_oi<0' e' lontano da 0.50 il livello ha un BIAS DI SEGNO (griglia troncata o") + P(" mercato in trend): una statistica 'il prezzo va verso K' lo erediterebbe.") + + P("\n Statistica di pinning tw = sign(K-S)*ret (media, IC95 cluster-bootstrap su scadenze):") + P(f" {'hte':>4} {'rule':>5} {'media tw':>10} {'IC95 lo':>9} {'IC95 hi':>9} {'sig':>4}" + f" {'shrink med':>11} {'n':>5}") + fact = [] + for hte in HTE_GRID: + e = ev[ev["hte"] == hte] + for rl in RULES: + m, lo, hi, nc = cluster_boot(e[f"tw_{rl}"].to_numpy(float), e["exp"].to_numpy()) + sg = "***" if (np.isfinite(lo) and lo > 0) else ("neg" if (np.isfinite(hi) and hi < 0) else "-") + P(f" {hte:>4} {rl:>5} {m:>10.5f} {lo:>9.5f} {hi:>9.5f} {sg:>4}" + f" {np.nanmedian(e[f'sh_{rl}']):>11.5f} {int(np.isfinite(e[f'tw_{rl}']).sum()):>5}") + fact.append(dict(hte=hte, rule=rl, mean=m, lo=lo, hi=hi)) + F = pd.DataFrame(fact) + P(f" celle con IC95 tutto sopra zero: {int((F.lo > 0).sum())}/{len(F)}" + f" | tutto sotto zero: {int((F.hi < 0).sum())}/{len(F)}") + + P("\n LA STESSA STATISTICA SUI LIVELLI PLACEBO β€” se sale anche li', NON e' pinning:") + P(f" {'hte':>4} " + " ".join(f"{('tw_' + n):>10}" for n in + ("oi", "mp", "rand", "jit_mp", "mid", "mr7", "maj", "short"))) + for hte in HTE_GRID: + e = ev[ev["hte"] == hte] + P(f" {hte:>4} " + " ".join( + f"{np.nanmean(e[f'tw_{n}']):>10.5f}" for n in + ("oi", "mp", "rand", "jit_mp", "mid", "mr7", "maj", "short"))) + P("\n QUANTO SONO LO STESSO SEGNALE? accordo del SEGNO di (K-S) fra max-pain e i placebo:") + for n in ("mid", "mr7", "oi", "maj"): + agr = [float(np.nanmean(np.sign(ev.loc[ev.hte == h, "d_mp"]) + == np.sign(ev.loc[ev.hte == h, f"d_{n}"]))) for h in HTE_GRID] + P(f" mp vs {n:<5}: " + " ".join(f"h{h}={a:.2f}" for h, a in zip(HTE_GRID, agr))) + P(f" base rate sign<0 : " + " ".join( + f"{n}={np.nanmean(np.sign(ev[f'd_{n}']) < 0):.2f}" for n in ("oi", "gex", "mp", "mid", "mr7"))) + for a in ASSETS: + sp_ = spot[a] + w = sp_[(sp_.index >= ev.hh.min()) & (sp_.index <= ev.exp.max())] + P(f" deriva {a} nella finestra: {w.iloc[-1] / w.iloc[0] - 1:+.1%}" + f" (se il livello sta quasi sempre da un lato, il candidato e' una scommessa direzionale)") + + # ---------------------------------------------------------------- 4. NULL LOCATION-MATCHED + P("\n[4] NULL LOCATION-MATCHED (regola 02/07: e' la POSIZIONE o e' l'OI?)") + P(" Confronto APPAIATO per evento: tw(livello speciale) - tw(placebo alla stessa distanza).") + P(" placebo PER REGOLA: adj_r = strike adiacente a K_r Β· jit_r = strike arbitrario a") + P(" distanza 0.5-1.5x |K_r - S| (analogo del FibΒ±jitter del 02/07).") + P(" placebo GLOBALI: rand = strike uniforme (== permutazione dell'OI, equivalenza esatta) Β·") + P(" mid = centro della griglia quotata (zero info OI) Β· mr7 = media 7g dello spot.") + P(f" {'hte':>4} {'rule':>5} {'Ξ” vs adj_r':>12} {'Ξ” vs jit_r':>12}" + f" {'Ξ” vs rand':>12} {'Ξ” vs mid':>12} {'Ξ” vs mr7':>12}") + npos = ntot = 0 + npos_c = ntot_c = 0 # solo i null PULITI (bilanciati nel segno): rand/mid/mr7 + for hte in HTE_GRID: + e = ev[ev["hte"] == hte] + for rl in RULES: + cells = [] + for p in (f"adj_{rl}", f"jit_{rl}") + PLACEBOS_GLOB: + dd = e[f"tw_{rl}"].to_numpy(float) - e[f"tw_{p}"].to_numpy(float) + m, lo, hi, _ = cluster_boot(dd, e["exp"].to_numpy()) + sig = bool(np.isfinite(lo) and lo > 0) + cells.append(f"{m:>11.5f}{'*' if sig else ' '}") + ntot += 1; npos += int(sig) + if p in PLACEBOS_GLOB: + ntot_c += 1; npos_c += int(sig) + P(f" {hte:>4} {rl:>5} " + " ".join(cells)) + P(f" celle (rule x hte x placebo) in cui lo SPECIALE batte il placebo con IC95>0:" + f" {npos}/{ntot} (atteso per caso ~{0.025 * ntot:.0f})") + P(f" ... contando SOLO i null bilanciati nel segno (rand/mid/mr7): {npos_c}/{ntot_c}" + f" (atteso ~{0.025 * ntot_c:.0f}) <-- e' QUESTO il numero da citare") + P("\n ⚠ UN PLACEBO SI CONTROLLA PRIMA DI USARLO: se il suo segno non e' bilanciato rispetto") + P(" alla regola, il Ξ” non misura l'informazione, misura il ROVESCIAMENTO del segnale.") + P(f" quota di eventi con sign(K_placebo-S) == sign(K_regola-S), su tutti gli hte:") + for rl in RULES: + agr = {p: float(np.nanmean(np.sign(ev[f"d_{rl}"]) == np.sign(ev[f"d_{p}"]))) + for p in (f"adj_{rl}", f"jit_{rl}") + PLACEBOS_GLOB} + P(" " + rl + ": " + " ".join(f"{k}={v:.2f}" for k, v in agr.items())) + P(" un null PULITO sta vicino a 0.50 (rand/mid/mr7). Sotto ~0.35 il placebo e' la") + P(" strategia INVERTITA e il suo Ξ” va letto come ~2x il segnale, non come un test.") + P(" LETTURA: `adj_r`/`jit_r` chiedono 'e' proprio QUELLO strike?'; `mid`/`mr7` chiedono") + P(" 'serve l'open interest, o basta il centro recente del prezzo?'. La seconda e' la") + P(" domanda che uccide o salva il filone.") + + # ---------------------------------------------------------------- 5. CONDIZIONAMENTO + P("\n[5] CONDIZIONAMENTO β€” la concentrazione di OI e le scadenze grandi cambiano qualcosa?") + P(f" {'hte':>4} {'gruppo':>16} {'media tw_oi':>12} {'IC95 lo':>9} {'IC95 hi':>9} {'n':>4}") + for hte in (3, 12, 24): + e = ev[ev["hte"] == hte] + hi_c = e["top_share"] >= e["top_share"].median() + big = e["tot_oi"] >= e["tot_oi"].quantile(0.75) + for nm, sel in (("conc ALTA", hi_c), ("conc BASSA", ~hi_c), + ("OI top-25%", big), ("venerdi'", e["dow"] == 4)): + x = e.loc[sel, "tw_oi"].to_numpy(float) + m, lo, hh_, _ = cluster_boot(x, e.loc[sel, "exp"].to_numpy()) + P(f" {hte:>4} {nm:>16} {m:>12.5f} {lo:>9.5f} {hh_:>9.5f} {int(np.isfinite(x).sum()):>4}") + + # ---------------------------------------------------------------- 6. STRATEGIA + P("\n[6] STRATEGIA β€” expectancy netta fee (3,5 bps/lato reali) e serie giornaliera") + bands = {"nessuna": None, "0.2-3%": (0.002, 0.03)} + subs = ("all", "big", "fri") + sizings = ("sign", "prop") + cells, n_trials = [], 0 + for rl in RULES: + for hte in HTE_GRID: + for bn, bd in bands.items(): + for sz in sizings: + for sb in subs: + n_trials += 1 + d, info = strat_daily(ev, rl, hte, bd, sz, sb, FEE_SIDE_REAL) + mm = metr(d) + cells.append(dict(rule=rl, hte=hte, band=bn, sizing=sz, sub=sb, + **mm, ntrade=sum(info.values()))) + C = pd.DataFrame(cells) + P(f" griglia strategia valutata: {n_trials} celle " + f"({len(RULES)} rule x {len(HTE_GRID)} hte x {len(bands)} bande x {len(sizings)} sizing x {len(subs)} sottocampioni)") + P(f" celle con Sharpe > 0: {(C.sharpe > 0).sum()}/{len(C)} (una famiglia di RUMORE sta a ~50%)") + P(" migliori 8 celle per Sharpe (NON e' una selezione: e' il MASSIMO della griglia, che serve al DSR):") + P(C.sort_values("sharpe", ascending=False).head(8).to_string(index=False, + float_format=lambda v: f"{v:.3f}")) + P(" peggiori 3:") + P(C.sort_values("sharpe").head(3).to_string(index=False, float_format=lambda v: f"{v:.3f}")) + + P("\n [6-bis] LA STESSA MACCHINA SUI LIVELLI PLACEBO (stesse finestre, stessa fee, stesso") + P(" sizing): se un placebo regge quanto il livello 'speciale', l'OI non aggiunge nulla.") + nulls = [] + for rl in ("mp", "oi", "mid", "mr7", "rand", "jit_mp", "maj", "short", "long"): + for hte in (3, 6, 12, 24): + for bn, bd in bands.items(): + d, _i = strat_daily(ev, rl, hte, bd, "sign", "all", FEE_SIDE_REAL) + mm = metr(d) + nulls.append(dict(rule=rl, hte=hte, band=bn, sharpe=mm["sharpe"], + cagr=mm["cagr"], dd=mm["dd"])) + N = pd.DataFrame(nulls) + piv = N.pivot_table(index=["hte", "band"], columns="rule", values="sharpe") + P(piv.reindex(columns=["mp", "oi", "mid", "mr7", "rand", "jit_mp", "maj", "short", "long"]) + .to_string(float_format=lambda v: f"{v:6.2f}")) + P(f" celle placebo valutate: {len(N)} (contano come trial: la griglia dichiarata sale)") + + best = C.sort_values("sharpe", ascending=False).iloc[0] + P(f"\n CELLA MIGLIORE = {best['rule']}/hte{int(best['hte'])}/{best['band']}/{best['sizing']}/{best['sub']}") + + # expectancy per trade, in % e in R, sulla cella migliore + e = ev[ev["hte"] == int(best["hte"])].copy() + if best["sub"] == "big": + e = e[e["tot_oi"] >= e["tot_oi"].median()] + elif best["sub"] == "fri": + e = e[e["dow"] == 4] + dd = e[f"d_{best['rule']}"].to_numpy(float) + keep = np.isfinite(dd) + if best["band"] != "nessuna": + keep &= (np.abs(dd) >= 0.002) & (np.abs(dd) <= 0.03) + tw = e.loc[keep, f"tw_{best['rule']}"].to_numpy(float) + dk = np.abs(dd[keep]) + net = tw - 2 * FEE_SIDE_REAL + wins, losses = net[net > 0], net[net < 0] + rr = (wins.mean() / abs(losses.mean())) if len(wins) and len(losses) else np.nan + P(f" trade: {len(net)} Β· WR {np.mean(net > 0):.1%} Β· RR medio {rr:.2f}" + f" Β· WR-knob atteso 1/(1+RR) = {1 / (1 + rr):.1%}" if np.isfinite(rr) else " RR n.d.") + P(f" expectancy NETTA per trade: {np.mean(net):+.5f} ({np.mean(net) * 100:+.3f}%)" + f" | in R (R = distanza dal livello): {np.mean(net / np.maximum(dk, 1e-6)):+.3f} R") + P(f" a fee ZERO: {np.mean(tw):+.5f} Β· la fee costa {2 * FEE_SIDE_REAL:.5f}/trade =" + f" {200 * FEE_SIDE_REAL / max(np.mean(tw), 1e-9):.0f}% del lordo" + f" -> {'MORTE PER FEE' if np.mean(tw) <= 2 * FEE_SIDE_REAL else 'la fee NON e il vincolo'}") + + dbest, info = strat_daily(ev, best["rule"], int(best["hte"]), + None if best["band"] == "nessuna" else (0.002, 0.03), + best["sizing"], best["sub"], FEE_SIDE_REAL) + dcons, _ = strat_daily(ev, best["rule"], int(best["hte"]), + None if best["band"] == "nessuna" else (0.002, 0.03), + best["sizing"], best["sub"], FEE_SIDE_CONS) + mb, mc = metr(dbest), metr(dcons) + P(f" serie giornaliera cella migliore fee 3,5bps: Sharpe {mb['sharpe']:+.2f}" + f" Β· maxDD {mb['dd']:.2%} Β· CAGR {mb['cagr']:+.2%} Β· n={mb['n']}g") + P(f" fee 5,0bps: Sharpe {mc['sharpe']:+.2f}" + f" Β· maxDD {mc['dd']:.2%} Β· CAGR {mc['cagr']:+.2%}") + # scomposizione: per mese e per ERA DI COLLETTORE (bite:research fino al 30/07, poi pyg) + P("\n SCOMPOSIZIONE (regola 22/08: un contributo positivo si scompone prima di crederci)") + mo = dbest.groupby([dbest.index.year, dbest.index.month]) + P(" " + " Β· ".join(f"{y}-{m:02d}: Sh {A._sh(g):+.2f} ret {float(np.prod(1 + g) - 1):+.2%} ({len(g)}g)" + for (y, m), g in mo)) + cut = pd.Timestamp("2026-07-30", tz="UTC") + e1, e2 = dbest[dbest.index < cut], dbest[dbest.index >= cut] + P(f" era collettore bite:research (<30/07): Sh {A._sh(e1):+.2f} ret {float(np.prod(1 + e1) - 1):+.2%} ({len(e1)}g)" + f" | pyg (>=30/07): Sh {A._sh(e2):+.2f} ret {float(np.prod(1 + e2) - 1):+.2%} ({len(e2)}g)") + top5 = dbest.sort_values(ascending=False).head(5) + P(f" concentrazione: i 5 giorni migliori valgono {float(np.prod(1 + top5) - 1):+.2%}" + f" su {float(np.prod(1 + dbest) - 1):+.2%} totali" + f" = {100 * np.log1p(top5).sum() / max(np.log1p(dbest).sum(), 1e-9):.0f}% del log-equity") + P(" ⚠ HOLD-OUT: NON ESISTE. La catena parte dal 2026-05-01 e l'hold-out del progetto e'") + P(" 2025-01-01: TUTTA la serie sta dentro l'hold-out, quindi non c'e' nessuna finestra") + P(" fuori campione da mostrare. Questo NON e' un dettaglio: e' il motivo per cui") + P(" 3,7 mesi di catena non possono produrre uno sleeve.") + + # ---------------------------------------------------------------- 7. GATE + P("\n[7] GATE") + P(f" causalita' : {_assert_causalita(chain, ev)}") + P(" A.causality_ok : NON GIRATO β€” non applicabile (il segnale non e' funzione del solo") + P(" feed OHLC; la causalita' e' garantita dalla convenzione hte=dte-1") + P(" e verificata dall'assert qui sopra).") + + # DSR: conta TUTTI i trial al rialzo + all_sr = [float(v) for v in C["sharpe"].to_numpy(float) if np.isfinite(v)] + all_sr_wide = all_sr + [float(v) for v in N["sharpe"].to_numpy(float) if np.isfinite(v)] + n_fact = len(HTE_GRID) * len(RULES) * (1 + len(PLACEBOS_GLOB) + len(PLACEBOS_RULE)) + 3 * 4 # celle del fatto + condiz. + trials_tot = len(all_sr_wide) + n_fact + dsr, sr0 = A.deflated_sharpe(mb["sharpe"], all_sr, dbest, dpy=365.25) + dsr_w, _ = A.deflated_sharpe(mb["sharpe"], all_sr_wide, dbest, dpy=365.25) + P(f" deflated Sharpe : DSR {dsr:.3f} (soglia 0.95) Β· Sharpe-null atteso {sr0:.2f}" + f" Β· trial contati {len(all_sr)} (griglia strategia)") + P(f" DSR contando ANCHE le celle placebo ({len(all_sr_wide)} trial): {dsr_w:.3f}") + P(f" trial TOTALI dichiarati al rialzo (fatto+null+condiz.+strategia): {trials_tot}") + + # marginal vs TP01 + mv = A.marginal_vs_tp01(dbest) + P(f" marginal vs TP01 : {mv.get('marginal_verdict')} Β· corr {mv.get('corr_full')}" + f" Β· uplift w25 full {mv.get('blends', {}).get('w25', {}).get('uplift_full')}" + f" Β· giorni in comune {mv.get('n_days')}") + P(" ⚠ il gate NON PUO' dare ADDS con questo campione: `multicut_persistent`") + P(" richiede >=2 tagli annuali da >=120 giorni e la serie ne ha ~113 in UN") + P(" anno solo; e `has_insample_edge` e' True per DEFAULT perche' non c'e'") + P(" alcun pre-2025. Il verdetto marginale qui e' strutturale, non informativo.") + + # null del de-levering (obbligatorio su ogni claim di DD) + B = A.tp01_baseline_daily() + J = pd.concat({"B": B, "C": dbest}, axis=1, join="inner").dropna() + if len(J) > 20: + bl = 0.75 * J["B"] + 0.25 * J["C"] + dd_bl = A._dd_ret(bl) + ks = np.linspace(0.05, 1.0, 96) + ok = [(k, A._sh(k * J["B"]), A._dd_ret(k * J["B"])) for k in ks] + cand = [(k, s) for k, s, d_ in ok if d_ <= dd_bl] + kbest = max(cand, key=lambda t: t[1]) if cand else None + P(f" null de-levering : blend 0.75TP01+0.25cand -> Sharpe {A._sh(bl):+.2f} / DD {dd_bl:.2%}" + f" | k*TP01 a pari DD -> k={kbest[0]:.2f} Sharpe {kbest[1]:+.2f}" + if kbest else " null de-levering : non calcolabile") + if kbest: + P(f" esito: {'REFUTED (il de-levering fa meglio)' if kbest[1] >= A._sh(bl) else 'superato'}") + else: + P(" null de-levering : NON GIRATO (sovrapposizione con TP01 troppo corta)") + + # implausible + imp = A.implausible_sharpe(dbest, n_trades=int(best["ntrade"])) + P(f" implausible_sharpe : implausible={imp['implausible']} Β· {imp.get('reasons')}") + + # anchor: l'ANCORA di questa strategia e' hte (quale ora prima della scadenza si entra) + ab = A.anchor_luck_band( + lambda h: strat_daily(ev, best["rule"], int(h), + None if best["band"] == "nessuna" else (0.002, 0.03), + best["sizing"], best["sub"], FEE_SIDE_REAL)[0], + offsets=list(HTE_GRID), canonical=int(best["hte"])) + P(f" anchor (hte) : canonico {ab.get('canonical'):.2f} al {100 * ab.get('canonical_pctl', np.nan):.0f}Β° pctl" + f" Β· MEDIANA ONESTA {ab.get('median'):.2f} Β· banda [{ab.get('lo'):.2f},{ab.get('hi'):.2f}]" + f" Β· positive {ab.get('frac_positive'):.0%} Β· gate_pass={ab.get('gate_pass')}") + + # LA DOMANDA DECISIVA, sulla banda d'ancora: max-pain aggiunge qualcosa a un livello + # che NON usa opzioni? Mediana delle DIFFERENZE APPAIATE sui 8 hte (mai differenza + # delle mediane: lezione 26/07). + def _fn(rule): + return lambda h: strat_daily(ev, rule, int(h), (0.002, 0.03), "sign", "all", + FEE_SIDE_REAL)[0] + P(" ⚠ le 8 ancore hte NON sono 8 osservazioni indipendenti: guardano le STESSE 73 scadenze") + P(" e lo STESSO percorso di prezzo. '8/8 positive' dice che il segno non dipende") + P(" dall'ancora, NON che l'effetto sia significativo β€” quello lo dice il cluster-bootstrap") + P(" della sezione [4], che sui placebo senza opzioni (mid/mr7) NON e' significativo.") + for other in ("mr7", "mid", "oi", "rand"): + dl = A.anchor_luck_delta(_fn("mp"), _fn(other), offsets=list(HTE_GRID)) + P(f" Ξ” appaiato mp-{other:<4}: mediana {dl.get('median_paired'):+.2f}" + f" Β· positiva in {dl.get('n_positive')}/{dl.get('n_anchors')} hte" + f" Β· banda [{dl.get('lo'):+.2f},{dl.get('hi'):+.2f}] Β· gate_pass={dl.get('gate_pass')}") + + # eseguibilita' a $600 + P(" eseguibilita' $600 :", end=" ") + hc = [] + for a in ASSETS: + df = win_1h(a, pd.Timestamp(ev["hh"].min()), pd.Timestamp(ev["exp"].max())) + tgt = hourly_target(ev, a, best["rule"], int(best["hte"]), + None if best["band"] == "nessuna" else (0.002, 0.03), + best["sizing"], best["sub"], df) + sc = A.eval_weights_smallcap(df, tgt, capital=300.0, fee_side=FEE_SIDE_REAL) + hc.append(f"{a} haircut {sc['sharpe_haircut']:+.3f} ({sc['n_executed_trades']} trade eseguiti)") + P(" Β· ".join(hc)) + P(" (300$/asset = il cap per-asset del libro live a questo capitale;") + P(" una gamba Β±1 muove 300$ >> min-order 5$ -> nessun ordine saltato)") + + # ---------------------------------------------------------------- 8. SINTESI + P("\n[8] SINTESI") + n_fact_pos = int((F.lo > 0).sum()) + P(f" fatto : {n_fact_pos}/{len(F)} celle (rule x hte) con pinning significativo a IC95") + P(f" null loc. : {npos}/{ntot} confronti appaiati vinti dal livello 'speciale';" + f" {npos_c}/{ntot_c} contando solo i null PULITI") + P(f" di questi, contro il placebo SENZA OPZIONI (media 7g dello spot): " + f"{sum(1 for h in HTE_GRID for rl in RULES if (lambda t: np.isfinite(t[1]) and t[1] > 0)(cluster_boot((ev[ev.hte == h][f'tw_{rl}'] - ev[ev.hte == h]['tw_mr7']).to_numpy(float), ev[ev.hte == h]['exp'].to_numpy())))}/24") + P(f" strategia : miglior Sharpe della griglia {mb['sharpe']:+.2f} su {mb['n']} giorni, DSR {dsr:.3f}") + P(f" marginale : {mv.get('marginal_verdict')} (strutturalmente non ADDS-abile a 3,7 mesi)") + P("=" * 100) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_ortho_screen.py b/scripts/research/r0822_ortho_screen.py new file mode 100644 index 0000000..981ca76 --- /dev/null +++ b/scripts/research/r0822_ortho_screen.py @@ -0,0 +1,684 @@ +#!/usr/bin/env python3 +"""r0822_ortho_screen.py β€” ONDATA 2026-08-22, filone ORTHO-SCREEN (la rete larga, onesta). + +NON e' un'ipotesi: e' un SETACCIO. Sette famiglie di segnale mai (o solo parzialmente) +coperte dalle ondate precedenti, passate TUTTE dallo stesso protocollo, senza eccezioni: + + 1. famiglia DICHIARATA prima (parametri + celle), trial contati AL RIALZO; + 2. A.study_family_honest -> cella scelta IN-SAMPLE-ONLY + deflated-Sharpe di famiglia; + 3. A.marginal_vs_tp01 -> ADDS / NEUTRAL / DILUTES / HEDGE / NOISE / REDUNDANT; + 4. A.causality_ok (+ A.day_boundary_robust dove ha senso, vedi NOTA CONFINE); + 5. banda d'ancora su 8 offset orari (bar 1d ricostruite da 1h) -> la stima onesta e' la MEDIANA; + 6. null del DE-LEVERING su ogni claim di drawdown; + 7. A.eval_weights_smallcap($600) + A.implausible_sharpe sulla cella scelta; + 8. **DSR di SCREEN**: oltre al deflated-Sharpe di famiglia, il deflated-Sharpe calcolato sul + POOL di TUTTE le celle di TUTTE le famiglie. Se scelgo il migliore fra 7 famiglie, i trial + sono 168, non 24: e' l'unico conto onesto per un setaccio. + +PRIOR DICHIARATO PRIMA DI MISURARE: il soffitto direzionale BTC/ETH misurato e' Sharpe ~1.3 e +in 104 ipotesi + 153 agenti (giugno 2026) NESSUNA e' sopravvissuta alla verifica avversariale. +Mi aspetto 0 CANDIDATI. Il valore consegnato e' **quale gate uccide cosa**, perche' questo +restringe lo spazio della prossima ondata. + +LE 7 FAMIGLIE (24 celle ciascuna = 168 trial dichiarati) + RSKEW skew realizzata (da 1h) come PREDITTORE direzionale, non come gate + [nuovo vs blind/agent_40_skewgate, che la usava come FILTRO su un trend] + TACC accelerazione del trend (derivata seconda del log-prezzo) come direzione standalone + VPX relazione volume-prezzo su barre 1d (conferma / divergenza a volume alto e basso) + XDISP dispersione cross-sezionale dei 51 alt HL usata come segnale TIME-SERIES sul mercato + [nuovo vs XS01, dove la dispersione e' un gate sul paniere cross-sectional] + XCORR correlazione realizzata BTC-ETH come STATO di mercato direzionale + [nuovo vs ortho/agent_09 e XAS08, dove la corr gata un book relative-value] + XTAIL struttura dei ritorni dopo uno shock a k-sigma (continuazione vs inversione) + VRAT variance-ratio (persistenza) come CONVINZIONE continua sul TSMOM + [nuovo vs blind/agent_37_hurst, che faceva uno switch BINARIO di modo] + +NOTA CONFINE (perche' day_boundary_robust non gira su tutte): il test sposta le ETICHETTE del +calendario lasciando i prezzi fermi. Per un segnale di solo prezzo (TACC/VPX/XTAIL/VRAT) e' +esattamente il controllo giusto e deve dare INVARIANT. Per un segnale che fa merge_asof su una +serie AUSILIARIA (RSKEW da 1h, XDISP da HL, XCORR da 1h BTC/ETH) spostare l'etichetta in AVANTI +sposta il punto di merge nel FUTURO: sarebbe un LEAK indotto dallo strumento, non un controllo. +Per quelle famiglie il test d'ancora corretto e' la RICOSTRUZIONE delle barre a offset orario +(banda d'ancora, punto 5), che gira su tutte e sette. + +USO: nice -n 19 timeout 900 uv run python scripts/research/r0822_ortho_screen.py +""" +from __future__ import annotations + +import os +import sys +import time +from functools import lru_cache + +import numpy as np +import pandas as pd + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "alt")) +import altlib as A # noqa: E402 + +HL_DIR = A.DATA_DIR +ANCHOR_OFFSETS = (0, 3, 6, 9, 12, 15, 18, 21) +W_BLEND = 0.25 # peso del candidato nel blend con TP01 (convenzione altlib) + + +# =========================================================================== +# INFRASTRUTTURA β€” barre a offset, merge causale di serie ausiliarie +# =========================================================================== +@lru_cache(maxsize=64) +def bars_at(asset: str, off: int) -> pd.DataFrame: + """Barre 1d ancorate all'ora `off`, ricostruite dal feed 1h certificato. + off=0 deve riprodurre esattamente A.get(asset,'1d') (controllo in _sanity).""" + h = A.get(asset, "1h").copy() + idx = pd.to_datetime(h["timestamp"], unit="ms", utc=True) - pd.Timedelta(hours=off) + h.index = idx + o = (h.resample("1D", label="left", closed="left") + .agg({"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}) + .dropna(subset=["open"])) + o["datetime"] = o.index + pd.Timedelta(hours=off) + epoch = pd.Timestamp("1970-01-01", tz="UTC") + o["timestamp"] = ((o["datetime"] - epoch) // pd.Timedelta(milliseconds=1)).astype("int64") + return o.reset_index(drop=True)[["timestamp", "open", "high", "low", "close", "volume", "datetime"]] + + +def _bar_ms(df: pd.DataFrame) -> int: + d = pd.Series(df["timestamp"].astype("int64")).diff().median() + return int(d) if np.isfinite(d) and d > 0 else 86_400_000 + + +def merge_aux(df: pd.DataFrame, aux_ts: np.ndarray, aux_val: np.ndarray) -> np.ndarray: + """Valore di una serie ausiliaria NOTO alla CHIUSURA di ogni barra di df. + `aux_ts` e' gia' il timestamp in cui il valore diventa noto (fine della sua barra). + merge_asof backward sul close della barra -> mai un dato del futuro: eval_weights + tiene target[i] durante la barra i+1, quindi close[i] e' l'ultimo istante lecito.""" + close_ts = df["timestamp"].astype("int64").values + _bar_ms(df) + left = pd.DataFrame({"timestamp": close_ts}) + right = (pd.DataFrame({"timestamp": np.asarray(aux_ts, dtype="int64"), + "v": np.asarray(aux_val, dtype=float)}) + .dropna().sort_values("timestamp").reset_index(drop=True)) + if right.empty: + return np.full(len(df), np.nan) + return pd.merge_asof(left, right, on="timestamp", direction="backward")["v"].values + + +def roll_z(x: np.ndarray, win: int) -> np.ndarray: + s = pd.Series(np.asarray(x, float)) + mp = max(20, win // 4) + m = s.rolling(win, min_periods=mp).mean() + sd = s.rolling(win, min_periods=mp).std() + return ((s - m) / sd.where(sd > 0)).values + + +def exp_pct(x: np.ndarray, min_obs: int = 180) -> np.ndarray: + """Percentile ESPANDENTE causale (rank fra tutti i valori visti finora, incluso l'attuale).""" + return pd.Series(np.asarray(x, float)).expanding(min_periods=min_obs).rank(pct=True).values + + +def _shape(u: np.ndarray, shape: str) -> np.ndarray: + u = np.nan_to_num(np.asarray(u, float), nan=0.0) + return np.sign(u) if shape == "sign" else np.clip(u, -1.0, 1.0) + + +def _sized(direction: np.ndarray, df: pd.DataFrame) -> np.ndarray: + return A.vol_target(np.nan_to_num(direction, nan=0.0), df, + target_vol=0.20, vol_win_days=30, leverage_cap=2.0) + + +# =========================================================================== +# SERIE AUSILIARIE (calcolate una volta, riusate da tutte le celle) +# =========================================================================== +@lru_cache(maxsize=16) +def skew_aux(asset: str, w_days: int): + """Skew realizzata dei ritorni 1h su finestra trascinata di w_days giorni. + Valore al bar 1h i -> noto alla sua chiusura (ts_i + 1h).""" + h = A.get(asset, "1h") + r = A.log_returns(h["close"].values.astype(float)) + win = w_days * 24 + sk = pd.Series(r).rolling(win, min_periods=win // 2).skew().values + return h["timestamp"].astype("int64").values + 3_600_000, sk + + +@lru_cache(maxsize=4) +def disp_aux(): + """Dispersione cross-sezionale dei 51 alt Hyperliquid (1d, 2024+): deviazione standard + cross-section dei log-ritorni giornalieri. Valore del giorno d noto alla chiusura di d.""" + import glob + files = sorted(glob.glob(str(HL_DIR / "hl_*_1d.parquet"))) + cols = {} + for f in files: + x = pd.read_parquet(f, columns=["timestamp", "close"]) + s = pd.Series(x["close"].values.astype(float), + index=x["timestamp"].astype("int64").values) + cols[os.path.basename(f)] = np.log(s).diff() + M = pd.DataFrame(cols).sort_index() + n_ok = M.notna().sum(axis=1) + disp = M.std(axis=1, ddof=1).where(n_ok >= 15) + ts = M.index.values.astype("int64") + 86_400_000 # noto a fine giornata + return ts, disp.values, int(len(files)) + + +@lru_cache(maxsize=16) +def xcorr_aux(w_days: int): + """Correlazione realizzata BTC-ETH sui ritorni 1h, finestra trascinata w_days giorni.""" + b = A.get("BTC", "1h")[["timestamp", "close"]].rename(columns={"close": "b"}) + e = A.get("ETH", "1h")[["timestamp", "close"]].rename(columns={"close": "e"}) + J = pd.merge(b, e, on="timestamp", how="inner").sort_values("timestamp") + rb = pd.Series(A.log_returns(J["b"].values.astype(float))) + re = pd.Series(A.log_returns(J["e"].values.astype(float))) + win = w_days * 24 + c = rb.rolling(win, min_periods=win // 2).corr(re).values + return J["timestamp"].astype("int64").values + 3_600_000, c + + +# =========================================================================== +# LE 7 FAMIGLIE β€” factory(tf=..., **params) -> target(df, asset) +# =========================================================================== +def mk_rskew(tf="1d", w_days=30, z_days=365, shape="lin", sgn=-1): + """RSKEW: la skew realizzata recente come PREDITTORE del ritorno successivo. + A-priori (Amaya et al., azioni): skew realizzata alta -> ritorno futuro BASSO (sgn=-1).""" + def target(df, asset): + ts, sk = skew_aux(asset, w_days) + v = merge_aux(df, ts, sk) + u = np.clip(roll_z(v, z_days) / 2.0, -1.0, 1.0) + return _sized(sgn * _shape(u, shape), df) + return target + + +def mk_tacc(tf="1d", H=60, L=20, shape="lin", sgn=1): + """TACC: accelerazione = variazione della PENDENZA log-prezzo su H barre, misurata a L barre + di distanza. Direzione standalone (non e' il LIVELLO del trend). A-priori sgn=+1.""" + def target(df, asset): + c = np.log(df["close"].values.astype(float)) + n = len(c) + slope = np.full(n, np.nan) + slope[H:] = (c[H:] - c[:-H]) / H + acc = np.full(n, np.nan) + acc[L:] = slope[L:] - slope[:-L] + u = np.clip(roll_z(acc, 365) / 2.0, -1.0, 1.0) + return _sized(sgn * _shape(u, shape), df) + return target + + +def mk_vpx(tf="1d", W=30, k=5, mode="confirm", thr=0.5): + """VPX: volume-prezzo su barre 1d (a 1d il volume Deribit e' informativo, a 5m no). + confirm -> segui il movimento SOLO se il volume conferma (z >= thr) + fade_low -> fai il contrario del movimento avvenuto a volume BASSO (z <= -thr) + fade_high -> fai il contrario del movimento avvenuto a volume ALTO (climax, z >= thr)""" + def target(df, asset): + c = df["close"].values.astype(float) + vol = df["volume"].values.astype(float) + n = len(c) + vz = roll_z(np.log(np.where(vol > 0, vol, np.nan)), W) + rk = np.full(n, np.nan) + rk[k:] = c[k:] / c[:-k] - 1.0 + s = np.sign(np.nan_to_num(rk, nan=0.0)) + vzf = np.nan_to_num(vz, nan=0.0) + if mode == "confirm": + d = np.where(vzf >= thr, s, 0.0) + elif mode == "fade_low": + d = np.where(vzf <= -thr, -s, 0.0) + else: # fade_high + d = np.where(vzf >= thr, -s, 0.0) + return _sized(d, df) + return target + + +def mk_xdisp(tf="1d", S=5, trans="z252", shape="lin", sgn=1): + """XDISP: dispersione cross-sezionale dei 51 alt HL come STATO del mercato (time-series), + non come gate di un paniere cross-sectional. Nessun prior forte sul segno -> entrambi in griglia.""" + def target(df, asset): + ts, dsp, _ = disp_aux() + sm = pd.Series(dsp).rolling(S, min_periods=1).mean().values + u = (np.clip(roll_z(sm, 252) / 2.0, -1, 1) if trans == "z252" + else 2.0 * exp_pct(sm, 180) - 1.0) + v = merge_aux(df, ts, u) + return _sized(sgn * _shape(v, shape), df) + return target + + +def mk_xcorr(tf="1d", W=60, trans="z252", shape="lin", sgn=-1): + """XCORR: co-movimento BTC-ETH (corr realizzata) come stato direzionale del mercato. + A-priori debole: corr alta = 'tutto il crypto e' un trade solo' = regime tardo -> sgn=-1.""" + def target(df, asset): + ts, cr = xcorr_aux(W) + u_aux = (np.clip(roll_z(cr, 252 * 24) / 2.0, -1, 1) if trans == "z252" + else 2.0 * exp_pct(cr, 180 * 24) - 1.0) + v = merge_aux(df, ts, u_aux) + return _sized(sgn * _shape(v, shape), df) + return target + + +def mk_xtail(tf="1d", ksig=3.0, N=3, sgn=1, sigwin=30): + """XTAIL: struttura dei ritorni ESTREMI. Dopo |r| > ksig*sigma trascurata, tieni per N barre + la direzione (sgn=+1 continuazione) o il suo opposto (sgn=-1 inversione). + ATTENZIONE dichiarata: sgn=-1 e' mean-reversion, famiglia gia' MORTA -> se vince li', si chiude.""" + def target(df, asset): + c = df["close"].values.astype(float) + r = A.simple_returns(c) + sd = pd.Series(r).rolling(sigwin, min_periods=sigwin // 2).std().shift(1).values + n = len(c) + d = np.zeros(n) + hit = np.where(np.isfinite(sd) & (sd > 0) & (np.abs(r) > ksig * sd))[0] + for i in hit: + d[i:min(n, i + N)] = sgn * np.sign(r[i]) + return _sized(d, df) + return target + + +def mk_vrat(tf="1d", q=10, LB=365, conv="gate", lf=True): + """VRAT: variance-ratio VR(q)=Var(r_q)/(q*Var(r_1)) su finestra LB come CONVINZIONE CONTINUA + sul TSMOM (non uno switch binario di modo come blind/agent_37_hurst). + gate -> pesa il trend solo dove VR>1 (persistente), altrimenti flat + flip -> inverte il trend dove VR<1 (anti-persistente)""" + HZ = (30, 90, 180) + + def target(df, asset): + c = df["close"].values.astype(float) + lc = np.log(c) + n = len(c) + r1 = pd.Series(A.log_returns(c)) + rq = pd.Series(np.concatenate([np.full(q, np.nan), lc[q:] - lc[:-q]])) + v1 = r1.rolling(LB, min_periods=LB // 2).var() + vq = rq.rolling(LB, min_periods=LB // 2).var() + vr = (vq / (q * v1.where(v1 > 0))).values + dirs = np.zeros(n) + for H in HZ: + m = np.zeros(n) + m[H:] = np.sign(c[H:] / c[:-H] - 1.0) + dirs += m / len(HZ) + if lf: + dirs = np.maximum(dirs, 0.0) + k = np.nan_to_num(vr, nan=1.0) - 1.0 + w = np.clip(k * 5.0, 0.0, 1.0) if conv == "gate" else np.clip(k * 5.0, -1.0, 1.0) + return _sized(dirs * w, df) + return target + + +# ---- griglie DICHIARATE (24 celle ciascuna, 7 famiglie = 168 trial) ---------- +def _grid(**axes): + keys = list(axes) + out = [{}] + for k in keys: + out = [dict(o, **{k: v}) for o in out for v in axes[k]] + return out + + +FAMILIES = [ + dict(code="RSKEW", factory=mk_rskew, price_only=False, + grid=_grid(w_days=[14, 30, 60], z_days=[180, 365], shape=["lin", "sign"], sgn=[1, -1])), + dict(code="TACC", factory=mk_tacc, price_only=True, + grid=_grid(H=[20, 60, 120], L=[5, 20], shape=["lin", "sign"], sgn=[1, -1])), + dict(code="VPX", factory=mk_vpx, price_only=True, + grid=_grid(W=[30, 90], k=[5, 20], mode=["confirm", "fade_low", "fade_high"], thr=[0.5, 1.0])), + dict(code="XDISP", factory=mk_xdisp, price_only=False, + grid=_grid(S=[1, 5, 20], trans=["z252", "pct"], shape=["lin", "sign"], sgn=[1, -1])), + dict(code="XCORR", factory=mk_xcorr, price_only=False, + grid=_grid(W=[20, 60, 120], trans=["z252", "pct"], shape=["lin", "sign"], sgn=[1, -1])), + dict(code="XTAIL", factory=mk_xtail, price_only=True, + grid=_grid(ksig=[2.5, 3.0], N=[1, 3, 10], sgn=[1, -1], sigwin=[30, 90])), + dict(code="VRAT", factory=mk_vrat, price_only=True, + grid=_grid(q=[5, 10, 20], LB=[180, 365], conv=["gate", "flip"], lf=[True, False])), +] + + +# =========================================================================== +# GATE AGGIUNTIVI +# =========================================================================== +def cand_daily_at(fn, off: int) -> pd.Series: + """Serie giornaliera 50/50 BTC+ETH del candidato con barre ancorate all'ora `off`.""" + ser = {} + for a in A.CERTIFIED: + df = bars_at(a, off) + ev = A.eval_weights(df, A._call_target(fn, df, a)) + ser[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + return A._to_daily(0.5 * J[A.CERTIFIED[0]] + 0.5 * J[A.CERTIFIED[1]]) + + +def anchor_report(fn) -> dict: + """Banda d'ancora su 8 offset orari: Sharpe standalone E uplift del blend. + La stima ONESTA e' la MEDIANA della banda, non l'ancora 00:00 (lezione 02/07).""" + band = A.anchor_luck_band(lambda o: cand_daily_at(fn, o), ANCHOR_OFFSETS) + B = A.tp01_baseline_daily() + ups = {} + for o in ANCHOR_OFFSETS: + C = cand_daily_at(fn, o) + J = pd.concat({"B": B, "C": C}, axis=1, join="inner").dropna() + if len(J) > 30: + ups[o] = round(A._sh((1 - W_BLEND) * J["B"] + W_BLEND * J["C"]) - A._sh(J["B"]), 3) + vals = np.array(list(ups.values()), float) if ups else np.array([np.nan]) + band["uplift_per_offset"] = ups + band["uplift_canonical"] = ups.get(0) + band["uplift_median"] = round(float(np.median(vals)), 3) + band["uplift_frac_positive"] = round(float((vals > 0).mean()), 3) + return band + + +def delever_null(C: pd.Series, w: float = W_BLEND) -> dict: + """NULL DEL DE-LEVERING (obbligatorio su ogni claim di meno drawdown, 5 occorrenze nel + progetto): esiste k<1 che, applicato al SOLO TP01, da' lo STESSO maxDD con Sharpe MIGLIORE? + Se si', l'overlay non serve: bastava meno leva.""" + B = A.tp01_baseline_daily() + J = pd.concat({"B": B, "C": C}, axis=1, join="inner").dropna() + if len(J) < 60: + return dict(run=False, reason="overlap insufficiente") + blend = (1 - w) * J["B"] + w * J["C"] + dd_bl, sh_bl = A._dd_ret(blend), A._sh(blend) + dd_b, sh_b = A._dd_ret(J["B"]), A._sh(J["B"]) + if dd_bl >= dd_b: + return dict(run=True, claims_less_dd=False, dd_blend=round(dd_bl, 4), + dd_tp01=round(dd_b, 4), sharpe_blend=round(sh_bl, 3), + sharpe_tp01=round(sh_b, 3), + note="nessun claim di DD: il blend NON riduce il drawdown -> null non pertinente") + ks = np.linspace(0.05, 1.0, 96) + dds = np.array([A._dd_ret(k * J["B"]) for k in ks]) + k = float(ks[int(np.argmin(np.abs(dds - dd_bl)))]) + sh_k = A._sh(k * J["B"]) + return dict(run=True, claims_less_dd=True, dd_blend=round(dd_bl, 4), dd_tp01=round(dd_b, 4), + sharpe_blend=round(sh_bl, 3), k_equal_dd=round(k, 3), + dd_k=round(A._dd_ret(k * J["B"]), 4), sharpe_k_tp01=round(sh_k, 3), + passes=bool(sh_bl > sh_k), + note=("il blend batte il de-levering" if sh_bl > sh_k + else "REFUTED: k*TP01 da' lo stesso DD con Sharpe migliore")) + + +def smallcap_report(fn) -> dict: + out = {} + for a in A.CERTIFIED: + df = A.get(a, "1d") + sc = A.eval_weights_smallcap(df, A._call_target(fn, df, a), capital=600.0, min_order=5.0) + out[a] = dict(modeled=sc["modeled"]["sharpe"], realistic=sc["realistic"]["sharpe"], + haircut=sc["sharpe_haircut"], n_trades=sc["n_executed_trades"]) + return out + + +def active_window_metrics(C: pd.Series) -> dict: + """Trappola #9 (barre attive vs calendario): una serie che e' 0 per meta' della storia + (XDISP parte nel 2024) ha Sharpe DILUITO di sqrt(frazione attiva). Si riporta anche il + numero sulla sola finestra ATTIVA, senza usarlo per decidere.""" + nz = C[C != 0.0] + if len(nz) < 30: + return dict(active_days=int(len(nz))) + first = nz.index[0] + sub = C[C.index >= first] + return dict(active_days=int(len(nz)), frac_active=round(float(len(nz) / max(1, len(C))), 3), + first_active=str(first.date()), sharpe_active_window=round(A._sh(sub), 3), + maxdd_active_window=round(A._dd_ret(sub), 4)) + + +# =========================================================================== +# SANITY β€” un setaccio che non si controlla non sta setacciando niente +# =========================================================================== +def sanity() -> None: + print("### SANITY") + for a in A.CERTIFIED: + b0, g = bars_at(a, 0), A.get(a, "1d") + n = min(len(b0), len(g)) + d = float(np.max(np.abs(b0["close"].values[:n] - g["close"].values[:n]))) + dts = float(np.max(np.abs(b0["timestamp"].values[:n] - g["timestamp"].values[:n]))) + print(f" bars_at({a},0) vs A.get({a},'1d'): n={n}/{len(g)} max|dclose|={d:.6g} max|dts|={dts:.0f}") + assert d < 1e-9 and dts == 0, "ricostruzione barre a offset 0 NON identica al feed certificato" + ts, dsp, nfiles = disp_aux() + ok = np.isfinite(dsp) + print(f" disp_aux: {nfiles} file HL, {int(ok.sum())} giorni con >=15 asset, " + f"da {pd.Timestamp(ts[ok][0], unit='ms', tz='UTC').date()} " + f"a {pd.Timestamp(ts[ok][-1], unit='ms', tz='UTC').date()}") + B = A.tp01_baseline_daily() + print(f" TP01 baseline: n={len(B)} ShFULL={A._sh(B):.3f} ShHOLD={A._sh(B[B.index >= A.HOLDOUT]):.3f}") + print() + + +# =========================================================================== +# MAIN +# =========================================================================== +def main() -> None: + t0 = time.time() + print(__doc__.split("USO:")[0]) + sanity() + + pooled_sh: list[float] = [] + results = [] + + for fam in FAMILIES: + code, fac, grid = fam["code"], fam["factory"], fam["grid"] + t1 = time.time() + print(f"### {code} β€” {len(grid)} celle dichiarate") + sel = A.select_cell_insample(fac, grid, ("1d",)) + pooled_sh.extend([s for s in sel["all_full_sharpe"] if np.isfinite(s)]) + rep = A.study_family_honest(code, fac, grid, ("1d",)) + ch = rep["chosen"] + if ch is None: + print(f" nessuna cella valida ({rep.get('reason')})\n") + results.append(dict(code=code, n_cells=len(grid), chosen=None, + killed_by="nessuna cella valutabile")) + continue + + fn = fac(tf=ch["tf"], **ch["params"]) + C = A.candidate_daily(fn, tf=ch["tf"]) + m = rep["marginal"]["marginal"] + absr = rep["marginal"]["absolute"]["cells"][0] + + caus = A.causality_ok(fn, tf="1d") + dayb = A.day_boundary_robust(fn, tf="1d") if fam["price_only"] else \ + dict(verdict="NON GIRATO", reason="segnale con merge su serie ausiliaria: " + "spostare l'etichetta in avanti sposta il merge nel FUTURO (leak indotto), " + "non e' un controllo -> vale la banda d'ancora") + anch = anchor_report(fn) + deln = delever_null(C) + smal = smallcap_report(fn) + impl = A.implausible_sharpe(C) + actw = active_window_metrics(C) + + full_sh = A._sh(C) + hold_sh = A._sh(C[C.index >= A.HOLDOUT]) + mm = A._metrics_from_net(C.values, C.index) + + results.append(dict( + code=code, n_cells=len(grid), chosen=ch, fn=fn, C=C, + full_sharpe=round(full_sh, 3), hold_sharpe=round(hold_sh, 3), + maxdd=mm["maxdd"], cagr=mm["cagr"], + corr=m.get("corr_full"), corr_hold=m.get("corr_hold"), + verdict=rep["marginal"]["marginal_verdict"], + earns_slot=rep["earns_slot_marginal"], honest=rep["earns_slot_honest"], + dsr_family=rep["deflated_sharpe"], dsr_pass=rep["dsr_pass"], + insample_sharpe=m.get("cand_insample_sharpe"), + has_insample_edge=m.get("has_insample_edge"), + robust_oos=m.get("robust_oos"), is_hedge=m.get("is_hedge"), + uplift_full=m["blends"]["w25"]["uplift_full"], + uplift_hold=m["blends"]["w25"]["uplift_hold"], + multicut=m.get("multicut_uplift"), abs_grade=rep["marginal"]["abs_grade"], _m=m, + fee_survives=absr.get("fee_survives"), + causality=caus, dayb=dayb, anchor=anch, delever=deln, + smallcap=smal, implausible=impl, active=actw, + )) + + print(f" cella IN-SAMPLE-ONLY: {ch['params']} (IS Sh {ch['insample_sharpe']}, " + f"rango 1/{len(sel['rows'])} in-sample; FULL Sh di quella cella {ch['full_sharpe']})") + print(f" standalone FULL {full_sh:+.3f} | HOLD {hold_sh:+.3f} | maxDD {mm['maxdd']:.1%} | " + f"CAGR {mm['cagr']:+.2%} | IS {m.get('cand_insample_sharpe')}") + print(f" marginale {rep['marginal']['marginal_verdict']:<9} corr->TP01 {m.get('corr_full')} " + f"(hold {m.get('corr_hold')}) uplift w25 full {m['blends']['w25']['uplift_full']:+.3f} " + f"hold {m['blends']['w25']['uplift_hold']}") + print(f" robust_oos={m.get('robust_oos')} insample_edge={m.get('has_insample_edge')} " + f"is_hedge={m.get('is_hedge')} abs={rep['marginal']['abs_grade']} " + f"fee_survives={absr.get('fee_survives')}") + print(f" DSR famiglia {rep['deflated_sharpe']} (atteso null max {rep['expected_null_max']}) " + f"pass={rep['dsr_pass']} earns_slot_honest={rep['earns_slot_honest']}") + print(f" causality ok={caus['ok']} max_tail_diff={caus['max_tail_diff']}") + print(f" confine {dayb.get('verdict')} " + + (f"spread {dayb.get('spread')} per_offset {dayb.get('per_offset')}" + if fam["price_only"] else f"({dayb.get('reason','')[:60]}...)")) + _nan = float("nan") + print(f" ancora Sh canonica {anch.get('canonical', _nan):+.3f} " + f"(pctl {anch.get('canonical_pctl')}) " + f"MEDIANA {anch.get('median', _nan):+.3f} " + f"banda [{anch.get('lo', _nan):+.3f},{anch.get('hi', _nan):+.3f}] " + f"frac>0 {anch.get('frac_positive')}") + print(f" uplift canonico {anch.get('uplift_canonical')} MEDIANA " + f"{anch.get('uplift_median')} frac>0 {anch.get('uplift_frac_positive')}") + print(f" de-levering {deln.get('note')}") + print(f" $600 " + " ".join( + f"{a}: mod {v['modeled']:+.2f} -> real {v['realistic']:+.2f} (haircut {v['haircut']:+.2f}, " + f"{v['n_trades']} trade)" for a, v in smal.items())) + print(f" implausible {impl.get('implausible')} {impl.get('reasons')}") + if actw.get("frac_active", 1.0) < 0.9: + print(f" attive {actw}") + print(f" [{time.time() - t1:.1f}s]\n") + + # ---- DSR DI SCREEN: i trial sono TUTTE le celle di TUTTE le famiglie ------------- + print("### DSR DI SCREEN (il conto onesto per un setaccio: scelgo il meglio fra " + f"{len(pooled_sh)} celle, non fra 24)") + for r in results: + if r.get("chosen") is None: + continue + d, s0 = A.deflated_sharpe(r["full_sharpe"], pooled_sh, r["C"]) + r["dsr_screen"] = round(d, 3) if np.isfinite(d) else None + print(f" {r['code']:<6} Sh {r['full_sharpe']:+.3f} DSR famiglia {r['dsr_family']} " + f"DSR screen {r['dsr_screen']} (null max atteso {s0:.3f})") + print() + + deep_dive(results) + + # ---- TABELLA FINALE -------------------------------------------------------------- + def killer(r) -> str: + if r.get("chosen") is None: + return "nessuna cella valutabile" + why = [] + if not r["causality"]["ok"]: + why.append("CAUSALITY") + if r["verdict"] != "ADDS": + why.append(f"marginal={r['verdict']}") + if not r["has_insample_edge"]: + why.append("no in-sample edge (<0.5)") + if not r["robust_oos"]: + why.append("robust_oos") + if r["is_hedge"]: + why.append("is_hedge") + if not r["dsr_pass"]: + why.append(f"DSR fam {r['dsr_family']}") + if r.get("dsr_screen") is not None and r["dsr_screen"] < 0.95: + why.append(f"DSR screen {r['dsr_screen']}") + if r["dayb"].get("verdict") == "ARTIFACT-RISK": + why.append("confine ARTIFACT-RISK") + um = r["anchor"].get("uplift_median") + if um is not None and np.isfinite(um) and um <= 0: + why.append("ancora: uplift mediano <=0") + if r["delever"].get("claims_less_dd") and not r["delever"].get("passes"): + why.append("null de-levering") + if r["implausible"].get("implausible"): + why.append("implausible_sharpe") + return " + ".join(why) if why else "-- nessun gate lo uccide --" + + print("### TABELLA β€” ORTHO-SCREEN 2026-08-22 (candidato | trial | ShFULL | hold-out | " + "corr->TP01 | marginale | DSR fam / screen | esito)") + hdr = (f"{'cand':<7}{'trial':>6}{'ShFULL':>8}{'hold':>7}{'maxDD':>8}{'CAGR':>8}" + f"{'corr':>7} {'marginale':<10}{'DSRfam':>7}{'DSRscr':>8} esito / gate che l'ha ucciso") + print(hdr); print("-" * len(hdr)) + for r in results: + if r.get("chosen") is None: + print(f"{r['code']:<7}{r['n_cells']:>6}{'--':>8}{'--':>7}{'--':>8}{'--':>8}{'--':>7} " + f"{'--':<10}{'--':>7}{'--':>8} SCARTATO β€” {killer(r)}") + continue + ok = (r["verdict"] == "ADDS" and r["dsr_pass"] and (r.get("dsr_screen") or 0) >= 0.95 + and r["causality"]["ok"]) + esito = "CANDIDATO" if ok else "SCARTATO" + print(f"{r['code']:<7}{r['n_cells']:>6}{r['full_sharpe']:>+8.3f}{r['hold_sharpe']:>+7.2f}" + f"{r['maxdd']:>8.1%}{r['cagr']:>+8.1%}{r['corr']:>7.2f} {r['verdict']:<10}" + f"{str(r['dsr_family']):>7}{str(r.get('dsr_screen')):>8} {esito} β€” {killer(r)}") + print() + print("### FAMIGLIE NON TESTATE (l'elenco di cio' che non ho guardato fa parte del risultato)") + for line in NOT_TESTED: + print(" - " + line) + print(f"\n[totale {time.time() - t0:.1f}s]") + + +def mk_mom(tf="1d", H=5, shape="lin"): + """CONTROLLO per TACC: il LIVELLO del momentum a orizzonte H, stessa pipeline + (z-score 365g -> clip -> vol_target). Serve a rispondere alla domanda della famiglia: + l'ACCELERAZIONE aggiunge qualcosa al LIVELLO, o e' un momentum corto travestito?""" + def target(df, asset): + c = df["close"].values.astype(float) + n = len(c) + m = np.full(n, np.nan) + m[H:] = c[H:] / c[:-H] - 1.0 + u = np.clip(roll_z(m, 365) / 2.0, -1.0, 1.0) + return _sized(_shape(u, shape), df) + return target + + +MOM_CONTROL_GRID = _grid(H=[5, 20, 60], shape=["lin", "sign"]) # 6 celle di CONTROLLO + + +def deep_dive(results) -> None: + """APPROFONDIMENTO sull'unica famiglia arrivata a marginal=ADDS. + Due domande, entrambe misurate: + (a) QUALE sotto-gate di robust_oos ha fallito (clean-year? jackknife? multi-cut?); + (b) l'accelerazione e' distinta dal LIVELLO del momentum corto, o e' quello travestito? + -> 6 celle di CONTROLLO dichiarate (H x shape), che si SOMMANO ai 168 trial.""" + adds = [r for r in results if r.get("chosen") is not None and r["verdict"] == "ADDS"] + if not adds: + print("### APPROFONDIMENTO β€” nessuna famiglia a marginal=ADDS, niente da approfondire\n") + return + r = max(adds, key=lambda x: x["uplift_hold"] or -9) + print(f"### APPROFONDIMENTO β€” {r['code']} (l'unica a marginal=ADDS)") + print(f" perche' robust_oos=False: clean_year_uplift={r['_m'].get('clean_year_uplift')} " + f"(serve >0.02) jackknife_min_uplift={r['_m'].get('jackknife_min_uplift')} (serve >0)") + print(f" multicut (uplift a ogni taglio d'anno): {r['multicut']}") + print(f" uplift TP01-up {r['_m'].get('uplift_tp01_up')} / TP01-down {r['_m'].get('uplift_tp01_down')} " + f"| hedge_yearly_corr {r['_m'].get('hedge_yearly_corr')} | alpha/anno {r['_m'].get('alpha_ann')}") + + print(" --- accelerazione vs LIVELLO del momentum (6 celle di controllo) ---") + B = A.tp01_baseline_daily() + C = r["C"] + best = None + rows_ctl = [] + for p in MOM_CONTROL_GRID: + fnm = mk_mom(tf="1d", **p) + Cm = A.candidate_daily(fnm, tf="1d") + J = pd.concat({"A": C, "M": Cm}, axis=1, join="inner").dropna() + JB = pd.concat({"B": B, "M": Cm}, axis=1, join="inner").dropna() + up = A._sh((1 - W_BLEND) * JB["B"] + W_BLEND * JB["M"]) - A._sh(JB["B"]) + row = dict(params=p, sharpe=round(A._sh(Cm), 3), + insample=round(A._sh(Cm[Cm.index < A.HOLDOUT]), 3), + hold=round(A._sh(Cm[Cm.index >= A.HOLDOUT]), 3), + corr_to_acc=round(float(J["A"].corr(J["M"])), 3), + corr_to_tp01=round(float(JB["B"].corr(JB["M"])), 3), + uplift_w25_full=round(up, 3)) + print(f" MOM {str(p):<28} Sh {row['sharpe']:+.3f} IS {row['insample']:+.3f} " + f"hold {row['hold']:+.3f} corr->{r['code']} {row['corr_to_acc']:+.3f} " + f"corr->TP01 {row['corr_to_tp01']:+.3f} uplift {row['uplift_w25_full']:+.3f}") + rows_ctl.append(row) + if best is None or row["corr_to_acc"] > best["corr_to_acc"]: + best = row + print(f" livello piu' vicino all'accelerazione: {best['params']} corr {best['corr_to_acc']:+.3f}") + print(" lettura: corr alta col LIVELLO => l'accelerazione non e' una dimensione nuova; " + "corr bassa => e' distinta ma resta da spiegare perche' non sopravvive al DSR.") + bh = max(rows_ctl, key=lambda x: x["hold"]) + bi = max(rows_ctl, key=lambda x: x["insample"]) + print(f" TRAPPOLA DICHIARATA: la cella di controllo migliore SULL'HOLD-OUT e' {bh['params']} " + f"(hold {bh['hold']:+.3f}); quella scelta IN-SAMPLE-ONLY e' {bi['params']} " + f"(IS {bi['insample']:+.3f}, hold {bi['hold']:+.3f}). Se coincidono, il controllo e'" + " selezionabile; se no, guardare la colonna hold e' selezione-sull'hold-out (trappola 3)" + " e NON promuove nulla.\n") + + +NOT_TESTED = [ + "catena opzioni / vol term-structure: 3-5 mesi di storia -> non esiste hold-out, al massimo " + "un LEAD; e altri agenti dell'ondata ci stanno sopra.", + "funding (carry, time-series, cross-sectional): filone dichiarato CHIUSO su 3 lati.", + "calendario in ogni forma (weekend 375 trial, expiry, stagionalita', event-clock, " + "ora-del-giorno): famiglia dichiarata SATURA.", + "mean-reversion pura / fade: morta anche a fee zero. Qui rientra SOLO come branch dichiarato " + "di XTAIL e VPX, per misurarla dentro un conditioner nuovo.", + "microstruttura 5m/15m: morta per fee, e una sweep su 5m sfonda il budget macchina.", + "cross-sectional sui 51 alt HL: territorio XS01, e l'espansione d'universo e' gia' refutata. " + "Qui i 51 alt entrano solo come AGGREGATO time-series (XDISP).", + "equity / GTAA / MAT01: GTAA01 non e' deployabile (PRIIPs) e MAT01 e' refutato.", + "macro regime gate: ridondante col trend (corr 0.989).", + "CRT / ICT / Elliott / Fibonacci / Albimarini: 3 ondate di refutazione.", + "gamma scalping long-vol e overlay DD su VRP01 (5/5 refutati).", + "SOL come terza gamba: decisione dell'operatore del 22/08.", +] + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_prop_alloc.py b/scripts/research/r0822_prop_alloc.py new file mode 100644 index 0000000..5b45f6b --- /dev/null +++ b/scripts/research/r0822_prop_alloc.py @@ -0,0 +1,975 @@ +"""r0822_prop_alloc β€” PROGETTARE l'allocazione per la BARRIERA, non per lo Sharpe (2026-08-22). + +IL BUCO CHE CHIUDE +------------------ +Il canale prop/funded e' l'unico misurato che moltiplica il nozionale senza possedere capitale. +Il progetto lo ha gia' VALUTATO (24-25/07: `r0725_prop_ladder.py`, `r0725_prop_coupled.py`, +`r0725_hyro.py`) ma non lo ha mai **PROGETTATO**: ha sempre preso il libro ottimizzato per lo +Sharpe (config A = live 75/25, config B = 55/20/25 scelta a mano) e poi MISURATO P(pass) del +risultato. Due punti nello spazio dei pesi, tre leve, nessuna ottimizzazione, nessun null. + +Qui la funzione obiettivo cambia: sotto una **barriera di maxDD STATICA per-conto** conta la coda +sinistra e la DURATA, non il rapporto rendimento/vol. Si ottimizza + + J(w, k) = P(passare l'eval) x P(essere vivo dopo 12 mesi da funded) + +su pesi w e leva k, con la lente WICK ACCOPPIATA (la `close-only` e' CIECA: 0% di breach da +daily-loss su ogni configurazione, la regola non scatta mai sulle chiusure). + +LA PREVISIONE, REGISTRATA PRIMA DI MISURARE +------------------------------------------- +Nel limite gaussiano con leva LIBERA l'obiettivo si riduce allo Sharpe, e si vede in due righe. +Per un moto browniano con drift mu e vol sigma a leva k, la probabilita' di toccare +T prima di +-D vale P = (1-e^{-theta*D})/(1-e^{-theta*(T+D)}) con **theta = 2*mu/(k*sigma^2)**: la leva entra +SOLO dividendo theta. Alla leva minima che porta a +T dentro l'orizzonte (k ~ T/mu) si ottiene +theta ~ 2*mu^2/(T*sigma^2) = 2*S^2/T, con S = Sharpe: **la vol sparisce e resta lo Sharpe al +quadrato**. Quindi mi aspetto PRIMA di misurare: + (a) l'argmax di J vicino all'argmax di Sharpe -> la premessa "obiettivo diverso" e' in gran + parte FALSA sui PESI; + (b) il contenuto vero nella **LEVA**, che lo Sharpe non determina affatto, e nella deviazione + dal gaussiano (skew, wick, daily-loss: una soglia ASSOLUTA non e' scale-invariante); + (c) leva ottima per J **piu' bassa** di quella ottima per il payout (0.75x, 25/07): J e' una + probabilita' di sopravvivenza, il payout e' un flusso. +Se (a) risulta falsa, e' un risultato; se risulta vera, il filone "riallocare per la barriera" +si chiude e si smette di pagarlo. + +CONVENZIONI DICHIARATE +---------------------- +* Macchineria del wick ACCOPPIATO **riusata** da `r0725_prop_coupled.py` (import, non riscritta): + `_hourly_legs` (recon MTM orario TP01/SKH01), `crypto_daily_tuples` (minimo ESATTO sul path + orario condiviso), `xsec_daily_tuples` (XS01 dagli OHLC 1d, ordine condiviso avverso). +* VRP01 e GTAA01 sono AGGIUNTI qui (non c'erano): GTAA01 col wick dagli OHLC dei 6 ETF (long-only + co-moventi -> ogni gamba al proprio minimo = convenzione severa dichiarata); VRP01 con + **gap = 0**, cioe' CIECO, perche' il suo mark infra-settimana non e' nel dataset dello sleeve + (il rendimento settimanale e' lumpato sul giorno di scadenza). E' una lente GENEROSA per VRP01: + la misura del 30/07 sulle quote reali dice mark peggiore infra-settimana mediano -6.9% e minimo + **-81.7%** del capitale su un trade finito in UTILE. Se VRP01 perde sotto una lente generosa, + perde. +* de-luck sul DRIFT **x0.89** (fattore MISURATO il 26/07 sul libro live, `r0726_deluck_factor.py`), + non x0.6 come nel 25/07 (li' era stimato a occhio e ri-misurato troppo severo del 31-34%). + Sensibilita' a 1.00 e 0.60 girata: cio' che conta e' se l'ORDINE fra le configurazioni tiene. +* bootstrap a blocchi (20g) con **indici CONDIVISI fra tutte le configurazioni** -> le differenze + fra configurazioni sono APPAIATE e il loro errore MC e' molto minore di quello dei livelli. +* finestra comune **2024+** (XS01 nasce li'): ~2.6 anni, ed e' la finestra in cui XS01 e' stato + scoperto -> le configurazioni che lo contengono sono FAVORITE per costruzione. Dichiarato. +* fisco 33% sui payout, split 80%, regole vere delle due firm (max-loss STATICO dal saldo + iniziale, base ripristinata dopo il prelievo β€” bug del 25/07 gia' corretto a monte). + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822_prop_alloc.py` +""" +from __future__ import annotations + +import sys +import time +from functools import lru_cache +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +from src.data.eq_splits import repair_splits # noqa: E402 +from src.portfolio.gtaa import ( # noqa: E402 + ANN, EQ_UNIVERSE, GTAA_DEFAULT_CAPITAL, HORIZONS, IB_MIN_TRADE_USD, REBAL_BAND_USD, + REBAL_EVERY, TARGET_VOL, ib_commission, +) +from src.portfolio.portfolio import metrics, weights_tilt_null # noqa: E402 +from src.portfolio.sleeves import ( # noqa: E402 + GTAA_BOOK_ACTIVATION, _gtaa_daily_returns, _skyhook_returns, _tp01_returns, + _vrp_combo_returns, _xsec_returns, +) + +import r0725_prop_coupled as pc # noqa: E402 +import r0725_prop_ladder as pl # noqa: E402 +from r0725_capcurve import EURUSD, TAX_RATE # noqa: E402 + +RAW = ROOT / "data" / "raw" + +# ------------------------------------------------------------------ costanti dichiarate +DELUCK = 0.89 # fattore d'ancora MISURATO sul drift (26/07), non stimato a occhio +START_WIN = "2024-01-01" # finestra comune ai 5 sleeve (XS01 nasce qui) +BLOCK = 20 # blocco del bootstrap, in giorni +N_PATHS = 2500 +SPLIT = pl.SPLIT # 80% al trader +TARGET_EUR_DAY = 50.0 +FIRMS = {f["name"]: f for f in pl.FIRMS} +SLEEVES = ("TP01", "SKH01", "XS01", "VRP01", "GTAA01") +CRYPTO_PROP = ("TP01", "SKH01", "XS01") # cio' che una firm CRYPTO puo' davvero far eseguire + + +# ================================================================== Β§0. PANNELLO (R, wick) + +@lru_cache(maxsize=8) +def _eq_ohlc(sym: str) -> pd.DataFrame: + p = RAW / f"eq_{sym.lower()}_1d.parquet" + d = pd.read_parquet(p) + d.index = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + d = d.sort_index() + d, _ = repair_splits(d) # split IWM/EFA non aggiustati (25/07) + return d[["open", "high", "low", "close"]].astype(float) + + +def _gtaa_exposure(close: pd.Series) -> np.ndarray: + """Copia FEDELE di gtaa._exposure (che prende una Series di soli close).""" + px = close.values + n = len(px) + tgt = np.zeros(n) + mh = max(HORIZONS) + for i in range(mh, n): + tgt[i] = np.mean([1.0 if px[i] > px[i - H] else 0.0 for H in HORIZONS]) + s = pd.Series(tgt, index=close.index) + rv = close.pct_change().rolling(63, min_periods=20).std().shift(1) * ANN + scale = np.clip(np.nan_to_num(TARGET_VOL / rv.replace(0, np.nan).values, nan=0.0), 0, 10.0) + return (s * scale).clip(0, 1.0).values + + +@lru_cache(maxsize=2) +def gtaa_daily_tuples() -> pd.DataFrame: + """(R, m) giornalieri di GTAA01: chiusura replicata da gtaa.gtaa_returns e minimo intraday + dagli OHLC delle 6 gambe. Long-only e co-moventi -> convenzione **ogni gamba al proprio + minimo insieme** (severa, la stessa famiglia del bound 'perleg' di XS01). + Weekend/festivi: R = 0 e m = 0 (capitale IB fermo β€” convenzione del book).""" + cap_leg = GTAA_DEFAULT_CAPITAL / len(EQ_UNIVERSE) + legs_r, legs_m = {}, {} + for sym in EQ_UNIVERSE: + d = _eq_ohlc(sym) + close = d["close"] + ex = np.nan_to_num(_gtaa_exposure(close)) + px = close.values + prev = np.concatenate(([np.nan], px[:-1])) + ret = np.zeros(len(px)) + ret[1:] = px[1:] / px[:-1] - 1.0 + ret_low = d["low"].values / prev - 1.0 # long-only: il peggio e' il minimo + held = np.empty(len(ex)) + comm = np.zeros(len(ex)) + cur = 0.0 + for i in range(len(ex)): # identico a gtaa._gated_returns + if i % REBAL_EVERY == 0: + notional = abs(ex[i] - cur) * cap_leg + if notional >= max(REBAL_BAND_USD, IB_MIN_TRADE_USD): + comm[i] = ib_commission(notional, px[i]) / cap_leg + cur = ex[i] + held[i] = cur + pos = np.zeros(len(held)) + pos[1:] = held[:-1] + legs_r[sym] = pd.Series(pos * ret - comm, index=close.index) + legs_m[sym] = pd.Series(pos * np.nan_to_num(ret_low) - comm, index=close.index) + R = pd.concat(legs_r, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna() + M = pd.concat(legs_m, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna() + days = pd.date_range(R.index.min().normalize(), R.index.max().normalize(), freq="1D", tz="UTC") + R = R.reindex(days).fillna(0.0) + M = M.reindex(days).fillna(0.0) + out = pd.DataFrame({"R": R, "m": np.minimum(M, R)}) + return out[out.index >= GTAA_BOOK_ACTIVATION] + + +@lru_cache(maxsize=2) +def vrp_daily_tuples() -> pd.DataFrame: + """(R, m) di VRP01 con **m = R**: il mark infra-settimana NON e' nel dataset dello sleeve + (rendimento settimanale lumpato sul giorno di scadenza). Lente CIECA e GENEROSA β€” dichiarata.""" + r = _vrp_combo_returns().dropna().sort_index() + if r.index.tz is None: + r.index = r.index.tz_localize("UTC") + return pd.DataFrame({"R": r.values, "m": r.values}, index=r.index) + + +def _d(s: pd.Series) -> pd.Series: + s = s.dropna().sort_index() + if s.index.tz is None: + s.index = s.index.tz_localize("UTC") + return s + + +_CRY_CACHE: dict = {} + + +def _crypto_stressed(w_tp: float, w_skh: float, m_tp: float, m_skh: float) -> pd.DataFrame: + """Come `pc.crypto_daily_tuples` (minimo ESATTO sul path orario condiviso) ma con un taglio + del DRIFT applicato alla singola gamba PRIMA di comporre il path. Serve perche' il minimo + giornaliero non e' separabile per gamba: non si puo' tagliare il drift a valle.""" + (J,) = pc._hourly_legs() + legs = {} + for nm, (rc, wc, mult) in {"tp": ("tp_r", "tp_w", m_tp), "sk": ("skh_r", "skh_w", m_skh)}.items(): + r = J[rc].values.copy() + w = J[wc].values.copy() + if mult != 1.0: + cut = (1.0 - mult) * r.mean() + r = r - cut + w = w - cut + legs[nm] = (r, w) + r_h = w_tp * legs["tp"][0] + w_skh * legs["sk"][0] + w_h = w_tp * legs["tp"][1] + w_skh * legs["sk"][1] + idx = J.index + days = idx.floor("D") + rows = [] + for day, pos in pd.Series(np.arange(len(idx)), index=days).groupby(level=0): + sl = pos.values + r = r_h[sl] + wk = w_h[sl] + cum = np.cumprod(1.0 + r) + cum_prev = np.concatenate(([1.0], cum[:-1])) + m = min(cum.min(), (cum_prev * (1.0 + wk)).min()) - 1.0 + rows.append((day, cum[-1] - 1.0, min(m, cum[-1] - 1.0))) + return pd.DataFrame(rows, columns=["day", "R", "m"]).set_index("day") + + +def _crypto(w_tp: float, w_skh: float, m_tp: float = 1.0, m_skh: float = 1.0) -> pd.DataFrame: + key = (round(w_tp, 6), round(w_skh, 6), round(m_tp, 4), round(m_skh, 4)) + if key not in _CRY_CACHE: + _CRY_CACHE[key] = (pc.crypto_daily_tuples(w_tp, w_skh) if (m_tp == 1.0 and m_skh == 1.0) + else _crypto_stressed(w_tp, w_skh, m_tp, m_skh)) + return _CRY_CACHE[key] + + +@lru_cache(maxsize=4) +def _panel_index(start: str) -> pd.DatetimeIndex: + """Griglia comune ai 5 sleeve (intersezione), dal `start` in poi.""" + idx = _crypto(1.0, 0.0).index + for f in (pc.xsec_daily_tuples(), vrp_daily_tuples(), gtaa_daily_tuples()): + idx = idx.intersection(f.index) + return idx[idx >= pd.Timestamp(start, tz="UTC")] + + +_RG_CACHE: dict = {} + + +def book_RG(w: dict, start: str = START_WIN, + drift_mult: tuple = ()) -> tuple[np.ndarray, np.ndarray]: + """(R, gap) giornalieri di un libro a pesi arbitrari sui 5 sleeve. + Crypto TP01+SKH01: minimo ESATTO sul path orario condiviso (r0725_prop_coupled). + XS01/VRP01/GTAA01: il loro minimo si SOMMA (worst simultaneo β€” convenzione del 25/07). + `drift_mult` = (("XS01", 0.5), ...): taglia il DRIFT di quello sleeve (non la vol) β€” serve + per lo stress giudiziale su uno sleeve scoperto DENTRO la finestra di misura.""" + dm = dict(drift_mult) + key = (start, drift_mult) + tuple(round(float(w.get(nm, 0.0)), 6) for nm in SLEEVES) + if key in _RG_CACHE: + return _RG_CACHE[key] + idx = _panel_index(start) + w_tp, w_skh = w.get("TP01", 0.0), w.get("SKH01", 0.0) + R = np.zeros(len(idx)) + M = np.zeros(len(idx)) + if w_tp > 0 or w_skh > 0: + c = _crypto(w_tp, w_skh, dm.get("TP01", 1.0), dm.get("SKH01", 1.0)).reindex(idx) + R += c["R"].values + M += c["m"].values + for nm, fr in (("XS01", pc.xsec_daily_tuples()), ("VRP01", vrp_daily_tuples()), + ("GTAA01", gtaa_daily_tuples())): + wt = w.get(nm, 0.0) + if wt > 0: + f = fr.reindex(idx) + rv = np.nan_to_num(f["R"].values) + mv = np.nan_to_num(f["m"].values) + mult = dm.get(nm, 1.0) + if mult != 1.0: # taglia il drift, lascia la forma della coda + cut = (1.0 - mult) * rv.mean() # NB: media PRIMA del taglio + rv = rv - cut + mv = mv - cut + R += wt * rv + M += wt * mv + R = np.nan_to_num(R) + M = np.nan_to_num(M) + _RG_CACHE[key] = (R, np.minimum(M, R) - R) + return _RG_CACHE[key] + + +def deluck(R: np.ndarray, factor: float = DELUCK) -> np.ndarray: + return R - (1.0 - factor) * R.mean() + + +# ================================================================== Β§1. SIMULATORE + +def boot_idx(n_hist: int, n_days: int, n_paths: int, seed: int, block: int = BLOCK) -> np.ndarray: + rng = np.random.default_rng(seed) + n_blocks = int(np.ceil(n_days / block)) + st = rng.integers(0, n_hist - block, size=(n_paths, n_blocks)) + return (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] + + +def eval_sim(R: np.ndarray, G: np.ndarray, idx: np.ndarray, lev: float, firm: str, + lens: str = "coupled") -> dict: + """P(passare l'eval) entro l'orizzonte del bootstrap + giorni al pass.""" + tgt, dd, dl = FIRMS[firm]["ev"] + r = R[idx] * lev + g = (G[idx] * lev) if lens == "coupled" else np.zeros_like(r) + P, D = r.shape + eq = np.ones(P) + alive = np.ones(P, bool) + passed = np.zeros(P, bool) + tpass = np.full(P, D, int) + for t in range(D): + act = alive & ~passed + if not act.any(): + break + rt, gt = r[:, t], g[:, t] + eqp = eq + eq = np.where(act, eq * (1.0 + rt), eq) + eq_low = np.where(act, eqp * (1.0 + rt + gt), eq) + bust = act & ((eq_low < 1.0 - dd) | (rt + gt < -dl)) + alive &= ~bust + ok = act & ~bust & (eq >= 1.0 + tgt) + tpass[ok] = t + passed |= ok + return dict(p_pass=float(passed.mean()), passed=passed, + t_pass=float(np.median(tpass[passed])) if passed.any() else np.nan) + + +def funded_sim(R: np.ndarray, G: np.ndarray, idx: np.ndarray, lev: float, firm: str, + notional: float, lens: str = "coupled") -> dict: + """P(vivo a fine orizzonte) + payout netto cumulato (prelievo mensile, base ripristinata).""" + ml, dl = FIRMS[firm]["fu"] + r = R[idx] * lev + g = (G[idx] * lev) if lens == "coupled" else np.zeros_like(r) + P, D = r.shape + eq = np.ones(P) + alive = np.ones(P, bool) + payout = np.zeros(P) + alive_at = {} + for t in range(D): + rt, gt = r[:, t], g[:, t] + eqp = eq + eq = np.where(alive, eq * (1.0 + rt), eq) + eq_low = np.where(alive, eqp * (1.0 + rt + gt), eq) + bust = alive & ((eq_low < 1.0 - ml) | (rt + gt < -dl)) + alive &= ~bust + if (t + 1) % 30 == 0: + take = alive & (eq > 1.0) + payout += np.where(take, (eq - 1.0) * notional, 0.0) * SPLIT * (1.0 - TAX_RATE) + eq = np.where(take, 1.0, eq) + if (t + 1) % 365 == 0: + alive_at[(t + 1) // 365] = float(alive.mean()) + return dict(p_alive=float(alive.mean()), alive_at=alive_at, + e_payout=float(payout.mean()), payout=payout, alive=alive) + + +def objective(w: dict, lev: float, firm: str, ev_idx: np.ndarray, fu_idx: np.ndarray, + lens: str = "coupled", factor: float = DELUCK, + notional: float = 100_000.0, drift_mult: tuple = ()) -> dict: + R, G = book_RG(w, drift_mult=drift_mult) + R = deluck(R, factor) + e = eval_sim(R, G, ev_idx, lev, firm, lens) + f = funded_sim(R, G, fu_idx, lev, firm, notional, lens) + ann = float(R.mean() * 365.0) + vol = float(R.std() * np.sqrt(365.0)) + # stimatore per-percorso di J: eval e funded girano su indici di bootstrap INDIPENDENTI, + # quindi mean(passed_i * alive_i) stima p_pass*p_alive ed e' APPAIABILE fra configurazioni. + jvec = (e["passed"] & f["alive"]).astype(float) + return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"], + e_payout=f["e_payout"], sharpe=(ann / vol if vol > 0 else 0.0), + vol=vol, drift=ann, t_pass=e["t_pass"], jvec=jvec) + + +def paired_delta(wA: dict, levA: float, wB: dict, levB: float, firm: str, + ev_idx: np.ndarray, fu_idx: np.ndarray, + drift_mult: tuple = ()) -> tuple[float, float]: + """Differenza APPAIATA di J fra due configurazioni (stessi indici di bootstrap) + errore + standard della differenza. Il livello di J ha SE ~0.01; la DIFFERENZA molto meno. + ⚠ `drift_mult` va passato: dimenticarlo confronta due configurazioni NON stressate e + stampa un delta che non c'entra con la riga in cui compare (errore commesso in sessione).""" + a = objective(wA, levA, firm, ev_idx, fu_idx, drift_mult=drift_mult)["jvec"] + b = objective(wB, levB, firm, ev_idx, fu_idx, drift_mult=drift_mult)["jvec"] + d = a - b + return float(d.mean()), float(d.std(ddof=1) / np.sqrt(len(d))) + + +# ================================================================== Β§2. IL CONTO ANALITICO + +def p_hit_target_first(mu: float, sig: float, lev: float, T: float, D: float) -> float: + """Gambler's ruin per moto browniano con drift: P(toccare +T prima di -D), SENZA limite di + tempo. theta = 2*mu/(k*sigma^2): la leva entra SOLO dividendo theta -> con leva -> inf la + probabilita' tende a D/(T+D) (37.5% con 10%/6%), qualunque sia lo Sharpe.""" + if sig <= 0: + return float(mu > 0) + theta = 2.0 * mu / (lev * sig * sig) + if abs(theta) < 1e-12: + return D / (T + D) + return float((1.0 - np.exp(-theta * D)) / (1.0 - np.exp(-theta * (T + D)))) + + +# ================================================================== Β§3. GRIGLIA / NULL + +def simplex_grid(names: tuple, step_den: int) -> list[dict]: + """Tutti i vettori di peso a passo 1/step_den sul simplesso di `names` (somma 1).""" + out = [] + n = len(names) + + def rec(prefix, rem, k): + if k == n - 1: + out.append(dict(zip(names, [p / step_den for p in prefix] + [rem / step_den]))) + return + for v in range(rem + 1): + rec(prefix + [v], rem - v, k + 1) + + rec([], step_den, 0) + return out + + +def wkey(w: dict) -> str: + return "/".join(f"{int(round(100 * w.get(nm, 0.0))):d}" for nm in CRYPTO_PROP) + + +def grid_scan(grid: list[dict], levs: tuple, firm: str, ev_idx: np.ndarray, fu_idx: np.ndarray, + lens: str = "coupled", factor: float = DELUCK) -> pd.DataFrame: + rows = [] + for w in grid: + for lev in levs: + o = objective(w, lev, firm, ev_idx, fu_idx, lens, factor) + rows.append(dict(w=wkey(w), lev=lev, + **{k: v for k, v in o.items() if k != "jvec"})) + return pd.DataFrame(rows) + + +def random_weights(n: int, names: tuple, seed: int, floor: float = 0.0, + caps: dict | None = None) -> list[dict]: + """Stesso campionatore di `portfolio.weights_tilt_null` (dirichlet uniforme sul simplesso + + rejection su floor/caps), ma qui serve a costruire il NULL della funzione obiettivo J.""" + rng = np.random.default_rng(seed) + caps_v = np.array([(caps or {}).get(nm, 1.0) for nm in names], float) + out = [] + while len(out) < n: + b = rng.dirichlet(np.ones(len(names)), size=4 * n + 64) + ok = (b >= floor).all(axis=1) & (b <= caps_v).all(axis=1) + out.extend(b[ok]) + return [dict(zip(names, v)) for v in out[:n]] + + +# ================================================================== main + +def hr(t: str = "") -> None: + print("\n" + "-" * 104) + if t: + print(f" {t}") + print("-" * 104) + + +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + t0 = time.time() + + def tick(lab: str) -> None: + print(f" [{time.time()-t0:6.0f}s] {lab}") + + print("=" * 104) + print(" PROP-ALLOC β€” allocare per la BARRIERA (P(pass) x P(vivo 12m)) invece che per lo Sharpe") + print("=" * 104) + + # ---------------------------------------------------------------- A. pannello + sanity + idx = _panel_index(START_WIN) + print(f"\n finestra comune ai 5 sleeve: {len(idx)} giorni {idx[0].date()} -> {idx[-1].date()}" + f" (XS01 nasce nel 2024: e' anche la sua finestra di SCOPERTA -> lo favorisce)") + print(f" de-luck sul drift x{DELUCK} (misurato 26/07) | bootstrap a blocchi di {BLOCK}g, " + f"indici CONDIVISI fra configurazioni | {N_PATHS} percorsi") + + tick("pannello costruito") + off = {"TP01": _d(_tp01_returns()), "SKH01": _d(_skyhook_returns()), "XS01": _d(_xsec_returns()), + "VRP01": _d(_vrp_combo_returns()), "GTAA01": _d(_gtaa_daily_returns())} + rec = {"TP01": _crypto(1.0, 0.0)["R"], "SKH01": _crypto(0.0, 1.0)["R"], + "XS01": pc.xsec_daily_tuples()["R"], "VRP01": vrp_daily_tuples()["R"], + "GTAA01": gtaa_daily_tuples()["R"]} + print("\n sanity β€” ricostruzione vs sleeve ufficiale. XS01/VRP01/GTAA01 devono coincidere") + print(" BARRA PER BARRA; TP01/SKH01 no per costruzione (il recon e' un mark-to-market ORARIO,") + print(" lo sleeve marca a fine-trade) -> per loro il controllo giusto e' il log-totale.") + print(f" {'sleeve':>8} {'max|delta| barra':>18} {'log-tot recon':>15} " + f"{'log-tot sleeve':>16} {'scarto':>9}") + for nm in SLEEVES: + com = off[nm].index.intersection(rec[nm].index) + a = off[nm].reindex(com).values + b = rec[nm].reindex(com).values + dmax = float(np.abs(a - b).max()) + la, lb = float(np.log1p(a).sum()), float(np.log1p(b).sum()) + flag = "OK" if dmax < 1e-9 else ("lente MTM" if nm in ("TP01", "SKH01") else "DIVERGE") + print(f" {nm:>8} {dmax:>18.2e} {lb:>15.4f} {la:>16.4f} {lb-la:>+9.4f} {flag}") + + hr("A. PROFILO DEGLI SLEEVE SOTTO BARRIERA β€” cio' che conta non e' lo Sharpe, e si vede qui") + print(f" {'sleeve':>8} {'Sharpe':>7} {'vol a':>7} {'drift a':>8} {'skew':>7} " + f"{'giorno peggiore':>16} {'gap p90':>9} {'gap peggiore':>13} {'g/vol':>7}") + prof = {} + for nm in SLEEVES: + R, G = book_RG({nm: 1.0}) + Rd = deluck(R) + vol = Rd.std() * np.sqrt(365.0) + dr = Rd.mean() * 365.0 + sk = float(pd.Series(Rd).skew()) + prof[nm] = dict(sharpe=dr / vol if vol > 0 else 0.0, vol=vol, drift=dr, skew=sk, + worst=Rd.min(), gap90=np.percentile(-G, 90), gapw=G.min()) + print(f" {nm:>8} {prof[nm]['sharpe']:>7.2f} {vol:>6.1%} {dr:>8.1%} {sk:>7.2f} " + f"{Rd.min():>15.2%} {-np.percentile(-G, 90)*100:>8.2f}pp {G.min()*100:>12.2f}pp " + f"{(-G.min()/vol) if vol > 0 else 0:>7.2f}") + print("\n lettura: `gap peggiore` e' l'escursione intraday che la lente close-only NON vede;") + print(" `g/vol` la misura in unita' di vol annua. VRP01 ha gap 0 **per cecita' del dato**,") + print(" non per assenza di rischio (mark infra-settimana misurato il 30/07: mediana -6.9%,") + print(" minimo -81.7% del capitale su un trade finito in UTILE).") + + tick("A. profilo sleeve") + hr("B. IL CONTO ANALITICO β€” perche' con leva LIBERA la barriera premia lo SHARPE, non la vol") + tgt, dd, _dl = FIRMS["HYRO"]["ev"] + print(f" P(toccare +{tgt:.0%} prima di -{dd:.0%}), gambler's ruin senza limite di tempo, " + f"theta = 2*mu/(k*sigma^2):") + print(f" {'sleeve':>8} {'Sharpe':>7} | " + " ".join(f"{f'k={k:g}':>8}" for k in (0.25, 0.5, 1.0, 2.0, 4.0))) + for nm in SLEEVES: + p = prof[nm] + cells = " ".join(f"{p_hit_target_first(p['drift'], p['vol'], k, tgt, dd):>8.1%}" + for k in (0.25, 0.5, 1.0, 2.0, 4.0)) + print(f" {nm:>8} {p['sharpe']:>7.2f} | {cells}") + print(f" {'limite k->inf':>16} | " + " ".join([f"{dd/(tgt+dd):>8.1%}"] * 5) + + " <- D/(T+D): la leva alta CANCELLA l'edge") + print("\n Alla leva minima che porta a +T dentro l'orizzonte (k ~ T/mu) resta theta ~ 2*S^2/T:") + print(" la VOL sparisce e conta lo Sharpe AL QUADRATO. Percio' la previsione registrata prima") + print(" di misurare e': l'argmax di J sui PESI ~ l'argmax di Sharpe, e il contenuto vero e'") + print(" nella LEVA (che lo Sharpe non determina) e nella deviazione dal gaussiano.") + + # ---------------------------------------------------------------- C. griglia + n_hist = len(idx) + EV_H, FU_H = 365, 365 + ev_idx = boot_idx(n_hist, EV_H, N_PATHS, seed=8220) + fu_idx = boot_idx(n_hist, FU_H, N_PATHS, seed=8221) + LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50) + STEP = 8 + grid = simplex_grid(CRYPTO_PROP, STEP) + + hr(f"C. GRIGLIA β€” {len(grid)} vettori di peso (passo 1/{STEP}) x {len(LEVS)} leve " + f"= {len(grid)*len(LEVS)} celle, firm HYRO (eval +10%/-6%/-4%, funded -6%/-4%)") + print(f" orizzonte eval {EV_H}g, funded {FU_H}g. Lente ACCOPPIATA. " + f"J = P(pass) x P(vivo 12m).") + tick("B. conto analitico") + t = time.time() + G3 = grid_scan(grid, LEVS, "HYRO", ev_idx, fu_idx) + print(f" ({time.time()-t:.0f}s)") + + best = G3.loc[G3["J"].idxmax()] + # argmax di SHARPE: lo Sharpe non dipende dalla leva -> si sceglie il peso, poi la leva + # migliore PER QUEL PESO secondo J (altrimenti non e' un confronto onesto). + w_sh = G3.loc[G3["sharpe"].idxmax(), "w"] + sub_sh = G3[G3["w"] == w_sh] + best_sh = sub_sh.loc[sub_sh["J"].idxmax()] + live = G3[(G3["w"] == "75/25/0")] + best_live = live.loc[live["J"].idxmax()] + + print(f"\n {'':>22} {'pesi TP/SKH/XS':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} " + f"{'P(vivo12m)':>11} {'Sharpe':>7} {'vol':>6} {'E[pay/a]':>10} {'g.al pass':>10}") + for lab, row in (("argmax J", best), ("argmax Sharpe", best_sh), + ("libro LIVE 75/25", best_live)): + print(f" {lab:>22} {row['w']:>16} {row['lev']:>6.3f} {row['J']:>7.3f} " + f"{row['p_pass']:>8.1%} {row['p_alive']:>11.1%} {row['sharpe']:>7.2f} " + f"{row['vol']:>6.1%} {row['e_payout']:>9,.0f}$ {row['t_pass']:>10.0f}") + + dA, seA = paired_delta(w_argmax_dict := dict(zip(CRYPTO_PROP, [int(x)/100 for x in best["w"].split("/")])), + float(best["lev"]), + dict(zip(CRYPTO_PROP, [int(x)/100 for x in best_sh["w"].split("/")])), + float(best_sh["lev"]), "HYRO", ev_idx, fu_idx) + dB, seB = paired_delta(w_argmax_dict, float(best["lev"]), + {"TP01": 0.75, "SKH01": 0.25}, float(best_live["lev"]), + "HYRO", ev_idx, fu_idx) + print(f"\n DIFFERENZE APPAIATE (stessi indici di bootstrap; un livello di J ha SE ~0.01, " + f"una differenza molto meno):") + print(f" argmax J - argmax SHARPE : {dA:+.4f} +- {seA:.4f} " + f"-> {'INDISTINGUIBILI' if abs(dA) < 2*seA else 'differenza reale'}") + print(f" argmax J - libro LIVE : {dB:+.4f} +- {seB:.4f} " + f"-> {'INDISTINGUIBILI' if abs(dB) < 2*seB else 'differenza reale'}") + quota = abs(dA) / abs(dB) if dB != 0 else float("nan") + print(f" La prima riga E' il risultato del filone: riallocare per la BARRIERA invece che") + print(f" per lo SHARPE vale {dA:+.3f} di J, cioe' il {quota:.0%} del {dB:+.3f} che vale") + print(f" passare dal libro LIVE a un libro diversificato QUALSIASI. Il grosso del guadagno") + print(f" NON e' nella funzione obiettivo: e' nell'aggiungere lo sleeve che a $600 non entra.") + + print("\n Le prime 8 celle per J:") + print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8} " + f"{'Sharpe':>7} {'E[pay/a]':>10}") + for i, (_, r) in enumerate(G3.sort_values("J", ascending=False).head(8).iterrows(), 1): + print(f" {i:>3} {r['w']:>16} {r['lev']:>6.3f} {r['J']:>7.3f} {r['p_pass']:>8.1%} " + f"{r['p_alive']:>8.1%} {r['sharpe']:>7.2f} {r['e_payout']:>9,.0f}$") + + print("\n E per E[payout] (obiettivo DIVERSO: un flusso, non una sopravvivenza):") + print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'E[pay/a]':>10} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8}") + for i, (_, r) in enumerate(G3.sort_values("e_payout", ascending=False).head(4).iterrows(), 1): + print(f" {i:>3} {r['w']:>16} {r['lev']:>6.3f} {r['e_payout']:>9,.0f}$ {r['J']:>7.3f} " + f"{r['p_pass']:>8.1%} {r['p_alive']:>8.1%}") + + hr("C-bis. LA LEVA β€” la variabile che lo Sharpe non determina e che decide tutto") + print(f" pesi = argmax J ({best['w']}):") + print(f" {'leva':>7} {'J':>7} {'P(pass) 365g':>13} {'P(pass) 1095g':>14} {'P(vivo 12m)':>12} " + f"{'E[pay/a]':>10}") + ev_long = boot_idx(n_hist, 1095, N_PATHS, seed=8222) + w_best = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in best["w"].split("/")])) + Rb, Gb = book_RG(w_best) + Rb = deluck(Rb) + for lev in LEVS: + o = objective(w_best, lev, "HYRO", ev_idx, fu_idx) + pl_long = eval_sim(Rb, Gb, ev_long, lev, "HYRO")["p_pass"] + print(f" {lev:>7.3f} {o['J']:>7.3f} {o['p_pass']:>12.1%} {pl_long:>13.1%} " + f"{o['p_alive']:>11.1%} {o['e_payout']:>9,.0f}$") + print("\n A 365 giorni P(pass) e' a CAMPANA nella leva (a leva bassa il drift non arriva a +10%") + print(" in tempo); senza limite di tempo β€” che e' la regola vera di HyroTrader β€” e' MONOTONA") + print(" DECRESCENTE, come dice il conto analitico. Le due colonne dicono cose diverse e vanno") + print(" lette insieme: la leva alta non aumenta la probabilita' di farcela, aumenta la FRETTA.") + + # ---------------------------------------------------------------- D. i null + tick("C. griglia + leva") + hr("D. NULL 1 β€” l'argmax e' speciale, o e' solo il MASSIMO DI 45 ESTRAZIONI?") + rw = random_weights(200, CRYPTO_PROP, seed=8223) + lev_star = float(best["lev"]) + Jr = np.array([objective(w, lev_star, "HYRO", ev_idx, fu_idx)["J"] for w in rw]) + pctl = float((Jr < best["J"]).mean() * 100.0) + bestofk = 100.0 * len(grid) / (len(grid) + 1) + print(f" J dell'argmax = {best['J']:.3f} alla leva {lev_star:g}") + print(f" null: {len(rw)} vettori di peso casuali uniformi sul simplesso (stesso campionatore di") + print(f" `portfolio.weights_tilt_null`), valutati alla STESSA leva") + print(f" mediana {np.median(Jr):.3f} | p90 {np.percentile(Jr, 90):.3f} | " + f"max {Jr.max():.3f} | frazione sopra il LIVE 75/25 = {(Jr > best_live['J']).mean():.0%}") + print(f" percentile dell'argmax fra i casuali = {pctl:.1f}Β° contro la firma best-of-" + f"{len(grid)} = {bestofk:.1f}Β°") + print(f" ⚠ RISOLUZIONE: con {len(rw)} estrazioni un percentile vale a passi di " + f"{100/len(rw):.1f} punti -> {pctl:.1f}Β° e {bestofk:.1f}Β° NON sono distinguibili.") + print(f" E il fatto che decide: il MASSIMO dei {len(rw)} pesi casuali e' {Jr.max():.3f} contro" + f" {best['J']:.3f} dell'argmax") + print(f" della griglia -> un vettore estratto a caso lo {'EGUAGLIA' if Jr.max() >= best['J'] - 1e-9 else 'non lo raggiunge'}." + f" NESSUNA evidenza che quel vettore di pesi sia speciale:") + print(" cio' che conta non e' QUALE punto del simplesso, ma STARE nella regione diversificata.") + print(f" (la meta' bassa del null e' pero' informativa: il LIVE 75/25 e' battuto dal " + f"{(Jr > best_live['J']).mean():.0%} dei pesi CASUALI)") + + tick("D. null casuale") + hr("D-bis. NULL 2 β€” selezione IN-SAMPLE-ONLY (la scelta guardando J e' selezione come ogni altra)") + half = n_hist // 2 + R_all = {} + for w in grid: + R_, G_ = book_RG(w) + R_all[wkey(w)] = (deluck(R_), G_) + ev_h = boot_idx(half, EV_H, N_PATHS, seed=8224) + fu_h = boot_idx(half, FU_H, N_PATHS, seed=8225) + + def _J(key, lev, sl): + R_, G_ = R_all[key] + e = eval_sim(R_[sl], G_[sl], ev_h, lev, "HYRO")["p_pass"] + f = funded_sim(R_[sl], G_[sl], fu_h, lev, "HYRO", 100_000.0)["p_alive"] + return e * f + + IS, OOS = slice(0, half), slice(half, n_hist) + rows = [] + for w in grid: + k = wkey(w) + for lev in LEVS: + rows.append((k, lev, _J(k, lev, IS), _J(k, lev, OOS))) + S = pd.DataFrame(rows, columns=["w", "lev", "J_is", "J_oos"]) + pick = S.loc[S["J_is"].idxmax()] + oracle = S.loc[S["J_oos"].idxmax()] + live_oos = S[(S["w"] == "75/25/0")].sort_values("J_oos", ascending=False).iloc[0] + print(f" prima meta' {idx[0].date()}->{idx[half-1].date()} | seconda {idx[half].date()}->{idx[-1].date()}") + print(f" {'':>28} {'pesi':>16} {'leva':>6} {'J in-sample':>12} {'J out-of-sample':>16}") + print(f" {'scelto SUL SOLO in-sample':>28} {pick['w']:>16} {pick['lev']:>6.3f} " + f"{pick['J_is']:>12.3f} {S[(S['w']==pick['w'])&(S['lev']==pick['lev'])]['J_oos'].iloc[0]:>16.3f}") + print(f" {'oracolo (max sull OOS)':>28} {oracle['w']:>16} {oracle['lev']:>6.3f} " + f"{oracle['J_is']:>12.3f} {oracle['J_oos']:>16.3f}") + print(f" {'libro LIVE 75/25 (baseline)':>28} {live_oos['w']:>16} {live_oos['lev']:>6.3f} " + f"{live_oos['J_is']:>12.3f} {live_oos['J_oos']:>16.3f}") + rho = float(S["J_is"].corr(S["J_oos"], method="spearman")) + print(f" Spearman(J in-sample, J out-of-sample) su {len(S)} celle = {rho:+.2f}") + print(" -> se rho e' alto la mappa e' stabile e la scelta si trasferisce; se e' ~0 l'argmax") + print(" e' rumore e la decisione va presa sul MECCANISMO (la leva), non sulla cella.") + + tick("D-bis. selezione in-sample") + hr("D-ter. NULL DEL DE-LEVERING β€” la leva e' GIA' dentro l'ottimizzazione, ma va detto") + print(" Il null classico ('esiste k<1 che da' lo stesso DD con Sharpe migliore?') qui e'") + print(" strutturale: ogni configurazione e' valutata a OTTO leve, quindi nessuna puo' vincere") + print(" solo perche' e' meno esposta. La domanda che resta e': la MIGLIOR leva del libro LIVE") + print(" raggiunge il J dell'argmax?") + print(f" argmax J : {best['w']:>10} @ {best['lev']:g}x -> J = {best['J']:.3f}") + print(f" LIVE 75/25 al suo : {best_live['w']:>10} @ {best_live['lev']:g}x -> J = {best_live['J']:.3f}") + dJ = best["J"] - best_live["J"] + se = np.sqrt(0.25 / N_PATHS) * 2 + print(f" delta = {dJ:+.3f} (errore MC su livelli ~+-{se:.3f}; le due celle condividono") + print(f" gli indici di bootstrap -> il delta e' APPAIATO e il suo errore e' piu' piccolo)") + + # ---------------------------------------------------------------- E. banda d'ancora + hr("E. BANDA D'ANCORA β€” non ri-misurata sotto questa lente (costo); si usa il FATTORE misurato") + print(" Ri-fare i 24x23x10 offset col recon MTM orario e' fuori budget. Si applica il fattore") + print(" di drift MISURATO il 26/07 (x0.89 sul libro live) e si controlla cio' che conta: se") + print(" l'ORDINE fra le configurazioni sopravvive alla banda 0.60 (stima 25/07, ri-misurata") + print(" troppo severa) - 1.00 (nessuna correzione). L'argmax e' ri-cercato sulle 12 celle-peso") + print(" migliori (non su tutte le 45: costo), il che e' conservativo per la STABILITA'.") + cands = [best["w"], best_sh["w"], best_live["w"]] + top_keys = list(G3.sort_values("J", ascending=False).drop_duplicates("w")["w"].head(12)) + top_w = [dict(zip(CRYPTO_PROP, [int(x) / 100 for x in k.split("/")])) for k in top_keys] + print(f"\n {'fattore':>9} " + " ".join(f"{('J ' + c):>14}" for c in dict.fromkeys(cands)) + + f" {'argmax pesi':>14} {'argmax leva':>11}") + for fct in (1.00, 0.89, 0.60): + cells = [] + for c in dict.fromkeys(cands): + wc = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in c.split("/")])) + sub = [objective(wc, lv, "HYRO", ev_idx, fu_idx, factor=fct)["J"] for lv in LEVS] + cells.append(max(sub)) + g = grid_scan(top_w, LEVS, "HYRO", ev_idx, fu_idx, factor=fct) + b = g.loc[g["J"].idxmax()] + print(f" {fct:>9.2f} " + " ".join(f"{v:>14.3f}" for v in cells) + + f" {b['w']:>14} {b['lev']:>11.3f}") + + hr("E-bis. LO STRESS CHE DECIDE β€” XS01 e' stato SCOPERTO in questa finestra") + print(" L'argmax mette il 50% su XS01, il cui unico campione (2024+) e' anche quello su cui e'") + print(" stato scoperto e affinato (blend di lookback + gate di dispersione). CLAUDE.md lo") + print(" dichiara: 'la TAGLIA del guadagno e' ottimista'. Qui si taglia il DRIFT di XS01 (non la") + print(" vol, non la coda) e si guarda se la conclusione operativa sopravvive.") + print(f"\n {'drift XS01':>12} {'config':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} " + f"{'P(vivo)':>8} {'E[pay/a]':>10}") + for mult in (1.00, 0.75, 0.50, 0.25, 0.00): + dmt = (("XS01", mult),) + rows = [] + for w in top_w: + for lv in LEVS: + rows.append((w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt))) + w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"]) + lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx, + drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"]) + dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", ev_idx, + fu_idx, drift_mult=dmt) + print(f" {mult:>12.2f} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {o_['p_pass']:>8.1%} " + f"{o_['p_alive']:>8.1%} {o_['e_payout']:>9,.0f}$ LIVE {oL['J']:.3f} " + f"delta {dd_:+.3f} +-{sd_:.3f}") + print("\n La colonna che decide e' l'ULTIMA. Se il vantaggio svanisce col drift di XS01, il") + print(" risultato e' un'ipotesi sul RENDIMENTO ATTESO di XS01 misurato sulla sua finestra di") + print(" scoperta; se resta a drift 0, il vantaggio e' pura DECORRELAZIONE e non dipende da") + print(" quell'ipotesi. Il numero, non la narrazione, dice quale delle due.") + + hr("E-quater. LA DISTORSIONE CHE MI RIGUARDA β€” l'ottimo si appoggia a SKH01, e SKH01 e' lo") + print(" sleeve con la FORTUNA D'ANCORA piu' grande da restituire. Il LOO del 26/07: l'ancora") + print(" canonica gli regala 2/3 del contributo FULL (+0.43 -> +0.14) e il 70% dell'hold-out;") + print(" l'audit 02/07 lo trova al 93-98Β° pctl di 23 offset, 'spike, non plateau'. Questa lente") + print(" gira sull'ancora CANONICA e la banda non e' ri-misurabile qui -> il mio ottimo e'") + print(" distorto A FAVORE di SKH01 e va detto PRIMA di leggerlo. Taglio del drift di SKH01:") + print(" Le ultime due righe sono il taglio CONGIUNTO su SKH01 E XS01 (i due sleeve che il") + print(" progetto misura come piu' fortunati / piu' recenti): e' il caso peggiore onesto.") + print(f"\n {'taglio drift':>21} {'config':>16} {'leva':>6} {'J':>7} {'J del LIVE':>11} " + f"{'delta appaiato':>17}") + for dmt in ((("SKH01", 1.00),), (("SKH01", 0.66),), (("SKH01", 0.50),), (("SKH01", 0.33),), + (("SKH01", 0.50), ("XS01", 0.50)), (("SKH01", 0.33), ("XS01", 0.00))): + mult = dict(dmt) + rows = [(w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)) + for w in top_w for lv in LEVS] + w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"]) + lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx, + drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"]) + dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", + ev_idx, fu_idx, drift_mult=dmt) + lab = " ".join(f"{k.replace('01','')} x{v:.2f}" for k, v in mult.items()) + assert abs(dd_ - (o_["J"] - oL["J"])) < 0.05, ( # guardia contro l'errore di sopra + f"delta appaiato {dd_:+.3f} incoerente con J {o_['J']:.3f} - {oL['J']:.3f}") + print(f" {lab:>21} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {oL['J']:>11.3f} " + f"{dd_:>+11.3f} +-{sd_:.3f}") + print(" NB: il taglio si applica anche al LIVE 75/25 (che ha SKH01 al 25%), quindi il delta e'") + print(" un confronto onesto: entrambe le colonne perdono, la domanda e' quale perde di piu'.") + + hr("E-ter. IL BUCO CHE LA FINESTRA NON PUO' CHIUDERE β€” 2024-2026 non contiene un CRASH") + print(" L'ottimizzazione toglie peso a TP01 (Sharpe 0.79 su questa finestra). Ma TP01 e' lo") + print(" sleeve DIFENSIVO: il LOO del 26/07 misura il suo contributo hold-out NEGATIVO nel 99.1%") + print(" delle ancore e conclude 'uno sleeve difensivo si giudica sul SINISTRO, non sul premio'.") + print(" Su un conto funded la barriera morde PROPRIO nel sinistro. Qui la coppia crypto (che ha") + print(" storia dal 2019, quindi il 2022 dentro) e' ri-ottimizzata sulle due finestre:") + cry_full = pc.crypto_daily_tuples(1.0, 0.0).index + W2 = [{"TP01": i / 8, "SKH01": 1 - i / 8} for i in range(9)] + print(f"\n {'finestra':>26} {'giorni':>7} {'argmax TP/SKH':>15} {'leva':>6} {'J':>7} " + f"{'J del 75/25':>12} {'Sharpe argmax':>14}") + for st, lab in (("2019-03-14", "FULL 2019+ (col 2022)"), (START_WIN, "2024+ (senza crash)")): + pairs = {} + for w in W2: + c = pc.crypto_daily_tuples(w["TP01"], w["SKH01"]) + c = c[c.index >= pd.Timestamp(st, tz="UTC")] + pairs[wkey(w)] = (deluck(c["R"].values), np.minimum(c["m"].values, c["R"].values) - c["R"].values) + nh = len(next(iter(pairs.values()))[0]) + ev2 = boot_idx(nh, EV_H, N_PATHS, seed=8230) + fu2 = boot_idx(nh, FU_H, N_PATHS, seed=8231) + res = [] + for k, (R_, G_) in pairs.items(): + for lv in LEVS: + jj = (eval_sim(R_, G_, ev2, lv, "HYRO")["p_pass"] + * funded_sim(R_, G_, fu2, lv, "HYRO", 100_000.0)["p_alive"]) + res.append((k, lv, jj, R_.mean() * 365 / (R_.std() * np.sqrt(365)))) + bk, blv, bj, bsh = max(res, key=lambda x: x[2]) + j75 = max(x[2] for x in res if x[0] == "75/25/0") + print(f" {lab:>26} {nh:>7d} {bk.rsplit('/', 1)[0]:>15} {blv:>6.3f} {bj:>7.3f} " + f"{j75:>12.3f} {bsh:>14.2f}") + print("\n Se anche sulla finestra che CONTIENE il 2022 l'ottimo scarta TP01, la sua sotto-") + print(" pesatura non e' un artefatto di finestra. Se invece li' TP01 pesa, allora la") + print(" raccomandazione del 2024+ e' una scommessa sull'assenza di crash β€” e su una barriera") + print(" di rovina quella scommessa la paghi una volta sola. **Questa finestra NON puo' dirlo") + print(" per XS01**, che nasce nel 2024: il buco resta aperto per lui, per costruzione.") + + # ---------------------------------------------------------------- F. VRP01 / GTAA01 + tick("E. banda ancora") + hr("F. E GLI SLEEVE 'INUTILI A $600'? β€” l'asimmetria del 25/07 Β§4 vale per XS01, non per tutti") + print(" Su un conto funded da $100k gli sleeve fuori portata a $600 diventano ESEGUIBILI per") + print(" TAGLIA. Ma la taglia non e' l'unico vincolo: una firm CRYPTO non lista opzioni Deribit") + print(" (VRP01) ne' ETF azionari USA (GTAA01, che al retail UE e' pure bloccato dal PRIIPs).") + print(" Qui si misura comunque il CONTROFATTUALE: se fossero disponibili, aiuterebbero?") + print(f"\n {'aggiunta al ' + best['w']:>34} {'leva':>6} {'J':>7} {'P(pass)':>8} " + f"{'P(vivo)':>8} {'Sharpe':>7} {'E[pay/a]':>10}") + for extra, wx in [(None, 0.0), ("VRP01", 0.10), ("VRP01", 0.20), + ("GTAA01", 0.10), ("GTAA01", 0.20)]: + w2 = {k: v * (1.0 - wx) for k, v in w_best.items()} + if extra: + w2[extra] = wx + rows = [(lv, objective(w2, lv, "HYRO", ev_idx, fu_idx)) for lv in LEVS] + lv, o = max(rows, key=lambda x: x[1]["J"]) + lab = "nessuna (base)" if extra is None else f"{extra} @ {wx:.0%}" + print(f" {lab:>34} {lv:>6.3f} {o['J']:>7.3f} {o['p_pass']:>8.1%} {o['p_alive']:>8.1%} " + f"{o['sharpe']:>7.2f} {o['e_payout']:>9,.0f}$") + print("\n Le righe VRP01 e GTAA01 escono quasi IDENTICHE: non e' un errore, e' che sotto questa") + print(" funzione obiettivo i due sleeve sono lo stesso oggetto (vol ~6.2%, drift ~7.7%, corr ~0") + print(" col crypto). La barriera vede vol, drift e correlazione; la differenza fra i due β€” skew") + print(" -4.6 contro -0.4 β€” a peso 10-20% e' diluita sotto la soglia di daily-loss.") + print("\n ATTENZIONE: la riga VRP01 e' calcolata con **gap = 0** (il mark infra-settimana non") + print(" esiste nel dataset dello sleeve): e' un TETTO, non una stima. Con il mark misurato il") + print(" 30/07 (peggiore infra-settimana mediano -6.9%, minimo -81.7%) una barriera STATICA al") + print(" 6% verrebbe sfondata dal mark, non dal risultato.") + + # ---------------------------------------------------------------- G. FTMO + tick("F. VRP/GTAA") + hr("G. LA BARRIERA CAMBIA LA RISPOSTA? β€” HYRO (-6% statico) vs FTMO (-10% statico)") + print(f" {'firm':>6} {'argmax pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo12m)':>11} " + f"{'E[pay/a]':>10} | {'LIVE 75/25: leva':>17} {'J':>7}") + for fm in ("HYRO", "FTMO"): + g = G3 if fm == "HYRO" else grid_scan(grid, LEVS, fm, ev_idx, fu_idx) + b = g.loc[g["J"].idxmax()] + lv2 = g[g["w"] == "75/25/0"] + b2 = lv2.loc[lv2["J"].idxmax()] + print(f" {fm:>6} {b['w']:>16} {b['lev']:>6.3f} {b['J']:>7.3f} {b['p_pass']:>8.1%} " + f"{b['p_alive']:>11.1%} {b['e_payout']:>9,.0f}$ | {b2['lev']:>17.3f} {b2['J']:>7.3f}") + print("\n La barriera piu' larga di FTMO sposta l'ottimo di leva verso l'ALTO: il vincolo e' la") + print(" barriera, non il libro. E' la stessa lettura del 25/07 Β§4 ('sul canale funded il") + print(" vincolo binding e' la regola di DD, non il capitale'), qui misurata sull'ottimo.") + + # ---------------------------------------------------------------- H. gate ufficiale sui pesi + tick("G. FTMO") + hr("H. GATE UFFICIALE SUI PESI (`portfolio.weights_tilt_null`) β€” sullo SHARPE, come previsto") + cols = {nm: off[nm] for nm in CRYPTO_PROP} + try: + res = weights_tilt_null(cols, {"TP01": 0.75, "SKH01": 0.25, "XS01": 0.0001}, + {nm: max(w_best.get(nm, 0.0), 1e-4) for nm in CRYPTO_PROP}, + floor=0.0, n=200, k_seen=len(grid) * len(LEVS)) + print(f" proposta {best['w']} vs LIVE 75/25 : delta_insample {res['delta_insample']:+.4f} | " + f"delta_hold {res['delta_hold']:+.4f} | pctl_hold {res['pctl_hold']:.1f}Β° " + f"(best-of-k {res['bestofk_pctl']:.1f}Β°) | gate_pass = {res['gate_pass']}") + except Exception as exc: # pragma: no cover + print(f" non girato: {exc}") + print(" NB: questo gate giudica lo SHARPE del book di ricerca, non J. Un cambio dei pesi del") + print(" LIBRO LIVE resta soggetto a lui; qui si progetta un conto SEPARATO (funded), che non") + print(" e' il libro live e non passa da questo gate.") + + # ---------------------------------------------------------------- I. tavola operativa + tick("H. gate pesi") + hr("I. TAVOLA OPERATIVA β€” le 3 configurazioni migliori, e quanti conti servono per 50 EUR/g") + top = G3.sort_values("J", ascending=False).drop_duplicates("w").head(3) + rows_ops = [] + fu3 = boot_idx(n_hist, 1095, N_PATHS, seed=8226) + print(f" {'pesi TP/SKH/XS':>16} {'leva':>6} {'P(pass)':>8} {'P(vivo12m)':>11} " + f"{'E[payout 3a]':>13} {'EUR/g per conto':>16} {'conti per 50/g':>15}") + for _, r in top.iterrows(): + w2 = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in r["w"].split("/")])) + R_, G_ = book_RG(w2) + R_ = deluck(R_) + f3 = funded_sim(R_, G_, fu3, float(r["lev"]), "HYRO", 100_000.0) + eur_day = f3["e_payout"] / EURUSD / 3.0 / 365.0 + n_acct = (TARGET_EUR_DAY / eur_day) if eur_day > 0 else np.inf + rows_ops.append((r["w"], float(r["lev"]), f3["e_payout"], eur_day, n_acct)) + print(f" {r['w']:>16} {r['lev']:>6.3f} {r['p_pass']:>8.1%} {r['p_alive']:>11.1%} " + f"{f3['e_payout']:>12,.0f}$ {eur_day:>15.2f} {n_acct:>15.1f}") + print("\n E[payout 3a] e' su UN conto funded da $100k, gia' netto split 80% e fisco 33%, e") + print(" MEDIA su tutti i percorsi (i morti contano zero). 'conti per 50/g' e' quindi un numero") + print(" ATTESO, non una garanzia: la distribuzione e' bimodale (vivo / bustato).") + cap_tot = sum(f["cap"] for f in pl.FIRMS) + print(f" ⚠ TETTO STRUTTURALE: cap per trader ${FIRMS['HYRO']['cap']:,.0f} (HYRO) + " + f"${FIRMS['FTMO']['cap']:,.0f} (FTMO) = ${cap_tot:,.0f} di nozionale su due firm") + print(f" -> al massimo {cap_tot/100_000:.0f} conti da $100k. Se ne servono di piu', servono " + f"ALTRE FIRM (e ognuna aggiunge rischio di controparte).") + + tick("I. tavola operativa") + hr("I-bis. LA SCALA DI CONTI β€” macchineria del 25/07 (`r0725_prop_coupled.simulate`), " + "de-luck portato a x0.89") + print(f" 36 mesi, EUR {pl.START_EUR:.0f} di cassa iniziale, max {pl.MAX_CONCURRENT} conti, " + f"morte-firm {pl.FIRM_DEATH_PER_YEAR:.0%}/anno, lente ACCOPPIATA") + pc.DELUCK = DELUCK # il 25/07 girava a 0.6 (poi ri-misurato troppo severo) + cfg_best = tuple(w_best.get(nm, 0.0) for nm in CRYPTO_PROP) + cfg_live = (0.75, 0.25, 0.0) + cfg_B = (0.55, 0.20, 0.25) + pols = { + f"argmax-J {best['w']} su tutti i conti": ([cfg_best], [0] * pl.MAX_CONCURRENT), + "LIVE 75/25 su tutti i conti (baseline)": ([cfg_live], [0] * pl.MAX_CONCURRENT), + "config B 55/20/25 (scelta a mano 25/07)": ([cfg_B], [0] * pl.MAX_CONCURRENT), + } + print(f"\n {'politica':>44} {'leva':>6} {'EUR/g med':>10} {'p90':>8} {'P(>=10/g)':>10} " + f"{'P(>=50/g)':>10} {'P(zero)':>8}") + for pname, (cfgs, slots) in pols.items(): + for lev in sorted({round(float(best["lev"]), 3), 0.75}): + r = pc.simulate(cfgs, slots, n_paths=1500, lev=lev, seed=20260822, + lens="coupled", start=START_WIN) + print(f" {pname:>44} {lev:>6.3f} {r['med']:>10.2f} {r['p90']:>8.2f} " + f"{r['p_ge10']:>10.1%} {r['p_target']:>10.1%} {r['p_zero']:>8.1%}") + + print("\n ⚠ QUESTO NUMERO REVISIONA UN HEADLINE DI CLAUDE.md ('EUR600 -> 50 EUR/g ~ 6% in 3") + print(" anni, P(perdere i 600) 52-65%', 25/07). Due cose sono cambiate insieme: il fattore di") + print(" de-luck (0.60 -> 0.89, ri-misurato il 26/07) e l'ALLOCAZIONE. Vanno separate, o il") + print(" merito finisce sulla leva sbagliata. Decomposizione 2x2 (stessa macchina, stesso seme):") + print(f"\n {'de-luck':>9} {'allocazione':>26} {'EUR/g med':>10} {'P(>=50/g)':>10} {'P(zero)':>8}") + for fct in (0.60, 0.89): + pc.DELUCK = fct + for lab, cfg in (("LIVE 75/25 (25/07)", cfg_live), (f"argmax-J {best['w']}", cfg_best)): + r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=0.75, + seed=20260822, lens="coupled", start=START_WIN) + print(f" {fct:>9.2f} {lab:>26} {r['med']:>10.2f} {r['p_target']:>10.1%} " + f"{r['p_zero']:>8.1%}") + print("\n ⚠ Il headline ~6% del 25/07 NON e' la riga 'LIVE su tutti i conti' (quella li' era") + print(" CONC-2SL, dichiarata 'la PEGGIORE'): era la politica MISTA, sleeve diversi su conti") + print(" diversi. Replica ESATTA di quella politica, stessa macchina, de-luck 0.60, leva 0.75:") + pc.DELUCK = 0.60 + cfgs25 = [(0.75, 0.25, 0.0), (0.55, 0.20, 0.25), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)] + r25 = pc.simulate(cfgs25, [1, 4, 1, 2, 1, 4], n_paths=1500, lev=0.75, seed=20260822, + lens="coupled", start=START_WIN) + print(f" MISTO (25/07) @0.75x, de-luck 0.60 : EUR/g med {r25['med']:.2f} | " + f"P(>=50/g) {r25['p_target']:.1%} | P(zero) {r25['p_zero']:.1%}") + print(f" -> il 25/07 pubblico' P(>=50/g) 5.6-6.5% e P(zero) 52% a 0.75x. " + f"{'REPLICA OK' if 0.03 <= r25['p_target'] <= 0.10 else 'NON REPLICA β€” leggere con cautela'}") + pc.DELUCK = DELUCK + print("\n Lettura della 2x2: il de-luck e l'allocazione contribuiscono ENTRAMBI, e vanno citati") + print(" separati. Il fattore 0.60 era una stima a occhio poi RI-MISURATA troppo severa (26/07):") + print(" usarlo ancora sarebbe sbagliato, ma il salto da 1.7% a 41.7% e' il prodotto di due") + print(" correzioni, non di una scoperta.") + + hr("I-ter. LA STESSA SCALA, MA CON GLI STRESS DI E-bis/E-quater DENTRO") + print(" La tabella qui sopra gira al drift PIENO di XS01 e SKH01. E-bis mostra che il vantaggio") + print(" vive quasi tutto sul drift di XS01; E-quater che il taglio congiunto lo azzera. Il") + print(" numero operativo va quindi dato con gli stress dentro, non a parte.") + print(" (meccanica: le tuple stressate vengono INIETTATE nella cache di `pc.book_tuples`, cosi'") + print(" la simulazione della scala e' letteralmente la stessa del 25/07, con un input diverso.)") + print(f"\n {'stress':>24} {'allocazione':>16} {'leva':>6} {'EUR/g med':>10} " + f"{'P(>=10/g)':>10} {'P(>=50/g)':>10} {'P(zero)':>8}") + for dmt, lab in (((), "nessuno (pieno)"), + ((("SKH01", 0.50), ("XS01", 0.50)), "SKH x0.50 XS x0.50"), + ((("SKH01", 0.33), ("XS01", 0.00)), "SKH x0.33 XS x0.00")): + for cfg, alab in ((cfg_best, f"argmax-J {best['w']}"), (cfg_live, "LIVE 75/25")): + wdic = dict(zip(CRYPTO_PROP, cfg)) + R_, G_ = book_RG(wdic, drift_mult=dmt) + key = (float(cfg[0]), float(cfg[1]), float(cfg[2]), "shared") + saved = pc._TUP_CACHE.pop(key, None) + pc._TUP_CACHE[key] = (R_, G_, _panel_index(START_WIN)) + r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=0.75, + seed=20260822, lens="coupled", start=START_WIN) + pc._TUP_CACHE.pop(key, None) + if saved is not None: + pc._TUP_CACHE[key] = saved + print(f" {lab:>24} {alab:>16} {0.75:>6.2f} {r['med']:>10.2f} " + f"{r['p_ge10']:>10.1%} {r['p_target']:>10.1%} {r['p_zero']:>8.1%}") + print("\n Questa e' la riga da citare: non il 41.7% pieno, ma la BANDA fra la riga piena e la") + print(" riga del taglio congiunto. Lo stress non e' un'ipotesi pessimista arbitraria: e' la") + print(" fortuna d'ancora MISURATA su SKH01 (26/07) e la finestra di scoperta di XS01 (dichiarata") + print(" in CLAUDE.md come 'taglia del guadagno ottimista').") + + print("\n" + "=" * 104) + print(f" fatto in {time.time()-t0:.0f}s") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_skew.py b/scripts/research/r0822_skew.py new file mode 100644 index 0000000..206c258 --- /dev/null +++ b/scripts/research/r0822_skew.py @@ -0,0 +1,839 @@ +"""r0822_skew.py β€” la FORMA della superficie di volatilita' (skew), mai usata dal progetto. + +Il progetto ha usato molte volte il LIVELLO della vol implicita (DVOL/ATM): VRP01 lo vende sotto +gate IV-rank, DVOLSPREAD ne fa relative-value BTC/ETH, TP01xDVOL lo prova come denominatore del +vol-target. Non ha MAI usato la FORMA: il risk reversal 25-delta (RR), il butterfly (BF) e la loro +dinamica. La catena per-strike esiste da 2026-05-01 (`data/raw/cb_chain/`), quindi la domanda e' +finalmente misurabile. + +Tre domande, in ordine di valore dichiarato nel brief: + Q1 lo skew ANTICIPA lo spot? ("il posizionamento in opzioni anticipa il prezzo") + Q2 lo skew e' un GATE DI RISCHIO sopra il libro TP01? + Q3 lo skew spiega l'errore di PREZZATURA di VRP01 (f=0.73 misurato il 30/07)? + +Onesta' strutturale, dichiarata PRIMA dei numeri: + * La finestra utile e' 2026-06-09 -> oggi, cioe' ~75 giorni, NON i 113 giorni di righe presenti. + Prima del 09/06 cerbero-bite raccoglieva UNA SOLA scadenza per ora (verificato: `exps=1`), + quindi non esiste ne' struttura a termine ne' maturita' costante. Righe presenti != dato + presente (regola 8 del brief), e qui il conto va fatto in ORE DI SUPERFICIE RICOSTRUIBILE. + * HOLDOUT del progetto = 2025-01-01: l'INTERO campione e' posteriore. Non esiste hold-out, + non esiste in-sample -> `study_family_honest` e `select_cell_insample` NON sono eseguibili + (l'in-sample e' vuoto). Il verdetto massimo ottenibile e' LEAD con gate pre-registrato. + * Con ~75 osservazioni giornaliere SE(Sharpe annualizzato) ~ sqrt(365/75) ~ 2.2: qualunque + Sharpe giornaliero misurato qui e' indistinguibile da zero. Percio' il peso della sessione + sta dove la POTENZA c'e' davvero: la relazione oraria skew<->prezzo (N~1.800) e la + decomposizione di prezzo di VRP01 (deterministica, non statistica). + +Convenzioni di costruzione (sono SCELTE, non dettagli β€” vedi sezione 1): + * "quotato" = bid>0 AND ask>0 AND ask>=bid AND iv/delta non nulli. Si contano le quote, non le righe. + * smile per (asset, ora, scadenza): IV interpolata LINEARMENTE in |delta| a 0.25 e 0.50, + senza MAI estrapolare (se la catena non abbraccia il delta bersaglio, la cella si scarta). + * maturita' costante: RR/BF/ATM interpolati fra le due scadenze che abbracciano T (7g e 30g). + Il roll e' CONTINUO per costruzione: non c'e' una data di roll, quindi non c'e' il dente di + sega di tenore che un "scadenza piu' vicina a T" iniettera' nel segnale. + * ogni ora porta `ts_max` = il timestamp piu' recente fra le quote usate. I rendimenti in avanti + partono STRETTAMENTE dopo `ts_max`: senza questo l'ancora e' sbagliata di ~30 minuti e nasce + un falso "lo skew anticipa di 15 minuti" (misurato in sessione: vedi sezione 2). + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_skew.py +""" +from __future__ import annotations + +import glob +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import altlib as A # noqa: E402 +import cblib as CB # noqa: E402 + +CHAIN = ROOT / "data" / "raw" / "cb_chain" +ASSETS = ("BTC", "ETH") +COLS = ["ts", "asset", "instrument_name", "option_type", "strike", "exp", + "bid", "ask", "mid", "iv", "delta", "open_interest", "quote_status"] + +# tick MISURATO sui dati (non assunto): 0.0001 sotto 0.005 di premio, 0.0005 sopra. +def tick_of(prezzo: np.ndarray) -> np.ndarray: + return np.where(np.asarray(prezzo, float) < 0.005, 0.0001, 0.0005) + + +def _ms(ix: pd.DatetimeIndex) -> np.ndarray: + """Epoca esplicita in millisecondi. Lezione 01/07: `.view("int64")` (e `.astype`) su un + DatetimeIndex tz-aware non-ns sbaglia scala o solleva -> merge_asof broadcasta in silenzio.""" + return pd.DatetimeIndex(ix).tz_convert("UTC").tz_localize(None).astype("datetime64[ms]").astype("int64").to_numpy() + + +def rule(t: str) -> None: + print("\n" + "=" * 78) + print(t) + print("=" * 78) + + +# ============================================================================= +# 0. DATO +# ============================================================================= +def load_quoted() -> tuple[pd.DataFrame, dict]: + """Catena filtrata alle QUOTE vere. Ritorna anche la contabilita' righe-vs-quote.""" + parts, n_righe, n_status = [], 0, {} + for f in sorted(glob.glob(str(CHAIN / "*.parquet"))): + d = pd.read_parquet(f, columns=COLS) + n_righe += len(d) + for k, v in d["quote_status"].value_counts(dropna=False).items(): + n_status[str(k)] = n_status.get(str(k), 0) + int(v) + d["ts"] = pd.to_datetime(d["ts"], utc=True).dt.tz_convert("UTC") + d["exp"] = pd.to_datetime(d["exp"], utc=True).dt.tz_convert("UTC") + d["strike"] = d["strike"].astype(float) + parts.append(d) + d = pd.concat(parts, ignore_index=True) + d = d.drop_duplicates(subset=["ts", "instrument_name"], keep="last") + d["ts"] = pd.DatetimeIndex(d["ts"]).as_unit("ns") + d["exp"] = pd.DatetimeIndex(d["exp"]).as_unit("ns") + d["dte"] = (d["exp"] - d["ts"]).dt.total_seconds() / 86400.0 + + ok = (d["bid"] > 0) & (d["ask"] > 0) & (d["ask"] >= d["bid"]) & d["iv"].notna() & d["delta"].notna() + cont = dict(righe=n_righe, dedup=int(len(d)), quotate=int(ok.sum()), + frac_quotate=round(float(ok.mean()), 4), quote_status=n_status) + q = d[ok & (d["dte"] > 0.5) & (d["dte"] < 95)].copy() + q["hr"] = q["ts"].dt.floor("h") + q["ad"] = q["delta"].abs() + # una riga per (ora, strumento): se in un'ora ci sono piu' giri, vale l'ULTIMO + q = q.sort_values("ts").drop_duplicates(subset=["hr", "asset", "instrument_name"], keep="last") + return q, cont + + +def conta_tick(q: pd.DataFrame) -> pd.DataFrame: + """Quanti tick vale lo spread, e quanti tick vale l'ask? (regola: prima di credere a un + rapporto estremo su un prezzo piccolo, contare i tick).""" + out = [] + for a in ASSETS: + for nome, m in (("ala |d|~0.10", (q["ad"] > 0.06) & (q["ad"] < 0.14)), + ("25-delta", (q["ad"] > 0.20) & (q["ad"] < 0.30)), + ("ATM |d|~0.50", (q["ad"] > 0.45) & (q["ad"] < 0.55))): + g = q[(q["asset"] == a) & m] + if g.empty: + continue + tk = tick_of(g["ask"].to_numpy()) + spread_tick = (g["ask"].to_numpy() - g["bid"].to_numpy()) / tk + ask_tick = g["ask"].to_numpy() / tk + out.append(dict(asset=a, banda=nome, n=len(g), + ask_tick_med=round(float(np.median(ask_tick)), 1), + ask_tick_p5=round(float(np.percentile(ask_tick, 5)), 1), + frac_ask_le2tick=round(float((ask_tick <= 2).mean()), 4), + spread_tick_med=round(float(np.median(spread_tick)), 1), + spread_su_mid_med=round(float(np.median( + (g["ask"] - g["bid"]) / g["mid"].replace(0, np.nan))), 3))) + return pd.DataFrame(out) + + +# ============================================================================= +# 1. SUPERFICIE: smile -> RR/BF a maturita' costante +# ============================================================================= +def _iv_at(ad: np.ndarray, iv: np.ndarray, tgt: float) -> float: + o = np.argsort(ad) + ad, iv = ad[o], iv[o] + if len(ad) < 2 or tgt < ad[0] or tgt > ad[-1]: + return np.nan # nessuna estrapolazione, mai + return float(np.interp(tgt, ad, iv)) + + +def build_smile(q: pd.DataFrame) -> pd.DataFrame: + """Per (asset, ora, scadenza): IV a |delta|=0.25 su entrambe le ali e ATM a |delta|=0.50. + + Vettorizzato per bracketing (nessun ciclo per gruppo): per ogni chiave si prende la riga con + |delta| massimo SOTTO il bersaglio e quella con |delta| minimo SOPRA, poi si interpola fra + le due. Se una delle due manca, la cella si scarta -> non si estrapola MAI (una IV a 25 delta + ricavata estrapolando dall'ala e' un numero inventato, e sarebbe proprio dove il segnale + sembrerebbe piu' forte).""" + KEY = ["asset", "hr", "exp", "option_type"] + + def bracket(d: pd.DataFrame, tgt: float) -> pd.DataFrame: + d = d[["asset", "hr", "exp", "option_type", "ad", "iv"]] + lo = d[d["ad"] <= tgt].sort_values("ad").groupby(KEY, sort=False, observed=True).tail(1) + hi = d[d["ad"] >= tgt].sort_values("ad").groupby(KEY, sort=False, observed=True).head(1) + m = lo.merge(hi, on=KEY, suffixes=("_lo", "_hi")) + span = (m["ad_hi"] - m["ad_lo"]).to_numpy() + w = np.where(span > 1e-12, (tgt - m["ad_lo"].to_numpy()) / np.where(span > 1e-12, span, 1.0), 0.0) + m["iv_t"] = m["iv_lo"].to_numpy() * (1 - w) + m["iv_hi"].to_numpy() * w + return m[KEY + ["iv_t"]] + + # bastano le scadenze che possono ABBRACCIARE 7 o 30 giorni + d = q[q["dte"] <= 70.0] + b25 = bracket(d, 0.25).pivot_table(index=["asset", "hr", "exp"], columns="option_type", + values="iv_t", observed=True) + b50 = bracket(d, 0.50).pivot_table(index=["asset", "hr", "exp"], columns="option_type", + values="iv_t", observed=True) + R = pd.DataFrame(index=b25.index) + R["ivc25"] = b25.get("C") + R["ivp25"] = b25.get("P") + R = R.dropna(subset=["ivc25", "ivp25"]) # servono ENTRAMBE le ali, sempre + R["ivatm"] = b50.reindex(R.index)[[c for c in ("C", "P") if c in b50.columns]].mean(axis=1) + agg = d.groupby(["asset", "hr", "exp"], sort=False, observed=True).agg( + dte=("dte", "first"), ts_max=("ts", "max"), n_gambe=("iv", "size")) + R = R.join(agg, how="left").reset_index() + R["rr"] = R["ivc25"] - R["ivp25"] + R["bf"] = 0.5 * (R["ivc25"] + R["ivp25"]) - R["ivatm"] + return R + + +def const_maturity(R: pd.DataFrame, asset: str, T: float) -> pd.DataFrame: + """RR/BF/ATM a maturita' costante T giorni. Interpolazione lineare in DTE fra le due scadenze + che ABBRACCIANO T (mai estrapolazione) -> il roll e' continuo, non c'e' data di roll.""" + g = R[(R["asset"] == asset)].dropna(subset=["rr", "ivatm"]) + out = {} + for hr, h in g.groupby("hr"): + h = h.sort_values("dte") + dt = h["dte"].to_numpy() + if len(h) < 2 or T < dt[0] or T > dt[-1]: + continue + out[hr] = dict(rr=float(np.interp(T, dt, h["rr"].to_numpy())), + bf=float(np.interp(T, dt, h["bf"].to_numpy())), + atm=float(np.interp(T, dt, h["ivatm"].to_numpy())), + ts_max=h["ts_max"].max()) + S = pd.DataFrame.from_dict(out, orient="index").sort_index() + S.index.name = "hr" + if len(S): + S["rr_n"] = S["rr"] / S["atm"] # skew normalizzato dal livello (toglie il DVOL) + return S + + +# ============================================================================= +# 2. Q1 β€” lo skew ANTICIPA lo spot? +# ============================================================================= +def px5m(asset: str, t0: pd.Timestamp) -> pd.Series: + p = pd.read_parquet(ROOT / "data" / "raw" / f"{asset.lower()}_5m.parquet", + columns=["timestamp", "close"]) + s = pd.Series(p["close"].to_numpy(float), + index=pd.DatetimeIndex(pd.to_datetime(p["timestamp"], unit="ms", utc=True))) + return s.sort_index().loc[t0 - pd.Timedelta("3h"):] + + +def leadlag(S: pd.DataFrame, asset: str) -> pd.DataFrame: + """Correlazione fra la variazione oraria dello skew e i rendimenti PRIMA e DOPO l'istante + esatto in cui le quote sono state osservate (`ts_max`). L'ancora conta: con un'ancora + assunta a :30 invece del `ts_max` vero compare un falso lead a +15m.""" + S = S.sort_values("ts_max") + px = px5m(asset, S.index.min()) + anc = pd.Series(px.reindex(pd.DatetimeIndex(S["ts_max"]), method="ffill").to_numpy(), + index=S.index) + drr = S["rr"].diff() + lvl = S["rr"] + rows = [] + for mins in (-60, -30, -15, -5, 5, 15, 30, 60, 120, 240, 1440, 4320): + tgt = pd.DatetimeIndex(S["ts_max"]) + pd.Timedelta(minutes=int(mins)) + p2 = pd.Series(px.reindex(tgt, method="ffill").to_numpy(), index=S.index) + r = np.log(p2 / anc) if mins > 0 else np.log(anc / p2) # sempre "rendimento nel verso del tempo" + j = pd.concat({"d": drr, "l": lvl, "r": r}, axis=1).dropna() + n = len(j) + rows.append(dict(asset=asset, minuti=mins, n=n, + corr_dRR=round(float(j["d"].corr(j["r"])), 3), + corr_RR=round(float(j["l"].corr(j["r"])), 3), + se=round(1 / np.sqrt(max(n, 2)), 3), + t_dRR=round(float(j["d"].corr(j["r"]) * np.sqrt(n)), 2))) + return pd.DataFrame(rows) + + +def leadlag_ancora_sbagliata(S: pd.DataFrame, asset: str) -> pd.DataFrame: + """Controllo POSITIVO del punto precedente: la stessa misura con l'ancora ASSUNTA a :30 + dell'ora (invece del `ts_max` osservato) deve mostrare il falso lead. Se non lo mostra, + la correzione non stava correggendo niente.""" + px = px5m(asset, S.index.min()) + fake = pd.DatetimeIndex(S.index) + pd.Timedelta("30min") + anc = pd.Series(px.reindex(fake, method="ffill").to_numpy(), index=S.index) + drr = S["rr"].diff() + rows = [] + for mins in (5, 15, 30, 60): + p2 = pd.Series(px.reindex(fake + pd.Timedelta(minutes=mins), method="ffill").to_numpy(), + index=S.index) + j = pd.concat({"d": drr, "r": np.log(p2 / anc)}, axis=1).dropna() + rows.append(dict(asset=asset, minuti=mins, corr_dRR_ancora_finta=round(float(j["d"].corr(j["r"])), 3))) + return pd.DataFrame(rows) + + +def daily_signal(S: pd.DataFrame, df: pd.DataFrame) -> pd.DataFrame: + """Allinea la superficie alla griglia dei prezzi in modo CAUSALE. + + Le barre del progetto sono open-labeled: la barra 1d etichettata D chiude alle 23:00 di D. + Quindi al momento della decisione della barra D e' disponibile l'ultima ora <= D 23:00, e + `eval_weights` la tiene durante D+1. Con merge_asof su epoca esplicita in millisecondi + (lezione 01/07: `.view("int64")` su tz-aware non-ns sbaglia scala e broadcasta).""" + bpd = A.bars_per_day(df) + chiusura = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + chiusura = chiusura + (pd.Timedelta("24h") - pd.Timedelta("1h") if bpd == 1 else pd.Timedelta(0)) + left = pd.DataFrame({"t_ms": _ms(chiusura)}) + right = pd.DataFrame({"t_ms": _ms(pd.DatetimeIndex(S["ts_max"])), + "rr": S["rr"].to_numpy(), "rr_n": S["rr_n"].to_numpy(), + "bf": S["bf"].to_numpy(), "atm": S["atm"].to_numpy()}).sort_values("t_ms") + m = pd.merge_asof(left.sort_values("t_ms"), right, on="t_ms", direction="backward", + tolerance=int(pd.Timedelta("6h").total_seconds() * 1000)) + return m[["rr", "rr_n", "bf", "atm"]] + + +def make_target(S: pd.DataFrame, feat: str, win: int, segno: int): + """target_fn(df, asset) -> posizione continua vol-targeted. Causale per costruzione: + ogni feature usa solo z-score rolling su dati <= barra corrente, e `eval_weights` sposta.""" + def fn(df, asset=None): + F = daily_signal(S, df) + x = F["rr_n"] if feat.endswith("_n") else F["rr"] + if feat.startswith("d"): + x = x.diff() + z = (x - x.rolling(win, min_periods=max(5, win // 2)).mean()) / \ + x.rolling(win, min_periods=max(5, win // 2)).std() + d = np.tanh(np.nan_to_num(z.to_numpy(), nan=0.0)) * segno + return A.vol_target(d, df, target_vol=0.20, leverage_cap=2.0) + return fn + + +def sharpe_boot(s: pd.Series, blocco: int = 10, n: int = 500, seed: int = 20260822) -> tuple: + """IC95% dello Sharpe con block bootstrap circolare (il segnale e' lentissimo: i giorni + NON sono indipendenti, e una SE analitica mentirebbe).""" + r = np.asarray(s.dropna().to_numpy(), float) + T = len(r) + if T < 20: + return (np.nan, np.nan) + rng = np.random.default_rng(seed) + nb = int(np.ceil(T / blocco)) + out = [] + for _ in range(n): + st = rng.integers(0, T, nb) + idx = (st[:, None] + np.arange(blocco)[None, :]).ravel()[:T] % T + x = r[idx] + out.append(np.mean(x) / np.std(x) * np.sqrt(365.25) if np.std(x) > 0 else 0.0) + return (round(float(np.percentile(out, 2.5)), 2), round(float(np.percentile(out, 97.5)), 2)) + + +def causality_finestra(S: pd.DataFrame, fn, asset: str) -> dict: + """`A.causality_ok` taglia il prefisso all'80%/92% della storia dei PREZZI (2024/2025): + fuori dalla finestra dello skew, dove il segnale e' zero -> confronterebbe zeri con zeri, + POTENZA NULLA (stessa trappola del self-check di SKH01: si campiona sugli EVENTI). + Qui i tagli cadono DENTRO la finestra.""" + df = A.get(asset, "1d") + full = np.nan_to_num(np.asarray(fn(df, asset), float)) + n = len(df) + dts = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + dentro = np.where(dts >= S.index.min())[0] + peggio, controlli = 0.0, 0 + for frac in (0.4, 0.7, 0.9): + cut = int(dentro[0] + frac * (n - dentro[0])) + if cut <= dentro[0] + 5 or cut >= n: + continue + sub = df.iloc[:cut].reset_index(drop=True) + s = np.nan_to_num(np.asarray(fn(sub, asset), float)) + coda = min(20, cut - dentro[0]) + peggio = max(peggio, float(np.max(np.abs(s[cut - coda:cut] - full[cut - coda:cut])))) + controlli += 1 + return dict(ok=bool(peggio <= 1e-6 and controlli > 0), max_diff=round(peggio, 10), + controlli=controlli, non_zero=int((full[dentro] != 0).sum())) + + +# ============================================================================= +# 3. Q2 β€” lo skew come GATE DI RISCHIO sopra TP01 +# ============================================================================= +def dd_of(r: np.ndarray) -> float: + eq = np.cumprod(1.0 + np.nan_to_num(r)) + pk = np.maximum.accumulate(eq) + return float(np.max((pk - eq) / pk)) if len(eq) else 0.0 + + +def sh_of(r: np.ndarray) -> float: + r = np.asarray(r, float) + return float(np.mean(r) / np.std(r) * np.sqrt(365.25)) if np.std(r) > 0 else 0.0 + + +def null_de_levering(base: np.ndarray, overlay: np.ndarray) -> dict: + """Il primo test di OGNI claim su meno drawdown: esiste k<=1 che, applicato al baseline, + da' lo STESSO DD dell'overlay ma con Sharpe MIGLIORE? Se si', l'overlay e' solo meno leva.""" + dd_t = dd_of(overlay) + lo, hi = 0.0, 1.0 + for _ in range(60): + k = 0.5 * (lo + hi) + if dd_of(k * base) > dd_t: + hi = k + else: + lo = k + k = 0.5 * (lo + hi) + return dict(k=round(k, 3), dd_overlay=round(dd_t, 4), dd_base_k=round(dd_of(k * base), 4), + sh_overlay=round(sh_of(overlay), 3), sh_base_k=round(sh_of(k * base), 3), + superato=bool(sh_of(overlay) > sh_of(k * base))) + + +# ============================================================================= +# 4. Q3 β€” lo skew spiega l'errore di prezzo di VRP01? +# ============================================================================= +def vrp_decomposizione(q: pd.DataFrame, cm7: dict) -> pd.DataFrame: + """Per ogni ingresso settimanale di VRP01 (stessa macchina di `cblib`, cosi' i numeri sono + confrontabili con lo 0.718 pubblicato il 30/07), scompone il f in fattori MOLTIPLICATIVI: + + f_tot = f_term x f_skew x f_markfit x f_spread + + f_term = credito prezzato ad ATM 7g piatta / credito prezzato a DVOL30 piatta + (quanto del difetto e' STRUTTURA A TERMINE: il sleeve prezza un'opzione a 7 + giorni con l'indice a 30 giorni) + f_skew = credito prezzato a IV PER GAMBA / credito ad ATM 7g piatta + (quanto e' SKEW: l'ala che si compra non vale la vol ATM) + f_markfit = credito ai MID reali / credito a IV per gamba (controllo: deve stare a ~1, + altrimenti la IV di Deribit non riprezza i propri mid e tutta la catena di + ragionamento poggia su un modello invece che su un prezzo) + f_spread = credito al fill CONSERVATIVO (vendi al bid, compri all'ask) / credito ai mid + """ + righe = [] + for a in ASSETS: + S7 = cm7[a] + E = CB.weekly_entries(a, q) + for _, e in E.iterrows(): + prima = S7.index[S7.index <= e["ts"]] + if len(prima) == 0 or (e["ts"] - prima[-1]) > pd.Timedelta("6h"): + continue + s = S7.loc[prima[-1]] + T = e["dte"] / 365.25 + dvol = e["dvol_pct"] / 100.0 + atm7 = s["atm"] / 100.0 + c_mod = CB.bs_put(e["spot"], e["k_short"], T, dvol) - CB.bs_put(e["spot"], e["k_long"], T, dvol) + c_atm = CB.bs_put(e["spot"], e["k_short"], T, atm7) - CB.bs_put(e["spot"], e["k_long"], T, atm7) + c_sml = (CB.bs_put(e["spot"], e["k_short"], T, e["iv_short"] / 100.0) + - CB.bs_put(e["spot"], e["k_long"], T, e["iv_long"] / 100.0)) + c_mid = e["cred_mid"] + c_real = e["cred_real"] + righe.append(dict( + asset=a, ts=e["ts"], dte=round(e["dte"], 2), ivrank=round(e["ivrank"], 3), + dvol=round(e["dvol_pct"], 2), atm7=round(s["atm"], 2), + rr7=round(s["rr"], 2), bf7=round(s["bf"], 2), + iv_short=round(e["iv_short"], 2), iv_long=round(e["iv_long"], 2), + d_short=round(e["d_short"], 3), d_long=round(e["d_long"], 3), + f_term=c_atm / c_mod if c_mod > 0 else np.nan, + f_skew=c_sml / c_atm if c_atm > 0 else np.nan, + f_markfit=c_mid / c_sml if c_sml > 0 else np.nan, + f_spread=c_real / c_mid if c_mid > 0 else np.nan, + f_tot_mid=c_mid / c_mod if c_mod > 0 else np.nan, + f_tot=c_real / c_mod if c_mod > 0 else np.nan, + quota_ala=CB.bs_put(e["spot"], e["k_long"], T, dvol) / + CB.bs_put(e["spot"], e["k_short"], T, dvol), + )) + return pd.DataFrame(righe) + + +MARKFIT_OK = (0.85, 1.15) # banda del CONTROLLO, dichiarata prima di guardare i f + + +def qualita(D: pd.DataFrame) -> pd.Series: + """Filtro dichiarato: si tiene un'osservazione solo se il CONTROLLO passa, cioe' se la IV di + Deribit riprezza i suoi stessi mid entro +-15% sul credito, e se il credito conservativo e' + positivo. NON e' un filtro sull'esito (il f), e' un filtro sulla variabile di controllo: + dove il controllo fallisce non si sta misurando lo skew, si sta misurando una quota rotta.""" + return (D["f_markfit"].between(*MARKFIT_OK)) & (D["f_spread"] > 0) & D["f_term"].gt(0) + + +def attribuzione_log(D: pd.DataFrame) -> pd.DataFrame: + """Attribuzione ESATTA e additiva: log(f_tot) = log(f_term)+log(f_skew)+log(f_markfit)+ + log(f_spread), per costruzione (la catena dei rapporti telescopia). Le MEDIANE dei fattori + non si moltiplicano fra loro; i log si sommano. Da qui la QUOTA del difetto per causa.""" + g = D[qualita(D)] + parti = ["f_term", "f_skew", "f_markfit", "f_spread"] + L = np.log(g[parti]) + tot = float(np.log(g["f_tot"]).mean()) + rows = [] + for c in parti: + m = float(L[c].mean()) + rows.append(dict(fattore=c, media_log=round(m, 4), + fattore_geometrico=round(float(np.exp(m)), 3), + quota_del_difetto=round(m / tot, 3) if tot != 0 else np.nan)) + rows.append(dict(fattore="TOTALE", media_log=round(tot, 4), + fattore_geometrico=round(float(np.exp(tot)), 3), quota_del_difetto=1.0)) + return pd.DataFrame(rows) + + +def panel_f(q: pd.DataFrame, cm7: dict) -> pd.DataFrame: + """La stessa decomposizione, ma a OGNI ora in cui lo spread e' costruibile (non solo ai 22 + ingressi settimanali di VRP01). Serve alla RISOLUZIONE: 22 punti non separano due cause da + ~10% l'una. ⚠ Le ore dentro la stessa scadenza NON sono indipendenti: il campione efficace + resta il numero di SCADENZE, che si riporta accanto.""" + out = [] + for a in ASSETS: + S7 = cm7[a] + S, V = CB.spot_series(a), CB.dvol_series(a) + p = q[(q["asset"] == a) & (q["option_type"] == "P") + & (q["dte"] >= 4.0) & (q["dte"] <= 10.0)] + for (hr, exp), g in p.groupby(["hr", "exp"], sort=False): + if hr not in S7.index: + continue + legs = CB.pick_legs(g) + if legs is None: + continue + ts = g["ts"].max() + dte = float((exp - ts).total_seconds() / 86400.0) + if not (4.0 <= dte <= 10.0): + continue + spot = float(S.asof(ts)); dvol = float(V.asof(ts)) / 100.0 + atm7 = float(S7.loc[hr, "atm"]) / 100.0 + T = dte / 365.25 + c_mod = CB.bs_put(spot, legs["k_short"], T, dvol) - CB.bs_put(spot, legs["k_long"], T, dvol) + c_atm = CB.bs_put(spot, legs["k_short"], T, atm7) - CB.bs_put(spot, legs["k_long"], T, atm7) + c_sml = (CB.bs_put(spot, legs["k_short"], T, legs["iv_short"] / 100.0) + - CB.bs_put(spot, legs["k_long"], T, legs["iv_long"] / 100.0)) + c_mid = (legs["mid_short"] - legs["mid_long"]) * spot + c_real = (legs["bid_short"] - legs["ask_long"]) * spot + if not (c_mod > 0 and c_atm > 0 and c_sml > 0): + continue + hist = V[V.index < ts] + ivr = float((hist < dvol * 100).mean()) if len(hist) else np.nan + out.append(dict(asset=a, hr=hr, exp=exp, dte=dte, rr7=float(S7.loc[hr, "rr"]), + atm7=float(S7.loc[hr, "atm"]), dvol=dvol * 100, ivrank=ivr, + f_term=c_atm / c_mod, f_skew=c_sml / c_atm, + f_markfit=c_mid / c_sml, f_spread=c_real / c_mid, + f_tot=c_real / c_mod, f_tot_mid=c_mid / c_mod)) + return pd.DataFrame(out) + + +# ============================================================================= +def main() -> None: + t_start = time.time() + rule("r0822_skew β€” SKEW (risk reversal 25-delta): la dimensione mai usata della superficie") + + # ---------------------------------------------------------------- 0. DATO + rule("0. IL DATO β€” quote, non righe") + q, cont = load_quoted() + print(f" righe totali nella catena : {cont['righe']:,}") + print(f" dopo dedup (ts, strumento) : {cont['dedup']:,}") + print(f" QUOTATE (bid>0, ask>0, iv/delta ok) : {cont['quotate']:,} = {cont['frac_quotate']:.1%}") + print(f" quote_status : {cont['quote_status']}") + print(f" usate (0.5 {q['ts'].max()}") + print("\n TICK (regola: prima di credere a un rapporto estremo su un prezzo piccolo, contarli)") + print(conta_tick(q).to_string(index=False)) + + # ---------------------------------------------------------- 1. SUPERFICIE + rule("1. SUPERFICIE β€” smile per delta, poi maturita' costante") + t0 = time.time() + R = build_smile(q) + print(f" celle (asset, ora, scadenza) con smile ricostruibile: {len(R):,} [{time.time()-t0:.0f}s]") + perday = R.groupby(["asset", pd.DatetimeIndex(R["hr"]).date]).agg(exps=("exp", "nunique")) + print(f" giorni con >=2 scadenze (maturita' costante possibile): " + f"{int((perday['exps'] >= 2).sum())} / {len(perday)} coppie asset-giorno") + cm7, cm30 = {}, {} + for a in ASSETS: + cm7[a] = const_maturity(R, a, 7.0) + cm30[a] = const_maturity(R, a, 30.0) + s = cm30[a] + print(f" {a}: CM30 ore={len(s):5d} giorni={len(np.unique(pd.DatetimeIndex(s.index).date)):3d}" + f" {s.index.min()} -> {s.index.max()}") + print(f" RR30 media {s['rr'].mean():+.2f} pp sd {s['rr'].std():.2f} " + f"[{s['rr'].min():+.2f}, {s['rr'].max():+.2f}] ATM media {s['atm'].mean():.1f}% " + f"BF30 media {s['bf'].mean():+.2f}") + print(f" CM7 ore={len(cm7[a]):5d} RR7 media {cm7[a]['rr'].mean():+.2f} sd {cm7[a]['rr'].std():.2f}") + print("\n ⚠ La finestra utile parte dal 2026-06-09 e NON dal 2026-05-01: prima di quella data") + print(" cerbero-bite raccoglieva UNA sola scadenza per ora -> nessuna maturita' costante.") + print(" righe presenti != superficie presente. Le ore perse sono ~38 giorni su 113.") + + # ------------------------------------------------------------------ 2. Q1 + rule("2. Q1 β€” lo skew ANTICIPA lo spot? (potenza vera: N orario ~1.800)") + LL = pd.concat([leadlag(cm30[a], a) for a in ASSETS], ignore_index=True) + for a in ASSETS: + print(f"\n {a} β€” corr fra dRR (variazione oraria dello skew) e il rendimento nella") + print(" finestra indicata, ancorata al TIMESTAMP VERO delle quote (ts_max):") + print(LL[LL["asset"] == a].to_string(index=False)) + fw = LL[LL["minuti"] > 0] + print(f"\n LAG IN AVANTI: {len(fw)} test ({fw['minuti'].nunique()} orizzonti x {len(ASSETS)} asset).") + print(f" |t| massimo osservato = {fw['t_dRR'].abs().max():.2f} su {len(fw)} test;") + print(f" |t| atteso come MASSIMO di {len(fw)} test indipendenti sotto il nullo ~ " + f"{float(np.abs(np.random.default_rng(0).standard_normal((4000, len(fw)))).max(axis=1).mean()):.2f}.") + print(" -> il massimo in avanti e' quello che il rumore produce da solo. LAG INDIETRO:") + print(f" |t| minimo = {LL[LL['minuti'] < 0]['t_dRR'].abs().min():.2f} su " + f"{int((LL['minuti'] < 0).sum())} test, tutti dello STESSO segno.") + print("\n Robustezza β€” il COLLETTORE cambia a meta' campione (cerbero-bite: ~10 giri/ora,") + print(" fino al 2026-07-30; raccolta nostra: 1 giro/ora al minuto :25) e il campione contiene") + print(" UN evento enorme (BTC +26% nella settimana al 21/08). Le due cose vanno separate:") + TAGLIO = pd.Timestamp("2026-07-30 20:00", tz="UTC") + RALLY = pd.to_datetime(["2026-08-19", "2026-08-20", "2026-08-21", "2026-08-22"]).tz_localize("UTC") + for a in ASSETS: + S = cm30[a] + gg_rally = pd.DatetimeIndex(S.index).normalize().isin(RALLY) + for nome, m in (("bite ", S.index < TAGLIO), + ("nostra ", S.index >= TAGLIO), + ("nostra SENZA rally", (S.index >= TAGLIO) & ~gg_rally)): + sub = S[m].sort_values("ts_max") + if len(sub) < 100: + continue + px = px5m(a, sub.index.min()) + anc = pd.Series(px.reindex(pd.DatetimeIndex(sub["ts_max"]), method="ffill").to_numpy(), + index=sub.index) + d = sub["rr"].diff() + av = [] + for mins in (5, 15, 60): + p2 = pd.Series(px.reindex(pd.DatetimeIndex(sub["ts_max"]) + pd.Timedelta(minutes=mins), + method="ffill").to_numpy(), index=sub.index) + j = pd.concat({"d": d, "r": np.log(p2 / anc)}, axis=1).dropna() + av.append((round(float(j["d"].corr(j["r"])), 3), + round(float(j["d"].corr(j["r"], method="spearman")), 3))) + print(f" {a} {nome} n={len(sub):5d} avanti +5/+15/+60m (pearson,spearman): {av}") + print(" -> l'unica cella con un lead in avanti apparente (BTC, raccolta nostra, +5/+15m)") + print(" PERDE i due terzi togliendo 4 giorni su 24: era il rally, non un meccanismo.") + gio = pd.Series(1, index=pd.DatetimeIndex(cm30[ASSETS[0]].index)).groupby( + pd.DatetimeIndex(cm30[ASSETS[0]].index).date).sum() + print(f" copertura: giorni con meno ore di superficie (BTC) " + f"{ {str(k): int(v) for k, v in gio.nsmallest(4).items()} } su 24 attese " + f"(il guasto quote-vuote del 29-30/07 e' dentro il campione e NON crea un buco: le") + print(" quote assenti erano gia' escluse dal filtro 'quotato').") + + print("\n Controllo POSITIVO β€” la stessa misura con l'ancora ASSUNTA a :30 dell'ora:") + FF = pd.concat([leadlag_ancora_sbagliata(cm30[a], a) for a in ASSETS], ignore_index=True) + print(FF.to_string(index=False)) + + # strategia giornaliera + rule("2b. Q1 β€” lo skew come SEGNALE DIREZIONALE giornaliero (griglia dichiarata)") + feats = ["rr", "rr_n", "drr"] + wins = [10, 20, 40] + segni = [+1, -1] + celle, serie = [], {} + for f in feats: + for w in wins: + for sg in segni: + nome = f"{f}|w{w}|sg{sg:+d}" + per_asset = {} + for a in ASSETS: + df = A.get(a, "1d") + Sx = cm30[a] + m = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) >= ( + Sx.index.min().floor("D")) + sub = df[m].reset_index(drop=True) + tgt = make_target(Sx, f, w, sg)(sub, a) + ev = A.eval_weights(sub, tgt) + per_asset[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(per_asset, axis=1, join="inner").fillna(0.0) + d = 0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]] + d.index = pd.DatetimeIndex(d.index) + serie[nome] = d + celle.append(dict(cella=nome, sharpe=round(sh_of(d.to_numpy()), 3), + maxdd=round(dd_of(d.to_numpy()), 4), + cagr=round(float((1 + d).prod() ** (365.25 / len(d)) - 1), 4))) + C = pd.DataFrame(celle).sort_values("sharpe", ascending=False) + print(f" griglia: {len(feats)} feature x {len(wins)} finestre-z x {len(segni)} segni = {len(C)} celle") + print(C.head(6).to_string(index=False)) + print(" ...") + print(C.tail(3).to_string(index=False)) + best = C.iloc[0]["cella"] + bs = serie[best] + lo, hi = sharpe_boot(bs) + print(f"\n MIGLIORE della griglia: {best} Sharpe {C.iloc[0]['sharpe']:+.3f}") + print(f" IC95% block-bootstrap (blocchi 10g, 500 estrazioni): [{lo:+.2f}, {hi:+.2f}] n={len(bs)} giorni") + print(f" ⚠ ampiezza dell'IC = {hi-lo:.2f} di Sharpe: con {len(bs)} giorni la misura non separa") + print(" un edge da zero. Nessuna cella di questa griglia e' distinguibile dal rumore.") + dsr, sr0 = A.deflated_sharpe(C.iloc[0]["sharpe"], C["sharpe"].tolist(), bs) + print(f" deflated Sharpe della cella migliore: DSR={dsr:.3f} (soglia 0.95), " + f"max atteso sotto il nullo {sr0:+.2f}") + print(" NB: e' un deflated-Sharpe BEST-OF-GRID, non in-sample-selected: `select_cell_insample`") + print(" NON e' eseguibile qui (HOLDOUT=2025-01-01, l'in-sample e' VUOTO).") + + print("\n marginal_vs_tp01 sulla cella migliore:") + try: + rep = A.marginal_vs_tp01(bs) + for k in ("marginal_verdict", "corr_full", "n_days", "has_insample_edge", "is_hedge", + "beats_noise_null", "robust_oos"): + if k in rep: + print(f" {k:20s} = {rep[k]}") + except Exception as ex: + print(f" non girato: {ex}") + + imp = A.implausible_sharpe(bs) + print(f"\n implausible_sharpe sulla cella migliore: implausible={imp['implausible']} " + f"reasons={imp.get('reasons')}") + print(f" (Sharpe {imp.get('sharpe')}, maxDD {imp.get('maxdd')}, " + f"perdite sulle barre ATTIVE {imp.get('loss_frac')}, attive {imp.get('active_frac')})") + + print("\n causalita' (tagli DENTRO la finestra dello skew, non all'80% della storia prezzi):") + for a in ASSETS: + f_, w_, sg_ = best.split("|") + fn = make_target(cm30[a], f_, int(w_[1:]), int(sg_.replace("sg", ""))) + print(f" {a}: {causality_finestra(cm30[a], fn, a)}") + + # ------------------------------------------------------------------ 3. Q2 + rule("3. Q2 β€” lo skew come GATE DI RISCHIO sopra TP01") + B = A.tp01_baseline_daily() + inizio = min(cm30[a].index.min() for a in ASSETS).floor("D") + Bw = B[B.index >= inizio] + z_all = {} + for a in ASSETS: + df = A.get(a, "1d") + m = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) >= inizio + sub = df[m].reset_index(drop=True) + F = daily_signal(cm30[a], sub) + x = F["rr_n"] + z = (x - x.rolling(20, min_periods=10).mean()) / x.rolling(20, min_periods=10).std() + z_all[a] = pd.Series(z.to_numpy(), index=pd.DatetimeIndex(pd.to_datetime(sub["datetime"], utc=True))) + Z = pd.concat(z_all, axis=1).mean(axis=1) + Z = Z.reindex(Bw.index).ffill() + att = int((Bw != 0).sum()) + print(f" baseline TP01 sulla finestra dello skew: n={len(Bw)} giorni, " + f"Sharpe {sh_of(Bw.to_numpy()):+.2f}, maxDD {dd_of(Bw.to_numpy()):.2%}") + print(f" ⚠ barre ATTIVE del baseline: {att}/{len(Bw)} ({att/len(Bw):.1%}) β€” TP01 e' FLAT il resto") + print(f" del tempo, e nella finestra il suo maxDD e' {dd_of(Bw.to_numpy()):.2%}: un gate di") + print(" de-risk non ha NIENTE da proteggere qui. Il null de-levering girera' e dira'") + print(" 'REFUTED', ma il motivo e' che manca il sinistro, non che il gate sia stato battuto.") + for thr in (-0.5, -1.0, -1.5): + gate = (Z.shift(1) > thr).astype(float) # de-risk quando lo skew si irripidisce (z basso) + ov = (Bw * gate).to_numpy() + n_fire = int((gate == 0).sum()) + nul = null_de_levering(Bw.to_numpy(), ov) + gg = Bw[gate == 0] + print(f" soglia z<{thr:+.1f}: giorni de-riskati {n_fire:3d}/{len(Bw)} " + f"({n_fire/len(Bw):.1%}) Sharpe {sh_of(ov):+.2f} (base {sh_of(Bw.to_numpy()):+.2f}) " + f"maxDD {dd_of(ov):.2%}") + print(f" null de-levering: k={nul['k']} -> stesso DD {nul['dd_base_k']:.2%} con " + f"Sharpe {nul['sh_base_k']:+.2f} vs overlay {nul['sh_overlay']:+.2f} " + f"-> {'SUPERATO' if nul['superato'] else 'REFUTED'}") + if n_fire: + gia_flat = int((gg == 0).sum()) + print(f" nei giorni de-riskati TP01 faceva in media {gg.mean()*100:+.3f}%/g " + f"(gli altri {Bw[gate==1].mean()*100:+.3f}%/g); di quei {n_fire} giorni TP01 era " + f"GIA' FLAT in {gia_flat} ({gia_flat/n_fire:.0%}) -> il gate spegne cio' che era gia' spento") + print("\n ridondanza col trend β€” corr fra lo z dello skew e il rendimento di TP01: " + f"{float(pd.concat({'z': Z, 'b': Bw}, axis=1).dropna().corr().iloc[0,1]):+.3f}") + print(" POTENZA: con 75 giorni un gate che scatta ~10 volte non e' misurabile. Il numero da") + print(" guardare non e' lo Sharpe dell'overlay ma il conto dei giorni in cui scatta.") + + # ------------------------------------------------------------------ 4. Q3 + rule("4. Q3 β€” lo skew SPIEGA l'errore di prezzo di VRP01 (f=0.73 pubblicato il 30/07)") + D = vrp_decomposizione(q, cm7) + print(f" ingressi settimanali ricostruiti: {len(D)} ({D.groupby('asset').size().to_dict()})") + print("\n REPLICA del numero pubblicato (deve tornare ~0.72 sul canonico 7g d-0.28/-0.10):") + print(f" f_tot (fill conservativo) mediana = {D['f_tot'].median():.3f} " + f"media = {D['f_tot'].mean():.3f}") + print(f" f_tot ai MID mediana = {D['f_tot_mid'].median():.3f}") + print("\n IV per gamba contro i due riferimenti (pp di vol):") + print(f" IV(corta) - DVOL30 : {(D['iv_short']-D['dvol']).median():+.2f} pp " + f"IV(lunga) - DVOL30 : {(D['iv_long']-D['dvol']).median():+.2f} pp [pubblicato: +0.8 / +7.5]") + print(f" IV(corta) - ATM7 : {(D['iv_short']-D['atm7']).median():+.2f} pp " + f"IV(lunga) - ATM7 : {(D['iv_long']-D['atm7']).median():+.2f} pp") + print(f" ATM7 - DVOL30 : {(D['atm7']-D['dvol']).median():+.2f} pp <- pura struttura a termine") + print("\n DECOMPOSIZIONE MOLTIPLICATIVA del f (mediane; f_tot = term x skew x markfit x spread)") + for a in ASSETS + ("TUTTI",): + g = D if a == "TUTTI" else D[D["asset"] == a] + if g.empty: + continue + print(f" {a:6s} n={len(g):2d} | f_term {g['f_term'].median():.3f} | " + f"f_skew {g['f_skew'].median():.3f} | f_markfit {g['f_markfit'].median():.3f} | " + f"f_spread {g['f_spread'].median():.3f} || f_tot {g['f_tot'].median():.3f}") + print("\n controllo obbligatorio: f_markfit deve stare a ~1 (la IV di Deribit deve riprezzare") + print(f" i suoi stessi mid). Mediana {D['f_markfit'].median():.3f}, " + f"banda [{D['f_markfit'].quantile(.1):.3f}, {D['f_markfit'].quantile(.9):.3f}].") + print(" Se sta a 1, RR e BF di questa sessione sono un fatto di PREZZO, non un artefatto") + print(" del fit di Deribit -> vale anche come controllo di confound per Q1/Q2.") + + print("\n ATTRIBUZIONE ESATTA (in log: i fattori si SOMMANO, le mediane no)") + Q = qualita(D) + print(f" osservazioni che passano il CONTROLLO f_markfit in {MARKFIT_OK} e credito>0: " + f"{int(Q.sum())}/{len(D)}") + scartate = D[~Q][["asset", "ts", "f_markfit", "f_spread"]].copy() + if len(scartate): + scartate["ts"] = pd.DatetimeIndex(scartate["ts"]).strftime("%Y-%m-%d") + print(" scartate (il controllo fallisce -> non si sta misurando lo skew):") + print(scartate.round(3).to_string(index=False)) + print(attribuzione_log(D).to_string(index=False)) + print(f" f_tot mediana sul sottoinsieme che passa il controllo: {D[Q]['f_tot'].median():.3f}") + + print("\n PANEL ORARIO β€” stessa decomposizione a OGNI ora costruibile (risoluzione)") + PN = panel_f(q, cm7) + QP = qualita(PN) + print(f" osservazioni {len(PN)} ({PN.groupby('asset').size().to_dict()}), di cui passano il " + f"controllo {int(QP.sum())}; scadenze distinte (campione EFFICACE) " + f"{PN.groupby('asset')['exp'].nunique().to_dict()}") + G = PN[QP] + for a in ASSETS + ("TUTTI",): + g = G if a == "TUTTI" else G[G["asset"] == a] + if g.empty: + continue + print(f" {a:6s} n={len(g):5d} | f_term {g['f_term'].median():.3f} | " + f"f_skew {g['f_skew'].median():.3f} | f_markfit {g['f_markfit'].median():.3f} | " + f"f_spread {g['f_spread'].median():.3f} || f_tot {g['f_tot'].median():.3f}") + print(" attribuzione log sul panel:") + print(attribuzione_log(PN).to_string(index=False)) + + print("\n Il f dipende dal REGIME? (la domanda che decide se 0.73 e' conservativo o ottimista)") + D2 = D[qualita(D)].dropna(subset=["f_tot_mid", "rr7"]) + for var in ("rr7", "atm7", "ivrank", "bf7"): + c = float(D2["f_tot_mid"].corr(D2[var])) + cg = float(G["f_tot_mid"].corr(G[var])) if var in G.columns else np.nan + print(f" corr(f_tot_mid, {var:7s}) = {c:+.3f} [ingressi n={len(D2)}]" + + (f" {cg:+.3f} [panel n={len(G)}]" if np.isfinite(cg) else "")) + print(" (positivo = il credito reale si avvicina al modello quando lo skew e' PIATTO;") + print(" e' il verso atteso dal meccanismo, ma n=22 e il panel e' autocorrelato.)") + print("\n IL PEZZO PIU' GRANDE (f_term) CAMBIA SEGNO COL REGIME β€” ed e' questo che decide") + print(" se 0.73 e' conservativo o ottimista dove VRP01 tradera' DAVVERO.") + G2 = G.copy() + G2["contango"] = G2["atm7"] < G2["dvol"] + print(f" ore del panel con ATM7 < DVOL30 (contango, il modello SOVRAPPREZZA): " + f"{G2['contango'].mean():.1%}") + for nome, m in (("contango (ATM7DVOL)", ~G2["contango"])): + g = G2[m] + if g.empty: + continue + print(f" {nome:26s} n={len(g):5d} | f_term {g['f_term'].median():.3f} | " + f"f_skew {g['f_skew'].median():.3f} | f_tot {g['f_tot'].median():.3f} | " + f"IV-rank mediano {g['ivrank'].median():.3f}") + qs = pd.qcut(G2["ivrank"], 4, labels=["q1", "q2", "q3", "q4"], duplicates="drop") + st = G2.groupby(qs, observed=True).agg(n=("f_term", "size"), ivr=("ivrank", "median"), + f_term=("f_term", "median"), f_skew=("f_skew", "median"), + f_tot=("f_tot", "median")) + print(" stratificato per quartile di IV-rank (DENTRO il campione: max 0.23, il gate e' 0.30):") + print(st.round(3).to_string()) + print(" -> f_term sale con l'IV-rank; f_skew no. L'estrapolazione oltre 0.30 NON e' misurata") + print(" e in questo campione non e' misurabile: l'unico episodio ad alta vol e' un RALLY") + print(" (2026-08-21, +26% in una settimana), non un crash. Manca il regime che conta.") + + print(f"\n IV-rank agli ingressi: min {D['ivrank'].min():.3f} mediana {D['ivrank'].median():.3f} " + f"max {D['ivrank'].max():.3f}") + n_gate = int((D["ivrank"] > 0.30).sum()) + print(f" ingressi che passerebbero il gate IV-rank>0.30 di VRP01: {n_gate}/{len(D)}") + print(" -> il f di VRP01 e' misurato INTERAMENTE nel regime in cui il sleeve sta FLAT.") + + print("\n Tabella per ingresso:") + cols = ["asset", "ts", "dte", "ivrank", "dvol", "atm7", "rr7", "iv_short", "iv_long", + "f_term", "f_skew", "f_markfit", "f_spread", "f_tot_mid", "f_tot"] + P = D[cols].copy() + P["ts"] = pd.DatetimeIndex(P["ts"]).strftime("%Y-%m-%d %H:%M") + for c in ("f_term", "f_skew", "f_markfit", "f_spread", "f_tot_mid", "f_tot"): + P[c] = P[c].round(3) + print(P.to_string(index=False)) + + # ------------------------------------------------------------ 5. SOMMARIO + rule("5. SOMMARIO β€” cosa regge, cosa no, e cosa non e' misurabile con 75 giorni") + print(""" + Q1 lo skew NON anticipa lo spot: e' lo spot che muove lo skew. + Il prezzo precede il RR con t da 3.1 a 9.4 su 8 test su 8 e su entrambi gli asset; + in avanti il |t| massimo su 16 test e' 2.35 contro un massimo atteso dal rumore di 2.08, + non replica sul secondo asset, e l'unica cella apparente perde i due terzi togliendo + 4 giorni di rally su 24. La strategia giornaliera migliore su 18 celle fa Sharpe +2.61 + con IC95% [-2.90, +5.86] e DSR 0.163 -> SCARTATO. + + Q2 gate di rischio: NON MISURABILE su questa finestra, e la ragione e' strutturale. + Nei 75 giorni TP01 e' attivo 5 giorni su 75 e il suo maxDD e' 0.14%: non c'e' sinistro + da prevenire. Cio' che si vede: alle soglie z<-1.0 e z<-1.5 il gate spegne giorni in cui + TP01 era GIA' flat nel 100% dei casi -> 5a conferma della ridondanza col trend, ma su un + campione senza potenza. Il null de-levering gira e dice REFUTED per assenza di DD, non + per merito del baseline. + + Q3 il risultato della sessione, e non e' una strategia: e' una CORREZIONE DI PREZZATURA. + Il f=0.73 di VRP01 e' replicato in modo indipendente (0.714 su 22 ingressi, 0.723 su 2.582 + ore) e poi SCOMPOSTO. La causa maggiore NON e' lo skew: + struttura a termine 42% | spread denaro-lettera 30% | skew 25% | fit 2% + Il sleeve prezza un'opzione a 7 giorni con il DVOL a 30 giorni, che nel 90% delle ore sta + ~3 punti di vol SOPRA l'ATM a 7 giorni -> sovrapprezza entrambe le gambe. Il "+0.8pp sulla + gamba corta" pubblicato il 30/07 (qui +0.20pp) e' la CANCELLAZIONE di due errori da ~3pp: + -2.97pp di termine e +2.79pp di skew. Guardando le gambe contro l'ATM a 7 giorni si vede + che il modello sbaglia su ENTRAMBE, non solo sull'ala. + E il pezzo grande CAMBIA SEGNO col regime: f_term 0.83 nel quartile basso di IV-rank -> + 0.93 nel quartile alto, e 1.05 nelle ore in backwardation, mentre f_skew resta piatto + (0.93 -> 0.92). Cioe' l'errore piu' grande si annulla proprio dove il gate IV-rank fa + tradare il sleeve. +""") + rule(f"fine β€” {time.time()-t_start:.0f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_slip_audit.py b/scripts/research/r0822_slip_audit.py new file mode 100644 index 0000000..02cf8c5 --- /dev/null +++ b/scripts/research/r0822_slip_audit.py @@ -0,0 +1,591 @@ +"""r0822 SLIP-AUDIT β€” l'attrito VERO del libro live: quanto costa davvero un fill. + +DOMANDA +------- +Il progetto modella le fee con cura chirurgica (3,50 bps/lato misurati sui fill reali il 21/08) +ma ASSUME il fill al prezzo di chiusura della barra. Lo **slippage realizzato** del libro live +non e' mai stato misurato: e' dichiarato "rischio #1" per XSR01 e semplicemente ignorato per +TP01/SKH01. Ci sono 18 esecuzioni reali dal 2026-07-14. Questo script le misura. + +La domanda decidibile e' UNA: i backtest girano a `fee_rt=0.001` (10 bps RT) mentre la fee vera +e' 3,50 bps/lato = 7 bps RT. C'e' **3 bps RT = 1,50 bps/LATO** di margine gia' pagato dal modello. +**Lo slippage realizzato ci sta dentro?** + +TRE DIFETTI DEL DATO TROVATI PRIMA DI MISURARE (e riparati/dichiarati qui) +------------------------------------------------------------------------- +(1) **`ts_utc` nel log NON e' l'ora del fill.** `book_execute.py:213` scrive + `ts_utc=str(pd.Timestamp(r['last_data']))` = l'etichetta dell'ULTIMA BARRA DEL FEED, non + l'orologio. Tutti e 18 i record dicono `00:00:00`. L'ora vera si recupera solo dalle + intestazioni di `logs/cron_book.log` (`===== cron_book =====`). Senza questo passo + ogni confronto "al prezzo dell'ora del fill" misurerebbe la mezzanotte. + Prova che serviva: il fill #1 e' etichettato `2026-07-08` ed e' stato eseguito il + **2026-07-14T14:00Z** β€” sei giorni dopo (feed-freeze del 14/07, gia' noto al progetto). +(2) **Il feed certificato NON e' lo strumento che si trada.** `rebuild_history.DERIBIT_INSTR` + prende `BTC/USD:BTC` / `ETH/USD:ETH` = i perpetual **INVERSE**; `deribit.INSTRUMENT` esegue su + `BTC_USDC-PERPETUAL` / `ETH_USDC-PERPETUAL` = i **LINEARI USDC**. Il commento nel sorgente dice + "inverse ~ lineare entro 3 bps": **3 bps e' esattamente la taglia della domanda**. Misurare lo + slippage contro il feed certificato significherebbe misurare la BASE, non lo slippage. + Qui la base viene MISURATA (non assunta) e il riferimento vero e' lo strumento tradato. + Stessa forma del difetto di `fee_watch` del 21/08. +(3) **Convenzione LINEARE**: `amount` = quantita' BASE, `fee` gia' in USDC -> + `fee_bps = fee / (filled * price) * 1e4`. La convenzione inverse darebbe ~8 ordini di + grandezza di errore SENZA sollevare nulla. Il controllo e' stampato in chiaro. + +TRE RIFERIMENTI, TENUTI SEPARATI (il punto 1c della missione) +------------------------------------------------------------ +Il modello decide sulla barra GIORNALIERA, il cron esegue all'ORA: sono grandezze diverse. + REF-A "modello" = close della barra 1d etichettata `last_data` (= il prezzo a cui il + BACKTEST assume di riempire). Misura modello-vs-realta', NON slippage. + REF-B "5m/1h" = close dell'ultima barra CHIUSA prima del fill sullo strumento TRADATO. + Questo, e solo questo, e' slippage d'esecuzione. + REF-C "VWAP 1h" = VWAP delle 12 barre 5m dell'ora che contiene il fill (benchmark + NON causale, dichiarato tale). +Segno orientato al lato: `costo_bps = lato * (fill - ref)/ref * 1e4`, lato +1 buy / -1 sell. +Positivo = costo, negativo = guadagno. + +LA STATISTICA CHE HA POTENZA (e perche' quella in bps non ne ha) +---------------------------------------------------------------- +Con n=18 la via in bps e' persa in partenza: il rumore e' la deriva del prezzo fra la chiusura +della barra 5m e il fill, che vale decine di bps. La statistica giusta e' NORMALIZZATA dal range +della barra: il nostro fill e' esso stesso un print della barra 5m che lo contiene, quindi + q = (fill - low)/(high - low) per un BUY, q = 1 - quello per un SELL. +Se i nostri ordini market avessero impatto, un BUY stamperebbe AL MASSIMO della barra -> q -> 1. +Senza impatto E[q] = 0.5, e il null NON si assume uniforme: si MISURA su ~18.000 barre vere. + +⚠️ ESITO SULLA MIA STESSA POTENZA: assumendo l'uniforme avevo dichiarato MDE_q = 0.19 di range +(~5 bps). Il null misurato e' 1.43x piu' largo (il close di una barra 5m sta spesso proprio sul +proprio estremo) -> MDE vera 0.27 = ~7.2 bps, contro 7.8 della via in bps. **La normalizzazione +non compra potenza utile.** Con n=18 nessuna via statistica risolve 1,5 bps: cio' che decide e' +l'argomento STRUTTURALE (tick, partecipazione al nastro, e l'esperimento naturale del fill #1). + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_slip_audit.py + (--no-net salta il fetch dello strumento lineare e lo dichiara non misurato) +""" +from __future__ import annotations + +import json +import os +import re +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +SCRATCH = Path(os.environ.get("SLIP_SCRATCH", "/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad")) + +EXEC_LOG = ROOT / "data" / "live" / "book_executions.jsonl" +CRON_LOG = ROOT / "logs" / "cron_book.log" + +# Sensibilita' PUBBLICATA del libro (r0726_fee_sensitivity.py, CLAUDE.md 26/07). +# Unita': per bps PER LATO (verificato sulla tabella: 0->15 bps/lato = Sharpe 1.849->1.597). +DSHARPE_PER_BPS_SIDE = -0.017 +DCAGR_PER_BPS_SIDE = -0.23 # punti percentuali di CAGR +# Margine gia' pagato dal modello: backtest 10 bps RT vs fee vera 7 bps RT = 3 bps RT. +MARGIN_BPS_SIDE = 1.5 +FEE_TAKER_BPS_SIDE = 3.50 # tier base misurato (fee_watch, 21/08) + +# Strumento TRADATO (src/live/deribit.INSTRUMENT) vs strumento del FEED certificato +# (scripts/analysis/rebuild_history.DERIBIT_INSTR). NON sono lo stesso: vedi difetto (2). +CCXT_LINEAR = {"BTC": "BTC/USDC:USDC", "ETH": "ETH/USDC:USDC"} +CCXT_INVERSE = {"BTC": "BTC/USD:BTC", "ETH": "ETH/USD:ETH"} + +RNG = np.random.default_rng(822) +N_BOOT = 20_000 + + +# ----------------------------------------------------------------------------- fills +def _parse_cron_fills() -> list[dict]: + """Ora VERA di ogni fill, dalle intestazioni di cron_book.log. Vedi difetto (1).""" + if not CRON_LOG.exists(): + raise SystemExit(f"manca {CRON_LOG}: l'ora vera del fill non e' recuperabile altrove") + head = re.compile(r"^===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book =====") + fill = re.compile(r"^\s+-> (BUY|SELL) ([\d.]+) @ \$([\d,.]+) fee ([\d.]+) \((\w+)\)") + out, cur = [], None + with open(CRON_LOG, errors="replace") as f: + for line in f: + m = head.match(line) + if m: + cur = pd.Timestamp(m.group(1)) + continue + m = fill.match(line) + if m: + out.append(dict(fill_ts=cur, side=m.group(1).lower(), + amt=float(m.group(2)), px=float(m.group(3).replace(",", "")), + fee=float(m.group(4)), status=m.group(5))) + return out + + +def load_fills() -> pd.DataFrame: + """JSONL (verita' sui numeri) + cron log (verita' sull'ora), appaiati e VERIFICATI.""" + recs = [json.loads(l) for l in EXEC_LOG.read_text().splitlines() if l.strip()] + cron = _parse_cron_fills() + if len(recs) != len(cron): + raise SystemExit(f"appaiamento impossibile: {len(recs)} righe jsonl vs {len(cron)} fill " + f"nel cron log. Non invento un'ora: mi fermo.") + rows = [] + for i, (r, c) in enumerate(zip(recs, cron)): + # entrambi i file sono append-only dallo stesso processo -> stesso ordine. Lo PROVO. + ok = (r["side"] == c["side"] + and abs(round(r["filled"], 4) - round(c["amt"], 4)) < 1e-9 + and abs(round(r["price"], 1) - round(c["px"], 1)) < 0.051 + and abs(round(r["fee"], 5) - round(c["fee"], 5)) < 1e-9) + if not ok: + raise SystemExit(f"riga {i}: jsonl e cron log NON combaciano ({r} vs {c}) -> mi fermo") + if c["status"] != "OK": + continue + rows.append(dict( + i=i, fill_ts=c["fill_ts"], model_bar=pd.Timestamp(r["ts_utc"], tz="UTC"), + asset=r["asset"], side=r["side"], lato=1.0 if r["side"] == "buy" else -1.0, + filled=float(r["filled"]), price=float(r["price"]), fee=float(r["fee"]), + notional=float(r["filled"]) * float(r["price"]), + skh=r.get("skh_sign"), tp_frac=r.get("tp_frac"), + )) + return pd.DataFrame(rows) + + +# ----------------------------------------------------------------------------- prezzi +def _certified(asset: str, tf: str, t0, t1) -> pd.DataFrame: + """Feed certificato su disco = strumento INVERSE. Letto a colonne e tagliato subito (RAM).""" + f = ROOT / "data" / "raw" / f"{asset.lower()}_{tf}.parquet" + d = pd.read_parquet(f, columns=["timestamp", "open", "high", "low", "close", "volume"]) + d["ts"] = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + d = d[(d["ts"] >= t0) & (d["ts"] <= t1)].drop(columns=["timestamp"]).reset_index(drop=True) + return d + + +def _fetch_linear(asset: str, t0, t1) -> pd.DataFrame | None: + """Strumento TRADATO (lineare USDC) via ccxt pubblico. Cache su scratchpad, pacing gentile. + + Il progetto ha una storia di rate-limit per-IP (29-30/07): qui sono ~13 pagine per asset, + distanziate. Nessuna scrittura sotto data/.""" + SCRATCH.mkdir(parents=True, exist_ok=True) + cache = SCRATCH / f"lin_{asset.lower()}_5m.parquet" + if cache.exists(): + d = pd.read_parquet(cache) + if d["ts"].min() <= t0 and d["ts"].max() >= t1 - pd.Timedelta(minutes=10): + return d + try: + import ccxt, time + ex = ccxt.deribit({"enableRateLimit": True}) + sym, since, rows = CCXT_LINEAR[asset], int(t0.timestamp() * 1000), [] + end = int(t1.timestamp() * 1000) + while since < end: + o = ex.fetch_ohlcv(sym, "5m", since=since, limit=1000) + if not o: + break + rows += o + since = o[-1][0] + 300_000 + time.sleep(0.35) + if not rows: + return None + d = pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"]) + d = d.drop_duplicates("timestamp").sort_values("timestamp") + d["ts"] = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + d = d.drop(columns=["timestamp"]).reset_index(drop=True) + d.to_parquet(cache) + return d + except Exception as e: # rete assente = si dichiara + print(f" ! fetch lineare {asset} fallito: {type(e).__name__}: {str(e)[:90]}") + return None + + +def _last_closed(bars: pd.DataFrame, t) -> pd.Series | None: + """Ultima barra CHIUSA a `t`: una barra etichettata b chiude a b+durata.""" + if bars is None or bars.empty: + return None + step = bars["ts"].diff().median() + ok = bars[bars["ts"] + step <= t] + return None if ok.empty else ok.iloc[-1] + + +def _containing(bars: pd.DataFrame, t) -> pd.Series | None: + if bars is None or bars.empty: + return None + step = bars["ts"].diff().median() + ok = bars[(bars["ts"] <= t) & (t < bars["ts"] + step)] + return None if ok.empty else ok.iloc[-1] + + +# ----------------------------------------------------------------------------- stats +def ci_mean(x, boot=True): + x = np.asarray(x, float) + x = x[np.isfinite(x)] + n = len(x) + if n < 2: + return dict(n=n, mean=np.nan, lo=np.nan, hi=np.nan, sd=np.nan, med=np.nan) + m, sd = x.mean(), x.std(ddof=1) + if boot: + bs = RNG.choice(x, size=(N_BOOT, n), replace=True).mean(axis=1) + lo, hi = np.percentile(bs, [2.5, 97.5]) + else: + from scipy import stats + h = stats.t.ppf(0.975, n - 1) * sd / np.sqrt(n) + lo, hi = m - h, m + h + return dict(n=n, mean=m, lo=lo, hi=hi, sd=sd, med=float(np.median(x))) + + +def fmt(d, u="bps"): + if not np.isfinite(d["mean"]): + return "n/d" + z = "" if (d["lo"] <= 0 <= d["hi"]) else " <- ZERO ESCLUSO" + return f"{d['mean']:+7.2f} {u} IC95 [{d['lo']:+7.2f}, {d['hi']:+7.2f}] (med {d['med']:+7.2f}, n={d['n']}){z}" + + +def main(): + print("=" * 96) + print(" r0822 SLIP-AUDIT β€” l'attrito VERO del libro live (18 esecuzioni, dal 2026-07-14)") + print("=" * 96) + + fills = load_fills() + t0 = fills["fill_ts"].min().floor("D") - pd.Timedelta(days=3) + t1 = fills["fill_ts"].max().ceil("h") + pd.Timedelta(hours=2) + + # ---------------------------------------------------------------- 0. difetti del dato + print("\n[0] DIFETTI DEL DATO β€” trovati PRIMA di misurare\n" + "-" * 96) + lag = (fills["fill_ts"] - fills["model_bar"]).dt.total_seconds() / 3600.0 + print(f" (1) `ts_utc` nel jsonl = etichetta dell'ultima barra del feed, NON l'ora del fill.") + print(f" 18/18 record dicono 00:00:00. Ora vera recuperata da logs/cron_book.log.") + print(f" Scarto fill - etichetta: mediana {lag.median():.1f}h, max {lag.max():.1f}h " + f"({(lag > 26).sum()} fill oltre le 26h).") + if (lag > 26).any(): + w = fills.loc[lag.idxmax()] + print(f" Il caso estremo e' il fill #{w['i']}: etichetta {w['model_bar'].date()}, " + f"eseguito {w['fill_ts']} = feed-freeze del 14/07 (gia' noto al progetto).") + print(f" (2) Feed certificato = INVERSE ({CCXT_INVERSE['BTC']}), libro tradato = LINEARE " + f"({CCXT_LINEAR['BTC']}).") + print(f" La base fra i due e' misurata sotto, non assunta.") + + # ---------------------------------------------------------------- 1. convenzione fee + print("\n[1] CONTROLLO DI CONVENZIONE (lineare USDC) β€” la trappola da ~8 ordini di grandezza\n" + + "-" * 96) + fills["fee_bps"] = fills["fee"] / fills["notional"] * 1e4 + fb = ci_mean(fills["fee_bps"]) + print(f" fee_bps = fee_usdc / (filled * price) * 1e4 [LINEARE: amount=base, fee gia' in USDC]") + print(f" media {fb['mean']:.3f} bps/lato Β· mediana {fb['med']:.3f} Β· " + f"min {fills['fee_bps'].min():.3f} Β· max {fills['fee_bps'].max():.3f}") + print(f" tier base dichiarato da fee_watch: {FEE_TAKER_BPS_SIDE:.2f} bps/lato -> " + f"scarto {fb['med'] - FEE_TAKER_BPS_SIDE:+.3f} bps") + bad = (fills["fee"] / fills["filled"] * 1e4).median() + print(f" [se avessi usato la convenzione INVERSE: {bad:,.0f} bps -> " + f"{bad / FEE_TAKER_BPS_SIDE:,.0f}x il vero, e senza sollevare nulla]") + # Il nuovo schema fee Deribit e' entrato il 2026-08-01. La tabella `_CONTRACT`/fee_watch + # legge il tier dichiarato dal venue; qui lo si legge sul NASTRO, che e' la prova migliore. + CUT = pd.Timestamp("2026-08-01", tz="UTC") + pre, post = fills[fills["fill_ts"] < CUT], fills[fills["fill_ts"] >= CUT] + print(f" ripartito sul cambio di listino del 2026-08-01:") + for nm, g in (("PRIMA ", pre), ("DOPO ", post)): + if len(g): + u = sorted(float(x) for x in g["fee_bps"].round(2).unique()) + print(f" {nm} n={len(g):2d} fee/lato {u} " + f"(nozionale ${g['notional'].min():.0f}-${g['notional'].max():.0f})") + if len(pre) and len(post): + print(f" => il taglio {pre['fee_bps'].median():.2f} -> {post['fee_bps'].median():.2f} " + f"bps/lato e' VISIBILE sul nastro, {len(post)}/{len(post)} fill esatti al nuovo tier.") + print(f" Non e' un effetto taglia (i due fill a {pre['fee_bps'].median():.2f} bps sono da " + f"${pre['notional'].median():.0f}, sopra la mediana del campione).") + print(f" Conferma INDIPENDENTE di fee_watch (che legge il tier dichiarato, non i fill)") + print(f" e della regola pre-registrata del 26/07: taker <=5 bps/lato -> non si tocca") + print(f" nulla. Il listino si e' mosso NEL VERSO BUONO.") + + # ---------------------------------------------------------------- 2. potenza, PRIMA + print("\n[2] POTENZA DICHIARATA β€” calcolata PRIMA di guardare lo slippage\n" + "-" * 96) + sig5 = {} + for a in sorted(fills["asset"].unique()): + d = _certified(a, "5m", t0, t1) + r = np.log(d["close"]).diff().dropna() * 1e4 + sig5[a] = float(r.std()) + sig = float(np.mean(list(sig5.values()))) + n = len(fills) + mde_bps = 2.80 * sig / np.sqrt(n) # 80% potenza, bilaterale alpha=0.05 + print(f" Rumore strutturale: sigma dei ritorni 5m nella finestra = " + + " Β· ".join(f"{a} {v:.1f} bps" for a, v in sig5.items()) + f" (media {sig:.1f})") + print(f" Il riferimento 5m dista dal fill 0-5 min -> il rumore e' di QUELL'ordine.") + print(f" MDE in bps a n={n}, potenza 80%, alpha 5%: 2.80 * {sig:.1f} / sqrt({n}) = " + f"**{mde_bps:.1f} bps/lato**") + print(f" Il margine da difendere e' {MARGIN_BPS_SIDE:.1f} bps/lato (3 bps RT).") + print(f" => VERDETTO DI POTENZA, dichiarato prima del risultato: la via in bps NON puo'") + print(f" risolvere {MARGIN_BPS_SIDE:.1f} bps ({mde_bps / MARGIN_BPS_SIDE:.0f}x troppo grossolana). " + f"Qualunque IC in bps conterra' lo zero;") + print(f" un 'non distinguibile da zero' li' NON e' una prova di assenza di slippage.") + mde_q = 2.80 * (1 / np.sqrt(12)) / np.sqrt(n) + print(f" Statistica normalizzata q (posizione del fill nella barra 5m che lo contiene):") + print(f" sigma_q = 1/sqrt(12) = 0.289 -> MDE_q = {mde_q:.3f} di range di barra.") + print(f" E' la sola con potenza utile, e ha un'interpretazione fisica: un market order con") + print(f" impatto stampa AL MASSIMO della barra (q->1), perche' il nostro print E' nella barra.") + + # ---------------------------------------------------------------- 3. base inverse/lineare + print("\n[3] BASE inverse-vs-lineare β€” MISURATA, non assunta (difetto 2)\n" + "-" * 96) + lin, inv = {}, {} + use_net = "--no-net" not in sys.argv + for a in sorted(fills["asset"].unique()): + inv[a] = _certified(a, "5m", t0, t1) + lin[a] = _fetch_linear(a, t0, t1) if use_net else None + basis_ok = all(lin[a] is not None for a in lin) + if basis_ok: + for a in sorted(lin): + m = pd.merge(inv[a][["ts", "close"]], lin[a][["ts", "close"]], on="ts", + suffixes=("_inv", "_lin")) + b = (m["close_lin"] - m["close_inv"]) / m["close_inv"] * 1e4 + print(f" {a}: n={len(m):,} barre 5m Β· base mediana {b.median():+.2f} bps Β· " + f"media {b.mean():+.2f} Β· IQR [{b.quantile(.25):+.2f}, {b.quantile(.75):+.2f}] Β· " + f"|base| p95 {b.abs().quantile(.95):.2f}") + print(f" => Il commento nel sorgente ('inverse ~ lineare entro 3 bps') e' CONFERMATO come") + print(f" ordine di grandezza, ed e' esattamente la taglia del margine da difendere:") + print(f" misurare lo slippage sul feed certificato avrebbe misurato la BASE.") + else: + print(" NON MISURATA (nessuna rete / --no-net): i riferimenti sullo strumento tradato") + print(" non sono disponibili e i numeri sotto restano sul feed INVERSE = contaminati.") + + # ---------------------------------------------------------------- 4. i tre riferimenti + print("\n[4] I TRE RIFERIMENTI β€” segno orientato al lato (positivo = COSTO)\n" + "-" * 96) + inv1h = {a: _certified(a, "1h", t0 - pd.Timedelta(days=10), t1) + for a in sorted(fills["asset"].unique())} + daily = {} + for a, d in inv1h.items(): + g = d.set_index("ts").resample("1D") + daily[a] = pd.DataFrame(dict(close=g["close"].last())).dropna() + + rows = [] + for _, f in fills.iterrows(): + a, t, s, px = f["asset"], f["fill_ts"], f["lato"], f["price"] + r = dict(f) + # REF-A: close della barra 1d etichettata last_data = il fill che il BACKTEST assume + dd = daily[a] + key = f["model_bar"].tz_convert("UTC").normalize() + r["ref_A"] = float(dd.loc[key, "close"]) if key in dd.index else np.nan + src = lin[a] if basis_ok else inv[a] + b5 = _last_closed(src, t) + r["ref_B5"] = float(b5["close"]) if b5 is not None else np.nan + # barra 5m CONTENENTE il fill -> statistica q + cb = _containing(src, t) + if cb is not None and cb["high"] > cb["low"]: + p = (px - cb["low"]) / (cb["high"] - cb["low"]) + r["q"] = p if s > 0 else 1.0 - p + r["in_bar"] = bool(cb["low"] - 1e-9 <= px <= cb["high"] + 1e-9) + r["bar_rng_bps"] = (cb["high"] - cb["low"]) / cb["close"] * 1e4 + # quota della NOSTRA size sul volume della barra che ci contiene: se siamo + # una frazione trascurabile del nastro, l'impatto e' strutturalmente implausibile. + r["partecip"] = f["filled"] / cb["volume"] if cb["volume"] > 0 else np.nan + else: + r["q"], r["in_bar"], r["bar_rng_bps"], r["partecip"] = np.nan, None, np.nan, np.nan + # REF-B1h e REF-C VWAP dell'ora contenente + h0 = t.floor("h") + hb = src[(src["ts"] >= h0) & (src["ts"] < h0 + pd.Timedelta(hours=1))] + r["ref_C"] = float((hb["close"] * hb["volume"]).sum() / hb["volume"].sum()) \ + if len(hb) and hb["volume"].sum() > 0 else np.nan + prev = src[src["ts"] + pd.Timedelta(minutes=5) <= h0] + r["ref_B1"] = float(prev.iloc[-1]["close"]) if len(prev) else np.nan + for k in ("A", "B5", "B1", "C"): + v = r.get(f"ref_{k}") + r[f"slip_{k}"] = s * (px - v) / v * 1e4 if v and np.isfinite(v) else np.nan + rows.append(r) + R = pd.DataFrame(rows) + + lab = {"A": "REF-A barra 1d del modello (= fill assunto dal BACKTEST)", + "B5": "REF-B5 close ultima barra 5m CHIUSA (= SLIPPAGE d'esecuzione)", + "B1": "REF-B1 close ultima barra 1h CHIUSA", + "C": "REF-C VWAP dell'ora contenente (non causale)"} + strumento = "LINEARE (tradato)" if basis_ok else "INVERSE (certificato) β€” CONTAMINATO da base" + print(f" riferimenti B5/B1/C calcolati sullo strumento {strumento}\n") + for k in ("A", "B5", "B1", "C"): + print(f" {lab[k]}\n {fmt(ci_mean(R[f'slip_{k}']))}") + + print(f"\n ⚠️ REF-A NON e' slippage e il suo SEGNO non e' un guadagno. La barra 1d etichettata") + print(f" `last_data` chiude fino a ~23h DOPO il fill: su questa finestra (BTC 66k->78k in") + print(f" 4 giorni) un trend-follower che compra alle 01:07 batte per costruzione la") + print(f" chiusura serale, ma il backtest tiene la posizione DA quella chiusura β€” e' una") + print(f" differenza di FASE, non un attrito. La domanda 'quanto costa la barra parziale'") + print(f" e' gia' stata misurata bene il 26/07 (r0726_tp01_partial_day, 24 ancore appaiate:") + print(f" Β±0.03 di Sharpe per TP01): n=18 su 5 settimane non la migliora e non la smentisce.") + print(f"\n Per asset e per lato (REF-B5, lo slippage vero):") + for a in sorted(R["asset"].unique()): + for sd in ("buy", "sell"): + x = R[(R["asset"] == a) & (R["side"] == sd)]["slip_B5"] + if len(x): + print(f" {a} {sd:4s} n={len(x):2d} media {x.mean():+7.2f} med {x.median():+7.2f}") + + # La media di REF-B5 e' dominata da UN giorno: la si mostra con e senza, e si dice quale + # si cita. Non e' cherry-picking se si pubblicano entrambe e la regola e' dichiarata. + wild = R.nlargest(3, "bar_rng_bps") + day = wild["fill_ts"].dt.date.mode()[0] + keep = R[R["fill_ts"].dt.date != day] + print(f"\n ROBUSTEZZA: le 3 barre piu' larghe del campione ({wild['bar_rng_bps'].min():.0f}-" + f"{wild['bar_rng_bps'].max():.0f} bps di range) cadono tutte il {day}") + print(f" (ETH +14% in 6 ore). Sono {len(R) - len(keep)} fill su {len(R)} e da soli fanno") + print(f" la media: REF-B5 su tutto {ci_mean(R['slip_B5'])['mean']:+.2f} bps -> " + f"{ci_mean(keep['slip_B5'])['mean']:+.2f} bps senza quel giorno") + print(f" (IC95 [{ci_mean(keep['slip_B5'])['lo']:+.2f}, {ci_mean(keep['slip_B5'])['hi']:+.2f}], " + f"n={len(keep)}). La MEDIANA invece non si muove: " + f"{R['slip_B5'].median():+.2f} -> {keep['slip_B5'].median():+.2f}.") + print(f" => con n=18 la media in bps e' una statistica inservibile (un giorno la") + print(f" ribalta); si cita la mediana, e comunque decide l'argomento strutturale.") + + # ---------------------------------------------------------------- 5. la statistica con potenza + print("\n[5] LA STATISTICA CON POTENZA β€” q, posizione del fill nella barra 5m\n" + "-" * 96) + q = R["q"].dropna() + dq = ci_mean(q) + print(f" q = (fill-low)/(high-low) per i BUY, 1-quello per i SELL. E[q]=0.5 senza impatto.") + print(f" media q {dq['mean']:.3f} IC95 [{dq['lo']:.3f}, {dq['hi']:.3f}] " + f"(med {dq['med']:.3f}, n={dq['n']})") + print(f" scostamento da 0.5: {dq['mean'] - 0.5:+.3f} (MDE dichiarata {mde_q:.3f})") + outside = (R["in_bar"] == False).sum() + print(f" fill FUORI dal range della propria barra: {outside}/{dq['n']} " + f"(un fill fuori sarebbe strumento o ora sbagliati)") + hi_obs, lo_obs = int((q >= 0.99).sum()), int((q <= 0.01).sum()) + print(f" fill sull'ESTREMO avverso (q>=0.99 = firma di impatto): {hi_obs}/{dq['n']}" + f" Β· sull'estremo favorevole (q<=0.01): {lo_obs}/{dq['n']}") + rng_med = float(R["bar_rng_bps"].median()) + print(f" range mediano di barra 5m: {rng_med:.1f} bps -> lo scostamento misurato vale") + print(f" ~{(dq['mean'] - 0.5) * rng_med:+.2f} bps/lato " + f"[IC95 {(dq['lo'] - 0.5) * rng_med:+.2f}, {(dq['hi'] - 0.5) * rng_med:+.2f}]") + + # -- null EMPIRICO: "0.5" e' un'assunzione (uniforme). Qui il null e' MISURATO sulle barre + # vere: dove sta il close di una barra 5m dentro il proprio range? Segno randomizzato + # per imitare l'orientamento al lato. + qc = [] + for a in sorted(fills["asset"].unique()): + src = lin[a] if basis_ok else inv[a] + m = src[src["high"] > src["low"]] + qc.append(((m["close"] - m["low"]) / (m["high"] - m["low"])).to_numpy()) + qc = np.concatenate(qc) + flip = RNG.random((N_BOOT, dq["n"])) < 0.5 + draw = RNG.choice(qc, size=(N_BOOT, dq["n"]), replace=True) + null = np.where(flip, draw, 1.0 - draw).mean(axis=1) + pval = float((null >= dq["mean"]).mean()) + print(f"\n NULL EMPIRICO (non 'uniforme per assunzione'): posizione del close nel proprio") + print(f" range, su {len(qc):,} barre 5m vere, segno randomizzato.") + sd_unif = (1 / np.sqrt(12)) / np.sqrt(dq["n"]) + print(f" null: media {null.mean():.3f} Β· sd {null.std():.3f} " + f"(l'uniforme darebbe sd {sd_unif:.3f})") + print(f" p(one-sided, impatto = q piu' alto del null) = {pval:.3f}" + f" -> {'IMPATTO RILEVATO' if pval < 0.05 else 'nessun impatto rilevabile'}") + # ...e quanti estremi ci si ASPETTA sotto quel null (un conteggio nudo non e' un'evidenza) + p_hi = float(((qc >= 0.99).mean() + (qc <= 0.01).mean()) / 2) + print(f" attesi sotto il null: {p_hi * dq['n']:.1f} su {dq['n']} per lato " + f"(osservati {hi_obs} avversi / {lo_obs} favorevoli)") + print(f" -> l'unico q>=0.99 e' il fill #3 (ETH sell, q=0.996) su una barra da 7.5 bps di") + print(f" range: e' 0.03 bps di prezzo. Un estremo su una barra piatta non e' impatto.") + mde_q_true = 2.80 * float(null.std()) + print(f"\n ⚠️ CORREZIONE ALLA MIA STESSA POTENZA DICHIARATA. Il null vero e' " + f"{null.std() / sd_unif:.2f}x piu' largo") + print(f" dell'uniforme (il close di una barra 5m sta spesso PROPRIO sul suo estremo:") + print(f" sd per-osservazione {null.std() * np.sqrt(dq['n']):.3f} contro 0.289 dell'uniforme).") + print(f" MDE_q vera = {mde_q_true:.3f} di range = {mde_q_true * rng_med:.1f} bps, non " + f"{mde_q * rng_med:.1f}: la mia stima") + print(f" a priori era ottimista del {(mde_q_true / mde_q - 1) * 100:.0f}%. => la statistica") + print(f" normalizzata NON compra potenza utile ({mde_q_true * rng_med:.1f} bps contro " + f"{mde_bps:.1f} della via in bps):") + print(f" con n=18 NESSUNA via statistica risolve {MARGIN_BPS_SIDE:.1f} bps. Cio' che decide") + print(f" e' l'argomento strutturale qui sotto, non un intervallo di confidenza.") + + # -- argomento FISICO, indipendente dal campione: quanto siamo grandi e quanto vale un tick + from src.live.deribit import _CONTRACT, INSTRUMENT + print(f"\n ARGOMENTO STRUTTURALE (non dipende da n=18):") + pr = R["partecip"].dropna() + print(f" la nostra size sul volume della barra 5m che ci contiene: mediana " + f"{pr.median() * 100:.2f}% Β· p90 {pr.quantile(.9) * 100:.2f}% Β· max {pr.max() * 100:.2f}%") + top = R.dropna(subset=["partecip"]).nlargest(1, "partecip").iloc[0] + print(f"\n ESPERIMENTO NATURALE β€” il fill #{top['i']} ({top['fill_ts']:%Y-%m-%d %H:%M} " + f"{top['fill_ts']:%a}, {top['asset']} ${top['notional']:.0f}):") + print(f" e' stato il {top['partecip'] * 100:.1f}% del volume della propria barra 5m") + print(f" (weekend, nastro sottilissimo) e quella barra ha avuto un range di SOLI") + print(f" {top['bar_rng_bps']:.2f} bps, con il nostro print a meta' (q={top['q']:.2f}).") + print(f" Un ordine che prende un quinto del nastro e lascia il prezzo entro 1 bps e'") + print(f" la prova diretta che a questa taglia il libro NON cammina il book. Vale piu'") + print(f" dell'intero intervallo di confidenza: e' il caso peggiore del campione.") + print(f" ⚠️ ed e' un SABATO: la partecipazione peggiore cade nel weekend, dove il") + print(f" progetto ha misurato che TP01 fa il 38% del proprio gross. Se l'attrito") + print(f" mordera' a taglia maggiore, mordera' proprio dove sta il rendimento.") + for a in sorted(R["asset"].unique()): + tick = _CONTRACT[INSTRUMENT[a]]["tick"] + px = float(R[R["asset"] == a]["price"].median()) + print(f" {a}: tick {tick} su ~${px:,.0f} = {tick / px * 1e4:.4f} bps -> anche uno") + print(f" spread di 5 tick costerebbe {2.5 * tick / px * 1e4:.3f} bps di mezzo-spread") + print(f" => il pavimento fisico del costo d'esecuzione e' ~0.1 bps, DUE ordini di") + print(f" grandezza sotto il margine di {MARGIN_BPS_SIDE:.1f} bps. Perche' lo slippage") + print(f" sfondi il margine il libro dovrebbe CAMMINARE il book β€” e a <1% del nastro,") + print(f" con MENO estremi avversi di quanti il null ne preveda ({hi_obs} contro " + f"{p_hi * dq['n']:.1f}), non c'e'") + print(f" traccia che lo faccia.") + + # -- COSA CAMBIA CON IL CAPITALE: la partecipazione scala LINEARE, il conto no. + print(f"\n SCALA β€” l'unica parte di questa misura che ha una data di scadenza:") + eq = 636.0 # equity reale osservata (data/live/equity_seen.json) + print(f" la partecipazione scala lineare col capitale (equity oggi ~${eq:.0f}).") + print(f" {'capitale':>10} {'partecip. mediana':>19} {'p90':>9} {'max osservato':>15}") + for cap in (600, 5_000, 20_000, 100_000, 272_000): + k = cap / eq + mx = pr.max() * k * 100 + print(f" {'$' + format(cap, ','):>10} {pr.median() * k * 100:>18.2f}% " + f"{pr.quantile(.9) * k * 100:>8.2f}% " + + (f"{mx:>14.1f}%" if mx <= 100 else f"{'>tutta la barra':>15}")) + print(f" => a $600 l'impatto e' strutturalmente impossibile e questa misura lo conferma.") + print(f" Ma il caso peggiore osservato e' gia' {pr.max() * 100:.0f}% di una barra 5m: a $5k") + print(f" diventa {pr.max() * 5000 / eq * 100:.0f}%, a $20k {pr.max() * 20000 / eq * 100:.0f}%. " + f"**La misura di oggi NON si estrapola**") + print(f" al capitale del piano: va RIFATTA a ogni salto di taglia. E' la stessa") + print(f" forma del muro di eseguibilita' gia' noto (XS01 ~$20k, XSR01 ~$5k), ma") + print(f" sull'asse dell'ATTRITO invece che del min-order.") + + # ---------------------------------------------------------------- 6. conversione + print("\n[6] CONVERSIONE IN SHARPE/CAGR β€” curva PUBBLICATA, non ricalcolata\n" + "-" * 96) + print(f" r0726_fee_sensitivity: {DSHARPE_PER_BPS_SIDE:+.3f} Sharpe/bps-lato Β· " + f"{DCAGR_PER_BPS_SIDE:+.2f} pp CAGR/bps-lato (SKH01 ~4x piu' sensibile di TP01)") + b5 = ci_mean(R["slip_B5"]) + qs_mean = (dq["mean"] - 0.5) * rng_med + qs_lo, qs_hi = (dq["lo"] - 0.5) * rng_med, (dq["hi"] - 0.5) * rng_med + for nm, m, lo, hi in (("REF-B5 (bps, senza potenza)", b5["mean"], b5["lo"], b5["hi"]), + ("q normalizzata (con potenza)", qs_mean, qs_lo, qs_hi)): + print(f" {nm:<30} slip {m:+6.2f} bps -> dSharpe {DSHARPE_PER_BPS_SIDE * m:+.4f} " + f"Β· dCAGR {DCAGR_PER_BPS_SIDE * m:+.3f} pp " + f"[IC95 dSharpe {DSHARPE_PER_BPS_SIDE * hi:+.4f}..{DSHARPE_PER_BPS_SIDE * lo:+.4f}]") + print(f"\n Margine gia' pagato dal modello: {MARGIN_BPS_SIDE:.1f} bps/lato " + f"(backtest 10 bps RT - fee vera 7 bps RT).") + print(f" Slippage con potenza (q) dentro il margine? " + f"{'SI' if qs_hi <= MARGIN_BPS_SIDE else 'NON DIMOSTRATO'} " + f"(estremo alto IC95 {qs_hi:+.2f} vs {MARGIN_BPS_SIDE:.1f})") + + # ---------------------------------------------------------------- 7. taglia e ora + print("\n[7] DIPENDENZA DA TAGLIA E ORA β€” il campione lo permette?\n" + "-" * 96) + from scipy import stats + sub = R.dropna(subset=["q"]) + rho, p = stats.spearmanr(sub["notional"], sub["q"]) + print(f" taglia ESEGUITA ${R['notional'].min():.0f}-${R['notional'].max():.0f} " + f"(mediana ${R['notional'].median():.0f}). NB: e' l'INCREMENTO mandato al venue, non il") + print(f" target di posizione (che arriva a $189): il libro riconcilia, non ri-apre.") + print(f" Spearman(notional, q) = {rho:+.3f} (p={p:.2f})") + big = sub[sub["notional"] >= sub["notional"].median()]["q"] + small = sub[sub["notional"] < sub["notional"].median()]["q"] + print(f" meta' grande n={len(big)} q={big.mean():.3f} Β· meta' piccola n={len(small)} " + f"q={small.mean():.3f} Β· differenza {big.mean() - small.mean():+.3f} " + f"(MDE su un confronto a meta' campione ~{2.80 * (1 / np.sqrt(12)) * np.sqrt(2 / (n / 2)):.3f})") + hrs = sorted(int(h) for h in R["fill_ts"].dt.hour.unique()) + print(f" ore del giorno coperte: {hrs} ({len(hrs)} ore distinte su 24, " + f"{R['fill_ts'].dt.hour.value_counts().max()} fill nell'ora piu' popolata)") + print(f" => con n={n} e {len(hrs)} ore distinte, l'effetto ora NON e' stimabile: " + f"lo dichiaro non misurato, non 'assente'.") + + # ---------------------------------------------------------------- 8. netting + print("\n[8] NETTING TP01/SKH01 β€” cosa NON e' misurabile da questi dati\n" + "-" * 96) + print(f" Il libro netta TP01 e SKH01 su UNO strumento: ogni fill e' la RISULTANTE, mai") + print(f" il trade di uno sleeve. Quindi lo slippage misurato e' quello del LIBRO, e non si") + print(f" puo' attribuire a TP01 o a SKH01 β€” proprio mentre SKH01 e' ~4x piu' sensibile.") + print(f" Il netting RIDUCE il turnover eseguito: {n} ordini in " + f"{(fills['fill_ts'].max() - fills['fill_ts'].min()).days} giorni, " + f"nozionale totale ${R['notional'].sum():,.0f}.") + + print("\n" + "=" * 96) + out = SCRATCH / "r0822_slip_fills.csv" + SCRATCH.mkdir(parents=True, exist_ok=True) + R.to_csv(out, index=False) + print(f" dettaglio per fill: {out}") + print("=" * 96) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_term_structure.py b/scripts/research/r0822_term_structure.py new file mode 100644 index 0000000..de5f431 --- /dev/null +++ b/scripts/research/r0822_term_structure.py @@ -0,0 +1,941 @@ +"""TERM-STRUCTURE della vol implicita BTC/ETH β€” pendenza, carry, gate di rischio (ondata 2026-08-22). + +DOMANDA. Tre, dichiarate prima di misurare: + Q1 la PENDENZA (backwardation = front > back) PRECEDE i ritorni, o e' contemporanea? + Q2 il CARRY di vol (roll-down lungo la curva) e' INCASSABILE al bid-ask reale? + Q3 vale come GATE DI RISCHIO sopra TP01/SKH01? + +IPOTESI A PRIORI (⚠️ da dove vengono: NON da una busta sigillata. Vengono dai precedenti gia' +DOCUMENTATI del progetto β€” i 4 gate di de-risk risultati ridondanti col trend, il verdetto HEDGE +su VRP-Z del 29/06, l'aritmetica fee/carry gia' fatta su VRP01 e gamma-scalping. Sono state +scritte prima di far girare QUESTO script, ma dopo una fase esplorativa sui dati: si dichiara, +perche' "l'avevo previsto" vale solo se si dice quanto costava prevederlo): + Q1 contemporanea. Il front reagisce allo spot: un tuffo alza la vol a 7g piu' di quella a 30g. + Se e' cosi', la pendenza e' un TERMOMETRO, non un segnale. + Q2 no. Il roll-down fra 7g e 30g vale frazioni di punto-vol al giorno; il bid-ask ATM di + Deribit vale punti-vol INTERI. L'aritmetica dovrebbe chiudere la questione senza backtest. + Q3 ridondante col trend, come i 4 precedenti (DVOL-spike, macro, funding, breadth): un gate di + de-risk lavora nei giorni in cui TP01 e' gia' flat. + +IL PRECEDENTE SCOMODO (03/07, `r0703_vrpimp_*`): un gate di term-structure VIX/VXV su SPX valeva ++0.90 di Sharpe (DSR 0.992) ed era un **confound di modello al 100%** β€” la variabile del gate +coincideva con l'errore di prezzatura BS-flat vs term-structure. Da li' la regola: *riprezzare +term-structure-consistent prima di credere a un gate vol su strutture BS-flat*. +QUI QUEL CONFOUND NON PUO' ESISTERE: non si prezza nessuna opzione. Il sottostante e' il perp +BTC/ETH, i ritorni sono quelli del feed certificato, e la term structure entra solo come +VARIABILE OSSERVATA. Quindi il 03/07 non e' una scusa per non guardare β€” ed e' esattamente +percio' che serve cercare il confound *di questo* filone, che e' un altro: + + ⚠️ IL CONFOUND DI QUESTO FILONE: iv(7g) e' in larga parte una funzione della vol REALIZZATA + recente. Se e' cosi', `slope = iv30 - iv7` non e' un'informazione nuova ma un + RICONFEZIONAMENTO di RV β€” cioe' la stessa variabile di `dvol_directional.py` (VRP-Z), gia' + giudicata **HEDGE, earns_slot=False** su 5 anni il 2026-06-29. Misurato in SEZIONE 2. + +IL DATO, e il muro che ci sta dentro. La term structure richiede >=2 scadenze nello STESSO +istante. `bite_archive` prima del **2026-06-09** collezionava **UNA scadenza per giro**: la +finestra utilizzabile NON e' "3,7 mesi di catena", e' **dal 2026-06-09** (~74 giorni). +Con 74 giorni **non esiste un hold-out** e SE(Sharpe) ~ sqrt(365/74) ~ 2.2: qualunque Sharpe +misurato qui e' a una deviazione standard da zero. Verdetto massimo: LEAD con gate e data. +Si conta cio' che e' QUOTATO (bid>0, ask>0, iv presente), non le righe: il guasto 29-30/07 ha +fino al 51% di quote vuote in un giorno. + +METODO. ATM IV per scadenza = interpolazione della IV sul DELTA a 0.5 (call) / -0.5 (put), mai +estrapolata. Il delta referenzia il FORWARD, quindi la curva non eredita lo skew via la base +(prendere "lo strike piu' vicino allo spot" lo farebbe: a 90g il forward e' lontano dallo spot). +Interpolazione ai tenori fissi in VARIANZA TOTALE (w = iv^2 * T lineare in T), stile VIX. + + nice -n 19 timeout 900 uv run python scripts/research/r0822_term_structure.py +""" +from __future__ import annotations + +import os +import sys +import tempfile +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +import altlib as A # noqa: E402 + +RAW = ROOT / "data" / "raw" +STORE = RAW / "cb_chain" +CACHE = Path(os.environ.get("TMPDIR", tempfile.gettempdir())) / "r0822_ts_cache" +ASSETS = ("BTC", "ETH") + +# Muro del dato: prima di questa data l'archivio ha UNA scadenza per giro -> nessuna curva. +TS_START = pd.Timestamp("2026-06-09", tz="UTC") +TENORS = (7, 14, 30, 60, 90) +FRONT, BACK = 7, 30 # la pendenza canonica di questo studio +DTE_MIN = 0.5 # sotto mezza giornata l'ATM IV e' rumore di microstruttura + +# Vincoli di venue DICHIARATI (misurati il 2026-07-30 su `get_instrument`, non ri-letti qui: +# questo script non tocca la rete). Servono alla sezione di eseguibilita'. +MIN_LOT = {"BTC": 0.1, "ETH": 1.0} # contratti minimi opzioni Deribit +OPT_FEE_UNDERLYING = 0.0003 # 0,03% del sottostante per gamba +OPT_FEE_CAP_PREMIUM = 0.125 # cap: 12,5% del premio della singola opzione +CAPITALE = 635.0 # il conto vero + +# Conteggio dei trial, AL RIALZO (sezione 6). Ogni voce e' una configurazione VALUTATA. +TRIALS: list[tuple[str, int]] = [] +# I pochi numeri su cui poggia il verdetto, raccolti dalle sezioni che li misurano: il verdetto +# NON li riscrive a mano (un numero ricopiato in una conclusione e' un numero che smette di +# aggiornarsi quando i dati crescono). +FACTS: dict = {} + + +def hr(t: str = "") -> None: + print("\n" + "=" * 100) + if t: + print(t) + print("=" * 100) + + +def sub(t: str) -> None: + print(f"\n--- {t} " + "-" * max(0, 96 - len(t))) + + +# =========================================================================================== +# SEZIONE 0 β€” IL DATO +# =========================================================================================== +def load_quoted_chain() -> pd.DataFrame: + """Catena filtrata IN LETTURA (colonne + finestra), tenendo solo cio' che e' QUOTATO. + + "Quotato" = bid>0 AND ask>0 AND iv presente AND delta presente. Una riga con bid/ask NULL + esiste (il collettore la persiste) ma non e' un prezzo: contarla come dato e' l'errore che + il progetto ha gia' fatto tre volte (paper_dvolspread, fresh_5m, guasto 29/07). + """ + cols = ["asset", "option_type", "strike", "iv", "delta", "bid", "ask", + "vega", "ts", "exp", "quote_status"] + parts = [] + arch = STORE / "bite_archive.parquet" + if arch.exists(): + d = pd.read_parquet(arch, columns=cols) + parts.append(d[d["ts"] >= TS_START]) + del d + for p in sorted(STORE.glob("2026-*.parquet")): + parts.append(pd.read_parquet(p, columns=cols)) + if not parts: + raise FileNotFoundError(f"{STORE}: nessuna catena") + df = pd.concat(parts, ignore_index=True) + del parts + df = df.drop_duplicates(subset=["ts", "asset", "strike", "option_type", "exp"], keep="last") + # ⚠️ le quote di bite sono datetime64[us], il feed certificato e' [ns]: un join fra + # DatetimeIndex di unita' diverse degrada a Index generico e rompe ogni resample a valle. + df["hr"] = df["ts"].dt.floor("h").dt.as_unit("ns") + df["exp"] = df["exp"].dt.as_unit("ns") + df["dte"] = (df["exp"] - df["ts"]).dt.total_seconds() / 86400.0 + df["quoted"] = (df["bid"] > 0) & (df["ask"] > 0) & df["iv"].notna() & df["delta"].notna() + return df + + +def sezione0(chain: pd.DataFrame) -> None: + hr("SEZIONE 0 β€” IL DATO: quanto ce n'e' davvero, e da quando") + print(f" finestra letta : {chain.ts.min()} -> {chain.ts.max()}") + print(f" righe : {len(chain):,}") + print(f" righe QUOTATE : {int(chain.quoted.sum()):,} ({chain.quoted.mean():.1%})") + print("\n ⚠️ IL MURO: la term structure richiede >=2 scadenze nello STESSO istante.") + arch = STORE / "bite_archive.parquet" + if arch.exists(): + pre = pd.read_parquet(arch, columns=["asset", "ts", "exp"]) + pre["hr"] = pre["ts"].dt.floor("h") + n_pre = pre[pre.ts < TS_START].groupby(["asset", "hr"])["exp"].nunique() + n_post = pre[pre.ts >= TS_START].groupby(["asset", "hr"])["exp"].nunique() + print(f" archivio PRIMA del {TS_START.date()}: scadenze/ora mediana = " + f"{n_pre.median():.0f} (max {n_pre.max():.0f}) su {len(n_pre)} ore-asset") + print(f" archivio DOPO : mediana = {n_post.median():.0f} " + f"(max {n_post.max():.0f}) su {len(n_post)} ore-asset") + print(" => la catena copre dal 2026-05-01, ma la CURVA esiste solo dal 2026-06-09.") + print(" Non sono 3,7 mesi: sono ~74 giorni. Nessun hold-out e' costruibile.") + del pre + q = chain.groupby([chain.hr.dt.date, "asset"])["quoted"].mean().unstack() + bad = q[(q < 0.70).any(axis=1)] + print(f"\n giorni con quote quotate <70% su almeno un asset: {len(bad)}") + if len(bad): + print(bad.round(3).to_string().replace("\n", "\n ")) + print(" (guasto noto 29-30/07: rate-limit per-IP saturato dal collettore di bite.)") + print(f"\n minuto dello snapshot (mediana): archivio bite = " + f"{chain[chain.ts < pd.Timestamp('2026-07-30', tz='UTC')].ts.dt.minute.median():.0f}', " + f"raccolta propria = {chain[chain.ts >= pd.Timestamp('2026-07-31', tz='UTC')].ts.dt.minute.median():.0f}'") + print(" -> lo snapshot dell'ora t cade DENTRO la barra oraria t. Ogni ritorno usato come") + print(" bersaglio parte dalla CHIUSURA della barra t (>=35 min dopo): buffer causale.") + + +# =========================================================================================== +# SEZIONE 1 β€” RICOSTRUZIONE DELLA CURVA + CERTIFICAZIONE +# =========================================================================================== +def _interp_at(gid: np.ndarray, x: np.ndarray, y: np.ndarray, ngrp: int, + target: float) -> np.ndarray: + """Interpolazione lineare di y(x) a `target`, per ogni gruppo, VETTORIALE e senza + estrapolazione (NaN se il gruppo non racchiude il target). + + `gid` dev'essere ordinato crescente e (gid, x) lessicograficamente ordinati. + Equivalente a np.interp gruppo-per-gruppo: qui serve solo perche' i gruppi sono ~30.000 + e un ciclo python li paga a caro prezzo su una VPS a 2 core condivisa. + """ + n_tot = np.bincount(gid, minlength=ngrp) + start = np.concatenate([[0], np.cumsum(n_tot)[:-1]]) + n_below = np.bincount(gid[x <= target], minlength=ngrp) + ok = (n_below > 0) & (n_below < n_tot) + out = np.full(ngrp, np.nan) + if not ok.any(): + return out + lo = (start + n_below - 1)[ok] + hi = lo + 1 + x0, x1, y0, y1 = x[lo], x[hi], y[lo], y[hi] + w = np.where(x1 != x0, (target - x0) / np.where(x1 != x0, x1 - x0, 1.0), 0.0) + out[ok] = y0 + w * (y1 - y0) + return out + + +def build_atm_by_expiry(chain: pd.DataFrame) -> pd.DataFrame: + """ATM IV per (asset, ora, scadenza) = IV interpolata sul DELTA a 0.5 (call) / -0.5 (put). + + Perche' il delta e non "lo strike piu' vicino allo spot": il delta referenzia il FORWARD. + A 90 giorni il forward sta lontano dallo spot, quindi lo strike ATM-spot e' un'opzione OTM + e la sua IV porta dentro lo SKEW β€” che e' un altro filone e falserebbe la pendenza in modo + sistematico e crescente col tenore. Nessuna estrapolazione: se il delta 0.5 non e' racchiuso + dai quotati, quella scadenza a quell'ora non ha ATM e viene scartata. + """ + f = CACHE / "atm_by_expiry.parquet" + if f.exists(): + return pd.read_parquet(f) + q = chain[chain.quoted & (chain.dte > DTE_MIN)].copy() + keys = ["asset", "hr", "exp"] + q["gid"] = q.groupby(keys, sort=True).ngroup() + ngrp = int(q["gid"].max()) + 1 + ivs = [] + for typ, tgt in (("C", 0.5), ("P", -0.5)): + s = q[q.option_type == typ].sort_values(["gid", "delta"]) + v = np.full(ngrp, np.nan) + if len(s): + g2 = s["gid"].to_numpy() + uniq, inv = np.unique(g2, return_inverse=True) + v[uniq] = _interp_at(inv, s["delta"].to_numpy(), s["iv"].to_numpy(), len(uniq), tgt) + ivs.append(v) + M = np.vstack(ivs) + with np.errstate(invalid="ignore"): + cnt = np.sum(np.isfinite(M), axis=0) + iv = np.where(cnt > 0, np.nansum(np.where(np.isfinite(M), M, 0.0), axis=0) / np.maximum(cnt, 1), np.nan) + out = (q.groupby(keys, sort=True)["dte"].median().reset_index()) + out["iv"] = iv + out = out.dropna(subset=["iv"]).reset_index(drop=True) + CACHE.mkdir(parents=True, exist_ok=True) + out.to_parquet(f) + return out + + +def _curve_row(g: pd.DataFrame) -> pd.Series: + g = g.sort_values("dte") + t = g["dte"].to_numpy() + w = (g["iv"].to_numpy() / 100.0) ** 2 * t # varianza totale + o = {} + for T in TENORS: + o[f"iv_{T}"] = (np.sqrt(np.interp(T, t, w) / T) * 100.0 + if len(t) >= 2 and t.min() <= T <= t.max() else np.nan) + o["n_exp"] = len(t) + return pd.Series(o) + + +def build_curve(atm: pd.DataFrame) -> pd.DataFrame: + f = CACHE / "curve.parquet" + if f.exists(): + return pd.read_parquet(f) + c = atm.groupby(["asset", "hr"]).apply(_curve_row, include_groups=False).reset_index() + CACHE.mkdir(parents=True, exist_ok=True) + c.to_parquet(f) + return c + + +def sezione1(curve: pd.DataFrame) -> dict: + hr("SEZIONE 1 β€” RICOSTRUZIONE DELLA CURVA E SUA CERTIFICAZIONE") + print(" ATM = IV interpolata sul delta 0.5/-0.5 (mai estrapolata); tenori fissi in varianza totale.") + cov = curve[[f"iv_{T}" for T in TENORS]].notna().mean() + print("\n copertura per tenore (frazione delle ore-asset ricostruibili):") + for T in TENORS: + print(f" iv_{T:<3d} {cov[f'iv_{T}']:.1%}") + print(f" -> iv_90 e' sotto il 10%: SCARTATO dallo studio. La pendenza canonica e' " + f"iv_{BACK} - iv_{FRONT}.") + + sub("controllo 1 β€” contro il logger indipendente `vol_term_*.parquet` (altro codice, altra fonte)") + print(" (`log_vol_termstructure.py`: mark_iv da book_summary, ATM = strike piu' vicino allo") + print(" spot, interpolazione diversa. Se le due strade coincidono, la mia non e' un artefatto.)") + agree = {} + for a in ASSETS: + p = RAW / f"vol_term_{a.lower()}.parquet" + if not p.exists(): + print(f" {a}: {p.name} assente β€” controllo NON GIRATO") + continue + vt = pd.read_parquet(p).set_index("date") + mine = curve[curve.asset == a].set_index("hr") + j = vt.join(mine[[f"iv_{T}" for T in TENORS]], how="inner") + for T in (7, 30, 60): + x, y = j[f"iv_{T}d"], j[f"iv_{T}"] + m = x.notna() & y.notna() + if m.sum() >= 10: + print(f" {a} iv_{T:<3d} n={m.sum():3d} corr={x[m].corr(y[m]):+.4f} " + f"scarto medio={(y[m] - x[m]).mean():+.3f} pt-vol") + agree[(a, T)] = float(x[m].corr(y[m])) + + sub("controllo 2 β€” contro il DVOL (indice Deribit a 30g, storia 2021+)") + for a in ASSETS: + dv = pd.read_parquet(RAW / f"dvol_{a.lower()}.parquet") + s = pd.Series(dv["close"].astype(float).values, + index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True)).sort_index() + mine = curve[curve.asset == a].set_index("hr")["iv_30"].dropna() + j = pd.DataFrame({"mine": mine}) + j["dvol"] = s.reindex(j.index, method="ffill") + j = j.dropna() + print(f" {a}: n={len(j)} corr={j.mine.corr(j.dvol):+.4f} " + f"bias={(j.mine - j.dvol).mean():+.3f} pt-vol sd={(j.mine - j.dvol).std():.3f}") + print(" Il bias NEGATIVO e' atteso e conferma la ricostruzione: il DVOL e' un indice tipo") + print(" variance-swap (integra tutto lo smile) e sta STRUTTURALMENTE sopra l'ATM per lo skew.") + print(" Un bias ~0 sarebbe stato il segnale d'allarme, non questo.") + return agree + + +# =========================================================================================== +# SEZIONE 2 β€” Q1: LEAD-LAG. La pendenza precede o segue? +# =========================================================================================== +def slope_hourly(curve: pd.DataFrame, a: str) -> pd.Series: + s = curve[curve.asset == a].copy() + s["hr"] = pd.to_datetime(s["hr"], utc=True).dt.as_unit("ns") + s = s.set_index("hr") + return (s[f"iv_{BACK}"] - s[f"iv_{FRONT}"]).dropna().sort_index() + + +def px_hourly(a: str) -> pd.Series: + d = A.get(a, "1h") + return pd.Series(d["close"].astype(float).values, + index=pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True))) + + +def _block_pctl(x: np.ndarray, y: np.ndarray, stat: float, L: int, n: int = 400, + seed: int = 20260822) -> float: + """Percentile della correlazione osservata contro il null a BLOCCHI (y ricampionata da + blocchi non allineati a x): conserva l'autocorrelazione e rompe solo l'accoppiamento.""" + rng = np.random.default_rng(seed) + m = len(x) + if m <= L + 5: + return float("nan") + nb = max(1, m // L) + out = np.empty(n) + for i in range(n): + ii = np.concatenate([np.arange(s, s + L) for s in rng.integers(0, m - L, nb)]) + jj = np.concatenate([np.arange(s, s + L) for s in rng.integers(0, m - L, nb)]) + out[i] = np.corrcoef(x[ii], y[jj])[0, 1] + return float((out < stat).mean()) + + +def sezione2(curve: pd.DataFrame) -> dict: + hr("SEZIONE 2 β€” Q1: la pendenza PRECEDE i ritorni o li SEGUE?") + res = {} + for a in ASSETS: + sl = slope_hourly(curve, a) + px = px_hourly(a) + lp = np.log(px) + j = pd.DataFrame({"slope": sl}).join(px.rename("px"), how="inner").dropna() + idx = j.index + r1 = lp.diff() + sub(f"{a} β€” n ore = {len(j)} ({idx.min()} -> {idx.max()})") + FACTS[f"back_sev_{a}"] = float((j.slope < -5).mean()) + FACTS[f"back_{a}"] = float((j.slope < 0).mean()) + print(f" pendenza iv_{BACK}-iv_{FRONT}: media {j.slope.mean():+.2f} pt-vol, " + f"sd {j.slope.std():.2f}, backwardation nel {100*(j.slope<0).mean():.1f}% delle ore, " + f"SEVERA (< -5 pt-vol) nel {100*(j.slope<-5).mean():.1f}%") + + print("\n (a) CROSS-CORRELAZIONE fra la VARIAZIONE oraria di pendenza e il ritorno orario.") + print(" lag<0 = ritorno PRIMA della variazione (il prezzo guida) | lag>0 = DOPO (la") + print(" pendenza guiderebbe). La barra t va da t a t+1h e lo snapshot cade dentro:") + print(" il lag -1 e' quindi in gran parte SOVRAPPOSTO, non un vero anticipo.") + ds = j.slope.diff() + row = [] + for k in (-3, -2, -1, 0, 1, 2, 3, 6, 12, 24): + row.append((k, float(ds.corr(r1.shift(-k).reindex(idx))))) + print(" " + " ".join(f"{k:+d}h:{v:+.3f}" for k, v in row)) + best = max(row, key=lambda t: abs(t[1])) + print(f" picco |corr| al lag {best[0]:+d}h ({best[1]:+.3f})") + + print("\n (b) LIVELLO della pendenza vs ritorni STRETTAMENTE FUTURI (ingresso alla") + print(" chiusura della barra t) e vs ritorni PASSATI.") + for h in (1, 3, 6, 24, 72, 168): + fwd = (lp.shift(-h) - lp).reindex(idx) + pst = (lp - lp.shift(h)).reindex(idx) + m = j.slope.notna() & fwd.notna() & pst.notna() + print(f" h={h:4d}h corr(pendenza, FUTURO) {j.slope[m].corr(fwd[m]):+.3f} " + f"corr(pendenza, PASSATO) {j.slope[m].corr(pst[m]):+.3f}") + TRIALS.append((f"Q1 {a}: 6 orizzonti x (futuro,passato) x (livello,variazione)", 24)) + + print("\n (c) IL CONFOUND DI QUESTO FILONE β€” la pendenza e' informazione NUOVA o RV riciclata?") + rvp = (r1.rolling(24).std() * np.sqrt(24 * 365) * 100).reindex(idx) + # RV su (t, t+24h]: rolling(24) etichettata a t+24 e riportata indietro a t. (Una prima + # stesura aveva uno shift di troppo e misurava la finestra (+24h, +47h): corretto in + # sessione confrontando con una seconda implementazione a serie rovesciata.) + rvf = (r1.rolling(24).std().shift(-24) * np.sqrt(24 * 365) * 100).reindex(idx) + cf = curve[curve.asset == a].copy() + cf["hr"] = pd.to_datetime(cf["hr"], utc=True).dt.as_unit("ns") + ivf = cf.set_index("hr")[f"iv_{FRONT}"].reindex(idx) + c_pp = float(j.slope.corr(rvp)) + c_pf = float(j.slope.corr(rvf)) + print(f" corr(pendenza, RV 24h PASSATA) {c_pp:+.3f} <-- quanto e' termometro") + print(f" corr(pendenza, RV 24h FUTURA) {c_pf:+.3f} <-- quanto e' previsione") + print(f" corr(iv_{FRONT}, RV 24h passata) {float(ivf.corr(rvp)):+.3f} " + f"corr(iv_{FRONT}, RV futura) {float(ivf.corr(rvf)):+.3f}") + res[(a, "conf_past")], res[(a, "conf_fwd")] = c_pp, c_pf + FACTS[f"conf_past_{a}"], FACTS[f"conf_fwd_{a}"] = c_pp, c_pf + + print("\n (d) La correlazione col futuro SOPRAVVIVE se si controlla per il PASSATO?") + print(" (regressione del ritorno futuro su pendenza + ritorno passato + RV passata;") + print(" se il coefficiente della pendenza evapora, il segnale era 'e' appena sceso')") + for h in (24, 72, 120): + fwd = (lp.shift(-h) - lp).reindex(idx) + pst = (lp - lp.shift(h)).reindex(idx) + m = j.slope.notna() & fwd.notna() & pst.notna() & rvp.notna() + X = np.column_stack([np.ones(m.sum()), j.slope[m], pst[m], rvp[m]]) + y = fwd[m].to_numpy() + b, *_ = np.linalg.lstsq(X, y, rcond=None) + # errori standard Newey-West (lag = h, finestre sovrapposte) + e = y - X @ b + XtXi = np.linalg.pinv(X.T @ X) + S = (X * e[:, None]).T @ (X * e[:, None]) + for L in range(1, h + 1): + w = 1.0 - L / (h + 1.0) + G = (X[L:] * e[L:, None]).T @ (X[:-L] * e[:-L, None]) + S += w * (G + G.T) + se = np.sqrt(np.diag(XtXi @ S @ XtXi)) + simple = float(j.slope[m].corr(fwd[m])) + print(f" h={h:4d}h corr semplice {simple:+.3f} | beta_pendenza={b[1]:+.5f} " + f"t(NW)={b[1]/se[1]:+.2f} | beta_ret_passato t={b[2]/se[2]:+.2f}") + TRIALS.append((f"Q1 {a}: 3 regressioni controllate", 3)) + + print("\n (e) Test giornaliero con null a BLOCCHI (la finestra e' di 74 giorni: la") + print(" correlazione va confrontata con la sua dispersione, non con zero).") + sld = j.slope.resample("1D").last().dropna() + pxd = px.resample("1D").last() + lpd = np.log(pxd) + for h in (1, 3, 5): + fwd = (lpd.shift(-h) - lpd).reindex(sld.index) + m = sld.notna() & fwd.notna() + x, y = sld[m].to_numpy(), fwd[m].to_numpy() + c = float(np.corrcoef(x, y)[0, 1]) + p = _block_pctl(x, y, c, L=max(3 * h, 7)) + c7 = float(np.corrcoef(x[:-7], y[:-7])[0, 1]) if len(x) > 20 else np.nan + c7b = float(np.corrcoef(x[7:], y[7:])[0, 1]) if len(x) > 20 else np.nan + if h == 5: + FACTS[f"c5_{a}"], FACTS[f"c5_no7_{a}"] = c, c7 + print(f" h={h}g n={m.sum():3d} corr={c:+.3f} pctl vs null a blocchi={p:.3f}" + f" | senza ultima settimana {c7:+.3f} | senza prima settimana {c7b:+.3f}") + print(" ^ le due colonne di destra sono il test di RISOLUZIONE: se togliere 7") + print(" giorni su 74 sposta la correlazione di piu' di quanto la correlazione") + print(" valga, il campione non sta misurando, sta oscillando.") + TRIALS.append((f"Q1 {a}: 3 orizzonti giornalieri x 3 sottocampioni", 9)) + return res + + +# =========================================================================================== +# SEZIONE 3 β€” Q2: il carry di vol e' incassabile? +# =========================================================================================== +def build_atm_cost(chain: pd.DataFrame) -> pd.DataFrame: + """Costo REALE di attraversare lo spread su un'opzione ATM, in PUNTI DI VOL. + + (ask-bid) e' quotato nel sottostante -> x spot = USD; / vega = punti di vol, che e' l'unita' + in cui si misura il roll-down. Cosi' costo e carry sono confrontabili senza modello. + """ + f = CACHE / "atm_cost.parquet" + if f.exists(): + return pd.read_parquet(f) + q = chain[chain.quoted & (chain.dte > DTE_MIN) & (chain.option_type == "C") & (chain.vega > 0)].copy() + q["ad"] = (q["delta"] - 0.5).abs() + atm = q.sort_values("ad").groupby(["asset", "hr", "exp"], as_index=False).first() + atm = atm[atm.ad < 0.08] + for a in ASSETS: + p = px_hourly(a) + m = atm.asset == a + atm.loc[m, "spot"] = p.reindex(atm.loc[m, "hr"]).ffill().to_numpy() + atm = atm.dropna(subset=["spot"]) + atm["spr_usd"] = (atm.ask - atm.bid) * atm.spot + atm["spr_vol"] = atm.spr_usd / atm.vega + atm["prem_usd"] = (atm.bid + atm.ask) / 2.0 * atm.spot + CACHE.mkdir(parents=True, exist_ok=True) + atm.to_parquet(f) + return atm + + + +def _roll_table(r: pd.DataFrame, H: int) -> pd.DataFrame: + """Roll IMPLICITO vs REALIZZATO su orizzonte H ore, appaiato per (scadenza, ISTANTE D'INGRESSO). + + Appaiato sull'INGRESSO e non sull'uscita: e' l'uscita che le due letture spostano, e un + inner-join sulla data d'uscita terrebbe solo i casi in cui non e' successo niente (trappola + incontrata 3 volte nel progetto: venue-watch, GTAA, VRP profit-take). + impl = iv_curva(T-H) al tempo t - iv(T) al tempo t (se la curva non si muovesse) + real = iv(T-H) al tempo t+H - iv(T) al tempo t (cio' che e' successo davvero) + """ + r = r[["hr", "exp", "iv", "dte"]].dropna() + nxt = r[["hr", "exp", "iv"]].copy() + nxt["hr"] = nxt["hr"] - pd.Timedelta(hours=H) + j = r.merge(nxt.rename(columns={"iv": "iv_fut"}), on=["hr", "exp"], how="inner") + j["T2"] = j["dte"] - H / 24.0 + j = j[j["T2"] > DTE_MIN] + if j.empty: + return pd.DataFrame(columns=["dte", "impl", "real"]) + # curva dell'ora t, ordinata: chiave monotona globale gid*1000 + dte -> searchsorted unico + c = r.sort_values(["hr", "dte"]).copy() + c["gid"] = c.groupby("hr", sort=True).ngroup() + key = c["gid"].to_numpy() * 1000.0 + c["dte"].to_numpy() + w = (c["iv"].to_numpy() / 100.0) ** 2 * c["dte"].to_numpy() # varianza totale + gmap = dict(zip(c["hr"].to_numpy(), c["gid"].to_numpy())) + n_tot = np.bincount(c["gid"].to_numpy()) + start = np.concatenate([[0], np.cumsum(n_tot)[:-1]]) + gq = np.array([gmap[h] for h in j["hr"].to_numpy()]) + pos = np.searchsorted(key, gq * 1000.0 + j["T2"].to_numpy(), side="left") + lo, hi = pos - 1, pos + ok = (lo >= start[gq]) & (hi < start[gq] + n_tot[gq]) + ivT2 = np.full(len(j), np.nan) + if ok.any(): + l, h2 = lo[ok], hi[ok] + x0 = key[l] - gq[ok] * 1000.0 + x1 = key[h2] - gq[ok] * 1000.0 + t2 = j["T2"].to_numpy()[ok] + ww = np.where(x1 != x0, (t2 - x0) / np.where(x1 != x0, x1 - x0, 1.0), 0.0) + var = w[l] + ww * (w[h2] - w[l]) + ivT2[ok] = np.sqrt(np.maximum(var, 0.0) / t2) * 100.0 + j = j.assign(ivT2=ivT2).dropna(subset=["ivT2"]) + return pd.DataFrame({"dte": j["dte"].to_numpy(), + "impl": j["ivT2"].to_numpy() - j["iv"].to_numpy(), + "real": j["iv_fut"].to_numpy() - j["iv"].to_numpy()}) + + +def sezione3(chain: pd.DataFrame, atm_exp: pd.DataFrame, curve: pd.DataFrame) -> None: + hr("SEZIONE 3 β€” Q2: il CARRY di vol (roll-down) e' incassabile al bid-ask reale?") + print(" Definizione senza modello: un'opzione a tenore T, dopo dt, sta a T-dt. Se la curva non") + print(" si muove la sua IV diventa iv(T-dt). Chi e' CORTO guadagna iv(T)-iv(T-dt) punti di vol.") + print(" Il costo di entrare e uscire e' lo spread bid-ask, misurabile negli STESSI punti di vol.") + + sub("(a) quanto vale il roll-down, e viene DAVVERO incassato?") + print(" Confronto appaiato per SCADENZA e per ISTANTE D'INGRESSO (mai allineato sull'uscita):") + print(" roll IMPLICITO = iv_curva(T-dt) - iv(T) al tempo t ; roll REALIZZATO = iv(T-dt) a t+dt - iv(T) a t") + roll_day = {} + for H in (24, 168): + for a in ASSETS: + d = _roll_table(atm_exp[atm_exp.asset == a], H) + d = d[(d.dte >= 4) & (d.dte <= 45)] + if len(d) < 50: + print(f" {a} dt={H}h: campione insufficiente ({len(d)})") + continue + b = np.polyfit(d.impl, d.real, 1)[0] + diff = d.real - d.impl + nblk = max(1, len(d) // max(1, H)) # osservazioni ~indipendenti + t = diff.mean() / diff.std() * np.sqrt(nblk) if diff.std() > 0 else 0.0 + print(f" {a} dt={H:3d}h n={len(d):5d} roll implicito medio {d.impl.mean():+.4f} pt-vol " + f"realizzato {d.real.mean():+.4f} beta(real~impl)={b:+.2f} " + f"corr={d.impl.corr(d.real):+.3f} (real-impl) t={t:+.2f}") + if H == 24: + roll_day[a] = -float(d.impl.mean()) # guadagno giornaliero di CHI E' CORTO + print("\n Lettura: se il roll-down fosse incassato, beta(real~impl) sarebbe ~+1. E' NEGATIVO") + print(" (~-1 a una settimana): sul campione la FORMA della curva torna indietro piu' di") + print(" quanto la curva rotoli. Il segno del bias da errore-di-misura sarebbe POSITIVO") + print(" (iv(T) compare in entrambi con segno opposto), quindi non spiega questo.") + TRIALS.append(("Q2: 2 orizzonti x 2 asset (roll implicito vs realizzato)", 4)) + + sub("(b) il costo reale: bid-ask ATM in PUNTI DI VOL, per tenore") + cost = build_atm_cost(chain) + cost["bucket"] = pd.cut(cost.dte, [DTE_MIN, 3, 10, 20, 45, 95], + labels=["1-3g", "3-10g", "10-20g", "20-45g", "45-95g"]) + tab = cost.groupby(["asset", "bucket"], observed=True).agg( + n=("spr_vol", "size"), spread_ptvol=("spr_vol", "median"), + spread_pct_premio=("spr_usd", lambda s: np.nan), + premio_usd=("prem_usd", "median"), vega_usd_per_ptvol=("vega", "median")) + tab["spread_pct_premio"] = (cost.assign(x=cost.spr_usd / cost.prem_usd * 100) + .groupby(["asset", "bucket"], observed=True)["x"].median()) + print(tab.round(3).to_string().replace("\n", "\n ")) + + sub("(c) l'aritmetica che chiude la domanda") + med = cost.groupby(["asset", "bucket"], observed=True)["spr_vol"].median() + for a in ASSETS: + rd = roll_day.get(a, np.nan) + sp = float(med.loc[(a, "10-20g")]) + print(f" {a}: roll-down incassabile da CORTO = {rd:+.4f} pt-vol/giorno | " + f"un round-trip ATM 10-20g costa {sp:.3f} pt-vol") + print(f" => servono {sp/rd:.1f} GIORNI di carry per pagare UN solo giro, " + f"e nel frattempo si porta il vega.") + FACTS[f"giorni_carry_{a}"] = sp / rd + TRIALS.append(("Q2: 5 secchi di tenore x 2 asset (costo)", 10)) + + sub("(d) e a $635 la domanda non e' nemmeno questa: il LOTTO MINIMO") + for a in ASSETS: + c = cost[(cost.asset == a) & (cost.bucket == "10-20g")] + if c.empty: + continue + prem = float(c.prem_usd.median()) * MIN_LOT[a] + vega = float(c.vega.median()) * MIN_LOT[a] + spot = float(c.spot.median()) + fee = min(OPT_FEE_UNDERLYING * spot, OPT_FEE_CAP_PREMIUM * float(c.prem_usd.median())) * MIN_LOT[a] + rd = roll_day.get(a, np.nan) + carry_g = rd * vega + friz = float(c.spr_vol.median()) * vega + 4 * fee # 1 spread pieno + 4 gambe di fee + print(f" {a}: lotto minimo {MIN_LOT[a]} -> premio ATM 10-20g = ${prem:,.2f} " + f"({prem/CAPITALE:.0%} del conto), vega ${vega:.2f}/pt-vol, fee ${fee:.2f}/gamba") + print(f" carry lordo ${carry_g:+.3f}/giorno per struttura; attrito per giro " + f"${friz:.2f} => {friz/carry_g:.0f} giorni per andare in pari") + print("\n Un calendar spread e' DUE gambe. Cio' che la tabella mostra e' il PREMIO di UNA") + print(" gamba al lotto minimo: BTC 26% del conto, ETH 10%. Il premio non e' tutto il") + print(" fabbisogno β€” la gamba CORTA vuole margine, e la misura del 2026-07-30 su una") + print(" struttura BTC a rischio definito dava $6.210 di collaterale per lotto. Quindi:") + print(" BTC resta fuori per il margine, ETH entra ma una sola struttura impegna il") + print(" 10-25% del conto per un carry LORDO di centesimi al giorno.") + print(" Il bersaglio dichiarato e' 50 EUR/giorno: qui siamo 2-3 ordini di grandezza sotto,") + print(" e il conto non regge la SECONDA struttura che servirebbe per diversificare.") + TRIALS.append(("Q2: 2 asset x eseguibilita' al lotto minimo", 2)) + + +# =========================================================================================== +# SEZIONE 4 β€” Q3: gate di rischio sopra TP01. RIDONDANZA PRIMA DELL'UPLIFT. +# =========================================================================================== +def tp01_positions() -> dict: + from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio + tp = TrendPortfolio(**CANONICAL) + out = {} + for a in ASSETS: + df = A.get(a, "1d") + out[a] = pd.Series(tp.target_series(df), + index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))) + return out + + +def _anchored(sl_h: pd.Series, px_h: pd.Series, off: int): + """Serie giornaliere (pendenza, ritorno) ancorate all'ora `off`.""" + m = sl_h.index.hour == off + s = sl_h[m] + p = px_h.reindex(s.index).ffill() + r = p.pct_change() + return s, r + + +def sezione4(curve: pd.DataFrame) -> None: + hr("SEZIONE 4 β€” Q3: la pendenza come GATE DI RISCHIO sopra TP01. RIDONDANZA PRIMA DELL'UPLIFT") + print(" Regola del progetto: 4 gate di de-risk su 4 (DVOL-spike, macro, funding, breadth) sono") + print(" risultati RIDONDANTI col trend. Percio' la ridondanza si misura PRIMA di rivendicare") + print(" qualunque uplift: quanti dei giorni che il gate spegne sono giorni in cui TP01 e' gia' flat?") + pos = tp01_positions() + + sub("(a) il fatto che decide da solo la sezione") + W = pd.concat(pos, axis=1).loc[TS_START:] + flat = (W.abs() < 0.05).mean() + FACTS["tp01_flat"] = float(flat.mean()) + FACTS["n_giorni"] = int(len(W)) + print(f" Nella finestra {TS_START.date()} -> oggi ({len(W)} giorni), TP01 e' FLAT nel " + f"{flat['BTC']:.1%} (BTC) / {flat['ETH']:.1%} (ETH) dei giorni.") + print(f" Esposizione media: BTC {W['BTC'].mean():.3f}, ETH {W['ETH'].mean():.3f} " + f"(target TSMOM risk-off, coerente col libro live dichiarato flat).") + print(" ⚠️ Un gate di de-risk su un libro gia' flat il 93% del tempo NON E' MISURABILE su") + print(" questa finestra: non c'e' rischio da togliere. Questo non e' un difetto del") + print(" gate, e' un difetto del CAMPIONE β€” e va detto prima di qualunque numero.") + + sub("(b) ridondanza, per quel che il campione consente") + for a in ASSETS: + sl = slope_hourly(curve, a).resample("1D").last().dropna() + sl.index = pd.DatetimeIndex(sl.index).as_unit("ns") + j = pd.concat({"slope": sl, "tp": pos[a]}, axis=1, join="inner").dropna() + for qq in (0.10, 0.25, 0.40): + th = j.slope.quantile(qq) + off = j.slope < th + already = float((j.tp[off].abs() < 0.05).mean()) if off.any() else np.nan + print(f" {a} gate OFF sotto p{int(qq*100)} ({th:+.2f} pt-vol): spegne il " + f"{off.mean():.1%} dei giorni; di questi TP01 era GIA' flat nel {already:.1%} " + f"| corr(pendenza, esposizione TP01) = {j.slope.corr(j.tp):+.3f}") + TRIALS.append((f"Q3 {a}: 3 soglie di gate", 3)) + print("\n Il segno di corr(pendenza, esposizione) e' NEGATIVO: la backwardation arriva") + print(" quando TP01 e' gia' uscito. E' la firma esatta dei 4 precedenti.") + + sub("(c) uplift sulla finestra, su TUTTE le 24 ancore (mai su una sola)") + print(" Candidato DIREZIONALE (non gate): contango -> long, backwardation -> flat; vol-target") + print(" 14g, cap 2x, fee 5 bps/lato. Soglia a ZERO = a priori, nessun parametro tarato.") + for a in ASSETS: + sl_h, px_h = slope_hourly(curve, a), px_hourly(a) + + def cand(off, s=sl_h, p=px_h): + s_d, r_d = _anchored(s, p, off) + d = pd.DataFrame({"slope": s_d, "r": r_d}).dropna() + if len(d) < 30: + return pd.Series(dtype=float) + vol = d["r"].rolling(14).std() * np.sqrt(365) + dirn = (d["slope"] > 0).astype(float) + tgt = np.clip(dirn * (0.20 / vol.replace(0, np.nan)), 0, 2.0).fillna(0.0) + hold = tgt.shift(1).fillna(0.0) + return (hold * d["r"] - A.FEE_SIDE * hold.diff().abs().fillna(0.0)).dropna() + + def base(off, p=px_h, s=sl_h): + s_d, r_d = _anchored(s, p, off) + d = pd.DataFrame({"r": r_d}).dropna() + return d["r"] + + band = A.anchor_luck_band(cand, list(range(24)), canonical=0) + print(f" {a}: Sharpe candidato β€” canonico(00:00) {band['canonical']:+.2f} " + f"(pctl {band['canonical_pctl']:.2f}) | MEDIANA ONESTA {band['median']:+.2f} | " + f"banda [{band['lo']:+.2f}, {band['hi']:+.2f}] | positivo in {band['frac_positive']:.0%} " + f"delle 24 ancore | gate_pass={band['gate_pass']}") + bh = A.anchor_luck_band(base, list(range(24)), canonical=0) + print(f" buy&hold sulla stessa finestra/ancore: mediana {bh['median']:+.2f} " + f"[{bh['lo']:+.2f}, {bh['hi']:+.2f}] <-- il vero termine di paragone in un toro") + dlt = A.anchor_luck_delta(cand, base, list(range(24))) + FACTS[f"vs_bh_{a}"] = dlt["median_paired"] + FACTS[f"vs_bh_n_{a}"] = (dlt["n_positive"], dlt["n_anchors"]) + print(f" mediana delle DIFFERENZE APPAIATE candidato-B&H: {dlt['median_paired']:+.2f} " + f"(positiva in {dlt['n_positive']}/{dlt['n_anchors']} ancore)") + TRIALS.append((f"Q3 {a}: 24 ancore x 2 varianti", 48)) + + sub("(d) null del DE-LEVERING β€” obbligatorio su ogni claim di minor rischio") + for a in ASSETS: + sl_h, px_h = slope_hourly(curve, a), px_hourly(a) + s_d, r_d = _anchored(sl_h, px_h, 0) + d = pd.DataFrame({"slope": s_d, "r": r_d}).dropna() + vol = d["r"].rolling(14).std() * np.sqrt(365) + tgt = np.clip((d["slope"] > 0).astype(float) * (0.20 / vol.replace(0, np.nan)), 0, 2.0).fillna(0.0) + hold = tgt.shift(1).fillna(0.0) + net = (hold * d["r"] - A.FEE_SIDE * hold.diff().abs().fillna(0.0)).dropna() + dd_c = A._dd_ret(net) + # ⚠️ il k giusto e' il PIU' GRANDE che sta sotto il DD del candidato, non il primo che + # trovi salendo da 0.05: partendo dal basso si confronta il candidato con un baseline + # quasi spento, che ha un DD ridicolo e uno Sharpe gonfiato dal rapporto. (Errore + # commesso e corretto in sessione: dava "candidato sopra il null" con k=0.05.) + ks = np.arange(0.05, 3.01, 0.05) + best = None + for k in ks[::-1]: + b = k * d["r"].reindex(net.index) + if A._dd_ret(b) <= dd_c: + best = (float(k), A._sh(b), A._dd_ret(b)) + break + sh_c = A._sh(net) + if best is None: + print(f" {a}: candidato Sharpe {sh_c:+.2f} maxDD {dd_c:.2%} | nessun k in " + f"[0.05,3.00] raggiunge quel DD") + else: + verdict = ("IL CANDIDATO NON SOPRAVVIVE (il de-levering fa meglio)" + if best[1] >= sh_c else "candidato sopra il null di de-levering") + FACTS[f"delev_{a}"] = bool(best[1] >= sh_c) + print(f" {a}: candidato Sharpe {sh_c:+.2f} maxDD {dd_c:.2%} | buy&hold a leva " + f"k={best[0]:.2f} -> stesso o minor DD ({best[2]:.2%}) con Sharpe {best[1]:+.2f}" + f" => {verdict}") + imp = A.implausible_sharpe(net) + FACTS[f"implaus_{a}"] = bool(imp["implausible"]) + print(f" implausible_sharpe sul candidato di finestra: implausible=" + f"{imp['implausible']} {imp['reasons']}") + TRIALS.append((f"Q3 {a}: 60 valori di k nel null de-levering", 60)) + + +# =========================================================================================== +# SEZIONE 5 β€” IL TEST LUNGO. La pendenza ha un PROXY con 5 anni di storia? +# =========================================================================================== +def _proxy_slope(df: pd.DataFrame, asset: str, rvwin: int = 7) -> np.ndarray: + """Proxy A PRIORI della pendenza su storia lunga: DVOL(30g) - RV(rvwin), tutto causale. + + Motivazione, non scelta a posteriori: iv_30 ~ DVOL (corr 0.99, SEZIONE 1) e iv_7 e' in larga + parte una funzione della RV recente (SEZIONE 2c). Il proxy e' quindi la pendenza con la gamba + corta sostituita dalla sua determinante. NON e' la stessa variabile: l'attenuazione e' + misurata sotto e va portata dietro a ogni conclusione. + """ + c = df["close"].to_numpy(float) + r = A.simple_returns(c) + bpd = A.bars_per_day(df) + rv = A.realized_vol(r, max(2, rvwin * bpd), bpd * 365.25) * 100.0 + return A.dvol(df, asset) - rv + + +def make_proxy_dir(long_only: bool = True, thr: float = 0.0): + def fn(df, asset): + p = _proxy_slope(df, asset) + d = np.where(np.isfinite(p), np.where(p > thr, 1.0, 0.0 if long_only else -1.0), 0.0) + return A.vol_target(d, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) + return fn + + +def sezione5(curve: pd.DataFrame) -> None: + hr("SEZIONE 5 β€” IL TEST LUNGO: la pendenza ha un proxy con 5 anni di storia?") + print(" Con 74 giorni nessun gate statistico ha potenza. L'unica via per DIRE qualcosa e' un") + print(" proxy a priori su storia lunga β€” e dichiararne l'attenuazione.") + + sub("(a) quanto e' buono il proxy (misurato sulla finestra vera, non assunto)") + for a in ASSETS: + sl = slope_hourly(curve, a).resample("1D").last().dropna() + sl.index = pd.DatetimeIndex(sl.index).as_unit("ns") + df = A.get(a, "1d") + p = pd.Series(_proxy_slope(df, a), + index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))).dropna() + j = pd.concat({"vero": sl, "proxy": p}, axis=1, join="inner").dropna() + c = float(j.vero.corr(j.proxy)) + print(f" {a}: corr(pendenza VERA, DVOL-RV7) = {c:+.3f} su n={len(j)} giorni " + f"-> attenuazione: il proxy spiega il {c**2:.0%} della varianza della pendenza") + print(" ⚠️ Un proxy a corr ~0.5-0.7 e' una lente SFOCATA: un risultato NEGATIVO sul proxy") + print(" e' evidenza piu' debole di un negativo sulla variabile vera. Va detto, non nascosto.") + + sub("(b) il candidato direzionale sul proxy, 5 anni, coi gate veri") + fn = make_proxy_dir(long_only=True) + cau = A.causality_ok(fn, tf="1d") + print(f" causality_ok: {cau}") + cd = A.candidate_daily(fn, tf="1d") + era = cd[cd.index >= pd.Timestamp("2021-10-01", tz="UTC")] + print(f" era DVOL (2021-10 -> oggi, {len(era)} giorni): Sharpe {A._sh(era):+.3f} " + f"maxDD {A._dd_ret(era):.2%}") + rep = A.marginal_vs_tp01(cd) + FACTS["proxy_marginal"] = rep.get("marginal_verdict") + FACTS["proxy_sharpe"] = A._sh(era) + FACTS["proxy_uplift"] = rep["blends"]["w25"]["uplift_full"] + print(f" marginal_vs_tp01 -> {rep.get('marginal_verdict')} | corr a TP01 " + f"{rep.get('corr_full')} | uplift w25 full {rep['blends']['w25']['uplift_full']:+.3f} " + f"hold {rep['blends']['w25']['uplift_hold']}") + for k in ("is_hedge", "has_insample_edge", "robust_oos", "beats_noise_null", "earns_slot"): + if k in rep: + print(f" {k:20s} = {rep[k]}") + imp = A.implausible_sharpe(era) + print(f" implausible_sharpe (proxy, era DVOL): implausible={imp['implausible']} " + f"{imp['reasons']}") + print(f" earns_slot = (assoluto non-FAIL) AND (marginale ADDS): qui il marginale e' " + f"{rep.get('marginal_verdict')} -> **earns_slot = False**.") + TRIALS.append(("Sez.5: 2 varianti proxy (long-only, L/S) x 1 soglia", 2)) + + sub("(c) il precedente che questo replica") + print(" `dvol_directional.py` (2026-06-29) testo' VRP-Z = z-score causale di (IV-RV) come") + print(" segnale DIREZIONALE su 5 anni, con la griglia completa: verdetto **HEDGE,") + print(" earns_slot=False**. Qui, con la variante a SOGLIA BINARIA invece che a z-score,") + print(" il verdetto misurato e' NEUTRAL con uplift NEGATIVO β€” un verdetto diverso, la") + print(" stessa conclusione (earns_slot=False in entrambi). La differenza sta nella") + print(" variante, non nell'esito, e va riportata cosi': non sto dichiarando una replica") + print(" che non ho fatto.") + print(" Il punto strutturale resta: la pendenza e' il premio di vol con la gamba") + print(" REALIZZATA sostituita da una IMPLICITA (SEZIONE 2c: corr(pendenza, RV passata)") + print(" = -0.50/-0.57). Non e' una variabile nuova per questo progetto: e' quella, con") + print(" un rumore di misura piu' basso e 5 anni di storia in meno.") + + +# =========================================================================================== +# SEZIONE 6 β€” CONTI, GATE, VERDETTO +# =========================================================================================== +def sezione6() -> None: + hr("SEZIONE 6 β€” GRIGLIA DICHIARATA E DEFLATED SHARPE") + tot = sum(n for _, n in TRIALS) + for name, n in TRIALS: + print(f" {n:5d} {name}") + print(f" {'-'*70}\n {tot:5d} TOTALE (contato al RIALZO: ogni variante valutata, anche scartata)") + print("\n Non e' stata ridotta nessuna griglia per il budget: il vincolo di questo filone") + print(" non e' il tempo di calcolo, e' che il campione dura 74 giorni.") + print("\n deflated_sharpe: NON GIRATO su un candidato di questo studio, e il motivo e' una") + print(" scelta, non una dimenticanza: il DSR e' una correzione per multiple-testing su UNA") + print(" serie di ritorni; qui il candidato migliore ha 74 osservazioni e la sua incertezza") + print(" di CAMPIONE (SE(Sharpe) ~ 2.2) domina di un ordine di grandezza quella da selezione.") + print(f" Deflazionare {tot} trial su 74 giorni darebbe un numero preciso e privo di senso.") + print(" Sul proxy a 5 anni il DSR e' invece pertinente ed e' riportato dal gate del 29/06") + print(" su cui questo filone ricade (VRP-Z: earns_slot=False).") + + + +# =========================================================================================== +# SEZIONE 7 β€” VERDETTO +# =========================================================================================== +def sezione7() -> None: + F = FACTS + hr("SEZIONE 7 β€” VERDETTO") + print(f""" + Q1 LA PENDENZA E' UN TERMOMETRO, NON UN SEGNALE β€” e la finestra non ha risoluzione. + Β· La cross-correlazione oraria ha il picco a lag -1h/0h (il prezzo si muove PRIMA); da + +1h in poi e' rumore (|corr| <= 0.05 su ogni lag fino a 24h). + Β· corr(pendenza, RV 24h PASSATA) = {F.get('conf_past_BTC', float('nan')):+.2f} (BTC) / {F.get('conf_past_ETH', float('nan')):+.2f} (ETH) + corr(pendenza, RV 24h FUTURA) = {F.get('conf_fwd_BTC', float('nan')):+.2f} (BTC) / {F.get('conf_fwd_ETH', float('nan')):+.2f} (ETH) + La pendenza SA qualcosa della vol futura, ma sa DI PIU' di quella passata: e' un premio + di vol con la gamba realizzata sostituita da una implicita, non una variabile nuova. + Β· L'unica correlazione col ritorno futuro che sembra qualcosa (h=5g: {F.get('c5_BTC', float('nan')):+.2f} BTC / + {F.get('c5_ETH', float('nan')):+.2f} ETH) NON SOPRAVVIVE alla rimozione dell'ULTIMA SETTIMANA: + diventa {F.get('c5_no7_BTC', float('nan')):+.2f} / {F.get('c5_no7_ETH', float('nan')):+.2f}. Sette giorni tolti da un campione di 70 ne + ribaltano il SEGNO β€” ed e' il test che conta piu' del percentile contro il null. + E nella regressione controllata per il ritorno passato il t di Newey-West sta sotto 1.0 + a ogni orizzonte. Non e' "debole": e' sotto la risoluzione del campione. + + Q2 IL CARRY C'E' MA NON PAGA IL BID-ASK, e non e' nemmeno incassato. + Β· Roll-down da corto: ~0.07 (BTC) / ~0.13 (ETH) punti-vol al GIORNO. + Round-trip ATM 10-20g: 1.26 (BTC) / 1.77 (ETH) punti-vol. + => {F.get('giorni_carry_BTC', float('nan')):.0f} giorni (BTC) / {F.get('giorni_carry_ETH', float('nan')):.0f} giorni (ETH) di carry per pagare UN giro. + Β· E il roll NON viene incassato: beta(realizzato ~ implicito) e' NEGATIVO (~-1 a una + settimana), cioe' la forma della curva torna indietro piu' di quanto la curva rotoli. + La differenza realizzato-implicito e' dentro il rumore (|t| <= 0.9 a blocchi). + Β· A $635 il vincolo binding e' prima di tutto il LOTTO MINIMO: una gamba ATM costa il 26% + (BTC) / 10% (ETH) del conto in premio, la gamba corta vuole margine, e il carry lordo + di una struttura ETH e' ~$0.19/giorno. Il bersaglio dichiarato e' 50 EUR/giorno. + + Q3 GATE DI RISCHIO: RIDONDANTE COL TREND β€” 5a occorrenza, e NON MISURABILE su questa finestra. + Β· TP01 e' FLAT nel {F.get('tp01_flat', float('nan')):.0%} dei giorni della finestra: non c'e' rischio da togliere. + Un gate che spegne il 25% dei giorni trova TP01 gia' flat nel 79-90% di quei giorni, e + corr(pendenza, esposizione TP01) e' NEGATIVA (-0.29 BTC / -0.52 ETH): la backwardation + arriva quando il trend e' gia' uscito. Firma identica ai 4 precedenti. + Β· Il candidato DIREZIONALE fa Sharpe ~3 su 24/24 ancore β€” e non vuol dire niente: il + buy&hold sulla stessa finestra fa altrettanto. Mediana delle DIFFERENZE APPAIATE contro + buy&hold: {F.get('vs_bh_BTC', float('nan')):+.2f} BTC (positiva in {F.get('vs_bh_n_BTC', (0, 0))[0]}/{F.get('vs_bh_n_BTC', (0, 0))[1]} ancore) e {F.get('vs_bh_ETH', float('nan')):+.2f} ETH ({F.get('vs_bh_n_ETH', (0, 0))[0]}/{F.get('vs_bh_n_ETH', (0, 0))[1]}). + Β· Null del de-levering: ETH {'NON superato' if F.get('delev_ETH') else 'superato'}, BTC {'NON superato' if F.get('delev_BTC') else 'superato'}. + Β· `implausible_sharpe` segnala ENTRAMBI i candidati di finestra (implaus BTC={F.get('implaus_BTC')}, + ETH={F.get('implaus_ETH')}): Calmar 26-52 su 74 giorni di toro = il modo di perdita non e' nel + campione. Il gate qui funziona da controllo positivo del CAMPIONE, non della strategia. + + TEST LUNGO (proxy DVOL-RV7, 5 anni): Sharpe {F.get('proxy_sharpe', float('nan')):+.2f}, marginale vs TP01 = + {F.get('proxy_marginal')}, uplift w25 {F.get('proxy_uplift', float('nan')):+.3f} (NEGATIVO), robust_oos=False + => earns_slot = False. Il proxy e' sfocato (spiega il 21-48% della varianza della pendenza + vera), quindi il suo negativo e' evidenza PIU' DEBOLE di un negativo diretto β€” ma va nella + stessa direzione del verdetto HEDGE gia' dato a VRP-Z il 2026-06-29. + + ====================================================================================== + VERDETTO: SCARTATO come sleeve e come gate. LEAD residuo SOLO sulla term structure come + PREVISORE DI VOL (non di ritorni), con gate pre-registrato qui sotto. + ====================================================================================== + + Perche' non "LEAD" sul lato direzionale: un LEAD e' una cosa che vale la pena SORVEGLIARE, e + sorvegliare costa (la lezione DVOLSPREAD: 35 giorni di limbo). Qui il lato direzionale non ha + bisogno di altri dati per essere giudicato: e' gia' misurato su 5 anni via proxy (earns_slot + False) e la variabile e' quella di VRP-Z. Aprire un monitor sarebbe pagare due volte lo stesso + filone. Il lato del CARRY e' chiuso dall'aritmetica, che non migliora col tempo: il bid-ask + ATM di Deribit dovrebbe stringersi di 10-20x perche' il roll-down lo paghi. + + GATE PRE-REGISTRATO (l'unico pezzo che il tempo puo' cambiare): + Β· COSA: la pendenza come previsore della RV a 24h (misurata: corr {F.get('conf_fwd_BTC', float('nan')):+.2f} / {F.get('conf_fwd_ETH', float('nan')):+.2f}). + Non serve a prendere posizione sul prezzo; servirebbe come DENOMINATORE del + vol-target, cioe' esattamente cio' che `tp01_dvol_overlay` (26/06) provo' col DVOL + a un tenore solo e trovo' essere puro de-levering. La curva a piu' tenori e' l'unica + variante di quell'esperimento mai fatta. + Β· QUANDO: 2027-02-01. A quella data la catena avra' ~8 mesi di curva (vs 2,4 oggi) e la + finestra conterra' almeno un regime di vol diverso da questo toro. + Β· SOGLIE (dichiarate ORA, non allora): si riapre SOLO se, sulla finestra estesa, + (a) corr(pendenza, RV futura 24h) resta <= -0.30 su ENTRAMBI gli asset, E + (b) la pendenza batte il DVOL a tenore singolo come previsore di RV + (R2 incrementale > 0 in una regressione che contiene gia' il DVOL: se non + aggiunge nulla al DVOL, la curva non serve e basta il tenore che abbiamo dal 2021), E + (c) il null del de-levering e' superato sul vol-target risultante. + Se (b) fallisce il filone si CHIUDE definitivamente: vorra' dire che la term + structure non porta informazione oltre il livello, e il livello ce l'abbiamo gia'. + Β· COSA MI SMENTIREBBE OGGI: una finestra che contenga un vero crash. Tutte e tre le risposte + qui sono misurate su 74 giorni di TORO con TP01 flat il {F.get('tp01_flat', float('nan')):.0%} del tempo. La backwardation + severa (pendenza < -5 pt-vol) copre il {F.get('back_sev_BTC', float('nan')):.1%} delle ore BTC e il {F.get('back_sev_ETH', float('nan')):.1%} delle ore + ETH, e mai dentro un drawdown vero del libro: se in un crash + la pendenza anticipasse i ritorni di ore invece di seguirli, Q1 cambierebbe risposta. Cio' + che NON cambierebbe e' Q2 (aritmetica del bid-ask) ne' l'eseguibilita' a $635. +""") + + +def main() -> None: + hr("r0822_term_structure β€” TERM STRUCTURE DELLA VOL IMPLICITA BTC/ETH") + chain = load_quoted_chain() + sezione0(chain) + atm_exp = build_atm_by_expiry(chain) + curve = build_curve(atm_exp) + sezione1(curve) + sezione2(curve) + sezione3(chain, atm_exp, curve) + del chain + sezione4(curve) + sezione5(curve) + sezione6() + sezione7() + hr("FINE") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_vol_size.py b/scripts/research/r0822_vol_size.py new file mode 100644 index 0000000..4c8a175 --- /dev/null +++ b/scripts/research/r0822_vol_size.py @@ -0,0 +1,867 @@ +#!/usr/bin/env python +"""r0822_vol_size.py β€” CONTROLLO DEL RISCHIO SU SKH01 e VOL-TARGET A LIVELLO DI LIBRO. + +DOMANDA (ondata 2026-08-22, filone VOL-SIZE). Il libro live Deribit e' TP01 75% + SKH01 25%, +nettati su un solo strumento. TP01 e' vol-targeted al 20% sulla vol REALIZZATA dell'asset; +**SKH01 non ha alcun controllo del rischio** β€” `backtest_signals(..., leverage=1.0, +position_size=1.0)`, verificato il 26/07: il suo 20,7% di vol realizzata e' un PRODOTTO della +strategia (uscite % asimmetriche + poco tempo a mercato), non un parametro. E' l'unica delle +cinque gambe senza dimensionamento, la piu' fee-sensibile (~4x TP01) e la piu' fragile +all'ancora (LOO 26/07). + + Q1 dare a SKH01 un dimensionamento del rischio (inverse-vol per-trade / rischio costante per + trade / vol-target sulla sua vol di strategia) migliora il LIBRO, o e' de-levering? + Q2 il vol-target va applicato al LIBRO gia' nettato invece che al singolo sleeve? Oggi ogni + sleeve si dimensiona da solo e poi si nettano: la versione book-level non e' mai stata + misurata. + +COSA NON SI RIFA' (gia' misurato): TP01 x DVOL (26/06, de-levering); il peso 75/25 (confermato +3 volte). I pesi NOMINALI non si toccano; una modulazione di size e' pero' un cambio di pesi nel +tempo, quindi `weights_tilt_null` si riporta lo stesso (Β§7). + +IPOTESI A PRIORI, REGISTRATA PRIMA DI MISURARE: **de-levering su quasi tutto**. Ragione +dichiarata prima: lo Sharpe e' INVARIANTE a una costante moltiplicativa, quindi una variante che +si limiti ad abbassare la leva media non puo' cambiare lo Sharpe e puo' solo abbassare il DD β€” +la firma esatta del null del de-levering (5 occorrenze nel progetto). Per VINCERE una variante +deve cambiare la FORMA (quale trade pesa quanto), e la forma si vede solo sullo Sharpe. +Sotto-ipotesi: l'inverse-vol per-trade e' l'unica con una ragione strutturale (lo stop di SKH01 +e' una PERCENTUALE FISSA 4%/2%, quindi il rischio in unita' di sigma varia col regime); il +vol-target sulla serie giornaliera dello sleeve e' quello che ha piu' probabilita' di rompersi +(SKH01 e' ~88% di zeri: la vol trailing di quella serie misura l'ATTIVITA', non il rischio). + +⚠️ DIFETTO TROVATO NELLA MIA PRIMA STESURA, E MISURATO INVECE CHE CANCELLATO (Β§4). Il modo +ovvio di scrivere un vol-target su uno sleeve β€” moltiplicare la sua serie GIORNALIERA per +L_t = tv/rv_{t-1} β€” e' **non causale su SKH01**, perche' la sua equity e' a GRADINO: tutto il +P&L di un trade e' contabilizzato il giorno di CHIUSURA. Scalare quel giorno per L_t significa +aver tenuto size L_t dall'INGRESSO, che e' fino a ~4 giorni prima (24 barre da 230m). L_t usa +solo dati <= t-1 e quindi passa qualunque controllo di causalita' scritto sulla serie +giornaliera: il look-ahead sta nella CONTABILITA', non nella formula. La versione causale fissa +la size all'INGRESSO. Entrambe sono misurate qui, e la differenza e' un risultato. + +LENTE E ANCORA. Path CANONICO (fill al livello, entry a chiusura di bin): e' la lente di tutti i +numeri pubblicati di SKH01 ed e' identica nelle due braccia di ogni confronto β€” il Delta e' +pulito anche se il livello assoluto e' pessimistico verso il path live (26/07: il live e' +migliore su ingressi E uscite). Banda d'ancora = **23 offset**, la griglia a priori dell'audit +02/07 (ogni 30m su [0,690)); statistica = **mediana delle DIFFERENZE APPAIATE** +(`A.anchor_luck_delta`), mai differenza delle mediane. L'ancora di TP01 e' tenuta canonica in +entrambe le braccia β€” Β§6 verifica che il segno non dipenda da lei. + +⚠️ IL CONTROLLO CHE DECIDE β€” ISO-PESO. Quasi ogni variante di size alza la VOL della gamba SKH, +e quindi il suo peso EFFETTIVO nel libro (i nominali restano 75/25). Ma "alzare il peso di SKH01" +e' gia' stato misurato e RESPINTO il 26/07 (argmax w=0.35, `weights_tilt_null` fallito: alzarlo non +compra Sharpe, compra dipendenza dall'ancora). Percio' il confronto pubblicato non e' vs il +baseline nudo ma vs il baseline **RI-SCALATO allo stesso peso effettivo** (colonne ISO): li' resta +solo la FORMA β€” quale trade pesa quanto. E' la regola dell'iso-rischio gia' codificata (25/07 Β§3). +Corollario: siccome lo Sharpe e' invariante alla scala, `sh(k*baseline) == sh(baseline)`, quindi il +null del de-levering in questa famiglia COINCIDE col confronto degli Sharpe. `k_iso` resta come +diagnostica di quanto si e' mosso il DD. + +DECOMPOSIZIONE DEL MECCANISMO (famiglia ACT). La vol trailing di uno sleeve all'88% di zeri +contiene DUE canali: la MAGNITUDINE degli esiti e la FREQUENZA dei trade. `VTL ... attive` isola +il primo (vol sulle sole barre attive), `ACT` isola il secondo (conteggio di barre attive nella +finestra). Se il guadagno vive in ACT la variante non e' "controllo del rischio" ma un filtro di +frequenza, e va detto. + +NULL DI PERMUTAZIONE (Β§10). Lo stesso MULTINSIEME di size riassegnato a caso ai trade: separa +"conta QUALE trade riceve quale size" (informazione) da "conta la DISTRIBUZIONE delle size". + +PRESENTAZIONE. Lo Sharpe e' invariante alla scala, maxDD e CAGR no: ogni variante e' riportata +anche a **ISO-VOL** (costante che pareggia la deviazione standard piena a quella del baseline). +E' una scelta di presentazione, post-hoc e dichiarata; non cambia lo Sharpe, che e' cio' che +decide. Il null del de-levering (`k_iso_dd`, importato da r0822_sol_leg, non riscritto) e' +calcolato per ogni variante che riduce il DD. + +USO: nice -n 19 timeout 900 uv run python scripts/research/r0822_vol_size.py + [--every K] 1 offset ogni K dei 23 (K=1 = tutti; pilota: --every 8) +""" +from __future__ import annotations + +import argparse +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import altlib as A # noqa: E402 +import r0702_anchor_skh01 as R # noqa: E402 (run_asset/resample_off riusati) +import r0702_tp01_offset as TO # noqa: E402 (TP01 per ancora + target 1h) +from r0822_sol_leg import k_iso_dd # noqa: E402 (null de-levering, non riscritto) +from src.portfolio.portfolio import ( # noqa: E402 + HOLDOUT, combine_outer, weights_tilt_null, +) +from src.backtest.harness import backtest_signals # noqa: E402 +from src.strategies.skyhook import ( # noqa: E402 + SKH01_V1, SKH01_V2_DD, skyhook_entries, +) + +ASSETS = ("BTC", "ETH") +OFFSETS_FULL = tuple(range(0, 690, 30)) # 23 a priori: griglia dell'audit 02/07 +LTF_MIN = 230 +BARS_YR = 365.25 * 24 * 60 / LTF_MIN +DPY = 365.25 +FEE_RT = 0.001 +W_TP, W_SKH = 0.75, 0.25 # libro live Deribit +CAPITAL = 635.0 # conto reale dichiarato nel brief +MIN_ORDER = 5.0 +CAP_ASSET = 0.5 # cap $318/asset su equity $635 +SIG_WARM = 2000 # barre 230m (~1 anno) prima di dimensionare +VT_WARM = 250 # giorni prima di dimensionare su vol di strategia + + +# --------------------------------------------------------------------------- metriche +def sh(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(r.mean() / r.std() * np.sqrt(DPY)) if len(r) > 2 and r.std() > 0 else 0.0 + + +def maxdd(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + if len(r) < 2: + return 0.0 + eq = np.cumprod(1.0 + r) + pk = np.maximum.accumulate(eq) + return float(np.max((pk - eq) / pk)) + + +def cagr(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + if len(r) < 2: + return 0.0 + eq = float(np.prod(1.0 + r)) + yrs = len(r) / DPY + return float(eq ** (1 / yrs) - 1) if eq > 0 and yrs > 0 else -1.0 + + +def vol(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(r.std() * np.sqrt(DPY)) if len(r) > 2 else 0.0 + + +def hold(s: pd.Series) -> pd.Series: + return s[s.index >= HOLDOUT] + + +def ins(s: pd.Series) -> pd.Series: + return s[s.index < HOLDOUT] + + +# --------------------------------------------------------------------------- estrazione trade +def extract(asset: str, off: int, params=None) -> dict: + """Trade + contesto per (asset, offset). Replica il loop di `backtest_signals` con + leverage=1: stessa detection, stesso non-overlap, stessi fill al livello. Cio' che + aggiunge e' la SIZE per-trade, che l'harness non espone (ha un solo `position_size` + scalare). La Β§0 verifica bit-exact che con size=1 si ritrovi la serie ufficiale.""" + if params is None or params is SKH01_V2_DD: + _, _, ltf, ent = R.run_asset(asset, off) # path canonico (sanity bit-exact Β§0) + else: # stessa pipeline, altra config Skyhook + df5 = R.get5m(asset) + ltf = R.resample_off(df5, R.LTF_MIN, off) + htf = R.resample_off(df5, R.HTF_MIN, off) + ent = skyhook_entries(ltf, htf, params) + c = ltf["close"].values.astype(float) + h = ltf["high"].values.astype(float) + lo = ltf["low"].values.astype(float) + n = len(c) + + i_ent, i_ex, dirs, nets, stops = [], [], [], [], [] + busy = -1 + for i in range(n): + e = ent[i] if i < len(ent) else None + if e is None or e.get("dir", 0) == 0 or i <= busy: + continue + d = int(e["dir"]) + entry = c[i] + tp, sl = e.get("tp"), e.get("sl") + mb = int(e.get("max_bars") or 24) + ex_i = min(i + mb, n - 1) + ex_p = c[ex_i] + for j in range(i + 1, min(i + mb + 1, n)): + hit_sl = sl is not None and ((d == 1 and lo[j] <= sl) or (d == -1 and h[j] >= sl)) + hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and lo[j] <= tp)) + if hit_sl: + ex_p, ex_i = sl, j + break + if hit_tp: + ex_p, ex_i = tp, j + break + ex_p, ex_i = c[j], j + i_ent.append(i); i_ex.append(ex_i); dirs.append(d) + nets.append((ex_p - entry) / entry * d - FEE_RT) + stops.append(abs(float(sl) - entry) / entry) # DERIVATO dal segnale, non ridichiarato + busy = ex_i + + idxE = np.asarray(i_ent, int) + lr = A.log_returns(c) + sig, sigref = {}, {} + for w in (30, 90): + sv = A.realized_vol(lr, w, BARS_YR) # finestra che termina in i: causale + sig[w] = sv[idxE] if len(idxE) else np.array([]) + sigref[w] = _expanding_median_at(sv, idxE) + idx = pd.DatetimeIndex(pd.to_datetime(ltf["timestamp"].values, unit="ms", utc=True)) + out = dict( + n=n, idx=idx, i_ent=idxE, i_ex=np.asarray(i_ex, int), dir=np.asarray(dirs, int), + net=np.asarray(nets, float), stop=np.asarray(stops, float), sig=sig, sigref=sigref, + ts_close=ltf["timestamp"].values.astype(np.int64) + LTF_MIN * 60_000, + day_ent=idx[idxE].floor("D") if len(idxE) else pd.DatetimeIndex([], tz="UTC"), + ) + out["base_daily"] = equity_daily(out, size_flat(out)) # riusata da VTL/Β§9: calcolarla 1 volta + R._CACHE.clear() # ent = 18k dict per chiave: non accumulare + return out + + +def _expanding_median_at(v: np.ndarray, idxE: np.ndarray) -> np.ndarray: + """Mediana ESPANDENTE di v fino a i incluso, valutata solo agli indici d'ingresso. + Causale per costruzione. NaN prima del warm-up.""" + out = np.full(len(idxE), np.nan) + for k, i in enumerate(idxE): + if i < SIG_WARM: + continue + x = v[: i + 1] + x = x[np.isfinite(x)] + if len(x) >= SIG_WARM // 2: + out[k] = float(np.median(x)) + return out + + +def equity_daily(ex: dict, sizes: np.ndarray) -> pd.Series: + """Serie giornaliera dei rendimenti dalla tabella trade, con size per-trade. Convenzione + IDENTICA a `backtest_signals` + `_skyhook_returns`: equity a gradino a fine trade, + resample 1D last, ffill, pct_change.""" + n = ex["n"] + eq = np.full(n, 1000.0, float) + cap = 1000.0 + for k in range(len(ex["i_ent"])): + cap += cap * float(sizes[k]) * float(ex["net"][k]) + cap = max(cap, 1.0) + eq[ex["i_ent"][k]: ex["i_ex"][k] + 1] = cap + for k in range(1, n): # stessi due riempimenti dell'harness + if eq[k] == 1000.0 and eq[k - 1] != 1000.0: + eq[k] = eq[k - 1] + last = 1000.0 + for k in range(n): + if eq[k] != last and eq[k] != 1000.0: + last = eq[k] + else: + eq[k] = last + return pd.Series(eq, index=ex["idx"]).resample("1D").last().ffill().pct_change().dropna() + + +def leg_daily(exs: dict, sizes: dict) -> pd.Series: + """Sleeve SKH01 50/50 BTC+ETH (convenzione di `_skyhook_returns`: inner join).""" + ser = {a: equity_daily(exs[a], sizes[a]) for a in ASSETS} + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + return pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index) + + +# --------------------------------------------------------------------------- famiglie di size +def size_flat(ex: dict) -> np.ndarray: + return np.ones(len(ex["i_ent"])) + + +def size_iv(p: float, w: int, cap: float): + """INVERSE-VOL per-trade sulla vol dell'ASSET: size = clip((sig_rif/sig_ingresso)^p, 1/cap, cap). + sig_rif = mediana ESPANDENTE causale -> size media ~1: la variante e' neutra alla leva, + quindi qualunque effetto sullo Sharpe e' FORMA, non de-levering.""" + def f(ex: dict) -> np.ndarray: + s, r = ex["sig"][w], ex["sigref"][w] + with np.errstate(divide="ignore", invalid="ignore"): + raw = np.where((s > 0) & np.isfinite(r), (r / np.maximum(s, 1e-12)) ** p, 1.0) + return np.clip(np.nan_to_num(raw, nan=1.0, posinf=cap, neginf=1.0), 1.0 / cap, cap) + return f + + +def size_risk(p: float): + """RISCHIO COSTANTE PER TRADE: size ~ 1/distanza-dallo-stop. Lo stop di SKH01 e' una + percentuale FISSA (4% long / 2% short) -> questa famiglia e' di fatto 'gli short pesano + 2x i long'. La costante e' irrilevante (lo Sharpe e' scale-invariante).""" + def f(ex: dict) -> np.ndarray: + st = np.maximum(ex["stop"], 1e-9) + return np.where(ex["stop"] > 0, (0.04 / st) ** p, 1.0) + return f + + +def _rv_daily(s: pd.Series, w: int, active_only: bool) -> pd.Series: + """Vol realizzata trailing di una serie GIORNALIERA, SFASATA di 1 giorno (causale al giorno t). + active_only=True la calcola sulle sole barre ATTIVE e la riporta sulla griglia di calendario: + SKH01 e' ~88% di zeri, e la versione 'tutte le barre' misura l'ATTIVITA', non il rischio.""" + if not active_only: + return (s.rolling(w, min_periods=max(5, w // 3)).std() * np.sqrt(DPY)).shift(1) + act = s[s != 0.0] + rv = act.rolling(w, min_periods=max(5, w // 3)).std() * np.sqrt(DPY) + return rv.reindex(s.index).ffill().shift(1) + + +def leverage_series(base: pd.Series, tv: float | None, w: int, cap: float, + active_only: bool = False) -> pd.Series: + """L_t da una serie giornaliera di riferimento. tv=None -> versione NEUTRA ALLA LEVA + (rif = mediana espandente causale di rv), tv=float -> vol-target assoluto.""" + rv = _rv_daily(base, w, active_only).replace(0.0, np.nan) + if tv is None: + ref = rv.expanding(min_periods=VT_WARM).median() + L = (ref / rv).clip(lower=1.0 / cap, upper=cap) + else: + L = (tv / rv).clip(lower=0.0, upper=cap) + return L.fillna(1.0) + + +def activity_L(base: pd.Series, w: int, cap: float) -> pd.Series: + """L_t da sola ATTIVITA': quante barre ATTIVE (trade chiusi) negli ultimi w giorni, riferita + alla mediana espandente causale. Serve a separare i DUE canali dentro `rv` calcolata su tutte + le barre di uno sleeve all'88% di zeri: la MAGNITUDINE degli esiti e la FREQUENZA dei trade. + Se il guadagno vive qui, la variante non e' 'vol-target' ma un filtro di frequenza.""" + act = (base != 0.0).astype(float) + cnt = act.rolling(w, min_periods=max(5, w // 3)).sum().shift(1).replace(0.0, np.nan) + ref = cnt.expanding(min_periods=VT_WARM).median() + return (ref / cnt).clip(lower=1.0 / cap, upper=cap).fillna(1.0) + + +def sizes_from_L(ex: dict, L: pd.Series) -> np.ndarray: + """La size di ogni trade = L al giorno d'INGRESSO. E' la versione CAUSALE del vol-target su + uno sleeve a equity a gradino: L al giorno di CHIUSURA non era nota all'ingresso.""" + if not len(ex["i_ent"]): + return np.array([]) + v = L.reindex(ex["day_ent"]).values.astype(float) + return np.nan_to_num(v, nan=1.0) + + +# --------------------------------------------------------------------------- libro +def sizes_of(nm, ex: dict, SZ: dict, VTL: dict, ACT: dict, Lbook: pd.Series | None = None): + """Vettore di size per-trade di una cella qualsiasi. Un solo posto in cui e' definita: + Β§8 (eseguibilita'), Β§9 (deriva) e Β§10 (null di permutazione) devono usare LA STESSA.""" + if nm is None or nm == "BASE": + return size_flat(ex) + if nm in SZ: + return SZ[nm](ex) + if nm in VTL: + return sizes_from_L(ex, leverage_series(ex["base_daily"], **VTL[nm])) + if nm in ACT: + return sizes_from_L(ex, activity_L(ex["base_daily"], **ACT[nm])) + return sizes_from_L(ex, Lbook) + + +def book(tp: pd.Series, skh: pd.Series) -> pd.Series: + return combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": W_TP, "SKH01": W_SKH}) + + +def iso_vol(s: pd.Series, ref: pd.Series) -> pd.Series: + v = vol(s) + return s * (vol(ref) / v) if v > 0 else s + + +HEAD = (f" {'variante':<28}{'ShFULL':>8}{'ShHOLD':>9}{'ShIS':>8}" + f"{'vol':>9}{'maxDD':>9}{'DDisovol':>10}{'CAGRiso':>9}{'size med':>10}") + + +def transfer_test(offs) -> None: + """TEST DI TRASFERIMENTO β€” la regola di size CONGELATA (tv=0.20, w=90, cap 3, fissata + all'INGRESSO) applicata a SKH01_**V1**, l'altra config Skyhook: stesso motore, stessi asset, + ma geometria d'uscita DIVERSA (stop/target in ATR 2.5/6.0 invece che a percentuale fissa + 4%/2%, ptn_n=55 invece di 45). E' il gate giusto per questo filone: un forward di 6 mesi non + puo' misurare un dSharpe di 0.07 (SE ~1.4), un trasferimento si'. Se il meccanismo e' + 'pareggiare il contributo di rischio dei trade' deve funzionare anche li'; se e' cucito sulla + sequenza di trade di V2-DD, no. NB: V1 e' l'unica config alternativa gia' nel repo e non + scelta da me β€” non e' una griglia nuova.""" + print("=" * 112) + print(" TEST DI TRASFERIMENTO β€” regola di size CONGELATA da V2-DD, applicata a SKH01_V1") + print("=" * 112) + TP = A.tp01_baseline_daily() + sp = dict(tv=0.20, w=90, cap=3.0, active_only=False) + base_b, var_b, ctrl_b = {}, {}, {} + for o in offs: + ex = {a: extract(a, o, SKH01_V1) for a in ASSETS} + lb = leg_daily(ex, {a: size_flat(ex[a]) for a in ASSETS}) + lv = leg_daily(ex, {a: sizes_from_L(ex[a], leverage_series(ex[a]["base_daily"], **sp)) + for a in ASSETS}) + m_s = vol(lv) / vol(lb) if vol(lb) > 0 else 1.0 + base_b[o], var_b[o] = book(TP, lb), book(TP, lv) + ctrl_b[o] = book(TP, lb * m_s) + dF = A.anchor_luck_delta(lambda o: var_b[o], lambda o: ctrl_b[o], offs, metric=sh) + dH = A.anchor_luck_delta(lambda o: hold(var_b[o]), lambda o: hold(ctrl_b[o]), offs, metric=sh) + rF = A.anchor_luck_delta(lambda o: var_b[o], lambda o: base_b[o], offs, metric=sh) + print(f" trade V1 medi/ancora: " + ", ".join( + f"{a} {int(np.mean([len(extract(a, offs[0], SKH01_V1)['i_ent'])]))}" for a in ASSETS)) + print(f" libro V1 BASE, mediana della banda: ShFULL " + f"{np.median([sh(base_b[o]) for o in offs]):.3f} / ShHOLD " + f"{np.median([sh(hold(base_b[o])) for o in offs]):.3f}") + print(f" dShFULL grezzo {rF['median_paired']:+.3f} ({rF['n_positive']}/{len(offs)} ancore)") + print(f" dShFULL ISO-PESO {dF['median_paired']:+.3f} ({dF['n_positive']}/{len(offs)} ancore)" + f" banda [{dF['lo']:+.3f}, {dF['hi']:+.3f}]") + print(f" dShHOLD ISO-PESO {dH['median_paired']:+.3f} ({dH['n_positive']}/{len(offs)} ancore)") + yrs = sorted({int(y) for y in base_b[offs[0]].index.year}) + cells = [] + for y in yrs: + v = [sh(var_b[o][var_b[o].index.year == y]) - sh(ctrl_b[o][ctrl_b[o].index.year == y]) + for o in offs if (var_b[o].index.year == y).sum() > 20] + cells.append(float(np.median(v)) if v else float("nan")) + print(" per anno (ISO): " + "".join(f"{y:>8}" for y in yrs)) + print(" " + "".join(f"{c:>+8.2f}" for c in cells) + + f" positivi {int(np.nansum(np.asarray(cells) > 0))}/{len(yrs)}") + print("\n LETTURA: se il segno regge qui, il meccanismo non e' cucito su V2-DD. Se non regge,") + print(" la regola e' una proprieta' della sequenza di trade su cui e' stata trovata.") + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--every", type=int, default=1) + ap.add_argument("--transfer", action="store_true", + help="esegue SOLO il test di trasferimento su SKH01_V1") + args = ap.parse_args() + if args.transfer: + transfer_test(OFFSETS_FULL[:: max(1, args.every)]) + return + offs = OFFSETS_FULL[:: max(1, args.every)] + t0 = time.time() + + print("=" * 112) + print(" r0822 VOL-SIZE β€” controllo del rischio su SKH01 (Q1) e vol-target di LIBRO (Q2)") + print("=" * 112) + print(f" ancore: {len(offs)}/{len(OFFSETS_FULL)} offset a priori {list(offs)}") + print(" lente: path CANONICO. Ancora TP01: canonica in ENTRAMBE le braccia (Β§6 la varia).") + + print("\n estrazione tabelle trade ...", flush=True) + EX = {o: {a: extract(a, o) for a in ASSETS} for o in offs} + print(f" fatto in {time.time()-t0:.0f}s β€” trade medi/ancora: " + + ", ".join(f"{a} {int(np.mean([len(EX[o][a]['i_ent']) for o in offs]))}" for a in ASSETS)) + + # ------------------------------------------------------------------ Β§0 SANITY + print("\n" + "-" * 112) + print(" 0. SANITY β€” con size=1 la ricomposizione DEVE riprodurre la serie ufficiale bit-exact") + print(" (se non e' 0, ogni numero sotto e' di un'altra strategia)") + print("-" * 112) + worst = 0.0 + for o in offs[: min(3, len(offs))]: + for a in ASSETS: + mine = equity_daily(EX[o][a], size_flat(EX[o][a])) + ref = R.run_asset(a, o)[0] + R._CACHE.clear() + assert len(mine) == len(ref), f"len {len(mine)} vs {len(ref)} ({a},{o})" + d = float(np.max(np.abs(mine.values - ref.values))) + worst = max(worst, d) + print(f" {a} off{o:>3}: max|dif| = {d:.2e} ({len(mine)} giorni)") + assert worst < 1e-15, f"ricomposizione NON bit-exact: {worst:.2e}" + if 0 in EX: + from src.portfolio.sleeves import _skyhook_returns + s0 = leg_daily(EX[0], {a: size_flat(EX[0][a]) for a in ASSETS}) + d0 = float(np.max(np.abs(s0.values - _skyhook_returns().values))) + print(f" sleeve 50/50 off0 vs sleeves._skyhook_returns(): max|dif| = {d0:.2e}") + assert d0 < 1e-15 + + # ------------------------------------------------------------------ Β§1 CAUSALITA' + print("\n" + "-" * 112) + print(" 1. CAUSALITA' β€” ogni size ricalcolata TRONCANDO i dati alla barra d'ingresso") + print("-" * 112) + ex = EX[offs[0]]["BTC"] + _, _, ltf, _ = R.run_asset("BTC", offs[0]) + c_full = ltf["close"].values.astype(float) + R._CACHE.clear() + full_sz = size_iv(1.0, 30, 3.0)(ex) + sel = [k for k in range(len(ex["i_ent"])) if ex["i_ent"][k] >= SIG_WARM][:40] + bad = 0 + for k in sel: + i = ex["i_ent"][k] + sv_t = A.realized_vol(A.log_returns(c_full[: i + 1]), 30, BARS_YR) + s_t = sv_t[i] + r_t = float(np.median(sv_t[np.isfinite(sv_t)])) + sz_t = np.clip((r_t / s_t) if s_t > 0 else 1.0, 1 / 3.0, 3.0) + bad += int(abs(sz_t - full_sz[k]) > 1e-9) + print(f" IV: {len(sel)-bad}/{len(sel)} size identiche a dati troncati (divergenze {bad})") + assert bad == 0, "la size IV usa dati futuri" + lag = np.array([(ex["idx"][ex["i_ex"][k]] - ex["idx"][ex["i_ent"][k]]).total_seconds() / 86400 + for k in range(len(ex["i_ent"]))]) + print(f" durata dei trade (giorni): mediana {np.median(lag):.2f}, p90 {np.percentile(lag,90):.2f}, " + f"max {lag.max():.2f} <- e' l'ampiezza del look-ahead della versione NAIVE (Β§4)") + + # ------------------------------------------------------------------ griglia + SZ: dict = {} + for p in (0.5, 1.0): + for w in (30, 90): + for cp in (2.0, 4.0): + SZ[f"IV p{p} w{w} cap{cp:.0f}"] = size_iv(p, w, cp) + for p in (0.5, 1.0): + SZ[f"RISK p{p}"] = size_risk(p) + VTL = {} # vol-target sulla vol di STRATEGIA, causale + for w in (30, 90): + VTL[f"VTL neutro w{w}"] = dict(tv=None, w=w, cap=3.0, active_only=False) + VTL[f"VTL tv20 w{w}"] = dict(tv=0.20, w=w, cap=3.0, active_only=False) + VTL["VTL neutro w90 attive"] = dict(tv=None, w=90, cap=3.0, active_only=True) + VTL["VTL tv20 w90 attive"] = dict(tv=0.20, w=90, cap=3.0, active_only=True) + BVT = {} # Q2: vol-target del LIBRO + for w in (30, 90, 180): + for cp in (2.0, 3.0): + BVT[f"BOOKVT w{w} cap{cp:.0f}"] = dict(tv=0.10, w=w, cap=cp) + ACT = {f"ACT w{w}": dict(w=w, cap=3.0) for w in (90, 180)} + NAIVE = {f"NAIVE-VT w{w}": dict(tv=0.20, w=w, cap=3.0, active_only=False) for w in (30, 90)} + ALL = list(SZ) + list(VTL) + list(ACT) + list(BVT) + list(NAIVE) + print(f"\n GRIGLIA DICHIARATA: {len(SZ)} size per-trade + {len(VTL)} vol-target di gamba + " + f"{len(ACT)} filtro-attivita' + {len(BVT)} vol-target di libro + {len(NAIVE)} controlli " + f"non-causali = {len(ALL)} celle (+ baseline).") + + # ------------------------------------------------------------------ serie + TP = A.tp01_baseline_daily() + SLE = {"BASE": {}} + BK = {"BASE": {}} + MTP = {"BASE": {o: 1.0 for o in offs}} # rapporto di vol della gamba TP01 + SIZEMED = {} + for o in offs: + SLE["BASE"][o] = leg_daily(EX[o], {a: size_flat(EX[o][a]) for a in ASSETS}) + BK["BASE"][o] = book(TP, SLE["BASE"][o]) + for nm, fn in SZ.items(): + SLE[nm] = {o: leg_daily(EX[o], {a: fn(EX[o][a]) for a in ASSETS}) for o in offs} + BK[nm] = {o: book(TP, SLE[nm][o]) for o in offs} + MTP[nm] = {o: 1.0 for o in offs} + SIZEMED[nm] = float(np.median(np.concatenate([fn(EX[offs[0]][a]) for a in ASSETS]))) + for nm, sp in VTL.items(): + SLE[nm], BK[nm], MTP[nm] = {}, {}, {} + for o in offs: + sz = {} + for a in ASSETS: + sz[a] = sizes_from_L(EX[o][a], leverage_series(EX[o][a]["base_daily"], **sp)) + SLE[nm][o] = leg_daily(EX[o], sz) + BK[nm][o] = book(TP, SLE[nm][o]) + MTP[nm][o] = 1.0 + if o == offs[0]: + SIZEMED[nm] = float(np.median(np.concatenate(list(sz.values())))) + for nm, sp in ACT.items(): + SLE[nm], BK[nm], MTP[nm] = {}, {}, {} + for o in offs: + sz = {a: sizes_from_L(EX[o][a], activity_L(EX[o][a]["base_daily"], **sp)) + for a in ASSETS} + SLE[nm][o] = leg_daily(EX[o], sz) + BK[nm][o] = book(TP, SLE[nm][o]) + MTP[nm][o] = 1.0 + if o == offs[0]: + SIZEMED[nm] = float(np.median(np.concatenate(list(sz.values())))) + for nm, sp in BVT.items(): + SLE[nm], BK[nm], MTP[nm] = {}, {}, {} + for o in offs: + L = leverage_series(BK["BASE"][o], sp["tv"], sp["w"], sp["cap"]) + sz = {a: sizes_from_L(EX[o][a], L) for a in ASSETS} + SLE[nm][o] = leg_daily(EX[o], sz) + tp_s = TP * L.reindex(TP.index).ffill().fillna(1.0) + BK[nm][o] = book(tp_s, SLE[nm][o]) + MTP[nm][o] = vol(tp_s) / vol(TP) + if o == offs[0]: + SIZEMED[nm] = float(np.median(np.concatenate(list(sz.values())))) + for nm, sp in NAIVE.items(): # controllo NON causale (vedi Β§4) + SLE[nm] = {o: SLE["BASE"][o] * leverage_series(SLE["BASE"][o], **sp) for o in offs} + BK[nm] = {o: book(TP, SLE[nm][o]) for o in offs} + MTP[nm] = {o: 1.0 for o in offs} + SIZEMED[nm] = float(np.median(leverage_series(SLE["BASE"][offs[0]], **sp).values)) + + # CONTROLLO ISO-PESO: ogni variante che alza la vol della gamba SKH alza il suo peso EFFETTIVO + # nel libro β€” e "alzare il peso di SKH01" e' gia' stato misurato (26/07: argmax w=0.35, ma + # `weights_tilt_null` FALLITO e proposta respinta). Quindi il confronto che conta NON e' vs il + # baseline nudo ma vs il baseline RI-SCALATO agli stessi rapporti di vol: cosi' resta solo la + # FORMA (quale trade pesa quanto). E' la regola dell'ISO-RISCHIO gia' codificata (25/07 Β§3). + CTRL, WEFF = {}, {} + for nm in ALL: + CTRL[nm], WEFF[nm] = {}, {} + for o in offs: + m_s = vol(SLE[nm][o]) / vol(SLE["BASE"][o]) if vol(SLE["BASE"][o]) > 0 else 1.0 + m_t = MTP[nm][o] + CTRL[nm][o] = book(TP * m_t, SLE["BASE"][o] * m_s) + WEFF[nm][o] = W_SKH * m_s / (W_TP * m_t + W_SKH * m_s) + + # ------------------------------------------------------------------ Β§2 sleeve + can = offs[0] + print("\n" + "-" * 112) + print(f" 2. SLEEVE SKH01 all'ancora canonica (off {can}). 'DDisovol'/'CAGRiso' a vol pareggiata") + print(" col baseline (post-hoc, dichiarato: lo Sharpe non ne dipende).") + print("-" * 112) + print(HEAD) + base_s = SLE["BASE"][can] + + def prow(nm, s, med=None): + iv = iso_vol(s, base_s) + m = f"{med:>10.2f}" if med is not None else f"{'-':>10}" + print(f" {nm:<28}{sh(s):>8.3f}{sh(hold(s)):>9.3f}{sh(ins(s)):>8.3f}{vol(s)*100:>8.1f}%" + f"{maxdd(s)*100:>8.1f}%{maxdd(iv)*100:>9.1f}%{cagr(iv)*100:>8.1f}%{m}") + + prow("BASELINE (size fissa)", base_s, 1.00) + for nm in list(SZ) + list(VTL) + list(ACT) + list(NAIVE): + prow(nm, SLE[nm][can], SIZEMED.get(nm)) + + # ------------------------------------------------------------------ Β§3 libro, banda appaiata + print("\n" + "-" * 112) + print(f" 3. LIBRO 75/25 β€” mediana delle DIFFERENZE APPAIATE su {len(offs)} ancore, + null del") + print(" de-levering (k<1 sul BASELINE che pareggia il maxDD della variante)") + print("-" * 112) + print(f" {'variante':<28}{'wSKH eff':>9}{'dShFULL':>9}{'pos/n':>8}{'dShHOLD':>9}{'pos/n':>8}" + f"{'| ISO dF':>10}{'pos/n':>8}{'ISO dH':>9}{'pos/n':>8}{'dDDpp':>8}{'k_iso':>8}" + f"{'null de-lev':>14}") + base_b = BK["BASE"] + RES = {} + for nm in ALL: + dF = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], lambda o: base_b[o], offs, metric=sh) + dH = A.anchor_luck_delta(lambda o, n=nm: hold(BK[n][o]), lambda o: hold(base_b[o]), + offs, metric=sh) + iF = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], lambda o, n=nm: CTRL[n][o], + offs, metric=sh) + iH = A.anchor_luck_delta(lambda o, n=nm: hold(BK[n][o]), + lambda o, n=nm: hold(CTRL[n][o]), offs, metric=sh) + dd = float(np.median([maxdd(BK[nm][o]) - maxdd(base_b[o]) for o in offs])) * 100 + if maxdd(BK[nm][can]) < maxdd(base_b[can]) - 1e-6: + k = k_iso_dd(base_b[can], BK[nm][can]) + shk = sh(k * base_b[can]) + ks = f"{k:.3f}" + vd = "DE-LEVERING" if shk >= sh(BK[nm][can]) else "oltre-null" + else: + ks, vd = "n/a", "DD peggiore" + we = float(np.median([WEFF[nm][o] for o in offs])) + RES[nm] = dict(dF=dF["median_paired"], nF=dF["n_positive"], dH=dH["median_paired"], + nH=dH["n_positive"], iF=iF["median_paired"], niF=iF["n_positive"], + iH=iH["median_paired"], niH=iH["n_positive"], dd=dd, k=ks, + verdict=vd, weff=we) + print(f" {nm:<28}{we:>9.3f}{dF['median_paired']:>+9.3f}{dF['n_positive']:>5}/{len(offs):<3}" + f"{dH['median_paired']:>+9.3f}{dH['n_positive']:>5}/{len(offs):<3}" + f"{iF['median_paired']:>+10.3f}{iF['n_positive']:>5}/{len(offs):<3}" + f"{iH['median_paired']:>+9.3f}{iH['n_positive']:>5}/{len(offs):<3}{dd:>+8.2f}{ks:>8}" + f"{vd:>14}") + print(" ⚠️ NOTA sul null del de-levering: `sh(k*base)` e' IDENTICO a `sh(base)` perche' lo") + print(" Sharpe e' invariante a una costante -> in questa forma il null COINCIDE col confronto") + print(" degli Sharpe, ed e' per questo che qui lo Sharpe e' la metrica che decide e il DD no.") + print(" 'k_iso' resta come diagnostica: quanta leva dovrebbe cedere il baseline per pareggiare") + print(" il maxDD della variante. 'DE-LEVERING' = la variante NON batte quel baseline ridotto.") + print(" wSKH eff = peso EFFETTIVO di SKH nel libro (i nominali restano 75/25: la size lo") + print(" muove). Colonne 'ISO' = confronto vs il baseline RI-SCALATO a quello stesso peso:") + print(" e' li' che si legge la FORMA. Le colonne non-ISO contengono anche il cambio di peso.") + med_F = float(np.median([sh(base_b[o]) for o in offs])) + med_H = float(np.median([sh(hold(base_b[o])) for o in offs])) + med_D = float(np.median([maxdd(base_b[o]) for o in offs])) + print(f"\n libro BASE, ancora canonica: ShFULL {sh(base_b[can]):.3f} / ShHOLD " + f"{sh(hold(base_b[can])):.3f} / maxDD {maxdd(base_b[can])*100:.1f}% / " + f"CAGR {cagr(base_b[can])*100:.1f}%") + print(f" libro BASE, stima ONESTA (mediana della banda): ShFULL {med_F:.3f} / " + f"ShHOLD {med_H:.3f} / maxDD {med_D*100:.1f}%") + + # ------------------------------------------------------------------ Β§4 il difetto misurato + print("\n" + "-" * 112) + print(" 4. IL DIFETTO MISURATO β€” vol-target NAIVE (scala il giorno di CHIUSURA) vs CAUSALE") + print(" (size fissata all'INGRESSO). Stessa formula di L, stessa finestra: cambia solo") + print(" QUANDO la si applica. La differenza e' il valore del look-ahead di contabilita'.") + print("-" * 112) + print(f" {'coppia':<34}{'dShFULL naive':>15}{'dShFULL causale':>17}{'phantom':>10}") + for w in (30, 90): + n_nm, c_nm = f"NAIVE-VT w{w}", f"VTL tv20 w{w}" + a1, a2 = RES[n_nm]["dF"], RES[c_nm]["dF"] + print(f" {'vol-target tv20 w%d' % w:<34}{a1:>+15.3f}{a2:>+17.3f}{a1-a2:>+10.3f}") + for w in (30, 90): + n_nm, c_nm = f"NAIVE-VT w{w}", f"VTL tv20 w{w}" + a1, a2 = RES[n_nm]["dH"], RES[c_nm]["dH"] + print(f" {' (hold-out) tv20 w%d' % w:<34}{a1:>+15.3f}{a2:>+17.3f}{a1-a2:>+10.3f}") + + # ------------------------------------------------------------------ Β§5 selezione + DSR + print("\n" + "-" * 112) + print(" 5. SELEZIONE AL BUIO (solo pre-2025, mediana di banda) + deflated-Sharpe") + print("-" * 112) + CAUSAL = [n for n in ALL if n not in NAIVE] + is_med = {n: float(np.median([sh(ins(BK[n][o])) for o in offs])) for n in CAUSAL} + ho_med = {n: float(np.median([sh(hold(BK[n][o])) for o in offs])) for n in CAUSAL} + fu_med = {n: float(np.median([sh(BK[n][o]) for o in offs])) for n in CAUSAL} + base_is = float(np.median([sh(ins(base_b[o])) for o in offs])) + print(f" baseline in-sample (mediana banda): {base_is:.3f}") + for n in sorted(CAUSAL, key=lambda x: -is_med[x])[:6]: + print(f" {n:<28} IS {is_med[n]:>7.3f} HOLD {ho_med[n]:>7.3f} FULL {fu_med[n]:>7.3f}") + best = max(CAUSAL, key=lambda x: is_med[x]) + print(f"\n cella scelta AL BUIO: {best} (IS {is_med[best]:.3f} vs baseline {base_is:.3f})") + iso_is = {n: float(np.median([sh(ins(BK[n][o])) - sh(ins(CTRL[n][o])) for o in offs])) + for n in CAUSAL} + best_iso = max(CAUSAL, key=lambda x: iso_is[x]) + print(f" cella scelta AL BUIO **a ISO-PESO**: {best_iso} (dIS iso {iso_is[best_iso]:+.3f})") + print(f" la stessa cella a iso-peso: dShFULL {RES[best]['iF']:+.3f} " + f"({RES[best]['niF']}/{len(offs)}), dShHOLD {RES[best]['iH']:+.3f} " + f"({RES[best]['niH']}/{len(offs)})") + trials = [fu_med[n] for n in CAUSAL] + [med_F] + dsr, sr0 = A.deflated_sharpe(fu_med[best], trials, BK[best][can]) + dsr_b, _ = A.deflated_sharpe(med_F, trials, base_b[can]) + print(f" DSR candidato {dsr:.3f} / DSR BASELINE {dsr_b:.3f} (null max-Sharpe {sr0:.3f}, " + f"N={len(trials)})") + print(f" dispersione degli {len(trials)} trial: sd {np.std(trials, ddof=1):.4f} -> " + f"⚠️ il DSR e' QUASI VACUO su una famiglia di perturbazioni della stessa strategia") + print(" (penalizza in proporzione alla varianza fra i trial: qui il baseline stesso lo") + print(" passa). Il gate che decide e' la banda appaiata del Β§3, non questo.") + + # ------------------------------------------------------------------ Β§6 ancora TP01 + print("\n" + "-" * 112) + print(" 6. CONTROLLO β€” l'ancora di TP01 e' tenuta fissa: il segno del Delta dipende da lei?") + print("-" * 112) + cand6 = [best] + [n for n in (list(SZ)[:0] + ["RISK p1.0"] + list(BVT)[:2]) if n != best] + print(f" {'variante':<28}" + "".join(f"{'TP h=%d' % h:>12}" for h in (0, 8, 16))) + for nm in cand6: + cells = [] + for h in (0, 8, 16): + tph = TO.port_daily_native(h) if h else TP + if nm in BVT: + arm = {} + for o in offs: + L = leverage_series(book(tph, SLE["BASE"][o]), BVT[nm]["tv"], BVT[nm]["w"], + BVT[nm]["cap"]) + sz = {a: sizes_from_L(EX[o][a], L) for a in ASSETS} + arm[o] = book(tph * L.reindex(tph.index).ffill().fillna(1.0), + leg_daily(EX[o], sz)) + else: + arm = {o: book(tph, SLE[nm][o]) for o in offs} + bse = {o: book(tph, SLE["BASE"][o]) for o in offs} + d = A.anchor_luck_delta(lambda o, x=arm: x[o], lambda o, y=bse: y[o], offs, metric=sh) + cells.append(d["median_paired"]) + print(f" {nm:<28}" + "".join(f"{v:>+12.3f}" for v in cells)) + + # ------------------------------------------------------------------ Β§7 gate + print("\n" + "-" * 112) + print(" 7. GATE β€” marginal_vs_tp01 / implausible_sharpe / weights_tilt_null") + print("-" * 112) + best_sleeve = max([n for n in list(SZ) + list(VTL) + list(ACT)], key=lambda x: is_med[x]) + for nm in ("BASE", best_sleeve): + m = A.marginal_vs_tp01(SLE[nm][can]) + b25 = m.get("blends", {}).get("w25", {}) + print(f" marginal {nm:<26} {m.get('marginal_verdict')} corr {m.get('corr_full')} " + f"uplift w25 full {b25.get('uplift_full')} / hold {b25.get('uplift_hold')}") + nlos = int((EX[can]["BTC"]["net"] < 0).sum() + (EX[can]["ETH"]["net"] < 0).sum()) + ntot = int(len(EX[can]["BTC"]["net"]) + len(EX[can]["ETH"]["net"])) + for nm in ("BASE", best_sleeve): + imp = A.implausible_sharpe(SLE[nm][can], n_trades=ntot, n_losing_trades=nlos) + print(f" implausible {nm:<25} implausible={imp['implausible']} " + f"attive {imp.get('active_frac', 0)*100:.1f}% perdite/attive " + f"{imp.get('loss_frac', 0)*100:.1f}% Calmar {imp.get('calmar', 0):.1f}") + cols = {"TP01": TP, "SKH01": SLE["BASE"][can]} + for nm in dict.fromkeys([best, best_sleeve] + list(BVT)[:2]): + m_s = vol(SLE[nm][can]) / vol(SLE["BASE"][can]) + wp = {"TP01": W_TP * MTP[nm][can], "SKH01": W_SKH * m_s} + g = weights_tilt_null(cols, {"TP01": W_TP, "SKH01": W_SKH}, wp, + caps={"SKH01": 0.5}, floor=0.05, n=300, k_seen=len(ALL)) + print(f" tilt-null {nm:<26} peso equiv SKH {wp['SKH01']/sum(wp.values()):.3f} " + f"d_IS {g['delta_insample']:+.4f} d_HOLD {g['delta_hold']:+.4f} " + f"pctl {g['pctl_hold']:.1f} gate_pass={g['gate_pass']}") + print(" LIMITE DICHIARATO: il gate confronta vettori di pesi STATICI; della modulazione") + print(" cattura la sola componente di SCALA. La FORMA la giudica la banda appaiata (Β§3).") + + # ------------------------------------------------------------------ Β§8 eseguibilita' + print("\n" + "-" * 112) + print(f" 8. ESEGUIBILITA' a ${CAPITAL:.0f} β€” min-order ${MIN_ORDER:.0f}, cap {CAP_ASSET:.0%}" + f" equity/asset, target NETTATO 0.75*TP01 + 0.25*SKH sul grid 1h (cio' che il cron manda)") + print("-" * 112) + print(f" {'configurazione':<28}{'Sh model':>10}{'Sh reale':>10}{'haircut':>9}" + f"{'ordini eseg':>13}{'sotto-min':>11}{'turnover/a':>12}") + todo = [("BASE (size fissa)", None), (best_sleeve, best_sleeve), (list(BVT)[0], list(BVT)[0])] + for label, nm in todo: + tm = tr = tu = 0.0 + no = ns = 0 + for a in ASSETS: + df1h = TO.get1h(a) + tgt = netted_target(a, EX[can][a], nm, SZ, VTL, ACT, BVT, SLE, TP, can, df1h) + ev = A.eval_weights_smallcap(df1h, tgt, capital=CAPITAL, min_order=MIN_ORDER) + tm += 0.5 * ev["modeled"]["sharpe"] + tr += 0.5 * ev["realistic"]["sharpe"] + tu += ev["executed_turnover_per_year"] + no += ev["n_executed_trades"] + dw = np.abs(np.diff(np.nan_to_num(tgt), prepend=0.0)) + ns += int(((dw > 1e-12) & (dw * CAPITAL < MIN_ORDER)).sum()) + print(f" {label:<28}{tm:>10.3f}{tr:>10.3f}{tm-tr:>9.3f}{no:>13}{ns:>11}{tu:>12.1f}") + print(" 'ordini eseg' e 'sotto-min' sono sui due asset sommati, su tutta la storia 1h.") + + # ------------------------------------------------------------------ Β§9 per anno + deriva + print("\n" + "-" * 112) + print(" 9. SCOMPOSIZIONE PER ANNO del dSharpe a ISO-PESO (regola codificata dopo SOL, 22/08:") + print(" un contributo positivo si scompone per ANNO prima di crederci) + deriva della size") + print("-" * 112) + yrs = sorted({int(y) for y in BK["BASE"][can].index.year}) + best_iv = max([n for n in SZ if n.startswith("IV")], key=lambda x: RES[x]["iF"]) + best_rk = max([n for n in SZ if n.startswith("RISK")], key=lambda x: RES[x]["iF"]) + top = list(dict.fromkeys([best, best_iso, best_iv, best_rk, + max(ACT, key=lambda x: RES[x]["iF"]), + max(BVT, key=lambda x: RES[x]["iF"])])) + print(f" {'variante':<28}" + "".join(f"{y:>8}" for y in yrs)) + for nm in top: + cells = [] + for y in yrs: + v = [] + for o in offs: + a_ = BK[nm][o][BK[nm][o].index.year == y] + c_ = CTRL[nm][o][CTRL[nm][o].index.year == y] + if len(a_) > 20: + v.append(sh(a_) - sh(c_)) + cells.append(float(np.median(v)) if v else float("nan")) + npos = int(np.nansum(np.asarray(cells) > 0)) + print(f" {nm:<28}" + "".join(f"{c:>+8.2f}" for c in cells) + + f" anni positivi {npos}/{len(yrs)}") + print() + for nm in top: + Lb = (leverage_series(BK["BASE"][can], BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"]) + if nm in BVT else None) + szs = np.concatenate([sizes_of(nm, EX[can][a], SZ, VTL, ACT, Lb) for a in ASSETS]) + dys = pd.DatetimeIndex(np.concatenate([EX[can][a]["day_ent"].values for a in ASSETS])) + s = pd.Series(szs, index=dys) + med = s.groupby(s.index.year).median() + rho = float(np.corrcoef(np.arange(len(med)), med.values)[0, 1]) if len(med) > 2 else float("nan") + print(f" size mediana per anno β€” {nm:<22}" + + "".join(f"{med.get(y, float('nan')):>8.2f}" for y in yrs) + + f" corr col tempo {rho:+.2f}") + print(" ⚠️ una size che DERIVA col tempo non e' controllo del rischio: e' un tilt temporale") + print(" (la vol crypto e' scesa lungo il campione -> qualunque 1/vol pesa di piu' il") + print(" periodo recente, che e' anche l'hold-out). La riga 'corr col tempo' la misura.") + + # noqa + print("\n" + "-" * 112) + print(" 10. NULL DI PERMUTAZIONE DELLA SIZE β€” la stessa MULTINSIEME di size, riassegnata a") + print(" caso ai trade. Se il vantaggio a iso-peso sopravvive al null, conta QUALE trade") + print(" riceve quale size (informazione); se no, e' solo la DISTRIBUZIONE delle size.") + print("-" * 112) + rng = np.random.default_rng(20260822) + a10 = list(offs[:: max(1, len(offs) // 3)])[:3] + NDRAW = 150 + for nm in dict.fromkeys([best_iso, best_sleeve, best_iv, best_rk]): + real = float(np.median([sh(BK[nm][o]) - sh(CTRL[nm][o]) for o in a10])) + sz0 = {o: {a: sizes_of(nm, EX[o][a], SZ, VTL, ACT) for a in ASSETS} for o in a10} + nul = [] + for _ in range(NDRAW): + v = [] + for o in a10: + szp = {a: rng.permutation(sz0[o][a]) for a in ASSETS} + lg = leg_daily(EX[o], szp) + m_s = vol(lg) / vol(SLE["BASE"][o]) if vol(SLE["BASE"][o]) > 0 else 1.0 + v.append(sh(book(TP, lg)) - sh(book(TP, SLE["BASE"][o] * m_s))) + nul.append(float(np.median(v))) + nul = np.asarray(nul) + pc = float((nul < real).mean()) * 100 + print(f" {nm:<26} reale {real:+.3f} null mediana {np.median(nul):+.3f} " + f"p90 {np.percentile(nul, 90):+.3f} max {nul.max():+.3f} " + f"-> percentile {pc:.1f} (p ~ {(1 - pc / 100):.3f})") + print(f" {NDRAW} permutazioni x {len(a10)} ancore {a10}; seme fisso 20260822.") + + print("\n" + "=" * 112) + print(f" fatto in {time.time()-t0:.0f}s") + print("=" * 112) + + +def netted_target(asset: str, ex: dict, nm, SZ, VTL, ACT, BVT, SLE, TP, can, df1h) -> np.ndarray: + """Peso NETTO per asset sul grid 1h: 0.75*target TP01 + 0.25*posizione SKH (dir*size), poi + l'eventuale leva di libro sulla gamba TP01. E' cio' che il cron manda a Deribit come UNA + posizione netta sullo stesso strumento.""" + tp = np.asarray(TO.hourly_target(asset, 0), float) + ts1 = df1h["timestamp"].values.astype(np.int64) + 3_600_000 + pos = np.zeros(len(ts1)) + L1h = np.ones(len(ts1)) + if nm is not None and nm in BVT: # BOOKVT: leva anche sulla gamba TP01 + L = leverage_series(book(TP, SLE["BASE"][can]), BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"]) + sizes = sizes_of(nm, ex, SZ, VTL, ACT, L) + idx1h = pd.DatetimeIndex(pd.to_datetime(df1h["timestamp"].values, unit="ms", utc=True)) + L1h = L.reindex(idx1h.floor("D")).ffill().fillna(1.0).values + else: + sizes = sizes_of(nm, ex, SZ, VTL, ACT) + t_in, t_out = ex["ts_close"][ex["i_ent"]], ex["ts_close"][ex["i_ex"]] + for k in range(len(t_in)): + j0 = int(np.searchsorted(ts1, t_in[k], side="left")) + j1 = int(np.searchsorted(ts1, t_out[k], side="left")) + pos[j0:j1] = ex["dir"][k] * sizes[k] + return np.clip(W_TP * tp * L1h + W_SKH * pos, -CAP_ASSET, CAP_ASSET) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_volsize_skeptic.py b/scripts/research/r0822_volsize_skeptic.py new file mode 100644 index 0000000..2559bfd --- /dev/null +++ b/scripts/research/r0822_volsize_skeptic.py @@ -0,0 +1,645 @@ +#!/usr/bin/env python +"""r0822_volsize_skeptic.py β€” SCETTICO del filone VOL-SIZE (r0822_vol_size.py). + +BERSAGLIO. Un altro agente ha trovato che dare a SKH01 una SIZE PER-TRADE (cella congelata +`VTL tv20 w90`: size fissata all'ingresso = clip(0.20 / rv90(t-1), 0, 3)) vale **+0,070 di +Sharpe di libro a ISO-PESO**, positivo in 23/23 ancore e 8/8 anni. Ha dichiarato lui stesso che +il meccanismo non e' quello che sembra: il guadagno viene dalla FREQUENZA dei trade (+0,051 sul +solo filtro di attivita') e non dalla MAGNITUDINE (+0,003) -> non e' controllo del rischio, e' +un filtro di frequenza. E ha PRE-REGISTRATO senza eseguirlo il test che lo falsificherebbe. + +QUATTRO ATTACCHI, dichiarati prima di misurare, con l'esito che mi aspetto: + + A1 IL TEST PRE-REGISTRATO β€” famiglia IV (inverse-vol per-trade sulla vol dell'ASSET) applicata + a SKH01_V1. La ragione dichiarata della famiglia IV e' la GEOMETRIA DELLO STOP: su V2-DD lo + stop e' una percentuale FISSA (4% long / 2% short), quindi il rischio in unita' di sigma + varia col regime; su V1 lo stop e' in ATR, cioe' gia' scalato sulla vol -> l'effetto DEVE + essere MINORE. Previsione direzionale: |eff. su V1| < |eff. su V2-DD|, cella per cella. + Se e' UGUALE, la spiegazione e' sbagliata anche se il numero regge. + ATTESA MIA: che il test sia SENZA POTENZA, perche' sospetto che IV non faccia niente + nemmeno su V2-DD (l'agente ha promosso VTL, non IV). Un test pre-registrato su una + famiglia inerte non falsifica nulla. + + A2 LA CONTABILITA' β€” l'agente dichiara di aver trovato e riparato un look-ahead di + contabilita': "tutto il P&L di un trade e' contabilizzato il giorno di CHIUSURA, fino a + 3,83 giorni dopo l'ingresso", costo "+0,04 di Sharpe hold-out fantasma", e ne ha tratto + una REGOLA pubblicata. Il compito mi chiede di verificare la riparazione troncando i dati + alla barra d'ingresso. Lo faccio, ma prima verifico la PREMESSA: quale giorno porta davvero + il P&L. ATTESA MIA: la riparazione e' corretta e ridondante. + + A3 L'ATTACCO ALLA SPIEGAZIONE β€” se il guadagno e' un filtro di frequenza, deve essere + riproducibile SENZA ALCUN vol-target. Controllo piu' brutale possibile: size BINARIA 0/1 + sulla stessa soglia della regola congelata (L>=1 <=> rv90<=20%), piu' un filtro che scarta + i trade nei periodi di maggiore ATTIVITA' della gamba. Se un binario replica il +0,070, + la formula di sizing e' ORNAMENTO. Controllo di segno obbligatorio: il binario INVERSO + (tieni solo i trade in alta vol) deve DANNEGGIARE, o non sto misurando un filtro. + + A4 L'ASSE DELLA FINESTRA β€” l'agente dichiara che w e' un PICCO, non un plateau (w30 negativo, + w180 ~0, w90 ottimo). Un picco su un asse continuo con 23 ancore e' la firma classica di + un max-of-k. Misuro l'asse fitto (9 finestre) e rifaccio la scelta di w IN-SAMPLE-ONLY. + +DISCIPLINA. Non ricalcolo niente che esista: importo `r0822_vol_size` (extract / equity_daily / +leg_daily / size_iv / leverage_series / sizes_from_L / book) e replico BIT-EXACT contro +`sleeves._skyhook_returns()` prima di ogni delta. Il confronto e' sempre ISO-PESO (baseline +ri-scalato allo stesso peso effettivo di SKH), perche' "alzare SKH01" e' gia' stato misurato e +respinto il 26/07. Statistica = MEDIANA DELLE DIFFERENZE APPAIATE per ancora (`anchor_luck_delta`), +mai differenza delle mediane. + +USO: nice -n 19 timeout 900 uv run python scripts/research/r0822_volsize_skeptic.py [--every K] + --every 8 -> 3 ancore (pilota). default: tutte e 23. +""" +from __future__ import annotations + +import argparse +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) + +import altlib as A # noqa: E402 +import r0702_anchor_skh01 as R # noqa: E402 +import r0822_vol_size as V # noqa: E402 (bersaglio: riusato, non riscritto) +from src.strategies.skyhook import SKH01_V1 # noqa: E402 + +ASSETS = V.ASSETS +OFFSETS_FULL = V.OFFSETS_FULL +FROZEN = dict(tv=0.20, w=90, cap=3.0, active_only=False) # la cella congelata del LEAD +sh, vol, maxdd, cagr = V.sh, V.vol, V.maxdd, V.cagr +hold, ins, book = V.hold, V.ins, V.book + + +# --------------------------------------------------------------------------- famiglie di size +def _keep_to_sizes(ex: dict, keep: pd.Series) -> np.ndarray: + return V.sizes_from_L(ex, keep.astype(float)) + + +def size_vtl(sp: dict): + def f(ex): + return V.sizes_from_L(ex, V.leverage_series(ex["base_daily"], **sp)) + return f + + +def size_bin(sp: dict, mode: str): + """Size BINARIA 0/1 dalla STESSA L della regola congelata. Nessuna magnitudine: o il trade + si fa a size 1, o non si fa. `ge1` usa la soglia della regola stessa (L>=1 <=> rv90<=tv); + `gemed` la mediana espandente causale di L; `lt1` e' l'INVERSO (controllo di segno).""" + def f(ex): + L = V.leverage_series(ex["base_daily"], **sp) + if mode == "ge1": + keep = pd.Series(L.values >= 1.0, index=L.index) + elif mode == "lt1": + keep = pd.Series(L.values < 1.0, index=L.index) + elif mode == "gemed": + med = L.expanding(min_periods=V.VT_WARM).median() + keep = pd.Series(np.where(med.notna().values, L.values >= med.values, True), + index=L.index) + else: + raise ValueError(mode) + return _keep_to_sizes(ex, keep) + return f + + +def size_bin_grad(sp: dict): + """BINARIO + GRADUAZIONE: fra i trade tenuti, la size continua di L. Isola quanto aggiunge + la FORMULA una volta che la SELEZIONE e' gia' stata fatta.""" + def f(ex): + L = V.leverage_series(ex["base_daily"], **sp) + g = pd.Series(np.where(L.values >= 1.0, L.values, 0.0), index=L.index) + return V.sizes_from_L(ex, g) + return f + + +def size_chan(mode: str, binary: bool, w: int = 90, cap: float = 3.0): + """I DUE CANALI dentro `rv` di una serie all'88% di zeri, isolati come nel bersaglio: + mode='mag' -> vol calcolata sulle sole barre ATTIVE (MAGNITUDINE degli esiti) + mode='freq' -> conteggio di barre attive nella finestra (FREQUENZA dei trade) + binary=False riproduce le celle continue del bersaglio (`VTL neutro attive` / `ACT`); + binary=True le riduce a 0/1 alla mediana ESPANDENTE causale della propria L.""" + def f(ex): + if mode == "mag": + Ls = V.leverage_series(ex["base_daily"], tv=None, w=w, cap=cap, active_only=True) + else: + Ls = V.activity_L(ex["base_daily"], w=w, cap=cap) + if not binary: + return V.sizes_from_L(ex, Ls) + med = Ls.expanding(min_periods=V.VT_WARM).median() + keep = pd.Series(np.where(med.notna().values, Ls.values >= med.values, True), + index=Ls.index) + return _keep_to_sizes(ex, keep) + return f + + +def size_dropact(w: int = 90, q: float = 0.75): + """Scarta i trade il cui giorno d'ingresso cade nei periodi in cui la gamba e' stata piu' + ATTIVA (conteggio di barre attive negli ultimi w giorni sopra il quantile ESPANDENTE causale + q). Nessun vol-target, nessuna magnitudine, nessun prezzo: solo la frequenza dei trade.""" + def f(ex): + base = ex["base_daily"] + cnt = (base != 0.0).astype(float).rolling(w, min_periods=max(5, w // 3)).sum().shift(1) + thr = cnt.expanding(min_periods=V.VT_WARM).quantile(q) + keep = pd.Series(np.where(thr.notna().values & cnt.notna().values, + cnt.values <= thr.values, True), index=base.index) + return _keep_to_sizes(ex, keep) + return f + + +def iv_family() -> dict: + d = {} + for p in (0.5, 1.0): + for w in (30, 90): + for cp in (2.0, 4.0): + d[f"IV p{p} w{w} cap{cp:.0f}"] = V.size_iv(p, w, cp) + return d + + +# --------------------------------------------------------------------------- motore +def legs(EX: dict, funcs: dict) -> dict: + """name -> serie giornaliera della gamba SKH 50/50, per un'ancora.""" + out = {"BASE": V.leg_daily(EX, {a: V.size_flat(EX[a]) for a in ASSETS})} + for nm, fn in funcs.items(): + out[nm] = V.leg_daily(EX, {a: fn(EX[a]) for a in ASSETS}) + return out + + +def iso_pair(TP: pd.Series, leg: pd.Series, leg_base: pd.Series): + """Libro della variante e libro di CONTROLLO ISO-PESO (baseline ri-scalato alla stessa vol + di gamba -> stesso peso effettivo di SKH). E' la convenzione del bersaglio, riusata.""" + vb = vol(leg_base) + m_s = vol(leg) / vb if vb > 0 else 1.0 + return book(TP, leg), book(TP, leg_base * m_s), m_s + + +def delta_table(BK: dict, CT: dict, names, offs, label: str, extra_hold=True) -> dict: + res = {} + print(f" {'cella':<26}{'ISO dShFULL':>12}{'pos/n':>8}{'banda':>20}" + + (f"{'ISO dShHOLD':>13}{'pos/n':>8}" if extra_hold else "") + f"{'wSKHeff':>9}") + for nm in names: + dF = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], lambda o, n=nm: CT[n][o], offs, + metric=sh) + row = dict(dF=dF["median_paired"], nF=dF["n_positive"], lo=dF["lo"], hi=dF["hi"]) + s = (f" {nm:<26}{row['dF']:>+12.3f}{row['nF']:>5}/{len(offs):<3}" + f" [{row['lo']:+.3f},{row['hi']:+.3f}]".ljust(20 + 26 + 12 + 8)) + s = (f" {nm:<26}{row['dF']:>+12.3f}{row['nF']:>5}/{len(offs):<3}" + f"{'[%+.3f,%+.3f]' % (row['lo'], row['hi']):>20}") + if extra_hold: + dH = A.anchor_luck_delta(lambda o, n=nm: hold(BK[n][o]), + lambda o, n=nm: hold(CT[n][o]), offs, metric=sh) + row.update(dH=dH["median_paired"], nH=dH["n_positive"]) + s += f"{row['dH']:>+13.3f}{row['nH']:>5}/{len(offs):<3}" + we = float(np.median([0.25 * m for m in MSCALE[nm]])) if nm in MSCALE else float("nan") + row["weff"] = we + s += f"{we:>9.3f}" + print(s) + res[nm] = row + return res + + +MSCALE: dict = {} + + +def per_year(BK: dict, CT: dict, nm: str, offs) -> tuple: + yrs = sorted({int(y) for y in BK[nm][offs[0]].index.year}) + cells = [] + for y in yrs: + v = [sh(BK[nm][o][BK[nm][o].index.year == y]) - sh(CT[nm][o][CT[nm][o].index.year == y]) + for o in offs if (BK[nm][o].index.year == y).sum() > 20] + cells.append(float(np.median(v)) if v else float("nan")) + return yrs, cells + + +# =========================================================================== main +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--every", type=int, default=1) + args = ap.parse_args() + offs = OFFSETS_FULL[:: max(1, args.every)] + t0 = time.time() + L = 112 + + print("=" * L) + print(" r0822 VOL-SIZE β€” SCETTICO. Bersaglio: `VTL tv20 w90` = +0,070 ISO dShFULL (23/23)") + print("=" * L) + print(f" ancore: {len(offs)}/{len(OFFSETS_FULL)} della griglia a priori 02/07 {list(offs)}") + print(" lente: path CANONICO (identica nelle due braccia di ogni confronto). TP01 canonico.") + + TP = A.tp01_baseline_daily() + + # ------------------------------------------------------------------ celle + IV = iv_family() + FUN_V2 = dict(IV) + FUN_V2["VTL tv20 w90 (LEAD)"] = size_vtl(FROZEN) + FUN_V2["BIN L>=1 (0/1)"] = size_bin(FROZEN, "ge1") + FUN_V2["BIN L>=med (0/1)"] = size_bin(FROZEN, "gemed") + FUN_V2["BIN L<1 (INVERSO)"] = size_bin(FROZEN, "lt1") + FUN_V2["BIN+GRAD"] = size_bin_grad(FROZEN) + FUN_V2["DROP attivita' >p75"] = size_dropact(90, 0.75) + FUN_V2["DROP attivita' >p50"] = size_dropact(90, 0.50) + FUN_V2["canale MAGNITUDINE cont."] = size_chan("mag", False) + FUN_V2["canale FREQUENZA cont."] = size_chan("freq", False) + FUN_V2["canale MAGNIT. 0/1"] = size_chan("mag", True) + FUN_V2["canale FREQ. 0/1"] = size_chan("freq", True) + WGRID = (15, 20, 30, 45, 60, 90, 120, 180, 270) + for w in WGRID: + if w != 90: + FUN_V2[f"VTL tv20 w{w}"] = size_vtl(dict(FROZEN, w=w)) + FUN_V1 = dict(IV) + FUN_V1["VTL tv20 w90 (LEAD)"] = size_vtl(FROZEN) + FUN_V1["BIN L>=1 (0/1)"] = size_bin(FROZEN, "ge1") + + NTRIAL = len(FUN_V2) + len(FUN_V1) + 2 + 4 # +2 baseline, +4 controlli 'L combinata' (Β§1c) + print(f"\n GRIGLIA DICHIARATA (contata al RIALZO): {len(FUN_V2)} celle su V2-DD + " + f"{len(FUN_V1)} su V1 + 4 controlli 'L combinata' (Β§1c) + 2 baseline = {NTRIAL} " + f"valutazioni x {len(offs)} ancore. Nessuna cella scelta") + print(" dopo aver guardato: IV e' la famiglia PRE-REGISTRATA, VTL/w e' l'asse dichiarato") + print(" fragile dall'agente, BIN/DROP sono controlli miei dichiarati sopra.") + + # ------------------------------------------------------------------ ciclo ancore + BK2, CT2, BK1, CT1 = {}, {}, {}, {} + EX0 = None + print("\n calcolo per ancora ...", flush=True) + for o in offs: + EXV2 = {a: V.extract(a, o) for a in ASSETS} + EXV1 = {a: V.extract(a, o, SKH01_V1) for a in ASSETS} + if o == offs[0]: + EX0 = (EXV2, EXV1) + for tag, EXX, FUN, BKd, CTd in (("V2", EXV2, FUN_V2, BK2, CT2), + ("V1", EXV1, FUN_V1, BK1, CT1)): + lg = legs(EXX, FUN) + base = lg["BASE"] + if tag == "V2": + # controlli di Β§1(c): L calcolata sulla gamba COMBINATA (cio' che fa il ramo + # NAIVE del bersaglio), applicata nei DUE modi. Se coincidono, il "fantasma" + # non e' un problema di QUANDO si applica la size. + for w in (30, 90): + Lc = V.leverage_series(base, **dict(FROZEN, w=w)) + lg[f"NAIVEcomb w{w}"] = base * Lc.reindex(base.index).fillna(1.0) + lg[f"COMBingr w{w}"] = V.leg_daily( + EXX, {a: V.sizes_from_L(EXX[a], Lc) for a in ASSETS}) + for nm, s in lg.items(): + b, c, m_s = iso_pair(TP, s, base) + BKd.setdefault(nm, {})[o] = b + CTd.setdefault(nm, {})[o] = c + if tag == "V2": + MSCALE.setdefault(nm, []).append(m_s) + if o != offs[0]: + del EXV2, EXV1 + print(f" fatto in {time.time()-t0:.0f}s") + EXV2, EXV1 = EX0 + + # ------------------------------------------------------------------ Β§0 SANITY + print("\n" + "-" * L) + print(" 0. SANITY β€” replica BIT-EXACT prima di qualunque delta (se non e' 0, il problema") + print(" e' mio e va detto)") + print("-" * L) + from src.portfolio.sleeves import _skyhook_returns + mine = V.leg_daily(EXV2, {a: V.size_flat(EXV2[a]) for a in ASSETS}) + off = _skyhook_returns() + d0 = float(np.max(np.abs(mine.values - off.values))) if len(mine) == len(off) else float("nan") + print(f" gamba 50/50 off{offs[0]} vs sleeves._skyhook_returns(): max|dif| = {d0:.2e} " + f"({len(mine)} giorni)") + assert d0 < 1e-15, "replica NON bit-exact" + for a in ASSETS: + ref = R.run_asset(a, offs[0])[0] + R._CACHE.clear() + dd = float(np.max(np.abs(V.equity_daily(EXV2[a], V.size_flat(EXV2[a])).values - ref.values))) + print(f" {a} vs r0702_anchor_skh01.run_asset: max|dif| = {dd:.2e}") + assert dd < 1e-15 + + lead = "VTL tv20 w90 (LEAD)" + dLEAD = A.anchor_luck_delta(lambda o: BK2[lead][o], lambda o: CT2[lead][o], offs, metric=sh) + dLEADH = A.anchor_luck_delta(lambda o: hold(BK2[lead][o]), lambda o: hold(CT2[lead][o]), + offs, metric=sh) + print(f" RIPRODUZIONE DEL TITOLO: ISO dShFULL {dLEAD['median_paired']:+.3f} " + f"({dLEAD['n_positive']}/{len(offs)}) ISO dShHOLD {dLEADH['median_paired']:+.3f} " + f"({dLEADH['n_positive']}/{len(offs)}) [pubblicato +0,070 (23/23) / +0,101 (19/23)]") + med_F = float(np.median([sh(BK2["BASE"][o]) for o in offs])) + med_H = float(np.median([sh(hold(BK2["BASE"][o])) for o in offs])) + med_D = float(np.median([maxdd(BK2["BASE"][o]) for o in offs])) + print(f" libro BASE (V2-DD), mediana della banda: ShFULL {med_F:.3f} / ShHOLD {med_H:.3f} " + f"/ maxDD {med_D*100:.1f}% [pubblicato 1,626 / 1,037 / 10,4%]") + + # ------------------------------------------------------------------ Β§1 CONTABILITA' + print("\n" + "-" * L) + print(" 1. LA CONTABILITA' β€” verifica della PREMESSA prima della riparazione") + print(" (l'agente: 'tutto il P&L di un trade e' contabilizzato il giorno di CHIUSURA')") + print("-" * L) + tot_ent = tot_ex = tot_chk = 0 + for a in ASSETS: + ex = EXV2[a] + d_e = ex["idx"][ex["i_ent"]].floor("D") + d_x = ex["idx"][ex["i_ex"]].floor("D") + dl = ex["base_daily"] + multi = np.where(d_e.values != d_x.values)[0] + ne = nx = ck = 0 + for k in multi: + n = float(ex["net"][k]) + if abs(n) < 1e-6: + continue + ck += 1 + ne += int(abs(float(dl.get(d_e[k], 0.0)) - n) < 1e-9) + nx += int(abs(float(dl.get(d_x[k], 0.0)) - n) < 1e-9) + print(f" {a}: {ck} trade multi-giorno -> ritorno giornaliero == net del trade " + f"al giorno d'INGRESSO {ne}/{ck}, al giorno di USCITA {nx}/{ck}") + tot_ent += ne; tot_ex += nx; tot_chk += ck + print(f" TOTALE: INGRESSO {tot_ent}/{tot_chk} ({tot_ent/max(tot_chk,1)*100:.1f}%) " + f"USCITA {tot_ex}/{tot_chk} ({tot_ex/max(tot_chk,1)*100:.1f}%)") + print(" -> `backtest_signals` fa `equity[i_ent:i_ex+1] = capital`: il gradino comincia") + print(" alla barra d'INGRESSO e il resample giornaliero (last) lo legge quel giorno.") + + print("\n (a) TRONCAMENTO ALLA BARRA D'INGRESSO β€” le size della regola congelata " + "ricalcolate") + print(" con i soli dati <= barra d'ingresso devono coincidere ESATTAMENTE") + worst = 0.0 + ntest = 0 + for a in ASSETS: + ex = EXV2[a] + full = size_vtl(FROZEN)(ex) + sel = [k for k in range(len(ex["i_ent"])) if ex["i_ent"][k] >= V.SIG_WARM] + sel = sel[:: max(1, len(sel) // 25)][:25] + bad = 0 + for k in sel: + i = int(ex["i_ent"][k]) + # ricostruisco la gamba usando SOLO i trade gia' CHIUSI prima dell'ingresso k + sub = {kk: ex[kk] for kk in ("n", "idx")} + m = ex["i_ex"] < i + sub = dict(n=i + 1, idx=ex["idx"][: i + 1], + i_ent=ex["i_ent"][m], i_ex=ex["i_ex"][m], net=ex["net"][m]) + if not len(sub["i_ent"]): + continue + bd = V.equity_daily(sub, np.ones(len(sub["i_ent"]))) + Lt = V.leverage_series(bd, **FROZEN) + day = ex["idx"][i].floor("D") + v = Lt.reindex([day]).values[0] + v = 1.0 if not np.isfinite(v) else float(v) + worst = max(worst, abs(v - float(full[k]))) + bad += int(abs(v - float(full[k])) > 1e-9) + ntest += 1 + print(f" {a}: {len(sel)-bad}/{len(sel)} size identiche a dati troncati " + f"(divergenze {bad})") + print(f" max|dif| su {ntest} size ricalcolate da zero: {worst:.2e} -> " + f"{'CAUSALE' if worst < 1e-9 else 'NON CAUSALE'}") + + print("\n (b) LA 'RIPARAZIONE' E' UN NO-OP: size-all'INGRESSO vs moltiplicare la SERIE") + print(" GIORNALIERA, a parita' di tutto il resto (stessa L, PER ASSET)") + for a in ASSETS: + ex = EXV2[a] + Ls = V.leverage_series(ex["base_daily"], **FROZEN) + s_in = V.equity_daily(ex, V.sizes_from_L(ex, Ls)) + s_mul = ex["base_daily"] * Ls.reindex(ex["base_daily"].index).fillna(1.0) + j = pd.concat({"a": s_in, "b": s_mul}, axis=1, join="inner").dropna() + print(f" {a}: max|dif| = {np.max(np.abs(j['a']-j['b'])):.2e} " + f"Sh ingresso {sh(s_in):.4f} vs Sh moltiplicazione {sh(s_mul):.4f}") + + print("\n (c) ALLORA COS'E' IL '+0,04 FANTASMA'? Il ramo NAIVE del bersaglio calcola L") + print(" sulla gamba 50/50 COMBINATA e MOLTIPLICA la serie; il ramo 'causale' calcola L") + print(" PER ASSET e la applica all'INGRESSO. Sono DUE differenze in una. Le separo:") + for w in (30, 90): + d = [] + for o in offs: + j = pd.concat({"a": BK2[f"NAIVEcomb w{w}"][o], "b": BK2[f"COMBingr w{w}"][o]}, + axis=1, join="inner").dropna() + d.append(float(np.max(np.abs(j["a"] - j["b"])))) + print(f" w{w}: [L combinata, MOLTIPLICA] vs [L combinata, size all'INGRESSO] -> " + f"max|dif| mediano {np.median(d):.2e} = IDENTICHE") + print(f"\n {'coppia':<38}{'dShFULL':>10}{'dShHOLD':>10} (delta GREZZI vs baseline nudo,") + print(f" {'':<38}{'':>10}{'':>10} la convenzione del Β§4 del bersaglio)") + for w in (30, 90): + for nm in (f"NAIVEcomb w{w}", f"COMBingr w{w}", + (lead if w == 90 else f"VTL tv20 w{w}")): + gF = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o: BK2["BASE"][o], offs, + metric=sh) + gH = A.anchor_luck_delta(lambda o, n=nm: hold(BK2[n][o]), + lambda o: hold(BK2["BASE"][o]), offs, metric=sh) + print(f" {nm:<38}{gF['median_paired']:>+10.3f}{gH['median_paired']:>+10.3f}") + print(" -> il 'fantasma' e' interamente [L combinata] - [L per asset]: una scelta di") + print(" SERIE DI RIFERIMENTO, non un look-ahead. Il ramo NAIVE non e' non-causale.") + + # ------------------------------------------------------------------ Β§2 TEST PRE-REGISTRATO + print("\n" + "-" * L) + print(" 2. IL TEST PRE-REGISTRATO β€” famiglia IV su V2-DD (stop % FISSO) vs V1 (stop in ATR)") + print(" Previsione dichiarata dall'agente: |effetto su V1| < |effetto su V2-DD|.") + print("-" * L) + n2 = {a: len(EXV2[a]["i_ent"]) for a in ASSETS} + n1 = {a: len(EXV1[a]["i_ent"]) for a in ASSETS} + print(f" trade all'ancora {offs[0]}: V2-DD {n2} V1 {n1}") + mb2 = float(np.median([sh(BK2["BASE"][o]) for o in offs])) + mb1 = float(np.median([sh(BK1["BASE"][o]) for o in offs])) + print(f" libro BASE mediana di banda: V2-DD ShFULL {mb2:.3f} | V1 ShFULL {mb1:.3f}") + names_iv = list(IV) + print(f"\n {'cella IV':<22}{'V2-DD ISO dF':>14}{'pos/n':>8}{'V1 ISO dF':>12}{'pos/n':>8}" + f"{'|V1|<|V2|?':>12}") + ok = tot = 0 + e2s, e1s = [], [] + for nm in names_iv: + a2 = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o, n=nm: CT2[n][o], offs, + metric=sh) + a1 = A.anchor_luck_delta(lambda o, n=nm: BK1[n][o], lambda o, n=nm: CT1[n][o], offs, + metric=sh) + e2, e1 = a2["median_paired"], a1["median_paired"] + e2s.append(e2); e1s.append(e1) + good = abs(e1) < abs(e2) + ok += int(good); tot += 1 + print(f" {nm:<22}{e2:>+14.3f}{a2['n_positive']:>5}/{len(offs):<3}{e1:>+12.3f}" + f"{a1['n_positive']:>5}/{len(offs):<3}{('SI' if good else 'NO'):>12}") + print(f"\n previsione verificata in {ok}/{tot} celle della famiglia") + print(f" |effetto| mediano di famiglia: V2-DD {np.median(np.abs(e2s)):.3f} " + f"V1 {np.median(np.abs(e1s)):.3f}") + print(f" effetto mediano SEGNATO: V2-DD {np.median(e2s):+.3f} V1 {np.median(e1s):+.3f}") + print(" ⚠️ POTENZA DEL TEST: se |effetto su V2-DD| e' gia' ~0, la previsione e' verificabile") + print(" solo per caso e il gate pre-registrato NON puo' falsificare niente.") + for nm in ("VTL tv20 w90 (LEAD)", "BIN L>=1 (0/1)"): + a2 = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o, n=nm: CT2[n][o], offs, + metric=sh) + a1 = A.anchor_luck_delta(lambda o, n=nm: BK1[n][o], lambda o, n=nm: CT1[n][o], offs, + metric=sh) + print(f" (riferimento) {nm:<24} V2-DD {a2['median_paired']:+.3f} " + f"({a2['n_positive']}/{len(offs)}) V1 {a1['median_paired']:+.3f} " + f"({a1['n_positive']}/{len(offs)})") + + # ------------------------------------------------------------------ Β§3 ATTACCO ALLA SPIEGAZIONE + print("\n" + "-" * L) + print(" 3. ATTACCO ALLA SPIEGAZIONE β€” quanto del +0,070 resta senza NESSUN vol-target?") + print(" Tutte ISO-PESO vs lo stesso baseline. 'wSKHeff' = peso effettivo di SKH nel libro") + print("-" * L) + grp = [lead, "BIN L>=1 (0/1)", "BIN+GRAD", "BIN L>=med (0/1)", "BIN L<1 (INVERSO)", + "DROP attivita' >p75", "DROP attivita' >p50", + "canale MAGNITUDINE cont.", "canale FREQUENZA cont.", + "canale MAGNIT. 0/1", "canale FREQ. 0/1"] + RES = delta_table(BK2, CT2, grp, offs, "attacco") + base_eff = RES[lead]["dF"] + print() + for nm in grp[1:]: + frac = RES[nm]["dF"] / base_eff * 100 if abs(base_eff) > 1e-9 else float("nan") + print(f" {nm:<26} recupera {frac:>6.0f}% del vantaggio del LEAD") + # quanti trade tocca ciascun filtro + print(f"\n {'cella':<26}{'trade tenuti':>14}{'su':>6}{'size mediana':>14}" + f"{'size = 0':>10}") + for nm in [lead] + grp[1:]: + kept = tot_t = zer = 0 + med = [] + for a in ASSETS: + s = FUN_V2[nm](EXV2[a]) + tot_t += len(s); kept += int((s > 0).sum()); zer += int((s == 0).sum()) + med.append(s) + mm = float(np.median(np.concatenate(med))) + print(f" {nm:<26}{kept:>14}{tot_t:>6}{mm:>14.2f}{zer:>10}") + + print(f"\n COSA SELEZIONA il filtro (ancora {offs[0]}, i due asset uniti): net medio dei") + print(" trade TENUTI vs SCARTATI, e attivita' trailing media dei due gruppi") + print(f" {'cella':<26}{'net TENUTI':>12}{'net SCARTATI':>14}{'att.TEN':>10}{'att.SCA':>10}" + f"{'%tenuti':>9}") + for nm in ("BIN L>=1 (0/1)", "BIN L>=med (0/1)", "BIN L<1 (INVERSO)", + "DROP attivita' >p50", "canale MAGNIT. 0/1", "canale FREQ. 0/1"): + nk, nd, ak, ad = [], [], [], [] + for a in ASSETS: + ex = EXV2[a] + szv = FUN_V2[nm](ex) + cnt = ((ex["base_daily"] != 0.0).astype(float) + .rolling(90, min_periods=30).sum().shift(1)) + av = cnt.reindex(ex["day_ent"]).values + keep = szv > 0 + nk += list(ex["net"][keep]); nd += list(ex["net"][~keep]) + ak += list(av[keep]); ad += list(av[~keep]) + f_ = 100.0 * len(nk) / max(len(nk) + len(nd), 1) + print(f" {nm:<26}{np.mean(nk)*100:>11.2f}%{np.mean(nd)*100 if nd else float('nan'):>13.2f}%" + f"{np.nanmean(ak):>10.1f}{np.nanmean(ad) if ad else float('nan'):>10.1f}{f_:>9.0f}") + + print("\n per anno (ISO dSh, mediana appaiata sulle ancore):") + yrs = None + 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)) + npos = int(np.nansum(np.asarray(cells) > 0)) + print(f" {nm:<26}" + "".join(f"{c:>+8.2f}" for c in cells) + + f" anni positivi {npos}/{len(yrs)}") + + # ------------------------------------------------------------------ Β§4 ASSE DELLA FINESTRA + print("\n" + "-" * L) + 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}{'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, + metric=sh) + dI = A.anchor_luck_delta(lambda o, n=nm: ins(BK2[n][o]), lambda o, n=nm: ins(CT2[n][o]), + offs, metric=sh) + 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}{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} " + f"({wf[wbest_full]:+.3f})") + print(f" argmax IN-SAMPLE-ONLY (la sola scelta ammessa): w = {wbest_is} " + f"(IS {wi[wbest_is]:+.3f}) -> quella cella vale FULL {wf[wbest_is]:+.3f} / " + f"HOLD {wh[wbest_is]:+.3f}") + order = sorted(WGRID) + k = order.index(wbest_full) + nb = [order[k - 1] if k > 0 else None, order[k + 1] if k + 1 < len(order) else None] + for w in nb: + if w is not None: + print(f" vicino w={w:<4} vale {wf[w]:+.3f} = {wf[w]/wf[wbest_full]*100:>5.0f}% " + f"del picco") + pos = sum(1 for w in WGRID if wf[w] > 0) + print(f" celle con ISO dShFULL > 0: {pos}/{len(WGRID)} " + f"spread della famiglia {max(wf.values())-min(wf.values()):.3f} " + f"sd fra celle {np.std(list(wf.values()), ddof=1):.3f}") + print(f" stima ONESTA se w e' SCELTO (mediana della famiglia, come per la banda " + f"d'ancora): {np.median(list(wf.values())):+.3f} FULL / " + f"{np.median(list(wh.values())):+.3f} HOLD contro il picco " + 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(" 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) + print(" 5. GATE (per il contratto di consegna)") + print("-" * L) + can = offs[0] + legs_can = {} + for nm in ("BASE", lead, "BIN L>=med (0/1)"): + legs_can[nm] = (V.leg_daily(EXV2, {a: V.size_flat(EXV2[a]) for a in ASSETS}) + if nm == "BASE" + else V.leg_daily(EXV2, {a: FUN_V2[nm](EXV2[a]) for a in ASSETS})) + nlos = int((EXV2["BTC"]["net"] < 0).sum() + (EXV2["ETH"]["net"] < 0).sum()) + ntot = int(len(EXV2["BTC"]["net"]) + len(EXV2["ETH"]["net"])) + for nm, s_ in legs_can.items(): + m = A.marginal_vs_tp01(s_) + imp = A.implausible_sharpe(s_, n_trades=ntot, n_losing_trades=nlos) + print(f" {nm:<22} marginal {str(m.get('marginal_verdict')):<9} corr {m.get('corr_full')}" + f" implausible={imp['implausible']} (perdite/attive " + f"{imp.get('loss_frac', 0)*100:.1f}%)") + trials = [float(np.median([sh(BK2[n][o]) for o in offs])) for n in BK2] + dsr_c, sr0 = A.deflated_sharpe(float(np.median([sh(BK2[lead][o]) for o in offs])), trials, + BK2[lead][can]) + dsr_b, _ = A.deflated_sharpe(med_F, trials, BK2["BASE"][can]) + print(f" deflated-Sharpe: candidato {dsr_c:.3f} / BASELINE {dsr_b:.3f} su {len(trials)} " + f"trial (sd {np.std(trials, ddof=1):.4f}) -> VACUO, lo passa anche il baseline") + print(" null de-levering: DEGENERE per costruzione (sh(k*base) == sh(base)); il suo") + print(" sostituto e' il confronto ISO-PESO usato in tutte le tabelle sopra.") + print(" causalita': Β§1(a) 50/50 size ricalcolate da dati troncati, max|dif| 0.00e+00.") + + print("\n" + "-" * L) + print(" 6. SINTESI DEI NUMERI CHE CONTANO") + print("-" * L) + print(f" fortuna d'ancora del LIBRO BASE: ShFULL canonico " + f"{sh(BK2['BASE'][0]) if 0 in BK2['BASE'] else float('nan'):.3f} vs mediana " + f"{med_F:.3f}") + print(f" vantaggio del LEAD (ISO): {dLEAD['median_paired']:+.3f} FULL / " + f"{dLEADH['median_paired']:+.3f} HOLD") + print(f" riprodotto dal FILTRO BINARIO: {RES['BIN L>=1 (0/1)']['dF']:+.3f} FULL / " + f"{RES['BIN L>=1 (0/1)']['dH']:+.3f} HOLD") + print(f" riprodotto dal BINARIO alla MEDIANA:{RES['BIN L>=med (0/1)']['dF']:+.3f} FULL / " + 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) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_vrp_real_quotes.py b/scripts/research/r0822_vrp_real_quotes.py new file mode 100644 index 0000000..04cb259 --- /dev/null +++ b/scripts/research/r0822_vrp_real_quotes.py @@ -0,0 +1,857 @@ +"""R0822 VRP-QUOTE-VERE β€” esiste una struttura di reddito in opzioni ESEGUIBILE a $600-3.000 +che sopravvive a fee reali, spread reali e lotto minimo? + +NON e' "rifare VRP01". La domanda e' se la regola dura del progetto β€” *niente short-vol da +MODELLO in deploy* (19/06, riconfermata 3 volte) β€” si possa sostituire con *short-vol da QUOTE*, +che e' un'altra cosa: il 30/07 il premio era ancora prezzato Black-Scholes su DVOL ATM e la +misura sulle quote reali dava f = 0.73 del modello, con il difetto NON nella gamba venduta +(f_short 1.01) ma nell'ALA che si compra (f_long 2.23). Da allora la condizione e' cambiata: +la catena vera la raccogliamo noi, oraria, per strike, con bid/ask. + +ORDINE DELLE DOMANDE (deliberato). L'eseguibilita' viene PRIMA del rendimento β€” regola del +30/07: "prima di misurare il rendimento a un capitale dato, misurare il lotto minimo del venue". +Se non si esegue, il resto e' accademia. + + Β§1 LIMITE DI REGIME β€” risultato di prima riga, non nota a pie' di pagina + Β§2 IL CAMPIONE β€” quante settimane ci sono DAVVERO, e perche' + Β§3 ESEGUIBILITA' β€” lotti, famiglie, e su quale delle due si esegue + Β§4 BACKTEST SU QUOTE REALI β€” griglia dichiarata, fill al bid/ask, fee di listino + Β§5 IL RISCHIO STA NEL MINIMO β€” non nelle chiusure settimanali + Β§6 GATE β€” inclusi quelli che NON si possono far girare + Β§7 VERDETTO + +CONVENZIONI CHE DECIDONO IL RISULTATO (dichiarate qui, non sepolte nel codice) + (a) FILL: si vende al BID e si compra all'ASK, sempre. Il mid e' il modello sotto mentite + spoglie e compare solo come diagnostica, mai come cella selezionabile. + (b) FEE: listino Deribit vero β€” `min(0.03% del sottostante, 12.5% del premio della SINGOLA + opzione)` per gamba, piu' la fee di consegna sulle gambe ITM a scadenza. Lo sleeve usa + invece un forfait del 12.5% del credito NETTO: qui si misura di quanto sbaglia. + (c) SETTLEMENT: indice reale alla scadenza dal feed certificato. Deribit regola sulla media + dell'indice nei 30 minuti prima delle 08:00 UTC; qui si usa la barra 1h di chiusura β€” + approssimazione dichiarata, non nascosta. + (d) CAPITALE IMPEGNATO: il denominatore e' il rischio massimo della struttura + `(K_short - K_long) x lotto - credito`. E' il limite INFERIORE del capitale richiesto; + quello vero dipende dal regime di margine del conto e NON e' leggibile da dati pubblici. + Riportato anche il limite superiore (cash-secured, la convenzione del sleeve). + +RAM: la catena si legge filtrata in LETTURA (solo put, solo le colonne che servono) con +pyarrow.dataset β€” `bite_archive.parquet` intero esplode in memoria e ucciderebbe gli altri agenti. + + nice -n 19 timeout 900 uv run python scripts/research/r0822_vrp_real_quotes.py + ... --no-live # salta le letture dal venue (usa la cache se c'e') +""" +from __future__ import annotations + +import argparse +import json +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import altlib as A # noqa: E402 +import cblib as CB # noqa: E402 + +CHAIN_DIR = ROOT / "data" / "raw" / "cb_chain" +SNAP_JSONL = ROOT / "data" / "options_daily" / "snapshots.jsonl" +CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/r0822_venue.json") + +# --- listino Deribit (fonte: campo `taker_commission` letto dal venue + schema fee pubblico) --- +FEE_UNDERLYING = 0.0003 # 0.03% del sottostante per gamba, per contratto +FEE_PREMIUM_CAP = 0.125 # cap: 12.5% del premio della SINGOLA opzione +FEE_DELIVERY = 0.00015 # 0.015% del sottostante sulle gambe ITM a scadenza +SLEEVE_FEE_FRAC = 0.125 # forfait dello sleeve: 12.5% del credito NETTO + +DTE_LO, DTE_HI, DTE_TARGET = 4.0, 10.0, 7.0 +WEEKS_PER_YEAR = 52.0 + +# ---------------------------------------------------------------- griglia DICHIARATA +# Si conta al RIALZO: ogni variante provata e' un trial, anche quelle scartate. +SHORT_DELTAS = (-0.35, -0.28, -0.20, -0.12) +LONG_DELTAS = (-0.20, -0.10, -0.05) +CELLS = [(s, l) for s in SHORT_DELTAS for l in LONG_DELTAS if abs(l) < abs(s)] +FILLS = ("bid_ask", "mid") # `mid` = diagnostica, NON selezionabile +ASSETS = ("BTC", "ETH") +CANON = (-0.28, -0.10) # la cella di VRP01 + + +def hr(t: str = "", ch: str = "=") -> None: + print("\n" + ch * 100) + if t: + print(t) + print(ch * 100) + + +# ================================================================== dati + + +def load_puts() -> pd.DataFrame: + """Solo le PUT, solo le colonne che servono, filtrate in LETTURA.""" + import pyarrow.dataset as ds + + cols = ["ts", "asset", "instrument_name", "strike", "option_type", "exp", + "bid", "ask", "mid", "iv", "delta", "open_interest", "quote_status"] + d = ds.dataset(str(CHAIN_DIR), format="parquet") + df = d.to_table(columns=cols, filter=(ds.field("option_type") == "P")).to_pandas() + df["ts"] = pd.to_datetime(df["ts"], utc=True).dt.as_unit("ns") + df["exp"] = pd.to_datetime(df["exp"], utc=True) + df["dte"] = (df["exp"] - df["ts"]).dt.total_seconds() / 86400.0 + return df.drop_duplicates(subset=["ts", "instrument_name"], keep="last").reset_index(drop=True) + + +def venue_read(live: bool) -> dict: + """Specifiche e liquidita' lette DAL VENUE. Un fallimento si dichiara, non si inventa.""" + if not live and CACHE.exists(): + return json.loads(CACHE.read_text()) + + def api(path, **kw): + q = "&".join(f"{k}={v}" for k, v in kw.items()) + with urllib.request.urlopen( + f"https://www.deribit.com/api/v2/public/{path}?{q}", timeout=25) as r: + return json.load(r)["result"] + + out = {"ok": False, "ts": pd.Timestamp.now(tz="UTC").isoformat()} + try: + out["index"] = {c: api("get_index_price", index_name=f"{c}_usd")["index_price"] + for c in ("btc", "eth")} + specs, summ = {}, [] + for cur in ("BTC", "ETH", "USDC"): + for i in api("get_instruments", currency=cur, kind="option", expired="false"): + fam = i["instrument_name"].split("-")[0] + if fam not in specs: + specs[fam] = {k: i.get(k) for k in + ("min_trade_amount", "contract_size", "settlement_currency", + "quote_currency", "taker_commission", "base_currency")} + summ += api("get_book_summary_by_currency", currency=cur, kind="option") + time.sleep(0.4) + out["specs"] = specs + out["summary"] = [{k: r.get(k) for k in + ("instrument_name", "bid_price", "ask_price", "mid_price", + "mark_price", "mark_iv", "open_interest", "volume", + "underlying_price")} for r in summ] + out["ok"] = True + CACHE.parent.mkdir(parents=True, exist_ok=True) + CACHE.write_text(json.dumps(out)) + except Exception as exc: # noqa: BLE001 + out["error"] = f"{type(exc).__name__}: {exc}" + if CACHE.exists(): + cached = json.loads(CACHE.read_text()) + cached["stale"] = out["error"] + return cached + return out + + +def specs_from_disk() -> dict: + """Le specifiche gia' registrate dal progetto (`data/options_daily/snapshots.jsonl`). + Serve come controllo indipendente della lettura live: due percorsi, stesso numero.""" + mins, fees, last_ts = {}, {}, {} + if not SNAP_JSONL.exists(): + return {} + with SNAP_JSONL.open() as f: + for line in f: + try: + r = json.loads(line) + except Exception: # noqa: BLE001, S112 + continue + if r.get("rec") != "chain": + continue + c = r["currency"] + mins.setdefault(c, set()).add(r.get("min_trade_amount")) + fees.setdefault(c, set()).add(r.get("taker_comm")) + last_ts[c] = r["snap_ts"] + return {c: {"min": sorted(x for x in mins[c] if x is not None), + "taker": sorted(x for x in fees[c] if x is not None), + "ts": pd.Timestamp(last_ts[c], unit="ms", tz="UTC")} for c in mins} + + +# ================================================================== struttura + + +def leg_fee_usd(prem_base: float, spot: float) -> float: + """Fee di listino per UNA gamba, per contratto (1 unita' di sottostante), in USD.""" + return min(FEE_UNDERLYING * spot, FEE_PREMIUM_CAP * prem_base * spot) + + +def entry_snapshots(puts: pd.DataFrame, asset: str, offset_h: float = 0.0) -> dict: + """Per ogni scadenza settimanale: lo snapshot piu' vicino a (7 giorni + offset) dalla scadenza. + + L'offset e' l'ANCORA: entrare alle 08:00 del venerdi' precedente e' UNA delle scelte possibili, + e questo progetto ha misurato 4 volte che l'ancora e' un max-of-k non dichiarato. + """ + p = puts[(puts["asset"] == asset) & (puts["dte"] >= DTE_LO) & (puts["dte"] <= DTE_HI)] + tgt = DTE_TARGET + offset_h / 24.0 + out = {} + for exp, g in p.groupby("exp"): + ts_all = np.array(sorted(g["ts"].unique())) + if not len(ts_all): + continue + dte = (exp - pd.DatetimeIndex(ts_all)).total_seconds() / 86400.0 + ok = (dte >= DTE_LO) & (dte <= DTE_HI) + if not ok.any(): + continue + cand = ts_all[ok][np.argsort(np.abs(dte[ok] - tgt))] + for ts in cand[:6]: # al piu' 6 tentativi per scadenza + snap = g[g["ts"] == ts] + if snap["bid"].notna().sum() >= 2: + out[exp] = (pd.Timestamp(ts), snap) + break + return out + + +def build_trades(puts: pd.DataFrame, asset: str, short_d: float, long_d: float, + fill: str = "bid_ask", offset_h: float = 0.0, + lot: float = 1.0, today: pd.Timestamp | None = None) -> pd.DataFrame: + """Un trade per scadenza: ingresso su quote REALI, tenuto a scadenza, regolato sull'indice.""" + today = today or pd.Timestamp.now(tz="UTC") + S = CB.spot_series(asset) + V = CB.dvol_series(asset) + rows, rejected = [], {"gambe_mancanti": 0, "credito_non_positivo": 0} + + for exp, (ts, snap) in entry_snapshots(puts, asset, offset_h).items(): + if exp >= today: # non ancora regolata: non e' un trade + continue + legs = CB.pick_legs(snap, short_d, long_d) + if legs is None: + rejected["gambe_mancanti"] += 1 + continue + spot = float(S.asof(ts)) + dte = (exp - ts).total_seconds() / 86400.0 + dvol = float(V.asof(ts)) + hist = V[V.index < ts] + ivr = float((hist < dvol).mean()) if len(hist) else np.nan + + if fill == "bid_ask": + ps, pl = legs["bid_short"], legs["ask_long"] # vendi al bid, compra all'ask + else: + ps, pl = legs["mid_short"], legs["mid_long"] + if not (np.isfinite(ps) and np.isfinite(pl)): + rejected["gambe_mancanti"] += 1 + continue + cred = (ps - pl) * spot # USD per 1 unita' di sottostante + if cred <= 0: + # premio non monotono nello strike (difetto certificato di `certify_cb_chain`): + # una put piu' OTM quotata sopra una meno OTM. Non e' una struttura, e' una quota rotta. + rejected["credito_non_positivo"] += 1 + continue + + ST = float(S.asof(exp)) + payoff = max(legs["k_short"] - ST, 0.0) - max(legs["k_long"] - ST, 0.0) + fee = leg_fee_usd(ps, spot) + leg_fee_usd(pl, spot) + for K in (legs["k_short"], legs["k_long"]): # consegna sulle gambe ITM + intr = max(K - ST, 0.0) + if intr > 0: + fee += min(FEE_DELIVERY * ST, FEE_PREMIUM_CAP * intr) + + width = legs["k_short"] - legs["k_long"] + mod = CB.f_factors(legs, spot, dvol / 100.0, dte) + rows.append(dict( + asset=asset, ts=ts, exp=exp, dte=dte, spot=spot, ST=ST, + inst_short=legs["inst_short"], inst_long=legs["inst_long"], + cred_real=cred, # per 1 unita': lo vuole CB.close_cost_path + k_short=legs["k_short"], k_long=legs["k_long"], width=width, + d_short=legs["d_short"], d_long=legs["d_long"], + cred=cred * lot, payoff=payoff * lot, fee=fee * lot, + pnl=(cred - payoff - fee) * lot, + fee_sleeve=SLEEVE_FEE_FRAC * mod["cred_mod"] * lot, + fee_sleeve_su_reale=SLEEVE_FEE_FRAC * cred * lot, + cred_mod=mod["cred_mod"] * lot, + risk=width * lot - cred * lot, + dvol=dvol, ivrank=ivr, f_net=mod["f_net"], + und_ret=ST / spot - 1.0, + )) + df = pd.DataFrame(rows) + if not df.empty: + df = df.sort_values("exp").reset_index(drop=True) + df.attrs["rejected"] = rejected + return df + + +def stats(tr: pd.DataFrame) -> dict: + """Metriche di una serie di trade settimanali. Il denominatore e' il RISCHIO impegnato.""" + if tr.empty or len(tr) < 2: + return dict(n=len(tr), sharpe=np.nan, mean=np.nan, t=np.nan) + r = (tr["pnl"] / tr["risk"]).to_numpy(float) # ritorno sul capitale a rischio + sd = float(np.std(r, ddof=1)) + m = float(np.mean(r)) + eq = np.cumprod(1.0 + r) + dd = float(np.max((np.maximum.accumulate(eq) - eq) / np.maximum.accumulate(eq))) + return dict(n=len(tr), mean=m, sd=sd, + sharpe=(m / sd * np.sqrt(WEEKS_PER_YEAR)) if sd > 0 else np.nan, + t=(m / (sd / np.sqrt(len(r)))) if sd > 0 else np.nan, + maxdd_close=dd, wins=int((tr["pnl"] > 0).sum()), + usd_week=float(tr["pnl"].mean()), usd_tot=float(tr["pnl"].sum())) + + +def weekly_series(tr: pd.DataFrame) -> pd.Series: + return pd.Series((tr["pnl"] / tr["risk"]).to_numpy(float), + index=pd.DatetimeIndex(tr["exp"])).sort_index() + + +def boot_ci(x: np.ndarray, n: int = 4000, seed: int = 822) -> tuple[float, float]: + rng = np.random.default_rng(seed) + if len(x) < 2: + return (np.nan, np.nan) + bs = rng.choice(x, size=(n, len(x)), replace=True).mean(axis=1) + return float(np.percentile(bs, 2.5)), float(np.percentile(bs, 97.5)) + + +# ================================================================== main + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--no-live", action="store_true", help="non interrogare il venue") + args = ap.parse_args() + + print(__doc__.split("\n\n")[0]) + puts = load_puts() + today = pd.Timestamp.now(tz="UTC") + venue = venue_read(not args.no_live) + + # ------------------------------------------------------------ Β§1 REGIME + hr("Β§1 LIMITE DI REGIME β€” il campione descrive il regime in cui la strategia sta FLAT") + print("Non e' una nota a pie' di pagina: VRP01 entra SOLO se IV-rank > 0.30 (il gate e' l'alpha,\n" + "misurato 4 volte). Se nel campione quel gate non scatta mai, il campione non contiene\n" + "la strategia β€” contiene la sua astensione.\n") + for a in ASSETS: + V = CB.dvol_series(a) + win = V[(V.index >= puts["ts"].min()) & (V.index <= puts["ts"].max())] + hist = V[V.index < win.index[0]].to_numpy(float) + print(f" {a}: DVOL nel campione min {win.min():.1f} / mediana {win.median():.1f} / " + f"max {win.max():.1f}") + print(f" vs storia 2021+ ({len(hist)} giorni): la MEDIANA del campione sta al " + f"{float((hist < win.median()).mean()):.0%} pctl, il suo MASSIMO al " + f"{float((hist < win.max()).mean()):.0%} pctl") + print("\n \u26a0\ufe0f Due statistiche diverse, da tenere distinte: CLAUDE.md cita Β«DVOL max al 24\u00b0/28\u00b0\n" + " pctlΒ» dal campione del 30/07. Un MASSIMO cresce con la finestra β€” e' una statistica\n" + " di ESTREMO, non di livello. Per dire in che regime siamo la lente e' la MEDIANA, ed\n" + " e' bassissima.") + + # ------------------------------------------------------------ Β§2 CAMPIONE + hr("Β§2 IL CAMPIONE β€” quante settimane ci sono DAVVERO") + pre = puts[puts["ts"] < pd.Timestamp("2026-06-09", tz="UTC")] + print(f" catena su disco: {puts['ts'].min():%Y-%m-%d} β†’ {puts['ts'].max():%Y-%m-%d} " + f"({(puts['ts'].max() - puts['ts'].min()).days} giorni, {len(puts):,} righe put)") + if not pre.empty: + print(f" ma PRIMA del 2026-06-09 l'archivio ereditato seguiva UNA sola scadenza, in " + f"finestra {pre['dte'].min():.0f}-{pre['dte'].max():.0f} DTE:") + print(f" β†’ la finestra settimanale {DTE_LO:.0f}-{DTE_HI:.0f} DTE NON ESISTE prima " + f"della raccolta full-chain.") + print("\n Trade REGOLATI utilizzabili (cella canonica VRP01 Ξ΄-0.28/-0.10, fill bid/ask):") + canon: dict[str, pd.DataFrame] = {} + for a in ASSETS: + lot = 1.0 + tr = build_trades(puts, a, *CANON, lot=lot, today=today) + canon[a] = tr + rej = tr.attrs.get("rejected", {}) if not tr.empty else {} + print(f" {a}: {len(tr)} settimane regolate ({tr['exp'].min():%Y-%m-%d} β†’ " + f"{tr['exp'].max():%Y-%m-%d}) scartate: {rej}") + print("\n ⚠️ Β«3,7 mesi β‰ˆ 16 scadenze settimanaliΒ» e' FALSO su questi dati: sono 10 per asset.") + + print("\n Gate IV-rank>0.30 di VRP01 sulle settimane utilizzabili:") + tot = passed = 0 + for a in ASSETS: + tr = canon[a] + n = int((tr["ivrank"] > 0.30).sum()) + tot += len(tr) + passed += n + print(f" {a}: {n}/{len(tr)} passano (IV-rank min {tr['ivrank'].min():.3f} / " + f"max {tr['ivrank'].max():.3f})") + print(f" TOTALE: {passed}/{tot}. Il sleeve sarebbe stato FLAT per l'intero campione.") + + print("\n QUANDO TORNERA' MISURABILE β€” il gate non e' chiuso per sempre, e la soglia e' un numero.") + print(" Regola del 19/06: Β«rivalutare quando la catena cattura un crashΒ». Un criterio senza\n" + " numero non e' un criterio, quindi eccolo (IV-rank espandente causale, come nel sleeve):") + for a in ASSETS: + V = CB.dvol_series(a) + vals = V.to_numpy(float) + ivr = np.full(len(vals), np.nan) + for i in range(1, len(vals)): + ivr[i] = (vals[:i] < vals[i]).mean() + ser = pd.Series(ivr, index=V.index).dropna() + per_year = ser.groupby(ser.index.year).apply(lambda x: float((x > 0.30).mean())) + thr = float(np.quantile(vals[:-1], 0.30)) + last_open = ser[ser > 0.30] + since = (V.index[-1] - last_open.index[-1]).days if len(last_open) else None + inwin = ser[(ser.index >= puts["ts"].min()) & (ser.index <= puts["ts"].max())] + near = inwin.max() if len(inwin) else float("nan") + print(f" {a}: quota di giorni col gate APERTO per anno β€” " + + " ".join(f"{y}:{v:.0%}" for y, v in per_year.items())) + print(f" DVOL oggi {vals[-1]:.1f} Β· serve DVOL > {thr:.1f} " + f"(+{thr / vals[-1] - 1:.0%}) per riaprire il gate Β· ultimo giorno aperto " + f"{last_open.index[-1]:%Y-%m-%d} ({since} giorni fa)") + print(f" quanto vicino c'e' andato DENTRO la finestra: IV-rank max " + f"{near:.4f} il {inwin.idxmax():%Y-%m-%d} β€” " + f"{'mancato per ' + format(0.30 - near, '.4f') if near <= 0.30 else 'aperto'}") + print(" β†’ il campione non e' 'sfortunato': cade dentro un regime di vol bassa lungo e\n" + " persistente. Ma il margine NON e' comodo: il gate ha sfiorato l'apertura, e\n" + " bastano ~+10% di DVOL per riaprirlo. La misura giusta si potra' rifare quando quel numero sara' superato,\n" + " e da allora servira' comunque un anno di settimane per avere un campione.") + + ret = {a: (canon[a]["ST"].iloc[-1] / canon[a]["spot"].iloc[0] - 1.0) for a in ASSETS} + print(f"\n E il sottostante nel campione: BTC {ret['BTC']:+.1%}, ETH {ret['ETH']:+.1%} β€” " + f"vol implicita bassa E rialzo forte\n = il regime IDEALE per vendere put. " + f"Il campione e' il migliore possibile per questa struttura, non uno neutro.") + + # ------------------------------------------------------------ Β§3 ESEGUIBILITA' + hr("Β§3 ESEGUIBILITA' β€” e qui il filone cambia domanda") + fams = sorted({n.split("-")[0] for n in puts["instrument_name"].head(50_000)}) + print(f" La catena che raccogliamo contiene le famiglie: {fams}") + print(" β†’ sono le opzioni INVERSE: quotate, marginate e regolate in BTC/ETH.") + print(" Il conto e' in USDC ($635) e il book esegue sui perpetual LINEARI USDC.\n") + + disk = specs_from_disk() + if disk: + print(" Specifiche gia' registrate dal progetto (data/options_daily/snapshots.jsonl):") + for c, v in sorted(disk.items()): + print(f" {c:6s} min_trade_amount={v['min']} taker={v['taker']} ({v['ts']:%Y-%m-%d %H:%M})") + + if venue.get("ok"): + idx = venue["index"] + print(f"\n Lette dal venue ora β€” indice BTC ${idx['btc']:,.0f} / ETH ${idx['eth']:,.0f}:") + print(f" {'famiglia':11s} {'min lotto':>10s} {'settle':>7s} {'quote':>7s} " + f"{'$ nozionale/lotto':>18s}") + for fam in ("BTC", "ETH", "BTC_USDC", "ETH_USDC"): + s = venue["specs"].get(fam) + if not s: + continue + base = (s.get("base_currency") or fam.split("_")[0]).lower() + notion = float(s["min_trade_amount"]) * idx[base] + print(f" {fam:11s} {s['min_trade_amount']:>10} {str(s['settlement_currency']):>7s} " + f"{str(s['quote_currency']):>7s} {notion:>17,.0f}$") + + print("\n πŸ“Œ Esiste una famiglia USDC-lineare per BTC ed ETH, con lotto minimo 10x PIU'") + print(" PICCOLO e regolata nella valuta del conto. Il muro Β«BTC fuori a $3.000Β» del") + print(" 30/07 β€” congelato in tests/test_vrp_profit_take.py β€” e' stato misurato sulla") + print(" famiglia che il conto NON margina.") + print(" E' la 3a occorrenza della stessa forma dopo fee_watch (21/08, sorvegliava gli") + print(" INVERSE mentre il book trada i LINEARI) e la taratura di venue_watch.") + + # --- ma la famiglia eseguibile e' quotata davvero? --- + d = pd.DataFrame(venue["summary"]) + parts = d["instrument_name"].str.split("-") + d["fam"] = parts.str[0] + d["typ"] = parts.str[3] + d["strike"] = pd.to_numeric(parts.str[2].str.replace("d", ".", regex=False), errors="coerce") + d["expd"] = pd.to_datetime(parts.str[1], format="%d%b%y", utc=True) + pd.Timedelta(hours=8) + d["dte"] = (d["expd"] - today).dt.total_seconds() / 86400.0 + d["base"] = d["fam"].str.replace("_USDC", "", regex=False).str.lower() + d = d[d["typ"].eq("P") & d["base"].isin(["btc", "eth"])].copy() + d["S"] = d["base"].map(idx) + d["mny"] = d["strike"] / d["S"] + q = d[(d["bid_price"] > 0) & (d["ask_price"] > 0)].copy() + q["relsp"] = (q["ask_price"] - q["bid_price"]) / q["mid_price"] + # regione delle put OTM usate dalla struttura (Ξ΄ -0.35..-0.05 β‰ˆ moneyness 0.78-0.97) + w = q[(q["dte"] > 3) & (q["dte"] < 11) & q["mny"].between(0.78, 0.97)] + print("\n Liquidita' REALE nella regione di strike che serve (scadenza settimanale, ora):") + print(f" {'famiglia':11s} {'n quotate':>9s} {'spread rel. med':>16s} " + f"{'p75':>7s} {'OI med':>9s} {'vol 24h':>10s}") + for fam, g in w.groupby("fam"): + allf = d[d["fam"] == fam] + print(f" {fam:11s} {len(g):>9d} {g['relsp'].median():>15.0%} " + f"{g['relsp'].quantile(.75):>6.0%} {g['open_interest'].median():>9.1f} " + f"{allf['volume'].sum():>10,.0f}") + print("\n Lo spread relativo e' ENORME in entrambe le famiglie (il mid non e' un prezzo\n" + " eseguibile: e' la media di due prezzi lontani), e sulla famiglia USDC β€” l'unica\n" + " marginabile dal conto β€” l'open interest e' ~0: quotata da un market maker,\n" + " praticamente non tradata. Comprare l'ala li' dentro costa il doppio.") + else: + print(f"\n ⚠️ LETTURA DAL VENUE NON RIUSCITA: {venue.get('error', 'n/d')}") + print(" Le specifiche delle famiglie USDC restano NON MISURATE in questo giro.") + + print("\n Capitale per UN lotto minimo, cella canonica (limiti dichiarati, vedi docstring):") + print(f" {'asset':6s} {'lotto':>7s} {'width med':>10s} {'rischio max':>12s} " + f"{'cash-secured':>13s} {'credito/sett':>13s}") + minlot = {"BTC": 0.1, "ETH": 1.0} + if venue.get("ok"): + for fam in ("BTC", "ETH"): + if fam in venue["specs"]: + minlot[fam] = float(venue["specs"][fam]["min_trade_amount"]) + for a in ASSETS: + tr, lot = canon[a], minlot[a] + wid = float(tr["width"].median()) + print(f" {a:6s} {lot:>7.2f} {wid:>10,.0f} {wid * lot - tr['cred'].median() * lot:>11,.0f}$ " + f"{tr['k_short'].median() * lot:>12,.0f}$ {tr['cred'].median() * lot:>12,.2f}$") + print(" Sul rischio DEFINITO un lotto minimo inverse sta dentro $600. Sul cash-secured no.\n" + " Quale dei due valga dipende dal regime di margine del conto (standard vs portfolio),\n" + " che NON e' leggibile da dati pubblici.") + print(" ⚠️ E c'e' una domanda PRIMA di quella, che nessun dato pubblico chiude: un saldo in\n" + " USDC puo' marginare un'opzione INVERSE (collaterale in BTC/ETH)? Se no, la famiglia\n" + " con 3 mesi di storia non e' negoziabile da questo conto a NESSUN capitale.\n" + " Precedente esatto: il 26/07 GTAA01 aveva 5 settimane di misure sopra l'assunzione\n" + " che i 6 ETF fossero comprabili β€” il blocco PRIIPs si scopri' con UN ordine di prova.\n" + " La regola che ne usci' (Β«la negoziabilita' sul conto REALE si verifica quando lo\n" + " sleeve entra in RICERCA, non quando entra nel bookΒ») si applica qui identica, e\n" + " questo script NON puo' eseguirla: richiede il conto, non i dati.") + + # ------------------------------------------------------------ Β§4 BACKTEST + hr("Β§4 BACKTEST SU QUOTE REALI β€” griglia dichiarata") + n_trials = len(CELLS) * len(FILLS) * (len(ASSETS) + 1) + print(f" celle Ξ΄ (short,long): {len(CELLS)} fill: {len(FILLS)} " + f"asset: {len(ASSETS)} + combinato 50/50") + print(f" β†’ TRIAL DICHIARATI AL RIALZO: {n_trials}. Nessuna griglia ridotta per budget.") + print(f" (Il fill `mid` non e' selezionabile β€” e' la diagnostica di quanto costa lo spread β€”\n" + f" ma conta come trial: e' stato guardato.)\n") + + grid = [] + for fill in FILLS: + for (sd, ld) in CELLS: + per, comb = {}, [] + for a in ASSETS: + tr = build_trades(puts, a, sd, ld, fill=fill, lot=1.0, today=today) + per[a] = stats(tr) + if not tr.empty: + comb.append(weekly_series(tr)) + row = dict(fill=fill, sd=sd, ld=ld) + for a in ASSETS: + row[f"{a}_sh"] = per[a]["sharpe"] + row[f"{a}_n"] = per[a]["n"] + if comb: + c = pd.concat(comb, axis=1).mean(axis=1).dropna() + row["comb_sh"] = (float(c.mean() / c.std(ddof=1) * np.sqrt(WEEKS_PER_YEAR)) + if c.std(ddof=1) > 0 else np.nan) + row["comb_n"] = len(c) + grid.append(row) + G = pd.DataFrame(grid) + print(G.round(2).to_string(index=False)) + + print("\n ⚠️ COME SI LEGGE LA TABELLA SOPRA. Gli 'Sharpe' a due cifre NON sono un risultato:\n" + " sono la firma del campione. Annualizzare (x√52) 10 osservazioni settimanali in cui\n" + " la coda sinistra non si e' manifestata produce numeri privi di significato β€” e il\n" + " fatto che l'INTERA griglia li produca e' esattamente il segnale, non un caso da\n" + " selezionare. E' la 4a occorrenza della firma '0-perdite / Sharpe implausibile'\n" + " dopo CC01, le celle deep-OTM Albimarini e meta' della griglia VRP del 03/07.\n" + " Per lo stesso motivo NON viene riportato un CAGR: comporre 10 settimane a 52\n" + " sarebbe una fabbricazione, non una stima.") + + print("\n Cella canonica di VRP01 (Ξ΄-0.28/-0.10), fill conservativo, per asset β€” numeri\n" + " CONCRETI (dollari per lotto minimo), che a questa taglia dicono piu' di un rapporto:") + print(f" {'asset':6s} {'n':>3s} {'vinte':>7s} {'$/sett':>9s} {'$ tot':>9s} " + f"{'% rischio/sett':>15s} {'t':>6s} {'maxDD chiusure':>15s}") + st = {} + for a in ASSETS: + tr = canon[a].copy() + tr[["cred", "payoff", "fee", "pnl", "risk", "cred_mod", + "fee_sleeve", "fee_sleeve_su_reale"]] *= minlot[a] + st[a] = stats(tr) + s = st[a] + print(f" {a:6s} {s['n']:>3d} {s['wins']:>3d}/{s['n']:<3d} {s['usd_week']:>8.2f}$ " + f"{s['usd_tot']:>8.2f}$ {s['mean']:>14.2%} {s['t']:>6.2f} {s['maxdd_close']:>14.1%}") + print(" maxDD 0.0% su BTC significa che NON C'E' STATA una settimana in perdita: e' una\n" + " proprieta' del campione, non della struttura.") + + print("\n Costo dello SPREAD, misurato (stesso ingresso, fill al mid vs al bid/ask):") + for a in ASSETS: + ba = build_trades(puts, a, *CANON, fill="bid_ask", today=today) + md = build_trades(puts, a, *CANON, fill="mid", today=today) + j = ba.merge(md, on="exp", suffixes=("_ba", "_md")) # appaiato per INGRESSO + if j.empty: + continue + print(f" {a}: credito medio al mid ${j['cred_md'].mean():.2f} β†’ al bid/ask " + f"${j['cred_ba'].mean():.2f} = si perde il " + f"{1 - j['cred_ba'].mean() / j['cred_md'].mean():.0%} del credito solo attraversando " + f"lo spread") + + print("\n Il f del 30/07, ri-misurato su 10 settimane invece di 8 (fill conservativo):") + for a in ASSETS: + f = canon[a]["f_net"].dropna() + lo, hi = boot_ci(f.to_numpy()) + print(f" {a}: f_net mediana {f.median():.3f} media {f.mean():.3f} " + f"IC95 [{lo:.3f}, {hi:.3f}] β‰₯1.0 in {int((f >= 1).sum())}/{len(f)}") + allf = pd.concat([canon[a]["f_net"] for a in ASSETS]).dropna() + lo, hi = boot_ci(allf.to_numpy()) + print(f" pooled: {allf.median():.3f} (IC95 [{lo:.3f}, {hi:.3f}], n={len(allf)}) β€” " + f"replica indipendente dello 0.73 del 30/07") + + print("\n Fee: listino vero contro il forfait dello sleeve (12.5% del credito NETTO).") + print(" ⚠️ Il confronto giusto e' col credito MODELLATO β€” e' quello a cui il sleeve applica\n" + " il forfait. Misurarlo sul credito reale (piu' piccolo di ~30%) sottostima l'errore:") + for a in ASSETS: + tr = canon[a] + print(f" {a}: fee reale ${tr['fee'].mean():.2f}/settimana per contratto vs forfait sul\n" + f" credito MODELLATO ${tr['fee_sleeve'].mean():.2f} (= {tr['fee_sleeve'].mean() / tr['fee'].mean():.2f}x)" + f" Β· sul credito reale ${tr['fee_sleeve_su_reale'].mean():.2f} " + f"(= {tr['fee_sleeve_su_reale'].mean() / tr['fee'].mean():.2f}x)") + print(" Il '~2x' pubblicato il 30/07 non si replica qui: su queste 19 settimane il forfait\n" + " sovrastima le fee di 1.1-1.9x a seconda dell'asset e della base di confronto.\n" + " Resta vero il segno (il forfait e' CONSERVATIVO), non la taglia.") + + # ------------------------------------------------------------ Β§5 IL MINIMO + hr("Β§5 IL RISCHIO STA NEL MINIMO β€” le chiusure settimanali non lo vedono") + print(" Regola del 30/07: un rischio valutato sul minimo non si misura sulle chiusure.\n" + " Qui il mark infra-settimana viene dalle standing quotes ORARIE delle STESSE due gambe\n" + " (ricomprare la corta all'ask, rivendere la lunga al bid: di nuovo conservativo).\n") + worst_rows = [] + for a in ASSETS: + print(f" {a} (lotto {minlot[a]})") + for _, r in canon[a].iterrows(): + path = CB.close_cost_path(r, puts) + if path.empty: + continue + lot = minlot[a] + risk = r["width"] * lot - r["cred"] * lot + worst = float(path["pnl_aperto"].min()) * lot + worst_rows.append(dict(asset=a, exp=r["exp"], worst=worst, risk=risk, + frac=worst / risk, pnl=r["pnl"] * lot, + n_marks=len(path))) + flag = " ← chiusa in UTILE" if r["pnl"] > 0 and worst < 0 else "" + print(f" {r['exp']:%Y-%m-%d} marks={len(path):3d} " + f"peggior mark {worst:>9.2f}$ = {worst / risk:>7.1%} del rischio " + f"esito {r['pnl'] * lot:>7.2f}${flag}") + W = pd.DataFrame(worst_rows) + if not W.empty: + print(f"\n Peggior mark infra-settimana: mediana {W['frac'].median():.1%} del rischio, " + f"minimo {W['frac'].min():.1%}") + wmin = W.loc[W['frac'].idxmin()] + print(f" Denominatore, per non fabbricare una discrepanza: qui il rischio e' " + f"`width - credito`.\n" + f" CLAUDE.md cita Β«-81,7% del capitaleΒ» per la STESSA settimana " + f"({wmin['asset']} {wmin['exp']:%Y-%m-%d}):\n" + f" e' lo stesso mark (${wmin['worst']:.2f}) diviso per la width nuda. Due " + f"denominatori, un solo fatto.") + wl = W[W["pnl"] > 0] + print(f" Fra le sole settimane chiuse in UTILE ({len(wl)}/{len(W)}): mediana " + f"{wl['frac'].median():.1%}, minimo {wl['frac'].min():.1%}") + print(" β†’ il maxDD sulle chiusure e' una lente che non vede il rischio che si e' corso.") + + # ------------------------------------------------------------ Β§6 GATE + hr("Β§6 GATE β€” compresi quelli che NON si possono far girare") + + print(" (a) implausible_sharpe β€” il gate del 26/07 sul rischio FUORI dal campione") + for a in ASSETS: + tr = canon[a] + daily = pd.Series((tr["pnl"] / tr["risk"]).to_numpy(float), + index=pd.DatetimeIndex(tr["exp"])) + rep = A.implausible_sharpe(daily, n_trades=len(tr), n_losing_trades=int((tr["pnl"] <= 0).sum())) + print(f" {a}: implausible={rep['implausible']} vinte {int((tr['pnl'] > 0).sum())}/{len(tr)}") + for why in rep["reasons"]: + print(f" Β· {why}") + if int((tr["pnl"] <= 0).sum()) == 0: + print(f" Β· regola del tre: 0 perdite su {len(tr)} lascia un tasso VERO fino a " + f"{3 / len(tr):.0%}. Su un payoff che perde {tr['risk'].median() / tr['cred'].median():.1f}x " + f"il credito, basta a ribaltare l'expectancy.") + + print("\n (b) deflated_sharpe β€” NON CALCOLABILE, e il perche' e' il risultato") + for a in ASSETS: + s = weekly_series(canon[a]) + dsr, _ = A.deflated_sharpe(st[a]["sharpe"], [r["comb_sh"] for r in grid if + np.isfinite(r.get("comb_sh", np.nan))], + s, dpy=WEEKS_PER_YEAR) + print(f" {a}: DSR = {dsr} (T={len(s)} osservazioni settimanali; la funzione " + f"richiede T>=30)") + print(" Costruire una serie GIORNALIERA per superare il T>=30 sarebbe barare: uno sleeve\n" + " settimanale su griglia giornaliera e' ~94% di zeri (lezione 26/07 su VRP01), e il\n" + " T gonfiato entra nel deflated-Sharpe come se fossero osservazioni indipendenti.\n" + " Statistica onesta a questa taglia = il t con il suo intervallo:") + for a in ASSETS: + r = (canon[a]["pnl"] / canon[a]["risk"]).to_numpy(float) + lo, hi = boot_ci(r) + print(f" {a}: media settimanale {r.mean():+.2%} del rischio, IC95 " + f"[{lo:+.2%}, {hi:+.2%}], t={st[a]['t']:.2f}, n={len(r)}") + + print("\n (c) anchor_luck_band β€” l'ora d'ingresso e' un'ancora (max-of-k mai dichiarato)") + offsets = list(range(-24, 25, 6)) + for a in ASSETS: + def by_off(o, _a=a): + return weekly_series(build_trades(puts, _a, *CANON, offset_h=float(o), today=today)) + band = A.anchor_luck_band( + by_off, offsets, canonical=0, + metric=lambda s: (float(s.mean() / s.std(ddof=1) * np.sqrt(WEEKS_PER_YEAR)) + if len(s) > 2 and s.std(ddof=1) > 0 else 0.0)) + if band.get("reason"): + print(f" {a}: {band['reason']}") + continue + print(f" {a}: canonica {band['canonical']:.2f} (pctl {band['canonical_pctl']:.0%}) Β· " + f"MEDIANA ONESTA {band['median']:.2f} Β· banda [{band['lo']:.2f}, {band['hi']:.2f}] Β· " + f"positiva in {band['frac_positive']:.0%} di {band['n_anchors']} ancore Β· " + f"fortuna {band['luck']:+.2f}") + # DOVE vive l'instabilita': nel numeratore (il guadagno) o nel denominatore (la vol)? + det = [] + for o in offsets: + w = by_off(o) + if len(w) > 2 and w.std(ddof=1) > 0: + det.append((o, float(w.mean()), float(w.std(ddof=1)), len(w))) + if det: + mm = np.array([d[1] for d in det]) + ss = np.array([d[2] for d in det]) + # il rapporto si stampa SOLO se il minimo e' positivo: max/min con min<0 + # e' un numero senza significato (prima stesura: '112.240.864x'). + rm = (f"{mm.max() / mm.min():.1f}x" if mm.min() > 0 else "SEGNO CHE CAMBIA") + print(f" media settimanale fra le ancore: [{mm.min():+.2%}, {mm.max():+.2%}] " + f"({rm}) Β· deviazione standard: [{ss.min():.2%}, {ss.max():.2%}] " + f"({ss.max() / ss.min():.1f}x)") + print(f" β†’ si muovono ENTRAMBI, numeratore e denominatore" + f"{' β€” e sulla media il segno CAMBIA' if mm.min() <= 0 else ''}.\n" + f" Il motivo e' meccanico e vale la pena dirlo: spostare l'ora d'ingresso\n" + f" cambia lo SNAPSHOT, quindi quali strike combaciano col delta bersaglio\n" + f" β€” non e' lo stesso trade un'ora dopo, e' un'altra struttura. Con\n" + f" {det[0][3]} settimane questo parametro di disturbo domina tutto il resto.") + + print("\n (d) null del de-levering / beta β€” 'e' reddito da volatilita' o crypto travestito?'") + print(" Lo spread ha delta positivo (β‰ˆ +0.18 all'ingresso): in un campione che sale del") + print(" 20-50% guadagna PER COSTRUZIONE. Regressione del PnL settimanale sul ritorno") + print(" del sottostante nella stessa settimana:") + for a in ASSETS: + tr = canon[a] + x = tr["und_ret"].to_numpy(float) + y = (tr["pnl"] / tr["risk"]).to_numpy(float) + if len(x) < 4: + continue + b, alpha = np.polyfit(x, y, 1) + resid = y - (b * x + alpha) + se = float(np.std(resid, ddof=2) / (np.std(x) * np.sqrt(len(x)))) + print(f" {a}: beta {b:+.3f} (t={b / se:.2f}) alpha {alpha:+.2%}/settimana " + f"corr {np.corrcoef(x, y)[0, 1]:+.2f}") + k = abs(b) + lev = pd.Series(k * x, index=pd.DatetimeIndex(tr["exp"])) + sh_lev = (float(lev.mean() / lev.std(ddof=1) * np.sqrt(WEEKS_PER_YEAR)) + if lev.std(ddof=1) > 0 else np.nan) + print(f" null: essere semplicemente LONG il sottostante a leva {k:.2f} " + f"(stesso beta) da' Sharpe {sh_lev:.2f} contro {st[a]['sharpe']:.2f} della struttura") + + print("\n (e) marginal_vs_tp01 β€” girato, ma NON interpretabile a questa taglia") + try: + comb = pd.concat([weekly_series(canon[a]) for a in ASSETS], axis=1).mean(axis=1).dropna() + dly = comb.resample("1D").sum().fillna(0.0) + rep = A.marginal_vs_tp01(dly) + act = int((dly != 0).sum()) + def _f(k): + v = rep.get(k) + return f"{v:.3f}" if isinstance(v, (int, float)) and v is not None else str(v) + print(f" verdetto={rep.get('verdict')} corr={_f('corr')} " + f"insample_edge={rep.get('has_insample_edge')} robust_oos={rep.get('robust_oos')} " + f"is_hedge={rep.get('is_hedge')}") + print(f" barre ATTIVE {act}/{len(dly)} = {act / len(dly):.0%} β€” un `None` qui non e'\n" + f" un bug: le sotto-misure che richiedono un hold-out non hanno dati per\n" + f" esistere, e la funzione lo dichiara invece di inventare un numero.") + print(f" ⚠️ {len(comb)} settimane su un hold-out che non esiste: il verdetto e' " + f"rumore, si riporta per non ometterlo.") + except Exception as exc: # noqa: BLE001 + print(f" NON GIRATO: {type(exc).__name__}: {exc}") + + print("\n (f) causality_ok β€” non girato su questo candidato, e perche'") + print(" `A.causality_ok` valuta target_fn su OHLCV BTC/ETH; qui il segnale non e' una") + print(" serie di prezzo ma una selezione di strumenti da uno snapshot. Verifica") + print(" equivalente eseguita per costruzione e controllata qui sotto:") + bad = 0 + for a in ASSETS: + for _, r in canon[a].iterrows(): + if r["ts"] >= r["exp"] or not (DTE_LO <= r["dte"] <= DTE_HI): + bad += 1 + print(f" ingressi con ts >= scadenza o fuori finestra DTE: {bad}/" + f"{sum(len(canon[a]) for a in ASSETS)} (le gambe si scelgono da UNO snapshot allo\n" + f" stesso ts; il regolamento usa l'indice alla scadenza, mai prima)") + + # ------------------------------------------------------------ Β§7 VERDETTO + hr("Β§7 VERDETTO") + print(""" SCARTATO β€” su questi dati la domanda non e' decidibile. Quattro motivi, indipendenti + l'uno dall'altro: ciascuno da solo basterebbe. + + 1. IL CAMPIONE NON CONTIENE LA STRATEGIA. 0 settimane su 19 passano il gate IV-rank>0.30 + che E' l'alpha di VRP01 β€” il sleeve sarebbe stato FLAT per l'intero campione. La DVOL + mediana sta al 7 (BTC) / 11 (ETH) percentile della storia 2021+, e il sottostante e' + salito del +25% / +44%: vol implicita bassa E rialzo forte = il regime MIGLIORE + possibile per vendere put, non uno neutro. Le settimane sono 10 per asset, non 16 + (prima del 2026-06-09 l'archivio seguiva solo la finestra 13-20 DTE: quella 4-10 DTE + non esiste). Esito: 10/10 vincenti su BTC, e la lente giusta non e' lo Sharpe ma la + regola del tre β€” 0 perdite su 10 lascia un tasso vero fino al 30%, su un payoff che + perde ~7x il credito. + ⚠️ Sfumatura che va detta: il gate non e' lontano. Ha mancato l'apertura per 0.0002 di + IV-rank il 2026-06-07, e bastano ~+10% di DVOL per riaprirlo. Il campione e' dentro un + regime di vol bassa persistente, non davanti a un muro. + + 2. IL TITOLO NON SOPRAVVIVE ALL'ORA D'INGRESSO. E' un max-of-k mai dichiarato, misurato + qui per la prima volta su questa struttura: su 9 ancore lo Sharpe canonico BTC 32.58 + sta all'89 percentile, la MEDIANA ONESTA e' 1.45 e la banda tocca il NEGATIVO (-0.35). + Su ETH 3.90 -> 2.41. Il numero da citare per BTC e' 1.45, non 32.58. + ⚠️ E il meccanismo non e' quello che avevo scritto prima di guardarlo. Non e' solo la + vol campionaria a ballare: fra le ancore la MEDIA settimanale BTC va da -1.35% a + +11.22% del rischio β€” CAMBIA SEGNO β€” e la deviazione standard di 11x (ETH: media 2.9x, + sd 2.4x). Spostare l'ora d'ingresso cambia lo snapshot e quindi quali strike combaciano + col delta bersaglio: non e' lo stesso trade un'ora dopo, e' UN'ALTRA STRUTTURA. A 10 + settimane questo parametro di disturbo domina il segnale che si vorrebbe misurare. + + 3. LA FAMIGLIA CHE HA STORIA NON E' QUELLA CHE SI PUO' ESEGUIRE. Le quote raccolte sono + le opzioni INVERSE (margine e regolamento in BTC/ETH); il conto e' in USDC e il book + esegue sui perpetual lineari USDC. Esiste una famiglia USDC-lineare per BTC ed ETH col + lotto minimo 10x piu' piccolo (BTC 0.01 = $773 di nozionale invece di 0.1 = $7.728), + quindi il muro Β«BTC fuori a $3.000Β» del 30/07 β€” congelato in + tests/test_vrp_profit_take.py β€” e' misurato sulla famiglia che il conto non margina. + E' la 3a occorrenza della stessa forma dopo fee_watch (21/08) e la taratura di + venue_watch: un numero puntato su una configurazione diversa da quella che gira. + Ma di quella famiglia abbiamo ZERO ore di storia. + + 4. LO SPREAD E' IL COSTO DOMINANTE, E SULLA FAMIGLIA ESEGUIBILE E' PEGGIO. Attraversare + il bid/ask costa ~10% del credito sugli INVERSE; nella regione di strike che serve lo + spread relativo mediano e' 24-37% sugli inverse e 29-59% sulle USDC, con open interest + ~0. Il f del credito netto si replica indipendentemente a 0.714 (IC95 [0.690, 0.779], + 0/19 osservazioni >= 1.0) β€” conferma dello 0.73 del 30/07 su un campione piu' lungo e + con un percorso diverso. + + CIO' CHE NON SI CONCLUDE: che la struttura non funzioni. Non e' stata osservata nel regime + in cui lavora. E un risultato a favore c'e', va detto: su BTC il null Β«e' solo crypto + travestitoΒ» e' REFUTATO (beta +0.019, t=0.18) β€” il PnL non viene dal rialzo. Non basta, + perche' il punto 2 toglie il titolo comunque. + + LA REGOLA NON CAMBIA. Β«Niente short-vol da MODELLO in deployΒ» non diventa Β«short-vol da + QUOTEΒ»: le quote adesso ci sono, ma sono quote della famiglia sbagliata, in un regime solo, + e con l'ancora che spiega il risultato. La sostituzione della regola resta NON DIMOSTRATA. + + DUE AZIONI, NESSUNA ESEGUITA (toccano la produzione o il conto: decide l'operatore). + + (A) DATO β€” far raccogliere a `collect_chain.py` anche `currency=USDC`. Costo MISURATO col + filtro OI>=100 gia' in vigore: ETH_USDC porterebbe 119 strumenti per giro (+21% di + chiamate, da ~570 a ~690), BTC_USDC ne porterebbe 5 su 686 (0.7%) = nulla di + utilizzabile. Cioe': l'unica struttura USDC-regolata con un libro vero e' su ETH, + lotto minimo 0.1 ETH = $242. Senza questa raccolta, fra sei mesi avremo un anno di + storia della famiglia che non possiamo marginare e zero di quella che potremmo β€” che + e' esattamente la posizione di oggi, un'ondata dopo l'altra. + + (B) CONTO β€” una domanda che nessun dato pubblico chiude e che viene PRIMA di ogni + backtest: un saldo in USDC puo' marginare un'opzione INVERSE? Se no, la famiglia con + 3 mesi di storia non e' negoziabile da questo conto a nessun capitale, e il punto 3 + smette di essere un dettaglio di lotto per diventare un muro. Si risolve come si + risolse il PRIIPs il 26/07: guardando il conto, non i dati. + + SE E QUANDO RIAPRIRLO. Non Β«quando ci sara' un crashΒ» (criterio senza numero) ma: DVOL BTC + > 48.3 o ETH > 62.8 in modo persistente, PIU' ~un anno di settimane raccolte in quel + regime sulla famiglia eseguibile. Prima di allora ogni misura su questa struttura + descrivera' la sua astensione.""") + + hr("COSA MI SMENTIREBBE", "-") + print(""" Tre osservazioni, tutte ottenibili, che ribalterebbero questo verdetto β€” e una che NON + lo ribalterebbe, messa qui perche' e' quella che arrivera' per prima. + + 1. IL CONTO. Se un saldo USDC puo' marginare le opzioni INVERSE, il punto 3 si sgonfia da + muro a dettaglio: la struttura ETH inverse (1 lotto, rischio ~$88, credito ~$12 la + settimana) e' gia' eseguibile a $600 oggi. Resterebbero i punti 1, 2 e 4 β€” sufficienti + a tenere il verdetto, ma il filone tornerebbe misurabile SUBITO invece che fra un anno. + Costo della verifica: un ordine di prova, come per il PRIIPs. + + 2. IL REGIME. Se, raccolte ~un anno di settimane col gate IV-rank APERTO, la media + settimanale restasse positiva CON la banda d'ancora che non contiene lo zero, allora + Β«short-vol da QUOTEΒ» sarebbe dimostrato e la regola del 19/06 andrebbe riscritta. + E' la sola prova che conta: la banda d'ancora, non la media. + + 3. IL LIBRO. Se l'open interest di ETH_USDC salisse stabilmente sopra ~100 sugli strike a + Ξ΄ -0.28/-0.10 con spread relativo sotto ~15%, esisterebbe una struttura regolata in + USDC ed eseguibile a $242 per lotto: il filone si riaprirebbe sul DATO invece che + sulla teoria. Ma senza raccoglierla adesso non lo sapremo mai β€” vedi azione (A). + + ⚠️ COSA NON MI SMENTIREBBE, ed e' quello che succedera' per primo: un'altra finestra di + 10 settimane di vol bassa con 10/10 vincenti e uno Sharpe a due cifre. Sarebbe lo + STESSO campione, piu' lungo β€” non evidenza nuova. Un campione che non contiene il modo + di perdere non diventa informativo aggiungendo settimane in cui non si perde.""") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_xs_lite.py b/scripts/research/r0822_xs_lite.py new file mode 100644 index 0000000..dc3bb8a --- /dev/null +++ b/scripts/research/r0822_xs_lite.py @@ -0,0 +1,786 @@ +"""XS-LITE β€” esiste una versione CONCENTRATA (2-8 gambe) degli edge cross-sectional del progetto +che conservi una frazione utile dell'edge ed e' ESEGUIBILE OGGI a $600-5.000? + +LA DOMANDA (mai posta in questa forma). Il progetto ha due edge cross-sectional che dichiara di non +poter eseguire per TAGLIA, non per debolezza: + XS01 β€” 19 major HL, long 5 / short 5, ribilancio ogni 10g. Dichiarato eseguibile da ~$20k. + XSR01 β€” 50 alt HL, residuo OLS su BTC demeanato cross-sezionalmente. Dichiarato ~$5k. +Il vincolo dichiarato e' il **min order** moltiplicato per il numero di gambe. Se si riducono le +gambe il vincolo si allenta β€” ma l'ampiezza (il numero di scommesse indipendenti) crolla, e +l'ampiezza e' l'unica ragione per cui una strategia cross-sectional ha uno Sharpe alto. +Questo script misura il CAMBIO, non lo assume. E misura anche la premessa: che il muro sia il +min-order. + +IPOTESI PRE-REGISTRATE (scritte prima di guardare i numeri): + (a) lo Sharpe scende con k come ~sqrt(ampiezza): k=2-3 conserva il 40-60% dell'edge; + (b) il min-order NON e' il muro di XS01 a $600 (a k=5 il ticket e' 0.5/5 del capitale dello + sleeve), e il "serve $20k" viene dal TURNOVER del vol-target, non dalla taglia della posizione; + (c) XSR01 concentrato perde molto di piu', perche' il suo edge E' l'ampiezza (il progetto ha gia' + misurato che il demeaning porta l'ampiezza 4.5 -> 37.4: e' quel numero, non il segnale, a + fare lo Sharpe 1.82). +Con 2.6 anni di storia monoregime il verdetto atteso e' LEAD o SCARTATO, mai CANDIDATO. + +MECCANISMI CONGELATI β€” non si rifitta nulla. + XS01 : blend lookback (30,90), gate dispersione p30 espandente causale, H=10, vol-target 20%, + cap 3x, fee 5 bps per unita' di turnover. UNICO parametro variato: k (gambe per lato). + Replica bit-exact di src.portfolio.sleeves._xsec_returns verificata al passo (0). + XSR01: W=45, sgn=+1, residuo OLS rolling causale su BTC, z-score, tanh, vol-target 20%, cap 2x, + demean cross-sezionale giornaliero, fee 5 bps/gamba. Segnale IMPORTATO da + r0725_statarb_multi, paniere da r0725_statarb_basket_gate (nessuna reimplementazione). + UNICO parametro variato: k. + Il LOOKBACK NON e' stato riaperto DI PROPOSITO: riaprire un parametro riapre la sua famiglia + (lezione VRP-tenore, 30/07) e moltiplicherebbe i trial. Qui si varia solo la CONCENTRAZIONE. + +GRIGLIA DICHIARATA PRIMA DI MISURARE: 2 famiglie x 2 universi x 6 valori di k (+ il "pieno" di +XSR01) = 26 celle valutabili. Le 10 fasi del ciclo H=10 di XS01 sono una banda d'ANCORA, non una +griglia di parametri (regola del progetto: il deflated-Sharpe non le conta) -> riportate come +mediana onesta e come differenze APPAIATE. Il deflated-Sharpe e' comunque pubblicato a N=26 E a +N=156 (sensibilita' del verdetto al conteggio, lezione 30/07). + + nice -n 19 timeout 900 uv run python scripts/research/r0822_xs_lite.py +""" +from __future__ import annotations + +import sys +import warnings +from pathlib import Path + +import numpy as np +import pandas as pd + +warnings.filterwarnings("ignore") + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import altlib as A # noqa: E402 +from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE, _xsec_returns # noqa: E402 +from r0725_statarb_multi import BASE, load_hl, pnl, signal # noqa: E402 +from r0725_statarb_basket_gate import basket_from_positions, pair_frames # noqa: E402 + +RAW = ROOT / "data" / "raw" +ANN = np.sqrt(365.25) +HOLDOUT = A.HOLDOUT # 2025-01-01 (convenzione altlib) +MIN_ORDER_HL = 10.0 # Hyperliquid: min order $10 (r0702_capital_scaling) +MIN_ORDER_5 = 5.0 # il $5 di Deribit, citato nella missione +KS = (1, 2, 3, 4, 5, 6) # 5 = canonico XS01 +COST_SWEEP = (0.0, 5.0, 10.0, 20.0, 30.0, 50.0) # bps per gamba TOTALI (fee + slippage) +# capitali dello SLEEVE. Il progetto tiene XS01 al 15% del book -> a book $600 lo sleeve ha $90. +# Si misurano entrambe le letture, perche' e' esattamente qui che nasce il numero "$20k". +SLEEVE_CAPS = (90.0, 300.0, 600.0, 750.0, 2000.0, 5000.0) + + +# ===================================================================================== +# misure +# ===================================================================================== +def sh(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(r.mean() / r.std() * ANN) if len(r) > 30 and r.std() > 0 else 0.0 + + +def dd(s) -> float: + r = np.nan_to_num(np.asarray(pd.Series(s).values, float)) + eq = np.cumprod(1.0 + np.clip(r, -0.99, None)) + return float(np.min(eq / np.maximum.accumulate(eq) - 1.0)) + + +def cagr(s) -> float: + x = pd.Series(s).dropna() + if len(x) < 30: + return 0.0 + yrs = (x.index[-1] - x.index[0]).days / 365.25 + eq = float(np.prod(1.0 + np.clip(x.values, -0.99, None))) + return float(eq ** (1.0 / yrs) - 1.0) if yrs > 0.2 and eq > 0 else 0.0 + + +def split(s): + x = pd.Series(s).dropna() + return x[x.index < HOLDOUT], x[x.index >= HOLDOUT] + + +HDR = f" {'cella':<20}{'ShFULL':>8}{'Sh IS':>8}{'Sh OOS':>8}{'maxDD':>9}{'CAGR':>9}" + + +def row(name, s, extra=""): + ins, out = split(s) + return (f" {name:<20}{sh(s):>8.2f}{sh(ins):>8.2f}{sh(out):>8.2f}" + f"{dd(s)*100:>8.1f}%{cagr(s)*100:>8.1f}%{extra}") + + +# ===================================================================================== +# (A) XS01 congelato, con k e fase parametrici e universo a copertura variabile +# ===================================================================================== +def load_closes(syms, how="inner") -> pd.DataFrame: + cols = {} + for s in syms: + p = RAW / f"hl_{s.lower()}_1d.parquet" + if not p.exists(): + continue + d = pd.read_parquet(p, columns=["timestamp", "close"]) + cols[s] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + C = pd.concat(cols, axis=1, join=("inner" if how == "inner" else "outer")).sort_index() + return C.dropna() if how == "inner" else C + + +def xs_run(C: pd.DataFrame, k: int, phase: int = 0): + """Replica di sleeves._xsec_returns con k e fase parametrici, tollerante ai simboli con + copertura piu' corta (si valutano solo quelli con storia sufficiente a quella data: senza + questo, un outer-join su 51 alt riduce la finestra comune a ~2/3 e cancella l'in-sample). + Con tutte le colonne valide il percorso e' NUMERICAMENTE IDENTICO all'originale β€” provato al + passo (0) con max|diff| = 0. + Ritorna (serie netta lente-sleeve, W [n x A], scale [n], dret [n x A]).""" + px = C.values.astype(float) + n, na = px.shape + lookbacks, H, mode, tv = XS_CFG["lookbacks"], XS_CFG["H"], XS_CFG["mode"], XS_CFG["target_vol"] + disp_pct, minhist = XS_CFG["disp_pct"], XS_CFG["disp_minhist"] + mlb = max(lookbacks) + fin = np.isfinite(px) + prev = np.vstack([np.full(na, np.nan), px[:-1]]) + dret = np.where(fin & np.isfinite(prev), px / prev - 1.0, 0.0) + dret = np.nan_to_num(dret) + W = np.zeros((n, na)) + w = np.zeros(na) + disp_hist = [] + for i in range(n): + if i >= mlb and (i - phase) % H == 0: + ok = fin[i].copy() + for L in lookbacks: + ok &= fin[i - L] + if ok.sum() >= 2 * k: + cols = np.where(ok)[0] + rLs = [px[i, cols] / px[i - L, cols] - 1.0 for L in lookbacks] + disp_i = float(np.mean([r.std() for r in rLs])) + thr = (np.percentile(disp_hist, disp_pct) + if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf) + if disp_i >= thr: + score = np.zeros(len(cols)); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if cnt: + score /= cnt + order = np.argsort(score) + w = np.zeros(na) + lo, hi = cols[order[:k]], cols[order[-k:]] + if mode == "mom": w[hi] = 0.5 / k; w[lo] = -0.5 / k + else: w[lo] = 0.5 / k; w[hi] = -0.5 / k + else: + w = np.zeros(na) + disp_hist.append(disp_i) + W[i] = w + gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) + turn = np.zeros(n); turn[0] = np.abs(W[0]).sum() + turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) + net = gross - turn * (0.001 / 2.0) + s = pd.Series(net, index=C.index) + rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) + scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) + return pd.Series(s.values * scale, index=C.index), W, scale, dret + + +def xs_effective_weights(W: np.ndarray, scale: np.ndarray) -> np.ndarray: + """Peso EFFETTIVO tenuto durante la barra i = W[i-1]*scale[i]. Entrambi noti a close[i-1] + (scale usa una rolling std gia' shiftata) -> causale.""" + Weff = np.zeros_like(W) + Weff[1:] = W[:-1] * scale[1:, None] + return Weff + + +# ===================================================================================== +# (B) XSR01 congelato, con k parametrico +# ===================================================================================== +def xsr_positions(P: pd.DataFrame, k: int | None) -> pd.DataFrame: + """Q = posizioni demeanate cross-sezionalmente (meccanismo congelato). + Se k e' dato: tiene le k piu' positive e le k piu' negative di ogni riga, azzera il resto e + RI-demeana sulle sopravvissute (resta dollar-neutral).""" + Q = P.sub(P.mean(axis=1), axis=0) + if k is None: + return Q + V = Q.to_numpy(float) + out = np.zeros_like(V) + for i in range(V.shape[0]): + v = V[i] + m = np.isfinite(v) + if m.sum() < 2 * k or not np.any(v[m] != 0.0): + continue + idx = np.where(m)[0] + order = idx[np.argsort(v[idx])] + keep = np.concatenate([order[:k], order[-k:]]) + r = np.zeros_like(v) + r[keep] = v[keep] + r[keep] -= r[keep].mean() + out[i] = r + return pd.DataFrame(out, index=P.index, columns=P.columns) + + +def xsr_legs(Q: pd.DataFrame, S: pd.DataFrame) -> pd.DataFrame: + """Matrice [data x gamba] dei pnl per gamba, nella lente PUBBLICATA (fee su 2 gambe per + coppia: alt + copertura BTC dedicata).""" + return pd.DataFrame({c: pnl(Q[c].to_numpy(float), S[c].to_numpy(float)) for c in Q.columns}, + index=Q.index) + + +def xsr_basket(Q: pd.DataFrame, S: pd.DataFrame, norm) -> pd.Series: + """norm = 'perrow' riproduce ESATTAMENTE basket_from_positions (media per-riga sui simboli + con dato: il numero di simboli cambia nel tempo). norm = intero -> normalizzazione COSTANTE, + l'unica che rende confrontabili celle con k diverso.""" + M = xsr_legs(Q, S) + if norm == "perrow": + return M.mean(axis=1).dropna() + return (M.fillna(0.0).sum(axis=1) / float(norm)).dropna() + + +# ===================================================================================== +# (C) simulatore di libro con MIN-ORDER (lente eseguibile, comune alle due famiglie) +# ===================================================================================== +def sim_book(Weff: np.ndarray, R: np.ndarray, idx, r_hedge=None, capital=None, + min_order=0.0, cost_bps=5.0): + """Weff[i] = pesi TARGET da tenere durante la barra i (decisi a close[i-1]). + R[i] = ritorni per gamba nella barra i. r_hedge = ritorno della gamba di copertura (si tiene + -sum(w)); None = libro gia' neutrale fra le gambe. min_order>0: una variazione di NOZIONALE + sotto la soglia NON si esegue (si tiene la vecchia). cost_bps = costo per unita' di turnover.""" + n, na = Weff.shape + c = cost_bps / 1e4 + held = np.zeros(na) + hedge = 0.0 + net = np.zeros(n) + n_fill = n_skip = 0 + tickets, gross = [], [] + for i in range(n): + tgt = Weff[i] + d_not = np.abs(tgt - held) * (capital or 1.0) + if min_order > 0.0 and capital: + move = d_not >= min_order + else: + move = d_not > 1e-12 + new = np.where(move, tgt, held) + n_fill += int(move.sum()) + n_skip += int(((~move) & (d_not > 1e-12)).sum()) + tickets.extend(d_not[move].tolist()) + turn = float(np.abs(new - held).sum()) + r = float(np.dot(new, R[i])) + if r_hedge is not None: + h = -float(new.sum()) + if min_order > 0.0 and capital and abs(h - hedge) * capital < min_order: + h = hedge + turn += abs(h - hedge) + hedge = h + r += hedge * float(r_hedge[i]) + net[i] = r - c * turn + held = new + gross.append(float(np.abs(new).sum()) + abs(hedge)) + return pd.Series(net, index=idx), dict( + n_fill=n_fill, n_skip=n_skip, + exec_share=(n_fill / (n_fill + n_skip)) if (n_fill + n_skip) else 1.0, + med_ticket=float(np.median(tickets)) if tickets else 0.0, + med_gross=float(np.median(gross)), + orders_py=(n_fill / (len(idx) / 365.25)) if len(idx) else 0.0) + + +# ===================================================================================== +# (D) ampiezza +# ===================================================================================== +def breadth(legs: pd.DataFrame, n_simult: int, min_overlap: int = 30): + """Due misure, perche' una sola mente. + (1) N_eff = N/(1+(N-1)*rbar), N = gambe SIMULTANEE, rbar = correlazione media fra i contributi + di due gambe CONDIZIONATA a essere entrambe attive. E' la formula pubblicata dal progetto + (4.5 -> 37.4), qui condizionata: senza condizionare, concentrando si conterebbe come + 'ampiezza' la ROTAZIONE nel tempo, che nello Sharpe sta gia' dentro sqrt(T). + INSTABILE per costruzione: rbar leggermente NEGATIVO (normale in un libro dollar-neutral) + la fa esplodere. Si legge insieme a rbar, mai da sola. + (2) N_hhi = numero effettivo di gambe che PORTANO il rischio, giorno per giorno: + 1/sum(share_i^2) sulle quote |contributo|, mediato sui giorni attivi. Limitato da N, + stabile, e non dipende dal segno delle correlazioni.""" + M = legs.to_numpy(float) + M = np.nan_to_num(M) + act = M != 0.0 + rs = [] + for a in range(M.shape[1]): + for b in range(a + 1, M.shape[1]): + m = act[:, a] & act[:, b] + if m.sum() < min_overlap: + continue + xa, xb = M[m, a], M[m, b] + if xa.std() > 0 and xb.std() > 0: + rs.append(float(np.corrcoef(xa, xb)[0, 1])) + rbar = float(np.mean(rs)) if rs else float("nan") + N = float(n_simult) + den = 1.0 + (N - 1) * rbar if rs else float("nan") + neff = (N / den) if (rs and den > 1e-3) else float("nan") + hh = [] + for i in range(M.shape[0]): + v = np.abs(M[i][act[i]]) + t = v.sum() + if t > 0 and len(v) > 0: + sHH = float(np.sum((v / t) ** 2)) + if sHH > 0: + hh.append(1.0 / sHH) + return neff, rbar, (float(np.mean(hh)) if hh else float("nan")), len(rs) + + +# ===================================================================================== +def main() -> None: + print("=" * 108) + print(" XS-LITE β€” una versione CONCENTRATA degli edge cross-sectional e' eseguibile a $600-5.000?") + print("=" * 108) + + maj19 = list(XS_UNIVERSE) + all51 = sorted({p.stem.replace("hl_", "").replace("_1d", "").upper() + for p in RAW.glob("hl_*_1d.parquet")}) + + # ---------------------------------------------------------------- (0) repliche + print("\n" + "-" * 108) + print(" (0) REPLICA DEI MECCANISMI CONGELATI β€” prima di ogni delta") + print("-" * 108) + C19 = load_closes(maj19, "inner") + C51 = load_closes(all51, "outer") + off, _, _, _ = xs_run(C19, k=XS_CFG["k"], phase=0) + ref = _xsec_returns() + J = pd.concat({"mio": off, "sleeve": ref}, axis=1, join="inner").dropna() + d_xs = float(np.max(np.abs(J["mio"] - J["sleeve"]))) + print(f" XS01 k=5 fase 0 vs sleeves._xsec_returns : max|diff| = {d_xs:.3e} " + f"({len(J)} barre) {'OK' if d_xs < 1e-12 else 'DIVERGE'}") + + P, S = pair_frames() + Qfull = xsr_positions(P, None) + xsr_perrow = xsr_basket(Qfull, S, "perrow") + xsr_ref = basket_from_positions(P, S, cols=None, demean=True) + J2 = pd.concat({"mio": xsr_perrow, "ref": xsr_ref}, axis=1, join="inner").dropna() + d_xsr = float(np.max(np.abs(J2["mio"] - J2["ref"]))) + print(f" XSR01 pieno vs basket_from_positions(demean): max|diff| = {d_xsr:.3e} " + f"({len(J2)} barre) {'OK' if d_xsr < 1e-12 else 'DIVERGE'}") + xsr_const = xsr_basket(Qfull, S, P.shape[1]) + print(f" XSR01 pieno, lente pubblicata (media per-riga sui simboli CON DATO): " + f"Sharpe {sh(xsr_perrow):.2f} maxDD {dd(xsr_perrow)*100:.1f}% (pubblicati 1.82 / -2.6%)") + print(f" XSR01 pieno, normalizzazione COSTANTE 1/{P.shape[1]} : " + f"Sharpe {sh(xsr_const):.2f} maxDD {dd(xsr_const)*100:.1f}%") + print(" ^ le due lenti coincidono qui (la copertura e' quasi piena); da qui in poi si usa la") + print(" COSTANTE, l'unica che rende confrontabili celle con k diverso, e i numeri XSR01 vanno") + print(f" letti contro {sh(xsr_const):.2f}.") + disc = pd.Timestamp("2026-07-25", tz="UTC") # giorno di scoperta/inception monitor + xsr_disc = xsr_perrow[xsr_perrow.index <= disc] + xsr_fwd = xsr_perrow[xsr_perrow.index > disc] + print(f" XSR01 sulla FINESTRA DI SCOPERTA (<= {disc.date()}, {len(xsr_disc)} barre): " + f"Sharpe {sh(xsr_disc):.2f} maxDD {dd(xsr_disc)*100:.1f}%") + print(" ⚠ IL NUMERO PUBBLICATO (1.82) NON SI RIPRODUCE OGGI, e non e' la mia implementazione:") + print(" le due repliche qui sopra sono bit-exact col codice del progetto. Sulla finestra") + print(f" IDENTICA a quella di scoperta la lente 'paniere' da' {sh(xsr_disc):.2f} e la lente 'libro'") + print(" (fee 1 gamba alt + copertura BTC NETTA, convenzione di paper_xsr) da' 2.23: l'1.82 non") + print(" e' nessuna delle due. Spiegazione piu' probabile: data/raw e' gitignored e il cron") + print(" riscrive i parquet HL -> stesso codice, dati diversi (identico allo scoperto del 07/08") + print(" su GTAA/ADJUSTED_LAST). NON l'ho inseguito: tutte le mie conclusioni sono confronti") + print(" RELATIVI dentro UNA lente. Ma tocca un numero pubblicato e il gate del 23/10.") + print(f" XSR01 dal 25/07 a oggi (forward, {len(xsr_fwd)} barre, stessa lente): " + f"ritorno {(np.prod(1+xsr_fwd.values)-1)*100:+.2f}%" + + (" (campione troppo corto per uno Sharpe: si legge il ritorno)" + if len(xsr_fwd) <= 30 else f" Sharpe {sh(xsr_fwd):.2f}")) + print(f" finestra MAJ19: {C19.index[0].date()} -> {C19.index[-1].date()} ({len(C19)} barre); " + f"in-sample = pre {HOLDOUT.date()} = {int((C19.index < HOLDOUT).sum())} barre") + cov51 = np.isfinite(C51.values).sum(axis=1) + print(f" finestra ALL51: {C51.index[0].date()} -> {C51.index[-1].date()} ({len(C51)} barre); " + f"simboli con dato: da {cov51.min()} a {cov51.max()} (outer-join: l'inner-join") + print(" taglierebbe la finestra a 627 barre e CANCELLEREBBE l'in-sample).") + print(" ⚠ l'in-sample e' UN ANNO SOLO e monoregime: ogni Sh IS qui sotto va letto cosi'.") + + # ---------------------------------------------------------------- (1) XS-LITE + print("\n" + "-" * 108) + print(" (1) FAMIGLIA A β€” XS-LITE: XS01 congelato, varia solo k (gambe per lato). k=5 = canonico") + print("-" * 108) + print(HDR + f"{'N_eff':>8}{'rbar':>8}{'N_hhi':>8}") + xs_series, xs_cells = {}, {} + for uni, Cx in (("MAJ19", C19), ("ALL51", C51)): + for k in KS: + s, W, sc, dr = xs_run(Cx, k=k, phase=0) + Weff = xs_effective_weights(W, sc) + legpnl = pd.DataFrame(Weff * dr, index=Cx.index, columns=Cx.columns) + ne, rbar, nhhi, _ = breadth(legpnl, 2 * k) + nm = f"{uni} k={k}" + ("*" if (uni == "MAJ19" and k == 5) else "") + xs_series[nm] = s + xs_cells[nm] = dict(uni=uni, k=k, C=Cx, W=W, scale=sc, dret=dr, + neff=ne, rbar=rbar, nhhi=nhhi) + print(row(nm, s, f"{ne:>8.1f}{rbar:>8.3f}{nhhi:>8.1f}")) + print(" (*) = configurazione canonica del sleeve XS01") + + # ---------------------------------------------------------------- (2) XSR-LITE + print("\n" + "-" * 108) + print(" (2) FAMIGLIA B β€” XSR-LITE: XSR01 congelato (demean), varia solo k. 'pieno' = tutte le gambe") + print("-" * 108) + maj18 = [s for s in maj19 if s != BASE] + Pm = P[[c for c in maj18 if c in P.columns]] + Sm = S[[c for c in maj18 if c in S.columns]] + print(f" ALL50: {P.shape[1]} gambe MAJ18: {Pm.shape[1]} gambe") + print(HDR + f"{'N_eff':>8}{'rbar':>8}{'N_hhi':>8}") + xsr_series, xsr_cells = {}, {} + for uni, (Pu, Su) in (("ALL50", (P, S)), ("MAJ18", (Pm, Sm))): + for k in list(KS) + [None]: + if k is not None and 2 * k > Pu.shape[1]: + continue + Q = xsr_positions(Pu, k) + norm = (2 * k) if k is not None else Pu.shape[1] + s = xsr_basket(Q, Su, norm) + legs = xsr_legs(Q, Su) / float(norm) + ne, rbar, nhhi, _ = breadth(legs, norm) + nm = f"{uni} k={k if k else 'pieno'}" + ("*" if (uni == "ALL50" and k is None) else "") + xsr_series[nm] = s + xsr_cells[nm] = dict(uni=uni, k=k, Q=Q, S=Su, norm=norm, + neff=ne, rbar=rbar, nhhi=nhhi) + print(row(nm, s, f"{ne:>8.1f}{rbar:>8.3f}{nhhi:>8.1f}")) + print(" (*) = XSR01 pieno (50 gambe demeanate), qui a normalizzazione costante") + + # banda di fase calcolata PRIMA della curva: la stima onesta di uno sleeve ancorato e' la + # mediana della banda, quindi e' quella che deve stare nella curva (la fase 0 e' un max-of-10). + base_by_phase = {ph: xs_run(C19, k=5, phase=ph)[0] for ph in range(XS_CFG["H"])} + phase_med, phase_rows = {}, {} + for k in KS: + by_ph = {ph: (base_by_phase[ph] if k == 5 else xs_run(C19, k=k, phase=ph)[0]) + for ph in range(XS_CFG["H"])} + vals = np.array([sh(by_ph[p_]) for p_ in range(XS_CFG["H"])]) + diffs = np.array([sh(by_ph[p_]) - sh(base_by_phase[p_]) for p_ in range(XS_CFG["H"])]) + phase_med[k] = float(np.median(vals)) + phase_rows[k] = (vals, diffs) + + # ---------------------------------------------------------------- (3) curva ampiezza + print("\n" + "-" * 108) + print(" (3) LA CURVA AMPIEZZA-vs-k β€” la domanda centrale: quanto ne resta concentrando?") + print("-" * 108) + sh_xs_star = sh(xs_series["MAJ19 k=5*"]) + sh_xsr_star = sh(xsr_series["ALL50 k=pieno*"]) + print(f" {'k':>4}{'gambe':>7} {'--------- XS-LITE MAJ19 ---------':>34} " + f"{'--------- XSR-LITE ALL50 --------':>34}") + print(f" {'':>4}{'':>7} {'N_hhi':>9}{'N_eff':>9}{'Sh f0':>8}{'Sh MED':>8}{'MED/MED*':>10} " + f"{'N_hhi':>9}{'N_eff':>9}{'Sh':>8}{'Sh/Sh*':>9}") + med5 = phase_med[5] + for k in KS: + an = f"MAJ19 k={k}" + ("*" if k == 5 else "") + bn = f"ALL50 k={k}" + ca, cb = xs_cells[an], xsr_cells[bn] + sa, sb = xs_series[an], xsr_series[bn] + print(f" {k:>4}{2*k:>7} {ca['nhhi']:>9.1f}{ca['neff']:>9.1f}{sh(sa):>8.2f}" + f"{phase_med[k]:>8.2f}{(phase_med[k]/med5 if med5 else 0):>10.0%} " + f"{cb['nhhi']:>9.1f}{cb['neff']:>9.1f}{sh(sb):>8.2f}" + f"{(sh(sb)/sh_xsr_star if sh_xsr_star else 0):>9.0%}") + cp = xsr_cells["ALL50 k=pieno*"] + print(f" {'pieno':>4}{cp['norm']:>7} {'':>9}{'':>9}{'':>8}{'':>8}{'':>10} " + f"{cp['nhhi']:>9.1f}{cp['neff']:>9.1f}{sh_xsr_star:>8.2f}{1.0:>9.0%}") + print(" 'Sh MED' = mediana delle 10 fasi d'ancora di XS01 = la stima ONESTA (la fase 0 e' un") + print(" max-of-10 non dichiarato). XSR01 e' giornaliero: non ha fase, la sua colonna e' unica.") + print(" N_hhi = gambe che PORTANO il rischio (stabile). N_eff = formula pubblicata, esplode") + print(" quando rbar e' negativo: si legge solo insieme a rbar, mai da sola.") + + # ---------------------------------------------------------------- (4) banda d'ancora + print("\n" + "-" * 108) + print(" (4) BANDA D'ANCORA β€” le 10 fasi del ciclo H=10 di XS01 (XSR01 e' giornaliero: nessuna") + print(" fase). Stima onesta = MEDIANA della banda, non la fase 0 con cui e' stato scoperto.") + print("-" * 108) + print(f" {'cella':<14}{'fase0':>8}{'mediana':>9}{'p10':>8}{'p90':>8}{'pctl f0':>9}" + f" d vs k=5: mediana APPAIATA (fasi >0)") + for k in KS: + vals, diffs = phase_rows[k] + pctl = float((vals <= vals[0]).mean() * 100) + print(f" MAJ19 k={k:<6}{vals[0]:>8.2f}{phase_med[k]:>9.2f}{np.percentile(vals,10):>8.2f}" + f"{np.percentile(vals,90):>8.2f}{pctl:>8.0f}% {np.median(diffs):>+8.3f}" + f" {int((diffs>0).sum())}/10") + print(" 'd vs k=5' = mediana delle DIFFERENZE APPAIATE per fase (mai differenza di mediane).") + + # ---------------------------------------------------------------- (5) eseguibilita' + print("\n" + "-" * 108) + print(" (5) ESEGUIBILITA' β€” contabilita' min-order per gamba, lente 'libro'") + print(" Il capitale in colonna e' quello dello SLEEVE. XS01 sta al 15% del book:") + print(" sleeve $90 = book $600, sleeve $300 = book $2.000, sleeve $750 = book $5.000,") + print(" sleeve $3.000 = book $20.000 (la soglia dichiarata dal progetto).") + print("-" * 108) + print(f" {'cella':<13}{'sleeve$':>8}{'minord':>7}{'Sh model':>10}{'Sh reale':>10}" + f"{'haircut':>9}{'gambe eseg':>12}{'tick med$':>11}{'ord/anno':>10}" + f"{'lordo/lordo*':>14}") + print(" ⚠ 'lordo/lordo*' = lordo mediano ESEGUITO / lordo mediano modellato. Sotto ~0.8 il") + print(" libro ha smesso di seguire il proprio target, e allora lo Sharpe NON e' la lettura") + print(" giusta (puo' perfino MIGLIORARE): e' il null del de-levering in veste di 'meno") + print(" costi'. Lezione GTAA-banda 27/07, riapplicata qui.") + + def xs_book_inputs(nm): + cell = xs_cells[nm] + return xs_effective_weights(cell["W"], cell["scale"]), cell["dret"], cell["C"].index, None + + def xsr_book_inputs(nm): + cell = xsr_cells[nm] + Q, Su, norm = cell["Q"], cell["S"], cell["norm"] + Wt = np.nan_to_num(Q.to_numpy(float)) / float(norm) + Weff = np.zeros_like(Wt); Weff[1:] = Wt[:-1] + rb = load_hl(BASE).reindex(Q.index).pct_change().fillna(0.0).to_numpy(float) + Ralt = np.zeros_like(Wt) + for j, c in enumerate(Q.columns): + Ralt[:, j] = np.nan_to_num(Su[c].to_numpy(float)) + rb + return Weff, Ralt, Q.index, rb + + for tag, names, getter in (("XS", ["MAJ19 k=1", "MAJ19 k=2", "MAJ19 k=3", "MAJ19 k=5*"], + xs_book_inputs), + ("XSR", ["ALL50 k=1", "ALL50 k=2", "ALL50 k=3", "ALL50 k=pieno*"], + xsr_book_inputs)): + for nm in names: + Weff, R, idx, rh = getter(nm) + base, dg0 = sim_book(Weff, R, idx, r_hedge=rh, capital=None, min_order=0.0) + for cap in SLEEVE_CAPS: + for mo in (MIN_ORDER_HL,): + real, dg = sim_book(Weff, R, idx, r_hedge=rh, capital=cap, min_order=mo) + lbl = f"{tag} {nm.split()[1]}" + trk = (dg["med_gross"] / dg0["med_gross"]) if dg0["med_gross"] > 0 else float("nan") + print(f" {lbl:<13}{cap:>8.0f}{mo:>7.0f}{sh(base):>10.2f}{sh(real):>10.2f}" + f"{sh(base)-sh(real):>9.2f}{dg['exec_share']*100:>11.0f}%" + f"{dg['med_ticket']:>11.0f}{dg['orders_py']:>10.0f}" + f"{trk:>12.2f}{' ⚠' if trk < 0.8 else ' '}") + print() + print(f" {'cella':<13}{'|dw| RIBILANCIO':>18}{'|dw| deriva':>14}{'cap sleeve min':>17}" + f"{'= book al 15%':>16}") + capmin_k5 = None + for nm in ("MAJ19 k=1", "MAJ19 k=2", "MAJ19 k=3", "MAJ19 k=5*"): + cell = xs_cells[nm] + W = cell["W"] + Weff = xs_effective_weights(W, cell["scale"]) + dW = np.abs(np.diff(W, axis=0)) # W cambia alla riga j+1 + dE = np.abs(np.diff(Weff, axis=0)) # Weff[j+1]-Weff[j], Weff[i]=W[i-1]*scale[i] + # il cambio di W alla riga j+1 si vede su Weff alla riga j+2 -> confine da rispettare + rb, dr = [], [] + for j in range(len(dW)): + if j + 1 >= len(dE): + break + m = dW[j] > 1e-12 + if m.any(): + rb.extend(dE[j + 1][m].tolist()) # gambe DAVVERO ribilanciate + dr.extend(dE[j + 1][~m][dE[j + 1][~m] > 1e-12].tolist()) + else: + dr.extend(dE[j + 1][dE[j + 1] > 1e-12].tolist()) + med_rb = float(np.median(rb)) if rb else float("nan") + med_dr = float(np.median(dr)) if dr else float("nan") + capmin = MIN_ORDER_HL / med_rb if med_rb > 0 else float("nan") + if nm.startswith("MAJ19 k=5"): + capmin_k5 = capmin + print(f" {'XS ' + nm.split()[1]:<13}{med_rb:>18.4f}{med_dr:>14.4f}{capmin:>16.0f}$" + f"{capmin/0.15:>15.0f}$") + print(" ^ ARITMETICA: il ticket vale |dw|*capitale_sleeve, e le due colonne |dw| sono ordini") + print(" di grandezza diversi: il RIBILANCIO (ogni 10 giorni, porta il segnale) e la DERIVA") + print(" giornaliera del vol-target (non porta segnale). Il min-order taglia la seconda molto") + print(" prima della prima, ed e' per questo che la quota di turnover eseguito puo' essere del") + print(f" 4% con haircut ~0. Per il CANONICO k=5 il ribilancio smette di passare sotto " + f"~{capmin_k5:.0f}$") + print(f" di sleeve = ~{capmin_k5/0.15:.0f}$ di book al 15%. La soglia '$20k' del progetto viene da") + print(" una diagnostica di TURNOVER, non di Sharpe β€” stessa lezione gia' imparata su TP01") + print(" ('a $600 il min-order $5 e' GIA' la banda ottimale'), mai applicata a XS01.") + print() + print(" 'gambe eseg' = quota delle VARIAZIONI di posizione sopra il min-order (la diagnostica") + print(" su cui si basa la soglia '$20k'). 'haircut' = quanto ne costa in Sharpe. Sono cose") + print(" DIVERSE: il turnover saltato e' la deriva del vol-target, non il segnale.") + print(" Con min-order $5 invece di $10 (Deribit invece di HL) l'haircut si dimezza per") + print(" costruzione; il caso $10 e' quello vero per Hyperliquid ed e' quello riportato.") + + # ---------------------------------------------------------------- (6) slippage + print("\n" + "-" * 108) + print(" (6) SLIPPAGE β€” costo TOTALE per gamba (i 5 bps di fee taker sono gia' dentro).") + print(" A che costo muore l'edge? E' il rischio #1 dichiarato di XSR01.") + print("-" * 108) + print(f" {'cella':<16}" + "".join(f"{int(c):>10}bps" for c in COST_SWEEP) + f"{'break-even':>13}") + + def cost_curve(label, Weff, R, idx, rh): + vals = [] + for c in COST_SWEEP: + s, _ = sim_book(Weff, R, idx, r_hedge=rh, capital=None, min_order=0.0, cost_bps=c) + vals.append(sh(s)) + be = None + for i in range(1, len(COST_SWEEP)): + if vals[i - 1] > 0 >= vals[i]: + x0, x1, y0, y1 = COST_SWEEP[i-1], COST_SWEEP[i], vals[i-1], vals[i] + be = x0 + (x1 - x0) * y0 / (y0 - y1) + break + print(f" {label:<16}" + "".join(f"{v:>13.2f}" for v in vals) + + (f"{be:>13.0f}" if be else f"{'>'+str(int(COST_SWEEP[-1])):>13}")) + return be + + be = {} + for nm in ("MAJ19 k=1", "MAJ19 k=2", "MAJ19 k=3", "MAJ19 k=5*"): + be["XS " + nm] = cost_curve("XS " + nm.split()[1], *xs_book_inputs(nm)) + for nm in ("ALL50 k=1", "ALL50 k=2", "ALL50 k=3", "ALL50 k=pieno*"): + be["XSR " + nm] = cost_curve("XSR " + nm.split()[1], *xsr_book_inputs(nm)) + + # ---------------------------------------------------------------- (7) selezione + DSR + print("\n" + "-" * 108) + print(" (7) SELEZIONE IN-SAMPLE-ONLY (mai sull'hold-out) + DEFLATED SHARPE") + print("-" * 108) + allcells = {**{f"XS {n}": s for n, s in xs_series.items()}, + **{f"XSR {n}": s for n, s in xsr_series.items()}} + ins_rank = sorted(allcells.items(), key=lambda kv: -sh(split(kv[1])[0])) + print(f" celle valutate: {len(allcells)} (2 famiglie x 2 universi x 6 k, + i 2 'pieno' di") + print(" XSR01, meno le celle impossibili 2k>gambe). Lookback NON riaperto -> nessun trial in piu'.") + print(f"\n {'rank':>5} {'cella':<20}{'Sh IS':>8}{'Sh OOS':>8}{'Sh FULL':>9}") + for i, (n, s) in enumerate(ins_rank[:8], 1): + ins, out = split(s) + print(f" {i:>5} {n:<20}{sh(ins):>8.2f}{sh(out):>8.2f}{sh(s):>9.2f}") + best_name, best_s = ins_rank[0] + all_sr = [sh(s) for s in allcells.values()] + print(f"\n cella scelta AL BUIO (solo in-sample): {best_name}") + for mult, label in ((1, "trial dichiarati = celle"), + (6, "conteggio conservativo x6 (se avessi riaperto il lookback)")): + pad = all_sr * mult + d, s0 = A.deflated_sharpe(sh(best_s), pad, pd.Series(best_s).dropna()) + print(f" DSR a N={len(pad):<4} ({label}): {d:.3f} " + f"{'PASS' if d >= 0.95 else 'FAIL'} (Sharpe-soglia del null {s0:.2f})") + conc = {n: s for n, s in allcells.items() if any(f" k={j}" in n for j in (1, 2, 3))} + cbest_name, cbest_s = max(conc.items(), key=lambda kv: sh(split(kv[1])[0])) + d2, _ = A.deflated_sharpe(sh(cbest_s), all_sr, pd.Series(cbest_s).dropna()) + print(f" cella CONCENTRATA (k<=3, cioe' <=6 gambe) scelta al buio: {cbest_name} " + f"Sh IS {sh(split(cbest_s)[0]):.2f} OOS {sh(split(cbest_s)[1]):.2f} FULL {sh(cbest_s):.2f}") + print(f" DSR della concentrata a N={len(all_sr)}: {d2:.3f} {'PASS' if d2 >= 0.95 else 'FAIL'}") + + # ---------------------------------------------------------------- (8) gate marginali + print("\n" + "-" * 108) + print(" (8) GATE MARGINALE vs TP01 + RIDONDANZA vs gli sleeve che gia' esistono") + print("-" * 108) + order = [best_name, cbest_name, "XS MAJ19 k=2", "XS MAJ19 k=3", "XS MAJ19 k=5*", + "XSR ALL50 k=2", "XSR ALL50 k=3", "XSR ALL50 k=pieno*"] + to_score = {} + for n in order: + if n in allcells and n not in to_score: + to_score[n] = allcells[n] + for n, s in to_score.items(): + ss = pd.Series(s).dropna(); ss.index = pd.to_datetime(ss.index, utc=True) + m = A.marginal_vs_tp01(ss) + b = m.get("blends", {}).get("w25", {}) + print(f" {n:<20} {str(m.get('marginal_verdict')):<10} corr {str(m.get('corr_full')):>7}" + f" uplift w25 full {str(b.get('uplift_full')):>7} hold {str(b.get('uplift_hold')):>7}" + f" robust={m.get('robust_oos')} noise_null={m.get('beats_noise_null')}" + f" is_edge={m.get('has_insample_edge')} hedge={m.get('is_hedge')}") + print("\n correlazione col materiale gia' in casa (un proxy eseguibile DEVE assomigliare al suo") + print(" originale: qui una corr ALTA e' l'obiettivo, non un difetto)") + ref_map = {"XS01 (sleeve)": ref, "XSR01 (pieno)": xsr_const, "TP01": A.tp01_baseline_daily()} + print(f" {'cella':<20}" + "".join(f"{k:>16}" for k in ref_map)) + for n, s in to_score.items(): + line = f" {n:<20}" + for _, r in ref_map.items(): + Jc = pd.concat({"a": pd.Series(s).dropna(), "b": r}, axis=1, join="inner").dropna() + line += f"{(Jc['a'].corr(Jc['b']) if len(Jc) > 30 else float('nan')):>16.3f}" + print(line) + print(" ⚠ corr a SKH01/VRP01/GTAA01 NON misurata: SKH01 richiede i 5m di DUE asset e il brief") + print(" vieta di caricarli insieme. Prior del progetto (25/07): XSR01 sta a |0.001|-|0.071|") + print(" da tutti e 5 gli sleeve; un suo sottoinsieme non puo' esserne lontano.") + + # ---------------------------------------------------------------- (9) null del de-levering + print("\n" + "-" * 108) + print(" (9) NULL DEL DE-LEVERING β€” obbligatorio su ogni confronto di maxDD (5 occorrenze note)") + print(" Domanda: esiste una LEVA del canonico che dia lo STESSO maxDD della variante?") + print(" Se a quel maxDD il canonico ha Sharpe >= alla variante, la variante non aggiunge.") + print("-" * 108) + def lev_for_dd(canon: pd.Series, target_abs: float, lo=0.01, hi=5.0): + """Leva k tale che |maxDD(canon*k)| = target. |maxDD| e' monotono crescente in k.""" + if abs(dd(canon * hi)) < target_abs: + return None # nemmeno a leva 5x si arriva al DD + for _ in range(60): + mid = 0.5 * (lo + hi) + if abs(dd(canon * mid)) < target_abs: lo = mid + else: hi = mid + return 0.5 * (lo + hi) + + def delever_null(variants: dict, canon: pd.Series, label: str): + print(f" {label:<22}{'maxDD':>9}{'Sh':>8} {'k_leva canonico':>16}{'Sh canonico':>13} esito") + for nm, s in variants.items(): + kl = lev_for_dd(canon, abs(dd(s))) + if kl is None: + print(f" {nm:<22}{dd(s)*100:>8.1f}%{sh(s):>8.2f} {'n.d. (>5x)':>16}" + f"{'':>13} maxDD irraggiungibile levando il canonico") + continue + sc_ = sh(canon * kl) + print(f" {nm:<22}{dd(s)*100:>8.1f}%{sh(s):>8.2f} {kl:>16.3f}{sc_:>13.2f} " + f"{'il canonico DOMINA' if sc_ >= sh(s) else 'la variante REGGE'}") + delever_null({n: xs_series[n] for n in xs_series if n.startswith("MAJ19") and "k=5" not in n}, + xs_series["MAJ19 k=5*"], "XS-LITE vs canonico") + print() + delever_null({n: xsr_series[n] for n in xsr_series if n.startswith("ALL50") and "pieno" not in n}, + xsr_series["ALL50 k=pieno*"], "XSR-LITE vs pieno") + + # ---------------------------------------------------------------- (10) implausible + causalita' + print("\n" + "-" * 108) + print(" (10) implausible_sharpe + CAUSALITA' (prefisso troncato + decadimento con lag)") + print("-" * 108) + for n in dict.fromkeys([best_name, cbest_name, "XS MAJ19 k=2", "XS MAJ19 k=5*", + "XSR ALL50 k=2"]): + if n not in allcells: + continue + ss = pd.Series(allcells[n]).dropna(); ss.index = pd.to_datetime(ss.index, utc=True) + r = A.implausible_sharpe(ss) + print(f" {n:<20} implausible={str(r['implausible']):<6} Sh {r.get('sharpe',0):>5.2f} " + f"perdite su barre ATTIVE {r.get('loss_frac',float('nan'))*100:>5.1f}% " + f"attive {r.get('active_frac',0)*100:>3.0f}% {'; '.join(r['reasons'])[:38]}") + cut = int(len(C19) * 0.85) + Wf = xs_run(C19, k=2, phase=0)[1] + Wp = xs_run(C19.iloc[:cut], k=2, phase=0)[1] + dmax = float(np.max(np.abs(Wp[cut - 80:cut] - Wf[cut - 80:cut]))) + print(f" XS k=2: pesi ricalcolati su prefisso troncato (cut={cut}) vs pieno, ultime 80 barre: " + f"max|diff| = {dmax:.2e} {'CAUSALE' if dmax < 1e-12 else 'LOOK-AHEAD'}") + Wq = xs_run(C19, k=2, phase=0) + Weff2 = xs_effective_weights(Wq[1], Wq[2]) + lags = [] + for lag in (0, 1, 2, 3, 5): + Wl = np.zeros_like(Weff2) + if lag: Wl[lag:] = Weff2[:-lag] + else: Wl = Weff2 + s, _ = sim_book(Wl, Wq[3], C19.index) + lags.append(sh(s)) + print(" XS k=2 decadimento con lag (0/1/2/3/5 giorni): " + + " / ".join(f"{v:.2f}" for v in lags) + + " (dolce = segnale lento, non firma di look-ahead)") + + # ---------------------------------------------------------------- (11) sintesi + print("\n" + "=" * 108) + print(" SINTESI β€” quanto costa concentrare, e dove sta davvero il muro") + print("=" * 108) + k2, k5 = xs_series["MAJ19 k=2"], xs_series["MAJ19 k=5*"] + print(f" XS-LITE k=2 (4 gambe): Sh fase0 {sh(k2):.2f} / fase-MEDIANA {phase_med[2]:.2f}" + f" vs canonico k=5 (10 gambe): {sh(k5):.2f} / {phase_med[5]:.2f}") + print(f" XSR-LITE k=2 (4 gambe): Sh {sh(xsr_series['ALL50 k=2']):.2f} " + f"(IS {sh(split(xsr_series['ALL50 k=2'])[0]):.2f}) vs pieno " + f"{sh_xsr_star:.2f} (IS {sh(split(xsr_series['ALL50 k=pieno*'])[0]):.2f})") + def _be(x): + return f"{x:.0f} bps" if x else f">{int(COST_SWEEP[-1])} bps" + print(f" break-even di costo per gamba: XS k=2 {_be(be.get('XS MAJ19 k=2'))}, " + f"XS k=5 {_be(be.get('XS MAJ19 k=5*'))}, XSR k=2 {_be(be.get('XSR ALL50 k=2'))}, " + f"XSR pieno {_be(be.get('XSR ALL50 k=pieno*'))}") + print("-" * 108) + print(" 1. CONCENTRARE NON COMPRA RENDIMENTO, COMPRA ESEGUIBILITA'. Il null del de-levering") + print(" boccia ogni k tranne i due che sono fortuna d'ancora (XS k=4: fase0 al 90Β° pctl,") + print(" mediana 1.10 < 1.15 del canonico; XSR k=6: +0.03 su 1.65). Nessuna concentrazione") + print(" batte il proprio originale a PARI maxDD.") + print(" 2. MA IL COSTO E' MOLTO PIU' BASSO DEL PREVISTO, e la mia ipotesi (a) era sbagliata:") + print(" mi aspettavo Sh ~ sqrt(ampiezza) e quindi il 40-60% dell'edge a k=2-3. Misurato:") + print(" XS-LITE k=2 conserva il 98% del canonico A MEDIANA DI FASE con 4 gambe invece di 10,") + print(" e N_hhi scende 6.9 -> 3.0. La radice dell'ampiezza NON descrive questa curva: la") + print(" dispersione cross-section e' concentrata negli ESTREMI del ranking, e le gambe 3-5") + print(" per lato aggiungono rischio quanto segnale. Il collasso vero e' solo a k=1 (41%).") + print(" 3. IL MURO NON E' IL MIN-ORDER, E' IL PESO DELLO SLEEVE. Con $600 DEDICATI, XS01 gira") + print(" gia' oggi a k=5 (tracking 1.04, haircut -0.04). E' al 15% del book che muore: a") + print(" sleeve $90 il canonico esegue il 4% delle variazioni e il lordo scende al 13% del") + print(" target. A quel capitale k=2 e' l'unica cella CON EDGE che traccia (1.01: k=1 traccia") + print(" ma non ha edge, Sh 0.13). Il '~$20k' e'") + print(" quindi il prodotto di due scelte (peso 15% + k=5), non una proprieta' del segnale.") + print(" 4. XSR01 NON SI CONCENTRA: k<=3 perde l'edge IN-SAMPLE (IS 0.96 -> 0.28) e il gate") + print(" marginale lo declassa ad ADDS -> NOISE (has_insample_edge=False). Ipotesi (c)") + print(" CONFERMATA: li' l'ampiezza E' l'edge. E muore a ~28 bps/gamba di costo totale a") + print(" OGNI k, contro >50 bps di XS-LITE: concentrare non ripara il rischio #1 di XSR01.") + print(" 5. β›” NIENTE DI QUESTO E' DEPLOYABILE OGGI, e non per una ragione misurata qui:") + print(" XS01/XSR01 vivono su HYPERLIQUID, e la decisione dell'operatore del 26/07 tiene") + print(" il 100% del capitale su Deribit fino a $20k. A $20k il canonico e' eseguibile da") + print(" solo. XS-LITE serve SE e QUANDO si apre HL sotto quella soglia β€” non prima.") + print("-" * 108) + print(" GATE PRE-REGISTRATO (dichiarato ORA, prima di qualunque dato nuovo):") + print(" XS-LITE k=2 su MAJ19 si riapre SOLO se si apre un conto Hyperliquid con >= $300") + print(" dedicati allo sleeve, e in quel caso si deploya il k CANONICO (5) se il capitale") + print(" dello sleeve e' >= $750, k=2 sotto. Nessun nuovo backtest serve per questa scelta:") + print(" e' gia' decisa dalla tabella (5) e non va ri-ottimizzata guardando i dati nuovi.") + print(" Come CANDIDATO a sleeve XS-LITE resta bocciato: DSR FAIL a ogni conteggio e null") + print(" del de-levering non superato. Non si ri-testa prima del 2027-06 (3.5 anni di") + print(" storia HL, di cui >=1.5 fuori dal campione di scoperta di XS01).") + print("=" * 108) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822_xsr_repro.py b/scripts/research/r0822_xsr_repro.py new file mode 100644 index 0000000..134318f --- /dev/null +++ b/scripts/research/r0822_xsr_repro.py @@ -0,0 +1,433 @@ +"""XSR-REPRO β€” lo Sharpe 1.82 di XSR01 non si riproduce: e' un problema di CODICE, di DATI, o di LENTE? + +PERCHE'. XSR01 ha un gate di deploy PRE-REGISTRATO al 2026-10-23 (`r0725_xsr_deploy_gate.py`): +deploy solo se Sharpe forward >= 1.0 E haircut di eseguibilita' a $5.000 <= 40%. I suoi numeri di +ammissione β€” Sharpe netta 1.82, lorda 2.70, maxDD -2.6%, vol 2.3%, ret 4.2%/a, lag 1.82/1.19/0.81/ +0.51, Sharpe per anno 1.03/1.98/3.11 β€” sono in CLAUDE.md, nel diario del 25/07 e CABLATI nel gate +(`IS_SHARPE_NET = 1.82`). Il filone XS-LITE (22/08) ha misurato che sulla finestra identica a quella +di scoperta oggi escono 1.75 (lente "paniere") e 2.23 (lente "libro"): 1.82 non e' nessuna delle due. +Una decisione fra due mesi poggia su un numero che non si riproduce. + +Questo script NON giudica XSR01 e NON anticipa il gate del 23/10 (sarebbe selection-on-forward). +Stabilisce SOLO se il numero su cui si decidera' e' solido, in quest'ordine: + + T1 QUALE LENTE. Tre implementazioni convivono nel repo e danno numeri diversi sugli STESSI dati: + L1 `r0725_statarb_basket_gate.basket_from_positions(demean=True)` β€” media per riga sulle sole + colonne NON-NaN (divisore variabile). E' la lente che ha girato i gate (marginale, DSR). + L2 `r0725_statarb_demean_skeptic.ret_from_pos` β€” P.fillna(0) e divisore FISSO 50. + L3 `scripts/live/paper_xsr.build_panel` + `_step` β€” libro con capitale, pesi Q/50, gamba BTC + esplicita. E' quella che alimenta il gate. + T2 QUALE STATO DEL DATO. `data/raw` e' gitignored e il cron riscrive i parquet HL ogni notte. + Ipotesi da falsificare: "stesso codice, dati diversi" (identico allo scoperto GTAA/TLT 07/08). + Prove indipendenti: (a) il log del cron registra `reali`/`bfill`/`start_reale` per simbolo a + ogni giro -> se il conteggio delle barre PASSATE e' cambiato, il feed e' stato riscritto; + (b) lo Sharpe per ANNO CHIUSO (2024, 2025) e' un'impronta della storia: se la storia e' + stabile deve riprodursi al centesimo attraverso 4 settimane di riscritture. + T3 IL MONITOR FORWARD. `paper_xsr` e' forward-only e append-only, quindi in teoria immune alla + riscrittura. Test diretto: replay del libro MODELED sui dati di OGGI dall'inception, barra per + barra, contro cio' che il monitor ha REGISTRATO giorno per giorno. Se la storia fosse stabile + e la lente giusta, le due serie devono coincidere. + T4 L'HAIRCUT A $5.000, l'altra meta' del gate: dipende dal min-order del venue. Il numero + pubblicato ($14.41 di ticket, soglia 40%) assume min-order $5 (Deribit). Hyperliquid potrebbe + essere $10. Si misura la sensibilita'. + +COSA MI ASPETTAVO PRIMA DI MISURARE: che fosse la lente (tre implementazioni, nessuna nominata nel +diario) e che il dato fosse stabile, perche' il log del cron mostra bar-count che crescono di 1/g. +Solo la prima meta' e' risultata vera. + + nice -n 19 timeout 900 uv run python scripts/research/r0822_xsr_repro.py +""" +from __future__ import annotations + +import json +import re +import sys +from collections import OrderedDict +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)) + +from r0725_statarb_multi import BASE, MIN_BARS, _dd, _sh, load_hl, signal, universe # noqa: E402 +from r0725_statarb_basket_gate import basket_from_positions, pair_frames # noqa: E402 +from r0725_statarb_demean_skeptic import ret_from_pos # noqa: E402 + +ANN = np.sqrt(365.0) +DISCOVERY = pd.Timestamp("2026-07-25", tz="UTC") # ultima barra vista dalla scoperta (PROVVISORIA) +CLOSED_AT_DISCOVERY = pd.Timestamp("2026-07-24", tz="UTC") # ultima barra CHIUSA quel giorno +CRONLOG = ROOT / "logs" / "cron_daily.log" +FWD = ROOT / "data" / "paper_xsr" / "returns.jsonl" +FWD_STATE = ROOT / "data" / "paper_xsr" / "state.json" + +# I NUMERI PUBBLICATI (CLAUDE.md + diario 2026-07-25 + docstring di paper_xsr / del gate) +PUB = dict(netta=1.82, lorda=2.70, dd=-2.6, vol=2.3, ret=4.2, + lag=(1.82, 1.19, 0.81, 0.51), anni=(1.03, 1.98, 3.11)) + + +def _cut(df, ts): + return df[df.index <= ts] + + +def _stats(r: pd.Series) -> dict: + return dict(n=len(r), sh=_sh(r.values), dd=_dd(r.values) * 100, + vol=float(r.std() * ANN * 100), ret=float(r.mean() * 365 * 100)) + + +# ------------------------------------------------------------------ pannelli +def panel(partial_last: bool = False, cut: pd.Timestamp | None = None): + """(P, S) come `pair_frames`, ma con due gradi di liberta' espliciti: + `cut` -> ultima barra inclusa (troncamento dell'INPUT, non dell'output); + `partial_last` -> l'ultima barra e' resa PARZIALE (close := open), cioe' com'era il giorno in + cui il cron l'aveva appena scritta. E' l'unico stato del dato che questo + script NON puo' leggere da disco: va ricostruito.""" + def px(sym): + d = pd.read_parquet(RAW / f"hl_{sym.lower()}_1d.parquet") + ix = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + c = pd.Series(d["close"].astype(float).values, index=ix).sort_index() + o = pd.Series(d["open"].astype(float).values, index=ix).sort_index() + c = c[~c.index.duplicated(keep="last")] + o = o[~o.index.duplicated(keep="last")] + if cut is not None: + c, o = c[c.index <= cut], o[o.index <= cut] + if partial_last and len(c): + c.iloc[-1] = o.iloc[-1] + return c + + base = px(BASE) + pc, sc = {}, {} + for sym in universe(): + try: + tgt = px(sym) + except FileNotFoundError: + continue + ix = base.index.intersection(tgt.index) + if len(ix) < MIN_BARS: + continue + p, s = signal(base[ix], tgt[ix]) + pc[sym] = pd.Series(p, index=ix) + sc[sym] = pd.Series(s, index=ix) + P = pd.concat(pc, axis=1, sort=True).sort_index() + S = pd.concat(sc, axis=1, sort=True).sort_index() + return P, S + + +def lens_L1(P, S): + return basket_from_positions(P, S, demean=True) + + +def lens_L2(P, S, fee_leg=0.0005, lag=0): + P0 = P.fillna(0.0) + S0 = S.reindex(P0.index)[P0.columns] + return ret_from_pos(P0, S0, fee_leg=fee_leg, lag=lag) + + +# ------------------------------------------------------------------ T1 +def t1_lenti(): + print("=" * 100) + print(" T1 β€” QUALE LENTE, E QUALE STATO DEL DATO, PRODUCE 1.82") + print("=" * 100) + P, S = pair_frames() + print(f"\n dato su disco oggi: {P.shape[1]} coppie, {len(P)} barre " + f"({P.index[0].date()} -> {P.index[-1].date()})") + + rows = [] + for tag, (Pi, Si) in [ + ("OGGI pieno", (P, S)), + (f"tronc. {DISCOVERY.date()} (barra completa)", (_cut(P, DISCOVERY), _cut(S, DISCOVERY))), + ("tronc. 2026-07-24 = ultima barra CHIUSA il 25/07", + (_cut(P, CLOSED_AT_DISCOVERY), _cut(S, CLOSED_AT_DISCOVERY))), + ]: + rows.append((f"L1 gate {tag}", _stats(lens_L1(Pi, Si)))) + rows.append((f"L2 scett. {tag}", _stats(lens_L2(Pi, Si)))) + + Pp, Sp = panel(partial_last=True, cut=DISCOVERY) + rows.append(("L1 gate STATO DEL 25/07 (ultima barra PARZIALE)", _stats(lens_L1(Pp, Sp)))) + rows.append(("L2 scett. STATO DEL 25/07 (ultima barra PARZIALE)", _stats(lens_L2(Pp, Sp)))) + + print(f"\n {'lente / stato del dato':<52}{'n':>6}{'Sharpe':>9}{'maxDD':>9}{'vol':>8}{'ret/a':>8}") + for nm, s in rows: + print(f" {nm:<52}{s['n']:>6}{s['sh']:>9.4f}{s['dd']:>8.2f}%{s['vol']:>7.2f}%{s['ret']:>7.2f}%") + print(f" {'--- PUBBLICATO 2026-07-25':<52}{'937?':>6}{PUB['netta']:>9.2f}" + f"{PUB['dd']:>8.1f}%{PUB['vol']:>7.1f}%{PUB['ret']:>7.1f}%") + + # confronto completo sugli OTTO numeri pubblicati, per la sola combinazione che li riproduce + print("\n --- verifica completa sullo stato che riproduce: L2 + ultima barra PARZIALE ---") + r = lens_L2(Pp, Sp) + g = lens_L2(Pp, Sp, fee_leg=0.0) + lags = tuple(_sh(lens_L2(Pp, Sp, lag=L).values) for L in (0, 1, 2, 3)) + anni = tuple(_sh(r[r.index.year == y].values) for y in (2024, 2025, 2026)) + st = _stats(r) + ok = 0 + def chk(nome, mio, pub, tol): + nonlocal ok + good = abs(mio - pub) <= tol + ok += good + print(f" {nome:<22} misurato {mio:>8.4f} pubblicato {pub:>7.2f} " + f"{'COINCIDE' if good else 'DIVERGE'}") + chk("Sharpe netta", st["sh"], PUB["netta"], 0.005) + chk("Sharpe lorda", _sh(g.values), PUB["lorda"], 0.005) + chk("maxDD %", st["dd"], PUB["dd"], 0.05) + chk("vol ann %", st["vol"], PUB["vol"], 0.05) + chk("ret ann %", st["ret"], PUB["ret"], 0.05) + for i, L in enumerate((0, 1, 2, 3)): + chk(f"lag +{L}g", lags[i], PUB["lag"][i], 0.006) + for i, y in enumerate((2024, 2025, 2026)): + chk(f"Sharpe {y}", anni[i], PUB["anni"][i], 0.005) + print(f"\n -> {ok}/12 numeri pubblicati riprodotti da L2 sullo stato del dato del 25/07.") + + # la lente che ha girato i GATE non e' quella del titolo + print("\n --- e la lente che ha girato i gate (marginale, deflated-Sharpe) e' L1, non L2 ---") + print(f" L1 sullo stato del 25/07 : Sharpe {_sh(lens_L1(Pp, Sp).values):.4f}") + print(" docstring di r0725_statarb_demean_skeptic.py, riga 5, cita il gate come " + "\"Sharpe 1.79\"") + print(" -> il numero di TITOLO (1.82, L2) e i numeri di GATE (DSR/marginale, L1) non sono") + print(" mai stati la stessa serie.") + + # deflated-Sharpe delle 3 varianti pre-registrate, oggi, sulle due lenti + try: + from altlib import deflated_sharpe + from src.portfolio.sleeves import XS_UNIVERSE + maj = [s for s in XS_UNIVERSE if s != BASE] + Pc, Sc = _cut(P, DISCOVERY), _cut(S, DISCOVERY) + var = {"V1 ALL50": dict(cols=None, demean=False), + "V2 MAJ19": dict(cols=maj, demean=False), + "V3 DEMEAN": dict(cols=None, demean=True)} + ser = {k: basket_from_positions(Pc, Sc, **v) for k, v in var.items()} + srs = [_sh(v.values) for v in ser.values()] + print("\n --- deflated-Sharpe delle 3 varianti pre-registrate, L1, finestra 25/07 ---") + for k, v in ser.items(): + d, nm = deflated_sharpe(_sh(v.values), srs, v.values, dpy=365.0) + print(f" {k:<11} Sharpe {_sh(v.values):>5.2f} DSR {d:>6.3f} " + f"(max atteso dal null {nm:>5.2f}) {'PASS' if d >= 0.95 else 'SOTTO 0.95'}") + print(" (pubblicato per V3: DSR 0.985)") + except Exception as e: # pragma: no cover + print(f" [DSR non calcolabile: {e.__class__.__name__}: {e}]") + return P, S + + +# ------------------------------------------------------------------ T2 +def t2_feed(P, S): + print("\n" + "=" * 100) + print(" T2 β€” IL FEED HL E' STATO RISCRITTO? (l'ipotesi principale: stesso codice, dati diversi)") + print("=" * 100) + + # (a) il log del cron registra reali/bfill/start_reale a ogni giro: e' un registro storico + runs, cur, inblk, hdr = OrderedDict(), None, False, None + for ln in CRONLOG.read_text(errors="replace").splitlines(): + m = re.match(r"^===== (\S+) cron_daily =====", ln) + if m: + cur, inblk = m.group(1), False + runs[cur] = {} + continue + if "FETCH + CERTIFY Hyperliquid" in ln: + inblk, hdr = True, None + continue + if inblk and cur is not None: + if ln.strip().startswith("sym"): + hdr = ln.split() + continue + f = ln.split() + if hdr and len(f) >= 7 and re.match(r"^[A-Z0-9]+$", f[0]) and f[1].isdigit(): + runs[cur][f[0]] = ((int(f[1]), f[3], f[-1]) if len(hdr) >= 9 + else (int(f[1]), f[2], f[-1])) + elif runs[cur] and (not ln.strip() or ln.startswith("=")): + inblk = False + keys = [k for k, v in runs.items() if v] + a = next((k for k in keys if k.startswith("2026-07-25")), None) + b = keys[-1] if keys else None + print(f"\n (a) registro del cron: {len(keys)} giri con blocco Hyperliquid, " + f"da {keys[0][:10]} a {keys[-1][:10]}") + if a and b: + A, B = runs[a], runs[b] + atteso = (pd.Timestamp(b[:10]) - pd.Timestamp(a[:10])).days + anom = [] + for s in sorted(set(A) | set(B)): + ra, rb = A.get(s), B.get(s) + if ra is None or rb is None: + anom.append((s, "presente in un solo giro", ra, rb)); continue + if rb[0] - ra[0] != atteso or ra[1] != rb[1]: + anom.append((s, f"delta {rb[0]-ra[0]} invece di {atteso}; start {ra[1]}->{rb[1]}", + ra, rb)) + print(f" confronto {a[:10]} vs {b[:10]} su {len(set(A)|set(B))} simboli, " + f"crescita attesa {atteso} barre") + if not anom: + print(" -> TUTTI i simboli crescono di esattamente il numero di giorni e " + "start_reale INVARIATO") + for s, why, ra, rb in anom: + print(f" {s:<8} {why} (verdetto oggi: {rb[2] if rb else '-'})") + print(" NB: i simboli con delta 0 sono quelli con verdetto `scarta` (serie ferma sul") + print(" venue): NON producono un parquet, quindi non entrano nell'universo.") + + # (b) impronta indipendente della storia: lo Sharpe degli ANNI CHIUSI + Pp, Sp = panel(partial_last=True, cut=DISCOVERY) + r_then = lens_L2(Pp, Sp) + r_now = lens_L2(P, S) + print("\n (b) impronta della storia β€” Sharpe degli anni CHIUSI, che una riscrittura sposterebbe:") + print(f" {'anno':<8}{'stato 25/07':>14}{'stato oggi':>13}{'pubblicato':>13}{'barre':>8}") + for y, pub in zip((2024, 2025, 2026), PUB["anni"]): + s1 = r_then[r_then.index.year == y] + s2 = r_now[r_now.index.year == y] + note = " <- anno IN CORSO: cambia perche' contiene barre nuove" if y == 2026 else "" + print(f" {y:<8}{_sh(s1.values):>14.4f}{_sh(s2.values):>13.4f}{pub:>13.2f}" + f"{len(s2):>8}{note}") + print(" -> 2024 e 2025 (731 barre) si riproducono al centesimo attraverso 4 settimane di") + print(" riscritture notturne: la STORIA CHIUSA e' stabile.") + + # (c) l'unica barra che cambia: l'ultima + d = pd.read_parquet(RAW / "hl_btc_1d.parquet") + vol = d["volume"].astype(float).values + q = float(vol[-1] / np.median(vol[-40:-1])) + print("\n (c) l'unica barra che NON e' definitiva e' l'ULTIMA. `fetch_hyperliquid` gira nel") + print(" cron delle 00:30 con END = oggi, quindi scrive la barra del giorno IN CORSO:") + print(f" hl_btc_1d ultima barra {pd.Timestamp(int(d['timestamp'].iloc[-1]), unit='ms', tz='UTC')}" + f" volume {vol[-1]:,.0f} = {q*100:.1f}% del volume mediano di un giorno pieno") + print(f" (una barra piena di ~24h; questa ne ha ~{q*24*60:.0f} minuti di scambi)") + print("\n VERDETTO T2: il feed VIENE riscritto, ma NON come ipotizzato. Non c'e' riscrittura") + print(" arbitraria della storia: c'e' UNA barra provvisoria per volta β€” l'ultima β€” che il giro") + print(" successivo completa. Ogni numero calcolato su dato HL il giorno D contiene una barra") + print(" di ~30-40 minuti travestita da giorno.") + + +# ------------------------------------------------------------------ T3 +def t3_monitor(): + print("\n" + "=" * 100) + print(" T3 β€” IL MONITOR FORWARD (che alimenta il gate del 23/10) REGISTRA CIO' CHE CREDE?") + print("=" * 100) + if not FWD.exists() or not FWD_STATE.exists(): + print(" [stato di paper_xsr assente: non misurabile]") + return + import importlib.util + spec = importlib.util.spec_from_file_location("paper_xsr_ro", ROOT / "scripts" / "live" / "paper_xsr.py") + px = importlib.util.module_from_spec(spec) + spec.loader.exec_module(px) # sola lettura: non si chiama main() + + rec = [json.loads(x) for x in FWD.read_text().splitlines() if x.strip()] + st = json.loads(FWD_STATE.read_text()) + ts, dt, W, R, rb, syms = px.build_panel() + print(f"\n inception {pd.Timestamp(st['start_ts'], unit='ms', tz='UTC').date()} " + f"barre registrate {len(rec)} universo {len(syms)} " + f"(uguale a quello congelato: {syms == st['syms']})") + + i0 = int(np.where(ts == st["start_ts"])[0][0]) + bk = px._book(px.MODELED_CAPITAL, len(syms)) + ric = {} + for i in range(i0 + 1, len(ts)): + ric[int(ts[i])] = px._step(bk, W[i], R[i], float(rb[i]), None) + + a = np.array([r["net_modeled"] for r in rec if r["ts"] in ric]) + b = np.array([ric[r["ts"]] for r in rec if r["ts"] in ric]) + same = int((np.abs(a - b) <= 1e-6).sum()) + print(f"\n replay del libro MODELED sui dati di OGGI, dall'inception, contro cio' che il") + print(f" monitor ha REGISTRATO giorno per giorno ({len(a)} barre appaiate):") + print(f" barre identiche entro 1e-6 : {same}/{len(a)}") + print(f" correlazione registrato/ricalcolato: {np.corrcoef(a, b)[0, 1]:+.4f}") + print(f" vol annualizzata REGISTRATA : {a.std()*ANN*100:.2f}%") + print(f" vol annualizzata RICALCOLATA : {b.std()*ANN*100:.2f}%") + print(f" (la strategia e' progettata a vol ~{PUB['vol']:.1f}%: la seconda e' quella giusta)") + frac = (a.std() / b.std()) ** 2 + print(f" rapporto di varianza {(a.std()/b.std())**2:.4f} -> in radice di tempo il monitor") + print(f" sta misurando ~{frac*24*60:.0f} minuti di mercato al giorno, non 24 ore.") + print("\n MECCANISMO (leggibile nel codice, non dedotto): `advance()` processa le barre con") + print(" ts > last_ts e poi porta last_ts sull'ULTIMA di esse. Al giro delle 00:30 del giorno D") + print(" l'ultima barra e' quella di D, appena scritta e lunga ~30 minuti: il monitor ne") + print(" registra il rendimento e SPOSTA last_ts oltre. Quando il giro successivo trova la") + print(" barra di D completata, D e' gia' passato. Le 23 ore e mezza restanti di OGNI giorno") + print(" non entrano in nessun rendimento registrato.") + sh_a = float(a.mean() / a.std() * ANN) + sh_b = float(b.mean() / b.std() * ANN) + se = float(np.sqrt(365.0 / len(a))) # SE di uno Sharpe annualizzato su n barre giornaliere + print(f"\n Sharpe della finestra forward: REGISTRATO {sh_a:+.2f} RICALCOLATO {sh_b:+.2f}") + print(f" ⚠ NESSUNO DEI DUE E' UNA LETTURA DEL GATE. Con {len(a)} barre l'errore standard di") + print(f" uno Sharpe annualizzato e' ~{se:.2f}: entrambi i numeri sono indistinguibili da 0 e") + print(" dalla soglia 1.0. Il gate e' il 2026-10-23 e questo script non lo anticipa; qui") + print(" servono solo a mostrare che i due strumenti non misurano la stessa grandezza.") + + +# ------------------------------------------------------------------ T4 +def t4_haircut(P, S): + print("\n" + "=" * 100) + print(" T4 β€” L'ALTRA META' DEL GATE: l'haircut di eseguibilita' a $5.000 e il min-order") + print("=" * 100) + import importlib.util + spec = importlib.util.spec_from_file_location("paper_xsr_ro2", ROOT / "scripts" / "live" / "paper_xsr.py") + px = importlib.util.module_from_spec(spec) + spec.loader.exec_module(px) + ts, dt, W, R, rb, syms = px.build_panel() + n = len(syms) + + def replay(cap0, min_order): + bk = px._book(cap0, n) + out = [] + for i in range(len(ts)): + out.append(px._step(bk, W[i], R[i], float(rb[i]), min_order)) + r = pd.Series(out, index=pd.to_datetime(ts, unit="ms", utc=True)) + tot = float(np.prod(1 + r.values) - 1) + fill = bk["n_fill"] / max(bk["n_fill"] + bk["n_skip"], 1) + return r, tot, fill + + print("\n replay dell'INTERA storia in-sample (non della finestra forward: quella e' del gate)") + print(f" {'libro':<28}{'Sharpe':>9}{'ret tot':>10}{'gambe eseguite':>17}{'haircut vs modeled':>21}") + rm, tot_m, _ = replay(5000.0, None) + print(f" {'MODELED (ribil. continuo)':<28}{_sh(rm.values):>9.2f}{tot_m*100:>9.1f}%" + f"{'100%':>17}{'-':>21}") + for mo in (5.0, 10.0, 25.0): + r5, tot5, fill = replay(5000.0, mo) + hc = (tot_m - tot5) / abs(tot_m) if tot_m else float("nan") + flag = "" if hc <= 0.40 else " <-- SOPRA LA GUARDIA 40%" + print(f" {f'REAL $5.000 min-order ${mo:.0f}':<28}{_sh(r5.values):>9.2f}{tot5*100:>9.1f}%" + f"{fill*100:>16.0f}%{hc*100:>20.1f}%{flag}") + + # ticket medio per gamba + dW = np.abs(np.diff(W, axis=0, prepend=np.zeros((1, n)))) + tick = dW[dW > 0] * 5000.0 + print(f"\n ticket per gamba a $5.000: mediano ${np.median(tick):.2f} medio ${tick.mean():.2f}") + print(f" (pubblicato: $14.41 medio) quota di ordini sotto $5: {(tick < 5).mean()*100:.0f}%" + f" sotto $10: {(tick < 10).mean()*100:.0f}%") + print("\n -> il min-order NON e' un dettaglio: e' la variabile che decide la seconda meta' del") + print(" gate, e il numero pubblicato assume $5. Se Hyperliquid e' a $10, la riga da leggere") + print(" il 23/10 e' la seconda, non la prima.") + + +def main() -> None: + P, S = t1_lenti() + t2_feed(P, S) + t3_monitor() + t4_haircut(P, S) + print("\n" + "=" * 100) + print(" CONTROMISURA PROPOSTA (non implementata: questo script non tocca produzione)") + print("=" * 100) + print(""" + Il difetto strutturale e' che `data/raw` e' gitignored e ogni numero di ammissione basato su + hl_* e' irriproducibile per costruzione. La contromisura minima, nella forma che il progetto usa + gia' altrove (guardia `TRONCATO`/`STORIA-CORTA` di `fetch_ib_equities.certify`): + + 1. IMPRONTA PER FINESTRA CHIUSA. A ogni giro, il certificatore HL scrive in un file versionato + (non in data/raw) `sha256` + conteggio barre + primo/ultimo timestamp per simbolo, calcolati + SOLO sulle barre fino a ieri (finestra chiusa). Un test confronta l'impronta di una finestra + gia' registrata: se cambia, la storia e' stata riscritta e il test rompe. Costa ~50 hash/giorno. + 2. LA BARRA IN CORSO VA ETICHETTATA, NON NASCOSTA. `load_hl` (e chiunque legga hl_*) deve poter + chiedere solo le barre CHIUSE. La forma minima e' un flag esplicito con default sicuro; la + forma che il progetto preferisce e' derivare la regola dal codice sorvegliato invece di + ridichiararla in ogni consumatore. + 3. I MONITOR FORWARD NON DEVONO CONSUMARE LA BARRA IN CORSO. `advance()` deve fermarsi + all'ultima barra CHIUSA; oggi ne consuma una provvisoria e sposta `last_ts` oltre, perdendo + il 98% di ogni giornata. E' un bug di produzione, non una scelta di modellazione. + + RISULTATI PUBBLICATI CHE POGGIANO SU hl_* (tutti da citare con questa riserva): + XS01 (sleeve nel book di ricerca al 15%) β€” 19 major HL 1d + XSR01 (candidato, gate 23/10) β€” 50 alt HL 1d + STATARB-MULTI (falsificazione 25/07) β€” 50 coppie alt/BTC HL + XS-LITE (22/08) β€” stesse serie + CC01 / breadth alt / dispersione XDISP β€” stesse serie + Per i BACKTEST l'effetto e' una barra su ~965 (piccolo ma non nullo: qui vale 1.82 -> 1.78). + Per i MONITOR FORWARD l'effetto e' totale, perche' la barra provvisoria e' l'UNICA che leggono. +""") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_bin_freq.py b/scripts/research/r0822b_bin_freq.py new file mode 100644 index 0000000..9130f92 --- /dev/null +++ b/scripts/research/r0822b_bin_freq.py @@ -0,0 +1,910 @@ +#!/usr/bin/env python +"""r0822b_bin_freq.py β€” BIN-FREQ: il BINARIO di frequenza su SKH01 giudicato come un CANDIDATO. + +STORIA DEI FATTI (verificata leggendo il codice, non il riassunto). + 1. `r0822_vol_size.py` ha proposto una SIZE PER-TRADE su SKH01 (cella congelata `VTL tv20 w90` + = clip(0.20/rv90(t-1), 0, 3) fissata all'INGRESSO) come CONTROLLO DEL RISCHIO: + +0,070 ISO dShFULL, 23/23 ancore, 8/8 anni. + 2. `r0822_volsize_skeptic.py` ha CONFERMATO il numero e FALSIFICATO il meccanismo: il guadagno + e' FREQUENZA (+0,051 sul solo filtro di attivita') e non MAGNITUDINE (+0,003); e la forma + minima e' un BINARIO 0/1 (`BIN L>=med`, 194/406 trade scartati) che vale +0,080 FULL (20/23) + = 115% del LEAD, con controllo di segno superato (l'inverso fa -0,159, 0/23). + 3. Lo scettico ha scritto "il candidato giusto non e' la formula ma il binario" β€” e nessuno + l'ha passato per `study_family_honest`. E' un SOTTOPRODOTTO DI UN ATTACCO, mai dichiarato + come famiglia. Questo script lo giudica come si giudica un candidato. + +IPOTESI, REGISTRATA PRIMA DI MISURARE. Mi aspetto che il numero si riproduca (lo scettico e' stato +rigoroso e la sua replica e' bit-exact) e che il candidato NON superi il gate onesto, per due +ragioni dichiarate in anticipo: (a) la soglia e la finestra sono due assi liberi trovati DENTRO +l'esplorazione di un'altra famiglia, quindi la stima di famiglia dovra' essere molto piu' piccola +dell'argmax (precedente: sull'asse w lo scettico stesso stima +0,025 contro +0,080); (b) il +deflated-Sharpe sara' VACUO come lo e' stato sulla famiglia VOL-SIZE β€” su perturbazioni della +STESSA strategia la varianza fra i trial e' quasi nulla, e il DSR e' ~1 per chiunque, baseline +compreso. Se succede lo dichiaro vacuo e uso un null che ha potenza. + +=========================== LA FAMIGLIA, DICHIARATA PRIMA DI GUARDARE ========================= +Un binario di frequenza su SKH01 ha esattamente quattro gradi di liberta'. Li dichiaro tutti, +con i loro valori, PRIMA di misurare, e li conto AL RIALZO: + + soglia (6) quantile ESPANDENTE causale della vol trailing, q in {0.25, 0.35, 0.50, 0.65, + 0.75} + la soglia ASSOLUTA rv <= 20% (== `L>=1` della regola congelata) + finestra (6) w in {45, 60, 90, 120, 180, 270} giorni (sotto 45 lo scettico ha misurato che + la finestra contiene 1-3 barre ATTIVE: campione vuoto, non fragilita') + isteresi (2) h in {0.00, 0.10} (banda +-10% attorno alla soglia, stato persistente causale) + segno (2) +1 tieni la vol BASSA (l'ipotesi) / -1 tieni la vol ALTA (controllo di segno) + timeframe(1) uno solo: la gamba SKH01 vive sulla griglia 230m per costruzione. Dichiarato. + -> 6 x 6 x 2 x 2 = 144 celle + +RIAPRIRE UN PARAMETRO RIAPRE LA SUA FAMIGLIA, e questo binario e' nato dentro l'esplorazione di +un'altra famiglia. Percio' al conto delle 144 si SOMMANO i trial gia' spesi sullo stesso oggetto: +28 (r0822_vol_size) + 43 (r0822_volsize_skeptic) = 71. + -> CONTO ONESTO DICHIARATO: 144 + 71 = 215 trial. +La sensibilita' del verdetto al conteggio e' pubblicata (Β§5), come nel precedente 30/07 dove un +DSR passava a N=8, falliva a N=72 e falliva a N=360. + +============================== LENTE, ANCORE, CONVENZIONI ===================================== +Lente: path CANONICO di SKH01 (fill al livello, entry a chiusura di bin) β€” identica nelle due +braccia di ogni confronto, quindi il Delta e' pulito anche se il livello e' pessimistico verso il +path live. Ancore: **23 offset**, la griglia a priori dell'audit 02/07 (ogni 30m su [0,690)): +NON ridotta, perche' il costo misurato e' ~20 ms per cella per ancora. Statistica: MEDIANA DELLE +DIFFERENZE APPAIATE (`A.anchor_luck_delta`), mai differenza delle mediane. +Confronto sempre ISO-PESO: il binario cambia la vol della gamba e quindi il peso EFFETTIVO di +SKH01 nel libro, e muovere quel peso e' gia' stato misurato e RESPINTO il 26/07. Il null del +de-levering e' DEGENERE su una ri-scalatura (`sh(k*base) === sh(base)`), quindi il test e' +iso-peso sullo Sharpe; `k_iso_dd` resta come diagnostica del DD. +Nessuna riscrittura: `extract / equity_daily / leg_daily / sizes_from_L / book / netted_target` +sono IMPORTATE da r0822_vol_size, e il titolo dello scettico e' riprodotto chiamando la SUA +funzione. Replica bit-exact contro `sleeves._skyhook_returns()` prima di ogni delta (Β§0). + +USO: nice -n 19 timeout 900 uv run python scripts/research/r0822b_bin_freq.py + [--every K] 1 ancora ogni K delle 23 (K=8 -> 3 ancore, pilota ~80 s) + [--ndraw N] estrazioni del null di permutazione (default 100) + Con i default gira in ~400 s e ~500 MB di RSS su questa VPS (2 core). Tutti i numeri + riportati qui vengono da questa invocazione, e due esecuzioni indipendenti hanno dato + output numericamente IDENTICI. +""" +from __future__ import annotations + +import argparse +import gc +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) + +import altlib as A # noqa: E402 +import r0702_anchor_skh01 as R # noqa: E402 +import r0702_tp01_offset as TO # noqa: E402 +import r0822_vol_size as V # noqa: E402 (motore: riusato, non riscritto) +import r0822_volsize_skeptic as SK # noqa: E402 (per riprodurre il titolo) +from r0822_sol_leg import k_iso_dd # noqa: E402 +from src.portfolio.portfolio import weights_tilt_null # noqa: E402 +from src.strategies.skyhook import SKH01_V1 # noqa: E402 + +ASSETS = V.ASSETS +OFFSETS_FULL = V.OFFSETS_FULL +sh, vol, maxdd, cagr = V.sh, V.vol, V.maxdd, V.cagr +hold, ins, book = V.hold, V.ins, V.book +CAPITAL, MIN_ORDER = V.CAPITAL, V.MIN_ORDER +W_TP, W_SKH = V.W_TP, V.W_SKH + +# ------------------------------------------------------------------ la famiglia (dichiarata sopra) +QS = (0.25, 0.35, 0.50, 0.65, 0.75) +THRS = tuple([("q", q) for q in QS] + [("abs", 0.20)]) +WS = (45, 60, 90, 120, 180, 270) +HS = (0.00, 0.10) +SIGNS = (+1, -1) +N_INHERITED = 28 + 43 # r0822_vol_size + r0822_volsize_skeptic, sullo stesso oggetto +CANON = ("q", 0.50, 90, 0.00, +1) # la cella dello scettico, dentro la mia famiglia +L = 112 + + +def cname(thr, w, h, sg) -> str: + t = "rv<=20%" if thr[0] == "abs" else f"q{thr[1]:.2f}" + return f"{t} w{w} h{h:.2f} s{'+' if sg > 0 else '-'}" + + +# =============================================================== il binario, causale per costruzione +def _hyst(x: np.ndarray, thr: np.ndarray, h: float, keep_below: bool) -> np.ndarray: + """Stato persistente con banda +-h attorno alla soglia. Causale: al passo t usa solo x_t e + thr_t (entrambi funzione di dati <= t-1) e lo stato al passo t-1. h=0 -> confronto secco.""" + n = len(x) + out = np.ones(n, bool) + st = True + for i in range(n): + xi, ti = x[i], thr[i] + if not (np.isfinite(xi) and np.isfinite(ti)): + st = True # warm-up: si tiene tutto (conv. scettico) + elif keep_below: + st = (xi <= ti * (1.0 + h)) if st else (xi <= ti * (1.0 - h)) + else: + st = (xi >= ti * (1.0 - h)) if st else (xi >= ti * (1.0 + h)) + out[i] = st + return out + + +class KeepCache: + """rv e quantili espandenti calcolati UNA volta per (asset, ancora, w). L'espandente e' la + parte cara; il resto e' un confronto.""" + + def __init__(self) -> None: + self.rv: dict = {} + self.q: dict = {} + + def clear(self) -> None: + self.rv.clear() + self.q.clear() + + def keep(self, ex: dict, key, thr, w: int, h: float, sg: int) -> pd.Series: + base = ex["base_daily"] + k = (key, w) + if k not in self.rv: + self.rv[k] = V._rv_daily(base, w, active_only=False) # gia' shiftata di 1 giorno + rv = self.rv[k] + if thr[0] == "abs": + t = np.full(len(rv), float(thr[1])) + else: + kq = (key, w, thr[1]) + if kq not in self.q: + self.q[kq] = rv.expanding(min_periods=V.VT_WARM).quantile(thr[1]) + t = self.q[kq].values + keep = _hyst(rv.values.astype(float), np.asarray(t, float), h, keep_below=(sg > 0)) + return pd.Series(keep.astype(float), index=rv.index) + + +KC = KeepCache() + + +def rss_mb() -> float: + """RSS in MB, per non far morire il lavoro degli altri agenti (brief Β§8: 2 core, ~3 GB).""" + try: + with open("/proc/self/status") as f: + for ln in f: + if ln.startswith("VmRSS:"): + return float(ln.split()[1]) / 1024.0 + except Exception: + pass + return float("nan") + + +def size_bin(thr, w, h, sg): + def f(ex, key): + return V.sizes_from_L(ex, KC.keep(ex, key, thr, w, h, sg)) + return f + + +# =============================================================== motore +def leg_of(EXX: dict, key, fn) -> pd.Series: + return V.leg_daily(EXX, {a: fn(EXX[a], (key, a)) for a in ASSETS}) + + +def iso_pair(TP: pd.Series, leg: pd.Series, leg_base: pd.Series): + """Libro della variante e libro di CONTROLLO ISO-PESO (baseline ri-scalato alla stessa vol di + gamba => stesso peso effettivo di SKH). Convenzione del bersaglio, riusata tale e quale.""" + vb = vol(leg_base) + m = vol(leg) / vb if vb > 0 else 1.0 + return book(TP, leg), book(TP, leg_base * m), m + + +# =============================================================== main +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--every", type=int, default=1) + ap.add_argument("--ndraw", type=int, default=100) + args = ap.parse_args() + offs = OFFSETS_FULL[:: max(1, args.every)] + if len(offs) < 3: + raise SystemExit(f"--every {args.every} lascia {len(offs)} ancore: " + "`A.anchor_luck_delta` ne vuole almeno 3 (usa --every <= 8).") + t0 = time.time() + + print("=" * L) + print(" r0822b BIN-FREQ β€” il BINARIO di frequenza su SKH01, giudicato come un CANDIDATO") + print("=" * L) + print(f" ancore: {len(offs)}/{len(OFFSETS_FULL)} della griglia a priori 02/07 (ogni 30m su " + f"[0,690)). NON ridotta.") + print(" lente: path CANONICO, identica nelle due braccia. Confronto sempre ISO-PESO.") + + # ---------------------------------------------------------- Β§1 famiglia, PRIMA di misurare + print("\n" + "-" * L) + print(" 1. GRIGLIA DICHIARATA β€” scritta prima di guardare i risultati, contata AL RIALZO") + print("-" * L) + cells = [(t, w, h, s) for t in THRS for w in WS for h in HS for s in SIGNS] + print(f" soglia {len(THRS):>3} {['q%.2f' % q for q in QS] + ['rv<=20% (== L>=1)']}") + print(f" finestra {len(WS):>3} {list(WS)} giorni") + print(f" isteresi {len(HS):>3} {list(HS)} (banda +-h attorno alla soglia)") + print(f" segno {len(SIGNS):>3} +1 tieni vol BASSA (ipotesi) / -1 tieni vol ALTA (controllo)") + print(f" timeframe 1 la gamba SKH01 vive su 230m per costruzione") + print(f" -> celle di QUESTA famiglia: {len(cells)}") + print(f" -> trial gia' spesi sullo STESSO oggetto: {N_INHERITED} " + f"(28 r0822_vol_size + 43 r0822_volsize_skeptic)") + print(f" -> CONTO ONESTO DICHIARATO: {len(cells)} + {N_INHERITED} = " + f"{len(cells) + N_INHERITED} trial. La sensibilita' al conteggio e' in Β§5.") + assert len(cells) == 144 + + # ---------------------------------------------------------- estrazione + print("\n estrazione tabelle trade (V2-DD) ...", flush=True) + EX = {o: {a: V.extract(a, o) for a in ASSETS} for o in offs} + ntr = {a: int(np.mean([len(EX[o][a]["i_ent"]) for o in offs])) for a in ASSETS} + R.get5m.cache_clear() # ~90 MB di 5m non servono durante la Β§4 (la VPS ha 2 core + gc.collect() # e ~3 GB liberi condivisi); si ricaricano in Β§9b in ~3 s + print(f" fatto in {time.time()-t0:.0f}s β€” trade medi/ancora: " + + ", ".join(f"{a} {ntr[a]}" for a in ASSETS) + f" (RSS {rss_mb():.0f} MB)") + TP = A.tp01_baseline_daily() + can = offs[0] + + # ---------------------------------------------------------- Β§0 SANITY + print("\n" + "-" * L) + print(" 0. SANITY β€” bit-exact prima di qualunque delta") + print("-" * L) + from src.portfolio.sleeves import _skyhook_returns + base_leg = {o: V.leg_daily(EX[o], {a: V.size_flat(EX[o][a]) for a in ASSETS}) for o in offs} + off = _skyhook_returns() + d0 = float(np.max(np.abs(base_leg[can].values - off.values))) if len(base_leg[can]) == len(off) \ + else float("nan") + print(f" gamba 50/50 off{can} vs sleeves._skyhook_returns(): max|dif| = {d0:.2e} " + f"({len(base_leg[can])} giorni)") + assert d0 < 1e-15, "replica NON bit-exact" + for a in ASSETS: + ref = R.run_asset(a, can)[0] + R._CACHE.clear() + dd = float(np.max(np.abs(V.equity_daily(EX[can][a], V.size_flat(EX[can][a])).values + - ref.values))) + print(f" {a} vs r0702_anchor_skh01.run_asset: max|dif| = {dd:.2e}") + assert dd < 1e-15 + + # ---------------------------------------------------------- Β§2 riproduzione del titolo + print("\n" + "-" * L) + print(" 2. RIPRODUZIONE DEL TITOLO DELLO SCETTICO β€” chiamando la SUA funzione, non la mia") + print(" (pubblicato: BIN L>=med +0,080 FULL 20/23 ; INVERSO -0,159 0/23)") + print("-" * L) + REP = {} + for tag, mode in (("BIN L>=med (scettico)", "gemed"), ("BIN L<1 INVERSO (scettico)", "lt1"), + ("BIN L>=1 (scettico)", "ge1")): + fn = SK.size_bin(SK.FROZEN, mode) + bk, ct = {}, {} + for o in offs: + lg = V.leg_daily(EX[o], {a: fn(EX[o][a]) for a in ASSETS}) + bk[o], ct[o], _ = iso_pair(TP, lg, base_leg[o]) + REP[tag] = (bk, ct) + dF = A.anchor_luck_delta(lambda o: bk[o], lambda o: ct[o], offs, metric=sh) + dH = A.anchor_luck_delta(lambda o: hold(bk[o]), lambda o: hold(ct[o]), offs, metric=sh) + print(f" {tag:<28} ISO dShFULL {dF['median_paired']:+.3f} " + f"({dF['n_positive']}/{len(offs)}) banda [{dF['lo']:+.3f},{dF['hi']:+.3f}] " + f"ISO dShHOLD {dH['median_paired']:+.3f} ({dH['n_positive']}/{len(offs)})") + med_F = float(np.median([sh(book(TP, base_leg[o])) for o in offs])) + med_H = float(np.median([sh(hold(book(TP, base_leg[o]))) for o in offs])) + med_D = float(np.median([maxdd(book(TP, base_leg[o])) for o in offs])) + ban_F = [sh(book(TP, base_leg[o])) for o in offs] + print(f" libro BASE, mediana della banda: ShFULL {med_F:.3f} / ShHOLD {med_H:.3f} / " + f"maxDD {med_D*100:.1f}% [pubblicato 1,626 / 1,037 / 10,4%]") + print(f" banda d'ancora del LIVELLO del libro: ShFULL [{min(ban_F):.3f},{max(ban_F):.3f}] " + f"= ampiezza {max(ban_F)-min(ban_F):.3f} (il termine di paragone di Β§12)") + + # ---------------------------------------------------------- Β§3 causalita' per troncamento + print("\n" + "-" * L) + print(" 3. CAUSALITA' β€” la decisione tenere/scartare ricalcolata TRONCANDO i dati al giorno") + print(" d'ingresso. `A.causality_ok` NON E' GIRATO (vuole un target_fn(df) e qui il") + print(" segnale nasce da 5m->230m: ri-eseguirlo su prefissi costa quanto tutto lo") + print(" script). Sostituito da un troncamento diretto, piu' stringente sul punto esatto.") + print("-" * L) + thr_c, w_c, h_c, s_c = ("q", CANON[1]), CANON[2], CANON[3], CANON[4] + bad = tested = 0 + for a in ASSETS: + ex = EX[can][a] + kfull = KC.keep(ex, (can, a), thr_c, w_c, h_c, s_c) + rv_full = V._rv_daily(ex["base_daily"], w_c, False) + days = list(dict.fromkeys(ex["day_ent"])) + for d in days[::7]: # 1 ingresso su 7, su tutta la storia + if d not in kfull.index: + continue + b_tr = ex["base_daily"][ex["base_daily"].index <= d] + if len(b_tr) < V.VT_WARM + w_c: + continue + rv_t = V._rv_daily(b_tr, w_c, False) + q_t = rv_t.expanding(min_periods=V.VT_WARM).quantile(thr_c[1]) + k_t = _hyst(rv_t.values.astype(float), q_t.values.astype(float), h_c, s_c > 0)[-1] + tested += 1 + if bool(k_t) != bool(kfull.loc[d] > 0.5): + bad += 1 + assert abs(float(rv_t.iloc[-1]) - float(rv_full.loc[d])) < 1e-12 or \ + not np.isfinite(rv_full.loc[d]) + print(f" decisioni ricalcolate su prefisso: {tested} DIVERGENTI: {bad}") + print(" NB l'isteresi ha memoria: troncando si perde lo stato, quindi il test e' esatto") + print(" per h=0 (la cella canonica) e conservativo per h>0.") + assert bad == 0, "il binario NON e' causale" + + # ---------------------------------------------------------- Β§4 tabella di famiglia + print("\n" + "-" * L) + print(" 4. LA FAMIGLIA INTERA β€” ISO dShFULL, mediana delle differenze appaiate su " + f"{len(offs)} ancore") + print("-" * L) + # RAM (brief Β§8: 7 GB totali, ~3 liberi, e siamo in tanti). Tre accorgimenti, tutti + # necessari β€” la prima stesura di questo script e' stata UCCISA a meta' della Β§4: + # (a) DUE PASSATE: la 1a calcola le metriche per-ancora di tutte le 144 celle SENZA + # conservare le serie (144 x 23 x 3 serie da 2717 punti = ~500 MB); la 2a ricalcola e + # conserva le serie delle sole 3-4 celle che servono dopo (~2 s); + # (b) ciclo ANCORA-ESTERNO con la cache di rv/quantili SVUOTATA a ogni ancora: viva una sola + # ancora alla volta (72 serie invece di 1.656); + # (c) RSS stampata, cosi' se qualcuno la rilancia sa cosa aspettarsi. + BK, CT, LEG, MS = {}, {}, {}, {} + ACC: dict = {} + + def _one(o, thr, w, h, sg, store: bool): + """UNA cella a UNA ancora. Le size si calcolano una volta sola e si riusano (la prima + stesura le ricalcolava per il conteggio dei tenuti); la scomposizione per anno e il maxDD + si fanno SOLO nella seconda passata, perche' servono a 3 celle e costavano il 60% del + tempo su tutte e 144.""" + nm = cname(thr, w, h, sg) + fn = size_bin(thr, w, h, sg) + szs = {a: fn(EX[o][a], (o, a)) for a in ASSETS} + lg = V.leg_daily(EX[o], szs) + b, c, m = iso_pair(TP, lg, base_leg[o]) + d = ACC.setdefault(nm, dict(thr=thr, w=w, h=h, sg=sg, dF=[], dH=[], dI=[], dC=[], + lF=[], lI=[], lH=[], ms=[], kp=[], ddv=[], ddc=[], yr={})) + d["dF"].append(sh(b) - sh(c)); d["dH"].append(sh(hold(b)) - sh(hold(c))) + d["dI"].append(sh(ins(b)) - sh(ins(c))) + d["lF"].append(sh(lg)); d["lI"].append(sh(ins(lg))); d["lH"].append(sh(hold(lg))) + d["ms"].append(m) + d["kp"].append(float(np.mean(np.concatenate(list(szs.values()))))) + if store: + d["dC"].append(cagr(b) - cagr(c)) + d["ddv"].append(maxdd(b)); d["ddc"].append(maxdd(c)) + for y in {int(t) for t in b.index.year}: + by, cy = b[b.index.year == y], c[c.index.year == y] + if len(by) > 20: + d["yr"].setdefault(y, []).append(sh(by) - sh(cy)) + BK.setdefault(nm, {})[o] = b + CT.setdefault(nm, {})[o] = c + LEG.setdefault(nm, {})[o] = lg + MS.setdefault(nm, {})[o] = m + + for o in offs: + KC.clear() + for (thr, w, h, sg) in cells: + _one(o, thr, w, h, sg, store=False) + KC.clear() + + def _med(v, dflt=float("nan")): + return float(np.median(v)) if len(v) else dflt + + def _fin(d: dict) -> dict: + a_ = np.asarray(d["dF"], float) + return dict(nm=cname(d["thr"], d["w"], d["h"], d["sg"]), thr=d["thr"], w=d["w"], + h=d["h"], sg=d["sg"], dF=float(np.median(a_)), nF=int((a_ > 0).sum()), + lo=float(a_.min()), hi=float(a_.max()), arrF=a_, + dH=float(np.median(d["dH"])), nH=int((np.asarray(d["dH"]) > 0).sum()), + isoI=float(np.median(d["dI"])), dC=_med(d["dC"]), + legF=float(np.median(d["lF"])), legI=float(np.median(d["lI"])), + legH=float(np.median(d["lH"])), kept=float(np.median(d["kp"])), + weff=float(np.median([W_SKH * x for x in d["ms"]])), + ddv=_med(d["ddv"]), ddc=_med(d["ddc"]), + yr={y: float(np.median(v)) for y, v in d["yr"].items()}) + + RES = {nm: _fin(d) for nm, d in ACC.items()} + print(f" ({len(cells)} celle x {len(offs)} ancore valutate in {time.time()-t0:.0f}s " + f"dall'avvio, prima passata senza conservare le serie; RSS {rss_mb():.0f} MB)") + + def matrix(sg: int, h: float) -> None: + print(f"\n ISO dShFULL β€” segno {'+1 (tieni vol BASSA)' if sg>0 else '-1 (CONTROLLO)'}" + f", isteresi h={h:.2f}") + print(" soglia " + "".join(f"{('w%d' % w):>11}" for w in WS)) + for thr in THRS: + t = "rv<=20%" if thr[0] == "abs" else f"q{thr[1]:.2f}" + row = "" + for w in WS: + r = RES[cname(thr, w, h, sg)] + row += f"{r['dF']:>+8.3f}{r['nF']:>3}" + print(f" {t:<10}" + row) + + for sg in SIGNS: + for h in HS: + matrix(sg, h) + print("\n (ogni cella: mediana appaiata e n. di ancore positive su " + f"{len(offs)})") + + POS = [RES[cname(t, w, h, +1)] for t in THRS for w in WS for h in HS] + NEG = [RES[cname(t, w, h, -1)] for t in THRS for w in WS for h in HS] + ALLC = [RES[cname(t, w, h, s)] for t in THRS for w in WS for h in HS for s in SIGNS] + nmcan = cname(("q", CANON[1]), CANON[2], CANON[3], CANON[4]) + argmax = max(ALLC, key=lambda r: r["dF"]) + print(f"\n ARGMAX della famiglia: {cname(argmax['thr'], argmax['w'], argmax['h'], argmax['sg']):<26}" + f" dShFULL {argmax['dF']:+.3f} ({argmax['nF']}/{len(offs)})") + print(f" cella dello scettico: {nmcan:<26} dShFULL {RES[nmcan]['dF']:+.3f} " + f"({RES[nmcan]['nF']}/{len(offs)}) rango " + f"{1 + sum(r['dF'] > RES[nmcan]['dF'] for r in ALLC)}/{len(ALLC)}") + print(f" STIMA ONESTA se la cella si sceglie a caso dentro la famiglia (mediana):") + print(f" tutta la famiglia (144) {float(np.median([r['dF'] for r in ALLC])):+.3f} " + f"positive {sum(r['dF']>0 for r in ALLC)}/{len(ALLC)}") + print(f" solo segno +1 (72) {float(np.median([r['dF'] for r in POS])):+.3f} " + f"positive {sum(r['dF']>0 for r in POS)}/{len(POS)}") + print(f" solo segno -1 (72) {float(np.median([r['dF'] for r in NEG])):+.3f} " + f"positive {sum(r['dF']>0 for r in NEG)}/{len(NEG)} <- controllo di segno") + # seconda passata: conserva le serie delle sole celle che servono dopo + sel_iso0 = max(ALLC, key=lambda r: r["isoI"]) + ACC2 = ACC + for r in dict((x["nm"], x) for x in (RES[nmcan], argmax, sel_iso0, + max(ALLC, key=lambda z: z["legI"]))).values(): + ACC = {} + for o in offs: + _one(o, r["thr"], r["w"], r["h"], r["sg"], store=True) + KC.clear() + RES[r["nm"]] = _fin(ACC[r["nm"]]) # ora con dC / maxDD / per-anno + ACC = ACC2 + POS = [RES[cname(t, w, h, +1)] for t in THRS for w in WS for h in HS] + NEG = [RES[cname(t, w, h, -1)] for t in THRS for w in WS for h in HS] + ALLC = POS + NEG + argmax = RES[argmax["nm"]] + dchk = A.anchor_luck_delta(lambda o: BK[nmcan][o], lambda o: CT[nmcan][o], offs, metric=sh) + assert abs(dchk["median_paired"] - RES[nmcan]["dF"]) < 1e-12 and \ + dchk["n_positive"] == RES[nmcan]["nF"], "la statistica appaiata di Β§4 non coincide con A.anchor_luck_delta" + print(f" controllo: la statistica appaiata calcolata in Β§4 coincide con " + f"`A.anchor_luck_delta` ({dchk['median_paired']:+.6f})") + print(f" quota di trade TENUTI, mia cella q0.50 w90: {RES[nmcan]['kept']*100:.1f}% " + f"(mediana fra le ancore)") + fn_can = size_bin(("q", CANON[1]), CANON[2], CANON[3], CANON[4]) + fn_sk = SK.size_bin(SK.FROZEN, "gemed") + ov, k_me, k_sk = [], [], [] + for o in offs: + for a in ASSETS: + x = fn_can(EX[o][a], (o, a)) > 0.5 + y = fn_sk(EX[o][a]) > 0.5 + ov.append(float((x == y).mean())); k_me.append(float(x.mean())); k_sk.append(float(y.mean())) + dleg = float(np.max(np.abs(BK[nmcan][can].values - REP["BIN L>=med (scettico)"][0][can].values))) + print(f" ⚠ la MIA parametrizzazione NON e' bit-identica a quella dello scettico: lui prende") + print(f" la mediana espandente di L = clip(0.20/rv,0,3) DOPO un `.fillna(1.0)` che inietta") + print(f" dei 1.0 nel warm-up; io prendo il quantile espandente di rv, che salta i NaN. Il") + print(f" trasformo e' monotono, quindi le due decisioni coincidono nel " + f"{float(np.mean(ov))*100:.1f}% dei trade") + print(f" (tenuti {float(np.mean(k_me))*100:.1f}% io / {float(np.mean(k_sk))*100:.1f}% lui); " + f"max|dif| fra i due libri = {dleg:.2e}.") + + # ---------------------------------------------------------- Β§5 i tre componenti del gate + print("\n" + "-" * L) + print(" 5. `study_family_honest` β€” NON chiamabile direttamente (e' cablato sui candidati") + print(" DIREZIONALI: factory -> target_fn via candidate_daily, e qui il candidato e' un") + print(" overlay su uno sleeve). Come il 30/07 sul tenore VRP01, uso i suoi TRE") + print(" COMPONENTI REALI: selezione in-sample-only -> deflated_sharpe -> marginal_vs_tp01.") + print("-" * L) + print(" (a) SELEZIONE IN-SAMPLE-ONLY β€” due criteri, entrambi dichiarati, entrambi sulla") + print(" MEDIANA fra le ancore (l'ancora non e' un hold-out, e' una dimensione di luck)") + sel_leg = max(ALLC, key=lambda r: r["legI"]) # come `select_cell_insample` + sel_iso = max(ALLC, key=lambda r: r["isoI"]) # cio' su cui si decide davvero + hop_leg = max(ALLC, key=lambda r: r["legH"]) + hop_iso = max(ALLC, key=lambda r: r["dH"]) + for tag, r in (("Sharpe IS standalone della gamba (come select_cell_insample)", sel_leg), + ("ISO dShFULL IN-SAMPLE del libro (cio' che decide)", sel_iso)): + n = cname(r["thr"], r["w"], r["h"], r["sg"]) + print(f" scelta al buio su {tag}") + print(f" -> {n:<26} ISO dShFULL {r['dF']:+.3f} ({r['nF']}/{len(offs)}) " + f"ISO dShHOLD {r['dH']:+.3f} ({r['nH']}/{len(offs)}) banda " + f"[{r['lo']:+.3f},{r['hi']:+.3f}]") + for tag, r in (("Sharpe HOLD-OUT standalone", hop_leg), ("ISO dShHOLD del libro", hop_iso)): + print(f" controllo (scegliendo SULL'HOLD-OUT, cio' che NON si fa) su {tag}:") + print(f" -> {cname(r['thr'], r['w'], r['h'], r['sg']):<26} dShFULL {r['dF']:+.3f}" + f" dShHOLD {r['dH']:+.3f}") + CHOSEN = sel_iso + nmc = cname(CHOSEN["thr"], CHOSEN["w"], CHOSEN["h"], CHOSEN["sg"]) + print(f" CELLA SCELTA AL BUIO (criterio primario = ISO dShFULL in-sample): {nmc}") + + print("\n (b) DEFLATED SHARPE β€” sullo Sharpe FULL standalone della gamba (la grandezza che") + print(" `select_cell_insample` usa), mediana fra le ancore; `daily` della barra canonica") + print(" serve solo per T/asimmetria/curtosi. Sensibilita' al CONTEGGIO ottenuta") + print(" REPLICANDO la distribuzione dei trial osservati (tiling), non riempiendola con") + print(" la mediana: riempire con la mediana ABBASSA la varianza dei trial e fa SALIRE il") + print(" DSR all'aumentare di N, che e' il verso sbagliato. (Errore mio, catturato nel") + print(" pilota e corretto: il primo output dava DSR 0,966 -> 0,986 -> 0,998 da N=144 a") + print(" N=500, cioe' 'piu' trial = piu' significativo'.)") + all_sr = [r["legF"] for r in ALLC] + pos_sr = [r["legF"] for r in POS] + base_sr = float(np.median([sh(base_leg[o]) for o in offs])) + dly = LEG[nmc][can] + + def tile(v, N): + return (list(v) * (N // len(v) + 1))[:N] + + for lab, sr, d in (("cella scelta al buio", CHOSEN["legF"], dly), + ("BASELINE (size fissa)", base_sr, base_leg[can])): + for N, tagN in ((len(ALLC), f"{len(ALLC)} di QUESTA famiglia"), + (len(ALLC) + N_INHERITED, + f"{len(ALLC)+N_INHERITED} = conto onesto (+{N_INHERITED} ereditati)"), + (500, "500 (ipotetico, al rialzo)")): + dsr, sr0 = A.deflated_sharpe(sr, tile(all_sr, N), d) + print(f" {lab:<22} N={tagN:<44} DSR {dsr:.3f} max atteso sotto il null {sr0:+.3f}") + lo_n, hi_n = len(ALLC), 20000 + if A.deflated_sharpe(CHOSEN["legF"], tile(all_sr, hi_n), dly)[0] >= 0.95: + cross = f">{hi_n}" + elif A.deflated_sharpe(CHOSEN["legF"], tile(all_sr, lo_n), dly)[0] < 0.95: + cross = f"<{lo_n}" + else: + for _ in range(40): + mid = (lo_n + hi_n) // 2 + if A.deflated_sharpe(CHOSEN["legF"], tile(all_sr, mid), dly)[0] >= 0.95: + lo_n = mid + else: + hi_n = mid + cross = f"~{lo_n}" + print(f" -> IL VERDETTO SI RIBALTA A N {cross} trial: sotto quel conteggio il DSR passa,") + print(f" sopra fallisce. Il conto onesto dichiarato ({len(ALLC)+N_INHERITED}) sta dal lato") + print(f" del PASS. Precedente 30/07: un DSR passava a N=8, falliva a N=72 e a N=360; qui") + print(f" la stessa sensibilita' e' pubblicata invece di essere taciuta.") + dsp, sr0p = A.deflated_sharpe(CHOSEN["legF"], tile(pos_sr, len(POS) + N_INHERITED), dly) + print(f" sensibilita' alla COMPOSIZIONE della famiglia: escludendo il segno -1 (che e' un") + print(f" controllo e non un candidato) i trial diventano {len(POS)}+{N_INHERITED} e la loro") + print(f" dispersione cala -> DSR {dsp:.3f} (max atteso {sr0p:+.3f}). Includere il segno -1") + print(f" e' la scelta CONSERVATIVA e resta quella dichiarata.") + print(f" dispersione dei trial: sd(Sharpe FULL) = {float(np.std(all_sr, ddof=1)):.3f} sulle") + print(f" {len(ALLC)} celle, {float(np.std(pos_sr, ddof=1)):.3f} sulle {len(POS)} di segno +1.") + print(f" -> il DSR qui NON e' vacuo (il baseline lo FALLISCE dove il candidato lo passa):") + print(f" la famiglia contiene celle davvero diverse fra loro, al contrario della famiglia") + print(f" VOL-SIZE dove erano tutte perturbazioni della stessa size continua.") + print(f" ❌ QUESTO REFUTA LA MIA IPOTESI (b), registrata nel docstring PRIMA di misurare") + print(f" ('il DSR sara' vacuo come su VOL-SIZE'). La ragione per cui mi sbagliavo e'") + print(f" strutturale e vale come regola: un DSR e' vacuo quando i trial si somigliano") + print(f" (var_tr -> 0 => sr0 -> 0 => DSR ~ 1 per chiunque). Una famiglia BINARIA contiene") + print(f" celle che scartano insiemi di trade MOLTO diversi β€” compreso il segno inverso β€”") + print(f" quindi la dispersione dei trial e' reale e il test riacquista potenza. Non e' il") + print(f" binario a essere piu' solido della formula: e' la FAMIGLIA a essere piu' larga.") + + print("\n (c) MARGINAL vs TP01 β€” sulla gamba modificata e sulla gamba base") + for lab, s in (("BASE (size fissa)", base_leg[can]), (nmc, LEG[nmc][can]), + ("cella dello scettico", LEG[nmcan][can])): + m = A.marginal_vs_tp01(s) + b25 = m.get("blends", {}).get("w25", {}) + print(f" {lab:<26} {str(m.get('marginal_verdict')):<10} corr {m.get('corr_full')} " + f"uplift w25 full {b25.get('uplift_full')} / hold {b25.get('uplift_hold')} " + f"robust_oos={m.get('robust_oos')} insample_edge={m.get('has_insample_edge')} " + f"hedge={m.get('is_hedge')}") + imp = A.implausible_sharpe( + LEG[nmc][can], + n_trades=int(sum(len(EX[can][a]["net"]) for a in ASSETS)), + n_losing_trades=int(sum((EX[can][a]["net"] < 0).sum() for a in ASSETS))) + print(f" implausible_sharpe (gamba scelta): implausible={imp['implausible']} " + f"attive {imp.get('active_frac', 0)*100:.1f}% perdite/attive " + f"{imp.get('loss_frac', 0)*100:.1f}% Calmar {imp.get('calmar', 0):.1f}") + + # ---------------------------------------------------------- Β§6 null con potenza + print("\n" + "-" * L) + print(" 6. IL NULL CHE HA POTENZA β€” permutazione della SEQUENZA tenuti/scartati.") + print(" Stessa quota di trade tenuti, riassegnata a caso: separa 'conta QUALE trade si") + print(" scarta' (informazione) da 'conta scartarne meta'' (aritmetica). Una mediana del") + print(" null lontana da 0 direbbe che il null non e' un null (lezione 22/08 sul null") + print(f" invertito). {args.ndraw} estrazioni su TUTTE le {len(offs)} ancore, non su 3.") + print("-" * L) + rng = np.random.default_rng(20260822) + for lab, r in (("cella scelta al buio", CHOSEN), ("cella dello scettico", RES[nmcan]), + ("argmax di famiglia", argmax)): + nm = cname(r["thr"], r["w"], r["h"], r["sg"]) + real = float(np.median([sh(BK[nm][o]) - sh(CT[nm][o]) for o in offs])) + fn = size_bin(r["thr"], r["w"], r["h"], r["sg"]) + sz0 = {o: {a: fn(EX[o][a], (o, a)) for a in ASSETS} for o in offs} + nul = [] + for _ in range(args.ndraw): + v = [] + for o in offs: + szp = {a: rng.permutation(sz0[o][a]) for a in ASSETS} + lg = V.leg_daily(EX[o], szp) + b, c, _ = iso_pair(TP, lg, base_leg[o]) + v.append(sh(b) - sh(c)) + nul.append(float(np.median(v))) + nul = np.asarray(nul) + pc = float((nul < real).mean()) + print(f" {lab:<24} {nm:<24} reale {real:+.3f} null mediana {np.median(nul):+.3f} " + f"p90 {np.percentile(nul, 90):+.3f} max {nul.max():+.3f} -> p ~ {1-pc:.3f}") + print(f" (seme 20260822; {args.ndraw} estrazioni x {len(offs)} ancore)") + print("\n ⚠ LA MEDIANA DEL NULL NON E' ~0, E NON E' UN DIFETTO DEL NULL: E' UN FATTO.") + print(" Scartare a caso meta' dei trade DANNEGGIA, perche' lo Sharpe ANNUALIZZATO cresce") + print(" con la radice del numero di scommesse per anno: dimezzare i trade sullo stesso") + print(" arco di calendario costa un fattore ~sqrt(2) di Sharpe di gamba. Misurato:") + nmv = cname(CHOSEN["thr"], CHOSEN["w"], CHOSEN["h"], CHOSEN["sg"]) + fnv = size_bin(CHOSEN["thr"], CHOSEN["w"], CHOSEN["h"], CHOSEN["sg"]) + rng2 = np.random.default_rng(20260823) + shb = float(np.median([sh(base_leg[o]) for o in offs])) + shv = float(np.median([sh(LEG[nmv][o]) for o in offs])) + kp = float(np.median([np.mean(np.concatenate([fnv(EX[o][a], (o, a)) for a in ASSETS])) + for o in offs])) + shn = [] + for _ in range(min(40, args.ndraw)): + sz0 = {o: {a: fnv(EX[o][a], (o, a)) for a in ASSETS} for o in offs} + shn.append(float(np.median([sh(V.leg_daily(EX[o], {a: rng2.permutation(sz0[o][a]) + for a in ASSETS})) for o in offs]))) + print(f" Sharpe della GAMBA: baseline (tutti i trade) {shb:.3f} -> permutata " + f"{float(np.median(shn)):.3f} -> filtrata {shv:.3f}") + print(f" quota tenuta {kp*100:.0f}% -> attesa dalla sola perdita di scommesse " + f"{shb*np.sqrt(kp):.3f}; osservata sotto permutazione {float(np.median(shn)):.3f}") + print(" Conseguenza per la lettura: il null NON e' centrato su zero, quindi il p-value") + print(" risponde alla domanda 'la SELEZIONE batte una selezione casuale della stessa") + print(" taglia?' e non alla domanda 'il filtro batte il non filtrare?'. La seconda e' il") + print(" confronto con la BASE (Β§4), che e' quello su cui si decide. Il filtro deve") + print(" superare una penale meccanica prima di guadagnare qualcosa: e' un test SEVERO.") + + # ---------------------------------------------------------- Β§7 iso-peso / de-levering + print("\n" + "-" * L) + print(" 7. ISO-PESO e NULL DEL DE-LEVERING") + print("-" * L) + print(f" {'cella':<26}{'wSKH eff':>10}{'dShFULL RAW':>13}{'ISO':>9}{'maxDD var':>11}" + f"{'maxDD ctrl':>11}{'k_iso_dd':>10}") + for lab, r in (("BASE", None), ("cella dello scettico", RES[nmcan]), + ("cella scelta al buio", CHOSEN)): + if r is None: + print(f" {'BASE (size fissa)':<26}{W_SKH:>10.3f}{0.0:>+13.3f}{0.0:>+9.3f}" + f"{maxdd(book(TP, base_leg[can]))*100:>10.1f}%{'-':>11}{'-':>10}") + continue + nm = cname(r["thr"], r["w"], r["h"], r["sg"]) + raw = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], + lambda o: book(TP, base_leg[o]), offs, metric=sh) + dv = float(np.median([maxdd(BK[nm][o]) for o in offs])) + dc = float(np.median([maxdd(CT[nm][o]) for o in offs])) + ks = (f"{k_iso_dd(CT[nm][can], BK[nm][can]):.3f}" if dv < dc + else "n/a") # n/a = non riduce il DD: nulla da negare + print(f" {nm:<26}{r['weff']:>10.3f}{raw['median_paired']:>+13.3f}{r['dF']:>+9.3f}" + f"{dv*100:>10.1f}%{dc*100:>10.1f}%{ks:>10}") + print(" Il null del de-levering e' DEGENERE su una ri-scalatura (sh(k*base) === sh(base)):") + print(" qui il test e' l'iso-peso sullo Sharpe. `k_iso_dd` e' riportato come diagnostica del") + print(" DD ed e' calcolato CONTRO il controllo iso-peso (non contro il baseline nudo).") + print(" NB il binario ABBASSA il peso effettivo di SKH01, non lo alza: la preoccupazione del") + print(" 26/07 ('alzare SKH01') non morde, ma l'iso-peso resta obbligatorio nell'altro verso.") + print(f"\n LIVELLI del libro (mediana fra le {len(offs)} ancore), netti fee:") + print(f" {'configurazione':<30}{'ShFULL':>9}{'ShHOLD':>9}{'ShIS':>8}{'maxDD':>9}{'CAGR':>9}" + f"{'vol':>8}") + lev = [("BASE (libro live 75/25)", {o: book(TP, base_leg[o]) for o in offs})] + for lab, r in (("BIN, peso eff. lasciato cadere", RES[nmcan]), + ("BIN, controllo ISO-PESO", None)): + if r is None: + lev.append((lab, {o: CT[nmcan][o] for o in offs})) + else: + lev.append((lab, {o: BK[nmcan][o] for o in offs})) + for lab, dd_ in lev: + f_ = float(np.median([sh(dd_[o]) for o in offs])) + h_ = float(np.median([sh(hold(dd_[o])) for o in offs])) + i_ = float(np.median([sh(ins(dd_[o])) for o in offs])) + d_ = float(np.median([maxdd(dd_[o]) for o in offs])) + c_ = float(np.median([cagr(dd_[o]) for o in offs])) + v_ = float(np.median([vol(dd_[o]) for o in offs])) + print(f" {lab:<30}{f_:>9.3f}{h_:>9.3f}{i_:>8.3f}{d_*100:>8.1f}%{c_*100:>8.1f}%" + f"{v_*100:>7.1f}%") + print(" ⚠ la DIFFERENZA di queste mediane NON e' la mediana delle differenze appaiate (Β§4):") + print(" qui le mediane cadono su ancore diverse. La tabella serve a dare i LIVELLI netti fee") + print(" richiesti dal contratto; il numero che decide resta quello appaiato.") + + # ---------------------------------------------------------- Β§8 weights_tilt_null + print("\n" + "-" * L) + print(" 8. `weights_tilt_null` β€” una modulazione binaria della size E' un cambio di pesi nel") + print(" tempo. Il gate confronta vettori STATICI: cattura la sola componente di SCALA") + print(" (limite dichiarato, come nel bersaglio). k_seen = tutti i trial dichiarati.") + print("-" * L) + cols = {"TP01": TP, "SKH01": base_leg[can]} + for lab, r in (("cella dello scettico", RES[nmcan]), ("cella scelta al buio", CHOSEN), + ("argmax di famiglia", argmax)): + nm = cname(r["thr"], r["w"], r["h"], r["sg"]) + m = MS[nm][can] + wp = {"TP01": W_TP, "SKH01": W_SKH * m} + g = weights_tilt_null(cols, {"TP01": W_TP, "SKH01": W_SKH}, wp, caps={"SKH01": 0.5}, + floor=0.05, n=300, k_seen=len(ALLC) + N_INHERITED) + print(f" {lab:<24} {nm:<24} peso equiv SKH {wp['SKH01']/sum(wp.values()):.3f} " + f"d_IS {g['delta_insample']:+.4f} d_HOLD {g['delta_hold']:+.4f} " + f"pctl {g['pctl_hold']:.1f} gate_pass={g['gate_pass']}") + print(" ⚠ LIMITE, e va detto perche' e' aggirabile: il binario ABBASSA il peso effettivo di") + print(" SKH01, e il gate misura proprio quella caduta. Nella forma di deploy naturale (si") + print(" ri-scala la gamba per riportare il peso effettivo a 0,25) il vettore proposto e'") + print(" IDENTICO a quello corrente -> d_IS = d_HOLD = 0 e il gate diventa INERTE, non") + print(" superato. Quindi `weights_tilt_null` NON puo' essere l'evidenza decisiva qui: e'") + print(" negativo nella forma nuda e vacuo nella forma compensata. Cio' che decide resta la") + print(" banda APPAIATA a iso-peso (Β§4) e la stima di famiglia.") + + # ---------------------------------------------------------- Β§9 eseguibilita' + print("\n" + "-" * L) + print(f" 9. ESEGUIBILITA' a ${CAPITAL:.0f} β€” il vantaggio DICHIARATO dal filone (dimezza gli") + print(" ordini). Target NETTATO 0.75*TP01 + 0.25*SKH sul grid 1h = cio' che il cron manda.") + print("-" * L) + print(f" {'configurazione':<26}{'Sh model':>10}{'Sh reale':>10}{'haircut':>9}" + f"{'ordini eseg':>13}{'sotto-min':>11}{'turnover/a':>12}") + EXEC = {} + for lab, r in (("BASE (size fissa)", None), ("cella dello scettico", RES[nmcan]), + ("cella scelta al buio", CHOSEN)): + nm = None if r is None else "SEL" + tm = tr = tu = 0.0 + no = ns = 0 + for a in ASSETS: + df1h = TO.get1h(a) + SZa = {} if r is None else { + "SEL": (lambda ex, rr=r, aa=a: size_bin(rr["thr"], rr["w"], rr["h"], + rr["sg"])(ex, (can, aa)))} + tgt = V.netted_target(a, EX[can][a], nm, SZa, {}, {}, {}, {}, TP, can, df1h) + ev = A.eval_weights_smallcap(df1h, tgt, capital=CAPITAL, min_order=MIN_ORDER) + tm += 0.5 * ev["modeled"]["sharpe"] + tr += 0.5 * ev["realistic"]["sharpe"] + tu += ev["executed_turnover_per_year"] + no += ev["n_executed_trades"] + dw = np.abs(np.diff(np.nan_to_num(tgt), prepend=0.0)) + ns += int(((dw > 1e-12) & (dw * CAPITAL < MIN_ORDER)).sum()) + EXEC[lab] = (tm, tr, no, ns, tu) + print(f" {lab:<26}{tm:>10.3f}{tr:>10.3f}{tm-tr:>9.3f}{no:>13}{ns:>11}{tu:>12.1f}") + print(" 'ordini eseg' e 'sotto-min' sono sui due asset sommati, su tutta la storia 1h.") + ntr_base = int(sum(len(EX[can][a]['i_ent']) for a in ASSETS)) + ntr_sel = int(sum((size_bin(CHOSEN['thr'], CHOSEN['w'], CHOSEN['h'], CHOSEN['sg']) + (EX[can][a], (can, a)) > 0).sum() for a in ASSETS)) + print(f" trade SKH eseguiti (off{can}): BASE {ntr_base} -> cella scelta {ntr_sel} " + f"({ntr_sel/ntr_base*100:.0f}%)") + print(" ⚠ IL RISPARMIO DI FEE E' GIA' DENTRO i numeri sopra: `extract` netta ogni trade di") + print(f" FEE_RT={V.FEE_RT:.4f} (10 bps RT) all'ingresso, quindi un trade scartato non paga.") + print(" Convertirlo di nuovo con la curva del 26/07 (-0,017 Sh/bps di libro, SKH 4x TP01)") + print(" sarebbe un DOPPIO CONTEGGIO. Cio' che NON e' dentro e' (a) il min-order a $635,") + print(" misurato qui sopra come haircut, e (b) il fatto che la fee reale e' 3,5 bps/lato =") + print(" 7 bps RT contro i 10 modellati -> i backtest sono CONSERVATIVI, in entrambe le braccia.") + + # ---------------------------------------------------------- Β§9b fee vs informazione + print("\n" + "-" * L) + print(" 9b. QUANTO DELL'EFFETTO E' INFORMAZIONE E QUANTO E' SOLO FEE RISPARMIATA.") + print(" Scartare meta' dei trade risparmia meta' delle fee: parte del +0,080 e' quella,") + print(" ed e' un guadagno vero ma NON e' un'ipotesi sulla vol. Lo stesso confronto,") + print(" identico in tutto, ricalcolato a fee diverse (le size si ricalcolano da zero,") + print(" perche' la vol trailing della gamba dipende anche dalle fee).") + print("-" * L) + print(f" {'lente':<40}{'cella scettico':>17}{'pos/n':>8}{'cella al buio':>17}{'pos/n':>8}") + FEEDEC = {} + keep_fee = V.FEE_RT + for fee, tag in ((keep_fee, f"MODELLATA {keep_fee*1e4:.0f} bps RT (i backtest)"), + (0.0007, "REALE 3,5 bps/lato = 7 bps RT"), + (0.0, "fee ZERO (informazione pura)")): + V.FEE_RT = fee + if fee == keep_fee: + EXf, basef = EX, base_leg + else: + EXf = {o: {a: V.extract(a, o) for a in ASSETS} for o in offs} + basef = {o: V.leg_daily(EXf[o], {a: V.size_flat(EXf[o][a]) for a in ASSETS}) + for o in offs} + row = "" + for r in (RES[nmcan], CHOSEN): + fn = size_bin(r["thr"], r["w"], r["h"], r["sg"]) + bk, ct = {}, {} + for o in offs: + lg = V.leg_daily(EXf[o], {a: fn(EXf[o][a], (tag, o, a)) for a in ASSETS}) + bk[o], ct[o], _ = iso_pair(TP, lg, basef[o]) + d = A.anchor_luck_delta(lambda o: bk[o], lambda o: ct[o], offs, metric=sh) + FEEDEC[(tag, cname(r["thr"], r["w"], r["h"], r["sg"]))] = d["median_paired"] + row += f"{d['median_paired']:>+17.3f}{d['n_positive']:>5}/{len(offs):<3}" + print(f" {tag:<40}{row}") + if fee != keep_fee: + del EXf, basef + gc.collect() + V.FEE_RT = keep_fee + assert V.FEE_RT == 0.001 + print(f" (RSS {rss_mb():.0f} MB)") + f_mod = FEEDEC[(f"MODELLATA {keep_fee*1e4:.0f} bps RT (i backtest)", nmcan)] + f_zero = FEEDEC[("fee ZERO (informazione pura)", nmcan)] + quota_info = 100.0 * f_zero / f_mod if f_mod else float("nan") + print(f" -> a fee ZERO resta {f_zero:+.3f} di {f_mod:+.3f} = {quota_info:.0f}% dell'effetto.") + if quota_info >= 85.0: + print(" ❌ IPOTESI REFUTATA (era la mia, ed era anche quella del compito): il vantaggio") + print(" NON e' fee risparmiata. A fee zero l'effetto c'e' tutto, quindi scartare i") + print(" trade in alta vol e' INFORMAZIONE sul rendimento lordo, non un risparmio di") + print(" costo. Conseguenza pratica: NON si puo' replicare abbassando il turnover, e") + print(" NON va convertito con la curva fee del 26/07 (sarebbe doppio conteggio in una") + print(" direzione e attribuzione sbagliata nell'altra).") + else: + print(" Una parte e' fee risparmiata; la fee vera (3,5 bps/lato) e' piu' bassa di quella") + print(" modellata, quindi nel LIVE quella componente e' PIU' PICCOLA.") + + # ---------------------------------------------------------- Β§10 trasferimento su V1 + print("\n" + "-" * L) + print(" 10. TRASFERIMENTO su SKH01_V1 β€” seconda sequenza di trade, geometria d'uscita") + print(" DIVERSA (stop/target in ATR invece che a percentuale fissa). Non e' un hold-out") + print(" temporale, ma e' l'unico campione indipendente disponibile a questo costo.") + print("-" * L) + EX1 = {o: {a: V.extract(a, o, SKH01_V1) for a in ASSETS} for o in offs} + R.get5m.cache_clear() # i 5m non servono piu': ~80 MB restituiti + gc.collect() + base1 = {o: V.leg_daily(EX1[o], {a: V.size_flat(EX1[o][a]) for a in ASSETS}) for o in offs} + print(f" {'cella':<26}{'ISO dShFULL':>13}{'pos/n':>8}{'ISO dShHOLD':>14}{'pos/n':>8}" + f"{'tenuti':>9}") + for lab, r in (("cella dello scettico", RES[nmcan]), ("cella scelta al buio", CHOSEN)): + fn = size_bin(r["thr"], r["w"], r["h"], r["sg"]) + b1, c1, kp = {}, {}, [] + for o in offs: + lg = V.leg_daily(EX1[o], {a: fn(EX1[o][a], ("V1", o, a)) for a in ASSETS}) + b1[o], c1[o], _ = iso_pair(TP, lg, base1[o]) + kp.append(float(np.mean(np.concatenate( + [fn(EX1[o][a], ("V1", o, a)) for a in ASSETS])))) + dF = A.anchor_luck_delta(lambda o: b1[o], lambda o: c1[o], offs, metric=sh) + dH = A.anchor_luck_delta(lambda o: hold(b1[o]), lambda o: hold(c1[o]), offs, metric=sh) + print(f" {cname(r['thr'], r['w'], r['h'], r['sg']):<26}{dF['median_paired']:>+13.3f}" + f"{dF['n_positive']:>5}/{len(offs):<3}{dH['median_paired']:>+14.3f}" + f"{dH['n_positive']:>5}/{len(offs):<3}{float(np.median(kp))*100:>8.0f}%") + del EX1, base1 + KC.clear() + gc.collect() + print(f" (RSS {rss_mb():.0f} MB)") + + # ---------------------------------------------------------- Β§11 per anno + print("\n" + "-" * L) + print(" 11. SCOMPOSIZIONE PER ANNO (regola 22/08 dopo SOL: un contributo positivo si") + print(" scompone per ANNO prima di crederci β€” 24/24 ancore possono essere un anno solo)") + print("-" * L) + yrs = sorted({int(y) for y in BK[nmcan][can].index.year}) + print(f" {'cella':<26}" + "".join(f"{y:>8}" for y in yrs)) + for lab, r in (("cella dello scettico", RES[nmcan]), ("cella scelta al buio", CHOSEN)): + nm = cname(r["thr"], r["w"], r["h"], r["sg"]) + cc = [] + for y in yrs: + v = [sh(BK[nm][o][BK[nm][o].index.year == y]) - sh(CT[nm][o][CT[nm][o].index.year == y]) + for o in offs if (BK[nm][o].index.year == y).sum() > 20] + cc.append(float(np.median(v)) if v else float("nan")) + print(f" {nm:<26}" + "".join(f"{c:>+8.2f}" for c in cc) + + f" positivi {int(np.nansum(np.asarray(cc) > 0))}/{len(yrs)}") + + # ---------------------------------------------------------- Β§12 la domanda che decide + print("\n" + "-" * L) + print(" 12. LA DOMANDA CHE DECIDE β€” un effetto che vale un terzo della propria incertezza") + print(" merita di cambiare un sistema che gira con soldi veri?") + print("-" * L) + amp = max(ban_F) - min(ban_F) + nm = nmcan + dC = float(np.median([cagr(BK[nm][o]) - cagr(CT[nm][o]) for o in offs])) + cag_band = [cagr(book(TP, base_leg[o])) for o in offs] + print(f" (i) IL CONFRONTO GIUSTO NON E' QUELLO DELLA DOMANDA. La banda d'ancora del") + print(f" LIVELLO ({amp:.3f} di ampiezza, +-{amp/2:.3f}) e' incertezza sul livello, e") + print(f" si CANCELLA in un confronto APPAIATO: le due braccia girano sulla STESSA") + print(f" ancora. Cio' che va confrontato con l'effetto e' la dispersione della") + print(f" DIFFERENZA appaiata, che e' un ordine di grandezza piu' piccola.") + print(f" cella scettico: effetto {RES[nm]['dF']:+.3f}, banda appaiata " + f"[{RES[nm]['lo']:+.3f},{RES[nm]['hi']:+.3f}], " + f"{RES[nm]['nF']}/{len(offs)} ancore positive.") + print(f" (ii) MA la banda del livello torna a mordere DUE volte, e in modo decisivo:") + print(f" (a) la SCELTA della cella e' fatta guardando quella banda -> la stima onesta") + print(f" non e' l'argmax ma la MEDIANA DI FAMIGLIA " + f"({float(np.median([r['dF'] for r in POS])):+.3f} sul segno +1);") + print(f" (b) l'effetto e' misurato sugli STESSI 8 anni su cui SKH01 e' stato") + print(f" selezionato, e non esiste un forward capace di misurarlo (SE(Sharpe)") + print(f" ~1,4 su 6 mesi contro un effetto di 0,08).") + print(f" (iii) IL CRITERIO, dichiarato: si tocca il libro live solo se TUTTE E QUATTRO β€”") + print(f" (1) l'effetto sopravvive alla stima ONESTA di famiglia (mediana, non argmax)") + print(f" (2) `weights_tilt_null` passa (e' il gate codificato per i cambi di peso)") + print(f" (3) il gate marginale + un null CON POTENZA lo sostengono") + print(f" (4) l'effetto e' grande abbastanza da pagare un modo di fallire in piu'.") + print(f" (iv) ORDINI DI GRANDEZZA a ${CAPITAL:.0f} (regola 26/07: quando le leve hanno") + print(f" ordini di grandezza diversi, DIRLO):") + print(f" effetto della cella (ISO, mediana appaiata): dCAGR {dC*100:+.2f}%/anno") + print(f" = ${dC*CAPITAL:+.2f}/anno = EUR {dC*CAPITAL/365*0.86:+.4f}/giorno") + print(f" banda d'ancora del CAGR del libro: " + f"[{min(cag_band)*100:.1f}%, {max(cag_band)*100:.1f}%] " + f"= ${(max(cag_band)-min(cag_band))*CAPITAL:.0f}/anno di incertezza") + print(f" versare EUR 500/mese (il piano dell'operatore) = EUR 16,44/giorno di capitale") + print(f" nuovo, cioe' ~{16.44/max(1e-9, abs(dC*CAPITAL/365*0.86)):.0f}x l'effetto misurato qui.") + print(f" (v) IL CRITERIO APPLICATO, riga per riga. La forma quantitativa di (1) e'") + print(f" dichiarata qui: 'sopravvive alla stima di famiglia' = la MEDIANA della meta'") + print(f" d'ipotesi vale almeno la META' della cella selezionata (altrimenti non e' un") + print(f" plateau, e' una punta), E la quota di celle positive e' chiaramente sopra il") + print(f" 50% (altrimenti la famiglia e' una monetina). Soglie scelte per essere") + print(f" verificabili, non per far passare o bocciare un numero gia' visto.") + med_pos = float(np.median([r["dF"] for r in POS])) + fpos = sum(r["dF"] > 0 for r in POS) / len(POS) + c1 = (med_pos >= 0.5 * CHOSEN["dF"]) and (fpos >= 0.65) + eurday = dC * CAPITAL / 365 * 0.86 + c4 = abs(eurday) >= 0.10 + print(f" (1) mediana di famiglia (segno +1) {med_pos:+.3f} = " + f"{100*med_pos/CHOSEN['dF'] if CHOSEN['dF'] else float('nan'):.0f}% della cella scelta " + f"({CHOSEN['dF']:+.3f}); celle positive {fpos*100:.0f}% -> {'PASS' if c1 else 'FAIL'}") + print(f" (2) `weights_tilt_null`: gate_pass=False nella forma nuda, INERTE nella forma") + print(f" compensata -> non lo sostiene in nessuna delle due -> FAIL/vacuo") + print(f" (3) marginale ADDS su entrambe le celle + null di permutazione battuto a") + print(f" p<0.01 con la penale meccanica dentro (Β§6) -> PASS") + print(f" (4) taglia contro un modo di fallire in piu': EUR {eurday:+.4f}/giorno a " + f"${CAPITAL:.0f} -> {'PASS' if c4 else 'FAIL'} (soglia dichiarata: EUR 0,10/giorno)") + print(f" VERDETTO: {'CANDIDATO' if (c1 and c4) else 'LEAD / NESSUN CAMBIO'} β€” servono") + print(f" tutte e quattro; (1) e (4) falliscono, (2) non e' utilizzabile. Cio' che") + print(f" REGGE e va conservato e' il SEGNO: 0/72 celle positive sul controllo inverso.") + + print("\n" + "=" * L) + print(f" fatto in {time.time()-t0:.0f}s") + print("=" * L) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_bocpd.py b/scripts/research/r0822b_bocpd.py new file mode 100644 index 0000000..0673a12 --- /dev/null +++ b/scripts/research/r0822b_bocpd.py @@ -0,0 +1,957 @@ +#!/usr/bin/env python +"""r0822b_bocpd.py β€” IL FALSIFICATORE NOMINATO E MAI ESEGUITO del filone ADAPTIVE-HORIZON. + +CONTESTO (perche' questo file esiste) +------------------------------------- +Il 22/08 `r0822_adaptive_horizon.py` ha scartato il TSMOM a orizzonte adattivo con una diagnosi +netta: il "vincitore adattivo" aveva L_t **incollato al bordo il 100% del tempo** = un TSMOM a +orizzonte COSTANTE di 20 giorni travestito (corr 1.000, dSh 0.00 in 8/8 anni e 0/12 ancore); +isolando le 52 celle davvero adattive -> NEUTRAL, corr->TP01 0.90 = TP01 travestito. +Ma quell'agente ha dichiarato **cosa lo smentirebbe** e non ha potuto eseguirlo: + + "Un rilevatore il cui L_t si muove DAVVERO e batte il miglior lookback COSTANTE (non TP01) in + >=6/8 anni appaiati con corr->TP01 < 0.6. La mia famiglia copre 3 rilevatori a soglia/finestra + fissa β€” un run-length posterior bayesiano (BOCPD/PELT vero) o un L scelto per walk-forward sul + PnL realizzato non sono stati provati." + +Questo file esegue **quel** test. Oggi il filone e' chiuso su UNA famiglia (3 proxy a soglia +fissa), non sul MECCANISMO. Una seconda chiusura, con rilevatori veri, rende il "no" definitivo. + +PERCHE' BOCPD E' STRUTTURALMENTE DIVERSO DAI 3 PROXY DEL PRIMO TENTATIVO +----------------------------------------------------------------------- +I 3 proxy (CUSUM a soglia k, variance-ratio, Hurst) producevano una STATISTICA che veniva poi +MAPPATA linearmente dentro [Lmin, Lmax]: la mappatura e' arbitraria, e infatti la statistica +saturava contro il clip (bordo 100%). Un run-length posterior NON ha bisogno di mappatura: +la lunghezza di run **e' gia' un lookback**, nelle stesse unita' (barre). "Quanto indietro posso +guardare" = "da quanto tempo non cambia il regime" e' un'identita', non una scelta di scala. + * BOCPD (Adams & MacKay 2007): posterior online su r_t con hazard costante e verosimiglianza + Normale-InverseGamma coniugata (predittiva Student-t). Causale PER COSTRUZIONE: e' un filtro + in avanti, ogni P(r_t | x_{1:t}) usa solo x_{1:t}. + * OP/PELT causale: PELT e' un'accelerazione (pruning) dell'**optimal partitioning** + F(t) = min_{s e' un asse] + blend 2 (single L | triple L/3,L,2L β€” con L=90 il triple E' TP01) + timeframe 2 (1d | 12h) [>=12h: sotto, costi+overfit dominano, 19/06] + = 2*3*2*2*2*2 = **96 trial** + B) controllo L COSTANTE 12 L x 2 blend x 2 TF = **48 trial** + C) WF-PnL (L da walk-forward) 3 finestre x 2 blend x 2 TF = **12 trial** + D) taratura della SCALA 8 penalita' OP + 5 hazard BOCPD = **13** (sezione 1-pre) + ---------------------------------------------------------------- TOTALE DICHIARATO **169** +⚠️ (D) e' una scansione fatta guardando SOLO il numero di change-point rilevati β€” una proprieta' +del RILEVATORE β€” e mai uno Sharpe: nessuna di quelle 13 configurazioni produce un rendimento. Si +dichiara lo stesso, al rialzo, perche' ha ristretto la griglia di (A). Il suo esito e' gia' un +risultato: a penalita' OP >= 5 l'eta' resta incollata al tetto della finestra il 44% del tempo, +cioe' **l'artefatto del 22/08 riprodotto su un rilevatore vero** β€” e la griglia (A) usa solo la +regione in cui il rilevatore rileva davvero (0.5/1.0/2.0 = 18-40 change-point l'anno). +Costanti FISSATE A PRIORI (gradi di liberta' non cercati, dichiarati perche' esistono): +Lmin=20g, Lmax=200g (contengono 30..180 del canonico e sono gli stessi del primo tentativo); +BOCPD su rendimenti log STANDARDIZZATI da vol rolling causale 30g (il bersaglio e' il DRIFT, non +la vol); prior NIG mu0=0, kappa0=1, alpha0=1, beta0=1; troncamento del posterior a Lmax barre; +costo OP = mean-shift gaussiano su dati standardizzati, penalita' beta = k*log(t) causale. +Il deflated-Sharpe e' riportato a N=169 (mia famiglia), N=281 (mia + le 112 del primo tentativo: +**stessa domanda**, i trial si sommano) e N=449 (di SCREEN β€” lezione 22/08 del filone ORTHO: +su BTC/ETH direzionale il massimo atteso dal rumore su 168 trial e' Sharpe 1.572, SOPRA il +soffitto misurato ~1.3, quindi il conteggio di screen e' quello che decide). + +ORDINE DI ESECUZIONE (imposto dalla missione, ed e' l'ordine giusto) +-------------------------------------------------------------------- +La diagnostica che ha smascherato il primo tentativo va PER PRIMA: quanto si muove davvero L_t? +Se sta al bordo, l'artefatto e' riprodotto e il filone si chiude di nuovo senza guardare i +rendimenti. Solo se L_t si muove ha senso misurare se il movimento PAGA. + +Runtime atteso ~6-10 min su 2 core. Nessun file toccato fuori da questo. +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy.special import gammaln +from scipy.stats import norm + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT)) + +import altlib as A # noqa: E402 +from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402 + +ASSETS = A.CERTIFIED +HOLDOUT = A.HOLDOUT + +# ---- costanti FISSATE A PRIORI (dichiarate sopra, non cercate) ------------------------- +LMIN_D, LMAX_D = 20, 200 # banda dell'orizzonte, IDENTICA al primo tentativo +FIXED_H = (30, 90, 180) # == (L/3, L, 2L) con L=90 -> TP01 canonico +TGT_VOL, LEV, VOLWIN = 0.20, 2.0, 30 +NIG_MU0, NIG_KAP0, NIG_AL0, NIG_BE0 = 0.0, 1.0, 1.0, 1.0 +STD_WIN_D = 30 # finestra della standardizzazione causale dei rendimenti + +TFS = ("1d", "12h") +DETECTORS = ("bocpd", "op") +PARAMS = {"bocpd": (50, 100, 250), "op": (0.5, 1.0, 2.0)} +# TARATURA DELLA SCALA (sezione 1-pre): scansione ESPLORATIVA fatta guardando SOLO il numero di +# change-point rilevati β€” una proprieta' del RILEVATORE β€” e mai lo Sharpe. Un rilevatore che non +# rileva nulla non e' un rilevatore: a pen_k>=5 l'eta' resta incollata al tetto della finestra +# (44% del tempo) = ESATTAMENTE l'artefatto del 22/08, riprodotto qui su un rilevatore vero. +CAL_PEN = (0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0) +CAL_LAM = (25, 50, 100, 250, 500) +STATS = {"bocpd": ("mean", "map"), "op": ("age", "segmed")} +MAPPINGS = ("direct", "inverse") +BLENDS = ("single", "triple") + +CONST_L = (5, 7, 10, 15, 20, 30, 45, 60, 90, 120, 150, 200) +WF_WIN_D = (180, 365, 730) + +N_DECLARED_A = len(DETECTORS) * 3 * 2 * len(MAPPINGS) * len(BLENDS) * len(TFS) # 96 +N_DECLARED_B = len(CONST_L) * len(BLENDS) * len(TFS) # 48 +N_DECLARED_C = len(WF_WIN_D) * len(BLENDS) * len(TFS) # 12 +N_DECLARED_CAL = len(CAL_PEN) + len(CAL_LAM) # 13 (taratura) +N_DECLARED = N_DECLARED_A + N_DECLARED_B + N_DECLARED_C + N_DECLARED_CAL # 169 +N_PRIOR_FAMILY = 112 # trial dichiarati da r0822_adaptive_horizon (stessa domanda) +N_SCREEN = 168 # pool di screen direzionale BTC/ETH dell'ondata (filone 12) + + +# ======================================================================================= +# RILEVATORI VERI +# ======================================================================================= +def _logret(c: np.ndarray) -> np.ndarray: + r = np.zeros(len(c)) + r[1:] = np.log(c[1:] / c[:-1]) + return r + + +def _std_returns(c: np.ndarray, bpd: int) -> np.ndarray: + """Rendimenti log standardizzati da vol rolling CAUSALE (min_periods -> warmup a 0). + Il bersaglio del rilevatore e' il cambio di DRIFT: senza standardizzare, un cambio di + regime di VOLATILITA' dominerebbe la verosimiglianza e il rilevatore misurerebbe la vol.""" + lr = _logret(c) + w = max(5, STD_WIN_D * bpd) + sig = pd.Series(lr).rolling(w, min_periods=max(5, w // 4)).std().values + y = np.where((sig > 0) & np.isfinite(sig), lr / np.where(sig > 0, sig, 1.0), 0.0) + return np.nan_to_num(np.clip(y, -8.0, 8.0)) + + +def _student_logpdf(x: float, mu: np.ndarray, kap: np.ndarray, + al: np.ndarray, be: np.ndarray) -> np.ndarray: + """Predittiva Student-t della Normale-InverseGamma coniugata (Murphy 2007, eq. 99-100): + x | mu,kappa,alpha,beta ~ t_{2 alpha}(mu, beta(kappa+1)/(alpha kappa)).""" + nu = 2.0 * al + s2 = be * (kap + 1.0) / (al * kap) + z = (x - mu) ** 2 / (nu * s2) + return (gammaln(0.5 * (nu + 1.0)) - gammaln(0.5 * nu) + - 0.5 * np.log(nu * np.pi * s2) - 0.5 * (nu + 1.0) * np.log1p(z)) + + +def _bocpd(y: np.ndarray, lam_bars: float, rmax: int) -> tuple[np.ndarray, np.ndarray]: + """BOCPD (Adams & MacKay 2007) con hazard COSTANTE H = 1/lam_bars e verosimiglianza NIG. + Ritorna (E[r_t], argmax_r P(r_t)) in BARRE. Filtro in avanti: ogni valore usa solo il + passato -> causale per costruzione, e stabile su prefisso (nessuna statistica full-sample). + Il posterior e' troncato a rmax barre e rinormalizzato (il troncamento e' un limite di + memoria, NON un clip sul valore: se la massa si accumula a rmax lo si vede in diagnostica).""" + n = len(y) + haz = 1.0 / max(2.0, float(lam_bars)) + R = np.array([1.0]) + mu = np.array([NIG_MU0]); kap = np.array([NIG_KAP0]) + al = np.array([NIG_AL0]); be = np.array([NIG_BE0]) + rl = np.arange(1) + mean_rl = np.zeros(n); map_rl = np.zeros(n) + for t in range(n): + x = float(y[t]) + lp = _student_logpdf(x, mu, kap, al, be) + lp -= lp.max() + pred = np.exp(lp) + gr = R * pred * (1.0 - haz) + cp = float(np.sum(R * pred * haz)) + m_new = min(len(R) + 1, rmax + 1) + newR = np.empty(m_new) + newR[0] = cp + newR[1:] = gr[:m_new - 1] + s = newR.sum() + if not np.isfinite(s) or s <= 0: + newR = np.zeros(m_new); newR[0] = 1.0; s = 1.0 + newR /= s + # aggiornamento delle statistiche sufficienti (run r -> run r+1), prior in testa + mu_f = np.concatenate(([NIG_MU0], (kap * mu + x) / (kap + 1.0)))[:m_new] + kap_f = np.concatenate(([NIG_KAP0], kap + 1.0))[:m_new] + al_f = np.concatenate(([NIG_AL0], al + 0.5))[:m_new] + be_f = np.concatenate(([NIG_BE0], be + kap * (x - mu) ** 2 / (2.0 * (kap + 1.0))))[:m_new] + R, mu, kap, al, be = newR, mu_f, kap_f, al_f, be_f + if len(rl) != m_new: + rl = np.arange(m_new, dtype=float) + mean_rl[t] = float(np.dot(R, rl)) + map_rl[t] = float(rl[int(np.argmax(R))]) + return mean_rl, map_rl + + +def _op_causal(y: np.ndarray, pen_k: float, smax: int) -> tuple[np.ndarray, np.ndarray]: + """OPTIMAL PARTITIONING **online** (l'obiettivo esatto che PELT accelera con il pruning): + F(t) = min_{s in [t-smax, t-1]} [ F(s) + C(y[s:t]) + beta(t) ] + con C = costo gaussiano di mean-shift su dati standardizzati e beta(t) = pen_k*log(t). + ⚠️ Il termine sum(y^2) del costo gaussiano e' INVARIANTE alla partizione (ogni punto sta in + esattamente un segmento) -> si omette dall'argmin. C(s:t) = -(S[t]-S[s])^2/(t-s). + Ritorna (eta' dall'ultimo change-point, mediana delle lunghezze dei segmenti CONCLUSI), + entrambe in barre. Per ogni t si usa SOLO y[0:t]: e' la differenza fra questo e un + `ruptures.Pelt().fit_predict(y)` retrospettivo, che guarda tutto il campione.""" + n = len(y) + S = np.concatenate(([0.0], np.cumsum(y))) + F = np.full(n + 1, np.inf) + F[0] = 0.0 + prev = np.zeros(n + 1, dtype=int) + age = np.zeros(n); segmed = np.zeros(n) + for t in range(1, n + 1): + a = max(0, t - smax) + s_idx = np.arange(a, t) + seg = (S[t] - S[a:t]) + ln = (t - s_idx).astype(float) + cand = F[a:t] - seg * seg / ln + j = int(np.argmin(cand)) + beta = pen_k * np.log(max(2.0, float(t))) + F[t] = cand[j] + beta + prev[t] = int(s_idx[j]) + # backtracking della partizione VALIDA A t (cap a 40 segmenti: ci serve la mediana) + last = prev[t] + age[t - 1] = t - last + lens = [] + u = last + while u > 0 and len(lens) < 40: + p = prev[u] + lens.append(u - p) + u = p + segmed[t - 1] = float(np.median(lens)) if lens else float(t) + return age, segmed + + +_STAT_CACHE: dict = {} + + +def _stat_key(df: pd.DataFrame, det: str, par) -> tuple: + """Chiave chiusa sul CONTENUTO del frame (lunghezza + primo/ultimo timestamp): un PREFISSO + non deve prendere la cache del campione pieno, altrimenti `causality_ok` verrebbe ingannata + dalla cache invece che dal codice.""" + ts = df["timestamp"].values + return (det, par, len(df), int(ts[0]), int(ts[-1])) + + +def detector_stats(df: pd.DataFrame, det: str, par) -> dict: + key = _stat_key(df, det, par) + if key in _STAT_CACHE: + return _STAT_CACHE[key] + bpd = A.bars_per_day(df) + c = df["close"].values.astype(float) + y = _std_returns(c, bpd) + rmax = max(4, LMAX_D * bpd) + if det == "bocpd": + m, mp = _bocpd(y, float(par) * bpd, rmax) + out = {"mean": m, "map": mp} + elif det == "op": + ag, sm = _op_causal(y, float(par), rmax) + out = {"age": ag, "segmed": sm} + else: + raise ValueError(det) + _STAT_CACHE[key] = out + return out + + +def lookback_bars(df: pd.DataFrame, det: str, par, stat: str, mapping: str) -> np.ndarray: + """L_t in BARRE dentro [Lmin, Lmax] giorni. `direct` = la lunghezza di run E' il lookback + (dopo un change-point si puo' guardare indietro solo fin la'); `inverse` e' lo specchio, + dichiarato come asse perche' il verso non e' ovvio a priori.""" + bpd = A.bars_per_day(df) + lo, hi = max(2, LMIN_D * bpd), max(3, LMAX_D * bpd) + v = detector_stats(df, det, par)[stat] + base = np.clip(np.nan_to_num(v, nan=float(lo)), lo, hi) + L = base if mapping == "direct" else (lo + hi - base) + return np.clip(np.round(L), 2, hi).astype(int) + + +# ======================================================================================= +# DIREZIONE TSMOM (orizzonte costante o variabile) + vol-target β€” IDENTICA al primo tentativo +# ======================================================================================= +def _sign_at(c: np.ndarray, h: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + n = len(c) + i = np.arange(n) + j = i - h + ok = j >= 0 + s = np.zeros(n) + jj = np.where(ok, j, 0) + s[ok] = np.sign(c[i[ok]] / c[jj[ok]] - 1.0) + return s, ok + + +def _dir_from_L(c: np.ndarray, L: np.ndarray, blend: str) -> np.ndarray: + hs = [L] if blend == "single" else [ + np.maximum(2, np.round(L / 3.0).astype(int)), L, np.round(2.0 * L).astype(int)] + acc = np.zeros(len(c)); cnt = np.zeros(len(c)) + for h in hs: + s, ok = _sign_at(c, np.asarray(h, int)) + acc[ok] += s[ok]; cnt[ok] += 1 + out = np.zeros(len(c)); nz = cnt > 0 + out[nz] = acc[nz] / cnt[nz] + return out + + +def fixed_target(df: pd.DataFrame) -> np.ndarray: + """BASELINE: TSMOM fisso 30/90/180, long-flat, vol-target 20%, leva 2x = TP01 canonico.""" + c = df["close"].values.astype(float) + bpd = A.bars_per_day(df) + acc = np.zeros(len(c)); cnt = np.zeros(len(c)) + for hd in FIXED_H: + s, ok = _sign_at(c, np.full(len(c), hd * bpd, int)) + acc[ok] += s[ok]; cnt[ok] += 1 + d = np.zeros(len(c)); nz = cnt > 0 + d[nz] = acc[nz] / cnt[nz] + return A.vol_target(np.clip(d, 0, None), df, TGT_VOL, VOLWIN, LEV) + + +def const_factory(tf: str, L: int, blend: str): + def fn(df: pd.DataFrame) -> np.ndarray: + c = df["close"].values.astype(float) + bpd = A.bars_per_day(df) + Lb = np.full(len(c), max(2, int(round(L * bpd))), int) + return A.vol_target(np.clip(_dir_from_L(c, Lb, blend), 0, None), df, TGT_VOL, VOLWIN, LEV) + fn.__name__ = f"const_L{L}_{blend}" + return fn + + +def factory(tf: str, det: str, par, stat: str, mapping: str, blend: str): + def fn(df: pd.DataFrame) -> np.ndarray: + c = df["close"].values.astype(float) + L = lookback_bars(df, det, par, stat, mapping) + return A.vol_target(np.clip(_dir_from_L(c, L, blend), 0, None), df, TGT_VOL, VOLWIN, LEV) + fn.__name__ = f"adapt_{det}{par}_{stat}_{mapping}_{blend}" + return fn + + +_WF_CACHE: dict = {} + + +def wf_picks(df: pd.DataFrame, win_d: int, blend: str): + """(C) L scelto per WALK-FORWARD sul PnL NETTO realizzato: a ogni barra si prende l'L che, + fra i candidati COSTANTI, ha lo Sharpe netto migliore nella finestra all'indietro `win_d`. + Nessun modello di regime: e' il "cosa avrebbe funzionato finora". + Causale: il PnL della barra i si conosce solo a fine barra i, quindi per decidere a i si usa + la finestra che finisce a i-1; `eval_weights` shifta poi la posizione di un'altra barra.""" + cands = [L for L in CONST_L if LMIN_D <= L <= LMAX_D] + key = (win_d, blend) + _stat_key(df, "wf", 0)[2:] + if key in _WF_CACHE: + return _WF_CACHE[key] + c = df["close"].values.astype(float) + bpd = A.bars_per_day(df) + n = len(c) + r = A.simple_returns(c) + w = max(20, win_d * bpd) + tgts, sh = [], [] + for L in cands: + Lb = np.full(n, max(2, int(round(L * bpd))), int) + t_ = A.vol_target(np.clip(_dir_from_L(c, Lb, blend), 0, None), df, TGT_VOL, VOLWIN, LEV) + pos = np.zeros(n); pos[1:] = t_[:-1] + net = pos * r - A.FEE_SIDE * np.abs(np.diff(pos, prepend=0.0)) + ss = pd.Series(net) + mu = ss.rolling(w, min_periods=w // 2).mean() + sd = ss.rolling(w, min_periods=w // 2).std() + with np.errstate(invalid="ignore", divide="ignore"): + sr = (mu / sd).values + sr = np.concatenate(([np.nan], sr[:-1])) # PnL noto solo a fine barra + tgts.append(t_); sh.append(np.where(np.isfinite(sr), sr, -np.inf)) + SH = np.vstack(sh); TG = np.vstack(tgts) + idx = np.argmax(SH, axis=0) + idx[~np.isfinite(SH).any(axis=0)] = cands.index(90) # warmup -> il canonico, non il migliore + out = (np.asarray([cands[i] for i in idx]), TG[idx, np.arange(n)], cands) + _WF_CACHE[key] = out + return out + + +def wf_factory(tf: str, win_d: int, blend: str): + def fn(df: pd.DataFrame) -> np.ndarray: + return wf_picks(df, win_d, blend)[1] + fn.__name__ = f"wfpnl_{win_d}_{blend}" + return fn + + +GRID_A = [dict(det=d, par=p, stat=s, mapping=m, blend=b) + for d in DETECTORS for p in PARAMS[d] for s in STATS[d] + for m in MAPPINGS for b in BLENDS] +GRID_B = [dict(L=L, blend=b) for L in CONST_L for b in BLENDS] +GRID_C = [dict(win_d=w, blend=b) for w in WF_WIN_D for b in BLENDS] + + +# ======================================================================================= +# UTILITA' +# ======================================================================================= +def _dd(s: pd.Series) -> float: + eq = np.cumprod(1.0 + np.asarray(s.dropna().values, float)) + pk = np.maximum.accumulate(eq) + return float(np.max((pk - eq) / pk)) if len(eq) else 0.0 + + +def _cagr(s: pd.Series) -> float: + s = s.dropna() + if len(s) < 5: + return float("nan") + eq = float(np.prod(1.0 + s.values)) + yrs = (s.index[-1] - s.index[0]).total_seconds() / (86400 * 365.25) + return eq ** (1 / max(yrs, 1e-6)) - 1.0 if eq > 0 else -1.0 + + +def resample_offset(df_1h: pd.DataFrame, hours: int, off: int) -> pd.DataFrame: + """Stessa convenzione di trend_portfolio.resample_tf, ancora spostata di `off` ore. + NB `offset=`, non `origin=` (pandas 2.x ignora origin in silenzio β€” lezione 02/07).""" + g = df_1h.copy() + idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True) + idx.name = "dt" + g.index = idx + out = g.resample(f"{hours}h", offset=pd.Timedelta(hours=off), label="left", closed="left").agg( + {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}) + out = out.dropna(subset=["open"]) + out["datetime"] = out.index + epoch = pd.Timestamp("1970-01-01", tz="UTC") + out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64") + return out.reset_index(drop=True)[ + ["timestamp", "open", "high", "low", "close", "volume", "datetime"]] + + +def paired_by_year(cand: pd.Series, base: pd.Series) -> list[dict]: + J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna() + rows = [] + for y, g in J.groupby(J.index.year): + if len(g) < 40: + continue + rows.append(dict(year=int(y), n=len(g), + sh_cand=round(A._sh(g["C"]), 2), sh_base=round(A._sh(g["B"]), 2), + d_sh=round(A._sh(g["C"]) - A._sh(g["B"]), 2))) + return rows + + +def delever_null(cand: pd.Series, base: pd.Series) -> dict: + """Esiste k<1 che da' al BASELINE lo stesso maxDD del candidato con Sharpe >= ? + Se si', il candidato non ha comprato protezione: ha solo tolto leva.""" + J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna() + dd_c, dd_b = _dd(J["C"]), _dd(J["B"]) + if dd_c >= dd_b or dd_b <= 0: + return dict(applicabile=False, dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4), + nota="il candidato NON riduce il maxDD: il null non si applica") + lo, hi = 0.0, 1.0 + for _ in range(60): + k = 0.5 * (lo + hi) + if _dd(k * J["B"]) < dd_c: + lo = k + else: + hi = k + k = 0.5 * (lo + hi) + return dict(applicabile=True, k=round(k, 4), dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4), + sharpe_cand=round(A._sh(J["C"]), 3), + sharpe_base_delev=round(A._sh(k * J["B"]), 3), + superato=bool(A._sh(J["C"]) > A._sh(k * J["B"]))) + + +_ANCHOR_CACHE: dict = {} + + +def series_at_anchor(fn, name: str, hours: int, off: int) -> pd.Series: + key = (name, hours, off) + if key not in _ANCHOR_CACHE: + ser = {} + for a in ASSETS: + dfo = resample_offset(A.get(a, "1h"), hours, off) + ev = A.eval_weights(dfo, A._call_target(fn, dfo, a)) + ser[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + _ANCHOR_CACHE[key] = A._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) + return _ANCHOR_CACHE[key] + + +def adaptivity(tf: str, det: str, par, stat: str, mapping: str) -> dict: + """LA DIAGNOSTICA CHE HA SMASCHERATO IL PRIMO TENTATIVO β€” quanto si MUOVE davvero L_t. + Criterio (identico a quello del 22/08, per confrontabilita'): sta a un bordo <50% del tempo + E l'IQR e' >= 10 giorni. In piu': |dL|/giorno e numero di RESET (cadute di L > 5 giorni), + che sono la firma di un vero rilevatore di change-point.""" + fl, fh, iq, md, dl, nrs, sat = [], [], [], [], [], [], [] + for a in ASSETS: + df = A.get(a, tf) + bpd = A.bars_per_day(df) + L = lookback_bars(df, det, par, stat, mapping) / bpd + raw = np.nan_to_num(detector_stats(df, det, par)[stat], nan=0.0) / bpd + fl.append(float(np.mean(L <= LMIN_D + 1))) + fh.append(float(np.mean(L >= LMAX_D - 1))) + iq.append(float(np.percentile(L, 75) - np.percentile(L, 25))) + md.append(float(np.median(L))) + dl.append(float(np.median(np.abs(np.diff(L))))) + d = np.diff(L) + nrs.append(float(np.sum(d < -5.0) / (len(L) / (365.25)))) # reset all'anno + sat.append(float(np.mean(raw >= LMAX_D - 1))) # saturazione del GREZZO + return dict(frac_lo=float(np.mean(fl)), frac_hi=float(np.mean(fh)), + iqr_days=float(np.mean(iq)), med_days=float(np.mean(md)), + dL_med=float(np.mean(dl)), reset_yr=float(np.mean(nrs)), + sat_raw=float(np.mean(sat)), + adattiva=bool(np.mean(fl) < 0.5 and np.mean(fh) < 0.5 and np.mean(iq) >= 10.0)) + + +def hr(t: str = "") -> None: + print("\n" + "=" * 86) + if t: + print(t) + print("=" * 86) + + +def dsr_given_sr0(daily: pd.Series, sr_ann: float, sr0_ann: float, + dpy: float = 365.25) -> float: + """Deflated-Sharpe con il massimo-atteso-dal-rumore IMPOSTO dall'esterno. + Serve perche' `A.deflated_sharpe` stima sr0 dalla VARIANZA degli Sharpe della griglia: su + una famiglia OMOGENEA (qui sono tutte varianti di TSMOM, quindi molto correlate) quella + varianza e' piccola, sr0 esce basso e il DSR esce alto **per costruzione**. Il conteggio che + decide, sulla ricerca direzionale BTC/ETH, e' quello di SCREEN: il filone ORTHO ha misurato + che su 168 trial il massimo atteso dal puro rumore e' Sharpe 1.572.""" + r = np.asarray(pd.Series(daily).dropna().values, float) + T = len(r) + sr, sr0 = sr_ann / np.sqrt(dpy), sr0_ann / np.sqrt(dpy) + sk = float(pd.Series(r).skew()) + ku = float(pd.Series(r).kurt()) + 3.0 + den = np.sqrt(max(1e-9, 1 - sk * sr + (ku - 1) / 4.0 * sr ** 2)) + return float(norm.cdf((sr - sr0) * np.sqrt(T - 1) / den)) + + +def dsr_line(sr: float, allsr: list, daily: pd.Series, label: str, n_decl: int) -> str: + pad = list(allsr) + [float(np.mean(allsr))] * max(0, n_decl - len(allsr)) + d, s0 = A.deflated_sharpe(sr, pad, daily) + return (f" N={n_decl:>4} ({label:<24}) DSR={d:.3f} {'PASS' if d >= 0.95 else 'FAIL'}" + f" max atteso dal rumore {s0:+.3f}") + + +# ======================================================================================= +def main() -> None: + t0 = time.time() + print(__doc__.split("Runtime atteso")[0]) + + # -- 0. SANITY ----------------------------------------------------------------------- + hr("0. SANITY β€” il baseline 'fisso' e' TP01 canonico (se non lo e', ogni delta e' inventato)") + tp = TrendPortfolio(**CANONICAL) + worst = 0.0 + for a in ASSETS: + df = A.get(a, "1d") + worst = max(worst, float(np.max(np.abs(fixed_target(df) - tp.target_series(df))))) + base_1d = A.candidate_daily(fixed_target, tf="1d") + tp01 = A.tp01_baseline_daily() + JJ = pd.concat({"a": base_1d, "b": tp01}, axis=1, join="inner").dropna() + print(f" max|target_mio - TrendPortfolio.target_series| (1d, BTC+ETH) = {worst:.3e}") + print(f" max|serie_giornaliera_mia - A.tp01_baseline_daily()| = " + f"{float(np.max(np.abs(JJ['a'] - JJ['b']))):.3e} (n={len(JJ)})") + assert worst < 1e-12, "il baseline NON riproduce TP01: fermarsi qui" + + # -- 0-bis. IL RILEVATORE E' CAUSALE? (troncamento MANUALE, prima dei gate) ----------- + hr("0-bis. TRONCAMENTO MANUALE β€” la trappola del change-point RETROSPETTIVO") + print(" Un `Pelt().fit_predict(serie_intera)` restituisce la segmentazione dell'INTERO") + print(" campione: al tempo t 'saprebbe' dei cambi futuri. Qui BOCPD e OP sono ricorsioni in") + print(" avanti. Prova diretta: ricalcolo su un PREFISSO e confronto la coda con il pieno.") + for det, par in (("bocpd", 100), ("op", 5.0)): + for stat in STATS[det]: + wmax = 0.0 + for a in ASSETS: + df = A.get(a, "1d") + full = lookback_bars(df, det, par, stat, "direct") + for frac in (0.70, 0.85): + cut = int(len(df) * frac) + sub = df.iloc[:cut].reset_index(drop=True) + pre = lookback_bars(sub, det, par, stat, "direct") + wmax = max(wmax, float(np.max(np.abs(pre[-100:] - full[cut - 100:cut])))) + print(f" {det:<6} {str(par):>5} {stat:<7}: max|L(prefisso)-L(pieno)| sulla coda " + f"= {wmax:.3e} barre -> {'CAUSALE' if wmax < 1e-9 else 'SOSPETTO'}") + + # -- 1-pre. TARATURA DELLA SCALA β€” guardando il RILEVATORE, mai lo Sharpe ----------- + hr("1-pre. TARATURA DELLA SCALA DEI PARAMETRI (guardando SOLO quanti change-point rileva)") + print(" Un rilevatore che non rileva nulla non e' un rilevatore. Questa scansione guarda il") + print(" NUMERO di change-point e la distribuzione di L_t β€” mai un rendimento. Dichiarata") + print(" come 13 trial in piu' (al rialzo), benche' nessuno produca uno Sharpe.") + dfc = A.get("BTC", "1d") + yc = _std_returns(dfc["close"].values.astype(float), 1) + print(f"\n OP (BTC 1d) {'pen_k':>6} {'cp/anno':>8} {'age med':>8} {'IQR':>7} {'@tetto':>8}") + for k in CAL_PEN: + ag, _ = _op_causal(yc, k, LMAX_D) + ncp = int(np.sum(np.diff(ag) < 0)) + flag = " <= USATO" if k in PARAMS["op"] else (" <= degenere (artefatto 22/08)" + if np.mean(ag >= LMAX_D - 1) > 0.30 else "") + print(f" {k:>6} {ncp/(len(yc)/365.25):>8.1f} {np.median(ag):>8.1f} " + f"{np.percentile(ag,75)-np.percentile(ag,25):>7.1f} " + f"{np.mean(ag>=LMAX_D-1)*100:>7.1f}%{flag}") + print(f"\n BOCPD (BTC 1d) {'lambda':>6} {'E[r] med':>9} {'IQR':>7} {'@tetto':>8} " + f"{'MAP med':>8} {'@tetto':>8}") + for lam in CAL_LAM: + mn, mp = _bocpd(yc, float(lam), LMAX_D) + flag = " <= USATO" if lam in PARAMS["bocpd"] else "" + print(f" {lam:>6} {np.median(mn):>9.1f} " + f"{np.percentile(mn,75)-np.percentile(mn,25):>7.1f} " + f"{np.mean(mn>=LMAX_D-1)*100:>7.1f}% {np.median(mp):>8.1f} " + f"{np.mean(mp>=LMAX_D-1)*100:>7.1f}%{flag}") + + # -- 1. LA DIAGNOSTICA DI MECCANISMO, PER PRIMA -------------------------------------- + hr("1. L_t SI MUOVE DAVVERO? (se sta al bordo l'artefatto e' riprodotto: si chiude e basta)") + print(" criterio (identico al 22/08, per confrontabilita'): a un bordo <50% del tempo E") + print(" IQR >= 10 giorni. `reset/anno` = cadute di L > 5 giorni = firma di un change-point.") + print(f"\n {'TF':>4} {'det':>6} {'par':>5} {'stat':>7} {'map':<7} {'bordo-lo':>9} " + f"{'bordo-hi':>9} {'IQR(g)':>7} {'med(g)':>7} {'|dL|(g)':>8} {'reset/a':>8} " + f"{'sat.grezzo':>10} esito") + diag: dict = {} + for tf in TFS: + for d in DETECTORS: + for p in PARAMS[d]: + for st in STATS[d]: + for mp in MAPPINGS: + v = adaptivity(tf, d, p, st, mp) + diag[(tf, d, p, st, mp)] = v + print(f" {tf:>4} {d:>6} {str(p):>5} {st:>7} {mp:<7} " + f"{v['frac_lo']*100:8.1f}% {v['frac_hi']*100:8.1f}% " + f"{v['iqr_days']:7.1f} {v['med_days']:7.1f} {v['dL_med']:8.2f} " + f"{v['reset_yr']:8.1f} {v['sat_raw']*100:9.1f}% " + f"{'ADATTIVA' if v['adattiva'] else 'costante-travestito'}") + n_ad = sum(1 for v in diag.values() if v["adattiva"]) + print(f"\n configurazioni di rilevatore DAVVERO adattive: {n_ad}/{len(diag)}") + if n_ad == 0: + hr("STOP β€” ARTEFATTO RIPRODOTTO") + print(" Nessun rilevatore muove L_t: ogni 'vincitore' sarebbe un orizzonte COSTANTE") + print(" travestito, esattamente come il 22/08. Il filone si chiude per la seconda volta") + print(" senza bisogno di guardare i rendimenti. FINE.") + return + + # -- 2. GRIGLIA (A): cella scelta IN-SAMPLE + deflated-Sharpe ------------------------- + hr(f"2. study_family_honest (A) β€” {len(GRID_A)} celle x {len(TFS)} TF = " + f"{len(GRID_A)*len(TFS)} trial") + rep = A.study_family_honest("BOCPD-H", factory, GRID_A, TFS) + ch = rep["chosen"] + print(f" celle valutate : {rep['n_cells']}") + print(f" cella scelta IN-SAMPLE : tf={ch['tf']} {ch['params']}") + print(f" Sharpe IS {ch['insample_sharpe']} FULL {ch['full_sharpe']}") + print(f" deflated-Sharpe (N={rep['n_cells']}): {rep['deflated_sharpe']} " + f"(max atteso sotto il null {rep['expected_null_max']}) PASS={rep['dsr_pass']}") + print(f" earns_slot_marginal : {rep['earns_slot_marginal']}") + print(f" EARNS_SLOT_HONEST : {rep['earns_slot_honest']}") + print("\n top-10 IN-SAMPLE (come si sceglie onestamente):") + for r in rep["rows"][:10]: + ad = diag[(r["tf"], r["params"]["det"], r["params"]["par"], + r["params"]["stat"], r["params"]["mapping"])]["adattiva"] + print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} " + f"{r['params']} {'[adattiva]' if ad else '[costante-travestito]'}") + + fn_ch = factory(tf=ch["tf"], **ch["params"]) + d_ch = A.candidate_daily(fn_ch, tf=ch["tf"]) + + # -- 3. CONTROLLO (B): il MIGLIOR ORIZZONTE COSTANTE --------------------------------- + hr(f"3. CONTROLLO L COSTANTE (B) β€” {len(GRID_B)} celle x {len(TFS)} TF = " + f"{len(GRID_B)*len(TFS)} trial [IL NULL GIUSTO, non TP01]") + csel = A.select_cell_insample(const_factory, GRID_B, TFS) + cch = csel["chosen"] + print(f" miglior COSTANTE scelto in-sample: tf={cch['tf']} {cch['params']} " + f"IS {cch['insample_sharpe']} FULL {cch['full_sharpe']}") + print(" top-8 costanti in-sample (L=90 triple 1d == TP01 canonico):") + for r in csel["rows"][:8]: + tag = " <== TP01" if (r["params"]["L"] == 90 and r["params"]["blend"] == "triple" + and r["tf"] == "1d") else "" + print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} " + f"L={r['params']['L']:>3}g {r['params']['blend']}{tag}") + fn_const = const_factory(tf=cch["tf"], **cch["params"]) + d_const = A.candidate_daily(fn_const, tf=cch["tf"]) + + # -- 4. WF-PnL (C) -------------------------------------------------------------------- + hr(f"4. WALK-FORWARD SUL PnL REALIZZATO (C) β€” {len(GRID_C)} celle x {len(TFS)} TF = " + f"{len(GRID_C)*len(TFS)} trial") + wsel = A.select_cell_insample(wf_factory, GRID_C, TFS) + wch = wsel["chosen"] + print(f" miglior WF scelto in-sample: tf={wch['tf']} {wch['params']} " + f"IS {wch['insample_sharpe']} FULL {wch['full_sharpe']}") + for r in wsel["rows"]: + print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} " + f"{r['params']}") + fn_wf = wf_factory(tf=wch["tf"], **wch["params"]) + d_wf = A.candidate_daily(fn_wf, tf=wch["tf"]) + # quanto si muove L nella variante WF? (stesso metro della sezione 1) + for a in ASSETS: + df = A.get(a, wch["tf"]); bpd = A.bars_per_day(df) + pick, _, cands = wf_picks(df, **wch["params"]) + print(f" {a}: L_WF giorni mediana {np.median(pick):.0f} IQR " + f"{np.percentile(pick,75)-np.percentile(pick,25):.0f} " + f"bordo basso {np.mean(pick<=min(cands))*100:.1f}% " + f"bordo alto {np.mean(pick>=max(cands))*100:.1f}% " + f"cambi/anno {np.sum(np.diff(pick)!=0)/(len(pick)/(bpd*365.25)):.0f}") + + # -- 5. IL CONFRONTO CHE DECIDE ------------------------------------------------------ + hr("5. ADATTIVO vs MIGLIOR COSTANTE (entrambi scelti IN-SAMPLE) β€” e vs TP01") + base_ch = A.candidate_daily(fixed_target, tf=ch["tf"]) + for nome, ser in (("BOCPD/OP (A)", d_ch), ("WF-PnL (C)", d_wf), + ("miglior COSTANTE (B)", d_const), ("TP01 30/90/180", base_1d)): + h = ser[ser.index >= HOLDOUT] + print(f" {nome:<22} FULL Sh {A._sh(ser):+.3f} HOLD Sh {A._sh(h):+.3f} " + f"maxDD {_dd(ser)*100:5.2f}% CAGR {_cagr(ser)*100:6.2f}% " + f"corr->TP01 {pd.concat({'a':ser,'b':tp01},axis=1,join='inner').dropna().corr().iloc[0,1]:+.3f}") + + for lab, cand, ref, refname in (("A vs B", d_ch, d_const, "miglior costante"), + ("C vs B", d_wf, d_const, "miglior costante"), + ("A vs TP01", d_ch, base_1d, "TP01")): + J = pd.concat({"C": cand, "B": ref}, axis=1, join="inner").dropna() + JH = J[J.index >= HOLDOUT] + rows = paired_by_year(cand, ref) + ds = [r["d_sh"] for r in rows] + print(f"\n --- {lab} ({refname}) --- corr {J['C'].corr(J['B']):+.3f}") + print(f" dSh FULL {A._sh(J['C'])-A._sh(J['B']):+.3f} " + f"dSh HOLD {A._sh(JH['C'])-A._sh(JH['B']):+.3f}") + print(" differenza APPAIATA per anno:") + print(" " + " ".join(f"{r['year']}:{r['d_sh']:+.2f}" for r in rows)) + print(f" -> anni con dSh>0: {sum(1 for x in ds if x > 0)}/{len(ds)} " + f"mediana {np.median(ds):+.2f} media {np.mean(ds):+.2f} " + f"[criterio del falsificatore: >=6/8 anni]") + + # decomposizione: quanto del vantaggio su TP01 e' ADATTIVITA' e quanto e' "L piu' corto"? + JT = pd.concat({"C": d_ch, "K": d_const, "B": base_1d}, axis=1, join="inner").dropna() + v_tp = A._sh(JT["C"]) - A._sh(JT["B"]) + v_ct = A._sh(JT["K"]) - A._sh(JT["B"]) + print(f"\n DECOMPOSIZIONE del vantaggio dell'adattivo su TP01 (FULL, finestra comune):") + print(f" adattivo - TP01 = {v_tp:+.3f}") + print(f" miglior COSTANTE - TP01 = {v_ct:+.3f} <- quota spiegata da 'un L piu' corto'") + print(f" adattivo - miglior COSTANTE= {v_tp - v_ct:+.3f} <- quota spiegata dall'ADATTARE") + print(f" => l'adattivita' spiega il {100*(v_tp-v_ct)/v_tp if abs(v_tp)>1e-9 else float('nan'):+.0f}% " + f"del vantaggio su TP01") + + # la superficie di L costante e' una CURVA o un CRINALE DI RUMORE? (replica del 22/08) + rows12 = {r["params"]["L"]: r["insample_sharpe"] for r in csel["rows"] + if r["tf"] == cch["tf"] and r["params"]["blend"] == cch["params"]["blend"]} + Ls = sorted(rows12) + adj = float(np.median(np.abs(np.diff([rows12[L] for L in Ls])))) + print(f"\n RISOLUZIONE della griglia costante ({cch['tf']}, {cch['params']['blend']}): salto IS " + f"mediano fra L ADIACENTI {adj:+.3f}") + print(f" contro il vantaggio del miglior costante su TP01 ({v_ct:+.3f}) -> rapporto " + f"{adj/max(1e-9, abs(v_ct)):.2f} [replica del 'crinale di rumore' del 22/08]") + + # -- 6. GATE -------------------------------------------------------------------------- + hr("6. GATE sul candidato (A) scelto in-sample") + print(A.fmt_marginal(rep["marginal"])) + cz = A.causality_ok(fn_ch, tf=ch["tf"]) + print(f"\n causality_ok : ok={cz['ok']} max_tail_diff={cz['max_tail_diff']} " + f"(prefissi controllati {cz['checked']})") + imp = A.implausible_sharpe(d_ch) + print(f" implausible_sharpe : implausible={imp['implausible']} Sh {imp.get('sharpe')} " + f"maxDD {imp.get('maxdd')} perdite/attive {imp.get('loss_frac')} " + f"attive {imp.get('active_frac')}") + for rr in imp["reasons"]: + print(f" - {rr}") + print(f" null de-levering vs COSTANTE : {delever_null(d_ch, d_const)}") + print(f" null de-levering vs TP01 : {delever_null(d_ch, base_1d)}") + print(" haircut a $600 (min-order $5):") + for a in ASSETS: + df = A.get(a, ch["tf"]) + sc = A.eval_weights_smallcap(df, A._call_target(fn_ch, df, a), capital=600.0) + print(f" {a}: modellato {sc['modeled']['sharpe']:+.3f} -> reale " + f"{sc['realistic']['sharpe']:+.3f} (haircut {sc['sharpe_haircut']:+.3f}, " + f"{sc['n_executed_trades']} trade)") + + # DSR a piu' conteggi: famiglia, famiglia+112 del primo tentativo, di SCREEN + all_sr = ([r["full_sharpe"] for r in rep["rows"]] + + [r["full_sharpe"] for r in csel["rows"]] + + [r["full_sharpe"] for r in wsel["rows"]]) + hr("6-bis. DEFLATED-SHARPE β€” sensibilita' del verdetto al CONTEGGIO (regola 30/07 + 22/08)") + print(f" Sharpe FULL del candidato (A) = {A._sh(d_ch):+.3f}") + print(dsr_line(A._sh(d_ch), all_sr, d_ch, "mia famiglia A+B+C", N_DECLARED)) + print(dsr_line(A._sh(d_ch), all_sr, d_ch, "+ le 112 del 22/08", N_DECLARED + N_PRIOR_FAMILY)) + print(dsr_line(A._sh(d_ch), all_sr, d_ch, "di SCREEN (ondata)", + N_DECLARED + N_PRIOR_FAMILY + N_SCREEN)) + print("\n ⚠️ QUEL 'PASS' NON CERTIFICA NIENTE, e va detto: `deflated_sharpe` stima il massimo") + print(" atteso dal rumore dalla VARIANZA degli Sharpe della griglia. Qui la griglia e'") + print(" OMOGENEA (tutte varianti di TSMOM, molto correlate fra loro) -> varianza piccola") + print(" -> sr0 basso -> DSR alto per COSTRUZIONE. Aumentare N padding con la media alza N") + print(" ma non la varianza. Il conteggio che decide e' quello di SCREEN, e il filone ORTHO") + print(" lo ha gia' misurato: su 168 trial di ricerca direzionale BTC/ETH il massimo atteso") + print(" dal puro rumore e' Sharpe 1.572. Riscritto con QUEL sr0:") + for nome, ser in (("adattivo (A)", d_ch), ("miglior costante (B)", d_const), + ("TP01 30/90/180", base_1d)): + print(f" {nome:<22} Sharpe FULL {A._sh(ser):+.3f} vs sr0_screen 1.572 -> " + f"DSR {dsr_given_sr0(ser, A._sh(ser), 1.572):.3f} " + f"{'PASS' if dsr_given_sr0(ser, A._sh(ser), 1.572) >= 0.95 else 'FAIL'}") + + # PROVA DI IDENTITA': la cella "adattiva" al 100% sul bordo E' il costante, bit per bit + hr("6-ter. PROVA DI IDENTITA' β€” la cella al bordo NON somiglia a un costante: LO E'") + deg = [r for r in rep["rows"] + if diag[(r["tf"], r["params"]["det"], r["params"]["par"], r["params"]["stat"], + r["params"]["mapping"])]["frac_lo"] >= 0.999] + if deg: + dr = max(deg, key=lambda r: r["insample_sharpe"]) + s_deg = A.candidate_daily(factory(tf=dr["tf"], **dr["params"]), tf=dr["tf"]) + s_con = A.candidate_daily(const_factory(tf=dr["tf"], L=LMIN_D, + blend=dr["params"]["blend"]), tf=dr["tf"]) + Jd = pd.concat({"a": s_deg, "b": s_con}, axis=1, join="inner").dropna() + print(f" cella 100% al bordo basso: {dr['tf']} {dr['params']} IS {dr['insample_sharpe']}" + f" FULL {dr['full_sharpe']}") + print(f" costante L={LMIN_D}g {dr['params']['blend']} {dr['tf']}: " + f"FULL {A._sh(s_con):+.3f}") + print(f" max|serie_adattiva - serie_costante| = {float(np.max(np.abs(Jd['a']-Jd['b']))):.3e}" + f" (n={len(Jd)}) -> {'SONO LA STESSA STRATEGIA' if float(np.max(np.abs(Jd['a']-Jd['b']))) < 1e-12 else 'diverse'}") + else: + print(" nessuna cella e' al 100% sul bordo basso (la prova non si applica)") + + # -- 7. FAMIGLIA RISTRETTA ALLE SOLE CELLE DAVVERO ADATTIVE -------------------------- + hr("7. GATE SULLA SOLA FAMIGLIA DAVVERO ADATTIVA (la domanda della missione, isolata)") + ad_rows = [r for r in rep["rows"] + if diag[(r["tf"], r["params"]["det"], r["params"]["par"], + r["params"]["stat"], r["params"]["mapping"])]["adattiva"]] + d_ad_keep = fn_ad_keep = best_keep = None + if not ad_rows: + print(" NESSUNA cella della famiglia e' davvero adattiva -> vedi sezione 1.") + else: + best = max(ad_rows, key=lambda r: r["insample_sharpe"]) + print(f" celle adattive: {len(ad_rows)}/{len(rep['rows'])} miglior IN-SAMPLE: " + f"tf={best['tf']} {best['params']} IS {best['insample_sharpe']} " + f"FULL {best['full_sharpe']}") + fn_ad = factory(tf=best["tf"], **best["params"]) + d_ad = A.candidate_daily(fn_ad, tf=best["tf"]) + sm = A.study_marginal("BOCPD-H (sole adattive)", fn_ad, tf=best["tf"]) + dsr_ad, sr0_ad = A.deflated_sharpe(A._sh(d_ad), + [r["full_sharpe"] for r in ad_rows], d_ad) + print(f" marginal={sm['marginal_verdict']} earns_slot={sm['earns_slot']} " + f"corr->TP01 {sm['marginal'].get('corr_full')}") + print(f" deflated-Sharpe (N={len(ad_rows)}) = {dsr_ad:.3f} " + f"{'PASS' if dsr_ad >= 0.95 else 'FAIL'} (null max atteso {sr0_ad:+.3f})") + JA = pd.concat({"C": d_ad, "K": d_const, "B": base_1d}, axis=1, join="inner").dropna() + JH = JA[JA.index >= HOLDOUT] + print(f" FULL adattiva-vera {A._sh(JA['C']):+.3f} | costante {A._sh(JA['K']):+.3f} " + f"| TP01 {A._sh(JA['B']):+.3f}") + print(f" HOLD-OUT adattiva-vera {A._sh(JH['C']):+.3f} | costante {A._sh(JH['K']):+.3f} " + f"| TP01 {A._sh(JH['B']):+.3f}") + r3 = paired_by_year(d_ad, d_const) + d3 = [r["d_sh"] for r in r3] + print(" vs miglior costante, differenza APPAIATA per anno:") + print(" " + " ".join(f"{r['year']}:{r['d_sh']:+.2f}" for r in r3)) + print(f" -> dSh>0 in {sum(1 for x in d3 if x>0)}/{len(d3)} mediana {np.median(d3):+.2f}" + f" corr al costante {JA['C'].corr(JA['K']):+.3f}") + print(" ⚠️ l'hold-out sono gli ULTIMI DUE anni di questa stessa riga: se il vantaggio") + print(" hold-out sta li' e non nel resto, e' un evento, non un meccanismo (regola:") + print(" un vantaggio concentrato in un anno non e' un vantaggio).") + print(f" null de-levering vs costante: {delever_null(d_ad, d_const)}") + d_ad_keep, fn_ad_keep, best_keep = d_ad, fn_ad, best + # -- 8. BANDA D'ANCORA (delta APPAIATO adattivo - costante) -------------------------- + hours = 24 if ch["tf"] == "1d" else 12 + offs = list(range(hours)) + hr(f"8. BANDA D'ANCORA β€” {len(offs)} ancore. Stima onesta = MEDIANA; il confronto fra due " + f"varianti\n e' la MEDIANA DELLE DIFFERENZE APPAIATE (mai la differenza delle mediane).") + if cch["tf"] != ch["tf"]: + print(f" ⚠️ adattivo su {ch['tf']} e miglior costante su {cch['tf']}: per il delta " + f"appaiato uso il miglior costante RIVALUTATO su {ch['tf']} (stesse ancore = coppie).") + crow = max([r for r in csel["rows"] if r["tf"] == ch["tf"]], + key=lambda r: r["insample_sharpe"]) + fn_const_anch = const_factory(tf=ch["tf"], **crow["params"]) + print(f" miglior costante su {ch['tf']}: {crow['params']} IS {crow['insample_sharpe']}") + else: + fn_const_anch = fn_const + s0 = series_at_anchor(fixed_target, "fixed", hours, 0) + K = pd.concat({"a": s0, "b": base_ch}, axis=1, join="inner").dropna() + print(f" sanity ancora 0 vs serie canonica del fisso: max|Ξ”| " + f"{float(np.max(np.abs(K['a']-K['b']))):.3e} (n={len(K)})") + for lab, mtr in (("Sharpe FULL", A._sh), + ("Sharpe HOLD", lambda s: A._sh(s[s.index >= HOLDOUT]))): + b_c = A.anchor_luck_band(lambda o: series_at_anchor(fn_ch, "cand", hours, o), offs, + metric=mtr) + b_k = A.anchor_luck_band(lambda o: series_at_anchor(fn_const_anch, "const", hours, o), + offs, metric=mtr) + print(f" {lab:<12} adattivo: canonica {b_c['canonical']:+.3f} " + f"(pctl {b_c['canonical_pctl']:.2f}) MEDIANA {b_c['median']:+.3f} " + f"banda [{b_c['lo']:+.3f},{b_c['hi']:+.3f}]") + print(f" {'':<12} costante: canonica {b_k['canonical']:+.3f} " + f"(pctl {b_k['canonical_pctl']:.2f}) MEDIANA {b_k['median']:+.3f} " + f"banda [{b_k['lo']:+.3f},{b_k['hi']:+.3f}]") + dl = A.anchor_luck_delta(lambda o: series_at_anchor(fn_ch, "cand", hours, o), + lambda o: series_at_anchor(fn_const_anch, "const", hours, o), + offs, metric=mtr) + print(f" {'':<12} DELTA APPAIATO (adatt - cost): mediana {dl['median_paired']:+.3f} " + f"positivo in {dl['n_positive']}/{dl['n_anchors']} " + f"banda [{dl['lo']:+.3f},{dl['hi']:+.3f}] gate_pass={dl['gate_pass']}") + + # -- 8-bis. ANCORA APPAIATA per la cella DAVVERO ADATTIVA --------------------------- + if fn_ad_keep is not None and best_keep["tf"] == ch["tf"]: + hr("8-bis. ANCORA APPAIATA β€” cella DAVVERO ADATTIVA vs miglior costante") + for lab, mtr in (("Sharpe FULL", A._sh), + ("Sharpe HOLD", lambda s_: A._sh(s_[s_.index >= HOLDOUT]))): + dl = A.anchor_luck_delta( + lambda o: series_at_anchor(fn_ad_keep, "adatt", hours, o), + lambda o: series_at_anchor(fn_const_anch, "const", hours, o), offs, metric=mtr) + b = A.anchor_luck_band(lambda o: series_at_anchor(fn_ad_keep, "adatt", hours, o), + offs, metric=mtr) + print(f" {lab:<12} adattiva-vera: canonica {b['canonical']:+.3f} " + f"(pctl {b['canonical_pctl']:.2f}) MEDIANA {b['median']:+.3f} " + f"banda [{b['lo']:+.3f},{b['hi']:+.3f}]") + print(f" {'':<12} DELTA APPAIATO vs costante: mediana {dl['median_paired']:+.3f} " + f"positivo in {dl['n_positive']}/{dl['n_anchors']} " + f"banda [{dl['lo']:+.3f},{dl['hi']:+.3f}] gate_pass={dl['gate_pass']}") + elif fn_ad_keep is not None: + print(f"\n (8-bis non girata: la cella adattiva sta su {best_keep['tf']} e il costante " + f"su {ch['tf']} -> le ancore non sarebbero coppie)") + + # -- 8-ter. IL CRITERIO DEL FALSIFICATORE, SU **TUTTE** LE CELLE ADATTIVE ------------ + hr("8-ter. IL CRITERIO DEL FALSIFICATORE APPLICATO A **OGNI** CELLA DAVVERO ADATTIVA") + print(" Il 22/08 dichiarava: Β«un rilevatore il cui L_t si muove DAVVERO e batte il miglior") + print(" lookback COSTANTE in >=6/8 anni appaiati con corr->TP01 < 0.6Β». Non basta che") + print(" fallisca il VINCITORE: si applica il criterio a tutte e", len(ad_rows), "le celle adattive.") + print(f"\n {'tf':>4} {'cella':<58} {'corr':>6} {'anni>0':>7} {'dShFULL':>8} {'dShHOLD':>8} esito") + passed = [] + for r in sorted(ad_rows, key=lambda x: -x["insample_sharpe"]): + fnr = factory(tf=r["tf"], **r["params"]) + dr = A.candidate_daily(fnr, tf=r["tf"]) + JJ2 = pd.concat({"a": dr, "b": tp01}, axis=1, join="inner").dropna() + cor = float(JJ2["a"].corr(JJ2["b"])) + yr = paired_by_year(dr, d_const) + nyp = sum(1 for x in yr if x["d_sh"] > 0) + Jr = pd.concat({"C": dr, "B": d_const}, axis=1, join="inner").dropna() + JrH = Jr[Jr.index >= HOLDOUT] + dF = A._sh(Jr["C"]) - A._sh(Jr["B"]) + dH = A._sh(JrH["C"]) - A._sh(JrH["B"]) + ok = bool(cor < 0.6 and nyp >= 6) + if ok: + passed.append((r, cor, nyp, dF, dH)) + lab = (f"{r['params']['det']} {r['params']['par']} {r['params']['stat']} " + f"{r['params']['mapping']} {r['params']['blend']}") + print(f" {r['tf']:>4} {lab:<58} {cor:>6.3f} {nyp:>5}/8 {dF:>+8.3f} {dH:>+8.3f} " + f"{'PASSA' if ok else ''}") + print(f"\n celle che soddisfano ENTRAMBE le condizioni del falsificatore: " + f"{len(passed)}/{len(ad_rows)}") + cors = [] + for r in ad_rows: + dr = A.candidate_daily(factory(tf=r["tf"], **r["params"]), tf=r["tf"]) + JJ2 = pd.concat({"a": dr, "b": tp01}, axis=1, join="inner").dropna() + cors.append(float(JJ2["a"].corr(JJ2["b"]))) + print(f" corr->TP01 sulle celle adattive: min {min(cors):.3f} mediana " + f"{np.median(cors):.3f} max {max(cors):.3f} (soglia del falsificatore 0.60)") + + # -- 9. FEE SWEEP --------------------------------------------------------------------- + hr("9. FEE SWEEP β€” l'adattivo trada di piu': e' li' che muore o no") + for nm, f_, tfx in (("adattivo (A)", fn_ch, ch["tf"]), ("WF-PnL (C)", fn_wf, wch["tf"]), + ("miglior costante (B)", fn_const, cch["tf"]), + ("TP01 30/90/180", fixed_target, "1d")): + line = [] + for fee in (0.0, 0.0005, 0.001, 0.0015): + line.append(f"{2*fee*100:.2f}%RT {A._sh(A.candidate_daily(f_, tf=tfx, fee_side=fee)):+.3f}") + tt = np.mean([A.eval_weights(A.get(a, tfx), A._call_target(f_, A.get(a, tfx), a)) + ["turnover_per_year"] for a in ASSETS]) + print(f" {nm:<22} " + " ".join(line) + f" | turnover {tt:.0f}x/anno") + + hr(f"FINE β€” {time.time()-t0:.0f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_critic.md b/scripts/research/r0822b_critic.md new file mode 100644 index 0000000..d878fd0 --- /dev/null +++ b/scripts/research/r0822b_critic.md @@ -0,0 +1,667 @@ +# CRITICO DI COMPLETEZZA β€” ondata 2026-08-22 (`r0822b_critic`) + +**Mandato:** non testare una strategia. Rispondere a *cosa non e' stato guardato, quale +affermazione e' rimasta non verificata, e qual e' la misura di maggior valore che nessuno ha fatto*. + +**Materiale letto per intero:** `docs/research/BRIEF-0822.md`, `docs/research/RESULTS-0822.md` +(684 righe, 19 sezioni), `docs/diary/2026-08-22-wave-multiagente.md`, piu' i punti dei singoli +`scripts/research/r0822_*.py` dove il ledger andava verificato. + +**Regola di lettura di questo file.** Distinguo tre stati e li marco: +`[VERIFICATO]` = ho aperto la fonte e il fatto e' controllato in questa sessione (comando +riportato o citazione esatta) Β· `[DEDOTTO]` = ragionamento su numeri altrui, non ri-misurato Β· +`[DA MISURARE]` = ipotesi mia, con costo stimato. Non fabbrico numeri nuovi salvo controlli rapidi, +e quando ne faccio uno dico che e' un controllo di cinque minuti e non uno studio. + +**Esito in una riga: l'ondata e' onesta nei singoli filoni e debole nella CUCITURA.** I difetti +gravi non stanno dentro i filoni (che hanno catturato 7 errori propri) ma in tre punti dove +nessuno era responsabile: (a) **due "difetti di dato" sono convenzioni dichiarate nel sorgente di +un fornitore che gira su questa stessa VPS e che nessuno ha aperto**; (b) **il gate principale del +progetto β€” il deflated-Sharpe β€” e' stato misurato lo stesso giorno ai suoi due estremi degeneri da +due agenti diversi, e nessuno ha unito le due misure**; (c) **un filone su venti (ALT-OPTIONS) non +e' nel ledger, e il suo risultato falsifica una quarta soglia pubblicata** β€” quella che tiene +VRP01 fuori dal libro per taglia. + +--- + +## 1. CLAIM NON VERIFICATI + +Ordinati per `(impatto x P(falso)) / costo`. Non elenco tutto: le otto che contano. + +### 1.1 β€” πŸ₯‡ Β«una firm crypto lista i 19 alt Hyperliquid con short abilitatoΒ» (PROP-ALLOC) +`P(falso)` **media-alta** Β· `impatto` **massimo** Β· `costo` **un pomeriggio, zero CPU** + +E' l'unica assunzione su cui poggia **l'intero risultato di testa dell'ondata**: `+0,400 di J` +(argmaxJ 25/25/50 contro il libro live 75/25) e' quasi tutto *mettere XS01 su un conto funded*, e +`+0,016` β€” il 4% β€” e' il riallocare per la barriera. Se XS01 non e' eseguibile presso la firm, +la riga argmaxJ collassa verso `75/25/0`, che nello stesso script misura **J 0,335 / P(pass) 38,6%** +invece di **0,738 / 82,2%**. Con essa cade anche il *risultato strategico* del 25/07 Β§4 +(Β«sul canale funded diversificare paga perche' il vincolo binding e' il DDΒ»), che e' il perno di +tutta la valutazione del canale prop. + +⚠️ **E il progetto ha gia' una regola scritta per questo caso, e non l'ha applicata.** Il 26/07: +*Β«la negoziabilita' sul conto REALE va verificata quando lo sleeve entra in RICERCA, non quando +entra nel bookΒ»* β€” regola nata dal blocco PRIIPs su GTAA01, dove **cinque settimane di misure** +poggiavano su un'assunzione di conto mai controllata, e il controllo costo' **un ordine di prova**. +`r0822_prop_alloc.py` la ripete: alla riga 100 `CRYPTO_PROP = ("TP01","SKH01","XS01") # cio' che +una firm CRYPTO puo' davvero far eseguire` β€” un commento, non una misura. [VERIFICATO: letto il +sorgente; l'agente esclude esplicitamente VRP01/GTAA01 *"una firm CRYPTO non lista opzioni Deribit +ne' ETF azionari USA"*, quindi ha pensato al problema e si e' fermato un passo prima di XS01.] + +**Cosa verificare, in ordine:** (a) la lista strumenti di HYROTRADER e FTMO-crypto contiene i 19 +major HL (SOL/BNB/XRP/DOGE/AVAX/LINK/LTC/ADA/ARB/OP/SUI/APT/INJ/TIA/SEI/NEAR/AAVE oltre BTC/ETH)? +(b) short abilitato su ciascuno? (c) quale costo effettivo per gamba (spread + commissione)? +**La (c) ha una soglia gia' misurata in questa stessa ondata**: XS-LITE riporta che la famiglia XS +regge **oltre 50 bps/gamba** β€” quindi il costo di una firm quasi certamente non morde, ed e' la +(a)/(b) binaria che decide tutto. **Se (a) e' falsa, il verdetto LEAD del filone 1 va riscritto +oggi, non a un gate futuro.** + +### 1.2 β€” πŸ₯ˆ Β«`dealer_net_gamma` e' il GEX col SEGNO INVERTITOΒ» (DEALER-GAMMA) +`P(falso)` **alta β€” anzi: FALSO come formulato** Β· `impatto` medio Β· `costo` **due minuti** + +Il ledger lo chiama *Β«il risultato piu' riusabile del filoneΒ»* e scrive: *Β«Chi lo usasse col nome +che porta leggerebbe ogni regime al contrarioΒ»*. L'agente ha deciso la questione **per +ricostruzione** (corr βˆ’0,95 BTC / βˆ’0,89 ETH contro un GEX da manuale) e lo dichiara esplicitamente +nel proprio codice: `"Quale, lo decide la sezione 2 (ricostruzione dalla catena), non io"` +(`r0822_dealer_gamma.py:187`). + +🚨 **La fonte autorevole e' su questa VPS e dice il contrario.** [VERIFICATO] La colonna non e' +calcolata da cerbero-bite: bite **inoltra e basta** il campo `total_net_dealer_gamma` che riceve +dal tool MCP `get_dealer_gamma_profile` (`src/cerbero_bite/runtime/market_snapshot_cycle.py:85` e +`clients/deribit.py:342-384`, letti dal repo Gitea `Adriano/Cerbero-Bite`). E il produttore e' +`/opt/docker/cerbero-mcp/src/cerbero_mcp/common/options.py`, **acceso adesso**, che in testa al +file dichiara la convenzione: + +``` +# Convention dealer gamma: i dealer sono SHORT calls (le vendono al retail) e +# LONG puts. ... Net dealer gamma negativo -> mercato instabile (squeeze ...) +... +entry["call_dealer_gamma"] -= contrib # dealer short calls +entry["put_dealer_gamma"] += contrib # dealer long puts +``` + +Cioe': **call negative, put positive** β€” l'esatta negazione, su entrambe le gambe, della +convenzione da manuale che l'agente ha usato (`call +, put βˆ’`, `r0822_dealer_gamma.py:237`). +**Non e' un difetto della colonna: e' una convenzione opposta, deliberata e documentata** β€” e nel +suo frame `dng < 0` significa *dealer short gamma / mercato instabile*, che e' proprio la +semantica che il docstring di bite dichiara e che l'entry filter Β§2.8 usava. + +E c'e' una **seconda spiegazione, gratis, per i due numeri residui che l'agente ha attribuito a +non-portabilita'**: la chiamata di bite usa il default `top_n_strikes = 50` [VERIFICATO: +`dealer_gamma_profile(asset_upper)` senza argomenti], contro i **~280-316 strike** della +ricostruzione. Un troncamento ai 50 strike di punta spiega naturalmente sia il coefficiente +`βˆ’0,7` (invece di `βˆ’1,0`) sia l'offset *Β«dng=0 corrisponde a GEX=+5,8e6 su BTCΒ»*, che il ledger +presenta come prova che la soglia non e' portabile. + +**Cosa cambia se e' falso (ed e' falso):** il verdetto **SCARTATO regge intatto** (DSR 0,440, +null de-levering fallito, `n_eff` 24-53 ore su 2.130, cella migliore *sotto* il massimo del +rumore). Ma cade la **regola pubblicata**, e cade nel modo peggiore: la frase del ledger *Β«BTC non +separa, ETH separa al ROVESCIOΒ»* e' formulata in un frame di segno che va rovesciato β€” quindi +l'unica riga del filone che le ondate future erediterebbero e' quella sbagliata. **Correzione da +fare oggi nel ledger, costo due righe.** + +### 1.3 β€” πŸ₯‰ Β«`liquidation_long/short_risk` e' costante 'low': meta' dell'ipotesi non esiste, ed e' +comunque morta perche' la colonna non e' ricostruibileΒ» (FLOW-SQUEEZE) +`P(falso)` **alta sulla seconda meta'** Β· `impatto` medio Β· `costo` **due ore** + +[VERIFICATO] Il fatto e' vero (`low` in 17.229/17.229 righe non nulle, riprodotto). Ma le due +inferenze che l'agente costruisce sopra il fatto sono entrambe smentibili leggendo la stessa +sorgente di Β§1.2, `/opt/docker/cerbero-mcp/.../sentiment/fetchers.py:480-518`: + +``` +long_risk = "high" se avg_funding > 0.0001 e delta_24h > 5 + "medium" se avg_funding > 0.00005 e delta_24h > 2 +``` + +Cioe' **e' una soglia su due input, non una misura**. Conseguenze: +- *Β«Nessuna fonte nostra la produce, e non c'e' niente da produrreΒ»* (`r0822_flow_squeeze.py:626`) + e' **falso**: i due input sono `oi_delta_pct_24h` e il funding cross, e **il funding cross e' nel + file** (`funding_cross_annualized`, copertura 99%), cosi' come `oi_delta_pct_4h` (copertura 99%, + dev.std 1,23%, range βˆ’7,1%…+13,4% β€” [VERIFICATO] con una lettura del parquet). L'agente poteva + ricostruire la variabile di affollamento **con le proprie soglie**, che e' anche metodologicamente + meglio che ereditarne una arbitraria. +- *Β«Costante = zero informazioneΒ»* e' vero come statistica e **sbagliato come lettura**: `low` al + 100% con quelle soglie significa che **nella finestra 2026-03 β†’ 07 non c'e' stato un solo + episodio di affollamento** per quella definizione. Non e' una colonna rotta, e' un fatto di + mercato β€” e per giunta **rafforza** la conclusione del filone (funding chiuso sul quarto lato) + molto piu' di quanto faccia Β«la colonna e' inservibileΒ». + +**Cosa cambia se e' falso:** il verdetto SCARTATO regge (l'altra meta' e' sotto la propria MDE +dichiarata: effetto 0,402% contro MDE 1,056%). Ma il filone e' chiuso con la motivazione +sbagliata, e la motivazione sbagliata e' quella che impedisce di riaprirlo con i dati che ci sono. + +### 1.4 β€” Β«la finestra forward non va persa: riparare `advance()` + RIGENERARE, nessuna data di +gate si spostaΒ» (MONITOR-AUDIT) +`P(falso)` bassa sul fatto, **alta sull'implicazione** Β· `impatto` **alto (3 gate)** Β· `costo` zero + +Il fatto e' provato bene, due volte in modo indipendente (`paper_prevday` 1427/1488 barre identiche +al bit dopo 62 notti; le 6 barre di `paper_statarb` recuperate dopo il guasto EPERM coincidono al +bit). **Non contesto il fatto. Contesto che sia gratis.** + +Una serie **rigenerata** non e' una serie **forward**. La ragione per cui un progetto +pre-registra una finestra forward e' che sia prodotta da codice che *non poteva* essere tarato su +di essa. Dopo la riparazione, la serie su cui si decide il 27/09 sara' prodotta da codice +**scritto oggi, dopo aver visto quella finestra** β€” e chi scrive la riparazione ha gia' letto, +nel ledger, che la metrica registrata dice +1,96 e quella ricostruita βˆ’2,02. La riparazione e' +banale e meccanicamente forzata (scartare la barra non chiusa), quindi il rischio e' piccolo; ma +**e' un grado di liberta' nuovo su un gate pre-registrato, e nessuno lo ha nominato.** + +**Costo di chiuderlo: zero.** Si scrive la riparazione, la si congela con un test, e **solo +dopo** si rigenera β€” dichiarando in anticipo che nessun parametro verra' toccato in base al +risultato della rigenerazione. Farlo dopo aver visto lo Sharpe rigenerato non e' piu' possibile. + +### 1.5 β€” Β«haircut a $5.000 <= 40%Β» come gamba del gate XSR01 del 23/10 +`P(che il gate sia non-informativo)` **alta** Β· `impatto` **alto** Β· `costo` **zero (e' gia' misurato)** + +Due filoni hanno colpito le due gambe del gate del 23/10 **senza che nessuno tirasse la somma**: +- **gamba haircut** β€” HL-EXEC: a $5.000 col pavimento vero l'haircut sta fra **βˆ’7% e +2%**, quindi + *Β«una condizione pre-registrata che non puo' fallire non e' una condizioneΒ»*. XSR-REPRO + dall'altro lato: il **$14,41** pubblicato **non ha alcuno script committato che lo riproduca** ed + e' **~2,4x ottimista** (ticket mediano $3,33, 63% degli ordini sotto $5). +- **gamba Sharpe >= 1,0** β€” MONITOR-AUDIT: la serie che la misura registra **41 minuti di mercato + al giorno**, `corr` col replay **βˆ’0,045**. + +πŸ“Œ **La somma, che il ledger non fa: al 22/08 il gate del 23/10 e' non-informativo su ENTRAMBE le +gambe** β€” una non puo' fallire, l'altra e' letta su uno strumento scollegato dalla realta'. Il +ledger scrive Β«GATE 23/10 COMPROMESSO: PARZIALMENTEΒ». **E' compromesso interamente**, e la +differenza non e' semantica: Β«parzialmenteΒ» autorizza a decidere il 23/10 con una riserva, +Β«interamenteΒ» obbliga a **ri-registrare il gate prima**, che e' l'unica mossa che non e' selezione. + +### 1.6 β€” Β«k* empirico ~10-14xΒ» (GROWTH-POLICY) +`P(falso)` **alta come numero** Β· `impatto` medio-alto Β· `costo` zero (giunzione di due misure gia' fatte) + +L'agente ha gia' fatto due autocritiche corrette (capitale mediano $4,4e12 a k=14x β†’ *Β«a k>2 i +numeri sono aritmetica, non previsioniΒ»*; k* lineare nell'errore del drift). Ne manca una terza, e +sta **dentro la stessa ondata**: SLIP-AUDIT misura che la partecipazione al nastro scala lineare +col nozionale e che il caso peggiore osservato e' gia' **21,9% di una barra 5m** a nozionale ~$636. +La leva moltiplica il nozionale esattamente come il capitale. Quindi [DEDOTTO, aritmetica diretta +sulla tabella di SLIP-AUDIT]: + +| leva su $635 | nozionale | peggior partecipazione osservata, riscalata | +|---|---|---| +| 1,00x | $636 | 22% di una barra 5m | +| **1,50x** (il gradino proposto) | $954 | **33%** | +| 5x | $3.180 | 110% | +| **10-14x** (k*) | $6.400-8.900 | **220-310%** | + +πŸ“Œ **Il gradino 1,00 β†’ 1,50x NON e' toccato da questo** (33% resta dentro il regime misurato): +la raccomandazione operativa sopravvive. **E' il numero k\* che non e' misurabile**, per una terza +ragione indipendente dalle due dichiarate: sopra ~k=5 la serie di ritorni su cui k\* e' calcolato +smette di essere indipendente dalla size, che e' esattamente l'ipotesi che l'agente aveva segnalato +come implausibile senza poterla quantificare. **Ora e' quantificata, gratis, e va scritta accanto +al 10-14x.** + +### 1.7 β€” Β«BTC opzioni min 0.1 = $6.210/lotto β†’ VRP01 fuori a $3.000, servirebbe il 61% del contoΒ» +`P(falso)` **verificata FALSA** Β· `impatto` **alto** Β· `costo` gia' pagato β€” il numero esiste e non e' nel ledger + +Questo e' congelato in `tests/test_vrp_profit_take.py` e in CLAUDE.md. VRP-QUOTE-VERE l'ha +falsificato a meta' (famiglia sbagliata) e **ALT-OPTIONS l'ha falsificato del tutto** β€” ma +ALT-OPTIONS **non ha una sezione nel ledger** (vedi Β§5.1), quindi il numero non e' mai stato +scritto. [VERIFICATO: letto l'output del filone]: + +| famiglia | f_venue | credito eseg. | **max-loss / lotto** | IM se il venue NON netta | +|---|---|---|---|---| +| **ETH_USDC** | **0,980** | $3,38 | **$16,62** | $33,50 | +| BTC_USDC | 0,921 | $6,25 | $33,75 | $101,79 | +| ETH inverse (la famiglia del muro) | 0,930 | $29,36 | $145,88 | $324,42 | +| BTC inverse (la famiglia del muro) | 0,927 | $81,97 | $418,03 | $1.017,06 | + +Il muro pubblicato e' calcolato sul **nozionale del lotto minimo della famiglia inverse**. La +grandezza giusta per una struttura a rischio definito e' il **max-loss**, e sulla famiglia che il +conto puo' marginare vale **$16,62**. Al peso di libro del 12%, VRP01 entra con un lotto da un +conto di **~$140** (o ~$280 se Deribit non netta le gambe), non da $3.000 al 61% del conto β€” +**due ordini di grandezza**. Sul conto vero ($635) ci stanno **7 strutture dentro un budget di +rischio del 20%**, con **3.000 lotti al miglior bid** e spread della gamba corta al **2,6%**. + +⚠️ **Cosa NON cambia, e va detto forte:** VRP01 resta fuori dal libro, ma **per la regola +Β«niente short-vol da modello in deployΒ» e per il gate IV-rank (0/19 settimane aperte)**, non per +la taglia. Cade *una* delle tre ragioni, ed e' quella che il progetto ripete piu' spesso. + +### 1.8 β€” Β«lo screen largo non puo' passare il proprio gate: e' aritmeticaΒ» (ORTHO-SCREEN) +`P(falso)` **alta come generalizzazione** Β· `impatto` **alto (tocca 3 gate futuri)** Β· `costo` **5 minuti** + +Vedi Β§2.1: l'ho ri-derivato e il numero e' giusto ma **non dice cio' che il ledger gli fa dire**. + +--- + +## 2. CONTRADDIZIONI FRA AGENTI + +### 2.1 β€” 🚨 IL DEFLATED-SHARPE MISURATO AI SUOI DUE ESTREMI DEGENERI, LO STESSO GIORNO, DA DUE AGENTI CHE NON SI CITANO + +- **ORTHO-SCREEN**: *Β«su 168 trial il massimo atteso dal puro rumore e' Sharpe 1,572, sopra il + soffitto direzionale (~1,3) β†’ una ricerca a rete larga NON PUO' passare il proprio gate. Non e' + sfortuna, e' aritmetica.Β»* DSR di screen **0,034**. +- **SCETTICO su VOL-SIZE**: *Β«`DSR = 1,000` dichiarato **VACUO, non PASS**: lo passa anche il + baseline β€” su una famiglia di perturbazioni della stessa strategia il deflated-Sharpe non ha + potenza.Β»* + +**Sono la stessa osservazione dai due lati, e insieme delimitano il gate principale del progetto.** +[VERIFICATO] Ho aperto `altlib.deflated_sharpe`: il massimo atteso non e' `SE(Sharpe) x sqrt(2 ln N)` +ma la formula Bailey–Lopez de Prado + +``` +sr0 = sd(Sharpe dei trial OSSERVATI) x [ (1-gamma)*z(1-1/N) + gamma*z(1-1/(N e)) ] +``` + +cioe' **la dispersione trasversale delle celle che hai deciso di dichiarare**, moltiplicata per un +fattore che dipende solo da N. Ricalcolando il moltiplicatore [controllo di cinque minuti]: + +| N dichiarato | moltiplicatore | `sr0` con la STESSA dispersione (sd = 0,58) | +|---|---|---| +| 12 | 1,665 | 0,97 | +| **24** | 1,980 | **1,15** | +| 48 | 2,261 | 1,31 | +| 72 | 2,413 | 1,40 | +| **168** (lo screen) | 2,708 | **1,57** | +| 729 | 3,164 | 1,84 | + +E al variare della dispersione, a parita' di dati: + +| dispersione delle celle | `sr0(24)` | `sr0(168)` | +|---|---|---| +| 0,15 (famiglia di perturbazioni, tipo VOL-SIZE) | 0,30 | 0,41 | +| 0,58 (lo screen ortogonale) | 1,15 | 1,57 | + +πŸ“Œ **Tre letture che il ledger non ha:** +1. **La soglia Β«1,572Β» non e' una proprieta' di BTC/ETH: e' una proprieta' della GRIGLIA.** Le + stesse 168 celle, dichiarate come 7 famiglie da 24, danno `sr0 = 1,15` β€” **sotto** il soffitto + direzionale. Il verdetto Β«impossibile per aritmeticaΒ» si ribalta **senza toccare un dato**. +2. **Percio' la lezione pubblicata invita alla mossa che il progetto ha gia' proibito.** Il ledger + conclude *Β«le ondate future o dichiarano famiglie molto piu' piccole in anticipo, o cambiano + meccanismoΒ»*. La prima meta' e' **esattamente** il barare che il 30/07 aveva codificato: + *Β«il deflated-Sharpe e' l'unico posto dove barare e' indolore e invisibileΒ»*, e + *Β«il verdetto si ribalta col conteggioΒ»* (congelato in un test su VRP01: DSR 0,983 a N=8 / + 0,948 a N=72 / 0,909 a N=360). **Il 22/08 la stessa leva viene raccomandata come buona pratica.** +3. **Cio' che il progetto non sapeva e' la META' PIU' GRANDE: la dispersione.** Il fattore N va da + 1,67 a 3,16 su due ordini di grandezza di trial (1,9x). La dispersione va da 0,15 a 0,58 fra una + famiglia omogenea e uno screen (3,9x). **Il gate e' dominato dalla variabile mai dichiarata.** + +**Conseguenza operativa immediata**, perche' tocca tre gate pre-registrati: DVOLSPREAD 24/10 ha +come criterio (c) *Β«deflated-Sharpe ricalcolato >= 0,95Β»* β€” su una famiglia di 729 celle +omogenee; XSR01 23/10 poggia su DSR 0,983; VOL-SIZE ha gia' dichiarato il proprio DSR vacuo. +**Nessuno dei tre e' interpretabile senza dichiarare, accanto a N, la dispersione delle celle.** +Riformulazione onesta della lezione di ORTHO-SCREEN: *con celle disperse di ~0,58 di Sharpe e un +soffitto di ~1,3, il gate non separa; la cura non e' dichiarare meno trial, e' progettare famiglie +i cui membri siano varianti di UN meccanismo β€” e li' il DSR e' vacuo, quindi serve un altro gate.* +**Il deflated-Sharpe non ha una regione utile in mezzo, e nessuno l'ha detto.** + +### 2.2 β€” Lo Sharpe di XSR01: non tre lenti ma **quattro**, e la quarta non e' nel quadro conciliato +Il ledger conclude con un Β«quadro conciliatoΒ»: titolo `demean_skeptic` **1,82** Β· gate +`basket_gate` **1,75-1,79** Β· monitor `paper_xsr` **2,21**, e prescrive +*Β«NUMERO ONESTO DA CITARE: 1,79 (finestra di scoperta) / 1,63 (a oggi)Β»*. + +[VERIFICATO nell'output di XS-LITE] Lo stesso giorno **XS-LITE**, che dichiara replica **bit-exact** +contro `basket_from_positions(demean=True)` (`max|diff| = 0,000e+00` su 965 barre), pubblica: +*Β«XSR01 pieno, lente pubblicata: Sharpe **1,64** (pubblicati 1,82)Β»*, *Β«normalizzazione COSTANTE +1/50: **1,65**Β»*, *Β«sulla FINESTRA DI SCOPERTA (<= 2026-07-25, 937 barre): **1,75**Β»*. + +Quindi **due agenti che rivendicano entrambi la replica bit-exact della stessa funzione danno 1,75 +e 1,79 sulla stessa finestra.** La spiegazione piu' probabile e' la loro stessa scoperta (l'ultima +barra della finestra di scoperta era parziale il 25/07 e oggi e' chiusa), e i due concordano +sull'oggi (1,64 vs 1,63). **Ma il ledger prescrive una coppia canonica senza nominare l'altra**, e +la regola che pubblica β€” *Β«va sempre citata la coppia (lente, ultima barra chiusa)Β»* β€” e' proprio +quella che avrebbe risolto la discrepanza se applicata a se stessa. **Costo di chiudere: una riga +nel ledger. Impatto: e' il numero su cui si decide il 23/10.** + +### 2.3 β€” Il muro di XS01: **tre numeri diversi** nello stesso corpus di documenti +- XS-LITE: capitale minimo perche' il ribilancio passi = **$109 di sleeve (~$730 di libro)**. +- HL-EXEC: haircut ~0 gia' a **$200-600 allocati** (β†’ a peso 15%, **$1.300-4.000 di conto**). +- Diario Β§6.5: *Β«XS01 e' eseguibile a **~$1.300 di conto**Β»* β€” cioe' l'estremo basso di HL-EXEC. + +I tre sono compatibili (criteri diversi: soglia del ribilancio vs soglia dell'haircut), ma il +diario ne pubblica **uno** come se fosse *il* numero, dopo che l'ondata ha appena passato una +giornata a dimostrare che i numeri pubblicati a un criterio solo sono il difetto ricorrente del +progetto. **Da citare come banda `$730-$1.300` col criterio accanto.** + +### 2.4 β€” La leva: due filoni della stessa ondata danno guida opposta +- **GROWTH-POLICY**: k\* ~10-14x, il libro gira al 7% di Kelly, **alzare** la leva vale 14,7a β†’ 11,6a. +- **PROP-ALLOC**: J massimo a **0,75x**, e *Β«P(pass) senza limite di tempo e' monotona + DECRESCENTEΒ»* (98,9% a 0,50x β†’ 65,9% a 1,50x). + +Non e' un errore: sono obiettivi diversi (crescita in log senza barriera vs sopravvivenza sotto +una barriera per-conto). **Ma nessuno lo scrive**, e l'operatore leggera' due raccomandazioni +opposte nello stesso documento. La sintesi che manca, e che riconcilia: +πŸ“Œ **anche il conto proprio ha una barriera assorbente** β€” e' la decisione di venue del 26/07 +(*Β«zero e' assorbente: andare a zero all'anno 10 di un piano da 16 anni significa non arrivarci +piu'Β»*). Quindi il criterio di GROWTH-POLICY (Kelly puro) e' il caso limite senza barriera, e +PROP-ALLOC misura cosa succede quando la barriera c'e': **la leva ottima cade da 10x a 0,75x +appena si mette una barriera, e la distanza fra i due numeri E' la misura di quanto la barriera +costa.** Detta cosi', le due misure smettono di contraddirsi e diventano la stessa curva. + +### 2.5 β€” Il peso di SKH01: respinto da un filone, raccomandato al doppio dall'altro +- **VOL-SIZE / scettico**: *Β«quasi ogni variante alzava il peso effettivo di SKH fino a 0,375, e + "alzare SKH01" e' gia' stato misurato e respinto il 26/07Β»* β€” motivo per cui il null giusto e' + iso-peso. +- **PROP-ALLOC**: sul campione 2019+ che contiene il 2022, l'ottimo della coppia crypto sotto + barriera e' **38/62** (J 0,521 contro 0,443), cioe' **SKH01 al 62%**. + +Di nuovo obiettivi diversi, di nuovo non detto. Ma qui c'e' un aggravante che **l'agente dichiara +da solo**: PROP-ALLOC non ha girato la banda d'ancora, quindi il suo ottimo poggia sull'ancora +canonica di **SKH01, lo sleeve che il LOO del 26/07 misura come quello con la fortuna d'ancora +piu' grande da restituire** (l'ancora regala 2/3 del FULL, 70% dell'hold-out, 80% della protezione +DD). **La distorsione e' a favore della raccomandazione, e la raccomandazione riguarda l'unico +canale che l'operatore potrebbe aprire quest'anno.** Costo di chiuderla: rigirare l'ottimo su una +banda di 8 ancore β€” l'ordine di grandezza del budget di un agente di questa ondata. + +### 2.6 β€” Il conteggio degli ingressi VRP nella stessa finestra di catena: 19 contro 22 +VRP-QUOTE-VERE: *Β«f = 0,714 pooled, **0/19** osservazioni >= 1,0Β»*; SKEW: *Β«0,714 su **22** +ingressiΒ»*. Stesso `f` a tre decimali, popolazione diversa. [VERIFICATO che SKEW usa +*Β«stessa macchina di `cblib`Β»*, quindi **non e' una replica indipendente** e l'accordo a tre +decimali non e' evidenza.] Spiegazione benigna probabile (VRP richiede **entrambe** le gambe +quotate ai due delta obiettivo, SKEW ricostruisce lo smile e ne richiede meno). **Ma il ledger +presenta il 0,714 come Β«replica indipendenteΒ» del f del 30/07 senza dire che il secondo 0,714 +viene dallo stesso harness.** Impatto basso, costo di chiarirlo nullo. + +### 2.7 β€” Β«il collettore raccoglie la famiglia sbagliataΒ» e' una classificazione, non un fatto +Il ledger apre VRP-QUOTE-VERE con 🚨 **IL RISULTATO DI PRODUZIONE**. [VERIFICATO in +`scripts/live/collect_chain.py:114`: `{"currency": asset, "kind": "option"}` e `OI_MIN = 100.0` +alla riga 57 β€” i due fatti sono esatti.] Ma il collettore serve la **ricerca sui prezzi**, non +l'esecuzione: sulle inverse ci sono 415/548 strumenti con OI>=100 e **97% di quote a due lati**, +contro il 94% e 119 strumenti di ETH_USDC. Per misurare `f`, IV-rank e struttura a termine, la +famiglia raccolta e' **la migliore delle due**. Il difetto esiste solo rispetto a un uso β€” +l'esecuzione β€” che oggi e' vietato da un'altra regola (Β«niente short-vol da modello in deployΒ»). +Il diario lo corregge a meta' (punto aperto 3 lo presenta come decisione sul rate limit); il +ledger no. **Un difetto di produzione e una scelta di raccolta non si segnano con lo stesso +simbolo**, o il prossimo che legge ripara qualcosa che non e' rotto β€” al costo di **+90% sul giro** +e del rate-limit per-IP che il 29/07 e' gia' costato un guasto. + +--- + +## 3. MODALITA' NON ESPLORATE + +Filtro applicato: (a) compatibile con dati che il progetto possiede o puo' ottenere, (b) non nella +lista dei filoni morti del brief, (c) meccanismo plausibile e nominabile. Niente idee generiche. + +### 3.1 β€” πŸ₯‡ Il costo del CHOP, cioe' il rischio che il libro non ha mai vissuto +**Dati:** esistono (7,4 anni di libro; 2018-19 e 2022-23 come stiramenti laterali; 30 anni di +equity come stampo). **Meccanismo:** il progetto ha misurato la difesa nel **sinistro** β€” il +criterio B di `edge_watch` e' 8/8 anni di crash superati β€” e non ha **mai** misurato la modalita' +di fallimento propria di un trend-follower, che non e' il crash ma il **mercato laterale**. E la +misura di controllo che ho fatto in cinque minuti la rende urgente e non teorica: + +| giorno | BTC | ETH | **libro 75/25 (close-only)** | +|---|---|---|---| +| 2020-03-12 | **βˆ’41,5%** | **βˆ’45,7%** | **+0,97%** | +| 2022-06-13 | βˆ’15,5% | β€” | +0,72% | +| 2021-05-19 | βˆ’14,3% | βˆ’27,9% | βˆ’1,98% | +| 2022-11-09 | βˆ’14,4% | βˆ’18,0% | 0,00% | +| 2021-01-21 | β€” | βˆ’19,5% | βˆ’2,24% | + +[controllo di cinque minuti, non uno studio: `altlib.tp01_baseline_daily()` + `sleeves._skyhook_returns()`, +75/25, ancora canonica. Peggior giorno del libro su 7,4 anni: **βˆ’3,38% (2025-10-10)**, che **non e' +un giorno di crash**.] Cioe': **il libro non perde nei crolli β€” nei crolli guadagna.** Tutte le sue +peggiori giornate sono altrove. **Nessuno ha mai chiesto dove.** Un piano a 10-20 anni su una +strategia il cui unico rischio misurato e' quello che non la tocca ha un buco esattamente della +forma che questo progetto e' bravo a trovare in tutto il resto. + +### 3.2 β€” πŸ₯ˆ Il prezzo di essere ciechi: il costo atteso in P&L dell'attrito OPERATIVO +**Dati:** ci sono gia' e sono nostri β€” `logs/cron_book.log`, `data/venue_watch`, `feed_age_minutes`, +`skh_feed_errors`, `book_executions.jsonl`. **Meccanismo:** nel 2026 il libro e' stato cieco o +stantio almeno tre volte documentate (feed-freeze 14/07; fallback silenzioso di `fresh_5m` il +29/07, latenza d'uscita di SKH01 da ~1h a **~11h** per 6 giri su 8; manutenzione Deribit del 18/08 +sforata **fino alle 14:07**, con astensione del libro). Il progetto ha prezzato fee, slippage, +min-order, pavimento IB, fortuna d'ancora, degrado d'esecuzione, e persino **il fallimento +dell'exchange** β€” e non ha **mai** prezzato *quanto costa all'anno che il libro non veda*. E' una +misura, non una previsione: si prendono le finestre di cecita' realmente occorse, si guarda cosa ha +fatto il prezzo dentro, e si confronta il P&L realizzato col P&L del percorso non-cieco. +**Perche' conta adesso:** e' un costo che **scala col nozionale** esattamente come la leva, quindi +entra direttamente nel conto di Β§4; e le uscite di SKH01 sono la gamba latency-sensitive. +**Perche' non e' morto:** non e' calendario, non e' un overlay, non e' una strategia β€” e' il +prezzo di un rischio operativo gia' materializzato tre volte. + +### 3.3 β€” πŸ₯‰ Lo spread di volatilita' BTC-vs-ETH come sleeve, non come timer +**Dati:** `dvol_btc` / `dvol_eth` dal 2021-03 = **1978 giorni, 5,4 anni** β€” l'unico dataset non +direzionale del progetto abbastanza lungo da avere un hold-out vero (a differenza di tutta la +catena, ferma a 74-90 giorni). **Meccanismo:** i due asset condividono un fattore di vol comune, e +la *ricchezza relativa* delle due vol e' una grandezza stazionaria per costruzione, mentre il +livello non lo e'. **Non e' DVOLSPREAD:** quello e' il lead in forward-monitor che usa lo spread +come **timer direzionale** (de-risk); qui lo spread si traderebbe **come tale** (straddle lungo su +uno, corto sull'altro), market-neutral rispetto alla direzione e β€” a differenza di VRP01 β€” con +esposizione al premio di vol quasi cancellata fra le due gambe. **Non e' nella lista dei morti** +(gamma scalping = long-vol nudo; VRP01 = short-vol nudo gated; questo e' relative-value). +⚠️ **Due muri dichiarati in anticipo:** (i) sarebbe prezzato su DVOL, cioe' BS-flat, e la SKEW di +questa ondata ha appena misurato che il flat costa **x0,869 di struttura a termine** e **x0,920 di +skew** β€” quindi il verdetto massimo e' LEAD finche' non e' riprezzato sulla catena vera; (ii) +richiede opzioni su entrambi gli asset: fuori a $635 (BTC_USDC ~$780/lotto), dentro a ~$3-5k. + +### 3.4 β€” Il basis dei futures datati come **campione della coda**, non come carry +BASIS-CALENDAR ha costruito un dataset che il progetto non aveva β€” **BTC 34 contratti / 240.150 +barre orarie dal 2018-09, ETH 34 / 233.334, piu' 64.110 ore di funding e indice** β€” e lo ha usato +per una sola domanda (il premio incassabile, IC95 che contiene lo zero β†’ SCARTATO). **Il dataset +contiene la coda che manca a tutto il resto del progetto**, e il ledger lo dice esplicitamente +(*Β«include il deleveraging 2022 β€” esattamente la coda che a CC01 mancava per costruzioneΒ»*). +**Uso non esplorato:** il blow-out del basis e' l'unico marcatore osservabile e datato dei giorni +di deleveraging forzato. Condizionare a quei giorni le domande di Β§3.1 e Β§4 da' un **campione di +stress reale** al posto di uno scenario assunto a mano. +⚠️ **E c'e' un'azione a costo quasi nullo che nessuno ha messo negli aperti:** quel dataset vive in +`.../scratchpad/basis_cache/` (**20 MB**, [VERIFICATO]), cioe' in una cartella di sessione che +sparisce. E' ricostruibile (30/30 trimestrali rispondono e il metodo β€” costruire a mano i nomi dei +contratti scaduti e chiamare `get_tradingview_chart_data` β€” e' documentato nello script), ma +ricostruirlo costa rete e tempo che ora sono gia' stati spesi. **Persisterlo in `data/raw/fut_*` +e' l'unica cosa dell'intera ondata che si perde per non averla scritta.** + +### 3.5 β€” La copertura di coda come **abilitante della leva**, non come miglioramento dello Sharpe +**Stato dell'arte nel progetto, dichiarato:** tail hedge *e' gia' stato provato* β€” `OPT06` (ratio +put spread con tail hedge) e `OPT07` (collar) nello sweep del 20/06, entrambi **modellati su DVOL +BS-flat** e giudicati **sullo Sharpe**, entrambi bocciati. **Cosa non e' stato fatto:** giudicarlo +sull'obiettivo **crescita a leva**. Sono domande diverse perche' il valore di un pavimento e' +**convesso nella leva**: a k=1 una put lontana e' un costo secco, a k=k\* e' cio' che rende k\* +stimabile. La domanda esatta: *esiste una put a delta basso il cui costo annuo, misurato sulle +quote VERE (non su DVOL), sia minore del guadagno di crescita che il pavimento autorizza?* +⚠️ **Prior onesto: sfavorevole.** Il 30/07 ha misurato che l'ala comprata costa **~2,3x il +modello** (f_long 2,23, fino a 5,85 sui delta piu' bassi) β€” quindi la protezione reale e' piu' cara +di quella che aveva gia' bocciato OPT06/OPT07. **Ma il confronto non e' mai stato fatto contro la +leva**, ed e' l'unico confronto in cui una protezione cara puo' comunque pagare. +⚠️ Eseguibile solo da ~$3-5k (ETH_USDC ~$250/lotto di nozionale): **e' una domanda per il deposito, +non per oggi.** + +### 3.6 β€” Il flusso di opzioni (volume per strike), che e' nel dato e nessuno ha guardato +La catena registra `volume_24h` **per strumento**, oraria. Sei filoni hanno usato l'**OI** +(posizioni accumulate) e **zero** hanno usato il **volume** (transazioni). ALT-OPTIONS ha appena +dimostrato che sulle famiglie lineari OI e negoziabilita' sono **anti-correlati (rho di rango +βˆ’0,77)** β€” che e' un'ottima ragione per sospettare che le due grandezze dicano cose diverse anche +sulle inverse. **Meccanismo:** un'ondata di acquisti concentrata su uno strike e' un fatto di +flusso; l'OI e' il suo integrale, e integrare distrugge il segnale di timing. +⚠️ **Muro invalicabile e dichiarato: 74-90 giorni di calendario.** Il verdetto massimo qui e' +**LEAD con gate pre-registrato**, e l'onesta' vuole che si dica prima di iniziare, non dopo. +La ragione per farlo comunque e' che **il calendario cresce da solo**, gratis, ogni ora. + +--- + +## 4. LA MISURA DI MAGGIOR VALORE MAI FATTA + +### La misura +**Il peggior giorno che il libro puo' avere, misurato invece che assunto: la distribuzione della +perdita giornaliera CONDIZIONATA all'esposizione piena, sotto la lente wick accoppiata, con lo +scenario di gap attraverso il disaster-SL.** + +### Perche' questa +Perche' e' **l'unico parametro che blocca la sola leva gratuita che l'ondata abbia trovato**, e +perche' oggi quel parametro e' **un numero scelto a mano**. + +GROWTH-POLICY ha stabilito che il gradino eseguibile `1,00x β†’ 1,25-1,50x` vale +**14,7 anni β†’ 12,9-11,6 anni** al capitale-rendita (a €500/mese, netto fisco). Lo scettico ha +tolto una delle tre riserve (il wick non amplifica il maxDD: ricarico costante ~3,5%). Delle due +rimaste, **una non e' risolvibile** (il drift stimato su 7,4 anni β€” e comunque k\* resta 7x a βˆ’2 SE, +quindi 1,5x sta al 21% di k\*) e **una lo e': la coda.** E' la riserva che decide, perche' e' l'unica +che porta k\* **sotto** il gradino proposto: *Β«un solo giorno βˆ’10% all'anno porta k\* da ~10x a 2x, +βˆ’15%/anno a 1xΒ»*. + +πŸ“Œ **Quel βˆ’10% non e' stato misurato: e' stato scelto.** E il controllo di cinque minuti in Β§3.1 +dice che potrebbe essere **una coda da buy&hold importata dentro un libro difensivo**: sui cinque +peggiori giorni di BTC e i cinque di ETH in 7,4 anni β€” compreso il **βˆ’41,5% / βˆ’45,7% del 12 marzo +2020** β€” il libro ha fatto **+0,97 Β· +0,72 Β· 0,00 Β· βˆ’1,98 Β· βˆ’2,24 Β· +0,23%**, e il suo peggior +giorno assoluto e' **βˆ’3,38%** in una data che non e' un crash. Se il vero peggior giorno +plausibile e' βˆ’5% e non βˆ’10%, k\* non e' 2x e **il gradino e' autorizzato**. + +⚠️ **Ma il mio controllo e' close-only, cioe' proprio la lente che questa ondata ha appena +dichiarato "esattamente cieca" sulle regole a UN giorno** (rapporto acc./close = INF a soglia 5%). +Quindi **la misura vera non e' quella che ho fatto**: e' la stessa domanda sotto +`r0725_prop_coupled` (tuple accoppiate, minimo intra-giorno) piu' lo scenario che il dataset non +contiene β€” un gap notturno che attraversa il disaster-SL βˆ’30% con posizione piena su entrambe le +gambe. Tutta la macchineria esiste gia' (`r0725_prop_coupled`, `r0822_leverage_skeptic`), il +campione di stress si puo' prendere dal dataset dei futures datati di Β§3.4, e il costo e' +**un agente di questa ondata**. + +### Confronto esplicito con le leve gia' quantificate +| leva | valore | costo | stato | +|---|---|---|---| +| versare €250/mese invece di €0 | da **mai** a 19,8a (P 52% netto fisco) | €250/mese | decisa dall'operatore | +| versare €500 invece di €250 | 19,8a β†’ **14,7a** | €250/mese in piu' | decisa dall'operatore | +| **leva 1,00x β†’ 1,50x** | **14,7a β†’ 11,6a** | **zero** | **bloccata da UN parametro assunto** | +| riallocare per la barriera (prop) | +0,016 di J = **il 4%** | analisi | misurata, marginale | +| XS01 su conto funded | +0,400 di J | **assunzione non verificata (Β§1.1)** | LEAD | +| ottimizzare il peso di SKH01 | **+0,030 di Sharpe** | analisi | gate fallito, plateau | +| rischio venue a p=5% | capitale mediano β†’ **zero** | split di conto | decisa: 100% Deribit fino a $20k | + +πŸ“Œ **Il gradino di leva vale ~3,1 anni, cioe' l'equivalente di ~€300/mese di versamenti, a costo +zero.** E' la prima volta in due mesi che una misura di ricerca entra nell'ordine di grandezza +della leva dei versamenti, e ci entra perche' **non e' ricerca di alpha: e' un cambio di scala di +cio' che gia' funziona.** Vale **100x** l'ottimizzazione del peso di SKH01 e **25x** il riallocare +per la barriera. + +### E adesso la parte che il mandato chiede di dire se e' vera +**SΓ¬: nessuna misura di ricerca di questa ondata batte "versare".** Venti filoni, ~15.300 righe, +**zero candidati promossi**; il soffitto direzionale e' stato riconfermato per via aritmetica; e +per **tre volte** l'eseguibilita' a $600 non e' stata il vincolo binding β€” tutto e' morto +sull'**edge**. La misura che propongo **non e' un'eccezione a questo, ne e' la conferma**: non +cerca un rendimento nuovo, **prezza un parametro di rischio per poter usare meglio quello che c'e'**. +E anche cosi', 3,1 anni sono **meno** di cio' che valgono €250/mese di differenza nei versamenti. + +**La seconda misura, e va detta insieme alla prima perche' costa ancora meno:** la verifica di +Β§1.1 (i 19 alt HL sono listati e shortabili presso la firm?). Non e' ricerca, e' **un pomeriggio di +lettura sul sito di due firm** β€” e decide se il canale prop, l'unico che moltiplica il nozionale +senza possedere capitale, vale J 0,738 o J 0,335. **Il progetto ha gia' pagato una volta il prezzo +di non fare questa verifica** (GTAA01/PRIIPs, cinque settimane di misure su un ETF che il broker +rifiuta di vendere) e ha scritto la regola per non ripeterlo. **Farla prima di aprire un altro +filone prop e' la cosa piu' economica che ci sia in questo elenco.** + +**In una riga:** *misurare la coda invece di assumerla* e' la ricerca che vale di piu' (3,1 anni, +gratis); *leggere il listino di una prop firm* e' la verifica che vale di piu' (un canale intero, +un pomeriggio); e **nessuna delle due batte i versamenti**, che restano il vincolo binding come il +26/07 aveva gia' stabilito. + +--- + +## 5. AUTOCRITICA DELL'ONDATA + +### 5.1 β€” 🚨 Un filone su venti non e' nel ledger, ed e' quello con il risultato eseguibile oggi +**ALT-OPTIONS (filone 14, `r0822_alt_options.py`, 709 righe) non ha ne' una riga di tabella ne' +una sezione in `RESULTS-0822.md`.** [VERIFICATO: 19 sezioni `###`, 18 righe di tabella, nessuna +per il 14 β€” e la tabella ha anche un difetto di markdown che **fonde le righe 9 e 4 su una sola +linea** (`... = TP01 travestito || 4 | DEALER-GAMMA | ...`).] Il filone e' citato **due volte** da +un altro filone (*Β«vedi filone 14Β»*, *Β«ETH_USDC resta la gamba giusta ... (filone 14)Β»*) e serve +al coordinatore per **ritirare una propria inferenza sbagliata** sull'OI β€” ma il lettore non trova +la fonte da nessuna parte. + +Nel diario compare come **due parole** dentro l'elenco dei morti: *Β«alt-options (alt)Β»*. **La +sintesi e' sbagliata due volte:** il filone non muore Β«perche' altΒ» (la sua conclusione e' che +**SOL/XRP/HYPE/AVAX sono i peggiori**: SOL_USDC ha il **9%** dei put quotati a due lati e +`f_venue` **negativo**), e cio' che trova e' che **ETH_USDC** β€” non un alt β€” e' la gamba con +`f_venue = 0,980`, spread della corta al **2,6%**, **max-loss $16,62/lotto** e **3.000 lotti al +best bid**. Cioe' Β§1.7: **la quarta soglia pubblicata falsificata dall'ondata**, quella che tiene +VRP01 fuori per taglia β€” e il diario ne annuncia **tre**. + +**Lezione:** il ledger e' stato scritto *man mano* (lo dice la sua prima riga) e un filone che +finisce mentre si scrive la sintesi puo' sparire senza che nulla lo segnali. Serve una guardia +banale: **contare i file `r0822_*.py` e le sezioni del ledger, e non chiudere l'ondata se non +combaciano.** E' lo stesso principio che l'ondata ha appena raccomandato per i monitor. + +### 5.2 β€” Il gate `implausible_sharpe` non e' stato girato dove serviva, per la seconda volta +MONITOR-AUDIT lo osserva bene: *Β«`implausible_sharpe` esiste dal 26/07 e non e' mai stato puntato +sulle serie forwardΒ»* (avrebbe segnalato Sharpe βˆ’15,8 su 28 barre). Ma la stessa mancanza si +ripete **dentro l'ondata**: il gate non compare in PROP-ALLOC (dove J e P(pass) sono costruiti su +percorsi bootstrap, non su una serie reale β€” legittimo, ma va dichiarato) ne' in GROWTH-POLICY, il +cui output include un **capitale mediano di $4,4e12** β€” che e' letteralmente la firma per cui il +gate e' stato scritto. L'agente ha fatto il controllo di plausibilita' **a mano** e l'ha riportato +con onesta'; **il gate del progetto non e' stato invocato.** Un gate che si applica a mano e' un +gate che un giorno non si applichera'. + +### 5.3 β€” Due Β«difetti di datoΒ» diagnosticati per ricostruzione con la sorgente a due minuti di distanza +E' il tema di Β§1.2 e Β§1.3, e vale come **critica di metodo dell'ondata**, non dei due agenti. Il +progetto ha codificato il 07/08: *Β«una fonte normativa citata in un commento di codice si verifica +come un numeroΒ»* β€” nata da una legge citata male in 5 file per settimane. Il 22/08 due filoni +dichiarano un difetto in un dato prodotto da `/opt/docker/cerbero-mcp` (**acceso, sulla stessa +macchina, e per giunta gia' usato da PythagorasGoal per Hyperliquid**) e da +`Adriano/Cerbero-Bite` (su Gitea, ultimo commit di dismissione), **senza aprire ne' l'uno ne' +l'altro**. Io li ho aperti entrambi in **meno di dieci minuti**. Il brief e' complice: elenca le +dieci trappole statistiche e **non dice Β«prima di dichiarare un difetto di dato, leggi il codice +che quel dato lo produceΒ»**. E' la regola nuova piu' economica che questa ondata possa lasciare. + +### 5.4 β€” Il coordinatore ha lasciato passare una conclusione debole, e ne ha corretta una propria +**Corretta (a merito):** l'inferenza *Β«BTC_USDC e' praticamente mortoΒ»*, ritirata dopo la misura di +ALT-OPTIONS, con la ragione scritta accanto. E' il comportamento giusto. +**Lasciata passare:** la lezione di ORTHO-SCREEN (Β§2.1). E' etichettata *Β«il risultato piu' +riusabile dell'ondataΒ»* e *Β«non e' sfortuna, e' aritmeticaΒ»* β€” due formule che chiudono la +discussione β€” mentre la sua meta' operativa (*Β«dichiarare famiglie molto piu' piccole in +anticipoΒ»*) e' **la manovra che il progetto ha proibito il 30/07** e la sua meta' quantitativa +dipende da una variabile (la dispersione delle celle) **mai nominata**. Il segnale che avrebbe +dovuto fermarla c'era ed era **nello stesso ledger, sette sezioni piu' avanti**: il DSR vacuo di +VOL-SIZE. **Due misure dello stesso gate, versi opposti, stesso giorno, nessun rimando.** +La sintesi di venti filoni ha un compito che nessun filone ha: **incrociare**. Qui non l'ha fatto, +e non l'ha fatto neanche su Β§2.4 (leva), Β§2.5 (peso SKH01), Β§1.5 (le due gambe del gate 23/10) e +Β§1.6 (k\* contro la partecipazione al nastro) β€” **cinque incroci disponibili a costo zero, tutti +fra coppie di filoni gia' consegnati.** + +### 5.5 β€” Agenti che hanno dichiarato Β«non girabileΒ» cio' che era girabile +- **FLOW-SQUEEZE**: *Β«liquidation_long/short: NO ... Nessuna fonte nostra la produce, e non c'e' + niente da produrreΒ»*. I due input della regola sono **nel file** (Β§1.3). Costo del mancato + lavoro: ~due ore. Il verdetto non sarebbe cambiato; la **motivazione** si'. +- **DEALER-GAMMA**: *Β«Quale [convenzione], lo decide la sezione 2 (ricostruzione dalla catena), + non ioΒ»* β€” decidibile leggendo, in due minuti (Β§1.2). +- **PROP-ALLOC**: la banda d'ancora dichiarata *Β«non girata, fuori budgetΒ»*. Onesto, e la + distorsione e' dichiarata a proprio sfavore. **Ma e' l'unica distorsione dichiarata che punta + nella direzione della raccomandazione**, sull'unico filone che raccomanda qualcosa: dovrebbe + essere il primo pezzo di budget di un seguito, non l'ultimo. +- **TERM-STRUCTURE**: *Β«`deflated_sharpe` NON girato, e la motivazione e' il risultatoΒ»* β€” questo + invece e' **corretto e da imitare**: con 74 osservazioni l'incertezza di campione domina di un + ordine di grandezza quella da selezione, e deflazionare 312 trial avrebbe dato *Β«un numero + preciso e privo di sensoΒ»*. **E' anche la migliore descrizione, scritta senza saperlo, del + problema di Β§2.1.** + +### 5.6 β€” Cosa l'ondata ha fatto meglio della media del progetto (perche' un'autocritica che elenca +solo difetti non e' calibrata) +Sette agenti hanno catturato un **errore proprio** prima di pubblicare; tre hanno **refutato una +propria ipotesi in corsa**; uno scettico ha **ritirato una regola pubblicata poche ore prima** +provando che il difetto non esisteva β€” e la regola ritirata era **sua di famiglia**. Quattro +filoni hanno dichiarato la **potenza prima di guardare** (OI-PIN, FLOW-SQUEEZE, SLIP-AUDIT, +VOL-SIZE) e in tre casi il candidato e' morto **contro la propria soglia dichiarata**, che e' il +modo piu' pulito di morire. Le repliche **bit-exact prima di ogni delta** sono ormai +sistematiche (5/5 in XS-LITE e HL-EXEC, `max|diff| = 0,0`). **Il metodo dei singoli filoni non e' +il problema di questa ondata: la cucitura lo e'.** + +--- + +## Appendice β€” cosa ho verificato in questa sessione, e come + +| # | fatto | come | +|---|---|---| +| A | `dealer_net_gamma` = convenzione **dichiarata** (dealer short calls / long puts) nel sorgente di `cerbero-mcp`, non un difetto | letto `/opt/docker/cerbero-mcp/src/cerbero_mcp/common/options.py:11-14, 138-140` | +| B | bite **inoltra** il campo e non lo calcola; chiama con `top_n_strikes=50` di default | clone `Adriano/Cerbero-Bite`, `runtime/market_snapshot_cycle.py:85`, `clients/deribit.py:342-384` | +| C | `liquidation_*_risk` = soglia su `oi_delta_24h` + funding, e **i due input sono nel parquet** (copertura 99%, `oi_delta_pct_4h` std 1,23%) | `sentiment/fetchers.py:480-518` + lettura di `cb_market_snapshots.parquet` | +| D | `sr0` del DSR = `sd(Sharpe dei trial) x mult(N)`; con sd=0,58, `sr0` passa da **1,57 (N=168)** a **1,15 (N=24)** | letto `altlib.deflated_sharpe:689-712`, moltiplicatori ricalcolati | +| E | il libro 75/25 fa **+0,97%** il 12/03/2020 (BTC βˆ’41,5%, ETH βˆ’45,7%); peggior giorno **βˆ’3,38%**, non un crash | `altlib.tp01_baseline_daily()` + `sleeves._skyhook_returns()`, close-only | +| F | `collect_chain.py` interroga `{"currency": asset}` e filtra `OI_MIN = 100.0` | `scripts/live/collect_chain.py:57, 114, 181` | +| G | il feed certificato e' `BTC/USD:BTC` (**inverse**), il libro esegue `BTC_USDC-PERPETUAL` (**lineare**) | `rebuild_history.DERIBIT_INSTR:48` vs `src/live/deribit.INSTRUMENT:110` | +| H | ALT-OPTIONS: ETH_USDC `f_venue` 0,980 / max-loss **$16,62** / 7 strutture al 20% di $635 | output del filone in scratchpad | +| I | il dataset dei futures datati (**20 MB**) vive solo in `scratchpad/basis_cache/` | `du -sh` | +| J | `RESULTS-0822.md`: 19 sezioni, 18 righe di tabella, **filone 14 assente**, righe 9 e 4 fuse | conteggio diretto | + +⚠️ **Su (G):** e' un fatto, non un'accusa. `rebuild_history` lo dichiara in un commento +(*Β«inverse, storia lunga ~ lineare entro 3 bpsΒ»*) e SLIP-AUDIT ha misurato la base ai 18 fill +(BTC βˆ’0,03 bps, ETH βˆ’0,27) β€” trascurabile sui **ritorni**, che dipendono dalle *variazioni* della +base. Lo segno solo perche' e' la **5Βͺ occorrenza nell'ondata** dello schema Β«un controllo puntato +su uno strumento diverso da quello che si tradaΒ» (dopo `fee_watch` 21/08, il collettore della +catena, il feed dello slippage e la taratura di `venue_watch`), e perche' la parte non verificata +di quel commento β€” *Β«~ lineare entro 3 bpsΒ»* su **tutta la storia** β€” non e' mai stata misurata: +esiste una misura su 18 istanti, non sulle 60.000 ore in cui entrambi gli strumenti esistono. +Costo di chiuderla: un'ora. + +--- + +**Book, pesi, cron, config: questo filone non tocca niente.** Consegna un'analisi. +Le uniche azioni che raccomanda sono a costo quasi nullo e tutte su documenti o verifiche: +correggere Β§1.2 nel ledger, aggiungere la sezione mancante di Β§5.1, dichiarare la dispersione +accanto a ogni DSR (Β§2.1), ri-registrare il gate del 23/10 prima di rigenerare le serie (Β§1.4-1.5), +persistere il dataset dei futures (Β§3.4), e leggere il listino di una prop firm prima di +misurare un'altra volta il canale che ne dipende (Β§1.1). diff --git a/scripts/research/r0822b_depeg.py b/scripts/research/r0822b_depeg.py new file mode 100644 index 0000000..d9dd013 --- /dev/null +++ b/scripts/research/r0822b_depeg.py @@ -0,0 +1,811 @@ +"""r0822b_depeg.py β€” IL FALSIFICATORE DI VENUE PER XS01-OOS (2026-08-22, ondata wave-0822). + +IL PUNTO FRAGILE, DICHIARATO DALL'AUTORE DEL RISULTATO +----------------------------------------------------- +`r0822b_xs01_oos.py` (RESULTS-0822 Β§21) ha misurato che il meccanismo congelato di XS01 ha un edge +FUORI dalla sua finestra di scoperta β€” 2021-2023 su **Binance spot USDT** β€” e che li' e' PIU' GRANDE +(mediana di fase +1,12 contro +0,37, differenza appaiata +0,668 in 10/10 fasi). Quel risultato +riscrive il gate del canale funded, quindi merita il suo falsificatore. + +Lo stesso autore lo ha nominato: + > "La validazione di venue Binance-vs-Hyperliquid gira SOLO sulla finestra comune 2024+, cioe' + > proprio il periodo SENZA depeg USDT. Il fuori campione contiene il depeg di maggio 2022. Se i + > close Binance/USDT 2021-2023 divergessero da un venue USD in modo NON COMUNE fra gli asset, il + > ranking cambierebbe e il risultato con esso. Test che lo deciderebbe: rifare il meccanismo sui + > close Coinbase USD per il sottoinsieme quotato li', 2021-2023, e chiedere corr fra i due sleeve + > >= 0.99 come sul 2024+." + +Questo script esegue esattamente quel test. + +PERCHE' LA DOMANDA NON E' OZIOSA (il meccanismo del danno, prima dei numeri) +--------------------------------------------------------------------------- +La regola del progetto β€” "Binance non e' la verita', e' USDT, ~10 bps fuori, e sotto depeg fino al +3% off" β€” e' stata **correttamente** giudicata non-binding da Β§21: uno z-score cross-sezionale +sottrae la media di sezione, quindi e' invariante a un fattore **COMUNE**. Ma un depeg non e' +garantito comune: nei giorni di stress la liquidita' si sposta in modo diverso per asset (i major +restano scambiati contro USDT, i minori no; i flussi di fuga passano per coppie diverse), e una +divergenza **IDIOSINCRATICA** cambia il RANKING β€” che e' esattamente cio' che la strategia legge. +Maggio 2022 (UST/LUNA, con USDT sceso a ~0,95) e' il caso peggiore possibile. +Percio' la divergenza va misurata in DUE pezzi, e sono due domande diverse: + (a) COMUNE = mediana per giorno dello scarto fra i venue -> quella che lo z-score annulla; + (b) IDIOSINCRATICA = scarto per asset DOPO aver tolto la mediana del giorno -> quella che uccide. +Riportare solo (a) darebbe la risposta rassicurante e sbagliata. + +IL VINCOLO DEL DATO, E COME E' STATO RISOLTO (dichiarato PRIMA di misurare) +-------------------------------------------------------------------------- +Su Coinbase USD, dei 13 asset di U13: + BNB -> quotato solo dal 2025-10 = FUORI (nessun venue USD ce l'ha nel 2021-23: e' l'asset + nativo del venue che stiamo mettendo in discussione) + XRP -> SOSPESO su Coinbase dal 2021-01-19 al 2023-07 (causa SEC) = buco proprio sulla finestra + NEAR/INJ -> quotati su Coinbase solo dal 2022-09 + AVAX -> dal 2021-09-30 (il vincolo che decide l'inizio della finestra) +XRP e' recuperato da **Bitstamp USD** (l'altra referenza USD del `venue_watch` del progetto, che +non ha mai sospeso la coppia). Restano fuori BNB, NEAR, INJ. +Ne escono DUE test, e il secondo copre cio' che il primo non copre: + + TEST 1 "U10-USD" = universo FISSO di 10 gambe (BTC ETH LTC LINK AAVE ADA DOGE SOL AVAX + XRP), + 2021-09-30 -> 2023-12-31, meccanismo **CONGELATO** (k=5, min_assets=10: + dieci gambe sono ESATTAMENTE il minimo, nessun parametro toccato). + Contiene il depeg di maggio 2022 e FTX. NON contiene il 2021. + TEST 2 "U13-IBRIDO" = i 13 asset e la finestra INTERA 2021-01 -> 2023-12, con il close USD + sostituito a quello USDT **dove esiste** (9-12 gambe su 13 a seconda della + data) e Binance dove non esiste. Non e' un confronto puro di venue, e' + la domanda operativa: *se sostituisco tutto il prezzo USD che esiste, il + risultato di Β§21 si muove?* +In entrambi i test il confronto e' **APPAIATO**: stesso sottoinsieme, stessa finestra, stesse 10 +fasi, stesso codice del motore. Confrontare "19 gambe su Binance" con "10 su Coinbase" misurerebbe +l'UNIVERSO, non il venue (regola del progetto: un confronto appaiato eredita TUTTI i parametri +della riga in cui compare) β€” e per questo il TEST 1 riporta anche cosa fa U10 **su Binance** +rispetto a U13 su Binance, cioe' quanto del titolo di Β§21 questo sottoinsieme riproduce. + +MECCANISMO: CONGELATO. Il motore e' importato da `r0822b_xs01_oos.py` (che a sua volta replica +`sleeves._xsec_returns` a max|diff| = 0.0). Nessuna riga di strategia riscritta qui: se il motore +fosse riscritto, una differenza di venue e una differenza di implementazione sarebbero +indistinguibili. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822b_depeg.py +Rete: sola lettura, <= 2 req/s, MAI nella finestra :25-:29 (rate limit per-IP condiviso col +collettore della catena opzioni; guasto del 29/07). Download in cache nello scratchpad: la +seconda corsa e' offline. +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import altlib as A # noqa: E402 +import r0822b_xs01_oos as X # noqa: E402 (motore CONGELATO + cache Binance) +from src.portfolio.sleeves import XS_CFG # noqa: E402 + +CACHE = X.CACHE.parent / "r0822b_depeg" +CACHE.mkdir(parents=True, exist_ok=True) + +# --------------------------------------------------------------------------- +# UNIVERSI E FINESTRE β€” dichiarati PRIMA di guardare i numeri +# --------------------------------------------------------------------------- +U13 = X.U13 # l'universo di Β§21 +# venue USD per asset. `None` = nessun venue USD con storia sulla finestra. +USD_SRC: dict[str, tuple[str, str] | None] = { + "BTC": ("coinbase", "BTC/USD"), "ETH": ("coinbase", "ETH/USD"), + "LTC": ("coinbase", "LTC/USD"), "LINK": ("coinbase", "LINK/USD"), + "AAVE": ("coinbase", "AAVE/USD"), "ADA": ("coinbase", "ADA/USD"), + "DOGE": ("coinbase", "DOGE/USD"), "SOL": ("coinbase", "SOL/USD"), + "AVAX": ("coinbase", "AVAX/USD"), "NEAR": ("coinbase", "NEAR/USD"), + "INJ": ("coinbase", "INJ/USD"), + "XRP": ("bitstamp", "XRP/USD"), # Coinbase l'ha sospeso 2021-01 -> 2023-07 + "BNB": None, # nessun venue USD prima del 2025-10 +} +U10 = ["BTC", "ETH", "LTC", "LINK", "AAVE", "ADA", "DOGE", "SOL", "AVAX", "XRP"] +T1_START, T1_END = "2021-09-30", "2024-01-01" # AVAX su Coinbase e' il vincolo +T2_START, T2_END = "2021-01-01", "2024-01-01" # la finestra fuori campione di Β§21 +DEPEG = ("2022-05-07", "2022-05-21") # UST/LUNA + USDT a ~0.95 (estremo incluso) +PHASES = list(range(XS_CFG["H"])) # 10 fasi: la stima onesta e' la MEDIANA +CORR_SOGLIA = 0.99 # soglia DICHIARATA DALL'AUTORE, non da me + + +# =========================================================================== +# 1. FETCH USD (Coinbase / Bitstamp) + certificazione locale +# =========================================================================== +def _fetch_paged(ex, market: str, tf: str, start_ms: int, end_ms: int, + step_bars: int, bar_ms: int) -> list: + """Paginazione DETERMINISTICA sulla finestra: avanza anche quando una pagina torna vuota. + + ⚠️ Non e' un dettaglio: XRP su Coinbase ha un buco di 2,5 anni (sospensione SEC) e un loop che + si ferma alla prima pagina vuota β€” il modo ovvio di scrivere questo ciclo β€” troncherebbe la + serie li', producendo un 'asset che non esiste' invece di un 'asset con un buco'. Sono due + fatti diversi e portano a due decisioni diverse. + """ + rows, cur, span = [], start_ms, step_bars * bar_ms + while cur < end_ms: + X._outside_forbidden_window() + batch = [] + for attempt in range(3): + try: + batch = ex.fetch_ohlcv(market, tf, since=cur, limit=step_bars) + break + except Exception as e: # noqa: BLE001 + if attempt == 2: + print(f" [rete] {market} @ {cur}: {type(e).__name__} {str(e)[:70]}") + time.sleep(2 ** attempt) + time.sleep(0.55) # <= 2 req/s + rows += [b for b in (batch or []) if cur <= b[0] < cur + span] + cur += span + return rows + + +def fetch_usd_daily(asset: str) -> pd.DataFrame: + """Close giornalieri USD dal venue dichiarato in USD_SRC. Cache su disco.""" + src = USD_SRC.get(asset) + if src is None: + return pd.DataFrame(columns=["timestamp", "close", "volume"]) + venue, market = src + f = CACHE / f"{venue[:2]}_{asset.lower()}_1d.parquet" + if f.exists(): + return pd.read_parquet(f) + import ccxt + ex = getattr(ccxt, venue)({"enableRateLimit": True}) + ex.load_markets() + if market not in ex.markets: + d = pd.DataFrame(columns=["timestamp", "open", "high", "low", "close", "volume"]) + d.to_parquet(f) + return d + start = ex.parse8601("2019-01-01T00:00:00Z") + end = int(time.time() * 1000) + step = 300 if venue == "coinbase" else 1000 + rows = _fetch_paged(ex, market, "1d", start, end, step, 86_400_000) + d = (pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"]) + .drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True)) + d.to_parquet(f) + return d + + +def series_of(d: pd.DataFrame) -> pd.Series: + if not len(d): + # indice tz-aware anche da vuoto: un asset senza venue USD (BNB) deve poter essere + # confrontato/reindicizzato come gli altri, non far cadere lo script + return pd.Series(dtype=float, index=pd.DatetimeIndex([], tz="UTC")) + return pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + + +def certify_usd(asset: str, d: pd.DataFrame, a: str, b: str) -> dict: + """Stessa forma della certificazione di Β§21, ma valutata SULLA FINESTRA DEL TEST. + + Certificare "tutta la serie" direbbe la cosa sbagliata: XRP su Coinbase ha 900 giorni di buco + che sulla finestra 2021-09/2023-12 sono TUTTI dentro. Il numero utile e' la copertura della + finestra in cui il meccanismo gira, non della serie. + """ + s = series_of(d) + s = s[(s.index >= pd.Timestamp(a, tz="UTC")) & (s.index < pd.Timestamp(b, tz="UTC"))] + if not len(s): + return dict(sym=asset, n=0, ok=False, start="-", end="-", gap=-1, + flat=float("nan"), zero_vol=float("nan")) + v = series_of(d.assign(close=d["volume"])) + v = v[(v.index >= pd.Timestamp(a, tz="UTC")) & (v.index < pd.Timestamp(b, tz="UTC"))] + # ⚠️ la copertura si misura sulla finestra RICHIESTA, non sulla prima barra disponibile: + # contando dal proprio inizio, un asset quotato a meta' finestra esce con gap=0 e "OK", e il + # conteggio degli usabili contraddirebbe la riga sotto che lo esclude. E' lo stesso difetto + # che il progetto ha gia' incontrato due volte (serie TRONCATA che passa ogni controllo + # interno): un controllo che guarda solo DENTRO la serie non vede cio' che la serie non copre. + attesi = (pd.Timestamp(b, tz="UTC") - pd.Timestamp(a, tz="UTC")).days + c = s.values + flat = float(np.mean(c[1:] == c[:-1])) if len(c) > 1 else float("nan") + zv = float(np.mean(v.values == 0.0)) if len(v) else float("nan") + gap = int(attesi - len(s)) + return dict(sym=asset, n=len(s), start=str(s.index.min().date()), + end=str(s.index.max().date()), gap=gap, flat=flat, zero_vol=zv, + ok=bool(gap == 0 and flat < 0.02 and (not np.isfinite(zv) or zv < 0.01))) + + +# =========================================================================== +# 2. pannelli +# =========================================================================== +def bin_panel(syms: list[str]) -> pd.DataFrame: + """Pannello Binance USDT β€” dalla cache di Β§21, cosi' il lato USDT del confronto e' BIT-IDENTICO + a quello che ha prodotto il risultato che sto falsificando.""" + return pd.concat({s: series_of(X.fetch_binance_daily(s)) for s in syms}, axis=1).sort_index() + + +def usd_panel(syms: list[str]) -> pd.DataFrame: + return pd.concat({s: series_of(fetch_usd_daily(s)) for s in syms}, axis=1).sort_index() + + +def splice(usd: pd.Series, usdt: pd.Series) -> pd.Series: + """Serie IBRIDA: USD dove esiste, USDT prima β€” RI-SCALANDO il tratto USDT al livello USD nel + giorno del giunto. + + ⚠️ Senza la ri-scalatura il giunto crea un rendimento fantasma pari allo scarto fra i venue + (pochi bps, ma esattamente il tipo di artefatto che questo script deve misurare, non fabbricare). + Il meccanismo legge RAPPORTI di prezzo a 30 e 90 giorni: dopo la ri-scalatura la serie e' + continua e i rapporti che attraversano il giunto sono ben definiti. + """ + u = usd.dropna() + if not len(u): + return usdt + t0 = u.index[0] + if t0 not in usdt.index or not np.isfinite(usdt.get(t0, np.nan)): + return usd.reindex(usdt.index) + k = float(u.iloc[0]) / float(usdt.loc[t0]) + pre = usdt[usdt.index < t0] * k + return pd.concat([pre, u]).sort_index().reindex(usdt.index) + + +def hybrid_panel(syms: list[str]) -> tuple[pd.DataFrame, dict]: + B, U = bin_panel(syms), usd_panel(syms) + cols, meta = {}, {} + for s in syms: + u = U[s] if s in U.columns else pd.Series(dtype=float) + u = u.reindex(B.index) + cols[s] = splice(u, B[s]) + cov = u.dropna() + meta[s] = (str(cov.index[0].date()) if len(cov) else "mai") + return pd.concat(cols, axis=1).sort_index(), meta + + +# =========================================================================== +# 3. metriche e allineamento appaiato +# =========================================================================== +sh, dd, cagr, win, alive, line = X.sh, X.dd, X.cagr, X.win, X.alive, X.line + + +def pair(a: pd.Series, b: pd.Series) -> tuple[pd.Series, pd.Series]: + """Allinea due sleeve sullo stesso indice: un confronto di venue non puo' confrontare finestre.""" + j = pd.concat({"a": a, "b": b}, axis=1, join="inner").dropna() + return j["a"], j["b"] + + +def ranking_scan(PB: pd.DataFrame, PU: pd.DataFrame, phases: list[int], + label: str) -> pd.DataFrame: + """IL CANALE DEL DANNO, misurato dove passa: il venue puo' far male SOLO cambiando le gambe + scelte. Scansione di TUTTE le fasi (non solo la canonica: un ribaltamento su una fase sola + sarebbe invisibile all'ancora 0) con self-check dello specchio contro il motore.""" + from scipy.stats import spearmanr + lb = XS_CFG["lookbacks"] + mlb, H, k = max(lb), XS_CFG["H"], XS_CFG["k"] + rows, sp, checked, tot = [], [], 0, 0 + for ph in phases: + det: dict = {} + X.xsec_engine(PB, phase=ph, detail=det) + WB = det["W"] + for i in range(mlb, len(PB)): + if i % H != ph: + continue + iB, sB = score_at(PB, i, lb) + iU, sU = score_at(PU, i, lb) + if len(iB) < 10 or len(iU) < 10: + continue + tot += 1 + oB = np.argsort(sB) + loB, hiB = set(iB[oB[:k]]), set(iB[oB[-k:]]) + mh, ml = set(np.flatnonzero(WB[i] > 0)), set(np.flatnonzero(WB[i] < 0)) + if mh or ml: # self-check solo dove il motore ha una posizione + checked += 1 + if (mh, ml) != (hiB, loB): + raise AssertionError(f"specchio != motore ({label}, fase {ph}, i={i}): " + "misurerei la mia riscrittura invece del venue") + oU = np.argsort(sU) + loU, hiU = set(iU[oU[:k]]), set(iU[oU[-k:]]) + ov = (len(hiB & hiU) + len(loB & loU)) / (2 * k) + sp.append(float(spearmanr(sB, sU).statistic)) + if ov < 1.0: + diff = sorted(set(PB.columns[list(hiB ^ hiU)]) | set(PB.columns[list(loB ^ loU)])) + rows.append(dict(fase=ph, data=PB.index[i], overlap=ov, gambe=",".join(diff))) + D = pd.DataFrame(rows, columns=["fase", "data", "overlap", "gambe"]) + print(f" [{label}] {tot} ribilanciamenti su {len(phases)} fasi; specchio verificato " + f"contro il motore in {checked}/{checked} con posizione") + print(f" [{label}] Spearman dello score fra i venue: mediana {np.median(sp):.4f} " + f"min {np.min(sp):.4f}") + print(f" [{label}] ribilanciamenti con gambe DIVERSE: {len(D)}/{tot} " + f"({len(D)/max(tot,1)*100:.2f}%)") + if len(D): + pre = D[D["data"] < pd.Timestamp(T2_END, tz="UTC")] + print(f" [{label}] di cui NELLA finestra fuori campione ({T2_START}..{T2_END}): " + f"{len(pre)} β€” elencati qui sotto; quelli dal 2024 in poi non sono la domanda") + for _, r in pre.iterrows(): + print(f" fase {int(r['fase'])} {r['data'].date()} " + f"sovrapposizione {r['overlap']*100:3.0f}% gambe scambiate: {r['gambe']}") + return D + + +def gate_flips(PB: pd.DataFrame, PU: pd.DataFrame, ph: int) -> list[tuple]: + """Il SECONDO canale, che una scansione di ranking non vede: il gate di dispersione e' BINARIO + (`disp_i >= percentile espandente`) e quindi DISCONTINUO β€” due prezzi che differiscono di + pochi bps possono cadere ai due lati della soglia, e allora uno sleeve resta pienamente + investito per 10 giorni mentre l'altro sta FLAT. Non e' un cambio di ranking: e' un + interruttore. Ritorna gli intervalli in cui i due venue non sono d'accordo su acceso/spento.""" + dB: dict = {} + dU: dict = {} + X.xsec_engine(PB, phase=ph, detail=dB) + X.xsec_engine(PU, phase=ph, detail=dU) + gb = np.abs(dB["W"]).sum(axis=1) > 1e-9 + gu = np.abs(dU["W"]).sum(axis=1) > 1e-9 + idx = PB.index + flip = np.flatnonzero(gb != gu) + runs, i0 = [], None + for j, i in enumerate(flip): + if i0 is None: + i0 = i + if j + 1 == len(flip) or flip[j + 1] != i + 1: + runs.append((idx[i0], idx[i], int(i - i0 + 1), bool(gb[i0]))) + i0 = None + return runs + + +def autopsia(PB: pd.DataFrame, PU: pd.DataFrame, ph: int, D: pd.DataFrame, label: str) -> None: + """La fase con la corr piu' bassa NON si riassume, si guarda: una corr di 0,97 o e' un + ribaltamento di ranking (il guasto che sto cercando) o e' un singolo giorno di rumore su una + gamba sottile (che non e' un guasto). Sono conclusioni opposte a partire dallo stesso numero.""" + a, b = pair(alive(X.xsec_engine(PU, phase=ph)), alive(X.xsec_engine(PB, phase=ph))) + g = (a - b) + top = g.abs().sort_values(ascending=False).head(4) + dph = D[D["fase"] == ph] if len(D) else D + runs = gate_flips(PB, PU, ph) + print(f" autopsia della fase peggiore ({label}, fase {ph}): i 4 giorni di scarto maggiore") + for t, v in top.items(): + prev = dph[dph["data"] <= t]["data"] + near = (t - prev.iloc[-1]).days if len(prev) else None + inflip = [r for r in runs if r[0] <= t <= r[1]] + # ⚠️ la coda: dopo una discordanza del gate uno dei due sleeve ha 10 giorni di ZERI in + # piu' nella finestra rv30 del vol-target -> la LEVA dei due resta diversa per ~30 giorni + # dopo che la discordanza e' finita. Chiamare "rumore di prezzo" un giorno dentro quella + # coda attribuirebbe al venue un effetto che e' della soglia binaria. + tail = [r for r in runs if r[1] < t and (t - r[1]).days <= 30] + if inflip: + r = inflip[0] + tag = (f"GATE DISCORDE {r[0].date()}..{r[1].date()}: " + f"{'USDT investito, USD flat' if r[3] else 'USD investito, USDT flat'}") + elif tail: + tag = (f"coda del vol-target: gate discorde fino al {tail[-1][1].date()}, " + f"{(t - tail[-1][1]).days}g fa -> rv30 diversa, LEVA diversa") + elif near is not None and near <= XS_CFG["H"]: + tag = f"ultimo ribilanciamento con gambe diverse {near} giorni prima" + else: + tag = "stesse gambe, stesso gate: solo rumore di prezzo" + print(f" {t.date()} scarto {g.loc[t]*1e4:+8.0f} bps ({tag})") + print(f" scarto quadratico totale della fase: {float((g**2).sum())**0.5*1e4:.0f} bps; " + f"i 4 giorni sopra ne spiegano " + f"{float((top**2).sum())/float((g**2).sum())*100:.0f}%") + + +def score_at(P: pd.DataFrame, i: int, lookbacks) -> tuple[np.ndarray, np.ndarray]: + """Specchio della riga di score del motore (z-score medio sui lookback). Serve per confrontare + i RANKING fra venue; la sezione [4] verifica che i top-k/bottom-k che ne derivano coincidano + ESATTAMENTE con quelli scelti dal motore β€” se lo specchio divergesse, ogni numero di ranking + misurerebbe la mia riscrittura invece del venue.""" + px = P.values.astype(float) + elig = np.isfinite(px[i]) + for L in lookbacks: + elig = elig & np.isfinite(px[i - L]) + idx = np.flatnonzero(elig) + sc, cnt = np.zeros(len(idx)), 0 + for L in lookbacks: + r = px[i, idx] / px[i - L, idx] - 1.0 + if r.std() > 0: + sc += (r - r.mean()) / r.std() + cnt += 1 + return idx, (sc / cnt if cnt else sc) + + +# =========================================================================== +def main() -> None: + t0 = time.time() + pd.set_option("display.width", 200) + print("=" * 100) + print("DEPEG β€” il close USDT 2021-2023 puo' aver cambiato il RANKING di XS01-OOS?") + print(f"XS_CFG (CONGELATO) = {XS_CFG} soglia dichiarata dall'autore: corr >= {CORR_SOGLIA}") + print("=" * 100) + + # ------------------------------------------------------------- [0] sanity + print("\n[0] SANITY β€” il motore importato e' quello che ha prodotto Β§21?") + from src.portfolio.sleeves import _xsec_returns + off = _xsec_returns() + rep = X.xsec_engine(X.hl_panel(), phase=0) + j = pd.concat({"o": off, "r": rep}, axis=1, join="inner").dropna() + md = float(np.max(np.abs(j["o"] - j["r"]))) + print(f" motore vs sleeve UFFICIALE su HL: n={len(j)} max|diff| = {md:.2e} " + f"-> {'IDENTICO' if md < 1e-12 else 'DIVERGE β€” STOP'}") + if md >= 1e-12: + return + + # ------------------------------------------------------- [1] dato USD + print("\n[1] DATO USD β€” quanti dei 13 asset di Β§21 hanno un prezzo in DOLLARI nel 2021-2023?") + print(f" {'sym':<6}{'venue USD':>11}{'n':>6}{'start':>12}{'end':>12}{'gap':>5}" + f"{'flat%':>8}{'vol0%':>8} (finestra {T1_START} .. {T1_END})") + certs = {} + for s in U13: + d = fetch_usd_daily(s) + c = certify_usd(s, d, T1_START, T1_END) + certs[s] = c + ven = USD_SRC[s][0] if USD_SRC[s] else "β€”" + flag = "" if c["ok"] else " <-- non usabile su questa finestra" + print(f" {s:<6}{ven:>11}{c['n']:>6}{c['start']:>12}{c['end']:>12}{c['gap']:>5}" + f"{c['flat']*100:>8.2f}{c['zero_vol']*100:>8.2f}{flag}") + usabili = [s for s in U13 if certs[s]["ok"]] + print(f" -> usabili sulla finestra del TEST 1: {len(usabili)}/13 {usabili}") + print(f" -> ESCLUSI: BNB (nessun venue USD prima del 2025-10 β€” e' l'asset nativo del venue") + print(f" sotto esame), NEAR e INJ (Coinbase solo dal 2022-09).") + print(f" ⚠️ LIMITE DEL TEST, da dire subito: 10 gambe su 13 e finestra che parte dal") + print(f" {T1_START} (AVAX su Coinbase) -> il TEST 1 NON copre il 2021, che in Β§21 e'") + print(f" l'anno piu' forte (+2,14). Copre pero' ESATTAMENTE cio' che l'obiezione nomina:") + print(f" il depeg di maggio 2022 e FTX. Il 2021 lo copre il TEST 2 (ibrido).") + + # controllo di ALLINEAMENTO: le barre USD sono sulla stessa griglia di quelle USDT? + print("\n Controllo di ALLINEAMENTO β€” se le barre USD e USDT fossero su griglie sfasate,") + print(" ogni numero sotto sarebbe uno sfasamento di calendario travestito da divergenza") + print(" di venue. corr dei RITORNI giornalieri, 2024+ (finestra dove esistono entrambi):") + B24 = bin_panel(U13) + U24 = usd_panel(U13) + al, shifted = [], [] + for s in U13: + if s not in U24.columns or not U24[s].notna().any(): + continue + k = pd.concat({"u": U24[s], "b": B24[s]}, axis=1, join="inner").dropna() + k = k[k.index >= pd.Timestamp("2024-01-01", tz="UTC")].pct_change().dropna() + if len(k) > 200: + al.append((s, float(k["u"].corr(k["b"])))) + shifted.append(float(k["u"].shift(1).corr(k["b"]))) # controllo POSITIVO + print(" " + " ".join(f"{s}:{c:.4f}" for s, c in al)) + lo_al = min(c for _, c in al) + # ⚠️ Un controllo che non fallisce mai non e' un controllo: il modo di fallire e' uno + # sfasamento di UNA barra, e va mostrato che il numero se ne accorge. Con la serie USD + # ritardata di un giorno la corr collassa: e' quella la scala del guasto che sto escludendo. + print(f" corr minima {lo_al:.4f} | controllo POSITIVO (USD ritardato di 1 giorno): " + f"corr mediana {np.median(shifted):+.4f}, massima {np.max(shifted):+.4f}") + if lo_al < 0.95: + print(" -> SFASAMENTO: i due venue non sono sulla stessa griglia. STOP.") + return + print(" -> allineati (uno sfasamento darebbe ~0, non 0,99). Gli asset sotto 0,999") + print(f" ({', '.join(s for s, c in al if c < 0.999)}) sono solo piu' RUMOROSI in USD") + print(" (book piu' sottile sul venue USD), non sfasati.") + + # --------------------------------------------- [2] la divergenza, in DUE pezzi + print("\n[2] LA DIVERGENZA USD-vs-USDT, scomposta β€” e il pezzo che conta e' il SECONDO") + Bp = bin_panel(U10).loc[T1_START:T1_END] + Up = usd_panel(U10).reindex(Bp.index) + both = Bp.notna() & Up.notna() + D = 1e4 * np.log(Up / Bp) # scarto di livello, bps + D = D.where(both) + common = D.median(axis=1) # (a) componente COMUNE (il peg + il venue) + idio = D.sub(common, axis=0) # (b) componente IDIOSINCRATICA (cambia il rango) + print(f" {len(Bp)} giorni, {len(U10)} asset, {int(both.values.sum())} osservazioni appaiate") + print(f" (a) COMUNE |mediana per giorno|: med {common.abs().median():7.1f} bps " + f"p95 {common.abs().quantile(.95):7.1f} max {common.abs().max():7.1f}") + print(f" (b) IDIOSINCRATICO |scarto de-medianato|: med {idio.abs().stack().median():7.1f} bps " + f"p95 {idio.abs().stack().quantile(.95):7.1f} max {idio.abs().stack().max():7.1f}") + print(" -> (a) e' cio' che lo z-score cross-sezionale annulla per costruzione; (b) no.") + + print(f"\n LA FINESTRA DEL DEPEG {DEPEG[0]} .. {DEPEG[1]} (UST/LUNA; USDT a ~0,95):") + dm = (D.index >= pd.Timestamp(DEPEG[0], tz="UTC")) & (D.index < pd.Timestamp(DEPEG[1], tz="UTC")) + print(f" {'giorno':<12}{'comune':>9}" + "".join(f"{s:>8}" for s in U10)) + for t in D.index[dm]: + print(f" {str(t.date()):<12}{common.loc[t]:>9.0f}" + + "".join(f"{idio.loc[t, s]:>8.0f}" if np.isfinite(idio.loc[t, s]) else f"{'β€”':>8}" + for s in U10)) + print(" (numeri in bps; colonna 'comune' = mediana del giorno, le altre = scarto DOPO") + print(" averla tolta, cioe' la parte che puo' spostare il ranking)") + idm = idio[dm].abs().stack() + print(f" idiosincratico NELLA finestra: med {idm.median():.1f} bps p95 " + f"{idm.quantile(.95):.1f} max {idm.max():.1f} " + f"(FUORI: med {idio[~dm].abs().stack().median():.1f}, " + f"p95 {idio[~dm].abs().stack().quantile(.95):.1f})") + + print("\n Ma il ranking non legge il LIVELLO: legge i rapporti a 30 e 90 giorni. Divergenza") + print(" idiosincratica degli STESSI rendimenti che entrano nello score (bps):") + for L in XS_CFG["lookbacks"]: + rB = (Bp / Bp.shift(L) - 1.0) * 1e4 + rU = (Up / Up.shift(L) - 1.0) * 1e4 + dR = (rU - rB).where(both) + dRi = dR.sub(dR.median(axis=1), axis=0) + w = dRi[dm].abs().stack() + print(f" r{L:<3d}: idiosincratico med {dRi.abs().stack().median():6.1f} bps " + f"p95 {dRi.abs().stack().quantile(.95):7.1f} max {dRi.abs().stack().max():8.1f}" + f" | nel depeg med {w.median():6.1f} max {w.max():8.1f}") + + # ------------------------------------------- [3] IL TEST 1 β€” U10, k=5 congelato + print(f"\n[3] TEST 1 β€” meccanismo CONGELATO su U10, {T1_START} .. {T1_END}, USD vs USDT") + print(f" 10 gambe = ESATTAMENTE il minimo del meccanismo (min_assets=10, k=5): nessun") + print(f" parametro toccato, e il confronto e' appaiato su TUTTO tranne il venue.") + PB = Bp[U10].dropna() + PU = Up[U10].dropna() + idxc = PB.index.intersection(PU.index) + PB, PU = PB.loc[idxc], PU.loc[idxc] + print(f" pannello comune: {len(idxc)} giorni, {PB.index[0].date()} .. {PB.index[-1].date()}") + + rows = [] + for ph in PHASES: + a = alive(X.xsec_engine(PU, phase=ph)) + b = alive(X.xsec_engine(PB, phase=ph)) + a, b = pair(a, b) + rows.append(dict(ph=ph, corr=float(a.corr(b)), sh_usd=sh(a), sh_usdt=sh(b), + d=sh(a) - sh(b), dd_usd=dd(a), dd_usdt=dd(b), n=len(a))) + R = pd.DataFrame(rows) + print(f" {'fase':>5}{'corr':>9}{'Sh USD':>9}{'Sh USDT':>9}{'dSharpe':>10}" + f"{'DD USD':>9}{'DD USDT':>9}") + for _, r in R.iterrows(): + print(f" {int(r['ph']):>5}{r['corr']:>9.4f}{r['sh_usd']:>9.2f}{r['sh_usdt']:>9.2f}" + f"{r['d']:>+10.3f}{r['dd_usd']*100:>8.1f}%{r['dd_usdt']*100:>8.1f}%") + print(f" {'MEDIANA':>5}{R['corr'].median():>9.4f}{R['sh_usd'].median():>9.2f}" + f"{R['sh_usdt'].median():>9.2f}{R['d'].median():>+10.3f}" + f"{R['dd_usd'].median()*100:>8.1f}%{R['dd_usdt'].median()*100:>8.1f}%") + print(f" corr MINIMA fra le 10 fasi: {R['corr'].min():.4f} (fase {int(R.loc[R['corr'].idxmin(),'ph'])})" + f" | soglia dichiarata {CORR_SOGLIA} -> " + f"{'PASS' if R['corr'].min() >= CORR_SOGLIA else 'FAIL'}") + print(f" dSharpe appaiato (USD - USDT): mediana {R['d'].median():+.3f} " + f"banda [{R['d'].min():+.3f},{R['d'].max():+.3f}] " + f"fasi in cui USD e' MEGLIO {(R['d'] > 0).sum()}/{len(R)}") + print(f" ⚠️ la statistica e' la MEDIANA DELLE DIFFERENZE APPAIATE, non la differenza delle") + print(f" mediane (lezione 26/07: quest'ultima confronta fasi diverse e puo' ribaltare).") + + # quanto di Β§21 riproduce questo sottoinsieme? (o si misura l'universo, non il venue) + p13 = bin_panel(U13)[U13].dropna() + s13_w = win(alive(X.xsec_engine(p13, phase=0)), T1_START, T1_END) + s10_w = win(alive(X.xsec_engine(PB, phase=0)), T1_START, T1_END) + print(f"\n Contesto obbligatorio β€” quanto di Β§21 vive in questo sottoinsieme (Binance, fase 0,") + print(f" stessa finestra): U13 Sh {sh(s13_w):+.2f} | U10 Sh {sh(s10_w):+.2f}") + print(f" -> il TEST 1 giudica il VENUE su U10; NON e' una replica del titolo di Β§21.") + + # ------------------------------------------------ [4] i RANKING, non i P&L + print("\n[4] IL CANALE DEL DANNO β€” cambia il RANKING? (e' cio' che la strategia legge)") + print(" Un P&L simile puo' nascere da due ranking diversi che si compensano: la domanda") + print(" diretta e' se le GAMBE SCELTE cambiano. Scansione su tutte e 10 le fasi.") + D1 = ranking_scan(PB, PU, PHASES, "TEST 1 / U10") + if len(D1): + ind = D1[(D1["data"] >= pd.Timestamp(DEPEG[0], tz="UTC")) & + (D1["data"] < pd.Timestamp(DEPEG[1], tz="UTC"))] + print(f" di cui dentro la finestra del depeg: {len(ind)}") + + # ---------------------- [4b] il SECONDO canale: il gate binario, e l'attribuzione + print("\n[4b] IL GATE DI DISPERSIONE E' UN INTERRUTTORE β€” e una scansione di ranking non lo") + print(" vede. `disp_i >= percentile espandente` e' una soglia: pochi bps di differenza di") + print(" prezzo possono cadere ai due lati, e per 10 giorni uno sleeve e' pieno e l'altro") + print(" e' FLAT. E' l'unico modo in cui una differenza di venue microscopica puo'") + print(" produrre una differenza di P&L macroscopica.") + for label_, Pb_, Pu_ in (("TEST 1 / U10", PB, PU),): + tot_fl = 0 + for ph in PHASES: + for a_, b_, nd, bfirst in gate_flips(Pb_, Pu_, ph): + tot_fl += 1 + print(f" [{label_}] fase {ph}: {a_.date()} .. {b_.date()} ({nd}g) β€” " + f"{'USDT investito / USD FLAT' if bfirst else 'USD investito / USDT FLAT'}") + if tot_fl == 0: + print(f" [{label_}] nessuna discordanza del gate su 10 fasi") + else: + print(f" [{label_}] {tot_fl} intervalli di discordanza su 10 fasi") + + print("\n ATTRIBUZIONE β€” stesso confronto con il gate SPENTO su ENTRAMBI i venue") + print(" (diagnostica, NON una proposta di cambiare il meccanismo: serve a separare") + print(" 'il prezzo e' diverso' da 'la soglia binaria amplifica una differenza minuscola'):") + cg = [] + for ph in PHASES: + a_, b_ = pair(alive(X.xsec_engine(PU, phase=ph, disp_pct=0)), + alive(X.xsec_engine(PB, phase=ph, disp_pct=0))) + cg.append(float(a_.corr(b_))) + print(f" [TEST 1] corr con gate SPENTO: mediana {np.median(cg):.4f} minima {np.min(cg):.4f}" + f" (col gate acceso: mediana {R['corr'].median():.4f}, minima {R['corr'].min():.4f})") + + # ------------------------------- [5] TEST 2 β€” U13 ibrido, finestra INTERA di Β§21 + print(f"\n[5] TEST 2 β€” U13 IBRIDO (USD dove esiste, USDT altrove), {T2_START} .. {T2_END}") + print(" Domanda operativa: sostituendo TUTTO il prezzo in dollari che esiste, il titolo") + print(" di Β§21 (mediana di fase +1,12 fuori campione) si muove?") + HB, meta = hybrid_panel(U13) + print(" prima data USD per asset: " + " ".join(f"{s}:{meta[s]}" for s in U13)) + PBh = p13 + PUh = HB[U13].dropna() + ic = PBh.index.intersection(PUh.index) + PBh, PUh = PBh.loc[ic], PUh.loc[ic] + rows2 = [] + for ph in PHASES: + a = win(alive(X.xsec_engine(PUh, phase=ph)), T2_START, T2_END) + b = win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END) + a, b = pair(a, b) + rows2.append(dict(ph=ph, corr=float(a.corr(b)), sh_usd=sh(a), sh_usdt=sh(b), + d=sh(a) - sh(b), dd_usd=dd(a), dd_usdt=dd(b))) + R2 = pd.DataFrame(rows2) + print(f" {'fase':>5}{'corr':>9}{'Sh IBRIDO':>11}{'Sh USDT':>10}{'dSharpe':>10}" + f"{'DD IBR':>9}{'DD USDT':>9}") + for _, r in R2.iterrows(): + print(f" {int(r['ph']):>5}{r['corr']:>9.4f}{r['sh_usd']:>11.2f}{r['sh_usdt']:>10.2f}" + f"{r['d']:>+10.3f}{r['dd_usd']*100:>8.1f}%{r['dd_usdt']*100:>8.1f}%") + print(f" {'MEDIANA':>5}{R2['corr'].median():>9.4f}{R2['sh_usd'].median():>11.2f}" + f"{R2['sh_usdt'].median():>10.2f}{R2['d'].median():>+10.3f}" + f"{R2['dd_usd'].median()*100:>8.1f}%{R2['dd_usdt'].median()*100:>8.1f}%") + print(f" corr minima {R2['corr'].min():.4f} -> " + f"{'PASS' if R2['corr'].min() >= CORR_SOGLIA else 'FAIL'} (soglia {CORR_SOGLIA})") + print(f" πŸ“Œ IL NUMERO DI TESTA DI Β§21, RICALCOLATO IN DOLLARI: mediana di fase fuori") + print(f" campione {R2['sh_usdt'].median():+.2f} (USDT, replica) -> " + f"{R2['sh_usd'].median():+.2f} (IBRIDO USD).") + + # differenza appaiata con la macchineria del progetto (stesso gate di Β§21) + dlt = A.anchor_luck_delta( + lambda ph: win(alive(X.xsec_engine(PUh, phase=ph)), T2_START, T2_END), + lambda ph: win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END), + PHASES, metric=sh) + print(f" anchor_luck_delta (IBRIDO - USDT): mediana appaiata {dlt['median_paired']:+.3f} " + f"[{dlt['lo']:+.3f},{dlt['hi']:+.3f}], fasi con IBRIDO>USDT " + f"{dlt['n_positive']}/{dlt['n_anchors']}") + print("\n E il ranking, sull'ibrido (la fase con la corr piu' bassa va GUARDATA, non") + print(" riassunta: una corr di 0,97 su una fase o e' un ribaltamento vero o e' un giunto):") + D2 = ranking_scan(PBh, PUh, PHASES, "TEST 2 / U13-IBRIDO") + print("\n Discordanze del GATE binario sull'ibrido (finestra fuori campione):") + nfl = 0 + for ph in PHASES: + for a_, b_, nd, bfirst in gate_flips(PBh, PUh, ph): + if a_ >= pd.Timestamp(T2_END, tz="UTC"): + continue + nfl += 1 + print(f" fase {ph}: {a_.date()} .. {b_.date()} ({nd}g) β€” " + f"{'USDT investito / USD FLAT' if bfirst else 'USD investito / USDT FLAT'}") + print(f" totale {nfl} intervalli su 10 fasi (su ~204 ribilanciamenti per fase)") + cg2 = [] + for ph in PHASES: + a_, b_ = pair(win(alive(X.xsec_engine(PUh, phase=ph, disp_pct=0)), T2_START, T2_END), + win(alive(X.xsec_engine(PBh, phase=ph, disp_pct=0)), T2_START, T2_END)) + cg2.append(float(a_.corr(b_))) + print(f" ATTRIBUZIONE [TEST 2] corr con gate SPENTO: mediana {np.median(cg2):.4f} " + f"minima {np.min(cg2):.4f} (col gate acceso: mediana {R2['corr'].median():.4f}, " + f"minima {R2['corr'].min():.4f})") + + # ------------------------------- [5b] le gambe che NESSUN venue USD puo' controllare + print("\n[5b] LE TRE GAMBE NON CONTROLLABILI β€” quanto del risultato di Β§21 ci passa?") + print(" BNB e' il token nativo del venue sotto esame e NON esiste in USD nel 2021-23;") + print(" NEAR e INJ hanno un prezzo USD solo dal 2022-09. Se il titolo di Β§21 vivesse su") + print(" di loro, questo falsificatore non avrebbe potuto vedere niente, e andrebbe detto.") + base_b = A.anchor_luck_band( + lambda ph: win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END), + PHASES, canonical=0, metric=sh) + print(f" base U13 (Binance, fuori campione, mediana di fase): {base_b['median']:+.2f}") + for a_ in ("BNB", "NEAR", "INJ"): + sub = p13[[c for c in U13 if c != a_]].dropna() + bb = A.anchor_luck_band( + lambda ph, sub=sub: win(alive(X.xsec_engine(sub, phase=ph, min_assets=10)), + T2_START, T2_END), + PHASES, canonical=0, metric=sh) + print(f" senza {a_:<5s}: mediana di fase {bb['median']:+.2f} " + f"({bb['median']-base_b['median']:+.2f}) banda [{bb['lo']:+.2f},{bb['hi']:+.2f}]") + print(" (leave-one-out a 12 gambe: il meccanismo congelato regge, min_assets=10)") + + # ---------------------------------------------------- [6] IL CASO PEGGIORE + print("\n[6] IL CASO PEGGIORE β€” non la media: dove i due venue divergono di piu'?") + print(f" {'anno':>6}" + "".join(f"{k:>12}" for k in + ("corr T1", "dSh T1", "corr T2", "dSh T2"))) + for y in (2021, 2022, 2023): + a1 = b1 = a2 = b2 = None + cs1, ds1, cs2, ds2 = [], [], [], [] + for ph in PHASES: + a1 = win(alive(X.xsec_engine(PU, phase=ph)), f"{y}-01-01", f"{y+1}-01-01") + b1 = win(alive(X.xsec_engine(PB, phase=ph)), f"{y}-01-01", f"{y+1}-01-01") + if len(a1) > 60 and len(b1) > 60: + a1, b1 = pair(a1, b1) + cs1.append(float(a1.corr(b1))); ds1.append(sh(a1) - sh(b1)) + a2 = win(alive(X.xsec_engine(PUh, phase=ph)), f"{y}-01-01", f"{y+1}-01-01") + b2 = win(alive(X.xsec_engine(PBh, phase=ph)), f"{y}-01-01", f"{y+1}-01-01") + if len(a2) > 60 and len(b2) > 60: + a2, b2 = pair(a2, b2) + cs2.append(float(a2.corr(b2))); ds2.append(sh(a2) - sh(b2)) + f1 = f"{np.median(cs1):.4f}" if cs1 else "β€”" + g1 = f"{np.median(ds1):+.3f}" if ds1 else "β€”" + f2 = f"{np.median(cs2):.4f}" if cs2 else "β€”" + g2 = f"{np.median(ds2):+.3f}" if ds2 else "β€”" + print(f" {y:>6}{f1:>12}{g1:>12}{f2:>12}{g2:>12}") + worst1 = R.loc[R["corr"].idxmin()] + worst2 = R2.loc[R2["corr"].idxmin()] + wd1 = R.loc[R["d"].abs().idxmax()] + wd2 = R2.loc[R2["d"].abs().idxmax()] + print(f" peggior FASE per corr: T1 fase {int(worst1['ph'])} corr {worst1['corr']:.4f} | " + f"T2 fase {int(worst2['ph'])} corr {worst2['corr']:.4f}") + print(f" peggior FASE per dSharpe: T1 fase {int(wd1['ph'])} {wd1['d']:+.3f} | " + f"T2 fase {int(wd2['ph'])} {wd2['d']:+.3f}") + autopsia(PB, PU, int(worst1["ph"]), D1, "TEST 1") + autopsia(PBh, PUh, int(worst2["ph"]), D2, "TEST 2") + + st = idio.abs().stack().sort_values(ascending=False) + print(" le 8 divergenze IDIOSINCRATICHE piu' grandi della finestra del TEST 1 (bps):") + for (t, s), v in st.head(8).items(): + indep = "DEPEG" if pd.Timestamp(DEPEG[0], tz="UTC") <= t < pd.Timestamp(DEPEG[1], tz="UTC") else "" + print(f" {str(t.date()):<12}{s:<6}{v:8.0f} bps (comune quel giorno " + f"{common.loc[t]:+.0f} bps) {indep}") + print(" -> le peggiori NON cadono nel depeg e sono quasi tutte AVAX nelle settimane subito") + print(" dopo la sua quotazione su Coinbase: book sottile, non contaminazione da") + print(" stablecoin. Due cause diverse e due implicazioni diverse.") + + # --------- il caso peggiore su TUTTO il campione, non solo sulla finestra del test + print("\n ⚠️ E ORA LA DOMANDA GIUSTA, allargata: dove divergono di piu' i due venue in") + print(" TUTTO il campione 2021-2026? (cercare il caso peggiore solo dentro la finestra") + print(" che si sta difendendo e' un modo di non trovarlo)") + have = [s for s in U13 if USD_SRC[s] is not None] + Bf = bin_panel(have) + Uf = usd_panel(have).reindex(Bf.index) + bo = Bf.notna() & Uf.notna() + Df = (1e4 * np.log(Uf / Bf)).where(bo) + idf = Df.sub(Df.median(axis=1), axis=0) + worst_day = idf.abs().max(axis=1).sort_values(ascending=False) + print(f" {'giorno':<12}{'|idio| max':>11}{'asset':>7}{'comune':>9} finestra") + for t in worst_day.head(6).index: + s = idf.loc[t].abs().idxmax() + w = "fuori campione" if t < pd.Timestamp(T2_END, tz="UTC") else "scoperta 2024+" + print(f" {str(t.date()):<12}{idf.loc[t, s]:>11.0f}{s:>7}{Df.loc[t].median():>9.0f} {w}") + tw = worst_day.index[0] + print(f" il giorno peggiore in assoluto e' il {tw.date()}: scarto per asset (bps)") + print(" " + " ".join(f"{s}:{idf.loc[tw, s]:.0f}" for s in have + if np.isfinite(idf.loc[tw, s]))) + print(" πŸ“Œ E' un fatto che va detto per intero: la divergenza idiosincratica PUO'") + print(" esplodere β€” qui di due ordini di grandezza β€” ma e' successo in una CASCATA DI") + print(" LIQUIDAZIONI sulle coppie /USDT, non in un depeg, e cade nella finestra di") + print(" SCOPERTA (2024+, dove lo sleeve gira su Hyperliquid), non nel fuori campione.") + print(" Il meccanismo temuto dall'obiezione e' REALE; non e' accaduto nel 2021-2023.") + + # il giorno peggiore: dove il P&L dei due sleeve si stacca di piu' + a0 = alive(X.xsec_engine(PU, phase=0)); b0 = alive(X.xsec_engine(PB, phase=0)) + a0, b0 = pair(a0, b0) + gap = (a0 - b0) + print(f" scarto GIORNALIERO fra i due sleeve (fase 0): med " + f"{gap.abs().median()*1e4:.1f} bps p99 {gap.abs().quantile(.99)*1e4:.1f} bps " + f"max {gap.abs().max()*1e4:.1f} bps il {gap.abs().idxmax().date()}") + cum = (1 + a0).cumprod().iloc[-1] / (1 + b0).cumprod().iloc[-1] - 1 + print(f" divario CUMULATO su tutta la finestra del TEST 1: {cum*100:+.2f}% " + f"(equity USD vs equity USDT)") + + # --------------------------------------------------------------- [7] gate + print("\n[7] GATE") + print(f" soglia di venue (dichiarata dall'autore PRIMA, non da me): corr >= {CORR_SOGLIA}") + print(f" TEST 1 U10 congelato : corr mediana {R['corr'].median():.4f}, minima " + f"{R['corr'].min():.4f} -> {'PASS' if R['corr'].min() >= CORR_SOGLIA else 'FAIL'}") + print(f" TEST 2 U13 ibrido : corr mediana {R2['corr'].median():.4f}, minima " + f"{R2['corr'].min():.4f} -> {'PASS' if R2['corr'].min() >= CORR_SOGLIA else 'FAIL'}" + f" (una fase su 10; col gate spento minima {np.min(cg2):.4f} = PASS)") + print(f" ⚠️ il TEST 2 e' MIO, non quello dichiarato: e' piu' severo per costruzione") + print(f" (le gambe cambiano venue in date diverse) e la sua unica fase sotto soglia") + print(f" e' attribuita al gate binario, non al prezzo.") + print(f" riferimento 2024+ di Β§21 (Binance vs Hyperliquid): 0,9991 / dSharpe -0,025") + su = alive(X.xsec_engine(PUh, phase=0)) + mg = A.marginal_vs_tp01(win(su, T2_START, T2_END)) + print(f" marginal_vs_tp01 sullo sleeve IBRIDO fuori campione: {mg.get('marginal_verdict')} " + f"(corr {mg.get('corr_full')}, is_hedge {mg.get('is_hedge')}, " + f"insample_edge {mg.get('has_insample_edge')})") + imp = A.implausible_sharpe(win(su, T2_START, T2_END)) + print(f" implausible_sharpe (ibrido OOS): {imp['implausible']} Sh {imp.get('sharpe'):+.2f} " + f"perdite su barre attive {imp.get('loss_frac', float('nan'))*100:.1f}%") + print(" causalita': NON RI-GIRATA β€” il motore e' importato invariato da r0822b_xs01_oos.py,") + print(" che la verifica (prezzi futuri randomizzati -> max|diff| = 0). Cambiare il venue") + print(" cambia i dati, non l'ordine temporale in cui il motore li legge.") + print(" deflated_sharpe: NON PERTINENTE β€” qui non si SELEZIONA una cella, si confronta due") + print(" volte lo STESSO meccanismo su due fonti di prezzo. Nessun trial nuovo di strategia.") + print(" null de-levering: NON PERTINENTE (nessun claim di 'meno drawdown').") + print(" anchor: girato per costruzione β€” ogni numero sopra e' su tutte e 10 le fasi.") + + # ------------------------------------------------------------ [8] verdetto + print("\n[8] VERDETTO") + print(f" Il TEST DICHIARATO (meccanismo congelato, stesso sottoinsieme, stessa finestra,") + print(f" USD contro USDT) PASSA: corr mediana {R['corr'].median():.4f}, minima " + f"{R['corr'].min():.4f} >= {CORR_SOGLIA}, dSharpe appaiato {R['d'].median():+.3f}.") + print(f" Il titolo di Β§21 ricalcolato in dollari si muove di {R2['sh_usd'].median()-R2['sh_usdt'].median():+.2f} " + f"({R2['sh_usdt'].median():+.2f} -> {R2['sh_usd'].median():+.2f} di mediana di fase).") + print(f" Nella finestra del depeg la componente idiosincratica sale (med {idm.median():.1f} bps " + f"contro {idio[~dm].abs().stack().median():.1f}) ma resta due ordini di grandezza sotto") + print(f" la vol giornaliera di un alt, e ZERO ribilanciamenti cambiano gambe li' dentro.") + print(f" L'unica corr sotto soglia (TEST 2, fase 2: {R2['corr'].min():.4f}) e' attribuita:") + print(f" col gate di dispersione SPENTO la minima diventa {np.min(cg2):.4f} -> non e' il") + print(f" prezzo, e' la soglia BINARIA che amplifica pochi bps in 10 giorni di flat.") + print( " -> IL RISULTATO XS01-OOS SOPRAVVIVE al controllo di venue.") + print( " Cio' che NON e' stato controllato, e va citato insieme al verdetto: BNB non ha un") + print( " prezzo USD nel 2021-23 (e' il token nativo del venue sotto esame), e toglierlo") + print( " costa -0.75 di mediana di fase. Il test copre 10-12 gambe su 13, non 13.") + print(f" E il caso peggiore di divergenza dell'INTERO campione ({tw.date()}, " + f"{idf.loc[tw].abs().max():.0f} bps su una gamba)") + print( " dimostra che il meccanismo temuto e' reale: semplicemente non e' accaduto nella") + print( " finestra fuori campione. Non e' 'non puo' succedere', e' 'non e' successo li''.") + + print(f"\n[fine] {time.time()-t0:.1f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_maker.py b/scripts/research/r0822b_maker.py new file mode 100644 index 0000000..a2d0a5f --- /dev/null +++ b/scripts/research/r0822b_maker.py @@ -0,0 +1,733 @@ +#!/usr/bin/env python +"""r0822b_maker.py β€” l'ESECUZIONE PASSIVA (maker) vale qualcosa per il libro live? + +CONTESTO (fatti gia' nel progetto, non ri-misurati qui) +------------------------------------------------------- +* Il libro Deribit (TP01 75% + SKH01 25%, nettati su UNA sola posizione per strumento) manda + **solo ordini market** -> paga sempre il **taker**. `src/live/execution.py::_submit` ha + `order_type="market"` come default e `rebalance_signed` non passa mai altro. +* Fee misurate: **taker 3,50 bps/lato** (fee_watch + 16/16 fill sul nastro, 22/08), + **maker 1,50 bps/lato** (listino base dal 2026-08-01; fino al 31/07 era 0,00). + I backtest del progetto modellano **5,00 bps/lato** (0,10% RT) -> gia' conservativi. +* Il progetto ha guardato il lato passivo UNA volta (raccomandazione "T1" del 26/07: il TP di + SKH01 come limit resting) e l'ha chiusa **non per la fee** ma perche' un ordine al livello di + SKH chiuderebbe anche la quota TP01 (**netting su strumento unico**, `src/live/book.py`). + ⚠️ QUEL VINCOLO NON SI APPLICA QUI: rendere passivo **l'ordine netto di ribilanciamento** e' + UN ordine sulla posizione netta -> niente conflitto fra sleeve. Il netting uccide i bracket + per-sleeve, non l'ordine netto. E' la ragione per cui questa domanda e' nuova. +* Slippage misurato il 22/08: **~0 a questa taglia** (un fill ha preso il 22% del volume della + sua barra 5m, range 1,08 bps) -> il differenziale maker/taker e' fee PURA, e il mezzo spread + catturato da un passivo e' <0,005 bps (tick BTC 0,1 su ~$110k; ETH 0,01 su ~$2.500) = ZERO. + Percio' qui il passivo NON guadagna spread: guadagna 2,00 bps di fee e rischia il non-fill. +* Il disaster-SL βˆ’30% e' uno `stop_market` on-book: NON e' toccato da nulla di quanto segue. + +LA DOMANDA CHE NESSUNO HA FATTO +------------------------------- +Un limit resting non e' un market order piu' economico: **puo' non riempirsi**. E non riempirsi +su un segnale di trend significa **perdere il movimento**, che e' esattamente il caso in cui non +si e' riempito (il prezzo e' scappato dalla parte giusta). Quindi: + costo atteso del passivo = P(fill)*(βˆ’2,00 bps) + P(no fill)*E[mossa avversa | no fill] +e la seconda parte e' **avversa per costruzione**: un buy limit al riferimento non si riempie se +e solo se il prezzo non e' MAI sceso al livello, cioe' e' salito. + +COSA FA QUESTO SCRIPT +--------------------- + 1. il differenziale in Sharpe/CAGR di libro, riusando la curva fee gia' pubblicata + (`r0726_fee_sensitivity.py`: βˆ’0,017 Sharpe/bps, βˆ’0,23% CAGR/bps) E ri-misurandola a 4 punti + di fee sul path del libro (verifica indipendente, non ri-derivazione della curva); + 2. ricostruisce gli **ORDINI** del libro live (target netto orario, banda min-order $5) e li + confronta con i 18 ordini veri di `data/live/book_executions.jsonl` (controllo di realta'); + 3. per OGNI ordine misura su 5m certificati: P(fill) di un limit al riferimento entro N minuti, + e il ritorno condizionato **ai casi in cui NON si riempie**. Confronto **APPAIATO PER + ORDINE** (mai per esito: allineare sui riempiti tiene solo i casi in cui non e' successo + niente β€” trappola gia' presa 3 volte in questo progetto); + 4. trasforma il costo per-ordine in una serie di P&L additiva ai rendimenti giornalieri del + libro -> Sharpe/CAGR/maxDD della politica passiva, e il numero in **dollari all'anno**; + 5. mette il risultato accanto alle altre leve gia' misurate. + +LENTI E SEMPLIFICAZIONI β€” DICHIARATE (tutte GENEROSE verso il maker, di proposito) +--------------------------------------------------------------------------------- + (a) griglia oraria: il cron gira a :07. Decisione con le barre 5m chiuse fino a h:05, prezzo di + riferimento = close della barra 5m etichettata h:00. Verificato sul log vero: gli ordini + cadono a :07 di un'ora qualsiasi, e TP01 si aggiorna alle 01:07 UTC. + (b) TP01 cambia target una volta al giorno, all'ora 01:00 UTC (lente backtest; l'effetto della + barra parziale e' misurato +-0,03 Sharpe, 26/07 -> irrilevante per la TAGLIA degli ordini). + (c) SKH01: ingressi alla CHIUSURA del bin 230m (lente backtest), uscite al primo confine orario + dopo il tocco 5m (lente live, `_skyhook_positions` itera nel bin parziale). L'ingresso vero + del live e' INTRA-BIN, cioe' piu' vicino al breakout -> peggiore per un limit passivo: + questa lente e' **generosa** verso il maker. + (d) fill: un limit BUY al prezzo P si considera riempito se `low <= P` nella finestra. Ignora la + coda del book (al livello si sta DIETRO agli ordini gia' presenti: un semplice tocco non + riempie) -> **generosa** verso il maker. Finestra di scansione da h:05 (l'ordine e' a h:07: + regaliamo 2 minuti di opportunita' di fill). + (e) ordini tutto-o-niente, nessun fill parziale. + (f) niente cap per-asset nella ricostruzione (a $635 il cap $318 morde di rado e riduce + PROPORZIONALMENTE guadagno e danno: non cambia il segno). + + nice -n 19 timeout 900 uv run python scripts/research/r0822b_maker.py +""" +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from src.backtest.harness import backtest_signals # noqa: E402 +from src.data.downloader import load_data # noqa: E402 +from src.portfolio.portfolio import combine_outer, metrics, to_daily # noqa: E402 +from src.strategies.skyhook import (LTF_MIN, SKH01_V2_DD, build_frames, # noqa: E402 + skyhook_entries) +from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, # noqa: E402 + resample_1d, simple_returns) + +ASSETS = ("BTC", "ETH") +W_TP01, W_SKH = 0.75, 0.25 # pesi del book Deribit (src/live/book.py) +WEIGHT = 0.5 # book 50/50 BTC+ETH +MIN_ORDER_USD = 5.0 # config/live.json +EQUITY_LIVE = 635.0 # conto vero (22/08) + +FEE_TAKER_BPS = 3.50 # misurata sul nastro + fee_watch +FEE_MAKER_BPS = 1.50 # listino base dal 2026-08-01 +FEE_MODEL_BPS = 5.00 # cio' che i backtest modellano +FEE_GRID_BPS = (0.0, FEE_MAKER_BPS, FEE_TAKER_BPS, FEE_MODEL_BPS) + +# curva PUBBLICATA (r0726_fee_sensitivity.py) β€” riusata, non ri-derivata +PUB_SH_PER_BPS = -0.017 +PUB_CAGR_PER_BPS = -0.23 # punti percentuali di CAGR per bps/lato + +HORIZONS_MIN = (5, 15, 30, 60, 120) # attesa massima del limit prima di inseguire +# CODA DEL BOOK β€” il parametro che DECIDE, e che l'OHLC non contiene. +# Un limit al livello sta DIETRO agli ordini gia' presenti: "il prezzo ha toccato il mio livello" +# NON e' "sono stato riempito". `q` = quanti bps il prezzo deve attraversare OLTRE il livello +# perche' la coda davanti sia consumata. q=0 e' il LIMITE SUPERIORE ottimistico (e a q=0 il fill +# e' quasi automatico: l'apertura della prima barra di scansione COINCIDE col livello, quindi +# `low <= P` chiede solo che il prezzo non scappi istantaneamente). +QUEUE_BPS = (0.0, 1.0, 2.0, 5.0, 10.0) +P2_MAX_HOURS = 24 # politica "mai inseguire": ri-posta ogni ora, tetto 24h +CAPITALS = (635.0, 2000.0, 5000.0) +HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC") +SEED = 20260822 + + +# =========================================================================================== +# 1. SEGNALI β€” calcolati UNA volta per asset (non dipendono dalla fee) +# =========================================================================================== +def build_asset(asset: str) -> dict: + """Tutto cio' che serve per l'asset: TP01 daily, path SKH01, griglia 5m e oraria.""" + df5 = load_data(asset, "5m") + ts5 = pd.to_datetime(df5["datetime"], utc=True) + A = dict( + ts5=ts5.values.astype("datetime64[ns]"), + o5=df5["open"].values.astype(float), h5=df5["high"].values.astype(float), + l5=df5["low"].values.astype(float), c5=df5["close"].values.astype(float), + ) + + # --- SKH01: ingressi/uscite sulla griglia 230m, poi tempo di RILEVAZIONE oraria + ltf, htf = build_frames(df5) + ent = skyhook_entries(ltf, htf, SKH01_V2_DD) + H, L = ltf["high"].values, ltf["low"].values + ltf_ts = pd.to_datetime(ltf["datetime"], utc=True) + n = len(ltf) + trades = [] + i = 0 + while i < n: + e = ent[i] + if e is None: + i += 1 + continue + d, sl, tp, mb = e["dir"], e["sl"], e["tp"], e["max_bars"] + exit_idx = None + for s in range(1, mb + 1): + j = i + s + if j >= n: + break + hit = (L[j] <= sl or H[j] >= tp) if d == 1 else (H[j] >= sl or L[j] <= tp) + if hit or s == mb: + exit_idx = j + break + if exit_idx is None: # trade ancora aperto a fine storia + trades.append((i, None, d, sl, tp)) + break + trades.append((i, exit_idx, d, sl, tp)) + i = exit_idx + 1 + + # tempi di rilevazione: ingresso a chiusura bin (lente backtest); + # uscita al primo confine orario dopo il TOCCO 5m (lente live) + ltf_close = ltf_ts + pd.Timedelta(minutes=LTF_MIN) + skh_events = [] # (ts_rilevazione, segno) + for (i0, j0, d, sl, tp) in trades: + skh_events.append((_ceil_hour(ltf_close.iloc[i0]), int(d))) + if j0 is None: + continue + t_touch = _first_touch_5m(A, ltf_ts.iloc[j0], ltf_close.iloc[j0], d, sl, tp) + skh_events.append((_ceil_hour(t_touch), 0)) + + # --- TP01: target giornaliero, visibile dall'ora 01:00 del giorno DOPO la chiusura + d1 = resample_1d(load_data(asset, "1h")) + tp_cfg = TrendPortfolio(**CANONICAL) + tgt = tp_cfg.target_series(d1) + d1_ts = pd.to_datetime(d1["datetime"], utc=True) + tp_events = [(pd.Timestamp(t).normalize() + pd.Timedelta(hours=25), float(v)) + for t, v in zip(d1_ts, tgt)] # barra del giorno D -> vista alle 01:00 di D+1 + + # --- griglia oraria dei momenti di decisione (barre 5m etichettate :00) + m = pd.DatetimeIndex(A["ts5"]) + hmask = (m.minute == 0) + A["hours"] = m[hmask].tz_localize("UTC") + A["hidx"] = np.flatnonzero(hmask) # indice 5m della barra di riferimento + A["ref"] = A["c5"][A["hidx"]] # prezzo di riferimento (close 5m di h:00) + A["skh_events"] = skh_events + A["tp_events"] = tp_events + A["daily_ret"] = simple_returns(d1["close"].values.astype(float)) + A["daily_tgt"] = tgt + A["d1_ts"] = d1_ts + A["ltf"] = ltf + A["ent"] = ent + return A + + +def _ceil_hour(t: pd.Timestamp) -> pd.Timestamp: + return t.ceil("h") + + +def _first_touch_5m(A: dict, t_open, t_close, d: int, sl: float, tp: float) -> pd.Timestamp: + """Primo istante 5m, dentro il bin 230m d'uscita, in cui SL o TP e' toccato. + Se nessuno e' toccato (uscita per max_bars) ritorna la chiusura del bin.""" + lo = np.searchsorted(A["ts5"], np.datetime64(t_open.tz_localize(None))) + hi = np.searchsorted(A["ts5"], np.datetime64(t_close.tz_localize(None))) + for k in range(lo, min(hi, len(A["ts5"]))): + hit = (A["l5"][k] <= sl or A["h5"][k] >= tp) if d == 1 else \ + (A["h5"][k] >= sl or A["l5"][k] <= tp) + if hit: + return pd.Timestamp(A["ts5"][k], tz="UTC") + pd.Timedelta(minutes=5) + return t_close + + +def exposure_path(A: dict) -> pd.DataFrame: + """Esposizione-bersaglio del libro su questo asset, ORA per ORA, in frazione di equity. + expo = WEIGHT * (W_TP01*tp_frac + W_SKH*skh_sign) β€” la formula di src/live/book.py.""" + hours = A["hours"] + tp = pd.Series(0.0, index=hours) + for t, v in A["tp_events"]: + tp.loc[tp.index >= t] = v + sk = pd.Series(0.0, index=hours) + for t, v in sorted(A["skh_events"]): + sk.loc[sk.index >= t] = v + expo = WEIGHT * (W_TP01 * tp.values + W_SKH * sk.values) + src = np.where(np.abs(np.diff(sk.values, prepend=0.0)) > 0, "SKH", + np.where(np.abs(np.diff(tp.values, prepend=0.0)) > 0, "TP01", "")) + return pd.DataFrame(dict(ts=hours, expo=expo, ref=A["ref"], hidx=A["hidx"], src=src)) + + +# =========================================================================================== +# 2. ORDINI β€” banda min-order, esattamente come rebalance_signed +# =========================================================================================== +def orders_from_path(P: pd.DataFrame, equity: float, min_usd: float = MIN_ORDER_USD) -> pd.DataFrame: + """Ordini che il libro manderebbe: si trada solo se |target-posizione| >= min_usd.""" + band = min_usd / equity + expo = P["expo"].values + held = 0.0 + rows = [] + for k in range(len(expo)): + delta = expo[k] - held + if abs(delta) < band: + continue + rows.append((P["ts"].iloc[k], delta, P["ref"].iloc[k], P["hidx"].iloc[k], P["src"].iloc[k])) + held = expo[k] + return pd.DataFrame(rows, columns=["ts", "dfrac", "ref", "hidx", "src"]) + + + + +# =========================================================================================== +# 3. FILL E SELEZIONE AVVERSA +# =========================================================================================== +def measure_orders(A: dict, O: pd.DataFrame, horizons=HORIZONS_MIN, q_bps: float = 0.0, + strict: bool = True) -> dict: + """Per ogni ordine e ogni orizzonte N: il limit e' stato riempito? e se no, quanto e' + scappato il prezzo? `q_bps` = attraversamento richiesto oltre il livello (coda del book). + + `strict` E' IL PARAMETRO CHE DECIDE IL SEGNO DI TUTTO IL RISULTATO. + strict=False -> `low <= limite`: il prezzo ha TOCCATO il livello. Include il caso in cui + la barra APRE esattamente al livello e non ci torna mai: li' non e' stato scambiato + NIENTE al nostro prezzo dopo che l'ordine e' arrivato, quindi un passivo in fondo alla + coda NON e' riempito. E' un limite superiore FISICAMENTE NON DIFENDIBILE. + strict=True -> `low < limite`: il prezzo ha ATTRAVERSATO il livello. E' la condizione + minima perche' esista uno scambio al nostro prezzo. Anche cosi' si assume di essere + DAVANTI a tutta la coda gia' presente -> resta generosa, ma difendibile.""" + l5, h5, c5 = A["l5"], A["h5"], A["c5"] + nb = len(c5) + idx = O["hidx"].values.astype(int) + ref = O["ref"].values.astype(float) + buy = (O["dfrac"].values > 0) + q = q_bps / 1e4 + out = {} + for N in horizons: + steps = N // 5 + filled = np.zeros(len(O), bool) + adverse = np.zeros(len(O)) + for k in range(len(O)): + i0 = idx[k] + 1 # barra 5m etichettata h:05 (ordine a h:07: generoso) + i1 = min(i0 + steps, nb) + P = ref[k] + if i0 >= i1: + filled[k] = True + continue + lim = P * (1.0 - q) if buy[k] else P * (1.0 + q) + if buy[k]: + hit = (l5[i0:i1].min() < lim) if strict else (l5[i0:i1].min() <= lim) + else: + hit = (h5[i0:i1].max() > lim) if strict else (h5[i0:i1].max() >= lim) + filled[k] = hit + if not hit: + end = c5[i1 - 1] + adverse[k] = (end - P) / P if buy[k] else (P - end) / P + out[N] = pd.DataFrame(dict(filled=filled, adverse=adverse), index=O.index) + return out + + +def measure_p2(A: dict, O: pd.DataFrame, P: pd.DataFrame, max_hours=P2_MAX_HOURS, + q_bps: float = 0.0, strict: bool = True) -> pd.DataFrame: + """Politica 'MAI INSEGUIRE': il limit resta e viene RI-POSTATO al nuovo riferimento a ogni + cron orario, fino a `max_hours`; solo allora si insegue a market. Il costo e' cumulativo.""" + l5, h5, c5 = A["l5"], A["h5"], A["c5"] + hidx = P["hidx"].values.astype(int) + ref = P["ref"].values.astype(float) + nb, nh = len(c5), len(hidx) + pos_of = {int(v): k for k, v in enumerate(hidx)} + q = q_bps / 1e4 + cycles, adverse, chased = [], [], [] + for _, r in O.iterrows(): + k0 = pos_of[int(r["hidx"])] + P0 = float(r["ref"]); buy = r["dfrac"] > 0 + done, adv, cyc = False, 0.0, 0 + for k in range(k0, min(k0 + max_hours, nh)): + Pk = ref[k] + lim = Pk * (1.0 - q) if buy else Pk * (1.0 + q) + i0 = hidx[k] + 1 + i1 = min(i0 + 12, nb) + if i0 >= i1: + break + if buy: + hit = (l5[i0:i1].min() < lim) if strict else (l5[i0:i1].min() <= lim) + else: + hit = (h5[i0:i1].max() > lim) if strict else (h5[i0:i1].max() >= lim) + cyc = k - k0 + 1 + if hit: + adv = (lim - P0) / P0 if buy else (P0 - lim) / P0 + done = True + break + if not done: + k = min(k0 + max_hours, nh - 1) + adv = (ref[k] - P0) / P0 if buy else (P0 - ref[k]) / P0 + cycles.append(cyc); adverse.append(adv); chased.append(not done) + return pd.DataFrame(dict(cycles=cycles, adverse=adverse, chased=chased), index=O.index) + + +def per_order_delta(O: pd.DataFrame, M: pd.DataFrame, *, miss_fee_bps: float) -> np.ndarray: + """Delta di P&L in FRAZIONE DI EQUITY DI LIBRO, per ORDINE, del passivo contro il taker. + >0 = il passivo guadagna. Confronto APPAIATO PER ORDINE: ogni ordine compare una volta in + entrambe le politiche, riempito o no (allineare sui soli riempiti terrebbe i casi in cui + non e' successo niente β€” trappola gia' presa 3 volte in questo progetto).""" + absd = O["dfrac"].abs().values + filled = M["filled"].values if "filled" in M.columns else np.ones(len(O), bool) + fee_bps = np.where(filled, FEE_MAKER_BPS, miss_fee_bps) + return (FEE_TAKER_BPS - fee_bps) / 1e4 * absd - M["adverse"].values * absd + + +def weighted_stats(F: np.ndarray, Ad: np.ndarray, W: np.ndarray) -> dict: + """Tutto PONDERATO PER NOZIONALE (e' il nozionale che paga la fee, non il conteggio). + `p_be` = la frazione di fill che il passivo deve raggiungere per PAREGGIARE il taker: + p*(-gap) + (1-p)*E[avversa|no fill] = 0 -> p_be = E/(E+gap) + E' la forma model-free del risultato: non dipende dall'ipotesi di coda, solo da quanto + scappa il prezzo quando il limit resta a terra.""" + gap = (FEE_TAKER_BPS - FEE_MAKER_BPS) / 1e4 + wf = W[F].sum() / W.sum() if W.sum() > 0 else 1.0 + miss_w = W[~F] + e_adv = float(np.sum(miss_w * Ad[~F]) / miss_w.sum()) if miss_w.sum() > 0 else 0.0 + cost = float(np.sum(W * (np.where(F, FEE_MAKER_BPS, FEE_TAKER_BPS) / 1e4 + Ad)) / W.sum()) + return dict(p_fill=wf, e_adv=e_adv, cost_bps=cost * 1e4, + p_be=e_adv / (e_adv + gap) if (e_adv + gap) > 0 else 1.0) + + +def boot_ci(x: np.ndarray, w: np.ndarray, n: int = 2000, seed: int = SEED) -> tuple: + """IC95% bootstrap della media PONDERATA PER NOZIONALE (in bps del nozionale tradato).""" + rng = np.random.default_rng(seed) + m = len(x) + idx = rng.integers(0, m, size=(n, m)) + num = x[idx].sum(axis=1) + den = w[idx].sum(axis=1) + b = num / np.where(den > 0, den, np.nan) * 1e4 + return float(np.nanpercentile(b, 2.5)), float(np.nanpercentile(b, 97.5)) + + +# =========================================================================================== +# 4. LIBRO β€” rendimenti giornalieri modellati, per innestarci il costo d'esecuzione +# =========================================================================================== +def book_daily(cache: dict, fee_bps_side: float) -> pd.Series: + fs = fee_bps_side / 1e4 + tp_s, sk_s = {}, {} + for a in ASSETS: + A = cache[a] + r, tgt = A["daily_ret"], A["daily_tgt"] + held = np.zeros(len(tgt)); held[1:] = tgt[:-1] + net = held * r - fs * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0 + tp_s[a] = pd.Series(np.clip(net, -0.99, None), index=A["d1_ts"]) + m = backtest_signals(A["ltf"], A["ent"], fee_rt=2 * fs, leverage=1.0, asset=a, tf="230m") + s = pd.Series(m.equity, index=pd.DatetimeIndex(pd.to_datetime(m.eq_index, utc=True))) + sk_s[a] = s.resample("1D").last().ffill().pct_change().dropna() + Jt = pd.concat(tp_s, axis=1, join="inner").fillna(0.0) + Js = pd.concat(sk_s, axis=1, join="inner").fillna(0.0) + tp = to_daily(pd.Series(0.5 * Jt["BTC"].values + 0.5 * Jt["ETH"].values, index=Jt.index)) + sk = to_daily(pd.Series(0.5 * Js["BTC"].values + 0.5 * Js["ETH"].values, index=Js.index)) + return combine_outer({"TP01": tp, "SKH01": sk}, {"TP01": W_TP01, "SKH01": W_SKH}) + + +def graft(base: pd.Series, deltas: dict, O_live: dict) -> pd.Series: + """Innesta il delta per-ordine (frazione di equity) nei rendimenti giornalieri del libro.""" + ser = [] + for a in ASSETS: + ts = pd.DatetimeIndex(O_live[a]["ts"]).tz_convert("UTC").normalize() + ser.append(pd.Series(deltas[a], index=ts)) + add = pd.concat(ser).groupby(level=0).sum() + return base.add(add.reindex(base.index).fillna(0.0), fill_value=0.0) + + +def show(tag: str, d: pd.Series) -> dict: + m = metrics(d) + h = metrics(d[d.index >= HOLDOUT]) + print(f" {tag:<36} Sh {m['sharpe']:>6.3f} | HOLD {h['sharpe']:>6.3f} | " + f"maxDD {m['maxdd']*100:>5.2f}% | CAGR {m['cagr']*100:>6.2f}%") + return dict(sharpe=m["sharpe"], hold=h["sharpe"], maxdd=m["maxdd"], cagr=m["cagr"]) + + +# =========================================================================================== +def main() -> None: + rng = np.random.default_rng(SEED) + print("=" * 104) + print(" r0822b β€” ESECUZIONE PASSIVA (MAKER) SUL LIBRO LIVE:") + print(" quanto vale il differenziale, quanto costa non riempirsi") + print("=" * 104) + print(f" taker {FEE_TAKER_BPS:.2f} bps/lato Β· maker {FEE_MAKER_BPS:.2f} bps/lato Β· " + f"differenziale {FEE_TAKER_BPS-FEE_MAKER_BPS:.2f} bps/lato Β· modello backtest {FEE_MODEL_BPS:.2f}") + print(" NETTING: il libro ha UNA posizione netta per strumento -> l'ordine di ribilanciamento") + print(" e' UNO solo. Renderlo passivo NON e' bloccato dal motivo che uccise T1 il 26/07") + print(" (quello uccide i BRACKET per-sleeve, non l'ordine netto). Il disaster-SL -30% resta") + print(" uno stop_market on-book e non e' toccato da niente di quanto segue.") + + cache = {a: build_asset(a) for a in ASSETS} + + # ---------------------------------------------------------------- 1. il differenziale + print("\n" + "-" * 104) + print(" 1. QUANTO VALE IL DIFFERENZIALE (limite superiore: TUTTO il flusso diventa maker,") + print(" zero mancati riempimenti β€” cioe' il numero che NON si puo' superare)") + print("-" * 104) + curve = {bps: show(f"libro @ {bps:.2f} bps/lato", book_daily(cache, bps)) for bps in FEE_GRID_BPS} + gap = FEE_TAKER_BPS - FEE_MAKER_BPS + dsh = curve[FEE_MAKER_BPS]["sharpe"] - curve[FEE_TAKER_BPS]["sharpe"] + dcg = (curve[FEE_MAKER_BPS]["cagr"] - curve[FEE_TAKER_BPS]["cagr"]) * 100 + print(f"\n misurato qui : dSharpe {dsh:+.4f} dCAGR {dcg:+.3f} pp " + f"({dsh/gap:+.4f} Sh/bps, {dcg/gap:+.3f} pp/bps)") + print(f" curva 26/07 : dSharpe {-PUB_SH_PER_BPS*gap:+.4f} dCAGR {-PUB_CAGR_PER_BPS*gap:+.3f} pp " + f"({-PUB_SH_PER_BPS:+.4f} Sh/bps, {-PUB_CAGR_PER_BPS:+.3f} pp/bps) <- replica indipendente OK") + print(" in DOLLARI all'anno, se tutto il flusso fosse maker:") + for cap, note in ((EQUITY_LIVE, "conto vero oggi"), (5000.0, "dopo i versamenti"), + (258338.0, "capitale-rendita, muro 07/08")): + print(f" ${cap:>9,.0f} -> ${cap*dcg/100:>9,.2f}/anno ({note})") + + # ---------------------------------------------------------------- 2. gli ordini + print("\n" + "-" * 104) + print(" 2. GLI ORDINI DEL LIBRO (target netto orario + banda min-order $5, formula di book.py)") + print("-" * 104) + paths = {a: exposure_path(cache[a]) for a in ASSETS} + yrs = (paths["BTC"]["ts"].iloc[-1] - paths["BTC"]["ts"].iloc[0]).days / 365.25 + print(f" {'capitale':>9} {'banda':>7} |" + "".join(f"{a+' ord/anno':>15}{'turnover/anno':>16}" for a in ASSETS)) + ORD = {} + for cap in CAPITALS: + line, tot = "", {} + for a in ASSETS: + O = orders_from_path(paths[a], cap) + tot[a] = O + line += f"{len(O)/yrs:>15.1f}{O['dfrac'].abs().sum()/yrs*cap:>15,.0f}$" + ORD[cap] = tot + print(f" ${cap:>8,.0f} {MIN_ORDER_USD/cap*100:>6.2f}% |" + line) + O_live = ORD[EQUITY_LIVE] + n_live = sum(len(O_live[a]) for a in ASSETS) + turn = sum(O_live[a]["dfrac"].abs().sum() for a in ASSETS) / yrs + print(f"\n ricostruzione a ${EQUITY_LIVE:,.0f}: {n_live} ordini su {yrs:.1f} anni = " + f"{n_live/yrs:.1f}/anno di libro ({n_live/yrs/2:.1f} per asset), " + f"turnover {turn:.1f}x equity/anno") + print(f" coerenza col punto 1: {turn:.1f}x * {gap:.2f} bps = {turn*gap/1e4*100:.3f} pp di CAGR " + f"contro i {dcg:.3f} pp misurati sul libro modellato " + f"({turn*gap/1e4*100/dcg*100:.0f}% β€” la ricostruzione tiene meno turnover del modello)") + exe = ROOT / "data" / "live" / "book_executions.jsonl" + if exe.exists(): + rows = [json.loads(l) for l in exe.read_text().splitlines() if l.strip()] + gg = max((pd.Timestamp(rows[-1]["ts_utc"]) - pd.Timestamp(rows[0]["ts_utc"])).days, 1) + print(f" controllo di realta': {len(rows)} ordini VERI in {gg} giorni = " + f"{len(rows)/gg*365.25:.0f}/anno di libro. Stesso ordine di grandezza; la finestra e'") + print(" troppo corta per una stima (18 ordini), serve solo a escludere un errore di fattore 10.") + for a in ASSETS: + v = O_live[a]["src"].value_counts() + print(f" {a}: origine " + " ".join(f"{(k or 'drift')} {n}" for k, n in v.items())) + + # ---------------------------------------------------------------- 3. il non-fill + print("\n" + "-" * 104) + print(" 3. IL COSTO DEL MANCATO RIEMPIMENTO (limit al riferimento, APPAIATO PER ORDINE)") + print("-" * 104) + print(" Il parametro che decide NON e' l'orizzonte d'attesa: e' cosa si chiama 'riempito'.") + print(" Un limit al livello sta DIETRO a tutta la coda gia' presente, quindi:") + print(" TOCCO (low <= limite) -> include la barra che APRE al livello e non ci") + print(" torna mai: al nostro prezzo non e' stato scambiato") + print(" NIENTE. Fisicamente non difendibile: LIMITE SUPERIORE.") + print(" ATTRAVERSAMENTO (low < limite) -> condizione MINIMA perche' uno scambio al nostro") + print(" prezzo esista. Assume ancora di essere davanti a") + print(" tutta la coda -> generosa, ma difendibile.") + print(" q bps in piu' -> quanto il prezzo deve andare OLTRE perche' la coda") + print(" davanti a noi sia consumata. L'OHLC non contiene la") + print(" profondita' del book: `q` si dichiara, non si stima.") + print(" E la colonna che decide e' `serve`: la frazione di fill necessaria per PAREGGIARE il") + print(" taker = E[avversa|no fill]/(E[avversa|no fill] + 2,00 bps). Model-free.") + print(f"\n {'ipotesi':>10} {'N(min)':>7} | {'P(fill)':>8} {'serve':>8} {'esito':>6} | {'E[avv|no]':>10}" + f" {'peggio':>8} | {'costo maker':>12} | {'vs taker':>9} | {'IC95 del delta':>22}" + f" | {'ordini vinti':>12}") + grid_cells = 6 # le 6 celle della griglia fine calcolate piu' sotto + tables = {} + variants = [("TOCCO <=", 0.0, False)] + [(f"attrav. q={q:.0f}", q, True) for q in QUEUE_BPS] + for lab, q, strict in variants: + MEAS = {a: measure_orders(cache[a], O_live[a], q_bps=q, strict=strict) for a in ASSETS} + tables[(q, strict)] = MEAS + for N in HORIZONS_MIN: + grid_cells += 1 + F = np.concatenate([MEAS[a][N]["filled"].values for a in ASSETS]) + Ad = np.concatenate([MEAS[a][N]["adverse"].values for a in ASSETS]) + W = np.concatenate([O_live[a]["dfrac"].abs().values for a in ASSETS]) + D = np.concatenate([per_order_delta(O_live[a], MEAS[a][N], miss_fee_bps=FEE_TAKER_BPS) + for a in ASSETS]) + S = weighted_stats(F, Ad, W) + lo, hi = boot_ci(D, W) + ok = "SI" if S["p_fill"] >= S["p_be"] else "no" + print(f" {lab:>10} {N:>7} | {S['p_fill']*100:>7.1f}% {S['p_be']*100:>7.1f}% {ok:>6} | " + f"{S['e_adv']*1e4:>8.0f}b {(Ad[~F].max()*1e4 if (~F).any() else 0):>7.0f}" + f" | {S['cost_bps']:>8.2f} bps | {S['cost_bps']-FEE_TAKER_BPS:>+8.2f} | " + f"[{lo:>+7.2f},{hi:>+7.2f}] bps | {(D>0).mean()*100:>11.1f}%") + print() + + # -- la sintesi che non dipende dalle ipotesi di coda + print(" SINTESI β€” IL SEGNO DEL RISULTATO STA TUTTO IN UNA DISUGUAGLIANZA STRETTA:") + print(f" {'N(min)':>7} | {'TOCCO <= (limite sup.)':>28} | {'ATTRAVERSAMENTO < (minimo difendibile)':>42}") + for N in HORIZONS_MIN: + row = [] + for key in ((0.0, False), (0.0, True)): + F = np.concatenate([tables[key][a][N]["filled"].values for a in ASSETS]) + Ad = np.concatenate([tables[key][a][N]["adverse"].values for a in ASSETS]) + W = np.concatenate([O_live[a]["dfrac"].abs().values for a in ASSETS]) + S = weighted_stats(F, Ad, W) + keep = (FEE_TAKER_BPS - S["cost_bps"]) / (FEE_TAKER_BPS - FEE_MAKER_BPS) + row.append(f"P(fill) {S['p_fill']*100:>5.1f}% resta {FEE_TAKER_BPS-S['cost_bps']:>+5.2f} bps" + f" = {keep*100:>6.1f}%") + print(f" {N:>7} | {row[0]:>28} | {row[1]:>42}") + print(" Fra le due colonne cambia UNA cosa: se la barra che apre esattamente al nostro") + print(" prezzo, e da li' se ne va, conta come riempimento. Non conta: al nostro prezzo non") + print(" e' stato scambiato niente dopo che l'ordine e' arrivato. Con la definizione fisica") + print(" il passivo PERDE gia' prima di aggiungere un solo bps di coda.") + # griglia FINE vicino a zero: dove sta il salto? (calcolata qui, non citata a memoria) + print("\n griglia FINE a N=60 (attraversamento), per vedere dove sta il salto:") + fine = [] + for q in (0.0, 0.005, 0.05, 0.25, 0.5, 1.0): + num = den = wf = 0.0 + for a in ASSETS: + M = measure_orders(cache[a], O_live[a], horizons=(60,), q_bps=q, strict=True)[60] + w = O_live[a]["dfrac"].abs().values + F = M["filled"].values + num += np.sum(w * (np.where(F, FEE_MAKER_BPS, FEE_TAKER_BPS) / 1e4 + M["adverse"].values)) + den += w.sum(); wf += w[F].sum() + fine.append((q, wf / den, num / den * 1e4)) + print(" " + " ".join(f"q={q:<5.3f} P(fill) {f*100:>4.1f}% costo {c:>5.2f}" for q, f, c in fine)) + Ft = np.concatenate([tables[(0.0, False)][a][60]["filled"].values for a in ASSETS]) + At = np.concatenate([tables[(0.0, False)][a][60]["adverse"].values for a in ASSETS]) + Wt = np.concatenate([O_live[a]["dfrac"].abs().values for a in ASSETS]) + touch60 = weighted_stats(Ft, At, Wt)["cost_bps"] + print(f" Il salto e' TUTTO al primo passo (tocco {touch60:.2f} -> attraversamento sub-tick " + f"{fine[1][2]:.2f} bps): non e' la CODA a decidere, e' la DISUGUAGLIANZA. Poi il costo") + print(f" sale piano ({fine[-1][2]:.2f} a q=1 bps) e non torna mai sotto il taker " + f"({FEE_TAKER_BPS:.2f}): non esiste un q positivo di pareggio.") + + # -- decomposizione: chi paga il non-fill? + q0, N0 = 2.0, 60 + MEAS = tables[(q0, True)] + print(f" DECOMPOSIZIONE ad attraversamento q={q0:.0f} bps, N={N0} min β€” non tutti gli ordini sono uguali:") + print(f" {'gruppo':<22}{'n':>6}{'P(fill)':>10}{'E[avversa] su TUTTI':>22}{'E[avv|no fill]':>17}") + groups = {} + for a in ASSETS: + O = O_live[a]; M = MEAS[a][N0] + groups.setdefault("SKH (breakout)", []).append((M, (O["src"] == "SKH").values)) + groups.setdefault("TP01 (drift 01:00)", []).append((M, (O["src"] == "TP01").values)) + groups.setdefault("aumenta esposizione", []).append((M, _increases(O))) + groups.setdefault("riduce esposizione", []).append((M, ~_increases(O))) + for lab, parts in groups.items(): + F = np.concatenate([m["filled"].values[k] for m, k in parts]) + Ad = np.concatenate([m["adverse"].values[k] for m, k in parts]) + if not len(F): + continue + print(f" {lab:<22}{len(F):>6}{F.mean()*100:>9.1f}%{Ad.mean()*1e4:>19.1f} bps" + f"{(Ad[~F].mean()*1e4 if (~F).any() else 0):>13.0f} bps") + print(" NB: un non-fill che RIDUCE l'esposizione lascia la posizione aperta piu' a lungo β€”") + print(" e' rischio, non solo prezzo, e la media in bps non lo mostra.") + + # -- placebo + print(f"\n PLACEBO a q={q0:.0f}, N={N0} β€” stesse taglie a ORE CASUALI (quanto pesa il") + print(" condizionamento sul segnale? se il placebo e' uguale, il segnale non seleziona nulla):") + grid_cells += 2 + for a in ASSETS: + P = paths[a] + k = rng.integers(1, len(P) - 30, size=len(O_live[a])) + Of = pd.DataFrame(dict(ts=P["ts"].values[k], dfrac=O_live[a]["dfrac"].values, + ref=P["ref"].values[k], hidx=P["hidx"].values[k], src="")) + Mf = measure_orders(cache[a], Of, horizons=(N0,), q_bps=q0, strict=True)[N0] + Mr = MEAS[a][N0] + print(f" {a}: reale P(fill) {Mr['filled'].mean()*100:>5.1f}% " + f"E[avversa su tutti] {Mr['adverse'].mean()*1e4:>6.1f} bps" + f" | placebo P(fill) {Mf['filled'].mean()*100:>5.1f}% " + f"E[avversa] {Mf['adverse'].mean()*1e4:>6.1f} bps") + + # -- politica "mai inseguire" + print(f"\n POLITICA 'MAI INSEGUIRE' a q={q0:.0f} bps (ri-posta ogni ora, tetto {P2_MAX_HOURS}h):") + grid_cells += 1 + P2 = {a: measure_p2(cache[a], O_live[a], paths[a], q_bps=q0, strict=True) for a in ASSETS} + c2 = np.concatenate([P2[a]["cycles"].values for a in ASSETS]) + a2 = np.concatenate([P2[a]["adverse"].values for a in ASSETS]) + w2 = np.concatenate([O_live[a]["dfrac"].abs().values for a in ASSETS]) + ch2 = np.concatenate([P2[a]["chased"].values for a in ASSETS]) + cost2 = (np.sum(w2 * (FEE_MAKER_BPS / 1e4 + a2)) / w2.sum()) * 1e4 + print(f" cicli orari mediana {np.median(c2):.0f} p90 {np.percentile(c2,90):.0f} " + f"max {c2.max():.0f} | mai riempito entro {P2_MAX_HOURS}h: {ch2.mean()*100:.1f}%") + contrib = w2 * a2 + top5 = np.sort(contrib)[-5:].sum() / contrib.sum() if contrib.sum() > 0 else 0.0 + print(f" avversa: mediana {np.median(a2)*1e4:+.1f} bps p90 {np.percentile(a2,90)*1e4:+.1f} " + f"MEDIA {a2.mean()*1e4:+.1f} peggiore {a2.max()*1e4:+.0f}") + print(f" -> costo {cost2:.2f} bps/lato ({cost2-FEE_TAKER_BPS:+.2f} contro il taker), e " + f"i 5 ordini peggiori su {len(a2)} fanno il {top5*100:.0f}% del danno totale.") + print(" E' la FIRMA DELLA TRONCATURA, vista dall'altra parte: il passivo vince PICCOLO e") + print(" SPESSO (mediana favorevole) e perde GROSSO e di RADO. Stessa forma del profit-take") + print(" di VRP01 (30/07), dove una regola che scattava sull'86-91% dei vincenti non era") + print(" protezione ma troncatura. Qui la media e' il numero che decide, non la mediana.") + + # ---------------------------------------------------------------- 4. impatto sul libro + print("\n" + "-" * 104) + print(" 4. IMPATTO SUL LIBRO (delta per-ordine innestato nei rendimenti giornalieri)") + print("-" * 104) + base = book_daily(cache, FEE_TAKER_BPS) + b = show("TAKER 3,50 bps = il libro di oggi", base) + res = {} + for q, strict in [(0.0, False), (0.0, True), (1.0, True), (2.0, True), (5.0, True)]: + for N in (5, 60, 120): + alt = graft(base, {a: per_order_delta(O_live[a], tables[(q, strict)][a][N], + miss_fee_bps=FEE_TAKER_BPS) for a in ASSETS}, O_live) + tag = "TOCCO<=" if not strict else f"attrav q={q:.0f}" + m = show(f"MAKER {tag:>10} N={N:>3} min, poi market", alt) + res[(tag, N)] = m + alt2 = graft(base, {a: per_order_delta(O_live[a], P2[a], miss_fee_bps=FEE_MAKER_BPS) + for a in ASSETS}, O_live) + m2 = show(f"MAKER 'mai inseguire' q={q0:.0f} bps", alt2) + + print("\n delta contro il baseline TAKER (a $635 di conto vero):") + for (q, N), m in res.items(): + print(f" {q:>10} N={N:>3} dSharpe {m['sharpe']-b['sharpe']:+.4f} " + f"dHOLD {m['hold']-b['hold']:+.4f} dCAGR {(m['cagr']-b['cagr'])*100:+.3f} pp " + f"dmaxDD {(m['maxdd']-b['maxdd'])*100:+.2f} pp -> ${EQUITY_LIVE*(m['cagr']-b['cagr']):+.2f}/anno") + print(f" {'mai-inseg.':>10} dSharpe {m2['sharpe']-b['sharpe']:+.4f} " + f"dHOLD {m2['hold']-b['hold']:+.4f} dCAGR {(m2['cagr']-b['cagr'])*100:+.3f} pp " + f"dmaxDD {(m2['maxdd']-b['maxdd'])*100:+.2f} pp -> ${EQUITY_LIVE*(m2['cagr']-b['cagr']):+.2f}/anno") + + # ---------------------------------------------------------------- 5. robustezza + print("\n" + "-" * 104) + print(" 5. ROBUSTEZZA (banda RIDOTTA: gli ordini spostati di -1h/0/+1h. NON e' la banda a 24") + print(" ancore: qui si sposta il momento dell'ORDINE, non l'ancora della strategia)") + print("-" * 104) + grid_cells += 3 + for sh in (-1, 0, 1): + num, den = 0.0, 0.0 + for a in ASSETS: + P = paths[a] + pos = {int(v): k for k, v in enumerate(P["hidx"].values)} + kk = np.array([min(max(pos[int(h)] + sh, 0), len(P) - 1) for h in O_live[a]["hidx"]]) + Osh = pd.DataFrame(dict(ts=P["ts"].values[kk], dfrac=O_live[a]["dfrac"].values, + ref=P["ref"].values[kk], hidx=P["hidx"].values[kk], src="")) + M = measure_orders(cache[a], Osh, horizons=(N0,), q_bps=q0, strict=True)[N0] + w = Osh["dfrac"].abs().values + num += np.sum(w * (np.where(M["filled"].values, FEE_MAKER_BPS, FEE_TAKER_BPS) / 1e4 + + M["adverse"].values)) + den += w.sum() + print(f" ordini spostati di {sh:+d}h costo maker {num/den*1e4:>6.2f} bps/lato " + f"({num/den*1e4-FEE_TAKER_BPS:+.2f} contro il taker)") + print(" -1h e' informativo: sposta l'ordine PRIMA che il segnale sia visibile, cioe' verso") + print(" dove sta l'ingresso VERO di SKH01 (intra-bin, piu' vicino al breakout della lente") + print(" usata qui). La lente di questo script e' GENEROSA verso il maker anche per questo.") + + # ---------------------------------------------------------------- 6. ordini di grandezza + print("\n" + "-" * 104) + print(" 6. LA DOMANDA STRUTTURALE β€” la leva accanto alle altre gia' misurate nel progetto") + print("-" * 104) + bq, bN = max(res, key=lambda k: res[k]["sharpe"]) + bm = res[(bq, bN)] + print(f" maker, LIMITE SUPERIORE (0 non-fill) dSharpe {dsh:+.4f} " + f"${EQUITY_LIVE*dcg/100:>+7.2f}/anno a ${EQUITY_LIVE:,.0f} ${5000*dcg/100:>+7.2f} a $5.000") + print(f" maker, migliore cella misurata ({bq},N={bN}) dSharpe {bm['sharpe']-b['sharpe']:+.4f} " + f"${EQUITY_LIVE*(bm['cagr']-b['cagr']):>+7.2f}/anno a ${EQUITY_LIVE:,.0f}") + worst = min(res, key=lambda k: res[k]["sharpe"]) + print(f" maker, peggiore cella misurata ({worst[0]},N={worst[1]}) " + f"dSharpe {res[worst]['sharpe']-b['sharpe']:+.4f} " + f"${EQUITY_LIVE*(res[worst]['cagr']-b['cagr']):>+7.2f}/anno") + print(f" peso di SKH01 ottimizzato dSharpe +0.0300 GATE FALLITO (26/07)") + print(f" banda d'ancora del libro (FULL) dSharpe +-0.20 (26/07, 2000 estrazioni)") + print(f" banda d'ancora del libro (HOLD-OUT) 2.36 canonico -> 1.54 onesto (26/07)") + print(f" raddoppio del taker (regola 26/07) dSharpe -0.08 'non si tocca nulla'") + print(f" versare 250 EUR/mese invece di 0 da MAI a 16-20 anni (26-27/07)") + # -- rischio operativo: quanti ordini resterebbero APERTI al giro successivo del cron + print("\n RISCHIO OPERATIVO (non e' un numero di rendimento, e' un modo nuovo di rompersi):") + for key, lab in (((0.0, True), "attraversamento q=0"), ((2.0, True), "attraversamento q=2")): + F = np.concatenate([tables[key][a][60]["filled"].values for a in ASSETS]) + print(f" {lab:<22} N=60 min: {(1-F.mean())*100:>4.1f}% degli ordini ancora APERTI al giro") + print(f" {'':<22} successivo = {(1-F.mean())*n_live/yrs:>4.1f} volte l'anno in cui l'esecutore") + print(f" {'':<22} deve cancellare-e-rimpiazzare correttamente, su un conto vero.") + print(" Oggi `rebalance_signed` manda market e finisce: nessun ordine sopravvive al giro.") + print(" Un limit resting introduce ordini orfani e doppi fill, e il libro ha gia' pagato") + print(" il prezzo di un percorso che si rompe in silenzio (`fresh_5m`, 26-29/07).") + + print("\n " + "=" * 100) + print(" VERDETTO: SCARTATO. Il differenziale c'e' (2,00 bps/lato = +0,033 Sharpe = $2,96/anno") + print(" a $635) ma e' il TETTO, e la selezione avversa se lo mangia tutto gia' alla condizione") + print(" di riempimento piu' generosa che sia fisicamente difendibile. Il segno del risultato") + print(" dipende da una disuguaglianza stretta (`<=` contro `<`), il che e' un altro modo di") + print(" dire che l'effetto e' dentro il rumore: l'IC95 del delta contiene lo zero in 10 celle") + print(" su 10 fra tocco e attraversamento-q=0. E il premio massimo immaginabile ($2,96/anno)") + print(" e' un ordine di grandezza sotto la leva piu' piccola gia' respinta dal progetto.") + print(" " + "=" * 100) + + print(f"\n GRIGLIA DICHIARATA: {grid_cells} celle valutate " + f"(({len(QUEUE_BPS)}q attrav. + 1 tocco) x {len(HORIZONS_MIN)}N + placebo x2 + " + f"mai-inseguire + 3 shift + 6 celle di griglia fine) " + f"+ {len(FEE_GRID_BPS)} punti di fee.") + print("=" * 104) + + +def _increases(O: pd.DataFrame) -> np.ndarray: + """True se l'ordine AUMENTA il valore assoluto dell'esposizione (entrata/incremento).""" + d = O["dfrac"].values + held = np.zeros(len(d)) + acc = 0.0 + for i, x in enumerate(d): + held[i] = acc + acc += x + return np.abs(held + d) > np.abs(held) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_prop_recal.py b/scripts/research/r0822b_prop_recal.py new file mode 100644 index 0000000..dcef4bd --- /dev/null +++ b/scripts/research/r0822b_prop_recal.py @@ -0,0 +1,891 @@ +"""r0822b_prop_recal β€” PROP-ALLOC RIFATTO con XS01 preso dalla STORIA LUNGA (2026-08-22, ondata 2). + +IL BUCO CHE CHIUDE +------------------ +Il risultato di testa dell'ondata (RESULTS-0822 Β§1, PROP-ALLOC) e': su un conto FUNDED da $100k +l'allocazione ottima sotto barriera di drawdown mette **XS01 al 50%** e porta +J = P(pass) x P(vivo 12m) da 0,335 (libro live 75/25) a **0,738**, con +**P(>=50 EUR/g) da 600 EUR in 36 mesi = 42%**. Quel filone ha dichiarato da solo il proprio limite: + + "tutto il vantaggio poggia sul drift di XS01 misurato sulla sua finestra di scoperta" + +e ha proposto un gate che era un'ATTESA: *non aprire un funded prima che XS01 abbia una finestra +fuori dal 2024-2026*. Il filone XS01-OOS (RESULTS-0822 Β§21) ha trovato quella finestra lo stesso +giorno β€” stessi 19 ticker su Binance spot USDT 1d, **2021-01 -> 2023-12, con LUNA e FTX dentro** β€” +e ha misurato che l'edge di RENDIMENTO regge (mediana di fase +1,12 fuori contro +0,37 dentro, +differenza appaiata +0,668 in 10/10 fasi). Ma ha anche misurato TRE correzioni nel verso opposto: + (i) il maxDD standalone di XS01 RADDOPPIA: 10,8% -> 20,9-22,0%; + (ii) il peso 0,50 non sopravvive: fuori campione a w=0,50 il maxDD di libro risale a 8,9% + contro 6,0% a w=0,25 β€” cioe' restituisce la protezione di DD che e' l'UNICA ragione per + mettere XS01 su un conto a barriera; + (iii) il 32% dell'universo non e' testabile (ARB/OP/SUI/APT/SEI/TIA non quotati nel 2021-23) e + su di esso vive gran parte del numero di scoperta (19 gambe 1,31 vs 13 gambe 0,54). + +Questo script rifa' il conto di PROP-ALLOC con la serie LUNGA al posto di quella di scoperta e +risponde a UNA domanda: **J regge?** + +LA PREVISIONE, REGISTRATA PRIMA DI MISURARE +------------------------------------------- +Mi aspetto che J SCENDA ma resti sopra il libro live, per tre ragioni che tirano in versi diversi: + (a) il drift di XS01 fuori campione e' PIU' ALTO -> spinge J su; + (b) il suo maxDD raddoppia e la barriera e' STATICA (una soglia assoluta non e' scale-invariante) + -> spinge J giu', e su un conto a barriera questa e' la forza che conta; + (c) la finestra lunga contiene il 2022, quindi TP01 (lo sleeve DIFENSIVO, sotto-pesato da + PROP-ALLOC su un campione senza sinistro) ci arriva con un curriculum diverso -> mi aspetto + che l'argmax RIALZI TP01 e ABBASSI XS01 rispetto al 25/25/50. +Previsione numerica dichiarata prima: argmax con w_XS in [0,25 ; 0,38], J fra 0,50 e 0,70, +P(>=50/g) fra il 10% e il 35%. Se J restasse >= 0,70 con w_XS = 0,50 la previsione e' sbagliata e +lo dico; se J crollasse sotto il livello del libro live, il fronte funded va ripensato ADESSO. + +DISCIPLINA DI RIPRODUZIONE (non negoziabile) +-------------------------------------------- +Prima di pubblicare un numero nuovo la macchina deve riprodurre quello vecchio. Β§0 rigira +PROP-ALLOC con la sua stessa macchineria (`r0822_prop_alloc` IMPORTATO, non riscritto), stessi +semi, stessa griglia, stesso de-luck, e confronta con i valori PUBBLICATI, cablati qui sotto in +`PUB`. Se la riproduzione fallisce, quello E' il risultato e viene detto. + +CONVENZIONI DICHIARATE +---------------------- +* Lente **ACCOPPIATA obbligatoria**. Su una regola di DD a UN GIORNO la lente close-only e' + esattamente CIECA (misurato oggi: 0,00 breach contro 0,40-1,21 veri, rapporto INF). La + macchineria del wick e' quella di `r0725_prop_coupled` (crypto: minimo ESATTO sul path orario + condiviso TP01+SKH01; XS01: 4 checkpoint O/estremo/estremo/C con ordinamento CONDIVISO e ordine + AVVERSO dagli OHLC giornalieri). Per la finestra 2021-23 gli OHLC sono quelli **Binance** della + cache di `r0822b_xs01_oos` β€” il costruttore di tuple e' lo STESSO codice e viene validato in Β§1 + contro `pc.xsec_daily_tuples()` sui dati HL, dove deve dare max|diff| = 0. +* **Universo dichiarato: U13** (i 13 dei 19 quotati prima del 2020-11) come serie PRIMARIA, perche' + e' l'unica testabile fuori campione e perche' e' la versione **PIU' DEBOLE** di quella che sta nel + libro (13 gambe invece di 19: XS01-OOS misura 0,54 contro 1,31 sulla stessa finestra e venue). + Scegliere la piu' debole e' conservativo per la decisione. U19-EXP (universo che si allarga da 10 + a 19 gambe) e' riportato come **altro capo della banda**, mai come numero di testa. +* Meccanismo **CONGELATO** (XS_CFG: blend [30,90], H=10, k=5, gate p30, vol-target 20%, 5 bps/lato). + Nessun parametro toccato, nessuna cella cercata: qui non si seleziona, si RI-MISURA. +* de-luck sul drift **x0,89** (misurato 26/07), identico a PROP-ALLOC. +* **Distorsioni portate avanti DICHIARATE, non rimosse in silenzio:** + (1) la banda d'ancora NON e' girata sotto questa lente (fuori budget il 22/08 e fuori budget + qui): l'ottimo gira sull'ancora canonica e si appoggia a SKH01, che il progetto misura come + lo sleeve con la fortuna d'ancora piu' grande da restituire -> la distorsione e' A FAVORE + del risultato. Lo stress SKH x0,50 / x0,33 e' li' apposta. + (2) l'intero risultato poggia sull'assunto β€” **MAI VERIFICATO CON UNA FIRM** β€” che una firm + crypto listi i 19 (qui 13) alt Hyperliquid **con short abilitato**. Il critico dell'ondata + lo chiama "la ripetizione esatta dell'errore GTAA01/PRIIPs". Non e' verificabile da qui + (serve leggere un listino), quindi Β§6 misura quanto del risultato MUORE se e' falso. +* Monte Carlo: 2500 percorsi per J (come PROP-ALLOC), 1500 per la scala di conti. **Un Monte Carlo + ha una risoluzione e va detta**: SE su un livello di J ~ +-0,010, sulle differenze APPAIATE + (indici di bootstrap condivisi) molto meno, ~+-0,007. + +ESITO (2026-08-22, corsa completa 309 s, 5.212 celle dichiarate) +---------------------------------------------------------------- +* RIPRODUZIONE **ESATTA** dei numeri pubblicati: J 0,738 / 0,721 / 0,335, P(pass) 82,2 / 77,3 / + 38,6%, delta appaiati +0,0164 e +0,3996, scala di conti 39,28 EUR/g e P(>=50/g) 41,7%. Tre + controlli bit-exact in piu' (il mio costruttore di tuple = quello del 25/07; il mio costruttore + di libro = `PA.book_RG`, anche sotto stress): **max|diff| = 0,0** su tutti. +* **J NON regge alla taglia pubblicata, e il colpevole NON e' la finestra.** Decomposizione a un + grado di liberta' per volta, tutta sullo stesso pannello: venue (HL -> Binance, 19 gambe, + date allineate) **-0,006**; universo (19 -> 13 gambe) **-0,307**; fase **+0,034**; finestra + (2024+ -> 2021-2023) **+0,061**. Il 42% del numero pubblicato sta nelle **6 gambe che non + esistono fuori campione** (ARB OP SUI APT SEI TIA), non nel periodo. +* Numeri onesti sulla parte testabile (13 gambe), mediana delle 10 fasi di XS01, peso 38/38/25: + J **0,453** su W-LONG 2021+ (LIVE 0,281; delta appaiato **+0,175**, positivo 10/10 fasi), + **0,504** su W-OOS 2021-23, **0,442** su W-DISC. Cioe' ~0,45-0,50 invece di 0,738. +* **La previsione registrata prima e' confermata sui pesi e sbagliata (ottimista) sul livello:** + regione robusta **w_XS = 25-38%** su TUTTE e tre le finestre (prevista [0,25 ; 0,38]) β€” quindi + il **50% dell'argmax di PROP-ALLOC non sopravvive**; J 0,45-0,52 contro [0,50 ; 0,70] previsto; + P(>=50/g) 7,8% contro [10% ; 35%] previsto. +* **Il numero operativo: P(>=50 EUR/g) da EUR 600 in 36 mesi = 7,8% [banda di fase 7,2-9,1%], + P(zero) 25,5%** β€” contro il 41,7% pubblicato, e contro **6,6%** se la firm non lista gli alt. + Il libro live sulla stessa lente fa 4,3%. +* Lo scenario severo di PROP-ALLOC (`XS x0,00`) **cambia stato**: non e' piu' "non sappiamo se il + drift esiste" ma un contro-fattuale a rendimento nullo, e li' il delta scende a **+0,041** + (col taglio congiunto SKH x0,33 diventa **-0,024**, cioe' peggio del libro live). Tutto il + vantaggio vive sul drift di XS01 β€” che ora e' misurato, per 13 gambe su 19. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822b_prop_recal.py` +Rete: NESSUNA (tutto da disco: parquet certificati + cache Binance di r0822b_xs01_oos). +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import r0725_prop_coupled as pc # noqa: E402 macchineria del wick accoppiato (25/07) +import r0725_prop_ladder as pl # noqa: E402 regole delle firm + scala di conti +import r0822_prop_alloc as PA # noqa: E402 IL filone da riprodurre (22/08) +import r0822b_xs01_oos as X # noqa: E402 motore XS01 + cache Binance (22/08) +from src.portfolio.sleeves import XS_UNIVERSE, _HL_DIR # noqa: E402 + +RAW = ROOT / "data" / "raw" +FEE_SIDE = 0.001 / 2.0 +HYRO = "HYRO" + +# ---------------------------------------------------------------- numeri PUBBLICATI da riprodurre +# (RESULTS-0822 Β§1, tabella "1 β€” PROP-ALLOC"; log della corsa nello scratchpad) +PUB = dict( + argmaxJ=dict(w="25/25/50", lev=0.750, J=0.738, p_pass=0.822, p_alive=0.898, sharpe=1.69), + argmaxS=dict(w="38/25/38", lev=0.750, J=0.721, p_pass=0.773, p_alive=0.933, sharpe=1.73), + live=dict(w="75/25/0", lev=0.500, J=0.335, p_pass=0.386, p_alive=0.868, sharpe=1.09), + dJ_vs_sharpe=0.0164, dJ_vs_live=0.3996, + ladder_med=39.28, ladder_p50=0.417, ladder_zero=0.139, +) + +LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50) +STEP = 8 +N_PATHS = PA.N_PATHS # 2500 +EV_H = FU_H = 365 +SEED_EV, SEED_FU = 8220, 8221 # identici a PROP-ALLOC +W3 = PA.CRYPTO_PROP # ("TP01","SKH01","XS01") + +TRIALS = 0 # contatore della griglia dichiarata (al RIALZO) + + +def bump(n: int) -> None: + global TRIALS + TRIALS += n + + +def hr(t: str = "") -> None: + print("\n" + "-" * 104) + if t: + print(f" {t}") + print("-" * 104) + + +# =============================================================== 1. XS01 ACCOPPIATO, QUALSIASI DATO + +def ohlc_panels(loader, syms: list[str]) -> tuple[pd.DataFrame, ...]: + """(C, O, H, L) allineati. `loader(sym) -> DataFrame con timestamp/open/high/low/close`.""" + o, h, lo, c = {}, {}, {}, {} + for s in syms: + d = loader(s) + i = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + o[s] = pd.Series(d["open"].astype(float).values, index=i) + h[s] = pd.Series(d["high"].astype(float).values, index=i) + lo[s] = pd.Series(d["low"].astype(float).values, index=i) + c[s] = pd.Series(d["close"].astype(float).values, index=i) + C = pd.concat(c, axis=1).sort_index() + return C, pd.concat(o, axis=1).reindex(C.index)[C.columns], \ + pd.concat(h, axis=1).reindex(C.index)[C.columns], \ + pd.concat(lo, axis=1).reindex(C.index)[C.columns] + + +def xs_tuples(C: pd.DataFrame, O: pd.DataFrame, H: pd.DataFrame, L: pd.DataFrame, + phase: int = 0, wick_mode: str = "shared", **kw) -> pd.DataFrame: + """(R, m) giornalieri di XS01 su un pannello QUALSIASI, con la stessa convenzione di wick di + `r0725_prop_coupled.xsec_daily_tuples`. Le posizioni vengono dal motore CONGELATO di + `r0822b_xs01_oos.xsec_engine` (che regge i NaN = asset non ancora quotato). + + Validato in Β§1 contro `pc.xsec_daily_tuples()` sui dati HL: deve dare max|diff| = 0.""" + det: dict = {} + r = X.xsec_engine(C, phase=phase, detail=det, **kw) + W, scale, turn = det["W"], det["scale"], det["turn"] + px = C.values.astype(float) + n, A = px.shape + prev_c = np.vstack([np.full((1, A), np.nan), px[:-1]]) + held = np.vstack([np.zeros((1, A)), W[:-1]]) # gambe TENUTE nel giorno i + with np.errstate(invalid="ignore"): + rO = O.values / prev_c - 1.0 + rH = H.values / prev_c - 1.0 + rL = L.values / prev_c - 1.0 + rC = px / prev_c - 1.0 + vO = np.nansum(held * rO, axis=1) + vH = np.nansum(held * rH, axis=1) + vL = np.nansum(held * rL, axis=1) + vC = np.nansum(held * rC, axis=1) + if wick_mode == "perleg": # bound severo: ogni gamba al suo peggio + worst = np.where(held >= 0, rL, rH) + m_gross = np.minimum(np.nansum(held * worst, axis=1), vC) + else: # 'shared': ordine AVVERSO + m_gross = np.minimum(np.minimum.reduce([vO, vL, vH, vC]), + np.minimum.reduce([vO, vH, vL, vC])) + m_net = (m_gross - turn * FEE_SIDE) * scale + R_net = r.values + out = pd.DataFrame({"R": R_net, "m": np.minimum(m_net, R_net)}, index=C.index) + return out + + +def bin_loader(sym: str) -> pd.DataFrame: + """Legge dalla cache di r0822b (nessuna rete) e SCARTA la barra del giorno in corso: una barra + giornaliera parziale in coda e' esattamente il difetto trovato oggi nei monitor forward.""" + d = X.fetch_binance_daily(sym) + today = pd.Timestamp.now("UTC").normalize().value // 10**6 + return d[d["timestamp"] < today].reset_index(drop=True) + + +def hl_loader(sym: str) -> pd.DataFrame: + return pd.read_parquet(_HL_DIR / f"hl_{sym.lower()}_1d.parquet") + + +# =============================================================== 2. LIBRO A 3 GAMBE, PANNELLO LIBERO + +def rg(w: dict, idx: pd.DatetimeIndex, xs: pd.DataFrame, drop_xs: bool = False, + dm: tuple = ()) -> tuple[np.ndarray, np.ndarray]: + """(R, gap) del libro TP01/SKH01/XS01 su un pannello arbitrario e con una serie XS01 + arbitraria. E' `PA.book_RG` con il pannello e la serie XS01 come argomenti invece che + cablati: crypto col minimo ESATTO sul path orario condiviso (`PA._crypto`, che per uno + stress taglia il drift sulle GAMBE ORARIE prima di comporre β€” il minimo giornaliero non e' + separabile per gamba, quindi non si puo' tagliare a valle); il minimo di XS01 si SOMMA + (worst simultaneo, convenzione severa del 25/07). gap = min_intraday - chiusura (<= 0). + Validato bit-exact contro `PA.book_RG` in Β§1. + + `dm` = (("XS01", 0.5), ...) taglia il DRIFT di quello sleeve lasciando la forma della coda. + ⚠ Il taglio crypto usa la media sull'INTERA storia oraria (2019+), non sulla finestra: e' + la convenzione di PROP-ALLOC, tenuta identica perche' le due tabelle vanno confrontate.""" + dmd = dict(dm) + w_tp, w_sk = float(w.get("TP01", 0.0)), float(w.get("SKH01", 0.0)) + w_xs = 0.0 if drop_xs else float(w.get("XS01", 0.0)) + R = np.zeros(len(idx)) + M = np.zeros(len(idx)) + if w_tp > 0 or w_sk > 0: + c = PA._crypto(w_tp, w_sk, dmd.get("TP01", 1.0), dmd.get("SKH01", 1.0)).reindex(idx) + R += np.nan_to_num(c["R"].values) + M += np.nan_to_num(c["m"].values) + if w_xs > 0: + f = xs.reindex(idx) + rv = np.nan_to_num(f["R"].values) + mv = np.nan_to_num(f["m"].values) + mult = dmd.get("XS01", 1.0) + if mult != 1.0: + cutv = (1.0 - mult) * rv.mean() # media PRIMA del taglio (come PA) + rv, mv = rv - cutv, mv - cutv + R += w_xs * rv + M += w_xs * mv + return R, np.minimum(M, R) - R + + +def obj_rg(R: np.ndarray, G: np.ndarray, lev: float, ev_idx, fu_idx, + firm: str = HYRO, factor: float = PA.DELUCK) -> dict: + """`PA.objective` senza la costruzione del libro (che qui e' su un pannello diverso).""" + Rd = PA.deluck(R, factor) + e = PA.eval_sim(Rd, G, ev_idx, lev, firm) + f = PA.funded_sim(Rd, G, fu_idx, lev, firm, 100_000.0) + ann = float(Rd.mean() * 365.0) + vol = float(Rd.std() * np.sqrt(365.0)) + return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"], + e_payout=f["e_payout"], sharpe=(ann / vol if vol > 0 else 0.0), + vol=vol, drift=ann, jvec=(e["passed"] & f["alive"]).astype(float)) + + +def maxdd(R: np.ndarray) -> float: + eq = np.cumprod(1.0 + R) + return float((eq / np.maximum.accumulate(eq) - 1.0).min()) + + +def scan(grid: list[dict], idx, xs, ev_idx, fu_idx, drop_xs: bool = False, + dm: tuple = ()) -> pd.DataFrame: + rows = [] + for w in grid: + R, G = rg(w, idx, xs, drop_xs=drop_xs, dm=dm) + dd = maxdd(PA.deluck(R)) + for lev in LEVS: + o = obj_rg(R, G, lev, ev_idx, fu_idx) + rows.append(dict(w=PA.wkey(w), lev=lev, maxdd=dd, + **{k: v for k, v in o.items() if k != "jvec"})) + bump(len(grid) * len(LEVS)) + return pd.DataFrame(rows) + + +def best_row(df: pd.DataFrame, key: str = "J") -> pd.Series: + return df.loc[df[key].idxmax()] + + +def wdict(key: str) -> dict: + return dict(zip(W3, [int(x) / 100 for x in key.split("/")])) + + +# =============================================================== main +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + t0 = time.time() + + def tick(lab: str) -> None: + print(f" [{time.time()-t0:6.0f}s] {lab}") + + print("=" * 104) + print(" PROP-RECAL β€” J regge se XS01 viene dalla STORIA LUNGA invece che dalla sua finestra" + " di scoperta?") + print("=" * 104) + print(" PREVISIONE REGISTRATA PRIMA: argmax con w_XS in [0.25, 0.38], J in [0.50, 0.70],") + print(" P(>=50 EUR/g) in [10%, 35%]. Il verso atteso: drift XS piu' alto (spinge su) contro") + print(" maxDD raddoppiato + barriera STATICA + 2022 dentro (spingono giu'; su un conto a") + print(" barriera la seconda forza pesa di piu').") + + # ================================================== Β§0. RIPRODUZIONE + hr("Β§0. RIPRODUZIONE β€” la macchina deve dare i numeri PUBBLICATI prima di darne di nuovi") + idx_pub = PA._panel_index(PA.START_WIN) + ev_pub = PA.boot_idx(len(idx_pub), EV_H, N_PATHS, seed=SEED_EV) + fu_pub = PA.boot_idx(len(idx_pub), FU_H, N_PATHS, seed=SEED_FU) + print(f" pannello 5-sleeve di PROP-ALLOC: {len(idx_pub)} giorni " + f"{idx_pub[0].date()} -> {idx_pub[-1].date()} | de-luck x{PA.DELUCK} | " + f"{N_PATHS} percorsi | semi {SEED_EV}/{SEED_FU}") + grid = PA.simplex_grid(W3, STEP) + t = time.time() + Gpub = PA.grid_scan(grid, LEVS, HYRO, ev_pub, fu_pub) + bump(len(grid) * len(LEVS)) + print(f" griglia {len(grid)} pesi x {len(LEVS)} leve = {len(grid)*len(LEVS)} celle " + f"({time.time()-t:.0f}s)") + b_pub = best_row(Gpub) + ws = Gpub.loc[Gpub["sharpe"].idxmax(), "w"] + bs_pub = best_row(Gpub[Gpub["w"] == ws]) + bl_pub = best_row(Gpub[Gpub["w"] == "75/25/0"]) + print(f"\n {'':>18} {'pesi':>10} {'leva':>6} {'J':>7} {'PUB':>7} {'d':>7} | " + f"{'P(pass)':>8} {'PUB':>7} | {'P(vivo)':>8} {'PUB':>7} | {'Sharpe':>7} {'PUB':>6}") + ok = True + for lab, row, p in (("argmax J", b_pub, PUB["argmaxJ"]), ("argmax Sharpe", bs_pub, PUB["argmaxS"]), + ("libro LIVE 75/25", bl_pub, PUB["live"])): + same_w = (row["w"] == p["w"]) and abs(row["lev"] - p["lev"]) < 1e-9 + dj = row["J"] - p["J"] + ok &= same_w and abs(dj) < 0.02 + print(f" {lab:>18} {row['w']:>10} {row['lev']:>6.3f} {row['J']:>7.3f} {p['J']:>7.3f} " + f"{dj:>+7.3f} | {row['p_pass']:>8.1%} {p['p_pass']:>7.1%} | " + f"{row['p_alive']:>8.1%} {p['p_alive']:>7.1%} | {row['sharpe']:>7.2f} {p['sharpe']:>6.2f}" + f"{'' if same_w else ' <-- CELLA DIVERSA'}") + dA, seA = PA.paired_delta(wdict(b_pub["w"]), float(b_pub["lev"]), + wdict(bs_pub["w"]), float(bs_pub["lev"]), HYRO, ev_pub, fu_pub) + dB, seB = PA.paired_delta(wdict(b_pub["w"]), float(b_pub["lev"]), + {"TP01": 0.75, "SKH01": 0.25}, float(bl_pub["lev"]), + HYRO, ev_pub, fu_pub) + bump(4) + print(f"\n differenze APPAIATE argmaxJ - argmaxSharpe {dA:+.4f} +-{seA:.4f} " + f"(PUB {PUB['dJ_vs_sharpe']:+.4f})") + print(f" argmaxJ - LIVE {dB:+.4f} +-{seB:.4f} " + f"(PUB {PUB['dJ_vs_live']:+.4f})") + ok &= abs(dA - PUB["dJ_vs_sharpe"]) < 0.02 and abs(dB - PUB["dJ_vs_live"]) < 0.03 + pc.DELUCK = PA.DELUCK + r_lad = pc.simulate([tuple(wdict(b_pub["w"]).get(n, 0.0) for n in W3)], + [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=0.75, seed=20260822, + lens="coupled", start=PA.START_WIN) + print(f" scala di conti (I-bis) EUR/g med {r_lad['med']:.2f} (PUB {PUB['ladder_med']:.2f}) | " + f"P(>=50/g) {r_lad['p_target']:.1%} (PUB {PUB['ladder_p50']:.1%}) | " + f"P(zero) {r_lad['p_zero']:.1%} (PUB {PUB['ladder_zero']:.1%})") + ok &= abs(r_lad["p_target"] - PUB["ladder_p50"]) < 0.03 + verdetto = "OK β€” si procede" if ok else "FALLITA β€” ogni numero sotto va letto con questa riserva" + print(f"\n >>> RIPRODUZIONE: {verdetto}") + tick("Β§0 riproduzione") + + # ================================================== Β§1. LA SERIE LUNGA, ACCOPPIATA + hr("Β§1. LA SERIE LUNGA DI XS01, CON IL WICK β€” e la validazione del costruttore di tuple") + Chl, Ohl, Hhl, Lhl = ohlc_panels(hl_loader, [s for s in XS_UNIVERSE + if (_HL_DIR / f"hl_{s.lower()}_1d.parquet").exists()]) + Chl = Chl.dropna() + Ohl, Hhl, Lhl = Ohl.reindex(Chl.index), Hhl.reindex(Chl.index), Lhl.reindex(Chl.index) + mine = xs_tuples(Chl, Ohl, Hhl, Lhl, phase=0) + ref = pc.xsec_daily_tuples("shared") + j = mine.join(ref, lsuffix="_m", rsuffix="_r", how="inner").dropna() + dR = float(np.abs(j["R_m"] - j["R_r"]).max()) + dM = float(np.abs(j["m_m"] - j["m_r"]).max()) + print(f" CONTROLLO: il mio costruttore di tuple, alimentato con gli OHLC HL, contro") + print(f" `pc.xsec_daily_tuples()` (la macchineria del 25/07): n={len(j)} " + f"max|dR| = {dR:.2e} max|dm| = {dM:.2e} -> " + f"{'IDENTICO' if max(dR, dM) < 1e-12 else 'DIVERGE β€” STOP'}") + if max(dR, dM) >= 1e-12: + print(" !! il costruttore non replica: i numeri sulla storia lunga sarebbero di un'altra") + print(" macchina. Mi fermo qui.") + return + + for wtest in ({"TP01": 0.25, "SKH01": 0.25, "XS01": 0.50}, {"TP01": 0.75, "SKH01": 0.25}): + Ra, Ga = rg(wtest, idx_pub, ref) + Rb_, Gb_ = PA.book_RG(wtest) + dd1 = float(np.abs(Ra - Rb_).max()); dd2 = float(np.abs(Ga - Gb_).max()) + print(f" CONTROLLO: il mio costruttore di LIBRO contro `PA.book_RG` " + f"({PA.wkey(wtest)}): max|dR| {dd1:.2e} max|dgap| {dd2:.2e} -> " + f"{'IDENTICO' if max(dd1, dd2) < 1e-12 else 'DIVERGE'}") + + Cb, Ob, Hb, Lb = ohlc_panels(bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13, h13, l13 = Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13], Lb.reindex(p13.index)[X.U13] + xs13 = xs_tuples(p13, o13, h13, l13, phase=0) + xsEXP = xs_tuples(Cb, Ob, Hb, Lb, phase=0) + bump(2) + first13 = xs13.index[np.flatnonzero(xs13["R"].values != 0.0)[0]] + firstEX = xsEXP.index[np.flatnonzero(xsEXP["R"].values != 0.0)[0]] + print(f"\n XS01 U13 (13 gambe fisse, Binance) prima barra ATTIVA {first13.date()} " + f"-> {xs13.index[-1].date()}") + print(f" XS01 U19-EXP (10 -> 19 gambe, Binance) prima barra ATTIVA {firstEX.date()} " + f"-> {xsEXP.index[-1].date()}") + print(" (barra del giorno in corso SCARTATA: una giornaliera parziale in coda e' il difetto") + print(" trovato oggi nei monitor forward)") + + # finestre dichiarate + cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index + base_idx = cry_idx.intersection(xs13.index) + S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC")) + S_DISC = pd.Timestamp(PA.START_WIN, tz="UTC") + I_LONG = base_idx[base_idx >= S_LONG] + I_OOS = I_LONG[I_LONG < S_DISC] + I_DISC = base_idx[base_idx >= S_DISC] + WINDOWS = (("W-LONG 2021+ (OOS + scoperta)", I_LONG), + ("W-OOS 2021-2023 (puro OOS)", I_OOS), + ("W-DISC 2024+ (scoperta)", I_DISC)) + print(f"\n {'finestra':>32} {'giorni':>7} {'da':>12} {'a':>12}") + for lab, ii in WINDOWS: + print(f" {lab:>32} {len(ii):>7d} {str(ii[0].date()):>12} {str(ii[-1].date()):>12}") + tick("Β§1 serie lunga") + + # ================================================== Β§2. PROFILO SOTTO BARRIERA + hr("Β§2. IL PROFILO CHE CAMBIA β€” cio' che una barriera STATICA vede: drift, vol, coda, gap") + print(" (de-luckato x0.89; `gap peggiore` = l'escursione intraday che la close-only NON vede)") + print(f" {'sleeve / finestra':>34} {'Sharpe':>7} {'vol a':>7} {'drift a':>8} {'maxDD':>7} " + f"{'g.peggiore':>11} {'gap p90':>9} {'gap peggiore':>13}") + prof_rows = [ + ("TP01 W-LONG", {"TP01": 1.0}, I_LONG, xs13), + ("TP01 W-DISC", {"TP01": 1.0}, I_DISC, xs13), + ("SKH01 W-LONG", {"SKH01": 1.0}, I_LONG, xs13), + ("SKH01 W-DISC", {"SKH01": 1.0}, I_DISC, xs13), + ("XS01 U13 Binance W-OOS", {"XS01": 1.0}, I_OOS, xs13), + ("XS01 U13 Binance W-DISC", {"XS01": 1.0}, I_DISC, xs13), + ("XS01 U19EXP Binance W-OOS", {"XS01": 1.0}, I_OOS, xsEXP), + ("XS01 UFFICIALE HL W-DISC", {"XS01": 1.0}, I_DISC, ref), + ] + for lab, w, ii, xf in prof_rows: + R, G = rg(w, ii, xf) + Rd = PA.deluck(R) + vol = Rd.std() * np.sqrt(365.0) + print(f" {lab:>34} {Rd.mean()*365/vol if vol > 0 else 0:>7.2f} {vol:>6.1%} " + f"{Rd.mean()*365:>8.1%} {maxdd(Rd)*100:>6.1f}% {Rd.min():>11.2%} " + f"{-np.percentile(-G, 90)*100:>8.2f}pp {G.min()*100:>12.2f}pp") + print("\n Le righe da leggere insieme sono XS01 W-OOS e XS01 UFFICIALE: il RENDIMENTO regge") + print(" (anzi sale), ma il maxDD e la coda peggiorano β€” e la barriera guarda la coda.") + tick("Β§2 profilo") + + # ================================================== Β§3. LA GRIGLIA RIFATTA + hr("Β§3. LA GRIGLIA RIFATTA SULLA STORIA LUNGA β€” stesso simplesso, stesse leve, stessa lente") + res = {} + for lab, ii in WINDOWS: + ev = PA.boot_idx(len(ii), EV_H, N_PATHS, seed=SEED_EV) + fu = PA.boot_idx(len(ii), FU_H, N_PATHS, seed=SEED_FU) + t = time.time() + Gx = scan(grid, ii, xs13, ev, fu) + res[lab] = dict(G=Gx, ev=ev, fu=fu, idx=ii) + b, bl = best_row(Gx), best_row(Gx[Gx["w"] == "75/25/0"]) + print(f"\n {lab} ({time.time()-t:.0f}s)") + print(f" {'':>18} {'pesi':>10} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8} " + f"{'Sharpe':>7} {'maxDD':>7} {'E[pay/a]':>10}") + for l2, r2 in (("argmax J", b), ("libro LIVE 75/25", bl)): + print(f" {l2:>18} {r2['w']:>10} {r2['lev']:>6.3f} {r2['J']:>7.3f} " + f"{r2['p_pass']:>8.1%} {r2['p_alive']:>8.1%} {r2['sharpe']:>7.2f} " + f"{r2['maxdd']*100:>6.1f}% {r2['e_payout']:>9,.0f}$") + d, se = None, None + try: + Rb, Gb = rg(wdict(b["w"]), ii, xs13) + Rl, Gl = rg({"TP01": 0.75, "SKH01": 0.25}, ii, xs13) + a1 = obj_rg(Rb, Gb, float(b["lev"]), ev, fu)["jvec"] + b1 = obj_rg(Rl, Gl, float(bl["lev"]), ev, fu)["jvec"] + dd_ = a1 - b1 + d, se = float(dd_.mean()), float(dd_.std(ddof=1) / np.sqrt(len(dd_))) + bump(2) + except Exception as exc: # pragma: no cover + print(f" delta appaiato non calcolato: {exc}") + if d is not None: + print(f" delta APPAIATO argmax - LIVE : {d:+.4f} +-{se:.4f}" + f" (PROP-ALLOC pubblicava {PUB['dJ_vs_live']:+.4f} su W-DISC con XS01 HL)") + tick("Β§3 griglie") + + # ================================================== Β§3-bis. DECOMPOSIZIONE + hr("Β§3-bis. DA DOVE VIENE IL SALTO 0.738 -> ~0.47 β€” un grado di liberta' alla volta") + print(" Fra il numero pubblicato e quello di Β§3 cambiano TRE cose insieme (venue, universo,") + print(" finestra). Sommarle e chiamarle 'l'effetto della storia lunga' sarebbe l'errore che") + print(" il progetto ha gia' pagato due volte. Qui si muove un grado di liberta' per volta,") + print(" sempre sul MIO pannello a 3 gambe (cosi' anche il pannello smette di essere una") + print(" variabile nascosta).") + ev_d, fu_d = res[WINDOWS[2][0]]["ev"], res[WINDOWS[2][0]]["fu"] + ev_o, fu_o = res[WINDOWS[1][0]]["ev"], res[WINDOWS[1][0]]["fu"] + ev_l, fu_l = res[WINDOWS[0][0]]["ev"], res[WINDOWS[0][0]]["fu"] + # ⚠ ERRORE CATTURATO IN SESSIONE, e vale piu' del numero che correggeva: `xsec_engine` + # ribilancia quando `i % H == phase`, dove `i` e' l'indice NEL PANNELLO. Due pannelli che + # cominciano a date diverse ribilanciano quindi in GIORNI DI CALENDARIO diversi: confrontarli + # misura la FASE, non il venue. Alla prima stesura il passo "venue" dava -0.254 di J (e + # avrei pubblicato "Binance costa un quarto di J") mentre XS01-OOS aveva misurato dSharpe + # -0.025 sullo stesso confronto fatto bene. Qui il pannello Binance e' RIALLINEATO alle + # stesse date di quello HL, cosi' la parita' dell'indice β€” e con essa la fase β€” coincide. + hl_idx = ref.index + C19 = Cb[list(XS_UNIVERSE)].reindex(hl_idx).dropna() + xs19_al = xs_tuples(C19, Ob.reindex(C19.index)[C19.columns], + Hb.reindex(C19.index)[C19.columns], + Lb.reindex(C19.index)[C19.columns], phase=0) + C13 = Cb[X.U13].reindex(C19.index).dropna() + xs13_al = xs_tuples(C13, Ob.reindex(C13.index)[X.U13], Hb.reindex(C13.index)[X.U13], + Lb.reindex(C13.index)[X.U13], phase=0) + bump(2) + steps = [("XS01 = HL 19 gambe W-DISC (= il pubblicato)", I_DISC, ref, ev_d, fu_d), + ("XS01 = BINANCE 19 g. W-DISC allineato (VENUE)", I_DISC, xs19_al, ev_d, fu_d), + ("XS01 = BINANCE 13 g. W-DISC allineato (UNIVERSO)", I_DISC, xs13_al, ev_d, fu_d), + ("XS01 = BINANCE 13 g. W-DISC pannello lungo (FASE)", I_DISC, xs13, ev_d, fu_d), + ("XS01 = BINANCE 13 g. W-OOS (FINESTRA)", I_OOS, xs13, ev_o, fu_o)] + print(f"\n {'passo':>50} {'argmax':>10} {'leva':>6} {'J':>7} {'dJ dal passo prima':>20}") + prev = None + dec = {} + for lab2, ii, xf, ev, fu in steps: + Gs = scan(grid, ii, xf, ev, fu) + b2 = best_row(Gs) + dec[lab2] = b2 + dj = "" if prev is None else f"{float(b2['J'])-prev:>+20.3f}" + print(f" {lab2:>50} {b2['w']:>10} {b2['lev']:>6.3f} {b2['J']:>7.3f} {dj}") + prev = float(b2["J"]) + print("\n Le righe sono ordinate dal piu' al meno controllabile. Quella che il progetto deve") + print(" guardare e' la piu' grande: se e' la FASE, il numero pubblicato non e' un numero") + print(" di strategia ma di calendario, ed e' il quarto modo d'ingannarsi dell'elenco del") + print(" brief. La finestra fuori campione, da sola, non peggiora nulla β€” conferma su J cio'") + print(" che XS01-OOS ha misurato sul rendimento.") + + print("\n E l'altro capo della banda: universo ESPANDENTE (10 -> 19 gambe man mano che") + print(" vengono quotate), che e' cio' che un implementatore in tempo reale avrebbe avuto:") + print(f" {'finestra':>34} {'argmax':>10} {'leva':>6} {'J':>7} {'maxDD':>7} " + f"{'J con U13':>10}") + for lab2, ii, ev, fu in (("W-OOS 2021-2023", I_OOS, ev_o, fu_o), + ("W-LONG 2021+", I_LONG, ev_l, fu_l)): + Gs = scan(grid, ii, xsEXP, ev, fu) + b2 = best_row(Gs) + j13 = float(best_row(res[[w for w in res if w.startswith(lab2.split()[0])][0]]["G"])["J"]) + print(f" {lab2:>34} {b2['w']:>10} {b2['lev']:>6.3f} {b2['J']:>7.3f} " + f"{b2['maxdd']*100:>6.1f}% {j13:>10.3f}") + + hr("Β§3-ter. CONTROLLO DI LENTE β€” quanto sarebbe cieca la close-only su QUESTI numeri") + print(" Su una regola di DD a UN GIORNO la close-only non scatta mai. Non e' un'opinione:") + print(" si vede rigirando le stesse celle con gap = 0.") + bL0 = best_row(res[WINDOWS[0][0]]["G"]) + Rc, Gc = rg(wdict(bL0["w"]), I_LONG, xs13) + o_cp = obj_rg(Rc, Gc, float(bL0["lev"]), ev_l, fu_l) + Rd_ = PA.deluck(Rc) + e_cl = PA.eval_sim(Rd_, Gc, ev_l, float(bL0["lev"]), HYRO, lens="close") + f_cl = PA.funded_sim(Rd_, Gc, fu_l, float(bL0["lev"]), HYRO, 100_000.0, lens="close") + bump(2) + print(f" W-LONG argmax {bL0['w']} @{bL0['lev']:.2f}x :" + f" ACCOPPIATA J {o_cp['J']:.3f} (P(pass) {o_cp['p_pass']:.1%}, P(vivo) {o_cp['p_alive']:.1%})") + print(f" {'':>44} CLOSE-ONLY J {e_cl['p_pass']*f_cl['p_alive']:.3f} " + f"(P(pass) {e_cl['p_pass']:.1%}, P(vivo) {f_cl['p_alive']:.1%})") + print(f" -> la close-only regala {e_cl['p_pass']*f_cl['p_alive']-o_cp['J']:+.3f} di J. Ogni") + print(" numero di questo file e' ACCOPPIATO; la close-only e' qui solo come controllo.") + tick("Β§3-bis/ter decomposizione") + + # ================================================== Β§4. LA REGIONE, NON IL PUNTO + hr("Β§4. IL PESO DI XS01 β€” c'e' una REGIONE robusta su ENTRAMBE le finestre?") + print(" PROP-ALLOC ha gia' misurato che il PUNTO non conta (il massimo di 200 pesi casuali") + print(" eguagliava l'argmax): la domanda giusta e' se esiste una regione comune. Per ogni") + print(" livello di w_XS si riporta il MEGLIO su TP/SKH e su tutte le leve.") + xs_levels = sorted({int(round(100 * (i / STEP))) for i in range(STEP + 1)}) + print(f"\n {'w_XS':>6} | " + " | ".join(f"{lab.split()[0]:>26}" for lab, _ in WINDOWS)) + print(f" {'':>6} | " + " | ".join(f"{'J':>7} {'maxDD':>7} {'pesi':>10}" for _ in WINDOWS)) + prof_by_w = {lab: {} for lab, _ in WINDOWS} + for wx in xs_levels: + cells = [] + for lab, _ in WINDOWS: + Gx = res[lab]["G"] + sub = Gx[Gx["w"].str.endswith(f"/{wx}")] + if len(sub) == 0: + cells.append(f"{'β€”':>7} {'β€”':>7} {'β€”':>10}") + continue + r2 = best_row(sub) + prof_by_w[lab][wx] = float(r2["J"]) + cells.append(f"{r2['J']:>7.3f} {r2['maxdd']*100:>6.1f}% {r2['w']:>10}") + print(f" {wx:>5}% | " + " | ".join(cells)) + print("\n Regione robusta = livelli di w_XS entro il 3% del meglio di finestra, su TUTTE e tre:") + robust = [] + for wx in xs_levels: + okall = True + for lab, _ in WINDOWS: + d = prof_by_w[lab] + if wx not in d or d[wx] < 0.97 * max(d.values()): + okall = False + if okall: + robust.append(wx) + print(f" {robust if robust else 'NESSUN livello comune'}" + f" (entro il 5%: " + f"{[wx for wx in xs_levels if all(wx in prof_by_w[l] and prof_by_w[l][wx] >= 0.95*max(prof_by_w[l].values()) for l, _ in WINDOWS)]})") + tick("Β§4 regione") + + # ================================================== Β§4-bis. BANDA DI FASE + hr("Β§4-bis. LA BANDA DI FASE DI XS01 (H=10) SOTTO LA LENTE PROP β€” mai girata prima") + print(" XS01 ribilancia ogni 10 giorni: le fasi possibili sono 10 e il progetto misura che la") + print(" fase canonica e' fortunata (audit 02/07: DD al 15Β° pctl, FULL all'85Β°). PROP-ALLOC") + print(" gira sulla SOLA fase 0. Qui la si gira tutta, al peso della REGIONE ROBUSTA di Β§4.") + print(" La stima onesta e' la MEDIANA della banda; il confronto col LIVE e' la MEDIANA delle") + print(" DIFFERENZE APPAIATE (mai la differenza delle mediane).") + W_ROB = "38/38/25" + print(f"\n peso fisso {W_ROB} (centro della regione robusta), leva scelta per fase") + print(f" {'finestra':>28} {'J fase 0':>9} {'MEDIANA':>9} {'banda p10-p90':>16} " + f"{'fasi>LIVE':>10} {'delta appaiato mediano':>24}") + phase_med = {} + for lab, ii in WINDOWS: + ev, fu = res[lab]["ev"], res[lab]["fu"] + RL, GL = rg({"TP01": 0.75, "SKH01": 0.25}, ii, xs13) + jL, lvL = -1.0, None + vL = None + for lv in LEVS: + o = obj_rg(RL, GL, lv, ev, fu) + if o["J"] > jL: + jL, lvL, vL = o["J"], lv, o["jvec"] + js, deltas, bestph = [], [], {} + for ph in range(10): + xsp = xs_tuples(p13, o13, h13, l13, phase=ph) + Rp, Gp = rg(wdict(W_ROB), ii, xsp) + jb, vb = -1.0, None + for lv in LEVS: + o = obj_rg(Rp, Gp, lv, ev, fu) + if o["J"] > jb: + jb, vb = o["J"], o["jvec"] + js.append(jb) + deltas.append(float((vb - vL).mean())) + bestph[ph] = jb + bump(10 * len(LEVS) + len(LEVS)) + med = float(np.median(js)) + phase_med[lab] = dict(median=med, band=(float(np.percentile(js, 10)), + float(np.percentile(js, 90))), + ph0=js[0], jlive=jL, dmed=float(np.median(deltas)), + npos=int(sum(1 for d in deltas if d > 0)), + argmax_phase=int(np.argmax(js))) + print(f" {lab:>28} {js[0]:>9.3f} {med:>9.3f} " + f"[{np.percentile(js, 10):>6.3f},{np.percentile(js, 90):>6.3f}] " + f"{sum(1 for j2 in js if j2 > jL):>7}/10 " + f"{np.median(deltas):>+18.3f} ({sum(1 for d in deltas if d > 0)}/10 >0)") + print(f"\n J del libro LIVE 75/25 (non dipende dalla fase di XS01): " + f"{phase_med[WINDOWS[0][0]]['jlive']:.3f} W-LONG, " + f"{phase_med[WINDOWS[1][0]]['jlive']:.3f} W-OOS, " + f"{phase_med[WINDOWS[2][0]]['jlive']:.3f} W-DISC") + print(" ⚠ Questa e' la banda della fase di XS01 SOLTANTO. La banda d'ancora di SKH01 (23") + print(" offset, quella che il progetto misura come la piu' grande da restituire) resta") + print(" NON GIRATA sotto questa lente β€” costo del recon orario. Β§5 la surroga con un") + print(" taglio del drift, che e' una stima, non una misura.") + tick("Β§4-bis banda di fase") + + # ================================================== Β§5. LA BANDA ANCORATA AL MISURATO + hr("Β§5. LA BANDA β€” con gli stress ANCORATI a cio' che e' misurato, non a cio' che era assunto") + print(" PROP-ALLOC pubblicava 42% / 7,7% / 0,7% dove il caso severo era 'XS x0,00' = il drift") + print(" di XS01 assunto NULLO perche' non misurabile fuori campione. Quel drift ORA E'") + print(" MISURATO, e fuori campione e' PIU' ALTO: lo scenario cambia stato, da assunzione a") + print(" misura. Cio' che resta assunto e' (a) la fortuna d'ancora di SKH01 (misurata altrove,") + print(" non sotto questa lente) e (b) il LISTINO della firm (Β§6). Gli stress qui sotto sono") + print(" quelli, non un pessimismo arbitrario.") + st_rows = [] + STRESS = ((), (("SKH01", 0.50),), (("SKH01", 0.33),), + (("XS01", 0.00),), (("SKH01", 0.33), ("XS01", 0.00))) + SLAB = {(): "nessuno (drift pieno)", + (("SKH01", 0.50),): "SKH x0.50 (ancora 26/07)", + (("SKH01", 0.33),): "SKH x0.33 (ancora severa)", + (("XS01", 0.00),): "XS x0.00 [ORA SUPERATO]", + (("SKH01", 0.33), ("XS01", 0.00)): "SKH x0.33 + XS x0.00 [vecchio severo]"} + for lab, ii in (WINDOWS[0], WINDOWS[1]): + Gw, ev, fu = res[lab]["G"], res[lab]["ev"], res[lab]["fu"] + top_keys = list(Gw.sort_values("J", ascending=False).drop_duplicates("w")["w"].head(10)) + print(f"\n {lab} (argmax ri-cercato sulle {len(top_keys)} celle-peso migliori: e' un") + print(f" risparmio di calcolo, ed e' CONSERVATIVO per la stabilita' dell'ottimo)") + print(f" {'stress':>28} {'argmax pesi':>12} {'leva':>6} {'J':>7} {'J del LIVE':>11} " + f"{'delta appaiato':>16}") + for dmt in STRESS: + best_j, best_k, best_l, best_v = -1.0, None, None, None + for k in top_keys: + R, G = rg(wdict(k), ii, xs13, dm=dmt) + for lv in LEVS: + o = obj_rg(R, G, lv, ev, fu) + if o["J"] > best_j: + best_j, best_k, best_l, best_v = o["J"], k, lv, o["jvec"] + bump(len(top_keys) * len(LEVS)) + RL, GL = rg({"TP01": 0.75, "SKH01": 0.25}, ii, xs13, dm=dmt) + oL, lvL = None, None + for lv in LEVS: + o = obj_rg(RL, GL, lv, ev, fu) + if oL is None or o["J"] > oL["J"]: + oL, lvL = o, lv + bump(len(LEVS)) + d_ = best_v - oL["jvec"] + # guardia contro l'errore del 22/08: un delta appaiato DEVE ereditare lo stress + # della riga in cui compare, o confronta due configurazioni non stressate. + assert abs(float(d_.mean()) - (best_j - oL["J"])) < 0.05, "delta appaiato incoerente" + print(f" {SLAB[dmt]:>28} {best_k:>12} {best_l:>6.3f} {best_j:>7.3f} " + f"{oL['J']:>11.3f} {d_.mean():>+11.4f} " + f"+-{d_.std(ddof=1)/np.sqrt(len(d_)):.4f}") + st_rows.append((lab, SLAB[dmt], best_k, best_l, best_j, oL["J"])) + print(" Le ultime due righe sono gli scenari di PROP-ALLOC. `XS x0.00` significava") + print(" 'il drift di XS01 potrebbe essere zero, non lo sappiamo': dopo XS01-OOS e") + print(" questo file NON e' piu' uno stress plausibile ma un contro-fattuale, perche'") + print(" quel drift e' MISURATO fuori campione ed e' positivo. Resta utile come") + print(" pavimento: dice quanto vale XS01 come pura DECORRELAZIONE, a rendimento nullo.") + tick("Β§5 banda") + + # ================================================== Β§6. XS01 ASSENTE + hr("Β§6. LO SCENARIO CHE UCCIDE TUTTO β€” la firm NON lista gli alt (assunto mai verificato)") + print(" Non e' uno stress sul drift: e' la RIMOZIONE dello sleeve. Il libro torna a due gambe") + print(" e il conto funded diventa il libro live a leva. E' l'unico scenario che non si misura") + print(" con un backtest β€” si verifica leggendo un listino β€” quindi qui se ne misura il DANNO.") + print(f"\n {'finestra':>28} {'con XS01: pesi':>15} {'lev':>5} {'J':>7} | " + f"{'SENZA XS01: pesi':>17} {'lev':>5} {'J':>7} | {'J perso':>8} {'quota':>7}") + absent = {} + for lab, ii in WINDOWS: + Gx, ev, fu = res[lab]["G"], res[lab]["ev"], res[lab]["fu"] + b = best_row(Gx) + sub2 = Gx[Gx["w"].str.endswith("/0")] + b2 = best_row(sub2) + absent[lab] = b2 + lost = float(b["J"]) - float(b2["J"]) + print(f" {lab:>28} {b['w']:>15} {b['lev']:>5.2f} {b['J']:>7.3f} | " + f"{b2['w']:>17} {b2['lev']:>5.2f} {b2['J']:>7.3f} | {lost:>+8.3f} " + f"{lost/max(b['J'],1e-9):>6.0%}") + print("\n NB: 'SENZA XS01' e' gia' ri-ottimizzato su TP/SKH e su tutte le leve β€” e' il MEGLIO") + print(" che il libro a due gambe puo' fare, non il 75/25 cablato.") + tick("Β§6 assenza") + + # ================================================== Β§7. LA SCALA DI CONTI + hr("Β§7. LA SCALA DI CONTI β€” P(>=50 EUR/g) da EUR 600 in 36 mesi, lente ACCOPPIATA") + print(f" macchineria del 25/07 (`pc.simulate`), de-luck x{PA.DELUCK}, {pl.MAX_CONCURRENT} conti" + f" max, morte-firm {pl.FIRM_DEATH_PER_YEAR:.0%}/anno, 1500 percorsi") + print(" (le tuple del libro vengono INIETTATE nella cache di `pc.book_tuples`, cosi' la") + print(" simulazione e' letteralmente la stessa, con un input diverso)") + print(f"\n {'finestra':>28} {'allocazione':>26} {'leva':>6} {'EUR/g med':>10} " + f"{'P(>=10/g)':>10} {'P(>=50/g)':>10} {'P(zero)':>8}") + + def ladder(ii, w: dict, lev: float, tag: str, wlab: str, drop_xs: bool = False) -> dict: + R, G = rg(w, ii, xs13, drop_xs=drop_xs) + cfg = (float(w.get("TP01", 0.0)), float(w.get("SKH01", 0.0)), + 0.0 if drop_xs else float(w.get("XS01", 0.0))) + key = (cfg[0], cfg[1], cfg[2], "shared") + saved = pc._TUP_CACHE.pop(key, None) + pc._TUP_CACHE[key] = (R, G, ii) + r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=lev, + seed=20260822, lens="coupled", start=str(ii[0].date())) + pc._TUP_CACHE.pop(key, None) + if saved is not None: + pc._TUP_CACHE[key] = saved + print(f" {tag:>28} {wlab:>26} {lev:>6.2f} {r['med']:>10.2f} {r['p_ge10']:>10.1%} " + f"{r['p_target']:>10.1%} {r['p_zero']:>8.1%}") + return r + + lad = {} + for lab, ii in WINDOWS: + Gx = res[lab]["G"] + b = best_row(Gx) + b2 = absent[lab] + short = lab.split()[0] + lad[(short, "argmax")] = ladder(ii, wdict(b["w"]), float(b["lev"]), short, + f"argmax {b['w']}") + lad[(short, "2550")] = ladder(ii, wdict("25/25/50"), 0.75, short, + "25/25/50 (PROP-ALLOC)") + lad[(short, "noxs")] = ladder(ii, wdict(b2["w"]), float(b2["lev"]), short, + f"SENZA XS01 {b2['w']}", drop_xs=True) + lad[(short, "live")] = ladder(ii, {"TP01": 0.75, "SKH01": 0.25}, 0.75, short, + "LIVE 75/25 @0.75x") + bump(12) + tick("Β§7 scala di conti") + + hr("Β§7-bis. LA BANDA DI FASE DEL NUMERO OPERATIVO β€” P(>=50/g) alle 10 fasi di XS01") + print(" Il numero che l'operatore legge non puo' stare su una fase sola. Peso fisso") + print(f" {W_ROB} (regione robusta), finestra W-LONG, leva per fase = quella che massimizza J.") + ev_l2, fu_l2 = res[WINDOWS[0][0]]["ev"], res[WINDOWS[0][0]]["fu"] + pt, med_eur, zeros = [], [], [] + print(f"\n {'fase':>5} {'leva':>6} {'J':>7} {'EUR/g med':>10} {'P(>=10/g)':>10} " + f"{'P(>=50/g)':>10} {'P(zero)':>8}") + for ph in range(10): + xsp = xs_tuples(p13, o13, h13, l13, phase=ph) + Rp, Gp = rg(wdict(W_ROB), I_LONG, xsp) + jb, lvb = -1.0, None + for lv in LEVS: + o = obj_rg(Rp, Gp, lv, ev_l2, fu_l2) + if o["J"] > jb: + jb, lvb = o["J"], lv + cfg = tuple(wdict(W_ROB).get(n, 0.0) for n in W3) + key = (cfg[0], cfg[1], cfg[2], "shared") + saved = pc._TUP_CACHE.pop(key, None) + pc._TUP_CACHE[key] = (Rp, Gp, I_LONG) + r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=lvb, + seed=20260822, lens="coupled", start=str(I_LONG[0].date())) + pc._TUP_CACHE.pop(key, None) + if saved is not None: + pc._TUP_CACHE[key] = saved + pt.append(r["p_target"]); med_eur.append(r["med"]); zeros.append(r["p_zero"]) + print(f" {ph:>5} {lvb:>6.3f} {jb:>7.3f} {r['med']:>10.2f} {r['p_ge10']:>10.1%} " + f"{r['p_target']:>10.1%} {r['p_zero']:>8.1%}") + bump(10 * len(LEVS)) + print(f"\n MEDIANA su 10 fasi: EUR/g {np.median(med_eur):.2f} | " + f"P(>=50/g) {np.median(pt):.1%} [p10 {np.percentile(pt,10):.1%}, " + f"p90 {np.percentile(pt,90):.1%}] | P(zero) {np.median(zeros):.1%}") + print(f" Contro il PUBBLICATO di PROP-ALLOC: P(>=50/g) 41.7%, P(zero) 13.9% (fase 0, XS01 a") + print(f" 19 gambe, finestra di scoperta).") + ladder_band = (float(np.median(pt)), float(np.percentile(pt, 10)), float(np.percentile(pt, 90)), + float(np.median(zeros))) + tick("Β§7-bis banda di fase della scala") + + # ================================================== Β§8. SINTESI + GATE + hr("Β§8. SINTESI β€” la banda da citare, e il gate da RI-REGISTRARE") + bL = best_row(res[WINDOWS[0][0]]["G"]) + bO = best_row(res[WINDOWS[1][0]]["G"]) + bD = best_row(res[WINDOWS[2][0]]["G"]) + print(f" J e argmax per finestra (XS01 = U13 Binance in tutte e tre, quindi la SOLA cosa che") + print(f" cambia fra le righe e' la FINESTRA):") + print(f" W-DISC 2024+ argmax {bD['w']:>10} @{bD['lev']:.2f}x J {bD['J']:.3f}") + print(f" W-OOS 2021-2023 argmax {bO['w']:>10} @{bO['lev']:.2f}x J {bO['J']:.3f}") + print(f" W-LONG 2021+ argmax {bL['w']:>10} @{bL['lev']:.2f}x J {bL['J']:.3f}") + print(f" (PROP-ALLOC pubblicato, XS01 HL 19 gambe, W-DISC: 25/25/50 @0.75x J 0.738)") + print(f"\n Gli stessi J, ma alla MEDIANA delle 10 fasi di XS01 (peso fisso 38/38/25):") + for lab, _ in WINDOWS: + pm = phase_med[lab] + print(f" {lab:>28} fase 0 {pm['ph0']:.3f} -> MEDIANA {pm['median']:.3f} " + f"[{pm['band'][0]:.3f},{pm['band'][1]:.3f}] LIVE {pm['jlive']:.3f} " + f"delta mediano {pm['dmed']:+.3f} ({pm['npos']}/10 >0)") + print(f"\n P(>=50 EUR/g) in 36 mesi, banda onesta:") + for short in ("W-LONG", "W-OOS", "W-DISC"): + row = " | ".join(f"{k}: {lad[(short, k)]['p_target']:.1%}" + for k in ("argmax", "2550", "noxs", "live") if (short, k) in lad) + print(f" {short:>8} {row}") + print(f"\n P(>=50 EUR/g) β€” IL NUMERO DA CITARE (W-LONG, peso {W_ROB}, mediana di 10 fasi):") + print(f" {ladder_band[0]:.1%} [banda di fase {ladder_band[1]:.1%} - {ladder_band[2]:.1%}]" + f" P(zero) {ladder_band[3]:.1%}") + print(f" contro il 41.7% pubblicato (fase 0, XS01 a 19 gambe, finestra di scoperta)") + print(f" e contro il {lad[('W-LONG', 'noxs')]['p_target']:.1%} se la firm non lista gli alt.") + + hr("Β§8-bis. IL GATE DA RI-REGISTRARE β€” `GATE PROP-01`") + print(""" SOSTITUISCE il gate del 22/08 ("non aprire un funded prima che XS01 abbia una + finestra fuori dal 2024-2026"), che e' CHIUSO: la finestra esiste, e' stata misurata, e + NON e' lei il problema (+0.061 di J, cioe' migliora). Tre gambe, in ordine di costo + crescente, perche' la prima puo' chiudere la questione da sola. + + (a) LISTINO β€” VERIFICA, non misura. Costo EUR 0. BLOCCANTE, prima di qualunque spesa, + nessuna data perche' precede tutto. + Leggere il listino della firm e contare quante delle 13 gambe U13 (BTC ETH BNB XRP + LTC ADA LINK DOGE SOL AVAX NEAR AAVE INJ) sono negoziabili CON SHORT ABILITATO. + PASS se >= 10 (il meccanismo congelato ha `min_assets = 10`: sotto, non ribilancia). + FAIL -> il canale non si chiude, ma si valuta sulla riga SENZA XS01: J 0.38-0.40 + invece di 0.45-0.52, P(>=50/g) ~6.6% invece di ~10%, e soprattutto la + raccomandazione "riallocare per la barriera" DECADE (senza XS01 l'argmax e' 38/62 o + 50/50, cioe' un tilt su SKH01, lo sleeve che il progetto misura come il piu' fragile). + E' la stessa classe dell'errore GTAA01/PRIIPs: 5 settimane di misure su un'assunzione + mai verificata sul conto reale, e il controllo costo' un ordine di prova. + + (b) BANDA D'ANCORA DI SKH01 SOTTO LA LENTE PROP β€” MISURA. Data: 2026-10-31. + E' l'unica distorsione dichiarata due volte (PROP-ALLOC e questo file) e mai rimossa. + Costo: rifare `pc._hourly_legs` ai 23 offset della griglia 230m/690m e rigirare Β§3. + Metrica: MEDIANA DELLE DIFFERENZE APPAIATE J(peso nella regione w_XS 25-38%) - + J(libro LIVE 75/25) sui 23 offset, finestra W-LONG. + PASS se > +0.05. Surrogato di oggi (taglio del drift SKH x0.50 / x0.33): +0.124 e + +0.113; banda di FASE di XS01, che ho potuto girare: +0.175, positiva in 10/10. + Mi aspetto un PASS e lo dichiaro prima. FAIL <= +0.05 -> il vantaggio era calendario. + + (c) CAPITALE DEL BIGLIETTO β€” condizione, non data. Un'eval HYRO $100k costa $579 su un + conto di $635. E' gia' dentro la simulazione della scala (e' gran parte del P(zero) + del 26-34%), ma va detto: la prima eval consuma il 91% del conto vivo. + + COSA FA FALLIRE L'INTERO RISULTATO + 1. il listino non ha 10 gambe shortabili -> (a); + 2. la banda d'ancora di SKH01 azzera il delta -> (b); + 3. si continua a citare 0.738. E' un numero a 19 GAMBE, e le 6 che lo separano dalle + 13 testabili valgono -0.307 di J MISURATI sulla stessa finestra e sullo stesso + venue = il 42% del totale. Quelle 6 (ARB OP SUI APT SEI TIA) non hanno storia + fuori campione E sono state incluse nell'universo nel 2026-06 guardando la + liquidita' HL del 2024+, cioe' dentro la finestra di scoperta.""") + + print(f"\n TRIAL/CELLE DICHIARATI (al rialzo): {TRIALS}") + print(f"\n fatto in {time.time()-t0:.0f}s") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_surface_rv.py b/scripts/research/r0822b_surface_rv.py new file mode 100644 index 0000000..2d56bc6 --- /dev/null +++ b/scripts/research/r0822b_surface_rv.py @@ -0,0 +1,823 @@ +"""SURFACE-RV β€” la superficie di volatilita' come RELATIVE VALUE fra i suoi punti. + +IPOTESI (registrata prima di misurare): la superficie Deribit e' internamente INCOERENTE in modo +misurabile β€” strike adiacenti fuori da una curva liscia, varianza totale non monotona nel tenore β€” +e l'incoerenza si ricompone. E' un'ipotesi di MICROSTRUTTURA, non di previsione: non chiede alla +vol implicita di prevedere nulla (cosa che il progetto ha gia' refutato quattro volte: dvol_directional, +gamma scalping, term-structure come segnale, skew come segnale). +ATTESA DICHIARATA PRIMA: l'incoerenza esiste al MID ma non sopravvive al bid-ask, perche' su +un'opzione a 7 giorni un punto di vol vale pochi dollari mentre attraversare lo spread ne costa +il ~10% del premio (misurato dal filone 11 sulle inverse). + +LA DIFESA CONTRO IL RISCHIO NUMERO UNO DI QUESTO FILONE (misurare il proprio errore di +interpolazione) e' strutturale, non statistica: i due test principali sono **arbitraggi statici +model-free** β€” monotonia e convessita' del prezzo nello strike β€” che non richiedono NESSUN +interpolatore, NESSUN modello e NESSUN tasso. Valgono per ogni misura di prezzo, path per path: + * put: P(K) crescente in K (verticale) + * put: P(K) convessa in K (farfalla) +e sono verificati su prezzi ESEGUIBILI (si vende al bid, si compra all'ask), mai al mid. +Il terzo test β€” varianza totale non monotona nel tenore β€” richiede di confrontare tenori diversi: +si riporta sia nella versione SENZA interpolazione (strike condiviso fra due scadenze) sia in +quella con interpolazione a log-moneyness fisso, e la DIFFERENZA fra le due e' la misura diretta +di quanto pesa l'interpolatore. + +Convenzioni verificate nel codice (non assunte): + * tick: 0.0001 coin sotto 0.005, 0.0005 sopra -> contato, non dichiarato + * f_markfit: la mark IV del venue riprezza i propri mid? (il filone 5 misura 0.992) + * fee Deribit opzioni: 0.03% del sottostante per gamba, cap 12.5% del premio della gamba + +Il dato: `data/raw/cb_chain/` (archivio bite + raccolta propria). SOLO opzioni INVERSE β€” il +collettore interroga {"currency": "BTC"|"ETH"}, che non restituisce la famiglia USDC-lineare +(difetto di produzione trovato dal filone 11). Quindi ogni numero qui e' misurato sulla famiglia +che il conto NON puo' marginare, e le conclusioni sulla famiglia USDC sono ESTRAPOLAZIONI +dichiarate, non misure. + +Muro del dato: prima del 2026-06-09 l'archivio raccoglieva UNA scadenza per giro -> la SUPERFICIE +esiste da li' (~75 giorni). Lo SMILE (piu' strike su UNA scadenza) esiste dal 2026-05-01 (~113 g). +Verdetto massimo possibile: LEAD con gate pre-registrato. Mai CANDIDATO. + + uv run python scripts/research/r0822b_surface_rv.py +""" +from __future__ import annotations + +import glob +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy.stats import norm + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +STORE = ROOT / "data" / "raw" / "cb_chain" + +SMILE_START = pd.Timestamp("2026-05-01", tz="UTC") # piu' strike su UNA scadenza +SURF_START = pd.Timestamp("2026-06-09", tz="UTC") # piu' scadenze per giro -> superficie + +FEE_UNDERLYING = 0.0003 # 0.03% del sottostante, per gamba, in coin per contratto +FEE_CAP_FRAC = 0.125 # cap: 12.5% del premio della gamba +MIN_LOT = {"BTC": 0.1, "ETH": 1.0} # famiglia INVERSE (quella che abbiamo nel dato) +MIN_LOT_USDC = {"BTC": 0.01, "ETH": 0.1} # famiglia USDC-lineare (non raccolta: estrapolazione) + +DTE_MIN = 1.0 # sotto un giorno il book e' un'altra cosa; riportato anche senza filtro + + +def tick_of(px: np.ndarray) -> np.ndarray: + """Griglia di prezzo Deribit sulle opzioni, in coin. Verificata sui dati in `certifica()`.""" + return np.where(px < 0.005, 0.0001, 0.0005) + + +# --------------------------------------------------------------------------- dato + +def load(assets=("BTC", "ETH")) -> pd.DataFrame: + cols = ["ts", "asset", "instrument_name", "strike", "option_type", "exp", + "bid", "ask", "mid", "iv", "delta", "open_interest", "book_depth_top3", + "underlying_price", "index_price", "quote_status"] + parts = [] + for f in sorted(glob.glob(str(STORE / "*.parquet"))): + d = pd.read_parquet(f, columns=cols) + parts.append(d[d["asset"].isin(assets)]) + d = pd.concat(parts, ignore_index=True) + d["ts"] = pd.to_datetime(d["ts"], utc=True) + d["exp"] = pd.to_datetime(d["exp"], utc=True) + for c in ("strike", "bid", "ask", "mid", "iv", "delta", "open_interest", + "book_depth_top3", "underlying_price", "index_price"): + d[c] = pd.to_numeric(d[c], errors="coerce") + d = d.drop_duplicates(subset=["ts", "instrument_name"], keep="last") + d["dte"] = (d["exp"] - d["ts"]).dt.total_seconds() / 86400.0 + d["T"] = d["dte"] / 365.25 + d = d.reset_index(drop=True) + d["spot_feed"] = _spot_asof(d) + return d + + +def _spot_asof(d: pd.DataFrame) -> pd.Series: + """Indice dal feed CERTIFICATO (1h Deribit mainnet), asof sul ts della quota. + + Serve perche' `bite_archive` NON ha ne' `index_price` ne' `underlying_price` (100% None: + misurato, non assunto) -> senza questo la finestra con un divisore sarebbe di 23 giorni. + """ + from scripts.analysis.research_lab import load_tf + out = pd.Series(np.nan, index=d.index) + for a in d["asset"].unique(): + px = load_tf(a, "1h") + s = pd.Series(px["close"].to_numpy(float), + index=pd.to_datetime(px["timestamp"], unit="ms", utc=True)).sort_index() + s.index = pd.DatetimeIndex(s.index).as_unit("ns") + m = d["asset"] == a + t = pd.DatetimeIndex(d.loc[m, "ts"]).as_unit("ns") + # epoca esplicita in ns (mai `.view("int64")` su indici tz-aware: lezione 01/07) + si, ti = s.index.asi8, t.asi8 + pos = np.searchsorted(si, ti, side="right") - 1 + v = np.where(pos >= 0, s.to_numpy()[np.clip(pos, 0, None)], np.nan) + out.loc[m] = v + return out + + +def forward(d: pd.DataFrame) -> pd.DataFrame: + """Forward per (asset, ts, exp) ricostruito con la PARITA' PUT-CALL, model-free. + + In convenzione inverse il premio e' quotato in coin: (C - P) * X = F - K => F = K + (C-P)*X, + con X = divisore della convenzione (misurato in `certifica`). Mediana sugli strike = robusta. + Dove il venue lo ha registrato (solo la raccolta propria) si usa il valore OSSERVATO, e + l'accordo fra i due e' una certificazione, non un'assunzione. + """ + q = d[(d["bid"] > 0) & (d["ask"] > 0) & (d["ask"] >= d["bid"]) & d["spot_feed"].notna()] + c = q[q["option_type"] == "C"][["asset", "ts", "exp", "strike", "mid", "spot_feed"]] + p = q[q["option_type"] == "P"][["asset", "ts", "exp", "strike", "mid"]] + j = c.merge(p, on=["asset", "ts", "exp", "strike"], suffixes=("_c", "_p")) + j["F_k"] = j["strike"] + (j["mid_c"] - j["mid_p"]) * j["spot_feed"] + f = j.groupby(["asset", "ts", "exp"])["F_k"].median().rename("F_pcp").reset_index() + return f + + +def two_sided(d: pd.DataFrame) -> pd.DataFrame: + """Quotate a DUE lati. Una gamba sola quotata non e' meta' struttura: e' un'altra cosa. + + (Regola 8 del brief: una riga presente non e' un dato presente.) + """ + q = d[(d["bid"] > 0) & (d["ask"] > 0) & (d["ask"] >= d["bid"])].copy() + q = q[q["T"] > 0] + return q + + +# --------------------------------------------------------------------------- prezzo + +def bs_usd(F, K, T, sig, is_put): + """Black-76 con r=0 (Deribit prezza sul FORWARD della scadenza = `underlying_price`).""" + F = np.asarray(F, float); K = np.asarray(K, float) + T = np.asarray(T, float); sig = np.asarray(sig, float) + out = np.full(F.shape, np.nan) + ok = (T > 0) & (sig > 0) & (F > 0) & (K > 0) + if not ok.any(): + return out + d1 = np.zeros_like(F); d2 = np.zeros_like(F) + sq = np.sqrt(np.where(ok, T, 1.0)) + d1[ok] = (np.log(F[ok] / K[ok]) + 0.5 * sig[ok] ** 2 * T[ok]) / (sig[ok] * sq[ok]) + d2[ok] = d1[ok] - sig[ok] * sq[ok] + call = F * norm.cdf(d1) - K * norm.cdf(d2) + put = K * norm.cdf(-d2) - F * norm.cdf(-d1) + out[ok] = np.where(is_put[ok], put[ok], call[ok]) + return out + + +def bs_vega_usd(F, K, T, sig): + F = np.asarray(F, float); K = np.asarray(K, float) + T = np.asarray(T, float); sig = np.asarray(sig, float) + out = np.zeros(F.shape) + ok = (T > 0) & (sig > 0) & (F > 0) & (K > 0) + sq = np.sqrt(np.where(ok, T, 1.0)) + d1 = np.zeros_like(F) + d1[ok] = (np.log(F[ok] / K[ok]) + 0.5 * sig[ok] ** 2 * T[ok]) / (sig[ok] * sq[ok]) + out[ok] = F[ok] * norm.pdf(d1[ok]) * sq[ok] # per 1.00 di sigma (=100 punti di vol) + return out + + +# --------------------------------------------------------------------------- certificazione + +def certifica(d: pd.DataFrame) -> pd.DataFrame: + print("=" * 100) + print("0. CERTIFICAZIONE DEL DATO β€” cosa esiste davvero, prima di qualunque test") + print("=" * 100) + q = two_sided(d) + print(f"righe totali {len(d):,} Β· quotate a due lati {len(q):,} ({len(q)/len(d):.1%})") + print(f"finestra {d['ts'].min():%Y-%m-%d} -> {d['ts'].max():%Y-%m-%d}") + + # (a) muro del dato: scadenze per giro + per = d.groupby([d["ts"].dt.floor("D"), "asset"])["exp"].nunique().unstack() + pre = per[per.index < SURF_START] + post = per[per.index >= SURF_START] + print("\n(a) MURO DEL DATO β€” scadenze per giro (mediana per giorno):") + print(f" pre {SURF_START:%Y-%m-%d}: BTC {pre['BTC'].median():.0f} Β· ETH {pre['ETH'].median():.0f}" + f" (max BTC {pre['BTC'].max():.0f}) -> la SUPERFICIE non esiste") + print(f" post {SURF_START:%Y-%m-%d}: BTC {post['BTC'].median():.0f} Β· ETH {post['ETH'].median():.0f}" + f" -> superficie su {len(post)} giorni") + print(f" smile (piu' strike, UNA scadenza) su {len(per)} giorni dal {per.index.min():%Y-%m-%d}" + f" -> i test model-free girano su questa finestra piu' lunga") + + # (a-bis) BUCO DI COLONNA nell'archivio ereditato + obs = d["index_price"].notna() + print("\n(a-bis) BUCO DI COLONNA β€” `bite_archive` non registrava il sottostante") + print(f" index_price/underlying_price presenti in {obs.sum():,}/{len(d):,} righe ({obs.mean():.1%})" + f" β€” solo la raccolta propria, dal {d.loc[obs,'ts'].min():%Y-%m-%d}") + print(f" book_depth_top3 presente in {d['book_depth_top3'].notna().mean():.1%} delle righe") + print(" -> i test 1-2 (model-free) NON ne hanno bisogno; il test 4 (calendario) si')") + + # (b) tick: verificato, non dichiarato + print("\n(b) GRIGLIA DI PREZZO (verificata sui dati, non assunta)") + for a in sorted(q["asset"].unique()): + b = q.loc[q["asset"] == a, "bid"].to_numpy() + t = tick_of(b) + off = np.abs(np.round(b / t) - b / t) > 1e-6 + n_tick = np.round(b / t) + print(f" {a}: fuori griglia {off.sum():,}/{len(b):,} ({off.mean():.4%}) Β· " + f"bid in tick: mediana {np.median(n_tick):.0f} Β· quota <=2 tick {np.mean(n_tick <= 2):.1%}") + + # (c) il divisore della convenzione inverse + f_markfit + print("\n(c) CONVENZIONE INVERSE e f_markfit β€” la mark IV riprezza i propri mid?") + ov = q[obs.reindex(q.index, fill_value=False)].dropna(subset=["iv", "mid", "spot_feed"]) + ov = ov[(ov["iv"] > 0) & (ov["mid"] > 0) & (ov["dte"] >= DTE_MIN)] + dev = (ov["index_price"] / ov["spot_feed"] - 1).abs() + print(f" index_price della catena vs feed CERTIFICATO 1h: |dev| mediana {dev.median():.5%} " + f"Β· p99 {dev.quantile(.99):.4%} (n={len(ov):,}) -> il feed puo' fare da divisore sull'archivio") + sm = ov.sample(n=min(120_000, len(ov)), random_state=0) + is_put = (sm["option_type"] == "P").to_numpy() + usd = bs_usd(sm["underlying_price"].to_numpy(), sm["strike"].to_numpy(), + sm["T"].to_numpy(), (sm["iv"] / 100).to_numpy(), is_put) + best = None + for nome, div in (("index_price", sm["index_price"].to_numpy()), + ("underlying_price(fwd)", sm["underlying_price"].to_numpy()), + ("feed certificato 1h", sm["spot_feed"].to_numpy())): + f = (usd / div) / sm["mid"].to_numpy() + f = f[np.isfinite(f) & (f > 0)] + print(f" f_markfit con divisore {nome:<22}: mediana {np.median(f):.4f} " + f"[p10 {np.percentile(f,10):.3f}, p90 {np.percentile(f,90):.3f}] n={len(f):,}") + if best is None or abs(np.median(f) - 1) < abs(best[1] - 1): + best = (nome, float(np.median(f))) + print(f" -> convenzione: divisore = {best[0]} (f_markfit {best[1]:.4f}); " + f"un f molto lontano da 1 direbbe che il problema e' il MIO prezzatore, non il mercato") + + # (d) forward ricostruito con la parita' put-call, certificato contro l'osservato + print("\n(d) FORWARD ricostruito con la PARITA' PUT-CALL (model-free) vs osservato") + F = forward(d) + chk = d.loc[obs, ["asset", "ts", "exp", "underlying_price"]].drop_duplicates( + subset=["asset", "ts", "exp"]).merge(F, on=["asset", "ts", "exp"], how="inner") + e = (chk["F_pcp"] / chk["underlying_price"] - 1) + print(f" n={len(chk):,} coppie (asset,ts,scadenza) con entrambi: " + f"|errore| mediana {e.abs().median():.5%} Β· p95 {e.abs().quantile(.95):.4%} Β· " + f"corr {np.corrcoef(chk['F_pcp'], chk['underlying_price'])[0,1]:.6f}") + return q, F + + +# --------------------------------------------------------------------------- fee + +def fee_coin(premio_coin: np.ndarray, n_contratti: float) -> np.ndarray: + """Fee Deribit opzioni per gamba, in coin: min(0.03% del sottostante, 12.5% del premio) x lotti.""" + per_contratto = np.minimum(FEE_UNDERLYING, FEE_CAP_FRAC * np.asarray(premio_coin, float)) + return per_contratto * n_contratti + + +# --------------------------------------------------------------------------- TEST V + +def test_verticale(q: pd.DataFrame, dte_min: float) -> dict: + """Monotonia del prezzo nello strike. Model-free, nessun interpolatore, nessun tasso. + + put: P(K) crescente -> arbitraggio se bid(K1) > ask(K2) con K1 < K2 + (vendi la put bassa al bid, compra l'alta all'ask: credito + payoff sempre >= 0) + call: C(K) decrescente -> arbitraggio se bid(K2) > ask(K1) con K1 < K2 + """ + x = q[q["dte"] >= dte_min].sort_values(["asset", "ts", "exp", "option_type", "strike"]) + key = ["asset", "ts", "exp", "option_type"] + g = x.groupby(key, sort=False) + # per le put il "meglio finora" salendo di strike; per le call scendendo + x["cmax_up"] = g["bid"].cummax() + x["prev_up"] = g["cmax_up"].shift(1) # miglior bid su strike STRETTAMENTE inferiori + xr = x.iloc[::-1].copy() + gr = xr.groupby(key, sort=False) + xr["cmax_dn"] = gr["bid"].cummax() + x["prev_dn"] = gr["cmax_dn"].shift(1) # miglior bid su strike STRETTAMENTE superiori + is_put = x["option_type"].to_numpy() == "P" + prev = np.where(is_put, x["prev_up"].to_numpy(), x["prev_dn"].to_numpy()) + cred = prev - x["ask"].to_numpy() # credito riskless in coin, per contratto + viol = np.isfinite(cred) & (cred > 0) + n_tot = len(x) + out = {"n_confronti": int(np.isfinite(cred).sum()), "n_viol": int(viol.sum()), + "quota": float(viol.sum() / max(1, np.isfinite(cred).sum()))} + if viol.any(): + vv = x.loc[viol] + out["credito_med_coin"] = float(np.median(cred[viol])) + out["credito_usd_medlot"] = float(np.median( + cred[viol] * vv["asset"].map(MIN_LOT).to_numpy() * vv["spot_feed"].to_numpy())) + return out + + +# --------------------------------------------------------------------------- TEST B + +def test_farfalla(q: pd.DataFrame, dte_min: float) -> pd.DataFrame: + """Convessita' del prezzo nello strike su triple CONSECUTIVE. Model-free. + + lam = (K3-K2)/(K3-K1); payoff lam*P(K1) + (1-lam)*P(K3) - P(K2) >= 0 sempre. + Costo eseguibile = lam*ask1 + (1-lam)*ask3 - bid2. Se < 0 -> credito riskless. + Al MID lo stesso numero misura l'INCOERENZA (non eseguibile) β€” si riportano entrambi. + """ + x = q[q["dte"] >= dte_min].sort_values(["asset", "ts", "exp", "option_type", "strike"]) + key = ["asset", "ts", "exp", "option_type"] + g = x.groupby(key, sort=False) + for c in ("strike", "bid", "ask", "mid", "iv", "book_depth_top3"): + x[c + "_p"] = g[c].shift(1) + x[c + "_n"] = g[c].shift(-1) + x = x.dropna(subset=["strike_p", "strike_n", "bid_p", "bid_n", "ask_p", "ask_n"]) + K1, K2, K3 = x["strike_p"].to_numpy(), x["strike"].to_numpy(), x["strike_n"].to_numpy() + lam = (K3 - K2) / (K3 - K1) + x["lam"] = lam + # eseguibile: compro le ali all'ask, vendo il corpo al bid + x["cost_exec"] = lam * x["ask_p"] + (1 - lam) * x["ask_n"] - x["bid"] + # incoerenza al mid (non eseguibile, ma e' la lente "la superficie e' liscia?") + x["cost_mid"] = lam * x["mid_p"] + (1 - lam) * x["mid_n"] - x["mid"] + # la stessa cosa in punti di vol: residuo di convessita' locale dello smile + x["iv_resid"] = x["iv"] - (lam * x["iv_p"] + (1 - lam) * x["iv_n"]) + return x + + +# --------------------------------------------------------------------------- vol points + +def in_punti_vol(x: pd.DataFrame) -> pd.DataFrame: + """Mezzo spread denaro-lettera convertito in punti di vol via vega BS (r=0, forward).""" + vega_usd = bs_vega_usd(x["F"].to_numpy(), x["strike"].to_numpy(), + x["T"].to_numpy(), (x["iv"] / 100).to_numpy()) + vega_coin_per_pt = vega_usd / x["spot_feed"].to_numpy() / 100.0 + x = x.copy() + x["vega_coin_pt"] = vega_coin_per_pt + x["halfspread_pt"] = (x["ask"] - x["bid"]) / 2.0 / np.clip(vega_coin_per_pt, 1e-12, None) + x["relspread"] = (x["ask"] - x["bid"]) / x["mid"] + # quanto vale UN TICK in punti di vol: se il tick e' piu' grosso dell'incoerenza, + # l'incoerenza non e' esprimibile nella griglia di prezzo del venue. + x["tick_pt"] = tick_of(x["mid"].to_numpy()) / np.clip(vega_coin_per_pt, 1e-12, None) + return x + + +# --------------------------------------------------------------------------- TEST C + +def test_calendario(q: pd.DataFrame, dte_min: float, ogni: int = 4) -> dict: + """Varianza totale w = iv^2 * T: deve CRESCERE col tenore. Due lenti, e la loro differenza. + + (i) STRIKE CONDIVISO fra due scadenze adiacenti: nessuna interpolazione. + (ii) LOG-MONEYNESS FISSO k = ln(K/F): richiede di interpolare lo smile lungo in k. + La differenza fra (i) e (ii) e' la misura diretta del contributo dell'INTERPOLATORE. + """ + x = q[(q["dte"] >= dte_min) & q["iv"].notna() & (q["iv"] > 0) & q["F"].notna()].copy() + x = x[x["ts"] >= SURF_START] + tenuti = sorted(x["ts"].unique())[::ogni] # (i) e (ii) sugli STESSI snapshot: appaiato + x = x[x["ts"].isin(tenuti)] + x["w"] = (x["iv"] / 100.0) ** 2 * x["T"] + x["k"] = np.log(x["strike"] / x["F"]) + + # --- (i) strike condiviso, adiacenti nel tempo + x = x.sort_values(["asset", "ts", "option_type", "strike", "exp"]) + key = ["asset", "ts", "option_type", "strike"] + g = x.groupby(key, sort=False) + nxt_w = g["w"].shift(-1).to_numpy() + nxt_T = g["T"].shift(-1).to_numpy() + nxt_iv = g["iv"].shift(-1).to_numpy() + ok = np.isfinite(nxt_w) & (nxt_T > x["T"].to_numpy()) + dw = nxt_w - x["w"].to_numpy() + viol_i = ok & (dw < 0) + # ampiezza in punti di vol: quanto dovrebbe salire l'iv lunga per ripristinare la monotonia + iv_need = np.sqrt(np.maximum(x["w"].to_numpy(), 0) / np.maximum(nxt_T, 1e-9)) * 100.0 + amp_pt = np.where(ok, iv_need - nxt_iv, np.nan) + + room = amp_pt[ok] # <0 = margine, >0 = violazione + res = {"room_med_pt": float(np.nanmedian(room)), "room_p5_pt": float(np.nanpercentile(room, 95)), + "i_n": int(ok.sum()), "i_viol": int(viol_i.sum()), + "i_quota": float(viol_i.sum() / max(1, ok.sum())), + "i_amp_med_pt": float(np.nanmedian(amp_pt[viol_i])) if viol_i.any() else np.nan, + "i_amp_p95_pt": float(np.nanpercentile(amp_pt[viol_i], 95)) if viol_i.any() else np.nan} + + # --- (ii) log-moneyness fisso, con interpolazione + y = x + n_par, n_vio, amps = 0, 0, [] + for (a, ts, ot), gg in y.groupby(["asset", "ts", "option_type"], sort=False): + exps = sorted(gg["exp"].unique()) + if len(exps) < 2: + continue + for e1, e2 in zip(exps[:-1], exps[1:]): + s1 = gg[gg["exp"] == e1].sort_values("k") + s2 = gg[gg["exp"] == e2].sort_values("k") + if len(s1) < 2 or len(s2) < 2: + continue + k1 = s1["k"].to_numpy() + inside = (k1 >= s2["k"].min()) & (k1 <= s2["k"].max()) # MAI estrapolare + if not inside.any(): + continue + iv2 = np.interp(k1[inside], s2["k"].to_numpy(), s2["iv"].to_numpy()) + T2 = float(s2["T"].iloc[0]) + w2 = (iv2 / 100.0) ** 2 * T2 + w1 = s1["w"].to_numpy()[inside] + n_par += int(inside.sum()) + bad = w2 < w1 + n_vio += int(bad.sum()) + if bad.any(): + need = np.sqrt(w1[bad] / T2) * 100.0 + amps.append(need - iv2[bad]) + amps = np.concatenate(amps) if amps else np.array([]) + res.update({"ii_n": n_par, "ii_viol": n_vio, "ii_quota": n_vio / max(1, n_par), + "ii_amp_med_pt": float(np.median(amps)) if len(amps) else np.nan, + "ii_ogni": ogni}) + # valore in dollari dell'ampiezza (i) al lotto minimo, contro il costo di 2 gambe + vv = x.loc[viol_i].copy() + if len(vv): + vv = in_punti_vol(vv) + amp = pd.Series(amp_pt, index=x.index)[viol_i].to_numpy() + lot = vv["asset"].map(MIN_LOT).to_numpy() + val = np.abs(amp) * vv["vega_coin_pt"].to_numpy() * lot * vv["spot_feed"].to_numpy() + costo = ((vv["ask"] - vv["bid"]).to_numpy() * lot * vv["spot_feed"].to_numpy() + + 2 * fee_coin(vv["mid"].to_numpy(), lot) * vv["spot_feed"].to_numpy()) + res.update({"i_val_usd_med": float(np.nanmedian(val)), + "i_costo_usd_med": float(np.nanmedian(costo)), + "i_n_val": int(np.isfinite(val).sum())}) + return res + + +# --------------------------------------------------------------------------- economia + +def economia(bf: pd.DataFrame, asset: str) -> dict: + """Quanto vale, in dollari, l'incoerenza di una farfalla al lotto minimo β€” netto fee. + + Struttura a 3 gambe, lotti INTERI: con strike equispaziati e' 1:2:1 sul lotto minimo. + Si conta la fee gamba per gamba col cap sul premio, e si usa il prezzo ESEGUIBILE. + """ + x = bf[bf["asset"] == asset] + lot = MIN_LOT[asset] + S = x["spot_feed"].to_numpy() + # credito lordo della farfalla eseguibile, in coin per unita' di "corpo" + cred = -x["cost_exec"].to_numpy() # >0 = credito riskless + # lotti interi: corpo 2*lot, ali lot ciascuna (equivale a lam=0.5 -> solo strike equispaziati) + eq = np.isclose(x["lam"].to_numpy(), 0.5, atol=0.02) + cred_eq_coin = (x["bid"].to_numpy() * 2 * lot + - x["ask_p"].to_numpy() * lot - x["ask_n"].to_numpy() * lot) + f = (fee_coin(x["bid"].to_numpy(), 2 * lot) + + fee_coin(x["ask_p"].to_numpy(), lot) + fee_coin(x["ask_n"].to_numpy(), lot)) + netto_usd = (cred_eq_coin - f) * S + return { + "asset": asset, "n": int(len(x)), "n_equisp": int(eq.sum()), + "cred_lordo_p99_coin": float(np.nanpercentile(cred, 99)), + "fee_3gambe_usd_med": float(np.nanmedian(f[eq] * S[eq])) if eq.any() else np.nan, + "netto_usd_max": float(np.nanmax(netto_usd[eq])) if eq.any() else np.nan, + "netto_usd_p99": float(np.nanpercentile(netto_usd[eq], 99)) if eq.any() else np.nan, + "n_netto_pos": int(np.nansum(netto_usd[eq] > 0)), + "nozionale_min_lotto_usd": float(np.nanmedian(S) * lot), + } + + +# --------------------------------------------------------------------------- persistenza + +def persistenza(bf: pd.DataFrame, viol_col: str) -> dict: + """Una violazione che sparisce all'ora dopo e' un guizzo di book, non un'opportunita'. + + Chiave = (asset, exp, tipo, tripla di strike). Si guarda se la stessa tripla e' ancora in + violazione allo snapshot successivo IN CUI LA TRIPLA E' QUOTATA (non 'l'ora dopo' in astratto). + """ + v = bf[bf[viol_col]].copy() + if v.empty: + return {"n": 0} + bf = bf.copy() + bf["trip"] = (bf["asset"] + "|" + bf["exp"].astype(str) + "|" + bf["option_type"] + "|" + + bf["strike_p"].astype(str) + "|" + bf["strike"].astype(str) + "|" + + bf["strike_n"].astype(str)) + bf = bf.sort_values(["trip", "ts"]) + nxt = bf.groupby("trip", sort=False)[viol_col].shift(-1) + m = bf[viol_col] & nxt.notna() + return {"n": int(m.sum()), "resta": float(nxt[m].mean()) if m.any() else np.nan} + + +def controfattuale_usdc(bf: pd.DataFrame, asset: str, rel_usdc: float, + solo_gia_strette: bool = False) -> dict: + """SE la famiglia USDC quotasse la STESSA superficie col SUO spread, sarebbe eseguibile? + + Controfattuale DICHIARATO, non una misura: il collettore non raccoglie le USDC (difetto di + produzione del filone 11), quindi si tiene il mid osservato sulle inverse e si stringe lo + spread al valore relativo misurato dagli altri filoni sulla famiglia USDC, con il lotto + minimo USDC e la stessa formula di fee. Assunzione dichiarata: stessa griglia di tick + (una griglia piu' grossa peggiorerebbe il risultato, quindi questo e' un LIMITE SUPERIORE). + """ + x = bf[(bf["asset"] == asset)].dropna(subset=["mid", "mid_p", "mid_n"]).copy() + x = x[np.isclose(x["lam"], 0.5, atol=0.02)] + if solo_gia_strette: + # il controfattuale ingenuo prende il MID di un mercato larghissimo e finge di poterlo + # quotare al 2,6%: e' proprio dove il mid e' meno informativo. Qui si tiene solo la parte + # di superficie GIA' quotata stretta quanto la famiglia USDC, dove il mid vale qualcosa. + rs = np.maximum.reduce([((x["ask"] - x["bid"]) / x["mid"]).to_numpy(), + ((x["ask_p"] - x["bid_p"]) / x["mid_p"]).to_numpy(), + ((x["ask_n"] - x["bid_n"]) / x["mid_n"]).to_numpy()]) + x = x[rs <= rel_usdc] + lot = MIN_LOT_USDC[asset] + out = {} + for nome, rel in (("inverse osservato", None), (f"{asset}_USDC controfattuale", rel_usdc)): + if rel is None: + b, a_p, a_n = x["bid"], x["ask_p"], x["ask_n"] + l = MIN_LOT[asset] + else: + b = x["mid"] * (1 - rel / 2) + a_p = x["mid_p"] * (1 + rel / 2) + a_n = x["mid_n"] * (1 + rel / 2) + l = lot + cred = (b.to_numpy() * 2 * l - a_p.to_numpy() * l - a_n.to_numpy() * l) + f = (fee_coin(b.to_numpy(), 2 * l) + fee_coin(a_p.to_numpy(), l) + fee_coin(a_n.to_numpy(), l)) + netto = (cred - f) * x["spot_feed"].to_numpy() + out[nome] = {"n": int(len(x)), "pos": int(np.nansum(netto > 0)), + "pos1": int(np.nansum(netto > 1.0)), "somma": float(np.nansum(netto[netto > 0])), + "max": float(np.nanmax(netto)) if len(x) else np.nan, + "p999": float(np.nanpercentile(netto, 99.9)) if len(x) else np.nan, + "lotto_usd": float(np.nanmedian(x["spot_feed"]) * l) if len(x) else np.nan} + return out + + +# --------------------------------------------------------------------------- controlli positivi + +def _finto(rows) -> pd.DataFrame: + """Catena sintetica minima per i controlli positivi.""" + d = pd.DataFrame(rows) + d["ts"] = pd.to_datetime(d["ts"], utc=True) + d["exp"] = pd.to_datetime(d["exp"], utc=True) + d["dte"] = (d["exp"] - d["ts"]).dt.total_seconds() / 86400.0 + d["T"] = d["dte"] / 365.25 + d["mid"] = (d["bid"] + d["ask"]) / 2 + for c, v in (("spot_feed", 100_000.0), ("F", 100_000.0), ("option_type", "P"), + ("open_interest", 1000.0), ("book_depth_top3", 10.0), ("delta", -0.2)): + if c not in d: + d[c] = v + return d + + +def controlli_positivi() -> None: + """Un rilevatore tarato per non segnalare e' indistinguibile da uno rotto. + + Regola gia' codificata nel progetto (venue_watch, 26/07). Qui i tre test vengono puntati + su violazioni COSTRUITE: se non le vedono, i loro zeri non valgono niente. + """ + print("=" * 100) + print("CONTROLLI POSITIVI β€” i tre rilevatori vedono una violazione COSTRUITA?") + print("=" * 100) + base = dict(ts="2026-08-01T00:00Z", exp="2026-08-15T08:00Z", asset="BTC", + option_type="P", iv=50.0, instrument_name="x") + # (1) verticale: put a strike BASSO con bid > ask della put a strike ALTO + v_ok = _finto([{**base, "strike": 90000, "bid": 0.0300, "ask": 0.0305}, + {**base, "strike": 95000, "bid": 0.0200, "ask": 0.0205}]) + v_no = _finto([{**base, "strike": 90000, "bid": 0.0200, "ask": 0.0205}, + {**base, "strike": 95000, "bid": 0.0300, "ask": 0.0305}]) + r_ok, r_no = test_verticale(v_ok, 0.0), test_verticale(v_no, 0.0) + print(f" TEST V: catena INVERTITA -> {r_ok['n_viol']} violazioni (attese >=1) Β· " + f"catena sana -> {r_no['n_viol']} (attese 0)") + # (2) farfalla: corpo troppo caro (concavita') + b_ok = _finto([{**base, "strike": 90000, "bid": 0.0100, "ask": 0.0105}, + {**base, "strike": 95000, "bid": 0.0400, "ask": 0.0405}, + {**base, "strike": 100000, "bid": 0.0500, "ask": 0.0505}]) + b_no = _finto([{**base, "strike": 90000, "bid": 0.0100, "ask": 0.0105}, + {**base, "strike": 95000, "bid": 0.0280, "ask": 0.0285}, + {**base, "strike": 100000, "bid": 0.0500, "ask": 0.0505}]) + f_ok = test_farfalla(b_ok, 0.0); f_no = test_farfalla(b_no, 0.0) + print(f" TEST B: corpo troppo caro -> {(f_ok['cost_exec'] < 0).sum()} violazioni eseguibili " + f"(attese >=1) Β· smile convesso -> {(f_no['cost_exec'] < 0).sum()} (attese 0)") + # (3) calendario: varianza totale che SCENDE col tenore, stesso strike + c_rows = [] + for e, iv in (("2026-08-08T08:00Z", 80.0), ("2026-08-15T08:00Z", 40.0)): + c_rows.append({**base, "exp": e, "strike": 100000, "bid": 0.01, "ask": 0.011, "iv": iv}) + c_ok = _finto(c_rows) + c_ok["ts"] = pd.Timestamp("2026-07-01", tz="UTC") + c_no = c_ok.copy(); c_no["iv"] = [40.0, 80.0] + r1 = test_calendario(c_ok, 0.0, ogni=1); r2 = test_calendario(c_no, 0.0, ogni=1) + print(f" TEST C: varianza totale DECRESCENTE -> {r1['i_viol']} violazioni (attese >=1) Β· " + f"crescente -> {r2['i_viol']} (attese 0)") + assert r_ok["n_viol"] >= 1 and r_no["n_viol"] == 0 + assert (f_ok["cost_exec"] < 0).sum() >= 1 and (f_no["cost_exec"] < 0).sum() == 0 + assert r1["i_viol"] >= 1 and r2["i_viol"] == 0 + print(" -> 3/3 rilevatori validati in ENTRAMBI i versi: gli zeri dei test veri sono zeri.\n") + + +# --------------------------------------------------------------------------- reversione + +def reversione(bf: pd.DataFrame, max_gap_h: float = 2.0) -> dict: + """Il residuo di convessita' si RICOMPONE? (l'ipotesi del filone, misurata) + + Coppie appaiate sulla STESSA tripla di strike a snapshot consecutivi entro `max_gap_h`. + beta di resid[t+1] - resid[t] su resid[t] : beta = -1 -> ricomposizione totale in un'ora, + beta = 0 -> il residuo e' una proprieta' persistente dello smile (skew), non un errore. + """ + x = bf.dropna(subset=["iv_resid"]).copy() + x["trip"] = (x["asset"] + "|" + x["exp"].astype(str) + "|" + x["option_type"] + "|" + + x["strike_p"].astype(str) + "|" + x["strike"].astype(str) + "|" + + x["strike_n"].astype(str)) + x = x.sort_values(["trip", "ts"]) + g = x.groupby("trip", sort=False) + r1 = g["iv_resid"].shift(-1).to_numpy() + dt = (g["ts"].shift(-1) - x["ts"]).dt.total_seconds().to_numpy() / 3600.0 + r0 = x["iv_resid"].to_numpy() + m = np.isfinite(r1) & np.isfinite(r0) & (dt > 0) & (dt <= max_gap_h) + if m.sum() < 100: + return {"n": int(m.sum())} + b = np.polyfit(r0[m], r1[m] - r0[m], 1)[0] + return {"n": int(m.sum()), "beta": float(b), "corr": float(np.corrcoef(r0[m], r1[m])[0, 1]), + "meta_vita_h": float(np.log(0.5) / np.log(max(1e-9, 1 + b))) if -1 < b < 0 else np.nan} + + +# --------------------------------------------------------------------------- main + +def main() -> None: + t0 = time.time() + controlli_positivi() + d = load() + q, F = certifica(d) + q = q.merge(F, on=["asset", "ts", "exp"], how="left") + q["F"] = q["underlying_price"].fillna(q["F_pcp"]) + print(f"\n forward disponibile su {q['F'].notna().mean():.1%} delle quote a due lati " + f"(osservato dove c'e', altrimenti parita' put-call)") + + for dte_min, etichetta in ((0.0, "SENZA filtro DTE"), (DTE_MIN, f"DTE >= {DTE_MIN:g}g")): + print("\n" + "=" * 100) + print(f"1-2. ARBITRAGGI STATICI MODEL-FREE su prezzi ESEGUIBILI β€” {etichetta}") + print("=" * 100) + v = test_verticale(q, dte_min) + print(f" TEST V (monotonia nello strike): {v['n_viol']:,} violazioni su {v['n_confronti']:,} " + f"confronti = {v['quota']:.4%}") + if v["n_viol"]: + print(f" credito mediano {v['credito_med_coin']:.5f} coin = " + f"${v['credito_usd_medlot']:.2f} al lotto minimo (LORDO fee)") + + bf = test_farfalla(q, dte_min) + bf = in_punti_vol(bf) + bf["viol_exec"] = bf["cost_exec"] < 0 + bf["viol_mid"] = bf["cost_mid"] < 0 + print(f" TEST B (convessita' nello strike): {len(bf):,} triple consecutive") + print(f" al MID (incoerenza, non eseguibile): {bf['viol_mid'].sum():,} = {bf['viol_mid'].mean():.3%}") + print(f" ESEGUIBILE (compra ali all'ask, vendi corpo al bid): " + f"{bf['viol_exec'].sum():,} = {bf['viol_exec'].mean():.4%}") + if dte_min == DTE_MIN: + bf_keep, v_keep = bf, v + + bf, vert = bf_keep, v_keep + print("\n" + "=" * 100) + print("3. QUANTO E' GRANDE L'INCOERENZA, E QUANTO COSTA ATTRAVERSARLA") + print("=" * 100) + for a in sorted(bf["asset"].unique()): + x = bf[bf["asset"] == a] + x = x[np.isfinite(x["halfspread_pt"]) & np.isfinite(x["tick_pt"])] + r = np.abs(x["iv_resid"].dropna()) + hs, tk = x["halfspread_pt"], x["tick_pt"] + print(f" {a} (n={len(x):,} triple con vega misurabile)") + print(f" |residuo di convessita' dello smile| mediana {r.median():.3f} pt-vol Β· " + f"p90 {r.quantile(.9):.3f} Β· p99 {r.quantile(.99):.3f}") + print(f" mezzo spread denaro-lettera mediana {hs.median():.3f} pt-vol Β· " + f"p25 {hs.quantile(.25):.3f} Β· p75 {hs.quantile(.75):.3f}") + sub = x.dropna(subset=["iv_resid"]) + print(f" UN TICK della griglia di prezzo mediana {tk.median():.3f} pt-vol " + f"-> l'incoerenza e' sotto il tick nel " + f"{np.mean(sub['iv_resid'].abs() < sub['tick_pt']):.1%} dei casi (n={len(sub):,})") + both = x.dropna(subset=["iv_resid"]) + rap = np.abs(both["iv_resid"]) / (2 * both["halfspread_pt"]) + print(f" |residuo| / (spread PIENO delle gambe): mediana {rap.median():.3f} Β· " + f"p99 {rap.quantile(.99):.3f} Β· quota > 1 = {np.mean(rap > 1):.3%}") + print(f" RELATIVE VALUE (non arbitraggio): per guadagnare serve |residuo| > 1x lo spread") + print(f" anche con reversione TOTALE e uscita gratis ({np.mean(rap > 1):.3%} dei casi), " + f"> 2x se si paga anche l'uscita ({np.mean(rap > 2):.3%})") + print(f" spread relativo (ask-bid)/mid del venue: mediana {x['relspread'].median():.1%} " + f"Β· p25 {x['relspread'].quantile(.25):.1%}") + # valore in dollari dell'incoerenza al MID (il caso migliore possibile: fee 0, spread 0) + eq = np.isclose(x["lam"], 0.5, atol=0.02) + lot = MIN_LOT[a] + val = -x.loc[eq, "cost_mid"].to_numpy() * 2 * lot * x.loc[eq, "spot_feed"].to_numpy() + pos = val[val > 0] + print(f" valore LORDO al mid della farfalla incoerente, lotto minimo: " + f"mediana ${np.median(pos):.2f} Β· p99 ${np.percentile(pos,99):.2f} Β· max ${pos.max():.2f}" + f" (n={len(pos):,})") + + print("\n economia della farfalla al LOTTO MINIMO (famiglia INVERSE, quella nel dato):") + for a in sorted(bf["asset"].unique()): + e = economia(bf, a) + print(f" {a}: nozionale/lotto ${e['nozionale_min_lotto_usd']:,.0f} Β· " + f"fee 3 gambe mediana ${e['fee_3gambe_usd_med']:.2f} Β· " + f"triple equispaziate {e['n_equisp']:,}") + print(f" credito netto: max ${e['netto_usd_max']:.2f} Β· p99 ${e['netto_usd_p99']:.2f} Β· " + f"positivi {e['n_netto_pos']:,}/{e['n_equisp']:,}") + + print("\n ESTRAPOLAZIONE DICHIARATA alla famiglia USDC (NON nel dato: il collettore raccoglie") + print(" le sole inverse β€” difetto di produzione del filone 11). Il lotto minimo e' 10x piu'") + print(" piccolo, ma il muro NON e' il ticket: e' lo spread in punti di vol, che e' una") + print(" QUOTA e non un livello -> ridurre il lotto non cambia |residuo|/spread.") + for a in sorted(bf["asset"].unique()): + x = bf[bf["asset"] == a] + x = x[np.isfinite(x["halfspread_pt"])].dropna(subset=["iv_resid"]) + rap = np.abs(x["iv_resid"]) / (2 * x["halfspread_pt"]) + rel_inv = x["relspread"].median() + for nome, rel_usdc in ((f"{a}_USDC", 0.082 if a == "BTC" else 0.026),): + k = rel_inv / rel_usdc # di quanto lo spread sarebbe piu' stretto + print(f" {a}: spread relativo inverse {rel_inv:.1%} vs {nome} {rel_usdc:.1%} " + f"-> fattore {k:.1f}x; rapporto mediano {rap.median():.3f} -> {rap.median()*k:.3f}, " + f"quota > 1 {np.mean(rap > 1):.2%} -> {np.mean(rap * k > 1):.2%}") + + print("\n" + "=" * 100) + print("4. TEST C β€” varianza totale nel tenore (l'unico che tocca un interpolatore)") + print("=" * 100) + cal = test_calendario(q, DTE_MIN) + print(f" (i) strike CONDIVISO, nessuna interpolazione: {cal['i_viol']:,}/{cal['i_n']:,} = {cal['i_quota']:.3%}" + f" Β· ampiezza mediana {cal['i_amp_med_pt']:.3f} pt-vol (p95 {cal['i_amp_p95_pt']:.3f})") + print(f" (ii) log-moneyness FISSO, con interpolazione (1 snapshot ogni {cal['ii_ogni']}): " + f"{cal['ii_viol']:,}/{cal['ii_n']:,} = {cal['ii_quota']:.3%} Β· ampiezza mediana {cal['ii_amp_med_pt']:.3f} pt-vol") + print(f" -> contributo dell'INTERPOLATORE = {cal['ii_quota'] - cal['i_quota']:+.3%} di quota di violazione") + if "i_val_usd_med" in cal: + print(f" valore mediano della violazione (i) al lotto minimo ${cal['i_val_usd_med']:.2f} " + f"contro un costo di 2 gambe (spread + fee) di ${cal['i_costo_usd_med']:.2f}") + print(f" margine di monotonia sull'intera superficie: la iv lunga sta {-cal['room_med_pt']:.2f} pt-vol " + f"SOPRA il minimo che la non-arbitraggio richiede (5% piu' stretto: {-cal['room_p5_pt']:.2f}) " + f"-> la superficie non e' nemmeno VICINA al bordo") + + print("\n" + "=" * 100) + print("5. TICK E PERSISTENZA β€” le due difese contro 'la mia incoerenza e' un artefatto'") + print("=" * 100) + for col, nome in (("viol_mid", "al MID"), ("viol_exec", "ESEGUIBILE")): + v = bf[bf[col]] + if len(v) == 0: + print(f" {nome}: nessuna violazione") + continue + legs = np.concatenate([v["bid"].to_numpy(), v["bid_p"].to_numpy(), v["bid_n"].to_numpy()]) + legs = legs[np.isfinite(legs) & (legs > 0)] + nt = legs / tick_of(legs) + p = persistenza(bf, col) + print(f" {nome}: n={len(v):,} Β· gambe a <=2 tick {np.mean(nt <= 2):.1%} " + f"(mediana {np.median(nt):.0f} tick) Β· resta in violazione allo snapshot dopo " + f"{p.get('resta', float('nan')):.1%} (n={p['n']:,})") + + print("\n CONTROFATTUALE (dichiarato, non misurato): la stessa superficie con lo spread USDC") + for a, rel in (("BTC", 0.082), ("ETH", 0.026)): + if a not in set(bf["asset"]): + continue + for strette, et in ((False, "tutta la superficie"), (True, "solo quote gia' strette")): + cf = controfattuale_usdc(bf, a, rel, solo_gia_strette=strette) + for nome, r in cf.items(): + if strette and "inverse" in nome: + continue + print(f" {a} {nome:<26} [{et:<24}]: lotto ${r['lotto_usd']:,.0f} Β· " + f"credito netto>0 {r['pos']:,}/{r['n']:,} Β· >$1 {r['pos1']:,} Β· " + f"migliore ${r['max']:.2f} Β· somma ${r['somma']:,.0f}") + # dove vive l'incoerenza: e' larga dove il mercato e' largo? + x = bf[(bf["asset"] == a)].dropna(subset=["iv_resid", "relspread"]) + x = x[np.isfinite(x["relspread"])] + dec = pd.qcut(x["relspread"], 5, labels=False, duplicates="drop") + t = x.assign(d=dec).groupby("d").agg(spread=("relspread", "median"), + resid=("iv_resid", lambda v: v.abs().median())) + print(f" |residuo| per quintile di spread relativo {a}: " + + " Β· ".join(f"{r.spread:.1%}->{r.resid:.3f}pt" for r in t.itertuples())) + + print("\n QUANTO MANCA per essere eseguibile (near-miss, triple equispaziate):") + for a in sorted(bf["asset"].unique()): + x = bf[(bf["asset"] == a) & np.isclose(bf["lam"], 0.5, atol=0.02)] + c = x["cost_exec"].to_numpy() * 2 * MIN_LOT[a] * x["spot_feed"].to_numpy() # >0 = costo + print(f" {a}: costo della farfalla eseguibile β€” migliore ${np.nanmin(c):.2f} Β· " + f"p0.1 ${np.nanpercentile(c, 0.1):.2f} Β· mediana ${np.nanmedian(c):.2f} " + f"(negativo = credito riskless)") + + print("\n" + "=" * 100) + print("6. IL RESIDUO SI RICOMPONE? (l'ipotesi del filone, misurata invece che assunta)") + print("=" * 100) + for a in sorted(bf["asset"].unique()): + rv = reversione(bf[bf["asset"] == a]) + if "beta" not in rv: + print(f" {a}: coppie insufficienti (n={rv['n']})") + continue + print(f" {a}: n={rv['n']:,} coppie appaiate (stessa tripla, snapshot consecutivo <=2h) Β· " + f"beta di ricomposizione {rv['beta']:+.3f} Β· corr(resid_t, resid_t+1) {rv['corr']:+.3f}") + if np.isfinite(rv["meta_vita_h"]): + print(f" -> meta-vita {rv['meta_vita_h']:.1f} h (beta=-1 sarebbe ricomposizione totale " + f"in un'ora, beta=0 residuo persistente = forma dello smile, non errore)") + + print("\n" + "=" * 100) + print("7. ROBUSTEZZA D'ANCORA β€” il risultato dipende dall'ora in cui guardo?") + print("=" * 100) + h = bf.assign(ora=bf["ts"].dt.hour).groupby("ora").agg( + mid=("viol_mid", "mean"), ex=("viol_exec", "mean"), n=("viol_mid", "size")) + print(f" quota di incoerenza al MID per ora UTC: mediana {h['mid'].median():.3%} Β· " + f"min {h['mid'].min():.3%} (h{h['mid'].idxmin():02d}) Β· max {h['mid'].max():.3%} (h{h['mid'].idxmax():02d})") + print(f" quota ESEGUIBILE per ora UTC: max {h['ex'].max():.5%} su {len(h)} ore -> " + f"nessuna ora del giorno apre una finestra") + + print("\n" + "=" * 100) + print("GRIGLIA DICHIARATA (contata al RIALZO)") + print("=" * 100) + print(" 3 test di arbitraggio (verticale Β· farfalla Β· calendario)") + print(" x 2 asset x 2 tipi (call/put) x 2 filtri DTE (0g e 1g) x 2 convenzioni di prezzo") + print(" (mid = incoerenza Β· bid/ask = eseguibile) = 48 celle") + print(" + calendario in 2 lenti (strike condiviso / log-moneyness interpolato) = 4") + print(" + controfattuale USDC x 2 asset x 2 sottocampioni = 4") + print(" + reversione x 2 asset Β· ancora oraria 24 ore x 2 metriche = 50") + print(" + 6 controlli positivi (3 rilevatori x 2 versi) = 6") + print(" TOTALE dichiarato ~112 valutazioni. NESSUNA e' una selezione: non c'e' una cella") + print(" scelta, c'e' un censimento. Il deflated-Sharpe non e' applicabile (nessuno stream).") + + print("\n" + "=" * 100) + print("SINTESI") + print("=" * 100) + nv = int(bf["viol_exec"].sum()); nt = len(bf) + print(f" 1. La superficie e' ARBITRAGE-FREE sui prezzi eseguibili: monotonia " + f"{vert['n_viol']}/{vert['n_confronti']:,},") + print(f" convessita' {nv}/{nt:,} = {nv/nt:.6%} (e quell'una vale $0,00 e sparisce l'ora dopo),") + print(f" calendario {cal['i_viol']}/{cal['i_n']:,} con margine mediano di {-cal['room_med_pt']:.0f} " + f"punti di vol dal bordo.") + print(f" 2. L'incoerenza al MID esiste ed e' grande ({bf['viol_mid'].mean():.1%} delle triple)") + hs_b = bf[bf.asset == "BTC"]["halfspread_pt"]; hs_e = bf[bf.asset == "ETH"]["halfspread_pt"] + print(f" ma vale {np.abs(bf['iv_resid']).median():.3f} punti di vol contro un mezzo spread di " + f"{hs_b[np.isfinite(hs_b)].median():.2f}-{hs_e[np.isfinite(hs_e)].median():.2f},") + print(f" ed e' SOTTO IL TICK del venue nel 95-97% dei casi: non e' esprimibile nella griglia.") + print(f" 3. MECCANISMO: |residuo| cresce in modo monotono col LARGO del mercato") + print(f" (BTC 0,080 pt a spread 2,6% -> 0,255 pt a spread 50%). L'incoerenza al mid E' la") + print(f" larghezza del mercato guardata attraverso il mid, non un errore di prezzo.") + print(f" 4. Il controfattuale USDC (lotto 10x piu' piccolo, spread 2,6%) sembra aprire 2.293") + print(f" farfalle a credito su ETH β€” ma restringendolo alla parte di superficie GIA' quotata") + print(f" stretta quanto le USDC diventa 0/13.287. Era un artefatto del controfattuale.") + print(f" -> l'incoerenza ESISTE e vale meno del costo di attraversarla: verdetto SCARTATO.") + print(f"\n[tempo {time.time()-t0:.1f}s]") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_tp01_sinistro.py b/scripts/research/r0822b_tp01_sinistro.py new file mode 100644 index 0000000..ff0002d --- /dev/null +++ b/scripts/research/r0822b_tp01_sinistro.py @@ -0,0 +1,954 @@ +"""r0822b_tp01_sinistro β€” quanto costa l'ASSICURAZIONE TP01 su un conto a barriera (2026-08-22). + +IL BUCO CHE CHIUDE +------------------ +`r0822_prop_alloc.py` (stessa ondata) ha ottimizzato l'allocazione di un conto **funded** sotto +barriera di drawdown e ha trovato l'ottimo a **TP01 25 / SKH01 25 / XS01 50**, cioe' TP01 pesato +**tre volte meno** del libro live (75%). Ha anche dichiarato il proprio limite: + + "la sotto-pesatura di TP01, lo sleeve DIFENSIVO, e' misurata su un campione SENZA SINISTRO + (2024-2026 non contiene un crash): su una barriera assorbente quella scommessa si paga una + volta sola." + +E questo si scontra con due misure del progetto: + (a) il **leave-one-out de-luckato** del 26/07: TP01 e' il maggior contributore al Sharpe FULL + (+0.390, positivo nel 100% delle estrazioni) e il suo hold-out negativo (-0.200, negativo + nel 99.1%) e' *la firma dell'assicurazione, non della morte* β€” "uno sleeve difensivo si + giudica sul SINISTRO, non sul premio"; + (b) `edge_watch` criterio B: negli **8 anni su 8** con drawdown buy&hold > 10% TP01 ha protetto + da **1.8x a 34.4x**. + +DOMANDA: qual e' il peso giusto di TP01 se l'obiettivo include sopravvivere a un crash che nel +campione di ottimizzazione NON c'e'? + +IPOTESI REGISTRATA PRIMA DI MISURARE +------------------------------------ +Mi aspettavo che (1) la sotto-pesatura di TP01 costasse molto **nel sinistro** e poco fuori, e che +(2) esistesse un peso di compromesso (~50%) dentro la regione robusta del funded e non +catastrofico al crash. In altre parole mi aspettavo di **confermare** il caveat di PROP-ALLOC. +Se la misura dice il contrario, e' un risultato e va scritto come tale. + +CIO' CHE E' UN'ASSUNZIONE E CIO' CHE E' UN DATO +----------------------------------------------- +* Lo **scenario di stress e' uno SCENARIO, non una previsione.** Due costruzioni, entrambe + dichiarate: (S1) bootstrap MISTO β€” ogni blocco ha probabilita' `p` di essere estratto dal pool + dei sinistri 2019-2023 invece che dalla finestra normale; (S2) **replay** β€” un episodio di + crash VERO innestato per intero, in ordine, a una data casuale dentro l'anno. In piu' il + replay deterministico del **2022 intero** (aprile-dicembre), che non e' un modello: e' successo. +* Il vincolo binding di TUTTO il filone e' che i sinistri osservati sono **QUATTRO**. Nessun + bootstrap crea informazione: l'ampiezza effettiva della coda resta 4 episodi, e ogni numero qui + eredita quell'errore. Detto una volta, vale per ogni tabella. +* Lente **ACCOPPIATA** (`r0725_prop_coupled`): il minimo intraday si compone ESATTO sul path + orario condiviso TP01/SKH01. Non e' un dettaglio: su una regola a UN giorno (daily-loss) la + lente close-only e' misurata **esattamente cieca** (0.00 breach contro 0.40-1.21 veri), mentre + su una statistica multi-giorno l'errore vale il 3.5%. Il vincolo funded E' una regola a un giorno. +* de-luck sul drift **x0.89** (fattore MISURATO il 26/07). ⚠️ L'offset di de-luck e' calcolato UNA + VOLTA sulla finestra 2019+ e sottratto ovunque in valore ASSOLUTO: de-luckare un pool di crash + sulla sua PROPRIA media (negativa) *aggiungerebbe* drift proprio dove non deve. +* macchineria RIUSATA, non riscritta: `r0725_prop_coupled` (tuple accoppiate) e `r0822_prop_alloc` + (`eval_sim`, `funded_sim`, `boot_idx`, `FIRMS`). Qui si aggiungono solo i pool e gli innesti. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822b_tp01_sinistro.py` +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +from src.data.downloader import load_data # noqa: E402 +from src.portfolio.sleeves import _tp01_returns, _skyhook_returns # noqa: E402 + +import r0725_prop_coupled as pc # noqa: E402 +import r0822_prop_alloc as PA # noqa: E402 + +# ------------------------------------------------------------------ costanti DICHIARATE +DELUCK = PA.DELUCK # 0.89, misurato il 26/07 +FULL_START = "2019-03-14" # inizio del recon crypto (TP01+SKH01) +RECENT_START = "2024-01-01" # finestra di ottimizzazione di PROP-ALLOC (nasce XS01) +BLOCK = 20 # blocco del bootstrap, in giorni +N_PATHS = 2500 +EPI_LEN = 30 # lunghezza di un episodio di sinistro, in giorni +HORIZON = 365 +LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50) +W_GRID = tuple(i / 8 for i in range(9)) # peso di TP01 nella coppia crypto, 0 -> 1 +SURV_TARGET = 0.90 # livello di sopravvivenza della frontiera ISO-SOPRAVVIVENZA + +N_EVAL = 0 # contatore onesto delle valutazioni (griglia dichiarata) + + +def hr(t: str = "") -> None: + print("\n" + "-" * 106) + if t: + print(f" {t}") + print("-" * 106) + + +# ================================================================== Β§A. PANNELLO + +_TUP: dict = {} + + +def crypto(w_tp: float) -> tuple[np.ndarray, np.ndarray, pd.DatetimeIndex]: + """(R, gap) giornalieri della coppia crypto a peso TP01 = w_tp, SKH01 = 1-w_tp. + gap = minimo_intraday - chiusura (<=0). Storia piena dal 2019-03.""" + k = round(float(w_tp), 6) + if k not in _TUP: + c = pc.crypto_daily_tuples(k, 1.0 - k) + R = c["R"].values.astype(float) + G = np.minimum(c["m"].values, R) - R + _TUP[k] = (R, G, c.index) + R, G, I = _TUP[k] + return R.copy(), G.copy(), I + + +_OFF: dict = {} + + +def deluck_offset(w_tp: float, factor: float = DELUCK) -> float: + """Offset ASSOLUTO di de-luck, calcolato UNA VOLTA sulla finestra 2019+ e riusato ovunque. + Calcolarlo sulla media di un sotto-campione di crash (negativa) *aggiungerebbe* drift.""" + key = (round(float(w_tp), 6), round(float(factor), 4)) + if key not in _OFF: + R, _, I = crypto(w_tp) + _OFF[key] = float((1.0 - factor) * R[I >= pd.Timestamp(FULL_START, tz="UTC")].mean()) + return _OFF[key] + + +def buyhold_daily(idx: pd.DatetimeIndex) -> pd.Series: + """Buy&hold 50/50 BTC+ETH sulla stessa griglia giornaliera del libro.""" + bh = None + for a in ("BTC", "ETH"): + d = load_data(a, "1h") + i = pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True)) + s = pd.Series(d["close"].astype(float).values, index=i).resample("1D").last() + r = s.pct_change().fillna(0.0) * 0.5 + bh = r if bh is None else bh.add(r, fill_value=0.0) + return bh.reindex(idx).fillna(0.0) + + +def worst_windows(lbh: np.ndarray, idx: pd.DatetimeIndex, K: int, L: int, + hi: int | None = None) -> list[int]: + """Le K finestre di L giorni NON sovrapposte col peggior rendimento buy&hold, greedy sul + minimo. `hi` limita i possibili inizi (per restare dentro 2019-2023).""" + cs = np.concatenate(([0.0], np.cumsum(lbh))) + win = np.expm1(cs[L:] - cs[:-L]) + cand = np.arange(len(win)) if hi is None else np.arange(min(hi, len(win))) + order = cand[np.argsort(win[cand])] + out: list[int] = [] + for t in order: + if all(abs(int(t) - q) >= L for q in out): + out.append(int(t)) + if len(out) == K: + break + return sorted(out) + + +def binom_two_sided(k: int, n: int) -> float: + """p-value esatto a due code di un test dei segni con p0=0.5 (n piccolo: si conta).""" + from math import comb + tot = 2 ** n + kk = max(k, n - k) + tail = sum(comb(n, j) for j in range(kk, n + 1)) + return min(1.0, 2.0 * tail / tot) + + +def maxdd(r: np.ndarray) -> float: + eq = np.cumprod(1.0 + r) + return float((eq / np.maximum.accumulate(eq) - 1.0).min()) + + +# ================================================================== Β§B. SIMULAZIONE + +def J_of(R: np.ndarray, G: np.ndarray, ev_idx: np.ndarray, fu_idx: np.ndarray, + lev: float, firm: str = "HYRO") -> dict: + """J = P(passare l'eval) x P(essere vivo a 12 mesi da funded), lente ACCOPPIATA.""" + global N_EVAL + N_EVAL += 1 + e = PA.eval_sim(R, G, ev_idx, lev, firm, "coupled") + f = PA.funded_sim(R, G, fu_idx, lev, firm, 100_000.0, "coupled") + return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"], + e_payout=f["e_payout"], jvec=(e["passed"] & f["alive"]).astype(float), + alive=f["alive"]) + + +def best_lev(R: np.ndarray, G: np.ndarray, ev_idx, fu_idx, firm: str = "HYRO") -> tuple: + rows = [(lv, J_of(R, G, ev_idx, fu_idx, lv, firm)) for lv in LEVS] + return max(rows, key=lambda x: x[1]["J"]) + + +def pool_paths(seg_bounds: list[tuple[int, int]], n_days: int, n_paths: int, seed: int, + block: int = BLOCK) -> np.ndarray: + """Indici di bootstrap a blocchi ristretti a segmenti CONTIGUI (ogni blocco vive dentro un + solo segmento -> non si incollano giorni di episodi diversi).""" + rng = np.random.default_rng(seed) + starts = [] + for a, b in seg_bounds: + if b - a >= block: + starts.extend(range(a, b - block + 1)) + starts = np.array(starts) + nb = int(np.ceil(n_days / block)) + st = starts[rng.integers(0, len(starts), size=(n_paths, nb))] + return (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] + + +def mix_paths(base_idx: np.ndarray, crash_starts: np.ndarray, p: float, seed: int, + block: int = BLOCK) -> np.ndarray: + """S1 β€” bootstrap MISTO: ogni blocco della path normale ha probabilita' `p` di essere + sostituito da un blocco estratto dal pool dei SINISTRI.""" + rng = np.random.default_rng(seed) + out = base_idx.copy() + P, D = out.shape + nb = int(np.ceil(D / block)) + hit = rng.random((P, nb)) < p + for b in range(nb): + sl = slice(b * block, min((b + 1) * block, D)) + L = sl.stop - sl.start + rows = np.where(hit[:, b])[0] + if len(rows) == 0: + continue + st = crash_starts[rng.integers(0, len(crash_starts), size=len(rows))] + out[rows, sl] = st[:, None] + np.arange(L)[None, :] + return out + + +def splice_paths(base_idx: np.ndarray, epi_idx: np.ndarray, seed: int) -> np.ndarray: + """S2 β€” REPLAY: l'episodio VERO (contiguo, in ordine) innestato una volta sola a una data + casuale dentro l'orizzonte. E' la domanda operativa: *se il sinistro arriva*.""" + rng = np.random.default_rng(seed) + out = base_idx.copy() + P, D = out.shape + L = len(epi_idx) + st = rng.integers(0, max(D - L, 1), size=P) + for i in range(P): + a = int(st[i]) + out[i, a:a + L] = epi_idx[:min(L, D - a)] + return out + + +# ================================================================== main + +def main() -> None: # noqa: C901 + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + t0 = time.time() + + def tick(lab: str) -> None: + print(f" [{time.time()-t0:6.0f}s] {lab} (valutazioni finora: {N_EVAL})") + + print("=" * 106) + print(" TP01-SINISTRO β€” quanto costa l'assicurazione su un conto a barriera, e quanto rende") + print(" quando il sinistro arriva. Lente ACCOPPIATA, firm HYRO.") + print("=" * 106) + + R75, G75, IDX = crypto(0.75) + n_full = len(IDX) + i_rec = int(np.searchsorted(IDX, pd.Timestamp(RECENT_START, tz="UTC"))) + print(f"\n finestra crypto piena: {n_full} giorni {IDX[0].date()} -> {IDX[-1].date()}") + print(f" finestra di ottimizzazione di PROP-ALLOC: {n_full - i_rec} giorni dal " + f"{IDX[i_rec].date()} (e' anche la finestra di SCOPERTA di XS01)") + + # --------------------------------------------------------- sanity: recon vs sleeve ufficiale + print("\n SANITY β€” il recon orario contro gli sleeve ufficiali. TP01/SKH01 NON coincidono") + print(" barra per barra per costruzione (il recon e' un mark-to-market ORARIO, lo sleeve") + print(" marca a fine-trade): il controllo giusto e' il log-totale.") + for nm, off_s, w in (("TP01", _tp01_returns(), 1.0), ("SKH01", _skyhook_returns(), 0.0)): + o = off_s.dropna().sort_index() + if o.index.tz is None: + o.index = o.index.tz_localize("UTC") + Rn, _, In = crypto(w) + com = o.index.intersection(In) + a = o.reindex(com).values + b = pd.Series(Rn, index=In).reindex(com).values + print(f" {nm:>6} log-tot sleeve {float(np.log1p(a).sum()):+8.4f} " + f"recon {float(np.log1p(b).sum()):+8.4f} scarto " + f"{float(np.log1p(a).sum()-np.log1p(b).sum()):+7.4f}") + + # ============================================================ Β§1. CHE COS'E' UN SINISTRO + hr("Β§1. CHE COS'E' UN SINISTRO β€” e la frase '2024-2026 non contiene un crash' e' FALSA") + bh = buyhold_daily(IDX) + lbh = np.log1p(bh.values) + print(" Peggior rendimento buy&hold 50/50 su finestra mobile, per periodo:") + print(f" {'W':>4} {'2019-2023 min':>15} {'2024+ min':>12} {'rapporto':>10} " + f"{'2019-23 p1':>11} {'2024+ p1':>10} {'data del min 2024+':>20}") + ratios = [] + for W in (1, 5, 10, 20, 60): + cs = np.concatenate(([0.0], np.cumsum(lbh))) + roll = np.expm1(cs[W:] - cs[:-W]) # finestra che FINISCE in t=W-1+k + tt = IDX[W - 1:] + pre = roll[tt < pd.Timestamp(RECENT_START, tz="UTC")] + post = roll[tt >= pd.Timestamp(RECENT_START, tz="UTC")] + tpost = tt[tt >= pd.Timestamp(RECENT_START, tz="UTC")] + rr = pre.min() / post.min() + ratios.append(rr) + print(f" {W:>4} {pre.min():>15.1%} {post.min():>12.1%} {rr:>10.2f}x " + f"{np.percentile(pre, 1):>11.1%} {np.percentile(post, 1):>10.1%} " + f"{str(tpost[int(np.argmin(post))].date()):>20}") + print(f"\n -> il 2024+ **contiene** un crash: peggior giorno -14.5%, peggiori 20g -39.6%,") + print(f" drawdown buy&hold fino a -60%. Cio' che NON contiene e' un crash della TAGLIA") + print(f" dei precedenti: il rapporto e' {np.mean(ratios[:3]):.1f}x sulla coda veloce (1-10g)") + print(f" e {ratios[-1]:.1f}x a 60 giorni. **La frase giusta non e' 'senza sinistro' ma") + print(f" 'con un sinistro di taglia dimezzata'** β€” e cambia la domanda: non 'cosa succede") + print(f" se arriva un crash' ma 'cosa succede se ne arriva uno DUE VOLTE piu' grande'.") + + # ----- replica del criterio B di edge_watch (protezione per anno) + print("\n Criterio B di `edge_watch` replicato qui (anni con DD buy&hold > 10%):") + tp_only, _, _ = crypto(1.0) + ser_bh = pd.Series(bh.values, index=IDX) + ser_tp = pd.Series(tp_only, index=IDX) + print(f" {'anno':>6} {'DD b&h':>9} {'DD TP01':>9} {'protezione':>12} {'DD SKH01':>10} " + f"{'DD 75/25':>10}") + skh_only, _, _ = crypto(0.0) + ser_sk = pd.Series(skh_only, index=IDX) + ser_75 = pd.Series(R75, index=IDX) + n_sin = n_ok = 0 + for y, g in ser_bh.groupby(ser_bh.index.year): + d_bh = maxdd(g.values) + if d_bh > -0.10: + continue + n_sin += 1 + d_tp = maxdd(ser_tp.loc[g.index].values) + d_sk = maxdd(ser_sk.loc[g.index].values) + d_75 = maxdd(ser_75.loc[g.index].values) + ok = (d_tp / d_bh) <= 0.75 + n_ok += ok + print(f" {y:>6} {d_bh:>9.1%} {d_tp:>9.1%} {d_bh/d_tp:>11.1f}x {d_sk:>10.1%} " + f"{d_75:>10.1%} {'ok' if ok else 'FAIL'}") + print(f" -> anni di sinistro {n_sin}, superati {n_ok}. **Ogni anno e' un anno di sinistro") + print(f" per il buy&hold**: la 'frequenza del sinistro' non e' un evento raro, e' lo") + print(f" stato normale dell'asset. Cio' che e' raro e' la TAGLIA (riga sopra).") + + # ----- definizione degli episodi + hr("Β§1-bis. GLI EPISODI DI SINISTRO β€” regola di selezione DICHIARATA PRIMA") + print(f" Regola: le {3} finestre di {EPI_LEN} giorni NON sovrapposte con il peggior rendimento") + print(f" buy&hold nel 2019-2023 (greedy sul minimo), PIU' l'FTX 2022-11-05 aggiunto **per") + print(f" NOME** (evento noto, non selezionato dal minimo: e' un controllo, non una scelta).") + cs = np.concatenate(([0.0], np.cumsum(lbh))) + win = np.expm1(cs[EPI_LEN:] - cs[:-EPI_LEN]) # finestra che INIZIA in t + pre_mask = IDX[:len(win)] < pd.Timestamp(RECENT_START, tz="UTC") + cand = np.where(pre_mask)[0] + order = cand[np.argsort(win[cand])] + picked = [] + for t in order: + if all(abs(int(t) - p) >= EPI_LEN for p in picked): + picked.append(int(t)) + if len(picked) == 3: + break + t_ftx = int(np.searchsorted(IDX, pd.Timestamp("2022-11-05", tz="UTC"))) + epis = sorted(picked + [t_ftx]) + NAMES = {} + print(f"\n {'episodio':>12} {'dal':>12} {'al':>12} {'b&h 30g':>10} {'peggior 1g b&h':>16}") + for t in epis: + lab = ("COVID" if IDX[t].year == 2020 else + "MAG-2021" if (IDX[t].year, IDX[t].month) == (2021, 5) else + "LUNA" if (IDX[t].year, IDX[t].month) in ((2022, 5), (2022, 6)) else + "FTX" if (IDX[t].year, IDX[t].month) == (2022, 11) else f"EPI-{IDX[t].date()}") + NAMES[t] = lab + seg = bh.values[t:t + EPI_LEN] + print(f" {lab:>12} {str(IDX[t].date()):>12} {str(IDX[t+EPI_LEN-1].date()):>12} " + f"{win[t]:>10.1%} {seg.min():>16.1%}") + crash_days = np.concatenate([np.arange(t, t + EPI_LEN) for t in epis]) + freq_day = len(crash_days) / n_full + print(f"\n giorni di sinistro {len(crash_days)} su {n_full} = **{freq_day:.1%} del tempo**") + print(f" ⚠️ AMPIEZZA EFFETTIVA DELLA CODA = **4 EPISODI**. Nessun bootstrap la aumenta.") + print(f" Ogni numero di questo file eredita l'errore di un campione di quattro eventi.") + + tick("Β§1 sinistri") + + # ============================================================ Β§2. IL CONTO DELL'ASSICURAZIONE + hr("Β§2. IL CONTO DELL'ASSICURAZIONE β€” premio fuori dal sinistro, indennizzo dentro") + print(" Confronto DUE ripartizioni della coppia crypto, appaiato giorno per giorno:") + print(" LIVE = TP01 75 / SKH01 25 (il libro che gira)") + print(" OPT = TP01 38 / SKH01 62 (l'ottimo di PROP-ALLOC sul campione 2019+)") + in_sin = np.zeros(n_full, bool) + in_sin[crash_days] = True + print(f"\n {'config':>10} {'drift fuori':>12} {'drift dentro':>13} {'Sharpe full':>12} " + f"{'maxDD':>8} {'peggior 1g':>11} {'peggior min 1g':>15}") + prof = {} + for lab, w in (("LIVE 75/25", 0.75), ("OPT 38/62", 0.375), ("TP01 puro", 1.0), + ("SKH01 puro", 0.0)): + R, G, _ = crypto(w) + R = R - deluck_offset(w) + out_d = R[~in_sin].mean() * 365.0 + in_d = R[in_sin].mean() * 365.0 + vol = R.std() * np.sqrt(365.0) + prof[lab] = dict(out=out_d, ind=in_d, sh=R.mean() * 365.0 / vol, dd=maxdd(R), + w1=R.min(), wm=(R + G).min()) + p = prof[lab] + print(f" {lab:>10} {p['out']:>12.1%} {p['ind']:>13.1%} {p['sh']:>12.2f} {p['dd']:>8.1%} " + f"{p['w1']:>11.2%} {p['wm']:>15.2%}") + prem = prof["LIVE 75/25"]["out"] - prof["OPT 38/62"]["out"] + ind = prof["LIVE 75/25"]["ind"] - prof["OPT 38/62"]["ind"] + R_live = crypto(0.75)[0] - deluck_offset(0.75) + R_opt = crypto(0.375)[0] - deluck_offset(0.375) + cum_live = float(np.expm1(np.log1p(R_live[in_sin]).sum())) + cum_opt = float(np.expm1(np.log1p(R_opt[in_sin]).sum())) + print(f"\n PREMIO (LIVE - OPT) fuori dal sinistro : {prem:+.2%}/anno = " + f"{prem*100_000:+,.0f} $/anno su un funded da $100k") + print(f" INDENNIZZO (LIVE - OPT) sui 120 giorni di sinistro, CUMULATO (non annualizzato,") + print(f" che su 120 giorni sarebbe una finta precisione): LIVE {cum_live:+.1%} contro " + f"OPT {cum_opt:+.1%} = {cum_live-cum_opt:+.1%}") + print(f" su un funded da $100k: {(cum_live-cum_opt)*100_000:+,.0f} $ per l'INTERO " + f"monte-sinistri di {n_full/365.0:.1f} anni") + print(f" frequenza: {freq_day:.1%} del tempo dentro un sinistro (4 episodi in " + f"{n_full/365.0:.1f} anni = uno ogni {n_full/365.0/4:.1f} anni)") + if ind < 0: + print("\n 🚨 **IL SEGNO E' ROVESCIATO RISPETTO ALLA MIA IPOTESI.** Dentro il sinistro il") + print(" libro con MENO TP01 fa MEGLIO, non peggio. Il meccanismo si legge in una riga:") + print(" TP01 nel crash va FLAT (non perde, non guadagna: e' assicurazione), SKH01 va") + print(" SHORT (guadagna). Su un conto a barriera *guadagnare batte non-perdere*.") + print("\n Rendimento dei 4 episodi, per peso di TP01 (30 giorni ciascuno, chiusura e MINIMO):") + print(f" {'w TP01':>8} " + " ".join(f"{NAMES[t]:>18}" for t in epis) + f" {'peggior min':>13}") + for w in (1.0, 0.75, 0.5, 0.375, 0.25, 0.0): + R, G, _ = crypto(w) + cells = [] + wm = 0.0 + for t in epis: + seg = R[t:t + EPI_LEN] + gp = G[t:t + EPI_LEN] + eq = np.cumprod(1.0 + seg) + mn = float(np.minimum(eq - 1.0, np.concatenate(([1.0], eq[:-1])) * (1 + seg + gp) - 1.0).min()) + wm = min(wm, mn) + cells.append(f"{float(eq[-1]-1.0):+7.1%}/{mn:+7.1%}") + print(f" {w:>8.3f} " + " ".join(f"{c:>18}" for c in cells) + f" {wm:>13.1%}") + print(" (formato: chiusura a 30g / minimo intraday cumulato dell'episodio)") + + print("\n ⚠️ QUANTA POTENZA HA QUESTO SEGNO? Con 4 episodi, '4 su 4 a favore' ha un test dei") + print(" segni con p = 0.125 a due code: **non e' significativo al 5%**. Il modo onesto di") + print(" guadagnare potenza non e' ripetere il bootstrap (non crea eventi) ma ESTENDERE la") + print(" stessa regola di selezione a piu' finestre. Le 12 peggiori finestre di 30g NON") + print(" sovrapposte su TUTTO il 2019+ (stessa regola greedy, K da 3 a 12):") + w12 = worst_windows(lbh, IDX, 12, EPI_LEN) + print(f"\n {'finestra':>12} {'b&h 30g':>9} {'LIVE 75/25':>11} {'OPT 38/62':>11} " + f"{'TP01 puro':>10} {'SKH01 puro':>11} {'OPT>LIVE':>9}") + wins = 0 + wins_sk = 0 + lr_live, lr_opt = np.log1p(R_live), np.log1p(R_opt) + R_tp = crypto(1.0)[0] - deluck_offset(1.0) + R_sk = crypto(0.0)[0] - deluck_offset(0.0) + for t in w12: + a = float(np.expm1(lr_live[t:t + EPI_LEN].sum())) + b = float(np.expm1(lr_opt[t:t + EPI_LEN].sum())) + c = float(np.expm1(np.log1p(R_tp[t:t + EPI_LEN]).sum())) + d = float(np.expm1(np.log1p(R_sk[t:t + EPI_LEN]).sum())) + wins += b > a + wins_sk += d > c + print(f" {str(IDX[t].date()):>12} {win[t]:>9.1%} {a:>11.1%} {b:>11.1%} " + f"{c:>10.1%} {d:>11.1%} {('si' if b > a else 'no'):>9}") + pre = [t for t in w12 if IDX[t] < pd.Timestamp(RECENT_START, tz="UTC")] + wins_pre = sum(1 for t in pre + if float(np.expm1(lr_opt[t:t + EPI_LEN].sum())) + > float(np.expm1(lr_live[t:t + EPI_LEN].sum()))) + print(f"\n OPT batte LIVE in **{wins}/12** finestre -> test dei segni " + f"p = {binom_two_sided(wins, 12):.4f}") + print(f" SKH01 puro batte TP01 puro in **{wins_sk}/12** -> p = " + f"{binom_two_sided(wins_sk, 12):.4f}") + print(f" ⚠️ {12-len(pre)} di queste 12 finestre stanno nel 2024+, cioe' DENTRO la finestra") + print(" su cui SKH01 e' stato tarato e su cui PROP-ALLOC ha ottimizzato: la potenza sale") + print(" ma l'indipendenza scende. **Sul solo sotto-campione pre-2024**, che e' quello") + print(f" indipendente: OPT batte LIVE in **{wins_pre}/{len(pre)}** -> p = " + f"{binom_two_sided(wins_pre, len(pre)):.4f}. E' questo il numero da citare.") + + tick("Β§2 conto assicurazione") + + # ============================================================ Β§3. SOTTO BARRIERA + hr("Β§3. SOTTO BARRIERA β€” griglia peso TP01 x leva su TRE lenti di campione") + print(f" {len(W_GRID)} pesi x {len(LEVS)} leve x 3 finestre. J = P(pass) x P(vivo 12m), HYRO") + print(f" (eval +10%/-6% statico/-4% giornaliero; funded -6% statico/-4% giornaliero).") + ev_f = PA.boot_idx(n_full, HORIZON, N_PATHS, seed=8230) + fu_f = PA.boot_idx(n_full, HORIZON, N_PATHS, seed=8231) + n_rec = n_full - i_rec + ev_r = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8240) + fu_r = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8241) + seg_sin = [(t, t + EPI_LEN) for t in epis] + ev_s = pool_paths(seg_sin, HORIZON, N_PATHS, seed=8250) + fu_s = pool_paths(seg_sin, HORIZON, N_PATHS, seed=8251) + + print(f"\n {'w TP01':>7} | {'2019+ (col 2022)':>26} | {'2024+ (PROP-ALLOC)':>26} | " + f"{'SOLO-SINISTRO':>26}") + print(f" {'':>7} | {'leva':>6} {'J':>7} {'P(viv)':>7} | {'leva':>6} {'J':>7} {'P(viv)':>7} | " + f"{'leva':>6} {'J':>7} {'P(viv)':>7}") + tab = {} + for w in W_GRID: + R, G, _ = crypto(w) + off = deluck_offset(w) + Rf, Gf = R - off, G + Rr, Gr = Rf[i_rec:], Gf[i_rec:] + lv1, o1 = best_lev(Rf, Gf, ev_f, fu_f) + lv2, o2 = best_lev(Rr, Gr, ev_r, fu_r) + lv3, o3 = best_lev(Rf, Gf, ev_s, fu_s) + tab[w] = dict(full=(lv1, o1), rec=(lv2, o2), sin=(lv3, o3)) + print(f" {w:>7.3f} | {lv1:>6.3f} {o1['J']:>7.3f} {o1['p_alive']:>7.1%} | " + f"{lv2:>6.3f} {o2['J']:>7.3f} {o2['p_alive']:>7.1%} | " + f"{lv3:>6.3f} {o3['J']:>7.3f} {o3['p_alive']:>7.1%}") + a_full = max(tab, key=lambda w: tab[w]["full"][1]["J"]) + a_rec = max(tab, key=lambda w: tab[w]["rec"][1]["J"]) + a_sin = max(tab, key=lambda w: tab[w]["sin"][1]["J"]) + print(f"\n argmax: 2019+ -> TP01 {a_full:.3f} | 2024+ -> TP01 {a_rec:.3f} | " + f"SOLO-SINISTRO -> TP01 {a_sin:.3f}") + print(f" ⚠️ RISOLUZIONE: un LIVELLO di J ha errore MC ~{np.sqrt(0.25/N_PATHS):.3f} " + f"(N={N_PATHS}) -> celle entro ~0.02 NON sono distinguibili, e infatti 0.375 e 0.500") + print(" si scambiano il posto fra semi diversi. La lettura e' 'la regione 0.25-0.50',") + print(" non 'il punto 0.375' (stessa lezione del null dei tilt casuali di PROP-ALLOC).") + print(" ⚠️ La colonna SOLO-SINISTRO e' una lente ESTREMA e va letta come tale: l'anno intero") + print(" e' fatto di soli blocchi di crash (4 episodi ricampionati). Non e' uno scenario") + print(" plausibile, e' il **limite superiore del danno**: se il verso della risposta e' lo") + print(" stesso qui e sul 2019+, non dipende dalla frazione di sinistro assunta.") + + # replica di controllo del numero pubblicato da PROP-ALLOC (E-ter) + print("\n REPLICA DI CONTROLLO β€” PROP-ALLOC E-ter pubblica, sul 2019+: argmax della coppia") + print(" crypto **38/62 @0.50x con J 0.521**, e **75/25 J 0.443**. Stesse seed, stessa lente:") + j38 = tab[0.375]["full"] + j75 = tab[0.75]["full"] + print(f" qui: 38/62 @{j38[0]:g}x J {j38[1]['J']:.3f} | 75/25 @{j75[0]:g}x " + f"J {j75[1]['J']:.3f} -> " + f"{'REPLICA OK' if abs(j38[1]['J']-0.521) < 0.02 and abs(j75[1]['J']-0.443) < 0.02 else 'NON REPLICA β€” leggere con cautela'}") + + tick("Β§3 griglia 3 lenti") + + # ============================================================ Β§4. LO STRESS ESPLICITO + hr("Β§4. LO STRESS ESPLICITO β€” iniettare nel 2024+ i crash che il 2024+ non ha") + print(" Costruzione DICHIARATA. Base = blocchi del 2024+ (la finestra di ottimizzazione).") + print(" S1 MISTO: ogni blocco ha probabilita' p di venire dal pool dei 4 sinistri 2019-2023.") + print(f" p = {freq_day:.3f} e' la frequenza STORICA; si girano anche 2x e 4x.") + print(" S2 REPLAY: un episodio VERO innestato per intero, in ordine, a data casuale dentro") + print(" l'anno. E' la domanda operativa: *se il sinistro arriva, chi resta in piedi?*") + print(" ⚠️ Sono SCENARI, non previsioni. E poggiano su 4 episodi.") + + # pool = 2024+ (blocchi normali) + gli episodi in coda, su un indice unico + def stress_panel(w: float) -> tuple[np.ndarray, np.ndarray, np.ndarray, list]: + R, G, _ = crypto(w) + off = deluck_offset(w) + Rn, Gn = R[i_rec:] - off, G[i_rec:] + segs = [] + Rc, Gc = [], [] + cur = len(Rn) + for t in epis: + Rc.append(R[t:t + EPI_LEN] - off) + Gc.append(G[t:t + EPI_LEN]) + segs.append((cur, cur + EPI_LEN)) + cur += EPI_LEN + Rp = np.concatenate([Rn] + Rc) + Gp = np.concatenate([Gn] + Gc) + return Rp, Gp, np.array([s for s, _ in segs]), segs + + base_ev = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8260) + base_fu = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8261) + + print(f"\n S1 β€” bootstrap MISTO (base 2024+, blocchi di sinistro con probabilita' p)") + print(f" {'w TP01':>7} | " + " | ".join(f"p={p:.3f}: {'J':>6} {'P(viv)':>7}" + for p in (0.0, freq_day, 2 * freq_day, 4 * freq_day))) + s1 = {} + for w in W_GRID: + Rp, Gp, cst, segs = stress_panel(w) + starts = np.concatenate([np.arange(a, b - BLOCK + 1) for a, b in segs]) + cells = [] + for j, p in enumerate((0.0, freq_day, 2 * freq_day, 4 * freq_day)): + ev = base_ev if p == 0 else mix_paths(base_ev, starts, p, seed=8270 + j) + fu = base_fu if p == 0 else mix_paths(base_fu, starts, p, seed=8280 + j) + lv, o = best_lev(Rp, Gp, ev, fu) + cells.append((lv, o)) + s1[w] = cells + print(f" {w:>7.3f} | " + " | ".join(f" @{c[0]:.3g}x {c[1]['J']:>6.3f} " + f"{c[1]['p_alive']:>7.1%}" for c in cells)) + for j, p in enumerate((0.0, freq_day, 2 * freq_day, 4 * freq_day)): + am = max(s1, key=lambda w: s1[w][j][1]["J"]) + print(f" p={p:.3f} -> argmax TP01 {am:.3f} (J {s1[am][j][1]['J']:.3f}; " + f"75/25 fa {s1[0.75][j][1]['J']:.3f})") + + tick("Β§4 S1 misto") + + print(f"\n S2 β€” REPLAY: un episodio innestato una volta nell'anno funded (365g)") + print(f" Metrica: **P(vivo a 12 mesi) con il crash dentro**, alla leva scelta sul 2024+") + print(f" (cioe' la leva che uno sceglierebbe NON sapendo del crash: e' il punto).") + hdr = " ".join(f"{NAMES[t]:>10}" for t in epis) + print(" ⚠️ DUE colonne di leva, e servono entrambe: la leva 'da 2024+' e' quella che uno") + print(" sceglierebbe non sapendo del crash (realistica, ma CONFONDE peso e leva perche' non") + print(" e' la stessa per tutte le righe); la leva COMUNE 0.50x isola l'effetto del peso.") + print(f"\n {'w TP01':>7} {'leva (da 2024+)':>16} {hdr:>44} {'peggiore':>10} " + f"{'peggiore @0.50x':>16} {'P(viv) 2024+':>13}") + s2 = {} + for w in W_GRID: + Rp, Gp, _, segs = stress_panel(w) + lv = tab[w]["rec"][0] + cells, cells_c = [], [] + for k, (a, b) in enumerate(segs): + fu = splice_paths(base_fu, np.arange(a, b), seed=8290 + k) + cells.append(J_of(Rp, Gp, base_ev, fu, lv)["p_alive"]) + cells_c.append(J_of(Rp, Gp, base_ev, fu, 0.50)["p_alive"]) + s2[w] = (lv, cells, min(cells_c), tab[w]["rec"][1]["p_alive"]) + print(f" {w:>7.3f} {lv:>16.3f} " + " ".join(f"{c:>10.1%}" for c in cells) + + f" {min(cells):>10.1%} {min(cells_c):>16.1%} " + f"{tab[w]['rec'][1]['p_alive']:>13.1%}") + bw_a = max(s2, key=lambda w: min(s2[w][1])) + bw_c = max(s2, key=lambda w: s2[w][2]) + print(f"\n argmax della colonna 'peggiore': leva-da-2024+ -> TP01 {bw_a:.3f} | " + f"leva comune 0.50x -> TP01 {bw_c:.3f}") + print(" ⚠️ LE DUE COLONNE DICONO COSE DIVERSE, E LA SECONDA E' A CAMPANA: a leva COMUNE la") + print(f" sopravvivenza al crash SALE col peso di TP01 fino a {bw_c:.3f} (" + f"{s2[0.0][2]:.1%} a TP01=0 -> {s2[bw_c][2]:.1%}) e poi ricade.") + print(" **Quindi TP01 protegge davvero: a pari leva, il libro con piu' difensivo sopravvive") + print(" di piu' al crash.** Il punto e' che quella protezione si compra anche abbassando la") + print(" leva β€” ed e' esattamente cio' che misura Β§5. Alla leva che ciascun libro sceglierebbe") + print(" (prima colonna) il vantaggio sparisce, perche' il libro senza TP01 la sceglie piu'") + print(" bassa. E' la firma del **null del de-levering**, qui in veste di 'protezione dal") + print(" crash' invece che di 'meno drawdown'.") + + + tick("Β§4 S2 replay") + + # ----- replay deterministico del 2022 + print("\n S2-bis β€” REPLAY DETERMINISTICO DEL 2022 (2022-04-01 -> 2022-12-31, 275 giorni).") + print(" Non e' un modello: e' successo. Un conto funded HYRO da $100k che parte il 1Β° aprile") + print(" 2022 con max-loss STATICO -6% e daily-loss -4%, alla leva scelta sul 2024+:") + a22 = int(np.searchsorted(IDX, pd.Timestamp("2022-04-01", tz="UTC"))) + b22 = int(np.searchsorted(IDX, pd.Timestamp("2023-01-01", tz="UTC"))) + def replay22(w: float, lev: float) -> tuple: + R, G, _ = crypto(w) + off = deluck_offset(w) + r = (R[a22:b22] - off) * lev + g = G[a22:b22] * lev + eq, mn, bust, why = 1.0, 1.0, None, "" + for i in range(len(r)): + low = eq * (1.0 + r[i] + g[i]) + eq = eq * (1.0 + r[i]) + mn = min(mn, low) + if bust is None and (low < 0.94 or (r[i] + g[i]) < -0.04): + bust = IDX[a22 + i].date() + why = "daily-loss -4%" if (r[i] + g[i]) < -0.04 else "max-loss -6%" + return eq - 1.0, mn - 1.0, bust, why, float((r + g).min()) + + print(f"\n {'w TP01':>7} {'leva':>6} {'rend. 9 mesi':>13} {'peggior giorno':>15} " + f"{'min equity':>11} {'esito':>22}") + for w in W_GRID: + lv = tab[w]["rec"][0] + ret, mn, bust, why, wd = replay22(w, lv) + esito = "VIVO" if bust is None else f"BUSTATO {bust}" + print(f" {w:>7.3f} {lv:>6.3f} {ret:>13.1%} {wd:>15.2%} {mn:>11.2%} {esito:>22}") + print("\n Tutti VIVI, e non e' una buona notizia mal letta: **alle leve funded il 2022 non") + print(" era una minaccia per questo libro** (min equity -1.4/-2.4% contro una barriera al") + print(" -6%). Cio' che uccide un conto a barriera non e' il crash del sottostante: e' la") + print(" regola a UN giorno su una giornata qualunque. Ecco fin dove si puo' spingere la leva:") + print(f"\n {'w TP01':>7} {'leva max viva sul 2022':>23} {'rend. a quella leva':>20} " + f"{'primo vincolo che morde':>25}") + for w in W_GRID: + lo, hi = 0.1, 8.0 + for _ in range(18): + mid = 0.5 * (lo + hi) + if replay22(w, mid)[2] is None: + lo = mid + else: + hi = mid + ret, _, _, _, _ = replay22(w, lo) + _, _, _, why, _ = replay22(w, hi) + print(f" {w:>7.3f} {lo:>23.2f}x {ret:>20.1%} {why:>25}") + print("\n πŸ“Œ La colonna 'leva max viva' CRESCE col peso di TP01 (fino a TP01=0.875; a TP01=1") + print(" ricade perche' li' a mordere non e' piu' il giorno singolo ma la perdita CUMULATA).") + print(" **L'assicurazione e' reale e si vede: piu' TP01 = piu' leva sopportabile nel 2022.**") + print(" E accanto c'e' il prezzo: alla PROPRIA leva massima il libro senza TP01 porta a casa") + print(" il +23.3% e quello a TP01 pieno il -5.9%. TP01 compra capacita' di leva, e la") + print(" capacita' di leva non si converte in rendimento perche' cio' che TP01 toglie e'") + print(" proprio il rendimento. E' il null del de-levering scritto in una riga sola.") + print(" ⚠️ Cambia anche il VINCOLO che morde: sotto TP01 0.50 e' la regola a UN GIORNO,") + print(" sopra e' il max-loss cumulato. Due libri diversi muoiono di due morti diverse, e una") + print(" sola delle due lenti (l'accoppiata) vede la prima.") + + tick("Β§4 replay 2022") + + # ============================================================ Β§5. NULL DEL DE-LEVERING + hr("Β§5. IL NULL DEL DE-LEVERING β€” la protezione di TP01 e' altro rispetto a meno leva?") + print(" Il null classico ('esiste k<1 che da' lo stesso rischio con risultato migliore?') qui") + print(" prende la forma giusta per una barriera: **frontiera ISO-SOPRAVVIVENZA**. Per ogni") + print(f" peso si cerca la leva k(w) tale che P(vivo 12m | replay del sinistro PEGGIORE) = " + f"{SURV_TARGET:.0%},") + print(" poi si confronta il PAYOUT atteso sulla finestra normale a quella leva. Se un libro") + print(" con meno TP01 e meno leva paga di piu' a pari sopravvivenza, la protezione di TP01") + print(" e' de-levering; se paga di meno, TP01 compra qualcosa che la leva non compra.") + + _SP: dict = {} + + def p_alive_stress(w: float, lev: float, seg: tuple) -> float: + """P(vivo 12m) col replay del sinistro dentro. Solo `funded_sim`: l'eval non entra + nella definizione della frontiera, e calcolarlo raddoppierebbe il costo per niente.""" + global N_EVAL + N_EVAL += 1 + k = (round(w, 6), seg) + if k not in _SP: + Rp, Gp, _, _ = stress_panel(w) + _SP[k] = (Rp, Gp, splice_paths(base_fu, np.arange(seg[0], seg[1]), seed=8291)) + Rp, Gp, fu = _SP[k] + return PA.funded_sim(Rp, Gp, fu, lev, "HYRO", 100_000.0, "coupled")["p_alive"] + + print(f"\n {'w TP01':>7} {'k iso-surv':>11} {'P(viv|crash)':>13} {'E[pay/a] normale':>17} " + f"{'P(pass) normale':>16} {'J normale':>11}") + front = {} + for w in W_GRID: + Rp, Gp, _, segs = stress_panel(w) + # episodio peggiore per QUESTO peso (il piu' severo, misurato) + worst_seg = min(segs, key=lambda sg: np.prod(1.0 + Rp[sg[0]:sg[1]])) + lo, hi = 0.05, 3.0 + for _ in range(14): + mid = 0.5 * (lo + hi) + if p_alive_stress(w, mid, worst_seg) >= SURV_TARGET: + lo = mid + else: + hi = mid + k = lo + R, G, _ = crypto(w) + off = deluck_offset(w) + o = J_of(R[i_rec:] - off, G[i_rec:], ev_r, fu_r, k) + front[w] = (k, o) + print(f" {w:>7.3f} {k:>11.3f} {p_alive_stress(w, k, worst_seg):>13.1%} " + f"{o['e_payout']:>16,.0f}$ {o['p_pass']:>16.1%} {o['J']:>11.3f}") + kbest = max(front, key=lambda w: front[w][1]["e_payout"]) + jbest = max(front, key=lambda w: front[w][1]["J"]) + print(f"\n argmax a ISO-SOPRAVVIVENZA: payout -> TP01 {kbest:.3f} | J -> TP01 {jbest:.3f}") + print(" ⚠️ La leva k(w) e' tarata SU un episodio che nel campione di ottimizzazione non c'e':") + print(" e' un esercizio con l'informazione del futuro. Serve a rispondere alla domanda del") + print(" filone ('se sapessi che arriva, come mi allocherei?'), NON e' una politica.") + + tick("Β§5 frontiera iso-sopravvivenza") + + # ============================================================ Β§6. IL TERZO SLEEVE + hr("Β§6. XS01 β€” la parte della domanda che NON e' rispondibile, e perche'") + print(" L'ottimo di PROP-ALLOC mette il 50% su XS01, che **nasce nel 2024**: per lui non") + print(" esiste NESSUN sinistro nel campione. Qui non lo si simula col bootstrap della sua") + print(" finestra (sarebbe assumere che nel crash si comporti come in un giorno normale, che") + print(" e' esattamente l'assunzione sotto esame). Si dichiarano TRE comportamenti e si") + print(" guarda se la conclusione dipende da quale:") + print(" A) FLAT nel crash (il gate di dispersione lo spegne) -> generosa") + print(" B) la sua PEGGIORE finestra di 30g del 2024+, contigua e in ordine -> severa") + print(" C) uno shock una-tantum di taglia X sul giorno peggiore del crash -> si cerca") + print(" la X di ROTTURA, cioe' quanto grande deve essere per portare P(vivo) sotto 50%") + xs = pc.xsec_daily_tuples() + xsR = xs["R"].values + xsG = np.minimum(xs["m"].values, xsR) - xsR + lxs = np.log1p(xsR) + cxs = np.concatenate(([0.0], np.cumsum(lxs))) + wxs30 = np.expm1(cxs[EPI_LEN:] - cxs[:-EPI_LEN]) + t_xw = int(np.argmin(wxs30)) + print(f"\n XS01: {len(xs)} giorni {xs.index[0].date()} -> {xs.index[-1].date()}; peggior " + f"giorno {xsR.min():.2%}, peggior minimo {xs['m'].min():.2%}") + print(f" peggior finestra di {EPI_LEN}g di XS01: {xs.index[t_xw].date()} -> " + f"{xs.index[t_xw+EPI_LEN-1].date()}, cumulato {wxs30[t_xw]:.1%} " + f"(ipotesi B). Per confronto la coppia crypto nel COVID fa -5.4% a TP01 pieno.") + print(" πŸ“Œ Il numero che conta e' questo: **la peggior finestra di 30g mai vista da XS01 vale") + print(f" {wxs30[t_xw]:.1%}**. Se nel crash XS01 facesse cosi', al 50% di peso porterebbe") + print(f" {0.5*wxs30[t_xw]:.1%} al libro. La domanda non e' se XS01 sopravvive a cio' che ha") + print(" gia' visto, e' se il suo comportamento nel crash somiglia a cio' che ha gia' visto β€”") + print(" e su questo il campione non ha NIENTE da dire.") + # ⚠ le ultime tre righe hanno XS01 FISSO al 50%: senza quel controllo si confonderebbe + # "piu' TP01" con "meno XS01", che e' un'altra domanda. + W3 = [(0.75, 0.25, 0.00), (0.375, 0.625, 0.00), (0.50, 0.25, 0.25), + (0.125, 0.375, 0.50), (0.25, 0.25, 0.50), (0.375, 0.125, 0.50)] + com = xs.index.intersection(IDX[i_rec:]) + xs_al = xs.reindex(com) + LEV_COM = 0.625 # leva comune per il blocco a XS01=50% (la minima fra le tre righe) + print(f"\n {'TP/SKH/XS':>16} {'ipotesi XS':>12} {'leva':>6} {'J normale':>10} " + f"{'P(viv|replay pegg.)':>21} {'idem @0.625x':>14} {'shock di rottura':>18}") + for cfg in W3: + wtp, wsk, wxs = cfg + base = wtp + wsk + wtp_n = wtp / base if base > 0 else 0.0 + Rc, Gc, Ic = crypto(wtp_n) + offc = deluck_offset(wtp_n) + m1 = Ic.isin(com) + Rn = base * (Rc[m1] - offc) + wxs * xs_al["R"].values + Gn = base * Gc[m1] + wxs * (np.minimum(xs_al["m"].values, xs_al["R"].values) + - xs_al["R"].values) + nn = len(Rn) + evn = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8300) + fun = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8301) + lv, on = best_lev(Rn, Gn, evn, fun) + bfu = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8302) + lab = f"{int(round(wtp*100))}/{int(round(wsk*100))}/{int(round(wxs*100))}" + + def build(hyp: str, shock: float = 0.0, lev_: float | None = None): + segsR, segsG = [], [] + for t in epis: + rc = base * (crypto(wtp_n)[0][t:t + EPI_LEN] - offc) + gc = base * crypto(wtp_n)[1][t:t + EPI_LEN] + if wxs > 0: + if hyp == "A-flat": + rx = np.zeros(EPI_LEN); gx = np.zeros(EPI_LEN) + elif hyp == "B-peggiore": + rx = xsR[t_xw:t_xw + EPI_LEN].copy() + gx = xsG[t_xw:t_xw + EPI_LEN].copy() + else: + rx = np.zeros(EPI_LEN); gx = np.zeros(EPI_LEN) + rx[int(np.argmin(rc))] = -shock + segsR.append(rc + wxs * rx); segsG.append(gc + wxs * gx) + else: + segsR.append(rc); segsG.append(gc) + Rp = np.concatenate([Rn] + segsR) + Gp = np.concatenate([Gn] + segsG) + sg = [(nn + k * EPI_LEN, nn + (k + 1) * EPI_LEN) for k in range(len(epis))] + worst = 1.0 + for k, g2 in enumerate(sg): + fu = splice_paths(bfu, np.arange(g2[0], g2[1]), seed=8320 + k) + worst = min(worst, J_of(Rp, Gp, evn, fu, lv if lev_ is None else lev_)["p_alive"]) + return worst + + if wxs == 0: + print(f" {lab:>16} {'n/a (no XS)':>12} {lv:>6.3f} {on['J']:>10.3f} " + f"{build('A-flat'):>21.1%} {build('A-flat', lev_=LEV_COM):>14.1%} {'-':>18}") + continue + for hyp in ("A-flat", "B-peggiore"): + print(f" {lab:>16} {hyp:>12} {lv:>6.3f} {on['J']:>10.3f} " + f"{build(hyp):>21.1%} {build(hyp, lev_=LEV_COM):>14.1%} {'-':>18}") + lo, hi = 0.0, 0.40 # shock di rottura: bisezione su P(vivo) = 50% + for _ in range(9): + mid = 0.5 * (lo + hi) + if build("C-shock", mid) >= 0.50: + lo = mid + else: + hi = mid + lo2, hi2 = 0.0, 0.40 + for _ in range(9): + mid = 0.5 * (lo2 + hi2) + if build("C-shock", mid, lev_=LEV_COM) >= 0.50: + lo2 = mid + else: + hi2 = mid + print(f" {lab:>16} {'C-shock':>12} {lv:>6.3f} {on['J']:>10.3f} {'(50% per def.)':>21} " + f"{'(50% per def.)':>14} {-lo:>17.1%} / {-lo2:.1%} @0.625x") + print("\n Lettura: 'shock di rottura' = il crollo di UN giorno su XS01 che porta P(vivo) al") + print(" 50% quando il crash arriva. Piu' e' piccolo, piu' l'allocazione XS01-pesante e' a un") + print(" giorno di coda dalla morte. Non e' una previsione: e' la taglia della scommessa.") + print(" ⚠️ Le ultime tre righe hanno XS01 FISSO al 50% e differiscono SOLO per come si") + print(" divide il resto fra TP01 e SKH01: e' li' che si legge se TP01 fa da zavorra contro") + print(" l'ignoto di XS01. **La colonna @0.625x e' quella da leggere**, perche' a leva libera") + print(" la riga con meno TP01 sceglie una leva piu' bassa e vince per quello.") + tick("Β§6 XS01") + + # ============================================================ Β§7. BANDA + VERDETTO + hr("Β§7. SENSIBILITA' AL DE-LUCK β€” l'ORDINE fra i pesi sopravvive alla banda?") + print(" La banda d'ancora non e' ri-misurabile sotto questa lente (costo, dichiarato anche da") + print(" PROP-ALLOC). Si applica il fattore misurato x0.89 e si controlla l'unica cosa che") + print(" conta: se l'argmax sui pesi si muove fra 1.00 (nessuna correzione) e 0.60 (la stima") + print(" a occhio del 25/07, poi ri-misurata troppo severa).") + print(f"\n {'fattore':>9} {'argmax 2019+':>14} {'argmax 2024+':>14} {'argmax SOLO-SIN':>16} " + f"{'J(75/25) 2019+':>16} {'J(38/62) 2019+':>16}") + for fct in (1.00, 0.89, 0.60): + best_f = best_r = best_s = None + vals = {} + for w in W_GRID: + R, G, _ = crypto(w) + o = deluck_offset(w, fct) + Rf = R - o + j1 = best_lev(Rf, G, ev_f, fu_f)[1]["J"] + j2 = best_lev(Rf[i_rec:], G[i_rec:], ev_r, fu_r)[1]["J"] + j3 = best_lev(Rf, G, ev_s, fu_s)[1]["J"] + vals[w] = (j1, j2, j3) + best_f = max(vals, key=lambda w: vals[w][0]) + best_r = max(vals, key=lambda w: vals[w][1]) + best_s = max(vals, key=lambda w: vals[w][2]) + print(f" {fct:>9.2f} {best_f:>14.3f} {best_r:>14.3f} {best_s:>16.3f} " + f"{vals[0.75][0]:>16.3f} {vals[0.375][0]:>16.3f}") + + hr("Β§8. I GATE β€” cosa e' girato, cosa NON e' girato e perche'") + print(" Questo filone non propone una strategia nuova: propone (o rifiuta) un'ALLOCAZIONE per") + print(" un conto che non esiste. I gate del progetto vanno dichiarati uno per uno.") + try: + sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + import altlib as A # noqa: E402 + for lab, w in (("LIVE 75/25", 0.75), ("OPT 38/62", 0.375)): + R = crypto(w)[0] - deluck_offset(w) + d = pd.Series(R, index=IDX) + n_act = int((np.abs(R) > 1e-12).sum()) + n_los = int((R[np.abs(R) > 1e-12] < 0).sum()) + f = A.implausible_sharpe(d, n_trades=n_act, n_losing_trades=n_los) + print(f" implausible_sharpe {lab:>11}: implausible={f['implausible']} " + f"(Sharpe {f['sharpe']:.2f}, maxDD {f['maxdd']:.1%}, quota barre in perdita " + f"{f['loss_frac']:.1%}, Calmar {f['calmar']:.2f}) {f['reasons']}") + except Exception as exc: # pragma: no cover + print(f" implausible_sharpe: NON GIRATO ({exc})") + print(" marginal_vs_tp01 / study_family_honest / deflated_sharpe : **NON GIRATI, e non") + print(" sono applicabili** β€” non c'e' uno stream candidato da confrontare con TP01: gli") + print(" stream sono i due sleeve gia' ammessi, e l'oggetto e' il loro PESO. Il gate dei") + print(" pesi del progetto e' `weights_tilt_null`, che pero' giudica lo SHARPE del libro") + print(" di RICERCA: qui l'obiettivo e' J su una barriera, e il conto e' separato.") + print(" (PROP-ALLOC lo ha girato: gate_pass False. Non cambia nulla qui: nessuna") + print(" proposta di questo file tocca il libro live.)") + print(" causality_ok : **non girato, non applicabile** β€” nessun segnale nuovo; le serie") + print(" sono quelle di produzione, gia' verificate leak-free a monte.") + print(" anchor_luck_band : **non ri-misurata** (costo, come in PROP-ALLOC). Si e' girata") + print(" la sensibilita' al FATTORE di de-luck (Β§7), che e' meno: dice se l'ORDINE regge,") + print(" non qual e' la banda. Distorsione dichiarata: la lente e' l'ancora CANONICA e") + print(" SKH01 e' lo sleeve con la fortuna d'ancora piu' grande da restituire (26/07)") + print(" -> **la distorsione e' A FAVORE della conclusione di questo file**, cioe' del") + print(" peso basso su TP01. E' il caveat piu' importante del filone.") + print(" null del de-levering : **GIRATO** (Β§5, frontiera iso-sopravvivenza) β€” e superato") + print(" NEL VERSO OPPOSTO a quello atteso: e' la protezione di TP01 a essere replicabile") + print(" con meno leva, e meglio.") + + hr("Β§9. LA RISPOSTA OPERATIVA") + print(f" valutazioni MC totali di questo file: **{N_EVAL}** " + f"(ognuna = {N_PATHS} percorsi di eval + {N_PATHS} di funded)") + print(f"\n {'lente':>26} {'argmax w TP01':>14} {'J argmax':>10} {'J a 0.75':>10} " + f"{'J a 0.375':>11}") + for lab, key in (("2019+ (contiene il 2022)", "full"), ("2024+ (PROP-ALLOC)", "rec"), + ("SOLO-SINISTRO", "sin")): + am = max(tab, key=lambda w: tab[w][key][1]["J"]) + print(f" {lab:>26} {am:>14.3f} {tab[am][key][1]['J']:>10.3f} " + f"{tab[0.75][key][1]['J']:>10.3f} {tab[0.375][key][1]['J']:>11.3f}") + print("\n ESISTE UN PESO DI COMPROMESSO? Il criterio dichiarato: (a) dentro il 5% del") + print(" massimo di J sulla finestra 2024+ (la 'regione robusta' del funded) E (b) dentro il") + print(" 5% del massimo di J sulla finestra 2019+, che il sinistro ce l'ha dentro.") + jr_max = max(tab[w]["rec"][1]["J"] for w in W_GRID) + jf_max = max(tab[w]["full"][1]["J"] for w in W_GRID) + js_max = max(tab[w]["sin"][1]["J"] for w in W_GRID) + print(f"\n {'w TP01':>7} {'J 2024+':>9} {'entro 5%':>9} {'J 2019+':>9} {'entro 5%':>9} " + f"{'J solo-sin':>11} {'entro 5%':>9} {'compromesso':>13}") + ok_both = [] + for w in W_GRID: + a, b, c = (tab[w]["rec"][1]["J"], tab[w]["full"][1]["J"], tab[w]["sin"][1]["J"]) + oa, ob, oc = a >= 0.95 * jr_max, b >= 0.95 * jf_max, c >= 0.95 * js_max + if oa and ob: + ok_both.append(w) + print(f" {w:>7.3f} {a:>9.3f} {('si' if oa else 'no'):>9} {b:>9.3f} " + f"{('si' if ob else 'no'):>9} {c:>11.3f} {('si' if oc else 'no'):>9} " + f"{('SI' if (oa and ob) else '-'):>13}") + print(f"\n -> pesi che soddisfano (a) E (b): {ok_both if ok_both else 'NESSUNO'}") + print(f" il libro live 75/25 e' {'dentro' if 0.75 in ok_both else 'FUORI'} da questa regione.") + print("\n LA RISPOSTA, IN QUATTRO RIGHE") + print(" 1) La premessa del caveat e' sbagliata nella FORMA: il 2024+ un crash ce l'ha (b&h") + print(" -60% di drawdown, -39.6% in 20 giorni). Manca la TAGLIA, ~2x sulla coda veloce.") + print(" 2) Il compromesso ESISTE e non e' un compromesso: lo stesso peso (TP01 ~0.375) e'") + print(" l'argmax sulla finestra con il 2022 dentro E su quella senza. Il caveat di") + print(" PROP-ALLOC e' onesto come cautela e **falsificato come previsione**.") + print(" 3) TP01 protegge davvero (a pari leva sopravvive di piu' al crash, e alza la leva") + print(" sopportabile nel 2022 da 0.93x a 2.20x) ma la stessa protezione si compra piu' a") + print(" buon mercato ABBASSANDO LA LEVA: a iso-sopravvivenza il payout e' massimo a") + print(" TP01 0.125-0.250 e cala monotonamente da li'. **7Βͺ occorrenza del null del") + print(" de-levering**, in una veste nuova: 'protezione dal crash' invece di 'meno DD'.") + print(" 4) Il rischio vero dell'ottimo di PROP-ALLOC NON e' TP01 sotto-pesato: e' XS01 al") + print(" 50% con un comportamento nel crash che il campione non contiene. Un solo giorno") + print(" a ~-10% su XS01 dimezza la sopravvivenza quando il crash arriva.") + print(" ⚠️ IPOTESI MIA, NATA E REFUTATA IN SESSIONE: avevo scritto che TP01 fa da") + print(" ZAVORRA contro quell'ignoto (a XS01 25% serviva uno shock -12.3% contro -8.0% a") + print(" XS01 50%). Il controllo a **XS01 FISSO al 50% e leva comune** lo smonta: lo shock") + print(" di rottura e' -10.1 / -10.2 / -10.2% muovendo TP01 da 12% a 38%, cioe' INVARIANTE,") + print(" e la sopravvivenza sotto l'ipotesi severa CALA (74.1 -> 72.6 -> 64.7%). Cio' che") + print(" protegge dall'ignoto di XS01 e' **meno XS01**, non piu' TP01: il primo numero") + print(" confrontava due pesi di XS01 diversi e attribuiva a TP01 il merito dell'altro.") + print("\n ⚠️ COSA MI SMENTIREBBE: un crash in cui SKH01 NON si gira short in tempo (gap") + print(" notturno oltre lo stop, o venue fermo). Il campione ne ha quattro e in tutti e") + print(" quattro SKH01 ha girato. Basterebbe un episodio in cui SKH01 perde quanto il") + print(" sottostante per rovesciare Β§2 e Β§5 β€” e con n=4 quell'episodio non e' improbabile.") + print("\n" + "=" * 106) + print(f" fatto in {time.time()-t0:.0f}s | valutazioni: {N_EVAL}") + print("=" * 106) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_xs01_oos.py b/scripts/research/r0822b_xs01_oos.py new file mode 100644 index 0000000..be1b40c --- /dev/null +++ b/scripts/research/r0822b_xs01_oos.py @@ -0,0 +1,683 @@ +"""r0822b_xs01_oos.py β€” XS01 FUORI dalla sua finestra di scoperta (2026-08-22, ondata wave-0822). + +IL PROBLEMA (filone PROP-ALLOC, RESULTS-0822 Β§1) +------------------------------------------------ +Il risultato piu' vicino all'obiettivo dell'operatore e': su un conto FUNDED da $100k, aggiungere +XS01 al libro porta J = P(pass) x P(vivo 12m) da 0,335 a 0,738. Ma **tutto** quel vantaggio poggia +sul drift di XS01 misurato sulla SUA STESSA finestra di scoperta (Hyperliquid, 2024-01 -> oggi, +~2,6 anni, e senza un solo mercato orso). Azzerando quel drift l'uplift scende a +0,038; sotto +stress congiunto diventa negativo. Il gate proposto era "non aprire un funded finche' XS01 non ha +una finestra fuori dal 2024-2026" = ANNI di attesa. + +LA DOMANDA DI QUESTO FILONE +--------------------------- +Si puo' testare il MECCANISMO CONGELATO su una storia PIU' LUNGA e INDIPENDENTE invece di aspettare? +La risposta e' si', e il vincolo non e' il dato ma il CALENDARIO DELLE QUOTAZIONI: 6 dei 19 asset +(APT, OP, ARB, SUI, SEI, TIA) non esistono prima del 2022-06/2023-10. Il piu' vecchio universo +>= 10 asset dei 19 e' del **2020-10** -> circa **3 anni interi fuori dalla finestra di scoperta, +2021-01 -> 2023-12**, che contengono il top 2021, il crollo di maggio 2021, **LUNA e FTX (2022)** e +la ripresa 2023. E' esattamente il regime che manca al campione di scoperta. + +IL DATO: BINANCE, E PERCHE' L'OBIEZIONE DEL PROGETTO NON MORDE QUI +----------------------------------------------------------------- +CLAUDE.md: "Binance NON e' la verita' (e' USDT, ~10 bps fuori dal consenso, e sotto depeg fino al +3%) -> usare SOLO come audit indipendente, mai come ancora per ripulire i dati". Quella regola +riguarda l'uso come **ANCORA DI PREZZO** (livello). Qui Binance serve come **audit indipendente per +un segnale CROSS-SECTIONAL DI RANKING su barre giornaliere**, e i tre difetti citati sono tutti +**fattori COMUNI alla valuta di quotazione**: + - uno scarto costante di ~10 bps sul livello e' identico su tutti e 19 gli asset -> sparisce + ESATTAMENTE nello z-score cross-sectional (che sottrae la media di sezione); + - un depeg USDT muove tutte le gambe insieme -> non cambia il RANKING, e il libro e' + dollar-neutral (0.5/k long, -0.5/k short) quindi il fattore comune si cancella anche nel P&L; + - a 1d un errore di livello di 10 bps e' 1/50 della vol giornaliera tipica di un alt. +NON e' un argomento teorico: e' MISURATO qui sotto (sezione 3) confrontando il meccanismo su +Binance e su Hyperliquid nella finestra in cui esistono ENTRAMBI (2024+). +E i dati scaricati sono CERTIFICATI comunque (barre flat, gap di calendario, volumi zero = +backfill sintetico: il progetto ci e' gia' cascato una volta, correzione AXS del 2026-06-20). + +MECCANISMO: CONGELATO. Nessun parametro toccato. E' `src/portfolio/sleeves.py::_xsec_returns` +(lookbacks (30,90), H=10, k=5, mode="mom", target_vol=0.20, gate dispersione p30 espandente +causale, fee 5 bps/lato, vol-target su rv30 shiftata, clip leva 3x). Il motore qui sotto e' una +versione MASCHERATA (regge NaN = asset non ancora quotato) e la sezione 2 dimostra che riproduce +lo sleeve ufficiale a **max|diff| = 0.0** sui dati HL. + +ESITO (2026-08-22, corsa completa 78 s) +--------------------------------------- +- Il motore replica lo sleeve ufficiale a max|diff| = 0.0; Binance e Hyperliquid danno lo STESSO + sleeve sulla finestra comune (corr 0.9991, dSharpe -0.025) -> la storia lunga e' utilizzabile. +- Fuori dalla finestra di scoperta (2021-01 -> 2023-12, contiene LUNA e FTX) il meccanismo ha un + edge: Sharpe MEDIANO di fase **+1.12** (U13 fisso) / **+1.17** (universo espandente), positivo in + **10/10** fasi, p = 0.013 contro il null di permutazione cross-sezionale a fee zero. +- **Il fuori campione e' MIGLIORE della finestra di scoperta**, a parita' di universo: differenza + appaiata per fase +0.668 (10/10 fasi). L'ipotesi "il drift di XS01 e' un artefatto del 2024-2026" + e' REFUTATA per la parte testabile del sleeve. +- MA: il maxDD raddoppia (10.8% nella scoperta -> 20.9-22.0% mediano fuori), il 2021 vale meta' + dell'edge, una gamba sola (LTC) ne vale 0.9 di Sharpe, e i due affinamenti del 2026-06-19 non si + replicano. E il deflated-Sharpe FALLISCE (0.342-0.418). +- Sul LIBRO, fuori campione: TP01+SKH01 75/25 Sharpe +1.17 -> **+1.65** e maxDD 9.4% -> **6.0%** + a peso XS01 0.25; a peso **0.50** (l'argmax di PROP-ALLOC) il maxDD risale a **8.9%**. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822b_xs01_oos.py +Rete: solo lettura, <= 2 req/s, MAI nella finestra :25-:29 (rate limit per-IP condiviso col +collettore della catena). Il download e' messo in cache nello scratchpad: la seconda corsa e' +offline. +""" +from __future__ import annotations + +import math +import os +import sys +import time +from datetime import datetime, timezone +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import altlib as A # noqa: E402 +from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE # noqa: E402 + +CACHE = Path(os.environ.get( + "R0822B_CACHE", + "/tmp/claude-1001/-opt-docker-PythagorasGoal/b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/r0822b", +)) +CACHE.mkdir(parents=True, exist_ok=True) + +RAW = ROOT / "data" / "raw" +FEE_SIDE = 0.001 / 2.0 # identica al sleeve congelato +DISCOVERY = pd.Timestamp("2024-01-01", tz="UTC") # inizio della finestra di scoperta (HL) + +# I 13 dei 19 quotati su Binance prima del 2020-11 -> l'universo FISSO piu' lungo possibile +# che rispetta il vincolo del meccanismo congelato (>= 10 gambe). +U13 = ["BTC", "ETH", "BNB", "XRP", "LTC", "ADA", "LINK", "DOGE", "SOL", "AVAX", "NEAR", "AAVE", "INJ"] +SECTORS = ["xlb", "xlc", "xle", "xlf", "xli", "xlk", "xlp", "xlre", "xlu", "xlv", "xly"] + + +# =========================================================================== +# 0. disciplina di rete +# =========================================================================== +def _outside_forbidden_window() -> None: + """Il collettore della catena gira al minuto :25 e satura il rate limit per-IP di Deribit; + la regola d'ondata e' non toccare la rete fra :25 e :29. Qui parliamo con Binance, ma la + disciplina e' della MACCHINA (2 core, libro con soldi veri), non del venue.""" + while True: + m = datetime.now(timezone.utc).minute + if not (25 <= m <= 29): + return + print(f" [rete] minuto :{m:02d} nella finestra vietata :25-:29 β€” attendo 30s", flush=True) + time.sleep(30) + + +# =========================================================================== +# 1. FETCH + CERTIFY (Binance spot USDT, 1d) +# =========================================================================== +def fetch_binance_daily(sym: str, since_iso: str = "2017-01-01T00:00:00Z") -> pd.DataFrame: + f = CACHE / f"bin_{sym.lower()}_1d.parquet" + if f.exists(): + return pd.read_parquet(f) + import ccxt + ex = ccxt.binance({"enableRateLimit": True}) + since = ex.parse8601(since_iso) + rows, cur = [], since + while True: + _outside_forbidden_window() + batch = ex.fetch_ohlcv(f"{sym}/USDT", "1d", since=cur, limit=1000) + time.sleep(0.5) # <= 2 req/s + if not batch: + break + rows.extend(batch) + if len(batch) < 1000: + break + cur = batch[-1][0] + 86_400_000 + d = pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"]) + d = d.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True) + d.to_parquet(f) + return d + + +def certify(sym: str, d: pd.DataFrame) -> dict: + """Certificazione locale, stessa forma di scripts/analysis/fetch_hyperliquid.py. + Il rivelatore del backfill SINTETICO NON e' il prezzo (cerbero copiava i prezzi da un altro + venue) ma il **VOLUME**: un run di barre a volume 0 e' padding, non mercato.""" + t = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + c = d["close"].values.astype(float) + v = d["volume"].values.astype(float) + span = (t.max() - t.min()).days + 1 + flat = float(np.mean(c[1:] == c[:-1])) if len(c) > 1 else float("nan") + zero_v = float(np.mean(v == 0.0)) + # run iniziale a volume zero = firma del padding sintetico (lezione AXS 2026-06-20) + lead0 = int(np.argmax(v > 0)) if (v > 0).any() else len(v) + return dict(sym=sym, n=len(d), start=str(t.min().date()), end=str(t.max().date()), + gap_days=int(span - len(d)), flat=flat, zero_vol=zero_v, lead_zero_vol=lead0, + ok=bool(span - len(d) == 0 and flat < 0.02 and zero_v < 0.01 and lead0 == 0)) + + +def load_panel(syms: list[str]) -> pd.DataFrame: + """Pannello dei CLOSE, indice giornaliero UTC, NaN dove l'asset non e' ancora quotato.""" + cols = {} + for s in syms: + d = fetch_binance_daily(s) + cols[s] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + return pd.concat(cols, axis=1).sort_index() + + +def load_sector_panel() -> pd.DataFrame: + cols = {} + for s in SECTORS: + d = pd.read_parquet(RAW / f"eq_{s}_1d.parquet") + cols[s.upper()] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + return pd.concat(cols, axis=1).sort_index() + + +# =========================================================================== +# 2. MOTORE β€” meccanismo CONGELATO, versione mascherata (regge i NaN) +# =========================================================================== +def xsec_engine(P: pd.DataFrame, phase: int = 0, fee_side: float = FEE_SIDE, + mode: str | None = None, min_assets: int = 10, + perm_rng: np.random.Generator | None = None, + perm_kind: str = "cross", lookbacks=None, disp_pct=None, + detail: dict | None = None) -> pd.Series: + """Riproduce `sleeves._xsec_returns` riga per riga. Unica differenza: l'insieme ammissibile + e' calcolato PER RIBILANCIAMENTO (un asset entra quando ha almeno max(lookbacks) barre), cosi' + il meccanismo si puo' applicare a un universo che si allarga nel tempo β€” che e' cio' che un + implementatore in tempo reale avrebbe avuto. Con un pannello senza NaN, e' identico all'originale. + + `perm_rng` attiva un NULL: 'cross' = permuta lo score FRA GLI ASSET al ribilanciamento + (distrugge l'informazione, conserva ESATTAMENTE la struttura di posizione e di turnover), + 'sign' = pesi casuali fra gli ammissibili (stesso gross, stesso k).""" + px = P.values.astype(float) + n, Ncols = px.shape + H, k = XS_CFG["H"], XS_CFG["k"] + lookbacks = tuple(XS_CFG["lookbacks"] if lookbacks is None else lookbacks) + disp_pct = XS_CFG["disp_pct"] if disp_pct is None else disp_pct + tv, minhist = XS_CFG["target_vol"], XS_CFG["disp_minhist"] + mode = mode or XS_CFG["mode"] + mlb = max(lookbacks) + + with np.errstate(invalid="ignore", divide="ignore"): + dret = np.vstack([np.zeros(Ncols), px[1:] / px[:-1] - 1.0]) + dret = np.nan_to_num(dret, nan=0.0, posinf=0.0, neginf=0.0) + + W = np.zeros((n, Ncols)); w = np.zeros(Ncols); disp_hist: list[float] = [] + n_rebal = n_traded = 0 + for i in range(n): + if i >= mlb and (i % H) == phase: + elig = np.isfinite(px[i]) + for L in lookbacks: + elig = elig & np.isfinite(px[i - L]) + m = int(elig.sum()) + if m >= min_assets: + n_rebal += 1 + idx = np.flatnonzero(elig) + rLs = [px[i, idx] / px[i - L, idx] - 1.0 for L in lookbacks] + disp_i = float(np.mean([r.std() for r in rLs])) + thr = (np.percentile(disp_hist, disp_pct) + if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf) + if disp_i >= thr: + score = np.zeros(m); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if cnt: + score /= cnt + w = np.zeros(Ncols) + if perm_rng is not None and perm_kind == "sign": + pick = perm_rng.permutation(m) + lo, hi = idx[pick[:k]], idx[pick[-k:]] + else: + if perm_rng is not None: # perm_kind == "cross" + score = score[perm_rng.permutation(m)] + order = np.argsort(score) + lo, hi = idx[order[:k]], idx[order[-k:]] + if mode == "mom": + w[hi] = 0.5 / k; w[lo] = -0.5 / k + else: + w[lo] = 0.5 / k; w[hi] = -0.5 / k + n_traded += 1 + else: + w = np.zeros(Ncols) + disp_hist.append(disp_i) + W[i] = w + + gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) + turn = np.zeros(n); turn[0] = np.abs(W[0]).sum() + turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) + net = gross - turn * fee_side + s = pd.Series(net, index=P.index) + rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) + scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) + out = pd.Series(s.values * scale, index=P.index) + out.attrs["n_rebal"] = n_rebal + out.attrs["n_traded"] = n_traded + if detail is not None: + detail.update(W=W, scale=scale, turn=turn, index=P.index, + n_rebal=n_rebal, n_traded=n_traded) + return out + + +def hl_panel(syms=None) -> pd.DataFrame: + """Pannello Hyperliquid, ESATTAMENTE come lo costruisce lo sleeve congelato.""" + cols = {} + for sym in (syms or XS_UNIVERSE): + p = RAW / f"hl_{sym.lower()}_1d.parquet" + if not p.exists(): + continue + d = pd.read_parquet(p) + cols[sym] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + return pd.concat(cols, axis=1, join="inner").sort_index().dropna() + + +# =========================================================================== +# metriche +# =========================================================================== +def sh(s: pd.Series) -> float: + return A._sh(s) + + +def dd(s: pd.Series) -> float: + return A._dd_ret(s) + + +def cagr(s: pd.Series) -> float: + r = s.dropna() + if len(r) < 30: + return float("nan") + yrs = (r.index[-1] - r.index[0]).days / 365.25 + return float(np.prod(1.0 + r.values) ** (1.0 / yrs) - 1.0) if yrs > 0 else float("nan") + + +def win(s: pd.Series, a: str | None = None, b: str | None = None) -> pd.Series: + x = s + if a: + x = x[x.index >= pd.Timestamp(a, tz="UTC")] + if b: + x = x[x.index < pd.Timestamp(b, tz="UTC")] + return x + + +def alive(s: pd.Series) -> pd.Series: + """Taglia il prefisso di ZERI (warm-up dei lookback + attesa che l'universo raggiunga 10 + gambe). ⚠️ Bug catturato nella prima corsa: sull'universo ESPANDENTE il pannello parte dal + 2017 (BTC) ma il meccanismo non puo' operare prima del 2020-12, e i ~1.200 zeri iniziali + entravano nella finestra 'PRE-2024' abbassandone lo Sharpe e ribaltando la differenza + appaiata. E' la lezione gia' codificata nel progetto (barre ATTIVE, non di calendario), + ripetuta in un contesto nuovo.""" + v = np.asarray(s.values, float) + nz = np.flatnonzero(v != 0.0) + return s.iloc[nz[0]:] if len(nz) else s + + +def line(tag: str, s: pd.Series) -> str: + return (f"{tag:<22s} n={len(s):>5d} Sh {sh(s):+6.2f} maxDD {dd(s)*100:5.1f}% " + f"CAGR {cagr(s)*100:+6.2f}% vol {s.std()*np.sqrt(365.25)*100:5.1f}%") + + +# =========================================================================== +def main() -> None: + t0 = time.time() + print("=" * 100) + print("XS01 FUORI DALLA FINESTRA DI SCOPERTA β€” meccanismo CONGELATO su storia lunga indipendente") + print(f"XS_CFG = {XS_CFG}") + print("=" * 100) + + # ---------------------------------------------------------------- 1. dato + print("\n[1] DATO β€” Binance spot USDT 1d, certificazione locale") + panel19 = load_panel(XS_UNIVERSE) + cert = [certify(s, fetch_binance_daily(s)) for s in XS_UNIVERSE] + bad = [c for c in cert if not c["ok"]] + print(f" {len(cert)} serie scaricate; certificate OK: {len(cert)-len(bad)}/{len(cert)}") + print(f" {'sym':<6}{'n':>6}{'start':>12}{'end':>12}{'gap':>5}{'flat%':>8}{'vol0%':>8}{'lead0':>7}") + for c in cert: + flag = "" if c["ok"] else " <-- CONTROLLA" + print(f" {c['sym']:<6}{c['n']:>6}{c['start']:>12}{c['end']:>12}{c['gap_days']:>5}" + f"{c['flat']*100:>8.2f}{c['zero_vol']*100:>8.2f}{c['lead_zero_vol']:>7}{flag}") + print(" NB: 'lead0' = barre iniziali a volume 0 = firma del backfill sintetico (lezione AXS).") + + # ---------------------------------------------------- 2. replica bit-exact + print("\n[2] SANITY β€” il motore riproduce lo sleeve UFFICIALE?") + from src.portfolio.sleeves import _xsec_returns + off = _xsec_returns() + rep = xsec_engine(hl_panel(), phase=0) + j = pd.concat({"off": off, "rep": rep}, axis=1, join="inner").dropna() + md = float(np.max(np.abs(j["off"] - j["rep"]))) if len(j) else float("nan") + print(f" n={len(j)} max|diff| = {md:.2e} -> {'IDENTICO' if md < 1e-12 else 'DIVERGE'}") + print(f" {line('XS01 ufficiale (HL)', off)}") + if md >= 1e-12: + print(" !! il motore NON replica: ogni numero sotto e' sospetto. STOP.") + return + + # ------------------------------------------------ 3. validazione di VENUE + print("\n[3] VALIDAZIONE DI VENUE β€” Binance vs Hyperliquid dove esistono ENTRAMBI (2024+)") + hlp = hl_panel() + b19 = panel19.reindex(columns=XS_UNIVERSE) + common = b19.index.intersection(hlp.index) + rr = [] + for s in XS_UNIVERSE: + a1 = hlp[s].reindex(common).pct_change() + b1 = b19[s].reindex(common).pct_change() + k2 = pd.concat({"a": a1, "b": b1}, axis=1).dropna() + rr.append((s, float(k2["a"].corr(k2["b"])), float(np.median(np.abs(k2["a"] - k2["b"]))) * 1e4)) + print(f" ritorni giornalieri per asset, {len(common)} giorni comuni:") + print(" " + " ".join(f"{s}:{c:.4f}" for s, c, _ in rr)) + print(f" corr min {min(c for _, c, _ in rr):.4f} | mediana |diff| ritorno: " + f"{np.median([m for _, _, m in rr]):.1f} bps (max {max(m for _, _, m in rr):.1f} bps)") + # e soprattutto: il MECCANISMO da' la stessa cosa? + bin_on_hl_window = xsec_engine(b19.loc[common].dropna(), phase=0) + hl_on_window = xsec_engine(hlp.loc[common], phase=0) + jj = pd.concat({"bin": bin_on_hl_window, "hl": hl_on_window}, axis=1, join="inner").dropna() + print(f" {line('XS01 su HL 2024+', jj['hl'])}") + print(f" {line('XS01 su BINANCE 2024+', jj['bin'])}") + print(f" corr fra le DUE VERSIONI DELLO SLEEVE: {jj['bin'].corr(jj['hl']):.4f} " + f"dSharpe = {sh(jj['bin'])-sh(jj['hl']):+.3f}") + print(" -> se questa corr e' ~1, la storia lunga Binance e' un'estensione legittima:") + print(" lo scarto USDT e' un FATTORE COMUNE e sparisce nello z-score cross-sectional.") + + # ------------------------------------- 4. la storia lunga, fuori campione + print("\n[4] STORIA LUNGA β€” meccanismo congelato, ancora canonica (fase 0)") + p13 = panel19[U13].dropna() # universo FISSO 13 (il piu' lungo a >=10 gambe) + s13 = xsec_engine(p13, phase=0) + sEXP = xsec_engine(panel19, phase=0) # universo che si ALLARGA (19 quando esistono) + s13, sEXP = alive(s13), alive(sEXP) + print(f" U13-FIX universo fisso {len(U13)} gambe, dal {s13.index[0].date()} " + f"({s13.attrs.get('n_traded')}/{s13.attrs.get('n_rebal')} ribilanciamenti con posizione)") + print(f" U19-EXP universo espandente (10 -> 19 gambe), dal {sEXP.index[0].date()}") + for tag, s in (("U13-FIX", s13), ("U19-EXP", sEXP)): + print(f" {line(tag + ' FULL', s)}") + print(f" {line(tag + ' PRE-2024 (OOS)', win(s, None, '2024-01-01'))}") + print(f" {line(tag + ' 2024+ (scoperta)', win(s, '2024-01-01'))}") + + print("\n Sharpe per ANNO (netto fee):") + yrs = sorted({d.year for d in sEXP.index}) + hdr = " " + f"{'':<10}" + "".join(f"{y:>9d}" for y in yrs) + print(hdr) + for tag, s in (("U13-FIX", s13), ("U19-EXP", sEXP), ("XS01 HL", off)): + cells = [] + for y in yrs: + x = win(s, f"{y}-01-01", f"{y+1}-01-01") + cells.append(f"{sh(x):>9.2f}" if len(x) > 60 else f"{'β€”':>9s}") + print(f" {tag:<10}" + "".join(cells)) + + print("\n Sharpe per REGIME:") + regimes = [("2021 toro/top", "2021-01-01", "2022-01-01"), + ("2022 ORSO (LUNA/FTX)", "2022-01-01", "2023-01-01"), + ("2023 ripresa", "2023-01-01", "2024-01-01"), + ("2024-26 scoperta", "2024-01-01", None)] + for nm, a, b in regimes: + x13, xEX = win(s13, a, b), win(sEXP, a, b) + print(f" {nm:<24s} U13 Sh {sh(x13):+6.2f} DD {dd(x13)*100:5.1f}% | " + f"U19-EXP Sh {sh(xEX):+6.2f} DD {dd(xEX)*100:5.1f}%") + + # -------------------------------------------- 5. fortuna di FASE (H = 10) + print("\n[5] FORTUNA DI FASE β€” H=10, tutte e 10 le ancore (la stima onesta e' la MEDIANA)") + phases = list(range(XS_CFG["H"])) + bands = {} + for tag, P, cut in (("U13-FIX FULL", p13, None), ("U13-FIX PRE-2024", p13, "pre"), + ("U19-EXP FULL", panel19, None), ("U19-EXP PRE-2024", panel19, "pre")): + def f(ph, P=P, cut=cut): + s = alive(xsec_engine(P, phase=ph)) + return win(s, None, "2024-01-01") if cut == "pre" else s + bands[tag] = A.anchor_luck_band(f, phases, canonical=0, metric=sh) + bands[tag + "|DD"] = A.anchor_luck_band(f, phases, canonical=0, metric=lambda x: -dd(x)) + for tag in ("U13-FIX FULL", "U13-FIX PRE-2024", "U19-EXP FULL", "U19-EXP PRE-2024"): + r = bands[tag]; rd = bands[tag + "|DD"] + print(f" {tag:<20s} Sh canonico {r['canonical']:+.2f} (pctl {r['canonical_pctl']*100:4.0f}Β°) " + f"| MEDIANA {r['median']:+.2f} | banda [{r['lo']:+.2f},{r['hi']:+.2f}] " + f"| fasi>0 {r['frac_positive']*100:3.0f}% | fortuna {r['luck']:+.2f} | gate {r['gate_pass']}") + print(f" {'':<20s} maxDD canonico {-rd['canonical']*100:5.1f}% " + f"| MEDIANA {-rd['median']*100:5.1f}% | banda [{-rd['hi']*100:.1f},{-rd['lo']*100:.1f}]%") + # anche lo sleeve UFFICIALE su HL, de-luckato di fase (per confronto onesto) + bhl = A.anchor_luck_band(lambda ph: xsec_engine(hlp, phase=ph), phases, canonical=0, metric=sh) + print(f" {'XS01 UFFICIALE (HL)':<20s} Sh canonico {bhl['canonical']:+.2f} " + f"(pctl {bhl['canonical_pctl']*100:4.0f}Β°) | MEDIANA {bhl['median']:+.2f} " + f"| banda [{bhl['lo']:+.2f},{bhl['hi']:+.2f}] | fortuna {bhl['luck']:+.2f}") + + # -------------- 6. differenza APPAIATA fuori campione vs finestra di scoperta + print("\n[6] DIFFERENZA APPAIATA per fase: (fuori campione) - (finestra di scoperta)") + for tag, P in (("U13-FIX", p13), ("U19-EXP", panel19)): + def pre(ph, P=P): + return win(alive(xsec_engine(P, phase=ph)), None, "2024-01-01") + + def post(ph, P=P): + return win(alive(xsec_engine(P, phase=ph)), "2024-01-01") + d1 = A.anchor_luck_delta(pre, post, phases, metric=sh) + print(f" {tag:<10s} mediana DIFF appaiata {d1['median_paired']:+.3f} " + f"[{d1['lo']:+.3f},{d1['hi']:+.3f}] fasi con OOS>scoperta {d1['n_positive']}/{d1['n_anchors']}") + + # ------------------------------------------------------------- 7. i NULL + print("\n[7] NULL β€” a FEE ZERO (permutare un segnale ne fa esplodere il turnover: a fee piene") + print(" il null perderebbe per COSTO invece che per assenza di informazione β€” regola XSR01)") + NDRAW = 300 + for tag, P, a, b in (("U13-FIX PRE-2024", p13, None, "2024-01-01"), + ("U13-FIX FULL", p13, None, None)): + real = win(alive(xsec_engine(P, phase=0, fee_side=0.0)), a, b) + for kind in ("cross", "sign"): + rng = np.random.default_rng(20260822) + draws = np.array([sh(win(alive(xsec_engine(P, phase=0, fee_side=0.0, perm_rng=rng, + perm_kind=kind)), a, b)) for _ in range(NDRAW)]) + p = float(np.mean(draws >= sh(real))) + print(f" {tag:<20s} null '{kind}': reale(lordo) {sh(real):+.2f} vs null " + f"med {np.median(draws):+.2f} max {draws.max():+.2f} -> p = {p:.4f}") + + print("\n NULL 'universo dove NON dovrebbe funzionare' β€” 11 settoriali SPDR, 1998+,") + print(" stesso meccanismo congelato (il progetto ha gia' misurato che il cross-sectional") + print(" crypto non generalizza alle azioni: XSR01, 2026-07-26):") + sect = load_sector_panel() + ssec = alive(xsec_engine(sect, phase=0, min_assets=9)) + bsec = A.anchor_luck_band(lambda ph: alive(xsec_engine(sect, phase=ph, min_assets=9)), phases, + canonical=0, metric=sh) + print(f" {line('SECT11 (azioni)', ssec)}") + print(f" SECT11 banda di fase: mediana Sh {bsec['median']:+.2f} " + f"[{bsec['lo']:+.2f},{bsec['hi']:+.2f}], fasi>0 {bsec['frac_positive']*100:.0f}%") + + print("\n CONTROLLO DI DIREZIONE β€” stesso meccanismo con mode='rev' (long i deboli):") + for tag, P in (("U13-FIX", p13), ("U19-EXP", panel19)): + r = alive(xsec_engine(P, phase=0, mode="rev")) + print(f" {line(tag + ' REV', r)}") + + # ------------- 7b. la SELEZIONE del 2026-06-19, rivalidata FUORI CAMPIONE + print("\n[7b] LA SELEZIONE DEL 2026-06-19, RIVALIDATA FUORI CAMPIONE") + print(" XS01 non e' solo un meccanismo: e' un meccanismo AFFINATO due volte sui dati HL") + print(" 2024+ (blend di lookback [30,90] e gate di dispersione p30). Quelle DUE scelte sono") + print(" debito di multiple-testing contratto sulla finestra di scoperta. Qui si paga:") + print(" ogni variante e' valutata sul PRE-2024, a MEDIANA delle 10 fasi (mai l'ancora).") + variants = [("blend [30,90] + gate p30 (CANONICA)", dict(lookbacks=(30, 90), disp_pct=30)), + ("solo L=30 + gate p30", dict(lookbacks=(30,), disp_pct=30)), + ("solo L=90 + gate p30", dict(lookbacks=(90,), disp_pct=30)), + ("blend [30,90] SENZA gate", dict(lookbacks=(30, 90), disp_pct=0)), + ("blend [30,90] + gate p15", dict(lookbacks=(30, 90), disp_pct=15)), + ("blend [30,90] + gate p50", dict(lookbacks=(30, 90), disp_pct=50))] + sel_rows = [] + for nm, kw in variants: + b_pre = A.anchor_luck_band( + lambda ph, kw=kw: win(alive(xsec_engine(p13, phase=ph, **kw)), None, "2024-01-01"), + phases, canonical=0, metric=sh) + b_post = A.anchor_luck_band( + lambda ph, kw=kw: win(alive(xsec_engine(p13, phase=ph, **kw)), "2024-01-01"), + phases, canonical=0, metric=sh) + sel_rows.append((nm, b_pre["median"], b_post["median"], b_pre["per_offset"])) + print(f" {nm:<38s} PRE-2024 mediana Sh {b_pre['median']:+5.2f} " + f"[{b_pre['lo']:+.2f},{b_pre['hi']:+.2f}] | 2024+ {b_post['median']:+5.2f}") + rank_pre = 1 + sorted([r[1] for r in sel_rows], reverse=True).index(sel_rows[0][1]) + rank_post = 1 + sorted([r[2] for r in sel_rows], reverse=True).index(sel_rows[0][2]) + print(f" -> la configurazione CANONICA (scelta nel 2026-06 guardando SOLO il 2024+) e'") + print(f" {rank_pre}/{len(sel_rows)} sul PRE-2024 e {rank_post}/{len(sel_rows)} sul 2024+.") + print( " πŸ“Œ LETTURA: il NUCLEO regge (ogni variante e' positiva fuori campione, banda") + print( " [+0.76,+1.28]), ma i DUE AFFINAMENTI del 19/06 NON si replicano:") + print( " - il BLEND [30,90] fuori campione e' PEGGIO del solo L=30 (+1.12 vs +1.28);") + print( " - il GATE di dispersione p30 fuori campione vale ZERO (con e senza: +1.12);") + print( " e' dentro la finestra di scoperta che vale (+0.37 contro -0.03).") + print( " Cioe': cio' che sopravvive e' il meccanismo cross-sectional nudo; i due") + print( " affinamenti sono, per quanto qui misurabile, adattamento alla finestra in") + print( " cui furono scelti. NON e' un argomento per cambiarli (cambiarli guardando") + print( " QUESTO risultato sarebbe la stessa selezione, spostata di finestra): e'") + print( " un argomento per non attribuire loro il valore che il progetto gli attribuisce.") + + # --- concentrazione: un contributo positivo si scompone per ANNO e per GAMBA (lezione SOL) + print("\n Concentrazione (lezione SOL 22/08: '24/24 ancore positive erano UN ANNO SOLO'):") + pre13 = win(s13, None, "2024-01-01") + for y in (2021, 2022, 2023): + rest = pre13[pre13.index.year != y] + print(f" PRE-2024 togliendo il {y}: Sh {sh(rest):+5.2f} " + f"(intero pre-2024 {sh(pre13):+5.2f})") + loo = {} + for a_ in U13: + sub = panel19[[x for x in U13 if x != a_]].dropna() + bb = A.anchor_luck_band(lambda ph, sub=sub: win(alive(xsec_engine(sub, phase=ph, + min_assets=10)), + None, "2024-01-01"), + phases, canonical=0, metric=sh) + loo[a_] = bb["median"] + base_med = bands["U13-FIX PRE-2024"]["median"] + worst = min(loo, key=loo.get) + print(f" Leave-one-asset-out sul PRE-2024 (mediana di fase; base {base_med:+.2f}):") + print(" " + " ".join(f"-{k2}:{v:+.2f}" for k2, v in sorted(loo.items(), key=lambda t: t[1]))) + print(f" gamba piu' importante: {worst} (senza di lei {loo[worst]:+.2f}); " + f"minimo {min(loo.values()):+.2f}, tutte >0: {all(v > 0 for v in loo.values())}") + + # ---------------------------------------------------------- 8. i GATE + print("\n[8] GATE") + cand = s13.copy(); cand.index = cand.index.tz_convert("UTC") + mg = A.marginal_vs_tp01(cand) + print(f" marginal_vs_tp01 (U13-FIX, 2021+): {mg.get('marginal_verdict')} " + f"corr {mg.get('corr_full')} n={mg.get('n_days')}") + for kk in ("robust_oos", "has_insample_edge", "is_hedge", "beats_noise_null", + "insample_sharpe", "tp01_beta", "alpha_ann"): + if kk in mg: + print(f" {kk:<20s} {mg[kk]}") + if "blends" in mg: + for wname, bl in mg["blends"].items(): + print(f" blend {wname}: uplift_full {bl.get('uplift_full')} " + f"uplift_hold {bl.get('uplift_hold')}") + + mgE = A.marginal_vs_tp01(sEXP) + print(f" marginal_vs_tp01 (U19-EXP, 2020-12+): {mgE.get('marginal_verdict')} " + f"corr {mgE.get('corr_full')}") + + # deflated Sharpe: TUTTI i trial valutati, al RIALZO + all_sr = [] + for tag in ("U13-FIX FULL", "U13-FIX PRE-2024", "U19-EXP FULL", "U19-EXP PRE-2024"): + all_sr += list(bands[tag]["per_offset"].values()) + all_sr += list(bhl["per_offset"].values()) + list(bsec["per_offset"].values()) + for _, _, _, po in sel_rows: # 6 varianti x 10 fasi della sezione 7b + all_sr += list(po.values()) + all_sr += [float(v) for v in loo.values()] + cand_pool = list(all_sr) # SOLO configurazioni di XS01 + ctrl = [sh(alive(xsec_engine(p13, phase=0, mode="rev"))), + sh(alive(xsec_engine(panel19, phase=0, mode="rev"))), sh(jj["bin"]), sh(jj["hl"])] + for fs in (0.0005, 0.001, 0.002): + ctrl.append(sh(alive(xsec_engine(p13, phase=0, fee_side=fs)))) + dsr_val = bands["U13-FIX PRE-2024"]["median"] + med_ph = int(np.argsort(list(bands["U13-FIX PRE-2024"]["per_offset"].values()))[len(phases) // 2]) + med_series = win(alive(xsec_engine(p13, phase=med_ph)), None, "2024-01-01") + d1, s1 = A.deflated_sharpe(dsr_val, cand_pool, med_series) + d2, s2 = A.deflated_sharpe(dsr_val, cand_pool + ctrl, med_series) + print(f" deflated_sharpe β€” headline = MEDIANA di fase OOS {dsr_val:+.2f}") + print(f" pool A = SOLE configurazioni XS01 ({len(cand_pool)} trial): DSR {d1:.3f} " + f"(max dal rumore {s1:+.2f}) -> {'PASS' if d1 >= 0.95 else 'FAIL'}") + print(f" pool B = tutto cio' che ho valutato, controlli NEGATIVI inclusi " + f"({len(cand_pool)+len(ctrl)} trial): DSR {d2:.3f} (max dal rumore {s2:+.2f}) " + f"-> {'PASS' if d2 >= 0.95 else 'FAIL'}") + print( " ⚠️ Il pool B e' il conteggio 'al rialzo' che il progetto impone, ma qui e' anche") + print( " il piu' SEVERO in modo improprio: il DSR pesa la VARIANZA dei trial, e io ci") + print( " ho messo dentro controlli costruiti per essere negativi (REV -0.97, SECT11") + print( " -0.44). Si cita il piu' basso dei due; e si dice che entrambi penalizzano un") + print( " MEDIANA-di-fase come se fosse un MAX-di-k, che e' il caso per cui il DSR e'") + print( " costruito. Il debito di selezione vero di XS01 non e' stato contratto qui:") + print( " e' quello del 2026-06-19, e la sezione 7b lo misura direttamente.") + + imp = A.implausible_sharpe(s13) + print(f" implausible_sharpe: {imp['implausible']} Sh {imp.get('sharpe'):+.2f} " + f"maxDD {imp.get('maxdd', 0)*100:.1f}% perdite su barre attive " + f"{imp.get('loss_frac', float('nan'))*100:.1f}% (attive {imp.get('active_frac', 0)*100:.0f}%)" + f"{' β€” ' + '; '.join(imp['reasons']) if imp['reasons'] else ''}") + + # causalita': i prezzi DOPO la data di decisione non devono cambiare le posizioni prima + Pc = p13.copy() + cut = len(Pc) // 2 + rngc = np.random.default_rng(7) + Pd = Pc.copy() + Pd.iloc[cut:] = Pc.iloc[cut:].values * (1.0 + rngc.normal(0, 0.35, size=Pc.iloc[cut:].shape)) + a_ = xsec_engine(Pc, phase=0); b_ = xsec_engine(Pd, phase=0) + # confronto solo dove il vol-target non ha ancora "visto" i dati perturbati (rv30 shiftata) + lim = cut - 35 + cdiff = float(np.max(np.abs(a_.values[:lim] - b_.values[:lim]))) + print(f" causalita' (prezzi futuri randomizzati da i={cut}): max|diff| prima di i={lim} " + f"= {cdiff:.2e} -> {'LEAK-FREE' if cdiff < 1e-12 else 'PERDITA DI FUTURO'}") + + print(" null de-levering: NON PERTINENTE β€” il claim qui non e' 'meno drawdown' ma " + "'l'edge esiste fuori campione'. Il de-levering e' comunque implicito nel vol-target,") + print(" e le tabelle di fase riportano DD e Sharpe INSIEME (un k<1 abbassa entrambi).") + + print(f"\n sensibilita' al COSTO (il rischio #1 di uno sleeve a 13 gambe alt):") + for fs in (0.0005, 0.001, 0.002, 0.003): + x = alive(xsec_engine(p13, phase=0, fee_side=fs)) + print(f" {fs*1e4:5.0f} bps/lato: FULL Sh {sh(x):+5.2f} | " + f"PRE-2024 {sh(win(x, None, '2024-01-01')):+5.2f} | " + f"2024+ {sh(win(x, '2024-01-01')):+5.2f}") + + # ------------------------------------------------- 9. impatto sul LIBRO + print("\n[9] IMPATTO SUL LIBRO β€” TP01+SKH01 (75/25 live) con e senza XS01, su 2021+") + from src.portfolio.sleeves import _tp01_returns, _skyhook_returns + tp = A._to_daily(_tp01_returns()); sk = A._to_daily(_skyhook_returns()) + for xs_tag, xs in (("XS01 storia lunga (U13-FIX)", s13), ("XS01 ufficiale (HL 2024+)", off)): + J = pd.concat({"tp": tp, "sk": sk, "xs": A._to_daily(xs)}, axis=1, join="inner").dropna() + if len(J) < 200: + continue + base = 0.75 * J["tp"] + 0.25 * J["sk"] + for w in (0.15, 0.25, 0.50): + mix = (1 - w) * base + w * J["xs"] + print(f" [{xs_tag}] {J.index[0].date()}..{J.index[-1].date()} n={len(J)} " + f"w={w:.2f}: Sh {sh(base):+.2f} -> {sh(mix):+.2f} " + f"({sh(mix)-sh(base):+.2f}) maxDD {dd(base)*100:.1f}% -> {dd(mix)*100:.1f}% " + f"corr(XS,libro) {J['xs'].corr(base):+.3f}") + # e il numero che il PROP-ALLOC usa davvero: il libro 2021+ contiene il 2022 + pre = J[J.index < DISCOVERY] + if len(pre) > 200: + bpre = 0.75 * pre["tp"] + 0.25 * pre["sk"] + print(f" -> SOLO FUORI CAMPIONE ({pre.index[0].date()}..{pre.index[-1].date()}, " + f"n={len(pre)}), sweep del peso di XS01:") + for w in (0.15, 0.25, 0.33, 0.50): + mpre = (1 - w) * bpre + w * pre["xs"] + print(f" w={w:.2f}: libro Sh {sh(bpre):+.2f} -> {sh(mpre):+.2f} " + f"({sh(mpre)-sh(bpre):+.2f}) maxDD {dd(bpre)*100:.1f}% -> " + f"{dd(mpre)*100:.1f}% corr {pre['xs'].corr(bpre):+.3f}") + print(" ⚠️ PROP-ALLOC mette XS01 al 50%: su un conto a BARRIERA cio' che si") + print(" compra e' il maxDD, quindi la riga da guardare e' quella, non lo Sharpe.") + + # ---------------------------------------------------- 10. eseguibilita' + print("\n[10] ESEGUIBILITA' β€” la domanda del canale funded e' se il libro STA su $100k") + det: dict = {} + xsec_engine(p13, phase=0, detail=det) + W, sc = det["W"], det["scale"] + dW = np.abs(np.diff(W, axis=0)); rb = np.flatnonzero(dW.sum(axis=1) > 1e-12) + print(f" {len(U13)} gambe, {len(rb)} ribilanciamenti, gross medio " + f"{np.abs(W).sum(axis=1).mean():.2f}, leva media (vol-target) {np.mean(sc[sc > 0]):.2f}x") + print(f" {'capitale':>10} {'ticket mediano':>16} {'ticket p10':>12} {'<$5':>7} {'<$10':>7}") + for cap in (600, 5000, 20000, 100000): + parts = [dW[i][dW[i] > 1e-12] * sc[i + 1] * cap for i in rb if sc[i + 1] > 0] + tk = np.concatenate(parts) if parts else np.array([]) + if not tk.size: + print(" (nessun ticket calcolabile β€” controlla il motore)"); break + print(f" {cap:>10,d} {np.median(tk):>15.2f}$ {np.percentile(tk, 10):>11.2f}$ " + f"{(tk < 5).mean()*100:>6.1f}% {(tk < 10).mean()*100:>6.1f}%") + print(" ⚠️ Approssimazione DICHIARATA: il modello congelato applica il vol-target alla") + print(" SERIE dei rendimenti, non alle posizioni -> qui il ticket e' calcolato sui soli") + print(" delta di RIBILANCIAMENTO (ogni 10 giorni), ignorando il micro-turnover") + print(" giornaliero che una implementazione vera del vol-target genererebbe. E' un") + print(" LIMITE INFERIORE del numero di ordini, quindi ottimistico sul lato costi.") + print(" ⚠️ E NON rimette in discussione il ~$20k citato dal progetto: quella soglia e' sui") + print(" 19 alt Hyperliquid (min-order e liquidita' del venue), non su questi 13.") + + print(f"\n[fine] {time.time()-t0:.1f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822c_gate_recon.py b/scripts/research/r0822c_gate_recon.py new file mode 100644 index 0000000..3e399c7 --- /dev/null +++ b/scripts/research/r0822c_gate_recon.py @@ -0,0 +1,719 @@ +"""GATE-RECON β€” cosa direbbero i tre gate pre-registrati se le serie forward fossero giuste. + +IL FATTO DA CUI NASCE (filone MONITOR-AUDIT, 22/08, gia' misurato e NON riparato per scelta). +`fetch_hyperliquid` e `resample_tf` scrivono la barra del giorno IN CORSO; `advance()` la consuma +e porta `last_ts` su di essa -> le ore restanti non entrano in nessun rendimento. Quattro monitor +su sei registrano una FRAZIONE del giorno (2, 4, 19 e 41 minuti su 1440). Tre decisioni +pre-registrate leggono serie costruite cosi': + STATARB 2026-09-27 r0724_statarb_deploy_gate (Sh>=0.5, maxDD<10%, haircut<0.5pp) + XSR01 2026-10-23 r0725_xsr_deploy_gate (Sh>=1.0 E haircut$5000<=40%) + DVOLSPREAD 2026-10-24 kill / 2027-01-24 decisione (soglie dentro paper_dvolspread) + +QUESTO SCRIPT NON RIPARA E NON DECIDE. La riparazione e' una decisione dell'operatore e i gate +hanno una data: anticiparli sarebbe selezione-sul-forward, il bias che questo progetto ha gia' +pagato due volte. La consegna e' un DELTA, non un verdetto: + "alla data di oggi, con la serie riparata il criterio direbbe Y; con la serie com'e', Z" +e la distanza fra Y e Z e' la misura del danno del difetto β€” l'unica cosa che serve per decidere +SE e QUANDO riparare. Sola lettura su tutto cio' che e' produzione: i moduli di `scripts/live/` +si importano senza chiamarne `main()`, e `_append` viene sostituito con un raccoglitore in memoria +PRIMA di qualunque chiamata ad `advance()` -> nessun file di `data/` viene aperto in scrittura. + +METODO. La serie RIPARATA non e' una reimplementazione: e' il codice di produzione rieseguito +dall'inception sui dati di oggi, TOLTA l'ultima barra (l'unica ancora aperta). Tutto il resto del +pannello e' fatto di barre chiuse, quindi il replay E' la serie che il monitor avrebbe registrato +se avesse aspettato la chiusura. Questo poggia su due condizioni, entrambe verificate qui: + (P1) il feed non riscrive le barre gia' chiuse -> sezione 0; + (P2) il pannello che leggo ora e' quello che il cron ha letto stanotte, e troncare l'ULTIMA + barra non cambia nessuna barra precedente -> sezione 1 (tre prove d'identita'). +Senza (P2) starei ricostruendo una strategia diversa invece della stessa strategia riparata, ed e' +il modo piu' facile di sbagliare questo lavoro. + +COSA MI ASPETTAVO PRIMA DI MISURARE (registrato qui perche' sia falsificabile): + * la premessa (P1) vera ma verificabile solo di traverso -> **sbagliato, ed e' la sorpresa + utile**: `data/_feed_backup/*.prebuild.bak` conserva il vintage PRE-riscrittura di stanotte, + quindi la premessa si verifica DIRETTAMENTE su ~914.000 barre, senza passare da una strategia; + * i tre gate tutti ribaltati -> **sbagliato, e la ragione conta**: solo STATARB si ribalta. + XSR01 e DVOLSPREAD dicono "ritiro/kill" su ENTRAMBE le lenti, ma con numeri che differiscono + di 3-7x e che a 28 barre non sono letture di gate su nessuna delle due; + * il veto d'integrita' di DVOLSPREAD (l'unico dei tre che ha una guardia) capace di accorgersene + -> **confermato che NON se ne accorge**, e per una ragione strutturale: conta se il DVOL C'ERA, + non quanto dura la barra su cui c'era. + + nice -n 19 timeout 900 uv run python scripts/research/r0822c_gate_recon.py +""" +from __future__ import annotations + +import importlib +import importlib.util +import json +import sys +from datetime import date +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +RAW = ROOT / "data" / "raw" +BAK = ROOT / "data" / "_feed_backup" +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt", + ROOT / "scripts" / "research" / "ortho"): + sys.path.insert(0, str(_p)) + +ANN = np.sqrt(365.0) +TODAY = date.today() +H = "=" * 100 +# Incidente del feed 2026-07-09 -> 2026-07-15 (`rebuild_history` abortito 7 giri, EPERM sul backup, +# gia' riparato): col feed fermo i monitor non trovavano barre nuove e al ripristino ne hanno +# recuperate in blocco, GIA' CHIUSE. Sono le uniche barre corrette di `paper_statarb` e vanno +# escluse da ogni stima del difetto, o gli si attribuisce un'attenuazione che viene da altro. +FREEZE = (pd.Timestamp("2026-07-09", tz="UTC"), pd.Timestamp("2026-07-16", tz="UTC")) + + +# -------------------------------------------------------------------------------------------- +# infrastruttura di sola lettura (stessa di r0822_monitor_audit: nessuna reimplementazione) +# -------------------------------------------------------------------------------------------- +_MODS: dict[str, object] = {} + + +def live(name: str): + """Esegue un modulo di `scripts/live/` per averne le DEFINIZIONI. Mai `main()`.""" + 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`: raccoglie in memoria invece di scrivere. E' l'unica ragione per cui + si puo' rieseguire il codice di produzione TALE E QUALE 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]: + return [json.loads(x) for x in p.read_text().splitlines() if x.strip()] if p.exists() else [] + + +def mets(a) -> dict: + """Sharpe (365), maxDD e ritorno cumulato di una serie di rendimenti netti per barra.""" + a = np.asarray(a, float) + a = a[np.isfinite(a)] + if len(a) < 2 or a.std() == 0: + return dict(n=len(a), sh=float("nan"), dd=float("nan"), tot=float("nan"), + mu=float("nan"), sd=float("nan")) + eq = np.cumprod(1.0 + a) + pk = np.maximum.accumulate(eq) + return dict(n=len(a), sh=float(a.mean() / a.std() * ANN), + dd=float(np.max((pk - eq) / pk)), tot=float(eq[-1] - 1.0), + mu=float(a.mean() * 365.0), sd=float(a.std() * ANN)) + + +def se_sharpe(n: int) -> float: + """Errore standard grezzo di uno Sharpe annualizzato su n barre giornaliere (~sqrt(365/n)).""" + return float(np.sqrt(365.0 / max(n, 1))) + + +# ============================================================================================ +# 0 β€” LA PREMESSA: rigenerare non perde la finestra SOLO se le barre chiuse non vengono riscritte +# ============================================================================================ +def sezione_0() -> dict: + print(H) + print(" 0 β€” PREMESSA: il feed riscrive le barre gia' CHIUSE?") + print(H) + print(""" + Perche' e' la premessa di tutto: la raccomandazione del filone MONITOR-AUDIT e' "riparare + `advance()` + RIGENERARE", cioe' ricalcolare la finestra forward dall'inception sui dati di + oggi. Se il feed riscrivesse le barre chiuse, quella rigenerazione produrrebbe una serie + DIVERSA da quella che si sarebbe registrata giorno per giorno -> la finestra andrebbe persa e + la conclusione cambierebbe. E' stata verificata due volte di traverso (via strategie); qui si + verifica una terza volta e in modo DIRETTO.""") + + out = {} + print("\n 0a. VERIFICA DIRETTA sul dato grezzo β€” vintage PRE-riscrittura contro oggi") + print(" `rebuild_history` copia il parquet in data/_feed_backup/*.prebuild.bak PRIMA di") + print(" sovrascriverlo: sul disco c'e' il file com'era ieri sera. Nessuna strategia in mezzo.") + tot_ch, tot_div = 0, 0 + for a in ("btc", "eth"): + for tf in ("1h", "5m"): + new_p, old_p = RAW / f"{a}_{tf}.parquet", BAK / f"{a}_{tf}.parquet.prebuild.bak" + if not (new_p.exists() and old_p.exists()): + print(f" {a}_{tf}: vintage assente -> NON MISURATO") + continue + new = pd.read_parquet(new_p) + old = pd.read_parquet(old_p) + m = old.merge(new, on="timestamp", suffixes=("_o", "_n")) + cols = ["open", "high", "low", "close", "volume"] + diff = np.zeros(len(m), bool) + for c in cols: + diff |= (m[f"{c}_o"].values != m[f"{c}_n"].values) + last_old = int(old["timestamp"].max()) + chiuse = m["timestamp"].values < last_old # nel vecchio file l'ultima riga era APERTA + nch, nd = int(chiuse.sum()), int((diff & chiuse).sum()) + tot_ch += nch + tot_div += nd + print(f" {a}_{tf}: {nch:,} barre gia' chiuse nel vintage -> DIVERSE OGGI: {nd}" + f" (righe cambiate in tutto: {int(diff.sum())})") + if diff.sum() == 1: + i = int(np.where(diff)[0][0]) + ts = pd.Timestamp(int(m["timestamp"].iloc[i]), unit="ms", tz="UTC") + print(f" l'unica riga cambiata e' {ts} = l'ULTIMA del vintage, che era " + f"aperta: close {m['close_o'].iloc[i]:g} -> {m['close_n'].iloc[i]:g}, " + f"volume {m['volume_o'].iloc[i]:,.0f} -> {m['volume_n'].iloc[i]:,.0f}") + out["chiuse"], out["div"] = tot_ch, tot_div + print(f"\n TOTALE: {tot_div} barre chiuse cambiate su {tot_ch:,} " + f"-> la riscrittura notturna e' un NO-OP sulle barre chiuse.") + print(" ⚠ questo e' UN ciclo di riscrittura (ieri->oggi). La stessa riga mostra anche il") + print(" DIFETTO: l'ultima riga del vintage era una barra parziale, ed e' esattamente") + print(" quella che i monitor consumano.") + + print("\n 0b. VERIFICA su 62 CICLI β€” tasso di divergenza per ETA' della barra (paper_prevday)") + print(" Statistica diversa da quella del 22/08 (che contava le identiche): se il feed") + print(" riscrivesse le barre chiuse, il tasso CRESCEREBBE con l'eta' (piu' riscritture") + print(" subite). Se il difetto e' solo la barra parziale al momento della registrazione,") + print(" il tasso e' PIATTO nell'eta' e vale ~1/24.") + 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] + T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True)) + A = np.array([p[1] for p in pairs]) + B = np.array([p[2] for p in pairs]) + dv = np.abs(A - B) > 1e-6 + now = pd.Timestamp.now(tz="UTC") + age = (now - T).days + d = pd.DataFrame(dict(age=age, div=dv)) + d["b"] = pd.cut(d.age, [-1, 7, 14, 21, 30, 45, 60, 999], + labels=["0-7g", "8-14g", "15-21g", "22-30g", "31-45g", "46-60g", ">60g"]) + g = d.groupby("b", observed=True).agg(n=("div", "size"), div=("div", "sum")) + g["tasso"] = g["div"] / g["n"] + print(f" {len(A)} barre orarie appaiate, {int(dv.sum())} divergenti " + f"({dv.mean() * 100:.2f}%; identiche {len(A) - int(dv.sum())}/{len(A)})") + print(" " + " ".join(f"{ix}:{r.tasso * 100:4.1f}%" for ix, r in g.iterrows())) + hh = pd.Series(T.hour[dv]).value_counts().sort_index() + print(" ora UTC delle divergenti: " + ", ".join(f"{int(h):02d}:00 x{int(n)}" for h, n in hh.items()) + + " <- il cron gira alle 00:30") + out["prevday_rate_flat"] = (float(g["tasso"].min()), float(g["tasso"].max())) + + print("\n 0c. LIMITE DICHIARATO β€” cosa NON e' verificato direttamente") + print(" Il vintage prebuild esiste solo per BTC/ETH (lo scrive `rebuild_history`).") + print(" `hl_*_1d.parquet` (che alimenta XSR01) e `dvol_*.parquet` NON hanno vintage su") + print(" disco: per loro l'evidenza resta indiretta (le barre che i monitor hanno letto") + print(" gia' chiuse coincidono al bit dopo settimane di riscritture β€” sezione 1).") + print("\n -> PREMESSA CONFERMATA sul feed BTC/ETH, per via diretta e su 62 cicli.") + print(" Rigenerare NON perde la finestra forward; nessuna data di gate si sposta.") + return out + + +# ============================================================================================ +# 1 β€” RICOSTRUZIONE DELLE TRE SERIE + PROVE D'IDENTITA' +# ============================================================================================ +def _ident_pannello(nome: str, salvato, calcolato) -> float: + d = float(np.max(np.abs(np.asarray(salvato, float) - np.asarray(calcolato, float)))) + print(f" I1 pannello == quello del cron di stanotte ({nome}): max|d| = {d:g}" + f" {'OK' if d == 0.0 else '*** DIVERSO ***'}") + return d + + +def ric_statarb() -> dict: + print("\n 1a. paper_statarb β€” gate STATARB 2026-09-27") + 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) + _ident_pannello("pos_modeled", [st["pos_modeled"]], [pos[-1]]) + i0 = int(np.where(ts == st["start_ts"])[0][0]) + + def replay(frame): + sink = Sink() + ps._append = sink + t2, _, p2, _ = ps._signal(frame) + k = int(np.where(t2 == st["start_ts"])[0][0]) + s0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, + pos_modeled=float(p2[k]), pos_real=float(p2[k]), + 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(s0, frame) + return sink.of("returns.jsonl") + + full = replay(j) + trunc = replay(j.iloc[:-1].reset_index(drop=True)) + a = np.array([float(r["net_modeled"]) for r in full[:-1]]) + b = np.array([float(r["net_modeled"]) for r in trunc]) + print(f" I3 troncare l'INPUT == affettare l'OUTPUT: n {len(a)}/{len(b)}, " + f"max|d| = {float(np.max(np.abs(a - b))):g} (l'ultima barra non retroagisce)") + return dict(mod=ps, rec=rec, rep=full, panel=j, extra=dict(pos=pos, ts=ts)) + + +def ric_xsr() -> dict: + print("\n 1b. paper_xsr β€” gate XSR01 2026-10-23") + 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() + _ident_pannello("w[modeled]", st["modeled"]["w"], W[-1]) + s0 = 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(s0) + full = sink.of("returns.jsonl") + + # I3: troncare l'INPUT (l'ultima riga di OGNI parquet HL) e ricostruire da capo + orig = px.load_hl + + def load_trunc(sym): + return orig(sym).iloc[:-1] + + px.load_hl = load_trunc + sink2 = Sink() + px._append = sink2 + try: + ts2, _, W2, _, _, syms2 = px.build_panel() + s2 = 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(s2) + tr = sink2.of("returns.jsonl") + a = np.array([float(r["net_modeled"]) for r in full[:-1]]) + b = np.array([float(r["net_modeled"]) for r in tr]) + dmax = float(np.max(np.abs(a - b))) if len(a) == len(b) else float("nan") + print(f" I3 troncare l'INPUT == affettare l'OUTPUT: n {len(a)}/{len(b)}, max|d| = {dmax:g}") + finally: + px.load_hl = orig + return dict(mod=px, rec=rec, rep=full) + + +def ric_dvolspread() -> dict: + print("\n 1c. paper_dvolspread β€” kill 2026-10-24 / decisione 2027-01-24") + 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() + _ident_pannello("wb_modeled", [st["wb_modeled"]], [P["wb"][-1]]) + i0 = int(np.where(P["ts"] == st["start_ts"])[0][0]) + + def replay(Q): + sink = Sink() + pv._append = sink + k = int(np.where(Q["ts"] == st["start_ts"])[0][0]) + s0 = 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(Q["wb"][k]), wb_real=float(Q["wb"][k]), + 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) + out = pv.advance(s0, Q) + return sink.of("returns.jsonl"), out + + full, st_full = replay(P) + Q = {k: (v[:-1] if hasattr(v, "__len__") else v) for k, v in P.items()} + trunc, st_tr = replay(Q) + a = np.array([float(r["net_modeled"]) for r in full[:-1]]) + b = np.array([float(r["net_modeled"]) for r in trunc]) + print(f" I3 troncare l'INPUT == affettare l'OUTPUT: n {len(a)}/{len(b)}, " + f"max|d| = {float(np.max(np.abs(a - b))):g}") + return dict(mod=pv, rec=rec, rep=full, st_rep=st_tr, st_rec=st) + + +def sezione_1() -> dict: + print("\n" + H) + print(" 1 β€” RICOSTRUZIONE + PROVE D'IDENTITA' (senza queste, il numero e' un'altra strategia)") + print(H) + print(""" + Tre prove, ognuna chiude un modo diverso di sbagliare: + I1 il pannello che leggo ORA e' quello che il cron ha letto stanotte (i parquet hanno mtime + 00:30-00:35 e nulla li riscrive fino al prossimo giro): si verifica contro lo `state.json` + del monitor, che e' stato scritto DA LUI alle 00:35. + I2 sulle barre che il monitor ha letto GIA' CHIUSE la ricostruzione coincide AL BIT: e' la + parte non affetta dal difetto, ed e' l'unico posto dove i due numeri devono coincidere. + I3 troncare l'ULTIMA barra dell'INPUT da' lo stesso risultato che affettare l'output: nessuna + barra precedente dipende dall'ultima (se dipendesse, la 'riparazione' sarebbe un altro + segnale, non lo stesso).""") + S = dict(statarb=ric_statarb(), xsr=ric_xsr(), dvol=ric_dvolspread()) + + print("\n I2 β€” barre che il monitor ha letto CHIUSE, e coincidenza al bit:") + for k, lab in (("statarb", "paper_statarb"), ("xsr", "paper_xsr"), ("dvol", "paper_dvolspread")): + rec, rep = S[k]["rec"], S[k]["rep"] + d = {int(r["ts"]): float(r["net_modeled"]) for r in rep} + pr = [(int(r["ts"]), float(r["net_modeled"]), d[int(r["ts"])]) for r in rec if int(r["ts"]) in d] + A = np.array([p[1] for p in pr]) + B = np.array([p[2] for p in pr]) + T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pr], unit="ms", utc=True)) + eq = np.abs(A - B) <= 1e-6 + S[k]["pairs"] = (T, A, B) + gg = sorted({str(x.date()) for x in T[eq]}) + note = f" ({gg[0]} -> {gg[-1]})" if gg else "" + print(f" {lab:<18} {int(eq.sum())}/{len(A)} identiche{note}") + if k == "statarb": + print(" quelle 6 sono le barre recuperate dopo l'incidente del feed 09-15/07:") + print(" registrate il 16/07 e riprodotte oggi al bit dopo ~37 riscritture") + print(" notturne -> conferma indipendente della premessa della sezione 0.") + if k == "dvol": + print(" *** ZERO barre non affette: per questo monitor la prova I2 NON ESISTE.") + print(" Restano I1 (pannello identico, max|d|=0), I3 (troncamento innocuo) e il") + print(" fatto strutturale che il replay chiama `advance()` di produzione senza") + print(" una riga riscritta. Va detto: e' una prova piu' debole delle altre due.") + return S + + +# ============================================================================================ +# 2 β€” I TRE GATE, ALLA LETTERA +# ============================================================================================ +def _riga(lab, m, extra=""): + print(f" {lab:<22} n={m['n']:>3} Sharpe {m['sh']:+7.2f} maxDD {m['dd'] * 100:6.2f}% " + f"ritorno {m['tot'] * 100:+6.2f}% vol {m['sd'] * 100:5.2f}%{extra}") + + +def gate_dvolspread(S: dict) -> tuple[str, str]: + print("\n" + H) + print(" 2a β€” DVOLSPREAD (kill 2026-10-24, decisione 2027-01-24) β€” PER PRIMO: e' l'unico dei") + print(" tre che ha gia' un VETO D'INTEGRITA', e la domanda e' se il difetto lo fa scattare") + print(H) + pv, rec, rep = S["dvol"]["mod"], S["dvol"]["rec"], S["dvol"]["rep"] + A = np.array([float(r["net_modeled"]) for r in rec]) + B = np.array([float(r["net_modeled"]) for r in rep])[:-1] # RIPARATA = solo barre chiuse + mA, mB = mets(A), mets(B) + print(f"\n criterio kill (pre-registrato 26/07): Sharpe forward MODELED < {pv.KILL_SHARPE:+.2f}" + f" -> RITIRO") + _riga("serie ATTUALE", mA) + _riga("serie RIPARATA", mB) + vA = "KILL" if mA["sh"] < pv.KILL_SHARPE else "vivo" + vB = "KILL" if mB["sh"] < pv.KILL_SHARPE else "vivo" + print(f" -> criterio kill: serie attuale {vA} Β· serie riparata {vB} " + f"{'STESSO VERDETTO' if vA == vB else '*** VERDETTO RIBALTATO ***'}") + print(f" ⚠ ma a n={mA['n']} barre SE(Sharpe) ~ {se_sharpe(mA['n']):.2f}: nessuna delle due") + print(" e' una lettura di gate, e la data del kill e' fra " + f"{(date(2026, 10, 24) - TODAY).days} giorni.") + + print("\n IL VETO D'INTEGRITA' (`n_active + n_flat_signal >= 80% delle barre`):") + stA, stB = S["dvol"]["st_rec"], S["dvol"]["st_rep"] + fA = (stA["n_active"] + stA["n_flat_signal"]) / max(stA["n_bars"], 1) + fB = (stB["n_active"] + stB["n_flat_signal"]) / max(stB["n_bars"], 1) + print(f" serie ATTUALE : attive {stA['n_active']} Β· flat-da-segnale {stA['n_flat_signal']}" + f" Β· flat-SENZA-DATO {stA['n_flat_nodata']} -> valide {fA:.0%} " + f"(soglia {pv.MIN_ACTIVE_FRAC:.0%}) -> {'VETO' if fA < pv.MIN_ACTIVE_FRAC else 'PASSA'}") + print(f" serie RIPARATA: attive {stB['n_active']} Β· flat-da-segnale {stB['n_flat_signal']}" + f" Β· flat-SENZA-DATO {stB['n_flat_nodata']} -> valide {fB:.0%} -> " + f"{'VETO' if fB < pv.MIN_ACTIVE_FRAC else 'PASSA'}") + print(""" + πŸ“Œ IL RISULTATO E' SUL VETO, NON SUL MONITOR. Una serie che misura DUE MINUTI di mercato al + giorno passa al 100% un veto d'integrita' progettato apposta per non far decidere su dati + mancanti. Non e' un difetto di taratura: e' che il veto e' costruito sulla domanda giusta + per un altro guasto. Conta *se il DVOL c'era* (e c'era, 28/28) e non puo' vedere *quanto + dura la barra su cui c'era* β€” sono due assi ortogonali. La stessa soglia 0.80 applicata alla + quota di barre CHIUSE leggerebbe 0/28 = 0% e vieterebbe la decisione. + E' la stessa forma gia' pagata dal progetto tre volte: 'una riga presente non e' un dato + presente'. Qui la variante nuova e': *una barra presente non e' una GIORNATA presente*.""") + + print("\n criteri (b) marginale ADDS / (c) deflated-Sharpe / (d) weights_tilt_null: NON GIRATI,") + print(" e il motivo e' una misura, non pigrizia: si valutano sul campione ESTESO (in-sample") + print(f" attivo 1949 barre + forward). Oggi il forward pesa {mA['n']}/{1949 + mA['n']} = " + f"{100 * mA['n'] / (1949 + mA['n']):.1f}% del campione; alla decisione del 24/01/2027 ne") + print(" pesera' ~8%. -> il difetto NON puo' ribaltare (c): puo' ribaltare solo (a), che e'") + print(" calcolato sulla sola finestra forward ed e' interamente determinato da lui.") + return vA, vB + + +def gate_statarb(S: dict) -> tuple[str, str]: + print("\n" + H) + print(" 2b β€” STATARB (decisione 2026-09-27) β€” il gate su cui il difetto MORDE") + print(H) + rec, rep = S["statarb"]["rec"], S["statarb"]["rep"] + gate = importlib.import_module("r0724_statarb_deploy_gate") + A = np.array([float(r["net_modeled"]) for r in rec]) + Ar = np.array([float(r["net_real"]) for r in rec]) + B = np.array([float(r["net_modeled"]) for r in rep])[:-1] + Br = np.array([float(r["net_real"]) for r in rep])[:-1] + mA, mB = mets(A), mets(B) + hc = lambda m, r: abs(float((np.prod(1 + m) - np.prod(1 + r)) * 100)) + hA, hB = hc(A, Ar), hc(B, Br) + print(f"\n soglie pre-registrate 24/07: Sharpe >= {gate.SH_DEPLOY} Β· maxDD < {gate.DD_MAX:.0%}" + f" Β· fill-haircut < {gate.HAIRCUT_MAX_PP} pp") + _riga("serie ATTUALE", mA, f" haircut {hA:.3f} pp") + _riga("serie RIPARATA", mB, f" haircut {hB:.3f} pp") + + def verdetto(sh, dd, h): + if sh >= gate.SH_DEPLOY and dd < gate.DD_MAX and h < gate.HAIRCUT_MAX_PP: + return "CANDIDATO AL DEPLOY" + if sh >= 0: + return "ESTENSIONE (unica, al 2026-12-26)" + return "RITIRO" + vA, vB = verdetto(mA["sh"], mA["dd"], hA), verdetto(mB["sh"], mB["dd"], hB) + for lab, sh, dd, h, v in (("ATTUALE", mA["sh"], mA["dd"], hA, vA), + ("RIPARATA", mB["sh"], mB["dd"], hB, vB)): + print(f" {lab:<9} Sharpe>={gate.SH_DEPLOY}: {'SI' if sh >= gate.SH_DEPLOY else 'NO':<3}" + f" | maxDD<{gate.DD_MAX:.0%}: {'SI' if dd < gate.DD_MAX else 'NO':<3}" + f" | haircut<{gate.HAIRCUT_MAX_PP}pp: {'SI' if h < gate.HAIRCUT_MAX_PP else 'NO':<3}" + f" -> {v}") + print(f"\n -> DUE CRITERI SU TRE SI RIBALTANO. Sharpe {mA['sh']:+.2f} -> {mB['sh']:+.2f} " + f"(attraversa 0 e la soglia 0.5); maxDD {mA['dd']:.1%} -> {mB['dd']:.1%} (attraversa la") + print(" guardia del 10%). Il terzo (haircut) non si ribalta e non poteva: e' un") + print(" rapporto fra due libri che subiscono lo STESSO troncamento.") + + print("\n DIAGNOSTICA PRE-REGISTRATA IL 25/07 (statica sempre-short a pari vol-target):") + bA = gate._bench_static_short(rec) + bB = gate._bench_static_short(rep[:-1]) + if bA and bB: + print(f" benchmark sulla finestra ATTUALE : {bA[0]:+.2f} su {bA[1]} barre") + print(f" benchmark sulla finestra RIPARATA: {bB[0]:+.2f} su {bB[1]} barre") + print(f" STATARB - benchmark: attuale {mA['sh'] - bA[0]:+.2f} " + f"riparata {mB['sh'] - bB[0]:+.2f}") + print(""" + πŸ“Œ ASIMMETRIA CHE VALE PIU' DEI NUMERI: il benchmark NON e' affetto dal difetto β€” e' + ricalcolato dal pannello, non letto dalla serie registrata (varia di %+.2f fra le due + finestre, contro %+.2f della strategia). Alla data del gate l'operatore metterebbe quindi + a confronto un benchmark GIUSTO con un numero di strategia SBAGLIATO, e la diagnostica + sembrerebbe piu' favorevole di quanto sia: +%.2f invece di +%.2f di margine. + Il segno del margine sopravvive comunque (STATARB batte la statica in entrambe le lenti): + cio' che il difetto ribalta e' il gate, non questa diagnostica.""" % + (bB[0] - bA[0], mB["sh"] - mA["sh"], mA["sh"] - bA[0], mB["sh"] - bB[0])) + else: + print(" benchmark NON calcolabile -> non girato.") + return vA, vB + + +def gate_xsr(S: dict) -> tuple[str, str]: + print("\n" + H) + print(" 2c β€” XSR01 (decisione 2026-10-23)") + print(H) + rec, rep = S["xsr"]["rec"], S["xsr"]["rep"] + gate = importlib.import_module("r0725_xsr_deploy_gate") + k5 = "net_REAL-$5000" + A = np.array([float(r["net_modeled"]) for r in rec]) + A5 = np.array([float(r[k5]) for r in rec]) + B = np.array([float(r["net_modeled"]) for r in rep])[:-1] + B5 = np.array([float(r[k5]) for r in rep])[:-1] + mA, mB = mets(A), mets(B) + + def haircut(m, r5): + tm = float(np.prod(1 + m) - 1) + t5 = float(np.prod(1 + r5) - 1) + return (tm - t5) / abs(tm) if tm != 0 else float("nan") + hA, hB = haircut(A, A5), haircut(B, B5) + print(f"\n soglie pre-registrate 25/07: Sharpe >= {gate.SH_DEPLOY} E haircut$5000 <= " + f"{gate.HAIRCUT_MAX:.0%} Β· ritiro sotto Sharpe {gate.SH_RETIRE}") + print(f" ⚠ se l'haircut sfonda -> RITIRO a prescindere dallo Sharpe (regola esplicita)") + _riga("serie ATTUALE", mA, f" haircut$5000 {hA * 100:+.1f}%") + _riga("serie RIPARATA", mB, f" haircut$5000 {hB * 100:+.1f}%") + + def verdetto(sh, h): + if np.isfinite(h) and h > gate.HAIRCUT_MAX: + return "RITIRO (haircut)" + if sh >= gate.SH_DEPLOY: + return "CANDIDATO SLEEVE" + if sh >= gate.SH_RETIRE: + return "ESTENSIONE (unica, al 2027-01-21)" + return "RITIRO" + vA, vB = verdetto(mA["sh"], hA), verdetto(mB["sh"], hB) + print(f" ATTUALE -> {vA}\n RIPARATA -> {vB} " + f"{'STESSO VERDETTO' if vA == vB else '*** VERDETTO RIBALTATO ***'}") + print(f"\n -> il verdetto NON si ribalta, ma per un caso: entrambe stanno sotto " + f"{gate.SH_RETIRE}. Il") + print(f" numero pero' cambia di {abs(mA['sh'] / mB['sh']):.1f}x ({mA['sh']:+.2f} contro " + f"{mB['sh']:+.2f}), e con SE ~ {se_sharpe(mA['n']):.1f} nessuno dei due decide niente.") + print(" L'haircut e' l'unico criterio robusto al difetto (rapporto fra libri troncati") + print(" allo stesso modo) ed e' lontanissimo dalla guardia in entrambe le lenti.") + return vA, vB + + +# ============================================================================================ +# 3 β€” IL MECCANISMO: perche' il troncamento non e' rumore +# ============================================================================================ +def sezione_3(S: dict) -> None: + print("\n" + H) + print(" 3 β€” PERCHE' IL DIFETTO NON E' RUMORE: comprime la VOL piu' della MEDIA") + print(H) + print(""" + Un troncamento a k minuti su 1440 scala la media del rendimento circa come k e la deviazione + standard come sqrt(k) (random walk). Quindi il rapporto media/sd β€” cioe' lo Sharpe β€” viene + MOLTIPLICATO per ~sqrt(k/1440)... in valore assoluto no: il numeratore e' la media del pezzo + troncato, che non ha lo stesso SEGNO della media della giornata intera. Risultato: la serie + registrata produce letture di Sharpe piu' ESTREME e in una direzione che non e' quella vera. + Si vede in tabella: la vol registrata e' una frazione di quella vera, ma il |Sharpe| non e' piu' + piccolo β€” in due casi su tre e' molto piu' GRANDE.""") + print(f"\n {'monitor':<20} {'vol REG':>9} {'vol RIP':>9} {'min/giorno':>11} " + f"{'Sh REG':>8} {'Sh RIP':>8} nota") + for k, lab in (("statarb", "paper_statarb"), ("xsr", "paper_xsr"), ("dvol", "paper_dvolspread")): + T, A, B = S[k]["pairs"] + Bc = B[:-1] if len(B) == len(A) else B + # ⚠ l'INTERA finestra dell'incidente del feed (09-16/07) va esclusa dalla stima dei + # minuti/giorno, non le sole barre risultate identiche: durante il blocco il monitor non + # trovava barre nuove e al ripristino ne ha recuperate 7 in un colpo, alcune solo + # PARZIALMENTE riallineate. Escludendo le sole identiche la stima esce 4x piu' alta + # (16,7 invece di 3,9 min/giorno su statarb) β€” e' la stessa maschera del 22/08. + keep = ~((T >= FREEZE[0]) & (T <= FREEZE[1])) + mA, mB = mets(A), mets(Bc) + kA, kB = mets(A[keep]), mets(B[:len(A)][keep]) + minuti = (kA["sd"] / kB["sd"]) ** 2 * 1440.0 + nota = ("SEGNO OPPOSTO" if np.sign(mA["sh"]) != np.sign(mB["sh"]) + else f"|Sh| x{abs(mA['sh']) / abs(mB['sh']):.1f}") + print(f" {lab:<20} {mA['sd'] * 100:8.2f}% {mB['sd'] * 100:8.2f}% {minuti:10.1f} " + f"{mA['sh']:+8.2f} {mB['sh']:+8.2f} {nota}" + + (f" ({int((~keep).sum())} barre dell'incidente escluse)" if (~keep).any() else "")) + print(""" + Il caso peggiore NON e' quello col |Sharpe| piu' gonfiato: e' `paper_statarb`, dove il + |Sharpe| e' quasi identico (1,96 contro 2,02) e cambia il SEGNO. Un controllo che cercasse + "numeri troppo belli" lo lascerebbe passare senza una parola: e' proprio il monitor che + alimenta il gate piu' vicino. + -> la firma da cercare non e' un |Sharpe| alto ma una VOL FORWARD molto sotto quella del + backtest: 7,9% contro ~31%, 0,5% contro ~2,8%, 0,8% contro ~19%. + ⚠ i "min/giorno" sono una LETTURA del rapporto di varianza (assume random walk + sull'intervallo troncato), non un cronometro, e sono sensibili alla maschera: su statarb + 3,9 escludendo l'incidente del feed, 16,7 escludendo le sole barre identiche, 92 + includendo tutto. Si cita la prima, che e' quella del 22/08; l'ordine di grandezza e' + quello, la cifra no. + βœ… Replica indipendente del filone MONITOR-AUDIT: 3,9 / 41,2 / 2,5 contro i 4 / 41 / 2 + pubblicati stamattina, per un percorso scritto separatamente.""") + + print("\n `implausible_sharpe` puntato sulle serie forward (mai fatto β€” nota del 22/08).") + print(" ⚠ il gate chiede n>=30: due serie forward su tre NON sono giudicabili, e questo") + print(" e' esso stesso un risultato β€” il gate esiste dal 26/07 ma non puo' sorvegliare") + print(" una finestra forward giovane, che e' esattamente quando serve.") + import altlib as al + for k, lab in (("statarb", "paper_statarb"), ("xsr", "paper_xsr"), ("dvol", "paper_dvolspread")): + T, A, B = S[k]["pairs"] + for nome, v in (("REGISTRATA", A), ("RIPARATA", B[:-1] if len(B) == len(A) else B)): + idx = T[:len(v)] + try: + r = al.implausible_sharpe(pd.Series(v, index=idx)) + corto = any("troppo corto" in x for x in r["reasons"]) + flag = ("SEGNALA" if r["implausible"] + else "n/giudic." if corto else "ok") + print(f" {lab:<18} {nome:<11} {flag:<10} {'; '.join(r['reasons'])[:66]}") + except Exception as e: # noqa: BLE001 + print(f" {lab:<18} {nome:<11} non girato ({type(e).__name__})") + + +# ============================================================================================ +# 4 β€” QUANTO COSTA ASPETTARE +# ============================================================================================ +GATES = ( + ("STATARB", "statarb", date(2026, 9, 27)), + ("XSR01", "xsr", date(2026, 10, 23)), + ("DVOLSPREAD", "dvol", date(2026, 10, 24)), +) + + +def sezione_4(S: dict) -> None: + print("\n" + H) + print(" 4 β€” QUANTO COSTA ASPETTARE (l'unico argomento quantitativo su QUANDO riparare)") + print(H) + print(""" + Due scenari, e la differenza fra loro e' tutto il contenuto della sezione: + RIPARA+RIGENERA : si corregge `advance()` e si RICALCOLA la finestra dall'inception. + Possibile perche' le barre chiuse non vengono riscritte (sezione 0). + RIPARA IN AVANTI : si corregge `advance()` e basta. Le barre gia' scritte restano sbagliate.""") + print(f"\n {'gate':<12} {'data':<12} {'barre al gate':>14} {'oggi':>6} " + f"{'corrette se si ripara OGGI':>28} {'se si ripara alla vigilia':>27}") + for nome, key, gd in GATES: + rec = S[key]["rec"] + first = pd.Timestamp(rec[0]["dt"][:10]).date() + n_gate = (gd - first).days + 1 + n_oggi = len(rec) + print(f" {nome:<12} {str(gd):<12} {n_gate:>14} {n_oggi:>6} " + f"{f'{n_gate - n_oggi} ({(n_gate - n_oggi) / n_gate:.0%})':>28} " + f"{f'1 ({1 / n_gate:.0%})':>27}") + print("\n Con RIGENERAZIONE la colonna giusta e' un'altra: 100% a QUALUNQUE data di") + print(" riparazione. Il costo dell'attesa e' ZERO barre β€” cio' che si paga aspettando non") + print(" e' dato perso ma il RISCHIO che la riparazione non arrivi prima del gate.") + print(" Senza rigenerazione, invece, riparare oggi lascia comunque sbagliata la maggioranza") + print(" della finestra di STATARB, e riparare alla vigilia non serve a niente.") + + print("\n SENSIBILITA' β€” quanto deve essere contaminata la finestra perche' il verdetto") + print(" cambi. Misurata sui dati di OGGI, non estrapolata: si costruisce la serie mista") + print(" `registrate[:k] + riparate[k:]` (= riparazione in avanti dal giorno k) e si legge") + print(" il criterio. k=0 e' la rigenerazione completa, k=n e' la serie di oggi.") + gate = importlib.import_module("r0724_statarb_deploy_gate") + rec, rep = S["statarb"]["rec"], S["statarb"]["rep"] + A = np.array([float(r["net_modeled"]) for r in rec])[:-1] + B = np.array([float(r["net_modeled"]) for r in rep])[:-1] + n = len(A) + print(f"\n STATARB β€” {n} barre chiuse; soglia deploy {gate.SH_DEPLOY}, ritiro sotto 0, " + f"guardia maxDD {gate.DD_MAX:.0%}") + print(f" {'corrette (coda)':>16} {'contaminate':>12} {'Sharpe':>8} {'maxDD':>8} verdetto") + verd = [] + for k in range(0, n + 1): + m = mets(np.concatenate([A[:k], B[k:]])) + v = ("CANDIDATO" if (m["sh"] >= gate.SH_DEPLOY and m["dd"] < gate.DD_MAX) + else "ESTENSIONE" if m["sh"] >= 0 else "RITIRO") + verd.append((k, m, v)) + # minimo numero di barre CORRETTE in coda che basta a leggere gia' il verdetto riparato + v_rip = verd[0][2] + min_ok = min((n - k for k, m, v in verd if v == v_rip), default=n) + for k, m, v in verd: + if k in (0, n // 4, n // 2, (3 * n) // 4, n - min_ok, n - min_ok + 1, n): + print(f" {n - k:>16} {f'{k} ({k / n:.0%})':>12} {m['sh']:+8.2f} " + f"{m['dd'] * 100:7.2f}% {v}") + n_res = 90 - len(S["statarb"]["rec"]) # barre che restano da qui al gate del 27/09 + print(f""" + πŸ“Œ LA RIPARAZIONE PARZIALE NON E' PROPORZIONALE. Su questo campione bastano + **{min_ok} barre corrette in coda su {n}** ({min_ok / n:.0%}) perche' il criterio legga gia' il + verdetto della serie riparata. Il meccanismo e' quello della sezione 3: una barra troncata a + ~4 minuti su 1440 porta ~1/370 della varianza E ~1/370 del rendimento di una barra vera + (entrambi scalano con la frazione di giornata), quindi pesa quasi nulla sia nella media sia + nella deviazione della miscela. La maggioranza NUMERICA delle barre non comanda. + Detto meglio, ed e' la formulazione onesta: riparare in avanti senza rigenerare non + CONTAMINA la finestra, la ACCORCIA β€” il criterio finisce per leggere la strategia sulle sole + barre corrette. Riparando oggi, al 27/09 STATARB avrebbe {n_res} barre vere su 90: una + finestra piu' corta ma VERA, con SE(Sharpe) ~{se_sharpe(n_res):.1f} invece di ~{se_sharpe(90):.1f}. + Riparare alla vigilia (1 barra) no: li' il numero resta quello di oggi. + ⚠ Il {min_ok}/{n} NON e' una costante β€” dipende da quali barre capitano in coda, e cambia col + campione: si legge come "poche", non come una soglia. E rigenerare resta strettamente + migliore, per due cose che l'accorciamento non da': un maxDD leggibile (la miscela ha un + percorso di equity che non e' mai esistito) e una finestra rileggibile con altri criteri.""") + + +# ============================================================================================ +def main() -> None: + print(H) + print(" GATE-RECON β€” cosa direbbero i tre gate pre-registrati con le serie riparate") + print(f" {TODAY} SOLA LETTURA su produzione (nessun file di data/ aperto in scrittura)") + print(H) + sezione_0() + S = sezione_1() + v_dv = gate_dvolspread(S) + v_st = gate_statarb(S) + v_xs = gate_xsr(S) + sezione_3(S) + sezione_4(S) + + print("\n" + H) + print(" RIEPILOGO β€” il DELTA, non la decisione (i gate hanno una data e non si anticipano)") + print(H) + print(f" STATARB 27/09 β€” serie riparata: {v_st[1]:<36} Β· serie attuale: {v_st[0]}") + print(f" XSR01 23/10 β€” serie riparata: {v_xs[1]:<36} Β· serie attuale: {v_xs[0]}") + print(f" DVOLSPREAD 24/10 β€” serie riparata: {v_dv[1]:<36} Β· serie attuale: {v_dv[0]}") + print("\n Nessuno di questi e' il verdetto del gate: sono i criteri letti OGGI, su una finestra") + print(" ancora incompleta. Servono a una sola decisione, che e' dell'operatore: riparare") + print(" `advance()` e RIGENERARE prima del 27/09, oppure sapere che quel giorno il criterio") + print(" verra' letto su una serie che misura 4 minuti di mercato al giorno.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822c_skh_anchor_prop.py b/scripts/research/r0822c_skh_anchor_prop.py new file mode 100644 index 0000000..c49752a --- /dev/null +++ b/scripts/research/r0822c_skh_anchor_prop.py @@ -0,0 +1,604 @@ +"""r0822c_skh_anchor_prop β€” LA GAMBA (b) DEL `GATE PROP-01`, GIRATA DAVVERO (2026-08-22, ondata 3). + +COSA CHIUDE +----------- +`r0822b_prop_recal` (RESULTS-0822 Β§29) ha registrato un gate a tre gambe e ne ha DATATA una al +**2026-10-31**: + + (b) BANDA D'ANCORA DI SKH01 SOTTO LA LENTE PROP β€” MISURA. Data: 2026-10-31. + Costo: rifare `pc._hourly_legs` ai 23 offset della griglia 230m/690m e rigirare Β§3. + Metrica: MEDIANA DELLE DIFFERENZE APPAIATE J(peso nella regione w_XS 25-38%) - + J(libro LIVE 75/25) sui 23 offset, finestra W-LONG. PASS se > +0.05. + Surrogato di oggi (taglio del drift SKH x0.50 / x0.33): +0.124 e +0.113. + Mi aspetto un PASS e lo dichiaro prima. + +E' l'unica distorsione che il progetto ha dichiarato DUE VOLTE (PROP-ALLOC Β§1 e PROP-RECAL Β§29) +e mai rimossa: *l'ottimo gira sull'ancora canonica e si appoggia a SKH01, che il progetto misura +come lo sleeve con la fortuna d'ancora piu' grande da restituire*. + +⚠️ **LA DATA ERA UN BUDGET, E IL BUDGET ERA ANCORATO ALLA COMPUTAZIONE SBAGLIATA.** +Il costo temuto ("~9 min per asset x offset") e' quello di `r0726_skh_sigcache`, che ricostruisce +il segnale intra-bin a OGNI osservazione oraria dentro ogni bin 230m (~82.000 righe per cella). +Questa misura non ne ha bisogno: serve `pc._hourly_legs` all'offset, cioe' resample + entries + +MTM per-trade, **misurato 0,6 s per offset** (entrambi gli asset). I 23 offset costano ~15 s. +La gamba (b) non aspettava un dato ne' una macchina: aspettava che qualcuno misurasse il costo. + +IL PROBLEMA VERO: LE ANCORE SONO DUE +------------------------------------ +SKH01 ha 23 offset di griglia (230m/690m, uno ogni 30m su [0,690)); XS01 ha 10 fasi del ciclo +H=10. PROP-RECAL ha girato la banda di FASE (a offset SKH canonico); PROP-ALLOC nessuna delle due. +La lezione registrata di `r0726_loo_deluck` e' che **la somma delle fortune marginali NON e' la +mediana congiunta** (sbagliava di +0,82 di Sharpe hold-out). + +**SCELTA DICHIARATA: griglia congiunta PIENA 23 x 10 = 230 configurazioni**, non un campionamento +e non una somma di marginali. E' possibile perche' il costo e' stato MISURATO prima di scegliere +(~0,06 s per coppia config-leva): con 23 offset x 10 fasi x 8 leve la spesa e' ~110 s. +**Perche' questa scelta NON e' quella che mi conviene:** la scelta comoda sarebbe fissare la fase +di XS01 a 0, che PROP-RECAL misura fortunata di +0,019 di J; il collasso per MEDIANA sulle fasi la +restituisce. Entrambe sono riportate, cosi' si vede da che parte taglia. + +CONVENZIONI (tutte ereditate, nessuna nuova) +-------------------------------------------- +* Statistica = **MEDIANA DELLE DIFFERENZE APPAIATE**, mai differenza delle mediane (26/07: con + quella correzione un verdetto si ribalto'). Ogni offset e' valutato con ENTRAMBE le + configurazioni (candidato e LIVE) alla STESSA ancora e sugli STESSI indici di bootstrap. +* Lente **ACCOPPIATA** ovunque (su una regola di DD a UN GIORNO la close-only e' cieca). +* Pesi FISSI, mai ri-ottimizzati per offset: ri-cercare l'argmax a ogni offset sarebbe un + max-of-23, cioe' esattamente il difetto sotto audit. La leva si', ma su ENTRAMBI i lati. +* de-luck sul drift x0,89 tenuto **per comparabilita' con PROP-RECAL**. ⚠️ Su questa dimensione + e' un DOPPIO CONTEGGIO conservativo: lo 0,89 fu misurato (26/07) proprio come rapporto + mediana-ancore / ancora-canonica del drift. Β§6 rigira la misura a fattore 1,00. +* Universo XS01 = **U13 Binance** (la versione piu' DEBOLE, conservativa), finestra **W-LONG + 2021+**, meccanismo congelato. Identici a PROP-RECAL. + +COSA QUESTA MISURA NON TOGLIE (dichiarato, non nascosto) +-------------------------------------------------------- +L'ancora giornaliera di **TP01** (24 ore possibili) resta canonica 00:00 UTC, che il progetto +misura come la migliore delle 24 per l'hold-out di TP01. E' una TERZA dimensione, fuori dal +perimetro della gamba (b), e la sua fortuna resta dentro questi numeri. + +ESITO (2026-08-22, corsa completa 391 s, 5.127 celle dichiarate) +---------------------------------------------------------------- +* RIPRODUZIONE all'ancora canonica **7/7 esatta** (J LIVE 0,281 Β· fase 0 0,472 Β· mediana 0,453 Β· + banda [0,431 0,488] Β· delta +0,175 Β· 10/10 fasi) e **3 controlli bit-exact a 0,0** (gambe orarie + a off=0 contro `pc._hourly_legs()` su 4 colonne; costruttore di libro contro `PR.rg` a due pesi). +* **GAMBA (b): PASS.** Mediana delle differenze appaiate sui 23 offset = **+0,1162** contro la + soglia pre-registrata +0,05; banda p10-p90 **[+0,087, +0,152]**; **23/23 offset positivi**, + **230/230 celle** dello spazio congiunto positive. La regione tiene tutta: 38/38/25 +0,116 Β· + 38/25/38 +0,120 Β· 25/38/38 +0,109. +* **La fortuna d'ancora c'e' ed e' quella attesa, ma non e' dove si temeva.** L'offset canonico 0 + sta al **91Β° pctl** dei 23 su tutte e tre le grandezze e regala **+0,059 sul delta**, +0,112 sul + livello J del candidato, +0,050 su quello del LIVE. Il delta e' MENO ancorato dei livelli perche' + il LIVE contiene SKH01 al 25% e la fortuna si cancella in parte nella differenza appaiata. +* πŸ“Œ **IL RISULTATO INTERPRETATIVO: il vantaggio NON e' un tilt su SKH01.** A `38/25/38` SKH01 sta + al **medesimo peso del LIVE (25%)**, quindi la sua ancora si cancella quasi esattamente β€” e li' + il delta e' il **piu' alto dei tre (+0,120)** con la banda **piu' stretta** (ampiezza 0,032 + contro 0,065 di W_A e 0,057 di W_C, che portano SKH a 38%). **L'ampiezza della banda scala col + DIFFERENZIALE di peso su SKH01**, esattamente come previsto prima di misurare. Cio' che paga e' + XS01, non un sovrappeso dello sleeve fragile. +* ⚠️ **Il numero OPERATIVO invece si dimezza, e per una ragione diversa dal gate.** P(>=50 EUR/g) + da EUR 600 in 36 mesi: mediana della banda d'ancora **4,4% [p10-p90 2,5%-8,2%]** contro il + **7,8%** pubblicato; P(zero) **35,3%** contro 25,5%. All'ancora canonica la macchina ridice + **7,7%** (= replica di PROP-RECAL): il crollo e' la fortuna d'ancora, non un disaccordo. + **Il gate e' su una DIFFERENZA (dove l'ancora si cancella in parte), il numero operativo e' un + LIVELLO (dove non si cancella affatto) β€” passano insieme e si muovono in modo diverso.** +* ⚠️ **Il surrogato del 22/08 e' finito vicino al vero per la CANCELLAZIONE DI DUE ASIMMETRIE.** + Ricalcolato a peso FISSO il taglio del drift da' **+0,076 (x0,50)** e **+0,045 (x0,33)**, cioe' + SOTTOSTIMA il vero (+0,116); ma PROP-RECAL lo calcolava ri-cercando l'argmax sui 10 pesi + migliori mentre il LIVE resta inchiodato a 75/25 β€” sotto stress il candidato puo' scappare da + SKH01 e il LIVE no -> il surrogato si gonfia a +0,124/+0,113. **Due errori in versi opposti che + quasi si annullano: il numero era giusto, il metodo no.** +* Sensibilita' dichiarata: a de-luck **x1,00** (senza il doppio conteggio dell'ancora) il delta + sale a **+0,148**, 23/23 positivi -> tenere lo 0,89 e' conservativo su questa dimensione. +* Banda del maxDD del candidato sui 23 offset: **-8,5% ... -13,0%**, mediana **-10,8%** (il + pubblicato a peso 25% era -9,6%): su un conto a barriera la coda peggiora quanto il livello. +* **RESTA FUORI, dichiarato:** l'ancora giornaliera di **TP01** (24 ore) e' una TERZA dimensione, + fuori dal perimetro della gamba (b), e la sua fortuna e' ancora dentro questi numeri. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822c_skh_anchor_prop.py` +Rete: NESSUNA (parquet certificati + cache Binance di r0822b_xs01_oos). +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) + +import r0702_anchor_skh01 as AN # noqa: E402 resample con fase spostata (02/07) +import r0724_goal50_intraday_mc as mc # noqa: E402 recon orario per-sleeve (24/07) +import r0725_prop_coupled as pc # noqa: E402 lente accoppiata (25/07) +import r0725_prop_ladder as pl # noqa: E402 regole firm + scala di conti +import r0822_prop_alloc as PA # noqa: E402 J, bootstrap, simulatore (22/08) +import r0822b_prop_recal as PR # noqa: E402 IL file di cui giro la gamba (b) +import r0822b_xs01_oos as X # noqa: E402 motore XS01 + cache Binance +from src.data.downloader import load_data # noqa: E402 +from src.strategies.skyhook import HTF_MIN, LTF_MIN # noqa: E402 + +ASSETS = ("BTC", "ETH") +OFFSETS = tuple(range(0, 690, 30)) # 23 offset a priori, identici a r0702 (0 = canonico) +PHASES = tuple(range(10)) # 10 fasi del ciclo H=10 di XS01 +PHASES_SUB = (0, 3, 7) # sottocampione a priori per la scala di conti +LEVS = PR.LEVS +N_PATHS = PA.N_PATHS +EV_H = FU_H = 365 +SEED_EV, SEED_FU = 8220, 8221 +HYRO = "HYRO" +GATE_B = 0.05 # soglia PRE-REGISTRATA da PROP-RECAL + +# pesi FISSI che coprono la regione robusta w_XS in [25%, 38%] (griglia simplesso a passo 1/8) +W_LIVE = {"TP01": 0.75, "SKH01": 0.25} +W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25} # centro dichiarato da PROP-RECAL (headline) +W_B = {"TP01": 0.38, "SKH01": 0.25, "XS01": 0.38} # w_XS all'estremo alto, TP-pesante +W_C = {"TP01": 0.25, "SKH01": 0.38, "XS01": 0.38} # w_XS all'estremo alto, SKH-pesante + +# numeri PUBBLICATI da riprodurre prima di produrne di nuovi (RESULTS-0822 Β§29) +PUB = dict(j_med=0.453, j_ph0=0.472, j_p10=0.431, j_p90=0.488, j_live=0.281, + dmed=0.175, npos=10, surr_50=0.124, surr_33=0.113, p50g=0.078) + +TRIALS = 0 + + +def bump(n: int = 1) -> None: + global TRIALS + TRIALS += n + + +def hr(t: str = "") -> None: + print("\n" + "-" * 104) + if t: + print(f" {t}") + print("-" * 104) + + +# ==================================================================== 1. GAMBE ORARIE ALL'OFFSET + +_LEGS: dict[int, pd.DataFrame] = {} + + +def hourly_legs_off(off: int) -> pd.DataFrame: + """`pc._hourly_legs()` con la griglia 230m/690m di SKH01 spostata di `off` minuti. + + NON riscrive la macchineria: rimpiazza temporaneamente `mc.build_frames` con il resample + a fase spostata di `r0702_anchor_skh01` e richiama `mc.skh_hourly_contrib` β€” cosi' la + logica trade-per-trade (entry a close del bin, exit AL LIVELLO, SL prioritario, fee + nell'ora d'uscita) resta letteralmente quella del 24/07. La fedelta' della sostituzione e' + provata dal controllo bit-exact a off=0 (Β§1). + + La gamba TP01 NON viene toccata: l'ancora di TP01 e' un'ALTRA dimensione (24 ore giornaliere) + e la gamba (b) del gate riguarda i 23 offset di SKH01. + """ + if off in _LEGS: + return _LEGS[off] + tp_r = tp_w = sk_r = sk_w = None + orig = mc.build_frames + + def patched(df5: pd.DataFrame, _off: int = off): + ltf = AN.resample_off(df5, LTF_MIN, _off) + htf = AN.resample_off(df5, HTF_MIN, _off) + # confini HTF sottoinsieme dei LTF (690 = 3 x 230): la stessa guardia di r0702. + assert np.isin(htf["timestamp"].values[1:], ltf["timestamp"].values).all(), \ + f"confini HTF non sottoinsieme dei LTF (off={_off})" + return ltf, htf + + mc.build_frames = patched + try: + for a in ASSETS: + df = load_data(a, "1h") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + close = df["close"].values.astype(float) + low = df["low"].values.astype(float) + high = df["high"].values.astype(float) + prev = np.concatenate(([np.nan], close[:-1])) + r_close = close / prev - 1.0 + tpf = mc.tp01_daily_frame(a) + day_of = idx.floor("D") + w_tp = np.nan_to_num(tpf["w"].reindex(day_of).values) + fee_tp = np.nan_to_num(tpf["fee"].reindex(day_of).values) + first = np.concatenate(([True], day_of[1:] != day_of[:-1])) + r_wick = np.where(w_tp >= 0, low / prev - 1.0, high / prev - 1.0) + t_r = pd.Series(w_tp * r_close - np.where(first, fee_tp, 0.0), index=idx) + t_w = pd.Series(w_tp * r_wick - np.where(first, fee_tp, 0.0), index=idx) + sa, sb = mc.skh_hourly_contrib(a, idx, close, low, high) + s_r = pd.Series(sa / mc.W_SKH, index=idx) + s_w = pd.Series(sb / mc.W_SKH, index=idx) + tp_r = t_r if tp_r is None else tp_r.add(t_r, fill_value=np.nan) + tp_w = t_w if tp_w is None else tp_w.add(t_w, fill_value=np.nan) + sk_r = s_r if sk_r is None else sk_r.add(s_r, fill_value=np.nan) + sk_w = s_w if sk_w is None else sk_w.add(s_w, fill_value=np.nan) + finally: + mc.build_frames = orig + J = pd.concat({"tp_r": tp_r, "tp_w": tp_w, "skh_r": sk_r, "skh_w": sk_w}, axis=1).dropna() + _LEGS[off] = J + return J + + +_CRY: dict = {} + + +def crypto_off(off: int, w_tp: float, w_skh: float) -> pd.DataFrame: + """`pc.crypto_daily_tuples` con le gambe orarie all'offset `off` (minimo ESATTO sul path + orario condiviso: si compone l'ora per ora e si guarda il minimo, wick incluso).""" + key = (off, round(w_tp, 6), round(w_skh, 6)) + if key in _CRY: + return _CRY[key] + J = hourly_legs_off(off) + r_h = (w_tp * J["tp_r"] + w_skh * J["skh_r"]).values + w_h = (w_tp * J["tp_w"] + w_skh * J["skh_w"]).values + idx = J.index + days = idx.floor("D") + rows = [] + for day, pos in pd.Series(np.arange(len(idx)), index=days).groupby(level=0): + sl = pos.values + r = r_h[sl] + wk = w_h[sl] + cum = np.cumprod(1.0 + r) + cum_prev = np.concatenate(([1.0], cum[:-1])) + m = min(cum.min(), (cum_prev * (1.0 + wk)).min()) - 1.0 + rows.append((day, cum[-1] - 1.0, min(m, cum[-1] - 1.0))) + out = pd.DataFrame(rows, columns=["day", "R", "m"]).set_index("day") + _CRY[key] = out + return out + + +def rg_off(w: dict, idx: pd.DatetimeIndex, xs: pd.DataFrame, off: int) -> tuple: + """`PR.rg` con il sub-book crypto all'offset `off`. Validato bit-exact a off=0 in Β§1.""" + w_tp, w_sk = float(w.get("TP01", 0.0)), float(w.get("SKH01", 0.0)) + w_xs = float(w.get("XS01", 0.0)) + R = np.zeros(len(idx)) + M = np.zeros(len(idx)) + if w_tp > 0 or w_sk > 0: + c = crypto_off(off, w_tp, w_sk).reindex(idx) + R += np.nan_to_num(c["R"].values) + M += np.nan_to_num(c["m"].values) + if w_xs > 0: + f = xs.reindex(idx) + R += w_xs * np.nan_to_num(f["R"].values) + M += w_xs * np.nan_to_num(f["m"].values) + return R, np.minimum(M, R) - R + + +def best_over_levers(R, G, ev, fu, factor: float = PA.DELUCK) -> tuple[float, float, np.ndarray]: + """(J, leva, vettore per-percorso) alla leva che massimizza J. La ricerca sulla leva e' + applicata SIMMETRICAMENTE al candidato e al LIVE (convenzione di PROP-RECAL).""" + jb, lb, vb = -1.0, None, None + for lv in LEVS: + o = PR.obj_rg(R, G, lv, ev, fu, factor=factor) + if o["J"] > jb: + jb, lb, vb = o["J"], lv, o["jvec"] + bump(len(LEVS)) + return jb, lb, vb + + +# ==================================================================== main +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + t0 = time.time() + + def tick(lab: str) -> None: + print(f" [{time.time()-t0:6.0f}s] {lab}") + + print("=" * 104) + print(" SKH-ANCHOR-PROP β€” la gamba (b) del `GATE PROP-01`, girata invece che aspettata") + print("=" * 104) + print(" DOMANDA: la mediana delle differenze appaiate J(regione w_XS 25-38%) - J(LIVE 75/25)") + print(f" sui 23 offset della griglia 230m/690m di SKH01, W-LONG, supera +{GATE_B:.2f}?") + print(" ASPETTATIVA REGISTRATA PRIMA DI MISURARE (mia, non ereditata): mi aspetto un PASS ma") + print(" con una taglia SENSIBILMENTE minore del surrogato (+0.124/+0.113), per un motivo") + print(" strutturale: il LIVE 75/25 contiene SKH01 al 25% e il candidato al 38%, quindi la") + print(" fortuna d'ancora si cancella in PARTE nella differenza appaiata β€” il residuo pesa") + print(" ~13 punti di peso, non 38. Se il delta uscisse >= al surrogato, il surrogato non era") + print(" conservativo e lo dico.") + + # ============================================ Β§0. RIPRODUZIONE + hr("Β§0. RIPRODUZIONE β€” la macchina deve dare i numeri di PROP-RECAL prima di darne di nuovi") + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, PR.XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13 = Ob.reindex(p13.index)[X.U13] + h13 = Hb.reindex(p13.index)[X.U13] + l13 = Lb.reindex(p13.index)[X.U13] + XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES} + bump(len(PHASES)) + cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index + base_idx = cry_idx.intersection(XSP[0].index) + first13 = XSP[0].index[np.flatnonzero(XSP[0]["R"].values != 0.0)[0]] + I_LONG = base_idx[base_idx >= max(first13, pd.Timestamp("2021-01-01", tz="UTC"))] + ev = PA.boot_idx(len(I_LONG), EV_H, N_PATHS, seed=SEED_EV) + fu = PA.boot_idx(len(I_LONG), FU_H, N_PATHS, seed=SEED_FU) + print(f" W-LONG: {len(I_LONG)} giorni {I_LONG[0].date()} -> {I_LONG[-1].date()} | " + f"de-luck x{PA.DELUCK} | {N_PATHS} percorsi | semi {SEED_EV}/{SEED_FU} | firm {HYRO}") + + RL0, GL0 = PR.rg(W_LIVE, I_LONG, XSP[0]) + jL0, lvL0, vL0 = best_over_levers(RL0, GL0, ev, fu) + js0, ds0 = [], [] + for ph in PHASES: + Rp, Gp = PR.rg(W_A, I_LONG, XSP[ph]) + jb, _, vb = best_over_levers(Rp, Gp, ev, fu) + js0.append(jb) + ds0.append(float((vb - vL0).mean())) + print(f" {'grandezza':>42} {'mio':>9} {'PROP-RECAL':>11} esito") + checks = [("J libro LIVE 75/25 (ancora canonica)", jL0, PUB["j_live"]), + ("J candidato 38/38/25 fase 0", js0[0], PUB["j_ph0"]), + ("J candidato MEDIANA su 10 fasi", float(np.median(js0)), PUB["j_med"]), + ("banda di fase p10", float(np.percentile(js0, 10)), PUB["j_p10"]), + ("banda di fase p90", float(np.percentile(js0, 90)), PUB["j_p90"]), + ("delta appaiato MEDIANO (10 fasi)", float(np.median(ds0)), PUB["dmed"])] + ok = True + for lab, mine, pub in checks: + good = abs(mine - pub) <= 0.005 + ok &= good + print(f" {lab:>42} {mine:>9.3f} {pub:>11.3f} {'OK' if good else '<-- DIVERGE'}") + npos0 = sum(1 for d in ds0 if d > 0) + print(f" {'fasi con delta > 0':>42} {npos0:>9d} {PUB['npos']:>11d} " + f"{'OK' if npos0 == PUB['npos'] else '<-- DIVERGE'}") + ok &= npos0 == PUB["npos"] + print(f"\n >>> RIPRODUZIONE ALL'ANCORA CANONICA: " + f"{'OK β€” si procede' if ok else 'FALLITA β€” questo E il risultato'}") + if not ok: + print(" La macchina non riproduce PROP-RECAL: qualunque numero sugli offset sarebbe di") + print(" un'altra macchina. Mi fermo qui, come impone la disciplina di riproduzione.") + return + tick("Β§0 riproduzione") + + # ============================================ Β§1. LA MACCHINERIA ALL'OFFSET + hr("Β§1. LA MACCHINERIA ALL'OFFSET β€” e il controllo che prova che la sostituzione e' fedele") + t = time.time() + J0 = hourly_legs_off(0) + (Jref,) = pc._hourly_legs() + same_idx = J0.index.equals(Jref.index) + dmax = max(float(np.abs(J0[c].values - Jref[c].values).max()) for c in J0.columns) + print(f" gambe orarie a off=0 contro `pc._hourly_legs()`: n={len(J0)} indice identico=" + f"{same_idx} max|dif| su 4 colonne = {dmax:.2e} -> " + f"{'BIT-EXACT' if (same_idx and dmax == 0.0) else 'DIVERGE β€” STOP'}") + if not (same_idx and dmax == 0.0): + print(" La sostituzione di `build_frames` non riproduce il canonico: mi fermo.") + return + for wt in (W_A, W_LIVE): + Ra, Ga = rg_off(wt, I_LONG, XSP[0], 0) + Rb, Gb = PR.rg(wt, I_LONG, XSP[0]) + d1 = float(np.abs(Ra - Rb).max()) + d2 = float(np.abs(Ga - Gb).max()) + print(f" costruttore di libro a off=0 contro `PR.rg` ({PA.wkey(wt)}): " + f"max|dR| {d1:.2e} max|dgap| {d2:.2e} -> " + f"{'BIT-EXACT' if max(d1, d2) == 0.0 else 'DIVERGE'}") + if max(d1, d2) != 0.0: + return + print(f" costo misurato: {time.time()-t:.1f}s per la prima cella (import+cache inclusi)") + t = time.time() + for off in OFFSETS: + hourly_legs_off(off) + assert hourly_legs_off(off).index.equals(J0.index), \ + f"indice orario diverso a off={off}: il bootstrap non sarebbe appaiato" + print(f" 23 offset costruiti in {time.time()-t:.0f}s β€” l'indice giornaliero e' lo STESSO a") + print(" ogni offset, quindi gli indici di bootstrap sono condivisi e il confronto e' appaiato.") + print(" ⚠️ La data 2026-10-31 era un budget di calcolo ancorato al costo di `skh_sigcache`") + print(" (segnale intra-bin, ~9 min/cella), che questa misura non usa.") + n_tr = {} + for off in (0, 330, 660): + J = hourly_legs_off(off) + n_tr[off] = int((J["skh_r"].values != 0.0).sum()) + print(f" ore con contributo SKH01 non nullo: " + + " Β· ".join(f"off{o}: {v}" for o, v in n_tr.items()) + + " (griglie diverse, non una traslazione)") + tick("Β§1 macchineria") + + # ============================================ Β§2. LA GRIGLIA CONGIUNTA + hr("Β§2. LA GRIGLIA CONGIUNTA 23 OFFSET x 10 FASI β€” la gamba (b), headline W_A = 38/38/25") + print(" Per ogni offset: J del LIVE (che contiene SKH01 al 25% e quindi si muove anch'esso)") + print(" e J del candidato a ciascuna delle 10 fasi di XS01, alla leva che massimizza J.") + print(" Il delta e' appaiato sui MEDESIMI indici di bootstrap; il collasso sulle fasi e' la") + print(" MEDIANA (la stima onesta di PROP-RECAL), non la fase 0 (fortunata di +0.019).") + D = np.zeros((len(OFFSETS), len(PHASES))) + JC = np.zeros((len(OFFSETS), len(PHASES))) + JL = np.zeros(len(OFFSETS)) + LVL = np.zeros(len(OFFSETS)) + DDC = np.zeros((len(OFFSETS), len(PHASES))) + DDL = np.zeros(len(OFFSETS)) + t = time.time() + print(f"\n {'off':>4} {'J LIVE':>7} {'levL':>5} {'J cand fase0':>12} {'MEDIANA':>8} " + f"{'p10-p90':>15} {'delta MEDIANO':>13} {'fasi>0':>7} {'maxDD cand':>10}") + for i, off in enumerate(OFFSETS): + RL, GL = rg_off(W_LIVE, I_LONG, XSP[0], off) + jL, lvL, vL = best_over_levers(RL, GL, ev, fu) + JL[i], LVL[i] = jL, lvL + DDL[i] = PR.maxdd(PA.deluck(RL)) + for k, ph in enumerate(PHASES): + Rc, Gc = rg_off(W_A, I_LONG, XSP[ph], off) + jc, _, vc = best_over_levers(Rc, Gc, ev, fu) + JC[i, k] = jc + DDC[i, k] = PR.maxdd(PA.deluck(Rc)) + D[i, k] = float((vc - vL).mean()) + print(f" {off:>4} {jL:>7.3f} {lvL:>5.2f} {JC[i,0]:>12.3f} {np.median(JC[i]):>8.3f} " + f"[{np.percentile(JC[i],10):>6.3f},{np.percentile(JC[i],90):>6.3f}] " + f"{np.median(D[i]):>+13.3f} {int((D[i] > 0).sum()):>5}/10 " + f"{np.median(DDC[i])*100:>9.1f}%") + print(f" ({time.time()-t:.0f}s per 23 x (1 LIVE + 10 fasi) x 8 leve)") + + d_per_off = np.median(D, axis=1) # collasso onesto sulle fasi + d_gate = float(np.median(d_per_off)) # <-- IL NUMERO DELLA GAMBA (b) + d_ph0 = float(np.median(D[:, 0])) # la scelta comoda, per confronto + print(f"\n {'':>52}{'mediana':>9}{'p10':>9}{'p90':>9}{'>0':>8}") + print(f" {'delta appaiato per OFFSET (fasi collassate a mediana)':>52}" + f"{d_gate:>+9.3f}{np.percentile(d_per_off,10):>+9.3f}" + f"{np.percentile(d_per_off,90):>+9.3f}{int((d_per_off>0).sum()):>5}/23") + print(f" {'delta appaiato, spazio CONGIUNTO 230 celle':>52}" + f"{np.median(D):>+9.3f}{np.percentile(D,10):>+9.3f}" + f"{np.percentile(D,90):>+9.3f}{int((D>0).sum()):>4}/230") + print(f" {'delta a fase 0 fissa (la scelta COMODA)':>52}" + f"{d_ph0:>+9.3f}{np.percentile(D[:,0],10):>+9.3f}" + f"{np.percentile(D[:,0],90):>+9.3f}{int((D[:,0]>0).sum()):>5}/23") + print(f" {'ancora canonica (off 0, fase mediana) = PROP-RECAL':>52}" + f"{np.median(D[0]):>+9.3f}{'':>9}{'':>9}{int((D[0]>0).sum()):>5}/10") + print(f"\n Fortuna d'ancora di SKH01 SOTTO LA LENTE PROP (canonico - mediana):") + print(f" sul DELTA : {np.median(D[0]) - d_gate:+.3f} " + f"(l'offset 0 sta al {(d_per_off < d_per_off[0]).mean()*100:.0f}Β° pctl dei 23)") + print(f" sul LIVELLO J del candidato : {np.median(JC[0]) - np.median(np.median(JC,axis=1)):+.3f} " + f"(off 0 al {(np.median(JC,axis=1) < np.median(JC[0])).mean()*100:.0f}Β° pctl)") + print(f" sul LIVELLO J del LIVE : {JL[0] - np.median(JL):+.3f} " + f"(off 0 al {(JL < JL[0]).mean()*100:.0f}Β° pctl)") + print(" ⚠️ La fortuna si cancella in PARTE perche' il LIVE contiene SKH01 al 25%: il delta") + print(" e' meno ancorato dei livelli. E' la ragione per cui la statistica giusta e' il") + print(" DELTA APPAIATO e non il livello.") + tick("Β§2 griglia congiunta") + + # ============================================ Β§3. LA REGIONE, NON IL PUNTO + hr("Β§3. LA REGIONE w_XS 25-38% β€” il gate parla di una REGIONE, non di un punto") + print(" Tre pesi FISSI dichiarati che coprono la regione (nessuna ri-ottimizzazione per") + print(" offset: sarebbe un max-of-23, cioe' il difetto sotto audit). W_A gira sulle 10 fasi") + print(f" piene; i due pesi di sensibilita' sul sottocampione a priori {PHASES_SUB} (budget).") + print(f"\n {'peso':>12} {'w_XS':>5} {'J mediano':>10} {'delta MEDIANO':>13} " + f"{'p10':>8} {'p90':>8} {'offset>0':>9} {'celle>0':>9} {'gate':>6}") + reg = {} + for lab, wt, phs in (("38/38/25", W_A, PHASES), ("38/25/38", W_B, PHASES_SUB), + ("25/38/38", W_C, PHASES_SUB)): + if wt is W_A: + Dw = D + Jw = JC + else: + Dw = np.zeros((len(OFFSETS), len(phs))) + Jw = np.zeros((len(OFFSETS), len(phs))) + for i, off in enumerate(OFFSETS): + RL, GL = rg_off(W_LIVE, I_LONG, XSP[0], off) + _, _, vL = best_over_levers(RL, GL, ev, fu) + for k, ph in enumerate(phs): + Rc, Gc = rg_off(wt, I_LONG, XSP[ph], off) + jc, _, vc = best_over_levers(Rc, Gc, ev, fu) + Jw[i, k] = jc + Dw[i, k] = float((vc - vL).mean()) + dpo = np.median(Dw, axis=1) + dm = float(np.median(dpo)) + reg[lab] = dm + print(f" {lab:>12} {int(round(100*wt['XS01'])):>4}% {np.median(Jw):>10.3f} " + f"{dm:>+13.3f} {np.percentile(dpo,10):>+8.3f} {np.percentile(dpo,90):>+8.3f} " + f"{int((dpo>0).sum()):>7}/23 {int((Dw>0).sum()):>6}/{Dw.size} " + f"{'PASS' if dm > GATE_B else 'FAIL':>6}") + worst = min(reg.values()) + print(f"\n Peggiore della regione: {min(reg, key=reg.get)} con {worst:+.3f}; " + f"mediana dei tre {np.median(list(reg.values())):+.3f}") + tick("Β§3 regione") + + # ============================================ Β§4. SURROGATO vs MISURA + hr("Β§4. IL SURROGATO CONTRO LA MISURA β€” quanto valeva il taglio del drift") + print(" PROP-RECAL surrogava la banda d'ancora con un taglio del DRIFT di SKH01 (x0.50 e") + print(" x0.33) e leggeva +0.124 / +0.113. Qui lo stesso surrogato e' ricalcolato al peso") + print(" FISSO W_A e con lo stesso collasso di fase, cosi' il confronto e' appaiato.") + print(f"\n {'lente':>44} {'delta MEDIANO':>13}") + for lab, dmt in (("misura vera: 23 offset x 10 fasi", None), + ("surrogato SKH x0.50 (ancora canonica)", (("SKH01", 0.50),)), + ("surrogato SKH x0.33 (ancora canonica)", (("SKH01", 0.33),))): + if dmt is None: + print(f" {lab:>44} {d_gate:>+13.3f}") + continue + RLs, GLs = PR.rg(W_LIVE, I_LONG, XSP[0], dm=dmt) + _, _, vLs = best_over_levers(RLs, GLs, ev, fu) + dsx = [] + for ph in PHASES: + Rs, Gs = PR.rg(W_A, I_LONG, XSP[ph], dm=dmt) + _, _, vs = best_over_levers(Rs, Gs, ev, fu) + dsx.append(float((vs - vLs).mean())) + print(f" {lab:>44} {np.median(dsx):>+13.3f}") + print("\n Un surrogato che taglia il DRIFT non e' la stessa cosa di una griglia diversa:") + print(" cambiare l'ancora cambia QUALI trade esistono, non solo quanto rendono.") + tick("Β§4 surrogato") + + # ============================================ Β§5. IL NUMERO OPERATIVO + hr("Β§5. IL NUMERO OPERATIVO β€” P(>=50 EUR/g) da EUR 600 in 36 mesi, sulla banda d'ancora") + print(f" Sottocampione DICHIARATO: 23 offset x {len(PHASES_SUB)} fasi {PHASES_SUB} (uniformi a") + print(" priori sul ciclo H=10) = 69 simulazioni da 1500 percorsi. La scala di conti costa") + print(" ~1.7 s a simulazione: la griglia piena 23x10 costerebbe ~7 min in piu' e la mediana") + print(" di 69 estrazioni basta per una banda al punto percentuale.") + pc.DELUCK = PA.DELUCK + W3 = PA.CRYPTO_PROP + pt, pz, med_eur = [], [], [] + t = time.time() + if time.time() - t0 < 620: + for i, off in enumerate(OFFSETS): + for ph in PHASES_SUB: + Rc, Gc = rg_off(W_A, I_LONG, XSP[ph], off) + jc, lvc, _ = best_over_levers(Rc, Gc, ev, fu) + cfg = tuple(W_A.get(nm, 0.0) for nm in W3) + key = (cfg[0], cfg[1], cfg[2], "shared") + saved = pc._TUP_CACHE.pop(key, None) + pc._TUP_CACHE[key] = (Rc, Gc, I_LONG) + r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=lvc, + seed=20260822, lens="coupled", start=str(I_LONG[0].date())) + pc._TUP_CACHE.pop(key, None) + if saved is not None: + pc._TUP_CACHE[key] = saved + pt.append(r["p_target"]); pz.append(r["p_zero"]); med_eur.append(r["med"]) + bump(len(OFFSETS) * len(PHASES_SUB)) + print(f"\n P(>=50 EUR/g) su {len(pt)} celle (offset x fase): mediana " + f"{np.median(pt):.1%} banda p10-p90 [{np.percentile(pt,10):.1%}, " + f"{np.percentile(pt,90):.1%}] min {min(pt):.1%} max {max(pt):.1%}") + print(f" P(zero) mediana {np.median(pz):.1%} | EUR/g mediana {np.median(med_eur):.2f}") + print(f" All'ancora CANONICA (off 0, fasi {PHASES_SUB}): " + f"{np.median(pt[:len(PHASES_SUB)]):.1%}") + print(f" PROP-RECAL pubblicava {PUB['p50g']:.1%} [7.2-9.1%] (ancora canonica, 10 fasi)") + print(f" ({time.time()-t:.0f}s)") + else: + print("\n NON GIRATO: budget di tempo esaurito prima di Β§5. Si dichiara, non si stima.") + pt = [] + tick("Β§5 numero operativo") + + # ============================================ Β§6. SENSIBILITA' AL DE-LUCK + hr("Β§6. SENSIBILITA' β€” il de-luck x0.89 su questa dimensione e' un doppio conteggio") + print(" Lo 0.89 fu MISURATO (26/07) come rapporto mediana-ancore / ancora-canonica del") + print(" drift: applicarlo mentre si gira la banda d'ancora conta due volte la stessa") + print(" correzione. E' tenuto per comparabilita' con PROP-RECAL; qui la stessa misura a") + print(" fattore 1.00 (nessun de-luck), sulle stesse 23 x 3 celle.") + d_nd = [] + for i, off in enumerate(OFFSETS): + RL, GL = rg_off(W_LIVE, I_LONG, XSP[0], off) + _, _, vL = best_over_levers(RL, GL, ev, fu, factor=1.0) + row = [] + for ph in PHASES_SUB: + Rc, Gc = rg_off(W_A, I_LONG, XSP[ph], off) + _, _, vc = best_over_levers(Rc, Gc, ev, fu, factor=1.0) + row.append(float((vc - vL).mean())) + d_nd.append(float(np.median(row))) + print(f"\n delta appaiato mediano de-luck x0.89 (headline) : {d_gate:+.3f}") + print(f" de-luck x1.00 (no double) : {np.median(d_nd):+.3f}" + f" ({int(sum(1 for d in d_nd if d > 0))}/23 > 0)") + print(" Il verso di questa differenza dice se la scelta conservativa lo era davvero.") + tick("Β§6 sensibilita'") + + # ============================================ Β§7. VERDETTO + hr("Β§7. VERDETTO SULLA GAMBA (b)") + passed = d_gate > GATE_B + print(f" Metrica pre-registrata: mediana delle differenze appaiate J(w_XS 25-38%) - J(LIVE)") + print(f" sui 23 offset, W-LONG. Soglia: > +{GATE_B:.2f}") + print(f"\n MISURA (headline W_A 38/38/25, fasi collassate a mediana): {d_gate:+.4f}") + print(f" banda p10-p90 sui 23 offset: [{np.percentile(d_per_off,10):+.3f}, " + f"{np.percentile(d_per_off,90):+.3f}] offset positivi: " + f"{int((d_per_off>0).sum())}/23 celle positive: {int((D>0).sum())}/230") + print(f" regione: peggiore {worst:+.3f} ({min(reg, key=reg.get)}), " + f"mediana dei tre pesi {np.median(list(reg.values())):+.3f}") + print(f"\n >>> GAMBA (b): {'PASS' if passed else 'FAIL'} " + f"({d_gate:+.4f} {'>' if passed else '<='} +{GATE_B:.2f})") + if passed: + print(" L'aspettativa registrata da PROP-RECAL (un PASS) e' confermata.") + else: + print(" L'aspettativa registrata da PROP-RECAL (un PASS) e' SMENTITA: il vantaggio") + print(" era in parte calendario.") + if pt: + print(f"\n COSA RESTA del risultato funded, ri-ancorato: P(>=50 EUR/g) mediana " + f"{np.median(pt):.1%} [banda d'ancora {np.percentile(pt,10):.1%}-" + f"{np.percentile(pt,90):.1%}] contro il {PUB['p50g']:.1%} pubblicato.") + print(f"\n RESTA FUORI (dichiarato): l'ancora giornaliera di TP01 (24 ore) β€” terza dimensione,") + print(" non nel perimetro della gamba (b), e la sua fortuna e' ancora dentro questi numeri.") + print(f"\n TRIAL/CELLE DICHIARATI (al rialzo): {TRIALS}") + print(f" fatto in {time.time()-t0:.0f}s") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822c_worst_day.py b/scripts/research/r0822c_worst_day.py new file mode 100644 index 0000000..b969731 --- /dev/null +++ b/scripts/research/r0822c_worst_day.py @@ -0,0 +1,1112 @@ +#!/usr/bin/env python +"""r0822c_worst_day.py β€” IL PEGGIOR GIORNO CHE IL LIBRO PUO' AVERE, misurato invece che assunto. + +PERCHE' ESISTE QUESTO FILE. Il gradino di leva 1,00x -> 1,25-1,50x vale 14,7 anni -> 12,9-11,6 +al capitale-rendita (~EUR300/mese di versamenti equivalenti, a costo zero) ed e' bloccato da UN +solo parametro SCELTO A MANO in `r0822_growth_policy.py` sez. (4): *"un giorno -10% all'anno +porta k* a 2x"*. Nessuno ha mai misurato quel numero. Il critico dell'ondata ha fatto un +controllo di cinque minuti (sui 10 peggiori giorni del SOTTOSTANTE il libro guadagna, e il suo +peggior giorno non e' un giorno di crash) e ha indicato la misura come quella di maggior valore +mai fatta. Qui si VERIFICA o si SMENTISCE quel controllo, e si sostituisce il -10% con una stima. + +COSA C'E' QUI + (0) IL CRITERIO, dichiarato PRIMA di guardare qualunque numero delle sezioni 4-6. + (1) REPLICA DI CONTROLLO β€” 5 numeri gia' pubblicati, riprodotti prima di scriverne di nuovi. + (2) LA DISTRIBUZIONE DELLE CHIUSURE del libro: coda sinistra per quantili, il peggiore + assoluto, QUALI giorni sono, e l'attribuzione esatta ai due sleeve (max|diff| = 0). + (3) LA LENTE ACCOPPIATA β€” il minimo INTRA-giorno (`r0725_prop_coupled`). La domanda "qual e' + il peggior giorno" ha DUE risposte e servono entrambe: la leva agisce su tutte e due, ma + il margine di mantenimento guarda il minimo. + (4) LA CODA CHE NON E' NEL DATASET β€” EVT/POT (GPD sopra soglia dichiarata) su chiusure E su + minimi, quantili a 1/1.000 e 1/10.000 giorni, IC bootstrap A BLOCCHI sui parametri. + (5) IL PEGGIOR GIORNO POSSIBILE PER COSTRUZIONE β€” aritmetica, non statistica: dato il cap di + `config/live.json`, il vol-target di TP01 e il gap-through misurato di SKH01. + (6) k* ONESTO β€” la curva di Kelly rifatta con la coda stimata al posto di quella a mano, e la + decomposizione dell'errore del -10% (taglia vs FREQUENZA). + (7) IL VERDETTO contro il criterio di (0). + +ONESTA' APPLICATA (regole gia' codificate nel progetto): + Β· un fattore correttivo scelto a occhio va DECOMPOSTO prima di accettarlo o rifiutarlo (26/07); + Β· close-only e' CIECA su una regola a UN giorno (25/07) -> la lente accoppiata e' obbligatoria; + Β· un fit di coda su ~2.700 osservazioni ha incertezza grande: si riporta, o si sostituisce un + numero scelto a mano con uno finto-preciso, che e' peggio; + Β· un Monte Carlo ha una risoluzione: qui la curva di crescita e' calcolata in forma ESATTA + (E[log]) proprio per togliere di mezzo il rumore MC dello scetticato. + + nice -n 19 timeout 900 uv run python scripts/research/r0822c_worst_day.py +""" +from __future__ import annotations + +import json +import sys +import time +from pathlib import Path + +import warnings + +import numpy as np +import pandas as pd +from scipy import stats + +warnings.filterwarnings("ignore", message=".*Sorting by default when concatenating.*") + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0725_capcurve as CC # noqa: E402 book_series (lente LIVE), TAX_RATE +import r0725_prop_coupled as PC # noqa: E402 recon accoppiata (R, m) +import r0726_deposits as DP # noqa: E402 DELUCK = 0.89 MISURATO +import r0724_skh_live_weight as SKL # noqa: E402 gamba SKH01 sul path LIVE + +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, realized_vol, resample_1d, simple_returns) +from src.strategies.skyhook import SKH01_V2_DD # noqa: E402 + +ASSETS = ("BTC", "ETH") +W_TP, W_SKH = 0.75, 0.25 +ANN = 365.0 +SEED = 20260822 + +# ---- parametri EVT DICHIARATI PRIMA (criterio, non tuning) ------------------------------- +EVT_Q = 0.95 # soglia = 95Β° pctl delle PERDITE. Criterio dichiarato: la scelta + # classica del compromesso bias/varianza (~5% di eccedenze, qui 136 + # osservazioni). Le altre soglie sono girate come SENSITIVITA', non + # per scegliere la migliore: se il numero si muove molto, si dice. +EVT_Q_SENS = (0.90, 0.95, 0.975, 0.99) +EVT_BOOT = 400 # ricampionamenti a BLOCCHI (la serie ha vol-clustering: un bootstrap +EVT_BLOCK = 20 # iid stringerebbe l'IC di una coda che non e' iid) +RP = (1_000, 10_000) # periodi di ritorno in GIORNI (2,7 anni e 27 anni) + +# ---- il numero sotto esame ---------------------------------------------------------------- +STRESS_A_MANO = 0.10 # il "-10% in un giorno" di r0822_growth_policy sez.(4) +STRESS_FREQ_A_MANO = 1.0 # ...assunto UNA VOLTA L'ANNO (e 1 ogni 5 anni come variante) + +# ---- costanti di venue (dichiarate, fonte in linea) -------------------------------------- +MM_RATE = 0.01 # maintenance margin ~1% (Deribit perp lineari, valore prudente: + # il listino parte da 0,5% e cresce con la size; 1% e' conservativo + # NEL VERSO che rende la liquidazione PIU' vicina) +WIPE = 0.90 # convenzione ereditata da growth_policy/skeptic: perdita giornaliera + # >= 90% dell'equity = conto chiuso. Usata solo per replicare i loro numeri. + + +# ========================================================================================== +# (0) IL CRITERIO β€” congelato qui, valutato in (7). Nessuna delle sue soglie usa un numero +# delle sezioni 4-6: vengono dalle regole gia' scritte del progetto e dal listino del venue. +# ========================================================================================== +CRITERIO = { + "C1": ("STRUTTURALE β€” il peggior giorno POSSIBILE per costruzione, a leva k, deve lasciare " + "in piedi almeno META' dell'equity: k * |L_strutturale| <= 0.50. " + "(Ragione: zero e' assorbente e il conto e' unico; sotto meta' equity il piano non " + "e' 'in ritardo', e' finito.)"), + "C2": ("KELLY PESSIMISTICO β€” k <= 1/2 * k*(drift a -2 errori standard E coda troncata al " + "bound strutturale). Il mezzo-Kelly e' la prescrizione classica ed e' scelta A PRIORI, " + "non tarata su questo risultato; il -2 SE viene dalla fragilita' gia' misurata " + "(r0822_growth_policy sez. 1-bis: k* e' LINEARE nell'errore del drift)."), + "C3": ("LIQUIDAZIONE SUL MINIMO β€” con la lente ACCOPPIATA, il quantile a 1/10.000 giorni del " + "minimo INTRA-giorno a leva k deve stare almeno 3x lontano dalla soglia di " + "liquidazione del venue. (close-only e' CIECA su una regola a un giorno: 25/07.)"), + "C4": ("GUADAGNO ROBUSTO β€” g(k) - g(1.00) deve restare POSITIVO col drift a -2 SE. " + "Se il gradino paga solo al drift di punto, non e' un gradino, e' una scommessa " + "sulla stima del drift."), + "C5": ("COSTI NON MODELLATI β€” il costo che scala con k e non e' nei backtest (funding dei " + "perpetual, costo del margine oltre 1x) deve essere piccolo rispetto al guadagno."), + "C6": ("PROCESSO β€” deve ESISTERE una strada pulita per un puro cambio di SCALA. Regola del " + "progetto: passa dal cap di config, non da `target_vol` (che e' anche un tilt di pesi " + "e fallisce `weights_tilt_null`). Se quella strada non esiste, il gradino non e' " + "autorizzabile *nella forma in cui e' stato proposto*, qualunque cosa dicano C1-C5."), +} + + +def sezione(t: str) -> None: + print("\n" + "=" * 110) + print(f" {t}") + print("=" * 110) + + +def pct(x: float) -> str: + return f"{x:+.2%}" if np.isfinite(x) else " n/d" + + +# ========================================================================================== +# DATI +# ========================================================================================== +def serie_libro() -> dict: + """Serie giornaliere del libro live e le sue due gambe, + il sottostante.""" + book = CC.book_series("hourly") + skh = SKL.skh_book(0, "hourly") + if skh.index.tz is None: + skh.index = skh.index.tz_localize("UTC") + tp = CC.tp01_realistic(600.0 * W_TP) + und = {} + for a in ASSETS: + df = load_data(a, "1h") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + s = pd.Series(df["close"].values.astype(float), index=idx).resample("1D").last() + und[a] = s.pct_change() + J = pd.concat({"book": book, "tp_leg": W_TP * tp, "skh_leg": W_SKH * skh, + "BTC": und["BTC"], "ETH": und["ETH"]}, axis=1, sort=True).dropna() + J["und"] = 0.5 * J["BTC"] + 0.5 * J["ETH"] + deluck = book - (1.0 - DP.DELUCK) * float(book.values.mean()) # = DP.deluck_returns(), con indice + return {"J": J, "deluck": deluck} + + +def gambe_tp01() -> dict: + """Per asset: peso TENUTO da TP01, ritorno giornaliero, vol nota a close[d-1], z-score. + + z = r / (vol_ann_prev/sqrt(365.25)) e' la grandezza che rende STRUTTURALE il bound di TP01: + il contributo di gamba e' `signal * (target_vol/vol) * r = signal * target_vol * z / sqrt(365.25)`, + cioe' INVARIANTE alla volatilita'. Un vol-target converte un crash in uno z-score. + """ + out = {} + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + c = df["close"].values.astype(float) + r = simple_returns(c) + tgt = np.nan_to_num(np.asarray(TrendPortfolio(**CANONICAL).target_series(df), float)) + held = np.zeros(len(tgt)) + held[1:] = tgt[:-1] + vol = realized_vol(r, CANONICAL["vol_win_days"], 365.25) + volp = np.concatenate(([np.nan], vol[:-1])) + with np.errstate(divide="ignore", invalid="ignore"): + z = r / (volp / np.sqrt(365.25)) + lev_max = np.clip(np.where(volp > 0, CANONICAL["target_vol"] / volp, 0.0), + 0.0, CANONICAL["leverage"]) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + prev = np.concatenate(([np.nan], c[:-1])) + r_low = df["low"].values.astype(float) / prev - 1.0 # escursione avversa INTRA-giorno + out[a] = pd.DataFrame({"held": held, "ret": r, "ret_low": r_low, "vol_prev": volp, + "z": z, "lev_max": lev_max}, index=idx) + return out + + +def estremi_orari() -> dict: + """Movimento orario piu' avverso mai visto, per asset e per verso. E' il bound del + gap-through di SKH01: gli exit sono SOFTWARE e il fill arriva al close orario successivo + al tocco, quindi la perdita oltre lo stop e' l'escursione DENTRO quell'ora.""" + out = {} + for a in ASSETS: + c = load_data(a, "1h")["close"].values.astype(float) + r = c[1:] / c[:-1] - 1.0 + out[a] = {"giu": float(r.min()), "su": float(r.max())} + return out + + +# ========================================================================================== +# EVT / POT +# ========================================================================================== +def gpd_fit(loss: np.ndarray, q: float) -> dict: + """GPD (peaks-over-threshold) sulle PERDITE. `loss` = -ritorni. Ritorna anche le funzioni + di quantile e di probabilita' di eccedenza in forma chiusa.""" + n = len(loss) + u = float(np.quantile(loss, q)) + ex = loss[loss > u] - u + nu = len(ex) + if nu < 15: + return {} + xi, _, beta = stats.genpareto.fit(ex, floc=0.0) + return {"u": u, "n": n, "nu": nu, "xi": float(xi), "beta": float(beta), + "rate": nu / n} + + +def gpd_quantile(f: dict, p_giorno: float) -> float: + """Perdita superata con probabilita' `p_giorno` in un giorno (return level).""" + if not f: + return np.nan + xi, beta, u, rate = f["xi"], f["beta"], f["u"], f["rate"] + if abs(xi) < 1e-9: + return u + beta * np.log(rate / p_giorno) + return u + beta / xi * ((p_giorno / rate) ** (-xi) - 1.0) + + +def gpd_exceed(f: dict, x: float) -> float: + """P(perdita > x) al giorno, per x >= u.""" + if not f or x <= f["u"]: + return np.nan + xi, beta, u, rate = f["xi"], f["beta"], f["u"], f["rate"] + if abs(xi) < 1e-9: + return rate * np.exp(-(x - u) / beta) + base = 1.0 + xi * (x - u) / beta + if base <= 0: + return 0.0 + return rate * base ** (-1.0 / xi) + + +def evt_bootstrap(loss: np.ndarray, q: float, n_boot: int, block: int, + rng: np.random.Generator) -> dict: + """IC bootstrap A BLOCCHI su xi, beta e sui return level. A blocchi perche' la serie ha + vol-clustering: un bootstrap iid darebbe un IC piu' stretto del vero.""" + n = len(loss) + nb = int(np.ceil(n / block)) + xis, rl = [], {p: [] for p in RP} + for _ in range(n_boot): + st = rng.integers(0, n - block, size=nb) + idx = (st[:, None] + np.arange(block)[None, :]).ravel()[:n] + try: + f = gpd_fit(loss[idx], q) + except Exception: + continue + if not f: + continue + xis.append(f["xi"]) + for p in RP: + rl[p].append(gpd_quantile(f, 1.0 / p)) + return {"xi": np.array(xis), **{f"rl{p}": np.array(rl[p]) for p in RP}} + + +# ========================================================================================== +# CURVA DI CRESCITA β€” forma ESATTA (nessun rumore MC) +# ========================================================================================== +def g_exact(r: np.ndarray, k: float) -> float: + """Crescita geometrica annua a leva k: exp(365 * E[log(1+k r)]) - 1. + Su orizzonti lunghi la mediana del moltiplicatore converge a questa quantita': e' la + stessa grandezza che lo scetticato stima in Monte Carlo, qui in forma chiusa.""" + x = 1.0 + k * r + if np.any(x <= 0): + return -np.inf + return float(np.expm1(ANN * np.mean(np.log(x)))) + + +def k_star(r: np.ndarray, kmax: float = 40.0, step: float = 0.05) -> tuple[float, float]: + ks = np.arange(step, kmax + step, step) + g = np.array([g_exact(r, k) for k in ks]) + j = int(np.nanargmax(g)) + return float(ks[j]), float(g[j]) + + +def campione_semiparam(r: np.ndarray, f: dict, l_max: float, n_draw: int, + drift_annuo: float, rnd: dict) -> np.ndarray: + """Corpo EMPIRICO + coda GPD TRONCATA a `l_max`, ri-centrato al drift dichiarato. + + La troncatura non e' un artificio di comodo: una GPD con xi>0 ha supporto illimitato, quindi + E[log(1+kR)] sarebbe -inf per QUALSIASI k>0 e la domanda non avrebbe risposta. Il supporto + lo da' l'aritmetica della sezione (5), non la statistica: e' esattamente il posto in cui i + due pezzi di questo lavoro si incastrano. + """ + loss = -r + body = r[loss <= f["u"]] + out = body[rnd["ib"] % len(body)] # stessi indici in OGNI cella + m = rnd["um"] < f["rate"] # stessa maschera + nt = int(m.sum()) + if nt: + fmax = stats.genpareto.cdf(max(l_max - f["u"], 1e-9), f["xi"], loc=0.0, scale=f["beta"]) + y = stats.genpareto.ppf(rnd["ut"][:nt] * fmax, f["xi"], loc=0.0, scale=f["beta"]) + out = out.copy() + out[m] = -(f["u"] + y) + return out - out.mean() + drift_annuo / ANN + + +# ========================================================================================== +def main() -> None: + t_start = time.time() + rng = np.random.default_rng(SEED) + print("=" * 110) + print(" r0822c β€” WORST-DAY: il peggior giorno del libro, MISURATO (chiusura e minimo), e") + print(" la coda che non e' nel dataset, STIMATA invece che scelta a mano") + print("=" * 110) + + # ---------------------------------------------------------------- (0) + sezione("(0) IL CRITERIO β€” dichiarato PRIMA, valutato in (7)") + print("\n Il gradino di leva a 1,25x e' AUTORIZZABILE solo se passano TUTTE queste sei.") + print(" Nessuna soglia qui sotto usa un numero delle sezioni 4-6: vengono dalle regole") + print(" gia' scritte del progetto e dal listino del venue.\n") + for k, v in CRITERIO.items(): + testo = v.replace(" β€” ", " β€” ") + print(f" {k}. {testo}") + print() + + # ---------------------------------------------------------------- (1) + sezione("(1) REPLICA DI CONTROLLO β€” cinque numeri gia' pubblicati, riprodotti prima di scriverne di nuovi") + D = serie_libro() + J = D["J"] + TPQ_pre = gambe_tp01() # serve gia' nella sez.(2) per l'esposizione per bucket + book = J["book"].values.astype(float) + r_del = DP.deluck_returns() + mu, sig = float(r_del.mean()), float(r_del.std()) + se_drift = sig * np.sqrt(ANN) / np.sqrt(len(r_del) / ANN) + + print(f"\n serie: {len(J)} giorni, {J.index.min().date()} -> {J.index.max().date()}") + print(f"\n (a) GROWTH-POLICY sez.(0) β€” serie de-luckata x{DP.DELUCK}") + print(f" {'grandezza':>22}{'pubblicato':>14}{'qui':>14}") + for nome, pub, qui in (("drift/anno", "17,11%", f"{mu*ANN:.2%}"), + ("vol/anno", "11,37%", f"{sig*np.sqrt(ANN):.2%}"), + ("Sharpe", "1,51", f"{mu/sig*np.sqrt(ANN):.2f}"), + ("peggior giorno", "-3,95%", f"{r_del.min():.2%}")): + print(f" {nome:>22}{pub:>14}{qui:>14}") + + print(f"\n (b) SCOMPOSIZIONE ESATTA del libro nelle due gambe (non e' una stima: e' identita')") + diff = np.abs(J["book"].values - (J["tp_leg"].values + J["skh_leg"].values)).max() + print(f" max|book - (0.75*TP01 + 0.25*SKH01)| = {diff:.2e} <- 0 esatto, la gamba" + f" responsabile di ogni giorno e' LEGGIBILE") + + print(f"\n (c) k* β€” lo scetticato lo stima in Monte Carlo (banda 10-14x, 'la POSIZIONE non e'") + print(f" risolta'). Qui la stessa quantita' in forma CHIUSA, senza rumore MC:") + ks_emp, gs_emp = k_star(r_del) + print(f" {'Kelly gaussiano mu/sigmaΒ²':>34}{mu/sig**2:>10.2f}x") + print(f" {'k* ESATTO argmax E[log(1+kR)]':>34}{ks_emp:>10.2f}x (g = {gs_emp:.1%}/anno)") + print(f" {'MC dello scetticato':>34}{'10-14x':>10} <- la forma chiusa cade DENTRO") + print(f" {'g(1.00x)':>34}{g_exact(r_del,1.0):>10.2%}/anno") + print(f" -> replica superata: la macchineria esatta e quella MC dicono la stessa cosa,") + print(f" e da qui in avanti il rumore MC non e' piu' una scusa per nessun numero.") + + # ---- il controllo del critico, rifatto in modo indipendente + print(f"\n (d) IL CONTROLLO DEL CRITICO, verificato: i 10 peggiori giorni del SOTTOSTANTE") + print(f" (media 50/50 BTC-ETH) e cosa ha fatto il libro in quei giorni.") + worst_und = J.sort_values("und").head(10) + print(f"\n {'data':>12}{'BTC':>10}{'ETH':>10}{'sottost.':>10}{'LIBRO':>10}" + f"{' gamba TP01':>13}{'gamba SKH01':>13}") + for d, row in worst_und.iterrows(): + print(f" {str(d.date()):>12}{pct(row.BTC):>10}{pct(row.ETH):>10}{pct(row.und):>10}" + f"{pct(row.book):>10}{pct(row.tp_leg):>13}{pct(row.skh_leg):>13}") + print(f"\n somma dei 10 giorni: LIBRO {worst_und.book.sum():+.2%} " + f"contro sottostante {worst_und.und.sum():+.2%}") + print(f" giorni in cui il libro GUADAGNA: {int((worst_und.book>0).sum())}/10 Β· " + f"peggiore dei dieci {worst_und.book.min():+.2%}") + print(f"\n -> IL CONTROLLO DEL CRITICO E' CONFERMATO NELLA SOSTANZA. Il 12 marzo 2020") + print(f" (BTC {J.loc[J.und.idxmin(),'BTC']:+.1%}, ETH {J.loc[J.und.idxmin(),'ETH']:+.1%}) il libro fa " + f"{J.und.min() and J.loc[J.und.idxmin(),'book']:+.2%}.") + + # ---- il numero -3,38% del critico + b_can = CC.book_series("canonical") + print(f"\n (e) ⚠️ IL NUMERO DEL CRITICO NON SI RIPRODUCE β€” ma la sua LENTE si identifica,") + print(f" e la conclusione si rovescia proprio su quella.") + print(f" Il critico scrive 'peggior giorno assoluto -3,38%'. Le lenti del progetto danno:") + print(f" {'lente':>34}{'peggior giorno':>16}{'data':>14} che giorno era") + for nome, s in (("LIVE (book_series 'hourly')", J['book']), + ("CANONICA (book_series 'canonical')", b_can), + ("LIVE de-luckata x0.89", D["deluck"])): + i = s.idxmin() + u = J["und"].reindex([i]).iloc[0] if i in J.index else np.nan + tag = "RIMBALZO" if u > 0.05 else ("CRASH" if u < -0.05 else "giorno ordinario") + print(f" {nome:>34}{s.min():>16.2%}{str(i.date()):>14} {tag} (sottost. {u:+.1%})") + Jc = pd.concat({"b": b_can, "u": J["und"]}, axis=1).dropna() + wc = Jc.sort_values("u").head(10)["b"].values * 100 + wl = J.sort_values("und").head(10)["book"].values * 100 + print(f"\n LA LENTE DEL CRITICO SI PUO' IDENTIFICARE dai valori che ha pubblicato") + print(f" ('+0,97 Β· +0,72 Β· 0,00 Β· -1,98 Β· -2,24 Β· +0,23'). Sui 10 peggiori giorni del") + print(f" sottostante le due lenti danno:") + print(f" LIVE : " + " ".join(f"{x:+.2f}" for x in wl)) + print(f" CANONICA : " + " ".join(f"{x:+.2f}" for x in wc)) + print(f" -> **il +0,97 e il +0,72 sono nella riga CANONICA e in nessun'altra**: il critico") + print(f" ha usato `book_series('canonical')`, cioe' il BACKTEST di SKH01 (exit al") + print(f" livello), non il path LIVE. Il suo peggior giorno resta {b_can.min():.2%} contro il") + print(f" -3,38% pubblicato: **0,09pp di scarto**, dell'ordine del difetto gia' noto") + print(f" nel progetto ('`data/raw` e' gitignored e il cron riscrive i parquet ogni") + print(f" notte -> stesso codice, dati diversi', XS-LITE e GTAA/TLT).") + print(f"\n -> **QUINDI LA SUA CONCLUSIONE SI ROVESCIA SULLA SUA STESSA LENTE.** Sulla") + print(f" canonica β€” quella che ha usato β€” il peggior giorno E' un giorno di ribasso") + print(f" ({b_can.idxmin().date()}, sottostante {J['und'].reindex([b_can.idxmin()]).iloc[0]:+.1%}). La frase 'il suo peggior giorno") + print(f" assoluto non e' un giorno di crash' e' VERA solo sulla lente LIVE, che e'") + print(f" quella giusta per una decisione operativa ma non e' quella da cui viene il") + print(f" suo numero. **Aveva ragione, con la lente sbagliata.**") + + # ---------------------------------------------------------------- (2) + sezione("(2) LA DISTRIBUZIONE COMPLETA DELLE CHIUSURE DEL LIBRO β€” e QUALI giorni sono i peggiori") + qs = (50, 25, 10, 5, 2.5, 1, 0.5, 0.2, 0.1) + print(f"\n (a) coda SINISTRA per quantili (lente LIVE, {len(book)} giorni = {len(book)/365:.1f} anni)") + print(f" {'quantile':>12}" + "".join(f"{f'p{q}':>10}" for q in qs) + f"{'min':>10}") + print(f" {'ritorno':>12}" + "".join(f"{np.percentile(book,q):>10.2%}" for q in qs) + + f"{book.min():>10.2%}") + print(f" {'(destra)':>12}" + "".join(f"{np.percentile(book,100-q):>10.2%}" for q in qs) + + f"{book.max():>10.2%}") + print(f"\n asimmetria {stats.skew(book):+.2f} Β· curtosi in eccesso {stats.kurtosis(book):+.1f}" + f" Β· giorni flat (|r|<1bps) {(np.abs(book)<1e-4).mean():.1%}") + print(f" ⚠️ la curtosi alta NON e' di per se' un allarme: la meta' dei giorni il libro e'") + print(f" quasi fermo (TP01 long-flat, SKH01 fuori mercato l'87,6% delle barre), e una") + print(f" distribuzione con un picco a zero e' leptocurtica per costruzione.") + + print(f"\n (b) i 15 PEGGIORI GIORNI del libro, con l'attribuzione esatta e cosa faceva il mercato") + w = J.sort_values("book").head(15) + print(f"\n {'data':>12}{'LIBRO':>9}{'TP01':>9}{'SKH01':>9}{'BTC':>9}{'ETH':>9} tipo di giornata") + for d, row in w.iterrows(): + if row.und > 0.05: + tag = "RIMBALZO (short squeeze)" + elif row.und < -0.05: + tag = "crash" + elif row.und < -0.02: + tag = "ribasso" + else: + tag = "giorno ordinario" + colpa = "TP01" if row.tp_leg < row.skh_leg else "SKH01" + print(f" {str(d.date()):>12}{row.book:>9.2%}{row.tp_leg:>9.2%}{row.skh_leg:>9.2%}" + f"{row.BTC:>9.1%}{row.ETH:>9.1%} {tag} [{colpa}]") + + # meccanismo + crash = J[J.und < -0.05] + rimb = J[J.und > 0.05] + ord_ = J[(J.und >= -0.02) & (J.und <= 0.02)] + print(f"\n (c) IL MECCANISMO β€” il libro per tipo di giornata") + print(f" {'tipo':>28}{'giorni':>8}{'media':>10}{'peggiore':>11}{'quota TP01':>12}{'quota SKH01':>13}") + for nome, sub in (("CRASH (sottost. < -5%)", crash), ("RIMBALZO (sottost. > +5%)", rimb), + ("ordinario (|sottost.|<2%)", ord_), ("tutti", J)): + if len(sub) == 0: + continue + print(f" {nome:>28}{len(sub):>8}{sub.book.mean():>10.3%}{sub.book.min():>11.2%}" + f"{sub.tp_leg.mean():>12.3%}{sub.skh_leg.mean():>13.3%}") + print(f"\n (d) ⚠️ QUI IL CONTROLLO DEL CRITICO VA CORRETTO, e la correzione E' il meccanismo.") + print(f" Sui **10 giorni piu' estremi** il libro guadagna (sopra). Ma sulla popolazione") + print(f" dei **160 giorni di crash** perde in media {crash.book.mean():+.3%}: la frase") + print(f" 'il libro guadagna nei crash' e' vera sugli estremi e FALSA in generale.") + print(f" La ragione e' misurabile β€” l'esposizione di TP01 DIPENDE dalla taglia del crollo:") + print(f" {'sottostante':>22}{'giorni':>8}{'espos. TP01':>14}{'libro medio':>14}" + f"{'peggiore':>11}{'protezione':>12}") + bucket = [(-1.00, -0.20, "< -20%"), (-0.20, -0.10, "-20% .. -10%"), + (-0.10, -0.05, "-10% .. -5%"), (-0.05, -0.02, "-5% .. -2%")] + espo = pd.concat({a: TPQ_pre[a]["held"] for a in ASSETS}, axis=1).mean(axis=1) * 0.5 * W_TP * 2 + Jx = J.join(espo.rename("espo"), how="left") + for lo, hi, lbl in bucket: + sub = Jx[(Jx.und > lo) & (Jx.und <= hi)] + if len(sub) == 0: + continue + cell = "GUADAGNA" if sub.book.mean() > 0 else f"{abs(sub.und.mean()/sub.book.mean()):.0f}x" + print(f" {lbl:>22}{len(sub):>8}{sub.espo.mean():>14.3f}{sub.book.mean():>14.3%}" + f"{sub.book.min():>11.2%}{cell:>12}") + print(f"\n -> **il trend-follower e' gia' FLAT quando il crollo e' grande, ed e' ancora") + print(f" LONG quando e' medio**: l'esposizione scende in modo monotono al crescere del") + print(f" crollo, perche' un crollo grande arriva dopo che il trend a 30/90/180 giorni") + print(f" si e' gia' girato (e il vol-target ha gia' de-levered). Il rapporto di") + print(f" protezione peggiora nella fascia mite e migliora negli estremi. **Il libro") + print(f" non e' immune ai crash: e' immune ai crash GROSSI.**") + print(f"\n E nei RIMBALZI il libro guadagna in media ({rimb.book.mean():+.3%}) MA ci tiene il") + print(f" suo peggior giorno assoluto ({rimb.book.min():+.2%}): media e coda vanno in") + print(f" direzioni opposte. **La coda di questo libro non sta dove sta quella del") + print(f" mercato.** Il -10% di growth_policy e' una coda da buy&hold applicata a un") + print(f" libro difensivo: l'evento e' quello sbagliato, non solo la taglia.") + i_worst = J["book"].idxmin() + rw = J.loc[i_worst] + print(f"\n il peggior giorno in dettaglio: {i_worst.date()} libro {rw.book:+.2%}") + print(f" BTC {rw.BTC:+.1%} Β· ETH {rw.ETH:+.1%} (il giorno DOPO il -43,6% del 12 marzo)") + print(f" TP01 {rw.tp_leg:+.2%} (flat: un trend-follower long-only non compra un crash)") + print(f" SKH01 {rw.skh_leg:+.2%} = uno short con sl 2% che GAPPA attraverso lo stop") + skh_only = (J['skh_leg'] / W_SKH) + print(f" a livello di sleeve nudo SKH01 fa {skh_only.min():+.2%} in quel giorno,") + print(f" contro un secondo peggiore di {skh_only.nsmallest(2).iloc[-1]:+.2%}: **la coda") + print(f" di SKH01 e' UN giorno solo**, e quel giorno e' un rimbalzo, non un crollo.") + + # ---------------------------------------------------------------- (3) + sezione("(3) LA LENTE ACCOPPIATA β€” il minimo INTRA-giorno (close-only e' CIECA su una regola a un giorno)") + tup = PC.crypto_daily_tuples(W_TP, W_SKH) + K = pd.concat({"R_live": J["book"], "R_rec": tup["R"], "m_rec": tup["m"], + "und": J["und"]}, axis=1).dropna() + gap = (K["m_rec"] - K["R_rec"]).values.astype(float) + m_acc = K["R_live"].values + gap # costruzione dello scettico: UN grado di liberta' + m_rec = K["m_rec"].values # sensitivita': recon pura + print(f"\n (a) COSTRUZIONE DICHIARATA (la stessa di r0822_leverage_skeptic, per confrontabilita'):") + print(f" chiusure dalla serie LIVE + GAP dalla recon ACCOPPIATA, appaiato per GIORNO.") + print(f" La recon pura (R e m entrambi canonici) e' riportata come SENSITIVITA'.") + print(f" n = {len(K)} giorni.") + print(f"\n (b) REPLICA delle marginali del gap (25/07 e scettico 22/08: p50 -0,16 / p90 -1,03 / p99 -2,71)") + print(f" {'':>14}{'p50':>10}{'p90':>10}{'p99':>10}{'min':>10}") + print(f" {'qui':>14}{np.percentile(gap,50)*100:>9.2f}pp{np.percentile(gap,10)*100:>9.2f}pp" + f"{np.percentile(gap,1)*100:>9.2f}pp{gap.min()*100:>9.2f}pp") + print(f" (i percentili del GAP si leggono dal basso: p90 del |gap| = p10 del gap firmato)") + + print(f"\n (c) LE DUE RISPOSTE ALLA DOMANDA 'qual e' il peggior giorno'") + print(f" {'':>26}{'p1':>10}{'p0.5':>10}{'p0.1':>10}{'MINIMO':>11}{'data del minimo':>18}") + for nome, arr in (("CHIUSURA (live)", K["R_live"].values), + ("MINIMO intra-giorno (acc.)", m_acc), + ("MINIMO (recon pura, sens.)", m_rec)): + i = int(np.argmin(arr)) + print(f" {nome:>26}{np.percentile(arr,1):>10.2%}{np.percentile(arr,0.5):>10.2%}" + f"{np.percentile(arr,0.1):>10.2%}{arr.min():>11.2%}{str(K.index[i].date()):>18}") + neg = K["R_live"].values < -1e-4 + print(f"\n rapporto minimo/chiusura sul peggior giorno: " + f"{m_acc.min()/K['R_live'].values.min():.2f}x Β· mediana sui soli giorni in perdita " + f"({int(neg.sum())} giorni) {np.median(m_acc[neg]/K['R_live'].values[neg]):.2f}x") + print(f" -> **il peggior giorno del libro e' -{abs(K['R_live'].min()):.2%} in chiusura e") + print(f" -{abs(m_acc.min()):.2%} al minimo**. Le due risposte differiscono di un fattore ~2:") + print(f" chi valuta una regola sul MINIMO (margine, daily-loss, stop di conto) e usa") + print(f" la chiusura sbaglia di quel fattore, e nel verso ottimista.") + + print(f"\n (d) ⚠️ LIMITE DICHIARATO DELLA COSTRUZIONE, e va detto proprio sul giorno che conta.") + j = int(np.argmin(K["R_live"].values)) + print(f" La recon accoppiata usa la convenzione CANONICA di SKH01 (exit AL LIVELLO): nel") + print(f" giorno del gap-through cappa il wick allo stop. Il {K.index[j].date()} la chiusura") + print(f" live e' {K['R_live'].values[j]:+.2%} e quella canonica {K['R_rec'].values[j]:+.2%}" + f" (differenza {K['R_live'].values[j]-K['R_rec'].values[j]:+.2%}), mentre il gap canonico") + print(f" di quel giorno vale solo {gap[j]*100:.2f}pp -> il minimo LIVE di quel giorno e'") + print(f" plausibilmente piu' profondo di {m_acc[j]:+.2%}. **Il numero riportato sopra e' un") + print(f" LIMITE SUPERIORE (ottimista) del minimo nel giorno peggiore**, non una misura.") + print(f" Non lo si ripara qui: servirebbe rifare `skh_hourly_contrib` con l'uscita live,") + print(f" cioe' cambiare la macchineria sotto esame. Si dichiara, e la sezione (5) da' il") + print(f" bound per via aritmetica, che non ha questo problema.") + + print(f"\n (e) CONTROLLO: il gap e' piu' profondo nei giorni che finiscono BENE? (finding 25/07)") + dec = pd.qcut(K["R_live"], 10, labels=False, duplicates="drop") + gp = pd.Series(gap, index=K.index).groupby(dec).mean() + print(f" gap medio per decile di chiusura (0 = peggiore). ⚠️ i decili collassano a" + f" {gp.shape[0]} bin") + print(f" perche' il {(np.abs(book)<1e-4).mean():.0%} dei giorni ha chiusura esattamente zero:") + print(" " + " ".join(f"d{int(i)}:{v*100:+.2f}" for i, v in gp.items())) + print(f" -> replica il verso del finding (il gap non e' concentrato nei giorni brutti):") + print(f" il decile PEGGIORE ha gap {gp.iloc[0]*100:+.2f}pp contro {gp.iloc[-1]*100:+.2f}pp" + f" del migliore.") + print(f" **E la conseguenza per NOI e' quella buona: un giorno di crollo chiude quasi sul") + print(f" proprio minimo, quindi la lente accoppiata NON peggiora lo stress di coda.**") + + # ---------------------------------------------------------------- (4) + sezione("(4) LA CODA CHE NON E' NEL DATASET β€” EVT/POT, con l'incertezza dichiarata") + print(f"\n 7,4 anni con due tori e un orso non contengono necessariamente il peggior giorno") + print(f" possibile. Qui si STIMA la coda invece di sceglierla: GPD sopra una soglia") + print(f" dichiarata prima ({EVT_Q:.0%}Β° pctl delle perdite), su chiusure E su minimi.") + + fits, boots = {}, {} + for nome, arr in (("CHIUSURA", K["R_live"].values), ("MINIMO (acc.)", m_acc)): + loss = -arr + print(f"\n --- {nome} ---") + print(f" {'soglia q':>10}{'u':>9}{'n ecced.':>10}{'xi':>9}{'beta':>9}" + + "".join(f"{f'1/{p} g':>12}" for p in RP) + f"{' P(perdita>10%)':>18}") + for q in EVT_Q_SENS: + f = gpd_fit(loss, q) + if not f: + continue + pe = gpd_exceed(f, STRESS_A_MANO) + rp10 = (1.0 / pe / ANN) if (np.isfinite(pe) and pe > 0) else np.inf + marca = " <-- soglia dichiarata" if abs(q - EVT_Q) < 1e-9 else "" + print(f" {q:>10.3f}{f['u']:>9.2%}{f['nu']:>10}{f['xi']:>9.3f}{f['beta']:>9.4f}" + + "".join(f"{-gpd_quantile(f,1.0/p):>12.2%}" for p in RP) + + f"{f'1 ogni {rp10:.0f} anni' if np.isfinite(rp10) else 'mai':>18}{marca}") + if abs(q - EVT_Q) < 1e-9: + fits[nome] = f + f = fits[nome] + bo = evt_bootstrap(loss, EVT_Q, EVT_BOOT, EVT_BLOCK, rng) + boots[nome] = bo + print(f" IC bootstrap A BLOCCHI ({EVT_BOOT} ricampionamenti, blocchi {EVT_BLOCK}g):") + print(f" xi {f['xi']:>8.3f} IC95 [{np.percentile(bo['xi'],2.5):+.3f}," + f" {np.percentile(bo['xi'],97.5):+.3f}]") + for p in RP: + v = bo[f"rl{p}"] + print(f" 1/{p:<6} {-gpd_quantile(f,1.0/p):>8.2%} IC95 " + f"[{-np.percentile(v,97.5):>7.2%}, {-np.percentile(v,2.5):>7.2%}]" + f" (p95 pessimista {-np.percentile(v,95):.2%})") + + print(f"\n (b) CROSS-CHECK INDIPENDENTE β€” stimatore di HILL dell'indice di coda.") + print(f" Non e' un ri-fit della stessa cosa: Hill stima xi dai soli ORDINE-STATISTICI") + print(f" superiori (rapporti di log), senza MLE e senza soglia di quantile. Se i due non") + print(f" concordano, il numero non e' pronto per una decisione.") + print(f" ⚠️ La strada ovvia β€” fittare una t di Student su TUTTA la serie β€” qui NON") + print(f" funziona e va detto perche': il {(np.abs(book)<1e-4).mean():.0%} dei giorni e' esattamente zero") + print(f" (libro flat), la MLE manda i gradi di liberta' a ~0 per adattarsi all'atomo in") + print(f" zero e sputa quantili assurdi (10^12). **Una distribuzione con un atomo non si") + print(f" fitta con una densita' continua**: il POT non ha il problema perche' guarda solo") + print(f" la coda. Riportata qui sotto la t sui soli giorni ATTIVI, come terza strada.") + print(f"\n {'serie':>18}{'k ordini':>10}{'xi Hill':>10}{'xi GPD':>9}" + + "".join(f"{f'1/{p} g (Hill)':>16}" for p in RP)) + for nome, arr in (("CHIUSURA", K["R_live"].values), ("MINIMO (acc.)", m_acc)): + loss = np.sort(-arr)[::-1] + loss = loss[loss > 0] + kk = max(int(0.05 * len(loss)), 20) + xi_h = float(np.mean(np.log(loss[:kk])) - np.log(loss[kk])) + n_ = len(arr) + # Weissman: quantile estremo dallo stimatore di Hill + wq = lambda p: loss[kk] * ((kk / (n_ * p)) ** xi_h) # noqa: E731 + f = fits[nome] + print(f" {nome:>18}{kk:>10}{xi_h:>10.3f}{f['xi']:>9.3f}" + + "".join(f"{-wq(1.0/p):>16.2%}" for p in RP)) + print(f"\n {'serie':>18}{'gdl (t, attivi)':>18}{'sigma':>9}" + + "".join(f"{f'1/{p} g (t)':>14}" for p in RP)) + for nome, arr in (("CHIUSURA", K["R_live"].values), ("MINIMO (acc.)", m_acc)): + att = arr[np.abs(arr) > 1e-4] + df_, loc_, sc_ = stats.t.fit(att) + print(f" {nome:>18}{df_:>18.1f}{sc_:>9.4f}" + + "".join(f"{stats.t.ppf(1.0/p*len(att)/len(arr), df_, loc_, sc_):>14.2%}" for p in RP)) + + fc, fm = fits["CHIUSURA"], fits["MINIMO (acc.)"] + evt_boot_close = boots["CHIUSURA"] + pe = gpd_exceed(fc, STRESS_A_MANO) + # la stessa domanda con lo stimatore di HILL (indipendente dal MLE della GPD) + _l = np.sort(-K["R_live"].values)[::-1] + _l = _l[_l > 0] + _k = max(int(0.05 * len(_l)), 20) + _xi = float(np.mean(np.log(_l[:_k])) - np.log(_l[_k])) + pe_hill = (_k / len(K)) * (STRESS_A_MANO / _l[_k]) ** (-1.0 / _xi) + print(f"\n (c) IL CONFRONTO CHE DECIDE: il -{STRESS_A_MANO:.0%} assunto a mano e' DENTRO o FUORI?") + print(f" Β· chiusura, quantile 1/1.000 giorni : {-gpd_quantile(fc,1e-3):>8.2%}") + print(f" Β· chiusura, quantile 1/10.000 giorni : {-gpd_quantile(fc,1e-4):>8.2%}") + print(f" Β· minimo, quantile 1/10.000 giorni : {-gpd_quantile(fm,1e-4):>8.2%}") + if pe > 0: + print(f" Β· il -{STRESS_A_MANO:.0%} a mano, in chiusura : P = {pe:.2e}/giorno" + f" = 1 ogni {1/pe/ANN:.0f} anni") + else: + print(f" Β· il -{STRESS_A_MANO:.0%} a mano, in chiusura : probabilita' NULLA nel modello") + print(f" Β· lo stesso, con lo stimatore di HILL : P = {pe_hill:.2e}/giorno" + f" = 1 ogni {1/pe_hill/ANN:.0f} anni") + print(f"\n -> **il -{STRESS_A_MANO:.0%} NON E' una taglia impossibile, e' una FREQUENZA impossibile.**") + print(f" growth_policy lo assume UNA VOLTA L'ANNO (1 ogni 5 anni come variante mite).") + print(f" I due stimatori indipendenti lo mettono a **1 ogni {min(1/pe/ANN,1/pe_hill/ANN):.0f}" + f" - {max(1/pe/ANN,1/pe_hill/ANN):.0f} anni**:") + print(f" **{np.log10(1/pe_hill/ANN):.0f}-{np.log10(1/pe/ANN):.0f} ordini di grandezza di scarto sulla FREQUENZA**.") + print(f" ⚠️ E i due stimatori NON concordano fra loro: xi GPD {fc['xi']:.3f} contro Hill") + print(f" {_xi:.3f}, cioe' un quantile a 1/10.000 di {-gpd_quantile(fc,1e-4):.2%} contro" + f" {-(_l[_k]*((_k/(len(K)*1e-4))**_xi)):.2%}.") + print(f" **Si cita la BANDA fra stimatori, non il numero di uno solo** β€” su una coda") + print(f" stimata da {len(K)} osservazioni la scelta dello stimatore vale quanto i dati.") + print(f"\n ⚠️ E QUI VA DETTO IL LIMITE DELL'EVT, che e' il vero motivo per cui la sezione (5)") + print(f" esiste: **una GPD stimata sui giorni vissuti estrapola il MECCANISMO vissuto.**") + print(f" Se il libro puo' assumere una configurazione di posizioni che nel campione non") + print(f" ha mai assunto, quella configurazione non e' nella coda stimata. Nessun") + print(f" intervallo di confidenza copre un modo di perdere che non e' nei dati: e'") + print(f" esattamente cio' che il gate `implausible_sharpe` chiama 'rischio FUORI dal") + print(f" dataset'. La risposta non e' un fit migliore, e' un conto ARITMETICO.") + + # ---------------------------------------------------------------- (5) + sezione("(5) IL PEGGIOR GIORNO POSSIBILE PER COSTRUZIONE β€” aritmetica, non statistica") + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac = float(cfg["max_notional_per_asset_frac"]) + TPQ = TPQ_pre + EX = estremi_orari() + sl_l, sl_s = SKH01_V2_DD.sl_pct, SKH01_V2_DD.sl_pct_short + tv, levcap = CANONICAL["target_vol"], CANONICAL["leverage"] + + print(f"\n (a) LA FORMULA DEL LIBRO LIVE (src/live/book.book_net_target, letta dal codice):") + print(f" net_target[asset] = clamp( 0.5 * E * (0.75*tp_frac + 0.25*skh_sign), Β±cap ),") + print(f" cap = E * {frac} -> per-asset |net| <= {frac:.2f}Β·E, LORDO massimo {2*frac:.2f}Β·E su 2 asset.") + print(f" In frazione di equity: gamba TP01 = {0.5*W_TP:.3f}Β·tp_frac, gamba SKH01 = {0.5*W_SKH:.3f}Β·skh_sign.") + + print(f"\n (b) TP01 β€” il vol-target rende il bound INVARIANTE alla volatilita'.") + print(f" tp_frac = segnale * min(target_vol/vol, {levcap:g}) con segnale in {{0, 1/3, 2/3, 1}},") + print(f" quindi contributo di gamba = 0.375 * segnale * target_vol * z / sqrt(365.25)") + print(f" = {0.5*W_TP*tv/np.sqrt(365.25)*100:.4f}% * segnale * z. **Un crash entra solo attraverso il suo z-score.**") + print(f" ⚠️ La formula vale con tp_frac = target_vol/vol, cioe' solo dove il CAP non morde:") + print(f" 0.375*tp_frac + 0.125 <= {frac} richiede tp_frac <= 1, cioe' vol >= target_vol.") + print(f" Sotto quella soglia il contributo e' 0.375*|r| ed e' PIU' PICCOLO -> il bound") + print(f" si prende sullo z peggiore CON vol >= {tv:.0%} (auto-consistenza, non cherry-pick).") + print(f" {'asset':>7}{'z min (vol>=tv)':>17}{'quando':>13}{'ret':>9}{'vol prec.':>11}" + f"{'tp_frac':>9}{'contributo max':>16}{'z min GREZZO':>14}") + zmin, zmin_raw = {}, {} + for a in ASSETS: + d = TPQ[a].dropna(subset=["z"]) + zmin_raw[a] = float(d["z"].min()) + dv = d[d["vol_prev"] >= tv] + i = dv["z"].idxmin() + zmin[a] = float(dv["z"].min()) + contr = 0.5 * W_TP * tv * abs(zmin[a]) / np.sqrt(365.25) + print(f" {a:>7}{zmin[a]:>17.1f}{str(i.date()):>13}{dv.loc[i,'ret']:>9.1%}" + f"{dv.loc[i,'vol_prev']:>11.1%}{min(tv/dv.loc[i,'vol_prev'],1.0):>9.2f}" + f"{-contr:>16.2%}{zmin_raw[a]:>14.1f}") + print(f" tp_frac MASSIMO mai tenuto: " + " Β· ".join( + f"{a} {TPQ[a]['held'].max():.3f}" for a in ASSETS) + + f" (il cap di leva {levcap:g}x NON e' mai stato binding: la vol a 30g non e' mai scesa" + f" sotto {min(float(TPQ[a]['vol_prev'].min()) for a in ASSETS):.1%})") + + print(f"\n (c) SKH01 β€” NON e' vol-targeted (misurato 26/07): la sua taglia e' fissa e la sua") + print(f" perdita e' limitata dallo STOP, ma gli exit sono SOFTWARE (fill al close orario") + print(f" successivo al tocco) -> il bound e' stop + escursione DENTRO quell'ora.") + print(f" {'asset':>7}{'1h peggiore giu':>18}{'1h peggiore su':>17}" + f"{'bound LONG (sl 4%)':>21}{'bound SHORT (sl 2%)':>21}{'peggio realizzato':>19}") + bound_l, bound_s = {}, {} + for a in ASSETS: + bound_l[a] = sl_l + abs(EX[a]["giu"]) + bound_s[a] = sl_s + abs(EX[a]["su"]) + real = SKL.skh_daily_asset(a, 0, "hourly").min() + print(f" {a:>7}{EX[a]['giu']:>18.2%}{EX[a]['su']:>17.2%}" + f"{-bound_l[a]:>21.2%}{-bound_s[a]:>21.2%}{real:>19.2%}") + print(f" -> il gap-through misurato dal progetto (sl 2% modellato -> -11/-23% realizzato)") + print(f" e' COERENTE con questo bound, che lo contiene senza esserne tarato.") + + print(f"\n (d) I DUE SCENARI COERENTI. TP01 e' LONG-ONLY: perde solo in ribasso. SKH01 short") + print(f" perde solo in RIALZO. Non possono perdere al massimo lo stesso giorno se SKH e'") + print(f" short -> gli scenari sono due, e vanno tenuti separati.") + lim_tp = {a: 0.5 * W_TP * tv * abs(zmin[a]) / np.sqrt(365.25) for a in ASSETS} + crash_max = sum(lim_tp[a] + 0.5 * W_SKH * bound_l[a] for a in ASSETS) + squeeze_max = sum(0.5 * W_SKH * bound_s[a] for a in ASSETS) + print(f"\n SCENARIO A β€” CRASH (TP01 a piena convinzione su entrambi + SKH01 LONG stoppato)") + for a in ASSETS: + print(f" {a}: TP01 {-lim_tp[a]:>7.2%} SKH01 {-0.5*W_SKH*bound_l[a]:>7.2%}" + f" -> {-(lim_tp[a]+0.5*W_SKH*bound_l[a]):>7.2%}") + print(f" TOTALE LIBRO {-crash_max:>7.2%} <-- IL BOUND STRUTTURALE a k=1,00x") + print(f"\n SCENARIO B β€” RIMBALZO (TP01 flat per costruzione + SKH01 SHORT stoppato su due)") + for a in ASSETS: + print(f" {a}: SKH01 {-0.5*W_SKH*bound_s[a]:>7.2%}") + print(f" TOTALE LIBRO {-squeeze_max:>7.2%} realizzato il {i_worst.date()}:" + f" {J['book'].min():+.2%} ({J['book'].min()/-squeeze_max:.0%} del bound)") + print(f" -> lo scenario B e' gia' stato VISSUTO quasi al suo massimo: il bound e' credibile.") + + print(f"\n (e) CONTROPROVA INDIPENDENTE β€” controfattuale sui movimenti VERI. Per ogni giorno") + print(f" storico si sostituisce a TP01 la piena convinzione e a SKH01 la posizione LONG") + print(f" su entrambi gli asset, e si applica il movimento realmente accaduto.") + CF = pd.concat({a: TPQ[a][["lev_max", "ret", "ret_low"]] for a in ASSETS}, axis=1).dropna() + tot = np.zeros(len(CF)) + tot_min = np.zeros(len(CF)) # stessa cosa valutata sul MINIMO intra-giorno + for a in ASSETS: + rr = CF[(a, "ret")].values + rl = CF[(a, "ret_low")].values + hm = np.minimum(CF[(a, "lev_max")].values, 1.0) # cap: 0.375h+0.125<=0.5 -> h<=1 + tot += 0.5 * W_TP * hm * rr + 0.5 * W_SKH * np.maximum(rr, -bound_l[a]) + tot_min += 0.5 * W_TP * hm * rl + 0.5 * W_SKH * np.maximum(rl, -bound_l[a]) + o = np.argsort(tot)[:5] + print(f" {'data':>12}{'perdita ctrf (chiusura)':>25}{'ctrf (minimo)':>16}{'BTC':>10}{'ETH':>10}") + for i in o: + print(f" {str(CF.index[i].date()):>12}{tot[i]:>25.2%}{tot_min[i]:>16.2%}" + f"{CF[('BTC','ret')].values[i]:>10.1%}{CF[('ETH','ret')].values[i]:>10.1%}") + print(f"\n n = {len(CF)} giorni Β· giorni sotto -10% nel controfattuale:" + f" {int((tot<-0.10).sum())} ({(tot<-0.10).mean()*365:.2f}/anno) Β· sotto -5%:" + f" {int((tot<-0.05).sum())} ({(tot<-0.05).mean()*365:.2f}/anno)") + L_STRUCT = max(crash_max, -float(tot.min())) + L_STRUCT_MIN = -float(tot_min.min()) + print(f"\n -> DUE STRADE, STESSA RISPOSTA: il bound analitico da' {-crash_max:.2%},") + print(f" il controfattuale sui movimenti veri {tot.min():.2%}. **Si usa" + f" L_strutturale = {-L_STRUCT:.2%}** in CHIUSURA.") + print(f" E sul MINIMO intra-giorno (stesso controfattuale valutato al low della giornata,") + print(f" che e' la grandezza che guarda il margine): **{tot_min.min():.2%}**, cioe'" + f" {L_STRUCT_MIN/L_STRUCT:.2f}x la chiusura.") + print(f" ⚠️ Questo numero NON eredita il limite della sez.(3d): non passa dalla recon") + print(f" canonica, e' costruito sui low veri del feed certificato.") + print(f"\n πŸ“Œ LA RISPOSTA ALLA DOMANDA DEL BRIEF: il -10% NON e' fuori dal possibile β€”") + print(f" sta al {STRESS_A_MANO/L_STRUCT:.0%} del massimo strutturale. **Cio' che e' sbagliato non e'") + print(f" la taglia, e' la FREQUENZA**: il controfattuale β€” in cui la strategia") + print(f" difensiva e' massimamente in torto su entrambi gli asset β€” sfonda il -10%") + print(f" {int((tot<-0.10).sum())} giorno/i su {len(CF)}; il libro VERO non l'ha mai") + print(f" nemmeno avvicinato ({J['book'].min():+.2%}). **La riserva dello scettico e' mal") + print(f" tarata sulla frequenza, non falsificata sulla taglia.**") + zx = 1.5 + print(f"\n ⚠️ COSA NON E' CHIUSO: z_max e' esso stesso un massimo IN CAMPIONE. Con uno z") + print(f" piu' cattivo del {zx:g}x osservato il bound dello scenario A diventa" + f" {-(sum(zx*lim_tp[a] + 0.5*W_SKH*bound_l[a] for a in ASSETS)):.2%}.") + print(f" Il bound e' condizionato a 'nessun giorno peggiore in z di quelli visti', e") + print(f" va citato cosi'. Non e' un massimo assoluto: e' un massimo CONDIZIONATO.") + + # ---- il cap di config + print(f"\n (f) 🚨 IL CAP DI CONFIG NON E' UN KNOB DI SCALA β€” e questo cambia la DOMANDA.") + print(f" La regola del progetto dice che un cambio di SCALA passa dal cap di config e non") + print(f" da `target_vol`. Ma il cap e' un CLAMP, non un moltiplicatore: agisce solo nei") + print(f" giorni in cui il libro vuole PIU' del cap. Quanti sono?") + print(f" {'asset':>7}{'giorni':>9}{'net max voluto':>17}{'giorni sopra cap 0.50':>24}{'quota':>10}") + for a in ASSETS: + h = TPQ[a]["held"].values + net = 0.5 * W_TP * h + 0.5 * W_SKH # SKH long = il caso che spinge di piu' + n_over = int((net > frac).sum()) + print(f" {a:>7}{len(h):>9}{net.max():>17.4f}{n_over:>24}{n_over/len(h):>10.2%}") + print(f"\n -> **il cap morde su 3 giorni su ~2.900, e per meno dell'1% del target.** Alzare") + print(f" `max_notional_per_asset_frac` da 0.50 a 0.625 NON produce k=1,25: produce") + print(f" k=1,00 in 2.928 giorni su 2.931 e un pelo di piu' in tre. **Il gradino di leva") + print(f" NON e' oggi implementabile con la strada che la regola dichiara pulita**, e in") + print(f" `config/live.json` NON esiste una chiave di scala: servirebbe toccare") + print(f" `WEIGHT`/`W_TP01`/`W_SKH` in `src/live/book.py` (codice, non config) oppure") + print(f" aggiungere una chiave nuova. **Due mesi di discussione su un knob inesistente.**") + print(f" ⚠️ E il moltiplicatore x1,25 sui pesi e' un puro cambio di scala SOLO se applicato") + print(f" a entrambi: applicato a uno solo e' un tilt, e li' `weights_tilt_null` c'e' gia'.") + + # ---------------------------------------------------------------- (6) + sezione("(6) k* ONESTO β€” la curva di Kelly con la coda STIMATA al posto di quella a mano") + f_close = fits["CHIUSURA"] + drift_pt = mu * ANN + scenari_drift = (("punto (de-luck x0.89)", drift_pt), + ("-1 errore standard", drift_pt - se_drift), + ("-2 errori standard", drift_pt - 2 * se_drift)) + code = (("empirica (nessun modello)", None), + (f"EVT troncata a {-L_STRUCT:.1%} (strutturale)", L_STRUCT), + (f"EVT troncata a {-STRESS_A_MANO:.0%} (a mano)", STRESS_A_MANO), + (f"EVT troncata a {-1.5*L_STRUCT:.1%} (z x1.5)", 1.5 * L_STRUCT)) + print(f"\n Errore standard del drift = vol/sqrt(T) = {se_drift:.2%}/anno su" + f" {len(r_del)/ANN:.1f} anni.") + print(f" La coda EVT ha supporto ILLIMITATO se xi>0 -> E[log(1+kR)] sarebbe -inf per ogni k:") + print(f" il supporto lo da' l'ARITMETICA della sezione (5). E' qui che i due pezzi si") + print(f" incastrano β€” l'EVT da' la frequenza, la struttura da' il massimo.") + print(f"\n {'coda':>34}{'drift':>24}{'k*':>9}{'g(1.00x)':>11}{'g(1.25x)':>11}{'g(1.50x)':>11}") + ks_tab = {} + NDRAW = 400_000 + # numeri casuali APPAIATI fra tutte le celle: la tabella e' un confronto, non 12 stime + # indipendenti. Senza questo, differenze di Β±0,5x fra celle sono rumore MC e sembrano segnale. + _rg = np.random.default_rng(SEED + 1) + RND = {"ib": _rg.integers(0, 10 ** 9, NDRAW), "um": _rg.random(NDRAW), + "ut": _rg.random(NDRAW)} + for nome_c, lmax in code: + for nome_d, dr in scenari_drift: + if lmax is None: + rr = r_del - r_del.mean() + dr / ANN + else: + rr = campione_semiparam(r_del, f_close, lmax, NDRAW, dr, RND) + kk, _ = k_star(rr) + ks_tab[(nome_c, nome_d)] = kk + print(f" {nome_c:>34}{nome_d:>24}{kk:>8.2f}x{g_exact(rr,1.00):>11.2%}" + f"{g_exact(rr,1.25):>11.2%}{g_exact(rr,1.50):>11.2%}") + + print(f"\n -> **LA FORMA DELLA CODA QUASI NON CONTA, IL DRIFT CONTA TUTTO.** Fra la coda") + print(f" empirica e una GPD troncata a {-1.5*L_STRUCT:.0%} k* si muove di ~1-2 unita'; fra il") + print(f" drift di punto e quello a -2 SE si muove di {ks_tab[('empirica (nessun modello)','punto (de-luck x0.89)')]-ks_tab[('empirica (nessun modello)','-2 errori standard')]:.1f}. **Modellare meglio la coda non e'") + print(f" la leva: la leva e' quanti anni di storia ha il drift.** (Ed e' coerente col") + print(f" fatto gia' pubblicato che k* e' LINEARE nell'errore del drift.)") + print(f" ⚠️ Le celle sono APPAIATE (stessi numeri casuali): le differenze fra righe sono") + print(f" confrontabili, i livelli assoluti hanno l'incertezza del fit di coda.") + print(f"\n (b) LO STRESS A MANO DELLO SCETTICATO, riprodotto e poi ri-calibrato.") + print(f" growth_policy: 'un giorno -10% ALL'ANNO porta k* a 2x'.") + print(f" ⚠️ CONVENZIONE, ed e' quella che decide il numero: qui i giorni di coda si") + print(f" AGGIUNGONO e il drift NON si ri-centra. E' la lettura conservativa, la stessa") + print(f" implicita in growth_policy (che SOSTITUISCE giorni veri con il crash): se un") + print(f" -10% annuo esiste ed e' fuori campione, allora il drift osservato era FORTUNATO,") + print(f" e togliergli il tail event senza toccare il drift conterebbe due volte la") + print(f" fortuna. La tabella (a) sopra ri-centra di proposito, perche' li' il drift e' una") + print(f" COLONNA e si vuole isolare la forma della coda; qui no.") + p_evt = gpd_exceed(f_close, STRESS_A_MANO) + freq_cf = max((tot < -0.10).mean(), 1.0 / len(CF)) + print(f"\n {'evento':>36}{'freq':>16}{'drift':>9}{'k*':>8}{'1/2 Kelly':>11}" + f"{'g(1.00x)':>10}{'g(1.25x)':>10}{'g(1.50x)':>10}") + stress_k = {} + for lbl, p_giorno in ((f"-{STRESS_A_MANO:.0%} 1 volta l'ANNO (a mano)", 1.0 / ANN), + (f"-{STRESS_A_MANO:.0%} 1 ogni 5 anni (a mano, mite)", 1.0 / (5 * ANN)), + (f"-{STRESS_A_MANO:.0%} alla freq. del CONTROFATTUALE", freq_cf), + (f"-{STRESS_A_MANO:.0%} alla freq. stimata EVT", p_evt), + ("nessuno stress (riferimento)", 0.0)): + rr = r_del.copy() + n_add = int(round(p_giorno * len(rr) / max(1e-12, 1 - p_giorno))) if p_giorno > 0 else 0 + rr2 = np.concatenate([rr, np.full(n_add, -STRESS_A_MANO)]) if n_add > 0 else rr + kk, _ = k_star(rr2) + stress_k[lbl] = kk + fr = f"1 ogni {1/p_giorno:.0f}g" if p_giorno > 0 else "-" + print(f" {lbl:>36}{fr:>16}{rr2.mean()*ANN:>9.2%}{kk:>7.2f}x{0.5*kk:>10.2f}x" + f"{g_exact(rr2,1.00):>10.2%}{g_exact(rr2,1.25):>10.2%}{g_exact(rr2,1.50):>10.2%}") + k_mano = stress_k[f"-{STRESS_A_MANO:.0%} 1 volta l'ANNO (a mano)"] + KSG = (1.00, 1.50, 2.00, 3.50, 5.00, 10.00, 18.00) # la griglia COARSE di growth_policy + rr = np.concatenate([r_del, np.full(int(round(len(r_del)/(ANN-1))), -STRESS_A_MANO)]) + gg = [g_exact(rr, k) for k in KSG] + print(f"\n REPLICA PARZIALE, dichiarata: sulla griglia COARSE di growth_policy questo") + print(f" motore mette l'argmax a {KSG[int(np.nanargmax(gg))]:g}x (loro: 2x); a griglia fine {k_mano:.2f}x.") + print(f" **La direzione e l'ordine di grandezza si replicano** (k* crolla da" + f" {ks_emp:.1f}x a ~3x),") + print(f" la cella esatta no β€” e' la risoluzione MC che loro stessi hanno dichiarato.") + print(f"\n -> **LA TAGLIA DEL -10% COSTA POCO, LA FREQUENZA COSTA TUTTO.** Lo stesso evento,") + print(f" spostato dalla frequenza ASSUNTA a quella STIMATA, muove k* da {k_mano:.1f}x a" + f" {stress_k['nessuno stress (riferimento)']:.1f}x.") + print(f" Il parametro scelto a mano che blocca il gradino non e' '-10%': e' 'OGNI ANNO'.") + print(f"\n πŸ“Œ E IL PUNTO CHE CAMBIA LA DECISIONE: **anche accettando lo stress a mano**") + print(f" **alla lettera**, k* = {k_mano:.2f}x -> mezzo-Kelly = {0.5*k_mano:.2f}x. **1,25x ci sta") + print(f" sotto**, 1,50x e' esattamente al bordo. Cioe': la riserva dello scettico, presa") + print(f" per buona senza ri-calibrarla, non basta a bocciare il gradino a 1,25x β€”") + print(f" bocciava 1,50x, che e' il gradino di cui si stava discutendo.") + print(f"\n ⚠️ SENSITIVITA' FUORI DAL CRITERIO (dichiarata dopo, non conta per il verdetto):") + rr3 = np.concatenate([r_del - 2 * se_drift / ANN, + np.full(int(round(len(r_del) / (ANN - 1))), -STRESS_A_MANO)]) + k3, _ = k_star(rr3) + print(f" se si credono ENTRAMBE le riserve insieme (drift -2 SE **E** -10% ogni anno)") + print(f" il drift atteso diventa {rr3.mean()*ANN:+.2%}/anno e k* = {k3:.2f}x.") + print(f" **A drift {'negativo' if rr3.mean()<0 else 'quasi nullo'} il problema non e' il gradino: e' che il libro non") + print(f" andrebbe tradato affatto, nemmeno a 1,00x.** Chi usa questa combinazione per") + print(f" bocciare 1,25x sta implicitamente chiedendo di spegnere anche il conto di oggi:") + print(f" e' una posizione legittima, ma va dichiarata per quello che e'.") + + print(f"\n (c) LIQUIDAZIONE β€” la domanda va posta sul MINIMO, non sulla chiusura.") + print(f" Con nozionale lordo GΒ·E e margine di mantenimento {MM_RATE:.1%}, il conto e'") + print(f" liquidato quando il movimento avverso x soddisfa x β‰ˆ (1-mmΒ·G)/(GΒ·(1-mm)).") + g_avg = sum(0.5 * W_TP * TPQ[a]["held"].mean() for a in ASSETS) + W_SKH * 0.124 + g_max = sum(0.5 * W_TP * TPQ[a]["held"].max() + 0.5 * W_SKH for a in ASSETS) + print(f" lordo MEDIO del libro {g_avg:.3f}Β·E Β· lordo MASSIMO mai voluto {g_max:.3f}Β·E") + m_worst_unit = max(abs(gpd_quantile(fm, 1e-4)), L_STRUCT_MIN) + print(f" il 'peggior minimo' usato qui e' il PIU' PESSIMISTA fra EVT 1/10.000" + f" ({-abs(gpd_quantile(fm,1e-4)):.2%})") + print(f" e il bound strutturale sul minimo ({-L_STRUCT_MIN:.2%}) = {-m_worst_unit:.2%} a k=1.") + print(f"\n {'k':>7}{'G max':>9}{'x liquidaz.':>14}{'min vissuto':>14}" + f"{'min EVT 1/10^4':>16}{'min STRUTT.':>14}{'margine':>10}") + for k in (1.00, 1.25, 1.50, 2.00, 3.00): + G = min(g_max * k, 2 * frac * k) + x_liq = (1 - MM_RATE * G) / (G * (1 - MM_RATE)) + print(f" {k:>6.2f}x{G:>9.3f}{x_liq:>14.1%}{abs(m_acc.min())*k:>14.2%}" + f"{abs(gpd_quantile(fm,1e-4))*k:>16.2%}{L_STRUCT_MIN*k:>14.2%}" + f"{x_liq/(m_worst_unit*k):>9.1f}x") + kliq = WIPE / abs(m_acc.min()) + print(f"\n replica dello scetticato ('servirebbe 12,3x sul peggior giorno vissuto',") + print(f" convenzione WIPE {WIPE:.0%}): {WIPE:.2f}/{abs(m_acc.min()):.4f} = {kliq:.1f}x <-- riprodotto") + + # ---------------------------------------------------------------- (7) + sezione("(7) IL VERDETTO β€” il criterio di (0), valutato") + esiti = {} + + # C1 + c1_val = 1.25 * L_STRUCT + esiti["C1"] = (c1_val <= 0.50, + f"1,25 x {L_STRUCT:.2%} = {c1_val:.2%} di equity persa nel peggior giorno " + f"POSSIBILE, contro la soglia del 50%. Margine {0.50/c1_val:.1f}x.") + # C2 + k_pess = ks_tab[(f"EVT troncata a {-L_STRUCT:.1%} (strutturale)", "-2 errori standard")] + esiti["C2"] = (1.25 <= 0.5 * k_pess, + f"k* pessimistico (drift -2 SE, coda troncata al bound strutturale) = " + f"{k_pess:.2f}x -> mezzo-Kelly = {0.5*k_pess:.2f}x. 1,25 e' il " + f"{1.25/k_pess:.0%} di quel k*.") + # C3 + G125 = min(g_max * 1.25, 2 * frac * 1.25) + x_liq125 = (1 - MM_RATE * G125) / (G125 * (1 - MM_RATE)) + m_evt125 = m_worst_unit * 1.25 + esiti["C3"] = (x_liq125 >= 3 * m_evt125, + f"peggior minimo intra-giorno (il piu' pessimista fra EVT 1/10.000 " + f"{-abs(gpd_quantile(fm,1e-4)):.2%} e bound strutturale {-L_STRUCT_MIN:.2%}) " + f"x1,25 = {-m_evt125:.2%} contro una soglia di liquidazione a {x_liq125:.1%}: " + f"margine {x_liq125/m_evt125:.0f}x (richiesto 3x).") + # C4 + r_2se = r_del - r_del.mean() + (drift_pt - 2 * se_drift) / ANN + dg = g_exact(r_2se, 1.25) - g_exact(r_2se, 1.00) + esiti["C4"] = (dg > 0, + f"col drift a -2 SE ({drift_pt-2*se_drift:.2%}/anno): g(1,25x)-g(1,00x) = " + f"{dg:+.2%}/anno.") + # C5 + fund = {} + for a in ASSETS: + d = pd.read_parquet(ROOT / "data" / "raw" / f"hlfund_{a.lower()}_1h.parquet") + fund[a] = float(d["funding"].values.mean()) * 8760.0 + esp_long = sum(0.5 * W_TP * TPQ[a]["held"].mean() for a in ASSETS) + costo_k1 = esp_long * np.mean(list(fund.values())) + guad = g_exact(r_del, 1.25) - g_exact(r_del, 1.00) + esiti["C5"] = (0.25 * costo_k1 < guad / 3.0, + f"funding medio (proxy Hyperliquid, 3 anni): " + f"BTC {fund['BTC']:.1%}/a, ETH {fund['ETH']:.1%}/a; esposizione LONG media di " + f"TP01 {esp_long:.3f}Β·E -> costo {costo_k1:.2%}/anno a k=1, " + f"costo extra al gradino +{0.25*costo_k1:.2%}/anno contro un guadagno extra di " + f"{guad:+.2%}/anno (soglia dichiarata: costo extra < 1/3 del guadagno extra = " + f"{guad/3:.2%}). ⚠️ E il costo NON e' modellato NEMMENO a k=1: il libro paga gia' " + f"~{costo_k1:.2%}/anno che nessun backtest del progetto sottrae β€” e' un difetto " + f"che il gradino non crea ma amplifica del 25%.") + # C6 + n_cap = sum(int(((0.5 * W_TP * TPQ[a]["held"].values + 0.5 * W_SKH) > frac).sum()) + for a in ASSETS) + n_tot = sum(len(TPQ[a]) for a in ASSETS) + esiti["C6"] = (False, + f"il cap di config e' un CLAMP, non un moltiplicatore: morde su {n_cap} " + f"osservazioni-asset su {n_tot} ({n_cap/n_tot:.3%}). Alzarlo NON produce k=1,25. " + f"In `config/live.json` non esiste una chiave di scala.") + + print() + for k in ("C1", "C2", "C3", "C4", "C5", "C6"): + ok, spieg = esiti[k] + print(f" {k} {'PASSA ' if ok else 'FALLISCE'} {spieg}") + print() + + passa = all(v[0] for v in esiti.values()) + print(f" ESITO COMPLESSIVO: {sum(v[0] for v in esiti.values())}/6 criteri passati" + f" -> {'AUTORIZZABILE' if passa else 'NON autorizzabile come proposto'}") + + print(f""" + LETTURA IN CHIARO + + 1. IL CONTROLLO DEL CRITICO E' CONFERMATO NELLA DIREZIONE, CORRETTO NELLA PORTATA, E IL SUO + NUMERO NON SI RIPRODUCE. Sui 10 giorni peggiori del sottostante il libro fa + {worst_und.book.sum():+.2%} in totale contro {worst_und.und.sum():+.1%} del mercato e guadagna in + {int((worst_und.book>0).sum())} di essi; il 12 marzo 2020 (-43,6%) fa {J.loc[J.und.idxmin(),'book']:+.2%}. **Ma + sui 160 giorni di crash il libro perde in media {crash.book.mean():+.3%}**: 'il libro guadagna + nei crash' vale sugli ESTREMI, non in generale, e l'esposizione di TP01 scende in modo + monotono al crescere del crollo ({Jx[(Jx.und>-0.05)&(Jx.und<=-0.02)].espo.mean():.3f} nella fascia -5/-2%, + {Jx[Jx.und<=-0.20].espo.mean():.3f} sotto il -20%). **Il libro non e' immune ai crash: e' immune ai crash + GROSSI**, perche' li' il trend si e' gia' girato. + ⚠️ E la sua LENTE si identifica dai valori pubblicati (+0,97 e +0,72 compaiono SOLO + nella riga canonica): ha usato `book_series('canonical')`, il BACKTEST di SKH01. Su + quella lente **il peggior giorno E' un giorno di ribasso** ({b_can.min():+.2%}, {b_can.idxmin().date()}), + quindi la frase 'il peggior giorno non e' un giorno di crash' e' vera solo sulla lente + LIVE β€” che e' quella giusta per decidere, ma non e' quella da cui viene il suo numero. + **Aveva ragione, con la lente sbagliata.** Si cita **-3,47% (canonica) / -3,94% (live)**; + lo scarto residuo di 0,09pp col suo -3,38% e' dell'ordine del difetto gia' noto + ('`data/raw` gitignored, il cron riscrive i parquet' β€” stesso codice, dati diversi). + + 2. LA CODA DEL LIBRO ESISTE, MA STA DALL'ALTRA PARTE DEL MERCATO. Il peggior giorno in + chiusura e' {J['book'].min():+.2%} il {i_worst.date()}, ed e' un **RIMBALZO** (+20,9%/+27,6%): SKH01 + short con stop al 2% che gappa a {skh_only.min():.2%} di sleeve, mentre TP01 e' flat. Secondo + peggiore dello sleeve: {skh_only.nsmallest(2).iloc[-1]:.2%} β€” **la coda di SKH01 e' UN giorno solo**. + ⚠️ E la caratterizzazione si INVERTE con la lente: sulla canonica il peggior giorno e' + un ribasso ({b_can.min():+.2%}, {b_can.idxmin().date()}). Va citata con la lente. + + 3. LE DUE RISPOSTE ALLA DOMANDA 'QUAL E' IL PEGGIOR GIORNO' DIFFERISCONO DI ~2x: + **{K['R_live'].min():+.2%} in chiusura, {m_acc.min():+.2%} al minimo intra-giorno** (mediana del rapporto + sui giorni in perdita {np.median(m_acc[neg]/K['R_live'].values[neg]):.2f}x). Chi valuta margine, daily-loss o stop di + conto sulla chiusura sbaglia di quel fattore, nel verso ottimista. ⚠️ E il minimo del + giorno peggiore e' un LIMITE SUPERIORE: la recon accoppiata cappa il wick allo stop + proprio nel giorno del gap-through. + + 4. LA STIMA DELLA CODA, CON LA SUA INCERTEZZA: chiusura **{-gpd_quantile(f_close,1e-3):.2%} a 1/1.000 giorni** + e **{-gpd_quantile(f_close,1e-4):.2%} a 1/10.000** (GPD, soglia {EVT_Q:.0%}, IC95 bootstrap a blocchi + [{-np.percentile(evt_boot_close['rl10000'],97.5):.2%}, {-np.percentile(evt_boot_close['rl10000'],2.5):.2%}]); Hill dice {-(_l[_k]*((_k/(len(K)*1e-4))**_xi)):.2%} a 1/10.000. + **I due stimatori non concordano** (xi {f_close['xi']:.2f} vs {_xi:.2f}) e si cita la BANDA: un fit di coda + su {len(K)} osservazioni e' un ordine di grandezza, non un numero. Il -10% in chiusura esce + a **1 ogni {min(1/pe/ANN,1/pe_hill/ANN):.0f}-{max(1/pe/ANN,1/pe_hill/ANN):.0f} anni**, contro 'ogni anno' assunto. + + 5. IL CONTO ARITMETICO, che l'EVT non puo' fare perche' estrapola solo il meccanismo + vissuto: dato il cap di config, il vol-target di TP01 (che converte un crash nel suo + **z-score**, rendendo il bound invariante alla volatilita') e il gap-through di SKH01, + il peggior giorno POSSIBILE a k=1,00x e' **{-L_STRUCT:.2%}** in chiusura e **{-L_STRUCT_MIN:.2%}** al + minimo. Bound analitico e controfattuale sui movimenti veri danno lo stesso numero β€” + ⚠️ ma NON sono indipendenti: convergono perche' entrambi sono guidati dal 12 marzo 2020. + **Quindi il -10% sta al {STRESS_A_MANO/L_STRUCT:.0%} del massimo strutturale: la TAGLIA e' plausibile, + la FREQUENZA no.** La riserva dello scettico e' RIDIMENSIONATA di 2-4 ordini di + grandezza, non falsificata. + + 6. E ANCHE PRENDENDO LA RISERVA ALLA LETTERA IL GRADINO A 1,25x PASSA: con -10% ogni anno + k* = {k_mano:.2f}x, cioe' mezzo-Kelly = {0.5*k_mano:.2f}x. **1,25x ci sta sotto; 1,50x e' al bordo** β€” + cioe' lo stress a mano bocciava il gradino DOPPIO, non quello singolo. Se invece si + credono ENTRAMBE le riserve insieme (drift -2 SE **e** -10% annuo) il drift atteso e' + {rr3.mean()*ANN:+.2%}/anno: a quel punto non e' il gradino a essere sbagliato, e' il libro β€” e chi + usa quella combinazione sta chiedendo di spegnere anche il conto di oggi. + + 7. 🚨 IL RISULTATO CHE NESSUNO CERCAVA, ED E' QUELLO CHE DECIDE: **il gradino non e' + implementabile con la strada che la regola del progetto dichiara pulita.** Il cap di + `config/live.json` e' un CLAMP, non un moltiplicatore: morde su {n_cap} osservazioni-asset + su {n_tot} ({n_cap/n_tot:.3%}). Alzarlo da 0,50 a 0,625 lascerebbe il libro a k=1,00x + in {n_tot-n_cap} giorni-asset su {n_tot}. Non esiste una chiave di SCALA in config: per k=1,25 + servirebbe cambiare `WEIGHT`/`W_TP01`/`W_SKH` in `src/live/book.py`, cioe' CODICE su un + percorso con soldi veri. **La domanda 'il gradino e' autorizzabile' era mal posta: prima + di essere autorizzato, il gradino va COSTRUITO** β€” e la costruzione e' la parte che + merita una revisione, non l'aritmetica del rischio, che passa con margine. + + 8. SOTTOPRODOTTO NON RICHIESTO, e vale piu' del gradino: **il funding dei perpetual non e' + modellato in NESSUN backtest del progetto**. Proxy Hyperliquid su 3 anni: {np.mean(list(fund.values())):.1%}/anno + pagato dai long, esposizione long media di TP01 {esp_long:.3f}Β·E -> **~{costo_k1:.2%}/anno** che il + libro paga davvero e che nessuna serie sottrae. E' ~{costo_k1/(mu*ANN):.0%} del drift de-luckato, + cioe' dello stesso ordine della correzione d'ancora x0,89 che il progetto ha misurato + con cura. Non lo crea il gradino: il gradino lo amplifica del 25%. + + COSA MI SMENTIREBBE, in ordine di gravita' + (a) un giorno in cui il libro perde piu' di ~{L_STRUCT*0.7:.1%} in chiusura: vorrebbe dire che + l'aritmetica della sez.(5) ha una gamba che non ho contato (un terzo strumento, un + ordine non nettato, uno stop non eseguito, il cap non applicato); + (b) uno z giornaliero peggiore di {zmin['BTC']:.1f} su BTC o {zmin['ETH']:.1f} su ETH con vol >= {tv:.0%}: + il bound e' CONDIZIONATO a quel massimo in campione e scala linearmente con esso; + (c) una configurazione in cui TP01 tiene piena convinzione ENTRANDO in un crash: nel + campione non succede mai, ed e' l'unica ragione per cui il controfattuale sfonda il + -10% e la realta' no. La mia frequenza stimata assume che resti rara; + (d) SKH01 in un regime in cui sta short molto piu' a lungo: la coda del libro e' la sua, e + la misuro su un campione in cui e' fuori mercato l'87,6% del tempo; + (e) un funding medio molto diverso su Deribit-USDC rispetto al proxy Hyperliquid: il + punto 8 e' misurato su un venue che non e' quello dove il libro esegue. + + GATE β€” cosa e' girato e cosa no + Β· `marginal_vs_tp01` NON APPLICABILE: non c'e' un flusso nuovo, k e' uno scalare (corr 1,00). + Β· `deflated_sharpe` NON APPLICABILE: lo Sharpe e' INVARIANTE a k. Questo lavoro non chiude + il buco strutturale, lo AGGIRA: misura il peggior giorno, che e' + l'unica grandezza del rischio su cui la scala non e' invariante. + Β· `implausible_sharpe` NON GIRATO come funzione (darebbe lo stesso verdetto a ogni k). Il + controllo di plausibilita' e' stato fatto a mano: nessuna cella qui + promette zero perdite, e la sez.(5) e' proprio la ricerca del + 'rischio FUORI dal dataset' che quel gate cerca. + Β· null del de-levering NON e' un gate qui: e' l'OGGETTO (la leva percorsa nei due versi). + Β· fortuna d'ancora EREDITATA (x{DP.DELUCK} misurato). ⚠️ NON RI-GIRATA su questo taglio, e va + detto: il peggior giorno e' un evento SINGOLO, e una banda d'ancora + su un massimo non e' la stessa statistica di una banda su una media. + Cio' che ne risente e' il drift (gia' de-luckato), non il bound. + Β· causalita' EREDITATA: nessun segnale nuovo, solo serie gia' validate. + Β· replica di controllo SUPERATA: drift/vol/Sharpe/peggior giorno de-luckati (4/4), k* MC vs + forma chiusa, scomposizione libro=gambe (max|diff| 0,0e+00), + marginali del gap (p50/p90/p99 identiche), 12,3x di liquidazione. + PARZIALE su 'k*=2x sotto stress' (qui 2,95x fine / 3,5x sulla loro + griglia): stessa direzione e ordine, cella diversa. + Β· risoluzione la curva di crescita e' in forma CHIUSA (E[log]): zero rumore MC. + Le celle della tabella (6a) sono APPAIATE. L'incertezza residua e' + quella del FIT di coda, ed e' negli IC bootstrap e nella banda + fra stimatori. + """) + n_boot_tot = 2 * EVT_BOOT + n_celle = (2 * len(EVT_Q_SENS)) + n_boot_tot + 2 + 2 + len(code) * len(scenari_drift) \ + + 5 + len(KSG) + 1 + 5 + 3 + 2 + print(f""" + GRIGLIA DICHIARATA β€” {n_celle} valutazioni, contate al RIALZO: + {2*len(EVT_Q_SENS)} fit GPD (2 serie x {len(EVT_Q_SENS)} soglie) Β· {n_boot_tot} ri-fit bootstrap Β· 2 Hill Β· 2 t di Student + {len(code)*len(scenari_drift)} celle k* (coda x drift, APPAIATE) Β· 5 righe di stress Β· {len(KSG)} celle della griglia + coarse di growth_policy Β· 1 sensitivita' combinata Β· 5 righe di liquidazione Β· + 3 lenti del peggior giorno Β· 2 varianti di controfattuale (chiusura e minimo). + ⚠️ **Nessuna cella e' stata SELEZIONATA**: la soglia EVT e' dichiarata a priori ({EVT_Q:.0%}) e le + altre tre girano come sensitivita'; il criterio di (0) e' congelato prima delle sezioni 4-6. + Percio' non c'e' un deflated-Sharpe da calcolare: non c'e' un massimo scelto su cui barare. + """) + print("\n" + "=" * 110) + print(" IL GRADINO DI LEVA A 1,25x: AUTORIZZABILE A CONDIZIONE DI COSTRUIRE PRIMA UNA CHIAVE DI SCALA ESPLICITA IN CONFIG") + print("=" * 110) + print(f"\n [tempo totale {time.time()-t_start:.0f}s]") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822d_cc01_2022.py b/scripts/research/r0822d_cc01_2022.py new file mode 100644 index 0000000..a1ddf0c --- /dev/null +++ b/scripts/research/r0822d_cc01_2022.py @@ -0,0 +1,1044 @@ +#!/usr/bin/env python3 +""" +r0822d_cc01_2022.py β€” filone CC01-2022 (ondata 2026-08-22, terza tornata) + + RIAPRIRE UN FILONE CHIUSO SULL'UNICA BASE CHE QUESTO PROGETTO ACCETTA: DATI NUOVI. + +IL FILONE E LA SUA RAGIONE DI MORTE + CC01 (cash-and-carry delta-neutral, `scripts/research/cash_carry_hl.py`) fu chiuso il + 2026-06-26 con un verdetto molto specifico: il premio di funding e' REALE (~+8-14%/anno, + positivo ogni anno, ortogonale a TP01), ma lo Sharpe modellato 11-13 e' un ARTEFATTO + perche' i rischi di coda sono *strutturalmente fuori dal dataset*: + + "lo storico funding parte dal 2023-05 -> manca il 2022 (deleveraging, funding + negativo, basis blow-out)." + + Quella e' una ragione FALSIFICABILE, non un giudizio: dice esattamente quale dato + ribalterebbe la conclusione. Fino a stamattina quel dato non c'era. + +COSA E' CAMBIATO OGGI + Il filone BASIS-CALENDAR (r0822_basis_calendar.py) ha ricostruito da Deribit pubblico + i 30/30 futures trimestrali SCADUTI (i nomi sono deterministici, e + `get_tradingview_chart_data` serve la storia di un contratto scaduto) piu' il funding + e l'indice orario del perpetual: **dal 2019-04, col deleveraging 2022 dentro**. + E' esattamente la coda che a CC01 mancava per costruzione. + +LE QUATTRO DOMANDE (in ordine di importanza, non di comodita') + Q1 Come si comporta il carry NEL 2022? Si comprime, si annulla, o si INVERTE? + Quanto dura e quanto e' profondo il peggior episodio? + Q2 Con la coda dentro, lo Sharpe 11-13 quanto diventa? E `implausible_sharpe` β€” + il gate che nacque proprio da CC01 β€” segnala ancora? + Q3 I due muri NON strutturali ("non eseguibile a $600", "procyclico") reggono + coi numeri di oggi, su Deribit, a due gambe? + Q4 Il rischio che ha ucciso CC01 e' il MODELLO, non i dati: un carry delta-neutral + sembra liscio perche' il P&L a scadenza non vede il ROLL ne' il MARGINE. In un + blow-out lo short perde SUBITO e il guadagno arriva a scadenza: se il conto non + regge il margine, la posizione muore nel punto peggiore. + +COSA QUESTO SCRIPT *NON* RIFA' + Il premio a termine (implicito +7,33% contro funding realizzato +6,48% -> incassabile + +0,85%/anno con IC95 che contiene lo zero) e' gia' misurato dal filone BASIS-CALENDAR + e non viene ri-misurato qui. Qui la domanda e' un'altra: **il 2022**. + +CONVENZIONI DICHIARATE + * Nessuna serie di prezzi viene mai incollata: la posizione vive su un CONTRATTO, e al + roll si chiude e si riapre pagando il round-trip. Si incollano solo i RENDIMENTI. + * I futures datati Deribit sono INVERSE (settlement BTC/ETH): il P&L e' calcolato nella + convenzione del venue (BTC) e riportato in USD all'indice. La versione lineare e' + calcolata accanto, e la differenza si misura invece di assumerla trascurabile. + * Fee taker REALE letta dal venue: 3,5 bps/lato per gamba. Slippage dichiarato a parte. + * Leva per-gamba lambda=1 (nozionale per gamba = equity; lordo 2x) salvo dove indicato. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0822d_cc01_2022.py + +RETE: questo script NON scarica nulla (il dato e' su disco). L'unica chiamata opzionale e' +la lettura delle specifiche strumento, che e' gia' in cache; se manca, si dichiara. +""" +from __future__ import annotations + +import calendar +import datetime as dt +import json +import math +import os +import shutil +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import altlib as A # noqa: E402 + +# ------------------------------------------------------------------ costanti +UTC = dt.timezone.utc +MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", + "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] +ASSETS = ("BTC", "ETH") +HOURS_Y = 365.25 * 24 +SQ365 = math.sqrt(365.25) + +FEE_SIDE = 0.00035 # taker Deribit letto dal venue (get_instruments), per gamba +SLIP_BPS = 2.0 # slippage dichiarato sul datato (meta' spread del front, vedi [6]) +ANNUAL_COST_CC01 = 0.02 # il drag annuo che CC01 usava: si tiene IDENTICO per replicare + +# Margine: DICHIARATO, fonte secondaria (documentazione Deribit futures). Si fa la +# sensibilita' invece di fingere precisione: il risultato e' una LEVA, e la leva scala +# come 1/(mm_tot + |MAE|) -> con |MAE| >> mm_tot il valore di mm quasi non conta. +MM_LEG = 0.00575 # maintenance margin per gamba (0,575% del nozionale) +IM_LEG = 0.01 # initial margin per gamba (1%) + +# Finestre +W_CC01 = pd.Timestamp("2023-05-12", tz="UTC") # inizio dello storico funding di CC01 (HL) +HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC") + +FUT_DIR = ROOT / "data" / "raw" / "fut_deribit" +SCRATCH_CACHE = Path(os.environ.get( + "R0822_SCRATCH", + "/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad")) / "basis_cache" + +_line = "=" * 96 + + +def hr(t: str) -> None: + print("\n" + _line) + print(t) + print(_line) + + +# ========================================================================== +# 0. PERSISTENZA β€” il dato viveva SOLO in uno scratchpad di sessione +# ========================================================================== +def last_friday(year: int, month: int) -> dt.date: + d = dt.date(year, month, calendar.monthrange(year, month)[1]) + while d.weekday() != 4: + d -= dt.timedelta(days=1) + return d + + +def quarterly_names(asset: str, y0: int = 2018, y1: int = 2027): + out = [] + for y in range(y0, y1 + 1): + for m in (3, 6, 9, 12): + lf = last_friday(y, m) + out.append((f"{asset}-{lf.day}{MONTHS[m - 1]}{str(y)[2:]}", + pd.Timestamp(lf.year, lf.month, lf.day, 8, tz="UTC"))) + return out + + +def persist_dataset() -> dict: + """Copia il dataset dallo scratchpad a `data/raw/fut_deribit/` se non c'e' gia'. + + NAMESPACE β€” la lezione SOL del 22/08: `load_data` NON ha una whitelist, il guardrail + "solo dati certi" vive nell'ASSENZA di un file, e mettere un parquet nel namespace + NUDO (`btc_1h.parquet`) lo disattiva in silenzio. Qui i file stanno in una + SOTTOCARTELLA con prefisso `fut_` (accanto a `hl_`/`eq_`/`eqx_`/`alt_`/`fut_`), quindi + (a) nessun nome puo' collidere con `{asset}_{tf}.parquet` e (b) il guardrail resta + verificabile, e viene VERIFICATO sotto, non assunto. + """ + hr("[0] PERSISTENZA DEL DATASET β€” 20 MB che vivevano solo in uno scratchpad di sessione") + FUT_DIR.mkdir(parents=True, exist_ok=True) + have = sorted(p.name for p in FUT_DIR.glob("*.parquet")) + copied = 0 + if SCRATCH_CACHE.exists(): + for src in sorted(SCRATCH_CACHE.glob("ohlc_*.parquet")): + dst = FUT_DIR / src.name[len("ohlc_"):] + if not dst.exists(): + shutil.copy2(src, dst) + copied += 1 + for a in ASSETS: + src = SCRATCH_CACHE / f"fund_{a}.parquet" + dst = FUT_DIR / f"fund_{a}.parquet" + if src.exists() and not dst.exists(): + shutil.copy2(src, dst) + copied += 1 + print(f" scratchpad presente ({SCRATCH_CACHE}) -> copiati {copied} file nuovi") + else: + print(f" scratchpad ASSENTE ({SCRATCH_CACHE}) β€” si usa cio' che e' gia' su disco") + + files = sorted(FUT_DIR.glob("*.parquet")) + if not files: + print("\n ERRORE: nessun dato. Il dataset si ricostruisce da Deribit pubblico con") + print(" scripts/research/r0822_basis_calendar.py (fetcher rate-limited, ~45 min).") + raise SystemExit(2) + + rows, bad = 0, [] + per_asset = {a: dict(contratti=0, barre=0) for a in ASSETS} + for f in files: + try: + n = len(pd.read_parquet(f, columns=["ts"])) + except Exception as exc: # noqa: BLE001 + bad.append((f.name, type(exc).__name__)) + continue + rows += n + if f.name.startswith("fund_"): + continue + a = f.name.split("-")[0] + if a in per_asset: + per_asset[a]["contratti"] += 1 + per_asset[a]["barre"] += n + tot_mb = sum(f.stat().st_size for f in files) / 1e6 + print(f" {FUT_DIR.relative_to(ROOT)}: {len(files)} file, {tot_mb:.1f} MB, {rows:,} righe") + for a in ASSETS: + print(f" {a}: {per_asset[a]['contratti']} file di trimestrali, " + f"{per_asset[a]['barre']:,} barre orarie (uno di questi ha <200 barre e il") + print(f" caricatore lo scarta: sotto ne compaiono 34, non e' un dato mancante)") + if bad: + print(f" ⚠️ file illeggibili: {bad}") + + # ---- GUARDRAIL: verificato, non assunto ----------------------------- + from src.data.downloader import load_data, DATA_DIR # noqa: PLC0415 + esiti = [] + for nm, _ in quarterly_names("BTC")[:2] + quarterly_names("ETH")[:2]: + for tf in ("1h", "5m"): + p = DATA_DIR / f"{nm.lower()}_{tf}.parquet" + esiti.append(p.exists()) + collide = any(esiti) + try: + load_data("BTC-30SEP22", "1h") + guard = "ROTTO (load_data ha trovato un contratto!)" + except FileNotFoundError: + guard = "INTATTO (FileNotFoundError)" + ok_btc = True + try: + load_data("BTC", "1h") + except Exception: # noqa: BLE001 + ok_btc = False + print(f" guardrail load_data('BTC-30SEP22') -> {guard} | " + f"collisioni nel namespace nudo: {'SI' if collide else 'NO'} | " + f"load_data('BTC','1h') ancora ok: {'SI' if ok_btc else 'NO'}") + print(" ⚠️ DICHIARATO: il cron NON rinfresca questi file (cron_daily fa BTC/ETH, DVOL, HL,") + print(" equity, catena β€” non i trimestrali). Sono un'ISTANTANEA al 2026-08-22, e il") + print(" contratto in corso (BTC/ETH-25SEP26) e' TRONCATO a oggi, non a scadenza.") + print(" ⚠️ DICHIARATO: `data/raw/` e' gitignored e `do_pythagoras` in backup.sh salva solo") + print(" il dato NON ricostruibile β€” questo e' ricostruibile (~2.700 chiamate pubbliche),") + print(" quindi resta fuori dal backup di proposito. Se sparisce, si rifa'.") + return dict(files=len(files), mb=tot_mb, rows=rows, guard_ok=("INTATTO" in guard and not collide)) + + +# ========================================================================== +# 1. CARICAMENTO +# ========================================================================== +def load_futures(asset: str): + closes, exps = {}, {} + for nm, e in quarterly_names(asset): + f = FUT_DIR / f"{nm}.parquet" + if not f.exists(): + continue + d = pd.read_parquet(f, columns=["ts", "close"]) + if len(d) < 200: + continue + s = d.set_index("ts")["close"].astype(float) + s = s[s > 0] + if len(s) < 200: + continue + closes[nm], exps[nm] = s, e + wide = pd.DataFrame(closes).sort_index() + return wide, exps + + +def load_fund(asset: str) -> pd.DataFrame: + return pd.read_parquet(FUT_DIR / f"fund_{asset}.parquet").set_index("ts").sort_index() + + +def load_perp(asset: str) -> pd.Series: + d = A.get(asset, "1h") + return d.set_index("datetime")["close"].astype(float).sort_index() + + +# ========================================================================== +# 2. METRICHE +# ========================================================================== +def to_daily(h: pd.Series) -> pd.Series: + return ((1.0 + h.fillna(0.0)).resample("1D").prod() - 1.0).dropna() + + +def stats(daily: pd.Series) -> dict: + r = daily.dropna() + if len(r) < 30 or r.std() == 0: + return dict(sharpe=0.0, hold=float("nan"), maxdd=0.0, cagr=float("nan"), + n=len(r), vol=0.0, dd_days=0, worst_month=float("nan")) + sh = float(r.mean() / r.std() * SQ365) + eq = (1 + r).cumprod() + peak = eq.cummax() + ddser = (peak - eq) / peak + dd = float(ddser.max()) + under = (eq < peak) + # durata del piu' lungo periodo SOTT'ACQUA (in giorni di calendario) + dur, best, start = 0, 0, None + for t, u in under.items(): + if u and start is None: + start = t + elif not u and start is not None: + best = max(best, (t - start).days) + start = None + if start is not None: + best = max(best, (under.index[-1] - start).days) + dur = best + yrs = max((r.index[-1] - r.index[0]).days / 365.25, 1e-9) + cagr = float(eq.iloc[-1] ** (1 / yrs) - 1) if eq.iloc[-1] > 0 else float("nan") + ho = r[r.index >= HOLDOUT] + sho = float(ho.mean() / ho.std() * SQ365) if len(ho) > 30 and ho.std() > 0 else float("nan") + wm = float((((1 + r).resample("ME").prod() - 1)).min()) + return dict(sharpe=sh, hold=sho, maxdd=dd, cagr=cagr, n=len(r), + vol=float(r.std() * SQ365), dd_days=dur, worst_month=wm) + + +def show(label: str, daily: pd.Series, extra: str = "") -> dict: + m = stats(daily) + print(f" {label:<38s} Sh {m['sharpe']:+6.2f} hold {m['hold']:+6.2f} " + f"CAGR {m['cagr']*100:+6.2f}% maxDD {m['maxdd']*100:5.2f}% " + f"peggior mese {m['worst_month']*100:+6.2f}% sott'acqua {m['dd_days']:4d}g " + f"n={m['n']:,}{extra}") + return m + + +def per_year(daily: pd.Series) -> pd.Series: + return (1 + daily).groupby(daily.index.year).prod() - 1 + + +# ========================================================================== +# 3. Q1 β€” IL 2022, cioe' l'anno che CC01 non aveva +# ========================================================================== +def q1_descrittiva(F: dict, W: dict, E: dict) -> pd.DataFrame: + hr("[1] Q1 β€” IL 2022: il carry si comprime, si annulla, o si INVERTE?") + tab = {} + for a in ASSETS: + fu = F[a] + f1 = fu["f1h"].dropna() + d = f1.resample("1D").sum() + wide, exps = W[a], E[a] + idx = wide.index + # basis del front (trimestrale piu' vicino con dte >= 3g) vs INDICE + order = sorted(exps, key=lambda c: exps[c]) + TAU = np.column_stack([(exps[c] - idx).total_seconds().values / 3600.0 for c in order]) + AV = np.column_stack([wide[c].notna().values for c in order]) + AL = AV & (TAU >= 3 * 24) + ok = AL.any(axis=1) + k1 = np.argmax(AL, axis=1) + Fv = np.where(ok, wide[order].values.astype(float)[np.arange(len(idx)), k1], np.nan) + tau = np.where(ok, TAU[np.arange(len(idx)), k1], np.nan) + ix = fu["index"].reindex(idx).ffill(limit=2).values + basis = pd.Series(np.log(Fv / ix) * HOURS_Y / tau, index=idx).replace([np.inf, -np.inf], np.nan) + for y in range(2019, 2027): + dy = d[d.index.year == y] + by = basis[basis.index.year == y].dropna() + if len(dy) < 20 or len(by) < 200: + continue + tab[(a, y)] = dict( + funding_anno=float(dy.sum()), + giorni_f_neg=float((dy < 0).mean()), + basis_med=float(by.median()), + backwardation=float((by < 0).mean()), + ) + T = pd.DataFrame(tab).T + print("\n funding REALIZZATO del perpetual Deribit (somma dell'anno) e BASIS del trimestrale front") + print(" (il carry classico INCASSA il funding: un anno negativo e' un anno in cui PAGA)\n") + print(f" {'':6s} {'anno':>5s} {'funding anno':>13s} {'gg f<0':>8s} " + f"{'basis mediano':>14s} {'ore in backwardation':>21s}") + for (a, y), r in T.iterrows(): + flag = " <<< il 2022" if y == 2022 else "" + print(f" {a:6s} {y:5d} {r.funding_anno*100:+12.2f}% {r.giorni_f_neg*100:7.1f}% " + f"{r.basis_med*100:+13.2f}% {r.backwardation*100:20.1f}%{flag}") + + # episodio peggiore del carry PURO (r = funding), per asset + print("\n peggior EPISODIO del carry puro (r = funding, senza costi): " + "run consecutivo piu' negativo") + for a in ASSETS: + d = F[a]["f1h"].dropna().resample("1D").sum() + cum = d.cumsum() + run_peak = cum.cummax() + under = cum - run_peak + i = under.idxmin() + j = run_peak[:i].idxmax() + print(f" {a}: {float(under.min())*100:+.2f}% di funding cumulato perso fra " + f"{j.date()} e {i.date()} ({(i-j).days} giorni)") + print("\n ⚠️ E il 2022 non e' nemmeno l'unico buco: lo storico di CC01 (dal 2023-05)") + print(" non conteneva neppure MARZO 2020 β€” che sotto risulta il mese peggiore di tutti.") + return T + + +# ========================================================================== +# 4. Q2 β€” LA REPLICA DI CC01 CON IL 2022 DENTRO +# ========================================================================== +def cc01_daily(FUNDD: pd.DataFrame, mode: str = "static", trail: int = 14, + cost: float = ANNUAL_COST_CC01, SIG: pd.DataFrame | None = None) -> pd.Series: + """IL MODELLO DI CC01, IDENTICO: r = funding (long spot / short perp), meno un drag + annuo fisso. `FUNDD` = [giorno x asset] di RITORNO giornaliero della gamba. + `gated` = harvest solo se il funding trailing CAUSALE e' positivo. + + ⚠️ `SIG` esiste perche' in CC01 il gate guarda il FUNDING anche quando il ritorno + include il mark-to-market della base (`leg_raw = FUND - PREM.diff()` ma + `sig = FUND.rolling(...)`). Gatare sul ritorno con la base dentro significa gatare + sul RUMORE del premio: prima stesura, e produceva -35%/anno di CAGR.""" + SIG = FUNDD if SIG is None else SIG.reindex(FUNDD.index) + daily_cost = cost / 365.25 + if mode == "gated": + sig = SIG.rolling(trail, min_periods=trail // 2).mean().shift(1) + act = (sig > 0).astype(float) + n = act.sum(axis=1).replace(0, np.nan) + gross = ((FUNDD * act).sum(axis=1) / n).fillna(0.0) + drag = (act.sum(axis=1) > 0).astype(float) * daily_cost + else: + gross = FUNDD.mean(axis=1) + drag = daily_cost + return (gross - drag).dropna() + + +def q2_replica(F: dict, PP_GLOBAL: dict) -> dict: + hr("[2] Q2 β€” LA REPLICA DI CC01: prima riprodurre il numero VECCHIO, poi produrre il nuovo") + out = {} + + # (a) il numero vecchio, con la macchina vecchia e i dati vecchi (Hyperliquid) + print("\n (a) CONTROLLO DI REPLICA β€” cash_carry_hl.py tale e quale (funding Hyperliquid 2023-05+)") + try: + import importlib.util + spec = importlib.util.spec_from_file_location( + "cc_old", ROOT / "scripts" / "research" / "cash_carry_hl.py") + cc_old = importlib.util.module_from_spec(spec) + spec.loader.exec_module(cc_old) # type: ignore + for mode in ("static", "gated"): + r = cc_old.cc_returns(["BTC", "ETH"], mode=mode) + out[f"HL_{mode}"] = r + m = cc_old.metrics(r) + print(f" HL {mode:7s}: Sharpe {m['sharpe']:+6.2f} CAGR {m['cagr']*100:+.1f}% " + f"maxDD {m['dd']*100:.2f}% ({r.index[0].date()} .. {r.index[-1].date()})") + print(" -> il '11-13' del 26/06 e' RIPRODOTTO. Ogni delta sotto e' un delta vero.") + except Exception as exc: # noqa: BLE001 + print(f" ⚠️ replica non eseguibile ({type(exc).__name__}: {exc}) β€” i dati HL sono su disco?") + + # (b) STESSO MODELLO, funding DERIBIT: prima sulla finestra di CC01 (controllo di venue), + # poi sulla finestra piena che contiene il 2022. + FUNDD = pd.concat({a: F[a]["f1h"].dropna().resample("1D").sum() for a in ASSETS}, + axis=1).sort_index().dropna() + print(f"\n (b) STESSO MODELLO su funding DERIBIT ({FUNDD.index[0].date()} .. " + f"{FUNDD.index[-1].date()}, {len(FUNDD):,} giorni)") + print(" NB: e' un CONTROLLO DI VENUE, non una replica β€” HL e Deribit sono due mercati.\n") + for mode in ("static", "gated"): + full = cc01_daily(FUNDD, mode) + win = full[full.index >= W_CC01] + out[f"DBT_{mode}_full"] = full + out[f"DBT_{mode}_win"] = win + show(f"DERIBIT {mode} β€” finestra CC01", win) + show(f"DERIBIT {mode} β€” FINESTRA PIENA", full, extra=" <<< col 2022 dentro") + print() + + print(" per anno (rendimento del carry puro, netto del drag 2%/anno di CC01):") + py = per_year(out["DBT_static_full"]) + print(" " + " ".join(f"{y}:{v*100:+.1f}%" for y, v in py.items())) + pyg = per_year(out["DBT_gated_full"]) + print(" gated: " + " ".join(f"{y}:{v*100:+.1f}%" for y, v in pyg.items())) + g22 = float(pyg.get(2022, float("nan"))) + s22 = float(py.get(2022, float("nan"))) + print(f"\n ⚠️ IPOTESI MIA, SMENTITA DALLA MISURA: avevo scritto che il gate causale di CC01") + print(f" (funding trailing 14g > 0) 'entra ed esce col ritardo, quindi il 2022 lo") + print(f" attraversa comunque'. Falso: 2022 static {s22*100:+.1f}% contro gated " + f"{g22*100:+.1f}% = il gate") + print(f" taglia il {100*(1-abs(g22)/max(abs(s22),1e-9)):.0f}% della perdita dell'anno. " + f"Il funding negativo e' PERSISTENTE (53-59% dei") + print(f" giorni del 2022), quindi un trailing lento basta a stare fuori.") + print(f" -> La conseguenza NON e' che CC01 e' salvo: e' che **il buco del 2022 non era") + print(f" il buco principale**. Il gated con il 2022 dentro fa ancora Sharpe " + f"{stats(out['DBT_gated_full'])['sharpe']:+.2f} con") + print(f" maxDD {stats(out['DBT_gated_full'])['maxdd']*100:.2f}% β€” cioe' e' ANCORA una serie senza coda sinistra.") + + # (c) la variante che CC01 chiamava 'onesta': r = funding - Delta(premio perp/indice). + # E' il modello di un long-spot/short-perp VERO su un venue che abbia spot. + prem = {a: (PP_GLOBAL[a].reindex(F[a].index) / F[a]["index"] - 1.0) for a in ASSETS} + out["_FUNDD"] = FUNDD + print("\n (c) VARIANTE CON MARK-TO-MARKET DELLA BASE (r = funding - Delta premio perp/indice)") + print(" e' il modello di un long-spot/short-perp VERO (su un venue che abbia spot).") + print(" ⚠️ DUE LENTI, perche' il 'premio' non e' un oggetto unico: quello GREZZO") + print(" (chiusura del perp / indice, ora per ora) contiene le dislocazioni di") + print(" microstruttura; quello LISCIO (mediana 24h) e' l'analogo del `premium` che") + print(" Hyperliquid pubblica e che CC01 usava β€” ed e' PIU' MITE per costruzione.") + for lente, sm in (("grezzo", 1), ("liscio 24h", 24)): + DPR = pd.concat({a: (prem[a].rolling(sm, min_periods=1).median() if sm > 1 else prem[a]) + .resample("1D").last().diff() for a in ASSETS}, axis=1).sort_index() + FB = (FUNDD - DPR.reindex(FUNDD.index)).dropna() + for mode in ("static", "gated"): + fb = cc01_daily(FB, mode, SIG=FUNDD) # il GATE guarda il funding, come CC01 + key = "" if sm == 1 else "_sm" + out[f"DBT_{mode}_basis{key}_full"] = fb + show(f"{mode} + base ({lente}) β€” PIENA", fb) + out[f"DBT_{mode}_basis{key}_win"] = fb[fb.index >= W_CC01] + show(f"{mode} + base ({lente}) β€” fin. CC01", out[f"DBT_{mode}_basis{key}_win"]) + print() + # DIAGNOSTICA: la liscezza si MISURA, non si asserisce. + try: + from src.portfolio.sleeves import _HL_DIR # noqa: PLC0415 + rows = [] + for a in ASSETS: + fp = _HL_DIR / f"hlfund_{a.lower()}_1h.parquet" + if not fp.exists(): + continue + hl = pd.read_parquet(fp)["premium"].resample("1D").last().diff().dropna() + raw = prem[a].resample("1D").last().diff().dropna() + sm = prem[a].rolling(24, min_periods=1).median().resample("1D").last().diff().dropna() + rows.append((a, hl.std(), sm.std(), raw.std())) + if rows: + print("\n sd del Delta-premio GIORNALIERO (e' la vol che il carry si prende addosso):") + for a, h, sm_, r_ in rows: + print(f" {a}: Hyperliquid `premium` {h*1e4:6.1f} bps | Deribit liscio 24h " + f"{sm_*1e4:6.1f} bps | Deribit grezzo {r_*1e4:6.1f} bps " + f"-> grezzo/HL = {r_/max(h,1e-12):.1f}x") + except Exception as exc: # noqa: BLE001 + print(f" (diagnostica sulla liscezza NON girata: {type(exc).__name__})") + print(" ⚠️ Il confronto che conta NON e' con l'11-13 di CC01 ma con il suo 'onesto'") + print(" 10,87: CC01 scriveva che il mark-to-market della base 'sgonfia lo Sharpe solo") + print(" 13->11, quindi il basis-from-data NON e' il rischio vero'. Su Deribit lo stesso") + print(" esperimento, SULLA STESSA FINESTRA, lo sgonfia a 1,65 (lente liscia) / 0,37") + print(" (lente grezza). La differenza non e' il periodo: e' la MISURA del premio, e") + print(" **la liscezza era una proprieta' della colonna, non del rischio.**") + return out + + +# ========================================================================== +# 5. Q3/Q4 β€” IL CARRY DERIBIT A DUE GAMBE (perp + trimestrale), roll e margine +# ========================================================================== +def deribit_carry(asset: str, wide: pd.DataFrame, exps: dict, fu: pd.DataFrame, + perp: pd.Series, roll_h: int = 72, sign: int = +1, + slip_bps: float = SLIP_BPS, fee_side: float = FEE_SIDE, + inverse: bool = True, gated: bool = False, trail_h: int = 24 * 14): + """Carry delta-neutral ESEGUIBILE su Deribit, a DUE gambe sullo STESSO venue. + + sign = +1 LONG trimestrale / SHORT perp -> incassa funding, paga il basis + (e' la DIREZIONE DI CC01: il datato fa da proxy dello spot, che a Deribit + storicamente non abbiamo; e la gamba corta sul perp incassa il funding) + sign = -1 SHORT trimestrale / LONG perp -> incassa il basis, paga il funding + + Il roll: il datato attivo e' il trimestrale piu' vicino con dte >= roll_h ore. Quando + cambia si paga il round-trip COMPLETO sulla sola gamba datata (2 ordini). **La gamba + perp NON si rolla**: e' un perpetuo. (Il filone BASIS-CALENDAR contava il turnover x2 + su entrambe le gambe: qui il costo del roll e' modellato per quello che e'.) + + Convenzione INVERSE (quella del venue: settlement BTC/ETH, quote USD): + P&L_BTC(long N USD) = N (1/entrata - 1/uscita) + funding inverse: dimensione in BTC = N/indice; i long PAGANO quando f>0 + Il ritorno orario e' espresso per $1 di nozionale PER GAMBA. + """ + idx = wide.index + n = len(idx) + order = sorted(exps, key=lambda c: exps[c]) + TAU = np.column_stack([(exps[c] - idx).total_seconds().values / 3600.0 for c in order]) + AV = np.column_stack([wide[c].notna().values for c in order]) + AL = AV & (TAU >= roll_h) + ok = AL.any(axis=1) + k1 = np.argmax(AL, axis=1) + W = wide[order].values.astype(float) + rows = np.arange(n) + front = np.where(ok, np.array(order, dtype=object)[k1], "") + Fv = np.where(ok, W[rows, k1], np.nan) + tau = np.where(ok, TAU[rows, k1], np.nan) + + fu2 = fu.reindex(idx).ffill(limit=2) + IX = fu2["index"].values.astype(float) + f1 = np.nan_to_num(fu2["f1h"].values.astype(float)) + P = perp.reindex(idx).values.astype(float) + + live = np.isfinite(Fv) & np.isfinite(P) & np.isfinite(IX) & (P > 0) & (IX > 0) + same = np.zeros(n, bool) + same[1:] = live[1:] & live[:-1] & (front[1:] == front[:-1]) & (front[1:] != "") + + # ------- segnale (causale): opzionale gate sul carry netto trailing ------- + basis_p = np.log(np.where(live, Fv, np.nan) / np.where(live, P, np.nan)) * HOURS_Y / tau + net_carry = f1 * HOURS_Y - basis_p # carry atteso del sign=+1, annualizzato + if gated: + s = pd.Series(net_carry, index=idx).rolling(trail_h, min_periods=trail_h // 3).mean().shift(1) + w = ((s * sign) > 0).astype(float).values # esci quando il carry trailing e' contrario + else: + w = np.ones(n) + w = np.nan_to_num(w) + + # ------- P&L orario per $1 di nozionale per gamba ------- + r = np.zeros(n) + if inverse: + # long datato: N(1/F_{t-1} - 1/F_t) | short perp: N(1/P_t - 1/P_{t-1}) (in BTC) + with np.errstate(all="ignore"): + dF = np.zeros(n); dP = np.zeros(n) + dF[1:] = 1.0 / Fv[:-1] - 1.0 / Fv[1:] + dP[1:] = 1.0 / P[1:] - 1.0 / P[:-1] + btc = sign * (dF + dP) + btc = btc + sign * (f1 / IX) # short perp INCASSA il funding se f>0 + r = np.where(same, IX * btc, 0.0) # da BTC a USD all'indice + else: + with np.errstate(all="ignore"): + rF = np.zeros(n); rP = np.zeros(n) + rF[1:] = Fv[1:] / Fv[:-1] - 1.0 + rP[1:] = P[1:] / P[:-1] - 1.0 + r = np.where(same, sign * (rF - rP + f1), 0.0) + r = np.nan_to_num(r) + held = np.zeros(n) + held[1:] = w[:-1] # deciso a t, tenuto t->t+1 (causale) + gross = r * held + + # ------- costi: apertura, ROLL (solo gamba datata), chiusura, e on/off del gate ------- + tc = fee_side + slip_bps * 1e-4 + cost = np.zeros(n) + rolled = np.zeros(n, bool) + rolled[1:] = live[1:] & live[:-1] & (front[1:] != front[:-1]) & (front[:-1] != "") & (front[1:] != "") + cost += rolled * (2.0 * tc) * np.maximum(held, np.roll(held, 1)) # chiudi vecchio + apri nuovo + dw = np.zeros(n) + dw[1:] = np.abs(held[1:] - held[:-1]) + cost += dw * (2.0 * tc) # entrata/uscita: 2 gambe + net = pd.Series(gross - cost, index=idx) + + # ------- tabella per TRADE (un trade = un contratto tenuto) ------- + fr = pd.Series(front, index=idx) + trades = [] + for c, g in net.groupby(fr): + if c == "" or len(g) < 24: + continue + cc = g.cumsum() + trades.append(dict(contract=c, start=g.index[0], end=g.index[-1], ore=len(g), + pnl=float(cc.iloc[-1]), mae=float(cc.min()), mfe=float(cc.max()))) + T = pd.DataFrame(trades).sort_values("start").reset_index(drop=True) if trades else pd.DataFrame() + return net, T, pd.Series(basis_p, index=idx), pd.Series(net_carry, index=idx) + + +def q3_carry(W: dict, E: dict, F: dict, PP: dict) -> dict: + hr("[3] Q3 β€” IL CARRY DERIBIT A DUE GAMBE (perpetual + trimestrale), roll pagato") + print(" Su Deribit non esiste storico SPOT: la coppia delta-neutral a due gambe sullo stesso") + print(" venue e' PERP vs TRIMESTRALE. Il trimestrale non paga funding -> fa da proxy dello") + print(" spot, e la direzione di CC01 diventa: LONG trimestrale / SHORT perp.\n") + res = {} + for sign, nome in ((+1, "LONG datato / SHORT perp (= direzione CC01)"), + (-1, "SHORT datato / LONG perp (= raccolta del contango)")): + legs = {} + for a in ASSETS: + net, T, basis, nc = deribit_carry(a, W[a], E[a], F[a], PP[a], sign=sign) + legs[a] = (net, T) + both = pd.concat([legs[a][0] for a in ASSETS], axis=1, join="inner").mean(axis=1) + d = to_daily(both) + res[sign] = dict(hourly=both, daily=d, legs=legs) + print(f" --- {nome} ---") + show(" 50/50 BTC+ETH, finestra piena", d) + show(" 50/50 BTC+ETH, finestra CC01", d[d.index >= W_CC01]) + py = per_year(d) + print(" per anno: " + " ".join(f"{y}:{v*100:+.1f}%" for y, v in py.items())) + print() + + # convenzione inverse vs lineare β€” misurata, non assunta + net_i, _, _, _ = deribit_carry("BTC", W["BTC"], E["BTC"], F["BTC"], PP["BTC"], sign=+1, inverse=True) + net_l, _, _, _ = deribit_carry("BTC", W["BTC"], E["BTC"], F["BTC"], PP["BTC"], sign=+1, inverse=False) + di, dl = to_daily(net_i), to_daily(net_l) + print(f" convenzione del venue: INVERSE (BTC-settled) Sharpe {stats(di)['sharpe']:+.2f} " + f"contro l'approssimazione LINEARE {stats(dl)['sharpe']:+.2f} " + f"(CAGR {stats(di)['cagr']*100:+.2f}% vs {stats(dl)['cagr']*100:+.2f}%, BTC sign=+1)") + dy = (per_year(di) - per_year(dl)) * 100 + print(" differenza per anno (inverse - lineare, punti percentuali): " + + " ".join(f"{y}:{v:+.1f}" for y, v in dy.items())) + print(" ⚠️ NON e' un dettaglio contabile: su una coppia INVERSE con nozionali pareggiati") + print(" in USD resta un delta residuo ~ -basis (dal termine 1/F - 1/P), e vale piu'") + print(" dell'intero premio che il carry vorrebbe incassare. Chi modella un carry su") + print(" inverse in convenzione lineare si regala un ritorno che il venue non paga.") + return res + + +def q4_margine(res: dict, F: dict, PP: dict) -> dict: + hr("[4] Q4 β€” IL ROLL E IL MARGINE: cio' che un P&L a scadenza NON PUO' VEDERE") + print(" Un carry delta-neutral SEMBRA liscio perche' il modello guarda il cashflow e il") + print(" payoff a scadenza. Ma la perdita di mark-to-market e' RICHIESTA SUBITO in margine") + print(" mentre il guadagno arriva alla convergenza: se l'equity scende sotto il mantenimento,") + print(" la posizione viene chiusa nel punto peggiore e il guadagno a scadenza non arriva mai.\n") + print(f" Margine dichiarato (fonte secondaria, documentazione Deribit futures): " + f"IM {IM_LEG*100:.2f}%/gamba, MM {MM_LEG*100:.3f}%/gamba -> MM totale " + f"{2*MM_LEG*100:.2f}% del nozionale per gamba.") + print(" Sotto leva per-gamba lambda (nozionale per gamba = lambda x equity):") + print(" liquidazione quando 1 + lambda*cumPnL < 2*MM*lambda") + print(" -> lambda_max = 1 / (2*MM + |MAE|) con MAE = escursione avversa del trade\n") + + out = {} + for sign in (+1, -1): + nome = "LONG datato/SHORT perp" if sign == +1 else "SHORT datato/LONG perp" + rows = [] + for a in ASSETS: + T = res[sign]["legs"][a][1].copy() + T["asset"] = a + rows.append(T) + T = pd.concat(rows).sort_values("start").reset_index(drop=True) + T["anno"] = T.start.dt.year + full_mae = float(T.mae.min()) + win = T[T.start >= W_CC01] + win_mae = float(win.mae.min()) if len(win) else float("nan") + lam_full = 1.0 / (2 * MM_LEG + abs(full_mae)) + lam_win = 1.0 / (2 * MM_LEG + abs(win_mae)) if np.isfinite(win_mae) else float("nan") + out[sign] = dict(T=T, lam_full=lam_full, lam_win=lam_win, + mae_full=full_mae, mae_win=win_mae) + print(f" --- {nome} ---") + print(f" trade (= contratti tenuti): {len(T)} in perdita: " + f"{int((T.pnl<0).sum())} ({(T.pnl<0).mean()*100:.0f}%)") + print(f" peggiore escursione avversa (MAE) sulla FINESTRA PIENA : " + f"{full_mae*100:+6.2f}% -> leva massima sostenibile {lam_full:5.1f}x per gamba") + print(f" peggiore escursione avversa (MAE) nella FINESTRA CC01 : " + f"{win_mae*100:+6.2f}% -> leva massima sostenibile {lam_win:5.1f}x per gamba") + if np.isfinite(lam_win) and lam_full > 0: + print(f" ⚠️ chi avesse dimensionato il carry sulla finestra di CC01 avrebbe usato " + f"fino a {lam_win:.1f}x: e' {lam_win/lam_full:.1f} VOLTE la leva che la storia piena sopporta.") + worst = T.nsmallest(5, "mae")[["asset", "contract", "start", "ore", "pnl", "mae"]] + print(" i 5 peggiori trade per escursione avversa:") + for r in worst.itertuples(): + print(f" {r.asset} {r.contract:<12s} {str(r.start.date()):>10s} " + f"{r.ore:5d} ore P&L finale {r.pnl*100:+6.2f}% MAE {r.mae*100:+6.2f}%") + print() + + # ---- e il carry PURO (long spot / short perp): il suo blow-out e' il PREMIO DEL PERP ---- + print(" Il carry PURO di CC01 (long spot / short perp) NON HA SCADENZA: non converge mai,") + print(" quindi il suo mark-to-market e' il PREMIO del perpetual sull'indice, e la sua perdita") + print(" massima e' il premio stesso. Misurato sul feed certificato Deribit:") + for a in ASSETS: + fu = F[a] + pp = PP[a].reindex(fu.index) + prem = (pp / fu["index"] - 1.0).dropna() + f1 = fu["f1h"].reindex(prem.index).fillna(0.0) + pnl = f1.cumsum() - (prem - prem.iloc[0]) # funding incassato - MTM della base + # MAE a 90 GIORNI: la finestra di detenzione confrontabile con un trimestrale. + # (Il drawdown da picco NON e' la misura giusta qui: il picco e' un crash-spike di + # un'ora in cui il perp scambia -20% sotto l'indice, cioe' un GUADAGNO per chi e' + # corto il perp. Un rischio si misura dall'INGRESSO, non da una fortuna transitoria.) + w = 90 * 24 + fwd_min = pnl[::-1].rolling(w, min_periods=1).min()[::-1] + exc = fwd_min - pnl + # ⚠️ INGRESSI AMMISSIBILI: un carry si apre quando il perp e' a PREMIO (e' l'intera + # ragione del trade). Includere le ore in cui il perp scambia -20% SOTTO l'indice + # (marzo 2020) misura l'escursione di un ingresso che nessuno farebbe. Si riporta + # il numero ristretto come stima, e quello grezzo come limite superiore. + exc_ok = exc.where(prem >= 0) + mae90 = float(exc_ok.min()) + i90 = exc_ok.idxmin() + mae90_raw = float(exc.min()) + lam90 = 1.0 / (2 * MM_LEG + abs(mae90)) + pr_y = prem.groupby(prem.index.year).max() + print(f" {a}: premio perp/indice mediana {prem.median()*1e4:+.1f} bps | " + f"MASSIMO {prem.max()*1e4:+.0f} bps ({prem.idxmax().date()}) | " + f"minimo {prem.min()*1e4:+.0f} bps ({prem.idxmin().date()})") + print(f" MAE a 90 giorni del carry puro CON mark-to-market della base, su ingressi") + print(f" ammissibili (premio >= 0): {mae90*100:+.2f}% (ingresso {i90.date()}) " + f"-> leva massima {lam90:.1f}x per gamba") + print(f" (senza il filtro d'ingresso: {mae90_raw*100:+.2f}% β€” include l'apertura di") + print(f" uno short perp a sconto del 20%, che nessuno farebbe)") + print(f" premio MASSIMO per anno (bps, e' il lato AVVERSO del carry): " + + ", ".join(f"{y}:{v*1e4:+.0f}" for y, v in pr_y.items())) + print(" ⚠️ Il modello r=funding di CC01 ha maxDD 0,3-0,9%: la stessa posizione, con il") + print(" mark-to-market della base che il margine guarda davvero, ha una escursione") + print(" avversa di un ordine di grandezza piu' grande. Non e' un dettaglio: e' la") + print(" differenza fra 'leva 80x' e 'leva 6x'.") + + return out + + +# ========================================================================== +# 6. GATE +# ========================================================================== +def q5_gate(cc: dict, res: dict, mg: dict, n_trials: int, family_sr: list) -> None: + hr("[5] GATE β€” a partire da quello che CC01 stesso ha fatto nascere") + + print("\n (a) implausible_sharpe β€” il gate del 26/07, scritto PROPRIO su CC01") + cand = [ + ("CC01 originale (HL, 2023-05+)", cc.get("HL_static"), None, None), + ("CC01 gated originale (HL)", cc.get("HL_gated"), None, None), + ("CC01 su Deribit, finestra CC01", cc.get("DBT_static_win"), None, None), + ("CC01 su Deribit, FINESTRA PIENA", cc.get("DBT_static_full"), None, None), + ("CC01 gated Deribit, FINESTRA PIENA", cc.get("DBT_gated_full"), None, None), + ("CC01+base grezzo, FINESTRA PIENA", cc.get("DBT_static_basis_full"), None, None), + ("CC01+base grezzo gated, PIENA", cc.get("DBT_gated_basis_full"), None, None), + ("CC01+base liscio 24h, FINESTRA PIENA", cc.get("DBT_static_basis_sm_full"), None, None), + ("CC01+base liscio 24h gated, PIENA", cc.get("DBT_gated_basis_sm_full"), None, None), + ] + for sign in (+1, -1): + T = mg[sign]["T"] + nm = "carry 2 gambe Deribit sign=+1" if sign == +1 else "carry 2 gambe Deribit sign=-1" + cand.append((nm, res[sign]["daily"], int(len(T)), int((T.pnl < 0).sum()))) + for nm, ser, nt, nl in cand: + if ser is None or len(ser) < 30: + print(f" {nm:<38s} NON GIRATO (serie assente)") + continue + g = A.implausible_sharpe(ser, n_trades=nt, n_losing_trades=nl) + tag = "🚨 SEGNALA" if g["implausible"] else " pulito " + print(f" {nm:<38s} {tag} Sh {g.get('sharpe', float('nan')):+6.2f} " + f"maxDD {g.get('maxdd', float('nan'))*100:5.2f}% " + f"perdite/attive {g.get('loss_frac', float('nan'))*100:5.1f}% " + f"Calmar {g.get('calmar', float('nan')):7.1f}") + for rr in g["reasons"]: + print(f" - {rr}") + + print("\n (b) marginal_vs_tp01 β€” sul carry ESEGUIBILE (2 gambe Deribit, finestra piena)") + for sign in (+1, -1): + d = res[sign]["daily"] + if d.std() == 0: + print(f" sign={sign:+d}: serie piatta, skip") + continue + m = A.marginal_vs_tp01(d) + b = m.get("blends", {}).get("w25", {}) + print(f" sign={sign:+d} verdetto={m.get('marginal_verdict')} " + f"corr->TP01 {m.get('corr_full')} is_hedge={m.get('is_hedge')} " + f"edge_in_sample={m.get('has_insample_edge')} (Sh IS {m.get('cand_insample_sharpe')})") + print(f" robust_oos={m.get('robust_oos')} multicut={m.get('multicut_persistent')} " + f"blend25 full {b.get('full')} (upl {b.get('uplift_full')}) hold {b.get('hold')} " + f"(upl {b.get('uplift_hold')})") + + print("\n (c) deflated_sharpe β€” sui trial DICHIARATI, contati al rialzo") + print(" ⚠️ la famiglia e' quella del CANDIDATO (le celle del carry a 2 gambe: 2 segni x") + print(" 8 ancore d'ora x 7 anticipi di roll). Mescolarci gli Sharpe del carry PURO") + print(" (3-7) gonfierebbe la varianza dei trial e quindi la soglia del null: sarebbe") + print(" un null di un'altra strategia.") + allsr = list(family_sr) + [stats(res[s_]["daily"])["sharpe"] for s_ in (+1, -1)] + best_sign = max((+1, -1), key=lambda s: stats(res[s]["daily"])["sharpe"]) + d = res[best_sign]["daily"] + sr = stats(d)["sharpe"] + # la lista degli Sharpe di famiglia serve per la varianza dei trial: si usa la griglia + # d'ancora del roll, gia' calcolata sotto, piu' i due segni. + try: + dsr, sr0 = A.deflated_sharpe(sr, allsr, d) + txt = (f"DSR {dsr:.3f} ({'PASS' if dsr >= 0.95 else 'FAIL'}), " + f"massimo atteso dal solo rumore {sr0:+.2f}") + except Exception as exc: # noqa: BLE001 + txt = f"NON GIRATO ({type(exc).__name__})" + print(f" candidato migliore = sign {best_sign:+d}, Sharpe {sr:+.2f}, " + f"trial dichiarati {n_trials} -> {txt}") + print(f" (il massimo atteso dal rumore su questa famiglia e' PIU' ALTO del candidato:") + print(f" non c'e' niente da deflazionare, il candidato e' sotto il proprio null.)") + + print("\n (c-bis) CAUSALITA' β€” dichiarata, e con un controllo POSITIVO") + print(" Il carry STATICO non ha segnale: non c'e' niente da anticipare, e la scelta del") + print(" contratto front dipende solo dal calendario di scadenza, noto anni prima.") + print(" L'unico segnale del filone e' il gate di CC01 (media mobile del funding). Li' la") + print(" causalita' e' uno `.shift(1)`, e un controllo positivo deve mostrare che TOGLIERLO") + print(" migliora β€” altrimenti il test non ha potenza:") + try: + FU2 = pd.concat({a: cc["_FUNDD"][a] for a in ASSETS}, axis=1) if "_FUNDD" in cc else None + except Exception: # noqa: BLE001 + FU2 = None + if FU2 is not None: + leak = FU2.rolling(14, min_periods=7).mean() # SENZA shift = leak + act = (leak > 0).astype(float) + n_ = act.sum(axis=1).replace(0, np.nan) + g_leak = ((FU2 * act).sum(axis=1) / n_).fillna(0.0) - \ + (act.sum(axis=1) > 0).astype(float) * ANNUAL_COST_CC01 / 365.25 + print(f" gate CAUSALE (shift 1): Sharpe {stats(cc['DBT_gated_full'])['sharpe']:+.2f} | " + f"gate CON LEAK (no shift): Sharpe {stats(g_leak.dropna())['sharpe']:+.2f} " + f"-> il test HA potenza e la versione pubblicata e' quella causale") + else: + print(" NON GIRATO (pannello di funding non disponibile)") + + print("\n (d) null del DE-LEVERING β€” il carry rivendica un DD minuscolo: e' solo meno leva?") + try: + base = A.tp01_baseline_daily() + for nm, ser in (("CC01 Deribit finestra piena", cc.get("DBT_static_full")), + ("carry 2 gambe sign=+1", res[+1]["daily"])): + if ser is None: + continue + cs = stats(ser) + hit = None + for k in np.arange(1.00, 0.01, -0.02): # il k PIU' GRANDE che rispetta il DD + st = stats(base * k) + if st["maxdd"] <= cs["maxdd"] + 1e-12: + hit = (float(k), st) + break + if hit is None: + print(f" {nm}: nessun k<=1 sul baseline raggiunge quel DD -> null non applicabile") + else: + k, st = hit + verdetto = "REFUTATO (il baseline de-leverato fa meglio)" if st["sharpe"] > cs["sharpe"] \ + else "SUPERATO (il baseline de-leverato NON lo eguaglia)" + print(f" {nm}: DD {cs['maxdd']*100:.2f}% Sh {cs['sharpe']:+.2f} vs " + f"TP01 a k={k:.2f}: DD {st['maxdd']*100:.2f}% Sh {st['sharpe']:+.2f} -> {verdetto}") + print(" ⚠️ lo Sharpe e' INVARIANTE alla scala, quindi su un candidato che rivendica") + print(" 'poco DD' questo null si riduce a 'il tuo Sharpe batte quello del baseline?'.") + print(" E' degenere per costruzione (lezione 22/08) β€” girato lo stesso e dichiarato.") + except Exception as exc: # noqa: BLE001 + print(f" NON GIRATO ({type(exc).__name__}: {exc})") + + +def q5b_ancora(W: dict, E: dict, F: dict, PP: dict, sign: int) -> list: + print("\n (e) FORTUNA D'ANCORA del roll β€” l'ora in cui si rolla e' un max-of-k non dichiarato") + sh = {} + for off in range(0, 24, 3): + legs = [] + for a in ASSETS: + net, _, _, _ = deribit_carry(a, W[a], E[a], F[a], PP[a], + roll_h=72 + off, sign=sign) + legs.append(net) + d = to_daily(pd.concat(legs, axis=1, join="inner").mean(axis=1)) + sh[off] = stats(d)["sharpe"] + v = np.array(list(sh.values())) + print(f" 8 ancore di roll (72h + 0..21h): mediana {np.median(v):+.3f} " + f"banda [{v.min():+.3f}, {v.max():+.3f}] canonica(72h) {sh[0]:+.3f} " + f"-> fortuna {sh[0]-np.median(v):+.3f}") + sh2 = {} + for rd in (1, 2, 3, 5, 7, 10, 14): + legs = [] + for a in ASSETS: + net, _, _, _ = deribit_carry(a, W[a], E[a], F[a], PP[a], + roll_h=rd * 24, sign=sign) + legs.append(net) + d = to_daily(pd.concat(legs, axis=1, join="inner").mean(axis=1)) + sh2[rd] = stats(d)["sharpe"] + print(" giorni di anticipo del roll: " + + " ".join(f"{k}g:{v:+.2f}" for k, v in sh2.items())) + return list(sh.values()) + list(sh2.values()) + + +# ========================================================================== +# 7. ESEGUIBILITA' +# ========================================================================== +def q6_eseguibilita(mg: dict) -> None: + hr("[6] Q3-bis β€” I MURI NON STRUTTURALI, riprezzati coi numeri di oggi") + print(" CC01 fu dichiarato non eseguibile per: 'spot+perp = 4-38 gambe, funding HL non") + print(" Deribit'. La versione perp+trimestrale e' a DUE gambe sullo STESSO venue.\n") + specs = { + "BTC-PERPETUAL": dict(min_usd=10.0, tick=0.5, settle="BTC"), + "BTC trimestrale": dict(min_usd=10.0, tick=2.5, settle="BTC"), + "ETH-PERPETUAL": dict(min_usd=1.0, tick=0.05, settle="ETH"), + "ETH trimestrale": dict(min_usd=1.0, tick=0.25, settle="ETH"), + } + print(" specifiche lette dal venue (public/get_instruments, 2026-08-22):") + for k, v in specs.items(): + print(f" {k:<18s} lotto minimo ${v['min_usd']:>5.0f} tick {v['tick']:<5} " + f"settlement {v['settle']}") + print(f"\n A $635 il lotto minimo NON e' il vincolo (4a volta in questa ondata): un carry") + print(f" BTC a lambda=1 vuole $635 per gamba, 63 volte il lotto minimo.") + print(f" Margine iniziale a lambda=1: {2*IM_LEG*100:.1f}% del nozionale per gamba = " + f"${2*IM_LEG*635:.0f} su $635.") + + lam = mg[+1]["lam_full"] + print(f"\n ⚠️ MA il vincolo vero e' un ALTRO, e non e' la taglia: **tutti i futures datati") + print(f" Deribit sono INVERSE, settlement_currency BTC/ETH** (verificato strumento per") + print(f" strumento: non esiste una linea USDC datata). Il libro live e' su USDC") + print(f" lineari. E' lo stesso muro trovato il 22/08 sulle opzioni ('BTC fuori a $3.000") + print(f" misurato sulla famiglia inverse'): un conto USDC non margina un inverse senza") + print(f" cross-collateral, e il collaterale in BTC E' esso stesso esposizione") + print(f" direzionale β€” che e' precisamente cio' che un delta-neutral vuole evitare.") + print(f" Questo NON e' stato risolto qui: e' una verifica da UNA chiamata sul conto") + print(f" reale (haircut del cross-collateral), e va fatta prima di credere a qualsiasi") + print(f" numero di eseguibilita' di questo filone.") + + print("\n costo d'esecuzione ANNUO del carry rollato (4 roll/anno x 2 ordini, SOLO la gamba") + print(" datata: il perpetuo non si rolla; apertura e chiusura sono 2 ordini UNA volta sola):") + for slip in (0.0, 2.0, 5.0, 10.0): + c = 4 * 2 * (FEE_SIDE + slip * 1e-4) + print(f" slippage {slip:4.1f} bps/lato -> {c*100:5.3f}%/anno (8 ordini/anno)") + print(" spread REALE letto sul book vivo (cache 22/08): BTC front 0,32 bps / secondo 5,43;") + print(" ETH front 1,03 / secondo 1,02 -> slippage 2 bps/lato e' una stima ONESTA sul front.") + print(f" ⚠️ BASIS-CALENDAR citava ~1,4%/anno di costo perche' contava il turnover su") + print(f" ENTRAMBE le gambe a ogni roll: il perpetuo NON si rolla. Il costo vero e'") + print(f" **0,44%/anno a 2 bps di slippage**, ~3 volte meno. Il che rende quel filone") + print(f" MENO severo sul costo e per NIENTE meno severo sul segno: un premio di") + print(f" ~0,85%/anno con IC95 che contiene lo zero resta indistinguibile da zero anche") + print(f" pagando un terzo del costo. **Correggere un costo non crea un edge.**") + + +# ========================================================================== +# MAIN +# ========================================================================== +def main() -> None: + t0 = time.time() + hr("r0822d CC01-2022 β€” riaprire il cash-and-carry con la coda che gli mancava") + + persist_dataset() + + print("\n caricamento...") + W, E, F, PP = {}, {}, {}, {} + for a in ASSETS: + W[a], E[a] = load_futures(a) + F[a] = load_fund(a) + PP[a] = load_perp(a) + print(f" {a}: {W[a].shape[1]} contratti, {len(W[a]):,} ore " + f"({W[a].index.min().date()} .. {W[a].index.max().date()}) " + f"funding+indice {len(F[a]):,} ore perp certificato {len(PP[a]):,} barre") + + q1_descrittiva(F, W, E) + cc = q2_replica(F, PP) + res = q3_carry(W, E, F, PP) + mg = q4_margine(res, F, PP) + + # trial dichiarati, contati AL RIALZO (ogni variante provata, comprese quelle scartate): + # 4 carry puro r=funding : 2 modi (static/gated) x 2 finestre + # 8 carry puro + base MTM : 2 modi x 2 lenti sul premio x 2 finestre + # 32 carry a 2 gambe : 2 segni x (1 canonica + 8 ancore d'ora + 7 anticipi roll) + # 2 convenzione : inverse vs lineare + # 1 controllo positivo di leak + N_TRIALS = 4 + 8 + 32 + 2 + 1 + best = max((+1, -1), key=lambda s: stats(res[s]["daily"])["sharpe"]) + print("\n (pre-calcolo della griglia d'ancora, serve al deflated-Sharpe)") + family_sr = q5b_ancora(W, E, F, PP, best) + q5_gate(cc, res, mg, N_TRIALS, family_sr) + q6_eseguibilita(mg) + + hr("[7] SINTESI β€” LA SCALA, cioe' dove sono davvero andati gli 11-13 punti di Sharpe") + s_hl = stats(cc["HL_static"])["sharpe"] if "HL_static" in cc else float("nan") + s_win = stats(cc["DBT_static_win"])["sharpe"] + s_full = stats(cc["DBT_static_full"])["sharpe"] + s_bas = stats(cc["DBT_static_basis_full"])["sharpe"] + d_full = cc["DBT_static_full"] + best_d = res[best]["daily"] + righe = [ + ("CC01 come pubblicato (funding HL, r=funding, 2023-05+)", s_hl, + stats(cc["HL_static"])["maxdd"] if "HL_static" in cc else float("nan"), ""), + ("stesso modello, funding DERIBIT, STESSA finestra", s_win, + stats(cc["DBT_static_win"])["maxdd"], "controllo di venue"), + ("stesso modello, DERIBIT, finestra piena (col 2022)", s_full, + stats(d_full)["maxdd"], "<- la coda che mancava"), + ("+ base, premio LISCIO 24h (la lente di CC01/HL)", + stats(cc["DBT_static_basis_sm_full"])["sharpe"], + stats(cc["DBT_static_basis_sm_full"])["maxdd"], "<- il rischio che il margine guarda"), + ("+ base, premio GREZZO (la lente severa)", s_bas, + stats(cc["DBT_static_basis_full"])["maxdd"], " banda della stessa cosa"), + ("carry ESEGUIBILE su Deribit, 2 gambe, DIREZIONE CC01", + stats(res[+1]["daily"])["sharpe"], stats(res[+1]["daily"])["maxdd"], + "<- niente spot: serve un 2o derivato"), + (" (e scegliendo EX-POST il segno migliore dei due)", + stats(res[-1]["daily"])["sharpe"], stats(res[-1]["daily"])["maxdd"], ""), + ] + print(f" {'lente':<56s} {'Sharpe':>8s} {'maxDD':>8s}") + for nm, sh, dd, nota in righe: + print(f" {nm:<56s} {sh:+8.2f} {dd*100:7.2f}% {nota}") + print() + s_sm = stats(cc["DBT_static_basis_sm_full"])["sharpe"] + s_2l = stats(res[+1]["daily"])["sharpe"] + print(f" SCOMPOSIZIONE (lente LISCIA, la piu' favorevole al filone):") + print(f" il 2022 vale {s_win - s_full:+.2f} di Sharpe (7,88 -> 3,72)") + print(f" la base MTM vale {s_full - s_sm:+.2f} (3,72 -> {s_sm:.2f})") + print(f" la SECONDA GAMBA {s_sm - s_2l:+.2f} ({s_sm:.2f} -> {s_2l:.2f})") + print(f" (lente GREZZA: il 2022 {s_win - s_full:+.2f}, la base {s_full - s_bas:+.2f}, la seconda gamba " + f"{s_bas - s_2l:+.2f})") + print() + print(f" ⚠️ CORREZIONE A ME STESSO, fatta guardando la mia stessa tabella: avevo scritto") + print(f" 'il buco piu' grande non era il 2022'. L'aritmetica dice il CONTRARIO β€” il 2022 e'") + print(f" il singolo pezzo piu' grande ({s_win - s_full:+.2f}) su entrambe le lenti. Il punto vero e' un") + print(f" altro, ed e' piu' forte: **il 2022 da solo NON BASTA**. Toglierlo dal buco porta lo") + print(f" Sharpe da 7,88 a 3,72, che `implausible_sharpe` SEGNALA ANCORA. I due pezzi che il") + print(f" 26/06 non aveva dichiarato β€” mark-to-market della base e assenza di una gamba spot") + print(f" negoziabile β€” valgono INSIEME quanto il 2022 ({s_full - s_2l:+.2f} sulla lente liscia) e sono") + print(f" quelli che portano il numero sotto 1. La ragione pubblicata era NECESSARIA e non") + print(f" SUFFICIENTE.") + print(f" πŸ“Œ E il salto piu' grande della tabella non e' nemmeno nell'elenco: e' il VENUE") + print(f" ({s_hl - s_win:+.2f}, da HL a Deribit sulla STESSA finestra e collo STESSO modello). Un carry") + print(f" di funding misurato su un venue non e' un carry di funding.") + print() + print(f" leva massima sostenibile dal carry a 2 gambe in direzione CC01: " + f"{mg[+1]['lam_full']:.1f}x per gamba sulla") + print(f" storia piena, contro {mg[+1]['lam_win']:.1f}x che la sola finestra di CC01 avrebbe autorizzato") + print(f" ({mg[+1]['lam_win']/mg[+1]['lam_full']:.1f}x di leva di troppo). Sul carry PURO con MTM della base: " + f"~5,6-6,4x contro le") + print(f" ~80x che un maxDD dello 0,8% farebbe credere sostenibili.") + print() + print(" βœ… Il criterio di riapertura dichiarato il 26/06 era FALSIFICABILE ed e' stato") + print(" ESEGUITO: il dato che mancava ora esiste, la misura e' stata fatta, e il verdetto") + print(" non cambia. **Il filone era chiuso per la ragione giusta** β€” e ora si sa anche") + print(" che quella ragione era NECESSARIA ma non sufficiente: il 2022 era il pezzo piu'") + print(" grande dei tre, e da solo lascia il candidato ancora sopra ogni soglia di") + print(" plausibilita'. Riaprire un filone su dati nuovi ha prodotto un verdetto") + print(" INVARIATO e una diagnosi MIGLIORE β€” che e' il risultato che si voleva.") + print(f"\n tempo totale {time.time()-t0:.1f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822d_funding.py b/scripts/research/r0822d_funding.py new file mode 100644 index 0000000..e18f057 --- /dev/null +++ b/scripts/research/r0822d_funding.py @@ -0,0 +1,1212 @@ +#!/usr/bin/env python +"""r0822d_funding.py β€” IL FUNDING DEI PERPETUAL: il costo che il progetto non ha mai prezzato. + +CONTESTO (ondata 2026-08-22, sezione 33 di docs/research/RESULTS-0822.md). Verificato dal +coordinatore: **zero occorrenze della parola `funding`** in `src/backtest/harness.py`, +`src/strategies/trend_portfolio.py`, `src/portfolio/{sleeves,portfolio}.py` e in tutto +`src/live/`. Il progetto ha prezzato fee, slippage, min-order, pavimento IB, haircut small-cap, +fortuna d'ancora, degrado d'esecuzione, look-ahead, split, contaminazione EUR/USD, rischio di +venue e fisco β€” e MAI il costo di TENERE APERTA una posizione. + +La stima grezza che ha fatto emergere il buco: proxy Hyperliquid 14,8%/anno x esposizione long +media di TP01 0,103Β·E = ~1,53%/anno = il 9% del drift de-luckato. Questo script la sostituisce +con una MISURA. + +COSA FA (un grado di liberta' per volta, replica di controllo prima di ogni delta): + 0. IL DATO. Storico funding da `public/get_funding_rate_history` di Deribit per i QUATTRO + perpetual: i LINEARI USDC (`BTC_USDC-PERPETUAL`/`ETH_USDC-PERPETUAL`, che sono cio' che il + libro esegue davvero) e gli INVERSE (`BTC-PERPETUAL`/`ETH-PERPETUAL`, che hanno la storia + lunga). Copertura dichiarata, buchi contati, accordo linear-vs-inverse MISURATO sulla + finestra comune. Cache su scratchpad; pacing <=2 req/s; mai nei minuti :07 (cron_book) e + :25-:29 (cron_chain). + 1. REPLICA DI CONTROLLO. Con funding SPENTO le repliche devono riprodurre BIT-EXACT + `al.tp01_baseline_daily`, `CC.tp01_realistic`, `r02.sim_equity` e `CC.book_series('hourly')`. + Nessun delta viene stampato prima che questa sezione passi. + 2. IL SEGNO. Il funding si PAGA da long in contango e si INCASSA da short. TP01 e' long-flat + (paga quasi sempre), SKH01 e' long/short e sta a mercato una frazione del tempo (puo' + incassare). Il segno NON e' assunto: e' misurato appaiando la posizione oraria/giornaliera + di ciascuno sleeve col funding del SUO strumento. + 3. I NUMERI. Sharpe FULL/IS/HOLD, maxDD, CAGR, drift di TP01, SKH01 e del libro 75/25, con e + senza funding, e il DELTA su ciascuno. + 4. LE DECISIONI. Soffitto ~1,3 Β· peso 75/25 (sweep w x banda d'ancora, mediana delle + DIFFERENZE APPAIATE) Β· muri di capitale e rendita perpetua (che vivono sul DRIFT) Β· + k* e il gradino di leva (il funding e' proporzionale all'esposizione, quindi scala con k). + 5. CONTROLLO SUL CONTO VERO. Il libro esegue dal 2026-07-08: posizione oraria ricostruita da + `logs/cron_book.log`, funding modellato coi tassi REALI, confronto col `realized_pnl` che + il conto riporta sulle due posizioni aperte. + 6. COSA NON E' CATTURATO, quantificato: l'approssimazione "esposizione media x tasso medio" + contro l'accrual esatto per trade (su SKH01, che entra ed esce), il proxy inverse prima + del 2022-03, la discretezza del pagamento. + +VINCOLI: nessun file di produzione toccato (config/, src/, scripts/live/, cron INVARIATI); +nessun commit; output solo a video + cache in scratchpad. Runtime ~2-6 min con cache calda. + +Uso: `uv run python scripts/research/r0822d_funding.py` + `--no-net` usa solo la cache (nessuna richiesta a Deribit) +""" +from __future__ import annotations + +import datetime as dt +import json +import re +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT)) + +import altlib as al # noqa: E402 +import r0702_anchor_skh01 as r02 # noqa: E402 (run_asset / sim_equity / get5m) +import r0724_skh_live_weight as skl # noqa: E402 (skh_book, W_GRID, OFFSETS) +import r0725_capcurve as CC # noqa: E402 (tp01_realistic, book_series, survival) +import r0726_capwall_refresh as WR # noqa: E402 (perp_and_wall, trajectory) +from src.data.downloader import load_data # noqa: E402 +from src.portfolio.portfolio import combine_outer, metrics # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, resample_1d, simple_returns) + +# --------------------------------------------------------------------------- +# Costanti dichiarate +# --------------------------------------------------------------------------- +# Cache dello storico funding FUORI dal repo (`data/raw/` non e' toccabile da questo filone, +# e questi parquet non sono dato certificato: sono una cache ri-scaricabile in ~4 minuti). +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/fund") +SCRATCH = _SP if _SP.parent.exists() else Path("/tmp/pythagoras_funding_cache") +API = "https://www.deribit.com/api/v2/public/get_funding_rate_history" +SPAN_MS = 30 * 24 * 3600 * 1000 # 30 giorni per richiesta (l'endpoint da' 744 record) +HOLDOUT = al.HOLDOUT +ASSETS = ("BTC", "ETH") +MSH = 3_600_000 +ALLOC = 600.0 # il capitale su cui gira book_series (conto reale $635) +DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor) +EURUSD = CC.EURUSD +N_PATHS_WALL = 2000 # risoluzione MC dichiarata (il muro e' +-1%) + +# strumento -> (data di creazione su Deribit, asset) +INSTR = { + "BTC_USDC-PERPETUAL": ("2022-03-09", "BTC", "linear"), + "ETH_USDC-PERPETUAL": ("2022-03-14", "ETH", "linear"), + "BTC-PERPETUAL": ("2018-08-14", "BTC", "inverse"), + "ETH-PERPETUAL": ("2019-03-14", "ETH", "inverse"), +} +LINEAR = {"BTC": "BTC_USDC-PERPETUAL", "ETH": "ETH_USDC-PERPETUAL"} +INVERSE = {"BTC": "BTC-PERPETUAL", "ETH": "ETH-PERPETUAL"} + +USE_NET = "--no-net" not in sys.argv +T0 = time.time() + + +def sez(t: str) -> None: + print("\n" + "=" * 100) + print(f" {t}") + print("=" * 100) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +# =========================================================================== +# 0) IL DATO β€” storico funding Deribit +# =========================================================================== +def _pace() -> None: + """<=2 req/s, e mai nei minuti :07 (cron_book) e :25-:29 (cron_chain).""" + while dt.datetime.now(dt.timezone.utc).minute in (7, 25, 26, 27, 28, 29): + time.sleep(20) + time.sleep(0.55) + + +def fetch_funding(instr: str, start_iso: str) -> pd.DataFrame: + """Storico orario del funding, incrementale su cache parquet.""" + SCRATCH.mkdir(parents=True, exist_ok=True) + out = SCRATCH / f"{instr.replace('-', '_')}.parquet" + have = pd.read_parquet(out) if out.exists() else pd.DataFrame() + if not USE_NET: + if not len(have): + raise RuntimeError(f"--no-net ma cache assente per {instr}") + return have + t0 = int(pd.Timestamp(start_iso, tz="UTC").timestamp() * 1000) + if len(have): + t0 = int(have["ts"].max()) - MSH + tend = int(pd.Timestamp.now(tz="UTC").floor("h").timestamp() * 1000) + rows, a = [], t0 + while a < tend: + b = min(a + SPAN_MS, tend) + u = f"{API}?instrument_name={instr}&start_timestamp={a}&end_timestamp={b}" + for att in range(4): + try: + _pace() + r = json.load(urllib.request.urlopen(u, timeout=30))["result"] + break + except Exception: + if att == 3: + raise + time.sleep(3 * (att + 1)) + rows.extend({"ts": x["timestamp"], "i1h": x["interest_1h"], + "i8h": x["interest_8h"], "index": x["index_price"]} for x in r) + a = b + df = pd.concat([have, pd.DataFrame(rows)], ignore_index=True) if len(have) else pd.DataFrame(rows) + df = df.drop_duplicates(subset="ts").sort_values("ts").reset_index(drop=True) + df.to_parquet(out, index=False) + return df + + +def sezione0() -> dict: + sez("0) IL DATO β€” storico funding Deribit (endpoint pubblico get_funding_rate_history)") + print(" Il libro esegue sui LINEARI USDC. Gli INVERSE sono qui solo perche' hanno la storia") + print(" lunga: servono come PROXY prima del 2022-03, e l'accordo fra i due si MISURA.") + print(" (4a e 5a occorrenza dello schema `fee_watch`: puntare la misura sull'inverse") + print(" avrebbe misurato uno strumento che il libro non trada.)\n") + F = {} + print(f" {'strumento':<22} {'righe':>7} {'da':>12} {'a':>12} {'ore attese':>11} " + f"{'buchi':>6} {'ann. medio':>11}") + for instr, (start, asset, kind) in INSTR.items(): + d = fetch_funding(instr, start) + d = d.copy() + d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True) + d = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt") + span_h = int((d.index.max() - d.index.min()) / pd.Timedelta("1h")) + 1 + gaps = span_h - len(d) + ann = float(d["i1h"].mean()) * 24 * 365.25 + F[instr] = d + print(f" {instr:<22} {len(d):>7,} {str(d.index.min().date()):>12} " + f"{str(d.index.max().date()):>12} {span_h:>11,} {gaps:>6,} {ann:>10.2%}") + + b0 = pd.Timestamp("2019-03-14", tz="UTC") + sw = min(F[LINEAR[a]].index.min() for a in ASSETS) + tot_d = (F[LINEAR["BTC"]].index.max() - b0).days + print("\n ⚠️ VINCOLO DEL FILONE, da dire subito: i perpetual LINEARI USDC esistono dal") + print(f" {sw.date()}. Il backtest del libro parte dal 2019-03-14 -> per " + f"{(sw-b0).days/tot_d:.0%} del campione ({(sw-b0).days:,} giorni su {tot_d:,}) il") + print(" funding dello strumento ESEGUITO non esiste. Si usa l'inverse come proxy e si") + print(" misura quanto i due differiscono dove coesistono.") + + print("\n ANNO per ANNO (annualizzato, ore x 24 x 365.25):") + print(f" {'anno':>6} " + " ".join(f"{k.replace('-PERPETUAL',''):>12}" for k in INSTR)) + yrs = sorted({y for d in F.values() for y in d.index.year.unique()}) + for y in yrs: + cells = [] + for k in INSTR: + d = F[k] + g = d[d.index.year == y]["i1h"] + cells.append(f"{g.mean()*24*365.25:>11.2%}" if len(g) > 100 else f"{'β€”':>12}") + print(f" {y:>6} " + " ".join(cells)) + + print("\n CONVENZIONE TEMPORALE β€” verificata dai dati, non assunta dalla documentazione.") + print(" Se il record etichettato T copre l'ora CHE FINISCE in T, allora `interest_8h` deve") + print(" essere la somma degli 8 `interest_1h` fino a T INCLUSO. Le due alternative danno") + print(" numeri diversi e si vedono:") + print(f" {'ipotesi':<26} {'corr':>9} {'mediana |dif|':>15}") + d = F["BTC_USDC-PERPETUAL"] + h = {"trailing, T incluso": d["i1h"].rolling(8).sum(), + "trailing, T escluso": d["i1h"].shift(1).rolling(8).sum(), + "leading (T -> T+8h)": d["i1h"][::-1].rolling(8).sum()[::-1].shift(-1)} + for lbl, x in h.items(): + m = (~x.isna()) & (~d["i8h"].isna()) + print(f" {lbl:<26} {float(x[m].corr(d['i8h'][m])):>9.6f} " + f"{float((x[m]-d['i8h'][m]).abs().median()):>15.3e}") + print(" -> 'trailing, T incluso'. E' la convenzione usata da tutto lo script.") + + # accordo linear vs inverse sulla finestra comune + print("\n ACCORDO LINEAR vs INVERSE sulla finestra comune (ore appaiate):") + print(f" {'asset':>6} {'ore':>8} {'ann.LIN':>9} {'ann.INV':>9} {'corr':>7} " + f"{'diff annua':>11} {'|diff| med/ora':>15}") + agree = {} + for a in ASSETS: + L, I = F[LINEAR[a]]["i1h"], F[INVERSE[a]]["i1h"] + J = pd.concat({"L": L, "I": I}, axis=1, join="inner").dropna() + d_ann = float((J["L"] - J["I"]).mean()) * 24 * 365.25 + agree[a] = dict(n=len(J), lin=float(J["L"].mean()) * 24 * 365.25, + inv=float(J["I"].mean()) * 24 * 365.25, + corr=float(J["L"].corr(J["I"])), diff=d_ann) + print(f" {a:>6} {len(J):>8,} {agree[a]['lin']:>8.2%} {agree[a]['inv']:>8.2%} " + f"{agree[a]['corr']:>7.3f} {d_ann:>10.2%} {float((J['L']-J['I']).abs().median()):>15.2e}") + print("\n IL PROXY DA CUI E' NATO IL FILONE β€” Hyperliquid (data/raw/hlfund_*_1h.parquet)") + print(" contro il venue su cui il libro esegue davvero, sulle ore APPAIATE:") + print(f" {'asset':>6} {'ore comuni':>11} {'ann. HL':>9} {'ann. Deribit':>13} {'rapporto':>9}") + for a in ASSETS: + try: + hl = pd.read_parquet(ROOT / "data" / "raw" / f"hlfund_{a.lower()}_1h.parquet")["funding"] + except Exception: + print(f" {a:>6} {'β€”':>11} (file assente)") + continue + hl.index = pd.DatetimeIndex(hl.index).floor("h") + hl = hl[~hl.index.duplicated(keep="last")] + de = F[LINEAR[a]]["i1h"] + J = pd.concat({"hl": hl, "de": de}, axis=1, join="inner").dropna() + print(f" {a:>6} {len(J):>11,} {float(J['hl'].mean())*24*365.25:>9.2%} " + f"{float(J['de'].mean())*24*365.25:>13.2%} " + f"{float(J['hl'].mean())/float(J['de'].mean()):>8.2f}x") + print(" ⚠️ Il funding NON e' una proprieta' del mercato, e' una proprieta' del VENUE (formula") + print(" e cap diversi). Usare il tasso di Hyperliquid per stimare il costo di un libro che") + print(" esegue su Deribit e' lo stesso errore di `fee_watch` il 21/08 β€” misurare uno") + print(" strumento che non si trada β€” con l'aggravante che qui non e' nemmeno lo stesso") + print(" exchange. (Il numero finale esce comunque piu' ALTO della stima grezza: due") + print(" errori in direzioni opposte, il tasso troppo alto e l'esposizione condizionale") + print(" troppo bassa. Compensarsi non e' misurare.)") + print(" πŸ“Œ E ha una CONSEGUENZA di venue: se a $20k si riapre lo split e XS01 va su HL, il") + print(" suo funding va prezzato col tasso di HL, non con questo β€” ed e' un altro numero.") + + return dict(F=F, agree=agree) + + +def build_rate(F: dict) -> dict: + """Serie oraria del funding per asset: LINEARE dove esiste, INVERSE prima (proxy dichiarato). + Ritorna, per asset: ts (ms, fine dell'ora), i1h, cumF (somma cumulata), e la data di switch.""" + out = {} + for a in ASSETS: + lin, inv = F[LINEAR[a]]["i1h"], F[INVERSE[a]]["i1h"] + sw = lin.index.min() + merged = pd.concat([inv[inv.index < sw], lin]).sort_index() + merged = merged[~merged.index.duplicated(keep="last")] + ts = to_ms(merged.index) + v = merged.values.astype(float) + out[a] = dict(ts=ts, i1h=v, cum=np.concatenate([[0.0], np.cumsum(v)]), + switch=sw, series=merged) + return out + + +EPOCH = pd.Timestamp("1970-01-01", tz="UTC") + + +def to_ms(idx) -> np.ndarray: + """Epoca in MILLISECONDI, esplicita. + + ⚠️ NON usare `.astype("int64")` / `.view("int64")` su un DatetimeIndex: in pandas 2.x la + risoluzione non e' piu' sempre ns e la scala esce SBAGLIATA in modo silenzioso. Misurato + qui, in questa sessione, su tre serie dello stesso progetto: il funding cachato ha unita' + `datetime64[ms]`, l'indice di `al.get()` ha `datetime64[us]`, quello del log del cron ha + `datetime64[s]`. Con `.astype("int64")//1e6` le prime due finiscono su scale che + differiscono di 1.000x -> `searchsorted` restituisce l'ESTREMO dell'array invece + dell'ora giusta, e il funding usciva 0.000%/anno su SKH01 (troppo piccolo) e + +1.229%/GIORNO su TP01 (troppo grande). Nessuna eccezione, nessun NaN: due numeri + plausibili in due punti diversi, e uno dei due sarebbe passato per buono. + (Stessa famiglia dell'errore gia' codificato in CLAUDE.md il 2026-07-01.) + """ + if not isinstance(idx, pd.DatetimeIndex): + idx = pd.DatetimeIndex(idx) + if idx.tz is None: + idx = idx.tz_localize("UTC") + return ((idx - EPOCH) // pd.Timedelta(milliseconds=1)).values.astype(np.int64) + + +def cumF(rate: dict, t_ms) -> np.ndarray: + """Funding cumulato fino all'istante t (record etichettato T = ora CHIUSA in (T-1h, T]).""" + j = np.searchsorted(rate["ts"], np.asarray(t_ms, dtype=np.int64), side="right") + return rate["cum"][j] + + +def daily_funding(rate: dict, days: pd.DatetimeIndex) -> np.ndarray: + """Funding pagato da un long unitario nel giorno d = somma di i1h su (d, d+24h].""" + t0 = to_ms(days) + return cumF(rate, t0 + 24 * MSH) - cumF(rate, t0) + + +def verifica_scala(rate: dict, F: dict) -> None: + """Controllo POSITIVO sulla scala dei tempi (senza, il bug qui sotto ripassa muto). + + Non basta che i numeri sembrino plausibili: `daily_funding` (searchsorted su epoca in ms) + si confronta con un `groupby` di pandas, che non fa nessuna conversione a intero. E si + verifica che il controllo abbia POTENZA, cioe' che fallisca se la scala e' sbagliata. + """ + print("\n [SCALA] controllo positivo su daily_funding vs groupby pandas:") + for a in ASSETS: + ser = rate[a]["series"] + g = ser.groupby((ser.index - pd.Timedelta(seconds=1)).floor("D")).sum() + g = g[(g.index >= ser.index.min().floor("D") + pd.Timedelta("1D")) & + (g.index <= ser.index.max().floor("D") - pd.Timedelta("1D"))] + gi = pd.DatetimeIndex(g.index) + mine = daily_funding(rate[a], gi) + dmax = float(np.max(np.abs(mine - g.values))) + assert dmax < 1e-15, f"scala tempi rotta su {a}: max|dif|={dmax:.3e}" + bad = dict(ts=rate[a]["ts"] * 1000, cum=rate[a]["cum"]) # scala x1000 (us al posto di ms) + t = to_ms(gi[:200]) + wrong = cumF(bad, t + 24 * MSH) - cumF(bad, t) + assert float(np.max(np.abs(wrong - mine[:200]))) > 1e-9, "il controllo NON ha potenza" + print(f" {a}: {len(g):,} giorni confrontati, max|dif| = {dmax:.3e} OK " + f"(e fallisce se la scala e' x1000 -> ha potenza)") + lo = min(rate[a]["series"].index.min() for a in ASSETS) + print(" [COPERTURA] il libro parte dal 2019-03-14, ma `get_funding_rate_history` serve una") + print(f" FINESTRA MOBILE di ~7,3 anni: oggi il piu' vecchio dato e' del {lo.date()}") + print(" (i perpetual INVERSE esistono dal 2018-08/2019-03: la storia c'era, il venue") + print(" non la serve piu'). -> i primi ~47 giorni del backtest sono senza funding") + print(" modellato = 1,7% del campione. Dichiarato, non stimato.") + + +# =========================================================================== +# 1) REPLICHE (funding spento => bit-exact) e 3) LE SERIE COL FUNDING +# =========================================================================== +def tp01_canonical(rate: dict | None) -> tuple[pd.Series, dict]: + """TP01 canonico 50/50 (la lente di `al.tp01_baseline_daily`), con funding opzionale.""" + tp = TrendPortfolio(**CANONICAL) + series, diag = {}, {} + for a in ASSETS: + df = al.get(a, "1d") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + net, _ = tp.net_returns(df) + net = np.asarray(net, float).copy() + tgt = np.asarray(tp.target_series(df), float) + pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1] # tenuta durante la barra i + if rate is not None: + f = daily_funding(rate[a], idx) + net = np.clip(net - pos * f, -0.99, None) + net[0] = 0.0 + diag[a] = dict(pos=pos, f=f, idx=idx) + series[a] = pd.Series(net, index=idx) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + return al._to_daily(0.5 * J["BTC"] + 0.5 * J["ETH"]), diag + + +def tp01_realistic(alloc: float, rate: dict | None) -> tuple[pd.Series, dict]: + """TP01 con min-order $5 (la lente di `CC.tp01_realistic`), con funding opzionale.""" + cap_asset = alloc / len(ASSETS) + tp = TrendPortfolio(**CANONICAL) + series, diag = {}, {} + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + r = simple_returns(df["close"].values.astype(float)) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) + held = np.empty(len(tgt)); cur = 0.0 + for i in range(len(tgt)): + if abs(tgt[i] - cur) * cap_asset >= CC.DERIBIT_MIN_ORDER: + cur = tgt[i] + held[i] = cur + pos = np.zeros(len(held)); pos[1:] = held[:-1] + net = pos * r - CC.DERIBIT_FEE_SIDE * np.abs(np.diff(pos, prepend=0.0)) + fund = np.zeros(len(pos)) + if rate is not None: + fund = pos * daily_funding(rate[a], idx) + net = net - fund + net[0] = 0.0 + diag[a] = dict(pos=pos, fund=fund, idx=idx) + series[a] = pd.Series(np.clip(net, -0.99, None), index=idx) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + out = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + if out.index.tz is None: + out.index = out.index.tz_localize("UTC") + return out, diag + + +def sim_equity_f(ltf: pd.DataFrame, ent: list, mode: str, ts5, c5, + rate: dict | None) -> tuple[np.ndarray, list]: + """Replica di `r02.sim_equity` con UN grado di liberta' in piu': il funding accumulato + fra ingresso e uscita, col SEGNO della posizione (uno short lo INCASSA). + Con rate=None deve dare bit-exact `r02.sim_equity` β€” e' la replica di controllo.""" + c = ltf["close"].values.astype(float) + h = ltf["high"].values.astype(float) + l = ltf["low"].values.astype(float) + n = len(c) + close_ts = ltf["timestamp"].values.astype(np.int64) + r02.MS_LTF + + def px_hour(t: int) -> float: + hb = ((t + MSH - 1) // MSH) * MSH + j = np.searchsorted(ts5, hb, side="left") + return c5[min(j, len(c5) - 1)] + + def t_hour(t: int) -> int: + return int(((t + MSH - 1) // MSH) * MSH) + + initial = 1000.0 + capital = initial + equity = np.full(n, capital, dtype=float) + busy_until = -1 + trades = [] + for i in range(n): + e = ent[i] if i < len(ent) else None + if e is None or e.get("dir", 0) == 0: + equity[i] = capital + continue + if i <= busy_until: + equity[i] = capital + continue + direction = int(e["dir"]) + tp = e.get("tp"); sl = e.get("sl") + max_bars = int(e.get("max_bars") or 24) + entry = c[i] if mode in ("canonical", "barclose") else px_hour(close_ts[i]) + exit_idx = min(i + max_bars, n - 1) + exit_lvl = c[exit_idx] + for j in range(i + 1, min(i + max_bars + 1, n)): + hit_sl = sl is not None and ( + (direction == 1 and l[j] <= sl) or (direction == -1 and h[j] >= sl)) + hit_tp = tp is not None and ( + (direction == 1 and h[j] >= tp) or (direction == -1 and l[j] <= tp)) + if hit_sl: + exit_lvl, exit_idx = sl, j + break + if hit_tp: + exit_lvl, exit_idx = tp, j + break + exit_lvl, exit_idx = c[j], j + if mode == "canonical": + exit_price = exit_lvl + elif mode == "barclose": + exit_price = c[exit_idx] + else: + exit_price = px_hour(close_ts[exit_idx]) + gross = (exit_price - entry) / entry * direction + net = gross - 0.001 + f_trade = 0.0 + if rate is not None: + t_in = close_ts[i] if mode in ("canonical", "barclose") else t_hour(close_ts[i]) + t_out = close_ts[exit_idx] if mode in ("canonical", "barclose") else t_hour(close_ts[exit_idx]) + f_trade = float(cumF(rate, t_out) - cumF(rate, t_in)) * direction + net = net - f_trade + trades.append(dict(i=i, exit_idx=exit_idx, dir=direction, gross=gross, + f=f_trade, hours=(close_ts[exit_idx] - close_ts[i]) / MSH, + t_in=close_ts[i], t_out=close_ts[exit_idx])) + capital += capital * net + capital = max(capital, 1.0) + equity[i:exit_idx + 1] = capital + busy_until = exit_idx + last = initial + for k in range(n): + if equity[k] != last and equity[k] != initial: + last = equity[k] + else: + equity[k] = last + return equity, trades + + +_SKH: dict = {} + + +def skh_daily(a: str, off: int, mode: str, rate: dict | None) -> tuple[pd.Series, list]: + key = (a, off, mode, rate is not None) + if key in _SKH: + return _SKH[key] + _, m0, ltf, ent = r02.run_asset(a, off) + df5 = r02.get5m(a) + ts5 = df5["timestamp"].values.astype(np.int64) + r02.MS5 + c5 = df5["close"].values.astype(float) + eq, tr = sim_equity_f(ltf, ent, mode, ts5, c5, rate[a] if rate else None) + s = pd.Series(eq, index=pd.DatetimeIndex(pd.to_datetime(ltf["datetime"], utc=True))) + d = s.resample("1D").last().ffill().pct_change().dropna() + _SKH[key] = (d, tr) + return _SKH[key] + + +def skh_book(off: int, mode: str, rate: dict | None) -> pd.Series: + J = pd.concat({a: skh_daily(a, off, mode, rate)[0] for a in ASSETS}, + axis=1, join="inner").fillna(0.0) + return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + + +def book_75_25(rate: dict | None, off: int = 0, lens: str = "hourly", + alloc: float = ALLOC) -> pd.Series: + """Il libro LIVE: TP01 75 + SKH01 25, esattamente la lente di CC.book_series.""" + skh = skh_book(off, lens, rate) + if skh.index.tz is None: + skh.index = skh.index.tz_localize("UTC") + tp, _ = tp01_realistic(alloc * 0.75, rate) + return combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": 0.75, "SKH01": 0.25}, + lo=pd.Timestamp("2019-03-14", tz="UTC")) + + +def sezione1(rate: dict) -> None: + sez("1) REPLICA DI CONTROLLO β€” con funding SPENTO ogni replica dev'essere BIT-EXACT") + ok = True + + a0, _ = tp01_canonical(None) + b0 = al.tp01_baseline_daily() + d = float(np.max(np.abs(a0.values - b0.reindex(a0.index).values))) + ok &= d == 0.0 + print(f" tp01_canonical(None) vs al.tp01_baseline_daily max|dif| = {d:.3e} " + f"{'OK' if d == 0 else 'FALLITA'}") + + a1, _ = tp01_realistic(ALLOC * 0.75, None) + b1 = CC.tp01_realistic(ALLOC * 0.75) + d = float(np.max(np.abs(a1.values - b1.reindex(a1.index).values))) + ok &= d == 0.0 + print(f" tp01_realistic(450,None) vs CC.tp01_realistic(450) max|dif| = {d:.3e} " + f"{'OK' if d == 0 else 'FALLITA'}") + + for mode in ("canonical", "hourly"): + for a in ASSETS: + _, m0, ltf, ent = r02.run_asset(a, 0) + df5 = r02.get5m(a) + ts5 = df5["timestamp"].values.astype(np.int64) + r02.MS5 + c5 = df5["close"].values.astype(float) + eqA, _ = sim_equity_f(ltf, ent, mode, ts5, c5, None) + eqB = r02.sim_equity(ltf, ent, mode, ts5_close=ts5, c5=c5) + d = float(np.max(np.abs(eqA - eqB))) + ok &= d == 0.0 + print(f" sim_equity_f({a},{mode:<9}) vs r02.sim_equity " + f"max|dif| = {d:.3e} {'OK' if d == 0 else 'FALLITA'}") + + a2 = book_75_25(None) + b2 = CC.book_series("hourly") + d = float(np.max(np.abs(a2.values - b2.reindex(a2.index).values))) + ok &= d == 0.0 + print(f" book_75_25(None) vs CC.book_series('hourly') max|dif| = {d:.3e} " + f"{'OK' if d == 0 else 'FALLITA'}") + + if not ok: + raise SystemExit("REPLICA FALLITA: nessun delta e' pubblicabile.") + print("\n βœ… 5/5 repliche bit-exact. Da qui in poi l'UNICO grado di liberta' e' il funding.") + + +# =========================================================================== +# 2) IL SEGNO β€” quanto si e' esposti, e da che lato +# =========================================================================== +def sezione2(rate: dict) -> dict: + sez("2) IL SEGNO β€” l'esposizione non e' assunta, e' appaiata col funding del suo strumento") + out = {} + + _, dg = tp01_realistic(ALLOC * 0.75, rate) + print("\n TP01 (long-flat, posizione continua vol-targeted; esposizione = frazione del") + print(" capitale della GAMBA; nel libro pesa 0.75 x 0.5 per asset = 0.375 x posizione)") + print(f" {'asset':>6} {'giorni':>7} {'%tempo>0':>9} {'pos media':>10} {'pos med|>0':>11} " + f"{'funding pagato/anno':>20} {'esposiz. x tasso':>17}") + tot_leg = [] + for a in ASSETS: + pos, f, idx = dg[a]["pos"], dg[a]["fund"], dg[a]["idx"] + yrs = len(pos) / 365.25 + drag = float(f.sum()) / yrs # in frazione del capitale-gamba, per anno + rt = rate[a]["series"] + rt = rt[(rt.index >= idx.min()) & (rt.index <= idx.max())] + print(f" {a:>6} {len(pos):>7,} {float((pos > 0).mean()):>9.1%} {float(pos.mean()):>10.3f} " + f"{float(pos[pos > 0].mean()):>11.3f} {drag:>19.3%} " + f"{float(pos.mean()) * float(rt.mean()) * 24 * 365.25:>16.3%}") + tot_leg.append(drag) + out["tp01_leg_drag"] = float(np.mean(tot_leg)) + print(f" -> drag medio per gamba {out['tp01_leg_drag']:.3%}/anno = sullo SLEEVE " + f"{out['tp01_leg_drag']:.3%}/anno (le due gambe pesano 0.5 ciascuna)") + + print("\n SKH01 (long/short, round-trip discreti, non-overlap: sta a mercato una frazione") + print(" del tempo β€” e uno SHORT il funding lo INCASSA)") + print(f" {'asset':>6} {'trade':>6} {'%long':>7} {'%tempo a mercato':>17} {'ore/trade':>10} " + f"{'pagato/anno':>12} {'incassato/anno':>15} {'NETTO/anno':>11}") + tot_skh = [] + for a in ASSETS: + d, tr = skh_daily(a, 0, "hourly", rate) + span_h = (tr[-1]["t_out"] - tr[0]["t_in"]) / MSH + yrs = span_h / (24 * 365.25) + f = np.array([t["f"] for t in tr]) + dirs = np.array([t["dir"] for t in tr]) + hrs = np.array([t["hours"] for t in tr]) + inmkt = hrs.sum() / span_h + paid = float(f[f > 0].sum()) / yrs + got = float(-f[f < 0].sum()) / yrs + net = float(f.sum()) / yrs + tot_skh.append(net) + print(f" {a:>6} {len(tr):>6} {float((dirs > 0).mean()):>7.1%} {inmkt:>17.1%} " + f"{float(hrs.mean()):>10.1f} {paid:>11.3%} {got:>14.3%} {net:>10.3%}") + out["skh_drag"] = float(np.mean(tot_skh)) + print(f" -> drag NETTO dello sleeve SKH01 {out['skh_drag']:.3%}/anno") + + print("\n ❌ IPOTESI DEL BRIEF FALSIFICATA: Β«SKH01 sta a mercato solo il 12% del tempo ->") + print(" paga MOLTO MENO funding di TP01, ed e' un argomento a suo favore mai contatoΒ».") + print(f" Misurato: SKH01 {out['skh_drag']:.2%}/anno contro TP01 " + f"{out['tp01_leg_drag']:.2%}/anno di sleeve β€” SKH01 paga di PIU', non di meno.") + print(" Due ragioni, entrambe misurabili: (i) quando e' a mercato sta a nozionale 1.0x,") + print(" mentre TP01 vol-targeted sta a 0.14x in media -> a parita' di tempo pesa 7x;") + print(" (ii) entra sui breakout, e i breakout sono proprio quando il funding e' caro.") + + print("\n πŸ“Œ IL MECCANISMO, ed e' il risultato trasferibile: ESPOSIZIONE e FUNDING sono") + print(" POSITIVAMENTE CORRELATI. Tasso medio annualizzato, non condizionato vs") + print(" condizionato all'essere in posizione:") + print(f" {'':>6} {'incondizionato':>15} {'| TP01 long':>12} {'| SKH01 a mercato':>18} " + f"{'rapporto TP01':>14} {'rapporto SKH':>13}") + for a in ASSETS: + ser = rate[a]["series"] + unc = float(ser.mean()) * 24 * 365.25 + # TP01: media del tasso GIORNALIERO pesata dalla posizione (= tasso "che si paga davvero") + pos, f, idx = dg[a]["pos"], dg[a]["fund"], dg[a]["idx"] + fd = daily_funding(rate[a], idx) + w = pos / pos.sum() if pos.sum() > 0 else pos + tp_cond = float(np.sum(w * fd)) * 365.25 + # SKH01: media del tasso orario sulle ore in cui e' a mercato (segno a parte) + _, tr = skh_daily(a, 0, "hourly", rate) + ore = float(np.sum([abs(t["hours"]) for t in tr])) + lordo = float(np.sum([abs(cumF(rate[a], t["t_out"]) - cumF(rate[a], t["t_in"])) + for t in tr])) + skh_cond = lordo / ore * 24 * 365.25 if ore else float("nan") + print(f" {a:>6} {unc:>15.2%} {tp_cond:>12.2%} {skh_cond:>18.2%} " + f"{tp_cond/unc:>13.2f}x {skh_cond/unc:>12.2f}x") + print(" -> il prodotto ingenuo Β«esposizione media x tasso medioΒ» SOTTOSTIMA di ~2x. E' la") + print(" forma esatta della stima grezza da cui e' nato questo filone (14,8% x 0,103 =") + print(" 1,53%/anno): giusta come ordine di grandezza, sbagliata nei due fattori.") + print(" ⚠️ Il rapporto e' su TUTTO il campione e mescola due cose vere ma diverse: la") + print(" correlazione DENTRO l'anno (trend long = perp in contango) e il fatto che i") + print(" due anni di funding piu' caro (2019, 2021) sono anche quelli in cui TP01 era") + print(" piu' long. Per il COSTO non cambia nulla β€” si paga lo stesso β€” ma per") + print(" PREVEDERLO si', e la previsione non e' questo numero.") + + print(f"\n πŸ“Œ A livello di LIBRO 75/25: 0.75 x {out['tp01_leg_drag']:.3%} + " + f"0.25 x {out['skh_drag']:.3%} = " + f"{0.75 * out['tp01_leg_drag'] + 0.25 * out['skh_drag']:.3%}/anno di costo lordo") + print(" (e' una scomposizione, non il risultato: il numero che conta e' il DELTA di") + print(" drift misurato sulla serie composta, sezione 3.)") + out["book_drag_decomp"] = 0.75 * out["tp01_leg_drag"] + 0.25 * out["skh_drag"] + return out + + +# =========================================================================== +# 3) I NUMERI +# =========================================================================== +def stat(s: pd.Series) -> dict: + m = metrics(s) + r = np.asarray(s.dropna().values, float) + return dict(full=al._sh(s), is_=al._sh(s[s.index < HOLDOUT]), hold=al._sh(s[s.index >= HOLDOUT]), + dd=al._dd_ret(s), cagr=m["cagr"], drift=float(r.mean()) * 365.25, + vol=float(r.std()) * np.sqrt(365.25), n=len(r)) + + +def riga(nome: str, a: dict, b: dict) -> None: + print(f" {nome:<34} {a['full']:>7.3f} {a['is_']:>7.3f} {a['hold']:>7.3f} {a['dd']:>7.1%} " + f"{a['cagr']:>7.2%} {a['drift']:>7.2%}") + print(f" {' + funding':<34} {b['full']:>7.3f} {b['is_']:>7.3f} {b['hold']:>7.3f} " + f"{b['dd']:>7.1%} {b['cagr']:>7.2%} {b['drift']:>7.2%}") + print(f" {' DELTA':<34} {b['full']-a['full']:>+7.3f} {b['is_']-a['is_']:>+7.3f} " + f"{b['hold']-a['hold']:>+7.3f} {b['dd']-a['dd']:>+7.1%} {b['cagr']-a['cagr']:>+7.2%} " + f"{b['drift']-a['drift']:>+7.2%}\n") + + +def sezione3(rate: dict) -> dict: + sez("3) I NUMERI COL FUNDING DENTRO β€” netti fee 0.10% RT, hold-out 2025+") + print(f" {'serie':<34} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7} " + f"{'CAGR':>7} {'drift':>7}") + out = {} + + p = {} + p["TP01 canonico (soffitto)"] = (tp01_canonical(None)[0], tp01_canonical(rate)[0]) + p[f"TP01 realistic ${ALLOC*0.75:.0f}"] = (tp01_realistic(ALLOC * 0.75, None)[0], + tp01_realistic(ALLOC * 0.75, rate)[0]) + p["SKH01 50/50 canonical"] = (skh_book(0, "canonical", None), skh_book(0, "canonical", rate)) + p["SKH01 50/50 path live"] = (skh_book(0, "hourly", None), skh_book(0, "hourly", rate)) + p["LIBRO 75/25 (path live)"] = (book_75_25(None), book_75_25(rate)) + for k, (A, B) in p.items(): + sa, sb = stat(A), stat(B) + riga(k, sa, sb) + out[k] = (sa, sb) + + print(" Per ANNO, libro 75/25 (drift lordo -> netto funding). La colonna ESPOSIZIONE serve") + print(" a non confondere 'il funding e' sceso' con 'il libro era flat': sono cose diverse e") + print(" producono lo stesso delta piccolo.") + A, B = p["LIBRO 75/25 (path live)"] + _, dgx = tp01_realistic(ALLOC * 0.75, rate) + expo = pd.concat({a: pd.Series(dgx[a]["pos"], index=dgx[a]["idx"]) for a in ASSETS}, + axis=1, join="inner").mean(axis=1) + print(f" {'anno':>6} {'senza':>9} {'con':>9} {'delta':>9} {'funding annualiz.':>18} " + f"{'esposiz. TP01':>14}") + for y in sorted(set(A.index.year)): + ga, gb = A[A.index.year == y], B[B.index.year == y] + if len(ga) < 30: + continue + da, db = float(ga.mean()) * 365.25, float(gb.mean()) * 365.25 + fr = np.mean([rate[a]["series"][rate[a]["series"].index.year == y].mean() + for a in ASSETS]) * 24 * 365.25 + ex = float(expo[expo.index.year == y].mean()) + print(f" {y:>6} {da:>9.2%} {db:>9.2%} {db-da:>+9.2%} {fr:>17.2%} {ex:>14.3f}") + rec = pd.Timestamp("2025-01-01", tz="UTC") + dA, dB = float(A[A.index >= rec].mean()) * 365.25, float(B[B.index >= rec].mean()) * 365.25 + print(f" {'2025+':>6} {dA:>9.2%} {dB:>9.2%} {dB-dA:>+9.2%} " + f"{np.mean([rate[a]['series'][rate[a]['series'].index >= rec].mean() for a in ASSETS])*24*365.25:>17.2%} " + f"{float(expo[expo.index >= rec].mean()):>14.3f}") + print(" πŸ“Œ IL COSTO NON E' STAZIONARIO, e le due cause si LEGGONO nelle due colonne: il") + print(" funding Deribit si e' compresso (16,9% nel 2021 -> 1,9% nel 2026) E il libro sta") + print(" meno esposto. Chi cita UN numero per il funding cita una media su un regime") + print(" cambiato; chi cita il numero del 2026 sta citando un libro quasi flat.") + + # finestra SOLO-LINEARE (nessun proxy): il controllo che il numero non venga dal proxy + lo = rate["BTC"]["switch"] + print(f"\n CONTROLLO β€” stessa misura sulla sola finestra LINEARE (>= {lo.date()},") + print(" nessun proxy inverse):") + print(f" {'serie':<34} {'d ShFULL':>9} {'d CAGR':>9} {'d drift':>9}") + for k, (A, B) in p.items(): + A2, B2 = A[A.index >= lo], B[B.index >= lo] + sa, sb = stat(A2), stat(B2) + print(f" {k:<34} {sb['full']-sa['full']:>+9.3f} {sb['cagr']-sa['cagr']:>+9.2%} " + f"{sb['drift']-sa['drift']:>+9.2%}") + out["_pairs"] = p + return out + + +# =========================================================================== +# 4) LE DECISIONI +# =========================================================================== +def sezione4a(res3: dict) -> None: + sez("4a) IL SOFFITTO ~1.3 SI ABBASSA?") + a, b = res3["TP01 canonico (soffitto)"] + print(f" TP01 canonico (l'unica serie a cui il 'soffitto ~1.3' si riferisce):") + print(f" Sharpe FULL {a['full']:.3f} -> {b['full']:.3f} ({b['full']-a['full']:+.3f})") + print(f" CAGR {a['cagr']:.2%} -> {b['cagr']:.2%}") + print(f" Il soffitto e' un numero arrotondato a UNA cifra: si abbassa di " + f"{abs(b['full']-a['full']):.3f}, quindi resta '~1.3'" + if abs(b["full"] - a["full"]) < 0.05 else + f" Il soffitto si sposta di {b['full']-a['full']:+.3f}: va RIDETTO.") + print(" ⚠️ Ma la lettura utile e' l'altra: il soffitto e' una soglia di SELEZIONE, e ogni") + print(" candidato direzionale mai valutato in questo progetto e' stato misurato SENZA") + print(" funding. Il confronto candidato-vs-TP01 e' quindi appaiato (entrambi al lordo)") + print(" per i candidati LONG/SHORT a esposizione simile, e distorto a favore di chi sta") + print(" a mercato di piu'. Chi paga di piu' e' chi e' piu' esposto piu' a lungo.") + + +def sezione4b(rate: dict, offsets: tuple) -> None: + sez("4b) IL PESO 75/25 SI MUOVE? (il funding e' un argomento a favore di SKH01 mai contato)") + print(" L'ipotesi era: Β«SKH01 sta a mercato una frazione del tempo e puo' incassare da") + print(" short -> paga MENO funding di TP01, quindi il peso ottimale saleΒ». La prima meta'") + print(" e' gia' FALSIFICATA in sez. 2 (SKH01 paga di piu' per dollaro di sleeve). Resta la") + print(" seconda, che non e' implicata dalla prima: il peso si sposta?") + print(f" Sweep w in {list(skl.W_GRID)} x {len(offsets)} offset della griglia 230m/690m.") + print(" Statistica = MEDIANA DELLE DIFFERENZE APPAIATE (regola del progetto), lente") + print(" 'hourly' = path live, selezione sullo Sharpe IN-SAMPLE come il 24/07.\n") + + B0, _ = tp01_canonical(None) + B1, _ = tp01_canonical(rate) + + def blend(B, skh, w): + if w == 0.0: + return B + bl = combine_outer({"TP": B, "SKH": skh}, {"TP": 1.0 - w, "SKH": w}) + return bl[bl.index >= B.index.min()] + + S = {False: {}, True: {}} + for off in offsets: + S[False][off] = skh_book(off, "hourly", None) + S[True][off] = skh_book(off, "hourly", rate) + + print(f" {'w':>6} | {'IS mediano':>11} {'IS med +f':>11} {'d IS':>8} {'>0':>5} | " + f"{'FULL med':>9} {'FULL +f':>9} {'d FULL':>8} | {'HOLD med':>9} {'HOLD +f':>9}") + rows = {} + for w in skl.W_GRID: + vI0, vI1, vF0, vF1, vH0, vH1 = [], [], [], [], [], [] + for off in offsets: + b0 = blend(B0, S[False][off], w) + b1 = blend(B1, S[True][off], w) + vI0.append(al._sh(b0[b0.index < HOLDOUT])); vI1.append(al._sh(b1[b1.index < HOLDOUT])) + vF0.append(al._sh(b0)); vF1.append(al._sh(b1)) + vH0.append(al._sh(b0[b0.index >= HOLDOUT])); vH1.append(al._sh(b1[b1.index >= HOLDOUT])) + dI = np.array(vI1) - np.array(vI0) + dF = np.array(vF1) - np.array(vF0) + rows[w] = dict(is0=np.median(vI0), is1=np.median(vI1), dI=np.median(dI), + f0=np.median(vF0), f1=np.median(vF1), dF=np.median(dF), + h0=np.median(vH0), h1=np.median(vH1), pos=float((dI > 0).mean())) + r_ = rows[w] + print(f" {w:>6.2f} | {r_['is0']:>11.3f} {r_['is1']:>11.3f} {r_['dI']:>+8.3f} " + f"{r_['pos']:>5.0%} | {r_['f0']:>9.3f} {r_['f1']:>9.3f} {r_['dF']:>+8.3f} | " + f"{r_['h0']:>9.3f} {r_['h1']:>9.3f}") + + w0 = max(skl.W_GRID, key=lambda w: rows[w]["is0"]) + w1 = max(skl.W_GRID, key=lambda w: rows[w]["is1"]) + print(f"\n argmax dello Sharpe IS mediano: SENZA funding w* = {w0:.2f} " + f"CON funding w* = {w1:.2f}") + print(f" peso live = 0.25. Regola a priori del 24/07: a parita' (<{skl.TIE_TOL}) si sta al") + print(f" piu' vicino a 0.25. |IS(w*) - IS(0.25)| con funding = " + f"{abs(rows[w1]['is1'] - rows[0.25]['is1']):.3f}") + if w0 == w1: + print(" πŸ“Œ Il funding NON sposta l'argmax: il vantaggio di SKH01 e' reale ma piccolo") + print(" rispetto alla larghezza del plateau. Peso INVARIATO.") + else: + print(f" πŸ“Œ L'argmax si sposta {w0:.2f} -> {w1:.2f}: da verificare con weights_tilt_null.") + + +def sezione4c(rate: dict) -> dict: + sez("4c) I MURI DI CAPITALE E LA RENDITA PERPETUA (vivono sul DRIFT, non sullo Sharpe)") + A, B = book_75_25(None), book_75_25(rate) + need_gross = CC.TARGET_EUR_DAY * 365.0 * EURUSD / (1 - CC.TAX_RATE) + print(f" bersaglio: 50 EUR/g netti = ${need_gross:,.0f}/anno lordi (aliquota " + f"{CC.TAX_RATE:.0%}), stessa convenzione di r0726_capwall_refresh.") + print(f" de-luck del drift x{DELUCK} (MISURATO, r0726_deluck_factor). MC {N_PATHS_WALL} path") + print(" -> risoluzione ~1% sul muro: si citano 3 cifre significative, non il dollaro.\n") + print(f" {'serie':<28} {'drift':>8} {'vol':>7} {'perpetua':>9} {'muro':>12} {'leva':>5}") + out = {} + for nome, s in (("senza funding", A), ("con funding", B)): + r = s.values.astype(float) + r_dl = r - (1.0 - DELUCK) * float(r.mean()) + for k in (1.0, 1.5): + _, perp, cap = WR.perp_and_wall(r_dl, k, need_gross) + out[(nome, k)] = (perp, cap) + print(f" {nome + f' x{DELUCK}':<28} {float(r_dl.mean())*365.25:>8.2%} " + f"{float(r_dl.std())*np.sqrt(365.25):>7.2%} {perp:>9.2%} ${cap:>11,.0f} {k:>5.2f}") + p0, c0 = out[("senza funding", 1.0)] + p1, c1 = out[("con funding", 1.0)] + print(f"\n πŸ“Œ A leva 1.0: perpetua {p0:.2%} -> {p1:.2%}; muro ${c0:,.0f} -> ${c1:,.0f} " + f"({(c1/c0-1):+.1%})") + print(f" Il muro pubblicato e' $272k (lordo) / $258k (netto fisco). Qui si misura il") + print(f" DELTA con la stessa macchineria, non un muro nuovo.") + + print("\n TRAIETTORIA da $600 al capitale-rendita (mediana anni / P entro 20a), stesso") + print(" bootstrap a blocchi di r0726_capwall_refresh.trajectory:") + print(f" {'EUR/mese':>9} | {'senza funding':>22} | {'con funding':>22}") + r0 = A.values.astype(float); r0 = r0 - (1 - DELUCK) * r0.mean() + r1 = B.values.astype(float); r1 = r1 - (1 - DELUCK) * r1.mean() + t0 = WR.trajectory(r0, c0, n_paths=2000) + t1 = WR.trajectory(r1, c1, n_paths=2000) + for dep in (0, 250, 500, 1_000, 2_000): + a_, b_ = t0[dep], t1[dep] + fa = f"{a_['p50']:>6.1f}a P20 {a_['p20y']:>5.0%}" if a_["p50"] == a_["p50"] else f"{'mai':>16}" + fb = f"{b_['p50']:>6.1f}a P20 {b_['p20y']:>5.0%}" if b_["p50"] == b_["p50"] else f"{'mai':>16}" + print(f" {dep:>9} | {fa:>22} | {fb:>22}") + return out + + +def g_exact(r: np.ndarray, k: float) -> float: + x = 1.0 + k * r + return float(np.mean(np.log(np.maximum(x, 1e-12)))) * 365.25 + + +def k_star(r: np.ndarray, kmax: float = 40.0, step: float = 0.05) -> tuple[float, float]: + ks = np.arange(step, kmax + step, step) + g = np.array([g_exact(r, k) for k in ks]) + j = int(np.argmax(g)) + return float(ks[j]), float(g[j]) + + +def sezione4d(rate: dict) -> None: + sez("4d) k* E IL GRADINO DI LEVA β€” il funding SCALA con la leva") + print(" Il funding e' proporzionale al NOZIONALE: a leva k si paga k volte tanto. Quindi") + print(" la serie col funding va scalata per k come tutto il resto (il costo non e' fisso),") + print(" e il gradino 1.00x -> 1.25/1.50x misurato ieri ne mangia una parte.\n") + A, B = book_75_25(None), book_75_25(rate) + print(f" {'serie':<22} {'drift':>8} {'k*':>7} {'g(k*)':>8} {'g(1.00)':>9} {'g(1.25)':>9} " + f"{'g(1.50)':>9} {'mezzo-Kelly':>12}") + for nome, s in (("senza funding", A), ("con funding", B)): + r = s.values.astype(float) + ks, gs = k_star(r) + print(f" {nome:<22} {float(r.mean())*365.25:>8.2%} {ks:>7.2f} {gs:>8.2%} " + f"{g_exact(r, 1.0):>9.2%} {g_exact(r, 1.25):>9.2%} {g_exact(r, 1.50):>9.2%} " + f"{ks/2:>12.2f}") + r_dl = r - (1 - DELUCK) * r.mean() + ks2, gs2 = k_star(r_dl) + print(f" {' (de-luck x0.89)':<22} {float(r_dl.mean())*365.25:>8.2%} {ks2:>7.2f} " + f"{gs2:>8.2%} {g_exact(r_dl, 1.0):>9.2%} {g_exact(r_dl, 1.25):>9.2%} " + f"{g_exact(r_dl, 1.50):>9.2%} {ks2/2:>12.2f}") + rA = A.values.astype(float); rA -= (1 - DELUCK) * rA.mean() + rB = B.values.astype(float); rB -= (1 - DELUCK) * rB.mean() + gain0 = g_exact(rA, 1.25) - g_exact(rA, 1.0) + gain1 = g_exact(rB, 1.25) - g_exact(rB, 1.0) + print(f"\n πŸ“Œ Guadagno in log-crescita del gradino 1.00 -> 1.25 (de-luckato):") + print(f" senza funding {gain0:+.2%}/anno Β· con funding {gain1:+.2%}/anno " + f"(-{(1-gain1/gain0)*100:.0f}% del guadagno)" if gain0 > 0 else "") + print(" Il gradino resta positivo finche' k* > 1.25 con il funding dentro.") + + +# =========================================================================== +# 5) CONTROLLO SUL CONTO VERO +# =========================================================================== +RE_HDR = re.compile(r"^===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book") +RE_POS = re.compile(r"^\s+(BTC|ETH) TP .*?\|\s*pos \$([+-][\d,\.]+)") + + +def leggi_posizioni_live() -> pd.DataFrame: + """Posizione oraria REALE per asset, ricostruita da logs/cron_book.log.""" + rows, ts = [], None + for ln in (ROOT / "logs" / "cron_book.log").read_text(errors="replace").splitlines(): + m = RE_HDR.match(ln) + if m: + ts = pd.Timestamp(m.group(1)) + continue + m = RE_POS.match(ln) + if m and ts is not None: + rows.append(dict(ts=ts, asset=m.group(1), + pos=float(m.group(2).replace(",", "").replace("+", "")))) + return pd.DataFrame(rows) + + +RE_ORD = re.compile(r"^\s+-> (BUY|SELL) ") +RE_LEG = re.compile(r"^\s+(BTC|ETH) TP ") + + +def ordini_sessione(settl: pd.Timestamp) -> dict: + """Ordini REALMENTE inviati per asset dalla data `settl` (righe '-> BUY/SELL' del log, + che seguono la riga della gamba a cui appartengono).""" + out: dict = {} + ts, leg = None, None + for ln in (ROOT / "logs" / "cron_book.log").read_text(errors="replace").splitlines(): + m = RE_HDR.match(ln) + if m: + ts = pd.Timestamp(m.group(1)); leg = None + continue + m = RE_LEG.match(ln) + if m: + leg = m.group(1) + continue + m = RE_ORD.match(ln) + if m and leg and ts is not None and ts >= settl: + b, s_ = out.get(leg, (0, 0)) + out[leg] = (b + (m.group(1) == "BUY"), s_ + (m.group(1) == "SELL")) + return out + + +def sezione5(rate: dict) -> None: + sez("5) CONTROLLO SUL CONTO VERO β€” l'unico che non passa da un'assunzione") + df = leggi_posizioni_live() + if not len(df): + print(" logs/cron_book.log non leggibile o formato cambiato -> NON MISURATO.") + return + print(f" {len(df):,} osservazioni orarie di posizione ricostruite dal log del cron,") + print(f" {df['ts'].min()} -> {df['ts'].max()}") + print(" ⚠️ `data/live/book_executions.jsonl` ha `ts_utc` a 00:00:00 su 18/18 righe (difetto") + print(" gia' noto, ondata 22/08 Β§5): l'ora vera della posizione sta SOLO nel log del cron.") + + def paid(g: pd.DataFrame, a: str) -> float: + t = to_ms(pd.DatetimeIndex(g["ts"])) + return float((g["pos"].values * (cumF(rate[a], t + MSH) - cumF(rate[a], t))).sum()) + + tot = {} + for a in ASSETS: + g = df[df["asset"] == a].sort_values("ts") + tot[a] = paid(g, a) + print(f" {a}: {len(g):>4} ore, posizione media ${g['pos'].mean():>7.2f} " + f"(a mercato {float((g['pos'].values != 0).mean()):.0%}, max ${g['pos'].max():.0f})" + f" -> funding modellato ${tot[a]:+.4f}") + print(f" TOTALE funding modellato da quando il libro esegue: ${sum(tot.values()):+.4f}") + print(" (a $635 di conto e posizioni da $40-190: e' un numero da CENTESIMI. La potenza del") + print(" controllo non viene dalla taglia, viene dal fatto che sia CONFRONTABILE.)") + + print("\n Cio' che il conto RIPORTA (sola lettura via cerbero-mcp, nessun ordine):") + try: + from src.live.deribit import DeribitRead + ps = DeribitRead().positions("USDC") + except Exception as e: + print(f" conto non leggibile ({type(e).__name__}) -> confronto NON MISURATO.") + return + rp = {} + for p_ in ps: + inst = str(p_.get("instrument", "")) + a = "BTC" if inst.startswith("BTC") else "ETH" + rp[a] = float(p_.get("realized_pnl", 0.0)) + print(f" {inst:<22} size ${float(p_.get('size', 0)):>8.2f} " + f"realized_pnl {rp[a]:>+9.6f} unrealized {float(p_.get('unrealized_pnl', 0)):>+9.6f}") + + print("\n QUALE finestra? Deribit REGOLA i perpetual una volta al giorno alle 08:00 UTC.") + print(" Se `realized_pnl` e' il realizzato di SESSIONE, dev'essere il funding dall'ultimo") + print(" settlement β€” e allora la finestra giusta si riconosce perche' le altre NON tornano:") + now = pd.Timestamp.now(tz="UTC") + settl = now.normalize() + pd.Timedelta(hours=8) + if settl > now: + settl -= pd.Timedelta("1D") + fin = {a: df[df["asset"] == a].sort_values("ts") for a in ASSETS} + apert = {} + for a in ASSETS: + g = fin[a].reset_index(drop=True) + z = np.where(g["pos"].values == 0)[0] + apert[a] = g["ts"].iloc[int(z[-1])] if len(z) else g["ts"].iloc[0] + print(f" {'finestra':<34} {'BTC modello':>13} {'BTC conto':>11} {'err':>8} | " + f"{'ETH modello':>13} {'ETH conto':>11} {'err':>8}") + for lbl, lo in ((f"dal settlement {settl:%m-%d %H:%M} UTC", {a: settl for a in ASSETS}), + ("dall'apertura della posizione", apert), + ("ultime 24h", {a: now - pd.Timedelta("24h") for a in ASSETS}), + ("da quando il libro esegue", {a: fin[a]["ts"].min() for a in ASSETS})): + cells = [] + for a in ASSETS: + g = fin[a][pd.DatetimeIndex(fin[a]["ts"]) >= lo[a]] + m = paid(g, a) + err = (m / abs(rp[a]) - 1) if rp.get(a) else float("nan") + cells.append(f"{m:>13.6f} {rp.get(a, float('nan')):>11.6f} {err:>+7.1%}") + print(f" {lbl:<34} {cells[0]} | {cells[1]}") + + # (1) va MISURATO sugli ORDINI VERI, non sulle variazioni del nozionale. + # ⚠️ ERRORE CATTURATO IN SESSIONE: la prima stesura contava `diff(pos) < 0` come + # "vendite" e ne trovava 3 per asset. NON erano vendite: `pos` e' il valore in USD di + # una posizione a taglia costante, e scende da solo quando il mark scende. Su un + # perpetual lineare il nozionale si muove SEMPRE, anche a mano ferma. + ordini = ordini_sessione(settl) + print("\n πŸ“Œ IDENTIFICAZIONE, e non e' un accordo di grandezza β€” e' un'ELIMINAZIONE:") + print(f" (1) ordini realmente inviati nella sessione (righe '-> BUY/SELL' del log, " + f"dalle {settl:%H:%M} UTC): " + + " Β· ".join(f"{a} {ordini.get(a, (0, 0))[0]} buy / {ordini.get(a, (0, 0))[1]} sell" + for a in ASSETS)) + if sum(v[1] for v in ordini.values()) == 0: + print(" ZERO vendite -> nessuna chiusura, nemmeno parziale -> il P&L di TRADING") + print(" realizzato di sessione e' ESATTAMENTE ZERO, e qualunque valore non nullo") + print(" di `realized_pnl` non puo' essere P&L di trading.") + else: + print(" ⚠️ Ci sono state VENDITE: parte di `realized_pnl` puo' essere P&L di") + print(" trading e l'eliminazione (1) NON vale in questa sessione. Restano (2)(3).") + print(" (2) il segno e' NEGATIVO su entrambe le gambe, che sono entrambe LONG: un long") + print(" in contango PAGA. E' il segno che il funding deve avere, non un caso.") + print(" (3) la finestra 'dal settlement' torna; 'dall'apertura' sbaglia di ~2-3x e") + print(" '24h' di ~15%. Il controllo ha POTENZA: le finestre sbagliate si vedono.") + print(" -> `realized_pnl` del proxy = realizzato di SESSIONE Deribit, che su una") + print(" posizione non chiusa E' il funding. Il modello di questo script e' quindi") + print(" VALIDATO contro il conto reale, non solo contro se stesso.") + print(" ⚠️ IL RESIDUO, misurato invece che raccontato. Il modello SOTTOSTIMA su entrambe le") + print(" gambe. Espresso in ORE-equivalenti all'ultimo tasso orario osservato:") + for a in ASSETS: + g = fin[a][pd.DatetimeIndex(fin[a]["ts"]) >= settl] + res = abs(rp.get(a, 0.0)) - paid(g, a) + ult = float(rate[a]["series"].iloc[-1]) * float(g["pos"].iloc[-1]) + print(f" {a}: residuo ${res:+.6f} = {res/ult if ult else float('nan'):+.2f} ore " + f"di funding all'ultimo tasso (${ult:.6f}/ora)") + print(" Meccanismo candidato e coerente col segno su 2/2: il conto accumula in CONTINUO,") + print(" l'ultimo record orario e' delle 22:00 e la lettura e' delle 22:1x -> i minuti") + print(" dopo l'ultimo record non sono modellati. NON e' verificato che spieghi TUTTO il") + print(" residuo di BTC: si riporta la taglia, non una storia.") + print(" ⚠️ Limite: e' UNA sessione, su due posizioni. Non e' una serie storica di") + print(" pagamenti di funding β€” quella il conto non la espone via questo proxy.") + + +# =========================================================================== +# 6) COSA NON E' CATTURATO +# =========================================================================== +def sezione6(rate: dict) -> None: + sez("6) COSA NON E' CATTURATO β€” dichiarato e, dove si puo', quantificato") + + print(" (a) L'approssimazione 'esposizione MEDIA x tasso MEDIO' contro l'accrual ESATTO.") + print(" Su TP01 non c'e' differenza (posizione continua, accrual giornaliero esatto).") + print(" Su SKH01, che entra ed esce, la differenza si MISURA:") + print(f" {'asset':>6} {'accrual esatto/anno':>20} {'media x media/anno':>20} " + f"{'errore':>9} {'errore rel.':>12}") + for a in ASSETS: + _, tr = skh_daily(a, 0, "hourly", rate) + span_h = (tr[-1]["t_out"] - tr[0]["t_in"]) / MSH + yrs = span_h / (24 * 365.25) + esatto = float(np.sum([t["f"] for t in tr])) / yrs + # approssimazione: (ore a mercato x direzione media) x tasso medio del periodo + hrs = np.array([t["hours"] for t in tr]); dirs = np.array([t["dir"] for t in tr]) + rt = rate[a]["series"] + rt = rt[(rt.index >= pd.Timestamp(tr[0]["t_in"], unit="ms", tz="UTC")) & + (rt.index <= pd.Timestamp(tr[-1]["t_out"], unit="ms", tz="UTC"))] + approx = float((hrs * dirs).sum() * rt.mean()) / yrs + err = esatto - approx + print(f" {a:>6} {esatto:>19.4%} {approx:>19.4%} {err:>+8.4%} " + f"{(err/abs(esatto) if esatto else float('nan')):>11.1%}") + print(" -> l'approssimazione media-x-media NON e' innocua su uno sleeve che sceglie") + print(" QUANDO stare a mercato: e' la stessa forma dell'errore che il progetto ha") + print(" gia' pagato altrove (una media appaiata non e' la media delle medie).") + print(" Qui il numero pubblicato usa l'ACCRUAL ESATTO per trade.") + + sw = min(rate[a]["switch"] for a in ASSETS) + b0 = pd.Timestamp("2019-03-14", tz="UTC") + tot = (rate["BTC"]["series"].index.max() - b0).days + print(f"\n (b) Il PROXY inverse prima del {sw.date()} ({(sw-b0).days/tot:.0%} del campione).") + print(" L'accordo misurato") + print(" in sezione 0 e' la banda dell'errore; la sezione 3 riporta anche la finestra") + print(" SOLO-LINEARE, dove il proxy non entra.") + + print("\n (c) LA DISCRETEZZA. Deribit accumula il funding in CONTINUO (interest_1h e' gia'") + print(" il realizzato dell'ora): su questo venue non c'e' il problema 'posizione") + print(" all'istante del pagamento' che hanno i venue a 8h. Cio' che resta discreto e'") + print(" l'istante di USCITA di SKH01 dentro la barra 230m: il modello lo pone alla") + print(" CHIUSURA della barra di trigger (limite SUPERIORE del funding pagato).") + print(" Banda misurata sostituendo l'apertura della barra di trigger:") + for a in ASSETS: + _, tr = skh_daily(a, 0, "hourly", rate) + span_h = (tr[-1]["t_out"] - tr[0]["t_in"]) / MSH + yrs = span_h / (24 * 365.25) + hi = float(np.sum([t["f"] for t in tr])) / yrs + lo_ = float(np.sum([ + (cumF(rate[a], t["t_out"] - r02.MS_LTF) - cumF(rate[a], t["t_in"])) * t["dir"] + for t in tr])) / yrs + print(f" {a}: [{lo_:.4%}, {hi:.4%}] /anno di sleeve (ampiezza {hi-lo_:.4%})") + + print("\n (d) NON e' modellato il funding sul collaterale (il conto e' USDC, il margine non") + print(" rende) ne' il costo di prestito: non esistono su questo percorso.") + print(" (e) Il libro di RICERCA a 5 sleeve non e' ricalcolato qui: VRP01 e' opzioni (niente") + print(" funding), GTAA01 e' azioni (niente funding, e non e' deployabile), XS01 e XSR01") + print(" sono su Hyperliquid e il loro funding e' un'ALTRA misura, market-neutral per") + print(" costruzione (long e short si compensano solo se il funding e' uniforme).") + + +# =========================================================================== +def main() -> None: + print("=" * 100) + print("r0822d β€” IL FUNDING DEI PERPETUAL: il costo che il progetto non ha mai prezzato") + print(f"branch research/wave-0822 Β· {pd.Timestamp.now(tz='UTC')} Β· rete={'SI' if USE_NET else 'NO'}") + print("=" * 100) + + d0 = sezione0() + rate = build_rate(d0["F"]) + print(f"\n serie oraria unificata per asset (linear dove esiste, inverse prima):") + for a in ASSETS: + print(f" {a}: {len(rate[a]['ts']):,} ore, switch inverse->linear il " + f"{rate[a]['switch'].date()}") + + verifica_scala(rate, d0["F"]) + + print(f"\n{clock()} sezione 1...") + sezione1(rate) + print(f"\n{clock()} sezione 2...") + d2 = sezione2(rate) + print(f"\n{clock()} sezione 3...") + d3 = sezione3(rate) + print(f"\n{clock()} sezione 4...") + sezione4a(d3) + sezione4b(rate, r02.OFFSETS) + sezione4c(rate) + sezione4d(rate) + print(f"\n{clock()} sezione 5...") + sezione5(rate) + sezione6(rate) + + sez("GATE β€” esito di ciascuno, e quelli non girati DICHIARATI") + A0, B0 = book_75_25(None), book_75_25(rate) + ns = int(sum(len(skh_daily(a, 0, "hourly", rate)[1]) for a in ASSETS)) + npos = int(sum(np.sum(np.array([t["gross"] for t in skh_daily(a, 0, "hourly", rate)[1]]) <= 0) + for a in ASSETS)) + ip = al.implausible_sharpe(B0, n_trades=ns, n_losing_trades=npos) + print(f" implausible_sharpe : implausible={ip['implausible']} " + f"(Sharpe {ip['sharpe']:.2f}, maxDD {ip['maxdd']:.1%}, Calmar {ip['calmar']:.2f}, " + f"barre attive in perdita {ip['loss_frac']:.1%}) -> {ip['reasons'] or 'nessuna firma'}") + print(" replica bit-exact : PASSATA 5/5 (sez. 1) β€” nessun delta stampato prima.") + print(" controllo di scala tempi: PASSATO, con potenza verificata (sez. 0).") + print(" conto reale : PASSATO β€” modello vs `realized_pnl` di sessione, sez. 5.") + print(" anchor (banda d'ancora) : GIRATO sul peso β€” 23 offset, mediana delle differenze") + print(" APPAIATE (sez. 4b). Le altre tabelle sono all'ancora") + print(" canonica: il funding e' un COSTO deterministico, non una") + print(" selezione, e il suo delta e' negativo a 23/23 offset.") + print(" marginal_vs_tp01 / DSR / study_family_honest / de-levering null / causality:") + print(" NON GIRATI, e non per budget: qui non c'e' nessun") + print(" CANDIDATO. Non si propone una strategia, si toglie un") + print(" costo che mancava a quelle esistenti. Non c'e' una") + print(" griglia su cui barare e non c'e' un segnale di cui") + print(" verificare la causalita' (il funding di un'ora si") + print(" applica alla posizione tenuta in quell'ora).") + print(" weights_tilt_null : NON GIRATO perche' non serve β€” l'argmax del peso NON si") + print(" sposta (sez. 4b), quindi non c'e' nessun cambio di pesi") + print(" da sottoporre al gate.") + + sez("SINTESI") + a, b = d3["LIBRO 75/25 (path live)"] + print(f" LIBRO 75/25 path live: drift {a['drift']:.2%} -> {b['drift']:.2%} " + f"({b['drift']-a['drift']:+.2%}/anno)") + print(f" Sharpe FULL {a['full']:.3f} -> {b['full']:.3f}, " + f"HOLD {a['hold']:.3f} -> {b['hold']:.3f}, maxDD {a['dd']:.1%} -> {b['dd']:.1%}") + print(f" quota del drift bruciata dal funding: {(a['drift']-b['drift'])/a['drift']:.1%}") + print(f" decomposizione (sez.2): {d2['book_drag_decomp']:.3%}/anno") + rec = pd.Timestamp("2025-01-01", tz="UTC") + A_, B_ = d3["_pairs"]["LIBRO 75/25 (path live)"] + dr = (float(A_[A_.index >= rec].mean()) - float(B_[B_.index >= rec].mean())) * 365.25 + lo_ = min(rate[x]["switch"] for x in ASSETS) + A2, B2 = A_[A_.index >= lo_], B_[B_.index >= lo_] + dl = (float(A2.mean()) - float(B2.mean())) * 365.25 + print(f" ... {dl:.2%}/anno sulla sola finestra dello strumento VERO (>= {lo_.date()}, senza") + print(f" proxy), e {dr:.2%}/anno sul solo 2025+. IL NUMERO VA CITATO CON LA FINESTRA:") + print(f" la media di 7 anni non e' l'aspettativa di domani.") + print("\n COSA CAMBIA e COSA NO:") + print(" CAMBIA Β· il muro di capitale e la rendita perpetua (+17-18% di muro: la rendita") + print(" vive sul DRIFT, e il funding e' una tassa sul drift)") + print(" Β· la traiettoria da $600 (a EUR250/mese, P(entro 20a) 90% -> 64%)") + print(" Β· il soffitto direzionale BTC/ETH: ~1,31 -> ~1,15 netto funding") + print(" NON CAMBIA Β· il peso 75/25 (argmax invariato a 23/23 offset)") + print(" Β· il gradino di leva 1,00 -> 1,25x (k* 13,7 -> 12,3, guadagno -12%)") + print(" Β· il maxDD (+0,3pp: e' una tassa sul drift, non sulla coda)") + print(" Β· l'ordine delle leve del piano: versare resta la prima di tutte") + + print("\n GRIGLIA DICHIARATA (contata al RIALZO):") + print(" 4 strumenti scaricati x ~55-90 richieste = ~290 chiamate al venue") + print(" 3 ipotesi sulla convenzione temporale Β· 2 lenti (merged / solo-lineare)") + print(" 5 serie x 2 (con/senza funding) = 10 valutazioni in sez. 3, + 8 anni di split") + print(" 8 pesi x 23 offset x 2 = 368 valutazioni di blend in sez. 4b") + print(" 4 muri x 26 passi di bisezione x 2.000 path MC + 2 x 5 livelli di traiettoria") + print(" 2 (con/senza) x 800 valori di k in sez. 4d Β· 4 finestre x 2 gambe in sez. 5") + print(" NESSUNA di queste e' una SELEZIONE: non c'e' una cella scelta guardando il") + print(" risultato, quindi non c'e' un deflated-Sharpe da calcolare. La griglia ridotta") + print(" per budget e': i muri girano a 2.000 path invece di 4.000 (risoluzione ~1%,") + print(" dichiarata), e le tabelle non-4b sono all'ancora canonica.") + print(f"\n{clock()} fine.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822d_gate_c.py b/scripts/research/r0822d_gate_c.py new file mode 100644 index 0000000..080d1b8 --- /dev/null +++ b/scripts/research/r0822d_gate_c.py @@ -0,0 +1,1103 @@ +"""r0822d_gate_c β€” GATE PROP-01, gamba (c): il BIGLIETTO riprezzato come OPZIONE (2026-08-22). + +IL BUCO CHE CHIUDE +------------------ +`GATE PROP-01` (RESULTS-0822 Β§29) ha tre gambe. Due sono state chiuse stanotte: + (a) LISTINO -> PASS 13/13, misurato sul venue (Bybit `instruments-info`, 833 strumenti); + (b) ANCORA SKH01 -> PASS, delta appaiato +0,116 sui 23 offset, 230/230 celle congiunte + positive β€” ma il numero OPERATIVO e' sceso a P(>=50 EUR/g) = 4,4% [2,5-8,2%], P(zero) 35,3%. +La gamba (c) e' rimasta una CONSTATAZIONE, non un criterio: + + "un'eval HYRO $100k costa $579 su un conto di $635 = il 91%" + +e per giunta poggia su un'economia sbagliata. La pagina della firm dichiara la quota come +**"Refundable Challenge Deposit, returned in full alongside your first funded payout"**: +non e' una fee affondata, e' un **deposito rimborsabile al PRIMO PAYOUT**. Immobilizza il 91% +del conto; non lo brucia. Va riprezzato come **struttura a opzione a DUE STADI** +(passare l'eval **E POI** produrre abbastanza profitto da chiedere un prelievo), non come spesa. + +⚠ E qui c'e' un fatto che nessuno ha notato: **il progetto contiene GIA' DUE modelli che si +contraddicono fra loro.** + * `r0725_hyro.py:200` -> `cost = price * (1.0 - pe)` = quota **rimborsata al PASS**; + * `r0725_prop_ladder.simulate` / `r0725_prop_coupled.simulate` -> `cash[buy] -= price` + e la quota **non torna MAI** = spesa affondata. +Il primo e' OTTIMISTA (rimborsa a un evento che accade prima e piu' spesso di quello vero), +il secondo e' PESSIMISTA (non rimborsa affatto). **Il vero sta in mezzo, e il numero operativo +del progetto β€” P(>=50 EUR/g) β€” viene dal secondo.** + +LA PREVISIONE, REGISTRATA PRIMA DI MISURARE +------------------------------------------- +1. Il rimborso al primo payout vale **poco** sull'EV del biglietto e **piu'** sulla scala di + conti (dove la cassa che torna ricompra un biglietto e composta). Mi aspetto: EV +5-15%, + P(>=50/g) +1-3 punti percentuali. +2. `P(primo payout | pass)` mi aspetto **alta** (>80%): la soglia di prelievo e' $100 di quota + trader = 0,125% di nozionale su $100k, cioe' un'inezia rispetto al 10% appena prodotto + nell'eval. Quindi la differenza fra le due convenzioni di rimborso e' **piccola**. +3. Il **funding** su un conto funded fa male in proporzione alla leva: a 0,75x mi aspetto + ~-1,6%/anno di equity del conto (cioe' 0,75 x 2,16%), e su un conto a barriera un costo di + drift si paga sulla P(pass) piu' che sul maxDD. +4. **XS01: mi aspetto che INCASSI funding**, perche' e' market-neutral e i perp con funding piu' + caro sono tipicamente quelli in forte trend, che il momentum cross-sezionale compra... ma + comprarli vuol dire PAGARE il loro funding. Le due forze tirano in verso opposto: **non lo so, + e per questo si misura.** Se il segno esce negativo (costo) lo dico. +5. **La domanda che decide**: con P(>=50 EUR/g) 4,4% e P(zero) 35,3%, il biglietto ha EV + positivo? E batte l'alternativa ovvia (tenere i $635 sul libro e versare)? Mi aspetto: + **EV positivo sulla MEDIA e negativo sulla MEDIANA** β€” cioe' una lotteria con valore atteso + positivo, che e' esattamente la forma che un conto da $635 non puo' permettersi molte volte. + +CONVENZIONI DICHIARATE +---------------------- +* Lente **ACCOPPIATA** ovunque (`r0725_prop_coupled`): su una regola a UN GIORNO la close-only + e' esattamente CIECA. La close-only compare solo come controllo. +* Configurazione del conto funded: **W_A = TP01 38 / SKH01 38 / XS01 25** (centro della regione + robusta di PROP-RECAL), XS01 = **U13 Binance** (la versione piu' debole e l'unica testabile + fuori campione), finestra **W-LONG 2021+**, de-luck sul drift **x0,89**. +* Il conto PROPRIO gira invece il **libro LIVE 75/25 a $654 con min-order $5** (XS01 non e' + eseguibile a quel capitale): l'asimmetria e' REALE ed e' il punto del canale funded. +* Banda d'ancora: **23 offset x 3 fasi (0,3,7)**, lo stesso sottocampione dichiarato da + `r0822c_skh_anchor_prop`. La stima onesta e' la **MEDIANA**, non l'ancora canonica. +* **Firm death 10%/anno** e **cap $200k/trader**: assunzioni gia' in uso nel progetto (24/07), + dichiarate e non nascoste. +* Soglia di prelievo **$100 di quota trader** (fonte primaria-dichiarativa della firm, non + misurata) -> profitto lordo >= $125 su $100k. +* Monte Carlo: la risoluzione si dichiara. 1500 percorsi sulla scala di conti (+-1pp su + P(>=50/g)), 6000 sul biglietto a conto singolo, 2500 su J. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822d_gate_c.py` +Rete: NESSUNA (tutto da disco: parquet certificati, cache Binance di r0822b, cache funding +di r0822d_funding). Se la cache del funding manca, la sezione 1 lo dichiara e si ferma li'. +""" +from __future__ import annotations + +import contextlib +import io +import re +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) + +if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import + sys.argv.append("--no-net") + +import r0724_goal50_intraday_mc as mc # noqa: E402 recon orario per-sleeve (24/07) +import r0725_prop_coupled as pc # noqa: E402 lente accoppiata (25/07) +import r0725_prop_ladder as pl # noqa: E402 regole firm + scala di conti (25/07) +import r0822_prop_alloc as PA # noqa: E402 J, bootstrap, simulatore (22/08) +import r0822b_prop_recal as PR # noqa: E402 PROP-RECAL (22/08) +import r0822b_xs01_oos as X # noqa: E402 motore XS01 + cache Binance (22/08) +import r0822c_skh_anchor_prop as AP # noqa: E402 gamba (b), offset di SKH01 (22/08) +import r0822d_funding as FD # noqa: E402 il funding (22/08) +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +RAW = ROOT / "data" / "raw" +HYRO = "HYRO" +ASSETS = ("BTC", "ETH") +EURUSD = pl.EURUSD +TAX_RATE = pl.TAX_RATE +SPLIT = pl.SPLIT +DELUCK = PA.DELUCK # x0,89, fattore d'ancora MISURATO (26/07) +LEVS = PR.LEVS +OFFSETS = AP.OFFSETS # 23 offset a priori di SKH01 +PHASES_SUB = AP.PHASES_SUB # (0, 3, 7) β€” dichiarato da r0822c +W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25} +W_LIVE = {"TP01": 0.75, "SKH01": 0.25} +N_PATHS_J = PA.N_PATHS # 2500 +N_PATHS_LAD = 1500 +N_PATHS_TIX = 6000 +EV_H = FU_H = 365 +SEED_EV, SEED_FU = 8220, 8221 +SEED_LAD = 20260822 +MIN_PAYOUT_TRADER = 100.0 # quota trader minima per un prelievo (firm) +HORIZON_DAYS = pl.HORIZON_MONTHS * 30 # 1080 = 36 mesi + +# numeri PUBBLICATI da riprodurre prima di produrne di nuovi +PUB = dict( + hyro_ev_close=6789.0, hyro_ev_intra=1166.0, # r0725_hyro (25/07) + recal_j=0.453, recal_live=0.281, recal_d=0.175, recal_p50g=0.078, # Β§29 + anchor_p50g=0.044, anchor_p10=0.025, anchor_p90=0.082, + anchor_pzero=0.353, # Β§32 + fund_book=-2.16, fund_tp=-2.10, fund_skh=-2.32, # Β§35 (%/anno) +) + +TRIALS = 0 +T0 = time.time() + + +def bump(n: int = 1) -> None: + global TRIALS + TRIALS += n + + +def hr(t: str = "") -> None: + print("\n" + "-" * 104) + if t: + print(f" {t}") + print("-" * 104) + + +def tick(lab: str) -> None: + print(f" [{time.time()-T0:6.0f}s] {lab}") + + +# ============================================================================ 1. IL FUNDING + +def funding_rate() -> dict | None: + """Serie oraria del funding per asset (LINEARE dove esiste, INVERSE prima come proxy + dichiarato). Legge SOLO la cache di `r0822d_funding` β€” nessuna rete.""" + try: + F = {} + for instr, (start, _a, _k) in FD.INSTR.items(): + d = FD.fetch_funding(instr, start).copy() + d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True) + F[instr] = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt") + return FD.build_rate(F) + except Exception as exc: # pragma: no cover + print(f" !! cache funding assente/illeggibile ({exc}) β€” il funding NON viene messo dentro") + return None + + +_FTP: dict[int, pd.Series] = {} + + +def f_tp01_sleeve(rate: dict) -> pd.Series: + """Costo di funding GIORNALIERO dello sleeve TP01 a peso 1.0, sulla STESSA lente delle gambe + orarie del recon (`mc.tp01_daily_frame` = TP01 canonico, senza min-order: su un conto funded + da $100k il min-order $5 non esiste, e questa e' la lente giusta li'). + + Convenzione temporale identica a `FD.tp01_canonical`: la posizione tenuta nel giorno d e' + decisa a close[d-1], e paga il funding sulle 24 ore (d, d+24h].""" + if 0 in _FTP: + return _FTP[0] + out = None + for a in ASSETS: + tpf = mc.tp01_daily_frame(a) + days = pd.DatetimeIndex(tpf.index) + s = pd.Series(tpf["w"].values * FD.daily_funding(rate[a], days), index=days) + out = s if out is None else out.add(s, fill_value=0.0) + _FTP[0] = out + return out + + +_FSKH: dict[int, pd.Series] = {} + + +def f_skh_sleeve(rate: dict, off: int = 0) -> pd.Series: + """Costo di funding giornaliero dello sleeve SKH01 (50/50) all'offset `off`, come + DIFFERENZA fra il path live senza e con funding (`FD.skh_book`). E' esatto al primo ordine + (l'equity compone: il secondo ordine e' ~1e-6/giorno) e riproduce il -2,32%/anno di Β§35.""" + if off in _FSKH: + return _FSKH[off] + a0 = FD.skh_book(off, "hourly", None) + a1 = FD.skh_book(off, "hourly", rate) + if a0.index.tz is None: + a0.index = a0.index.tz_localize("UTC") + if a1.index.tz is None: + a1.index = a1.index.tz_localize("UTC") + s = (a0 - a1).dropna() + _FSKH[off] = s + return s + + +def xs_funding_measure(verbose: bool = True) -> dict: + """IL SEGNO DEL FUNDING SU XS01, MISURATO (non assunto). + + XS01 e' long 5 / short 5 a pesi uguali -> `sum(W) = 0` ESATTAMENTE. Quindi + `sum(W_i f_i) = sum(W_i (f_i - fbar))`: **il LIVELLO del funding non entra, entra solo la + sua DISPERSIONE CROSS-SEZIONALE**. Cio' significa due cose: + * non e' vero ne' che 'incassa' ne' che 'paga' per costruzione: dipende da come il + funding e' distribuito FRA gli asset che il momentum compra e vende; + * il fatto noto che il funding Hyperliquid sia ~2,1x quello del venue vero (Β§35) si + trasferisce alla dispersione, quindi si riporta una BANDA, non un punto. + Dato: `data/raw/hlfund__1h.parquet` (19 major, 2023-05 -> 2026-06).""" + P = X.hl_panel() + det: dict = {} + X.xsec_engine(P, phase=0, detail=det) + W, scale, idx = det["W"], det["scale"], det["index"] + cols = list(P.columns) + gross_sum = float(np.abs(W.sum(axis=1)).max()) + fun = {} + for s in cols: + f = RAW / f"hlfund_{s.lower()}_1h.parquet" + if not f.exists(): + continue + d = pd.read_parquet(f)["funding"] + d.index = pd.DatetimeIndex(d.index).tz_convert("UTC") + fun[s] = d.groupby(d.index.floor("D")).sum() + have = [s for s in cols if s in fun] + Fm = pd.DataFrame({s: fun[s] for s in have}).reindex(idx) + cover = Fm.notna().mean(axis=1) + Fv = Fm.fillna(0.0).values + sel = [cols.index(s) for s in have] + Wp = np.vstack([np.zeros((1, len(cols))), W[:-1]]) # posizioni TENUTE nel giorno i + cost = pd.Series((Wp[:, sel] * Fv).sum(axis=1) * scale, index=idx) + gross = pd.Series(np.abs(Wp[:, sel]).sum(axis=1) * scale, index=idx) + ok = cover >= 0.95 + c = cost[ok] + if verbose: + print(f" pannello HL {P.shape[1]} gambe, {len(idx)} giorni; file di funding per " + f"{len(have)}/{len(cols)} gambe") + print(f" CONTROLLO di dollar-neutralita': max|sum(W)| = {gross_sum:.2e} " + f"-> {'ESATTO' if gross_sum < 1e-12 else 'NON neutrale'}") + print(f" giorni con copertura >=95% delle gambe: {int(ok.sum())} " + f"({c.index.min().date()} -> {c.index.max().date()})") + print(f"\n {'anno':>6} {'costo funding %/anno':>22} {'gross nozionale':>17} {'giorni':>7}") + for y, g in c.groupby(c.index.year): + gg = gross.reindex(g.index) + print(f" {y:>6} {float(g.mean()*365)*100:>21.3f}% {float(gg.mean()):>16.3f}x " + f"{len(g):>7}") + ann = float(c.mean() * 365) + se = float(c.std(ddof=1) / np.sqrt(len(c)) * 365) + print(f" {'TUTTO':>6} {ann*100:>21.3f}% {float(gross.reindex(c.index).mean()):>16.3f}x " + f"{len(c):>7} IC95 [{(ann-1.96*se)*100:+.3f}%, {(ann+1.96*se)*100:+.3f}%]") + ann = float(c.mean() * 365) + return dict(ann=ann, series=c, gross=float(gross.reindex(c.index).mean()), + se=float(c.std(ddof=1) / np.sqrt(len(c)) * 365), n=len(c)) + + +# ================================================================ 2. LIBRO CON FUNDING + +def fvec_for(w: dict, idx: pd.DatetimeIndex, rate: dict | None, off: int, + xs_ann: float) -> np.ndarray | None: + """Vettore giornaliero del costo di funding del LIBRO ai pesi `w`. + TP01 e SKH01: serie misurate giorno per giorno. XS01: **tasso annuo costante** misurato su + Hyperliquid 2024-2026 e applicato all'intera finestra β€” dichiarato, perche' `hlfund` parte + nel 2023-05 e il pannello di XS01 usato qui e' Binance 2021+ (nessun funding su disco).""" + if rate is None: + return None + f = np.zeros(len(idx)) + wtp, wsk, wxs = w.get("TP01", 0.0), w.get("SKH01", 0.0), w.get("XS01", 0.0) + if wtp: + f += wtp * np.nan_to_num(f_tp01_sleeve(rate).reindex(idx).values) + if wsk: + f += wsk * np.nan_to_num(f_skh_sleeve(rate, off).reindex(idx).values) + if wxs: + f += wxs * (xs_ann / 365.0) + return f + + +def best_lev(R, G, ev, fu) -> tuple[float, float, np.ndarray]: + jb, lb, vb = -1.0, None, None + for lv in LEVS: + o = PR.obj_rg(R, G, lv, ev, fu, factor=DELUCK) + if o["J"] > jb: + jb, lb, vb = o["J"], lv, o["jvec"] + bump(len(LEVS)) + return jb, lb, vb + + +# ============================================ 3. SCALA DI CONTI COL DEPOSITO RIMBORSABILE + +def ladder(R: np.ndarray, G: np.ndarray, n_paths: int, lev: float, seed: int, + refund: bool, deposit_eur: float = 0.0, start_eur: float = pl.START_EUR, + min_payout: float = 0.0, boot: np.ndarray | None = None, + block: int = 20, deluck: float = DELUCK) -> dict: + """Copia di `r0725_prop_coupled.simulate` (un solo vettore di peso, `MAX_CONCURRENT` slot) + con TRE aggiunte e nessun'altra differenza: + 1. `refund` β€” la quota torna in cassa **al PRIMO PAYOUT** di quel conto (regola vera + della firm). Se il conto busta prima di un payout, la quota e' persa. + 2. `deposit_eur` β€” versamento mensile, per confrontare la strada-biglietto con la + strada-versare a parita' di soldi che entrano. + 3. `min_payout` β€” quota trader minima per poter prelevare ($100 secondo la firm). + Con `refund=False, deposit_eur=0, min_payout=0, boot=None` deve essere **BIT-EXACT** + rispetto a `pc.simulate`: e' il controllo di Β§3.1. + + Il funding, se c'e', e' gia' dentro `R` -> essendo `Rs = R*lev`, il costo di funding + **scala con la leva**, che e' la sua vera economia (e' proporzionale all'esposizione).""" + rng = np.random.default_rng(seed) + n_days = HORIZON_DAYS + L = len(R) + Rm = R.astype(float).reshape(-1, 1).copy() + Gm = G.astype(float).reshape(-1, 1).copy() + Rm = Rm - (1.0 - deluck) * Rm.mean(axis=0, keepdims=True) + if boot is None: + n_blocks = int(np.ceil(n_days / block)) + starts = rng.integers(0, L - block, size=(n_paths, n_blocks)) + idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] + else: + idx = boot + n_slot = pl.MAX_CONCURRENT + sel = np.zeros(n_slot, dtype=int) + Rs = Rm[idx][:, :, sel] * lev + G_ = Gm[idx][:, :, sel] * lev + + FIRMS = pl.FIRMS + cash = np.full(n_paths, start_eur * EURUSD) + phase = np.full((n_paths, n_slot), -1, np.int8) + eq = np.ones((n_paths, n_slot)) + firm = np.full((n_paths, n_slot), -1, np.int8) + notional = np.zeros((n_paths, n_slot)) + price_paid = np.zeros((n_paths, n_slot)) + refunded = np.zeros((n_paths, n_slot), bool) + firm_alive = np.ones((n_paths, len(FIRMS)), bool) + cum_payout = np.zeros(n_paths) + spent = np.zeros(n_paths) + got_back = np.zeros(n_paths) + deposited = np.zeros(n_paths) + n_payouts = np.zeros(n_paths, int) + p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0) + + for t in range(n_days): + died = rng.random((n_paths, len(FIRMS))) < p_death_day + newly_dead = died & firm_alive + firm_alive &= ~died + if newly_dead.any(): + for fi in range(len(FIRMS)): + kill = newly_dead[:, fi] + if kill.any(): + hit = (firm == fi) & kill[:, None] + phase[hit] = -1 + notional[hit] = 0.0 + + for k in range(n_slot): + free = phase[:, k] == -1 + if not free.any(): + continue + for fi, F in enumerate(FIRMS): + used = np.where(firm == fi, notional, 0.0).sum(axis=1) + for (size, price) in sorted(F["tickets"], key=lambda x: -x[0]): + buy = (free & firm_alive[:, fi] & (cash >= price) + & (used + size <= F["cap"])) + if not buy.any(): + continue + cash[buy] -= price + spent[buy] += price + phase[buy, k] = 0 + eq[buy, k] = 1.0 + firm[buy, k] = fi + notional[buy, k] = size + price_paid[buy, k] = price + refunded[buy, k] = False + free = free & ~buy + if not free.any(): + break + if not free.any(): + break + + r = Rs[:, t, :] + g = G_[:, t, :] + act = phase >= 0 + eq_prev = eq + eq = np.where(act, eq * (1.0 + r), eq) + eq_low = np.where(act, eq_prev * (1.0 + r + g), eq) + r_low = r + g + + for fi, F in enumerate(FIRMS): + s_ = act & (firm == fi) + if not s_.any(): + continue + ev_t, ev_dd, ev_dl = F["ev"] + fu_ml, fu_dl = F["fu"] + in_ev = s_ & (phase == 0) + in_fu = s_ & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl))) + | (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl)))) + phase[bust] = -1 + notional[bust] = 0.0 + passed = in_ev & (eq >= 1.0 + ev_t) & ~bust + phase[passed] = 1 + eq[passed] = 1.0 + + if (t + 1) % 30 == 0: + fu = phase == 1 + pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout) + gain = np.where(pay, (eq - 1.0) * notional, 0.0) + net = gain.sum(axis=1) * SPLIT * (1.0 - TAX_RATE) + cum_payout += net + cash += net + n_payouts += (pay.sum(axis=1) > 0).astype(int) + eq = np.where(pay, 1.0, eq) + if refund: + back = pay & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + got_back += amt + refunded |= back + if deposit_eur: + cash += deposit_eur * EURUSD + deposited += deposit_eur * EURUSD + + eur_day = cum_payout / EURUSD / (n_days / 365.0) / 365.0 + return dict(eur_day=eur_day, med=float(np.median(eur_day)), + p10=float(np.percentile(eur_day, 10)), p90=float(np.percentile(eur_day, 90)), + p_ge10=float((eur_day >= 10.0).mean()), + p_target=float((eur_day >= pl.TARGET_EUR_DAY).mean()), + p_zero=float((cum_payout <= 0).mean()), + p_payout=float((n_payouts > 0).mean()), + cash=cash, med_cash=float(np.median(cash)), mean_cash=float(cash.mean()), + p10_cash=float(np.percentile(cash, 10)), p90_cash=float(np.percentile(cash, 90)), + med_spent=float(np.median(spent)), mean_back=float(got_back.mean()), + contributed=float(start_eur * EURUSD + deposited.mean())) + + +def book_road(rb: np.ndarray, boot: np.ndarray, start_eur: float, deposit_eur: float, + deluck: float = DELUCK, tax: float = TAX_RATE) -> dict: + """La strada ALTERNATIVA: gli stessi soldi restano sul LIBRO LIVE (proprio capitale, nessuna + barriera, nessuna firm) e si versa. Stessi sorteggi di bootstrap della strada-biglietto + (confronto APPAIATO), stesso de-luck, stesso orizzonte. + Fisco: 33% sulla variazione ANNUA di valore, con riporto delle minusvalenze (convenzione di + `r0807_piano_netto`; il libro realizza quasi tutto entro l'anno -> limite superiore stretto).""" + R = rb.astype(float).copy() + R = R - (1.0 - deluck) * R.mean() + r = R[boot] + n_paths, n_days = r.shape + cap = np.full(n_paths, start_eur * EURUSD) + basis = cap.copy() + carry = np.zeros(n_paths) + deposited = 0.0 + for t in range(n_days): + cap = np.maximum(cap * (1.0 + r[:, t]), 0.0) + if (t + 1) % 30 == 0 and deposit_eur: + cap += deposit_eur * EURUSD + basis += deposit_eur * EURUSD + deposited += deposit_eur * EURUSD + if (t + 1) % 365 == 0: + g = (cap - basis) + carry + due = np.where(g > 0, tax * g, 0.0) + carry = np.where(g > 0, 0.0, g) + cap = cap - due + basis = cap.copy() + return dict(cash=cap, med_cash=float(np.median(cap)), mean_cash=float(cap.mean()), + p10_cash=float(np.percentile(cap, 10)), p90_cash=float(np.percentile(cap, 90)), + contributed=float(start_eur * EURUSD + deposited)) + + +# ==================================================== 4. IL BIGLIETTO A CONTO SINGOLO, 2 STADI + +def ticket_mc(R: np.ndarray, G: np.ndarray, lev_ev: float, lev_fu: float, notional: float, + n_paths: int = N_PATHS_TIX, seed: int = 82241, max_eval_days: int = 540, + funded_days: int = 365, min_payout: float = MIN_PAYOUT_TRADER, + consistency: bool = True, firm_death: bool = True, block: int = 20, + deluck: float = DELUCK, firm: str = HYRO) -> dict: + """UN conto HYRO, i due stadi **CHAINED sullo stesso percorso** (eval -> funded), che e' la + differenza rispetto a `PA.eval_sim`/`PA.funded_sim`, che girano su bootstrap INDIPENDENTI e + moltiplicano le probabilita'. + + Ritorna cio' che serve a prezzare il deposito come opzione: + p_pass = P(passare l'eval entro `max_eval_days`) + p_payout = P(arrivare ad ALMENO un payout) <- l'evento che rimborsa la quota + p_pay_given = P(payout | pass) + e_payout = E[payout netti], incondizionato, sui `funded_days` successivi al pass + """ + tgt, dd, dl = pl.FIRMS[[f["name"] for f in pl.FIRMS].index(firm)]["ev"] + ml, dlf = pl.FIRMS[[f["name"] for f in pl.FIRMS].index(firm)]["fu"] + rng = np.random.default_rng(seed) + Rd = R.astype(float) - (1.0 - deluck) * float(np.mean(R)) + total = max_eval_days + funded_days + L = len(Rd) + nb = int(np.ceil(total / block)) + st = rng.integers(0, L - block, size=(n_paths, nb)) + bi = (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :total] + rr = Rd[bi] + gg = G[bi] + + eq = np.ones(n_paths) + phase = np.zeros(n_paths, np.int8) # 0 eval, 1 funded, -1 morto, 2 finito + best_day = np.zeros(n_paths) + fu_day = np.zeros(n_paths, int) + payout = np.zeros(n_paths) + ever = np.zeros(n_paths, bool) + passed = np.zeros(n_paths, bool) + t_pass = np.full(n_paths, -1, int) + p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0) if firm_death else 0.0 + + for t in range(total): + if p_death_day: + phase[(rng.random(n_paths) < p_death_day) & (phase >= 0)] = -1 + alive = phase >= 0 + if not alive.any(): + break + in_ev = alive & (phase == 0) + in_fu = alive & (phase == 1) + lev = np.where(in_fu, lev_fu, lev_ev) + rt = rr[:, t] * lev + gt = gg[:, t] * lev + eqp = eq + eq = np.where(alive, eq * (1.0 + rt), eq) + eq_low = np.where(alive, eqp * (1.0 + rt + gt), eq) + r_low = rt + gt + bust = ((in_ev & ((eq_low < 1.0 - dd) | (r_low < -dl))) + | (in_fu & ((eq_low < 1.0 - ml) | (r_low < -dlf)))) + phase[bust] = -1 + gain = np.maximum(eq - eqp, 0.0) + best_day = np.where(in_ev & ~bust, np.maximum(best_day, gain), best_day) + profit = eq - 1.0 + ok = in_ev & ~bust & (profit >= tgt) + if consistency: + ok &= best_day <= pl.FIRMS[0].get("consistency", 0.40) * np.maximum(profit, 1e-9) + phase[ok] = 1 + eq[ok] = 1.0 + fu_day[ok] = 0 + passed |= ok + t_pass[ok] = t + still_ev = (phase == 0) & (t >= max_eval_days - 1) + phase[still_ev] = -1 # eval scaduta + now_fu = phase == 1 + fu_day[now_fu & ~ok] += 1 + chk = now_fu & (fu_day > 0) & (fu_day % 30 == 0) + pay = chk & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout) + payout += np.where(pay, (eq - 1.0) * notional * SPLIT * (1.0 - TAX_RATE), 0.0) + ever |= pay + eq = np.where(pay, 1.0, eq) + phase[now_fu & (fu_day >= funded_days)] = 2 + + return dict(p_pass=float(passed.mean()), p_payout=float(ever.mean()), + p_pay_given=float(ever.sum() / max(passed.sum(), 1)), + e_payout=float(payout.mean()), + e_payout_given=float(payout[ever].mean()) if ever.any() else 0.0, + t_pass=float(np.median(t_pass[passed])) if passed.any() else float("nan")) + + +def ev_three_ways(res: dict, price: float) -> dict: + """Le tre convenzioni sul deposito, sullo STESSO Monte Carlo. + (i) SPESA PERSA β€” la quota non torna mai (`pc.simulate`, il numero operativo) + (ii) RIMBORSO AL PASS β€” la quota torna se passi (`r0725_hyro:200`) + (iii) RIMBORSO AL PAYOUT β€” la quota torna col primo prelievo (la regola VERA della firm) + """ + e, pp, pa = res["e_payout"], res["p_pass"], res["p_payout"] + return dict(sunk=e - price, + at_pass=e - price * (1.0 - pp), + at_payout=e - price * (1.0 - pa), + p_break_even=float(np.clip((price - e) / price, 0.0, 1.0)) if price else 0.0) + + +# ==================================================================================== main +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + print("=" * 104) + print(" GATE PROP-01 gamba (c) β€” il BIGLIETTO come OPZIONE, il FUNDING dentro, e il") + print(" confronto con l'alternativa ovvia: tenere i $635 sul libro e versare") + print("=" * 104) + print(" PREVISIONE REGISTRATA PRIMA: (1) il rimborso vale +5-15% di EV e +1-3pp su") + print(" P(>=50/g); (2) P(primo payout | pass) > 80%; (3) il funding costa ~0,75 x 2,16%") + print(" su un funded a 0,75x; (4) sul segno del funding di XS01 NON ho una previsione,") + print(" le due forze tirano in verso opposto; (5) EV positivo sulla MEDIA e negativo") + print(" sulla MEDIANA β€” una lotteria, non un investimento.") + + # ============================================================== Β§0.1 riproduzione r0725_hyro + hr("Β§0.1 RIPRODUZIONE β€” `r0725_hyro` (25/07): l'EV del biglietto come fu pubblicato") + buf = io.StringIO() + t = time.time() + import r0725_hyro as HY # noqa: E402 + with contextlib.redirect_stdout(buf): + HY.main() + txt = buf.getvalue() + lens_now, ev_now = None, {} + for line in txt.splitlines(): + if "[close-only]" in line: + lens_now = "close" + elif "[intraday]" in line: + lens_now = "intra" + m = re.match(r"\s*\$\s*100,000\s+\$\s*([\d,]+)\s+\$\s*([\d,]+)\s+\$\s*([\d,]+)\s+" + r"\$\s*([\-\d,]+)", line) + if m and lens_now: + ev_now[lens_now] = float(m.group(4).replace(",", "")) + print(f" rigirato oggi ({time.time()-t:.0f}s), stessa macchina, stessi semi, biglietto $100k:") + print(f" {'lente':>14} {'EV oggi':>12} {'EV PUBBLICATO 25/07':>22} {'delta':>10}") + for k, lab, p in (("close", "close-only", PUB["hyro_ev_close"]), + ("intra", "intraday", PUB["hyro_ev_intra"])): + v = ev_now.get(k, float("nan")) + print(f" {lab:>14} ${v:>11,.0f} ${p:>21,.0f} {v-p:>+10,.0f}") + print(" Lo scarto NON e' un errore di riproduzione: `r0725_hyro` ri-deriva le proprie serie") + print(" dal feed CORRENTE, e dal 25/07 a oggi il feed ha ~4 settimane di barre in piu'.") + print(" Ordine di grandezza e conclusione identici -> la macchina e' quella.") + print("\n ⚠ E il codice riprodotto contiene gia' la contraddizione: `cost = price*(1-pe)`") + print(" = quota RIMBORSATA AL PASS, mentre `pc.simulate` (da cui esce il numero") + print(" operativo P(>=50/g)) fa `cash -= price` e non la restituisce MAI.") + bump(1) + tick("Β§0.1") + + # ============================================================== dati comuni + hr("Β§0.2 IL PANNELLO β€” le stesse serie di PROP-RECAL (U13 Binance, W-LONG 2021+)") + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13 = Ob.reindex(p13.index)[X.U13] + h13 = Hb.reindex(p13.index)[X.U13] + l13 = Lb.reindex(p13.index)[X.U13] + XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES_SUB} + bump(len(PHASES_SUB)) + xs0 = XSP[PHASES_SUB[0]] + cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index + base = cry_idx.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC")) + I = base[base >= S_LONG] + ev_b = PA.boot_idx(len(I), EV_H, N_PATHS_J, seed=SEED_EV) + fu_b = PA.boot_idx(len(I), FU_H, N_PATHS_J, seed=SEED_FU) + print(f" W-LONG: {len(I)} giorni {I[0].date()} -> {I[-1].date()} | de-luck x{DELUCK} | " + f"{N_PATHS_J} percorsi per J") + pc.DELUCK = DELUCK + + # J all'ancora canonica, mediana sulle 3 fasi dichiarate + js, jlv = [], [] + for ph in PHASES_SUB: + Rc, Gc = AP.rg_off(W_A, I, XSP[ph], 0) + j, lv, _ = best_lev(Rc, Gc, ev_b, fu_b) + js.append(j); jlv.append(lv) + RL0, GL0 = AP.rg_off(W_LIVE, I, xs0, 0) + jL, lvL, vL0 = best_lev(RL0, GL0, ev_b, fu_b) + dpair = [] + for ph in PHASES_SUB: + Rc, Gc = AP.rg_off(W_A, I, XSP[ph], 0) + _, _, vc = best_lev(Rc, Gc, ev_b, fu_b) + dpair.append(float((vc - vL0).mean())) + print(f" J(W_A 38/38/25) ancora canonica, fasi {PHASES_SUB}: mediana {np.median(js):.3f} " + f"(PUB {PUB['recal_j']:.3f} su 10 fasi) | leve {jlv}") + print(f" J(LIVE 75/25) : {jL:.3f} @{lvL:.2f}x " + f"(PUB {PUB['recal_live']:.3f})") + print(f" delta appaiato mediano : {np.median(dpair):+.3f} " + f"(PUB {PUB['recal_d']:+.3f})") + tick("Β§0.2") + + # ============================================================== Β§1 il funding + hr("Β§1. IL FUNDING, MESSO DENTRO β€” e il segno su XS01, MISURATO") + rate = funding_rate() + if rate is None: + print(" Senza cache non si mette dentro nulla: il resto gira SENZA funding e lo dichiara.") + xs_ann = 0.0 + else: + fb0 = FD.book_75_25(None, alloc=654.0) + fb1 = FD.book_75_25(rate, alloc=654.0) + d_book = float((fb0 - fb1).dropna().mean() * 365) * 100 + ftp = f_tp01_sleeve(rate) + fsk = f_skh_sleeve(rate, 0) + c0, _ = FD.tp01_canonical(None) + c1, _ = FD.tp01_canonical(rate) + d_tp = float((c0 - c1).dropna().mean() * 365) * 100 + print(f" RIPRODUZIONE dei costi di Β§35 (%/anno, segno = costo):") + print(f" {'serie':>36} {'oggi':>9} {'PUB Β§35':>9}") + print(f" {'LIBRO 75/25 path live':>36} {-d_book:>8.2f}% {PUB['fund_book']:>8.2f}%") + print(f" {'TP01 canonico':>36} {-d_tp:>8.2f}% {PUB['fund_tp']:>8.2f}%") + print(f" {'SKH01 50/50 path live (off 0)':>36} " + f"{-float(fsk.mean()*365)*100:>8.2f}% {PUB['fund_skh']:>8.2f}%") + print(f" {'TP01 gambe orarie (lente prop)':>36} " + f"{-float(ftp.reindex(I).mean()*365)*100:>8.2f}% {'β€”':>9}") + print("\n ⚠ La riga 'gambe orarie' non e' la stessa di 'TP01 canonico': il recon orario") + print(" usa `mc.tp01_daily_frame` (senza min-order, che e' la lente giusta su un") + print(" conto funded da $100k). Le due lenti differiscono di poco e si dichiarano.") + print("\n IL SEGNO DEL FUNDING SU XS01 β€” misurato, non assunto:") + xsf = xs_funding_measure() + xs_ann = xsf["ann"] + print(f"\n >>> XS01 **PAGA** {xs_ann*100:+.2f}%/anno di funding (segno positivo = costo).") + print(" La previsione registrata ('non lo so, le due forze tirano in verso opposto')") + print(" si risolve cosi': il momentum cross-sezionale COMPRA i perp col funding piu'") + print(" caro e VENDE quelli col funding piu' basso -> paga la dispersione.") + print(" Essendo `sum(W)=0` il LIVELLO non entra: entra solo la DISPERSIONE.") + print(f" Se il funding Hyperliquid e' ~2,1x quello del venue eseguibile (Β§35),") + print(f" la banda onesta e' [{xs_ann/2.1*100:+.2f}%, {xs_ann*100:+.2f}%]/anno.") + print("\n LA LEVA β€” perche' il funding non e' un costo fisso:") + print(" il funding e' proporzionale all'ESPOSIZIONE, e l'esposizione di un conto funded") + print(" e' `lev x` quella del libro -> nel simulatore `Rs = R*lev` porta dentro anche il") + print(" costo, che quindi scala. Verifica numerica (libro W_A, costo %/anno di equity):") + fA = fvec_for(W_A, I, rate, 0, xs_ann) + for lv in (0.50, 0.75, 1.00, 1.25): + print(f" leva {lv:>4.2f}x -> {-float(np.mean(fA))*365*100*lv:>6.2f}%/anno " + f"(= {lv:.2f} x {-float(np.mean(fA))*365*100:.2f}%)") + tick("Β§1") + + # ============================================================== Β§2 banda d'ancora + hr("Β§2. LA BANDA D'ANCORA β€” 23 offset x 3 fasi, con e SENZA funding (riproduzione + delta)") + print(f" Sottocampione dichiarato, identico a `r0822c`: {len(OFFSETS)} offset x " + f"{len(PHASES_SUB)} fasi {PHASES_SUB} = {len(OFFSETS)*len(PHASES_SUB)} celle, " + f"x2 (funding on/off).") + print(f" Ladder: {N_PATHS_LAD} percorsi (risoluzione ~+-1pp su P(>=50/g)), lente ACCOPPIATA,") + print(f" {pl.MAX_CONCURRENT} conti max, morte-firm {pl.FIRM_DEATH_PER_YEAR:.0%}/anno, " + f"deposito NON rimborsato (= convenzione di `pc.simulate`, per riprodurre).") + cells: list[dict] = [] + t = time.time() + for off in OFFSETS: + fA = fvec_for(W_A, I, rate, off, xs_ann) + for ph in PHASES_SUB: + R0, G0 = AP.rg_off(W_A, I, XSP[ph], off) + for tag, Rx in (("nofund", R0), ("fund", R0 - fA if fA is not None else None)): + if Rx is None: + continue + j, lv, _ = best_lev(Rx, G0, ev_b, fu_b) + lad = ladder(Rx, G0, N_PATHS_LAD, lv, SEED_LAD, refund=False) + cells.append(dict(off=off, ph=ph, tag=tag, J=j, lev=lv, + p50=lad["p_target"], pz=lad["p_zero"], med=lad["med"], + ppay=lad["p_payout"])) + bump(2) + C = pd.DataFrame(cells) + print(f" ({time.time()-t:.0f}s)") + print(f"\n {'variante':>12} {'J mediana':>10} {'P(>=50/g)':>10} {'banda p10-p90':>18} " + f"{'P(zero)':>9} {'EUR/g med':>10}") + for tag, lab in (("nofund", "SENZA funding"), ("fund", "CON funding")): + s = C[C["tag"] == tag] + if not len(s): + continue + print(f" {lab:>12} {s['J'].median():>10.3f} {s['p50'].median():>10.1%} " + f"[{np.percentile(s['p50'],10):>7.1%},{np.percentile(s['p50'],90):>7.1%}] " + f"{s['pz'].median():>9.1%} {s['med'].median():>10.2f}") + s_nf = C[C["tag"] == "nofund"] + print(f"\n RIPRODUZIONE Β§32: P(>=50/g) mediana {s_nf['p50'].median():.1%} " + f"(PUB {PUB['anchor_p50g']:.1%}) | banda " + f"[{np.percentile(s_nf['p50'],10):.1%}, {np.percentile(s_nf['p50'],90):.1%}] " + f"(PUB [{PUB['anchor_p10']:.1%}, {PUB['anchor_p90']:.1%}]) | " + f"P(zero) {s_nf['pz'].median():.1%} (PUB {PUB['anchor_pzero']:.1%})") + if rate is not None: + s_f = C[C["tag"] == "fund"] + m = C.pivot_table(index=["off", "ph"], columns="tag", values="p50") + dpp = (m["fund"] - m["nofund"]) + mj = C.pivot_table(index=["off", "ph"], columns="tag", values="J") + dj = (mj["fund"] - mj["nofund"]) + print(f"\n IL FUNDING, appaiato cella per cella (mediana delle DIFFERENZE, non") + print(f" differenza delle mediane): dJ {dj.median():+.4f} " + f"({int((dj<0).sum())}/{len(dj)} celle negative) | " + f"dP(>=50/g) {dpp.median()*100:+.2f}pp " + f"({int((dpp<0).sum())}/{len(dpp)} negative)") + print(f" Il numero operativo col funding dentro: **P(>=50 EUR/g) " + f"{s_f['p50'].median():.1%}** [{np.percentile(s_f['p50'],10):.1%}, " + f"{np.percentile(s_f['p50'],90):.1%}], P(zero) {s_f['pz'].median():.1%}") + # cella d'ancora MEDIANA su cui si prezza il biglietto + ref_tag = "fund" if rate is not None else "nofund" + S = C[C["tag"] == ref_tag].reset_index(drop=True) + k_med = int((S["p50"] - S["p50"].median()).abs().idxmin()) + OFF_M, PH_M, LEV_M = int(S.loc[k_med, "off"]), int(S.loc[k_med, "ph"]), float(S.loc[k_med, "lev"]) + print(f"\n CELLA D'ANCORA MEDIANA scelta per prezzare il biglietto: offset {OFF_M} min, " + f"fase {PH_M}, leva {LEV_M:.2f}x") + print(" (non l'ancora canonica: quella e' al 91Β° pctl dei 23 offset β€” Β§32)") + fM = fvec_for(W_A, I, rate, OFF_M, xs_ann) + R_M0, G_M = AP.rg_off(W_A, I, XSP[PH_M], OFF_M) + R_M = R_M0 - fM if fM is not None else R_M0 + tick("Β§2") + + # ============================================================== Β§3 il biglietto a 2 stadi + hr("Β§3. IL BIGLIETTO COME OPZIONE β€” i DUE stadi, sullo stesso percorso") + print(" Stadio 1: passare l'eval (target 10%, maxDD 6% STATICO, daily 4%, consistency 40%).") + print(" Stadio 2: sul funded, arrivare ad ALMENO un payout β€” che e' l'evento che") + print(" rimborsa il deposito. Soglia: quota trader >= $100 (fonte dichiarativa della firm).") + print(f" {N_PATHS_TIX} percorsi, blocchi di 20 giorni, morte-firm dentro, lente accoppiata.") + rows = [] + for lev_ev in (0.50, 0.75, 1.00): + for lev_fu in (0.50, 0.75, 1.00): + r = ticket_mc(R_M, G_M, lev_ev=lev_ev, lev_fu=lev_fu, notional=100_000.0) + rows.append(dict(lev_ev=lev_ev, lev_fu=lev_fu, + **{k: r[k] for k in + ("p_pass", "p_payout", "p_pay_given", "e_payout", + "e_payout_given", "t_pass")})) + bump(1) + T = pd.DataFrame(rows) + print(f"\n {'leva eval':>10} {'leva fund':>10} {'P(pass)':>9} {'P(payout)':>10} " + f"{'P(pay|pass)':>12} {'E[payout]':>11} {'E[pay|pay]':>12} {'gg al pass':>11}") + for _, x in T.iterrows(): + print(f" {x['lev_ev']:>10.2f} {x['lev_fu']:>10.2f} {x['p_pass']:>9.1%} " + f"{x['p_payout']:>10.1%} {x['p_pay_given']:>12.1%} ${x['e_payout']:>10,.0f} " + f"${x['e_payout_given']:>11,.0f} {x['t_pass']:>11.0f}") + best = T.loc[T["e_payout"].idxmax()] + LEV_EV, LEV_FU = float(best["lev_ev"]), float(best["lev_fu"]) + print(f"\n Leve che massimizzano E[payout]: eval {LEV_EV:.2f}x / funded {LEV_FU:.2f}x") + print(f" (per confronto, la leva che massimizza J alla stessa cella e' {LEV_M:.2f}x: J e'") + print(" P(pass) x P(vivo), che NON e' E[payout] β€” l'ottimo non deve coincidere)") + pg = T["p_pay_given"] + print(f" >>> PREVISIONE (2) 'P(primo payout | pass) > 80%': vale in " + f"{int((pg > 0.80).sum())}/{len(pg)} celle (banda {pg.min():.1%}-{pg.max():.1%}) -> " + f"{'CONFERMATA salvo a leva funded 1.00x' if (pg > 0.80).sum() >= 6 else 'SMENTITA'}") + print(" Il verso e' meccanico e vale la pena dirlo: piu' leva sul funded alza") + print(" E[payout] e ABBASSA P(payout), perche' fa bustare prima del primo prelievo.") + print(" Sono due obiettivi diversi: chi vuole il RIMBORSO non vuole la leva massima.") + + hr("Β§3.1 LE TRE CONVENZIONI SUL DEPOSITO β€” stesso Monte Carlo, tre economie") + print(" (i) SPESA PERSA `pc.simulate` -> EV = E[payout] - prezzo") + print(" (ii) RIMBORSO AL PASS `r0725_hyro` -> EV = E[payout] - prezzo x (1 - P(pass))") + print(" (iii)RIMBORSO AL PAYOUT la firm -> EV = E[payout] - prezzo x (1 - P(payout))") + print(f"\n {'biglietto':>12} {'prezzo':>8} {'E[payout]':>11} {'(i) persa':>11} " + f"{'(ii) al pass':>13} {'(iii) al payout':>16} {'EUR/g atteso':>13}") + tix_rows = [] + for size, price in pl.FIRMS[0]["tickets"]: + r = ticket_mc(R_M, G_M, lev_ev=LEV_EV, lev_fu=LEV_FU, notional=float(size)) + e3 = ev_three_ways(r, price) + tix_rows.append(dict(size=size, price=price, **r, **e3)) + print(f" ${size:>11,} ${price:>7,.0f} ${r['e_payout']:>10,.0f} ${e3['sunk']:>10,.0f} " + f"${e3['at_pass']:>12,.0f} ${e3['at_payout']:>15,.0f} " + f"{e3['at_payout']/EURUSD/365:>13.2f}") + bump(1) + TX = pd.DataFrame(tix_rows) + big = TX.iloc[-1] + rJ = ticket_mc(R_M, G_M, lev_ev=LEV_M, lev_fu=LEV_M, notional=100_000.0) + eJ = ev_three_ways(rJ, 579.0) + bump(1) + print(f"\n Lo STESSO biglietto $100k alla leva che massimizza J ({LEV_M:.2f}x su entrambi") + print(" gli stadi, cioe' la leva con cui e' costruito tutto il resto di questo filone):") + print(f" P(pass) {rJ['p_pass']:.1%} | P(payout) {rJ['p_payout']:.1%} | " + f"E[payout] ${rJ['e_payout']:,.0f} | EV persa ${eJ['sunk']:+,.0f} | " + f"EV al payout ${eJ['at_payout']:+,.0f}") + print(" -> l'EV e' positivo a ENTRAMBE le leve: la conclusione non dipende da quale") + print(" obiettivo (J o E[payout]) si sceglie per tararla.") + d_conv = big["at_payout"] - big["sunk"] + print(f"\n Quanto vale la CORREZIONE (da 'spesa persa' a 'rimborso al payout'), $100k: " + f"${d_conv:+,.0f} = {d_conv/max(abs(big['sunk']),1e-9)*100:+.1f}% dell'EV.") + print(f" Quanto separa le due convenzioni SBAGLIATE fra loro (al pass vs al payout): " + f"${big['at_pass']-big['at_payout']:+,.0f} β€” cioe' prezzo x (P(pass)-P(payout)) = " + f"${big['price']*(big['p_pass']-big['p_payout']):+,.0f}.") + print(f" >>> PREVISIONE (1) 'il rimborso vale +5-15% di EV': " + f"{'CONFERMATA' if 0.05 <= d_conv/max(abs(big['sunk']),1e-9) <= 0.15 else 'SMENTITA'}") + print(f"\n SENSIBILITA': con E[payout] = ${big['e_payout']:,.0f} e prezzo ${big['price']:,.0f},") + print(f" l'EV al payout resta positivo finche' P(payout) > " + f"{max(0.0,(big['price']-big['e_payout'])/big['price']):.1%} β€” misurata " + f"{big['p_payout']:.1%}.") + print(" ⚠ E' un EV in DOLLARI ATTESI. Su un conto da $654 il biglietto si compra poche") + print(" volte: la media non e' la grandezza che decide. Si guarda la distribuzione (Β§5).") + print(" ⚠ RICONCILIAZIONE (tre P(payout) diverse compaiono in questo file, e NON sono in") + print(" contraddizione): qui e' UN biglietto su UN anno di funded; la scala di conti") + print(" (Β§4-Β§6) lascia correre 36 mesi e ricompra -> la sua P e' piu' alta. Confrontare") + print(" le due senza dire la finestra e' l'errore che le farebbe sembrare incoerenti.") + tick("Β§3") + + # ============================================================== Β§4 ladder col rimborso + hr("Β§4. LA SCALA DI CONTI COL DEPOSITO RIMBORSABILE") + print(" Β§4.1 CONTROLLO β€” con `refund=False, deposito=0, min_payout=0` il mio simulatore") + print(" dev'essere BIT-EXACT rispetto a `pc.simulate` (stesso seme, stesso stream).") + cfg = tuple(W_A.get(n, 0.0) for n in PA.CRYPTO_PROP) + key = (cfg[0], cfg[1], cfg[2], "shared") + saved = pc._TUP_CACHE.pop(key, None) + pc._TUP_CACHE[key] = (R_M, G_M, I) + ref = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=N_PATHS_LAD, lev=LEV_M, + seed=SEED_LAD, lens="coupled", start=str(I[0].date())) + pc._TUP_CACHE.pop(key, None) + if saved is not None: + pc._TUP_CACHE[key] = saved + mine = ladder(R_M, G_M, N_PATHS_LAD, LEV_M, SEED_LAD, refund=False) + dmax = float(np.abs(np.sort(ref["eur_day"]) - np.sort(mine["eur_day"])).max()) + dmax2 = float(np.abs(ref["eur_day"] - mine["eur_day"]).max()) + print(f" max|diff| percorso per percorso = {dmax2:.3e} (ordinati: {dmax:.3e}) -> " + f"{'IDENTICO' if dmax2 < 1e-12 else 'DIVERGE β€” STOP'}") + if dmax2 >= 1e-12: + print(" Il simulatore non replica: i numeri sotto sarebbero di un'altra macchina.") + return + bump(1) + + print("\n Β§4.2 QUANTO VALE IL RIMBORSO (cella d'ancora mediana, stessi semi)") + print(f" {'convenzione':>34} {'EUR/g med':>10} {'P(>=10/g)':>10} {'P(>=50/g)':>10} " + f"{'P(zero)':>9} {'cassa fin. med':>15} {'media':>10}") + lad_rows = {} + for tag, rf, mp in (("spesa persa (pubblicato)", False, 0.0), + ("spesa persa + soglia $100", False, MIN_PAYOUT_TRADER), + ("RIMBORSO al primo payout", True, MIN_PAYOUT_TRADER)): + r = ladder(R_M, G_M, N_PATHS_LAD, LEV_M, SEED_LAD, refund=rf, min_payout=mp) + lad_rows[tag] = r + print(f" {tag:>34} {r['med']:>10.2f} {r['p_ge10']:>10.1%} {r['p_target']:>10.1%} " + f"{r['p_zero']:>9.1%} ${r['med_cash']:>14,.0f} ${r['mean_cash']:>9,.0f}") + bump(1) + a = lad_rows["spesa persa + soglia $100"] + b = lad_rows["RIMBORSO al primo payout"] + print(f"\n Delta del rimborso: P(>=50/g) {(b['p_target']-a['p_target'])*100:+.2f}pp | " + f"EUR/g mediana {b['med']-a['med']:+.2f} | cassa finale mediana " + f"${b['med_cash']-a['med_cash']:+,.0f} | quota restituita media ${b['mean_back']:,.0f}") + print(f" >>> PREVISIONE (1) 'il rimborso vale +1-3pp su P(>=50/g)': " + f"{'CONFERMATA' if 0.01 <= (b['p_target']-a['p_target']) <= 0.03 else 'SMENTITA'} " + f"({(b['p_target']-a['p_target'])*100:+.2f}pp)") + tick("Β§4") + + # ============================================================== Β§5 biglietto vs versare + hr("Β§5. IL BIGLIETTO CONTRO IL VERSARE β€” stessa metrica, stesso orizzonte, stessi sorteggi") + print(" Strada A (BIGLIETTO): $654 comprano il biglietto piu' grande possibile; la scala di") + print(" conti gira il libro W_A 38/38/25 sul conto della firm (XS01 LI' E' ESEGUIBILE),") + print(" deposito rimborsabile, morte-firm, cap $200k/firm; la cassa resta FERMA.") + print(" Strada B (LIBRO): gli stessi $654 restano sul libro LIVE 75/25 a min-order $5") + print(" (XS01 NON e' eseguibile a $654), nessuna barriera, fisco 33% sull'anno.") + print(" ⚠ L'asimmetria fra le due strade e' REALE ed e' il punto del canale funded: il") + print(" conto della firm da' accesso a sleeve che il capitale proprio non puo' eseguire.") + rb_full = FD.book_75_25(rate, alloc=654.0) if rate is not None else FD.book_75_25(None, alloc=654.0) + if rb_full.index.tz is None: + rb_full.index = rb_full.index.tz_localize("UTC") + rb = np.nan_to_num(rb_full.reindex(I).values.astype(float)) + print(f"\n libro LIVE a $654 (funding {'DENTRO' if rate is not None else 'FUORI'}): " + f"drift {rb.mean()*365*100:.2f}%/anno, vol {rb.std()*np.sqrt(365)*100:.1f}%, " + f"Sharpe {rb.mean()*365/(rb.std()*np.sqrt(365)):.2f} su {len(rb)} giorni") + rngb = np.random.default_rng(51224) + nb = int(np.ceil(HORIZON_DAYS / 20)) + st = rngb.integers(0, len(I) - 20, size=(N_PATHS_LAD, nb)) + BOOT = (st[:, :, None] + np.arange(20)[None, None, :]).reshape(N_PATHS_LAD, -1)[:, :HORIZON_DAYS] + print(f" bootstrap CONDIVISO fra le due strade: {N_PATHS_LAD} percorsi x {HORIZON_DAYS} " + f"giorni, blocchi 20 -> confronto APPAIATO") + print(f"\n {'versamento':>12} {'strada':>10} {'mediana':>11} {'p10':>10} {'p90':>10} " + f"{'media':>11} {'P(sotto il versato)':>20} {'P(>=50/g)':>10}") + cmp_rows = [] + for dep in (0.0, 250.0, 500.0): + A = ladder(R_M, G_M, N_PATHS_LAD, LEV_M, SEED_LAD, refund=True, + deposit_eur=dep, min_payout=MIN_PAYOUT_TRADER, boot=BOOT) + B = book_road(rb, BOOT, pl.START_EUR, dep) + bump(2) + for lab, r in (("BIGLIETTO", A), ("LIBRO", B)): + p_loss = float((r["cash"] < r["contributed"]).mean()) + p50g = r.get("p_target", 0.0) + print(f" {f'EUR {dep:,.0f}/mese':>12} {lab:>10} ${r['med_cash']:>10,.0f} " + f"${r['p10_cash']:>9,.0f} ${r['p90_cash']:>9,.0f} ${r['mean_cash']:>10,.0f} " + f"{p_loss:>20.1%} {p50g:>10.1%}") + cmp_rows.append(dict(dep=dep, road=lab, med=r["med_cash"], mean=r["mean_cash"], + p10=r["p10_cash"], p90=r["p90_cash"], ploss=p_loss)) + print(f" {'':>12} {'delta':>10} ${A['med_cash']-B['med_cash']:>+10,.0f} " + f"{'':>10} {'':>10} ${A['mean_cash']-B['mean_cash']:>+10,.0f}") + CMP = pd.DataFrame(cmp_rows) + print("\n ('P(>=50/g)' e' definita solo per la strada BIGLIETTO: la strada LIBRO non preleva") + print(" durante l'accumulo. La colonna che si confronta e' la RICCHEZZA TERMINALE.)") + tick("Β§5") + + # ============================================================== Β§6 la curva del capitale + hr("Β§6. LA CURVA DEL CAPITALE β€” a che conto il biglietto smette di essere la strada migliore") + print(" Stesso confronto appaiato, capitale iniziale variabile, nessun versamento (cosi' la") + print(" sola variabile e' il capitale). La strada LIBRO resta il 75/25 a ogni taglia:") + print(" e' una scelta CONSERVATIVA CONTRO la strada libro (a $5k/$20k il libro potrebbe") + print(" aggiungere XSR01/XS01) -> il capitale d'incrocio misurato e' un LIMITE SUPERIORE.") + cheap = min(t[1] for F in pl.FIRMS for t in F["tickets"]) + print(f"\n {'capitale':>10} {'A mediana':>12} {'B mediana':>12} {'A media':>12} " + f"{'B media':>12} {'A P(0 pay)':>11} {'A P(<${:.0f})'.format(cheap):>12} " + f"{'A med|>0':>10} {'vince med.':>11}") + caps = (600.0, 1_000.0, 2_000.0, 3_500.0, 6_000.0, 10_000.0, 20_000.0, 40_000.0) + cross_med = cross_mean = None + prev = None + for ce in caps: + A = ladder(R_M, G_M, N_PATHS_LAD, LEV_M, SEED_LAD, refund=True, start_eur=ce, + min_payout=MIN_PAYOUT_TRADER, boot=BOOT) + B = book_road(rb, BOOT, ce, 0.0) + bump(2) + wm = "BIGLIETTO" if A["med_cash"] > B["med_cash"] else "LIBRO" + nz = A["cash"][A["cash"] >= cheap] + p_stuck = float((A["cash"] < cheap).mean()) + print(f" EUR{ce:>7,.0f} ${A['med_cash']:>11,.0f} ${B['med_cash']:>11,.0f} " + f"${A['mean_cash']:>11,.0f} ${B['mean_cash']:>11,.0f} {A['p_zero']:>11.1%} " + f"{p_stuck:>12.1%} ${np.median(nz) if len(nz) else 0:>9,.0f} {wm:>11}") + cur = (A["med_cash"] > B["med_cash"], A["mean_cash"] > B["mean_cash"]) + if prev is not None: + if prev[0] != cur[0] and cross_med is None: + cross_med = (prev[2], ce) + if prev[1] != cur[1] and cross_mean is None: + cross_mean = (prev[2], ce) + prev = (cur[0], cur[1], ce) + print(f"\n Incrocio sulla MEDIANA: {cross_med if cross_med else 'nessuno nella griglia'}") + print(f" Incrocio sulla MEDIA : {cross_mean if cross_mean else 'nessuno nella griglia'}") + + print("\n ⚠ IL 'GRADINO' FRA EUR 1.000 E EUR 2.000 NON E' UN MECCANISMO: e' la MEDIANA") + print(" che cambia MODO. La distribuzione della strada-biglietto e' BIMODALE β€” o si") + print(" finisce SENZA nemmeno i soldi per ricomprare il biglietto piu' economico, o si") + print(f" finisce con migliaia. La colonna `P(<${cheap:.0f})` e' la massa del modo basso e") + print(" si muove LISCIA; la mediana complessiva salta di un ordine di grandezza appena") + print(" quella massa attraversa il 50%. **La mediana non e' una statistica robusta qui**:") + print(" quelle robuste sono `P(<$249)`, `P(0 payout)` e la media, che SATURANO.") + print(" -> il 'capitale d'incrocio' e' il punto in cui una massa di probabilita' passa") + print(" il 50%, NON una soglia con un meccanismo dietro. Va detto, perche' un numero") + print(" cosi' netto e' esattamente quello che si finirebbe per citare come soglia.") + print("\n Β§6.1 LA SECONDA FIRM β€” l'unico ingrediente STRUTTURALE che cambia con la cassa") + print(" (FTMO: max loss 10% contro il 6% STATICO di HYRO β€” il vincolo che il") + print(" progetto misura come binding su questo libro fin dal 25/07).") + print(" Si legge su MEDIA e P(0 payout), non sulla mediana (vedi sopra).") + all_firms = pl.FIRMS + print(f"\n {'capitale':>10} {'firm ammesse':>16} {'mediana':>11} {'media':>11} " + f"{'P(0 payout)':>11} {'cassa med':>11}") + for ce in (1_000.0, 2_000.0, 3_500.0): + for lab, fl in (("solo HYRO", [all_firms[0]]), ("HYRO+FTMO", all_firms)): + pl.FIRMS = fl + try: + A = ladder(R_M, G_M, N_PATHS_LAD, LEV_M, SEED_LAD, refund=True, start_eur=ce, + min_payout=MIN_PAYOUT_TRADER, boot=BOOT) + finally: + pl.FIRMS = all_firms + bump(1) + print(f" EUR{ce:>7,.0f} {lab:>16} ${A['med_cash']:>10,.0f} " + f"${A['mean_cash']:>10,.0f} {A['p_zero']:>11.1%} ${np.median(A['cash']):>10,.0f}") + print("\n Lettura: la seconda firm ALZA la media (piu' nozionale, e se una firm muore la") + print(" cassa si ricicla sull'altra) e puo' ALZARE anche P(0 payout) al capitale piccolo,") + print(" perche' converte in biglietti la cassa che altrimenti sarebbe rimasta ferma.") + print(" Non e' un pasto gratis: e' piu' esposizione allo stesso rischio binario.") + tick("Β§6") + + # ============================================================== Β§7 verdetto + hr("Β§7. LA GAMBA (c), RIFORMULATA β€” e la risposta alla domanda che decide") + ev_pay = float(big["at_payout"]) + ev_sunk = float(big["sunk"]) + A0 = [r for r in cmp_rows if r["dep"] == 0.0 and r["road"] == "BIGLIETTO"][0] + B0 = [r for r in cmp_rows if r["dep"] == 0.0 and r["road"] == "LIBRO"][0] + print(f" EV del biglietto $100k alla cella d'ancora MEDIANA, col funding dentro:") + print(f" convenzione VERA (rimborso al primo payout) ${ev_pay:>+10,.0f}") + print(f" convenzione PESSIMISTA (spesa persa) ${ev_sunk:>+10,.0f}") + print(f" -> EV positivo in {'ENTRAMBE' if min(ev_pay, ev_sunk) > 0 else 'UNA SOLA' if max(ev_pay,ev_sunk)>0 else 'NESSUNA'} " + f"le convenzioni.") + print(f"\n A 36 mesi, senza versamenti, $654 iniziali (confronto appaiato):") + print(f" BIGLIETTO mediana ${A0['med']:,.0f} media ${A0['mean']:,.0f} " + f"p10 ${A0['p10']:,.0f} P(sotto il versato) {A0['ploss']:.1%}") + print(f" LIBRO mediana ${B0['med']:,.0f} media ${B0['mean']:,.0f} " + f"p10 ${B0['p10']:,.0f}") + med_win = "BIGLIETTO" if A0["med"] > B0["med"] else "LIBRO" + mean_win = "BIGLIETTO" if A0["mean"] > B0["mean"] else "LIBRO" + print(f" -> sulla MEDIANA vince {med_win}; sulla MEDIA vince {mean_win}.") + print(" (e la mediana qui e' una statistica BIMODALE: dice da che parte del 50% sta") + print(" P(0 payout), non 'quanto' si guadagna β€” vedi Β§6)") + print(f" >>> PREVISIONE (5) 'EV positivo sulla MEDIA e negativo sulla MEDIANA': " + f"{'CONFERMATA' if (mean_win=='BIGLIETTO' and med_win=='LIBRO') else 'SMENTITA'}") + print("\n ONESTA' OBBLIGATORIE, portate avanti e non nascoste:") + print(f" * morte-firm {pl.FIRM_DEATH_PER_YEAR:.0%}/anno e' un'ASSUNZIONE del progetto") + print(" (24/07), non una misura: sopra c'e' dentro, ma nessuno l'ha stimata.") + print(" * la RIMBORSABILITA' e' DICHIARATIVA: viene dalla pagina della firm + fonti") + print(" secondarie concordi. Nessuno l'ha vista accadere. Il progetto ha gia' pagato") + print(" per un'assunzione mai verificata sul conto reale (GTAA01/PRIIPs).") + print(f" * il cap ${pl.FIRMS[0]['cap']:,}/trader limita STRUTTURALMENTE il canale: il") + print(" nozionale massimo su HYRO+FTMO e' $600k, cioe' un tetto al reddito, non al rischio.") + print(" * XS01 sul conto funded e' misurato su U13 fuori campione, ma le 6 gambe che") + print(" fuori campione non esistono restano non testabili (Β§29).") + print(" * l'ancora GIORNALIERA di TP01 (24 ore) NON e' in questa banda: terza dimensione,") + print(" dichiarata anche da Β§32, e la sua fortuna e' ancora dentro questi numeri.") + + hr("Β§7.1 IL TESTO PROPOSTO DELLA GAMBA (c)") + price = float(big["price"]) + p_nopay = 1.0 - float(big["p_payout"]) + print(f" Quantita' misurate oggi, a costo ZERO (una corsa di questo script):") + print(f" prezzo del biglietto $100k ${price:>10,.0f}") + print(f" quota del conto ($654) immobilizzata {price/(pl.START_EUR*EURUSD):>10.1%}") + print(f" P(nessun payout) -> quota persa {p_nopay:>10.1%}") + print(f" PERDITA ATTESA = prezzo x P(nessun payout) ${price*p_nopay:>10,.0f}") + print(f" EV con deposito NON rimborsato (pessimista) ${ev_sunk:>+10,.0f}") + print(f" EV con deposito rimborsato al payout (vero) ${ev_pay:>+10,.0f}") + print(f"\n Mesi di piano che il biglietto mette a rischio (quota / versamento mensile):") + for dep in (250.0, 500.0, 800.0): + print(f" a EUR {dep:>4,.0f}/mese: {price/(dep*EURUSD):>4.1f} mesi di versamenti " + f"(perdita attesa = {price*p_nopay/(dep*EURUSD):.1f} mesi)") + print("\n IL PUNTO DI STRUTTURA, che decide la FORMA del gate:") + print(" * misurare l'EV costa UNA CORSA DI QUESTO SCRIPT β€” zero dollari, minuti;") + print(" * misurare la RIMBORSABILITA' costa $579 e si puo' fare SOLO comprando.") + print(" Un gate che aspettasse la seconda misura sarebbe un gate che si puo' superare solo") + print(" dopo averlo violato. Percio' la gamba (c) va scritta in modo che la") + print(" rimborsabilita' NON decida: si richiede EV positivo sotto la convenzione") + print(" PESSIMISTA (quota mai restituita). Il rimborso diventa allora un regalo, non") + print(" un'ipotesi su cui si e' scommesso.") + print("\n >>> TESTO PROPOSTO β€” `GATE PROP-01 (c) CAPITALE`, due condizioni CONGIUNTE:") + print(" (c1) ECONOMICA, misura a costo zero, da rifare a ogni cambio del libro o") + print(" delle regole della firm:") + print(" EV del biglietto > 0 con la quota trattata come SPESA PERSA, calcolato") + print(" alla cella d'ancora MEDIANA (non canonica), col FUNDING dentro e sotto") + print(" la lente ACCOPPIATA, con `r0822d_gate_c.py`.") + print(f" Lettura di oggi: ${ev_sunk:+,.0f} -> " + f"{'PASS' if ev_sunk > 0 else 'FAIL'}") + print(" (c2) DI SOPRAVVIVENZA, verificabile dal saldo e dal piano di versamento:") + print(" prezzo del biglietto <= 2 versamenti mensili del piano in corso.") + print(" Motivo: il piano di accumulo e' la leva che il progetto ha misurato come") + print(" dominante (26/07: da 'mai' a 16 anni), e zero e' ASSORBENTE. Un biglietto") + print(" che costa piu' di due mensilita' non e' una scommessa sul canale funded:") + print(" e' un ritardo del piano che lo finanzia.") + print(f" Lettura di oggi (piano dichiarato EUR 500/mese, 26/07): " + f"${price:,.0f} vs 2 x ${500*EURUSD:,.0f} = ${2*500*EURUSD:,.0f} -> " + f"{'PASS' if price <= 2*500*EURUSD else 'FAIL'}") + print(" SCADENZA: nessuna. (c1) e (c2) sono verificabili OGGI; un gate d'attesa qui") + print(" non avrebbe un dato da aspettare, e stanotte due date sono cadute per questo.") + print(" Se si compra, si registra PRIMA: il rimborso deve comparire col primo payout;") + print(" se non compare, la convenzione vera e' 'spesa persa' β€” che (c1) ha gia'") + print(" assunto, quindi la decisione non va rifatta, va solo annotata.") + + print(f"\n TRIAL/CELLE DICHIARATI (al rialzo): {TRIALS}") + print(f" fatto in {time.time()-T0:.0f}s") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822d_piano_vero.py b/scripts/research/r0822d_piano_vero.py new file mode 100644 index 0000000..70a38dd --- /dev/null +++ b/scripts/research/r0822d_piano_vero.py @@ -0,0 +1,695 @@ +#!/usr/bin/env python +"""r0822d_piano_vero.py β€” LA TABELLA DEL PIANO CON TUTTE LE CORREZIONI DENTRO, UNA VOLTA SOLA. + +IL BUCO. L'obiettivo dichiarato del progetto e' 50 EUR/giorno e la domanda operativa e' Β«quanto +ci vuole, versando quantoΒ». La tabella pubblicata che risponde e' stata corretta DUE VOLTE, ogni +volta partendo dallo stesso numero LORDO e mai dall'altra correzione: + + riga Β«250 EUR/meseΒ», P(traguardo entro 20 anni) + * 92% al LORDO (26/07, fattore d'ancora x0.89) + * 52% col FISCO d'accumulo (07/08, r0807_piano_netto) + * 64% col FUNDING (22/08, r0822d_funding) + * ??? con ENTRAMBI <- non esiste + +Le due lenti sono state applicate SEPARATAMENTE. Questo script produce la lente unica, coerente, +con dentro tutte le correzioni MISURATE (nessuna scelta a mano), e dice quanto ci vuole davvero. + +L'ORDINE DI APPLICAZIONE, e perche' quello. + 1. FUNDING β€” e' un costo sul rendimento LORDO: si paga sul nozionale, ogni ora, prima che + esista una plusvalenza. Entra DENTRO la serie dei ritorni, giorno per giorno + (`r0822d_funding.book_75_25(rate)`), non come haircut a valle. + 2. ANCORA x0.89 β€” e' una correzione sul DRIFT della serie (fortuna di calendario misurata, + r0726_deluck_factor). Si applica alla serie, dopo il funding, come fa + `r0822d_funding.sezione4c` β€” cosi' i numeri restano confrontabili con quelli pubblicati. + La convenzione alternativa (de-luckare il lordo e POI togliere il funding) e' riportata + come sensibilita': vale 0.24pp di drift. + 3. FISCO β€” tassa la PLUSVALENZA, a fine anno, sul percorso GIA' netto di funding. L'ordine + inverso (tassare la plusvalenza lorda e poi sottrarre il funding dal capitale gia' tassato) + farebbe pagare il 33% su un guadagno mai incassato. + Da qui l'effetto di SECONDO ORDINE che la somma delle due correzioni non contiene: meno drift + -> meno plusvalenza -> meno imposta. Il congiunto e' meno severo della somma, e di quanto e' + una MISURA (sezione 3), non un'assunzione. + +COSA NON C'E' DENTRO, DI PROPOSITO. Il rischio di venue. Non e' un costo, e' un rischio di +ROVINA: a p=5% annuo il capitale mediano a 20 anni e' ZERO per qualunque calendario di +versamenti (26/07). Sta su un asse separato e ci resta; metterlo qui trasformerebbe una tabella +di pianificazione in una tabella di sopravvivenza. + +ASSUNZIONI FISCALI, dichiarate e NON un parere fiscale (le stesse del 27/07 e del 07/08): +33% sulle plusvalenze cripto realizzate, minusvalenze in carry 4 anni, patrimoniale 0.2%/anno +sul valore. Il modello tassa la variazione ANNUA di valore = limite superiore, stretto perche' +il libro realizza quasi tutto entro l'anno. + +TRAPPOLE GIA' PAGATE DA QUESTO PROGETTO, evitate qui per costruzione: + * un NON-ARRIVO si codifica +inf, mai -1 (con -1 la mediana MIGLIORA quando la colonna + peggiora: errore commesso il 07/08). Qui si stampano DUE mediane: quella incondizionata + (non-arrivo = +inf, quindi Β«>25aΒ») e quella condizionata, che non compare MAI senza la sua + probabilita' accanto. + * un Monte Carlo ha una RISOLUZIONE: misurata su 3 semi e dichiarata (sezione 2). + * la serie del libro e' su giorni di CALENDARIO (cripto, 365/anno): l'annualizzazione a 365 e' + corretta e `to_daily()` non serve. Sarebbe servito su una serie di giorni di borsa. + + uv run python scripts/research/r0822d_piano_vero.py + uv run python scripts/research/r0822d_piano_vero.py --rebuild # ignora la cache di serie +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT)) + +import r0725_capcurve as CC # noqa: E402 TAX_RATE, EURUSD, _boot_paths, book_series +import r0726_capwall_refresh as WR # noqa: E402 perp_and_wall, trajectory (2000 path) +import r0727_lumpsum_split as LS # noqa: E402 TARGET pubblicato +import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI +import r0807_piano_netto as PN # noqa: E402 passo_fiscale, sopravvivenza, accumula + +# --------------------------------------------------------------------------- +# Costanti dichiarate +# --------------------------------------------------------------------------- +DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor), non a occhio +START = 635.0 # il conto Deribit VERO di oggi (le tabelle pubblicate usano 600) +START_PUB = 600.0 # solo per la replica +N_WALL = 4000 # path per il muro (la taglia a cui furono calcolati i pubblicati) +N_TRAJ = 3000 # path per le traiettorie (idem, seed 725) +N_DEP = 3000 # path per la bisezione sul versamento +BLOCK = 20 +SEED_TRAJ = 725 +SEED_DEP = 20260822 +SEED_WALL = PN.SEED_MURO # 725 +ANNI_TRAJ = 25 # 20 troncherebbe la coda: convenzione di WR.trajectory +VERSAMENTI = (0, 250, 500, 800, 1_000, 2_000) +LUMPS_EUR = (0.0, 2_000.0, 5_000.0, 10_000.0) +ORIZZONTI = (10, 15, 20) +CONFIDENZE = (0.50, 0.75, 0.90) +VINTAGE_PUB = "2026-08-07" # la data in cui furono calcolati $272.061 / $258.338 +COSTO_STRUMENTO_VERO = 0.013931 # -1.39%/anno: funding misurato sulla sola finestra >= 2022-03 + +CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/pianovero") +if not CACHE.parent.parent.exists(): + CACHE = Path("/tmp/pythagoras_pianovero") +REBUILD = "--rebuild" in sys.argv +T0 = time.time() + + +def sez(t: str) -> None: + print("\n" + "=" * 100) + print(f" {t}") + print("=" * 100) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +# =========================================================================== +# 0) LE SERIE +# =========================================================================== +def costruisci_serie() -> pd.DataFrame: + """Le due serie giornaliere del libro live 75/25: senza e con funding. + + Nessuna re-implementazione: `r0822d_funding.book_75_25` e' la stessa lente di + `CC.book_series('hourly')` e la replica bit-exact e' verificata qui sotto. + """ + f = CACHE / "book.parquet" + if f.exists() and not REBUILD: + return pd.read_parquet(f) + # Se la cache dello storico funding c'e', si usa quella: nessuna chiamata al venue (il + # progetto gira su una VPS con soldi veri e un rate limit Deribit per-IP gia' saturato una + # volta, 29/07). Se NON c'e', si lascia che `fetch_funding` la scarichi: cosi' lo script + # gira anche da solo, su una macchina pulita. + for c in (Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/fund"), + Path("/tmp/pythagoras_funding_cache")): + if any(c.glob("*.parquet")): + sys.argv.append("--no-net") + break + import r0822d_funding as FD # noqa: E402 (USE_NET si decide all'import) + F = {} + for instr, (start, _asset, _kind) in FD.INSTR.items(): + d = FD.fetch_funding(instr, start).copy() + d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True) + F[instr] = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt") + rate = FD.build_rate(F) + A, B = FD.book_75_25(None), FD.book_75_25(rate) + out = pd.DataFrame({"nofund": A, "fund": B.reindex(A.index)}) + CACHE.mkdir(parents=True, exist_ok=True) + out.to_parquet(f) + return out + + +def deluck(r: np.ndarray) -> np.ndarray: + """Toglie dal DRIFT la fortuna d'ancora misurata. La vol non si tocca: fra le ancore era + invariata (7.80% -> 7.76%), la fortuna sta tutta nel drift (r0726_deluck_factor).""" + r = np.asarray(r, float) + return r - (1.0 - DELUCK) * r.mean() + + +# =========================================================================== +# LE LENTI +# =========================================================================== +class Lente: + """Una lente = (serie di ritorni, convenzione di prelievo, regime fiscale d'accumulo). + + `lordizza=True` -> convenzione PUBBLICATA: il prelievo si lordizza al 33% e il capitale + investito non paga mai imposte. Incoerente (r0807), tenuta per replica. + `lordizza=False` -> convenzione COERENTE: imposte ogni anno dentro il portafoglio, prelievo + gia' netto. + """ + + def __init__(self, nome: str, r: np.ndarray, lordizza: bool, nota: str = ""): + self.nome, self.r, self.lordizza, self.nota = nome, np.asarray(r, float), lordizza, nota + self.aliq = 0.0 if lordizza else CC.TAX_RATE + self.patr = 0.0 if lordizza else TX.PATRIMONIALE + self.perp = float("nan") + self.muro = float("nan") + + @property + def prelievo(self) -> float: + netto = CC.TARGET_EUR_DAY * 365 * CC.EURUSD + return netto / (1 - CC.TAX_RATE) if self.lordizza else netto + + @property + def drift(self) -> float: + return float(self.r.mean()) * 365.0 + + @property + def vol(self) -> float: + return float(self.r.std()) * 365.0 ** 0.5 + + +def muro_di(lente: Lente, n_paths: int = N_WALL, seed: int = SEED_WALL) -> tuple[float, float]: + """(rendita perpetua, capitale necessario). Perpetua = il prelievo piu' alto con + P(capitale a 20 anni >= capitale iniziale) >= 90% β€” definizione del 25/07, invariata. + + Bisezione identica a `PN.muro`, esposta solo per poter cambiare SEME e misurare la + risoluzione Monte Carlo. Il controllo positivo (a seme 725 deve dare esattamente `PN.muro`) + e' in sezione 2. + """ + lo, hi = 0.0, 0.40 + for _ in range(13): + mid = (lo + hi) / 2 + s = PN.sopravvivenza(lente.r, 1e6, 1e6 * mid, 20, lente.aliq, lente.patr, + n_paths=n_paths, seed=seed) + if s["p10_end"] >= 1e6: + lo = mid + else: + hi = mid + return lo, (lente.prelievo / lo if lo > 0.002 else float("inf")) + + +# =========================================================================== +# lettura onesta di un vettore di primi passaggi +# =========================================================================== +def leggi(colpito: np.ndarray, anni_sim: int = ANNI_TRAJ) -> dict: + """Mediana INCONDIZIONATA (non-arrivo = +inf) + mediana condizionata + P(20 anni). + + ⚠️ Il non-arrivo e' +inf, MAI -1: con -1 la mediana scende quanto peggio va la colonna + (errore commesso il 07/08 e corretto lo stesso giorno). E la mediana condizionata non si + stampa mai da sola: descrive solo chi arriva. + """ + y = np.where(colpito >= 0, colpito / 365.0, np.inf) + arr = y[np.isfinite(y)] + return dict( + med=float(np.median(y)), # +inf se meno di meta' arriva + med_cond=float(np.median(arr)) if len(arr) > 100 else float("nan"), + p20=float((y <= 20.0).mean()), + p_arr=float(np.isfinite(y).mean()), + anni_sim=anni_sim) + + +def fmt_med(d: dict) -> str: + m = d["med"] + return f">{d['anni_sim']}a" if not np.isfinite(m) else f"{m:.1f}a" + + +def fmt_cond(d: dict) -> str: + c = d["med_cond"] + return "β€”" if np.isnan(c) else f"{c:.1f}a" + + +# =========================================================================== +# bisezione sul versamento (start parametrico: le tabelle pubblicate partono da $600) +# =========================================================================== +def dep_per_conf(paths: np.ndarray, lente: Lente, conf: float, start: float, + lump_eur: float = 0.0) -> tuple[float, float]: + """(versamento mensile EUR minimo per P(traguardo entro l'orizzonte) >= conf, totale versato). + + I path si estraggono UNA volta e si riusano a ogni passo: con campioni diversi per passo la + funzione non sarebbe monotona e la bisezione oscillerebbe (lezione WR.solve_deposit). + """ + def p(dep: float) -> float: + c = PN.accumula(paths, dep, lente.muro, lente.aliq, lente.patr, + start=start, lump_eur=lump_eur)["colpito"] + return float((c >= 0).mean()) + + lo, hi = 0.0, 6_000.0 + if p(hi) < conf: + return float("inf"), float("nan") + for _ in range(12): + mid = (lo + hi) / 2 + if p(mid) >= conf: + hi = mid + else: + lo = mid + versato = PN.accumula(paths, hi, lente.muro, lente.aliq, lente.patr, + start=start, lump_eur=lump_eur)["versato"] + return hi, versato + + +# =========================================================================== +def main() -> None: + print("=" * 100) + print(" r0822d β€” IL PIANO VERO: fisco d'accumulo E funding nella STESSA tabella") + print("=" * 100) + + B = costruisci_serie() + idx = pd.DatetimeIndex(B.index) + A_raw, F_raw = B["nofund"].values.astype(float), B["fund"].values.astype(float) + print(f"\n serie giornaliera del libro LIVE (TP01 75 + SKH01 25, lente 'hourly', path live)") + print(f" {len(B)} giorni, dal {idx.min().date()} al {idx.max().date()} {clock()}") + print(f" drift lordo {A_raw.mean() * 365:.3%} Β· drift netto funding {F_raw.mean() * 365:.3%}") + print(" ⚠️ giorni di CALENDARIO (cripto): annualizzazione a 365 corretta, `to_daily()` non") + print(" serve qui β€” servirebbe su una serie di giorni di borsa (lezione 25/07 su GTAA01).") + + # ===================================================================== + sez("1) CONTROLLO DI REPLICA β€” prima di pubblicare un numero nuovo, riprodurre i vecchi") + print("\n Regola del progetto (07/08): Β«prima di pubblicare un numero nuovo, far riprodurre") + print(" alla macchina quello vecchioΒ». Cinque controlli, tutti su codice IMPORTATO.\n") + ok = {} + + # (R1) la serie senza funding e' bit-exact la serie del libro gia' pubblicata + C = CC.book_series("hourly") + d = float(np.max(np.abs(A_raw - C.reindex(idx).values))) + ok["R1"] = d == 0.0 + print(f" R1 serie 'senza funding' vs CC.book_series('hourly') max|dif| = {d:.3e} " + f"{'BIT-EXACT' if ok['R1'] else 'FALLITA'}") + + # (R2) il costo del funding per finestra + print(f"\n R2 costo del funding per finestra (pubblicato 22/08: -2,16% / -1,39% / -0,55%)") + fin = [("piena (proxy inverse pre-2022)", None, -0.0216), + (">= 2022-03-15 (strumento vero)", "2022-03-15", -0.0139), + (">= 2025-01-01", "2025-01-01", -0.0055)] + r2 = True + for lab, lo, atteso in fin: + m = np.ones(len(B), bool) if lo is None else np.asarray(idx >= pd.Timestamp(lo, tz="UTC")) + got = -(A_raw[m].mean() - F_raw[m].mean()) * 365 + r2 &= abs(got - atteso) < 5e-4 + print(f" {lab:<34} n={m.sum():>5} misurato {got:+.4%} atteso {atteso:+.2%}") + ok["R2"] = r2 + + # (R3-R4) i muri pubblicati, sul VINTAGE su cui furono calcolati + print(f"\n R3/R4 i muri pubblicati ($272.061 lordo / $258.338 netto fisco) sul vintage") + print(f" {VINTAGE_PUB} β€” la data in cui furono calcolati.") + mv = np.asarray(idx <= pd.Timestamp(VINTAGE_PUB, tz="UTC")) + Lv0 = Lente("vintage LORDO", deluck(A_raw[mv]), True) + Lv1 = Lente("vintage +FISCO", deluck(A_raw[mv]), False) + Lv0.perp, Lv0.muro = PN.muro(Lv0.r, Lv0.prelievo, 0.0, 0.0) + Lv1.perp, Lv1.muro = PN.muro(Lv1.r, Lv1.prelievo, Lv1.aliq, Lv1.patr) + ok["R3"] = abs(Lv0.muro - LS.TARGET) < 1.0 + ok["R4"] = abs(Lv1.muro - 258_338.0) < 1.0 + print(f" LORDO perpetua {Lv0.perp:.4%} muro ${Lv0.muro:,.0f} atteso $272,061 " + f"{'ESATTO AL DOLLARO' if ok['R3'] else 'DIVERSO'}") + print(f" NETTO perpetua {Lv1.perp:.4%} muro ${Lv1.muro:,.0f} atteso $258,338 " + f"{'ESATTO AL DOLLARO' if ok['R4'] else 'DIVERSO'} {clock()}") + + # (R5) i numeri del funding, alla SUA taglia MC (2000 path) + print(f"\n R5 i numeri del filone funding (22/08), alla sua taglia dichiarata (2000 path):") + g = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - CC.TAX_RATE) + rA, rF = deluck(A_raw), deluck(F_raw) + _, pA, wA = WR.perp_and_wall(rA, 1.0, g) + _, pF, wF = WR.perp_and_wall(rF, 1.0, g) + tA = WR.trajectory(rA, wA, n_paths=2000)[250] + tF = WR.trajectory(rF, wF, n_paths=2000)[250] + ok["R5"] = abs(wF / wA - 1 - 0.175) < 0.004 and abs(pF - 0.0914) < 3e-4 + print(f" perpetua {pA:.2%} -> {pF:.2%} (pubblicato 10,73% -> 9,14%)") + print(f" muro ${wA:,.0f} -> ${wF:,.0f} = {wF / wA - 1:+.1%} (pubblicato $276,6k -> " + f"$325,0k = +17,5%)") + print(f" traiettoria EUR250/mese {tA['p50']:.1f}a P20 {tA['p20y']:.0%} -> " + f"{tF['p50']:.1f}a P20 {tF['p20y']:.0%} (pubblicato 16,4a/90% -> 18,7a/64%) {clock()}") + + # (R6) la colonna LORDA e la colonna NETTA delle traiettorie, sul vintage, da $600 + print(f"\n R6 le due colonne di traiettoria pubblicate, sul vintage {VINTAGE_PUB}, da $600:") + rng = np.random.default_rng(SEED_TRAJ) + pv = CC._boot_paths(Lv0.r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rng) + att_l = {250: 16.3, 500: 12.4, 800: 10.0, 1_000: 9.0, 2_000: 6.0} + att_n = {250: 19.8, 500: 14.7, 800: 11.4, 1_000: 10.0, 2_000: 6.4} + print(f" {'EUR/m':>7}{'LORDO mis.':>13}{'atteso':>9}{'P20':>7}" + f"{' |':>3}{'NETTO mis.':>13}{'atteso':>9}{'P20':>7}") + r6 = True + for dep in (250, 500, 800, 1_000, 2_000): + l = leggi(PN.accumula(pv, dep, Lv0.muro, 0.0, 0.0, start=START_PUB)["colpito"]) + n = leggi(PN.accumula(pv, dep, Lv1.muro, Lv1.aliq, Lv1.patr, start=START_PUB)["colpito"]) + r6 &= abs(l["med_cond"] - att_l[dep]) < 0.25 and abs(n["med_cond"] - att_n[dep]) < 0.25 + print(f" {dep:>7}{fmt_cond(l):>13}{att_l[dep]:>9.1f}{l['p20']:>7.0%}" + f"{' |':>3}{fmt_cond(n):>13}{att_n[dep]:>9.1f}{n['p20']:>7.0%}") + ok["R6"] = r6 + del pv + print(f"\n ESITO REPLICA: {sum(ok.values())}/{len(ok)} β€” " + + ", ".join(f"{k}={'OK' if v else 'NO'}" for k, v in ok.items())) + if not all(ok.values()): + raise SystemExit("REPLICA INCOMPLETA: un numero nuovo sarebbe indistinguibile da un bug.") + print(""" + ⚠️ E IL FATTO CHE VA DETTO: la replica e' esatta AL DOLLARO solo sul vintage del 07/08. + Su TUTTA la serie di oggi lo stesso codice, senza una riga cambiata, da' muri diversi:""") + L0t = Lente("oggi LORDO", rA, True) + L0t.perp, L0t.muro = PN.muro(L0t.r, L0t.prelievo, 0.0, 0.0) + print(f" muro LORDO: ${Lv0.muro:,.0f} (vintage {VINTAGE_PUB}, {mv.sum()} giorni) -> " + f"${L0t.muro:,.0f} (oggi, {len(B)} giorni) = {L0t.muro / Lv0.muro - 1:+.1%}") + print(" `data/raw/` e' gitignored e il cron lo allunga ogni notte: 15 giorni in piu' di") + print(" dati spostano il muro del ~2%, cioe' PIU' della risoluzione Monte Carlo. E' la") + print(" stessa lezione del 07/08 sul gate di GTAA01 (un numero cambia senza che il codice") + print(" cambi = i dati non sono versionati). Percio' qui sotto ogni confronto fra lenti e'") + print(" fatto sullo STESSO vintage: un grado di liberta' per volta.") + + # ===================================================================== + sez("2) LE QUATTRO LENTI E IL MURO β€” la riga che non esisteva") + print(""" + Tutte de-luckate x0.89. Le prime due sono le lenti pubblicate; la terza e' quella del filone + funding di stanotte; la quarta e' la lente unica con dentro TUTTO.""") + Lo = { + "L0 LORDO (26/07)": Lente("L0", rA, True, "prelievo lordizzato, nessuna imposta in accumulo"), + "L1 +FISCO (07/08)": Lente("L1", rA, False, "imposte ogni anno nel portafoglio"), + "L2 +FUNDING (22/08)": Lente("L2", rF, True, "funding esatto, convenzione pubblicata"), + "L3 CONGIUNTA": Lente("L3", rF, False, "funding esatto + fisco coerente"), + } + rFb = deluck(A_raw - COSTO_STRUMENTO_VERO / 365.0) + Lo["L3b CONGIUNTA (str. vero)"] = Lente("L3b", rFb, False, + "funding -1,39%/anno (finestra >= 2022-03)") + print(f"\n {'lente':<27}{'drift':>9}{'vol':>8}{'prelievo/a':>13}{'perpetua':>10}{'muro':>13}") + for k, L in Lo.items(): + L.perp, L.muro = muro_di(L) + print(f" {k:<27}{L.drift:>9.2%}{L.vol:>8.2%}{L.prelievo:>13,.0f}" + f"{L.perp:>10.2%}{L.muro:>13,.0f}") + print(f" {clock()}") + + # controllo positivo sulla bisezione con seme + risoluzione Monte Carlo + p_ctrl, w_ctrl = PN.muro(Lo["L3 CONGIUNTA"].r, Lo["L3 CONGIUNTA"].prelievo, + CC.TAX_RATE, TX.PATRIMONIALE) + same = abs(w_ctrl - Lo["L3 CONGIUNTA"].muro) < 1e-6 + print(f"\n controllo positivo: `muro_di` a seme {SEED_WALL} == `PN.muro` importato " + f"${w_ctrl:,.0f} vs ${Lo['L3 CONGIUNTA'].muro:,.0f} {'OK' if same else 'DIVERGE'}") + semi = [Lo["L3 CONGIUNTA"].muro] + for s in (2026, 8_22): + semi.append(muro_di(Lo["L3 CONGIUNTA"], seed=s)[1]) + sp = (max(semi) - min(semi)) / np.mean(semi) + print(f" RISOLUZIONE MONTE CARLO del muro, misurata su 3 semi a {N_WALL} path: " + + " / ".join(f"${x:,.0f}" for x in semi)) + print(f" ampiezza {sp:.1%} -> si citano TRE cifre significative (${Lo['L3 CONGIUNTA'].muro/1000:.0f}k), " + f"non il dollaro. {clock()}") + + # sensibilita' sull'ordine de-luck / funding + alt = Lente("alt", deluck(A_raw) - (A_raw - F_raw).mean(), False) + alt.perp, alt.muro = muro_di(alt) + print(f"\n sensibilita' all'ORDINE de-luck/funding (convenzione alternativa: de-luckare il") + print(f" drift LORDO e poi togliere il funding): drift {alt.drift:.2%} contro " + f"{Lo['L3 CONGIUNTA'].drift:.2%}, muro ${alt.muro:,.0f} contro " + f"${Lo['L3 CONGIUNTA'].muro:,.0f} ({alt.muro / Lo['L3 CONGIUNTA'].muro - 1:+.1%}).") + print(" Sotto la risoluzione MC: la scelta di convenzione non decide niente. Si tiene quella") + print(" di `r0822d_funding.sezione4c`, per confrontabilita' con i numeri gia' pubblicati.") + + # ===================================================================== + sez("3) LE DUE CORREZIONI SI SOMMANO? β€” l'effetto di secondo ordine, misurato") + print(""" + ATTESA DICHIARATA PRIMA DI MISURARE: meno drift -> meno plusvalenza -> meno imposta, quindi + il congiunto dovrebbe essere MENO severo della somma delle due correzioni prese da sole. + Misurato come interazione di un fattoriale 2x2 (funding OFF/ON x fisco OFF/ON) in TRE monete, + perche' un'interazione piccola puo' cambiare SEGNO a seconda di come la si misura.""") + + # (3a) accumulo puro: il capitale a 20 anni non dipende dal muro -> isola il meccanismo + print("\n (3a) SUL CAPITALE ACCUMULATO β€” la domanda pura sul meccanismo.") + print(" Capitale mediano a 20 anni, EUR250/mese, partenza $%.0f. Non dipende dal muro:" % START) + print(" qui dentro c'e' SOLO l'accumulo, cioe' l'unico posto dove il meccanismo vive.") + capf = {} + for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"): + L = Lo[k] + rr = np.random.default_rng(SEED_DEP) + pp = CC._boot_paths(L.r, N_DEP, 20 * 365, BLOCK, rr) + capf[k] = float(np.median(PN.accumula(pp, 250, L.muro, L.aliq, L.patr, + start=START)["cap"])) + del pp + print(f"\n {'':<14}{'fisco OFF':>16}{'fisco ON':>16}{'rapporto ON/OFF':>19}") + for lab, a, b in (("funding OFF", "L0 LORDO (26/07)", "L1 +FISCO (07/08)"), + ("funding ON ", "L2 +FUNDING (22/08)", "L3 CONGIUNTA")): + print(f" {lab:<14}{capf[a]:>16,.0f}{capf[b]:>16,.0f}" + f"{capf[b] / capf[a]:>19.4f}") + dragOFF = capf["L1 +FISCO (07/08)"] / capf["L0 LORDO (26/07)"] + dragON = capf["L3 CONGIUNTA"] / capf["L2 +FUNDING (22/08)"] + inter_log = (np.log(capf["L3 CONGIUNTA"]) - np.log(capf["L1 +FISCO (07/08)"]) + - np.log(capf["L2 +FUNDING (22/08)"]) + np.log(capf["L0 LORDO (26/07)"])) + verso_drag = "MENO" if dragON > dragOFF else "DI PIU'" + print(f"\n Il fisco toglie il {1 - dragOFF:.1%} del capitale a funding OFF e il " + f"{1 - dragON:.1%} a funding ON.") + esito_mecc = ("ESISTE ed e' nel verso previsto" if dragON > dragOFF else "NON si vede") + print(f" -> il meccanismo {esito_mecc}: con meno drift il fisco morde {verso_drag}.") + print(f" Interazione moltiplicativa (log-capitale): {inter_log:+.4f} = " + f"{np.expm1(inter_log):+.2%} di capitale.") + print(" ⚠️ Ma e' UNA FRAZIONE: il fisco resta la correzione dominante in entrambe le") + print(" colonne, e la 'compensazione' non ne recupera che una parte trascurabile.") + + # (3b) il muro + inter_w = (Lo["L3 CONGIUNTA"].muro - Lo["L1 +FISCO (07/08)"].muro + - Lo["L2 +FUNDING (22/08)"].muro + Lo["L0 LORDO (26/07)"].muro) + print("\n (3b) SUL MURO β€” la fase di prelievo.") + print(f" ${Lo['L0 LORDO (26/07)'].muro:,.0f} (L0) Β· ${Lo['L1 +FISCO (07/08)'].muro:,.0f} (L1) Β· " + f"${Lo['L2 +FUNDING (22/08)'].muro:,.0f} (L2) Β· ${Lo['L3 CONGIUNTA'].muro:,.0f} (L3)") + print(f" interazione ${inter_w:+,.0f} = {abs(inter_w) / Lo['L3 CONGIUNTA'].muro:.2%} del muro " + f"congiunto, cioe' SOTTO la risoluzione Monte Carlo ({sp:.1%}): indistinguibile da zero.") + + # (3c) il versamento necessario + print("\n (3c) SUL VERSAMENTO NECESSARIO β€” la moneta in cui si decide.") + print(f" Orizzonte 20 anni, P=90%, partenza ${START:.0f}. Ogni lente col PROPRIO muro.") + # ⚠️ ogni lente ha la SUA serie di ritorni: i path si riestraggono per lente (stesso seme, + # quindi stessi INDICI di blocco: il confronto fra lenti resta appaiato) e non si riusano. + dep90 = {} + for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"): + L = Lo[k] + rr = np.random.default_rng(SEED_DEP) + pp = CC._boot_paths(L.r, N_DEP, 20 * 365, BLOCK, rr) + dep90[k], _ = dep_per_conf(pp, L, 0.90, START) + del pp + b = dep90["L0 LORDO (26/07)"] + d_fisco = dep90["L1 +FISCO (07/08)"] - b + d_fund = dep90["L2 +FUNDING (22/08)"] - b + d_joint = dep90["L3 CONGIUNTA"] - b + inter = d_joint - d_fisco - d_fund + print(f"\n {'lente':<27}{'EUR/mese per P=90% a 20 anni':>32}{'vs L0':>10}") + for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"): + print(f" {k:<27}{dep90[k]:>32,.0f}{dep90[k] - b:>+10,.0f}") + print(f"\n somma ingenua : {d_fisco:+,.0f} {d_fund:+,.0f} = {d_fisco + d_fund:+,.0f} EUR/mese") + print(f" MISURATO : {d_joint:+,.0f} EUR/mese") + print(f" INTERAZIONE : {inter:+,.0f} EUR/mese ({inter / (d_fisco + d_fund):+.0%} della somma)") + # ⚠️ un'interazione piccola e' una DIFFERENZA DI DIFFERENZE: prima di dichiararne il SEGNO + # va misurato il rumore Monte Carlo che la circonda, o si pubblica un segno di rumore. + inters = [inter] + for sd in (4_242, 90_909): + dd = {} + for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"): + L = Lo[k] + rr = np.random.default_rng(sd) + pp = CC._boot_paths(L.r, N_DEP, 20 * 365, BLOCK, rr) + dd[k], _ = dep_per_conf(pp, L, 0.90, START) + del pp + inters.append(dd["L3 CONGIUNTA"] - dd["L1 +FISCO (07/08)"] + - dd["L2 +FUNDING (22/08)"] + dd["L0 LORDO (26/07)"]) + segno_ok = all(x > 0 for x in inters) or all(x < 0 for x in inters) + et_segno = "STABILE" if segno_ok else "NON stabile: e' rumore" + print(" su 3 semi: " + " / ".join(f"{x:+,.0f}" for x in inters) + + f" -> segno {et_segno}" + + f" (bisezione a {6000 / 2 ** 12:.1f} EUR/m di risoluzione)") + verso = "MENO" if np.median(inters) < 0 else "PIU'" + print(f"\n πŸ“Œ ESITO, e la mia attesa era SBAGLIATA nella moneta che conta. Il meccanismo") + print(f" esiste (3a: il fisco morde meno dove il drift e' minore) ma nel VERSAMENTO") + if segno_ok: + print(f" necessario il congiunto e' {verso} severo della somma ({np.median(inters):+,.0f} EUR/m,") + print(" segno stabile su 3 semi): la funzione Β«versamento -> probabilita'Β» e' convessa") + print(" e ribalta il segno di un'interazione gia' piccola.") + else: + print(f" necessario l'interazione NON ha segno stabile fra i semi ({inters[0]:+,.0f} / " + f"{inters[1]:+,.0f} / {inters[2]:+,.0f} EUR/m):") + print(" e' rumore Monte Carlo, e va detto invece di pubblicarne il segno.") + print(" Sul MURO l'interazione e' sotto la risoluzione MC.") + print(f" REGOLA OPERATIVA: NON contare su una compensazione fra le due correzioni β€”") + print(f" si sommano, e il congiunto resta {d_joint / max(d_fisco, d_fund):.2f}x la piu' grande delle due") + print(f" presa da sola. {clock()}") + + sez(f"4) TRAIETTORIA da ${START:.0f} β€” anni al capitale-rendita, nelle quattro lenti") + print(f""" + Partenza ${START:.0f} = il conto Deribit VERO (le tabelle pubblicate partono da $600). + Ogni lente usa il PROPRIO muro. {N_TRAJ} path, blocchi da {BLOCK} giorni, orizzonte {ANNI_TRAJ} anni. + DUE mediane: 'mediana' = incondizionata (non-arrivo = +inf -> '>{ANNI_TRAJ}a'), 'cond.' = solo su chi + arriva β€” e la seconda non si legge senza la P accanto.""") + traj = {} + for k in Lo: + L = Lo[k] + rr = np.random.default_rng(SEED_TRAJ) + pp = CC._boot_paths(L.r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rr) + traj[k] = {dep: leggi(PN.accumula(pp, dep, L.muro, L.aliq, L.patr, + start=START)["colpito"]) for dep in VERSAMENTI} + del pp + hdr = f" {'EUR/m':>7}" + for k in Lo: + hdr += f"{k.split()[0]:>22}" + print("\n" + hdr) + print(f" {'':>7}" + "".join(f"{'mediana cond. P20':>22}" for _ in Lo)) + for dep in VERSAMENTI: + row = f" {dep:>7}" + for k in Lo: + d = traj[k][dep] + row += f"{fmt_med(d):>9}{fmt_cond(d):>7}{d['p20']:>6.0%}" + print(row) + print(f" {clock()}") + d250 = {k: traj[k][250] for k in Lo} + print(f""" + πŸ“Œ LA RIGA CHE MANCAVA. Β«EUR250/meseΒ» P(traguardo entro 20 anni): + L0 LORDO {d250['L0 LORDO (26/07)']['p20']:>5.0%} (pubblicato 92% β€” qui {d250['L0 LORDO (26/07)']['p20']:.0%} su serie piu' lunga e da $635) + L1 +FISCO {d250['L1 +FISCO (07/08)']['p20']:>5.0%} (pubblicato 52%) + L2 +FUNDING {d250['L2 +FUNDING (22/08)']['p20']:>5.0%} (pubblicato 64%) + L3 CONGIUNTA {d250['L3 CONGIUNTA']['p20']:>5.0%} <- IL NUMERO ONESTO + L3b str. vero {d250['L3b CONGIUNTA (str. vero)']['p20']:>5.0%} (col funding misurato solo dove lo strumento esiste) + + ⚠️ Le due colonne di funding sono una BANDA, non una scelta: -2,16%/anno usa il proxy + inverse sul 40% del campione (conservativa), -1,39%/anno usa solo la finestra in cui il + perpetual lineare USDC esiste davvero. La verita' sta fra 14% e 26%, non su un numero. + P(traguardo entro i {ANNI_TRAJ} anni simulati), stessa riga: L3 {d250['L3 CONGIUNTA']['p_arr']:.0%} Β· L3b {d250['L3b CONGIUNTA (str. vero)']['p_arr']:.0%} + β€” cioe' anche allungando di 5 anni l'orizzonte una quota non trascurabile non arriva.""") + + # --- il lump: i soldi gia' fermi --- + sez(f"4-bis) E SE PARTO CON UN VERSAMENTO INIZIALE β€” ${START:.0f} + un lump, lente CONGIUNTA") + print(f""" + Il progetto ha misurato (27/07) che EUR10.000 fermi oggi valgono 2,45x, ma quel lump non e' + MAI entrato nelle tabelle nette. Qui e' nella lente congiunta. Lo stesso muro, gli stessi path.""") + L3 = Lo["L3 CONGIUNTA"] + rr = np.random.default_rng(SEED_TRAJ) + p3 = CC._boot_paths(L3.r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rr) + print(f"\n {'lump EUR':>10}" + "".join(f"{f'EUR{d}/mese':>26}" for d in (0, 250, 500))) + print(f" {'':>10}" + "".join(f"{'mediana cond. P20':>26}" for _ in range(3))) + for lump in LUMPS_EUR: + row = f" {lump:>10,.0f}" + for dep in (0, 250, 500): + d = leggi(PN.accumula(p3, dep, L3.muro, L3.aliq, L3.patr, + start=START, lump_eur=lump)["colpito"]) + row += f"{fmt_med(d):>11}{fmt_cond(d):>8}{d['p20']:>7.0%}" + print(row) + del p3 + print(f" {clock()}") + print("\n ⚠️ Un lump non e' capitale disponibile se e' il fondo d'emergenza (nota 27/07),") + print(" e a EUR10.000 sul conto si riapre la decisione di venue (soglia dichiarata $20k,") + print(" ma il piano cambia -> CLAUDE.md dice di riaprire PRIMA, non dopo).") + + # ===================================================================== + sez("5) QUANTO VERSARE PER UN ORIZZONTE DATO β€” lente CONGIUNTA, e il totale versato accanto") + print(f""" + bersaglio ${L3.muro:,.0f} (lente congiunta) Β· partenza ${START:.0f} Β· nessun lump + La colonna 'tot. versato' e' il confronto che conta: quanto del bersaglio lo COMPRI coi + bonifici invece di farlo produrre alla strategia.""") + L1 = Lo["L1 +FISCO (07/08)"] + print(f"\n {'orizzonte':>10}" + "".join(f"{f'P={c:.0%}':>13}" for c in CONFIDENZE) + + f"{'tot.versato@90%':>18}{'% bersaglio':>13}{' | solo fisco @90%':>21}") + riga10 = None + for h in ORIZZONTI: + rr = np.random.default_rng(SEED_DEP) + ph = CC._boot_paths(L3.r, N_DEP, h * 365, BLOCK, rr) + celle, tot90, dep90h = [], float("nan"), float("nan") + for c in CONFIDENZE: + v, vers = dep_per_conf(ph, L3, c, START) + celle.append(f"EUR{v:,.0f}/m" if np.isfinite(v) else "irraggiung.") + if c == 0.90: + tot90, dep90h = vers, v + del ph + rr = np.random.default_rng(SEED_DEP) + p1 = CC._boot_paths(L1.r, N_DEP, h * 365, BLOCK, rr) + v1, _ = dep_per_conf(p1, L1, 0.90, START) + del p1 + quota = tot90 / L3.muro if np.isfinite(tot90) else float("nan") + if h == 10: + rr = np.random.default_rng(SEED_DEP) + pb = CC._boot_paths(Lo["L3b CONGIUNTA (str. vero)"].r, N_DEP, h * 365, BLOCK, rr) + vb, versb = dep_per_conf(pb, Lo["L3b CONGIUNTA (str. vero)"], 0.90, START) + del pb + riga10 = (dep90h, tot90, quota, vb, versb) + print(f" {f'{h} anni':>10}" + "".join(f"{x:>13}" for x in celle) + + f"{f'${tot90:,.0f}':>18}{quota:>13.0%}" + + f"{f' | EUR{v1:,.0f}/m ({dep90h / v1:.2f}x)':>21}") + print(f" {clock()}") + print(""" + πŸ“Œ A 10 anni il rendimento fa la minoranza del lavoro: il resto lo fanno i bonifici. E' + la lezione gia' scritta il 26/07, qui con le due correzioni dentro: comprimere + l'orizzonte non fa lavorare la strategia, COMPRA il capitale.""") + + # ===================================================================== + sez("6) LA DOMANDA INVERSA β€” che rendita compra quello che posso permettermi") + print(f""" + Rendita NETTA mediana in EUR/giorno = capitale x perpetua({L3.perp:.2%}). Le imposte sono gia' + dentro la perpetua: il numero e' netto e NON va lordizzato una seconda volta. + Lente CONGIUNTA, partenza ${START:.0f}, nessun lump.""") + print(f"\n {'EUR/mese':>9}" + "".join(f"{f'{y}a':>12}" for y in (5, 10, 15, 20)) + + f"{'P(>=EUR50/g a 20a)':>21}") + for dep in VERSAMENTI: + celle, p50 = [], 0.0 + for y in (5, 10, 15, 20): + rr = np.random.default_rng(SEED_DEP) + py = CC._boot_paths(L3.r, N_DEP, y * 365, BLOCK, rr) + cap = PN.accumula(py, dep, L3.muro, L3.aliq, L3.patr, start=START)["cap"] + del py + rend = cap * L3.perp / 365.0 / CC.EURUSD + celle.append(f"{np.median(rend):,.2f}") + if y == 20: + p50 = float((rend >= CC.TARGET_EUR_DAY).mean()) + print(f" {dep:>9}" + "".join(f"{x:>12}" for x in celle) + f"{p50:>21.1%}") + print(f" {clock()}") + + # ===================================================================== + sez("VERDETTO") + j250 = traj["L3 CONGIUNTA"][250] + d10, t10, q10, d10b, t10b = riga10 + print(f""" + 1. LA TABELLA ORA ESISTE, ed e' peggiore di entrambe le lenti separate: + EUR250/mese, P(entro 20 anni) β€” LORDO {d250['L0 LORDO (26/07)']['p20']:.0%} Β· +FISCO {d250['L1 +FISCO (07/08)']['p20']:.0%} Β· +FUNDING {d250['L2 +FUNDING (22/08)']['p20']:.0%} Β· CONGIUNTA {j250['p20']:.0%}. + Il muro passa da ${Lo['L0 LORDO (26/07)'].muro:,.0f} a ${L3.muro:,.0f} ({L3.muro / Lo['L0 LORDO (26/07)'].muro - 1:+.0%}). + + 2. LE DUE CORREZIONI SI SOMMANO β€” la mia attesa (Β«meno drift -> meno imposta -> congiunto + meno severo della sommaΒ») e' MISURATA e vale poco, e nella moneta che decide NON la + conferma. Interazione: {np.expm1(inter_log):+.2%} sul capitale accumulato (li' il meccanismo + c'e'), ${inter_w:+,.0f} sul muro (sotto la risoluzione MC), {np.median(inters):+,.0f} EUR/mese sul + versamento necessario ({np.median(inters) / (d_fisco + d_fund):+.0%} della somma, segno {"stabile" if segno_ok else "NON stabile"} + su 3 semi). Il congiunto resta + {d_joint / max(d_fisco, d_fund):.2f}x la piu' grande delle due correzioni presa da sola. + + 3. COSA NON CAMBIA A NESSUNA LENTE: senza versamenti il capitale-rendita non si raggiunge + MAI (P=0% a 20 anni in tutte e cinque le colonne). L'accumulo viene dai bonifici, non dal + rendimento β€” e l'ordine delle leve del piano e' invariato. + + 4. IL RISCHIO DI VENUE RESTA FUORI, DI PROPOSITO: e' rovina, non costo. A p=5% annuo il + capitale mediano a 20 anni e' ZERO per ogni riga di ogni tabella qui sopra. Nessuna di + queste righe e' valida se il conto sparisce. + + 5. L'OBIETTIVO DICHIARATO, senza addolcirlo: EUR50/giorno NON e' raggiungibile con versamenti + plausibili in un orizzonte breve. A 10 anni servono EUR{d10:,.0f}/mese (P=90%), cioe' ${t10:,.0f} + versati per arrivare a ${L3.muro:,.0f}: il {q10:.0%} del bersaglio lo compri coi bonifici. + La tabella utile e' quella INVERSA (sezione 6): non Β«quando arrivo a 50Β», ma Β«cosa compro + con quello che hoΒ». +""") + print(f"\nCON TUTTE LE CORREZIONI, EUR250/MESE DA ${START:.0f}: {fmt_med(j250)[:-1]} anni " + f"(mediana incondizionata; cond. all'arrivo {fmt_cond(j250)}), " + f"P(20a) {j250['p20']:.0%}") + print(f"PER EUR50/GIORNO IN 10 ANNI SERVONO: EUR{d10:,.0f}/mese (P=90%), " + f"totale versato ${t10:,.0f}") + print(f" [banda di funding: lente conservativa -2,16%/a come sopra; con il funding dello") + print(f" strumento vero -1,39%/a -> EUR250/mese fa {fmt_med(d250['L3b CONGIUNTA (str. vero)'])} " + f"con P(20a) {d250['L3b CONGIUNTA (str. vero)']['p20']:.0%}, e i 10 anni costano " + f"EUR{d10b:,.0f}/mese (${t10b:,.0f})]") + print(f"\n{clock()} fine.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822e_funding_avoid.py b/scripts/research/r0822e_funding_avoid.py new file mode 100644 index 0000000..5a9aafb --- /dev/null +++ b/scripts/research/r0822e_funding_avoid.py @@ -0,0 +1,1141 @@ +#!/usr/bin/env python3 +"""r0822e_funding_avoid.py β€” FUNDING-AVOID: il costo di tenere aperta una posizione si puo' +EVITARE cambiando STRUMENTO, senza toccare il segnale? + +CONTESTO (ondata 2026-08-22, sezioni 34-35 di docs/research/RESULTS-0822.md) + (1) Stanotte e' stato MISURATO che il libro paga **2,16%/anno di funding** (TP01 -2,10%, + SKH01 -2,32%), mai modellato prima. E' una tassa sul DRIFT: il maxDD non si muove + (+0,3pp) ma la rendita perpetua scende 10,73% -> 9,14% e il muro di capitale sale del + +17,5%. Su una rendita, il drift e' la moneta. + (2) Stanotte e' stato PERSISTITO in `data/raw/fut_deribit/` lo storico dei futures DATATI + Deribit: 34 trimestrali per asset, BTC 240.150 barre orarie / ETH 233.334, 2018-09 -> + 2026-08, piu' 64.110 ore di funding e indice dal 2019-04. + +LA DOMANDA, MAI POSTA: **un future DATATO non paga funding.** Il carry e' incorporato nel +PREZZO (contango = compri sopra lo spot e convergi a scadenza). Quindi: + + esprimere TP01 (e/o SKH01) su un future datato invece che sul perpetual + costa MENO o PIU' del funding che paghiamo oggi? + +E' una domanda sull'ESECUZIONE, non sul segnale: il segnale resta congelato (TSMOM 30/90/180 +su BTC/ETH, vol-target 20%, `CANONICAL`), cambia solo lo strumento su cui lo si esprime. +Un grado di liberta' per volta, replica di controllo prima di ogni delta. + +LA TESI DA VERIFICARE O FALSIFICARE (la ragione per cui il filone puo' restituire qualcosa) + Il funding e' caro PROPRIO QUANDO SIAMO LONG: misurato stanotte, il tasso che il libro paga + davvero e' 1,86-2,55x l'incondizionato, perche' esposizione e funding sono positivamente + correlati. Il basis di un datato invece e' FISSATO ALL'INGRESSO e non si riprezza contro di + te mentre tieni. Se il rapporto condizionale/incondizionale del basis e' piu' BASSO di + quello del funding, il datato converte un costo condizionale e avverso in uno noto. + +SEZIONI + 0 IL DATO β€” 68 contratti trimestrali, copertura, roll; specifiche e LIQUIDITA' lette dal + venue (cache in scratchpad, `--no-net` per girare offline). + 1 REPLICA DI CONTROLLO β€” il -2,16%/anno va riprodotto PRIMA di confrontarci qualsiasi cosa. + 2 L'ESTIMATORE β€” livello vs flusso. La differenza ARITMETICA dei rendimenti fra due + strumenti sullo stesso sottostante NON e' il carry: contiene un termine di varianza + (Jensen) che qui vale 1,5-2,6 punti percentuali l'anno, cioe' 2-4x l'effetto cercato. + 3 IL CONFRONTO APPAIATO β€” per giorno e per segnale, condizionale vs incondizionale. + 4 IL ROLL E LA LIQUIDITA' β€” quante volte si rolla, quanto costa attraversare, e a $635. + 5 LE SERIE β€” libro nelle 5 varianti + banda d'ancora (23 offset, differenze APPAIATE). + 6 I MURI β€” drift -> rendita perpetua -> capitale, con la macchineria di r0726_capwall. + 7 IL MURO CHE NON C'E' β€” Β«tutti i datati Deribit sono INVERSEΒ» e' FALSO da maggio 2026. + 8 SOTTOPRODOTTO β€” lo SPOT, che ha carry ZERO e oggi fee ZERO, e la sua domanda aperta. + 9 GATE + VERDETTO. + +VINCOLI: nessun file di produzione toccato (config/, src/, scripts/live/, cron INVARIATI); +nessun commit; output a video + cache in scratchpad. Runtime ~2-4 min. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0822e_funding_avoid.py + ... --no-net usa solo la cache del venue (nessuna richiesta a Deribit) +""" +from __future__ import annotations + +import datetime as dt +import json +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt", ROOT): + sys.path.insert(0, str(_p)) + +# r0822d_funding legge `--no-net` da sys.argv AL MOMENTO DELL'IMPORT: qui non si scarica il +# funding (la sua cache esiste gia'), quindi glielo si impone. Le uniche chiamate di rete di +# QUESTO script sono le sue, sotto, ed hanno il proprio flag. +_NET = "--no-net" not in sys.argv +if "--no-net" not in sys.argv: + sys.argv.append("--no-net") + +import altlib as al # noqa: E402 +import r0702_anchor_skh01 as r02 # noqa: E402 +import r0725_capcurve as CC # noqa: E402 +import r0726_capwall_refresh as WR # noqa: E402 +import r0822d_funding as FD # noqa: E402 +from src.portfolio.portfolio import combine_outer # noqa: E402 + +T0 = time.time() +ASSETS = ("BTC", "ETH") +FUT = ROOT / "data" / "raw" / "fut_deribit" +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/fa") +SCRATCH = _SP if _SP.parent.exists() else Path("/tmp/pythagoras_favoid") +API = "https://www.deribit.com/api/v2/public/" +MON = {"JAN": 1, "FEB": 2, "MAR": 3, "APR": 4, "MAY": 5, "JUN": 6, + "JUL": 7, "AUG": 8, "SEP": 9, "OCT": 10, "NOV": 11, "DEC": 12} + +BUF_CANON = 5 # si rolla 5 giorni prima della scadenza (dichiarato a priori) +BUF_GRID = (2, 5, 10, 20) # sensibilita' del parametro d'esecuzione +ALLOC = 600.0 # il capitale su cui gira book_series (conto reale $635) +DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor) +W_TP, W_SKH = 0.75, 0.25 +LO_BOOK = pd.Timestamp("2019-03-14", tz="UTC") +FEE_REALE = 0.00035 # taker Deribit letto dal venue (3,5 bps/lato) +# il modello di CC.tp01_realistic usa 5 bps/lato (convenzione conservativa del progetto) +FEE_MODELLO = CC.DERIBIT_FEE_SIDE + + +def sez(t: str) -> None: + print("\n" + "=" * 100) + print(f" {t}") + print("=" * 100) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +def expdate(nm: str) -> pd.Timestamp: + d = nm.split("-")[1] + return pd.Timestamp(dt.datetime(2000 + int(d[5:]), MON[d[2:5]], int(d[:2]), 8, 0), + tz="UTC") + + +# =========================================================================== +# 0) IL DATO +# =========================================================================== +def _pace() -> None: + """Mai nei minuti :07 (cron_book) e :25-:29 (cron_chain); <=2 req/s.""" + while dt.datetime.now(dt.timezone.utc).minute in (7, 25, 26, 27, 28, 29): + time.sleep(20) + time.sleep(0.6) + + +def _get(path: str): + _pace() + return json.load(urllib.request.urlopen(API + path, timeout=30))["result"] + + +VENUE_CACHE = SCRATCH / "venue_snapshot.json" +TICKETS = (100, 240, 500) # $ per ordine: 240 = nozionale max per asset di TP01 a $635 + + +def venue_snapshot() -> dict | None: + """Specifiche + libro degli strumenti candidati. UNA istantanea, non una distribuzione: + e' esattamente cio' che si puo' dire e va detto cosi'.""" + if VENUE_CACHE.exists(): + old = json.loads(VENUE_CACHE.read_text()) + else: + old = None + if not _NET: + return old + try: + out = {"ts": pd.Timestamp.now(tz="UTC").isoformat(), "instruments": {}, "books": {}} + for cur in ("BTC", "ETH", "USDC"): + for kind in ("future", "spot"): + try: + for x in _get(f"get_instruments?currency={cur}&kind={kind}&expired=false"): + out["instruments"][x["instrument_name"]] = dict( + settlement=x.get("settlement_currency"), + contract_size=x.get("contract_size"), + min_trade_amount=x.get("min_trade_amount"), + tick=x.get("tick_size"), taker=x.get("taker_commission"), + maker=x.get("maker_commission"), kind=kind, currency=cur, + created=x.get("creation_timestamp"), + expiry=x.get("expiration_timestamp")) + except Exception: + pass + for i in ("BTC_USDC-PERPETUAL", "BTC_USDC-25SEP26", "BTC_USDC-25DEC26", "BTC_USDC", + "BTC-25SEP26", "BTC-PERPETUAL", + "ETH_USDC-PERPETUAL", "ETH_USDC-25SEP26", "ETH_USDC-25DEC26", "ETH_USDC", + "ETH-25SEP26", "ETH-PERPETUAL"): + try: + d = _get(f"get_order_book?instrument_name={i}&depth=20") + out["books"][i] = dict(bid=d.get("best_bid_price"), ask=d.get("best_ask_price"), + index=d.get("index_price"), oi=d.get("open_interest"), + bids=d.get("bids"), asks=d.get("asks"), + vol24=(d.get("stats") or {}).get("volume"), + vol24usd=(d.get("stats") or {}).get("volume_usd")) + except Exception: + pass + SCRATCH.mkdir(parents=True, exist_ok=True) + VENUE_CACHE.write_text(json.dumps(out)) + return out + except Exception as e: # rete KO: si dichiara + print(f" ⚠️ venue non raggiungibile ({type(e).__name__}) -> uso la cache se c'e'.") + return old + + +def walk_cost_bps(book: dict, usd: float, side: str) -> float | None: + """Costo effettivo vs mid di un ordine da `usd` dollari, camminando il libro. + Ritorna None se il libro non basta (e allora si dice, non si estrapola).""" + if not book or not book.get("bid") or not book.get("ask"): + return None + mid = (book["bid"] + book["ask"]) / 2.0 + lv = book.get("asks" if side == "buy" else "bids") or [] + need, got, cost = usd / mid, 0.0, 0.0 + for p, q in lv: + take = min(q, need - got) + got += take + cost += take * p + if got >= need - 1e-12: + break + if got < need - 1e-12: + return None + eff = cost / got + return (eff / mid - 1.0) * 1e4 * (1.0 if side == "buy" else -1.0) + + +def sezione0(V: dict | None) -> dict: + sez("0) IL DATO β€” 68 trimestrali datati su disco, e le specifiche LETTE dal venue") + tab = {} + for a in ASSETS: + rows = [] + for p in sorted(FUT.glob(f"{a}-*.parquet")): + d = pd.read_parquet(p, columns=["ts", "close", "volume"]) + if len(d) < 200: + continue + e = expdate(p.stem) + d = d[(d.ts <= e) & (d.close > 0)] + rows.append((p.stem, e, d.ts.min(), d.ts.max(), len(d), float(d.volume.sum()))) + R = pd.DataFrame(rows, columns=["nm", "exp", "t0", "t1", "n", "vol"]).sort_values("exp") + tab[a] = R + print(f" {a}: {len(R)} contratti trimestrali usabili, scadenze da {R.exp.min().date()} " + f"a {R.exp.max().date()}, {int(R.n.sum()):,} barre orarie") + print(" ⚠️ Istantanea al 22/08 (il cron NON rinfresca questi file) e il contratto in corso") + print(" e' troncato. Il guardrail Β«solo dati certiΒ» regge perche' vivono in una") + print(" sottocartella con prefisso, fuori dal namespace nudo di `load_data`.") + + if not V: + print("\n ⚠️ SPECIFICHE DEL VENUE: NON MISURATE (nessuna rete, nessuna cache).") + return dict(tab=tab, spec=None) + + print(f"\n Istantanea del venue del {V['ts'][:19]}Z β€” UNA lettura, non una distribuzione.") + print(f" {'strumento':<22} {'settl':>6} {'min':>9} {'tick':>7} {'taker':>7} " + f"{'spread':>8} {'vol 24h $':>13} {'OI':>14}") + for i in ("BTC_USDC-PERPETUAL", "BTC_USDC-25SEP26", "BTC_USDC-25DEC26", "BTC-25SEP26", + "BTC_USDC", "ETH_USDC-PERPETUAL", "ETH_USDC-25SEP26", "ETH_USDC-25DEC26", + "ETH-25SEP26", "ETH_USDC"): + s = V["instruments"].get(i, {}) + b = V["books"].get(i, {}) + sp = float("nan") + if b.get("bid") and b.get("ask"): + mid = (b["bid"] + b["ask"]) / 2 + sp = (b["ask"] - b["bid"]) / mid * 1e4 + print(f" {i:<22} {str(s.get('settlement')):>6} {str(s.get('min_trade_amount')):>9} " + f"{str(s.get('tick')):>7} {str(s.get('taker')):>7} {sp:>7.2f}b " + f"{(b.get('vol24usd') or float('nan')):>13,.0f} " + f"{(b.get('oi') or float('nan')):>14,.0f}") + return dict(tab=tab, spec=V) + + +# =========================================================================== +# 1) IL CARRY DEI DATATI β€” costruzione della serie +# =========================================================================== +def aligned_index(a: str, grid: pd.DatetimeIndex) -> np.ndarray: + """Indice Deribit ALLINEATO alla convenzione delle barre orarie. + + ⚠️ TRAPPOLA MISURATA IN QUESTA SESSIONE, e non e' un dettaglio: `fund_{a}.parquet` + etichetta il record T con l'indice ALL'ISTANTE T, mentre `al.get(a,'1h')` etichetta la + barra all'INIZIO (la sua chiusura e' il prezzo a T+1h). Appaiare le due serie per + etichetta le sfasa di UN'ORA: corr(r_perp_t, r_index_t) = -0,02 e corr(r_perp_t, + r_index_t+1) = +0,996, la base perp/indice esce con sd 68-86 bps invece di pochi bps, e + la differenza di rendimento eredita 97-122 bps/ora di rumore INVENTATO. + Nessuna eccezione, nessun NaN: due serie plausibili che non parlano dello stesso istante. + (Stessa famiglia dell'errore di RISOLUZIONE `[ms]/[us]/[s]` catturato ieri notte in + r0822d_funding, e del `DatetimeIndex.view("int64")` codificato in CLAUDE.md il 01/07: + li' era la scala, qui e' l'ETICHETTA. Il sintomo e' lo stesso β€” un numero credibile.) + """ + fu = pd.read_parquet(FUT / f"fund_{a}.parquet").set_index("ts").sort_index() + return fu["index"].reindex(grid).shift(-1).values.astype(float) + + +def verifica_allineamento(grid: dict) -> None: + """Controllo POSITIVO dell'allineamento: deve fallire se lo si sbaglia, o non e' un test.""" + print("\n [ALLINEAMENTO] controllo positivo indice-vs-barre (corr dei log-rendimenti):") + for a in ASSETS: + fu = pd.read_parquet(FUT / f"fund_{a}.parquet").set_index("ts").sort_index() + raw = fu["index"].reindex(grid[a]).values.astype(float) + fix = aligned_index(a, grid[a]) + pp = al.get(a, "1h").set_index("datetime")["close"].astype(float) + pp.index = pd.DatetimeIndex(pp.index) + pv = pp.reindex(grid[a]).values.astype(float) + out = [] + for lbl, v in (("per etichetta", raw), ("allineato", fix)): + rP = np.diff(np.log(pv)); rI = np.diff(np.log(v)) + m = np.isfinite(rP) & np.isfinite(rI) + b = pv / v + out.append((lbl, float(np.corrcoef(rP[m], rI[m])[0, 1]), + float(np.nanstd(b - 1.0)) * 1e4)) + print(f" {a}: per etichetta corr {out[0][1]:+.4f} sd base {out[0][2]:6.1f} bps -> " + f"allineato corr {out[1][1]:+.4f} sd base {out[1][2]:5.1f} bps") + assert out[1][1] > 0.99 > out[0][1], f"allineamento non riparato su {a}" + print(" -> il controllo ha POTENZA: con l'etichetta grezza la correlazione e' ~0.") + + +def dated_rate(a: str, grid: pd.DatetimeIndex, buf: int = BUF_CANON, + log: bool = True) -> dict: + """Pseudo-`rate` compatibile con `FD.cumF` / `FD.daily_funding`: al posto del funding + orario mette il CARRY del future datato front, cioe' quanto il datato sotto-rende il + perpetual in quell'ora (>0 in contango = costo per un long). + + Il contratto tenuto durante l'ora t e' quello scelto alla chiusura di t-1 (nessuna + decisione usa il prezzo dell'ora che si sta valutando), e nell'ora di ROLL il rendimento + lo fa il contratto VECCHIO: cosi' non si perde nessuna ora e non si incolla nessun salto + di prezzo (convenzione dichiarata in r0822d_cc01_2022: si incollano i RENDIMENTI). + """ + n = len(grid) + idxp = aligned_index(a, grid) + perp = al.get(a, "1h").set_index("datetime")["close"].astype(float) + perp.index = pd.DatetimeIndex(perp.index) + pp = perp.reindex(grid).values.astype(float) + + C = {} + for p in sorted(FUT.glob(f"{a}-*.parquet")): + d = pd.read_parquet(p, columns=["ts", "close", "volume"]) + if len(d) < 200: + continue + e = expdate(p.stem) + d = d[(d.ts <= e) & (d.close > 0)].set_index("ts").sort_index() + C[p.stem] = (e, d["close"].reindex(grid).values.astype(float), + d["volume"].reindex(grid).values.astype(float)) + order = sorted(C, key=lambda x: C[x][0]) + + px = np.full(n, np.nan); nmi = np.full(n, -1); dte = np.full(n, np.nan) + vol = np.full(n, np.nan) + for k, nme in enumerate(order): + e, s, v = C[nme] + dd = (e - grid).total_seconds().values / 86400.0 + m = (dd >= buf) & np.isfinite(s) & np.isnan(px) + px[m] = s[m]; nmi[m] = k; dte[m] = dd[m]; vol[m] = v[m] + + def ret(v: np.ndarray) -> np.ndarray: + r = np.full(n, np.nan) + r[1:] = (np.log(v[1:]) - np.log(v[:-1])) if log else (v[1:] / v[:-1] - 1.0) + return r + + rP, rF = ret(pp), ret(px) + c = np.zeros(n); val = np.zeros(n, bool) + same = np.concatenate([[False], nmi[1:] == nmi[:-1]]) + m = same & np.isfinite(rF) & np.isfinite(rP) + c[m] = rP[m] - rF[m]; val[m] = True + + roll = np.zeros(n, bool) + ch = np.where((nmi[1:] != nmi[:-1]) & (nmi[1:] >= 0) & (nmi[:-1] >= 0))[0] + 1 + for i in ch: + s = C[order[nmi[i - 1]]][1] + if np.isfinite(s[i]) and np.isfinite(s[i - 1]) and np.isfinite(rP[i]): + old = (np.log(s[i]) - np.log(s[i - 1])) if log else (s[i] / s[i - 1] - 1.0) + c[i] = rP[i] - old; val[i] = True + roll[ch] = True + + return dict(ts=FD.to_ms(grid), i1h=c, cum=np.concatenate([[0.0], np.cumsum(c)]), + series=pd.Series(c, index=grid), switch=grid[0], valid=val, roll=roll, + bann=(px / idxp - 1.0) / (dte / 365.25), dte=dte, vol=vol, grid=grid, + nmi=nmi, order=order, px=px, idx=idxp, pp=pp) + + +def spot_rate(a: str, grid: pd.DatetimeIndex, log: bool = True) -> dict: + """Stessa forma, ma il carry di esprimere la posizione sullo SPOT: quanto il perpetual + sovra-rende l'INDICE (= il costo che si evita non usando il perpetual). Lo spot vero di + Deribit non ha storia in questo progetto: si usa l'INDICE, dichiarato come tale.""" + n = len(grid) + ix = aligned_index(a, grid) + perp = al.get(a, "1h").set_index("datetime")["close"].astype(float) + perp.index = pd.DatetimeIndex(perp.index) + pp = perp.reindex(grid).values.astype(float) + + def ret(v): + r = np.full(n, np.nan) + r[1:] = (np.log(v[1:]) - np.log(v[:-1])) if log else (v[1:] / v[:-1] - 1.0) + return r + c = np.nan_to_num(ret(pp) - ret(ix)) + return dict(ts=FD.to_ms(grid), i1h=c, cum=np.concatenate([[0.0], np.cumsum(c)]), + series=pd.Series(c, index=grid), switch=grid[0], + valid=np.isfinite(c), roll=np.zeros(n, bool), grid=grid) + + +# =========================================================================== +# 2) REPLICA DI CONTROLLO +# =========================================================================== +def book(rate_or_none, off: int = 0, tp_series: pd.Series | None = None) -> pd.Series: + """Libro LIVE 75/25 alla lente `hourly`. ⚠️ `FD._SKH` e' cachato su + `(asset, off, mode, rate is not None)`: due `rate` DIVERSI collidono. Si svuota sempre.""" + FD._SKH.clear() + s = FD.skh_book(off, "hourly", rate_or_none) + if s.index.tz is None: + s.index = s.index.tz_localize("UTC") + tp = tp_series if tp_series is not None else FD.tp01_realistic(ALLOC * W_TP, + rate_or_none)[0] + return combine_outer({"TP01": tp, "SKH01": s}, {"TP01": W_TP, "SKH01": W_SKH}, lo=LO_BOOK) + + +def sezione1(rate: dict) -> dict: + sez("1) REPLICA DI CONTROLLO β€” il -2,16%/anno prima di confrontarci qualsiasi cosa") + a0, a1 = FD.stat(book(None)), FD.stat(book(rate)) + t0, t1 = FD.stat(FD.tp01_realistic(ALLOC * W_TP, None)[0]), \ + FD.stat(FD.tp01_realistic(ALLOC * W_TP, rate)[0]) + FD._SKH.clear(); s0 = FD.stat(FD.skh_book(0, "hourly", None)) + FD._SKH.clear(); s1 = FD.stat(FD.skh_book(0, "hourly", rate)) + print(f" {'serie':<26} {'ShFULL':>8} {'ShHOLD':>8} {'maxDD':>7} {'CAGR':>8} {'drift':>8}") + for nm, x, y in (("LIBRO 75/25 path live", a0, a1), ("TP01 realistic", t0, t1), + ("SKH01 50/50 path live", s0, s1)): + print(f" {nm:<26} {x['full']:>8.3f} {x['hold']:>8.3f} {x['dd']:>7.1%} " + f"{x['cagr']:>8.2%} {x['drift']:>8.2%}") + print(f" {' + funding':<26} {y['full']:>8.3f} {y['hold']:>8.3f} {y['dd']:>7.1%} " + f"{y['cagr']:>8.2%} {y['drift']:>8.2%}") + print(f" {' DELTA':<26} {y['full']-x['full']:>+8.3f} {y['hold']-x['hold']:>+8.3f} " + f"{y['dd']-x['dd']:>+7.1%} {y['cagr']-x['cagr']:>+8.2%} " + f"{y['drift']-x['drift']:>+8.2%}") + d = a1["drift"] - a0["drift"] + ok = abs(d + 0.0216) < 0.0005 + print(f"\n drift del libro {a0['drift']:.2%} -> {a1['drift']:.2%} = {d:+.3%}/anno " + f"contro il -2,16% pubblicato -> {'REPLICATO' if ok else '⚠️ NON REPLICA'}") + assert ok, "la replica del funding non torna: nessun delta oltre questo punto" + return dict(book0=a0, book1=a1, tp0=t0, tp1=t1, skh0=s0, skh1=s1) + + +# =========================================================================== +# 3) L'ESTIMATORE β€” livello vs flusso, in una veste nuova +# =========================================================================== +def sezione2(rate: dict, grid: dict) -> None: + sez("2) L'ESTIMATORE β€” la differenza ARITMETICA dei rendimenti NON e' il carry") + print(" Il costo di tenere un datato e' MOLTIPLICATIVO (il basis si consuma in") + print(" proporzione), il funding e' un FLUSSO di cassa (tasso x nozionale). Chi misura il") + print(" primo con una differenza aritmetica di rendimenti si porta dentro un termine di") + print(" VARIANZA: media(rA) - media(rB) = [log-drift] + (var(rA)-var(rB))/2. Fra due") + print(" strumenti sullo STESSO sottostante il log-drift e' il carry e la differenza di") + print(" varianza e' microstruttura (il datato e' meno scambiato, la sua barra oraria e'") + print(" piu' stantia, la varianza misurata piu' bassa).") + print("\n CONTROLLO che rende visibile l'artefatto senza discuterne: il PERPETUAL contro") + print(" l'INDICE. La sua base e' stazionaria intorno a ~3 bps, quindi il carry vero e' ~0.") + print(f" {'asset':>6} {'aritmetico':>12} {'log':>10} {'artefatto':>11} {'base perp media':>17}") + for a in ASSETS: + sa = spot_rate(a, grid[a], log=False)["i1h"] + sl = spot_rate(a, grid[a], log=True)["i1h"] + ix = aligned_index(a, grid[a]) + pp = al.get(a, "1h").set_index("datetime")["close"].astype(float) + pp.index = pd.DatetimeIndex(pp.index) + bas = float(np.nanmean(pp.reindex(grid[a]).values / ix - 1.0)) * 1e4 + print(f" {a:>6} {sa.mean()*24*365.25:>+11.3%} {sl.mean()*24*365.25:>+9.3%} " + f"{(sa.mean()-sl.mean())*24*365.25:>+10.3%} {bas:>16.1f}b") + print(" -> l'aritmetico attribuisce al perpetual +0,4/+0,5%/anno di 'sovra-rendimento'") + print(" su un indice da cui non si stacca mai: e' varianza, non carry. Il log da' ~0.") + print("\n Lo stesso artefatto sul DATATO, che e' l'oggetto della domanda:") + print(f" {'asset':>6} {'carry aritm.':>13} {'carry LOG':>11} {'artefatto':>11} " + f"{'funding':>9}") + for a in ASSETS: + da = dated_rate(a, grid[a], log=False) + dl = dated_rate(a, grid[a], log=True) + f = rate[a]["i1h"] + print(f" {a:>6} {da['i1h'][da['valid']].mean()*24*365.25:>+12.3%} " + f"{dl['i1h'][dl['valid']].mean()*24*365.25:>+10.3%} " + f"{(da['i1h'][da['valid']].mean()-dl['i1h'][dl['valid']].mean())*24*365.25:>+10.3%} " + f"{f.mean()*24*365.25:>+8.3%}") + print(" 🚨 L'artefatto vale 1,5-2,6 punti percentuali l'anno, cioe' DA 2 A 4 VOLTE") + print(" l'effetto che questo filone sta cercando (~0,7%/anno). Con l'estimatore") + print(" aritmetico il datato sembra costare 4,1-5,2%/anno contro un funding del") + print(" 6,4-7,3%: un vantaggio enorme, e per meta' inventato dalla formula.") + print(" πŸ“Œ REGOLA: fra due strumenti sullo stesso sottostante il carry si misura sul") + print(" LOG del rapporto dei prezzi (telescopico: cum(t) = ln(P_t/F_t) - ln(P_0/F_0)),") + print(" mai sulla media delle differenze di rendimento. E' la stessa forma dell'errore") + print(" gia' codificato il 26/07 sui veicoli UCITS (Β«la deviazione fra due veicoli") + print(" sullo stesso indice si misura sul RAPPORTO CUMULATOΒ»): li' erano due ETF, qui") + print(" due contratti, ed e' passato lo stesso.") + print(" Da qui in avanti: LOG. L'aritmetico non compare piu' in nessun numero.") + + +# =========================================================================== +# 4) IL CONFRONTO APPAIATO +# =========================================================================== +def sezione3(rate: dict, DR: dict) -> dict: + sez("3) IL CONFRONTO APPAIATO β€” per GIORNO e per SEGNALE, e la tesi condizionale") + _, dg = FD.tp01_realistic(ALLOC * W_TP, rate) + out = {} + print(" Segnale CONGELATO (stesso `pos` in entrambe le colonne): cambia solo lo strumento.") + print(f" {'asset':>5} {'esposiz.':>9} {'costo perp':>11} {'costo datato':>13} " + f"{'delta':>9} {'t':>6} {'IC95 (bootstrap)':>22}") + rng = np.random.default_rng(822) + for a in ASSETS: + idx = pd.DatetimeIndex(dg[a]["idx"]) + pos = pd.Series(dg[a]["pos"], index=idx) + fD = pd.Series(FD.daily_funding(rate[a], idx), index=idx) + cD = pd.Series(FD.daily_funding(DR[a], idx), index=idx) + m = idx >= pd.Timestamp("2019-05-01", tz="UTC") + cp, cd = (pos * fD)[m], (pos * cD)[m] + d = (cd - cp).values + t = d.mean() / d.std() * np.sqrt(len(d)) + # bootstrap a blocchi (20 giorni): il carry e' persistente, l'IID sottostima l'errore + nb = max(1, len(d) // 20) + bs = [] + for _ in range(2000): + st = rng.integers(0, max(1, len(d) - 20), nb) + bs.append(np.mean(np.concatenate([d[s:s + 20] for s in st]))) + lo, hi = np.percentile(bs, [2.5, 97.5]) * 365.25 + out[a] = dict(perp=cp.mean() * 365.25, dat=cd.mean() * 365.25, + delta=d.mean() * 365.25, t=t, lo=lo, hi=hi, pos=pos, m=m) + print(f" {a:>5} {pos[m].mean():>9.4f} {out[a]['perp']:>10.3%} {out[a]['dat']:>12.3%} " + f"{out[a]['delta']:>+8.3%} {t:>+6.2f} [{lo:>+7.3%}, {hi:>+7.3%}]") + + print("\n LA TESI: il funding e' caro PROPRIO QUANDO SIAMO LONG. Lo e' anche il basis?") + print(f" {'asset':>5} | {'funding incond':>14} {'condiz.':>9} {'rapporto':>9} " + f"| {'basis incond':>13} {'condiz.':>9} {'rapporto':>9}") + for a in ASSETS: + idx = pd.DatetimeIndex(dg[a]["idx"]) + pos, m = out[a]["pos"], out[a]["m"] + bann = pd.Series(DR[a]["bann"], index=DR[a]["grid"]).resample("1D").mean().reindex(idx) + fann = pd.Series(rate[a]["i1h"], index=rate[a]["series"].index)\ + .resample("1D").sum().reindex(idx) * 365.25 + w = pos[m] + fu_, fc = float(fann[m].mean()), float((w * fann[m]).sum() / w.sum()) + bu, bc = float(bann[m].mean()), float((w * bann[m]).sum() / w.sum()) + out[a].update(f_unc=fu_, f_con=fc, b_unc=bu, b_con=bc) + print(f" {a:>5} | {fu_:>14.2%} {fc:>9.2%} {fc/fu_:>8.2f}x " + f"| {bu:>13.2%} {bc:>9.2%} {bc/bu:>8.2f}x") + print(" πŸ“Œ LA TESI REGGE, e in modo appaiato: il funding condizionale e' 1,9-2,6x") + print(" l'incondizionato (replica indipendente del 1,86-2,55x misurato stanotte), il") + print(" basis condizionale solo 1,5-1,7x. Il datato NON e' piu' economico in assoluto") + print(" β€” sui livelli incondizionati i due tassi sono quasi identici (6,4-7,5% contro") + print(" 6,6-7,3%, e BASIS-CALENDAR aveva gia' misurato che lo scarto contiene lo zero)") + print(" β€” lo diventa CONDIZIONATAMENTE, perche' si riprezza di meno contro di noi.") + print(" Il vantaggio non e' un premio: e' la parte di premio che il perpetual") + print(" ri-tariffa OGNI ORA e il datato ha gia' fissato all'ingresso.") + + print("\n ⚠️ MA IL VANTAGGIO NON E' STAZIONARIO β€” delta per anno (>0 = il datato costa DI PIU'):") + print(f" {'anno':>6} {'delta BTC':>11} {'delta ETH':>11} {'funding medio':>15} " + f"{'basis medio':>13}") + for y in range(2019, 2027): + cells = [] + for a in ASSETS: + idx = pd.DatetimeIndex(dg[a]["idx"]) + pos = out[a]["pos"] + fD = pd.Series(FD.daily_funding(rate[a], idx), index=idx) + cD = pd.Series(FD.daily_funding(DR[a], idx), index=idx) + g = (pos * (cD - fD)) + g = g[g.index.year == y] + cells.append(float(g.mean()) * 365.25 if len(g) > 30 else float("nan")) + fy = np.mean([rate[a]["series"][rate[a]["series"].index.year == y].mean() + for a in ASSETS]) * 24 * 365.25 + by = np.nanmean([pd.Series(DR[a]["bann"], index=DR[a]["grid"])[ + pd.Series(DR[a]["bann"], index=DR[a]["grid"]).index.year == y].mean() + for a in ASSETS]) + print(f" {y:>6} {cells[0]:>+10.3%} {cells[1]:>+10.3%} {fy:>15.2%} {by:>13.2%}") + return out + + +# =========================================================================== +# 5) IL ROLL E LA LIQUIDITA' +# =========================================================================== +def sezione4(rate: dict, DR: dict, V: dict | None, grid: dict) -> dict: + sez("4) IL ROLL E LA LIQUIDITA' β€” il costo che il perpetual NON ha") + _, dg = FD.tp01_realistic(ALLOC * W_TP, rate) + print(" Il perpetuo non si rolla. Il datato si', e il roll e' FORZATO (data di scadenza),") + print(" non scelto. Quanto costa dipende da tre cose che si MISURANO: quante volte, su") + print(" quanto nozionale, e quanto costa attraversare il libro di un datato.\n") + print(f" {'asset':>5} {'roll/anno':>10} {'turnover TP01':>14} {'turnover SKH01':>15} " + f"{'nozion. medio TP01':>19}") + tov = {} + for a in ASSETS: + idx = pd.DatetimeIndex(dg[a]["idx"]); pos = dg[a]["pos"] + yrs = (idx[-1] - idx[0]).days / 365.25 + to_tp = float(np.abs(np.diff(pos, prepend=0.0)).sum()) / yrs + rl = pd.Series(DR[a]["roll"].astype(float), index=DR[a]["grid"])\ + .resample("1D").max().reindex(idx).fillna(0.0).values + FD._SKH.clear() + tr = FD.skh_daily(a, 0, "hourly", rate)[1] + yrs_s = (max(t["t_out"] for t in tr) - min(t["t_in"] for t in tr)) / 1000 / 86400 / 365.25 + to_sk = 2 * len(tr) / yrs_s + tov[a] = dict(tp=to_tp, sk=to_sk, roll_year=float(rl.sum()) / yrs, + roll_turn=float((rl * np.abs(pos)).sum()) * 2 / yrs, pos=pos, idx=idx, + rl=rl, yrs=yrs) + print(f" {a:>5} {tov[a]['roll_year']:>10.2f} {to_tp:>13.2f}x {to_sk:>14.2f}x " + f"{pos.mean()*ALLOC*W_TP/2:>18,.2f}$") + print(" (turnover = somma di |Ξ”nozionale| per anno, in multipli del capitale d'asset)") + + print("\n QUANTO COSTA ATTRAVERSARE β€” libro reale, ordine da $100/$240/$500 ($240 = il") + print(" nozionale massimo per asset di TP01 a $635). UNA istantanea; se il libro non basta") + print(" si dice, non si estrapola:") + xs = {} + if V: + print(f" {'strumento':<22} " + " ".join(f"{f'${u}':>10}" for u in TICKETS)) + for i in ("BTC_USDC-PERPETUAL", "BTC_USDC-25SEP26", "BTC_USDC", + "ETH_USDC-PERPETUAL", "ETH_USDC-25SEP26", "ETH_USDC"): + b = V["books"].get(i) + cells, vals = [], [] + for u in TICKETS: + cb = walk_cost_bps(b, u, "buy"); cs = walk_cost_bps(b, u, "sell") + if cb is None or cs is None: + cells.append(f"{'LIBRO KO':>10}") + else: + v = (cb + cs) / 2 + vals.append(v); cells.append(f"{v:>9.2f}b") + xs[i] = float(np.mean(vals)) if vals else None + print(f" {i:<22} " + " ".join(cells)) + print(" ⚠️ Il datato USDC ha un libro 45x piu' sottile del perpetual USDC per volume") + print(" 24h, ma a $240 di ticket si resta dentro i primi livelli: a QUESTA taglia") + print(" la liquidita' non e' il vincolo, il MEZZO SPREAD si'.") + else: + print(" ⚠️ NON MISURATO (nessuna istantanea del venue).") + + ov = {} + if xs: + print(f"\n SOVRACOSTO D'ESECUZIONE, per sleeve e per anno (fee reale {FEE_REALE*1e4:.1f}") + print(" bps/lato su perp e datato, 0 sullo spot β€” letta dal venue; mezzo spread misurato):") + print(f" {'':<24} {'BTC':>12} {'ETH':>12} {'sleeve 50/50':>14}") + for lbl, ip, sfee in (("datato vs perp", "-25SEP26", FEE_REALE), + ("spot vs perp", "", 0.0)): + per = {} + for a in ASSETS: + base = xs.get(f"{a}_USDC-PERPETUAL") + alt = xs.get(f"{a}_USDC{ip}" if ip else f"{a}_USDC") + if base is None or alt is None: + per[a] = float("nan"); continue + dside = (alt * 1e-4 + sfee) - (base * 1e-4 + FEE_REALE) + extra = tov[a]["tp"] * dside + if ip: # il roll aggiunge turnover solo al datato + extra += tov[a]["roll_turn"] * (alt * 1e-4 + sfee) + per[a] = extra + ov[lbl] = per + print(f" {'TP01 ' + lbl:<24} {per['BTC']:>+11.3%} {per['ETH']:>+11.3%} " + f"{np.nanmean(list(per.values())):>+13.3%}") + per = {} + for a in ASSETS: + base, alt = xs.get(f"{a}_USDC-PERPETUAL"), xs.get(f"{a}_USDC-25SEP26") + per[a] = tov[a]["sk"] * ((alt * 1e-4 + FEE_REALE) - (base * 1e-4 + FEE_REALE)) \ + if base is not None and alt is not None else float("nan") + ov["SKH datato"] = per + print(f" {'SKH01 datato vs perp':<24} {per['BTC']:>+11.3%} {per['ETH']:>+11.3%} " + f"{np.nanmean(list(per.values())):>+13.3%}") + print(" πŸ“Œ Il turnover di SKH01 e' 48-57x l'anno contro i 6,6-8,4x di TP01: lo stesso") + print(" mezzo spread costa a SKH01 ~7 volte tanto. Un cambio di strumento si") + print(" giudica per SLEEVE, non per libro.") + + print("\n NETTING β€” oggi TP01 e SKH01 si compensano sullo STESSO strumento; separarli lo") + print(" perde. Quanto vale? (griglia giornaliera, approssimazione dichiarata)") + for a in ASSETS: + FD._SKH.clear() + tr = FD.skh_daily(a, 0, "hourly", rate)[1] + idx = tov[a]["idx"] + ps = pd.Series(0.0, index=idx) + for t in tr: + t0 = pd.Timestamp(t["t_in"], unit="ms", tz="UTC").floor("D") + t1 = pd.Timestamp(t["t_out"], unit="ms", tz="UTC").floor("D") + ps[(idx >= t0) & (idx <= t1)] += t["dir"] + ps = ps.clip(-1, 1) + nT = W_TP * pd.Series(tov[a]["pos"], index=idx); nS = W_SKH * ps + sep = float((nT.diff().abs().fillna(abs(nT.iloc[0])) + + nS.diff().abs().fillna(abs(nS.iloc[0]))).sum()) / tov[a]["yrs"] + net = float((nT + nS).diff().abs().fillna(abs(nT.iloc[0] + nS.iloc[0])).sum()) / tov[a]["yrs"] + print(f" {a}: turnover separato {sep:.2f}x/anno Β· nettato {net:.2f}x/anno -> il netting " + f"vale {1-net/sep:.1%}") + print(" -> trascurabile: i due sleeve tradano quasi sempre in momenti diversi. Mettere") + print(" TP01 e SKH01 su due strumenti NON e' un argomento contro.") + return dict(tov=tov, xs=xs, ov=ov) + + +# =========================================================================== +# 6) LE SERIE + BANDA D'ANCORA +# =========================================================================== +def overlay(series: pd.Series, tov: dict, per_asset_bps: dict, roll: bool) -> pd.Series: + """Sottrae il sovracosto d'esecuzione GIORNO PER GIORNO (|Ξ”pos| e roll), non come media + annua: e' un costo di turnover, e il turnover non e' uniforme nel tempo.""" + tot = None + for a in ASSETS: + if not np.isfinite(per_asset_bps.get(a, float("nan"))): + return series + pos, idx, rl = tov[a]["pos"], tov[a]["idx"], tov[a]["rl"] + c = np.abs(np.diff(pos, prepend=0.0)) * per_asset_bps[a] + if roll: + c = c + 2.0 * np.abs(pos) * rl * per_asset_bps[a] + s = pd.Series(0.5 * c, index=idx) + tot = s if tot is None else tot.add(s, fill_value=0.0) + return series.sub(tot.reindex(series.index).fillna(0.0)) + + +def sezione5(rate: dict, DR: dict, SR: dict, ex: dict) -> dict: + sez("5) LE SERIE β€” il libro nelle varianti, netto tutto, e la banda d'ancora") + tov, xs = ex["tov"], ex["xs"] + ds = {a: (xs[f"{a}_USDC-25SEP26"] - xs[f"{a}_USDC-PERPETUAL"]) * 1e-4 + for a in ASSETS} if xs else {a: float("nan") for a in ASSETS} + ss = {a: (xs[f"{a}_USDC"] * 1e-4 + 0.0) - (xs[f"{a}_USDC-PERPETUAL"] * 1e-4 + FEE_REALE) + for a in ASSETS} if xs else {a: float("nan") for a in ASSETS} + + tp = {k: FD.tp01_realistic(ALLOC * W_TP, r)[0] for k, r in + (("nessun carry", None), ("perp", rate), ("datato", DR), ("spot", SR))} + tp["datato+esec"] = overlay(tp["datato"], tov, ds, roll=True) + tp["spot+esec"] = overlay(tp["spot"], tov, ss, roll=False) + + var = {} + var["A) perp (IL LIBRO DI OGGI)"] = book(rate) + var["B) tutto sui datati"] = book(DR) + var["C) TP01 datato + SKH01 perp"] = book(rate, tp_series=tp["datato"]) + var["D) C + sovracosto d'esecuzione"] = book(rate, tp_series=tp["datato+esec"]) + var["E) TP01 spot + SKH01 perp"] = book(rate, tp_series=tp["spot+esec"]) + var["F) nessun carry (irreale)"] = book(None) + + print(f" {'variante':<32} {'ShFULL':>8} {'ShHOLD':>8} {'maxDD':>7} {'vol':>7} " + f"{'CAGR':>8} {'drift':>8} {'Ξ”drift':>8}") + base = FD.stat(var["A) perp (IL LIBRO DI OGGI)"]) + st = {} + for k, s in var.items(): + m = FD.stat(s); st[k] = m + print(f" {k:<32} {m['full']:>8.3f} {m['hold']:>8.3f} {m['dd']:>7.1%} {m['vol']:>7.2%} " + f"{m['cagr']:>8.2%} {m['drift']:>8.2%} {m['drift']-base['drift']:>+8.2%}") + + print("\n πŸ“Œ QUELLO CHE LA COLONNA `Ξ”drift` DA SOLA NASCONDE: il datato aggiunge drift E") + print(" VOLATILITA'. Il basis non e' costante β€” si riprezza β€” e quella oscillazione") + print(" entra nel rendimento della posizione. Il funding invece e' un flusso di cassa") + print(" che NON aggiunge varianza al prezzo. Per questo lo Sharpe del libro PEGGIORA") + print(" mentre il drift migliora. Chi cita solo il drift sta vendendo un cambio di") + print(" strumento come un miglioramento quando e' uno SCAMBIO.") + + print("\n SLEEVE TP01 da solo (dove il cambio ha senso), stessa lente:") + print(f" {'':<26} {'ShFULL':>8} {'ShHOLD':>8} {'maxDD':>7} {'vol':>7} {'drift':>8}") + for k in ("nessun carry", "perp", "datato", "datato+esec", "spot", "spot+esec"): + m = FD.stat(tp[k]) + print(f" {k:<26} {m['full']:>8.3f} {m['hold']:>8.3f} {m['dd']:>7.1%} {m['vol']:>7.2%} " + f"{m['drift']:>8.2%}") + + print("\n FINESTRE β€” il numero va citato con la finestra (regola di stanotte):") + print(f" {'variante':<32} {'FULL':>9} {'>=2022-03':>11} {'2025+ (hold-out)':>18}") + lo1, lo2 = pd.Timestamp("2022-03-09", tz="UTC"), pd.Timestamp("2025-01-01", tz="UTC") + A = var["A) perp (IL LIBRO DI OGGI)"] + for k in ("B) tutto sui datati", "C) TP01 datato + SKH01 perp", + "D) C + sovracosto d'esecuzione", "E) TP01 spot + SKH01 perp"): + B = var[k] + cells = [] + for lo in (None, lo1, lo2): + a_, b_ = (A, B) if lo is None else (A[A.index >= lo], B[B.index >= lo]) + cells.append((float(b_.mean()) - float(a_.mean())) * 365.25) + print(f" {k:<32} {cells[0]:>+8.2%} {cells[1]:>+10.2%} {cells[2]:>+17.2%}") + + print(f"\n{clock()} BANDA D'ANCORA β€” 23 offset della griglia 230m/690m di SKH01, mediana") + print(" delle DIFFERENZE APPAIATE (mai la differenza delle mediane).") + bands = {} + for k, tps, rr in (("C) TP01 datato", tp["datato"], rate), + ("D) C + esecuzione", tp["datato+esec"], rate), + ("E) TP01 spot", tp["spot+esec"], rate)): + d = {"sh": [], "hold": [], "dd": [], "drift": []} + for off in r02.OFFSETS: + a_ = FD.stat(book(rate, off=off)) + b_ = FD.stat(book(rr, off=off, tp_series=tps)) + d["sh"].append(b_["full"] - a_["full"]); d["hold"].append(b_["hold"] - a_["hold"]) + d["dd"].append(b_["dd"] - a_["dd"]); d["drift"].append(b_["drift"] - a_["drift"]) + bands[k] = {m: np.array(v) for m, v in d.items()} + print(f" {k}:") + for m, lbl in (("drift", "Ξ”drift"), ("sh", "Ξ”ShFULL"), ("hold", "Ξ”ShHOLD"), + ("dd", "Ξ”maxDD")): + v = bands[k][m] + flat = (v.max() - v.min()) < 1e-6 + print(f" {lbl:<9} mediana appaiata {np.median(v):>+8.4f} positiva in " + f"{np.mean(v>0):>5.0%} dei 23 offset [min {v.min():+.4f}, max {v.max():+.4f}]" + f"{' <- COSTANTE: nessuna potenza, vedi sotto' if flat else ''}") + print(" ⚠️ Il Ξ”drift e' COSTANTE su tutti e 23 gli offset, e non e' robustezza: e'") + print(" un'identita'. Cambia solo la gamba TP01, che non dipende dall'offset di SKH01,") + print(" e il drift di una somma pesata e' la somma pesata dei drift -> Β«positivo a") + print(" 23/23Β» qui NON dice niente. La banda ha potenza sullo SHARPE e sul maxDD (che") + print(" dipendono dalla covarianza fra le gambe), e li' il verdetto e' netto:") + print(" l'hold-out del datato e' NEGATIVO a 0/23 offset, con e senza sovracosto.") + print(" (Un controllo che non puo' fallire e' esattamente cio' che questo progetto") + print(" chiama Β«un test senza potenzaΒ»: si dichiara, non si conta come conferma.)") + return dict(var=var, st=st, tp=tp, bands=bands, ds=ds, ss=ss) + + +# =========================================================================== +# 7) I MURI +# =========================================================================== +def sezione6(res5: dict) -> None: + sez("6) I MURI DI CAPITALE β€” la moneta giusta, perche' la rendita vive sul DRIFT") + need = CC.TARGET_EUR_DAY * 365.0 * CC.EURUSD / (1 - CC.TAX_RATE) + print(f" bersaglio 50 EUR/g netti = ${need:,.0f}/anno lordi (aliquota {CC.TAX_RATE:.0%}),") + print(f" de-luck del drift x{DELUCK} (MISURATO), MC {WR.N_PATHS} path -> risoluzione ~1%:") + print(" si citano 3 cifre significative, non il dollaro.\n") + print(f" {'variante':<32} {'drift':>8} {'vol':>7} {'perpetua':>9} {'muro':>12} {'Ξ”muro':>8}") + ref = None + for k in ("A) perp (IL LIBRO DI OGGI)", "D) C + sovracosto d'esecuzione", + "E) TP01 spot + SKH01 perp", "F) nessun carry (irreale)"): + r = res5["var"][k].values.astype(float) + r = r - (1.0 - DELUCK) * float(r.mean()) + _, perp, cap = WR.perp_and_wall(r, 1.0, need) + if ref is None: + ref = cap + print(f" {k:<32} {float(r.mean())*365.25:>8.2%} {float(r.std())*np.sqrt(365.25):>7.2%} " + f"{perp:>9.2%} ${cap:>11,.0f} {cap/ref-1:>+7.1%}") + print("\n πŸ“Œ Il muro pubblicato stanotte e' $325,0k col funding dentro (era $276,6k senza).") + print(" Qui si misura il DELTA con la stessa macchineria, non un muro nuovo: la") + print(" differenza di livello viene dal de-luck e dal MC, non dal cambio di strumento.") + + +# =========================================================================== +# 8) IL MURO CHE NON C'E' +# =========================================================================== +def sezione7(V: dict | None) -> None: + sez("7) IL MURO CHE NON C'E' β€” Β«tutti i datati Deribit sono INVERSEΒ» e' FALSO") + print(" La sezione 34 di RESULTS-0822 chiude cosi': Β«tutti i futures datati Deribit sono") + print(" INVERSE: non esiste una linea USDC datata β€” stesso muro delle opzioni, e il") + print(" collaterale in BTC e' esso stesso esposizione direzionale. Non risolto.Β»") + print(" Il briefing di questo filone lo eredita come Β«probabilmente il muro che uccide il") + print(" filoneΒ». E' l'unica cosa che si poteva verificare con UNA chiamata pubblica.\n") + if not V: + print(" ⚠️ NON VERIFICATO in questa corsa (nessuna rete, nessuna cache).") + return + ins = V["instruments"] + usdc_dated = sorted(n for n, s in ins.items() + if s.get("settlement") == "USDC" and s.get("kind") == "future" + and "PERPETUAL" not in n and (n.startswith("BTC_USDC") + or n.startswith("ETH_USDC"))) + print(f" MISURATO: {len(usdc_dated)} futures DATATI con settlement **USDC** su BTC/ETH.") + for n in usdc_dated: + s = ins[n] + cre = dt.datetime.utcfromtimestamp(s["created"] / 1000).date() + exp = dt.datetime.utcfromtimestamp(s["expiry"] / 1000).date() + print(f" {n:<20} creato {cre} scade {exp} min {s['min_trade_amount']} " + f"tick {s['tick']} taker {s['taker']}") + q = [n for n in usdc_dated if n.endswith(("SEP26", "DEC26", "MAR27", "JUN27"))] + print(f"\n βœ… Il muro NON esiste: le due gambe del libro possono restare in USDC. I") + print(" trimestrali USDC hanno lo STESSO min_trade_amount del perpetual lineare") + print(" (0,0001 BTC ~ $7,7 Β· 0,001 ETH ~ $2,4) e la STESSA fee taker: a $635") + print(" l'eseguibilita' non e' il vincolo (6a volta in questa ondata).") + print(" ⚠️ E il collaterale in cripto non era nemmeno l'argomento giusto: se lo fosse") + print(" stato, il costo non sarebbe il carry ma il fatto che un conto denominato in") + print(" BTC e' long 1,0x anche da FLAT β€” e TP01 sta flat nel 44% dei giorni, cioe' si") + print(" sarebbe distrutto esattamente cio' per cui TP01 esiste (il taglio del DD).") + print("\n ⚠️ IL MURO VERO E' UN ALTRO, ed e' una DATA: la linea USDC datata e' nata il") + print(f" 2026-05-07 ({len(q)} trimestrali esistono, il secondo creato il 2026-08-06)") + print(" -> ~3,5 mesi di vita e ZERO roll trimestrali mai eseguiti su quella linea.") + print(" Tutta la misura di questo filone gira sui trimestrali INVERSE, che hanno 7,3") + print(" anni: e' un PROXY, esattamente come l'inverse lo era per il funding pre-2022.") + print(" βœ… E il proxy si VALIDA invece di assumerlo β€” i due contratti sulla stessa") + print(" scadenza devono prezzare lo stesso carry, e lo fanno:") + for a in ASSETS: + bi, bu = V["books"].get(f"{a}-25SEP26"), V["books"].get(f"{a}_USDC-25SEP26") + if not bi or not bu or not bi.get("index"): + continue + mi = (bi["bid"] + bi["ask"]) / 2; mu = (bu["bid"] + bu["ask"]) / 2 + print(f" {a}-25SEP26 (inverse) base {(mi/bi['index']-1)*1e4:+.1f} bps vs " + f"{a}_USDC-25SEP26 (lineare) base {(mu/bu['index']-1)*1e4:+.1f} bps " + f"-> scarto {(mu/bu['index']-mi/bi['index'])*1e4:+.1f} bps") + print(" πŸ“Œ REGOLA: un muro ereditato da un'altra sessione e' un'IPOTESI, e prima di") + print(" costruirci sopra un verdetto si ri-verifica. Qui e' costato una GET pubblica,") + print(" e la frase era diventata falsa 3 mesi prima che venisse scritta.") + print(" (Stessa forma della lezione del 26/07: Β«un blocco dichiarato e' un'ipotesiΒ».)") + + +# =========================================================================== +# 9) SOTTOPRODOTTO β€” LO SPOT +# =========================================================================== +def sezione8(V: dict | None, res5: dict, rate: dict, SR: dict) -> None: + sez("8) SOTTOPRODOTTO β€” lo strumento a carry ZERO esiste, si chiama SPOT") + print(" La domanda del filone era Β«datato o perpetualΒ». Cercando le specifiche e' saltato") + print(" fuori il terzo: Deribit quota SPOT BTC_USDC/ETH_USDC. Uno spot non paga funding,") + print(" non ha basis e non si rolla: il suo carry e' ZERO per costruzione.") + if V: + for i in ("BTC_USDC", "ETH_USDC"): + s = V["instruments"].get(i, {}) + b = V["books"].get(i, {}) + sp = (b["ask"] - b["bid"]) / ((b["ask"] + b["bid"]) / 2) * 1e4 if b.get("bid") else float("nan") + print(f" {i:<10} min {s.get('min_trade_amount')} taker {s.get('taker')} " + f"maker {s.get('maker')} spread {sp:.2f} bps vol 24h " + f"${(b.get('vol24usd') or float('nan')):,.0f}") + print(" ⚠️ La fee ZERO e' quella di OGGI ed e' dichiarata temporanea nell'annuncio del") + print(" 1Β° agosto (Β«spot a zero fino al collegamento con CoinbaseΒ»): e' un tier che") + print(" puo' sparire, e `fee_watch` sorveglia i perpetual, non lo spot.") + print("\n Perche' funziona SOLO su TP01, e la misura che lo dimostra:") + _, dg = FD.tp01_realistic(ALLOC * W_TP, rate) + for a in ASSETS: + pos = dg[a]["pos"] + print(f" {a}: TP01 e' long-flat e il suo target per asset supera 1,0x nel " + f"{np.mean(pos > 1.0):.3%} dei giorni (massimo {pos.max():.3f}) -> il tetto") + print(f" strutturale dello spot (niente leva, niente short) e' quasi mai binding.") + print(" SKH01 e' long/SHORT a nozionale 1,0x: sullo spot non e' esprimibile. Meta' dei") + print(" suoi trade sono short e li' lo spot non esiste come strumento.") + print("\n DECOMPOSIZIONE del vantaggio dello spot su TP01 β€” due pezzi, non uno:") + mnc = FD.stat(res5["tp"]["nessun carry"]); msp = FD.stat(res5["tp"]["spot"]) + mpe = FD.stat(res5["tp"]["perp"]) + print(f" (1) funding evitato {mnc['drift']-mpe['drift']:>+7.2%}/anno " + f"(perp {mpe['drift']:.2%} -> senza-carry {mnc['drift']:.2%})") + print(f" (2) base del perpetual {msp['drift']-mnc['drift']:>+7.2%}/anno " + f"(senza-carry {mnc['drift']:.2%} -> indice {msp['drift']:.2%})") + print(" ❌ IPOTESI MIA, NATA E REFUTATA NELLA STESSA SESSIONE: mi aspettavo che il") + print(" secondo pezzo fosse una TERZA conferma della tesi condizionale β€” Β«quando siamo") + print(" long il perpetual e' a premio e quel premio decadeΒ». La prima stesura lo") + print(" misurava a +0,32%/anno e ci avrei creduto: era il MISALLINEAMENTO di un'ora") + print(" fra indice e barre (vedi [ALLINEAMENTO] in testa). Allineato, il pezzo vale") + print(" +0,07%/anno: il VERSO e' quello della tesi e l'IC95 esclude lo zero per un") + print(" soffio su entrambi gli asset, ma la TAGLIA e' il 3% del pezzo (1). Un verso") + print(" giusto a un trentesimo della grandezza non e' una conferma, e' un residuo:") + print(" tutto il vantaggio dello spot e' il funding evitato, e va detto cosi'.") + print(" πŸ“Œ E il modo in cui l'artefatto si e' fatto vedere e' la cosa da ricordare: non") + print(" dal numero, che era plausibile e nel verso atteso, ma dalla VOLATILITA' β€” la") + print(" variante spot mostrava +1,07pp di vol su una strategia identica, e uno strumento") + print(" che traccia lo stesso sottostante non puo' aggiungere un punto di vol.") + rngz = np.random.default_rng(823) + for a in ASSETS: + idx = pd.DatetimeIndex(dg[a]["idx"]); pos = pd.Series(dg[a]["pos"], index=idx) + c = pd.Series(FD.daily_funding(SR[a], idx), index=idx) + d = (pos * c).values + nb = max(1, len(d) // 20) + bs = [np.mean(np.concatenate([d[x:x+20] for x in + rngz.integers(0, max(1, len(d)-20), nb)])) + for _ in range(1000)] + lo, hi = np.percentile(bs, [2.5, 97.5]) * 365.25 + print(f" {a}: base del perpetual pagata da TP01 {d.mean()*365.25:+.3%}/anno " + f"IC95 a blocchi [{lo:+.3%}, {hi:+.3%}]") + + m0 = FD.stat(res5["tp"]["perp"]); m1 = FD.stat(res5["tp"]["spot+esec"]) + print(f"\n TP01: drift {m0['drift']:.2%} -> {m1['drift']:.2%} ({m1['drift']-m0['drift']:+.2%}/anno), " + f"Sharpe {m0['full']:.3f} -> {m1['full']:.3f}, maxDD {m0['dd']:.1%} -> {m1['dd']:.1%}") + print(" πŸ“Œ E' l'unica variante che alza il drift SENZA alzare la volatilita' (vol di TP01") + print(" 12,16% sul perpetual e 12,16% sullo spot, alla terza cifra): lo spot non ha una") + print(" base che si riprezza. Il datato invece scambia carry contro varianza, ed e' per") + print(" questo che il suo Sharpe non migliora mentre il suo drift si'.") + print("\n ⚠️ MA C'E' UNA DOMANDA APERTA CHE VALE PIU' DEL RISPARMIO, e non e' risolvibile") + print(" con un backtest. CLAUDE.md la registra gia' (07/08): i DERIVATI su cripto") + print(" potrebbero qualificarsi `c-quater` (26%) invece di `c-sexies` (33%). Se e'") + print(" cosi', spostare TP01 dal perpetual (derivato) allo SPOT (cripto-attivita')") + print(" sposta il suo P&L nel comparto al 33%.") + cagr = FD.stat(res5["var"]["A) perp (IL LIBRO DI OGGI)"])["cagr"] + print(f" Ordine di grandezza: 7 punti di aliquota su un CAGR di libro del {cagr:.1%} " + f"valgono ~{0.07*cagr:.2%}/anno,") + print(" cioe' PIU' del risparmio di carry che lo spot porta. La risposta e' del") + print(" commercialista, non del backtest β€” ed e' esattamente la stessa domanda gia'") + print(" aperta nel progetto, che ora ha un secondo motivo per essere posta.") + if V: + xs = {} + for i in (f"{a}_USDC" for a in ASSETS): + xs[i] = np.mean([x for x in (walk_cost_bps(V["books"].get(i), u, sd) + for u in TICKETS for sd in ("buy", "sell")) + if x is not None]) if V["books"].get(i) else float("nan") + for i in (f"{a}_USDC-PERPETUAL" for a in ASSETS): + xs[i] = np.mean([x for x in (walk_cost_bps(V["books"].get(i), u, sd) + for u in TICKETS for sd in ("buy", "sell")) + if x is not None]) if V["books"].get(i) else float("nan") + _, dgz = FD.tp01_realistic(ALLOC * W_TP, rate) + now, later = [], [] + for a in ASSETS: + to = float(np.abs(np.diff(dgz[a]["pos"], prepend=0.0)).sum()) / \ + ((pd.DatetimeIndex(dgz[a]["idx"])[-1] + - pd.DatetimeIndex(dgz[a]["idx"])[0]).days / 365.25) + base = xs[f"{a}_USDC-PERPETUAL"] * 1e-4 + FEE_REALE + now.append(to * (xs[f"{a}_USDC"] * 1e-4 + 0.0 - base)) + later.append(to * (xs[f"{a}_USDC"] * 1e-4 + FEE_REALE - base)) + print(f"\n ⚠️ E se la fee spot smettesse di essere zero: il sovracosto d'esecuzione di") + print(f" TP01 passa da {np.mean(now):+.3%}/anno a {np.mean(later):+.3%}/anno, cioe' " + f"mangia il {(np.mean(later)-np.mean(now))/0.0207:.0%} del vantaggio") + print(f" (che a livello di sleeve vale ~2,07%/anno). Sopravviverebbe, ma va") + print(f" sorvegliato: `fee_watch` oggi guarda i perpetual, non lo spot.") + print(" ⚠️ Secondo limite dichiarato: qui lo spot e' modellato con l'INDICE Deribit, non") + print(" col libro spot vero (che non ha storia in questo progetto). Sul carry e'") + print(" corretto per costruzione (l'indice E' il sottostante); sull'esecuzione no, e") + print(" infatti il mezzo spread e' preso dall'istantanea del libro vero.") + + +# =========================================================================== +# 10) GATE +# =========================================================================== +def sezione9(rate: dict, DR: dict, res5: dict, grid: dict) -> None: + sez("9) GATE β€” esito di ciascuno; quelli non girati sono DICHIARATI, non omessi") + + # causalita': controllo POSITIVO (un test che non puo' fallire non e' un test) + a = "BTC" + _, dg = FD.tp01_realistic(ALLOC * W_TP, rate) + idx = pd.DatetimeIndex(dg[a]["idx"]); pos = pd.Series(dg[a]["pos"], index=idx) + c = pd.Series(FD.daily_funding(DR[a], idx), index=idx) + base = float((pos * c).mean()) * 365.25 + ahead = float((pos * c.shift(-1).fillna(0.0)).mean()) * 365.25 + lag = float((pos * c.shift(1).fillna(0.0)).mean()) * 365.25 + print(f" causality : il carry dell'ora t si applica alla posizione tenuta in t,") + print(f" e il contratto e' scelto alla chiusura di t-1. Controllo") + print(f" POSITIVO: usando il carry di DOMANI {ahead:.3%} e di IERI") + print(f" {lag:.3%} contro {base:.3%} -> il test ha POTENZA " + f"({abs(ahead-base)*1e4:.0f}/{abs(lag-base)*1e4:.0f} bps di scarto).") + + # sensibilita' al parametro d'esecuzione (griglia dichiarata) + print(f" griglia del roll : BUF in {BUF_GRID} giorni (parametro d'ESECUZIONE, e un") + print(" parametro d'esecuzione scelto guardando il risultato e'") + print(" selezione come ogni altra β€” regola 27/07). Canonico BUF=5") + print(" dichiarato PRIMA. Ξ”drift di libro per cella:") + for b in BUF_GRID: + DRb = {x: dated_rate(x, grid[x], buf=b) for x in ASSETS} + tpb = FD.tp01_realistic(ALLOC * W_TP, DRb)[0] + d = (float(book(rate, tp_series=tpb).mean()) + - float(res5["var"]["A) perp (IL LIBRO DI OGGI)"].mean())) * 365.25 + print(f" BUF={b:>2}g -> Ξ”drift {d:>+7.3%}" + f"{' <- canonico' if b == BUF_CANON else ''}") + print(" -> piatto: la scelta non e' dove vive il risultato.") + + # de-levering null + A = res5["var"]["A) perp (IL LIBRO DI OGGI)"] + D = res5["var"]["D) C + sovracosto d'esecuzione"] + sa, sd = FD.stat(A), FD.stat(D) + k = sa["vol"] / sd["vol"] + rk = D.values.astype(float) * k + shk = float(np.mean(rk) / np.std(rk) * np.sqrt(365.25)) + print(f" null del de-levering : NON APPLICABILE nella forma consueta (nessun claim di") + print(f" Β«meno drawdownΒ»: il maxDD si muove di {sd['dd']-sa['dd']:+.2%}).") + print(f" Girato il suo SPECULARE β€” a ISO-VOLATILITA' il candidato") + print(f" deve restare migliore: k={k:.3f} -> Sharpe {shk:.3f} contro") + print(f" {sa['full']:.3f} del baseline -> " + f"{'PASSA' if shk > sa['full'] else 'FALLISCE'} (il datato compra drift PAGANDOLO in") + print(" varianza, ed e' esattamente quello che il gate deve vedere).") + + E = res5["var"]["E) TP01 spot + SKH01 perp"] + se = FD.stat(E) + ke = sa["vol"] / se["vol"] + rke = E.values.astype(float) * ke + she = float(np.mean(rke) / np.std(rke) * np.sqrt(365.25)) + print(f" Stesso gate sulla variante SPOT: k={ke:.3f} -> Sharpe " + f"{she:.3f} contro {sa['full']:.3f} -> " + f"{'PASSA' if she > sa['full'] else 'FALLISCE'} (lo spot alza il drift senza") + print(" comprare varianza: e' l'unica delle tre che sopravvive).") + + ns = int(sum(len(FD.skh_daily(x, 0, "hourly", rate)[1]) for x in ASSETS)) + nl = int(sum(np.sum(np.array([t["gross"] for t in FD.skh_daily(x, 0, "hourly", rate)[1]]) <= 0) + for x in ASSETS)) + ip = al.implausible_sharpe(D, n_trades=ns, n_losing_trades=nl) + print(f" implausible_sharpe : implausible={ip['implausible']} (Sharpe {ip['sharpe']:.2f}, " + f"maxDD {ip['maxdd']:.1%}, Calmar {ip['calmar']:.2f}) -> {ip['reasons'] or 'nessuna firma'}") + print(f" anchor_luck (banda) : GIRATO, 23 offset, mediana delle differenze APPAIATE " + f"(sez. 5).") + print(" replica bit-exact : PASSATA β€” il -2,16%/anno del funding e' riprodotto in sez. 1") + print(" e l'assert ferma lo script se non torna.") + print(" marginal_vs_tp01 / deflated_sharpe / study_family_honest / weights_tilt_null:") + print(" NON GIRATI, e non per budget. Qui non c'e' un CANDIDATO:") + print(" il segnale e' congelato, i pesi non si toccano, non c'e'") + print(" una famiglia da cui selezionare. C'e' un COSTO che si") + print(" sposta cambiando strumento. Un `marginal_vs_tp01` su TP01") + print(" eseguito diversamente misurerebbe TP01 contro se stesso.") + + +def main() -> None: + print("=" * 100) + print("r0822e β€” FUNDING-AVOID: il 2,16%/anno si evita cambiando strumento?") + print(f"branch research/wave-0822 Β· {pd.Timestamp.now(tz='UTC')} Β· rete={'SI' if _NET else 'NO'}") + print("=" * 100) + print(" IPOTESI A PRIORI, registrata prima di misurare: il basis di un datato e il funding") + print(" di un perpetual sono lo STESSO premio (arbitraggio), quindi in livello devono") + print(" costare uguale; se il datato vince e' solo perche' il funding si riprezza contro") + print(" chi e' long e il basis no. Attesa: differenza piccola, forse nulla, e comunque") + print(" mangiata dal roll o dall'ineseguibilita'.") + + V = venue_snapshot() + d0 = sezione0(V) + + F = {} + for instr, (start, asset, kind) in FD.INSTR.items(): + d = FD.fetch_funding(instr, start).copy() + d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True) + F[instr] = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt") + rate = FD.build_rate(F) + grid = {a: pd.DatetimeIndex(rate[a]["series"].index) for a in ASSETS} + + verifica_allineamento(grid) + + print(f"\n{clock()} sezione 1...") + sezione1(rate) + + DR = {a: dated_rate(a, grid[a], buf=BUF_CANON, log=True) for a in ASSETS} + SR = {a: spot_rate(a, grid[a], log=True) for a in ASSETS} + print(f"\n copertura del front datato sulla griglia del funding: " + " Β· ".join( + f"{a} {DR[a]['valid'].mean():.4f}" for a in ASSETS)) + + print(f"\n{clock()} sezione 2...") + sezione2(rate, grid) + print(f"\n{clock()} sezione 3...") + sezione3(rate, DR) + print(f"\n{clock()} sezione 4...") + ex = sezione4(rate, DR, V, grid) + print(f"\n{clock()} sezione 5...") + res5 = sezione5(rate, DR, SR, ex) + print(f"\n{clock()} sezione 6...") + sezione6(res5) + sezione7(V) + sezione8(V, res5, rate, SR) + print(f"\n{clock()} sezione 9...") + sezione9(rate, DR, res5, grid) + + sez("SINTESI") + A = FD.stat(res5["var"]["A) perp (IL LIBRO DI OGGI)"]) + for k in ("B) tutto sui datati", "C) TP01 datato + SKH01 perp", + "D) C + sovracosto d'esecuzione", "E) TP01 spot + SKH01 perp"): + m = res5["st"][k] + print(f" {k:<32} Ξ”drift {m['drift']-A['drift']:>+7.2%} Ξ”ShFULL " + f"{m['full']-A['full']:>+7.3f} Ξ”ShHOLD {m['hold']-A['hold']:>+7.3f} " + f"Ξ”maxDD {m['dd']-A['dd']:>+6.2%}") + c_perp = res5["st"]["F) nessun carry (irreale)"]["drift"] - A["drift"] + print(f"\n costo del carry per il libro, sul perpetual: {c_perp:.2%}/anno " + f"(il -2,16% replicato)") + dD = res5["st"]["D) C + sovracosto d'esecuzione"]["drift"] - A["drift"] + tot = c_perp - dD + print(f" ... e su TP01-datato/SKH01-perp, tutto compreso (carry + mezzo spread + roll):" + f" {tot:.2%}/anno") + mD = res5["st"]["D) C + sovracosto d'esecuzione"] + print(f" risparmio netto: {dD:+.2%}/anno di drift, pagato con " + f"{mD['full']-A['full']:+.3f} di Sharpe FULL e {mD['hold']-A['hold']:+.3f} di hold-out.") + D_ = res5["st"]["D) C + sovracosto d'esecuzione"] + E_ = res5["st"]["E) TP01 spot + SKH01 perp"] + costo_dat = (c_perp - dD) * 100 + sez("VERDETTO") + print(" IPOTESI: esprimere il segnale congelato su un future DATATO invece che sul") + print(" perpetual costa meno del funding, perche' il basis e' fissato all'ingresso") + print(" mentre il funding si riprezza contro chi e' long. Attesa PRIMA di misurare:") + print(" differenza piccola o nulla, e comunque mangiata dal roll o dall'ineseguibilita'.") + print(" GRIGLIA DICHIARATA (al RIALZO): 2 estimatori (aritmetico/log) x 4 buffer di roll") + print(" (2/5/10/20 g) x 3 strumenti (perp/datato/spot) x 2 sleeve = 48 valutazioni di") + print(" carry; 6 varianti di libro x 23 offset d'ancora = 138 valutazioni di libro;") + print(" 4 muri x 26 passi di bisezione x 2.000 path. NESSUNA e' una SELEZIONE: il") + print(" segnale e i pesi sono congelati, non c'e' una cella scelta guardando il") + print(" risultato -> nessun deflated-Sharpe da calcolare.") + print(f" NUMERI (netti fee, a $600 di allocazione, lente `hourly` del libro live):") + print(f" A) perpetual, oggi Sh {A['full']:.3f} / hold {A['hold']:.3f} / " + f"maxDD {A['dd']:.1%} / CAGR {A['cagr']:.2%} / drift {A['drift']:.2%}") + print(f" D) TP01 datato, tutto dentro Sh {D_['full']:.3f} / hold {D_['hold']:.3f} / " + f"maxDD {D_['dd']:.1%} / CAGR {D_['cagr']:.2%} / drift {D_['drift']:.2%}") + print(f" E) TP01 spot, tutto dentro Sh {E_['full']:.3f} / hold {E_['hold']:.3f} / " + f"maxDD {E_['dd']:.1%} / CAGR {E_['cagr']:.2%} / drift {E_['drift']:.2%}") + print(f" haircut a $600: gia' dentro (le serie girano a $600 con min-order $5); il") + print(f" ticket massimo per asset e' $240 e resta nei primi livelli del libro datato.") + print(" VERDETTO: **SCARTATO** per i datati (il risparmio di drift si paga in varianza:") + print(f" hold-out {D_['hold']-A['hold']:+.3f}, negativo a 0/23 offset, e il gate") + print(" iso-volatilita' fallisce) β€” **LEAD** per lo SPOT su TP01, con la domanda") + print(" fiscale aperta che vale piu' del risparmio.") + print(" COSA MI SMENTIREBBE: (a) sui datati β€” un campione in cui il basis condizionale") + print(" NON si riprezza meno del funding, cioe' un regime in cui l'hold-out smette di") + print(" essere negativo: qui il vantaggio e' tutto 2019-2023 e vale ~0 dal 2024;") + print(" (b) sullo spot β€” che il fisco tratti i derivati come le cripto-attivita' (33%") + print(" entrambi): allora il LEAD diventa un candidato d'esecuzione; oppure che la fee") + print(" spot 0 sparisca col collegamento Coinbase (misurato in sez. 8: il vantaggio") + print(" si riduce ma non si annulla);") + print(" (c) su entrambi β€” che il mio proxy inverse per il carry dei datati non valga:") + print(" si smentisce raccogliendo qualche mese di quote della linea USDC datata e") + print(" confrontando i basis, oggi a 0,4-0,9 bps l'uno dall'altro.") + print(" SCRIPT: scripts/research/r0822e_funding_avoid.py") + print() + print(f"ESPRIMERE IL LIBRO SUI DATATI: COSTA {costo_dat:.2f} %/ANNO CONTRO 2,16% DEL PERP " + f"-> NON CONVIENE, perche' i {dD*100:.2f} punti risparmiati sul drift si pagano con " + f"{D_['hold']-A['hold']:+.3f} di Sharpe hold-out (negativo a 0/23 offset d'ancora) e " + f"il gate iso-volatilita' fallisce; NON e' INESEGUIBILE (il muro Β«tutti i datati " + f"sono INVERSEΒ» e' FALSO: 16 datati USDC esistono da 2026-05-07, min-order e fee " + f"identici al perpetual) ma la linea USDC ha 3,5 mesi di vita e zero roll mai " + f"eseguiti.") + print(f"\n{clock()} fine.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822e_scale_proto.py b/scripts/research/r0822e_scale_proto.py new file mode 100644 index 0000000..02a76ca --- /dev/null +++ b/scripts/research/r0822e_scale_proto.py @@ -0,0 +1,698 @@ +"""r0822e β€” PROTOTIPO ISOLATO della CHIAVE DI SCALA del libro live (filone SCALE-SPEC). + + ⚠️ QUESTO FILE NON E' PRODUZIONE E NON E' IMPORTATO DA NIENTE IN PRODUZIONE. + Non scrive nulla: ne' config/, ne' data/, ne' src/. Non manda ordini. Non tocca il conto. + La sezione (0) VERIFICA entrambe le cose a runtime invece di dichiararle: + - nessun file fuori da scripts/research/ nomina questo modulo; + - il sorgente di questo file non contiene una sola primitiva di scrittura. + Documento di specifica che accompagna questo prototipo: docs/research/SPEC-scale-key.md + +COSA DIMOSTRA + Il filone WORST-DAY (r0822c) ha misurato che il gradino di leva 1,00x -> 1,25x vale + 14,7 -> 11,6 anni al capitale-rendita, che C1-C5 passano, e che **C6 (processo) FALLISCE**: + `config/live.json` non ha una chiave di scala e il cap e' un CLAMP, non un moltiplicatore + (morde su 3 osservazioni-asset su 5.650). Questo prototipo mostra la MECCANICA della chiave + che manca, senza costruirla in produzione: + + (1) SANITY la copia della funzione a scala 1.0 e' BIT-EXACT alla produzione + (2) DOVE la catena segnale -> target -> ordine, letta dal sorgente vero con le righe + (3) SIMMETRIA la scala e' identica sui due sleeve (e il controllo positivo la smaschera) + (4) ORDINE scala PRIMA o DOPO il clamp: i due ordini danno k_eff diversi, misurato + (5) REPLICA i numeri a k=1,00x coincidono con quelli pubblicati da WORST-DAY + (6) GUARDIE tetto composito, disaster-SL, liquidazione: quale guardia MORDE per prima + (7) ANTI-CREEP il gate mancante: la finestra recente non deve poter autorizzare il gradino + + uv run python scripts/research/r0822e_scale_proto.py +""" +from __future__ import annotations + +import json +import re +import subprocess +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0725_capcurve as CC # noqa: E402 book_series (lente LIVE del libro) + +from src.data.downloader import load_data # noqa: E402 +from src.live.book import ( # noqa: E402 SOLA LETTURA (nessuna di queste scrive) + CAP_UNKNOWN_USD, W_SKH, W_TP01, _cap, _read_watermark, book_net_target) +from src.live.shadow import WEIGHT # noqa: E402 0.5 = split BTC/ETH +from src.strategies.skyhook import SKH01_V2_DD # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, realized_vol, resample_1d, simple_returns) + +ASSETS = ("BTC", "ETH") +ANN = 365.0 +DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor) +MM_RATE = 0.01 # maintenance margin perp lineari Deribit (valore prudente) +WIPE = 0.90 + +# ---- i numeri PUBBLICATI da WORST-DAY (sez. 33 di RESULTS-0822), per il confronto di replica --- +PUB = { + "worst_close": -0.0394, # peggior giorno in chiusura, lente LIVE + "worst_date": "2020-03-13", + "worst_min": -0.0733, # peggior minimo intra-giorno (lente accoppiata) + "L_struct": 0.1432, # peggior giorno POSSIBILE a k=1,00x, in chiusura + "cap_bite_n": 3, # osservazioni-asset in cui il clamp morde + "cap_bite_tot": 5650, + "drift_deluck": 0.1711, # r0822_growth_policy + "vol": 0.1137, + "sharpe_deluck": 1.51, +} + +# ---- la PROPOSTA che questo prototipo specifica ------------------------------------------ +SCALA_PROPOSTA = 1.25 +LEVA_LORDA_MAX = 1.25 # tetto sul PRODOTTO n_asset * frac * scala (costante di CODICE) +MEZZO_EQUITY = 0.50 # C1: il peggior giorno possibile deve lasciare meta' equity +GRIGLIA_K = (1.00, 1.10, 1.25, 1.50, 2.00) + + +def sez(t: str) -> None: + print("\n" + "=" * 108) + print(f" {t}") + print("=" * 108) + + +# ========================================================================================== +# LA COPIA β€” l'unica differenza rispetto a src/live/book.book_net_target e' `scala` +# ========================================================================================== +def book_net_target_scalato(tp_frac: float, skh_sign: int, equity: float, cap: float, + weight: float = WEIGHT, scala: float = 1.0, + scala_prima_del_clamp: bool = False) -> float: + """COPIA di src/live/book.book_net_target con la chiave di scala. + + `scala_prima_del_clamp` esiste SOLO per misurare l'ordine sbagliato nella sezione (4): + la specifica propone l'ordine DOPO (scala * clamp(raw)), non PRIMA. + """ + raw = weight * equity * (W_TP01 * max(tp_frac, 0.0) + W_SKH * float(skh_sign)) + if scala_prima_del_clamp: + raw = float(scala) * raw + return max(-cap, min(cap, raw)) + clamped = max(-cap, min(cap, raw)) + return float(scala) * clamped + + +def book_net_target_ASIMMETRICO(tp_frac: float, skh_sign: int, equity: float, cap: float, + weight: float = WEIGHT, scala: float = 1.0) -> float: + """CONTROLLO POSITIVO (deve essere SMASCHERATO dal test di simmetria): la scala applicata + alla sola gamba TP01. E' esattamente cio' che fa `target_vol`, ed e' un TILT DI PESI.""" + raw = weight * equity * (W_TP01 * float(scala) * max(tp_frac, 0.0) + W_SKH * float(skh_sign)) + return float(scala) * 0.0 + max(-cap, min(cap, raw)) + + +# ========================================================================================== +# (0) ISOLAMENTO β€” verificato, non dichiarato +# ========================================================================================== +def sezione_0_isolamento() -> None: + sez("(0) ISOLAMENTO β€” questo file non e' produzione, e la verifica e' qui dentro") + slug = Path(__file__).stem + try: + out = subprocess.run(["grep", "-rl", slug, str(ROOT / "src"), str(ROOT / "scripts"), + str(ROOT / "tests"), str(ROOT / "config")], + capture_output=True, text=True, timeout=60).stdout.split() + except Exception as e: # grep assente: si dichiara, non si finge + out = [] + print(f" ⚠️ grep non disponibile ({type(e).__name__}): controllo non eseguito") + fuori = [p for p in out if not p.endswith(f"scripts/research/{slug}.py")] + print(f" file che nominano `{slug}` fuori da se stesso : {len(fuori)} {fuori}") + assert not fuori, "QUALCUNO IMPORTA IL PROTOTIPO: non e' piu' isolato" + + src = Path(__file__).read_text() + # I nomi delle primitive vietate sono COMPOSTI a runtime: scriverli per esteso qui li + # farebbe trovare dal controllo stesso (primo tentativo: il file falliva su se' stesso). + vietate = tuple("".join(p) for p in (("write", "_text("), ("to_", "parquet("), ("to_", "csv("), + ("json.", "dump("), ("os.", "remove"), ("shu", "til."), + ("mk", "dir("), ("os.", "rename"))) + scritture = [m for m in vietate if m in src] + aperture_w = re.findall(r"open\([^)]*['\"][wa]", src) + print(f" primitive di scrittura nel sorgente : {scritture or 'nessuna'}") + print(f" open() in scrittura/append : {len(aperture_w)}") + assert not scritture and not aperture_w, "il prototipo scrive: non e' piu' innocuo" + print(" -> il prototipo LEGGE il codice di produzione e non lo tocca. Il cron non lo vede.") + + +# ========================================================================================== +# (1) SANITY β€” la copia e' la produzione +# ========================================================================================== +def sezione_1_sanity() -> None: + sez("(1) SANITY β€” a scala 1,00 la copia e' BIT-EXACT alla funzione di produzione") + dif = 0.0 + n = 0 + for eq in (600.0, 2_000.0, 6_047.0, 50_000.0): + for cap in (300.0, eq * 0.5, eq * 0.25, 1e9): + for tp in (0.0, 0.13, 1 / 3, 0.5, 2 / 3, 0.87, 1.0, 1.4, 2.0, -1.0): + for sg in (-1, 0, 1): + a = book_net_target(tp, sg, eq, cap) + b = book_net_target_scalato(tp, sg, eq, cap, scala=1.0) + dif = max(dif, abs(a - b)) + n += 1 + print(f" {n} combinazioni (equity x cap x tp_frac x segno) Β· max|copia - produzione| = {dif:.2e}") + assert dif == 0.0, "la copia NON e' la produzione: ogni numero sotto e' senza valore" + print(" -> tutto cio' che segue e' una proprieta' della FUNZIONE DI PRODUZIONE, non di una sua imitazione.") + + +# ========================================================================================== +# (2) DOVE VA LA CHIAVE β€” la catena letta dal sorgente vero +# ========================================================================================== +def _righe(path: Path, pattern: str) -> list[tuple[int, str]]: + out = [] + for i, ln in enumerate(path.read_text().splitlines(), 1): + if re.search(pattern, ln): + out.append((i, ln.rstrip())) + return out + + +def sezione_2_dove() -> None: + sez("(2) DOVE β€” la catena segnale -> target -> ordine, letta da src/live/ con le righe vere") + book = ROOT / "src" / "live" / "book.py" + shadow = ROOT / "src" / "live" / "shadow.py" + exe = ROOT / "scripts" / "live" / "book_execute.py" + for titolo, path, pat in ( + ("segnale TP01 (frazione long-flat, causale)", shadow, r"current_target"), + ("segnale SKH01 (solo il SEGNO: nessuna taglia)", book, r"_skh_sign|_skyhook_positions\(load5m"), + ("cap per-asset (clamp, watermark-bound)", book, r"^def _cap|cap = _cap\("), + ("TARGET NETTO <== QUI VA LA SCALA", book, r"^def book_net_target|net = book_net_target\("), + ("ordine (delta verso il target)", book, r"^def build_book_order|order = build_book_order\("), + ("invio (porta il conto al target)", exe, r"rebalance_signed|ensure_disaster_sl\(inst"), + ): + print(f"\n {titolo}") + for i, ln in _righe(path, pat)[:3]: + print(f" {path.relative_to(ROOT)}:{i:>4} {ln.strip()[:92]}") + print(f""" + LETTURA. `book_net_target` e' l'UNICO punto in cui le due gambe si incontrano con una + TAGLIA: sopra di lei TP01 da' una frazione e SKH01 da' un SEGNO (+1/-1/0, nessuna + grandezza); sotto di lei `build_book_order` calcola solo un DELTA verso il target. + Percio' e' l'unico punto in cui un moltiplicatore e' un puro cambio di scala: + Β· su TP01 (`target_vol`) -> scala il 75% del libro e lascia fermo il 25% = TILT DI PESI + Β· su SKH01 -> non c'e' niente da scalare: e' un segno + Β· su `WEIGHT` (0.5) -> e' il SPLIT BTC/ETH, ed e' condiviso con shadow_report/dashboard + (percorso TP01-only): cambiarlo cambierebbe un ALTRO libro + Β· su `W_TP01`/`W_SKH` -> sono il RAPPORTO 75/25 e sono legati per test ai pesi del + backtest (`deribit_book_sleeves`): moltiplicarli entrambi + romperebbe quella parita' e confonderebbe scala e mix + Β· su `build_book_order` -> scalerebbe il DELTA, non il target: cambia la VELOCITA' di + convergenza, non la taglia, e il reconciler litigherebbe con se' + Β· dentro l'esecutore -> il report e il log stamperebbero un target diverso da quello + eseguito (il difetto che questo progetto ha gia' pagato 3 volte)""") + + +# ========================================================================================== +# (3) SIMMETRIA +# ========================================================================================== +def sezione_3_simmetria() -> None: + sez("(3) SIMMETRIA β€” la scala e' identica sui due sleeve, e il controllo positivo lo prova") + eq, cap = 6_047.0, 1e12 # cap disattivato: qui si guarda SOLO la formula + print(f" {'k':>6}{'solo TP01 (tp=1,sg=0)':>24}{'solo SKH01 (tp=0,sg=1)':>25}" + f"{'rapporto TP/SKH':>18}{'peso implicito TP01':>21}") + base_ratio = None + for k in GRIGLIA_K: + t = book_net_target_scalato(1.0, 0, eq, cap, scala=k) + s = book_net_target_scalato(0.0, 1, eq, cap, scala=k) + rap = t / s + w = t / (t + s) + base_ratio = rap if base_ratio is None else base_ratio + print(f" {k:>6.2f}{t:>24,.2f}{s:>25,.2f}{rap:>18.6f}{w:>21.6f}") + assert abs(rap - W_TP01 / W_SKH) < 1e-12, "la scala ha cambiato il MIX: e' un tilt" + print(f" -> rapporto costante = W_TP01/W_SKH = {W_TP01/W_SKH:.4f} a OGNI k. La scala non e' un tilt.") + + print("\n proprieta' forte (quella che il test deve asserire): target(k) == k * target(1)") + peggio = 0.0 + for k in GRIGLIA_K: + for tp in (0.0, 1 / 3, 0.5, 2 / 3, 1.0, 1.35): + for sg in (-1, 0, 1): + a = book_net_target_scalato(tp, sg, eq, cap, scala=k) + b = k * book_net_target_scalato(tp, sg, eq, cap, scala=1.0) + peggio = max(peggio, abs(a - b)) + print(f" max|target(k) - k*target(1)| su {len(GRIGLIA_K)*6*3} combinazioni (cap disattivato) = {peggio:.2e}") + assert peggio == 0.0 + + print("\n CONTROLLO POSITIVO β€” la stessa proprieta' su un'implementazione ASIMMETRICA") + print(" (scala sulla sola gamba TP01 = cio' che farebbe `target_vol`):") + print(f" {'k':>6}{'solo TP01':>16}{'solo SKH01':>14}{'rapporto':>12}{'peso TP01':>12} esito del test") + smascherato = False + for k in (1.00, 1.25, 1.50): + t = book_net_target_ASIMMETRICO(1.0, 0, eq, cap, scala=k) + s = book_net_target_ASIMMETRICO(0.0, 1, eq, cap, scala=k) + ok = abs(t / s - W_TP01 / W_SKH) < 1e-12 + smascherato |= (not ok) + print(f" {k:>6.2f}{t:>16,.0f}{s:>14,.0f}{t/s:>12.4f}{t/(t+s):>12.6f} " + f"{'passa (k=1: non c e nulla da vedere)' if ok else 'FALLISCE <- smascherato'}") + assert smascherato, "il test di simmetria NON smaschera l'asimmetria: non sta controllando niente" + print(" -> a k=1 le due implementazioni COINCIDONO: un test che gira solo a k=1 ha potenza ZERO.") + print(" Il test di simmetria deve girare su una GRIGLIA di k, o non e' un test.") + + +# ========================================================================================== +# dati storici: peso TENUTO da TP01 per asset (stessa costruzione di r0822c gambe_tp01) +# ========================================================================================== +def pesi_tp01() -> dict: + out = {} + tv = CANONICAL["target_vol"] + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + c = df["close"].values.astype(float) + r = simple_returns(c) + tgt = np.nan_to_num(np.asarray(TrendPortfolio(**CANONICAL).target_series(df), float)) + held = np.zeros(len(tgt)) + held[1:] = tgt[:-1] # cio' che il libro TIENE il giorno d + vol = realized_vol(r, CANONICAL["vol_win_days"], 365.25) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + out[a] = pd.DataFrame({"held": held, "ret": r, "vol_prev": np.r_[np.nan, vol[:-1]]}, + index=idx).iloc[1:] + out[a] = out[a].assign(z=out[a]["ret"] / (out[a]["vol_prev"] / np.sqrt(365.25))) + out[a].attrs["tv"] = tv + return out + + +# ========================================================================================== +# (4) L'ORDINE COL CAP +# ========================================================================================== +def sezione_4_ordine(TPQ: dict, frac: float) -> None: + sez("(4) L'ORDINE COL CAP β€” il punto sottile: scala PRIMA o DOPO il clamp") + print(f""" + Il cap e' un CLAMP: net = clamp(0.5*E*(0.75*tp + 0.25*sg), Β±cap), cap = frac*E = {frac:.2f}*E. + A piena convinzione (tp=1, sg=+1) il grezzo vale ESATTAMENTE 0.5*E*1.0 = {0.5:.2f}*E = cap. + Quindi il clamp e' *tangente* al libro proprio nei giorni di massima esposizione.""") + E = 1.0 # tutto in frazione di equity + cap = frac * E + print(f"\n {'k':>6}{'ordine':>10}{'giorni-asset':>14}{'clampati':>11}{'quota':>9}" + f"{'k_eff medio':>13}{'k_eff a piena convinz.':>24}{'lordo max (x E)':>17}") + for k in GRIGLIA_K: + for prima in (True, False): + tot = n_cl = 0 + num = den = 0.0 + for a in ASSETS: + h = np.clip(TPQ[a]["held"].values, 0.0, None) + for sg in (1,): # SKH long: il caso che spinge di piu' + base = np.array([book_net_target_scalato(x, sg, E, cap, scala=1.0) for x in h]) + got = np.array([book_net_target_scalato(x, sg, E, cap, scala=k, + scala_prima_del_clamp=prima) for x in h]) + m = np.abs(base) > 1e-12 + num += float(np.sum(got[m])) + den += float(np.sum(base[m])) + n_cl += int(np.sum(np.abs(got) < np.abs(k * base) - 1e-12)) + tot += int(m.sum()) + piena = book_net_target_scalato(1.0, 1, E, cap, scala=k, scala_prima_del_clamp=prima) / \ + book_net_target_scalato(1.0, 1, E, cap, scala=1.0) + lordo = len(ASSETS) * abs(book_net_target_scalato(1.0, 1, E, cap, scala=k, + scala_prima_del_clamp=prima)) + print(f" {k:>6.2f}{'PRIMA' if prima else 'DOPO':>10}{tot:>14}{n_cl:>11}{n_cl/tot:>9.2%}" + f"{num/den:>13.4f}{piena:>24.4f}{lordo:>17.3f}") + print(f""" + LETTURA β€” l'ordine non e' un dettaglio, e' la differenza fra una scala e un'altra strategia. + + Β· SCALA PRIMA DEL CLAMP: il clamp la mangia proprio nei giorni di massima convinzione + (k_eff a piena convinzione = 1,00 a OGNI k). Il risultato non e' "il libro a leva k": + e' un libro che alza i giorni piccoli e lascia fermi i grandi, cioe' un APPIATTIMENTO + del profilo di convinzione. E' un cambio di FORMA travestito da cambio di taglia β€” la + curva g(k) di growth_policy/WORST-DAY NON descrive quel libro, perche' quella curva e' + calcolata su k * r, cioe' sull'ordine DOPO. + Β· SCALA DOPO IL CLAMP: k_eff = k esattamente, in ogni giorno e in ogni stato del segnale. + E' l'unico ordine per cui "leva k" significa la stessa cosa nel codice e nel backtest. + + ⚠️ IL PREZZO, e va detto perche' e' il cuore della specifica: con l'ordine DOPO il cap + SMETTE di essere il tetto assoluto del nozionale. Il lordo massimo diventa + n_asset * frac * k = {len(ASSETS)} * {frac:.2f} * k, cioe' {len(ASSETS)*frac*SCALA_PROPOSTA:.3f}x l'equity a k={SCALA_PROPOSTA:.2f}. + E' ESATTAMENTE la quantita' che `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` + (tests/test_fee_sensitivity.py) asserisce essere <= 1x. Quel test DEVE rompersi: sta + misurando `frac * n_asset` mentre la grandezza vera diventa `frac * n_asset * scala`.""") + + +# ========================================================================================== +# (5) REPLICA DEI NUMERI DI WORST-DAY a k = 1,00 +# ========================================================================================== +def sezione_5_replica(TPQ: dict, frac: float) -> dict: + sez("(5) REPLICA β€” a k=1,00 i numeri devono coincidere con quelli pubblicati da WORST-DAY") + book = CC.book_series("hourly") + r = book.values.astype(float) + r_del = r - (1.0 - DELUCK) * float(r.mean()) + m = { + "n": len(r), + "drift_deluck": float(r_del.mean()) * ANN, + "vol": float(r_del.std(ddof=0)) * np.sqrt(ANN), + "sharpe_deluck": float(r_del.mean() / r_del.std(ddof=0)) * np.sqrt(ANN), + "worst_close": float(r_del.min()), + "worst_date": str(book.index[int(np.argmin(r_del))].date()), + } + print(f"\n serie del libro LIVE (`book_series('hourly')`), {m['n']} giorni " + f"{book.index.min().date()} -> {book.index.max().date()}") + print(f" {'grandezza':>26}{'qui':>12}{'pubblicato':>13}{'scarto':>10}") + for nome, ch, pu, fmt in (("drift de-luckato x0.89", "drift_deluck", PUB["drift_deluck"], "%"), + ("vol annua", "vol", PUB["vol"], "%"), + ("Sharpe de-luckato", "sharpe_deluck", PUB["sharpe_deluck"], "f"), + ("peggior giorno (chiusura)", "worst_close", PUB["worst_close"], "%")): + v = m[ch] + s = f"{v:>12.2%}{pu:>13.2%}{v-pu:>10.2%}" if fmt == "%" else f"{v:>12.3f}{pu:>13.3f}{v-pu:>10.3f}" + print(f" {nome:>26}{s}") + print(f" {'data del peggior giorno':>26}{m['worst_date']:>12}{PUB['worst_date']:>13}" + f"{' coincide' if m['worst_date'] == PUB['worst_date'] else ' DIVERSA':>10}") + assert m["worst_date"] == PUB["worst_date"] + assert abs(m["worst_close"] - PUB["worst_close"]) < 5e-4 + + # --- il clamp morde su 3 osservazioni-asset su 5.650 + n_cap = sum(int(((0.5 * W_TP01 * TPQ[a]["held"].values + 0.5 * W_SKH) > frac).sum()) for a in ASSETS) + n_tot = sum(len(TPQ[a]) for a in ASSETS) + print(f"\n il CLAMP a frac={frac:.2f} morde su {n_cap} osservazioni-asset su {n_tot} " + f"({n_cap/n_tot:.3%}) [pubblicato: {PUB['cap_bite_n']}/{PUB['cap_bite_tot']}]") + for f2 in (0.50, 0.5625, 0.625, 0.75): + nn = sum(int(((0.5 * W_TP01 * TPQ[a]["held"].values + 0.5 * W_SKH) > f2).sum()) for a in ASSETS) + print(f" frac={f2:.4f} -> morde {nn:>3} volte Β· leva lorda MASSIMA {2*f2:.3f}x Β· " + f"leva EFFETTIVA mediana {np.median(np.concatenate([np.minimum(0.5*W_TP01*TPQ[a]['held'].values+0.5*W_SKH, f2)*2 for a in ASSETS])):.3f}x") + print(" -> alzare `frac` NON alza la leva: alza un tetto che non tocca quasi mai il libro.") + + # --- il bound strutturale a k = 1 + tv = CANONICAL["target_vol"] + sl_l = SKH01_V2_DD.sl_pct + ex_giu = {} + for a in ASSETS: + d = load_data(a, "1h") + ex_giu[a] = float((d["close"].values / d["open"].values - 1.0).min()) + lim_tp, lim_sk = {}, {} + for a in ASSETS: + d = TPQ[a].dropna(subset=["z"]) + dv = d[d["vol_prev"] >= tv] + z = float(dv["z"].min()) + lim_tp[a] = 0.5 * W_TP01 * tv * abs(z) / np.sqrt(365.25) + lim_sk[a] = 0.5 * W_SKH * (sl_l + abs(ex_giu[a])) + L = sum(lim_tp[a] + lim_sk[a] for a in ASSETS) + print(f"\n bound STRUTTURALE (scenario crash: TP01 a piena convinzione + SKH01 long stoppato)") + for a in ASSETS: + print(f" {a}: TP01 {-lim_tp[a]:>7.2%} SKH01 {-lim_sk[a]:>7.2%} -> {-(lim_tp[a]+lim_sk[a]):>7.2%}") + print(f" TOTALE a k=1,00x {-L:>7.2%} [pubblicato: {-PUB['L_struct']:.2%}]") + assert abs(L - PUB["L_struct"]) < 5e-3, "il bound strutturale non si replica" + print(" -> replica superata: la sezione (6) puo' scalare QUESTI numeri e non dei numeri copiati.") + return {"r_del": r_del, "L": L, "book": book} + + +# ========================================================================================== +# (6) LE GUARDIE β€” quale morde per prima +# ========================================================================================== +def sezione_6_guardie(rep: dict, TPQ: dict, frac: float, sl_pct: float) -> None: + sez("(6) GUARDIE β€” il tetto e' sul PRODOTTO, e la guardia che morde per prima non e' quella attesa") + L = rep["L"] + g_max = sum(0.5 * W_TP01 * TPQ[a]["held"].max() + 0.5 * W_SKH for a in ASSETS) + m_min = PUB["worst_min"] # peggior minimo intra-giorno misurato (accoppiato) + print(f""" + Tre grandezze scalano LINEARMENTE con k e vanno lette insieme (sl disaster = {sl_pct:.0%}): + A) peggior giorno POSSIBILE in chiusura : k * {L:.2%} + B) costo in equity se scatta il disaster-SL : k * n_asset * frac * {sl_pct:.0%} + (il -{sl_pct:.0%} e' un movimento di PREZZO sul mark; l'impatto in equity e' quel + movimento moltiplicato per il LORDO, quindi cresce con k anche se sl_pct non cambia) + C) distanza dalla liquidazione sul MINIMO : margine = x_liq / (|min| * k) +""") + print(f" {'k':>6}{'lordo max':>11}{'A) peggior giorno':>19}{'B) costo disaster-SL':>22}" + f"{'C) x_liq':>10}{'min intra k':>13}{'margine C':>11} verdetto") + for k in GRIGLIA_K: + G = min(g_max * k, len(ASSETS) * frac * k) + x_liq = (1 - MM_RATE * G) / (G * (1 - MM_RATE)) + A = k * L + B = k * len(ASSETS) * frac * sl_pct + mk = abs(m_min) * k + margine = x_liq / mk + v = [] + if A > MEZZO_EQUITY: + v.append("A sfonda meta' equity") + if B > MEZZO_EQUITY: + v.append("B sfonda meta' equity") + if margine < 3.0: + v.append("C sotto 3x") + if k * len(ASSETS) * frac > LEVA_LORDA_MAX: + v.append(f"oltre il tetto {LEVA_LORDA_MAX:.2f}x") + print(f" {k:>6.2f}{G:>11.3f}{A:>19.2%}{B:>22.2%}{x_liq:>10.1%}{mk:>13.2%}" + f"{margine:>11.1f}x {'; '.join(v) if v else 'ok'}") + + k_da_A = MEZZO_EQUITY / L + k_da_B = MEZZO_EQUITY / (len(ASSETS) * frac * sl_pct) + print(f""" + πŸ“Œ IL RISULTATO CHE WORST-DAY NON AVEVA CALCOLATO: la guardia che morde per prima NON e' + il peggior giorno possibile, e' il DISASTER-SL. + Β· dal peggior giorno (A): k <= {MEZZO_EQUITY:.0%} / {L:.2%} = {k_da_A:.2f}x + Β· dal disaster-SL (B): k <= {MEZZO_EQUITY:.0%} / ({len(ASSETS)} * {frac:.2f} * {sl_pct:.0%}) = {k_da_B:.2f}x + Il disaster-SL e' {k_da_A/k_da_B:.1f}x piu' stringente. A k={SCALA_PROPOSTA:.2f} il margine di (A) e' + {MEZZO_EQUITY/(SCALA_PROPOSTA*L):.1f}x mentre quello di (B) e' solo {MEZZO_EQUITY/(SCALA_PROPOSTA*len(ASSETS)*frac*sl_pct):.2f}x. + -> il tetto di codice non puo' essere un numero solo: deve essere l'INVARIANTE + `n_asset * frac * scala * disaster_sl_pct <= {MEZZO_EQUITY:.2f}`, che lega TRE chiavi. + Con sl_pct={sl_pct:.0%} e frac={frac:.2f} quell'invariante da' scala <= {k_da_B:.2f}x. + + ⚠️ E il buco che un tetto sulla SOLA chiave lascerebbe aperto: `frac` e `scala` MOLTIPLICANO. + Un tetto "scala <= {SCALA_PROPOSTA:.2f}" e' rispettato anche da frac=0.75 + scala={SCALA_PROPOSTA:.2f} + = leva lorda {2*0.75*SCALA_PROPOSTA:.3f}x. Il tetto va messo sul PRODOTTO.""") + print(f"\n {'frac':>8}{'scala':>8}{'prodotto = leva lorda':>24}{'tetto scala-only':>18}{'tetto sul PRODOTTO':>21}") + for f2, k in ((0.50, 1.00), (0.50, 1.25), (0.50, 1.50), (0.625, 1.25), (0.75, 1.25), (0.50, 2.00)): + p = len(ASSETS) * f2 * k + print(f" {f2:>8.3f}{k:>8.2f}{p:>24.3f}{'passa' if k <= SCALA_PROPOSTA else 'BLOCCA':>18}" + f"{'passa' if p <= LEVA_LORDA_MAX else 'BLOCCA':>21}") + + +# ========================================================================================== +# (6-bis) GLI STATI DELL'EQUITY β€” la scala non deve rendere il FALLBACK piu' permissivo +# ========================================================================================== +def cap_copia(equity, real_equity, eq_fallback, fixed, frac, wm): + """COPIA di src/live/book._cap (verificata sotto contro l'originale). Riprodotta qui perche' + l'originale legge il watermark REALE da disco e questo prototipo non deve dipenderne.""" + if frac is None: + return fixed + trusted = (real_equity is not None) and (not eq_fallback) and (equity is not None) and (equity > 0) + if trusted: + return float(equity) * float(frac) + return min(fixed, wm * float(frac)) if wm is not None else min(fixed, CAP_UNKNOWN_USD) + + +def sezione_6bis_equity(frac: float, fixed: float) -> None: + sez("(6-bis) GLI STATI DELL'EQUITY β€” la regola proposta: la scala vive SOLO sul percorso fidato") + wm = _read_watermark() + print(f" watermark reale sul disco (letto, non scritto): " + f"{'$%.2f' % wm if wm is not None else 'assente'}") + + # la copia e' la produzione, verificata sui due rami che non dipendono dal file + d = max(abs(cap_copia(6047.0, 6047.0, None, fixed, frac, wm) - _cap(6047.0, 6047.0, None)), + abs(cap_copia(2000.0, None, "fallback", fixed, frac, wm) - _cap(2000.0, None, "fallback"))) + print(f" max|copia del cap - produzione| sui due rami reali = {d:.2e}") + assert d == 0.0, "la copia del cap non e' la produzione" + + E_VERO = wm if wm is not None else 635.0 + print(f""" + Proposta: `scala_effettiva = scala se l'equity REALE e' leggibile, altrimenti 1.00`. + Motivo: una scala e' una decisione di rischio presa CONOSCENDO il conto; quando il conto + non si sa quanto vale, non si prende. Cosi' il vincolo "il fallback non e' piu' permissivo" + e' vero PER COSTRUZIONE e non per aritmetica. + Colonna che conta: LORDO / equity VERA (${E_VERO:,.2f}), cioe' la leva davvero corsa.""") + scen = [ + ("normale (equity reale letta)", E_VERO, E_VERO, None, wm), + ("online ma equity illeggibile", 2000.0, None, "fallback", wm), + ("watermark assente (1o avvio)", 2000.0, None, "fallback", None), + ("watermark vecchio e basso", 2000.0, None, "fallback", 100.0), + ("dopo un deposito a $6.047", 6047.0, 6047.0, None, 6047.0), + ] + print(f"\n {'stato':>32}{'cap':>11}{'scala eff.':>12}{'lordo max':>12}" + f"{'/ equity vera':>15}{'tetto rispettato':>18}") + for nome, eq, real, fb, w in scen: + cap = cap_copia(eq, real, fb, fixed, frac, w) + k_eff = SCALA_PROPOSTA if (real is not None and not fb) else 1.0 + lordo = len(ASSETS) * cap * k_eff + # l'equity VERA e' quella del conto (nota: qui la si conosce), NON il watermark: un + # watermark vecchio e basso non rende il conto piccolo, rende il libro SOTTO-esposto. + ev = real if real is not None else E_VERO + lev = lordo / ev + print(f" {nome:>32}{cap:>11,.2f}{k_eff:>12.2f}{lordo:>12,.2f}{lev:>15.3f}x" + f"{'si' if lev <= LEVA_LORDA_MAX + 1e-9 else 'NO':>18}") + print(""" + -> in nessuno stato degradato la leva supera quella dello stato normale: il fallback resta + la configurazione PIU' PRUDENTE, come oggi. E il caso "watermark assente" resta a + CAP_UNKNOWN_USD ($300) con scala 1.00: due protezioni indipendenti, non una sola. + ⚠️ PREZZO DICHIARATO: nei giri in cui l'equity non e' leggibile il libro gira a k=1 e al + giro dopo torna a k -> un ribilancio in piu' per ogni oscillazione. Costo: un delta di + (k-1)*frac*E per asset, cioe' ~$%.2f a testa sul conto di oggi, a 3,5 bps = $%.4f. + Frequenza MISURATA sui log (`logs/cron_book.log`): %s.""" % ( + (SCALA_PROPOSTA - 1) * frac * E_VERO, + (SCALA_PROPOSTA - 1) * frac * E_VERO * 0.00035 * 2, + _freq_fallback())) + + +def _freq_fallback() -> str: + """Quante volte il ramo 'equity fallback' e' stato preso davvero. Un prezzo dichiarato senza + la sua frequenza non e' un prezzo.""" + log = ROOT / "logs" / "cron_book.log" + if not log.exists(): + return "log assente, NON misurata" + try: + txt = log.read_text(errors="ignore") + except Exception: + return "log illeggibile, NON misurata" + giri = txt.count("BOOK DERIBIT LIVE EXECUTE") + fb = txt.count("EQUITY FALLBACK") + off = txt.count("conto non leggibile") + return (f"{fb} giri su {giri} ({fb/max(giri,1):.2%}); " + f"{off} giri sono invece OFFLINE (nessun ordine, la scala non si pone)") + + +# ========================================================================================== +# (7) IL GATE MANCANTE β€” anti-creep, e la finestra recente non deve poter autorizzare +# ========================================================================================== +def g_exact(r: np.ndarray, k: float) -> float: + x = 1.0 + k * r + if np.any(x <= 0): + return -np.inf + return float(np.expm1(ANN * np.mean(np.log(x)))) + + +def k_star(r: np.ndarray, kmax: float = 30.0, step: float = 0.05) -> float: + ks = np.arange(step, kmax + step, step) + return float(ks[int(np.nanargmax([g_exact(r, k) for k in ks]))]) + + +def _k_ammesso(r: np.ndarray, L: float) -> dict: + """k autorizzato dal criterio: mezzo-Kelly sul drift a -2 SE, con la coda TRONCATA al bound + strutturale (senza troncatura E[log(1+kR)] non esiste). Deliberatamente SEMPLIFICATO + rispetto a r0822c (nessuna GPD): qui interessa la MECCANICA del gate, non il suo numero.""" + mu, sd = float(r.mean()), float(r.std(ddof=0)) + se = sd * np.sqrt(ANN) / np.sqrt(len(r) / ANN) + r2 = np.maximum(r - mu + (mu * ANN - 2 * se) / ANN, -L) # -2SE + coda troncata + ks = k_star(r2) + return {"drift_pt": mu * ANN, "se": se, "drift_2se": mu * ANN - 2 * se, + "k_star": ks, "k_ammesso": 0.5 * ks, "n": len(r)} + + +def sezione_7_gate(rep: dict) -> None: + sez("(7) IL GATE MANCANTE β€” la scala e' invisibile allo Sharpe: serve un gate su un ALTRO asse") + r = rep["r_del"] + L = rep["L"] + book = rep["book"] + + print("\n (a) PERCHE' i gate esistenti non possono servire: lo Sharpe e' INVARIANTE alla scala.") + print(f" {'k':>6}{'Sharpe':>10}{'drift':>10}{'vol':>10}{'maxDD':>10}{'g(k) esatto':>14}") + for k in GRIGLIA_K: + x = k * r + eq = np.cumprod(1.0 + x) + dd = float((eq / np.maximum.accumulate(eq) - 1.0).min()) + print(f" {k:>6.2f}{x.mean()/x.std(ddof=0)*np.sqrt(ANN):>10.4f}{x.mean()*ANN:>10.2%}" + f"{x.std(ddof=0)*np.sqrt(ANN):>10.2%}{dd:>10.2%}{g_exact(r,k):>14.2%}") + print(" -> `deflated_sharpe` e `marginal_vs_tp01` leggono la 1a colonna: non FALLISCONO,") + print(" NON VEDONO la variabile. Un gate sulla scala deve guardare g(k), il DD e la rovina.") + + print("\n (b) IL CRITERIO ANTI-RECENCY β€” Β«una chiave di scala si alza dopo un mese buonoΒ».") + print(" Forma verificabile: il gradino dev'essere autorizzato ANCHE dai dati che") + print(" ESCLUDONO la finestra recente che ha motivato la richiesta.") + print(f"\n {'finestra':>34}{'n':>7}{'drift punto':>13}{'drift -2SE':>12}{'k*':>8}" + f"{'k ammesso':>11} {SCALA_PROPOSTA:.2f}x?") + fin = [("TUTTA la storia", r), + ("esclusi gli ultimi 90 giorni", r[:-90]), + ("esclusi gli ultimi 180 giorni", r[:-180]), + ("esclusi gli ultimi 365 giorni", r[:-365]), + ("SOLO gli ultimi 365 giorni", r[-365:])] + for nome, rr in fin: + d = _k_ammesso(rr, L) + ok = SCALA_PROPOSTA <= d["k_ammesso"] + print(f" {nome:>34}{d['n']:>7}{d['drift_pt']:>13.2%}{d['drift_2se']:>12.2%}" + f"{d['k_star']:>8.2f}{d['k_ammesso']:>11.2f} {'SI' if ok else 'NO'}") + d_full = _k_ammesso(r, L) + d_ex = _k_ammesso(r[:-90], L) + print(f""" + ⚠️ LETTURA ONESTA, e la prima stesura di questa sezione l'aveva sbagliata. La riga + Β«SOLO gli ultimi 365 giorniΒ» NON e' l'esempio di Β«un anno buonoΒ»: quell'anno e' stato + DEBOLE (drift di punto {_k_ammesso(r[-365:], L)['drift_pt']:.2%}). Cio' che quella riga mostra e' solo che su + 365 osservazioni l'errore standard divora tutto β€” vero e insufficiente, perche' il rischio + da coprire e' l'opposto: un tratto recente BUONO che alza la richiesta.""") + + print("\n Percio' la domanda va posta al contrario: QUANTO puo' spostare il k ammesso un") + print(" tratto recente artificialmente buono? Si sovrascrive il drift degli ultimi 90") + print(" giorni e si guarda quanto si muove il criterio, sull'INTERA storia.") + print(f"\n {'ultimi 90g portati a':>24}{'drift punto (full)':>21}{'drift -2SE':>13}" + f"{'k ammesso':>11}{'delta vs base':>15}") + base_k = d_full["k_ammesso"] + for shock in (0.0, 0.25, 0.50, 1.00, 2.00): + rr = r.copy() + rr[-90:] = rr[-90:] - rr[-90:].mean() + shock / ANN + d = _k_ammesso(rr, L) + print(f" {shock:>23.0%}{d['drift_pt']:>21.2%}{d['drift_2se']:>13.2%}" + f"{d['k_ammesso']:>11.2f}{d['k_ammesso']-base_k:>+15.2f}") + print(f""" + -> un trimestre portato a +100%/anno (assurdo) muove il k ammesso di poche unita' su una + base di {base_k:.2f}: e' il -2 SE che fa il lavoro, perche' un tratto eccezionale alza + insieme il drift di punto E la sua incertezza. **Ma questo NON basta come gate**: il + criterio resta comunque una funzione dei dati recenti, e la protezione vera e' la + DOPPIA VALUTAZIONE β€” la richiesta dev'essere autorizzata anche da `r[:-90]` + (qui k ammesso {d_ex['k_ammesso']:.2f} contro {base_k:.2f}), cioe' da dati che NON contengono + la finestra che ha motivato la richiesta. E' l'analogo, sull'asse della scala, di + `select_cell_insample`. + ⚠️ E va detto il numero che rende tutto questo poco drammatico OGGI: con questa versione + semplificata il k ammesso e' {base_k:.2f}x, cioe' il gradino di 1,25x non e' vicino a + quel vincolo. **Il vincolo che morde e' quello della sez.(6) β€” il disaster-SL β€” non + il mezzo-Kelly.** Il criterio anti-recency serve al gradino SUCCESSIVO, non a questo.""") + + print("\n (c) LA CADENZA β€” un gate che si valuta solo quando lo si chiede non protegge da niente.") + ultimo = book.index.max() + print(f" ultimo giorno della serie: {ultimo.date()}. Il sorvegliante deve rispondere a") + print(" TRE domande diverse, e sono davvero tre (azioni diverse):") + print(" 1. la config dichiara una scala che il giornale non ha mai autorizzato? -> 🚨") + print(" 2. il criterio che autorizzo' la scala CORRENTE passa ancora oggi? -> 🚨 (si scende)") + print(" 3. il prodotto frac*scala*n_asset ha superato il tetto di codice? -> 🚨 (si blocca)") + print(" Nessuna delle tre e' deducibile dalle altre due.") + + +# ========================================================================================== +def main() -> None: + t0 = time.time() + print("=" * 108) + print(" r0822e β€” PROTOTIPO ISOLATO della CHIAVE DI SCALA (filone SCALE-SPEC)") + print(" Specifica: docs/research/SPEC-scale-key.md Β· NON e' produzione, non scrive nulla") + print("=" * 108) + + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac = float(cfg["max_notional_per_asset_frac"]) + sl_pct = float(cfg["disaster_sl_pct"]) + print(f"\n config/live.json (SOLA LETTURA): frac={frac} Β· cap fisso ${cfg['max_notional_per_asset_usd']:,.0f}" + f" Β· min order ${cfg['min_order_usd']} Β· disaster-SL {sl_pct:.0%}") + print(f" chiavi presenti: {sorted(k for k in cfg if not k.startswith('_'))}") + print(" -> nessuna chiave di scala. E' esattamente il buco che questo filone specifica.") + + sezione_0_isolamento() + sezione_1_sanity() + sezione_2_dove() + sezione_3_simmetria() + TPQ = pesi_tp01() + sezione_4_ordine(TPQ, frac) + rep = sezione_5_replica(TPQ, frac) + sezione_6_guardie(rep, TPQ, frac, sl_pct) + sezione_6bis_equity(frac, float(cfg['max_notional_per_asset_usd'])) + sezione_7_gate(rep) + + sez("RIEPILOGO β€” cosa questo prototipo ha DIMOSTRATO (e cosa resta alla specifica)") + print(f""" + DIMOSTRATO QUI + Β· la copia e' bit-exact alla produzione a k=1 -> le proprieta' misurate sono SUE; + Β· la scala applicata in `book_net_target` e' simmetrica sui due sleeve a ogni k, e il + controllo positivo prova che il test ha potenza SOLO se gira su una griglia di k; + Β· l'ordine col clamp decide se "leva k" significa la stessa cosa nel codice e nel + backtest: DOPO il clamp k_eff = k sempre; PRIMA il clamp la mangia a piena convinzione; + Β· i numeri a k=1,00 replicano WORST-DAY (peggior giorno, data, bound strutturale, morso + del clamp) -> le righe scalate della sez.(6) non sono numeri copiati; + Β· la guardia che morde per prima e' il DISASTER-SL, non il peggior giorno: il tetto va + messo sul PRODOTTO `n_asset * frac * scala * disaster_sl_pct`; + Β· lo Sharpe e' invariante alla scala a tutte le celle -> nessun gate esistente la vede; + Β· il criterio anti-recency e' misurabile: si valuta escludendo la finestra recente. + + NON DIMOSTRATO QUI, E VA DETTO + Β· non e' un backtest della leva: g(k) qui e' la forma chiusa su una serie storica, con + lo stesso limite gia' dichiarato da growth_policy (ritorno indipendente dalla size); + Β· la lente e' CLOSE-ONLY tranne dove usa il minimo pubblicato da WORST-DAY: su una regola + a UN giorno close-only e' CIECA (25/07), e la sez.(6) prende quel numero da fuori; + Β· il funding dei perpetual non e' in nessuna di queste serie (difetto trovato dal filone + FUNDING): a k=1 il libro paga gia' ~1,5%/anno che nessun backtest sottrae, e il gradino + lo amplifica del 25%; + Β· niente qui autorizza il gradino: autorizzarlo e' il gate della specifica, non un run. + + tempo: {time.time()-t0:.1f}s""") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_anchor_ensemble.py b/scripts/research/r0823_anchor_ensemble.py new file mode 100644 index 0000000..02e12c8 --- /dev/null +++ b/scripts/research/r0823_anchor_ensemble.py @@ -0,0 +1,1230 @@ +#!/usr/bin/env python +"""r0823_anchor_ensemble.py β€” Β§47 ANCHOR-ENSEMBLE: si puo' ESEGUIRE l'ensemble d'ancora? + +LA DOMANDA. Il progetto ha misurato con enorme cura la FORTUNA D'ANCORA (TP01 gira sull'ancora +daily 00:00 UTC, la migliore delle 24; SKH01 sull'offset 0 di 23; XS01 fase 0 di 10; VRP01 fase +0 di 7) e ha sempre risposto DE-LUCKANDO IL NUMERO β€” mai cambiando il modo di eseguire. Qui si +chiede l'altra cosa: invece di scegliere un'ancora e poi correggere la stima, si puo' dividere +lo sleeve in N tranche, ognuna ribilanciata a un'ora diversa e ognuna a 1/N della size? + +PRECEDENTE DA SUPERARE (non ignorare). Il 2026-07-02 (`r0702_tp01_offset.py`, diario +`2026-07-02-timing-crt-wave.md`) misuro' il tranching K=2/K=4 e concluse: *"sola riduzione della +varianza della STIMA (dSharpe n.s., dDD ~0.5pt), NO deploy a $600 β€” a $225/asset di quota TP01 +i delta per-ancora (~$1-2) sono sotto il min-order $5 -> degenererebbe comunque in K=1; serve +feed intraday fuori path certificato. Rivalutare a >=5-10k."* +Tre cose sono cambiate: (a) il FUNDING e' misurato (-2,16%/anno, 22/08) e il tranching cambia il +profilo orario d'esposizione; (b) la banda d'ancora del LIBRO e' misurata sullo spazio congiunto +(26/07); (c) il 22/08 ha misurato che il pavimento min-order non e' il vincolo che si credeva. + +E c'e' un quarto fatto, di CODICE, che nessuno aveva messo accanto alla conclusione del 02/07 β€” +si veda Β§4: `src/live/book.build_book_order` applica `abs(delta) < min_usd -> None` sulla +posizione **NETTA**, e manda **un solo ordine per asset per giro**. Il "delta per-ancora" non e' +mai un ordine. La premessa del 02/07 descrive un'esecuzione che il libro non fa. + +COSA MISURA, nelle tre parti chieste: + (i) l'ensemble batte l'ancora MEDIANA? di quanto su Sharpe E su DRIFT, e con che + compressione di dispersione fra realizzazioni; + (ii) l'ensemble batte l'ancora CANONICA che gira oggi? (attesa: NO in-sample) β€” e quale dei + due numeri e' la stima onesta del FUTURO; + (iii) a quale CAPITALE l'ensemble diventa eseguibile senza degenerare? curva N_max(capitale) + con il pavimento VERO del venue, per $635 / $2k / $5k / $20k / $50k. +piu': il costo (ordini/anno, fee/anno), il FUNDING (l'unico canale per cui l'ensemble potrebbe +GUADAGNARE invece di ridurre varianza), SKH01 separatamente e insieme a TP01, la replica del +02/07 PRIMA di ogni delta, e il valore della riduzione di varianza SOTTO UNA BARRIERA ASSORBENTE +(canale funded), che e' la domanda che resta se la risposta e' "non aggiunge ritorno". + +RIUSO (non riscrittura): `altlib` (eval_weights/_sh/_to_daily/_dd_ret/HOLDOUT), +`r0702_tp01_offset` (resample_offset/hourly_target/smallcap_net β€” la replica e' like-for-like), +`r0702_anchor_skh01` (run_asset/resample_off/OFFSETS), `r0822d_funding` (fetch/build_rate/cumF β€” +il funding NON viene rimodellato), `src.live.deribit._CONTRACT` (il pavimento si DERIVA dal +codice sorvegliato, non si ridichiara β€” regola `fee_watch` 21/08), +`src.portfolio.portfolio.combine_outer`. + +VINCOLI: nessun file toccato fuori da questo script; nessuna rete (cache funding gia' su disco); +niente commit di dati. Runtime ~5-8 min su 2 core. + +Uso: `nice -n 19 uv run python scripts/research/r0823_anchor_ensemble.py` +""" +from __future__ import annotations + +import sys +import time +from functools import lru_cache +from itertools import product +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in ("scripts/research", "scripts/research/alt", ""): + sys.path.insert(0, str(ROOT / _p)) + +import altlib as al # noqa: E402 +import r0702_anchor_skh01 as r02 # noqa: E402 +import r0702_tp01_offset as r07 # noqa: E402 +import r0822d_funding as fd # noqa: E402 +from src.live.deribit import _CONTRACT # noqa: E402 (il pavimento si DERIVA, non si ridichiara) +from src.portfolio.portfolio import combine_outer # noqa: E402 + +fd.USE_NET = False # cache su disco: questo filone non tocca la rete + +ASSETS = ("BTC", "ETH") +HOURS = tuple(range(24)) +HOLDOUT = al.HOLDOUT +FEE = al.FEE_SIDE # 0.05%/lato = 0.10% RT +MSH = 3_600_000 + +# Il libro live: `src/live/book.py` W_TP01=0.75, W_SKH=0.25, WEIGHT=0.5 (50/50 BTC+ETH). +W_TP, W_SKH, W_ASSET = 0.75, 0.25, 0.5 +MIN_ORDER_USD = 5.0 # config/live.json min_order_usd (letto sotto) +CAPITALI = (635.0, 2000.0, 5000.0, 20000.0, 50000.0) + +# famiglie di tranching a priori, simmetriche, zero tuning (stessa logica del 02/07) +FAM_TP = {1: 24, 2: 12, 3: 8, 4: 6, 6: 4, 8: 3, 12: 2, 24: 1} # K -> n. rotazioni + +T0 = time.time() + + +def sez(t: str) -> None: + print("\n" + "=" * 100) + print(f" {t}") + print("=" * 100) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +def fam_rotations(K: int) -> list[tuple]: + """Le TUTTE rotazioni della famiglia K (nessuna selezione): (s, s+24/K, s+2*24/K, ...).""" + step = 24 // K + return [tuple((s + step * j) % 24 for j in range(K)) for s in range(step)] + + +# =========================================================================== +# Β§0 β€” ATTESE A PRIORI (scritte e stampate PRIMA di qualunque misura) +# =========================================================================== +ATTESE = [ + ("A1", "ALGEBRA: la posizione dell'ensemble e' la MEDIA delle posizioni delle tranche, e il " + "lordo orario e' lineare nella posizione -> lordo_ens == media dei lordi, ESATTAMENTE " + "(max|dif| = 0). Il turnover invece e' SUB-additivo (|media dei delta| <= media dei " + "|delta|) -> turnover_ens <= turnover MEDIO delle ancore. Quindi l'ensemble domina " + "debolmente l'ancora MEDIA su drift E su vol: non e' 'solo' riduzione di varianza, ma " + "il pezzo di drift e' solo fee-netting, cioe' briciole."), + ("A2", "(i) l'ensemble batte l'ancora MEDIANA su Sharpe. Quasi per costruzione: stesso drift, " + "meno vol. Attesa dSharpe FULL fra +0,03 e +0,15; dDRIFT ~ 0 (<0,3pp)."), + ("A3", "(ii) l'ensemble NON batte l'ancora CANONICA sull'hold-out. La canonica sta al ~92-96Β° " + "pctl delle 24: una media non batte il massimo. Attesa: dSharpe HOLD NEGATIVO. " + "E la stima onesta del futuro e' comunque l'ensemble, non la canonica."), + ("A4", "FUNDING: l'ensemble NON puo' guadagnare un centesimo. Il funding pagato in un'ora e' " + "pos*f, ESATTAMENTE lineare in pos -> funding_ens == media dei funding delle ancore, " + "max|dif| = 0. Il rapporto condizionale mean(pos*f)/(mean(pos)*mean(f)) β€” misurato " + "1,86-2,55x il 22/08 β€” e' esso stesso una media e NON si attenua: la correlazione " + "esposizione-funding vive alla scala del REGIME, non dell'ora del giorno. " + "Il canale sperato e' chiuso per ALGEBRA, non per piccolezza dell'effetto."), + ("A5", "(iii) ESEGUIBILITA': gli ordini NON sono K volte tanti. `build_book_order` banda la " + "posizione NETTA e manda un ordine per asset per giro -> il numero di ordini e' " + "limitato dalla BANDA, non da K. La premessa del 02/07 ('i delta per-ancora sono sotto " + "il min-order') descrive un'esecuzione che il libro non fa. MA la conclusione del 02/07 " + "sopravvivera' comunque, per un'altra strada: a $635 la banda mangia gia' K=1 (il " + "ribilancio MEDIANO di TP01 e' sotto $5), quindi non c'e' K da degradare. " + "Attesa: N_max($635) = 1, e N_max cresce con il capitale."), + ("A6", "SKH01: peggiore rapporto costo/beneficio (4x piu' fee-sensibile, 22/08) e " + "690/30 = 23 e' PRIMO -> sulla griglia a 30m dichiarata il 02/07 non esiste alcun " + "sotto-ensemble simmetrico (o 1 o 23). Le famiglie simmetriche esistono solo sulla " + "griglia 5m (138 = 2*3*23 -> K in {2,3,6,23})."), + ("A7", "BARRIERA: la riduzione di varianza vale poco su un conto proprio (dove il nemico e' il " + "drift) e MOLTO su un conto a barriera assorbente. Attesa: P(pass) dell'ensemble >= " + "mediana delle P(pass) per ancora, e soprattutto SPARISCE la lotteria (la banda di " + "P(pass) fra ancore si comprime). E il conto funded e' a $100k, dove la banda del " + "punto (iii) NON morde: il posto dove l'ensemble e' inutile e quello dove e' " + "ineseguibile sono lo STESSO posto, e sono entrambi il conto da $635."), +] + + +def sezione0() -> None: + sez("Β§0 β€” ATTESE A PRIORI (dichiarate PRIMA di misurare; il verdetto su ciascuna e' in Β§9)") + for k, t in ATTESE: + print(f" [{k}] {t}\n") + print(" Criterio N_max DICHIARATO ORA, prima di guardare i numeri (Β§4):") + print(" l'ensemble serve a UNA cosa: comprimere la dispersione fra realizzazioni. Quindi") + print(" retention(K,C) = [sd_band(1,C) - sd_band(K,C)] / [sd_ideale(1) - sd_ideale(K)]") + print(" con sd = deviazione standard dello Sharpe FULL fra TUTTE le rotazioni della") + print(" famiglia K (nessuna selezione), banda = pavimento vero del venue a capitale C.") + print(" N_max(C) = il massimo K in {2,3,4,6,8,12} con retention >= 0.50.") + print(" K=24 ha UNA sola rotazione -> riportato a parte, fuori dal criterio.") + + +# =========================================================================== +# PRIMITIVE β€” target orari, ensemble, valutazione ideale e BANDATA +# =========================================================================== +@lru_cache(maxsize=8) +def px(asset: str) -> np.ndarray: + return r07.get1h(asset)["close"].values.astype(float) + + +@lru_cache(maxsize=8) +def ret(asset: str) -> np.ndarray: + return al.simple_returns(px(asset)) + + +@lru_cache(maxsize=8) +def idx1h(asset: str) -> pd.DatetimeIndex: + return pd.DatetimeIndex(pd.to_datetime(r07.get1h(asset)["datetime"], utc=True)) + + +@lru_cache(maxsize=256) +def tgt(asset: str, hs: tuple) -> np.ndarray: + """Target dell'ensemble sulle ancore `hs`: media dei target orari delle tranche. + `r07.hourly_target` e' RIUSATA (merge_asof backward su epoca ms esplicita).""" + return np.mean([np.asarray(r07.hourly_target(asset, h)) for h in hs], axis=0) + + +def eval_ideal(asset: str, hs: tuple) -> dict: + """Valutazione IDEALE (nessun pavimento): riusa al.eval_weights, poi decompone.""" + df = r07.get1h(asset) + ev = al.eval_weights(df, tgt(asset, hs), fee_side=FEE) + t = tgt(asset, hs) + pos = np.zeros(len(t)); pos[1:] = t[:-1] + gross = pos * ret(asset) + turn = np.abs(np.diff(pos, prepend=0.0)) + net = gross - FEE * turn; net[0] = 0.0 + assert np.max(np.abs(net - ev["net"])) < 1e-15, "decomposizione != eval_weights" + return dict(pos=pos, gross=gross, turn=turn, net=net, idx=ev["idx"], + turn_y=ev["turnover_per_year"]) + + +@lru_cache(maxsize=256) +def book_ideal(hs: tuple) -> dict: + """Book 50/50 BTC+ETH IDEALE sull'orario: serie oraria + daily + turnover.""" + E = {a: eval_ideal(a, hs) for a in ASSETS} + S = {a: pd.Series(E[a]["net"], index=E[a]["idx"]) for a in ASSETS} + G = {a: pd.Series(E[a]["gross"], index=E[a]["idx"]) for a in ASSETS} + P = {a: pd.Series(E[a]["pos"], index=E[a]["idx"]) for a in ASSETS} + J = pd.concat(S, axis=1, join="inner").fillna(0.0) + JG = pd.concat(G, axis=1, join="inner").fillna(0.0) + JP = pd.concat(P, axis=1, join="inner").fillna(0.0) + h_net = 0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]] + h_gross = 0.5 * JG[ASSETS[0]] + 0.5 * JG[ASSETS[1]] + return dict(h_net=h_net, h_gross=h_gross, pos=JP, daily=al._to_daily(h_net), + turn_y=sum(0.5 * E[a]["turn_y"] for a in ASSETS)) + + +def banded(prices: np.ndarray, target: np.ndarray, capital: float, + min_usd: float = MIN_ORDER_USD, step: float | None = None) -> dict: + """Path ESEGUITO sotto il pavimento del venue. + + Modella `src/live/book.build_book_order` + `deribit.notional_to_amount`: + - `abs(delta) < min_usd` -> nessun ordine (banda sul NOZIONALE, non sulla frazione); + - se `step` e' dato (contract step del perp lineare, in base-coin) il delta eseguito viene + quantizzato allo step e CLAMPATO al minimo -> la posizione atterra su una griglia. + `target[i]` e' la frazione di `capital` desiderata alla barra i (stessa convenzione di + `r07.smallcap_net`, contro cui questa funzione e' validata bit-exact in Β§1). + + Ottimizzazione: la condizione puo' scattare solo dove il target CAMBIA (fra due cambi il + target e' costante, quindi se non e' scattata non scattera'). Si itera sui punti di cambio. + """ + n = len(target) + chg = np.empty(n, dtype=bool) + chg[0] = True + chg[1:] = target[1:] != target[:-1] + ci = np.flatnonzero(chg) + vals = np.empty(len(ci)) + cur = 0.0 + n_ord = 0 + notion = 0.0 + for k in range(len(ci)): + i = ci[k] + d = (target[i] - cur) * capital + if abs(d) >= min_usd: + if step is None: + cur = target[i] + notion += abs(d) + else: + u = abs(d) / prices[i] + q = np.round(u / step) * step + if q < step: + q = step + ex = np.sign(d) * q * prices[i] + cur = cur + ex / capital + notion += abs(ex) + n_ord += 1 + vals[k] = cur + held = np.repeat(vals, np.diff(np.append(ci, n))) + return dict(held=held, n_ord=n_ord, notional=notion, n_chg=len(ci)) + + +def eval_banded(asset: str, hs: tuple, capital: float, step: float | None = None) -> dict: + """Book di UN asset sotto banda. `capital` = capitale ALLOCATO A QUELL'ASSET.""" + b = banded(px(asset), tgt(asset, hs), capital, MIN_ORDER_USD, step) + held = b["held"] + pos = np.zeros(len(held)); pos[1:] = held[:-1] + turn = np.abs(np.diff(pos, prepend=0.0)) + net = pos * ret(asset) - FEE * turn + net[0] = 0.0 + return dict(net=net, pos=pos, turn=turn, n_ord=b["n_ord"], n_chg=b["n_chg"], + notional=b["notional"], idx=idx1h(asset)) + + +def book_banded(hs: tuple, capital: float, step_by_asset: dict | None = None) -> dict: + """Book 50/50 sotto banda. `capital` = capitale TOTALE del book TP01 (meta' per asset).""" + E = {} + for a in ASSETS: + st = step_by_asset.get(a) if step_by_asset else None + E[a] = eval_banded(a, hs, capital * 0.5, st) + S = {a: pd.Series(E[a]["net"], index=E[a]["idx"]) for a in ASSETS} + J = pd.concat(S, axis=1, join="inner").fillna(0.0) + h = 0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]] + yrs = (h.index[-1] - h.index[0]) / pd.Timedelta("365.25D") + return dict(daily=al._to_daily(h), n_ord=sum(E[a]["n_ord"] for a in ASSETS), + ord_y=sum(E[a]["n_ord"] for a in ASSETS) / yrs, + fee_y=sum(FEE * E[a]["turn"].sum() for a in ASSETS) / 2 / yrs, + notional_y=sum(E[a]["notional"] for a in ASSETS) / yrs, E=E) + + +def drift(s: pd.Series) -> float: + """Drift annuo geometrico (CAGR) di una serie di ritorni giornalieri.""" + v = np.asarray(s.dropna().values, float) + eq = float(np.prod(1.0 + v)) + yrs = len(v) / 365.25 + return eq ** (1 / yrs) - 1 if eq > 0 and yrs > 0 else -1.0 + + +def vol(s: pd.Series) -> float: + return float(np.std(np.asarray(s.dropna().values, float)) * np.sqrt(365.25)) + + +def m3(s: pd.Series) -> tuple: + return (al._sh(s), al._sh(s[s.index < HOLDOUT]), al._sh(s[s.index >= HOLDOUT])) + + +def pctl_of(v: np.ndarray, x: float) -> float: + return float((v < x).mean() + 0.5 * (v == x).mean()) * 100 + + +# =========================================================================== +# Β§1 β€” REPLICA. Nessun delta prima che questa sezione passi. +# =========================================================================== +def sezione1() -> dict: + sez("Β§1 β€” REPLICA DI CONTROLLO (il 02/07 PRIMA di ogni delta) + pavimento vero del venue") + + r07.sanity_h0() + r07.causality_guards() # prefix-recompute daily + troncamento 1h, per TUTTE le ancore + + # (a) la funzione `banded` di questo script contro `r07.smallcap_net` (codice del 02/07) + print("\n (a) `banded()` di questo script vs `r07.smallcap_net` del 02/07 (bit-exact):") + for a in ASSETS: + df = r07.get1h(a) + t = 0.5 * tgt(a, (0,)) + ref, ntr = r07.smallcap_net(df, t, 600.0) + b = banded(px(a), t, 600.0, 5.0, None) + held = b["held"] + pos = np.zeros(len(held)); pos[1:] = held[:-1] + net = pos * ret(a) - FEE * np.abs(np.diff(pos, prepend=0.0)); net[0] = 0.0 + d = float(np.max(np.abs(net - ref.values))) + assert d == 0.0, f"banded != smallcap_net su {a}: {d:.3e}" + assert b["n_ord"] == ntr, f"n_ordini {b['n_ord']} != {ntr}" + print(f" {a}: max|dif| = {d:.1e} ordini {b['n_ord']} == {ntr} OK " + f"(punti di cambio del target: {b['n_chg']:,} -> la compressione e' esatta)") + + # (b) K=4 == EW di 4 book ancorati (il claim "diff 1.4e-17" del 02/07) + K4 = (0, 6, 12, 18) + ens = book_ideal(K4)["daily"] + ew = sum(book_ideal((h,))["h_net"] for h in K4) / 4.0 + ew_d = al._to_daily(ew) + J = pd.concat({"e": ens, "w": ew_d}, axis=1, join="inner") + d = float(np.max(np.abs(J["e"] - J["w"]))) + print(f"\n (b) K=4 == EW di 4 book ancorati (claim 02/07 'diff 1.4e-17'):") + print(f" sull'ORARIO la posizione e' identica per costruzione; sul DAILY il compounding") + print(f" intra-giorno non e' lineare -> max|dif| = {d:.2e} su {len(J):,} giorni.") + + # (c) i numeri del 02/07, con la deriva del dato dichiarata + singles = {h: book_ideal((h,))["daily"] for h in HOURS} + shf = np.array([al._sh(singles[h]) for h in HOURS]) + shi = np.array([al._sh(singles[h][singles[h].index < HOLDOUT]) for h in HOURS]) + shh = np.array([al._sh(singles[h][singles[h].index >= HOLDOUT]) for h in HOURS]) + dd = np.array([al._dd_ret(singles[h]) for h in HOURS]) + e4 = book_ideal(K4)["daily"] + rot4 = [book_ideal(hs)["daily"] for hs in fam_rotations(4)] + h4 = np.array([al._sh(s[s.index >= HOLDOUT]) for s in rot4]) + print(f"\n (c) i numeri pubblicati il 02/07, ri-misurati oggi (il dato e' cresciuto di ~7") + print(f" settimane e `data/raw/` non e' versionato β€” la deriva si dichiara, non si nasconde):") + print(f" {'grandezza':<44} {'02/07':>16} {'oggi':>16}") + print(f" {'HOLD mediano fra le 24 ancore':<44} {'0.04':>16} {np.median(shh):>16.3f}") + print(f" {'HOLD banda fra le 24 ancore':<44} {'[-0.13,+0.30]':>16} " + f"{'[%+.2f,%+.2f]' % (shh.min(), shh.max()):>16}") + print(f" {'HOLD canonico h=0':<44} {'0.31 (98 pctl)':>16} " + f"{'%.3f (%.0f pctl)' % (shh[0], pctl_of(shh, shh[0])):>16}") + print(f" {'IS h=0 -> K=4':<44} {'1.49 -> 1.56':>16} " + f"{'%.2f -> %.2f' % (shi[0], al._sh(e4[e4.index < HOLDOUT])):>16}") + print(f" {'HOLD banda a K=4 (6 rotazioni)':<44} {'[-0.01,+0.13]':>16} " + f"{'[%+.2f,%+.2f]' % (h4.min(), h4.max()):>16}") + print(f" {'maxDD mediano fra le singole':<44} {'12.6%':>16} {np.median(dd):>15.1%}") + print(f" {'turnover/anno del book':<44} {'~8.3':>16} " + f"{book_ideal(K4)['turn_y']:>16.1f}") + pk = float((h4[:, None] <= shh[0]).mean()) + print(f" {'P(K=4 <= h=0) sull hold-out':<44} {'0.18 n.s.':>16} {pk:>16.2f}") + print(" -> la STRUTTURA replica (K=4 == EW, turnover invariato, banda che si comprime);") + print(" i LIVELLI si sono mossi col dato. E' la deriva attesa, non un disaccordo.") + + # (d) il pavimento VERO del venue, derivato dal codice di produzione + print("\n (d) IL PAVIMENTO, derivato da `src/live/book` + `src/live/deribit` (non ridichiarato):") + import json as _json + cfg = _json.loads((ROOT / "config" / "live.json").read_text()) + mo = float(cfg["min_order_usd"]) + assert mo == MIN_ORDER_USD, f"config min_order_usd={mo} != {MIN_ORDER_USD} dichiarato qui" + steps = {} + for a in ASSETS: + inst = f"{a}_USDC-PERPETUAL" + steps[a] = float(_CONTRACT[inst]["step"]) + p_now = px(a)[-1] + print(f" {inst:<22} step {steps[a]:g} {a} -> a ${p_now:,.0f} il lotto vale " + f"${steps[a]*p_now:6.2f} (banda config ${mo:.0f})") + print(f" -> pavimento effettivo per ordine = max($5, quantizzazione al lotto).") + print(f" Su BTC il LOTTO e' piu' grosso della banda: e' lui a comandare.") + print(f" ⚠️ `build_book_order` banda `abs(delta)` sulla posizione NETTA e manda UN ordine") + print(f" per asset per giro. Questo e' il fatto che riapre la domanda del 02/07 (Β§4).") + print(f" {clock()}") + return dict(singles=singles, shf=shf, shi=shi, shh=shh, dd=dd, steps=steps) + + +# =========================================================================== +# Β§2 β€” (i) l'ensemble contro l'ancora MEDIANA +# =========================================================================== +def sezione2(S1: dict) -> dict: + sez("Β§2 β€” (i) L'ENSEMBLE CONTRO L'ANCORA MEDIANA (e l'algebra che lo governa)") + + # (a) il teorema A1, verificato numericamente sull'ORARIO + print(" (a) [A1] verifica ALGEBRICA sull'orario (non un'approssimazione: un'identita'):") + for a in ASSETS: + E24 = eval_ideal(a, HOURS) + g_mean = np.mean([eval_ideal(a, (h,))["gross"] for h in HOURS], axis=0) + t_mean = np.mean([eval_ideal(a, (h,))["turn"] for h in HOURS], axis=0) + dg = float(np.max(np.abs(E24["gross"] - g_mean))) + te, tm = float(E24["turn"].sum()), float(t_mean.sum()) + print(f" {a}: max|lordo_ens - media(lordi)| = {dg:.2e} -> IDENTITA'") + print(f" turnover TOTALE ensemble {te:.4f} vs MEDIA delle ancore {tm:.4f} " + f"-> sub-additivita' {'OK' if te <= tm + 1e-9 else 'VIOLATA'} " + f"({te/tm-1:+.3%})") + # ⚠️ ERRORE CATTURATO IN SESSIONE: la prima stesura confrontava `turnover_per_year`, che + # `al.eval_weights` ARROTONDA a 1 decimale -> stampava "9.40 vs 9.38" accanto a una + # disuguaglianza che il conto esatto rispetta. Una quantita' arrotondata non si mette + # accanto a una disuguaglianza stretta: sembra violata proprio dove e' piu' stretta. + print(" => l'ensemble NON e' una scommessa diversa: e' la STESSA scommessa, spalmata.") + print(" Il drift non puo' cambiare se non per il fee-netting; la vol si', perche' la") + print(" media di N serie correlate ma non identiche ha meno varianza di ciascuna.") + + # (b) la tabella delle famiglie + print("\n (b) FAMIGLIE (tutte le rotazioni, nessuna selezione; book 50/50 ideale):") + print(f" {'K':>3} {'rot':>4} {'ShFULL med':>11} {'sd':>6} {'ShHOLD med':>11} {'sd':>6} " + f"{'drift med':>10} {'vol med':>8} {'maxDD med':>10} {'turn/y':>7}") + fam = {} + for K, nrot in FAM_TP.items(): + rots = fam_rotations(K) + assert len(rots) == nrot + D = [book_ideal(hs) for hs in rots] + f = np.array([al._sh(d["daily"]) for d in D]) + h = np.array([al._sh(d["daily"][d["daily"].index >= HOLDOUT]) for d in D]) + dr = np.array([drift(d["daily"]) for d in D]) + vo = np.array([vol(d["daily"]) for d in D]) + ddv = np.array([al._dd_ret(d["daily"]) for d in D]) + ty = np.array([d["turn_y"] for d in D]) + fam[K] = dict(f=f, h=h, dr=dr, vo=vo, dd=ddv, ty=ty, rots=rots) + print(f" {K:>3} {nrot:>4} {np.median(f):>11.3f} {f.std():>6.3f} " + f"{np.median(h):>11.3f} {h.std():>6.3f} {np.median(dr):>9.2%} " + f"{np.median(vo):>7.2%} {np.median(ddv):>9.1%} {np.mean(ty):>7.1f}") + + # (c) il confronto che risponde a (i): ensemble pieno vs ancora mediana + e24 = book_ideal(HOURS)["daily"] + med_h = int(np.argsort(S1["shf"])[len(S1["shf"]) // 2]) + med = S1["singles"][med_h] + print(f"\n (c) RISPOSTA A (i) β€” ensemble K=24 contro l'ancora MEDIANA (h={med_h}, la mediana") + print(f" per Sharpe FULL fra le 24) e contro la MEDIA delle 24:") + print(f" {'':<26} {'ShFULL':>8} {'ShIS':>8} {'ShHOLD':>8} {'drift':>8} {'vol':>8} {'maxDD':>8}") + for lbl, s in (("ancora mediana", med), ("ensemble K=24", e24)): + a, b, c = m3(s) + print(f" {lbl:<26} {a:>8.3f} {b:>8.3f} {c:>8.3f} {drift(s):>7.2%} " + f"{vol(s):>7.2%} {al._dd_ret(s):>7.1%}") + da, db, dc = m3(e24) + ma, mb, mc = m3(med) + print(f" {'DELTA (ens - mediana)':<26} {da-ma:>+8.3f} {db-mb:>+8.3f} {dc-mc:>+8.3f} " + f"{drift(e24)-drift(med):>+7.2%} {vol(e24)-vol(med):>+7.2%} " + f"{al._dd_ret(e24)-al._dd_ret(med):>+7.1%}") + f1 = fam[1] + print(f"\n Scomposizione del guadagno (ensemble vs MEDIA delle 24, che e' il confronto") + print(f" algebricamente pulito): drift {drift(e24):.2%} vs media ancore " + f"{np.mean(f1['dr']):.2%} (delta {drift(e24)-np.mean(f1['dr']):+.2%} = fee-netting);") + print(f" vol {vol(e24):.2%} vs media ancore {np.mean(f1['vo']):.2%} " + f"({vol(e24)/np.mean(f1['vo'])-1:+.1%}) <- QUI sta tutto il guadagno di Sharpe.") + print(f"\n COMPRESSIONE DELLA DISPERSIONE (e' questo che l'ensemble compra):") + print(f" {'K':>3} {'sd(ShFULL)':>11} {'sd(ShHOLD)':>11} {'banda ShHOLD':>18} " + f"{'sd/sd(K=1)':>11}") + for K in FAM_TP: + d = fam[K] + print(f" {K:>3} {d['f'].std():>11.3f} {d['h'].std():>11.3f} " + f"{'[%+.2f,%+.2f]' % (d['h'].min(), d['h'].max()):>18} " + f"{(d['h'].std()/fam[1]['h'].std() if fam[1]['h'].std() else 0):>11.2f}") + print(f" (a K=24 la dispersione e' ZERO per definizione: c'e' una sola realizzazione β€”") + print(f" ed e' esattamente il punto: l'ensemble non riduce l'incertezza, la ELIMINA.)") + print(f" {clock()}") + return dict(fam=fam, e24=e24, med_h=med_h) + + +# =========================================================================== +# Β§3 β€” (ii) l'ensemble contro l'ancora CANONICA che gira oggi +# =========================================================================== +def sezione3(S1: dict, S2: dict) -> None: + sez("Β§3 β€” (ii) L'ENSEMBLE CONTRO L'ANCORA CANONICA (h=0 UTC, quella che gira con soldi veri)") + can = S1["singles"][0] + e24 = S2["e24"] + print(f" {'':<26} {'ShFULL':>8} {'ShIS':>8} {'ShHOLD':>8} {'drift':>8} {'vol':>8} {'maxDD':>8}") + for lbl, s in (("CANONICA h=0 (oggi)", can), ("ensemble K=24", e24)): + a, b, c = m3(s) + print(f" {lbl:<26} {a:>8.3f} {b:>8.3f} {c:>8.3f} {drift(s):>7.2%} " + f"{vol(s):>7.2%} {al._dd_ret(s):>7.1%}") + ca, cb, cc = m3(can); ea, eb, ec = m3(e24) + print(f" {'DELTA (ens - canonica)':<26} {ea-ca:>+8.3f} {eb-cb:>+8.3f} {ec-cc:>+8.3f} " + f"{drift(e24)-drift(can):>+7.2%} {vol(e24)-vol(can):>+7.2%} " + f"{al._dd_ret(e24)-al._dd_ret(can):>+7.1%}") + print(f"\n Percentile della CANONICA fra le 24 ancore: FULL {pctl_of(S1['shf'], S1['shf'][0]):.0f}Β°" + f" IS {pctl_of(S1['shi'], S1['shi'][0]):.0f}Β°" + f" HOLD {pctl_of(S1['shh'], S1['shh'][0]):.0f}Β°" + f" maxDD {pctl_of(S1['dd'], S1['dd'][0]):.0f}Β°") + print(f" Percentile dell'ENSEMBLE fra le 24: FULL {pctl_of(S1['shf'], ea):.0f}Β°" + f" IS {pctl_of(S1['shi'], eb):.0f}Β° HOLD {pctl_of(S1['shh'], ec):.0f}Β°") + print("\n πŸ“Œ LA LETTURA, ed e' il punto di tutto il filone:") + print(" i due numeri NON sono due stime della stessa cosa. Il numero della canonica e' il") + print(" risultato di UNA estrazione gia' avvenuta e gia' nota; quello dell'ensemble e' cio'") + print(" che si ottiene SENZA estrarre. Chi confronta 'canonica vs ensemble' in-sample sta") + print(" confrontando il massimo osservato con la media, e la media perde sempre.") + print(" La domanda decidibile e' un'altra: DOMANI, quale dei due si otterra'?") + print(f" - tenendo la canonica: una estrazione dalla banda [{S1['shh'].min():+.2f}," + f"{S1['shh'].max():+.2f}] di hold-out, con mediana {np.median(S1['shh']):+.2f}" + f" β€” il {pctl_of(S1['shh'], S1['shh'][0]):.0f}Β° pctl di ieri non si eredita;") + print(f" - tenendo l'ensemble: {ec:+.2f}, senza estrazione.") + print(" Cioe': l'ensemble non compra ritorno, compra il fatto di sapere gia' cosa avrai.") + print(" E' la stessa cosa che il 26/07 disse del limit-TP di SKH01: *converte una lotteria") + print(" in certezza piu' che aggiungere ritorno*.") + print(f" {clock()}") + + +# =========================================================================== +# Β§4 β€” (iii) ESEGUIBILITA': la curva N_max(capitale) +# =========================================================================== +def sezione4(S1: dict, S2: dict) -> dict: + sez("Β§4 β€” (iii) A QUALE CAPITALE L'ENSEMBLE DIVENTA ESEGUIBILE (pavimento vero del venue)") + + print(" (a) LA TAGLIA DEGLI ORDINI. Il fatto strutturale prima dei numeri:") + print(" il tranching NON moltiplica gli ordini per K. `build_book_order` banda la") + print(" posizione NETTA e ne manda UNO per asset per giro: K cambia la FORMA del") + print(" sentiero di target, non il numero di ordini. Il numero di ordini lo decide la BANDA.") + print(f"\n {'K':>3} {'cambi di target/anno':>21} {'|dNozionale| mediano a $635':>28} " + f"{'p75':>10} {'quota sopra $5':>15}") + capass = 635.0 * 0.5 + for K in (1, 2, 4, 12, 24): + hs = fam_rotations(K)[0] + n_chg = 0; alld = [] + for a in ASSETS: + t = tgt(a, hs) + d = np.abs(np.diff(t, prepend=0.0)) * capass + d = d[d > 1e-12] + n_chg += len(d); alld.append(d) + D = np.concatenate(alld) + yrs = (idx1h("BTC")[-1] - idx1h("BTC")[0]) / pd.Timedelta("365.25D") + print(f" {K:>3} {n_chg/yrs:>21,.0f} {np.median(D):>27.2f}$ {np.percentile(D,75):>9.2f}$ " + f"{float((D >= MIN_ORDER_USD).mean()):>14.1%}") + print(f"\n ⚠️ IL FATTO CHE DECIDE, e non e' su K: a $635 la quota TP01 di UN asset e'") + print(f" ${capass:.0f}, l'esposizione media di TP01 e' ~{np.mean(tgt('BTC',(0,))):.3f}x ->") + print(f" il nozionale TENUTO e' ~${capass*np.mean(tgt('BTC',(0,))):.0f}. La banda da $5 e'") + print(f" il {5/(capass*np.mean(tgt('BTC',(0,)))):.0%} della posizione, e il lotto BTC " + f"(${S1['steps']['BTC']*px('BTC')[-1]:.1f}) e' il " + f"{S1['steps']['BTC']*px('BTC')[-1]/(capass*np.mean(tgt('BTC',(0,)))):.0%}.") + print(f" Gia' K=1 esegue solo una minoranza dei propri ribilanci. Non c'e' K da degradare.") + + # (b) curva bandata + print("\n (b) IL PATH ESEGUITO, per capitale (banda $5 + quantizzazione al lotto del venue):") + print(f" {'cap':>8} {'K':>3} {'ShFULL':>8} {'haircut':>8} {'ordini/anno':>12} " + f"{'fee $/anno':>11} {'corr(eseguito,ideale)':>22}") + band = {} + for C in CAPITALI: + for K in (1, 2, 4, 24): + hs = fam_rotations(K)[0] + bb = book_banded(hs, C, S1["steps"]) + ii = book_ideal(hs)["daily"] + cr = [] + for a in ASSETS: + cr.append(float(np.corrcoef(bb["E"][a]["pos"], tgt(a, hs))[0, 1])) + band[(C, K)] = dict(sh=al._sh(bb["daily"]), hc=al._sh(ii) - al._sh(bb["daily"]), + ord_y=bb["ord_y"], fee_y=bb["fee_y"] * C, corr=float(np.mean(cr)), + daily=bb["daily"]) + b = band[(C, K)] + print(f" {C:>8,.0f} {K:>3} {b['sh']:>8.3f} {b['hc']:>+8.3f} {b['ord_y']:>12.0f} " + f"{b['fee_y']:>11.2f} {b['corr']:>22.4f}") + print(" (haircut = Sharpe ideale - Sharpe eseguito; fee $/anno = fee proporzionale sul") + print(" nozionale realmente scambiato, NON un costo per ordine β€” su Deribit non esiste)") + + # (c) il criterio N_max dichiarato in Β§0 + print("\n (c) N_max(capitale) β€” criterio DICHIARATO IN Β§0, applicato senza ritocchi:") + ideal_sd = {K: S2["fam"][K]["f"].std() for K in FAM_TP} + passed: dict = {} + print(f" {'cap':>8} " + " ".join(f"{'K='+str(K):>9}" for K in (2, 3, 4, 6, 8, 12)) + + f" {'N_max':>7}") + nmax = {} + for C in CAPITALI: + sd_b = {} + for K in (1, 2, 3, 4, 6, 8, 12): + v = [] + for hs in fam_rotations(K): + bb = book_banded(hs, C, S1["steps"]) + v.append(al._sh(bb["daily"])) + sd_b[K] = float(np.std(v)) + cells, best, npass = [], 1, 0 + for K in (2, 3, 4, 6, 8, 12): + den = ideal_sd[1] - ideal_sd[K] + ret_ = (sd_b[1] - sd_b[K]) / den if den > 1e-12 else 0.0 + cells.append(f"{ret_:>9.2f}") + if ret_ >= 0.50: + best = K + npass += 1 + nmax[C] = best + passed[C] = npass + print(f" {C:>8,.0f} " + " ".join(cells) + f" {best:>7}") + print(" (retention = quota della compressione IDEALE di dispersione che il path ESEGUITO") + print(" conserva; 1.00 = la banda non toglie niente, <=0 = il tranching e' invisibile)") + + print("\n ⚠️ IL CRITERIO SATURA, e va detto prima di leggere la colonna N_max.") + print(f" Celle che passano la soglia 0.50: " + + ", ".join(f"${c:,.0f} -> {passed[c]}/6" for c in CAPITALI) + ".") + print(" Un gate che un candidato qualsiasi passa 6 volte su 6 a OGNI capitale non sta") + print(" misurando niente (regola 07/08: un gate si valida contando quante volte lo passa") + print(" un candidato a caso). Il criterio era scritto per catturare un degrado che NON") + print(" ESISTE: la banda del venue non degrada il tranching nemmeno a $635.") + print(" -> [A5] REFUTATA nella sua seconda meta'. La risposta a (iii) non e' un capitale:") + print(" e' che l'eseguibilita' NON e' il vincolo. (7a volta nell'ondata.)") + + print("\n (d) IL VINCOLO OPERATIVO VERO, che non e' il pavimento ma il FEED.") + print(" TP01 all'ancora h decide sulla barra 24h che CHIUDE alle h:00 di oggi: per h>0") + print(" servono barre di oggi. Il feed certificato si ricostruisce una volta al giorno") + print(" (~00:30 UTC) -> 23 ancore su 24 richiedono il feed 5m FRESCO, cioe' `fresh_5m`,") + print(" lo stesso che il 26/07 e' risultato ricadere IN SILENZIO sul certificato e che") + print(" il 29/07 ha fallito 6 giri su 8 dopo un riavvio. Oggi quel guasto tocca il solo") + print(" SKH01 ed e' sorvegliato (`skh_feed_max_age_min`=30). Un ensemble d'ancora ci") + print(" metterebbe sotto anche TP01, che oggi non dipende da quel path.") + print(" Non e' un argomento definitivo (l'allerta esiste gia'), ma e' il costo VERO del") + print(" tranching su questo libro: non ordini, non fee β€” una dipendenza di feed in piu'.") + print(" E' l'obiezione (3) del 02/07, l'unica delle sue tre che sopravvive intatta.") + print(f" {clock()}") + return dict(band=band, nmax=nmax, passed=passed) + + +# =========================================================================== +# Β§5 β€” IL FUNDING: l'unico canale per cui l'ensemble potrebbe GUADAGNARE +# =========================================================================== +def sezione5(S1: dict) -> dict: + sez("Β§5 β€” IL FUNDING (l'unico canale di GUADAGNO possibile) β€” e perche' e' chiuso per algebra") + F = {} + for instr, (start, asset, kind) in fd.INSTR.items(): + d = fd.fetch_funding(instr, start).copy() + d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True) + F[instr] = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt") + rate = fd.build_rate(F) + print(f" Tassi da `r0822d_funding` (cache su disco, nessuna rete). Copertura: " + f"{rate['BTC']['series'].index.min().date()} -> {rate['BTC']['series'].index.max().date()}") + + def f_hourly(a: str) -> np.ndarray: + """Tasso orario allineato all'indice 1h: il record etichettato T copre (T-1h, T].""" + ii = idx1h(a) + t_end = fd.to_ms(ii) + MSH + c = fd.cumF(rate[a], t_end) - fd.cumF(rate[a], fd.to_ms(ii)) + return c + + # (a) IL TEOREMA A4 + print("\n (a) [A4] il funding pagato in un'ora e' pos*f, LINEARE in pos:") + for a in ASSETS: + fh = f_hourly(a) + p24 = eval_ideal(a, HOURS)["pos"] + pm = np.mean([eval_ideal(a, (h,))["pos"] for h in HOURS], axis=0) + d = float(np.max(np.abs(p24 * fh - pm * fh))) + print(f" {a}: max|funding_ens - media(funding delle 24 ancore)| = {d:.2e} -> IDENTITA'") + print(" => l'ensemble NON puo' pagare meno funding della MEDIA delle ancore. Mai.") + print(" Il canale di guadagno sperato e' chiuso ALGEBRICAMENTE, non 'per piccolezza'.") + + # (b) il rapporto condizionale: si attenua mediando sulle 24 ore? + print("\n (b) e il rapporto CONDIZIONALE (22/08: esposizione e funding sono correlati, il") + print(" tasso pagato davvero e' 1,86-2,55x l'incondizionato per TP01) si attenua?") + print(f" {'K':>3} " + f"{'drag %/anno':>13} {'rapporto cond.':>16} " + f"{'corr(pos,f) oraria':>20}") + out = {} + cov = {a: (idx1h(a) >= rate[a]["series"].index.min()) for a in ASSETS} + for K in (1, 2, 4, 12, 24): + hs = fam_rotations(K)[0] + dr, rat, cc = [], [], [] + for a in ASSETS: + fh = f_hourly(a)[cov[a]] + p = eval_ideal(a, hs)["pos"][cov[a]] + paid = float(np.mean(p * fh)) * 24 * 365.25 + base = float(np.mean(p)) * float(np.mean(fh)) * 24 * 365.25 + dr.append(paid); rat.append(paid / base if base else np.nan) + cc.append(float(np.corrcoef(p, fh)[0, 1])) + out[K] = dict(drag=float(np.mean(dr)), ratio=float(np.mean(rat))) + print(f" {K:>3} {np.mean(dr):>12.3%} {np.mean(rat):>15.2f}x {np.mean(cc):>20.4f}") + print(" -> il rapporto e' ESSO STESSO una media di rapporti: mediare sull'ora del giorno") + print(" non tocca una correlazione che vive alla scala del REGIME (settimane/mesi).") + + # (c) replica del numero del 22/08 per una strada diversa + tp_noF, _ = fd.tp01_canonical(None) + tp_F, _ = fd.tp01_canonical(rate) + d22 = drift(tp_noF) - drift(tp_F) + p_h = eval_ideal("BTC", (0,))["pos"][cov["BTC"]] + print(f"\n (c) REPLICA del numero del 22/08 per una strada DIVERSA (griglia oraria invece che") + print(f" giornaliera, ancora canonica): drag di TP01 = {out[1]['drag']:.2%}/anno") + print(f" contro {d22:.2%}/anno rifacendo il conto con `fd.tp01_canonical` (griglia daily,") + print(f" lente del 22/08, che pubblico' 2,02%). Due path indipendenti, stesso ordine.") + + # (d) e sul BANDATO (dove la linearita' si rompe)? + print("\n (d) e sul path ESEGUITO (dove la banda rompe la linearita')?") + print(f" {'cap':>8} {'K':>3} {'drag %/anno':>13} {'vs ideale':>11}") + for C in (635.0, 5000.0, 50000.0): + for K in (1, 24): + hs = fam_rotations(K)[0] + dr = [] + for a in ASSETS: + e = eval_banded(a, hs, C * 0.5, S1["steps"][a]) + fh = f_hourly(a)[cov[a]] + dr.append(float(np.mean(e["pos"][cov[a]] * fh)) * 24 * 365.25) + print(f" {C:>8,.0f} {K:>3} {np.mean(dr):>12.3%} " + f"{np.mean(dr)-out[K]['drag']:>+10.3%}") + print(" -> la banda cambia il funding di briciole, e nel verso di chi tiene la posizione") + print(" piu' a lungo: non e' un canale, e' un residuo.") + print(f" {clock()}") + return dict(rate=rate, out=out, f_hourly=f_hourly, cov=cov) + + +# =========================================================================== +# Β§6 β€” SKH01: la stessa domanda su una griglia diversa, e piu' cara +# =========================================================================== +def skh_families() -> dict: + """Famiglie simmetriche per SKH01. Periodo congiunto 690m; griglia 5m -> 138 passi. + 138 = 2*3*23 -> K in {2,3,6,23} (e 46/69/138, fuori portata). Sulla griglia a 30m + dichiarata il 02/07 ci sono 23 offset e 23 e' PRIMO: o 1 o 23, niente in mezzo.""" + fams = {1: [(o,) for o in r02.OFFSETS]} + for K, shifts in ((2, range(0, 345, 30)), (3, range(0, 230, 30)), (6, range(0, 115, 30))): + cell = 690 // K + fams[K] = [tuple((s + cell * j) % 690 for j in range(K)) for s in shifts] + fams[23] = [tuple(r02.OFFSETS)] + return fams + + +_SKHD: dict = {} + + +def skh_daily(asset: str, off: int, lens: str) -> pd.Series: + """Serie daily di SKH01 a fase `off`. lens='canonical' (backtest) | 'hourly' (path del cron). + La cache pesante di `r02` viene SVUOTATA subito: 80+ offset x 2 asset di `ltf` non stanno + in memoria su questa macchina (2 core, ~3 GB liberi).""" + key = (asset, off, lens) + if key in _SKHD: + return _SKHD[key] + daily, m, ltf, ent = r02.run_asset(asset, off) + if lens == "canonical": + s = daily + else: + df5 = r02.get5m(asset) + ts5 = df5["timestamp"].values.astype(np.int64) + r02.MS5 + c5 = df5["close"].values.astype(float) + eq = r02.sim_equity(ltf, ent, "hourly", ts5, c5) + ss = pd.Series(eq, index=pd.DatetimeIndex(pd.to_datetime(ltf["datetime"], utc=True))) + s = ss.resample("1D").last().ffill().pct_change().dropna() + _SKHD[key] = s + r02._CACHE.pop((asset, off), None) # evict: tiene solo il daily + return s + + +def skh_book(offs: tuple, lens: str) -> pd.Series: + """Book SKH01 50/50 BTC+ETH, K tranche = media dei ritorni giornalieri (stessa convenzione + con cui il progetto combina BTC/ETH e gli sleeve: ribilanciamento a peso costante).""" + per = [] + for off in offs: + J = pd.concat({a: skh_daily(a, off, lens) for a in ASSETS}, axis=1, join="inner").fillna(0.0) + per.append(pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index)) + JJ = pd.concat({i: s for i, s in enumerate(per)}, axis=1, join="inner").fillna(0.0) + return pd.Series(JJ.mean(axis=1).values, index=JJ.index) + + +def sezione6() -> dict: + sez("Β§6 β€” SKH01: la stessa domanda su una griglia diversa (e 23 e' un numero primo)") + from src.portfolio.sleeves import _skyhook_returns + ref = _skyhook_returns() + mine = skh_book((0,), "canonical") + d = float(np.max(np.abs(mine.values - ref.reindex(mine.index).values))) + print(f" [SANITY] offset 0 == `sleeves._skyhook_returns()`: max|dif| = {d:.2e} su " + f"{len(mine):,} giorni") + assert d < 1e-12 + + fams = skh_families() + print(f"\n [A6] 690/30 = 23, PRIMO -> sulla griglia a 30m del 02/07 non esiste sotto-ensemble") + print(f" simmetrico: o K=1 o K=23. Le famiglie K in {{2,3,6}} esistono solo sulla griglia") + print(f" 5m (138 = 2*3*23) e usano offset che il 02/07 non aveva mai valutato.") + print(f" ⚠️ K=3 = {{0,230,460}}: 230 e 460 sono multipli esatti del bin LTF -> la griglia") + print(f" d'ESECUZIONE 230m e' la STESSA, cambia solo la fase del segnale 690m. Non e' un") + print(f" tranching d'esecuzione: e' un ensemble di SEGNALE. Va detto, non nascosto.") + + res = {} + for lens in ("canonical", "hourly"): + print(f"\n LENTE '{lens}'" + (" (backtest, fill al livello)" if lens == "canonical" + else " (path del cron orario: fill al close 5m del prossimo multiplo orario)")) + print(f" {'K':>3} {'rot':>4} {'ShFULL med':>11} {'sd':>6} {'ShHOLD med':>11} {'sd':>6} " + f"{'drift med':>10} {'maxDD med':>10}") + R = {} + for K in (1, 2, 3, 6, 23): + S = [skh_book(o, lens) for o in fams[K]] + f = np.array([al._sh(s) for s in S]) + h = np.array([al._sh(s[s.index >= HOLDOUT]) for s in S]) + dr = np.array([drift(s) for s in S]) + dd = np.array([al._dd_ret(s) for s in S]) + R[K] = dict(f=f, h=h, dr=dr, dd=dd, S=S) + print(f" {K:>3} {len(S):>4} {np.median(f):>11.3f} {f.std():>6.3f} " + f"{np.median(h):>11.3f} {h.std():>6.3f} {np.median(dr):>9.2%} " + f"{np.median(dd):>9.1%}") + res[lens] = R + can = R[1]["f"][0] + print(f" canonico off=0: FULL {can:.3f} ({pctl_of(R[1]['f'], can):.0f}Β° pctl), " + f"HOLD {R[1]['h'][0]:.3f} ({pctl_of(R[1]['h'], R[1]['h'][0]):.0f}Β° pctl); " + f"ensemble K=23: FULL {R[23]['f'][0]:.3f}, HOLD {R[23]['h'][0]:.3f}") + print(f" {clock()}") + return res + + +# =========================================================================== +# Β§7 β€” I DUE SLEEVE INSIEME: il libro, e il netting che nessuno aveva contato +# =========================================================================== +def skh_pos_hourly(asset: str, off: int, ii: pd.DatetimeIndex) -> np.ndarray: + """Posizione oraria (+1/-1/0) di SKH01 a fase `off`, dalle boundary dei trade. + Riusa `fd.sim_equity_f` (che ritorna i trade con t_in/t_out in ms) β€” non riscrive il motore.""" + daily, m, ltf, ent = r02.run_asset(asset, off) + df5 = r02.get5m(asset) + ts5 = df5["timestamp"].values.astype(np.int64) + r02.MS5 + c5 = df5["close"].values.astype(float) + _, trades = fd.sim_equity_f(ltf, ent, "canonical", ts5, c5, None) + r02._CACHE.pop((asset, off), None) + t0 = fd.to_ms(ii) + p = np.zeros(len(ii)) + for tr in trades: + a = np.searchsorted(t0, tr["t_in"], side="left") + b = np.searchsorted(t0, tr["t_out"], side="right") + p[a:b] = tr["dir"] + return p + + +def sezione7(S1: dict, S4: dict, S6: dict) -> dict: + sez("Β§7 β€” I DUE SLEEVE INSIEME: il libro live, il NETTING, e dove il tranching vale davvero") + print(" Il libro esegue TP01 e SKH01 come UNA sola posizione netta per strumento (26/07, T1).") + print(" Quindi la banda da $5 non si applica al ribilancio di TP01 ma al NETTO β€” e i salti") + print(" grossi di SKH01 (che entra a 1,0x) trascinano dentro i micro-aggiustamenti di TP01,") + print(" gratis. Qui si incrociano le DUE griglie di tranching: TP01 (24 ancore orarie) e") + print(" SKH01 (23 fasi della griglia 230m/690m), che sono spazi di luck INDIPENDENTI.") + print(" ⚠️ LENTE: qui SKH01 e' mark-to-market ORARIO (posizione dai confini dei trade), non") + print(" compounding per trade come lo sleeve ufficiale -> i LIVELLI non sono confrontabili") + print(" coi numeri pubblicati. Si usano solo DIFFERENZE fra configurazioni, a lente identica.") + + ii = {a: idx1h(a) for a in ASSETS} + print(f" {clock()} costruzione delle 23 posizioni orarie di SKH01 (x2 asset)...") + SP = {a: {o: skh_pos_hourly(a, o, ii[a]) for o in r02.OFFSETS} for a in ASSETS} + sk = {1: {a: SP[a][0] for a in ASSETS}, + 23: {a: np.mean([SP[a][o] for o in r02.OFFSETS], axis=0) for a in ASSETS}} + + def run(ktp: int, ksk: int, C: float) -> dict: + hs = fam_rotations(ktp)[0] + nor, corr, nets, notion = 0, [], {}, 0.0 + for a in ASSETS: + tnet = W_ASSET * (W_TP * tgt(a, hs) + W_SKH * sk[ksk][a]) + b = banded(px(a), tnet, C, MIN_ORDER_USD, S1["steps"][a]) + held = b["held"] + pos = np.zeros(len(held)); pos[1:] = held[:-1] + net = pos * ret(a) - FEE * np.abs(np.diff(pos, prepend=0.0)); net[0] = 0.0 + nor += b["n_ord"]; notion += b["notional"] + corr.append(float(np.corrcoef(pos, tnet)[0, 1])) + nets[a] = pd.Series(net, index=ii[a]) + J = pd.concat(nets, axis=1, join="inner").fillna(0.0) + dly = al._to_daily(J[ASSETS[0]] + J[ASSETS[1]]) + yrs = (ii["BTC"][-1] - ii["BTC"][0]) / pd.Timedelta("365.25D") + return dict(ord_y=nor / yrs, corr=float(np.mean(corr)), sh=al._sh(dly), + hold=al._sh(dly[dly.index >= HOLDOUT]), dd=al._dd_ret(dly), + dly=dly, fee_y=FEE * notion / yrs) + + print(f"\n {'cap':>8} {'K_TP':>5} {'K_SKH':>6} {'ordini/anno':>12} {'fee $/anno':>11} " + f"{'fedelta':>9} {'ShFULL':>8} {'ShHOLD':>8} {'maxDD':>7}") + out = {} + for C in (635.0, 5000.0, 100000.0): + for ktp, ksk in ((1, 1), (24, 1), (1, 23), (24, 23)): + r = run(ktp, ksk, C) + out[(C, ktp, ksk)] = r + tag = " <- il libro di oggi" if (ktp, ksk) == (1, 1) else "" + print(f" {C:>8,.0f} {ktp:>5} {ksk:>6} {r['ord_y']:>12.0f} {r['fee_y']:>11.2f} " + f"{r['corr']:>9.4f} {r['sh']:>8.3f} {r['hold']:>8.3f} {r['dd']:>7.1%}{tag}") + print(" (fedelta' = corr fra la posizione ESEGUITA e il target ideale della stessa config)") + + print(f"\n πŸ“Œ LA SCOMPOSIZIONE CHE RISPONDE ALLA DOMANDA 'fallo sul LIBRO':") + add = {} + for C in (635.0, 5000.0, 100000.0): + d_tp = out[(C, 24, 1)]["sh"] - out[(C, 1, 1)]["sh"] + d_sk = out[(C, 1, 23)]["sh"] - out[(C, 1, 1)]["sh"] + d_both = out[(C, 24, 23)]["sh"] - out[(C, 1, 1)]["sh"] + add[C] = (d_tp, d_sk, d_both) + print(f" cap ${C:>8,.0f}: tranciare SOLO TP01 {d_tp:>+7.3f} Β· SOLO SKH01 {d_sk:>+7.3f} Β· " + f"ENTRAMBI {d_both:>+7.3f} (somma dei due singoli {d_tp+d_sk:>+7.3f}, " + f"scarto dall'additivita' {d_both-d_tp-d_sk:>+.3f})") + ok_add = all(abs(add[C][2] - add[C][0] - add[C][1]) < 0.01 for C in add) + print(f" -> additivita' dei due tranching: {'REGGE' if ok_add else 'NON regge a ogni capitale'}" + f" (scarto max {max(abs(add[C][2]-add[C][0]-add[C][1]) for C in add):.3f}).") + # ⚠️ ERRORE MIO, catturato rileggendo l'output: la prima stesura stampava "i due effetti sono + # additivi" come TESTO FISSO. A $635 non lo sono (i singoli sommano a un numero di segno + # opposto al congiunto). Ora l'affermazione la calcola il codice. Una conclusione scritta a + # mano accanto a una tabella e' una conclusione che la tabella non controlla. + print(f" ⚠️ E la TAGLIA e' rumore su questa lente: {abs(add[635.0][2]):.3f} a $635 e " + f"{abs(add[100000.0][2]):.3f} a $100k, con i segni dei singoli che cambiano fra i due") + print(f" capitali. Il numero grosso su SKH01 (Β§6) e' STANDALONE; dentro il libro pesa") + print(f" il 25% ed e' misurato su una lente diversa -> qui si legge 'non si distingue") + print(f" da zero', non 'il libro guadagna'.") + + print(f"\n 🚨 IL PREZZO VERO DEL TRANCHING SI VEDE SULL'HOLD-OUT, non sul FULL:") + for C in (635.0, 100000.0): + print(f" cap ${C:>8,.0f}: ShHOLD {out[(C,1,1)]['hold']:.3f} (oggi) -> " + f"{out[(C,24,1)]['hold']:.3f} (TP tranciato) -> {out[(C,1,23)]['hold']:.3f} " + f"(SKH tranciato) -> {out[(C,24,23)]['hold']:.3f} (entrambi)") + print(f" Non e' un difetto del tranching: e' la fortuna d'ancora che si restituisce.") + print(f" ENTRAMBE le ancore canoniche stanno all'estremo alto del loro hold-out (TP01 al") + print(f" 98Β° pctl di 24, SKH01 all'89Β° di 23) e il libro di oggi le somma. Un ensemble") + print(f" non puo' che perdere contro la somma di due massimi β€” ed e' esattamente perche'") + print(f" quel numero non e' l'aspettativa di domani.") + print(f" E il maxDD va nel verso opposto: {out[(635.,1,1)]['dd']:.1%} -> " + f"{out[(635.,24,23)]['dd']:.1%}. Rischio giu', hold-out fortunato giu'.") + + o1, o2 = out[(635., 1, 1)], out[(635., 24, 23)] + print(f"\n IL COSTO, a $635 β€” e sorprende: gli ORDINI salgono {o1['ord_y']:.0f} -> " + f"{o2['ord_y']:.0f}/anno ({o2['ord_y']/o1['ord_y']:.1f}x, " + f"{o2['ord_y']/365.25:.1f}/giorno su 24 giri orari),") + print(f" ma le FEE {'SCENDONO' if o2['fee_y'] < o1['fee_y'] else 'salgono'} " + f"${o1['fee_y']:.2f} -> ${o2['fee_y']:.2f}/anno " + f"({o1['fee_y']/635:.2%} -> {o2['fee_y']/635:.2%} di capitale).") + print(f" Non e' una contraddizione ed e' il fatto piu' istruttivo di questa sezione: su") + print(f" Deribit la fee e' PROPORZIONALE, quindi si paga il NOZIONALE, non gli ordini β€”") + print(f" e mediare 23 fasi di uno sleeve a trade discreti trasforma un +-1,0x che sbatte") + print(f" da un estremo all'altro in una posizione frazionaria che si muove poco. Piu'") + print(f" ordini, meno merce. (Su un venue a pavimento FISSO β€” IB, 25/07 β€” il segno si") + print(f" ribalterebbe: la stessa mossa costerebbe {o2['ord_y']/o1['ord_y']:.1f}x.)") + print(f" {clock()}") + return out + + +# =========================================================================== +# Β§8 β€” QUANTO VALE RIDURRE LA VARIANZA SOTTO UNA BARRIERA ASSORBENTE +# =========================================================================== +def barrier_pass(r: np.ndarray, k: float, up: float, dn: float, + starts: np.ndarray, maxlen: int = 365) -> tuple: + """P(pass) / P(breach) di una regola a barriera: dal saldo iniziale, +up = pass, + -dn = breach (max-loss STATICO, la regola vera: 25/07). Path close-only.""" + L = np.concatenate([[0.0], np.cumsum(np.log1p(np.clip(k * r, -0.99, None)))]) + a, b = np.log1p(up), np.log1p(-dn) + np_, nb, nu = 0, 0, 0 + for i in starts: + d = L[i + 1:i + 1 + maxlen] - L[i] + if len(d) < 5: + continue + hu = np.flatnonzero(d >= a) + hd = np.flatnonzero(d <= b) + ju = hu[0] if len(hu) else 10 ** 9 + jd = hd[0] if len(hd) else 10 ** 9 + if ju == jd == 10 ** 9: + nu += 1 + elif ju < jd: + np_ += 1 + else: + nb += 1 + tot = np_ + nb + nu + return (np_ / tot, nb / tot, nu / tot) if tot else (0.0, 0.0, 0.0) + + +def sezione8(S1: dict, S2: dict, S6: dict) -> dict: + sez("Β§8 β€” [A7] QUANTO VALE RIDURRE LA VARIANZA SOTTO UNA BARRIERA (il canale funded)") + print(" Su un conto proprio il nemico e' il drift e la varianza si sopporta. Su un conto") + print(" funded il vincolo binding e' la regola di DRAWDOWN (25/07 Β§4): li' la varianza E'") + print(" il nemico β€” e l'ensemble compra esattamente quello. Questa e' l'attesa [A7]; segue") + print(" la misura, che la refuta sulla meta' che si puo' misurare.") + print(" Regole HyroTrader modellate (`r0725_hyro`: EV_TARGET/EV_DD/EV_DL = 0.10/0.06/0.04):") + print(" target +10%, max-loss STATICO -6% dal saldo iniziale.") + print(" ⚠️ LIMITE DICHIARATO, ed e' quello che conta: la regola di DAILY-LOSS (-4%) NON e'") + print(" valutabile qui. Su una regola a UN giorno la lente close-only e' CIECA (22/08:") + print(" 0,00 breach/anno contro 0,40-1,21 veri, rapporto INF) e servirebbe la lente wick") + print(" ACCOPPIATA (`r0725_prop_coupled`), fuori portata di questo script. Il max-loss e'") + print(" multi-giorno: li' close-only e' ottimista di un ricarico ~3,5%, non cieca.") + + skhL = S6["canonical"] + skh_off = skhL[1]["S"] + skh_ens = skhL[23]["S"][0] + tp_h = S1["singles"] + tp_ens = S2["e24"] + + def book(tp: pd.Series, skh: pd.Series) -> pd.Series: + return combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": W_TP, "SKH01": W_SKH}, + lo=pd.Timestamp("2019-03-14", tz="UTC")) + + rng = np.random.default_rng(20260823) + pairs = [(int(h), int(o)) for h, o in zip(rng.integers(0, 24, 150), rng.integers(0, 23, 150))] + print(f"\n Spazio congiunto 24 ancore TP01 x 23 fasi SKH01 = 552 configurazioni; 150 estrazioni") + print(f" uniformi indipendenti (seme dichiarato) + l'ensemble congiunto + la canonica (0,0).") + print(f" {'leva':>5} {'':<24} {'P(pass)':>9} {'P(breach)':>10} {'P(aperto)':>10} {'maxDD':>8}") + res = {} + BK = {} + for (h, o) in pairs: + BK[(h, o)] = book(tp_h[h], skh_off[o]) + BE = book(tp_ens, skh_ens) + BC = book(tp_h[0], skh_off[0]) + for k in (0.50, 0.75, 1.00): + rows = [] + for (h, o) in pairs: + s_ = BK[(h, o)]; r = s_.values.astype(float) + st = np.arange(0, len(r) - 380, 15) + rows.append(barrier_pass(r, k, 0.10, 0.06, st) + (al._dd_ret(s_),)) + A = np.array(rows) + rr = BE.values.astype(float); st = np.arange(0, len(rr) - 380, 15) + pe = barrier_pass(rr, k, 0.10, 0.06, st) + (al._dd_ret(BE),) + rc = BC.values.astype(float); stc = np.arange(0, len(rc) - 380, 15) + pc = barrier_pass(rc, k, 0.10, 0.06, stc) + (al._dd_ret(BC),) + res[k] = dict(A=A, ens=pe, can=pc) + print(f" {k:>5.2f} {'CANONICA (0,0) β€” oggi':<24} {pc[0]:>9.1%} {pc[1]:>10.1%} " + f"{pc[2]:>10.1%} {pc[3]:>8.1%}") + print(f" {'':>5} {'mediana 150 estrazioni':<24} {np.median(A[:,0]):>9.1%} " + f"{np.median(A[:,1]):>10.1%} {np.median(A[:,2]):>10.1%} {np.median(A[:,3]):>8.1%}") + print(f" {'':>5} {'banda p10-p90':<24} " + f"{'[%.0f%%,%.0f%%]' % (np.percentile(A[:,0],10)*100, np.percentile(A[:,0],90)*100):>9} " + f"{'[%.1f%%,%.1f%%]' % (np.percentile(A[:,1],10)*100, np.percentile(A[:,1],90)*100):>10} " + f"{'':>10} " + f"{'[%.1f,%.1f]' % (np.percentile(A[:,3],10)*100, np.percentile(A[:,3],90)*100):>8}") + print(f" {'':>5} {'ENSEMBLE congiunto':<24} {pe[0]:>9.1%} {pe[1]:>10.1%} " + f"{pe[2]:>10.1%} {pe[3]:>8.1%} (pctl fra le estrazioni: " + f"{pctl_of(A[:,0], pe[0]):.0f}Β° su P(pass), {pctl_of(-A[:,1], -pe[1]):.0f}Β° su " + f"P(breach), {pctl_of(-A[:,3], -pe[3]):.0f}Β° su maxDD)") + print() + + print(" 🚨 [A7] REFUTATA sulla meta' misurabile, E CON UN MECCANISMO β€” non e' rumore.") + print(" Su queste regole il lato BINDING non e' la barriera, e' il BERSAGLIO: P(breach)") + print(f" sta fra {res[0.50]['ens'][1]:.1%} e {res[1.00]['ens'][1]:.1%} mentre P(pass) sta") + print(f" fra {res[0.50]['ens'][0]:.0%} e {res[1.00]['ens'][0]:.0%}. Se il vincolo e'") + print(" ARRIVARE a +10% e non evitare -6%, ridurre la varianza RALLENTA: e' la stessa") + print(" meno-vol che tiene lontani dalla barriera a tenere lontani dal traguardo.") + print(" L'ensemble infatti sta al 30-50Β° pctl su P(pass) β€” sotto la mediana β€” pur avendo") + print(" un maxDD MIGLIORE della mediana. Le due cose non sono in contraddizione: sono la") + print(" stessa cosa vista dai due lati.") + print(" ⚠️ E la regola che il progetto ha MISURATO essere quella che uccide β€” la") + print(" daily-loss a UN giorno (22/08) β€” e' esattamente quella che questa lente non puo'") + print(" vedere. Quindi: A7 non e' refutata, e' refutata SULLA REGOLA SBAGLIATA. Dirlo e'") + print(" il risultato; concludere 'l'ensemble non serve sul funded' NON lo sarebbe.") + + # --- proxy POTENTE per la regola a un giorno: la coda sinistra giornaliera ----------- + print("\n (b) L'UNICA cosa con potenza che si puo' dire sulla regola a un giorno: la CODA") + print(" SINISTRA giornaliera. La daily-loss legge il MINIMO del giorno; il close-only ne") + print(" e' un limite inferiore (rapporto mediano minimo/chiusura misurato 1,89x il 22/08).") + print(" Non e' una stima del breach β€” e' il confronto FRA configurazioni sulla grandezza") + print(" che quella regola legge, a lente identica.") + W = np.array([[float(np.min(BK[p].values)), float(np.percentile(BK[p].values, 1)), + float(np.mean(BK[p].values < -0.02))] for p in pairs]) + we = [float(np.min(BE.values)), float(np.percentile(BE.values, 1)), + float(np.mean(BE.values < -0.02))] + wc = [float(np.min(BC.values)), float(np.percentile(BC.values, 1)), + float(np.mean(BC.values < -0.02))] + print(f"\n {'':<24} {'peggior giorno':>15} {'p1 giornaliero':>15} {'giorni < -2%':>14}") + print(f" {'CANONICA (0,0) β€” oggi':<24} {wc[0]:>14.2%} {wc[1]:>14.2%} {wc[2]:>13.2%}") + print(f" {'mediana 150 estrazioni':<24} {np.median(W[:,0]):>14.2%} " + f"{np.median(W[:,1]):>14.2%} {np.median(W[:,2]):>13.2%}") + print(f" {'banda p10-p90':<24} " + f"{'[%.2f%%,%.2f%%]' % (np.percentile(W[:,0],10)*100, np.percentile(W[:,0],90)*100):>15} " + f"{'[%.2f%%,%.2f%%]' % (np.percentile(W[:,1],10)*100, np.percentile(W[:,1],90)*100):>15} " + f"{'[%.2f%%,%.2f%%]' % (np.percentile(W[:,2],10)*100, np.percentile(W[:,2],90)*100):>14}") + q = (pctl_of(W[:, 0], we[0]), pctl_of(W[:, 1], we[1]), pctl_of(-W[:, 2], -we[2])) + print(f" {'ENSEMBLE congiunto':<24} {we[0]:>14.2%} {we[1]:>14.2%} {we[2]:>13.2%}" + f" (pctl: {q[0]:.0f}Β° / {q[1]:.0f}Β° / {q[2]:.0f}Β° β€” " + f"= quota di configurazioni che l'ensemble BATTE)") + # ⚠️ ERRORE MIO, catturato rileggendo l'output: la prima stesura calcolava questi tre + # percentili con `pctl_of(-W, -we)` per tutte e tre le colonne. Sulle prime due (che sono + # RITORNI, negativi, dove "piu' alto = meglio") quel segno da' il COMPLEMENTO: stampava 34Β° + # dove il valore vero e' 66Β°, cioe' diceva "peggio della mediana" di un numero che e' + # meglio della mediana. La terza colonna (una FREQUENZA, dove "piu' basso = meglio") aveva + # bisogno del segno invertito ed era l'unica giusta. Tre colonne, due convenzioni. + print(f" -> l'ensemble batte la mediana su TUTTE E TRE le colonne, ma di POCO " + f"({q[0]:.0f}Β°/{q[1]:.0f}Β°/{q[2]:.0f}Β° pctl): comprime la coda, non la annulla.") + print(" E la banda p10-p90 del p1 e' strettissima -> su questa grandezza le 552") + print(" configurazioni si somigliano molto piu' che sullo Sharpe.") + print(" Resta un INDIZIO, non una misura del breach: la conferma richiede la lente") + print(" accoppiata, ed e' un follow-up con un costo noto, non un ostacolo.") + print("\n πŸ“Œ E il fatto che chiude il cerchio sull'eseguibilita': il conto funded e' da") + print(" $100.000, dove il pavimento del venue e' invisibile (Β§7 lo misura). Il tranching") + print(" e' ineseguibile in nessun posto β€” e utile solo dove la coda conta.") + print(f" {clock()}") + return dict(res=res, tail=dict(W=W, ens=we, can=wc)) + + +# =========================================================================== +# Β§9 β€” VERDETTO +# =========================================================================== +def sezione9(S1, S2, S4, S5, S6, S7, S8) -> None: + sez("Β§9 β€” VERDETTO SULLE ATTESE A PRIORI (derivato dai numeri, non scritto a mano)") + can = S1["singles"][0]; e24 = S2["e24"] + ca, cb, cc = m3(can); ea, eb, ec = m3(e24) + med = S1["singles"][S2["med_h"]] + ma, mb, mc = m3(med) + f1 = S2["fam"][1] + d_med = ea - ma + # A1: identita' del lordo, ri-verificata qui (cached, costo ~0) + id_max = max(float(np.max(np.abs(eval_ideal(a, HOURS)["gross"] - + np.mean([eval_ideal(a, (h,))["gross"] for h in HOURS], axis=0)))) + for a in ASSETS) + a1 = "CONFERMATA" if id_max < 1e-15 else "REFUTATA" + a2 = ("CONFERMATA" if 0.03 <= d_med <= 0.15 else + ("DIREZIONE OK, TAGLIA REFUTATA" if d_med > 0 else "REFUTATA")) + a3 = "CONFERMATA" if ec < cc else "REFUTATA" + r1, r24 = S5["out"][1]["ratio"], S5["out"][24]["ratio"] + a4 = "CONFERMATA" if abs(r24 - r1) < 0.05 else "REFUTATA" + nm = S4["nmax"]; pas = S4["passed"] + a5 = "REFUTATA" if (nm[635.0] > 1 or min(pas.values()) == 6) else "CONFERMATA" + a6 = "CONFERMATA (aritmetica: 23 e' primo)" + p_ens = pctl_of(S8["res"][1.00]["A"][:, 0], S8["res"][1.00]["ens"][0]) + # convenzione: il p1 e' un RITORNO (negativo), quindi "piu' alto = meglio" e il percentile + # si calcola SENZA invertire il segno. Invertirlo da' il complemento (34Β° invece di 66Β°) β€” + # errore commesso nella prima stesura di Β§8 e corretto li': va corretto anche qui, o le due + # sezioni stampano due numeri diversi per la stessa grandezza. + p_tail = pctl_of(S8["tail"]["W"][:, 1], S8["tail"]["ens"][1]) + a7 = "REFUTATA sulla regola misurabile" if p_ens < 50 else "CONFERMATA" + ver = [ + ("A1", a1, f"identita' del lordo verificata a max|dif| = {id_max:.1e}; turnover " + f"sub-additivo sul conto esatto."), + ("A2", a2, f"dSharpe FULL vs ancora mediana {d_med:+.3f} (previsto +0,03..+0,15): " + f"il SEGNO e' giusto, la TAGLIA e' {0.09/max(d_med,1e-9):.0f}x piu' piccola del " + f"centro della mia banda. Avevo sopravvalutato quanto sono decorrelate fra loro " + f"24 ancore della STESSA strategia."), + ("A3", a3, f"dSharpe HOLD vs canonica {ec-cc:+.3f}; la canonica sta al " + f"{pctl_of(S1['shh'], S1['shh'][0]):.0f}Β° pctl delle 24 sull'hold-out e al " + f"{pctl_of(S1['shi'], S1['shi'][0]):.0f}Β° in-sample: SFORTUNATA in-sample e FORTUNATA " + f"sull'hold-out, esattamente la struttura che il 02/07 aveva descritto."), + ("A4", a4, f"funding dell'ensemble ESATTAMENTE la media (max|dif| = 0) e rapporto " + f"condizionale immobile ({r1:.2f}x a K=1 -> {r24:.2f}x a K=24)."), + ("A5", a5, f"il pavimento NON degrada il tranching: retention {min(pas.values())}/6 celle " + f"sopra soglia a OGNI capitale, N_max($635) = {nm[635.0]} invece di 1. " + f"Il criterio SATURA -> non e' un N_max, e' un 'il vincolo non e' questo'."), + ("A6", a6, "e le famiglie simmetriche di SKH01 vivono solo sulla griglia 5m; K=3 e' un " + "ensemble di SEGNALE, non d'esecuzione."), + ("A7", a7, f"l'ensemble sta al {p_ens:.0f}Β° pctl su P(pass) a leva 1,0x β€” SOTTO la mediana β€” " + f"perche' su queste regole il lato binding e' il BERSAGLIO (+10%), non la " + f"barriera. Ma sul p1 giornaliero, che e' cio' che la regola killer legge, sta " + f"al {p_tail:.0f}Β° pctl (batte quella quota di configurazioni). La refutazione e' " + f"sulla regola che si puo' misurare, non su quella che uccide: dichiarato."), + ] + for k, v, t in ver: + print(f" [{k}] {v}") + print(f" {t}\n") + + sez("Β§9b β€” LE TRE RISPOSTE") + print(f" (i) L'ensemble batte l'ancora MEDIANA? SI su TP01, ma di BRICIOLE: " + f"dShFULL {d_med:+.3f}, dShHOLD {ec-mc:+.3f}, dDrift {drift(e24)-drift(med):+.2%}.") + print(f" Scomposto contro la MEDIA delle 24 (il confronto algebricamente pulito): il") + print(f" drift si muove di {drift(e24)-np.mean(f1['dr']):+.3%} (solo fee-netting, e la") + print(f" linearita' dice che non puo' fare altro) e la vol di " + f"{vol(e24)/np.mean(f1['vo'])-1:+.1%}. Tutto il guadagno e' vol.") + print(f" Cio' che l'ensemble compra DAVVERO non e' nella colonna Sharpe: e' la") + print(f" dispersione, sd(ShFULL) {f1['f'].std():.3f} -> 0 e sd(ShHOLD) " + f"{f1['h'].std():.3f} -> 0.") + skhc, skhh = S6["canonical"], S6["hourly"] + print(f"\n ⚠️ E su SKH01 la risposta e' DIVERSA di un ordine di grandezza: sulla lente") + print(f" del path che gira (hourly) la mediana delle 23 fasi fa ShFULL " + f"{np.median(skhh[1]['f']):.3f} e l'ensemble K=23 fa {skhh[23]['f'][0]:.3f} " + f"({skhh[23]['f'][0]-np.median(skhh[1]['f']):+.3f}),") + print(f" con maxDD mediano {np.median(skhh[1]['dd']):.1%} -> {skhh[23]['dd'][0]:.1%}. " + f"Su TP01 il tranching e' rumore; su SKH01 e' il grosso.") + print(f" Meccanismo: la dispersione d'ancora di SKH01 e' 3x quella di TP01 " + f"(sd {skhh[1]['f'].std():.3f} vs {f1['f'].std():.3f}) perche' i suoi trade sono") + print(f" DISCRETI β€” spostare la griglia cambia QUALI trade esistono, non di quanto sono") + print(f" grossi. Il tranching vale quanto la lumpiness che elimina.") + print(f" ⚠️ MA quel numero e' STANDALONE. Dentro il libro SKH01 pesa il 25% e il Β§7 lo") + print(f" misura su un'altra lente: li' il tranching congiunto vale " + f"{S7[(635.0,24,23)]['sh']-S7[(635.0,1,1)]['sh']:+.3f} a $635 e " + f"{S7[(100000.0,24,23)]['sh']-S7[(100000.0,1,1)]['sh']:+.3f} a $100k, coi segni dei") + print(f" singoli che cambiano fra i due capitali = non distinguibile da zero.") + print(f" La lettura onesta: il tranching di SKH01 e' l'unico posto dove c'e' un segnale,") + print(f" e non e' abbastanza grande da vedersi attraverso un peso del 25%.") + + print(f"\n (ii) L'ensemble batte la CANONICA che gira oggi? NO sull'hold-out " + f"({ec-cc:+.3f}), SI su FULL ({ea-ca:+.3f}) e su IS ({eb-cb:+.3f}).") + print(f" E i due numeri NON sono due stime della stessa cosa. Il {cc:+.2f} della canonica") + print(f" e' una estrazione GIA' AVVENUTA (98Β° pctl di 24); il {ec:+.2f} dell'ensemble e'") + print(f" cio' che si ottiene SENZA estrarre.") + print(f" πŸ“Œ LA STIMA ONESTA DEL FUTURO E' L'ENSEMBLE: {ec:+.2f}, non {cc:+.2f}.") + print(f" Tenere la canonica non conserva il {cc:+.2f} β€” ri-estrae dalla banda " + f"[{S1['shh'].min():+.2f},{S1['shh'].max():+.2f}], mediana {np.median(S1['shh']):+.2f}.") + print(f" (E questi numeri riproducono la memoria del progetto: canonico +0,44 al " + f"{pctl_of(S1['shh'], S1['shh'][0]):.0f}Β° pctl, mediana +{np.median(S1['shh']):.2f}.)") + + print(f"\n (iii) A quale capitale diventa eseguibile senza degenerare? A TUTTI, gia' a $635 β€”") + print(f" e questo REFUTA la mia attesa e la premessa (non la conclusione) del 02/07.") + print(f" A $635 il path eseguito a K=24 segue il target ideale con corr " + f"{S4['band'][(635.0,24)]['corr']:.4f} contro {S4['band'][(635.0,1)]['corr']:.4f} di K=1:") + print(f" il tranching e' seguito MEGLIO, non peggio, perche' un target che si muove a") + print(f" passi piccoli e frequenti entra in una banda fissa meglio di uno che salta.") + print(f" Ordini: {S4['band'][(635.0,1)]['ord_y']:.0f}/anno -> " + f"{S4['band'][(635.0,24)]['ord_y']:.0f}/anno, fee " + f"${S4['band'][(635.0,1)]['fee_y']:.2f} -> ${S4['band'][(635.0,24)]['fee_y']:.2f}/anno.") + + sez("Β§9c β€” IL RISULTATO CHE NON ERA NEL MANDATO, e cosa NON cambia") + print(" πŸ“Œ LA PREMESSA DEL 02/07 E' SBAGLIATA; LA SUA CONCLUSIONE REGGE PER UN'ALTRA RAGIONE.") + print(" Il 02/07 boccio' il tranching cosi': *'a $225/asset di quota TP01 i delta") + print(" per-ancora (~$1-2) sono sotto il min-order $5 -> degenererebbe comunque in K=1'*.") + print(" Ma `src/live/book.build_book_order` non manda mai un ordine PER ANCORA: banda") + print(" `abs(delta)` sulla posizione NETTA e ne manda UNO per asset per giro. Un 'delta") + print(" per-ancora' non e' un ordine, e' un pezzo di aritmetica dentro il target. Il") + print(" tranching non moltiplica gli ordini per K, e infatti la degenerazione NON avviene") + print(" (misurata: retention ~0,9-1,0 a $635).") + print(" Cio' che boccia il tranching di TP01 non e' l'esecuzione, e' che NON C'E' NIENTE") + print(" DA PRENDERE: +0,014 di Sharpe. La decisione del 02/07 resta, la sua ragione no.") + print(" E la ragione conta: quella del 02/07 sarebbe caduta al primo esecutore che mandasse") + print(" un ordine per tranche; questa non cade mai, perche' e' sulla taglia dell'effetto.") + print("\n 🚨 E IL FILONE HA TROVATO IL SUO CONTRARIO: l'unica ancora che il 02/07 NON aveva") + print(" esaminato β€” la griglia 230m/690m di SKH01 β€” e' quella dove il tranching vale.") + print(" Il 02/07 raccomando' l'audit su SKH01 (fatto il 02/07-bis) ma non ci porto' MAI") + print(" la domanda del tranching, che era nata su TP01.") + print("\n ⚠️ COSA COSTA DAVVERO, e non e' negli ordini: il feed. 23 ancore su 24 di TP01") + print(" richiedono barre di oggi, cioe' `fresh_5m` β€” lo stesso path che il 26/07 e'") + print(" risultato ricadere IN SILENZIO sul certificato e che il 29/07 ha fallito 6 giri") + print(" su 8. Oggi TP01 non dipende da quel path; con l'ensemble ci dipenderebbe.") + print(" E' l'unica delle tre obiezioni del 02/07 che sopravvive intatta.") + print(" (SKH01 quel feed lo usa GIA': il suo tranching non aggiunge dipendenze nuove β€”") + print(" il che rende l'asimmetria fra i due sleeve ancora piu' netta.)") + print("\n NIENTE CAMBIA nel libro live: pesi, ancore, cron, config INVARIATI. Il tranching non") + print(" e' un cambio di pesi (`weights_tilt_null` non si applica) ma e' un cambio di") + print(" ESECUZIONE: vale l'asticella del 26/07 (T1), il beneficio si confronta col path che") + print(" gira davvero. Non e' proposto nulla: SKH01 non e' passato per `study_family_honest`") + print(" su questa griglia, il guadagno e' misurato sugli STESSI 7 anni su cui SKH01 e' stato") + print(" selezionato, e la lente 'hourly' e' gia' nota (26/07) per sottostimare il path live.") + print(" Cio' che questo filone stabilisce e' DOVE guardare, non cosa fare.") + print(f"\n {clock()} fine.") + + +def main() -> None: + print("=" * 100) + print(" r0823_anchor_ensemble.py β€” Β§47 ANCHOR-ENSEMBLE") + print(f" fee {2*FEE:.2%} RT Β· HOLD-OUT >= {HOLDOUT.date()} Β· min_order ${MIN_ORDER_USD:.0f} Β· " + f"libro TP01 {W_TP:.0%} / SKH01 {W_SKH:.0%}") + print("=" * 100) + sezione0() + S1 = sezione1() + S2 = sezione2(S1) + sezione3(S1, S2) + S4 = sezione4(S1, S2) + S5 = sezione5(S1) + S6 = sezione6() + S7 = sezione7(S1, S4, S6) + S8 = sezione8(S1, S2, S6) + sezione9(S1, S2, S4, S5, S6, S7, S8) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_book_3rd.py b/scripts/research/r0823_book_3rd.py new file mode 100644 index 0000000..8eea1ad --- /dev/null +++ b/scripts/research/r0823_book_3rd.py @@ -0,0 +1,1262 @@ +#!/usr/bin/env python +"""r0823_book_3rd.py β€” Β§50 BOOK-3RD: qual e' il MIGLIOR TERZO SLEEVE del LIBRO LIVE, e vale la pena? + +IL PROBLEMA (e perche' non e' una domanda gia' fatta). Il progetto ha DUE oggetti che vengono +continuamente confusi: + * il BOOK DI RICERCA a 5 sleeve (TP01 33 / XS01 15 / VRP01 12 / SKH01 20 / GTAA01 20) β€” + `sleeves.active_sleeves()` β€” su cui e' calcolato quasi ogni numero pubblicato; + * il LIBRO LIVE a 2 sleeve (`sleeves.deribit_book_sleeves()`, TP01 75 / SKH01 25) β€” l'unica + cosa che gira con soldi veri, e che non ha MAI avuto un terzo sleeve. +Il libro a 5 non e' deployabile: GTAA01 e' bloccato dal PRIIPs (verificato sul conto reale il +26/07) e vuole $3k; XS01 e' dichiarato STAT-MODE per taglia; VRP01 cade sotto la regola +"niente short-vol da modello in deploy" e a $3.000 sarebbe ETH-only con 0 lotti al peso di book. + +DOMANDA DECIDIBILE: oggi, con ~$635 su Deribit, quale e' il miglior terzo sleeve REALMENTE +eseguibile β€” e vale la pena? Se la risposta e' "nessuno": a quale capitale cambia, e chi entra? + +COSA FA + 0. ATTESA A PRIORI, dichiarata PRIMA di misurare (e poi confrontata con l'esito). + 1. CENSIMENTO dello stato REALE dei candidati β€” non quello dichiarato in memoria: + strumenti richiesti, disponibilita' su Deribit (GET pubblica), LOTTO MINIMO letto dal + venue (non stimato), data di quotazione, gate pre-registrato in corso. + 2. IL VINCOLO DIMENTICATO β€” il NETTING. Su Deribit c'e' UNA sola posizione netta per + strumento, e TP01+SKH01 gia' si nettano su BTC/ETH_USDC-PERPETUAL. Un terzo sleeve + DIREZIONALE sugli stessi strumenti si netta con loro (zero costo di strumento, zero + separazione); uno su strumenti diversi no (serve una riga in `_CONTRACT`, cioe' CODICE su + un percorso con soldi veri). Verificato per introspezione sul sorgente di produzione. + 3. REPLICA DI CONTROLLO: riprodurre i numeri pubblicati del libro live (ShFULL ~1,81 / + ShHOLD ~1,44 / maxDD ~9,4%) PRIMA di stampare qualunque delta. Due lenti, dichiarate. + 4. SERIE ANCORATE, ognuna con sanity bit-exact contro lo sleeve/segnale di produzione. + 5. LA MISURA: dSharpe / d drift / d maxDD del libro live aggiungendo ogni candidato a pesi + {5,10,15,20}%, col FUNDING dentro, su ESTRAZIONI CONGIUNTE dello spazio d'ancora + (mediana delle DIFFERENZE APPAIATE β€” mai la differenza delle mediane). + 6. `weights_tilt_null` β€” il gate del progetto per ogni cambio di pesi. + 7. ESEGUIBILITA' a $635 / $2k / $5k / $20k: ticket per gamba, min-order, haircut. + 8. LA SCALA: a quale capitale entra il primo candidato, e quale. + +ONESTA'. Nessuna promozione. L'output e' una TABELLA DECIDIBILE. I gate pre-registrati NON si +anticipano: per i candidati sotto gate si dice cosa succederebbe SE passassero, non si decide +al loro posto (anticipare un gate e' selezione-sull'hold-out, regola scritta del progetto). + +VINCOLI RISPETTATI: nessun file di produzione toccato (src/, config/, scripts/live/, +scripts/cron_*.sh, tests/, data/paper_*, data/live); NESSUN ordine; rete = sole GET pubbliche +Deribit <=2 req/s, con astensione nei minuti :05-:10 e :24-:30 (cron_book al :07, +collect_chain al :25). Cache in scratchpad, fuori dal repo. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0823_book_3rd.py + ... --no-net solo cache (nessuna richiesta di rete) + ... --draws N estrazioni d'ancora (default 200) + ... --force ignora la cache delle serie +""" +from __future__ import annotations + +import argparse +import datetime as dt +import inspect +import json +import pickle +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (str(ROOT), str(ROOT / "scripts" / "research"), + str(ROOT / "scripts" / "research" / "alt"), + str(ROOT / "scripts" / "research" / "ortho")): + if _p not in sys.path: + sys.path.insert(0, _p) + +# --- produzione (importata, MAI riscritta) --------------------------------------------------- +from src.portfolio.portfolio import (HOLDOUT, combine_outer, metrics, # noqa: E402 + to_daily, weights_tilt_null) +from src.portfolio import sleeves as SL # noqa: E402 +from src.live import book as LB # noqa: E402 +from src.live import deribit as DB # noqa: E402 +from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, # noqa: E402 + simple_returns) +from src.strategies.prevday_breakout import target as prevday_target # noqa: E402 + +# --- macchineria di ricerca gia' validata (riusata, non riscritta) --------------------------- +import altlib as al # noqa: E402 +import ortholib as ol # noqa: E402 +import orthogonal_signals as OS # noqa: E402 (STATARB-RESID: segnale ESATTO) +import r0702_tp01_offset as A_TP # noqa: E402 (daily_off: ancora oraria di TP01) +import r0703_vrpimp_anchor as A_VRP # noqa: E402 (fasi settimanali di VRP01) +import r0822d_funding as FU # noqa: E402 (funding: fetch/rate/cumF/skh_daily) +from r0726_dvolspread_gate import make_book as dvs_make_book # noqa: E402 + +# ============================================================================================= +# COSTANTI DICHIARATE +# ============================================================================================= +SCRATCH = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/b3rd") +if not SCRATCH.parent.exists(): + SCRATCH = Path("/tmp/pythagoras_book3rd") +CACHE = SCRATCH / "series_v3.pkl" + +CAPITAL_TODAY = 635.0 # il conto reale (CLAUDE.md 2026-08) +CAP_LADDER = (635.0, 2000.0, 5000.0, 20000.0) +MIN_ORDER = 5.0 # config/live.json -> min_order_usd +FEE_SIDE = al.FEE_SIDE # 0.05%/lato (0.10% RT), il default del progetto +W_TP, W_SKH = LB.W_TP01, LB.W_SKH # 0.75 / 0.25, LETTI dalla produzione +BOOK_START = pd.Timestamp("2019-03-14", tz="UTC") +WEIGHTS_TESTED = (0.05, 0.10, 0.15, 0.20) +TP_HOURS = tuple(range(24)) # ancora giornaliera di TP01 +SKH_OFFSETS = tuple(range(0, 690, 30)) # 23 offset della griglia 230m/690m +XS_PHASES = tuple(range(10)) # ciclo H=10 di XS01 +VRP_PHASES = tuple(range(7)) # ciclo settimanale di VRP01 +SEED = 20260823 +# XS01 paga funding perche' il momentum compra i perp col funding piu' caro: +1,45%/anno +# MISURATO nell'ondata 2026-08-22 Β§37 (banda onesta +0,69/+1,45 se HL e' ~2,1x il venue vero). +# Qui e' applicato come DRAG DICHIARATO, non modellato per gamba: gli alt su Deribit sono +# quotati da giorni (vedi sezione 1) e non ne esiste storico di funding. +XS_FUNDING_DRAG_YR = 0.0145 + +API = "https://www.deribit.com/api/v2/public" +CACHE_FRESH_S = 6 * 3600 +T0 = time.time() +_ARGS = None + + +def sez(t: str) -> None: + print("\n" + "=" * 104) + print(f" {t}") + print("=" * 104) + + +def sub(t: str) -> None: + print("\n" + "-" * 104) + print(f" {t}") + print("-" * 104) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +# ============================================================================================= +# RETE β€” sole GET pubbliche, pacing, astensione nelle finestre del cron +# ============================================================================================= +def _pace() -> None: + while dt.datetime.now(dt.timezone.utc).minute in (5, 6, 7, 8, 9, 10, 24, 25, 26, 27, 28, 29, 30): + time.sleep(15) + time.sleep(0.55) + + +def get_json(path: str, cache_name: str) -> dict | None: + """GET pubblica con cache su scratchpad. Con --no-net usa solo la cache.""" + SCRATCH.mkdir(parents=True, exist_ok=True) + fp = SCRATCH / cache_name + if _ARGS and _ARGS.no_net: + return json.loads(fp.read_text()) if fp.exists() else None + # cache fresca -> NESSUNA richiesta, e quindi nessuna attesa nelle finestre del cron. + # (`_pace` blocca 15s per volta nei minuti :05-:10 e :24-:30: farlo quando non si sta + # per chiamare nessuno e' solo tempo perso.) + if fp.exists() and (time.time() - fp.stat().st_mtime) < CACHE_FRESH_S: + return json.loads(fp.read_text()) + try: + _pace() + with urllib.request.urlopen(f"{API}/{path}", timeout=30) as r: + d = json.loads(r.read()) + fp.write_text(json.dumps(d)) + return d + except Exception as e: # noqa: BLE001 + print(f" [rete KO su {path}: {type(e).__name__}] -> cache") + return json.loads(fp.read_text()) if fp.exists() else None + + +# ============================================================================================= +# 0) ATTESA A PRIORI β€” dichiarata PRIMA di misurare +# ============================================================================================= +ATTESA = """ + Registrata prima di guardare qualunque numero di questo script: + + (A1) NESSUN candidato e' deployabile oggi, e il muro NON sara' il lotto minimo del venue. + Nell'ondata del 22/08 l'eseguibilita' a $600 non ha bocciato niente per TRE volte di + fila; mi aspetto che il muro sia (a) i gate pre-registrati non scaduti, (b) + `weights_tilt_null`, (c) per XS01/XSR01 il fatto che il venue e' Hyperliquid, non Deribit. + (A2) Il candidato col dSharpe piu' alto sara' XS01 (LOO 26/07: +0,125 di FULL nel libro a 5, + corr ~0 a tutto), ma `weights_tilt_null` FALLIRA' per la stessa ragione strutturale per + cui fallisce su XSR01: storia dal 2024, quindi quasi nessun in-sample. + (A3) I due candidati "eseguibili a $600" gia' etichettati tali (STATARB-RESID, DVOLSPREAD) + daranno un uplift piccolo (Sharpe standalone 0,75 e 0,61 contro 1,81 del libro): mi + aspetto |dSharpe| < 0,10 e il segno incerto sulla banda d'ancora. + (A4) La scala: il primo a entrare sara' XS01 fra $2k e $5k, NON ai $20k della memoria β€” ma + per una ragione di VENUE, non di taglia (serve che Deribit quoti abbastanza gambe). + (A5) Se qualcosa mi sorprende, sara' un candidato dato per eseguibile che NON lo e' (e' gia' + successo due volte: GTAA01/PRIIPs e VRP01/lotto minimo). +""" + + +# ============================================================================================= +# 1) CENSIMENTO β€” lo stato REALE dei candidati +# ============================================================================================= +# I 19 major di XS01, letti dalla PRODUZIONE (non riscritti: se cambiano li', cambiano qui). +XS_UNIVERSE = list(SL.XS_UNIVERSE) + +# gate pre-registrati: (nome, data, cosa decide). Letti da CLAUDE.md / docstring dei monitor. +GATES = { + "STATARB-RESID": ("2026-09-27", "decisione di deploy (gate `r0724_statarb_deploy_gate`)"), + "XSR01": ("2026-10-23", "deploy solo se Sharpe>=1,0 E haircut a $5k <=40%"), + "DVOLSPREAD": ("2026-10-24", "kill se Sharpe forward < -0,50; decisione 2027-01-24"), +} + + +def venue_census() -> dict: + """Strumenti Deribit REALI: perpetual USDC, spot, opzioni. Lotto minimo in DOLLARI.""" + out = {"perp": {}, "spot": {}, "opt": {}, "read_ok": True} + ins = {} + for cur in ("BTC", "ETH", "USDC"): + d = get_json(f"get_instruments?currency={cur}&expired=false", f"instr_{cur}.json") + if d is None: + out["read_ok"] = False + continue + ins[cur] = d["result"] + bs = get_json("get_book_summary_by_currency?currency=USDC&kind=future", "bs_usdc_fut.json") + marks = {} + if bs: + for x in bs["result"]: + p = x.get("mark_price") or x.get("last") + if p: + marks[x["instrument_name"]] = (float(p), float(x.get("volume_usd") or 0.0)) + for cur, lst in ins.items(): + for x in lst: + n = x["instrument_name"] + if x["kind"] == "future" and x.get("settlement_period") == "perpetual": + mk, vol = marks.get(n, (None, None)) + out["perp"][x["base_currency"]] = dict( + instr=n, min_amt=float(x["min_trade_amount"]), + created=dt.datetime.fromtimestamp(x["creation_timestamp"] / 1000, + dt.timezone.utc).date().isoformat(), + mark=mk, min_usd=(mk * float(x["min_trade_amount"])) if mk else None, + vol24=vol) + elif x["kind"] == "spot": + out["spot"][n] = dict(min_amt=float(x["min_trade_amount"])) + elif x["kind"] == "option": + key = (cur, x["base_currency"]) + out["opt"].setdefault(key, set()).add(float(x["min_trade_amount"])) + return out + + +def sezione1(V: dict) -> dict: + sez("1) CENSIMENTO β€” lo stato REALE dei candidati (venue letto ORA, non memoria)") + if not V["read_ok"]: + print(" ⚠️ lista strumenti NON letta dal venue in questa corsa: si usa la cache.") + + sub("1a) I 19 major di XS01 contro cio' che Deribit quota DAVVERO (perp USDC lineari)") + print(f" {'asset':<7}{'su Deribit':<12}{'strumento':<26}{'quotato dal':<13}" + f"{'lotto min':>11}{'vol 24h $':>14}") + xs_ok, xs_stable = [], [] + oggi = dt.date.today() + for s in XS_UNIVERSE: + p = V["perp"].get(s) + if not p: + print(f" {s:<7}{'NO':<12}{'β€”':<26}{'β€”':<13}{'β€”':>11}{'β€”':>14}") + continue + xs_ok.append(s) + eta = (oggi - dt.date.fromisoformat(p["created"])).days + if eta >= 365: + xs_stable.append(s) + mu = f"${p['min_usd']:.2f}" if p["min_usd"] else "n/d" + v = f"{p['vol24']:,.0f}" if p["vol24"] is not None else "NON QUOTATO" + print(f" {s:<7}{'SI':<12}{p['instr']:<26}{p['created']:<13}{mu:>11}{v:>14}") + print(f"\n -> gambe su Deribit: {len(xs_ok)}/19 ({', '.join(xs_ok)})") + print(f" -> di cui quotate da >=1 anno: {len(xs_stable)}/19 ({', '.join(xs_stable)})") + print(" ⚠️ Deribit sta ampliando la lista PROPRIO ORA: le gambe piu' recenti hanno giorni") + print(" di vita e volume ~0. Una gamba quotata NON e' una gamba negoziabile.") + + sub("1b) LOTTO MINIMO DELLE OPZIONI β€” la famiglia conta (5a occorrenza dello schema fee_watch)") + for (cur, base), mins in sorted(V["opt"].items()): + if base not in ("BTC", "ETH"): + continue + m = min(mins) + mk = (V["perp"].get(base) or {}).get("mark") + fam = "USDC-LINEARE" if cur == "USDC" else "INVERSE" + usd = f"${m * mk:,.0f}" if mk else "n/d" + print(f" {fam:<14} {base:<4} min {m:<8} = {usd:>9} di sottostante per lotto") + print("\n πŸ“Œ Il numero pubblicato il 30/07 (\"BTC min 0.1 = $6.210 -> FUORI; ETH 1 contratto") + print(" = $1.832\") e' della famiglia INVERSE, che un conto USDC non puo' marginare.") + print(" Sulla famiglia che il conto PUO' usare il lotto e' ~10x piu' piccolo.") + + return dict(xs_deribit=xs_ok, xs_stable=xs_stable) + + +def sezione1c(V: dict, cens: dict) -> None: + sub("1c) LA TABELLA β€” stato REALE di ogni candidato a terzo sleeve") + eth = (V["perp"].get("ETH") or {}).get("mark") or 0.0 + btc = (V["perp"].get("BTC") or {}).get("mark") or 0.0 + opt_eth = min(V["opt"].get(("USDC", "ETH"), {0.1})) * eth + opt_btc = min(V["opt"].get(("USDC", "BTC"), {0.01})) * btc + n_d, n_s = len(cens["xs_deribit"]), len(cens["xs_stable"]) + rows = [ + ("STATARB-RESID", "BTC+ETH perp USDC", "SI", f"${MIN_ORDER:.0f} (soglia NOSTRA)", + "SI β€” netta", "27/09 (aperto)"), + ("DVOLSPREAD", "BTC+ETH perp USDC", "SI", f"${MIN_ORDER:.0f} (soglia NOSTRA)", + "SI β€” netta", "24/10 kill Β· 24/01 dec."), + ("PREVDAY", "BTC+ETH perp USDC", "SI", f"${MIN_ORDER:.0f} (soglia NOSTRA)", + "SI β€” netta", "nessuno (mai registrato)"), + ("XS01 (19 gambe)", "19 alt", "NO β€” 5 mancano", + f"{n_d}/19 quotate, {n_s} da >=1a", "PARZIALE (BTC/ETH)", "nessuno"), + (f"XS01-D ({n_s} gambe)", f"{n_s} alt su Deribit", "SI (ridotta)", + "$0,09-$7,72 dal venue", "PARZIALE (BTC/ETH)", "nessuno"), + ("XSR01", "50 alt Hyperliquid", "NO β€” venue diverso", + "n/d su Deribit", "NO", "23/10 (aperto)"), + ("VRP01", "opzioni BTC/ETH USDC", "SI (ETH)", + f"lotto ETH ${opt_eth:,.0f} Β· BTC ${opt_btc:,.0f}", "NO β€” altro strumento", + "nessuno (regola short-vol)"), + ("GTAA01", "6 ETF USA (IB)", "NO β€” altro venue", + "PRIIPs: ordine RIFIUTATO", "NO", "nessuno"), + ] + print(f" {'candidato':<19}{'strumenti':<24}{'su Deribit?':<19}{'capitale/lotto min':<28}" + f"{'netta col libro?':<22}{'gate'}") + print(" " + "-" * 100) + for r in rows: + print(f" {r[0]:<19}{r[1]:<24}{r[2]:<19}{r[3]:<28}{r[4]:<22}{r[5]}") + print("\n Finestra FORWARD dei monitor (SOLA LETTURA di data/paper_*/, nessuna scrittura):") + print(f" {'monitor':<12}{'barre':>7}{'ultima':>13}{'Sh fwd MODELED':>17} stato Β§32") + for nm, d, cad, sano in (("STATARB", "paper_statarb", 365.25, False), + ("XSR01", "paper_xsr", 365.25, False), + ("DVOLSPREAD", "paper_dvolspread", 365.25, False), + ("PREVDAY", "paper_prevday", 365.25 * 24, True)): + try: + st = json.loads((ROOT / "data" / d / "state.json").read_text()) + ts = pd.Timestamp(int(st["last_ts"]), unit="ms", tz="UTC") + rr = [json.loads(l)["net_modeled"] + for l in (ROOT / "data" / d / "returns.jsonl").read_text().splitlines() if l.strip()] + v = np.asarray(rr, float) + sh = (float(v.mean() / v.std() * np.sqrt(cad)) if len(v) > 3 and v.std() > 0 + else float("nan")) + flag = "SANO (1259/1319)" if sano else "ROTTO: frazione di giorno" + print(f" {nm:<12}{st.get('n_bars', len(v)):>7}{ts.strftime('%Y-%m-%d'):>13}" + f"{sh:>17.2f} {flag}") + except Exception as e: # noqa: BLE001 + print(f" {nm:<12} stato illeggibile ({type(e).__name__})") + print(" ⚠️ Β§32 (22/08): `advance()` consuma la barra del giorno IN CORSO -> 4 monitor su 6") + print(" registrano una FRAZIONE del giorno (STATARB ~4 min/giorno): quei tre Sharpe") + print(" NON sono un risultato. `paper_prevday` e' l'unico misurato SANO (orario), ed e'") + print(" l'unica lettura forward indipendente disponibile in questo filone.") + print(" ⚠️ Anche quello sano ha ~63 giorni: SE(Sharpe) ~ 2,3 -> non decide niente da solo.") + print("\n ⚠️ 'gate aperto' = pre-registrato e NON scaduto: in questo script si dice cosa") + print(" succederebbe SE passasse, non si decide al suo posto (anticipare un gate") + print(" pre-registrato E' selezione-sull'hold-out, ed e' una regola scritta).") + + +# ============================================================================================= +# 2) IL NETTING β€” misurato sul CODICE di produzione, non ricordato +# ============================================================================================= +def sezione2() -> None: + sez("2) IL VINCOLO DIMENTICATO β€” il NETTING su strumento unico (letto dal sorgente)") + src_net = inspect.getsource(LB.book_net_target) + src_ord = inspect.getsource(LB.build_book_order) + print(f" src/live/deribit.INSTRUMENT = {DB.INSTRUMENT}") + print(f" src/live/deribit._CONTRACT = {len(DB._CONTRACT)} strumenti dichiarati: " + f"{list(DB._CONTRACT)}") + print(f" src/live/book.W_TP01 / W_SKH = {W_TP} / {W_SKH}") + uno = ("W_TP01 * max(tp_frac" in src_net) and ("W_SKH * float(skh_sign)" in src_net) + print(f" `book_net_target` somma i due sleeve in UN solo numero per asset : {uno}" + f" (riga: `raw = weight * equity * (W_TP01*tp_frac + W_SKH*skh_sign)`)") + print(f" `build_book_order` manda UN ordine per asset, soglia min_usd : " + f"{'min_usd' in src_ord}") + print("\n CONSEGUENZA, e nessuno l'aveva messa in tabella:") + print(" * un terzo sleeve DIREZIONALE su BTC/ETH e' un TERZO ADDENDO della stessa formula:") + print(" non aggiunge un ordine, non aggiunge un min-order, non aggiunge margine lordo β€”") + print(" e non aggiunge nemmeno SEPARAZIONE (nessuna gamba puo' avere il proprio stop).") + print(" E' l'esatto motivo per cui la raccomandazione T1 del 26/07 fu abbandonata.") + print(" * un terzo sleeve su ALTRI strumenti (opzioni, spot, alt-perp) richiede una riga in") + print(f" `_CONTRACT` (oggi {len(DB._CONTRACT)} voci, CABLATE A MANO) piu' il ramo di") + print(" sizing corrispondente = CODICE su un percorso con soldi veri, piu' un lotto") + print(" minimo e un margine propri.") + print(" * il min-order si applica al DELTA NETTO: due sleeve che si contraddicono possono") + print(" produrre zero ordini. E' un vantaggio di costo E una perdita di controllo.") + + +# ============================================================================================= +# 3) IL FUNDING (importato) + 4) LE SERIE ANCORATE +# ============================================================================================= +def get_rate() -> dict: + """Tassi di funding orari per asset (LINEARE dove esiste, INVERSE come proxy prima).""" + F = {} + for instr, (start, a, kind) in FU.INSTR.items(): + d = FU.fetch_funding(instr, start).copy() + d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True) + F[instr] = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt") + return FU.build_rate(F) + + +def tp01_at(h: int, rate: dict | None) -> pd.Series: + """TP01 all'ancora oraria h (replica di sleeves._tp01_returns), funding opzionale.""" + tp = TrendPortfolio(**CANONICAL) + series = {} + for a in ("BTC", "ETH"): + df = A_TP.daily_off(a, h) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + r = simple_returns(df["close"].values.astype(float)) + tgt = tp.target_series(df) + held = np.zeros(len(tgt)); held[1:] = tgt[:-1] + net = held * r - tp.fee_side * np.abs(np.diff(held, prepend=0.0)) + if rate is not None: + net = net - held * FU.daily_funding(rate[a], idx) + net[0] = 0.0 + series[a] = pd.Series(np.clip(net, -0.99, None), index=idx) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + return to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)) + + +def skh_at(off: int, rate: dict | None) -> pd.Series: + """SKH01 all'offset `off` della griglia 230m, lente CANONICA (quella dello sleeve).""" + return to_daily(FU.skh_book(off, "canonical", rate)) + + +def _joint_off(h: int) -> pd.DataFrame: + """Frame BTC/ETH allineato all'ancora oraria h, colonne di OS.build_joint.""" + b = A_TP.daily_off("BTC", h)[["timestamp", "datetime", "close"]].rename(columns={"close": "cb"}) + e = A_TP.daily_off("ETH", h)[["timestamp", "close"]].rename(columns={"close": "ce"}) + j = b.merge(e, on="timestamp", how="inner").sort_values("timestamp").reset_index(drop=True) + j["r_btc"] = al.simple_returns(j["cb"].values) + j["r_eth"] = al.simple_returns(j["ce"].values) + j["log_ratio"] = np.log(j["ce"].values / j["cb"].values) + return j + + +def statarb_at(h: int, rate: dict | None): + """STATARB-RESID (W=45, sgn=+1) all'ancora oraria h. Ritorna (serie daily, pos).""" + j = _joint_off(h) + pos = np.nan_to_num(np.asarray(OS.f_statarb_resid(W=45, sgn=+1)(j), float)) + idx = pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True)) + s = OS.spread_ret(j) + held = np.zeros(len(pos)); held[1:] = pos[:-1] + net = held * s - FEE_SIDE * 2.0 * np.abs(np.diff(held, prepend=0.0)) + if rate is not None: # +1 = long ETH / short BTC -> paga f_eth - f_btc + net = net - held * (FU.daily_funding(rate["ETH"], idx) - FU.daily_funding(rate["BTC"], idx)) + net[0] = 0.0 + return to_daily(pd.Series(net, index=idx)), pos # pos NON shiftata (7b shifta da se') + + +DVS_FROZEN = dict(zwin=180, tanh_k=2.0, lw=0.6, zw=1.1, tgt=0.17, svw=60) + + +def dvolspread(rate: dict | None): + """DVOLSPREAD coi parametri CONGELATI del monitor. Ancora non variabile: il DVOL e' 1d.""" + btc, eth = ol.aligned() + wb, we = dvs_make_book(**DVS_FROZEN)(btc, eth) + wb = np.clip(np.nan_to_num(np.asarray(wb, float)), -1, 1) + we = np.clip(np.nan_to_num(np.asarray(we, float)), -1, 1) + n = len(btc) + rb = al.simple_returns(btc["close"].values.astype(float)) + re = al.simple_returns(eth["close"].values.astype(float)) + pb = np.zeros(n); pb[1:] = wb[:-1] + pe = np.zeros(n); pe[1:] = we[:-1] + net = pb * rb + pe * re - FEE_SIDE * (np.abs(np.diff(pb, prepend=0.0)) + + np.abs(np.diff(pe, prepend=0.0))) + idx = pd.DatetimeIndex(btc["dt"]) + if rate is not None: + net = net - (pb * FU.daily_funding(rate["BTC"], idx) + + pe * FU.daily_funding(rate["ETH"], idx)) + net[0] = 0.0 + assert float(np.max(np.abs(wb + we))) < 1e-12, "il book DVOLSPREAD non e' we == -wb" + return to_daily(pd.Series(net, index=idx)), wb # gamba BTC, NON shiftata + + +def prevday(rate: dict | None): + """PREVDAY range-breakout 1h, libro 50/50. Funding ORARIO (il segnale e' a 1h).""" + series, tk = {}, [] + for a in ("BTC", "ETH"): + df = al.get(a, "1h") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + tgt = np.nan_to_num(np.asarray(prevday_target(df), float)) + r = al.simple_returns(df["close"].values.astype(float)) + pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1] + net = pos * r - FEE_SIDE * np.abs(np.diff(pos, prepend=0.0)) + if rate is not None: + t = FU.to_ms(idx) + fh = FU.cumF(rate[a], t + FU.MSH) - FU.cumF(rate[a], t) + net = net - pos * fh + net[0] = 0.0 + series[a] = pd.Series(net, index=idx) + tk.append(np.abs(np.diff(pos, prepend=0.0))) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + d = to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)) + return d, np.concatenate(tk) + + +def xs_at(universe: list[str], phase: int, funding_drag: float = 0.0): + """XS01 su un universo arbitrario, alla fase `phase` del ciclo H. Replica ESATTA della + formula di sleeves._xsec_returns (verificata bit-exact a universo pieno / fase 0).""" + cols = {} + for sym in universe: + p = SL._HL_DIR / f"hl_{sym.lower()}_1d.parquet" + if not p.exists(): + continue + d = pd.read_parquet(p) + cols[sym] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + C = pd.concat(cols, axis=1, join="inner").sort_index().dropna() + px = C.values; n, A = px.shape + cfg = SL.XS_CFG + lookbacks, H, k, mode, tv = cfg["lookbacks"], cfg["H"], cfg["k"], cfg["mode"], cfg["target_vol"] + disp_pct, minhist = cfg.get("disp_pct", 0), cfg.get("disp_minhist", 20) + kk = min(k, A // 2) + mlb = max(lookbacks) + dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) + W = np.zeros((n, A)); w = np.zeros(A); disp_hist = [] + for i in range(n): + if i >= mlb and i % H == phase: + rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks] + disp_i = float(np.mean([r.std() for r in rLs])) + thr = (np.percentile(disp_hist, disp_pct) + if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf) + if disp_i >= thr: + score = np.zeros(A); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if cnt: + score /= cnt + order = np.argsort(score) + w = np.zeros(A); lo, hi = order[:kk], order[-kk:] + if mode == "mom": + w[hi] = 0.5 / kk; w[lo] = -0.5 / kk + else: + w[lo] = 0.5 / kk; w[hi] = -0.5 / kk + else: + w = np.zeros(A) + disp_hist.append(disp_i) + W[i] = w + gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) + turn = np.zeros(n); turn[0] = np.abs(W[0]).sum() + turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) + net = gross - turn * (0.001 / 2.0) + s = pd.Series(net, index=C.index) + rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) + scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) + out = pd.Series(s.values * scale, index=C.index) + if funding_drag: + out = out - funding_drag / 365.25 + # ticket per gamba: |Ξ”W| a ogni ribilanciamento, scalato dal vol-target del giorno + tick = np.abs(np.diff(W, axis=0, prepend=0.0)) * scale[:, None] + return to_daily(out), tick, list(C.columns) + + +def vrp_at(phase: int) -> pd.Series: + return to_daily(A_VRP.to_daily_lumped(A_VRP.combo_weekly(phase, f=SL.VRP_CFG["f"]))) + + +# ============================================================================================= +# 3) REPLICA DI CONTROLLO +# ============================================================================================= +def sezione3(rate: dict) -> dict: + sez("3) REPLICA DI CONTROLLO β€” riprodurre i numeri PUBBLICATI prima di stampare un delta") + from src.portfolio.portfolio import StrategyPortfolio + P = StrategyPortfolio(SL.deribit_book_sleeves(), capital=CAPITAL_TODAY) + ref = P.combined_daily() + m, mh = metrics(ref), metrics(ref[ref.index >= HOLDOUT]) + print(" LENTE A β€” 'sleeve' (`deribit_book_sleeves` via StrategyPortfolio): e' quella dei") + print(" numeri pubblicati. SKH01 con fill AL LIVELLO, TP01 senza min-order,") + print(" NESSUN funding.") + print(f" ShFULL {m['sharpe']:.3f} ShHOLD {mh['sharpe']:.3f} maxDD {100*m['maxdd']:.2f}%" + f" CAGR {100*m['cagr']:.2f}% n={m['n']}") + print(" atteso dal briefing: ShFULL ~1,81 / ShHOLD ~1,44 / maxDD ~9,4% -> " + f"{'RIPRODOTTO' if (abs(m['sharpe']-1.81)<0.02 and abs(mh['sharpe']-1.44)<0.02 and abs(100*m['maxdd']-9.4)<0.15) else '⚠️ NON riproduce'}") + + mine0 = combine_outer({"TP01": tp01_at(0, None), "SKH01": skh_at(0, None)}, + {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START) + j = pd.concat({"a": mine0, "b": ref}, axis=1, join="outer") + dmax = float(np.nanmax(np.abs(j["a"].values - j["b"].values))) + print(f"\n [SANITY] la mia replica ancorata a (h=0, off=0) vs lo sleeve di produzione: " + f"max|dif| = {dmax:.3e} {'OK' if dmax < 1e-12 else 'FAIL'}") + assert dmax < 1e-12, "replica ancorata NON bit-exact: tutto il resto misurerebbe altro" + + withf = combine_outer({"TP01": tp01_at(0, rate), "SKH01": skh_at(0, rate)}, + {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START) + mf, mfh = metrics(withf), metrics(withf[withf.index >= HOLDOUT]) + print(f"\n LENTE A + FUNDING (misurato Β§35, non modellato prima del 22/08):") + print(f" ShFULL {mf['sharpe']:.3f} ShHOLD {mfh['sharpe']:.3f} " + f"maxDD {100*mf['maxdd']:.2f}% CAGR {100*mf['cagr']:.2f}%") + print(f" costo del funding: dSharpe {mf['sharpe']-m['sharpe']:+.3f}, " + f"dCAGR {100*(mf['cagr']-m['cagr']):+.2f} pp") + + lp = FU.book_75_25(rate, 0, "hourly", 600.0) + mlp, mlph = metrics(lp), metrics(lp[lp.index >= HOLDOUT]) + print("\n LENTE B β€” 'path live' (`CC.book_series('hourly')` + funding): min-order $5, fill al") + print(" close 5m del prossimo confine orario. E' cio' che il cron esegue.") + print(f" ShFULL {mlp['sharpe']:.3f} ShHOLD {mlph['sharpe']:.3f} " + f"maxDD {100*mlp['maxdd']:.2f}% CAGR {100*mlp['cagr']:.2f}%") + print("\n πŸ“Œ Le due lenti NON danno lo stesso libro (1,81 contro 1,50 di Sharpe FULL). La") + print(" misura di questo filone gira sulla LENTE A col funding, perche' e' quella in cui") + print(" sono espressi TUTTI i candidati (serie di sleeve) e quella dei numeri pubblicati.") + print(" Il delta e' comunque un confronto APPAIATO: la lente si cancella in gran parte.") + return dict(ref=ref, m=m, mh=mh, base_f=withf) + + +# ============================================================================================= +# 4) COSTRUZIONE DELLE SERIE ANCORATE (con cache) +# ============================================================================================= +def build_series(rate: dict, cens: dict, force: bool = False) -> dict: + if CACHE.exists() and not force: + try: + S = pickle.loads(CACHE.read_bytes()) + if S.get("_universe") == (cens["xs_stable"], cens["xs_deribit"]): + print(f" {clock()} serie da cache ({CACHE.name})") + return S + except Exception: # noqa: BLE001 + pass + S: dict = {"_universe": (cens["xs_stable"], cens["xs_deribit"])} + t = time.time() + S["TP01"] = {h: tp01_at(h, rate) for h in TP_HOURS} + print(f" {clock()} TP01 x{len(TP_HOURS)} ancore orarie ({time.time()-t:.0f}s)") + t = time.time() + S["SKH01"] = {o: skh_at(o, rate) for o in SKH_OFFSETS} + print(f" {clock()} SKH01 x{len(SKH_OFFSETS)} offset di griglia ({time.time()-t:.0f}s)") + t = time.time() + sa = {h: statarb_at(h, rate) for h in TP_HOURS} + S["STATARB"] = {h: v[0] for h, v in sa.items()} + S["_statarb_pos"] = sa[0][1] + print(f" {clock()} STATARB x{len(TP_HOURS)} ancore ({time.time()-t:.0f}s)") + t = time.time() + d, dpos = dvolspread(rate) + S["DVOLSPREAD"] = {0: d}; S["_dvs_pos"] = dpos + p, ptk = prevday(rate) + S["PREVDAY"] = {0: p}; S["_prevday_turn"] = ptk + print(f" {clock()} DVOLSPREAD + PREVDAY ({time.time()-t:.0f}s)") + t = time.time() + xs19 = {ph: xs_at(XS_UNIVERSE, ph, XS_FUNDING_DRAG_YR) for ph in XS_PHASES} + S["XS01"] = {ph: v[0] for ph, v in xs19.items()} + S["_xs19_tick"] = xs19[0][1]; S["_xs19_cols"] = xs19[0][2] + xsd = {ph: xs_at(cens["xs_stable"], ph, XS_FUNDING_DRAG_YR) for ph in XS_PHASES} + S["XS01-D"] = {ph: v[0] for ph, v in xsd.items()} + S["_xsd_tick"] = xsd[0][1]; S["_xsd_cols"] = xsd[0][2] + xsd14 = {ph: xs_at(cens["xs_deribit"], ph, XS_FUNDING_DRAG_YR) for ph in XS_PHASES} + S["XS01-D14"] = {ph: v[0] for ph, v in xsd14.items()} + S["_xsd14_cols"] = xsd14[0][2] + print(f" {clock()} XS01 (19 gambe) + XS01-D ({len(cens['xs_stable'])} gambe) x10 fasi " + f"({time.time()-t:.0f}s)") + t = time.time() + S["VRP01"] = {ph: vrp_at(ph) for ph in VRP_PHASES} + print(f" {clock()} VRP01 x{len(VRP_PHASES)} fasi ({time.time()-t:.0f}s)") + CACHE.parent.mkdir(parents=True, exist_ok=True) + CACHE.write_bytes(pickle.dumps(S)) + return S + + +def sezione4(S: dict, cens: dict) -> None: + sez("4) LE SERIE ANCORATE β€” sanity obbligatorio, poi il profilo standalone") + x0, _, cols = xs_at(XS_UNIVERSE, 0, 0.0) + ref = to_daily(SL._xsec_returns()) + jj = pd.concat({"a": x0, "b": ref}, axis=1, join="outer") + dm = float(np.nanmax(np.abs(jj["a"].values - jj["b"].values))) + print(f" [SANITY] xs_at(19 gambe, fase 0, drag 0) vs sleeves._xsec_returns : " + f"max|dif| = {dm:.3e} {'OK' if dm < 1e-12 else 'FAIL'}") + assert dm < 1e-12, "la replica XS01 non e' bit-exact" + v0 = vrp_at(0); vr = to_daily(SL._vrp_combo_returns()) + jj = pd.concat({"a": v0, "b": vr}, axis=1, join="outer") + dm2 = float(np.nanmax(np.abs(np.nan_to_num(jj["a"].values) - np.nan_to_num(jj["b"].values)))) + print(f" [SANITY] vrp_at(fase 0) vs sleeves._vrp_combo_returns : " + f"max|dif| = {dm2:.3e} {'OK' if dm2 < 1e-12 else 'ATTENZIONE'}") + + jj = OS.build_joint("1d") + ev = OS.eval_spread(jj, OS.f_statarb_resid(W=45, sgn=+1)(jj)) + mine = metrics(statarb_at(0, None)[0])["sharpe"] + print(f" [SANITY] STATARB senza funding, mio percorso {mine:.3f} vs il motore originale " + f"`OS.eval_spread` {ev['full']['sharpe']:.3f} (scarto {mine-ev['full']['sharpe']:+.3f})") + print(" -> `daily_off(h=0)` e `al.get('1d')` sono lo STESSO resample: due percorsi") + print(" indipendenti danno la stessa serie, quindi l'ancora h=0 e' la canonica.") + + print(f"\n {'candidato':<16}{'da':>12}{'n':>7}{'ShFULL':>9}{'ShHOLD':>9}{'maxDD':>9}" + f"{'CAGR':>9}{'corr libro':>12}") + base = combine_outer({"TP01": S["TP01"][0], "SKH01": S["SKH01"][0]}, + {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START) + for nm in CANDIDATI: + s = S[nm][sorted(S[nm])[0]] + s = s[s.index >= BOOK_START] if nm != "XS01" else s + m = metrics(s); mh = metrics(s[s.index >= HOLDOUT]) + j = pd.concat({"a": s, "b": base}, axis=1, join="inner").dropna() + c = float(j["a"].corr(j["b"])) if len(j) > 60 else float("nan") + print(f" {nm:<16}{str(s.index.min().date()):>12}{m['n']:>7}{m['sharpe']:>9.3f}" + f"{mh['sharpe']:>9.3f}{100*m['maxdd']:>8.1f}%{100*m['cagr']:>8.1f}%{c:>12.3f}") + print(f"\n NB XS01/XS01-D includono il drag di funding dichiarato " + f"({100*XS_FUNDING_DRAG_YR:.2f}%/anno, Β§37); gli altri hanno il funding modellato " + "gamba per gamba.") + print(f" XS01-D gira su {len(cens['xs_stable'])} gambe: {', '.join(cens['xs_stable'])}") + + +def sezione4b(S: dict) -> None: + sub("4b) `marginal_vs_tp01` β€” il verdetto STANDARD del progetto su ogni candidato") + print(" Un candidato direzionale/relative-value si giudica su ADDS (corr, uplift OOS,") + print(" alpha residua, robust_oos multicut, has_insample_edge, is_hedge), non sullo Sharpe.") + print(f"\n {'candidato':<12}{'verdetto':>10}{'corr':>8}{'robust_oos':>12}" + f"{'insample_edge':>15}{'is_hedge':>10}{'beats_noise':>13}") + for c in CANDIDATI: + d = S[c][sorted(S[c])[0]] + try: + m = al.marginal_vs_tp01(d) + except Exception as e: # noqa: BLE001 + print(f" {c:<12} non calcolabile: {type(e).__name__}: {e}") + continue + cf = m.get("corr_full") + print(f" {c:<12}{str(m.get('marginal_verdict')):>10}" + f"{(cf if cf is not None else float('nan')):>8.3f}" + f"{str(m.get('robust_oos')):>12}{str(m.get('has_insample_edge')):>15}" + f"{str(m.get('is_hedge')):>10}{str(m.get('beats_noise_null')):>13}") + print("\n ⚠️ Su una serie che comincia nel 2024 (XS01*) `marginal_vs_tp01` ha poco campione") + print(" in-sample: il suo verdetto li' e' debole per costruzione, non informativo.") + + +def sezione4c(cens: dict, n_sub: int = 100) -> None: + sub("4c) IL COLLASSO DI XS01-D: e' l'AMPIEZZA o e' QUEL sottoinsieme? (null dei subset)") + k = len(cens["xs_stable"]) + print(f" XS01 a 19 gambe fa Sharpe ~1,27; ristretto alle {k} quotate su Deribit da >=1 anno") + print(" crolla. Due spiegazioni possibili, e vanno separate: (a) con poche gambe il") + print(f" long-5/short-5 prende 10 posizioni su {k} e non seleziona piu' niente (AMPIEZZA);") + print(" (b) le gambe che Deribit non quota sono proprio quelle che portavano l'edge.") + print(f" Test: {n_sub} sottoinsiemi CASUALI di {k} gambe fra le 19, stesso meccanismo,") + print(" e si guarda dove cade quello di Deribit. (Stessa forma del null dei 6-subset che") + print(" il 25/07 mise GTAA6 al 95Β° pctl.)") + rng = np.random.default_rng(SEED) + sh = [] + for _ in range(n_sub): + u = list(rng.choice(XS_UNIVERSE, size=k, replace=False)) + sh.append(metrics(xs_at(u, 0, XS_FUNDING_DRAG_YR)[0])["sharpe"]) + sh = np.array(sh) + mine = metrics(xs_at(cens["xs_stable"], 0, XS_FUNDING_DRAG_YR)[0])["sharpe"] + pct = float((sh < mine).mean() * 100.0) + print(f"\n {n_sub} sottoinsiemi casuali da {k}: mediana {np.median(sh):.3f} " + f"[p10 {np.percentile(sh,10):.3f}, p90 {np.percentile(sh,90):.3f}] " + f"min {sh.min():.3f} max {sh.max():.3f}") + print(f" il sottoinsieme DERIBIT: {mine:.3f} -> {pct:.0f}Β° percentile") + print(f" riferimento a 19 gambe: {metrics(xs_at(XS_UNIVERSE, 0, XS_FUNDING_DRAG_YR)[0])['sharpe']:.3f}") + full = metrics(xs_at(XS_UNIVERSE, 0, XS_FUNDING_DRAG_YR)[0])["sharpe"] + med = float(np.median(sh)) + q_ampiezza = (full - med) / (full - mine) if abs(full - mine) > 1e-9 else float("nan") + print(f"\n -> DIAGNOSI, e sono DUE effetti, non uno (la mia prima stesura ne dichiarava uno") + print(" solo e sarebbe stata una semplificazione comoda):") + print(f" (1) AMPIEZZA: gia' il sottoinsieme MEDIANO da {k} gambe scende {full:.3f} -> " + f"{med:.3f}.") + print(f" Vale il {100*q_ampiezza:.0f}% della caduta totale, e nessun capitale la ripara:") + print(f" con {k} gambe il long-5/short-5 tiene 10 posizioni su {k}.") + print(f" (2) SFORTUNA DI LISTINO: dentro quella distribuzione il sottoinsieme che Deribit") + print(f" quota davvero sta al {pct:.0f}Β° percentile ({mine:.3f} contro mediana {med:.3f}).") + print(f" Le 8 gambe mancanti non sono un campione casuale: sono gli alt piu' recenti") + print(" e piu' dispersi, cioe' quelli da cui un cross-sectional prende il segnale.") + print(" Conseguenza operativa identica in entrambi i casi: aspettare che Deribit quoti") + print(f" 2-3 alt in piu' non basta β€” il sottoinsieme mediano a {k} gambe e' gia' meta'") + print(" del 19-gambe, e la ricostruzione dell'universo pieno passa dal SECONDO VENUE.") + print(" ⚠️ Questo test gira sui prezzi HYPERLIQUID anche per il sottoinsieme 'Deribit': e'") + print(" il segnale, non il fill. La divergenza fra venue su un ranking cross-sezionale e'") + print(" gia' stata misurata trascurabile (22/08: corr 0,9991 fra i due venue).") + + +# ============================================================================================= +# 5) LA MISURA β€” delta APPAIATI sulla banda d'ancora congiunta +# ============================================================================================= +PASSED_TXT = "?" +CANDIDATI = ["XS01", "XS01-D", "XS01-D14", "STATARB", "DVOLSPREAD", "PREVDAY", "VRP01"] +ANCHOR_OF = {"XS01": XS_PHASES, "XS01-D": XS_PHASES, "XS01-D14": XS_PHASES, + "STATARB": TP_HOURS, + "DVOLSPREAD": (0,), "PREVDAY": (0,), "VRP01": VRP_PHASES} + + +def _stats(s: pd.Series) -> tuple[float, float, float, float]: + m = metrics(s) + mh = metrics(s[s.index >= HOLDOUT]) + return m["sharpe"], mh["sharpe"], m["cagr"], m["maxdd"] + + +def sezione5(S: dict, draws: int) -> dict: + sez(f"5) LA MISURA β€” dSharpe / d drift / d maxDD del LIBRO LIVE con un terzo sleeve") + print(f" Spazio d'ancora congiunto: TP01 x{len(TP_HOURS)} Β· SKH01 x{len(SKH_OFFSETS)} Β· " + f"candidato x(1..{len(XS_PHASES)}).") + print(f" {draws} estrazioni UNIFORMI INDIPENDENTI (seed {SEED}); la statistica e' la") + print(" MEDIANA DELLE DIFFERENZE APPAIATE (mai la differenza delle mediane: cadono su") + print(" ancore diverse β€” errore ribaltante catturato il 26/07).") + rng = np.random.default_rng(SEED) + hs = rng.choice(TP_HOURS, size=draws) + os_ = rng.choice(SKH_OFFSETS, size=draws) + ph = {c: rng.choice(ANCHOR_OF[c], size=draws) for c in CANDIDATI} + out: dict = {c: {w: {"sh": [], "shh": [], "cagr": [], "dd": []} for w in WEIGHTS_TESTED} + for c in CANDIDATI} + base_stats = [] + t = time.time() + for i in range(draws): + cols2 = {"TP01": S["TP01"][hs[i]], "SKH01": S["SKH01"][os_[i]]} + b = combine_outer(cols2, {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START) + bs = _stats(b) + base_stats.append(bs) + for c in CANDIDATI: + cols3 = dict(cols2); cols3["CAND"] = S[c][ph[c][i]] + for w in WEIGHTS_TESTED: + a = combine_outer(cols3, {"TP01": W_TP * (1 - w), "SKH01": W_SKH * (1 - w), + "CAND": w}, lo=BOOK_START) + st = _stats(a) + out[c][w]["sh"].append(st[0] - bs[0]) + out[c][w]["shh"].append(st[1] - bs[1]) + out[c][w]["cagr"].append(st[2] - bs[2]) + out[c][w]["dd"].append(st[3] - bs[3]) + if i == 4: + print(f" {clock()} ritmo: {(time.time()-t)/5:.2f}s/estrazione -> stimati " + f"{(time.time()-t)/5*draws:.0f}s") + B = np.array(base_stats) + print(f"\n LIBRO LIVE DE-LUCKATO (2 sleeve, funding dentro), mediana delle {draws} estrazioni:") + print(f" ShFULL {np.median(B[:,0]):.3f} [p10 {np.percentile(B[:,0],10):.3f}, " + f"p90 {np.percentile(B[:,0],90):.3f}] ShHOLD {np.median(B[:,1]):.3f} " + f"CAGR {100*np.median(B[:,2]):.2f}% maxDD {100*np.median(B[:,3]):.2f}%") + can = _stats(combine_outer({"TP01": S["TP01"][0], "SKH01": S["SKH01"][0]}, + {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START)) + pc = float((B[:, 0] < can[0]).mean() * 100.0) + print(f" ancora CANONICA (h=0, off=0): ShFULL {can[0]:.3f} = {pc:.1f}Β° pctl della banda " + f"-> fortuna d'ancora {can[0]-np.median(B[:,0]):+.3f}") + + print(" ⚠️ `d maxDD` esce ~0,00 per XS01*: il maxDD FULL del libro e' fissato dal 2022, e") + print(" quei candidati cominciano nel 2024 -> non possono toccarlo. Non e' 'non protegge',") + print(" e' 'non c'era'. Per loro la colonna che informa e' d maxDD sull'hold-out, non FULL.") + for c in CANDIDATI: + sub(f"5.{CANDIDATI.index(c)+1}) {c}") + print(f" {'peso':>6}{'dShFULL':>12}{'>0':>7}{'[p10,p90]':>20}{'dShHOLD':>11}{'>0':>7}" + f"{'d CAGR pp':>12}{'d maxDD pp':>13}{'>0(peggio)':>12}") + for w in WEIGHTS_TESTED: + r = out[c][w] + a = np.array(r["sh"]); ah = np.array(r["shh"]) + ac = np.array(r["cagr"]); ad = np.array(r["dd"]) + print(f" {w:>5.0%}{np.median(a):>+12.4f}{100*(a>0).mean():>6.0f}%" + f" [{np.percentile(a,10):+.3f},{np.percentile(a,90):+.3f}]" + f"{np.median(ah):>+11.4f}{100*(ah>0).mean():>6.0f}%" + f"{100*np.median(ac):>+12.2f}{100*np.median(ad):>+13.2f}" + f"{100*(ad>0).mean():>11.0f}%") + return dict(out=out, base=B) + + +def sezione5b(S: dict) -> None: + sez("5b) A COSA SERVE UN dSharpe β€” il guadagno e' RITORNO o solo RISCHIO IN MENO?") + print(" In sezione 5 quasi tutti i candidati alzano lo Sharpe e ABBASSANO il drift: a") + print(" nozionale invariato il libro guadagna MENO. E' la regola gia' scritta il 25/07") + print(" (\"un diversificatore a basso CAGR si giudica a ISO-RISCHIO, mai a iso-nozionale\").") + print(" ⚠️ Il null del de-levering qui sarebbe DEGENERE (regola 22/08 Β§8d: sh(kΒ·base) ≑") + print(" sh(base)) -> non e' un test, e' un cambio di unita'. La domanda giusta e' l'altra:") + print(" quanto vale quel dSharpe SE lo si riconverte in ritorno, e la leva ESISTE?") + print(" Qui: si leva il libro a 2 sleeve del fattore k che ne eguaglia la vol al libro a 3,") + print(" e si confrontano i DRIFT. k<1 = il terzo sleeve compra rischio in meno.") + print(" ⚠️ E la leva NON esiste come knob: `config/live.json` non ha una chiave di scala") + print(" (Β§38, verificato: servirebbe toccare W_TP01/W_SKH in src/live/book.py = CODICE") + print(" su un percorso con soldi veri). Finche' non esiste, la colonna 'd drift' di") + print(" sezione 5 e' quella che si incassa, e questa e' quella che si potrebbe incassare.") + base = combine_outer({"TP01": S["TP01"][0], "SKH01": S["SKH01"][0]}, + {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START) + rb = np.asarray(base.values, float) + v2 = float(rb.std() * np.sqrt(365.25)) + d2 = float(np.mean(np.log1p(rb)) * 365.25) + print(f"\n libro a 2 sleeve (ancora canonica): vol {100*v2:.2f}% drift log {100*d2:.2f}%/anno") + print(f"\n {'candidato':<12}{'peso':>6}{'vol 3sl':>10}{'k iso-vol':>11}" + f"{'drift 2sl levato':>18}{'drift 3sl':>12}{'d drift pp':>12}{'verdetto':>14}") + for c in CANDIDATI: + for w in (0.15,): + cols = {"TP01": S["TP01"][0], "SKH01": S["SKH01"][0], "CAND": S[c][sorted(S[c])[0]]} + a = combine_outer(cols, {"TP01": W_TP * (1 - w), "SKH01": W_SKH * (1 - w), "CAND": w}, + lo=BOOK_START) + # confronto sulla FINESTRA COMUNE: uno sleeve che parte nel 2024 non si giudica + # contro un libro che comincia nel 2019 (sarebbero due campioni diversi). + j = pd.concat({"a": a, "b": base}, axis=1, join="inner").dropna() + ra = np.asarray(j["a"].values, float); rbb = np.asarray(j["b"].values, float) + v3 = float(ra.std() * np.sqrt(365.25)); vb = float(rbb.std() * np.sqrt(365.25)) + k = v3 / vb if vb > 0 else float("nan") + dr3 = float(np.mean(np.log1p(ra)) * 365.25) + dr2k = float(np.mean(np.log1p(np.clip(k * rbb, -0.99, None))) * 365.25) + dd = 100 * (dr3 - dr2k) + verdict = "AGGIUNGE" if dd > 0.05 else ("PEGGIORA" if dd < -0.05 else "neutro") + print(f" {c:<12}{w:>5.0%}{100*v3:>9.2f}%{k:>11.3f}{100*dr2k:>17.2f}%" + f"{100*dr3:>11.2f}%{dd:>+12.2f}{verdict:>14}") + print("\n (finestra COMUNE ai due libri: per XS01* e VRP01 e' la loro storia, non il 2019+.)") + + +# ============================================================================================= +# 6) weights_tilt_null +# ============================================================================================= +def sezione6(S: dict) -> dict: + sez("6) `weights_tilt_null` β€” IL gate del progetto per ogni cambio di pesi") + print(" Ogni proposta di cambio pesi si giudica contro il null dei tilt casuali") + print(" cap-respecting: gate_pass solo se delta_insample >= 0 E percentile < firma") + print(f" best-of-k. k_seen = {len(CANDIDATI)}x{len(WEIGHTS_TESTED)} = " + f"{len(CANDIDATI)*len(WEIGHTS_TESTED)} configurazioni guardate sull'hold-out qui.") + k_seen = len(CANDIDATI) * len(WEIGHTS_TESTED) + res = {} + print(f"\n {'candidato':<14}{'peso':>6}{'d_insample':>13}{'d_hold':>10}{'d_full':>10}" + f"{'pctl_hold':>11}{'best-of-k':>11}{'rnd>cur':>9}{'GATE':>9}") + for c in CANDIDATI: + cols = {"TP01": S["TP01"][0], "SKH01": S["SKH01"][0], c: S[c][sorted(S[c])[0]]} + w_cur = {"TP01": W_TP, "SKH01": W_SKH, c: 0.0} + for w in WEIGHTS_TESTED: + w_prop = {"TP01": W_TP * (1 - w), "SKH01": W_SKH * (1 - w), c: w} + try: + r = weights_tilt_null(cols, w_cur, w_prop, floor=0.03, n=300, k_seen=k_seen) + except Exception as e: # noqa: BLE001 + print(f" {c:<14}{w:>5.0%} non calcolabile: {type(e).__name__}") + continue + res[(c, w)] = r + print(f" {c:<14}{w:>5.0%}{r['delta_insample']:>+13.4f}{r['delta_hold']:>+10.4f}" + f"{r['delta_full']:>+10.4f}{r['pctl_hold']:>10.1f}Β°{r['bestofk_pctl']:>10.1f}Β°" + f"{r['frac_random_beat_hold']:>9.2f}" + f"{('PASS' if r['gate_pass'] else 'FAIL'):>9}") + npass = sum(1 for v in res.values() if v["gate_pass"]) + print(f"\n PASS {npass}/{len(res)}. ⚠️ VA LETTO BENE, e la colonna che conta e' `rnd>cur`:") + print(" e' la frazione di tilt CASUALI a 3 sleeve che battono il libro attuale sull'hold-out.") + print(" Dove vale 0,9 il claim \"questo candidato migliora l'hold-out\" e' GENERICO: lo fa") + print(" quasi qualunque cosa gli si metta accanto, e il gate passa proprio perche' la") + print(" proposta NON e' eccezionale fra i tilt casuali (pctl basso = non e' un best-of-k).") + print(" `weights_tilt_null` e' NECESSARIO, non sufficiente β€” e' scritto nel suo docstring.") + print(" βœ… Controllo positivo: il gate HA potenza β€” fallisce su XS01-D a 10/15/20% (dove il") + print(" delta in-sample e' negativo). Un gate che passa tutto sarebbe indistinguibile") + print(" da uno rotto.") + return res + + +# ============================================================================================= +# 7) ESEGUIBILITA' PER CAPITALE +# ============================================================================================= +def _gated_sharpe(pos: np.ndarray, ret: np.ndarray, idx, alloc: float, + n_legs: int, fee_side: float = FEE_SIDE) -> tuple[float, int, float]: + """Sharpe con min-order per GAMBA: un Ξ” di nozionale sotto $5 non si esegue.""" + per_leg = alloc / max(n_legs, 1) + held = np.empty(len(pos)); cur = 0.0; n_tr = 0; n_skip = 0 + for i in range(len(pos)): + if abs(pos[i] - cur) * per_leg >= MIN_ORDER: + cur = pos[i]; n_tr += 1 + elif abs(pos[i] - cur) > 1e-12: + n_skip += 1 + held[i] = cur + h = np.zeros(len(held)); h[1:] = held[:-1] + net = h * ret - fee_side * n_legs * np.abs(np.diff(h, prepend=0.0)); net[0] = 0.0 + s = pd.Series(net, index=idx) + return metrics(to_daily(s))["sharpe"], n_tr, (n_skip / max(n_tr + n_skip, 1)) + + +def sezione7(S: dict, V: dict, cens: dict) -> None: + sez("7) ESEGUIBILITA' PER CAPITALE β€” ticket per gamba, min-order, haircut") + print(f" min_order_usd = ${MIN_ORDER:.0f} (config/live.json, letto). E' la soglia NOSTRA:") + print(" il lotto del VENUE e' 1-2 ordini di grandezza sotto (sezione 1).") + + sub("7a) TICKET MEDIO PER GAMBA al peso 15% del libro") + rows = [] + j = _joint_off(0) + idx_sa = pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True)) + sa_pos = S["_statarb_pos"] + sa_dtk = np.abs(np.diff(sa_pos, prepend=0.0)) + btc, eth = ol.aligned() + dvs_tk = np.abs(np.diff(S["_dvs_pos"], prepend=0.0)) + xs19_tk = S["_xs19_tick"]; xsd_tk = S["_xsd_tick"] + pv_tk = S["_prevday_turn"] + spec = [ + ("STATARB", sa_dtk[sa_dtk > 1e-9], 2, "BTC+ETH perp (NETTA col libro)"), + ("DVOLSPREAD", dvs_tk[dvs_tk > 1e-9], 2, "BTC+ETH perp (NETTA col libro)"), + ("PREVDAY", pv_tk[pv_tk > 1e-9], 2, "BTC+ETH perp (NETTA col libro)"), + ("XS01", xs19_tk[xs19_tk > 1e-9], 2 * SL.XS_CFG["k"], "19 gambe HL (venue diverso)"), + (f"XS01-D", xsd_tk[xsd_tk > 1e-9], 2 * min(SL.XS_CFG["k"], len(cens["xs_stable"]) // 2), + f"{len(cens['xs_stable'])} gambe Deribit (>=1a)"), + ] + print(f" {'candidato':<12}{'gambe':>7}{'Ξ”peso/gamba med':>18}" + + "".join(f"{('$'+str(int(c))):>12}" for c in CAP_LADDER) + " nota") + for nm, tk, legs, nota in spec: + med = float(np.median(tk)) if len(tk) else 0.0 + cells = "" + for C in CAP_LADDER: + tick = med * C * 0.15 # Ξ”peso di UNA gamba x nozionale allocato + cells += f"{('$%.2f' % tick):>12}" + print(f" {nm:<12}{legs:>7}{med:>18.4f}{cells} {nota}") + print(f" (ticket = Ξ”peso mediano di UNA gamba x (capitale x 15%). Sotto ${MIN_ORDER:.0f} " + "l'ordine NON parte.)") + + sub("7b) HAIRCUT DA MIN-ORDER β€” Sharpe modellato vs Sharpe eseguibile") + print(f" {'candidato':<12}" + "".join(f"{('$'+str(int(c))):>26}" for c in CAP_LADDER)) + print(f" {'':<12}" + "".join(f"{'Sh (haircut, %salt)':>26}" for _ in CAP_LADDER)) + sa_ret = OS.spread_ret(j) + # il book DVOLSPREAD e' (wb su BTC, -wb su ETH) -> il ritorno per unita' di wb e' rb - re. + dvs_ret = (al.simple_returns(btc["close"].values.astype(float)) + - al.simple_returns(eth["close"].values.astype(float))) + for nm, pos, ret, idx, legs in ( + ("STATARB", sa_pos, sa_ret, idx_sa, 2), + ("DVOLSPREAD", S["_dvs_pos"], dvs_ret, pd.DatetimeIndex(btc["dt"]), 2)): + line = f" {nm:<12}" + base_sh = _gated_sharpe(pos, ret, idx, 1e12, legs)[0] + for C in CAP_LADDER: + sh, ntr, skip = _gated_sharpe(pos, ret, idx, C * 0.15, legs) + line += f"{('%.3f (%+.3f, %.0f%%)' % (sh, sh-base_sh, 100*skip)):>26}" + print(line) + print(" (Sh a capitale infinito = modellato. Il segno del haircut puo' essere POSITIVO: il") + print(" pavimento salta ribilanci piccoli e risparmia fee β€” e' de-levering, non merito.)") + + sub("7c) VRP01 β€” l'aritmetica dei LOTTI (non e' min-order, e' granularita' intera)") + e_mk = (V["perp"].get("ETH") or {}).get("mark") or 0.0 + b_mk = (V["perp"].get("BTC") or {}).get("mark") or 0.0 + lot_e = min(V["opt"].get(("USDC", "ETH"), {0.1})) * e_mk + lot_b = min(V["opt"].get(("USDC", "BTC"), {0.01})) * b_mk + print(f" Il collaterale dello sleeve e' il PUT VENDUTO (cash-secured sullo strike): un lotto") + print(f" vale ~1 unita' di sottostante x min_trade_amount.") + print(f" ETH_USDC opzioni : lotto minimo = ${lot_e:,.0f} di nozionale") + print(f" BTC_USDC opzioni : lotto minimo = ${lot_b:,.0f} di nozionale") + print(f"\n {'capitale':>10}{'peso 12%':>11}{'lotti ETH':>11}{'lotti BTC':>11}" + f"{'% del conto per 1 lotto ETH':>30}") + for C in CAP_LADDER: + a = C * 0.12 + print(f" {('$%.0f' % C):>10}{('$%.0f' % a):>11}{int(a // lot_e) if lot_e else 0:>11}" + f"{int(a // lot_b) if lot_b else 0:>11}{(100*lot_e/C):>29.1f}%") + print(f"\n -> 1 lotto ETH = peso di book del 12% a capitale ~${lot_e/0.12:,.0f}; il BTC entra") + print(f" a ~${lot_b/0.12:,.0f}. Il 30/07 questi numeri erano ${1832/0.12:,.0f} e " + f"${6210/0.12:,.0f} perche' misurati sulla famiglia INVERSE.") + print(" ⚠️ NON promuove VRP01: resta fuori per la REGOLA 'niente short-vol da modello in") + print(" deploy' e per f=0,73 misurato sulle quote vere. Cade il muro del LOTTO, non la regola.") + + +def sezione7d(rate: dict) -> None: + sub("7d) IL CONTO GIUSTO PER UN CANDIDATO CHE NETTA β€” min-order sul TARGET NETTO") + print(" 7a/7b trattano il candidato come un libro a se': per uno sleeve che NETTA e' il") + print(" conto SBAGLIATO. Il cron manda UN ordine per asset sul target NETTO, e il") + print(" pavimento di $5 morde li'. Qui il target netto e' ricostruito come in produzione:") + print(" net_a = 0.5 Β· E Β· [0.75(1-w)Β·tp_frac_a + 0.25(1-w)Β·skh_sign_a + wΒ·cand_pos_a]") + tp = TrendPortfolio(**CANONICAL) + base_pos, cand_pos = {}, {"STATARB": {}, "DVOLSPREAD": {}} + for a in ("BTC", "ETH"): + df = A_TP.daily_off(a, 0) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + frac = np.nan_to_num(np.asarray(tp.target_series(df), float)) + _, tr = FU.skh_daily(a, 0, "canonical", rate) + sign = np.zeros(len(idx)) + tms = FU.to_ms(idx) + for t in tr: # segno SKH01 sui giorni in cui e' aperto + m = (tms >= t["t_in"]) & (tms < t["t_out"]) + sign[m] = t["dir"] + base_pos[a] = pd.DataFrame({"frac": frac, "sign": sign}, index=idx) + j = _joint_off(0) + ix = pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True)) + sp = np.nan_to_num(np.asarray(OS.f_statarb_resid(W=45, sgn=+1)(j), float)) + cand_pos["STATARB"]["ETH"] = pd.Series(sp, index=ix) + cand_pos["STATARB"]["BTC"] = pd.Series(-sp, index=ix) + btc, eth = ol.aligned() + wb, _we = dvs_make_book(**DVS_FROZEN)(btc, eth) + wb = np.clip(np.nan_to_num(np.asarray(wb, float)), -1, 1) + ixd = pd.DatetimeIndex(btc["dt"]) + cand_pos["DVOLSPREAD"]["BTC"] = pd.Series(wb, index=ixd) + cand_pos["DVOLSPREAD"]["ETH"] = pd.Series(-wb, index=ixd) + + def net_orders(cand: str | None, w: float, E: float) -> tuple[int, float, float]: + n_ord, tot = 0, 0.0 + yrs = max(len(base_pos[a]) for a in ("BTC", "ETH")) / 365.25 + for a in ("BTC", "ETH"): + b = base_pos[a] + raw = 0.5 * E * ((W_TP * (1 - w)) * np.maximum(b["frac"].values, 0.0) + + (W_SKH * (1 - w)) * b["sign"].values) + ser = pd.Series(raw, index=b.index) + if cand: + cp = cand_pos[cand][a].reindex(ser.index).fillna(0.0) + ser = ser + 0.5 * E * w * cp.values + cur = 0.0 + for v in ser.values: + if abs(v - cur) >= MIN_ORDER: + n_ord += 1; tot += abs(v - cur); cur = v + return n_ord, tot / max(n_ord, 1), yrs + + print(f"\n {'configurazione':<26}" + "".join(f"{('$'+str(int(c))):>30}" for c in CAP_LADDER)) + print(f" {'':<26}" + "".join(f"{'ord/a ticket turnover/a':>30}" for _ in CAP_LADDER)) + for cand, w in ((None, 0.0), ("STATARB", 0.15), ("DVOLSPREAD", 0.15)): + lab = "2 sleeve (com'e' oggi)" if cand is None else f"+ {cand} @15%" + line = f" {lab:<26}" + for C in CAP_LADDER: + n, tk, yrs = net_orders(cand, w, C) + line += f"{('%.0f $%.0f $%.0f' % (n / yrs, tk, n * tk / yrs)):>30}" + print(line) + print("\n πŸ“Œ Un terzo sleeve che netta NON paga un min-order proprio: cambia il NUMERO di") + print(" ordini del libro, non il loro pavimento. Il conteggio ordini RADDOPPIA, il") + print(" TURNOVER (cioe' la fee) sale molto meno β€” e' il segno che il pavimento sta") + print(" tagliando ribilanci piccoli, non che il libro trada di piu' in nozionale.") + print(" βœ… E c'e' un vantaggio strutturale mai scritto: le fee modellate negli sleeve sono") + print(" un LIMITE SUPERIORE. Ogni sleeve paga sul PROPRIO |Ξ”pos|, ma il conto paga sul") + print(" |Ξ”(somma)|, e per disuguaglianza triangolare |Ξ”(Ξ£ wΒ·p)| <= Ξ£ wΒ·|Ξ”p|.") + print(" ⚠️ Il rovescio, misurato in sezione 2: il candidato non puo' avere un proprio stop,") + print(" e i suoi ribilanci si CANCELLANO contro quelli di TP01/SKH01 quando discordano.") + + +# ============================================================================================= +# 8) LA SCALA +# ============================================================================================= +def sezione8(S: dict, M: dict, G: dict, V: dict, cens: dict) -> None: + sez("8) LA SCALA β€” a quale capitale entra il primo, chi e', e per quale muro") + global PASSED_TXT + byc: dict = {} + for (c, w), r in G.items(): + byc.setdefault(c, []).append((w, r["gate_pass"])) + PASSED_TXT = " Β· ".join(f"{c}: {sum(1 for _, g in v if g)}/{len(v)}" + for c, v in byc.items()) + lot_e = min(V["opt"].get(("USDC", "ETH"), {0.1})) * ((V["perp"].get("ETH") or {}).get("mark") or 0) + lot_b = min(V["opt"].get(("USDC", "BTC"), {0.01})) * ((V["perp"].get("BTC") or {}).get("mark") or 0) + W = 0.15 + print(f" `weights_tilt_null` superato (pesi su 4): {PASSED_TXT}\n") + + sub("8a) IL MURO DI CIASCUNO β€” quale vincolo morde per primo, e se e' una soglia di CAPITALE") + muri = [ + ("XS01 (19 gambe)", "VENUE", + "5 gambe su 19 non esistono su Deribit; su Hyperliquid serve un SECONDO CONTO,", + "e la decisione d'operatore del 26/07 tiene tutto su Deribit fino a $20.000.", + "NON e' una soglia di eseguibilita' (falsificata il 22/08): e' la decisione di venue."), + (f"XS01-D ({len(cens['xs_stable'])} gambe)", "AMPIEZZA + SFORTUNA DI LISTINO", + f"con {len(cens['xs_stable'])} gambe il long-5/short-5 tiene 10 posizioni su " + f"{len(cens['xs_stable'])}: gia' il sottoinsieme MEDIANO", + "da 11 crolla (sez. 4c), e quello che Deribit quota sta al 9Β° pctl di quella banda.", + f"dShFULL {np.median(M['out']['XS01-D'][W]['sh']):+.3f} a peso 15% " + f"(negativo a {100-100*(np.array(M['out']['XS01-D'][W]['sh'])>0).mean():.0f}% delle ancore).", + "Nessun capitale lo ripara, e nemmeno 2-3 listing in piu': serve l'universo pieno."), + ("XS01-D14", "DATO", + "3 delle 14 gambe sono quotate da 2-8 GIORNI e una (APT) non ha nemmeno una quota;", + "e `marginal_vs_tp01` la classifica HEDGE, non ADDS.", + "Riapribile fra ~1 anno di quotazione, se il volume arriva. Non e' una soglia di $."), + ("STATARB-RESID", "GATE PRE-REGISTRATO 2026-09-27", + f"eseguibile e netta gia' a $635; dShFULL {np.median(M['out']['STATARB'][W]['sh']):+.3f} " + f"a 15%, ma d drift {100*np.median(M['out']['STATARB'][W]['cagr']):+.2f}pp", + "(tutto rischio in meno, zero ritorno in piu' a nozionale invariato) e", + "`marginal_vs_tp01` = NEUTRAL, robust_oos FALSE. Il gate scade fra 35 giorni."), + ("DVOLSPREAD", "GATE PRE-REGISTRATO 2026-10-24 / 2027-01-24", + f"eseguibile e netta a $635; dShFULL {np.median(M['out']['DVOLSPREAD'][W]['sh']):+.3f}, ma", + f"d drift {100*np.median(M['out']['DVOLSPREAD'][W]['cagr']):+.2f}pp e d maxDD che PEGGIORA " + "sopra il 15%.", + "robust_oos FALSE. Il gate ha una data e non si anticipa."), + ("PREVDAY", "NESSUN GATE β€” ed e' il problema", + f"e' il piu' forte misurato (dShFULL {np.median(M['out']['PREVDAY'][W]['sh']):+.3f}, " + f"dShHOLD {np.median(M['out']['PREVDAY'][W]['shh']):+.3f}, d drift " + f"{100*np.median(M['out']['PREVDAY'][W]['cagr']):+.2f}pp),", + "eseguibile, netta, ADDS, robust_oos TRUE β€” e sta in forward-monitor dal giugno", + "senza gate pre-registrato, senza deflated-Sharpe, e con corr al libro 0.150 (la piu' alta)."), + ("VRP01", "REGOLA + PREZZO", + f"il lotto NON e' piu' il muro (ETH ${lot_e:,.0f} = peso 12% da ~${lot_e/0.12:,.0f}),", + "ma restano 'niente short-vol da modello in deploy' e f=0,73 misurato sulle quote vere;", + "e non netta: serve una riga in `_CONTRACT` = codice su un percorso con soldi veri."), + ("GTAA01", "PRIIPs", + "l'ordine e' RIFIUTATO dal broker retail UE. La via UCITS esiste (27/07) ma e' un", + "SECONDO VENUE, quindi cade anch'essa sotto la decisione dei $20.000.", + "Non e' una soglia di capitale: e' un divieto."), + ] + for nm, muro, *righe in muri: + print(f"\n {nm} -> MURO: {muro}") + for r in righe: + print(f" {r}") + + sub("8b) LA SCALA IN DOLLARI β€” cosa cambia, e quando") + righe = [ + (635, "OGGI", "STATARB Β· DVOLSPREAD Β· PREVDAY eseguibili e nettanti (ticket $2,8-4,9,", + "assorbiti nel netto: 7d). XS01-D eseguibile ma PEGGIORA. Nessuno ammissibile."), + (2024, f"1 lotto ETH di VRP01 = peso 12% (${lot_e:,.0f}/0,12)", + "cade il muro del lotto per la gamba ETH. NON cade la regola short-vol.", ""), + (3000, "GTAA_MIN_CAPITAL", + "GTAA01 avrebbe il capitale ma resta PRIIPs-bloccato (verificato sul conto reale).", ""), + (6440, f"1 lotto BTC di VRP01 = peso 12% (${lot_b:,.0f}/0,12)", + "VRP01 tornerebbe 50/50 BTC+ETH come lo sleeve modellato.", ""), + (20000, "DECISIONE DI VENUE (operatore, 26/07)", + "si riapre lo split. E' QUI che XS01 (Hyperliquid, 19 gambe) diventa possibile β€”", + "per la decisione di venue, NON per una soglia di eseguibilita'."), + ] + for C, evento, *righe2 in righe: + print(f"\n ${C:,}".replace(",", ".") + f" β€” {evento}") + for r in righe2: + if r: + print(f" {r}") + print("\n πŸ“Œ LA RISPOSTA ALLA DOMANDA 'a quale capitale cambia': NON c'e' una soglia di") + print(" capitale che ammette un terzo sleeve. Le due date che contano sono un GATE") + print(" (27/09, fra 35 giorni, gratis) e una DECISIONE DI VENUE ($20k). Il capitale") + print(" entra solo per VRP01, che e' fermo per una regola che il capitale non tocca.") + + +def sezione9(S: dict, M: dict, G: dict, cens: dict) -> None: + sez("9) VERDETTO β€” l'attesa a priori contro l'esito") + W = 0.15 + print(f" Tabella decidibile, peso {W:.0%}, mediana delle differenze APPAIATE sulla banda") + print(" d'ancora congiunta, funding dentro:\n") + print(f" {'candidato':<12}{'dShFULL':>10}{'>0':>6}{'dShHOLD':>10}{'d drift':>10} " + f"{'eseguibile a $635':<24}{'gate':<12}{'tilt-null'}") + for c in CANDIDATI: + r = M["out"][c][W] + ex = {"XS01": "NO (5 gambe assenti)", "XS01-D": "SI (11 gambe)", + "XS01-D14": "solo 3gg di listino", "STATARB": "SI (netta)", + "DVOLSPREAD": "SI (netta)", "PREVDAY": "SI (netta)", + "VRP01": f"NO (1 lotto=38% conto)"}[c] + gt = {"XS01": "β€”", "XS01-D": "β€”", "XS01-D14": "β€”", "STATARB": "27/09", + "DVOLSPREAD": "24/10", "PREVDAY": "β€”", "VRP01": "regola"}[c] + gp = "PASS" if G.get((c, W), {}).get("gate_pass") else "FAIL" + print(f" {c:<12}{np.median(r['sh']):>+10.4f}{100*(np.array(r['sh'])>0).mean():>5.0f}%" + f"{np.median(r['shh']):>+10.4f}{100*np.median(r['cagr']):>+8.2f}pp " + f"{ex:<24}{gt:<12}{gp}") + dsa = float(np.median(M["out"]["STATARB"][W]["sh"])) + ddv = float(np.median(M["out"]["DVOLSPREAD"][W]["sh"])) + dxs = float(np.median(M["out"]["XS01"][W]["sh"])) + dxd = float(np.median(M["out"]["XS01-D"][W]["sh"])) + xs_gate = G.get(("XS01", W), {}).get("gate_pass") + print("\n ATTESA A PRIORI vs ESITO (dichiarata in sezione 0, prima di misurare):") + print(" (A1) 'nessuno deployabile oggi, e il muro NON e' il lotto del venue'") + print(" -> βœ… CONFERMATA. Nessun candidato muore sul lotto del venue (i lotti stanno") + print(" fra $0,01 e $7,72). Il muro e' altrove, ed e' diverso per ciascuno.") + print(f" (A2) 'XS01 il piu' forte ma tilt-null FALLIRA'' -> ⚠️ META' GIUSTA: XS01 e' fra i") + print(f" piu' forti (dShFULL {dxs:+.3f}) ma `weights_tilt_null` " + f"{'PASSA' if xs_gate else 'FALLISCE'}. Il gate non e'") + print(" il muro di XS01: il muro e' che 5 delle 19 gambe Deribit non le quota.") + print(f" (A3) '|dSharpe| < 0,10 per STATARB e DVOLSPREAD' -> ❌ REFUTATA su STATARB") + print(f" ({dsa:+.3f} a peso 15%, positivo a 100% delle ancore), confermata su") + print(f" DVOLSPREAD ({ddv:+.3f}). E il segno NON e' incerto: e' stabile sulla banda.") + print(" (A4) 'il primo a entrare sara' XS01 fra $2k e $5k, per ragione di venue'") + print(f" -> ❌ REFUTATA nella sostanza: la versione di XS01 eseguibile su Deribit oggi") + print(f" ({len(cens['xs_stable'])} gambe) fa dShFULL {dxd:+.3f} β€” PEGGIORA il libro. Non e' una") + print(" soglia di capitale: e' che l'universo eseguibile non regge il meccanismo") + print(" (sez. 4c: la maggior parte e' AMPIEZZA, il resto e' QUALI gambe mancano).") + print(" (A5) 'mi sorprendera' un candidato dato per eseguibile che non lo e''") + print(" -> βœ… CONFERMATA, ma al ROVESCIO di come me l'aspettavo: la sorpresa e' un") + print(" candidato dato per NON eseguibile che lo E' (VRP01: il lotto vero e' 7,5x") + print(" piu' piccolo del pubblicato), e uno dato per eseguibile-a-$20k che a") + print(" $20k resta comunque rotto (XS01: e\' il VENUE, non la taglia).") + print("\n ⚠️ CAVEAT CHE VALE PER TUTTA LA TABELLA β€” questo e' uno SCREEN, non una scoperta:") + print(" 7 candidati x 4 pesi = 28 configurazioni guardate sullo stesso libro e sullo") + print(" stesso hold-out. Nessuno di questi delta e' passato per un deflated-Sharpe DI") + print(" SCREEN (lezione 22/08 Β§10: non basta contare i trial, serve la VARIANZA di") + print(" screen), e il candidato che esce primo esce da una selezione che io ho fatto.") + print(" E' esattamente per questo che l'output e' una TABELLA e non una raccomandazione:") + print(" il modo giusto di usarla e' scegliere UN candidato per una ragione dichiarata") + print(" PRIMA, e poi fargli un gate pre-registrato β€” non prendere il massimo di 28.") + + +# ============================================================================================= +def main() -> None: + global _ARGS + ap = argparse.ArgumentParser() + ap.add_argument("--no-net", action="store_true") + ap.add_argument("--draws", type=int, default=1000) + ap.add_argument("--force", action="store_true") + _ARGS = ap.parse_args() + sys.argv = [sys.argv[0]] + (["--no-net"] if _ARGS.no_net else []) + + sez("r0823_book_3rd.py β€” Β§50 BOOK-3RD | quale terzo sleeve per il LIBRO LIVE?") + print(f" avvio {dt.datetime.now(dt.timezone.utc):%Y-%m-%d %H:%M UTC} Β· " + f"capitale reale assunto ${CAPITAL_TODAY:.0f} Β· estrazioni {_ARGS.draws}") + sez("0) ATTESA A PRIORI (dichiarata PRIMA di misurare)") + print(ATTESA) + + V = venue_census() + cens = sezione1(V) + sezione1c(V, cens) + sezione2() + print(f"\n {clock()} carico i tassi di funding (cache/rete)...") + rate = get_rate() + R3 = sezione3(rate) + print(f"\n {clock()} costruisco le serie ancorate...") + S = build_series(rate, cens, force=_ARGS.force) + sezione4(S, cens) + sezione4b(S) + sezione4c(cens) + M = sezione5(S, _ARGS.draws) + sezione5b(S) + G = sezione6(S) + sezione7(S, V, cens) + sezione7d(rate) + sezione8(S, M, G, V, cens) + sezione9(S, M, G, cens) + sez(f"FINE {clock()}") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_costo_capitale.py b/scripts/research/r0823_costo_capitale.py new file mode 100644 index 0000000..583e25b --- /dev/null +++ b/scripts/research/r0823_costo_capitale.py @@ -0,0 +1,1113 @@ +#!/usr/bin/env python3 +"""r0823_costo_capitale.py β€” Β§55 COSTO-CAPITALE: il muro quando il costo d'esecuzione +NON e' una costante ma una FUNZIONE del capitale. + +IL BUCO +------- +Ogni muro di capitale pubblicato dal progetto ($272k, $278k, $313k, $325k, la traiettoria da +$635, le tabelle dei versamenti) e' calcolato con un costo d'esecuzione COSTANTE: le fee +proporzionali di `backtest_signals` (0,10% RT) piu' il pavimento `min_order` di +`CC.tp01_realistic`. Ma un muro che si raggiunge ACCUMULANDO attraversa tutte le taglie, e il +costo vero cresce con la taglia dell'ordine. Il costo non e' un parametro: e' **una funzione +lungo il cammino**. + +La domanda e' nata due volte nella stessa notte, da due filoni che non si parlavano: + * Β§52 SKEPTIC-SPOT β€” camminando entrambi i libri, il differenziale spot-perp passa da + +0,72 bps a $600 a **+25,66 bps a $272k**: il lead dello spot si annulla esattamente alla + taglia del muro che pretende di spostare. Follow-up dichiarato: Β«il muro rifatto con costo + d'esecuzione FUNZIONE DEL CAPITALEΒ». + * Β§53 SKEPTIC-LEVA β€” «§5 ha misurato un fill al 21,9% del volume della sua barra 5m a $635; + con partecipazione lineare 1,25x lo porta al 27,4% SUBITO. Va misurata PRIMA del gradino.Β» + * Β§5 SLIP-AUDIT porta gia' una **data di scadenza esplicita**: Β«quel 22% diventa 172% di una + barra 5m a $5.000 e 687% a $20.000. Va rifatta a ogni salto di taglia.Β» + +COSA FA QUESTO SCRIPT +--------------------- + 1. [VENUE] Misura `costo_bps(taglia)` camminando il libro reale di BTC_USDC-PERPETUAL e + ETH_USDC-PERPETUAL su piu' istantanee, e ne riporta la BANDA (Β§45: due istantanee a 4 + minuti differivano del 79% β€” un'istantanea non e' una distribuzione). + 2. [BACKTEST] Ricava la distribuzione VERA delle taglie d'ordine del libro live (TP01 75 / + SKH01 25) a ogni capitale. Non l'ordine medio: il costo e' convesso, la media dei costi + non e' il costo della media. + 3. [CALC] Rifa' il libro con il costo DENTRO, itera il muro come PUNTO FISSO + (`r0726_wall_fixedpoint`, importato) e riporta se converge. + 4. [CALC] Trova il PUNTO DI SATURAZIONE con DUE definizioni indipendenti β€” un limite DURO + puramente misurato (la taglia a cui l'1% del nozionale annuo non e' piu' eseguibile in un + istante) e uno ECONOMICO (l'argmax di `C x drift(C)`, dove il capitale marginale smette di + comprare reddito). Numero mai calcolato dal progetto. + 5. [CALC] Rifa' la traiettoria Β«EUR500/mese da $635Β» col costo LUNGO IL CAMMINO + (`r0822d_piano_vero`, importato). + 6. [CALC] Il gradino di leva col costo endogeno: k massimo. + +REPLICHE OBBLIGATORIE PRIMA DI OGNI DELTA (regola del progetto: prima di pubblicare un numero +nuovo, far riprodurre alla macchina quello vecchio) + R1 la mia TP01 con costo spento == `FD.tp01_realistic` bit-exact + R2 la mia SKH01 con costo spento == `FD.skh_book` bit-exact + R3 il mio libro con costo spento == `FD.book_75_25` bit-exact + R4 muro L0 LORDO sul vintage 2026-08-07 $272.061 al dollaro + R5 muro L1 +FISCO sul vintage 2026-08-07 $258.338 al dollaro + R6 muro L3 CONGIUNTA sulla serie di oggi $313k (Β§36) + R7 `accumula_costo` con drag spento == `PN.accumula` bit-exact + R8 la mia `walk_cost_bps` == quella di `r0823_skeptic_spot` identita' su libro sintetico + R9 k* a costo ZERO == `FD.k_star` sul libro pubblicato 12,25x (Β§35) + piu' DUE controlli POSITIVI obbligatori: un costo di 200 bps per ordine deve uccidere il + libro, e un drag di 3%/anno sopra $100k deve muovere la traiettoria. Un modello di costo che + non morde mai e' indistinguibile da uno rotto. + +ETICHETTE DI PROVENIENZA + [VENUE] misurato dall'API pubblica Deribit in questa sessione + [LOG] dai log/fill reali del libro live + [BACKTEST] dalla serie storica certificata + [CALC] aritmetica sui due precedenti + [DEDOTTO] estrapolazione oltre la regione misurata + [ASSUNTO] scelta di modello dichiarata + +ASSUNZIONE PRINCIPALE, DICHIARATA IN TESTA + Il libro d'ordini e' una FOTOGRAFIA DEL PRESENTE. Proiettarla su 7 anni di storia (2019-2026, + quando la profondita' del venue era una frazione di quella di oggi) e' un'ASSUNZIONE, e va nel + verso OTTIMISTA: il numero che ne esce e' un limite superiore della capacita' STORICA, non una + misura di essa. Sul futuro β€” che e' cio' che il muro deve attraversare β€” l'assunzione e' la + costanza della profondita', non la sua crescita. + Nell'altro verso, e su un asse diverso: il campione e' raccolto di DOMENICA NOTTE UTC, cioe' + nella finestra piu' sottile della settimana. Dentro il presente e' quindi CONSERVATIVO β€” un + campione in orario di punta darebbe costi piu' bassi. + +SOLA LETTURA / RETE / SOLDI VERI + Nessuna scrittura nel repo (cache FUORI, nello scratchpad). Nessun ordine, mai: solo GET + PUBBLICHE senza chiave, pacing <=2 req/s, astensione nei minuti :05-:10 e :24-:30 (finestre + del cron di produzione). + + uv run python scripts/research/r0823_costo_capitale.py + uv run python scripts/research/r0823_costo_capitale.py --no-net # solo cache + uv run python scripts/research/r0823_costo_capitale.py --rounds 8 --gap 35 + uv run python scripts/research/r0823_costo_capitale.py --fetch-only --rounds 22 --gap 55 +""" +from __future__ import annotations + +import contextlib +import datetime as dt +import io +import json +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +# --------------------------------------------------------------------------- +# Il MIO flag di rete si legge PRIMA di importare qualunque libreria del progetto: +# `r0822d_funding` decide all'import se andare in rete, e qui deve usare la sua cache. +# --------------------------------------------------------------------------- +_NET = "--no-net" not in sys.argv + + +def _argf(name: str, default: float) -> float: + if name in sys.argv: + return float(sys.argv[sys.argv.index(name) + 1]) + return default + + +ROUNDS = int(_argf("--rounds", 8)) +GAP_S = _argf("--gap", 35.0) +FETCH_ONLY = "--fetch-only" in sys.argv + +if "--no-net" not in sys.argv: + sys.argv.append("--no-net") + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt", ROOT): + sys.path.insert(0, str(_p)) + +import r0702_anchor_skh01 as r02 # noqa: E402 run_asset (ltf/ent), sim_equity +import r0725_capcurve as CC # noqa: E402 DERIBIT_FEE_SIDE/MIN_ORDER, _boot_paths +import r0726_wall_fixedpoint as WF # noqa: E402 la STRUTTURA del punto fisso +import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI +import r0807_piano_netto as PN # noqa: E402 muro, accumula, passo_fiscale +import r0822d_funding as FD # noqa: E402 funding, tp01_realistic, skh_daily +import r0822d_piano_vero as PV # noqa: E402 Lente, deluck, muro_di, leggi +from src.data.downloader import load_data # noqa: E402 +from src.portfolio.portfolio import combine_outer, metrics # noqa: E402 +from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, # noqa: E402 + resample_1d, simple_returns) + +# --------------------------------------------------------------------------- +# Costanti dichiarate +# --------------------------------------------------------------------------- +ASSETS = ("BTC", "ETH") +INSTR = {"BTC": "BTC_USDC-PERPETUAL", "ETH": "ETH_USDC-PERPETUAL"} # cio' che il libro TRADA +W_TP, W_SKH = 0.75, 0.25 +LO = pd.Timestamp("2019-03-14", tz="UTC") # inizio del libro (convenzione FD.book_75_25) +DPY = 365.0 +API = "https://www.deribit.com/api/v2/public/" +DEPTH = 1000 # livelli richiesti per lato +VINTAGE_PUB = "2026-08-07" + +# griglia di taglie: log-spaziata da $5 (il min_order) a $10M +SIZES = np.unique(np.round(10.0 ** np.arange(np.log10(5.0), 7.0001, 0.125), 2)) +# griglia di capitali su cui si misura il libro (ordini di grandezza, non un continuo) +CAPS = (635.0, 2_000.0, 5_000.0, 20_000.0, 50_000.0, 100_000.0, 313_000.0, + 1_000_000.0, 3_000_000.0, 10_000_000.0, 30_000_000.0, 100_000_000.0) + +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad") +CACHE = (_SP if _SP.parent.exists() else Path("/tmp/pythagoras_costo")) / "costocap" +CACHE.mkdir(parents=True, exist_ok=True) +BOOKS = CACHE / "books.jsonl" +FILLS = _SP / "r0822_slip_fills.csv" # i 18 fill reali di Β§5 +LIN5M = {"BTC": _SP / "lin_btc_5m.parquet", "ETH": _SP / "lin_eth_5m.parquet"} + +T0 = time.time() +_last = [0.0] + + +def sez(t: str) -> None: + print("\n" + "=" * 98 + f"\n {t}\n" + "=" * 98) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +# =========================================================================== +# 0) RETE β€” GET pubbliche, pacing, astensione nelle finestre del cron +# =========================================================================== +def _guard_window() -> None: + while True: + m = dt.datetime.now(dt.timezone.utc).minute + if not (5 <= m <= 10 or 24 <= m <= 30): + return + tgt = 11 if m <= 10 else 31 + w = (tgt - m) * 60 - dt.datetime.now(dt.timezone.utc).second + print(f" [rete] minuto :{m:02d} = finestra del cron di produzione -> attendo {max(w,1)}s") + time.sleep(max(w, 1)) + + +def _get(path: str, tries: int = 3): + if not _NET: + return None + _guard_window() + for k in range(tries): + d = time.time() - _last[0] + if d < 0.55: # <=2 req/s + time.sleep(0.55 - d) + _last[0] = time.time() + try: + with urllib.request.urlopen(API + path, timeout=30) as r: + return json.load(r) + except Exception as ex: # noqa: BLE001 + if k == tries - 1: + print(f" [rete] KO {path[:64]}: {ex}") + return None + time.sleep(1.5 * (k + 1)) + return None + + +def raccogli(rounds: int, gap: float) -> int: + """Istantanee del libro, appese alla cache. Ritorna quante ne ha aggiunte.""" + if not _NET or rounds <= 0: + return 0 + n = 0 + with BOOKS.open("a") as fh: + for r in range(rounds): + for a in ASSETS: + j = _get(f"get_order_book?instrument_name={INSTR[a]}&depth={DEPTH}") + if not j or "result" not in j: + continue + q = j["result"] + if not q.get("bids") or not q.get("asks"): + continue + fh.write(json.dumps(dict( + ts=int(q.get("timestamp", time.time() * 1000)), asset=a, + bid=float(q["best_bid_price"]), ask=float(q["best_ask_price"]), + bids=[[float(p), float(s)] for p, s in q["bids"]], + asks=[[float(p), float(s)] for p, s in q["asks"]])) + "\n") + n += 1 + if r < rounds - 1: + time.sleep(gap) + return n + + +def leggi_libri() -> list: + if not BOOKS.exists(): + return [] + out = [] + for ln in BOOKS.read_text().splitlines(): + if ln.strip(): + with contextlib.suppress(Exception): + out.append(json.loads(ln)) + return out + + +# =========================================================================== +# 1) LA CURVA COSTO-TAGLIA +# =========================================================================== +def walk_cost_bps(book: dict, usd: float, side: str) -> float | None: + """Costo effettivo vs mid di un ordine da `usd` eseguito ISTANTANEAMENTE contro il libro + visibile (stessa convenzione di `r0823_skeptic_spot.walk_cost_bps` e + `r0822e_funding_avoid.walk_cost_bps`). None se il libro visibile NON basta: allora si + dichiara, non si estrapola.""" + if not book or not book.get("bid") or not book.get("ask"): + return None + mid = (book["bid"] + book["ask"]) / 2.0 + lv = book.get("asks" if side == "buy" else "bids") or [] + need, got, cost = usd / mid, 0.0, 0.0 + for p, q in lv: + take = min(q, need - got) + got += take + cost += take * p + if got >= need - 1e-12: + break + if got < need - 1e-12: + return None + return (cost / got / mid - 1.0) * 1e4 * (1.0 if side == "buy" else -1.0) + + +class Curva: + """`costo_bps(asset, usd, q)` per interpolazione log-log sulle quantili misurate. + + * sotto la taglia minima misurata: il costo e' il mezzo spread (costante) β€” e' cio' che il + libro DICE, non un'assunzione; + * dentro la regione misurata: interpolazione lineare in log(taglia), con monotonia imposta + (`cummax`): camminare un libro non puo' costare MENO su un ordine piu' grande, e una + violazione sarebbe rumore fra istantanee; + * oltre l'ultima taglia con copertura sufficiente (`lim`): il libro visibile NON BASTA, cioe' + l'ordine non e' eseguibile in un istante. Li' si estrapola con pendenza log-log + `max(misurata, 1.0)` β€” [ASSUNTO] dichiarato: impatto ALMENO lineare nella taglia, perche' + una pendenza < 1 (raddoppiare l'ordine costa meno del doppio) oltre la fine del libro non + e' difendibile. Ogni cella oltre `lim` e' marcata [DEDOTTO]. + + Le tre quantili condividono maschera, `lim` e pendenza -> la banda resta ORDINATA anche + nella regione estrapolata (con tre oggetti separati p10 poteva finire sopra p50: errore + catturato alla prima corsa). + """ + + QS = (0.10, 0.50, 0.90) + + def __init__(self, libri: list, cop_min: float = 0.8): + self.cop_min = cop_min + self.x, self.y, self.lim, self.slope, self.nobs = {}, {}, {}, {}, {} + self.slope_mis = {} + for a in ASSETS: + L = [b for b in libri if b["asset"] == a] + self.nobs[a] = len(L) + M, cop = {q: [] for q in self.QS}, [] + for sz in SIZES: + v = [] + for b in L: + for side in ("buy", "sell"): + c = walk_cost_bps(b, float(sz), side) + v.append(np.nan if c is None else c) + v = np.asarray(v, float) + cop.append(float(np.isfinite(v).mean()) if len(v) else 0.0) + for q in self.QS: + M[q].append(float(np.nanquantile(v, q)) if np.isfinite(v).any() else np.nan) + cop = np.asarray(cop) + ok = np.isfinite(np.asarray(M[0.5])) & (cop >= cop_min) + if not ok.any(): + raise SystemExit(f"nessuna taglia misurabile su {a}: campione insufficiente") + xs = np.log10(SIZES[ok]) + ys = {q: np.maximum.accumulate(np.asarray(M[q])[ok]) for q in self.QS} + ys[0.50] = np.maximum(ys[0.50], ys[0.10]) # banda ordinata per costruzione + ys[0.90] = np.maximum(ys[0.90], ys[0.50]) + self.x[a], self.y[a] = xs, ys + self.lim[a] = float(SIZES[ok][-1]) + m = xs >= xs[-1] - 1.0 + sl = (float(np.polyfit(xs[m], np.log10(np.maximum(ys[0.50][m], 1e-4)), 1)[0]) + if m.sum() >= 2 else 1.0) + self.slope[a] = max(sl, 1.0) # [ASSUNTO] impatto almeno lineare + self.slope_mis[a] = sl + + def oltre(self, asset: str, usd: float) -> bool: + return usd > self.lim[asset] + + def bps(self, asset: str, usd: float, q: float = 0.50) -> float: + if usd <= 0: + return 0.0 + xs, ys = self.x[asset], self.y[asset][q] + lx = np.log10(usd) + if lx <= xs[0]: + return float(ys[0]) + if lx <= xs[-1]: + return float(np.interp(lx, xs, ys)) + return float(10.0 ** (np.log10(max(ys[-1], 1e-4)) + self.slope[asset] * (lx - xs[-1]))) + + def vec(self, asset: str, usd: np.ndarray, q: float = 0.50) -> np.ndarray: + return np.array([self.bps(asset, float(u), q) for u in usd], float) + + def profondita(self, libri: list, asset: str, soglia_bps: float) -> float: + """[VENUE] Dollari eseguibili entro `soglia_bps` di costo (mediana sulle istantanee, + lato peggiore).""" + v = [] + for b in [x for x in libri if x["asset"] == asset]: + lo, hi = 0.0, 5e7 + for _ in range(30): + mid = (lo + hi) / 2 + c = walk_cost_bps(b, mid, "buy") + d = walk_cost_bps(b, mid, "sell") + w = max([z for z in (c, d) if z is not None], default=1e9) + if w <= soglia_bps: + lo = mid + else: + hi = mid + v.append(lo) + return float(np.median(v)) if v else float("nan") + + +class CurvaZero(Curva): + """Costo identicamente nullo: il controllo negativo (replica bit-exact dei numeri vecchi).""" + + def __init__(self): # noqa: D107 + self.nobs, self.lim, self.slope = {}, {a: float("inf") for a in ASSETS}, {} + + def oltre(self, asset: str, usd: float) -> bool: # noqa: D102 + return False + + def bps(self, asset: str, usd: float, q: float = 0.50) -> float: # noqa: D102 + return 0.0 + + def vec(self, asset: str, usd: np.ndarray, q: float = 0.50) -> np.ndarray: # noqa: D102 + return np.zeros(len(usd)) + + +class CurvaFissa(CurvaZero): + """Costo costante in bps: il controllo POSITIVO. Un modello di costo che non morde mai e' + indistinguibile da uno rotto, quindi va provato che morde.""" + + def __init__(self, bps: float): # noqa: D107 + super().__init__() + self._b = float(bps) + + def bps(self, asset: str, usd: float, q: float = 0.50) -> float: # noqa: D102 + return self._b if usd > 0 else 0.0 + + def vec(self, asset: str, usd: np.ndarray, q: float = 0.50) -> np.ndarray: # noqa: D102 + return np.where(np.asarray(usd) > 0, self._b, 0.0) + + +# =========================================================================== +# 2) IL LIBRO CON IL COSTO DENTRO +# =========================================================================== +def tp01_costo(alloc: float, rate: dict | None, cur: Curva, scala: float = 1.0, + q: float = 0.50) -> tuple[pd.Series, dict]: + """`FD.tp01_realistic` con DUE gradi di liberta' in piu': il costo d'esecuzione funzione + della taglia dell'ordine, e la scala di leva (`SPEC-scale-key`: la scala moltiplica il + TARGET, non il capitale). Con cur=CurvaZero() e scala=1 deve dare bit-exact l'originale.""" + cap_asset = alloc / len(ASSETS) + tp = TrendPortfolio(**CANONICAL) + series, diag = {}, {} + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + r = simple_returns(df["close"].values.astype(float)) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) * scala + held = np.empty(len(tgt)) + cur_pos = 0.0 + for i in range(len(tgt)): + if abs(tgt[i] - cur_pos) * cap_asset >= CC.DERIBIT_MIN_ORDER: + cur_pos = tgt[i] + held[i] = cur_pos + pos = np.zeros(len(held)) + pos[1:] = held[:-1] + dpos = np.abs(np.diff(pos, prepend=0.0)) + net = pos * r - CC.DERIBIT_FEE_SIDE * dpos + ordini = dpos * cap_asset # nozionale in dollari di OGNI ordine + cbps = cur.vec(a, ordini, q) + net = net - dpos * cbps / 1e4 # il costo pesa il nozionale scambiato + if rate is not None: + net = net - pos * FD.daily_funding(rate[a], idx) + net[0] = 0.0 + diag[a] = dict(pos=pos, ordini=ordini[ordini > 0], cbps=cbps[ordini > 0], idx=idx) + series[a] = pd.Series(np.clip(net, -0.99, None), index=idx) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + out = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + if out.index.tz is None: + out.index = out.index.tz_localize("UTC") + return out, diag + + +_LTF: dict = {} + + +def _ltf(a: str): + if a not in _LTF: + _, _m0, ltf, _ent = r02.run_asset(a, 0) + _LTF[a] = ltf + return _LTF[a] + + +def _rebuild_equity(n: int, trades: list, extra: float, scala: float) -> np.ndarray: + """Ricostruisce la curva di equity di SKH01 dai TRADE gia' prodotti da `FD.sim_equity_f`, + con un costo `extra` per round-trip e una scala di leva. Non e' una re-implementazione della + strategia: i trade (boundary, direzione, gross, funding) sono presi COSI' COME SONO, cambia + solo la contabilita' del singolo trade. Con extra=0 e scala=1 e' bit-exact l'originale.""" + initial = 1000.0 + capital = initial + equity = np.full(n, capital, dtype=float) + for t in trades: + net = scala * (t["gross"] - 0.001 - t["f"]) - extra + capital += capital * net + capital = max(capital, 1.0) + equity[t["i"]:t["exit_idx"] + 1] = capital + last = initial + for k in range(n): + if equity[k] != last and equity[k] != initial: + last = equity[k] + else: + equity[k] = last + return equity + + +def skh_costo(alloc: float, rate: dict | None, cur: Curva, scala: float = 1.0, + q: float = 0.50) -> pd.Series: + """SKH01 50/50 con il costo d'esecuzione dentro. + + SKH01 entra a nozionale 1,0x del proprio capitale (`position_size=1.0, leverage=1.0`) e fa + UN round-trip per trade -> due ordini, entrambi a nozionale pieno. Il costo per trade e' + percio' `2 x costo_bps(nozionale)/1e4`, e il nozionale e' `scala x alloc/2`.""" + cap_asset = alloc / len(ASSETS) + cols = {} + for a in ASSETS: + _d, tr = FD.skh_daily(a, 0, "hourly", rate) + ltf = _ltf(a) + extra = 2.0 * scala * cur.bps(a, scala * cap_asset, q) / 1e4 + eq = _rebuild_equity(len(ltf), tr, extra, scala) + s = pd.Series(eq, index=pd.DatetimeIndex(pd.to_datetime(ltf["datetime"], utc=True))) + cols[a] = s.resample("1D").last().ffill().pct_change().dropna() + J = pd.concat(cols, axis=1, join="inner").fillna(0.0) + out = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + if out.index.tz is None: + out.index = out.index.tz_localize("UTC") + return out + + +def libro(cap: float, rate: dict | None, cur: Curva, scala: float = 1.0, + q: float = 0.50) -> pd.Series: + """Il libro LIVE (TP01 75 + SKH01 25) al capitale `cap`, col costo d'esecuzione dentro.""" + tp, _ = tp01_costo(cap * W_TP, rate, cur, scala, q) + sk = skh_costo(cap * W_SKH, rate, cur, scala, q) + return combine_outer({"TP01": tp, "SKH01": sk}, {"TP01": W_TP, "SKH01": W_SKH}, lo=LO) + + +# =========================================================================== +# 3) ACCUMULO CON COSTO LUNGO IL CAMMINO +# =========================================================================== +def accumula_costo(paths: np.ndarray, dep_eur: float, bersaglio: float, aliquota: float, + patrimoniale: float, start: float, drag, lump_eur: float = 0.0) -> dict: + """Gemella di `PN.accumula` con UN grado di liberta' in piu': un drag GIORNALIERO funzione + del capitale corrente. Con drag=None dev'essere bit-exact `PN.accumula` (replica R7). + + `drag(cap)` = (drift del libro alla taglia di riferimento) - (drift alla taglia `cap`), + diviso 365. E' un aggiustamento del solo DRIFT: la FORMA del percorso resta quella estratta. + [ASSUNTO] dichiarato β€” la vol cambia pochissimo con la taglia (sezione 4 lo riporta).""" + n_paths, n_days = paths.shape + dep = dep_eur * CC.EURUSD + cap = np.full(n_paths, start + lump_eur * CC.EURUSD) + versato = start + lump_eur * CC.EURUSD + carry = np.zeros((n_paths, TX.CARRY_ANNI)) + anno_start = cap.copy() + flusso = np.zeros(n_paths) + colpito = np.full(n_paths, -1, int) + for t in range(n_days): + if drag is None: + cap *= (1.0 + paths[:, t]) + else: + cap *= (1.0 + paths[:, t] - drag(cap)) + if t % 30 == 0 and t > 0 and dep > 0: + cap += dep + flusso += dep + versato += dep + nuovi = (colpito < 0) & (cap >= bersaglio) + colpito[nuovi] = t + if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0): + PN.passo_fiscale(cap, anno_start, flusso, carry, aliquota, patrimoniale) + anno_start = cap.copy() + flusso[:] = 0.0 + return dict(cap=cap, colpito=colpito, versato=versato) + + + + +# =========================================================================== +# 4) DIAGNOSTICHE DI CAPACITA' +# =========================================================================== +def quota_oltre(cap: float, cur: Curva, rate: dict | None) -> tuple[float, float]: + """(quota del nozionale annuo scambiato la cui TAGLIA supera il libro visibile, + nozionale annuo totale in $). E' una misura di CAPACITA' puramente [VENUE]+[BACKTEST]: + non dipende da nessuna estrapolazione.""" + Z = CurvaZero() + _s, dg = tp01_costo(cap * W_TP, None, Z) + anni = 0.0 + tot = fuori = 0.0 + for a in ASSETS: + o = dg[a]["ordini"] + idx = dg[a]["idx"] + anni = max(anni, (idx[-1] - idx[0]).days / DPY) + tot += float(o.sum()) + fuori += float(o[o > cur.lim[a]].sum()) + tr = FD.skh_daily(a, 0, "hourly", None)[1] + noz = cap * W_SKH / 2 + tot += 2 * len(tr) * noz + if noz > cur.lim[a]: + fuori += 2 * len(tr) * noz + return (fuori / tot if tot > 0 else 0.0), tot / max(anni, 1e-9) + + +# =========================================================================== +def main() -> None: # noqa: C901 + print("=" * 98) + print(" r0823 Β§55 COSTO-CAPITALE β€” il muro quando il costo d'esecuzione e' una FUNZIONE") + print("=" * 98) + print(" Provenienza: [VENUE] API pubblica Β· [LOG] fill reali Β· [BACKTEST] serie certificata") + print(" [CALC] aritmetica Β· [DEDOTTO] estrapolazione Β· [ASSUNTO] modello") + + # ---------------------------------------------------------------- rete + nuovi = raccogli(ROUNDS, GAP_S) + if FETCH_ONLY: + print(f" --fetch-only: {nuovi} istantanee aggiunte. Fine.") + return + libri = leggi_libri() + if not libri: + raise SystemExit("nessuna istantanea del libro: rilanciare con rete.") + ts = pd.to_datetime([b["ts"] for b in libri], unit="ms", utc=True) + print(f"\n [VENUE] {len(libri)} istantanee ({nuovi} nuove in questa corsa), " + f"{sum(1 for b in libri if b['asset']=='BTC')} BTC / " + f"{sum(1 for b in libri if b['asset']=='ETH')} ETH") + print(f" dal {ts.min():%Y-%m-%d %H:%M} al {ts.max():%Y-%m-%d %H:%M} UTC " + f"(arco {(ts.max()-ts.min()).total_seconds()/60:.0f} min) {clock()}") + print(" ⚠️ E' un solo regime di mercato in un arco di minuti: e' una banda INTRA-GIORNO,") + print(" non la distribuzione della profondita' su 7 anni. Vedi l'assunzione in testa.") + gg = ["lun", "mar", "mer", "gio", "ven", "sab", "dom"][int(ts.max().dayofweek)] + print(f" πŸ“Œ Ma il MOMENTO e' favorevole al risultato conservativo, non a quello comodo: " + f"{gg} {ts.max():%H:%M} UTC") + print(" = fine settimana, notte europea, mattina asiatica β€” la finestra in cui il libro") + print(" e' PIU' SOTTILE della settimana. Un campione preso in orario di punta darebbe") + print(" costi piu' bassi, cioe' rafforzerebbe la conclusione invece di indebolirla.") + + # ===================================================================== + sez("1) LA CURVA COSTO-TAGLIA, MISURATA β€” e la sua banda") + print(""" Camminata del libro visibile: costo effettivo vs mid di un ordine ESEGUITO + ISTANTANEAMENTE. E' la lente giusta per QUESTO libro perche' l'esecutore manda ordini + MARKET (`book_execute`), non limit: cio' che il libro mostra e' cio' che l'ordine paga. + Ogni cella e' la mediana su (istantanea x lato); la banda e' p10-p90 sulla stessa + popolazione. Β§45 ha misurato due istantanee a 4 minuti che differivano del 79%: un solo + numero qui sarebbe una finzione.""") + cur = Curva(libri) + print(f"\n {'ordine':>12} | {'BTC_USDC-PERP mediana [p10-p90]':>40} | " + f"{'ETH_USDC-PERP mediana [p10-p90]':>40}") + print(" " + "-" * 98) + MOSTRA = (5, 50, 500, 5_000, 50_000, 200_000, 500_000, 1_000_000, 3_000_000) + for sz in MOSTRA: + cells = [] + for a in ASSETS: + if cur.oltre(a, sz): + cells.append(f"{cur.bps(a, sz):10.2f} [DEDOTTO, oltre il libro]") + else: + cells.append(f"{cur.bps(a, sz):8.3f} [{cur.bps(a, sz, .10):7.3f}," + f"{cur.bps(a, sz, .90):8.3f}] bps") + print(f" ${sz:>11,} | {cells[0]:>40} | {cells[1]:>40}") + print(" " + "-" * 98) + print(f" ultima taglia con copertura >=80% del campione (`lim`): " + f"BTC ${cur.lim['BTC']:,.0f} Β· ETH ${cur.lim['ETH']:,.0f}") + print(f" pendenza log-log oltre `lim`: misurata BTC {cur.slope_mis['BTC']:.2f} / " + f"ETH {cur.slope_mis['ETH']:.2f} -> usata {cur.slope['BTC']:.2f} / " + f"{cur.slope['ETH']:.2f} (floor 1.0 = impatto almeno lineare) [ASSUNTO]") + print(""" + ⚠️ ERRORE MIO, catturato alla prima corsa. La prima stesura costruiva TRE oggetti-curva + separati (p10, p50, p90), ognuno con la propria maschera di copertura e la propria + pendenza di estrapolazione: oltre il libro visibile la p10 finiva SOPRA la p50 (ETH a $3M: + 18,7 contro 15,5 bps). Una banda che si incrocia non e' una banda. Ora le tre quantili + condividono maschera, `lim` e pendenza, e l'ordinamento e' imposto per costruzione.""") + hs = {a: cur.bps(a, 5.0) for a in ASSETS} + print(f"\n [VENUE] Il fondo della curva E' il mezzo spread: BTC {hs['BTC']:.4f} bps, " + f"ETH {hs['ETH']:.4f} bps β€” cioe' UN TICK.") + print(" ⚠️ Questo NON e' il libro SPOT di Β§52 (mezzo spread misurato 1,5-3,1 bps, e 5,3 in") + print(" sessione): 100-500x piu' largo. La conclusione di Β§52 non e' contraddetta, e'") + print(" CONFERMATA dall'altro lato β€” il costo che si annullava li' era dello SPOT.") + + print("\n [VENUE] PROFONDITA' entro una soglia di costo (mediana sulle istantanee, " + "lato peggiore):") + print(f" {'soglia':>10}{'BTC $ eseguibili':>22}{'ETH $ eseguibili':>22}") + PROF = {} + for soglia in (0.5, 1.0, 5.0, 20.0, 100.0): + row = [cur.profondita(libri, a, soglia) for a in ASSETS] + PROF[soglia] = dict(zip(ASSETS, row)) + print(f" {soglia:>8.1f}bps{row[0]:>22,.0f}{row[1]:>22,.0f}") + + # ===================================================================== + sez("2) LA DISTRIBUZIONE VERA DELLE TAGLIE D'ORDINE β€” non l'ordine medio") + print(""" Il costo e' CONVESSO: la media dei costi non e' il costo della media. Percio' il + costo si applica ordine per ordine, sul nozionale che il libro manda DAVVERO. + TP01: |Dpos| x (cap x 0,75/2), col pavimento min_order $5 gia' dentro (CC.tp01_realistic). + SKH01: due ordini a nozionale PIENO (cap x 0,25/2) per trade, 1,0x per costruzione.""") + with contextlib.redirect_stdout(io.StringIO()): + F = FD.sezione0()["F"] + rate = FD.build_rate(F) + Z = CurvaZero() + print(f"\n {'capitale':>13}{'TP01 n.ord':>12}{'mediana':>11}{'p90':>12}{'max':>13}" + f"{'quota noz.':>12}{'SKH01 ordine':>14}{'x n.':>8}{'quota noz.':>12}") + ordini_cache = {} + for C in (635.0, 5_000.0, 50_000.0, 313_000.0, 3_000_000.0): + _s, dg = tp01_costo(C * W_TP, None, Z) + o = np.concatenate([dg[a]["ordini"] for a in ASSETS]) + ordini_cache[C] = o + ns = sum(len(FD.skh_daily(a, 0, "hourly", None)[1]) for a in ASSETS) + nsk = 2 * ns * C * W_SKH / 2 + print(f" ${C:>12,.0f}{len(o):>12,}{np.median(o):>11,.0f}" + f"{np.percentile(o, 90):>12,.0f}{o.max():>13,.0f}" + f"{o.sum()/(o.sum()+nsk):>11.0%}{C * W_SKH / 2:>14,.0f}{2 * ns:>8,}" + f"{nsk/(o.sum()+nsk):>11.0%}") + print("\n πŸ“Œ Le due gambe hanno distribuzioni OPPOSTE: TP01 manda MOLTI ordini piccoli") + print(" (deriva del vol-target), SKH01 POCHI ordini a nozionale pieno β€” ma sul") + print(" NOZIONALE SCAMBIATO SKH01 pesa il doppio di TP01. A pari capitale il suo") + print(" ordine e' ~100x la mediana di TP01: e' SKH01 a incontrare per primo il muro di") + print(" liquidita', e non e' cio' che si indovinerebbe contando gli ordini (Β§5: contare") + print(" gli ordini da' 16%, contare il nozionale da' 75% β€” stessa trappola).") + + # ===================================================================== + sez("3) REPLICA β€” prima di ogni delta, riprodurre i numeri pubblicati") + ok = {} + tpZ, _ = tp01_costo(635.0 * W_TP, rate, Z) + ref_tp, _ = FD.tp01_realistic(635.0 * W_TP, rate) + d = float(np.max(np.abs(tpZ.values - ref_tp.reindex(tpZ.index).values))) + ok["R1"] = d == 0.0 + print(f" R1 TP01 con costo spento vs `FD.tp01_realistic` max|dif| = {d:.1e} " + f"{'BIT-EXACT' if ok['R1'] else 'FALLITA'}") + + skZ = skh_costo(635.0 * W_SKH, rate, Z) + ref_sk = FD.skh_book(0, "hourly", rate) + d = float(np.max(np.abs(skZ.values - ref_sk.reindex(skZ.index).values))) + ok["R2"] = d == 0.0 + print(f" R2 SKH01 con costo spento vs `FD.skh_book` max|dif| = {d:.1e} " + f"{'BIT-EXACT' if ok['R2'] else 'FALLITA'}") + + bZ = libro(635.0, rate, Z) + ref_b = FD.book_75_25(rate, alloc=635.0) + d = float(np.max(np.abs(bZ.values - ref_b.reindex(bZ.index).values))) + ok["R3"] = d == 0.0 + print(f" R3 libro con costo spento vs `FD.book_75_25` max|dif| = {d:.1e} " + f"{'BIT-EXACT' if ok['R3'] else 'FALLITA'}") + + B = PV.costruisci_serie() + idx = pd.DatetimeIndex(B.index) + A_raw, F_raw = B["nofund"].values.astype(float), B["fund"].values.astype(float) + mv = np.asarray(idx <= pd.Timestamp(VINTAGE_PUB, tz="UTC")) + Lv0 = PV.Lente("vintage LORDO", PV.deluck(A_raw[mv]), True) + Lv1 = PV.Lente("vintage +FISCO", PV.deluck(A_raw[mv]), False) + Lv0.perp, Lv0.muro = PN.muro(Lv0.r, Lv0.prelievo, 0.0, 0.0) + Lv1.perp, Lv1.muro = PN.muro(Lv1.r, Lv1.prelievo, Lv1.aliq, Lv1.patr) + ok["R4"] = abs(Lv0.muro - 272_061.0) < 1.0 + ok["R5"] = abs(Lv1.muro - 258_338.0) < 1.0 + print(f" R4 muro L0 LORDO vintage {VINTAGE_PUB} ${Lv0.muro:>11,.0f} atteso $272,061 " + f"{'ESATTO AL DOLLARO' if ok['R4'] else 'DIVERSO'}") + print(f" R5 muro L1 +FISCO vintage {VINTAGE_PUB} ${Lv1.muro:>11,.0f} atteso $258,338 " + f"{'ESATTO AL DOLLARO' if ok['R5'] else 'DIVERSO'}") + + L3 = PV.Lente("L3 CONGIUNTA", PV.deluck(F_raw), False) + L3.perp, L3.muro = PV.muro_di(L3) + ok["R6"] = abs(L3.muro - 313_000.0) < 4_000.0 + print(f" R6 muro L3 CONGIUNTA (oggi) ${L3.muro:>11,.0f} atteso ~$313k " + f"{'RIPRODOTTO' if ok['R6'] else 'DIVERSO'} {clock()}") + + rng = np.random.default_rng(PV.SEED_TRAJ) + pv = CC._boot_paths(L3.r, 400, 5 * 365, PV.BLOCK, rng) + a1 = PN.accumula(pv, 500.0, L3.muro, L3.aliq, L3.patr, start=635.0) + a2 = accumula_costo(pv, 500.0, L3.muro, L3.aliq, L3.patr, 635.0, None) + d = float(np.max(np.abs(a1["cap"] - a2["cap"]))) + ok["R7"] = d == 0.0 + print(f" R7 `accumula_costo` con drag spento vs `PN.accumula` max|dif| = {d:.1e} " + f"{'BIT-EXACT' if ok['R7'] else 'FALLITA'}") + + import r0823_skeptic_spot as SS # noqa: E402 + synth = dict(bid=100.0, ask=100.1, bids=[[100.0, 3.0], [99.0, 50.0]], + asks=[[100.1, 3.0], [101.0, 50.0]]) + same = all( + (walk_cost_bps(synth, u, sd) is None) == (SS.walk_cost_bps(synth, u, sd) is None) + and (walk_cost_bps(synth, u, sd) is None + or abs(walk_cost_bps(synth, u, sd) - SS.walk_cost_bps(synth, u, sd)) < 1e-12) + for u in (10.0, 300.0, 2_000.0, 1e9) for sd in ("buy", "sell")) + ok["R8"] = same + print(f" R8 la mia `walk_cost_bps` == quella di Β§52 (libro sintetico) " + f"{'IDENTICHE' if same else 'DIVERGONO'}") + print(f"\n ESITO: {sum(ok.values())}/{len(ok)} β€” " + + ", ".join(f"{k}={'OK' if v else 'NO'}" for k, v in ok.items())) + if not all(ok.values()): + raise SystemExit("REPLICA INCOMPLETA: un numero nuovo sarebbe indistinguibile da un bug.") + + big = libro(635.0, rate, CurvaFissa(200.0)) + m0, mB = metrics(bZ), metrics(big) + morde = mB["sharpe"] < m0["sharpe"] - 0.5 + print("\n CONTROLLO POSITIVO del modello di costo β€” a 200 bps per ordine il libro DEVE") + print(f" morire: Sharpe {m0['sharpe']:.3f} -> {mB['sharpe']:.3f}, " + f"CAGR {m0['cagr']:+.2%} -> {mB['cagr']:+.2%}. " + f"{'MORDE' if morde else '⚠️ NON MORDE: modello sospetto'}") + if not morde: + raise SystemExit("il modello di costo non morde: indistinguibile da uno rotto.") + + # ===================================================================== + sez("4) IL COSTO A $635 β€” deve riprodurre Β§5, o la calibrazione e' sbagliata") + o635 = ordini_cache[635.0] + c635 = np.array([cur.bps("BTC", x) for x in o635]) + sk635 = 635.0 * W_SKH / 2 + print(f" [CALC] TP01 a $635: ordine mediano ${np.median(o635):,.0f} -> " + f"{np.median(c635):.4f} bps; ordine massimo ${o635.max():,.0f} -> " + f"{cur.bps('BTC', o635.max()):.4f} bps.") + print(f" [CALC] SKH01 a $635: ordine ${sk635:,.0f} -> {cur.bps('BTC', sk635):.4f} bps.") + b635 = libro(635.0, rate, cur) + print(f" [CALC] drift del libro: {bZ.mean()*DPY:+.4%} senza costo -> " + f"{b635.mean()*DPY:+.4%} col costo = {(b635.mean()-bZ.mean())*DPY*1e4:+.2f} bps/anno.") + if FILLS.exists(): + f = pd.read_csv(FILLS) + sl = f["slip_B5"].astype(float).abs() + print(f" [LOG] i 18 fill reali di Β§5: |slippage| mediana {sl.median():.2f} bps, " + f"p90 {sl.quantile(0.9):.2f} bps, nozionale mediano ${f['notional'].median():,.0f}.") + print(f" πŸ“Œ Il costo MODELLATO ({np.median(c635):.4f} bps) sta " + f"{sl.median()/max(np.median(c635),1e-9):,.0f}x SOTTO la dispersione misurata sui") + print(" fill veri: a questa taglia l'impatto non e' misurabile nemmeno in linea di") + print(" principio, ed e' esattamente cio' che Β§5 concluse (Β«estremi avversi 1/18") + print(" contro 4,7 attesiΒ»). La calibrazione riproduce il fatto, non lo assume.") + + if all(p.exists() for p in LIN5M.values()): + print("\n ⚠️ Β§53 SU QUESTO PUNTO: Β«un fill al 21,9% del volume della sua barra 5m a $635,") + print(" con partecipazione lineare 1,25x lo porta al 27,4% SUBITOΒ».") + print(" Misurato sul feed LINEARE USDC β€” quello che il libro trada davvero:") + print(f" {'asset':>6}{'vol 5m med. (>0)':>20}{'libro a 1bps':>15}{'libro a 20bps':>15}" + f"{'ordine $635':>13}{'quota del libro 1bps':>22}") + rap, rap20 = [], [] + for a in ASSETS: + d5 = pd.read_parquet(LIN5M[a]) + v = (d5["volume"] * d5["close"]).values + vm = float(np.median(v[v > 0])) + rap.append(PROF[1.0][a] / vm) + rap20.append(PROF[20.0][a] / vm) + print(f" {a:>6}{vm:>20,.0f}{PROF[1.0][a]:>15,.0f}{PROF[20.0][a]:>15,.0f}" + f"{sk635:>13,.0f}{sk635/PROF[1.0][a]:>21.2%}") + print(" πŸ“Œ La partecipazione a una barra 5m NON e' un costo: e' la quota del VOLUME") + print(" SCAMBIATO in cinque minuti. Il costo lo decide la liquidita' RESTANTE nel") + print(f" libro: entro 1 bps vale {rap[0]:.1f}x (BTC) / {rap[1]:.1f}x (ETH) quel") + print(f" volume, entro 20 bps {rap20[0]:.0f}x / {rap20[1]:.0f}x.") + print(f" L'ordine di OGGI e' lo {sk635/PROF[1.0]['BTC']:.2%} (BTC) / " + f"{sk635/PROF[1.0]['ETH']:.2%} (ETH) della profondita' a 1 bps;") + print(f" a 1,25x diventa lo {1.25*sk635/PROF[1.0]['BTC']:.2%} / " + f"{1.25*sk635/PROF[1.0]['ETH']:.2%}. L'allarme di Β§53 e' una DOMANDA giusta") + print(" con una grandezza sbagliata: il 21,9% era la quota di una barra a volume") + print(" BASSO, e il volume di una barra non e' la liquidita' disponibile.") + + # ===================================================================== + sez("5) IL LIBRO ALLA TAGLIA β€” drift, vol, Sharpe e capacita' come funzione del capitale") + print(" [ASSUNTO] Il libro d'ordini di OGGI proiettato su 2019-2026. Nel 2019 la") + print(" profondita' di Deribit era una frazione di quella odierna -> ottimista sul passato,") + print(" onesto sul futuro (ed e' il futuro che il muro deve attraversare).") + print(f"\n {'capitale':>15}{'drift':>10}{'vol':>9}{'Sharpe':>9}{'costo/anno':>12}" + f"{'di cui TP01':>13}{'di cui SKH01':>14}{'noz. fuori libro':>18}") + D, VOLS, SH = {}, {}, {} + for C in CAPS: + s = libro(C, rate, cur) + stp, _ = tp01_costo(C * W_TP, rate, cur) + ssk = skh_costo(C * W_SKH, rate, cur) + stp0, _ = tp01_costo(C * W_TP, rate, Z) + ssk0 = skh_costo(C * W_SKH, rate, Z) + b0 = libro(C, rate, Z) + D[C] = float(s.mean() * DPY) + VOLS[C] = float(s.std() * np.sqrt(DPY)) + SH[C] = metrics(s)["sharpe"] + ct = float((stp0.mean() - stp.mean()) * DPY) + cs = float((ssk0.mean() - ssk.mean()) * DPY) + tot = float((b0.mean() - s.mean()) * DPY) + fq, _noz = quota_oltre(C, cur, rate) + print(f" ${C:>14,.0f}{D[C]:>10.2%}{VOLS[C]:>9.2%}{SH[C]:>9.3f}{tot:>12.3%}" + f"{ct:>13.3%}{cs:>14.3%}{fq:>17.1%}") + print(f" {clock()}") + print("\n πŸ“Œ La vol e' invariata su tutta la colonna finche' il costo non esplode: il costo") + print(" d'esecuzione e' una tassa sul DRIFT, non sul rischio β€” stessa forma del funding") + print(" (Β§35). E' cio' che autorizza il modello a un grado di liberta' della sezione 8.") + print(f" πŸ“Œ TROVATO PER STRADA: il drift a $635 ({D[635.0]:.2%}) e' PIU' BASSO che a $5.000") + print(f" ({D[5000.0]:.2%}) e il costo d'esecuzione non c'entra (0,00x%). E' il PAVIMENTO") + print(" `min_order` $5: a $635 un ribilanciamento su tre non si esegue, e la posizione") + print(" insegue il target peggio. Il capitale piccolo ha un suo costo, di segno opposto") + print(" a quello dello slippage β€” e nessuna tabella pubblicata lo distingue dall'altro.") + + # ===================================================================== + sez("6) IL MURO COME PUNTO FISSO, COL COSTO DENTRO") + print(""" Struttura da `r0726_wall_fixedpoint` (importato): serve capitale C per girare il + libro che determina il muro C, quindi C_{n+1} = muro(libro(C_n)). Con un costo FUNZIONE di C + l'iterazione ha un contenuto nuovo: il libro non cambia solo di composizione, cambia di + RENDIMENTO. Lente L3 CONGIUNTA (funding + fisco d'accumulo), de-luck x0,89.""") + inc = WF.included_at(L3.muro) + print("\n [struttura] `WF.included_at` alla taglia del muro direbbe " + + ", ".join(f"{k} {v:.0%}" for k, v in inc.items())) + print(" ⚠️ ma quella e' la composizione DEPLOYABLE del 26/07: GTAA01 e' fuori dallo stesso") + print(" 26/07 (PRIIPs) e la soglia $20k di XS01 e' caduta (Β§5-bis). Il libro che GIRA e") + print(" resta TP01+SKH01, ed e' quello su cui tutti i muri pubblicati sono calcolati:") + print(" qui cambia il COSTO alla taglia, non la composizione.") + print(f"\n {'iter':>5}{'ipotesi':>15}{'drift':>9}{'Sharpe':>9}{'perpetua':>10}{'-> muro':>15}") + Cn = L3.muro + for i in range(6): + s = libro(Cn, rate, cur) + L = PV.Lente("Ln", PV.deluck(s.values.astype(float)), False) + L.perp, L.muro = PV.muro_di(L) + print(f" {i:>5}{'$'+format(Cn,',.0f'):>15}{L.drift:>9.2%}" + f"{metrics(s)['sharpe']:>9.3f}{L.perp:>10.2%}{'$'+format(L.muro,',.0f'):>15}") + conv = abs(L.muro - Cn) / max(Cn, 1) < 0.02 + Cn = L.muro + if conv: + print(f"\n CONVERGE a ${Cn:,.0f} (variazione < 2%) {clock()}") + break + else: + print(f"\n non converge in 6 iterazioni, ultimo valore ${Cn:,.0f}") + muro_endo = Cn + Lf = PV.Lente("Lf", PV.deluck(libro(muro_endo, rate, cur).values.astype(float)), False) + Lf.perp, Lf.muro = PV.muro_di(Lf) + print(f"\n {'lente':<56}{'perpetua':>11}{'muro':>14}") + print(f" {'L3 CONGIUNTA, costo COSTANTE (pubblicato Β§36)':<56}{L3.perp:>11.2%}" + f"{'$'+format(L3.muro,',.0f'):>14}") + print(f" {'L3 + costo ENDOGENO alla taglia (punto fisso)':<56}{Lf.perp:>11.2%}" + f"{'$'+format(Lf.muro,',.0f'):>14}") + print(f"\n >>> il muro si sposta di {Lf.muro/L3.muro-1:+.1%} rispetto al numero pubblicato") + print(" ⚠️ La bisezione del muro e' un Monte Carlo a 4.000 percorsi: la sua risoluzione e'") + print(" ~1% (Β§36). Uno spostamento di questa taglia va letto come NULLO, non come +0,5%.") + + # ===================================================================== + sez("7) IL PUNTO DI SATURAZIONE β€” dove il costo marginale mangia il rendimento marginale") + print(""" Definizione: il profitto annuo in DOLLARI e' P(C) = C x drift(C). Il capitale + marginale smette di comprare reddito all'argmax di P (dP/dC = 0). E' il vero Β«quanto puo' + crescere questo libroΒ», e il progetto non l'aveva mai calcolato. + ⚠️ La camminata assume l'ordine INTERO eseguito nell'istante contro il libro VISIBILE: e' + cio' che fa l'esecutore di OGGI (ordini market, nessuno slicing). Il numero e' percio' una + proprieta' dell'ESECUTORE, non del mercato β€” affettare l'ordine lo sposterebbe in alto.""") + print("\n (a) LIMITE DURO DI CAPACITA' β€” puramente [VENUE]+[BACKTEST], nessuna estrapolazione:") + print(f" {'capitale':>15}{'quota del nozionale annuo NON eseguibile in un istante':>58}") + Chard = None + for C in 10.0 ** np.arange(6.0, 8.01, 0.25): + fq, _ = quota_oltre(float(C), cur, rate) + if Chard is None and fq >= 0.01: + Chard = float(C) + print(f" ${C:>14,.0f}{fq:>57.1%}") + print(f" -> prima taglia con >=1% del nozionale fuori dal libro visibile: " + f"${Chard:,.0f}" if Chard else " -> mai, entro $100M") + print("\n (b) SATURAZIONE ECONOMICA β€” argmax di C x drift(C). Le celle oltre `lim` sono") + print(" [DEDOTTO] (pendenza log-log floor 1.0): la parte destra della curva dipende da") + print(" quell'assunzione, la SINISTRA no.") + grid = 10.0 ** np.arange(np.log10(3e5), 8.01, 0.05) + d0 = float(bZ.mean() * DPY) + for nome, qq in (("mediana", 0.50), ("p10 (libro migliore)", 0.10), ("p90 (peggiore)", 0.90)): + P, dd = [], [] + for C in grid: + s = libro(float(C), rate, cur, q=qq) + d = float(s.mean() * DPY) + dd.append(d) + P.append(C * d) + P, dd = np.asarray(P), np.asarray(dd) + j = int(np.argmax(P)) + c10 = grid[int(np.argmax(dd < 0.9 * d0))] if (dd < 0.9 * d0).any() else float("nan") + c50 = grid[int(np.argmax(dd < 0.5 * d0))] if (dd < 0.5 * d0).any() else float("nan") + fq, _ = quota_oltre(float(grid[j]), cur, rate) + print(f"\n curva {nome:<21} C* = ${grid[j]:>13,.0f} P(C*) = " + f"${P[j]:>11,.0f}/anno drift li' {dd[j]:.2%}") + print(f" {'':<27} costo = 10% del drift a ${c10:>13,.0f} 50% a ${c50:>13,.0f}") + print(f" {'':<27} a C* il {fq:.0%} del nozionale e' gia' fuori dal libro visibile") + print(f"\n {clock()}") + print(f" πŸ“Œ Il muro sta a ${Lf.muro:,.0f}. La saturazione e' PIU' DI UN ORDINE DI GRANDEZZA") + print(" sopra: il vincolo del piano non e' la capacita' del libro.") + print(" πŸ“Œ E le due definizioni CONCORDANO β€” argmax economico e primo 1% di nozionale non") + print(" eseguibile cadono nella stessa decade. Non e' un ottimo liscio: la saturazione") + print(" E' la fine del libro visibile.") + o_big = 1.25e6 + print(f"\n [DEDOTTO] E la cura e' nota e costa poco: un ordine da ${o_big:,.0f} costa " + f"{cur.bps('BTC', o_big):.1f} bps in un") + print(f" istante, ma spezzato in 10 tranche da ${o_big/10:,.0f} ne costa " + f"{cur.bps('BTC', o_big/10):.1f} l'una β€” {cur.bps('BTC', o_big)/max(cur.bps('BTC', o_big/10),1e-9):.0f}x") + print(" meno, SE il libro si ricostituisce fra una tranche e l'altra ([ASSUNTO]: non e'") + print(" misurato qui). Cioe' la capacita' di questo libro e' limitata dall'ESECUTORE, e la") + print(" riparazione e' una riga di codice β€” che pero' serve solo a ~$10M.") + + print("\n (c) CHI SATURA PER PRIMO (costo annuo per gamba, in punti di drift di LIBRO):") + print(f" {'capitale':>15}{'TP01':>10}{'SKH01':>10}{'ordine SKH':>15}" + f"{'bps su quello':>15}{'':>4}") + for C in (1e6, 3e6, 1e7, 3e7, 1e8): + stp, _ = tp01_costo(C * W_TP, rate, cur) + stp0, _ = tp01_costo(C * W_TP, rate, Z) + ssk, ssk0 = skh_costo(C * W_SKH, rate, cur), skh_costo(C * W_SKH, rate, Z) + o = C * W_SKH / 2 + flag = " [DED]" if cur.oltre("ETH", o) else "" + print(f" ${C:>14,.0f}{float((stp0.mean()-stp.mean())*DPY):>10.2%}" + f"{float((ssk0.mean()-ssk.mean())*DPY):>10.2%}{o:>15,.0f}" + f"{cur.bps('BTC', o):>15,.1f}{flag:>6}") + print(" πŸ“Œ SKH01 satura PRIMA di TP01 (ordine 100x piu' grande) ma TP01 lo SUPERA") + print(" subito dopo, perche' la sua CODA di ordini (max ~0,38 x capitale) e' ancora") + print(" piu' grande: due meccanismi diversi che mordono a due decadi diverse.") + + # ===================================================================== + sez("8) LA TRAIETTORIA COL COSTO LUNGO IL CAMMINO") + print(""" Attesa a priori del coordinatore, dichiarata prima di misurare: Β«poco, perche' il + grosso del tempo si passa a capitale piccolo dove il costo e' basso β€” ma il numero conta, + perche' se il muro sale mentre il cammino rallenta i due effetti si SOMMANOΒ». + Modello: i percorsi si estraggono dal libro a $635 (quello di oggi) e ogni giorno si sottrae + `drift($635) - drift(cap_t)`, interpolato in log(capitale) sulla colonna della sezione 5.""") + caps_v = np.array(sorted(D)) + dr_v = np.array([D[c] for c in caps_v]) + + def mk_drag(dmap: dict): + cv = np.array(sorted(dmap)) + dv = np.array([dmap[c] for c in cv]) + + def _f(cap: np.ndarray) -> np.ndarray: + d = np.interp(np.log10(np.maximum(cap, 1.0)), np.log10(cv), dv) + return (dmap[635.0] - d) / DPY + return _f + + drag = mk_drag(D) + print("\n ⚠️ Il drag e' `D($635) - D(cap)` e contiene DUE effetti di segno opposto: il") + print(" pavimento min_order (che penalizza il capitale PICCOLO) e lo slippage (che") + print(" penalizza il GRANDE). Lungo il cammino cambia segno:") + print(f" {'capitale':>14}{'drag (in punti di drift annuo)':>36}") + for c in (2_000.0, 20_000.0, 100_000.0, 315_000.0, 1_000_000.0, 3_000_000.0): + print(f" ${c:>13,.0f}{drag(np.array([c]))[0]*DPY:>35.3%}") + print(" πŸ“Œ Per quasi tutto il tragitto il costo d'esecuzione e' NEGATIVO: uscire dal") + print(" pavimento min_order vale piu' di quanto costi lo slippage fino a ~$300k.") + base = libro(635.0, rate, cur) + Lb = PV.Lente("Lb", PV.deluck(base.values.astype(float)), False) + rng = np.random.default_rng(PV.SEED_TRAJ) + paths = CC._boot_paths(Lb.r, PV.N_TRAJ, PV.ANNI_TRAJ * 365, PV.BLOCK, rng) + print(f"\n {'EUR/mese':>9}{' costo COSTANTE (bersaglio $'+format(L3.muro,',.0f')+')':>40}" + f"{' costo ENDOGENO (bersaglio $'+format(Lf.muro,',.0f')+')':>40}") + print(f" {'':>9}{'mediana':>22}{'P(20a)':>18}{'mediana':>22}{'P(20a)':>18}") + for dep in (250.0, 500.0, 800.0): + a = PV.leggi(PN.accumula(paths, dep, L3.muro, Lb.aliq, Lb.patr, start=635.0)["colpito"]) + b = PV.leggi(accumula_costo(paths, dep, Lf.muro, Lb.aliq, Lb.patr, 635.0, + drag)["colpito"]) + print(f" {dep:>9,.0f}{PV.fmt_cond(a):>22}{a['p20']:>17.1%}" + f"{PV.fmt_cond(b):>22}{b['p20']:>17.1%}") + print(f" {clock()}") + print("\n Scomposizione a EUR500/mese (un grado di liberta' per volta):") + a0 = PV.leggi(PN.accumula(paths, 500.0, L3.muro, Lb.aliq, Lb.patr, start=635.0)["colpito"]) + a1 = PV.leggi(PN.accumula(paths, 500.0, Lf.muro, Lb.aliq, Lb.patr, start=635.0)["colpito"]) + a2 = PV.leggi(accumula_costo(paths, 500.0, Lf.muro, Lb.aliq, Lb.patr, 635.0, + drag)["colpito"]) + print(f" (i) base pubblicata {PV.fmt_cond(a0):>7} " + f"P(20a) {a0['p20']:.1%}") + print(f" (ii) solo il BERSAGLIO piu' alto {PV.fmt_cond(a1):>7} " + f"P(20a) {a1['p20']:.1%}") + print(f" (iii) + il CAMMINO piu' lento {PV.fmt_cond(a2):>7} " + f"P(20a) {a2['p20']:.1%}") + # controllo positivo del drag β€” e un errore MIO, catturato dal controllo stesso + def drag_finto(cap: np.ndarray) -> np.ndarray: + return np.where(cap > 100_000.0, 0.03 / DPY, 0.0) + + ab = PV.leggi(accumula_costo(paths, 500.0, Lf.muro, Lb.aliq, Lb.patr, 635.0, + drag_finto)["colpito"]) + mosso = abs(ab["p20"] - a2["p20"]) > 0.02 + print("\n CONTROLLO POSITIVO del drag β€” con un drag finto di 3%/anno sopra $100k la") + print(f" traiettoria DEVE muoversi: {PV.fmt_cond(ab):>6} P(20a) {ab['p20']:.1%} " + f"{'SI MUOVE' if mosso else '⚠️ NON SI MUOVE: drag sospetto'}") + if not mosso: + raise SystemExit("il drag non morde nemmeno a 3%/anno: macchina sospetta.") + Dfissa = {C: float(libro(C, rate, CurvaFissa(20.0)).mean() * DPY) for C in CAPS} + df20 = mk_drag(Dfissa) + print(f""" + ⚠️ ERRORE MIO, catturato da questo stesso controllo. La prima stesura usava come controllo + positivo un COSTO COSTANTE di 20 bps per ordine β€” e non muoveva niente: il drag di questa + sezione e' una DIFFERENZA, `D($635) - D(cap)`, e un costo che non dipende dalla taglia + abbassa i due termini della stessa quantita' e sparisce nella sottrazione. Misurato: + drag a $1M con costo costante 20 bps = {df20(np.array([1e6]))[0]*DPY:+.4%}/anno contro + {drag(np.array([1e6]))[0]*DPY:+.4%}/anno col costo vero. E' la stessa forma del null + degenere di Β§5-bis (Β«il null del de-levering e' degenere quando la variante e' una + ri-scalaturaΒ»): un controllo puo' fallire perche' e' costruito male, non perche' la + macchina e' rotta β€” e distinguere le due cose e' il controllo.""") + + # ===================================================================== + sez("9) IL GRADINO DI LEVA COL COSTO ENDOGENO") + print(""" Β§53 lo lascio' aperto proprio qui: a 1,25x il nozionale sale SUBITO, quindi il + costo si paga SUBITO. La scala moltiplica il TARGET (SPEC-scale-key, Β§38), quindi moltiplica + anche la taglia di OGNI ordine: `costo_bps(k x nozionale)`, non `k x costo_bps(nozionale)`.""") + print(""" + ⚠️ CONVENZIONE, e non e' un dettaglio. Il `k*` pubblicato dal progetto (Β§35: 13,7 senza + de-luck, 12,25 con) e' calcolato da `FD.k_star` come argmax di `mean(log(1 + k*r))` sulla + serie di libro. RICOSTRUIRE il libro a scala k da' un numero DIVERSO e piu' alto, perche' i + due sleeve hanno clamp interni (`np.clip(net,-0.99)` in TP01, `max(capital,1.0)` in SKH01) + che a leva alta impediscono la rovina che il calcolo pubblicato invece conta. Per restare + comparabile, qui il costo endogeno entra come UN SOLO grado di liberta' sulla serie + pubblicata: `g(k) = g_exact(deluck(r_libro) - drag(k)/k, k)`, dove `drag(k)` e' il costo + d'esecuzione MISURATO alla taglia d'ordine k x nozionale.""") + KS = (1.0, 1.25, 1.5, 2.0, 3.0, 5.0) + KGRID = np.arange(0.5, 30.01, 0.25) + kstar_ctrl = None + KSTAR = {} + for cap_test, etichetta in ((635.0, "a $635 (il conto di oggi)"), + (float(Lf.muro), "alla taglia del muro"), + (5_000_000.0, "a $5M (dove il costo comincia a mordere)")): + print(f"\n --- {etichetta} ---") + r_base = PV.deluck(libro(cap_test, rate, Z).values.astype(float)) + + def gk(k: float) -> tuple[float, float]: + sk = libro(cap_test, rate, cur, scala=k) + s0k = libro(cap_test, rate, Z, scala=k) + dr = float(s0k.mean() - sk.mean()) # drag giornaliero alla scala k + return FD.g_exact(r_base - dr / k, k), dr * DPY + + print(f" {'k':>6}{'g(k) senza costo':>19}{'g(k) col costo':>17}{'costo/anno':>13}" + f"{'ordine SKH':>14}") + for k in KS: + g1, ca = gk(k) + g0 = FD.g_exact(r_base, k) + print(f" {k:>6.2f}{g0:>18.2%}{g1:>17.2%}{ca:>13.3%}" + f"{cap_test * W_SKH / 2 * k:>14,.0f}") + g0v = np.array([FD.g_exact(r_base, float(k)) for k in KGRID]) + g1v = np.array([gk(float(k))[0] for k in KGRID]) + k0 = float(KGRID[int(np.argmax(g0v))]) + k1 = float(KGRID[int(np.argmax(g1v))]) + KSTAR[etichetta] = (k0, k1) + if kstar_ctrl is None: + kstar_ctrl = k0 + print(f" k* con costo ZERO {k0:.2f}x -> k* con costo ENDOGENO {k1:.2f}x" + f" (mezzo-Kelly {k1/2:.2f}x) perdita {k0-k1:.2f}x") + kref = FD.k_star(PV.deluck(FD.book_75_25(rate).values.astype(float)))[0] + print(f"\n R9 controllo di replica: k* a costo ZERO su $635 = {kstar_ctrl:.2f}x contro " + f"{kref:.2f}x") + print(f" di `FD.k_star` sul libro pubblicato (alloc $600) " + f"{'RIPRODOTTO' if abs(kstar_ctrl - kref) <= 0.5 else '⚠️ DIVERGE'}") + print(f" {clock()}") + print("\n πŸ“Œ Il gradino 1,00x -> 1,25x costa, in costo d'esecuzione aggiuntivo, meno di un") + print(" punto base l'anno al conto di oggi. Β§53 chiedeva di misurarlo PRIMA: misurato,") + print(" non morde. Cio' che resta a bloccare il gradino sono le riserve gia' scritte") + print(" (drift stimato su 7,4 anni, coda assente dal dataset, chiave di scala assente") + print(" dalla config), NON il costo d'esecuzione.") + + # ===================================================================== + sez("VERDETTO") + kz, ke = KSTAR["alla taglia del muro"] + k5 = KSTAR["a $5M (dove il costo comincia a mordere)"][1] + print(f""" + [VENUE] La curva costo-taglia del perpetual USDC e' PIATTA al fondo (mezzo spread + {hs['BTC']:.4f}/{hs['ETH']:.4f} bps = UN TICK) e resta sotto ~2 bps fino a mezzo milione di + dollari per ordine β€” misurata di domenica notte UTC, cioe' nella finestra piu' sottile della + settimana. Il libro live manda ordini di ${sk635:,.0f} oggi e di ${Lf.muro*W_SKH/2:,.0f} alla + taglia del muro: entrambi dentro la regione piatta. + + [CALC] Il muro passa da ${L3.muro:,.0f} (costo costante, Β§36) a ${Lf.muro:,.0f} col costo + endogeno = {Lf.muro/L3.muro-1:+.1%}, cioe' DENTRO la risoluzione Monte Carlo del muro stesso. + La traiettoria EUR500/mese non si muove (P(20a) {a0['p20']:.1%} -> {a2['p20']:.1%}). La + saturazione economica sta a ~$1x10^7 e il primo 1% di nozionale non eseguibile in un istante + a ${Chard:,.0f}: due definizioni indipendenti che cadono nella stessa decade, oltre un ordine + di grandezza sopra il muro. + + [CALC] Il gradino di leva sopravvive: k* passa da {kz:.2f}x a {ke:.2f}x alla taglia del muro + ({ke-kz:+.2f}x), e a $635 il costo aggiuntivo di 1,25x sta sotto il punto base l'anno. Il + costo endogeno diventa vincolante sulla leva solo a $5M, dove k* crolla a {k5:.2f}x. + + Il rischio che i due scettici avevano nominato e' REALE ma vive su un ALTRO STRUMENTO e a + un'ALTRA TAGLIA: e' lo SPOT (Β§52, spread 100-500x piu' largo) e sono i milioni, non le + centinaia di migliaia. Sul perpetual, a questo capitale, il costo d'esecuzione NON e' il + vincolo. + + E il fatto piu' utile e' quello trovato per strada: alla taglia di OGGI il costo d'esecuzione + che le tabelle pubblicate non contengono ha segno NEGATIVO. Non e' lo slippage a essere + sottostimato: e' il PAVIMENTO `min_order`, che a $635 costa {(D[5000.0]-D[635.0])*1e4:.0f} bps + di drift l'anno β€” circa {(D[5000.0]-D[635.0])/max(D[313000.0]-D[1000000.0],1e-9):.0f}x cio' + che lo slippage costa passando da $313k a $1M, e in direzione opposta. +""") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_critico_chiusura.py b/scripts/research/r0823_critico_chiusura.py new file mode 100644 index 0000000..f5a9c44 --- /dev/null +++ b/scripts/research/r0823_critico_chiusura.py @@ -0,0 +1,821 @@ +"""r0823 β€” CRITICO DI CHIUSURA dell'ondata 2026-08-22/23 (55 filoni, `research/wave-0822`). + +Non cerca strategie: attacca cio' che l'ondata ha CONCLUSO, coordinatore incluso. +Sola lettura. Nessun ordine, nessuna rete, nessun file di produzione toccato. + +============================================================================================= +ATTESE A PRIORI β€” scritte PRIMA di misurare (un critico che ha sempre ragione non sta criticando) +============================================================================================= +A1 Le repliche dei numeri di testa RIUSCIRANNO (muri, funding, dShFULL della short di TP01): + l'ondata ha fatto molte repliche interne e i suoi script sono ripetibili. + -> attesa: CONFERMATA. Se fallisse, sarebbe il risultato piu' grosso del filone. +A2 Il muro di $313k sara' molto sensibile al drift, ma ci aspettiamo un fattore ~1,5x fra + +1SE e -1SE, cioe' una banda dell'ordine di [$230k, $430k]. + -> attesa: DA MISURARE. (esito: REFUTATA β€” misurato [$204k, $707k] a +/-1 SE, e a -2 SE + il traguardo NON esiste: la mia stima era ottimista di ~2x sull'ampiezza) +A3 Le 24 ancore / 10 fasi porteranno meno informazione di quanta ne sembri, ma su una + DIFFERENZA APPAIATA la componente comune si cancellera' e il numero effettivo di + osservazioni indipendenti sara' >= 5. + -> attesa: DA MISURARE. (esito: REFUTATA β€” non si cancella quasi nulla) +A4 La maggior parte degli SCARTATI dell'ondata sara' correttamente etichettata come + "non misurabile" quando lo e': l'ondata cita gli MDE spesso. + -> attesa: CONFERMATA in larga parte, con eccezioni nominate. +A5 L'aritmetica pubblicata (muri = prelievo/perpetua, tetti di leva) tornera'. + -> attesa: CONFERMATA. +A6 Nessuna misura dell'ondata battera' "versare": ce lo aspettiamo perche' l'ondata stessa + lo ha scritto due volte. Il valore e' nel METTERLE IN FILA con la stessa unita'. + -> attesa: CONFERMATA. + +METODO + * ogni accusa e' MISURATA, non argomentata; + * ogni rilevatore ha un controllo positivo (un rilevatore che non trova nulla e' + indistinguibile da uno rotto); + * le repliche vengono PRIMA di ogni numero nuovo. + +USO: uv run python scripts/research/r0823_critico_chiusura.py [--fast] +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, simple_returns) +from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE, _HL_DIR, _xsec_returns # noqa: E402 + +import r0725_capcurve as CC # noqa: E402 EURUSD, TARGET_EUR_DAY, TAX_RATE, _boot_paths +import r0807_piano_netto as PN # noqa: E402 sopravvivenza / accumula +import r0727_tasse as TX # noqa: E402 PATRIMONIALE + +FAST = "--fast" in sys.argv +N_WALL = 2500 if FAST else 4000 +N_TRAJ = 2000 if FAST else 3000 +BLOCK = 20 +DELUCK = 0.89 +SEED = 725 +T0 = time.time() + +# La serie giornaliera del libro live 75/25, con e senza funding, prodotta da +# r0822d_piano_vero.costruisci_serie(). Se la cache non c'e' si rigenera (richiede rete). +CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/pianovero/book.parquet") + + +def sez(t: str) -> None: + print("\n" + "=" * 102) + print(f" {t} [{time.time() - T0:5.0f}s]") + print("=" * 102) + + +def sh(r: np.ndarray) -> float: + r = np.asarray(r, float) + return float(r.mean() / r.std() * np.sqrt(365.25)) + + +def deluck(r: np.ndarray) -> np.ndarray: + """Toglie dal DRIFT la fortuna d'ancora misurata (r0726_deluck_factor). La vol resta.""" + r = np.asarray(r, float) + return r - (1.0 - DELUCK) * r.mean() + + +def boot_stat(x: np.ndarray, fn, n: int = 3000, block: int = BLOCK, seed: int = 7): + """Block bootstrap di una statistica scalare su una serie (o su una matrice n x k).""" + rng = np.random.default_rng(seed) + x = np.asarray(x, float) + N = x.shape[0] + nb = int(np.ceil(N / block)) + out = np.empty(n) + for i in range(n): + st = rng.integers(0, N - block, size=nb) + ix = (st[:, None] + np.arange(block)[None, :]).reshape(-1)[:N] + out[i] = fn(x[ix]) + return out + + +def n_eff(M: pd.DataFrame) -> tuple[float, float, float]: + """(corr media fuori diagonale, N_eff da rbar, participation ratio degli autovalori).""" + C = M.corr().values + k = C.shape[0] + iu = np.triu_indices(k, 1) + rbar = float(C[iu].mean()) + ev = np.linalg.eigvalsh(C) + pr = float(ev.sum() ** 2 / (ev ** 2).sum()) + return rbar, k / (1.0 + (k - 1) * rbar), pr + + +# ============================================================================================= +# 1) REPLICHE β€” i controlli positivi della MIA macchineria, prima di qualunque accusa +# ============================================================================================= +def resample_off(df: pd.DataFrame, h: int) -> pd.DataFrame: + """Daily con ancora all'ora h. Stessa convenzione di r0702_tp01_offset.resample_offset.""" + d = df.copy() + d["datetime"] = pd.to_datetime(d["datetime"]) + return (d.set_index("datetime") + .resample("24h", offset=pd.Timedelta(hours=h), label="left", closed="left") + .agg({"open": "first", "high": "max", "low": "min", + "close": "last", "volume": "sum"}) + .dropna().reset_index()) + + +_RAW: dict[str, pd.DataFrame] = {} + + +def tp01_at(h: int, long_only: bool = True) -> pd.Series: + """Serie giornaliera netta di TP01 50/50 all'ancora h (long-flat o long-short).""" + if not _RAW: + for a in ("BTC", "ETH"): + _RAW[a] = load_data(a, "1h") + tp = TrendPortfolio(**{**CANONICAL, "long_only": long_only}) + out = {} + for a in ("BTC", "ETH"): + df = resample_off(_RAW[a], h) + r = simple_returns(df["close"].values.astype(float)) + tgt = tp.target_series(df) + held = np.zeros(len(tgt)) + held[1:] = tgt[:-1] + net = held * r - tp.fee_side * np.abs(np.diff(held, prepend=0.0)) + net[0] = 0.0 + out[a] = pd.Series(np.clip(net, -0.99, None), + index=pd.to_datetime(df["datetime"]).dt.normalize()) + J = pd.concat(out, axis=1, join="inner").fillna(0.0) + return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + + +def xs01_phase(phase: int) -> pd.Series: + """XS01 col meccanismo CONGELATO, ribilanciato alla fase `phase` del ciclo H=10.""" + cols = {} + for sym in XS_UNIVERSE: + p = _HL_DIR / f"hl_{sym.lower()}_1d.parquet" + if p.exists(): + d = pd.read_parquet(p) + cols[sym] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + C = pd.concat(cols, axis=1, join="inner").sort_index().dropna() + px = C.values + n, A = px.shape + lbs, H, k, tv = XS_CFG["lookbacks"], XS_CFG["H"], XS_CFG["k"], XS_CFG["target_vol"] + dp, mh, mlb = XS_CFG["disp_pct"], XS_CFG["disp_minhist"], max(XS_CFG["lookbacks"]) + dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) + W = np.zeros((n, A)) + w = np.zeros(A) + hist: list[float] = [] + for i in range(n): + if i >= mlb and i % H == phase: + rLs = [px[i] / px[i - L] - 1.0 for L in lbs] + disp = float(np.mean([r.std() for r in rLs])) + thr = np.percentile(hist, dp) if (dp > 0 and len(hist) >= mh) else -np.inf + if disp >= thr: + score = np.zeros(A) + cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd + cnt += 1 + if cnt: + score /= cnt + o = np.argsort(score) + w = np.zeros(A) + w[o[-k:]] = 0.5 / k + w[o[:k]] = -0.5 / k + else: + w = np.zeros(A) + hist.append(disp) + W[i] = w + gross = np.zeros(n) + gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) + turn = np.zeros(n) + turn[0] = np.abs(W[0]).sum() + turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) + s = pd.Series(gross - turn * (0.001 / 2.0), index=C.index) + rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) + scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) + return pd.Series(s.values * scale, index=C.index) + + +def perpetua(r: np.ndarray, aliq: float, patr: float, + n_paths: int = N_WALL, seed: int = SEED) -> float: + """Prelievo annuo massimo (frazione) con P(capitale a 20a >= iniziale) >= 90%. + Stessa definizione e stessa bisezione di r0807_piano_netto.muro / r0822d_piano_vero.muro_di.""" + lo, hi = 0.0, 0.40 + for _ in range(13): + mid = (lo + hi) / 2 + s = PN.sopravvivenza(r, 1e6, 1e6 * mid, 20, aliq, patr, n_paths=n_paths, seed=seed) + if s["p10_end"] >= 1e6: + lo = mid + else: + hi = mid + return lo + + +PREL_NET = CC.TARGET_EUR_DAY * 365 * CC.EURUSD # 50 EUR/g netti, in $/anno +PREL_LORDO = PREL_NET / (1 - CC.TAX_RATE) + + +def sezione_repliche(B: pd.DataFrame) -> dict: + sez("1) REPLICHE β€” la mia macchineria riproduce l'ondata? (prima di ogni accusa)") + out = {} + A, F = B["nofund"].values, B["fund"].values + print(f" serie del libro live 75/25, {len(B)} giorni, {B.index[0].date()} -> {B.index[-1].date()}") + print(f" {'':22}{'drift':>10}{'vol':>10}{'Sharpe':>10} pubblicato (Β§35)") + print(f" {'senza funding':22}{A.mean()*365:9.2%}{A.std()*365**.5:10.2%}{sh(A):10.3f}" + f" 19,24% / 11,37% / 1,692") + print(f" {'con funding':22}{F.mean()*365:9.2%}{F.std()*365**.5:10.2%}{sh(F):10.3f}" + f" 17,08% / 11,29% / 1,513") + dfund = (F.mean() - A.mean()) * 365 + print(f" costo del funding {dfund:+.4%}/anno pubblicato -2,1597% " + f"{'OK' if abs(dfund + 0.021597) < 2e-4 else 'DIVERGE'}") + out["fund_ok"] = abs(dfund + 0.021597) < 2e-4 + + L0, L1, L3 = deluck(A), deluck(A), deluck(F) + rows = [("L0 LORDO (26/07)", L0, 0.0, 0.0, PREL_LORDO, 0.1068, 278_000), + ("L1 +FISCO (07/08)", L1, CC.TAX_RATE, TX.PATRIMONIALE, PREL_NET, 0.0752, 264_000), + ("L2 +FUNDING (22/08)", L3, 0.0, 0.0, PREL_LORDO, 0.0906, 328_000), + ("L3 CONGIUNTA (Β§36)", L3, CC.TAX_RATE, TX.PATRIMONIALE, PREL_NET, 0.0635, 313_000)] + print(f"\n {'lente':<22}{'drift':>9}{'perpetua':>10}{'muro':>13}{'pubbl.':>11}{'scarto':>9}") + for nome, r, al, pa, prel, p_pub, m_pub in rows: + p = perpetua(r, al, pa) + muro = prel / p + print(f" {nome:<22}{r.mean()*365:8.2%}{p:10.2%}{muro:13,.0f}{m_pub:11,.0f}" + f"{muro/m_pub-1:+9.1%}") + out[nome] = dict(perp=p, muro=muro, r=r, aliq=al, patr=pa, prel=prel) + out["_raw_nofund"] = A + print(" βœ… le quattro lenti si riproducono; il muro E' esattamente `prelievo / perpetua`.") + + # replica di Β§42 (la short del trend) con implementazione indipendente + d0 = sh(tp01_at(0, long_only=False).values) - sh(tp01_at(0, long_only=True).values) + print(f"\n Β§42 dShFULL(floor=-1) all'ancora 0: {d0:+.4f} pubblicato -0,450 (estremo p10)") + print(f" Β§41/Β§35 TP01 canonico ShFULL: {sh(tp01_at(0).values):.3f} pubblicato 1,305-1,314") + # replica di XS01 + x = _xsec_returns() + p0 = xs01_phase(0) + print(f" XS01 fase 0 == sleeve di produzione: max|diff| = " + f"{float(np.abs(p0.values - x.values).max()):.3e}") + return out + + +# ============================================================================================= +# 2) LA BANDA CHE NON E' STATA PUBBLICATA +# ============================================================================================= +def sezione_muro(L: dict) -> dict: + sez("2) IL MURO E' UNA FUNZIONE ESPLOSIVA DI UN DRIFT STIMATO SU 7,4 ANNI") + l3 = L["L3 CONGIUNTA (Β§36)"] + r = l3["r"] + d = boot_stat(r, lambda x: x.mean() * 365, n=4000, seed=11) + SE = float(d.std()) + print(f" drift L3 {r.mean()*365:.3%}/anno Β· SE block-bootstrap (blocchi 20g) {SE:.3%}" + f" Β· IC95 [{np.percentile(d,2.5):.2%}, {np.percentile(d,97.5):.2%}]") + print(f" (Β§53 misura SE 5,09% su un'altra lente: βœ… concorda)") + print("\n Muro e traiettoria propagando SOLO l'incertezza del drift (vol e forma invariate):") + print(f" {'':>10}{'drift':>9}{'perpetua':>10}{'muro':>14}{'x muro pubbl.':>15}") + band = {} + for lab, k in (("+2 SE", 2.0), ("+1 SE", 1.0), ("p90", 1.2816), ("PUNTO", 0.0), + ("p10", -1.2816), ("-1 SE", -1.0), ("-2 SE", -2.0)): + rr = r + k * SE / 365.0 + p = perpetua(rr, l3["aliq"], l3["patr"]) + muro = PREL_NET / p if p > 0.002 else float("inf") + band[lab] = (rr, p, muro) + m = f"${muro:,.0f}" if np.isfinite(muro) else "IRRAGGIUNGIBILE" + x = f"{muro/l3['muro']:.2f}x" if np.isfinite(muro) else "β€”" + print(f" {lab:>10}{rr.mean()*365:8.2%}{p:10.2%}{m:>14}{x:>15}") + lo = band["p10"][2] + hi = band["p90"][2] + print(f"\n 🚨 BANDA p10-p90 DEL MURO: [${hi:,.0f}, " + f"{('$%s' % f'{lo:,.0f}') if np.isfinite(lo) else 'IRRAGGIUNGIBILE'}]" + f" contro il punto pubblicato ${l3['muro']:,.0f}") + print(" Il muro e' `prelievo / perpetua` e la perpetua va a ZERO ben prima del drift:") + print(" e' un 1/x su una quantita' che si annulla, quindi l'errore NON e' simmetrico.") + print(" ⚠️ La SE del block-bootstrap e' una LIMITE INFERIORE dell'incertezza vera") + print(" (misura la variabilita' dentro gli stessi 7,4 anni, non il cambio di regime).") + + print("\n Traiettorie sotto la stessa banda (start $635, 25 anni, block bootstrap):") + print(f" {'drift':>8}{'muro':>13}" + "".join(f"{f'EUR{d}/m':>18}" for d in (250, 500, 1000))) + for lab in ("+1 SE", "PUNTO", "-1 SE"): + rr, p, muro = band[lab] + if not np.isfinite(muro): + continue + rng = np.random.default_rng(SEED) + paths = CC._boot_paths(rr, N_TRAJ, 25 * 365, BLOCK, rng) + cells = [] + for dep in (250, 500, 1000): + a = PN.accumula(paths, dep, muro, l3["aliq"], l3["patr"], start=635.0) + y = np.where(a["colpito"] >= 0, a["colpito"] / 365.0, np.inf) + med = np.median(y) + cells.append(f"{('>25a' if not np.isfinite(med) else f'{med:.1f}a')} P{float((y<=20).mean()):.0%}") + print(f" {rr.mean()*365:7.2%}{muro:13,.0f}" + "".join(f"{c:>18}" for c in cells)) + print("\n πŸ“Œ Β«EUR500/mese -> P(20a) 85%Β» e' il PUNTO. A -1 SE di drift la stessa riga e' ~0%.") + print(" L'ondata ha misurato la risoluzione MONTE CARLO del muro (0,7%) e mai quella") + print(" del suo INPUT. Le due differiscono di due ordini di grandezza.") + return band + + +# ============================================================================================= +# 3) Β«N/N ANCOREΒ» NON E' N OSSERVAZIONI +# ============================================================================================= +def sezione_ancore() -> None: + sez("3) Β«0/24 ANCOREΒ» E Β«10/10 FASIΒ»: quante osservazioni indipendenti sono davvero?") + + # -- controllo positivo del rilevatore (obbligatorio) ------------------------------------- + rng = np.random.default_rng(1) + indip = pd.DataFrame(rng.normal(size=(2000, 24))) + uguali = pd.DataFrame(np.repeat(rng.normal(size=(2000, 1)), 24, axis=1) + + rng.normal(scale=1e-6, size=(2000, 24))) + for nome, M in (("24 serie INDIPENDENTI", indip), ("24 serie IDENTICHE", uguali)): + rb, ne, pr = n_eff(M) + print(f" [controllo positivo] {nome:<24} rbar {rb:+.3f} N_eff {ne:5.2f} PR {pr:5.2f}") + print(" -> il rilevatore distingue i due estremi: il suo Β«pocoΒ» vale.\n") + + # -- TP01: 24 ancore, LIVELLI -------------------------------------------------------------- + S = {h: tp01_at(h) for h in range(24)} + M = pd.DataFrame(S).dropna() + shs = np.array([sh(M[h].values) for h in range(24)]) + rb, ne, pr = n_eff(M) + print(f" TP01 β€” 24 ancore, LIVELLI: Sharpe da {shs.min():.2f} a {shs.max():.2f}" + f" (canonico {shs[0]:.2f})") + print(f" corr media fra ancore {rb:.3f} -> N_eff {ne:.2f} / participation ratio {pr:.2f}") + + # -- TP01: 24 ancore, DIFFERENZA APPAIATA (la forma che l'ondata usa di piu') ------------- + D, dsh = {}, [] + for h in range(24): + a, b = tp01_at(h, True), tp01_at(h, False) + ix = a.index.intersection(b.index) + D[h] = b.reindex(ix) - a.reindex(ix) + dsh.append(sh(b.reindex(ix).values) - sh(a.reindex(ix).values)) + dsh = np.array(dsh) + MD = pd.DataFrame(D).dropna() + rbd, ned, prd = n_eff(MD) + print(f"\n TP01 β€” 24 ancore, DIFFERENZA APPAIATA (Β§42, floor=-1 vs canonico):") + print(f" dShFULL mediana {np.median(dsh):+.3f} banda [{dsh.min():+.3f}, {dsh.max():+.3f}]" + f" >0 in {int((dsh>0).sum())}/24 (Β§42 pubblica -0,354 [-0,450,-0,251] 0/24)") + print(f" corr media delle serie-differenza {rbd:.3f} -> N_eff {ned:.2f} / PR {prd:.2f}") + print(" 🚨 A3 REFUTATA: nella differenza appaiata la componente comune NON si cancella" + f" ({rbd:.2f} contro {rb:.2f} dei livelli).") + + # -- la banda d'ancora NON e' un intervallo di confidenza --------------------------------- + a0, b0 = tp01_at(0, True), tp01_at(0, False) + ix = a0.index.intersection(b0.index) + A0, B0 = a0.reindex(ix).values, b0.reindex(ix).values + X = np.column_stack([A0, B0]) + bs = boot_stat(X, lambda z: sh(z[:, 1]) - sh(z[:, 0]), n=3000, seed=7) + ic = (float(np.percentile(bs, 2.5)), float(np.percentile(bs, 97.5))) + amp_mm = float(dsh.max() - dsh.min()) + amp_pp = float(np.percentile(dsh, 90) - np.percentile(dsh, 10)) + amp_ic = ic[1] - ic[0] + print(f"\n 🚨 Per la STESSA grandezza, all'ancora canonica:") + print(f" banda d'ANCORA min-max (quella che Β§42 pubblica) ampiezza {amp_mm:.3f}" + f" Β· p10-p90 {amp_pp:.3f}") + print(f" IC95 bootstrap a blocchi [{ic[0]:+.3f}, {ic[1]:+.3f}] ampiezza {amp_ic:.3f}" + f" -> {amp_ic/amp_mm:.1f}x la banda pubblicata ({amp_ic/amp_pp:.1f}x la p10-p90)," + f" e CONTIENE LO ZERO.") + print(" La DECISIONE di Β§42 (SCARTATO) regge lo stesso β€” poggia su iso-vol FAIL 3/3,") + print(" maxDD +8,80pp, selection-on-holdout e fee-zero, non su questo Ξ”. Ma il NUMERO") + print(" e la sua banda sono pubblicati con una precisione che il campione non ha.") + + # -- XS01: 10 fasi ------------------------------------------------------------------------- + P = {p: xs01_phase(p) for p in range(10)} + MP = pd.DataFrame(P).dropna() + shp = np.array([sh(MP[p].values) for p in range(10)]) + rbp, nep, prp = n_eff(MP) + bsp = boot_stat(MP[0].values, sh, n=3000, seed=3) + icp = (float(np.percentile(bsp, 2.5)), float(np.percentile(bsp, 97.5))) + amp_f = float(np.percentile(shp, 90) - np.percentile(shp, 10)) + anni = len(MP) / 365.25 + print(f"\n XS01 β€” 10 fasi del ciclo H=10 (universo 19, {anni:.2f} anni):") + print(f" Sharpe per fase da {shp.min():.2f} a {shp.max():.2f}, mediana {np.median(shp):.2f}") + print(f" corr media {rbp:.3f} -> N_eff {nep:.2f} / PR {prp:.2f}") + print(f" banda di FASE p10-p90 ampiezza {amp_f:.3f} contro IC95 bootstrap " + f"[{icp[0]:+.2f}, {icp[1]:+.2f}] ampiezza {icp[1]-icp[0]:.3f} = " + f"{(icp[1]-icp[0])/amp_f:.1f}x") + print(f" MDE sul LIVELLO (1,96/sqrt(anni)) = {1.96/np.sqrt(anni):.2f}" + f" contro Sharpe mediano {np.median(shp):.2f}") + print("\n πŸ“Œ CONSEGUENZA, e vale per TUTTA l'ondata: Β«positivo in N/N ancore o fasiΒ» e' una") + print(" dichiarazione di ROBUSTEZZA ALLA SCELTA DELL'ANCORA, non un test. Il suo") + print(" equivalente in osservazioni indipendenti e' ~2 (ancore) e ~1,3 (fasi), quindi") + print(" NON vale 2^-24 ne' 2^-10; e la Β«bandaΒ» che l'accompagna e' ~4x piu' stretta") + print(" dell'intervallo di confidenza della stessa grandezza.") + print(" Dove l'ondata attacca un p-value a un conteggio del genere (Β§26, Β«test dei segni") + print(" 12/12, p=0,0005Β», su finestre 30g SOVRAPPOSTE e per giunta SELEZIONATE come le") + print(" peggiori) quel p-value non e' calcolabile cosi'.") + + +# ============================================================================================= +# 4) MDE β€” quali SCARTATI sono non-risultati +# ============================================================================================= +def sezione_mde() -> None: + sez("4) MDE β€” quanti SCARTATI dell'ondata sono Β«non misurabileΒ» invece che Β«misurato e noΒ»") + + # finestra reale della catena opzioni, misurata (non presa dal registro) + p = ROOT / "data" / "raw" / "cb_chain" / "bite_archive.parquet" + giorni_sup = np.nan + if p.exists(): + d = pd.read_parquet(p, columns=["asset", "ts", "exp"]) + g = d.groupby(["asset", d["ts"].dt.floor("D")])["exp"].nunique() + gg = min(int((g.loc[a] >= 2).sum()) for a in ("BTC", "ETH")) + prop = len(list((ROOT / "data" / "raw" / "cb_chain").glob("2026-*.parquet"))) + giorni_sup = gg + prop + print(f" [misurato] catena opzioni: archivio {d['ts'].dt.floor('D').nunique()} giorni di") + print(f" calendario, ma solo {gg} con >=2 scadenze; + {prop} di raccolta") + print(f" propria = {giorni_sup} giorni di SUPERFICIE (il registro dice 74-75 βœ…)") + + ANNI_SUP = giorni_sup / 365.25 if np.isfinite(giorni_sup) else 75 / 365.25 + R = [ + # (filone, oggetto misurato, anni utili, effetto dichiarato, unita', tipo) + ("Β§4 DEALER-GAMMA", "Sharpe di cella", 90 / 365.25, 4.98, "Sharpe", "maxk:5.12"), + ("Β§5 SKEW Q1", "Sharpe direzionale", ANNI_SUP, 0.0, "Sharpe", "livello"), + ("Β§5 SKEW Q2", "gate de-risk su TP01", 5 / 365.25, 0.0, "Sharpe", "livello"), + ("Β§6 OI-PIN", "Sharpe max-pain", 75 / 365.25, 5.59, "Sharpe", "maxk:6.79"), + ("Β§7 TERM-STRUCTURE", "Sharpe di cella", 74 / 365.25, 0.14, "Sharpe", "livello"), + ("Β§10 FLOW-SQUEEZE", "Sharpe di cella", 114 / 365.25, 3.25, "Sharpe", "maxk:5.40"), + ("Β§11 VRP-QUOTE-VERE", "Sharpe (T=10)", 10 * 7 / 365.25, 0.0, "Sharpe", "rifiutato"), + ("Β§13 BASIS-CALENDAR", "premio a termine", 7.4, 0.0085, "ret/anno", "livello"), + ("Β§27 SURFACE-RV", "censimento 1,24M quote", ANNI_SUP, 0.0, "$", "censimento"), + ("Β§43 DATA-UNUSED", "Sharpe hold-out", 1.40, 0.951, "Sharpe", "maxk:1.009"), + ("Β§48 VOLVOL", "dSharpe vs TP01", 985 / 365.25, 0.034, "Sharpe", "livello"), + ("Β§21 XS01-OOS (LEAD)", "Sharpe fuori campione", 3.0, 1.12, "Sharpe", "livello"), + ("Β§50 PREVDAY (LEAD)", "Sharpe forward", 63 / 365.25, 2.04, "Sharpe", "livello"), + ("Β§25 BIN-FREQ (LEAD)", "dSharpe di libro", 7.4, 0.112, "Sharpe", "appaiato"), + ("Β§8 VOL-SIZE (LEAD)", "dSharpe di libro", 7.4, 0.070, "Sharpe", "appaiato"), + ("Β§42 TP01-LS", "dSharpe di libro", 7.4, -0.354, "Sharpe", "appaiato"), + ("Β§41 TP01-SIZE", "dSharpe di libro", 7.4, 0.0006, "Sharpe", "appaiato"), + ("Β§49 TP01-TWIN", "dSharpe hold-out", 7.4, -0.127, "Sharpe", "appaiato"), + ] + print(f"\n MDE sui LIVELLI = 1,96/sqrt(anni). Sugli APPAIATI l'MDE del livello NON si applica") + print(f" (la componente comune si cancella): li' serve l'IC bootstrap della DIFFERENZA,") + print(f" che per Β§42 ho misurato = {0.862:.3f} di ampiezza (sez. 3).\n") + print(f" {'filone':<24}{'anni':>7}{'MDE liv.':>10}{'effetto':>10}{'|eff|/MDE':>11} lettura") + for nome, _obj, anni, eff, unita, tipo in R: + mde = 1.96 / np.sqrt(anni) + if tipo == "censimento": + lett = "N di quote, non anni: potenza OK" + rap = float("nan") + elif tipo == "appaiato": + lett = "APPAIATO: l'MDE del livello non si applica" + rap = float("nan") + elif tipo == "rifiutato": + lett = "titolo 32,58 NON riportato dall'ondata: βœ… disciplina" + rap = float("nan") + elif tipo.startswith("maxk:"): + nmax = float(tipo.split(":")[1]) + rap = abs(eff) / mde + lett = (f"max-of-k: il rumore da' {nmax:.2f} -> il candidato " + f"{'STA SOTTO' if abs(eff) < nmax else 'sta sopra'} il proprio null") + elif unita != "Sharpe": + lett = "il registro dichiara SE 1,79pp -> non misurabile" + rap = float("nan") + else: + rap = abs(eff) / mde + lett = ("sotto il proprio MDE = NON MISURABILE" if rap < 1 + else "sopra l'MDE" if rap < 2 else "misurato con margine") + rs = " β€” " if not np.isfinite(rap) else f"{rap:9.2f}" + print(f" {nome:<24}{anni:7.2f}{mde:10.2f}{eff:10.3f}{rs} {lett}") + + print("\n πŸ“Œ A4 CONFERMATA IN LARGA PARTE: l'ondata dichiara il proprio MDE in Β§6, Β§10, Β§43,") + print(" Β§48, Β§56, e in Β§7 spiega perche' NON ha girato il deflated-Sharpe. E' disciplina") + print(" reale, e va detto.") + print(" ⚠️ MA restano tre etichette piu' forti della misura:") + print(" (a) Β§48 chiude un FILONE (Β«quinto lato del DVOL chiusoΒ») su una branca dove") + print(" dichiara esso stesso un fattore 44 sotto l'MDE. La parziale negativa e il") + print(" controllo non-causale hanno potenza; Β«filone chiusoΒ» no.") + print(" (b) Β§4/Β§5/Β§7/Β§11 vivono su ~75-90 giorni: MDE 4,2-4,4 di Sharpe. Ogni loro") + print(" SCARTATO che poggia su un confronto di Sharpe e' un NON-RISULTATO su") + print(" quell'asse (le loro parti valide sono i lead-lag, i placebo e i censimenti,") + print(" non gli Sharpe).") + print(" (c) Β§21 XS01-OOS, che e' il PILASTRO del canale funded, ha effetto +1,12 contro") + print(f" un MDE di {1.96/np.sqrt(3.0):.2f} sui suoi 3 anni: il titolo sta esattamente al") + print(" proprio limite di rilevabilita', e la Β«banda 10 fasiΒ» non e' un IC (sez. 3).") + print(" Nota di equita': Β§21 gira ANCHE un null di permutazione a fee zero (p=0,013),") + print(" che ha piu' potenza del t-stat sullo Sharpe -> il problema non e' Β«nessuna") + print(" evidenzaΒ», e' che il LIVELLO +1,12 viene poi propagato fino a P(>=50/g) e") + print(" al muro come se fosse noto a due cifre.") + + +# ============================================================================================= +# 4bis) CONTRADDIZIONI FRA SEZIONI β€” 55 sezioni scritte da agenti diversi +# ============================================================================================= +def sezione_contraddizioni() -> None: + sez("4bis) CONTRADDIZIONI FRA SEZIONI DELLO STESSO REGISTRO") + + print(" (A) 🚨 MISURATA β€” Β«il libro 75/25Β» ha TRE baseline diverse sotto LO STESSO NOME.") + out = {} + for lens in ("hourly", "canonical"): + s = CC.book_series(lens=lens, alloc=600.0) + r = s.values + eq = np.cumprod(1 + r) + dd = float((1 - eq / np.maximum.accumulate(eq)).max()) + out[lens] = (sh(r), r.mean() * 365, dd) + print(f" lente {lens:<10} ShFULL {sh(r):.3f} drift {r.mean()*365:.2%} maxDD {dd:.2%}") + print(" terza: Β§8 e Β§25 usano 1,626 / maxDD 10,4% = la MEDIANA della banda d'ancora.") + print(f" spread fra le tre: {abs(out['canonical'][0]-out['hourly'][0]):.3f} di Sharpe e " + f"{abs(out['canonical'][2]-out['hourly'][2])*100:.1f}pp di maxDD.") + print(" Chi usa quale: Β§35/Β§36/Β§39/Β§45/Β§52/Β§53 e OGNI muro e traiettoria -> `hourly`") + print(" (1,69); Β§41/Β§42/Β§49/Β§50/Β§54 -> `canonical` (1,81); Β§8/Β§25 -> 1,63.") + print(" 🚨 Lo spread e' PIU' GRANDE di quasi tutti gli effetti che l'ondata misura") + print(" (BIN-FREQ +0,112 Β· VOL-SIZE +0,070 Β· PREVDAY de-luckato +0,100): un Ξ” letto") + print(" accanto al baseline sbagliato cambia di piu' del Ξ” stesso. Ogni filone lo") + print(" dichiara nel proprio script; il REGISTRO no, e CLAUDE.md nemmeno.") + + print("\n (B) 🚨 IL Β«SOFFITTO DIREZIONALEΒ» β€” Β§35 lo abbassa a ~1,15 (era ~1,31) per il") + print(" funding. Ma nella STESSA ondata, sulla STESSA coppia BTC/ETH e su lente netta") + print(" fee: Β§23 pubblica il miglior lookback COSTANTE a ShFULL 1,639 e Β§54 la cella") + print(" PREVDAY scelta al buio a 1,621 (che PASSA il deflated-Sharpe a N=336).") + print(" Le due cose non stanno insieme senza una DEFINIZIONE di Β«soffittoΒ»:") + print(" Β«massimo osservabileΒ» (>=1,64), Β«massimo che sopravvive a un DSRΒ» (>=1,62),") + print(" o Β«Sharpe di TP01Β» (1,15 col funding)? Il registro usa la terza e la cita come") + print(" se fosse la prima.") + print(" ⚠️ E la cosa ha una conseguenza operativa: Β§12 costruisce un argomento") + print(" ARITMETICO (Β«uno screen largo su BTC/ETH non puo' passare il proprio DSR") + print(" perche' il massimo dal rumore, 1,572, sta SOPRA il soffitto ~1,3Β»). Con") + print(" soffitto 1,62-1,64 quell'argomento si inverte β€” ed e' la SECONDA refutazione") + print(" dello stesso argomento, indipendente da quella di Β§24 (che lo ribalta sulla") + print(" partizione della griglia). Un argomento refutato due volte per due strade") + print(" diverse non va ereditato come regola.") + + print("\n (C) Β§50 chiama PREVDAY Β«il piu' forte misurato, robust_oos TRUE, ADDSΒ» e Β§54") + print(" misura che la cella CHE GIRA fallisce il deflated-Sharpe (0,905) mentre quella") + print(" scelta onestamente lo passa (0,993) ma e' un'ALTRA strategia (4h, long-flat).") + print(" Non e' un errore di nessuno dei due β€” Β§50 valuta la cella congelata, Β§54 la") + print(" famiglia β€” ma le due righe finiscono nella stessa memoria e la prima e' quella") + print(" che suona come una promozione.") + + print("\n (D) βœ… CONTRADDIZIONI CHE L'ONDATA HA GIA' RISOLTO DA SOLA, e vanno a suo credito:") + print(" Β§45 Β«lo spot non si liquidaΒ» -> Β§52 lo falsifica (in_cross_collateral_pool);") + print(" Β§39 Β«non esiste una linea USDC datataΒ» -> falsificato lo stesso giorno;") + print(" Β§8 Β«overlay non causaleΒ» -> RITIRATA dallo scettico in giornata;") + print(" Β§24 Β«dealer_net_gamma e' il GEX invertitoΒ» -> ritirata leggendo il sorgente.") + print(" Quattro auto-correzioni in 24 ore: e' il dato piu' sano dell'ondata.") + + +# ============================================================================================= +# 5) ARITMETICA: cosa torna, cosa no +# ============================================================================================= +def sezione_aritmetica(L: dict) -> None: + sez("5) ARITMETICA PUBBLICATA β€” verifica diretta") + + print(" (a) tetti di leva (Β§33/Β§38/Β§53) β€” aritmetica pura sul config che GIRA") + import json + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac, dsl = cfg["max_notional_per_asset_frac"], cfg["disaster_sl_pct"] + n_asset = 2 + peggior_giorno_k1 = 0.1432 # Β§33, bound strutturale + print(f" config letto: frac={frac} Β· disaster_sl_pct={dsl} Β· n_asset={n_asset}") + print(f" leva lorda massima a k=1: {n_asset*frac:.2f}x (test di guardia: <=1x) " + f"{'OK' if n_asset*frac <= 1.0 else 'ROTTO'}") + for lab, tetto, pub in (("disaster-SL <= 50% equity", 0.50 / (n_asset * frac * dsl), 1.67), + ("peggior giorno <= 50%", 0.50 / peggior_giorno_k1, 3.49), + ("peggior giorno <= 20% (scettico)", 0.20 / peggior_giorno_k1, 1.40)): + print(f" {lab:<34} k <= {tetto:5.2f} pubblicato {pub:.2f} " + f"{'βœ…' if abs(tetto-pub) < 0.02 else '⚠️ DIVERGE'}") + print(f" 1,50x -> peggior giorno {1.50*peggior_giorno_k1:.2%} (>20% -> Β§53 lo boccia) βœ…") + print(f" 1,25x -> costo di un disaster-SL {1.25*n_asset*frac*dsl:.1%} (Β§38 dice 37,5%) βœ…") + + print("\n (b) muri = prelievo / perpetua β€” βœ… verificato in sez. 1 su 4 lenti su 4.") + print(f" prelievo netto = {CC.TARGET_EUR_DAY:.0f} EUR/g x 365 x {CC.EURUSD} = " + f"${PREL_NET:,.0f}/anno ; lordizzato ${PREL_LORDO:,.0f}") + + print("\n (c) 🚨 Β«EUR X/meseΒ» NON e' al mese: `accumula` versa ogni 30 GIORNI.") + for anni in (10, 15, 20): + nd = anni * 365 + k = len([t for t in range(nd) if t % 30 == 0 and t > 0]) + print(f" {anni:>2} anni: {k} versamenti invece di {anni*12} " + f"({k/(anni*12)-1:+.2%} di denaro versato)") + print(" -> asintoticamente 365/30 = 12,17 versamenti l'anno invece di 12 (+1,39%).") + print(" Effetto piccolo (0,8-1,4%) ma SISTEMATICO e a FAVORE del piano, presente in") + print(" ogni tabella pubblicata dal 25/07 in poi. Va detto perche' l'ondata dichiara") + print(" la risoluzione del proprio Monte Carlo a 0,7% accanto agli stessi numeri.") + + print("\n (d) 🚨 Β«a 10 anni si versano $229k... i bonifici fanno il 73%Β» β€” il contatore") + print(" `versato` di r0807_piano_netto.accumula e' uno SCALARE che non si ferma al") + print(" traguardo (riga 157-167): conta i versamenti dell'INTERO orizzonte anche sui") + print(" percorsi che sono gia' arrivati. Misura del vero versato-fino-all'arrivo:") + l3 = L["L3 CONGIUNTA (Β§36)"] + rng = np.random.default_rng(SEED) + paths = CC._boot_paths(l3["r"], N_TRAJ, 10 * 365, BLOCK, rng) + dep_m = 1733.0 + dep = dep_m * CC.EURUSD + cap = np.full(N_TRAJ, 635.0) + versato = np.full(N_TRAJ, 635.0) + colpito = np.full(N_TRAJ, -1, int) + carry = np.zeros((N_TRAJ, TX.CARRY_ANNI)) + anno0 = cap.copy() + flusso = np.zeros(N_TRAJ) + for t in range(10 * 365): + cap *= (1.0 + paths[:, t]) + if t % 30 == 0 and t > 0: + vivo = colpito < 0 + cap += dep + flusso += dep + versato += dep * vivo # <- si ferma al traguardo + nuovi = (colpito < 0) & (cap >= l3["muro"]) + colpito[nuovi] = t + if (t + 1) % 365 == 0: + PN.passo_fiscale(cap, anno0, flusso, carry, l3["aliq"], l3["patr"]) + anno0 = cap.copy() + flusso[:] = 0.0 + arr = colpito >= 0 + tot_pieno = 635.0 + 121 * dep + tot_vero = float(versato[arr].mean()) if arr.any() else float("nan") + print(f" EUR{dep_m:.0f}/m, 10 anni: P(traguardo) {arr.mean():.1%}") + print(f" versato come lo conta l'ondata ${tot_pieno:,.0f} (=$229k pubblicato βœ…)") + print(f" versato FINO all'arrivo, medio ${tot_vero:,.0f}" + f" -> i bonifici fanno il {tot_vero/l3['muro']:.0%}, non il 73%") + + print("\n (e) composizione ancora x funding: `deluck` e' applicato DOPO la sottrazione del") + print(" funding, quindi sconta dell'11% anche il funding (2,16% -> 1,92%).") + # ⚠️ ERRORE MIO CATTURATO IN SESSIONE: la prima stesura faceva `deluck(L0["r"])`, ma quella + # serie e' GIA' de-luckata -> 0,89^2 e un finto +31,5% sul muro. Qui si parte dal GREZZO. + A = L["_raw_nofund"] + alt = deluck(A) - 0.021597 / 365.0 + p_alt = perpetua(alt, l3["aliq"], l3["patr"]) + print(f" ordine dell'ondata : drift {l3['r'].mean()*365:.2%} -> muro ${l3['muro']:,.0f}") + print(f" ordine alternativo : drift {alt.mean()*365:.2%} -> muro ${PREL_NET/p_alt:,.0f}" + f" ({PREL_NET/p_alt/l3['muro']-1:+.1%})") + print(" Scelta di modello, non errore β€” ma la sua taglia (+4,0% sul muro) e' SEI VOLTE") + print(" la risoluzione MC (0,7%) che l'ondata dichiara accanto allo stesso numero.") + print(" ⚠️ ERRORE MIO catturato prima di pubblicare: la prima stesura de-luckava una") + print(" serie GIA' de-luckata (0,89^2) e stampava +31,5%. Il controllo che l'ha preso") + print(" e' stato guardare il DRIFT stampato accanto al muro, non il muro.") + + +# ============================================================================================= +# 6) IL LIBRO MASTRO β€” la risposta al mandato, in una sola unita' +# ============================================================================================= +def sezione_ledger(L: dict) -> None: + sez("6) IL MANDATO ERA Β«ARRIVARE AI 50 GIORNALIERI VELOCEMENTEΒ». Tutto in una sola unita'.") + E = 635.0 + l3 = L["L3 CONGIUNTA (Β§36)"] + + def eur_g(drift_pp: float) -> float: + """pp di drift annuo -> EUR/giorno di reddito atteso a $635, al netto del fisco.""" + return drift_pp / 100.0 * E * (1 - CC.TAX_RATE) / 365.0 / CC.EURUSD + + righe = [ + ("VERSARE EUR250 -> EUR500 al mese", "leva del PIANO", None, "P(20a) 14% -> 85%", "SI'"), + ("gradino di leva 1,25x (Β§33/Β§38/Β§53)", "politica", 21.39 - 17.11, + "+1,8a (muro congelato) / +3,0a (mobile)", "NO β€” knob inesistente + GATE SCALA-01"), + ("lead SPOT su TP01 (Β§39/Β§45/Β§52)", "strumento", 1.55, + "muro -10,9% a $600, 0% a $272k", "NO β€” domanda fiscale aperta"), + ("PREVDAY al 15% (Β§50/Β§54)", "sleeve nuovo", 1.31, + "dShFULL +0,100 de-luckato", "NO β€” GATE PREVDAY-01 al 2027-06-21"), + ("XS01 su conto funded (Β§21/Β§29/Β§37)", "canale", None, + "P(>=50/g) 2,6% [1,5-4,7] in 36 mesi", "gate PASS 3/3, spesa = 91% del conto"), + ("BIN-FREQ su SKH01 (Β§25)", "size", None, "EUR0,0113/giorno", "NO β€” 3 gate su 4 FAIL"), + ("VOL-SIZE su SKH01 (Β§8)", "size", None, "+0,070 Sh, gate 22/12 gia' fallito", "NO"), + ("MAKER (Β§22)", "esecuzione", 0.466, "tetto $2,96/anno, IC contiene lo 0", "NO"), + ("--- COSTI SCOPERTI (segno opposto) ---", "", None, "", ""), + ("funding dei perpetual (Β§35)", "costo", -2.16, "muro +17,5%", "gia' subito"), + ("i 4 monitor rotti (Β§31)", "integrita'", None, "STATARB Sharpe +1,96 -> -2,02", "da riparare"), + ("il notifier senza retry (NOTIFIER)", "sicurezza", None, "~7% degli allarmi persi", "da riparare"), + ] + print(f" {'cosa':<40}{'tipo':<12}{'EUR/giorno a $635':>19} effetto / stato") + for nome, tipo, dpp, eff, stato in righe: + if nome.startswith("---"): + print(f"\n {nome}") + continue + cell = f"{eur_g(dpp):+.4f}" if dpp is not None else " β€” " + print(f" {nome:<40}{tipo:<12}{cell:>19} {eff}") + if stato: + print(f" {'':<40}{'':<12}{'':>19} eseguibile oggi: {stato}") + + print("\n πŸ“Œ IL CONFRONTO, con la stessa macchina e gli stessi percorsi:") + rng = np.random.default_rng(SEED) + paths = CC._boot_paths(l3["r"], N_TRAJ, 25 * 365, BLOCK, rng) + for dep in (0, 250, 500): + a = PN.accumula(paths, dep, l3["muro"], l3["aliq"], l3["patr"], start=635.0) + y = np.where(a["colpito"] >= 0, a["colpito"] / 365.0, np.inf) + med = np.median(y) + print(f" EUR{dep:>4}/mese -> mediana {'>25a' if not np.isfinite(med) else f'{med:.1f}a'}" + f" P(20a) {float((y<=20).mean()):5.1%}") + tot = sum(eur_g(x) for x in (1.55, 1.31, 0.466)) + print(f"\n Somma di TUTTI i lead positivi dell'ondata, se fossero tutti autorizzati e") + print(f" tutti additivi (non lo sono, e nessuno lo e' oggi): {tot:+.3f} EUR/giorno.") + print(f" Il gradino di leva da solo, se il knob esistesse: {eur_g(21.39-17.11):+.3f} EUR/g.") + print(f" Un versamento di EUR500/mese e' {500/30.44:+.2f} EUR/giorno di CASSA β€” e le due") + print(" grandezze NON sono la stessa cosa (una e' rendimento, l'altra e' capitale che") + print(" entra). Il confronto onesto e' quello sopra, sulla stessa macchina e sugli") + print(" STESSI percorsi: EUR250 -> EUR500 al mese porta P(20a) dal 14% all'85% e la") + print(" mediana da 23,4 a 17,3 anni; la somma di tutti i lead dell'ondata, se fossero") + print(" autorizzati e additivi, vale +0,036 EUR/giorno su un conto da $635, cioe' meno") + print(" dell'incertezza Monte Carlo della riga sopra.") + print("\n 🚨 RISPOSTA AL MANDATO: NO. L'ondata non ha prodotto NIENTE che avvicini i 50 EUR/g.") + print(" Ha prodotto (i) un COSTO gia' in essere che porta P(20a) a EUR250/mese dal 49%") + print(" al 14% (il funding, Β§35/Β§36), (ii) quattro monitor rotti che avrebbero fatto") + print(" decidere un gate") + print(" al contrario, (iii) un punto singolo di guasto nella rete di sicurezza,") + print(" (iv) tre lead reali ma tutti bloccati da qualcosa che NON e' la ricerca.") + print(" Il suo valore e' DIFENSIVO, ed e' reale: ha impedito decisioni sbagliate.") + print(" Ma la domanda che era stata posta ha risposta negativa, e la 56Βͺ ondata") + print(" avra' la stessa risposta della 55Βͺ.") + + +# ============================================================================================= +# 7) LE TRE COSE CHE, SE SCRITTE COME LE HO CAPITE IO, SARANNO SBAGLIATE +# ============================================================================================= +def sezione_tre() -> None: + sez("7) LE TRE COSE CHE, SE SCRITTE IN CLAUDE.md COME SONO STATE CAPITE, SARANNO SBAGLIATE") + print(""" + (1) Β«IL MURO E' $313kΒ» β€” e' una MEDIANA, non un numero, e la sua banda non e' mai stata + pubblicata. + CLAUDE.md sta per ereditare $313k, Β«EUR250/mese -> P(20a) 14%Β» e Β«EUR1.733/mese per + 10 anniΒ» come se il drift fosse noto. Misurato in sez. 2: propagando SOLO la SE del + drift (5,15%/anno, block bootstrap β€” e Β§53 la misura 5,09 su un'altra lente) il muro + va da $204k (+1 SE) a $707k (-1 SE), a p10 supera $1,1M e a -2 SE il traguardo NON + ESISTE a nessun capitale. La riga Β«EUR500/mese -> P(20a) 85%Β» diventa ~0% a -1 SE. + Il meccanismo e' strutturale: il muro e' `prelievo / perpetua` e la perpetua si + ANNULLA molto prima del drift -> e' un 1/x su una quantita' che va a zero, quindi + l'errore e' asimmetrico verso l'alto. + 🚨 E l'ondata ha pubblicato la risoluzione MONTE CARLO del muro (0,7%) accanto a un + numero la cui incertezza di PARAMETRO e' cento volte piu' grande: ha misurato la + precisione del simulatore e mai quella del suo input. + DA SCRIVERE: Β«$313k e' la MEDIANA; a -1 SE di drift e' ~$707k e a -2 SE il traguardo + non esiste. Il piano si dimensiona sul VERSAMENTO, che e' certo, non sul muro.Β» + + (2) Β«POSITIVO IN 24/24 ANCOREΒ» / Β«10/10 FASIΒ» β€” non sono 24 ne' 10 osservazioni. + Misurato in sez. 3 su serie vere e con controllo positivo (24 serie indipendenti -> + N_eff 24,5; 24 identiche -> 1,00): le 24 ancore di TP01 hanno corr media 0,63 -> + N_eff ~1,6-2,1; le 10 fasi di XS01 corr 0,79 -> N_eff ~1,2-1,5. E la componente + comune NON si cancella nella DIFFERENZA APPAIATA (corr 0,59, N_eff 1,6-2,2): era la + mia attesa A3 ed e' REFUTATA. + 🚨 Conseguenza diretta: la Β«bandaΒ» che accompagna ogni Ξ” dell'ondata e' la banda + d'ANCORA, e per la stessa grandezza (Β§42) l'IC95 bootstrap e' 4,3x piu' largo + ([-0,81, +0,05] contro [-0,45, -0,25]) e CONTIENE LO ZERO. Chi legge fra sei mesi + leggera' quella banda come un intervallo di confidenza β€” perche' ne ha la forma. + E dove l'ondata ci attacca un p-value (Β§26: Β«12/12, p=0,0005Β», su finestre 30g + sovrapposte e selezionate come le peggiori) il p-value non e' calcolabile cosi'. + DA SCRIVERE: Β«N/N ancore = robustezza alla scelta dell'ancora, ~2 osservazioni + indipendenti; la banda d'ancora NON e' un IC ed e' ~4x piu' stretta.Β» + ⚠️ Cio' che questo NON dice: che i verdetti cadano. Β§42 resta SCARTATO (iso-vol FAIL + 3/3, maxDD +8,8pp, selection-on-holdout, edge lordo assente). Cade la PRECISIONE dei + numeri, non la direzione delle decisioni. + + (3) Β«IL GRADINO DI LEVA VALE 14,7a -> 12,9-11,6a, ~EUR300/meseΒ» β€” meta' di quella frase + e' gia' morta nell'ondata stessa. + Β§53 ha misurato che il **1,50x NON sopravvive** (peggior giorno strutturale 21,48%, + sopra la soglia 20%, e al 90% del tetto G6) e che il **k massimo difendibile e' + 1,40**. Il Β«~EUR300/meseΒ» e' del 1,50x. Il 1,25x vale **+EUR144/mese misurati + direttamente**, non EUR164, e **+1,8 anni a muro CONGELATO / +3,0 a muro MOBILE** β€” + una differenza di 1,2 anni fra due convenzioni che Β§33 non dichiarava. + βœ… L'aritmetica dei tetti l'ho verificata sul config che gira (sez. 5a): 1,67 / 3,49 / + 1,40 tornano tutti e tre al centesimo. Non e' quella il problema: e' che CLAUDE.md + porta ancora la riga Β«1,25-1,50xΒ» come se fossero due opzioni vive. + DA SCRIVERE: Β«gradino 1,25x; k max difendibile 1,40; il 1,50x e' BOCCIATO. Guadagno + +1,8a (muro congelato) / +3,0a (mobile) β€” la convenzione va sempre detta.Β» + + ⚠️ BONUS 1 β€” Β«lo SPOT vale +1,55-1,66%/annoΒ». Vale quello SOLO a $600. Β§52 ha camminato + entrambi i libri: il differenziale spot-perp passa da +0,72 bps a $600 a +25,66 bps a + $272k -> **-0,11%/anno alla taglia del muro**. Il lead si annulla esattamente dove + pretendeva di servire. Numero onesto: Β«+1,38-1,58%/anno a $600, ~0 a $272kΒ», e sotto, + la domanda fiscale (~0,83%/anno = piu' di meta' del lead) che nessun backtest chiude. + + ⚠️ BONUS 2 β€” Β«PREVDAY e' il piu' forte misurato, +0,192 FULL / +0,363 HOLDΒ». De-luckato + (Β§54) e' +0,100 / +0,246; la cella che GIRA fallisce il deflated-Sharpe (0,905) mentre + quella scelta al buio lo passa (0,993) ma e' un'ALTRA strategia (4h, long-flat) che + butta via la gamba short per cui PREVDAY fu promosso; e il Β«forward +2,04Β» e' su lente + ORARIA (su lente giornaliera +1,56, 63 giorni, MDE 4,7). + + ⚠️ BONUS 3 β€” Β«il libro 75/25 fa ShFULL 1,8Β». Ne fa TRE (sez. 4bis-A, misurate): 1,68 + (`hourly`, la lente di tutti i muri e le traiettorie), 1,80 (`canonical`, la lente di + Β§41/Β§42/Β§49/Β§50/Β§54) e 1,63 (mediana d'ancora, Β§8/Β§25). Lo spread, 0,12 di Sharpe e + 1,7pp di maxDD, e' PIU' GRANDE di quasi tutti gli effetti che l'ondata misura: un Ξ” + accostato al baseline sbagliato cambia piu' del Ξ”. Ogni numero di libro va scritto con + la sua lente attaccata. + + ⚠️ BONUS 4 β€” Β«il soffitto direzionale e' ~1,15Β». Nella stessa ondata Β§23 pubblica 1,639 e + Β§54 pubblica 1,621, stessa coppia, stessa lente netta-fee. Β«SoffittoΒ» sta per tre cose + diverse e il registro usa la piu' bassa citandola come la piu' alta β€” e su quella + poggia l'argomento aritmetico di Β§12, che si inverte (seconda refutazione indipendente + dopo quella di Β§24). +""") + + +def main() -> None: + print("=" * 102) + print(" r0823 β€” CRITICO DI CHIUSURA dell'ondata 2026-08-22/23") + print("=" * 102) + print(__doc__.split("=====")[2].strip()[:1800]) + if not CACHE.exists(): + print(f"\n ⚠️ manca la cache {CACHE}; rigenerala con " + f"`uv run python scripts/research/r0822d_piano_vero.py --rebuild`") + sys.exit(2) + B = pd.read_parquet(CACHE) + L = sezione_repliche(B) + sezione_muro(L) + sezione_ancore() + sezione_mde() + sezione_contraddizioni() + sezione_aritmetica(L) + sezione_ledger(L) + sezione_tre() + print(f"\n[{time.time() - T0:.0f}s] fine. Sola lettura: nessun file di produzione toccato.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_data_unused.py b/scripts/research/r0823_data_unused.py new file mode 100644 index 0000000..53544c4 --- /dev/null +++ b/scripts/research/r0823_data_unused.py @@ -0,0 +1,594 @@ +"""r0823_data_unused β€” Β§43 DATA-UNUSED: i dati che il progetto possiede e nessuno legge (2026-08-23). + +DOMANDA (ondata `research/wave-0822`, filone 43): *"nuove strategie possibili"*. La fonte piu' +probabile di una strategia nuova non e' un'idea nuova sui soliti dati: e' un dato che nessuno ha +ancora guardato. Due fasi, la seconda dipende dalla prima. + +FASE 1 β€” INVENTARIO (e' gia' un risultato, non un preambolo). + Censisce TUTTO cio' che sta su disco sotto `data/` e, per ogni dataset a schema ricco, quale + COLONNA e' letta da `src/` o da uno script di ricerca e quale non lo e' MAI. La lettura non e' + greppata a mano: si indicizzano una volta tutti i `.py` di src/ scripts/ tests/ (escluso `Old/`, + che e' archivio) e si cerca il nome della colonna come sottostringa. E' una misura GENEROSA verso + "letta" (un nome comune come `close` combacia ovunque) -> quando dice MAI LETTA, e' un fatto. + +FASE 2 β€” UNA misura, la migliore. + Dall'inventario esce un solo candidato che soddisfa tutti e quattro i criteri richiesti: + (i) piu' storia utilizzabile, (ii) meccanismo economico dicibile in una frase, (iii) eseguibile + su Deribit a $635, (iv) non gia' chiuso dal progetto. E' `FlowInExNtv`/`FlowOutExNtv` di + CoinMetrics (`data/external/coinmetrics/cm_{btc,eth}.csv`): **0 file del repo li leggono**, + copertura 100% dal 2018, entrambi gli asset. + + ⚠️ IL PUNTO DELICATO, DICHIARATO IN TESTA. La famiglia "exchange-flow" e' stata UCCISA il + 2026-07-24 (`r0724_onchain_wave.py`, famiglia EXS, 0/6 slot). Riaprirla richiede un meccanismo + NUOVO, non un secondo tentativo. Il meccanismo nuovo qui e' ARITMETICO e si MISURA prima di + usarlo (blocco 1): + - EXS ha testato `SplyExNtv`, lo STOCK di monete sugli exchange; + - `FlowIn - FlowOut` e' la DERIVATA di quello stock -> stessa informazione (lo si misura: + corr con d(SplyEx) attesa ~1); + - `FlowIn + FlowOut`, il turnover LORDO, si CANCELLA nella differenza -> e' algebricamente + ASSENTE da cio' che e' stato testato (corr con d(SplyEx) attesa ~0). + Quindi la parte onestamente nuova del dato e' il LORDO, non il netto. Meccanismo in una frase: + *il turnover lordo sugli exchange misura quanta parte del flottante viene ri-posizionata, cioe' + la partecipazione REGOLATA, a prescindere dalla direzione in cui si agisce.* + + E siccome "riaprire un parametro riapre la sua famiglia" (regola 2026-07-30), il conteggio dei + trial e' fatto AL RIALZO: 36 celle nuove, piu' le 4 gia' spese su EXS il 24/07 -> il deflated + Sharpe si riporta a N=36 E a N=40, e si pubblica la sensibilita' del verdetto al conteggio. + + Controllo positivo obbligatorio: la famiglia UCCISA viene ri-girata con la SUA factory + (`r0724_onchain_wave.exs_factory`, importata, non riscritta). Se il macchinario non riproduce + "earns_slot=False" sul caso noto, non e' credibile su quello nuovo. + +CAUSALITA'. Riusa `r0724_onchain_wave._to_target` e `AVAIL_LAG_D=1` -> lag totale attivita' -> +posizione = 2 giorni (1 di disponibilita' del dato + 1 di `eval_weights`). Non e' una scelta di +oggi: e' la convenzione gia' congelata per questa fonte. + +FINESTRA IN BARRE ATTIVE, non di calendario. Il CSV CoinMetrics community si ferma al 2026-05-24: +dopo quella data il segnale e' assente e la posizione e' FLAT. Contare quei giorni come evidenza +sarebbe l'errore gia' pagato dal progetto (94% di zeri letti come "nessuna perdita"). Qui la +finestra si riporta in barre con posizione != 0. + +COSA NON FA: non manda ordini, non scrive nulla, non tocca `src/` `config/` `scripts/live/` +`data/paper_*`. Sola lettura. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0823_data_unused.py + (aggiungere --skip-inventory per la sola FASE 2) +""" +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy import stats + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import altlib as al # noqa: E402 +from altlib import (causality_ok, deflated_sharpe, eval_weights_smallcap, # noqa: E402 + fmt_marginal, implausible_sharpe, study_family_honest) +import r0724_onchain_wave as ow # noqa: E402 (loader + causalita' + factory della famiglia uccisa) + +DATA = ROOT / "data" +RAW = DATA / "raw" +LINE = "=" * 100 + + +# =========================================================================== +# FASE 1 β€” INVENTARIO +# =========================================================================== +def _index_repo() -> dict[str, str]: + """Tutti i .py di src/ scripts/ tests/ in memoria una volta sola. `Old/` e' ARCHIVIO + (il progetto lo dichiara non fidato) -> escluso: un dato letto solo li' NON e' letto.""" + idx = {} + for d in ("src", "scripts", "tests"): + for p in (ROOT / d).rglob("*.py"): + if "Old/" in str(p): + continue + try: + idx[str(p.relative_to(ROOT))] = p.read_text(errors="ignore") + except OSError: + pass + return idx + + +def _readers(idx: dict[str, str], needle: str, exclude_self: bool = True, + word: bool = True) -> list[str]: + """`word=True` -> confini di parola. La prima stesura cercava la SOTTOSTRINGA e dava + `iv` in 571 file (e' dentro "drive", "receive", ...): un conteggio che non misura niente. + Resta comunque una misura GENEROSA verso "letta" (una variabile locale omonima conta) -> + e' quando dice MAI LETTA che l'affermazione e' forte.""" + pat = re.compile(rf"(? tuple[int, list[str], str]: + try: + d = pd.read_parquet(p) + except Exception as e: # noqa: BLE001 + return 0, [f""], "" + rng = "" + for c in d.columns: + if pd.api.types.is_datetime64_any_dtype(d[c]): + rng = f"{pd.Timestamp(d[c].min()).date()}->{pd.Timestamp(d[c].max()).date()}" + break + else: + if isinstance(d.index, pd.DatetimeIndex) and len(d): + rng = f"{d.index.min().date()}->{d.index.max().date()}" + elif "timestamp" in d.columns and len(d): + t = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + rng = f"{t.min().date()}->{t.max().date()}" + return len(d), list(d.columns), rng + + +# famiglie di file: (etichetta, glob, campione su cui leggere lo schema, CHIAVE di ricerca). +# La chiave e' ESPLICITA e non derivata dall'etichetta: derivandola, due famiglie realmente +# lette (`eqx_`, `fund_`) risultavano "NESSUNO" β€” un falso negativo che avrebbe fabbricato +# un dato "mai usato" inesistente. +FAMILIES = [ + ("btc/eth certificati 5m/15m/1h", "btc_*.parquet|eth_*.parquet", "btc_1h.parquet", ("load_data", "altlib")), + ("alt_sol_* (SOL, escluso 22/08)", "alt_sol_*.parquet", "alt_sol_1h.parquet", ("alt_sol",)), + ("hl_*_1d (51 alt Hyperliquid)", "hl_*_1d.parquet", "hl_btc_1d.parquet", ("hl_{",)), + ("hlfund_*_1h (19 major)", "hlfund_*_1h.parquet", "hlfund_btc_1h.parquet", ("hlfund_{",)), + ("eq_*_1d (29 ETF USA)", "eq_*_1d.parquet", "eq_spy_1d.parquet", ("eq_{", 'eq_*.parquet')), + ("eqx_*_1d (12 UCITS)", "eqx_*_1d.parquet", "eqx_vuaa_1d.parquet", ("eqx",)), + ("fut_*_1h (7 futures indice)", "fut_*_1h.parquet", "fut_es_1h.parquet", ("fut_{",)), + ("fut_deribit/ (70 datati scaduti)", "fut_deribit/*-*.parquet", "fut_deribit/BTC-27DEC24.parquet", ("fut_deribit",)), + ("fut_deribit/fund_* (funding 1h)", "fut_deribit/fund_*.parquet", "fut_deribit/fund_BTC.parquet", ("fut_deribit",)), + ("dvol_* (vol implicita)", "dvol_*.parquet", "dvol_btc.parquet", ("dvol_{",)), + ("vol_term_* (term structure)", "vol_term_*.parquet", "vol_term_btc.parquet", ("vol_term_{",)), + ("fundnews_short_screen", "fundnews_*.parquet", "fundnews_short_screen.parquet", ("fundnews_",)), + ("cb_chain/ (catena opzioni oraria)", "cb_chain/*.parquet", "cb_chain/bite_archive.parquet", ("cb_chain",)), + ("cb_market_snapshots", "cb_market_snapshots.parquet", "cb_market_snapshots.parquet", ("cb_market_snapshots",)), +] + +# dataset a schema RICCO: qui si scende a livello di COLONNA +RICH = [ + ("cb_chain/bite_archive.parquet", RAW / "cb_chain" / "bite_archive.parquet"), + ("cb_market_snapshots.parquet", RAW / "cb_market_snapshots.parquet"), + ("vol_term_btc.parquet", RAW / "vol_term_btc.parquet"), + ("hlfund_btc_1h.parquet", RAW / "hlfund_btc_1h.parquet"), +] + +NON_PARQUET = [ + ("external/coinmetrics/cm_{btc,eth}.csv", DATA / "external/coinmetrics/cm_btc.csv", "cm_btc"), + ("external/coinmetrics/fng.json", DATA / "external/coinmetrics/fng.json", "fng.json"), + ("external/coinmetrics/stables.json", DATA / "external/coinmetrics/stables.json", "stables.json"), + ("external/premium/{cb,upbit}_*.csv + usdkrw", DATA / "external/premium/cb_btc.csv", "usdkrw"), + ("external/stable_snapshots/snapshots.jsonl", DATA / "external/stable_snapshots/snapshots.jsonl", "stable_snapshots"), + ("external/hlp_deepdive/ + hlp_vault.json", DATA / "external/hlp_vault.json", "hlp_"), + ("options_daily/snapshots.jsonl (0DTE)", DATA / "options_daily/snapshots.jsonl", "options_daily"), + ("chain_collect/runs.jsonl (battito)", DATA / "chain_collect/runs.jsonl", "chain_collect"), + ("instruments_registry.json", DATA / "instruments_registry.json", "instruments_registry"), +] + + +def fase1() -> None: + print(LINE) + print(" FASE 1 β€” INVENTARIO: cosa c'e' su disco e CHI lo legge") + print(LINE) + idx = _index_repo() + print(f" indice repo: {len(idx)} file .py in src/ scripts/ tests/ (Old/ escluso = archivio)\n") + + print(" (A) FAMIGLIE DI FILE") + print(f" {'dataset':36s} {'file':>5s} {'righe*':>10s} {'intervallo':22s} lettori (script che aprono il path)") + print(" " + "-" * 130) + for label, pattern, sample, keys in FAMILIES: + files: list[Path] = [] + for pat in pattern.split("|"): + files += sorted(RAW.glob(pat)) + if not files: + print(f" {label:36s} {'0':>5s} ASSENTE") + continue + n, cols, rng = _pq_info(RAW / sample) + # lettori: chi cita il MODO in cui quei path si costruiscono (chiavi esplicite: derivarle + # dall'etichetta dava 2 falsi "NESSUNO" su famiglie realmente lette). Colonna INDICATIVA: + # i due fut_deribit condividono l'accessore (FUT_DIR) e non si distinguono. + rd = sorted({f for k in keys for f in _readers(idx, k, word=False)}) + rdl = ", ".join(Path(x).name for x in rd[:3]) + (f" (+{len(rd)-3})" if len(rd) > 3 else "") + print(f" {label:36s} {len(files):5d} {n:10,d} {rng:22s} {rdl if rd else '*** NESSUNO ***'}") + print(f" {'':36s} {'':5s} {'':10s} cols: {', '.join(cols[:12])}{' ...' if len(cols) > 12 else ''}") + print(" * righe = del file CAMPIONE, non della famiglia intera.\n") + + print(" (B) COLONNE, dataset per dataset β€” 'MAI LETTA' = zero file in src/ scripts/ tests/") + for label, p in RICH: + if not p.exists(): + continue + n, cols, rng = _pq_info(p) + print(f"\n --- {label} ({n:,} righe, {rng})") + for c in cols: + rd = _readers(idx, c) + if not rd: + print(f" {c:26s} *** MAI LETTA ***") + else: + print(f" {c:26s} {len(rd):3d} file ({', '.join(Path(x).name for x in rd[:3])})") + + print("\n --- external/coinmetrics/cm_{btc,eth}.csv (31 colonne, daily dal 2009/2015)") + cm = pd.read_csv(DATA / "external/coinmetrics/cm_btc.csv", low_memory=False) + mai, lette = [], [] + for c in cm.columns: + if c == "time": + continue + (lette if _readers(idx, c) else mai).append(c) + print(f" LETTE ({len(lette)}): {', '.join(lette)}") + print(f" *** MAI LETTE ({len(mai)}) ***: {', '.join(mai)}") + + print("\n (C) SORGENTI NON-PARQUET") + for label, p, key in NON_PARQUET: + if not p.exists(): + print(f" {label:44s} ASSENTE") + continue + sz = p.stat().st_size + rd = _readers(idx, key, word=False) + print(f" {label:44s} {sz/1e6:7.2f} MB lettori: " + f"{', '.join(Path(x).name for x in rd[:3]) if rd else '*** NESSUNO ***'}") + + print("\n (D) DIRECTORY DI STATO (non sono sorgenti di segnale β€” elencate per completezza)") + for d in sorted(DATA.iterdir()): + if not d.is_dir() or d.name in ("raw", "external"): + continue + nf = sum(1 for _ in d.rglob("*") if _.is_file()) + sz = sum(f.stat().st_size for f in d.rglob("*") if f.is_file()) + print(f" data/{d.name:22s} {nf:4d} file {sz/1e6:8.2f} MB") + + +# =========================================================================== +# FASE 2 β€” LA MISURA +# =========================================================================== +# ---- il dato mai letto ----------------------------------------------------- +def _flows(asset: str) -> tuple[pd.Series, pd.Series]: + """FlowInExNtv / FlowOutExNtv: monete che ENTRANO e che ESCONO dagli exchange, in unita' + native, giornaliere. Zero file del repo le leggono (verificato in FASE 1).""" + cm = ow._CM[asset] + I = cm["FlowInExNtv"].astype(float) + O = cm["FlowOutExNtv"].astype(float) + return I.where(I > 0), O.where(O > 0) + + +def _z(s: pd.Series, W: int) -> pd.Series: + mp = max(20, W // 2) + return (s - s.rolling(W, min_periods=mp).mean()) / s.rolling(W, min_periods=mp).std() + + +# ---- le tre variabili (segno a priori dichiarato nel docstring) ------------ +def gross_factory(tf: str, W: int = 90, mode: str = "LF", sign: int = 1): + """GROSS β€” turnover lordo `log(In+Out)`, z-score causale su W. LA PARTE NUOVA: si cancella + nella differenza, quindi lo STOCK gia' testato non la contiene. Segno a priori +1 + (partecipazione alta = mercato vivo).""" + def fn(df, asset): + I, O = _flows(asset) + z = _z(np.log(I + O), W) * sign + pos = np.sign(z) if mode == "LS" else (z > 0).astype(float) + return ow._to_target(df, pos) + return fn + + +def imb_factory(tf: str, W: int = 90, mode: str = "LF", sign: int = 1): + """IMB β€” squilibrio normalizzato `(Out-In)/(Out+In)`, z-score su W. Netto RISCALATO dal + lordo: meta' informazione nuova, meta' del vecchio. Segno a priori +1 (deflussi = accumulo).""" + def fn(df, asset): + I, O = _flows(asset) + z = _z((O - I) / (O + I), W) * sign + pos = np.sign(z) if mode == "LS" else (z > 0).astype(float) + return ow._to_target(df, pos) + return fn + + +def netctl_factory(tf: str, W: int = 90, mode: str = "LF", sign: int = 1): + """NETCTL β€” CONTROLLO NEGATIVO: deflusso netto cumulato su W diviso lo stock. E' la stessa + informazione di `SplyExNtv`, cioe' della famiglia gia' UCCISA. Sta nella griglia apposta: + se la selezione in-sample lo preferisce alle due variabili nuove, la risposta e' che il + dataset non contiene nient'altro di selezionabile.""" + def fn(df, asset): + I, O = _flows(asset) + S = ow._CM[asset]["SplyExNtv"].astype(float) + S = S.where(S > 0) + z = (((O - I).rolling(W, min_periods=max(20, W // 2)).sum()) / S) * sign + pos = np.sign(z) if mode == "LS" else (z > 0).astype(float) + return ow._to_target(df, pos) + return fn + + +VARS = {"GROSS": gross_factory, "IMB": imb_factory, "NETCTL": netctl_factory} + +# ---- GRIGLIA DICHIARATA PRIMA DI GUARDARE QUALSIASI SHARPE ---------------- +WINDOWS = (30, 90, 180) +MODES = ("LF", "LS") +SIGNS = (1, -1) +GRID_1VAR = [dict(W=w, mode=m, sign=s) for w in WINDOWS for m in MODES for s in SIGNS] # 12 +GRID_FULL = [dict(var=v, **p) for v in VARS for p in GRID_1VAR] # 36 +EXS_GRID_SPESA = [dict(L=30, mode="LF"), dict(L=90, mode="LF"), + dict(L=30, mode="LS"), dict(L=90, mode="LS")] # 4, gia' spese 24/07 + + +def family_factory(tf: str, var: str = "GROSS", **p): + return VARS[var](tf=tf, **p) + + +def _active_bars(fn, asset: str) -> tuple[int, int, int, int]: + """barre con posizione != 0, FULL e HOLD-OUT. Il CSV CoinMetrics si ferma al 2026-05-24: + dopo, posizione 0 per assenza di dato -> non e' evidenza, e non va contata come tale.""" + df = al.get(asset, "1d") + t = np.nan_to_num(np.asarray(al._call_target(fn, df, asset), float)) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + act = np.abs(t) > 0 + h = idx >= al.HOLDOUT + return int(act.sum()), len(t), int((act & h).sum()), int(h.sum()) + + +def fase2() -> None: + print("\n" + LINE) + print(" FASE 2 β€” EXFLOW: il turnover LORDO sugli exchange (colonne mai lette)") + print(LINE) + + # ---------------------------------------------------------------- 0. perche' questa + print(""" + PERCHE' QUESTA E NON UN'ALTRA (i quattro criteri, applicati all'inventario): + (i) storia β€” FlowIn/FlowOut coprono 2018->2026 al 100% su ENTRAMBI gli asset. Le altre + colonne mai lette hanno finestre di mesi: `volume_24h` della catena (113 g), + `book_depth_top3` (113 g), `options_daily` 0DTE (30 g), `iv_90d` (58 righe), + `fetch_errors_json` (diagnostica, non segnale). + (ii) meccanismo β€” il turnover lordo misura quanta parte del flottante viene ri-posizionata. + (iii) eseguibile β€” segnale giornaliero -> long-flat/long-short BTC+ETH sui perp Deribit: + esattamente l'impianto di TP01. Verificato sotto a $635 con min-order $5. + (iv) non chiusa β€” le due colonne non sono lette da NESSUN file. Ma la FAMIGLIA (exchange-flow) + e' stata uccisa il 24/07 sullo STOCK -> serve un meccanismo nuovo, misurato + al blocco 1, e i trial vanno contati AL RIALZO (blocco 5).""") + + # ---------------------------------------------------------------- 1. il fatto strutturale + print("\n" + "-" * 100) + print(" 1. IL FATTO CHE GIUSTIFICA (o no) LA RIAPERTURA β€” si misura, non si asserisce") + print("-" * 100) + print(" EXS (24/07) ha testato SplyExNtv = lo STOCK. d(stock) = In - Out = il NETTO.") + print(" Se corr(d stock, netto) ~ 1 e corr(d stock, lordo) ~ 0, allora il LORDO e'") + print(" algebricamente assente da cio' che e' stato provato β€” ed e' l'unica parte nuova.\n") + for a in ("BTC", "ETH"): + I, O = _flows(a) + S = ow._CM[a]["SplyExNtv"].astype(float).where(lambda s: s > 0) + m = I.notna() & O.notna() & S.notna() + dS, net, gross = S[m].diff(), (I - O)[m], (I + O)[m] + ok = dS.notna() + zg = _z(np.log(gross), 180) + mom = np.log(S[m]) - np.log(S[m]).shift(30) + ratio = (net.abs() / gross) + big = gross[zg > 3] + print(f" {a} n={int(m.sum()):,} {I[m].index[0].date()} -> {I[m].index[-1].date()}") + print(f" corr( d(SplyEx) , In-Out ) = {dS[ok].corr(net[ok]):+.4f} <- il NETTO E' la derivata dello stock") + print(f" corr( d(SplyEx) , In+Out ) = {dS[ok].corr(gross[ok]):+.4f} <- il LORDO no") + print(f" corr( z(lordo) , momentum-30g dello stock ) = {zg.corr(mom):+.4f}") + print(f" |In-Out| / (In+Out) mediano = {float(ratio.median()):.3f} -> il {100*(1-float(ratio.median())):.0f}% del") + print(" flusso lordo SI CANCELLA: lo stock vede sei monete su cento fra quelle che si muovono.") + if len(big): + print(f" (nei {len(big)} giorni a z(lordo)>3 il rapporto resta {float(ratio[zg > 3].median()):.3f})") + + # ---------------------------------------------------------------- 2. griglia dichiarata + print("\n" + "-" * 100) + print(" 2. GRIGLIA DICHIARATA PRIMA DI GUARDARE, E CONTATA AL RIALZO") + print("-" * 100) + print(f" variabili {tuple(VARS)} x W {WINDOWS} x mode {MODES} x sign {SIGNS}") + print(f" = {len(GRID_FULL)} celle nuove, tf 1d soltanto.") + print(f" + {len(EXS_GRID_SPESA)} celle GIA' SPESE su questa famiglia il 24/07 (EXS) = {len(GRID_FULL)+len(EXS_GRID_SPESA)} trial") + print(" 'quando si riapre un parametro si riapre la sua famiglia' (regola 2026-07-30).") + + # ---------------------------------------------------------------- 3. controllo positivo + print("\n" + "-" * 100) + print(" 3. CONTROLLO POSITIVO β€” la famiglia UCCISA, ri-girata con la SUA factory") + print("-" * 100) + rep_exs = study_family_honest("EXS-replica(24/07)", ow.exs_factory, EXS_GRID_SPESA, tfs=("1d",)) + ch = rep_exs["chosen"] + print(f" cella IS {ch['params']} IS {ch['insample_sharpe']} FULL {ch['full_sharpe']} " + f"DSR {rep_exs['deflated_sharpe']} earns_slot_honest={rep_exs['earns_slot_honest']}") + print(" atteso: False (l'ondata 24/07 chiuse 0/6). Se qui uscisse True, il macchinario") + print(" non sarebbe credibile sul caso nuovo e il resto andrebbe buttato.") + exs_sharpes = [r["full_sharpe"] for r in rep_exs["rows"]] + + # ---------------------------------------------------------------- 4. il gate di famiglia + print("\n" + "-" * 100) + print(" 4. GATE DI FAMIGLIA β€” `study_family_honest` (cella scelta IN-SAMPLE, DSR, marginal)") + print("-" * 100) + rep = study_family_honest("EXFLOW", family_factory, GRID_FULL, tfs=("1d",)) + ch = rep["chosen"] + daily = al.candidate_daily(family_factory(tf="1d", **ch["params"]), tf="1d") + hold = daily[daily.index >= al.HOLDOUT] + print(f" celle valutate: {rep['n_cells']}") + print(f" cella scelta AL BUIO (solo in-sample): {ch['params']}") + print(f" Sharpe IS {ch['insample_sharpe']} FULL {ch['full_sharpe']} HOLD-OUT {al._sh(hold):.3f}") + print(f" deflated-Sharpe {rep['deflated_sharpe']} (massimo atteso dal PURO RUMORE: " + f"Sharpe {rep['expected_null_max']}) pass={rep['dsr_pass']}") + print(fmt_marginal(rep["marginal"])) + print(f" >>> earns_slot_honest = {rep['earns_slot_honest']}") + + print("\n classifica per Sharpe IN-SAMPLE (le prime 6 e le ultime 3 di 36):") + for r in rep["rows"][:6] + rep["rows"][-3:]: + d = al.candidate_daily(family_factory(tf="1d", **r["params"]), tf="1d") + print(f" {str(r['params']):58s} IS {r['insample_sharpe']:6.3f} FULL {r['full_sharpe']:6.3f}" + f" HOLD {al._sh(d[d.index >= al.HOLDOUT]):6.3f}") + + # ---------------------------------------------------------------- 5. sensibilita' al conteggio + print("\n" + "-" * 100) + print(" 5. SENSIBILITA' DEL VERDETTO AL CONTEGGIO DEI TRIAL (regola 2026-07-30)") + print("-" * 100) + new_sh = [r["full_sharpe"] for r in rep["rows"]] + # ⚠️ ERRORE MIO, CORRETTO PRIMA DI PUBBLICARE: la prima stesura usava `new_sh[:12]` come + # "una sola variabile". `rows` e' ORDINATA per Sharpe in-sample -> quelle 12 erano le 12 + # MIGLIORI, un pool a varianza artificialmente bassa, e il DSR usciva 0.975 = PASS. Non e' + # una lettura generosa, e' un pool sbagliato. La partizione legittima e' la variabile scelta. + own = [r["full_sharpe"] for r in rep["rows"] if r["params"]["var"] == ch["params"]["var"]] + for lab, pool in ((f"N=12 (la sola variabile scelta, {ch['params']['var']})", own), + ("N=36 (griglia nuova dichiarata)", new_sh), + ("N=40 (+ le 4 gia' spese su EXS il 24/07)", new_sh + exs_sharpes)): + dsr, sr0 = deflated_sharpe(al._sh(daily), pool, daily) + print(f" {lab:48s} DSR {dsr:.3f} null-max {sr0:.3f} (n pool {len(pool)})") + print(" Il verdetto e' lo stesso a ogni conteggio LEGITTIMO. Ma il DSR resta sensibile a") + print(" COME si partiziona la griglia (lezione Β§10 dell'ondata): con un pool scelto male") + print(" β€” le 12 celle migliori β€” lo stesso candidato PASSA a 0.975. La partizione e' il") + print(" secondo posto dove barare e' indolore e invisibile.") + + # ---------------------------------------------------------------- 6. sotto-famiglie + print("\n" + "-" * 100) + print(" 6. DECOMPOSIZIONE PER VARIABILE (diagnostica: riportare il meglio di tre E' selezione,") + print(" quindi il numero che vale resta quello del blocco 4)") + print("-" * 100) + for nm, fac in VARS.items(): + r = study_family_honest(nm, fac, GRID_1VAR, tfs=("1d",)) + c = r["chosen"] + d = al.candidate_daily(fac(tf="1d", **c["params"]), tf="1d") + print(f" {nm:7s} IS-pick {str(c['params']):40s} IS {c['insample_sharpe']:6.3f} " + f"FULL {c['full_sharpe']:6.3f} HOLD {al._sh(d[d.index >= al.HOLDOUT]):6.3f} " + f"DSR(12) {r['deflated_sharpe']} marg {r['marginal']['marginal_verdict']} " + f"corr->TP01 {r['marginal']['marginal']['corr_full']}") + print(" La selezione in-sample sull'INTERA griglia preferisce NETCTL, cioe' il CONTROLLO") + print(" NEGATIVO = l'informazione gia' uccisa. La parte nuova non e' selezionabile.") + + # ---------------------------------------------------------------- 7. direzione o volatilita' + print("\n" + "-" * 100) + print(" 7. DOVE STA L'INFORMAZIONE β€” direzione o volatilita'? (la letteratura che il progetto") + print(" stesso cita il 24/07 dice: i flussi predicono la VOL, non la direzione)") + print("-" * 100) + + def _t(x, y): + c = float(np.corrcoef(x, y)[0, 1]) + return c, c * np.sqrt(len(x) - 2) / np.sqrt(max(1e-12, 1 - c * c)) + + for a in ("BTC", "ETH"): + df = al.get(a, "1d") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)).floor("D") + r = pd.Series(al.simple_returns(df["close"].values.astype(float)), index=idx) + I, O = _flows(a) + zg = _z(np.log(I + O), 180).shift(ow.AVAIL_LAG_D).reindex(idx) + zi = _z((O - I) / (O + I), 180).shift(ow.AVAIL_LAG_D).reindex(idx) + rv = r.rolling(30).std() + fdir, fabs = r.shift(-1), r.abs().shift(-1) + m = zg.notna() & zi.notna() & fdir.notna() & rv.notna() + n = int(m.sum()) + cd, td = _t(zg[m], fdir[m]) + ci, ti = _t(zi[m], fdir[m]) + ca, ta = _t(zg[m], fabs[m]) + cb, tb = _t(rv[m], fabs[m]) + # incrementale ONESTO: residuo OLS di |r_{t+1}| su RV30_t, poi correlato con z(lordo) + X = np.column_stack([np.ones(n), rv[m].values]) + beta = np.linalg.lstsq(X, fabs[m].values, rcond=None)[0] + resid = fabs[m].values - X @ beta + cr, tr = _t(zg[m].values, resid) + rho, pv = stats.spearmanr(zg[m].values, resid) + yr_idx = zg[m].index + per_yr = {int(y): round(float(np.corrcoef(zg[m].values[yr_idx.year == y], + resid[yr_idx.year == y])[0, 1]), 3) + for y in sorted(set(yr_idx.year))} + print(f" {a} n={n}") + print(f" DIREZIONE corr(z lordo, r_domani) = {cd:+.4f} t={td:+5.2f}") + print(f" corr(z squilibrio, r_dom) = {ci:+.4f} t={ti:+5.2f}") + print(f" VOLATILITA corr(z lordo, |r|_domani) = {ca:+.4f} t={ta:+5.2f}") + print(f" baseline corr(RV30, |r|) = {cb:+.4f} t={tb:+5.2f} <- il confronto giusto") + print(f" z lordo sul RESIDUO OLS = {cr:+.4f} t={tr:+5.2f} <- l'incremento vero") + print(f" lo stesso in RANGO (Spearman) = {rho:+.4f} p={pv:.4f} <- e' monotono o e' coda?") + print(f" Pearson per anno: {per_yr}") + + # ---------------------------------------------------------------- 8. gate residui + print("\n" + "-" * 100) + print(" 8. GATE RESIDUI SULLA CELLA SCELTA") + print("-" * 100) + fn = family_factory(tf="1d", **ch["params"]) + print(f" causality_ok : {causality_ok(fn, tf='1d')}") + imp = implausible_sharpe(daily) + print(f" implausible_sharpe: implausible={imp['implausible']} reasons={imp['reasons']} " + f"attive={imp['n_active']} quota-perdite-su-attive={imp['loss_frac']:.3f}") + print(" eseguibilita' a $635 (min-order $5):") + for a in ("BTC", "ETH"): + df = al.get(a, "1d") + sc = eval_weights_smallcap(df, al._call_target(fn, df, a), capital=635.0, min_order=5.0) + af, tf_, ah, th = _active_bars(fn, a) + print(f" {a}: haircut Sharpe {sc['sharpe_haircut']:+.3f} trade eseguiti {sc['n_executed_trades']:4d}" + f" | barre ATTIVE {af}/{tf_} ({100*af/tf_:.1f}%) hold-out attive {ah}/{th}") + + # ---------------------------------------------------------------- 9. potenza + print("\n" + "-" * 100) + print(" 9. POTENZA β€” in barre ATTIVE, non di calendario") + print("-" * 100) + tgt = np.abs(np.nan_to_num(np.asarray(al._call_target(fn, al.get('BTC', '1d'), 'BTC'), float))) > 0 + print(f" il CSV CoinMetrics si ferma al {ow._CM['BTC'].index[-1].date()}: dopo quella data la") + print(" posizione e' FLAT per ASSENZA DI DATO. Contarlo come evidenza sarebbe l'errore gia'") + print(" pagato dal progetto (zeri letti come 'nessuna perdita').") + for lab, s in (("FULL", daily), ("HOLD-OUT", hold)): + nz = int((s != 0).sum()) + yr = nz / 365.25 + se = np.sqrt(1.0 / max(yr, 1e-9)) + print(f" {lab:9s} barre non-nulle {nz:5d} ({yr:.2f} anni) SE(Sharpe) ~ {se:.2f}" + f" -> differenza rilevabile al 95% ~ {1.96*se:.2f} di Sharpe") + print(f" (usato solo per dire cosa la finestra puo' e non puo' decidere: n_target_attivo={int(tgt.sum())})") + + # ---------------------------------------------------------------- 10. verdetto + print("\n" + LINE) + print(" VERDETTO") + print(LINE) + print(f""" + SCARTATO, per tre ragioni indipendenti e in quest'ordine di forza: + + (1) LA CELLA SCELTA AL BUIO STA SOTTO IL RUMORE. Sharpe FULL {ch['full_sharpe']} contro un + massimo atteso dal PURO RUMORE di {rep['expected_null_max']} su {rep['n_cells']} trial. + Non e' "non passa il gate per poco": e' sotto la soglia che una griglia di monete + raggiungerebbe da sola. Stessa aritmetica di ORTHO-SCREEN (Β§12). + + (2) LA SELEZIONE IN-SAMPLE TORNA SUL CONTROLLO NEGATIVO. Fra le tre variabili sceglie + NETCTL, cioe' l'informazione dello STOCK gia' uccisa il 24/07. Il turnover LORDO β€” + l'unica parte algebricamente nuova (corr con d(stock) 0.03-0.08, e il 94% del flusso + che si cancella) β€” NON e' selezionabile: la sua migliore cella in-sample fa hold-out + NEGATIVO (-0.42) ed e' DILUTES contro TP01. + + (3) ZERO INFORMAZIONE DIREZIONALE, E IL LEGAME CON LA VOL NON REGGE AL SECONDO SGUARDO. + corr(segnale, ritorno di domani): |t| < 1 su 2 asset x 2 variabili nuove β€” ed e' la + direzione cio' di cui una strategia aveva bisogno. Sul |ritorno| il quadro sembra + diverso β€” ETH da' Pearson incrementale sul residuo di RV30 t=+2.70 β€” ma: + * in RANGO (Spearman) diventa +0.015 con p=0.46 -> non e' una relazione monotona, + e' la coda della distribuzione dei livelli; + * per anno decade e cambia segno (2021 +0.13 ... 2024 -0.03, 2026 -0.06); + * su BTC lo stesso test da' -0.005 (Pearson) e -0.051 con p=0.007 (Spearman), cioe' + "significativo" nel verso SBAGLIATO = firma di rumore, non di segnale. + Il turnover sugli exchange sale quando la vol e' GIA' salita: termometro contemporaneo, + stessa forma gia' trovata su TERM-STRUCTURE (Β§7) e SKEW (Β§5). + ⚠️ E anche se il legame fosse reale, non produrrebbe un candidato: l'unica espressione + eseguibile di una vista sulla VOL a $635 e' short-vol (VRP01: 4/4 gate falliti, 5/5 + overlay refutati, regola "niente short-vol da modello in deploy") oppure il denominatore + del vol-target di TP01 (TP01xDVOL, refutato come de-levering il 26/06). E' chiuso a valle, + non solo a monte. + + COSA SI PORTA A CASA COMUNQUE: + * il marginal scorer da' corr->TP01 0.50-0.65 su colonne che l'ondata 24/07 non aveva MAI + letto -> REPLICA INDIPENDENTE del suo finding ("l'on-chain tradabile e' prezzo travestito, + corr 0.50-0.82"), raggiunta da un'altra porta e con un altro stimatore; + * l'eseguibilita' a $635 non ha bocciato nulla (haircut 0.000 su entrambe le gambe): ennesima + conferma che il vincolo del progetto non e' piu' il capitale ma l'edge; + * il pezzo di METODO riusabile e' il CONTROLLO NEGATIVO DENTRO LA GRIGLIA: infilare nella + stessa famiglia una variabile che si SA gia' morta trasforma "il candidato perde" in + "il dato non contiene nient'altro di selezionabile", che e' un'affermazione molto piu' + forte e costa una factory. + + RISPOSTA ALLA DOMANDA DEL FILONE: dei dati che nessuno legge, uno solo aveva storia + sufficiente per sostenere un'ipotesi, ed e' stato misurato fino in fondo. Tutto il resto + dell'inventario e' telemetria a finestra corta (30-113 giorni) o gia' analizzato. + **Nessun dato inutilizzato sostiene oggi un'ipotesi nuova.** +""") + + +def main() -> None: + skip = "--skip-inventory" in sys.argv + print(LINE) + print(" Β§43 DATA-UNUSED β€” quali dati il progetto possiede e nessuna strategia legge") + print(f" repo {ROOT} (sola lettura: nessuna scrittura, nessun ordine)") + print(LINE) + if not skip: + fase1() + fase2() + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_deposit_timing.py b/scripts/research/r0823_deposit_timing.py new file mode 100644 index 0000000..630d0ec --- /dev/null +++ b/scripts/research/r0823_deposit_timing.py @@ -0,0 +1,695 @@ +#!/usr/bin/env python +"""r0823_deposit_timing.py β€” Β§44 DEPOSIT-TIMING: il timing dei BONIFICI paga? + +LA DOMANDA. Nel progetto e' misurato che la leva dominante non e' la strategia ma il +versamento: *versare o no* (da mai a ~16 anni) > *quando* (fattore 6 a pari soldi totali) > +*quanto presto si smette* (i primi 5 anni sono il 25% dei soldi e il 58% del risultato) > +*piatto vs crescente* (24%) > *frequenza* (1-3%). Ma TUTTE quelle misure confrontano +**calendari deterministici**. Nessuno ha mai misurato un calendario **CONDIZIONALE**: versare +di piu' dopo un drawdown, o quando il libro e' risk-off, o su una qualunque regola che guarda +lo stato del conto. + + A pari soldi totali versati e pari calendario di DISPONIBILITA', una regola di deposito + condizionale batte il versamento piatto? + +⚠️ E' MARKET TIMING SUI PROPRI BONIFICI, e va giudicato come tale. Il null e' severo (sotto). + +ATTESA A PRIORI, DICHIARATA PRIMA DI MISURARE (coordinatore, e la sottoscrivo): **NON +funziona**. Comprare piu' in basso alza il rendimento sul capitale versato, ma la mediana e' +dominata dal fatto che i soldi entrano PIU' TARDI, e il progetto ha gia' misurato che entrare +prima vale un fattore 6 (r0726_deposits punto 3). L'esito e' riportato comunque, anche se +refuta l'attesa. + +IL VINCOLO DI REALTA' CHE RENDE LA DOMANDA NON BANALE. I soldi non compaiono. Ogni politica +riceve lo STESSO flusso di cassa (EUR X/mese sul conto corrente); cio' che cambia e' **quando +entrano nel libro**. La liquidita' in attesa rende **0% reale netto** (assunzione del progetto: +XEON) β€” ed e' meta' del costo di ogni politica che aspetta. Senza questo vincolo la domanda +sarebbe truccata: "versare di piu' nei cali" senza dire da dove vengono i soldi e' un confronto +fra due piani di versamento diversi, non fra due timing. + +LE POLITICHE, DICHIARATE TUTTE PRIMA (e contate come trial: 12 celle candidate) + P0 PIATTO β€” versa tutto il buffer il giorno stesso. E' l'incumbent. + P1 BUY-THE-DIP(th) β€” accantona; versa TUTTO il buffer quando l'indice di equity del libro + sta >= th sotto il suo massimo. th in {5,10,15,20}% (4 celle) + P2 RISK-OFF β€” versa quando TP01 e' a 0x (segnale gia' pubblico e causale, ~33% dei + giorni). (1 cella) + P3 VALORE-MEDIO(g) β€” versa quanto serve a riportare il capitale su una traiettoria + bersaglio che cresce a g. Mai vendere. g in {0,8,15}%/anno (3 celle) + P4 ANTI-DIP(th) β€” lo SPECULARE OBBLIGATORIO: versa dopo un rialzo di th dal minimo + corrente. th in {5,10,15,20}% (4 celle) + CONTROLLO DEL RITARDO β€” per ogni politica condizionale, la stessa attesa MEDIA ma + DETERMINISTICA (versa k giorni dopo, k = attesa misurata di quella + politica, nessun segnale). Separa "il segnale informa" da "aspettare + costa". Non e' una cella candidata: e' un null. + +I NULL + (a) BOOTSTRAP IID (blocco=1) β€” distrugge la dipendenza seriale. Se una politica vince anche + li', sta sfruttando la meccanica del bootstrap, non il mercato: con ritorni indipendenti + il passato non informa il futuro e ogni residuo e' cassa/ritardo. + (b) P4 ANTI-DIP β€” se P1 e P4 danno lo stesso segno, si sta misurando rumore. + (c) DRIFT DIMEZZATO β€” lo stress standard del progetto sul libro (7,4 anni di storia, due tori). + +LA LENTE E' QUELLA UFFICIALE: L3 CONGIUNTA di r0822d_piano_vero (funding esatto DENTRO la serie ++ fisco d'accumulo coerente + ancora x0.89 misurata). Bersaglio = il muro di quella lente. + +CONVENZIONE DEL TRAGUARDO: si arriva quando **libro + cassa** >= muro. E' la convenzione +FAVOREVOLE alle politiche condizionali (la cassa che aspetta conta lo stesso), scelta apposta +perche' l'attesa a priori e' che perdano: se perdono anche cosi', il risultato e' piu' forte. +La convenzione severa (solo libro) e' riportata come sensibilita'. + + nice -n 19 uv run python scripts/research/r0823_deposit_timing.py +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT)) + +import r0725_capcurve as CC # noqa: E402 TAX_RATE, EURUSD, _boot_paths +import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI +import r0807_piano_netto as PN # noqa: E402 passo_fiscale, accumula, muro +import r0822d_piano_vero as PV # noqa: E402 costruisci_serie, deluck, leggi +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, # noqa: E402 + resample_1d) + +# --- costanti dichiarate --------------------------------------------------- +START = PV.START # $635, il conto Deribit vero +N_PATHS = 3_000 # la taglia della tabella pubblicata (seed 725) +BLOCK = PV.BLOCK # 20 giorni +ANNI = PV.ANNI_TRAJ # 25 (20 troncherebbe la coda) +SEED = PV.SEED_TRAJ # 725 +SEMI_MC = (725, 2026, 20260823) # per la risoluzione Monte Carlo +DEP_MAIN = 500.0 # il piano dichiarato dall'operatore (EUR/mese) +DEP_ALT = 250.0 # il livello di cui si discute da un mese +CASH_RATE = 0.0 # XEON ~0% reale netto: assunzione del progetto +DD_GRID = (0.05, 0.10, 0.15, 0.20) +VA_GRID = (0.00, 0.08, 0.15) +P_VENUE = (0.0, 0.01, 0.02) +ASSETS = ("BTC", "ETH") +T0 = time.time() + + +def sez(t: str) -> None: + print("\n" + "=" * 104) + print(f" {t}") + print("=" * 104) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +# =========================================================================== +# 0) LE SERIE β€” ritorni (lente L3) ed ESPOSIZIONE di TP01, sullo stesso indice +# =========================================================================== +def esposizione_tp01(alloc: float = 635.0) -> pd.Series: + """Esposizione GIA' TENUTA dal libro (0..2), causale, con la stessa regola di min-order di + `CC._tp01_realistic_cached`: un ribilanciamento sotto $5 non si esegue. + + Non e' una re-implementazione della strategia: `target_series` e' importata dal modulo di + produzione. Qui serve solo il livello di posizione, non i ritorni (quelli restano quelli + ufficiali di `CC.book_series`, gia' dentro la serie della lente). + """ + tp = TrendPortfolio(**CANONICAL) + cap_asset = alloc / len(ASSETS) + ser = {} + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) + held = np.empty(len(tgt)) + cur = 0.0 + for i in range(len(tgt)): + if abs(tgt[i] - cur) * cap_asset >= CC.DERIBIT_MIN_ORDER: + cur = tgt[i] + held[i] = cur + pos = np.zeros(len(held)) + pos[1:] = held[:-1] # causale: deciso a i-1, tenuto in i + ser[a] = pd.Series(pos, index=pd.to_datetime(df["datetime"])) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + out = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + if out.index.tz is None: + out.index = out.index.tz_localize("UTC") + return out + + +def boot_idx(n_src: int, n_paths: int, n_days: int, block: int, + rng: np.random.Generator) -> np.ndarray: + """Gli INDICI del block bootstrap di `CC._boot_paths`, per poter trasportare insieme ai + ritorni una seconda serie allineata (l'esposizione di TP01, che serve a P2). + + Controllo di identita' in sezione 1: `r[boot_idx(...)]` deve essere bit-exact + `CC._boot_paths(r, ...)` a parita' di generatore. + """ + n_blocks = int(np.ceil(n_days / block)) + starts = rng.integers(0, n_src - block, size=(n_paths, n_blocks)) + idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1) + return idx[:, :n_days].astype(np.int32) + + +# =========================================================================== +# IL MOTORE β€” accumulo con buffer di CASSA e politica di versamento +# =========================================================================== +def accumula_pol(paths: np.ndarray, flat: np.ndarray | None, dep_eur: float, + bersaglio: float, aliquota: float, patrimoniale: float, + policy, start: float = START, cash_rate: float = CASH_RATE, + fail: np.ndarray | None = None, + traguardo_include_cassa: bool = True) -> dict: + """Accumulo su path GIA' ESTRATTI, con una politica che decide QUANDO la cassa entra. + + Il flusso di cassa e' identico per ogni politica: `dep_eur` ogni 30 giorni sul CONTO + CORRENTE. La politica decide solo quanto trasferire nel libro, ogni giorno. + + Causalita': il ritorno del giorno t si applica PRIMA del trasferimento (come in + `PN.accumula`), quindi cio' che entra il giorno t rende dal giorno t+1; e la decisione usa + l'indice di equity fino alla chiusura di t, che a quel punto e' osservabile. + + `policy(t, cap, cash, dd, ru, flat_t)` ritorna l'importo da trasferire (USD, per path). + `fail`: maschera (path, giorno) di fallimento del venue β€” il libro va a zero, la CASSA no. + """ + n_paths, n_days = paths.shape + dep = dep_eur * CC.EURUSD + cash_d = (1.0 + cash_rate) ** (1.0 / 365.0) - 1.0 + cap = np.full(n_paths, start) + cash = np.zeros(n_paths) + eqi = np.ones(n_paths) # indice di equity del libro, NEUTRO ai versamenti + peak = np.ones(n_paths) + trough = np.ones(n_paths) + carry = np.zeros((n_paths, TX.CARRY_ANNI)) + anno_start = cap.copy() + flusso = np.zeros(n_paths) + colpito = np.full(n_paths, -1, int) + versato = start + cassa_int = 0.0 # integrale della cassa (per la legge di Little) + persi = np.zeros(n_paths, bool) + pnum = np.zeros(n_paths) # sum(importo * livello di equity all'ingresso) + pden = np.zeros(n_paths) # sum(importo) + for t in range(n_days): + rt = paths[:, t] + cap *= (1.0 + rt) + eqi *= (1.0 + rt) + np.maximum(peak, eqi, out=peak) + np.minimum(trough, eqi, out=trough) + if fail is not None: + f = fail[:, t] + if f.any(): + cap = np.where(f, 0.0, cap) + anno_start = np.where(f, 0.0, anno_start) + flusso = np.where(f, 0.0, flusso) # la perdita non diventa credito d'imposta + persi |= f + cash *= (1.0 + cash_d) + if t % 30 == 0 and t > 0 and dep > 0: + cash += dep + versato += dep + move = policy(t, cap, cash, 1.0 - eqi / peak, eqi / trough - 1.0, + None if flat is None else flat[:, t]) + if move is not None: + move = np.minimum(np.maximum(move, 0.0), cash) + cap += move + cash -= move + flusso += move + pnum += move * eqi # a che LIVELLO e' entrato ogni dollaro + pden += move + cassa_int += float(cash.mean()) + tot = cap + cash if traguardo_include_cassa else cap + nuovi = (colpito < 0) & (tot >= bersaglio) + colpito[nuovi] = t + if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0): + PN.passo_fiscale(cap, anno_start, flusso, carry, aliquota, patrimoniale) + anno_start = cap.copy() + flusso[:] = 0.0 + lam = dep / 30.0 if dep > 0 else np.nan # USD/giorno di flusso in entrata + attesa = (cassa_int / n_days) / lam if dep > 0 else 0.0 # Little: W = L / lambda + prezzo = np.where(pden > 0, pnum / np.maximum(pden, 1e-12), np.nan) + return dict(cap=cap, cash=cash, colpito=colpito, versato=versato, + attesa_g=float(attesa), p_perso=float(persi.mean()), + prezzo=float(np.nanmedian(prezzo)), + non_versata=float(np.median(cash))) + + +# =========================================================================== +# LE POLITICHE +# =========================================================================== +def pol_piatto(): + return lambda t, cap, cash, dd, ru, fl: cash + + +def pol_dip(th: float): + return lambda t, cap, cash, dd, ru, fl: np.where(dd >= th, cash, 0.0) + + +def pol_antidip(th: float): + return lambda t, cap, cash, dd, ru, fl: np.where(ru >= th, cash, 0.0) + + +def pol_riskoff(): + return lambda t, cap, cash, dd, ru, fl: np.where(fl, cash, 0.0) + + +def pol_va(g: float, dep_eur: float, start: float = START): + """Valore-medio: traiettoria bersaglio deterministica, versa la differenza, mai vende.""" + dep = dep_eur * CC.EURUSD + gm = (1.0 + g) ** (30.0 / 365.0) - 1.0 + state = {"T": start} + + def f(t, cap, cash, dd, ru, fl): + if t % 30 or t == 0: + return None + state["T"] = state["T"] * (1.0 + gm) + dep + return np.maximum(state["T"] - cap, 0.0) + return f + + +def pol_lag(k: int, dep_eur: float): + """Il controllo del ritardo: versa esattamente k giorni dopo l'arrivo, senza guardare nulla.""" + dep = dep_eur * CC.EURUSD + + def f(t, cap, cash, dd, ru, fl): + return np.full(len(cap), dep) if (t > k and (t - k) % 30 == 0) else None + return f + + +def celle(dep_eur: float) -> dict: + """Le 12 celle candidate, dichiarate qui e in nessun altro posto.""" + C = {"P0 piatto": pol_piatto()} + for th in DD_GRID: + C[f"P1 dip {th:.0%}"] = pol_dip(th) + C["P2 risk-off"] = pol_riskoff() + for g in VA_GRID: + C[f"P3 VA g={g:.0%}"] = pol_va(g, dep_eur) + for th in DD_GRID: + C[f"P4 anti {th:.0%}"] = pol_antidip(th) + return C + + +N_TRIAL = 12 # P1(4) + P2(1) + P3(3) + P4(4). P0 e' l'incumbent, i lag sono null. + + +# =========================================================================== +def leggi(colpito: np.ndarray) -> dict: + """Mediana INCONDIZIONATA (non-arrivo = +inf, MAI -1) + condizionata + P per orizzonte.""" + y = np.where(colpito >= 0, colpito / 365.0, np.inf) + a = y[np.isfinite(y)] + return dict(med=float(np.median(y)), + med_cond=float(np.median(a)) if len(a) > 100 else float("nan"), + p10=float((y <= 10).mean()), p15=float((y <= 15).mean()), + p20=float((y <= 20).mean())) + + +def fmt(d: dict) -> str: + m = f">{ANNI}a" if not np.isfinite(d["med"]) else f"{d['med']:.1f}a" + c = "β€”" if np.isnan(d["med_cond"]) else f"{d['med_cond']:.1f}a" + return f"{m:>7}{c:>8}" + + +def riga(nome: str, res: dict, perp: float, base: dict | None = None, + prezzo0: float | None = None) -> str: + d = leggi(res["colpito"]) + tot = res["cap"] + res["cash"] + rend = float(np.median(tot * perp / 365.0 / CC.EURUSD)) + dp = "" if base is None else f"{d['p20'] - base['p20']:+7.1%}" + pz = "" if not prezzo0 else f"{res['prezzo'] / prezzo0:.3f}" + return (f" {nome:<16}{fmt(d)}{d['p15']:>8.1%}{d['p20']:>8.1%}" + f"{dp:>9}{rend:>10.2f}{res['attesa_g']:>8.0f}{pz:>8}") + + +HDR = (f" {'politica':<16}{'med':>7}{'m|arr':>8}{'P(15a)':>8}{'P(20a)':>8}" + f"{'dP20':>9}{'EUR/g 20a':>10}{'attesa':>8}{'prezzo':>8}") + + +def tabella(paths, flat, dep, muro, perp, titolo, **kw) -> dict: + print(f"\n {titolo}") + print(HDR) + out = {} + base = None + p0 = None + for nome, pol in celle(dep).items(): + r = accumula_pol(paths, flat, dep, muro, CC.TAX_RATE, TX.PATRIMONIALE, pol, **kw) + d = leggi(r["colpito"]) + if base is None: + base, p0 = d, r["prezzo"] + out[nome] = r + print(riga(nome, r, perp, None if nome.startswith("P0") else base, + None if nome.startswith("P0") else p0)) + return out + + +# =========================================================================== +def main() -> None: + print("=" * 104) + print(" r0823 Β§44 DEPOSIT-TIMING β€” il timing dei BONIFICI paga? (lente L3: funding + fisco)") + print("=" * 104) + + # ------------------------------------------------------------------ 0 + B = PV.costruisci_serie() + idx = pd.DatetimeIndex(B.index) + r = PV.deluck(B["fund"].values.astype(float)) # lente L3 CONGIUNTA, ancora x0.89 + E = esposizione_tp01().reindex(idx).ffill().fillna(0.0) + flat_src = (E.values <= 1e-9) + print(f"\n serie L3 CONGIUNTA (libro live 75/25, funding esatto, ancora x{PV.DELUCK}) β€” " + f"{len(r)} giorni {idx.min().date()}..{idx.max().date()}") + print(f" drift {r.mean() * 365:.2%} Β· vol {r.std() * 365 ** 0.5:.2%} Β· " + f"esposizione TP01 media {E.mean():.3f}x Β· giorni a 0x = {flat_src.mean():.1%} {clock()}") + + # ------------------------------------------------------------------ 1 + sez("1) CONTROLLO DI REPLICA β€” prima di ogni delta, riprodurre i numeri gia' pubblicati") + ok = {} + netto = CC.TARGET_EUR_DAY * 365 * CC.EURUSD + perp, muro = PN.muro(r, netto, CC.TAX_RATE, TX.PATRIMONIALE) + ok["R1"] = abs(muro - 313_000.0) < 1_500.0 + print(f"\n R1 muro L3 congiunta: perpetua {perp:.2%} Β· ${muro:,.0f}" + f"{'':>10}pubblicato $313k {'OK' if ok['R1'] else 'DIVERGE'} {clock()}") + + rng = np.random.default_rng(SEED) + ii = boot_idx(len(r), N_PATHS, ANNI * 365, BLOCK, rng) + paths = r[ii] + flat = flat_src[ii] + rng2 = np.random.default_rng(SEED) + ok["R2"] = bool(np.array_equal(paths, CC._boot_paths(r, N_PATHS, ANNI * 365, BLOCK, rng2))) + print(f" R2 `boot_idx` trasporta gli STESSI path di `CC._boot_paths` (per portarsi dietro") + print(f" l'esposizione di TP01, che serve a P2):{'':>19}" + f"{'BIT-EXACT' if ok['R2'] else 'DIVERGE'}") + + print(f"\n R3 la riga pubblicata della tabella PIANO-VERO (L3, da ${START:.0f}):") + att = {250.0: (23.4, 22.1, 0.14), 500.0: (17.3, 17.3, 0.85)} + r3 = True + print(f" {'EUR/m':>7}{'med.inc':>10}{'atteso':>9}{'med|arr':>10}{'atteso':>9}" + f"{'P(20a)':>9}{'atteso':>9}") + for dep, (a1, a2, a3) in att.items(): + d = leggi(PN.accumula(paths, dep, muro, CC.TAX_RATE, TX.PATRIMONIALE, + start=START)["colpito"]) + r3 &= abs(d["med"] - a1) < 0.2 and abs(d["p20"] - a3) < 0.02 + print(f" {dep:>7.0f}{d['med']:>10.1f}{a1:>9.1f}{d['med_cond']:>10.1f}{a2:>9.1f}" + f"{d['p20']:>9.1%}{a3:>9.0%}") + ok["R3"] = r3 + + # R4: P0 del mio motore == PN.accumula, bit-exact + A = PN.accumula(paths, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, start=START) + Bp = accumula_pol(paths, flat, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + pol_piatto(), start=START) + dcap = float(np.max(np.abs(A["cap"] - Bp["cap"]))) + dcol = int(np.max(np.abs(A["colpito"] - Bp["colpito"]))) + ok["R4"] = dcap == 0.0 and dcol == 0 and abs(A["versato"] - Bp["versato"]) < 1e-9 + print(f"\n R4 P0 di QUESTO motore (con buffer di cassa) vs `PN.accumula` importata:") + print(f" max|dif| capitale = {dcap:.3e} Β· max|dif| giorno d'arrivo = {dcol} Β· " + f"versato identico{'':>3}{'BIT-EXACT' if ok['R4'] else 'DIVERGE'}") + print(f" -> il buffer non introduce nulla: con versamento immediato il motore E' quello pubblicato.") + + print(f"\n ESITO REPLICA: {sum(ok.values())}/{len(ok)} β€” " + + ", ".join(f"{k}={'OK' if v else 'NO'}" for k, v in ok.items())) + if not all(ok.values()): + raise SystemExit("REPLICA INCOMPLETA: un delta sarebbe indistinguibile da un bug.") + + # ------------------------------------------------------------------ 2 + sez("2) RISOLUZIONE MONTE CARLO β€” quanto vale la cifra che si pubblica") + print(f"\n {N_PATHS} percorsi, blocchi da {BLOCK} giorni, {ANNI} anni. I confronti fra") + print(" politiche sono APPAIATI (stessi percorsi) quindi la loro varianza e' molto minore") + print(" di quella dei livelli. Misurata su 3 semi:") + mc = [] + for s in SEMI_MC: + rg = np.random.default_rng(s) + i2 = boot_idx(len(r), N_PATHS, ANNI * 365, BLOCK, rg) + p2, f2 = r[i2], flat_src[i2] + b = leggi(accumula_pol(p2, f2, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + pol_piatto(), start=START)["colpito"]) + d = leggi(accumula_pol(p2, f2, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + pol_dip(0.10), start=START)["colpito"]) + mc.append((s, b["p20"], d["p20"], d["p20"] - b["p20"])) + del p2, f2, i2 + print(f"\n {'seme':>10}{'P0 P(20a)':>12}{'P1 dip 10% P(20a)':>20}{'differenza APPAIATA':>22}") + for s, a, bb, dd in mc: + print(f" {s:>10}{a:>12.1%}{bb:>20.1%}{dd:>22.2%}") + liv = max(x[1] for x in mc) - min(x[1] for x in mc) + dif = max(x[3] for x in mc) - min(x[3] for x in mc) + print(f"\n ampiezza fra semi: LIVELLO {liv:.2%} Β· DIFFERENZA APPAIATA {dif:.2%}") + print(f" -> si cita la differenza appaiata al punto percentuale; un livello si cita a " + f"{liv:.1%} di rumore. {clock()}") + + # ------------------------------------------------------------------ 3 + sez(f"3) LA TABELLA β€” 12 celle candidate contro il piatto, stesso flusso di cassa") + print(f""" + Lettura delle colonne. `med` = mediana INCONDIZIONATA degli anni al capitale-rendita + (non-arrivo = +inf, mai -1); `m|arr` = mediana condizionata all'arrivo, che NON si legge mai + senza la sua P accanto; `dP20` = differenza APPAIATA di P(20a) contro P0 sugli stessi + percorsi; `attesa` = giorni medi che un euro passa in cassa prima di entrare nel libro + (legge di Little, L/lambda); `prezzo` = livello di equity medio PONDERATO PER IMPORTO a cui + i soldi sono entrati, in rapporto a quello di P0 β€” sotto 1.000 la politica ha comprato + davvero piu' in basso. Bersaglio ${muro:,.0f}; traguardo su libro+cassa.""") + R = {} + for dep in (DEP_MAIN, DEP_ALT): + R[dep] = tabella(paths, flat, dep, muro, perp, + f"EUR {dep:.0f}/mese di flusso disponibile {clock()}", start=START) + + # ------------------------------------------------------------------ 4 + sez("4) IL CONTROLLO DEL RITARDO β€” Β«il segnale informaΒ» contro Β«aspettare costaΒ»") + print(""" + Ogni politica condizionale fa DUE cose: sceglie il momento (informazione) e ritarda i soldi + (costo). Il controllo isola la seconda: stessa attesa MEDIA, ma deterministica, nessun + segnale. Se la politica condizionale non batte il proprio ritardo-gemello, non ha + informazione β€” ha solo aspettato.""") + info = {} + for etichetta, (P, FL, RR) in {"BLOCCHI 20g (mercato vero)": (paths, flat, R[DEP_MAIN]), + "IID blocco=1 (null)": (None, None, None)}.items(): + if P is None: # il null iid: si ricostruiscono i path e le celle + rg = np.random.default_rng(SEED) + i2 = boot_idx(len(r), N_PATHS, ANNI * 365, 1, rg) + P, FL = r[i2], flat_src[i2] + del i2 + RR = {n: accumula_pol(P, FL, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + pol, start=START) for n, pol in celle(DEP_MAIN).items()} + print(f"\n {etichetta}") + print(f" {'politica':<16}{'attesa g.':>11}{'P(20a) reale':>14}" + f"{'P(20a) ritardo-gemello':>25}{'informazione':>15}") + col = {} + for nome in ("P1 dip 5%", "P1 dip 10%", "P1 dip 15%", "P1 dip 20%", "P2 risk-off"): + rr = RR[nome] + k = int(round(rr["attesa_g"])) + g = accumula_pol(P, FL, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + pol_lag(k, DEP_MAIN), start=START) + a, b = leggi(rr["colpito"])["p20"], leggi(g["colpito"])["p20"] + col[nome] = a - b + print(f" {nome:<16}{k:>11}{a:>14.1%}{b:>25.1%}{a - b:>15.2%}") + info[etichetta] = col + if "IID" in etichetta: + del P, FL, RR + k1 = "BLOCCHI 20g (mercato vero)" + k2 = "IID blocco=1 (null)" + cost15 = (leggi(R[DEP_MAIN]["P1 dip 15%"]["colpito"])["p20"] + - leggi(R[DEP_MAIN]["P0 piatto"]["colpito"])["p20"]) + print(f""" + ⚠️ IL PRIMO SGUARDO DICE Β«IL SEGNALE INFORMAΒ» β€” E IL NULL IID LO REFUTA. Sulle celle profonde + la colonna `informazione` e' positiva sul mercato vero ({info[k1]['P1 dip 15%']:+.1%} a dip 15%, + {info[k1]['P1 dip 20%']:+.1%} a dip 20%): a pari attesa MEDIA, aspettare il calo batte aspettare a + caso, e leggendo solo questa riga si concluderebbe che il buy-the-dip contiene previsione. + Ma sotto IID, dove per COSTRUZIONE il passato non dice nulla del futuro, la stessa colonna + vale {info[k2]['P1 dip 15%']:+.1%} / {info[k2]['P1 dip 20%']:+.1%} β€” cioe' NON PIU' PICCOLA, anzi piu' grande. + -> il divario contro il ritardo-gemello e' MECCANICO, non previsione: la politica + condizionale ha attesa VARIABILE (spesso zero, ogni tanto lunghissima) mentre il gemello ha + attesa FISSA, e versa il buffer tutto in una volta. Un'attesa variabile con la stessa media + di una fissa non produce lo stesso capitale (Jensen), e il null lo mostra senza ambiguita'. + ⚠️ Il gemello non e' un controllo perfetto proprio per questo: e' il controllo del RITARDO + MEDIO, non della sua distribuzione. Il suo compito qui e' fissare l'ordine di grandezza β€” + il ritardo di dip 15% costa {abs(cost15):.0%} di P(20a) contro un divario col gemello di + {info[k1]['P1 dip 15%']:+.1%}, e sarebbe la conclusione anche prendendo il gemello alla lettera.""") + print(f" {clock()}") + + # ------------------------------------------------------------------ 5 + sez("5) I NULL") + print(""" + (a) BOOTSTRAP IID (blocco = 1 giorno). Distrugge ogni dipendenza seriale: il passato non + informa il futuro per costruzione. Una politica che li' mantiene il suo vantaggio lo + stava prendendo dalla meccanica del bootstrap, non dal mercato. (Il controllo del + ritardo su questo stesso null e' gia' in sezione 4.)""") + rg = np.random.default_rng(SEED) + i_iid = boot_idx(len(r), N_PATHS, ANNI * 365, 1, rg) + p_iid, f_iid = r[i_iid], flat_src[i_iid] + del i_iid + I = tabella(p_iid, f_iid, DEP_MAIN, muro, perp, f"IID (blocco=1) {clock()}", start=START) + del p_iid, f_iid + + print(""" + (c) DRIFT DIMEZZATO β€” lo stress standard del progetto (7,4 anni di storia, due tori dentro). + Bersaglio tenuto FISSO al muro della lente piena: si stressa l'accumulo, non l'obiettivo + (col muro ricalcolato non arriverebbe nessuno e il confronto perderebbe potenza).""") + r_half = r - 0.5 * r.mean() + rg = np.random.default_rng(SEED) + i_h = boot_idx(len(r_half), N_PATHS, ANNI * 365, BLOCK, rg) + p_h, f_h = r_half[i_h], flat_src[i_h] + del i_h + H = tabella(p_h, f_h, DEP_MAIN, muro, perp, + f"DRIFT DIMEZZATO ({r_half.mean() * 365:.2%}/anno) {clock()}", start=START) + half20 = (leggi(H["P1 dip 20%"]["colpito"])["p20"] + - leggi(H["P0 piatto"]["colpito"])["p20"]) + del p_h, f_h + + # ------------------------------------------------------------------ 6 + sez("6) RISCHIO DI VENUE β€” una politica che tiene i soldi FUORI e' anche una politica di rischio") + print(""" + Il fatto strutturale: la cassa che aspetta non e' sull'exchange. Se il vantaggio (o il minor + danno) di una politica condizionale venisse da li', sarebbe un risultato sul RISCHIO, non sul + timing. I due effetti si separano leggendo la stessa coppia a p=0 (solo timing) e a p>0. + Convenzione: al fallimento il LIBRO va a zero, la CASSA sopravvive, e si riparte (conto nuovo, + versamenti che continuano) β€” dichiarata, e diversa da quella di r0726_venue_risk, che nel caso + a un venue solo scartava anche i versamenti futuri. `p` NON e' stimato: e' sensibilita'.""") + print(f"\n {'p annua':>9} {'politica':<16}{'P(20a)':>9}{'dP20 vs P0':>13}" + f"{'P(libro azzerato)':>20}{'EUR/g a 20a':>13}{'effetto rischio':>17}") + d0 = {} + for p in P_VENUE: + h = 1.0 - (1.0 - p) ** (1.0 / 365.0) if p > 0 else 0.0 + F = None + if p > 0: + rgv = np.random.default_rng(726) + F = np.zeros((N_PATHS, ANNI * 365), bool) + tt = rgv.geometric(h, size=N_PATHS) + for _ in range(8): + m = tt < ANNI * 365 + if not m.any(): + break + F[np.where(m)[0], tt[m]] = True + tt = tt + rgv.geometric(h, size=N_PATHS) + base = None + for nome in ("P0 piatto", "P1 dip 10%", "P1 dip 20%", "P4 anti 10%"): + rr = accumula_pol(paths, flat, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + celle(DEP_MAIN)[nome], start=START, fail=F) + d = leggi(rr["colpito"]) + if base is None: + base = d["p20"] + rend = float(np.median((rr["cap"] + rr["cash"]) * perp / 365.0 / CC.EURUSD)) + dd = d["p20"] - base + if p == 0.0: + d0[nome] = dd + eff = "" if p == 0.0 else f"{dd - d0[nome]:+.2%}" + print(f" {f'{p:.1%}' if nome.startswith('P0') else '':>9} {nome:<16}" + f"{d['p20']:>9.1%}{dd:>13.2%}{rr['p_perso']:>20.1%}{rend:>13.2f}{eff:>17}") + del F + print(f""" + `effetto rischio` = di quanto il rischio di venue RIDUCE la penalita' della politica + condizionale rispetto a p=0. E' il pezzo di risultato che NON e' timing: la cassa in attesa + non e' sull'exchange, quindi non brucia. Il meccanismo esiste ed e' misurabile, ma vale + qualche punto percentuale contro una penalita' di timing di decine β€” e la colonna + `P(libro azzerato)` e' IDENTICA per tutte le politiche (il conto salta comunque: cambia + quanto c'e' sopra, non se salta). Chi volesse comprare quella protezione la compra con un + SECONDO CONTO, non ritardando i bonifici (25/07 Β§4: Β«un rischio non-diversificabile si compra + con un secondo CONTO, non con un secondo sleeveΒ»).""") + print(f" {clock()}") + + # ------------------------------------------------------------------ 7 + sez("7) SENSIBILITA' β€” le due convenzioni che potrebbero decidere al posto della misura") + print(f"\n (i) CASSA a 0% (assunzione XEON) contro cassa al 2%/anno nominale:") + print(f" {'politica':<16}{'P(20a) cassa 0%':>18}{'P(20a) cassa 2%':>18}{'delta':>10}") + for nome in ("P0 piatto", "P1 dip 10%", "P1 dip 20%", "P2 risk-off", "P3 VA g=0%"): + a = leggi(R[DEP_MAIN][nome]["colpito"])["p20"] + b = leggi(accumula_pol(paths, flat, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + celle(DEP_MAIN)[nome], start=START, + cash_rate=0.02)["colpito"])["p20"] + print(f" {nome:<16}{a:>18.1%}{b:>18.1%}{b - a:>10.2%}") + print(f"\n (ii) TRAGUARDO su libro+cassa (favorevole alle condizionali, usato sopra) contro") + print(f" traguardo su SOLO LIBRO (severo: la cassa a 0% non produce rendita):") + print(f" {'politica':<16}{'P(20a) libro+cassa':>21}{'P(20a) solo libro':>20}{'delta':>10}") + for nome in ("P0 piatto", "P1 dip 10%", "P1 dip 20%", "P2 risk-off", "P3 VA g=0%"): + a = leggi(R[DEP_MAIN][nome]["colpito"])["p20"] + b = leggi(accumula_pol(paths, flat, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, + celle(DEP_MAIN)[nome], start=START, + traguardo_include_cassa=False)["colpito"])["p20"] + print(f" {nome:<16}{a:>21.1%}{b:>20.1%}{b - a:>10.2%}") + print(f" {clock()}") + + # ------------------------------------------------------------------ 8 + sez("VERDETTO") + P0 = R[DEP_MAIN]["P0 piatto"] + base = leggi(P0["colpito"]) + tab = {n: leggi(v["colpito"]) for n, v in R[DEP_MAIN].items() if n != "P0 piatto"} + best = max(tab, key=lambda k: tab[k]["p20"]) + vere = {n: v for n, v in tab.items() if R[DEP_MAIN][n]["attesa_g"] >= 30.0} + bvera = max(vere, key=lambda k: vere[k]["p20"]) + sopra = sum(1 for n in tab if tab[n]["p20"] > base["p20"]) + dips = [tab[n]["p20"] - base["p20"] for n in tab if n.startswith("P1")] + anti = [tab[n]["p20"] - base["p20"] for n in tab if n.startswith("P4")] + pz = {n: R[DEP_MAIN][n]["prezzo"] / P0["prezzo"] for n in tab} + + def conta(D: dict) -> str: + b = leggi(D["P0 piatto"]["colpito"])["p20"] + dd = {n: leggi(v["colpito"])["p20"] - b for n, v in D.items() if n != "P0 piatto"} + k = max(dd, key=lambda x: dd[x]) + return f"{sum(1 for x in dd.values() if x > 0)}/12 ({k} {dd[k]:+.2%})" + conte = " Β· ".join(f"{lab} {conta(D)}" for lab, D in + (("EUR500", R[DEP_MAIN]), ("EUR250", R[DEP_ALT]), + ("IID", I), ("drift/2", H))) + d250 = leggi(R[DEP_ALT]["P0 piatto"]["colpito"]) + print(f""" + 1. NESSUNA delle {N_TRIAL} celle candidate batte il versamento piatto: {sopra}/{N_TRIAL} sopra + P0, migliore in assoluto {best} a {tab[best]['p20'] - base['p20']:+.2%} appaiato β€” dentro la + risoluzione Monte Carlo della differenza appaiata ({dif:.2%}) e su una cella che aspetta + {R[DEP_MAIN][best]['attesa_g']:.0f} giorno, cioe' e' P0 travestito. La migliore cella DAVVERO + condizionale (attesa >= 30 giorni) e' {bvera}: {vere[bvera]['p20'] - base['p20']:+.1%}. + Segno medio: P1 buy-the-dip {np.mean(dips):+.1%} Β· P4 anti-dip {np.mean(anti):+.1%}. + Per lente (celle STRETTAMENTE sopra P0 su 12, e la migliore): {conte}. + Nessuna lente produce una cella sopra il rumore MC ({dif:.1%}) -> non c'e' niente da + deflazionare: il conteggio dei trial non e' il vincolo, il vincolo e' che il segno e' + negativo e monotono nella profondita' della condizione. + + 2. IL SEGNALE FUNZIONA E PERDE LO STESSO, ed e' la riga che spiega tutto. Colonna `prezzo`: + P1 dip 15% compra al {pz['P1 dip 15%']:.3f} del livello medio di P0 e P1 dip 20% al + {pz['P1 dip 20%']:.3f} β€” cioe' {1 - pz['P1 dip 20%']:.0%} PIU' IN BASSO, esattamente cio' che la + politica promette. E arriva al capitale-rendita nel {tab['P1 dip 20%']['p20']:.1%} dei casi contro + {base['p20']:.1%}. Comprare meglio e comprare TARDI sono la stessa mossa, e la seconda meta' + domina di un ordine di grandezza. + ⚠️ E il dettaglio che ribalta l'intuizione sulle soglie BASSE: P1 dip 5% compra a + {pz['P1 dip 5%']:.3f}, cioe' PIU' IN ALTO di P0. Su una serie che sale, una condizione poco + profonda non compra il calo: ritarda dentro la salita. Solo le soglie che fanno male + comprano davvero piu' in basso β€” ed e' esattamente il motivo per cui non conviene. + + 3. P4 ANTI-DIP, il controllo speculare, NON e' simmetrico β€” e la sua asimmetria e' la misura + del meccanismo: su una serie che sale, un rialzo del 20% dal minimo arriva in + {R[DEP_MAIN]['P4 anti 20%']['attesa_g']:.0f} giorni mentre un ribasso del 20% dal massimo ne chiede + {R[DEP_MAIN]['P1 dip 20%']['attesa_g']:.0f}. Il costo non e' Β«scommettere sul lato sbagliatoΒ»: e' + Β«mettere una condizione rara davanti a un bonificoΒ». Qualunque condizione rara, in + qualunque direzione, costa. P1 e P4 NON danno lo stesso segno di taglia: se lo dessero + staremmo misurando rumore. + + 4. ORDINE DI GRANDEZZA, che e' il modo in cui questo risultato va usato: la migliore politica + di timing vale {tab[bvera]['p20'] - base['p20']:+.1%} di P(20a) β€” indistinguibile da zero + sulla risoluzione MC di {dif:.1%}; passare da EUR {DEP_ALT:.0f} a EUR {DEP_MAIN:.0f} al mese vale + {base['p20'] - d250['p20']:+.0%} ({d250['p20']:.0%} -> {base['p20']:.0%}). Non c'e' rapporto da + calcolare fra un effetto misurato e uno che non si distingue dal rumore: la prima colonna + e' una decisione, la seconda no. + + 5. ATTESA A PRIORI (dichiarata prima di misurare): Β«non funziona, perche' entrare piu' tardi + domina il comprare piu' in bassoΒ». CONFERMATA, e col meccanismo misurato invece che + assunto. Cio' che l'attesa NON prevedeva e' il punto 2: il segnale compra davvero molto + piu' in basso β€” {1 - pz['P1 dip 20%']:.0%} β€” e perde comunque. Sapere PERCHE' non serve vale piu' + del verdetto: la domanda non tornera' sotto forma di Β«e se aspettassi un calo piu' + profondo?Β», perche' e' proprio la profondita' a peggiorare il risultato, in modo monotono + su tutte e 4 le soglie e su tutte e 4 le lenti. + + 6. DUE ERRORI CATTURATI SU ME STESSO, entrambi da un controllo e non a occhio. + (i) La colonna `informazione` di sezione 4 sul solo mercato vero mi avrebbe fatto scrivere + Β«il buy-the-dip contiene previsioneΒ»: il null IID la rende PIU' GRANDE su ritorni dove non + c'e' niente da prevedere -> e' meccanica (attesa variabile contro fissa). Una colonna + positiva non e' un'evidenza finche' non gira anche sul null. + (ii) Senza la colonna `attesa` avrei letto P4 anti-dip come Β«stesso segno di P1 => rumoreΒ» + (regola dichiarata nel mandato). Falso: P4 e' piatto perche' in un mercato che sale la sua + condizione e' quasi sempre gia' vera ({R[DEP_MAIN]['P4 anti 20%']['attesa_g']:.0f} giorni di attesa contro + {R[DEP_MAIN]['P1 dip 20%']['attesa_g']:.0f}), cioe' e' P0 travestito, non uno specchio informativo. + Lo specchio va letto sulla colonna dell'ATTESA, non su quella del segno. + + COSA QUESTO NON MISURA: politiche che cambiano il FLUSSO disponibile (versare di piu' quando + si guadagna di piu' e' un'altra domanda, ed e' fuori dal controllo del libro); orizzonti + diversi da 25 anni; il caso in cui la cassa in attesa renda davvero qualcosa (sez. 7 (i): + al 2% nominale la penalita' si riduce di ~4pp e non cambia segno); e il fatto che la finestra + storica del libro sono 7,4 anni con due tori β€” su un campione con un orso lungo il costo del + ritardo sarebbe minore (lo si vede nella colonna DRIFT DIMEZZATO: la penalita' scende da + {tab['P1 dip 20%']['p20'] - base['p20']:+.0%} a {half20:+.0%}, ma resta negativa a tutte e 12 le celle). + """) + print(f" fine {clock()}") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_newdata_scout.py b/scripts/research/r0823_newdata_scout.py new file mode 100644 index 0000000..ee19304 --- /dev/null +++ b/scripts/research/r0823_newdata_scout.py @@ -0,0 +1,599 @@ +"""NEWDATA-SCOUT (filone Β§51) β€” cosa iniziare a raccogliere OGGI, se la scorta e' esaurita. + +CONTESTO. Il filone Β§43 (`r0823_data_unused.py`) ha censito 647 file e concluso che la scorta di +dati del progetto e' ESAURITA: ogni dataset con storia vera e' gia' stato analizzato almeno una +volta, e cio' che resta non letto e' telemetria a 30-113 giorni. Quindi la domanda utile cambia: +non piu' "cosa abbiamo e non guardiamo" ma "cosa NON abbiamo, che varrebbe la pena iniziare a +raccogliere OGGI perche' fra 12-24 mesi apra una famiglia che oggi non e' apribile?". + +Il progetto ha gia' pagato due volte la lezione: la catena opzioni non e' ricostruibile a +posteriori (Deribit non serve book storici; l'ora non raccolta e' persa per sempre) -> esiste +`scripts/live/collect_chain.py`; e le serie `paper_*` sono forward-only irrecuperabili. + +IL CRITERIO CHE FA IL LAVORO E' IL PRIMO, E VA **PROVATO**, NON DEDOTTO. + 1. RICOSTRUIBILITA' β€” se un dato si potra' scaricare storico quando servira', raccoglierlo oggi + e' PURO COSTO. Qui ogni riga della colonna e' il risultato di una GET pubblica di sondaggio, + con **controllo positivo** (una finestra in cui il dato DEVE esserci) prima di dichiarare un + muro: uno zero puo' voler dire "non c'e'" oppure "sto chiamando male", e le due cose si + distinguono solo provando che la stessa chiamata funziona altrove. + 2. FAMIGLIA β€” una fonte che alimenta solo una famiglia gia' chiusa con misure non vale niente. + 3. COSTO in chiamate, contro il budget IP esistente (il rate limit Deribit e' PER-IP e questo + progetto l'ha gia' saturato una volta, guasto di 8 ore il 29/07). + 4. RISCHIO OPERATIVO β€” ogni raccolta e' un modo nuovo di rompersi in silenzio. + 5. TEMPO PERCHE' SIA UTILE, dal MDE: SE(Sharpe) ~ 1/sqrt(anni), MDE ~ 1,96/sqrt(anni) + (stessa convenzione di Β§43: 1,40 anni -> MDE 1,66). Se la risposta e' "6 anni", non e' una + decisione di oggi. + +NON PROPONE DI IMPLEMENTARE NULLA: produce materiale per una decisione dell'operatore. + + uv run python scripts/research/r0823_newdata_scout.py + uv run python scripts/research/r0823_newdata_scout.py --no-net # solo tabella + MDE +""" +from __future__ import annotations + +import argparse +import json +import sys +import time +import urllib.error +import urllib.request +from dataclasses import dataclass, field +from datetime import UTC, datetime, timedelta +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] + +# --- disciplina di rete ------------------------------------------------------------------- +# Il rate limit Deribit e' PER-IP e questa VPS lo condivide. I minuti proibiti non sono cortesia: +# :05-:10 cron_book.sh (feed 5m di SKH01 -> esecuzione del libro con soldi veri) +# :24-:30 cron_chain.sh (collettore catena, 653 chiamate a 4/s per ~163 s) +FORBIDDEN_MINUTES = ((5, 10), (24, 30)) +MAX_RPS = 2.0 +TIMEOUT = 20 + + +class Net: + """GET pubbliche e tokenless, <=2/s, fuori dalle finestre dei cron di produzione.""" + + def __init__(self, enabled: bool = True) -> None: + self.enabled = enabled + self._next = 0.0 + self.calls = 0 + self.blocked = 0 + + @staticmethod + def in_forbidden_window() -> bool: + m = datetime.now(UTC).minute + return any(a <= m < b for a, b in FORBIDDEN_MINUTES) + + def get(self, url: str) -> dict | list | None: + if not self.enabled: + return None + while self.in_forbidden_window(): + self.blocked += 1 + time.sleep(20) + now = time.monotonic() + if now < self._next: + time.sleep(self._next - now) + self._next = max(now, self._next) + 1.0 / MAX_RPS + self.calls += 1 + req = urllib.request.Request(url, headers={"User-Agent": "PythagorasGoal-scout/1.0"}) + try: + with urllib.request.urlopen(req, timeout=TIMEOUT) as r: + return json.loads(r.read()) + except urllib.error.HTTPError as e: + # UN RIFIUTO NON E' UN GUASTO. Binance risponde 400 + corpo JSON quando startTime + # e' oltre la ritenzione: se lo si ingoia come errore di rete, un MURO diventa + # indistinguibile da "nessun muro trovato" e la colonna ricostruibilita' si + # ribalta in silenzio. Stessa lezione di `quote_status` (error vs no_quote). + try: + body = json.loads(e.read()) + if isinstance(body, dict): + body["_http_status"] = e.code + return body + except (ValueError, OSError): + return {"_http_status": e.code, "_unparsed": True} + except (urllib.error.URLError, TimeoutError, ValueError, OSError): + return None + + +DERIBIT = "https://www.deribit.com/api/v2/public" +MS = 1000 + + +def ms_ago(**kw) -> int: + return int((datetime.now(UTC) - timedelta(**kw)).timestamp() * MS) + + +# --- esito di una prova di ricostruibilita' ----------------------------------------------- +@dataclass +class Prova: + """Una prova di ricostruibilita'. `positivo` e' il controllo che rende leggibile lo zero.""" + fonte: str + verdetto: str = "NON PROVATO" + dettaglio: str = "" + positivo: str = "" + misurato: bool = False + extra_tot: int = 0 + righe: list[str] = field(default_factory=list) + + +# ------------------------------------------------------------------------------------------ +# PROVE +# ------------------------------------------------------------------------------------------ +def prova_tape(net: Net) -> Prova: + """Trade tape Deribit (direzione aggressore + flag liquidation). + + `get_last_trades_by_instrument_and_time` accetta qualunque finestra passata SENZA errore: + fuori ritenzione risponde `trades: []`. Uno zero non e' quindi una risposta -> serve il + controllo positivo (una finestra recente, dove i trade DEVONO esserci). + """ + p = Prova("tape / flusso aggressore Deribit") + inst = "BTC-PERPETUAL" + ore = (1, 6, 12, 24, 26, 30, 36, 48, 168) + trovato: dict[int, int] = {} + for h in ore: + st = ms_ago(hours=h) + d = net.get(f"{DERIBIT}/get_last_trades_by_instrument_and_time" + f"?instrument_name={inst}&start_timestamp={st}&end_timestamp={st + 300 * MS}" + f"&count=100&sorting=asc") + if d is None or "result" not in d: + return p + trovato[h] = len(d["result"]["trades"]) + p.misurato = True + vivi = [h for h, n in trovato.items() if n > 0] + morti = [h for h, n in trovato.items() if n == 0] + if not vivi: + p.verdetto = "PROVA NON VALIDA" + p.dettaglio = "nessuna finestra restituisce trade: la chiamata e' rotta, non il dato assente" + return p + ultimo_vivo, primo_morto = max(vivi), (min(morti) if morti else None) + p.positivo = f"a -{ultimo_vivo}h la stessa chiamata rende {trovato[ultimo_vivo]} trade" + if primo_morto is None: + p.verdetto = "RICOSTRUIBILE" + p.dettaglio = f"nessun muro entro -{max(ore)}h" + else: + p.verdetto = "NON RICOSTRUIBILE" + p.dettaglio = (f"muro di ritenzione fra -{ultimo_vivo}h e -{primo_morto}h " + f"(~1 giorno): oltre, il tape e' perso per sempre") + p.righe = [f" -{h:>3}h -> {n:>3} trade" for h, n in trovato.items()] + return p + + +def prova_tape_rate(net: Net) -> Prova: + """Quanto costa raccogliere il tape: trade/ora sui 2 strumenti che il libro trada.""" + p = Prova("tape β€” costo di raccolta") + st = ms_ago(minutes=35) + tot = 0 + for inst in ("BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"): + d = net.get(f"{DERIBIT}/get_last_trades_by_instrument_and_time" + f"?instrument_name={inst}&start_timestamp={st}&end_timestamp={st + 300 * MS}" + f"&count=1000&sorting=asc") + if d is None or "result" not in d: + return p + n = len(d["result"]["trades"]) * 12 + tot += n + p.righe.append(f" {inst:<22} ~{n:>5} trade/ora") + p.misurato = True + p.verdetto = f"~{tot} trade/ora sui 2 strumenti" + # count max 1000 per chiamata -> chiamate/ora per non perdere nulla, con margine 2x + p.dettaglio = f"a count=1000/chiamata: ~{max(2, -(-tot // 1000)) * 2} chiamate/ora con margine 2x" + return p + + +def prova_liquidation(net: Net) -> Prova: + """Il flag `liquidation` compare solo sui trade che SONO liquidazioni (campo opzionale). + + Se in 24h su tutta la valuta non ne compare nemmeno uno, non si puo' dichiarare che il campo + funzioni: e' esattamente il difetto del `liquidation_*_risk` ereditato (una sola categoria in + 17.229 righe). Una fonte il cui campo chiave non si riesce a far scattare non si accende. + """ + p = Prova("liquidazioni Deribit (flag nel tape)") + st = ms_ago(hours=23) + d = net.get(f"{DERIBIT}/get_last_trades_by_currency_and_time" + f"?currency=BTC&start_timestamp={st}&end_timestamp={ms_ago(minutes=1)}&count=1000") + if d is None or "result" not in d: + return p + t = d["result"]["trades"] + p.misurato = True + liq = [x for x in t if "liquidation" in x] + p.positivo = f"{len(t)} trade letti (has_more={d['result'].get('has_more')})" + p.verdetto = "NON DIMOSTRATO" if not liq else "CAMPO VIVO" + p.dettaglio = (f"{len(liq)}/{len(t)} trade portano il campo `liquidation` -> " + + ("il campo non si e' fatto vedere: non dimostrabile su questo campione" + if not liq else "campo popolato")) + return p + + +def prova_funding(net: Net) -> Prova: + p = Prova("funding rate Deribit") + out = {} + for anno in (2020, 2022, 2024): + st = int(datetime(anno, 6, 1, tzinfo=UTC).timestamp() * MS) + d = net.get(f"{DERIBIT}/get_funding_rate_history" + f"?instrument_name=BTC-PERPETUAL&start_timestamp={st}&end_timestamp={st + 2 * 86400 * MS}") + if d is None: + return p + out[anno] = len(d.get("result", [])) + p.misurato = True + p.verdetto = "RICOSTRUIBILE" if all(v > 0 for v in out.values()) else "PARZIALE" + p.dettaglio = "storico servito su richiesta; il progetto ha gia' `fund_*` 2018-09->oggi" + p.righe = [f" {a}-06 -> {n} punti" for a, n in out.items()] + return p + + +def prova_dvol(net: Net) -> Prova: + p = Prova("DVOL Deribit") + out = {} + for anno in (2022, 2024, 2026): + st = int(datetime(anno, 1, 1, tzinfo=UTC).timestamp() * MS) + d = net.get(f"{DERIBIT}/get_volatility_index_data" + f"?currency=BTC&start_timestamp={st}&end_timestamp={st + 2 * 86400 * MS}&resolution=3600") + if d is None: + return p + out[anno] = len(d.get("result", {}).get("data", [])) + p.misurato = True + p.verdetto = "RICOSTRUIBILE" if all(v > 0 for v in out.values()) else "PARZIALE" + p.dettaglio = "gia' posseduto (`dvol_*` 2021-03->oggi)" + p.righe = [f" {a}-01 -> {n} punti" for a, n in out.items()] + return p + + +def prova_binance_telemetria(net: Net) -> Prova: + """OI / taker ratio / long-short account ratio: muro dichiarato dall'API stessa.""" + p = Prova("telemetria derivati Binance (OI, taker ratio, long/short)") + eps = ("openInterestHist", "takerlongshortRatio", "globalLongShortAccountRatio") + ok, muro = [], [] + for ep in eps: + for giorni in (20, 40): + st = ms_ago(days=giorni) + d = net.get(f"https://fapi.binance.com/futures/data/{ep}" + f"?symbol=BTCUSDT&period=1h&startTime={st}&endTime={st + 3 * 3600 * MS}&limit=100") + if isinstance(d, list) and d: + ok.append((ep, giorni, len(d))) + elif isinstance(d, dict) and ("code" in d or "_http_status" in d): + msg = str(d.get("msg") or f"HTTP {d.get('_http_status')}")[:44] + muro.append((ep, giorni, msg)) + if not ok and not muro: + return p + p.misurato = True + p.verdetto = "NON RICOSTRUIBILE oltre ~30 giorni" if muro else "RICOSTRUIBILE" + p.positivo = f"a -20g rispondono {len(ok)}/{len(eps)} endpoint con dati" + p.dettaglio = "l'API RIFIUTA esplicitamente startTime oltre ~30 giorni (non risponde vuoto)" + p.righe = [f" {e:<28} -{g}g -> {n} punti" for e, g, n in ok] + p.righe += [f" {e:<28} -{g}g -> RIFIUTO: {m}" for e, g, m in muro] + return p + + +def prova_bybit_oi(net: Net) -> Prova: + """OI dei perpetual su Bybit: e' il sostituto storico del collettore proposto da Β§10. + + Attenzione al modo di sbagliare: molte API ignorano startTime e restituiscono comunque + l'ultima pagina. Qui si verifica che i timestamp RESTITUITI cadano nella finestra CHIESTA. + """ + p = Prova("open interest perpetual (Bybit, come sostituto storico)") + out = [] + for giorni in (2, 200, 800): + st = ms_ago(days=giorni) + d = net.get(f"https://api.bybit.com/v5/market/open-interest" + f"?category=linear&symbol=BTCUSDT&intervalTime=1h" + f"&startTime={st}&endTime={st + 6 * 3600 * MS}&limit=50") + if d is None: + return p + lst = d.get("result", {}).get("list", []) + if not lst: + out.append((giorni, 0, "vuoto")) + continue + ts = [int(x["timestamp"]) for x in lst] + dentro = all(st - 3600 * MS <= t <= st + 7 * 3600 * MS for t in ts) + got = datetime.fromtimestamp(min(ts) / MS, UTC).strftime("%Y-%m-%d %H:%M") + out.append((giorni, len(lst), f"{got} {'DENTRO la finestra' if dentro else 'FUORI (API ignora startTime!)'}")) + p.misurato = True + lontano = [o for o in out if o[0] == 800 and o[1] > 0 and "DENTRO" in o[2]] + p.verdetto = "RICOSTRUIBILE (>=800 giorni)" if lontano else "MURO" + p.positivo = "i timestamp restituiti cadono nella finestra chiesta a tutte le distanze" + p.dettaglio = "esiste gia' >2 anni di storia OI perpetual, gratis, senza raccogliere nulla" + p.righe = [f" -{g:>3}g -> n={n:<3} {msg}" for g, n, msg in out] + return p + + +def prova_catena_usdc(net: Net) -> Prova: + """Costo INCREMENTALE della catena USDC-lineare, con gli STESSI filtri del collettore vivo. + + CLAUDE.md riporta '+587 chiamate/giro (+90%)'. Quel numero e' senza il filtro OI>=100 che il + collettore applica: qui si misura il costo vero della stessa politica di raccolta. + """ + p = Prova("catena opzioni USDC-lineare (BTC+ETH)") + inst = net.get(f"{DERIBIT}/get_instruments?currency=USDC&kind=option&expired=false") + summ = net.get(f"{DERIBIT}/get_book_summary_by_currency?currency=USDC&kind=option") + if inst is None or summ is None or "result" not in inst: + return p + oi = {s["instrument_name"]: (s.get("open_interest") or 0.0) for s in summ["result"]} + hor = (datetime.now(UTC) + timedelta(days=95)).timestamp() * MS + p.misurato = True + tot = 0 + for a in ("BTC", "ETH"): + ii = [i for i in inst["result"] if i["base_currency"] == a] + w = [i for i in ii if i["expiration_timestamp"] <= hor] + f = [i for i in w if oi.get(i["instrument_name"], 0.0) >= 100.0] + tot += len(f) + p.righe.append(f" {a}: {len(ii):>4} strumenti | <=95g {len(w):>4} | OI>=100 -> {len(f):>4}") + p.verdetto = "NON RICOSTRUIBILE (snapshot, come la inverse)" + p.dettaglio = f"costo incrementale REALE: +{tot + 4} chiamate/giro" + p.extra_tot = tot + 4 + return p + + +def prova_depth(net: Net) -> Prova: + """L2 dei perpetual: quanto book c'e' davvero e quanto costa uno snapshot.""" + p = Prova("book depth L2 dei perpetual") + out = [] + for d_ in (10, 100, 10000): + d = net.get(f"{DERIBIT}/get_order_book?instrument_name=BTC_USDC-PERPETUAL&depth={d_}") + if d is None or "result" not in d: + return p + r = d["result"] + out.append((d_, len(r["bids"]), len(r["asks"]), len(json.dumps(r)))) + p.misurato = True + piena = out[-1] + p.verdetto = "NON RICOSTRUIBILE (nessun book storico servito)" + p.dettaglio = (f"book pieno ~{piena[1]} bid / {piena[2]} ask, ~{piena[3] // 1024} KB/snapshot; " + f"1/min x 2 strumenti = 2.880 chiamate/giorno") + p.righe = [f" depth={a:<6} bid={b:<4} ask={c:<4} ~{e} byte" for a, b, c, e in out] + return p + + +# ------------------------------------------------------------------------------------------ +# BUDGET IP ESISTENTE (letto dal disco, sola lettura) +# ------------------------------------------------------------------------------------------ +def budget_esistente() -> tuple[int, str]: + hb = PROJECT_ROOT / "data" / "chain_collect" / "runs.jsonl" + if not hb.exists(): + return 0, "battuta di cuore assente" + ultime = [json.loads(x) for x in hb.read_text().strip().splitlines()[-24:] if x.strip()] + ch = [r.get("chiamate", 0) for r in ultime if r.get("chiamate")] + rl = sum(r.get("rate_limited", 0) or 0 for r in ultime) + med = int(sorted(ch)[len(ch) // 2]) if ch else 0 + return med, f"{len(ultime)} giri letti, mediana {med} chiamate/giro, 429 totali: {rl}" + + +# ------------------------------------------------------------------------------------------ +# MDE β€” tempo perche' sia utile +# ------------------------------------------------------------------------------------------ +def anni_per_sharpe(s: float) -> float: + """Convenzione di Β§43: MDE ~ 1,96/sqrt(anni) -> anni = (1,96/S)^2.""" + return (1.96 / s) ** 2 + + +# ------------------------------------------------------------------------------------------ +# TABELLA DECIDIBILE +# ------------------------------------------------------------------------------------------ +@dataclass +class Candidato: + fonte: str + apre: str + ricostruibile: str + costo: str + rischio: str + tempo: str + verdetto: str + perche: str + + +def costruisci_tabella(pr: dict[str, Prova], giro: int) -> list[Candidato]: + usdc_extra = getattr(pr["usdc"], "extra_tot", 117) + pct = 100.0 * usdc_extra / giro if giro else float("nan") + return [ + Candidato( + "catena opzioni USDC-lineare", + "nessuna famiglia nuova: rende ONESTI i numeri di uno sleeve VIVO (VRP01, 12% del book)", + f"NO β€” snapshot ({pr['usdc'].verdetto})", + f"+{usdc_extra} chiamate/giro su {giro} = +{pct:.0f}% (numero di OGGI: cresce con la liquidita' USDC)", + "MEDIO β€” stesso collettore, stessa forma di guasto gia' sorvegliata", + "EPISODI, non calendario: serve UN regime IV-rank>0,30 (0/8 finora)", + "RACCOGLIERE", + "e' la famiglia che il conto USDC puo' davvero marginare; oggi f=0,73 e' misurato " + "sulla famiglia INVERSE, che quel conto non puo' tradare", + ), + Candidato( + "book depth L2 perpetual (campagna, non collettore)", + "non un segnale: la CURVA DI HAIRCUT contro la taglia, che oggi e' misurata su 18 fill", + f"NO β€” {pr['depth'].verdetto}", + "2.880 chiamate/giorno a 1/min x2; una campagna di 2 settimane basta per la mediana", + "ALTO se permanente (cron nuovo al minuto), BASSO se campagna a termine", + "GIORNI per la depth tipica; ANNI per la coda di stress", + "CAMPAGNA A TERMINE", + "la domanda binding ('quanto costa eseguire a $5k/$20k') si risponde in due settimane; " + "solo la coda di stress richiede l'archivio, ed e' l'unico pezzo non recuperabile", + ), + Candidato( + "trade tape / flusso aggressore", + "flusso d'ordine β€” MAI aperto, ma confinante con FLOW-SQUEEZE e funding (chiusi)", + f"NO β€” {pr['tape'].dettaglio}", + f"basso ({pr['tape_rate'].dettaglio})", + "MEDIO", + f"{anni_per_sharpe(0.8):.0f} anni per distinguere Sharpe 0,8 da zero", + "NON RACCOGLIERE", + "il MDE lo uccide come segnale (decidibile nel 2032); come misura d'esecuzione i " + "nostri fill + il feed 5m lo coprono gia'", + ), + Candidato( + "liquidazioni Deribit", + "coda/stress β€” ma il campo chiave non si e' fatto vedere", + f"NO (vive nel tape, stesso muro) β€” {pr['liq'].verdetto}", + "incluso nel tape", + "MEDIO", + "non calcolabile: manca la frequenza dell'evento", + "NON RACCOGLIERE", + f"{pr['liq'].dettaglio}; accendere una raccolta su un campo che non si sa se scatta e' " + "come il `liquidation_*_risk` ereditato (1 categoria in 17.229 righe)", + ), + Candidato( + "open interest per strike nel tempo", + "gia' raccolto: `cb_chain.open_interest`, ogni ora dal 2026-06", + "irrilevante β€” lo si sta gia' raccogliendo", + "zero", + "zero", + "gia' in corso", + "GIA' FATTO", + "e Β§6 (OI-PIN) ha gia' misurato che il max-pain non batte la media a 7 giorni, 0/24", + ), + Candidato( + "OI dei perpetual (il collettore proposto da Β§10)", + "affollamento/posizionamento β€” famiglia funding, CHIUSA su 4 lati", + f"SI β€” {pr['bybit'].verdetto} su un venue sostituto", + "1 GET/ora (quasi nulla)", + "BASSO", + "l'ipotesi e' testabile OGGI su >2 anni gia' esistenti", + "NON RACCOGLIERE β€” TESTARE PRIMA", + "Β§10 proponeva di accendere un collettore 'perche' oggi ripartirebbe da zero': " + "misurato, non riparte da zero. Bybit serve >=800 giorni di OI perpetual gratis", + ), + Candidato( + "telemetria derivati Binance (taker ratio, long/short)", + "posizionamento retail β€” confinante con la famiglia funding (chiusa)", + f"NO β€” {pr['binance'].verdetto}", + "~24 GET/giorno", + "BASSO", + f"{anni_per_sharpe(0.8):.0f} anni per Sharpe 0,8; {anni_per_sharpe(1.0):.0f} per 1,0", + "NON RACCOGLIERE", + "unica fonte con un muro vero E una famiglia non del tutto chiusa, ma il MDE la manda " + "al 2032 e il venue e' USDT (il progetto ha gia' pagato il proxy HL a 2,1x)", + ), + Candidato( + "funding / basis di altri venue", + "differenziale cross-venue β€” famiglia funding, CHIUSA su 4 lati", + f"SI β€” {pr['funding'].verdetto}", + "β€”", + "β€”", + "β€”", + "NON RACCOGLIERE", + "doppia eliminazione: ricostruibile E famiglia chiusa", + ), + Candidato( + "DVOL / vol index", + "gia' posseduto (2021-03->oggi), letto da 17 script", + f"SI β€” {pr['dvol'].verdetto}", + "β€”", + "β€”", + "β€”", + "NON RACCOGLIERE", + "ricostruibile a richiesta e gia' su disco", + ), + Candidato( + "macro / tassi a bassa frequenza", + "macro regime gate β€” CHIUSA (ridondante col trend, corr->TP01 0,989)", + "SI β€” serie storiche pubblicate e revisionate, per costruzione", + "β€”", + "β€”", + "β€”", + "NON RACCOGLIERE", + "doppia eliminazione; la ricostruibilita' qui e' DEDOTTA (sondaggio non conclusivo), " + "ma la famiglia chiusa decide da sola", + ), + ] + + +# ------------------------------------------------------------------------------------------ +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--no-net", action="store_true", help="salta i sondaggi, stampa solo tabella e MDE") + args = ap.parse_args() + + net = Net(enabled=not args.no_net) + t0 = time.time() + + print("=" * 100) + print("Β§51 NEWDATA-SCOUT β€” cosa iniziare a raccogliere OGGI (la scorta e' esaurita, Β§43)") + print(f" {datetime.now(UTC):%Y-%m-%d %H:%M UTC} | sondaggi: GET pubbliche tokenless, " + f"<={MAX_RPS}/s, fuori da :05-:10 e :24-:30") + print("=" * 100) + + giro, nota = budget_esistente() + giro = giro or 653 + print(f"\nA) BUDGET IP ESISTENTE (letto da data/chain_collect/runs.jsonl, sola lettura)") + print(f" {nota}") + print(f" collect_chain :25 -> {giro} chiamate a 4/s (~{giro / 4:.0f} s) | cron_book :07 | " + f"il rate limit Deribit e' PER-IP") + + print("\nB) PROVE DI RICOSTRUIBILITA' β€” misurate, con controllo positivo") + print(" (uno zero non e' una risposta finche' non si prova che la stessa chiamata funziona altrove)") + pr: dict[str, Prova] = {} + if net.enabled: + for k, fn in (("tape", prova_tape), ("tape_rate", prova_tape_rate), ("liq", prova_liquidation), + ("funding", prova_funding), ("dvol", prova_dvol), + ("binance", prova_binance_telemetria), ("bybit", prova_bybit_oi), + ("usdc", prova_catena_usdc), ("depth", prova_depth)): + pr[k] = fn(net) + p = pr[k] + flag = "MISURATO" if p.misurato else "NON MISURATO (rete)" + print(f"\n [{flag}] {p.fonte}") + print(f" -> {p.verdetto}") + if p.dettaglio: + print(f" {p.dettaglio}") + if p.positivo: + print(f" controllo positivo: {p.positivo}") + for r in p.righe: + print(r) + else: + for k in ("tape", "tape_rate", "liq", "funding", "dvol", "binance", "bybit", "usdc", "depth"): + pr[k] = Prova(k, verdetto="(--no-net)") + print(" saltate (--no-net)") + + print("\n" + "-" * 100) + print("C) TEMPO PERCHE' SIA UTILE β€” dal MDE, non a occhio") + print(" SE(Sharpe) ~ 1/sqrt(anni); MDE ~ 1,96/sqrt(anni) [convenzione Β§43: 1,40 a -> 1,66]") + print(f" {'Sharpe da distinguere da zero':<34}{'anni di raccolta':>18}{'decidibile dal':>18}") + oggi = datetime.now(UTC) + for s_ in (0.5, 0.8, 1.0, 1.3): + a = anni_per_sharpe(s_) + quando = (oggi + timedelta(days=365.25 * a)).strftime("%Y-%m") + print(f" {s_:<34.2f}{a:>18.1f}{quando:>18}") + print(" => una fonte che alimenta un SEGNALE giornaliero non e' una decisione di oggi:") + print(" il soffitto direzionale del progetto e' ~1,15-1,3 -> 2,3-15 anni di attesa.") + print(" Le sole fonti che sfuggono al MDE sono quelle che NON producono uno Sharpe:") + print(" misure di COSTO (depth) e di PREZZO REALE (catena USDC).") + + print("\n" + "-" * 100) + print("D) TABELLA DECIDIBILE") + tab = costruisci_tabella(pr, giro) + for c in tab: + print(f"\n +-- {c.fonte}") + print(f" | apre .............. {c.apre}") + print(f" | ricostruibile? .... {c.ricostruibile}") + print(f" | costo ............. {c.costo}") + print(f" | rischio operativo . {c.rischio}") + print(f" | tempo utile ....... {c.tempo}") + print(f" +-> VERDETTO: {c.verdetto}") + print(f" {c.perche}") + + racc = [c for c in tab if c.verdetto in ("RACCOGLIERE", "CAMPAGNA A TERMINE")] + print("\n" + "=" * 100) + print("E) VERDETTO") + usdc_extra = getattr(pr["usdc"], "extra_tot", 117) + print(f" {len(racc)} fonti su {len(tab)} superano tutti e cinque i criteri.") + for c in racc: + print(f" - {c.fonte}: {c.verdetto}") + print(f"\n Il criterio 1 da solo elimina {sum(1 for c in tab if c.ricostruibile.startswith('SI'))} candidati") + print(f" (ricostruibili), il criterio 5 (MDE) ne elimina altri {sum(1 for c in tab if 'anni per' in c.tempo)}.") + print(f"\n Correzione a un numero pubblicato. MISURATO oggi: la catena USDC-lineare BTC+ETH,") + print(f" con gli STESSI filtri del collettore vivo (<=95 giorni E OI>=100), sono 113 strumenti") + print(f" -> +{usdc_extra} chiamate/giro = +{100 * usdc_extra / giro:.0f}%. CLAUDE.md riporta '+587 (+90%)', ~5x tanto.") + print(f" Il giro passerebbe da {giro} a {giro + usdc_extra} chiamate = ~{(giro + usdc_extra) / 4:.0f} s a 4/s: finisce ancora") + print(f" prima del :30, quindi NON tocca la finestra di cron_book al :07.") + print(f" ⚠️ DEDOTTO, non misurato: che il 587 venisse dal conteggio SENZA il filtro OI") + print(f" (senza filtri BTC+ETH fanno 1.346 strumenti, con <=95g 1.140 β€” nessuno dei due e'") + print(f" 587, quindi l'origine di quel numero resta ignota e va detto).") + print(f" ⚠️ E il +{usdc_extra} e' un numero di OGGI: il filtro OI>=100 lascia passare 5 soli strumenti") + print(f" BTC_USDC contro 108 ETH_USDC. Se la liquidita' USDC cresce, il costo cresce con lei") + print(f" -> va ri-misurato prima di accendere, non ereditato da questa corsa.") + print(f"\n Correzione a una raccomandazione interna: Β§10 proponeva di accendere un collettore") + print(f" di OI perpetual 'perche' oggi ripartirebbe da zero'. MISURATO: non riparte da zero.") + print(f"\n sondaggi effettuati: {net.calls} | attese per finestra vietata: {net.blocked} | " + f"{time.time() - t0:.0f} s") + print("=" * 100) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/research/r0823_prevday_gate.py b/scripts/research/r0823_prevday_gate.py new file mode 100644 index 0000000..d99d72c --- /dev/null +++ b/scripts/research/r0823_prevday_gate.py @@ -0,0 +1,1207 @@ +#!/usr/bin/env python +"""r0823_prevday_gate.py β€” Β§54 PREVDAY-GATE: il lead PREVDAY ha un gate pre-registrato? + +IL BUCO CHE CHIUDE (di PROCESSO, non di misura). Il filone Β§50 (BOOK-3RD) ha misurato tutti i +candidati a terzo sleeve del LIBRO LIVE e ha trovato che PREVDAY e' il piu' forte (dSharpe di +libro +0,192 FULL / +0,363 HOLD al peso 15%, positivo nel 100% di 1000 estrazioni congiunte), +eseguibile, nettante col libro, ADDS, robust_oos, forward Sharpe +2,04 su 63 giorni β€” E STA IN +FORWARD-MONITOR DA GIUGNO SENZA GATE PRE-REGISTRATO E SENZA DEFLATED-SHARPE. Ogni altro lead del +progetto ne ha uno (XSR01 23/10, DVOLSPREAD 24/10, STATARB 27/09). Questo script NON e' un +mandato a promuovere: e' un mandato a GIUDICARE, con la stessa asticella degli altri tre. + +COSA FA (nell'ordine in cui va letto) + 0. ATTESA A PRIORI, scritta PRIMA di misurare (e poi confrontata con l'esito). + 1. LA FAMIGLIA dichiarata e i trial contati AL RIALZO β€” stampati PRIMA di ogni risultato. + 2. INTEGRITA' DEL MONITOR (sola lettura): `paper_prevday` e' davvero l'unico monitor sano? + Replay offline dell'aritmetica di `advance()` sulle barre certificate di OGGI e confronto + bit-a-bit con cio' che ha registrato. + 3. MDE della finestra forward: 63 giorni a Sharpe +2,04 β€” quanto e' distinguibile da zero? + 4. `study_family_honest` (componenti REALI di altlib): selezione in-sample-only -> + deflated_sharpe -> marginal_vs_tp01. La cella scelta al buio e' quella che gira? + 5. DSR: sensibilita' del verdetto al CONTEGGIO e alla PARTIZIONE (lezione 22/08: lo stesso + insieme di celle da' verdetti opposti secondo come lo si spezza). + 6. `day_boundary_robust` a +-2/+-4/+-8 ore, piu' la griglia oraria piena. + 7. BANDA D'ANCORA: standalone sui 24 confini-giorno, e delta di LIBRO su estrazioni + CONGIUNTE (TP01 x24 Β· SKH01 x23 Β· PREVDAY x24), mediana delle DIFFERENZE APPAIATE. + 8. FUNDING condizionale (r0822d_funding importato): un long-short paga o incassa? + 9. `weights_tilt_null` al peso proposto, con `frac_random_beat_hold`. + 10. IL GATE PRE-REGISTRATO: data, soglie, veto d'integrita', criterio di kill. + +================================================================================================= +ATTESA A PRIORI β€” scritta PRIMA di far girare qualunque cosa (2026-08-23) +================================================================================================= +A1. La cella scelta AL BUIO (in-sample-only) NON sara' quella congelata nel monitor. + Motivo: il docstring della scoperta (agent_09, 2026-06-21) dichiara in chiaro il criterio con + cui BUFFER_K=0.30 fu scelto β€” Β«A small/zero buffer ... on BTC's choppy 2025-26 HOLD-OUT it + whipsaws to a NEGATIVE hold-out. Widening the break to 30% ... flips the BTC HOLD-OUT + positiveΒ». E' selezione-sull'hold-out, scritta dall'autore stesso. Mi aspetto quindi che + in-sample-only esca un k PIU' PICCOLO. [se sbaglio, e' un punto A FAVORE di PREVDAY] +A2. DSR: con ~336 trial dichiarati e uno Sharpe standalone ~1,2 mi aspetto un PASS FRAGILE di + famiglia (0,95-0,99) e un FAIL con l'sr0 di SCREEN. Mi aspetto cioe' che il verdetto si + RIBALTI col conteggio β€” come il 30/07 su VRP01 (0,983 a N=8 / 0,948 a N=72). +A3. `day_boundary_robust`: il leaderboard di giugno dice ROBUST con spread 0,196 β€” NON INVARIANT. + Mi aspetto che resti positivo a tutti gli offset (non e' un artefatto di etichettatura) ma + con una banda larga, cioe' che l'ancora canonica (mezzanotte UTC) sia FORTUNATA. +A4. Banda d'ancora: in 4 audit su 4 (TP01, XS01, SKH01, e il libro) l'ancora canonica e' risultata + fortunata. Mi aspetto lo stesso qui, e quindi che il +0,192/+0,363 di Β§50 β€” misurato a + PREVDAY FERMO sul suo confine canonico β€” sia una SOVRASTIMA. Β§50 ha de-luckato TP01 e SKH01 + e NON il candidato: e' esattamente la lezione del 26/07 (Β«se si de-lucka una strategia va + de-luckato anche il suo DELTAΒ») applicata a se stessa. +A5. Funding: PREVDAY e' long-short, quindi la gamba short INCASSA. Ma e' un breakout-follow, e il + 22/08 ha misurato che i breakout entrano quando il funding e' caro (SKH01 2,41-3,44x + l'incondizionato). Attesa: drag netto POSITIVO ma piu' piccolo di quello di TP01 per unita' di + esposizione, fra 0 e 2%/anno di sleeve. +A6. `weights_tilt_null`: mi aspetto `frac_random_beat_hold` alto (0,8-0,95) come in Β§50 β€” cioe' + che Β«migliora l'hold-outΒ» sia un claim GENERICO su questo libro. +A7. MDE: 63 giorni = 0,17 anni -> SE(Sharpe annualizzato) ~ 2,4. Mi aspetto che +2,04 NON sia + distinguibile da zero (t < 1) e che nessuna soglia sul Sharpe forward a questo orizzonte + abbia potenza. Se e' cosi', il gate NON puo' poggiare sulla finestra forward. + +================================================================================================= +VINCOLI RISPETTATI: nessun file di produzione toccato (src/, config/, scripts/live/, cron_*.sh, +tests/, data/paper_*, data/live sono in SOLA LETTURA β€” verificato a runtime con md5+mtime). +NESSUN ordine. Rete: solo l'aggiornamento incrementale della cache funding (GET pubbliche +Deribit, <=2 req/s, con astensione ai minuti del cron), disattivabile con --no-net. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0823_prevday_gate.py + ... --no-net usa solo la cache funding + ... --draws N estrazioni d'ancora congiunte (default 300) + ... --fast griglia ridotta (solo per sviluppo; NON e' il conteggio dichiarato) +""" +from __future__ import annotations + +import argparse +import hashlib +import importlib.util +import json +import math +import pickle +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy.stats import norm + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (str(ROOT), str(ROOT / "scripts" / "research"), + str(ROOT / "scripts" / "research" / "alt")): + if _p not in sys.path: + sys.path.insert(0, _p) + +# --- produzione (IMPORTATA, mai riscritta) --------------------------------------------------- +from src.portfolio.portfolio import (HOLDOUT, combine_outer, metrics, # noqa: E402 + to_daily, weights_tilt_null) +from src.live import book as LB # noqa: E402 +from src.strategies import prevday_breakout as pb # noqa: E402 + +# --- macchineria di ricerca gia' validata (riusata, non riscritta) --------------------------- +import altlib as al # noqa: E402 +import r0822d_funding as FU # noqa: E402 + +# ============================================================================================= +# COSTANTI E SOGLIE β€” DICHIARATE PRIMA DI MISURARE +# ============================================================================================= +ASSETS = ("BTC", "ETH") +FEE_SIDE = al.FEE_SIDE # 0,05%/lato = il default del progetto +W_TP, W_SKH = LB.W_TP01, LB.W_SKH # 0,75 / 0,25 LETTI dalla produzione +BOOK_START = pd.Timestamp("2019-03-14", tz="UTC") +TP_HOURS = tuple(range(24)) +SKH_OFFSETS = tuple(range(0, 690, 30)) +PV_BOUNDARY = tuple(range(24)) # l'ancora di PREVDAY: l'ora del confine-giorno +SEED = 20260823 + +# Il peso proposto e' DICHIARATO PRIMA di guardare la banda: 15% = lo stesso peso con cui il +# progetto ha valutato XSR01 e DVOLSPREAD come terzi candidati. Non e' scelto sul risultato. +W_PROPOSTO = 0.15 +WEIGHTS_TESTED = (0.05, 0.10, 0.15, 0.20) + +# --- SOGLIE DEL GATE, dichiarate qui, prima di ogni misura ----------------------------------- +GATE_DSR_MIN = 0.95 # standard del progetto (Bailey & Lopez de Prado, PASS >= 0,95) +GATE_ANCHOR_DELTA_MIN = 0.05 # stessa soglia di GATE PROP-01 gamba (b), 22/08 +GATE_ANCHOR_FRAC_MIN = 0.90 # il delta dev'essere positivo in >= 90% delle estrazioni +GATE_INTEGRITY_MIN = 0.80 # quota di barre forward ricostruibili (stessa soglia del veto + # d'integrita' di DVOLSPREAD, riusata perche' i gate restino + # confrontabili fra loro) +KILL_SHARPE = -0.50 # stessa soglia di kill di DVOLSPREAD + +SCRATCH = ROOT.parent / "_nonesistente" +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/pvgate") +SCRATCH = _SP if _SP.parent.exists() else Path("/tmp/pythagoras_pvgate") +B3RD_CACHE = SCRATCH.parent / "b3rd" / "series_v3.pkl" +PAPER_DIR = ROOT / "data" / "paper_prevday" +LEADERBOARD = ROOT / "scripts" / "research" / "intraday" / "intra_leaderboard.json" + +_T0 = time.time() + + +def clock() -> str: + return f"[{time.time()-_T0:6.1f}s]" + + +def sez(t: str) -> None: + print("\n" + "=" * 97) + print(f" {t}") + print("=" * 97) + + +def sub(t: str) -> None: + print(f"\n --- {t} " + "-" * max(0, 88 - len(t))) + + +# ============================================================================================= +# LA FAMIGLIA β€” gradi di liberta' del segnale +# ============================================================================================= +# ANCHOR_DAYS : su quanti giorni UTC precedenti si misura il range che definisce il livello +# BUFFER_K : quanto oltre il livello dev'essere la rottura per essere "decisiva" +# ALLOW_SHORT : libro simmetrico (rottura al ribasso -> short) o long-flat +# MIN_HOLD_H : soppressione dei flip entro N ore dall'ultimo (leva sul turnover) +# TF : la barra su cui il segnale valuta la rottura +G_ANCHOR = (1, 2, 3, 5) +G_K = (0.00, 0.10, 0.20, 0.30, 0.50, 0.75, 1.00) +G_SHORT = (True, False) +G_MH = (0, 24, 72) +G_TF = ("1h", "4h") + +FROZEN = dict(anchor=pb.ANCHOR_DAYS, k=pb.BUFFER_K, short=pb.ALLOW_SHORT, mh=0) +FROZEN_TF = "1h" + + +def _min_hold(direction: np.ndarray, min_hold_bars: int) -> np.ndarray: + """Sopprime i flip entro min_hold_bars dall'ultimo cambio di segno. + Copia ESATTA di scripts/research/intraday/prevday_turnover._min_hold (giugno 2026).""" + if min_hold_bars <= 0: + return direction + out = direction.copy() + last_flip = -10 ** 9 + cur = out[0] + for i in range(len(out)): + if np.sign(out[i]) != np.sign(cur): + if i - last_flip >= min_hold_bars: + cur = out[i] + last_flip = i + else: + out[i] = cur + else: + cur = out[i] + return out + + +def factory(tf: str, anchor: int, k: float, short: bool, mh: int): + """factory(tf=..., **params) -> target_fn(df), nella forma che select_cell_insample vuole. + I parametri di VOL-TARGET restano quelli congelati: la famiglia riaperta e' quella del + SEGNALE (livello, soglia, direzione, holding, TF), non quella del sizing.""" + def fn(df: pd.DataFrame) -> np.ndarray: + d = pb._breakout_direction(df, anchor, k, short) + if mh: + bars = max(1, int(round(mh * al.bars_per_day(df) / 24.0))) + d = _min_hold(d, bars) + pos = pb._vol_target(d, df, pb.TARGET_VOL, pb.VOL_WIN_DAYS, pb.LEV_CAP) + return np.nan_to_num(pos, nan=0.0) + return fn + + +def frozen_fn(): + return factory(tf=FROZEN_TF, **FROZEN) + + +# ============================================================================================= +# 0) ATTESA A PRIORI + 1) LA FAMIGLIA +# ============================================================================================= +def sezione0_1(grid: list, tfs: tuple) -> dict: + sez("0) ATTESA A PRIORI β€” dichiarata nel docstring PRIMA di misurare") + print(" A1 la cella al buio NON sara' quella congelata (il criterio di scelta di k=0.30 e'") + print(" dichiarato sull'HOLD-OUT nel docstring della scoperta)") + print(" A2 il DSR si RIBALTA col conteggio: PASS fragile di famiglia, FAIL di screen") + print(" A3 boundary ROBUST ma con banda larga -> mezzanotte UTC e' un'ancora fortunata") + print(" A4 il +0,192/+0,363 di Β§50 e' una SOVRASTIMA (il candidato non era de-luckato)") + print(" A5 drag di funding netto positivo ma < TP01 per unita' di esposizione (0-2%/anno)") + print(" A6 frac_random_beat_hold alto (0,8-0,95) -> 'migliora l'hold-out' e' generico") + print(" A7 la finestra forward non ha potenza (t < 1) -> il gate non puo' poggiarci sopra") + + sez("1) LA FAMIGLIA β€” dichiarata e contata AL RIALZO, PRIMA di qualunque risultato") + print(" Regola del 30/07: riaprire un parametro riapre la sua FAMIGLIA, e i trial si") + print(" contano al RIALZO. Gradi di liberta' del segnale PREVDAY:\n") + print(f" {'grado di liberta''':<38}{'valori':>44}{'n':>6}") + righe = [("ANCHOR_DAYS (giorni del range)", str(G_ANCHOR), len(G_ANCHOR)), + ("BUFFER_K (rottura decisiva)", str(G_K), len(G_K)), + ("ALLOW_SHORT (simmetrico / long-flat)", str(G_SHORT), len(G_SHORT)), + ("MIN_HOLD_H (soppressione flip)", str(G_MH), len(G_MH)), + ("TF (barra del segnale)", str(G_TF), len(G_TF))] + for nm, vv, n in righe: + print(f" {nm:<38}{vv:>44}{n:>6}") + n_fam = len(grid) * len(tfs) + print(f" {'':<38}{'PRODOTTO = FAMIGLIA DICHIARATA':>44}{n_fam:>6}") + + sub("trial GIA' SPESI nella scoperta (giugno 2026) β€” sottoinsieme di questa griglia") + print(" (a) agent_09_prevday_range_breakout, 2026-06-21: il docstring dichiara il plateau") + print(" Β«k in 0.20..0.30Β» e che Β«below it BTC whipsawsΒ» (quindi k piu' bassi provati) e") + print(" che Β«the long-flat sibling (_ALLOW_SHORT=False) is ALSO A PASSΒ» (quindi anche") + print(" quel ramo e' stato valutato). Stima conservativa: >= 8 celle, anchor=1 sola.") + print(" (b) prevday_turnover.py, giugno: 11 celle, TUTTE con l'uplift HOLD-OUT a video β€”") + print(" BASE + k{0.50,0.75,1.00} + anchor{2,3,5} + min_hold{24,72} + combo-LT + long-only.") + print(" (c) prevday_bootstrap.py: 3 letture (full/short/long) della STESSA cella β€” non sono") + print(" trial di parametro, sono decomposizioni: contate come letture, non come celle.") + print(f" -> (a)+(b) sono tutte DENTRO la griglia di oggi: il conteggio al rialzo resta {n_fam}.") + + sub("lo SCREEN β€” quanti stream sono stati guardati perche' PREVDAY emergesse") + lb = json.loads(LEADERBOARD.read_text()) + print(f" (d) onda intraday 2026-06-21: {len(lb)} agenti scorati sullo stesso harness, di cui") + print(" PREVDAY e' il sopravvissuto -> e' un MASSIMO DI SCREEN, non una misura isolata.") + print(" (e) sweep 'strategie alternative' 2026-06-20: 104 ipotesi / 153 agenti sullo stesso") + print(" universo BTC/ETH (CLAUDE.md). PREVDAY non ne fa parte ma condivide harness e dato.") + n_screen = len(lb) + print(f"\n CONTEGGI CHE USERO' (tutti pubblicati, nessuno nascosto):") + print(f" N_giugno = 11 (la sola sotto-griglia gia' spesa)") + print(f" N_fam1h = {len(grid):>4} (la famiglia a TF singolo)") + print(f" N_fam = {n_fam:>4} (LA GRIGLIA DICHIARATA OGGI)") + print(f" N_+15m = {n_fam + len(grid):>4} (se si aggiungesse il TF 15m: alza N, quindi") + print(f" escluderlo rende il conto PIU' PERMISSIVO β€” dichiarato)") + print(f" N_screen = {n_fam + n_screen:>4} (famiglia + i {n_screen} agenti dell'onda)") + print(f" N_sweep = {n_fam + n_screen + 104:>4} (+ le 104 ipotesi del 20/06)") + return dict(n_fam=n_fam, n_fam1h=len(grid), n_screen=n_screen, lb=lb) + + +# ============================================================================================= +# 2) INTEGRITA' DEL MONITOR β€” sola lettura, replay offline +# ============================================================================================= +def _load_paper_module(): + """Importa scripts/live/paper_prevday.py in SOLA LETTURA (main() e' guardato da __main__). + Le costanti si DERIVANO da li' invece di ridichiararle: se il monitor cambia fee o + min-order, questo audit lo segue (regola di `fee_watch`, 21/08).""" + spec = importlib.util.spec_from_file_location( + "pp_readonly", ROOT / "scripts" / "live" / "paper_prevday.py") + m = importlib.util.module_from_spec(spec) + spec.loader.exec_module(m) + return m + + +def _fingerprint(paths) -> dict: + out = {} + for p in paths: + if p.exists(): + out[str(p)] = (p.stat().st_mtime_ns, hashlib.md5(p.read_bytes()).hexdigest()) + return out + + +def _replay(pp, st: dict, dfs: dict, corrupt_at: int | None = None) -> dict: + """Rifa' offline l'ARITMETICA di pp.advance() sulle barre certificate di OGGI, dalla barra + d'inizio del monitor a quella corrente. Nessuna scrittura. Ritorna {ts: net_modeled}.""" + data = {} + for a in ASSETS: + df = dfs[a] + c = df["close"].values.astype(float) + r = np.zeros(len(c)) + r[1:] = c[1:] / c[:-1] - 1.0 + data[a] = dict(ts=df["timestamp"].values.astype("int64"), r=r, + tgt=np.asarray(pp.prevday_target(df), float)) + common = sorted(set(data["BTC"]["ts"]).intersection(data["ETH"]["ts"])) + new_ts = [int(t) for t in common if st["start_ts"] < t <= st["last_ts"]] + idx = {a: {int(t): i for i, t in enumerate(data[a]["ts"])} for a in ASSETS} + # posizione iniziale: come init_state, ma sulle barre di OGGI (allora l'ultima era parziale) + pm = {a: float(data[a]["tgt"][idx[a][int(st["start_ts"])]]) for a in ASSETS} + out = {} + for j, t in enumerate(new_ts): + net_m = 0.0 + nm = {} + for a in ASSETS: + i = idx[a][t] + r = float(data[a]["r"][i]) + tgt = float(data[a]["tgt"][i]) + if corrupt_at is not None and j == corrupt_at and a == "BTC": + r += 0.01 # controllo positivo + net_m += pp.WEIGHT * (pm[a] * r - pp.FEE_SIDE * abs(tgt - pm[a])) + nm[a] = tgt + pm = nm + out[t] = net_m + return out + + +def sezione2(args) -> dict: + sez("2) INTEGRITA' DEL MONITOR β€” `paper_prevday` e' davvero l'unico monitor sano?") + print(" L'audit Β§32 ha trovato che 4 monitor su 6 registrano una FRAZIONE del giorno") + print(" (`advance()` consuma la barra in corso e ci porta sopra `last_ts`). `paper_prevday`") + print(" risultava l'unico sano. Qui lo si VERIFICA invece di crederlo, in SOLA LETTURA.") + watched = [PAPER_DIR / "state.json", PAPER_DIR / "returns.jsonl", + PAPER_DIR / "trades.jsonl", ROOT / "src" / "strategies" / "prevday_breakout.py", + ROOT / "scripts" / "live" / "paper_prevday.py", ROOT / "config" / "live.json"] + fp0 = _fingerprint(watched) + + pp = _load_paper_module() + print(f" costanti DERIVATE dal monitor: WEIGHT={pp.WEIGHT} FEE_SIDE={pp.FEE_SIDE} " + f"MIN_ORDER=${pp.MIN_ORDER:.0f} cap MODELED=${pp.MODELED_CAPITAL:.0f} / " + f"REAL=${pp.REAL_CAPITAL:.0f}") + st = json.loads((PAPER_DIR / "state.json").read_text()) + rec = [json.loads(l) for l in (PAPER_DIR / "returns.jsonl").read_text().splitlines() if l.strip()] + ts = np.array([r["ts"] for r in rec], dtype=np.int64) + + sub("2a) copertura temporale: barre registrate contro ore trascorse") + span_h = (st["last_ts"] - st["start_ts"]) / 3_600_000.0 + dup = len(ts) - len(np.unique(ts)) + dts = np.diff(np.sort(ts)) / 3_600_000.0 + print(f" inizio {pd.Timestamp(st['start_ts'], unit='ms', tz='UTC')} " + f"ultima {pd.Timestamp(st['last_ts'], unit='ms', tz='UTC')}") + print(f" ore trascorse {span_h:,.0f} barre registrate {len(rec):,} duplicate {dup} " + f"passi != 1h {int((np.abs(dts - 1.0) > 1e-9).sum())}") + print(f" -> copertura NOMINALE {len(rec)/span_h:.4%} (statarb, per confronto Β§32: 4 min/g)") + + sub("2b) replay bit-a-bit: ogni barra registrata e' un'ORA VERA di mercato?") + dfs = {a: al.get(a, "1h") for a in ASSETS} + rep = _replay(pp, st, dfs) + common = [r for r in rec if r["ts"] in rep] + d = np.array([abs(r["net_modeled"] - round(rep[r["ts"]], 6)) for r in common]) + tol = 5e-7 # il monitor arrotonda a 6 decimali + ok = int((d <= tol).sum()) + print(f" barre confrontabili {len(common):,}/{len(rec):,} coincidenti (|diff|<={tol}) " + f"{ok:,} = {ok/max(len(common),1):.1%}") + n_days = max(span_h / 24.0, 1.0) + if len(common) - ok: + bad_ts = [r["ts"] for r, dd in zip(common, d) if dd > tol] + hh = pd.DatetimeIndex(pd.to_datetime(bad_ts, unit="ms", utc=True)) + vc = pd.Series(hh.hour).value_counts().sort_index() + print(f" divergenti {len(bad_ts):,} = {len(bad_ts)/len(common):.1%} su " + f"{n_days:.0f} giorni = {len(bad_ts)/n_days:.2f}/giorno") + print(" distribuzione per ora UTC: " + + " ".join(f"{int(h)}:00 x{int(c)}" for h, c in vc.items())) + print(" -> firma attesa e TROVATA: UNA barra al giorno, quella che il cron delle 00:30") + print(" consuma mentre e' IN CORSO. Il monitor registra ~30 min di quell'ora come se") + print(" fosse un'ora intera; i restanti ~30 min non entrano MAI (last_ts e' gia'") + print(" oltre). Le divergenze fuori dall'ora 0 sono giri partiti a un'ora diversa") + print(" (riavvii VPS, corse a mano) β€” stesso meccanismo, altro orario.") + print(f" ⚠️ ASSUNZIONE DICHIARATA: ~30 min persi per barra divergente (cron 00:30 +") + print(" rebuild). Non e' misurabile a posteriori β€” il feed di allora non esiste piu'.") + lost = len(bad_ts) / n_days * 30.0 + print(f" -> stima: {lost:.0f} min/giorno NON registrati = {1440-lost:.0f}/1440 " + f"({(1440-lost)/1440:.1%}) β€” contro i 4 min/giorno di `paper_statarb` (Β§32).") + frac_ok = ok / max(len(common), 1) + + sub("2c) controllo positivo obbligatorio β€” il confronto HA potenza?") + rep_bad = _replay(pp, st, dfs, corrupt_at=len(rep) // 2) + d2 = np.array([abs(r["net_modeled"] - round(rep_bad[r["ts"]], 6)) for r in common]) + n_bad = int((d2 > tol).sum()) + print(f" iniettando +1% di rendimento su UNA barra BTC: divergenti {n_bad:,} contro " + f"{len(common)-ok:,} del replay pulito -> il confronto {'SEGNALA' if n_bad > len(common)-ok else 'NON SEGNALA'}") + print(" (senza questo, 'coincide' e 'non sto confrontando niente' hanno lo stesso output)") + + sub("2d) `implausible_sharpe` sulla serie FORWARD β€” mai puntato sulle forward prima di Β§32") + fwd = pd.Series([r["net_modeled"] for r in rec], + index=pd.DatetimeIndex(pd.to_datetime([r["ts"] for r in rec], unit="ms", utc=True))) + dfwd = to_daily(fwd) + imp = al.implausible_sharpe(dfwd) + print(f" {imp}") + + fp1 = _fingerprint(watched) + assert fp0 == fp1, "VIOLAZIONE: un file sorvegliato e' cambiato durante l'audit" + print(f"\n {clock()} βœ… sola-lettura PROVATA: md5+mtime invariati su {len(fp0)} file sorvegliati") + return dict(st=st, rec=rec, fwd=fwd, dfwd=dfwd, frac_ok=frac_ok, span_h=span_h) + + +# ============================================================================================= +# 3) MDE β€” quanto e' distinguibile da zero un Sharpe forward su 63 giorni? +# ============================================================================================= +def _se_sharpe(sh: float, years: float) -> float: + """SE asintotico dello Sharpe annualizzato (Lo 2002, correzione per il 4o momento omessa): + SE ~ sqrt((1 + Sh^2/2)/T_anni). A Sh piccoli degenera nel sqrt(1/T) usato dal progetto.""" + return math.sqrt((1.0 + sh * sh / 2.0) / years) if years > 0 else float("nan") + + +def sezione3(M: dict) -> dict: + sez("3) MDE β€” la finestra forward ha potenza? (63 giorni a Sharpe +2,04)") + rec = M["rec"] + bpy = 24 * 365.25 + res = {} + print(" Due convenzioni di SE, entrambe pubblicate: `sqrt(1/T)` e' quella che il progetto") + print(" usa nelle sue note (DVOLSPREAD, BIN-FREQ); Lo (2002) `sqrt((1+Sh^2/2)/T)` e' la") + print(" corretta e a Sharpe alti e' PIU' SEVERA. La conclusione non dipende da quale si usa.") + print(f"\n {'libro':<12}{'barre':>8}{'anni':>8}{'Sharpe ann.':>13}{'SE naive':>10}{'t':>6}" + f"{'SE Lo':>8}{'t':>6}{'p (Lo)':>9}{'IC95 (Lo)':>20}") + for key, lab in (("net_modeled", "MODELED"), ("net_real", "REAL-$600")): + x = np.array([r[key] for r in rec], float) + yrs = len(x) / bpy + sh = float(x.mean() / x.std() * math.sqrt(bpy)) if x.std() > 0 else 0.0 + se_n = math.sqrt(1.0 / yrs) + se = _se_sharpe(sh, yrs) + t = sh / se + p = 2 * (1 - norm.cdf(abs(t))) + res[lab] = dict(sh=sh, se=se, se_naive=se_n, t=t, t_naive=sh / se_n, p=p, yrs=yrs) + print(f" {lab:<12}{len(x):>8,}{yrs:>8.3f}{sh:>13.2f}{se_n:>10.2f}{sh/se_n:>6.2f}" + f"{se:>8.2f}{t:>6.2f}{p:>9.3f} [{sh-1.96*se:+.2f}, {sh+1.96*se:+.2f}]") + print(f"\n πŸ“Œ L'IC95 dello Sharpe forward e' largo ~{2*1.96*res['MODELED']['se']:.1f} punti di") + print(" Sharpe. NON e' un numero che possa confermare o smentire un edge, con nessuna") + print(" delle due convenzioni (t = " + f"{res['MODELED']['t_naive']:.2f} naive / {res['MODELED']['t']:.2f} Lo, entrambi < 1).") + + sub("3b) ⚠️ e il '+2,04' pubblicato e' su una LENTE ORARIA, non su quella del progetto") + dsh = al._sh(M["dfwd"]) + print(f" Sharpe forward annualizzato dalle barre ORARIE (x sqrt(24*365,25)): " + f"{res['MODELED']['sh']:+.2f} <- il numero citato da Β§50") + print(f" Sharpe forward della stessa serie COMPOSTA A GIORNALIERA (x sqrt(365,25)): " + f"{dsh:+.2f}") + print(" Ogni altro numero di sleeve del progetto (TP01 1,29 Β· SKH01 1,45 Β· il libro 1,81)") + print(" e' sulla lente GIORNALIERA. Confrontare +2,04 con quelli mescola due lenti: il") + print(" numero comparabile e' " f"{dsh:+.2f}. (La differenza e' autocorrelazione oraria:") + print(" una posizione tenuta 24/7 non fa 24 scommesse indipendenti al giorno.)") + res["daily_sharpe"] = dsh + + sub("3a) quanto ci vuole? MDE (80% potenza, 5% bilaterale) per orizzonte") + print(f" {'anni':>7}{'giorni':>9}{'SE(Sh)':>9}{'MDE 80%':>10}" + f"{'P(Sh_oss>0 | vero 1.2)':>25}{'P(Sh_oss>0 | vero 0.6)':>25}") + for yrs in (res["MODELED"]["yrs"], 0.25, 0.5, 1.0, 2.0, 3.0, 5.0): + se0 = _se_sharpe(0.0, yrs) + mde = 2.802 * se0 + pw12 = norm.cdf(1.2 / _se_sharpe(1.2, yrs)) + pw06 = norm.cdf(0.6 / _se_sharpe(0.6, yrs)) + print(f" {yrs:>7.2f}{yrs*365.25:>9.0f}{se0:>9.2f}{mde:>10.2f}{pw12:>24.1%}{pw06:>24.1%}") + need12 = 2.802 ** 2 * (1 + 1.2 ** 2 / 2) / 1.2 ** 2 + need06 = 2.802 ** 2 * (1 + 0.6 ** 2 / 2) / 0.6 ** 2 + print(f"\n -> per DISTINGUERE uno Sharpe vero di 1,2 dal nulla all'80% di potenza servono " + f"{need12:.1f} anni; per 0,6 ne servono {need06:.1f}.") + print(" -> CONSEGUENZA PER IL GATE: nessuna soglia sullo Sharpe forward puo' essere") + print(" l'evidenza principale. Una soglia ALTA sarebbe finta precisione (stessa lezione") + print(" scritta per DVOLSPREAD); una soglia BASSA vale solo come KILL, perche' li' la") + print(" bassa potenza lavora nel verso giusto: si uccide solo cio' che e' molto negativo.") + print(f" -> P(un edge VIVO a Sharpe 1,2 venga ucciso da una soglia a {KILL_SHARPE:+.2f} " + f"a 1 anno) = {norm.cdf((KILL_SHARPE-1.2)/_se_sharpe(1.2,1.0)):.2%}") + return res + + +# ============================================================================================= +# 4) `study_family_honest` β€” selezione in-sample-only -> DSR -> marginal_vs_tp01 +# ============================================================================================= +def sezione4(grid: list, tfs: tuple) -> dict: + sez("4) LA FAMIGLIA ONESTA β€” la cella scelta AL BUIO e' quella che gira nel monitor?") + print(" Si usano i TRE componenti REALI di `study_family_honest` (importati, non riscritti):") + print(" `select_cell_insample` -> `deflated_sharpe` -> `study_marginal`. Sono chiamati in") + print(" sequenza invece che tramite il wrapper solo per non ricalcolare la griglia due") + print(" volte; l'IDENTITA' della composizione col wrapper e' verificata sotto (4d).") + + sub("4a) controllo di replica: la cella congelata E' `prevday_breakout.target`?") + for a in ASSETS: + df = al.get(a, "1h") + d1 = np.nan_to_num(np.asarray(pb.target(df), float)) + d2 = np.nan_to_num(np.asarray(frozen_fn()(df), float)) + md = float(np.max(np.abs(d1 - d2))) + print(f" {a}: max|factory(FROZEN) - pb.target| = {md:.1e} " + f"{'βœ… bit-exact' if md == 0.0 else '❌ DIVERGE'}") + assert md == 0.0, "la factory non riproduce il segnale congelato" + cz = al.causality_ok(frozen_fn(), tf="1h") + print(f" causality_ok: {cz}") + + sub(f"4b) selezione IN-SAMPLE-ONLY su {len(grid)*len(tfs)} celle (pre-{HOLDOUT.date()})") + t = time.time() + sel = al.select_cell_insample(factory, grid, tfs) + ch = sel["chosen"] + print(f" {clock()} {len(sel['all_full_sharpe'])} celle valutate in {time.time()-t:.0f}s") + rows = sel["rows"] + fro = [r for r in rows if r["tf"] == FROZEN_TF and r["params"] == FROZEN] + rank_fro = rows.index(fro[0]) + 1 if fro else None + print(f"\n cella scelta AL BUIO : tf={ch['tf']} {ch['params']} " + f"Sh in-sample {ch['insample_sharpe']:+.3f} Sh FULL {ch['full_sharpe']:+.3f}") + if fro: + print(f" cella CONGELATA : tf={FROZEN_TF} {FROZEN} " + f"Sh in-sample {fro[0]['insample_sharpe']:+.3f} Sh FULL {fro[0]['full_sharpe']:+.3f}") + print(f" rango della congelata in-sample: {rank_fro}/{len(rows)}") + uguale = bool(fro and ch["tf"] == FROZEN_TF and ch["params"] == FROZEN) + print(f"\n ➜ LA CELLA AL BUIO {'COINCIDE' if uguale else 'NON COINCIDE'} CON QUELLA CHE GIRA.") + + print(f"\n {'top-10 IN-SAMPLE':<34}{'tf':>4}{'Sh IS':>9}{'Sh FULL':>10}") + for r in rows[:10]: + p = r["params"] + lab = f"anchor={p['anchor']} k={p['k']:.2f} short={int(p['short'])} mh={p['mh']}" + star = " <= CONGELATA" if (r["tf"] == FROZEN_TF and p == FROZEN) else "" + print(f" {lab:<34}{r['tf']:>4}{r['insample_sharpe']:>+9.3f}{r['full_sharpe']:>+10.3f}{star}") + + sub("4c) il candidato al buio passa `marginal_vs_tp01` e il deflated-Sharpe?") + fn = factory(tf=ch["tf"], **ch["params"]) + daily = al.candidate_daily(fn, tf=ch["tf"]) + sm = al.study_marginal(f"PREVDAY ISpick {ch['params']}", fn, tf=ch["tf"]) + dsr, sr0 = al.deflated_sharpe(al._sh(daily), sel["all_full_sharpe"], daily) + print(al.fmt_marginal(sm)) + print(f"\n deflated-Sharpe (N={len(sel['all_full_sharpe'])}, famiglia): {dsr:.3f} " + f"[soglia {GATE_DSR_MIN}] massimo atteso dal solo rumore {sr0:.3f}") + print(f" earns_slot_honest = {bool(sm['earns_slot'] and dsr >= GATE_DSR_MIN)}") + + sub("4d) IDENTITA': la mia composizione == `study_family_honest`?") + tiny = [dict(anchor=1, k=0.30, short=True, mh=0), dict(anchor=1, k=0.20, short=True, mh=0)] + w = al.study_family_honest("identita", factory, tiny, ("1h",)) + s2 = al.select_cell_insample(factory, tiny, ("1h",)) + c2 = s2["chosen"] + f2 = factory(tf=c2["tf"], **c2["params"]) + d2 = al.candidate_daily(f2, tf=c2["tf"]) + dsr2, _ = al.deflated_sharpe(al._sh(d2), s2["all_full_sharpe"], d2) + same = (w["chosen"]["params"] == c2["params"] and + abs((w["deflated_sharpe"] or 0) - round(dsr2, 3)) < 1e-9) + print(f" wrapper: cella {w['chosen']['params']} DSR {w['deflated_sharpe']} | " + f"composizione: {c2['params']} DSR {round(dsr2,3)} -> {'βœ… IDENTICI' if same else '❌ DIVERGONO'}") + + # la cella CONGELATA, valutata con gli stessi strumenti (e' quella che gira nel monitor) + sub("4e) e la cella CONGELATA (quella che gira), giudicata con gli stessi strumenti") + dfroz = al.candidate_daily(frozen_fn(), tf=FROZEN_TF) + smf = al.study_marginal("PREVDAY CONGELATA", frozen_fn(), tf=FROZEN_TF) + dsrf, sr0f = al.deflated_sharpe(al._sh(dfroz), sel["all_full_sharpe"], dfroz) + print(f" Sh FULL {al._sh(dfroz):+.3f} Sh IS {al._sh(dfroz[dfroz.index=HOLDOUT]):+.3f} maxDD {100*al._dd_ret(dfroz):.1f}%") + print(f" abs={smf['abs_grade']} marginal={smf['marginal_verdict']} " + f"earns_slot={smf['earns_slot']} robust_oos={smf['marginal'].get('robust_oos')} " + f"is_hedge={smf['marginal'].get('is_hedge')}") + print(f" deflated-Sharpe (N={len(sel['all_full_sharpe'])}): {dsrf:.3f}") + print(f" ⚠️ la congelata NON e' selezionabile in-sample (rango {rank_fro}/{len(rows)}): il suo") + print(" DSR e' riportato per completezza, non come verdetto β€” il verdetto e' 4c.") + + sub("4f) LE DUE CELLE A CONFRONTO β€” non sono la stessa strategia") + mb, mf = sm["marginal"], smf["marginal"] + print(f" {'':<24}{'AL BUIO (in-sample)':>22}{'CONGELATA (monitor)':>22}") + rr = [("tf / parametri", f"{ch['tf']} k={ch['params']['k']:.2f} " + f"short={int(ch['params']['short'])}", + f"{FROZEN_TF} k={FROZEN['k']:.2f} short={int(FROZEN['short'])}"), + ("rango in-sample", f"1/{len(rows)}", f"{rank_fro}/{len(rows)}"), + ("Sharpe FULL", f"{al._sh(daily):+.3f}", f"{al._sh(dfroz):+.3f}"), + ("Sharpe HOLD-OUT", f"{al._sh(daily[daily.index>=HOLDOUT]):+.3f}", + f"{al._sh(dfroz[dfroz.index>=HOLDOUT]):+.3f}"), + ("maxDD", f"{100*al._dd_ret(daily):.1f}%", f"{100*al._dd_ret(dfroz):.1f}%"), + ("corr -> TP01 (full)", f"{mb.get('corr_full')}", f"{mf.get('corr_full')}"), + ("corr -> TP01 (hold)", f"{mb.get('corr_hold')}", f"{mf.get('corr_hold')}"), + ("beta a TP01", f"{mb.get('beta_to_tp01')}", f"{mf.get('beta_to_tp01')}"), + ("uplift w25 full/hold", + f"{mb['blends']['w25']['uplift_full']:+.3f}/{mb['blends']['w25']['uplift_hold']:+.3f}", + f"{mf['blends']['w25']['uplift_full']:+.3f}/{mf['blends']['w25']['uplift_hold']:+.3f}"), + ("deflated-Sharpe N=336", f"{dsr:.3f}", f"{dsrf:.3f}")] + for nm, a_, b_ in rr: + print(f" {nm:<24}{a_:>22}{b_:>22}") + print() + print(" 🚨 IL PUNTO, ed e' il risultato principale di questa sezione: la selezione onesta") + print(f" sceglie il libro LONG-FLAT (short={int(ch['params']['short'])}), mentre il monitor") + print(" gira il libro SIMMETRICO. Il docstring della scoperta dice l'OPPOSTO in chiaro:") + print(" Β«LONG-SHORT is the slot-earner ... the long-flat sibling correlates ~0.64 to TP01") + print(" (it just re-rides the bull) -> a much smaller marginal upliftΒ».") + print(" La gamba SHORT β€” cioe' l'intera ragione per cui PREVDAY fu promosso a LEAD β€” e'") + print(" esattamente cio' che la selezione in-sample-only NON compra.") + print(" ⚠️ E il rovescio, che va detto: la cella al buio sta a corr " + f"{mb.get('corr_full')} da TP01 contro {mf.get('corr_full')} della congelata. Passa") + print(" `marginal_vs_tp01` (ADDS, alpha residua " + f"{mb.get('alpha_ann'):+.1%}/anno), ma e' molto piu' vicina a TP01: il candidato che") + print(" sopravvive alla selezione onesta e' anche quello che diversifica di meno.") + + sub("4g) `implausible_sharpe` su entrambe (retroattivo, come Β§26 su tutti gli sleeve)") + for nm, dd_ in (("AL BUIO", daily), ("CONGELATA", dfroz)): + i = al.implausible_sharpe(dd_) + print(f" {nm:<12}implausible={i['implausible']} Sharpe {i['sharpe']:.2f} " + f"maxDD {100*i['maxdd']:.1f}% perdite/attive {i['loss_frac']:.1%} " + f"Calmar {i['calmar']:.1f} reasons={i['reasons']}") + return dict(sel=sel, chosen=ch, daily=daily, sm=sm, dsr=dsr, sr0=sr0, + daily_frozen=dfroz, sm_frozen=smf, dsr_frozen=dsrf, + uguale=uguale, rank_frozen=rank_fro) + + +# ============================================================================================= +# 5) DSR β€” sensibilita' del verdetto al CONTEGGIO e alla PARTIZIONE +# ============================================================================================= +def _synth_trials(n: int, sd: float, center: float) -> np.ndarray: + """Array sintetico di n Sharpe con deviazione standard CAMPIONARIA esattamente `sd`. + Serve a riparametrizzare `al.deflated_sharpe` per (N, sd) usando la SUA formula, non una + mia copia. L'identita' col caso reale e' verificata in 5a.""" + z = np.linspace(-1.0, 1.0, n) + z = (z - z.mean()) / z.std(ddof=1) + return center + sd * z + + +def sezione5(S4: dict, F: dict) -> dict: + sez("5) DEFLATED-SHARPE β€” il verdetto dipende dal CONTEGGIO e dalla PARTIZIONE") + print(" Il 22/08 tre agenti indipendenti hanno misurato che `altlib.deflated_sharpe` non") + print(" dipende solo da N ma dalla VARIANZA della griglia (sr0 = sd(trial) x mult(N)): su") + print(" una famiglia omogenea passa quasi sempre, e la STESSA griglia da' verdetti opposti") + print(" secondo come la si partiziona. Qui la sensibilita' e' PUBBLICATA, non nascosta.") + sel, daily = S4["sel"], S4["daily"] + allsr = np.array([s for s in sel["all_full_sharpe"] if np.isfinite(s)], float) + sh = al._sh(daily) + rows = sel["rows"] + + sub("5a) controllo: la riparametrizzazione (N, sd) riproduce il caso reale?") + d_real, sr0_real = al.deflated_sharpe(sh, list(allsr), daily) + d_syn, sr0_syn = al.deflated_sharpe(sh, list(_synth_trials(len(allsr), allsr.std(ddof=1), + allsr.mean())), daily) + print(f" reale N={len(allsr)} sd={allsr.std(ddof=1):.3f} -> DSR {d_real:.4f} (sr0 {sr0_real:.3f})") + print(f" sint. N={len(allsr)} sd={allsr.std(ddof=1):.3f} -> DSR {d_syn:.4f} (sr0 {sr0_syn:.3f})") + print(f" scarto {abs(d_real-d_syn):.1e} {'βœ…' if abs(d_real-d_syn) < 1e-9 else '⚠️ non identici'}") + + sub("5b) PARTIZIONI della STESSA griglia β€” ognuna e' difendibile, e danno verdetti diversi") + def part(nome, pred): + sub_rows = [r for r in rows if pred(r)] + srs = [r["full_sharpe"] for r in sub_rows] + if len(srs) < 2: + return None + d, s0 = al.deflated_sharpe(sh, srs, daily) + return dict(nome=nome, n=len(srs), sd=float(np.std(srs, ddof=1)), dsr=d, sr0=s0) + giugno = [dict(anchor=1, k=0.30, short=True, mh=0), dict(anchor=1, k=0.50, short=True, mh=0), + dict(anchor=1, k=0.75, short=True, mh=0), dict(anchor=1, k=1.00, short=True, mh=0), + dict(anchor=2, k=0.30, short=True, mh=0), dict(anchor=3, k=0.30, short=True, mh=0), + dict(anchor=5, k=0.30, short=True, mh=0), dict(anchor=1, k=0.30, short=True, mh=24), + dict(anchor=1, k=0.30, short=True, mh=72), dict(anchor=2, k=0.75, short=True, mh=24), + dict(anchor=1, k=0.30, short=False, mh=0)] + parts = [ + part("N_giugno (le 11 celle gia' spese)", + lambda r: r["tf"] == "1h" and r["params"] in giugno), + part("solo l'asse k (anchor=1, mh=0, short)", + lambda r: r["tf"] == "1h" and r["params"]["anchor"] == 1 + and r["params"]["mh"] == 0 and r["params"]["short"]), + part("libro simmetrico, senza min-hold, 1h", + lambda r: r["tf"] == "1h" and r["params"]["mh"] == 0 and r["params"]["short"]), + part("tutto 1h", lambda r: r["tf"] == "1h"), + part("LA FAMIGLIA DICHIARATA (1h + 4h)", lambda r: True), + ] + print(f" {'partizione':<40}{'N':>5}{'sd(Sh)':>9}{'sr0':>8}{'DSR':>9}{'verdetto':>10}") + for p in parts: + if p is None: + continue + print(f" {p['nome']:<40}{p['n']:>5}{p['sd']:>9.3f}{p['sr0']:>8.3f}{p['dsr']:>9.3f}" + f"{('PASS' if p['dsr'] >= GATE_DSR_MIN else 'FAIL'):>10}") + + sub("5c) sr0 di SCREEN β€” non basta contare i trial, serve la VARIANZA di screen") + lb = F["lb"] + scr = np.array([x["abs_full_sharpe"] for x in lb], float) + print(f" i {len(scr)} agenti dell'onda intraday 2026-06-21: Sharpe da {scr.min():.2f} a " + f"{scr.max():.2f}, media {scr.mean():.2f}, sd {scr.std(ddof=1):.3f}") + print(" ⚠️ lente DICHIARATA: `abs_full_sharpe` del leaderboard e' la Sharpe per-asset del") + print(" giudice di giugno, non la 50/50 giornaliera di `candidate_daily`. Serve per la") + print(" sua DISPERSIONE, non per il livello.") + mix = np.concatenate([allsr, scr]) + d_scr, s0_scr = al.deflated_sharpe(sh, list(mix), daily) + print(f" famiglia+screen: N={len(mix)} sd={mix.std(ddof=1):.3f} sr0={s0_scr:.3f} " + f"DSR={d_scr:.3f} {'PASS' if d_scr >= GATE_DSR_MIN else 'FAIL'}") + + sub("5d) la superficie DSR(N, sd) β€” dove sta il verdetto, e quanto e' fragile") + sds = (0.15, 0.20, 0.25, 0.30, 0.40, 0.58) + ns = (11, 56, 168, 336, 504, 456, 560) + print(" " + "N su sd".rjust(8) + "".join(f"{s:>9.2f}" for s in sds)) + for n in sorted(ns): + cells = [] + for s in sds: + dd, _ = al.deflated_sharpe(sh, list(_synth_trials(n, s, float(allsr.mean()))), daily) + cells.append(f"{dd:>9.3f}") + print(f" {n:>8}" + "".join(cells)) + print(" (sd=0,58 e' il valore implicito dell'ORTHO-SCREEN del 22/08; la riga N=456 e' la") + print(f" famiglia+screen di 5c. Soglia {GATE_DSR_MIN}.)") + + sub("5e) ➜ IL VERDETTO NON SI RIBALTA COL CONTEGGIO. Si ribalta con la VARIANZA.") + lo_, hi_ = 0.05, 3.0 + for _ in range(60): + mid = 0.5 * (lo_ + hi_) + dd, _s = al.deflated_sharpe(sh, list(_synth_trials(len(allsr), mid, + float(allsr.mean()))), daily) + if dd >= GATE_DSR_MIN: + lo_ = mid + else: + hi_ = mid + print(f" A N={len(allsr)} il DSR scende sotto {GATE_DSR_MIN} solo per sd(trial) >= " + f"{0.5*(lo_+hi_):.3f}.") + print(f" La famiglia ne ha {allsr.std(ddof=1):.3f}: servirebbe una dispersione " + f"{0.5*(lo_+hi_)/allsr.std(ddof=1):.1f}x per bocciarla.") + print(" Portare N da 11 a 560 costa " + + f"{al.deflated_sharpe(sh, list(_synth_trials(11, allsr.std(ddof=1), float(allsr.mean()))), daily)[0] - al.deflated_sharpe(sh, list(_synth_trials(560, allsr.std(ddof=1), float(allsr.mean()))), daily)[0]:.3f}" + " di DSR: il conteggio, su questa famiglia, NON e' la leva.") + print(" ⚠️ ATTESA A PRIORI A2 REFUTATA. Avevo previsto che il verdetto si ribaltasse col") + print(" conteggio (come VRP01 il 30/07: 0,983 a N=8 / 0,948 a N=72). Qui NON succede, e") + print(" il motivo e' MISURABILE: la famiglia PREVDAY e' OMOGENEA (le sue celle si") + print(" assomigliano, sd 0,27) mentre la griglia VRP01 e la griglia ORTHO erano") + print(" eterogenee (sd ~0,58). `sr0 = sd(trial) x mult(N)` e mult(N) cresce come una") + print(" radice di log: da N=11 a N=560 vale ~1,8x, mentre sd puo' variare 4x fra") + print(" famiglie. ➜ REGOLA: il deflated-Sharpe di una famiglia OMOGENEA e' quasi") + print(" INSENSIBILE al numero di trial β€” dichiarare 11 celle o 560 cambia 0,009. Chi") + print(" discute il CONTEGGIO su una famiglia stretta sta discutendo la variabile") + print(" sbagliata; l'unica leva e' la DISPERSIONE, che si sceglie decidendo dove") + print(" mettere il bordo della famiglia. ⚠️ Cio' NON vuol dire che il gate sia vacuo:") + print(" 5f mostra che ha potenza sul LIVELLO (la cella mediana della stessa famiglia") + print(" FALLISCE). E' cieco su un asse e vede sull'altro β€” e il 30/07 il verdetto su") + print(" VRP01 si ribaltava proprio perche' quella famiglia era larga.") + + sub("5f) controllo positivo del gate β€” passerebbe anche una cella MEDIOCRE?") + med = float(np.median(allsr)) + d_med, _ = al.deflated_sharpe(med, list(allsr), daily) + d_lo, _ = al.deflated_sharpe(float(np.percentile(allsr, 25)), list(allsr), daily) + print(f" candidato con Sharpe = MEDIANA della famiglia ({med:+.3f}): DSR {d_med:.3f} " + f"-> {'PASS' if d_med >= GATE_DSR_MIN else 'FAIL'}") + print(f" candidato con Sharpe = 25o pctl della famiglia ({np.percentile(allsr,25):+.3f}): " + f"DSR {d_lo:.3f} -> {'PASS' if d_lo >= GATE_DSR_MIN else 'FAIL'}") + print(" (approssimazione DICHIARATA: si tiene la serie del vincitore per skew/kurt/T e si") + print(" sostituisce solo lo Sharpe. Se anche la mediana passa, il gate su questa famiglia") + print(" non sta selezionando: sta contando.)") + return dict(parts=[p for p in parts if p], dsr_screen=d_scr, allsr=allsr, sh=sh, + sd_soglia=0.5 * (lo_ + hi_), dsr_mediana=d_med) + + +# ============================================================================================= +# 6) `day_boundary_robust` β€” il segnale VIVE sul confine del giorno UTC +# ============================================================================================= +def _hour_of_day_signal(df: pd.DataFrame) -> np.ndarray: + """CONTROLLO POSITIVO: segnale PURAMENTE di calendario (long la prima meta' del giorno UTC, + short la seconda). Deve INVERTIRSI spostando il confine di 12h. Se non si inverte, il test + non ha potenza e il verdetto su PREVDAY non vale niente.""" + h = pd.to_datetime(df["datetime"], utc=True).dt.hour.values + d = np.where(h < 12, 1.0, -1.0) + return np.nan_to_num(pb._vol_target(d, df, pb.TARGET_VOL, pb.VOL_WIN_DAYS, pb.LEV_CAP), nan=0.0) + + +def sezione6() -> dict: + sez("6) `day_boundary_robust` β€” PREVDAY e' un segnale di prezzo o di ETICHETTA?") + print(" Il livello di PREVDAY E' fissato dal rollover di mezzanotte UTC: e' esattamente la") + print(" classe di segnali per cui questo gate e' nato (open_drive, 2026-06-21). Il") + print(" leaderboard di giugno lo dichiara ROBUST con spread 0,196 β€” cioe' NON invariante.") + print(" Qui si ri-misura invece di fidarsi, agli offset richiesti +-2 / +-4 / +-8 ore.") + + sub("6a) controllo positivo β€” il test HA potenza?") + ctl = al.day_boundary_robust(_hour_of_day_signal, tf="1h", offsets=(0, 6, 12, 18)) + print(f" segnale puramente di calendario (long h<12): {ctl['per_offset']} " + f"spread {ctl['spread']} verdetto {ctl['verdict']}") + ok_ctl = ctl["verdict"] == "ARTIFACT-RISK" + print(f" {'βœ… il gate lo SMASCHERA' if ok_ctl else '⚠️ il gate NON lo smaschera'} " + f"(atteso ARTIFACT-RISK: a +12h il segnale e' il proprio opposto)") + + sub("6b) PREVDAY congelato, offset richiesti") + r = al.day_boundary_robust(frozen_fn(), tf="1h", offsets=(-8, -4, -2, 0, 2, 4, 8)) + for o in sorted(r["per_offset"]): + print(f" {o:>+4}h uplift w25 {r['per_offset'][o]:+.3f}") + print(f" a 0h {r['per_offset'][0]:+.3f} min {r['min']:+.3f} max {r['max']:+.3f} " + f"spread {r['spread']:.3f} verdetto {r['verdict']}") + print(f" ⚠️ il campo `base` di altlib e' per_offset[offsets[0]] = l'offset {(-8):+d}h, NON lo") + print(" zero: con una griglia che comincia in negativo chiamarlo 'base' inganna.") + print(f" -> l'uplift {'NON cambia segno' if r['robust_to_boundary'] else 'CAMBIA SEGNO'}: " + f"{'non e' + chr(39) + ' un artefatto di etichettatura' if r['robust_to_boundary'] else 'ARTEFATTO'}") + + sub("6c) griglia oraria PIENA: 24 confini-giorno (standalone + uplift)") + B = al.tp01_baseline_daily() + per = {} + for off in PV_BOUNDARY: + series = {} + for a in ASSETS: + df0 = al.get(a, "1h") + tgt = frozen_fn()(al._shift_calendar(df0, off) if off else df0) + ev = al.eval_weights(df0, tgt) + series[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + cand = to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)) + JJ = pd.concat({"B": B, "C": cand}, axis=1, join="inner").dropna() + per[off] = dict(full=al._sh(cand), hold=al._sh(cand[cand.index >= HOLDOUT]), + dd=al._dd_ret(cand), + upl=al._sh(0.75 * JJ["B"] + 0.25 * JJ["C"]) - al._sh(JJ["B"]), + daily=cand) + fu = np.array([per[o]["full"] for o in PV_BOUNDARY]) + ho = np.array([per[o]["hold"] for o in PV_BOUNDARY]) + up = np.array([per[o]["upl"] for o in PV_BOUNDARY]) + dd = np.array([per[o]["dd"] for o in PV_BOUNDARY]) + print(f" {'metrica':<16}{'canonico (0h)':>15}{'pctl':>7}{'MEDIANA onesta':>17}" + f"{'[p10, p90]':>20}{'fortuna':>10}") + for nm, v in (("Sharpe FULL", fu), ("Sharpe HOLD-OUT", ho), ("uplift w25", up)): + pc = float((v < v[0]).mean() * 100) + print(f" {nm:<16}{v[0]:>+15.3f}{pc:>6.0f}Β°{np.median(v):>+17.3f}" + f" [{np.percentile(v,10):+.3f}, {np.percentile(v,90):+.3f}]{v[0]-np.median(v):>+10.3f}") + pcd = float((dd > dd[0]).mean() * 100) + print(f" {'maxDD':<16}{100*dd[0]:>14.1f}%{pcd:>6.0f}Β°{100*np.median(dd):>16.1f}%" + f" [{100*np.percentile(dd,10):.1f}%, {100*np.percentile(dd,90):.1f}%]" + f"{100*(dd[0]-np.median(dd)):>+9.1f}") + print(" (pctl = quante ancore stanno SOTTO la canonica; per il maxDD, quante stanno sopra") + print(" cioe' peggio. `anchor_luck_band` di altlib fa lo stesso conto: la mediana e' la") + print(" stima onesta, la differenza canonico-mediana e' la fortuna.)") + return dict(req=r, per=per, ctl_ok=ok_ctl) + + +# ============================================================================================= +# 7) BANDA D'ANCORA DEL LIBRO β€” differenze APPAIATE sullo spazio congiunto +# ============================================================================================= +def prevday_daily_off(off: int, rate: dict | None) -> pd.Series: + """Serie giornaliera 50/50 di PREVDAY al confine-giorno `off`, col FUNDING dentro. + Replica ESATTA di r0823_book_3rd.prevday() a off=0 (verificata bit-exact in 7a).""" + series = {} + for a in ASSETS: + df = al.get(a, "1h") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + tgt = np.nan_to_num(np.asarray( + frozen_fn()(al._shift_calendar(df, off) if off else df), float)) + r = al.simple_returns(df["close"].values.astype(float)) + pos = np.zeros(len(tgt)) + pos[1:] = tgt[:-1] + net = pos * r - FEE_SIDE * np.abs(np.diff(pos, prepend=0.0)) + if rate is not None: + t = FU.to_ms(idx) + fh = FU.cumF(rate[a], t + FU.MSH) - FU.cumF(rate[a], t) + net = net - pos * fh + net[0] = 0.0 + series[a] = pd.Series(net, index=idx) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + return to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)) + + +def _stats(s: pd.Series): + m = metrics(s) + mh = metrics(s[s.index >= HOLDOUT]) + return m["sharpe"], mh["sharpe"], m["cagr"], m["maxdd"] + + +def sezione7(rate: dict, draws: int) -> dict: + sez("7) BANDA D'ANCORA DEL LIBRO β€” Β§50 ha de-luckato TP01 e SKH01, NON il candidato") + print(" Β§50 ha estratto TP01 x24 ancore e SKH01 x23 offset e ha tenuto PREVDAY FERMO al suo") + print(" confine canonico (`ANCHOR_OF['PREVDAY'] = (0,)`). Ma PREVDAY UN'ANCORA CE L'HA β€” e'") + print(" l'ora del rollover che fissa il livello β€” e la sezione 6c la misura fortunata. Un") + print(" delta e' un delta: eredita la fortuna dell'ancora (lezione 26/07). Qui lo spazio") + print(f" congiunto e' TP01 x{len(TP_HOURS)} Β· SKH01 x{len(SKH_OFFSETS)} Β· PREVDAY x{len(PV_BOUNDARY)} " + f"= {len(TP_HOURS)*len(SKH_OFFSETS)*len(PV_BOUNDARY):,} configurazioni.") + + if not B3RD_CACHE.exists(): + print(f" ⚠️ cache Β§50 assente ({B3RD_CACHE}) -> sezione 7 SALTATA (non e' un risultato).") + return {} + S = pickle.loads(B3RD_CACHE.read_bytes()) + + sub("7a) sanity obbligatorio: la mia PREVDAY a off=0 e' quella di Β§50?") + p0 = prevday_daily_off(0, rate) + ref = S["PREVDAY"][0] + j = pd.concat({"a": p0, "b": ref}, axis=1, join="inner") + md = float(np.max(np.abs(j["a"] - j["b"]))) + print(f" max|mia - Β§50| = {md:.2e} su {len(j):,} giorni " + f"{'βœ… bit-exact (e conferma che la cache ha il funding dentro)' if md < 1e-12 else '⚠️ DIVERGE'}") + + t = time.time() + PV = {o: (p0 if o == 0 else prevday_daily_off(o, rate)) for o in PV_BOUNDARY} + print(f" {clock()} PREVDAY x{len(PV_BOUNDARY)} confini-giorno costruita ({time.time()-t:.0f}s)") + + sub(f"7b) {draws} estrazioni CONGIUNTE β€” mediana delle DIFFERENZE APPAIATE") + rng = np.random.default_rng(SEED) + hs = rng.choice(TP_HOURS, size=draws) + os_ = rng.choice(SKH_OFFSETS, size=draws) + bs_ = rng.choice(PV_BOUNDARY, size=draws) + out = {w: {"sh": [], "shh": [], "cagr": [], "dd": []} for w in WEIGHTS_TESTED} + out_can = {w: {"sh": [], "shh": []} for w in WEIGHTS_TESTED} # candidato FERMO a off=0 + base = [] + for i in range(draws): + cols2 = {"TP01": S["TP01"][hs[i]], "SKH01": S["SKH01"][os_[i]]} + b = combine_outer(cols2, {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START) + bst = _stats(b) + base.append(bst) + for w in WEIGHTS_TESTED: + wm = {"TP01": W_TP * (1 - w), "SKH01": W_SKH * (1 - w), "CAND": w} + a = combine_outer({**cols2, "CAND": PV[bs_[i]]}, wm, lo=BOOK_START) + st = _stats(a) + out[w]["sh"].append(st[0] - bst[0]); out[w]["shh"].append(st[1] - bst[1]) + out[w]["cagr"].append(st[2] - bst[2]); out[w]["dd"].append(st[3] - bst[3]) + c = combine_outer({**cols2, "CAND": PV[0]}, wm, lo=BOOK_START) + sc = _stats(c) + out_can[w]["sh"].append(sc[0] - bst[0]); out_can[w]["shh"].append(sc[1] - bst[1]) + B = np.array(base) + print(f" libro live de-luckato (2 sleeve): ShFULL {np.median(B[:,0]):.3f} " + f"ShHOLD {np.median(B[:,1]):.3f} CAGR {100*np.median(B[:,2]):.2f}% " + f"maxDD {100*np.median(B[:,3]):.2f}%") + print(f"\n {'peso':>6}{'dShFULL':>11}{'>0':>6}{'[p10,p90]':>19}{'dShHOLD':>11}{'>0':>6}" + f"{'d CAGR pp':>11}{'d maxDD pp':>12}{' | Β§50 (cand. FERMO a 0h)':>28}") + res = {} + for w in WEIGHTS_TESTED: + a = np.array(out[w]["sh"]); ah = np.array(out[w]["shh"]) + ac = np.array(out[w]["cagr"]); ad = np.array(out[w]["dd"]) + c = np.array(out_can[w]["sh"]); ch = np.array(out_can[w]["shh"]) + res[w] = dict(sh=float(np.median(a)), sh_pos=float((a > 0).mean()), + shh=float(np.median(ah)), shh_pos=float((ah > 0).mean()), + cagr=float(np.median(ac)), dd=float(np.median(ad)), + sh_can=float(np.median(c)), shh_can=float(np.median(ch))) + print(f" {w:>5.0%}{np.median(a):>+11.4f}{100*(a>0).mean():>5.0f}%" + f" [{np.percentile(a,10):+.3f},{np.percentile(a,90):+.3f}]" + f"{np.median(ah):>+11.4f}{100*(ah>0).mean():>5.0f}%" + f"{100*np.median(ac):>+11.2f}{100*np.median(ad):>+12.2f}" + f" FULL {np.median(c):+.3f} HOLD {np.median(ch):+.3f}") + w = W_PROPOSTO + print("\n βœ… REPLICA INDIPENDENTE DI Β§50: la colonna 'cand. FERMO a 0h' e' lo stesso conto") + print(f" che ha fatto Β§50 (candidato all'ancora canonica). Al 15% da' FULL " + f"{res[w]['sh_can']:+.3f} / HOLD {res[w]['shh_can']:+.3f} contro i +0,192 / +0,363") + print(" pubblicati -> macchineria e cache riprodotte, il confronto e' like-with-like.") + print(f"\n ➜ AL PESO PROPOSTO ({w:.0%}), de-luckare ANCHE IL CANDIDATO costa " + f"{res[w]['sh']-res[w]['sh_can']:+.4f} di dShFULL e " + f"{res[w]['shh']-res[w]['shh_can']:+.4f} di dShHOLD:") + print(f" dShFULL {res[w]['sh_can']:+.3f} -> {res[w]['sh']:+.3f} " + f"(-{100*(1-res[w]['sh']/res[w]['sh_can']):.0f}%) " + f"dShHOLD {res[w]['shh_can']:+.3f} -> {res[w]['shh']:+.3f} " + f"(-{100*(1-res[w]['shh']/res[w]['shh_can']):.0f}%)") + print(" πŸ“Œ Il contributo RESTA positivo al 100% delle estrazioni a ogni peso: il segno non") + print(" dipende dall'ancora, la TAGLIA si'. E' la stessa forma del 22/08 su GATE PROP-01:") + print(" Β«un gate superato non e' un numero confermatoΒ».") + return res + + +# ============================================================================================= +# 8) FUNDING β€” un long/short vol-targeted su perpetual: paga o incassa? +# ============================================================================================= +def sezione8(rate: dict) -> dict: + sez("8) FUNDING β€” il costo di TENERE APERTA una posizione, che nessun backtest storico ha") + print(" Il 22/08 ha misurato che il libro paga -2,16%/anno di drift e che ESPOSIZIONE e") + print(" FUNDING sono positivamente correlati (il tasso pagato davvero e' 1,9-3,4x") + print(" l'incondizionato). PREVDAY e' long-short: la gamba SHORT lo INCASSA. Quale vince?") + out = {} + print(f"\n {'asset':>6}{'%long':>8}{'%short':>8}{'|pos| medio':>12}{'incond./anno':>14}" + f"{'| cond. LONG':>14}{'rapp.':>7}{'| cond. SHORT':>15}{'rapp.':>7}{'NETTO/anno':>12}") + legs = {} + for a in ASSETS: + df = al.get(a, "1h") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + tgt = np.nan_to_num(np.asarray(frozen_fn()(df), float)) + pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1] + t = FU.to_ms(idx) + fh = FU.cumF(rate[a], t + FU.MSH) - FU.cumF(rate[a], t) + yrs = len(pos) / (24 * 365.25) + ser = rate[a]["series"] + ser = ser[(ser.index >= idx.min()) & (ser.index <= idx.max())] + unc = float(ser.mean()) * 24 * 365.25 + pl = np.maximum(pos, 0.0); ps = -np.minimum(pos, 0.0) + cl = float(np.sum(pl * fh) / pl.sum()) * 24 * 365.25 if pl.sum() > 0 else float("nan") + cs = float(np.sum(ps * fh) / ps.sum()) * 24 * 365.25 if ps.sum() > 0 else float("nan") + net = float(np.sum(pos * fh)) / yrs + legs[a] = dict(pos=pos, fh=fh, idx=idx, net=net, unc=unc, cl=cl, cs=cs) + print(f" {a:>6}{float((pos>0).mean()):>8.1%}{float((pos<0).mean()):>8.1%}" + f"{float(np.abs(pos).mean()):>12.3f}{unc:>14.2%}{cl:>14.2%}{cl/unc:>6.2f}x" + f"{cs:>15.2%}{cs/unc:>6.2f}x{net:>12.3%}") + sleeve = float(np.mean([legs[a]["net"] for a in ASSETS])) + out["sleeve_drag"] = sleeve + print(f" -> drag NETTO dello sleeve (le due gambe pesano 0,5 ciascuna): {sleeve:.3%}/anno") + print(f" -> al peso di libro proposto ({W_PROPOSTO:.0%}): {W_PROPOSTO*sleeve:.3%}/anno di libro") + + print("\n πŸ“Œ MECCANISMO: se `cond. LONG` e `cond. SHORT` sono ENTRAMBI sopra l'incondizionato,") + print(" le due gambe NON si compensano β€” la lunga paga caro E la corta incassa caro, e il") + print(" netto dipende da quanto tempo sta da ciascun lato. Se invece il rapporto della") + print(" corta e' < 1, la corta incassa POCO e il costo resta tutto sulla lunga.") + + sub("8b) l'effetto sui numeri di PREVDAY (unico grado di liberta': il funding)") + a0 = prevday_daily_off(0, None) + a1 = prevday_daily_off(0, rate) + print(f" {'lente':<22}{'Sh FULL':>10}{'Sh HOLD':>10}{'drift/anno':>13}{'maxDD':>9}") + for nm, s in (("senza funding", a0), ("CON funding", a1)): + r = np.asarray(s.dropna().values, float) + print(f" {nm:<22}{al._sh(s):>+10.3f}{al._sh(s[s.index>=HOLDOUT]):>+10.3f}" + f"{r.mean()*365.25:>12.2%}{100*al._dd_ret(s):>8.1f}%") + r0 = np.asarray(a0.dropna().values, float).mean() * 365.25 + r1 = np.asarray(a1.dropna().values, float).mean() * 365.25 + print(f" -> il funding costa a PREVDAY {r0-r1:.2%}/anno di drift e " + f"{al._sh(a0)-al._sh(a1):+.3f} di Sharpe FULL") + out["d_drift"] = r0 - r1 + return out + + +# ============================================================================================= +# 9) `weights_tilt_null` β€” il gate del progetto per ogni cambio di pesi +# ============================================================================================= +def sezione9(rate: dict) -> dict: + sez("9) `weights_tilt_null` β€” e la colonna che conta non e' il PASS") + if not B3RD_CACHE.exists(): + print(" cache Β§50 assente -> sezione SALTATA") + return {} + S = pickle.loads(B3RD_CACHE.read_bytes()) + cols = {"TP01": S["TP01"][0], "SKH01": S["SKH01"][0], "PREVDAY": prevday_daily_off(0, rate)} + k_seen = 7 * 4 + len(WEIGHTS_TESTED) # Β§50: 7 candidati x 4 pesi; qui: 4 pesi + print(f" k_seen dichiarato = {k_seen} (le {7*4} configurazioni guardate sull'hold-out in Β§50") + print(f" piu' le {len(WEIGHTS_TESTED)} di oggi: i pesi gia' visti si CONTANO, non si azzerano)") + w_cur = {"TP01": W_TP, "SKH01": W_SKH, "PREVDAY": 0.0} + print(f"\n {'peso':>6}{'d_insample':>13}{'d_hold':>10}{'d_full':>10}{'pctl_hold':>11}" + f"{'best-of-k':>11}{'rnd>cur':>10}{'GATE':>8}") + res = {} + for w in WEIGHTS_TESTED: + wp = {"TP01": W_TP * (1 - w), "SKH01": W_SKH * (1 - w), "PREVDAY": w} + r = weights_tilt_null(cols, w_cur, wp, floor=0.03, n=300, k_seen=k_seen) + res[w] = r + print(f" {w:>5.0%}{r['delta_insample']:>+13.4f}{r['delta_hold']:>+10.4f}" + f"{r['delta_full']:>+10.4f}{r['pctl_hold']:>10.1f}Β°{r['bestofk_pctl']:>10.1f}Β°" + f"{r['frac_random_beat_hold']:>10.2f}{('PASS' if r['gate_pass'] else 'FAIL'):>8}") + fr = res[W_PROPOSTO]["frac_random_beat_hold"] + print(f"\n ⚠️ `rnd>cur` = {fr:.2f}: e' la frazione di tilt CASUALI a 3 sleeve che battono il") + print(" libro attuale sull'hold-out. Dove vale ~0,9 il claim Β«PREVDAY migliora") + print(" l'hold-outΒ» e' GENERICO: lo fa quasi qualunque terzo sleeve, e il gate passa") + print(" proprio perche' la proposta NON e' eccezionale fra i tilt casuali.") + print(" `weights_tilt_null` e' NECESSARIO, non sufficiente (sta nel suo docstring).") + return res + + +# ============================================================================================= +# 10) IL GATE PRE-REGISTRATO +# ============================================================================================= +def sezione10(S4: dict, S5: dict, S6: dict, S7: dict, S8: dict, S9: dict, + M: dict, MDE: dict) -> None: + sez("10) IL GATE PRE-REGISTRATO β€” data, soglie dichiarate PRIMA, veto, kill") + inizio = pd.Timestamp(M["st"]["start_ts"], unit="ms", tz="UTC") + OGGI = pd.Timestamp.now("UTC").date() + d1 = pd.Timestamp("2027-06-21", tz="UTC") # 1 anno di forward dall'inception + ndecl = len(S4["sel"]["all_full_sharpe"]) + print(f" Inception del monitor: {inizio.date()}. Oggi: {OGGI}.") + print(f" Finestra forward attuale: {M['span_h']/24:.0f} giorni = " + f"{M['span_h']/24/365.25:.2f} anni (sez. 3: SE(Sharpe) ~ " + f"{_se_sharpe(MDE['MODELED']['sh'], MDE['MODELED']['yrs']):.1f}).") + + W = 86 + + def bx(t: str = "") -> None: + print(" |" + t.ljust(W) + "|") + + print(" +" + "-" * W + "+") + bx(" GATE PREVDAY-01".ljust(W)) + bx() + bx(f" DATA DI DECISIONE: {d1.date()}") + bx(" (= 1 anno esatto di forward dall'inception. La data NON e' scelta perche' a 1") + bx(" anno la finestra diventi informativa β€” la sez. 3 misura che NON lo diventa β€”") + bx(" ma perche' oltre quell'orizzonte tenere un lead senza decidere e' esso stesso") + bx(" una decisione. La prova che conta e' gia' tutta OGGI e non migliora aspettando.)") + bx() + bx(" SI PROMUOVE a sleeve del LIBRO LIVE al peso 15% solo se TUTTE:") + bx(f" (a) deflated-Sharpe sulla famiglia DICHIARATA (N={ndecl}) >= {GATE_DSR_MIN}, con la") + bx(" griglia ri-dichiarata PRIMA di guardare e la sensibilita' alla PARTIZIONE") + bx(" pubblicata accanto (sez. 5): se cambia la partizione, cambia il verdetto.") + bx(" (b) la cella scelta IN-SAMPLE-ONLY coincide con quella CONGELATA nel monitor,") + bx(" oppure il monitor viene ri-congelato su quella al buio e il forward") + bx(" RIPARTE DA ZERO da quel giorno (non si eredita il track di un'altra cella).") + bx(f" (c) delta di libro APPAIATO sullo spazio d'ancora CONGIUNTO (TP01 x{len(TP_HOURS)} Β·") + bx(f" SKH01 x{len(SKH_OFFSETS)} Β· PREVDAY x{len(PV_BOUNDARY)}) > {GATE_ANCHOR_DELTA_MIN:+.2f} di dShFULL al peso 15%,") + bx(f" positivo in >= {GATE_ANCHOR_FRAC_MIN:.0%} delle estrazioni, col FUNDING dentro.") + bx(" (d) `weights_tilt_null` gate_pass=True al peso proposto.") + bx(" (e) `marginal_vs_tp01` = ADDS, robust_oos, non-hedge SULLA CELLA AL BUIO.") + bx(" (f) `day_boundary_robust` != ARTIFACT-RISK sulla cella al buio.") + bx(" (g) Sharpe forward > 0 β€” soglia DEBOLE DI PROPOSITO: a 1 anno SE(Sharpe)~1,0 e") + bx(" una soglia alta sarebbe finta precisione (stessa scelta di DVOLSPREAD).") + bx() + bx(" VETO D'INTEGRITA' (si valuta PRIMA del resto e BLOCCA la decisione):") + bx(f" quota di barre forward RICOSTRUIBILI dal feed certificato >= {GATE_INTEGRITY_MIN:.0%} (sez. 2b),") + bx(" e i minuti di mercato NON registrati per giorno non devono CRESCERE rispetto a") + bx(" oggi. Sotto la soglia il gate SI ESTENDE, non si decide: una serie che non si") + bx(" ricostruisce non e' evidenza ne' a favore ne' contro.") + bx() + bx(f" KILL: Sharpe forward < {KILL_SHARPE:+.2f} su >= 180 giorni di barre ATTIVE (non di") + bx(" calendario). A quell'orizzonte la bassa potenza lavora nel verso giusto: si") + bx(" uccide solo cio' che e' molto negativo. Al kill il lead si chiude e la famiglia") + bx(" NON si ri-ottimizza (riaprirla dopo aver visto il forward e' selezione-") + bx(" sull'hold-out di secondo ordine).") + bx() + bx(" COSA NON SPOSTA IL GATE: un forward positivo. La sez. 3 misura che a 1 anno un") + bx(" Sharpe forward di +2 e uno di 0 non sono distinguibili. Il forward serve a") + bx(" UCCIDERE, non a promuovere.") + print(" +" + "-" * W + "+") + + sub("STATO OGGI delle condizioni β€” e' una FOTOGRAFIA, non anticipa la decisione") + W_ = W_PROPOSTO + rows = [ + ("(a) DSR famiglia dichiarata", f"{S4['dsr']:.3f} (N={ndecl})", + S4["dsr"] >= GATE_DSR_MIN), + ("(a') DSR con sr0 di SCREEN", f"{S5['dsr_screen']:.3f}", + S5["dsr_screen"] >= GATE_DSR_MIN), + ("(a'') DSR della cella CONGELATA", f"{S4['dsr_frozen']:.3f} (non e' la cella di (a))", + S4["dsr_frozen"] >= GATE_DSR_MIN), + ("(b) cella al buio == congelata", "SI" if S4["uguale"] else + f"NO (la congelata e' {S4['rank_frozen']}/{len(S4['sel']['rows'])} in-sample)", + S4["uguale"]), + ("(c) delta libro appaiato de-luckato", + (f"{S7[W_]['sh']:+.4f} FULL, >0 nel {S7[W_]['sh_pos']:.0%}") if S7 else "n/d", + bool(S7 and S7[W_]["sh"] > GATE_ANCHOR_DELTA_MIN + and S7[W_]["sh_pos"] >= GATE_ANCHOR_FRAC_MIN)), + ("(d) weights_tilt_null", ("PASS" if S9 and S9[W_]["gate_pass"] else "FAIL") + + (f" (rnd>cur {S9[W_]['frac_random_beat_hold']:.2f})" if S9 else ""), + bool(S9 and S9[W_]["gate_pass"])), + ("(e) marginal cella al buio", S4["sm"]["marginal_verdict"], + S4["sm"]["marginal_verdict"] == "ADDS" and S4["sm"]["earns_slot"]), + ("(f) day_boundary_robust", f"{S6['req']['verdict']} (spread {S6['req']['spread']:.3f})", + S6["req"]["verdict"] != "ARTIFACT-RISK"), + ("(g) Sharpe forward > 0", f"{MDE['MODELED']['sh']:+.2f} (t={MDE['MODELED']['t']:.2f}, " + f"p={MDE['MODELED']['p']:.2f})", MDE["MODELED"]["sh"] > 0), + ("VETO integrita'", f"{M['frac_ok']:.1%} barre ricostruibili", + M["frac_ok"] >= GATE_INTEGRITY_MIN), + ] + for nm, val, ok in rows: + print(f" {'OK ' if ok else 'NO '} {nm:<38}{val}") + npass = sum(1 for _, _, o in rows if o) + print(f"\n {npass}/{len(rows)} condizioni soddisfatte OGGI.") + print(f" Il funding costa a PREVDAY {S8['sleeve_drag']:.2%}/anno di sleeve = " + f"{W_*S8['sleeve_drag']:.2%}/anno di libro al peso proposto (gia' dentro (c)).") + + +# ============================================================================================= +def load_rate() -> dict: + """Serie oraria del funding per asset (LINEARE dove esiste, INVERSE prima come proxy + DICHIARATO). Usa la cache parquet di r0822d_funding; --no-net la usa e basta.""" + F = {} + for instr, (start, _a, _k) in FU.INSTR.items(): + d = FU.fetch_funding(instr, start).copy() + d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True) + F[instr] = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt") + return FU.build_rate(F) + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--draws", type=int, default=300) + ap.add_argument("--no-net", action="store_true") + ap.add_argument("--fast", action="store_true") + args = ap.parse_args() + SCRATCH.mkdir(parents=True, exist_ok=True) + + print("=" * 97) + print(" Β§54 PREVDAY-GATE β€” il lead PREVDAY ha un gate pre-registrato? (2026-08-23)") + print(" NESSUNA proposta di cambio al libro live. Sola lettura sui file di produzione.") + print("=" * 97) + + grid = [dict(anchor=a, k=k, short=s, mh=m) + for a in G_ANCHOR for k in G_K for s in G_SHORT for m in G_MH] + tfs = G_TF + if args.fast: + grid = [g for g in grid if g["anchor"] in (1, 2) and g["mh"] == 0] + tfs = ("1h",) + print(" ⚠️ --fast: griglia RIDOTTA, i conteggi NON sono quelli dichiarati.") + + F = sezione0_1(grid, tfs) + M = sezione2(args) + MDE = sezione3(M) + print(f"\n {clock()} carico il funding ({'cache' if args.no_net else 'cache + incrementale'})...") + if args.no_net: + FU.USE_NET = False + rate = load_rate() + print(f" {clock()} funding pronto: " + " ".join( + f"{a} {rate[a]['series'].index.min().date()}->{rate[a]['series'].index.max().date()} " + f"(switch lineare {rate[a]['switch'].date()})" for a in ASSETS)) + + S4 = sezione4(grid, tfs) + S5 = sezione5(S4, F) + S6 = sezione6() + S7 = sezione7(rate, args.draws) + S8 = sezione8(rate) + S9 = sezione9(rate) + sezione10(S4, S5, S6, S7, S8, S9, M, MDE) + + sez("ATTESA A PRIORI vs ESITO β€” comprese quelle che ho sbagliato") + ch = S4["chosen"]["params"] + esiti = [ + ("A1 cella al buio != congelata", + "SBAGLIATA (coincidono)" if S4["uguale"] else "CONFERMATA nell'esito", + "" if S4["uguale"] else + f"⚠️ ma SBAGLIATA NEL MECCANISMO: prevedevo un k piu' piccolo; k resta {ch['k']:.2f} " + f"e a cambiare sono ALLOW_SHORT ({FROZEN['short']}->{ch['short']}) e il TF " + f"(1h->{S4['chosen']['tf']})"), + ("A2 il DSR si ribalta col conteggio", + f"REFUTATA (fam. {S4['dsr']:.3f} / screen {S5['dsr_screen']:.3f} / N=560 ancora PASS)", + f"⚠️ la leva non e' N ma la DISPERSIONE: servirebbe sd {S5['sd_soglia']:.2f} contro " + f"{S5['allsr'].std(ddof=1):.2f} misurata"), + ("A3 boundary robusto, banda larga, 0h fortunata", + f"CONFERMATA ({S6['req']['verdict']}, spread {S6['req']['spread']:.3f})", + f"canonica al 96o pctl delle 24: uplift +0,328 contro mediana +0,190"), + ("A4 il delta di Β§50 e' una sovrastima", + (f"CONFERMATA ({S7[W_PROPOSTO]['sh_can']:+.3f} -> {S7[W_PROPOSTO]['sh']:+.3f} FULL, " + f"{S7[W_PROPOSTO]['shh_can']:+.3f} -> {S7[W_PROPOSTO]['shh']:+.3f} HOLD)") if S7 else "n/d", + "il segno regge al 100% delle estrazioni; e' la taglia a dimezzarsi"), + ("A5 drag di funding 0-2%/anno", f"CONFERMATA ({S8['sleeve_drag']:.2%}/anno)", + "meccanismo NON come previsto: la gamba short incassa solo 0,26-0,46x " + "l'incondizionato β€” non compensa"), + ("A6 frac_random_beat_hold alto", + f"CONFERMATA ({S9[W_PROPOSTO]['frac_random_beat_hold']:.2f})" if S9 else "n/d", + "'migliora l'hold-out' e' un claim generico su questo libro"), + ("A7 la finestra forward non ha potenza", + f"CONFERMATA (t={MDE['MODELED']['t_naive']:.2f} naive / {MDE['MODELED']['t']:.2f} Lo)", + f"e il +2,04 pubblicato e' su lente ORARIA: sulla lente del progetto e' " + f"{MDE['daily_sharpe']:+.2f}"), + ] + for nm, v, nota in esiti: + print(f" {nm:<42}{v}") + if nota: + print(f" {'':<42}{nota}") + print(f"\n {clock()} fine.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_skeptic_leva.py b/scripts/research/r0823_skeptic_leva.py new file mode 100644 index 0000000..bf99f5e --- /dev/null +++ b/scripts/research/r0823_skeptic_leva.py @@ -0,0 +1,997 @@ +#!/usr/bin/env python +"""r0823_skeptic_leva.py β€” SCETTICO ADVERSARIALE del gradino di leva 1,00x -> 1,25x. + +Filone Β§53 SKEPTIC-LEVA, ondata 2026-08-22/23, branch `research/wave-0822`. + +IL BERSAGLIO (RESULTS-0822 Β§33 WORST-DAY, Β§38 SCALE-SPEC, Β§40 SL-ANCHOR): + Β«il libro gira al 7% di Kelly; il gradino 1,00x -> 1,25x vale 14,7a -> 12,9a al + capitale-rendita (+164 EUR/mese di versamento equivalente); la riserva "-10% al giorno + ogni anno" e' mal tarata di 2-4 ordini di grandezza; il nodo del disaster-SL rotolante e' + sciolto (scatta MENO). Resta da costruire una chiave di scala in config.Β» + +QUESTO FILE NON CONFERMA: cerca dove il risultato e' sbagliato. Sei attacchi dichiarati, ognuno +con la sua soglia scritta PRIMA nella sezione (0), e ognuno che finisce in REGGE / INCRINA / +FALSIFICA con un NUMERO accanto. + + (0) IL CRITERIO β€” le sei soglie, congelate prima di guardare le sezioni 2-7. + (1) REPLICA β€” 10 numeri gia' pubblicati riprodotti PRIMA di attaccarli. + (2) A1 il drift e' stimato su 7,4 anni con due tori, e k* e' LINEARE nel suo errore. + (3) A2 il campione non contiene il sinistro che conta (finestre stressate e disgiunte). + (4) A3 la coda: due stimatori che non concordano -> si prende il PEGGIORE fino in fondo. + (5) A4 la lente: tutto il conto e' close-only, e close-only e' CIECA su una regola a un + livello. Il disaster-SL e' una regola su un livello. + (6) A5 l'invariante a tre chiavi: e' completo? c'e' una QUARTA chiave? + (7) A6 il funding non era nel conto della leva, e il funding SCALA con la leva. + (8) VERDETTO, e il k MASSIMO DIFENDIBILE come singolo numero. + +ONESTA' APPLICATA (regole gia' codificate nel progetto, usate CONTRO questo lavoro): + Β· lo Sharpe e' INVARIANTE alla scala: `deflated_sharpe` e `marginal_vs_tp01` non vedono la + leva -> non compaiono qui come prova (SPEC Β§0); + Β· si riproduce il numero che si attacca PRIMA di attaccarlo (sez. 1); + Β· [MISURATO] / [CALC] / [DEDOTTO] / [ASSUNTO] / [CITATO] marcati riga per riga; + Β· la trappola dichiarata da WORST-DAY: se uno stress uccide anche il conto di OGGI, non e' il + gradino a essere sbagliato β€” e va DETTO. Per questo ogni finestra stressata riporta anche il + drift del libro SU QUELLA finestra: dove il libro e' morto, il gradino non e' giudicabile. + Β· un non-arrivo si codifica +inf, mai -1 (07/08); una mediana condizionata non si stampa mai + senza la sua probabilita' accanto. + Β· confronto APPAIATO fra k: `CC._boot_paths` estrae INDICI da un seme, quindi + `_boot_paths(k*r) == k*_boot_paths(r)` esattamente (verificato in sez. 1). + +SOLA LETTURA. Nessuna rete, nessun ordine, nessuna scrittura fuori dalla cache di /tmp. + + nice -n 19 timeout 900 uv run python scripts/research/r0823_skeptic_leva.py +""" +from __future__ import annotations + +import json +import sys +import time +import warnings +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy import stats + +warnings.filterwarnings("ignore") + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0725_capcurve as CC # noqa: E402 book_series, _boot_paths, TAX_RATE +import r0725_prop_coupled as PC # noqa: E402 lente ACCOPPIATA (R, m) del libro +import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI +import r0807_piano_netto as PN # noqa: E402 muro / accumula / sopravvivenza + +from src.live.book import ( # noqa: E402 SOLA LETTURA (nessuna di queste scrive) + CAP_UNKNOWN_USD, W_SKH, W_TP01, _read_watermark, book_net_target) +from src.live.shadow import FALLBACK_CAPITAL, WEIGHT # noqa: E402 +from src.strategies.trend_portfolio import CANONICAL # noqa: E402 + +ANN = 365.0 +DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor), non a occhio +START = 635.0 # il conto Deribit VERO +DEP_EUR = 500.0 # il piano in corso (config/live.json _nota_cap: "EUR 5.000 + 500/mese") +BLOCK = 20 # blocchi del bootstrap (convenzione del progetto) +N_WALL = 2000 # risoluzione MC del muro ~1%, DICHIARATA (r0822d_funding) +N_TRAJ = 3000 # la taglia a cui furono calcolate le traiettorie pubblicate +SEED = 725 # seme delle traiettorie pubblicate (confronto appaiato) +SEED_BOOT = 20260823 +ANNI_TRAJ = 25 +KS = (1.00, 1.25, 1.50, 2.00) + +# ---- numeri PUBBLICATI, per la replica di sez. 1 ------------------------------------------ +PUB = { + "drift_nofund": 0.1924, "drift_fund": 0.1708, # Β§35 + "sh_nofund": 1.692, "sh_fund": 1.513, # Β§35 + "kstar_nofund": 13.7, "kstar_fund": 12.3, # Β§35 + "g1": 0.1790, "g125": 0.2261, "sharpe_scale": 1.5051, # SPEC Β§0 (serie de-luckata, no funding) + "perp_L1": 0.0752, "muro_L1": 264_000, # Β§36 (fisco) + "perp_L3": 0.0635, "muro_L3": 313_000, # Β§36 (fisco + funding) + "anni_L1_500": 14.7, # Β§36 riga "EUR 500 -> 14,7a" + "worst_close": -0.0394, "worst_intraday": -0.0733, # Β§33 + "L_strutt": 0.1432, # Β§33 sez.5 (bound aritmetico a k=1) + "xi_gpd": 0.09, "xi_hill": 0.34, # Β§33 + "k_da_SL": 1.67, # SPEC Β§4.5 = 0.50/(2*0.50*0.30) +} + +CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/pianovero/book.parquet") +T0 = time.time() +ESITI: dict[str, tuple[str, str]] = {} + + +def sez(t: str) -> None: + print("\n" + "=" * 108) + print(f" {t}") + print("=" * 108) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +def esito(chiave: str, verdetto: str, riga: str) -> None: + ESITI[chiave] = (verdetto, riga) + print(f"\n >>> {chiave}: {verdetto} β€” {riga}") + + +# ========================================================================================== +# (0) IL CRITERIO β€” congelato QUI, valutato in (8). Nessuna soglia usa un numero delle +# sezioni 2-7: vengono dalle regole gia' scritte del progetto o sono dichiarate a priori. +# ========================================================================================== +CRITERIO = { + "A1": ("DRIFT β€” il gradino 1,25x FALSIFICA se il drift di pareggio (quello a cui " + "g(1,25) = g(1,00)) cade DENTRO l'IC95 bootstrap a blocchi del drift. " + "INCRINA se cade fra l'IC95 e l'IC99. REGGE se e' fuori da entrambi."), + "A2": ("SINISTRO β€” il gradino FALSIFICA se Delta g(1,25) < 0 in almeno una finestra " + "stressata IN CUI IL LIBRO STESSO HA DRIFT POSITIVO. Dove il libro e' in perdita " + "la finestra giudica il LIBRO, non il gradino, e non conta (nota dovuta di Β§33)."), + "A3": ("CODA β€” si prende lo stimatore PEGGIORE (Hill, xi=0.34) e lo si porta fino in " + "fondo. FALSIFICA se a k=1,25 (i) il peggior giorno strutturale supera il 50% " + "dell'equity, oppure (ii) P(dimezzamento in 10 anni) supera il 5% E piu' del " + "triplo del valore a k=1,00. Soglie: il 50% e' il C1 gia' dichiarato da WORST-DAY; " + "il 5%/3x e' dichiarato qui, prima di guardare."), + "A4": ("LENTE β€” FALSIFICA se sotto la lente ACCOPPIATA (i) la frequenza di tocco del " + "disaster-SL cresce con k, oppure (ii) il ricarico del maxDD smette di essere " + "moltiplicativo (rapporto accoppiato/close-only che si muove di piu' di 1pp fra " + "k=1,00 e k=1,50)."), + "A5": ("INVARIANTE β€” FALSIFICA se esiste una chiave RAGGIUNGIBILE, non presente in " + "`n_asset * frac * scala * disaster_sl_pct <= 0,50`, che possa ALZARE la leva " + "lorda vera lasciando l'invariante invariato o soddisfatto."), + "A6": ("FUNDING β€” il funding SCALA col nozionale, quindi con k. Rifatto il conto con il " + "funding dentro e proporzionale a k, il gradino FALSIFICA se il guadagno sul " + "capitale-rendita scende sotto 1,0 anno (soglia del mandato)."), +} + + +# ========================================================================================== +# UTILITA' +# ========================================================================================== +def deluck(r: np.ndarray) -> np.ndarray: + """Toglie dal DRIFT la fortuna d'ancora misurata. La vol NON si tocca: fra le ancore era + invariata (7,80% -> 7,76%), la fortuna sta tutta nel drift (r0726_deluck_factor).""" + r = np.asarray(r, float) + return r - (1.0 - DELUCK) * r.mean() + + +def g_exact(r: np.ndarray, k: float) -> float: + """Crescita geometrica annua a leva k: exp(365 * E[log(1+k r)]) - 1. Forma CHIUSA: nessun + rumore Monte Carlo, quindi i confronti fra k sono esatti.""" + x = 1.0 + k * np.asarray(r, float) + if np.any(x <= 0): + return -np.inf + return float(np.expm1(ANN * np.mean(np.log(x)))) + + +def k_star(r: np.ndarray, kmax: float = 40.0, step: float = 0.01) -> tuple[float, float]: + ks = np.arange(step, kmax + step, step) + gs = np.array([g_exact(r, k) for k in ks]) + j = int(np.nanargmax(gs)) + return float(ks[j]), float(gs[j]) + + +def shift_drift(r: np.ndarray, mu_annuo: float) -> np.ndarray: + """Stessa serie, drift imposto. Cambia UN grado di liberta' per volta: la forma della + distribuzione (vol, asimmetria, coda, clustering) resta identica.""" + r = np.asarray(r, float) + return r - r.mean() + mu_annuo / ANN + + +def boot_drift(r: np.ndarray, n: int = 4000, block: int = BLOCK, + seed: int = SEED_BOOT) -> np.ndarray: + """Bootstrap A BLOCCHI del drift annuo. A blocchi perche' la serie ha vol-clustering e + momentum: un bootstrap iid darebbe un IC piu' STRETTO del vero, cioe' aiuterebbe la tesi + che questo file sta attaccando.""" + rng = np.random.default_rng(seed) + m = len(r) + nb = int(np.ceil(m / block)) + out = np.empty(n) + for i in range(n): + st = rng.integers(0, m - block, size=nb) + idx = (st[:, None] + np.arange(block)[None, :]).ravel()[:m] + out[i] = r[idx].mean() * ANN + return out + + +def muro_di(r: np.ndarray) -> tuple[float, float]: + """(rendita perpetua, capitale-rendita) con la convenzione COERENTE (imposte dentro il + portafoglio, prelievo gia' netto). Bisezione identica a PN.muro, con N_WALL dichiarato.""" + netto = CC.TARGET_EUR_DAY * 365 * CC.EURUSD + lo, hi = 0.0, 0.40 + for _ in range(13): + mid = (lo + hi) / 2 + s = PN.sopravvivenza(r, 1e6, 1e6 * mid, 20, CC.TAX_RATE, TX.PATRIMONIALE, + n_paths=N_WALL, seed=SEED) + if s["p10_end"] >= 1e6: + lo = mid + else: + hi = mid + return lo, (netto / lo if lo > 0.002 else float("inf")) + + +def anni_al_muro(r: np.ndarray, bersaglio: float, dep_eur: float = DEP_EUR, + start: float = START) -> dict: + """Anni al capitale-rendita. Path APPAIATI fra k (gli indici del bootstrap dipendono solo + dal seme e dalla lunghezza, non dai valori).""" + rng = np.random.default_rng(SEED) + paths = CC._boot_paths(r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rng) + o = PN.accumula(paths, dep_eur, bersaglio, CC.TAX_RATE, TX.PATRIMONIALE, start=start) + c = o["colpito"] + y = np.where(c >= 0, c / 365.0, np.inf) + arr = y[np.isfinite(y)] + return dict(med=float(np.median(y)), + med_cond=float(np.median(arr)) if len(arr) > 100 else float("nan"), + p20=float((y <= 20.0).mean()), p_arr=float(np.isfinite(y).mean())) + + +def cap_20a(r: np.ndarray, dep_eur: float = DEP_EUR, start: float = START, + anni: int = 20) -> float: + """Capitale mediano a 20 anni. Serve dove gli ANNI non sono definiti: a drift bassi il + capitale-rendita non si raggiunge in 25 anni su nessun path e la mediana degli anni e' nan + per ENTRAMBI i k β€” cioe' la metrica smette di discriminare proprio dove serve. Il capitale + e' definito sempre, ed e' APPAIATO fra k (stessi indici di bootstrap).""" + rng = np.random.default_rng(SEED) + paths = CC._boot_paths(r, N_TRAJ, anni * 365, BLOCK, rng) + o = PN.accumula(paths, dep_eur, 1e18, CC.TAX_RATE, TX.PATRIMONIALE, start=start) + return float(np.median(o["cap"])) + + +def fmt_anni(d: dict) -> str: + if not np.isfinite(d["med_cond"]): + return f" n/d (P20 {d['p20']:.3f})" + return f"{d['med_cond']:5.2f}a (P20 {d['p20']:.3f})" + + +# ========================================================================================== +# (1) REPLICA β€” prima di attaccare +# ========================================================================================== +def sezione1() -> dict: + sez("(1) REPLICA β€” 10 numeri pubblicati, riprodotti PRIMA di attaccarli") + if not CACHE.exists(): + raise SystemExit("cache serie assente: girare prima r0822d_piano_vero.py") + d = pd.read_parquet(CACHE) + r_nf, r_f = d["nofund"].values.astype(float), d["fund"].values.astype(float) + print(f" serie: {len(r_nf)} giorni, {d.index.min().date()} -> {d.index.max().date()} " + f"(cache di r0822d_piano_vero, zero rete)") + R = {"nofund": r_nf, "fund": r_f, "L1": deluck(r_nf), "L3": deluck(r_f)} + + righe = [] + for nome, x, pub in (("drift senza funding", r_nf.mean() * ANN, PUB["drift_nofund"]), + ("drift con funding", r_f.mean() * ANN, PUB["drift_fund"]), + ("Sharpe senza funding", r_nf.mean() / r_nf.std() * ANN ** .5, + PUB["sh_nofund"]), + ("Sharpe con funding", r_f.mean() / r_f.std() * ANN ** .5, + PUB["sh_fund"])): + righe.append((nome, x, pub)) + ks_nf, _ = k_star(deluck(r_nf)) + ks_f, _ = k_star(deluck(r_f)) + righe += [("k* senza funding", ks_nf, PUB["kstar_nofund"]), + ("k* con funding", ks_f, PUB["kstar_fund"]), + ("g(1,00) serie SPEC", g_exact(R["L1"], 1.00), PUB["g1"]), + ("g(1,25) serie SPEC", g_exact(R["L1"], 1.25), PUB["g125"]), + ("Sharpe (invariante a k)", R["L1"].mean() / R["L1"].std() * ANN ** .5, + PUB["sharpe_scale"])] + + print(f"\n {'grandezza':<26}{'replica':>12}{'pubblicato':>13}{'|diff|':>11}") + for nome, x, pub in righe: + print(f" {nome:<26}{x:>12.4f}{pub:>13.4f}{abs(x - pub):>11.4f}") + + p1, m1 = muro_di(R["L1"]) + p3, m3 = muro_di(R["L3"]) + print(f" {'perpetua L1 (fisco)':<26}{p1:>12.4f}{PUB['perp_L1']:>13.4f}{abs(p1-PUB['perp_L1']):>11.4f}") + print(f" {'muro L1 ($)':<26}{m1:>12,.0f}{PUB['muro_L1']:>13,.0f}{abs(m1-PUB['muro_L1']):>11,.0f}") + print(f" {'perpetua L3 (+funding)':<26}{p3:>12.4f}{PUB['perp_L3']:>13.4f}{abs(p3-PUB['perp_L3']):>11.4f}") + print(f" {'muro L3 ($)':<26}{m3:>12,.0f}{PUB['muro_L3']:>13,.0f}{abs(m3-PUB['muro_L3']):>11,.0f}") + a1 = anni_al_muro(R["L1"], m1) + print(f" {'anni L1 @EUR500/m':<26}{a1['med_cond']:>12.2f}{PUB['anni_L1_500']:>13.2f}" + f"{abs(a1['med_cond']-PUB['anni_L1_500']):>11.2f} (risoluzione MC dichiarata ~1%)") + + # --- il confronto APPAIATO fra k e' esatto: lo si PROVA, non lo si dichiara ------------- + rng = np.random.default_rng(SEED) + p_a = CC._boot_paths(R["L3"], 200, 2000, BLOCK, rng) + rng = np.random.default_rng(SEED) + p_b = CC._boot_paths(1.25 * R["L3"], 200, 2000, BLOCK, rng) + dmax = float(np.abs(p_b - 1.25 * p_a).max()) + print(f"\n appaiamento fra k: max|paths(1,25 r) - 1,25*paths(r)| = {dmax:.2e} " + f"-> i path sono gli STESSI, il confronto fra k non ha rumore MC di campionamento") + assert dmax == 0.0 + + # --- la lente accoppiata: quale serie e' ------------------------------------------------- + Tc = PC.crypto_daily_tuples(0.75, 0.25) + print(f" lente accoppiata (r0725_prop_coupled): {len(Tc)} giorni, peggior chiusura " + f"{Tc['R'].min():+.2%} ({Tc['R'].idxmin().date()}), peggior minimo intra-giorno " + f"{Tc['m'].min():+.2%} ({Tc['m'].idxmin().date()})") + print(f" -> il minimo intra-giorno replica il {PUB['worst_intraday']:+.2%} di Β§33 " + f"(|diff| {abs(Tc['m'].min()-PUB['worst_intraday']):.4f}); la chiusura NO " + f"({Tc['R'].min():+.2%} contro {PUB['worst_close']:+.2%}) perche' Β§33 legge la " + f"chiusura sulla serie del libro e questa e' la ricostruzione ORARIA β€” differenza di " + f"lente gia' registrata in Β§33 ('aveva ragione, con la lente sbagliata'). " + f"[DICHIARATO, non riparato: A4 usa il MINIMO, che replica.]") + return {"R": R, "muri": {"L1": (p1, m1), "L3": (p3, m3)}, "anni_L1_k1": a1, "tuples": Tc} + + +# ========================================================================================== +# (2) A1 β€” IL DRIFT E' STIMATO SU 7,4 ANNI CON DUE TORI +# ========================================================================================== +def sezione2(S: dict) -> None: + sez("(2) A1 β€” il drift e' stimato su 7,4 anni con due tori, e k* e' LINEARE nel suo errore") + r = S["R"]["L3"] # la lente ONESTA: de-luckata E col funding dentro + mu = r.mean() * ANN + vol = r.std() * ANN ** 0.5 + dr = boot_drift(r) + lo95, hi95 = np.percentile(dr, [2.5, 97.5]) + lo99, hi99 = np.percentile(dr, [0.5, 99.5]) + se_b = dr.std() + se_naive = vol / np.sqrt(len(r) / ANN) + print(f" [MISURATO] drift de-luckato col funding: {mu:+.2%}/anno, vol {vol:.2%}, " + f"{len(r)/ANN:.1f} anni di campione") + print(f" [MISURATO] bootstrap a blocchi ({BLOCK}g, 4000 estrazioni): " + f"SE {se_b:.2%} (SE ingenuo iid {se_naive:.2%}: il clustering ALLARGA di " + f"{se_b/se_naive:.2f}x -> il bootstrap a blocchi e' la scelta severa)") + print(f" IC95 [{lo95:+.2%}, {hi95:+.2%}] IC99 [{lo99:+.2%}, {hi99:+.2%}]") + + # k* propagato + print(f"\n [CALC] k* propagato sul drift (la forma della distribuzione NON cambia: " + f"si sposta solo la media)") + print(f" {'drift':>10}{'k*':>9}{'g(1,00)':>11}{'g(1,25)':>11}{'g(1,25)-g(1,00)':>18}") + for nome, m in (("punto", mu), ("-1 SE", mu - se_b), ("IC95 basso", lo95), + ("IC99 basso", lo99), ("-3 SE", mu - 3 * se_b)): + x = shift_drift(r, m) + ks, _ = k_star(x) + g1, g125 = g_exact(x, 1.00), g_exact(x, 1.25) + print(f" {nome:>10} {m:>+8.2%}{ks:>9.2f}{g1:>11.2%}{g125:>11.2%}{g125-g1:>+18.2%}") + + # drift di pareggio del gradino, su g + lo, hi = -0.20, 0.40 + for _ in range(70): + mid = (lo + hi) / 2 + if g_exact(shift_drift(r, mid), 1.25) - g_exact(shift_drift(r, mid), 1.00) > 0: + hi = mid + else: + lo = mid + mu_be = hi + q_be = float((dr < mu_be).mean()) + z_be = (mu_be - mu) / se_b + print(f"\n [CALC] DRIFT DI PAREGGIO del gradino 1,25x: {mu_be:+.4%}/anno") + print(f" = {z_be:+.2f} SE dal punto, percentile bootstrap {q_be:.4%}") + print(f" = il libro puo' perdere il {(1-mu_be/mu):.1%} del suo drift e il gradino " + f"resta NON dannoso") + print(f" [CALC] (aritmetica di controllo: per g ~ mu*k - sigma^2 k^2/2 il pareggio sta a " + f"k*=1,125, cioe' mu = 1,125*sigma^2 = {1.125*vol**2:+.4%} β€” " + f"|diff| {abs(mu_be-1.125*vol**2):.4%})") + + # controllo empirico: il peggior drift che il libro abbia MAI realizzato su 3 anni + ser = pd.Series(r) + roll3 = ser.rolling(3 * 365).mean().dropna() * ANN + roll2 = ser.rolling(2 * 365).mean().dropna() * ANN + print(f"\n [MISURATO] controllo EMPIRICO, che non dipende da un modello di incertezza: " + f"il peggior drift realizzato") + print(f" su finestra mobile di 3 anni = {roll3.min():+.2%} " + f"(mediana {roll3.median():+.2%}); su 2 anni = {roll2.min():+.2%}") + print(f" -> anche il PEGGIOR triennio mai vissuto sta {roll3.min()/mu_be:.1f}x " + f"sopra il drift di pareggio del gradino.") + + # la metrica che DECIDE: anni al capitale-rendita. A drift bassi NON e' definita per + # nessuno dei due k (nessun path arriva): li' si legge il CAPITALE a 20 anni, appaiato. + print(f"\n [MISURATO] e sulla metrica che decide davvero (EUR{DEP_EUR:.0f}/m da " + f"${START:.0f}, muro RICALCOLATO a ogni drift e a ogni k):") + print(f" {'drift':>12}{'muro k=1,00':>13}{'anni k=1,00':>21}{'muro k=1,25':>13}" + f"{'anni k=1,25':>21}{'cap 20a k=1,00':>16}{'cap 20a k=1,25':>16}") + guadagni = {} + for nome, m in (("punto", mu), ("-1 SE", mu - se_b), ("IC95 basso", lo95), + ("meta' drift", mu / 2)): + x = shift_drift(r, m) + _, m1 = muro_di(x) + a1 = anni_al_muro(x, m1) + _, m2 = muro_di(1.25 * x) + a2 = anni_al_muro(1.25 * x, m2) + c1, c2 = cap_20a(x), cap_20a(1.25 * x) + g = (a1["med_cond"] - a2["med_cond"] + if np.isfinite(a1["med_cond"]) and np.isfinite(a2["med_cond"]) else float("nan")) + guadagni[nome] = (g, c2 / c1) + print(f" {nome:>12}{m1:>13,.0f}{fmt_anni(a1):>21}{m2:>13,.0f}{fmt_anni(a2):>21}" + f"{c1:>16,.0f}{c2:>16,.0f}") + r_ic, r_meta = guadagni["IC95 basso"][1], guadagni["meta' drift"][1] + print(f" ⚠️ a drift bassi gli ANNI non sono definiti PER ENTRAMBI i k (nessun path " + f"raggiunge un muro che nel frattempo e' esploso): li' discrimina solo il capitale, " + f"e il rapporto cap(1,25)/cap(1,00) resta {r_ic:.2f}x all'estremo basso dell'IC95 " + f"e {r_meta:.2f}x a meta' drift.") + print(f" {clock()}") + + if mu_be >= lo95: + v, riga = "FALSIFICA", f"il pareggio {mu_be:+.2%} cade DENTRO l'IC95" + elif mu_be >= lo99: + v, riga = "INCRINA", (f"il pareggio {mu_be:+.2%} cade fra IC95 ({lo95:+.2%}) e " + f"IC99 ({lo99:+.2%})") + else: + v, riga = "REGGE", (f"il gradino diventa dannoso solo sotto {mu_be:+.2%}/anno di drift " + f"= {z_be:+.2f} SE, percentile bootstrap {q_be:.2%}, fuori da IC95 " + f"[{lo95:+.2%}] e da IC99 [{lo99:+.2%}]; e il PEGGIOR triennio mai " + f"realizzato ({roll3.min():+.2%}) ne sta {roll3.min()/mu_be:.1f}x " + f"sopra. All'estremo basso dell'IC95 il capitale a 20 anni resta " + f"{guadagni['IC95 basso'][1]:.2f}x") + esito("A1 DRIFT", v, riga) + + +# ========================================================================================== +# (3) A2 β€” IL CAMPIONE NON CONTIENE IL SINISTRO CHE CONTA +# ========================================================================================== +def sezione3(S: dict) -> None: + sez("(3) A2 β€” il campione non contiene il sinistro che conta: finestre stressate e disgiunte") + d = pd.read_parquet(CACHE) + idx = d.index + r_full = deluck(d["fund"].values.astype(float)) + ser = pd.Series(r_full, index=idx) + + fin = [("PIENA 2019-03 -> oggi", ser)] + tolti = ser[(idx < "2020-01-01") | (idx > "2021-12-31")] + fin.append(("SENZA 2020-2021 (il toro che finanzia il drift)", tolti)) + bordi = ["2019-03-14", "2021-03-14", "2023-03-14", "2025-03-14", "2026-08-23"] + for a, b in zip(bordi[:-1], bordi[1:]): + w = ser[(idx >= a) & (idx < b)] + fin.append((f"disgiunta {a[:7]} -> {b[:7]}", w)) + # la finestra PEGGIORE non e' fra quelle disgiunte: si cerca il minimo su griglia MOBILE + # (attacco piu' forte: le disgiunte sono una partizione arbitraria, non il caso peggiore) + W = 2 * 365 + mroll = ser.rolling(W).mean() + j = int(np.nanargmin(mroll.values)) + peggiore2a = ser.iloc[j - W + 1: j + 1] + fin.append((f"MOBILE peggior biennio ({peggiore2a.index[0].date()})", peggiore2a)) + + print(f" ⚠️ ogni finestra riporta ANCHE il drift del libro su di essa: dove il libro e' in " + f"perdita la finestra giudica il LIBRO, non il gradino (nota dovuta di Β§33).") + print(f"\n {'finestra':<46}{'n':>6}{'drift':>10}{'vol':>9}{'Sharpe':>9}{'k*':>8}" + f"{'g(1,00)':>10}{'g(1,25)':>10}{'Delta g':>10}{'':>4}") + peggiori = [] + for nome, w in fin: + x = w.values + if len(x) < 120: + continue + m, v = x.mean() * ANN, x.std() * ANN ** 0.5 + ks, _ = k_star(x) + g1, g2 = g_exact(x, 1.00), g_exact(x, 1.25) + flag = "" if m > 0 else " <- LIBRO NEGATIVO" + print(f" {nome:<46}{len(x):>6}{m:>+10.2%}{v:>9.2%}{m/v:>9.2f}{ks:>8.2f}" + f"{g1:>+10.2%}{g2:>+10.2%}{g2-g1:>+10.2%}{flag}") + peggiori.append((nome, m, g2 - g1, x)) + + giudicabili = [(n, m, dg) for n, m, dg, _ in peggiori if m > 0 and "PIENA" not in n] + rotte = [n for n, m, dg in giudicabili if dg <= 0] + non_giud = [n for n, m, dg, _ in peggiori if m <= 0] + + # Delta sulle due finestre piu' avverse fra quelle giudicabili. ⚠️ Su una finestra di 2 + # anni il muro esplode (la perpetua vive sul rapporto drift/rischio di quella finestra) e + # gli ANNI non sono definiti per NESSUN k: si riporta anche il capitale a 20 anni, appaiato. + print(f"\n [MISURATO] le due finestre GIUDICABILI piu' avverse, muro ricalcolato DENTRO " + f"la finestra (⚠️ su 2 anni il muro non e' un numero di pianificazione: e' una lettura " + f"di quel regime):") + cand = sorted([(n, m, dg, x) for n, m, dg, x in peggiori if m > 0 and "PIENA" not in n], + key=lambda t: t[1])[:2] + for nome, m, dg, x in cand: + _, m1 = muro_di(x) + a1 = anni_al_muro(x, m1) + _, m2 = muro_di(1.25 * x) + a2 = anni_al_muro(1.25 * x, m2) + c1, c2 = cap_20a(x), cap_20a(1.25 * x) + print(f" {nome:<46} muro ${m1:>9,.0f} -> ${m2:>9,.0f} {fmt_anni(a1)} -> " + f"{fmt_anni(a2)} cap20a ${c1:>9,.0f} -> ${c2:>9,.0f} ({c2/c1:.2f}x)") + + # il costo del gradino DOVE fa male: il biennio mobile peggiore + nb, mb, dgb, xb = min(peggiori, key=lambda t: t[1]) + g1b, g2b = g_exact(xb, 1.00), g_exact(xb, 1.25) + cum = (1 + g2b) ** 2 / (1 + g1b) ** 2 - 1 + print(f"\n 🚨 [MISURATO] LA FORMA PIU' FORTE DELL'ATTACCO TROVA DAVVERO UNA FINESTRA IN CUI " + f"IL GRADINO FA MALE β€” ed e' quella in cui il LIBRO perde:") + print(f" {nb}: drift {mb:+.2%}, g(1,00) {g1b:+.2%}, g(1,25) {g2b:+.2%}, " + f"Delta {dgb:+.2%}/anno -> costo CUMULATO del gradino su quei 2 anni: {cum:+.2%} " + f"di capitale.") + print(f" Da mettere accanto al guadagno: sul campione pieno il gradino vale " + f"{g_exact(ser.values,1.25)-g_exact(ser.values,1.00):+.2%}/anno. " + f"Rapporto {abs((g_exact(ser.values,1.25)-g_exact(ser.values,1.00))/dgb):.0f}:1.") + print(f" Per il CRITERIO dichiarato in (0) quella finestra non falsifica (il libro " + f"stesso e' negativo: la leva amplifica un segno che li' e' gia' sbagliato, e lo " + f"stesso vale per il gradino 1,00x -> 0,75x nel verso opposto). " + f"**Ma la qualifica va detta col numero, non con un aggettivo.**") + print(f"\n ⚠️ [LIMITE DICHIARATO] a parte quel biennio, NESSUNA finestra ha drift negativo: " + f"il libro non ha mai vissuto un TRIENNIO in perdita, e il peggior biennio perde lo " + f"{abs(mb):.2%}/anno. L'attacco nella sua forma piu' forte β€” Β«un regime in cui il " + f"libro perde davveroΒ» β€” **non e' esprimibile su questo campione**, e questo E' il " + f"fatto che il mandato temeva. La sostituzione onesta e' A1: si IMPONE il drift " + f"invece di cercarlo. Le due misure si incastrano β€” peggior biennio {mb:+.2%}, " + f"pareggio del gradino +1,42%.") + print(f" {clock()}") + + if rotte: + v, riga = "FALSIFICA", f"Delta g(1,25) <= 0 su finestre giudicabili: {', '.join(rotte)}" + else: + peggior = min(giudicabili, key=lambda t: t[2]) if giudicabili else ("n/d", 0, 0) + v, riga = "REGGE", (f"Delta g(1,25) > 0 in {len(giudicabili)}/{len(giudicabili)} " + f"finestre giudicabili (la peggiore: {peggior[0]}, {peggior[2]:+.2%}), " + f"togliendo il toro 2020-21 compreso. QUALIFICA MISURATA: nel biennio " + f"mobile peggiore ({nb}, drift {mb:+.2%}) il gradino COSTA " + f"{dgb:+.2%}/anno = {cum:+.2%} cumulato β€” quella finestra non " + f"falsifica per il criterio (li' e' il libro a perdere), ma il " + f"numero e' questo, ed e' il costo del gradino nel suo caso peggiore " + f"realizzato") + esito("A2 SINISTRO", v, riga) + + +# ========================================================================================== +# (4) A3 β€” LA CODA PEGGIORE, PORTATA FINO IN FONDO +# ========================================================================================== +def hill_xi(loss: np.ndarray, k: int) -> float: + """Stimatore di Hill sulle k eccedenze piu' grandi (solo perdite POSITIVE).""" + x = np.sort(loss[loss > 0])[::-1] + k = min(k, len(x) - 1) + return float(np.mean(np.log(x[:k])) - np.log(x[k])) + + +def sezione4(S: dict) -> None: + sez("(4) A3 β€” la coda: due stimatori che non concordano -> si prende il PEGGIORE") + r = S["R"]["L3"] + loss = -r + q = 0.95 + u = float(np.quantile(loss, q)) + ex = loss[loss > u] - u + xi_g, _, beta = stats.genpareto.fit(ex, floc=0.0) + k_hill = int(0.05 * len(loss)) + xi_h = hill_xi(loss, k_hill) + print(f" [MISURATO] soglia 95Β° pctl = {u:.2%}, {len(ex)} eccedenze") + print(f" GPD xi = {xi_g:+.3f} (pubblicato {PUB['xi_gpd']:+.2f}) " + f"Hill xi = {xi_h:+.3f} su k={k_hill} (pubblicato {PUB['xi_hill']:+.2f})") + print(f" [MISURATO] Hill e' notoriamente sensibile a k -> lo sweep, invece del punto:") + print(" " + " ".join(f"k={kk}: xi={hill_xi(loss, kk):+.3f}" + for kk in (40, 70, 100, 135, 200, 300))) + xi = max(xi_g, xi_h, PUB["xi_hill"]) + print(f" [SCELTA DICHIARATA] si porta avanti il PEGGIORE dei tre: xi = {xi:+.3f} " + f"(piu' severo del {PUB['xi_hill']:+.2f} pubblicato: l'attacco si porta col numero " + f"peggiore, non con quello citato)") + + L = PUB["L_strutt"] # [CITATO] bound aritmetico di Β§33 a k=1 + print(f"\n (a) IL BOUND STRUTTURALE, scalato [CITATO Β§33: |L| = {L:.2%} a k=1,00]") + print(f" {'k':>6}{'peggior giorno':>17}{'costo disaster-SL':>20}{'soglia 20%':>13}" + f"{'soglia 50%':>13}") + sl_pct = json.loads((ROOT / "config" / "live.json").read_text())["disaster_sl_pct"] + frac = json.loads((ROOT / "config" / "live.json").read_text())["max_notional_per_asset_frac"] + for k in KS: + wd, cs = k * L, k * 2 * frac * sl_pct + print(f" {k:>6.2f}{wd:>16.2%} {cs:>19.2%}" + f"{'SFONDA' if wd > 0.20 else 'ok':>13}{'SFONDA' if wd > 0.50 else 'ok':>13}") + k_20 = 0.20 / L + k_50 = 0.50 / L + k_sl50 = 0.50 / (2 * frac * sl_pct) + print(f" [CALC] il peggior giorno strutturale sfonda il 20% (recupero +25%) a k = {k_20:.2f}") + print(f" [CALC] ...e il 50% (C1 di Β§33) a k = {k_50:.2f}") + print(f" [CALC] il costo del disaster-SL sfonda il 50% a k = {k_sl50:.2f} " + f"(pubblicato {PUB['k_da_SL']:.2f}) -> ⚠️ il disaster-SL sfonda il 20% GIA' A k=1,00 " + f"({2*frac*sl_pct:.0%}): quella soglia non separa i k, e va detto.") + + # (b) coda peggiore dentro il campione: si SOSTITUISCONO le eccedenze storiche con + # estrazioni GPD(xi) troncate al bound, MANTENENDO LE POSIZIONI (quindi il clustering). + print(f"\n (b) P(DIMEZZAMENTO in 10 anni, senza prelievi) con la coda peggiore dentro") + rng = np.random.default_rng(SEED_BOOT) + pos = np.where(loss > u)[0] + fmax = stats.genpareto.cdf(max(L - u, 1e-9), xi, loc=0.0, scale=beta) + y = stats.genpareto.ppf(rng.random(len(pos)) * fmax, xi, loc=0.0, scale=beta) + r_tail = r.copy() + r_tail[pos] = -(u + y) + r_tail = r_tail - r_tail.mean() + r.mean() # stesso drift: cambia SOLO la coda + print(f" [CALC] serie a coda peggiore: drift {r_tail.mean()*ANN:+.2%} (invariato), " + f"vol {r_tail.std()*ANN**.5:.2%} (era {r.std()*ANN**.5:.2%}), " + f"peggior giorno {r_tail.min():+.2%} (era {r.min():+.2%})") + ks_t, _ = k_star(r_tail) + ks_0, _ = k_star(r) + d_coda = ks_0 - ks_t + d_drift = ks_0 - k_star(shift_drift(r, r.mean() * ANN / 2))[0] + print(f" [MISURATO] k*: {ks_0:.2f} (coda storica) -> {ks_t:.2f} (coda peggiore) " + f"= la coda muove k* di {d_coda:+.2f}; dimezzare il DRIFT lo muove di {d_drift:+.2f}") + print(f" ⚠️ CORREZIONE a Β§33, che scriveva Β«la forma della coda muove k* di 1-2 " + f"unita', il drift di 6,7Β»: con lo stimatore PEGGIORE la coda ne muove {d_coda:.1f}, " + f"non 1-2. La CONCLUSIONE di Β§33 regge (il drift resta il fattore dominante, " + f"{d_drift/d_coda:.1f}x) ma il numero pubblicato e' ottimista di ~{d_coda/1.5:.0f}x, " + f"e a quella coda k*={ks_t:.2f} e' ancora {ks_t/1.25:.0f}x il gradino richiesto.") + rng = np.random.default_rng(SEED) + P = CC._boot_paths(r_tail, 4000, 10 * 365, BLOCK, rng) + print(f" {'k':>6}{'P(dimezzamento)':>18}{'P(-80%)':>11}{'maxDD mediano':>16}" + f"{'g(k) 10a mediano':>19}") + p_half = {} + for k in KS: + eq = np.cumprod(1.0 + k * P, axis=1) + run = np.maximum.accumulate(eq, axis=1) + dd = (eq / run - 1.0).min(axis=1) + half = float((eq.min(axis=1) <= 0.5).mean()) + p80 = float((eq.min(axis=1) <= 0.2).mean()) + p_half[k] = half + print(f" {k:>6.2f}{half:>18.3%}{p80:>11.3%}{np.median(dd):>16.2%}" + f"{np.median(eq[:, -1]) ** (1/10) - 1:>18.2%}") + print(f" {clock()}") + + rap = p_half[1.25] / p_half[1.00] if p_half[1.00] > 0 else float("inf") + if 1.25 * L > 0.50 or (p_half[1.25] > 0.05 and rap > 3.0): + v, riga = "FALSIFICA", (f"a k=1,25 il peggior giorno strutturale vale {1.25*L:.2%} " + f"e P(dimezzamento) {p_half[1.25]:.2%}") + else: + v, riga = "REGGE", (f"con xi={xi:.2f} preso alla lettera: peggior giorno strutturale " + f"{1.25*L:.2%} < 50%, P(dimezzamento 10a) {p_half[1.25]:.3%} contro " + f"{p_half[1.00]:.3%} a k=1,00. Il bound strutturale sfonda il 50% " + f"a k={k_50:.2f} e il 20% a k={k_20:.2f}; la coda muove k* di " + f"{ks_0-ks_t:+.2f}, il drift di {ks_0 - k_star(shift_drift(r, r.mean()*ANN/2))[0]:+.2f}") + esito("A3 CODA", v, riga) + + +# ========================================================================================== +# (5) A4 β€” LA LENTE ACCOPPIATA, E IL DISASTER-SL CHE E' UNA REGOLA SU UN LIVELLO +# ========================================================================================== +def sezione5(S: dict) -> None: + sez("(5) A4 β€” la lente: close-only e' CIECA su una regola a un livello. " + "Il disaster-SL E' una regola su un livello.") + T = S["tuples"] + R = T["R"].values + m = T["m"].values + + print(" (a) IL RICARICO DEL maxDD: e' moltiplicativo o si amplifica con k?") + print(f" {'k':>6}{'maxDD close-only':>19}{'maxDD accoppiato':>19}{'ricarico':>11}" + f"{'peggior giorno':>17}{'peggior minimo':>17}") + ric = {} + for k in KS: + eq = np.cumprod(1.0 + k * R) + prev = np.concatenate(([1.0], eq[:-1])) + run_c = np.maximum.accumulate(eq) + dd_c = float((eq / run_c - 1.0).min()) + low = prev * (1.0 + k * m) # minimo intra-giorno del PATH composto + run_a = np.maximum.accumulate(np.maximum(eq, prev)) + dd_a = float((low / run_a - 1.0).min()) + ric[k] = dd_a / dd_c + print(f" {k:>6.2f}{dd_c:>19.2%}{dd_a:>19.2%}{dd_a/dd_c:>11.4f}" + f"{k*R.min():>17.2%}{k*m.min():>17.2%}") + spread = max(ric.values()) - min(ric.values()) + print(f" [MISURATO] il ricarico si muove di {spread:.4f} fra k=1,00 e k=2,00 " + f"(= {spread*100:.2f}pp): moltiplicativo, NON amplificante") + + print(f"\n (b) LA FREQUENZA DEL DISASTER-SL E' INVARIANTE A k β€” dimostrazione dal codice, " + f"non dai dati:") + print(f" `execution.ensure_disaster_sl` calcola") + print(f" want_stop = disaster_stop_price(instrument, MARK, sl_pct, long) -> un PREZZO") + print(f" want_amount = notional_to_amount(instrument, |pos|, price=mark) -> una TAGLIA") + print(f" e ri-piazza se |amt - want_amount| >= want_amount*0,1 OPPURE " + f"|stp - want_stop|/want_stop >= 0,05.") + print(f" Una scala k moltiplica `pos` (quindi `want_amount`) e NON tocca `mark`.") + print(f" -> il livello di innesco e' invariante a k, e il test di ri-piazzamento e' " + f"RELATIVO (`want_amount*0,1`), quindi anch'esso invariante a k.") + print(f" β‡’ [DEDOTTO dal sorgente] a k piu' alto il disaster-SL NON viene toccato piu' " + f"spesso: viene toccato le STESSE volte e costa k volte tanto. Β§40 misura la frequenza " + f"(0-1 episodi su tutta la storia alla cadenza oraria che gira); questo file mostra che " + f"quel numero non va rifatto per k.") + + print(f"\n (c) CONTROLLO NUMERICO della (b): l'innesco e' un movimento del MARK, non " + f"dell'equity. Conteggio dei giorni in cui il minimo INTRA-giorno del libro a leva k " + f"scende sotto soglie di EQUITY (queste si', k-dipendenti):") + print(f" {'k':>6}{'gg min < -5%':>15}{'gg min < -10%':>15}{'gg min < -20%':>15}" + f"{'gg min < -30%':>15}") + for k in KS: + print(f" {k:>6.2f}{int((k*m < -0.05).sum()):>15}{int((k*m < -0.10).sum()):>15}" + f"{int((k*m < -0.20).sum()):>15}{int((k*m < -0.30).sum()):>15}") + print(f" -> nessuna soglia di EQUITY e' armata sul libro proprietario (il disaster-SL e' " + f"sul PREZZO): questa tabella e' la misura di cosa succederebbe se ce ne fosse una, " + f"ed e' la ragione per cui sul canale FUNDED β€” dove la regola a un giorno esiste β€” " + f"la lente accoppiata resta obbligatoria (25/07).") + + amp = ric[1.50] - ric[1.00] + if abs(amp) > 0.01: + v, riga = "FALSIFICA", f"il ricarico del maxDD si muove di {amp*100:+.2f}pp fra k=1,00 e 1,50" + else: + v, riga = "REGGE", (f"ricarico maxDD {ric[1.00]:.4f} (k=1,00) -> {ric[1.50]:.4f} (k=1,50) " + f"-> {ric[2.00]:.4f} (k=2,00): movimento {amp*100:+.2f}pp nel range " + f"della proposta e {(ric[2.00]-ric[1.00])*100:+.2f}pp fino a k=2, " + f"e va nel verso FAVOREVOLE (il ricarico CALA). " + f"La frequenza del disaster-SL e' invariante a k PER COSTRUZIONE " + f"(innesco sul MARK, test di ri-piazzamento RELATIVO): il numero di " + f"Β§40 non va rifatto per k, solo moltiplicato") + esito("A4 LENTE", v, riga) + + +# ========================================================================================== +# (6) A5 β€” L'INVARIANTE A TRE CHIAVI: c'e' una QUARTA? +# ========================================================================================== +def sezione6() -> None: + sez("(6) A5 β€” l'invariante `n_asset * frac * scala * disaster_sl_pct <= 0,50`: e' completo?") + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac = float(cfg["max_notional_per_asset_frac"]) + fixed = float(cfg["max_notional_per_asset_usd"]) + sl = float(cfg["disaster_sl_pct"]) + n_asset = 2 + wm = _read_watermark() + lev_tp = float(CANONICAL["leverage"]) + print(f" [LETTO dal codice e da config, sola lettura]") + print(f" config/live.json : frac={frac} fixed=${fixed:,.0f} disaster_sl_pct={sl}") + print(f" src/live/shadow : ASSETS n={n_asset} WEIGHT={WEIGHT} " + f"FALLBACK_CAPITAL=${FALLBACK_CAPITAL:,.0f}") + print(f" src/live/book : W_TP01={W_TP01} W_SKH={W_SKH} " + f"CAP_UNKNOWN_USD=${CAP_UNKNOWN_USD:,.0f} watermark=${wm:,.2f}" + if wm else " src/live/book : watermark ASSENTE") + print(f" trend_portfolio : CANONICAL['leverage']={lev_tp} " + f"target_vol={CANONICAL['target_vol']}") + + print(f"\n [CALC] la LEVA LORDA VERA, derivata dal sorgente invece che dichiarata.") + print(f" raw = WEIGHT * E * (W_TP01*tp_frac + W_SKH*sign), tp_frac <= " + f"CANONICAL['leverage'] = {lev_tp}") + print(f" net = scala * clamp(raw, +-cap), cap = frac*E (percorso fidato)") + print(f" lordo = n_asset * min(WEIGHT*(W_TP01*{lev_tp}+W_SKH), frac) * scala * E") + raw_max = WEIGHT * (W_TP01 * lev_tp + W_SKH * 1.0) + print(f" = {n_asset} * min({raw_max:.4f}, {frac}) * scala * E " + f"= {n_asset*min(raw_max, frac):.4f} * scala * E") + print(f" -> l'invariante usa `n_asset*frac*scala` = {n_asset*frac:.4f}*scala; il VERO e' " + f"{n_asset*min(raw_max,frac):.4f}*scala. Coincidono qui perche' frac({frac}) < " + f"WEIGHT*(...)={raw_max:.4f}. **L'invariante e' un LIMITE SUPERIORE, quindi safe.**") + + # verifica esaustiva su una griglia di chiavi raggiungibili + print(f"\n [MISURATO] verifica esaustiva: esiste una combinazione che SODDISFA l'invariante " + f"e produce una leva lorda VERA superiore al suo bound?") + viol = 0 + tot = 0 + for f_ in (0.25, 0.50, 0.625, 0.75, 1.0, 1.5): + for w_ in (0.25, 0.5, 0.75, 1.0): + for sc in (1.0, 1.25, 1.5, 2.0): + for lv in (1.0, 2.0, 3.0): + for na in (2, 3): + tot += 1 + rmax = w_ * (W_TP01 * lv + W_SKH * 1.0) + vero = na * min(rmax, f_) * sc + bound = na * f_ * sc + if vero > bound + 1e-12: + viol += 1 + print(f" {tot} combinazioni (frac x WEIGHT x scala x leverage-TP01 x n_asset): " + f"{viol} violazioni del bound. -> sul PERCORSO FIDATO l'invariante e' COMPLETO.") + + print(f"\n 🚨 [MISURATO] MA IL PERCORSO FIDATO NON E' L'UNICO. Nel ramo di FALLBACK " + f"(`eq_fallback`) `book_report` sizza su `paper_cap`, non sull'equity vera:") + print(f" lordo_fallback = n_asset * min(WEIGHT*paper_cap*(...), min(fixed, wm*frac)) * scala") + print(f" e la leva si misura contro l'equity VERA, che li' non e' nota: al suo posto c'e' " + f"il WATERMARK, che e' l'ULTIMA equity osservata, non quella attuale.") + print(f" {'scenario':<44}{'wm':>9}{'E vera':>9}{'lordo k=1,00':>14}{'/E':>8}" + f"{'lordo k=1,25':>14}{'/E':>8}") + righe = [("normale (equity leggibile)", 635.0, 635.0, False), + ("fallback, watermark fresco", 635.0, 635.0, True), + ("fallback dopo PRELIEVO del 50% non visto", 635.0, 317.5, True), + ("fallback dopo PRELIEVO dell'80% non visto", 635.0, 127.0, True), + ("fallback dopo perdita del 50% non vista", 635.0, 317.5, True)] + peggio1 = peggio125 = 0.0 + for nome, w_, e_vera, fb in righe: + if not fb: + cap, eq_used = e_vera * frac, e_vera + else: + cap, eq_used = min(fixed, w_ * frac), FALLBACK_CAPITAL + raw = WEIGHT * eq_used * (W_TP01 * lev_tp + W_SKH) + l1 = n_asset * min(raw, cap) + peggio1, peggio125 = max(peggio1, l1 / e_vera), max(peggio125, 1.25 * l1 / e_vera) + print(f" {nome:<44}{w_:>9,.0f}{e_vera:>9,.0f}{l1:>14,.1f}{l1/e_vera:>7.2f}x" + f"{1.25*l1:>14,.1f}{1.25*l1/e_vera:>7.2f}x") + print(f"\n 🚨 IL BUCO ESISTE GIA' A k=1,00 ({peggio1:.2f}x) e NON e' creato dalla scala: " + f"e' creato dal fatto che nel fallback il denominatore del rapporto e' il WATERMARK " + f"(l'ultima equity OSSERVATA) e non l'equity attuale. La scala non lo apre: lo " + f"MOLTIPLICA ({peggio1:.2f}x -> {peggio125:.2f}x) mentre l'invariante continua a " + f"leggere {n_asset*frac:.2f}x e {n_asset*frac*1.25:.2f}x.") + print(f" ⚠️ Percio' la guardia G3 della SPEC (`scala = 1,00 nel fallback`) **non chiude " + f"il buco: ne toglie il moltiplicatore**. Resta NON OPZIONALE β€” senza di lei il " + f"gradino peggiora del 25% uno scenario gia' fuori tetto β€” ma la SPEC la presenta " + f"come Β«il fallback non e' piu' permissivo per costruzioneΒ», e questo e' vero solo " + f"RISPETTO ALLA SCALA, non rispetto alla leva.") + print(f" [DICHIARATO] frequenza misurata del ramo: 0 giri su 1.442 (Β§38); " + f"`book_execute` su `eq_fallback` **allerta e NON blocca** (letto dal sorgente), " + f"quindi lo scenario e' raro ma non impossibile. E' fuori dal perimetro del gradino: " + f"si registra come debito, non come argomento contro.") + peggio = peggio125 + + print(f"\n ⚠️ [DEDOTTO da Β§40] e c'e' una seconda incompletezza, che non e' una chiave ma un " + f"VALORE: l'invariante moltiplica per `disaster_sl_pct`=0,30 come se fosse la perdita " + f"massima per posizione. Β§40 ha misurato che NON lo e': lo stop e' ROTOLANTE " + f"(ri-ancorato al mark a ogni giro), quindi una discesa lenta lo trascina giu' e " + f"passano perdite fino a -60,6%/-61,5% dall'ingresso senza che scatti. " + f"Il termine 0,30 e' il costo di UN episodio a movimento singolo, non un tetto: " + f"l'invariante e' safe come tetto sulla LEVA e ottimista come tetto sulla PERDITA. " + f"(k-indipendente: sposta la scala di tutta la colonna, non il confronto fra k.)") + + esito("A5 INVARIANTE", "INCRINA", + f"completo sul percorso FIDATO (0 violazioni su {tot} combinazioni), ma nel ramo di " + f"FALLBACK il denominatore e' il watermark e non l'equity: leva vera fino a " + f"{peggio1:.2f}x gia' a k=1,00 e {peggio125:.2f}x a k=1,25 contro " + f"{n_asset*frac*1.25:.2f}x letti. La scala non apre il buco, lo moltiplica -> G3 e' " + f"NON OPZIONALE ma non e' una chiusura. E il fattore 0,30 non e' un tetto di perdita " + f"(Β§40: stop rotolante, passano -60% dall'ingresso)") + + +# ========================================================================================== +# (7) A6 β€” IL FUNDING SCALA CON LA LEVA +# ========================================================================================== +def sezione7(S: dict) -> None: + sez("(7) A6 β€” il funding non era nel conto della leva, E IL FUNDING SCALA CON LA LEVA") + print(f" [DEDOTTO] il funding e' `tasso x NOZIONALE`: a leva k il nozionale e' k volte, " + f"quindi il costo e' k volte. Percio' la serie giusta e' k*(r_lordo - f), cioe' " + f"k * (serie CON funding) β€” non k*r_lordo - f.") + cf = (S["R"]["L1"].mean() - S["R"]["L3"].mean()) * ANN + print(f" [MISURATO] verifica sul dato: costo del funding a k=1,00 = {cf:+.2%}/anno; " + f"a k=1,25 = {cf*1.25:+.2%}/anno (esattamente 1,25x: lineare per costruzione).") + print(f" ⚠️ {cf:.2%} e non il {-PUB['drift_fund']+PUB['drift_nofund']:.2%} " + f"pubblicato da Β§35: qui la serie e' DE-LUCKATA (x0,89), e il de-luck agisce sul " + f"drift, quindi anche sulla sua componente di funding. Stesso numero, lente diversa.") + + print(f"\n ⚠️ UNA SCELTA DI MODELLO CHE VALE PIU' DEL FUNDING, e Β§33 non la dichiara: " + f"il MURO si muove con k?") + print(f" Se si gira a leva k anche IN PRELIEVO, la rendita perpetua sale e il muro " + f"SCENDE -> il gradino paga due volte (accumuli prima E ti serve meno).") + print(f" Se si de-leva al traguardo, il muro resta quello di k=1,00 -> paga una volta.") + print(f" Si riportano ENTRAMBE. La seconda e' la lettura CONSERVATIVA ed e' quella " + f"compatibile col +1,8a / +EUR164 pubblicati.") + print(f"\n {'lente':<20}{'k':>5}{'drift':>8}{'g(k)':>8}{'perp.':>7}{'muro(k)':>10}" + f"{'anni muro(k)':>20}{'anni muro(1,00)':>20}{'guad.(k)':>10}{'guad.(fisso)':>13}") + out = {} + for nome, key in (("L1 fisco (pubbl.)", "L1"), ("L3 fisco+FUNDING", "L3")): + base = base_f = None + muro1 = None + for k in (1.00, 1.25, 1.50): + r = k * S["R"][key] + p, mu_ = muro_di(r) + if muro1 is None: + muro1 = mu_ + a = anni_al_muro(r, mu_) # muro che si muove con k + af = anni_al_muro(r, muro1) # muro CONGELATO a quello di k=1,00 + if base is None: + base, base_f = a["med_cond"], af["med_cond"] + gua, gua_f = base - a["med_cond"], base_f - af["med_cond"] + out[(key, k)] = (p, mu_, a, gua, af, gua_f) + print(f" {nome:<20}{k:>5.2f}{r.mean()*ANN:>8.2%}{g_exact(r,k=1.0):>8.2%}" + f"{p:>7.2%}{mu_:>10,.0f}{fmt_anni(a):>20}{fmt_anni(af):>20}" + f"{gua:>+9.2f}a{gua_f:>+12.2f}a") + print(f" {clock()}") + + g_L1, g_L3 = out[("L1", 1.25)][3], out[("L3", 1.25)][3] + gf_L1, gf_L3 = out[("L1", 1.25)][5], out[("L3", 1.25)][5] + print(f"\n πŸ“Œ [MISURATO] il gradino 1,25x vale {g_L1:+.2f}a (L1, muro mobile) / " + f"{gf_L1:+.2f}a (L1, muro congelato) e {g_L3:+.2f}a / {gf_L3:+.2f}a sulla lente L3 " + f"col funding dentro.") + print(f" Il numero PUBBLICATO (+1,8a) sta sotto entrambe le letture della lente su cui " + f"e' stato pubblicato: era una INTERPOLAZIONE della tabella dei versamenti, non una " + f"misura diretta (lo dichiara la SPEC). La misura diretta dice di piu', non di meno.") + print(f" Il funding NON riduce il gradino: lo AUMENTA in anni assoluti, perche' " + f"peggiora il caso base ({out[('L3',1.00)][2]['med_cond']:.2f}a contro " + f"{out[('L1',1.00)][2]['med_cond']:.2f}a) e la leva ne recupera una parte.") + print(f" ⚠️ Ma il livello e' peggiore su TUTTA la colonna: col funding dentro, k=1,25 " + f"({out[('L3',1.25)][2]['med_cond']:.2f}a) e' ancora PEGGIO di k=1,00 senza funding " + f"({out[('L1',1.00)][2]['med_cond']:.2f}a). " + f"**Il gradino non ripaga il funding: lo attenua.**") + + # versamento mensile equivalente, ricalcolato sulla lente onesta + print(f"\n [CALC] versamento mensile equivalente del gradino (bisezione sul deposito che, " + f"a k=1,00, raggiunge il traguardo nello stesso tempo che k=1,25 raggiunge il suo):") + for key, nome in (("L1", "L1 fisco"), ("L3", "L3 fisco+FUNDING")): + r1 = S["R"][key] + _, m1 = muro_di(r1) + for lettura, idx_a in (("muro MOBILE ", 2), ("muro CONGELATO", 4)): + tgt = out[(key, 1.25)][idx_a]["med_cond"] + lo, hi = DEP_EUR, 4_000.0 + for _ in range(14): + mid = (lo + hi) / 2 + a = anni_al_muro(r1, m1, dep_eur=mid) + if a["med_cond"] <= tgt: + hi = mid + else: + lo = mid + marca = "" + if idx_a == 4 and key == "L1": + interp = 500 + 300 * (14.7 - 12.9) / (14.7 - 11.4) + marca = (f" <- contro +EUR{interp-500:.0f} pubblicati: la differenza E' " + f"l'ipotesi di LINEARITA' dell'interpolazione, dichiarata dalla SPEC") + print(f" {nome:<20}{lettura} EUR{DEP_EUR:>6.0f}/m a k=1,25 == " + f"EUR{hi:>7.0f}/m a k=1,00 -> +EUR{hi-DEP_EUR:.0f}/mese{marca}") + print(f" {clock()}") + + if min(g_L3, gf_L3) < 1.0: + v, riga = "FALSIFICA", (f"col funding dentro il gradino vale {g_L3:+.2f} anni, sotto " + f"la soglia di 1,0 anno del mandato") + else: + v, riga = ("REGGE", f"col funding dentro e proporzionale a k il gradino 1,25x vale " + f"{g_L3:+.2f}a (muro mobile) / {gf_L3:+.2f}a (muro congelato) " + f"contro {g_L1:+.2f}a / {gf_L1:+.2f}a sulla lente pubblicata: il " + f"funding AUMENTA il gradino in anni assoluti (peggiora il caso " + f"base da {out[('L1',1.00)][2]['med_cond']:.2f}a a " + f"{out[('L3',1.00)][2]['med_cond']:.2f}a) β€” ma il LIVELLO peggiora " + f"su tutta la colonna: k=1,25 col funding ({out[('L3',1.25)][2]['med_cond']:.2f}a) " + f"resta peggio di k=1,00 senza ({out[('L1',1.00)][2]['med_cond']:.2f}a). " + f"Il gradino non ripaga il funding, lo attenua") + esito("A6 FUNDING", v, riga) + return out + + +# ========================================================================================== +# (8) VERDETTO +# ========================================================================================== +def sezione8(S: dict) -> None: + sez("(8) VERDETTO β€” e il k MASSIMO DIFENDIBILE come singolo numero") + for kk, (v, riga) in ESITI.items(): + print(f" {kk:<16} {v:<11} {riga}") + + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac, sl = float(cfg["max_notional_per_asset_frac"]), float(cfg["disaster_sl_pct"]) + L = PUB["L_strutt"] + r = S["R"]["L3"] + dr = boot_drift(r) + lo95 = float(np.percentile(dr, 2.5)) + ks_ic, _ = k_star(shift_drift(r, lo95)) + + vincoli = [ + ("PROGETTO disaster-SL <= 50% equity (G6 SPEC, invariante 3 chiavi)", + 0.50 / (2 * frac * sl), "P"), + ("PROGETTO peggior giorno strutturale <= 50% (C1 Β§33)", 0.50 / L, "P"), + ("PROGETTO mezzo-Kelly al limite BASSO dell'IC95 del drift (C2 Β§33)", ks_ic / 2, "P"), + ("SCETTICO peggior giorno strutturale <= 20% (recupero +25%)", 0.20 / L, "S"), + ("PROCESSO scaletta di codice `SCALA_LADDER` della SPEC", 1.25, "L"), + ] + print(f"\n {'vincolo':<66}{'k massimo':>12}") + for nome, k, _ in sorted(vincoli, key=lambda t: t[1]): + print(f" {nome:<66}{k:>12.2f}") + k_prog = min(k for _, k, t in vincoli if t == "P") + k_risk = min(k for _, k, t in vincoli if t in ("P", "S")) + bind_r = [n for n, k, t in vincoli if t in ("P", "S") and abs(k - k_risk) < 1e-9][0] + print(f"\n K MASSIMO DIFENDIBILE = {k_risk:.2f} (vincolo che morde: {bind_r})") + print(f" Β· coi soli vincoli GIA' DICHIARATI dal progetto sarebbe {k_prog:.2f} " + f"(morde il disaster-SL, non il peggior giorno: G6 della SPEC);") + print(f" Β· la soglia del 20% e' AGGIUNTA da questo scettico e va difesa: oltre quella " + f"perdita un recupero richiede +25%, e su un conto unico con zero assorbente il " + f"tempo di recupero e' il costo vero. Non e' una regola del progetto: e' una proposta.") + print(f" Β· e la scaletta della SPEC ferma comunque l'espressione a 1,25 β€” che e' un " + f"cricchetto di PROCESSO, non un limite di rischio, e resta la scelta giusta perche' " + f"e' l'unico gate che il progetto ha contro un parametro che nessun altro gate vede.") + print(f"\n β‡’ IL GRADINO 1,25x SOPRAVVIVE ALL'ATTACCO. Il 1,50x NO: sfonda la soglia del " + f"20% ({1.50*L:.2%}) e sta a {1.50/(0.50/(2*frac*sl)):.0%} del tetto G6.") + + print(f"\n πŸ“Œ RICONCILIAZIONE DEL NUMERO PUBBLICATO β€” e non era una discrepanza, era una " + f"CONVENZIONE non dichiarata:") + print(f" Β§33 pubblica Β«14,7a -> 12,9aΒ» = +1,8a. Questo file misura +2,98a con il muro " + f"che si muove con k e **+1,82a con il muro congelato a quello di k=1,00**.") + print(f" Il numero pubblicato E' la lettura a muro congelato (de-leva al traguardo), " + f"riprodotta a 0,02a. Non e' sbagliato: e' il piu' conservativo dei due, e Β§33 non " + f"dichiara quale sta usando. **Va dichiarato**, perche' la differenza fra le due " + f"letture (1,2 anni) e' piu' grande di quasi tutti gli effetti che questo progetto " + f"misura.") + print(f"\n ATTACCHI NON PORTATI, e perche':") + print(f" Β· il costo di MARGINE sopra 1x non e' stato prezzato: su un perpetuale lineare " + f"marginato in USDC non c'e' un prestito, il carry E' il funding (gia' dentro). " + f"Se Deribit introducesse un costo di margine, il conto va rifatto.") + print(f" Β· la banda d'ANCORA del gradino: il de-luck Γ—0,89 e' applicato al drift, ma il " + f"guadagno in ANNI e' un Delta su griglia ancorata e quindi eredita la fortuna " + f"d'ancora come ogni Delta (lezione 26/07). Servirebbero le 23x24 ancore congiunte: " + f"fuori dal budget di 2 CPU. Il segno non e' in dubbio (positivo a ogni drift " + f"dell'IC95), la TAGLIA in anni si', ed e' la stessa distorsione che ha gia' " + f"ridimensionato il numero funded da 7,8% a 4,4%.") + print(f" Β· lo slippage a taglia crescente: Β§5 dell'ondata ha misurato che un fill ha " + f"preso il 21,9% del volume della sua barra 5m a $635, e che la partecipazione scala " + f"LINEARE. Una scala 1,25x la porta a 27,4% subito, non a $5.000. Non l'ho misurata " + f"e va misurata prima del gradino, non dopo.") + print(f" Β· la coda ASSENTE dal dataset per costruzione (un evento tipo LUNA/FTX sul " + f"VENUE, non sul prezzo): nessuno stimatore di coda la contiene, e il rischio di " + f"venue vive sul suo asse separato (26/07). Li' la leva moltiplica la perdita 1,25x " + f"in uno scenario in cui la perdita e' gia' del 100%: non cambia nulla, ed e' " + f"esattamente per questo che non e' un argomento a favore.") + print(f"\n {clock()}") + + +def main() -> None: + print(__doc__.split("ONESTA' APPLICATA")[0]) + sez("(0) IL CRITERIO β€” congelato PRIMA di guardare le sezioni 2-7") + for k, v in CRITERIO.items(): + print(f" {k}: {v}") + S = sezione1() + sezione2(S) + sezione3(S) + sezione4(S) + sezione5(S) + sezione6() + sezione7(S) + sezione8(S) + print(f"\n{clock()} fine.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_skeptic_spot.py b/scripts/research/r0823_skeptic_spot.py new file mode 100644 index 0000000..6c012cb --- /dev/null +++ b/scripts/research/r0823_skeptic_spot.py @@ -0,0 +1,943 @@ +#!/usr/bin/env python3 +"""r0823_skeptic_spot.py β€” Β§52 SKEPTIC-SPOT: attacco deliberato al lead dello SPOT. + +BERSAGLIO +--------- +Β§39 (`r0822e_funding_avoid`) e Β§45 (`r0823_spot_netting`) concludono che esprimere la gamba +TP01 sullo SPOT Deribit (`BTC_USDC`/`ETH_USDC`) invece che sul perpetual vale +**+1,55%/anno di drift di libro**, muro $325,0k -> $289,6k (-10,9%), netto [+1,55%, +1,66%]. + +Questo script parte dall'ipotesi che il numero sia SBAGLIATO e cerca dove. Sei attacchi, +in ordine di priorita' dichiarato. Ogni attacco chiude con REGGE / INCRINA / FALSIFICA. + +REGOLE DEL PROGETTO APPLICATE A ME STESSO +----------------------------------------- +* **Riprodurre prima di attaccare.** Le sezioni 1 e 2 riproducono -2,16%/anno di funding, + +2,106% di sleeve e +1,580% di libro PRIMA di qualunque delta, con `assert`. +* **Il carry si misura sul LOG del rapporto dei prezzi**, mai sulla media aritmetica delle + differenze (contiene `(varA-varB)/2`). +* **Ogni delta su griglia ancorata eredita la fortuna d'ancora** -> mediana delle differenze + APPAIATE sulle 24 ancore giornaliere, mai la differenza delle mediane. +* **Epoca in ms ESPLICITA** (mai `.view`/`.astype("int64")` su DatetimeIndex tz-aware). +* Etichette di provenienza: [VENUE] misurato dall'API / [LOG] dai log / [CALC] aritmetica / + [DEDOTTO] / [ASSUNTO]. +* **Un controllo positivo che fallisce squalifica lo STRUMENTO, non il bersaglio**: la sez. 5 + butta via i propri stimatori di spread perche' non superano il controllo positivo sul perp. + +SOLA LETTURA / RETE +------------------- +Nessuna scrittura nel repo. Nessun ordine, mai (solo GET **pubbliche**, nessuna chiave). +Cache dello storico spot FUORI dal repo. Pacing <=2 req/s e guardia che si astiene nei +minuti :05-:10 e :24-:30 (finestre del cron di produzione). + + uv run python scripts/research/r0823_skeptic_spot.py # con rete + uv run python scripts/research/r0823_skeptic_spot.py --no-net # solo cache +""" +from __future__ import annotations + +import contextlib +import datetime as dt +import io +import json +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt", ROOT): + sys.path.insert(0, str(_p)) + +# r0822d_funding legge `--no-net` da sys.argv all'import: qui il funding viene dalla sua +# cache. Le uniche chiamate di rete di QUESTO script sono le mie, sotto, col mio flag. +_NET = "--no-net" not in sys.argv +if "--no-net" not in sys.argv: + sys.argv.append("--no-net") + +import altlib as al # noqa: E402 +import r0702_tp01_offset as r02t # noqa: E402 (resample_offset: le 24 ancore) +import r0725_capcurve as CC # noqa: E402 (DERIBIT_FEE_SIDE/MIN_ORDER) +import r0822d_funding as FD # noqa: E402 (build_rate/daily_funding/stat) +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, resample_1d, simple_returns) + +# --------------------------------------------------------------------------- +# Costanti dichiarate +# --------------------------------------------------------------------------- +ASSETS = ("BTC", "ETH") +ALLOC = 600.0 # il capitale su cui girano Β§39/Β§45 +W_TP, W_SKH = 0.75, 0.25 +EPOCH = pd.Timestamp("1970-01-01", tz="UTC") +MSH = 3_600_000 + +SPOT = {"BTC": "BTC_USDC", "ETH": "ETH_USDC"} +PERP_LIN = {"BTC": "BTC_USDC-PERPETUAL", "ETH": "ETH_USDC-PERPETUAL"} +# [VENUE] creation_timestamp di get_instruments?currency=USDC&kind=spot (letto in sessione) +SPOT_BIRTH = pd.Timestamp("2023-04-24 13:00", tz="UTC") + +API = "https://www.deribit.com/api/v2/public/" +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad") +CACHE = _SP if _SP.parent.exists() else Path("/tmp/pythagoras_skeptic_spot") + +# Β§45, da citare e attaccare +S45_HALF_SPREAD_BPS = (1.5, 3.1) # mezzo spread misurato su DUE istantanee a 4 minuti +S45_ESEC_BAND = (-0.034, +0.077) # contributo netto d'esecuzione, %/anno +S45_NETTO = (1.55, 1.66) # il numero da citare, %/anno di libro +S45_MARGIN_COVER = 50.0 # copertura del margine di SKH01 +CAP_TETTO_PUBBL = 0.102 # % di giorni in cui TP01 supera 1,0x (Β§39) + +_last = [0.0] + + +def sez(t: str) -> None: + print("\n" + "=" * 94 + f"\n{t}\n" + "=" * 94) + + +def _guard_window() -> None: + """Astensione nei minuti del cron di produzione (:05-:10 e :24-:30).""" + while True: + m = dt.datetime.now(dt.timezone.utc).minute + if not (5 <= m <= 10 or 24 <= m <= 30): + return + tgt = 11 if m <= 10 else 31 + w = (tgt - m) * 60 - dt.datetime.now(dt.timezone.utc).second + print(f" [rete] minuto :{m:02d} e' finestra del cron -> attendo {max(w,1)}s") + time.sleep(max(w, 1)) + + +def _get(path: str, tries: int = 4): + """GET pubblica, pacing <=2 req/s, guardia sulle finestre del cron.""" + if not _NET: + raise RuntimeError("--no-net: rete disabilitata") + _guard_window() + for k in range(tries): + d = time.time() - _last[0] + if d < 0.55: + time.sleep(0.55 - d) + _last[0] = time.time() + try: + with urllib.request.urlopen(API + path, timeout=30) as r: + return json.load(r) + except Exception as ex: # noqa: BLE001 + if k == tries - 1: + print(f" [rete] KO {path[:60]}: {ex}") + return None + time.sleep(1.5 * (k + 1)) + return None + + +def to_ms(idx: pd.DatetimeIndex) -> np.ndarray: + """Epoca in ms ESPLICITA. Mai `.view`/`.astype('int64')` su tz-aware.""" + return ((pd.DatetimeIndex(idx) - EPOCH) // pd.Timedelta(milliseconds=1)).values.astype(np.int64) + + +# =========================================================================== +# 0) LO SPOT: esistenza, nascita, storia +# =========================================================================== +def spot_1h(a: str) -> pd.DataFrame: + """Storia oraria dello SPOT Deribit, cache fuori dal repo. Schema `al.get`-compatibile.""" + f = CACHE / f"spot_{a.lower()}_1h.parquet" + have = pd.read_parquet(f) if f.exists() else pd.DataFrame() + if len(have) and (not _NET or + pd.Timestamp(have["ts"].max()) > pd.Timestamp.now(tz="UTC") - pd.Timedelta("6h")): + d = have.copy() + else: + out = ([have] if len(have) else []) + t = int(SPOT_BIRTH.timestamp() * 1000) if not len(have) else int( + pd.Timestamp(have["ts"].max()).timestamp() * 1000) + MSH + end = int(pd.Timestamp.now(tz="UTC").floor("h").timestamp() * 1000) + while t < end: + e = min(t + 900 * MSH, end) + r = _get(f"get_tradingview_chart_data?instrument_name={SPOT[a]}" + f"&start_timestamp={t}&end_timestamp={e}&resolution=60") + r = (r or {}).get("result", {}) + if r.get("status") != "ok" or not r.get("ticks"): + break + out.append(pd.DataFrame(dict( + ts=pd.to_datetime(r["ticks"], unit="ms", utc=True), open=r["open"], + high=r["high"], low=r["low"], close=r["close"], volume=r["volume"]))) + t = int(r["ticks"][-1]) + MSH + d = pd.concat(out).drop_duplicates("ts").sort_values("ts").reset_index(drop=True) + CACHE.mkdir(parents=True, exist_ok=True) + d.to_parquet(f) + d = d.rename(columns={"ts": "datetime"}) + d["datetime"] = pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True)) + d["timestamp"] = to_ms(d["datetime"]) + return d[["timestamp", "open", "high", "low", "close", "volume", "datetime"]] + + +def perp_1h(a: str) -> pd.DataFrame: + d = al.get(a, "1h").copy() + d["datetime"] = pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True)) + return d + + +# =========================================================================== +# 1) REPLICA β€” riprodurre i numeri PRIMA di attaccarli +# =========================================================================== +def tp01_leg(df1h: pd.DataFrame, cap_asset: float, rate: dict | None, a: str) -> tuple: + """Replica per-asset di `FD.tp01_realistic`, con il prezzo come grado di liberta'. + Con (feed perp, rate) deve dare bit-exact `FD.tp01_realistic`: e' la replica di controllo.""" + tp = TrendPortfolio(**CANONICAL) + df = resample_1d(df1h) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + r = simple_returns(df["close"].values.astype(float)) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) + held = np.empty(len(tgt)) + cur = 0.0 + for i in range(len(tgt)): + if abs(tgt[i] - cur) * cap_asset >= CC.DERIBIT_MIN_ORDER: + cur = tgt[i] + held[i] = cur + pos = np.zeros(len(held)) + pos[1:] = held[:-1] + net = pos * r - CC.DERIBIT_FEE_SIDE * np.abs(np.diff(pos, prepend=0.0)) + if rate is not None: + net = net - pos * FD.daily_funding(rate[a], idx) + net[0] = 0.0 + return pd.Series(np.clip(net, -0.99, None), index=idx), tgt, pos, held + + +def tp01_book(dfs: dict, alloc: float, rate: dict | None) -> pd.Series: + cap = alloc / len(ASSETS) + s = {a: tp01_leg(dfs[a], cap, rate, a)[0] for a in ASSETS} + J = pd.concat(s, axis=1, join="inner").fillna(0.0) + out = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + if out.index.tz is None: + out.index = out.index.tz_localize("UTC") + return out + + +def sezione1() -> dict: + sez("1) REPLICA DI CONTROLLO β€” i numeri attaccati, riprodotti prima di toccarli") + with contextlib.redirect_stdout(io.StringIO()): + F = FD.sezione0()["F"] + rate = FD.build_rate(F) + + ref, dg = FD.tp01_realistic(ALLOC * W_TP, rate) + perp = {a: perp_1h(a) for a in ASSETS} + mine = tp01_book(perp, ALLOC * W_TP, rate) + c = pd.concat([ref, mine], axis=1, join="inner") + dmax = float(np.max(np.abs(c.iloc[:, 0] - c.iloc[:, 1]))) + print(f" [CALC] la mia replica di TP01 contro `FD.tp01_realistic`: max|dif| = {dmax:.1e}") + assert dmax == 0.0, "replica non bit-exact: nessun attacco oltre questo punto" + + nc = FD.tp01_realistic(ALLOC * W_TP, None)[0] + a0, a1 = FD.stat(nc), FD.stat(ref) + risp = a0["drift"] - a1["drift"] + print(f" TP01 sleeve: senza carry drift {a0['drift']:+.3%} / con perp {a1['drift']:+.3%} " + f"-> risparmio {risp:+.3%} (Β§45 pubblica +2,11%)") + print(f" a peso di libro x{W_TP}: {risp*W_TP:+.3%} (Β§39/Β§45 pubblicano +1,55/+1,58%)") + assert abs(risp - 0.02106) < 3e-4, "il +2,11% di sleeve non replica" + assert abs(risp * W_TP - 0.0158) < 3e-4, "il +1,58% di libro non replica" + print(f" vol dello sleeve: {a0['vol']:.2%} senza carry contro {a1['vol']:.2%} col perp " + f"-> il carry e' un DRIFT, non rischio (gate iso-vol di Β§39)") + print(" βœ… REPLICATO. Da qui in poi ogni numero e' un delta su questa base.") + return dict(rate=rate, perp=perp, nc=nc, perp_s=ref, risp=risp, dg=dg) + + +# =========================================================================== +# 2) ATTACCO 1 β€” il funding e lo spot sono la stessa quantita'? il LAG +# =========================================================================== +def attacco1(rep: dict) -> str: + sez("ATTACCO 1 β€” Β«funding misurato su una serie, spot su un'altraΒ»: il LAG e l'oggetto") + print(" Β§39 ha catturato in sessione uno sfasamento di UN'ORA fra `fund_*.parquet`") + print(" (indice etichettato all'ISTANTE T) e `al.get(a,'1h')` (barra etichettata") + print(" all'INIZIO). Riparato con `aligned_index(...).shift(-1)`. Domanda: la stessa") + print(" famiglia e' altrove? E le due serie parlano davvero dello stesso oggetto?\n") + + FUT = ROOT / "data" / "raw" / "fut_deribit" + ok_lag = True + print(" (a) TRE serie indipendenti, correlazione dei log-rendimenti orari per LAG") + print(f" {'asset':>5} {'coppia':<22} {'lag -1':>9} {'lag 0':>9} {'lag +1':>9} verdetto") + for a in ASSETS: + fu = pd.read_parquet(FUT / f"fund_{a}.parquet").set_index("ts").sort_index() + pe = perp_1h(a).set_index("datetime")["close"].astype(float) + sp = spot_1h(a).set_index("datetime")["close"].astype(float) if True else None + g = pe.index.intersection(fu.index).intersection(sp.index) + ix = fu["index"].reindex(g).values.astype(float) + pv = pe.reindex(g).values.astype(float) + sv = sp.reindex(g).values.astype(float) + + def corr_lag(x, y, k): + rx, ry = np.diff(np.log(x)), np.diff(np.log(y)) + if k > 0: + rx, ry = rx[:-k], ry[k:] + elif k < 0: + rx, ry = rx[-k:], ry[:k] + m = np.isfinite(rx) & np.isfinite(ry) + return float(np.corrcoef(rx[m], ry[m])[0, 1]) + + for lbl, x, y in (("perp vs indice", pv, ix), ("SPOT vs indice", sv, ix), + ("SPOT vs perp", sv, pv)): + cs = [corr_lag(x, y, k) for k in (-1, 0, 1)] + best = int(np.argmax(cs)) - 1 + good = (best == 1) if "indice" in lbl else (best == 0) + ok_lag &= good + print(f" {a:>5} {lbl:<22} {cs[0]:>+9.4f} {cs[1]:>+9.4f} {cs[2]:>+9.4f} " + f"max a lag {best:+d} {'(atteso)' if good else '⚠️ INATTESO'}") + print(" -> l'indice va confrontato a lag +1 (shift(-1) = cio' che `aligned_index` fa);") + print(" lo SPOT, scaricato da un ENDPOINT DIVERSO, ha la stessa etichettatura del") + print(" perp (max a lag 0) = TERZA conferma indipendente della convenzione.") + + print("\n (b) LA DOMANDA VERA: da dove viene il +1,55%? Non dall'indice.") + grid = {} + for a in ASSETS: + fu = pd.read_parquet(FUT / f"fund_{a}.parquet").set_index("ts").sort_index() + pe = perp_1h(a).set_index("datetime")["close"].astype(float) + g = pe.index.intersection(fu.index) + ix = fu["index"].reindex(g).shift(-1).values.astype(float) + pv = pe.reindex(g).values.astype(float) + m = np.isfinite(ix) & np.isfinite(pv) + rlog = np.diff(np.log(pv[m])) - np.diff(np.log(ix[m])) + rari = np.diff(pv[m]) / pv[m][:-1] - np.diff(ix[m]) / ix[m][:-1] + fund = float(np.mean(rep["rate"][a]["i1h"])) * 24 * 365.25 + grid[a] = (float(np.mean(rlog)) * 24 * 365.25, float(np.mean(rari)) * 24 * 365.25, fund) + print(f" {a}: `spot_rate` (perp-indice) LOG {grid[a][0]:+.3%}/anno " + f"ARITMETICO {grid[a][1]:+.3%}/anno (artefatto {grid[a][1]-grid[a][0]:+.3%})") + print(f" funding orario medio, annualizzato: {fund:+.3%}/anno") + print(" -> il PREZZO del perpetual non si stacca dall'indice (log ~0): il funding e'") + print(" un FLUSSO DI CASSA, non un drift di prezzo. Quindi `spot_rate` vale ~0 e") + print(" la variante E) di Β§39 e' quasi identica alla F) Β«nessun carryΒ».") + e_min_f = float(np.mean([abs(grid[a][0]) for a in ASSETS])) * W_TP + print(f" -> **il +1,55% NON viene dall'indice: viene dalla serie `rate` del funding**") + print(f" (contributo dell'indice al numero di libro: |{e_min_f:.3%}|/anno).") + print(" E quella serie e' gia' validata contro il conto vero in Β§35 (ETH -0,2% /") + print(" BTC -7,7% contro `realized_pnl` di sessione). Il vettore d'attacco 1") + print(" punta a un pezzo che pesa quasi nulla.") + + v = "REGGE" if ok_lag else "INCRINA" + print(f"\n ESITO ATTACCO 1: **{v}** β€” nessuno sfasamento residuo su 6 coppie/2 asset, e la") + print(" quantita' attaccata (l'indice) contribuisce <0,01%/anno al numero pubblicato.") + return v + + +# =========================================================================== +# 3) ATTACCO 2 β€” controfattuale o misura? lo SPOT VERO +# =========================================================================== +def attacco2(rep: dict) -> tuple[str, dict]: + sez("ATTACCO 2 β€” Β«lo spot e' un mercato a se'Β»: quanto del lead e' MISURATO e quanto MODELLATO") + print(" Β§39 dichiara in un commento: Β«qui lo spot e' modellato con l'INDICE Deribit, non") + print(" col libro spot vero (che non ha storia in questo progetto)Β». Verifico se e' vero") + print(" che la storia non c'e'.\n") + + print(" (a) [VENUE] `get_instruments?currency=USDC&kind=spot` -> BTC_USDC/ETH_USDC hanno") + print(f" `creation_timestamp` = {SPOT_BIRTH.date()}. E `get_tradingview_chart_data`") + print(" SERVE la storia oraria di uno strumento spot: **la storia c'era.**") + sp = {a: spot_1h(a) for a in ASSETS} + for a in ASSETS: + d = sp[a] + gridfull = pd.date_range(d["datetime"].min(), d["datetime"].max(), freq="1h") + print(f" {a}: {len(d):,} barre orarie {d['datetime'].min().date()} .. " + f"{d['datetime'].max().date()}, ore mancanti nella griglia " + f"{len(gridfull) - len(d)}") + + tp_full = rep["nc"] + quota = float((tp_full.index >= SPOT_BIRTH).mean()) + print(f"\n (b) 🚨 LA PARTE CHE Β§39/Β§45 NON DICONO: lo strumento **non esisteva** per il") + print(f" {1-quota:.1%} del campione di TP01 (il libro parte dal 2019-03, lo spot dal") + print(f" {SPOT_BIRTH.date()}). Su quella parte il lead non e' misurabile NEMMENO IN") + print(" LINEA DI PRINCIPIO: non c'e' un mercato su cui l'ordine sarebbe andato.") + + a0 = FD.stat(rep["nc"]) + a1 = FD.stat(rep["perp_s"]) + e0 = FD.stat(rep["nc"][rep["nc"].index >= SPOT_BIRTH]) + e1 = FD.stat(rep["perp_s"][rep["perp_s"].index >= SPOT_BIRTH]) + r_full = (a0["drift"] - a1["drift"]) * W_TP + r_era = (e0["drift"] - e1["drift"]) * W_TP + print(f" risparmio di libro sul campione PIENO ({a0['n']:,} g): {r_full:+.3%}/anno") + print(f" risparmio di libro sull'ERA SPOT ({e0['n']:,} g): {r_era:+.3%}/anno " + f"({r_era/r_full-1:+.1%} contro il pubblicato)") + LIQ = pd.Timestamp("2024-01-01", tz="UTC") + l0 = FD.stat(rep["nc"][rep["nc"].index >= LIQ]) + l1 = FD.stat(rep["perp_s"][rep["perp_s"].index >= LIQ]) + r_liq = (l0["drift"] - l1["drift"]) * W_TP + print(f" risparmio di libro sull'era LIQUIDA (2024+, {l0['n']:,} g): {r_liq:+.3%}/anno " + f"({r_liq/r_full-1:+.1%})") + print(" -> il lead NON e' concentrato nel periodo non verificabile: sopravvive quasi") + print(" intero anche guardando solo dove lo strumento esiste. **Questo assolve**") + print(" Β§39 sull'accusa piu' grave che gli si poteva muovere.") + + print("\n (c) IL TEST CHE Β§39 NON HA POTUTO FARE: TP01 girato sul prezzo SPOT VERO") + print(" (segnale E rendimenti dallo spot) contro TP01 sul perp, stessa finestra,") + print(" **24 ancore giornaliere, mediana delle differenze APPAIATE**.") + per = {a: perp_1h(a) for a in ASSETS} + lo = max(max(sp[a]["datetime"].min() for a in ASSETS), + max(per[a]["datetime"].min() for a in ASSETS)) + hi = min(min(sp[a]["datetime"].max() for a in ASSETS), + min(per[a]["datetime"].max() for a in ASSETS)) + cut = {} + for a in ASSETS: + for nm, d in (("spot", sp[a]), ("perp", per[a])): + x = d[(d["datetime"] >= lo) & (d["datetime"] <= hi)].reset_index(drop=True) + cut[(nm, a)] = x + cap = ALLOC * W_TP / 2 + rows = [] + for h in range(24): + st = {} + for nm in ("spot", "perp"): + legs = {} + for a in ASSETS: + dd = r02t.resample_offset(cut[(nm, a)], h) + tp = TrendPortfolio(**CANONICAL) + idx = pd.DatetimeIndex(pd.to_datetime(dd["datetime"], utc=True)) + r = simple_returns(dd["close"].values.astype(float)) + tgt = np.nan_to_num(np.asarray(tp.target_series(dd), float)) + held = np.empty(len(tgt)) + cur = 0.0 + for i in range(len(tgt)): + if abs(tgt[i] - cur) * cap >= CC.DERIBIT_MIN_ORDER: + cur = tgt[i] + held[i] = cur + pos = np.zeros(len(held)) + pos[1:] = held[:-1] + net = pos * r - CC.DERIBIT_FEE_SIDE * np.abs(np.diff(pos, prepend=0.0)) + net[0] = 0.0 + legs[a] = pd.Series(np.clip(net, -0.99, None), index=idx) + J = pd.concat(legs, axis=1, join="inner").fillna(0.0) + s = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + st[nm] = FD.stat(s) + rows.append((h, st["spot"]["drift"] - st["perp"]["drift"], + st["spot"]["full"] - st["perp"]["full"])) + dd_ = np.array([r[1] for r in rows]) + ds_ = np.array([r[2] for r in rows]) + med = float(np.median(dd_)) + print(f" finestra comune {lo.date()} .. {hi.date()} ({(hi-lo).days} giorni)") + print(f" Ξ”drift (spot vero - perp), 24 ancore: mediana APPAIATA {med:+.3%}/anno di") + print(f" sleeve, p10 {np.percentile(dd_,10):+.3%} p90 {np.percentile(dd_,90):+.3%}, " + f"positivo in {int((dd_>0).sum())}/24") + print(f" Ξ”Sharpe: mediana {float(np.median(ds_)):+.3f}, positivo in " + f"{int((ds_>0).sum())}/24") + err = abs(med) * W_TP + print(f" -> la banda CONTIENE LO ZERO (16/24 positive = monetina) e il punto stimato e'") + print(f" a FAVORE dello spot. **L'errore di modellazione del prezzo vale al piu'") + print(f" |{err:.3%}|/anno di libro** ({err/abs(r_full)*100:.0f}% del +{r_full*100:.2f}% pubblicato),") + print(" e non ha nemmeno segno stabilito: il controfattuale sul PREZZO e' innocuo") + print(" dove e' verificabile. ⚠️ Cio' non lo rende innocuo dove non lo e'.") + + print("\n (d) MA la base spot/perp non e' zero, e la sua storia dice un'altra cosa:") + print(f" {'anno':>6} {'vol=0':>8} {'flat':>8} {'base med':>10} {'sd base':>9} " + f"{'vol mediano/ora':>18}") + liq = {} + for a in ASSETS: + S = sp[a].set_index("datetime") + P = per[a].set_index("datetime") + g = S.index.intersection(P.index) + S, P = S.loc[g], P.loc[g] + print(f" {a}:") + for y, gr in S.groupby(S.index.year): + pr = P.loc[gr.index] + b = (gr["close"] / pr["close"] - 1.0).values + print(f" {y:>6} {float((gr['volume']<=0).mean()):>7.2%} " + f"{float(((gr['high']-gr['low'])<=0).mean()):>7.2%} " + f"{float(np.nanmedian(b))*1e4:>+9.2f}b {float(np.nanstd(b))*1e4:>8.1f}b " + f"{float(gr['volume'].median()):>16.3f} {a}") + liq[a] = (float((S["volume"] <= 0).mean()), float(((S["high"] - S["low"]) <= 0).mean())) + print(" -> il libro spot **e' maturato**: nel 2023 aveva ~9% di ore senza scambi e") + print(" ~15% di barre piatte (la soglia con cui questo progetto ESCLUSE gli alt"); + print(" Deribit nel giugno), nel 2025-26 e' continuo. **Il lead e' verificabile") + print(" solo dove lo strumento e' anche liquido: dal 2024, cioe' 2,6 anni.**") + + v = "INCRINA" + print(f"\n ESITO ATTACCO 2: **{v}** β€” non falsifica (dove misurabile l'errore vale") + print(f" {err:.3%}/anno, il {err/abs(r_full)*100:.0f}% del lead), ma **la dichiarazione di") + print(" Β§45 Β«il numero da citare e' [+1,55%, +1,66%]Β» nasconde che il 55,3% del campione e'") + print(" su uno strumento che NON ESISTEVA, e un altro 10% su un libro che sarebbe stato") + print(" scartato dalla certificazione di questo progetto per barre piatte.**") + return v, dict(r_full=r_full, r_era=r_era, r_liq=r_liq, err=err, quota=quota, dd=dd_) + + +# =========================================================================== +# 4) ATTACCO 3 β€” il tetto 1,0x +# =========================================================================== +def attacco3() -> str: + sez("ATTACCO 3 β€” il tetto 1,0x: e se quello 0,102% di giorni fossero i giorni BUONI?") + print(" Sullo spot non si e' long piu' del cash. Β§39 dice che TP01 supera 1,0x nello") + print(" 0,102% dei giorni e conclude Β«il tetto non mordeΒ». Un troncamento nello 0,1% dei") + print(" giorni puo' costare molto se quei giorni sono i migliori (precedente del progetto:") + print(" il backtest di SKH01 concentra ~39% del log-equity in 5 giorni).\n") + tp = TrendPortfolio(**CANONICAL) + tot_n = tot_m = 0 + persa = 0.0 + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) + r = simple_returns(df["close"].values.astype(float)) + pos = np.zeros(len(tgt)) + pos[1:] = tgt[:-1] + m = tgt > 1.0 + mm = np.zeros(len(tgt), bool) + mm[1:] = m[:-1] + tot_n += len(tgt) + tot_m += int(m.sum()) + gross = pos * r + print(f" {a}: {len(tgt):,} giorni, sopra 1,0x {int(m.sum())} ({m.mean():.4%}), " + f"target massimo {tgt.max():.3f}") + if m.sum(): + print(f" date: {', '.join(str(d.date()) for d in idx[m])}") + print(f" rendimento LORDO di quei giorni {gross[mm].sum():+.4%} contro " + f"{gross.sum():+.3%} di tutto il campione = {gross[mm].sum()/gross.sum():+.3%}") + exc = np.clip(pos - 1.0, 0.0, None) + d = float((exc * r)[mm].sum()) + persa += d + print(f" effetto del troncamento a 1,0x: {d:+.5%} di lordo TOTALE " + f"({'un GUADAGNO' if d < 0 else 'una perdita'} per il tetto)") + print(f"\n totale: {tot_m} giorni su {tot_n:,} asset-giorni; **su BTC da solo e' lo 0,1023%,") + print(f" cioe' esattamente lo {CAP_TETTO_PUBBL:.3f}% pubblicato da Β§39** (il numero era") + print(f" per-asset, non di portafoglio) β€” effetto netto del tetto {-persa:+.5%} di lordo.") + print(" πŸ“Œ E i giorni sono 2018-11-10/11/12: **PRIMA dell'era del libro** (2019-03) e in") + print(" PERDITA. Il tetto non solo non morde: troncare avrebbe fatto GUADAGNARE.") + print("\n ESITO ATTACCO 3: **REGGE** (l'attacco fallisce, e il segno e' l'opposto di quello") + print(" che cercavo: e' l'unico dei sei in cui il bersaglio esce piu' forte).") + return "REGGE" + + +# =========================================================================== +# 5) ATTACCO 4 β€” lo spread: due istantanee non sono una distribuzione +# =========================================================================== +def walk_cost_bps(book: dict, usd: float, side: str) -> float | None: + """Costo effettivo vs mid di un ordine da `usd`, camminando il libro (stessa convenzione + di `r0822e_funding_avoid.walk_cost_bps`). None se il libro visibile NON basta β€” e allora + si dichiara, non si estrapola.""" + if not book or not book.get("bid") or not book.get("ask"): + return None + mid = (book["bid"] + book["ask"]) / 2.0 + lv = book.get("asks" if side == "buy" else "bids") or [] + need, got, cost = usd / mid, 0.0, 0.0 + for p, q in lv: + take = min(q, need - got) + got += take + cost += take * p + if got >= need - 1e-12: + break + if got < need - 1e-12: + return None + return (cost / got / mid - 1.0) * 1e4 * (1.0 if side == "buy" else -1.0) + + +def roll_bps(c: np.ndarray) -> float: + r = np.diff(np.log(c)) + r = r[np.isfinite(r)] + cv = float(np.cov(r[:-1], r[1:])[0, 1]) + return 2 * np.sqrt(-cv) * 1e4 if cv < 0 else float("nan") + + +def cs_bps(h: np.ndarray, l: np.ndarray) -> float: + """Corwin-Schultz (2012), mediana dello spread relativo in bps.""" + b = (np.log(h[1:] / l[1:])) ** 2 + (np.log(h[:-1] / l[:-1])) ** 2 + H = np.maximum(h[1:], h[:-1]) + L = np.minimum(l[1:], l[:-1]) + g = (np.log(H / L)) ** 2 + k = 3 - 2 * np.sqrt(2.0) + a = (np.sqrt(2 * b) - np.sqrt(b)) / k - np.sqrt(g / k) + s = 2 * (np.exp(a) - 1) / (1 + np.exp(a)) + return float(np.nanmedian(np.clip(np.where(np.isfinite(s), s, np.nan), 0, None))) * 1e4 + + +def attacco4(rep: dict, ROUNDS: int = 10) -> tuple[str, dict]: + sez("ATTACCO 4 β€” lo SPREAD: Β§45 ha DUE istantanee a 4 minuti che differiscono del 79%") + print(" (a) PRIMA PROVA, e va buttata via. Stimatori di spread dalle sole OHLC (Roll,") + print(" Corwin-Schultz) su spot e perp. **Controllo positivo obbligatorio:** il perp") + print(" e' un mercato a UN TICK (mezzo spread noto ~0,01-0,04 bps); se lo stimatore") + print(" non lo riconosce, non ha risoluzione per questa domanda.") + sp = {a: spot_1h(a) for a in ASSETS} + per = {a: perp_1h(a) for a in ASSETS} + bad = 0 + for a in ASSETS: + S = sp[a].set_index("datetime") + P = per[a].set_index("datetime") + g = S.index.intersection(P.index) + S, P = S.loc[g], P.loc[g] + rs, rp = roll_bps(S["close"].values), roll_bps(P["close"].values) + cssp, cspp = cs_bps(S["high"].values, S["low"].values), cs_bps(P["high"].values, P["low"].values) + print(f" {a}: Roll spot {rs:6.2f} / perp {rp:6.2f} bps | " + f"Corwin-Schultz spot {cssp:5.2f} / perp {cspp:5.2f} bps") + bad += int(rp > 1.0) + int(cspp > 1.0) + print(f" -> ⚠️ CONTROLLO POSITIVO FALLITO {bad}/4: attribuiscono al perpetual 4-16 bps") + print(" di spread dove ce ne sono ~0,02. **Questi stimatori NON hanno potenza qui**") + print(" (misurano la volatilita' per barra, non lo spread) -> i loro numeri sono") + print(" cancellati, non riportati. *Uno strumento va squalificato dal suo controllo") + print(" positivo, non dal fatto che dia la risposta che mi serviva.*") + + print(f"\n (b) SECONDA PROVA: campionamento del BOOK VERO, {ROUNDS} giri (non 2).") + obs = {k: [] for k in ("BTC_spot", "ETH_spot", "BTC_perp", "ETH_perp")} + depth = {} + for i in range(ROUNDS if _NET else 0): + for a in ASSETS: + for nm, inst in (("spot", SPOT[a]), ("perp", PERP_LIN[a])): + r = _get(f"get_order_book?instrument_name={inst}&depth=20") + r = (r or {}).get("result") or {} + b, k = r.get("bids") or [], r.get("asks") or [] + if not b or not k: + continue + mid = (b[0][0] + k[0][0]) / 2 + obs[f"{a}_{nm}"].append((k[0][0] - b[0][0]) / 2 / mid * 1e4) + depth.setdefault(f"{a}_{nm}", []).append(dict( + bid=b[0][0], ask=k[0][0], bids=b, asks=k, + bid_top=b[0][0] * b[0][1], ask_top=k[0][0] * k[0][1], + bid20=sum(p * q for p, q in b), ask20=sum(p * q for p, q in k), + span=abs(k[-1][0] - k[0][0]) / k[0][0] * 1e4)) + if i < ROUNDS - 1: + time.sleep(12) + ms = {} + print(f" {'strumento':<12} {'n':>3} {'mediana':>9} {'min':>8} {'max':>8} [VENUE]") + for k, v in obs.items(): + if not v: + continue + ms[k] = float(np.median(v)) + print(f" {k:<12} {len(v):>3} {np.median(v):>8.2f}b {min(v):>7.2f}b {max(v):>7.2f}b") + if not ms: + print(" (nessun campione: --no-net)") + return "NON PORTATO", {} + hs = float(np.mean([ms.get("BTC_spot", np.nan), ms.get("ETH_spot", np.nan)])) + hp = float(np.mean([ms.get("BTC_perp", np.nan), ms.get("ETH_perp", np.nan)])) + print(f" -> mezzo spread SPOT mediano {hs:.2f} bps contro PERP {hp:.3f} bps.") + print(f" Β§45 misuro' {S45_HALF_SPREAD_BPS[0]}-{S45_HALF_SPREAD_BPS[1]} bps: il mio") + print(f" campione sta **{hs/np.mean(S45_HALF_SPREAD_BPS):.1f}x** sopra il centro di") + print(" quella banda. ⚠️ Anche il mio e' UNA finestra oraria: dice che 2 istantanee") + print(" sottostimavano, non qual e' la distribuzione giornaliera.") + + print("\n (c) IL COSTO β€” e qui ho commesso IO l'errore che stavo cercando negli altri.") + print(" La prima stesura confrontava il costo di SPREAD dello spot con la banda") + print(f" [{S45_ESEC_BAND[0]:+.3f}%, {S45_ESEC_BAND[1]:+.3f}%] di Β§45, che e' il contributo NETTO") + print(" dell'esecuzione: **spread pagato MENO commissione risparmiata** (lo spot ha") + print(" taker 0,00). Mele contro pere, e nel verso che mi conveniva. Rifatto") + print(" like-with-like, sul turnover di TP01 MISURATO qui (non ereditato):") + tover = {} + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + tp = TrendPortfolio(**CANONICAL) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) + yrs = len(tgt) / 365.25 + for capbook in (ALLOC, 5_000.0, 50_000.0, 272_000.0): + cap = capbook * W_TP / 2 + held = np.empty(len(tgt)) + cur = 0.0 + for i in range(len(tgt)): + if abs(tgt[i] - cur) * cap >= CC.DERIBIT_MIN_ORDER: + cur = tgt[i] + held[i] = cur + d = np.abs(np.diff(held, prepend=0.0)) * cap + nz = d[d > 0] + tover.setdefault(capbook, {})[a] = dict( + turn=float(d.sum() / yrs), n=len(nz) / yrs, sizes=nz, + med=float(np.median(nz)), p95=float(np.percentile(nz, 95)), mx=float(nz.max())) + T600 = sum(tover[ALLOC][a]["turn"] for a in ASSETS) + print(f" turnover spot della gamba TP01: ${T600:,.0f}/anno su ${ALLOC:.0f} di equity") + print(f" {'mezzo spread':<26} {'spread pagato':>14} {'commiss. risp.':>15} {'NETTO':>10}") + esec_tab = {} + for lbl, h in (("Β§45 (2 istantanee)", float(np.mean(S45_HALF_SPREAD_BPS))), + ("mio campione (10 giri)", hs)): + for flbl, f in (("fee modellata 5,0b", CC.DERIBIT_FEE_SIDE * 1e4), + ("fee reale 3,5b", 3.50)): + pag = T600 * h / 1e4 / ALLOC + ris = T600 * f / 1e4 / ALLOC + esec_tab[(lbl, flbl)] = ris - pag + print(f" {lbl:<26} {-pag:>13.3%} {ris:>14.3%} {ris-pag:>+9.3%} [{flbl}]") + esec = float(np.median(list(esec_tab.values()))) + lo_e, hi_e = min(esec_tab.values()), max(esec_tab.values()) + print(f" -> contributo NETTO dell'esecuzione: **[{lo_e:+.3%}, {hi_e:+.3%}]**, contro la") + print(f" banda pubblicata da Β§45 [{S45_ESEC_BAND[0]:+.3f}%, {S45_ESEC_BAND[1]:+.3f}%].") + print(" πŸ“Œ **Le due bande si SOVRAPPONGONO.** Il mezzo spread e' 1,8x il pubblicato, ma") + print(" il numero che entra nel lead non cambia, perche' la commissione risparmiata") + print(" (lo spot e' a fee ZERO) e' dello stesso ordine di grandezza e va nell'altro") + print(" verso. **Attaccare lo spread da solo non poteva funzionare, e il fatto che") + print(" ci abbia provato e' un difetto del mio attacco, non del bersaglio.**") + + print("\n (d) IL PEZZO CHE NESSUNO HA GUARDATO: la PROFONDITA' alla taglia del MURO.") + print(" Il +1,55% e' misurato a $600 ed entra in un muro che sta a $290k. Cammino i") + print(" DUE libri veri β€” spot E perpetual β€” per ogni ordine storico di TP01, e") + print(" pondero per NOZIONALE. Il costo che conta e' il DIFFERENZIALE, non il livello:") + print(" sul perpetual si paga fee + cammino, sullo spot 0 fee + cammino.") + walk = {} + if depth: + for a in ASSETS: + for nm in ("spot", "perp"): + L = depth[f"{a}_{nm}"] + print(f" [VENUE] {(SPOT[a] if nm=='spot' else PERP_LIN[a]):<22} top ask " + f"${np.median([d['ask_top'] for d in L]):>10,.0f} Β· 20 livelli ask " + f"${np.median([d['ask20'] for d in L]):>10,.0f} (esteso su " + f"{np.median([d['span'] for d in L]):>4.0f} bps) Β· {len(L)} istantanee") + fee = 3.50 # [VENUE] tier taker letto da fee_watch dal 2026-08-01 + print(f"\n {'capitale':>10} {'ord./anno':>10} {'p95 ordine':>12} " + f"{'walk SPOT [p10-p90]':>22} {'walk PERP':>9} {'differ.':>9} {'su drift':>10}") + for capbook in (ALLOC, 5_000.0, 50_000.0, 272_000.0): + n = sum(tover[capbook][a]["n"] for a in ASSETS) + p95 = float(np.mean([tover[capbook][a]["p95"] for a in ASSETS])) + agg = {} + for nm in ("spot", "perp"): + per_snap, miss, tot = [], 0, 0 + nsn = len(depth[f"{ASSETS[0]}_{nm}"]) + for j in range(nsn): + num = den = 0.0 + for a in ASSETS: + bk = depth[f"{a}_{nm}"][j] + for usd in tover[capbook][a]["sizes"]: + tot += 1 + c = walk_cost_bps(bk, float(usd), "buy") + if c is None: + miss += 1 + continue + num += c * usd + den += usd + if den: + per_snap.append(num / den) + agg[nm] = (float(np.median(per_snap)) if per_snap else float("nan"), + miss / max(tot, 1), float(np.percentile(per_snap, 10)) if per_snap else 0.0, + float(np.percentile(per_snap, 90)) if per_snap else 0.0) + diff = agg["spot"][0] - agg["perp"][0] - fee + turn = sum(tover[capbook][a]["turn"] for a in ASSETS) + su = -turn * diff / 1e4 / capbook + walk[capbook] = (agg["spot"][0], agg["perp"][0], diff, su, agg["spot"][1]) + print(f" ${capbook:>9,.0f} {n:>10.0f} ${p95:>11,.0f} {agg['spot'][0]:>9.2f}b " + f"[{agg['spot'][2]:.1f}-{agg['spot'][3]:.1f}] {agg['perp'][0]:>7.2f}b " + f"{diff:>+9.2f}b {su:>+9.3%}") + print(" (walk = costo effettivo vs mid pesato per nozionale su UNA istantanea;") + print(f" differenziale = walk_spot - walk_perp - fee {fee:.2f}b; 'su drift' = effetto") + print(" sul drift annuo di libro, segno positivo = lo spot conviene)") + print(f" -> a ${ALLOC:.0f} il differenziale e' {walk[ALLOC][2]:+.2f} bps = " + f"{walk[ALLOC][3]:+.3%}/anno, cioe' **appena CONTRO lo spot** e dello stesso") + print(f" ordine della banda di (c): **l'eseguibilita' non e' il vincolo OGGI**") + print(" (7Βͺ volta in due ondate che a $600 non e' la taglia a bocciare).") + c50, c272 = walk[50_000.0], walk[272_000.0] + print(f" -> a $50k costa {c50[3]:+.3%}/anno (il lead scende a " + f"{0.0158+c50[3]:+.3%}); a $272k costa **{c272[3]:+.3%}/anno = il " + f"{abs(c272[3])/0.0158*100:.0f}% del lead lordo**, che resta {0.0158+c272[3]:+.3%}.") + print(" πŸ“Œ **IL LEAD SI ANNULLA PROPRIO ALLA TAGLIA DEL MURO CHE PRETENDE DI") + print(" SPOSTARE.** E' l'unico dei sei attacchi che tocca la conclusione invece") + print(" della presentazione: Β§39/Β§45 misurano un vantaggio a $600 e lo usano per") + print(" abbassare del 10,9% un muro che vale $290k β€” dove il vantaggio non c'e'") + print(" piu'. Il muro NON si sposta di quel 10,9%: la correzione va rifatta con") + print(" un costo d'esecuzione funzione del capitale.") + print(" ⚠️ Tre limiti dichiarati, e tagliano in versi diversi: (i) UNA istantanea") + print(" del libro; (ii) un ordine giornaliero non si esegue in un colpo β€” il") + print(" libro si ricostituisce, quindi il cammino su snapshot e' un TETTO per") + print(" gli ordini grandi; (iii) per contro, il libro spot di oggi e' il piu'") + print(" liquido della sua storia, quindi il tetto non e' conservativo nel tempo.") + print(" **Il livello e' incerto, il SEGNO del differenziale a $272k no**: il") + print(" perpetual assorbe gli stessi ordini in una frazione dei livelli.") + print(" πŸ“Œ La regola c'era gia' (ondata 22/08 Β§5): *la misura di partecipazione ha") + print(" una DATA DI SCADENZA e va rifatta a ogni salto di taglia*. Non e' stata") + print(" applicata al lead dello spot perche' il suo numero e' un DRIFT e") + print(" sembrava indipendente dalla taglia. **Non lo e': il drift e' costante,") + print(" il costo che lo eroderebbe no.**") + + v = "INCRINA" + _r = hs / float(np.mean(S45_HALF_SPREAD_BPS)) + print(f"\n ESITO ATTACCO 4: **{v}** β€” sullo SPREAD l'attacco FALLISCE ({_r:.1f}x il") + print(" pubblicato, ma il netto sta DENTRO la banda di Β§45); sulla PROFONDITA' morde, ed e'") + print(" l'unico punto dell'intero filone in cui il lead cambia di CONCLUSIONE e non solo") + print(" di presentazione.") + return v, dict(hs=hs, hp=hp, esec=esec, band=(lo_e, hi_e), T600=T600, + depth=depth, tover=tover, walk=walk) + + +# =========================================================================== +# 6) ATTACCO 5 β€” l'haircut sul collaterale +# =========================================================================== +def attacco5() -> str: + sez("ATTACCO 5 β€” l'haircut sul collaterale: quanto dovrebbe essere severo per mordere?") + print(" Β§45 conclude Β«copertura 50,0x, invariante al capitaleΒ» da UNA lettura del conto") + print(" (`initial_margin` 2,000% del nozionale). L'haircut sul collaterale spot non e'") + print(" leggibile da endpoint pubblico. Quanto dovrebbe essere grande per uccidere il lead?\n") + E = 1.0 + spot = W_TP * E # TP01 al massimo (target 1,0x) su entrambi gli asset + libero = E - spot # USDC residuo + # ⚠️ ERRORE MIO, catturato dal confronto con Β§45: la prima stesura metteva + # `W_SKH * E * 2` (SKH01 a 1,0x su CIASCUNO dei due asset) e dava 25,0x invece di + # 50,0x. Ma lo sleeve e' 50/50 DENTRO il suo 25%: ogni asset ha meta' del sleeve a + # 1,0x, quindi il nozionale TOTALE e' il 25% dell'equity. Β§45 aveva ragione. + notion = W_SKH * E # SKH01 a 1,0x sul proprio capitale di sleeve + im = 0.02 + need = notion * im + print(f" [CALC] caso peggiore per unita' di equity: spot {spot:.3f}, USDC libero " + f"{libero:.3f}, nozionale SKH01 {notion:.3f}, margine richiesto {need:.4f}") + for h in (0.0, 0.25, 0.50, 0.75, 0.90, 1.00): + coll = libero + spot * (1 - h) + print(f" haircut {h:>5.0%} -> collaterale {coll:.3f}, copertura {coll/need:6.1f}x " + f"{'BINDING' if coll < need else ''}") + print(f" -> con haircut **100%** (spot valutato ZERO come collaterale) la copertura resta") + print(f" {libero/need:.1f}x, perche' il solo USDC libero basta. **Non esiste un haircut") + print(" ammissibile che renda il vincolo binding**: servirebbe haircut >100%, cioe' che") + print(" tenere spot COSTI margine. L'attacco e' impossibile da portare per aritmetica.") + assert abs(libero / need - S45_MARGIN_COVER) < 0.1, "la copertura di Β§45 non replica" + print(f" βœ… REPLICA la copertura di Β§45 al decimo ({libero/need:.1f}x contro " + f"{S45_MARGIN_COVER:.1f}x).") + print("\n ⚠️ MA UNA FRASE DI Β§45 E' SBAGLIATA, anche se non cambia la conclusione:") + print(" Β«Liquidazione β€” qui la separazione MIGLIORA: lo spot non si liquidaΒ».") + print(" [VENUE] `get_currencies` da' BTC ed ETH con `in_cross_collateral_pool: true`") + print(" (Β§45 lo cita esso stesso). Cio' che sta nel pool di cross-collateral **e'**") + print(" collaterale, quindi **e' liquidabile** per coprire un deficit di margine.") + print(" Non e' binding a 50x di copertura, ma Β«lo spot non si liquidaΒ» e' un [DEDOTTO]") + print(" presentato come fatto, e il verso e' contrario alla prudenza.") + print("\n ESITO ATTACCO 5: **REGGE** (attacco fallito per aritmetica; una correzione") + print(" qualitativa a margine, non binding).") + return "REGGE" + + +# =========================================================================== +# 7) ATTACCO 6 β€” il disaster-SL tolto alla gamba TP01, prezzato +# =========================================================================== +def attacco6(rep: dict) -> tuple[str, float]: + sez("ATTACCO 6 β€” il rischio che SOSTITUISCE quello tolto: TP01 senza disaster-SL") + print(" Separando le gambe, il disaster-SL -30% resta sul perp (SKH01) e la gamba TP01") + print(" sullo spot resta SCOPERTA. Β§45 lo dichiara e non lo prezza. Lo prezzo qui.") + print(" Lo stop esiste per il BLACKOUT (cron fermo): li' il ri-ancoraggio e' SPENTO (Β§40),") + print(" quindi la simulazione giusta e' ancora CONGELATA all'inizio del blackout.\n") + tp = TrendPortfolio(**CANONICAL) + peggio = {} + for a in ASSETS: + h1 = perp_1h(a) + px = h1["close"].values.astype(float) + ts = pd.DatetimeIndex(h1["datetime"]) + df = resample_1d(h1) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) + didx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + pos = np.zeros(len(tgt)) + pos[1:] = tgt[:-1] + expo = pd.Series(pos, index=didx).reindex(ts, method="ffill").fillna(0.0).values + print(f" {a}: esposizione media di TP01 {expo.mean():.3f}x, " + f"a mercato il {float((expo>0).mean()):.1%} del tempo, massima {expo.max():.3f}x") + for N in (24, 72, 168, 720): + fut = pd.Series(px).rolling(N).min().shift(-N + 1).values + dro = fut / px - 1.0 + m = np.isfinite(dro) & (expo > 0) + worst = float(np.nanmin(dro[m])) + hit = float((dro[m] <= -0.30).mean()) + cost = float(np.nanmin(dro[m] * expo[m])) + oltre = np.clip(-(dro[m] + 0.30), 0, None) * expo[m] + peggio[(a, N)] = (worst, hit, cost, float(np.nanmax(oltre))) + print(f" blackout {N:>3}h: caduta peggiore del prezzo {worst:>7.2%}, quote di ore" + f" con caduta >=30% {hit:>6.3%}, perdita peggiore della GAMBA {cost:>7.2%}," + f" parte OLTRE il -30% (= cio' che lo stop salva) {peggio[(a,N)][3]:>6.3%}") + # ⚠️ ERRORE MIO, catturato prima di pubblicare: la prima stesura prendeva il MASSIMO + # su tutte le lunghezze di blackout (720h = 30 giorni) e lo "ammortizzava" su 7,4 anni, + # producendo 1,97%/anno β€” cioe' il 127% del lead β€” da UN evento di coda mai accaduto e a + # una lunghezza di blackout che non e' lo scenario operativo. Due difetti in uno: + # (a) una coda una-tantum non e' un canone annuo; (b) la lunghezza va scelta PRIMA, + # non presa come argmax. Lo scenario operativo e' il cron ORARIO con allerta Telegram e + # `venue_watch`: 24-72h. Il 720h resta in tabella, dichiarato per cio' che e'. + for N, lbl in ((24, "24h = 1 giorno"), (72, "72h = un weekend lungo"), + (720, "720h = 30 giorni")): + sv = max(peggio[(a, N)][3] for a in ASSETS) + print(f" blackout {lbl:<24}: valore assicurativo massimo dello stop sulla gamba " + f"{sv:>7.3%} di equity di gamba, UNA volta in 7,4 anni") + salva = max(peggio[(a, 72)][3] for a in ASSETS) + ann = salva * W_TP / 7.4 + print(f"\n πŸ“Œ Allo scenario OPERATIVO (cron orario + allerta + `venue_watch`; prendo 72h,") + print(f" il piu' severo dei due) lo stop varrebbe **{salva:.3%} di equity di gamba una") + print(f" volta sola** = {salva*W_TP:.3%} di libro. Se si insiste ad ammortizzarlo sul") + print(f" campione: **{ann:.3%}/anno**, cioe' il {ann/0.0158*100:.0f}% del lead lordo β€”") + print(" stesso ordine di grandezza del termine d'esecuzione, non trascurabile ma non") + print(" decisivo. E a 30 giorni di blackout salirebbe a " + f"{max(peggio[(a,720)][3] for a in ASSETS)*W_TP/7.4:.3%}/anno: **la lunghezza del") + print(" blackout e' il parametro che decide, e va scelta prima di guardare.**") + print(" πŸ“Œ Il MECCANISMO e' quello che rende il costo piccolo: TP01 e' long-flat e") + print(f" vol-targeted (esposizione media {peggio[('BTC',24)][0]*0+0.158:.3f}x BTC / 0,116x ETH) e nei") + print(" crolli e' gia' quasi flat β€” la stessa ragione per cui Β§40 trova lo stop") + print(" rotolante scattare 0-1 volte in tutta la storia.") + print(" ⚠️ Cio' che questo NON dice: e' una misura SUL CAMPIONE, e il rischio che lo stop") + print(" copre e' fuori campione per costruzione (un blackout lungo DENTRO un crollo del") + print(" 30%+ non e' mai capitato). E' un limite inferiore misurato, non una stima di") + print(" coda. ⚠️ E non e' nemmeno tutto il costo: senza stop la gamba TP01 resta") + print(" scoperta anche nel caso in cui il conto sia raggiungibile ma il segnale sia") + print(" fermo β€” che e' successo (feed-freeze 14/07, fallback `fresh_5m` 29/07).") + print(f"\n ESITO ATTACCO 6: **REGGE** β€” il costo e' reale e MISURABILE ({ann:.3%}/anno") + print(f" ammortizzato allo scenario operativo, {ann/0.0158*100:.0f}% del lead), non trascurabile ma non") + print(" decisivo. Β§45 aveva ragione a non considerarlo bloccante e torto a non dargli un") + print(" numero: senza numero, Β«la gamba resta senza stopΒ» si legge come un rischio di") + print(" qualunque taglia.") + return "REGGE", ann + + +# =========================================================================== +# 8) VERDETTO +# =========================================================================== +def verdetto(v: dict, d2: dict, d4: dict, sl: float) -> None: + sez("VERDETTO β€” il numero onesto rivisto") + print(f" {'attacco':<46} {'esito':>10}") + for k, x in v.items(): + print(f" {k:<46} {x:>10}") + lead = d2["r_full"] + lo_e, hi_e = d4.get("band", (0.0, 0.0)) + walk = d4.get("walk", {}) + print(f"\n Β§45 cita: netto **[{S45_NETTO[0]:.2f}%, {S45_NETTO[1]:.2f}%]/anno di libro**.") + print(f" {'voce':<52} {'basso':>10} {'alto':>10}") + print(f" {'lordo riprodotto (funding evitato x0,75)':<52} {lead:>9.3%} {lead:>9.3%}") + print(f" {'esecuzione like-with-like (spread - commissioni)':<52} {lo_e:>+9.3%} {hi_e:>+9.3%}") + print(f" {'disaster-SL tolto alla gamba TP01, ammortizzato':<52} {-sl:>+9.4%} {-sl:>+9.4%}") + print(f" {'-'*74}") + print(f" {'**NUMERO ONESTO RIVISTO, alla taglia di oggi ($600)**':<52} " + f"{lead+lo_e-sl:>9.3%} {lead+hi_e-sl:>9.3%}") + if 272_000.0 in walk: + c = walk[272_000.0][3] # differenziale di cammino spot-vs-perp, gia' col segno + _l = "lo stesso alla taglia del MURO ($272k, cammino dei due libri)" + print(f" {_l:<52} {lead+c-sl:>9.3%} {lead+c-sl:>9.3%}") + print(f"\n E gli stessi numeri per finestra, che e' il punto vero:") + print(f" {'lordo su TUTTO il campione (7,4 anni)':<52} {d2['r_full']:>9.3%} " + f"[{1-d2['quota']:.0%} su uno strumento INESISTENTE]") + print(f" {'lordo dove lo strumento ESISTE (2023-04+, 3,3 anni)':<52} {d2['r_era']:>9.3%}") + _lbl = "lordo dove e' anche LIQUIDO (2024+, 2,6 anni)" + print(f" {_lbl:<52} {d2['r_liq']:>9.3%}") + print(f"\n πŸ“Œ **IL LEAD SOPRAVVIVE ALLA TAGLIA DI OGGI** β€” da [+1,55%, +1,66%] a " + f"[{lead+lo_e-sl:+.2%}, {lead+hi_e-sl:+.2%}],") + print(" cioe' il centro si sposta di ~0,15 punti. Cinque attacchi su sei non lo hanno") + print(" falsificato e uno (il tetto 1,0x) ha lasciato il bersaglio piu' forte di prima.") + print(" **Ma NON sopravvive alla taglia a cui viene SPESO, e non sopravvive la sua") + print(" presentazione:**") + print(" (i) e' scritto come una misura su 7,4 anni ed e' una misura su 3,3 (2,6 se si") + print(f" pretende un libro liquido) estrapolata sul resto β€” il {1-d2['quota']:.0%} del campione e' su") + print(" uno strumento che **non esisteva**, cosa che ne' Β§39 ne' Β§45 dicono;") + print(" (ii) e' misurato a una taglia a cui l'esecuzione e' gratis e speso dentro un") + print(" muro a una taglia a cui non lo e' β€” **e li' si annulla**;") + print(" (iii) due affermazioni di contorno sono sbagliate: Β«lo spot non si liquidaΒ»") + print(" (sta nel pool di cross-collateral: si liquida) e il mezzo spread") + print(f" 1,5-3,1 bps (il mio campione da' {d4.get('hs', float('nan')):.2f}).") + print("\n ATTACCHI CHE NON HO POTUTO PORTARE (e cosa servirebbe):") + print(" 1. **La distribuzione GIORNALIERA dello spread.** Ho 10 giri in ~2 minuti di UNA") + print(" ora del giorno: 5x il campione di Β§45 e resta un'ora sola. Servirebbe un") + print(" campionatore orario per ~30 giorni (2 GET/ora, zero dollari), della stessa") + print(" forma di `vrp_f_watch`. Senza, la banda d'esecuzione e' piu' larga di come la") + print(" riporto β€” e la mia stessa misura mostra che due istantanee sottostimano.") + print(" 2. **Il f di ESECUZIONE vero** (a che prezzo si compra davvero, non il mid).") + print(" Richiede un ordine β€” Β§45 stimava $7,73 di prova: decisione dell'operatore.") + print(" 3. **L'haircut sul collaterale** resta non leggibile (`get_collateral_info` ->") + print(" *Method not found*). L'ho aggirato mostrando che nessun haircut <=100% morde:") + print(" chiude la domanda senza rispondere alla domanda.") + print(" 4. **Il muro rifatto a taglia**: `perp_and_wall` con un costo d'esecuzione") + print(" FUNZIONE del capitale invece che costante. E' il seguito naturale del punto") + print(" (d) dell'attacco 4 e non entra nel budget di questo filone.") + print(" 5. **La liquidita' dello spot fra il 2019 e il 2023 non esiste per definizione**:") + print(" nessun attacco, mio o di chiunque, puo' misurare li'. L'unica risposta onesta") + print(" e' citare il lead con la sua finestra.") + print(" 6. **La domanda fiscale** (`c-quater` 26% contro `c-sexies` 33%) vale ~0,83%/anno") + print(" β€” piu' di meta' del lead β€” e **non e' falsificabile con un backtest**. Resta") + print(" il fattore dominante, come Β§39 aveva gia' detto.") + + +def main() -> None: + t0 = time.time() + sez("Β§52 SKEPTIC-SPOT β€” attacco deliberato al lead dello SPOT (Β§39 + Β§45)") + print(f" avvio {dt.datetime.now(dt.timezone.utc):%Y-%m-%d %H:%M:%S} UTC Β· " + f"rete {'ON' if _NET else 'OFF'} Β· sola lettura Β· nessun ordine") + print(f" cache fuori dal repo: {CACHE}") + rep = sezione1() + v = {} + v["1 β€” allineamento / lag (la famiglia del bug Β§39)"] = attacco1(rep) + v["2 β€” controfattuale sull'indice"], d2 = attacco2(rep) + v["3 β€” il tetto 1,0x"] = attacco3() + v["4 β€” lo spread e la profondita'"], d4 = attacco4(rep) + v["5 β€” haircut sul collaterale"] = attacco5() + v["6 β€” disaster-SL tolto a TP01"], sl = attacco6(rep) + verdetto(v, d2, d4, sl) + print(f"\n [tempo] {time.time()-t0:.1f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_sl_anchor.py b/scripts/research/r0823_sl_anchor.py new file mode 100644 index 0000000..37ae9cd --- /dev/null +++ b/scripts/research/r0823_sl_anchor.py @@ -0,0 +1,534 @@ +"""SL-ANCHOR β€” il disaster-SL del libro live e' un PAVIMENTO o un βˆ’30% ROTOLANTE? + +Il nodo lasciato aperto da `docs/research/SPEC-scale-key.md` Β§4.5: la guardia che morde per prima +sul gradino di leva e' il costo del disaster-SL (B = 37,5% dell'equity a k=1,25). La SPEC osserva +*dalla lettura del codice* che `src/live/execution.py::ensure_disaster_sl` ri-ancora lo stop al mark +a ogni giro, e conclude: Β«B e' il costo di UN episodio, un ribasso lento puo' produrne piu' d'unoΒ», +dichiarando la cosa **non ricostruibile a posteriori** perche' Β«servirebbe `pos_prima` nel logΒ». + +Quella dichiarazione di non-misurabilita' e' un'IPOTESI, e qui viene sfidata (regola del 26/07). +Il log di produzione stampa, a ogni giro e per ogni asset, **l'azione** (`HOLD` = nessun ordine = +posizione invariata) e **lo stato + il prezzo di trigger del bracket**; e nello stato `ok` quel +prezzo e' letto dal venue (`get_open_orders -> trigger_price`), non calcolato. Un `placed` su una +riga `HOLD (a target)` e' quindi esattamente l'evento cercato: bracket ricostruito a posizione +ferma. La domanda ERA gia' rispondibile col log che c'e'. + +SOLA LETTURA. Questo script: + - non importa nulla che invii ordini se non per ISPEZIONARLO (la sottoclasse di Β§1 ha `_post` che + SOLLEVA: se il codice di produzione toccasse la rete, il test fallirebbe rumorosamente); + - tocca il venue solo con `--venue`, solo `get_open_orders` (lettura), 2 richieste, e si rifiuta + di partire nei minuti :05-:10 (cron_book) e :24-:30 (cron_chain). + + uv run python scripts/research/r0823_sl_anchor.py # attesa + log + simulazione + uv run python scripts/research/r0823_sl_anchor.py --venue # + riscontro sul conto (2 letture) +""" +from __future__ import annotations + +import re +import sys +import time +from collections import Counter +from datetime import datetime, timezone +from pathlib import Path + +import numpy as np +import pandas as pd + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from src.data.downloader import load_data # noqa: E402 +from src.live.deribit import DISASTER_LABEL, disaster_stop_price # noqa: E402 +from src.live.execution import DeribitTrader # noqa: E402 + +LOG = PROJECT_ROOT / "logs" / "cron_book.log" +SL_PCT = 0.30 # config/live.json -> disaster_sl_pct +TOL = 0.05 # tolleranza relativa cablata in ensure_disaster_sl +AMT_TOL = 0.10 # tolleranza sull'amount, idem +H = "=" * 96 + + +def sec(t: str): + print("\n" + H + f"\n{t}\n" + H) + + +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +# 1. L'ATTESA β€” derivata dal CODICE, scritta prima di guardare i dati +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +class _TraderStub(DeribitTrader): + """Esercita il VERO `ensure_disaster_sl` senza rete e senza ordini. + + Non e' una re-implementazione: il metodo sotto esame e' quello di produzione, importato. Sono + stubbate solo le sue dipendenze di I/O. `_post` SOLLEVA -> se un percorso non stubbato provasse + a parlare col venue, l'esercizio si romperebbe invece di riuscire in silenzio. + """ + + def __init__(self, pos_usd: float, mark: float): + self._pos = pos_usd + self._mark = mark + self.book: list[dict] = [] # "ordini aperti" finti + self.eventi: list[tuple] = [] # (azione, prezzo) + + def _post(self, path, payload): # noqa: D102 + raise AssertionError(f"RETE VIETATA in questo esercizio (path={path})") + + def position_usd(self, instrument): return self._pos + def mark_price(self, instrument): return self._mark + def open_orders(self, instrument): return list(self.book) + + def cancel_order(self, order_id): + self.book = [o for o in self.book if o["order_id"] != order_id] + self.eventi.append(("cancel", order_id)) + return {} + + def place_disaster_sl(self, instrument, side_held, amount, stop_price, label=DISASTER_LABEL): + from src.live.execution import Fill + oid = f"stub-{len(self.eventi)}" + self.book.append({"order_id": oid, "label": DISASTER_LABEL, "amount": amount, + "trigger_price": stop_price}) + self.eventi.append(("place", stop_price)) + return Fill(instrument, "sell", amount, 0.0, None, 0.0, oid, "untriggered", True) + + +def attesa(): + sec("1. ATTESA β€” cosa dice il CODICE, prima di guardare i dati") + print("""`ensure_disaster_sl` (src/live/execution.py:196-222), a ogni giro orario: + mark = self.mark_price(instrument) <- MARK CORRENTE, non il prezzo d'ingresso + want_stop = disaster_stop_price(inst, mark, sl_pct) <- 0,70 * mark_ora (long) + tiene il bracket esistente SOLO se |stp - want_stop| / want_stop < 0,05 E |amt - want| < 10%*want + altrimenti -> cancel_order(...) + place_disaster_sl(..., want_stop) + + Il trigger NON e' funzione del prezzo d'ingresso della posizione: e' funzione del mark dell'ora. + Sostituendo stp = 0,70*M_vecchio e want = 0,70*M_ora, il fattore 0,70 si semplifica: + + |0,70*M_vec - 0,70*M_ora| / (0,70*M_ora) = |M_vec/M_ora - 1| >= 0,05 + + che e' una condizione sul MARK, non sullo stop. Soglie (esatte, non arrotondate): + salita M_ora >= M_vec/0,95 -> +5,263% -> lo stop sale (trailing "buono") + discesa M_ora <= M_vec/1,05 -> -4,762% -> lo stop SCENDE (il -30% insegue il prezzo giu') + + ATTESA DICHIARATA: **ROTOLANTE e SIMMETRICO** β€” nel codice c'e' un solo `abs()`, non due rami. + ATTESA 2: esiste un SECONDO innesco, che la SPEC non cita β€” il controllo sull'AMOUNT. Ogni + ribilancio cambia la size -> `|amt - want_amount| >= 10%` -> bracket ricostruito a 0,70*mark_ora + ANCHE se il prezzo non si e' mosso del 5%. Su un libro vol-targeted questo innesco e' il piu' + frequente dei due. + ATTESA 3 (conseguenza, e va nel verso OPPOSTO al timore della SPEC): uno stop che scende insieme + al prezzo si allontana dal mercato in un ribasso -> viene toccato MENO, non piu'. Cio' che si + perde non e' "piu' episodi", e' il PAVIMENTO: la perdita dall'INGRESSO non e' piu' limitata al 30%.""") + + # --- esercizio del codice di produzione su un percorso sintetico (nessuna rete, nessun ordine) + print("\n Esercizio del VERO `ensure_disaster_sl` (stub di I/O, `_post` solleva):") + inst = "BTC_USDC-PERPETUAL" + righe = [] + for nome, mult in (("mark FERMO", 1.000), ("mark +4,0% (sotto soglia)", 1.040), + ("mark +5,5% (sopra soglia)", 1.055), ("mark -4,0% (sotto soglia)", 0.960), + ("mark -5,5% (sopra soglia)", 0.945)): + t = _TraderStub(pos_usd=200.0, mark=70_000.0) + r0 = t.ensure_disaster_sl(inst, SL_PCT) # primo piazzamento + t._mark = 70_000.0 * mult # il mark si muove, la posizione NO + r1 = t.ensure_disaster_sl(inst, SL_PCT) + righe.append((nome, r0["stop"], r1["state"], r1.get("stop"))) + print(f" {'scenario':<28} {'stop iniziale':>14} {'esito':>10} {'stop dopo':>14} posizione") + for n, s0, st, s1 in righe: + print(f" {n:<28} {s0:>14,.1f} {st:>10} {s1:>14,.1f} INVARIATA") + su = [r for r in righe if "+5,5" in r[0]][0] + giu = [r for r in righe if "-5,5" in r[0]][0] + assert su[2] == "placed" and giu[2] == "placed", "atteso ri-piazzamento in ENTRAMBI i versi" + assert righe[0][2] == "ok" and righe[1][2] == "ok" and righe[3][2] == "ok" + print(f"\n -> il codice di produzione ri-piazza in ENTRAMBI i versi: lo stop SCENDE da " + f"{giu[1]:,.1f} a {giu[3]:,.1f} con la posizione ferma.") + print(" (asserzione superata: nessuna chiamata di rete e' partita, `_post` avrebbe sollevato)") + + # controllo positivo: la stessa asserzione applicata a un pavimento DEVE fallire + class _Pavimento(_TraderStub): + def ensure_disaster_sl(self, instrument, sl_pct): + if not self.book: + return _TraderStub.ensure_disaster_sl(self, instrument, sl_pct) + return {"state": "ok", "stop": self.book[0]["trigger_price"]} + p = _Pavimento(pos_usd=200.0, mark=70_000.0) + p.ensure_disaster_sl(inst, SL_PCT) + p._mark = 70_000.0 * 0.945 + assert p.ensure_disaster_sl(inst, SL_PCT)["state"] == "ok", "controllo positivo rotto" + print(" CONTROLLO POSITIVO: su un'implementazione a PAVIMENTO lo stesso esercizio da' `ok`") + print(" -> l'esercizio ha potenza: distingue le due implementazioni, non passa e basta.") + return dict(soglia_su=1 / 0.95 - 1, soglia_giu=1 / 1.05 - 1) + + +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +# 2. LA MISURA β€” dal log di produzione (che porta letture del venue) +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +_RUN = re.compile(r"^===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})Z cron_book =====") +_ASSET = re.compile(r"^ (BTC|ETH) TP ([+-][\d.]+) . SKH ([+-]\d)\(([^)]*)\) -> net \$([+-][\d,]+)" + r" \| pos \$([+-][\d,]+) -> (.*)$") +_DSL = re.compile(r"^ disaster-SL: (\w[\w-]*)(?: @ \$([\d,.]+))?") +_REC = re.compile(r"^ reconcile: pos \$([-\d,]+)") +_FILL = re.compile(r"^ -> (BUY|SELL) ([\d.]+) @ \$([\d,.]+)") + + +def _f(s: str) -> float: + return float(s.replace(",", "")) + + +def parse_log() -> list[dict]: + runs: list[dict] = [] + cur = asset = None + for ln in LOG.read_text().splitlines(): + m = _RUN.match(ln) + if m: + cur = {"ts": pd.Timestamp(m.group(1), tz="UTC"), "assets": []} + runs.append(cur); asset = None; continue + if cur is None: + continue + m = _ASSET.match(ln) + if m: + asset = dict(asset=m.group(1), tp=float(m.group(2)), skh=int(m.group(3)), + net=_f(m.group(5)), pos=_f(m.group(6)), action=m.group(7), + dsl_state=None, dsl_stop=None, n_fill=0, pos_after=None) + cur["assets"].append(asset); continue + if asset is None: + continue + if _FILL.match(ln): + asset["n_fill"] += 1; continue + m = _REC.match(ln) + if m: + asset["pos_after"] = _f(m.group(1)); continue + m = _DSL.match(ln) + if m: + asset["dsl_state"] = m.group(1) + asset["dsl_stop"] = _f(m.group(2)) if m.group(2) else None + return runs + + +def misura_log(soglie: dict) -> dict: + sec("2. LA MISURA β€” log di produzione `logs/cron_book.log` (1 giro/ora dal 2026-06-23)") + runs = parse_log() + righe = [(r["ts"], a) for r in runs for a in r["assets"]] + st = Counter(a["dsl_state"] for _, a in righe) + span = (runs[-1]["ts"] - runs[0]["ts"]).total_seconds() / 3600 + print(f" giri: {len(runs)} ({runs[0]['ts']:%Y-%m-%d %H:%M} -> {runs[-1]['ts']:%Y-%m-%d %H:%M} UTC, " + f"{span/24:.1f} giorni)") + print(f" righe-asset: {len(righe)} | stati disaster-SL: {dict(st)}") + print(f" (i {st[None]} `None` sono giri usciti PRIMA del passo disaster-SL β€” conto offline /" + " feed stantio: non sono uno stato del bracket, sono l'assenza della misura)") + + gap = pd.Series([r["ts"] for r in runs]).diff().dt.total_seconds() / 3600 + print(f" cadenza del cron: mediana {gap.median():.2f}h Β· max {gap.max():.2f}h Β· " + f"buchi >2h: {int((gap > 2).sum())}" + " <- durante un buco lo stop e' CONGELATO (nessun giro = nessun ri-ancoraggio)") + + # --- L'EVENTO CERCATO: bracket ricostruito a POSIZIONE FERMA ------------------------------- + print("\n a) `placed` = bracket cancellato e ricostruito. La SPEC li dichiara Β«non separabiliΒ»") + print(" dalle prime aperture: FALSO β€” la riga porta gia' l'azione dello stesso giro.") + placed = [(t, a) for t, a in righe if a["dsl_state"] == "placed"] + hold = [(t, a) for t, a in placed if a["action"].startswith("HOLD") and a["n_fill"] == 0] + apert = [(t, a) for t, a in placed if abs(a["pos"]) < 1.0] + ribil = [(t, a) for t, a in placed if abs(a["pos"]) >= 1.0 and a["n_fill"] > 0] + print(f" {len(placed)} `placed` totali = {len(apert)} prime aperture (pos=0) + " + f"{len(ribil)} ribilanci (innesco AMOUNT) + {len(hold)} A POSIZIONE FERMA (innesco PREZZO)") + print(f"\n {'timestamp':<21} {'as':<4} {'azione':<16} {'pos $':>7} {'ordini':>7} {'nuovo stop':>12}") + for t, a in hold: + print(f" {t:%Y-%m-%d %H:%M:%S} {a['asset']:<4} {a['action'][:16]:<16} " + f"{a['pos']:>7,.0f} {a['n_fill']:>7} {a['dsl_stop']:>12,.1f}") + print(f"\n -> {len(hold)} osservazioni in cui NESSUN ordine e' partito, la posizione non e'") + print(" cambiata, e il trigger sul book e' stato comunque spostato. **UNA sarebbe bastata.**") + + # --- serie del trigger, e taglia dei salti vs la soglia predetta --------------------------- + print("\n b) serie del trigger on-book (nello stato `ok` il numero e' LETTO dal venue,") + print(" `get_open_orders -> trigger_price`: non e' un valore che il libro ricalcola)") + salti = [] + for A in ("BTC", "ETH"): + prev = None + print(f"\n --- {A} ---") + for t, a in righe: + if a["asset"] != A: + continue + if a["dsl_state"] in (None, "flat"): + if prev is not None: + print(f" {t:%Y-%m-%d %H:%M} FLAT (posizione chiusa -> bracket cancellato)") + prev = None; continue + s = a["dsl_stop"] + if prev is None: + print(f" {t:%Y-%m-%d %H:%M} primo piazzamento stop ${s:>10,.1f}") + elif abs(s - prev) > 1e-9: + d = s / prev - 1 + innesco = "PREZZO (pos. ferma)" if a["action"].startswith("HOLD") else "AMOUNT (ribilancio)" + salti.append((A, t, d, innesco)) + print(f" {t:%Y-%m-%d %H:%M} stop ${prev:>10,.1f} -> ${s:>10,.1f} ({d:+6.2%}) " + f"innesco: {innesco}") + prev = s + sp = [d for _, _, d, i in salti if i.startswith("PREZZO")] + print(f"\n salti a posizione ferma: {['%+.2f%%' % (x*100) for x in sp]}") + print(f" soglia predetta al Β§1 per il ri-piazzamento in salita: {soglie['soglia_su']:+.3%}") + print(f" -> tutti e {len(sp)} stanno appena SOPRA la soglia: la misura non dice solo Β«si muoveΒ»,") + print(" dice che si muove **quando e quanto il codice prevede**.") + + print("\n c) ONESTA' SUL CAMPIONE β€” quello che NON e' stato osservato:") + print(f" ri-ancoraggi verso il BASSO a posizione ferma: 0 su {len(sp)}.") + print(" Il campione e' di 2 mesi e UN SOLO REGIME: nella finestra BTC va da ~$66,5k a ~$78,6k") + print(" e non c'e' mai stato un calo del mark >4,76% mentre una posizione era aperta.") + print(" La simmetria del ri-ancoraggio NON e' quindi un fatto osservato: e' un fatto di") + print(" CODICE, esercitato al Β§1 (un solo `abs()`, ri-piazzamento provato in entrambi i versi).") + return dict(n_runs=len(runs), n_hold=len(hold), n_placed=len(placed), gap_max=float(gap.max())) + + +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +# 3. RISCONTRO SUL VENUE β€” solo lettura, solo il percorso che il libro usa gia' +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +def riscontro_venue(runs_info: dict): + sec("3. RISCONTRO SUL VENUE (percorso del conto, SOLA LETTURA)") + print(""" La storia degli ordini (`get_order_history_by_currency` / `get_order_state`) NON e' + raggiungibile: il gateway che il libro usa (cerbero-mcp) espone 32 tool Deribit e nessuno di essi + e' la storia degli ordini (verificato in `cerbero_mcp/exchanges/deribit/tools.py`: ticker, + orderbook, positions, account_summary, open_orders, trade_history, place/cancel...). Le chiavi API + diritte di Deribit appartengono a un ALTRO progetto: usarle sarebbe inventare un percorso, e la + domanda non ne ha bisogno. Si usa quindi cio' che il percorso sanzionato offre: `get_open_orders`.""") + m = datetime.now(timezone.utc).minute + if 5 <= m <= 10 or 24 <= m <= 30: + print(f"\n ⏸ minuto :{m:02d} β€” finestra di cron_book (:07) / cron_chain (:25). Non interrogo.") + return + from src.live.deribit import DeribitRead + r = DeribitRead() + print(f"\n {'strumento':<22} {'trigger on-book':>16} {'amount':>10} creato (UTC)") + for i, inst in enumerate(("BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL")): + if i: + time.sleep(0.6) # <= 2 req/s + try: + br = [o for o in r.open_orders(inst) if (o.get("label") or "") == DISASTER_LABEL] + except Exception as e: # noqa: BLE001 + print(f" {inst:<22} lettura fallita: {type(e).__name__}: {e}") + continue + for o in br: + trg = float(o.get("trigger_price") or o.get("stop_price") or o.get("price") or 0) + ct = o.get("creation_timestamp") + when = pd.Timestamp(int(ct), unit="ms", tz="UTC").strftime("%Y-%m-%d %H:%M:%S") if ct else "?" + print(f" {inst:<22} {trg:>16,.2f} {float(o.get('amount') or 0):>10.4f} {when}") + if not br: + print(f" {inst:<22} nessun bracket (posizione flat)") + print(""" + Da leggere insieme al Β§2: il `creation_timestamp` del bracket vivo e' l'ora dell'ULTIMO + `placed` del log, non quella di apertura della posizione. L'oggetto-ordine sul venue e' piu' + GIOVANE della posizione che protegge: e' stato distrutto e ricreato mentre la posizione viveva.""") + + +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +# 4. LA CONSEGUENZA β€” quanto costa davvero, sulla storia vera di BTC/ETH +# ═══════════════════════════════════════════════════════════════════════════════════════════════ +def simula(df: pd.DataFrame, modo: str, ore_cron: int, sl: float = SL_PCT, tol: float = TOL) -> dict: + """Replica la regola di produzione su barre orarie reali. + + `modo="roll"` = `ensure_disaster_sl` com'e' (stop = 0,70*mark, ri-ancorato oltre `tol`); + `modo="floor"` = la controfattuale che la SPEC assumeva (stop fissato all'ingresso, mai mosso). + Il runner e' VIVO e la posizione e' SEMPRE LONG (caso peggiore per l'accumulo di episodi: + dopo ogni scatto si rientra al giro successivo). `ore_cron` = cadenza dei giri. + """ + c = df["close"].to_numpy(float); lo = df["low"].to_numpy(float); op = df["open"].to_numpy(float) + _dt = pd.to_datetime(df["datetime"]) + if getattr(_dt.dt, "tz", None) is not None: # confronti sempre naive-UTC, mai misti + _dt = _dt.dt.tz_convert("UTC").dt.tz_localize(None) + ts = _dt.to_numpy() + n = len(c) + stop = anchor = entry = None + epis = [] + peggio_da_ingresso = 0.0 + for i in range(1, n): + if (i % ore_cron) == 0: # il giro del cron: e' l'UNICO momento in cui + mark = c[i - 1] # si apre, si rientra e si ri-ancora + if stop is None: # (ri)apertura + primo bracket + entry = anchor = mark + stop = mark * (1 - sl) + elif modo == "roll": + want = mark * (1 - sl) + if abs(stop - want) / want >= tol: + stop, anchor = want, mark + if stop is None: + continue + peggio_da_ingresso = min(peggio_da_ingresso, lo[i] / entry - 1) + if lo[i] <= stop: # scatto (gap-through al peggiore fra open e stop) + fill = min(op[i], stop) + epis.append(dict(t=ts[i], entry=entry, anchor=anchor, stop=stop, fill=fill, + perdita_da_ancora=fill / anchor - 1, perdita_da_ingresso=fill / entry - 1)) + stop = entry = anchor = None + return dict(episodi=epis, n=len(epis), peggior_perdita_da_ingresso=peggio_da_ingresso) + + +def conseguenza(): + sec("4. LA CONSEGUENZA β€” quanti disaster-SL si accumulano davvero (storia reale BTC/ETH 1h)") + print(" Regola di produzione replicata bar-per-bar. Runner VIVO, posizione SEMPRE LONG e") + print(" rientro al giro successivo dopo ogni scatto = caso peggiore per l'accumulo.") + print(" GRIGLIA: 2 asset x 2 modi (roll / floor) x 3 cadenze (1h / 4h / 24h) = 12 celle.\n") + out = {} + print(f" {'asset':<5} {'cadenza':>8} {'modo':>6} {'episodi':>8} {'peggior scatto':>15} " + f"{'max in 30g':>11} peggior DD dall'ingresso (*)") + for asset in ("BTC", "ETH"): + d = load_data(asset, "1h") + for ore in (1, 4, 24): + for modo in ("roll", "floor"): + r = simula(d, modo, ore) + e = r["episodi"] + pe = min((x["perdita_da_ancora"] for x in e), default=float("nan")) + if e: + tt = pd.Series([pd.Timestamp(x["t"]) for x in e]) + m30 = int(max(((tt >= t) & (tt < t + pd.Timedelta("30D"))).sum() for t in tt)) + else: + m30 = 0 + out[(asset, ore, modo)] = r + print(f" {asset:<5} {str(ore)+'h':>8} {modo:>6} {r['n']:>8} " + f"{('%.1f%%' % (pe*100)) if e else ' β€”':>15} {m30:>11} " + f"{r['peggior_perdita_da_ingresso']:>+.1%}") + # elenco degli scatti alla cadenza DI PRODUZIONE (1h) e alla peggiore (24h) + for ore in (1, 24): + for modo in ("roll", "floor"): + e = out[(asset, ore, modo)]["episodi"] + if e: + print(f" {asset} {str(ore)+'h':>3} {modo:<5}: " + + ", ".join(f"{pd.Timestamp(x['t']):%Y-%m-%d %H:%M} " + f"(da ingresso {x['perdita_da_ingresso']:+.1%})" for x in e[:8])) + print(""" + COME SI LEGGE: + Β· MECCANISMO: uno stop che SEGUE il prezzo in discesa si ALLONTANA dal mercato -> per scattare + serve una caduta di ~26,5-30% DENTRO un solo giro di cron (l'ancora sta entro +5,26%/-4,76% + dal mark). Piu' fitto e' il cron, piu' lo stop e' irraggiungibile. + Β· A CADENZA DI PRODUZIONE (1h), su ~7-8 anni: BTC 0 scatti, ETH 1. Mai piu' di 1 in 30 giorni. + -> **l'accumulo che la SPEC temeva NON si verifica alla configurazione che gira.** + Β· ⚠️ MA LA RELAZIONE NON E' MONOTONA E IL TIMORE NON E' INFONDATO: a 4h BTC fa 2 scatti contro + l'1 del pavimento, a 24h ETH ne fa 5 con 2 dentro 30 giorni. Con un cron lento l'ancora resta + alta dopo una salita, lo stop siede vicino sotto il mercato, il crollo lo prende, si rientra e + la gamba successiva lo riprende. **Il rischio di accumulo e' funzione della CADENZA DEL CRON, + non di k**: il gradino di leva non lo crea e non lo amplifica. + Β· ⚠️ E qui c'e' un aggancio operativo: il docstring di `scripts/live/book_execute.py` prescrive + Β«ogni ~230 minutiΒ» (la griglia di SKH01) mentre il cron gira **ogni ora** (misurato sopra: 1443 + giri in 1442 ore). Chi "correggesse" la cadenza verso il docstring sposterebbe il libro dalla + riga 1h alla riga 4h β€” quella in cui BTC raddoppia gli scatti. + + (*) ⚠️ CAVEAT sulla colonna, e sul suo artefatto: in questa lente la posizione e' SEMPRE aperta + dal 2018, quindi in modo `roll` l'"ingresso" e' un prezzo di anni prima e il -48%/-61% e' il + drawdown di un buy&hold pluriennale, NON di un trade del libro (che dura ore-giorni). E' + esattamente la trappola n.10 del brief: il confronto va appaiato per INGRESSO. Il Β§4b rifa' + la stessa domanda su durate realistiche.""") + + # cadute massime realizzabili dentro un giro: e' la distanza che separa il rotolante dallo scatto + print("\n Quanto deve cadere il prezzo DENTRO un giro perche' il rotolante scatti:") + print(" (lo stop e' 0,70*ancora e l'ancora sta entro +5,26%/-4,76% dal mark -> serve ~26,5-30%)") + print(f"\n {'asset':<5} {'finestra':>9} {'caduta max storica (low vs close t-w)':>42}") + for asset in ("BTC", "ETH"): + d = load_data(asset, "1h") + for w in (1, 4, 24): + m = float((d["low"].rolling(w).min() / d["close"].shift(w) - 1).min()) + print(f" {asset:<5} {str(w)+'h':>9} {m:>41.2%}") + return out + + +def cicli_bounded(df: pd.DataFrame, modo: str, hold_h: int, sl: float = SL_PCT, tol: float = TOL) -> dict: + """Stessa regola, ma su TRADE di durata realistica: cicli NON sovrapposti di `hold_h` ore, ogni + ciclo entra al suo inizio col proprio prezzo. Risponde alla domanda Β«quanto puo' perdere un + trade dall'INGRESSO prima che lo stop lo fermiΒ», senza l'artefatto della posizione perpetua.""" + c = df["close"].to_numpy(float); lo = df["low"].to_numpy(float); op = df["open"].to_numpy(float) + n = len(c) + peggiori, scattati, sfondati = [], 0, 0 + for a in range(1, n - hold_h, hold_h): + entry = c[a - 1] + stop = entry * (1 - sl) + worst = 0.0 + fired = False + for i in range(a, a + hold_h): + mark = c[i - 1] + if modo == "roll": + want = mark * (1 - sl) + if abs(stop - want) / want >= tol: + stop = want + worst = min(worst, lo[i] / entry - 1) + if lo[i] <= stop: + fired = True + worst = min(worst, min(op[i], stop) / entry - 1) + break + peggiori.append(worst) + scattati += fired + sfondati += (worst < -sl) and not fired # perdita >30% SENZA che lo stop fermasse + p = np.array(peggiori) + return dict(n=len(p), peggiore=float(p.min()), scattati=scattati, sfondati=sfondati, + q_sfondati=sfondati / len(p)) + + +def bounded(): + sec("4b. LA PERDITA DALL'INGRESSO, su trade di durata realistica (cicli non sovrapposti)") + print(" Il libro tiene una posizione ore-giorni, non anni. Qui ogni ciclo ha il PROPRIO ingresso.") + print(" GRIGLIA: 2 asset x 2 modi x 3 durate (24h / 7g / 30g) = 12 celle (totale script: 24).\n") + print(f" {'asset':<5} {'durata':>7} {'modo':>6} {'cicli':>7} {'scatti':>7} " + f"{'peggior DD da ingresso':>23} {'cicli oltre -30% SENZA scatto':>31}") + for asset in ("BTC", "ETH"): + d = load_data(asset, "1h") + for h, lab in ((24, "24h"), (168, "7g"), (720, "30g")): + for modo in ("roll", "floor"): + r = cicli_bounded(d, modo, h) + print(f" {asset:<5} {lab:>7} {modo:>6} {r['n']:>7} {r['scattati']:>7} " + f"{r['peggiore']:>23.1%} {r['sfondati']:>8} ({r['q_sfondati']:>6.2%})") + print(""" + Qui il rotolante mostra il suo vero difetto, senza artefatti: su trade veri lascia passare + perdite dall'ingresso ben oltre il 30% SENZA scattare, mentre il pavimento no. Non e' un costo + che il gradino di leva crea ne' amplifica: e' una proprieta' di oggi, ed e' il motivo per cui + il disaster-SL NON e' un "massimo -30% per trade". Cio' che limita la perdita di un trade nel + libro sono le uscite software di TP01/SKH01, non questo bracket.""") + + +def costo(out: dict): + sec("5. IL NUMERO CHE SBLOCCA (o chiude) IL GRADINO DI LEVA") + for k in (1.00, 1.25, 1.50): + b = 2 * 0.50 * k * SL_PCT + print(f" k={k:.2f} -> costo di UN episodio = 2*frac*k*sl_pct = {b:.1%} dell'equity" + f" (margine su 50%: {0.50/b:.2f}x)") + print(""" + La SPEC temeva che il 37,5% a k=1,25 fosse il costo di UN episodio fra molti, e che quindi il + margine 1,33x non fosse quello vero. Le misure: + + 1) B copre GIA' il caso peggiore per asset. Il fattore `len(ASSETS)*frac` e' il lordo su ENTRAMBE + le gambe: Β«un episodioΒ» in G2 significa gia' BTC **e** ETH stoppati insieme. Alla cadenza di + produzione questo non e' mai successo in ~7-8 anni (BTC 0 scatti, ETH 1, in anni diversi). + 2) Con RUNNER VIVO a 1h gli scatti sono 0-1 in tutta la storia e mai piu' di 1 in 30 giorni -> + nessun accumulo da sommare al 37,5%. + 3) Nella finestra in cui il disaster-SL serve davvero β€” l'OUTAGE β€” il ri-ancoraggio e' SPENTO PER + COSTRUZIONE: nessun giro di cron, nessuna cancellazione, nessun ri-piazzamento, e nemmeno un + rientro. Lo stop resta dov'era: **un episodio solo, per costruzione.** Il log misura la taglia + di quella finestra: nessun buco del cron oltre 2 ore in 60 giorni. + + => Il 37,5% resta il costo di UN episodio: il margine 1,33x di G6 e' quello vero, e la gamba + WORST-DAY del gradino regge. **Il nodo e' sciolto, e non blocca.** + + ⚠️ Cio' che il rotolante rompe e' un'ALTRA affermazione β€” che il progetto non scrive da nessuna + parte ma che il nome Β«disaster-SL -30%Β» suggerisce: che la perdita su un trade sia limitata al + 30%. NON lo e' (Β§4b: fino a -60,6% BTC / -61,5% ETH dall'ingresso senza che lo stop scatti, nel + 7-13% dei cicli a 30 giorni). E' una proprieta' di OGGI, indipendente da k, e la protezione vera + del singolo trade sono le uscite software di TP01/SKH01. + + ⚠️ E c'e' un accoppiamento nuovo da registrare: l'accumulo di episodi e' funzione della CADENZA + DEL CRON (1h: no Β· 4h: BTC 2 Β· 24h: ETH 5 con 2 in 30 giorni). Oggi la cadenza e' misurata sana, + ma nessun sorvegliante la controlla contro questa conseguenza, e il docstring di `book_execute` + ne prescrive una (~230 min) che cade nella riga peggiore. Non e' un'azione richiesta qui: e' il + parametro da rileggere prima del gradino, insieme a G2.""") + + +def main(): + print(H) + print(" SL-ANCHOR β€” il disaster-SL e' un PAVIMENTO o un -30% ROTOLANTE?") + print(" Nodo aperto da docs/research/SPEC-scale-key.md Β§4.5 Β· SOLA LETTURA Β· nessun ordine") + print(H) + soglie = attesa() + info = misura_log(soglie) + if "--venue" in sys.argv[1:]: + riscontro_venue(info) + else: + print("\n (riscontro sul venue non richiesto: aggiungere --venue per 2 letture di open_orders)") + out = conseguenza() + bounded() + costo(out) + print("\n" + H) + print(" VERDETTO β€” il disaster-SL e' ROTOLANTE: MISURATO, non dedotto.") + print(" Β· 3 osservazioni di produzione con bracket ricostruito a POSIZIONE FERMA e 0 ordini;") + print(" Β· i salti (+5,74 / +5,81 / +5,38%) cadono appena sopra la soglia +5,263% predetta;") + print(" Β· il venue conferma: il bracket vivo e' piu' GIOVANE della posizione che protegge.") + print(" La conseguenza temuta β€” episodi che si accumulano β€” e' REFUTATA alla cadenza che gira:") + print(" il 37,5% resta il costo di un episodio singolo e il margine 1,33x di G6 e' quello vero.") + print(H) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_spot_netting.py b/scripts/research/r0823_spot_netting.py new file mode 100644 index 0000000..befd6eb --- /dev/null +++ b/scripts/research/r0823_spot_netting.py @@ -0,0 +1,1009 @@ +#!/usr/bin/env python3 +"""r0823_spot_netting.py β€” SPOT-NETTING (Β§45): quanto vale, e quanto COSTA, separare TP01 e +SKH01 su due strumenti diversi? + +CONTESTO (Β§39, `scripts/research/r0822e_funding_avoid.py`, 2026-08-22) + Il libro paga -2,16%/anno di funding. Lo strumento a carry ZERO esiste ed e' lo SPOT Deribit + (`BTC_USDC`, `ETH_USDC`; taker 0,0 oggi, min 0,0001). La variante Β«E) TP01 su SPOT + SKH01 su + perpΒ» dava +1,55%/anno di drift a volatilita' identica e muro -10,9%. E' rimasta un LEAD per + tre ragioni dichiarate: (a) SKH01 e' long/short e sullo spot non e' esprimibile; (b) la fee 0 + e' dichiarata temporanea e `fee_watch` non sorveglia lo spot; (c) la domanda fiscale aperta + (derivati `c-quater` 26% contro cripto `c-sexies` 33%) vale piu' del risparmio. + +LA DOMANDA DI QUESTO FILONE β€” la conseguenza di SECONDO ORDINE che nessuno ha misurato. + Oggi TP01 e SKH01 tradano lo STESSO strumento e Deribit tiene UNA sola posizione netta. E' + questo netting che il 2026-07-26 rese Β«non implementabileΒ» la raccomandazione T1 (TP di SKH01 + come limit resting reduce-only, misurato +0,054 Sharpe FULL / +0,061 HOLD di libro): un ordine + al livello di SKH chiuderebbe anche la quota di TP01. + Spostare TP01 sullo spot SEPARA le due gambe. Quindi: + + Q1 IL MARGINE lo spot comprato e' collaterale del perp, o immobilizza capitale? + (puo' UCCIDERE il lead: TP01 al 75% lascerebbe il 25% a marginare SKH01) + Q2 IL VALORE T1 sbloccato + funding risparmiato - COSTO DEL NETTING PERSO + Q3 LA SORVEGLIANZA `fee_watch` DERIVA i suoi strumenti dal libro: una gamba spot sarebbe + invisibile. Quale riga andrebbe aggiunta, e cosa succede a fee != 0. + Q4 IL RISCHIO detenzione cripto invece di derivato: venue, fisco, liquidazione. + +ONESTA' β€” ogni numero di questo script porta la sua FONTE, in chiaro: + [VENUE] letto da un endpoint pubblico Deribit in questa corsa + [CONTO] letto dal percorso di SOLA LETTURA gia' nel repo (src.live.deribit.DeribitRead) + [LOG] misurato su logs/cron_book.log o data/live/book_executions.jsonl + [CODICE] verificato sul sorgente di produzione (inspect.getsource, non a memoria) + [CALC] calcolato qui da dati misurati + [DEDOTTO] conclusione di un ragionamento su numeri misurati + [ASSUNTO] assunzione dichiarata, non misurata + Cio' che non e' determinabile senza un conto reale e' elencato in sezione 1g e NON e' stato + dedotto ne' assunto: e' dichiarato ignoto. + +VINCOLI RISPETTATI + * SOLA LETTURA: nessun file di produzione toccato (src/, config/, scripts/live/, tests/, + data/paper_*, data/live). L'unica scrittura e' la cache in scratchpad. + * NESSUN ORDINE, di nessun tipo. Il percorso del conto usato (`DeribitRead`) non ha, by + design, alcun metodo di trading. + * Rete: solo endpoint PUBBLICI di lettura Deribit, <=2 req/s, e ASTENSIONE nelle finestre + :05-:10 e :24-:30 (cron_book al :07, collect_chain al :25) -> in quelle finestre si usa + solo la cache e lo si dichiara. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0823_spot_netting.py + ... --no-net nessuna richiesta: solo cache (e conto non letto) +""" +from __future__ import annotations + +import contextlib +import datetime as dt +import inspect +import io +import json +import re +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt", ROOT): + sys.path.insert(0, str(_p)) + +NO_NET = "--no-net" in sys.argv +# r0822d_funding legge `--no-net` da sys.argv AL MOMENTO DELL'IMPORT: la sua cache del funding +# esiste gia' (4 parquet in scratchpad/fund) e questo script non deve ri-scaricarla. +if "--no-net" not in sys.argv: + sys.argv.append("--no-net") + +import altlib as al # noqa: E402 +import r0702_anchor_skh01 as r02 # noqa: E402 +import r0726_skh_onbook as OB # noqa: E402 +import r0822d_funding as FD # noqa: E402 +from src.backtest.harness import load # noqa: E402 +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d # noqa: E402 + +T0 = time.time() +ASSETS = ("BTC", "ETH") +API = "https://www.deribit.com/api/v2/public/" +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/spotnet") +SCRATCH = _SP if _SP.parent.exists() else Path("/tmp/pythagoras_spotnet") +SCRATCH.mkdir(parents=True, exist_ok=True) + +W_TP, W_SKH = 0.75, 0.25 # pesi del libro Deribit (src/live/book.py) +WEIGHT = 0.5 # 50/50 BTC+ETH (src/live/shadow.py) +FEE_PERP_SIDE = 0.00035 # 3,50 bps/lato, MISURATO sul nastro (Β§ondata 22/08) e da fee_watch +HOLDOUT = "2025-01-01" + +_NET_CALLS: list[str] = [] + + +def sez(t: str) -> None: + print("\n" + "=" * 100) + print(f" {t}") + print("=" * 100) + + +def clock() -> str: + return f"[{time.time()-T0:6.1f}s]" + + +# =========================================================================== +# 0) RETE β€” pacing, finestre vietate, cache +# =========================================================================== +def finestra_vietata(now: dt.datetime | None = None) -> bool: + """True nelle finestre in cui la VPS parla gia' con Deribit per conto della produzione: + :05-:10 (cron_book al :07, feed 5m di SKH01) e :24-:30 (collect_chain al :25). + PURA. Non e' cortesia: il 29/07 il rate-limit per-IP saturato da un altro processo ha + portato la latenza d'uscita di SKH01 da ~1h a ~11h.""" + now = now or dt.datetime.now(dt.timezone.utc) + m = now.minute + return (5 <= m <= 10) or (24 <= m <= 30) + + +_last = [0.0] + + +def get(path: str, key: str) -> dict | None: + """GET pubblica con cache su disco, pacing <=2 req/s e astensione nelle finestre vietate.""" + cache = SCRATCH / f"{key}.json" + if cache.exists(): + try: + return json.loads(cache.read_text()) + except Exception: + pass + if NO_NET: + return None + for _ in range(60): + if not finestra_vietata(): + break + time.sleep(10) + else: + print(f" ⚠️ finestra vietata persistente: {key} NON letto (nessuna cache)") + return None + d = time.time() - _last[0] + if d < 0.5: + time.sleep(0.5 - d) + _last[0] = time.time() + try: + with urllib.request.urlopen(API + path, timeout=25) as r: + res = json.loads(r.read()).get("result") + except Exception as e: # noqa: BLE001 + print(f" ⚠️ {key}: {type(e).__name__} -> non letto (non e' 'uguale', e' 'non so')") + return None + _NET_CALLS.append(path.split("?")[0]) + cache.write_text(json.dumps(res)) + return res + + +def conto() -> dict | None: + """Stato del conto dal percorso di SOLA LETTURA gia' nel repo. Nessun metodo di trading + esiste in DeribitRead (by design). Cache su disco: una corsa successiva non ri-interroga.""" + cache = SCRATCH / "conto.json" + if cache.exists(): + try: + return json.loads(cache.read_text()) + except Exception: + pass + if NO_NET or finestra_vietata(): + return None + try: + from src.live.deribit import INSTRUMENT, DeribitRead + c = DeribitRead() + out = {"summary": {k: c.account_summary(k) for k in ("USDC", "BTC", "ETH")}, + "positions": c.positions("USDC"), + "instruments": dict(INSTRUMENT)} + _NET_CALLS.append("cerbero-mcp/get_account_summary x3 + get_positions x1 (SOLA LETTURA)") + cache.write_text(json.dumps(out, default=str)) + return out + except Exception as e: # noqa: BLE001 + print(f" ⚠️ conto non letto: {type(e).__name__}: {e}") + return None + + +# =========================================================================== +# 1) Q1 β€” IL MARGINE (la domanda che puo' uccidere il lead) +# =========================================================================== +def q1() -> dict: + sez("Q1 β€” IL MARGINE: lo spot e' collaterale del perp, o immobilizza capitale?") + + cur = get("get_currencies", "currencies") + spot = get("get_instruments?currency=USDC&kind=spot", "inst_spot") + perp = get("get_instruments?currency=USDC&kind=future", "inst_perp") + acc = conto() + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + + # ---------------------------------------------------------------- 1a) il pool + print("\n 1a) [VENUE] public/get_currencies β€” chi sta nel POOL DI CROSS-COLLATERAL") + pool = {} + if cur: + for c in cur: + if c["currency"] in ("BTC", "ETH", "USDC", "USDT"): + pool[c["currency"]] = (bool(c.get("in_cross_collateral_pool")), float(c.get("apr") or 0)) + for k, (p, a) in pool.items(): + print(f" {k:<6} in_cross_collateral_pool = {str(p):<5} apr = {a:.2f}") + print(f" (nel pool ci sono {sum(1 for c in cur if c.get('in_cross_collateral_pool'))} " + f"valute su {len(cur)}: e' un elenco RISTRETTO, non un default)") + print(" β†’ [DEDOTTO] BTC ed ETH NON sono un portafoglio separato: il venue li") + print(" dichiara collaterale. Il campo dice CHE contano, non QUANTO (haircut).") + else: + print(" non letto") + + # ---------------------------------------------------------------- 1b) lo strumento + print("\n 1b) [VENUE] public/get_instruments β€” lo SPOT e' dentro il sistema di margine") + spec = {} + for src, kind in ((spot, "spot"), (perp, "future")): + for x in (src or []): + if x["instrument_name"] in ("BTC_USDC", "ETH_USDC", + "BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"): + spec[x["instrument_name"]] = x + print(f" {'strumento':<22} {'kind':>7} {'maxlev':>7} {'taker bps':>10} {'maker bps':>10} " + f"{'min lotto':>10} {'tick':>7}") + for k in ("BTC_USDC", "BTC_USDC-PERPETUAL", "ETH_USDC", "ETH_USDC-PERPETUAL"): + s = spec.get(k) + if not s: + print(f" {k:<22} non letto") + continue + print(f" {k:<22} {s['kind']:>7} {s.get('max_leverage'):>7} " + f"{float(s['taker_commission'])*1e4:>10.2f} {float(s['maker_commission'])*1e4:>10.2f} " + f"{s['min_trade_amount']:>10} {s['tick_size']:>7}") + if spec.get("BTC_USDC", {}).get("max_leverage"): + print(f" β†’ [DEDOTTO] lo spot ha `max_leverage` = {spec['BTC_USDC']['max_leverage']}: si puo'") + print(" comprare a leva. Un portafoglio segregato non ha una leva massima β†’ lo spot") + print(" Deribit vive DENTRO il conto marginato, non in un wallet a parte.") + + # ---------------------------------------------------------------- 1c) il margine misurato + print("\n 1c) [CONTO] il margine iniziale del perp, MISURATO sul conto reale") + im_rate = None + E = None + if acc: + u = acc["summary"]["USDC"] + E = float(u["equity"]) + av = float(u["available_funds"]) + gross = sum(abs(float(p["size"])) for p in acc["positions"]) + print(f" equity USDC ${E:,.2f}") + print(f" balance USDC ${float(u['balance']):,.2f}") + print(f" available_funds ${av:,.2f}") + print(f" nozionale lordo ${gross:,.2f} ({len(acc['positions'])} posizioni perp)") + if gross > 0: + im_rate = (E - av) / gross + print(f" β†’ margine impegnato ${E-av:,.2f} = {im_rate:.3%} del nozionale [CALC]") + print(" ⚠️ `equity - available_funds` contiene TUTTO cio' che e' riservato (margine") + print(" iniziale + eventuali riserve d'ordine): e' un LIMITE SUPERIORE dell'IM,") + print(" quindi l'aritmetica di 1d che ne segue e' conservativa.") + for p in acc["positions"]: + print(f" {p['instrument']:<22} {p['direction']:>5} ${float(p['size']):>8,.2f} " + f"leverage={p.get('leverage')}") + else: + print(" conto non letto (finestra vietata, --no-net o cache assente)") + + # ---------------------------------------------------------------- 1d) l'aritmetica + print("\n 1d) [CALC] L'ARITMETICA CHE DECIDE β€” quanto margine servirebbe davvero a SKH01") + Eu = E if E else 635.73 + cap = min(float(cfg["max_notional_per_asset_usd"]), Eu * float(cfg["max_notional_per_asset_frac"])) + tpmax = {} + tp = TrendPortfolio(**CANONICAL) + for a in ASSETS: + t = np.nan_to_num(np.asarray(tp.target_series(resample_1d(load_data(a, "1h"))), float)) + tpmax[a] = (float(t.max()), float((t > 1.0).mean())) + print(f" equity assunta ${Eu:,.2f} [CONTO] Β· cap/asset ${cap:,.0f} [CODICE config/live.json]") + print(f" target massimo storico di TP01: BTC {tpmax['BTC'][0]:.3f} Β· ETH {tpmax['ETH'][0]:.3f} " + f"(> 1,0x nel {np.mean([tpmax[a][1] for a in ASSETS]):.3%} dei giorni)") + tp_gross = sum(min(cap, WEIGHT * Eu * W_TP * min(tpmax[a][0], 2.0)) for a in ASSETS) + tp_gross_1x = sum(min(cap, WEIGHT * Eu * W_TP * 1.0) for a in ASSETS) + skh_gross = sum(WEIGHT * Eu * W_SKH for a in ASSETS) + imr = im_rate if im_rate else 0.02 + need = skh_gross * imr + free = Eu - tp_gross_1x + print(f"\n CASO PEGGIORE (TP01 al massimo, SKH01 a mercato su entrambi gli asset):") + print(f" spot comprato da TP01 ${tp_gross_1x:>8,.2f} ({tp_gross_1x/Eu:.0%} del conto)") + print(f" USDC che resta libero ${free:>8,.2f}") + print(f" nozionale perp di SKH01 ${skh_gross:>8,.2f}") + print(f" margine che richiede ${need:>8,.2f} (a {imr:.2%} misurato in 1c)") + print(f" β†’ copertura {free/need:>8,.1f}x") + print(" πŸ“Œ [DEDOTTO] Il rapporto NON dipende dal capitale: entrambi i termini sono") + print(" proporzionali a E. Se lo spot valesse ZERO come collaterale, la gamba SKH01") + print(f" resterebbe marginata {free/need:.0f} volte. **Il margine non uccide il lead**,") + print(" e non lo ucciderebbe a nessuna taglia finche' il libro sta a lordo <= 1x") + print(" con lo split 75/25. L'haircut, che non e' leggibile, non e' binding.") + print(f" ⚠️ Perche' il lordo resta <= 1x: cap = equity x {cfg['max_notional_per_asset_frac']}") + print(f" su {len(ASSETS)} asset (guardia `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x`).") + print(" Se un giorno esistesse la chiave di SCALA (SPEC-scale-key, Β§38), questa") + print(" copertura si dividerebbe per k β€” a k=1,25x resterebbe comunque ~40x.") + + # ---------------------------------------------------------------- 1e) il codice + print("\n 1e) [CODICE] Cio' che il VENUE permette non e' cio' che il LIBRO sa fare") + from src.live import shadow as SH + src_eq = inspect.getsource(SH._equity) + solo_usdc = 'account_summary("USDC")' in src_eq and "if eq > 1:" in src_eq + from src.live.deribit import DeribitRead as DR + src_pos = inspect.getsource(DR.position_usd) + per_instr = 'instrument_name") == instrument' in src_pos + print(f" src/live/shadow.py::_equity legge SOLO account_summary('USDC') -> {solo_usdc}") + print(" e ripiega sulle valute-moneta SOLO se quell'equity e' <= 1 (`if eq > 1: return`).") + print(f" src/live/deribit.py::position_usd matcha per instrument_name -> {per_instr}") + print(" e cerca fra `get_positions(currency=settle)`: uno spot COMPRATO e' un SALDO,") + print(" non una posizione con quel nome.") + print(" β†’ [DEDOTTO] CONSEGUENZA MISURABILE, e non e' piccola:") + print(f" comprando ${tp_gross_1x:,.0f} di spot, l'equity che il libro LEGGE passerebbe da") + print(f" ${Eu:,.0f} a ${free:,.0f} ({free/Eu:.0%}) β†’ il sizing di TUTTO il libro si") + print(f" ridurrebbe al {free/Eu:.0%}, e la gamba spot non comparirebbe fra le posizioni") + print(" β†’ il riconciliatore la ricomprerebbe. **Non e' un cambio di strumento: e' un") + print(" cambio del percorso di sizing del libro live.**") + print(" ⚠️ Questo NON dice che il venue sbagli: dice che `_equity` e' scritta per un conto") + print(" mono-valuta. Il rimedio (sommare i saldi moneta x mark) e' poche righe β€” ma sono") + print(" righe sul percorso con soldi veri, e non e' questo filone a scriverle.") + + # ---------------------------------------------------------------- 1f) il rendimento dell'USDC + print("\n 1f) IPOTESI MIA, NATA E REFUTATA QUI: Β«convertire USDC in spot rinuncia a un interesseΒ»") + apr = pool.get("USDC", (None, None))[1] + if apr is None: + print(" [VENUE] get_currencies NON letto in questa corsa -> sezione non eseguibile.") + print(" Β«non l'ho guardatoΒ» non e' Β«e' zeroΒ»: si dichiara, non si assume.") + return dict(E=Eu, cap=cap, im=imr, free=free, need=need, spec=spec, apr=None, + tp_gross=tp_gross_1x, skh_gross=skh_gross) + print(f" [VENUE] get_currencies dichiara USDC `apr` = {apr} (USDT 0,0 Β· BTC 0,0 Β· ETH 0,0)") + print(" Se quel tasso fosse accreditato a questo conto, mettere il 75% del capitale in") + print(f" spot BTC/ETH (apr 0,0) costerebbe fino a {(apr or 0)*0.75:.2f}%/anno = meta' del") + print(" vantaggio di Β§39. Va MISURATO, non assunto.") + runs = flat_runs() + if runs: + print(f"\n [LOG] logs/cron_book.log β€” periodi in cui il conto e' stato FLAT (nessuna") + print(" posizione: equity = balance, e l'unica cosa che potrebbe muoverla e' l'interesse)") + print(f" {'da':>20} {'a':>20} {'giri':>6} {'giorni':>7} {'equity':>18} {'atteso a apr':>13}") + for r0, r1, n, days, e0, e1 in runs[:6]: + att = e0 * (apr or 0) / 100 * days / 365.25 + print(f" {r0:>20} {r1:>20} {n:>6} {days:>7.1f} " + f"${e0:>8.2f} -> ${e1:>7.2f} {att:>+13.3f}") + best = max(runs, key=lambda r: r[3]) + att = best[4] * (apr or 0) / 100 * best[3] / 365.25 + print(f"\n ❌ REFUTATA: sul run piu' lungo ({best[3]:.1f} giorni) l'equity si e' mossa di") + print(f" ${best[5]-best[4]:+.2f} contro i ${att:+.2f} che {apr:.2f}%/anno avrebbe prodotto.") + bound = 0.005 / best[4] * 365.25 / best[3] * 100 + print(f" La stampa e' a 2 decimali β†’ limite superiore su QUALSIASI interesse:") + print(f" < {bound:.3f}%/anno. **L'USDC di questo conto non rende nulla.**") + print(" (Un accredito MENSILE e' escluso: un run attraversa il 31/07-01/08 con d=0,00.)") + print(" πŸ“Œ Il campo `apr` e' letto dal venue ma la sua SEMANTICA non e' documentata") + print(" nella risposta: sui token a rendimento (STETH 2,24 Β· BUIDL 3,49 Β· USDE 4,00)") + print(" descrive il rendimento dell'ASSET. Su USDC descrive qualcosa che questo") + print(" conto non riceve. β†’ l'obiezione del rendimento perso e' MORTA, e il lead") + print(" non deve pagarla.") + + # ---------------------------------------------------------------- 1g) l'ignoto + print("\n 1g) COSA NON E' DETERMINABILE senza un conto reale (dichiarato, non dedotto)") + print(" (i) L'HAIRCUT del collaterale BTC/ETH. Nessun endpoint pubblico lo espone") + print(" (`public/get_collateral_info` non esiste: -32601 Method not found).") + print(" β†’ non binding per 1d, ma resta ignoto.") + print(" (ii) Se `account_summary('USDC').equity` includa il valore del saldo BTC/ETH.") + print(" Il conto tiene 7,8e-05 ETH (~$0,19): troppo piccolo per discriminare a 2") + print(" decimali. Il wrapper cerbero-mcp espone 8 campi e NON quelli che") + print(" risolverebbero la domanda (`cross_collateral_enabled`, `spot_reserve`,") + print(" `initial_margin`, `maintenance_margin`).") + print(" (iii) Se `get_positions` restituisca lo spot come posizione. Il wrapper non") + print(" passa `kind` e oggi non c'e' spot da vedere.") + print(" β†’ Le tre si risolvono con UN acquisto di prova da $7,73 (lotto minimo BTC_USDC)") + print(" leggendo equity/available/positions prima e dopo. **E' un ORDINE: fuori dal") + print(" perimetro di questo filone, e' una decisione dell'operatore.**") + return dict(E=Eu, cap=cap, im=imr, free=free, need=need, spec=spec, apr=apr, + tp_gross=tp_gross_1x, skh_gross=skh_gross) + + +def flat_runs() -> list[tuple]: + """[LOG] Run consecutivi di giri orari con conto FLAT: (t0, t1, n giri, giorni, eq0, eq1).""" + p = ROOT / "logs" / "cron_book.log" + if not p.exists(): + return [] + blocks = re.split(r"===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book =====", p.read_text()) + rows = [] + for i in range(1, len(blocks), 2): + m = re.search(r"conto reale\s*:\s*\$([\d.,]+)", blocks[i + 1]) + pos = [float(x.replace(",", "")) for x in re.findall(r"pos \$([+-][\d,.]+)", blocks[i + 1])] + if m and pos and all(abs(x) < 1 for x in pos): + rows.append((blocks[i], float(m.group(1).replace(",", "")))) + out, cur, prev = [], [], None + for ts, eq in rows: + t = dt.datetime.strptime(ts, "%Y-%m-%dT%H:%M:%SZ") + if prev and (t - prev).total_seconds() <= 3900: + cur.append((ts, eq)) + else: + if len(cur) > 24: + out.append(cur) + cur = [(ts, eq)] + prev = t + if len(cur) > 24: + out.append(cur) + res = [] + for run in out: + t0 = dt.datetime.strptime(run[0][0], "%Y-%m-%dT%H:%M:%SZ") + t1 = dt.datetime.strptime(run[-1][0], "%Y-%m-%dT%H:%M:%SZ") + res.append((run[0][0], run[-1][0], len(run), (t1 - t0).total_seconds() / 86400, + run[0][1], run[-1][1])) + res.sort(key=lambda r: -r[3]) + return res + + +# =========================================================================== +# 2) Q2 β€” IL VALORE DELLA SEPARAZIONE +# =========================================================================== +def q2a() -> dict: + """T1 sbloccato β€” RIPRODOTTO, non citato a memoria.""" + sez("Q2 (i) β€” T1 SBLOCCATO: il numero pubblicato riprodotto, e la lente giusta") + print(" Il 26/07 T1 valeva +0,054 Sharpe FULL / +0,061 HOLD di libro (mediana APPAIATA sui") + print(" 23 offset, `onbook_tp` contro `hourly`) ed era bloccato dal netting. Qui si RIFA' il") + print(" conto con la macchineria originale (`r0726_skh_onbook`), e si aggiunge la lente che") + print(" il 26/07 stesso ha stabilito essere quella del live vero: `fastdetect` (rilevazione") + print(" INTRA-BARRA + uscita a mercato entro ~1h), non `hourly`.\n") + modes = ("hourly", "fastdetect", "onbook_tp", "onbook", "canonical") + A = {m: [] for m in modes} + for off in r02.OFFSETS: + for m in modes: + s, _ = OB.skh_series(off, m) + A[m].append(OB.sh3(OB.book(s, W_SKH))) + A = {m: np.array(v) for m, v in A.items()} + print(f" {'modalita':<44} {'FULL med':>9} {'HOLD med':>9} {'DD med':>8}") + for m in modes: + med = np.median(A[m], axis=0) + print(f" {OB.LABEL[m]:<44} {med[0]:>9.3f} {med[2]:>9.3f} {med[3]:>7.1%}") + print("\n differenze APPAIATE per-offset (mediana, e in quanti dei 23 offset e' positiva):") + out = {} + for base in ("hourly", "fastdetect"): + for m in ("onbook_tp", "onbook"): + d0 = A[m][:, 0] - A[base][:, 0] + d2 = A[m][:, 2] - A[base][:, 2] + out[(m, base)] = (float(np.median(d0)), int((d0 > 0).sum()), + float(np.median(d2)), int((d2 > 0).sum())) + print(f" {m:>10} - {base:<11} dFULL {np.median(d0):>+7.3f} ({int((d0>0).sum()):>2}/23)" + f" dHOLD {np.median(d2):>+7.3f} ({int((d2>0).sum()):>2}/23)") + dfl = A["fastdetect"][:, 0] - A["hourly"][:, 0] + print(f"\n [CALC] fastdetect - hourly (= quanto la lente `hourly` SOTTOSTIMA il live):") + print(f" dFULL {np.median(dfl):>+.3f} in {int((dfl>0).sum())}/23 offset " + f"(il 26/07 pubblico' +0,081)") + a, b = out[("onbook_tp", "hourly")], out[("onbook_tp", "fastdetect")] + print(f"\n πŸ“Œ IL PUNTO: il numero pubblicato (+{a[0]:.3f} FULL / +{a[2]:.3f} HOLD contro `hourly`)") + print(f" e' RIPRODOTTO. Ma contro il path che il live gia' percorre (`fastdetect`) il TP") + print(f" a limite vale {b[0]:+.3f} FULL ({b[1]}/23) e {b[2]:+.3f} HOLD ({b[3]}/23).") + if b[0] <= 0: + print(" β†’ **Sbloccare T1 non compra niente: lo comprerebbe a un path PEGGIORE di quello") + print(" che gira.** E' esattamente la conclusione del 26/07 (Β«il fix richiesto sarebbe") + print(" un DECLASSAMENTOΒ»), qui ri-misurata sulla banda appaiata invece che a off 0.") + else: + print(" β†’ il TP a limite aggiungerebbe ancora qualcosa sopra il live: taglia sopra.") + return out + + +def q2b() -> dict: + """Il funding risparmiato dalla sola gamba TP01.""" + sez("Q2 (ii) β€” IL FUNDING RISPARMIATO dalla gamba TP01 (riprodotto da Β§35/Β§39)") + with contextlib.redirect_stdout(io.StringIO()): + F = FD.sezione0()["F"] + rate = FD.build_rate(F) + alloc = 600.0 + s_perp, dg = FD.tp01_realistic(alloc * W_TP, rate) + s_spot, _ = FD.tp01_realistic(alloc * W_TP, None) + a, b = FD.stat(s_spot), FD.stat(s_perp) + pagato = float(np.mean([dg[x]["fund"].mean() for x in ASSETS])) * 365.25 + print(f" {'':<34} {'ShFULL':>8} {'ShHOLD':>8} {'drift':>8} {'vol':>8} {'maxDD':>8}") + print(f" {'TP01 su PERP (oggi)':<34} {b['full']:>8.3f} {b['hold']:>8.3f} {b['drift']:>8.2%} " + f"{b['vol']:>8.2%} {b['dd']:>8.1%}") + print(f" {'TP01 senza carry (= SPOT)':<34} {a['full']:>8.3f} {a['hold']:>8.3f} {a['drift']:>8.2%} " + f"{a['vol']:>8.2%} {a['dd']:>8.1%}") + print(f" {'DELTA':<34} {a['full']-b['full']:>+8.3f} {a['hold']-b['hold']:>+8.3f} " + f"{a['drift']-b['drift']:>+8.2%} {a['vol']-b['vol']:>+8.2%} {a['dd']-b['dd']:>+8.1%}") + print(f"\n funding pagato dalla gamba TP01: {pagato:+.2%}/anno di sleeve [CALC su dati VENUE]") + print(f" a peso di libro (x{W_TP}): {(a['drift']-b['drift'])*W_TP:+.2%}/anno di drift di LIBRO") + print(f" β†’ Β§39 pubblico' +1,55%/anno sulla variante E: riprodotto " + f"({(a['drift']-b['drift'])*W_TP:+.2%} + il residuo di base del perpetual, +0,07%).") + print(" πŸ“Œ La vol NON cambia (terza cifra): e' l'unica variante che alza il drift senza") + print(" alzare il rischio. E' questo, e non lo Sharpe, il motivo per cui i MURI si muovono.") + return dict(d_drift_book=(a["drift"] - b["drift"]) * W_TP, pagato=pagato, + tp_drift_spot=a["drift"]) + + +# --------------------------------------------------------------------------- il netting +def skh_pos_path(a: str, off: int = 0) -> tuple[pd.DatetimeIndex, np.ndarray, dict]: + """Segno di SKH01 su griglia 230m, replicando ESATTAMENTE `backtest_signals` (stessa + regola d'uscita, stessa priorita' SL, stesso non-overlap). Validato contro il suo + `n_trades` e la sua `time_in_market`: se diverge, il controllo lo dice.""" + daily, m, ltf, ent = r02.run_asset(a, off) + c = ltf["close"].values.astype(float) + h = ltf["high"].values.astype(float) + lo = ltf["low"].values.astype(float) + n = len(c) + pos = np.zeros(n) + busy, ntr, bars = -1, 0, 0 + for i in range(n): + e = ent[i] if i < len(ent) else None + if e is None or e.get("dir", 0) == 0 or i <= busy: + continue + d = int(e["dir"]); tp = e.get("tp"); sl = e.get("sl"); mb = int(e.get("max_bars") or 24) + ex = min(i + mb, n - 1) + for j in range(i + 1, min(i + mb + 1, n)): + if sl is not None and ((d == 1 and lo[j] <= sl) or (d == -1 and h[j] >= sl)): + ex = j; break + if tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and lo[j] <= tp)): + ex = j; break + ex = j + pos[i + 1:ex + 1] = d + busy, ntr, bars = ex, ntr + 1, bars + (ex - i) + idx = pd.to_datetime(ltf["timestamp"].values + r02.MS_LTF, unit="ms", utc=True) + chk = dict(ntr=ntr, ntr_ref=int(m.n_trades), bars=bars, + bars_ref=int(round(m.time_in_market * n))) + return idx, pos, chk + + +def walk_bps(levels: list, usd: float, mid: float | None) -> float | None: + """Costo in bps di attraversare `usd` dollari sul lato dato del libro, rispetto al mid. + None se il libro visibile non basta. PURA.""" + if not levels: + return None + mid = mid or levels[0][0] + resto, speso, preso = usd, 0.0, 0.0 + for px, am in levels: + cap = px * am + q = min(resto, cap) + speso += q + preso += q / px + resto -= q + if resto <= 1e-9: + break + if resto > 1e-9 or preso <= 0: + return None + return (speso / preso / mid - 1) * 1e4 + + +def q2c(Q1: dict) -> dict: + """Il COSTO del netting perso β€” simulato sul percorso di produzione, cadenza oraria.""" + sez("Q2 (iii) β€” IL COSTO DEL NETTING PERSO (il lato che Β§39 stimava Β«1-2%Β»)") + E = Q1["E"]; cap = Q1["cap"]; imr = Q1["im"] + minu = 5.0 + print(f" Simulazione della RICONCILIAZIONE ORARIA (quella che gira), 2 percorsi a parita' di") + print(f" segnale: NETTA (oggi, un ordine per asset) contro SEPARATA (TP01 spot + SKH01 perp).") + print(f" E = ${E:,.2f} costante [ASSUNTO] Β· cap/asset ${cap:,.0f} Β· min ordine ${minu:.0f} " + f"[CODICE config]") + print(" Il nozionale in USD si muove col prezzo anche a segnale fermo: e' la prima causa di") + print(" ordini nel live, ed e' modellata (pos *= px[t]/px[t-1]).\n") + + tp = TrendPortfolio(**CANONICAL) + res = {} + for a in ASSETS: + h1 = load(a, "1h") + idx1 = pd.DatetimeIndex(pd.to_datetime(h1["datetime"], utc=True)) + px = h1["close"].values.astype(float) + d1 = resample_1d(load_data(a, "1h")) + tgt = np.nan_to_num(np.asarray(tp.target_series(d1), float)) + di = pd.DatetimeIndex(pd.to_datetime(d1["datetime"], utc=True)) + # il target deciso sulla barra i e' noto alla sua CHIUSURA -> vale da i+1 (causale) + tps = pd.Series(tgt, index=di).shift(1).reindex(idx1, method="ffill").fillna(0.0).values + si, sp, chk = skh_pos_path(a, 0) + sks = pd.Series(sp, index=si).reindex(idx1, method="ffill").fillna(0.0).values + ok = (chk["ntr"] == chk["ntr_ref"]) and abs(chk["bars"] - chk["bars_ref"]) <= 1 + print(f" [CONTROLLO] {a}: percorso SKH01 ricostruito = harness? trade " + f"{chk['ntr']} vs {chk['ntr_ref']}, barre in mercato {chk['bars']} vs " + f"{chk['bars_ref']} -> {'OK' if ok else 'DIVERGE'}") + assert ok, f"ricostruzione SKH01 divergente su {a}" + res[a] = dict(px=px, tp=tps, sk=sks, idx=idx1) + + def simula(a: str, sep: bool) -> dict: + d = res[a]; px, tps, sks = d["px"], d["tp"], d["sk"] + n = len(px) + p_net = p_tp = p_sk = 0.0 + ordini = ord_live = ord_spot = 0 + turn_perp = turn_spot = gross_sum = track = 0.0 + # finestra del libro LIVE (arming del cron_book): serve come CONTROLLO POSITIVO del + # conteggio ordini contro le 18 esecuzioni realmente avvenute. + t_live = pd.Timestamp("2026-06-23", tz="UTC") + anni_live = (d["idx"][-1] - t_live).days / 365.25 + for t in range(1, n): + g = px[t] / px[t - 1] + p_net *= g; p_tp *= g; p_sk *= g + raw_tp = WEIGHT * E * W_TP * max(tps[t], 0.0) + raw_sk = WEIGHT * E * W_SKH * sks[t] + if sep: + # cap: si conserva il tetto per-asset sul LORDO delle due gambe + lordo = abs(raw_tp) + abs(raw_sk) + k = min(1.0, cap / lordo) if lordo > 0 else 1.0 + t_tp, t_sk = raw_tp * k, raw_sk * k + if abs(t_tp - p_tp) >= minu: + turn_spot += abs(t_tp - p_tp); p_tp = t_tp; ordini += 1; ord_spot += 1 + ord_live += int(d["idx"][t] >= t_live) + if abs(t_sk - p_sk) >= minu: + turn_perp += abs(t_sk - p_sk); p_sk = t_sk; ordini += 1 + ord_live += int(d["idx"][t] >= t_live) + gross_sum += abs(p_tp) + abs(p_sk) + track += abs((p_tp + p_sk) - (t_tp + t_sk)) + else: + t_net = max(-cap, min(cap, raw_tp + raw_sk)) + if abs(t_net - p_net) >= minu: + turn_perp += abs(t_net - p_net); p_net = t_net; ordini += 1 + ord_live += int(d["idx"][t] >= t_live) + gross_sum += abs(p_net) + track += abs(p_net - t_net) + anni = (d["idx"][-1] - d["idx"][0]).days / 365.25 + return dict(ordini=ordini / anni, spot=turn_spot / anni, perp=turn_perp / anni, + gross=gross_sum / (n - 1), anni=anni, track=track / (n - 1), + ord_live=ord_live, anni_live=anni_live, ord_spot=ord_spot / anni) + + print() + tot = {} + for sep in (False, True): + agg = dict(ordini=0.0, spot=0.0, perp=0.0, gross=0.0, track=0.0, ord_live=0.0, + ord_spot=0.0) + for a in ASSETS: + r = simula(a, sep) + for k in agg: + agg[k] += r[k] + r0 = simula("BTC", sep) + agg["anni"] = r0["anni"] + agg["ord_live"] = agg["ord_live"] / r0["anni_live"] + tot["SEPARATA" if sep else "NETTA"] = agg + print(f" {'percorso':<12} {'ordini/anno':>12} {'turnover perp':>14} {'turnover spot':>14} " + f"{'lordo medio':>12}") + for k, v in tot.items(): + print(f" {k:<12} {v['ordini']:>12.1f} {v['perp']:>13,.0f}$ {v['spot']:>13,.0f}$ " + f"{v['gross']:>11,.2f}$") + dn = tot["SEPARATA"]; nn = tot["NETTA"] + print(f" {'DELTA':<12} {dn['ordini']-nn['ordini']:>+12.1f} " + f"{dn['perp']-nn['perp']:>+13,.0f}$ {dn['spot']-nn['spot']:>+13,.0f}$ " + f"{dn['gross']-nn['gross']:>+11,.2f}$") + print(f"\n su {nn['anni']:.1f} anni di storia (2019-03 -> oggi), a capitale COSTANTE ${E:,.0f}") + print(f"\n ⚠️ MENO ORDINI NON E' MEGLIO β€” il controllo giusto non e' il conteggio ma il") + print(f" TRACKING (5Βͺ occorrenza della lezione Β«banda in valuta assolutaΒ», 27/07):") + print(f" scarto medio |posizione tenuta - target| : NETTA ${nn['track']:.2f} Β· " + f"SEPARATA ${dn['track']:.2f}") + print(f" La separata fa {nn['ordini']-dn['ordini']:.1f} ordini/anno in MENO perche' la banda da $5 e'") + print(f" applicata a due target piu' PICCOLI invece che alla loro somma: filtra di piu'.") + print(f" Lo scarto {'peggiora' if dn['track'] > nn['track'] else 'migliora'} di " + f"${dn['track']-nn['track']:+.2f} ({(dn['track']-nn['track'])/max(nn['track'],1e-9):+.0%}).") + print(f"\n [CONTROLLO POSITIVO] il conteggio ordini regge sul campione dove esiste la verita'?") + print(f" simulato sulla finestra del libro live (dal 2026-06-23): " + f"NETTA {nn['ord_live']:.0f} ordini/anno") + nex = len([x for x in (ROOT / "data" / "live" / "book_executions.jsonl") + .read_text().splitlines() if x.strip()]) + gg = (pd.Timestamp.now(tz="UTC") - pd.Timestamp("2026-06-23", tz="UTC")).days + print(f" osservato: {nex} esecuzioni in {gg} giorni = {nex*365.25/gg:.0f}/anno [LOG]") + print(f" β†’ la simulazione e' nello stesso ordine di grandezza dell'osservato; la media su") + print(f" 8 anni ({nn['ordini']:.0f}/anno) e' piu' alta perche' il 2026 e' un anno in cui il libro") + print(f" e' stato flat l'85% del tempo. **Il DELTA fra i due percorsi, non il livello,") + print(f" e' la grandezza su cui si decide.**") + + fee_net = nn["perp"] * FEE_PERP_SIDE + fee_sep_oggi = dn["perp"] * FEE_PERP_SIDE + dn["spot"] * 0.0 + fee_sep_dopo = (dn["perp"] + dn["spot"]) * FEE_PERP_SIDE + print(f"\n [CALC] COMMISSIONI (perp {FEE_PERP_SIDE*1e4:.2f} bps/lato [VENUE/nastro], " + f"spot {Q1['spec'].get('BTC_USDC',{}).get('taker_commission',0)*1e4 if Q1['spec'] else 0:.2f} bps [VENUE]):") + print(f" NETTA (oggi) ${fee_net:>7.2f}/anno = {fee_net/E:>6.3%} di equity") + print(f" SEPARATA, spot a fee ZERO ${fee_sep_oggi:>7.2f}/anno = {fee_sep_oggi/E:>6.3%}" + f" ({(fee_sep_oggi-fee_net)/E:+.3%})") + print(f" SEPARATA, spot al taker del perp ${fee_sep_dopo:>7.2f}/anno = {fee_sep_dopo/E:>6.3%}" + f" ({(fee_sep_dopo-fee_net)/E:+.3%})") + print(" πŸ“Œ [DEDOTTO] Il netting NON e' un risparmio di commissioni: e' il contrario. Oggi la") + print(" separazione fa PAGARE MENO, perche' i tre quarti del turnover passano su uno") + print(" strumento a fee zero. Il netting risparmia ORDINI, non dollari.") + + im_net = nn["gross"] * imr + im_sep = dn["gross"] * imr + print(f"\n [CALC] MARGINE IMMOBILIZZATO (a {imr:.2%} misurato in 1c):") + print(f" NETTA lordo medio ${nn['gross']:.2f} -> IM ${im_net:.2f} = {im_net/E:.3%} di equity") + print(f" SEPARATA lordo medio ${dn['gross']:.2f} -> il perp e' solo la gamba SKH01") + print(f" ⚠️ Il Β«1-2%Β» stimato da Β§39 come risparmio del netting su margine e commissioni:") + print(f" misurato, il margine in gioco vale {im_net/E:.3%} di equity e le commissioni") + print(f" {fee_net/E:.3%}/anno. **La stima era alta di un ordine di grandezza.**") + + STRUM = ("BTC_USDC", "BTC_USDC-PERPETUAL", "ETH_USDC", "ETH_USDC-PERPETUAL") + books = {k: get(f"get_order_book?instrument_name={k}&depth=20", f"ob20_{k}") for k in STRUM} + hs = {} + for k, b in books.items(): + if b and b.get("best_bid_price") and b.get("best_ask_price"): + mid = (b["best_bid_price"] + b["best_ask_price"]) / 2 + hs[k] = (b["best_ask_price"] - b["best_bid_price"]) / 2 / mid * 1e4 + # TUTTE le istantanee del libro presenti in cache (ogni corsa ne lascia una): il + # differenziale di spread e' l'input piu' rumoroso di questa sezione e va mostrato + # come BANDA, non come punto. + snaps = [] + for pref in sorted({f.name.rsplit("_", 3)[0] for f in SCRATCH.glob("ob*_*_USDC*.json")}): + bb = {} + for k in STRUM: + f = SCRATCH / f"{pref}_{k}.json" + if f.exists(): + try: + bb[k] = json.loads(f.read_text()) + except Exception: + pass + if len(bb) == 4 and all(bb[k].get("best_bid_price") for k in STRUM): + h = {k: (bb[k]["best_ask_price"] - bb[k]["best_bid_price"]) / + (bb[k]["best_ask_price"] + bb[k]["best_bid_price"]) * 1e4 for k in STRUM} + d = np.mean([h["BTC_USDC"] - h["BTC_USDC-PERPETUAL"], + h["ETH_USDC"] - h["ETH_USDC-PERPETUAL"]]) + ts = pd.Timestamp(bb["BTC_USDC"].get("timestamp", 0), unit="ms", tz="UTC") + snaps.append((pref, ts, float(d))) + if hs: + print("\n [VENUE] ⚠️ LA FEE NON E' TUTTO IL COSTO: mezzo spread al top del book, ORA") + for k in ("BTC_USDC", "BTC_USDC-PERPETUAL", "ETH_USDC", "ETH_USDC-PERPETUAL"): + if k in hs: + b = books[k] + print(f" {k:<22} {hs[k]:>6.2f} bps bid {b['best_bid_price']:>10,.2f} " + f"x{b.get('best_bid_amount')} ask {b['best_ask_price']:>10,.2f} " + f"x{b.get('best_ask_amount')}") + d_hs = np.mean([hs.get("BTC_USDC", np.nan) - hs.get("BTC_USDC-PERPETUAL", np.nan), + hs.get("ETH_USDC", np.nan) - hs.get("ETH_USDC-PERPETUAL", np.nan)]) + extra = dn["spot"] * d_hs * 1e-4 + print(f" β†’ lo spot e' {d_hs:+.2f} bps piu' largo del perp (istantanea, 1 momento solo)") + print(f" su ${dn['spot']:,.0f}/anno di turnover deviato = ${extra:+.2f}/anno = " + f"{extra/E:+.3%} di equity") + print(f" Sommato al risparmio di fee ({(fee_sep_oggi-fee_net)/E:+.3%}): " + f"{(fee_sep_oggi-fee_net+extra)/E:+.3%}/anno.") + print("\n ⚠️ E QUANTO VALGA QUELLA ISTANTANEA LO DICONO LE ISTANTANEE STESSE. Questo") + print(" script ne lascia una in cache a ogni corsa; quelle presenti:") + for pref, ts, d in snaps: + print(f" {str(ts)[:19]} UTC differenziale spot-perp {d:>+6.2f} bps " + f"-> sovracosto {d*1e-4*dn['spot']/E:>+7.3%}/anno") + if len(snaps) >= 2: + lo, hi = min(x[2] for x in snaps), max(x[2] for x in snaps) + print(f" β†’ due letture a pochi minuti di distanza danno {lo:.2f} e {hi:.2f} bps " + f"({(hi-lo)/max(lo,1e-9):+.0%}).") + print(f" BANDA del sovracosto d'esecuzione: " + f"[{lo*1e-4*dn['spot']/E:+.3%}, {hi*1e-4*dn['spot']/E:+.3%}]/anno, contro un") + print(f" risparmio di fee di {(fee_sep_oggi-fee_net)/E:+.3%} -> il netto d'esecuzione") + n_lo = (fee_sep_oggi - fee_net) / E + lo * 1e-4 * dn["spot"] / E + n_hi = (fee_sep_oggi - fee_net) / E + hi * 1e-4 * dn["spot"] / E + print(f" CAMBIA SEGNO fra le due letture: [{n_lo:+.3%}, {n_hi:+.3%}]/anno.") + print(f" **Non e' risolvibile con un'istantanea**, e non serve che lo sia:") + print(f" l'ampiezza della banda ({n_hi-n_lo:.3%}) e' il {(n_hi-n_lo)/0.0158:.0%} del lead.") + print(f" Il numero onesto oggi e' Β«l'esecuzione non decideΒ»; se un giorno") + print(f" dovesse decidere, si raccoglie il libro su piu' ore.") + else: + print(" (una sola istantanea in cache: rilanciare lo script a ore diverse per") + print(" avere la banda β€” un punto solo non e' una misura di spread)") + print(" ⚠️ Il book si allarga di notte e nei crash. Β§39 fece la stessa cosa e la") + print(" dichiaro'. Va misurata su piu' ore prima di metterla in un muro.") + print("\n [VENUE] ⚠️ E IL MEZZO SPREAD NON BASTA: il MIGLIOR LIVELLO dello spot e'") + print(" SOTTILE. Profondita' cumulata (20 livelli) e costo di attraversare un") + print(" ticket, misurati camminando il libro:") + tick = dn["spot"] / max(dn["ord_spot"], 1e-9) + print(f" ticket medio della gamba spot a ${E:,.0f} di equity: ~${tick:.0f} " + f"({dn['ord_spot']:.0f} ordini spot/anno) -> scala col capitale") + print(f" {'strumento':<22} {'top ask $':>10} {'20 liv. $':>11} " + + "".join(f"{'$'+f'{tick*m:,.0f}':>10}" for m in (1, 20 / 0.636, 80 / 0.636))) + prof = {} + for k in ("BTC_USDC", "BTC_USDC-PERPETUAL", "ETH_USDC", "ETH_USDC-PERPETUAL"): + b = books.get(k) + if not b or not b.get("asks"): + continue + top = b["asks"][0][0] * b["asks"][0][1] + cum = sum(px * am for px, am in b["asks"]) + prof[k] = cum + costi = [] + for m in (1, 20 / 0.636, 80 / 0.636): + costi.append(walk_bps(b["asks"], tick * m, + (b["best_bid_price"] + b["best_ask_price"]) / 2)) + print(f" {k:<22} {top:>10,.0f} {cum:>11,.0f} " + + "".join((f"{c:>9.1f}b" if c is not None else f"{'FUORI':>10}") for c in costi)) + print(" (le tre colonne sono il costo in bps di attraversare il ticket medio a") + print(f" ${E:,.0f} / $20.000 / $80.000 di equity; 'FUORI' = il libro visibile non basta)") + cb = walk_bps(books["BTC_USDC"]["asks"], tick, None) if books.get("BTC_USDC") else None + print(" πŸ“Œ [DEDOTTO] A questa taglia il ticket e' cosi' piccolo che cammina un") + print(" livello solo: il costo vero e' il MEZZO SPREAD, ed e' quello contato") + print(" sopra. La profondita' diventa il vincolo molto dopo i $20k β€” ma va") + print(" RI-MISURATA a ogni salto di taglia, come lo slippage del libro (Β§ondata).") + sp_cost = extra + + opp = 0.0 + tots = 0 + for a in ASSETS: + d = res[a] + m = (d["tp"] > 0) & (d["sk"] < 0) + opp += int(m.sum()); tots += len(m) + print(f"\n [CALC] Quando il netting serve DAVVERO (segni opposti: TP01 long e SKH01 short):") + print(f" {opp/tots:.2%} delle ore di storia. Li' la netta tiene {'' }un nozionale piu' piccolo") + print(f" della somma delle gambe; nel resto del tempo netta e separata tengono lo STESSO") + print(f" lordo e il netting risparmia solo l'ordine.") + return dict(net=nn, sep=dn, fee_net=fee_net, fee_sep_oggi=fee_sep_oggi, + fee_sep_dopo=fee_sep_dopo, opp=opp / tots, im_net=im_net, + spread=locals().get("sp_cost", 0.0), E=E, + spread_banda=[x[2] * 1e-4 * dn["spot"] for x in snaps]) + + +def q2d() -> None: + """Il percorso LIVE reale, per contro-prova.""" + sez("Q2 (iii-bis) β€” CONTRO-PROVA SUL PATH LIVE REALE (2 mesi, 1443 giri orari)") + p = ROOT / "logs" / "cron_book.log" + blocks = re.split(r"===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book =====", p.read_text()) + pat = re.compile(r"(BTC|ETH) TP ([+-][\d.]+) Β· SKH ([+-]\d)\((flat|LONG|SHORT)") + stato = {"flat": 0, "LONG": 0, "SHORT": 0} + giri = 0 + both = 0 + for i in range(1, len(blocks), 2): + rows = pat.findall(blocks[i + 1]) + if rows: + giri += 1 + for a, t, s, st in rows: + stato[st if st in stato else "flat"] += 1 + if float(t) > 0 and int(s) < 0: + both += 1 + ex = [json.loads(x) for x in (ROOT / "data" / "live" / "book_executions.jsonl") + .read_text().splitlines() if x.strip()] + print(f" [LOG] {giri} giri con segnale leggibile Β· stati di SKH01 (conteggio ASSET-ORA, " + f"2 asset per giro):") + print(f" flat {stato['flat']} Β· LONG {stato['LONG']} Β· SHORT {stato['SHORT']} " + f"(totale {sum(stato.values())} = 2 x {giri})") + print(f" [LOG] asset-ora con TP01 long E SKH01 short (il caso in cui il netting conta): {both}") + print(f" [LOG] {len(ex)} esecuzioni reali; fee totale pagata ${sum(e['fee'] for e in ex):.4f}") + print(f" su nozionale ${sum(abs(e['filled']*e['price']) for e in ex):,.2f} = " + f"{sum(e['fee'] for e in ex)/sum(abs(e['filled']*e['price']) for e in ex)*1e4:.2f} bps/lato") + print(" πŸ“Œ In 2 mesi di vita del libro SKH01 non e' MAI stato short: il campione live NON") + print(" puo' misurare il costo del netting, e va detto invece di estrapolarlo. La misura") + print(" che decide e' quella su 7,4 anni di sezione precedente.") + print(" ⚠️ `ts_utc` in book_executions.jsonl NON e' l'ora del fill (18/18 a 00:00:00): l'ora") + print(" vera sta solo in logs/cron_book.log. Qui non serve (si contano fee e nozionale).") + + +# =========================================================================== +# 3) Q3 β€” LA SORVEGLIANZA MANCANTE +# =========================================================================== +def q3(Q1: dict, Q2b: dict, Q2c: dict) -> None: + sez("Q3 β€” LA SORVEGLIANZA: `fee_watch` non vedrebbe una gamba spot") + import importlib.util + _spec = importlib.util.spec_from_file_location( + "fee_watch_ro", ROOT / "scripts" / "live" / "fee_watch.py") + FW = importlib.util.module_from_spec(_spec) + _spec.loader.exec_module(FW) # import puro: il main e' sotto __main__, non gira + src = inspect.getsource(FW) + derivato = "INSTRUMENTS = tuple(BOOK_INSTRUMENT[a] for a in sorted(BOOK_INSTRUMENT))" in src + print(f" [CODICE] scripts/live/fee_watch.py DERIVA i suoi strumenti dal libro -> {derivato}") + print(f" INSTRUMENTS = {FW.INSTRUMENTS}") + print(" β†’ una gamba su BTC_USDC/ETH_USDC non comparirebbe: il sorvegliante continuerebbe a") + print(" dire OK guardando due strumenti su cui passerebbe solo un quarto del turnover.") + print(" (E' ESATTAMENTE il difetto corretto il 21/08, in cui sorvegliava gli inverse.)") + print("\n LA RIGA β€” non scritta, dichiarata (questo filone non tocca scripts/live/):") + print(" in `src/live/book.py`, accanto a INSTRUMENT:") + print(' SPOT_INSTRUMENT = {"BTC": "BTC_USDC", "ETH": "ETH_USDC"}') + print(" in `scripts/live/fee_watch.py`:") + print(" from src.live.book import SPOT_INSTRUMENT as BOOK_SPOT") + print(" INSTRUMENTS = (tuple(BOOK_INSTRUMENT[a] for a in sorted(BOOK_INSTRUMENT))") + print(" + tuple(BOOK_SPOT[a] for a in sorted(BOOK_SPOT)))") + print(f" ⚠️ NON basta: `convenzione('BTC_USDC')` = '{FW.convenzione('BTC_USDC')}' -> il") + print(" cross-check sui fill reali tornerebbe None (= Β«non misurataΒ», che e' onesto ma") + print(" cieco). Servirebbe una terza famiglia 'spot' (amount in BASE, fee in USDC).") + print(" Senza, si sorveglia il TIER ma non cio' che si paga davvero.") + + sp = Q1["spec"].get("BTC_USDC") + if sp: + print(f"\n [VENUE] listino spot OGGI: taker {float(sp['taker_commission'])*1e4:.2f} bps Β· " + f"maker {float(sp['maker_commission'])*1e4:.2f} bps Β· " + f"min lotto {sp['min_trade_amount']} BTC") + print(" ⚠️ La fee 0 e' dichiarata TEMPORANEA nell'annuncio del 1Β° agosto (Β«spot a zero fino") + print(" al collegamento con CoinbaseΒ»). Fonte: articolo Insights, SECONDARIA.") + costo = (Q2c["fee_sep_dopo"] - Q2c["fee_sep_oggi"]) / Q1["E"] + print(f" Se lo spot passasse al taker dei lineari (3,50 bps) il lead perderebbe " + f"{costo:.3%}/anno") + print(f" ({costo/Q2b['d_drift_book']:.0%} del guadagno di {Q2b['d_drift_book']:.2%}): " + f"**sopravviverebbe**. La ragione per cui") + print(" sorvegliarlo non e' che lo ucciderebbe, e' che oggi nessuno se ne accorgerebbe β€”") + print(" e un costo che nessuno vede non entra in nessun muro.") + + +# =========================================================================== +# 4) Q4 β€” IL RISCHIO CHE SI AGGIUNGE +# =========================================================================== +def q4(Q1: dict, Q2b: dict, Q2c: dict) -> None: + sez("Q4 β€” IL RISCHIO: detenere cripto non e' tenere un derivato") + E = Q1["E"] + print(" (i) VENUE β€” invariato, e non e' una buona notizia: il rischio era GIA' totale") + print(" (decisione dell'operatore 26/07: 100% Deribit fino a $20k). Lo spot non lo") + print(" aumenta, ma cambia COSA c'e' sul conto: oggi margine USDC, domani cripto.") + print(" In un fallimento la differenza e' di procedura concorsuale, non di quantita'.") + print(" [ASSUNTO] nessun numero: `p` non e' stimato, e' un parametro dell'operatore.") + + print("\n (ii) FISCO β€” e' qui che sta il numero grosso, ed e' lo stesso del 30/07 e di Β§39.") + tp_share = Q2b["tp_drift_spot"] * W_TP + print(f" La gamba TP01 porta ~{tp_share:.1%}/anno di drift al libro [CALC da Q2ii].") + print(f" Se i derivati fossero `c-quater` (26%) e lo spot `c-sexies` (33%), spostare") + print(f" TP01 sullo spot sposterebbe quel P&L nel comparto piu' caro: 7 punti su") + print(f" {tp_share:.1%} = {0.07*tp_share:.2%}/anno.") + print(f" Confronto diretto col guadagno misurato in Q2(ii): {Q2b['d_drift_book']:+.2%}/anno.") + if 0.07 * tp_share > Q2b["d_drift_book"] * 0.5: + print(" β†’ **la domanda fiscale vale una frazione maggioritaria del guadagno**: si") + print(" risponde prima di provare, e la risposta non e' un backtest.") + print(" ⚠️ Comparti SEPARATI: se i derivati sono `c-quater`, le minusvalenze del perp") + print(" (SKH01) NON compenserebbero le plusvalenze spot (TP01). Oggi si compensano") + print(" perche' stanno nello stesso comparto. Questo e' un costo NUOVO della") + print(" separazione, mai considerato prima. [DEDOTTO, da verificare col commercialista]") + + print("\n (iii) LIQUIDAZIONE β€” e qui la separazione MIGLIORA, ed e' misurabile.") + print(f" Oggi il 100% del lordo del libro (medio ${Q2c['net']['gross']:.2f}) e' su un perp") + print(" marginato: liquidabile. Separando, la gamba TP01 diventa un ACQUISTO: uno spot") + print(" non ha prezzo di liquidazione, qualunque cosa faccia il prezzo.") + print(f" β†’ esce dal perimetro di liquidazione il {W_TP:.0%} del libro per peso " + f"(gamba TP01),") + print(f" e col lordo misurato in Q2(iii) restano ~${Q2c['sep']['gross']*W_SKH/(W_TP+W_SKH):.0f}" + f" di nozionale liquidabile invece di ${Q2c['net']['gross']:.0f}.") + print(" ⚠️ CONTROPARTITA: il disaster-SL -30% on-book oggi protegge la posizione NETTA.") + print(" Separando, potrebbe stare solo sul perp (SKH01). La gamba spot resterebbe") + print(" senza stop β€” il che e' coerente (non puo' essere liquidata) ma significa che") + print(" in un blackout prolungato TP01 resta long al 75% del conto senza protezione.") + print(" Oggi il disaster-SL, sulla netta, copre anche quella quota.") + print(" πŸ“Œ [DEDOTTO] Il rischio non sparisce: cambia forma. Da Β«liquidazione forzata") + print(" a -30%Β» a Β«esposizione lunga non protettaΒ». Sono due code diverse, e la") + print(" seconda e' quella che TP01 accetta per costruzione (long-flat, non a leva).") + + print("\n (iv) HAIRCUT β€” leggibile? NO (1g). Binding? NO (1d): la copertura del margine di") + print(f" SKH01 col solo USDC libero e' {Q1['free']/Q1['need']:.0f}x e non dipende dal capitale.") + print(" β†’ e' l'unico caso di questo filone in cui Β«non lo soΒ» e Β«non contaΒ» coincidono,") + print(" e va detto in quest'ordine: prima non lo so, poi perche' non conta.") + + +# =========================================================================== +# 5) VERDETTO +# =========================================================================== +def verdetto(Q1: dict, Q2a: dict, Q2b: dict, Q2c: dict) -> None: + sez("VERDETTO") + t1_live = Q2a[("onbook_tp", "fastdetect")] + print(f" Q1 MARGINE il venue NON uccide il lead: BTC/ETH sono nel pool di") + print(f" cross-collateral [VENUE], lo spot ha max_leverage 10 [VENUE], e la") + print(f" gamba SKH01 resterebbe marginata {Q1['free']/Q1['need']:.0f}x dal solo USDC") + print(f" libero [CALC], a QUALUNQUE capitale. Uccide il lead il CODICE:") + print(f" `_equity` legge solo USDC e leggerebbe {Q1['free']/Q1['E']:.0%} del conto vero.") + bnd = Q2c["spread_banda"] or [Q2c["spread"]] + print(f" Q2 VALORE il funding risparmiato e' reale ({Q2b['d_drift_book']:+.2%}/anno di drift") + print(f" di libro, riprodotto). T1 sbloccato vale {t1_live[0]:+.3f} Sharpe FULL") + print(f" contro il path che il live gia' percorre: **zero o negativo**.") + print(f" Il netting perso NON costa commissioni ({(Q2c['fee_sep_oggi']-Q2c['fee_net'])/Q1['E']:+.3%}: e' un") + print(f" RISPARMIO) ne' ordini ({Q2c['sep']['ordini']-Q2c['net']['ordini']:+.1f}/anno): costa TRACKING (+12%) e") + print(f" SPREAD ([{min(bnd)/Q1['E']:.3%},{max(bnd)/Q1['E']:.3%}]/anno su 2 istantanee: lo spot e'") + print(f" 2-4 bps piu' largo del perp, che e' un mercato a UN TICK). I due") + print(f" si compensano quasi: l'esecuzione NON decide questo lead.") + print(f" Q3 SORVEGLIANZA una gamba spot sarebbe INVISIBILE a `fee_watch`, che deriva i suoi") + print(f" strumenti dal libro. Riga dichiarata; serve anche una convenzione") + print(f" 'spot' o il cross-check sui fill resta muto.") + print(f" Q4 RISCHIO la liquidazione MIGLIORA (il 75% del libro esce dal perimetro), il") + print(f" fisco PEGGIORA e vale {0.07*Q2b['tp_drift_spot']*W_TP:.2%}/anno se i derivati sono") + print(f" `c-quater` β€” piu' della meta' del guadagno.") + E = Q1["E"] + lordo = Q2b["d_drift_book"] + print("\n πŸ“Œ IL NUMERO DA CITARE β€” il lead non vale +1,55%/anno, vale meno, e si scrive") + print(" con la sua colonna d'esecuzione (che Β§39 aveva stimato e qui e' misurata):") + print(f" funding risparmiato dalla gamba TP01 {lordo:>+8.2%}/anno di drift di libro") + e_lo = (Q2c["fee_sep_oggi"] - Q2c["fee_net"]) / E + min(bnd) / E + e_hi = (Q2c["fee_sep_oggi"] - Q2c["fee_net"]) / E + max(bnd) / E + print(f" esecuzione (fee zero MENO spread), contributo netto " + f"[{-e_hi:+.3%},{-e_lo:+.3%}]/anno") + print(f" (fee {-(Q2c['fee_sep_oggi']-Q2c['fee_net'])/E:+.3%} certo Β· spread " + f"{-max(bnd)/E:+.3%}..{-min(bnd)/E:+.3%} da {len(bnd)} istantanee del libro)") + print(f" ------------------------------------------------------") + print(f" NETTO OGGI [{lordo-e_hi:+.2%},{lordo-e_lo:+.2%}]/anno") + print(f" NETTO se lo spot torna a 3,50 bps " + f"[{lordo-e_hi-(Q2c['fee_sep_dopo']-Q2c['fee_sep_oggi'])/E:+.2%}," + f"{lordo-e_lo-(Q2c['fee_sep_dopo']-Q2c['fee_sep_oggi'])/E:+.2%}]/anno") + print(f" imposta in piu' se i derivati sono c-quater " + f"{-0.07*Q2b['tp_drift_spot']*W_TP:>+7.2%}/anno ← non misurabile qui") + print(f" NETTO nello scenario fiscale sfavorevole " + f"{lordo-e_hi-(Q2c['fee_sep_dopo']-Q2c['fee_sep_oggi'])/E-0.07*Q2b['tp_drift_spot']*W_TP:>+8.2%}/anno" + f" (estremo conservativo)") + print(" ⚠️ Che il netto di oggi cada su +1,55% β€” lo stesso numero di Β§39 β€” e' una") + print(" COINCIDENZA di due decomposizioni diverse, non una conferma: Β§39 pubblico'") + print(" +1,55% come delta di drift della variante E (lordo dello spread, che stimava") + print(" a parte); qui il LORDO e' +1,58% (riprodotto entro 0,03 punti) e il netto ci") + print(" ricasca sopra dopo aver sottratto uno spread MISURATO. Due strade, un numero.") + print("\n πŸ“Œ IL LEAD REGGE, MA IL SUO VALORE NON E' QUELLO CHE SEMBRAVA.") + print(f" Il valore vero e' il FUNDING, e basta: {Q2b['d_drift_book']:+.2%}/anno lordo.") + print(" La separazione delle gambe β€” la ragione per cui questo filone e' stato aperto β€”") + print(" NON aggiunge: T1 e' un declassamento del path che gira, il netting non risparmia") + print(" commissioni, il margine non era il vincolo. E introduce due costi nuovi che") + print(" nessuno aveva contato: la SEPARAZIONE DEI COMPARTI FISCALI (le minusvalenze del") + print(" perp smetterebbero di compensare le plusvalenze spot) e la gamba TP01 senza") + print(" disaster-SL.") + print(" β†’ Il prossimo passo NON e' un backtest: e' (1) la domanda al commercialista e") + print(" (2) un acquisto di prova da $7,73 che risolve le tre incognite di 1g. Entrambi") + print(" stanno fuori dal perimetro di questo filone.") + + +def main() -> None: + print("=" * 100) + print(" r0823_spot_netting β€” Β§45 SPOT-NETTING: quanto vale, e quanto costa, separare le gambe") + print("=" * 100) + print(f" {clock()} avvio Β· finestra vietata ora: {finestra_vietata()} Β· --no-net: {NO_NET}") + Q1 = q1() + print(f"\n{clock()} Q1 fatta") + A = q2a() + print(f"\n{clock()} Q2(i) fatta") + B = q2b() + print(f"\n{clock()} Q2(ii) fatta") + C = q2c(Q1) + q2d() + print(f"\n{clock()} Q2(iii) fatta") + q3(Q1, B, C) + q4(Q1, B, C) + verdetto(Q1, A, B, C) + sez("PROVA DI SOLA LETTURA") + print(f" chiamate di rete di questa corsa ({len(_NET_CALLS)}):") + for c in _NET_CALLS: + print(f" - {c}") + if not _NET_CALLS: + print(" - nessuna (tutto da cache)") + print(f" scritture: SOLO {SCRATCH} (cache). Nessun file del repo aperto in scrittura.") + print(f" ordini inviati: 0 β€” `DeribitRead` non ha, by design, alcun metodo di trading.") + print(f"\n{clock()} fine") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_tail_hedge.py b/scripts/research/r0823_tail_hedge.py new file mode 100644 index 0000000..9cd00e8 --- /dev/null +++ b/scripts/research/r0823_tail_hedge.py @@ -0,0 +1,881 @@ +"""r0823_tail_hedge β€” Β§46 TAIL-HEDGE: comprare put deep-OTM come ASSICURAZIONE STATICA sul libro. + +LA DOMANDA (decidibile, e NON e' una domanda di alpha) + Il progetto ha gia' testato la VENDITA di volatilita' (VRP01, gated su IV-rank), il GAMMA + SCALPING (long-vol delta-hedgiato, SCARTATO: perde ogni anno) e le strutture Albimarini. + Non ha mai testato la cosa piu' semplice: **comprare put deep-OTM e tenerle**, senza + delta-hedge, senza pretendere alpha. E' un riduttore di DRAWDOWN, e il drawdown e' il vincolo + binding su due assi gia' misurati: (a) il canale funded (daily-loss 4%, max-loss 6%), (b) la + rendita perpetua, che vive sul DD (il libro rende quanto un ETF S&P e serve 2,5x meno capitale + solo perche' perde meno). + Domanda: **comprare protezione di coda compra riduzione di DD a un prezzo migliore di quello + che si ottiene semplicemente DE-LEVERANDO?** + +=========================== ATTESA A PRIORI (scritta PRIMA di misurare) =========================== +Mi aspetto **REFUTATO al f realistico**, e forse marginale a f=1. Tre ragioni, in ordine di forza: + (c) **Si sta comprando assicurazione contro il rischio sbagliato.** Il 22/08 ha misurato che il + peggior giorno del libro NON e' un crash: e' uno SHORT SQUEEZE di SKH01 (short con stop 2% + che gappa a -15,77% di sleeve mentre TP01 e' flat). Una PUT non protegge da un rialzo. + (b) **Il libro e' GIA' difensivo dove la put paga.** L'esposizione di TP01 scende MONOTONA col + crollo (0,096 -> 0,042 sotto il -20%, misurato 22/08) perche' li' il trend si e' gia' girato: + la put paga proprio quando il libro non ha piu' esposizione da proteggere. + (a) **Comprare vol e' pagare il VRP**, cioe' l'esatto specchio di cio' che VRP01 incassa e che il + gamma scalping ha gia' pagato perdendo ogni anno, ogni variante, ogni frequenza. +Previsione quantitativa dichiarata: la riduzione di DD ci sara' (la put paga davvero nei crolli), +ma il de-levering la comprera' a meno; e il costo salira' ~linearmente col budget mentre il +beneficio saturera'. +================================================================================================== + +IL GATE CHE DECIDE β€” NULL DEL DE-LEVERING (6 occorrenze nel progetto: VRP-DD, TP01xDVOL, MAT01, +azioni intere UCITS, SOL, TP01-SIZE). Si trova il `k` costante per cui `k*libro` ha lo STESSO +maxDD della variante coperta, e si confronta Sharpe e DRIFT a quel punto. + ⚠️ Corollario 22/08: quando la variante e' una pura ri-scalatura il null e' DEGENERE + (`sh(k*base) === sh(base)`, identita' aritmetica). Qui NON lo e' **sul drift**: il drift + geometrico di `k*base` cambia con k, mentre lo Sharpe no. Quindi il confronto ha DUE gambe: + * Sharpe: Sharpe(coperto) vs Sharpe(base) [ = Sharpe(k*base), identicamente ] + * Drift: drift(coperto) vs drift(k*base) [ questa e' la gamba informativa ] + La non-linearita' della put e' cio' che rende la PRIMA gamba non-degenere (la copertura puo' + davvero alzare lo Sharpe, una ri-scalatura no). + +COSA C'E' DENTRO + 1) Motore prezzi RIUSATO (`src.portfolio.sleeves._bs_put` / `_strike_from_delta`, gli stessi di + VRP01) su DVOL reale. Premio **MODELLATO**, non quotato: dichiarato come tale. + 2) f MISURATO OGGI sulle quote vere della catena Deribit (BTC_USDC + inverse), sulle ali che + questa strategia comprerebbe β€” non assunto dal 30/07. + 3) Griglia dichiarata prima: delta {-0.05,-0.10,-0.15} x tenore {7,14,30} x budget + {0.5%,1%,2%}/anno x roll {scadenza, meta' vita} = **54 celle** per lente di f. + 4) Lente di CODA (non media): maxDD, 5 peggiori mesi, peggior giorno, breach 4%/6% con la + **lente wick accoppiata** (riusata da r0725_prop_coupled, non riscritta). + 5) Eseguibilita' misurata DAL VENUE (min_trade_amount + tick), non assunta. + 6) Controlli positivo e negativo obbligatori. + +CONVENZIONI DICHIARATE + * Libro = TP01 75 / SKH01 25 (il book live Deribit). Il costo si sottrae al DRIFT DEL LIBRO e il + beneficio si misura SUL LIBRO, non sullo sleeve. + * Copertura 50/50 BTC/ETH (l'esposizione del libro e' 50/50). + * **Mark-to-market GIORNALIERO** della put (BS a DVOL e spot correnti). E' essenziale: una + copertura marcata solo a scadenza sarebbe misurata male proprio sul DD, che e' l'unica cosa + che questo filone valuta. + * `f` applicato SIMMETRICAMENTE (si compra E si marca a f x modello). Conseguenza strutturale + che il modello cattura ed e' un risultato in se': **f si paga solo sulla parte di valore che + converge a INTRINSECO (la scadenza); un roll anticipato non lo paga.** Sensitivita' con f + asimmetrico (compri caro, vendi a modello) riportata a parte. + * Spread bid-ask MISURATO dalla catena vera per bucket di tenore, pagato a ogni compra e a ogni + vendita: senza, il roll a meta' vita vincerebbe per finzione. + * Fee opzioni Deribit: taker 0,03% del sottostante con cap 12,5% del premio; consegna ITM + 0,015% con lo stesso cap. + * Finestra: **2021-03-24 -> oggi** (il DVOL non esiste prima). Contiene il 2022. NON contiene + il crollo di marzo 2020, che e' il sinistro piu' grande della storia del libro: dichiarato. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823_tail_hedge.py` +""" +from __future__ import annotations + +import json +import math +import sys +import time +import urllib.request +from functools import lru_cache +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import resample_1d # noqa: E402 +# MOTORE PREZZI RIUSATO β€” gli stessi di VRP01, non una reimplementazione. +from src.portfolio.sleeves import ( # noqa: E402 + _bs_put, _strike_from_delta, _tp01_returns, _skyhook_returns, VRP_CFG, +) +import altlib as al # noqa: E402 +from scipy.stats import norm as _norm # noqa: E402 + +ASSETS = ("BTC", "ETH") +W_TP, W_SKH = 0.75, 0.25 # book live Deribit (sleeves.deribit_book_sleeves) +DPY = 365.25 + +# ---- costi opzioni Deribit (listino pubblico) ------------------------------------------------- +OPT_TAKER_UNDERLYING = 0.0003 # 0,03% del sottostante per gamba +OPT_FEE_CAP_FRAC = 0.125 # cap 12,5% del premio dell'opzione +OPT_DELIVERY_UNDERLYING = 0.00015 # 0,015% consegna ITM + +# ---- GRIGLIA DICHIARATA PRIMA DI GUARDARE ----------------------------------------------------- +G_DELTA = (-0.05, -0.10, -0.15) +G_TENOR = (7, 14, 30) +G_BUDGET = (0.005, 0.010, 0.020) # quota di equity spesa in premio, ANNUA +G_ROLL = ("expiry", "half") +N_CELLS = len(G_DELTA) * len(G_TENOR) * len(G_BUDGET) * len(G_ROLL) # = 54 + +# ---- lenti di f: 1.0 (modello nudo) + i due misurati il 30/07 + quello misurato OGGI ----------- +F_LENSES_STATIC = (1.0, 2.23, 5.85) + +DERIBIT = "https://www.deribit.com/api/v2/public/" + + +# ==================================================================== 0. UTIL METRICHE + +def _sh(x) -> float: + r = np.asarray(pd.Series(x).dropna().values, float) + return float(np.mean(r) / np.std(r) * math.sqrt(DPY)) if len(r) > 2 and np.std(r) > 0 else 0.0 + + +def _dd(x) -> float: + r = np.asarray(pd.Series(x).dropna().values, float) + eq = np.cumprod(1.0 + r) + pk = np.maximum.accumulate(eq) + return float(np.max((pk - eq) / pk)) if len(eq) else 0.0 + + +def _drift(x) -> float: + """Drift GEOMETRICO annualizzato (CAGR). E' la grandezza su cui vive la rendita perpetua, + ed e' l'unica delle due che CAMBIA quando si de-leva (lo Sharpe no: identita' aritmetica).""" + r = np.asarray(pd.Series(x).dropna().values, float) + if len(r) < 2: + return 0.0 + tot = float(np.prod(1.0 + r)) + if tot <= 0: + return -1.0 + return float(tot ** (DPY / len(r)) - 1.0) + + +def _dd_of_k(r: np.ndarray, k: float) -> float: + return _dd(k * r) + + +def k_for_same_drift(base: np.ndarray, target_drift: float) -> float: + """Il de-levering a PARI DRIFT. Serve sull'asse funded: se la copertura NON riduce il maxDD + (che e' cio' che questo filone misura), il confronto a pari DD e' degenere -> la domanda + onesta diventa 'a parita' di rendimento atteso, chi sfora meno la regola giornaliera?'. + drift(k*r) e' crescente in k su un libro a drift positivo.""" + lo, hi = 1e-4, 1.0 + if _drift(hi * base) <= target_drift: + return 1.0 + for _ in range(60): + mid = 0.5 * (lo + hi) + if _drift(mid * base) > target_drift: + hi = mid + else: + lo = mid + return 0.5 * (lo + hi) + + +def k_for_same_dd(base: np.ndarray, target_dd: float) -> float: + """Il `k` del NULL DEL DE-LEVERING: la scala costante che porta il maxDD del libro NUDO + esattamente al maxDD della variante coperta. maxDD(k*r) e' monotono crescente in k + (compounding), quindi bisezione.""" + lo, hi = 1e-4, 1.0 + if _dd_of_k(base, hi) <= target_dd: + return 1.0 # la copertura non riduce il DD: k=1 + for _ in range(60): + mid = 0.5 * (lo + hi) + if _dd_of_k(base, mid) > target_dd: + hi = mid + else: + lo = mid + return 0.5 * (lo + hi) + + +# ==================================================================== 1. DATI + +@lru_cache(maxsize=1) +def daily_panel() -> pd.DataFrame: + """Pannello giornaliero allineato: rendimenti del libro + prezzi/low/DVOL per asset. + Un solo indice, cosi' nessuna domanda successiva puo' disallinearsi.""" + tp = _tp01_returns() + skh = _skyhook_returns() + J = pd.concat({"tp": tp, "skh": skh}, axis=1, join="inner").fillna(0.0) + book = W_TP * J["tp"] + W_SKH * J["skh"] + + cols = {"book": book, "tp": J["tp"], "skh": J["skh"]} + for a in ASSETS: + d = resample_1d(load_data(a, "1h")) + idx = pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True)) + for c in ("close", "low", "high"): + assert c in d.columns, f"resample_1d senza colonna {c}" + cols[f"px_{a}"] = pd.Series(d["close"].values.astype(float), index=idx) + cols[f"lo_{a}"] = pd.Series(d["low"].values.astype(float), index=idx) + dv = pd.read_parquet(ROOT / "data" / "raw" / f"dvol_{a.lower()}.parquet") + cols[f"iv_{a}"] = pd.Series(dv["close"].values.astype(float) / 100.0, + index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True)) + P = pd.concat(cols, axis=1, join="inner").sort_index().dropna() + return P + + +# ==================================================================== 2. IL MOTORE DELLA COPERTURA + +def _entry_fee(prem_paid: float, S: float) -> float: + return min(OPT_TAKER_UNDERLYING * S, OPT_FEE_CAP_FRAC * prem_paid) if prem_paid > 0 else 0.0 + + +def hedge_leg(px: np.ndarray, lo: np.ndarray, iv: np.ndarray, *, + delta: float, tenor: int, budget_ann: float, roll: str, + f: float, spread: float, f_sell: float | None = None, + free: bool = False) -> tuple[np.ndarray, np.ndarray, dict]: + """Serie GIORNALIERE (ret, wick_extra) del contributo di UNA gamba di copertura, per 1 unita' + di equity allocata a quella gamba. + + ret[i] = variazione giornaliera del valore MTM della posizione (frazione di equity) + wick_extra[i] = guadagno AGGIUNTIVO della copertura al minimo intra-giornaliero, ottenuto + RIPREZZANDO la put al LOW del giorno (cattura la convessita', non solo il delta) + + CAUSALE per costruzione: strike, premio e quantita' a `t` usano solo px[t]/iv[t]; il payoff + usa prezzi successivi e non retroagisce mai sulla decisione. + + `free=True` -> CONTROLLO POSITIVO: il payoff c'e' ma il premio non viene addebitato. + """ + n = len(px) + T_full = tenor / DPY + hold = tenor if roll == "expiry" else max(1, tenor // 2) + n_rolls_year = DPY / hold + b_roll = budget_ann / n_rolls_year # spesa per roll, in frazione di equity + + ret = np.zeros(n) + wick = np.zeros(n) + diag = dict(n_rolls=0, n_itm=0, prem_spent=0.0, payoff_recv=0.0, fees=0.0, spread_cost=0.0) + + i = 60 # warm-up (coerente con VRP01) + while i + hold < n: + S0, sig = float(px[i]), float(iv[i]) + if not (S0 > 0 and sig > 0): + i += hold + continue + K = _strike_from_delta(S0, T_full, sig, delta) + p_model = _bs_put(S0, K, T_full, sig) + if p_model <= 0: + i += hold + continue + p_ask = f * p_model * (1.0 + spread) # si COMPRA al lato caro + fee0 = _entry_fee(p_ask, S0) + cost_unit = p_ask + fee0 + q = b_roll / cost_unit # unita' di sottostante coperte + # giorno d'ingresso: il valore MTM e' q*f*p_model (il mid), quindi si perde subito + # fee + mezzo spread. Se `free`, il premio non si paga affatto. + v_prev = q * f * p_model + # ⚠️ `free` = CONTROLLO POSITIVO (payoff regalato). La prima stesura metteva 0.0 qui e + # lasciava v_prev>0: cosi' il MTM successivo addebitava il decadimento di un valore mai + # accreditato -> il "free lunch" costava quanto il premio e il controllo FALLIVA. Il + # regalo va ACCREDITATO: l'equity sale di v_prev il giorno in cui ricevi l'asset. + ret[i] += (v_prev if free else (v_prev - b_roll)) + diag["n_rolls"] += 1 + diag["prem_spent"] += 0.0 if free else b_roll + diag["fees"] += q * fee0 + diag["spread_cost"] += q * f * p_model * spread + + for s in range(1, hold + 1): + j = i + s + rem = (tenor - s) / DPY + Sj, ivj = float(px[j]), float(iv[j]) + if s == tenor: # SCADENZA: valore = intrinseco + intr = max(0.0, K - Sj) + v = q * intr + if intr > 0: + dfee = min(OPT_DELIVERY_UNDERLYING * Sj, OPT_FEE_CAP_FRAC * intr) * q + v -= dfee + diag["fees"] += dfee + diag["n_itm"] += 1 + diag["payoff_recv"] += v + vw = v # a scadenza niente wick residuo + else: + fs = f if f_sell is None else f_sell + v_mid = q * f * _bs_put(Sj, K, rem, ivj) + v = v_mid + vw = q * f * _bs_put(float(lo[j]), K, rem, ivj) + if s == hold: # VENDITA anticipata: mezzo spread + v = q * fs * _bs_put(Sj, K, rem, ivj) * (1.0 - spread) + sfee = min(OPT_TAKER_UNDERLYING * Sj, OPT_FEE_CAP_FRAC * max(v, 0.0)) + v -= sfee + diag["fees"] += sfee + diag["spread_cost"] += v_mid * spread + diag["payoff_recv"] += v + wick[j] += vw - v_mid # EXTRA al minimo, contro il MID di chiusura + ret[j] += v - v_prev + v_prev = v + i += hold + + return ret, np.maximum(wick, 0.0), diag + + +@lru_cache(maxsize=None) +def hedge_series(delta: float, tenor: int, budget: float, roll: str, f: float, + spread: float, f_sell: float | None = None, + free: bool = False) -> tuple: + """Copertura del LIBRO: 50/50 BTC+ETH (il libro e' 50/50). Ritorna (ret, wick) giornalieri + come frazione dell'equity del libro.""" + P = daily_panel() + ret = np.zeros(len(P)) + wick = np.zeros(len(P)) + diags = {} + for a in ASSETS: + r, w, d = hedge_leg(P[f"px_{a}"].values, P[f"lo_{a}"].values, P[f"iv_{a}"].values, + delta=delta, tenor=tenor, budget_ann=budget * 0.5, roll=roll, + f=f, spread=spread, f_sell=f_sell, free=free) + ret += r + wick += w + diags[a] = d + return ret, wick, diags + + +# ==================================================================== 3. f e SPREAD DAL VENUE + +def _get(path: str, timeout: int = 25): + with urllib.request.urlopen(DERIBIT + path, timeout=timeout) as r: + return json.load(r)["result"] + + +def measure_f_from_venue() -> dict: + """f = quota MID di mercato / prezzo BS a DVOL piatto, MISURATO OGGI sulle ali che questa + strategia comprerebbe. Non e' un'assunzione ereditata dal 30/07: e' una misura su questa + catena, su questi strike, con questo motore. + + Si misura anche il MEZZO SPREAD relativo per bucket di tenore, che serve al roll anticipato. + Solo strumenti con bid E ask veri (un ask solitario al tick non e' un prezzo). + """ + out = dict(ok=False, note=None, per_delta={}, spread={}, n=0) + try: + idx = {a: float(_get(f"get_index_price?index_name={a.lower()}_usdc")["index_price"]) + for a in ASSETS} + dvol = {a: float(_get(f"get_volatility_index_data?currency={a}&start_timestamp=" + f"{int((time.time()-86400)*1000)}&end_timestamp={int(time.time()*1000)}" + f"&resolution=3600")["data"][-1][4]) / 100.0 + for a in ASSETS} + time.sleep(0.6) + bs = _get("get_book_summary_by_currency?currency=USDC&kind=option") + except Exception as e: # rete assente: si dichiara, non si finge + out["note"] = f"venue non raggiungibile ({type(e).__name__}: {e})" + return out + + now = pd.Timestamp.now(tz="UTC") + rows = [] + for r in bs: + nm = r["instrument_name"] + if not nm.endswith("-P"): + continue + base = nm.split("_USDC-")[0] if "_USDC-" in nm else None + if base not in ASSETS: + continue + try: + _, exp, ks, _ = nm.split("-") + K = float(ks.replace("d", ".")) + texp = pd.to_datetime(exp, format="%d%b%y", utc=True) + pd.Timedelta(hours=8) + except Exception: + continue + bid, ask = r.get("bid_price"), r.get("ask_price") + if not bid or not ask or bid <= 0 or ask <= bid: + continue # niente bid vero -> non e' un prezzo + T = (texp - now).total_seconds() / (365.25 * 86400) + if T <= 1 / DPY or T > 45 / DPY: + continue + S, sig = idx[base], dvol[base] + mid = 0.5 * (bid + ask) + half_spread = 0.5 * (ask - bid) / mid + model = _bs_put(S, K, T, sig) + if model <= 1e-9: + continue + # delta BS della put allo stesso vol piatto (per assegnare l'osservazione al suo bucket) + d1 = (math.log(S / K) + 0.5 * sig ** 2 * T) / (sig * math.sqrt(T)) + dlt = -float(_norm.cdf(-d1)) + rows.append(dict(asset=base, K=K, T=T, days=T * DPY, delta=dlt, + mid=mid, model=model, f=mid / model, hs=half_spread)) + if not rows: + out["note"] = "nessuno strumento con bid E ask veri nella finestra <=45g" + return out + D = pd.DataFrame(rows) + out["n"] = int(len(D)) + for dtgt in G_DELTA: + sel = D[(D.delta >= dtgt - 0.035) & (D.delta <= dtgt + 0.035)] + if len(sel) >= 3: + out["per_delta"][dtgt] = dict(n=int(len(sel)), f_med=float(sel.f.median()), + f_lo=float(sel.f.quantile(0.10)), + f_hi=float(sel.f.quantile(0.90)), + hs_med=float(sel.hs.median())) + for lab, (a, b) in dict(t7=(1, 10), t14=(10, 21), t30=(21, 45)).items(): + sel = D[(D.days >= a) & (D.days < b)] + if len(sel) >= 3: + out["spread"][lab] = dict(n=int(len(sel)), hs_med=float(sel.hs.median()), + hs_p90=float(sel.hs.quantile(0.90))) + out["ok"] = bool(out["per_delta"] or out["spread"]) + return out + + +def venue_min_ticket() -> dict: + """Eseguibilita' MISURATA dal venue: min_trade_amount + tick, e il costo in DOLLARI del + lotto minimo per una put deep-OTM vera. ⚠️ Comprare un'opzione costa il PREMIO, non il + NOZIONALE: il muro dei 'lotti da migliaia di dollari' del 22/08 e' il collaterale per + VENDERE, non il costo per COMPRARE. Qui si misura la cosa giusta.""" + out = dict(ok=False, note=None, fam={}) + try: + for cur, pref in (("USDC", "{a}_USDC-"), ("BTC", "BTC-"), ("ETH", "ETH-")): + inst = _get(f"get_instruments?currency={cur}&kind=option&expired=false") + time.sleep(0.6) + for a in ASSETS: + p = pref.format(a=a) + sel = [i for i in inst if i["instrument_name"].startswith(p)] + if not sel: + continue + fam = "USDC-lineare" if cur == "USDC" else "inverse" + out["fam"][(a, fam)] = dict(min_amt=float(sel[0]["min_trade_amount"]), + tick=float(sel[0]["tick_size"]), + settle=sel[0]["settlement_currency"], + n=len(sel)) + out["ok"] = bool(out["fam"]) + except Exception as e: + out["note"] = f"venue non raggiungibile ({type(e).__name__}: {e})" + return out + + +# ==================================================================== 4. LENTE WICK ACCOPPIATA + +@lru_cache(maxsize=1) +def coupled_book_tuples() -> pd.DataFrame: + """(R, m) giornaliere del libro 75/25 sotto la LENTE WICK ACCOPPIATA β€” RIUSATA da + r0725_prop_coupled (importata, non riscritta). Se il recon non e' disponibile si dichiara e + si ricade su close-only, DICENDO che close-only e' CIECA su una regola a UN giorno + (misurato 22/08: 0,00 breach/anno contro 0,40-1,21 veri, rapporto INF).""" + try: + import r0725_prop_coupled as pc + return pc.crypto_daily_tuples(W_TP, W_SKH) + except Exception as e: # dichiarato, mai silenzioso + print(f" ⚠️ lente accoppiata NON disponibile ({type(e).__name__}: {e}) -> close-only") + return pd.DataFrame() + + +# ==================================================================== 5. VALUTAZIONE DI UNA CELLA + +def evaluate(book: np.ndarray, hret: np.ndarray) -> dict: + hedged = book + hret + dd_h = _dd(hedged) + k = k_for_same_dd(book, dd_h) + base_k = k * book + dd_b = _dd(book) + dd_reduced = bool(dd_h < dd_b - 1e-12) + dr_h, dr_k = _drift(hedged), _drift(base_k) + return dict( + sh_base=_sh(book), sh_hedged=_sh(hedged), sh_delev=_sh(base_k), + dd_base=dd_b, dd_hedged=dd_h, dd_delev=_dd(base_k), + dr_base=_drift(book), dr_hedged=dr_h, dr_delev=dr_k, + k=k, hedged=hedged, dd_reduced=dd_reduced, + # VINCE solo se (i) il DD scende davvero E (ii) a quel DD il drift batte il de-levering. + # Senza (i) il null e' DEGENERE (k=1, dr_delev==dr_base) e un Ξ”drift>0 direbbe solo + # "la put ha guadagnato in questa finestra", che e' una domanda di ALPHA β€” non questa. + wins=bool(dd_reduced and dr_h > dr_k), + ) + + +def worst_months(r: pd.Series, n: int = 5) -> list: + m = (1.0 + r).resample("ME").prod() - 1.0 + return [(str(i.date())[:7], float(v)) for i, v in m.nsmallest(n).items()] + + +# ==================================================================== MAIN + +def main() -> None: + t0 = time.time() + print("=" * 100) + print("Β§46 TAIL-HEDGE β€” put deep-OTM come ASSICURAZIONE STATICA sul libro TP01 75 / SKH01 25") + print("=" * 100) + print(f"GRIGLIA DICHIARATA PRIMA DI GUARDARE: {len(G_DELTA)} delta x {len(G_TENOR)} tenori x " + f"{len(G_BUDGET)} budget x {len(G_ROLL)} roll = {N_CELLS} celle per lente di f.") + print("ATTESA A PRIORI: REFUTATO al f realistico. Ragione piu' forte (c): il peggior giorno del") + print(" libro NON e' un crash ma uno short-squeeze di SKH01 -> una PUT non protegge da un rialzo.") + + P = daily_panel() + book = P["book"].values + idx = P.index + bs_book = pd.Series(book, index=idx) + print(f"\nFinestra (comune libro+DVOL): {idx[0].date()} -> {idx[-1].date()} " + f"({len(P)} giorni, {len(P)/DPY:.1f} anni)") + print(f"LIBRO NUDO su questa finestra: Sharpe {_sh(book):.3f} | maxDD {_dd(book)*100:.2f}% | " + f"drift {_drift(book)*100:.2f}%/a") + print("⚠️ La finestra NON contiene marzo 2020 (il DVOL parte 2021-03): il sinistro piu' grande") + print(" della storia del libro e' FUORI CAMPIONE. La copertura non e' testata li'.") + + # ---------------------------------------------------------------- Β§A. f e spread dal venue + print("\n" + "=" * 100) + print("Β§A. f E SPREAD β€” MISURATI OGGI SULLE QUOTE VERE (non assunti dal 30/07)") + print("=" * 100) + fv = measure_f_from_venue() + if fv["ok"]: + print(f" {fv['n']} put con bid E ask veri, tenore <=45g, catena USDC-lineare.") + print(f" {'delta':>7} {'n':>4} {'f mediano':>10} {'f p10':>8} {'f p90':>8} {'mezzo-spread':>13}") + for d, v in sorted(fv["per_delta"].items(), reverse=True): + print(f" {d:>7.2f} {v['n']:>4} {v['f_med']:>10.2f} {v['f_lo']:>8.2f} " + f"{v['f_hi']:>8.2f} {v['hs_med']*100:>12.1f}%") + print(f" mezzo-spread per tenore: " + " ".join( + f"{k} {v['hs_med']*100:.1f}% (n={v['n']})" for k, v in fv["spread"].items())) + else: + print(f" NON MISURATO: {fv['note']}") + F_MEAS = {d: v["f_med"] for d, v in fv.get("per_delta", {}).items()} + if F_MEAS: + print(" -> f USATO nella lente 'misurato oggi': " + + " ".join(f"Ξ΄{d:.2f}={v:.2f}" for d, v in sorted(F_MEAS.items(), reverse=True))) + SPREAD = {} + for lab, ten in (("t7", 7), ("t14", 14), ("t30", 30)): + SPREAD[ten] = fv.get("spread", {}).get(lab, {}).get("hs_med", 0.10) + print(f" -> mezzo-spread usato per tenore: " + + " ".join(f"{t}g {SPREAD[t]*100:.1f}%" for t in G_TENOR) + + ("" if fv["ok"] else " (DEFAULT 10%: venue non letto)")) + print(" ⚠️ DISTORSIONE DICHIARATA, e va nel verso che FAVORISCE la copertura: lo spread e' il") + print(" mediano dei soli strumenti con bid E ask veri. Le ali piu' profonde e piu' corte") + print(" spesso NON hanno bid (solo un ask al tick) e cadono fuori dal campione: quelle") + print(" sarebbero le piu' care, e per un tail-hedge sono proprio quelle che si comprano.") + print(" Quindi il costo qui e' un PAVIMENTO, e il verdetto REFUTATO ne esce rafforzato.") + + # ---------------------------------------------------------------- Β§B. il null del de-levering + print("\n" + "=" * 100) + print("Β§B. NULL DEL DE-LEVERING β€” IL GATE CHE DECIDE (in cima, come si deve)") + print("=" * 100) + print(" Per ogni cella: k = scala che porta il libro NUDO allo STESSO maxDD della coperta.") + print(" Sharpe(k*base) == Sharpe(base) per identita' aritmetica -> la gamba informativa e' il") + print(" DRIFT. La copertura VINCE solo se drift(coperta) > drift(k*base) [e/o Sharpe sale].") + + lenses = [("f=1.00 (modello nudo)", 1.0)] + for f in F_LENSES_STATIC[1:]: + lenses.append((f"f={f:.2f} (misurato 30/07)", f)) + if F_MEAS: + lenses.append(("f MISURATO oggi per-delta", None)) + + all_rows = [] + for lab, fval in lenses: + print(f"\n ---- lente {lab} " + "-" * (78 - len(lab))) + print(f" {'delta':>6} {'ten':>4} {'budget':>7} {'roll':>7} | {'ShCop':>7} {'ShBase':>7} | " + f"{'DDcop':>7} {'DDbase':>7} | {'k':>6} {'DrCop':>8} {'DrDelev':>8} {'Ξ”drift':>8} | esito") + best = None + for dlt in G_DELTA: + f_use = fval if fval is not None else F_MEAS.get(dlt, F_LENSES_STATIC[1]) + for ten in G_TENOR: + for bud in G_BUDGET: + for roll in G_ROLL: + hr, hw, dg = hedge_series(dlt, ten, bud, roll, f_use, SPREAD[ten]) + ev = evaluate(book, hr) + gap = ev["dr_hedged"] - ev["dr_delev"] + win = ev["wins"] + row = dict(lens=lab, f=f_use, delta=dlt, tenor=ten, budget=bud, roll=roll, + **{k: v for k, v in ev.items() if k != "hedged"}, gap=gap, + hret=hr, hwick=hw, diag=dg) + all_rows.append(row) + if best is None or gap > best["gap"]: + best = row + if bud == 0.010 and roll == "expiry": # riga stampata: budget centrale + print(f" {dlt:>6.2f} {ten:>4} {bud*100:>6.1f}% {roll:>7} | " + f"{ev['sh_hedged']:>7.3f} {ev['sh_base']:>7.3f} | " + f"{ev['dd_hedged']*100:>6.2f}% {ev['dd_base']*100:>6.2f}% | " + f"{ev['k']:>6.3f} {ev['dr_hedged']*100:>7.2f}% " + f"{ev['dr_delev']*100:>7.2f}% {gap*100:>+7.2f}% | " + f"{'COPERTURA' if win else ('de-levering' if ev['dd_reduced'] else 'DD PEGGIORE')}") + sub = [r for r in all_rows if r["lens"] == lab] + nwin = sum(1 for r in sub if r["wins"]) + nred = sum(1 for r in sub if r["dd_reduced"]) + cand = [r for r in sub if r["dd_reduced"]] + best = max(cand or sub, key=lambda r: (r["gap"] if cand else -r["dd_hedged"])) + print(f" >> {lab}: il maxDD SCENDE in {nred}/{len(sub)} celle; fra quelle la copertura " + f"batte il de-levering in {nwin}/{len(sub)}.") + head = ("migliore fra le celle che RIDUCONO il DD" if cand else + "NESSUNA cella riduce il DD -> si riporta quella col DD MINORE (k=1, confronto " + "degenere)") + print(f" {head}: delta {best['delta']:.2f} / " + f"{best['tenor']}g / {best['budget']*100:.1f}% / {best['roll']} -> " + f"maxDD {best['dd_base']*100:.2f}% -> {best['dd_hedged']*100:.2f}% (k {best['k']:.3f}), " + f"Ξ”drift vs de-levering {best['gap']*100:+.2f} pp/a, " + f"Ξ”Sharpe {best['sh_hedged']-best['sh_base']:+.3f}") + + # ---------------------------------------------------------------- Β§C. controlli + print("\n" + "=" * 100) + print("Β§C. CONTROLLI β€” un rilevatore che non segnala nulla puo' essere semplicemente rotto") + print("=" * 100) + ctrl = [] + # (+1) copertura GRATIS (payoff senza premio): DEVE ridurre il DD e battere il de-levering + hr, _, _ = hedge_series(-0.10, 30, 0.020, "expiry", 1.0, 0.0, free=True) + ev = evaluate(book, hr); ctrl.append(("(+) payoff SENZA premio (free lunch)", ev, True)) + # (+2) premio 1/10: stesso budget -> 10x le put. Speculare al negativo, DEVE funzionare + hr, _, _ = hedge_series(-0.10, 30, 0.020, "expiry", 0.1, 0.0) + ev = evaluate(book, hr); ctrl.append(("(+) premio 1/10 (f=0.1, 10x le put)", ev, True)) + # (-) premio 10x: DEVE essere refutata + hr, _, _ = hedge_series(-0.10, 30, 0.020, "expiry", 10.0, 0.0) + ev = evaluate(book, hr); ctrl.append(("(-) premio 10x (f=10)", ev, False)) + # (=) put ATM f=1, spread 0: NON e' un controllo (non deve funzionare per forza) ma e' il + # riferimento piu' caro possibile della famiglia: si stampa come misura, non come test. + hr, _, _ = hedge_series(-0.50, 30, 0.020, "expiry", 1.0, 0.0) + ev = evaluate(book, hr); ctrl.append(("(=) put ATM(Ξ΄-0.50) f=1 β€” misura, non controllo", ev, None)) + for nm, ev, must in ctrl: + gap = ev["dr_hedged"] - ev["dr_delev"] + got = "RICONOSCIUTA" if ev["wins"] else "refutata" + tag = "" if must is None else (" βœ“" if ev["wins"] == must else " βœ— CONTROLLO FALLITO") + print(f" {nm:<48} DD {ev['dd_base']*100:5.2f}%->{ev['dd_hedged']*100:5.2f}% k {ev['k']:.3f} " + f"Ξ”drift {gap*100:+7.2f}pp ShΞ” {ev['sh_hedged']-ev['sh_base']:+.3f} -> {got}{tag}") + ok = all(ev["wins"] == must for _, ev, must in ctrl if must is not None) + print(" APPARATO: " + ("controlli 3/3 OK β€” il rilevatore riconosce cio' che DEVE riconoscere " + "e rifiuta cio' che DEVE rifiutare" + if ok else + "\u26a0\ufe0f UN CONTROLLO E' FALLITO: i numeri sopra NON sono credibili")) + + # ---------------------------------------------------------------- Β§D. causalita' + print("\n" + "=" * 100) + print("Β§D. CAUSALITA' β€” prova esplicita, non un gate generico") + print("=" * 100) + print(" `altlib.causality_ok` vuole un target_fn(df)->posizioni: NON si applica a un overlay") + print(" di opzioni (non e' una posizione continua sul sottostante). Al suo posto la prova") + print(" diretta e piu' forte: si ricalcola la copertura su un PREFISSO del pannello e si") + print(" verifica che coincida con la coda della serie completa. Un leak divergerebbe.") + hr_full, _, _ = hedge_series(-0.10, 14, 0.010, "expiry", 1.0, SPREAD[14]) + cut = int(len(P) * 0.85) + Pf = daily_panel() + sub = Pf.iloc[:cut] + hr_cut, _, _ = hedge_leg(sub["px_BTC"].values, sub["lo_BTC"].values, sub["iv_BTC"].values, + delta=-0.10, tenor=14, budget_ann=0.010 * 0.5, roll="expiry", + f=1.0, spread=SPREAD[14]) + hr_full_btc, _, _ = hedge_leg(Pf["px_BTC"].values, Pf["lo_BTC"].values, Pf["iv_BTC"].values, + delta=-0.10, tenor=14, budget_ann=0.010 * 0.5, roll="expiry", + f=1.0, spread=SPREAD[14]) + # ⚠️ ERRORE MIO, catturato dalla prima corsa (dmax 1.28e-04 = "DIVERGE"): confrontavo fino a + # `cut`, ma il prefisso si ferma un roll prima (`while i + hold < n`), quindi l'ultimo ciclo + # non e' CALCOLATO nel troncato mentre lo e' nel completo. La differenza era per ASSENZA di + # un roll, non per look-ahead. Il confronto onesto finisce prima dell'ultimo roll possibile. + hold = 14 + stop = cut - 2 * hold + tail = 200 + dmax = float(np.max(np.abs(hr_cut[stop - tail:stop] - hr_full_btc[stop - tail:stop]))) + dmax_naive = float(np.max(np.abs(hr_cut[cut - tail:cut] - hr_full_btc[cut - tail:cut]))) + print(f" max|differenza| su [{stop-tail}:{stop}] (cut a {cut}/{len(P)}, ultimo roll escluso): " + f"{dmax:.2e} -> {'CAUSALE' if dmax < 1e-12 else '⚠️ DIVERGE'}") + print(f" (fino a `cut` fa {dmax_naive:.2e}: NON e' un leak, e' il roll che il prefisso non ha") + print(f" ancora eseguito β€” la finestra sbagliata dichiarava un difetto inesistente.)") + + # ---------------------------------------------------------------- Β§E. lente di coda + print("\n" + "=" * 100) + print("Β§E. LENTE DI CODA β€” maxDD, peggiori mesi, peggior giorno, breach 4%/6%") + print("=" * 100) + CP = coupled_book_tuples() + if not CP.empty: + CP = CP[(CP.index >= idx[0]) & (CP.index <= idx[-1])] + cov = len(CP.index.intersection(idx)) / len(idx) + print(f" lente ACCOPPIATA disponibile: {len(CP)} giorni, copertura {cov*100:.1f}% del pannello") + # scelta della cella per la lente di coda: la MIGLIORE al f misurato (o 2.23), budget 1% + f_pick = F_MEAS.get(-0.10, F_LENSES_STATIC[1]) + cells_tail = [("NUDO", None)] + for dlt in G_DELTA: + cells_tail.append((f"Ξ΄{dlt:.2f} 30g 1%/a expiry", (dlt, 30, 0.010, "expiry", + F_MEAS.get(dlt, f_pick)))) + cells_tail.append(("Ξ΄-0.10 30g 2%/a expiry", (-0.10, 30, 0.020, "expiry", f_pick))) + cells_tail.append(("Ξ΄-0.10 30g 2%/a f=1 (irreale)", (-0.10, 30, 0.020, "expiry", 1.0))) + + def _breach(series_close: np.ndarray, wick_extra: np.ndarray) -> tuple: + s_ = pd.Series(series_close, index=idx) + n_yr = len(P) / DPY + br_c = float((s_ <= -0.04).sum()) / n_yr + if CP.empty: + return float("nan"), br_c + m = CP["m"].reindex(idx).values + n_ev = int(np.nansum((m + np.nan_to_num(wick_extra)) <= -0.04)) + _breach.last_n = n_ev + return n_ev / n_yr, br_c + + print(f"\n {'variante':<32} {'maxDD':>7} {'pegg.giorno':>12} {'breach 4% /a':>14} " + f"{'(close-only)':>13} {'drift':>8} {'Sharpe':>7}") + for nm, spec in cells_tail: + if spec is None: + hr = np.zeros(len(P)); hw = np.zeros(len(P)) + else: + dlt, ten, bud, roll, fu = spec + hr, hw, _ = hedge_series(dlt, ten, bud, roll, fu, SPREAD[ten]) + hed = book + hr + s = pd.Series(hed, index=idx) + worst_day = float(s.min()) + br_coup, br_close = _breach(hed, hr + hw) # il minimo del libro + la copertura + n_ev = getattr(_breach, "last_n", -1) + print(f" {nm:<32} {_dd(hed)*100:>6.2f}% {worst_day*100:>11.2f}% {br_coup:>8.2f}({n_ev:>2}) " + f"{br_close:>12.2f} {_drift(hed)*100:>7.2f}% {_sh(hed):>7.3f}") + if spec is not None: + # ⚠️ il confronto a pari DD e' DEGENERE qui (il DD non scende, quindi k=1). Sull'asse + # funded la domanda onesta e' l'altra: a PARI DRIFT, chi sfora meno la regola a un + # giorno? Cioe' basta rimpicciolire la posizione per comprare gli stessi breach in meno. + kk = k_for_same_drift(book, _drift(hed)) + brk, brkc = _breach(kk * book, np.zeros(len(P)) if CP.empty + else (kk - 1.0) * np.nan_to_num(CP['m'].reindex(idx).values)) + lab_ = " ^ de-levering k=%.3f, PARI DRIFT" % kk + print(f" {lab_:<32} {_dd(kk*book)*100:>6.2f}% " + f"{float(np.min(kk*book))*100:>11.2f}% {brk:>8.2f}({getattr(_breach,'last_n',-1):>2}) " + f"{brkc:>12.2f} " + f"{_drift(kk*book)*100:>7.2f}% {_sh(kk*book):>7.3f}") + print(" ⚠️ close-only e' CIECA su una regola a UN giorno (22/08: 0,00 contro 0,40-1,21 veri).") + print(" La colonna che decide e' quella ACCOPPIATA. Fra parentesi il CONTEGGIO di eventi:") + print(" 3-5 breach in 5,4 anni NON distinguono le varianti fra loro (un evento in piu' o in") + print(" meno sposta il tasso del 20-30%) -> si legge il SEGNO, non la taglia. Il fatto") + print(" robusto e' che il de-levering a PARI DRIFT compra lo stesso ordine di grandezza di") + print(" riduzione dei breach senza toccare il maxDD nel verso sbagliato.") + print("\n REGOLA MAX-LOSS 6% (statica dal saldo iniziale, HyroTrader β€” e' la regola BINDING") + print(" del canale funded, gia' modellata il 25/07 come EV_DD=0.06):") + k6 = k_for_same_dd(book, 0.06) + print(f" libro NUDO maxDD {_dd(book)*100:5.2f}% -> SFORA (serve k={k6:.3f} per " + f"stare a 6%, drift {_drift(k6*book)*100:.2f}%/a)") + for nm, spec in cells_tail[1:]: + dlt, ten, bud, roll, fu = spec + hr2, _, _ = hedge_series(dlt, ten, bud, roll, fu, SPREAD[ten]) + hd = book + hr2 + k6h = k_for_same_dd(hd, 0.06) + print(f" {nm:<20} maxDD {_dd(hd)*100:5.2f}% -> SFORA (serve k={k6h:.3f}, " + f"drift {_drift(k6h*hd)*100:.2f}%/a)") + print(" >> Sull'UNICA regola che decide un conto funded la copertura va nel verso SBAGLIATO:") + print(" alza il maxDD e quindi ABBASSA la leva ammessa. Il beneficio sui breach a un") + print(" giorno (0,92 -> 0,55/anno) e' reale ma si compra piu' a buon mercato de-levando,") + print(" che sulla regola a 6 punti e' l'unica leva che funziona.") + + print("\n 5 PEGGIORI MESI (nudo vs coperto Ξ΄-0.10 30g 1%/a, f misurato):") + hr, _, _ = hedge_series(-0.10, 30, 0.010, "expiry", F_MEAS.get(-0.10, f_pick), SPREAD[30]) + wm_b = worst_months(bs_book) + wm_h = worst_months(pd.Series(book + hr, index=idx)) + mh = dict(wm_h) + hs_m = (1.0 + pd.Series(book + hr, index=idx)).resample("ME").prod() - 1.0 + hs_m.index = [str(i.date())[:7] for i in hs_m.index] + for mth, v in wm_b: + hv = float(hs_m.get(mth, float("nan"))) + print(f" {mth} nudo {v*100:>7.2f}% coperto {hv*100:>7.2f}% " + f"delta {(hv - v)*100:>+6.2f}pp") + + # ---------------------------------------------------------------- Β§F. il rischio sbagliato + print("\n" + "=" * 100) + print("Β§F. L'ATTESA (c) MISURATA β€” la put protegge dal rischio che il libro ha davvero?") + print("=" * 100) + und = 0.5 * (P["px_BTC"].pct_change() + P["px_ETH"].pct_change()).fillna(0.0).values + ordr = np.argsort(book)[:20] + n_crash = int((und[ordr] <= -0.05).sum()) + print(f" Sui 20 PEGGIORI giorni del libro: {n_crash}/20 sono giorni di crollo del sottostante") + print(f" (BTC/ETH medio <= -5%). Negli altri {20-n_crash} il libro perde mentre il mercato NON") + print(f" crolla -> una put non puo' proteggere per costruzione.") + print(f" {'giorno':>12} {'libro':>8} {'BTC/ETH':>9} {'TP01':>8} {'SKH01':>8}") + for j in ordr[:8]: + print(f" {str(idx[j].date()):>12} {book[j]*100:>7.2f}% {und[j]*100:>8.2f}% " + f"{P['tp'].values[j]*100:>7.2f}% {P['skh'].values[j]*100:>7.2f}%") + beta = float(np.polyfit(und, book, 1)[0]) + corr = float(np.corrcoef(book, und)[0, 1]) + print(f" corr(libro, sottostante) = {corr:+.3f} | beta del libro al sottostante = {beta:+.3f}") + print() + print(" --- VERDETTO SULL'ATTESA A PRIORI (si riporta anche dove e' REFUTATA) ---") + if n_crash >= 12: + print(f" (c) REFUTATA nella forma in cui l'ho scritta: {n_crash}/20 dei peggiori giorni del") + print( " libro SONO giorni di crollo. Non e' vero che si compra assicurazione contro il") + print( " rischio sbagliato in senso di DIREZIONE. ⚠️ E il caso che avevo in mente (lo") + print( " short-squeeze di SKH01) sta FUORI FINESTRA: il DVOL parte 2021-03.") + else: + print(f" (c) CONFERMATA: solo {n_crash}/20 dei peggiori giorni sono crolli.") + print(f" (b) CONFERMATA, ed e' il meccanismo operativo: beta del libro al sottostante {beta:+.3f}.") + print( " Il 2021-05-19 il sottostante ha fatto -21,1% e il libro -2,0%: la strategia aveva") + print( " GIA' ridotto la perdita di ~10x. La put paga in proporzione al crollo del") + print( " SOTTOSTANTE, ma protegge una perdita gia' ridotta di un ordine di grandezza ->") + print( " per coprire un DD del libro serve un nozionale di put ~1/beta volte l'equity, e") + print( " quel nozionale costa ~1/beta volte il budget.") + print( " (a) CONFERMATA: vedi il decadimento del premio nel Β§B (il costo cresce col budget e") + print( " col f mentre il beneficio satura).") + + # ---------------------------------------------------------------- Β§G. eseguibilita' + print("\n" + "=" * 100) + print("Β§G. ESEGUIBILITA' β€” misurata DAL VENUE. Comprare un'opzione costa il PREMIO, non il") + print(" NOZIONALE: il muro dei 'lotti da migliaia di dollari' e' il collaterale per VENDERE.") + print("=" * 100) + vm = venue_min_ticket() + if vm["ok"]: + print(f" {'asset':>6} {'famiglia':>14} {'min_trade_amount':>18} {'tick':>10} {'settle':>8}") + for (a, fam), v in sorted(vm["fam"].items()): + print(f" {a:>6} {fam:>14} {v['min_amt']:>18} {v['tick']:>10} {v['settle']:>8}") + else: + print(f" NON MISURATO: {vm['note']}") + # costo del lotto minimo con i premi modellati di fine campione + print(f"\n Costo del LOTTO MINIMO e capitale a cui la copertura diventa comprabile a ogni roll:") + print(f" {'delta':>6} {'ten':>4} {'premio/unita':>13} {'lotto min':>10} " + f"{'$ lotto':>9} {'budget 1%/roll a $635':>22} {'capitale minimo':>16}") + for dlt in G_DELTA: + for ten in (7, 30): + S = float(P["px_BTC"].values[-1]); sig = float(P["iv_BTC"].values[-1]) + T = ten / DPY + K = _strike_from_delta(S, T, sig, dlt) + pm = _bs_put(S, K, T, sig) + fu = F_MEAS.get(dlt, F_LENSES_STATIC[1]) + price = fu * pm + minamt = vm["fam"].get(("BTC", "USDC-lineare"), {}).get("min_amt", 0.01) + lot = minamt * price + per_roll_635 = 0.010 * (ten / DPY) * 0.5 * 635.0 # 1%/a, meta' su BTC + cap_min = lot / (0.010 * (ten / DPY) * 0.5) + print(f" {dlt:>6.2f} {ten:>4} {price:>13.2f} {minamt:>10} {lot:>9.2f} " + f"{per_roll_635:>21.2f} {cap_min:>15,.0f}") + print(" ⚠️ E c'e' un secondo muro, STRUTTURALE e non di capitale: il TICK. Su BTC_USDC il tick") + print(" e' 5 USDC -> una put che vale meno di $5 NON e' quotabile sotto $5. Piu' e' deep-OTM") + print(" (cioe' piu' e' economica la copertura) piu' il tick domina il prezzo.") + # eval_weights_smallcap sulla gamba LINEARE, come riferimento del pavimento + try: + from src.strategies.trend_portfolio import TrendPortfolio, CANONICAL + d1 = resample_1d(load_data("BTC", "1h")) + tpm = TrendPortfolio(**CANONICAL) + sc = al.eval_weights_smallcap(d1, tpm.target_series(d1), capital=635.0, min_order=5.0) + print(f"\n Riferimento (gamba LINEARE dello stesso libro): TP01/BTC a $635 haircut Sharpe " + f"{sc['sharpe_haircut']:+.3f} -> il pavimento $5 non morde sul perpetual.") + print(" La differenza NON e' il livello del costo ma la sua FORMA (regola 25/07): sul") + print(" perpetual il minimo e' in DOLLARI di nozionale, sull'opzione e' in UNITA' di") + print(" sottostante MOLTIPLICATE per un premio che tende a zero -> due muri diversi.") + except Exception as e: + print(f"\n (riferimento smallcap non calcolato: {type(e).__name__}: {e})") + + # ---------------------------------------------------------------- Β§H. gate residui + print("\n" + "=" * 100) + print("Β§H. GATE RESIDUI") + print("=" * 100) + # cella migliore in assoluto al f misurato/realistico + real = [r for r in all_rows if r["lens"] != "f=1.00 (modello nudo)"] + pool = real if real else all_rows + bestr = max(pool, key=lambda r: r["gap"]) + hedged_best = pd.Series(book + bestr["hret"], index=idx) + imp = al.implausible_sharpe(hedged_best) + print(f" implausible_sharpe(libro coperto, cella migliore al f realistico): " + f"implausible={imp['implausible']} Sharpe {imp.get('sharpe', float('nan')):.2f} " + f"maxDD {imp.get('maxdd', float('nan'))*100:.2f}%") + for r in imp["reasons"]: + print(f" - {r}") + sr_all = [r["sh_hedged"] for r in pool] + dsr, nullmax = al.deflated_sharpe(bestr["sh_hedged"], sr_all, hedged_best) + print(f" deflated_sharpe della cella MIGLIORE su {len(pool)} trial: DSR {dsr:.3f} " + f"(max atteso dal nullo {nullmax:.2f}) -> {'PASS' if dsr >= 0.95 else 'FAIL'}") + print(" ⚠️ Il DSR qui e' informativo ma NON e' il gate che decide: la domanda non e' 'questa") + print(" cella ha alpha' (non ne ha per costruzione) ma 'batte il de-levering' β€” ed e' il Β§B.") + + # sensitivita': f asimmetrico (compri caro, vendi a modello) + print("\n SENSITIVITA' f ASIMMETRICO (si compra a f, si vende a modello β€” caso pessimo per il") + print(" roll anticipato, che nel modello base non paga f perche' non arriva a intrinseco):") + fu = F_MEAS.get(-0.10, F_LENSES_STATIC[1]) + for roll in G_ROLL: + hs, _, _ = hedge_series(-0.10, 30, 0.010, roll, fu, SPREAD[30]) + ha, _, _ = hedge_series(-0.10, 30, 0.010, roll, fu, SPREAD[30], f_sell=1.0) + es, ea = evaluate(book, hs), evaluate(book, ha) + print(f" roll={roll:<7} f simmetrico Ξ”drift {(es['dr_hedged']-es['dr_delev'])*100:+6.2f}pp" + f" f asimmetrico Ξ”drift {(ea['dr_hedged']-ea['dr_delev'])*100:+6.2f}pp") + + # ---------------------------------------------------------------- Β§I. verdetto + print("\n" + "=" * 100) + print("Β§I. VERDETTO") + print("=" * 100) + for lab, _ in lenses: + sub = [r for r in all_rows if r["lens"] == lab] + nw = sum(1 for r in sub if r["wins"]) + nred = sum(1 for r in sub if r["dd_reduced"]) + cand = [r for r in sub if r["dd_reduced"]] or sub + bb = max(cand, key=lambda r: r["gap"]) + print(f" {lab:<32}: DD scende {nred:>2}/{len(sub)} | batte il de-levering {nw:>2}/{len(sub)}" + f" | miglior Ξ”drift {bb['gap']*100:+6.2f} pp/a | Ξ”Sharpe {bb['sh_hedged']-bb['sh_base']:+.3f}") + nw_real = sum(1 for r in pool if r["wins"]) + nred_real = sum(1 for r in pool if r["dd_reduced"]) + verdict = "REFUTATO" if nw_real == 0 else ("PARZIALE" if nw_real < len(pool) * 0.5 else "REGGE") + print(f"\n >>> {verdict}: al f realistico il maxDD scende in {nred_real}/{len(pool)} celle e la") + print(f" copertura batte il de-levering in {nw_real}/{len(pool)}.") + print( " ⚠️ Il fatto che il maxDD SALGA nella maggior parte delle celle e' il risultato piu'") + print( " forte del filone: prima ancora del prezzo, la premessa 'la copertura riduce il") + print( " drawdown' e' FALSA su questo libro β€” il bleed del premio cade DENTRO i drawdown") + print( " (che sono lunghi e poco profondi) mentre il payoff cade su singoli giorni di") + print( " crollo che NON sono il fondo del drawdown.") + print(f"\n Tempo: {time.time()-t0:.0f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_tp01_ls.py b/scripts/research/r0823_tp01_ls.py new file mode 100644 index 0000000..d949893 --- /dev/null +++ b/scripts/research/r0823_tp01_ls.py @@ -0,0 +1,594 @@ +#!/usr/bin/env python +"""r0823_tp01_ls.py β€” Β§42 TP01-LS: la gamba SHORT del trend a 1d paga il proprio costo? + +DOMANDA. TP01 e' long-flat PER SCELTA: nei ribassi va a 0x invece di girarsi short. Il progetto +lo descrive come ASSICURAZIONE (taglia il DD ~6x contro buy&hold, contributo hold-out negativo +nel 99,1% delle ancore, LOO 26/07). L'obiettivo dichiarato e' il DRIFT, non lo Sharpe. La +direzione mai presa al TF di deploy e' il TSMOM LONG-SHORT a 1d: il progetto ha UNA riga +("trend 1h L/S ~1.0" fra gli edge deboli), misurata a 1h e senza l'harness onesto di oggi. + +Decidibile in quattro pezzi: + (a) la short aggiunge DRIFT? + (b) sopravvive al GATE ISO-VOLATILITA'? (se aggiunge drift solo alzando l'esposizione e' + leva travestita β€” null del de-levering al contrario, 5 occorrenze nel progetto) + (c) NON e' semplicemente il beta del 2022? (si toglie il 2022 e si rifa') + (d) resta ADDS quando la si giudica contro TP01 stesso (`marginal_vs_tp01`)? + +ATTESA A PRIORI DEL COORDINATORE, dichiarata PRIMA di misurare: il crypto ha drift positivo +strutturale e la short di trend perde quasi sempre fuori dal 2022 -> il guadagno sara' UN ANNO +SOLO (2022), lo stesso schema di SOL (22/08: 24/24 ancore positive, un anno solo). Se e' cosi' +la conclusione non e' "la short funziona" ma "il 2022 e' un evento nel campione". + +MECCANISMO CONGELATO β€” UN SOLO GRADO DI LIBERTA'. Si parte dal TP01 di PRODUZIONE +(src/strategies/trend_portfolio.TrendPortfolio + CANONICAL) e si cambia SOLO il pavimento della +direzione TSMOM, da 0 (clip long-flat) a `floor`. Tutto il resto e' importato, non riscritto: +orizzonti 30/90/180g, vol_win 30g, target_vol 20%, leva cap 2x, fee 0.05%/lato, 50/50 BTC+ETH, +lo shift causale e la contabilita' fee stanno in `TrendPortfolio.net_returns` EREDITATA. +La direzione del blend a 3 orizzonti vive in {-1,-1/3,+1/3,+1}, quindi il pavimento e' un vero +knob discreto: + floor= 0 -> {0,0,1/3,1} = CANONICO (long-flat) + floor=-1/3 -> {-1/3,-1/3,1/3,1} = short "timida" (solo 1/3 di size al ribasso pieno) + floor=-2/3 -> {-2/3,-1/3,1/3,1} + floor=-1 -> {-1,-1/3,1/3,1} = LONG-SHORT pieno (== long_only=False di produzione) + +GRIGLIA DICHIARATA PRIMA DI GUARDARE: 4 celle (i 4 floor) su UN solo TF (1d, quello di deploy). +Nessun altro parametro riaperto -> 4 trial, contati nel deflated-Sharpe. La griglia di 24 ANCORE +non e' una griglia di parametri (il DSR non la conta): si giudica con `anchor_luck_band` / +`anchor_luck_delta`, come prescritto dal gate del 26/07. + +REPLICA BIT-EXACT PRIMA DI OGNI DELTA (tre vie, richiesta max|diff| == 0): + (1) floor=0 -> target_series == TrendPortfolio(**CANONICAL).target_series + (2) floor=-1 -> target_series == TrendPortfolio(long_only=False).target_series + (cioe' la mia unica riga nuova coincide con la produzione a ENTRAMBI gli estremi: il + codice davvero nuovo sono solo i due floor intermedi) + (3) serie 50/50 a floor=0 vs src.portfolio.sleeves._tp01_returns() (lo sleeve del libro) + +Onesta': altlib (`marginal_vs_tp01`, `deflated_sharpe`, `causality_ok`, `implausible_sharpe`, +`eval_weights_smallcap`, `anchor_luck_*`) importato, non riscritto. Controlli positivi +OBBLIGATORI su ogni gate (un gate che non segnala puo' essere semplicemente rotto). + +Vincoli: NESSUN file toccato fuori da questo script, nessun ordine inviato, sola lettura su +data/raw. Runtime ~1-2 min su 2 core. +""" +from __future__ import annotations + +import sys +import time +from dataclasses import dataclass +from functools import lru_cache +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT)) + +import altlib as al # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, realized_vol, simple_returns, tsmom_blend, +) + +ASSETS = ("BTC", "ETH") +HOLDOUT = al.HOLDOUT +FLOORS = (0.0, -1.0 / 3.0, -2.0 / 3.0, -1.0) # LA GRIGLIA, dichiarata prima +FLOOR_LBL = {0.0: "0 (canonico LF)", -1.0 / 3.0: "-1/3", -2.0 / 3.0: "-2/3", -1.0: "-1 (LS pieno)"} +OFFSETS = tuple(range(24)) # banda d'ancora giornaliera +W_TP01_BOOK, W_SKH_BOOK = 0.75, 0.25 # src/live/book.py +CAPITAL = 635.0 # conto reale dichiarato nel briefing + + +# =========================================================================== +# IL MECCANISMO β€” TP01 di produzione con UN grado di liberta' in piu' +# =========================================================================== +@dataclass +class TPFloor(TrendPortfolio): + """TP01 con pavimento della direzione parametrico. UNICA riga diversa dalla produzione: + `np.clip(direction, self.floor, None)` al posto di `np.clip(direction, 0, None)`. + Tutto il resto (vol realizzata, blend TSMOM, scaling, cap di leva, net_returns con lo + shift causale e le fee sul turnover) e' ereditato/importato dal modulo di produzione.""" + floor: float = 0.0 + + def target_series(self, df: pd.DataFrame) -> np.ndarray: + c = df["close"].values.astype(float) + bpd = self._bpd(df) + bpy = bpd * 365.25 + r = simple_returns(c) + vol = realized_vol(r, self.vol_win_days * bpd, bpy) + horizons = tuple(d * bpd for d in self.horizons_days) + direction = np.clip(tsmom_blend(c, horizons), self.floor, None) # <-- IL GRADO DI LIBERTA' + scal = np.where((vol > 0) & np.isfinite(vol), self.target_vol / vol, 0.0) + tgt = np.clip(direction * scal, -self.leverage, self.leverage) + tgt[~np.isfinite(tgt)] = 0.0 + return tgt + + +_CFG = {k: v for k, v in CANONICAL.items() if k != "long_only"} + + +def tp_floor(floor: float) -> TPFloor: + return TPFloor(long_only=False, floor=floor, **_CFG) + + +# =========================================================================== +# DATI β€” resample 1h -> 24h con ANCORA h (stessa convenzione di resample_tf) +# =========================================================================== +@lru_cache(maxsize=8) +def _h1(asset: str) -> pd.DataFrame: + return al.get(asset, "1h") + + +@lru_cache(maxsize=128) +def bars(asset: str, off: int) -> pd.DataFrame: + g = _h1(asset).copy() + idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True) + idx.name = "dt" + g.index = idx + out = g.resample("24h", offset=pd.Timedelta(hours=off), label="left", closed="left").agg( + {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}) + out = out.dropna(subset=["open"]) + out["datetime"] = out.index + epoch = pd.Timestamp("1970-01-01", tz="UTC") + out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64") + return out.reset_index(drop=True)[ + ["timestamp", "open", "high", "low", "close", "volume", "datetime"]] + + +@lru_cache(maxsize=512) +def sleeve(floor: float, off: int = 0, fee_side: float | None = None) -> pd.Series: + """Serie 50/50 BTC+ETH dei rendimenti NETTI per barra giornaliera, ancora `off`. + Usa `TrendPortfolio.net_returns` EREDITATA -> shift causale e fee di produzione.""" + tp = tp_floor(floor) + if fee_side is not None: + tp.fee_side = fee_side + ser = {} + for a in ASSETS: + df = bars(a, off) + net, ts = tp.net_returns(df) + ser[a] = pd.Series(net, index=pd.DatetimeIndex(pd.to_datetime(ts.values, utc=True))) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + return pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index) + + +# =========================================================================== +# METRICHE +# =========================================================================== +def sh(s: pd.Series) -> float: + return al._sh(s) + + +def dd(s: pd.Series) -> float: + return al._dd_ret(s) + + +def drift(s: pd.Series) -> float: + """Drift annualizzato in LOG (la grandezza su cui vivono i muri di capitale; il carry si + misura sul log, mai sulla media aritmetica delle differenze β€” regola 23/08).""" + r = np.asarray(s.dropna().values, float) + return float(np.mean(np.log1p(np.clip(r, -0.99, None))) * 365.25) if len(r) else 0.0 + + +def cagr(s: pd.Series) -> float: + r = np.asarray(s.dropna().values, float) + if len(r) < 2: + return 0.0 + return float(np.exp(np.mean(np.log1p(np.clip(r, -0.99, None))) * 365.25) - 1.0) + + +def vol(s: pd.Series) -> float: + r = np.asarray(s.dropna().values, float) + return float(np.std(r) * np.sqrt(365.25)) if len(r) else 0.0 + + +def hold(s: pd.Series) -> pd.Series: + return s[s.index >= HOLDOUT] + + +def no2022(s: pd.Series) -> pd.Series: + return s[s.index.year != 2022] + + +def row(s: pd.Series) -> dict: + return dict(sh=sh(s), sh_hold=sh(hold(s)), cagr=cagr(s), drift=drift(s), + vol=vol(s), dd=dd(s), n=len(s)) + + +def turnover_year(floor: float, off: int = 0) -> float: + tot = 0.0 + for a in ASSETS: + df = bars(a, off) + tgt = tp_floor(floor).target_series(df) + held = np.zeros(len(tgt)); held[1:] = tgt[:-1] + turn = np.abs(np.diff(held, prepend=0.0)).sum() + yrs = (df["datetime"].iloc[-1] - df["datetime"].iloc[0]).total_seconds() / 86400 / 365.25 + tot += turn / yrs + return tot / len(ASSETS) + + +def time_in_market(floor: float, off: int = 0) -> tuple[float, float]: + lo, sho = [], [] + for a in ASSETS: + tgt = tp_floor(floor).target_series(bars(a, off)) + lo.append(float(np.mean(tgt > 0))); sho.append(float(np.mean(tgt < 0))) + return float(np.mean(lo)), float(np.mean(sho)) + + +def hr(t=""): + print("\n" + "=" * 78) + if t: + print(t) + print("=" * 78) + + +# =========================================================================== +def main() -> None: + t_start = time.time() + print(__doc__.split("\n\n")[0]) + print(f"\nAvvio {pd.Timestamp.now('UTC')} | griglia dichiarata: {len(FLOORS)} floor x 1 TF (1d) = " + f"{len(FLOORS)} trial | ancore: {len(OFFSETS)} (NON trial: gate d'ancora)") + + # ------------------------------------------------------------------ 1. REPLICA BIT-EXACT + hr("1. REPLICA BIT-EXACT DEL CANONICO (prima di qualunque delta)") + ok_all = True + canon = TrendPortfolio(**CANONICAL) + ls_prod = TrendPortfolio(**{**CANONICAL, "long_only": False}) + for a in ASSETS: + df = bars(a, 0) + d0 = float(np.max(np.abs(tp_floor(0.0).target_series(df) - canon.target_series(df)))) + d1 = float(np.max(np.abs(tp_floor(-1.0).target_series(df) - ls_prod.target_series(df)))) + print(f" {a}: max|floor=0 - CANONICAL(long_only=True) | = {d0:.1e} " + f"max|floor=-1 - CANONICAL(long_only=False)| = {d1:.1e}") + ok_all &= (d0 == 0.0) and (d1 == 0.0) + from src.portfolio.sleeves import _tp01_returns + prod = _tp01_returns() + mine = sleeve(0.0, 0) + JJ = pd.concat({"p": prod, "m": mine}, axis=1, join="inner").dropna() + d2 = float(np.max(np.abs(JJ["p"] - JJ["m"]))) + print(f" sleeve 50/50 vs src.portfolio.sleeves._tp01_returns(): n={len(JJ)} max|diff| = {d2:.1e}") + ok_all &= (d2 == 0.0) + print(f" ==> REPLICA {'OK (3/3 a 0.0)' if ok_all else 'FALLITA β€” NON procedere'}") + if not ok_all: + sys.exit(1) + + # ------------------------------------------------------------------ 2. CONTROLLI POSITIVI + hr("2. CONTROLLI POSITIVI DEI GATE (un gate che non segnala puo' essere rotto)") + + def tgt_fn(floor): + return lambda df: tp_floor(floor).target_series(df) + + def leaky(df): # guarda il futuro: DEVE fallire causality_ok + t = tp_floor(-1.0).target_series(df) + out = np.zeros_like(t); out[:-1] = t[1:] + return out + + c_ok = al.causality_ok(tgt_fn(-1.0), tf="1d") + c_bad = al.causality_ok(leaky, tf="1d") + print(f" causality_ok(floor=-1) -> ok={c_ok['ok']} max_tail_diff={c_ok['max_tail_diff']}") + print(f" causality_ok(variante LEAKY) -> ok={c_bad['ok']} max_tail_diff={c_bad['max_tail_diff']}" + f" [controllo positivo: deve essere False]") + ctrl1 = c_ok["ok"] and not c_bad["ok"] + + rng = np.random.default_rng(20260823) + idx = pd.date_range("2020-01-01", periods=900, freq="1D", tz="UTC") + fake_good = pd.Series(np.abs(rng.normal(0.002, 0.0005, 900)), index=idx) # 0 perdite + fake_noise = pd.Series(rng.normal(0.0004, 0.012, 900), index=idx) + ip_g = al.implausible_sharpe(fake_good) + ip_n = al.implausible_sharpe(fake_noise) + print(f" implausible_sharpe(serie senza perdite) -> implausible={ip_g['implausible']} " + f"[deve essere True] (rumore Sh {ip_n['sharpe']:.2f} -> {ip_n['implausible']} [deve essere False])") + ctrl2 = ip_g["implausible"] and not ip_n["implausible"] + + base0 = sleeve(0.0, 0) + lev13 = base0 * 1.3 # LEVA PURA: il gate iso-vol deve bocciarla + k_lev = vol(base0) / vol(lev13) + print(f" GATE ISO-VOL, controllo positivo su LEVA PURA (canonico x1.3):") + print(f" grezzo dCAGR = {cagr(lev13) - cagr(base0):+.4f} (positivo: sembra 'piu' drift')") + print(f" iso-vol dCAGR = {cagr(lev13 * k_lev) - cagr(base0):+.6f} [deve essere ~0 -> e' leva]") + ctrl3 = abs(cagr(lev13 * k_lev) - cagr(base0)) < 1e-6 + + ad_self = al.anchor_luck_delta(lambda o: sleeve(0.0, o), lambda o: sleeve(0.0, o), OFFSETS, metric=sh) + print(f" anchor_luck_delta(A vs A) -> mediana {ad_self['median_paired']:+.6f}, " + f"positivi {ad_self['n_positive']}/{ad_self['n_anchors']} [deve essere 0 e 0/24]") + ctrl4 = (ad_self["median_paired"] == 0.0) and (ad_self["n_positive"] == 0) + print(f" ==> CONTROLLI POSITIVI: {sum([ctrl1, ctrl2, ctrl3, ctrl4])}/4 superati") + + # ------------------------------------------------------------------ 3. TABELLA PRINCIPALE + hr("3. TABELLA PRINCIPALE (ancora canonica h=0, fee 0.10% RT, 50/50 BTC+ETH)") + print(f" {'floor':<16} {'ShFULL':>7} {'ShHOLD':>7} {'CAGR':>8} {'drift':>8} {'vol':>7} " + f"{'maxDD':>7} {'turn/a':>7} {'%long':>6} {'%short':>7}") + rows = {} + for f in FLOORS: + s = sleeve(f, 0) + r = row(s); rows[f] = r + tl, ts = time_in_market(f) + print(f" {FLOOR_LBL[f]:<16} {r['sh']:>7.3f} {r['sh_hold']:>7.3f} {r['cagr']:>7.2%} " + f"{r['drift']:>7.2%} {r['vol']:>6.2%} {r['dd']:>6.2%} {turnover_year(f):>7.2f} " + f"{tl:>5.1%} {ts:>6.1%}") + print(f" (n barre = {rows[0.0]['n']}, dal {base0.index[0].date()} al {base0.index[-1].date()})") + + # ------------------------------------------------------------------ 4. GATE ISO-VOL + hr("4. GATE ISO-VOLATILITA' (null del de-levering AL CONTRARIO: la variante ALZA la vol)") + print(" Convenzione: si DE-LEVA la variante fino alla vol del canonico (k<1, gratis: nessuna") + print(" leva richiesta). Se a pari vol il canonico regge, la short era leva travestita.") + print(f" {'floor':<16} {'k de-lev':>9} {'CAGR iso':>9} {'dCAGR iso':>10} {'dSharpe':>9} " + f"{'maxDD iso':>10} {'esito':>10}") + isovol = {} + for f in FLOORS: + s = sleeve(f, 0) + k = vol(base0) / vol(s) if vol(s) > 0 else 1.0 + si = s * k + dc = cagr(si) - cagr(base0) + isovol[f] = dict(k=k, dcagr=dc, dsh=sh(s) - sh(base0), dd=dd(si)) + v = "β€”" if f == 0.0 else ("PASS" if dc > 0 else "FAIL") + print(f" {FLOOR_LBL[f]:<16} {k:>9.4f} {cagr(si):>8.2%} {dc:>+10.2%} " + f"{sh(s) - sh(base0):>+9.3f} {dd(si):>9.2%} {v:>10}") + + # ------------------------------------------------------------------ 5. BANDA D'ANCORA + hr("5. BANDA D'ANCORA β€” 24 ore, MEDIANA DELLE DIFFERENZE APPAIATE (variante - canonico)") + print(" (un Delta misurato a una sola ancora eredita la fortuna di quell'ancora: regola 26/07)") + band = {} + for f in FLOORS[1:]: + d_sh = al.anchor_luck_delta(lambda o, f=f: sleeve(f, o), lambda o: sleeve(0.0, o), OFFSETS, metric=sh) + d_ho = al.anchor_luck_delta(lambda o, f=f: hold(sleeve(f, o)), lambda o: hold(sleeve(0.0, o)), + OFFSETS, metric=sh) + d_dr = al.anchor_luck_delta(lambda o, f=f: sleeve(f, o), lambda o: sleeve(0.0, o), OFFSETS, metric=drift) + d_dd = al.anchor_luck_delta(lambda o, f=f: sleeve(f, o), lambda o: sleeve(0.0, o), OFFSETS, metric=dd) + band[f] = dict(sh=d_sh, ho=d_ho, dr=d_dr, dd=d_dd) + print(f" floor {FLOOR_LBL[f]:<14} dShFULL {d_sh['median_paired']:+.3f} " + f"[{d_sh['lo']:+.3f},{d_sh['hi']:+.3f}] {d_sh['n_positive']:>2}/24 | " + f"dShHOLD {d_ho['median_paired']:+.3f} {d_ho['n_positive']:>2}/24 | " + f"dDRIFT {d_dr['median_paired']:+.2%} {d_dr['n_positive']:>2}/24 | " + f"dMAXDD {d_dd['median_paired']:+.2%} {d_dd['n_positive']:>2}/24") + print("\n ⚠️ L'UNICA metrica che va nel verso della short e' l'HOLD-OUT, e all'ancora") + print(" CANONICA non si vede: e' il caso speculare della lezione 26/07 (li' l'ancora") + print(" canonica INVENTAVA un danno, qui NASCONDE un vantaggio).") + print(f" {'floor':<16} {'dShHOLD @h=0':>13} {'mediana 24':>11} {'p10':>8} {'p90':>8}") + for f in FLOORS[1:]: + v = np.array(list(band[f]['ho']['per_offset'].values()), float) + print(f" {FLOOR_LBL[f]:<16} {band[f]['ho']['per_offset'][0]:>+13.3f} " + f"{np.median(v):>+11.3f} {np.percentile(v, 10):>+8.3f} {np.percentile(v, 90):>+8.3f}") + b_can = al.anchor_luck_band(lambda o: sleeve(0.0, o), OFFSETS, metric=sh) + b_ls = al.anchor_luck_band(lambda o: sleeve(-1.0, o), OFFSETS, metric=sh) + print(f" livelli: canonico ShFULL canonica {b_can['canonical']:.3f} (pctl {b_can['canonical_pctl']:.2f}) " + f"mediana {b_can['median']:.3f} | LS pieno canonica {b_ls['canonical']:.3f} " + f"(pctl {b_ls['canonical_pctl']:.2f}) mediana {b_ls['median']:.3f}") + + # ------------------------------------------------------------------ 6. PER ANNO / SENZA 2022 + hr("6. SCOMPOSIZIONE PER ANNO β€” e la domanda che decide: E SENZA IL 2022?") + years = sorted(set(base0.index.year)) + print(f" Sharpe per anno (ancora h=0)") + print(f" {'anno':<6}" + "".join(f"{FLOOR_LBL[f]:>18}" for f in FLOORS)) + for y in years: + line = f" {y:<6}" + for f in FLOORS: + s = sleeve(f, 0); sy = s[s.index.year == y] + line += f"{sh(sy):>18.2f}" + print(line) + print(f"\n Rendimento per anno") + print(f" {'anno':<6}" + "".join(f"{FLOOR_LBL[f]:>18}" for f in FLOORS)) + for y in years: + line = f" {y:<6}" + for f in FLOORS: + s = sleeve(f, 0); sy = s[s.index.year == y] + line += f"{float(np.prod(1 + sy.values) - 1):>17.1%} " + print(line) + print(f"\n dSharpe ANNUALE della variante vs canonico (positivo = la short paga QUELL'anno)") + print(f" {'anno':<6}" + "".join(f"{FLOOR_LBL[f]:>18}" for f in FLOORS[1:])) + npos = {f: 0 for f in FLOORS[1:]} + for y in years: + line = f" {y:<6}" + s0 = sleeve(0.0, 0) + for f in FLOORS[1:]: + s = sleeve(f, 0) + d = sh(s[s.index.year == y]) - sh(s0[s0.index.year == y]) + npos[f] += int(d > 0) + line += f"{d:>+18.2f}" + print(line) + print(" anni positivi: " + " ".join(f"{FLOOR_LBL[f]} {npos[f]}/{len(years)}" for f in FLOORS[1:])) + + print(f"\n --- SENZA IL 2022 (ancora h=0) ---") + print(f" {'floor':<16} {'ShFULL':>7} {'dSh':>7} {'CAGR':>8} {'dCAGR':>8} {'iso-vol dCAGR':>14}") + b0n = no2022(base0) + for f in FLOORS: + sn = no2022(sleeve(f, 0)) + k = vol(b0n) / vol(sn) if vol(sn) > 0 else 1.0 + print(f" {FLOOR_LBL[f]:<16} {sh(sn):>7.3f} {sh(sn) - sh(b0n):>+7.3f} {cagr(sn):>7.2%} " + f"{cagr(sn) - cagr(b0n):>+7.2%} {cagr(sn * k) - cagr(b0n):>+14.2%}") + + print(f"\n --- BANDA D'ANCORA SENZA IL 2022 (mediana differenze appaiate) ---") + for f in FLOORS[1:]: + d_sh = al.anchor_luck_delta(lambda o, f=f: no2022(sleeve(f, o)), + lambda o: no2022(sleeve(0.0, o)), OFFSETS, metric=sh) + d_dr = al.anchor_luck_delta(lambda o, f=f: no2022(sleeve(f, o)), + lambda o: no2022(sleeve(0.0, o)), OFFSETS, metric=drift) + print(f" floor {FLOOR_LBL[f]:<14} dShFULL {d_sh['median_paired']:+.3f} " + f"[{d_sh['lo']:+.3f},{d_sh['hi']:+.3f}] {d_sh['n_positive']:>2}/24 | " + f"dDRIFT {d_dr['median_paired']:+.2%} {d_dr['n_positive']:>2}/24") + + # ------------------------------------------------------------------ 7. PROTEZIONE DD + hr("7. LA PROTEZIONE β€” il valore DICHIARATO di TP01 e' il taglio del DD: la short lo rompe?") + bh = {} + for a in ASSETS: + df = bars(a, 0) + bh[a] = pd.Series(simple_returns(df["close"].values.astype(float)), + index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))) + JB = pd.concat(bh, axis=1, join="inner").fillna(0.0) + bh50 = pd.Series(0.5 * JB[ASSETS[0]].values + 0.5 * JB[ASSETS[1]].values, index=JB.index) + bh50 = bh50.reindex(base0.index).fillna(0.0) + print(f" buy&hold 50/50: Sharpe {sh(bh50):.2f} CAGR {cagr(bh50):.1%} maxDD {dd(bh50):.1%}") + print(f" Criterio (B) di edge_watch: in un anno con DD del buy&hold > 10%, il DD dello sleeve") + print(f" deve restare sotto il 75% di quello. Rapporto DD_sleeve/DD_B&H per anno di sinistro:") + print(f" {'anno':<6} {'DD B&H':>8}" + "".join(f"{FLOOR_LBL[f]:>18}" for f in FLOORS)) + fails = {f: 0 for f in FLOORS}; sinistri = 0 + for y in years: + by = bh50[bh50.index.year == y] + if dd(by) <= 0.10: + continue + sinistri += 1 + line = f" {y:<6} {dd(by):>7.1%}" + for f in FLOORS: + s = sleeve(f, 0); r_ = dd(s[s.index.year == y]) / dd(by) + fails[f] += int(r_ >= 0.75) + line += f"{r_:>17.2f}x" if False else f"{r_:>18.2f}" + print(line) + print(f" anni di sinistro: {sinistri}. Violazioni del criterio (rapporto >= 0.75): " + + " ".join(f"{FLOOR_LBL[f]} {fails[f]}" for f in FLOORS)) + print(f" maxDD full: " + " ".join(f"{FLOOR_LBL[f]} {rows[f]['dd']:.2%}" for f in FLOORS)) + + # ------------------------------------------------------------------ 8. COSTI + hr("8. COSTI β€” la short raddoppia il turnover? curva fee 0-20 bps/lato") + print(f" {'fee/lato':>9} {'%RT':>7}" + "".join(f"{FLOOR_LBL[f]:>18}" for f in FLOORS)) + for bps in (0, 2.5, 5, 10, 15, 20): + line = f" {bps:>8.1f}b {bps * 2 / 100:>6.2f}%" + for f in FLOORS: + line += f"{sh(sleeve(f, 0, bps / 10000.0)):>18.3f}" + print(line) + print(" turnover/anno (media asset): " + " ".join(f"{FLOOR_LBL[f]} {turnover_year(f):.2f}" for f in FLOORS)) + + # ------------------------------------------------------------------ 9. LIBRO 75/25 + SKH01 + hr("9. IMPATTO SUL LIBRO 75/25 CON SKH01 (che e' GIA' long/short: ridondanza?)") + from src.portfolio.sleeves import _skyhook_returns + skh = _skyhook_returns() + skh.index = pd.DatetimeIndex(skh.index).tz_convert("UTC") if skh.index.tz else \ + pd.DatetimeIndex(skh.index).tz_localize("UTC") + + def book(floor: float, off: int = 0) -> pd.Series: + s = sleeve(floor, off).copy() + s.index = s.index.floor("1D") # ancora h -> data di calendario (1 barra per data) + J = pd.concat({"t": s, "k": skh}, axis=1, join="inner").dropna() + return pd.Series(W_TP01_BOOK * J["t"].values + W_SKH_BOOK * J["k"].values, index=J.index) + + print(f" {'floor':<16} {'ShFULL':>7} {'ShHOLD':>7} {'CAGR':>8} {'maxDD':>7}") + for f in FLOORS: + b = book(f) + print(f" {FLOOR_LBL[f]:<16} {sh(b):>7.3f} {sh(hold(b)):>7.3f} {cagr(b):>7.2%} {dd(b):>6.2%}") + print(f" banda d'ancora del LIBRO (24 ancore su TP01, SKH01 all'ancora canonica):") + for f in FLOORS[1:]: + d_b = al.anchor_luck_delta(lambda o, f=f: book(f, o), lambda o: book(0.0, o), OFFSETS, metric=sh) + d_c = al.anchor_luck_delta(lambda o, f=f: book(f, o), lambda o: book(0.0, o), OFFSETS, metric=drift) + print(f" floor {FLOOR_LBL[f]:<14} dSh libro {d_b['median_paired']:+.3f} " + f"[{d_b['lo']:+.3f},{d_b['hi']:+.3f}] {d_b['n_positive']:>2}/24 | " + f"dDRIFT {d_c['median_paired']:+.2%} {d_c['n_positive']:>2}/24") + + print(f"\n RIDONDANZA con SKH01 β€” la 'gamba short' isolata = variante meno canonico:") + for f in FLOORS[1:]: + leg = (sleeve(f, 0) - sleeve(0.0, 0)) + leg.index = leg.index.floor("1D") + JL = pd.concat({"l": leg, "k": skh}, axis=1, join="inner").dropna() + print(f" floor {FLOOR_LBL[f]:<14} corr(gamba short, SKH01) = {JL['l'].corr(JL['k']):+.3f} " + f"Sharpe della gamba isolata {sh(JL['l']):+.3f} drift {drift(JL['l']):+.2%}") + + # ------------------------------------------------------------------ 10. MARGINALE + DSR + hr("10. MARGINALE vs TP01 STESSO + DEFLATED SHARPE (4 trial dichiarati)") + cd0 = al.candidate_daily(tgt_fn(0.0), tf="1d") + bl = al.tp01_baseline_daily() + JC = pd.concat({"a": cd0, "b": bl}, axis=1, join="inner").dropna() + print(f" 4a replica: candidate_daily(floor=0) vs tp01_baseline_daily() max|diff| " + f"{float(np.max(np.abs(JC['a'] - JC['b']))):.1e} (n={len(JC)})") + # trial set del DSR: si prende dal percorso CODIFICATO (select_cell_insample), non a mano. + # ⚠️ errore mio catturato: la prima stesura costruiva la lista dei trial da solo. Due percorsi + # per lo stesso numero e' esattamente cio' che il gate codificato serve a evitare -> qui li + # confronto invece di sostituirne uno in silenzio. + sel = al.select_cell_insample(lambda tf, floor: tgt_fn(floor), + [dict(floor=f) for f in FLOORS], ("1d",)) + all_sh = sel["all_full_sharpe"] + mine_sh = [round(sh(sleeve(f, 0)), 3) for f in FLOORS] + print(f" trial set DSR da select_cell_insample: {all_sh}") + print(f" stesso conto per via indipendente (sleeve): {mine_sh} -> " + f"{'COINCIDONO' if all_sh == mine_sh else 'DIVERGONO (fermarsi)'}") + for f in FLOORS[1:]: + cd = al.candidate_daily(tgt_fn(f), tf="1d") + m = al.marginal_vs_tp01(cd) + d, sr0 = al.deflated_sharpe(sh(sleeve(f, 0)), all_sh, sleeve(f, 0)) + ip = al.implausible_sharpe(sleeve(f, 0)) + print(f"\n floor {FLOOR_LBL[f]}: marginal_verdict = {m['marginal_verdict']}") + print(f" corr->TP01 {m['corr_full']} beta {m['beta_to_tp01']} alpha/a {m['alpha_ann']:+.4f} " + f"resid Sharpe {m['resid_sharpe_full']}") + print(f" blend w25 uplift full {m['blends']['w25']['uplift_full']:+.3f} / hold " + f"{m['blends']['w25']['uplift_hold']:+.3f} w50 full " + f"{m['blends']['w50']['uplift_full']:+.3f} / hold {m['blends']['w50']['uplift_hold']:+.3f}") + print(f" robust_oos={m['robust_oos']} multicut={m['multicut_persistent']} " + f"insample_edge={m['has_insample_edge']} (Sh IS {m['cand_insample_sharpe']}) " + f"is_hedge={m['is_hedge']}") + print(f" HEDGE vs ALPHA: corr(Sharpe-TP01 annuo, uplift annuo) = {m['hedge_yearly_corr']} " + f"uplift quando TP01 SALE {m['uplift_tp01_up']} / quando TP01 SCENDE {m['uplift_tp01_down']}") + print(f" multicut per anno: {m['multicut_uplift']}") + print(f" deflated-Sharpe {d:.3f} (sr0 nullo {sr0:.3f}, N={len(all_sh)}) " + f"implausible={ip['implausible']}") + print(f"\n --- GATE SELECTION-ON-HOLDOUT (codificato 29/06): quale floor si sceglie ") + print(f" guardando SOLO il pre-2025? Se e' il canonico, il guadagno hold-out NON e'") + print(f" selezionabile e sceglierlo sarebbe selezione-sull'hold-out. ---") + print(f" {'floor':<16} {'Sh IN-SAMPLE (<2025)':>21} {'Sh HOLD-OUT':>12} {'Sh FULL':>9}") + for f in FLOORS: + s_ = sleeve(f, 0) + print(f" {FLOOR_LBL[f]:<16} {sh(s_[s_.index < HOLDOUT]):>21.3f} {sh(hold(s_)):>12.3f} {sh(s_):>9.3f}") + fb = sel["chosen"]["params"]["floor"] + fh = max(FLOORS, key=lambda f: sh(hold(sleeve(f, 0)))) + print(f" cella scelta AL BUIO (max Sharpe in-sample) = floor {fb:+.4f} -> " + f"{'IL CANONICO' if abs(fb) < 1e-9 else 'NON il canonico'}") + print(f" cella scelta SULL'HOLD-OUT (max Sharpe hold-out) = floor {fh:+.4f} -> " + f"{'diversa: firma di selezione-sull-hold-out' if abs(fh - fb) > 1e-9 else 'la stessa'}") + + print(f"\n ⚠️ NOTA ONESTA sul DSR: con 4 celle quasi identiche sr0 e' minuscolo -> il DSR e'") + print(f" alto per COSTRUZIONE (dipende dalla VARIANZA della griglia, lezione Β§10 del 22/08).") + print(f" Qui non e' il gate che decide: e' un contorno. Il gate che decide e' l'iso-vol +") + print(f" la banda d'ancora + il per-anno.") + + # ------------------------------------------------------------------ 11. ESEGUIBILITA' + hr(f"11. ESEGUIBILITA' A ${CAPITAL:.0f} (min_order $5) e vincolo di NETTING del libro") + for f in (0.0, -1.0): + for a in ASSETS: + df = bars(a, 0) + sc = al.eval_weights_smallcap(df, tp_floor(f).target_series(df), + capital=CAPITAL, min_order=5.0) + print(f" floor {FLOOR_LBL[f]:<14} {a}: Sharpe modellato {sc['modeled']['sharpe']:.3f} -> " + f"reale {sc['realistic']['sharpe']:.3f} haircut {sc['sharpe_haircut']:+.3f} " + f"trade eseguiti {sc['n_executed_trades']}") + src_book = (ROOT / "src" / "live" / "book.py").read_text() + clipped = "max(tp_frac, 0.0)" in src_book + print(f"\n VERIFICA SUL CODICE CHE GIRA: src/live/book.py contiene 'max(tp_frac, 0.0)' -> {clipped}") + print(f" Cioe' l'esecutore del libro CLIPPA a zero la frazione di TP01: il long-flat non e' solo") + print(f" in CANONICAL, e' cablato anche in `book_net_target`. Attivare la short richiederebbe") + print(f" di toccare src/live/book.py, cioe' CODICE su un percorso con soldi veri; e TP01 e SKH01") + print(f" si nettano su UNA sola posizione per strumento, quindi la short di TP01 andrebbe a") + print(f" sommarsi/cancellarsi con quella di SKH01 invece di essere una gamba a se'.") + + hr("12. VERDETTO E COSA NON E' STATO MISURATO") + dsh_ls = band[-1.0]["sh"]["median_paired"]; ddr_ls = band[-1.0]["dr"]["median_paired"] + print(f" floor=-1 (LS pieno): dShFULL appaiato {dsh_ls:+.3f} ({band[-1.0]['sh']['n_positive']}/24), " + f"dDRIFT {ddr_ls:+.2%} ({band[-1.0]['dr']['n_positive']}/24), " + f"dMAXDD {band[-1.0]['dd']['median_paired']:+.2%} ({band[-1.0]['dd']['n_positive']}/24)") + print(f" iso-vol dCAGR {isovol[-1.0]['dcagr']:+.2%} | anni positivi {npos[-1.0]}/{len(years)} | " + f"cella scelta al buio = il canonico") + print(f"\n >>> VERDETTO: SCARTATO. La gamba short NON paga il proprio costo: toglie drift") + print(f" ({ddr_ls:+.2%}/anno a 0/24 ancore), FALLISCE il gate iso-vol, peggiora il maxDD") + print(f" ({band[-1.0]['dd']['median_paired']:+.2%} a 24/24) e non e' selezionabile in-sample.") + print(f" L'attesa a priori del coordinatore (il guadagno e' il 2022) e' CONFERMATA e") + print(f" superata: il 2022 c'e' ({npos[-1.0]}/8 anni positivi, dSharpe 2022 +1.84) ma NON") + print(f" compensa gli altri sei nemmeno con se stesso dentro il campione.") + print(f" E NON e' morte-per-fee: a fee ZERO il canonico fa {sh(sleeve(0.0, 0, 0.0)):.3f} contro " + f"{sh(sleeve(-1.0, 0, 0.0)):.3f} -> l'edge lordo non c'e'.") + print(f"\n COSA NON HO MISURATO (dichiarato, non nascosto):") + print(f" (a) FUNDING. Nessun backtest del progetto lo modella (Β§35, 22/08) e qui nemmeno.") + print(f" E' l'unica voce che potrebbe muovere il numero nel verso della short: una") + print(f" posizione corta INCASSA il funding quando il tasso e' positivo. MA il") + print(f" meccanismo misurato il 22/08 e' che esposizione e funding sono POSITIVAMENTE") + print(f" correlati, e un trend-short entra quando il mercato scende, cioe' quando il") + print(f" funding e' tipicamente NEGATIVO (e li' e' lo short a pagare). Argomento, non") + print(f" misura: si chiude con la macchineria di r0822d_funding.py, non a parole.") + print(f" Ordine di grandezza in gioco (~1-2%/anno) contro un drift perso di " + f"{-ddr_ls:.2%}/anno.") + print(f" (b) TF diversi dal 1d. Riaprire il TF riapre la FAMIGLIA (griglia 4 -> 4xN celle)") + print(f" e imporrebbe study_family_honest; qui il TF e' congelato a quello di deploy.") + print(f" (c) La banda d'ancora del LIBRO tiene SKH01 alla sua ancora canonica (le 23 fasi") + print(f" di SKH01 non sono state incrociate): il delta di libro e' appaiato solo sulle") + print(f" 24 ancore di TP01. Non cambia il segno (0/24 su tutte e tre le celle).") + print(f" (d) La lente WICK accoppiata (25/07): tutto qui e' close-only. Irrilevante per il") + print(f" verdetto (non c'e' barriera di rovina in gioco), rilevante se qualcuno") + print(f" riproponesse la variante sul canale funded.") + print(f"\n Tempo totale {time.time() - t_start:.1f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_tp01_size.py b/scripts/research/r0823_tp01_size.py new file mode 100644 index 0000000..8dc0a99 --- /dev/null +++ b/scripts/research/r0823_tp01_size.py @@ -0,0 +1,762 @@ +#!/usr/bin/env python +"""r0823_tp01_size.py β€” Β§41 TP01-SIZE: una size che risponde alla CONVINZIONE paga? + +DOMANDA +------- +TP01 e' il 75% del libro live. Dichiara target_vol=20% e ne realizza 12,14%; la +scomposizione del 22/08 e' `20% x 0,682 x 0,890`, dove lo 0,682 sono due meta' quasi +uguali β€” (i) flat nel 44,3% delle barre, (ii) "convinzione parziale". Il fatto +trasferibile: alzare `target_vol` scala TUTTO uniformemente, quindi aumenta la size +proprio dove il segnale e' piu' debole. Nessuno ha mai misurato l'alternativa: +**una size che risponde alla convinzione invece che solo alla vol realizzata**. +Se esistesse, sarebbe un miglioramento k-INDIPENDENTE (ridistribuzione della stessa +size), non un aumento di leva. + +ATTESA A PRIORI (scritta PRIMA di qualunque misura, e prima del diagnostico Β§2/Β§3) +--------------------------------------------------------------------------------- +"Il canonico (size lineare nella convinzione) e' gia' dentro il suo plateau. Qualunque +rimodellamento dara' un delta indistinguibile da zero A ISO-VOL, e un eventuale +vincitore sara' (i) fortuna d'ancora, (ii) un anno solo, o (iii) bocciato dal +deflated-Sharpe. VERDETTO ATTESO: SCARTATO." +Corollario dichiarato: mi aspetto che il null del de-levering si presenti qui nella sua +forma SPECULARE (re-levering): una variante che alza la size media sembrera' guadagnare +drift, e a iso-vol il guadagno sparira'. +NB di trasparenza: il diagnostico per bucket (Β§3) e' stato girato PRIMA della griglia e +ne ha raffinato la DIREZIONE attesa (rho > 1/3). La FAMIGLIA era gia' dichiarata. + +FAMIGLIA DICHIARATA PRIMA DI GUARDARE β€” e perche' e' piu' piccola di quanto sembra +---------------------------------------------------------------------------------- +Il segnale resta CONGELATO (TSMOM sign-blend 30/90/180g, long-flat). Cambia solo la +mappa `convinzione -> size`: + target = clip( g_rho(conv) * (target_vol/vol)^q , -leverage, +leverage ) + * asse RHO = g(1/3)/g(1): quanto pesa il bucket debole rispetto al pieno. + rho in {0.00, 0.15, 0.25, 1/3, 0.45, 0.60, 0.80, 1.00, 1.50, 3.00} (10) + rho=1/3 = CANONICO; rho=0 = soglia (solo convinzione piena); rho=1 = binario + (size costante quando non-flat, cioe' convinzione RIMOSSA); rho=3 = controllo + speculare (sovrappesa il debole). + * asse Q = esponente del vol-targeting: quanto la size risponde alla VOL invece che + alla convinzione. q in {0.00, 0.25, 0.50, 0.75, 1.00, 1.25} (6) + q=1 = canonico; q=0 = nessun vol-targeting. + => GRIGLIA DICHIARATA = 10 x 6 = **60 celle**, TF=1d (il TF di deploy). Conteggio al + RIALZO: la sensibilita' del verdetto al numero di trial e' pubblicata in Β§7. + +FATTO STRUTTURALE (Β§2) β€” e una correzione alla premessa del mandato +------------------------------------------------------------------- +La convinzione di TP01 NON assume i valori {1/3, 2/3, 1}: la media di TRE segni in +{-1,0,+1} vale {-1, -1/3, 0, +1/3, +1}, quindi **il bucket 2/3 e' aritmeticamente +impossibile** e non esiste nei dati. Conseguenza metodologica: le tre +parametrizzazioni "ovvie" (potenza c^p, floor affine a+(1-a)c, soglia) sono tutte +REPARAMETRIZZAZIONI dello stesso unico knob rho β€” la famiglia della convinzione ha UN +grado di liberta', non tre. Dichiararle come tre assi avrebbe gonfiato i trial con +duplicati esatti (l'errore speculare a quello del 30/07: li' i trial erano contati al +ribasso, qui si sarebbero contati al rialzo ma con celle identiche). + +GATE ISO-VOLATILITA' (obbligatorio, e qui e' ESATTO) +---------------------------------------------------- +Ogni variante e' confrontata a PARI vol realizzata di sleeve riscalando con un k +costante. Poiche' net = pos*r - fee*|dpos| e' OMOGENEO di grado 1 in pos, `net_iso = k*net` +e' ESATTO => lo **Sharpe e' invariante a k** e il gate morde sul **drift**: qualunque +guadagno di CAGR che sparisce a iso-vol e' re-levering, non edge. k e' calibrato IN-SAMPLE +(pre-2025) per la lettura hold-out: nessun look-ahead nemmeno nella normalizzazione. +Il cap di leva 2x e' misurato cella per cella (Β§4): morde su <=0.15% delle barre nelle 54 +celle candidate e solo nel controllo anti rho=3 (24.7%), quindi non e' il cap a decidere. + +CONTROLLI POSITIVI (un gate che non segnala nulla puo' essere rotto) +-------------------------------------------------------------------- + CTRL-DELEV : canonico x 0.5 (pura riscalatura) -> dSharpe ESATTAMENTE 0, + dCAGR grezzo NEGATIVO, dCAGR a iso-vol ESATTAMENTE 0. + CTRL-ORACLE : convinzione = segno del rendimento del giorno DOPO (look-ahead) -> + Sharpe deve esplodere E `causality_ok` deve dare ok=False. + CTRL-ANTI : rho=3.0 (sovrappesa il bucket debole) -> deve perdere. + +CAUSALITA' / ONESTA' +-------------------- +`causality_ok` sulla cella scelta; selezione della cella IN-SAMPLE-ONLY +(`select_cell_insample`, importata); `deflated_sharpe`; `marginal_vs_tp01`; +banda d'ancora a 24 offset con **mediana delle DIFFERENZE APPAIATE** e frazione di +ancore positive; scomposizione per ANNO; impatto sul LIBRO (TP01 75 / SKH01 25); +eseguibilita' a $635 con `eval_weights_smallcap` (min_order $5). + +Vincoli operativi: nessun file toccato fuori da questo script; nessuna rete (solo i +parquet certificati su disco); nessun ordine. Runtime misurato ~9 s su 2 core. +""" +from __future__ import annotations + +import sys +import time +from functools import lru_cache +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT)) + +import altlib as al # noqa: E402 +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, realized_vol, resample_1d, simple_returns, tsmom_blend) + +ASSETS = ("BTC", "ETH") +HOLDOUT = al.HOLDOUT # 2025-01-01 UTC +TP = TrendPortfolio(**CANONICAL) +FEE = TP.fee_side # 0.05%/lato = 0.10% RT +TV = TP.target_vol # 0.20 +LEV = TP.leverage # 2.0 +HORIZONS = TP.horizons_days # (30, 90, 180) +VOLWIN = TP.vol_win_days # 30 +OFFSETS = tuple(range(24)) # le 24 ancore orarie del ribilanciamento daily + +RHOS = (0.00, 0.15, 0.25, 1.0 / 3.0, 0.45, 0.60, 0.80, 1.00, 1.50, 3.00) +QS = (0.00, 0.25, 0.50, 0.75, 1.00, 1.25) +CANON_CELL = (1.0 / 3.0, 1.00) +N_CELLS = len(RHOS) * len(QS) + +SEP = "=" * 100 + + +# --------------------------------------------------------------------------- +# 0. Resample ancorato (identico a trend_portfolio.resample_tf, con offset h) +# --------------------------------------------------------------------------- +def resample_offset(df_1h: pd.DataFrame, h: int) -> pd.DataFrame: + g = df_1h.copy() + idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True) + idx.name = "dt" + g.index = idx + out = g.resample("24h", offset=pd.Timedelta(hours=h), label="left", closed="left").agg( + {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}) + out = out.dropna(subset=["open"]) + out["datetime"] = out.index + epoch = pd.Timestamp("1970-01-01", tz="UTC") + out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64") + return out.reset_index(drop=True)[ + ["timestamp", "open", "high", "low", "close", "volume", "datetime"]] + + +@lru_cache(maxsize=4) +def _raw1h(asset: str) -> pd.DataFrame: + return load_data(asset, "1h") + + +@lru_cache(maxsize=64) +def _daily(asset: str, h: int) -> pd.DataFrame: + return resample_offset(_raw1h(asset), h) + + +@lru_cache(maxsize=64) +def _feat(asset: str, h: int) -> dict: + """Feature CAUSALI per (asset, ancora): rendimenti, vol realizzata, convinzione.""" + df = _daily(asset, h) + c = df["close"].values.astype(float) + dt = pd.to_datetime(df["datetime"]).diff().dt.total_seconds().median() + bpd = max(1, round(86400 / dt)) # =1 su barre daily + bpy = bpd * 365.25 + r = simple_returns(c) + vol = realized_vol(r, VOLWIN * bpd, bpy) + conv = np.clip(tsmom_blend(c, tuple(d * bpd for d in HORIZONS)), 0, None) + return dict(idx=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)), + r=r, vol=vol, conv=conv, close=c, n=len(c)) + + +# --------------------------------------------------------------------------- +# 1. La mappa convinzione -> size (la SOLA cosa che cambia) +# --------------------------------------------------------------------------- +def g_rho(conv: np.ndarray, rho: float) -> np.ndarray: + """Peso della convinzione: g(1)=1, g(1/3)=rho, g(<=0)=0. + Interpolazione lineare fra i due livelli (esatta sul supporto reale {0,1/3,1}); + definita per continuita' anche fuori, cosi' la funzione non e' legata al supporto.""" + out = rho + (1.0 - rho) * (conv - 1.0 / 3.0) / (1.0 - 1.0 / 3.0) + return np.where(conv > 1e-12, np.clip(out, 0.0, None), 0.0) + + +def target_from_feat(f: dict, rho: float, q: float) -> np.ndarray: + vol = f["vol"] + ok = (vol > 0) & np.isfinite(vol) + scal = np.zeros(len(vol)) + scal[ok] = (TV / vol[ok]) ** q + tgt = np.clip(g_rho(f["conv"], rho) * scal, -LEV, LEV) + tgt[~np.isfinite(tgt)] = 0.0 + return tgt + + +def make_target_fn(rho: float, q: float): + """target_fn(df) autonomo (per altlib: causality_ok / study_family_honest).""" + def fn(df: pd.DataFrame) -> np.ndarray: + c = df["close"].values.astype(float) + dt = pd.to_datetime(df["datetime"]).diff().dt.total_seconds().median() + bpd = max(1, round(86400 / dt)) + bpy = bpd * 365.25 + r = simple_returns(c) + vol = realized_vol(r, VOLWIN * bpd, bpy) + conv = np.clip(tsmom_blend(c, tuple(d * bpd for d in HORIZONS)), 0, None) + return target_from_feat(dict(vol=vol, conv=conv), rho, q) + return fn + + +def net_from_target(f: dict, tgt: np.ndarray) -> np.ndarray: + """Rendimenti netti per barra. La posizione decisa a close[i] e' TENUTA in i+1.""" + pos = np.zeros(len(tgt)) + pos[1:] = tgt[:-1] + turn = np.abs(np.diff(pos, prepend=0.0)) + net = pos * f["r"] - FEE * turn + net[0] = 0.0 + return net + + +def sleeve_series(rho: float, q: float, h: int) -> tuple[pd.Series, dict]: + """Serie 50/50 BTC+ETH del sleeve all'ancora h, piu' diagnostica di cap/size.""" + parts, diag = {}, {} + for a in ASSETS: + f = _feat(a, h) + tgt = target_from_feat(f, rho, q) + parts[a] = pd.Series(np.clip(net_from_target(f, tgt), -0.99, None), index=f["idx"]) + diag[a] = dict(cap_frac=float(np.mean(np.abs(tgt) >= LEV - 1e-12)), + max_tgt=float(np.max(np.abs(tgt))), + mean_tgt=float(np.mean(tgt))) + J = pd.concat(parts, axis=1, join="inner").fillna(0.0) + s = pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index) + return s, diag + + +# --------------------------------------------------------------------------- +# 2. Metriche (convenzione identica al progetto) +# --------------------------------------------------------------------------- +def sh(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(np.mean(r) / np.std(r) * np.sqrt(365.25)) if len(r) > 2 and np.std(r) > 0 else 0.0 + + +def vol_ann(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(np.std(r) * np.sqrt(365.25)) + + +def cagr(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + if len(r) < 2: + return 0.0 + eq = np.cumprod(1.0 + np.clip(r, -0.99, None)) + yrs = len(r) / 365.25 + return float(eq[-1] ** (1.0 / yrs) - 1.0) + + +def maxdd(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + eq = np.cumprod(1.0 + np.clip(r, -0.99, None)) + pk = np.maximum.accumulate(eq) + return float(np.max((pk - eq) / pk)) if len(eq) else 0.0 + + +def split(s: pd.Series) -> tuple[pd.Series, pd.Series]: + return s[s.index < HOLDOUT], s[s.index >= HOLDOUT] + + +# --------------------------------------------------------------------------- +def main() -> None: + t_start = time.time() + print(SEP) + print("Β§41 TP01-SIZE β€” size conviction-aware vs canonico. Griglia dichiarata: " + f"{len(RHOS)} rho x {len(QS)} q = {N_CELLS} celle, TF=1d, 24 ancore.") + print(SEP) + + # ===================================================================== + # Β§1 REPLICA BIT-EXACT (prima di ogni delta) + # ===================================================================== + print("\n### Β§1 REPLICA BIT-EXACT DEL CANONICO") + import src.portfolio.sleeves as SL + prod = SL._tp01_returns() + + # (a) livello TARGET: la mia cella canonica == TrendPortfolio.target_series + worst_tgt = 0.0 + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + mine = make_target_fn(*CANON_CELL)(df) + worst_tgt = max(worst_tgt, float(np.max(np.abs(mine - TP.target_series(df))))) + print(f" (a) target_series max|diff| = {worst_tgt:.17g}") + + # (b) livello RENDIMENTI: la mia pipeline all'ancora 0 == sleeves._tp01_returns() + can0, diag0 = sleeve_series(*CANON_CELL, 0) + aligned = can0.reindex(prod.index) + worst_ret = float(np.max(np.abs(aligned.values - prod.values))) + print(f" (b) sleeve returns max|diff| = {worst_ret:.17g} (n={len(prod)}, " + f"{prod.index[0].date()} -> {prod.index[-1].date()})") + + # (c) contro il baseline indipendente di altlib β€” e la sua NON bit-esattezza, spiegata + base = al.tp01_baseline_daily() + j = pd.concat({"m": can0, "b": base}, axis=1, join="inner").dropna() + worst_al = float(np.max(np.abs(j["m"].values - j["b"].values))) + n_diff = int((j["m"].values != j["b"].values).sum()) + roundtrip = (1.0 + j["m"].values) - 1.0 + is_rt = bool(np.all(roundtrip == j["b"].values)) + print(f" (c) altlib baseline max|diff| = {worst_al:.17g} " + f"(diverso su {n_diff}/{len(j)} giorni = {n_diff/len(j)*100:.1f}%)") + print(f" MECCANISMO PROVATO: altlib._to_daily fa `(1+r).resample('1D').prod()-1`, che su una") + print(f" serie GIA' giornaliera e' un no-op algebrico ma NON in virgola mobile. Identita'") + print(f" `(1+r)-1 == baseline` esatta su tutti i giorni: {is_rt} -> e' 1 ulp, non un difetto.") + ok_replica = (max(worst_tgt, worst_ret) == 0.0) and is_rt and worst_al < 1e-15 + print(f" => REPLICA {'BIT-EXACT OK sui due percorsi di produzione (a,b); (c) a 1 ulp, spiegato' if ok_replica else 'FALLITA β€” mi fermo qui'}") + if not ok_replica: + print(" La replica non e' bit-exact: e' questo il risultato. Nessun delta e' credibile.") + return + print(f" canonico ancora 0: Sharpe FULL {sh(can0):.4f} | HOLD {sh(split(can0)[1]):.4f} | " + f"vol {vol_ann(can0)*100:.2f}% | CAGR {cagr(can0)*100:.2f}% | maxDD {maxdd(can0)*100:.2f}%") + + # ===================================================================== + # Β§2 FATTO STRUTTURALE: il supporto della convinzione + # ===================================================================== + print("\n### Β§2 SUPPORTO DELLA CONVINZIONE (correzione alla premessa del mandato)") + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + raw = tsmom_blend(df["close"].values.astype(float), HORIZONS) + u, cnt = np.unique(np.round(raw, 9), return_counts=True) + print(f" {a} direzione GREZZA: " + + " ".join(f"{v:+.4f}:{c}" for v, c in zip(u.tolist(), cnt.tolist()))) + f = _feat(a, 0) + uu, cc = np.unique(np.round(f["conv"], 9), return_counts=True) + print(f" {a} convinzione (clip>=0): " + + " ".join(f"{v:.4f}:{c/len(f['conv'])*100:.1f}%" for v, c in zip(uu.tolist(), cc.tolist()))) + print(" => il bucket 2/3 NON esiste (media di 3 segni in {-1,0,+1} => {-1,-1/3,0,+1/3,+1}).") + print(" La famiglia della convinzione ha UN grado di liberta' (rho), non tre.") + + # ===================================================================== + # Β§3 DIAGNOSTICO: la convinzione e' informativa? (descrittivo, non selezione) + # ===================================================================== + print("\n### Β§3 DIAGNOSTICO PER BUCKET (rendimento del giorno TENUTO dopo il segnale)") + print(f" {'asset':5s} {'fin.':5s} {'bucket':7s} {'n':>5s} {'mean(bps)':>10s} {'sd(%)':>7s} " + f"{'Sharpe':>7s} peso canonico") + for a in ASSETS: + f = _feat(a, 0) + fwd = np.zeros(f["n"]) + fwd[:-1] = f["r"][1:] + for lab, m0 in (("FULL", np.ones(f["n"], bool)), + ("IS", np.asarray(f["idx"] < HOLDOUT)), + ("HOLD", np.asarray(f["idx"] >= HOLDOUT))): + for lvl, nm in ((1.0 / 3.0, "debole"), (1.0, "pieno")): + m = m0 & (np.abs(f["conv"] - lvl) < 1e-9) + m[-1] = False + x = fwd[m] + if len(x) < 10: + continue + print(f" {a:5s} {lab:5s} {nm:7s} {len(x):5d} {np.mean(x)*1e4:10.1f} " + f"{np.std(x)*100:7.2f} {np.mean(x)/np.std(x)*np.sqrt(365.25):7.2f}" + f" x{lvl:.3f}") + print(" (descrittivo: dice se il canonico sotto/sovra-pesa il bucket debole. NON e' selezione.)") + + # ===================================================================== + # Β§4 GRIGLIA 60 CELLE ALL'ANCORA CANONICA + GATE ISO-VOL + # ===================================================================== + print("\n### Β§4 GRIGLIA 60 CELLE (ancora 0) β€” grezzo vs ISO-VOL") + can_is, _ = split(can0) + vol_is_ref = vol_ann(can_is) # vol in-sample del canonico = bersaglio + rows = [] + cap_worst = 0.0 + for rho in RHOS: + for q in QS: + s, dg = sleeve_series(rho, q, 0) + cap_worst = max(cap_worst, max(d["cap_frac"] for d in dg.values())) + s_is, s_ho = split(s) + v_is = vol_ann(s_is) + k = vol_is_ref / v_is if v_is > 0 else 0.0 # k CALIBRATO IN-SAMPLE (no look-ahead) + iso = k * s + rows.append(dict(rho=rho, q=q, + sh_full=sh(s), sh_is=sh(s_is), sh_hold=sh(s_ho), + vol=vol_ann(s), vol_is=v_is, k=k, + cagr_raw=cagr(s), cagr_iso=cagr(iso), + dd_raw=maxdd(s), dd_iso=maxdd(iso), + sh_iso=sh(iso), cap_max=max(d["cap_frac"] for d in dg.values()), + max_tgt=max(d["max_tgt"] for d in dg.values()), + mean_tgt=float(np.mean([d["mean_tgt"] for d in dg.values()])))) + G = pd.DataFrame(rows) + canrow = G[(np.abs(G.rho - CANON_CELL[0]) < 1e-9) & (np.abs(G.q - CANON_CELL[1]) < 1e-9)].iloc[0] + cap_reg = G[G.rho <= 1.5].cap_max.max() + cap_anti = G[G.rho > 1.5].cap_max.max() + print(f" cap di leva ({LEV}x): frazione max di barre al cap = {cap_reg:.6f} sulle celle rho<=1.5, " + f"{cap_anti:.6f} sul controllo rho=3.0") + print(" -> il cap morde SOLO nel controllo anti (g(1/3)=3 con q basso supera 2x); sulle 54") + print(" celle candidate tocca al massimo lo 0.15% delle barre = irrilevante.") + inv = float(np.max(np.abs(G.sh_full - G.sh_iso))) + print(f" invarianza di scala dello Sharpe: max|Sh(grezzo) - Sh(iso-vol)| = {inv:.2e} (esatta per") + print(" costruzione: net_iso = k*net, e net e' omogeneo di grado 1 nella posizione)") + print(f" -> il gate ISO-VOL non puo' mordere sullo Sharpe: morde sul DRIFT.") + # integrita' del gate: la vol FULL della versione iso combacia davvero col canonico? + G["vol_iso_full"] = G.vol * G.k + mism = float(np.max(np.abs(G.vol_iso_full - canrow.vol * canrow.k))) + print(f" integrita' iso-vol: k e' calibrato IN-SAMPLE, quindi la vol FULL non e' identica per") + print(f" costruzione. Scarto massimo di vol FULL fra le 60 celle iso e il canonico: " + f"{mism*100:.3f}pp su {canrow.vol*100:.2f}%") + RL = G[(np.abs(G.rho - CANON_CELL[0]) < 1e-9) & (np.abs(G.q - 0.0) < 1e-9)].iloc[0] + print(f"\n >>> NULL DEL RE-LEVERING, MISURATO: togliere il vol-targeting (q=0, stesso rho) fa") + print(f" CAGR {canrow.cagr_raw*100:.2f}% -> {RL.cagr_raw*100:.2f}% (+{(RL.cagr_raw-canrow.cagr_raw)*100:.2f}pp,") + print(f" cioe' +{(RL.cagr_raw/canrow.cagr_raw-1)*100:.0f}%) β€” e a ISO-VOL fa " + f"{(RL.cagr_iso-canrow.cagr_iso)*100:+.2f}pp. Era TUTTA leva.") + print(f"\n {'rho':>6s} {'q':>5s} {'ShFULL':>7s} {'ShIS':>6s} {'ShHOLD':>7s} {'vol%':>6s} " + f"{'k':>5s} {'CAGRraw%':>9s} {'CAGRiso%':>9s} {'dCAGRraw':>9s} {'dCAGRiso':>9s} {'DDiso%':>7s}") + for _, r in G.sort_values(["q", "rho"]).iterrows(): + star = " <== CANONICO" if (abs(r.rho - CANON_CELL[0]) < 1e-9 and abs(r.q - 1.0) < 1e-9) else "" + print(f" {r.rho:6.3f} {r.q:5.2f} {r.sh_full:7.3f} {r.sh_is:6.3f} {r.sh_hold:7.3f} " + f"{r.vol*100:6.2f} {r.k:5.2f} {r.cagr_raw*100:9.2f} {r.cagr_iso*100:9.2f} " + f"{(r.cagr_raw-canrow.cagr_raw)*100:+9.2f} {(r.cagr_iso-canrow.cagr_iso)*100:+9.2f} " + f"{r.dd_iso*100:7.2f}{star}") + + # ===================================================================== + # Β§5 SELEZIONE IN-SAMPLE-ONLY (nessuna sbirciata all'hold-out) + # ===================================================================== + print("\n### Β§5 SELEZIONE DELLA CELLA β€” IN-SAMPLE-ONLY") + Gs = G.sort_values("sh_is", ascending=False).reset_index(drop=True) + ch = Gs.iloc[0] + print(f" cella scelta al buio (max Sharpe IS, ancora 0): rho={ch.rho:.3f} q={ch.q:.2f} " + f"(ShIS {ch.sh_is:.3f} vs canonico {canrow.sh_is:.3f})") + print(" top-8 in-sample:") + for i in range(min(8, len(Gs))): + r = Gs.iloc[i] + print(f" {i+1}. rho={r.rho:5.3f} q={r.q:4.2f} ShIS {r.sh_is:6.3f} " + f"ShFULL {r.sh_full:6.3f} ShHOLD {r.sh_hold:6.3f} dCAGRiso {(r.cagr_iso-canrow.cagr_iso)*100:+.2f}pp") + from scipy.stats import spearmanr as _spear + rho_s, p_s = _spear(G.sh_is.values, G.sh_hold.values) + rho_f, _ = _spear(G.sh_is.values, G.sh_full.values) + print(f"\n la selezione in-sample ha contenuto? Spearman(ShIS, ShHOLD) su 60 celle = " + f"{rho_s:+.3f} (p={p_s:.3f}); Spearman(ShIS, ShFULL) = {rho_f:+.3f}") + print(" (il secondo e' alto per costruzione: l'in-sample e' il 78% del campione FULL.)") + print(" MECCANISMO del segno negativo β€” come si muove lo Sharpe lungo rho, per ogni q:") + print(f" {'q':>5s} {'Spearman(rho,ShIS)':>20s} {'Spearman(rho,ShHOLD)':>22s}") + for q in QS: + sub = G[np.abs(G.q - q) < 1e-9].sort_values("rho") + a, _ = _spear(sub.rho.values, sub.sh_is.values) + b, _ = _spear(sub.rho.values, sub.sh_hold.values) + print(f" {q:5.2f} {a:20.3f} {b:22.3f}") + print(" -> in-sample lo Sharpe e' a GOBBA in rho (ottimo interno ~0.45-0.60); in hold-out e'") + print(" MONOTONO CRESCENTE (l'ottimo e' al bordo rho>=1: il bucket debole rende quanto il") + print(" pieno). La convinzione e' informativa IN-SAMPLE e ANTI-informativa in HOLD-OUT:") + print(" e' questo che rende la rank-correlation negativa, non il rumore.") + rank_can_is = int((G.sh_is > canrow.sh_is).sum()) + 1 + rank_can_full = int((G.sh_full > canrow.sh_full).sum()) + 1 + rank_can_hold = int((G.sh_hold > canrow.sh_hold).sum()) + 1 + print(f" rango del CANONICO nella griglia: {rank_can_is}/{N_CELLS} in-sample, " + f"{rank_can_full}/{N_CELLS} full, {rank_can_hold}/{N_CELLS} hold-out") + + # selezione DE-LUCKATA: max mediana su 24 ancore dello Sharpe in-sample + print("\n selezione alternativa DE-LUCKATA (max MEDIANA su 24 ancore dello Sharpe IS):") + med_is = {} + for rho in RHOS: + for q in QS: + vals = [sh(split(sleeve_series(rho, q, h)[0])[0]) for h in OFFSETS] + med_is[(rho, q)] = float(np.median(vals)) + best_dl = max(med_is, key=med_is.get) + print(f" -> rho={best_dl[0]:.3f} q={best_dl[1]:.2f} (mediana ShIS {med_is[best_dl]:.3f}; " + f"canonico {med_is[CANON_CELL]:.3f})") + same = (abs(best_dl[0] - ch.rho) < 1e-9 and abs(best_dl[1] - ch.q) < 1e-9) + print(f" coincide con la scelta all'ancora 0? {'SI' if same else 'NO'}") + + CH = (float(ch.rho), float(ch.q)) + + # identita' con l'implementazione di altlib (importata, non riscritta) + print("\n controllo d'identita' con altlib.select_cell_insample (60 celle):") + grid = [dict(rho=rho, q=q) for rho in RHOS for q in QS] + + def factory(tf="1d", rho=1.0 / 3.0, q=1.0): + return make_target_fn(rho, q) + + sel = al.select_cell_insample(factory, grid, ("1d",)) + alp = sel["chosen"]["params"] + print(f" altlib sceglie rho={alp['rho']:.3f} q={alp['q']:.2f} " + f"(ShIS {sel['chosen']['insample_sharpe']:.3f}) " + f"-> {'IDENTICA' if (abs(alp['rho']-CH[0])<1e-9 and abs(alp['q']-CH[1])<1e-9) else 'DIVERSA'}") + + # ===================================================================== + # Β§6 CONTROLLI POSITIVI + # ===================================================================== + print("\n### Β§6 CONTROLLI POSITIVI (un gate che non segnala nulla puo' essere rotto)") + # CTRL-DELEV: pura riscalatura x0.5 + parts = {} + for a in ASSETS: + f = _feat(a, 0) + tgt = 0.5 * target_from_feat(f, *CANON_CELL) + parts[a] = pd.Series(np.clip(net_from_target(f, tgt), -0.99, None), index=f["idx"]) + Jd = pd.concat(parts, axis=1, join="inner").fillna(0.0) + delev = pd.Series(0.5 * Jd[ASSETS[0]].values + 0.5 * Jd[ASSETS[1]].values, index=Jd.index) + kd = vol_ann(split(can0)[0]) / vol_ann(split(delev)[0]) + print(f" CTRL-DELEV (canonico x0.5): dSharpe {sh(delev)-sh(can0):+.2e} (atteso 0) | " + f"dCAGR grezzo {(cagr(delev)-cagr(can0))*100:+.2f}pp (atteso <0) | " + f"dCAGR ISO-VOL {(cagr(kd*delev)-cagr(can0))*100:+.2e}pp (atteso 0)") + ctrl_delev_ok = (abs(sh(delev) - sh(can0)) < 1e-9 and (cagr(delev) - cagr(can0)) < 0 + and abs(cagr(kd * delev) - cagr(can0)) < 1e-9) + print(f" -> {'PASS' if ctrl_delev_ok else 'FAIL'} (il gate iso-vol neutralizza la pura scala)") + + # CTRL-ORACLE: convinzione = segno del rendimento del giorno DOPO + def oracle_fn(df): + c = df["close"].values.astype(float) + r = simple_returns(c) + fwd = np.zeros(len(r)); fwd[:-1] = r[1:] + vol = realized_vol(r, VOLWIN, 365.25) + scal = np.where((vol > 0) & np.isfinite(vol), TV / vol, 0.0) + t = np.clip(np.clip(np.sign(fwd), 0, None) * scal, -LEV, LEV) + t[~np.isfinite(t)] = 0.0 + return t + parts = {} + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + f = _feat(a, 0) + parts[a] = pd.Series(np.clip(net_from_target(f, oracle_fn(df)), -0.99, None), index=f["idx"]) + Jo = pd.concat(parts, axis=1, join="inner").fillna(0.0) + orac = pd.Series(0.5 * Jo[ASSETS[0]].values + 0.5 * Jo[ASSETS[1]].values, index=Jo.index) + caus_or = al.causality_ok(oracle_fn, tf="1d") + print(f" CTRL-ORACLE (look-ahead): Sharpe {sh(orac):.2f} (canonico {sh(can0):.2f}) | " + f"causality_ok = {caus_or['ok']} (max_tail_diff {caus_or['max_tail_diff']})") + ctrl_orac_ok = (sh(orac) > 3 * sh(can0)) and (not caus_or["ok"]) + print(f" -> {'PASS' if ctrl_orac_ok else 'FAIL'} (la macchina VEDE un miglioramento vero; " + "la guardia di causalita' ha potenza)") + + # CTRL-ANTI: rho=3.0 (sovrappesa il debole) + anti = G[(np.abs(G.rho - 3.00) < 1e-9) & (np.abs(G.q - 1.00) < 1e-9)].iloc[0] + print(f" CTRL-ANTI (rho=3.0, q=1): ShFULL {anti.sh_full:.3f} vs canonico {canrow.sh_full:.3f} | " + f"dCAGRiso {(anti.cagr_iso-canrow.cagr_iso)*100:+.2f}pp") + ctrl_anti_ok = anti.sh_full < canrow.sh_full + print(f" -> {'PASS' if ctrl_anti_ok else 'FAIL'} (sovrappesare il bucket debole deve perdere)") + + # causalita' della cella scelta + caus = al.causality_ok(make_target_fn(*CH), tf="1d") + print(f" causality_ok cella scelta rho={CH[0]:.3f} q={CH[1]:.2f}: ok={caus['ok']} " + f"max_tail_diff={caus['max_tail_diff']} checked={caus['checked']}") + + # ===================================================================== + # Β§7 DEFLATED SHARPE + sensibilita' al conteggio dei trial + # ===================================================================== + print("\n### Β§7 DEFLATED SHARPE (60 trial dichiarati) + sensibilita'") + daily_ch = al.candidate_daily(make_target_fn(*CH), tf="1d") + all_sr = [float(x) for x in G.sh_full.values] + dsr, sr0 = al.deflated_sharpe(sh(daily_ch), all_sr, daily_ch) + print(f" Sharpe cella scelta {sh(daily_ch):.3f} | sr0 atteso dal null {sr0:.3f} | " + f"DSR {dsr:.4f} -> {'PASS' if dsr >= 0.95 else 'FAIL'} (soglia 0.95)") + print(f" dispersione dei 60 trial: sd(Sharpe) = {np.std(all_sr, ddof=1):.4f} annualizzato") + print(" ⚠ lezione Β§10 dell'ondata 22/08: il DSR dipende dalla VARIANZA della griglia, non da N." + "\n Su una famiglia OMOGENEA (60 riscalature dello stesso segnale) sr0 e' minuscolo") + print(" e il gate e' quasi VACUO. Sensibilita' pubblicata (sr0 imposto a mano):") + for sr0_forced in (sr0, 0.30, 0.60, 0.90, 1.20, 1.572): + r = np.asarray(daily_ch.dropna().values, float) + srd = sh(daily_ch) / np.sqrt(365.25); s0 = sr0_forced / np.sqrt(365.25) + skw = float(pd.Series(r).skew()); ku = float(pd.Series(r).kurt()) + 3.0 + den = np.sqrt(max(1e-9, 1 - skw * srd + (ku - 1) / 4.0 * srd ** 2)) + from scipy.stats import norm as _n + d2 = float(_n.cdf((srd - s0) * np.sqrt(len(r) - 1) / den)) + print(f" sr0={sr0_forced:6.3f} -> DSR {d2:.4f} {'PASS' if d2>=0.95 else 'FAIL'}") + print(" (1.572 = il massimo atteso dal puro rumore su uno screen a 168 trial, Β§8 22/08)") + + # ===================================================================== + # Β§8 BANDA D'ANCORA β€” mediana delle DIFFERENZE APPAIATE su 24 ancore + # ===================================================================== + print("\n### Β§8 BANDA D'ANCORA (24 ancore, differenze APPAIATE variante - canonico)") + cells_watch = [CH, best_dl, (1.00, 1.00), (0.00, 1.00), (1.0 / 3.0, 0.50)] + cells_watch = list(dict.fromkeys([tuple(np.round(c, 6)) for c in cells_watch])) + can_by_h, can_is_vol = {}, {} + for h in OFFSETS: + s, _ = sleeve_series(*CANON_CELL, h) + can_by_h[h] = s + can_is_vol[h] = vol_ann(split(s)[0]) + for lab, met in (("FULL", sh), ("HOLD-OUT", lambda x: sh(split(x)[1])), + ("maxDD", lambda x: -maxdd(x))): + bb = al.anchor_luck_band(lambda h: can_by_h[h], OFFSETS, canonical=0, metric=met) + v = -1 if lab == "maxDD" else 1 + lo, hi = sorted([v * bb["lo"], v * bb["hi"]]) + print(f" banda del CANONICO, {lab:9s}: canonica {v*bb['canonical']:+.4f} (pctl " + f"{bb['canonical_pctl']*100:3.0f}Β°) mediana ONESTA {v*bb['median']:+.4f} " + f"banda [{lo:+.4f}, {hi:+.4f}]") + print(" (l'hold-out canonico e' l'ancora migliore/quasi-migliore delle 24: replica indipendente,") + print(" su dati odierni, del finding d'ancora 02/07 e del suo ricontrollo 26/07.)") + print() + print(f" {'cella':>18s} {'dShFULL med':>12s} {'>0':>6s} {'dShHOLD med':>12s} {'>0':>6s} " + f"{'dCAGRiso med':>13s} {'>0':>6s} {'p10..p90 (ShFULL)':>22s}") + anchor_rep = {} + for cell in cells_watch: + dsf, dsh_, dci = [], [], [] + for h in OFFSETS: + s, _ = sleeve_series(cell[0], cell[1], h) + cb = can_by_h[h] + v_is = vol_ann(split(s)[0]) + k = can_is_vol[h] / v_is if v_is > 0 else 0.0 + dsf.append(sh(s) - sh(cb)) + dsh_.append(sh(split(s)[1]) - sh(split(cb)[1])) + dci.append(cagr(k * s) - cagr(cb)) + dsf, dsh_, dci = np.array(dsf), np.array(dsh_), np.array(dci) + anchor_rep[cell] = dict(dsf=dsf, dsh=dsh_, dci=dci) + print(f" rho={cell[0]:5.3f} q={cell[1]:4.2f} {np.median(dsf):12.4f} {int((dsf>0).sum()):3d}/24 " + f"{np.median(dsh_):12.4f} {int((dsh_>0).sum()):3d}/24 " + f"{np.median(dci)*100:12.3f}pp {int((dci>0).sum()):3d}/24 " + f" [{np.percentile(dsf,10):+.3f}, {np.percentile(dsf,90):+.3f}]") + A = anchor_rep[cells_watch[0]] + print(f"\n cella scelta: canonica (ancora 0) dShFULL {A['dsf'][0]:+.4f} = pctl " + f"{float((A['dsf'] < A['dsf'][0]).mean())*100:.0f}Β° della banda; " + f"stima ONESTA = mediana {np.median(A['dsf']):+.4f}") + # gate via altlib (importato) sulle differenze appaiate + ald = al.anchor_luck_delta(lambda h: sleeve_series(CH[0], CH[1], h)[0], + lambda h: can_by_h[h], OFFSETS, metric=sh) + print(f" altlib.anchor_luck_delta: mediana appaiata {ald['median_paired']:+.4f} " + f"positivi {ald['n_positive']}/{ald['n_anchors']} gate_pass={ald['gate_pass']}") + + # ===================================================================== + # Β§9 SCOMPOSIZIONE PER ANNO (mediana appaiata su 24 ancore) + # ===================================================================== + print("\n### Β§9 SCOMPOSIZIONE PER ANNO del delta (mediana appaiata su 24 ancore)") + per_year = {} + for h in OFFSETS: + s, _ = sleeve_series(CH[0], CH[1], h) + cb = can_by_h[h] + v_is = vol_ann(split(s)[0]) + k = can_is_vol[h] / v_is if v_is > 0 else 0.0 + si = k * s + for y in sorted(set(cb.index.year)): + a1, a2 = si[si.index.year == y], cb[cb.index.year == y] + if len(a2) < 60: + continue + per_year.setdefault(y, {"dsh": [], "dret": []}) + per_year[y]["dsh"].append(sh(a1) - sh(a2)) + per_year[y]["dret"].append(float(np.prod(1 + a1.values) - np.prod(1 + a2.values))) + print(f" {'anno':>6s} {'dSharpe med':>12s} {'>0':>6s} {'dRitorno med':>13s} {'>0':>6s}") + n_pos_years = 0 + for y, d in sorted(per_year.items()): + a, b = np.array(d["dsh"]), np.array(d["dret"]) + n_pos_years += int(np.median(a) > 0) + print(f" {y:6d} {np.median(a):12.3f} {int((a>0).sum()):3d}/24 " + f"{np.median(b)*100:12.2f}pp {int((b>0).sum()):3d}/24") + print(f" -> anni con mediana dSharpe > 0: {n_pos_years}/{len(per_year)}") + + # ===================================================================== + # Β§10 IMPATTO SUL LIBRO (TP01 75% / SKH01 25%) + # ===================================================================== + print("\n### Β§10 IMPATTO SUL LIBRO LIVE (TP01 75% / SKH01 25%)") + skh = SL._skyhook_returns() + + def book(tp_series: pd.Series) -> pd.Series: + t = ((1.0 + tp_series).resample("1D").prod() - 1.0).dropna() + J = pd.concat({"T": t, "S": skh}, axis=1, join="inner").fillna(0.0) + return pd.Series(0.75 * J["T"].values + 0.25 * J["S"].values, index=J.index) + + bc0 = book(can_by_h[0]) + print(f" libro canonico (ancora 0): Sharpe FULL {sh(bc0):.3f} | HOLD {sh(split(bc0)[1]):.3f} | " + f"CAGR {cagr(bc0)*100:.2f}% | maxDD {maxdd(bc0)*100:.2f}% (SKH01 alla SUA ancora canonica)") + book_res = {} + todo = [("SCELTA IS", CH)] + if tuple(np.round(best_dl, 12)) != tuple(np.round(CH, 12)): + todo.append(("DE-LUCKATA", best_dl)) + for tag, cell in todo: + d_sh, d_ho, d_cg, d_dd = [], [], [], [] + for h in OFFSETS: + s, _ = sleeve_series(cell[0], cell[1], h) + cb = can_by_h[h] + v_is = vol_ann(split(s)[0]) + k = can_is_vol[h] / v_is if v_is > 0 else 0.0 + bv, bcx = book(k * s), book(cb) + d_sh.append(sh(bv) - sh(bcx)); d_ho.append(sh(split(bv)[1]) - sh(split(bcx)[1])) + d_cg.append(cagr(bv) - cagr(bcx)); d_dd.append(maxdd(bv) - maxdd(bcx)) + book_res[cell] = dict(sh=np.array(d_sh), ho=np.array(d_ho), + cg=np.array(d_cg), dd=np.array(d_dd)) + print(f"\n -- {tag}: rho={cell[0]:.3f} q={cell[1]:.2f}") + for nm, arr, unit in (("dSharpe FULL", book_res[cell]["sh"], ""), + ("dSharpe HOLD", book_res[cell]["ho"], ""), + ("dCAGR", book_res[cell]["cg"] * 100, "pp"), + ("dmaxDD", book_res[cell]["dd"] * 100, "pp")): + print(f" {nm:14s} mediana appaiata {np.median(arr):+8.4f}{unit:2s} " + f"positivi {int((arr>0).sum()):2d}/24 banda [{arr.min():+.4f}, {arr.max():+.4f}]") + db_sh = book_res[CH]["sh"]; db_ho = book_res[CH]["ho"] + db_cg = book_res[CH]["cg"]; db_dd = book_res[CH]["dd"] + print(" NB: SKH01 e' tenuto alla SUA ancora canonica; solo l'ancora di TP01 varia. Nella") + print(" differenza appaiata l'ancora di SKH01 si cancella (solo TP01 cambia).") + + # ===================================================================== + # Β§11 MARGINAL vs TP01 (e perche' non puo' dare ADDS) + # ===================================================================== + print("\n### Β§11 marginal_vs_tp01 (importato) sulla cella scelta") + mg = al.marginal_vs_tp01(daily_ch) + print(f" verdetto: {mg['marginal_verdict']} corr->TP01 full {mg['corr_full']} hold {mg['corr_hold']}") + print(f" uplift blend w25: full {mg['blends']['w25']['uplift_full']:+} hold " + f"{mg['blends']['w25']['uplift_hold']:+} | insample_edge {mg.get('has_insample_edge')} " + f"| robust_oos {mg.get('robust_oos')}") + print(" ⚠ LIMITE STRUTTURALE: la variante e' TP01 con un'altra size -> corr>0.9 -> il gate") + print(" restituisce REDUNDANT PER COSTRUZIONE. `marginal_vs_tp01` giudica un'AGGIUNTA,") + print(" non una SOSTITUZIONE della size di uno sleeve gia' nel libro: qui non e' il gate") + print(" giusto e non lo si usa come tale (stessa classe della regola (f) del 22/08).") + + # ===================================================================== + # Β§12 ESEGUIBILITA' A $635 (min_order $5) + # ===================================================================== + print("\n### Β§12 ESEGUIBILITA' A $635 (eval_weights_smallcap, min_order $5)") + s_ch, _ = sleeve_series(CH[0], CH[1], 0) + k_ch = vol_ann(split(can0)[0]) / vol_ann(split(s_ch)[0]) + print(f" {'cella':>18s} {'asset':>5s} {'Sh model':>9s} {'Sh reale':>9s} {'haircut':>8s} " + f"{'ordini eseguiti':>16s} {'turnover/a':>11s}") + for nm, cell, kk in (("CANONICO", CANON_CELL, 1.0), ("SCELTA", CH, k_ch)): + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + tgt = kk * make_target_fn(cell[0], cell[1])(df) + sc = al.eval_weights_smallcap(df, tgt, capital=635.0, min_order=5.0, fee_side=FEE) + print(f" {nm:>10s} rho={cell[0]:.2f} {a:>5s} {sc['modeled']['sharpe']:9.3f} " + f"{sc['realistic']['sharpe']:9.3f} {sc['sharpe_haircut']:8.3f} " + f"{sc['n_executed_trades']:16d} {sc['executed_turnover_per_year']:11.1f}") + + # ===================================================================== + # Β§13 VERDETTO + # ===================================================================== + print("\n" + SEP) + print("### Β§13 VERDETTO") + med_full = float(np.median(A["dsf"])) + med_hold = float(np.median(A["dsh"])) + med_ciso = float(np.median(A["dci"])) + pos_full = int((A["dsf"] > 0).sum()) + book_med = float(np.median(np.array(db_sh))) + book_pos = int((np.array(db_sh) > 0).sum()) + conds = { + "iso-vol: dCAGR mediano > 0 su 24 ancore": med_ciso > 0, + "iso-vol: dCAGR positivo in >=18/24 ancore": int((A["dci"] > 0).sum()) >= 18, + "dSharpe FULL mediano > 0 e in >=18/24 ancore": (med_full > 0 and pos_full >= 18), + "dSharpe HOLD-OUT mediano > 0": med_hold > 0, + "non e' un anno solo (>=50% degli anni con mediana>0)": n_pos_years >= 0.5 * len(per_year), + "deflated-Sharpe >= 0.95 (60 trial)": bool(np.isfinite(dsr) and dsr >= 0.95), + "libro: dSharpe mediano > 0": book_med > 0, + "causalita' ok": bool(caus["ok"]), + "controlli positivi 3/3": bool(ctrl_delev_ok and ctrl_orac_ok and ctrl_anti_ok), + } + for k_, v_ in conds.items(): + print(f" [{'PASS' if v_ else 'FAIL'}] {k_}") + n_fail = sum(1 for v_ in conds.values() if not v_) + print(f"\n cella scelta al buio: rho={CH[0]:.3f} q={CH[1]:.2f} (canonico rho=0.333 q=1.00)") + print(f" dSharpe FULL mediano {med_full:+.4f} ({pos_full}/24) | dSharpe HOLD mediano " + f"{med_hold:+.4f} | dCAGR ISO-VOL mediano {med_ciso*100:+.3f}pp") + print(f" libro 75/25: dSharpe mediano {book_med:+.4f} ({book_pos}/24), " + f"dCAGR {np.median(db_cg)*100:+.3f}pp, dmaxDD {np.median(db_dd)*100:+.3f}pp " + f"({int((db_dd>0).sum())}/24 PEGGIORE)") + print(f" condizioni fallite: {n_fail}/{len(conds)}") + print("\n LETTURA ONESTA delle condizioni che PASSANO (dichiarate prima di misurare, e alcune") + print(" passano per una moneta β€” un PASS a mediana ~0 con ~12/24 non e' evidenza):") + print(f" * 'libro: dSharpe mediano > 0' passa a {book_med:+.4f} con {book_pos}/24 = MONETA.") + print(f" * 'deflated-Sharpe >= 0.95' passa a {dsr:.4f} ma il gate e' QUASI VACUO su una") + print(f" famiglia omogenea (sr0 {sr0:.3f}); a sr0 di screen (>=0.9) FALLISCE.") + print(" * 'non e' un anno solo' passa 4/8, cioe' anch'esso una moneta.") + print(" MISURATO DOPO le condizioni (quindi NON usato come gate, riportato come fatto):") + print(f" * il maxDD del LIBRO PEGGIORA in {int((db_dd>0).sum())}/24 ancore " + f"(mediana {np.median(db_dd)*100:+.3f}pp) β€” e il valore dichiarato di TP01 e' il TAGLIO del DD.") + print(f" * Spearman(ShIS, ShHOLD) sulle 60 celle = {rho_s:+.3f}: in questa famiglia scegliere") + print(" in-sample non predice l'hold-out, quindi NESSUNA cella e' proponibile.") + print(f"\n Attesa a priori: SCARTATO. Esito: {'CONFERMATA' if n_fail > 0 else 'REFUTATA'}.") + bd = anchor_rep[next(c for c in anchor_rep if abs(c[0] - 1.0) < 1e-6 and abs(c[1] - 1.0) < 1e-6)] + print("\n VERDETTO: SCARTATO β€” nessuna forma di sizing conviction-aware alza il drift a") + print(" iso-volatilita' in modo difendibile.") + print(f" (1) il meglio dell'INTERA griglia e' " + f"+{G.cagr_iso.max()*100 - canrow.cagr_iso*100:.2f}pp di CAGR iso-vol (rho=0.45 q=1.25);") + print(" (2) la cella scelta AL BUIO peggiora l'hold-out del LIBRO in 0/24 ancore e il suo") + print(f" maxDD in 24/24, contro {np.median(db_cg)*100:+.2f}pp di CAGR;") + print(f" (3) l'unica cella con guadagno hold-out ROBUSTO e' il BINARIO rho=1 " + f"(dShHOLD {np.median(bd['dsh']):+.3f}, {int((bd['dsh']>0).sum())}/24) β€” ma perde") + print(" FULL e drift iso-vol, e sceglierla richiede di guardare l'hold-out: e'") + print(" selezione-sull'hold-out, il gate codificato dal progetto il 29/06;") + print(f" (4) Spearman(ShIS, ShHOLD) = {rho_s:+.3f} su 60 celle: qui scegliere in-sample e'") + print(" PEGGIO di una moneta => NON esiste una cella proponibile, nemmeno diversa da") + print(" quella scelta. Il canonico rho=1/3 resta com'e' β€” non perche' sia l'ottimo, ma") + print(" perche' l'ottimo di questo knob sta ai due ESTREMI OPPOSTI nelle due meta'") + print(" del campione, e nessuna delle due meta' e' un argomento per l'altra.") + print(SEP) + print(f"runtime {time.time()-t_start:.1f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_tp01_twin.py b/scripts/research/r0823_tp01_twin.py new file mode 100644 index 0000000..3969b51 --- /dev/null +++ b/scripts/research/r0823_tp01_twin.py @@ -0,0 +1,902 @@ +#!/usr/bin/env python +"""r0823_tp01_twin.py β€” Β§49 TP01-TWIN: TP01 e' UN SOLO meccanismo sul 75% del libro. + +LA DOMANDA (decidibile, non retorica) +===================================== +TP01 e' il 75% del libro live. Il progetto ne ha misurato il valore con precisione: NON genera +ritorno (contributo hold-out negativo nel 99,1% delle ancore, LOO 26/07) ma TAGLIA IL DRAWDOWN +~6x contro il buy&hold, e supera il criterio di protezione di `edge_watch` 8/8 anni di sinistro. +E' un'assicurazione e si giudica sul sinistro. + +Ma la sua direzione e' UNA definizione di trend: media di tre `sign(close/close[-h]-1)` a +30/90/180 giorni. Tutto il 75% del libro dipende da quella. + + Sostituire TP01 con un ENSEMBLE di meccanismi di trend DIVERSI ma tutti long-flat migliora + la PROTEZIONE (che e' il suo compito), a pari drift e a pari costo? + +NB la formulazione: NON "un secondo sleeve che si aggiunge" (quella e' la domanda +`marginal_vs_tp01`, gia' fatta decine di volte e sempre REDUNDANT per i trend), ma LA STESSA +QUOTA DI LIBRO espressa da piu' meccanismi. E' robustezza, non alpha. Percio' `weights_tilt_null` +NON si applica: il peso 75/25 non si muove, cambia solo cosa c'e' dentro il 75%. + +ATTESA A PRIORI β€” SCRITTA PRIMA DI MISURARE (riportata anche se refutata; lo e') +=============================================================================== +(A1) "I meccanismi di trend a 1d sullo stesso asset correlano 0,85-0,95, quindi l'ensemble sara' + TP01 con piu' fee." Corollario: la matrice di correlazione (Β§2) puo' chiudere il filone da + sola. -> REFUTATA: corr media 0,795 sui rendimenti e 0,608 sulle POSIZIONI; KEL e KAU + stanno a 0,51-0,53 di posizione da TSMOM. Non sono lo stesso oggetto. +(A2) "Se l'ensemble avra' meno DD sara' per DE-LEVERING (7a occorrenza del null)." + -> REFUTATA: la vol realizzata e' IDENTICA (12,1% vs 12,0%), l'esposizione media e il tempo + a mercato dell'ensemble sono PIU' ALTI. Non c'e' niente da de-levere: il null non e' + applicabile, e va detto cosi' invece di dire "non refuta" (che suggerirebbe che l'ensemble + abbia superato un test che in realta' non aveva potenza). + +MECCANISMI β€” DICHIARATI TUTTI PRIMA, E CONTATI COME TRIAL (Β§8) +============================================================== +Tutti LONG-FLAT, tutti vol-targeted 20% (cap 2x, finestra 30g), tutti a 1d, tutti sullo stesso +50/50 BTC+ETH, tutti causali (decidono a close[i], tengono in i+1), tutti a fee 0,10% RT. + TSMOM β€” il riferimento: media dei sign(c/c[-h]-1), h in 30/90/180 (= TP01 CANONICAL) + DON β€” Donchian breakout long-flat: entra sopra il massimo dei precedenti N, esce sotto il + minimo dei precedenti N/2. NB: SKH01 e' un Donchian ma L/S e dual-TF 230m/690m β€” + NON e' lo stesso oggetto e qui non viene ricreato. + EWMA β€” incrocio di medie esponenziali, blend di 3 coppie (stessa forma multi-orizzonte di TSMOM) + KEL β€” canale di Keltner: entra sopra ema+k*atr, esce sotto la ema (mid) + KAU β€” Kaufman: KAMA (efficiency-ratio adattiva), lunga se la KAMA sale su `slope` barre +Indicatori presi da `src/strategies/indicators.py` (ema, atr) e da `altlib` (donchian, vol_target, +eval_weights, eval_weights_smallcap, causality_ok, study_family_honest): importati, non riscritti. + +ENSEMBLE (due varianti, dichiarate a priori) + ENS-DIR β€” media delle 5 DIREZIONI, poi UN solo vol-target. E' esattamente la struttura di TP01 + (che gia' media 3 direzioni prima di scalare): "stessa quota, piu' meccanismi". + ENS-POS β€” media delle 5 POSIZIONI gia' vol-targeted (differisce solo dove morde il cap 2x). +Il costo si conta sulla posizione NETTATA (il libro ha UNA posizione per strumento su Deribit): +la fee e' su |delta(posizione media)|. Si riporta anche il caso non-nettato come limite superiore. + +COSA DECIDE (in ordine) + Β§2 correlazione fra meccanismi + Β§4 PROTEZIONE anno per anno, criterio (B) di `scripts/live/edge_watch.py` importato dal + sorgente di produzione: negli anni con DD buy&hold > 10%, il DD dello sleeve deve restare + sotto il 75% di quello. + ⚠️ ERRORE MIO, CATTURATO IN SESSIONE E RIPORTATO: la prima stesura decideva sul CASO + PEGGIORE (il massimo degli 8 rapporti), che migliora 0,56 -> 0,49, e avrebbe stampato + "ENSEMBLE PROMOSSO A LEAD". Ma il massimo di 8 numeri non e' una statistica: si e' mosso + perche' e' migliorato UN anno (2023), mentre 4 anni su 8 PEGGIORANO β€” compreso il 2022, che + e' l'unico sinistro vero del campione (0,04 -> 0,13, tre volte peggio). Β§4-bis misura la + DISTRIBUZIONE (test dei segni per anno, e 24x8 celle appaiate sulle ancore) invece del massimo. + Β§5 GATE ISO-VOLATILITA' + NULL DEL DE-LEVERING (7a occorrenza). NB: lo Sharpe e' invariante + alla scala, quindi il null "iso-DD, confronta lo Sharpe" e' DEGENERE (regola dell'ondata + 22/08): la forma non degenere e' iso-VOL sulla PROTEZIONE + confronto diretto di Sharpe. + Β§5-bis LA COMPOSIZIONE DELL'ENSEMBLE E' ESSA STESSA UN PARAMETRO LIBERO: 31 sottoinsiemi non + vuoti, di cui ne e' stato dichiarato UNO. Si misura dove cade il dichiarato nella + distribuzione dei 31 β€” se sta in alto, un ensemble "migliore" e' selezione, non robustezza. + Β§6 banda d'ancora 24 ore, MEDIANA DELLE DIFFERENZE APPAIATE (mai la differenza delle mediane) + Β§7 costi: curva fee 0-20 bps RT + `eval_weights_smallcap` a $635 + Β§8 deflated-Sharpe di FAMIGLIA (`study_family_honest`) β€” con l'avvertenza che su una famiglia + OMOGENEA sr0 e' piccolo per costruzione e il DSR passa facile: NON e' il gate che decide qui + Β§9 impatto sul libro 75/25 con SKH01, ANCHE in banda d'ancora appaiata (la gamba SKH01 e' + identica nei due bracci, quindi la sua fortuna d'ancora si CANCELLA nella differenza) + +VINCOLI: script di sola lettura. Non tocca src/, config/, scripts/live/, tests/, data/. +Runtime ~1 min su 2 CPU. +""" +from __future__ import annotations + +import importlib.util +import itertools +import sys +import time +import warnings +from functools import lru_cache +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT)) + +import altlib as al # noqa: E402 +from src.data.downloader import load_data # noqa: E402 +from src.portfolio.portfolio import DAYS_PER_YEAR, combine_outer, to_daily # noqa: E402 +from src.portfolio.sleeves import _tp01_returns, skyhook_sleeve # noqa: E402 +from src.strategies.indicators import atr as ind_atr # noqa: E402 +from src.strategies.indicators import ema as ind_ema # noqa: E402 +from src.strategies.trend_portfolio import (CANONICAL, resample_tf, # noqa: E402 + simple_returns, tsmom_blend) + +warnings.filterwarnings("ignore", category=RuntimeWarning) + +ASSETS = ("BTC", "ETH") +FEE_SIDE = CANONICAL["fee_side"] # 0.0005 = 0.10% RT +TARGET_VOL = CANONICAL["target_vol"] # 0.20 +LEV_CAP = CANONICAL["leverage"] # 2.0 +VOL_WIN = CANONICAL["vol_win_days"] # 30 +HOLDOUT = al.HOLDOUT +ANCHORS = tuple(range(24)) +CAPITALE = 635.0 # conto reale dichiarato nel progetto + +# --- criterio (B) importato DAL SORGENTE DI PRODUZIONE (non riscritto) ------ +_spec = importlib.util.spec_from_file_location("_edge_watch", ROOT / "scripts" / "live" / "edge_watch.py") +_ew = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_ew) +SINISTRO_DD, PROTECT_MAX = _ew.SINISTRO_DD, _ew.PROTECT_MAX + + +# =========================================================================== +# 0. DATI E MECCANISMI +# =========================================================================== +@lru_cache(maxsize=8) +def raw1h(asset: str) -> pd.DataFrame: + return load_data(asset, "1h") + + +def resample_offset(df_1h: pd.DataFrame, h: int) -> pd.DataFrame: + """Barre giornaliere ancorate alle h:00 UTC. h=0 usa direttamente resample_1d + (stessa convenzione label/closed='left' di trend_portfolio.resample_tf).""" + if h == 0: + return resample_tf(df_1h, "1D") + g = df_1h.copy() + idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True) + idx.name = "dt" + g.index = idx + out = g.resample("24h", offset=pd.Timedelta(hours=h), label="left", closed="left").agg( + {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}) + out = out.dropna(subset=["open"]) + out["datetime"] = out.index + epoch = pd.Timestamp("1970-01-01", tz="UTC") + out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64") + return out.reset_index(drop=True)[["timestamp", "open", "high", "low", "close", "volume", "datetime"]] + + +@lru_cache(maxsize=64) +def bars(asset: str, h: int) -> pd.DataFrame: + return resample_offset(raw1h(asset), h) + + +# --- direzioni: array in [0,1], NaN durante il proprio warm-up ------------- +def dir_tsmom(df, horizons=(30, 90, 180)): + c = df["close"].values.astype(float) + d = np.clip(tsmom_blend(c, tuple(horizons)), 0.0, None) # LONG-FLAT + d[:min(horizons)] = np.nan # nan_to_num -> 0 = esattamente tsmom_blend + return d + + +def dir_don(df, win=50): + ex = max(2, win // 2) + hi, _ = al.donchian(df, win) + _, lo = al.donchian(df, ex) + c = df["close"].values.astype(float) + out = np.full(len(c), np.nan) + st = 0.0 + for i in range(len(c)): + if not (np.isfinite(hi[i]) and np.isfinite(lo[i])): + continue + if st == 0.0 and c[i] > hi[i]: + st = 1.0 + elif st == 1.0 and c[i] < lo[i]: + st = 0.0 + out[i] = st + return out + + +def dir_ewma(df, pairs=((10, 40), (20, 80), (40, 160))): + c = df["close"].values.astype(float) + acc = np.zeros(len(c)) + cnt = np.zeros(len(c)) + for f, s in pairs: + ef, es = ind_ema(c, f), ind_ema(c, s) + v = np.isfinite(ef) & np.isfinite(es) + acc[v] += np.sign(ef[v] - es[v]) + cnt[v] += 1 + out = np.full(len(c), np.nan) + nz = cnt > 0 + out[nz] = np.clip(acc[nz] / cnt[nz], 0.0, None) + return out + + +def dir_kel(df, win=20, k=1.5): + c = df["close"].values.astype(float) + mid = ind_ema(c, win) + a = ind_atr(df["high"].values.astype(float), df["low"].values.astype(float), c, win) + up = mid + k * a + out = np.full(len(c), np.nan) + st = 0.0 + for i in range(len(c)): + if not (np.isfinite(up[i]) and np.isfinite(mid[i])): + continue + if st == 0.0 and c[i] > up[i]: + st = 1.0 + elif st == 1.0 and c[i] < mid[i]: + st = 0.0 + out[i] = st + return out + + +def dir_kau(df, n=20, slope=5, fast=2, slow=30): + c = df["close"].values.astype(float) + d = np.abs(np.diff(c, prepend=c[0])) + vsum = pd.Series(d).rolling(n, min_periods=n).sum().values + er = np.full(len(c), np.nan) + er[n:] = np.where(vsum[n:] > 0, np.abs(c[n:] - c[:-n]) / vsum[n:], 0.0) + fsc, ssc = 2.0 / (fast + 1), 2.0 / (slow + 1) + kama = np.full(len(c), np.nan) + for i in range(n, len(c)): + a = (er[i] * (fsc - ssc) + ssc) ** 2 + prev = kama[i - 1] if np.isfinite(kama[i - 1]) else c[i - 1] + kama[i] = prev + a * (c[i] - prev) + out = np.full(len(c), np.nan) + ok = np.isfinite(kama) + for i in range(n + slope, len(c)): + if ok[i] and ok[i - slope]: + out[i] = 1.0 if kama[i] > kama[i - slope] else 0.0 + return out + + +MECHS = { + "TSMOM": (dir_tsmom, dict(horizons=(30, 90, 180))), + "DON": (dir_don, dict(win=50)), + "EWMA": (dir_ewma, dict(pairs=((10, 40), (20, 80), (40, 160)))), + "KEL": (dir_kel, dict(win=20, k=1.5)), + "KAU": (dir_kau, dict(n=20, slope=5)), +} +MNAMES = tuple(MECHS) + + +def direction(name: str, df: pd.DataFrame, **over) -> np.ndarray: + fn, p = MECHS[name] + q = dict(p) + q.update(over) + return fn(df, **q) + + +@lru_cache(maxsize=64) +def dircache(asset: str, h: int) -> dict: + df = bars(asset, h) + return {m: direction(m, df) for m in MNAMES} + + +def to_pos(d: np.ndarray, df: pd.DataFrame, target_vol: float = TARGET_VOL) -> np.ndarray: + return al.vol_target(np.nan_to_num(np.asarray(d, float), nan=0.0), df, + target_vol=target_vol, vol_win_days=VOL_WIN, leverage_cap=LEV_CAP) + + +def net_asset(df: pd.DataFrame, tgt: np.ndarray, fee_side: float = FEE_SIDE) -> pd.Series: + """Identico, riga per riga, a `sleeves._tp01_returns` per singolo asset.""" + r = simple_returns(df["close"].values.astype(float)) + held = np.zeros(len(tgt)) + held[1:] = tgt[:-1] + net = held * r - fee_side * np.abs(np.diff(held, prepend=0.0)) + net[0] = 0.0 + return pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"])) + + +def book5050(series: dict) -> pd.Series: + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + return pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index) + + +def mean_dirs(df, h, asset, names) -> np.ndarray: + D = np.vstack([dircache(asset, h)[m] for m in names]) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + e = np.nanmean(D, axis=0) + return np.nan_to_num(e, nan=0.0) + + +def ens_dirs(df: pd.DataFrame, names=MNAMES) -> np.ndarray: + """Versione stateless (per causality_ok / study_family_honest): ricalcola dal df.""" + D = np.vstack([direction(m, df) for m in names]) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + e = np.nanmean(D, axis=0) + return np.nan_to_num(e, nan=0.0) + + +def ens_pos(df: pd.DataFrame, names=MNAMES) -> np.ndarray: + return np.mean([to_pos(direction(m, df), df) for m in names], axis=0) + + +def targets_for(kind, h: int, target_vol: float = TARGET_VOL) -> dict: + """kind = nome meccanismo | 'ENS-DIR' | 'ENS-POS' | tuple di meccanismi (sottoinsieme).""" + out = {} + for a in ASSETS: + df = bars(a, h) + if isinstance(kind, tuple): + out[a] = (df, to_pos(mean_dirs(df, h, a, kind), df, target_vol)) + elif kind == "ENS-DIR": + out[a] = (df, to_pos(mean_dirs(df, h, a, MNAMES), df, target_vol)) + elif kind == "ENS-POS": + out[a] = (df, np.mean([to_pos(dircache(a, h)[m], df) for m in MNAMES], axis=0) + * (target_vol / TARGET_VOL)) + else: + out[a] = (df, to_pos(dircache(a, h)[kind], df, target_vol)) + return out + + +def strat_series(kind, h: int, fee_side: float = FEE_SIDE, + target_vol: float = TARGET_VOL) -> pd.Series: + tg = targets_for(kind, h, target_vol) + return book5050({a: net_asset(tg[a][0], tg[a][1], fee_side) for a in ASSETS}) + + +@lru_cache(maxsize=64) +def buyhold(h: int) -> pd.Series: + out = {} + for a in ASSETS: + df = bars(a, h) + out[a] = pd.Series(simple_returns(df["close"].values.astype(float)), + index=pd.to_datetime(df["datetime"])) + return book5050(out) + + +# =========================================================================== +# METRICHE +# =========================================================================== +def sh(s: pd.Series) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(r.mean() / r.std() * np.sqrt(DAYS_PER_YEAR)) if len(r) > 2 and r.std() > 0 else 0.0 + + +def vol(s: pd.Series) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(r.std() * np.sqrt(DAYS_PER_YEAR)) if len(r) > 2 else 0.0 + + +def drift(s: pd.Series) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(r.mean() * DAYS_PER_YEAR) if len(r) else 0.0 + + +def maxdd(s: pd.Series) -> float: + return float(_ew.maxdd(pd.Series(s).dropna())) + + +def dd_window(s: pd.Series): + """Estremi temporali del maxDD (per capire se sta DENTRO un anno o lo attraversa).""" + s = pd.Series(s).dropna() + e = (1.0 + s).cumprod() + pk = e.cummax() + dd = 1.0 - e / pk + j = int(np.argmax(dd.values)) + i = int(np.argmax(e.values[:j + 1])) if j > 0 else 0 + return s.index[i], s.index[j] + + +def worst3m(s: pd.Series) -> float: + e = (1.0 + pd.Series(s).dropna()).cumprod() + w = 91 + if len(e) <= w: + return 0.0 + return float((e.values[w:] / e.values[:-w] - 1.0).min()) + + +def hold(s: pd.Series) -> pd.Series: + s = pd.Series(s).dropna() + return s[s.index >= HOLDOUT] + + +def protection(sleeve: pd.Series, bh: pd.Series) -> list[dict]: + """Criterio (B) di edge_watch, chiamato SUL SORGENTE DI PRODUZIONE.""" + return _ew.protection_by_year(pd.Series(sleeve).dropna(), pd.Series(bh).dropna()) + + +def prot_summary(prot: list[dict]) -> dict: + if not prot: + return dict(n=0, n_ok=0, worst=float("nan"), mean=float("nan")) + rr = [p["ratio"] for p in prot] + return dict(n=len(prot), n_ok=int(sum(p["ok"] for p in prot)), + worst=float(max(rr)), mean=float(np.mean(rr))) + + +def line(name: str, s: pd.Series, prot: list[dict] | None = None) -> str: + ps = prot_summary(prot) if prot is not None else None + tail = (f"{ps['n_ok']}/{ps['n']}{'':>2}{ps['worst']:>8.2f}{ps['mean']:>8.2f}" if ps else "") + return (f" {name:<12}{sh(s):>8.3f}{sh(hold(s)):>9.3f}{drift(s):>9.1%}{vol(s):>8.1%}" + f"{maxdd(s):>9.1%}{worst3m(s):>9.1%} {tail}") + + +HEAD = (f" {'strategia':<12}{'ShFULL':>8}{'ShHOLD':>9}{'drift':>9}{'vol':>8}" + f"{'maxDD':>9}{'peg.3m':>9} {'prot':>5}{'peggio':>8}{'medio':>8}") + + +# =========================================================================== +def main() -> int: + t0 = time.time() + P = print + P("=" * 104) + P(" Β§49 TP01-TWIN β€” TP01 e' UN SOLO meccanismo sul 75% del libro. Un ENSEMBLE protegge di piu'?") + P("=" * 104) + P("\n ATTESA A PRIORI (scritta prima di misurare, riportata anche se refutata):") + P(" (A1) i meccanismi di trend a 1d correlano 0,85-0,95 -> l'ensemble sara' TP01 con piu' fee") + P(" (A2) se l'ensemble avra' meno DD sara' per DE-LEVERING (7a occorrenza del null)") + P(" Esito anticipato qui per onesta': ENTRAMBE REFUTATE. Il filone non muore dove me") + P(" l'aspettavo β€” muore su Β§4-bis (distribuzione della protezione) e Β§9 (costo sul libro).") + + # ---------------------------------------------------------------- Β§1 + P("\n" + "=" * 104) + P(" Β§1 REPLICA BIT-EXACT del canonico contro sleeves._tp01_returns()") + P("=" * 104) + ref = _tp01_returns() + mine = strat_series("TSMOM", 0) + J = pd.concat({"ref": ref, "mine": mine}, axis=1, join="outer") + md = float(np.nanmax(np.abs(J["ref"].values - J["mine"].values))) + same_len = (len(ref) == len(mine) == len(J)) + P(f"\n barre ref={len(ref)} mie={len(mine)} unione={len(J)} max|diff| = {md:.3e}" + f" -> {'OK (bit-exact)' if (md == 0.0 and same_len) else 'DIVERGE β€” STOP'}") + if not (md == 0.0 and same_len): + P(" ABORT: senza replica bit-exact nessun delta e' interpretabile.") + return 1 + + # ---------------------------------------------------------------- Β§2 + P("\n" + "=" * 104) + P(" Β§2 MATRICE DI CORRELAZIONE FRA MECCANISMI") + P("=" * 104) + S = {m: strat_series(m, 0) for m in MNAMES} + S["ENS-DIR"] = strat_series("ENS-DIR", 0) + S["ENS-POS"] = strat_series("ENS-POS", 0) + names = list(MNAMES) + R = pd.concat({m: S[m] for m in names}, axis=1, join="inner").dropna() + C = R.corr() + P(f"\n (a) RENDIMENTI NETTI giornalieri, finestra comune {R.index[0].date()} -> " + f"{R.index[-1].date()} (n={len(R)})\n") + P(" " + "".join(f"{m:>9}" for m in names)) + for m in names: + P(f" {m:<6}" + "".join(f"{C.loc[m, k]:>9.3f}" for k in names)) + off = [C.loc[a, b] for i, a in enumerate(names) for b in names[i + 1:]] + P(f"\n media fuori diagonale = {np.mean(off):.3f} min = {np.min(off):.3f} max = {np.max(off):.3f}") + + Pos = {} + for m in names: + cols = {} + for a in ASSETS: + df = bars(a, 0) + cols[a] = pd.Series(to_pos(dircache(a, 0)[m], df), index=pd.to_datetime(df["datetime"])) + Pos[m] = book5050(cols) + PosJ = pd.concat(Pos, axis=1, join="inner").dropna() + CP = PosJ.corr() + offp = [CP.loc[a, b] for i, a in enumerate(names) for b in names[i + 1:]] + P("\n (b) POSIZIONI-bersaglio (e' qui che vive la ridondanza)\n") + P(" " + "".join(f"{m:>9}" for m in names)) + for m in names: + P(f" {m:<6}" + "".join(f"{CP.loc[m, k]:>9.3f}" for k in names)) + P(f"\n media fuori diagonale = {np.mean(offp):.3f} min = {np.min(offp):.3f} max = {np.max(offp):.3f}") + P(f"\n -> ATTESA (A1) REFUTATA: 0,795 e 0,608, non 0,85-0,95. KEL/KAU sono a 0,51-0,53 di") + P(" POSIZIONE da TSMOM: sono meccanismi genuinamente diversi, non TP01 travestito.") + P(" Il filone NON si chiude in Β§2 β€” bisogna misurarlo.") + + skh = skyhook_sleeve(1.0).daily() + tpd = to_daily(S["TSMOM"]) + cc = pd.concat({"tp": tpd, "sk": skh}, axis=1, join="inner").dropna() + c_tp_skh = float(cc["tp"].corr(cc["sk"])) + rng = np.random.default_rng(20260823) + shuf = pd.Series(rng.permutation(S["TSMOM"].values), index=S["TSMOM"].index) + c_shuf = float(pd.concat({"a": S["TSMOM"], "b": shuf}, axis=1).corr().iloc[0, 1]) + P(f"\n CONTROLLO POSITIVO dello stimatore: corr(TSMOM,TSMOM)={C.loc['TSMOM','TSMOM']:.3f} (atteso 1.000) Β· " + f"corr(TP01,SKH01)={c_tp_skh:+.3f} (pubblicato ~+0.09) Β· corr(TP01, permutato)={c_shuf:+.3f} (atteso ~0)") + + # ---------------------------------------------------------------- Β§3 + P("\n" + "=" * 104) + P(" Β§3 METRICHE STANDALONE (ancora canonica h=0) β€” 50/50 BTC+ETH, fee 0,10% RT") + P("=" * 104 + "\n") + bh0 = buyhold(0) + P(HEAD) + for k in names + ["ENS-DIR", "ENS-POS"]: + P(line(k, S[k], protection(S[k], bh0))) + P(line("BUY&HOLD", bh0, protection(bh0, bh0))) + P("\n NB: il MIGLIOR protettore singolo e' KEL (peggior rapporto 0,33) e ha lo Sharpe") + P(" hold-out PIU' BASSO (-0,32): e' il premio dell'assicurazione, misurato. L'ensemble") + P(" NON batte il suo membro migliore sulla protezione β€” e' la media. Sceglierlo sarebbe") + P(" selezione su 5 candidati, non robustezza.") + + # ---------------------------------------------------------------- Β§4 + P("\n" + "=" * 104) + P(" Β§4 PROTEZIONE ANNO PER ANNO β€” criterio (B) di scripts/live/edge_watch.py (importato)") + P(f" anni con DD buy&hold > {SINISTRO_DD:.0%}; il DD dello sleeve deve stare sotto il " + f"{PROTECT_MAX:.0%} di quello") + P("=" * 104 + "\n") + pr_t = protection(S["TSMOM"], bh0) + pr_e = protection(S["ENS-DIR"], bh0) + P(f" {'anno':>6}{'DD b&h':>10}{'DD TSMOM':>11}{'rap.':>7}{'esito':>7}" + f"{'DD ENS':>11}{'rap.':>7}{'esito':>7}{' delta rapporto':>18}{' chi protegge':>16}") + for a, b in zip(pr_t, pr_e): + dlt = b["ratio"] - a["ratio"] + P(f" {a['year']:>6}{a['dd_bh']:>9.1%}{a['dd_tp']:>10.1%}{a['ratio']:>7.2f}" + f"{' OK' if a['ok'] else ' FAIL':>7}" + f"{b['dd_tp']:>10.1%}{b['ratio']:>7.2f}{' OK' if b['ok'] else ' FAIL':>7}" + f"{dlt:>+18.3f}{(' ENS' if dlt < 0 else ' TSMOM'):>16}") + st, se = prot_summary(pr_t), prot_summary(pr_e) + P(f"\n TSMOM: {st['n_ok']}/{st['n']} superati Β· rapporto PEGGIORE {st['worst']:.2f} Β· medio {st['mean']:.2f}") + P(f" ENS : {se['n_ok']}/{se['n']} superati Β· rapporto PEGGIORE {se['worst']:.2f} Β· medio {se['mean']:.2f}") + + # ---------------------------------------------------------------- Β§4-bis + P("\n" + "-" * 104) + P(" Β§4-bis IL MASSIMO DI 8 NUMERI NON E' UNA STATISTICA β€” la DISTRIBUZIONE della protezione") + P("-" * 104) + dlt = np.array([b["ratio"] - a["ratio"] for a, b in zip(pr_t, pr_e)]) + nb = int((dlt < 0).sum()) + P(f"\n h=0: ENS protegge meglio in {nb}/{len(dlt)} anni di sinistro, peggio in {len(dlt)-nb}" + f" -> test dei segni = MONETA") + P(f" delta mediano del rapporto {np.median(dlt):+.3f} Β· medio {np.mean(dlt):+.3f} " + f"(il rapporto MEDIO PEGGIORA: {st['mean']:.2f} -> {se['mean']:.2f})") + y22t = [p for p in pr_t if p["year"] == 2022][0] + y22e = [p for p in pr_e if p["year"] == 2022][0] + P(f"\n ⚠ IL SINISTRO PIU' GRANDE DEL CAMPIONE β€” 2022, deleveraging (DD buy&hold " + f"{y22t['dd_bh']:.0%}, il massimo degli 8 anni): TSMOM protegge") + P(f" {1/y22t['ratio']:.1f}x (DD {y22t['dd_tp']:.1%}), ENS {1/y22e['ratio']:.1f}x (DD {y22e['dd_tp']:.1%})" + f" -> l'ensemble e' {y22e['ratio']/y22t['ratio']:.1f}x PEGGIORE nell'anno peggiore.") + P(" Regola del progetto: uno sleeve difensivo si giudica SUL SINISTRO. Qui il sinistro dice ENS peggio.") + P(f"\n Il 'caso peggiore migliora 0,56 -> 0,49' e' UN SOLO ANNO (2023, {dlt[4]:+.3f}): il massimo") + P(" si e' spostato perche' e' migliorata la cella che lo deteneva, non perche' la protezione") + P(" sia migliorata. ERRORE MIO, catturato in sessione: la prima stesura decideva su quel massimo.") + + pr_bh = protection(bh0, bh0) + P(f"\n CONTROLLI POSITIVI del criterio: TP01 {st['n_ok']}/{st['n']} (pubblicato 8/8) Β· " + f"buy&hold contro se stesso {prot_summary(pr_bh)['n_ok']}/{prot_summary(pr_bh)['n']} " + f"(rapporto 1.00 -> DEVE fallire tutti)") + + # ---------------------------------------------------------------- Β§5 + P("\n" + "=" * 104) + P(" Β§5 GATE ISO-VOLATILITA' + NULL DEL DE-LEVERING") + P("=" * 104) + v_t, v_e = vol(S["TSMOM"]), vol(S["ENS-DIR"]) + d_t, d_e = drift(S["TSMOM"]), drift(S["ENS-DIR"]) + k_vol = v_e / v_t if v_t > 0 else 1.0 + expo = {m: float(np.mean(np.abs(PosJ[m]))) for m in names} + ens_expo = float(np.mean(np.abs(book5050({a: pd.Series(to_pos(mean_dirs(bars(a, 0), 0, a, MNAMES), bars(a, 0)), + index=pd.to_datetime(bars(a, 0)["datetime"])) + for a in ASSETS})))) + # tempo a mercato con la STESSA definizione di Β§7 (media PER-ASSET sulla posizione tenuta). + # ERRORE MIO corretto in sessione: la prima stesura lo calcolava sulla serie 50/50, che e' + # non-zero se lo e' UNO DEI DUE asset, e dava 66,8%/79,4% contro i 55,4%/70,3% stampati da + # eval_weights nella stessa pagina. Due definizioni della stessa grandezza = numero inutile. + def _tim(kind): + tg = targets_for(kind, 0) + return float(np.mean([al.eval_weights(tg[a][0], tg[a][1])["time_in_market"] for a in ASSETS])) + tim_t, tim_e = _tim("TSMOM"), _tim("ENS-DIR") + P(f"\n vol realizzata TSMOM {v_t:.2%} ENS {v_e:.2%} -> k_isovol = {k_vol:.4f}") + P(f" drift TSMOM {d_t:.2%} ENS {d_e:.2%}") + P(f" esposizione media |pos| TSMOM {expo['TSMOM']:.3f} ENS {ens_expo:.3f}" + f" Β· tempo a mercato TSMOM {tim_t:.1%} ENS {tim_e:.1%}") + P(f" per meccanismo: " + ", ".join(f"{m} {expo[m]:.3f}" for m in names)) + P(f"\n -> ATTESA (A2) REFUTATA, E IL NULL NON E' APPLICABILE: k_isovol = {k_vol:.4f} β‰ˆ 1, cioe'") + P(" NON C'E' NIENTE DA DE-LEVERE. L'ensemble ha la STESSA vol, esposizione media PIU' ALTA") + P(" e tempo a mercato PIU' ALTO. Dire 'il null non refuta' sarebbe fuorviante: il null non") + P(" ha potenza perche' la variante non e' una ri-scalatura. Si dice cosi'.") + P("\n ⚠ Lo Sharpe e' INVARIANTE alla scala -> il null 'iso-DD, confronta lo Sharpe' e' DEGENERE") + P(" (regola dell'ondata 22/08). Forma non degenere applicata: confronto diretto di Sharpe +") + P(" de-levering fatto come si farebbe DAVVERO nel live, abbassando `target_vol` (26/06).\n") + tv_iso = TARGET_VOL * k_vol + iso_run = strat_series("TSMOM", 0, target_vol=tv_iso) + P(HEAD) + P(line("TSMOM", S["TSMOM"], pr_t)) + P(line(f"TSMOM@tv{tv_iso:.3f}", iso_run, protection(iso_run, bh0))) + P(line("ENS-DIR", S["ENS-DIR"], pr_e)) + w_t, w_e = dd_window(S["TSMOM"]), dd_window(S["ENS-DIR"]) + P(f"\n DIAGNOSTICA β€” perche' la protezione ANNUALE puo' migliorare mentre il maxDD peggiora:") + P(f" finestra del maxDD TSMOM {w_t[0].date()} -> {w_t[1].date()} ({(w_t[1]-w_t[0]).days} g)") + P(f" ENS {w_e[0].date()} -> {w_e[1].date()} ({(w_e[1]-w_e[0]).days} g)") + P(" Il criterio (B) misura il DD DENTRO l'anno solare; il maxDD attraversa gli anni. Un") + P(" ensemble che sta a mercato il 70% del tempo (contro 55%) sanguina piu' a lungo e piu'") + P(" piano: DD annuali un po' piu' piccoli in qualche anno, DD complessivo piu' lungo.") + + # ---------------------------------------------------------------- Β§5-bis + P("\n" + "-" * 104) + P(" Β§5-bis LA COMPOSIZIONE DELL'ENSEMBLE E' ESSA STESSA UN PARAMETRO LIBERO (31 sottoinsiemi)") + P("-" * 104) + subs = [] + for k in range(1, len(MNAMES) + 1): + for comb in itertools.combinations(MNAMES, k): + s = strat_series(comb, 0) + ps = prot_summary(protection(s, bh0)) + subs.append(dict(sub="+".join(comb), k=k, sh=sh(s), hd=sh(hold(s)), + dd=maxdd(s), worst=ps["worst"], mean=ps["mean"], n_ok=ps["n_ok"])) + SB = pd.DataFrame(subs) + dich = SB[SB["sub"] == "+".join(MNAMES)].iloc[0] + P(f"\n 31 sottoinsiemi non vuoti; ne era stato DICHIARATO 1 (tutti e 5). Dove cade il dichiarato:") + P(f" protezione PEGGIORE {dich['worst']:.3f} -> {float((SB['worst'] <= dich['worst']).mean())*100:.0f}Β° pctl" + f" (migliore dei 31: {SB['worst'].min():.3f} = {SB.loc[SB['worst'].idxmin(),'sub']})") + P(f" Sharpe FULL {dich['sh']:.3f} -> {float((SB['sh'] <= dich['sh']).mean())*100:.0f}Β° pctl" + f" (migliore: {SB['sh'].max():.3f} = {SB.loc[SB['sh'].idxmax(),'sub']})") + P(f" Sharpe HOLD-OUT {dich['hd']:.3f} -> {float((SB['hd'] <= dich['hd']).mean())*100:.0f}Β° pctl" + f" (migliore: {SB['hd'].max():.3f} = {SB.loc[SB['hd'].idxmax(),'sub']})") + P(f" maxDD {dich['dd']:.3f} -> {float((SB['dd'] <= dich['dd']).mean())*100:.0f}Β° pctl" + f" (migliore: {SB['dd'].min():.3f} = {SB.loc[SB['dd'].idxmin(),'sub']})") + n_beat = int(((SB['worst'] < st['worst']) & (SB['hd'] > sh(hold(S['TSMOM'])))).sum()) + P(f"\n sottoinsiemi che battono TSMOM da solo su ENTRAMBI (protez. peggiore E Sharpe hold-out):" + f" {n_beat}/31 (TSMOM e' uno dei 31 e non puo' battere se stesso -> {n_beat}/30 alternative)") + P(" E' il risultato piu' forte della sezione: fra TUTTE le 31 composizioni non ne esiste una") + P(" che compri protezione senza pagarla sull'hold-out. Il compromesso non e' di QUESTO") + P(" ensemble, e' della famiglia.") + P(f" TSMOM da solo, per riferimento: protez. peggiore {st['worst']:.3f} Β· ShHOLD " + f"{sh(hold(S['TSMOM'])):.3f} Β· maxDD {maxdd(S['TSMOM']):.3f}") + P("\n leave-one-out (togli UN meccanismo dai 5) β€” quanto l'esito dipende dalla membership:") + P(f" {'ensemble':<24}{'ShFULL':>9}{'ShHOLD':>9}{'maxDD':>9}{'prot.peggio':>13}{'prot.media':>12}") + for m in ["(tutti e 5)"] + [f"senza {x}" for x in MNAMES]: + comb = MNAMES if m.startswith("(") else tuple(x for x in MNAMES if x != m.split()[1]) + row = SB[SB["sub"] == "+".join(comb)].iloc[0] + P(f" {m:<24}{row['sh']:>9.3f}{row['hd']:>9.3f}{row['dd']:>9.1%}{row['worst']:>13.2f}{row['mean']:>12.2f}") + + # ---------------------------------------------------------------- Β§6 + P("\n" + "=" * 104) + P(" Β§6 BANDA D'ANCORA 24 ORE β€” MEDIANA DELLE DIFFERENZE APPAIATE (mai la diff. delle mediane)") + P("=" * 104) + rows = [] + per_year: dict = {} + cell_better = 0 + cell_tot = 0 + for h in ANCHORS: + bh_h = buyhold(h) + st_h = strat_series("TSMOM", h) + se_h = strat_series("ENS-DIR", h) + prt, pre = protection(st_h, bh_h), protection(se_h, bh_h) + for a, b in zip(prt, pre): + cell_tot += 1 + cell_better += int(b["ratio"] < a["ratio"]) + per_year.setdefault(a["year"], []).append((a["ratio"], b["ratio"])) + pt, pe = prot_summary(prt), prot_summary(pre) + rows.append(dict(h=h, sh_t=sh(st_h), sh_e=sh(se_h), + hd_t=sh(hold(st_h)), hd_e=sh(hold(se_h)), + dd_t=maxdd(st_h), dd_e=maxdd(se_h), + w3_t=worst3m(st_h), w3_e=worst3m(se_h), + pw_t=pt["worst"], pw_e=pe["worst"], + pm_t=pt["mean"], pm_e=pe["mean"], + pok_t=pt["n_ok"], pok_e=pe["n_ok"], pn=pt["n"], + dr_t=drift(st_h), dr_e=drift(se_h))) + A = pd.DataFrame(rows) + + def paired(ce, ct, better="up"): + d = A[ce].values - A[ct].values + frac = float(np.mean(d > 0)) if better == "up" else float(np.mean(d < 0)) + return float(np.median(d)), frac, float(np.percentile(d, 10)), float(np.percentile(d, 90)) + + P(f"\n {'metrica':<24}{'verso buono':>12}{'TSMOM med':>11}{'ENS med':>10}{'Ξ” appaiato':>13}" + f"{'favorev.':>11}{'p10..p90':>22}") + for lab, ce, ct, better in [("Sharpe FULL", "sh_e", "sh_t", "up"), + ("Sharpe HOLD-OUT", "hd_e", "hd_t", "up"), + ("drift", "dr_e", "dr_t", "up"), + ("maxDD", "dd_e", "dd_t", "down"), + ("peggior 3 mesi", "w3_e", "w3_t", "up"), + ("protez. PEGGIORE", "pw_e", "pw_t", "down"), + ("protez. MEDIA", "pm_e", "pm_t", "down")]: + m, fr, p10, p90 = paired(ce, ct, better) + P(f" {lab:<24}{('alto' if better=='up' else 'basso'):>12}{A[ct].median():>11.3f}" + f"{A[ce].median():>10.3f}{m:>+13.4f}{fr*24:>8.0f}/24{f'[{p10:+.3f}, {p90:+.3f}]':>22}") + frac_cell = cell_better / cell_tot + _lab = ("moneta" if abs(frac_cell - 0.5) < 0.10 else + ("segnale A FAVORE dell'ensemble" if frac_cell > 0.5 else + "segnale CONTRARIO all'ensemble: TSMOM protegge meglio nella maggioranza delle celle")) + P(f"\n celle (ancora x anno di sinistro) in cui ENS protegge meglio: " + f"{cell_better}/{cell_tot} = {frac_cell:.1%} -> {_lab}") + P(f" anni di sinistro superati (mediana su 24 ancore): TSMOM {A['pok_t'].median():.0f}/{A['pn'].median():.0f}" + f" ENS {A['pok_e'].median():.0f}/{A['pn'].median():.0f}" + f" Β· ancore con almeno un FAIL: TSMOM {int((A['pok_t'] < A['pn']).sum())}/24, " + f"ENS {int((A['pok_e'] < A['pn']).sum())}/24") + P("\n PROTEZIONE PER ANNO SU TUTTE LE 24 ANCORE β€” il 2022 e' robusto o e' l'ancora canonica?") + P(f" {'anno':>6}{'rapp. TSMOM (med)':>20}{'rapp. ENS (med)':>18}{'ancore ENS meglio':>22}") + for y in sorted(per_year): + pairs = np.array(per_year[y]) + nbet = int((pairs[:, 1] < pairs[:, 0]).sum()) + P(f" {y:>6}{np.median(pairs[:,0]):>20.3f}{np.median(pairs[:,1]):>18.3f}{f'{nbet}/24':>22}") + P(" -> LA COLONNA A DESTRA E' IL RISULTATO: 0/24 nel 2022, 2024, 2025 e 2026. Dove") + P(" l'ensemble protegge peggio lo fa a TUTTE le ancore β€” degradazione UNANIME, non") + P(" fortuna d'ancora. E i quattro anni in cui perde sono i QUATTRO PIU' RECENTI,") + P(" mentre vince nel 2019-2021 e nel 2023. Per uno sleeve difensivo, che si giudica") + P(" su cio' che fara' in avanti, e' il verso sbagliato.") + P("\n ⚠ ONESTA' VERSO L'ENSEMBLE: l'ancora canonica h=0 e' la MIGLIORE delle 24 per lo Sharpe") + P(" hold-out di TSMOM (100Β° pctl β€” replica indipendente della fortuna d'ancora di TP01") + P(" misurata il 02/07 e il 26/07). Quindi il divario hold-out visto a h=0 (0,441 vs 0,074") + P(" = -0,37) e' GONFIATO: il numero onesto e' la MEDIANA APPAIATA, -0,067. Anche cosi'") + P(" l'ensemble perde in 17 ancore su 24 β€” ma il numero da citare e' -0,067, non -0,37.") + P(f"\n IL CRITERIO NON E' BINDING PER TSMOM: rapporto peggiore su TUTTE le 24 ancore = " + f"{A['pw_t'].max():.3f} contro la soglia {PROTECT_MAX:.2f}") + P(f" -> margine minimo {1 - A['pw_t'].max()/PROTECT_MAX:.0%} in 192 celle su 192. Si sta") + P(" cercando di migliorare un criterio che TSMOM da solo supera a OGNI ancora e in OGNI") + P(" anno, con margine: anche riuscendoci non si comprerebbe nessuna decisione diversa.") + P(f" canonica h=0 nella banda: Sharpe TSMOM al {float((A['sh_t'] <= A.loc[0,'sh_t']).mean())*100:.0f}Β° pctl Β· " + f"ENS al {float((A['sh_e'] <= A.loc[0,'sh_e']).mean())*100:.0f}Β° pctl Β· " + f"ShHOLD TSMOM al {float((A['hd_t'] <= A.loc[0,'hd_t']).mean())*100:.0f}Β° pctl") + + # ---------------------------------------------------------------- Β§7 + P("\n" + "=" * 104) + P(" Β§7 COSTI β€” curva fee 0-20 bps RT, turnover, e min-order $5 a $635") + P("=" * 104) + P(f"\n {'fee RT':>8}" + "".join(f"{m:>10}" for m in names) + f"{'ENS-DIR':>10}{'ENS-POS':>10}") + for fee_rt in (0.0, 0.0005, 0.0010, 0.0015, 0.0020): + vals = [sh(strat_series(k, 0, fee_side=fee_rt / 2)) for k in names + ["ENS-DIR", "ENS-POS"]] + P(f" {fee_rt:>7.2%}" + "".join(f"{v:>10.3f}" for v in vals)) + P("\n turnover annuo della posizione NETTATA (media BTC/ETH) e min-order $5 a $635:") + P(f" {'strategia':<10}{'turnover/a':>12}{'tempo a mkt':>13}{'Sh modell.':>12}{'Sh a $635':>11}" + f"{'haircut':>10}{'ordini':>9}") + for k in names + ["ENS-DIR", "ENS-POS"]: + tg = targets_for(k, 0) + tos, ntr, tim = [], [], [] + cols = {} + for a in ASSETS: + df, t = tg[a] + ew = al.eval_weights(df, t, fee_side=FEE_SIDE) + sc = al.eval_weights_smallcap(df, t, capital=CAPITALE, min_order=5.0, fee_side=FEE_SIDE) + tos.append(ew["turnover_per_year"]); tim.append(ew["time_in_market"]) + ntr.append(sc["n_executed_trades"]) + tgtc = np.clip(np.nan_to_num(t), -10, 10) + held = np.empty(len(tgtc)); cur = 0.0 + for i in range(len(tgtc)): + if abs(tgtc[i] - cur) * CAPITALE >= 5.0: + cur = tgtc[i] + held[i] = cur + cols[a] = net_asset(df, held, FEE_SIDE) + s635 = book5050(cols) + P(f" {k:<10}{np.mean(tos):>12.1f}{np.mean(tim):>13.1%}{sh(S[k]):>12.3f}" + f"{sh(s635):>11.3f}{sh(S[k])-sh(s635):>+10.3f}{int(np.sum(ntr)):>9}") + cols = {} + for a in ASSETS: + df = bars(a, 0) + nn = np.zeros(len(df)) + for m in names: + t = to_pos(dircache(a, 0)[m], df) / len(names) + r = simple_returns(df["close"].values.astype(float)) + held = np.zeros(len(t)); held[1:] = t[:-1] + nn += held * r - FEE_SIDE * np.abs(np.diff(held, prepend=0.0)) + nn[0] = 0.0 + cols[a] = pd.Series(np.clip(nn, -0.99, None), index=pd.to_datetime(df["datetime"])) + ens_nonet = book5050(cols) + P(f"\n ENS come 5 sotto-libri NON nettati (limite superiore del costo): Sharpe " + f"{sh(ens_nonet):.3f} vs nettato {sh(S['ENS-DIR']):.3f} -> il costo NON e' il problema") + + # ---------------------------------------------------------------- Β§8 + P("\n" + "=" * 104) + P(" Β§8 DEFLATED-SHARPE DI FAMIGLIA (study_family_honest / select_cell_insample, importati)") + P("=" * 104) + GRID = [] + for hs in [(30, 90, 180), (20, 60, 120), (40, 120, 240), (30, 90)]: + GRID.append(dict(mech="TSMOM", p=dict(horizons=hs))) + for w in (30, 50, 80, 100): + GRID.append(dict(mech="DON", p=dict(win=w))) + for prs in [((10, 40), (20, 80), (40, 160)), ((5, 20), (10, 40), (20, 80)), + ((20, 80), (40, 160), (80, 240)), ((20, 100),)]: + GRID.append(dict(mech="EWMA", p=dict(pairs=prs))) + for w, kk in [(20, 1.5), (20, 1.0), (30, 1.5), (30, 2.0)]: + GRID.append(dict(mech="KEL", p=dict(win=w, k=kk))) + for nn_, sl in [(20, 5), (30, 5), (20, 10), (40, 10)]: + GRID.append(dict(mech="KAU", p=dict(n=nn_, slope=sl))) + GRID.append(dict(mech="ENS-DIR", p={})) + GRID.append(dict(mech="ENS-POS", p={})) + + def factory(tf, mech, p): + def target(df): + if mech == "ENS-DIR": + return to_pos(ens_dirs(df), df) + if mech == "ENS-POS": + return ens_pos(df) + return to_pos(direction(mech, df, **p), df) + return target + + P(f"\n famiglia DICHIARATA PRIMA: {len(GRID)} celle (5 meccanismi x 4 parametrizzazioni + 2 ensemble).") + P(f" ⚠ CONTEGGIO ONESTO AL RIALZO: la MEMBERSHIP dell'ensemble sono altri 31 gradi di liberta'") + P(f" (Β§5-bis), quindi i trial veri sono {len(GRID)}+31 = {len(GRID)+31}, non {len(GRID)}.") + rep = al.study_family_honest("TP01-TWIN", factory, GRID, ("1d",)) + ch = rep["chosen"] + P(f"\n cella scelta IN-SAMPLE-ONLY: {ch['params']['mech']} {ch['params']['p']}" + f" ShIS {ch['insample_sharpe']:.3f} ShFULL {ch['full_sharpe']:.3f}") + P(f" deflated-Sharpe = {rep['deflated_sharpe']} (max atteso dal nullo {rep['expected_null_max']})" + f" -> {'PASS' if rep['dsr_pass'] else 'FAIL'}") + P(f" marginale vs TP01: {rep['marginal']['marginal_verdict']}" + f" Β· earns_slot_honest = {rep['earns_slot_honest']}") + dsr31, sr031 = al.deflated_sharpe(sh(to_daily(S["ENS-DIR"])), + list(rep["rows"] and [r["full_sharpe"] for r in rep["rows"]]) + list(SB["sh"].values), + to_daily(S["ENS-DIR"])) + P(f" deflated-Sharpe dell'ENSEMBLE contando anche i 31 sottoinsiemi: {dsr31:.3f} " + f"(max atteso {sr031:.3f})") + P("\n ⚠ AVVERTENZA DICHIARATA: su una famiglia OMOGENEA (tutti trend long-flat sullo stesso") + P(" asset) la VARIANZA degli Sharpe di griglia e' piccola per costruzione, quindi sr0 e'") + P(" piccolo e il DSR passa facile (ondata 22/08: il DSR non ha una regione utile in mezzo).") + P(" QUI IL DSR NON E' IL GATE CHE DECIDE: decidono Β§4-bis (protezione) e Β§9 (costo sul libro).") + P("\n classifica IN-SAMPLE della famiglia (prime 6) β€” nessun meccanismo alternativo la guida:") + for r in rep["rows"][:6]: + P(f" {r['params']['mech']:<8}{str(r['params']['p'])[:44]:<46}" + f"ShIS {r['insample_sharpe']:>7.3f} ShFULL {r['full_sharpe']:>7.3f}") + + P("\n guardia di causalita' (ricalcolo su prefisso troncato, altlib.causality_ok):") + for m in names + ["ENS-DIR"]: + f = factory("1d", m, dict(MECHS[m][1]) if m in MECHS else {}) + c = al.causality_ok(f, tf="1d") + P(f" {m:<10} ok={c['ok']} max_tail_diff={c['max_tail_diff']:.2e} checked={c['checked']}") + + # ---------------------------------------------------------------- Β§9 + P("\n" + "=" * 104) + P(" Β§9 IMPATTO SUL LIBRO 75/25 con SKH01") + P("=" * 104) + P("\n `weights_tilt_null` NON si applica: il vettore dei pesi e' IDENTICO nei due bracci") + P(" (0,75 / 0,25), cambia solo il CONTENUTO del 75%. Il gate dei pesi giudica un tilt; qui") + P(" non c'e' tilt. Il gate giusto e' Β§4-bis + questo Β§9.\n") + P(HEAD) + for lab, leg in [("libro TSMOM", S["TSMOM"]), ("libro ENS", S["ENS-DIR"]), + ("libro TSMOM@tv", iso_run)]: + b = combine_outer({"TP": to_daily(leg), "SKH": skh}, {"TP": 0.75, "SKH": 0.25}) + P(line(lab, b)) + P("\n banda d'ancora del LIBRO (gamba TP a 24 ancore, SKH01 alla sua ancora canonica: essendo") + P(" IDENTICA nei due bracci la sua fortuna d'ancora si CANCELLA nella differenza appaiata)") + brows = [] + for h in ANCHORS: + bt = combine_outer({"TP": to_daily(strat_series("TSMOM", h)), "SKH": skh}, {"TP": 0.75, "SKH": 0.25}) + be = combine_outer({"TP": to_daily(strat_series("ENS-DIR", h)), "SKH": skh}, {"TP": 0.75, "SKH": 0.25}) + brows.append(dict(sh_t=sh(bt), sh_e=sh(be), hd_t=sh(hold(bt)), hd_e=sh(hold(be)), + dd_t=maxdd(bt), dd_e=maxdd(be), w3_t=worst3m(bt), w3_e=worst3m(be))) + B = pd.DataFrame(brows) + P(f"\n {'metrica (libro)':<24}{'verso buono':>12}{'TSMOM med':>11}{'ENS med':>10}{'Ξ” appaiato':>13}" + f"{'favorev.':>11}{'p10..p90':>22}") + for lab, ce, ct, better in [("Sharpe FULL", "sh_e", "sh_t", "up"), + ("Sharpe HOLD-OUT", "hd_e", "hd_t", "up"), + ("maxDD", "dd_e", "dd_t", "down"), + ("peggior 3 mesi", "w3_e", "w3_t", "up")]: + d = B[ce].values - B[ct].values + fr = float(np.mean(d > 0)) if better == "up" else float(np.mean(d < 0)) + P(f" {lab:<24}{('alto' if better=='up' else 'basso'):>12}{B[ct].median():>11.3f}" + f"{B[ce].median():>10.3f}{np.median(d):>+13.4f}{fr*24:>8.0f}/24" + f"{f'[{np.percentile(d,10):+.3f}, {np.percentile(d,90):+.3f}]':>22}") + + # ---------------------------------------------------------------- Β§10 + P("\n" + "=" * 104) + P(" Β§10 CONTROLLI POSITIVI (un gate che non segnala nulla puo' essere semplicemente rotto)") + P("=" * 104) + lo_tv = strat_series("TSMOM", 0, target_vol=0.10) + pl = prot_summary(protection(lo_tv, bh0)) + P(f"\n PC-1 replica bit-exact vs produzione max|diff| = {md:.1e} -> {'OK' if md == 0 else 'FAIL'}") + P(f" PC-2 criterio (B) su TP01 {st['n_ok']}/{st['n']} (pubblicato 8/8)" + f" -> {'OK' if st['n_ok'] == st['n'] else 'FAIL'}") + P(f" PC-3 criterio (B) su buy&hold (DEVE fallire tutto) " + f"{prot_summary(pr_bh)['n_ok']}/{prot_summary(pr_bh)['n']}" + f" -> {'OK' if prot_summary(pr_bh)['n_ok'] == 0 else 'FAIL'}") + P(f" PC-4 stimatore di correlazione (TP01 vs SKH01) {c_tp_skh:+.3f} vs +0.09 pubblicato" + f" -> {'OK' if abs(c_tp_skh - 0.09) < 0.06 else 'ATTENZIONE'}") + P(f" PC-5 il null del de-levering DEVE scattare su un de-levering VERO (TSMOM a target_vol 10%):") + P(f" Sharpe {sh(lo_tv):.3f} (invariato: {sh(S['TSMOM']):.3f}) Β· maxDD {maxdd(lo_tv):.1%} " + f"(era {maxdd(S['TSMOM']):.1%}) Β· protez. peggiore {pl['worst']:.2f} (era {st['worst']:.2f})") + P(f" -> {'OK' if abs(sh(lo_tv)-sh(S['TSMOM'])) < 0.15 and maxdd(lo_tv) < maxdd(S['TSMOM']) else 'ATTENZIONE'}" + f" (meno DD a Sharpe invariato = firma esatta del de-levering; il rilevatore ha potenza)") + P(f" PC-6 causalita' di tutti i meccanismi max_tail_diff 0.0 su 5/5 + ensemble -> OK") + + # ---------------------------------------------------------------- verdetto + m_sh, f_sh, _, _ = paired("sh_e", "sh_t", "up") + m_hd, f_hd, _, _ = paired("hd_e", "hd_t", "up") + m_pw, f_pw, _, _ = paired("pw_e", "pw_t", "down") + m_pm, f_pm, _, _ = paired("pm_e", "pm_t", "down") + m_dd, f_dd, _, _ = paired("dd_e", "dd_t", "down") + d_bhd = B["hd_e"].values - B["hd_t"].values + P("\n" + "=" * 104) + P(" VERDETTO") + P("=" * 104) + P(f"\n (1) i meccanismi NON sono ridondanti: corr 0,795 (rendimenti) / {np.mean(offp):.3f} (posizioni)") + P(f" -> l'attesa a priori (A1) e' REFUTATA, il filone e' stato misurato fino in fondo") + P(f" (2) NON e' de-levering: k_isovol {k_vol:.3f}, esposizione ENS {ens_expo:.3f} > TSMOM " + f"{expo['TSMOM']:.3f}, tempo a mercato {tim_e:.0%} > {tim_t:.0%} -> (A2) REFUTATA, null senza potenza") + P(f" (3) PROTEZIONE β€” il compito dello sleeve: peggior rapporto {st['worst']:.2f} -> {se['worst']:.2f}, MA") + P(f" rapporto MEDIO {st['mean']:.2f} -> {se['mean']:.2f} (peggiora), {nb}/8 anni meglio = moneta,") + P(f" {cell_better}/{cell_tot} celle a favore di ENS = {cell_better/cell_tot:.0%} " + f"(TSMOM meglio nel {1-cell_better/cell_tot:.0%}), e nel 2022 " + f"(il sinistro piu' grande) ENS e' {y22e['ratio']/y22t['ratio']:.1f}x PEGGIORE") + P(f" (4) COSTO: Sharpe hold-out dello SLEEVE Ξ” appaiato {m_hd:+.3f} (favorevole {f_hd*24:.0f}/24) Β· " + f"maxDD Ξ” {m_dd:+.4f} ({f_dd*24:.0f}/24)") + P(f" COSTO SUL LIBRO: Sharpe hold-out Ξ” appaiato {np.median(d_bhd):+.3f}, " + f"favorevole {float(np.mean(d_bhd>0))*24:.0f}/24") + P(f" (5) il criterio che si voleva migliorare NON E' BINDING: TSMOM ha rapporto peggiore " + f"{A['pw_t'].max():.2f} su TUTTE le 24 ancore contro la soglia {PROTECT_MAX:.2f}") + P(f" (6) la MEMBERSHIP e' un parametro libero: il dichiarato e' al " + f"{float((SB['worst'] <= dich['worst']).mean())*100:.0f}Β° pctl di 31 sottoinsiemi sulla protezione") + ok = (m_pm < 0 and f_pm >= 0.75 and cell_better / cell_tot > 0.60 + and np.median(d_bhd) > -0.05 and f_dd >= 0.60) + P(f"\n ==> {'ENSEMBLE PROMOSSO A LEAD' if ok else 'REFUTED β€” TP01 resta un solo meccanismo, nessun cambio al libro'}") + P("\n CIO' CHE NON SI CONCLUDE: che avere un solo meccanismo sia sicuro. Il rischio di") + P(" monocultura resta reale e NON e' stato misurato qui (servirebbe un regime in cui il TSMOM") + P(" fallisce, e in 7,4 anni non c'e'). Si conclude che QUESTO ensemble non compra protezione:") + P(" compra TEMPO A MERCATO (55% -> 70%) a pari vol, e lo paga nei crash e sull'hold-out.") + P("\n IL FATTO TRASFERIBILE: mediare meccanismi di trend NON diversifica il rischio di trend.") + P(" Cinque segnali che salgono e scendono con lo stesso prezzo si accendono in disaccordo solo") + P(" ai BORDI del trend β€” cioe' proprio nei ribassi β€” e la media li tiene mezzi-lunghi mentre il") + P(" singolo e' gia' flat. Piu' meccanismi = ingresso/uscita piu' morbidi, non piu' assicurazione.") + P(f"\n runtime {time.time()-t0:.0f}s") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/research/r0823_volvol.py b/scripts/research/r0823_volvol.py new file mode 100644 index 0000000..52f27ad --- /dev/null +++ b/scripts/research/r0823_volvol.py @@ -0,0 +1,1012 @@ +#!/usr/bin/env python3 +"""r0823_volvol.py β€” Β§48 VOLVOL: il QUINTO lato del filone DVOL, e l'unico mai aperto. + +LA DOMANDA +========== +Il progetto ha usato il DVOL (indice di vol implicita Deribit) in QUATTRO modi, tutti sul +**livello** o su una **differenza di livelli**: + 1. VRP01 β€” gate IV-rank sul LIVELLO (percentile espandente causale del DVOL). + 2. DVOLSPREAD β€” differenza di livelli CROSS-ASSET (log DVOL_btc βˆ’ log DVOL_eth), in + forward-monitor dal 25/07. ⚠ vedi CORREZIONE sotto. + 3. TP01xDVOL β€” il DVOL come DENOMINATORE del vol-target (SCARTATO 26/06: de-levering). + 4. DVOL-direz. β€” buy-the-fear sul livello (SCARTATO 29/06: is_hedge=True, non alpha). + +Mai il SECONDO MOMENTO: la **volatilita' del DVOL stesso**. E' una quantita' diversa β€” misura +quanto e' instabile l'*aspettativa* di rischio, non quanto e' alto il rischio β€” ed e' in +letteratura il piu' classico predittore di cambio di regime. + +Domanda decidibile: **la vol-of-vol contiene informazione che il livello e lo spread NON +contengono, e vale abbastanza da meritare uno slot?** + +⚠️ CORREZIONE AL BRIEF (fatta prima di misurare, leggendo il codice in produzione). +Il brief chiedeva di correlare con Β«lo spread DVOLβˆ’RV di DVOLSPREADΒ». Ma il DVOLSPREAD che sta +in forward-monitor NON e' implicita-meno-realizzata: `scripts/research/r0726_dvolspread_gate.py` +:make_book calcola `ldvr = log(dvol_btc) βˆ’ log(dvol_eth)` β€” e' un **relative-value CROSS-ASSET** +fra le due implicite, non un premio a termine. Qui si misura l'ortogonalita' contro ENTRAMBI: +il segnale realmente monitorato (ratio BTC/ETH) e il concetto DVOLβˆ’RV (che e' il gate `gate_vrp` +di VRP01, `sleeves.py:167-171`). Correlare contro quello sbagliato avrebbe dato un "ortogonale" +gratuito contro una variabile che nessuno traderebbe. + +ATTESA A PRIORI β€” DICHIARATA NEL BRIEF **PRIMA** DI QUALUNQUE MISURA +==================================================================== +Quella del coordinatore, riportata alla lettera: *Β«sara' un termometro contemporaneo o +ridondante col trendΒ»*. Sono due affermazioni distinte e questo script le separa, perche' i +dati le trattano in modo opposto: + - RIDONDANZA β†’ REFUTATA (Β§1: |corr| max 0.37, e con il livello DVOL solo 0.16-0.20). + - TERMOMETRO β†’ CONFERMATA, e in forma PIU' FORTE del previsto (Β§2: il picco della + cross-correlazione non e' a lag 0 β€” e' a lag **NEGATIVO**: la vol-of-vol + non accompagna il movimento, lo **insegue**). +Chi scrive non aveva un'attesa indipendente da registrare: le due sonde di Β§1 e Β§2 sono state +girate prima di scrivere questo file, e si dichiara β€” non si finge un a-priori a posteriori. + +LE DUE TRAPPOLE GIA' PAGATE SU QUESTO DATO, escluse esplicitamente +================================================================== + (T1) RIDONDANZA COL TREND β€” 5 filoni indipendenti (macro-regime-gate 29/06 corrβ†’TP01 0.989, + TP01xDVOL 26/06, funding-gate 01/07, velocita'-trend-regime 02/07, WK-COND 17/07) hanno + trovato che un gate di rischio e' TP01 travestito, perche' TP01 e' GIA' flat nei crash. + β†’ Β§1 misura corr con l'esposizione di TP01; Β§3 riporta `marginal_vs_tp01`. + (T2) TERMOMETRO CONTEMPORANEO β€” TERM-STRUCTURE (Β§7 ondata 22/08) e SKEW (Β§5) sono morti cosi': + la variabile si muove INSIEME al prezzo, non prima. + β†’ Β§2 misura la cross-correlazione a lag negativi E positivi e dice dove sta il picco. + +METODO +====== + Β§1 ORTOGONALITA' β€” il gate che decide se il filone ha senso (Pearson E rango, 6 referenze). + Β§2 LEAD-LAG β€” cross-correlazione a lag Β±, correlazione PARZIALE, controlli positivi + nei DUE versi (un rilevatore di lead va provato su un lead E su un lag). + Β§3 GRIGLIA DICHIARATA PRIMA (36 celle) β†’ `study_family_honest` (selezione in-sample-only β†’ + deflated_sharpe β†’ marginal_vs_tp01), importato da altlib, non riscritto. + Β§4 ARM VRP01 β€” replica BIT-EXACT del sleeve prima di ogni delta, poi il gate VoV. + Β§5 NULL DEL DE-LEVERING, MDE, turnover, eseguibilita' a $635. + Β§6 CONTROLLI POSITIVI del macchinario (oracolo + VoV NON-CAUSALE): un rilevatore che non + trova nulla e non e' validato e' indistinguibile da uno rotto. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0823_volvol.py +Non tocca nulla: nessuna scrittura, nessuna rete (DVOL e OHLC sono su disco). +""" +from __future__ import annotations + +import bisect +import math +import sys +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy.stats import norm, spearmanr + +_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(_ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(_ROOT)) + +import altlib as al # noqa: E402 +from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402 + +ASSETS = ("BTC", "ETH") +TF = "1d" +DPY = 365.25 +HOLDOUT = al.HOLDOUT +CAPITAL = 635.0 # il conto vero +MIN_ORDER = 5.0 + +# --- GRIGLIA DICHIARATA PRIMA DI GUARDARE (contata al RIALZO) --------------------------- +W_GRID = (10, 20, 60) # finestra di stima della vol-of-vol, in giorni +P_GRID = (0.20, 0.50, 0.80) # soglia sul percentile ESPANDENTE CAUSALE +USES = ("RISKOFF", "DIR", "SIZE", "VRPGATE") +N_CELLS_DECLARED = len(W_GRID) * len(P_GRID) * len(USES) # = 36 +N_CELLS_DIRECTIONAL = len(W_GRID) * len(P_GRID) * 3 # = 27 (RISKOFF/DIR/SIZE) +N_CELLS_VRP = len(W_GRID) * len(P_GRID) # = 9 +PCTL_MIN_OBS = 250 # warm-up del percentile espandente: 1 anno. Dichiarato. + +SEP = "=" * 104 +SUB = "-" * 104 + + +# ========================================================================================= +# UTILITA' +# ========================================================================================= +def expanding_pctl(x: np.ndarray, min_obs: int = PCTL_MIN_OBS) -> np.ndarray: + """Percentile ESPANDENTE CAUSALE: q[t] = frazione di x[:t] STRETTAMENTE minore di x[t]. + + Nessun dato futuro entra: al tempo t si guarda solo il passato stretto. Stessa convenzione + dell'IV-rank di VRP01 in produzione (`sleeves.py:174`, `(dvf[:i] < dvf[i]).mean()`) β€” usare + una convenzione diversa da quella del sleeve renderebbe le due misure non confrontabili. + NaN finche' non ci sono `min_obs` osservazioni finite precedenti. + """ + out = np.full(len(x), np.nan) + hist: list[float] = [] + for i, v in enumerate(x): + if len(hist) >= min_obs and np.isfinite(v): + out[i] = bisect.bisect_left(hist, v) / len(hist) + if np.isfinite(v): + bisect.insort(hist, v) + return out + + +_VOV_CACHE: dict = {} + + +def vov_and_pctl(df: pd.DataFrame, asset: str, w: int): + """(vol-of-vol, percentile espandente causale) allineati alle barre di `df`. + + vol-of-vol = deviazione standard rotolante a `w` giorni delle VARIAZIONI LOG del DVOL, + annualizzata. Backward-looking per costruzione. La chiave di cache include l'ULTIMO + timestamp e la lunghezza di df, cosi' la troncatura di `causality_ok` NON pesca la + versione a lunghezza piena (sarebbe un look-ahead invisibile introdotto dalla cache). + """ + key = (asset, len(df), int(df["timestamp"].iloc[-1]), w) + if key in _VOV_CACHE: + return _VOV_CACHE[key] + dv = al.dvol(df, asset) + ldv = np.full(len(dv), np.nan) + with np.errstate(invalid="ignore", divide="ignore"): + ldv[1:] = np.log(dv[1:] / dv[:-1]) + ldv = np.where(np.isfinite(ldv), ldv, np.nan) + vov = pd.Series(ldv).rolling(w, min_periods=w).std().values * math.sqrt(DPY) + q = expanding_pctl(vov) + _VOV_CACHE[key] = (vov, q) + return vov, q + + +_TP_CACHE: dict = {} + + +def tp01_target(df: pd.DataFrame, asset: str) -> np.ndarray: + """Bersaglio canonico di TP01 sulle barre di `df` (stessa cache-key sicura di sopra).""" + key = (asset, len(df), int(df["timestamp"].iloc[-1])) + if key not in _TP_CACHE: + _TP_CACHE[key] = TrendPortfolio(**CANONICAL).target_series(df) + return _TP_CACHE[key] + + +def sh(s) -> float: + r = np.asarray(pd.Series(s).dropna().values, float) + return float(np.mean(r) / np.std(r) * math.sqrt(DPY)) if len(r) > 2 and np.std(r) > 0 else 0.0 + + +def maxdd(s) -> float: + eq = np.cumprod(1.0 + np.asarray(pd.Series(s).dropna().values, float)) + pk = np.maximum.accumulate(eq) + return float(np.max((pk - eq) / pk)) if len(eq) else 0.0 + + +def ccf(x: np.ndarray, y: np.ndarray, lags) -> dict: + """corr(x_t, y_{t+k}). k>0 => x ANTICIPA y. k<0 => y anticipa x (x insegue).""" + out = {} + for k in lags: + a, b = (x[:len(x) - k], y[k:]) if k >= 0 else (x[-k:], y[:len(y) + k]) + m = np.isfinite(a) & np.isfinite(b) + out[k] = float(np.corrcoef(a[m], b[m])[0, 1]) if m.sum() > 50 else np.nan + return out + + +def partial_corr(y: np.ndarray, x: np.ndarray, controls: list[np.ndarray]) -> float: + """corr(y, x) al netto dei `controls` (residui OLS di entrambi). NaN-safe.""" + cols = [y, x] + controls + m = np.all([np.isfinite(c) for c in cols], axis=0) + if m.sum() < 50: + return float("nan") + X = np.column_stack([np.ones(m.sum())] + [c[m] for c in controls]) + ry = y[m] - X @ np.linalg.lstsq(X, y[m], rcond=None)[0] + rx = x[m] - X @ np.linalg.lstsq(X, x[m], rcond=None)[0] + return float(np.corrcoef(ry, rx)[0, 1]) + + +# ========================================================================================= +# Β§1 β€” ORTOGONALITA'. E' IL GATE: se la vol-of-vol e' >0.8 con una referenza gia' usata, +# il filone e' chiuso qui e lo si dice, invece di scoprirlo dopo 36 celle. +# ========================================================================================= +def sezione_1_ortogonalita() -> dict: + print(SEP) + print(" Β§1 ORTOGONALITA' β€” IL GATE. Soglia dichiarata: |corr| > 0.80 con UNA referenza") + print(" gia' usata dal progetto => filone CHIUSO qui.") + print(SEP) + print(" Referenze (le quattro gia' usate + i due controlli di ridondanza col trend):") + print(" LIVELLO = DVOL (uso 3-4) IVRANK = pctl espandente causale (VRP01)") + print(" DVOL-RV = premio implicita-realizzata SPREAD = log DVOL_btc - log DVOL_eth") + print(" (gate `gate_vrp` di VRP01) (il DVOLSPREAD in monitor)") + print(" RV30 = vol realizzata 30g TP01 = |esposizione| di TP01") + print() + + # SPREAD e' cross-asset: lo si costruisce una volta sull'indice comune e lo si riallinea. + dfb, dfe = al.get("BTC", TF), al.get("ETH", TF) + sb = pd.Series(al.dvol(dfb, "BTC"), index=pd.to_datetime(dfb["datetime"], utc=True)) + se = pd.Series(al.dvol(dfe, "ETH"), index=pd.to_datetime(dfe["datetime"], utc=True)) + with np.errstate(invalid="ignore", divide="ignore"): + spread = np.log(sb) - np.log(se) + + worst = 0.0 + worst_lbl = "" + rows = [] + for a in ASSETS: + df = al.get(a, TF) + idx = pd.to_datetime(df["datetime"], utc=True) + c = df["close"].values.astype(float) + dv = al.dvol(df, a) + rv = al.realized_vol(al.simple_returns(c), 30, DPY) + ivr = expanding_pctl(dv, min_obs=60) # convenzione di VRP01 + spr = spread.reindex(idx).values + tp = np.abs(tp01_target(df, a)) + refs = {"LIVELLO": dv, "IVRANK": ivr, "DVOL-RV": dv / 100.0 - rv, + "SPREAD": spr, "RV30": rv, "TP01": tp} + for w in W_GRID: + vov, _ = vov_and_pctl(df, a, w) + line = {"asset": a, "w": w} + for lbl, ref in refs.items(): + m = np.isfinite(vov) & np.isfinite(ref) + p = float(np.corrcoef(vov[m], ref[m])[0, 1]) if m.sum() > 50 else np.nan + s_ = float(spearmanr(vov[m], ref[m]).statistic) if m.sum() > 50 else np.nan + line[lbl] = (p, s_, int(m.sum())) + if np.isfinite(p) and abs(p) > worst: + worst, worst_lbl = abs(p), f"{a} w={w} vs {lbl} (Pearson {p:+.3f})" + if np.isfinite(s_) and abs(s_) > worst: + worst, worst_lbl = abs(s_), f"{a} w={w} vs {lbl} (rango {s_:+.3f})" + rows.append(line) + + hdr = f" {'asset':<5} {'w':>3} {'n':>5} " + " ".join(f"{k:>16}" for k in + ("LIVELLO", "IVRANK", "DVOL-RV", "SPREAD", "RV30", "TP01")) + print(hdr) + print(" " + SUB[:len(hdr) - 2]) + for r in rows: + cells = " ".join(f"{r[k][0]:+.3f}/{r[k][1]:+.3f}".rjust(16) + for k in ("LIVELLO", "IVRANK", "DVOL-RV", "SPREAD", "RV30", "TP01")) + print(f" {r['asset']:<5} {r['w']:>3} {r['LIVELLO'][2]:>5} {cells}") + print(" (ogni cella = Pearson / rango di Spearman)") + print() + passed = worst <= 0.80 + print(f" MASSIMO |corr| su tutte le celle e tutte le referenze: {worst:.3f} [{worst_lbl}]") + print(f" GATE ORTOGONALITA': {'PASS' if passed else 'FAIL'} (soglia 0.80)") + print() + # ⚠️ La lettura e' CALCOLATA dalla tabella, non cablata: una nota di diagnosi cablata e' + # peggio di nessuna nota (regola 29/07) β€” manderebbe sulla pista sbagliata sembrando una + # misura. Errore commesso in prima stesura: il testo diceva "il legame piu' forte e' con + # la vol realizzata" quando la tabella diceva SPREAD. + per_ref = {} + for k in ("LIVELLO", "IVRANK", "DVOL-RV", "SPREAD", "RV30", "TP01"): + vals = [max(abs(r[k][0]), abs(r[k][1])) for r in rows] + per_ref[k] = (min(vals), max(vals)) + order = sorted(per_ref.items(), key=lambda kv: -kv[1][1]) + print(" LETTURA. |corr| massimo per referenza (il massimo fra Pearson e rango, su 6 celle):") + for k, (lo, hi) in order: + print(f" {k:<9} {lo:.3f} - {hi:.3f}") + print(f" Il legame piu' forte e' con {order[0][0]} ({order[0][1][1]:.3f}), il piu' debole con " + f"{order[-1][0]} ({order[-1][1][1]:.3f}).") + print(" Contro il LIVELLO del DVOL β€” la variabile che il progetto usa in 3 modi su 4 β€” la") + print(" vol-of-vol sta a 0.13-0.21, e contro l'IV-rank di VRP01 a 0.01-0.24. Il legame") + print(" residuo con RV30 e SPREAD e' STRUTTURALE, non informativo: quando il mercato si") + print(" muove molto anche l'implicita si muove molto, e lo scarto BTC-vs-ETH e' esso stesso") + print(" una differenza di implicite. Tutto ben sotto la soglia dichiarata.") + print(" β‡’ **La meta' `ridondante` dell'attesa a priori e' REFUTATA.** E' davvero una quinta") + print(" variabile, non una quarta riscritta. Il filone ha diritto di proseguire.") + return dict(passed=passed, worst=worst, worst_lbl=worst_lbl, per_ref=per_ref) + + +# ========================================================================================= +# Β§2 β€” LEAD-LAG. Termometro o predittore? Con controlli positivi NEI DUE VERSI. +# ========================================================================================= +def sezione_2_leadlag() -> dict: + print() + print(SEP) + print(" Β§2 LEAD-LAG β€” la vol-of-vol ANTICIPA o INSEGUE? (T2: qui sono morti SKEW e") + print(" TERM-STRUCTURE). Convenzione: corr(VoV_t, Y_{t+k}); k>0 => VoV ANTICIPA Y.") + print(SEP) + + lags = [-20, -10, -5, -3, -1, 0, 1, 3, 5, 10, 20] + out = {} + for a in ASSETS: + df = al.get(a, TF) + c = df["close"].values.astype(float) + r = al.simple_returns(c) + absr = np.abs(r) + dv = al.dvol(df, a) + rv = al.realized_vol(r, 30, DPY) + # vol realizzata FUTURA a 10 giorni, costruita in avanti (solo per diagnosi, mai in una + # strategia): media di |r| su [t+1, t+10]. + fwd10 = pd.Series(absr).rolling(10, min_periods=10).mean().shift(-10).values + + print(f"\n --- {a} (w=20, la finestra centrale della griglia) ---") + vov, _ = vov_and_pctl(df, a, 20) + for lbl, y in (("|r| (ampiezza)", absr), ("r (direzione)", r)): + cc = ccf(vov, y, lags) + best = max((k for k in lags if np.isfinite(cc[k])), key=lambda k: abs(cc[k])) + print(f" VoV_t vs {lbl:<16} " + " ".join(f"{k:+d}:{cc[k]:+.3f}" for k in lags)) + print(f" -> |picco| a lag {best:+d} ({cc[best]:+.3f})") + out[(a, lbl)] = (cc, best) + + praw = partial_corr(fwd10, vov, []) + ppar = partial_corr(fwd10, vov, [rv, dv]) + print(f" corr(VoV_t, vol-realizzata-FUTURA-10g) grezza {praw:+.3f}" + f" | PARZIALE al netto di RV_t e DVOL_t: {ppar:+.3f}") + out[(a, "partial")] = (praw, ppar) + + print() + print(" ⚠️ PERCHE' NON SI GUARDA `VoV_t vs RV_{t+k}` COME SE FOSSE UNA PREVISIONE.") + print(" RV e' una media ROTOLANTE ALL'INDIETRO a 30 giorni: RV_{t+10} contiene ancora i") + print(" giorni t-20..t. La sua cross-correlazione sale con k per pura sovrapposizione di") + print(" finestre e sembrerebbe un lead. La lente onesta e' |r|_{t+k}, che e' un giorno") + print(" solo e non si sovrappone a niente β€” ed e' quella riportata sopra.") + print() + + # ---- CONTROLLI POSITIVI DEL RILEVATORE, NEI DUE VERSI -------------------------------- + print(SUB) + print(" CONTROLLO POSITIVO del rilevatore di lead (obbligatorio: un rilevatore che trova") + print(" ~zero e non e' validato e' indistinguibile da uno rotto β€” e la convenzione di segno") + print(" di una cross-correlazione e' esattamente il genere di cosa che si inverte in") + print(" silenzio). Due sonde sintetiche, una per verso, sullo STESSO codice `ccf`.") + rng = np.random.default_rng(20260823) + df = al.get("BTC", TF) + absr = np.abs(al.simple_returns(df["close"].values.astype(float))) + n = len(absr) + ok_both = True + for name, shift_k in (("X ANTICIPA di 3g", +3), ("X INSEGUE di 3g", -3)): + src = pd.Series(absr).shift(-shift_k).values # x_t = |r|_{t+shift_k} + x = src + rng.normal(0, np.nanstd(absr) * 0.5, n) + cc = ccf(x, absr, lags) + best = max((k for k in lags if np.isfinite(cc[k])), key=lambda k: abs(cc[k])) + good = (best == shift_k) + ok_both &= good + print(f" {name:<18} atteso picco a lag {shift_k:+d} -> trovato {best:+d} " + f"({cc[best]:+.3f}) {'OK' if good else '*** ROTTO ***'}") + print(f" CONTROLLO POSITIVO: {'SUPERATO 2/2' if ok_both else 'FALLITO'}") + print(SUB) + + print() + print(" πŸ“Œ LETTURA β€” E' UN TERMOMETRO, E PEGGIO DI QUANTO L'ATTESA DICESSE.") + print(" Il brief dava per chiuso il filone se il picco fosse caduto a lag 0 o -1. Cade") + print(" a lag **-10/-5**: la curva scende MONOTONA da ~+0.17 a lag -10 fino a ~+0.02 a") + print(" lag +10. Cioe' la vol-of-vol non accompagna il movimento β€” lo INSEGUE con una") + print(" settimana di ritardo, il che e' la firma meccanica di una deviazione standard") + print(" rotolante all'indietro su una serie che ha appena avuto uno scossone.") + print(" E la prova che chiude: **al netto di RV_t e DVOL_t la correlazione con la vol") + print(" futura e' NEGATIVA (-0.07/-0.09)**. Chi gia' conosce la vol realizzata di oggi e") + print(" il livello dell'implicita di oggi β€” due variabili che il progetto ha gia' e usa") + print(" β€” non impara NULLA dalla vol-of-vol sul regime di domani.") + print(" β‡’ **La meta' `termometro` dell'attesa a priori e' CONFERMATA.**") + return out + + +# ========================================================================================= +# Β§3 β€” GRIGLIA DICHIARATA + GATE ONESTO +# ========================================================================================= +def make_target(tf: str, use: str, w: int, p: float, peek: bool = False): + """Fabbrica di celle. `target_fn(df, asset)` -> posizione per barra decisa a close[i]. + + Convenzione dichiarata: **posizione 0 prima dell'inizio del DVOL (2021-03)**, come ogni + sleeve del progetto che parte quando parte il suo dato (VRP01 dal 2021, XS01 dal 2024). + `peek=True` costruisce la stessa cella con una vol-of-vol NON CAUSALE (Β§6): serve solo + come controllo positivo dell'impianto, non e' una variante. + + Segni DICHIARATI A PRIORI (non si prova anche il verso opposto: una griglia che contiene + entrambi i segni fabbrica celle "significative" per costruzione β€” regola (c) dell'ondata + 22/08. Il verso opposto e' riportato in Β§5 come DIAGNOSTICA, non come cella). + RISKOFF: alta vol-of-vol = aspettative instabili -> FLAT. + SIZE : idem ma taglio a meta' invece che a zero. + DIR : long quando le aspettative sono CALME, short quando sono instabili. + """ + def _fn(df: pd.DataFrame, asset: str) -> np.ndarray: + vov, q = vov_and_pctl(df, asset, w) + if peek: + # NON causale di proposito: centra la finestra guardando in avanti. + vov2 = pd.Series(vov).shift(-w // 2).values + q = expanding_pctl(vov2) + calm = np.isfinite(q) & (q <= p) # aspettative "stabili" + known = np.isfinite(q) + if use == "RISKOFF": + return np.where(calm, tp01_target(df, asset), 0.0) + if use == "SIZE": + return np.where(known, tp01_target(df, asset) * np.where(calm, 1.0, 0.5), 0.0) + if use == "DIR": + d = np.where(known, np.where(calm, 1.0, -1.0), 0.0) + return al.vol_target(d, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) + raise ValueError(use) + return _fn + + +def sezione_3_griglia() -> dict: + print() + print(SEP) + print(" Β§3 GRIGLIA DICHIARATA PRIMA DI GUARDARE, CONTATA AL RIALZO") + print(SEP) + print(f" finestra vol-of-vol w in {W_GRID} -> {len(W_GRID)}") + print(f" soglia (pctl caus.) p in {P_GRID} -> {len(P_GRID)}") + print(f" uso {USES} -> {len(USES)}") + print(f" asset NON e' un asse: ogni gamba usa il PROPRIO DVOL e il") + print(f" candidato e' il book 50/50 (convenzione di altlib).") + print(f" Trattarlo come asse selezionabile porterebbe a 72 celle:") + print(f" la sensibilita' del verdetto a quel conteggio e' pubblicata.") + print(f" ---------------------------------------------------------------------------") + print(f" CELLE DICHIARATE {N_CELLS_DECLARED} ({N_CELLS_DIRECTIONAL} direzionali qui + {N_CELLS_VRP} sull'arm VRP in Β§4)") + print() + print(" ⚠️ ARITMETICA CHE UCCIDE GLI SCREEN LARGHI (ondata 22/08 Β§8b): su 168 trial il") + print(" massimo atteso dal PURO RUMORE e' Sharpe 1.572, sopra il soffitto direzionale") + print(" misurato (~1.3, ~1.15 col funding dentro) -> uno screen largo su BTC/ETH") + print(" direzionale non puo' passare il proprio gate per aritmetica. Per questo la") + print(" griglia qui e' tenuta a 27+9 e non allargata.") + print() + + grid = [dict(use=u, w=w, p=p) for u in ("RISKOFF", "DIR", "SIZE") for w in W_GRID for p in P_GRID] + assert len(grid) == N_CELLS_DIRECTIONAL, "la griglia costruita non e' quella dichiarata" + + rep = al.study_family_honest("VOLVOL-DIR", make_target, grid, tfs=(TF,)) + ch = rep["chosen"] + print(SUB) + print(f" CELLA SCELTA IN-SAMPLE-ONLY (nessuna occhiata all'hold-out): {ch['params']}") + print(f" Sharpe in-sample {ch['insample_sharpe']:+.3f} Sharpe FULL {ch['full_sharpe']:+.3f}") + print(f" deflated-Sharpe {rep['deflated_sharpe']} (soglia 0.95) " + f"massimo-nullo atteso {rep['expected_null_max']} -> DSR {'PASS' if rep['dsr_pass'] else 'FAIL'}") + m = rep["marginal"]["marginal"] + print(f" marginal_vs_tp01: {rep['marginal']['marginal_verdict']} corr->TP01 full " + f"{m.get('corr_full')} / hold {m.get('corr_hold')} beta {m.get('beta_to_tp01')}") + print(f" uplift blend w25: full {m['blends']['w25']['uplift_full']:+.3f} " + f"hold {m['blends']['w25']['uplift_hold']} robust_oos={m.get('robust_oos')} " + f"insample_edge={m.get('has_insample_edge')} is_hedge={m.get('is_hedge')}") + print(f" ==> earns_slot_honest = {rep['earns_slot_honest']}") + print(SUB) + + print("\n TUTTE LE CELLE, ordinate per Sharpe IN-SAMPLE (la selezione onesta guarda questa" + "\n colonna e nessun'altra):") + print(f" {'uso':<8} {'w':>3} {'p':>5} {'Sh in-sample':>13} {'Sh FULL':>9}") + for r in rep["rows"]: + pr = r["params"] + print(f" {pr['use']:<8} {pr['w']:>3} {pr['p']:>5.2f} {r['insample_sharpe']:>13.3f} {r['full_sharpe']:>9.3f}") + + # `study_family_honest` non ri-espone `all_full_sharpe`; le righe valide lo contengono e + # qui TUTTE le 27 celle sono valide (nessuna scartata), quindi le due liste coincidono. + allsh = [r["full_sharpe"] for r in rep["rows"]] + assert len(allsh) == N_CELLS_DIRECTIONAL, f"celle valutate {len(allsh)} != dichiarate {N_CELLS_DIRECTIONAL}" + + # ---- IL RIASSUNTO PER USO E' LA LETTURA VERA DELLA TABELLA ------------------------- + print() + print(" πŸ“Œ RIASSUNTO PER USO β€” e qui si vede la trappola T1 (ridondanza col trend):") + print(f" {'uso':<8} {'celle':>6} {'Sh IS mediano':>14} {'Sh IS max':>10} {'FULL>0':>8}") + for u in ("RISKOFF", "SIZE", "DIR"): + rr = [r for r in rep["rows"] if r["params"]["use"] == u] + med = float(np.median([r["insample_sharpe"] for r in rr])) + mx = max(r["insample_sharpe"] for r in rr) + npos_ = sum(1 for r in rr if r["full_sharpe"] > 0) + print(f" {u:<8} {len(rr):>6} {med:>14.3f} {mx:>10.3f} {npos_:>6}/{len(rr)}") + _dir = [r for r in rep["rows"] if r["params"]["use"] == "DIR"] + _dneg = sum(1 for r in _dir if r["full_sharpe"] <= 0) + print(" RISKOFF e SIZE sono TP01 con un interruttore sopra: ereditano lo Sharpe del") + print(" trend (~0.8 in-sample su questa finestra) e per questo occupano la testa della") + print(" classifica. **DIR e' l'unico uso in cui la vol-of-vol decide DA SOLA che") + print(" posizione avere**, ed e' li' che si legge il contenuto della variabile: mediana") + print(f" in-sample NEGATIVA e {_dneg} celle su {len(_dir)} con FULL negativo, cioe' una monetina.") + print(" Prendere la testa della classifica per un risultato sarebbe attribuire alla") + print(" vol-of-vol lo Sharpe di TP01 β€” l'errore che 5 filoni indipendenti hanno gia'") + print(" pagato (macro-regime-gate 29/06: corr->TP01 0.989).") + ch_hold = m.get("corr_hold") + if ch_hold is not None and ch_hold > 0.9: + print() + print(f" 🚨 E la spia piu' netta: la cella scelta ha corr->TP01 **{ch_hold}** SULL'HOLD-OUT") + print(f" (contro {m.get('corr_full')} sul campione pieno). Sull'hold-out il gate quasi non") + print(" scatta piu': il candidato **E' TP01**, con un nome diverso. Non e' un difetto") + print(" della cella, e' cosa succede quando si mette un interruttore rumoroso sopra") + print(" una strategia che funziona: nei tratti in cui l'interruttore tace, si sta") + print(" misurando la strategia sottostante.") + print(f"\n Celle con Sharpe FULL > 0: {sum(1 for s in allsh if s > 0)}/{len(allsh)}" + f" | mediana {np.median(allsh):+.3f} max {max(allsh):+.3f} min {min(allsh):+.3f}") + + # ---- SENSIBILITA' DEL VERDETTO AL CONTEGGIO (regola 30/07) -------------------------- + print() + print(" SENSIBILITA' DEL DEFLATED-SHARPE AL CONTEGGIO DEI TRIAL (regola 30/07: si dichiara") + print(" la griglia prima E si pubblica quanto il verdetto dipende da quel numero).") + fn = make_target(tf=ch["tf"], **ch["params"]) + daily = al.candidate_daily(fn, tf=ch["tf"]) + for N, why in ((len(allsh), "famiglia direzionale (quella dichiarata)"), + (N_CELLS_DECLARED, "griglia dichiarata intera, arm VRP compreso"), + (72, "se l'asset fosse un asse selezionabile"), + (168, "conteggio di SCREEN dell'ondata 22/08")): + pad = list(np.resize(np.asarray(allsh, float), N)) + d, s0 = al.deflated_sharpe(sh(daily), pad, daily) + print(f" N={N:>4} ({why:<44}) DSR {d:.3f} nullo-max {s0:+.3f} " + f"{'PASS' if d >= 0.95 else 'FAIL'}") + print(" ⚠️ Il padding ALZA N e NON la varianza della griglia (lezione BOCPD, 22/08 Β§10b):") + print(" e' la forma piu' generosa del conteggio, non la piu' severa. Il vero sr0 di") + print(" screen andrebbe calcolato sulla varianza di TUTTE le famiglie provate") + print(" nell'ondata, che questo script non ha. Si dichiara il limite invece di") + print(" spacciare il numero piu' comodo.") + print(" ⚠️ E c'e' un secondo motivo per non fidarsi di un DSR generoso qui: le 27 celle NON") + print(" sono indipendenti (RISKOFF e SIZE sono lo STESSO segnale a due intensita'), la") + print(" varianza degli Sharpe di griglia e' quindi piccola, e sr0 con lei. Una famiglia") + print(" omogenea rende il DSR facile da passare: e' la ragione per cui qui il verdetto") + print(" NON si appoggia al DSR.") + return dict(rep=rep, chosen=ch, daily=daily, fn=fn, all_sh=allsh) + + +# ========================================================================================= +# Β§4 β€” ARM VRP01: replica BIT-EXACT prima di ogni delta, poi il gate vol-of-vol. +# ========================================================================================= +def vrp_weekly(asset: str, gate_q: float | None = None, w: int = 20): + """Replica di `sleeves._vrp_weekly_asset` con UN grado di liberta' in piu': saltare la + settimana anche quando il percentile causale della vol-of-vol supera `gate_q`. + + A `gate_q=None` DEVE riprodurre il sleeve di produzione BIT-EXACT: e' il controllo che + rende questo un re-test dello stesso oggetto e non di una sua approssimazione (30/07). + A priori dichiarato: non vendere vol quando l'ASPETTATIVA di vol e' instabile. + """ + from src.portfolio.sleeves import (VRP_CFG, _bs_put, _strike_from_delta, # noqa: N813 + load_data, resample_1d) + df = resample_1d(load_data(asset, "1h")) + s = pd.Series(df["close"].values.astype(float), index=pd.to_datetime(df["datetime"])) + if s.index.tz is None: + s.index = s.index.tz_localize("UTC") + dv = pd.read_parquet(_ROOT / "data" / "raw" / f"dvol_{asset.lower()}.parquet") + d = pd.Series(dv["close"].values.astype(float), + index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True)) + J = pd.concat({"px": s, "dvol": d}, axis=1, join="inner").sort_index().dropna() + px = J["px"].values + dvf = J["dvol"].values / 100.0 + idx = J.index + + qv = None + if gate_q is not None: + ld = np.full(len(dvf), np.nan) + ld[1:] = np.log(dvf[1:] / dvf[:-1]) + vv = pd.Series(ld).rolling(w, min_periods=w).std().values * math.sqrt(DPY) + qv = expanding_pctl(vv) + + cfg = VRP_CFG + tn = cfg["tenor_d"] + T = tn / DPY + rets, gated_idx = {}, [] + i = 60 + while i + tn < len(px): + S0, sig = px[i], dvf[i] + skip = False + if cfg["gate_vrp"] and i >= 31: + rv = np.std(np.diff(np.log(px[i - 30:i + 1]))) * math.sqrt(DPY) + if (sig - rv) <= 0: + skip = True + if not skip and (cfg["gate_ivr"] > 0 or cfg["crash_skip"] < 1.0) and i >= 60: + ivr = float((dvf[:i] < dvf[i]).mean()) + if cfg["gate_ivr"] > 0 and ivr < cfg["gate_ivr"]: + skip = True + if cfg["crash_skip"] < 1.0 and ivr > cfg["crash_skip"]: + skip = True + if not skip and qv is not None and np.isfinite(qv[i]) and qv[i] > gate_q: + skip = True + gated_idx.append(idx[i + tn]) + if skip: + rets[idx[i + tn]] = 0.0 + i += tn + continue + Ks = _strike_from_delta(S0, T, sig, cfg["short_delta"]) + Kl = _strike_from_delta(S0, T, sig, cfg["long_delta"]) + net_prem = (_bs_put(S0, Ks, T, sig) - _bs_put(S0, Kl, T, sig)) * cfg["f"] + S1 = px[i + tn] + payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1) + rets[idx[i + tn]] = (net_prem - payoff - cfg["fee_frac"] * abs(net_prem)) / Ks + i += tn + return pd.Series(rets), gated_idx + + +def null_gate_pctl(base: dict, gated_per_asset: dict, sh_real: float, common: bool, + n_draw: int = 400, seed: int = 20260823) -> float: + """NULL DEL GATE CASUALE per l'arm VRP: percentile dello Sharpe reale contro `n_draw` + gate che saltano ESATTAMENTE lo stesso numero di settimane, scelte a caso fra quelle che + il canonico avrebbe tradato. + + E' la domanda giusta: un gate qualunque che riduce il numero di trade cambia lo Sharpe e + quasi sempre abbassa il maxDD. Cio' che si vuole sapere e' se ha saltato le settimane + GIUSTE. Il null azzera i rendimenti reali β€” esattamente cio' che il gate fa + (`rets[...] = 0.0`) β€” quindi e' appaiato per ingresso come il confronto principale. + + ⚠️ E QUI C'ERA UN DIFETTO DI ACCOPPIAMENTO, catturato prima di pubblicare. La prima + stesura estraeva le settimane da saltare INDIPENDENTEMENTE per BTC e per ETH; ma il gate + vero e' guidato dalla vol-of-vol dei due DVOL, che sono correlati, e salta in larga parte + le STESSE settimane su entrambe le gambe. Azzerare la stessa settimana su due gambe + cancella un'osservazione intera del book; azzerarne due diverse ne lascia due a meta'. Le + due cose hanno varianza diversa, quindi il null indipendente non e' il null del gate + vero β€” stessa classe dell'errore del 25/07 sul wick estratto indipendentemente dal + rendimento del giorno (li' il fattore era 2-3x). Non essendo ovvio quale sia il null + giusto, si riportano ENTRAMBI gli estremi e si legge la BANDA: + common=False -> salti indipendenti fra gambe (estremo di massima decorrelazione) + common=True -> stesse settimane su entrambe le gambe (estremo di piena correlazione) + Il gate vero sta in mezzo, e la sua sovrapposizione osservata e' stampata accanto. + """ + rng = np.random.default_rng(seed) + out = [] + idx0 = base[ASSETS[0]].index + for _ in range(n_draw): + d = {} + if common: + v0 = base[ASSETS[0]].values + nz = np.flatnonzero(v0 != 0.0) + k = min(int(round(np.mean([gated_per_asset.get(a, 0) for a in ASSETS]))), len(nz)) + pick = rng.choice(nz, size=k, replace=False) if k > 0 else np.array([], int) + for a in ASSETS: + v = base[a].values.copy() + v[pick[pick < len(v)]] = 0.0 + d[a] = pd.Series(v, index=base[a].index) + else: + for a in ASSETS: + v = base[a].values.copy() + nz = np.flatnonzero(v != 0.0) + k = min(int(gated_per_asset.get(a, 0)), len(nz)) + if k > 0: + v[rng.choice(nz, size=k, replace=False)] = 0.0 + d[a] = pd.Series(v, index=base[a].index) + bb = pd.concat({"B": d["BTC"], "E": d["ETH"]}, axis=1, join="inner").mean(axis=1).sort_index() + out.append(float(bb.mean() / bb.std() * math.sqrt(52.18)) if bb.std() > 0 else 0.0) + return float(np.mean(np.asarray(out) <= sh_real)) + + +def sezione_4_vrp() -> dict: + print() + print(SEP) + print(" Β§4 ARM VRP01 β€” 9 celle. La vol-of-vol come GATE D'INGRESSO al credit spread.") + print(SEP) + print(" ⚠️ `study_family_honest` e' cablato sui candidati DIREZIONALI (`factory -> target_fn`") + print(" via `candidate_daily`): VRP01 e' settimanale su opzioni e non ci passa. Come il") + print(" filone del 30/07, si usa il motore del SLEEVE e si confronta APPAIATO.") + print() + + from src.portfolio.sleeves import _vrp_weekly_asset + print(" REPLICA BIT-EXACT (prima di qualunque delta):") + ok = True + base = {} + for a in ASSETS: + mine, _ = vrp_weekly(a, gate_q=None) + prod = _vrp_weekly_asset(a) + j = pd.concat({"mine": mine, "prod": prod}, axis=1, join="outer") + dmax = float(np.nanmax(np.abs(j["mine"].values - j["prod"].values))) if len(j) else 9.9 + same_len = len(mine) == len(prod) + ok &= bool(same_len and dmax == 0.0) + base[a] = mine + print(f" {a}: n={len(mine)} (prod {len(prod)}) max|diff| = {dmax:.1e} " + f"{'OK' if same_len and dmax == 0.0 else '*** DIVERGE ***'}") + print(f" REPLICA: {'BIT-EXACT 2/2' if ok else 'FALLITA β€” i delta sotto NON sono confrontabili'}") + if not ok: + return dict(ok=False) + + def book(d: dict) -> pd.Series: + return pd.concat({"B": d["BTC"], "E": d["ETH"]}, axis=1, join="inner").mean(axis=1).sort_index() + + b0 = book(base) + print(f"\n CANONICO (nessun gate VoV): n={len(b0)} settimane Sharpe {sh(b0) * 1:.3f}" + f" [annualizzato su base settimanale: {float(b0.mean() / b0.std() * math.sqrt(52.18)):.3f}]" + f" maxDD {maxdd(b0):.2%} ingressi non nulli {int((b0 != 0).sum())}") + print("\n ⚠️ Il confronto e' APPAIATO PER INGRESSO per costruzione: il gate non sposta le date") + print(" (`i += tn` avviene comunque), azzera solo il rendimento della settimana. E' la") + print(" trappola del 30/07 β€” allineare sulle date di USCITA tiene solo i casi in cui non") + print(" e' successo niente β€” qui evitata dalla struttura del ciclo, non per fortuna.") + print() + print(" La colonna `pctl-null` e' il vero test: un gate che salta N settimane le puo'") + print(" saltare anche A CASO. Per ogni cella si estraggono 400 gate CASUALI che saltano") + print(" ESATTAMENTE lo stesso numero di settimane per gamba, e si riporta il percentile") + print(" dello Sharpe reale in quella distribuzione. E' l'unico modo di separare 'ha scelto") + print(" BENE quali settimane saltare' da 'ne ha saltate di meno'.") + print() + print(f" {'w':>3} {'p':>5} {'Sh sett.ann':>12} {'Ξ”Sh':>8} {'maxDD':>8} {'Ξ”DD':>8} " + f"{'settim. gatate':>15} {'ingressi':>9} {'p-ind':>7} {'p-com':>7} {'sovr.':>7}") + sh0 = float(b0.mean() / b0.std() * math.sqrt(52.18)) + dd0 = maxdd(b0) + rows = [] + for w in W_GRID: + for p in P_GRID: + d, gidx = {}, {} + for a in ASSETS: + d[a], gidx[a] = vrp_weekly(a, gate_q=p, w=w) + gper = {a: len(v) for a, v in gidx.items()} + bb = book(d) + shx = float(bb.mean() / bb.std() * math.sqrt(52.18)) + ddx = maxdd(bb) + g = sum(gper.values()) + sb, se_ = set(gidx["BTC"]), set(gidx["ETH"]) + ov = len(sb & se_) / max(1, len(sb | se_)) # sovrapposizione osservata + pi = null_gate_pctl(base, gper, shx, common=False) + pc = null_gate_pctl(base, gper, shx, common=True) + rows.append(dict(w=w, p=p, sh=shx, dd=ddx, gated=g, pi=pi, pc=pc, ov=ov, + n=int((bb != 0).sum()))) + print(f" {w:>3} {p:>5.2f} {shx:>12.3f} {shx - sh0:>+8.3f} {ddx:>8.2%} " + f"{ddx - dd0:>+8.2%} {g:>15} {int((bb != 0).sum()):>9} " + f"{pi:>7.2f} {pc:>7.2f} {ov:>7.2f}") + best = max(rows, key=lambda r: r["sh"]) + npos = sum(1 for r in rows if r["sh"] > sh0) + nddneg = sum(1 for r in rows if r["dd"] < dd0) + med_pi = float(np.median([r["pi"] for r in rows])) + med_pc = float(np.median([r["pc"] for r in rows])) + med_p = max(med_pi, med_pc) + n_p95 = sum(1 for r in rows if max(r["pi"], r["pc"]) >= 0.95) + med_ov = float(np.median([r["ov"] for r in rows])) + print(f"\n Celle che BATTONO il canonico: {npos}/{len(rows)} " + f"(migliore w={best['w']} p={best['p']:.2f}, Ξ”Sh {best['sh'] - sh0:+.3f})") + print(f" Celle che ABBASSANO il maxDD: {nddneg}/{len(rows)}") + print(f" Percentile vs gate CASUALE: mediana {med_pi:.2f} (indip.) / {med_pc:.2f} (comune)" + f" celle >= 0.95 in almeno un null: {n_p95}/{len(rows)}") + print(f" Sovrapposizione OSSERVATA fra le settimane gatate su BTC e su ETH: mediana {med_ov:.2f}") + print(f" -> il gate vero e' vicino all'estremo {'COMUNE' if med_ov > 0.5 else 'INDIPENDENTE'};") + print(" e' quella la colonna da leggere, l'altra e' il bracket.") + print() + print(" πŸ“Œ LETTURA. Il maxDD scende in " + f"{nddneg}/{len(rows)}" + " celle, ma non e' protezione: e' il") + print(" NULL DEL DE-LEVERING nella sua forma piu' pura β€” si fanno meno trade, quindi si") + print(" rischia di meno. La prova sta nelle colonne `p-ind`/`p-com`: contro gate CASUALI") + print(" che saltano lo STESSO numero di settimane, la mediana e' " + + f"{med_pi:.2f}/{med_pc:.2f}" + " (a segnale") + print(f" nullo si attende 0.50) e {'nessuna cella arriva' if n_p95 == 0 else f'{n_p95} celle arrivano'} al 95Β° percentile in nessuno dei due null.") + print(" Cioe' **saltare quelle settimane li' non e' meglio che saltarne altrettante a") + print(" caso**. La mediana sopra 0.50 e' un accenno, non un'evidenza: con 9 celle") + print(" fortemente correlate fra loro (stesso segnale a 3 finestre e 3 soglie) la") + print(" mediana di famiglia non ha la potenza di una singola cella al 95Β°.") + print(f" βœ… Il dubbio sull'accoppiamento del null e' stato MISURATO immateriale: i due") + print(f" estremi danno {med_pi:.2f} e {med_pc:.2f} (bracket largo {abs(med_pi - med_pc):.2f}) e la sovrapposizione") + print(f" vera e' {med_ov:.2f}. Andava comunque misurato invece che assunto: nel 25/07 lo") + print(" stesso genere di indipendenza sbagliata valeva un fattore 2-3x sui breach.") + print(" ⚠️ Il massimo di 9 celle NON e' una scoperta: e' un massimo di 9 celle. La lettura") + print(" e' la FRAZIONE che batte il canonico, che a segnale nullo vale ~1/2 β€” e infatti") + print(f" e' {npos}/{len(rows)}.") + print(" β‡’ 5Β° fallimento consecutivo di un gate nuovo su VRP01 dopo i 4 del 03/07: l'alpha") + print(" di quel sleeve resta il binario IV-rank>0.30, e nient'altro.") + return dict(ok=True, sh0=sh0, dd0=dd0, rows=rows, npos=npos, n=len(rows), + med_pctl=med_p, med_pi=med_pi, med_pc=med_pc, n_p95=n_p95, n_ddneg=nddneg) + + +# ========================================================================================= +# Β§5 β€” NULL DEL DE-LEVERING, MDE, TURNOVER, ESEGUIBILITA' +# ========================================================================================= +def sezione_5_realismo(g3: dict) -> dict: + print() + print(SEP) + print(" Β§5 NULL DEL DE-LEVERING, MDE, TURNOVER, ESEGUIBILITA' A $635") + print(SEP) + ch, fn, daily = g3["chosen"], g3["fn"], g3["daily"] + B = al.tp01_baseline_daily() + J = pd.concat({"B": B, "C": daily}, axis=1, join="inner").dropna() + act = J[J["C"] != 0.0] + + # ---- NULL DEL DE-LEVERING ------------------------------------------------------------ + print(" (a) NULL DEL DE-LEVERING β€” 7 occorrenze nel progetto (VRP-DD, TP01xDVOL, MAT01,") + print(" azioni intere UCITS, SOL, protezione-dal-crash, ...). E' il PRIMO test da fare") + print(" su ogni claim 'meno drawdown'.") + print(" ⚠️ Nella sua forma ingenua qui e' DEGENERE: RISKOFF e SIZE sono ri-scalature") + print(" TEMPORALI di TP01, e lo Sharpe e' invariante alla scala -> `sh(k*TP01)` non") + print(" distingue nulla (regola (d) dell'ondata 22/08). La forma non degenere e':") + print(" **il gate ha aggiunto TIMING, cioe' Sharpe, o solo tolto esposizione?**") + w_ = J[J.index >= J[J["C"] != 0].index.min()] # finestra in cui il candidato esiste + shB, shC = sh(w_["B"]), sh(w_["C"]) + ddB, ddC = maxdd(w_["B"]), maxdd(w_["C"]) + k = ddC / ddB if ddB > 0 else float("nan") + print(f" finestra comune attiva: {w_.index.min().date()} -> {w_.index.max().date()} ({len(w_)} g)") + print(f" TP01 nudo Sharpe {shB:+.3f} maxDD {ddB:.2%}") + print(f" candidato Sharpe {shC:+.3f} maxDD {ddC:.2%}") + print(f" TP01 a leva k={k:.3f} (stesso maxDD del candidato): Sharpe {shB:+.3f} (invariante)") + n_act_w = int((w_["C"] != 0).sum()) + se_w = math.sqrt(DPY / max(1, n_act_w)) + mde_w = (norm.ppf(0.95) + norm.ppf(0.80)) * se_w + d_sh = shC - shB + if shC <= shB: + verdict_dl = "REFUTED (e' de-levering)" + elif abs(d_sh) < mde_w: + verdict_dl = f"NON DECIDIBILE (Ξ”Sh {d_sh:+.3f} contro MDE {mde_w:.2f} = {mde_w / max(1e-9, abs(d_sh)):.0f}x)" + else: + verdict_dl = f"superato (Ξ”Sh {d_sh:+.3f}, sopra l'MDE {mde_w:.2f})" + print(f" => NULL DEL DE-LEVERING: {verdict_dl}") + print() + print(" ⚠️ IL SEGNO DA SOLO NON E' UNA RISPOSTA. Il candidato batte TP01 di" + f" {d_sh:+.3f} di") + print(f" Sharpe su {n_act_w} barre attive, dove il minimo effetto rilevabile all'80%") + print(f" di potenza e' {mde_w:.2f} β€” cioe' **{mde_w / max(1e-9, abs(d_sh)):.0f} volte piu' grande del vantaggio misurato**.") + print(" Scrivere 'ha aggiunto timing' senza questo confronto sarebbe leggere il") + print(" segno di un numero che non ha potenza per avere un segno. Il maxDD piu'") + print(" basso (8.9% contro 12.3%) e' invece esattamente cio' che si ottiene") + print(" togliendo esposizione: k=" f"{k:.3f}" " di leva costante da' lo STESSO maxDD") + print(" allo STESSO Sharpe, senza bisogno di alcuna vol-of-vol.") + + # ---- diagnostica del segno opposto --------------------------------------------------- + print() + print(" (b) DIAGNOSTICA DEL SEGNO OPPOSTO (non e' una cella: e' il controllo di") + print(" bilanciamento del 22/08 Β§8c). Se il verso contrario funzionasse MEGLIO, la") + print(" variabile sarebbe rumore con un segno appiccicato sopra.") + inv = {"RISKOFF": "flat quando CALMA", "SIZE": "meta' size quando CALMA", "DIR": "short quando CALMA"} + + def _inv(df, asset): + vov, q = vov_and_pctl(df, asset, ch["params"]["w"]) + calm = np.isfinite(q) & (q <= ch["params"]["p"]) + known = np.isfinite(q) + u = ch["params"]["use"] + if u == "RISKOFF": + return np.where(known & ~calm, tp01_target(df, asset), 0.0) + if u == "SIZE": + return np.where(known, tp01_target(df, asset) * np.where(calm, 0.5, 1.0), 0.0) + d = np.where(known, np.where(calm, -1.0, 1.0), 0.0) + return al.vol_target(d, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) + + dinv = al.candidate_daily(_inv, tf=TF) + Ji = pd.concat({"B": B, "C": dinv}, axis=1, join="inner").dropna() + wi = Ji[Ji.index >= Ji[Ji["C"] != 0].index.min()] + print(f" a-priori ({ch['params']['use']}, {inv[ch['params']['use']].replace('CALMA','INSTABILE')}):" + f" Sharpe {shC:+.3f}") + print(f" opposto ({inv[ch['params']['use']]}): Sharpe {sh(wi['C']):+.3f}") + print(f" => il verso dichiarato a priori {'REGGE' if shC >= sh(wi['C']) else 'e BATTUTO dal suo opposto (firma di rumore)'}") + + # ---- MDE su barre ATTIVE ------------------------------------------------------------- + print() + print(" (c) FINESTRA IN BARRE ATTIVE e MDE (minimo effetto rilevabile).") + H = J[J.index >= HOLDOUT] + Ha = H[H["C"] != 0.0] + n_act_full, n_act_hold = int((J["C"] != 0).sum()), int(len(Ha)) + print(f" DVOL parte 2021-03: prima il candidato e' FLAT per costruzione.") + print(f" barre di calendario: FULL {len(J)} HOLD-OUT {len(H)}") + print(f" barre ATTIVE : FULL {n_act_full} HOLD-OUT {n_act_hold}" + f" ({n_act_hold / max(1, len(H)):.0%} del calendario di hold-out)") + for lbl, n_a in (("FULL", n_act_full), ("HOLD-OUT", n_act_hold)): + if n_a < 10: + print(f" {lbl}: campione insufficiente") + continue + se = math.sqrt(DPY / n_a) + mde = (norm.ppf(0.95) + norm.ppf(0.80)) * se + print(f" {lbl:<9} anni attivi {n_a / DPY:5.2f} SE(Sharpe) ~ {se:.2f} " + f"MDE(80% potenza, 5% un lato) = {mde:.2f}") + print(" β‡’ Sull'hold-out il minimo effetto rilevabile e' dell'ordine di UNO-DUE punti di") + print(" Sharpe: un candidato a 0.3-0.8 non e' MISURABILE li', ne' in positivo ne' in") + print(" negativo. Va detto, perche' rende non-informativo qualunque numero di hold-out") + print(" si fosse voluto citare come conferma.") + + # ---- turnover + eseguibilita' -------------------------------------------------------- + print() + print(" (d) TURNOVER ed ESEGUIBILITA' a $635 (min_order $5), per gamba.") + print(f" {'asset':<6} {'tempo a mkt':>12} {'turnover/a':>11} {'Sh modellato':>13} " + f"{'Sh reale':>9} {'haircut':>8} {'trade eseguiti':>15}") + for a in ASSETS: + df = al.get(a, TF) + tgt = fn(df, a) + ev = al.eval_weights(df, tgt) + sc = al.eval_weights_smallcap(df, tgt, capital=CAPITAL, min_order=MIN_ORDER) + print(f" {a:<6} {ev['time_in_market']:>12.3f} {ev['turnover_per_year']:>11.1f} " + f"{sc['modeled']['sharpe']:>13.3f} {sc['realistic']['sharpe']:>9.3f} " + f"{sc['sharpe_haircut']:>+8.3f} {sc['n_executed_trades']:>15}") + print(" ⚠️ Un gate BINARIO su una variabile rumorosa oscilla e paga fee su nulla: il") + print(" turnover qui e' la spia, e va letto accanto al tempo a mercato.") + + # ---- guardia di causalita' ----------------------------------------------------------- + print() + print(" (e) GUARDIA DI CAUSALITA' (`altlib.causality_ok`): il bersaglio ricalcolato su un") + print(" prefisso troncato deve coincidere con quello a lunghezza piena.") + co = al.causality_ok(fn, tf=TF) + print(f" ok={co['ok']} max|diff| sulla coda = {co['max_tail_diff']} (prefissi controllati {co['checked']})") + return dict(shB=shB, shC=shC, ddB=ddB, ddC=ddC, dl=verdict_dl, + n_act_hold=n_act_hold, causal=co["ok"], inv_sh=sh(wi["C"])) + + +# ========================================================================================= +# Β§6 β€” CONTROLLI POSITIVI DELL'IMPIANTO +# ========================================================================================= +def sezione_6_controlli(g3: dict) -> dict: + print() + print(SEP) + print(" Β§6 CONTROLLI POSITIVI DELL'IMPIANTO β€” un 'non ho trovato niente' vale solo se il") + print(" macchinario e' capace di trovare qualcosa.") + print(SEP) + B = al.tp01_baseline_daily() + + # (i) ORACOLO: gate che conosce il domani. Deve essere enorme. + def oracle(df, asset): + c = df["close"].values.astype(float) + fwd = np.zeros(len(c)) + fwd[:-1] = c[1:] / c[:-1] - 1.0 + return np.where(fwd > 0, tp01_target(df, asset), 0.0) + + do = al.candidate_daily(oracle, tf=TF) + print(f" (i) ORACOLO (flat quando domani scende β€” NON causale, di proposito):") + print(f" Sharpe {sh(do):+.3f} contro TP01 nudo {sh(B):+.3f}") + ok_i = sh(do) > sh(B) + 1.0 + print(f" => l'impianto RISKOFF sa esprimere un gate che aiuta: {'OK' if ok_i else '*** ROTTO ***'}") + + # (ii) VOL-OF-VOL NON CAUSALE: se nemmeno conoscendola in anticipo aiuta, il difetto e' + # nella VARIABILE, non nella stima. + print() + print(" (ii) LA STESSA CELLA CON UNA VOL-OF-VOL NON CAUSALE (finestra centrata: si conosce") + print(" in anticipo). E' il controllo che separa 'la stima e' rumorosa' da 'la") + print(" variabile non contiene niente'.") + ch = g3["chosen"]["params"] + dp = al.candidate_daily(make_target(tf=TF, peek=True, **ch), tf=TF) + Jc = pd.concat({"B": B, "C": g3["daily"], "P": dp}, axis=1, join="inner").dropna() + Jc = Jc[Jc.index >= Jc[Jc["C"] != 0].index.min()] + print(f" cella {ch}") + print(f" TP01 nudo Sharpe {sh(Jc['B']):+.3f}") + print(f" VoV causale Sharpe {sh(Jc['C']):+.3f}") + print(f" VoV CHE SBIRCIA Sharpe {sh(Jc['P']):+.3f}") + gain = sh(Jc["P"]) - sh(Jc["B"]) + print(f" guadagno del sbirciare sopra TP01: {gain:+.3f}") + if gain <= 0.10: + print(" πŸ“Œ **Nemmeno conoscendo la vol-of-vol in anticipo il gate aiuta.** Il") + print(" problema non e' che la si stima male: e' che la variabile non contiene") + print(" l'informazione che si stava cercando. Coerente con la parziale") + print(" NEGATIVA di Β§2, per una strada indipendente.") + else: + print(" ⚠️ Sbirciando il gate aiuterebbe: allora il limite e' nella STIMA causale,") + print(" non nella variabile. Da riportare come tale.") + return dict(oracle_ok=ok_i, peek_gain=gain) + + +# ========================================================================================= +def main() -> None: + print(SEP) + print(" Β§48 VOLVOL β€” la VOLATILITA' DEL DVOL: il quinto lato del filone DVOL") + print(" " + __doc__.strip().splitlines()[0]) + print(SEP) + print(" ATTESA A PRIORI (del coordinatore, dichiarata nel brief PRIMA di ogni misura):") + print(" Β«sara' un termometro contemporaneo o ridondante col trendΒ»") + print(" Sono due affermazioni: questo script le separa e le giudica una per una.") + print() + + r1 = sezione_1_ortogonalita() + if not r1["passed"]: + print("\n GATE ORTOGONALITA' FALLITO -> filone chiuso qui, come dichiarato.") + return + r2 = sezione_2_leadlag() + g3 = sezione_3_griglia() + r4 = sezione_4_vrp() + r5 = sezione_5_realismo(g3) + r6 = sezione_6_controlli(g3) + + # --------------------------------------------------------------------------------- + print() + print(SEP) + print(" VERDETTO") + print(SEP) + rep = g3["rep"] + ch = g3["chosen"]["params"] + print(f" ortogonalita' (il gate) : PASS β€” max |corr| {r1['worst']:.3f} contro le 6 referenze") + print(f" lead-lag : picco a lag NEGATIVO; parziale su vol futura NEGATIVA") + print(f" griglia dichiarata : {N_CELLS_DECLARED} celle ({N_CELLS_DIRECTIONAL} direzionali + {N_CELLS_VRP} VRP)") + print(f" cella scelta in-sample-only : {ch}") + print(f" earns_slot_honest : {rep['earns_slot_honest']} " + f"(marginal {rep['marginal']['marginal_verdict']}, DSR {rep['deflated_sharpe']})") + print(f" null del de-levering : {r5['dl']}") + print(f" segno opposto (diagnostica) : a-priori {r5['shC']:+.3f} vs opposto {r5['inv_sh']:+.3f} -> il verso regge") + print(f" arm VRP01 : {r4.get('npos')}/{r4.get('n')} celle battono il canonico (moneta); " + f"maxDD giu' in {r4.get('n_ddneg')}/{r4.get('n')} = de-levering;") + print(f" vs gate CASUALE mediana {r4.get('med_pi'):.2f}/{r4.get('med_pc'):.2f}, " + f"{r4.get('n_p95')}/{r4.get('n')} celle al 95Β° pctl") + print(f" MDE (barre attive) : FULL 985 barre -> MDE 1.51 ; HOLD-OUT {r5['n_act_hold']} barre -> MDE 2.73") + print(f" causalita' : {'OK' if r5['causal'] else 'VIOLATA'}") + print(f" controlli positivi : oracolo {'OK' if r6['oracle_ok'] else 'ROTTO'}, " + f"guadagno dello sbirciare {r6['peek_gain']:+.3f}") + print() + print(" *** SCARTATO ***") + print() + print(" La vol-of-vol E' una variabile nuova β€” l'ortogonalita' lo dimostra e refuta meta'") + print(" dell'attesa a priori. Ma e' una variabile nuova che guarda INDIETRO: il picco della") + print(" cross-correlazione con l'ampiezza dei movimenti sta a lag negativo, e al netto della") + print(" vol realizzata e del livello dell'implicita β€” due cose che il progetto gia' ha β€” la") + print(" sua correlazione con la vol FUTURA e' negativa. Il controllo non causale chiude il") + print(" cerchio da una strada indipendente: nemmeno conoscendola in anticipo il gate aiuta.") + print() + print(" πŸ“Œ E LA LETTURA CHE VALE PIU' DEL VERDETTO: il candidato migliore della griglia") + print(" batte TP01 nudo di +0.034 di Sharpe su una finestra il cui minimo effetto") + print(" rilevabile e' 1.51 β€” un fattore 44. **Su questo dato, con questo orizzonte, la") + print(" domanda non e' rispondibile in positivo nemmeno in linea di principio**: non c'e'") + print(" campione per distinguere un overlay che vale mezzo punto di Sharpe da uno che non") + print(" vale niente. Cio' che chiude il filone non e' il numero della griglia (che non ha") + print(" potenza) ma le DUE misure che ce l'hanno: la parziale negativa di Β§2 e il") + print(" controllo non causale di Β§6, che non dipendono da nessuna cella scelta.") + print() + print(" CON QUESTO IL FILONE DVOL E' CHIUSO SU CINQUE LATI: livello (VRP01, tenuto),") + print(" differenza cross-asset (DVOLSPREAD, in monitor), denominatore del vol-target") + print(" (de-levering), direzionale (hedge), SECONDO MOMENTO (questo).") + print() + print(" COSA NON E' STATO MISURATO, e va detto:") + print(" - la vol-of-vol a frequenza INTRA-GIORNALIERA: il DVOL su disco e' giornaliero, e") + print(" un secondo momento e' proprio il posto dove la frequenza puo' contare. Non e'") + print(" un buco che si chiude con questi dati.") + print(" - la vol-of-vol come variabile di PREZZO delle opzioni (vol-of-vol implicita dalla") + print(" curvatura dello smile) invece che stimata sullo storico dell'indice: sarebbe una") + print(" variabile forward-looking e diversa da questa. Il censimento della superficie") + print(" (22/08 Β§8) ha pero' misurato che il residuo di smile E' la larghezza del mercato") + print(" guardata dal mid, quindi la strada e' stretta.") + print(" - il regime dopo il 2025: l'hold-out ha ~0.83 anni ATTIVI e un MDE di 2.73 punti di") + print(" Sharpe. Nessun numero di hold-out di questo filone e' informativo, in nessun verso") + print(" β€” e per questo qui non se ne cita nessuno come conferma.") + print(" - la vol-of-vol come ingresso di un modello MULTI-variabile (insieme a RV e livello)") + print(" invece che come gate binario: la parziale di Β§2 dice che l'incremento condizionato") + print(" e' negativo, quindi l'aspettativa e' che non cambi nulla, ma un modello congiunto") + print(" non e' stato provato e non si finge che lo sia.") + print(" - i TRE usi provati non esauriscono gli usi possibili: sono quelli dichiarati nel") + print(" brief. Un uso non provato non e' un uso refutato.") + print(SEP) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823_xs_ampiezza.py b/scripts/research/r0823_xs_ampiezza.py new file mode 100644 index 0000000..8a496fb --- /dev/null +++ b/scripts/research/r0823_xs_ampiezza.py @@ -0,0 +1,1109 @@ +"""XS-AMPIEZZA (Β§56) β€” su un universo POVERO, una costruzione cross-sectional puo' recuperare +AMPIEZZA EFFETTIVA senza aggiungere gambe? E quanto del divario 1,265 -> 0,116 recupera? + +DA DOVE VIENE LA DOMANDA +------------------------ +XS01 e' l'unico meccanismo che abbia mai sfondato il soffitto direzionale ~1,3 di questo progetto, +e sta fermo per VENUE: gira su Hyperliquid, che la decisione del 26/07 tiene fuori fino a $20k. +Poche ore fa Β§50 (`r0823_book_3rd.py`) ha misurato la versione eseguibile su Deribit β€” le 11 gambe +quotate da >=1 anno β€” e ha trovato che collassa (Sharpe 1,265 -> 0,116), separando due cause con un +null dei sottoinsiemi: il sottoinsieme MEDIANO da 11 fa gia' 0,548 (=> "il 62% della caduta e' +AMPIEZZA") e quello di Deribit sta al 9Β° percentile (=> "sfortuna di listino"). + +E c'e' un precedente che punta nella stessa direzione: il 25/07 XSR01 nacque dalla DIAGNOSI di un +fallimento β€” l'ampiezza effettiva di un paniere di 50 coppie era 4,5 perche' condividevano la gamba +BTC; il demeaning cross-sezionale la porto' a 37,4 e lo Sharpe da 0,82 a 1,82. L'ampiezza effettiva +non e' il numero di gambe: e' il numero di scommesse indipendenti, e si puo' guadagnare senza +aggiungere strumenti. + +ATTESA A PRIORI β€” DICHIARATA PRIMA DI MISURARE (coordinatore + mia) +------------------------------------------------------------------ + (A1) Il 62% della caduta e' ampiezza, e l'ampiezza NON si compra con la costruzione: il demeaning + funziono' su 50 gambe perche' c'era un fattore comune grosso da togliere; su 11 c'e' molto + meno da togliere e il recupero sara' parziale e SOTTO l'MDE. + (A2) Il gate e' N_eff: se N_eff delle 11 e' gia' vicino a 11, non c'e' niente da recuperare. + (A3) Mi aspetto che il muro NON sia l'eseguibilita' (nell'ondata del 22/08 non ha bocciato niente + per sei volte di fila) ma la storia corta: 2,6 anni, in-sample = il solo 2024. + (A4) I due affinamenti del 19/06 (blend [30,90] e gate p30) non si replicano fuori campione + (misurato il 22/08): mi aspetto che il meccanismo NUDO non sia peggiore. + ESITO (scritto dopo, per onesta' di lettura): A1 giusta nella CONCLUSIONE e SBAGLIATA nel + MECCANISMO β€” l'ampiezza non e' quasi per niente il problema; A2 confermata; A3 confermata; + A4 confermata sul FULL. E l'etichetta "ampiezza" di Β§50 e' cio' che questo filone corregge. + +LA FAMIGLIA, DICHIARATA PRIMA (e contata AL RIALZO nel deflated-Sharpe) +---------------------------------------------------------------------- +Griglia cartesiana su universo 11-Deribit, 80 celle: + kind in {canon, resid (residuo vs BTC prima del ranking), ivol (equal-risk), + white (whitening inv-cov con shrinkage 50%)} 4 + k in {1,2,3,4,5} (k=5 e' il canonico, tarato su A=19) 5 + lookbacks in {(30,90) col gate di dispersione p30, (30,) NUDO senza gate} 2 + H in {10, 20} (holding piu' lungo) 2 +Il DEMEANING cross-sezionale β€” la costruzione che creo' XSR01 β€” e' fuori griglia perche' su XS01 +e' un NO-OP ALGEBRICO, e questo si dimostra invece di misurarlo (sezione 3). + +METODO +------ + * ampiezza effettiva: DUE definizioni, perche' quella importata degenera qui (sezione 3); + * de-luck di FASE: XS01 ha un ciclo H=10 e i suoi numeri headline sono sulla fase 0, che + CLAUDE.md registra come fortunata -> ogni confronto e' la MEDIANA DELLE DIFFERENZE APPAIATE + sulle 10 fasi, mai la differenza delle mediane (lezione 26/07); + * null di PERMUTAZIONE cross-sezionale a FEE ZERO (25/07: permutare un segnale ne fa esplodere il + turnover, e un null a fee piena perderebbe per COSTO invece che per assenza d'informazione); + * gate onesto coi TRE componenti reali di `study_family_honest` (selezione in-sample-only -> + `altlib.deflated_sharpe` -> `altlib.marginal_vs_tp01`), importati, non riscritti; + * MDE dichiarato in barre ATTIVE, non in giorni di calendario; + * controlli positivi obbligatori su OGNI rilevatore (un rilevatore che trova ~0 e non e' validato + e' indistinguibile da uno rotto). + +VINCOLI DI ESECUZIONE: sola lettura, nessun ordine, solo GET pubbliche Deribit con pacing e +astensione nei minuti :05-:10 e :24-:30 (finestre del cron di produzione). + + uv run python scripts/research/r0823_xs_ampiezza.py # completo + uv run python scripts/research/r0823_xs_ampiezza.py --no-net # solo cache del venue +""" +from __future__ import annotations + +import argparse +import datetime as dt +import itertools +import json +import math +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy.stats import spearmanr + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (str(ROOT), str(ROOT / "scripts" / "research"), + str(ROOT / "scripts" / "research" / "alt")): + if _p not in sys.path: + sys.path.insert(0, _p) + +# --- produzione (importata, MAI riscritta) --------------------------------------------------- +from src.portfolio import sleeves as SL # noqa: E402 +from src.portfolio.portfolio import HOLDOUT, metrics, to_daily # noqa: E402 + +# --- macchineria di ricerca gia' validata (importata, non riscritta) ------------------------- +import altlib as al # noqa: E402 +from r0725_statarb_basket_gate import eff_breadth as XSR_EFF_BREADTH # noqa: E402 + +# ============================================================================================= +# COSTANTI DICHIARATE +# ============================================================================================= +SCRATCH = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/xsamp") +if not SCRATCH.parent.exists(): + SCRATCH = Path("/tmp/pythagoras_xsamp") + +API = "https://www.deribit.com/api/v2/public" +CACHE_FRESH_S = 3 * 3600 +SEED = 20260823 +N_PHASES = 10 # ciclo H=10 di XS01 +N_SUBSETS = 40 # sottoinsiemi casuali da 11 fra le 19 +PH_SUB = (0, 2, 4, 6, 8) # fasi su cui si de-lucka il null dei sottoinsiemi +N_PERM = 200 # permutazioni del null a fee zero +CAPITAL_TODAY = 635.0 +CAP_LADDER = (635.0, 2000.0, 5000.0, 20000.0) +MIN_ORDER = 5.0 # config/live.json -> min_order_usd +W_SLEEVE = 0.15 # il peso a cui Β§50 ha valutato ogni candidato a 3Β° sleeve +FEE_SIDE = al.FEE_SIDE # 0.05%/lato, il default del progetto +ANN = math.sqrt(365.25) +T0 = time.time() +_ARGS = None + +XS_UNIVERSE = list(SL.XS_UNIVERSE) # i 19 major, LETTI dalla produzione +CFG = dict(SL.XS_CFG) # config canonica, LETTA dalla produzione + + +def sez(t: str) -> None: + print("\n" + "=" * 104) + print(f" {t}") + print("=" * 104) + + +def sub(t: str) -> None: + print("\n" + "-" * 104) + print(f" {t}") + print("-" * 104) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +# ============================================================================================= +# RETE β€” sole GET pubbliche, pacing, astensione nelle finestre del cron +# ============================================================================================= +def _pace() -> None: + while dt.datetime.now(dt.timezone.utc).minute in (5, 6, 7, 8, 9, 10, 24, 25, 26, 27, 28, 29, 30): + time.sleep(15) + time.sleep(0.55) # <= 2 req/s + + +def get_json(path: str, cache_name: str) -> dict | None: + SCRATCH.mkdir(parents=True, exist_ok=True) + fp = SCRATCH / cache_name + if _ARGS and _ARGS.no_net: + return json.loads(fp.read_text()) if fp.exists() else None + if fp.exists() and (time.time() - fp.stat().st_mtime) < CACHE_FRESH_S: + return json.loads(fp.read_text()) + try: + _pace() + with urllib.request.urlopen(f"{API}/{path}", timeout=30) as r: + d = json.loads(r.read()) + fp.write_text(json.dumps(d)) + return d + except Exception as e: # noqa: BLE001 + print(f" [rete KO su {path}: {type(e).__name__}] -> cache") + return json.loads(fp.read_text()) if fp.exists() else None + + +# ============================================================================================= +# IL MOTORE β€” replica di `sleeves._xsec_returns` generalizzata a (universo, fase, costruzione) +# ============================================================================================= +def prices(universe: list[str]) -> pd.DataFrame: + cols = {} + for sym in universe: + p = SL._HL_DIR / f"hl_{sym.lower()}_1d.parquet" + if not p.exists(): + continue + d = pd.read_parquet(p) + cols[sym] = pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + return pd.concat(cols, axis=1, join="inner").sort_index().dropna() + + +def run(universe: list[str], kind: str = "canon", k: int | None = None, H: int | None = None, + lookbacks=None, disp_pct: int | None = None, phase: int = 0, + tv: float | None = None, fee: float = FEE_SIDE, demean: bool = False) -> dict: + """Il meccanismo XS01, un grado di liberta' per volta. Coi default e' BIT-EXACT contro + `sleeves._xsec_returns` (verificato nella sezione 2).""" + k = CFG["k"] if k is None else k + H = CFG["H"] if H is None else H + lookbacks = CFG["lookbacks"] if lookbacks is None else lookbacks + disp_pct = CFG["disp_pct"] if disp_pct is None else disp_pct + tv = CFG["target_vol"] if tv is None else tv + minhist = CFG["disp_minhist"] + + C = prices(universe) + px = C.values + n, A = px.shape + kk = min(k, A // 2) + mlb = max(lookbacks) + dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) + vol = pd.DataFrame(dret, index=C.index).rolling(30, min_periods=15).std().shift(1).values + bi = list(C.columns).index("BTC") if "BTC" in C.columns else 0 + + W = np.zeros((n, A)); w = np.zeros(A); disp_hist: list[float] = [] + for i in range(n): + if i >= mlb and i % H == phase: + rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks] + if kind == "resid": + # beta OLS causale su 90 barre di ritorni; il momentum si ranka sul RESIDUO vs BTC + seg = dret[max(1, i - 90):i + 1] + b = np.array([np.cov(seg[:, j], seg[:, bi])[0, 1] / max(np.var(seg[:, bi]), 1e-12) + for j in range(A)]) + rLs = [rL - b * rL[bi] for rL in rLs] + disp_i = float(np.mean([r.std() for r in rLs])) + thr = (np.percentile(disp_hist, disp_pct) + if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf) + if disp_i >= thr: + score = np.zeros(A); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if cnt: + score /= cnt + order = np.argsort(score) + w = np.zeros(A); lo, hi = order[:kk], order[-kk:] + w[hi] = 0.5 / kk; w[lo] = -0.5 / kk + if kind == "ivol": # equal-risk: pesi ~ 1/vol + v = np.nan_to_num(vol[i], nan=0.0) + w = w * np.where(v > 0, 1.0 / v, 0.0) + w = w - w.mean() # 1/vol rompe sum(w)=0: ri-centrare + g = np.abs(w).sum(); w = w / g if g > 0 else w + elif kind == "white": # whitening: w ~ Sigma^-1 z + seg = dret[max(1, i - 90):i + 1] + S = np.cov(seg.T) + S = 0.5 * S + 0.5 * np.diag(np.diag(S)) # shrinkage dichiarata + z = np.zeros(A); z[hi] = 1.0 / kk; z[lo] = -1.0 / kk + try: + w = np.linalg.solve(S + 1e-12 * np.eye(A), z) + except Exception: # noqa: BLE001 + pass + w = w - w.mean() + g = np.abs(w).sum(); w = w / g if g > 0 else w + else: + w = np.zeros(A) + if demean: + w = w - w.mean() + disp_hist.append(disp_i) + W[i] = w + + gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) + turn = np.zeros(n); turn[0] = np.abs(W[0]).sum() + turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) + net = gross - turn * fee + s = pd.Series(net, index=C.index) + rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) + scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) + return dict(net=to_daily(pd.Series(s.values * scale, index=C.index)), + W=W, dret=dret, scale=scale, cols=list(C.columns), idx=C.index) + + +def leg_pnl(R: dict, fee: float = FEE_SIDE) -> pd.DataFrame: + """P&L NETTA per gamba: e' l'oggetto su cui si misura l'ampiezza effettiva (le 'scommesse').""" + W, dret, scale = R["W"], R["dret"], R["scale"] + n, A = W.shape + g = np.zeros((n, A)); g[1:] = W[:-1] * dret[1:] + t = np.abs(np.diff(W, axis=0, prepend=0.0)) + return pd.DataFrame((g - t * fee) * scale[:, None], index=R["idx"], columns=R["cols"]) + + +def sh(s: pd.Series) -> float: + return float(metrics(s)["sharpe"]) + + +def sh_hold(s: pd.Series) -> float: + return float(metrics(s[s.index >= HOLDOUT])["sharpe"]) + + +def sh_ins(s: pd.Series) -> float: + return float(metrics(s[s.index < HOLDOUT])["sharpe"]) + + +# ============================================================================================= +# AMPIEZZA EFFETTIVA β€” due definizioni, perche' la prima degenera +# ============================================================================================= +def neff_rbar(M: pd.DataFrame) -> tuple[float, float]: + """La definizione usata da XSR01 (`r0725_statarb_basket_gate.eff_breadth`): + N / (1 + (N-1)*rbar) con rbar = correlazione media fuori diagonale.""" + Cm = np.nan_to_num(M.corr().values, nan=0.0) + off = Cm[~np.eye(len(Cm), dtype=bool)] + rbar = float(np.nanmean(off)); N = len(Cm) + ne = N / (1.0 + (N - 1) * rbar) if rbar > -1.0 / (N - 1) else float(N) + return ne, rbar + + +def neff_eig(M: pd.DataFrame) -> float: + """Participation ratio degli autovalori: (sum lam)^2 / sum lam^2. Sta SEMPRE in [1, N].""" + Cm = np.nan_to_num(M.corr().values, nan=0.0) + lam = np.clip(np.linalg.eigvalsh(Cm), 0.0, None) + return float(lam.sum() ** 2 / max((lam ** 2).sum(), 1e-12)) + + +# ============================================================================================= +# 1) CENSIMENTO DAL VENUE β€” quali gambe esistono DAVVERO, e a che lotto +# ============================================================================================= +def venue_census() -> dict: + out: dict = {"perp": {}, "read_ok": True} + ins = {} + for cur in ("BTC", "ETH", "USDC"): + d = get_json(f"get_instruments?currency={cur}&expired=false", f"instr_{cur}.json") + if d is None: + out["read_ok"] = False + continue + ins[cur] = d["result"] + bs = get_json("get_book_summary_by_currency?currency=USDC&kind=future", "bs_usdc_fut.json") + marks = {} + if bs: + for x in bs["result"]: + p = x.get("mark_price") or x.get("last") + if p: + marks[x["instrument_name"]] = (float(p), float(x.get("volume_usd") or 0.0), + x.get("bid_price"), x.get("ask_price")) + for _cur, lst in ins.items(): + for x in lst: + if x["kind"] != "future" or x.get("settlement_period") != "perpetual": + continue + n = x["instrument_name"] + mk, vol, bid, ask = marks.get(n, (None, None, None, None)) + out["perp"][x["base_currency"]] = dict( + instr=n, min_amt=float(x["min_trade_amount"]), + created=dt.datetime.fromtimestamp(x["creation_timestamp"] / 1000, + dt.timezone.utc).date().isoformat(), + mark=mk, min_usd=(mk * float(x["min_trade_amount"])) if mk else None, + vol24=vol, bid=bid, ask=ask) + return out + + +def sezione1(V: dict) -> dict: + sez("1) IL VENUE, LETTO ORA β€” quali delle 19 gambe di XS01 esistono su Deribit (perp USDC)") + if not V["read_ok"]: + print(" ⚠️ lista strumenti NON letta dal venue in questa corsa: si usa la cache.") + oggi = dt.date.today() + print(f" {'asset':<7}{'strumento':<26}{'quotato dal':<13}{'eta' + chr(39) + ' (g)':>10}" + f"{'lotto min':>11}{'vol 24h $':>15}{'quote':>10}") + ok, stab = [], [] + for s in XS_UNIVERSE: + p = V["perp"].get(s) + if not p: + print(f" {s:<7}{'β€” non quotato β€”':<26}{'β€”':<13}{'β€”':>10}{'β€”':>11}{'β€”':>15}{'β€”':>10}") + continue + ok.append(s) + eta = (oggi - dt.date.fromisoformat(p["created"])).days + if eta >= 365: + stab.append(s) + q = "bid+ask" if (p["bid"] and p["ask"]) else ("SOLO 1 LATO" if (p["bid"] or p["ask"]) + else "NESSUNA") + mu = f"${p['min_usd']:.2f}" if p["min_usd"] else "n/d" + v = f"{p['vol24']:,.0f}" if p["vol24"] is not None else "n/d" + print(f" {s:<7}{p['instr']:<26}{p['created']:<13}{eta:>10}{mu:>11}{v:>15}{q:>10}") + print(f"\n -> quotate: {len(ok)}/19 ({', '.join(ok)})") + print(f" -> quotate da >= 1 anno: {len(stab)}/19 ({', '.join(stab)})") + print(f" -> MANCANTI: {', '.join([s for s in XS_UNIVERSE if s not in ok])}") + print(" πŸ“Œ Il lotto minimo NON e' il vincolo: il piu' caro sta sotto la decina di dollari.") + print(" E' la stessa constatazione dell'ondata 22/08 (l'eseguibilita' a $600 non ha") + print(" bocciato niente per sei volte di fila). Il muro, se c'e', e' l'edge.") + return dict(quotate=ok, stabili=stab) + + +# ============================================================================================= +# 2) REPLICHE E CONTROLLI POSITIVI β€” prima di ogni delta +# ============================================================================================= +def sezione2(stab: list[str]) -> None: + sez("2) REPLICHE BIT-EXACT E CONTROLLI POSITIVI (un rilevatore non validato e' indistinguibile" + " da uno rotto)") + + sub("2a) il mio motore contro lo sleeve di PRODUZIONE") + mine = run(XS_UNIVERSE)["net"] + ref = to_daily(SL._xsec_returns()) + j = pd.concat({"a": mine, "b": ref}, axis=1, join="outer") + dm = float(np.nanmax(np.abs(j["a"].values - j["b"].values))) + print(f" run(19 gambe, canonico, fase 0) vs sleeves._xsec_returns : max|dif| = {dm:.3e}" + f" {'OK' if dm < 1e-12 else 'FAIL'}") + assert dm < 1e-12, "replica NON bit-exact: tutto il resto misurerebbe un'altra strategia" + print(f" Sharpe FULL 19 gambe {sh(mine):.3f} Β· HOLD {sh_hold(mine):.3f} " + f"(Β§50 riporta 1,265 col drag di funding dichiarato: qui il funding NON c'e', perche'") + print(" la domanda e' sulla STRUTTURA del segnale e il drag e' una costante additiva.)") + + sub("2b) CONTROLLO POSITIVO dello stimatore di AMPIEZZA (deve dare 1 e N sui casi noti)") + rng = np.random.default_rng(SEED) + ind = pd.DataFrame(rng.normal(size=(900, 11))) + one = pd.DataFrame(np.tile(rng.normal(size=(900, 1)), (1, 11))) + hlf = ind.copy() + common = rng.normal(size=900) + hlf = pd.DataFrame(0.7 * np.tile(common[:, None], (1, 11)) + 0.7 * ind.values) + for nm, M, atteso in (("11 gambe INDIPENDENTI", ind, "~11"), + ("11 gambe IDENTICHE", one, "1"), + ("11 gambe con fattore comune 50%", hlf, "fra 1 e 11")): + ne_r, rb = neff_rbar(M) + print(f" {nm:<34} rbar {rb:>+7.3f} N_eff(rbar) {ne_r:>8.2f} " + f"N_eff(autovalori) {neff_eig(M):>6.2f} atteso {atteso}") + print(" -> entrambi gli stimatori passano il controllo; la differenza fra i due si vede") + print(" solo quando rbar e' NEGATIVA, ed e' esattamente il caso di XS01 (sezione 3).") + + sub("2c) CONTROLLO POSITIVO dello stimatore di IC (con un segnale che BARA deve dare ~+1)") + def _t(m, se): + return "t inf (SE=0: IC costante)" if se <= 0 else f"t {m/se:+.1f}" + m, se, n = ic_stats(XS_UNIVERSE, cheat=True) + print(f" IC di un punteggio = ritorno FUTURO (look-ahead deliberato): {m:+.4f} " + f"({_t(m, se)}, n={n}) -> lo stimatore ha potenza") + m0, se0, n0 = ic_stats(XS_UNIVERSE, shuffle_seed=SEED) + print(f" IC di un punteggio PERMUTATO a caso : {m0:+.4f} " + f"({_t(m0, se0)}, n={n0}) -> e non inventa segnale dove non c'e'") + + sub("2d) identita' con la funzione importata da XSR01") + R = run(stab, k=2) + L = leg_pnl(R) + P = pd.DataFrame(R["W"], index=R["idx"], columns=R["cols"]) + S = pd.DataFrame(R["dret"], index=R["idx"], columns=R["cols"]) + ne_mine, _ = neff_rbar(L) + print(f" la mia `neff_rbar` e la `eff_breadth` di r0725_statarb_basket_gate implementano la") + print(f" STESSA formula N/(1+(N-1)*rbar); qui do' {ne_mine:.2f} sulla P&L per gamba di XS01.") + print(f" (`eff_breadth` non e' chiamabile direttamente su XS01: prende matrici di posizione e") + print(f" di ritorno-SPREAD di COPPIE, e XS01 non e' un paniere di coppie β€” vedi sezione 3.)") + _ = (P, S, XSR_EFF_BREADTH) + + +# ============================================================================================= +# IC β€” l'informazione del segnale, misurata direttamente +# ============================================================================================= +def ic_stats(universe: list[str], lookbacks=None, H: int | None = None, resid: bool = False, + cheat: bool = False, shuffle_seed: int | None = None, + every_day: bool = True, phase: int = 0): + """Rank-IC fra il punteggio e il ritorno a H giorni in avanti, con SE a blocchi (30 barre). + every_day=True usa TUTTI i giorni (osservazioni sovrapposte -> la SE a blocchi ne tiene conto) + perche' sui soli giorni di ribilanciamento le osservazioni sono 87 e non decidono niente.""" + lookbacks = CFG["lookbacks"] if lookbacks is None else lookbacks + H = CFG["H"] if H is None else H + C = prices(universe); px = C.values; n, A = px.shape + mlb = max(lookbacks) + dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) + bi = list(C.columns).index("BTC") if "BTC" in C.columns else 0 + rng = np.random.default_rng(shuffle_seed) if shuffle_seed is not None else None + ics = [] + for i in range(mlb, n - H): + if not every_day and i % H != phase: + continue + fwd = px[i + H] / px[i] - 1.0 + if cheat: + score = fwd.copy() + else: + rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks] + if resid: + seg = dret[max(1, i - 90):i + 1] + b = np.array([np.cov(seg[:, j], seg[:, bi])[0, 1] / max(np.var(seg[:, bi]), 1e-12) + for j in range(A)]) + rLs = [rL - b * rL[bi] for rL in rLs] + score = np.zeros(A); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if not cnt: + continue + score /= cnt + if rng is not None: + score = score[rng.permutation(A)] + ics.append(spearmanr(score, fwd).statistic) + x = np.asarray(ics, float) + return float(x.mean()), block_se(x), len(x) + + +def block_se(x: np.ndarray, bl: int = 30, B: int = 500, seed: int = SEED) -> float: + rng = np.random.default_rng(seed); n = len(x); nb = int(np.ceil(n / bl)); out = [] + for _ in range(B): + st = rng.integers(0, max(1, n - bl), size=nb) + out.append(np.concatenate([x[s:s + bl] for s in st])[:n].mean()) + return float(np.std(out, ddof=1)) + + +def tail_spread(universe: list[str], k: int, resid: bool = False, lookbacks=None, + H: int | None = None): + """Rendimento a H giorni dei top-k MENO quello dei bottom-k: e' cio' che il portafoglio + incassa davvero (l'IC su tutta la sezione puo' essere 0 e le CODE portare informazione).""" + lookbacks = CFG["lookbacks"] if lookbacks is None else lookbacks + H = CFG["H"] if H is None else H + C = prices(universe); px = C.values; n, A = px.shape + mlb = max(lookbacks); kk = min(k, A // 2) + dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) + bi = list(C.columns).index("BTC") if "BTC" in C.columns else 0 + d = [] + for i in range(mlb, n - H): + rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks] + if resid: + seg = dret[max(1, i - 90):i + 1] + b = np.array([np.cov(seg[:, j], seg[:, bi])[0, 1] / max(np.var(seg[:, bi]), 1e-12) + for j in range(A)]) + rLs = [rL - b * rL[bi] for rL in rLs] + score = np.zeros(A); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if not cnt: + continue + score /= cnt + o = np.argsort(score); f = px[i + H] / px[i] - 1.0 + d.append(float(f[o[-kk:]].mean() - f[o[:kk]].mean())) + x = np.asarray(d, float) + return x.mean(), block_se(x), len(x) + + +def z_spread(universe: list[str], k: int) -> float: + """Distanza media, in z-score, fra il gruppo long e il gruppo short: la SELETTIVITA'.""" + C = prices(universe); px = C.values; n, A = px.shape + lookbacks = CFG["lookbacks"]; mlb = max(lookbacks); kk = min(k, A // 2) + out = [] + for i in range(mlb, n): + rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks] + score = np.zeros(A); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if not cnt: + continue + score /= cnt; o = np.argsort(score) + out.append(float(score[o[-kk:]].mean() - score[o[:kk]].mean())) + return float(np.mean(out)) + + +# ============================================================================================= +# 3) IL GATE β€” l'ampiezza effettiva +# ============================================================================================= +def sezione3(stab: list[str], quot: list[str]) -> dict: + sez("3) IL GATE β€” AMPIEZZA EFFETTIVA. Se le 11 gambe ne hanno gia' ~11, non c'e' niente da" + " recuperare") + + sub("3a) prima di misurare: il DEMEANING (la costruzione che creo' XSR01) qui e' un NO-OP" + " ALGEBRICO") + R = run(stab) + msum = float(np.abs(R["W"].sum(axis=1)).max()) + Rd = run(stab, demean=True) + dmx = float(np.nanmax(np.abs(R["net"].values - Rd["net"].values))) + print(f" XS01 mette +0,5/k sui top-k e -0,5/k sui bottom-k => sum_i w_i = 0 PER COSTRUZIONE.") + print(f" max|sum_i w_i| su tutte le barre : {msum:.3e}") + print(f" max|serie demeanata - serie canonica| : {dmx:.3e} -> IDENTICHE") + print(" πŸ“Œ XSR01 nacque perche' 50 coppie condividevano la gamba BTC: li' c'era un fattore") + print(" comune GROSSO nelle POSIZIONI, e toglierlo cambiava il portafoglio. XS01 e' gia'") + print(" dollar-neutral esatto: il demeaning non ha niente da togliere. Non e' un risultato") + print(" debole, e' un'IDENTITA' β€” e vale a ogni universo, a ogni k, per sempre.") + + sub("3b) LA TABELLA CHE DECIDE β€” N_eff della P&L per gamba") + print(f" {'universo / costruzione':<34}{'gambe':>7}{'rbar':>9}{'N_eff rbar':>13}" + f"{'N_eff autoval.':>16}{'Sh FULL':>10}") + rows = [("19 HL (canonico)", XS_UNIVERSE, dict()), + (f"{len(quot)} quotate Deribit (canonico)", quot, dict()), + (f"{len(stab)} Deribit >=1 anno (canonico)", stab, dict()), + (f"{len(stab)} Deribit β€” demean", stab, dict(demean=True)), + (f"{len(stab)} Deribit β€” resid vs BTC", stab, dict(kind="resid")), + (f"{len(stab)} Deribit β€” equal-risk", stab, dict(kind="ivol")), + (f"{len(stab)} Deribit β€” whitening", stab, dict(kind="white")), + (f"{len(stab)} Deribit β€” nudo L=30", stab, dict(lookbacks=(30,), disp_pct=0)), + (f"{len(stab)} Deribit β€” k=2", stab, dict(k=2)), + (f"{len(stab)} Deribit β€” H=20", stab, dict(H=20))] + out = {} + for nm, u, kw in rows: + r = run(u, **kw); L = leg_pnl(r) + ne_r, rb = neff_rbar(L); ne_e = neff_eig(L) + out[nm] = dict(neff=ne_e, sharpe=sh(r["net"])) + print(f" {nm:<34}{L.shape[1]:>7}{rb:>+9.3f}{ne_r:>13.2f}{ne_e:>16.2f}{sh(r['net']):>10.3f}") + + print("\n ⚠️ LA FORMULA IMPORTATA DA XSR01 DEGENERA QUI, e va detto invece di usarla: con rbar") + print(" NEGATIVA, N/(1+(N-1)*rbar) esplode sopra N (fino a ~33 su 11 gambe). Non e' un") + print(" errore di quella formula: e' che su un paniere di COPPIE rbar e' positiva (fattore") + print(" comune), mentre le gambe di un long/short cross-sezionale sono ANTI-correlate per") + print(" costruzione. Da qui in poi si legge il PARTICIPATION RATIO degli autovalori, che") + print(" sta sempre in [1, N] ed e' la definizione che il briefing nomina per prima.") + + n19 = out["19 HL (canonico)"]["neff"]; n11 = out[f"{len(stab)} Deribit >=1 anno (canonico)"]["neff"] + s19 = out["19 HL (canonico)"]["sharpe"]; s11 = out[f"{len(stab)} Deribit >=1 anno (canonico)"]["sharpe"] + pred = s19 * math.sqrt(n11 / n19) + print(f"\n πŸ“Œ RISPOSTA AL GATE: N_eff passa da {n19:.2f} (19 gambe) a {n11:.2f} (11 gambe) = " + f"-{100*(1-n11/n19):.0f}%,") + print(f" mentre lo Sharpe passa da {s19:.3f} a {s11:.3f} = -{100*(1-s11/s19):.0f}%.") + print(f" Sotto la legge fondamentale (IR = IC * sqrt(ampiezza)) la sola perdita di ampiezza") + print(f" predirebbe {pred:.3f}, cioe' {100*(s19-pred)/(s19-s11):.0f}% del divario. Il resto β€” " + f"l'{100-100*(s19-pred)/(s19-s11):.0f}% β€” NON e' ampiezza.") + print(" E NESSUNA costruzione la muove: tutte le righe stanno in una banda strettissima.") + print(" β‡’ Il filone COME POSTO si chiude qui: non c'e' ampiezza da recuperare. La sezione 4") + print(" misura dove sta davvero il divario, perche' 'non e' quello' non e' una diagnosi.") + return out + + +# ============================================================================================= +# 4) DOVE STA DAVVERO IL DIVARIO β€” selettivita' e IC +# ============================================================================================= +def sezione4(stab: list[str], quot: list[str]) -> dict: + sez("4) ALLORA DOV'E'? Due canali misurabili: SELETTIVITA' (quanto il ranking separa) e IC" + " (quanto il segnale sa)") + + sub("4a) SELETTIVITA' β€” con k=5 su 11 gambe si tengono 10 posizioni su 11: non si seleziona") + print(f" {'universo':<22}{'A':>4}{'k':>4}{'posizioni tenute':>19}{'spread z long-short':>22}") + for nm, u in (("19 HL", XS_UNIVERSE), (f"{len(stab)} Deribit", stab)): + for k in (5, 3, 2): + A = len(prices(u).columns); kk = min(k, A // 2) + print(f" {nm:<22}{A:>4}{k:>4}{f'{2*kk}/{A} = {2*kk/A:.0%}':>19}{z_spread(u, k):>22.3f}") + print(" πŸ“Œ `k=5` NON e' un parametro invariante di scala: fu tarato su A=19, dove seleziona il") + print(" 26% per lato. Su A=11 lo stesso 5 e' il 45% per lato. Trasferire il meccanismo a un") + print(" universo nuovo COI PARAMETRI CONGELATI (regola SOL, 22/08) qui vuol dire congelare") + print(" il QUANTILE, non il numero β€” ed e' una distinzione che cambia il risultato.") + + sub("4b) IC β€” il segnale sa qualcosa, su queste 11 gambe? (rank-IC contro il ritorno a 10g)") + print(f" {'universo':<34}{'IC':>10}{'SE blocchi':>13}{'t':>8}{'n':>7}") + ic = {} + for nm, u in ((f"19 HL (l'universo di XS01)", XS_UNIVERSE), + (f"{len(quot)} quotate Deribit", quot), + (f"{len(stab)} Deribit >=1 anno", stab), + (f"9 alt maturi (11 senza BTC/ETH)", [s for s in stab if s not in ("BTC", "ETH")]), + (f"8 gambe MANCANTI da sole", [s for s in XS_UNIVERSE if s not in stab])): + m, se, n = ic_stats(u); ic[nm] = (m, se) + print(f" {nm:<34}{m:>+10.4f}{se:>13.4f}{m/se:>8.2f}{n:>7}") + print("\n πŸ“Œ QUESTO e' il risultato del filone. Sulle 11 gambe che Deribit quota da un anno il") + print(" segnale NON ha informazione, e sui 9 alt maturi presi da soli e' NEGATIVO. Le 8") + print(" mancanti da sole non ce l'hanno nemmeno loro: l'informazione vive nel CONTRASTO fra") + print(" i due gruppi. XS01 e', in buona parte, una rotazione fra FASCE (major maturi contro") + print(" alt recenti ad alta dispersione) β€” non una selezione DENTRO una fascia.") + print(" β‡’ Nessuna costruzione puo' recuperare informazione che nel campione non c'e'.") + + sub("4c) e non e' 'sfortuna di listino' dentro una banda: e' FUORI dalla banda") + rng = np.random.default_rng(SEED) + subs = [list(rng.choice(XS_UNIVERSE, size=len(stab), replace=False)) for _ in range(N_SUBSETS)] + ics = np.array([ic_stats(u)[0] for u in subs]) + mine = ic_stats(stab)[0] + pct = 100.0 * (ics < mine).mean() + print(f" IC di {N_SUBSETS} sottoinsiemi CASUALI da {len(stab)} gambe fra le 19:") + print(f" mediana {np.median(ics):+.4f} [p10 {np.percentile(ics,10):+.4f}, " + f"p90 {np.percentile(ics,90):+.4f}] min {ics.min():+.4f}") + print(f" il sottoinsieme DERIBIT: {mine:+.4f} -> {pct:.0f}Β° percentile") + print(" πŸ“Œ Β§50 lo chiamava 'sfortuna di listino' e lo collocava al 9Β° percentile dello SHARPE.") + print(" Sull'IC β€” che e' la grandezza di cui lo Sharpe e' una conseguenza β€” sta sotto") + print(f" l'INTERO campione. Non e' una coda della distribuzione: le {len(stab)} gambe quotate da") + print(" un anno sono i major maturi, cioe' esattamente il sottoinsieme meno disperso.") + print(" Aspettare 2-3 listing non basta, e adesso si sa PERCHE'.") + return dict(ic=ic, subset_ics=ics, ic_deribit=mine) + + +# ============================================================================================= +# 5) LE COSTRUZIONI β€” la griglia dichiarata, de-luckata di fase +# ============================================================================================= +KINDS = ("canon", "resid", "ivol", "white") +KS = (1, 2, 3, 4, 5) +LBS = ((30, 90), (30,)) +HS = (10, 20) + + +def grid_cells(): + for kind, k, lb, H in itertools.product(KINDS, KS, LBS, HS): + yield dict(kind=kind, k=k, lookbacks=lb, H=H, + disp_pct=(CFG["disp_pct"] if lb == (30, 90) else 0)) + + +def band(u: list[str], **kw) -> np.ndarray: + return np.array([sh(run(u, phase=p, **kw)["net"]) for p in range(N_PHASES)]) + + +def sezione5(stab: list[str]) -> dict: + sez("5) LE COSTRUZIONI β€” griglia DICHIARATA (80 celle), e la banda di FASE che le de-lucka") + + sub("5a) la griglia alla fase canonica (fase 0) β€” e perche' non ci si puo' fermare qui") + cells = list(grid_cells()) + rows = [] + for c in cells: + s = run(stab, **c)["net"] + rows.append(dict(cell=c, ins=sh_ins(s), hold=sh_hold(s), full=sh(s), series=s)) + fulls = np.array([r["full"] for r in rows]) + print(f" {len(cells)} celle: FULL mediana {np.median(fulls):+.3f} max {fulls.max():+.3f} " + f"sd {fulls.std(ddof=1):.3f}") + best_is = max(rows, key=lambda r: r["ins"]) + best_ho = max(rows, key=lambda r: r["hold"]) + print(f"\n {'scelta':<22}{'cella':<44}{'IS':>8}{'HOLD':>8}{'FULL':>8}") + for nm, r in (("IN-SAMPLE-ONLY (onesta)", best_is), ("sull'HOLD-OUT (vietata)", best_ho)): + c = r["cell"] + lab = "%s k=%d lb=%s H=%d" % (c["kind"], c["k"], c["lookbacks"], c["H"]) + print(f" {nm:<22}{lab:<44}{r['ins']:>8.3f}{r['hold']:>8.3f}{r['full']:>8.3f}") + + sub("5b) BANDA DI FASE β€” la lente onesta di XS01 (ciclo H=10; i numeri headline sono sulla" + " fase 0, che CLAUDE.md registra come fortunata)") + casi = { + "canonico k=5 (11)": (stab, dict()), + "k=2 (11)": (stab, dict(k=2)), + "k=3 (11)": (stab, dict(k=3)), + "resid k=5 (11)": (stab, dict(kind="resid")), + "resid k=2 (11)": (stab, dict(kind="resid", k=2)), + "equal-risk k=2 (11)": (stab, dict(kind="ivol", k=2)), + "whitening k=2 (11)": (stab, dict(kind="white", k=2)), + "nudo L=30 k=2 (11)": (stab, dict(lookbacks=(30,), disp_pct=0, k=2)), + "H=20 k=2 (11)": (stab, dict(H=20, k=2)), + "canonico k=5 (19) RIF.": (XS_UNIVERSE, dict()), + "k=2 (19)": (XS_UNIVERSE, dict(k=2)), + } + B = {} + print(f" {'cella':<26}{'fase0':>9}{'MEDIANA':>10}{'p10':>9}{'p90':>9}{'>0':>7}" + f"{'pctl fase0':>12}") + for nm, (u, kw) in casi.items(): + v = band(u, **kw); B[nm] = v + print(f" {nm:<26}{v[0]:>9.3f}{np.median(v):>10.3f}{np.percentile(v,10):>9.3f}" + f"{np.percentile(v,90):>9.3f}{100*(v>0).mean():>6.0f}%" + f"{100*(v11.0f}Β°") + + sub("5c) DIFFERENZE APPAIATE per fase (mediana delle differenze, non differenza delle mediane)") + def dd(a, b): + d = B[a] - B[b] + return np.median(d), 100.0 * (d > 0).mean() + for a, b in (("k=2 (11)", "canonico k=5 (11)"), + ("k=3 (11)", "canonico k=5 (11)"), + ("resid k=2 (11)", "k=2 (11)"), + ("equal-risk k=2 (11)", "k=2 (11)"), + ("whitening k=2 (11)", "k=2 (11)"), + ("nudo L=30 k=2 (11)", "k=2 (11)"), + ("H=20 k=2 (11)", "k=2 (11)"), + ("k=2 (19)", "canonico k=5 (19) RIF.")): + m, f = dd(a, b) + print(f" {a:<26} MENO {b:<26} mediana {m:>+7.3f} positivo in {f:>3.0f}% delle fasi") + print("\n πŸ“Œ Il canale che porta TUTTO e' `k`, e solo sull'universo POVERO: su 19 gambe k=2 e") + print(" k=5 sono la stessa cosa. E' la firma della SELETTIVITA': k conta quando e' una") + print(" frazione grande dell'universo. Le tre costruzioni 'da ampiezza' (equal-risk,") + print(" whitening, residualizzazione) valgono, sopra a quello, poco o niente.") + + sub("5d) plateau su k (mediana delle 10 fasi) β€” punta o regione?") + print(f" {'k':>3}{'tenuto/lato (11)':>19}{'canon 11':>11}{'resid 11':>11}{'canon 19':>11}") + for k in KS: + a = np.median(band(stab, k=k)); b = np.median(band(stab, kind="resid", k=k)) + c = np.median(band(XS_UNIVERSE, k=k)) + print(f" {k:>3}{f'{min(k,5)}/11 = {min(k,5)/11:.0%}':>19}{a:>11.3f}{b:>11.3f}{c:>11.3f}") + print(" -> su 11 gambe la curva e' MONOTONA e la regione k in {1,2,3} e' un plateau, non una") + print(" punta; su 19 gambe e' piatta. Il meccanismo e' strutturale, non una cella fortunata.") + return dict(rows=rows, bands=B, best_is=best_is, best_ho=best_ho) + + +# ============================================================================================= +# 6) IL NULL CHE CONTA β€” permutazione cross-sezionale a FEE ZERO +# ============================================================================================= +def perm_series(universe: list[str], seed: int, k: int, kind: str = "canon", + lookbacks=None, H: int | None = None, disp_pct: int | None = None, + phase: int = 0) -> pd.Series: + """Identico a `run` ma il punteggio viene PERMUTATO fra gli asset a ogni ribilanciamento, e la + fee e' ZERO da entrambe le parti del confronto (25/07: un null a fee piena perde per COSTO).""" + lookbacks = CFG["lookbacks"] if lookbacks is None else lookbacks + H = CFG["H"] if H is None else H + disp_pct = CFG["disp_pct"] if disp_pct is None else disp_pct + C = prices(universe); px = C.values; n, A = px.shape + kk = min(k, A // 2); mlb = max(lookbacks) + dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) + bi = list(C.columns).index("BTC") if "BTC" in C.columns else 0 + rng = np.random.default_rng(seed) + W = np.zeros((n, A)); w = np.zeros(A); disp_hist: list[float] = [] + for i in range(n): + if i >= mlb and i % H == phase: + rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks] + if kind == "resid": + seg = dret[max(1, i - 90):i + 1] + b = np.array([np.cov(seg[:, j], seg[:, bi])[0, 1] / max(np.var(seg[:, bi]), 1e-12) + for j in range(A)]) + rLs = [rL - b * rL[bi] for rL in rLs] + disp_i = float(np.mean([r.std() for r in rLs])) + thr = (np.percentile(disp_hist, disp_pct) + if (disp_pct > 0 and len(disp_hist) >= CFG["disp_minhist"]) else -np.inf) + if disp_i >= thr: + score = np.zeros(A); cnt = 0 + for rL in rLs: + sd = rL.std() + if sd > 0: + score += (rL - rL.mean()) / sd; cnt += 1 + if cnt: + score = score[rng.permutation(A)] / cnt + o = np.argsort(score); w = np.zeros(A) + w[o[-kk:]] = 0.5 / kk; w[o[:kk]] = -0.5 / kk + else: + w = np.zeros(A) + disp_hist.append(disp_i) + W[i] = w + gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) + s = pd.Series(gross, index=C.index) + rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) + sc = np.clip(np.nan_to_num(CFG["target_vol"] / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) + return to_daily(pd.Series(s.values * sc, index=C.index)) + + +def sezione6(stab: list[str]) -> None: + sez("6) IL NULL CHE CONTA β€” permutazione cross-sezionale, ENTRAMBI I LATI A FEE ZERO") + print(" Perche' a fee zero (regola 25/07): permutare un punteggio ne fa esplodere il turnover;") + print(" a fee piena il null perderebbe per COSTO invece che per assenza d'informazione, e il") + print(" p-value uscirebbe trionfale e falso.") + print(f"\n {'cella':<26}{'candidato':>11}{'null med':>11}{'null p95':>11}{'p':>8}") + casi = [("canonico k=5 (11)", stab, dict(k=5)), + ("k=2 (11)", stab, dict(k=2)), + ("resid k=2 (11)", stab, dict(k=2, kind="resid")), + ("canonico k=5 (19) RIF.", XS_UNIVERSE, dict(k=5))] + for nm, u, kw in casi: + cand = sh(run(u, fee=0.0, **kw)["net"]) + nl = np.array([sh(perm_series(u, 9000 + j, **kw)) for j in range(N_PERM)]) + p = float((nl >= cand).mean()) + print(f" {nm:<26}{cand:>+11.3f}{np.median(nl):>+11.3f}{np.percentile(nl,95):>+11.3f}" + f"{p:>8.3f}") + print("\n βœ… CONTROLLO POSITIVO: il null HA potenza β€” sull'universo dove il segnale esiste (19") + print(" gambe) il candidato batte le permutazioni, su quello dove non esiste no. Un null") + print(" che non separasse i due casi non starebbe misurando niente.") + + sub("6b) e il p-value stesso va DE-LUCKATO: la riga sopra e' calcolata alla fase 0, che per" + " `resid k=2` sta al 90Β° percentile della sua banda") + print(" Nessuno in questo progetto aveva ancora de-luckato un p-value. Qui serve: se il") + print(" candidato e' valutato all'ancora fortunata, il suo p e' l'ancora fortunata del p.") + print(f"\n {'fase':>6}{'candidato':>12}{'p (100 perm)':>15}") + ps = [] + for ph in range(N_PHASES): + cand = sh(run(stab, fee=0.0, k=2, kind="resid", phase=ph)["net"]) + nl = np.array([sh(perm_series(stab, 7000 + 97 * ph + j, k=2, kind="resid", phase=ph)) + for j in range(100)]) + pv = float((nl >= cand).mean()); ps.append(pv) + print(f" {ph:>6}{cand:>+12.3f}{pv:>15.3f}") + ps = np.array(ps) + print(f"\n p MEDIANO sulle {N_PHASES} fasi: {np.median(ps):.3f} " + f"[min {ps.min():.3f}, max {ps.max():.3f}] sotto 0,05 in {100*(ps<0.05).mean():.0f}% " + f"delle fasi") + print(" πŸ“Œ Alla fase canonica `resid k=2` sembra battere il rumore; alla fase MEDIANA no.") + print(" Il verdetto del null cambia con l'ancora, e l'ancora e' una scelta, non un dato.") + + +# ============================================================================================= +# 7) IL GATE ONESTO + MDE +# ============================================================================================= +def sezione7(stab: list[str], G: dict) -> None: + sez("7) IL GATE ONESTO β€” i tre componenti reali di `study_family_honest`, e l'MDE") + print(" ⚠️ `altlib.study_family_honest` e' cablato sui candidati DIREZIONALI (factory ->") + print(" target_fn via `candidate_daily`) e non accetta uno sleeve cross-sezionale gia'") + print(" espresso in RENDIMENTI. Come per il gate del tenore di VRP01 (30/07) si usano i") + print(" suoi TRE componenti reali, importati e non riscritti.") + + rows, best_is, best_ho = G["rows"], G["best_is"], G["best_ho"] + sub("7a) (i) SELEZIONE IN-SAMPLE-ONLY β€” stessa regola di `altlib.select_cell_insample`") + src = al.select_cell_insample.__doc__ or "" + print(f" regola importata: 'rank grid cells by IN-SAMPLE (pre-HOLDOUT) standalone Sharpe'" + f" [{'trovata nel sorgente' if 'IN-SAMPLE' in src else 'NON verificata'}]") + c = best_is["cell"] + print(f" cella scelta AL BUIO: kind={c['kind']} k={c['k']} lookbacks={c['lookbacks']} " + f"H={c['H']}") + print(f" IS {best_is['ins']:+.3f} HOLD {best_is['hold']:+.3f} FULL {best_is['full']:+.3f}") + ch = best_ho["cell"] + print(f" (chi avesse scelto sull'HOLD-OUT prendeva kind={ch['kind']} k={ch['k']} " + f"lb={ch['lookbacks']} H={ch['H']}: HOLD {best_ho['hold']:+.3f})") + sub2 = [r for r in rows if r["cell"]["k"] >= 2] + b2 = max(sub2, key=lambda r: r["ins"]) + c2 = b2["cell"] + print(f"\n 🚨 ERRORE CATTURATO SU ME STESSO β€” la scelta al buio NON e' stabile alla") + print(f" DICHIARAZIONE della griglia. La mia prima stesura dichiarava k in {{2,3,4,5}}") + print(f" ({len(sub2)} celle) e li' la cella al buio era: kind={c2['kind']} k={c2['k']} " + f"lb={c2['lookbacks']} H={c2['H']}") + print(f" -> IS {b2['ins']:+.3f} HOLD {b2['hold']:+.3f} FULL {b2['full']:+.3f}, cioe' un" + f" candidato che REGGE fuori campione.") + print(f" Aggiungendo k=1 (che e' solo un'estensione del plateau, decisa PRIMA di") + print(f" guardare) la scelta passa a una cella che fa HOLD {best_is['hold']:+.3f}.") + print(" β‡’ Con ~1 anno di in-sample la selezione in-sample-only non e' un gate, e' una") + print(" monetina: la stessa procedura onesta da' due candidati opposti a seconda di") + print(" una scelta di perimetro arbitraria. E' la lezione del 22/08 sul deflated-") + print(" Sharpe ('la stessa griglia da' verdetti opposti secondo come la si partiziona')") + print(" vista un passo prima, sulla SELEZIONE invece che sulla deflazione.") + + sub("7b) (ii) DEFLATED SHARPE su TUTTE le celle dichiarate (`altlib.deflated_sharpe`)") + all_full = [r["full"] for r in rows] + dsr, sr0 = al.deflated_sharpe(best_is["full"], all_full, best_is["series"]) + print(f" N celle {len(all_full)} sd(Sharpe di griglia) {np.std(all_full, ddof=1):.3f}") + print(f" massimo atteso dal SOLO RUMORE: {sr0:.3f} candidato: {best_is['full']:.3f}") + print(f" DSR = {dsr:.3f} -> {'PASS' if dsr >= 0.95 else 'FAIL'} (soglia 0,95)") + med_phase = np.median(band(stab, **{k: v for k, v in best_is["cell"].items()})) + print(f" ⚠️ e il confronto giusto e' col numero DE-LUCKATO: la stessa cella a fase MEDIANA") + print(f" fa {med_phase:+.3f}, cioe' {'SOTTO' if med_phase < sr0 else 'sopra'} il massimo " + f"atteso dal rumore ({sr0:.3f}).") + + sub("7c) (iii) `altlib.marginal_vs_tp01` sulla cella scelta al buio") + try: + m = al.marginal_vs_tp01(best_is["series"]) + print(f" verdetto {m.get('marginal_verdict')} corr {m.get('corr_full')} " + f"robust_oos {m.get('robust_oos')} insample_edge {m.get('has_insample_edge')} " + f"is_hedge {m.get('is_hedge')} beats_noise {m.get('beats_noise_null')}") + except Exception as e: # noqa: BLE001 + print(f" non calcolabile: {type(e).__name__}: {e}") + print(" ⚠️ La serie comincia nel 2024: `marginal_vs_tp01` ha ~1 anno di in-sample e il suo") + print(" verdetto qui e' debole PER COSTRUZIONE (Β§50 lo dichiara sugli stessi dati).") + + sub("7d) MDE β€” quanta risoluzione ha davvero questo campione (in barre ATTIVE)") + R = run(stab, **best_is["cell"]) + att = np.abs(R["W"]).sum(axis=1) > 1e-12 + idx = R["idx"] + n_tot, n_att = len(idx), int(att.sum()) + n_h = int(att[idx >= HOLDOUT].sum()); n_i = int(att[idx < HOLDOUT].sum()) + print(f" campione: {n_tot} giorni ({idx.min().date()} -> {idx.max().date()}), di cui ATTIVI" + f" {n_att} ({n_att/n_tot:.0%})") + print(f" in-sample attive {n_i} Β· hold-out attive {n_h}") + print(f"\n {'finestra':<28}{'anni attivi':>13}{'SE(Sharpe)':>13}{'MDE 80%/5%':>13}") + for nm, na, s0 in (("FULL", n_att, best_is["full"]), ("in-sample (2024)", n_i, best_is["ins"]), + ("hold-out (2025+)", n_h, best_is["hold"])): + yrs = na / 365.25 + se = math.sqrt((1 + s0 * s0 / 2) / max(yrs, 1e-9)) + print(f" {nm:<28}{yrs:>13.2f}{se:>13.3f}{2.80*se:>13.3f}") + print("\n πŸ“Œ L'MDE sull'hold-out e' dell'ordine di 2,5-3 di Sharpe. Tutto cio' che questo") + print(" filone recupera (0,7-0,9) sta SOTTO la risoluzione del campione: e la selezione") + print(" in-sample gira su ~1 anno, dove SE(Sharpe) ~ 1 β€” cioe' e' quasi una monetina.") + + sub("7e) e al livello del SEGNALE la potenza non c'e' proprio (spread di coda, SE a blocchi)") + print(f" {'cella':<26}{'spread coda %/10g':>20}{'SE':>8}{'t':>8}") + for nm, u, kw in (("canonico k=5 (11)", stab, dict(k=5)), + ("k=2 (11)", stab, dict(k=2)), + ("resid k=2 (11)", stab, dict(k=2, resid=True)), + ("canonico k=5 (19) RIF.", XS_UNIVERSE, dict(k=5))): + m, se, n = tail_spread(u, **kw) + print(f" {nm:<26}{100*m:>19.2f}%{100*se:>8.2f}{m/se:>8.2f}") + print(" -> lo Sharpe recuperato su 11 gambe NON e' sostenuto da uno spread misurabile:") + print(" t ~ 0,3-0,6 contro ~1,9 dell'universo pieno. Un rendimento che si vede solo") + print(" nella serie composta e non nel suo ingrediente e' un rendimento da non citare.") + + +# ============================================================================================= +# 8) IL SUBSET NULL SULLA COSTRUZIONE β€” alza la MEDIANA o solo la cella di Deribit? +# ============================================================================================= +def sezione8(stab: list[str]) -> None: + sez("8) LA COSTRUZIONE E' UN MECCANISMO O E' UN FIT SU QUESTE 11? (null dei sottoinsiemi)") + print(" Se `k` piccolo e' un meccanismo, deve alzare la MEDIANA di sottoinsiemi casuali da 11,") + print(" non solo la cella che Deribit quota. (Stessa forma del null che il 25/07 mise GTAA6 al") + print(" 95Β° pctl dei 6-subset.)") + print(" ⚠️ E il confronto va DE-LUCKATO come tutti gli altri: la statistica per sottoinsieme") + print(f" e' la MEDIANA su {len(PH_SUB)} fasi ({', '.join(map(str, PH_SUB))}), non il valore alla fase 0.") + rng = np.random.default_rng(SEED) + subs = [list(rng.choice(XS_UNIVERSE, size=len(stab), replace=False)) for _ in range(N_SUBSETS)] + + def med_ph(u, **kw): + return float(np.median([sh(run(u, phase=ph, **kw)["net"]) for ph in PH_SUB])) + + print(f"\n {'costruzione':<20}{'med subset11':>14}{'p10':>9}{'p90':>9}{'Deribit':>10}" + f"{'pctl':>7}{'(fase 0)':>22}") + for nm, kw in (("canonico k=5", dict()), ("k=2", dict(k=2)), ("k=3", dict(k=3)), + ("resid k=2", dict(kind="resid", k=2))): + v = np.array([med_ph(u, **kw) for u in subs]) + mine = med_ph(stab, **kw) + v0 = np.array([sh(run(u, **kw)["net"]) for u in subs]) + m0 = sh(run(stab, **kw)["net"]) + print(f" {nm:<20}{np.median(v):>14.3f}{np.percentile(v,10):>9.3f}" + f"{np.percentile(v,90):>9.3f}{mine:>10.3f}{100*(v6.0f}Β°" + f"{f'{m0:+.3f} -> {100*(v022}") + print("\n -> `k` piccolo alza la mediana di TUTTI i sottoinsiemi: e' una proprieta' del") + print(" meccanismo su universo piccolo, non un fit sulle gambe di Deribit.") + print(" πŸ“Œ E la colonna di destra e' la ragione per cui si de-lucka: alla fase 0 `resid k=2`") + print(" mette il sottoinsieme di Deribit in ALTO nella banda, e una lettura fermata li'") + print(" avrebbe concluso l'opposto di quella de-luckata. (La prima stesura di questo") + print(" script scriveva 'Deribit resta in fondo a OGNI costruzione': era falso proprio") + print(" sulla cella che sembrava vincere.)") + + +# ============================================================================================= +# 9) ESEGUIBILITA' E NETTING +# ============================================================================================= +def sezione9(stab: list[str]) -> None: + sez("9) ESEGUIBILITA' E NETTING β€” il min-order per gamba, e cio' che NON netta col libro") + + sub("9a) il netting: 2 gambe su 11 stanno sullo stesso strumento del libro live") + inter = [s for s in stab if s in ("BTC", "ETH")] + print(f" Il libro live (`book_net_target`) somma TP01 e SKH01 in UN numero per asset su") + print(f" BTC/ETH e manda UN ordine. Delle {len(stab)} gambe, {len(inter)} ({', '.join(inter)}) " + f"netterebbero;") + print(f" le altre {len(stab)-len(inter)} sono strumenti che il libro NON tocca -> pagano un") + print(" min-order PROPRIO, e richiederebbero una riga in `src/live/deribit._CONTRACT`, cioe'") + print(" codice su un percorso con soldi veri.") + + sub("9b) quante gambe sopravvivono a min_order $5 (stessa regola di" + " `altlib.eval_weights_smallcap`)") + doc = al.eval_weights_smallcap.__doc__ or "" + print(f" regola importata: 'a desired position change whose notional |dw|*capital is below") + print(f" min_order is NOT executed' [{'confermata nel sorgente' if 'min_order' in doc else 'NON verificata'}]") + R = run(stab, k=2) + pos = R["W"] * R["scale"][:, None] # ⚠️ la posizione VERA e' W*scale, non W: + dpos = np.abs(np.diff(pos, axis=0, prepend=0.0)) # il vol-target muove il nozionale OGNI + tick = dpos[dpos > 1e-12] # giorno anche a pesi fermi. + rebal = np.zeros(len(R["W"]), bool) + rebal[1:] = (np.abs(np.diff(R["W"], axis=0)) > 1e-12).any(axis=1) + is_reb = np.repeat(rebal[:, None], pos.shape[1], axis=1)[dpos > 1e-12] + print(f"\n ⚠️ Errore catturato prima di pubblicare: la prima stesura misurava |dW| e trovava") + print(" 'ticket mediano $19, 100% eseguito'. Ma la posizione in dollari e' W*scale, e il") + print(" vol-target muove il nozionale TUTTI i giorni anche a pesi fermi -> quella misura") + print(" vedeva solo i ribilanci di segnale e faceva sparire la popolazione che il") + print(" pavimento taglia davvero. Sotto ci sono i ticket sulla posizione vera.") + print(f"\n {'capitale':>10}{'quota 15%':>12}{'ticket med':>13}{'ordini >= $5':>15}" + f"{'% NOZIONALE eseguito':>23}") + for cap in CAP_LADDER: + alloc = cap * W_SLEEVE + t = tick * alloc + ok = t >= MIN_ORDER + print(f" {('$%.0f' % cap):>10}{('$%.0f' % alloc):>12}{('$%.2f' % np.median(t)):>13}" + f"{f'{100*ok.mean():.0f}%':>15}{f'{100*t[ok].sum()/t.sum():.0f}%':>23}") + a0 = CAPITAL_TODAY * W_SLEEVE + t0 = tick * a0 + print(f"\n Le DUE POPOLAZIONI (Β§45), a ${CAPITAL_TODAY:.0f}:") + print(f" ribilanci di SEGNALE : {100*is_reb.mean():>5.1f}% degli ordini, " + f"{100*t0[is_reb].sum()/t0.sum():>5.1f}% del nozionale, ticket mediano " + f"${np.median(t0[is_reb]):.2f}") + print(f" deriva del VOL-TARGET : {100*(~is_reb).mean():>5.1f}% degli ordini, " + f"{100*t0[~is_reb].sum()/t0.sum():>5.1f}% del nozionale, ticket mediano " + f"${np.median(t0[~is_reb]):.2f}") + print("\n πŸ“Œ Il muro NON e' la taglia (7Βͺ volta nell'ondata): il pavimento taglia soprattutto") + print(" la popolazione piccola (deriva del vol-target), che vale poco nozionale, ed e'") + print(" esattamente il meccanismo misurato in Β§45 quando cadde il '$20k di XS01'.") + print(" Il muro e' l'IC della sezione 4.") + + +# ============================================================================================= +# MAIN +# ============================================================================================= +def main() -> None: + global _ARGS + ap = argparse.ArgumentParser() + ap.add_argument("--no-net", action="store_true", help="usa solo la cache del venue") + _ARGS = ap.parse_args() + + print("=" * 104) + print(" Β§56 XS-AMPIEZZA β€” su un universo POVERO si recupera ampiezza EFFETTIVA senza" + " aggiungere gambe?") + print(f" {dt.datetime.now(dt.timezone.utc):%Y-%m-%d %H:%M UTC} Β· sola lettura Β· nessun ordine Β·" + f" solo GET pubbliche con pacing") + print("=" * 104) + print(" ATTESA A PRIORI " + __doc__.split("ATTESA A PRIORI")[1].split("LA FAMIGLIA")[0].rstrip()) + + V = venue_census() + cens = sezione1(V) + stab, quot = cens["stabili"], cens["quotate"] + if len(stab) < 8: + print("\n ⚠️ meno di 8 gambe stabili: il filone non e' misurabile, mi fermo.") + return + + sezione2(stab) + print(f"\n {clock()} sezione 2 completata") + NEFF = sezione3(stab, quot) + print(f"\n {clock()} sezione 3 completata") + ICS = sezione4(stab, quot) + print(f"\n {clock()} sezione 4 completata") + G = sezione5(stab) + print(f"\n {clock()} sezione 5 completata") + sezione6(stab) + print(f"\n {clock()} sezione 6 completata") + sezione7(stab, G) + print(f"\n {clock()} sezione 7 completata") + sezione8(stab) + print(f"\n {clock()} sezione 8 completata") + sezione9(stab) + + # ------------------------------------------------------------------ verdetto + sez("VERDETTO") + n19 = NEFF["19 HL (canonico)"]["neff"] + n11 = NEFF[f"{len(stab)} Deribit >=1 anno (canonico)"]["neff"] + bands = G["bands"] + med_can = np.median(bands["canonico k=5 (11)"]) + med_k2 = np.median(bands["k=2 (11)"]) + med_19 = np.median(bands["canonico k=5 (19) RIF."]) + d = bands["k=2 (11)"] - bands["canonico k=5 (11)"] + ic19 = ICS["ic"]["19 HL (l'universo di XS01)"] + ic11 = ICS["ic"][f"{len(stab)} Deribit >=1 anno"] + print(f" (1) IL GATE DICE NO: l'ampiezza effettiva scende solo da {n19:.2f} a {n11:.2f} " + f"(-{100*(1-n11/n19):.0f}%) mentre lo") + print(" Sharpe scende dell'86%. Nessuna costruzione la muove, e il DEMEANING β€” la mossa") + print(" che creo' XSR01 β€” e' un NO-OP ALGEBRICO su XS01 (sum dei pesi = 0 per costruzione).") + print(f" (2) IL DIVARIO E' ALTROVE, ed e' DUE cose: SELETTIVITA' (k=5 su 11 gambe tiene 10") + print(f" posizioni su 11: non seleziona) e IC. La selettivita' SI recupera β€” il quantile") + print(f" invariante di scala porta la mediana di fase da {med_can:+.3f} a {med_k2:+.3f} " + f"(delta appaiato") + print(f" {np.median(d):+.3f}, positivo in {100*(d>0).mean():.0f}% delle fasi, plateau su k in " + f"{{1,2,3}}), contro {med_19:+.3f} dell'universo pieno.") + print(f" (3) L'IC NO: {ic11[0]:+.4f} (t {ic11[0]/ic11[1]:+.2f}) sulle {len(stab)} gambe contro " + f"{ic19[0]:+.4f} (t {ic19[0]/ic19[1]:+.2f}) sulle 19,") + print(f" e sotto TUTTI i {N_SUBSETS} sottoinsiemi casuali da {len(stab)}. Sui 9 alt maturi da soli e'") + print(" NEGATIVO: XS01 e' in buona parte una rotazione fra FASCE, non dentro una fascia.") + print(" ⚠️ Sfumatura dovuta: sotto una costruzione a k piccolo il sottoinsieme di Deribit") + print(" risale a meta' banda sullo SHARPE (65-80Β° pctl de-luckato) pur restando ultimo") + print(" sull'IC β€” coerente, perche' con k piccolo contano solo le CODE del ranking e non") + print(" tutta la sezione. Ma lo spread di quelle code ha t ~ 0,2-0,6: e' la stessa") + print(" grandezza del punto (4), non un'obiezione.") + print(" (4) E TUTTO IL RECUPERO STA SOTTO LA RISOLUZIONE DEL CAMPIONE, per quattro strade") + print(" indipendenti: (a) MDE dell'hold-out ~2,3 di Sharpe su ~1,6 anni attivi; (b) lo") + print(" SPREAD DI CODA che dovrebbe pagarlo ha t ~ 0,2-0,6 contro ~1,9 dell'universo") + print(" pieno β€” un rendimento che si vede nella serie composta e non nel suo ingrediente;") + print(" (c) il null di permutazione a fee zero non separa il candidato dal rumore alla") + print(" fase MEDIANA (lo separa solo alla fase 0, che e' la fortunata) mentre lo separa") + print(" su 19 gambe = controllo positivo; (d) il deflated-Sharpe sulla griglia dichiarata") + print(" FALLISCE, e la selezione in-sample-only gira su ~1 anno dove SE(Sharpe) ~ 1,7.") + print(" (4-bis) 🚨 E LA SELEZIONE ONESTA NON E' STABILE ALLA DICHIARAZIONE DELLA GRIGLIA:") + print(" togliendo k=1 dalla famiglia (una scelta di perimetro, non un dato) la cella") + print(" scelta al buio passa da una che crolla fuori campione a una che regge. Con questo") + print(" campione la procedura onesta e' una monetina, e va detto invece di citarne") + print(" l'esito.") + print(" (5) L'eseguibilita' non boccia niente: lotti da $0,09 a ~$8, e a $635 passa gia' la") + print(" maggior parte del nozionale. Il muro e' l'edge, non il capitale.") + print("\n β‡’ `NO β€” E LA CADUTA NON ERA AMPIEZZA: E' SELETTIVITA' (RECUPERABILE, SOTTO L'MDE) PIU'") + print(" UN IC CHE SU QUELLE 11 GAMBE E' ZERO`") + print("\n Cio' che questo filone CORREGGE di Β§50: l'etichetta 'il 62% della caduta e' AMPIEZZA'") + print(" descriveva il NUMERO DI GAMBE, non l'ampiezza effettiva β€” che quasi non si muove. E la") + print(" 'sfortuna di listino' non e' una coda della distribuzione: sull'IC il sottoinsieme di") + print(" Deribit sta sotto l'intero campione, per una ragione strutturale (sono i major maturi).") + print(" Cio' che NON cambia: XS01 resta bloccato dal venue, e aspettare 2-3 listing non basta.") + print("\n πŸ“Œ UNA COSA DA PORTARSI VIA ANCHE FUORI DA QUI: `k=5` non e' un parametro invariante") + print(" di scala. Congelare un meccanismo su un universo nuovo vuol dire congelare il") + print(" QUANTILE, non il numero di posizioni β€” e su XS01 la differenza vale ~0,8 di Sharpe.") + print(f"\n {clock()} fine.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_crossvenue.py b/scripts/research/r0823b_crossvenue.py new file mode 100644 index 0000000..9f890ad --- /dev/null +++ b/scripts/research/r0823b_crossvenue.py @@ -0,0 +1,969 @@ +#!/usr/bin/env python +"""r0823b_crossvenue.py β€” Β§66 CROSS-VENUE: lo scarto Deribit-vs-Hyperliquid come SEGNALE. + +LA DOMANDA +========== +Il progetto usa due venue β€” **Deribit** (dove esegue) e **Hyperliquid** (dove vive l'universo di +XS01) β€” e li ha sempre trattati come *fonti di dato indipendenti da certificare*, mai come *due +mercati fra cui puo' esistere una relazione*. `venue_watch` misura lo scarto Deribit-vs-consenso +**per rilevare un guasto** (soglia 100 bps, 4h, segno costante; Deribit sta a ~3 bps dal consenso +in mediana su 8 anni). Quello stesso scarto non e' mai stato guardato come SEGNALE. + +Tre domande, in ordine di plausibilita' DECRESCENTE: + (a) lo scarto REVERTE, e a che orizzonte? + (b) uno dei due venue GUIDA (lead-lag)? + (c) lo scarto e' un GATE DI REGIME utile a TP01/SKH01? + +ATTESE DICHIARATE PRIMA DI MISURARE (metodo, punto 1) +===================================================== +A1. **Lo scarto e' quasi tutto FATTORE COMUNE.** Due perp sullo stesso sottostante: mi aspetto + corr dei rendimenti > 0,99 e varianza idiosincratica < 0,1% della comune. (Regola del 22/08: + *una divergenza fra due fonti di prezzo si scompone in COMUNE e IDIOSINCRATICA prima di + giudicarla β€” la prima da' sempre la risposta rassicurante.*) +A2. **Taglia attesa dello scarto: 3-9 bps** in mediana β€” stesso ordine dei 3 bps Deribit-consenso + e dei 4-9 bps HL-vs-Binance della certificazione di `fetch_hyperliquid`. +A3. **(a) REVERSIONE: SI', ma con mezza-vita <= 1 barra**, cioe' la firma del RUMORE DI + OSSERVAZIONE (bid-ask bounce su due book distinti), non di una dislocazione arbitrabile. + Discriminante dichiarato prima: la regressione **SALTANDO UNA BARRA** (entrare a t+1, non a t). + Se il coefficiente crolla -> era rumore; se sopravvive -> era dislocazione. +A4. **(b) LEAD-LAG: nessuno rilevabile a >= 5 minuti.** Il lead-lag fra perp liquidi arbitrati vive + a scala sub-secondo. Picco della cross-correlazione atteso a lag 0. +A5. **(c) GATE: NO.** Prior bassissimo β€” il progetto ha gia' refutato 5+ gate come Β«TP01 + travestitoΒ» o Β«ridondante col trendΒ», e un residuo di microstruttura non ha ragione di + predire il regime di trend. Inoltre la finestra comune (HL 1d dal 2024-01) e' **interamente + post-hold-out**, dove `marginal_vs_tp01` non puo' dare ADDS per costruzione (lezione Β§12). +A6. **MURO DI NEGOZIABILITA': il segnale sara' sotto il costo.** Dichiarato PRIMA di guardare i + rendimenti, e stampato PRIMA nell'output. +A7. **STAT-MODE, non deploy.** Una gamba su Hyperliquid non e' eseguibile con questo conto + (Deribit, ~$635) senza aprire un secondo venue, e c'e' la **decisione dell'operatore del + 26/07** che tiene tutto su Deribit fino a $20k. Questo filone si giudica su *Β«esiste il + fenomeno?Β»*, non su *Β«e' deployabile?Β»*. + +PRIOR ART (non si ripete): il progetto ha GIA' testato un premio cross-venue con una barriera +STRUTTURALE vera (kimchi Upbit/USD, controlli sui capitali, `r0724_premium_wave` + +`r0724_kimchi_skeptic`, 2026-07-24): EARNS_SLOT=True al marginal scorer ma DSR 0,891, nessun +plateau, lag +1g lo azzera -> SCARTATO come parameter-luck. Deribit-HL **non ha alcuna barriera** +(stessa valuta di conto, nessun controllo sui capitali, arbitraggio libero) -> il prior e' PEGGIORE +di quello, non migliore. + +REPLICHE PRIMA DI PRODURRE NUMERI NUOVI (metodo, punto 2) +========================================================= + R1 `venue_watch`: |scarto| mediano Deribit-vs-consenso(coinbase+bitstamp) ~ **3 bps** su 8 anni. + R2 SLIP-AUDIT (Β§ ondata 22/08): base perp INVERSE vs LINEARE USDC = **BTC -0,03 bps, + ETH -0,27 bps**. Serve a due cose: replicare un numero pubblicato E chiudere in anticipo la + 4a occorrenza dello schema `fee_watch` (misurare su una configurazione diversa da quella che + gira: il feed certificato e' l'INVERSE, il libro trada il LINEARE). + R3 certificazione `fetch_hyperliquid`: HL 1d vs Binance **4-9 bps** sui 19 major. + +DATI +==== + * Deribit BTC/ETH: feed certificato su disco (1h, 2018-08/2019-03 -> oggi). + * HL 1d: `data/raw/hl_{btc,eth}_1d.parquet`, certificato, **dal 2024-01-01 (~2,6 anni)**. + * HL 5m/15m/1h: **NON su disco** -> presi dall'endpoint pubblico `candleSnapshot` + (tokenless, GET/POST in lettura) e **certificati qui** (flat, volume, n. trade, gap, + duplicati, cross-venue vs Coinbase USD dalla cache di `venue_tripwire`). + ⚠️ L'endpoint tiene **~5000 candele** per intervallo, punto: 5m = ~17 giorni, + 15m = ~52 giorni, 1h = ~209 giorni. Non c'e' modo pubblico di andare piu' indietro. + * ⚠️ **Mai USDT come ancora** (regola del progetto): la referenza terza e' Coinbase USD. + +MDE (metodo, punto 4) β€” dichiarato prima +======================================== + * Sharpe, finestra 1h (0,57 anni): 1,96/sqrt(0,57) = **2,59** -> nessuna affermazione di + STRATEGIA e' possibile su questa finestra. Si misura il FENOMENO, non un edge. + * Sharpe, finestra 1d (2,6 anni): 1,96/sqrt(2,6) = **1,22**. + * Correlazione/AR su n barre: 1,96/sqrt(n) -> a n~5000 = **0,028**. QUI la potenza c'e'. + Cioe': *reversione e lead-lag sono misurabili; un edge no.* Sono due frasi diverse. + + uv run python scripts/research/r0823b_crossvenue.py [--refresh] +""" +from __future__ import annotations + +import pickle +import sys +import time +import datetime as dt +from pathlib import Path + +import numpy as np +import pandas as pd +import requests + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from src.data.downloader import load_data # noqa: E402 + +CACHE = ROOT / "data" / "_cache" / "r0823b_crossvenue.pkl" +REF_CACHE = ROOT / "data" / "_cache" / "venue_ref_1h.pkl" + +HL_INFO = "https://api.hyperliquid.xyz/info" +DERIBIT_CHART = "https://www.deribit.com/api/v2/public/get_tradingview_chart_data" + +ASSETS = ("BTC", "ETH") +TF_MS = {"5m": 300_000, "15m": 900_000, "1h": 3_600_000, "1d": 86_400_000} +BARS_PER_YEAR = {"5m": 365.25 * 288, "15m": 365.25 * 96, "1h": 365.25 * 24, "1d": 365.25} + +# --- costi MISURATI dal progetto (non assunti) --------------------------------------------- +DER_TAKER_BPS = 3.50 # fee_watch + SLIP-AUDIT sul nastro (16/16 fill esatti) dal 2026-08-01 +HL_TAKER_BPS = 4.50 # HL-EXEC (due fonti indipendenti coincidono) +HL_HALF_SPREAD_BPS = 0.70 # HL-EXEC, 19 major +DER_TICK_USDC = {"BTC": 0.1, "ETH": 0.01} # lineare USDC dopo il cambio del 2026-08-18 + +# --- numeri pubblicati da replicare --------------------------------------------------------- +PUB_VENUEWATCH_BPS = 3.0 +PUB_BASE_INV_LIN = {"BTC": -0.03, "ETH": -0.27} +PUB_HL_BINANCE_BPS = (4.0, 9.0) + + +# =========================================================================================== +# rete: gentile, e fuori dalle finestre del cron live +# =========================================================================================== +def wait_ok_minute() -> None: + """Il cron live gira ai minuti :05-:10 (cron_book) e :24-:30 (collettore catena). + Nessuna richiesta di ricerca deve trovarsi nella stessa finestra.""" + while True: + m = dt.datetime.now(dt.timezone.utc).minute + if not (5 <= m <= 10 or 24 <= m <= 30): + return + time.sleep(20) + + +def hl_candles(coin: str, interval: str) -> pd.DataFrame: + """`candleSnapshot` pubblico. Ritorna al massimo ~5000 candele, sempre le PIU' RECENTI: + startTime piu' indietro NON allunga la storia (verificato: 2024 -> lista vuota).""" + wait_ok_minute() + now = int(time.time() * 1000) + start = now - 6000 * TF_MS[interval] + r = requests.post(HL_INFO, json={"type": "candleSnapshot", + "req": {"coin": coin, "interval": interval, + "startTime": start, "endTime": now}}, timeout=40) + r.raise_for_status() + d = r.json() + if not d: + return pd.DataFrame() + df = pd.DataFrame([{"ts": int(x["t"]), "open": float(x["o"]), "high": float(x["h"]), + "low": float(x["l"]), "close": float(x["c"]), "volume": float(x["v"]), + "ntrades": int(x["n"])} for x in d]) + return df.drop_duplicates("ts").sort_values("ts").reset_index(drop=True) + + +def deribit_chart(instrument: str, resolution: str, start_ms: int, end_ms: int) -> pd.DataFrame: + wait_ok_minute() + r = requests.get(DERIBIT_CHART, params=dict(instrument_name=instrument, resolution=resolution, + start_timestamp=start_ms, end_timestamp=end_ms), + timeout=40) + r.raise_for_status() + res = r.json().get("result", {}) + if res.get("status") == "no_data" or not res.get("ticks"): + return pd.DataFrame() + return pd.DataFrame({"ts": res["ticks"], "close": res["close"], + "volume": res["volume"]}).drop_duplicates("ts").sort_values("ts") + + +def load_cache(refresh: bool) -> dict: + if CACHE.exists() and not refresh: + return pickle.loads(CACHE.read_bytes()) + return {} + + +def save_cache(c: dict) -> None: + CACHE.parent.mkdir(parents=True, exist_ok=True) + CACHE.write_bytes(pickle.dumps(c)) + + +# =========================================================================================== +# nucleo statistico (niente statsmodels nell'ambiente -> OLS + Newey-West a mano) +# =========================================================================================== +def ols_hac(y: np.ndarray, X: np.ndarray, lags: int | None = None): + """OLS con covarianza Newey-West. X DEVE gia' contenere la costante.""" + y = np.asarray(y, float) + X = np.asarray(X, float) + n, k = X.shape + XtXi = np.linalg.pinv(X.T @ X) + b = XtXi @ (X.T @ y) + e = y - X @ b + if lags is None: + lags = max(1, int(np.floor(4 * (n / 100.0) ** (2.0 / 9.0)))) + Xe = X * e[:, None] + S = Xe.T @ Xe + for L in range(1, lags + 1): + w = 1.0 - L / (lags + 1.0) + G = Xe[L:].T @ Xe[:-L] + S = S + w * (G + G.T) + V = XtXi @ S @ XtXi + se = np.sqrt(np.maximum(np.diag(V), 0.0)) + r2 = 1.0 - e.var() / y.var() if y.var() > 0 else np.nan + return dict(b=b, se=se, t=b / np.where(se > 0, se, np.nan), V=V, e=e, n=n, r2=r2, lags=lags) + + +def wald_block(fit: dict, idx: list[int]) -> tuple[float, float]: + """Wald chi2 su un blocco di coefficienti (HAC). Ritorna (chi2, p).""" + from scipy import stats + b = fit["b"][idx] + V = fit["V"][np.ix_(idx, idx)] + try: + w = float(b @ np.linalg.pinv(V) @ b) + except Exception: + return np.nan, np.nan + return w, float(stats.chi2.sf(w, len(idx))) + + +def ar1(x: np.ndarray) -> dict: + """AR(1) sul livello: x[t+1] = a + phi x[t]. Mezza-vita in barre.""" + x = np.asarray(x, float) + y, X = x[1:], np.column_stack([np.ones(len(x) - 1), x[:-1]]) + f = ols_hac(y, X) + phi = float(f["b"][1]) + hl = float(np.log(0.5) / np.log(phi)) if 0 < phi < 1 else (0.0 if phi <= 0 else np.inf) + return dict(phi=phi, se=float(f["se"][1]), t=float(f["t"][1]), half_life=hl, n=f["n"]) + + +def ccf(a: np.ndarray, b: np.ndarray, lags: range) -> dict: + """corr(a[t], b[t-L]). L>0 => b GUIDA a (b di ieri spiega a di oggi).""" + a = np.asarray(a, float) + b = np.asarray(b, float) + out = {} + for L in lags: + if L >= 0: + x, y = a[L:], b[:len(b) - L] if L else b + else: + x, y = a[:len(a) + L], b[-L:] + m = np.isfinite(x) & np.isfinite(y) + out[L] = float(np.corrcoef(x[m], y[m])[0, 1]) if m.sum() > 30 else np.nan + return out + + +def bp(x) -> float: + return float(x) + + +# =========================================================================================== +# costruzione delle coppie allineate +# =========================================================================================== +def deribit_series(asset: str, tf: str) -> pd.Series: + """Feed certificato, chiuse. Indice = timestamp di APERTURA della barra (ms).""" + d = load_data(asset, "1h" if tf in ("1h", "1d") else tf) + s = pd.Series(d["close"].to_numpy(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + if tf == "1d": + s = s.resample("1D").last() + return s.dropna() + + +def hl_series(cache: dict, asset: str, tf: str) -> pd.DataFrame: + if tf == "1d": + h = pd.read_parquet(ROOT / "data" / "raw" / f"hl_{asset.lower()}_1d.parquet") + h.index = pd.to_datetime(h["timestamp"], unit="ms", utc=True) + h["ntrades"] = np.nan + return h[["open", "high", "low", "close", "volume", "ntrades"]] + key = ("hl", asset, tf) + if key not in cache: + cache[key] = hl_candles(asset, tf) + time.sleep(0.7) + df = cache[key].copy() + df.index = pd.to_datetime(df["ts"], unit="ms", utc=True) + return df[["open", "high", "low", "close", "volume", "ntrades"]] + + +def pair(cache: dict, asset: str, tf: str) -> pd.DataFrame: + """Coppia allineata sull'APERTURA della barra: le chiuse cadono allo stesso istante.""" + d = deribit_series(asset, tf) + h = hl_series(cache, asset, tf) + # NB: `ntrades` NON entra qui. A 1d e' una colonna tutta-NaN (il parquet certificato non la + # ha) e i chiamanti fanno `.dropna()`: includerla svuotava la coppia 1d **in silenzio** + # (n=0 stampato come "picco a -2 -> sfasamento"). Errore commesso e corretto in sessione: + # un dropna su una colonna che non serve e' un filtro invisibile. + j = pd.concat({"der": d, "hl": h["close"], "vol_hl": h["volume"]}, + axis=1, join="inner").dropna(subset=["der", "hl"]) + j["s_bps"] = 1e4 * np.log(j["hl"] / j["der"]) + j["r_der"] = np.log(j["der"]).diff() + j["r_hl"] = np.log(j["hl"]).diff() + return j + + +# =========================================================================================== +# 0 β€” REPLICHE +# =========================================================================================== +def repliche(cache: dict) -> None: + print("\n" + "=" * 100) + print(" [0] REPLICHE β€” un numero gia' pubblicato PRIMA di produrne uno nuovo") + print("=" * 100) + + # R1 β€” venue_watch: Deribit vs consenso (coinbase + bitstamp), orario, 8 anni + ok = REF_CACHE.exists() + print(f"\n R1 venue_watch |scarto| mediano Deribit-vs-consenso β€” pubblicato ~{PUB_VENUEWATCH_BPS:.0f} bps su 8 anni") + if not ok: + print(" cache referenze assente -> NON replicato (non rifaccio 8 anni di fetch)") + else: + refs = pickle.loads(REF_CACHE.read_bytes()) + for a in ASSETS: + d = load_data(a, "1h") + der = pd.Series(d["close"].to_numpy(float), index=d["timestamp"].to_numpy()) + cols = {"der": der} + for eid in ("coinbase", "bitstamp"): + r = refs.get((a, eid)) + if r is not None and len(r): + cols[eid] = pd.Series(np.asarray(r, float), index=np.asarray(r.index)) + m = pd.concat(cols, axis=1, join="outer") + m = m[m["der"].notna()] + R = m[[c for c in m.columns if c != "der"]] + cons = R.median(axis=1, skipna=True) + spread = (R.max(axis=1) - R.min(axis=1)) / cons * 1e4 + usable = (R.notna().sum(axis=1) >= 2) & (spread.fillna(np.inf) <= 100.0) + bps = ((m["der"] - cons) / cons * 1e4)[usable & cons.notna()] + print(f" {a}: n={len(bps):,} |scarto| mediano = {bps.abs().median():.2f} bps" + f" (p95 {bps.abs().quantile(.95):.1f})") + print(" -> REPLICATO: l'ordine di grandezza dei 3 bps e' confermato su entrambi gli asset.") + + # R2 β€” base INVERSE vs LINEARE USDC (chiude in anticipo lo schema `fee_watch`) + print(f"\n R2 base perp INVERSE vs LINEARE USDC β€” pubblicato BTC {PUB_BASE_INV_LIN['BTC']:+.2f} / " + f"ETH {PUB_BASE_INV_LIN['ETH']:+.2f} bps") + for a in ASSETS: + key = ("der_lin", a) + if key not in cache: + now = int(time.time() * 1000) + cache[key] = deribit_chart(f"{a}_USDC-PERPETUAL", "5", now - 17 * 86_400_000, now) + time.sleep(0.7) + lin = cache[key] + if not len(lin): + print(f" {a}: nessun dato lineare -> non replicato") + continue + L = pd.Series(np.asarray(lin["close"], float), + index=pd.to_datetime(np.asarray(lin["ts"]), unit="ms", utc=True)) + I = deribit_series(a, "5m") + j = pd.concat({"inv": I, "lin": L}, axis=1, join="inner").dropna() + b = 1e4 * np.log(j["inv"] / j["lin"]) + print(f" {a}: n={len(j):,} barre 5m base(inv-lin) mediana = {b.median():+.3f} bps " + f"(media {b.mean():+.3f}, sd {b.std():.2f})") + print(" -> ⚠️ REPLICA PARZIALE: l'ORDINE DI GRANDEZZA (sotto il mezzo bps) e' confermato, il") + print(" SEGNO no (+0,24/+0,27 contro -0,03/-0,27). Non e' una contraddizione: il numero") + print(" pubblicato viene da **18 fill** di luglio, questo da 4.849 barre 5m di agosto β€”") + print(" due stimandi diversi. Si cita l'ordine di grandezza, non il segno.") + print(" -> πŸ“Œ E il fatto che conta e' un ALTRO: la **sd** di quella base (2,7 bps BTC / 4,0") + print(" ETH) e' dello stesso ordine dello scarto cross-venue mediano a 5m (2,8/3,1 bps).") + print(" Cioe': **quale CONTRATTO Deribit si guarda sposta la misura quanto il VENUE**.") + + print(f"\n R3 certificazione fetch_hyperliquid: HL 1d vs Binance {PUB_HL_BINANCE_BPS[0]:.0f}-" + f"{PUB_HL_BINANCE_BPS[1]:.0f} bps sui 19 major β€” non ri-misurato qui (userebbe USDT come") + print(" ancora, che la regola del progetto vieta). Citato come contesto della TAGLIA attesa.") + + +# =========================================================================================== +# 1 β€” CERTIFICAZIONE del dato NUOVO (HL intraday) +# =========================================================================================== +def certifica_hl(cache: dict) -> dict: + print("\n" + "=" * 100) + print(" [1] CERTIFICAZIONE del dato HL intraday (NUOVO: non era su disco)") + print("=" * 100) + refs = pickle.loads(REF_CACHE.read_bytes()) if REF_CACHE.exists() else {} + print(f"\n {'asset':<5}{'tf':<5}{'barre':>7}{'da':>13}{'a':>13}{'gg':>6}" + f"{'flat%':>7}{'vol0%':>7}{'ntr_med':>9}{'gap':>5}{'dup':>5}{'vsCB_bps':>10}") + info = {} + for a in ASSETS: + for tf in ("5m", "15m", "1h"): + h = hl_series(cache, a, tf) + n = len(h) + flat = float((h["high"] == h["low"]).mean() * 100) + vol0 = float((h["volume"] <= 0).mean() * 100) + ntr = float(np.nanmedian(h["ntrades"])) if h["ntrades"].notna().any() else np.nan + # epoca ESPLICITA in ms: `.view("int64")` su indici tz-aware e' la trappola + # codificata il 01/07 (scala sbagliata senza errore). + ms = ((h.index - pd.Timestamp("1970-01-01", tz="UTC")) // pd.Timedelta("1ms")).to_numpy() + dts = np.diff(ms) + gap = int((dts != TF_MS[tf]).sum()) + dup = int(h.index.duplicated().sum()) + # cross-venue vs Coinbase USD (mai USDT), solo dove la cache copre + vs = np.nan + r = refs.get((a, "coinbase")) + if r is not None and len(r) and tf == "1h": + cb = pd.Series(np.asarray(r, float), + index=pd.to_datetime(np.asarray(r.index), unit="ms", utc=True)) + j = pd.concat({"hl": h["close"], "cb": cb}, axis=1, join="inner").dropna() + if len(j) > 100: + vs = float((1e4 * np.log(j["hl"] / j["cb"])).abs().median()) + gg = (h.index[-1] - h.index[0]).total_seconds() / 86400 + print(f" {a:<5}{tf:<5}{n:>7,}{h.index[0].strftime('%Y-%m-%d'):>13}" + f"{h.index[-1].strftime('%Y-%m-%d'):>13}{gg:>6.0f}{flat:>7.2f}{vol0:>7.2f}" + f"{ntr:>9.0f}{gap:>5}{dup:>5}" + (f"{vs:>10.2f}" if np.isfinite(vs) else f"{'-':>10}")) + info[(a, tf)] = dict(n=n, days=gg, flat=flat, vol0=vol0, gap=gap, dup=dup, vs_cb=vs) + print("\n ⚠️ Il cross-venue vs Coinbase e' calcolabile solo a 1h (la cache di venue_tripwire e'") + print(" oraria e si ferma al 2026-07-24): a 5m/15m il dato HL e' certificato su liquidita' e") + print(" integrita', NON su un terzo venue. Dichiarato, non nascosto.") + print(" ⚠️ HL vs Coinbase e' un confronto PERP-vs-SPOT: contiene la base, non solo il rumore.") + return info + + +# =========================================================================================== +# 2 β€” VERIFICA DELLE CONVENZIONI COL LAG (il progetto ha appena pagato uno sfasamento di 1h) +# =========================================================================================== +def verifica_lag(cache: dict) -> None: + print("\n" + "=" * 100) + print(" [2] CONVENZIONI DI ETICHETTATURA, verificate COL LAG (non credute)") + print("=" * 100) + print(" Deribit: indice = APERTURA della barra (feed tradingview). HL candleSnapshot: 't' =") + print(" apertura, 'T' = t + durata - 1 ms (verificato sul campo). Allineando sull'apertura,") + print(" le CHIUSE cadono allo stesso istante. Prova: la corr dei rendimenti deve avere il") + print(" MASSIMO a lag 0. (Lezione Β§39: due serie dello stesso progetto possono avere") + print(" convenzioni diverse β€” corr 0,996 al lag +1 e ~0 al lag 0.)") + print(f"\n {'asset':<5}{'tf':<5}{'n':>7}" + "".join(f"{f'lag{L:+d}':>9}" for L in range(-2, 3)) + " verdetto") + for a in ASSETS: + for tf in ("5m", "15m", "1h", "1d"): + j = pair(cache, a, tf).dropna() + c = ccf(j["r_der"].to_numpy(), j["r_hl"].to_numpy(), range(-2, 3)) + best = max(c, key=lambda k: (c[k] if np.isfinite(c[k]) else -9)) + ok = "OK (picco a 0)" if best == 0 else f"!! PICCO A {best:+d} β€” sfasamento" + print(f" {a:<5}{tf:<5}{len(j):>7,}" + "".join(f"{c[L]:>9.4f}" for L in range(-2, 3)) + f" {ok}") + + +# =========================================================================================== +# 3 β€” SCOMPOSIZIONE COMUNE / IDIOSINCRATICA (controllo obbligatorio) +# =========================================================================================== +def scomposizione(cache: dict) -> dict: + print("\n" + "=" * 100) + print(" [3] SCOMPOSIZIONE COMUNE / IDIOSINCRATICA β€” il controllo contro l'auto-inganno") + print("=" * 100) + print(" r_der e r_hl si scompongono in comune c=(r_der+r_hl)/2 e idio d=(r_hl-r_der)=Ds.") + print(" La quota idiosincratica e' TUTTO cio' su cui una strategia cross-venue puo' vivere.") + print(f"\n {'asset':<5}{'tf':<5}{'n':>7}{'corr(r)':>9}{'sd_com%':>10}{'sd_idio%':>10}" + f"{'var_idio/var_tot':>18}{'|s| med bps':>13}{'sd(s) bps':>11}{'|s| p99':>9}") + out = {} + for a in ASSETS: + for tf in ("5m", "15m", "1h", "1d"): + j = pair(cache, a, tf).dropna() + rd, rh = j["r_der"].to_numpy(), j["r_hl"].to_numpy() + com, idio = (rd + rh) / 2, (rh - rd) + corr = float(np.corrcoef(rd, rh)[0, 1]) + vt = float(np.var(com) + np.var(idio) / 4) # var(r_der) ~ var(com)+var(idio)/4 + share = float(np.var(idio) / 4) / vt if vt > 0 else np.nan + s = j["s_bps"] + print(f" {a:<5}{tf:<5}{len(j):>7,}{corr:>9.5f}{np.std(com)*100:>10.4f}" + f"{np.std(idio)*100:>10.4f}{share:>18.6f}{s.abs().median():>13.2f}" + f"{s.std():>11.2f}{s.abs().quantile(.99):>9.1f}") + out[(a, tf)] = dict(corr=corr, share=share, med=float(s.abs().median()), sd=float(s.std())) + print("\n πŸ“Œ La quota idiosincratica e' il tetto ASSOLUTO di qualunque cosa questo filone possa") + print(" produrre: tutto il resto e' lo stesso bitcoin visto da due finestre.") + + # --- confronto a FINESTRA COMUNE: la sd(s) cresce col timeframe o con la FINESTRA? --- + print("\n ⚠️ Le tre finestre intraday NON sono la stessa finestra (5m=18gg, 15m=52gg, 1h=208gg):") + print(" un confronto fra timeframe letto cosi' misura il CALENDARIO, non il timeframe.") + print(" Ricalcolo tutto sugli STESSI 18 giorni (differenza a un grado di liberta' per volta).") + print(f"\n {'asset':<5}{'tf':<5}{'n(18gg)':>9}{'|s| med':>9}{'sd(s)':>8}{'phi':>8}{'HL barre':>10}" + f"{'HL ore':>9}") + for a in ASSETS: + for tf in ("5m", "15m", "1h"): + j = pair(cache, a, tf).dropna() + t0 = j.index[-1] - pd.Timedelta(days=18) + k = j[j.index >= t0] + if len(k) < 100: + continue + A = ar1(k["s_bps"].to_numpy()) + ore = A["half_life"] * TF_MS[tf] / 3_600_000 + print(f" {a:<5}{tf:<5}{len(k):>9,}{k['s_bps'].abs().median():>9.2f}{k['s_bps'].std():>8.2f}" + f"{A['phi']:>8.3f}{A['half_life']:>10.2f}{ore:>9.2f}") + + # --- lento vs veloce: quanta parte dello scarto e' un livello che dura ore? --- + print("\n Scomposizione dello scarto in LENTO (media mobile 24h, causale) e VELOCE (residuo):") + print(f" {'asset':<5}{'tf':<5}{'var lento %':>13}{'var veloce %':>14} lettura") + for a in ASSETS: + for tf in ("5m", "15m", "1h"): + j = pair(cache, a, tf).dropna() + w = max(2, int(round(86_400_000 / TF_MS[tf]))) + sl = j["s_bps"].rolling(w).mean() + fa = j["s_bps"] - sl + m = sl.notna() + vs, vf = float(sl[m].var()), float(fa[m].var()) + tot = vs + vf + lett = "quasi tutto rumore veloce" if vf / tot > 0.6 else "componente LENTA dominante" + print(f" {a:<5}{tf:<5}{100*vs/tot:>13.1f}{100*vf/tot:>14.1f} {lett}") + return out + + +# =========================================================================================== +# 4 β€” MURO DI NEGOZIABILITA' (dichiarato PRIMA dei rendimenti) +# =========================================================================================== +def muro(cache: dict, dec: dict) -> dict: + print("\n" + "=" * 100) + print(" [4] MURO DI NEGOZIABILITA' β€” dichiarato PRIMA di guardare un solo rendimento") + print("=" * 100) + px = {a: float(deribit_series(a, "1h").iloc[-1]) for a in ASSETS} + soglie = {} + for a in ASSETS: + der_half = 0.5 * DER_TICK_USDC[a] / px[a] * 1e4 + arb_rt = 2 * (DER_TAKER_BPS + HL_TAKER_BPS + der_half + HL_HALF_SPREAD_BPS) + dir_rt = 2 * (DER_TAKER_BPS + der_half) + soglie[a] = dict(der_half=der_half, arb=arb_rt, dirz=dir_rt) + print(f"\n {a} (ultimo {px[a]:,.0f}) β€” mezzo spread Deribit lineare = {der_half:.4f} bps (1 tick), " + f"HL = {HL_HALF_SPREAD_BPS:.2f} bps") + print(f" (i) ARBITRAGGIO cross-venue (4 gambe: apri D+HL, chiudi D+HL)" + f" = 2x({DER_TAKER_BPS:.2f}+{HL_TAKER_BPS:.2f}+{der_half:.3f}+{HL_HALF_SPREAD_BPS:.2f})" + f" = **{arb_rt:.2f} bps** di andata-ritorno") + print(f" (ii) DIREZIONALE su Deribit soltanto (2 gambe, HL usato solo come SEGNALE)" + f" = **{dir_rt:.2f} bps**") + for tf in ("5m", "15m", "1h", "1d"): + d = dec[(a, tf)] + print(f" {tf:<4} |scarto| mediano {d['med']:5.2f} bps sd {d['sd']:5.2f} -> " + f"serve {arb_rt/max(d['sd'],1e-9):5.1f} sd per pagare l'arb, " + f"{dir_rt/max(d['sd'],1e-9):4.1f} sd per pagare il direzionale") + print("\n πŸ“Œ SOGLIA DI NEGOZIABILITA' CONGELATA QUI: un segnale che promette meno di ~17 bps di") + print(" movimento atteso non e' arbitrabile, e meno di ~7 bps non e' nemmeno tradabile su un") + print(" venue solo. Il fondo di rumore dello scarto e' di 4-6 bps: **il muro sta sopra la") + print(" DEVIAZIONE STANDARD dell'intero fenomeno**, non sopra il suo valore tipico.") + return soglie + + +# =========================================================================================== +# 5 β€” (a) REVERSIONE, con il discriminante rumore-vs-dislocazione +# =========================================================================================== +def reversione(cache: dict, soglie: dict) -> dict: + print("\n" + "=" * 100) + print(" [5] (a) LO SCARTO REVERTE? β€” e la reversione e' RUMORE o DISLOCAZIONE?") + print("=" * 100) + print(" s(t) = 1e4*ln(p_HL/p_DER). AR(1) sul livello -> mezza-vita.") + print(" Poi la decomposizione che conta: r_DER(t+1) = a + bD*s(t) e r_HL(t+1) = a + bH*s(t).") + print(" Solo **bD** e' tradabile su Deribit; bH richiede la gamba HL (fuori perimetro).") + print(" DISCRIMINANTE dichiarato in A3: la stessa regressione SALTANDO UNA BARRA (r a t+2).") + print(" Se bD crolla -> era rumore di osservazione; se sopravvive -> dislocazione vera.") + print(" MDE(bD) = 1,96*sd(r)/(sd(s)*sqrt(n)): stampata accanto, perche' un coefficiente") + print(" sotto la propria MDE non e' 'piccolo', e' NON MISURATO.\n") + print(f" {'asset':<5}{'tf':<5}{'n':>7}{'phi':>7}{'HLbar':>7}{'HLore':>7}" + f"{'bD':>9}{'t':>6}{'MDE(bD)':>9}{'bD skip1':>10}{'t':>6}" + f"{'E|1sd| bps':>12}{'muro':>7}{'x muro':>8} verso") + out = {} + for a in ASSETS: + for tf in ("5m", "15m", "1h", "1d"): + j = pair(cache, a, tf).dropna() + s_ = j["s_bps"].to_numpy() + rd = j["r_der"].to_numpy() * 1e4 # bps + A = ar1(s_) + y, X = rd[1:], np.column_stack([np.ones(len(s_) - 1), s_[:-1]]) + f1 = ols_hac(y, X) + y2, X2 = rd[2:], np.column_stack([np.ones(len(s_) - 2), s_[:-2]]) + f2 = ols_hac(y2, X2) + bD, tD = float(f1["b"][1]), float(f1["t"][1]) + bD2, tD2 = float(f2["b"][1]), float(f2["t"][1]) + sd = float(np.std(s_)) + n = len(s_) - 1 + mde = 1.96 * float(np.std(rd)) / (sd * np.sqrt(n)) if sd > 0 else np.nan + mossa = abs(bD) * sd + w = soglie[a]["dirz"] + ore = A["half_life"] * TF_MS[tf] / 3_600_000 + verso = "converge" if bD > 0 else "diverge" + if abs(bD) < mde: + verso += " (sotto MDE)" + print(f" {a:<5}{tf:<5}{len(j):>7,}{A['phi']:>7.3f}{A['half_life']:>7.2f}{ore:>7.1f}" + f"{bD:>9.4f}{tD:>6.1f}{mde:>9.4f}{bD2:>10.4f}{tD2:>6.1f}" + f"{mossa:>12.2f}{w:>7.2f}{mossa/w:>8.3f} {verso}") + out[(a, tf)] = dict(phi=A["phi"], hl=A["half_life"], bD=bD, tD=tD, bD2=bD2, tD2=tD2, + mossa=mossa, muro=w, sd=sd, mde=mde) + + print("\n bH (la gamba HL) per completezza β€” NON eseguibile con questo conto." + " Identita' di controllo: bH - bD deve valere (phi-1).") + print(f" {'asset':<5}{'tf':<5}{'bH':>9}{'t':>7}{'bH-bD':>9}{'phi-1':>9}{'|dif|':>9}") + for a in ASSETS: + for tf in ("5m", "1h", "1d"): + j = pair(cache, a, tf).dropna() + s_ = j["s_bps"].to_numpy() + rh = j["r_hl"].to_numpy() * 1e4 + f = ols_hac(rh[1:], np.column_stack([np.ones(len(s_) - 1), s_[:-1]])) + bH = float(f["b"][1]) + dif = (bH - out[(a, tf)]["bD"]) - (out[(a, tf)]["phi"] - 1.0) + print(f" {a:<5}{tf:<5}{bH:>9.4f}{float(f['t'][1]):>7.1f}" + f"{bH - out[(a, tf)]['bD']:>9.4f}{out[(a, tf)]['phi'] - 1:>9.4f}{abs(dif):>9.2e}") + + # --- il conto che decide: la strategia MIGLIORE POSSIBILE su questo segnale --------------- + print("\n LA STRATEGIA IN-SAMPLE-OTTIMA (massimamente generosa: il SEGNO di bD e' preso") + print(" dagli stessi dati, la posizione e' gia' scalata al meglio, niente ritardo di") + print(" esecuzione). pos(t) = clip(sgn(bD)*s(t)/sd(s), -1, +1); pnl = pos*r_DER(t+1);") + print(" costo = |Dpos| * (taker Deribit + mezzo spread) su OGNI barra.") + print(" In piu' la stessa strategia RITARDATA DI UNA BARRA (l'unica versione che non si") + print(" finanzia col proprio rumore di osservazione): se il lordo crolla li', il lordo") + print(" della prima colonna era bid-ask bounce, non informazione.") + print(f"\n {'asset':<5}{'tf':<5}{'barre/anno':>11}{'turnover/anno':>14}{'Sh LORDO':>10}" + f"{'Sh LORDO skip1':>16}{'costo %/anno':>14}{'Sh NETTO':>10}{'MDE Sh':>8}") + for a in ASSETS: + for tf in ("5m", "15m", "1h", "1d"): + j = pair(cache, a, tf).dropna() + s_ = j["s_bps"].to_numpy() + rd = j["r_der"].to_numpy() + sd = float(np.std(s_)) + sgn = np.sign(out[(a, tf)]["bD"]) or 1.0 + pos = np.clip(sgn * s_ / sd, -1, 1) + pnl_g = pos[:-1] * rd[1:] + pnl_s = pos[:-2] * rd[2:] # entrata ritardata di una barra + one_way = (DER_TAKER_BPS + soglie[a]["der_half"]) / 1e4 + dpos = np.abs(np.diff(pos, prepend=pos[0]))[:-1] + pnl_n = pnl_g - dpos * one_way + bpy = BARS_PER_YEAR[tf] + anni = len(j) / bpy + def shp(x): + return float(np.mean(x) / np.std(x) * np.sqrt(bpy)) if np.std(x) > 0 else np.nan + print(f" {a:<5}{tf:<5}{bpy:>11,.0f}{dpos.mean()*bpy:>14,.0f}{shp(pnl_g):>10.2f}" + f"{shp(pnl_s):>16.2f}{dpos.mean()*one_way*bpy*100:>14.1f}" + f"{shp(pnl_n):>10.2f}{1.96/np.sqrt(anni):>8.2f}") + print("\n πŸ“Œ La colonna 'costo %/anno' NON e' una stima statistica: e' aritmetica " + "(turnover x listino).") + print(" La colonna 'Sh LORDO' e' invece sotto la propria MDE su ogni riga intraday: e' il") + print(" numero che NON si puo' rivendicare. Il verdetto poggia sulla prima, non sulla seconda.") + return out + + +# =========================================================================================== +# 6 β€” (b) LEAD-LAG: cross-correlazione + causalita' diretta + CONTROLLI +# =========================================================================================== +def leadlag(cache: dict) -> dict: + print("\n" + "=" * 100) + print(" [6] (b) UNO DEI DUE VENUE GUIDA? β€” cross-correlazione E test di causalita' diretto") + print("=" * 100) + print(" ccf(L) = corr(r_DER[t], r_HL[t-L]). L>0 => HL GUIDA Deribit. L<0 => il contrario.") + print(f"\n {'asset':<5}{'tf':<5}{'n':>7}" + "".join(f"{f'{L:+d}':>9}" for L in range(-3, 4))) + ll = {} + for a in ASSETS: + for tf in ("5m", "15m", "1h"): + j = pair(cache, a, tf).dropna() + c = ccf(j["r_der"].to_numpy(), j["r_hl"].to_numpy(), range(-3, 4)) + print(f" {a:<5}{tf:<5}{len(j):>7,}" + "".join(f"{c[L]:>9.4f}" for L in range(-3, 4))) + ll[(a, tf)] = c + print(f"\n MDE sulla correlazione a n~5000: 1,96/sqrt(n) = {1.96/np.sqrt(5000):.4f}") + + print("\n CAUSALITA' DIRETTA (Granger con HAC Newey-West, k=4 ritardi, Wald chi2 sul blocco).") + print(" 🚨 12 test in questa tabella -> soglia BONFERRONI alpha=0,05/12 = 0,0042, non 0,05.") + + def _granger(j, k=4): + """RIPARAMETRIZZATA: al posto dei ritardi di r_HL uso quelli di d = r_HL - r_DER. + + ⚠️ AUTOCORREZIONE: avevo scritto che la parametrizzazione ingenua produce coefficienti + gonfiati dalla collinearita' e che la loro somma "non e' una grandezza economica". + E' FALSO, e l'algebra lo dice in due righe: `span{r_D, r_H} == span{r_D, r_H-r_D}` con + `a_i = a'_i - c'_i` e **`c_i = c'_i`** β€” il coefficiente del blocco e' letteralmente lo + STESSO nelle due scritture, e infatti il controllo numerico sotto da' uno scarto di + 2,7e-12. La riparametrizzazione compra solo un'assicurazione sul condizionamento (con + corr(r_D,r_H)=0,994) e la leggibilita' di `sum c` = **bps di Deribit per bps di + divergenza passata**. Cio' che ha davvero riparato la lettura non e' questa, e' la + colonna `sd null`: senza di lei una "sd predetta" e' in gran parte gradi di liberta'. + """ + rd, rh = j["r_der"].to_numpy() * 1e4, j["r_hl"].to_numpy() * 1e4 + dd = rh - rd + n = len(rd) - k + if n < 200: + return dict(ok=False) + L = lambda x, i: x[k - i - 1: k - i - 1 + n] # noqa: E731 + Xd = np.column_stack([np.ones(n)] + [L(rd, i) for i in range(k)] + [L(dd, i) for i in range(k)]) + fd = ols_hac(rd[k:], Xd) + w1, p1 = wald_block(fd, list(range(1 + k, 1 + 2 * k))) + Xh = np.column_stack([np.ones(n)] + [L(rh, i) for i in range(k)] + [L(dd, i) for i in range(k)]) + fh = ols_hac(rh[k:], Xh) + w2, p2 = wald_block(fh, list(range(1 + k, 1 + 2 * k))) + cb = fd["b"][1 + k: 1 + 2 * k] + contrib = Xd[:, 1 + k: 1 + 2 * k] @ cb # parte di r_DER predetta dalla DIVERGENZA + # controllo di equivalenza con la parametrizzazione ingenua (r_HL al posto di d) + Xn = np.column_stack([np.ones(n)] + [L(rd, i) for i in range(k)] + [L(rh, i) for i in range(k)]) + fn = ols_hac(rd[k:], Xn) + wn, _ = wald_block(fn, list(range(1 + k, 1 + 2 * k))) + # null della sd predetta: con p regressori su n punti, il puro rumore produce gia' + # sd(fit) ~ sd(y)*sqrt(p/n). Senza questo confronto una 'sd predetta' e' un artefatto + # di gradi di liberta' β€” e a n=429 con 8 regressori sarebbe quasi tutto artefatto. + sd_null = float(np.std(rd[k:])) * np.sqrt(k / n) + return dict(ok=True, w1=w1, p1=p1, w2=w2, p2=p2, sum_c=float(cb.sum()), + sd_fit=float(np.std(contrib)), sd_null=sd_null, r2=float(fd["r2"]), n=n, + w_naive=wn, cnaive=float(fn["b"][1 + k: 1 + 2 * k].sum())) + + def _lettura(p1, p2, alpha=0.05 / 12): + if not np.isfinite(p1): + return "campione insufficiente" + if p1 > alpha and p2 > alpha: + return "nessuna direzione" + if p1 <= alpha and p2 > alpha: + return "HL guida" + if p2 <= alpha and p1 > alpha: + return "DER guida" + return "reciproca" + + gs = [] + for etichetta, giorni, tfs in (("FINESTRA PIENA di ciascun tf (18 / 52 / 208 gg)", None, + ("5m", "15m", "1h")), + ("STESSI 52 giorni (isola il tf dalla finestra)", 52, ("15m", "1h")), + ("STESSI 18 giorni (idem, al limite del campione)", 18, + ("5m", "15m", "1h"))): + print(f"\n -- {etichetta} --") + print(f" {'asset':<5}{'tf':<5}{'n':>7}{'div->DER chi2':>15}{'p':>9}{'div->HL chi2':>14}{'p':>9}" + f"{'sum c':>8}{'sd pred':>9}{'sd null':>9}{'muro':>7} lettura (Bonferroni)") + for a in ASSETS: + for tf in tfs: + j = pair(cache, a, tf).dropna() + if giorni is not None: + j = j[j.index >= j.index[-1] - pd.Timedelta(days=giorni)] + g = _granger(j) + if not g.get("ok"): + print(f" {a:<5}{tf:<5}{len(j):>7,} campione insufficiente") + continue + w = 2 * (DER_TAKER_BPS + 0.5 * DER_TICK_USDC[a] / float(j['der'].iloc[-1]) * 1e4) + print(f" {a:<5}{tf:<5}{g['n']:>7,}{g['w1']:>15.1f}{g['p1']:>9.4f}{g['w2']:>14.1f}" + f"{g['p2']:>9.4f}{g['sum_c']:>8.3f}{g['sd_fit']:>9.3f}{g['sd_null']:>9.3f}" + f"{w:>7.2f} {_lettura(g['p1'], g['p2'])}") + if giorni is None: + gs.append((a, tf, etichetta, g)) + print("\n πŸ“Œ Se HL guidasse davvero Deribit, l'effetto sarebbe MASSIMO alla risoluzione piu'") + print(" fine e si diluirebbe salendo. Un effetto che compare SOLO a 15m e sparisce sia a 5m") + print(" sia a 1h non e' un lead-lag: e' una finestra.") + if gs: + alpha = 0.05 / 12 + passa = [x for x in gs if x[3]["p1"] <= alpha or x[3]["p2"] <= alpha] + print(f"\n 🚨 La colonna che DECIDE non e' il p-value, e' 'sd pred' β€” e va letta accanto a") + print(f" 'sd null' (con 4 regressori di blocco su n punti il PURO RUMORE produce gia'") + print(f" sd(y)*sqrt(4/n) di 'predizione').") + for a_, tf_, _, g in sorted(passa, key=lambda x: -x[3]["sd_fit"]): + w = 2 * DER_TAKER_BPS + print(f" Β· {a_} {tf_} (p={min(g['p1'], g['p2']):.4f}, passa Bonferroni): sd pred " + f"**{g['sd_fit']:.2f} bps** contro {g['sd_null']:.2f} di rumore e {w:.1f} bps di" + f" costo -> manca **{w/max(g['sd_fit'],1e-9):.1f}x**") + if not passa: + print(" Β· nessuna cella passa Bonferroni sulla finestra piena.") + mx = max(gs, key=lambda x: x[3]["sd_fit"]) + g = mx[3] + print(f" Il massimo assoluto di 'sd pred' e' {g['sd_fit']:.2f} bps ({mx[0]} {mx[1]}), ma li'") + print(f" il rumore da solo ne fa {g['sd_null']:.2f} e il test NON e' significativo" + f" (p={min(g['p1'], g['p2']):.4f}).") + # controllo di equivalenza fra le due parametrizzazioni + if gs: + dif = max(abs(g[3]["w1"] - g[3]["w_naive"]) / max(g[3]["w1"], 1e-9) for g in gs) + print(f"\n βœ… Controllo: le due parametrizzazioni (ritardi di r_HL vs ritardi della") + print(f" divergenza) danno lo STESSO chi2 β€” scarto relativo massimo {dif:.2e}. La") + print(f" riparametrizzazione non cambia il test, cambia solo la LEGGIBILITA' dei") + print(f" coefficienti (somma ingenua fino a {max(abs(g[3]['cnaive']) for g in gs):.2f}, " + f"somma leggibile {max(abs(g[3]['sum_c']) for g in gs):.3f}).") + + # ---- CONTROLLI: un rilevatore che non trova nulla e' indistinguibile da uno rotto ---- + print("\n CONTROLLI OBBLIGATORI (metodo, punto 3) β€” sugli STESSI stimatori:") + rng = np.random.default_rng(823) + n = 5000 + r = rng.normal(0, 0.001, n) # prezzo comune + pD = np.exp(np.cumsum(r)) + # PC1 lead-lag: HL = Deribit RITARDATO di 1 barra + rumore -> Deribit GUIDA (picco a lag -1) + pH = np.concatenate([[pD[0]], pD[:-1]]) * np.exp(rng.normal(0, 5e-5, n)) + rd_, rh_ = np.diff(np.log(pD)) * 1e4, np.diff(np.log(pH)) * 1e4 + c = ccf(rd_, rh_, range(-3, 4)) + best = max(c, key=lambda k_: c[k_]) + print(f" PC1 HL = Deribit ritardato di 1 barra -> picco atteso a lag -1; " + f"MISURATO {best:+d} (ccf {c[best]:.3f}) {'OK' if best == -1 else 'ROTTO'}") + # PC2 reversione: spread AR(1) noto phi=0.7 -> mezza-vita attesa 1.94 barre + phi_true = 0.7 + e = rng.normal(0, 3.0, n) + s_syn = np.zeros(n) + for i in range(1, n): + s_syn[i] = phi_true * s_syn[i - 1] + e[i] + A = ar1(s_syn) + hl_att = np.log(0.5) / np.log(phi_true) + print(f" PC2 spread AR(1) phi={phi_true} (mezza-vita {hl_att:.2f}) -> " + f"MISURATO phi {A['phi']:.3f}, mezza-vita {A['half_life']:.2f} " + f"{'OK' if abs(A['phi'] - phi_true) < 0.05 else 'ROTTO'}") + # NC: due random walk indipendenti -> nessun lead-lag, nessuna reversione + p1_ = np.exp(np.cumsum(rng.normal(0, 0.001, n))) + p2_ = np.exp(np.cumsum(rng.normal(0, 0.001, n))) + c0 = ccf(np.diff(np.log(p1_)) * 1e4, np.diff(np.log(p2_)) * 1e4, range(-3, 4)) + mx = max(abs(v) for v in c0.values()) + print(f" NC due random walk indipendenti -> max |ccf| = {mx:.4f} " + f"(soglia rumore {1.96/np.sqrt(n):.4f}) {'OK' if mx < 3*1.96/np.sqrt(n) else 'ROTTO'}") + return ll + + +# =========================================================================================== +# 7 β€” (c) GATE DI REGIME +# =========================================================================================== +def gate(cache: dict) -> None: + print("\n" + "=" * 100) + print(" [7] (c) LO SCARTO E' UN GATE DI REGIME PER TP01/SKH01?") + print("=" * 100) + j = {a: pair(cache, a, "1d").dropna() for a in ASSETS} + n = min(len(j[a]) for a in ASSETS) + anni = n / 365.25 + mde_sh = 1.96 / np.sqrt(anni) + mde_c = 1.96 / np.sqrt(n) + print(f"\n Finestra comune: {n} giorni = {anni:.2f} anni (HL 1d parte dal 2024-01-01).") + print(f" MDE dichiarato PRIMA: Sharpe {mde_sh:.2f} Β· correlazione {mde_c:.3f}.") + print(" ⚠️ La finestra e' INTERAMENTE dentro l'hold-out 2025-26 + il 2024 -> `marginal_vs_tp01`") + print(" non puo' dare ADDS per costruzione (lezione Β§12): non lo uso, misuro il") + print(" CONTENUTO PREDITTIVO direttamente, dove la potenza c'e'.\n") + + print(" 🚨 12 correlazioni in questa tabella -> soglia BONFERRONI |t| > 2,86, non 1,96.\n") + print(f" {'asset':<5}{'feature -> bersaglio (t+1)':<42}{'corr':>8}{'t(HAC)':>9}{'MDE':>8} esito") + for a in ASSETS: + d = j[a] + s_ = d["s_bps"].to_numpy() + r = d["r_der"].to_numpy() * 1e4 + absr = np.abs(r) + a20 = pd.Series(np.abs(s_)).rolling(20).mean().to_numpy() # causale + feats = {"s(t) firmato": s_, "|s(t)|": np.abs(s_), "media20 |s| (causale)": a20} + targs = {"rendimento": r, "|rendimento| (vol)": absr} + for fn, fv in feats.items(): + for tn, tv in targs.items(): + x, y = fv[:-1], tv[1:] + m = np.isfinite(x) & np.isfinite(y) + if m.sum() < 100: + continue + c = float(np.corrcoef(x[m], y[m])[0, 1]) + f = ols_hac(y[m], np.column_stack([np.ones(m.sum()), x[m]])) + t = float(f["t"][1]) + if abs(t) > 2.86: + ok = "SEGNALE (regge Bonferroni)" + elif abs(c) > mde_c: + ok = "sopra MDE ma NON regge Bonferroni" + else: + ok = "sotto MDE" + print(f" {a:<5}{fn + ' -> ' + tn:<42}{c:>8.4f}{t:>9.2f}{mde_c:>8.3f} {ok}") + + # ------------------------------------------------------------------ gate vero su TP01 + print("\n GATE VERO su TP01 (baseline daily di altlib), finestra comune.") + print(" ⚠️ ERRORE MIO, COMMESSO E CORRETTO IN SESSIONE: la prima stesura usava come null") + print(" il **de-levering** (base x k a pari vol). Su un gate quel null e' DEGENERE β€”") + print(" `sh(k*base) === sh(base)`, e infatti stampava dSharpe identico nelle due colonne.") + print(" E' esattamente il caso avvertito in Β§12(d). Il null giusto per un GATE e' un") + print(" **gate CASUALE con lo stesso ciclo di servizio**: spegne gli stessi giorni di") + print(" mercato, ma scelti a caso. Cosi' si misura se il gate spegne i giorni GIUSTI.") + try: + from scripts.research.alt import altlib as al + tp = al.tp01_baseline_daily() + except Exception as ex: # pragma: no cover + print(f" (baseline TP01 non disponibile: {ex})") + return + d = j["BTC"] + idx = d.index.tz_convert("UTC").normalize() + sabs = pd.Series(np.abs(d["s_bps"].to_numpy()), index=idx).rolling(20).mean() + tpi = tp.copy() + tpi.index = (pd.DatetimeIndex(tpi.index).tz_localize("UTC") + if pd.DatetimeIndex(tpi.index).tz is None else pd.DatetimeIndex(tpi.index)) + tpi.index = tpi.index.normalize() + m = pd.concat({"tp": tpi, "g": sabs}, axis=1, join="inner").dropna() + if len(m) < 200: + print(f" finestra comune troppo corta ({len(m)}) -> non misurabile") + return + thr = m["g"].expanding(60).median().shift(1) # causale, espandente + base = m["tp"].to_numpy() + + def sh(x): + x = np.asarray(x, float) + return float(np.mean(x) / np.std(x) * np.sqrt(365.25)) if np.std(x) > 0 else np.nan + + rng = np.random.default_rng(66) + print(f"\n {'gate':<34}{'on%':>6}{'Sh':>8}{'null casuale (mediana)':>24}{'pctl':>7}" + f"{'IC95 del null':>22} esito") + for nome, on in (("CALMO acceso (|s| bassa = on)", (m["g"].shift(1) <= thr)), + ("AGITATO acceso (|s| alta = on)", (m["g"].shift(1) > thr))): + onv = on.astype(float).fillna(1.0).to_numpy() + gated = base * onv + duty = float(np.nanmean(onv)) + nulls = [] + for _ in range(2000): + # ROTAZIONE CIRCOLARE, non permutazione: conserva il ciclo di servizio **e** la + # struttura a run del gate (un gate vero e' fatto di periodi, non di giorni sparsi), + # randomizzando solo l'allineamento col rendimento. Null piu' severo e piu' onesto. + perm = np.roll(onv, int(rng.integers(1, len(onv)))) + nulls.append(sh(base * perm)) + nulls = np.array(nulls) + nulls = nulls[np.isfinite(nulls)] + pct = float((nulls < sh(gated)).mean() * 100) + lo, hi = np.percentile(nulls, [2.5, 97.5]) + esito = ("il gate spegne i giorni GIUSTI" if pct > 97.5 + else ("il gate spegne i giorni SBAGLIATI" if pct < 2.5 else "indistinguibile dal caso")) + print(f" {nome:<34}{duty*100:>6.0f}{sh(gated):>8.3f}{np.median(nulls):>24.3f}{pct:>7.1f}" + f"{f'[{lo:+.3f}, {hi:+.3f}]':>22} {esito}") + print(f"\n Sharpe di TP01 SENZA gate sulla stessa finestra: {sh(base):+.3f} (n={len(m)})") + print(" πŸ“Œ I due gate sono SPECULARI: se uno spegne i giorni sbagliati, l'altro spegne") + print(" quelli giusti β€” ma sceglierlo DOPO aver visto la tabella e' selezione, e va") + print(" detto. Cio' che si puo' concludere senza barare e' il FATTO: i giorni a") + print(" |scarto| alto NON sono i giorni da evitare per TP01, sono l'opposto.") + + +# =========================================================================================== +# 8 β€” rinforzo indipendente della taratura di venue_watch +# =========================================================================================== +def rinforzo(cache: dict) -> None: + print("\n" + "=" * 100) + print(" [8] SOTTOPRODOTTO β€” HL come QUARTA referenza per `venue_watch` (soglia 100 bps / 4h)") + print("=" * 100) + from src.live.venue_watch import PERSIST_HOURS, THRESHOLD_BPS + print(f" Soglia in produzione: {THRESHOLD_BPS:.0f} bps persistenti {PERSIST_HOURS}h a segno costante.") + print(f"\n {'asset':<5}{'tf':<5}{'n':>7}{'|s|med':>8}{'|s|p99':>9}{'|s|max':>9}" + f"{'>100bps':>9}{'episodi 4h':>12}") + for a in ASSETS: + for tf in ("1h", "1d"): + d = pair(cache, a, tf).dropna() + s = d["s_bps"] + over = (s.abs() > THRESHOLD_BPS).to_numpy() + sg = np.sign(s.to_numpy()) + ep, run, cs = 0, 0, 0 + for i in range(len(over)): + if over[i] and (run == 0 or sg[i] == cs): + run = run + 1 if run else 1 + cs = sg[i] + if run == PERSIST_HOURS: + ep += 1 + else: + run = 1 if over[i] else 0 + cs = sg[i] if over[i] else 0 + print(f" {a:<5}{tf:<5}{len(s):>7,}{s.abs().median():>8.2f}{s.abs().quantile(.99):>9.1f}" + f"{s.abs().max():>9.1f}{int(over.sum()):>9}{ep:>12}") + print("\n ⚠️ HL NON e' una referenza USD indipendente e non va aggiunta a `venue_watch` senza") + print(" verificare la composizione del suo oracolo: se e' costruito da prezzi CEX che") + print(" includono coppie USDT, aggiungerla farebbe rientrare l'USDT dalla finestra β€”") + print(" esattamente cio' che la regola Β«mai USDTΒ» vieta. Qui e' usata come MISURA, non") + print(" come referenza di produzione.") + print(" πŸ“Œ Cio' che invece e' un fatto utile: HL e' l'unico candidato-referenza con piu' di un") + print(" mese di storia recuperabile (2,6 anni a 1d, contro il MESE di Kraken) β€” ma a 1d, e") + print(" `venue_watch` decide a 1h. Non ripara la taratura degli 8 anni; puo' fare da") + print(" conferma LENTA.") + + +# =========================================================================================== +def main() -> None: + refresh = "--refresh" in sys.argv + print("=" * 100) + print(" Β§66 CROSS-VENUE β€” lo scarto Deribit-vs-Hyperliquid come SEGNALE (STAT-MODE)") + print("=" * 100) + print(" 🚨 STAT-MODE DICHIARATO IN TESTA: una gamba su Hyperliquid non e' eseguibile con questo") + print(" conto (Deribit, ~$635) senza aprire un secondo venue, e la decisione dell'operatore") + print(" del 26/07 tiene tutto su Deribit fino a $20k. Il filone si giudica su Β«esiste il") + print(" fenomeno?Β», non su Β«e' deployabile?Β».") + cache = load_cache(refresh) + try: + repliche(cache) + certifica_hl(cache) + verifica_lag(cache) + dec = scomposizione(cache) + soglie = muro(cache, dec) + rev = reversione(cache, soglie) + leadlag(cache) + gate(cache) + rinforzo(cache) + finally: + save_cache(cache) + + print("\n" + "=" * 100) + print(" VERDETTO") + print("=" * 100) + kall = max(rev, key=lambda k: rev[k]["mossa"] / rev[k]["muro"]) + misurate = {k: v for k, v in rev.items() if abs(v["bD"]) > v["mde"]} + print(f" Su 8 celle (2 asset x 4 timeframe) **{len(misurate)} hanno |bD| sopra la propria MDE**:" + f" {', '.join(f'{k[0]} {k[1]}' for k in misurate) or 'nessuna'}.") + if misurate: + kb = max(misurate, key=lambda k: misurate[k]["mossa"] / misurate[k]["muro"]) + v = misurate[kb] + arb = soglie[kb[0]]["arb"] + print(f" Miglior rapporto segnale/muro fra le celle MISURATE: **{v['mossa']/v['muro']:.3f}** " + f"({kb[0]} {kb[1]}: {v['mossa']:.2f} bps attesi a 1 sd contro {v['muro']:.2f} bps di costo" + f" su UN venue) -> manca un fattore **{v['muro']/v['mossa']:.1f}x**; per l'arbitraggio a" + f" due venue ({arb:.1f} bps) manca **{arb/v['mossa']:.1f}x**.") + va = rev[kall] + print(f" Il massimo INCONDIZIONATO sarebbe {va['mossa']/va['muro']:.3f} ({kall[0]} {kall[1]}), ma" + f" li' |bD|={abs(va['bD']):.2f} contro MDE {va['mde']:.2f}: **non e' piccolo, e' NON MISURATO**" + f" β€” citarlo sarebbe scegliere la cella piu' rumorosa perche' e' la piu' alta.") + print(" E ogni numero qui e' OTTIMISTICO tre volte: e' a 1 sd di scarto (non al valore tipico),") + print(" il segno di bD e' preso dagli stessi dati, e IGNORA che per entrare bisogna attraversare") + print(" gli stessi due book che generano lo scarto β€” cioe' si paga il rumore che si vuole incassare.") + print("\n In EUR/giorno: ZERO. Nessuna configurazione di questo filone e' negoziabile, quindi il") + print(" contributo alla rendita e' esattamente 0,00 EUR/giorno, oggi e a ogni capitale fino a") + print(" quando il costo per giro non scende sotto il fondo di rumore dei due book.") + print("=" * 100) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_curva_datati.py b/scripts/research/r0823b_curva_datati.py new file mode 100644 index 0000000..5ae91c3 --- /dev/null +++ b/scripts/research/r0823b_curva_datati.py @@ -0,0 +1,896 @@ +#!/usr/bin/env python3 +""" +r0823b_curva_datati.py β€” filone Β§65 CURVA-DATATI (ondata 2026-08-23) + +=========================================================================== +LA DOMANDA +=========================================================================== +L'ondata 1 (Β§13 BASIS-CALENDAR) ha ricostruito un dataset che il progetto non +aveva: **30 futures trimestrali SCADUTI** di Deribit (BTC 240.150 barre orarie / +ETH 233.334, dal 2018-09), **col deleveraging 2022 dentro** β€” la coda che a CC01 +mancava per costruzione. + +Finora e' stato usato per UNA cosa sola: il **livello** del carry (Β§13 lo ha +scartato come edge; Β§39 lo ha scartato come veicolo di TP01). **La FORMA della +curva β€” pendenza e curvatura β€” non e' mai stata guardata.** + +=========================================================================== +ATTESE DICHIARATE **PRIMA** DI MISURARE (regola 1 del metodo) +=========================================================================== + A1 Il LIVELLO della curva e' funding travestito. Β§39 ha gia' misurato che il + basis dei datati E' il funding del perp (+7,33% implicito contro +6,48% + realizzato). Mi aspetto |corr(livello, funding)| ALTA (>0,5). + -> se confermata, il ramo "livello" e' chiuso in partenza. + A2 La FORMA (pendenza, curvatura) e' un grado di liberta' NUOVO: mi aspetto + corr col funding BASSA. E' l'unica ragione per cui questo filone esiste. + A3 Nessun ramo produce un candidato. Il filone funding e' chiuso su QUATTRO + lati (carry cross-sectional, price-clock, time-series, affollamento) e Β§13 + ha gia' esaurito il primo ordine. Mi aspetto SCARTATO su tutti i rami. + A4 Il 2022 e' l'unico regime di stress del campione: mi aspetto che qualunque + segnale di curva abbia li' il suo unico anno buono (o il suo unico anno + cattivo) -> **scomposizione per anno OBBLIGATORIA prima di credere a + qualsiasi aggregato** (lezione SOL del 22/08: 24/24 ancore positive che + erano un anno solo). + A5 Il roll trimestrale costa. Β§39: SKH01 sui datati fa 48-57 roll-equivalenti + l'anno contro 6,6-8,4 di TP01 -> lo stesso mezzo spread gli costa 7 volte + tanto. Mi aspetto che il netto sia molto peggiore del lordo su qualunque + cosa ribilanci spesso. + A6 MDE: 7,4 anni -> MDE sullo Sharpe ~= 1,96/sqrt(7,4) = **0,72**. Un ramo + DELTA-NEUTRO (Β§13: SE del rendimento annuo 1,79pp, vol 4,9%) non e' + rispondibile; un ramo DIREZIONALE (vol 50-80%) lo e' in linea di principio. + +Esito di ciascuna: riportato in fondo, confermata/refutata. + +=========================================================================== +COME E' COSTRUITA LA CURVA β€” e perche' COSI' +=========================================================================== +Deribit lista SOLO trimestrali (piu' settimanali/mensili che qui non usiamo): +a ogni ora sono vivi 2-5 contratti (**4 nel 73% delle ore**). Il problema e' che +la scadenza del PRIMO oscilla fra 0 e 91 giorni (dente di sega deterministico). + +Interpolare a maturita' costante richiede estrapolazione nel 41-70% delle ore +(misurato). Quindi la curva e' costruita in **spazio FORWARD**, che non ha il +problema: + b1 = ln(F1/S) * 365,25/dte1 carry spot->primo (denominatore + che si annulla a scadenza: rumoroso) + fwd12 = ln(F2/F1) * 365,25/(dte2-dte1) forward carry, finestra ~91g FISSA + fwd23, fwd34 idem + slope = fwd23 - fwd12 curvatura = fwd34 - 2*fwd23 + fwd12 +Ogni forward carry e' misurato su una finestra di ~91 giorni SEMPRE, quindi non +c'e' denominatore che si annulla e non c'e' dente di sega nell'annualizzazione. +Cio' che resta di sega (la finestra del fwd12 PARTE a dte1, che oscilla) e' una +proprieta' vera della curva, e viene comunque controllata con un PLACEBO +esplicito (un segnale che e' funzione SOLA di dte1). + +πŸ“Œ Tutto in LOG del rapporto dei prezzi, mai medie aritmetiche di differenze: + la media aritmetica contiene (varA - varB)/2, che su questi dati vale + 1,48-2,60%/anno = 2-4x l'effetto cercato (regola gia' pagata, Β§39). + +=========================================================================== +IL LAG β€” verificato, non assunto (Β§39: corr 0,996 al lag +1) +=========================================================================== +`get_tradingview_chart_data` etichetta la barra all'INIZIO: il close della barra +etichettata T e' il prezzo a T+1h. `get_funding_rate_history` etichetta +l'indice all'ISTANTE T. Appaiarli senza lag sfasa di un'ora. +Il lag NON e' assunto: e' scelto misurando la sd del basis perp-vs-indice a +k = -2..+2 e la convergenza a scadenza. Vedi Β§0. + +=========================================================================== +DISCIPLINA +=========================================================================== +* SOLA LETTURA. Nessuna rete: tutto da `data/raw/fut_deribit/` (persistito + dall'ondata 1). Nessun ordine, nessuna chiave. +* Nessun file di produzione toccato. +* Famiglia DICHIARATA PRIMA e contata AL RIALZO: 86 backtest + 5 regressioni. + Il deflated-Sharpe e' riportato sia per RAMO sia sull'UNIONE, perche' la + stessa griglia da' verdetti diversi a seconda di come la si partiziona (Β§10, + Β§24): la partizione e' un secondo posto dove barare e' indolore e invisibile. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0823b_curva_datati.py +""" +from __future__ import annotations + +import sys +import warnings +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy import stats + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) +sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt")) + +import altlib as al # noqa: E402 +from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402 + +warnings.filterwarnings("ignore", category=RuntimeWarning) + +DATA = PROJECT_ROOT / "data" / "raw" / "fut_deribit" +MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", + "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] +YR = 365.25 +ASSETS = ("BTC", "ETH") +HOLDOUT = al.HOLDOUT +SIG_HOUR = 22 # ora di lettura della curva: nota a 23:00 UTC, un'ora + # PRIMA della chiusura della barra giornaliera -> causale + # con margine. La sensibilita' all'ora e' misurata in Β§6. + +# ---- famiglia DICHIARATA PRIMA (contata al rialzo) ------------------------ +VARS_A = ("b1", "fwd12", "slope", "slope_o", "curv") # 5 +ZWINS = (90, 180, 365) # 3 +SIGNS = (+1, -1) # 2 +MAPS = ("sgn", "lin") # 2 -> 60 celle +VARS_D = ("fwd12", "slope_o", "curv") # 3 +PCTLS = (0.30, 0.50, 0.70) # 3 +DIRS = (+1, -1) # 2 -> 18 celle +VARS_B = ("curv", "slope_o") # 2 +ZWINS_B = (90, 365) # 2 +SIGNS_B = (+1, -1) # 2 -> 8 celle +N_DECLARED_BT = 60 + 18 + 8 # 86 backtest +N_DECLARED_REG = 5 # 5 regressioni ramo C + +FEE_SIDE = al.FEE_SIDE # 5 bps/lato: la convenzione dei backtest del progetto + + +def hr(t: str = "") -> None: + print("\n" + "=" * 92) + if t: + print(t) + print("=" * 92) + + +# =========================================================================== +# Β§0 DATO β€” caricamento, LAG verificato, certificazione +# =========================================================================== +def expiry_of(name: str) -> pd.Timestamp: + _, tag = name.split("-") + return pd.Timestamp(2000 + int(tag[5:]), MONTHS.index(tag[2:5]) + 1, + int(tag[:2]), 8, tz="UTC") + + +def load_contracts(asset: str): + closes, exps, vols = {}, {}, {} + for f in sorted(DATA.glob(f"{asset}-*.parquet")): + nm = f.stem + df = pd.read_parquet(f) + if len(df) < 200: + continue + s = df.set_index("ts")["close"].astype(float) + s = s[s > 0] + s.index = s.index.tz_convert("UTC") + v = df.set_index("ts")["volume"].astype(float) + v.index = v.index.tz_convert("UTC") + e = expiry_of(nm) + s = s[s.index < e - pd.Timedelta(hours=2)] # esclude l'ora di settlement + closes[nm], exps[nm], vols[nm] = s, e, v + return closes, exps, vols + + +def load_fund(asset: str) -> pd.DataFrame: + f = pd.read_parquet(DATA / f"fund_{asset}.parquet").set_index("ts").sort_index() + f.index = f.index.tz_convert("UTC") + return f + + +def lag_check(asset: str) -> int: + """Sceglie il lag MISURANDOLO (Β§39: appaiare due serie dello stesso progetto con + convenzioni di etichettatura diverse e' un bug gia' pagato). Ritorna il lag in ore.""" + fund = load_fund(asset) + idx = fund["index"].astype(float) + p = al.get(asset, "1h") + perp = pd.Series(p["close"].values.astype(float), + index=pd.DatetimeIndex(pd.to_datetime(p["datetime"], utc=True))) + out = {} + for k in (-2, -1, 0, 1, 2): + ii = idx.copy() + ii.index = ii.index - pd.Timedelta(hours=k) + j = pd.concat({"p": perp, "i": ii}, axis=1, join="inner").dropna() + b = np.log(j["p"] / j["i"]) * 1e4 + out[k] = (len(j), float(b.median()), float(b.std())) + best = min(out, key=lambda k: out[k][2]) + print(f" {asset}: basis perp-vs-indice per lag (sd = il criterio)") + for k, (n, m, s) in out.items(): + mark = " <- SCELTO (sd minima)" if k == best else "" + print(f" lag {k:+d}h n={n:,} mediana {m:+7.2f} bps sd {s:8.2f} bps{mark}") + return best + + +def build_curve(asset: str, lag_h: int) -> pd.DataFrame: + closes, exps, vols = load_contracts(asset) + fund = load_fund(asset) + idx = fund["index"].astype(float) + f1h = fund["f1h"].astype(float) + wide = pd.DataFrame(closes).sort_index() + S = idx.reindex(wide.index + pd.Timedelta(hours=lag_h)) + S.index = wide.index + keep = S.notna() + wide, S = wide[keep], S[keep] + DTE = pd.DataFrame({nm: (exps[nm] - wide.index).total_seconds() / 86400.0 + for nm in wide.columns}, index=wide.index).where(wide.notna()) + dt_v, px_v, sv = DTE.values, wide.values, S.values + n = len(wide) + out = np.full((n, 6), np.nan) + for i in range(n): + m = np.isfinite(dt_v[i]) & np.isfinite(px_v[i]) & (dt_v[i] > 0.5) + d, p = dt_v[i][m], px_v[i][m] + o = np.argsort(d) + d, p = d[o], p[o] + k = len(d) + out[i, 5] = k + if k >= 1: + out[i, 0] = d[0] + out[i, 1] = np.log(p[0] / sv[i]) * YR / d[0] + if k >= 2: + out[i, 2] = np.log(p[1] / p[0]) * YR / (d[1] - d[0]) + if k >= 3: + out[i, 3] = np.log(p[2] / p[1]) * YR / (d[2] - d[1]) + if k >= 4: + out[i, 4] = np.log(p[3] / p[2]) * YR / (d[3] - d[2]) + C = pd.DataFrame(out, index=wide.index, + columns=["dte1", "b1", "fwd12", "fwd23", "fwd34", "nlive"]) + C["slope"] = C["fwd23"] - C["fwd12"] + C["curv"] = C["fwd34"] - 2 * C["fwd23"] + C["fwd12"] + C["fund24"] = f1h.reindex(C.index).rolling(24).mean() * 24 * YR + C["S"] = S + return C + + +def causal_orth(y: pd.Series, x: pd.Series, min_obs: int = 180) -> pd.Series: + """Residuo di y su x con beta stimato SOLO sul passato (media espandente, shift 1). + Se il beta usasse tutto il campione sarebbe look-ahead che `causality_ok` non vede.""" + j = pd.concat({"y": y, "x": x}, axis=1) + v = j.notna().all(axis=1) + yy = j["y"].where(v) + xx = j["x"].where(v) + n = v.astype(float).cumsum().shift(1) + sx = xx.fillna(0).cumsum().shift(1) + sy = yy.fillna(0).cumsum().shift(1) + sxx = (xx.fillna(0) ** 2).cumsum().shift(1) + sxy = (xx.fillna(0) * yy.fillna(0)).cumsum().shift(1) + cov = sxy / n - (sx / n) * (sy / n) + var = sxx / n - (sx / n) ** 2 + beta = (cov / var).where(n >= min_obs) + alpha = (sy / n) - beta * (sx / n) + return (yy - (alpha + beta * xx)).where(n >= min_obs) + + +def daily_curve(asset: str, C: pd.DataFrame, hour: int = SIG_HOUR) -> pd.DataFrame: + d = C[C.index.hour == hour].copy() + d.index = d.index.normalize() + d = d[~d.index.duplicated(keep="last")] + d["slope_o"] = causal_orth(d["slope"], d["fwd12"]) + return d + + +# =========================================================================== +# SEGNALI E BACKTEST +# =========================================================================== +def zscore(s: pd.Series, w: int) -> pd.Series: + m = s.rolling(w, min_periods=max(30, w // 2)).mean() + sd = s.rolling(w, min_periods=max(30, w // 2)).std() + return ((s - m) / sd).replace([np.inf, -np.inf], np.nan) + + +def direction_from(z: pd.Series, sign: int, mp: str) -> pd.Series: + if mp == "sgn": + d = np.sign(z) + else: + d = np.clip(z / 2.0, -1.0, 1.0) + return (sign * d).fillna(0.0) + + +def _align_to_bars(df: pd.DataFrame, s: pd.Series, fill: float = 0.0) -> np.ndarray: + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + return s.reindex(idx).fillna(fill).values.astype(float) + + +def _ms(idx: pd.DatetimeIndex) -> np.ndarray: + """Epoca in MILLISECONDI, esplicita. + ⚠️ `idx.view("int64")` NON e' sicuro: i parquet dei datati hanno risoluzione + `datetime64[ms]` e `al.get` `[ns]` -> la stessa riga di codice ritorna ms in un + posto e ns nell'altro, senza eccezione e senza NaN. E' la famiglia di bug + codificata il 01/07 (`.view("int64")` su indici tz-aware non-ns) e ripagata in + Β§35. Qui e' stata ripagata una TERZA volta, da me, e l'ha resa visibile un + `n=0` β€” non un numero sbagliato.""" + return (pd.DatetimeIndex(idx).tz_convert("UTC").as_unit("ns") + .astype("int64") // 10 ** 6) + + +def expanding_pctl(s: pd.Series, min_obs: int = 180) -> pd.Series: + """Percentile ESPANDENTE causale: quota delle osservazioni STRETTAMENTE + precedenti che stanno sotto quella corrente. O(n log n) con bisect.""" + import bisect + vals, seen, out = s.values.astype(float), [], np.full(len(s), np.nan) + for i, v in enumerate(vals): + if np.isfinite(v): + if len(seen) >= min_obs: + out[i] = bisect.bisect_right(seen, v) / len(seen) + bisect.insort(seen, v) + return pd.Series(out, index=s.index) + + +def cell_daily_A(dcur: dict, var: str, w: int, sign: int, mp: str, + fee_side: float = FEE_SIDE) -> pd.Series: + """Serie giornaliera netta 50/50 BTC+ETH del ramo A (stessa convenzione di + `altlib.candidate_daily`: eval_weights fa lo shift, non si puo' fare leak).""" + ser = {} + for a in ASSETS: + df = al.get(a, "1d") + d = dcur[a] + dirn = direction_from(zscore(d[var], w), sign, mp) + tgt = al.vol_target(_align_to_bars(df, dirn), df, target_vol=0.20, + vol_win_days=30, leverage_cap=2.0) + ev = al.eval_weights(df, tgt, fee_side=fee_side) + ser[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + return al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) + + +def tp01_targets() -> dict: + tp = TrendPortfolio(**CANONICAL) + return {a: tp.target_series(al.get(a, "1d")) for a in ASSETS} + + +def cell_daily_D(dcur: dict, tgts: dict, var: str, pctl: float, dirn: int, + g_off: float = 0.0, fee_side: float = FEE_SIDE, + t0: pd.Timestamp | None = None) -> pd.Series: + """TP01 moltiplicato per un gate di curva. Il percentile e' ESPANDENTE e causale.""" + ser = {} + for a in ASSETS: + df = al.get(a, "1d") + d = dcur[a] + rk = expanding_pctl(d[var]) + on = (rk >= pctl) if dirn > 0 else (rk <= pctl) + g = pd.Series(np.where(on.fillna(False), 1.0, g_off), index=d.index) + g[rk.isna()] = 1.0 # prima del warm-up: gate INATTIVO + # ⚠️ fill=1.0, NON 0.0: fuori dalla finestra della curva il gate non esiste, + # e riempire a zero spegnerebbe TP01 su meta' della sua storia. E' il difetto + # che il controllo d'identita' ha catturato (max|diff| 2,4e-02, non 0). + tgt = tgts[a] * _align_to_bars(df, g, fill=1.0) + ev = al.eval_weights(df, tgt, fee_side=fee_side) + ser[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + out = al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) + return out[out.index >= t0] if t0 is not None else out + + +def beta_baseline(fee_side: float = FEE_SIDE) -> pd.Series: + """LONG SEMPRE, vol-targeted 20% con lo stesso motore: e' il benchmark che il + ramo A deve battere. Senza di lui una cella 'quasi sempre lunga' su un campione + 2019-2026 con due tori sembra un segnale e non lo e'.""" + ser = {} + for a in ASSETS: + df = al.get(a, "1d") + tgt = al.vol_target(np.ones(len(df)), df, target_vol=0.20, + vol_win_days=30, leverage_cap=2.0) + ev = al.eval_weights(df, tgt, fee_side=fee_side) + ser[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + return al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) + + +def mean_pos(dcur: dict, var: str, w: int, sign: int, mp: str) -> float: + """Esposizione media della cella: se e' ~1 la cella e' un LONG travestito.""" + v = [] + for a in ASSETS: + df = al.get(a, "1d") + dirn = direction_from(zscore(dcur[a][var], w), sign, mp) + v.append(float(np.mean(_align_to_bars(df, dirn)))) + return float(np.mean(v)) + + +def sh(s) -> float: + return al._sh(s) + + +def dd(s) -> float: + return al._dd_ret(s) + + +def split(s: pd.Series): + return s[s.index < HOLDOUT], s[s.index >= HOLDOUT] + + +def per_year(s: pd.Series) -> dict: + return {int(y): round(sh(g), 2) for y, g in s.groupby(s.index.year) if len(g) > 60} + + +# =========================================================================== +def main() -> None: + print(__doc__.split("USO")[0]) + + # ---------------------------------------------------------------- Β§0 + hr("Β§0 IL DATO β€” lag MISURATO (non assunto), curva, certificazione") + lags = {} + for a in ASSETS: + lags[a] = lag_check(a) + lag = lags["BTC"] + print(f"\n -> lag adottato: +{lag}h su ENTRAMBI gli asset " + f"(BTC {lags['BTC']:+d}, ETH {lags['ETH']:+d})") + + curves, dcur = {}, {} + for a in ASSETS: + C = build_curve(a, lag) + curves[a] = C + dcur[a] = daily_curve(a, C) + nl = C["nlive"].value_counts().sort_index() + print(f"\n {a}: {len(C):,} ore {C.index.min().date()} .. {C.index.max().date()}" + f" ({(C.index.max()-C.index.min()).days/365.25:.1f} anni)") + print(f" contratti vivi/ora: " + + " ".join(f"{int(k)}:{v/len(C)*100:.0f}%" for k, v in nl.items())) + print(f" giorni con curva (ora {SIG_HOUR:02d}): {len(dcur[a]):,}") + + print("\n CONVERGENZA A SCADENZA |ln(F/indice)| ultime 4h β€” col lag e senza") + for a in ASSETS: + closes, exps, _ = load_contracts(a) + idx = load_fund(a)["index"].astype(float) + for k in (0, lag): + v = [] + for nm, s in closes.items(): + tail = s[s.index >= exps[nm] - pd.Timedelta(hours=6)] + if not len(tail): + continue + ii = idx.reindex(tail.index + pd.Timedelta(hours=k)) + x = np.abs(np.log(tail.values / ii.values)) * 1e4 + x = x[np.isfinite(x)] + if len(x): + v.append(float(np.median(x))) + print(f" {a} lag +{k}h: mediana {np.median(v):5.1f} bps (n={len(v)})") + print(" ⚠️ Β§13 pubblico' 33-47 bps: era calcolato SENZA il lag (merge_asof 'nearest').") + print(" Correzione a un numero pubblicato: il dato e' migliore di come e' stato descritto.") + + # ---------------------------------------------------------------- Β§1 + hr("Β§1 LA FORMA DELLA CURVA β€” livello, pendenza, curvatura (mai guardate prima)") + print(" Tutte le grandezze sono carry ANNUALIZZATI in log; fwdXY = forward carry") + print(" su finestra ~91g FISSA (niente denominatore che si annulla).\n") + for a in ASSETS: + C = curves[a] + print(f" --- {a} ---") + print(" var n mediana p05 p95 sd") + for c in ("b1", "fwd12", "fwd23", "fwd34", "slope", "curv", "fund24"): + s = C[c].dropna() + print(f" {c:9s} {len(s):7,} {s.median()*100:8.2f}% {s.quantile(.05)*100:9.2f}% " + f"{s.quantile(.95)*100:9.2f}% {s.std()*100:8.2f}%") + + # ---------------------------------------------------------------- Β§2 + hr("Β§2 PRIOR A1/A2 β€” la curva e' funding travestito? (si misura PRIMA di backtestare)") + print(" Attesa dichiarata: LIVELLO si (|corr|>0,5), FORMA no.\n") + corr_tab = {} + for a in ASSETS: + d = dcur[a] + print(f" --- {a} (giornaliero, n={len(d):,}) ---") + for c in ("b1", "fwd12", "fwd23", "fwd34", "slope", "slope_o", "curv"): + j = d[[c, "fund24"]].dropna() + r = float(j[c].corr(j["fund24"])) + corr_tab[(a, c)] = r + tag = " <- LIVELLO: funding travestito" if abs(r) > 0.5 else "" + print(f" corr({c:8s}, funding24h) = {r:+.3f} n={len(j):,}{tag}") + j = d[["slope", "fwd12"]].dropna() + print(f" [contorno] corr(slope, livello) = {j['slope'].corr(j['fwd12']):+.3f}" + f" -> la pendenza GREZZA e' in gran parte il livello col segno rovesciato") + print("\n CONTROLLO POSITIVO del rilevatore di correlazione (un rilevatore che non trova") + print(" nulla e' indistinguibile da uno rotto): rumore bianco vs funding, e funding vs se stesso") + rng = np.random.default_rng(20260823) + d = dcur["BTC"] + noise = pd.Series(rng.normal(size=len(d)), index=d.index) + j = pd.concat({"n": noise, "f": d["fund24"]}, axis=1).dropna() + print(f" corr(rumore, funding) = {j['n'].corr(j['f']):+.3f} (atteso ~0)") + print(f" corr(funding, funding) = {1.0:+.3f} (atteso +1)") + print("\n πŸ“Œ ESITO: A1 CONFERMATA (b1 +0,6 su entrambi), A2 CONFERMATA β€” e in modo piu'") + print(" forte dell'atteso: la pendenza ORTOGONALIZZATA al livello e la curvatura") + print(" sono praticamente scorrelate dal funding. **La forma e' un grado di") + print(" liberta' nuovo: il ramo non e' chiuso in partenza.**") + + # ---------------------------------------------------------------- Β§3 + hr("Β§3 PRE-TEST DI REGRESSIONE β€” e il suo CONTROLLO POSITIVO, che FALLISCE") + print(" Regressione dei rendimenti spot futuri sulla variabile di curva (z-unit).") + print(" t corretto grossolanamente per sovrapposizione: t_naive/sqrt(h).\n") + reg_rows = [] + for a in ASSETS: + d = dcur[a] + lp = np.log(d["S"]) + print(f" --- {a} ---") + print(" var h=5g h=21g h=63g") + for v in list(VARS_A) + ["mom90(CTRL+)"]: + if v.startswith("mom90"): + x0 = lp - lp.shift(90) + else: + x0 = d[v] + row = f" {v:11s}" + for h in (5, 21, 63): + fw = lp.shift(-h) - lp + jj = pd.concat([x0, fw], axis=1).dropna() + jj.columns = ["x", "y"] + if len(jj) < 100 or jj["x"].std() == 0: + row += " n/a " + continue + xz = (jj["x"] - jj["x"].mean()) / jj["x"].std() + sl, ic, rr, pv, se = stats.linregress(xz, jj["y"]) + t = sl / se / np.sqrt(h) + row += f" {sl*100:+6.2f}%(t{t:+5.2f})" + if not v.startswith("mom90"): + reg_rows.append(dict(asset=a, var=v, h=h, beta=sl, t=t)) + print(row) + print("\n 🚨 IL CONTROLLO POSITIVO NON PASSA: il momentum a 90 giorni β€” il segnale che") + print(" REGGE il 75% del libro live (TP01, Sharpe FULL 1,31 = t~3,6 su 7,4 anni) β€”") + print(" in questa lente da |t| 0,4-1,4. **La regressione predittiva univariata su") + print(" rendimenti crypto NON HA POTENZA a questo campione**: un t basso qui non e'") + print(" evidenza di assenza. -> il ramo si giudica sui BACKTEST (Β§4-Β§5), non su questi t.") + print(" (Le regressioni restano a registro: 5 variabili x 3 orizzonti x 2 asset.)") + + # ---------------------------------------------------------------- Β§4 + hr("Β§4 RAMO A β€” la curva come segnale DIREZIONALE (60 celle dichiarate)") + B = al.tp01_baseline_daily() + print(f" βœ… REPLICA prima di produrre numeri nuovi: TP01 baseline 50/50") + print(f" Sharpe FULL {sh(B):.3f} (pubblicato ~1,30) HOLD {sh(B[B.index>=HOLDOUT]):.3f}") + print(f" ⚠️ l'hold-out pubblicato e' ~0,31-0,44 a seconda del VINTAGE di data/raw/") + print(f" (gitignored, riscritto ogni notte): la terza cifra non e' confrontabile.\n") + + BB = beta_baseline() + bi, bh = split(BB) + print(f" BENCHMARK BETA (long sempre, vol-target 20%, stesso motore):") + print(f" Sharpe IS {sh(bi):+.3f} FULL {sh(BB):+.3f} HOLD {sh(bh):+.3f}" + f" maxDD {dd(BB)*100:.1f}%") + print(" ⚠️ e' IL benchmark del ramo A: su 2019-2026 (due tori) una cella che sta") + print(" long quasi sempre eredita questo Sharpe senza contenere informazione.\n") + + rowsA = [] + for var in VARS_A: + for w in ZWINS: + for sg in SIGNS: + for mp in MAPS: + s = cell_daily_A(dcur, var, w, sg, mp) + ins, hol = split(s) + rowsA.append(dict(var=var, w=w, sign=sg, map=mp, daily=s, + is_sh=sh(ins) if len(ins) > 60 else np.nan, + full=sh(s), hold=sh(hol) if len(hol) > 60 else np.nan, + dd=dd(s))) + assert len(rowsA) == 60, len(rowsA) + print(f" celle valutate: {len(rowsA)} (dichiarate 60)") + validA = [r for r in rowsA if np.isfinite(r["is_sh"])] + chosenA = max(validA, key=lambda r: r["is_sh"]) + print(f" distribuzione dei 60 Sharpe FULL: mediana {np.median([r['full'] for r in rowsA]):+.3f}" + f" min {min(r['full'] for r in rowsA):+.3f} max {max(r['full'] for r in rowsA):+.3f}") + print(f" frazione di celle con FULL>0: {np.mean([r['full']>0 for r in rowsA]):.0%}" + f" con HOLD>0: {np.mean([r['hold']>0 for r in rowsA]):.0%}") + print("\n CELLA SCELTA IN-SAMPLE-ONLY (nessuna occhiata all'hold-out):") + print(f" {chosenA['var']} w={chosenA['w']} sign={chosenA['sign']:+d} map={chosenA['map']}" + f" Sh IS {chosenA['is_sh']:+.3f} FULL {chosenA['full']:+.3f}" + f" HOLD {chosenA['hold']:+.3f} maxDD {chosenA['dd']*100:.1f}%") + best_hold = max(validA, key=lambda r: r["hold"]) + print(f" (per contrasto, la cella a MASSIMO hold-out β€” che NON e' selezionabile β€”:") + print(f" {best_hold['var']} w={best_hold['w']} sign={best_hold['sign']:+d} " + f"map={best_hold['map']} IS {best_hold['is_sh']:+.3f} HOLD {best_hold['hold']:+.3f})") + print("\n top-8 per Sharpe IN-SAMPLE (pos = esposizione media: ~1 => LONG travestito)") + print(" var w sgn map ShIS ShFULL ShHOLD maxDD pos") + for r in sorted(validA, key=lambda r: r["is_sh"], reverse=True)[:8]: + mp_ = mean_pos(dcur, r["var"], r["w"], r["sign"], r["map"]) + print(f" {r['var']:9s} {r['w']:4d} {r['sign']:+2d} {r['map']:4s}" + f" {r['is_sh']:+7.3f} {r['full']:+8.3f} {r['hold']:+7.3f} {r['dd']*100:7.1f}%" + f" {mp_:+6.2f}") + print(f" 🚨 NESSUNA delle 60 celle batte il benchmark beta ({sh(BB):+.3f} FULL): la") + print(f" migliore in-sample fa {chosenA['full']:+.3f}, la migliore in assoluto " + f"{max(r['full'] for r in rowsA):+.3f}.") + + allA = [r["full"] for r in rowsA] + dsrA, sr0A = al.deflated_sharpe(chosenA["full"], allA, chosenA["daily"]) + print(f"\n deflated-Sharpe (ramo A, N=60): {dsrA:.3f} " + f"(massimo atteso dal PURO RUMORE {sr0A:+.3f}) " + f"{'PASS' if dsrA >= 0.95 else 'FAIL'}") + + mgA = al.marginal_vs_tp01(chosenA["daily"]) + print(f" marginal_vs_tp01: {mgA['marginal_verdict']} corr {mgA['corr_full']:+.3f} " + f"beta {mgA['beta_to_tp01']:+.3f} alpha {mgA['alpha_ann']*100:+.2f}%/anno") + print(f" uplift w25 FULL {mgA['blends']['w25']['uplift_full']:+.3f} " + f"HOLD {mgA['blends']['w25']['uplift_hold']:+.3f} " + f"robust_oos {mgA['robust_oos']} has_insample_edge {mgA['has_insample_edge']} " + f"is_hedge {mgA['is_hedge']}") + earnsA = bool(mgA["marginal_verdict"] == "ADDS" and dsrA >= 0.95) + print(f" -> earns_slot_honest (ramo A) = {earnsA}") + + print("\n PER ANNO della cella scelta (A4: un aggregato si scompone prima di crederci):") + py = per_year(chosenA["daily"]) + print(" " + " ".join(f"{y}:{v:+.2f}" for y, v in py.items())) + pos = sum(1 for v in py.values() if v > 0) + print(f" anni positivi {pos}/{len(py)} 2022 (l'unico stress del campione): " + f"{py.get(2022, float('nan')):+.2f}") + + print("\n PLACEBO DEL DENTE DI SEGA (il rischio strutturale di questo dataset):") + print(" un segnale che e' funzione SOLA di dte1 (giorni alla prima scadenza) non") + print(" contiene informazione di curva. Se battesse la cella scelta, il ramo A") + print(" starebbe misurando il calendario dei roll, non la forma della curva.") + dsaw = {a: dcur[a].assign(**{"__saw": dcur[a]["dte1"]}) for a in ASSETS} + for w in ZWINS: + s = cell_daily_A(dsaw, "__saw", w, +1, "lin") + s2 = cell_daily_A(dsaw, "__saw", w, -1, "lin") + print(f" dte1 w={w:3d}: Sh FULL {sh(s):+.3f} / {sh(s2):+.3f} (segno +/-)" + f" pos {mean_pos(dsaw, '__saw', w, +1, 'lin'):+.2f}") + print(" 🚨 IL PLACEBO BATTE OGNI CELLA DI CURVA β€” e la diagnosi e' il contrario di") + print(" quella che sembra. I due segni sono SPECCHI quasi esatti e l'esposizione") + print(" media e' ~+1: il placebo non sta misurando il dente di sega, sta stando") + print(f" LONG, e riproduce il benchmark beta ({sh(BB):+.3f}). Cio' che dimostra e'") + print(" piu' severo: **nessun segnale di curva batte 'stai lungo e basta'** su") + print(" questo campione. Un placebo va letto sulla sua ESPOSIZIONE prima che sul") + print(" suo Sharpe, o si conclude l'opposto.") + + # ---------------------------------------------------------------- Β§5 + hr("Β§5 RAMO D β€” la curva come GATE DI REGIME su TP01 (18 celle dichiarate)") + tgts = tp01_targets() + T0 = max(dcur[a].index.min() for a in ASSETS) + base_gate = cell_daily_D(dcur, tgts, "fwd12", 0.0, +1) # gate sempre acceso + print(f" βœ… CONTROLLO DI IDENTITA': gate sempre acceso vs TP01 baseline sulla finestra comune") + j = pd.concat({"g": base_gate, "b": B}, axis=1, join="inner").dropna() + print(f" max|diff| = {float((j['g']-j['b']).abs().max()):.2e} " + f"Sh {sh(j['g']):.4f} vs {sh(j['b']):.4f}") + ref = base_gate[base_gate.index >= T0] + rowsD = [] + for var in VARS_D: + for p in PCTLS: + for dr in DIRS: + s = cell_daily_D(dcur, tgts, var, p, dr, t0=T0) + ins, hol = split(s) + rowsD.append(dict(var=var, p=p, dir=dr, daily=s, + is_sh=sh(ins), full=sh(s), hold=sh(hol), dd=dd(s))) + assert len(rowsD) == 18, len(rowsD) + chosenD = max(rowsD, key=lambda r: r["is_sh"]) + ri, rh = split(ref) + print(f"\n riferimento (TP01 sulla finestra della curva): IS {sh(ri):+.3f} " + f"FULL {sh(ref):+.3f} HOLD {sh(rh):+.3f} maxDD {dd(ref)*100:.1f}%") + print(" celle (18): var pctl dir ShIS ShFULL ShHOLD maxDD dShFULL") + for r in sorted(rowsD, key=lambda r: r["is_sh"], reverse=True): + mark = " <- scelta IS" if r is chosenD else "" + print(f" {r['var']:9s} {r['p']:.2f} {r['dir']:+d} {r['is_sh']:+7.3f}" + f" {r['full']:+8.3f} {r['hold']:+7.3f} {r['dd']*100:7.1f}% " + f"{r['full']-sh(ref):+8.3f}{mark}") + allD = [r["full"] for r in rowsD] + dsrD, sr0D = al.deflated_sharpe(chosenD["full"], allD, chosenD["daily"]) + print(f"\n deflated-Sharpe (ramo D, N=18): {dsrD:.3f} (rumore max atteso {sr0D:+.3f}) " + f"{'PASS' if dsrD >= 0.95 else 'FAIL'}") + + print("\n 🚨 NULL DEL DE-LEVERING (7 occorrenze pagate dal progetto): un gate SPEGNE") + print(" esposizione, quindi va confrontato con un semplice k*TP01 a PARI maxDD.") + tgt_dd = chosenD["dd"] + lo, hi = 0.05, 1.0 + for _ in range(40): + k = (lo + hi) / 2 + if dd(k * ref) > tgt_dd: + hi = k + else: + lo = k + k = (lo + hi) / 2 + print(f" gate scelto: maxDD {tgt_dd*100:.2f}% ShFULL {chosenD['full']:+.3f} " + f"ShHOLD {chosenD['hold']:+.3f}") + print(f" k*TP01 a pari maxDD (k={k:.3f}): maxDD {dd(k*ref)*100:.2f}% " + f"ShFULL {sh(k*ref):+.3f} ShHOLD {sh((k*ref)[(k*ref).index>=HOLDOUT]):+.3f}") + beats = chosenD["full"] > sh(k * ref) + 1e-9 + print(f" -> il gate batte il de-levering puro? {beats}") + print(" (nota: lo Sharpe e' invariante alla scala, quindi il de-levering NON puo'") + print(" cambiare lo Sharpe: il confronto giusto e' 'a pari DD lo Sharpe migliora?'") + print(" -> qui la domanda si riduce a dShFULL > 0, e il null e' DEGENERE (Β§24 (d)).") + print(" Il fatto che conta e' che il gate cambia lo Sharpe, non che riduca il DD.") + print("\n PER ANNO della cella D scelta, contro TP01:") + pyD, pyR = per_year(chosenD["daily"]), per_year(ref) + for y in sorted(set(pyD) | set(pyR)): + print(f" {y}: gate {pyD.get(y, float('nan')):+6.2f} TP01 {pyR.get(y, float('nan')):+6.2f}" + f" d {pyD.get(y,np.nan)-pyR.get(y,np.nan):+6.2f}") + + # ---------------------------------------------------------------- Β§6 + hr("Β§6 SENSIBILITA' ALL'ORA DI LETTURA (robustezza alla SCELTA, non un IC β€” Β§57)") + print(" 24 ore di lettura della curva, stessa griglia giornaliera 00:00. Tutte causali.") + bandA, bandD = [], [] + for h in range(24): + dc = {a: daily_curve(a, curves[a], hour=h) for a in ASSETS} + bandA.append(sh(cell_daily_A(dc, chosenA["var"], chosenA["w"], + chosenA["sign"], chosenA["map"]))) + bandD.append(sh(cell_daily_D(dc, tgts, chosenD["var"], chosenD["p"], + chosenD["dir"], t0=T0))) + for nm, band, canon in (("A", bandA, chosenA["full"]), ("D", bandD, chosenD["full"])): + b = np.array(band) + print(f" ramo {nm}: canonica(h={SIG_HOUR}) {canon:+.3f} mediana {np.median(b):+.3f}" + f" p10 {np.percentile(b,10):+.3f} p90 {np.percentile(b,90):+.3f}" + f" pctl della canonica {float((b<=canon).mean())*100:.0f}Β°") + bD = np.array(bandD) - sh(ref) + print(f" ramo D, dShFULL vs TP01 su 24 ore: mediana {np.median(bD):+.3f}, " + f"positivo in {int((bD>0).sum())}/24 (⚠️ ~2 osservazioni indipendenti, Β§57)") + + # ---------------------------------------------------------------- Β§7 + hr("Β§7 RAMO B β€” la BUTTERFLY delta-neutra (8 celle) e il muro dell'MDE") + print(" F1 - 2*F2 + F3 a nozionale netto ZERO: e' esattamente la CURVATURA, quotata.") + print(" Β§13 aveva testato il calendar spread (F1 vs F2, 108 celle); la butterfly e'") + print(" il secondo ordine e non era mai stata guardata.\n") + fly = {} + for a in ASSETS: + C = curves[a] + closes, exps, _ = load_contracts(a) + wide = pd.DataFrame(closes).sort_index().reindex(C.index) + DTEa = pd.DataFrame({nm: (exps[nm] - wide.index).total_seconds() / 86400.0 + for nm in wide.columns}, index=wide.index).where(wide.notna()) + lr = np.log(wide).diff() + dv, lv = DTEa.values, lr.values + pv = wide.values + n = len(wide) + out = np.full(n, np.nan) + turn = np.zeros(n) + prev = None + for i in range(n): + m = np.isfinite(dv[i]) & np.isfinite(pv[i]) & (dv[i] > 0.5) + ordr = np.argsort(np.where(m, dv[i], np.inf)) + sel = ordr[:3] + if not np.all(m[sel]) or len(sel) < 3: + prev = None + continue + wts = np.array([1.0, -2.0, 1.0]) + if prev is not None and np.array_equal(prev, sel): + r = np.nansum(wts * lv[i][sel]) + out[i] = r + else: + turn[i] = 4.0 # 4 gambe di nozionale unitario cambiate (1+2+1) + prev = sel + fly[a] = pd.Series(out, index=wide.index), pd.Series(turn, index=wide.index) + print(" rendimento LORDO della butterfly (nozionale lordo 4x, hold continuo):") + for a in ASSETS: + r, t = fly[a] + rd = r.resample("1D").sum().dropna() + yrs = (rd.index[-1] - rd.index[0]).days / YR + mu = float(rd.mean() * YR) + sd = float(rd.std() * np.sqrt(YR)) + se = sd / np.sqrt(yrs) + n_roll = int((t > 0).sum()) + print(f" {a}: drift {mu*100:+6.2f}%/anno vol {sd*100:5.2f}% Sharpe {mu/sd:+.2f}" + f" SE(drift) {se*100:.2f}pp roll {n_roll} in {yrs:.1f} anni") + print(f" 🚨 MDE: serve |drift| > {1.96*se*100:.2f}%/anno per distinguerlo da zero.") + rowsB = [] + for var in VARS_B: + for w in ZWINS_B: + for sg in SIGNS_B: + ser, ser0 = {}, {} + for a in ASSETS: + r, t = fly[a] + d = dcur[a] + z = zscore(d[var], w) + pos = (sg * np.clip(z / 2, -1, 1)).fillna(0.0) + rd = r.resample("1D").sum() + td = t.resample("1D").sum() + idx = rd.index.intersection(pos.index) + p = pos.reindex(idx).shift(1).fillna(0.0) + gross = p * rd.reindex(idx) + cost = (p.abs() * td.reindex(idx).fillna(0.0) + + 4.0 * p.diff().abs().fillna(0.0)) + ser[a] = gross - FEE_SIDE * cost + ser0[a] = gross + J = pd.concat(ser, axis=1, join="inner").fillna(0.0) + J0 = pd.concat(ser0, axis=1, join="inner").fillna(0.0) + s = 0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]] + s0 = 0.5 * J0[ASSETS[0]] + 0.5 * J0[ASSETS[1]] + ins, hol = split(s) + rowsB.append(dict(var=var, w=w, sign=sg, daily=s, is_sh=sh(ins), + full=sh(s), hold=sh(hol), gross=sh(s0))) + assert len(rowsB) == 8, len(rowsB) + chosenB = max(rowsB, key=lambda r: r["is_sh"]) + print("\n celle (8): var w sgn Sh LORDO ShIS ShFULL ShHOLD") + for r in sorted(rowsB, key=lambda r: r["is_sh"], reverse=True): + mark = " <- scelta IS" if r is chosenB else "" + print(f" {r['var']:9s} {r['w']:4d} {r['sign']:+d} {r['gross']:+9.3f}" + f" {r['is_sh']:+7.3f} {r['full']:+8.3f} {r['hold']:+7.3f}{mark}") + gmax = max(r["gross"] for r in rowsB) + print(f"\n πŸ“Œ il MIGLIOR lordo della famiglia e' {gmax:+.3f}: sotto l'MDE (Sharpe 0,72)") + print(" ANCHE a fee zero. Quindi NON e' morte-per-fee β€” la fee la seppellisce, ma") + print(" sotto non c'e' un edge lordo da salvare. (Distinzione che il progetto fa") + print(" sistematicamente: Β§42 TP01-LS, Β§7 CRT.)") + + # ---------------------------------------------------------------- Β§8 + hr("Β§8 RAMO C β€” la curva predice la VOLATILITA' futura? (5 regressioni)") + print(" RV30 futura su curva, CONTROLLANDO RV30 corrente e DVOL (che e' la previsione") + print(" di mercato). Senza i controlli si misura solo 'la vol e' persistente'.\n") + for a in ASSETS: + d = dcur[a] + lp = np.log(d["S"]) + r = lp.diff() + rv = r.rolling(30).std() * np.sqrt(YR) + fut = rv.shift(-30) + ts = pd.DataFrame({"timestamp": _ms(d.index)}) + dv = pd.Series(al.dvol(ts, a), index=d.index) / 100.0 + assert dv.notna().sum() > 500 and 0.2 < float(dv.median()) < 1.5, ( + "controllo positivo DVOL fallito: merge vuoto o fuori scala " + f"(n={int(dv.notna().sum())}, mediana={float(dv.median()):.3f})") + base = pd.concat({"y": fut, "rv": rv, "dv": dv}, axis=1).dropna() + print(f" --- {a} (n={len(base):,}, dal {base.index.min().date()}: DVOL parte 2021-03) ---") + for v in VARS_A: + jj = pd.concat([base, d[v].rename("x")], axis=1).dropna() + if len(jj) < 200: + print(f" {v:9s} n troppo piccolo") + continue + X = np.column_stack([np.ones(len(jj)), jj["rv"], jj["dv"], + (jj["x"] - jj["x"].mean()) / jj["x"].std()]) + bb, *_ = np.linalg.lstsq(X, jj["y"].values, rcond=None) + res = jj["y"].values - X @ bb + s2 = res @ res / (len(jj) - 4) + se = np.sqrt(np.diag(s2 * np.linalg.pinv(X.T @ X)))[3] + print(f" {v:9s} beta {bb[3]*100:+6.2f}pp di vol per 1sd " + f"t {bb[3]/se:+6.2f} (naive) t/sqrt(30) {bb[3]/se/np.sqrt(30):+5.2f}") + print("\n ⚠️ stesso limite di Β§3: nessun controllo positivo passa a questa risoluzione,") + print(" quindi queste righe sono descrittive, non decisive.") + + # ---------------------------------------------------------------- Β§9 + hr("Β§9 DEFLATED-SHARPE SULL'UNIONE, e la sua dipendenza dalla PARTIZIONE (Β§10/Β§24)") + allBT = allA + allD + [r["full"] for r in rowsB] + print(f" backtest dichiarati: {N_DECLARED_BT}, valutati: {len(allBT)} " + f"(+{N_DECLARED_REG} regressioni ramo C)") + for nm, pool, ch in (("ramo A (60)", allA, chosenA), ("ramo D (18)", allD, chosenD), + ("ramo B (8)", [r["full"] for r in rowsB], chosenB), + ("UNIONE (86)", allBT, chosenA)): + dsr, sr0 = al.deflated_sharpe(ch["full"], pool, ch["daily"]) + print(f" {nm:12s} N={len(pool):3d} sd(Sharpe di griglia) {np.std(pool, ddof=1):.3f}" + f" max atteso dal rumore {sr0:+.3f} DSR {dsr:.3f} " + f"{'PASS' if dsr >= 0.95 else 'FAIL'}") + print(" πŸ“Œ La stessa griglia da' verdetti diversi a seconda di come la si partiziona:") + print(" e' il secondo posto dove barare e' indolore e invisibile (dopo il conteggio).") + + # ---------------------------------------------------------------- Β§10 + hr("Β§10 ESEGUIBILITA' β€” il roll trimestrale, e il muro che nessun backtest supera") + lb = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/basis_cache/live_books.parquet") + if lb.exists(): + d = pd.read_parquet(lb) + print(" spread bid-ask VIVI (fotografia del 22/08, non storia β€” va detto):") + for _, r in d.iterrows(): + print(f" {r['instrument']:16s} spread {r['spread_bps']:6.2f} bps" + f" mezzo-spread {r['spread_bps']/2:5.2f} bps") + q = d[d.instrument.str.contains("-2")] + hs = float(q["spread_bps"].median()) / 2 + else: + hs = 3.0 + print(" (cache dei book non presente: mezzo-spread assunto 3,0 bps)") + print(f"\n costo per gamba = fee taker 3,50 bps + mezzo-spread mediano {hs:.2f} bps" + f" = {3.5+hs:.2f} bps") + print(f" un ROLL trimestrale = 2 gambe (chiudi F1, apri F2) = {2*(3.5+hs):.1f} bps,") + print(f" 4 roll/anno = {4*2*(3.5+hs):.0f} bps/anno = {4*2*(3.5+hs)/100:.2f}%/anno di drag") + print(" -> contro un premio a termine misurato da Β§13 a +0,85%/anno con IC95 che") + print(" CONTIENE LO ZERO. Il costo del roll da solo e' dello stesso ordine.") + print(f" ⚠️ A5: una butterfly ribilanciata ogni giorno paga 4 gambe per ribilancio.") + print(" Il ramo B sopra e' gia' al NETTO di questo, ed e' il motivo per cui il") + print(" suo lordo e il suo netto non si somigliano.") + + # ---------------------------------------------------------------- Β§11 + hr("Β§11 VERDETTO") + print(f" A1 (livello = funding travestito) -> CONFERMATA " + f"(corr b1/funding {corr_tab[('BTC','b1')]:+.2f} BTC, {corr_tab[('ETH','b1')]:+.2f} ETH)") + print(f" A2 (la FORMA e' un grado di liberta' nuovo) -> CONFERMATA e piu' forte " + f"dell'atteso (slope_o {corr_tab[('BTC','slope_o')]:+.2f}/" + f"{corr_tab[('ETH','slope_o')]:+.2f}, curv {corr_tab[('BTC','curv')]:+.2f}/" + f"{corr_tab[('ETH','curv')]:+.2f})") + print(f" A3 (nessun candidato) -> " + f"{'CONFERMATA' if not earnsA else 'REFUTATA'}") + print(f" A4 (2022 domina) -> vedi la scomposizione per anno") + print(f" A5 (il roll costa) -> {4*2*(3.5+hs)/100:.2f}%/anno, " + f"stesso ordine del premio") + print(f" A6 (MDE) -> ramo B non rispondibile, " + f"rami A/D si'") + print() + print(f" RAMO A direzionale : earns_slot_honest = {earnsA} " + f"(DSR {dsrA:.3f}, marginale {mgA['marginal_verdict']})") + print(f" RAMO D gate TP01 : dShFULL mediano su 24 ore {np.median(bD):+.3f}, " + f"DSR {dsrD:.3f}") + print(f" RAMO B butterfly : sotto l'MDE per costruzione") + print(f" RAMO C volatilita' : nessun controllo positivo passa -> non decisivo") + print() + isp = al.implausible_sharpe(chosenA["daily"]) + print(f" implausible_sharpe sulla cella A scelta: implausible={isp['implausible']}" + f" motivi={isp['reasons'] or 'nessuno'} barre attive {isp['active_frac']:.0%}" + f" barre attive in perdita {isp['loss_frac']:.0%}") + print() + verdetto = ("LA FORMA DELLA CURVA E' UN GRADO DI LIBERTA' NUOVO (scorrelato dal funding) " + "E NON PREDICE NULLA: 0 candidati su 4 rami") + print(f" `{verdetto}`") + print() + print(" In EUR/giorno: questo filone NON avvicina i 50 EUR/giorno. Il miglior effetto") + print(" misurato (ramo D, dShFULL mediano di banda) e' dentro il rumore d'ancora del") + print(" libro stesso; a $635 di conto vale **0,00 EUR/giorno**. Versare 500 EUR/mese") + print(" resta la leva dominante misurata dal progetto.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_fisco_payout.py b/scripts/research/r0823b_fisco_payout.py new file mode 100644 index 0000000..550c09f --- /dev/null +++ b/scripts/research/r0823b_fisco_payout.py @@ -0,0 +1,890 @@ +"""r0823b_fisco_payout β€” Β§63 FISCO-PAYOUT: l'aliquota sui payout funded e' un FORFAIT mai +giustificato, e il canale funded non e' mai stato misurato contro di essa (2026-08-23). + +IL BUCO CHE CHIUDE +------------------ +Il progetto ha modellato con cura il fisco sull'accumulo del LIBRO PROPRIO (33% sulle plusvalenze +cripto, minusvalenze in carry 4 anni, 2 per mille patrimoniale) e il 07/08 ha misurato che +ignorarlo costava -30% a 10 anni. Sul canale FUNDED non ha modellato niente: `r0725_prop_ladder.py` +importa `TAX_RATE = 0.33` da `r0725_capcurve` e lo applica ai payout con una riga + + net = gross * (1.0 - TAX_RATE) # r0725_prop_ladder:269 + +senza una giustificazione. Ed e' l'aliquota **sbagliata di CATEGORIA**, non solo di livello: su un +conto funded il trader **non possiede le posizioni** e **non possiede il conto**; cio' che riceve e' +un **compenso contrattuale** (profit share su un contratto di servizi), non una plusvalenza da +cripto-attivita' ex art. 67 c.1 lett. c-sexies TUIR. Il 33% e' l'aliquota di una cosa che qui non +succede. + +Importa perche' Β§37 conclude che **il biglietto batte il versare** con EUR 500/mese (mediana +$39.820 contro $22.444). Se l'aliquota vera fosse molto piu' alta di 33%, quella conclusione +potrebbe ribaltarsi; se fosse piu' bassa (regime forfettario), il canale varrebbe di piu'. + +QUESTO FILONE NON CONCLUDE QUALE SIA L'ALIQUOTA. Il progetto ha gia' una regola: una domanda +fiscale si **pone al commercialista**, non si risolve backtestando (07/08). Qui si fa l'unica cosa +che uno script puo' fare: **misurare quanto vale la risposta**, cioe' la sensibilita' delle +conclusioni del canale funded all'aliquota, e **formulare la domanda in modo che sia rispondibile**. +NON e' un parere fiscale. + +LE ATTESE, REGISTRATE PRIMA DI MISURARE +--------------------------------------- +A1. **L'EV del biglietto e' ESATTAMENTE lineare in (1-tau)** a livello di conto singolo: la soglia + di prelievo ($100 di quota trader) e' sul LORDO, quindi l'imposta non cambia quali eventi + accadono, solo quanto valgono. Previsione forte e falsificabile: misurato == analitico + **al bit**, non "entro il rumore MC". +A2. **A livello di SCALA DI CONTI la linearita' si ROMPE**, e nel verso sfavorevole: i payout netti + tornano in cassa e **ricomprano biglietti**, quindi l'imposta si compone. Mi aspetto che la + ricchezza terminale scenda **piu' che proporzionalmente** a (1-tau), e che il divario cresca + col deposito mensile (piu' cassa = piu' biglietti = piu' composizione). +A3. **L'EV del singolo biglietto resta positivo fino a un'aliquota assurda** (>70%), perche' il + prezzo ($579) e' piccolo rispetto a E[payout]. Quindi la gamba (c1) del `GATE PROP-01` + **non e' il criterio che morde**: mordera' il confronto con la strada-libro. +A4. **La soglia di ribaltamento cade DENTRO la banda plausibile** delle qualificazioni fiscali + (che va dal ~21% del forfettario al ~55-60% di IRPEF+addizionali+Gestione Separata) β†’ + la domanda al commercialista diventa **BLOCCANTE**. Questa e' l'attesa che conta: se invece + la soglia cadesse fuori dalla banda, la domanda resterebbe interessante e non decisiva. +A5. **L'aliquota EFFETTIVA di uno scaglione progressivo, al livello di reddito che questo canale + produce, e' molto piu' bassa dell'aliquota MARGINALE del suo scaglione.** EUR 50/giorno sono + ~EUR 18.250/anno: un'aliquota "43%" citata a mente sarebbe la marginale del terzo scaglione, + che a quel reddito non si tocca nemmeno. Mi aspetto che il modello progressivo, a reddito + preesistente ZERO, dia un'effettiva **sotto** il 33% che il progetto usa oggi β€” cioe' che il + forfait attuale sia **conservativo** proprio nello scenario che sembra il piu' minaccioso. + Ma con un reddito preesistente da lavoro dipendente il payout si somma **al margine** e + l'effettiva sale: **la stessa qualificazione da' due risposte diverse a seconda di chi la + riceve.** Questo, se confermato, e' il risultato trasferibile del filone. + +CONVENZIONI DICHIARATE (ereditate e congelate da Β§37, nessuna scelta nuova) +--------------------------------------------------------------------------- +* Cella d'ancora **MEDIANA** dei 23 offset x 3 fasi (non la canonica, che sta al 91 pctl), scelta + con lo stesso criterio di `r0822d_gate_c` Β§2 β€” **ricalcolata qui**, non copiata, e confrontata + con quella pubblicata. +* Conto funded: **W_A = TP01 38 / SKH01 38 / XS01 25**, XS01 = U13 Binance, finestra W-LONG 2021+, + de-luck sul drift x0,89, **funding DENTRO**, lente **ACCOPPIATA**. +* Conto proprio: libro **LIVE 75/25** a $654 con min-order $5, funding dentro, fisco sul modello + di `r0807_piano_netto` (aliquota sulla variazione ANNUA di valore, carry delle minusvalenze). +* Morte-firm 10%/anno, cap $200k/trader, split 80%, soglia di prelievo $100: assunzioni del + progetto, dichiarate. +* Bootstrap **CONDIVISO** fra tutte le aliquote e fra le due strade -> ogni confronto e' APPAIATO + e la differenza fra due aliquote e' molto meglio risolta del livello. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823b_fisco_payout.py` +Rete: NESSUNA. Scritture: NESSUNA. +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +T0 = time.time() +ROOT = Path(__file__).resolve().parents[2] +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) +if "--no-net" not in sys.argv: + sys.argv.append("--no-net") + +import r0725_prop_coupled as pc # noqa: E402 lente accoppiata +import r0725_prop_ladder as pl # noqa: E402 regole firm +import r0822_prop_alloc as PA # noqa: E402 boot_idx, DELUCK +import r0822b_prop_recal as PR # noqa: E402 pannelli, obj_rg +import r0822b_xs01_oos as X # noqa: E402 U13 +import r0822c_skh_anchor_prop as AP # noqa: E402 offset di SKH01 +import r0822d_funding as FD # noqa: E402 funding +import r0822d_gate_c as GC # noqa: E402 LA MACCHINERIA DI Β§37 +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +EURUSD = pl.EURUSD +SPLIT = pl.SPLIT +DELUCK = PA.DELUCK +W_A = GC.W_A +W_LIVE = GC.W_LIVE +OFFSETS = AP.OFFSETS +PHASES_SUB = AP.PHASES_SUB +N_LAD = GC.N_PATHS_LAD +N_TIX = GC.N_PATHS_TIX +SEED_LAD = GC.SEED_LAD +MIN_PAYOUT = GC.MIN_PAYOUT_TRADER +HORIZON_DAYS = GC.HORIZON_DAYS +PRICE_100K = 579.0 +# Cella d'ancora MEDIANA dei 23 offset x 3 fasi: **stampata da `r0822d_gate_c` sullo stesso +# vintage di feed** (corsa del 23/08: "offset 30 min, fase 7, leva 0.75x"). E' un INPUT +# ereditato e congelato, come tutte le altre convenzioni di Β§37, non una scelta di questo +# filone. Ri-derivarla costa ~70 valutazioni della scala di conti: `--band` la ricalcola e la +# confronta con questa costante (fatto una volta; su una macchina scarica sono ~4 minuti). +CELL_M = (30, 7, 0.75) +BAND = "--band" in sys.argv + +# numeri PUBBLICATI da riprodurre PRIMA di produrne di nuovi (Β§37 / Β§32) +PUB = dict(ev_sunk=1615.0, ev_at_pass=1919.0, ev_at_payout=1835.0, + p50g_fund=0.026, pzero_fund=0.403, + med_big_500=39820.0, med_lib_500=22444.0, + med_big_0=132.0, med_lib_0=787.0, + mean_big_500=46298.0, mean_lib_500=22694.0, + mean_big_0=9592.0, mean_lib_0=803.0) + + +# =============================================================================== 0. IL FISCO +# ATTENZIONE β€” QUESTA SEZIONE NON E' UN PARERE FISCALE. +# Ogni riga qui sotto e' una IPOTESI DI QUALIFICAZIONE con la sua fonte e il suo grado di +# affidabilita'. Il progetto ha gia' pagato una citazione normativa sbagliata cablata in 5 file +# (la "L.199/2025" che non era l'origine del 33%, corretta il 07/08): percio' cio' che NON e' +# stato verificato su fonte primaria e' etichettato come tale, e non c'e' un default nascosto. +# +# IL FATTO STRUTTURALE, che vale piu' di qualunque aliquota: +# su un conto funded il trader NON possiede il conto, NON possiede le posizioni, e non c'e' +# nessun "realizzo di cripto-attivita'". Riceve una quota di un risultato SIMULATO in base a un +# contratto. Percio' l'art. 67 c.1 lett. c-sexies (33%, cripto) e la lett. c-quater (26%, +# derivati) β€” le due caselle su cui il progetto sta discutendo per il LIBRO PROPRIO β€” sono +# entrambe **fuori categoria** per il canale funded. La discussione vera e' fra reddito +# diverso da "obbligazione di fare" (art. 67 c.1 lett. l), lavoro autonomo (abituale o +# occasionale) e reddito d'impresa. Cambia la CATEGORIA, non solo il numero. + +# IRPEF 2026 β€” VERIFICATA in sessione (WebSearch 23/08): tre scaglioni 23% / 33% / 43%, con il +# SECONDO sceso da 35% a 33% per effetto della **L. 199/2025** (Legge di Bilancio 2026). +# ⚠ NOTA CHE EVITA UN EQUIVOCO CHE QUESTO PROGETTO HA GIA' PAGATO UNA VOLTA: la "L. 199/2025" +# e' la legge che il 07/08 fu tolta dai commenti come origine SBAGLIATA del 33% sulle cripto +# (quello viene dalla L. 207/2024 art. 1 c. 23-29). Qui la stessa legge ricompare LEGITTIMAMENTE, +# ma per un'altra cosa: il secondo scaglione IRPEF. **E il 33% IRPEF e il 33% cripto sono lo +# stesso numero per pura coincidenza** β€” confonderli renderebbe invisibile tutta la differenza +# di CATEGORIA che questo filone misura. Fonte: rassegna professionale (IPSOA/FiscoeTasse), +# non letta su Normattiva -> PROFESSIONALE, non primaria. +IRPEF_SCAGLIONI = ((28_000.0, 0.23), (50_000.0, 0.33), (float("inf"), 0.43)) +IRPEF_2025 = ((28_000.0, 0.23), (50_000.0, 0.35), (float("inf"), 0.43)) # controllo di robustezza +ADDIZIONALI = 0.0203 # regionale minima 1,23% + comunale max 0,80% (fonte SECONDARIA) +INPS_GS_ALIQ = 0.2607 # Gestione Separata, non assicurati altrove +INPS_GS_QUOTA_PRESTATORE = 1.0 / 3.0 +INPS_OCC_FRANCHIGIA = 5_000.0 +FORFETTARIO_COEFF = 0.78 # coefficiente di redditivita' tipico dei servizi professionali +FORFETTARIO_ALIQ = 0.05 # 5% start-up (primi 5 anni), poi 15% + + +def irpef(imponibile: float, scaglioni=None) -> float: + """IRPEF lorda a 3 scaglioni. Nessuna detrazione (conservativo: le detrazioni la abbassano).""" + t, prev = 0.0, 0.0 + for soglia, aliq in (scaglioni or IRPEF_SCAGLIONI): + if imponibile <= prev: + break + t += (min(imponibile, soglia) - prev) * aliq + prev = soglia + return t + + +def eff_progressiva(lordo_annuo: float, altro_reddito: float, inps: str, + scaglioni=None, spese: float = 0.0) -> float: + """Aliquota EFFETTIVA su `lordo_annuo` di payout che si SOMMA a `altro_reddito`. + `inps`: 'no' | 'occasionale' (franchigia 5k, quota prestatore 1/3) | 'piena' (tutta a carico). + Committente ESTERO -> nessuna ritenuta d'acconto, tutto in dichiarazione: qui e' irrilevante + per l'ALIQUOTA (cambia la cassa, non il dovuto) e va detto. + Il contributo previdenziale e' deducibile dall'imponibile IRPEF: modellato.""" + if lordo_annuo <= 0: + return 0.0 + # art. 71 c.2 TUIR (verificato sul testo, 23/08): i redditi di cui all'art. 67 c.1 lett. l) + # "sono costituiti dalla DIFFERENZA fra l'ammontare percepito nel periodo di imposta e le + # spese specificamente inerenti alla loro produzione" -> le quote di challenge sono un costo + # inerente e si DEDUCONO. Sotto un'imposta sostitutiva su reddito finanziario, no. + netto = max(0.0, lordo_annuo - spese) + if inps == "no": + contrib = 0.0 + elif inps == "occasionale": + base = max(0.0, netto - INPS_OCC_FRANCHIGIA) + contrib = base * INPS_GS_ALIQ * INPS_GS_QUOTA_PRESTATORE + else: + contrib = netto * INPS_GS_ALIQ + imp_con = max(0.0, altro_reddito + netto - contrib) + imp_senza = max(0.0, altro_reddito) + d_irpef = irpef(imp_con, scaglioni) - irpef(imp_senza, scaglioni) + d_add = (imp_con - imp_senza) * ADDIZIONALI + return (contrib + d_irpef + d_add) / lordo_annuo + + +def eff_forfettario(lordo_annuo: float, aliq: float = FORFETTARIO_ALIQ, + coeff: float = FORFETTARIO_COEFF) -> float: + """Regime forfettario: imponibile = lordo x coefficiente; imposta sostitutiva `aliq`; + Gestione Separata piena sull'imponibile (deducibile).""" + if lordo_annuo <= 0: + return 0.0 + imponibile = lordo_annuo * coeff + contrib = imponibile * INPS_GS_ALIQ + imposta = max(0.0, imponibile - contrib) * aliq + return (contrib + imposta) / lordo_annuo + + +# ==================================================== 1. LA SCALA DI CONTI CON L'IMPOSTA APERTA + +def ladder_tax(R: np.ndarray, G: np.ndarray, n_paths: int, lev: float, seed: int, + netfn, refund: bool = True, deposit_eur: float = 0.0, + start_eur: float = pl.START_EUR, min_payout: float = MIN_PAYOUT, + boot: np.ndarray | None = None, block: int = 20, + deluck: float = DELUCK, pay_per_year: int = 12) -> dict: + """COPIA di `r0822d_gate_c.ladder` con **un solo grado di liberta' in piu'**: al posto della + riga `net = gross * (1 - TAX_RATE)` c'e' `netfn(gross, lordo_annuo_gia_incassato)`. + Tutto il resto β€” ordine delle chiamate al generatore compreso β€” e' identico, e Β§2.1 lo + verifica **bit-exact** contro l'originale con `netfn` piatta al 33%. + + `pay_per_year` = payout per anno fiscale (12 prelievi da 30 giorni = 360 gg: l'accumulatore + annuale si azzera li'). Serve solo alle aliquote PROGRESSIVE, che dipendono dal lordo annuo. + L'imposta e' applicata **al momento del prelievo** con l'aliquota marginale sul cumulato + annuo: la somma sull'anno e' l'imposta progressiva esatta, ma la CASSA la vede prima che + a saldo. Differenza dichiarata, e conservativa (toglie liquidita' prima).""" + rng = np.random.default_rng(seed) + n_days = HORIZON_DAYS + L = len(R) + Rm = R.astype(float).reshape(-1, 1).copy() + Gm = G.astype(float).reshape(-1, 1).copy() + Rm = Rm - (1.0 - deluck) * Rm.mean(axis=0, keepdims=True) + if boot is None: + n_blocks = int(np.ceil(n_days / block)) + starts = rng.integers(0, L - block, size=(n_paths, n_blocks)) + idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] + else: + idx = boot + n_slot = pl.MAX_CONCURRENT + sel = np.zeros(n_slot, dtype=int) + Rs = Rm[idx][:, :, sel] * lev + G_ = Gm[idx][:, :, sel] * lev + + FIRMS = pl.FIRMS + cash = np.full(n_paths, start_eur * EURUSD) + phase = np.full((n_paths, n_slot), -1, np.int8) + eq = np.ones((n_paths, n_slot)) + firm = np.full((n_paths, n_slot), -1, np.int8) + notional = np.zeros((n_paths, n_slot)) + price_paid = np.zeros((n_paths, n_slot)) + refunded = np.zeros((n_paths, n_slot), bool) + firm_alive = np.ones((n_paths, len(FIRMS)), bool) + cum_payout = np.zeros(n_paths) + cum_gross = np.zeros(n_paths) + ann_gross = np.zeros(n_paths) + spent_base = np.zeros(n_paths) + spent = np.zeros(n_paths) + got_back = np.zeros(n_paths) + deposited = np.zeros(n_paths) + n_payouts = np.zeros(n_paths, int) + n_pay_events = 0 + p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0) + + for t in range(n_days): + died = rng.random((n_paths, len(FIRMS))) < p_death_day + newly_dead = died & firm_alive + firm_alive &= ~died + if newly_dead.any(): + for fi in range(len(FIRMS)): + kill = newly_dead[:, fi] + if kill.any(): + hit = (firm == fi) & kill[:, None] + phase[hit] = -1 + notional[hit] = 0.0 + + for k in range(n_slot): + free = phase[:, k] == -1 + if not free.any(): + continue + for fi, F in enumerate(FIRMS): + used = np.where(firm == fi, notional, 0.0).sum(axis=1) + for (size, price) in sorted(F["tickets"], key=lambda x: -x[0]): + buy = (free & firm_alive[:, fi] & (cash >= price) + & (used + size <= F["cap"])) + if not buy.any(): + continue + cash[buy] -= price + spent[buy] += price + phase[buy, k] = 0 + eq[buy, k] = 1.0 + firm[buy, k] = fi + notional[buy, k] = size + price_paid[buy, k] = price + refunded[buy, k] = False + free = free & ~buy + if not free.any(): + break + if not free.any(): + break + + r = Rs[:, t, :] + g = G_[:, t, :] + act = phase >= 0 + eq_prev = eq + eq = np.where(act, eq * (1.0 + r), eq) + eq_low = np.where(act, eq_prev * (1.0 + r + g), eq) + r_low = r + g + + for fi, F in enumerate(FIRMS): + s_ = act & (firm == fi) + if not s_.any(): + continue + ev_t, ev_dd, ev_dl = F["ev"] + fu_ml, fu_dl = F["fu"] + in_ev = s_ & (phase == 0) + in_fu = s_ & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl))) + | (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl)))) + phase[bust] = -1 + notional[bust] = 0.0 + passed = in_ev & (eq >= 1.0 + ev_t) & ~bust + phase[passed] = 1 + eq[passed] = 1.0 + + if (t + 1) % 30 == 0: + fu = phase == 1 + pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout) + gain = np.where(pay, (eq - 1.0) * notional, 0.0) + gross = gain.sum(axis=1) * SPLIT + net = netfn(gross, ann_gross, spent - spent_base) + cum_payout += net + cum_gross += gross + ann_gross += gross + cash += net + n_payouts += (pay.sum(axis=1) > 0).astype(int) + eq = np.where(pay, 1.0, eq) + if refund: + back = pay & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + got_back += amt + refunded |= back + if deposit_eur: + cash += deposit_eur * EURUSD + deposited += deposit_eur * EURUSD + n_pay_events += 1 + if n_pay_events % pay_per_year == 0: + ann_gross[:] = 0.0 + spent_base = spent.copy() + + eur_day = cum_payout / EURUSD / (n_days / 365.0) / 365.0 + return dict(eur_day=eur_day, med=float(np.median(eur_day)), + p_ge10=float((eur_day >= 10.0).mean()), + p_target=float((eur_day >= pl.TARGET_EUR_DAY).mean()), + p_zero=float((cum_payout <= 0).mean()), + cash=cash, med_cash=float(np.median(cash)), mean_cash=float(cash.mean()), + p10_cash=float(np.percentile(cash, 10)), + p90_cash=float(np.percentile(cash, 90)), + gross=float(cum_gross.mean()), net=float(cum_payout.mean()), + spent=float(spent.mean()), + excess=cash - (start_eur * EURUSD + deposited), + mean_back=float(got_back.mean()), med_spent=float(np.median(spent)), + contributed=float(start_eur * EURUSD + deposited.mean())) + + +def net_flat(rate: float): + """Imposta sostitutiva piatta su reddito FINANZIARIO: le quote di challenge NON sono + deducibili (una challenge persa non e' una minusvalenza da cripto-attivita'). + Espressione IDENTICA a quella di `GC.ladder` -> bit-exact.""" + def f(gross, ann, spese): + return gross * (1.0 - rate) + return f + + +def net_prog(altro_reddito: float, inps: str, scaglioni=None, deduci: bool = True): + """Aliquota progressiva: marginale sul cumulato annuo, cosi' la somma sull'anno e' esatta. + `deduci` = le quote di challigne dell'anno si sottraggono dalla base (art. 71 c.2 TUIR).""" + def f(gross, ann, spese): + out = np.zeros_like(gross) + nz = gross > 0 + if nz.any(): + sp = np.asarray(spese, float) / EURUSD if deduci else np.zeros(len(gross)) + for i in np.flatnonzero(nz): + a0, g = float(ann[i]) / EURUSD, float(gross[i]) / EURUSD + dovuto = (eff_progressiva(a0 + g, altro_reddito, inps, scaglioni, sp[i]) * (a0 + g) + - eff_progressiva(a0, altro_reddito, inps, scaglioni, sp[i]) * a0) + out[i] = float(gross[i]) * (1.0 - dovuto / g) + return out + return f + + +def net_forf(aliq: float = FORFETTARIO_ALIQ, coeff: float = FORFETTARIO_COEFF): + """Forfettario: i costi NON si deducono per costruzione β€” li sostituisce il coefficiente.""" + def f(gross, ann, spese): + out = np.zeros_like(gross) + nz = gross > 0 + if nz.any(): + for i in np.flatnonzero(nz): + a0, g = float(ann[i]) / EURUSD, float(gross[i]) / EURUSD + dovuto = (eff_forfettario(a0 + g, aliq, coeff) * (a0 + g) + - eff_forfettario(a0, aliq, coeff) * a0) + out[i] = float(gross[i]) * (1.0 - dovuto / g) + return out + return f + + +# ================================================================= 2. IL PANNELLO E LA CELLA + +def hr(t: str = "") -> None: + print("\n" + "-" * 104) + if t: + print(" " + t) + print("-" * 104) + + +def build(verbose: bool = True) -> dict: + """Ricostruisce ESATTAMENTE il contesto di `r0822d_gate_c` Β§0.2 + Β§2, cella d'ancora + MEDIANA compresa. Non e' copiata dal diario: e' ricalcolata e poi confrontata.""" + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13 = Ob.reindex(p13.index)[X.U13] + h13 = Hb.reindex(p13.index)[X.U13] + l13 = Lb.reindex(p13.index)[X.U13] + XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES_SUB} + xs0 = XSP[PHASES_SUB[0]] + cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index + base = cry_idx.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC")) + I = base[base >= S_LONG] + ev_b = PA.boot_idx(len(I), GC.EV_H, PA.N_PATHS, seed=GC.SEED_EV) + fu_b = PA.boot_idx(len(I), GC.FU_H, PA.N_PATHS, seed=GC.SEED_FU) + pc.DELUCK = DELUCK + rate = GC.funding_rate() + if rate is None: + raise SystemExit("cache funding assente: senza funding i numeri non sono quelli di Β§37") + xs_ann = GC.xs_funding_measure(verbose=False)["ann"] + if verbose: + print(f" W-LONG: {len(I)} giorni {I[0].date()} -> {I[-1].date()} | de-luck x{DELUCK}") + print(f" funding di XS01 misurato: {xs_ann*100:+.2f}%/anno (costo) β€” Β§37") + print(f" banda d'ancora: {len(OFFSETS)} offset x {len(PHASES_SUB)} fasi {PHASES_SUB} " + f"= {len(OFFSETS)*len(PHASES_SUB)} celle, ladder {N_LAD} percorsi ciascuna") + + S = None + if BAND: + rows = [] + t = time.time() + for off in OFFSETS: + fA = GC.fvec_for(W_A, I, rate, off, xs_ann) + for ph in PHASES_SUB: + R0, G0 = AP.rg_off(W_A, I, XSP[ph], off) + Rx = R0 - fA + j, lv, _ = GC.best_lev(Rx, G0, ev_b, fu_b) + lad = GC.ladder(Rx, G0, N_LAD, lv, SEED_LAD, refund=False) + rows.append(dict(off=off, ph=ph, J=j, lev=lv, p50=lad["p_target"], + pz=lad["p_zero"])) + S = pd.DataFrame(rows) + k = int((S["p50"] - S["p50"].median()).abs().idxmin()) + OFF_M, PH_M, LEV_M = int(S.loc[k, "off"]), int(S.loc[k, "ph"]), float(S.loc[k, "lev"]) + if verbose: + print(f" banda RI-DERIVATA ({time.time()-t:.0f}s) P(>=50/g) mediana " + f"{S['p50'].median():.1%} [{np.percentile(S['p50'],10):.1%}, " + f"{np.percentile(S['p50'],90):.1%}], P(zero) {S['pz'].median():.1%} " + f"(PUB Β§37 {PUB['p50g_fund']:.1%} / {PUB['pzero_fund']:.1%})") + print(f" cella ri-derivata {(OFF_M, PH_M, LEV_M)} contro la costante {CELL_M} -> " + f"{'COINCIDE' if (OFF_M, PH_M, LEV_M) == CELL_M else 'DIVERGE'}") + else: + OFF_M, PH_M, LEV_M = CELL_M + if verbose: + print(f" cella d'ancora MEDIANA ereditata da `r0822d_gate_c`: {CELL_M} " + f"(offset min, fase, leva). `--band` la ri-deriva dalle 69 celle.") + fM = GC.fvec_for(W_A, I, rate, OFF_M, xs_ann) + R_M0, G_M = AP.rg_off(W_A, I, XSP[PH_M], OFF_M) + R_M = R_M0 - fM + rb_full = FD.book_75_25(rate, alloc=654.0) + if rb_full.index.tz is None: + rb_full.index = rb_full.index.tz_localize("UTC") + rb = np.nan_to_num(rb_full.reindex(I).values.astype(float)) + rngb = np.random.default_rng(51224) + nb = int(np.ceil(HORIZON_DAYS / 20)) + st = rngb.integers(0, len(I) - 20, size=(N_LAD, nb)) + BOOT = (st[:, :, None] + np.arange(20)[None, None, :]).reshape(N_LAD, -1)[:, :HORIZON_DAYS] + return dict(I=I, R_M=R_M, G_M=G_M, LEV_M=LEV_M, OFF_M=OFF_M, PH_M=PH_M, + rb=rb, BOOT=BOOT, band=S) + + +# ============================================================== 3. GLI SCENARI DI QUALIFICAZIONE +# Ogni riga e' un'IPOTESI, con la sua fonte e il suo grado. NESSUNA e' "la risposta". +# Ricerca documentale del 23/08 (15 fra ricerche e fetch): **nessuna fonte PRIMARIA italiana +# sulle prop firm** β€” nessuna circolare, risoluzione o interpello dell'Agenzia delle Entrate che +# qualifichi il profit share di un funded account. Due fonti professionali lo dichiarano +# espressamente ("mancano chiarimenti ufficiali"). Le fonti professionali convergono sull'area +# LAVORO AUTONOMO; l'ipotesi "reddito finanziario 26%" e' sostenuta da UNA sola fonte, la meno +# autorevole del corpus, che nello stesso paragrafo afferma una ritenuta alla fonte del 26% da +# parte della firm estera β€” quasi certamente falsa (un non residente senza stabile organizzazione +# non e' sostituto d'imposta ex d.P.R. 600/1973). + +def scenari() -> list[dict]: + """(etichetta, funzione netta, grado della fonte). Ordinate per aliquota effettiva crescente + a EUR 18.250 lordi/anno (= EUR 50/giorno, il bersaglio del progetto).""" + return [ + dict(k="S0", lab="nessuna imposta (CONTROLLO, non uno scenario)", + fn=net_flat(0.0), src="controllo", tag="CTRL"), + dict(k="S1", lab="forfettario 5% start-up, coeff 67% (ATECO 82.99)", + fn=net_forf(0.05, 0.67), src="PROFESSIONALE (coeff.), ATECO non motivato", tag=""), + dict(k="S2", lab="forfettario 5% start-up, coeff 78% (ATECO 66.xx/74.90.99)", + fn=net_forf(0.05, 0.78), src="PROFESSIONALE (coeff.), ATECO non motivato", tag=""), + dict(k="S3", lab="sostitutiva 26% β€” art. 67 c-quater (redditi diversi finanz.)", + fn=net_flat(0.26), src="SECONDARIA: UNA fonte, non motivata", tag=""), + dict(k="S4", lab="forfettario 15% a regime, coeff 78%", + fn=net_forf(0.15, 0.78), src="PROFESSIONALE", tag=""), + dict(k="S5", lab="lav.aut. OCCASIONALE, INPS 1/3, nessun altro reddito", + fn=net_prog(0.0, "occasionale"), src="PROFESSIONALE: l'ipotesi piu' sostenuta", + tag="OCC"), + dict(k="S6", lab="sostitutiva 33% c-sexies = IL FORFAIT DEL PROGETTO OGGI", + fn=net_flat(0.33), src="aliquota primaria, CATEGORIA senza alcuna fonte", + tag="PROGETTO"), + dict(k="S7", lab="lav.aut. OCCASIONALE, INPS 1/3, altro reddito EUR 30.000", + fn=net_prog(30_000.0, "occasionale"), src="PROFESSIONALE + assunzione sul reddito", + tag="OCC30"), + dict(k="S8", lab="lav.aut. ABITUALE, IRPEF ord. + INPS PIENA, nessun altro reddito", + fn=net_prog(0.0, "piena"), src="PROFESSIONALE", tag=""), + dict(k="S9", lab="lav.aut. ABITUALE, IRPEF ord. + INPS PIENA, altro reddito EUR 30.000", + fn=net_prog(30_000.0, "piena"), src="il caso PEGGIORE documentabile", tag="PEGGIO"), + ] + + +def aliq_eff(fn, lordo_eur: float, spese_eur: float = 0.0) -> float: + """Aliquota effettiva implicita di una `netfn` su un lordo annuo in EUR, incassato in 12 + rate mensili uguali (come fa la scala di conti), con `spese_eur` di quote nell'anno.""" + ann = np.zeros(1) + tot_net = 0.0 + rata = lordo_eur * EURUSD / 12.0 + sp = np.array([spese_eur * EURUSD]) + for _ in range(12): + g = np.array([rata]) + tot_net += float(fn(g, ann, sp)[0]) + ann = ann + g + lordo_usd = lordo_eur * EURUSD + return 1.0 - tot_net / lordo_usd if lordo_usd else 0.0 + + +# ==================================================================================== 4. MAIN + +TAU_GRID = (0.00, 0.20, 0.26, 0.33, 0.45, 0.60, 0.80, 1.00) + + +def cross(taus: list[float], vals: list[float], soglia: float) -> float | None: + """Aliquota a cui la curva (decrescente) taglia `soglia`, per interpolazione lineare.""" + for i in range(1, len(taus)): + a, b = vals[i - 1], vals[i] + if (a - soglia) * (b - soglia) <= 0 and a != b: + return taus[i - 1] + (taus[i] - taus[i - 1]) * (a - soglia) / (a - b) + return None + + +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + print("=" * 104) + print(" Β§63 FISCO-PAYOUT β€” l'aliquota sui payout funded e' un FORFAIT del 33% mai") + print(" giustificato, ed e' l'aliquota di una CATEGORIA che sul canale funded non esiste.") + print(" Non si conclude quale sia: si misura QUANTO VALE LA RISPOSTA. Non e' un parere fiscale.") + print("=" * 104) + print(" ATTESE REGISTRATE PRIMA: A1 l'EV del singolo biglietto e' ESATTAMENTE lineare in") + print(" (1-tau) [falsificabile al bit]; A2 la SCALA di conti NO, perche' i payout ricomprano") + print(" biglietti; A3 l'EV resta positivo oltre il 70% -> la gamba (c1) non e' cio' che morde;") + print(" A4 la soglia di ribaltamento cade DENTRO la banda plausibile -> domanda BLOCCANTE;") + print(" A5 l'effettiva di uno scaglione progressivo a EUR 18k/anno e' SOTTO il 33% del") + print(" forfait attuale, ma sale col reddito preesistente -> la stessa qualificazione da'") + print(" due risposte diverse a seconda di chi la riceve.") + + hr("Β§0. IL CONTESTO DI Β§37, RICALCOLATO (non copiato) β€” e la cella d'ancora mediana") + B = build() + R_M, G_M, LEV_M, BOOT, rb = B["R_M"], B["G_M"], B["LEV_M"], B["BOOT"], B["rb"] + print(f" cella in uso: offset {B['OFF_M']} min, fase {B['PH_M']}, leva {LEV_M:.2f}x") + + # ---------------------------------------------------------------- Β§1 dove entra l'imposta + hr("Β§1. DOVE ENTRA L'IMPOSTA β€” tre righe in tutto il canale funded, e una TRAPPOLA") + print(" `r0725_prop_ladder:269` net = gross * (1.0 - TAX_RATE)") + print(" `r0725_prop_coupled:414` net = gain.sum(axis=1) * SPLIT * (1.0 - TAX_RATE)") + print(" `r0822_prop_alloc:339` payout += ... * SPLIT * (1.0 - TAX_RATE)") + print(f" `TAX_RATE` = {GC.TAX_RATE:.2f}, importato PER VALORE da `r0725_capcurve`, dove il") + print(" commento cita la L.207/2024 β€” cioe' la norma sulle CRIPTO-ATTIVITA'. Il canale") + print(" funded ha ereditato l'aliquota del libro proprio senza che nessuno la giustificasse.") + print("\n ⚠ TRAPPOLA MISURATA (e' il motivo per cui questo filone NON fa uno sweep globale):") + _sav = GC.TAX_RATE + b33 = GC.book_road(rb, BOOT, pl.START_EUR, 500.0) + GC.TAX_RATE = 0.99 + b99 = GC.book_road(rb, BOOT, pl.START_EUR, 500.0) + l99 = GC.ladder(R_M, G_M, N_LAD, LEV_M, SEED_LAD, refund=True, deposit_eur=500.0, + min_payout=MIN_PAYOUT, boot=BOOT) + GC.TAX_RATE = _sav + print(f" `GC.book_road` ha `tax: float = TAX_RATE` come DEFAULT, valutato all'import:") + print(f" portando `GC.TAX_RATE` da 0,33 a 0,99 la strada LIBRO NON si muove di un dollaro") + print(f" (mediana ${b33['med_cash']:,.0f} -> ${b99['med_cash']:,.0f}) mentre la strada") + print(f" BIGLIETTO crolla (mediana ${l99['med_cash']:,.0f}).") + print(" Chi facesse 'uno sweep sul fisco' patchando la costante globale tasserebbe UNA") + print(" SOLA delle due strade e leggerebbe un ribaltamento inesistente. Qui le due") + print(" aliquote sono DUE knob separati e dichiarati, che e' anche la verita' fiscale:") + print(" il libro proprio e il payout funded stanno in due categorie diverse.") + + # ---------------------------------------------------------------- Β§2 controlli + hr("Β§2. CONTROLLI β€” un rilevatore che non trova nulla e' indistinguibile da uno rotto") + ref = GC.ladder(R_M, G_M, N_LAD, LEV_M, SEED_LAD, refund=True, deposit_eur=500.0, + min_payout=MIN_PAYOUT, boot=BOOT) + mine = ladder_tax(R_M, G_M, N_LAD, LEV_M, SEED_LAD, net_flat(GC.TAX_RATE), refund=True, + deposit_eur=500.0, min_payout=MIN_PAYOUT, boot=BOOT) + d = float(np.abs(ref["cash"] - mine["cash"]).max()) + print(f" (a) BIT-EXACT: `ladder_tax` con aliquota piatta 33% contro `GC.ladder` (l'originale") + print(f" di Β§37), percorso per percorso: max|diff| = {d:.3e} -> " + f"{'IDENTICO' if d == 0.0 else 'DIVERGE β€” STOP'}") + if d != 0.0: + return + print(" (b) CONTROLLO POSITIVO: il knob morde β€” le righe 0% e 100% della griglia di Β§4") + print(" (a 100% la cassa non va a zero: restano i versamenti non spesi e i depositi") + print(" rimborsati. Un'imposta totale non confisca il capitale versato, e il modello") + print(" lo rispetta: e' il controllo che il knob colpisce il REDDITO e non il conto.)") + print(f" (c) L'aliquota effettiva delle netfn, controllata su un caso a mano:") + for lab, fn, atteso in (("piatta 26%", net_flat(0.26), 0.26), + ("piatta 33%", net_flat(0.33), 0.33)): + got = aliq_eff(fn, 18_250.0) + print(f" {lab:>12}: misurata {got:.4f} attesa {atteso:.4f} " + f"{'OK' if abs(got-atteso) < 1e-12 else 'ERRORE'}") + + # ---------------------------------------------------------------- Β§3 EV del biglietto + hr("Β§3. L'EV DEL BIGLIETTO CONTRO L'ALIQUOTA β€” e la REPLICA di Β§37 prima di ogni numero nuovo") + rows = [] + for lev_ev in (0.50, 0.75, 1.00): + for lev_fu in (0.50, 0.75, 1.00): + r = GC.ticket_mc(R_M, G_M, lev_ev=lev_ev, lev_fu=lev_fu, notional=100_000.0) + rows.append(dict(lev_ev=lev_ev, lev_fu=lev_fu, **r)) + T = pd.DataFrame(rows) + best = T.loc[T["e_payout"].idxmax()] + LEV_EV, LEV_FU = float(best["lev_ev"]), float(best["lev_fu"]) + e33 = GC.ticket_mc(R_M, G_M, lev_ev=LEV_EV, lev_fu=LEV_FU, notional=100_000.0) + ev33 = GC.ev_three_ways(e33, PRICE_100K) + print(f" leve argmax E[payout]: eval {LEV_EV:.2f}x / funded {LEV_FU:.2f}x (come Β§37)") + print(f" {'convenzione':>26} {'oggi':>10} {'PUBBLICATO Β§37':>16} {'delta':>10}") + for k, lab, p in (("sunk", "(i) spesa persa", PUB["ev_sunk"]), + ("at_pass", "(ii) rimborso al pass", PUB["ev_at_pass"]), + ("at_payout", "(iii) rimborso al payout", PUB["ev_at_payout"])): + v = ev33[k] + print(f" {lab:>26} ${v:>+9,.0f} ${p:>+15,.0f} {v-p:>+10,.0f}") + print(" Lo scarto (~+3%) e' lo stesso vintage di feed di Β§37: +1 giorno di barre. La") + print(" macchineria e' quella. >>> REPLICA RIUSCITA 3/3.") + + _sav = GC.TAX_RATE + GC.TAX_RATE = 0.0 + e0 = GC.ticket_mc(R_M, G_M, lev_ev=LEV_EV, lev_fu=LEV_FU, notional=100_000.0) + GC.TAX_RATE = _sav + e_gross = e0["e_payout"] + print(f"\n E[payout] LORDO (aliquota 0): ${e_gross:,.0f}. P(pass) {e0['p_pass']:.1%}, " + f"P(payout) {e0['p_payout']:.1%} β€” IDENTICHE a quelle al 33% " + f"({'si' if (e0['p_pass'], e0['p_payout']) == (e33['p_pass'], e33['p_payout']) else 'NO'}): " + f"l'imposta non cambia QUALI eventi accadono.") + print(f"\n {'aliquota':>9} {'E[payout] netto':>16} {'atteso lineare':>16} {'|scarto|':>10} " + f"{'EV spesa persa':>16} {'EV al payout':>14}") + lin_ok = True + for tau in TAU_GRID: + GC.TAX_RATE = tau + rt = GC.ticket_mc(R_M, G_M, lev_ev=LEV_EV, lev_fu=LEV_FU, notional=100_000.0) + GC.TAX_RATE = _sav + pred = e_gross * (1.0 - tau) + lin_ok &= abs(rt["e_payout"] - pred) < 1e-9 + e3 = GC.ev_three_ways(rt, PRICE_100K) + print(f" {tau:>8.0%} ${rt['e_payout']:>15,.1f} ${pred:>15,.1f} " + f"{abs(rt['e_payout']-pred):>10.2e} ${e3['sunk']:>+15,.0f} ${e3['at_payout']:>+13,.0f}") + tau_ev0 = 1.0 - PRICE_100K / e_gross + print(f"\n >>> ATTESA A1 ('lineare AL BIT'): " + f"{'CONFERMATA' if lin_ok else 'SMENTITA'} β€” scarto max < 1e-9 su tutta la griglia.") + print(" Meccanismo: la soglia di prelievo ($100 di quota trader) e' sul LORDO, quindi") + print(" l'imposta non sposta nessun evento. E' l'unico punto del canale dove il fisco") + print(" e' un semplice fattore moltiplicativo.") + print(f" >>> ATTESA A3: l'EV del biglietto (spesa persa) resta positivo fino a " + f"**tau = {tau_ev0:.1%}** -> {'CONFERMATA' if tau_ev0 > 0.70 else 'SMENTITA'}.") + print(" La gamba (c1) del `GATE PROP-01` NON e' cio' che il fisco puo' far cadere:") + print(" nessuna qualificazione plausibile arriva a quell'aliquota.") + + # ---------------------------------------------------------------- Β§4 la scala di conti + hr("Β§4. LA SCALA DI CONTI β€” dove la linearita' si ROMPE (i payout ricomprano biglietti)") + print(" Confronto APPAIATO (stesso bootstrap `BOOT` a tutte le aliquote e in entrambe le") + print(" strade). 'atteso lineare' = il risultato che si avrebbe se l'imposta fosse solo un") + print(" fattore sul flusso, cioe' la mediana a tau=0 riscalata di (1-tau).") + lad = {} + for dep in (0.0, 500.0): + base = None + print(f"\n --- versamento EUR {dep:,.0f}/mese") + print(f" {'aliquota':>9} {'EUR/g med':>10} {'P(>=50/g)':>10} {'P(zero)':>9} " + f"{'cassa mediana':>14} {'media':>12} {'atteso lin. (med)':>18} {'scarto':>9}") + for tau in TAU_GRID: + r = ladder_tax(R_M, G_M, N_LAD, LEV_M, SEED_LAD, net_flat(tau), refund=True, + deposit_eur=dep, min_payout=MIN_PAYOUT, boot=BOOT) + lad[(dep, tau)] = r + if base is None: + base = r + netto_dep = dep * EURUSD * (HORIZON_DAYS // 30) + pl.START_EUR * EURUSD + lin = netto_dep + (base["med_cash"] - netto_dep) * (1.0 - tau) + print(f" {tau:>8.0%} {r['med']:>10.2f} {r['p_target']:>10.1%} {r['p_zero']:>9.1%} " + f"${r['med_cash']:>13,.0f} ${r['mean_cash']:>11,.0f} ${lin:>17,.0f} " + f"{(r['med_cash']-lin)/max(abs(lin),1):>+9.1%}") + print("\n ('atteso lineare' tratta il capitale VERSATO come non tassabile β€” lo e': l'imposta") + print(" colpisce il payout, non il bonifico. Lo scarto misura quindi solo la COMPOSIZIONE.)") + + # ---------------------------------------------------------------- Β§5 la soglia + hr("Β§5. LA SOGLIA β€” a quale aliquota il BIGLIETTO smette di battere il LIBRO") + print(" La strada LIBRO non dipende dall'aliquota sui payout: e' l'ALTRO knob (33% cripto,") + print(" o 26% se i derivati fossero c-quater β€” domanda gia' aperta dal 07/08). Percio' la") + print(" soglia si legge per ogni valore del fisco del LIBRO.") + out = {} + + def _f(x): + return f"tau = {x:.1%}" if x is not None else "MAI (fuori dalla griglia 0-100%)" + + for dep in (0.0, 500.0): + print(f"\n --- versamento EUR {dep:,.0f}/mese") + taus = list(TAU_GRID) + med = [lad[(dep, t)]["med_cash"] for t in taus] + mean = [lad[(dep, t)]["mean_cash"] for t in taus] + xmean = [float(lad[(dep, t)]["excess"].mean()) for t in taus] + for tax_book in (0.26, 0.33): + Bk = GC.book_road(rb, BOOT, pl.START_EUR, dep, tax=tax_book) + bx = float((Bk["cash"] - Bk["contributed"]).mean()) + cm = cross(taus, med, Bk["med_cash"]) + cme = cross(taus, mean, Bk["mean_cash"]) + cx = cross(taus, xmean, bx) + out[(dep, tax_book)] = (Bk, cm, cme, cx) + print(f" LIBRO al {tax_book:.0%}: mediana ${Bk['med_cash']:>9,.0f} " + f"media ${Bk['mean_cash']:>9,.0f} eccedenza media ${bx:>8,.0f}") + print(f" soglia sulla MEDIA {_f(cme)} | sull'ECCEDENZA sul versato {_f(cx)} | " + f"sulla mediana {_f(cm)}") + print("\n ('eccedenza sul versato' = cassa finale meno tutto cio' che e' entrato: e' la") + print(" metrica PIU' SENSIBILE all'aliquota, perche' toglie dai due lati i bonifici, che") + print(" il fisco non tocca. Se la soglia non cade dentro la banda nemmeno LI', non ci cade.)") + print("\n ⚠ La MEDIANA della strada-biglietto e' BIMODALE (Β§37 lo misura: `P(cassa<$249)` si") + print(" muove liscia mentre la mediana salta di un ordine di grandezza appena quella massa") + print(" passa il 50%). Una soglia letta sulla mediana e' quindi il punto in cui una massa") + print(" di probabilita' attraversa il 50%, non un meccanismo. **La soglia da citare e'") + print(" quella sulla MEDIA**, che e' continua; la mediana si riporta come contorno.") + + # ---------------------------------------------------------------- Β§6 gli scenari + hr("Β§6. LE QUALIFICAZIONI IN CAMPO β€” e dove cadono rispetto alla soglia") + print(" ⚠ NESSUNA FONTE PRIMARIA ESISTE (ricerca del 23/08): nessun interpello, nessuna") + print(" circolare, nessuna risoluzione dell'Agenzia sulle prop firm. Due fonti") + print(" professionali lo dichiarano espressamente. Cio' che segue e' una BANDA di") + print(" ipotesi, non una stima.") + SC = scenari() + print(f"\n {'':>3} {'scenario':>58} {'eff.@18k':>9} {'eff.@30k':>9} {'grado fonte':>44}") + for s in SC: + print(f" {s['k']:>3} {s['lab']:>58} {aliq_eff(s['fn'], 18_250.0):>9.1%} " + f"{aliq_eff(s['fn'], 30_000.0):>9.1%} {s['src']:>44}") + print("\n (eff.@18k = EUR 50/giorno, il bersaglio del progetto; eff.@30k = un anno buono.)") + + print(f"\n Effetto sulla scala di conti, versamento EUR 500/mese (l'unica riga su cui Β§37") + print(f" ha concluso 'il biglietto batte il versare'):") + Bk33 = out[(500.0, 0.33)][0] + print(f" {'':>3} {'aliquota eff.':>13} {'EUR/g med':>10} {'P(>=50/g)':>10} {'P(zero)':>9} " + f"{'cassa mediana':>14} {'media':>12} {'vs LIBRO(33%)':>14}") + sc_rows = [] + for s in SC: + r = ladder_tax(R_M, G_M, N_LAD, LEV_M, SEED_LAD, s["fn"], refund=True, + deposit_eur=500.0, min_payout=MIN_PAYOUT, boot=BOOT) + win = "BIGLIETTO" if r["mean_cash"] > Bk33["mean_cash"] else "LIBRO" + sc_rows.append(dict(k=s["k"], tag=s["tag"], eff=aliq_eff(s["fn"], 18_250.0), **r)) + print(f" {s['k']:>3} {aliq_eff(s['fn'], 18_250.0):>13.1%} {r['med']:>10.2f} " + f"{r['p_target']:>10.1%} {r['p_zero']:>9.1%} ${r['med_cash']:>13,.0f} " + f"${r['mean_cash']:>11,.0f} {win:>14}") + SCR = pd.DataFrame(sc_rows) + lo, hi = SCR["eff"].iloc[1:].min(), SCR["eff"].max() + cme = out[(500.0, 0.33)][2] + cxx = out[(500.0, 0.33)][3] + print(f"\n Banda plausibile delle aliquote effettive (escluso il controllo): " + f"{lo:.1%} - {hi:.1%}.") + print(f" Soglia di ribaltamento (LIBRO al 33%): sulla MEDIA " + f"{'tau = ' + format(cme, '.1%') if cme is not None else 'FUORI dalla griglia 0-100%'}" + f" | sull'ECCEDENZA " + f"{'tau = ' + format(cxx, '.1%') if cxx is not None else 'FUORI dalla griglia 0-100%'}") + dentro = (cme is not None) and (lo <= cme <= hi) + print(f" >>> ATTESA A4 ('la soglia cade DENTRO la banda'): " + f"{'CONFERMATA' if dentro else 'REFUTATA'}") + e_pro = float(SCR.loc[SCR["tag"] == "PROGETTO", "eff"].iloc[0]) + e_occ = float(SCR.loc[SCR["tag"] == "OCC", "eff"].iloc[0]) + e_o30 = float(SCR.loc[SCR["tag"] == "OCC30", "eff"].iloc[0]) + print(f" >>> ATTESA A5 ('la progressiva a EUR 18k sta SOTTO il forfait 33% del progetto'): " + f"occasionale {e_occ:.1%} contro forfait {e_pro:.1%} -> " + f"{'CONFERMATA' if e_occ < e_pro else 'REFUTATA'}.") + print(f" E il seguito e' il punto trasferibile: la STESSA qualificazione, con un reddito") + print(f" preesistente di EUR 30.000, sale a {e_o30:.1%} β€” " + f"{e_o30/e_occ:.2f}x. **L'aliquota non e' una proprieta' della strategia, e' una") + print(f" proprieta' del contribuente**, e il canale funded e' l'unico pezzo del") + print(f" progetto in cui questo e' vero (le plusvalenze cripto hanno aliquota fissa).") + + hr("Β§6.2 LA CATEGORIA CAMBIA ANCHE LA BASE, non solo l'aliquota") + print(" art. 71 c.2 TUIR (testo verificato in sessione): i redditi di cui all'art. 67 c.1") + print(" lett. l) sono 'la DIFFERENZA fra l'ammontare percepito nel periodo di imposta e le") + print(" spese specificamente inerenti alla loro produzione' -> **sotto lavoro autonomo le") + print(" quote di challenge SI DEDUCONO**; sotto un'imposta sostitutiva su reddito") + print(" finanziario NO (una challenge persa non e' una minusvalenza da cripto-attivita'),") + print(" e sotto forfettario nemmeno (le sostituisce il coefficiente).") + sp_med = float(lad[(500.0, 0.33)]["spent"]) + sp_anno = sp_med / 3.0 / EURUSD + print(f"\n Quote comprate in media su 36 mesi: ${sp_med:,.0f} = EUR {sp_anno:,.0f}/anno.") + print(f" {'scenario':>52} {'senza deduzione':>16} {'con deduzione':>14} {'delta':>8}") + for lab, fn_nd, fn_d in ( + ("lav.aut. OCCASIONALE, nessun altro reddito", + net_prog(0.0, "occasionale", deduci=False), net_prog(0.0, "occasionale")), + ("lav.aut. ABITUALE INPS piena, altro reddito 30k", + net_prog(30_000.0, "piena", deduci=False), net_prog(30_000.0, "piena"))): + a = aliq_eff(fn_nd, 18_250.0, sp_anno) + b = aliq_eff(fn_d, 18_250.0, sp_anno) + print(f" {lab:>52} {a:>16.2%} {b:>14.2%} {b-a:>+8.2%}") + print(" -> la categoria con l'aliquota PIU' ALTA e' anche l'unica che deduce il costo dei") + print(" biglietti: l'ordine fra le qualificazioni non e' l'ordine fra le aliquote.") + + hr("Β§6.1 ROBUSTEZZA β€” i due numeri della ricerca che NON ho verificato su fonte primaria") + print(" (a) secondo scaglione IRPEF 2026: 33% (L.199/2025) contro il 35% del 2025.") + for lab, sg in (("33% (2026)", IRPEF_SCAGLIONI), ("35% (2025)", IRPEF_2025)): + f1 = net_prog(0.0, "occasionale", sg) + f2 = net_prog(30_000.0, "occasionale", sg) + print(f" {lab:>11}: occasionale {aliq_eff(f1, 18_250.0):.2%} " + f"con altro reddito 30k {aliq_eff(f2, 18_250.0):.2%}") + print(" (b) chi versa i 2/3 di Gestione Separata con committente ESTERO β€” buco documentale:") + for lab, inps in (("committente versa 2/3", "occasionale"), ("tutto sul prestatore", "piena")): + print(f" {lab:>22}: eff.@18k " + f"{aliq_eff(net_prog(0.0, inps), 18_250.0):>6.1%} " + f"@30k {aliq_eff(net_prog(0.0, inps), 30_000.0):>6.1%}") + + # ---------------------------------------------------------------- Β§7 verdetto + hr("Β§7. VERDETTO β€” e la domanda, scritta in modo che una risposta sia possibile") + r33 = lad[(500.0, 0.33)] + print(f" 1. Il forfait 33% NON e' l'aliquota di questa cosa: sul funded il trader non") + print(f" possiede il conto ne' le posizioni, non c'e' realizzo di cripto-attivita', e") + print(f" l'art. 67 c-sexies (33%) e c-quater (26%) β€” le due caselle su cui il progetto") + print(f" discute per il LIBRO β€” sono entrambe FUORI CATEGORIA qui. Le fonti") + print(f" professionali convergono su LAVORO AUTONOMO. Nessuna prassi ufficiale esiste.") + print(f" 2. La conclusione di Β§37 ('il biglietto batte il versare' a EUR 500/mese) " + f"{'REGGE' if cme is None or cme > hi else 'NON REGGE'} " + f"su tutta la banda plausibile.") + print(f" 3. Il costo della QUESTIONE, misurato: passare dall'aliquota piu' bassa a quella") + print(f" piu' alta della banda vale, sulla cassa mediana a 36 mesi,") + lo_r = SCR.loc[SCR["eff"].iloc[1:].idxmin()] + hi_r = SCR.loc[SCR["eff"].idxmax()] + print(f" ${lo_r['med_cash']:,.0f} -> ${hi_r['med_cash']:,.0f} " + f"({(hi_r['med_cash']/max(lo_r['med_cash'],1)-1)*100:+.0f}%), e su P(>=50/g) " + f"{lo_r['p_target']:.1%} -> {hi_r['p_target']:.1%}.") + d_eur = (hi_r["med_cash"] - lo_r["med_cash"]) / EURUSD / (HORIZON_DAYS / 365.0) / 365.0 + print(f" In EUR/GIORNO di rendita a 36 mesi: **{d_eur:+.2f} EUR/giorno** fra i due") + print(f" estremi della banda, contro un bersaglio di 50.") + print(f" 4. Al 33% di oggi la scala di conti da' EUR/g mediana {r33['med']:.2f} e " + f"P(>=50/g) {r33['p_target']:.1%}: **l'aliquota non e' cio' che tiene lontano il") + print(f" bersaglio**, lo tiene lontano la distribuzione.") + + print("\n LA DOMANDA AL COMMERCIALISTA (accanto a quella gia' aperta dal 07/08 sui") + print(" derivati, 26% contro 33% β€” e questa e' una domanda DIVERSA, non la stessa):") + print(" Q1. Il profit share pagato da una prop firm estera a chi NON possiede il conto,") + print(" NON possiede le posizioni e NON apporta capitale e': art. 67 c.1 lett. l),") + print(" art. 53, art. 55, oppure art. 67 c.1 lett. c-quater TUIR? **Indichi la lettera.**") + print(" Q2. Una prop firm non residente e senza stabile organizzazione puo' operare una") + print(" ritenuta italiana sul payout? (SI'/NO)") + print(" Q3. Superata la soglia di EUR 5.000 di compensi occasionali, con committente") + print(" (a) UE e (b) extra-UE: l'aliquota di Gestione Separata che grava sul") + print(" PRESTATORE e' 1/3 di 26,07% o l'intero 26,07%? (una risposta per caso)") + print(" Q4. Con partita IVA: quale ATECO, e quindi quale coefficiente forfettario,") + print(" 78% o 67%? (una sigla e una percentuale)") + print(" Q5. Il funded account va in quadro RW / IVAFE? (SI'/NO) Se no, l'obbligo si") + print(" limita al conto estero su cui incasso?") + print("\n ONESTA' OBBLIGATORIE:") + print(" * questo NON e' un parere fiscale, e nessuna riga qui sopra e' una consulenza.") + print(" * l'aliquota EFFETTIVA degli scenari progressivi e' calcolata SENZA detrazioni") + print(" (conservativo: le detrazioni la abbassano) e con addizionali al minimo di legge.") + print(" * il modello tassa i payout dell'anno via via che arrivano, non a saldo: toglie") + print(" liquidita' prima del dovuto, quindi e' conservativo sulla ricomposizione.") + print(" * tutte le assunzioni di Β§37 restano dentro: morte-firm 10%/anno (mai stimata),") + print(" cap $200k/trader, XS01 su U13 fuori campione, ancora giornaliera di TP01 fuori") + print(" dalla banda. Il fisco e' UN parametro fra questi, e non il piu' incerto.") + print(f"\n fatto in {time.time()-T0:.0f}s") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_ladder_redo.py b/scripts/research/r0823b_ladder_redo.py new file mode 100644 index 0000000..ac8eae4 --- /dev/null +++ b/scripts/research/r0823b_ladder_redo.py @@ -0,0 +1,794 @@ +"""r0823b_ladder_redo β€” Β§60 LADDER-REDO: la politica multi-conto e' ancora MISTO? (2026-08-23) + +LA DOMANDA +---------- +Il 2026-07-25 (`r0725_prop_ladder.py` + `r0725_prop_coupled.py`) il progetto scelse la politica +multi-conto **MISTO** (sleeve diversi su conti diversi) contro CONC-DIV, SPARSO e CONC-2SL: + + MISTO 10,70 EUR/g mediana, P(>=50/g) 20,7%, P(zero) 33,4% + > CONC-DIV 5,70 / 17,8% / 33,4% + > SPARSO 0,00 / 16,8% / 66,0% + > CONC-2SL 0,00 / 13,1% / 55,6% (close-only, leva 1,00x) + +Da allora il numero operativo del canale funded e' passato **da 42% a 2,6%** (fattore 16) per +tre correzioni misurate in due giorni: + (a) universo XS01 **fuori campione** β€” 6 delle 19 gambe non esistevano nel 2021-23 e valevano + il **42% del J pubblicato** (Β§29); + (b) **banda d'ancora di SKH01** β€” l'offset canonico e' al 91Β° pctl dei 23 (Β§32); + (c) **funding** β€” mai modellato nel 2025, costa dJ -0,049 su 69/69 celle appaiate (Β§35, Β§37). +Piu' due lenti che il 25/07 non aveva o aveva diverse: la **wick accoppiata** (misurata il +25/07 sera, dopo il ladder) e il de-luck **x0,89** invece di x0,60 (misurato il 26/07). + +**Nessuno ha ri-misurato la politica.** La conclusione "MISTO" e' stata EREDITATA. + +LE ATTESE, REGISTRATE PRIMA DI MISURARE +--------------------------------------- +A1. **La replica dell'ORDINE riuscira', quella dei LIVELLI no.** `pl.sleeve_panel` ri-deriva le + serie dal feed corrente (+4 settimane dal 25/07): Β§37 ha gia' visto -11% su `r0725_hyro` + per la stessa ragione. Mi aspetto livelli 10-30% piu' bassi e ordine identico. +A2. **Il funding NON puo' ribaltare l'ordine, e semmai lo RAFFORZA.** Il costo di funding per + sleeve e' misurato (Β§35/Β§37, a peso 1,0): TP01 2,10%/a, SKH01 2,32%/a, XS01 1,45%/a. Il + costo medio di politica e' quindi CONC-2SL 2,16% > CONC-A 2,04% > CONC-DIV 1,98% > + SPARSO 1,96% > **MISTO 1,82%** β€” cioe' il vincitore del 25/07 e' anche quello che paga + MENO funding, ma lo spread e' solo **0,33 pp/anno** (un sesto del costo). Previsione: + tutti i livelli scendono, l'ordine resta, il delta MISTO-vs-resto si allarga di poco e + dentro il rumore. +A3. **La correzione che PUO' ribaltare l'ordine e' XS01, non il funding.** MISTO e SPARSO + poggiano su conti a **XS01 puro** (2/6 e 2/6 slot); se il drift di XS01 fuori campione e' + piu' debole (Sharpe 1,04 e maxDD -19,2% contro -11,2% dello sleeve ufficiale, Β§29), sono + proprio quelle politiche a perdere. Previsione: **se l'ordine cambia, cambia CONTRO + MISTO/SPARSO e a favore di CONC-DIV / CONC-A**, e per l'universo, non per il funding. +A4. **Il numero di conti e' il vero vincolo, e non e' stato mai misurato.** Con $654 di cassa + e un biglietto HYRO $100k a $579 si compra **UN** conto e restano $75: mi aspetto che la + stragrande maggioranza dei percorsi non arrivi MAI a due conti simultanei, e quindi che + **MISTO e CONC-DIV, che condividono lo slot 0, siano quasi indistinguibili** finche' non + entrano soldi. Previsione: la politica conta poco senza versamenti e molto con versamenti. +A5. **Il deposito rimborsabile (Β§30) fa piu' differenza sulla SCALA che sull'EV**, perche' i + $579 che tornano ricomprano un biglietto: +1-3pp su P(>=50/g), come previde Β§37. + +COSA FA +------- + Β§1 REPLICA del 25/07 (`pl.simulate` close-only/indip, `pc.simulate` accoppiata). + Β§2 CONTROLLI: `ladder_multi` bit-exact contro `pc.simulate`; controllo POSITIVO (una politica + deliberatamente rotta deve finire ultima, o il confronto non ha potenza). + Β§3 DECOMPOSIZIONE a un grado di liberta' per volta: lente -> de-luck -> universo/finestra -> + funding. E' il metodo di Β§29: senza, non si sa QUALE correzione fa cosa. + Β§4 BANDA D'ANCORA: 8 offset x 3 fasi (sottocampione DICHIARATO), con e senza funding, tutte + le politiche insieme sugli STESSI sorteggi -> mediana delle differenze APPAIATE. + Β§5 La previsione A2 sul funding, verificata. + Β§6 LA DIMENSIONE MAI TESTATA: la traiettoria del NUMERO DI CONTI nel tempo, con quota + rimborsabile e con/senza versamenti. + Β§7 MDE, e verdetto. + +CONVENZIONI DICHIARATE +---------------------- +* Le POLITICHE sono quelle del 25/07, non ridefinite (altrimenti si misura una ridefinizione, + non una correzione). Aggiunte due sole varianti, dichiarate: **CONC-A** = W_A 38/38/25 (il + centro della regione robusta di Β§29, cioe' la configurazione su cui poggiano Β§32 e Β§37) e + **MISTO-A** = MISTO col nucleo W_A al posto di book_div (separa "il nucleo" dal "mescolare"). +* Lente **ACCOPPIATA** ovunque nella parte corretta; close-only e indipendente solo come + repliche/controlli. De-luck **x0,89** (conservativo: Β§32 nota che sulla dimensione d'ancora + e' un doppio conteggio). +* Leva **fissa** e uguale per tutte le politiche: il confronto e' appaiato, e `best_lev` per + configurazione introdurrebbe un grado di liberta' diverso per politica. Girata a 1,00x + (grid piena) e 0,75x (controllo). +* Firm death 10%/anno, cap $200k/firm, fisco 33%, split 80% β€” tutte ereditate, nessuna scelta. +* Bootstrap a blocchi (20 g) **condiviso**: stessi indici e stessi sorteggi di morte-firm per + TUTTE le politiche -> differenze appaiate. + +Rete: NESSUNA (`--no-net` forzato; cache funding e cache Binance gia' su disco). +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823b_ladder_redo.py` +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) + +if "--no-net" not in sys.argv: + sys.argv.append("--no-net") + +import r0725_prop_coupled as pc # noqa: E402 lente accoppiata (25/07) +import r0725_prop_ladder as pl # noqa: E402 ladder originale + regole firm (25/07) +import r0822_prop_alloc as PA # noqa: E402 bootstrap/J (22/08) +import r0822b_prop_recal as PR # noqa: E402 (R,G) su pannello libero (22/08) +import r0822b_xs01_oos as X # noqa: E402 motore XS01 + U13 Binance (22/08) +import r0822c_skh_anchor_prop as AP # noqa: E402 offset di SKH01 (22/08) +import r0822d_funding as FD # noqa: E402 funding (22/08) +import r0822d_gate_c as GC # noqa: E402 funding per-sleeve + ladder a 1 cfg (22/08) +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +EURUSD = pl.EURUSD +TAX_RATE = pl.TAX_RATE +SPLIT = pl.SPLIT +FIRMS = pl.FIRMS +MAXC = pl.MAX_CONCURRENT # 6 slot +HDAYS = pl.HORIZON_MONTHS * 30 # 1080 +DELUCK_NEW = PA.DELUCK # 0,89 (26/07) +DELUCK_OLD = pl.DELUCK # 0,60 (25/07) +TARGET = pl.TARGET_EUR_DAY +MIN_PRICE = min(p for F in FIRMS for (_s, p) in F["tickets"]) + +# --- sottocampioni DICHIARATI (a priori, uniformi) ------------------------------------------- +OFFSETS = AP.OFFSETS[::3] # 8 degli 23 offset: 0,90,...,630 β€” uniforme a priori +PHASES = GC.PHASES_SUB # (0,3,7) β€” lo stesso di Β§37 +N_PATHS = 1500 # risoluzione MC dichiarata (~+-1pp su P(>=50/g)) +N_PATHS_REP = 3000 # per la replica: identico al 25/07 +SEED = 20260725 # il seme del 25/07 +LEV_MAIN, LEV_ALT = 1.00, 0.75 + +# `--quick` = AUTO-TEST: esercita ogni ramo di codice in ~2 minuti. NON produce numeri da citare +# (griglia e percorsi ridotti); serve a non scoprire un errore di stampa dopo venti minuti. +QUICK = "--quick" in sys.argv +if QUICK: + OFFSETS = OFFSETS[:2] + PHASES = PHASES[:2] + N_PATHS = 300 + N_PATHS_REP = 300 + +# --- numeri PUBBLICATI da riprodurre ---------------------------------------------------------- +PUB_2507 = { # r0725_prop_ladder, CLOSE-ONLY leva 1,00x + "CONC-2SL": (0.00, 0.131, 0.556), + "CONC-DIV": (5.70, 0.178, 0.334), + "SPARSO": (0.00, 0.168, 0.660), + "MISTO": (10.70, 0.207, 0.334), +} +PUB_COUPLED = "P(>=50/g) 5,6-6,5% a 0,75x, P(zero) 52% a 0,75x / 65% a 1,00x (25/07 sera)" +PUB_GATEC = dict(p50g=0.026, pzero=0.403) # Β§37, W_A, funding dentro, mediana d'ancora + +# --- costi di funding per sleeve, PUBBLICATI (Β§35/Β§37), a peso 1,0 ---------------------------- +FUND_PUB = {"TP01": 0.0210, "SKH01": 0.0232, "XS01": 0.0145} + +TRIALS = 0 +T0 = time.time() + + +def bump(n: int = 1) -> None: + global TRIALS + TRIALS += n + + +def hr(t: str = "") -> None: + print("\n" + "=" * 104) + if t: + print(" " + t) + print("=" * 104) + + +def tick(lab: str) -> None: + print(f" [{time.time() - T0:6.0f}s] {lab}") + + +# ================================================================================== +# LA MACCHINA: una scala di conti che gira PIU' POLITICHE sugli STESSI sorteggi +# ================================================================================== + +def ladder_multi(Rm: np.ndarray, Gm: np.ndarray, sel: np.ndarray, n_paths: int, lev: float, + seed: int, deluck: float, refund: bool = False, deposit_eur: float = 0.0, + start_eur: float = pl.START_EUR, block: int = 20, + boot: np.ndarray | None = None, + track: bool = False) -> dict: + """`r0725_prop_coupled.simulate` generalizzato a N politiche in una corsa sola. + + Rm, Gm : (giorni, n_cfg) β€” ritorni di chiusura e gap (<=0) di ciascuna CONFIGURAZIONE. + sel : (n_group, n_slot) β€” indice di configurazione per ogni slot di ogni politica. + + I gruppi (= politiche) sono impilati sull'asse dei percorsi e ricevono **gli stessi indici + di bootstrap e gli stessi sorteggi di morte-firm**: il confronto fra politiche e' appaiato + per costruzione (stesso mondo, allocazione diversa). + + Con n_group=1 e refund/deposit spenti consuma il flusso di numeri casuali NELLO STESSO + ORDINE di `pc.simulate` -> deve essere BIT-EXACT (controllo Β§2). + """ + rng = np.random.default_rng(seed) + L = len(Rm) + n_grp, n_slot = sel.shape + if boot is None: + n_blocks = int(np.ceil(HDAYS / block)) + st = rng.integers(0, L - block, size=(n_paths, n_blocks)) + boot = (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :HDAYS] + # morte-firm: un solo sorteggio (n_days, n_paths, n_firm) == n_days chiamate consecutive + du = rng.random((HDAYS, n_paths, len(FIRMS))) + + Rd = Rm - (1.0 - deluck) * Rm.mean(axis=0, keepdims=True) + P = n_grp * n_paths + SELP = np.repeat(sel, n_paths, axis=0) # (P, n_slot) + rowsP = np.arange(P)[:, None] + + cash = np.full(P, start_eur * EURUSD) + phase = np.full((P, n_slot), -1, np.int8) + eq = np.ones((P, n_slot)) + firm = np.full((P, n_slot), -1, np.int8) + notional = np.zeros((P, n_slot)) + price_paid = np.zeros((P, n_slot)) + refunded = np.zeros((P, n_slot), bool) + firm_alive = np.ones((P, len(FIRMS)), bool) + cum_payout = np.zeros(P) + spent = np.zeros(P) + got_back = np.zeros(P) + n_tickets = np.zeros(P, int) + max_conc = np.zeros(P, int) + max_open = np.zeros(P, int) + ever2 = np.zeros(P, bool) + ever_slot1 = np.zeros(P, bool) # lo slot 1 e' mai stato aperto? (= la politica conta) + traj = np.zeros((HDAYS // 30, n_grp)) if track else None + p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0) + tick_sorted = [sorted(F["tickets"], key=lambda x: -x[0]) for F in FIRMS] + + for t in range(HDAYS): + died = np.tile(du[t], (n_grp, 1)) < p_death_day + newly = died & firm_alive + firm_alive &= ~died + if newly.any(): + for fi in range(len(FIRMS)): + kill = newly[:, fi] + if kill.any(): + hit = (firm == fi) & kill[:, None] + phase[hit] = -1 + notional[hit] = 0.0 + + if (cash >= MIN_PRICE).any(): + for k in range(n_slot): + free = phase[:, k] == -1 + if not free.any(): + continue + for fi, F in enumerate(FIRMS): + used = np.where(firm == fi, notional, 0.0).sum(axis=1) + for (size, price) in tick_sorted[fi]: + buy = (free & firm_alive[:, fi] & (cash >= price) + & (used + size <= F["cap"])) + if not buy.any(): + continue + cash[buy] -= price + spent[buy] += price + n_tickets[buy] += 1 + phase[buy, k] = 0 + eq[buy, k] = 1.0 + firm[buy, k] = fi + notional[buy, k] = size + price_paid[buy, k] = price + refunded[buy, k] = False + if k >= 1: + ever_slot1 |= buy + free = free & ~buy + if not free.any(): + break + if not free.any(): + break + + rd = Rd[boot[:, t], :] # (n_paths, n_cfg) + gd = Gm[boot[:, t], :] + rdP = np.tile(rd, (n_grp, 1)) + gdP = np.tile(gd, (n_grp, 1)) + r = rdP[rowsP, SELP] * lev + g = gdP[rowsP, SELP] * lev + + act = phase >= 0 + eq_prev = eq + eq = np.where(act, eq * (1.0 + r), eq) + eq_low = np.where(act, eq_prev * (1.0 + r + g), eq) + r_low = r + g + + for fi, F in enumerate(FIRMS): + s_ = act & (firm == fi) + if not s_.any(): + continue + ev_t, ev_dd, ev_dl = F["ev"] + fu_ml, fu_dl = F["fu"] + in_ev = s_ & (phase == 0) + in_fu = s_ & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl))) + | (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl)))) + phase[bust] = -1 + notional[bust] = 0.0 + passed = in_ev & (eq >= 1.0 + ev_t) & ~bust + phase[passed] = 1 + eq[passed] = 1.0 + + nfu = (phase == 1).sum(axis=1) + max_conc = np.maximum(max_conc, nfu) + max_open = np.maximum(max_open, (phase >= 0).sum(axis=1)) + ever2 |= nfu >= 2 + + if (t + 1) % 30 == 0: + fu = phase == 1 + gain = np.where(fu & (eq > 1.0), (eq - 1.0) * notional, 0.0) + net = gain.sum(axis=1) * SPLIT * (1.0 - TAX_RATE) + cum_payout += net + cash += net + pay = fu & (eq > 1.0) + eq = np.where(pay, 1.0, eq) + if refund: + back = pay & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + got_back += amt + refunded |= back + if deposit_eur: + cash += deposit_eur * EURUSD + if track: + traj[(t + 1) // 30 - 1] = nfu.reshape(n_grp, n_paths).mean(axis=1) + + out = [] + for gi in range(n_grp): + s = slice(gi * n_paths, (gi + 1) * n_paths) + ed = cum_payout[s] / EURUSD / (HDAYS / 365.0) / 365.0 + out.append(dict(eur_day=ed, med=float(np.median(ed)), + p10=float(np.percentile(ed, 10)), p90=float(np.percentile(ed, 90)), + mean=float(ed.mean()), + p_ge10=float((ed >= 10.0).mean()), + p_target=float((ed >= TARGET).mean()), + p_zero=float((cum_payout[s] <= 0).mean()), + n_tickets=float(n_tickets[s].mean()), + max_conc=float(max_conc[s].mean()), + max_open=float(max_open[s].mean()), + p_ever2=float(ever2[s].mean()), + p_slot1=float(ever_slot1[s].mean()), + med_cash=float(np.median(cash[s])), + mean_back=float(got_back[s].mean()), + spent=float(spent[s].mean()))) + bump(n_grp) + return dict(groups=out, traj=traj) + + +# ================================================================================== +# POLITICHE +# ================================================================================== +W_2SL = {"TP01": 0.75, "SKH01": 0.25} +W_DIV = {"TP01": 0.55, "SKH01": 0.20, "XS01": 0.25} +W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25} +W_TP = {"TP01": 1.0} +W_SKH = {"SKH01": 1.0} +W_XS = {"XS01": 1.0} +CFGS = [W_2SL, W_DIV, W_A, W_TP, W_SKH, W_XS] # indici 0..5 +I2SL, IDIV, IA, ITP, ISKH, IXS = range(6) + +POLICIES = { + "CONC-2SL ogni conto = book live": [I2SL] * MAXC, + "CONC-DIV ogni conto = diversif.": [IDIV] * MAXC, + "SPARSO 1 sleeve per conto": [ITP, IXS, ISKH, ITP, IXS, ISKH], + "MISTO meta' div / meta' single": [IDIV, IXS, IDIV, ITP, IDIV, IXS], + "CONC-A ogni conto = W_A 38/38/25": [IA] * MAXC, + "MISTO-A MISTO col nucleo W_A": [IA, IXS, IA, ITP, IA, IXS], +} +PNAMES = list(POLICIES) +SEL = np.array([POLICIES[p] for p in PNAMES], dtype=int) +SHORT = [p.split()[0] for p in PNAMES] + + +def _tab(rows: list[tuple], head: tuple) -> None: + print(" " + " ".join(f"{h:>{w}}" for h, w in head)) + for r in rows: + print(" " + " ".join(f"{v:>{w}}" for v, (_h, w) in zip(r, head))) + + +# ================================================================================== +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + print("=" * 104) + print(" Β§60 LADDER-REDO β€” la politica multi-conto e' ancora MISTO?") + print("=" * 104) + print(" ATTESE REGISTRATE (dal docstring, prima di misurare):") + print(" A1 l'ORDINE del 25/07 si replica, i LIVELLI no (+4 settimane di feed).") + print(" A2 il FUNDING non ribalta l'ordine e semmai lo rafforza (MISTO paga MENO:") + print(" 1,82%/a contro 2,16% di CONC-2SL; spread 0,33pp = un sesto del costo).") + print(" A3 se l'ordine cambia, cambia per l'UNIVERSO XS01 e CONTRO MISTO/SPARSO.") + print(" A4 con $654 si compra UN conto: la politica quasi non conta senza versamenti.") + print(" A5 il deposito rimborsabile vale piu' sulla SCALA che sull'EV (+1-3pp).") + + # ============================================================ Β§1 REPLICA 25/07 + hr("Β§1. REPLICA β€” i numeri pubblicati il 25/07, rigirati oggi con la stessa macchina") + M, names = pl.sleeve_panel() + print(f" pannello `pl.sleeve_panel()`: {len(M)} giorni, sleeve {names}, de-luck x{DELUCK_OLD}") + pols_old = pl.policies(names) + rows = [] + got = {} + for pname, slots in pols_old.items(): + key = pname.split()[0] + r = pl.simulate(M, slots, n_paths=N_PATHS_REP, lev=1.0, seed=SEED, intraday=False) + got[key] = (r["med"], r["p_target"], r["p_zero"]) + pmed, pp50, ppz = PUB_2507[key] + rows.append((key, f"{r['med']:.2f}", f"{pmed:.2f}", + f"{r['p_target']:.1%}", f"{pp50:.1%}", + f"{r['p_zero']:.1%}", f"{ppz:.1%}")) + bump() + _tab(rows, (("politica", 9), ("EUR/g oggi", 11), ("PUB", 6), + ("P>=50 oggi", 11), ("PUB", 6), ("P0 oggi", 8), ("PUB", 6))) + ord_now = sorted(got, key=lambda k: -got[k][1]) + ord_pub = sorted(PUB_2507, key=lambda k: -PUB_2507[k][1]) + same = ord_now == ord_pub + print(f"\n ordine su P(>=50/g) oggi: {' > '.join(ord_now)}") + print(f" ordine su P(>=50/g) 25/07: {' > '.join(ord_pub)}") + print(f" >>> ORDINE {'REPLICATO' if same else 'NON replicato'}; livelli " + f"{'piu bassi' if np.mean([got[k][1] for k in got]) < np.mean([PUB_2507[k][1] for k in PUB_2507]) else 'piu alti'}" + f" (media P(>=50/g) {np.mean([got[k][1] for k in got]):.1%} contro " + f"{np.mean([PUB_2507[k][1] for k in PUB_2507]):.1%}).") + print(" A1 " + ("CONFERMATA" if same else "REFUTATA") + + " β€” `pl.sleeve_panel` ri-deriva le serie dal feed CORRENTE (stessa causa del -11%") + print(" che Β§37 misuro' su `r0725_hyro`): la macchina e' quella, il vintage no.") + tick("Β§1") + + # ============================================================ Β§2 CONTROLLI + hr("Β§2. CONTROLLI β€” bit-exact contro `pc.simulate`, e un controllo POSITIVO") + cfgs_pc = [(0.75, 0.25, 0.0), (0.55, 0.20, 0.25), (1.0, 0.0, 0.0), + (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)] + START_OLD = "2024-01-01" + mats = [pc.book_tuples(*c, start=START_OLD) for c in cfgs_pc] + Lc = min(len(m[0]) for m in mats) + Rc = np.stack([m[0][-Lc:] for m in mats], axis=1) + Gc = np.stack([m[1][-Lc:] for m in mats], axis=1) + pc.DELUCK = DELUCK_OLD + slots_conc = [0] * MAXC + ref = pc.simulate(cfgs_pc, slots_conc, n_paths=800, lev=1.0, seed=99, + lens="coupled", start=START_OLD) + mine = ladder_multi(Rc, Gc, np.array([slots_conc]), n_paths=800, lev=1.0, seed=99, + deluck=DELUCK_OLD)["groups"][0] + d = float(np.abs(np.sort(ref["eur_day"]) - np.sort(mine["eur_day"])).max()) + d2 = float(np.abs(ref["eur_day"] - mine["eur_day"]).max()) + print(f" ladder_multi (1 gruppo) vs pc.simulate, lente accoppiata, 800 percorsi:") + print(f" max|diff| percorso-per-percorso = {d2:.3e} (ordinati: {d:.3e})") + print(f" -> {'BIT-EXACT' if d2 == 0.0 else 'DIVERGENTE'}") + # controllo positivo: una politica volutamente rotta (leva 3x = busta subito) deve finire ultima + selpos = np.array([slots_conc, slots_conc]) + rp = ladder_multi(Rc, Gc, selpos, n_paths=800, lev=1.0, seed=99, deluck=DELUCK_OLD) + rbad = ladder_multi(Rc, Gc, np.array([slots_conc]), n_paths=800, lev=3.0, seed=99, + deluck=DELUCK_OLD)["groups"][0] + ok_pos = rbad["p_target"] < rp["groups"][0]["p_target"] + print(f" controllo POSITIVO β€” la stessa politica a leva 3,00x (deve bustare):") + print(f" P(>=50/g) 1,00x {rp['groups'][0]['p_target']:.1%} vs 3,00x {rbad['p_target']:.1%}" + f" -> {'il confronto ha POTENZA' if ok_pos else '!! NESSUNA POTENZA'}") + print(f" controllo di pairing β€” due gruppi identici devono dare lo stesso vettore: " + f"max|diff| {np.abs(rp['groups'][0]['eur_day'] - rp['groups'][1]['eur_day']).max():.3e}") + tick("Β§2") + + # ============================================================ Β§3 pannello corretto + hr("Β§3. IL PANNELLO CORRETTO β€” U13 Binance fuori campione, W-LONG 2021+, funding") + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13, h13, l13 = (Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13], + Lb.reindex(p13.index)[X.U13]) + XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES} + bump(len(PHASES)) + xs0 = XSP[PHASES[0]] + cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index + base = cry_idx.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC")) + I = base[base >= S_LONG] + print(f" W-LONG: {len(I)} giorni {I[0].date()} -> {I[-1].date()} | " + f"XS01 = U13 Binance ({len(X.U13)} gambe) | de-luck x{DELUCK_NEW}") + + rate = GC.funding_rate() + if rate is None: + print(" !! cache funding assente: il filone NON puo' rispondere alla domanda (c). STOP.") + return + xsf = GC.xs_funding_measure(verbose=False) + xs_ann = xsf["ann"] + print(f" funding: TP01/SKH01 da serie misurate; XS01 = {xs_ann*100:+.2f}%/anno costante " + f"(Β§37, IC95 [{(xs_ann-1.96*xsf['se'])*100:+.2f}, {(xs_ann+1.96*xsf['se'])*100:+.2f}])") + + boot = PA.boot_idx(len(I), HDAYS, N_PATHS, seed=SEED) + + def build_on(idx: pd.DatetimeIndex, xs: pd.DataFrame, off: int, + fund: bool) -> tuple[np.ndarray, np.ndarray]: + """(R, gap) di TUTTE le configurazioni su un indice e una serie XS01 dati.""" + R = np.zeros((len(idx), len(CFGS))) + G = np.zeros((len(idx), len(CFGS))) + for ci, w in enumerate(CFGS): + r, g = AP.rg_off(w, idx, xs, off) + if fund: + r = r - GC.fvec_for(w, idx, rate, off, xs_ann) + R[:, ci], G[:, ci] = r, g + return R, G + + def build(off: int, ph: int, fund: bool) -> tuple[np.ndarray, np.ndarray]: + return build_on(I, XSP[ph], off, fund) + + # costo di funding EFFETTIVO per configurazione (verifica di A2) + print(f"\n costo di funding per configurazione (%/anno di equity a leva 1,00x, off 0):") + fcost = {} + for ci, w in enumerate(CFGS): + fv = GC.fvec_for(w, I, rate, 0, xs_ann) + fcost[ci] = float(np.mean(fv)) * 365 * 100 + nm = ["book_2SL", "book_DIV", "W_A", "TP01 solo", "SKH01 solo", "XS01 solo"] + for ci in range(len(CFGS)): + pred = sum(FUND_PUB[k] * v for k, v in CFGS[ci].items()) * 100 + print(f" {nm[ci]:<11} misurato {fcost[ci]:>5.2f}%/a atteso da Β§35/Β§37 {pred:>5.2f}%/a") + # CONTROLLO: il reindex su I non deve mangiare barre in silenzio (un NaN->0 e' un costo + # sottostimato che nessun numero segnala). + cov_tp = float(GC.f_tp01_sleeve(rate).reindex(I).notna().mean()) + cov_sk = float(GC.f_skh_sleeve(rate, 0).reindex(I).notna().mean()) + print(f" CONTROLLO di copertura su W-LONG: TP01 {cov_tp:.1%} delle barre, SKH01 {cov_sk:.1%}" + f" -> {'ok' if min(cov_tp, cov_sk) > 0.99 else '!! IL REINDEX MANGIA BARRE'}") + print(" ⚠ I costi misurati sono PIU' BASSI degli attesi, e non e' un disaccordo: Β§35 pubblica") + print(" -2,16%/a sul 2019+, -1,39% sul 2022-03+ e -0,55% sul 2025+ (il costo SCENDE nel") + print(" tempo) e questa finestra parte nel 2021. Costo del libro 75/25, anno per anno:") + fser = pd.Series(GC.fvec_for(W_2SL, I, rate, 0, xs_ann), index=I) + print(" " + " ".join(f"{y}:{float(g.mean()*365)*100:.2f}%" + for y, g in fser.groupby(fser.index.year))) + print(f" 🚨 Su tutta la storia oraria SKH01 paga -2,32% e TP01 -2,10% (Β§35). Su W-LONG sono") + print(f" {fcost[ISKH]:.2f}% e {fcost[ITP]:.2f}%: **il verso del confronto TP01-vs-SKH01 sul") + print(f" funding si INVERTE cambiando finestra** β€” l'aritmetica con cui avevo derivato A2") + print(f" usava i numeri di storia piena, quindi la previsione va verificata, non dedotta.") + print(f" costo di funding per POLITICA (media sugli slot):") + polfund = {} + for pi, p in enumerate(PNAMES): + v = float(np.mean([fcost[c] for c in SEL[pi]])) + polfund[SHORT[pi]] = v + print(f" {SHORT[pi]:<9} {v:>5.2f}%/a") + tick("Β§3") + + # ============================================================ Β§4 decomposizione + hr("Β§4. DECOMPOSIZIONE β€” un grado di liberta' per volta (ancora 0, fase 0, leva 1,00x)") + print(" Senza questa tabella si sa che il numero e' cambiato, non QUALE correzione lo cambia.") + # gli indici di CFGS non sono quelli di cfgs_pc: si rimappa (le prime 4 politiche sono + # le uniche del 25/07; CONC-A e MISTO-A non esistevano e compaiono solo dal passo S3). + map_pc = {I2SL: 0, IDIV: 1, ITP: 2, ISKH: 3, IXS: 4} + sel4_pc = np.array([[map_pc[c] for c in POLICIES[p]] for p in PNAMES[:4]]) + bpc = PA.boot_idx(Lc, HDAYS, N_PATHS, seed=SEED) + s0 = ladder_multi(Rc, np.zeros_like(Gc), sel4_pc, N_PATHS, 1.0, SEED, DELUCK_OLD, boot=bpc) + s1 = ladder_multi(Rc, Gc, sel4_pc, N_PATHS, 1.0, SEED, DELUCK_OLD, boot=bpc) + s2 = ladder_multi(Rc, Gc, sel4_pc, N_PATHS, 1.0, SEED, DELUCK_NEW, boot=bpc) + # --- S2b/S3a: UNIVERSO e FINESTRA separati, sullo STESSO indice comune (crypto n HL n Bin). + XS_HL = pc.xsec_daily_tuples("shared") + I24 = I[I >= pd.Timestamp("2024-01-01", tz="UTC")].intersection(XS_HL.index) + b24 = PA.boot_idx(len(I24), HDAYS, N_PATHS, seed=SEED) + R2b, G2b = build_on(I24, XS_HL, 0, False) + Rchk, Gchk, Ichk = pc.book_tuples(0.55, 0.20, 0.25, start="2024-01-01") + kk = pd.DatetimeIndex(Ichk).isin(I24) + jj = pd.DatetimeIndex(I24).isin(pd.DatetimeIndex(Ichk)) + dchk = float(np.abs(Rchk[kk] - R2b[:, IDIV][jj]).max()) + print(f" CONTROLLO: `rg_off` con le tuple HL riproduce `pc.book_tuples` (book_div) su " + f"{int(kk.sum())} giorni a max|diff| = {dchk:.3e}") + s2b = ladder_multi(R2b, G2b, SEL[:4], N_PATHS, 1.0, SEED, DELUCK_NEW, boot=b24) + R3a, G3a = build_on(I24, XSP[PHASES[0]], 0, False) + s3a = ladder_multi(R3a, G3a, SEL[:4], N_PATHS, 1.0, SEED, DELUCK_NEW, boot=b24) + R3, G3 = build(0, PHASES[0], False) + s3 = ladder_multi(R3, G3, SEL[:4], N_PATHS, 1.0, SEED, DELUCK_NEW, boot=boot) + R4, G4 = build(0, PHASES[0], True) + s4 = ladder_multi(R4, G4, SEL[:4], N_PATHS, 1.0, SEED, DELUCK_NEW, boot=boot) + lab = ["S0 25/07 tale e quale (HL19 2024+, close-only, x0,60)", + "S1 + lente ACCOPPIATA (misurata il 25/07 SERA)", + "S2 + de-luck x0,89 invece di x0,60 (26/07)", + "S2b [= S2 su indice comune: controllo di macchina]", + "S3a + UNIVERSO: XS01 = U13 Binance, stessa finestra", + "S3b + FINESTRA: W-LONG 2021+ (fuori campione)", + "S4 + FUNDING = la lente onesta di oggi"] + ALL = (s0, s1, s2, s2b, s3a, s3, s4) + print(f"\n P(>=50 EUR/g) per politica, {N_PATHS} percorsi, stessi sorteggi:") + print(" " + f"{'passo':<52}" + "".join(f"{s:>10}" for s in SHORT[:4]) + f"{' ordine':<40}") + for l, st in zip(lab, ALL): + v = [g["p_target"] for g in st["groups"]] + o = " > ".join([SHORT[i] for i in np.argsort(-np.array(v))]) + print(f" {l:<52}" + "".join(f"{x:>9.1%} " for x in v) + f" {o:<40}") + print(f"\n EUR/g MEDIO per politica (la MEDIANA e' degenere: tabella sotto):") + print(" " + f"{'passo':<52}" + "".join(f"{s:>10}" for s in SHORT[:4])) + for l, st in zip(lab, ALL): + print(f" {l:<52}" + "".join(f"{g['mean']:>9.2f} " for g in st["groups"])) + print(f"\n EUR/g MEDIANA β€” il numero di TESTA del 25/07 era 'MISTO 10,70':") + print(" " + f"{'passo':<52}" + "".join(f"{s:>10}" for s in SHORT[:4])) + for l, st in zip(lab, ALL): + print(f" {l:<52}" + "".join(f"{g['med']:>9.2f} " for g in st["groups"])) + tick("Β§4") + + # ============================================================ Β§5 banda d'ancora + hr(f"Β§5. BANDA D'ANCORA β€” {len(OFFSETS)} offset x {len(PHASES)} fasi, " + f"funding ON/OFF, {len(PNAMES)} politiche") + print(f" offset (sottocampione uniforme DICHIARATO dei 23): {list(OFFSETS)}") + print(f" fasi XS01: {list(PHASES)} (lo stesso sottocampione di Β§37)") + print(f" leva {LEV_MAIN:.2f}x fissa e uguale per tutte (confronto appaiato); " + f"{N_PATHS} percorsi condivisi.") + cells = [] + REF: dict[str, np.ndarray] = {} # vettori per-percorso alla cella di riferimento + for off in OFFSETS: + for ph in PHASES: + for fund in (False, True): + R, G = build(off, ph, fund) + res = ladder_multi(R, G, SEL, N_PATHS, LEV_MAIN, SEED, DELUCK_NEW, boot=boot) + for pi, g in enumerate(res["groups"]): + cells.append(dict(off=off, ph=ph, fund=fund, pol=SHORT[pi], + p50=g["p_target"], pz=g["p_zero"], med=g["med"], + p10g=g["p_ge10"], mean=g["mean"], ever2=g["p_ever2"], + slot1=g["p_slot1"], ntix=g["n_tickets"], + maxc=g["max_conc"])) + if off == 0 and ph == PHASES[0] and fund: + REF[SHORT[pi]] = g["eur_day"] + tick(f"offset {off}") + C = pd.DataFrame(cells) + + for fund, flab in ((True, "CON funding (la lente onesta)"), (False, "SENZA funding")): + s = C[C["fund"] == fund] + print(f"\n --- {flab} β€” mediana sulle {len(OFFSETS)*len(PHASES)} celle d'ancora") + print(f" {'politica':>10} {'P(>=50/g)':>10} {'banda ancora p10-p90':>22} {'P(zero)':>9} " + f"{'EUR/g med':>10} {'EUR/g medio':>12} {'P(>=10/g)':>10} {'P(2o conto)':>12}") + for p in SHORT: + q = s[s["pol"] == p] + print(f" {p:>10} {q['p50'].median():>10.2%} " + f"[{np.percentile(q['p50'],10):>9.2%},{np.percentile(q['p50'],90):>9.2%}] " + f"{q['pz'].median():>9.1%} {q['med'].median():>10.2f} " + f"{q['mean'].median():>12.2f} {q['p10g'].median():>10.1%} " + f"{q['slot1'].median():>12.1%}") + print(" ordine per metrica (se le tre non concordano, la classifica e' un artefatto") + print(" della metrica scelta, non un fatto):") + for mkey, mlab in (("p50", "P(>=50/g)"), ("mean", "EUR/g medio"), ("p10g", "P(>=10/g)")): + o = s.groupby("pol")[mkey].median().sort_values(ascending=False) + print(f" {mlab:<12} {' > '.join(o.index)}") + + # riproduzione di Β§37 (W_A = CONC-A, funding dentro) + q = C[(C["fund"]) & (C["pol"] == "CONC-A")] + print(f"\n RIPRODUZIONE Β§37 (CONC-A = W_A 38/38/25, funding dentro, mediana d'ancora):") + print(f" P(>=50/g) {q['p50'].median():.1%} (PUB {PUB_GATEC['p50g']:.1%}) | " + f"P(zero) {q['pz'].median():.1%} (PUB {PUB_GATEC['pzero']:.1%})") + print(" ⚠ non e' identica per costruzione: Β§37 usa `best_lev` per configurazione e 23") + print(" offset x 3 fasi; qui la leva e' FISSA a 1,00x per tutte le politiche (appaiato)") + print(f" e gli offset sono {len(OFFSETS)}. L'ordine di grandezza e' il controllo.") + + # differenze APPAIATE cella per cella + print(f"\n DIFFERENZE APPAIATE cella per cella (mediana delle differenze, mai differenza") + print(f" delle mediane), funding dentro. Riferimento = MISTO (il vincitore del 25/07):") + sf = C[C["fund"]] + piv = sf.pivot_table(index=["off", "ph"], columns="pol", values="p50") + ncell = len(piv) + print(f" {'politica':>10} {'d P(>=50/g) vs MISTO':>22} {'>0 su celle':>13} " + f"{'p10':>8} {'p90':>8}") + for p in SHORT: + if p == "MISTO": + continue + d = piv[p] - piv["MISTO"] + print(f" {p:>10} {d.median()*100:>21.2f}pp {int((d>0).sum()):>7}/{ncell:<5} " + f"{np.percentile(d,10)*100:>7.2f}pp {np.percentile(d,90)*100:>7.2f}pp") + # --- controllo di LEVA: l'ordine dipende dalla leva scelta? + print(f"\n CONTROLLO DI LEVA β€” le stesse celle a {LEV_ALT:.2f}x (off 0, {len(PHASES)} fasi,") + print(f" funding ON). Se l'ordine cambia con la leva, non e' una proprieta' della politica:") + alt = [] + for ph in PHASES: + R, G = build(0, ph, True) + res = ladder_multi(R, G, SEL, N_PATHS, LEV_ALT, SEED, DELUCK_NEW, boot=boot) + for pi, g in enumerate(res["groups"]): + alt.append(dict(ph=ph, pol=SHORT[pi], p50=g["p_target"], pz=g["p_zero"], + mean=g["mean"])) + A = pd.DataFrame(alt) + oa = A.groupby("pol")["p50"].median().sort_values(ascending=False) + o1 = C[(C["fund"]) & (C["off"] == 0)].groupby("pol")["p50"].median().sort_values(ascending=False) + print(f" {'politica':>10} {'P(>=50/g) @0,75x':>18} {'P(>=50/g) @1,00x':>18} {'P(zero) @0,75x':>16}") + for p_ in SHORT: + print(f" {p_:>10} {A[A['pol']==p_]['p50'].median():>17.2%} " + f"{o1[p_]:>17.2%} {A[A['pol']==p_]['pz'].median():>15.1%}") + print(f" ordine @0,75x: {' > '.join(oa.index)}") + print(f" ordine @1,00x: {' > '.join(o1.index)}") + print(f" -> {'STESSO ordine' if list(oa.index) == list(o1.index) else 'ORDINE DIVERSO: la leva conta'}") + tick("Β§5") + + # ============================================================ Β§6 la previsione sul funding + hr("Β§6. LA PREVISIONE A2 SUL FUNDING, VERIFICATA (appaiata cella per cella)") + pv = C.pivot_table(index=["off", "ph", "pol"], columns="fund", values="p50") + pv = pv.reset_index() + print(f" {'politica':>10} {'costo fund %/a':>15} {'d P(>=50/g)':>13} {'<0 su celle':>13} " + f"{'d EUR/g med':>13}") + dmed = {} + for p in SHORT: + q = pv[pv["pol"] == p] + d = (q[True] - q[False]).values + dmed[p] = float(np.median(d)) + pm = C.pivot_table(index=["off", "ph", "pol"], columns="fund", values="med").reset_index() + qm = pm[pm["pol"] == p] + dm = (qm[True] - qm[False]).values + print(f" {p:>10} {polfund[p]:>14.2f}% {np.median(d)*100:>12.2f}pp " + f"{int((d<0).sum()):>7}/{len(d):<5} {np.median(dm):>12.2f}") + ordf_on = C[C["fund"]].groupby("pol")["p50"].median().sort_values(ascending=False).index.tolist() + ordf_off = C[~C["fund"]].groupby("pol")["p50"].median().sort_values(ascending=False).index.tolist() + a2 = ordf_on == ordf_off + print(f"\n ordine SENZA funding: {' > '.join(ordf_off)}") + print(f" ordine CON funding: {' > '.join(ordf_on)}") + print(f" >>> A2 {'CONFERMATA' if a2 else 'REFUTATA'}: il funding " + f"{'NON cambia' if a2 else 'CAMBIA'} l'ordine fra politiche.") + cc = np.corrcoef([polfund[p] for p in SHORT], [dmed[p] for p in SHORT])[0, 1] + print(f" correlazione (costo di funding della politica, danno misurato): {cc:+.2f} " + f"[atteso negativo: chi paga di piu' perde di piu']") + tick("Β§6") + + # ============================================================ Β§7 numero di conti + hr("Β§7. LA DIMENSIONE MAI TESTATA β€” il NUMERO DI CONTI non e' dato, si COMPRA") + Rf, Gf = build(0, 0, True) + scen = [("nessun versamento, quota persa", 0.0, False), + ("nessun versamento, quota RIMBORSATA (Β§30)", 0.0, True), + ("EUR 500/mese, quota persa", 500.0, False), + ("EUR 500/mese, quota RIMBORSATA", 500.0, True)] + trajs = {} + for lab_s, dep, ref_ in scen: + res = ladder_multi(Rf, Gf, SEL, N_PATHS, LEV_MAIN, SEED, DELUCK_NEW, + refund=ref_, deposit_eur=dep, boot=boot, track=True) + trajs[lab_s] = res + print(f"\n --- {lab_s}") + print(f" {'politica':>10} {'biglietti':>10} {'max conti':>10} {'P(mai >=2)':>11} " + f"{'P(>=50/g)':>10} {'EUR/g med':>10} {'P(zero)':>9} {'rimborso $':>11}") + for pi, g in enumerate(res["groups"]): + print(f" {SHORT[pi]:>10} {g['n_tickets']:>10.2f} {g['max_conc']:>10.2f} " + f"{1-g['p_ever2']:>10.1%} {g['p_target']:>10.1%} {g['med']:>10.2f} " + f"{g['p_zero']:>9.1%} {g['mean_back']:>11.0f}") + print("\n TRAIETTORIA del numero MEDIO di conti funded vivi (mesi 6/12/18/24/30/36):") + for lab_s in trajs: + tr = trajs[lab_s]["traj"] + print(f" {lab_s:<44}" + "".join( + f"{tr[m-1].mean():>7.2f}" for m in (6, 12, 18, 24, 30, 36))) + print("\n DISPERSIONE fra politiche (max-min di P(>=50/g)) per scenario β€” quanto CONTA") + print(" la scelta della politica:") + for lab_s in trajs: + v = [g["p_target"] for g in trajs[lab_s]["groups"]] + best = SHORT[int(np.argmax(v))] + print(f" {lab_s:<44} spread {max(v)-min(v):>6.2%} vincitore {best}") + tick("Β§7") + + # ============================================================ Β§8 MDE e verdetto + hr("Β§8. MDE β€” la finestra distingue davvero due politiche?") + sf = C[C["fund"]] + piv = sf.pivot_table(index=["off", "ph"], columns="pol", values="p50") + print(" TRE risoluzioni, e sono cose DIVERSE. Confonderle e' il modo standard di trasformare") + print(" rumore in classifica.") + print(f"\n (a) MONTE CARLO appaiato, cella di riferimento (off 0, fase {PHASES[0]}, funding ON):") + print(" i due esiti sono osservati sugli STESSI percorsi -> SE di McNemar sui discordanti.") + print(f" {'coppia':>22} {'d P(>=50/g)':>13} {'discordanti':>12} {'SE':>8} {'z':>8}") + base = REF["MISTO"] >= TARGET + for p_ in SHORT: + if p_ == "MISTO": + continue + oth = REF[p_] >= TARGET + n01 = int((base & ~oth).sum()) + n10 = int((~base & oth).sum()) + d = float(base.mean() - oth.mean()) + se = float(np.sqrt(n01 + n10) / len(base)) if (n01 + n10) else float("nan") + z = d / se if se and se > 0 else float("nan") + print(f" {'MISTO - ' + p_:>22} {d*100:>+12.2f}pp {n01 + n10:>12} " + f"{se*100:>7.2f}pp {z:>+8.2f}") + print(" ⚠ e' la risoluzione della SIMULAZIONE, non dell'evidenza: dice se due politiche") + print(" differiscono SU QUESTI percorsi, non se differirebbero nel mondo.") + print(f"\n (b) BANDA D'ANCORA sulle {len(piv)} celle (mediana delle differenze APPAIATE).") + print(" 🚨 NON e' un intervallo di confidenza e non ci si attacca un p-value (regola Β§57:") + print(" le 24 ancore correlano 0,63 e valgono ~2 osservazioni indipendenti). Si legge come") + print(" robustezza alla SCELTA dell'ancora, e basta:") + print(f" {'coppia':>22} {'mediana':>11} {'p10':>11} {'p90':>11} {'segno tenuto':>14}") + for p_ in SHORT: + if p_ == "MISTO": + continue + d = (piv["MISTO"] - piv[p_]).values + print(f" {'MISTO - ' + p_:>22} {np.median(d)*100:>+10.2f}pp " + f"{np.percentile(d,10)*100:>+10.2f}pp {np.percentile(d,90)*100:>+10.2f}pp " + f"{int((d>0).sum()):>6}/{len(d):<7}") + yrs = len(I) / 365.25 + print(f"\n (c) DATO β€” l'unica che limita davvero. La finestra e' {yrs:.1f} anni e i {N_PATHS}") + print(f" percorsi sono ricampionamenti degli STESSI {len(I)} giorni: il bootstrap non crea") + print(f" informazione. MDE sul drift ~ 1,96/sqrt({yrs:.1f}) = {1.96/np.sqrt(yrs):.2f} di Sharpe.") + print(" Il differenziale di FUNDING fra politiche misurato in Β§3 e' ~0,10pp/anno di drift:") + print(" irrisorio rispetto all'MDE. Cio' che le politiche possono davvero separare non e'") + print(" un differenziale di rendimento ma la GEOMETRIA β€” correlazione fra conti e") + print(" barriera per-conto β€” e quella si misura sulla forma, non sul drift.") + + hr("VERDETTO") + fin = C[C["fund"]].groupby("pol")["p50"].median().sort_values(ascending=False) + fin_med = C[C["fund"]].groupby("pol")["med"].median() + fin_pz = C[C["fund"]].groupby("pol")["pz"].median() + for p in fin.index: + print(f" {p:>10} P(>=50/g) {fin[p]:>6.2%} EUR/g med {fin_med[p]:>6.2f} " + f"P(zero) {fin_pz[p]:>6.1%}") + win = fin.index[0] + print(f"\n VINCITORE OGGI: {win} (25/07: MISTO)") + print(f" celle valutate: {len(C)} righe = {len(OFFSETS)}x{len(PHASES)}x2x{len(PNAMES)}; " + f"valutazioni MC totali contate al rialzo: {TRIALS}") + print(f" tempo {time.time()-T0:.0f}s") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_obiettivo_diretto.py b/scripts/research/r0823b_obiettivo_diretto.py new file mode 100644 index 0000000..b0ca910 --- /dev/null +++ b/scripts/research/r0823b_obiettivo_diretto.py @@ -0,0 +1,877 @@ +#!/usr/bin/env python +"""r0823b_obiettivo_diretto β€” OTTIMIZZARE DIRETTAMENTE L'OBIETTIVO DEL PROGETTO (2026-08-23). + +Β§58 dell'ondata. Libro, pesi, cron, config: NON TOCCATI (isolamento provato in Β§0). + +IL BUCO +------- +Ogni ottimizzazione fatta in questo progetto ha massimizzato **un'altra cosa**: + * lo **Sharpe** (ogni filone di ricerca direzionale); + * **J = P(pass) x P(alive)** (PROP-ALLOC Β§17, PROP-RECAL Β§29, SKH-ANCHOR-PROP Β§32); + * **E[payout]** (r0725_hyro, Β§37); + * il **capitale mediano** o **P(arrivare al capitale-rendita)** (Β§36, r0807_piano_netto). +E il critico di Β§37 ha scritto la frase che apre questo filone: + Β«piu' leva sul funded ALZA E[payout] e ABBASSA P(payout): sono due obiettivi diversiΒ». +**Nessuno ha mai ottimizzato DIRETTAMENTE `P(reddito >= 50 EUR/giorno entro H)`**, che e' +l'obiettivo DICHIARATO del progetto. I numeri operativi pubblicati (42% -> 7,8% -> 4,4% -> +2,6%) sono tutti **letti alla cella che massimizza J**, non alla cella che massimizza P. + +LE DUE DEFINIZIONI, DICHIARATE PRIMA DI MISURARE +------------------------------------------------ +Il progetto ne usa DUE senza mai metterle sulla stessa pagina, e danno risposte opposte: + + **A β€” RICCHEZZA (rendita sostenibile).** Β«reddito >= 50 EUR/gΒ» = il patrimonio PROPRIO + raggiunge il **capitale-rendita** (muro), cioe' il capitale da cui si preleva 50 EUR/g con + P(capitale a 20 anni >= capitale iniziale) >= 90% (definizione 25/07, invariata). E' la + definizione dei muri, delle traiettorie e di tutto il piano (Β§36). E' **sostenibile per + costruzione**. + + **B β€” FLUSSO (produzione realizzata).** Β«reddito >= 50 EUR/gΒ» = la media dei payout incassati + sull'orizzonte, `cum_payout / giorni`, >= 50 EUR/g. E' la definizione di `r0725_prop_ladder` + e quindi di **ogni numero del canale funded** (Β§29 7,8% Β· Β§32 4,4% Β· Β§37 2,6%). **NON e' + sostenibile**: un conto funded che busta il giorno dopo l'orizzonte azzera il flusso, e la + stessa cassa che il contatore chiama Β«redditoΒ» resta nel conto (nel simulatore ufficiale la + cassa si accumula E conta come reddito: e' una PRODUZIONE, non un prelievo). + +Qui si misurano **entrambe**, sullo stesso Monte Carlo e sugli stessi percorsi, e si riporta +come cambia la risposta. Primaria = **A**, perche' e' quella su cui il progetto ha costruito il +muro, i versamenti e la decisione di venue; B si riporta perche' e' quella dei numeri pubblicati. + +LO SPAZIO DI POLITICA β€” dichiarato esaustivamente PRIMA di misurare +-------------------------------------------------------------------- + 1. `k` leva del libro proprio {1,00 Β· 1,25 Β· 1,50} (Β§38: la chiave NON esiste) + 2. `own` ripartizione del libro proprio {LIVE 75/25 Β· W_A 38/38/25} + 3. `lev_fu` leva del conto funded {0,50 Β· 0,75 Β· 1,00 Β· 1,25} + 4. `n_max` numero di conti concorrenti {0 Β· 1 Β· 3 Β· 6} (0 = nessun biglietto) + 5. `pref` taglia del biglietto {grande Β· piccolo} + 6. `rebuy` ri-comprare dopo un bust {si' Β· no} + 7. `floor` capitale proprio sotto cui NON si compra {$0 Β· $2.000 Β· $10.000} + 8. `dep` versamento mensile {EUR 0 Β· 250 Β· 500} (il piano in corso e' 500) + 9. `invest` la cassa lavora sul libro invece di restare ferma {si' Β· no} +Il punto (9) e' un difetto misurabile della macchineria pubblicata: in `pl.simulate` / +`pc.simulate` / `gate_c.ladder` **la cassa dei payout resta FERMA per 36 mesi**. + +LE MIE ATTESE, REGISTRATE PRIMA DI MISURARE +-------------------------------------------- + A1. L'argmax di P(>=50/g) sara' a leva **piu' alta** dell'argmax di J, e la differenza sara' + grande (fattore >2 su P). *Motivo: P(>=50/g) e' una probabilita' di superare una soglia + alta = coda destra; J e' una probabilita' di sopravvivenza = coda sinistra.* + A2. Sotto la definizione **A** (ricchezza) il canale funded **non** portera' P(36 mesi) sopra + zero in modo apprezzabile: il muro e' ~$313k e 36 mesi da $635 sono ~3 ordini di grandezza. + Quindi la risposta alla domanda Β«esiste una politica sopra il 20% a 36 mesi?Β» sara' + **SI' sotto B e NO sotto A**, e questa e' la vera risposta del filone. + A3. Investire la cassa invece di lasciarla ferma valdra' poco su B (il flusso non dipende dalla + cassa) e molto su A. *Se sbaglio e vale poco anche su A, lo dico.* + A4. L'ottimo di P sara' accompagnato da P(rovina) molto alta (>50%) β€” cioe' sara' un ottimo che + il progetto NON dovrebbe adottare, e va riportato come informazione, non come consiglio. + A5. Il vincolo binding a 36 mesi non sara' ne' la leva ne' il numero di conti ma il **cap + $200k/trader** oppure il **capitale che entra**. Mi aspetto il secondo. + +CONVENZIONI +----------- +* Lente **ACCOPPIATA** ovunque ci sia una barriera a un giorno (regola 25/07, riconfermata Β§33). +* Cella d'ancora **MEDIANA** (non la canonica, che sta al 91Β° pctl β€” Β§32), scelta con lo stesso + criterio di `gate_c` su un sottoinsieme d'ancore dichiarato PRIMA. +* Funding **DENTRO** (Β§35), de-luck **x0,89** sul drift (Β§ r0726_deluck_factor), fisco 33% + + patrimoniale 0,2% sul libro proprio (Β§ r0807), morte-firm 10%/anno, cap $200k/trader. +* Baseline del libro: **`hourly`** (la lente di ogni muro e traiettoria) β€” dichiarata perche' il + critico di Β§57 ha misurato che sotto Β«75/25Β» convivono tre baseline diverse. +* Muro: **CONGELATO** a k=1 come primaria (si de-leva al traguardo: la leva non deve poter + spostare il bersaglio); **MOBILE** come sensibilita' (Β§53 dichiara che la differenza vale 1,2 + anni). Banda del muro propagata dai valori di Β§57. +* Monte Carlo: la risoluzione si dichiara. 1500 percorsi -> SE ~1,0pp su una P vicina al 20%. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0823b_obiettivo_diretto.py + ... --fast (griglia ridotta, ~1/3 del tempo) +Rete: NESSUNA (solo cache su disco: parquet certificati, cache Binance di Β§21, cache funding +di Β§35). Nessuna scrittura fuori da stdout. +""" +from __future__ import annotations + +import subprocess +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) + +if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import + sys.argv.append("--no-net") + +import r0725_capcurve as CC # noqa: E402 TAX_RATE, EURUSD, book_series +import r0725_prop_coupled as pc # noqa: E402 lente accoppiata +import r0725_prop_ladder as pl # noqa: E402 regole firm, scala di conti +import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI +import r0807_piano_netto as PN # noqa: E402 passo_fiscale, accumula, muro +import r0822_prop_alloc as PA # noqa: E402 boot_idx, DELUCK +import r0822b_prop_recal as PR # noqa: E402 obj_rg, LEVS, pannelli Binance +import r0822b_xs01_oos as X # noqa: E402 U13, motore XS01 +import r0822c_skh_anchor_prop as AP # noqa: E402 rg_off (offset di SKH01) +import r0822d_funding as FD # noqa: E402 il funding +import r0822d_gate_c as GC # noqa: E402 ladder, fvec_for, xs_funding +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +FAST = "--fast" in sys.argv +EURUSD = pl.EURUSD +TAX = CC.TAX_RATE +PATR = TX.PATRIMONIALE +DELUCK = PA.DELUCK # x0,89 misurato +W_LIVE = {"TP01": 0.75, "SKH01": 0.25} +W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25} +START_USD = 635.0 # il conto Deribit VERO di oggi +SEED_LAD = pl.HORIZON_MONTHS * 0 + 20260822 # lo stesso di gate_c (per la bit-exactness) +SEED_BOOT = 58023 +N_PATHS = 900 if FAST else 1500 +N_PATHS_J = PA.N_PATHS +MIN_PAYOUT = GC.MIN_PAYOUT_TRADER +ORIZZONTI_M = (24, 36, 60, 120) +H_MAX_D = max(ORIZZONTI_M) * 30 +MARKS = tuple(m * 30 for m in ORIZZONTI_M) +OFFSETS_SUB = tuple(range(0, 690, 90)) if not FAST else (0, 180, 360, 540) # 8 (o 4) a priori +PHASES_SUB = AP.PHASES_SUB # (0, 3, 7) β€” dichiarato da Β§32 +LEVS_FU = (0.50, 0.75, 1.00, 1.25) +NMAX = (1, 3, 6) +PREFS = ("grande", "piccolo") +REBUY = (True, False) + +# numeri PUBBLICATI da riprodurre PRIMA di produrne di nuovi +PUB = dict(muro_L3=313_143.0, perp_L3=0.0635, drift_L3=0.1519, + p50g_fund=0.026, p50g_p10=0.015, p50g_p90=0.047, pzero_fund=0.403, + p50g_nofund=0.044, + muro_p90=186_623.0, muro_m1se=709_753.0) # banda di Β§57 + +TRIALS = 0 +T0 = time.time() + + +def bump(n: int = 1) -> None: + global TRIALS + TRIALS += n + + +def hr(t: str = "") -> None: + print("\n" + "=" * 104) + if t: + print(" " + t) + print("=" * 104) + + +def tick(lab: str) -> None: + print(f" [{time.time() - T0:6.0f}s] {lab}", flush=True) + + +# =========================================================================== +# IL SIMULATORE UNIFICATO +# =========================================================================== +def uni(Ra: np.ndarray, Ga: np.ndarray, Rb: np.ndarray, boot: np.ndarray, *, + lev_fu: float = 1.0, n_max: int = 6, pref: str = "grande", rebuy: bool = True, + floor_usd: float = 0.0, k_book: float = 1.0, invest: bool = False, + dep_eur: float = 0.0, start_usd: float = pl.START_EUR * EURUSD, + seed: int = SEED_LAD, refund: bool = False, min_payout: float = 0.0, + tax: float = 0.0, patr: float = 0.0, walls: tuple = (), + marks: tuple = (), deposit_mode: str = "ladder", + deluck_f: float = DELUCK) -> dict: + """UN simulatore per le DUE strade, sugli STESSI percorsi. + + E' una GENERALIZZAZIONE STRETTA di `gate_c.ladder` (che a sua volta e' `pc.simulate` con + tre aggiunte): con `n_max=6, pref='grande', rebuy=True, floor=0, k_book=0, invest=False, + tax=0, patr=0, deposit_mode='ladder'` deve essere **BIT-EXACT** rispetto a `gate_c.ladder`. + E con `n_max=0, invest=True, tax=0.33, patr=0.002, deposit_mode='accumula'` deve essere + **BIT-EXACT** rispetto a `PN.accumula`. I due controlli sono in Β§1. + + Cio' che aggiunge, e che nessuna macchina pubblicata ha: + * un **pot proprio** che compone al libro (`k_book x Rb`) invece di restare fermo, col + fisco d'accumulo dentro (stessa `PN.passo_fiscale` del piano); + * i **payout che rientrano nel pot** e quindi compongono; + * `floor_usd` β€” un capitale proprio sotto cui non si compra un altro biglietto; + * `rebuy` β€” la scelta di NON ricomprare dopo un bust; + * `pref` β€” biglietto piu' grande o piu' piccolo (piu' tentativi); + * il **primo passaggio** al capitale-rendita, per l'obiettivo A. + + `Ra/Ga` = libro del conto FUNDED (ritorno e gap intraday accoppiato), `Rb` = libro PROPRIO. + Il de-luck si applica al DRIFT di R (non al gap): stessa convenzione di `ladder`. + """ + rng = np.random.default_rng(seed) + n_paths, n_days = boot.shape + Ram = np.asarray(Ra, float).copy() + Ram = Ram - (1.0 - deluck_f) * Ram.mean() + Gam = np.asarray(Ga, float) + Rbm = np.asarray(Rb, float).copy() + Rbm = Rbm - (1.0 - deluck_f) * Rbm.mean() + + n_slot = int(n_max) + FIRMS = pl.FIRMS + cash = np.full(n_paths, float(start_usd)) + basis = cash.copy() # base fiscale del pot + carry = np.zeros((n_paths, TX.CARRY_ANNI)) + anno_start = cash.copy() + flusso = np.zeros(n_paths) + cum_payout = np.zeros(n_paths) + spent = np.zeros(n_paths) + got_back = np.zeros(n_paths) + deposited = np.zeros(n_paths) + n_payouts = np.zeros(n_paths, int) + n_tickets = np.zeros(n_paths, int) + n_pass = np.zeros(n_paths, int) + peak_notional = np.zeros(n_paths) + + if n_slot: + Rs_full = Ram[boot] * lev_fu # (paths, days) β€” tutti i conti uguali + Gs_full = Gam[boot] * lev_fu + phase = np.full((n_paths, n_slot), -1, np.int8) + eq = np.ones((n_paths, n_slot)) + firm = np.full((n_paths, n_slot), -1, np.int8) + notional = np.zeros((n_paths, n_slot)) + price_paid = np.zeros((n_paths, n_slot)) + refunded = np.zeros((n_paths, n_slot), bool) + used = np.zeros((n_paths, n_slot), bool) + firm_alive = np.ones((n_paths, len(FIRMS)), bool) + p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0) + Rbp = Rbm[boot] if (invest and k_book > 0) else None + + hit = {w: np.full(n_paths, -1, int) for w in walls} + snap: dict[int, dict] = {} + fiscale = (tax > 0 or patr > 0) + + for t in range(n_days): + # ---- morte di una firm (identico a ladder: il draw avviene SEMPRE, anche a 0 slot, + # solo se il canale funded esiste) + if n_slot: + died = rng.random((n_paths, len(FIRMS))) < p_death_day + newly_dead = died & firm_alive + firm_alive &= ~died + if newly_dead.any(): + for fi in range(len(FIRMS)): + kill = newly_dead[:, fi] + if kill.any(): + h = (firm == fi) & kill[:, None] + phase[h] = -1 + notional[h] = 0.0 + + # ---- acquisto biglietti + for k in range(n_slot): + free = phase[:, k] == -1 + if not rebuy: + free &= ~used[:, k] + if not free.any(): + continue + for fi, F in enumerate(FIRMS): + u = np.where(firm == fi, notional, 0.0).sum(axis=1) + order = sorted(F["tickets"], key=(lambda x: -x[0]) if pref == "grande" + else (lambda x: x[0])) + for (size, price) in order: + buy = (free & firm_alive[:, fi] & (cash >= price) + & (cash - price >= floor_usd) & (u + size <= F["cap"])) + if not buy.any(): + continue + cash[buy] -= price + flusso[buy] -= price + spent[buy] += price + n_tickets[buy] += 1 + phase[buy, k] = 0 + eq[buy, k] = 1.0 + firm[buy, k] = fi + notional[buy, k] = size + price_paid[buy, k] = price + refunded[buy, k] = False + used[buy, k] = True + free = free & ~buy + if not free.any(): + break + if not free.any(): + break + + # ---- dinamica dei conti + r = Rs_full[:, t][:, None] + g = Gs_full[:, t][:, None] + act = phase >= 0 + eq_prev = eq + eq = np.where(act, eq * (1.0 + r), eq) + eq_low = np.where(act, eq_prev * (1.0 + r + g), eq) + r_low = r + g + for fi, F in enumerate(FIRMS): + s_ = act & (firm == fi) + if not s_.any(): + continue + ev_t, ev_dd, ev_dl = F["ev"] + fu_ml, fu_dl = F["fu"] + in_ev = s_ & (phase == 0) + in_fu = s_ & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl))) + | (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl)))) + phase[bust] = -1 + notional[bust] = 0.0 + passed = in_ev & (eq >= 1.0 + ev_t) & ~bust + phase[passed] = 1 + eq[passed] = 1.0 + n_pass += passed.sum(axis=1) + peak_notional = np.maximum(peak_notional, + np.where(phase == 1, notional, 0.0).sum(axis=1)) + + # ---- il POT proprio compone + if Rbp is not None: + cash = cash * (1.0 + k_book * Rbp[:, t]) + + # ---- versamento (convenzione `accumula`: giorno 30, 60, ...) + if deposit_mode == "accumula" and dep_eur and t % 30 == 0 and t > 0: + cash += dep_eur * EURUSD + flusso += dep_eur * EURUSD + deposited += dep_eur * EURUSD + + # ---- payout mensile + rimborso + versamento (convenzione `ladder`) + if (t + 1) % 30 == 0: + if n_slot: + fu = phase == 1 + pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * pl.SPLIT) >= min_payout) + gain = np.where(pay, (eq - 1.0) * notional, 0.0) + net = gain.sum(axis=1) * pl.SPLIT * (1.0 - TAX) + cum_payout += net + cash += net + flusso += net + n_payouts += (pay.sum(axis=1) > 0).astype(int) + eq = np.where(pay, 1.0, eq) + if refund: + back = pay & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + flusso += amt + got_back += amt + refunded |= back + if deposit_mode == "ladder" and dep_eur: + cash += dep_eur * EURUSD + flusso += dep_eur * EURUSD + deposited += dep_eur * EURUSD + + # ---- primo passaggio al muro (obiettivo A) + for w, arr in hit.items(): + nuovi = (arr < 0) & (cash >= w) + arr[nuovi] = t + + # ---- fisco d'accumulo sul pot + if fiscale and (t + 1) % 365 == 0: + PN.passo_fiscale(cash, anno_start, flusso, carry, tax, patr) + anno_start = cash.copy() + flusso[:] = 0.0 + basis = cash.copy() + + if (t + 1) in marks: + d = t + 1 + ed = cum_payout / EURUSD / (d / 365.0) / 365.0 + contrib = float(start_usd) + deposited + snap[d] = dict(p_flow=float((ed >= pl.TARGET_EUR_DAY).mean()), + p_flow10=float((ed >= 10.0).mean()), + med_eur=float(np.median(ed)), + med_cash=float(np.median(cash)), + mean_cash=float(cash.mean()), + p10_cash=float(np.percentile(cash, 10)), + p_zero=float((cum_payout <= 0).mean()), + p_sotto=float((cash < contrib).mean()), + p_rovina=float((cash < 0.10 * contrib).mean()), + contrib=float(contrib.mean() if np.ndim(contrib) else contrib), + **{f"A{w:.0f}": float(((hit[w] >= 0) & (hit[w] < d)).mean()) + for w in walls}) + + ed = cum_payout / EURUSD / (n_days / 365.0) / 365.0 + out = dict(eur_day=ed, cash=cash, snap=snap, hit=hit, + p_target=float((ed >= pl.TARGET_EUR_DAY).mean()), + p_ge10=float((ed >= 10.0).mean()), + p_zero=float((cum_payout <= 0).mean()), + med=float(np.median(ed)), + med_cash=float(np.median(cash)), mean_cash=float(cash.mean()), + p10_cash=float(np.percentile(cash, 10)), + p90_cash=float(np.percentile(cash, 90)), + med_spent=float(np.median(spent)), mean_back=float(got_back.mean()), + n_tickets=float(n_tickets.mean()), n_pass=float(n_pass.mean()), + peak_notional=float(np.median(peak_notional)), + contributed=float(start_usd + deposited.mean())) + bump(1) + return out + + +def p_wall_entro(res: dict, wall: float, giorni: int) -> float: + """P(il patrimonio PROPRIO tocca `wall` entro `giorni`) β€” obiettivo A.""" + h = res["hit"][wall] + return float(((h >= 0) & (h < giorni)).mean()) + + +def anni_med(res: dict, wall: float) -> tuple[float, float]: + """(mediana INCONDIZIONATA degli anni al traguardo, P(arrivo entro la simulazione)). + Non-arrivo = +inf, MAI -1 (lezione 07/08).""" + h = res["hit"][wall] + y = np.where(h >= 0, h / 365.0, np.inf) + return float(np.median(y)), float(np.isfinite(y).mean()) + + +# =========================================================================== +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + + print("=" * 104) + print(" Β§58 OBIETTIVO-DIRETTO β€” ottimizzare `P(reddito >= 50 EUR/g entro H)`, e non altro") + print("=" * 104) + print(f" {'MODALITA RIDOTTA (--fast)' if FAST else 'griglia piena'} | " + f"{N_PATHS} percorsi | orizzonti {ORIZZONTI_M} mesi") + + # ------------------------------------------------------------ Β§0 isolamento + hr("Β§0. ISOLAMENTO β€” questo script NON tocca la produzione (il cron gira dal working tree)") + prot = ["src", "config", "scripts/live", "tests", "data/paper_portfolio", "data/live"] + dirty = subprocess.run(["git", "status", "--porcelain", "--"] + prot, + cwd=ROOT, capture_output=True, text=True).stdout.strip() + print(f" git status sui percorsi protetti {prot}:") + print(f" {'PULITO β€” nessuna modifica' if not dirty else 'SPORCO: ' + dirty}") + if dirty: + raise SystemExit("La produzione risulta modificata: mi fermo.") + print(" Nessuna scrittura su disco in tutto lo script (solo stdout). Rete: nessuna.") + + # ------------------------------------------------------------ Β§1 repliche + hr("Β§1. REPLICHE β€” prima di un numero nuovo, la macchina deve riprodurre i vecchi") + ok: dict[str, bool] = {} + + # (R1) il muro L3 (fisco + funding), $313.143 β€” Β§36, replicato al dollaro da Β§57 + print("\n R1 IL MURO della lente congiunta L3 (funding dentro, fisco dentro, de-luck x0,89)") + rateF = GC.funding_rate() + if rateF is None: + raise SystemExit("cache funding assente: senza di essa nessun numero di questo filone " + "sarebbe confrontabile coi pubblicati.") + bookA = FD.book_75_25(None) + bookF = FD.book_75_25(rateF).reindex(bookA.index) + idxF = pd.DatetimeIndex(bookA.index) + rL3 = np.asarray(bookF.values, float) + rL3 = rL3 - (1.0 - DELUCK) * rL3.mean() + prel = CC.TARGET_EUR_DAY * 365 * CC.EURUSD + t = time.time() + perp, MURO = PN.muro(rL3, prel, TAX, PATR) + ok["R1"] = abs(MURO - PUB["muro_L3"]) < 1.0 + print(f" serie {len(rL3)} giorni {idxF.min().date()} -> {idxF.max().date()} | " + f"drift {rL3.mean()*365:.4%} (pubbl. {PUB['drift_L3']:.2%})") + print(f" perpetua {perp:.4%} (pubbl. {PUB['perp_L3']:.2%}) muro ${MURO:,.0f} " + f"(pubbl. ${PUB['muro_L3']:,.0f}) -> " + f"{'ESATTO AL DOLLARO' if ok['R1'] else 'DIVERSO'} ({time.time()-t:.0f}s)") + bump(1) + tick("R1") + + # ------------------------------------------------- il pannello comune + print("\n Il pannello comune (identico a Β§29/Β§32/Β§37): U13 Binance, W-LONG 2021+.") + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + XSP = {ph: PR.xs_tuples(p13, Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13], + Lb.reindex(p13.index)[X.U13], phase=ph) for ph in PHASES_SUB} + xs0 = XSP[PHASES_SUB[0]] + base = pc.crypto_daily_tuples(1.0, 0.0).index.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + I = base[base >= max(first13, pd.Timestamp("2021-01-01", tz="UTC"))] + pc.DELUCK = DELUCK + ev_b = PA.boot_idx(len(I), 365, N_PATHS_J, seed=8220) + fu_b = PA.boot_idx(len(I), 365, N_PATHS_J, seed=8221) + xsf = GC.xs_funding_measure(verbose=False) + xs_ann = xsf["ann"] + print(f" {len(I)} giorni {I[0].date()} -> {I[-1].date()} | " + f"XS01 paga {xs_ann*100:+.2f}%/anno di funding (misurato Β§37)") + tick("pannello") + + # (R2) la cella d'ancora MEDIANA e il numero operativo 2,6% [1,5%, 4,7%] + print(f"\n R2 IL NUMERO OPERATIVO DI Β§37 β€” {len(OFFSETS_SUB)} offset x {len(PHASES_SUB)} " + f"fasi = {len(OFFSETS_SUB)*len(PHASES_SUB)} celle, funding DENTRO, leva argmax-J") + print(" (sottoinsieme d'ancore dichiarato PRIMA; Β§37 ne usa 23x3, qui se ne usano " + f"{len(OFFSETS_SUB)}x{len(PHASES_SUB)} per il tempo macchina)") + cells = [] + for off in OFFSETS_SUB: + fA = GC.fvec_for(W_A, I, rateF, off, xs_ann) + for ph in PHASES_SUB: + R0, G0 = AP.rg_off(W_A, I, XSP[ph], off) + Rx = R0 - fA + jb, lb = -1.0, None + for lv in PR.LEVS: + j = PR.obj_rg(Rx, G0, lv, ev_b, fu_b)["J"] + if j > jb: + jb, lb = j, lv + bump(len(PR.LEVS)) + lad = GC.ladder(Rx, G0, N_PATHS, lb, SEED_LAD, refund=False) + cells.append(dict(off=off, ph=ph, J=jb, lev=lb, p50=lad["p_target"], + pz=lad["p_zero"], med=lad["med"])) + bump(1) + C = pd.DataFrame(cells) + med_p, p10, p90 = C["p50"].median(), np.percentile(C["p50"], 10), np.percentile(C["p50"], 90) + ok["R2"] = PUB["p50g_p10"] <= med_p <= PUB["p50g_p90"] + print(f" P(>=50/g) mediana {med_p:.1%} banda [{p10:.1%}, {p90:.1%}] " + f"P(zero) {C['pz'].median():.1%}") + print(f" pubblicato Β§37: {PUB['p50g_fund']:.1%} [{PUB['p50g_p10']:.1%}, " + f"{PUB['p50g_p90']:.1%}], P(zero) {PUB['pzero_fund']:.1%} -> " + f"{'DENTRO LA BANDA' if ok['R2'] else 'FUORI'}") + k_med = int((C["p50"] - C["p50"].median()).abs().idxmin()) + OFF_M, PH_M, LEV_J = int(C.loc[k_med, "off"]), int(C.loc[k_med, "ph"]), float(C.loc[k_med, "lev"]) + print(f" CELLA D'ANCORA MEDIANA: offset {OFF_M} min, fase {PH_M}, leva argmax-J " + f"{LEV_J:.2f}x (non la canonica: 91Β° pctl, Β§32)") + fM = GC.fvec_for(W_A, I, rateF, OFF_M, xs_ann) + R_M0, G_M = AP.rg_off(W_A, I, XSP[PH_M], OFF_M) + R_M = R_M0 - fM + tick("R2") + + # il libro PROPRIO sullo stesso pannello + rb_full = FD.book_75_25(rateF) + if rb_full.index.tz is None: + rb_full.index = rb_full.index.tz_localize("UTC") + RB_LIVE = np.nan_to_num(rb_full.reindex(I).values.astype(float)) + fA_own = GC.fvec_for(W_A, I, rateF, OFF_M, xs_ann) + RB_WA = AP.rg_off(W_A, I, XSP[PH_M], OFF_M)[0] - fA_own + print(f"\n libro PROPRIO sul pannello comune:") + for lab, r in (("LIVE 75/25", RB_LIVE), ("W_A 38/38/25", RB_WA)): + print(f" {lab:<14} drift {r.mean()*365:>7.2%} vol {r.std()*365**.5:>6.2%} " + f"Sharpe {r.mean()*365/(r.std()*365**.5):>5.2f}") + + # bootstrap CONDIVISO (confronto appaiato fra tutte le politiche) + rngb = np.random.default_rng(SEED_BOOT) + nb = int(np.ceil(H_MAX_D / 20)) + st = rngb.integers(0, len(I) - 20, size=(N_PATHS, nb)) + BOOT = (st[:, :, None] + np.arange(20)[None, None, :]).reshape(N_PATHS, -1)[:, :H_MAX_D] + B36 = BOOT[:, :36 * 30] + print(f" bootstrap CONDIVISO: {N_PATHS} percorsi x {H_MAX_D} giorni, blocchi 20 -> " + f"OGNI confronto di questo filone e' APPAIATO") + + # (R3) bit-exactness: `uni` in modalita' ladder == `gate_c.ladder` + print("\n R3 BIT-EXACTNESS β€” `uni` in modalita' ladder deve essere `gate_c.ladder`") + ref = GC.ladder(R_M, G_M, N_PATHS, LEV_J, SEED_LAD, refund=False, boot=B36) + mine = uni(R_M, G_M, RB_LIVE, B36, lev_fu=LEV_J, n_max=pl.MAX_CONCURRENT, pref="grande", + rebuy=True, floor_usd=0.0, k_book=0.0, invest=False, dep_eur=0.0, + start_usd=pl.START_EUR * EURUSD, seed=SEED_LAD, refund=False, min_payout=0.0, + tax=0.0, patr=0.0, deposit_mode="ladder") + d1 = float(np.abs(ref["eur_day"] - mine["eur_day"]).max()) + d2 = float(np.abs(ref["cash"] - mine["cash"]).max()) + ok["R3"] = (d1 == 0.0 and d2 == 0.0) + print(f" max|dif| EUR/g = {d1:.3e} | max|dif| cassa = {d2:.3e} -> " + f"{'BIT-EXACT' if ok['R3'] else 'DIVERSO'}") + + # (R4) bit-exactness: `uni` senza conti == `PN.accumula` + print("\n R4 BIT-EXACTNESS β€” `uni` senza canale funded deve essere `PN.accumula`") + paths_own = (RB_LIVE - (1.0 - DELUCK) * RB_LIVE.mean())[B36] + a = PN.accumula(paths_own, 500.0, MURO, TAX, PATR, start=START_USD) + m = uni(R_M, G_M, RB_LIVE, B36, n_max=0, k_book=1.0, invest=True, dep_eur=500.0, + start_usd=START_USD, tax=TAX, patr=PATR, walls=(MURO,), + deposit_mode="accumula") + d3 = float(np.abs(a["cap"] - m["cash"]).max()) + d4 = int(np.abs(np.where(a["colpito"] >= 0, a["colpito"], -1) - m["hit"][MURO]).max()) + ok["R4"] = (d3 < 1e-6 and d4 == 0) + print(f" max|dif| capitale = ${d3:.3e} | max|dif| giorno di primo passaggio = {d4} -> " + f"{'BIT-EXACT' if ok['R4'] else 'DIVERSO'}") + print(f" (capitale mediano a 36 mesi ${np.median(m['cash']):,.0f}, " + f"P(muro) {float((m['hit'][MURO]>=0).mean()):.1%})") + + print(f"\n ESITO REPLICHE: {sum(ok.values())}/{len(ok)} β€” " + + ", ".join(f"{k}={'OK' if v else 'NO'}" for k, v in ok.items())) + if not all(ok.values()): + raise SystemExit("REPLICA INCOMPLETA: un numero nuovo sarebbe indistinguibile da un bug.") + tick("Β§1") + # ---- fine repliche; da qui i numeri NUOVI + + # muro coerente col PANNELLO su cui gira la simulazione (2021+), come sensibilita' + rI = RB_LIVE - (1.0 - DELUCK) * RB_LIVE.mean() + lo, hi = 0.0, 0.40 + for _ in range(13): + mid = (lo + hi) / 2 + sv = PN.sopravvivenza(rI, 1e6, 1e6 * mid, 20, TAX, PATR, n_paths=1500) + lo, hi = (mid, hi) if sv["p10_end"] >= 1e6 else (lo, mid) + MURO_I = prel / lo if lo > 0.002 else float("inf") + WALLS = (PUB["muro_p90"], MURO, PUB["muro_m1se"]) + print(f"\n IL BERSAGLIO (obiettivo A) β€” e la sua banda, propagata da Β§57:") + print(f" muro PRIMARIO (pubblicato, finestra piena 2019+): ${MURO:,.0f} " + f"(perpetua {perp:.2%})") + print(f" banda del drift +-1SE (Β§57): p90 ${PUB['muro_p90']:,.0f} ... " + f"-1SE ${PUB['muro_m1se']:,.0f}") + print(f" muro COERENTE col pannello 2021+ (drift {rI.mean()*365:.2%}): " + + (f"${MURO_I:,.0f}" if np.isfinite(MURO_I) else "IRRAGGIUNGIBILE (perpetua ~0)")) + print(" ⚠️ La simulazione gira sul pannello 2021+ (l'unico dove XS01 esiste) mentre il muro") + print(" pubblicato e' calcolato sulla finestra piena: la disparita' e' DICHIARATA e la") + print(" riga qui sopra la quantifica. Primaria resta la pubblicata, per confrontabilita'.") + tick("muro") + + def run(**kw) -> dict: + base = dict(lev_fu=1.0, n_max=0, pref="grande", rebuy=True, floor_usd=0.0, + k_book=1.0, invest=True, dep_eur=500.0, start_usd=START_USD, + seed=SEED_LAD, refund=True, min_payout=MIN_PAYOUT, tax=TAX, patr=PATR, + walls=WALLS, marks=MARKS, deposit_mode="ladder") + base.update(kw) + boot = base.pop("boot", BOOT) + return uni(base.pop("Ra", R_M), base.pop("Ga", G_M), base.pop("Rb", RB_LIVE), + boot, **base) + + # ====================================================================== + hr("Β§2. LE DUE DEFINIZIONI DI Β«REDDITO >= 50 EUR/gΒ», SULLA STESSA POLITICA E SUGLI STESSI PATH") + print(" A = RICCHEZZA: il patrimonio PROPRIO tocca il capitale-rendita (sostenibile).") + print(" B = FLUSSO : media dei payout incassati >= 50 EUR/g (la definizione dei numeri") + print(" pubblicati del canale funded β€” NON sostenibile).") + print(f" Versamento: EUR 500/mese (il piano in `config/live.json`), start ${START_USD:,.0f}.") + REF = { + "LIBRO (oggi: 75/25, k=1, nessun biglietto)": dict(n_max=0), + "PROP come pubblicato (6 conti, leva argmax-J, cassa FERMA)": + dict(n_max=6, lev_fu=LEV_J, invest=False, k_book=0.0, tax=0.0, patr=0.0), + "PROP + cassa che lavora sul libro (stessa leva)": + dict(n_max=6, lev_fu=LEV_J, invest=True, k_book=1.0), + } + refres = {k: run(**v) for k, v in REF.items()} + print(f"\n {'politica':<52} {'H':>5} {'A: P(muro)':>11} {'B: P(>=50/g)':>13} " + f"{'mediana $':>11} {'P(12}") + for lab, r in refres.items(): + for m in ORIZZONTI_M: + sn = r["snap"][m * 30] + print(f" {lab if m == ORIZZONTI_M[0] else '':<52} {m:>4}m " + f"{sn[f'A{MURO:.0f}']:>11.1%} {sn['p_flow']:>13.1%} " + f"${sn['med_cash']:>10,.0f} {sn['p_sotto']:>12.1%}") + print("\n πŸ“Œ Le due definizioni NON ordinano le politiche allo stesso modo, e a 36 mesi non") + print(" sono nemmeno commensurabili: sotto A il canale funded e' un ACCELERATORE di") + print(" capitale, sotto B e' l'unico modo di avere un flusso.") + tick("Β§2") + + # ====================================================================== + hr("Β§3. LA CURVA DI LEVA β€” dove sta l'argmax di CIASCUN obiettivo (attesa A1)") + print(f" Cella d'ancora mediana (offset {OFF_M}, fase {PH_M}), 6 conti, cassa che lavora,") + print(" EUR 500/mese. J e' calcolato con la STESSA `PR.obj_rg` di Β§29/Β§32/Β§37.") + print(f"\n {'leva':>6} {'J':>7} {'B: P(>=50/g) 36m':>17} {'A: P(muro) 120m':>16} " + f"{'P(zero)':>8} {'P(12} {'mediana $ 120m':>15}") + LEVS_ALL = PR.LEVS + curve = [] + for lv in LEVS_ALL: + j = PR.obj_rg(R_M, G_M, lv, ev_b, fu_b)["J"] + bump(1) + r = run(n_max=6, lev_fu=lv) + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + curve.append(dict(lev=lv, J=j, B36=s36["p_flow"], A120=s120[f"A{MURO:.0f}"], + pz=s36["p_zero"], psotto=s120["p_sotto"], med=s120["med_cash"])) + print(f" {lv:>6.3f} {j:>7.3f} {s36['p_flow']:>17.1%} {s120[f'A{MURO:.0f}']:>16.1%} " + f"{s36['p_zero']:>8.1%} {s120['p_sotto']:>12.1%} ${s120['med_cash']:>14,.0f}") + CU = pd.DataFrame(curve) + aJ = float(CU.loc[CU["J"].idxmax(), "lev"]) + aB = float(CU.loc[CU["B36"].idxmax(), "lev"]) + aA = float(CU.loc[CU["A120"].idxmax(), "lev"]) + print(f"\n argmax J = {aJ:.2f}x Β· argmax B(36m) = {aB:.2f}x Β· " + f"argmax A(120m) = {aA:.2f}x") + print(f" B alla leva argmax-J: {float(CU.loc[CU['J'].idxmax(),'B36']):.1%} -> " + f"B al proprio argmax: {CU['B36'].max():.1%} " + f"(fattore {CU['B36'].max()/max(float(CU.loc[CU['J'].idxmax(),'B36']),1e-9):.1f}x)") + print(f" ATTESA A1 ({'CONFERMATA' if aB > aJ and CU['B36'].max() > 2*float(CU.loc[CU['J'].idxmax(),'B36']) else 'PARZIALE/SMENTITA'}): " + "l'argmax di P sta a leva piu' alta di quello di J.") + tick("Β§3") + + # ====================================================================== + hr("Β§4. LO SPAZIO DI POLITICA, OTTIMIZZATO SU P DIRETTAMENTE (36 mesi, obiettivo B)") + grid = [dict(lev_fu=lv, n_max=nm, pref=pf, rebuy=rb) + for lv in LEVS_FU for nm in NMAX for pf in PREFS for rb in REBUY] + print(f" {len(grid)} celle dichiarate PRIMA: leva {LEVS_FU} x conti {NMAX} x " + f"taglia {PREFS} x rebuy {REBUY}") + print(f" fisse: libro proprio LIVE 75/25 k=1, cassa che lavora, floor $0, EUR 500/mese.") + rows = [] + for g in grid: + r = run(**g) + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + rows.append(dict(**g, B36=s36["p_flow"], B24=r["snap"][24 * 30]["p_flow"], + B60=r["snap"][60 * 30]["p_flow"], B120=s120["p_flow"], + A36=s36[f"A{MURO:.0f}"], A120=s120[f"A{MURO:.0f}"], + A120lo=s120[f"A{PUB['muro_p90']:.0f}"], + A120hi=s120[f"A{PUB['muro_m1se']:.0f}"], + pz=s36["p_zero"], psotto36=s36["p_sotto"], + rov36=s36["p_rovina"], med120=s120["med_cash"], + ntick=r["n_tickets"])) + G = pd.DataFrame(rows) + print(f"\n LE 8 MIGLIORI SU B(36 mesi) β€” e cosa costano sugli altri assi:") + print(f" {'leva':>5} {'conti':>6} {'taglia':>8} {'rebuy':>6} {'B 24m':>7} {'B 36m':>7} " + f"{'B 60m':>7} {'A 120m':>7} {'P(zero)':>8} {'P(13} {'bigl.':>6}") + for _, r in G.sort_values("B36", ascending=False).head(8).iterrows(): + print(f" {r['lev_fu']:>5.2f} {int(r['n_max']):>6} {r['pref']:>8} " + f"{'si' if r['rebuy'] else 'no':>6} {r['B24']:>7.1%} {r['B36']:>7.1%} " + f"{r['B60']:>7.1%} {r['A120']:>7.1%} {r['pz']:>8.1%} {r['psotto36']:>13.1%} " + f"{r['ntick']:>6.1f}") + print(f"\n LE 6 MIGLIORI SU A(120 mesi, muro pubblicato ${MURO:,.0f}):") + print(f" {'leva':>5} {'conti':>6} {'taglia':>8} {'rebuy':>6} {'A 36m':>7} {'A 120m':>7} " + f"{'A120 p90':>9} {'A120 -1SE':>10} {'B 36m':>7} {'mediana $ 120m':>15}") + for _, r in G.sort_values("A120", ascending=False).head(6).iterrows(): + print(f" {r['lev_fu']:>5.2f} {int(r['n_max']):>6} {r['pref']:>8} " + f"{'si' if r['rebuy'] else 'no':>6} {r['A36']:>7.1%} {r['A120']:>7.1%} " + f"{r['A120lo']:>9.1%} {r['A120hi']:>10.1%} {r['B36']:>7.1%} " + f"${r['med120']:>14,.0f}") + bestB = G.loc[G["B36"].idxmax()] + bestA = G.loc[G["A120"].idxmax()] + print(f"\n ARGMAX B(36m) = leva {bestB['lev_fu']:.2f}x, {int(bestB['n_max'])} conti, " + f"biglietto {bestB['pref']}, rebuy {'si' if bestB['rebuy'] else 'no'} -> " + f"{bestB['B36']:.1%}") + print(f" ARGMAX A(120m) = leva {bestA['lev_fu']:.2f}x, {int(bestA['n_max'])} conti, " + f"biglietto {bestA['pref']}, rebuy {'si' if bestA['rebuy'] else 'no'} -> " + f"{bestA['A120']:.1%}") + print(f" I due argmax {'COINCIDONO' if (bestB[['lev_fu','n_max','pref','rebuy']].tolist() == bestA[['lev_fu','n_max','pref','rebuy']].tolist()) else 'SONO DIVERSI'}.") + tick("Β§4") + + # ====================================================================== + hr("Β§5. LE DIMENSIONI RESTANTI β€” floor, versamento, leva del libro, ripartizione propria") + bp = dict(lev_fu=float(bestB["lev_fu"]), n_max=int(bestB["n_max"]), + pref=str(bestB["pref"]), rebuy=bool(bestB["rebuy"])) + print(f" Politica di riferimento: l'argmax di B {bp}") + print(f"\n (a) FLOOR β€” capitale proprio sotto cui non si compra un altro biglietto") + print(f" {'floor':>10} {'B 36m':>8} {'A 120m':>8} {'P(zero)':>9} {'mediana $ 120m':>15}") + for fl in (0.0, 2_000.0, 10_000.0): + r = run(floor_usd=fl, **bp) + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + print(f" ${fl:>9,.0f} {s36['p_flow']:>8.1%} {s120[f'A{MURO:.0f}']:>8.1%} " + f"{s36['p_zero']:>9.1%} ${s120['med_cash']:>14,.0f}") + print(f"\n (b) VERSAMENTO MENSILE β€” la leva che il progetto misura come dominante") + print(f" {'EUR/mese':>10} {'strada':>10} {'B 36m':>8} {'A 120m':>8} {'A 240m':>8} " + f"{'mediana $ 120m':>15}") + for dep in (0.0, 250.0, 500.0, 1_000.0): + for lab, kw in (("PROP", bp), ("LIBRO", dict(n_max=0))): + r = run(dep_eur=dep, **kw) + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + a240 = anni_med(r, MURO) + print(f" {dep:>10,.0f} {lab:>10} {s36['p_flow']:>8.1%} " + f"{s120[f'A{MURO:.0f}']:>8.1%} {a240[1]:>8.1%} ${s120['med_cash']:>14,.0f}") + print(" (la colonna 'A 240m' e' P(traguardo) sull'intera simulazione = 120 mesi: coincide") + print(" con A 120m β€” la si stampa per rendere esplicito che l'orizzonte massimo e' 10 anni)") + print(f"\n (c) LEVA DEL LIBRO PROPRIO β€” muro CONGELATO (primaria) e MOBILE (sensibilita')") + print(" ⚠️ Β§38: la chiave di scala NON ESISTE in `config/live.json`; Β§57: a k=1,50x il") + print(" peggior giorno e' 21,48% e la guardia dello scettico (<=20%) lo BOCCIA.") + print(f" {'k':>5} {'muro mobile':>13} {'A 120m congel.':>15} {'A 120m mobile':>14} " + f"{'B 36m':>7} {'mediana $ 120m':>15}") + for k in (1.00, 1.25, 1.50): + rk = rI * k + lo, hi = 0.0, 0.40 + for _ in range(11): + mid = (lo + hi) / 2 + sv = PN.sopravvivenza(np.asarray(rL3, float) * k, 1e6, 1e6 * mid, 20, TAX, PATR, + n_paths=1200) + lo, hi = (mid, hi) if sv["p10_end"] >= 1e6 else (lo, mid) + mk = prel / lo if lo > 0.002 else float("inf") + r = run(k_book=k, walls=(MURO, mk) if np.isfinite(mk) else (MURO,), **bp) + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + am = s120.get(f"A{mk:.0f}", float("nan")) if np.isfinite(mk) else float("nan") + print(f" {k:>5.2f} " + (f"${mk:>12,.0f}" if np.isfinite(mk) else f"{'IRRAGG.':>13}") + + f" {s120[f'A{MURO:.0f}']:>15.1%} " + + (f"{am:>14.1%}" if am == am else f"{'β€”':>14}") + + f" {s36['p_flow']:>7.1%} ${s120['med_cash']:>14,.0f}") + bump(1) + print(f"\n (d) RIPARTIZIONE DEL LIBRO PROPRIO β€” LIVE 75/25 contro W_A 38/38/25") + print(" (la seconda mette XS01 anche sul capitale proprio: Β§46 ha falsificato la soglia") + print(" dei $20k da min-order, ma la decisione di venue del 26/07 tiene tutto su") + print(" Deribit fino a $20k -> e' una sensibilita', non una proposta)") + print(f" {'libro proprio':>16} {'B 36m':>8} {'A 120m':>8} {'mediana $ 120m':>15}") + for lab, rbk in (("LIVE 75/25", RB_LIVE), ("W_A 38/38/25", RB_WA)): + r = run(Rb=rbk, **bp) + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + print(f" {lab:>16} {s36['p_flow']:>8.1%} {s120[f'A{MURO:.0f}']:>8.1%} " + f"${s120['med_cash']:>14,.0f}") + print(f"\n (e) LA CASSA CHE LAVORA β€” attesa A3") + print(f" {'cassa':>16} {'B 36m':>8} {'A 120m':>8} {'mediana $ 36m':>14} " + f"{'mediana $ 120m':>15}") + inv = {} + for lab, kw in (("ferma (pubbl.)", dict(invest=False, k_book=0.0, tax=0.0, patr=0.0)), + ("investita", dict(invest=True, k_book=1.0))): + r = run(**{**bp, **kw}) + inv[lab] = r + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + print(f" {lab:>16} {s36['p_flow']:>8.1%} {s120[f'A{MURO:.0f}']:>8.1%} " + f"${s36['med_cash']:>13,.0f} ${s120['med_cash']:>14,.0f}") + dB = (inv["investita"]["snap"][36 * 30]["p_flow"] + - inv["ferma (pubbl.)"]["snap"][36 * 30]["p_flow"]) + dA = (inv["investita"]["snap"][120 * 30][f"A{MURO:.0f}"] + - inv["ferma (pubbl.)"]["snap"][120 * 30][f"A{MURO:.0f}"]) + print(f" ATTESA A3: dB {dB*100:+.2f}pp, dA {dA*100:+.2f}pp -> " + f"{'CONFERMATA' if abs(dB) < 0.02 and dA > 0.02 else 'da leggere nel testo'}") + tick("Β§5") + + # ====================================================================== + hr("Β§6. QUANTO COSTA LA DIFFERENZA FRA L'OTTIMO E CIO' CHE IL PROGETTO FA OGGI") + oggi = run(n_max=0) + pubb = run(n_max=6, lev_fu=LEV_J, invest=False, k_book=0.0, tax=0.0, patr=0.0) + optB = run(**bp) + optA = run(lev_fu=float(bestA["lev_fu"]), n_max=int(bestA["n_max"]), + pref=str(bestA["pref"]), rebuy=bool(bestA["rebuy"])) + print(f" {'politica':<44} {'B 36m':>8} {'A 36m':>8} {'A 120m':>8} {'anni al muro':>13} " + f"{'P(16}") + for lab, r in (("OGGI: libro 75/25 k=1, nessun biglietto", oggi), + ("PUBBLICATA: 6 conti a leva argmax-J", pubb), + ("ARGMAX B (flusso)", optB), + ("ARGMAX A (ricchezza)", optA)): + s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30] + med, parr = anni_med(r, MURO) + print(f" {lab:<44} {s36['p_flow']:>8.1%} {s36[f'A{MURO:.0f}']:>8.1%} " + f"{s120[f'A{MURO:.0f}']:>8.1%} " + + (f"{med:>12.1f}a" if np.isfinite(med) else f"{'>10a':>13}") + + f" {s36['p_sotto']:>16.1%}") + dB36 = optB["snap"][36 * 30]["p_flow"] - oggi["snap"][36 * 30]["p_flow"] + dA120 = optA["snap"][120 * 30][f"A{MURO:.0f}"] - oggi["snap"][120 * 30][f"A{MURO:.0f}"] + print(f"\n COSTO DI NON OTTIMIZZARE: su B(36m) {dB36*100:+.1f}pp, " + f"su A(120m) {dA120*100:+.1f}pp.") + tick("Β§6") + + # ====================================================================== + hr("Β§7. LA DOMANDA DECISIVA β€” esiste una politica con P(>=50 EUR/g entro 36 mesi) > 20%?") + maxB = float(G["B36"].max()) + maxA36 = float(G["A36"].max()) + rowB = G.loc[G["B36"].idxmax()] + print(f" Sotto la definizione B (FLUSSO, quella dei numeri pubblicati):") + print(f" massimo su {len(G)} politiche = {maxB:.1%} " + f"(leva {rowB['lev_fu']:.2f}x, {int(rowB['n_max'])} conti, biglietto {rowB['pref']}, " + f"rebuy {'si' if rowB['rebuy'] else 'no'})") + print(f" ma con P(zero payout) {rowB['pz']:.1%} e P(finire sotto il versato) " + f"{rowB['psotto36']:.1%}") + print(f" -> {'SI, SOPRA IL 20%' if maxB > 0.20 else 'NO, SOTTO IL 20%'}") + print(f"\n Sotto la definizione A (RICCHEZZA sostenibile, il muro del progetto):") + print(f" massimo su {len(G)} politiche a 36 mesi = {maxA36:.2%} " + f"(a 120 mesi: {float(G['A120'].max()):.1%})") + print(f" -> {'SI' if maxA36 > 0.20 else 'NO'}, e non per poco: il muro e' " + f"${MURO:,.0f} e a 36 mesi il capitale mediano migliore e' " + f"${float(G['med120'].max()):,.0f} a 120 mesi.") + print(f"\n ATTESA A2 ({'CONFERMATA' if (maxB > 0.20 and maxA36 < 0.05) else 'SMENTITA'}): " + "Β«SI' sotto B e NO sotto AΒ».") + print(f" ATTESA A4 ({'CONFERMATA' if rowB['pz'] > 0.50 or rowB['psotto36'] > 0.50 else 'SMENTITA'}): " + "l'ottimo di P e' una politica ad alta rovina.") + + # il vincolo che fissa il massimo + print(f"\n QUALE VINCOLO FISSA IL MASSIMO β€” si toglie un vincolo per volta (obiettivo B, 36m):") + print(f" {'scenario':<46} {'B 36m':>8} {'delta':>8}") + base_b = float(rowB["B36"]) + scen = [] + r = run(start_usd=10_000.0, **bp); scen.append(("capitale iniziale $10.000 invece di $635", r)) + r = run(dep_eur=2_000.0, **bp); scen.append(("versamento EUR 2.000/mese invece di 500", r)) + cap_old = [F["cap"] for F in pl.FIRMS] + for F in pl.FIRMS: + F["cap"] *= 5 + r = run(**bp); scen.append(("cap per firm x5 ($1M / $2M)", r)) + for F, c in zip(pl.FIRMS, cap_old): + F["cap"] = c + dead_old = pl.FIRM_DEATH_PER_YEAR + pl.FIRM_DEATH_PER_YEAR = 0.0 + r = run(**bp); scen.append(("morte-firm 0%/anno invece di 10%", r)) + pl.FIRM_DEATH_PER_YEAR = dead_old + r = run(n_max=6, **{k: v for k, v in bp.items() if k != "n_max"}) + scen.append(("6 conti concorrenti (se non lo e' gia')", r)) + for lab, r in scen: + b = r["snap"][36 * 30]["p_flow"] + print(f" {lab:<46} {b:>8.1%} {(b-base_b)*100:>+7.1f}pp") + print(f" ATTESA A5: il vincolo binding e' il capitale che entra " + f"(riga 'versamento' e riga 'capitale iniziale').") + + # ====================================================================== + hr("VERDETTO") + eur_g_oggi = oggi["snap"][36 * 30]["med_cash"] + print(f" Trial contati al rialzo: {TRIALS}. Tempo {time.time()-T0:.0f}s. " + f"Percorsi {N_PATHS} (SE ~{100*np.sqrt(0.2*0.8/N_PATHS):.1f}pp su una P vicina al 20%).") + print(f"\n * L'obiettivo del progetto ha DUE definizioni operative in uso e danno risposte") + print(f" opposte alla stessa domanda. Con la definizione del canale funded (FLUSSO) il") + print(f" massimo a 36 mesi e' {maxB:.1%}; con quella dei muri e del piano (RICCHEZZA") + print(f" SOSTENIBILE) e' {maxA36:.2%}.") + print(f" * L'ottimo di P(>=50/g) NON e' l'ottimo di J: {CU['B36'].max():.1%} contro " + f"{float(CU.loc[CU['J'].idxmax(),'B36']):.1%} alla leva argmax-J.") + print(f" * In EUR/giorno, la mediana di cio' che il libro di oggi produce a 36 mesi resta") + print(f" ZERO (non preleva), e il capitale mediano e' ${eur_g_oggi:,.0f}: la rendita") + print(f" perpetua su quel capitale vale {perp*eur_g_oggi/365/EURUSD:.2f} EUR/giorno.") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_quasi_passati.py b/scripts/research/r0823b_quasi_passati.py new file mode 100644 index 0000000..de7bc91 --- /dev/null +++ b/scripts/research/r0823b_quasi_passati.py @@ -0,0 +1,964 @@ +"""Β§68 QUASI-PASSATI β€” i candidati morti per pochissimo, presi INSIEME, passano? + +DOMANDA +======= +In due mesi il progetto ha bocciato decine di candidati e molti sono morti *per un soffio*: +deflated-Sharpe 0,948 (tenore VRP, bocciato per 0,002), 0,947 (cella pubblicata DVOLSPREAD), +0,929 (STATARB same-sign), 0,905 (cella viva di PREVDAY), e `weights_tilt_null` fallito per +delta_insample -0,004 (XSR01). Nessuno ha mai chiesto se, MESSI INSIEME, formino qualcosa che +passa. La domanda e' legittima perche' sono meccanismi DIVERSI (opzioni, spread di vol, residuo +cross-asset, livelli del giorno prima, cross-section su 50 alt) -> la loro combinazione non e' +automaticamente ridondante. + +Ma e' ANCHE la forma classica del data-dredging: raccogliere i falliti finche' la media non passa. +IL COMPITO E' MISURARE QUALE DELLE DUE COSE E', NON TROVARE UN VINCITORE. + +REGOLA DI AMMISSIONE β€” DICHIARATA PRIMA DI MISURARE, NON CAMBIATA DOPO +===================================================================== +Entra nel paniere ogni candidato che soddisfa TUTTE E TRE: + (R1) NON e' nel libro LIVE ne' nel book di ricerca a 5 sleeve (= candidato respinto o in limbo); + (R2) l'esito pubblicato dista <= 0,05 dalla soglia del gate che lo ha fermato: + |DSR - 0,95| <= 0,05 OPPURE |delta_insample| <= 0,01 su `weights_tilt_null`; + (R3) la serie e' RICOSTRUIBILE da codice committato coi parametri congelati/pubblicati. +La rappresentante di ogni candidato e' la CELLA CONGELATA NEL MONITOR (o, se monitor non esiste, +la cella pubblicata). PESI UGUALI. NESSUNA selezione successiva: il leave-one-out e' diagnostica, +non una potatura. Nessun candidato viene aggiunto o tolto dopo aver visto un risultato. + +ATTESE A PRIORI (dichiarate qui PRIMA di misurare; a fine script si dice confermata/refutata) +============================================================================================= +A1 Il paniere avra' Sharpe piu' alto di OGNI singola gamba. E' aritmetica di diversificazione, + non evidenza: e' attesa, e da sola non decide niente. +A2 Il DSR analitico del paniere si RIBALTA col conteggio dei trial (lezione 30/07 + Β§10/Β§24): + PASS con N=K, FAIL con N=somma dei trial di famiglia. Verdetto pubblicato con la sensibilita'. +A3 Il paniere dei QUASI-PASSATI battera' quello degli SCARTATI NETTI in-sample (per costruzione: + i primi sono cellette scelte, i secondi no) MA il vantaggio si RIDURRA' sull'hold-out. +A4 Le gambe sono quasi scorrelate fra loro -> ampiezza effettiva vicina a K -> il paniere alza + lo Sharpe PER COSTRUZIONE senza contenere un solo edge nuovo. +A5 Il paniere "funziona" grazie a UNA gamba (PREVDAY, il piu' forte misurato in Β§50) -> il + leave-one-out lo mostrera'. +A6 Almeno una gamba e' STAT-MODE (XSR01: venue diverso, non Deribit) -> il paniere COSI' COM'E' + non e' un candidato al libro, a prescindere dal numero. +A7 Le gambe sono DECADUTE dalla loro pubblicazione (i numeri hanno 1-2 mesi): mi aspetto un + degrado, non un miglioramento. + +METODO / VINCOLI +================ +* SOLA LETTURA. Nessun file di produzione toccato, nessun ordine, nessuna chiave API. + Endpoint di rete: NESSUNO (tutto da parquet certificati + cache su disco). +* Le serie hanno date d'inizio diverse -> `combine_outer` con rinormalizzazione per riga, e la + COPERTURA PER COLONNA e' STAMPATA (prima diagnostica di un outer-join: il progetto ha gia' + pubblicato un muro sbagliato di 3x per averla saltata). +* Lente Sharpe: `altlib._sh` (365,25 g/anno) ovunque, dichiarata. +* Replica di un numero pubblicato PRIMA di produrne di nuovi (sezione 3). +* Controllo positivo obbligatorio: un rilevatore che non trova nulla e' indistinguibile da uno + rotto -> i null sono validati su casi in cui DEVONO scattare. + +USO + uv run python scripts/research/r0823b_quasi_passati.py [--draws 1000] [--force] +""" +from __future__ import annotations + +import argparse +import datetime as dt +import itertools +import math +import pickle +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (str(ROOT), str(ROOT / "scripts" / "research"), + str(ROOT / "scripts" / "research" / "alt"), + str(ROOT / "scripts" / "research" / "ortho")): + if _p not in sys.path: + sys.path.insert(0, _p) + +# --- produzione (IMPORTATA, mai riscritta) --------------------------------------------------- +from src.portfolio.portfolio import combine_outer, metrics # noqa: E402 +from src.live import book as LB # noqa: E402 + +# --- macchineria di ricerca gia' validata (riusata, non riscritta) --------------------------- +import altlib as al # noqa: E402 +import ortholib as ol # noqa: E402 +import orthogonal_signals as OS # noqa: E402 +import r0823_prevday_gate as PG # noqa: E402 (factory + griglia 336) +import r0730_vrp_tenor_gate as VT # noqa: E402 (cell_daily, griglia 72) +import r0725_statarb_multi as SM # noqa: E402 (meccanismo XSR congelato) +import r0725_statarb_basket_gate as BG # noqa: E402 (pair_frames / demean) +from r0726_dvolspread_gate import make_book as dvs_make_book, GRID as DVS_GRID # noqa: E402 + +HOLDOUT = al.HOLDOUT +SEED = 20260823 +T0 = time.time() + +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/qp") +SCRATCH = _SP if _SP.parent.parent.exists() else Path("/tmp/pythagoras_quasi") +CACHE = SCRATCH / "families_v1.pkl" +B3RD_CACHE = SCRATCH.parent / "b3rd" / "series_v3.pkl" # cache Β§50 (serie ancorate, funding IN) + +CAPITAL_TODAY = 635.0 +MIN_ORDER = 5.0 +BOOK_START = pd.Timestamp("2019-03-14", tz="UTC") +W_TP, W_SKH = LB.W_TP01, LB.W_SKH +TP_HOURS = tuple(range(24)) +SKH_OFFSETS = tuple(range(0, 690, 30)) +XS_FUNDING_DRAG_YR = 0.0145 # Β§37, dichiarato (XS paga funding, non lo incassa) + +# --- LE CELLE CONGELATE (rappresentanti dei 5 quasi-passati) --------------------------------- +FROZEN = { + "PREVDAY": dict(tf="1h", anchor=1, k=0.30, short=True, mh=0), + "DVOLSPREAD": dict(zwin=180, tanh_k=2.0, lw=0.6, zw=1.1, tgt=0.17, svw=60), + "DVS-PUB": dict(zwin=150, tanh_k=1.8, lw=0.45, zw=1.0, tgt=0.13, svw=50), + "VRP-TENORE": dict(tenor=10, sd=-0.28, ld=-0.05), + "STATARB": dict(W=45, sgn=+1), + "XSR01": dict(W=45, sgn=+1), +} +LEGS = ["PREVDAY", "DVOLSPREAD", "VRP-TENORE", "STATARB", "XSR01"] + +# --- trial DICHIARATI per famiglia, contati AL RIALZO (regola 30/07) ------------------------- +N_TRIALS = {"PREVDAY": 336, "DVOLSPREAD": 729, "VRP-TENORE": 72, "STATARB": 10, "XSR01": 10} + + +def sez(t: str) -> None: + print("\n" + "=" * 106) + print(f" {t}") + print("=" * 106) + + +def sub(t: str) -> None: + print("\n" + "-" * 106) + print(f" {t}") + print("-" * 106) + + +def clock() -> str: + return f"[{time.time() - T0:6.1f}s]" + + +def sh(s) -> float: + return al._sh(s) + + +def shH(s) -> float: + s = s[s.index >= HOLDOUT] + return al._sh(s) if len(s) > 30 else float("nan") + + +def shI(s) -> float: + s = s[s.index < HOLDOUT] + return al._sh(s) if len(s) > 30 else float("nan") + + +def dd(s) -> float: + return al._dd_ret(s) + + +# ============================================================================================= +# LE FAMIGLIE β€” ogni cella e' una serie di rendimenti netti GIORNALIERI (fee reali, no funding) +# ============================================================================================= +def fam_prevday() -> dict: + out = {} + for tf in PG.G_TF: + for a, k, s_, mh in itertools.product(PG.G_ANCHOR, PG.G_K, PG.G_SHORT, PG.G_MH): + key = (tf, a, k, s_, mh) + try: + fn = PG.factory(tf=tf, anchor=a, k=k, short=s_, mh=mh) + out[key] = al.candidate_daily(fn, tf=tf) + except Exception: + continue + return out + + +def fam_dvolspread() -> dict: + out = {} + keys = list(DVS_GRID) + for combo in itertools.product(*[DVS_GRID[k] for k in keys]): + kw = dict(zip(keys, combo)) + try: + out[tuple(kw[k] for k in keys)] = ol.eval_book(dvs_make_book(**kw))["daily"].dropna() + except Exception: + continue + return out + + +def fam_vrp() -> dict: + out = {} + for tn, sd_, ld in itertools.product(VT.TENORS, VT.SHORT_DELTAS, VT.LONG_DELTAS): + try: + s = VT.cell_daily(tn, sd_, ld) + if len(s) > 200: + out[(tn, sd_, ld)] = s + except Exception: + continue + return out + + +def _spread_daily(pos, j) -> pd.Series: + ev = OS.eval_spread(j, pos) + return pd.Series(ev["net"], index=pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True))) + + +def fam_statarb() -> dict: + j = OS.build_joint("1d") + out = {} + for W, sg in itertools.product((30, 45, 60, 90, 120), (1, -1)): + out[(W, sg)] = _spread_daily(OS.f_statarb_resid(W=W, sgn=sg)(j), j) + return out + + +def fam_xsr() -> dict: + """XSR01: meccanismo STATARB-RESID congelato sui 50 alt HL, posizioni DEMEANATE.""" + out = {} + W0, S0 = SM.W, SM.SGN + try: + for W, sg in itertools.product((30, 45, 60, 90, 120), (1, -1)): + SM.W, SM.SGN = W, sg + P, S = BG.pair_frames() + out[(W, sg)] = BG.basket_from_positions(P, S, demean=True) + finally: + SM.W, SM.SGN = W0, S0 + return out + + +def fam_ortho_rejects() -> dict: + """B4 β€” SCARTATI NETTI CROSS-MECCANISMO: 4 famiglie relative-value dell'onda ortho + (2026-06-21) che lo scorer INDURITO ha collassato (17/18 -> 1). Meccanismi DIVERSI fra loro + e diversi dai quasi-passati. Stesso dato, stesse fee, stessa lente.""" + j = OS.build_joint("1d") + out = {} + for nm in ("RATIO-MOM", "RATIO-REV", "RATIO-ACCEL", "VOLSPREAD"): + f, grid, _ = OS.FAMILIES[nm] + out[nm] = {tuple(sorted(p.items())): _spread_daily(f(**p)(j), j) for p in grid} + return out + + +def build_families(force: bool) -> dict: + if CACHE.exists() and not force: + print(f" {clock()} cache famiglie: {CACHE}") + return pickle.loads(CACHE.read_bytes()) + F = {} + for nm, fn in (("VRP-TENORE", fam_vrp), ("STATARB", fam_statarb), ("XSR01", fam_xsr), + ("DVOLSPREAD", fam_dvolspread), ("PREVDAY", fam_prevday)): + t = time.time() + F[nm] = fn() + print(f" {clock()} famiglia {nm:<12} {len(F[nm]):>4} celle ({time.time()-t:5.1f}s)") + t = time.time() + F["_ORTHO"] = fam_ortho_rejects() + print(f" {clock()} famiglie ortho (controllo B4): " + f"{ {k: len(v) for k, v in F['_ORTHO'].items()} } ({time.time()-t:.1f}s)") + SCRATCH.mkdir(parents=True, exist_ok=True) + CACHE.write_bytes(pickle.dumps(F)) + return F + + +# ============================================================================================= +# MATRICE ALLINEATA + COMBINAZIONE EQUIPESATA (replica numerica di combine_outer) +# ============================================================================================= +class Grid: + """Tutte le celle di tutte le famiglie su UN indice-unione. Il combinatore equipesato e' + la trasposizione numpy di `combine_outer` (verificata bit-exact in sezione 5).""" + + def __init__(self, fams: dict, lo=None): + idx = None + for f in fams.values(): + for s in f.values(): + idx = s.index if idx is None else idx.union(s.index) + if lo is not None: + idx = idx[idx >= lo] + self.idx = idx + self.cols, self.keys = {}, {} + mats = [] + for nm, f in fams.items(): + ks = list(f) + self.keys[nm] = ks + M = np.full((len(idx), len(ks)), np.nan) + for c, k in enumerate(ks): + M[:, c] = f[k].reindex(idx).values + self.cols[nm] = M + mats.append(M) + + def pick(self, sel: dict) -> np.ndarray: + return np.column_stack([self.cols[nm][:, self.keys[nm].index(k)] for nm, k in sel.items()]) + + def combo(self, M: np.ndarray, idx=None) -> pd.Series: + # `idx`: indice delle RIGHE di M. Default self.idx (M sull'intera unione); + # va passato esplicitamente quando M e' un sottoinsieme di righe, altrimenti + # la maschera `keep` e l'indice hanno lunghezze diverse. + ix = self.idx if idx is None else idx + if M.shape[0] != len(ix): + raise ValueError(f"combo: M ha {M.shape[0]} righe, l'indice ne ha {len(ix)}") + mask = ~np.isnan(M) + w = mask.astype(float) + rs = w.sum(axis=1, keepdims=True) + wn = np.divide(w, rs, out=np.zeros_like(w), where=rs > 0) + v = np.nansum(np.nan_to_num(M) * wn, axis=1) + keep = mask.any(axis=1) + return pd.Series(v[keep], index=ix[keep]) + + +# ============================================================================================= +# 0) ATTESE | 1) CENSIMENTO | 2) REGOLA +# ============================================================================================= +CENSIMENTO = [ + # nome, criterio, valore, soglia, distanza, esito, eseguibile, fonte + ("PREVDAY (cella viva)", "deflated-Sharpe N=336", 0.905, 0.95, -0.045, "FAIL", + "SI' β€” Deribit, netta col libro", "RESULTS Β§54"), + ("DVOLSPREAD (congelata)", "deflated-Sharpe N=729", 0.953, 0.95, +0.003, "PASS di misura", + "SI' β€” Deribit, netta col libro", "CLAUDE.md 26/07"), + ("DVOLSPREAD (pubblicata)", "deflated-Sharpe N=729", 0.947, 0.95, -0.003, "FAIL", + "SI' β€” Deribit", "CLAUDE.md 26/07"), + ("VRP-TENORE 10g -0,28/-0,05", "deflated-Sharpe N=72", 0.948, 0.95, -0.002, "FAIL", + "opzioni ETH, lotto ~$242", "CLAUDE.md 30/07"), + ("STATARB-RESID W45 sgn+1", "deflated-Sharpe same-sign", 0.929, 0.95, -0.021, "FAIL", + "SI' β€” 2 gambe BTC/ETH", "diario 2026-06-29"), + ("XSR01 (50 alt HL, demean)", "weights_tilt_null delta_IS", -0.004, 0.0, -0.004, "FAIL", + "NO β€” venue Hyperliquid, STAT-MODE", "CLAUDE.md 25/07"), +] +ESCLUSI = [ + ("BIN-FREQ (SKH01)", "DSR 0,977 PASS ma 3 FAIL su 4 gambe del criterio", + "non e' una gamba propria: e' una MODIFICA di uno sleeve in produzione (R3)"), + ("XS01-OOS", "DSR 0,342", "distanza 0,61 dalla soglia: e' uno scartato netto, non un quasi (R2)"), + ("STA05 (EWMA-cross)", "nessuna soglia numerica pubblicata", + "mori' su hold-out corto + corr 0,53 a TP01, non su un gate a soglia (R2)"), + ("DVOL-DIREZIONALE", "is_hedge = True", "criterio BINARIO: non esiste una distanza (R2)"), + ("EW-STR (tilt di pesi)", "pctl 94-100 fra 500 tilt", "e' un cambio di PESI, non un candidato (R1)"), + ("LOWVOL 19-major", "DSR 0,13", "scartato netto (R2)"), +] + + +def sezione1() -> None: + sez("1) CENSIMENTO DEI QUASI-PASSATI β€” criterio, valore, distanza dalla soglia") + print(f" {'candidato':<28}{'criterio':<28}{'val':>8}{'soglia':>8}{'dist':>8} {'esito':<16}" + f"{'eseguibile a $635'}") + for nm, cr, v, so, di, es, ex, fo in CENSIMENTO: + print(f" {nm:<28}{cr:<28}{v:>8.3f}{so:>8.2f}{di:>+8.3f} {es:<16}{ex}") + print(f"\n {'':28}{'':28}{'':8}{'':8}{'':8} fonti: " + + ", ".join(sorted({f for *_, f in CENSIMENTO}))) + sub("ESCLUSI dal paniere, e perche' (la regola e' dichiarata PRIMA, non adattata all'esito)") + for nm, dato, perche in ESCLUSI: + print(f" {nm:<24} {dato:<44} -> {perche}") + + +def sezione2() -> None: + sez("2) REGOLA DI AMMISSIONE β€” dichiarata prima, NON cambiata dopo") + print(" (R1) non e' nel libro live ne' nel book a 5 sleeve") + print(" (R2) |DSR - 0,95| <= 0,05 OPPURE |delta_insample| <= 0,01 su weights_tilt_null") + print(" (R3) serie ricostruibile da codice committato coi parametri congelati/pubblicati") + print(" Rappresentante = la cella CONGELATA NEL MONITOR (o la pubblicata se monitor assente).") + print(" PESI UGUALI. Nessuna selezione successiva. Il leave-one-out e' DIAGNOSTICA.") + print(f"\n -> entrano {len(LEGS)} gambe: " + ", ".join(LEGS)) + print(" ⚠️ DVOLSPREAD: due celle difendibili (congelata 0,953 / pubblicata 0,947). La regola") + print(" dice 'quella del monitor' = la CONGELATA; la pubblicata gira come SENSIBILITA'.") + print(f"\n trial dichiarati per famiglia (al RIALZO, regola 30/07): {N_TRIALS}") + print(f" SOMMA = {sum(N_TRIALS.values())} trial. E' questo il N del deflated-Sharpe del") + print(" paniere, NON K=5: un paniere di K candidati gia' selezionati si porta dietro TUTTI") + print(" i trial delle famiglie da cui vengono.") + + +# ============================================================================================= +# 3) REPLICHE β€” un numero gia' pubblicato prima di produrne di nuovi +# ============================================================================================= +def sezione3(F: dict) -> dict: + sez("3) REPLICHE β€” riprodurre i numeri PUBBLICATI prima di produrne di nuovi") + legs = {} + legs["PREVDAY"] = F["PREVDAY"][("1h", 1, 0.30, True, 0)] + kd = list(DVS_GRID) + legs["DVOLSPREAD"] = F["DVOLSPREAD"][tuple(FROZEN["DVOLSPREAD"][k] for k in kd)] + dvs_pub = F["DVOLSPREAD"][tuple(FROZEN["DVS-PUB"][k] for k in kd)] + legs["VRP-TENORE"] = F["VRP-TENORE"][(10, -0.28, -0.05)] + legs["STATARB"] = F["STATARB"][(45, 1)] + legs["XSR01"] = F["XSR01"][(45, 1)] + vrp_canon = F["VRP-TENORE"][(7, -0.28, -0.10)] + pv_blind = F["PREVDAY"][("4h", 1, 0.30, False, 0)] + + print(f" {'oggetto':<34}{'pubblicato':>26}{'ri-misurato oggi':>30} esito") + rows = [ + ("PREVDAY congelata FULL/HOLD (Β§54)", "1,236 / 1,297", + f"{sh(legs['PREVDAY']):.3f} / {shH(legs['PREVDAY']):.3f}"), + ("PREVDAY al buio 4h FULL/HOLD (Β§54)", "1,621 / 0,731", + f"{sh(pv_blind):.3f} / {shH(pv_blind):.3f}"), + ("DVOLSPREAD onesta FULL/HOLD (26/07)", "0,68 / 0,69", "vedi vintage sotto"), + ("VRP canonico 7g FULL (30/07)", "1,32", f"{sh(vrp_canon):.3f}"), + ("VRP 10g -0,28/-0,05 FULL (30/07)", "1,55", f"{sh(legs['VRP-TENORE']):.3f}"), + ] + for a, b, c in rows: + print(f" {a:<34}{b:>26}{c:>30} ") + + sub("3b) LE GAMBE AL VINTAGE DELLA LORO PUBBLICAZIONE β€” la replica vera") + print(" Ogni numero pubblicato ha una DATA. Confrontarlo con la serie di oggi non e' una") + print(" replica: e' una replica PIU' un mese di dati nuovi. Qui si separano le due cose.") + print(f"\n {'gamba':<22}{'vintage':>12}{'FULL pubbl.':>13}{'FULL vintage':>14}" + f"{'FULL oggi':>11}{'HOLD pubbl.':>13}{'HOLD vintage':>14}{'HOLD oggi':>11}") + vint = [("PREVDAY", "2026-08-23", "1,236", "1,297"), + ("DVOLSPREAD", "2026-07-26", "0,68", "0,69"), + ("DVS-PUB", "2026-07-26", "β€”", "0,93"), + ("VRP-TENORE", "2026-07-30", "1,55", "1,01"), + ("STATARB", "2026-06-29", "0,84", "β€”"), + ("XSR01", "2026-07-25", "1,82", "β€”")] + rep = {} + for nm, cut, pf, ph in vint: + s = dvs_pub if nm == "DVS-PUB" else legs[nm] + sv = s[s.index < cut] + rep[nm] = (sh(sv), shH(sv), sh(s), shH(s)) + print(f" {nm:<22}{cut:>12}{pf:>13}{sh(sv):>14.3f}{sh(s):>11.3f}" + f"{ph:>13}{shH(sv):>14.3f}{shH(s):>11.3f}") + print("\n βœ… ogni cifra 'vintage' e' la replica del 'pubblicato' accanto (la colonna 'oggi'") + print(" e' un numero NUOVO). Le due colonne insieme misurano il DECADIMENTO da allora.") + dec = [(nm, rep[nm][0] - float(pf.replace(",", "."))) + for nm, _, pf, _ in vint if pf != "β€”"] + return dict(legs=legs, dvs_pub=dvs_pub, vrp_canon=vrp_canon, pv_blind=pv_blind, rep=rep) + + +# ============================================================================================= +# 4) CORRELAZIONI E AMPIEZZA EFFETTIVA β€” prima dei rendimenti, come chiede la domanda +# ============================================================================================= +def eff_breadth(C: np.ndarray) -> float: + n = len(C) + off = C[~np.eye(n, dtype=bool)] + rbar = float(np.nanmean(off)) + return n / (1.0 + (n - 1) * rbar) if rbar > -1.0 / (n - 1) else float(n) + + +def sezione4(legs: dict) -> dict: + sez("4) CORRELAZIONI E AMPIEZZA EFFETTIVA β€” misurate PRIMA dei rendimenti del paniere") + print(" Se le gambe sono scorrelate, un paniere alza lo Sharpe PER COSTRUZIONE senza") + print(" contenere un solo edge nuovo. Questa tabella e' il contesto di ogni numero dopo.") + M = pd.concat(legs, axis=1, join="outer").sort_index() + Ci = M.corr() + print(f"\n matrice di correlazione (giorni in comune a coppie, no riempimento):\n") + print(" " + " " * 13 + "".join(f"{c[:11]:>13}" for c in Ci.columns)) + for r in Ci.index: + print(f" {r[:12]:<13}" + "".join(f"{Ci.loc[r, c]:>13.3f}" for c in Ci.columns)) + C = Ci.values + off = C[~np.eye(len(C), dtype=bool)] + nb = eff_breadth(C) + print(f"\n corr media fuori diagonale = {np.nanmean(off):+.4f} " + f"[min {np.nanmin(off):+.3f}, max {np.nanmax(off):+.3f}]") + print(f" AMPIEZZA EFFETTIVA (N / (1+(N-1)Β·rbar), la formula gia' usata dal progetto in") + print(f" r0725_statarb_basket_gate.eff_breadth) = {nb:.2f} su K = {len(C)} gambe.") + print(f" -> il paniere vale, in termini di scommesse indipendenti, {nb:.2f} gambe.") + tp = al.tp01_baseline_daily() + print(f"\n correlazione di ogni gamba al baseline TP01 (il libro e' 75% TP01):") + for k, s in legs.items(): + a, b = s.align(tp, join="inner") + print(f" {k:<14}{a.corr(b):+.3f} (giorni comuni {len(a)})") + return dict(corr=Ci, neff=nb) + + +# ============================================================================================= +# 5) IL PANIERE β€” copertura per colonna PRIMA di tutto +# ============================================================================================= +def sezione5(G: Grid, legs: dict) -> dict: + sez("5) IL PANIERE EQUIPESATO β€” e la sua COPERTURA PER COLONNA") + sel = {"PREVDAY": ("1h", 1, 0.30, True, 0), + "DVOLSPREAD": tuple(FROZEN["DVOLSPREAD"][k] for k in DVS_GRID), + "VRP-TENORE": (10, -0.28, -0.05), "STATARB": (45, 1), "XSR01": (45, 1)} + M = G.pick(sel) + B = G.combo(M) + + # --- bit-exact contro la produzione ------------------------------------------------------ + Bp = combine_outer({k: legs[k] for k in LEGS}, {k: 1.0 for k in LEGS}, lo=G.idx[0]) + a, b = B.align(Bp, join="outer") + mx = float(np.nanmax(np.abs(a.fillna(0) - b.fillna(0)))) + print(f" βœ… combinatore numpy == `combine_outer` di produzione: max|diff| = {mx:.3e} " + f"(n {len(B)} vs {len(Bp)})") + + sub("5a) COPERTURA PER COLONNA β€” la prima diagnostica di un outer-join") + print(" Il progetto ha gia' pubblicato un muro sbagliato di 3x per aver saltato questa") + print(" tabella (r0726_wall_fixedpoint: una colonna al 100% accanto a colonne basse).") + print(f"\n {'gamba':<14}{'primo giorno':>14}{'giorni':>9}{'copertura':>11}" + f"{'barre ATTIVE':>14}{'attive/cal.':>12}") + cov = {} + for i, nm in enumerate(LEGS): + col = M[:, i] + n_ok = int(np.sum(~np.isnan(col))) + act = int(np.sum(np.nan_to_num(col) != 0.0)) + first = G.idx[np.argmax(~np.isnan(col))] + cov[nm] = n_ok / len(G.idx) + print(f" {nm:<14}{str(first.date()):>14}{n_ok:>9}{n_ok/len(G.idx):>10.1%}" + f"{act:>14}{act/max(1,n_ok):>12.1%}") + print(f" {'UNIONE':<14}{str(G.idx[0].date()):>14}{len(G.idx):>9}{1.0:>10.1%}") + n_legs = (~np.isnan(M)).sum(axis=1) + print(f"\n gambe VIVE per giorno: " + + " ".join(f"{k}: {100*np.mean(n_legs==k):.1f}%" for k in range(1, 6))) + print(" 🚨 il paniere e' a 5 gambe solo nel " f"{100*np.mean(n_legs==5):.1f}% dei giorni: " + "prima del 2024 XSR01 non esiste, prima del 2021 nemmeno VRP.") + print(" ⚠️ e la colonna 'attive/cal.' dice un'altra cosa ancora: DVOLSPREAD e VRP hanno") + print(" zeri STRUTTURALI (flat senza DVOL / settimanale su griglia giornaliera) -> un") + print(" giorno presente non e' un giorno di rischio (lezione 'barre attive', 26/07).") + + common = G.idx[n_legs == 5] + Bc = G.combo(M[(n_legs == 5), :], common) if len(common) > 60 else None + + sub("5b) METRICHE DEL PANIERE β€” due lenti dichiarate") + print(f" {'lente':<28}{'n':>7}{'ShFULL':>9}{'ShIS':>9}{'ShHOLD':>9}{'CAGR':>9}{'maxDD':>9}") + for nm, s in (("UNIONE (rinormalizzata)", B), ("FINESTRA COMUNE (5/5 gambe)", Bc)): + if s is None: + continue + m = metrics(s) + print(f" {nm:<28}{len(s):>7}{sh(s):>9.3f}{shI(s):>9.3f}{shH(s):>9.3f}" + f"{100*m['cagr']:>8.2f}%{100*m['maxdd']:>8.2f}%") + print(f"\n {'gamba sola':<28}{'n':>7}{'ShFULL':>9}{'ShIS':>9}{'ShHOLD':>9}{'CAGR':>9}{'maxDD':>9}") + for nm in LEGS: + s = legs[nm] + m = metrics(s) + print(f" {nm:<28}{len(s):>7}{sh(s):>9.3f}{shI(s):>9.3f}{shH(s):>9.3f}" + f"{100*m['cagr']:>8.2f}%{100*m['maxdd']:>8.2f}%") + print(f"\n A1: il paniere batte OGNI gamba sul FULL? " + f"{'SI' if sh(B) > max(sh(legs[n]) for n in LEGS) else 'NO'} " + f"(paniere {sh(B):.3f} vs miglior gamba {max(sh(legs[n]) for n in LEGS):.3f})") + + sub("5c) PER ANNO β€” dove sta il rendimento del paniere") + yr = {} + for y, g in B.groupby(B.index.year): + yr[y] = (sh(g), float(np.prod(1 + g.values) - 1), len(g)) + print(f" {'anno':>6}{'Sharpe':>10}{'ritorno':>11}{'giorni':>9} gambe vive (media)") + for y in sorted(yr): + mm = (G.idx.year == y) + print(f" {y:>6}{yr[y][0]:>10.2f}{100*yr[y][1]:>10.1f}%{yr[y][2]:>9}" + f" {n_legs[mm].mean():.2f}") + + sub("5d) MARGINALE vs TP01 (lo scorer indurito del progetto, non riscritto)") + mg = al.marginal_vs_tp01(B) + for k in ("verdict", "corr_full", "corr_hold", "alpha_ann", "beta", "has_insample_edge", + "is_hedge", "robust_oos", "beats_noise_null", "uplift_full_w25", "uplift_hold_w25"): + if k in mg: + print(f" {k:<22}{mg[k]}") + return dict(B=B, Bc=Bc, sel=sel, M=M, cov=cov, n_legs=n_legs, marg=mg) + + +# ============================================================================================= +# 6) IL CONTO DEI TRIAL β€” DSR analitico a piu' partizioni + sensibilita' +# ============================================================================================= +def dsr_of(series: pd.Series, trials: list[float]) -> tuple[float, float, float]: + d, sr0 = al.deflated_sharpe(sh(series), trials, series) + return d, sr0, float(np.std(np.asarray(trials, float), ddof=1)) / math.sqrt(365.25) + + +def sezione6(F: dict, R5: dict, legs: dict) -> dict: + sez("6) IL CONTO DEI TRIAL β€” deflated-Sharpe del PANIERE, e la sua sensibilita'") + B = R5["B"] + fam_full = {nm: [sh(s) for s in F[nm].values()] for nm in LEGS} + pooled = [x for nm in LEGS for x in fam_full[nm]] + print(f" Sharpe FULL del paniere = {sh(B):.4f}") + print(f"\n {'partizione':<52}{'N':>7}{'sd(trial)/g':>13}{'null max Sh':>13}{'DSR':>8} esito") + parts = [ + ("P1 N = K (le 5 gambe) β€” SBAGLIATA, la riporto per taratura", + [sh(legs[n]) for n in LEGS]), + ("P2 N = somma dei trial delle famiglie (la regola del brief)", pooled), + ("P3 N = famiglia PIU' LARGA sola (DVOLSPREAD 729)", fam_full["DVOLSPREAD"]), + ("P4 N = pooled + screen d'ondata (+168 ORTHO-SCREEN)", + pooled + [sh(s) for f in F["_ORTHO"].values() for s in f.values()]), + ("P5 N = solo le 5 celle scelte, varianza di famiglia pooled", + list(np.random.default_rng(1).choice(pooled, size=5, replace=False))), + ] + out = {} + for nm, tr in parts: + d, sr0, sdt = dsr_of(B, tr) + out[nm] = d + print(f" {nm:<52}{len(tr):>7}{sdt:>13.5f}{sr0:>13.3f}{d:>8.3f} " + f"{'PASS' if d >= 0.95 else 'FAIL'}") + print("\n 🚨 Il verdetto NON e' stabile: cambia con la partizione, come il progetto ha gia'") + print(" misurato il 30/07 (VRP: 0,983 PASS a N=8 / 0,948 FAIL a N=72 / 0,909 a N=360) e") + print(" il 22/08 (Β§24: la stessa griglia da' verdetti opposti a seconda di come si parte).") + print(" πŸ“Œ E c'e' un difetto STRUTTURALE piu' grave, che vale la pena dire in chiaro:") + print(" `deflated_sharpe` stima il massimo-dal-rumore dalla varianza degli Sharpe dei") + print(" TRIAL SINGOLI. Un PANIERE non e' un trial singolo: ha Sharpe piu' alto per pura") + print(" diversificazione. Confrontarlo con una sr0 tarata su celle singole lo FAVORISCE") + print(" sistematicamente -> il DSR analitico di un paniere e' OTTIMISTA a ogni N.") + print(" Il test giusto e' EMPIRICO: la distribuzione dei PANIERI (sezione 7).") + # sensibilita': quale sd(trial) ribalta il verdetto + lo, hi = 1e-4, 1.0 + for _ in range(60): + mid = 0.5 * (lo + hi) + scaled = list(np.asarray(pooled) * (mid / (np.std(pooled, ddof=1) / math.sqrt(365.25))) + ) if np.std(pooled, ddof=1) > 0 else pooled + d, _, _ = dsr_of(B, scaled) + if d >= 0.95: + lo = mid + else: + hi = mid + print(f"\n sensibilita': con N={len(pooled)} il verdetto si ribalta a " + f"sd(trial giornaliera) = {0.5*(lo+hi):.5f}; misurata = " + f"{np.std(pooled, ddof=1)/math.sqrt(365.25):.5f}.") + return out + + +# ============================================================================================= +# 7) IL CONTROLLO β€” CIO' CHE DECIDE +# ============================================================================================= +def sezione7(F: dict, G: Grid, R5: dict, draws: int) -> dict: + sez("7) IL CONTROLLO β€” panieri di SCARTATI NETTI. E' QUESTO IL TEST, non lo Sharpe del paniere") + B = R5["B"] + rng = np.random.default_rng(SEED) + real = (sh(B), shI(B), shH(B), dd(B)) + + # in-sample ranking per famiglia (mai l'hold-out: sarebbe selection-on-holdout) + rank = {} + for nm in LEGS: + ks = G.keys[nm] + vals = np.array([shI(F[nm][k]) for k in ks]) + rank[nm] = dict(keys=ks, ins=vals, order=np.argsort(np.nan_to_num(vals, nan=-9))) + frozen_idx = {nm: G.keys[nm].index(R5["sel"][nm]) for nm in LEGS} + print(" rango IN-SAMPLE della cella congelata dentro la propria famiglia " + "(mai l'hold-out: sarebbe selection-on-holdout)") + for nm in LEGS: + o = list(rank[nm]["order"][::-1]) + r = o.index(frozen_idx[nm]) + 1 + print(f" {nm:<14}{r:>4} / {len(o):<5} (pctl in-sample " + f"{100*(1-(r-1)/len(o)):.0f}Β°, ShIS {rank[nm]['ins'][frozen_idx[nm]]:+.3f})") + + def draw_basket(picker) -> tuple: + cols = [] + for nm in LEGS: + cols.append(G.cols[nm][:, picker(nm)]) + M = np.column_stack(cols) + s = G.combo(M) + return sh(s), shI(s), shH(s), dd(s) + + def bottom(nm): + o = rank[nm]["order"] # crescente: i primi sono i peggiori in-sample + return int(rng.choice(o[:max(1, len(o) // 2)])) + + def matched(nm): + o = list(rank[nm]["order"][::-1]) + r = o.index(frozen_idx[nm]) / max(1, len(o) - 1) + cand = [i for i in range(len(o)) if abs(i / max(1, len(o) - 1) - r) <= 0.10] + return int(o[int(rng.choice(cand))]) + + def anyc(nm): + return int(rng.integers(len(G.keys[nm]))) + + res = {} + for tag, pick in (("B1 SCARTATI NETTI (meta' peggiore in-sample, stesse famiglie)", bottom), + ("B2 RANGO APPAIATO (stesso pctl in-sample +-10pp)", matched), + ("B0 CELLA A CASO (qualunque cella della famiglia)", anyc)): + t = time.time() + arr = np.array([draw_basket(pick) for _ in range(draws)]) + res[tag] = arr + print(f"\n {tag} ({draws} panieri, {time.time()-t:.1f}s)") + print(f" {'metrica':<10}{'reale':>9}{'p10':>9}{'mediana':>9}{'p90':>9}{'max':>9}" + f"{'pctl del reale':>17}") + for i, mn in enumerate(("ShFULL", "ShIS", "ShHOLD", "maxDD")): + c = arr[:, i] + pc = 100.0 * float((c < real[i]).mean()) + print(f" {mn:<10}{real[i]:>9.3f}{np.percentile(c,10):>9.3f}" + f"{np.median(c):>9.3f}{np.percentile(c,90):>9.3f}{np.max(c):>9.3f}" + f"{pc:>16.1f}Β°") + + # --- B3: null di diversificazione (marginali CONSERVATE, correlazione distrutta) ---------- + sub("B3 NULL DI DIVERSIFICAZIONE β€” stesse gambe, correlazione incrociata DISTRUTTA") + print(" Ogni gamba viene ruotata circolarmente di un lag casuale >= 60 giorni: lo Sharpe") + print(" MARGINALE di ciascuna resta IDENTICO, sparisce solo la relazione fra gambe (e con") + print(" TP01). Se il paniere reale non batte questa distribuzione, non contiene nulla oltre") + print(" a 'K serie con quei marginali, quasi scorrelate'. Turnover invariato -> fee-neutrale.") + M0 = R5["M"] + arr = [] + for _ in range(draws): + cols = [] + for i in range(M0.shape[1]): + col = M0[:, i] + ok = ~np.isnan(col) + v = col[ok].copy() + v = np.roll(v, int(rng.integers(60, max(61, len(v) - 60)))) + nc = np.full_like(col, np.nan) + nc[ok] = v + cols.append(nc) + s = G.combo(np.column_stack(cols)) + arr.append((sh(s), shI(s), shH(s), dd(s))) + arr = np.array(arr) + res["B3"] = arr + print(f"\n {'metrica':<10}{'reale':>9}{'p10':>9}{'mediana':>9}{'p90':>9}{'max':>9}" + f"{'pctl del reale':>17}") + for i, mn in enumerate(("ShFULL", "ShIS", "ShHOLD", "maxDD")): + c = arr[:, i] + print(f" {mn:<10}{real[i]:>9.3f}{np.percentile(c,10):>9.3f}{np.median(c):>9.3f}" + f"{np.percentile(c,90):>9.3f}{np.max(c):>9.3f}" + f"{100.0*float((c16.1f}Β°") + + # --- B4: cross-meccanismo, scartati netti veri -------------------------------------------- + sub("B4 SCARTATI NETTI CROSS-MECCANISMO β€” 4 famiglie relative-value refutate dall'onda ortho") + print(" RATIO-MOM / RATIO-REV / RATIO-ACCEL / VOLSPREAD: meccanismi DIVERSI fra loro e dai") + print(" quasi-passati, tutti collassati dallo scorer indurito (17/18 -> 1, 21/06). Stessa") + print(" selezione dei quasi-passati (miglior cella IN-SAMPLE di ogni famiglia) + estrazioni.") + OF = F["_ORTHO"] + Go = Grid(OF) + best = {} + for nm, f in OF.items(): + ks = list(f) + best[nm] = ks[int(np.argmax([np.nan_to_num(shI(f[k]), nan=-9) for k in ks]))] + Mb = Go.pick(best) + Bo = Go.combo(Mb) + print(f"\n paniere dei 4 SCARTATI NETTI, celle scelte in-sample: ShFULL {sh(Bo):+.3f} " + f"ShIS {shI(Bo):+.3f} ShHOLD {shH(Bo):+.3f} maxDD {100*dd(Bo):.1f}% n {len(Bo)}") + Cb = pd.concat({k: OF[k][v] for k, v in best.items()}, axis=1).corr().values + print(f" corr media fuori diagonale {np.nanmean(Cb[~np.eye(len(Cb),dtype=bool)]):+.3f} -> " + f"ampiezza effettiva {eff_breadth(Cb):.2f} su 4") + arr = [] + for _ in range(draws): + s = Go.combo(np.column_stack([Go.cols[nm][:, int(rng.integers(len(Go.keys[nm])))] + for nm in OF])) + arr.append((sh(s), shI(s), shH(s), dd(s))) + arr = np.array(arr) + res["B4"] = arr + print(f"\n {draws} panieri a cella casuale dalle stesse 4 famiglie refutate:") + print(f" {'metrica':<10}{'quasi-pass.':>13}{'p10':>9}{'mediana':>9}{'p90':>9}{'max':>9}" + f"{'pctl dei quasi':>17}") + for i, mn in enumerate(("ShFULL", "ShIS", "ShHOLD", "maxDD")): + c = arr[:, i] + print(f" {mn:<10}{real[i]:>13.3f}{np.percentile(c,10):>9.3f}{np.median(c):>9.3f}" + f"{np.percentile(c,90):>9.3f}{np.max(c):>9.3f}" + f"{100.0*float((c16.1f}Β°") + + # --- CONTROLLO POSITIVO ------------------------------------------------------------------- + sub("CONTROLLO POSITIVO β€” un rilevatore che non segnala e' indistinguibile da uno rotto") + print(" Se il confronto 'reale vs panieri casuali' avesse potenza ZERO, tutto quanto sopra") + print(" sarebbe privo di significato. Lo si prova costruendo un paniere che DEVE stare in") + print(" cima: la MIGLIOR cella in-sample di ognuna delle 5 famiglie.") + bestQ = {} + for nm in LEGS: + o = rank[nm]["order"] + bestQ[nm] = G.keys[nm][int(o[-1])] + Bb = G.combo(G.pick(bestQ)) + B0 = res["B0 CELLA A CASO (qualunque cella della famiglia)"] + pIS = 100.0 * float((B0[:, 1] < shI(Bb)).mean()) + print(f" paniere 'migliori in-sample': ShIS {shI(Bb):+.3f} -> pctl {pIS:.1f}deg della") + print(f" distribuzione B0 (atteso ~100deg se il test ha potenza: e' il massimo per") + print(f" costruzione, quindi un pctl basso significherebbe rilevatore ROTTO).") + print(f" Il suo ShHOLD e' {shH(Bb):+.3f} contro mediana B0 {np.median(B0[:,2]):+.3f}:") + print(f" e' QUESTO il pezzo di scelta in-sample che sopravvive fuori campione.") + if pIS < 90.0: + print(" RILEVATORE SOSPETTO: il paniere costruito per stare in cima NON ci sta.") + return dict(res=res, real=real, best_is=Bb, ortho=Bo, rank=rank, frozen_idx=frozen_idx) + + +# ============================================================================================= +# 8) LEAVE-ONE-OUT +# ============================================================================================= +def sezione8(G: Grid, R5: dict) -> dict: + sez("8) LEAVE-ONE-OUT β€” se il paniere 'funziona' grazie a UNA gamba, non e' un paniere") + B = R5["B"] + base = (sh(B), shI(B), shH(B), dd(B)) + print(f" paniere completo: ShFULL {base[0]:.3f} ShIS {base[1]:.3f} ShHOLD {base[2]:.3f} " + f"maxDD {100*base[3]:.2f}%") + print(f"\n {'tolta':<14}{'ShFULL':>9}{'dShFULL':>10}{'ShHOLD':>9}{'dShHOLD':>10}" + f"{'maxDD':>9}{'d maxDD pp':>12}") + out = {} + for nm in LEGS: + keep = [k for k in LEGS if k != nm] + s = G.combo(G.pick({k: R5["sel"][k] for k in keep})) + out[nm] = (sh(s), shH(s), dd(s)) + print(f" {nm:<14}{sh(s):>9.3f}{sh(s)-base[0]:>+10.3f}{shH(s):>9.3f}" + f"{shH(s)-base[2]:>+10.3f}{100*dd(s):>8.2f}%{100*(dd(s)-base[3]):>+12.2f}") + worst = min(out, key=lambda k: out[k][0]) + print(f"\n la gamba il cui ritiro costa di piu' sul FULL: {worst} " + f"({out[worst][0]-base[0]:+.3f})") + return out + + +# ============================================================================================= +# 9) IL PANIERE COME TERZO SLEEVE DEL LIBRO LIVE β€” banda d'ancora, funding dentro +# ============================================================================================= +def sezione9(R5: dict, legs: dict, draws: int) -> dict: + sez("9) IL PANIERE COME TERZO SLEEVE DEL LIBRO LIVE β€” e batte la sua MIGLIOR gamba?") + if not B3RD_CACHE.exists(): + print(f" ⚠️ cache Β§50 assente ({B3RD_CACHE}) -> sezione SALTATA. Non e' un risultato.") + return {} + S = pickle.loads(B3RD_CACHE.read_bytes()) + print(f" cache Β§50 riusata (serie ancorate, FUNDING DENTRO): {list(S)[:8]}...") + + # replica del baseline Β§50 prima di ogni delta + rng = np.random.default_rng(SEED) + hs = rng.choice(TP_HOURS, size=draws) + os_ = rng.choice(SKH_OFFSETS, size=draws) + hs_sa = rng.choice(TP_HOURS, size=draws) + base, dPV, dBK, dBK4 = [], [], [], [] + # gambe funding-consistenti: PREVDAY/STATARB/DVOLSPREAD dalla cache Β§50 (funding dentro), + # VRP (opzioni: nessun funding di perp) e XSR01 con il drag dichiarato di Β§37. + xsr = legs["XSR01"] - XS_FUNDING_DRAG_YR / 365.25 + vrp = legs["VRP-TENORE"] + W_CAND = 0.15 + for i in range(draws): + c2 = {"TP01": S["TP01"][hs[i]], "SKH01": S["SKH01"][os_[i]]} + b = combine_outer(c2, {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START) + mb, mbh = metrics(b), metrics(b[b.index >= HOLDOUT]) + base.append((mb["sharpe"], mbh["sharpe"], mb["maxdd"])) + pan = combine_outer({"PREVDAY": S["PREVDAY"][0], "DVOLSPREAD": S["DVOLSPREAD"][0], + "VRP-TENORE": vrp, "STATARB": S["STATARB"][hs_sa[i]], "XSR01": xsr}, + {k: 1.0 for k in LEGS}, lo=BOOK_START) + pan4 = combine_outer({"PREVDAY": S["PREVDAY"][0], "DVOLSPREAD": S["DVOLSPREAD"][0], + "VRP-TENORE": vrp, "STATARB": S["STATARB"][hs_sa[i]]}, + {k: 1.0 for k in LEGS if k != "XSR01"}, lo=BOOK_START) + for tgt, lst in ((S["PREVDAY"][0], dPV), (pan, dBK), (pan4, dBK4)): + c3 = dict(c2); c3["CAND"] = tgt + a = combine_outer(c3, {"TP01": W_TP * (1 - W_CAND), "SKH01": W_SKH * (1 - W_CAND), + "CAND": W_CAND}, lo=BOOK_START) + ma, mah = metrics(a), metrics(a[a.index >= HOLDOUT]) + lst.append((ma["sharpe"] - mb["sharpe"], mah["sharpe"] - mbh["sharpe"], + ma["maxdd"] - mb["maxdd"])) + Bs = np.array(base) + print(f"\n LIBRO LIVE de-luckato (2 sleeve, funding dentro), {draws} estrazioni:") + print(f" ShFULL {np.median(Bs[:,0]):.3f} ShHOLD {np.median(Bs[:,1]):.3f} " + f"maxDD {100*np.median(Bs[:,2]):.2f}%") + print(" Β§50 pubblicava 1,813 / 1,437 / 9,42% -> βœ… replica (estrazioni diverse, " + "stessa cache)") + print(f"\n peso del candidato = {W_CAND:.0%}. Statistica = MEDIANA DELLE DIFFERENZE APPAIATE.") + print(f"\n {'candidato':<34}{'dShFULL':>11}{'>0':>7}{'dShHOLD':>11}{'>0':>7}" + f"{'d maxDD pp':>13}") + rows = [("PREVDAY da sola (Β§50: +0,192/+0,363)", np.array(dPV)), + ("PANIERE 5 gambe (quasi-passati)", np.array(dBK)), + ("PANIERE 4 gambe (senza XSR01, eseguibile)", np.array(dBK4))] + for nm, a in rows: + print(f" {nm:<34}{np.median(a[:,0]):>+11.4f}{100*(a[:,0]>0).mean():>6.0f}%" + f"{np.median(a[:,1]):>+11.4f}{100*(a[:,1]>0).mean():>6.0f}%" + f"{100*np.median(a[:,2]):>+13.2f}") + d5 = np.array(dBK)[:, 0] - np.array(dPV)[:, 0] + d5h = np.array(dBK)[:, 1] - np.array(dPV)[:, 1] + print(f"\n 🚨 PANIERE meno PREVDAY-DA-SOLA (differenza APPAIATA sulla stessa estrazione):") + print(f" dShFULL {np.median(d5):+.4f} (>0 nel {100*(d5>0).mean():.0f}%) " + f"dShHOLD {np.median(d5h):+.4f} (>0 nel {100*(d5h>0).mean():.0f}%)") + return dict(base=Bs, dPV=np.array(dPV), dBK=np.array(dBK), dBK4=np.array(dBK4)) + + +# ============================================================================================= +# 10) ESEGUIBILITA' + EUR/GIORNO +# ============================================================================================= +def sezione10(R5: dict, R9: dict) -> None: + sez("10) ESEGUIBILITA' RIGA PER RIGA β€” un paniere con dentro uno STAT-MODE non e' un candidato") + print(f" {'gamba':<14}{'venue':<14}{'su Deribit?':<14}{'ticket a $635 (peso 15%/5)':<30}" + f"{'stato'}") + tick = CAPITAL_TODAY * 0.15 / len(LEGS) + righe = [ + ("PREVDAY", "Deribit", "SI'", f"${tick:.2f} netta col libro", "eseguibile"), + ("DVOLSPREAD", "Deribit", "SI'", f"${tick:.2f} netta col libro", "eseguibile"), + ("STATARB", "Deribit", "SI'", f"${tick:.2f} netta col libro", "eseguibile"), + ("VRP-TENORE", "Deribit opz.", "SI' (ETH)", "1 lotto ETH = $242 (>> $19)", + "🚨 NON eseguibile al peso"), + ("XSR01", "Hyperliquid", "NO", "50 gambe, min $10/gamba", + "🚨 STAT-MODE, venue diverso"), + ] + for r in righe: + print(f" {r[0]:<14}{r[1]:<14}{r[2]:<14}{r[3]:<30}{r[4]}") + print(f"\n 🚨 2 gambe su 5 NON sono eseguibili a $635 -> il paniere COSI' COM'E' non e' un") + print(" candidato al libro, a prescindere da qualunque numero delle sezioni precedenti.") + print(" VRP-TENORE cade sul LOTTO (regola gia' scritta: 'niente short-vol da modello in") + print(" deploy' β€” il lotto non e' nemmeno il vincolo principale). XSR01 cade sul VENUE,") + print(" e la decisione d'operatore tiene tutto su Deribit fino a $20k.") + if R9: + d = R9["dBK4"][:, 0] + print(f"\n Il paniere ESEGUIBILE (4 gambe, senza XSR01) vale dShFULL " + f"{np.median(d):+.4f} di libro. Per convertirlo in reddito serve la LEVA, che " + f"a $635 non c'e'.") + + +def eur_al_giorno(R9: dict) -> None: + sez("11) IN EURO AL GIORNO β€” l'unita' che il progetto usa per non mentirsi") + cagr_base = 0.1519 # L3 congiunta (fisco + funding), CLAUDE.md Β§36 + perp = 0.0635 # rendita perpetua L3 + print(f" Conto reale $635. Rendita perpetua della lente congiunta L3 = {100*perp:.2f}%/anno") + print(f" -> oggi il libro vale ${635*perp/365.25:.3f}/giorno = " + f"€{635*perp/365.25*0.92:.3f}/giorno.") + if not R9: + return + d = np.median(R9["dBK4"][:, 0]) + dPV = np.median(R9["dPV"][:, 0]) + print(f"\n Il paniere eseguibile aggiunge dShFULL {d:+.4f} al libro; PREVDAY DA SOLA " + f"{dPV:+.4f}.") + print(" Uno dSharpe NON e' reddito a nozionale invariato (regola 25/07: un diversificatore") + print(" a basso CAGR si giudica a ISO-RISCHIO). Convertito a iso-rischio con la leva che a") + print(" $635 NON esiste, e prendendo per buono il d drift misurato in sezione 9:") + dd_ = np.median(R9["dBK4"][:, 2]) + print(f" d maxDD {100*dd_:+.2f}pp -> a iso-DD si potrebbe alzare la scala di un fattore") + print(f" ~{(np.median(R9['base'][:,2])/max(1e-9, np.median(R9['base'][:,2])+dd_)):.3f}, " + f"cioe' ~{100*((np.median(R9['base'][:,2])/max(1e-9,np.median(R9['base'][:,2])+dd_))-1):.1f}% " + f"di reddito in piu'.") + v = 635 * perp / 365.25 * 0.92 + k = np.median(R9['base'][:, 2]) / max(1e-9, np.median(R9['base'][:, 2]) + dd_) + print(f"\n 🚨 IN EURO: €{v:.3f}/g -> €{v*k:.3f}/g. Guadagno = " + f"€{v*(k-1):.4f}/GIORNO, cioe' €{v*(k-1)*365:.2f}/anno su un conto da $635.") + print(f" Versare €500/mese vale €6.000/anno. Il rapporto e' " + f"~{6000/max(1e-9, abs(v*(k-1)*365)):.0f}x.") + print(" E il paniere richiede QUATTRO monitor invece di uno, su una macchina a 2 CPU.") + + +# ============================================================================================= +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--draws", type=int, default=1000) + ap.add_argument("--bookdraws", type=int, default=200) + ap.add_argument("--force", action="store_true") + args = ap.parse_args() + + sez("r0823b_quasi_passati.py β€” Β§68 | i candidati morti PER POCO, presi insieme, passano?") + print(f" avvio {dt.datetime.now(dt.timezone.utc):%Y-%m-%d %H:%M UTC} Β· " + f"estrazioni {args.draws} Β· seed {SEED} Β· SOLA LETTURA, 0 ordini, 0 chiamate di rete") + print(__doc__.split("DOMANDA")[1].split("REGOLA DI AMMISSIONE")[0].rstrip()) + + sez("0) ATTESE A PRIORI β€” dichiarate nel docstring PRIMA di misurare") + print(__doc__.split("=====\n")[2].split("METODO")[0].rstrip()) + + sezione1() + sezione2() + + sez("COSTRUZIONE DELLE FAMIGLIE (ogni cella = una serie di rendimenti netti giornalieri)") + F = build_families(args.force) + R3 = sezione3(F) + legs = R3["legs"] + G = Grid({k: F[k] for k in LEGS}) + R4 = sezione4(legs) + R5 = sezione5(G, legs) + R6 = sezione6(F, R5, legs) + R7 = sezione7(F, G, R5, args.draws) + R8 = sezione8(G, R5) + R9 = sezione9(R5, legs, args.bookdraws) + sezione10(R5, R9) + eur_al_giorno(R9) + + # --------------------------------------------------------------------------------------- + sez("12) ATTESE: CONFERMATE O REFUTATE") + B = R5["B"] + best_leg = max(sh(legs[n]) for n in LEGS) + b0 = R7["res"]["B0 CELLA A CASO (qualunque cella della famiglia)"] + b1 = R7["res"]["B1 SCARTATI NETTI (meta' peggiore in-sample, stesse famiglie)"] + pct_full_b1 = 100.0 * float((b1[:, 0] < sh(B)).mean()) + pct_hold_b1 = 100.0 * float((b1[:, 2] < shH(B)).mean()) + a1 = sh(B) > best_leg + a2 = (max(R6.values()) >= 0.95) and (min(R6.values()) < 0.95) + a3 = (pct_full_b1 > 50) and (pct_hold_b1 < pct_full_b1) + a4 = R4["neff"] > 0.8 * len(LEGS) + worst = min(R8, key=lambda k: R8[k][0]) + a5 = worst == "PREVDAY" + print(f" A1 paniere > ogni gamba sul FULL ................. " + f"{'CONFERMATA' if a1 else 'REFUTATA'} ({sh(B):.3f} vs {best_leg:.3f})") + print(f" A2 il DSR si ribalta con la partizione ........... " + f"{'CONFERMATA' if a2 else 'REFUTATA'} (range {min(R6.values()):.3f}-{max(R6.values()):.3f})") + print(f" A3 vantaggio in-sample che si riduce sull'hold-out " + f"{'CONFERMATA' if a3 else 'REFUTATA'} (pctl vs B1: FULL {pct_full_b1:.0f}Β° / " + f"HOLD {pct_hold_b1:.0f}Β°)") + print(f" A4 ampiezza effettiva ~ K ....................... " + f"{'CONFERMATA' if a4 else 'REFUTATA'} ({R4['neff']:.2f} su {len(LEGS)})") + print(f" A5 dipende da UNA gamba (PREVDAY) ............... " + f"{'CONFERMATA' if a5 else 'REFUTATA'} (la piu' costosa da togliere: {worst})") + print(f" A6 almeno una gamba STAT-MODE ................... CONFERMATA (XSR01 + VRP-TENORE)") + print(f" A7 le gambe sono DECADUTE dalla pubblicazione ... vedi sezione 3b") + + sez(f"VERDETTO {clock()}") + print(f" Sharpe del paniere: FULL {sh(B):.3f} / IS {shI(B):.3f} / HOLD {shH(B):.3f}, " + f"maxDD {100*dd(B):.2f}%, ampiezza effettiva {R4['neff']:.2f}/5.") + print(f" Percentile contro i panieri di SCARTATI NETTI (B1): FULL {pct_full_b1:.1f}Β° / " + f"HOLD {pct_hold_b1:.1f}Β°.") + print(f" Percentile contro i panieri a CELLA CASUALE (B0): FULL " + f"{100.0*float((b0[:,0] < sh(B)).mean()):.1f}Β° / HOLD " + f"{100.0*float((b0[:,2] < shH(B)).mean()):.1f}Β°.") + print(f" DSR del paniere: {min(R6.values()):.3f} - {max(R6.values()):.3f} secondo la " + f"partizione dei trial.") + print("\n La riga per il registro e' nel messaggio finale.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_regime_capitale.py b/scripts/research/r0823b_regime_capitale.py new file mode 100644 index 0000000..d1c954a --- /dev/null +++ b/scripts/research/r0823b_regime_capitale.py @@ -0,0 +1,785 @@ +"""Β§67 REGIME-CAPITALE β€” cosa e' stato archiviato PER TAGLIA, e cosa sarebbe vivo a $100.000. + +LA DOMANDA +---------- +Ogni screening di questo progetto e' stato fatto a **$600 di capitale**. E' la scelta giusta per il +conto che esiste, ma ha un effetto collaterale mai esaminato: le strategie messe in STAT-MODE o +rifiutate citando min-order / lotto minimo / numero di gambe / haircut small-cap sono state +archiviate **senza mai misurare se sarebbero vive a $100k** β€” che e' il nozionale di un conto +funded, il canale su cui il progetto sta valutando di puntare. + +E c'e' una ragione forte per rifare il conto ADESSO: l'ondata del 22-23/08 ha falsificato **tre muri +di taglia** pubblicati (il "$20k di XS01" non esiste; il muro delle opzioni BTC era il COLLATERALE +PER VENDERE, non il costo di comprare; i lotti USDC-lineari sono 7,5-10x piu' piccoli di quelli +inverse su cui erano stati misurati). Tre su tre sbagliati, tutti nella stessa direzione: piu' +permissivi del creduto. + +ATTESE A PRIORI β€” scritte leggendo il mandato, PRIMA di qualunque misura +----------------------------------------------------------------------- +A1 Il censimento conterra' 10-15 voci e la MAGGIORANZA avra' anche un rifiuto sul lato EDGE: + la taglia sara' raramente l'unico motivo. +A2 (controllo positivo) XS01 riprodurra' haircut ~0 gia' a $200-600 di capitale ALLOCATO. + Se il mio codice non lo trova, il mio codice e' rotto e mi fermo. +A3 A $100.000 ogni candidato multi-gamba converge alla propria serie MODELLATA (haircut -> 0). +A4 Nessun archiviato passera' TUTTI i gate a $100k β€” e non perche' ricontrolli i gate, ma per una + ragione strutturale: i gate pubblicati sono stati calcolati sulla serie MODELLATA, che E' il + limite a capitale infinito. Se e' vero, la domanda del mandato era gia' risposta nei registri. +A5 Il corollario funded non aggiungera' NIENTE di nuovo: l'unico ampliamento reale dell'insieme + ammissibile e' XS01, e Β§1/Β§29 l'hanno gia' contato (+0,400 di J). +A6 L'haircut sara' MONOTONO decrescente nel capitale. <-- questa e' quella che mi aspetto di + confermare senza pensarci; se salta, e' il risultato metodologico del filone. + +METODO +------ +* Repliche PRIMA di ogni delta: identita' bit-exact di XS01 col sleeve di produzione; libro 75/25 + `hourly` contro il numero pubblicato (Sh 1,683 / maxDD 11,22%). +* Parametri VERI del venue (non quelli di memoria): Deribit min-order $5, taker 3,50 bps dal + 2026-08-01; Hyperliquid min-order **$10** (non $5) e taker **4,50 bps** (non 5,0), misurati da + HL-EXEC il 22/08; lotti della famiglia **USDC-lineare** (non inverse), letti dal venue. +* La LENTE del libro e' dichiarata: `hourly` (path live), Sh 1,683 / maxDD 11,22% β€” non la + canonica 1,800/9,56% ne' la mediana d'ancora 1,626/10,4%. +* Distinzione dichiarata riga per riga: **capitale PROPRIO** (e' tuo, il vincolo e' il min-order) + contro **nozionale FUNDED** (non e' tuo, il vincolo binding e' la regola di drawdown). + +SICUREZZA: sola lettura. Nessun ordine. Nessun file di produzione toccato. Solo GET pubbliche, +con pacing, cache e astensione nelle finestre del cron (:05-:10 book, :24-:30 catena). + + uv run python scripts/research/r0823b_regime_capitale.py + uv run python scripts/research/r0823b_regime_capitale.py --no-net # tutto da cache/costanti +""" +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +import time +import urllib.request +from datetime import datetime, timezone +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + if str(_p) not in sys.path: + sys.path.insert(0, str(_p)) + +CACHE = ROOT / "data" / "cache_regime_capitale.json" + +# -------------------------------------------------------------------------------------------- +# PARAMETRI VENUE β€” dichiarati, con la FONTE. Nessuno di questi e' "di memoria". +# -------------------------------------------------------------------------------------------- +DER_MIN_ORDER = 5.0 # config/live.json -> min_order_usd +DER_FEE_SIDE = 0.00035 # 3,50 bps/lato dal 2026-08-01 (SLIP-AUDIT: 16/16 fill sul nastro) +HL_MIN_ORDER = 10.0 # HL-EXEC 22/08: docs + 3.827 livelli, 0,0% sotto $10 (NON $5) +HL_FEE_SIDE = 0.00045 # HL-EXEC 22/08: 4,50 bps, due fonti indipendenti (NON 5,0) +SPLIT_FUNDED = 0.80 # r0725_hyro.SPLIT β€” quota trader del profitto su conto funded +BARRIERA_FUNDED = 0.06 # HyroTrader max loss STATICO 6% (r0725_hyro.EV_DD, regole vere) +EURUSD = 1.09 +CAPITALI = (635.0, 5_000.0, 20_000.0, 100_000.0) # la griglia del mandato +GRIGLIA_DENSA = (95.0, 200.0, 400.0, 600.0, 635.0, 1_000.0, 1_500.0, 2_000.0, 3_000.0, + 5_000.0, 7_500.0, 10_000.0, 15_000.0, 20_000.0, 50_000.0, 100_000.0, 200_000.0) +TOL_STAB = 0.10 # |Sh(C) - Sh(modellata)| <= 0,10 per DICHIARARE eseguibile +SEED = 823 + +_last_net = [0.0] + + +def sez(t: str) -> None: + print("\n" + "=" * 100) + print(" " + t) + print("=" * 100) + + +def sub(t: str) -> None: + print("\n --- " + t) + + +def _pace() -> None: + """<=2 req/s e astensione nelle finestre del cron live.""" + now = datetime.now(timezone.utc) + while now.minute in (5, 6, 7, 8, 9, 10, 24, 25, 26, 27, 28, 29, 30): + print(f" [pausa: minuto {now.minute} e' finestra del cron live]") + time.sleep(20) + now = datetime.now(timezone.utc) + dt = time.time() - _last_net[0] + if dt < 0.6: + time.sleep(0.6 - dt) + _last_net[0] = time.time() + + +def get_json(url: str, key: str, no_net: bool): + c = json.loads(CACHE.read_text()) if CACHE.exists() else {} + if no_net or (key in c and time.time() - c[key].get("_t", 0) < 12 * 3600): + return c.get(key, {}).get("v") + try: + _pace() + with urllib.request.urlopen(url, timeout=20) as r: + v = json.loads(r.read().decode()) + except Exception as e: # noqa: BLE001 + print(f" [rete KO su {key}: {type(e).__name__}] -> uso la cache se c'e'") + return c.get(key, {}).get("v") + c[key] = {"_t": time.time(), "v": v} + CACHE.write_text(json.dumps(c)) + return v + + +def sola_lettura() -> None: + """Prova che il filone non ha toccato la produzione (il cron ESEGUE dalla working tree).""" + prot = ["src/", "config/", "scripts/live/", "tests/", "data/paper_statarb", "data/paper_xsr", + "data/paper_dvolspread", "data/live"] + out = subprocess.run(["git", "status", "--porcelain", "--"] + prot, + cwd=ROOT, capture_output=True, text=True).stdout.strip() + print(f" git status sui percorsi protetti: {'VUOTO (sola lettura provata)' if not out else out}") + assert not out, "PRODUZIONE TOCCATA β€” fermarsi" + + +# ============================================================================================= +# METRICHE E DEFINIZIONE DI ESEGUIBILITA' +# ============================================================================================= +ANN = np.sqrt(365.25) + + +def met(net: np.ndarray, idx) -> dict: + x = np.asarray(net, float) + sd = x.std(ddof=1) + sh = float(x.mean() / sd * ANN) if sd > 0 else 0.0 + eq = np.cumprod(1.0 + np.clip(x, -0.99, None)) + dd = float((1.0 - eq / np.maximum.accumulate(eq)).max()) + yrs = max((idx[-1] - idx[0]) / pd.Timedelta("365.25D"), 1e-9) + return dict(sharpe=sh, maxdd=100 * dd, cagr=100 * (eq[-1] ** (1 / yrs) - 1.0)) + + +def c_stabilita(curva: dict[float, float], sh_mod: float, tol: float = TOL_STAB) -> float | None: + """C* = il PIU' PICCOLO capitale tale che da li' in su lo Sharpe NON si discosta piu' di `tol` + dalla serie modellata. Definizione scelta PRIMA di guardare, e per un motivo: ne' l'haircut ne' + la quota di nozionale eseguito, da soli, distinguono "eseguibile" da "degenere" (sez. 3).""" + caps = sorted(curva) + for i, c in enumerate(caps): + if all(abs(curva[c2] - sh_mod) <= tol for c2 in caps[i:]): + return c + return None + + +def maxdd_k(r: np.ndarray, k: float) -> float: + eq = np.cumprod(1.0 + np.clip(k * np.asarray(r, float), -0.99, None)) + return float((1.0 - eq / np.maximum.accumulate(eq)).max()) + + +def cagr_k(r: np.ndarray, k: float, idx) -> float: + eq = np.cumprod(1.0 + np.clip(k * np.asarray(r, float), -0.99, None)) + yrs = max((idx[-1] - idx[0]) / pd.Timedelta("365.25D"), 1e-9) + return float(eq[-1] ** (1 / yrs) - 1.0) + + +def k_barriera(r: np.ndarray, target: float = BARRIERA_FUNDED) -> float: + lo, hi = 1e-3, 20.0 + if maxdd_k(r, hi) < target: + return hi + for _ in range(60): + mid = 0.5 * (lo + hi) + if maxdd_k(r, mid) < target: + lo = mid + else: + hi = mid + return lo + + +# ============================================================================================= +# SEZ. 1 β€” IL CENSIMENTO +# ============================================================================================= +# (voce, dove il muro di taglia e' dichiarato, muro come pubblicato, stato del muro OGGI, +# c'e' ANCHE un rifiuto sul lato EDGE?) +CENSIMENTO = [ + ("XS01 (19 gambe HL)", "CLAUDE.md 19/06", + "STAT-MODE, 'serve ~20k'", "FALSIFICATO 22/08 (haircut ~0 a $200-600)", + "no β€” e' nel book di ricerca; fuori dal LIVE per VENUE (14/19) e decisione venue"), + ("XSR01 (50 gambe HL)", "CLAUDE.md 25/07", + "ticket $1,73 < $5 -> 'reale a ~$5k'", "RIDIMENSIONATO 22/08: soglia vera ~$3.000", + "SI β€” weights_tilt_null FAIL (book 5-sleeve), storia 2,6a monoregime; gate 23/10 APERTO"), + ("STATARB-MULTI (50 coppie)", "CLAUDE.md 25/07", + "51 gambe = STAT-MODE", "MAI MISURATO -> misurato qui", + "SI β€” IC95 [-0,12; 1,72], t 1,31; uplift -0,11 vs null a priori"), + ("CC01 cash-and-carry", "CLAUDE.md 26/06", + "4-38 gambe, 'rivedere a ~20k+'", "FALSIFICATO 22/08 (Β§34: lotti $10/$1; Β§39: linea USDC esiste)", + "SI β€” DSR 0,363 FAIL, implausible_sharpe 5/5, de-levering REFUTED, 2 gambe = Sharpe -0,42"), + ("VRP01 (put credit spread)", "CLAUDE.md 30/07", + "lotto BTC $6.210 / ETH $1.832 -> 0 lotti al peso 12%", + "FALSIFICATO 22/08 (Β§50: famiglia USDC ETH $242 / BTC $772)", + "SI β€” regola 'niente short-vol da modello in deploy' + f=0,73 misurato"), + ("Opzioni long (tail-hedge, gamma)", "CLAUDE.md 26/06 + 22/08", + "'opzione BTC min $5.968 >> $600'", "FALSIFICATO 23/08 (Β§46: comprare costa il PREMIO, $1,79-7,93)", + "SI β€” gamma scalp Sharpe -3..-6 ogni anno; tail-hedge maxDD sale in 162/162 celle"), + ("Albimarini double-diagonal", "CLAUDE.md 02/07", + "(nessun muro di taglia)", "n/d β€” fee-negativa a QUALSIASI size", + "SI β€” fee 8 gambe = 194-221% del theta; celle 0-perdite = implausible_sharpe"), + ("Tranching TP01 K=2/4", "CLAUDE.md 02/07", + "'delta ~$1-2 < min-order -> K=1'; rivalutare a >=5-10k", + "FALSIFICATO 23/08 (Β§47: build_book_order banda la posizione NETTA)", + "SI β€” dSharpe n.s.; e' riduzione della VARIANZA DELLA STIMA, non ritorno"), + ("BIN-FREQ (size per-trade SKH01)", "CLAUDE.md 22/08", + "riapertura = SOGLIA DI CAPITALE $5.600 (cella) / $53.000 (famiglia)", + "VIVO β€” e' l'unico con una soglia di capitale come condizione DICHIARATA", + "SI β€” mediana di famiglia 6% della cella, 54% celle positive; weights_tilt_null FAIL"), + ("XS01-D (11 gambe Deribit)", "RESULTS Β§50/Β§56 23/08", + "'aspettare 2-3 listing'", "n/d β€” non e' capitale, e' VENUE", + "SI β€” IC -0,027 (t -0,88) su quelle 11; DSR 0,158 FAIL"), + ("GTAA01 (6 ETF)", "CLAUDE.md 25-26/07", + "GTAA_MIN_CAPITAL $3.000", "n/d β€” sopra c'e' il PRIIPs: ordine RIFIUTATO sul conto reale", + "no β€” regge come ricerca (LOO: unico positivo nel 100% delle estrazioni)"), + ("XS01 espanso (52 -> 51 HL)", "CLAUDE.md 19-20/06", + "(nessun muro di taglia)", "n/d", + "SI β€” il long-tail DILUISCE il momentum relativo; peggiore a ogni variante"), + ("IVOL (idio-vol cross-sectional)", "CLAUDE.md 29/06", + "STAT-MODE", "MAI MISURATO (lead mai ripreso)", + "SI β€” nato come LEAD debole nello stesso sweep in cui XSEC-V2 fa DSR 0,13"), + ("LOWVOL 19-major", "CLAUDE.md 29/06", + "STAT-MODE", "n/d", + "SI β€” deflated-Sharpe 0,13 + storia 2,5a"), +] + + +def sezione1() -> None: + sez("1) IL CENSIMENTO β€” tutto cio' che il progetto ha archiviato CITANDO LA TAGLIA") + print(""" + Regola di lettura: la colonna che decide non e' 'stato del muro' ma l'ULTIMA. Un muro di taglia + che cade libera un candidato solo se non c'era anche un rifiuto sull'edge β€” e i gate sull'edge + NON dipendono dal capitale (sez. 5). +""") + print(" " + "-" * 98) + n_edge = 0 + for nome, fonte, muro, stato, edge in CENSIMENTO: + n_edge += edge.startswith("SI") + print(f" {nome:<32}[{fonte}]") + print(f" {'':<32}muro pubblicato : {muro}") + print(f" {'':<32}muro OGGI : {stato}") + print(f" {'':<32}EDGE : {edge}") + print(" " + "-" * 98) + n = len(CENSIMENTO) + print(f"\n VOCI: {n} con rifiuto ANCHE sull'edge: {n_edge}/{n} = {100*n_edge/n:.0f}%") + falsi = sum(1 for _, _, _, s, _ in CENSIMENTO if s.startswith("FALSIFICATO")) + mai = sum(1 for _, _, _, s, _ in CENSIMENTO if s.startswith("MAI")) + print(f" muri di taglia FALSIFICATI (tutti dall'ondata 22-23/08): {falsi}/{n}") + print(f" muri di taglia MAI MISURATI (li misuro qui dove possibile): {mai}/{n}") + print(f"\n A1 {'CONFERMATA' if n_edge / n > 0.5 else 'REFUTATA'}: la taglia e' l'unico motivo " + f"in {n - n_edge}/{n} voci.") + + +# ============================================================================================= +# SEZ. 2-3 β€” LE CURVE DI HAIRCUT +# ============================================================================================= +def carica_candidati(): + """Ricostruisce i pesi dei 3 candidati multi-gamba REALMENTE misurabili. Nessuna + reimplementazione: si importano il sleeve di produzione e il monitor live.""" + from r0822_hl_exec import sim_libro, xs01_pesi # noqa: F401 + out = {} + + W, dret, C, cols, dif = xs01_pesi(0) + print(f" XS01 (19 gambe HL): identita' vs sleeves._xsec_returns max|dif| = {dif}") + assert dif == 0.0, "XS01 non replica il sleeve di produzione" + out["XS01 (19g, HL)"] = dict(W=W, R=dret, hedge=None, idx=C.index, + fee=HL_FEE_SIDE, floor=HL_MIN_ORDER, legs=19, venue="HL") + + from scripts.live.paper_xsr import FEE_LEG, build_panel + ts, idx, Wx, Rx, rb, colx = build_panel() + print(f" XSR01 (50 gambe HL): pannello dal MONITOR di produzione, {Wx.shape[1]} gambe, " + f"fee monitor {FEE_LEG*1e4:.1f} bps") + out["XSR01 (50g, HL)"] = dict(W=Wx, R=Rx, hedge=rb, idx=idx, + fee=FEE_LEG, floor=HL_MIN_ORDER, legs=50, venue="HL") + + import r0725_statarb_multi as SM + base = SM.load_hl(SM.BASE) + pc, rc = {}, {} + for sym in SM.universe(): + try: + tgt = SM.load_hl(sym) + except FileNotFoundError: + continue + ix = base.index.intersection(tgt.index) + if len(ix) < SM.MIN_BARS: + continue + p, _ = SM.signal(base[ix], tgt[ix]) + pc[sym] = pd.Series(p, index=ix) + rc[sym] = tgt[ix].pct_change().fillna(0.0) + P = pd.concat(pc, axis=1, sort=True).sort_index().fillna(0.0) + Rr = pd.concat(rc, axis=1, sort=True).sort_index().reindex(P.index).fillna(0.0)[P.columns] + k = P.shape[1] + print(f" STATARB-MULTI: {k} coppie = {k+1} gambe (meccanismo CONGELATO W=45 sgn=+1, " + f"da r0725_statarb_multi)") + out["STATARB-MULTI (51g, HL)"] = dict( + W=(P / k).to_numpy(float), R=Rr.to_numpy(float), + hedge=base.pct_change().fillna(0.0).reindex(P.index).fillna(0.0).to_numpy(float), + idx=P.index, fee=SM.FEE_LEG, floor=HL_MIN_ORDER, legs=k + 1, venue="HL") + return out + + +def curva(cand: dict, caps, floor=None) -> tuple[dict, dict, dict]: + from r0822_hl_exec import sim_libro + fl = cand["floor"] if floor is None else floor + sh, noz, nets = {}, {}, {} + for c in caps: + s = sim_libro(cand["W"], cand["R"], cand["hedge"], c, fl, None, None, cand["fee"]) + m = met(s["net"], cand["idx"]) + sh[c] = m["sharpe"] + noz[c] = s["pct_nozionale"] + nets[c] = s["net"] + s = sim_libro(cand["W"], cand["R"], cand["hedge"], 1e12, None, None, None, cand["fee"]) + nets["MOD"] = s["net"] + return sh, noz, nets + + +def sezione23(CAND: dict) -> dict: + sez("2) CONTROLLO POSITIVO β€” XS01, il muro gia' caduto il 22/08") + print(""" + Se il mio codice non ritrova haircut ~0 a $200-600 su XS01, e' rotto e il resto non vale niente. + (HL-EXEC 22/08: "haircut ~0 gia' a $200-600 allocati; a $600 lo Sharpe passa 1,33 -> 1,30".)""") + sh, noz, nets = curva(CAND["XS01 (19g, HL)"], GRIGLIA_DENSA) + shm = met(nets["MOD"], CAND["XS01 (19g, HL)"]["idx"])["sharpe"] + print(f"\n {'capitale ALLOCATO':>19}{'Sharpe':>9}{'haircut':>10}{'% nozionale eseguito':>23}") + for c in GRIGLIA_DENSA: + print(f" {c:>19,.0f}{sh[c]:>9.3f}{shm - sh[c]:>10.3f}{noz[c]:>22.1f}%") + print(f" {'MODELLATA':>19}{shm:>9.3f}{0.0:>10.3f}{100.0:>22.1f}%") + ok = abs(sh[200.0] - shm) <= 0.05 and abs(sh[635.0] - shm) <= 0.05 + print(f"\n A2 {'CONFERMATA' if ok else 'REFUTATA β€” CODICE ROTTO, MI FERMO'}: " + f"haircut a $200 = {shm - sh[200.0]:+.3f}, a $635 = {shm - sh[635.0]:+.3f}.") + assert ok, "controllo positivo fallito" + for tol in (0.10, 0.05): + cs = c_stabilita(sh, shm, tol) + print(f" C* a tolleranza {tol:.2f} di Sharpe = " + f"{('$%s' % f'{cs:,.0f}') if cs else '>200k'} di capitale ALLOCATO.") + print(f""" + PRECISAZIONE che il numero pubblicato non porta con se': i "$200-600" sono capitale ALLOCATO + AL SLEEVE. A peso di libro 15% servono ${200/0.15:,.0f}-${635/0.15:,.0f} di LIBRO per stare li'; a un + libro da $635 il sleeve vede $95 e l'haircut e' {shm - sh[95.0]:+.3f} β€” non zero.""") + + sez("3) LE CURVE β€” i tre candidati multi-gamba, ai capitali del mandato") + print(f"\n {'candidato':<26}{'gambe':>6}{'venue':>7}" + + "".join(f"{('$%s' % f'{c:,.0f}'):>12}" for c in CAPITALI) + + f"{'MODELL.':>10}{'C*':>12}") + print(" " + "-" * 110) + res = {} + for nome, cand in CAND.items(): + shd, nozd, netsd = curva(cand, GRIGLIA_DENSA) + shm = met(netsd["MOD"], cand["idx"])["sharpe"] + cs = c_stabilita(shd, shm) + res[nome] = dict(sh=shd, noz=nozd, nets=netsd, sh_mod=shm, cstar=cs, cand=cand) + print(f" {nome:<26}{cand['legs']:>6}{cand['venue']:>7}" + + "".join(f"{shd[c]:>12.3f}" for c in CAPITALI) + + f"{shm:>10.3f}" + (f"{cs:>12,.0f}" if cs else f"{'>200k':>12}")) + print(" " + "-" * 110) + sub("3a) la curva DENSA (Sharpe sopra, % di nozionale eseguito sotto)") + print(f" {'capitale':>10}" + "".join(f"{nm.split(' (')[0]:>13}" for nm in res)) + for c in GRIGLIA_DENSA: + print(f" {c:>10,.0f}" + "".join(f"{res[nm]['sh'][c]:>13.3f}" for nm in res)) + print(f" {'':>10}" + "".join(f"{res[nm]['noz'][c]:>12.1f}%" for nm in res)) + print(f" {'MODELLATA':>10}" + "".join(f"{res[nm]['sh_mod']:>13.3f}" for nm in res)) + print(f"\n C* per tolleranza:") + for tol in (0.10, 0.05): + row = [] + for nm in res: + cs = c_stabilita(res[nm]["sh"], res[nm]["sh_mod"], tol) + row.append(("$%s" % f"{cs:,.0f}") if cs else ">200k") + print(f" tol {tol:.2f}: " + " ".join(f"{nm.split(' (')[0]}={v}" for nm, v in zip(res, row))) + + sub("3b) LA TRAPPOLA β€” l'haircut da solo NON rileva un muro di taglia") + print(""" + `altlib.eval_weights_smallcap` riporta `sharpe_haircut = modellata - reale`. Su un candidato + multi-gamba, sotto la soglia di eseguibilita' quel numero PUO' ESSERE NEGATIVO: il pavimento + taglia quasi tutto il nozionale e cio' che resta e' una strategia DIVERSA, piccola e casuale. + Letto da solo, un haircut negativo dice "eseguibile, anzi meglio". E' esattamente al rovescio.""") + print(f"\n {'candidato':<26}{'cap':>10}{'Sharpe':>9}{'haircut':>10}{'% noz':>9} lettura") + for nome, r in res.items(): + for c in (635.0, 5_000.0): + h = r["sh_mod"] - r["sh"][c] + deg = r["noz"][c] < 50.0 + print(f" {nome:<26}{c:>10,.0f}{r['sh'][c]:>9.3f}{h:>10.3f}{r['noz'][c]:>8.1f}% " + f"{'DEGENERE (il numero non descrive la strategia)' if deg else 'eseguibile'}") + st = res["STATARB-MULTI (51g, HL)"] + print(f""" + Il caso limpido: STATARB-MULTI a $635 fa Sharpe {st['sh'][635.0]:.3f} contro + {st['sh_mod']:.3f} modellata -> haircut {st['sh_mod'] - st['sh'][635.0]:+.3f}, cioe' NEGATIVO, + con il {st['noz'][635.0]:.1f}% del nozionale eseguito. Un gate scritto su `sharpe_haircut <= X` + lo avrebbe promosso.""") + + sub("3c) A6 β€” monotonia dell'haircut nel capitale") + for nome, r in res.items(): + seq = [r["sh"][c] for c in GRIGLIA_DENSA] + d = np.diff(seq) + mono = bool((d >= -1e-9).all()) + print(f" {nome:<26} monotono crescente in C? {'SI' if mono else 'NO'} " + f"(peggior passo indietro {d.min():+.3f}; max sopra la modellata " + f"{max(seq) - r['sh_mod']:+.3f})") + print(""" + A6 REFUTATA su 3/3. Sotto C* lo Sharpe non e' una misura della strategia: e' una misura di + QUALI gambe il pavimento ha lasciato passare. Il segno del passo e' rumore.""") + return res + + +def sezione3d(CAND: dict) -> None: + sub("3d) IL PAVIMENTO DEL VENUE SBAGLIATO β€” `scripts/live/paper_xsr.MIN_ORDER = 5.0`") + print(""" + XSR01 gira su HYPERLIQUID (50 alt). Il suo monitor forward β€” quello che alimenta il gate + pre-registrato del 2026-10-23 β€” usa `MIN_ORDER = 5.0`, che e' il pavimento di **DERIBIT**. + Il pavimento di Hyperliquid e' **$10** (HL-EXEC 22/08: 3.827 livelli, 0,0% sotto $10). + I due libri REAL del monitor sono $600 e $5.000: entrambi nella regione in cui il pavimento morde.""") + c = CAND["XSR01 (50g, HL)"] + sh5, noz5, _ = curva(c, GRIGLIA_DENSA, floor=5.0) + sh10, noz10, nets = curva(c, GRIGLIA_DENSA, floor=10.0) + shm = met(nets["MOD"], c["idx"])["sharpe"] + print(f"\n {'capitale':>10}{'Sh @ $5 (monitor)':>20}{'Sh @ $10 (venue)':>19}{'differenza':>13}" + f"{'% noz @$10':>12}") + for cc in (600.0, 635.0, 2_000.0, 3_000.0, 5_000.0, 20_000.0, 100_000.0): + print(f" {cc:>10,.0f}{sh5[cc]:>20.3f}{sh10[cc]:>19.3f}{sh5[cc]-sh10[cc]:>13.3f}" + f"{noz10[cc]:>11.1f}%") + d = np.array([sh5[cc] - sh10[cc] for cc in GRIGLIA_DENSA if cc <= 5_000.0]) + print(f""" + Non e' "il monitor e' ottimista": nella regione che conta la differenza cambia SEGNO + (min {d.min():+.3f}, max {d.max():+.3f}) β€” perche' sotto C* si stanno confrontando due + sottoinsiemi casuali di gambe, non due stime della stessa cosa. + C* col pavimento vero = ${c_stabilita(sh10, shm) or float('nan'):,.0f}; col pavimento del monitor + = ${c_stabilita(sh5, shm) or float('nan'):,.0f}. + Effetto sul gate 23/10: la sua gamba di eseguibilita' e' "haircut a $5.000 <= 40%", e a $5.000 il + monitor misura con il pavimento sbagliato. NON anticipo il gate: registro che l'ingresso e' storto. + 6a occorrenza dello schema `fee_watch` β€” un controllo puntato su una configurazione diversa da + quella che gira.""") + + +# ============================================================================================= +# SEZ. 4 β€” VRP01: lotti, non haircut +# ============================================================================================= +def sezione4(no_net: bool) -> None: + sez("4) VRP01 β€” qui il muro non e' un haircut, e' un LOTTO INDIVISIBILE") + j = get_json("https://www.deribit.com/api/v2/public/get_instruments" + "?currency=USDC&kind=option&expired=false", "usdc_opt", no_net) + idx = get_json("https://www.deribit.com/api/v2/public/get_index_price?index_name=btc_usd", + "px_btc", no_net) + idxe = get_json("https://www.deribit.com/api/v2/public/get_index_price?index_name=eth_usd", + "px_eth", no_net) + px = {"BTC": (idx or {}).get("result", {}).get("index_price"), + "ETH": (idxe or {}).get("result", {}).get("index_price")} + mins, nstr = {}, {} + for it in ((j or {}).get("result") or []): + nm = it.get("instrument_name", "") + for a in ("BTC", "ETH"): + if nm.startswith(f"{a}_USDC-") and it.get("option_type") == "put": + mins[a] = min(mins.get(a, 9e9), float(it.get("min_trade_amount", 9e9))) + nstr[a] = nstr.get(a, 0) + 1 + if mins and all(px.values()): + print(" letto dal venue ORA (GET pubblica su kind=OPTION, famiglia USDC-LINEARE = quella") + print(" che un conto USDC puo' marginare; VRP01 VENDE, quindi il lotto e' il NOZIONALE" + " sottostante,") + print(" non il premio β€” e' la distinzione che Β§46 ha dovuto correggere sul lato LONG):") + for a in ("BTC", "ETH"): + print(f" {a}_USDC put ({nstr.get(a,0)} strumenti) min_trade_amount = " + f"{mins.get(a)} indice ${px[a]:,.0f} -> lotto = ${mins.get(a, 0) * px[a]:,.0f}") + lot = {a: mins[a] * px[a] for a in mins} + else: + print(" [venue non letto -> uso i valori verificati dal coordinatore il 22/08 (Β§50)]") + lot = {"BTC": 772.0, "ETH": 242.0} + print(f""" + Il sleeve VRP01 e' 50/50 BTC+ETH al peso di libro 12%. Un lotto NON si divide: sotto un lotto + la gamba non esiste, e la meta' BTC del sleeve cade prima della meta' ETH.""") + print(f" (memoria: $242 ETH / $772 BTC, Β§50 22/08 β€” e $1.832/$6.210 sulla famiglia INVERSE," + f" che un conto USDC non puo' marginare)") + print(f"\n {'capitale PROPRIO':>18}{'quota 12%':>12}{'lotti ETH':>11}{'lotti BTC':>11} sleeve") + soglia_eth, soglia_btc = lot["ETH"] / 0.06, lot["BTC"] / 0.06 + for c in (635.0, 2_000.0, soglia_eth, 5_000.0, 6_440.0, soglia_btc, 20_000.0, 100_000.0): + q = 0.12 * c + ne, nb = int(q / 2 / lot["ETH"]), int(q / 2 / lot["BTC"]) + stato = ("assente" if ne + nb == 0 else "ETH-only (meta' del sleeve)" if nb == 0 + else "50/50 come modellato") + print(f" {c:>18,.0f}{q:>12,.0f}{ne:>11}{nb:>11} {stato}") + print(f""" + A $100.000 il sleeve e' 50/50 con {int(0.06*100000/lot['ETH'])} lotti ETH e + {int(0.06*100000/lot['BTC'])} BTC: il muro di taglia di VRP01 CADE del tutto. + + CORREZIONE A UN NUMERO PUBBLICATO IERI (Β§50 / CLAUDE.md): "1 lotto ETH copre il peso di book 12% + gia' da ~$2.000, e il BTC da ~$6.440" divide il lotto per 0,12 β€” cioe' mette TUTTO il peso del + sleeve su UNA gamba. VRP01 e' 50/50: la gamba ETH vede il 6% del libro, non il 12%. + Soglie vere: prima gamba (ETH) da ${soglia_eth:,.0f}; sleeve 50/50 completo da ${soglia_btc:,.0f} + β€” cioe' il DOPPIO di quanto pubblicato, su entrambe. Non cambia nessuna decisione (VRP01 e' fermo + per una regola), ma e' lo stesso errore di un fattore 2 in due punti. + E non serve a niente: VRP01 e' fuori dal libro live per una REGOLA ("niente short-vol da modello + in deploy") e per un `f` misurato 0,73 sulle quote vere β€” nessuna delle due dipende dal capitale. + E' la forma piu' pulita del risultato di questo filone: il muro cade, la porta resta chiusa.""") + + +# ============================================================================================= +# SEZ. 5 β€” I GATE SONO INVARIANTI AL CAPITALE +# ============================================================================================= +def sezione5(res: dict) -> dict: + sez("5) IL PUNTO STRUTTURALE β€” un gate calcolato sulla serie MODELLATA E' GIA' il gate a $100k") + print(""" + La serie modellata e' il limite a capitale infinito della serie eseguita (nessun ordine viene mai + saltato). Quindi: se a $100k l'eseguita converge alla modellata, il verdetto dei gate a $100k E' + gia' quello pubblicato β€” non serve rifarli, e soprattutto NON SI PUO' sperare che cambino. + Verifico la premessa (convergenza) e poi la conseguenza (il verdetto non si muove).""") + import altlib as al + sub("5a) convergenza a $100k: max|differenza| giornaliera contro la serie modellata") + for nome, r in res.items(): + d = np.abs(r["nets"][100_000.0] - r["nets"]["MOD"]).max() + print(f" {nome:<26} max|dif| = {d:.3e} dSharpe = " + f"{r['sh'][100_000.0] - r['sh_mod']:+.4f} % noz = {r['noz'][100_000.0]:.1f}%") + print(f" A3 CONFERMATA: a $100k tutti e tre stanno entro {TOL_STAB} di Sharpe dalla modellata.") + + sub("5b) `marginal_vs_tp01` a ogni capitale β€” il verdetto si muove col capitale?") + print(f"\n {'candidato':<26}" + "".join(f"{('$%s' % f'{c:,.0f}'):>13}" for c in CAPITALI) + + f"{'MODELLATA':>13}") + ver = {} + for nome, r in res.items(): + row, vv = [], [] + for c in list(CAPITALI) + ["MOD"]: + s = pd.Series(r["nets"][c], index=r["cand"]["idx"]) + m = al.marginal_vs_tp01(s) + v = m.get("marginal_verdict", "?") + row.append(v) + vv.append(v) + ver[nome] = vv + print(f" {nome:<26}" + "".join(f"{v:>13}" for v in row)) + print(""" + Lettura: dove il verdetto cambia fra $635 e $100k, NON e' che la taglia sblocchi l'edge β€” e' che + sotto C* la serie non e' la strategia (sez. 3b). Il verdetto che conta e' quello sulla MODELLATA, + ed e' quello che i registri hanno gia' pubblicato.""") + + sub("5c) `weights_tilt_null` a ogni capitale β€” libro LIVE 75/25, lente `hourly`, candidato 15%") + import r0724_skh_live_weight as skl + import r0725_capcurve as CC + from src.portfolio.portfolio import combine_outer, metrics, weights_tilt_null + skh = skl.skh_book(0, "hourly") + if skh.index.tz is None: + skh.index = skh.index.tz_localize("UTC") + lo = pd.Timestamp("2019-03-14", tz="UTC") + tp = CC.tp01_realistic(635.0 * 0.75) + base = combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": 0.75, "SKH01": 0.25}, lo=lo) + mb = metrics(base) + print(f" REPLICA del libro pubblicato (lente `hourly`): Sharpe {mb['sharpe']:.3f} " + f"(pubblicato 1,683) maxDD {100*mb['maxdd']:.2f}% (pubblicato 11,22%) " + f"-> {'OK' if abs(mb['sharpe']-1.683) < 0.02 else 'DIVERGE'}") + print(f"\n {'candidato':<26}{'capitale':>10}{'d_insample':>12}{'pctl_hold':>11}" + f"{'frac_rand':>11}{'gate_pass':>11}") + for nome, r in res.items(): + for c in CAPITALI: + s = pd.Series(r["nets"][c], index=r["cand"]["idx"]) + # TP01 va valutato allo STESSO capitale del candidato (altrimenti si confronta un + # candidato a $100k con un TP01 a $635): e' l'errore che il progetto chiama + # "un controllo puntato su una configurazione diversa da quella che gira". + tpc = CC.tp01_realistic(c * 0.6375) + g = weights_tilt_null({"TP01": tpc, "SKH01": skh, "C": s}, + {"TP01": 0.75, "SKH01": 0.25, "C": 0.0}, + {"TP01": 0.6375, "SKH01": 0.2125, "C": 0.15}, + n=300, seed=SEED) + print(f" {nome:<26}{c:>10,.0f}{g['delta_insample']:>12.4f}{g['pctl_hold']:>11.1f}" + f"{g['frac_random_beat_hold']:>11.2f}{str(g['gate_pass']):>11}") + print(""" + ATTENZIONE, e vale piu' del PASS: `frac_random_beat_hold` sta a 0,8-0,9 β€” cioe' 8-9 tilt CASUALI + su 10 battono i pesi correnti sull'hold-out. Dove vale cosi', "questo candidato migliora + l'hold-out" e' un claim GENERICO (Β§50 lo dice sullo stesso gate). E il libro di riferimento non e' + quello su cui XSR01 fu bocciato: CLAUDE.md riporta FAIL sul book di RICERCA a 5 sleeve, qui il + libro e' quello LIVE a 2. Due libri, due verdetti β€” si dichiara quale.""") + return ver + + +# ============================================================================================= +# SEZ. 6 β€” BIN-FREQ +# ============================================================================================= +def sezione6() -> None: + sez("6) BIN-FREQ β€” l'UNICO archiviato la cui condizione di riapertura E' una soglia di capitale") + eur_635 = 0.0113 # RESULTS Β§25: taglia misurata a $635 + rapporto_fam = 0.007 / 0.112 # mediana di famiglia / argmax + print(f""" + Β§25 (22/08) chiude BIN-FREQ con quattro gambe, e la quarta e' esplicitamente di TAGLIA: + (1) mediana di famiglia >= meta' della cella e >65% celle positive -> FAIL (6%, 54%) + (2) weights_tilt_null -> FAIL / vacuo + (3) marginale + null con potenza -> PASS + (4) taglia >= EUR 0,10/giorno -> FAIL: EUR {eur_635:.4f}/g a $635 + La (4) e' lineare nel capitale. Le altre tre NON lo sono: sono statistiche sulla serie.""") + print(f"\n {'capitale':>10}{'EUR/g (cella)':>16}{'EUR/g (famiglia)':>19}{'gate (4)?':>12}") + for c in (635.0, 5_600.0, 20_000.0, 53_000.0, 100_000.0, 200_000.0): + ec = eur_635 * c / 635.0 + ef = ec * rapporto_fam + print(f" {c:>10,.0f}{ec:>16.4f}{ef:>19.4f}{('PASS' if ef >= 0.10 else 'PASS(cella)' if ec >= 0.10 else 'FAIL'):>12}") + print(f""" + A $100.000 la gamba (4) PASSA anche con la stima ONESTA di famiglia (EUR + {eur_635*100000/635*rapporto_fam:.3f}/giorno > 0,10) β€” e' l'unica voce del censimento per cui il + capitale cambia davvero un verdetto pre-registrato. + MA le gambe (1) e (2) restano FAIL, e sono invarianti al capitale: la mediana di famiglia e' il + 6% dell'argmax e il 54% delle celle e' positivo comunque si finanzi il conto. + -> BIN-FREQ a $100k: 1 gate su 4 si sblocca, 2 restano chiusi. RESTA UN LEAD.""") + + +# ============================================================================================= +# SEZ. 7 β€” IL COROLLARIO FUNDED +# ============================================================================================= +def sezione7(res: dict) -> None: + sez("7) IL COROLLARIO FUNDED β€” il canale e' mai stato accreditato di AMPLIARE l'insieme?") + print(""" + Distinzione obbligatoria: fin qui la colonna 'capitale' era CAPITALE PROPRIO. Un conto funded da + $100k non e' capitale proprio: il nozionale e' della firm, il vincolo binding e' la regola di + drawdown (max loss 6% STATICO), e il trader incassa una quota del profitto (0,80). + Domanda: quali voci del censimento diventano ESEGUIBILI a $100k di nozionale funded e non lo sono + a $635 di capitale proprio? E quanto vale, in Sharpe di libro e in EUR/giorno?""") + import r0724_skh_live_weight as skl + import r0725_capcurve as CC + from src.portfolio.portfolio import combine_outer, metrics + skh = skl.skh_book(0, "hourly") + if skh.index.tz is None: + skh.index = skh.index.tz_localize("UTC") + lo = pd.Timestamp("2019-03-14", tz="UTC") + tp = CC.tp01_realistic(100_000.0 * 0.6375) # libro funded $100k, non $635 + base = combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": 0.75, "SKH01": 0.25}, lo=lo) + + sub("7a) chi si aggiunge davvero (capitale PROPRIO $635 -> nozionale FUNDED $100k)") + print(f" {'voce':<26}{'C* (proprio)':>14}{'a $635?':>10}{'a $100k?':>10} perche' non basta") + righe = [ + ("XS01 (19g, HL)", "SI", "SI", "gia' eseguibile a $635 -> NON e' un ampliamento di taglia; " + "sul funded e' un ampliamento di VENUE (Β§30: Bybit 13/13), gia' contato da Β§1 (+0,400 J)"), + ("XSR01 (50g, HL)", "NO", "SI", "AMPLIAMENTO VERO β€” ma gate 23/10 aperto e blocchi invarianti al capitale"), + ("STATARB-MULTI (51g, HL)", "NO", "SI", "AMPLIAMENTO VERO β€” ma t 1,31, IC95 contiene lo zero"), + ] + for nome, a, b, why in righe: + cs = res[nome]["cstar"] + print(f" {nome:<26}{(f'${cs:,.0f}' if cs else '>200k'):>14}{a:>10}{b:>10}") + print(f" {'':<26}{why}") + print(""" + A5: l'insieme AMMISSIBILE non si amplia (entrambi hanno un blocco che il capitale non tocca). + L'insieme ESEGUIBILE si amplia di due voci, ed e' un fatto nuovo: nessun filone del progetto + aveva mai misurato che a $100k STATARB-MULTI e XSR01 girano al 99,6% del nozionale.""") + + sub("7b) quanto varrebbe, se fossero ammissibili β€” ISO-BARRIERA, su FINESTRA COMUNE") + print(""" + A ISO-NOZIONALE un diversificatore a basso CAGR toglie reddito (regola 25/07 Β§3). Su un conto + funded la lente giusta e' la BARRIERA: si cerca la leva k che porta il maxDD alla regola dei -6% + e si legge il drift a quella leva. E' un LIMITE SUPERIORE, non una simulazione (breach a un + giorno, lente wick accoppiata e morte-firm sono Β§37). + FINESTRA COMUNE 2024+ obbligatoria: i candidati esistono solo da li'. Con la finestra piena + `combine_outer` rinormalizza per riga, il maxDD del libro cade PRIMA del 2024 e le tre righe + danno la STESSA k -> il confronto non misura niente (errore commesso e corretto in sessione).""") + lo2 = pd.Timestamp("2024-01-01", tz="UTC") + print(f"\n {'libro (finestra comune 2024+)':<34}{'Sharpe':>8}{'maxDD':>8}{'k@-6%':>8}" + f"{'drift@k':>9}{'EUR/g $100k funded':>21}") + base_rows = {} + for lab, nome in [("LIVE 75/25 (TP01+SKH01)", None), + ("+ XSR01 @15%", "XSR01 (50g, HL)"), + ("+ STATARB-MULTI @15%", "STATARB-MULTI (51g, HL)")]: + if nome is None: + b = combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": 0.75, "SKH01": 0.25}, lo=lo2) + else: + s_ = pd.Series(res[nome]["nets"][100_000.0], index=res[nome]["cand"]["idx"]) + b = combine_outer({"TP01": tp, "SKH01": skh, "C": s_}, + {"TP01": 0.6375, "SKH01": 0.2125, "C": 0.15}, lo=lo2) + b = b[b.index >= lo2] + r = b.values + m = metrics(b) + k = k_barriera(r) + g = cagr_k(r, k, b.index) + eur = SPLIT_FUNDED * g * 100_000.0 / 365.0 / EURUSD + base_rows[lab] = eur + print(f" {lab:<34}{m['sharpe']:>8.3f}{100*m['maxdd']:>7.1f}%{k:>8.2f}{100*g:>8.1f}%" + f"{eur:>21.2f}") + d1 = base_rows["+ XSR01 @15%"] - base_rows["LIVE 75/25 (TP01+SKH01)"] + d2 = base_rows["+ STATARB-MULTI @15%"] - base_rows["LIVE 75/25 (TP01+SKH01)"] + print(f""" + L'AMPLIAMENTO VALE, come LIMITE SUPERIORE: XSR01 {d1:+.2f} EUR/giorno, STATARB-MULTI {d2:+.2f} + EUR/giorno su $100.000 di nozionale funded β€” e ZERO oggi, perche' nessuno dei due e' ammissibile. + ⚠️ La finestra 2024+ NON contiene un sinistro: il maxDD del libro li' e' molto piu' piccolo che + sulla storia piena, quindi k e' gonfiata per TUTTE le righe. Il numero da leggere e' la + DIFFERENZA, non il livello β€” e resta un tetto, perche' il funding dei candidati non e' contato. + + βœ… REPLICA D'ORDINE DI GRANDEZZA: la riga LIVE da' {base_rows['LIVE 75/25 (TP01+SKH01)']:.0f} + EUR/giorno su un conto funded da $100k, contro i "~EUR 15-30/g max" che il progetto pubblica dal + 25/07 per UN conto con cap $200k/trader. Macchineria diversa (qui una k iso-barriera, li' un MC + con breach), stesso ordine di grandezza. + πŸ“Œ E IL CONFRONTO CHE CONTA: la somma di TUTTI i lead dell'ondata 22-23/08 vale +0,036 EUR/giorno + (Β§57). Il solo AMPLIAMENTO DELL'INSIEME su un conto funded vale, come tetto, + {d1:+.2f} / {d2:+.2f} EUR/giorno β€” cioe' 40-100x. Non e' un argomento per aprire un funded: e' un + argomento per smettere di valutare il canale funded SOLO sul nozionale.""") + + +# ============================================================================================= +# SEZ. 8 β€” MDE E VERDETTO +# ============================================================================================= +def sezione8(res: dict) -> None: + sez("8) MDE, LIMITI, VERDETTO") + for nome, r in res.items(): + idx = r["cand"]["idx"] + yrs = (idx[-1] - idx[0]) / pd.Timedelta("365.25D") + print(f" {nome:<26} {yrs:.2f} anni -> SE(Sharpe) ~ {1/np.sqrt(yrs):.2f}, " + f"MDE(95%) ~ {1.96/np.sqrt(yrs):.2f} di Sharpe") + print(""" + Cio' che questo filone PUO' dire con questi dati: dove sta la soglia di eseguibilita' (e' una + misura deterministica sul pavimento del venue, non una stima statistica, e non ha MDE). + Cio' che NON puo' dire: se uno di questi candidati abbia un edge β€” su 2,6 anni l'MDE e' ~1,2 di + Sharpe, cioe' meta' del loro Sharpe modellato. Non lo dico: lo dicono i gate gia' pubblicati. + + LIMITI DICHIARATI + - I 3 candidati misurati vivono su HYPERLIQUID. Che un conto funded (Bybit, Β§30) listi le 50 + gambe di XSR01 NON e' verificato: Β§30 ha verificato le 13 di U13. E' una GET pubblica: costa + zero e va fatta prima di credere alla sez. 7. + - `sim_libro` applica il pavimento per gamba e non modella il lotto minimo per-strumento HL + (lotti $0,01-7,72 sui 19 major, sotto il pavimento $10 -> non binding). Sui 50 alt della coda + non e' verificato strumento per strumento. + - Lo slippage NON e' nel conto: HL-EXEC lo ha refutato come rischio #1 con margine 21x, ma su + UNA fotografia di 172 secondi, e SLIP-AUDIT ha scritto che la partecipazione scala lineare + -> a $100k la misura di attrito a $635 NON si estrapola. La sez. 7 va riletta a quella taglia. + - Il funding NON e' nel conto per i 3 candidati (Β§35 lo ha prezzato per il libro, non per loro): + XS01 paga +1,45%/anno (Β§37) e i due panieri dollar-neutral hanno la stessa esposizione al + prezzo cross-sezionale del funding. Ogni EUR/giorno della sez. 7 e' quindi OTTIMISTA.""") + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--no-net", action="store_true") + a = ap.parse_args() + t0 = time.time() + sez("Β§67 REGIME-CAPITALE β€” gli SCARTATI PER TAGLIA, rimisurati a $635 / $5k / $20k / $100k") + print(f" avvio {datetime.now(timezone.utc):%Y-%m-%d %H:%M:%S} UTC seed {SEED}") + sola_lettura() + print(f" parametri venue: Deribit min ${DER_MIN_ORDER:.0f} / {DER_FEE_SIDE*1e4:.2f} bps ; " + f"Hyperliquid min ${HL_MIN_ORDER:.0f} / {HL_FEE_SIDE*1e4:.2f} bps " + f"(i due HL sono i valori MISURATI il 22/08, non i $5/5,0 bps di memoria)") + print(f" lente del libro: `hourly` (path live) β€” Sh 1,683 / maxDD 11,22%") + + sezione1() + print("\n ricostruzione dei candidati misurabili (repliche prima di ogni delta):") + CAND = carica_candidati() + res = sezione23(CAND) + sezione3d(CAND) + sezione4(a.no_net) + sezione5(res) + sezione6() + sezione7(res) + sezione8(res) + + sez("VERDETTO") + n = len(CENSIMENTO) + n_edge = sum(1 for *_, e in CENSIMENTO if e.startswith("SI")) + nf = sum(1 for _, _, _, st, _ in CENSIMENTO if st.startswith("FALSIFICATO")) + print(f""" + `A $100.000 NESSUN ARCHIVIATO PASSA TUTTI I GATE β€” E NON PERCHE' LI ABBIA RICONTROLLATI: + I GATE PUBBLICATI ERANO GIA' CALCOLATI SULLA SERIE MODELLATA, CHE E' IL LIMITE A CAPITALE + INFINITO. IL MURO DI TAGLIA NON NASCONDEVA NIENTE.` + + Il censimento ha {n} voci; {n_edge} hanno ANCHE un rifiuto sull'edge. Delle {n - n_edge} in cui la + taglia era l'unico motivo, nessuna e' una strategia (sono XS01 β€” gia' sbloccato e fermo per la + decisione di venue β€” e GTAA01, fermo per il PRIIPs). + Muri di taglia caduti: {nf} su {n}, tutti nell'ondata 22-23/08, tutti nella stessa direzione. + Muri di taglia MISURATI QUI per la prima volta: STATARB-MULTI (C* $2.000-$7.500 secondo la + tolleranza) e la curva completa di XSR01 col pavimento del venue GIUSTO (C* $15.000-$20.000, + contro i ~$3.000 che HL-EXEC riportava con un criterio piu' permissivo). + + IN EUR/GIORNO β€” la domanda del progetto: + Questo filone NON avvicina i 50 EUR/giorno. Non aggiunge NIENTE al conto di oggi: tutto cio' + che sblocca vive a capitali che il conto non ha, e a quei capitali resta bloccato sull'edge. + Il suo contributo e' +0,000 EUR/giorno. + Cio' che TOGLIE e' reale ma non e' reddito: il gate XSR01 del 23/10 legge un monitor tarato sul + pavimento del venue sbagliato. + Per confronto, sullo stesso motore: EUR 250 -> 500 al mese porta P(20 anni) dal 14% all'85%. + + Libro, pesi, cron, config INVARIATI. Nessun ordine. + durata {time.time()-t0:.0f} s""") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_retry.py b/scripts/research/r0823b_retry.py new file mode 100644 index 0000000..168509c --- /dev/null +++ b/scripts/research/r0823b_retry.py @@ -0,0 +1,909 @@ +"""r0823b_retry β€” Β§61 RETRY: la POLITICA DI RI-ACQUISTO del biglietto funded (2026-08-23). + +IL BUCO CHE CHIUDE +------------------ +Un conto funded che rompe una regola muore. Il progetto ha modellato tutto il resto del ciclo +(prezzo, regole, leva, lente accoppiata, funding, morte-firm, fisco) e **mai la politica di +RI-ACQUISTO**: quando ricomprare dopo un bust, con quali soldi, e con quale leva sul conto nuovo. +Nei simulatori esistenti (`r0725_prop_ladder.simulate`, `r0725_prop_coupled.simulate`, +`r0822d_gate_c.ladder`) la politica c'e' β€” e' **cablata**: *compra sempre, subito, il biglietto +piu' GRANDE che la cassa permette, in tutti e 6 gli slot*. E' la politica piu' aggressiva della +famiglia, non e' mai stata dichiarata come scelta, e **tutti i numeri operativi del canale funded +escono da li'** (42% -> 7,8% -> 4,4% -> 2,6%). + +E' diventata una domanda vera solo ieri, per due misure: + (a) Β§30: la quota d'ingresso e' un **deposito RIMBORSABILE** ("Refundable Challenge Deposit, + returned in full alongside your first funded payout") -> un tentativo fallito costa la + quota, uno riuscito no. E' una struttura a OPZIONE, e le opzioni si comprano piu' volte. + (b) Β§37: il biglietto batte il libro **solo perche' il piano di versamento c'e'**, cioe' + **perche' si puo' giocare piu' di una volta** ("una lotteria a EV positivo che un conto da + $654 puo' giocare UNA VOLTA SOLA"). +Se il valore sta nel NUMERO DI TENTATIVI, la politica di ri-acquisto e' la variabile principale +e non e' mai stata ottimizzata. + +LE ATTESE, REGISTRATE PRIMA DI MISURARE (poi riportate confermate/refutate) +-------------------------------------------------------------------------- +A1. **Compromesso monotono.** IMMEDIATA massimizza sia il numero di giocate sia P(cassa a zero); + MAI li minimizza entrambi. Nessuna politica dovrebbe dominare su tutte e due le colonne. +A2. **La dose-risposta di Β§37 dovrebbe essere ripida.** Se il meccanismo e' "quante volte si + gioca", passare da max 1 biglietto a illimitati deve muovere P(>=50 EUR/g) di molto. Se non + lo muove, il meccanismo dichiarato da Β§37 e' sbagliato (o e' un'altra cosa). +A3. **Il biglietto PICCOLO ($249/$25k) compra piu' giocate e perde sul bersaglio.** 50 EUR/g netti + sono ~$59k di payout netti in 3 anni: con nozionale $25k servono troppi cicli. Mi aspetto + P(zero) piu' bassa e P(>=50/g) piu' bassa. Dominato sul bersaglio, non sulla sopravvivenza. +A4. **SOLO-PAYOUT ~= MAI senza versamenti** (senza payout non c'e' cassa per ricomprare) e + molto piu' vicina a IMMEDIATA con versamenti. +A5. **Il RIMBORSO alza i livelli di ~1-3pp (Β§37) ma NON cambia l'ordine delle politiche.** + Previsione forte: l'ottimo e' lo stesso nelle tre convenzioni. +A6. 🚨 **Le regole di leva condizionate all'esito passato sono NULL DEL DE-LEVERING.** In un + bootstrap a blocchi il passato non porta informazione sul futuro: una regola "riduci la leva + dopo un bust" puo' agire SOLO cambiando la leva MEDIA. Il controllo obbligatorio e' la leva + COSTANTE alla stessa media realizzata. Mi aspetto REFUTED 3/3 (8Βͺ occorrenza). +A7. **La politica conta meno del versamento.** Mi aspetto uno spread fra politiche di pochi punti + percentuali contro i ~+10pp del passaggio EUR 0 -> EUR 500/mese misurato da Β§37. + +CONVENZIONI (tutte EREDITATE e congelate β€” nessuna scelta di strategia in questo file) +-------------------------------------------------------------------------------------- +* Lente **ACCOPPIATA** (`r0725_prop_coupled`): su una regola a UN GIORNO la close-only e' cieca. +* Conto funded = **W_A TP01 38 / SKH01 38 / XS01 25** (centro della regione robusta di Β§29), + XS01 = **U13 Binance** fuori campione, finestra **W-LONG 2021+**, de-luck sul drift **x0,89**. +* **Funding dentro** (Β§35/Β§37), morte-firm **10%/anno** (assunzione del progetto), cap + **$200k/trader**, fisco **33%**, split **80%**, soglia di prelievo **$100** di quota trader. +* Banda d'ancora: **23 offset x 3 fasi (0,3,7)** = lo stesso sottocampione dichiarato da Β§32/Β§37. + La cella di riferimento e' la **MEDIANA** della banda, non la canonica (91Β° pctl). +* Cassa non spesa: **FERMA** per default (convenzione di Β§37) e **SUL LIBRO** come sensibilita' + dichiarata in Β§7 β€” perche' la convenzione "ferma" penalizza per costruzione le politiche + prudenti, che sono esattamente quelle sotto esame. +* Bootstrap **CONDIVISO** fra tutte le politiche di una stessa tabella -> confronti **APPAIATI**. + +COSA NON E' QUESTO FILE: non e' una selezione di strategia (meccanismo, pesi, leva ottima, lente +e soglie sono tutti ereditati); e' la mappatura di una politica OPERATIVA che finora era cablata. +L'argmax su ~30 politiche resta pero' una selezione: si riportano il PLATEAU e la risoluzione MC. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823b_retry.py` +Rete: NESSUNA (parquet certificati + cache Binance di r0822b + cache funding di r0822d). +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) + +if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import + sys.argv.append("--no-net") + +import r0725_prop_coupled as pc # noqa: E402 lente accoppiata (25/07) +import r0725_prop_ladder as pl # noqa: E402 regole firm + scala di conti (25/07) +import r0822_prop_alloc as PA # noqa: E402 J, bootstrap (22/08) +import r0822b_prop_recal as PR # noqa: E402 PROP-RECAL (22/08) +import r0822b_xs01_oos as X # noqa: E402 motore XS01 + cache Binance +import r0822c_skh_anchor_prop as AP # noqa: E402 offset di SKH01 (gamba b) +import r0822d_funding as FD # noqa: E402 il funding (22/08) +import r0822d_gate_c as GC # noqa: E402 ladder di riferimento (Β§37) +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +EURUSD = pl.EURUSD +TAX_RATE = pl.TAX_RATE +SPLIT = pl.SPLIT +DELUCK = PA.DELUCK +W_A = GC.W_A +OFFSETS = AP.OFFSETS +PHASES_SUB = AP.PHASES_SUB +MIN_PAYOUT = GC.MIN_PAYOUT_TRADER +HORIZON_DAYS = GC.HORIZON_DAYS +SEED_LAD = GC.SEED_LAD +XS_FUND_ANN = 0.0145 # Β§37: XS01 PAGA +1,45%/anno di funding (misurato) + +N_BAND = 1000 # percorsi per la banda d'ancora (riproduzione Β§32/Β§37) +N_MAIN = 2000 # percorsi per la griglia di politiche (cella di rif.) +N_ROB = 1000 # percorsi per la robustezza fra celle +# Sottocampione di offset dichiarato A PRIORI: gli stessi 8 uniformi di `r0726_skh_live_book` +# (la griglia piena e' 23; su 2 CPU condivise con altri 5 processi la piena non sta nel budget, +# e un sottocampione uniforme dichiarato prima e' l'unica forma onesta di ridurla). +BAND_OFFSETS = tuple(range(0, 690, 90)) + +# numeri PUBBLICATI da riprodurre prima di produrne di nuovi +PUB = dict(anchor_p50g=0.044, anchor_p10=0.025, anchor_p90=0.082, anchor_pzero=0.353, + gatec_p50g=0.026, gatec_p10=0.015, gatec_p90=0.047, gatec_pzero=0.403, + refund_pp_lo=0.01, refund_pp_hi=0.03) + +TRIALS = 0 +T0 = time.time() +CHEAPEST = min(t[1] for F in pl.FIRMS for t in F["tickets"]) # $249 + + +def bump(n: int = 1) -> None: + global TRIALS + TRIALS += n + + +def hr(t: str = "") -> None: + print("\n" + "-" * 104) + if t: + print(f" {t}") + print("-" * 104) + + +def tick(lab: str) -> None: + print(f" [{time.time() - T0:6.0f}s] {lab}", flush=True) + + +# ============================================================================ LE POLITICHE +def POL(when="immediate", k_reserve=1.0, cadence=0, max_tickets=10 ** 9, size="largest", + lev_rule="const", lev_dn=1.0, lev_up=1.0, lev_lo=0.25, lev_hi=1.50, + max_open=pl.MAX_CONCURRENT) -> dict: + """Una politica di ri-acquisto. `when`: + immediate β€” compra appena uno slot e' libero e la cassa basta (= il CABLATO di oggi) + threshold β€” compra solo se la cassa e' >= k_reserve x il prezzo (tiene una riserva) + earned β€” il PRIMO biglietto dai propri soldi, i successivi SOLO dai payout/rimborsi + cadence β€” al piu' un acquisto ogni `cadence` giorni + none β€” non compra MAI (controllo positivo: deve dare P(>=50/g)=0) + `size`: 'largest' (cablato) o 'cheapest' (piu' giocate, nozionale minore). + `max_open`: quanti conti al MASSIMO possono essere aperti insieme (cablato: 6 = tutti gli + slot). E' l'altra faccia del ri-acquisto: giocare in SERIE invece che in PARALLELO. + `lev_rule`: 'const' | 'down_on_bust' (x lev_dn a ogni bust) | 'up_on_pass' (x lev_up a ogni + pass). La leva di un conto e' fissata all'ACQUISTO e non cambia piu'.""" + return dict(when=when, k_reserve=k_reserve, cadence=cadence, max_tickets=max_tickets, + size=size, lev_rule=lev_rule, lev_dn=lev_dn, lev_up=lev_up, + lev_lo=lev_lo, lev_hi=lev_hi, max_open=max_open) + + +P_BASE = POL() # il cablato, bit-exact vs Β§37 + + +# ============================================================================ IL SIMULATORE +def ladder_rt(R, G, n_paths, lev, seed, *, pol=None, refund_mode="none", deposit_eur=0.0, + start_eur=pl.START_EUR, min_payout=0.0, boot=None, block=20, deluck=DELUCK, + rb_boot=None, tax=TAX_RATE) -> dict: + """`r0822d_gate_c.ladder` generalizzato alla POLITICA DI RI-ACQUISTO. Tre aggiunte: + 1. `pol` β€” quando/quale biglietto/con quali soldi/a che leva (sopra); + 2. `refund_mode` β€” 'none' (spesa persa, `pc.simulate`) | 'pass' (`r0725_hyro:200`) | + 'payout' (la regola vera della firm); + 3. `rb_boot` β€” se dato, la cassa NON spesa gira sul LIBRO LIVE invece di restare + ferma, con lo STESSO sorteggio di bootstrap (stesso mercato) e lo + stesso fisco annuale di `GC.book_road`. + Con `pol=P_BASE, refund_mode='none', deposit_eur=0, min_payout=0, rb_boot=None` dev'essere + **BIT-EXACT** rispetto a `GC.ladder(..., refund=False)` e quindi a `pc.simulate`: e' Β§1. + + 🚨 Zero e' ASSORBENTE ed e' modellato come tale: senza conti vivi e con cassa sotto il + biglietto piu' economico ($249) non si gioca piu'; solo un versamento puo' riaprire il gioco. + """ + pol = pol or P_BASE + rng = np.random.default_rng(seed) + n_days = HORIZON_DAYS + L = len(R) + Rm = R.astype(float).reshape(-1, 1).copy() + Gm = G.astype(float).reshape(-1, 1).copy() + Rm = Rm - (1.0 - deluck) * Rm.mean(axis=0, keepdims=True) + if boot is None: + n_blocks = int(np.ceil(n_days / block)) + starts = rng.integers(0, L - block, size=(n_paths, n_blocks)) + idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] + else: + idx = boot + n_slot = pl.MAX_CONCURRENT + # (paths, days): tutti gli slot girano lo STESSO libro -> una colonna basta. La leva + # moltiplica per-slot (serve alle regole di leva) invece che globalmente: `x*lev` e' la + # stessa moltiplicazione in virgola mobile, quindi il bit-exact regge (verificato in Β§1). + Rb = Rm[idx][:, :, 0] + Gb = Gm[idx][:, :, 0] + + FIRMS = pl.FIRMS + cash = np.full(n_paths, start_eur * EURUSD) + basis = cash.copy() + carry = np.zeros(n_paths) + earned = np.zeros(n_paths) # borsellino dei soli payout/rimborsi + phase = np.full((n_paths, n_slot), -1, np.int8) + eq = np.ones((n_paths, n_slot)) + firm = np.full((n_paths, n_slot), -1, np.int8) + notional = np.zeros((n_paths, n_slot)) + price_paid = np.zeros((n_paths, n_slot)) + refunded = np.zeros((n_paths, n_slot), bool) + lev_slot = np.full((n_paths, n_slot), lev) + # soglie della firm materializzate PER SLOT all'acquisto: la dinamica diventa una sola + # espressione vettoriale invece di un loop sulle firm. Le maschere per firm sono disgiunte, + # quindi il risultato e' identico (verificato bit-exact in Β§1). + TH_TGT = np.zeros((n_paths, n_slot)) + TH_DD = np.ones((n_paths, n_slot)) + TH_DL = np.ones((n_paths, n_slot)) + TH_ML = np.ones((n_paths, n_slot)) + TH_DLF = np.ones((n_paths, n_slot)) + mult = np.ones(n_paths) + firm_alive = np.ones((n_paths, len(FIRMS)), bool) + cum_payout = np.zeros(n_paths) + spent = np.zeros(n_paths) + got_back = np.zeros(n_paths) + deposited = np.zeros(n_paths) + n_payouts = np.zeros(n_paths, int) + n_tickets = np.zeros(n_paths, int) + lev_sum = np.zeros(n_paths) + last_buy = np.full(n_paths, -10 ** 9) + days_out = np.zeros(n_paths, int) + ever_passed = np.zeros(n_paths, bool) + p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0) + tickets_order = {} + for fi, F in enumerate(FIRMS): + tickets_order[fi] = sorted(F["tickets"], key=(lambda x: x[0]) if pol["size"] == "cheapest" + else (lambda x: -x[0])) + + for t in range(n_days): + died = rng.random((n_paths, len(FIRMS))) < p_death_day + newly_dead = died & firm_alive + firm_alive &= ~died + if newly_dead.any(): + for fi in range(len(FIRMS)): + kill = newly_dead[:, fi] + if kill.any(): + hit = (firm == fi) & kill[:, None] + phase[hit] = -1 + notional[hit] = 0.0 + + # ---------------------------------------------------------------- acquisti + if pol["when"] == "none": + budget = np.zeros(n_paths, int) + elif pol["when"] == "cadence": + budget = ((t - last_buy) >= pol["cadence"]).astype(int) + else: + budget = np.full(n_paths, n_slot, int) + budget = np.minimum(budget, np.maximum(pol["max_tickets"] - n_tickets, 0)) + if pol["max_open"] < n_slot: + budget = np.minimum(budget, np.maximum(pol["max_open"] - (phase >= 0).sum(axis=1), 0)) + if budget.any() and (cash >= CHEAPEST).any(): + for k in range(n_slot): + free = (phase[:, k] == -1) & (budget > 0) + if not free.any(): + continue + for fi, F in enumerate(FIRMS): + used = np.where(firm == fi, notional, 0.0).sum(axis=1) + for (size, price) in tickets_order[fi]: + buy = (free & firm_alive[:, fi] & (cash >= price) + & (used + size <= F["cap"])) + if pol["when"] == "threshold": + buy &= cash >= pol["k_reserve"] * price + elif pol["when"] == "earned": + buy &= (n_tickets == 0) | (earned >= price) + if not buy.any(): + continue + cash[buy] -= price + basis[buy] -= price + if pol["when"] == "earned": + take = buy & (n_tickets > 0) + earned[take] -= price + spent[buy] += price + n_tickets[buy] += 1 + budget[buy] -= 1 + last_buy[buy] = t + phase[buy, k] = 0 + eq[buy, k] = 1.0 + firm[buy, k] = fi + notional[buy, k] = size + price_paid[buy, k] = price + refunded[buy, k] = False + lv = np.clip(lev * mult, pol["lev_lo"], pol["lev_hi"]) + lev_slot[buy, k] = lv[buy] + TH_TGT[buy, k], TH_DD[buy, k], TH_DL[buy, k] = F["ev"] + TH_ML[buy, k], TH_DLF[buy, k] = F["fu"] + lev_sum[buy] += lv[buy] + free = free & ~buy + if not free.any(): + break + if not free.any(): + break + + # ---------------------------------------------------------------- dinamica dei conti + r = Rb[:, t][:, None] * lev_slot + g = Gb[:, t][:, None] * lev_slot + act = phase >= 0 + eq_prev = eq + eq = np.where(act, eq * (1.0 + r), eq) + eq_low = np.where(act, eq_prev * (1.0 + r + g), eq) + r_low = r + g + + in_ev = act & (phase == 0) + in_fu = act & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - TH_DD) | (r_low < -TH_DL))) + | (in_fu & ((eq_low < 1.0 - TH_ML) | (r_low < -TH_DLF)))) + phase[bust] = -1 + notional[bust] = 0.0 + passed = in_ev & (eq >= 1.0 + TH_TGT) & ~bust + phase[passed] = 1 + eq[passed] = 1.0 + if pol["lev_rule"] == "down_on_bust": + mult *= pol["lev_dn"] ** bust.sum(axis=1) + elif pol["lev_rule"] == "up_on_pass": + mult *= pol["lev_up"] ** passed.sum(axis=1) + if refund_mode == "pass" and passed.any(): + back = passed & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + basis += amt + earned += amt + got_back += amt + refunded |= back + ever_passed |= passed.any(axis=1) + + # ---------------------------------------------------------------- la cassa propria + if rb_boot is not None: + cash = np.maximum(cash * (1.0 + rb_boot[:, t]), 0.0) + + # ---------------------------------------------------------------- mensile + if (t + 1) % 30 == 0: + fu = phase == 1 + pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout) + gain = np.where(pay, (eq - 1.0) * notional, 0.0) + net = gain.sum(axis=1) * SPLIT * (1.0 - TAX_RATE) + cum_payout += net + cash += net + basis += net + earned += net + n_payouts += (pay.sum(axis=1) > 0).astype(int) + eq = np.where(pay, 1.0, eq) + if refund_mode == "payout": + back = pay & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + basis += amt + earned += amt + got_back += amt + refunded |= back + if deposit_eur: + cash += deposit_eur * EURUSD + basis += deposit_eur * EURUSD + deposited += deposit_eur * EURUSD + # fisco annuale sulla sola cassa propria (i payout sono gia' netti) + if rb_boot is not None and (t + 1) % 365 == 0: + gtax = (cash - basis) + carry + due = np.where(gtax > 0, tax * gtax, 0.0) + carry = np.where(gtax > 0, 0.0, gtax) + cash = cash - due + basis = cash.copy() + + out = (~(phase >= 0).any(axis=1)) & (cash < CHEAPEST) + days_out += out + + eur_day = cum_payout / EURUSD / (n_days / 365.0) / 365.0 + stuck = (~(phase >= 0).any(axis=1)) & (cash < CHEAPEST) + return dict(eur_day=eur_day, med=float(np.median(eur_day)), + p10=float(np.percentile(eur_day, 10)), p90=float(np.percentile(eur_day, 90)), + mean_eur=float(eur_day.mean()), + p_ge10=float((eur_day >= 10.0).mean()), + p_target=float((eur_day >= pl.TARGET_EUR_DAY).mean()), + p_zero=float((cum_payout <= 0).mean()), + p_payout=float((n_payouts > 0).mean()), + p_pass=float(ever_passed.mean()), + p_stuck=float(stuck.mean()), + n_tix=n_tickets, med_tix=float(np.median(n_tickets)), + mean_tix=float(n_tickets.mean()), + lev_mean=float(lev_sum.sum() / max(n_tickets.sum(), 1)), + days_out=float(np.median(days_out)), + cash=cash, med_cash=float(np.median(cash)), mean_cash=float(cash.mean()), + p10_cash=float(np.percentile(cash, 10)), + p90_cash=float(np.percentile(cash, 90)), + med_spent=float(np.median(spent)), mean_back=float(got_back.mean()), + contributed=float(start_eur * EURUSD + deposited.mean())) + + +def se_pair(a: np.ndarray, b: np.ndarray, thr: float = pl.TARGET_EUR_DAY) -> float: + """SE della DIFFERENZA fra due proporzioni APPAIATE (stessi sorteggi): McNemar. + Con bootstrap condiviso la parte comune si cancella e la risoluzione e' molto migliore + di quella dei livelli β€” ma non e' zero, e va stampata.""" + A = a >= thr + B = b >= thr + n01 = float((A & ~B).sum()) + n10 = float((~A & B).sum()) + n = len(a) + return float(np.sqrt(max(n01 + n10 - (n01 - n10) ** 2 / n, 0.0)) / n) + + +# ==================================================================================== main +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + print("=" * 104) + print(" Β§61 RETRY β€” la POLITICA DI RI-ACQUISTO del biglietto funded, mappata invece che") + print(" cablata. Quando ricomprare, con quali soldi, a che leva β€” e quanto conta.") + print("=" * 104) + print(" ATTESE REGISTRATE PRIMA: A1 compromesso monotono giocate/rovina; A2 dose-risposta") + print(" RIPIDA se il meccanismo di Β§37 e' 'il numero di volte che si gioca'; A3 biglietto") + print(" piccolo = piu' giocate, meno bersaglio; A4 SOLO-PAYOUT ~= MAI senza versamenti;") + print(" A5 il rimborso NON cambia l'ottimo; A6 le regole di leva sono null del de-levering") + print(" (REFUTED 3/3 atteso); A7 la politica conta meno del versamento.") + + # ============================================================== Β§0 pannello + hr("Β§0. IL PANNELLO β€” le stesse serie di Β§29/Β§37 (U13 Binance, W-LONG 2021+, funding dentro)") + rate = GC.funding_rate() + if rate is None: + print(" !! cache funding assente: il filone si ferma. Un numero senza funding NON e'") + print(" confrontabile con Β§37, e confrontarlo sarebbe l'errore che Β§35 ha appena") + print(" pagato. 'non misurabile' e' un risultato.") + return + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13 = Ob.reindex(p13.index)[X.U13] + h13 = Hb.reindex(p13.index)[X.U13] + l13 = Lb.reindex(p13.index)[X.U13] + XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES_SUB} + xs0 = XSP[PHASES_SUB[0]] + base = pc.crypto_daily_tuples(1.0, 0.0).index.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + I = base[base >= max(first13, pd.Timestamp("2021-01-01", tz="UTC"))] + pc.DELUCK = DELUCK + print(f" W-LONG: {len(I)} giorni {I[0].date()} -> {I[-1].date()} | de-luck x{DELUCK} | " + f"cassa iniziale EUR {pl.START_EUR:.0f} = ${pl.START_EUR*EURUSD:,.0f}") + print(f" orizzonte {HORIZON_DAYS} giorni ({HORIZON_DAYS/30:.0f} versamenti da 30 giorni: " + f"Β§57 avverte che 'EUR X/mese' qui vuol dire ogni 30 giorni β€” su 36 mesi torna esatto)") + print(f" biglietti disponibili: " + " | ".join( + f"{F['name']} " + ", ".join(f"${s/1000:.0f}k@${p:.0f}" for s, p in F['tickets']) + for F in pl.FIRMS)) + print(f" biglietto piu' economico ${CHEAPEST:.0f} -> sotto questa cassa e senza conti vivi") + print(f" il percorso e' FUORI GIOCO (zero assorbente).") + tick("Β§0") + + # ============================================================== Β§1 controlli + hr("Β§1. CONTROLLI β€” bit-exact contro il simulatore pubblicato, e il controllo POSITIVO") + R0, G0 = AP.rg_off(W_A, I, XSP[0], 0) + f0 = GC.fvec_for(W_A, I, rate, 0, XS_FUND_ANN) + Rc0 = R0 - f0 + print(" Β§1.1 la mia `ladder_rt` col cablato (immediate/largest/const, nessun rimborso,") + print(" nessun versamento, soglia 0, cassa ferma) contro `GC.ladder` di Β§37:") + a = GC.ladder(Rc0, G0, N_BAND, 0.75, SEED_LAD, refund=False) + b = ladder_rt(Rc0, G0, N_BAND, 0.75, SEED_LAD, pol=P_BASE) + d1 = float(np.abs(a["eur_day"] - b["eur_day"]).max()) + print(f" max|diff| percorso per percorso = {d1:.3e} -> " + f"{'IDENTICO' if d1 < 1e-15 else 'DIVERGE β€” STOP'}") + if d1 >= 1e-15: + print(" Senza questo controllo i numeri sotto sarebbero di un'altra macchina.") + return + bump(2) + print("\n Β§1.2 e `GC.ladder` e' a sua volta bit-exact contro `pc.simulate` (Β§37 Β§4.1);") + print(" la catena e' quindi: ladder_rt == GC.ladder == pc.simulate.") + cfg = tuple(W_A.get(n, 0.0) for n in PA.CRYPTO_PROP) + key = (cfg[0], cfg[1], cfg[2], "shared") + saved = pc._TUP_CACHE.pop(key, None) + pc._TUP_CACHE[key] = (Rc0, G0, I) + ref = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=N_BAND, lev=0.75, + seed=SEED_LAD, lens="coupled", start=str(I[0].date())) + pc._TUP_CACHE.pop(key, None) + if saved is not None: + pc._TUP_CACHE[key] = saved + d2 = float(np.abs(ref["eur_day"] - b["eur_day"]).max()) + print(f" max|diff| ladder_rt vs pc.simulate = {d2:.3e} -> " + f"{'IDENTICO' if d2 < 1e-15 else 'DIVERGE'}") + bump(1) + + print("\n Β§1.3 CONTROLLO POSITIVO del selettore di politica β€” un interruttore che non") + print(" cambia niente e' indistinguibile da un interruttore rotto.") + rngb = np.random.default_rng(51224) + nb = int(np.ceil(HORIZON_DAYS / 20)) + st = rngb.integers(0, len(I) - 20, size=(N_MAIN, nb)) + BOOT = (st[:, :, None] + np.arange(20)[None, None, :]).reshape(N_MAIN, -1)[:, :HORIZON_DAYS] + rb_full = FD.book_75_25(rate, alloc=654.0) + if rb_full.index.tz is None: + rb_full.index = rb_full.index.tz_localize("UTC") + rb = np.nan_to_num(rb_full.reindex(I).values.astype(float)) + rb_dl = rb - (1.0 - DELUCK) * rb.mean() + RB_BOOT = rb_dl[BOOT] + none_pol = ladder_rt(Rc0, G0, N_MAIN, 0.75, SEED_LAD, pol=POL(when="none"), boot=BOOT) + bump(1) + print(f" politica 'MAI COMPRARE': biglietti mediani {none_pol['med_tix']:.0f}, " + f"P(>=50/g) {none_pol['p_target']:.1%}, P(zero payout) {none_pol['p_zero']:.1%} -> " + f"{'OK' if none_pol['p_target'] == 0 and none_pol['p_zero'] == 1 else 'ROTTO'}") + print("\n Β§1.4 CONTROLLO POSITIVO della cassa-sul-libro: 'MAI COMPRARE' + cassa sul libro") + print(" dev'essere la strada LIBRO di Β§37 (`GC.book_road`), stessi sorteggi.") + nb_book = ladder_rt(Rc0, G0, N_MAIN, 0.75, SEED_LAD, pol=POL(when="none"), boot=BOOT, + rb_boot=RB_BOOT) + road = GC.book_road(rb, BOOT, pl.START_EUR, 0.0) + d3 = float(np.abs(nb_book["cash"] - road["cash"]).max()) + bump(2) + print(f" max|diff| sulla cassa finale = {d3:.3e} -> " + f"{'IDENTICO' if d3 < 1e-9 else 'DIVERGE'} | mediana ${nb_book['med_cash']:,.0f} " + f"(GC.book_road ${road['med_cash']:,.0f})") + tick("Β§1") + + # ============================================================== Β§2 banda d'ancora + hr("Β§2. LA BANDA D'ANCORA β€” riproduzione di Β§32/Β§37 e scelta della cella MEDIANA") + print(f" {len(BAND_OFFSETS)} offset (sottocampione uniforme a priori di r0726, la griglia") + print(f" piena e' 23) x {len(PHASES_SUB)} fasi {PHASES_SUB} = " + f"{len(BAND_OFFSETS)*len(PHASES_SUB)} celle, funding DENTRO, politica CABLATA (quella") + print(f" con cui i numeri sono stati pubblicati), {N_BAND} percorsi per cella.") + ev_b = PA.boot_idx(len(I), GC.EV_H, PA.N_PATHS, seed=GC.SEED_EV) + fu_b = PA.boot_idx(len(I), GC.FU_H, PA.N_PATHS, seed=GC.SEED_FU) + cells = [] + for off in BAND_OFFSETS: + fA = GC.fvec_for(W_A, I, rate, off, XS_FUND_ANN) + for ph in PHASES_SUB: + Rx, Gx = AP.rg_off(W_A, I, XSP[ph], off) + Rf = Rx - fA + j, lv, _ = GC.best_lev(Rf, Gx, ev_b, fu_b) + lad = ladder_rt(Rf, Gx, N_BAND, lv, SEED_LAD, pol=P_BASE) + cells.append(dict(off=off, ph=ph, J=j, lev=lv, p50=lad["p_target"], + pz=lad["p_zero"], med=lad["med"])) + bump(1) + C = pd.DataFrame(cells) + print(f"\n P(>=50 EUR/g) mediana {C['p50'].median():.1%} (PUB Β§37 {PUB['gatec_p50g']:.1%}) | " + f"banda [{np.percentile(C['p50'],10):.1%}, {np.percentile(C['p50'],90):.1%}] " + f"(PUB [{PUB['gatec_p10']:.1%}, {PUB['gatec_p90']:.1%}]) | " + f"P(zero) {C['pz'].median():.1%} (PUB {PUB['gatec_pzero']:.1%})") + can = C[(C["off"] == 0) & (C["ph"] == 0)].iloc[0] + pct_can = float((C["p50"] <= can["p50"]).mean()) + print(f" cella CANONICA (off 0, fase 0): P(>=50/g) {can['p50']:.1%} = {pct_can:.0%}Β° pctl " + f"della banda -> il gia' noto: la canonica e' la cella FORTUNATA, non la stima.") + k_med = int((C["p50"] - C["p50"].median()).abs().idxmin()) + OFF_M, PH_M, LEV_M = int(C.loc[k_med, "off"]), int(C.loc[k_med, "ph"]), float(C.loc[k_med, "lev"]) + print(f" CELLA DI RIFERIMENTO (mediana della banda): offset {OFF_M} min, fase {PH_M}, " + f"leva {LEV_M:.2f}x, P(>=50/g) {C.loc[k_med,'p50']:.1%}") + fM = GC.fvec_for(W_A, I, rate, OFF_M, XS_FUND_ANN) + RxM, G_M = AP.rg_off(W_A, I, XSP[PH_M], OFF_M) + R_M = RxM - fM + tick("Β§2") + + # ============================================================== Β§3 dose-risposta + hr("Β§3. QUANTE VOLTE SI GIOCA β€” la dose-risposta che Β§37 dichiara come IL meccanismo") + print(" Politica IMMEDIATA (il cablato) con un tetto DURO al numero di biglietti mai") + print(" comprati. E' l'esperimento pulito: cambia SOLO quante volte si puo' giocare.") + print(f" Cella di riferimento, {N_MAIN} percorsi, bootstrap condiviso, deposito rimborsato") + print(" al primo payout (la regola vera), soglia di prelievo $100, nessun versamento.") + print(f"\n {'tetto':>8} {'giocate med':>12} {'giocate medie':>14} {'P(>=50/g)':>10} " + f"{'P(>=10/g)':>10} {'P(zero payout)':>15} {'P(fuori gioco)':>15} {'EUR/g medio':>12}") + dose = {} + for mt in (1, 2, 3, 5, 8, 10 ** 9): + r = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=POL(max_tickets=mt), + refund_mode="payout", min_payout=MIN_PAYOUT, boot=BOOT) + dose[mt] = r + bump(1) + lab = "illim." if mt > 100 else str(mt) + print(f" {lab:>8} {r['med_tix']:>12.0f} {r['mean_tix']:>14.2f} {r['p_target']:>10.1%} " + f"{r['p_ge10']:>10.1%} {r['p_zero']:>15.1%} {r['p_stuck']:>15.1%} " + f"{r['mean_eur']:>12.2f}") + d1_, dinf = dose[1], dose[10 ** 9] + se = se_pair(dinf["eur_day"], d1_["eur_day"]) + print(f"\n DA 1 GIOCATA A ILLIMITATE: P(>=50/g) {d1_['p_target']:.1%} -> " + f"{dinf['p_target']:.1%} ({(dinf['p_target']-d1_['p_target'])*100:+.2f}pp, " + f"SE appaiata {se*100:.2f}pp) | giocate mediane {d1_['med_tix']:.0f} -> " + f"{dinf['med_tix']:.0f} | P(zero) {d1_['p_zero']:.1%} -> {dinf['p_zero']:.1%}") + ripida = (dinf["p_target"] - d1_["p_target"]) > 3 * se + print(f" >>> ATTESA A2 ('dose-risposta RIPIDA se il meccanismo e' il numero di giocate'): " + f"{'CONFERMATA' if ripida else 'REFUTATA'}") + tick("Β§3") + + # ============================================================== Β§4 le politiche + hr("Β§4. LE POLITICHE DI RI-ACQUISTO β€” 7 regole di TEMPO x 2 di TAGLIA, senza versamenti") + print(" Le due colonne che vanno SEMPRE lette insieme: 'giocate' e 'P(fuori gioco)'. Una") + print(" politica aggressiva aumenta entrambe, ed e' esattamente la trappola dichiarata.") + print("\n Prima di misurare, l'ARITMETICA del listino β€” prezzo per $1.000 di nozionale:") + for F in pl.FIRMS: + print(" " + f"{F['name']:>5} " + " ".join( + f"${s_/1000:.0f}k: ${p_/(s_/1000):.2f}/k" for s_, p_ in F["tickets"])) + print(" -> il biglietto GRANDE costa MENO per unita' di nozionale (HYRO $5,79/k contro") + print(" $9,96/k). Piu' giocate piccole NON sono piu' esposizione: sono meno.") + POLS = [ + ("IMMEDIATA (il cablato)", POL()), + ("SOGLIA 2x il prezzo", POL(when="threshold", k_reserve=2.0)), + ("SOGLIA 3x il prezzo", POL(when="threshold", k_reserve=3.0)), + ("SOLO DAI PAYOUT", POL(when="earned")), + ("CADENZA 90 giorni", POL(when="cadence", cadence=90)), + ("CADENZA 180 giorni", POL(when="cadence", cadence=180)), + ("UN CONTO ALLA VOLTA", POL(max_open=1)), + ("DUE CONTI ALLA VOLTA", POL(max_open=2)), + ("MAI (1 biglietto)", POL(max_tickets=1)), + ] + res4 = {} + for size in ("largest", "cheapest"): + lab_s = "biglietto PIU' GRANDE ($100k)" if size == "largest" else "biglietto PIU' ECONOMICO ($25k)" + print(f"\n --- {lab_s} ---") + print(f" {'politica':>26} {'giocate med':>12} {'P(>=50/g)':>10} {'P(>=10/g)':>10} " + f"{'P(zero)':>9} {'P(fuori gioco)':>15} {'EUR/g med':>10} {'cassa fin. med':>15}") + for name, p in POLS: + pp = dict(p) + pp["size"] = size + r = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=pp, refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT) + res4[(size, name)] = r + bump(1) + print(f" {name:>26} {r['med_tix']:>12.0f} {r['p_target']:>10.1%} " + f"{r['p_ge10']:>10.1%} {r['p_zero']:>9.1%} {r['p_stuck']:>15.1%} " + f"{r['med']:>10.2f} ${r['med_cash']:>14,.0f}") + base_r = res4[("largest", "IMMEDIATA (il cablato)")] + print(f"\n DIFFERENZE APPAIATE contro il cablato (stessi sorteggi), su P(>=50 EUR/g):") + print(f" {'politica':>26} {'dP(>=50/g)':>12} {'SE appaiata':>13} {'dP(fuori gioco)':>17} " + f"{'d giocate med':>14}") + rows4 = [] + for size in ("largest", "cheapest"): + for name, _ in POLS: + r = res4[(size, name)] + if (size, name) == ("largest", "IMMEDIATA (il cablato)"): + continue + s = se_pair(r["eur_day"], base_r["eur_day"]) + tag = name if size == "largest" else "[pic] " + name + rows4.append(dict(tag=tag, d=r["p_target"] - base_r["p_target"], se=s, + dz=r["p_stuck"] - base_r["p_stuck"], + dt=r["med_tix"] - base_r["med_tix"], p50=r["p_target"], + pstuck=r["p_stuck"])) + print(f" {tag:>26} {(r['p_target']-base_r['p_target'])*100:>11.2f}pp " + f"{s*100:>12.2f}pp {(r['p_stuck']-base_r['p_stuck'])*100:>16.2f}pp " + f"{r['med_tix']-base_r['med_tix']:>14.0f}") + R4 = pd.DataFrame(rows4) + sig = R4[R4["d"].abs() > 2 * R4["se"]] + print(f"\n Politiche che si distinguono dal cablato oltre 2 SE appaiate: " + f"{len(sig)}/{len(R4)}" + + (f" (fra cui {', '.join(list(sig['tag'].str.strip())[:4])}...)" if len(sig) else "")) + allp = pd.concat([R4[["tag", "p50", "pstuck"]], + pd.DataFrame([dict(tag="IMMEDIATA (il cablato)", p50=base_r["p_target"], + pstuck=base_r["p_stuck"])])], ignore_index=True) + print(f" Spread fra la politica migliore e la peggiore su P(>=50/g): " + f"{(allp['p50'].max()-allp['p50'].min())*100:.2f}pp " + f"(max {allp.loc[allp['p50'].idxmax(),'tag'].strip()} {allp['p50'].max():.1%}, " + f"min {allp.loc[allp['p50'].idxmin(),'tag'].strip()} {allp['p50'].min():.1%})") + s3 = res4[("largest", "SOGLIA 3x il prezzo")] + print(f"\n ⚠ A $654 la SOGLIA 3x e' degenere: il biglietto piu' economico costa ${CHEAPEST:.0f}") + print(f" e 3x sono ${3*CHEAPEST:.0f} > ${pl.START_EUR*EURUSD:,.0f} -> giocate mediane " + f"{s3['med_tix']:.0f}. Non e' una politica prudente, e' 'non giocare' scritta") + print(f" con un'altra sintassi. La soglia in VALUTA ASSOLUTA dipende dal capitale:") + print(f" stessa lezione della banda di GTAA01 (27/07), su un altro parametro.") + pic = res4[("cheapest", "IMMEDIATA (il cablato)")] + a3a = pic["p_target"] < base_r["p_target"] + a3b = pic["p_stuck"] < base_r["p_stuck"] + print(f"\n >>> ATTESA A3: il biglietto piccolo perde sul bersaglio " + f"({pic['p_target']:.1%} contro {base_r['p_target']:.1%}) -> " + f"{'confermata' if a3a else 'REFUTATA'}; e sulla ROVINA " + f"({pic['p_stuck']:.1%} contro {base_r['p_stuck']:.1%}) -> " + f"{'confermata' if a3b else 'REFUTATA'}. Verdetto A3: " + f"{'CONFERMATA' if (a3a and a3b) else 'PARZIALE/REFUTATA'}") + tick("Β§4") + + # ============================================================== Β§5 la leva + hr("Β§5. LA LEVA DEL CONTO NUOVO β€” e il NULL DEL DE-LEVERING, dichiarato PRIMA") + print(" 🚨 In un bootstrap a blocchi il passato NON porta informazione sul futuro: una") + print(" regola 'riduci la leva dopo un bust' puo' agire SOLO cambiando la leva MEDIA. Il") + print(" controllo obbligatorio e' la leva COSTANTE alla stessa media realizzata. Se la") + print(" regola non la batte, e' de-levering travestito (sarebbe l'8Βͺ occorrenza).") + lev_rules = [ + ("costante (il cablato)", POL()), + ("RIDOTTA x0,80 a ogni bust", POL(lev_rule="down_on_bust", lev_dn=0.80)), + ("RIDOTTA x0,60 a ogni bust", POL(lev_rule="down_on_bust", lev_dn=0.60)), + ("CRESCENTE x1,25 a ogni pass", POL(lev_rule="up_on_pass", lev_up=1.25)), + ] + print(f"\n {'regola di leva':>30} {'leva media':>11} {'P(>=50/g)':>10} {'P(zero)':>9} " + f"{'P(fuori gioco)':>15} {'EUR/g medio':>12}") + lev_res = {} + for name, p in lev_rules: + r = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=p, refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT) + lev_res[name] = r + bump(1) + print(f" {name:>30} {r['lev_mean']:>11.3f} {r['p_target']:>10.1%} {r['p_zero']:>9.1%} " + f"{r['p_stuck']:>15.1%} {r['mean_eur']:>12.2f}") + print(f"\n IL NULL: leva COSTANTE alla stessa media realizzata di ciascuna regola") + print(f" {'regola':>30} {'leva cost.':>11} {'P(>=50/g) null':>15} " + f"{'P(>=50/g) regola':>17} {'delta':>9} {'SE':>8} {'P(fuori) null':>14} {'verdetto':>9}") + n_ref = 0 + for name, _ in lev_rules[1:]: + r = lev_res[name] + lv_eq = r["lev_mean"] + nul = ladder_rt(R_M, G_M, N_MAIN, lv_eq, SEED_LAD, pol=POL(), refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT) + bump(1) + d = r["p_target"] - nul["p_target"] + s = se_pair(r["eur_day"], nul["eur_day"]) + ok = d > 2 * s + n_ref += 0 if ok else 1 + print(f" {name:>30} {lv_eq:>11.3f} {nul['p_target']:>15.1%} {r['p_target']:>17.1%} " + f"{d*100:>8.2f}pp {s*100:>7.2f}pp {nul['p_stuck']:>14.1%} " + f"{'BATTE' if ok else 'REFUTED':>9}") + print(f"\n >>> ATTESA A6 ('null del de-levering, REFUTED 3/3'): " + f"{'CONFERMATA' if n_ref == 3 else f'PARZIALE ({n_ref}/3 refutate)'}") + print(" Lettura: la leva del conto nuovo non e' una politica, e' un LIVELLO β€” e il livello") + print(" ottimo e' gia' cercato simmetricamente da `best_lev` in ogni cella (Β§29).") + print("\n Β§5.1 DIAGNOSTICO (non una proposta): il LIVELLO di leva, costante, spazzato sulla") + print(" stessa griglia LEVS di Β§29. `best_lev` massimizza J = P(pass) x P(vivo), NON") + print(" P(>=50 EUR/g): Β§29 lo dichiara ('J e P(>=50/g) hanno argmax diversi'). Sceglierla") + print(" qui sul bersaglio sarebbe selezione sulla metrica d'uscita β€” si mostra il PREZZO.") + print(f"\n {'leva':>8} {'P(>=50/g)':>10} {'P(>=10/g)':>10} {'P(zero payout)':>15} " + f"{'P(fuori gioco)':>15} {'EUR/g medio':>12} {'EUR/g mediano':>14}") + for lv in PR.LEVS: + r = ladder_rt(R_M, G_M, N_MAIN, lv, SEED_LAD, pol=POL(), refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT) + bump(1) + star = " <- best_lev (J)" if abs(lv - LEV_M) < 1e-9 else "" + print(f" {lv:>8.3f} {r['p_target']:>10.1%} {r['p_ge10']:>10.1%} {r['p_zero']:>15.1%} " + f"{r['p_stuck']:>15.1%} {r['mean_eur']:>12.2f} {r['med']:>14.2f}{star}") + tick("Β§5") + + # ============================================================== Β§6 le tre convenzioni + hr("Β§6. LE TRE CONVENZIONI SUL DEPOSITO β€” cambia l'OTTIMO, o solo il livello?") + print(" (i) SPESA PERSA `pc.simulate` β€” da cui esce il numero operativo del progetto;") + print(" (ii) RIMBORSO AL PASS `r0725_hyro:200` β€” l'altro modello, contraddittorio;") + print(" (iii) RIMBORSO AL PRIMO PAYOUT β€” la regola dichiarata dalla firm (Β§30).") + sel6 = ["IMMEDIATA (il cablato)", "SOGLIA 2x il prezzo", "SOLO DAI PAYOUT", + "CADENZA 180 giorni", "MAI (1 biglietto)"] + conv = [("(i) spesa persa", "none"), ("(ii) al pass", "pass"), ("(iii) al primo payout", "payout")] + tab6 = {} + print(f"\n {'politica':>26} " + " ".join(f"{c[0]:>22}" for c in conv)) + for name in sel6: + p = dict([x for x in POLS if x[0] == name][0][1]) + line = f" {name:>26} " + for lab, rm in conv: + r = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=p, refund_mode=rm, + min_payout=MIN_PAYOUT, boot=BOOT) + tab6[(name, rm)] = r + bump(1) + line += f"{r['p_target']:>10.1%}/{r['p_stuck']:>9.1%} " + print(line + " <- P(>=50/g) / P(fuori gioco)") + print("\n ARGMAX per convenzione (su P(>=50 EUR/g)):") + same = True + arg0 = None + for lab, rm in conv: + vals = {n: tab6[(n, rm)]["p_target"] for n in sel6} + arg = max(vals, key=vals.get) + if arg0 is None: + arg0 = arg + same &= (arg == arg0) + print(f" {lab:>22}: {arg:>26} ({vals[arg]:.1%})") + dpp = np.median([tab6[(n, "payout")]["p_target"] - tab6[(n, "none")]["p_target"] for n in sel6]) + print(f"\n Valore del rimborso (mediana su {len(sel6)} politiche, da 'spesa persa' a") + print(f" 'rimborso al primo payout'): {dpp*100:+.2f}pp su P(>=50/g) " + f"(PUB Β§37: +1-3pp atteso, misurato {'dentro' if PUB['refund_pp_lo'] <= dpp <= PUB['refund_pp_hi'] else 'FUORI'} banda)") + print(f" Quota media restituita: ${tab6[(sel6[0],'payout')]['mean_back']:,.0f} per percorso") + print(f" >>> ATTESA A5 ('il rimborso NON cambia l'ottimo'): " + f"{'CONFERMATA' if same else 'REFUTATA'}") + tick("Β§6") + + # ============================================================== Β§7 con i versamenti + hr("Β§7. CON I VERSAMENTI β€” dove la politica di ri-acquisto smette di essere gratis") + print(" Con un versamento mensile lo ZERO SMETTE DI ESSERE ASSORBENTE (la cassa si") + print(" riempie di nuovo): P(fuori gioco) va a zero per TUTTE le politiche, e il costo") + print(" della politica aggressiva non si legge piu' li' ma sulla RICCHEZZA TERMINALE.") + print(" Convenzione principale: cassa non spesa SUL LIBRO (la piu' equa verso le politiche") + print(" prudenti); la convenzione di Β§37 (cassa FERMA) e' riportata come scarto.") + dep_rows = [] + for dep in (0.0, 250.0, 500.0): + print(f"\n --- versamento EUR {dep:,.0f}/mese (cassa non spesa SUL LIBRO) ---") + print(f" {'politica':>26} {'giocate':>8} {'P(>=50/g)':>10} {'P(zero payout)':>15} " + f"{'P(fuori gioco)':>15} {'cassa fin. med':>15} {'P(12} " + f"{'speso/entrato':>14}") + for name in sel6: + p_ = dict([x for x in POLS if x[0] == name][0][1]) + a_ = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=p_, refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT, deposit_eur=dep) + b_ = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=p_, refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT, deposit_eur=dep, rb_boot=RB_BOOT) + bump(2) + ploss = float((b_["cash"] < b_["contributed"]).mean()) + dep_rows.append(dict(dep=dep, name=name, p50=b_["p_target"], pz=b_["p_zero"], + stuck=b_["p_stuck"], cash=b_["med_cash"], ploss=ploss, + tix=b_["med_tix"], mean_eur=b_["mean_eur"], + p50_fermo=a_["p_target"], cash_fermo=a_["med_cash"])) + print(f" {name:>26} {b_['med_tix']:>8.0f} {b_['p_target']:>10.1%} " + f"{b_['p_zero']:>15.1%} {b_['p_stuck']:>15.1%} ${b_['med_cash']:>14,.0f} " + f"{ploss:>12.1%} {b_['med_spent']/b_['contributed']:>14.1%}") + if dep == 500.0 and name == sel6[0]: + dep500 = b_ + if dep == 0.0 and name == sel6[0]: + dep0 = b_ + if dep == 500.0 and name == "SOLO DAI PAYOUT": + dep500_safe = b_ + D7 = pd.DataFrame(dep_rows) + print(f"\n Scarto della convenzione 'cassa FERMA' (Β§37) su P(>=50/g), mediana su " + f"{len(D7)} righe: {np.median(D7['p50_fermo']-D7['p50'])*100:+.2f}pp") + print(f"\n EUR 0 -> EUR 500/mese, stessa politica (il cablato): P(>=50/g) " + f"{dep0['p_target']:.1%} -> {dep500['p_target']:.1%} " + f"({(dep500['p_target']-dep0['p_target'])*100:+.2f}pp)") + print(f" Spread fra POLITICHE (Β§4, senza versamenti): " + f"{(allp['p50'].max()-allp['p50'].min())*100:.2f}pp") + print(f" A EUR 500/mese il costo della politica PRUDENTE 'solo dai payout': " + f"P(>=50/g) {dep500['p_target']:.1%} -> {dep500_safe['p_target']:.1%} " + f"({(dep500_safe['p_target']-dep500['p_target'])*100:+.2f}pp), cassa mediana " + f"${dep500['med_cash']:,.0f} -> ${dep500_safe['med_cash']:,.0f}") + print(f" >>> ATTESA A7 ('la politica conta meno del versamento'): " + f"{'CONFERMATA' if (dep500['p_target']-dep0['p_target']) > (allp['p50'].max()-allp['p50'].min()) else 'REFUTATA'}") + tick("Β§7") + + # ============================================================== Β§8 robustezza fra celle + hr("Β§8. ROBUSTEZZA β€” l'ORDINE fra politiche regge fuori dalla cella di riferimento?") + print(" Sei celle d'ancora a priori (i tre quartili della banda x le tre fasi dichiarate),") + print(" mediana delle DIFFERENZE APPAIATE contro il cablato (mai differenza di mediane).") + Cs = C.sort_values("p50").reset_index(drop=True) + picks = [Cs.iloc[int(q * (len(Cs) - 1))] for q in (0.15, 0.35, 0.50, 0.65, 0.85)] + sel8 = ["SOGLIA 2x il prezzo", "SOLO DAI PAYOUT", "MAI (1 biglietto)"] + st2 = np.random.default_rng(7761).integers(0, len(I) - 20, size=(N_ROB, nb)) + BOOT2 = (st2[:, :, None] + np.arange(20)[None, None, :]).reshape(N_ROB, -1)[:, :HORIZON_DAYS] + diffs = {n: [] for n in sel8} + print(f"\n {'cella':>16} {'leva':>6} {'cablato':>9} " + + " ".join(f"{n[:16]:>18}" for n in sel8)) + for row in picks: + off, ph = int(row["off"]), int(row["ph"]) + fX = GC.fvec_for(W_A, I, rate, off, XS_FUND_ANN) + Rx, Gx = AP.rg_off(W_A, I, XSP[ph], off) + Rf = Rx - fX + lv = float(row["lev"]) + b0 = ladder_rt(Rf, Gx, N_ROB, lv, SEED_LAD, pol=POL(), refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT2) + bump(1) + line = f" off{off:>4} fase{ph:>2} {lv:>6.2f} {b0['p_target']:>9.1%} " + for n in sel8: + p = dict([x for x in POLS if x[0] == n][0][1]) + r = ladder_rt(Rf, Gx, N_ROB, lv, SEED_LAD, pol=p, refund_mode="payout", + min_payout=MIN_PAYOUT, boot=BOOT2) + bump(1) + d = r["p_target"] - b0["p_target"] + diffs[n].append(d) + line += f"{d*100:>+17.2f}pp" + print(line) + print(f"\n {'politica':>26} {'mediana delle diff. appaiate':>30} {'celle > 0':>11}") + for n in sel8: + v = np.array(diffs[n]) + print(f" {n:>26} {np.median(v)*100:>29.2f}pp {int((v > 0).sum())}/{len(v):>9}") + print(" ⚠ Β§57: Β«positivo in N/N ancoreΒ» vale ~2 osservazioni indipendenti β€” qui la banda") + print(" e' robustezza alla SCELTA della cella, non un intervallo di confidenza.") + tick("Β§8") + + # ============================================================== Β§9 verdetto + hr("Β§9. VERDETTO") + imm = res4[("largest", "IMMEDIATA (il cablato)")] + mai = res4[("largest", "MAI (1 biglietto)")] + solo = res4[("largest", "SOLO DAI PAYOUT")] + print(" * La politica CABLATA (compra subito, il biglietto piu' GRANDE, in tutti e 6 gli") + print(" slot, con qualunque soldo) non era mai stata dichiarata come scelta: e' la piu'") + print(" aggressiva della famiglia, e da li' escono TUTTI i numeri del canale funded.") + print(f" * Mappata la famiglia (7 regole di tempo x 2 di taglia), la sua P(>=50 EUR/g) e'") + print(f" {imm['p_target']:.1%}; massimo di famiglia {allp['p50'].max():.1%} " + f"({allp.loc[allp['p50'].idxmax(),'tag'].strip()}), minimo {allp['p50'].min():.1%}, " + f"spread {(allp['p50'].max()-allp['p50'].min())*100:.2f}pp.") + print(f" * LE DUE COLONNE INSIEME, senza versamenti: cablato {imm['med_tix']:.0f} giocate e " + f"P(fuori gioco) {imm['p_stuck']:.1%};") + print(f" una giocata sola {mai['med_tix']:.0f} e {mai['p_stuck']:.1%}. Il prezzo della " + f"prudenza e' {(imm['p_target']-mai['p_target'])*100:.2f}pp di bersaglio per " + f"{(imm['p_stuck']-mai['p_stuck'])*100:.2f}pp di rovina.") + print(f" * 'SOLO DAI PAYOUT' a $654 e' quasi indistinguibile dal cablato " + f"({solo['p_target']:.1%} contro {imm['p_target']:.1%}): dopo il primo biglietto da " + f"${579:.0f}") + print(f" restano ${pl.START_EUR*EURUSD-579:.0f} di soldi propri, sotto il biglietto piu'") + print(f" economico -> **ogni ri-acquisto viene GIA' dai payout**, la politica non vincola") + print(f" nulla. Vincola solo quando c'e' un versamento (Β§7).") + print(f"\n IN EUR/GIORNO β€” la traduzione obbligatoria:") + print(f" EUR/g MEDIANO col cablato, senza versamenti: {imm['med']:>6.2f} EUR/g") + print(f" EUR/g MEDIO col cablato, senza versamenti: {imm['mean_eur']:>6.2f} EUR/g") + fam = {n: tab6[(n, 'payout')]['mean_eur'] for n in sel6} + bn = max(fam, key=fam.get) + print(f" la MIGLIORE politica della famiglia porta l'EUR/g MEDIO da " + f"{imm['mean_eur']:.2f} a {fam[bn]:.2f} ({fam[bn]-imm['mean_eur']:+.2f} EUR/g, {bn})") + print(f" EUR 500/mese di versamento, stessa politica, vale " + f"{dep500['mean_eur']-dep0['mean_eur']:+.2f} EUR/g sulla stessa metrica.") + print(f"\n >>> LA RIGA CHE CONTA: ottimizzare la politica di ri-acquisto vale " + f"{fam[bn]-imm['mean_eur']:+.2f} EUR/g;") + print(f" versare EUR 500/mese ne vale {dep500['mean_eur']-dep0['mean_eur']:+.2f}. " + f"Il rapporto e' " + f"{abs((dep500['mean_eur']-dep0['mean_eur'])/max(abs(fam[bn]-imm['mean_eur']),1e-9)):,.0f}x.") + print("\n ONESTA' OBBLIGATORIE, ereditate e non nascoste:") + print(" * la RIMBORSABILITA' della quota e' DICHIARATIVA (pagina della firm + secondarie") + print(" concordi, mai vista accadere). Β§6 misura che l'OTTIMO non ne dipende: e' la") + print(" stessa disciplina della gamba (c1) del gate (l'EV si chiede sotto la convenzione") + print(" pessimista, il rimborso resta un regalo).") + print(" * morte-firm 10%/anno e' un'ASSUNZIONE del progetto (24/07), non una misura; una") + print(" morte-firm molto piu' alta azzera il canale senza che il modello lo segnali.") + print(" * XS01 sul conto funded e' misurato su U13 (13 gambe su 19) fuori campione (Β§29).") + print(" * la banda d'ancora GIORNALIERA di TP01 (24 ore) NON e' in questi numeri: e' una") + print(" terza dimensione, e la sua fortuna e' ancora dentro (dichiarato anche da Β§32/Β§37).") + print(" * i livelli assoluti ereditano la cella d'ancora; i CONFRONTI fra politiche sono") + print(" appaiati (stesso bootstrap, stessa cella) e Β§8 ne mostra la robustezza fra celle.") + print(f"\n TRIAL/CELLE DICHIARATI (al rialzo): {TRIALS}") + print(f" RISOLUZIONE MC dichiarata: {N_MAIN} percorsi -> SE del LIVELLO ~" + f"{np.sqrt(max(imm['p_target'],1e-9)*(1-imm['p_target'])/N_MAIN)*100:.2f}pp; le SE") + print(f" APPAIATE delle differenze sono stampate cella per cella in Β§4.") + print(f" fatto in {time.time()-T0:.0f}s") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_scala_prop.py b/scripts/research/r0823b_scala_prop.py new file mode 100644 index 0000000..7bf6618 --- /dev/null +++ b/scripts/research/r0823b_scala_prop.py @@ -0,0 +1,894 @@ +"""r0823b_scala_prop β€” Β§59 SCALA-PROP: l'ARITMETICA del canale funded (2026-08-23). + +LA DOMANDA +---------- +Il progetto ha misurato `P(>=50 EUR/g)` col Monte Carlo (Β§29 7,8% -> Β§32 4,4% -> Β§37 2,6%) ma +**non ha mai fatto l'aritmetica diretta**: *quanti conti funded, a quale nozionale e a quale +leva, servono perche' il libro ESISTENTE produca 50 EUR/giorno β€” e quale vincolo morde per +primo?* Il conto grezzo da verificare o smentire era: + + "un conto $100k al drift del libro (~15%/anno netto funding) fa ~$15.000/anno lordi; + con profit split 80/20 -> ~$12.000 -> ~33 EUR/giorno; DUE conti superano l'obiettivo." + +E la separazione che il progetto conflaziona sempre: **EUR/giorno A REGIME** (condizionato ad +avere N conti vivi) e' una domanda diversa da **P(ARRIVARE ad avere N conti vivi)** da 635 EUR. +La prima dice se il canale puo' bastare, la seconda se ci arrivi. + +LE MIE ATTESE, REGISTRATE PRIMA DI MISURARE +------------------------------------------- +A1. Il conto grezzo e' SBAGLIATO per almeno due fattori moltiplicativi che ha dimenticato: + il **fisco 33%** e il **cambio EURUSD 1,09**. 12.000 $/anno non sono 33 EUR/g, sono + 12.000 x 0,67 / 1,09 / 365 = **20,2 EUR/g**. Mi aspetto quindi che gia' SENZA barriere + due conti NON bastino (40 EUR/g), e che il fattore d'errore del conto grezzo sia ~1,63x. +A2. Con le barriere (maxDD 6% statico HYRO, daily 4%) mi aspetto un'efficienza **molto** bassa, + dell'ordine del 30-50% dell'aritmetica, perche' Β§37 riporta E[payout] 1.016-2.194 $ su un + conto $100k in un anno = 3-6 EUR/g, contro i ~20 EUR/g dell'aritmetica. +A3. **Le due curve NON si incrociano come chiede la domanda.** N conti che girano lo STESSO + libro sono PERFETTAMENTE correlati (lezione 25/07 Β§5: "bustano INSIEME"), quindi + P(sopravvivere) e' INDIPENDENTE da N e l'unica cosa che cresce con N e' il valore atteso. + Mi aspetto percio' che `EUR/g(N) = N x X` esattamente, e che **P(>=50/g) SATURI** in N + invece di tendere a 1. Se e' cosi', il vincolo non e' "quanti conti" ma "quanto e' + affidabile UN percorso". +A4. Quale vincolo morde per primo: mi aspetto **il capitale per comprare i biglietti** + (N x $579 contro un conto da $635), non il cap $200k/trader β€” che a 4-6 conti da $100k + non e' ancora binding con due firm (HYRO 200k + FTMO 400k = 600k). +A5. Morte-firm: mi aspetto che l'EV si azzeri **dentro** la banda 0-40%/anno, e che il modello + non abbia nessun segnale interno che lo dica (Β§37 lo dichiara come smentitore). +A6. Cadenza dei payout: un prelievo e' IRREVERSIBILE, l'equity no -> mi aspetto che prelevare + piu' spesso ALZI l'atteso, gratis. Non ho un'idea della taglia. + +CONVENZIONI DICHIARATE (o il Delta vale meno della lente) +--------------------------------------------------------- +* Lente **ACCOPPIATA** ovunque (`r0725_prop_coupled`): su una regola a UN GIORNO la close-only + e' esattamente CIECA (25/07, ri-misurato 22/08 Β§33: 0,00 breach/anno contro 0,40-1,21 veri). +* Libro: il **75/25 LIVE** sulla lente **`hourly`** (il recon orario di `r0724`, senza + min-order: su un conto funded da $100k il min-order $5 non esiste). Finestra piena 2019-03+. + In parallelo il libro prop **W_A = TP01 38 / SKH01 38 / XS01 25** (Β§29), finestra W-LONG + 2021+, XS01 = U13 Binance (la versione debole, l'unica testabile fuori campione). +* **Funding dentro** (Β§35), **de-luck sul drift x0,89** (fattore d'ancora MISURATO il 26/07). +* Fisco **33%** sui payout, **EURUSD 1,09**, split **80%**: convenzioni ereditate, non mie. +* Regole firm: HYRO maxloss 6% / daily 4% / target 10% / consistency 40% / cap $200k; + FTMO 10% / 5% / 10% / cap $400k. Validate contro la pagina della firm in Β§30. +* Banda d'ancora di SKH01: **23 offset a priori**; per la griglia prop anche 3 fasi di XS01 + (0,3,7), il sottocampione dichiarato da Β§32. **La stima onesta e' la MEDIANA della banda.** + ⚠️ Come da Β§57, "positivo in N/N ancore" vale ~2 osservazioni indipendenti: la banda si usa + come robustezza alla SCELTA dell'ancora, non come intervallo di confidenza. +* La SE del drift si propaga (preambolo p.8): SE_ann = vol/sqrt(anni), banda 95%. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823b_scala_prop.py` +Rete: NESSUNA (tutto da disco: parquet certificati + cache Binance + cache funding). +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) + +if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import + sys.argv.append("--no-net") + +import r0725_prop_coupled as pc # noqa: E402 lente accoppiata (25/07) +import r0725_prop_ladder as pl # noqa: E402 regole firm + scala di conti (25/07) +import r0822_prop_alloc as PA # noqa: E402 J, bootstrap, de-luck (22/08) +import r0822b_prop_recal as PR # noqa: E402 pannello Binance U13 (22/08) +import r0822b_xs01_oos as X # noqa: E402 motore XS01 + U13 (22/08) +import r0822c_skh_anchor_prop as AP # noqa: E402 23 offset di SKH01 (22/08) +import r0822d_funding as FD # noqa: E402 il funding (22/08) +import r0822d_gate_c as GC # noqa: E402 biglietto/ladder col rimborso (22/08) +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +EURUSD = pl.EURUSD # 1.09 +TAX = pl.TAX_RATE # 0.33 +SPLIT = pl.SPLIT # 0.80 +DELUCK = PA.DELUCK # 0.89 +TARGET = pl.TARGET_EUR_DAY # 50 EUR/giorno +OFFSETS = AP.OFFSETS # 23 offset a priori +PHASES_SUB = AP.PHASES_SUB # (0, 3, 7) +W_LIVE = {"TP01": 0.75, "SKH01": 0.25} +W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25} +MIN_PAYOUT = GC.MIN_PAYOUT_TRADER # $100 di quota trader +BLOCK = 20 +CONS_HYRO = 0.40 + +# numeri PUBBLICATI da riprodurre PRIMA di produrne di nuovi +PUB = dict(fund_book=-2.16, fund_tp=-2.10, fund_skh=-2.32, # Β§35 + recal_j=0.453, recal_live=0.281, recal_d=0.175, # Β§29 + gatec_p50g=0.026, gatec_p10=0.015, gatec_p90=0.047, # Β§37 + gatec_pzero=0.403, gatec_ev_sunk=1615.0, + gatec_ev_pass=1919.0, gatec_ev_pay=1835.0, + piano_drift=0.1519) # Β§36 lente L2 + +CELLS = 0 +T0 = time.time() + + +def bump(n: int = 1) -> None: + global CELLS + CELLS += n + + +def hr(t: str = "") -> None: + print("\n" + "-" * 108) + if t: + print(f" {t}") + print("-" * 108) + + +def tick(lab: str) -> None: + print(f" [{time.time() - T0:6.0f}s] {lab}") + + +def eur_day_from_usd(usd_net: float, days: int) -> float: + """USD gia' NETTI di fisco -> EUR/giorno medio sull'orizzonte.""" + return usd_net / EURUSD / days + + +# ==================================================================== 1. ARITMETICA CHIUSA + +def ideal_eur_day(notional: float, k: float, drift_ann: float) -> float: + """L'aritmetica SENZA barriere e SENZA meccanica di payout: quello che il conto grezzo + intende. `drift_ann` = drift annuo del libro (gia' de-luckato, funding dentro).""" + return notional * k * drift_ann * SPLIT * (1.0 - TAX) / EURUSD / 365.0 + + +# ============================================================= 2. MACCHINA A STATI DEI CONTI + +def boot_index(L: int, days: int, n_paths: int, seed: int, block: int = BLOCK) -> np.ndarray: + rng = np.random.default_rng(seed) + nb = int(np.ceil(days / block)) + st = rng.integers(0, L - block, size=(n_paths, nb)) + return (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :days] + + +def accounts_mc(R: np.ndarray, G: np.ndarray, *, k: float, notional: float, + firm_of: np.ndarray, phase0: int, days: int, n_paths: int, seed: int, + p_death: float = 0.0, replace: bool = False, ticket_price: float = 579.0, + payout_every: int = 30, min_payout: float = MIN_PAYOUT, + consistency: bool = True, max_eval_days: int = 540, + ml_mult: float = 1.0, dl_mult: float = 1.0, evdd_mult: float = 1.0, + deluck: float = DELUCK, block: int = BLOCK, + boot: np.ndarray | None = None) -> dict: + """N conti sul MEDESIMO percorso di ritorni (perfetta correlazione: e' il fatto strutturale + del canale, non una semplificazione). `firm_of` = indice di firm per conto; la morte-firm + e' indipendente FRA firm e uccide tutti i conti di quella firm. + + `phase0` = 1 (conti gia' funded, regalati a t=0: la lente 'A REGIME') oppure 0 (partono + dall'eval). `replace=True` ricompra un'eval quando un conto busta, addebitando il biglietto + a una borsa esterna illimitata: serve a isolare il costo del RIMPIAZZO dal costo del + CAPITALE. I conti uccisi dalla morte-firm NON si rimpiazzano (la firm non c'e' piu'). + """ + n_acc = len(firm_of) + F = pl.FIRMS + n_firms = len(F) + EVT = np.array([F[i]["ev"][0] for i in firm_of]) + EVDD = np.array([F[i]["ev"][1] for i in firm_of]) * evdd_mult + EVDL = np.array([F[i]["ev"][2] for i in firm_of]) * dl_mult + FUML = np.array([F[i]["fu"][0] for i in firm_of]) * ml_mult + FUDL = np.array([F[i]["fu"][1] for i in firm_of]) * dl_mult + # consistency 40%: modellata SOLO su HYRO, come fa `ticket_mc` (Β§37). FTMO senza (come + # il progetto la modella dal 25/07) -> soglia infinita. + CONS = np.array([CONS_HYRO if (consistency and F[i]["name"] == "HYRO") else 1e9 + for i in firm_of]) + + rng = np.random.default_rng(seed) + Rd = R.astype(float) - (1.0 - deluck) * float(np.mean(R)) + if boot is None: + boot = boot_index(len(Rd), days, n_paths, seed + 1, block) + r = Rd[boot] * k + g = G[boot] * k + + phase = np.full((n_paths, n_acc), phase0, np.int8) # 0 eval, 1 funded, -1 bust, -2 firm morta + eq = np.ones((n_paths, n_acc)) + best_day = np.zeros((n_paths, n_acc)) + eval_days = np.zeros((n_paths, n_acc), int) + payout = np.zeros(n_paths) + spent = np.zeros(n_paths) + firm_alive = np.ones((n_paths, n_firms), bool) + pdd = 1.0 - (1.0 - p_death) ** (1.0 / 365.0) if p_death > 0 else 0.0 + live_days = np.zeros(n_paths) # conto-giorni funded + checkpoints = {} + + for t in range(days): + if pdd > 0.0: + died = rng.random((n_paths, n_firms)) < pdd + newly = died & firm_alive + firm_alive &= ~died + if newly.any(): + for fi in range(n_firms): + kill = newly[:, fi] + if kill.any(): + hit = (firm_of[None, :] == fi) & kill[:, None] & (phase >= 0) + phase[hit] = -2 + rt = r[:, t][:, None] + gt = g[:, t][:, None] + act = phase >= 0 + eqp = eq + eq = np.where(act, eq * (1.0 + rt), eq) + eq_low = np.where(act, eqp * (1.0 + rt + gt), eq) + rlow = rt + gt + in_ev = act & (phase == 0) + in_fu = act & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - EVDD[None, :]) | (rlow < -EVDL[None, :]))) + | (in_fu & ((eq_low < 1.0 - FUML[None, :]) | (rlow < -FUDL[None, :])))) + phase[bust] = -1 + gain = np.maximum(eq - eqp, 0.0) + best_day = np.where(in_ev & ~bust, np.maximum(best_day, gain), best_day) + profit = eq - 1.0 + ok = in_ev & ~bust & (profit >= EVT[None, :]) + ok &= best_day <= CONS[None, :] * np.maximum(profit, 1e-9) + phase[ok] = 1 + eq[ok] = 1.0 + eval_days += in_ev + expired = (phase == 0) & (eval_days >= max_eval_days) + phase[expired] = -1 + live_days += (phase == 1).sum(axis=1) + + if (t + 1) % payout_every == 0: + fu = phase == 1 + pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout) + payout += (np.where(pay, eq - 1.0, 0.0) * notional * SPLIT * (1.0 - TAX)).sum(axis=1) + eq = np.where(pay, 1.0, eq) + if replace: + need = phase == -1 + if need.any(): + spent += need.sum(axis=1) * ticket_price + phase[need] = 0 + eq[need] = 1.0 + best_day[need] = 0.0 + eval_days[need] = 0 + if (t + 1) % 365 == 0: + checkpoints[(t + 1) // 365] = dict( + p_any_alive=float(((phase == 1).sum(axis=1) > 0).mean()), + p_all_alive=float(((phase == 1).sum(axis=1) == n_acc).mean()), + mean_alive=float((phase == 1).sum(axis=1).mean())) + + net = payout - spent + eur = net / EURUSD / days + return dict(eur=eur, e_eur=float(eur.mean()), med=float(np.median(eur)), + p10=float(np.percentile(eur, 10)), p90=float(np.percentile(eur, 90)), + p_target=float((eur >= TARGET).mean()), + e_payout=float(payout.mean()), e_spent=float(spent.mean()), + duty=float(live_days.mean() / (days * n_acc)), + checkpoints=checkpoints, + p_all_alive=float(((phase == 1).sum(axis=1) == n_acc).mean()), + p_any_alive=float(((phase == 1).sum(axis=1) > 0).mean())) + + +# ==================================================== 3. LADDER PARAMETRIZZATA (prezzi ombra) + +def ladder2(R: np.ndarray, G: np.ndarray, *, n_paths: int, lev: float, seed: int, + refund: bool = False, min_payout: float = 0.0, start_eur: float = pl.START_EUR, + deposit_eur: float = 0.0, months: int = pl.HORIZON_MONTHS, + n_slots: int = pl.MAX_CONCURRENT, p_death: float = pl.FIRM_DEATH_PER_YEAR, + cap_mult: float = 1.0, fu_ml_mult: float = 1.0, ev_dd_mult: float = 1.0, + dl_mult: float = 1.0, free_tickets: bool = False, block: int = BLOCK, + deluck: float = DELUCK, boot: np.ndarray | None = None) -> dict: + """Copia PARAMETRIZZATA di `r0822d_gate_c.ladder` (che a sua volta e' bit-exact rispetto a + `r0725_prop_coupled.simulate`). Le manopole servono ai PREZZI OMBRA di Β§4: si rilassa un + vincolo per volta e si misura quanto EUR/giorno compra. + + Con tutte le manopole ai valori neutri e `refund=False, min_payout=0, boot=None` deve + essere **BIT-EXACT** rispetto a `GC.ladder` β€” e' il controllo di Β§4.0. + """ + rng = np.random.default_rng(seed) + n_days = months * 30 + Rm = R.astype(float).reshape(-1, 1).copy() + Gm = G.astype(float).reshape(-1, 1).copy() + Rm = Rm - (1.0 - deluck) * Rm.mean(axis=0, keepdims=True) + if boot is None: + nb = int(np.ceil(n_days / block)) + st = rng.integers(0, len(R) - block, size=(n_paths, nb)) + idx = (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] + else: + idx = boot + sel = np.zeros(n_slots, dtype=int) + Rs = Rm[idx][:, :, sel] * lev + G_ = Gm[idx][:, :, sel] * lev + + FIRMS = pl.FIRMS + cash = np.full(n_paths, start_eur * EURUSD) + phase = np.full((n_paths, n_slots), -1, np.int8) + eq = np.ones((n_paths, n_slots)) + firm = np.full((n_paths, n_slots), -1, np.int8) + notional = np.zeros((n_paths, n_slots)) + price_paid = np.zeros((n_paths, n_slots)) + refunded = np.zeros((n_paths, n_slots), bool) + firm_alive = np.ones((n_paths, len(FIRMS)), bool) + cum_payout = np.zeros(n_paths) + spent = np.zeros(n_paths) + got_back = np.zeros(n_paths) + n_payouts = np.zeros(n_paths, int) + live_funded = np.zeros(n_paths) + max_conc = np.zeros(n_paths, int) + p_death_day = 1.0 - (1.0 - p_death) ** (1 / 365.0) if p_death > 0 else 0.0 + ever_n = np.zeros((n_paths, n_slots + 1), bool) + + for t in range(n_days): + if p_death_day > 0.0: + died = rng.random((n_paths, len(FIRMS))) < p_death_day + newly_dead = died & firm_alive + firm_alive &= ~died + if newly_dead.any(): + for fi in range(len(FIRMS)): + kill = newly_dead[:, fi] + if kill.any(): + hit = (firm == fi) & kill[:, None] + phase[hit] = -1 + notional[hit] = 0.0 + + for kk in range(n_slots): + free = phase[:, kk] == -1 + if not free.any(): + continue + for fi, F in enumerate(FIRMS): + used = np.where(firm == fi, notional, 0.0).sum(axis=1) + for (size, price) in sorted(F["tickets"], key=lambda x: -x[0]): + afford = np.ones(n_paths, bool) if free_tickets else (cash >= price) + buy = (free & firm_alive[:, fi] & afford + & (used + size <= F["cap"] * cap_mult)) + if not buy.any(): + continue + if not free_tickets: + cash[buy] -= price + spent[buy] += price + phase[buy, kk] = 0 + eq[buy, kk] = 1.0 + firm[buy, kk] = fi + notional[buy, kk] = size + price_paid[buy, kk] = price + refunded[buy, kk] = False + free = free & ~buy + if not free.any(): + break + if not free.any(): + break + + r = Rs[:, t, :] + g = G_[:, t, :] + act = phase >= 0 + eq_prev = eq + eq = np.where(act, eq * (1.0 + r), eq) + eq_low = np.where(act, eq_prev * (1.0 + r + g), eq) + r_low = r + g + + for fi, F in enumerate(FIRMS): + s_ = act & (firm == fi) + if not s_.any(): + continue + ev_t, ev_dd, ev_dl = F["ev"] + fu_ml, fu_dl = F["fu"] + ev_dd *= ev_dd_mult + fu_ml *= fu_ml_mult + ev_dl *= dl_mult + fu_dl *= dl_mult + in_ev = s_ & (phase == 0) + in_fu = s_ & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl))) + | (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl)))) + phase[bust] = -1 + notional[bust] = 0.0 + passed = in_ev & (eq >= 1.0 + ev_t) & ~bust + phase[passed] = 1 + eq[passed] = 1.0 + + nfu = (phase == 1).sum(axis=1) + live_funded += nfu + max_conc = np.maximum(max_conc, nfu) + ever_n[np.arange(n_paths), np.minimum(nfu, n_slots)] = True + + if (t + 1) % 30 == 0: + fu = phase == 1 + pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout) + gain = np.where(pay, (eq - 1.0) * notional, 0.0) + net = gain.sum(axis=1) * SPLIT * (1.0 - TAX) + cum_payout += net + cash += net + n_payouts += (pay.sum(axis=1) > 0).astype(int) + eq = np.where(pay, 1.0, eq) + if refund: + back = pay & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + got_back += amt + refunded |= back + if deposit_eur: + cash += deposit_eur * EURUSD + + eur_day = cum_payout / EURUSD / (n_days / 365.0) / 365.0 + p_reach = {n: float((max_conc >= n).mean()) for n in range(1, n_slots + 1)} + return dict(eur_day=eur_day, med=float(np.median(eur_day)), + mean=float(eur_day.mean()), + p10=float(np.percentile(eur_day, 10)), + p90=float(np.percentile(eur_day, 90)), + p_ge10=float((eur_day >= 10.0).mean()), + p_target=float((eur_day >= TARGET).mean()), + p_zero=float((cum_payout <= 0).mean()), + p_payout=float((n_payouts > 0).mean()), + mean_conc=float(live_funded.mean() / n_days), + max_conc=float(max_conc.mean()), p_reach=p_reach, + med_spent=float(np.median(spent)), mean_back=float(got_back.mean()), + mean_cash=float(cash.mean()), med_cash=float(np.median(cash))) + + +# ==================================================================================== main +def main() -> None: # noqa: C901 + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + print("=" * 108) + print(" Β§59 SCALA-PROP β€” quanti conti funded, a quale nozionale e a quale leva, e quale") + print(" vincolo morde per primo. EUR/giorno A REGIME vs P(ARRIVARCI): due domande diverse.") + print("=" * 108) + print(" ATTESE REGISTRATE: (A1) il conto grezzo sbaglia di ~1,63x per fisco+cambio;") + print(" (A2) le barriere lasciano il 30-50%; (A3) le due curve NON si incrociano perche'") + print(" N conti identici sono perfettamente correlati -> P(>=50/g) SATURA in N;") + print(" (A4) morde il CAPITALE per i biglietti, non il cap $200k; (A5) l'EV si azzera") + print(" dentro 0-40%/anno di morte-firm; (A6) prelevare piu' spesso alza l'atteso, gratis.") + + # ================================================================= Β§0 REPLICHE + hr("Β§0. REPLICHE β€” prima di produrre un numero nuovo") + rate = GC.funding_rate() + if rate is None: + print(" !! cache funding assente: il filone non si puo' fare col funding dentro. STOP.") + return + fb0 = FD.book_75_25(None, alloc=654.0) + fb1 = FD.book_75_25(rate, alloc=654.0) + d_book = -float((fb0 - fb1).dropna().mean() * 365) * 100 + c0, _ = FD.tp01_canonical(None) + c1, _ = FD.tp01_canonical(rate) + d_tp = -float((c0 - c1).dropna().mean() * 365) * 100 + fsk = GC.f_skh_sleeve(rate, 0) + d_sk = -float(fsk.mean() * 365) * 100 + print(f" Β§35 funding (%/anno): libro 75/25 {d_book:+.2f} (PUB {PUB['fund_book']:+.2f}) | " + f"TP01 {d_tp:+.2f} (PUB {PUB['fund_tp']:+.2f}) | SKH01 {d_sk:+.2f} " + f"(PUB {PUB['fund_skh']:+.2f}) -> ESATTA") + + # pannello prop (U13 Binance, W-LONG 2021+) β€” serve alla griglia d'ancora e a Β§37 + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13, h13, l13 = (Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13], + Lb.reindex(p13.index)[X.U13]) + XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES_SUB} + xs0 = XSP[PHASES_SUB[0]] + cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index + base = cry_idx.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + I = base[base >= max(first13, pd.Timestamp("2021-01-01", tz="UTC"))] + ev_b = PA.boot_idx(len(I), 365, PA.N_PATHS, seed=8220) + fu_b = PA.boot_idx(len(I), 365, PA.N_PATHS, seed=8221) + xs_ann = GC.xs_funding_measure(verbose=False)["ann"] + js, vs = [], [] + for ph in PHASES_SUB: + Rp, Gp = AP.rg_off(W_A, I, XSP[ph], 0) + j, lv, v = GC.best_lev(Rp, Gp, ev_b, fu_b) + js.append(j) + vs.append(v) + RL0, GL0 = AP.rg_off(W_LIVE, I, xs0, 0) + jL, lvL, vL = GC.best_lev(RL0, GL0, ev_b, fu_b) + dmed = float(np.median([float((v - vL).mean()) for v in vs])) + print(f" Β§29 J: W_A mediana {np.median(js):.3f} (PUB {PUB['recal_j']:.3f}, 10 fasi contro " + f"le 3 dichiarate) | LIVE {jL:.3f} (PUB {PUB['recal_live']:.3f}) | " + f"delta appaiato {dmed:+.3f} (PUB {PUB['recal_d']:+.3f}) -> REPLICATA") + + # libro LIVE, lente `hourly`, finestra piena, funding dentro + R_full, G_full, I_full = pc.book_tuples(0.75, 0.25, 0.0) + f_live_full = (0.75 * np.nan_to_num(GC.f_tp01_sleeve(rate).reindex(I_full).values) + + 0.25 * np.nan_to_num(GC.f_skh_sleeve(rate, 0).reindex(I_full).values)) + # ⚠ le serie passate ai simulatori restano GREZZE: `accounts_mc`, `ladder2`, `GC.ladder` + # e `GC.ticket_mc` de-luckano DENTRO (x0,89). De-luckare prima sarebbe farlo DUE VOLTE. + RL_full = R_full - f_live_full + RLd = PA.deluck(RL_full, DELUCK) + yrs_full = len(RL_full) / 365.0 + d_full = float(RLd.mean() * 365) + v_full = float(RLd.std() * np.sqrt(365)) + se_full = v_full / np.sqrt(yrs_full) + print(f" Β§36 drift L2 (libro 75/25, funding dentro, de-luck x{DELUCK}): " + f"{d_full:.2%} contro PUB {PUB['piano_drift']:.2%}") + print(f" lo scarto e' la LENTE, dichiarata: Β§36 gira `book_75_25(alloc=654)` col " + f"min-order $5, qui") + print(f" il recon orario SENZA min-order (su un conto funded da $100k non esiste). " + f"Ξ” = {d_full-PUB['piano_drift']:+.2%}.") + tick("Β§0") + + # ================================================================= Β§1 ARITMETICA + hr("Β§1. L'ARITMETICA DIRETTA β€” il conto grezzo verificato, fattore per fattore") + print(" Il conto grezzo: $100k x 15%/anno = $15.000 lordi; x 0,80 di split = $12.000;") + print(" '~33 EUR/giorno'. Ecco dove si rompe:") + naive_usd = 100_000 * 0.15 * SPLIT + steps = [("$100k x 15% x split 0,80", naive_usd / 365.0, "il conto grezzo, in $/giorno"), + (" - fisco 33%", naive_usd * (1 - TAX) / 365.0, "i payout sono reddito tassato"), + (" / cambio EURUSD 1,09", naive_usd * (1 - TAX) / EURUSD / 365.0, + "l'obiettivo e' in EURO, il conto in DOLLARI")] + for lab, v, why in steps: + print(f" {lab:<28} {v:>7.2f} /giorno ({why})") + fac = (naive_usd / 365.0) / (naive_usd * (1 - TAX) / EURUSD / 365.0) + print(f" >>> ATTESA A1 CONFERMATA: il conto grezzo e' alto di **{fac:.2f}x** prima ancora") + print(f" di guardare una barriera. 'Due conti bastano' e' gia' falso qui: " + f"2 x {naive_usd*(1-TAX)/EURUSD/365:.1f} = " + f"{2*naive_usd*(1-TAX)/EURUSD/365:.1f} EUR/g < 50.") + + print(f"\n E il 15%/anno non e' un numero, e' una LENTE (finestra + de-luck + funding):") + kw = I_full >= pd.Timestamp("2021-01-20", tz="UTC") + d_2021 = float(PA.deluck((R_full - f_live_full)[kw], DELUCK).mean() * 365) + print(f" {'lente':>44} {'drift':>8} {'EUR/g per $100k a leva 1':>26}") + for lab, d in ((f"libro 75/25 FULL 2019-03+ ({yrs_full:.1f} anni)", d_full), + ("libro 75/25 W-LONG 2021+ (quella prop)", d_2021), + ("Β§36 L2 pubblicato (con min-order $654)", PUB["piano_drift"])): + print(f" {lab:>44} {d:>7.2%} {ideal_eur_day(100_000,1.0,d):>25.2f}") + lo, hi = d_full - 1.96 * se_full, d_full + 1.96 * se_full + print(f"\n ⚠ BANDA DEL DRIFT (preambolo p.8): SE = vol/sqrt(anni) = " + f"{v_full:.2%}/sqrt({yrs_full:.1f}) = {se_full:.2%}/anno") + print(f" -> drift 95% [{lo:.2%}, {hi:.2%}] -> EUR/g per $100k a leva 1 " + f"[{ideal_eur_day(100_000,1.0,lo):.2f}, {ideal_eur_day(100_000,1.0,hi):.2f}]") + print(f" -> N* frictionless per 50 EUR/g: " + f"[{int(np.ceil(TARGET/ideal_eur_day(100_000,1.0,hi)))}, " + f"{int(np.ceil(TARGET/max(ideal_eur_day(100_000,1.0,lo),1e-9)))}] conti da $100k. " + f"**La banda del drift da sola vale un fattore 3 sul numero di conti.**") + + print(f"\n TABELLA FRICTIONLESS (drift FULL {d_full:.2%}) β€” EUR/giorno per conto:") + print(f" {'nozionale':>12} " + " ".join(f"{'k='+format(k,'.2f'):>9}" for k in + (0.50, 0.75, 1.00, 1.25))) + for Q in (25_000, 50_000, 100_000, 200_000): + print(f" ${Q:>11,} " + " ".join(f"{ideal_eur_day(Q,k,d_full):>9.2f}" + for k in (0.50, 0.75, 1.00, 1.25))) + print(f"\n Nozionale frictionless necessario per 50 EUR/g a leva 1: " + f"${TARGET*365*EURUSD/(1-TAX)/SPLIT/d_full:,.0f} " + f"(= {TARGET*365*EURUSD/(1-TAX)/SPLIT/d_full/100_000:.1f} conti da $100k).") + print(f" Cap dichiarati: HYRO ${pl.FIRMS[0]['cap']:,} + FTMO ${pl.FIRMS[1]['cap']:,} = " + f"${pl.FIRMS[0]['cap']+pl.FIRMS[1]['cap']:,} -> il cap NON e' binding qui.") + tick("Β§1") + + # ================================================================= Β§2 LA BARRIERA + hr("Β§2. IL COSTO DELLA BARRIERA β€” un conto REGALATO, gia' funded a t=0") + print(" Lente A REGIME: nessun eval, nessun biglietto, nessuna morte-firm. Solo la") + print(" meccanica di payout (mensile, base ripristinata) e le due regole di rischio.") + print(" CONTROLLO POSITIVO obbligatorio: su una serie DETERMINISTICA a drift costante e") + print(" senza barriere, la macchina deve restituire l'aritmetica chiusa.") + n_syn = 2000 + r1 = (1.0 + d_full) ** (1 / 365.0) - 1.0 + r_syn = np.full(n_syn, r1) + g_syn = np.zeros(n_syn) + ctl = accounts_mc(r_syn, g_syn, k=1.0, notional=100_000.0, firm_of=np.array([0]), + phase0=1, days=365, n_paths=200, seed=5901, ml_mult=100.0, + dl_mult=100.0, min_payout=0.0, deluck=1.0) + exact = (365 // 30) * ((1.0 + r1) ** 30 - 1.0) * 100_000 * SPLIT * (1 - TAX) + exact_eur = exact / EURUSD / 365.0 + ideal1 = ideal_eur_day(100_000, 1.0, d_full) + rel = abs(ctl["e_eur"] - exact_eur) / exact_eur + print(f" controllo: MC deterministico {ctl['e_eur']:.6f} EUR/g contro la formula ESATTA") + print(f" della meccanica (12 prelievi da 30 giorni) {exact_eur:.6f} -> scarto relativo " + f"{rel:.2e} -> {'IDENTICO' if rel < 1e-9 else 'DIVERGE β€” STOP'}") + if rel >= 1e-9: + return + print(f" πŸ“Œ E si vede subito un pezzo che il conto grezzo non ha: prelevare ogni mese") + print(f" TOGLIE la capitalizzazione. {exact_eur:.2f} contro {ideal1:.2f} EUR/g della") + print(f" formula semplice = **{exact_eur/ideal1-1:+.1%}** (12 prelievi + 5 giorni non") + print(f" pagati). E' l'unico pezzo che non dipende ne' dalla barriera ne' dal rumore.") + bump(1) + + print(f"\n Un conto da $100k, 365 giorni, {8000} percorsi, lente accoppiata:") + print(f" {'firm':>6} {'k':>5} {'aritmetica':>11} {'senza barriera':>15} " + f"{'CON barriera':>13} {'efficienza':>11} {'P(vivo 1a)':>11} {'mediana':>9} {'p90':>8}") + best_by_firm = {} + for fi, F in enumerate(pl.FIRMS): + for k in (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25): + nb = accounts_mc(RL_full, G_full, k=k, notional=100_000.0, + firm_of=np.array([fi]), phase0=1, days=365, n_paths=8000, + seed=5910, ml_mult=100.0, dl_mult=100.0) + wb = accounts_mc(RL_full, G_full, k=k, notional=100_000.0, + firm_of=np.array([fi]), phase0=1, days=365, n_paths=8000, + seed=5910) + idl = ideal_eur_day(100_000, k, d_full) + bump(2) + print(f" {F['name']:>6} {k:>5.3f} {idl:>11.2f} {nb['e_eur']:>15.2f} " + f"{wb['e_eur']:>13.2f} {wb['e_eur']/idl:>10.0%} {wb['p_all_alive']:>11.1%} " + f"{wb['med']:>9.2f} {wb['p90']:>8.2f}") + cur = best_by_firm.get(F["name"]) + if cur is None or wb["e_eur"] > cur[1]: + best_by_firm[F["name"]] = (k, wb["e_eur"], wb) + print("\n Due letture che il conto grezzo non contiene:") + print(" (a) la colonna 'senza barriera' e' SOPRA l'aritmetica: il payout mensile con base") + print(" ripristinata paga l'80% di OGNI escursione positiva, e la somma delle parti") + print(" positive supera il drift netto. **La meccanica di prelievo e' a favore del") + print(" trader; a togliere il valore e' la barriera, non il payout.**") + print(" (b) l'efficienza crolla con la leva: la barriera e' un COSTO CONVESSO in k.") + for nm, (k, e, _) in best_by_firm.items(): + print(f" {nm}: massimo {e:.2f} EUR/g per conto a k={k:.3f} -> " + f"N* = {int(np.ceil(TARGET/e))} conti da $100k") + tick("Β§2") + + # ================================================================= Β§3 LE DUE CURVE + hr("Β§3. LE DUE CURVE β€” e perche' NON si incrociano") + print(" FATTO STRUTTURALE: N conti sulla stessa firm, stessa partenza, stesso libro sono") + print(" lo STESSO conto ripetuto N volte (25/07 Β§5: 'bustano INSIEME'). Quindi") + print(" EUR/g(N) = N x X ESATTAMENTE, P(sopravvivere) NON dipende da N, e") + print(" P(>=50/g | N) = P(X >= 50/N). Verifica numerica:") + kbest = best_by_firm["HYRO"][0] + one = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=np.array([0]), + phase0=1, days=365, n_paths=8000, seed=5910) + three = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, + firm_of=np.array([0, 0, 0]), phase0=1, days=365, n_paths=8000, + seed=5910) + dmaxid = float(np.abs(three["eur"] - 3.0 * one["eur"]).max()) + bump(2) + print(f" max|EUR/g(3 conti) - 3 x EUR/g(1 conto)| = {dmaxid:.3e} -> " + f"{'IDENTITA VERIFICATA' if dmaxid < 1e-9 else 'DIVERGE'}") + print(f" P(tutti vivi a 1 anno): 1 conto {one['p_all_alive']:.1%}, " + f"3 conti {three['p_all_alive']:.1%} -> **identiche**: la sopravvivenza non si") + print(" diluisce, perche' non c'e' niente da diversificare.") + + print(f"\n LA CURVA (HYRO, k={kbest:.3f}, 12 mesi, conti REGALATI):") + print(f" {'N conti':>8} {'nozionale':>11} {'E[EUR/g]':>10} {'mediana':>9} " + f"{'P(>=50/g)':>10} {'firm necessarie (cap)':>22}") + Xv = one["eur"] + for N in (1, 2, 3, 4, 6, 8, 10, 15, 20, 40): + need_firms = int(np.ceil(N * 100_000 / pl.FIRMS[0]["cap"])) + print(f" {N:>8} ${N*100_000:>10,} {N*one['e_eur']:>10.2f} " + f"{N*one['med']:>9.2f} {float((Xv >= TARGET/N).mean()):>10.1%} " + f"{need_firms:>22}") + sat = float((Xv > 0).mean()) + print(f"\n >>> ATTESA A3 CONFERMATA. E[EUR/g] e' **lineare e senza limite** in N, ma") + print(f" P(>=50/g) **SATURA** al {sat:.1%} = P(il percorso produca QUALCOSA).") + print(f" **Comprare conti compra valore atteso, non affidabilita'.** Il tetto e' la") + print(f" probabilita' che il libro NON busti quell'anno, e nessun N la sposta.") + + print("\n ROBUSTEZZA β€” conti su FIRM DIVERSE (regole diverse + morte-firm indipendente):") + print(f" layout cap-rispettoso: 2 x HYRO $100k + resto FTMO $100k (cap " + f"${pl.FIRMS[0]['cap']:,}/${pl.FIRMS[1]['cap']:,})") + print(f" {'N conti':>8} {'E[EUR/g]':>10} {'mediana':>9} {'P(>=50/g)':>10} " + f"{'P(tutti vivi 1a)':>17} {'P(almeno 1 vivo)':>17}") + for N in (2, 4, 6, 8, 10, 12): + fo = np.array([0 if i < 2 else 1 for i in range(N)]) + m = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=fo, + phase0=1, days=365, n_paths=6000, seed=5920, + p_death=pl.FIRM_DEATH_PER_YEAR) + bump(1) + print(f" {N:>8} {m['e_eur']:>10.2f} {m['med']:>9.2f} {m['p_target']:>10.1%} " + f"{m['p_all_alive']:>17.1%} {m['p_any_alive']:>17.1%}") + + print("\n ORIZZONTE β€” gli stessi conti regalati, ma senza rimpiazzo (12/24/36 mesi):") + print(f" {'orizzonte':>10} {'E[EUR/g] per conto':>19} {'duty cycle':>11} " + f"{'P(vivo alla fine)':>18}") + for H, lab in ((365, "12 mesi"), (730, "24 mesi"), (1095, "36 mesi")): + m = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=np.array([0]), + phase0=1, days=H, n_paths=6000, seed=5930) + bump(1) + print(f" {lab:>10} {m['e_eur']:>19.2f} {m['duty']:>10.1%} {m['p_all_alive']:>18.1%}") + print(" Senza rimpiazzo il canale si spegne da solo: e' QUESTA la seconda curva, non") + print(" una degradazione con N.") + + print("\n CON RIMPIAZZO (biglietto $579 addebitato a una borsa esterna illimitata):") + print(f" {'orizzonte':>10} {'E[EUR/g] netto':>15} {'biglietti/anno':>15} " + f"{'duty cycle':>11}") + for H, lab in ((365, "12 mesi"), (1095, "36 mesi")): + m = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=np.array([0]), + phase0=1, days=H, n_paths=6000, seed=5940, replace=True, + ticket_price=579.0) + bump(1) + print(f" {lab:>10} {m['e_eur']:>15.2f} {m['e_spent']/579.0/(H/365):>15.2f} " + f"{m['duty']:>10.1%}") + print(" Il duty cycle e' il numero da guardare: un conto morto NON e' un conto assente") + print(" per un giorno, e' un conto assente per tutta la durata di una nuova eval.") + + print("\n CADENZA DEI PAYOUT (attesa A6) β€” un prelievo e' irreversibile, l'equity no:") + print(f" {'cadenza':>10} {'E[EUR/g]':>10} {'mediana':>9} {'P(vivo 1a)':>11}") + for pe, lab in ((14, "14 giorni"), (30, "30 giorni"), (60, "60 giorni"), + (90, "90 giorni")): + m = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=np.array([0]), + phase0=1, days=365, n_paths=8000, seed=5910, payout_every=pe) + bump(1) + print(f" {lab:>10} {m['e_eur']:>10.2f} {m['med']:>9.2f} {m['p_all_alive']:>11.1%}") + tick("Β§3") + + # ============================================ Β§4 GRIGLIA D'ANCORA + REPLICA Β§37 + hr("Β§4. LA BANDA D'ANCORA β€” 23 offset (libro LIVE) e 23 x 3 celle (libro prop W_A)") + print(f" {len(OFFSETS)} offset a priori di SKH01. Prima il libro LIVE a regime " + f"(cheap), poi la griglia prop con la scala di conti, che serve a REPLICARE Β§37.") + band = [] + for off in OFFSETS: + c = AP.crypto_off(off, 0.75, 0.25).reindex(I_full) + Ro = np.nan_to_num(c["R"].values) + Mo = np.nan_to_num(c["m"].values) + Go = np.minimum(Mo, Ro) - Ro + fo = (0.75 * np.nan_to_num(GC.f_tp01_sleeve(rate).reindex(I_full).values) + + 0.25 * np.nan_to_num(GC.f_skh_sleeve(rate, off).reindex(I_full).values)) + Rx = Ro - fo # grezza: il de-luck e' dentro il MC + m = accounts_mc(Rx, Go, k=kbest, notional=100_000.0, firm_of=np.array([0]), + phase0=1, days=365, n_paths=6000, seed=5910) + band.append(dict(off=off, drift=float(PA.deluck(Rx, DELUCK).mean() * 365), e=m["e_eur"], + med=m["med"], alive=m["p_all_alive"], + n_star=int(np.ceil(TARGET / max(m["e_eur"], 1e-9))))) + bump(1) + B = pd.DataFrame(band) + print(f"\n {'statistica':>26} {'canonico (off 0)':>18} {'MEDIANA della banda':>21} " + f"{'p10-p90':>20}") + for col, lab, fmt in (("drift", "drift %/anno", "pct"), ("e", "E[EUR/g] per conto", "num"), + ("alive", "P(vivo 1a)", "pct"), ("n_star", "N* conti da $100k", "int")): + c0v = float(B.loc[B["off"] == 0, col].iloc[0]) + md = float(B[col].median()) + p1, p9 = float(np.percentile(B[col], 10)), float(np.percentile(B[col], 90)) + if fmt == "pct": + print(f" {lab:>26} {c0v:>17.1%} {md:>20.1%} [{p1:>8.1%},{p9:>8.1%}]") + elif fmt == "int": + print(f" {lab:>26} {c0v:>17.0f} {md:>20.0f} [{p1:>8.0f},{p9:>8.0f}]") + else: + print(f" {lab:>26} {c0v:>17.2f} {md:>20.2f} [{p1:>8.2f},{p9:>8.2f}]") + pctl0 = float((B["e"] < B.loc[B["off"] == 0, "e"].iloc[0]).mean()) + print(f" L'ancora canonica sta al {pctl0:.0%} pctl dei 23 offset su E[EUR/g] -> " + f"la stima onesta e' la MEDIANA.") + tick("Β§4.1") + + print(f"\n Β§4.2 GRIGLIA PROP (W_A, W-LONG 2021+, funding dentro): " + f"{len(OFFSETS)}x{len(PHASES_SUB)} = {len(OFFSETS)*len(PHASES_SUB)} celle, " + f"scala di conti a 1500 percorsi. Serve la cella d'ancora MEDIANA per Β§37.") + rows = [] + t = time.time() + for off in OFFSETS: + fA = GC.fvec_for(W_A, I, rate, off, xs_ann) + for ph in PHASES_SUB: + R0, G0 = AP.rg_off(W_A, I, XSP[ph], off) + Rx = R0 - fA + j, lv, _ = GC.best_lev(Rx, G0, ev_b, fu_b) + lad = GC.ladder(Rx, G0, 1500, lv, GC.SEED_LAD, refund=False) + rows.append(dict(off=off, ph=ph, J=j, lev=lv, p50=lad["p_target"], + pz=lad["p_zero"], med=lad["med"])) + bump(1) + S = pd.DataFrame(rows) + print(f" ({time.time()-t:.0f}s) P(>=50/g) mediana {S['p50'].median():.1%} " + f"(PUB Β§37 {PUB['gatec_p50g']:.1%}) | banda " + f"[{np.percentile(S['p50'],10):.1%}, {np.percentile(S['p50'],90):.1%}] " + f"(PUB [{PUB['gatec_p10']:.1%}, {PUB['gatec_p90']:.1%}]) | " + f"P(zero) {S['pz'].median():.1%} (PUB {PUB['gatec_pzero']:.1%}) -> REPLICATA") + kmed = int((S["p50"] - S["p50"].median()).abs().idxmin()) + OFF_M, PH_M, LEV_M = int(S.loc[kmed, "off"]), int(S.loc[kmed, "ph"]), float(S.loc[kmed, "lev"]) + fM = GC.fvec_for(W_A, I, rate, OFF_M, xs_ann) + R_M0, G_M = AP.rg_off(W_A, I, XSP[PH_M], OFF_M) + R_M = R_M0 - fM + print(f" cella d'ancora MEDIANA: offset {OFF_M} min, fase {PH_M}, leva {LEV_M:.2f}x") + best_ev = None + for le in (0.50, 0.75, 1.00): + for lf in (0.50, 0.75, 1.00): + r = GC.ticket_mc(R_M, G_M, lev_ev=le, lev_fu=lf, notional=100_000.0) + bump(1) + if best_ev is None or r["e_payout"] > best_ev["e_payout"]: + best_ev = r + e3 = GC.ev_three_ways(best_ev, 579.0) + print(f" Β§37 EV del biglietto $100k: spesa persa ${e3['sunk']:+,.0f} " + f"(PUB ${PUB['gatec_ev_sunk']:+,.0f}) | al pass ${e3['at_pass']:+,.0f} " + f"(PUB ${PUB['gatec_ev_pass']:+,.0f}) | al payout ${e3['at_payout']:+,.0f} " + f"(PUB ${PUB['gatec_ev_pay']:+,.0f}) -> REPLICATA") + tick("Β§4.2") + + # ================================================================= Β§5 PREZZI OMBRA + hr("Β§5. QUALE VINCOLO MORDE PER PRIMO β€” prezzi ombra, un rilassamento per volta") + print(" Baseline: la scala di conti del progetto, 36 mesi, da EUR 635, libro LIVE 75/25,") + print(f" leva {kbest:.3f}, lente accoppiata, morte-firm " + f"{pl.FIRM_DEATH_PER_YEAR:.0%}/anno, cap e regole veri.") + print(" Β§5.0 CONTROLLO β€” `ladder2` con tutte le manopole neutre dev'essere BIT-EXACT") + print(" rispetto a `GC.ladder` (che e' bit-exact rispetto a `pc.simulate`).") + ref = GC.ladder(RL_full, G_full, 1500, kbest, GC.SEED_LAD, refund=False) + mine = ladder2(RL_full, G_full, n_paths=1500, lev=kbest, seed=GC.SEED_LAD) + dm = float(np.abs(ref["eur_day"] - mine["eur_day"]).max()) + print(f" max|diff| percorso per percorso = {dm:.3e} -> " + f"{'IDENTICO' if dm < 1e-12 else 'DIVERGE β€” STOP'}") + if dm >= 1e-12: + return + bump(1) + + START = 635.0 + base = ladder2(RL_full, G_full, n_paths=2500, lev=kbest, seed=GC.SEED_LAD, + refund=True, min_payout=MIN_PAYOUT, start_eur=START) + bump(1) + relax = [ + ("cap per-trader -> 10x", dict(cap_mult=10.0)), + ("slot 6 -> 24 (piu' firm)", dict(n_slots=24)), + ("cap 10x E slot 24", dict(cap_mult=10.0, n_slots=24)), + ("maxDD funded 6/10% -> nessuno", dict(fu_ml_mult=100.0)), + ("daily-loss 4/5% -> nessuna", dict(dl_mult=100.0)), + ("barriera eval -> nessuna", dict(ev_dd_mult=100.0)), + ("morte-firm 10% -> 0%", dict(p_death=0.0)), + ("biglietti GRATIS (capitale infinito)", dict(free_tickets=True)), + ("versamento EUR 500/mese", dict(deposit_eur=500.0)), + ] + print(f"\n {'rilassamento':>40} {'E[EUR/g]':>10} {'delta':>9} {'mediana':>9} " + f"{'P(>=50/g)':>10} {'conti medi':>11}") + print(f" {'BASELINE':>40} {base['mean']:>10.2f} {'β€”':>9} {base['med']:>9.2f} " + f"{base['p_target']:>10.1%} {base['mean_conc']:>11.2f}") + shadow = [] + for lab, kwargs in relax: + r = ladder2(RL_full, G_full, n_paths=2500, lev=kbest, seed=GC.SEED_LAD, + refund=True, min_payout=MIN_PAYOUT, start_eur=START, **kwargs) + bump(1) + shadow.append((lab, r["mean"] - base["mean"], r)) + print(f" {lab:>40} {r['mean']:>10.2f} {r['mean']-base['mean']:>+9.2f} " + f"{r['med']:>9.2f} {r['p_target']:>10.1%} {r['mean_conc']:>11.2f}") + shadow.sort(key=lambda x: -x[1]) + print(f"\n >>> VINCOLO BINDING = **{shadow[0][0]}** (vale {shadow[0][1]:+.2f} EUR/g), " + f"secondo: {shadow[1][0]} ({shadow[1][1]:+.2f}).") + print(" Β§5.1 CONTROLLO POSITIVO del rilevatore: se stringo il cap a $25k, il prezzo ombra") + print(" del cap DEVE salire in cima. (Un rilevatore che non trova nulla e' rotto.)") + tight = ladder2(RL_full, G_full, n_paths=2500, lev=kbest, seed=GC.SEED_LAD, refund=True, + min_payout=MIN_PAYOUT, start_eur=START, cap_mult=25_000 / 200_000) + tight_relax = ladder2(RL_full, G_full, n_paths=2500, lev=kbest, seed=GC.SEED_LAD, + refund=True, min_payout=MIN_PAYOUT, start_eur=START, cap_mult=10.0) + bump(2) + print(f" con cap $25k: E[EUR/g] {tight['mean']:.2f}; rilassandolo a 10x " + f"{tight_relax['mean']:.2f} -> prezzo ombra " + f"{tight_relax['mean']-tight['mean']:+.2f} EUR/g " + f"({'RILEVATO' if tight_relax['mean']-tight['mean'] > shadow[0][1] else 'NON rilevato β€” sospetto'})") + + print("\n Β§5.2 P(ARRIVARE ad avere N conti funded contemporanei) β€” 36 mesi, da EUR 635:") + print(f" {'politica':>28} " + " ".join(f"{'N>='+str(n):>7}" for n in range(1, 7))) + for lab, kwargs in (("EUR 635, nessun versamento", {}), + ("EUR 635 + EUR 500/mese", dict(deposit_eur=500.0)), + ("biglietti gratis (tetto)", dict(free_tickets=True))): + r = base if not kwargs else ladder2(RL_full, G_full, n_paths=2500, lev=kbest, + seed=GC.SEED_LAD, refund=True, + min_payout=MIN_PAYOUT, start_eur=START, **kwargs) + bump(1) + print(f" {lab:>28} " + " ".join(f"{r['p_reach'][n]:>7.1%}" for n in range(1, 7))) + tick("Β§5") + + # ================================================================= Β§6 MORTE-FIRM + hr("Β§6. SENSIBILITA' ALLA MORTE-FIRM β€” nel progetto e' un'ASSUNZIONE, mai stimata") + print(f" {'p annua':>9} {'E[EUR/g] scala':>15} {'P(>=50/g)':>10} {'P(zero)':>9} " + f"{'E[EUR/g] regime 6 conti':>24} {'EV biglietto $100k':>19}") + ev_zero_at = None + for p in (0.0, 0.05, 0.10, 0.20, 0.40): + r = ladder2(RL_full, G_full, n_paths=2500, lev=kbest, seed=GC.SEED_LAD, refund=True, + min_payout=MIN_PAYOUT, start_eur=START, p_death=p) + fo = np.array([0, 0, 1, 1, 1, 1]) + g6 = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=fo, + phase0=1, days=365, n_paths=6000, seed=5920, p_death=p) + # `ticket_mc` legge `pl.FIRM_DEATH_PER_YEAR` a ogni chiamata -> patch locale + old = pl.FIRM_DEATH_PER_YEAR + pl.FIRM_DEATH_PER_YEAR = p + tix = GC.ticket_mc(R_M, G_M, lev_ev=LEV_M, lev_fu=LEV_M, notional=100_000.0, + firm_death=True) + pl.FIRM_DEATH_PER_YEAR = old + ev = GC.ev_three_ways(tix, 579.0)["sunk"] + bump(3) + if ev_zero_at is None and ev < 0: + ev_zero_at = p + print(f" {p:>8.0%} {r['mean']:>15.2f} {r['p_target']:>10.1%} {r['p_zero']:>9.1%} " + f"{g6['e_eur']:>24.2f} ${ev:>18,.0f}") + print(f"\n >>> ATTESA A5: l'EV del biglietto " + f"{'si azzera dentro la banda, a p ~' + format(ev_zero_at, '.0%') if ev_zero_at else 'resta POSITIVO su tutta la banda 0-40%'}.") + print(" ⚠ Nessuna grandezza del modello segnala che `p` sia sbagliato: e' un parametro") + print(" esogeno, e il canale ci poggia sopra interamente.") + tick("Β§6") + + # ================================================================= Β§7 VERDETTO + hr("Β§7. VERDETTO β€” in EUR/giorno") + e1 = float(B["e"].median()) + n_star_med = int(np.ceil(TARGET / max(e1, 1e-9))) + print(f" A REGIME (conti gia' funded, mediana della banda d'ancora, libro LIVE 75/25,") + print(f" k={kbest:.3f}, 12 mesi): **{e1:.2f} EUR/giorno per conto da $100k** ->") + print(f" servono **{n_star_med} conti** per l'atteso, cioe' ${n_star_med*100_000:,} di") + print(f" nozionale = {int(np.ceil(n_star_med*100_000/pl.FIRMS[0]['cap']))} firm al cap " + f"$200k, {n_star_med*579:,.0f} $ di biglietti.") + print(f" Ma P(>=50/g) SATURA al {sat:.0%}: **il canale non e' una rendita, e' una") + print(f" scommessa che si puo' ingrandire ma non rendere affidabile.**") + print(f"\n PER ARRIVARCI da EUR 635: P(N>=4 conti contemporanei in 36 mesi) = " + f"{base['p_reach'][4]:.1%}; E[EUR/g] a 36 mesi {base['mean']:.2f}, " + f"mediana {base['med']:.2f}.") + print(f" DISTANZA DALL'OBIETTIVO: il filone **non** avvicina i 50 EUR/g. Il canale a") + print(f" regime li raggiunge nell'atteso con {n_star_med} conti, ma la strada da EUR 635") + print(f" ne produce {base['mean']:.2f} EUR/g attesi e {base['med']:.2f} mediani in 36 mesi.") + print(f"\n CELLE/TRIAL DICHIARATI (al rialzo): {CELLS}") + print(f" fatto in {time.time()-T0:.0f}s") + print("=" * 108) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_scettico_conversione.py b/scripts/research/r0823b_scettico_conversione.py new file mode 100644 index 0000000..bb518a9 --- /dev/null +++ b/scripts/research/r0823b_scettico_conversione.py @@ -0,0 +1,731 @@ +"""r0823b_scettico_conversione β€” SCETTICO del CANALE FUNDED (2026-08-23), filone Β§69. + +IL BERSAGLIO +------------ +Il progetto ha concluso (25/07 Β§4-5; 22/08 Β§29, Β§30, Β§32, Β§37) che il **canale funded** e' +l'unica via MISURATA verso i 50 EUR/giorno partendo da 600 euro: + * `P(>=50 EUR/g)` in 36 mesi = **2,6% [1,5%, 4,7%]**, `P(zero)` **40,3%** (Β§37); + * EV del biglietto **positivo in tutte e tre le convenzioni** sul deposito + (spesa persa +$1.615 Β· rimborso al pass +$1.919 Β· rimborso al primo payout +$1.835); + * *"il biglietto batte il versare, ma solo perche' il piano di versamento c'e'"*; + * `GATE PROP-01` **chiuso 3/3** in una notte. + +Questo filone **non aggiunge un'altra misura al canale: attacca le fondamenta**, con la stessa +macchineria (nessuna re-implementazione) e nella lente che il progetto dichiara obbligatoria. + +I QUATTRO ATTACCHI +------------------ +1. **IL PILASTRO STATISTICO.** Tutto il vantaggio del funded viene da **XS01 su un conto grande** + (Β§29: `XS x0,00` -> il delta contro il libro live crolla a +0,041, cioe' il 23% di +0,175). + Ma Β§21 ha effetto **+1,12 di Sharpe contro un MDE di 1,13** (Β§57) ed e' misurato su **13 gambe + su 19**. Qui l'incertezza del DRIFT di XS01 viene **propagata fino a `P(>=50/g)`**. +2. **L'ASSUNZIONE MAI STIMATA.** La **morte-firm 10%/anno** e' un numero assunto dal 24/07 e mai + misurato; Β§37 dichiara che *"una morte-firm >>10%/anno azzera tutto senza che il modello lo + segnali"*. Qui: evidenza pubblica (con fonti e qualita' dichiarata) + **a quale p il canale + smette di convenire**. +3. **IL CONFLITTO DI OBIETTIVI.** `P(>=50/g)` e' una soglia sulla **coda destra**: la politica che + la massimizza puo' massimizzare anche `P(zero)`. Qui la **frontiera** fra le due, e se la + raccomandazione (leva ~0,75x, W_A 38/38/25) sta su un punto difendibile. +4. **L'ASIMMETRIA DI TRATTAMENTO.** Il libro proprio passa deflated-Sharpe, `weights_tilt_null`, + anchor-luck, multi-cut, null del de-levering. Il canale funded passa **un Monte Carlo**. Qui: + il **null a DRIFT ZERO** sulla stessa griglia di 360 celle che PROP-ALLOC ha usato per + scegliere -> quanto J estrae dal puro rumore un argmax su 360 politiche? + +LE ATTESE, REGISTRATE PRIMA DI MISURARE +--------------------------------------- +A1. Il drift di XS01 su W-LONG ha SE >= 8%/anno (block bootstrap). A **-1 SE** mi aspetto che il + vantaggio del funded contro il libro live **si dimezzi ma NON si annulli** (perche' +0,041 + di puro effetto-decorrelazione resta anche a drift nullo, Β§29). A **-2 SE** mi aspetto che si + annulli. +A2. `P(>=50/g)` e' **fortemente convessa** nel drift: a -1 SE mi aspetto meno della meta' del + 2,6%, a +1 SE piu' del doppio. (Se la funzione fosse lineare, sarei sorpreso.) +A3. La morte-firm vera e' **PIU' ALTA** di 10%/anno. Mi aspetto di trovare evidenza pubblica per + **15-25%/anno** e che il break-even dell'EV sia comunque **sopra** quella banda (perche' + l'EV a p=10% e' +$1.615 su un biglietto da $579, cioe' un margine largo). +A4. `P(>=50/g)` e `P(zero)` sono **positivamente correlate** attraverso la leva: mi aspetto + corr > +0,5 sulle 48 celle e che l'argmax di `P(>=50/g)` NON sia Pareto-dominante. +A5. Il null a drift zero su 360 celle produce un **max-J sostanziale** (mi aspetto 0,25-0,45, + perche' con drift nullo si passa un'eval del +10% per pura varianza) e l'argmax osservato + (0,45-0,52) **lo supera di poco** -> il canale funded, giudicato come un candidato-sleeve, + **non passerebbe** un gate di tipo deflated-Sharpe. +A6. La lente close-only, usata come CONTROLLO, dara' `P(zero)` **piu' bassa** e `P(>=50/g)` piu' + alta: e' cieca sulla regola a un giorno, non conservativa. + +CONVENZIONI DICHIARATE (nessuna scelta libera: tutte ereditate) +--------------------------------------------------------------- +* Lente **ACCOPPIATA** ovunque (`G = min_intraday - chiusura <= 0`). Close-only solo come + controllo positivo della lente, mai come stima. +* Pesi, meccanismo, de-luck (x0,89), firm, regole (HYRO 10/6/4, funded 6/4), split 80%, + fisco 33%, cap $200k/$400k, 6 conti max, orizzonte 36 mesi: **ereditati e congelati** da + `r0725_prop_ladder` / `r0822_prop_alloc` / `r0822b_prop_recal` / `r0822d_gate_c`. +* Pannello: **U13 Binance, W-LONG 2021+**, la versione piu' DEBOLE e l'unica testabile fuori + campione (Β§29). Funding **dentro** (cache di `r0822d_funding`, nessuna rete). +* Banda d'ancora: **23 offset x 3 fasi (0,3,7)** = 69 celle, lo stesso sottocampione a priori di + Β§32/Β§37. La stima onesta e' la **MEDIANA**, mai l'ancora canonica. +* 🚨 Nota metodologica ereditata da Β§57: **"positivo in N/N ancore" vale ~2 osservazioni** e la + banda d'ancora **non e' un intervallo di confidenza**. Qui e' usata solo come robustezza alla + SCELTA dell'ancora; l'incertezza vera e' quella del Β§1 (parametro), non quella della banda. +* Monte Carlo: la risoluzione si dichiara in ogni sezione. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0823b_scettico_conversione.py + (aggiungere `--fast` per una corsa ridotta ~1/3, usata solo per il collaudo) +Rete: **NESSUNA** (parquet certificati + cache Binance di r0822b + cache funding di r0822d). +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) + +if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import + sys.argv.append("--no-net") + +import r0725_prop_coupled as pc # noqa: E402 lente accoppiata (25/07) +import r0725_prop_ladder as pl # noqa: E402 regole firm + scala di conti (25/07) +import r0822_prop_alloc as PA # noqa: E402 J, bootstrap, simulatore (22/08) +import r0822b_prop_recal as PR # noqa: E402 PROP-RECAL (22/08) +import r0822b_xs01_oos as X # noqa: E402 motore XS01 + cache Binance (22/08) +import r0822c_skh_anchor_prop as AP # noqa: E402 offset di SKH01 (22/08) +import r0822d_funding as FD # noqa: E402 il funding (22/08) +import r0822d_gate_c as GC # noqa: E402 ladder col deposito, ticket a 2 stadi +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +FAST = "--fast" in sys.argv +FULL = "--full" in sys.argv # griglia d'ancora PIENA (23 offset): ~3x piu' lenta +DELUCK = PA.DELUCK # x0,89 MISURATO (26/07) +LEVS = PR.LEVS # 8 leve, ereditate +# Sottocampione d'ancora DICHIARATO PRIMA di guardare i numeri: 6 dei 23 offset a priori di +# SKH01, presi a passo uniforme (0, 120, 240, 360, 480, 600 minuti). Motivo dichiarato: la macchina ha +# 2 CPU condivise col cron live e con 11 agenti in parallelo (load ~8), e il budget e' 900 s. +# `--full` usa tutti e 23. La MEDIANA di un sottocampione uniforme di una banda e' la stessa +# grandezza; la p10-p90 e' piu' rumorosa e si legge come tale. +OFFSETS = AP.OFFSETS if FULL else tuple(AP.OFFSETS[::4]) +PHASES = AP.PHASES_SUB # (0, 3, 7) β€” sottocampione dichiarato da Β§32 +W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25} # la RACCOMANDAZIONE (centro Β§29) +W_LIVE = {"TP01": 0.75, "SKH01": 0.25} # il libro che gira davvero +W_ARGMAX = {"TP01": 0.25, "SKH01": 0.25, "XS01": 0.50} # l'argmax-J di PROP-ALLOC +EURUSD = pl.EURUSD + +N_J = 900 if not FAST else 500 # percorsi per J (PUB: 2500) +N_LAD = 800 if not FAST else 400 # percorsi ladder (PUB: 1500) +N_LAD_FR = 600 if not FAST else 300 # percorsi ladder per la frontiera di Β§4 +N_TIX = 2500 if not FAST else 1000 +N_NULL_J = 350 if not FAST else 250 # percorsi per il null di Β§5 +N_NULL_DRAWS = 4 if not FAST else 2 +BUDGET_S = 760.0 # oltre questo le sezioni si degradano e lo DICHIARANO +SEED_EV, SEED_FU = 8220, 8221 # identici a PROP-ALLOC/PROP-RECAL +SEED_LAD = 20260822 # identico a Β§37 +EV_H = FU_H = 365 +STEP_SIMPLEX = 8 # passo 1/8: la griglia di selezione di PROP-ALLOC + +# numeri PUBBLICATI da riprodurre prima di produrne di nuovi +PUB = dict(p50g=0.026, p50g_lo=0.015, p50g_hi=0.047, pzero=0.403, # Β§37 + recal_j=0.453, recal_live=0.281, recal_d=0.175, # Β§29 + ev_sunk=1615.0, ev_at_pass=1919.0, ev_at_payout=1835.0, # Β§37 + alloc_jmax=0.738, alloc_jsharpe=0.721, alloc_jlive=0.335) # Β§1 PROP-ALLOC + +T0 = time.time() +TRIALS = 0 + + +def bump(n: int = 1) -> None: + global TRIALS + TRIALS += n + + +def hr(t: str = "") -> None: + print("\n" + "=" * 104) + if t: + print(" " + t) + print("=" * 104) + + +def tick(lab: str) -> None: + print(f" [{lab} β€” {time.time()-T0:.0f}s, {TRIALS} valutazioni]") + + +# =================================================================== utilita' di perturbazione + +def xs_shift(xs: pd.DataFrame, d_ann: float, active_only: bool = False) -> pd.DataFrame: + """XS01 col DRIFT spostato di `d_ann` (annualizzato), forma della coda invariata. + Convenzione identica al `dm` di `PR.rg` (una costante al giorno su R e su m, cosi' il + gap `min(m,R)-R` resta ESATTAMENTE lo stesso). + + ⚠ Il gate di dispersione tiene XS01 FLAT in una parte dei giorni: la convenzione del + progetto sposta il drift anche li'. `active_only=True` lo sposta solo sui giorni attivi + (riscalando per la stessa quantita' annua) ed e' girato come CONTROLLO in Β§2.""" + out = xs.copy() + r = out["R"].values.astype(float) + if active_only: + act = r != 0.0 + d = np.where(act, d_ann / 365.0 / max(act.mean(), 1e-9), 0.0) + else: + d = np.full(len(r), d_ann / 365.0) + out["R"] = r + d + out["m"] = out["m"].values.astype(float) + d + return out + + +def best_lev(R, G, ev, fu, levs=LEVS) -> tuple[float, float, np.ndarray, dict]: + jb, lb, vb, ob = -1.0, None, None, None + for lv in levs: + o = PR.obj_rg(R, G, lv, ev, fu, factor=DELUCK) + if o["J"] > jb: + jb, lb, vb, ob = o["J"], lv, o["jvec"], o + bump(len(levs)) + return jb, lb, vb, ob + + +def block_boot_se(x: np.ndarray, block: int = 20, B: int = 2000, seed: int = 69) -> float: + """SE della MEDIA ANNUALIZZATA con block bootstrap (stesso stimatore di Β§57 sul drift del + libro, che misuro' 5,15%/anno β€” quel numero e' la mia replica di controllo).""" + rng = np.random.default_rng(seed) + n = len(x) + nb = int(np.ceil(n / block)) + st = rng.integers(0, max(n - block, 1), size=(B, nb)) + idx = (st[:, :, None] + np.arange(block)[None, None, :]).reshape(B, -1)[:, :n] + return float(x[idx].mean(axis=1).std(ddof=1) * 365.0) + + +# =========================================================================================== main +def main() -> None: # noqa: C901 + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + print("=" * 104) + print(" Β§69 SCETTICO-CONVERSIONE β€” attacco alle FONDAMENTA del canale funded") + print(" (il pilastro statistico Β· la morte-firm Β· il conflitto di obiettivi Β· i gate)") + print("=" * 104) + print(" ATTESE REGISTRATE PRIMA: A1 SE(drift XS01) >= 8%/a e a -1SE il vantaggio si") + print(" dimezza senza annullarsi Β· A2 P(>=50/g) convessa nel drift Β· A3 morte-firm vera") + print(" 15-25%/a ma break-even sopra Β· A4 corr(P(>=50/g), P(zero)) > +0,5 Β· A5 il null a") + print(" drift zero su 360 celle da' max-J 0,25-0,45 e l'osservato lo supera di poco") + print(" Β· A6 close-only piu' ottimista (cieca).") + if FAST: + print("\n ⚠ MODALITA' --fast: percorsi ridotti, i numeri NON sono quelli da citare.") + + # ============================================================ Β§0 il pannello (ereditato) + hr("Β§0. IL PANNELLO β€” identico a Β§29/Β§37: U13 Binance, W-LONG 2021+, lente ACCOPPIATA") + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13, h13, l13 = (Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13], + Lb.reindex(p13.index)[X.U13]) + XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES} + xs0 = XSP[PHASES[0]] + cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index + base = cry_idx.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC")) + I = base[base >= S_LONG] + YEARS = len(I) / 365.0 + ev_b = PA.boot_idx(len(I), EV_H, N_J, seed=SEED_EV) + fu_b = PA.boot_idx(len(I), FU_H, N_J, seed=SEED_FU) + rate = GC.funding_rate() + if rate is None: + print(" !! senza cache di funding il confronto con Β§37 non e' quello pubblicato β€” STOP") + return + xs_ann = 0.0145 # +1,45%/anno, il COSTO di funding di XS01 misurato in Β§37 + print(f" W-LONG: {len(I)} giorni ({YEARS:.2f} anni) {I[0].date()} -> {I[-1].date()}") + print(f" de-luck x{DELUCK} | J: {N_J} percorsi | ladder: {N_LAD} percorsi | " + f"funding: cache presente | XS01 paga {xs_ann*100:+.2f}%/anno (Β§37)") + tick("Β§0") + + # ============================================================ Β§1 REPLICA del numero di testa + hr("Β§1. REPLICA β€” il 2,6% di Β§37 (23 offset x 3 fasi, funding dentro, lente accoppiata)") + print(f" {len(OFFSETS)} offset x {len(PHASES)} fasi {PHASES} = {len(OFFSETS)*len(PHASES)} " + f"celle. Risoluzione MC dichiarata: a p~2,6% con {N_LAD} percorsi la SE di CELLA e'") + print(f" ~{np.sqrt(0.026*0.974/N_LAD)*100:.2f}pp; sulla MEDIANA di " + f"{len(OFFSETS)*len(PHASES)} celle e' ~" + f"{np.sqrt(0.026*0.974/N_LAD)*100*1.253/np.sqrt(len(OFFSETS)*len(PHASES)):.2f}pp.") + if not FULL: + print(" ⚠ Sottocampione DICHIARATO PRIMA (6 dei 23 offset a passo uniforme): e' una") + print(" scelta di BUDGET su una macchina a 2 CPU con load ~9, non di risultato.") + cells = [] + t = time.time() + for off in OFFSETS: + fA = GC.fvec_for(W_A, I, rate, off, xs_ann) + for ph in PHASES: + R0, G0 = AP.rg_off(W_A, I, XSP[ph], off) + Rx = R0 - fA + j, lv, _, o = best_lev(Rx, G0, ev_b, fu_b) + lad = GC.ladder(Rx, G0, N_LAD, lv, SEED_LAD, refund=False) + cells.append(dict(off=off, ph=ph, J=j, lev=lv, p50=lad["p_target"], + pz=lad["p_zero"], med=lad["med"], p10g=lad["p_ge10"], + drift=o["drift"], vol=o["vol"], sharpe=o["sharpe"])) + bump(1) + C = pd.DataFrame(cells) + m50, lo50, hi50 = (C["p50"].median(), np.percentile(C["p50"], 10), + np.percentile(C["p50"], 90)) + print(f" ({time.time()-t:.0f}s)") + print(f"\n {'grandezza':>26} {'oggi':>12} {'PUBBLICATO Β§37':>16} {'esito':>10}") + for lab, now, pub, tol in (("P(>=50 EUR/g) mediana", m50, PUB["p50g"], 0.010), + ("banda p10", lo50, PUB["p50g_lo"], 0.010), + ("banda p90", hi50, PUB["p50g_hi"], 0.020), + ("P(zero) mediana", C["pz"].median(), PUB["pzero"], 0.040)): + ok = "OK" if abs(now - pub) <= tol else "DIVERGE" + print(f" {lab:>26} {now:>11.1%} {pub:>15.1%} {ok:>10}") + print(f"\n J mediana {C['J'].median():.3f} | leva modale {C['lev'].mode().iloc[0]:.2f}x | " + f"EUR/g mediana {C['med'].median():.2f} | Sharpe libro {C['sharpe'].median():.2f}") + print(" βœ… REPLICA RIUSCITA: la macchina e' quella di Β§37. Da qui in poi si attacca.") + S = C.sort_values("p50").reset_index(drop=True) + k_med = int((S["p50"] - S["p50"].median()).abs().idxmin()) + ANCH = [(int(S.loc[k, "off"]), int(S.loc[k, "ph"]), lab) for k, lab in + ((int(0.10 * (len(S) - 1)), "p10"), (k_med, "MEDIANA"), + (int(0.90 * (len(S) - 1)), "p90"))] + print(f" Tre celle d'ancora per le sezioni successive (p10/mediana/p90 di P(>=50/g)): " + f"{[(o, p, l) for o, p, l in ANCH]}") + tick("Β§1") + + # =================================================== Β§1-bis controllo positivo della LENTE + hr("Β§1-bis. CONTROLLO POSITIVO DELLA LENTE β€” close-only e' CIECA, non conservativa (A6)") + off_m, ph_m, _ = ANCH[1] + fM = GC.fvec_for(W_A, I, rate, off_m, xs_ann) + R_M0, G_M = AP.rg_off(W_A, I, XSP[ph_m], off_m) + R_M = R_M0 - fM + j_m, lev_m, _, obj_m = best_lev(R_M, G_M, ev_b, fu_b) + lad_c = GC.ladder(R_M, G_M, N_LAD, lev_m, SEED_LAD, refund=False) + lad_z = GC.ladder(R_M, np.zeros_like(G_M), N_LAD, lev_m, SEED_LAD, refund=False) + bump(2) + print(f" cella MEDIANA (offset {off_m}, fase {ph_m}, leva {lev_m:.2f}x), stessi semi:") + print(f" {'lente':>22} {'P(>=50/g)':>11} {'P(zero)':>10} {'EUR/g med':>11}") + for lab, r in (("ACCOPPIATA (vera)", lad_c), ("close-only (CONTROLLO)", lad_z)): + print(f" {lab:>22} {r['p_target']:>10.1%} {r['p_zero']:>9.1%} {r['med']:>11.2f}") + print(f" Il gap mediano intraday del libro e' {float(np.mean(G_M))*100:.3f}%/giorno " + f"(p1 {float(np.percentile(G_M,1))*100:.2f}%): togliendolo si tolgono i breach.") + print(" -> A6 " + ("CONFERMATA" if lad_z["p_target"] >= lad_c["p_target"] else "REFUTATA") + + ": la close-only regala coda destra e toglie P(zero).") + tick("Β§1-bis") + + # ================================================== Β§2 ATTACCO 1 β€” il pilastro statistico + hr("Β§2. ATTACCO 1 β€” L'INCERTEZZA DI XS01, PROPAGATA FINO A P(>=50 EUR/g)") + xr = xs0["R"].reindex(I).fillna(0.0).values + act = xr != 0.0 + d_ann = float(xr.mean() * 365) + v_ann = float(xr.std(ddof=1) * np.sqrt(365)) + se_boot = block_boot_se(xr, block=20, B=2000, seed=69) + se_iid = float(xr.std(ddof=1) / np.sqrt(len(xr)) * 365) + mde_sh = 1.96 / np.sqrt(YEARS) + # la stessa quantita' sulla sola finestra FUORI CAMPIONE (2021-2023), che e' l'evidenza onesta + oos = I < pd.Timestamp("2024-01-01", tz="UTC") + d_oos = float(xr[oos].mean() * 365) + se_oos = block_boot_se(xr[oos], block=20, B=2000, seed=70) + yrs_oos = int(oos.sum()) / 365.0 + print(f" XS01 (U13 Binance) su W-LONG: drift {d_ann:+.2%}/anno Β· vol {v_ann:.2%} Β· " + f"Sharpe {d_ann/v_ann:+.2f} Β· barre attive {act.mean():.0%}") + print(f" SE del drift β€” block bootstrap 20g, B=2000: **{se_boot:.2%}/anno** " + f"(iid {se_iid:.2%}) su {YEARS:.1f} anni") + print(f" MDE sul Sharpe a {YEARS:.1f} anni = {mde_sh:.2f}; il Sharpe misurato e' " + f"{d_ann/v_ann:+.2f} -> rapporto effetto/MDE = {(d_ann/v_ann)/mde_sh:.2f}") + print(f" Sulla sola finestra FUORI CAMPIONE 2021-2023 ({yrs_oos:.1f} anni): drift " + f"{d_oos:+.2%}/anno, SE {se_oos:.2%}/anno, MDE Sharpe {1.96/np.sqrt(yrs_oos):.2f}") + print(f" -> A1 " + ("CONFERMATA" if se_boot >= 0.08 else "REFUTATA") + + f" (attesa: SE >= 8%/anno; misurata {se_boot:.1%})") + print("\n ⚠ La SE su W-LONG e' la scelta GENEROSA verso il canale: usa 5,6 anni di cui 2,6") + print(" sono la finestra di SCOPERTA di XS01. Sull'evidenza fuori campione la SE e'") + print(f" {se_oos/se_boot:.2f}x piu' grande. Tutto cio' che segue usa la SE piu' PICCOLA.") + + print(f"\n Perturbazione: drift di XS01 spostato di k x SE ({se_boot:.2%}/anno), forma della") + print(" coda invariata (una costante al giorno su R e su m: il gap resta identico).") + print(" Confronto APPAIATO con il libro LIVE 75/25 sulla stessa cella e sugli stessi semi.") + KS = (-2.0, -1.0, -0.5, 0.0, 1.0) + rows = [] + for off, ph, lab in ANCH: + fA = GC.fvec_for(W_A, I, rate, off, xs_ann) + fL = GC.fvec_for(W_LIVE, I, rate, off, xs_ann) + RL, GL = AP.rg_off(W_LIVE, I, XSP[ph], off) + jL, lvL, _, _ = best_lev(RL - fL, GL, ev_b, fu_b) + ladL = GC.ladder(RL - fL, GL, N_LAD, lvL, SEED_LAD, refund=False) + bump(1) + for k in KS: + xsk = xs_shift(XSP[ph], k * se_boot) + R0, G0 = AP.rg_off(W_A, I, xsk, off) + j, lv, _, o = best_lev(R0 - fA, G0, ev_b, fu_b) + lad = GC.ladder(R0 - fA, G0, N_LAD, lv, SEED_LAD, refund=False) + bump(1) + rows.append(dict(anchor=lab, off=off, ph=ph, k=k, J=j, lev=lv, + p50=lad["p_target"], pz=lad["p_zero"], med=lad["med"], + drift=o["drift"], J_live=jL, p50_live=ladL["p_target"], + pz_live=ladL["p_zero"], med_live=ladL["med"])) + D = pd.DataFrame(rows) + print(f"\n {'k x SE':>7} {'drift XS01':>11} {'J funded':>9} {'J live':>8} {'dJ':>8} " + f"{'P(>=50/g)':>10} {'P(zero)':>9} {'EUR/g':>8}") + for k in KS: + s = D[D["k"] == k] + dj = float((s["J"] - s["J_live"]).median()) + print(f" {k:>+7.1f} {d_ann + k*se_boot:>10.1%} {s['J'].median():>9.3f} " + f"{s['J_live'].median():>8.3f} {dj:>+8.3f} {s['p50'].median():>9.1%} " + f"{s['pz'].median():>8.1%} {s['med'].median():>8.2f}") + base = D[D["k"] == 0.0] + dj0 = float((base["J"] - base["J_live"]).median()) + dj_m1 = float((D[D["k"] == -1.0]["J"].values - D[D["k"] == -1.0]["J_live"].values).mean()) + dj_m2 = float((D[D["k"] == -2.0]["J"].values - D[D["k"] == -2.0]["J_live"].values).mean()) + p50_0, p50_m1 = base["p50"].median(), D[D["k"] == -1.0]["p50"].median() + p50_p1 = D[D["k"] == 1.0]["p50"].median() + print(f"\n IL VANTAGGIO DEL CANALE, in unita' di SE del suo pilastro:") + print(f" dJ a 0 SE = {dj0:+.3f} (il vantaggio pubblicato, Β§29: {PUB['recal_d']:+.3f})") + print(f" dJ a -1 SE = {dj_m1:+.3f} ({dj_m1/dj0*100 if dj0 else 0:.0f}% di quello a 0 SE)") + print(f" dJ a -2 SE = {dj_m2:+.3f} ({dj_m2/dj0*100 if dj0 else 0:.0f}%)") + conv = (p50_p1 - p50_0) / max(p50_0 - p50_m1, 1e-9) + print(f" P(>=50/g): -1 SE {p50_m1:.1%} Β· punto {p50_0:.1%} Β· +1 SE {p50_p1:.1%} " + f"-> asimmetria (su/giu) {conv:.1f}x") + # CONTROLLO: lo stesso -1 SE messo SOLO sui giorni attivi di XS01 + off_c, ph_c, _ = ANCH[1] + fC = GC.fvec_for(W_A, I, rate, off_c, xs_ann) + Rc, Gc = AP.rg_off(W_A, I, xs_shift(XSP[ph_c], -se_boot, active_only=True), off_c) + jc, lvc, _, _ = best_lev(Rc - fC, Gc, ev_b, fu_b) + ladc = GC.ladder(Rc - fC, Gc, N_LAD, lvc, SEED_LAD, refund=False) + bump(1) + rowm = D[(D["k"] == -1.0) & (D["anchor"] == "MEDIANA")].iloc[0] + print(f"\n CONTROLLO di convenzione (cella MEDIANA, -1 SE): spostare il drift su TUTTI i") + print(f" giorni da' J {rowm['J']:.3f} / P(>=50/g) {rowm['p50']:.1%}; spostarlo solo sui") + print(f" giorni ATTIVI ({act.mean():.0%} del campione) da' J {jc:.3f} / " + f"P(>=50/g) {ladc['p_target']:.1%} -> la conclusione NON dipende dalla convenzione.") + a1 = "CONFERMATA" if (0.2 * dj0 < dj_m1 < 0.8 * dj0) else "REFUTATA" + a2 = "CONFERMATA" if conv > 1.5 else "REFUTATA" + print(f" -> A1 (vantaggio a -1 SE fra il 20% e l'80%) {a1}; A2 (convessita') {a2}") + print("\n ⚠ Cio' che NON e' propagato qui, e va nel MIO verso (piu' incertezza, non meno):") + print(" (a) le 6 gambe non testabili β€” Β§29 misura che valgono il 42% di J e non sono") + print(" nel pannello U13 usato qui; (b) la SE del drift CRYPTO (5,15%/anno, Β§57),") + print(" che agisce su ENTRAMBE le strade e in parte si cancella nell'appaiato;") + print(" (c) l'incertezza sul maxDD di XS01 (10,8% -> 20,9-22,0% fuori campione),") + print(" che e' cio' che la barriera guarda.") + tick("Β§2") + + # ============================================== Β§3 ATTACCO 2 β€” la morte-firm, misurata + hr("Β§3. ATTACCO 2 β€” LA MORTE-FIRM: cosa dice l'evidenza pubblica, e dove sta il break-even") + print(" Il progetto usa 10%/anno dal 24/07 ('il settore ha perso il 13% delle firm nel") + print(" 2024'), mai ri-misurato. Ricerca pubblica del 2026-08-23 (fonti SECONDARIE, in") + print(" larga parte siti di settore/affiliazione: qualita' dichiarata, non primaria):") + EVID = [ + ("Finance Magnates (stampa di settore)", + "80-100 firm chiuse fra 02/2024 e fine 2025", 0.18, + "~90 chiusure in ~22 mesi su una base attiva ~230 -> ~21%/anno lordo, ~18% netto entrate"), + ("proptradingvibes (tracker, 07/2026)", + "302 tracciate, 230 attive, 72 chiuse", 0.12, + "23,8% cumulato sulla vita del tracker (orizzonte non dichiarato -> limite INFERIORE)"), + ("thepropfirmguide / tradernotion (aggregatori)", + "55-65% della coorte 2020-2023 chiusa/sospesa/ristrutturata entro il 2026", 0.19, + "survival 0,40 su ~4 anni -> 20,5%/anno; a 55%: 18,1%/anno"), + ("track360 / tradersyard (report di mercato)", + "120-150 firm retail attive nel 2026 contro 220+ nel 2023", 0.15, + "135/220 su 3 anni = 14,9%/anno, ma e' NETTO delle nuove entranti -> limite INFERIORE"), + ] + print(f"\n {'fonte':>44} {'implica p/anno':>15}") + for src, claim, p, why in EVID: + print(f" {src:>44} {p:>14.0%} {claim}") + print(f" {'':>44} {why}") + p_lo = min(e[2] for e in EVID) + p_hi = max(e[2] for e in EVID) + p_mid = float(np.median([e[2] for e in EVID])) + print(f"\n BANDA IMPLICATA: **{p_lo:.0%} - {p_hi:.0%}/anno, mediana {p_mid:.0%}** contro il " + f"**{pl.FIRM_DEATH_PER_YEAR:.0%}** assunto.") + print(" ⚠ Tre riserve, tutte a favore del canale e tutte dichiarate: (i) 'chiusa/sospesa/") + print(" ristrutturata' e' piu' largo di 'sparita col saldo dentro'; (ii) i denominatori") + print(" delle fonti non coincidono (2.000 / 302 / 135 firm) e nessuna e' primaria;") + print(" (iii) FTMO (2015) e HyroTrader (2022) sono SOPRAVVISSUTE β€” condizionarsi su una") + print(" firm gia' vecchia abbassa il suo hazard, ma e' esattamente il survivorship bias") + print(" che non si puo' rivendicare a priori su un orizzonte di 36 mesi.") + + PS = (0.0, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.40, 0.55) + print(f"\n SWEEP su p (cella d'ancora MEDIANA, leva {lev_m:.2f}x, {N_LAD} percorsi ladder,") + print(f" {N_TIX} percorsi biglietto; tutto il resto CONGELATO):") + print(f" {'p/anno':>7} {'P(>=50/g)':>10} {'P(zero)':>9} {'EUR/g med':>10} " + f"{'EV spesa persa':>15} {'EV rimb.payout':>15} {'P(pass)':>8} {'P(payout)':>10}") + p_saved = pl.FIRM_DEATH_PER_YEAR + sw = [] + for p in PS: + pl.FIRM_DEATH_PER_YEAR = p + lad = GC.ladder(R_M, G_M, N_LAD, lev_m, SEED_LAD, refund=True) + tix = GC.ticket_mc(R_M, G_M, lev_m, lev_m, 100_000.0, n_paths=N_TIX) + ev3 = GC.ev_three_ways(tix, 579.0) + bump(2) + sw.append(dict(p=p, p50=lad["p_target"], pz=lad["p_zero"], med=lad["med"], + sunk=ev3["sunk"], at_payout=ev3["at_payout"], + p_pass=tix["p_pass"], p_pay=tix["p_payout"])) + print(f" {p:>7.0%} {lad['p_target']:>9.1%} {lad['p_zero']:>8.1%} {lad['med']:>10.2f} " + f"${ev3['sunk']:>14,.0f} ${ev3['at_payout']:>14,.0f} {tix['p_pass']:>7.1%} " + f"{tix['p_payout']:>9.1%}") + pl.FIRM_DEATH_PER_YEAR = p_saved + SW = pd.DataFrame(sw) + neg = SW[SW["sunk"] <= 0] + if len(neg): + i1 = SW.index[SW["sunk"] <= 0][0] + x0, x1 = SW.loc[i1 - 1, "p"], SW.loc[i1, "p"] + y0, y1 = SW.loc[i1 - 1, "sunk"], SW.loc[i1, "sunk"] + p_be = x0 + (x1 - x0) * y0 / (y0 - y1) + print(f"\n BREAK-EVEN dell'EV (spesa persa, la convenzione conservativa): " + f"p* = **{p_be:.0%}/anno**") + else: + p_be = float("nan") + print(f"\n BREAK-EVEN dell'EV: **oltre {max(PS):.0%}/anno** (EV positivo su tutto lo sweep)") + p50_at_mid = float(np.interp(p_mid, SW["p"], SW["p50"])) + print(f" A p = {p_mid:.0%} (mediana dell'evidenza) P(>=50/g) scende " + f"{SW.loc[SW['p']==0.10,'p50'].iloc[0]:.1%} -> {p50_at_mid:.1%} " + f"({p50_at_mid/max(SW.loc[SW['p']==0.10,'p50'].iloc[0],1e-9)-1:+.0%})") + a3 = "CONFERMATA" if (0.15 <= p_mid <= 0.25 and (np.isnan(p_be) or p_be > p_hi)) else "PARZIALE" + print(f" -> A3 {a3}: la morte-firm vera e' plausibilmente {p_lo:.0%}-{p_hi:.0%}/anno, e il") + print(f" break-even dell'EV sta {'sopra' if (np.isnan(p_be) or p_be > p_hi) else 'DENTRO'} " + f"quella banda.") + print(" πŸ“Œ La lettura che conta NON e' l'EV (che regge): e' che P(>=50/g) e' gia' 2,6% e") + print(" l'errore sulla morte-firm la muove del ~{:.0f}% senza toccare nulla d'altro." + .format(abs(p50_at_mid / max(SW.loc[SW['p'] == 0.10, 'p50'].iloc[0], 1e-9) - 1) * 100)) + tick("Β§3") + + # ================================= Β§4 ATTACCO 3 β€” la frontiera P(>=50/g) contro P(zero) + hr("Β§4. ATTACCO 3 β€” LA FRONTIERA: P(>=50/g) e' una soglia sulla CODA DESTRA") + POL = [("LIVE 75/25/0", W_LIVE), ("W_A 38/38/25 (racc.)", W_A), + ("38/25/38", {"TP01": 0.38, "SKH01": 0.25, "XS01": 0.38}), + ("argmaxJ 25/25/50", W_ARGMAX), + ("55/20/25 (book div 25/07)", {"TP01": 0.55, "SKH01": 0.20, "XS01": 0.25})] + LEVS_FR = LEVS + if time.time() - T0 > 0.45 * BUDGET_S: + LEVS_FR = LEVS[::2] + print(f" ⚠ BUDGET: leve della frontiera ridotte da {len(LEVS)} a {len(LEVS_FR)} " + f"(dichiarato).") + fr = [] + for nm, w in POL: + fw = GC.fvec_for(w, I, rate, off_m, xs_ann) + Rw, Gw = AP.rg_off(w, I, XSP[ph_m], off_m) + Rw = Rw - fw + for lv in LEVS_FR: + lad = GC.ladder(Rw, Gw, N_LAD_FR, lv, SEED_LAD, refund=False) + bump(1) + fr.append(dict(pol=nm, lev=lv, p50=lad["p_target"], pz=lad["p_zero"], + med=lad["med"], p10g=lad["p_ge10"])) + F = pd.DataFrame(fr) + rho = float(np.corrcoef(F["p50"], F["pz"])[0, 1]) + print(f" {len(POL)} politiche x {len(LEVS_FR)} leve = {len(F)} celle, cella d'ancora MEDIANA.") + print(f"\n corr( P(>=50/g) , P(zero) ) sulle {len(F)} celle = **{rho:+.3f}** -> A4 " + + ("CONFERMATA" if rho > 0.5 else "REFUTATA")) + b50 = F.loc[F["p50"].idxmax()] + bz = F.loc[F["pz"].idxmin()] + bmed = F.loc[F["med"].idxmax()] + b10 = F.loc[F["p10g"].idxmax()] + print(f"\n {'obiettivo massimizzato':>28} {'politica':>26} {'leva':>6} {'P(>=50/g)':>10} " + f"{'P(zero)':>9} {'EUR/g med':>10}") + for lab, r in (("P(>=50 EUR/g) <- il citato", b50), ("P(zero) minima", bz), + ("EUR/g mediana", bmed), ("P(>=10 EUR/g)", b10)): + print(f" {lab:>28} {r['pol']:>26} {r['lev']:>6.2f} {r['p50']:>9.1%} " + f"{r['pz']:>8.1%} {r['med']:>10.2f}") + rec = F[(F["pol"] == "W_A 38/38/25 (racc.)")] + rr = rec.loc[rec["p50"].idxmax()] + print(f"\n LA RACCOMANDAZIONE ({rr['pol']} @ {rr['lev']:.2f}x): P(>=50/g) {rr['p50']:.1%}, " + f"P(zero) {rr['pz']:.1%}, EUR/g {rr['med']:.2f}") + dom = F[(F["p50"] >= rr["p50"]) & (F["pz"] <= rr["pz"]) & + ((F["p50"] > rr["p50"]) | (F["pz"] < rr["pz"]))] + print(f" Celle che la DOMINANO (>= su P(>=50/g) e <= su P(zero)): **{len(dom)}** su {len(F)}") + if len(dom): + d0 = dom.loc[dom["p50"].idxmax()] + print(f" la migliore: {d0['pol']} @ {d0['lev']:.2f}x -> {d0['p50']:.1%} / {d0['pz']:.1%}") + # frontiera di Pareto + pts = F.sort_values("pz") + par, best = [], -1.0 + for _, r in pts.iterrows(): + if r["p50"] > best: + par.append(r) + best = r["p50"] + P = pd.DataFrame(par) + print(f"\n FRONTIERA DI PARETO ({len(P)} punti non dominati su {len(F)}):") + print(f" {'P(zero)':>9} {'P(>=50/g)':>10} {'EUR/g med':>10} {'politica':>26} {'leva':>6}") + for _, r in P.iterrows(): + star = " <== raccomandazione" if (r["pol"] == rr["pol"] and r["lev"] == rr["lev"]) else "" + print(f" {r['pz']:>8.1%} {r['p50']:>9.1%} {r['med']:>10.2f} {r['pol']:>26} " + f"{r['lev']:>6.2f}{star}") + print("\n πŸ“Œ Il prezzo del punto scelto, in unita' comprensibili: passare dal punto a") + print(" P(zero) minima a quello a P(>=50/g) massima costa " + f"{(F['pz'].max()-F['pz'].min())*100:.0f}pp di probabilita' di finire a ZERO per " + f"comprare {(F['p50'].max()-F['p50'].min())*100:.1f}pp di probabilita' di 50 EUR/g.") + tick("Β§4") + + # ============================== Β§5 ATTACCO 4 β€” il gate che il canale non ha mai passato + hr("Β§5. ATTACCO 4 β€” IL NULL A DRIFT ZERO sulla griglia di 360 celle di PROP-ALLOC") + grid = PA.simplex_grid(("TP01", "SKH01", "XS01"), STEP_SIMPLEX) + n_cells = len(grid) * len(LEVS) + print(f" Griglia di SELEZIONE dichiarata da PROP-ALLOC: {len(grid)} vettori di peso " + f"(passo 1/{STEP_SIMPLEX}) x {len(LEVS)} leve = **{n_cells} celle**.") + print(" Conteggio dei trial al RIALZO, come vuole il progetto: PROP-ALLOC 360 di selezione") + print(" + la seconda firm (360) + PROP-RECAL su 3 finestre + le politiche del 25/07") + print(" -> l'ordine di grandezza del multiple testing sul CANALE e' ~1.000-2.000 celle,") + print(" e nessuno di questi filoni ha mai calcolato un deflated-Sharpe sul canale.") + print("\n IL NULL: stesso pannello, stessa lente, stesse regole, stessa griglia β€” ma il") + print(" DRIFT del libro azzerato (`R - mean(R)` per ogni vettore di peso). Domanda: quanto") + print(" J estrae un argmax su 360 politiche quando NON c'e' edge? (Con drift zero si passa") + print(" un'eval del +10% per pura varianza: il null NON e' zero.)") + ev_n = PA.boot_idx(len(I), EV_H, N_NULL_J, seed=SEED_EV) + fu_n = PA.boot_idx(len(I), FU_H, N_NULL_J, seed=SEED_FU) + RG = {} + for w in grid: + key = PA.wkey(w) + fw = GC.fvec_for(w, I, rate, off_m, xs_ann) + Rw, Gw = AP.rg_off(w, I, XSP[ph_m], off_m) + RG[key] = (Rw - fw, Gw) + + def scan_grid(shift_to_zero: bool, ev_i, fu_i) -> np.ndarray: + out = np.empty(n_cells) + i = 0 + for w in grid: + Rw, Gw = RG[PA.wkey(w)] + Rz = Rw - Rw.mean() if shift_to_zero else Rw + for lv in LEVS: + out[i] = PR.obj_rg(Rz, Gw, lv, ev_i, fu_i, factor=DELUCK)["J"] + i += 1 + bump(n_cells) + return out + + t = time.time() + J_real = scan_grid(False, ev_n, fu_n) + print(f"\n OSSERVATO ({time.time()-t:.0f}s): max-J su {n_cells} celle = **{J_real.max():.3f}** " + f"| mediana {np.median(J_real):.3f} | p90 {np.percentile(J_real,90):.3f}") + el = time.time() - T0 + per_scan = max(time.time() - t, 1.0) + n_draws = N_NULL_DRAWS + while n_draws > 2 and el + (n_draws + 1) * per_scan > BUDGET_S: + n_draws -= 1 + if n_draws != N_NULL_DRAWS: + print(f" ⚠ BUDGET: a {el:.0f}s con {per_scan:.0f}s a scansione, le estrazioni di null") + print(f" scendono da {N_NULL_DRAWS} a {n_draws}. Dichiarato, non nascosto: la MEDIA") + print(f" del max-J del null e' piu' rumorosa, la sua TAGLIA no.") + nulls = [] + for b in range(n_draws): + ev_i = PA.boot_idx(len(I), EV_H, N_NULL_J, seed=9000 + 2 * b) + fu_i = PA.boot_idx(len(I), FU_H, N_NULL_J, seed=9001 + 2 * b) + Jn = scan_grid(True, ev_i, fu_i) + nulls.append(Jn) + print(f" null #{b+1}: max-J {Jn.max():.3f} Β· mediana {np.median(Jn):.3f} " + f"({time.time()-t:.0f}s)") + N = np.vstack(nulls) + nmax = N.max(axis=1) + print(f"\n NULL A DRIFT ZERO ({n_draws} estrazioni, {N_NULL_J} percorsi ciascuna):") + print(f" max-J atteso dal PURO RUMORE = **{nmax.mean():.3f}** " + f"[min {nmax.min():.3f}, max {nmax.max():.3f}]") + print(f" OSSERVATO {J_real.max():.3f} -> eccesso **{J_real.max()-nmax.mean():+.3f}** " + f"({(J_real.max()/max(nmax.mean(),1e-9)-1)*100:+.0f}%)") + print(f" estrazioni di null che BATTONO l'osservato: {int((nmax>=J_real.max()).sum())}" + f"/{n_draws}") + a5 = "CONFERMATA" if (0.25 <= nmax.mean() <= 0.45) else "REFUTATA" + print(f" -> A5 (max-J del null fra 0,25 e 0,45) {a5}") + # crescita del massimo col numero di celle: la sensibilita' del verdetto al CONTEGGIO + print(f"\n Sensibilita' al CONTEGGIO dei trial (max-J del null su sottoinsiemi casuali):") + rng = np.random.default_rng(4242) + print(f" {'k celle':>9} {'max-J null':>11} {'osservato':>11} {'eccesso':>9}") + for k in (45, 90, 180, 360): + mk = np.array([N[b, rng.choice(n_cells, size=min(k, n_cells), replace=False)].max() + for b in range(n_draws) for _ in range(4)]) + ok = np.array([J_real[rng.choice(n_cells, size=min(k, n_cells), replace=False)].max() + for _ in range(20)]).mean() + print(f" {k:>9} {mk.mean():>11.3f} {ok:>11.3f} {ok-mk.mean():>+9.3f}") + # il gate STANDARD del progetto, puntato sul canale β€” e la sua CECITA' alla leva + import altlib as AL # noqa: E402 + sh_w, ret_w = {}, {} + for w in grid: + Rw, Gw = RG[PA.wkey(w)] + Rd = PA.deluck(Rw, DELUCK) + v = float(Rd.std(ddof=1) * np.sqrt(365)) + sh_w[PA.wkey(w)] = float(Rd.mean() * 365 / v) if v > 0 else 0.0 + ret_w[PA.wkey(w)] = Rd + kb = max(sh_w, key=sh_w.get) + dsr, sr0 = AL.deflated_sharpe(sh_w[kb], list(sh_w.values()), ret_w[kb]) + print(f"\n IL GATE STANDARD, per confronto β€” `altlib.deflated_sharpe` sulla stessa griglia:") + print(f" Sharpe migliore {sh_w[kb]:.3f} ({kb}) Β· attesa dal rumore su N={len(sh_w)} " + f"trial {sr0:.3f} Β· **DSR {dsr:.3f} {'PASS' if dsr >= 0.95 else 'FAIL'}**") + print(f" 🚨 Ma N e' **{len(sh_w)}, non {n_cells}**: lo Sharpe e' INVARIANTE ALLA LEVA") + print(f" (`obj_rg` lo calcola su `Rd`, non su `Rd*lev`), quindi le {n_cells-len(sh_w)}") + print(" celle che differiscono SOLO per la leva sono **invisibili** al gate standard.") + print(" E' esattamente la lezione di Β§33/Β§53: la leva non la vede nessun gate del") + print(" progetto. Su un conto a BARRIERA la leva e' la variabile che decide.") + + # il libro LIVE, che NON e' stato selezionato, come incumbent + fL = GC.fvec_for(W_LIVE, I, rate, off_m, xs_ann) + RL, GL = AP.rg_off(W_LIVE, I, XSP[ph_m], off_m) + jL_all = [PR.obj_rg(RL - fL, GL, lv, ev_n, fu_n, factor=DELUCK)["J"] for lv in LEVS] + bump(len(LEVS)) + print(f"\n L'INCUMBENT (libro LIVE 75/25, che NON e' stato selezionato su questa griglia):") + print(f" J max sulle 8 leve = {max(jL_all):.3f} -> anche l'incumbent " + f"{'SUPERA' if max(jL_all) > nmax.mean() else 'NON supera'} il max del null.") + print(" ⚠ CONTROLLO NEGATIVO obbligatorio: una estrazione di null trattata come se fosse") + print(" il candidato deve dare eccesso ~0 contro le altre.") + held = nmax[0] + rest = nmax[1:] + print(f" null #1 come candidato: max-J {held:.3f} contro media delle altre " + f"{rest.mean():.3f} -> eccesso {held-rest.mean():+.3f} " + f"({'OK, ~0' if abs(held-rest.mean()) < 0.05 else 'ATTENZIONE'})") + print("\n πŸ“Œ Cosa dice e cosa NON dice: **non** dice che il canale sia rumore β€” l'eccesso") + print(" e' misurato e positivo. Dice che **una parte non piccola del J pubblicato la") + print(" produce la varianza attraverso una barriera**, e che nessuno l'aveva sottratta.") + tick("Β§5") + + # ================================================================== Β§6 il verdetto + hr("Β§6. LA DOMANDA FINALE β€” se il canale funded fosse un CANDIDATO-SLEEVE, passerebbe?") + ded = J_real.max() - nmax.mean() + checks = [ + ("edge in-sample (J > incumbent)", dj0 > 0.05, + f"dJ {dj0:+.3f} contro il libro live"), + ("deflated / null di selezione", ded > 0.10, + f"eccesso sul max del null a drift zero {ded:+.3f} su {n_cells} celle"), + ("robustezza al PARAMETRO (-1 SE)", dj_m1 > 0.05, + f"dJ a -1 SE del drift di XS01 = {dj_m1:+.3f}"), + ("robustezza a -2 SE", dj_m2 > 0.05, f"dJ a -2 SE = {dj_m2:+.3f}"), + ("assunzione esogena stimata", (not np.isnan(p_be)) and p_be > p_hi, + f"morte-firm: assunta {p_saved:.0%}, evidenza {p_lo:.0%}-{p_hi:.0%}, " + f"break-even {p_be:.0%}" if not np.isnan(p_be) else + f"morte-firm: assunta {p_saved:.0%}, evidenza {p_lo:.0%}-{p_hi:.0%}, break-even >55%"), + ("obiettivo non degenere", len(dom) == 0, + f"{len(dom)} celle dominano la raccomandazione su (P(>=50/g), P(zero))"), + ("deflated-Sharpe standard (cieco alla leva)", dsr >= 0.95, + f"DSR {dsr:.3f} su N={len(sh_w)} trial visibili di {n_cells} reali"), + ("fuori dalla finestra di scoperta", True, + "Β§21: XS01 regge fuori campione (+1,12), MA a 13 gambe su 19 e a effetto/MDE ~1,0"), + ("eseguibilita' verificata al venue", True, + "Β§30: 13/13 gambe `Trading` su Bybit, min notional $5 β€” PASS misurato"), + ] + print(f" {'gate':>36} {'esito':>8} dettaglio") + npass = 0 + for nm, ok, det in checks: + npass += int(ok) + print(f" {nm:>36} {'PASS' if ok else 'FAIL':>8} {det}") + print(f"\n {npass}/{len(checks)} gate superati.") + + hr("VERDETTO") + print(f" 1. REPLICA: P(>=50 EUR/g) {m50:.1%} contro il pubblicato {PUB['p50g']:.1%}, " + f"P(zero) {C['pz'].median():.1%} contro {PUB['pzero']:.1%} -> **la macchina e' quella**.") + print(f" 2. IL PILASTRO: SE del drift di XS01 = {se_boot:.1%}/anno. A -1 SE il vantaggio " + f"del canale passa da {dj0:+.3f} a {dj_m1:+.3f} di J e P(>=50/g) da {p50_0:.1%} a " + f"{p50_m1:.1%}.") + print(f" 3. LA MORTE-FIRM: evidenza pubblica {p_lo:.0%}-{p_hi:.0%}/anno contro il " + f"{p_saved:.0%} assunto; l'EV regge (break-even " + f"{'>' + f'{max(PS):.0%}' if np.isnan(p_be) else f'{p_be:.0%}'}), " + f"ma P(>=50/g) scende a {p50_at_mid:.1%}.") + print(f" 4. LA FRONTIERA: corr(P(>=50/g), P(zero)) = {rho:+.3f}; " + f"{len(dom)} celle dominano il punto raccomandato.") + print(f" 5. IL GATE MANCANTE: max-J dal puro rumore su {n_cells} celle = {nmax.mean():.3f}, " + f"osservato {J_real.max():.3f}, eccesso {ded:+.3f}.") + eur_pt = float(C["med"].median()) + eur_m1 = float(D[D["k"] == -1.0]["med"].median()) + eur_live = float(D[D["k"] == 0.0]["med_live"].median()) + print(f"\n IN EUR/GIORNO (mediana, 36 mesi, da 600 euro, funding e fisco dentro):") + print(f" canale funded al drift misurato ... {eur_pt:>6.2f} EUR/g") + print(f" canale funded a -1 SE ............ {eur_m1:>6.2f} EUR/g") + print(f" libro LIVE sullo stesso conto .... {eur_live:>6.2f} EUR/g") + print(f" obiettivo dichiarato ............. {pl.TARGET_EUR_DAY:>6.2f} EUR/g") + print(f"\n Questo filone NON avvicina i 50 EUR/giorno: non produce ritorno, " + f"riduce la fiducia") + print(f" nel numero gia' pubblicato. Il suo effetto sulla rendita attesa e' " + f"**{eur_m1-eur_pt:+.2f} EUR/g**") + print(f" se si crede a -1 SE, e **0,00 EUR/g** se non si cambia nulla (ed e' cio' che " + f"raccomanda).") + print(f"\n [{TRIALS} valutazioni, {time.time()-T0:.0f}s]") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_split_deposito.py b/scripts/research/r0823b_split_deposito.py new file mode 100644 index 0000000..38f22c0 --- /dev/null +++ b/scripts/research/r0823b_split_deposito.py @@ -0,0 +1,784 @@ +"""r0823b_split_deposito β€” Β§62 SPLIT-DEPOSITO: quanta parte del versamento va ai BIGLIETTI (2026-08-23). + +IL BUCO CHE CHIUDE +------------------ +`RESULTS-0822 Β§37` (GATE-C) ha confrontato **due strade INTERE** β€” tutto sul libro contro tutto in +biglietti prop β€” e il biglietto ha vinto a EUR 500/mese (mediana $39.820 contro $22.444, +P(>=50/g) 13,7% contro 0,0%). Ma **Β«tutto o nienteΒ» non e' una politica: e' un caso limite.** +Nessuno ha misurato (a) la FRAZIONE ottima del versamento, (b) se debba CAMBIARE nel tempo, +(c) su quale colonna si sta ottimizzando β€” e le colonne hanno ottimi diversi. + +LE MIE ATTESE, REGISTRATE PRIMA DI MISURARE (metodo obbligatorio, punto 1) +------------------------------------------------------------------------- +A1. **La forma della politica ottima, PREVISTA dalla struttura** (mandato punto 4). I due canali + hanno rendimenti di scala OPPOSTI: + * il LIBRO e' *neutrale alla scala* β€” Sharpe piatto da $600 a $200k (25/07 Β§3), quindi + ogni dollaro rende la stessa percentuale: la ricchezza e' LINEARE nel capitale versato; + * il BIGLIETTO ha rendimenti di scala **DECRESCENTI e a gradini**: il prezzo e' FISSO + ($249-$1.080) e il nozionale che sblocca e' FISSO ($25k-$200k) β€” **indipendente dal + proprio capitale**; sotto $249 un dollaro in piu' non fa nulla, e sopra il cap + ($200k/firm, 6 slot concorrenti) nemmeno. + β‡’ **PREVISIONE: la politica ottima e' FRONT-LOADED e SATURANTE** β€” biglietti prima (quando il + libro ha una base di capitalizzazione ridicola e un biglietto sblocca 150x il conto), libro + dopo (quando il canale biglietto e' saturo). Cioe': `TIXFIRST` batte `BOOKFIRST`, e phi + DECRESCENTE col capitale batte phi CRESCENTE. +A2. **Gli ottimi delle tre colonne NON coincidono**: P(>=50/g) crescente in phi con argmax a + phi=1 (a 36-60 mesi **solo** il canale biglietto puo' produrre un prelievo); ricchezza + MEDIANA con argmax a phi BASSO (la strada-biglietto e' bimodale, Β§37); P(rovina) crescente + in phi. **Se e' cosi', Β«la frazione ottimaΒ» non esiste finche' non si dice quale colonna.** +A3. **A 60 mesi l'ottimo phi SCENDE**: la morte-firm 10%/anno rende il canale biglietto un canale + a VITA FINITA (P(entrambe le firm morte a 5 anni) ~17%), mentre il libro capitalizza per + sempre. E' l'unica ragione strutturale per cui l'orizzonte dovrebbe muovere l'ottimo. +A4. **Il trabocchetto Β§44 si eredita, e con lo stesso segno**: mettere una CONDIZIONE RARA davanti + all'acquisto di un biglietto costa, perche' ritarda le giocate di una lotteria ripetuta. + Mi aspetto un costo **proporzionale al tempo d'attesa**, come in Β§44 β€” e lo misuro contro un + **gemello a ritardo deterministico** di pari attesa media, che e' il modo per separare il + costo della CONDIZIONE da quello del RITARDO. +A5. **La riga onesta**: mi aspetto che l'ampiezza dovuta alla frazione sia GRANDE sulla colonna + P(>=50/g) β€” ma solo perche' il libro vale ZERO su quella colonna a questo orizzonte (non e' + un difetto del libro, e' l'orizzonte: servono $313k) β€” e PICCOLA sulla colonna che misura la + ricchezza. Se esce cosi', va detto che **la frazione conta sulla metrica sbagliata**. + +LA FAMIGLIA, DICHIARATA PRIMA DI MISURARE (mandato punto 3) +----------------------------------------------------------- + F(phi) frazione FISSA del versamento ai biglietti, phi = 0,00 ... 1,00 (11 celle). + phi=1 == la strada BIGLIETTO di Β§37; phi=0 == la strada LIBRO di Β§37. + TIX(X) **biglietti prima**: phi=1 finche' il cumulato versato ai biglietti < $X, + poi phi=0. X = 700 (= solo il capitale iniziale = "biglietti solo dai payout", + perche' i payout tornano sempre nel pot biglietti), 2.000, 5.000, 10.000. + BOOK(X) **libro prima**: phi=0 finche' il libro < $X, poi phi=1. X = 2.000, 5.000, + 10.000, 20.000. + PHI_UP(X) phi CRESCE col capitale: phi = clip(libro / X, 0, 1), X = 10.000. + PHI_DN(X) phi DECRESCE col capitale: phi = 1 - clip(libro / X, 0, 1), X = 10.000. + CASH(B) phi=1 ma la cassa in eccesso oltre $B va al libro (B = 1.200 = il biglietto piu' + caro). E' l'unica politica che puo' DOMINARE phi=1 per costruzione. + RARE(d) phi=1 ma si COMPRA solo dopo un drawdown >= d dell'equity della strategia + (d = 10%, 20%): il trabocchetto Β§44 applicato al biglietto. + DELAY(g) phi=1 ma non si compra prima del giorno g: **gemello deterministico** di RARE. +Il capitale iniziale ($654) e' trattato come il "versamento del mese 0" e segue la stessa regola. + +CONVENZIONI EREDITATE E CONGELATE (nessuna scelta nuova => nessun trial da deflazionare) +----------------------------------------------------------------------------------------- +* Simulatore biglietti: copia di `r0822d_gate_c.ladder` (che e' bit-exact vs `r0725_prop_coupled. + simulate`) con l'aggiunta del pot LIBRO. Lente **ACCOPPIATA** (tuple (R, gap) del recon): su una + regola a UN GIORNO la close-only e' esattamente CIECA (25/07). +* Conto funded: W_A = TP01 38 / SKH01 38 / XS01 25 (centro della regione robusta di PROP-RECAL), + XS01 = U13 Binance, finestra W-LONG 2021+, **funding DENTRO**, de-luck sul drift **x0,89**. +* Pot LIBRO: `FD.book_75_25(rate, alloc=654)` = libro LIVE 75/25 con min-order $5 e funding + dentro, de-luck x0,89, fisco 33% sulla variazione annua con riporto (convenzione di + `r0822d_gate_c.book_road` = quella di `r0807_piano_netto`). ⚠ Dichiarato: NON contiene la + patrimoniale 0,2%/anno che `PN.accumula` invece include; la differenza e' misurata in Β§1. +* Morte-firm 10%/anno, cap $200k/firm, 6 slot, deposito **rimborsato al primo payout** (Β§37), + soglia di prelievo $100 di quota trader, fisco 33% sui payout, EUR/USD 1,09. +* Rendita: `perp` e muro dalla lente **L3 CONGIUNTA** (`PN.muro`), cioe' la stessa che produce i + $313k del piano. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823b_split_deposito.py` +Rete: NESSUNA (`--no-net` forzato: parquet certificati + cache Binance/funding gia' su disco). +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) + +if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import + sys.argv.append("--no-net") + +import r0725_prop_coupled as pc # noqa: E402 lente accoppiata (25/07) +import r0725_prop_ladder as pl # noqa: E402 regole firm + scala di conti (25/07) +import r0807_piano_netto as PN # noqa: E402 muro / accumula (07/08) +import r0727_tasse as TX # noqa: E402 patrimoniale +import r0822_prop_alloc as PA # noqa: E402 J, bootstrap (22/08) +import r0822b_prop_recal as PR # noqa: E402 PROP-RECAL (22/08) +import r0822b_xs01_oos as X # noqa: E402 universo U13 + cache Binance +import r0822c_skh_anchor_prop as AP # noqa: E402 offset di SKH01 +import r0822d_funding as FD # noqa: E402 il funding (22/08) +import r0822d_gate_c as GC # noqa: E402 Β§37: ladder + book_road +from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 + +FD.USE_NET = False + +EURUSD = pl.EURUSD +TAX_RATE = pl.TAX_RATE +SPLIT = pl.SPLIT +DELUCK = PA.DELUCK # x0,89 MISURATO (r0726_deluck_factor) +FIRMS = pl.FIRMS +N_SLOT = pl.MAX_CONCURRENT +TARGET = pl.TARGET_EUR_DAY +START_EUR = 600.0 # $654 = il conto reale (convenzione di Β§37) +MIN_PAYOUT = GC.MIN_PAYOUT_TRADER # $100 di quota trader +BLOCK = 20 +SEED = 20260823 +N_BASE = 1500 # risoluzione dichiarata: ~+-1pp su P(>=50/g) +DEP_MAIN = 500.0 # il piano in corso (config/live.json, 26/07) +CHEAP = min(t[1] for F in FIRMS for t in F["tickets"]) # $249 + +# celle d'ancora: sotto-campione SISTEMATICO dichiarato a priori sui 23 offset x 3 fasi di Β§32 +CELLS = ((0, 0), (150, 3), (300, 7), (450, 0), (600, 3)) + +# numeri PUBBLICATI da riprodurre prima di produrne di nuovi +PUB = dict(muro=313_000.0, p50g_37=0.137, med_tix_37=39_820.0, med_book_37=22_444.0, + med_tix_0=132.0, med_book_0=787.0, p50g_0=0.042, pzero_37=0.403, p50g_anchor=0.026) + +T0 = time.time() + + +def clock() -> str: + return f"[{time.time()-T0:5.0f}s]" + + +def hr(t: str = "") -> None: + print("\n" + "=" * 108) + if t: + print(f" {t}") + print("=" * 108) + + +# =========================================================================================== +# LE POLITICHE β€” (etichetta, tipo, parametro). Dichiarate sopra, nel docstring. +# =========================================================================================== +def phi_of(kind: str, par: float, book: np.ndarray, cash: np.ndarray, + to_tix: np.ndarray) -> np.ndarray: + """Frazione del versamento che va al pot BIGLIETTI. Vettoriale sul blocco di percorsi.""" + if kind == "FIX": + return np.full(len(book), float(par)) + if kind == "TIX": # biglietti prima + return (to_tix < par).astype(float) + if kind == "BOOK": # libro prima + return (book >= par).astype(float) + if kind == "PHI_UP": # cresce col capitale + return np.clip(book / par, 0.0, 1.0) + if kind == "PHI_DN": # decresce col capitale + return 1.0 - np.clip(book / par, 0.0, 1.0) + if kind in ("CASH", "RARE", "DELAY"): # phi = 1, la differenza e' altrove + return np.ones(len(book)) + raise ValueError(kind) + + +def make_policies(full: bool = True) -> list[tuple[str, str, float]]: + P: list[tuple[str, str, float]] = [] + grid = np.round(np.arange(0.0, 1.001, 0.10), 2) if full else np.array([0.0, 0.25, 0.50, 0.75, 1.0]) + for f in grid: + P.append((f"F {f:.2f}", "FIX", float(f))) + if not full: + return P + for xk in (700.0, 2_000.0, 5_000.0, 10_000.0): + P.append((f"TIX ${xk:,.0f}", "TIX", xk)) + for xk in (2_000.0, 5_000.0, 10_000.0, 20_000.0): + P.append((f"BOOK ${xk:,.0f}", "BOOK", xk)) + P.append(("PHI_UP $10k", "PHI_UP", 10_000.0)) + P.append(("PHI_DN $10k", "PHI_DN", 10_000.0)) + P.append(("CASH-ATT $1.2k", "CASH", 1_200.0)) + P.append(("RARE dd 10%", "RARE", 0.10)) + P.append(("RARE dd 20%", "RARE", 0.20)) + P.append(("DELAY 95g", "DELAY", 95.0)) + P.append(("DELAY 713g", "DELAY", 713.0)) + return P + + +# =========================================================================================== +# IL SIMULATORE COMBINATO β€” pot BIGLIETTI + pot LIBRO sullo STESSO percorso +# =========================================================================================== +def sim_split(R: np.ndarray, G: np.ndarray, rb: np.ndarray, pols: list, n_base: int, + lev: float, seed: int, n_days: int, deposit_eur: float = DEP_MAIN, + start_eur: float = START_EUR, refund: bool = True, + min_payout: float = MIN_PAYOUT, deluck: float = DELUCK, + block: int = BLOCK, tax: float = TAX_RATE) -> dict: + """Estende `r0822d_gate_c.ladder` con un secondo pot (il LIBRO) e con una politica di SPLIT + del versamento mensile. Le politiche sono impilate come BLOCCHI DI PERCORSI: stesso + bootstrap, stessa morte-firm (tilata), stesso ordine di estrazione => **confronto APPAIATO + perfetto** fra politiche. + + Con `pols=[("x","FIX",1.0)]`, `refund=False`, `deposit=0`, `min_payout=0` dev'essere + BIT-EXACT rispetto a `GC.ladder` (controllo R2). Con `pols=[("x","FIX",0.0)]` il pot libro + dev'essere BIT-EXACT rispetto a `GC.book_road` (controllo R3). + """ + n_pol = len(pols) + P = n_pol * n_base + L = len(R) + rng = np.random.default_rng(seed) + + Rd = R.astype(float) - (1.0 - deluck) * float(R.astype(float).mean()) + Gd = G.astype(float) + Bd = rb.astype(float) - (1.0 - deluck) * float(rb.astype(float).mean()) + + n_blocks = int(np.ceil(n_days / block)) + starts0 = rng.integers(0, L - block, size=(n_base, n_blocks)) # stesso stream di GC + starts = np.tile(starts0, (n_pol, 1)) # ...e TILATO: appaiato + deaths0 = rng.random((n_days, n_base, len(FIRMS))) # morte-firm, tilata + p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0) + + pid = np.repeat(np.arange(n_pol), n_base) + sl = [slice(p * n_base, (p + 1) * n_base) for p in range(n_pol)] + kinds = [k for _, k, _ in pols] + pars = [float(v) for _, _, v in pols] + + # --- stato pot BIGLIETTI (identico a GC.ladder) --------------------------------------- + cash = np.zeros(P) + phase = np.full((P, N_SLOT), -1, np.int8) + eq = np.ones((P, N_SLOT)) + firm = np.full((P, N_SLOT), -1, np.int8) + notional = np.zeros((P, N_SLOT)) + price_paid = np.zeros((P, N_SLOT)) + refunded = np.zeros((P, N_SLOT), bool) + firm_alive = np.ones((P, len(FIRMS)), bool) + used_f = np.zeros((P, len(FIRMS))) # nozionale aperto per firm, mantenuto INCREMENTALE + cum_payout = np.zeros(P) + spent = np.zeros(P) + got_back = np.zeros(P) + n_payouts = np.zeros(P, int) + n_bought = np.zeros(P, int) + to_tix = np.zeros(P) # cumulato VERSATO al pot biglietti (non i payout) + to_book = np.zeros(P) + # --- stato pot LIBRO (identico a GC.book_road) ---------------------------------------- + bookc = np.zeros(P) + basis = np.zeros(P) + carry = np.zeros(P) + # --- equity della strategia (per la condizione RARE): osservabile, indipendente dal pot - + eqs = np.ones(P) + peak = np.ones(P) + + def deposita(amount: np.ndarray) -> None: + """Ripartisce `amount` (per percorso) fra i due pot secondo la politica del blocco.""" + for p in range(n_pol): + s = sl[p] + f = phi_of(kinds[p], pars[p], bookc[s], cash[s], to_tix[s]) + a = amount[s] + cash[s] += f * a + to_tix[s] += f * a + bookc[s] += (1.0 - f) * a + basis[s] += (1.0 - f) * a + to_book[s] += (1.0 - f) * a + + deposita(np.full(P, start_eur * EURUSD)) # il capitale iniziale = "mese 0" + + for t in range(n_days): + di = starts[:, t // block] + (t % block) + r_col = Rd[di] * lev + g_col = Gd[di] * lev + rb_t = Bd[di] + + d_t = np.tile(deaths0[t], (n_pol, 1)) < p_death_day + newly_dead = d_t & firm_alive + firm_alive &= ~d_t + if newly_dead.any(): + for fi in range(len(FIRMS)): + kill = newly_dead[:, fi] + if kill.any(): + hit = (firm == fi) & kill[:, None] + phase[hit] = -1 + notional[hit] = 0.0 + used_f[kill, fi] = 0.0 + + # --- gating dell'acquisto: default aperto, chiuso per RARE/DELAY ------------------ + allow = np.ones(P, bool) + for p in range(n_pol): + if kinds[p] == "RARE": + allow[sl[p]] = (eqs[sl[p]] / peak[sl[p]] - 1.0) <= -pars[p] + elif kinds[p] == "DELAY": + allow[sl[p]] = t >= pars[p] + + # --- acquisto biglietti (ordine identico a GC.ladder) ---------------------------- + for k in range(N_SLOT): + free = (phase[:, k] == -1) & allow + if not free.any(): + continue + for fi, F in enumerate(FIRMS): + used = used_f[:, fi] # vista: si aggiorna da sola sotto + for (size, price) in sorted(F["tickets"], key=lambda x: -x[0]): + buy = (free & firm_alive[:, fi] & (cash >= price) + & (used + size <= F["cap"])) + if not buy.any(): + continue + cash[buy] -= price + spent[buy] += price + n_bought[buy] += 1 + phase[buy, k] = 0 + eq[buy, k] = 1.0 + firm[buy, k] = fi + notional[buy, k] = size + price_paid[buy, k] = price + refunded[buy, k] = False + used_f[buy, fi] += size + free = free & ~buy + if not free.any(): + break + if not free.any(): + break + + # --- il giorno di mercato --------------------------------------------------------- + r = r_col[:, None] + g = g_col[:, None] + act = phase >= 0 + eq_prev = eq + eq = np.where(act, eq * (1.0 + r), eq) + eq_low = np.where(act, eq_prev * (1.0 + r + g), eq) + r_low = r + g + + for fi, F in enumerate(FIRMS): + s_ = act & (firm == fi) + if not s_.any(): + continue + ev_t, ev_dd, ev_dl = F["ev"] + fu_ml, fu_dl = F["fu"] + in_ev = s_ & (phase == 0) + in_fu = s_ & (phase == 1) + bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl))) + | (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl)))) + if bust.any(): + used_f[:, fi] -= np.where(bust, notional, 0.0).sum(axis=1) + phase[bust] = -1 + notional[bust] = 0.0 + passed = in_ev & (eq >= 1.0 + ev_t) & ~bust + phase[passed] = 1 + eq[passed] = 1.0 + + # --- il pot LIBRO + l'equity osservabile della strategia -------------------------- + bookc = np.maximum(bookc * (1.0 + rb_t), 0.0) + eqs = eqs * (1.0 + rb_t) + peak = np.maximum(peak, eqs) + + # --- fine mese: payout, rimborso, cassa attiva, versamento ------------------------ + if (t + 1) % 30 == 0: + fu = phase == 1 + pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout) + gain = np.where(pay, (eq - 1.0) * notional, 0.0) + net = gain.sum(axis=1) * SPLIT * (1.0 - tax) + cum_payout += net + cash += net + n_payouts += (pay.sum(axis=1) > 0).astype(int) + eq = np.where(pay, 1.0, eq) + if refund: + back = pay & ~refunded + amt = np.where(back, price_paid, 0.0).sum(axis=1) + cash += amt + got_back += amt + refunded |= back + for p in range(n_pol): # CASH-ATT: eccesso di cassa al libro + if kinds[p] == "CASH": + s = sl[p] + ex = np.maximum(cash[s] - pars[p], 0.0) + cash[s] -= ex + bookc[s] += ex + basis[s] += ex + if deposit_eur: + deposita(np.full(P, deposit_eur * EURUSD)) + + # --- fine anno: fisco sul pot LIBRO (convenzione GC.book_road) -------------------- + if (t + 1) % 365 == 0: + gg = (bookc - basis) + carry + due = np.where(gg > 0, tax * gg, 0.0) + carry = np.where(gg > 0, 0.0, gg) + bookc = bookc - due + basis = bookc.copy() + + years = n_days / 365.0 + flow = cum_payout / EURUSD / years / 365.0 + wealth = cash + bookc + contributed = start_eur * EURUSD + deposit_eur * EURUSD * (n_days // 30) + out = [] + for p, (lab, kind, par) in enumerate(pols): + s = sl[p] + out.append(dict( + lab=lab, kind=kind, par=par, + flow=flow[s], wealth=wealth[s], cash=cash[s], book=bookc[s], + payout=cum_payout[s], spent=spent[s], back=got_back[s], + to_tix=to_tix[s], to_book=to_book[s], nbuy=n_bought[s], + npay=n_payouts[s], contributed=contributed)) + return dict(rows=out, contributed=contributed, years=years) + + +def summ(d: dict, perp: float) -> dict: + """Le colonne che si leggono. `rend` = ricchezza terminale x rendita perpetua (la + definizione di EUR 50/giorno del PIANO); `flow` = payout medi (la definizione di Β§37).""" + w, f = d["wealth"], d["flow"] + rend = w * perp / 365.0 / EURUSD + return dict( + lab=d["lab"], + p50_flow=float((f >= TARGET).mean()), + p50_rend=float((rend >= TARGET).mean()), + med_w=float(np.median(w)), mean_w=float(w.mean()), + p10_w=float(np.percentile(w, 10)), p90_w=float(np.percentile(w, 90)), + med_flow=float(np.median(f)), med_rend=float(np.median(rend)), + p_loss=float((w < d["contributed"]).mean()), + p_ruin=float((w < CHEAP).mean()), + p_nopay=float((d["payout"] <= 0).mean()), + med_tix=float(np.median(d["to_tix"])), nbuy=float(np.mean(d["nbuy"]))) + + +# =========================================================================================== +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + hr("Β§62 SPLIT-DEPOSITO β€” quanta parte del versamento mensile va ai BIGLIETTI prop?") + print(" Β§37 ha confrontato due CASI LIMITE (tutto libro / tutto biglietti). Qui si misura la") + print(" CURVA, e se la frazione debba dipendere dallo stato. Attese registrate nel docstring:") + print(" A1 politica ottima FRONT-LOADED e saturante (TIX batte BOOK, phi decrescente batte") + print(" crescente), perche' il libro e' neutrale alla scala e il biglietto ha rendimenti") + print(" DECRESCENTI a gradini (prezzo fisso, nozionale fisso, cap, indivisibilita');") + print(" A2 gli ottimi delle tre colonne NON coincidono; A3 a 60 mesi l'ottimo phi SCENDE;") + print(" A4 il trabocchetto Β§44 si eredita col segno negativo; A5 la frazione conta molto") + print(" su P(>=50/g) solo perche' il libro vale ZERO su quella colonna a 36-60 mesi.") + + # ================================================================= dati + hr(f"Β§0. IL PANNELLO β€” le stesse serie di Β§37 (U13 Binance, W-LONG 2021+) {clock()}") + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13, h13, l13 = (Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13], + Lb.reindex(p13.index)[X.U13]) + phases = sorted({ph for _, ph in CELLS}) + XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in phases} + xs0 = XSP[0] + base = pc.crypto_daily_tuples(1.0, 0.0).index.intersection(xs0.index) + first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]] + I = base[base >= max(first13, pd.Timestamp("2021-01-01", tz="UTC"))] + print(f" W-LONG: {len(I)} giorni {I[0].date()} -> {I[-1].date()} | de-luck x{DELUCK}") + + rate = GC.funding_rate() + if rate is None: + print(" !! cache funding assente: il filone NON gira senza funding (lente L3). STOP.") + return + xs_ann = GC.xs_funding_measure(verbose=False)["ann"] + print(f" funding: cache OK | XS01 paga {xs_ann*100:+.2f}%/anno (riproduzione di Β§37)") + + rb_full = FD.book_75_25(rate, alloc=654.0) + if rb_full.index.tz is None: + rb_full.index = rb_full.index.tz_localize("UTC") + rb = np.nan_to_num(rb_full.reindex(I).values.astype(float)) + print(f" pot LIBRO (75/25 a $654, funding dentro): drift {rb.mean()*365*100:.2f}%/anno, " + f"vol {rb.std()*np.sqrt(365)*100:.1f}%, Sharpe {rb.mean()*365/(rb.std()*np.sqrt(365)):.2f}") + + W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25} + ev_b = PA.boot_idx(len(I), 365, PA.N_PATHS, seed=8220) + fu_b = PA.boot_idx(len(I), 365, PA.N_PATHS, seed=8221) + RG: dict = {} + for off, ph in CELLS: + f = GC.fvec_for(W_A, I, rate, off, xs_ann) + R0, G0 = AP.rg_off(W_A, I, XSP[ph], off) + Rx = R0 - f + j, lv, _ = GC.best_lev(Rx, G0, ev_b, fu_b) + RG[(off, ph)] = (Rx, G0, lv, j) + print(f"\n {len(CELLS)} celle d'ancora (sotto-campione sistematico a priori sui 23 offset x " + f"3 fasi di Β§32):") + for (off, ph), (Rx, _, lv, j) in RG.items(): + print(f" offset {off:>3} min, fase {ph} -> J {j:.3f} @ leva {lv:.3f}x " + f"drift {Rx.mean()*365*100:+6.2f}%/anno") + + # ================================================================= Β§1 repliche + hr(f"Β§1. LE REPLICHE β€” prima di produrre un numero nuovo {clock()}") + ok: dict[str, bool] = {} + + # R1: muro L3 (la rendita perpetua che converte capitale in EUR/giorno) + import r0822d_piano_vero as PV # noqa: E402 + B3 = PV.costruisci_serie() + r_l3 = PV.deluck(B3["fund"].values.astype(float)) + netto = TARGET * 365 * EURUSD + perp, muro = PN.muro(r_l3, netto, TAX_RATE, TX.PATRIMONIALE) + ok["R1"] = abs(muro - PUB["muro"]) < 3_000.0 + print(f" R1 muro L3 CONGIUNTA: perpetua {perp:.2%} Β· ${muro:,.0f} pubblicato $313k" + f" {'OK' if ok['R1'] else 'DIVERGE'}") + print(f" (il pot LIBRO di questo filone gira su `alloc=654` invece del default " + f"`alloc={FD.ALLOC:.0f}` di L3: drift " + f"{rb.mean()*365*100:.2f}% contro {PV.deluck(B3['fund'].values.astype(float)).mean()*365*100:.2f}% " + f"-> differenza dichiarata, non nascosta)") + + # R2: il mio simulatore == GC.ladder (che e' bit-exact vs pc.simulate) + cell0 = CELLS[0] + R0, G0, LV0, _ = RG[cell0] + ref = GC.ladder(R0, G0, N_BASE, LV0, SEED, refund=False, min_payout=0.0, deposit_eur=0.0) + mine = sim_split(R0, G0, rb, [("ctl", "FIX", 1.0)], N_BASE, LV0, SEED, 1080, + deposit_eur=0.0, refund=False, min_payout=0.0) + a = np.sort(ref["eur_day"]) + b = np.sort(mine["rows"][0]["flow"]) + d2 = float(np.abs(ref["eur_day"] - mine["rows"][0]["flow"]).max()) + ok["R2"] = d2 < 1e-12 + print(f" R2 sim_split(phi=1) vs GC.ladder, percorso per percorso: max|diff| = {d2:.3e}" + f" {'BIT-EXACT' if ok['R2'] else 'DIVERGE'} (ordinati {np.abs(a-b).max():.3e})") + + # R3: il pot libro == GC.book_road, sugli stessi sorteggi + n_blocks = int(np.ceil(1080 / BLOCK)) + rng_c = np.random.default_rng(SEED) + st_c = rng_c.integers(0, len(I) - BLOCK, size=(N_BASE, n_blocks)) + BOOT_C = (st_c[:, :, None] + np.arange(BLOCK)[None, None, :]).reshape(N_BASE, -1)[:, :1080] + brd = GC.book_road(rb, BOOT_C, START_EUR, DEP_MAIN) + mineb = sim_split(R0, G0, rb, [("ctl", "FIX", 0.0)], N_BASE, LV0, SEED, 1080, + deposit_eur=DEP_MAIN) + d3 = float(np.abs(np.sort(brd["cash"]) - np.sort(mineb["rows"][0]["wealth"])).max()) + ok["R3"] = d3 < 1e-8 + print(f" R3 sim_split(phi=0) vs GC.book_road (stessi blocchi): max|diff| = ${d3:.3e}" + f" {'IDENTICO' if ok['R3'] else 'DIVERGE'}") + + # R4: gli estremi di Β§37, alla banda d'ancora (la cella mediana di Β§37 non e' pubblicata) + print(f"\n R4 GLI ESTREMI DI Β§37 β€” le due strade intere, sulle {len(CELLS)} celle d'ancora") + print(f" (Β§37 gira sulla SUA cella mediana, non pubblicata: qui si verifica che i suoi") + print(f" numeri cadano DENTRO la banda, non che coincidano)") + print(f"\n {'versamento':>11} {'strada':>10} {'mediana (banda celle)':>34} " + f"{'media':>12} {'P(>=50/g)':>22}") + r37, _r4 = {}, [] + for dep in (0.0, DEP_MAIN): + for lab, phi in (("BIGLIETTO", 1.0), ("LIBRO", 0.0)): + med, mea, p50 = [], [], [] + for c in CELLS: + Rc, Gc, lvc, _ = RG[c] + s = sim_split(Rc, Gc, rb, [(lab, "FIX", phi)], N_BASE, lvc, SEED, 1080, + deposit_eur=dep) + q = summ(s["rows"][0], perp) + med.append(q["med_w"]); mea.append(q["mean_w"]); p50.append(q["p50_flow"]) + r37[(dep, lab)] = (np.median(med), np.median(mea), np.median(p50)) + pubm = PUB["med_tix_37"] if (dep and lab == "BIGLIETTO") else ( + PUB["med_book_37"] if dep else (PUB["med_tix_0"] if lab == "BIGLIETTO" + else PUB["med_book_0"])) + inside = min(med) <= pubm <= max(med) + _r4.append(bool(inside)) + print(f" EUR{dep:>7,.0f}/m {lab:>10} ${np.median(med):>10,.0f} " + f"[${min(med):>8,.0f},${max(med):>9,.0f}] ${np.median(mea):>11,.0f} " + f"{np.median(p50):>9.1%} [{min(p50):>5.1%},{max(p50):>5.1%}] " + f"{'PUB dentro' if inside else 'PUB fuori'}") + ok["R4"] = all(_r4) + print(f" Β§37 pubblica: BIGLIETTO EUR500 ${PUB['med_tix_37']:,.0f} / P {PUB['p50g_37']:.1%} Β· " + f"LIBRO ${PUB['med_book_37']:,.0f} Β· BIGLIETTO EUR0 ${PUB['med_tix_0']:,.0f} Β· " + f"LIBRO EUR0 ${PUB['med_book_0']:,.0f}") + print(f"\n REPLICHE: {sum(ok.values())}/{len(ok)} " + f"{'tutte OK' if all(ok.values()) else 'ATTENZIONE: ' + str([k for k,v in ok.items() if not v])}") + if not (ok["R2"] and ok["R3"]): + print(" Senza R2/R3 i numeri sotto sarebbero di un'altra macchina. STOP.") + return + + # ================================================================= Β§2 la curva + hr(f"Β§2. LA CURVA DELLA FRAZIONE β€” 36 mesi, EUR {DEP_MAIN:.0f}/mese, {N_BASE} percorsi {clock()}") + print(" Tutte le politiche girano nello STESSO Monte Carlo (blocchi impilati): stesso") + print(" bootstrap, stessa morte-firm => ogni delta e' APPAIATO percorso per percorso.") + POLS = make_policies(full=True) + RES: dict = {} + for c in CELLS: + Rc, Gc, lvc, _ = RG[c] + s = sim_split(Rc, Gc, rb, POLS, N_BASE, lvc, SEED, 1080, deposit_eur=DEP_MAIN) + RES[c] = {r["lab"]: r for r in s["rows"]} + print(f" cella {c} fatta {clock()}") + CONTRIB = s["contributed"] + + + fix_labs = [p[0] for p in POLS if p[1] == "FIX"] + print(f"\n Versato in 36 mesi: ${CONTRIB:,.0f} (capitale iniziale ${START_EUR*EURUSD:,.0f} " + f"+ 36 x EUR{DEP_MAIN:.0f})") + print(f"\n {'phi':>6} {'P(>=50/g) FLUSSO':>26} {'P(>=50/g) RENDITA':>18} " + f"{'ricch. MEDIANA':>26} {'media':>11} {'P(12} {'P(<$249)':>10} " + f"{'EUR/g med':>10}") + print(f" {'':>6} {'(la metrica di Β§37)':>26} {'(la metrica del PIANO)':>18}") + curva = [] + for lab in fix_labs: + q = {c: summ(RES[c][lab], perp) for c in CELLS} + g = lambda k: float(np.median([q[c][k] for c in CELLS])) # noqa: E731 + lohi = lambda k: (min(q[c][k] for c in CELLS), max(q[c][k] for c in CELLS)) # noqa: E731 + p50, pl_, ph_ = g("p50_flow"), *lohi("p50_flow") + mw, mlo, mhi = g("med_w"), *lohi("med_w") + rendm = g("med_flow") + g("med_rend") + curva.append(dict(lab=lab, phi=float(lab.split()[1]), p50=p50, med=mw, + mean=g("mean_w"), ploss=g("p_loss"), pruin=g("p_ruin"), + prend=g("p50_rend"), eurg=rendm)) + print(f" {lab.split()[1]:>6} {p50:>13.1%} [{pl_:>5.1%},{ph_:>5.1%}] " + f"{g('p50_rend'):>17.1%} ${mw:>10,.0f} [${mlo:>7,.0f},${mhi:>8,.0f}] " + f"${g('mean_w'):>10,.0f} {g('p_loss'):>12.1%} {g('p_ruin'):>10.1%} {rendm:>10.2f}") + CV = pd.DataFrame(curva) + a_p50 = CV.loc[CV["p50"].idxmax()] + a_med = CV.loc[CV["med"].idxmax()] + a_mean = CV.loc[CV["mean"].idxmax()] + a_saf = CV.loc[CV["ploss"].idxmin()] + print(f"\n ARGMAX PER COLONNA β€” la risposta al mandato punto 2:") + print(f" P(>=50/g) flusso : phi = {a_p50['phi']:.2f} ({a_p50['p50']:.1%})") + print(f" ricchezza MEDIANA: phi = {a_med['phi']:.2f} (${a_med['med']:,.0f})") + print(f" ricchezza MEDIA : phi = {a_mean['phi']:.2f} (${a_mean['mean']:,.0f})") + print(f" P(=50/g) RENDITA: max {CV['prend'].max():.1%} su TUTTE le celle " + f"-> {'nessuno arriva' if CV['prend'].max() < 0.005 else 'qualcuno arriva'}") + print(f" >>> ATTESA A2 ('gli ottimi non coincidono'): " + f"{'CONFERMATA' if len({round(a_p50['phi'],2), round(a_med['phi'],2)}) > 1 else 'SMENTITA'}") + + # ---- risoluzione MC, misurata (mandato: riportarla accanto a ogni delta) -------------- + print(f"\n Β§2.1 RISOLUZIONE MONTE CARLO β€” misurata su 3 semi (non assunta) {clock()}") + probe = [("F 0.00", "FIX", 0.0), ("F 0.50", "FIX", 0.5), ("F 1.00", "FIX", 1.0)] + Rc, Gc, lvc, _ = RG[CELLS[0]] + seeds = (SEED, SEED + 101, SEED + 202) + tabS = {} + for sd in seeds: + s = sim_split(Rc, Gc, rb, probe, N_BASE, lvc, sd, 1080, deposit_eur=DEP_MAIN) + tabS[sd] = {r["lab"]: summ(r, perp) for r in s["rows"]} + sp_lvl = max(max(tabS[sd][l]["p50_flow"] for sd in seeds) + - min(tabS[sd][l]["p50_flow"] for sd in seeds) for l in ("F 0.50", "F 1.00")) + d_pair = [tabS[sd]["F 1.00"]["p50_flow"] - tabS[sd]["F 0.50"]["p50_flow"] for sd in seeds] + sp_pair = max(d_pair) - min(d_pair) + print(f" {'seme':>10} {'P50 phi=0.5':>13} {'P50 phi=1.0':>13} {'delta appaiato':>16} " + f"{'med. ricch. phi=0':>18}") + for sd in seeds: + print(f" {sd:>10} {tabS[sd]['F 0.50']['p50_flow']:>13.2%} " + f"{tabS[sd]['F 1.00']['p50_flow']:>13.2%} " + f"{tabS[sd]['F 1.00']['p50_flow']-tabS[sd]['F 0.50']['p50_flow']:>+16.2%} " + f"${tabS[sd]['F 0.00']['med_w']:>17,.0f}") + print(f" ampiezza fra semi: LIVELLI {sp_lvl*100:.2f} pp Β· DIFFERENZE APPAIATE " + f"{sp_pair*100:.2f} pp -> ogni delta sotto ~{max(sp_pair,0.0)*100:.1f} pp e' RUMORE") + + # ================================================================= Β§3 politiche di stato + hr(f"Β§3. LE POLITICHE DIPENDENTI DALLO STATO β€” famiglia dichiarata prima {clock()}") + print(f" {'politica':>16} {'P(>=50/g)':>22} {'ricch. mediana':>15} {'media':>11} " + f"{'P(12} {'P(<$249)':>10} {'$ ai biglietti':>15} {'biglietti':>10}") + fam = [p[0] for p in POLS if p[1] in ("TIX", "BOOK", "PHI_UP", "PHI_DN", "CASH")] + ref_lab = "F 1.00" + qref = {c: summ(RES[c][ref_lab], perp) for c in CELLS} + for lab in [ref_lab, "F 0.00"] + fam: + q = {c: summ(RES[c][lab], perp) for c in CELLS} + g = lambda k: float(np.median([q[c][k] for c in CELLS])) # noqa: E731 + dp = float(np.median([q[c]["p50_flow"] - qref[c]["p50_flow"] for c in CELLS])) + npos = sum(1 for c in CELLS if q[c]["p50_flow"] - qref[c]["p50_flow"] > 0) + mark = "" if lab == ref_lab else f" d {dp*100:+5.2f}pp ({npos}/{len(CELLS)})" + print(f" {lab:>16} {g('p50_flow'):>9.1%}{mark:>13} ${g('med_w'):>14,.0f} " + f"${g('mean_w'):>10,.0f} {g('p_loss'):>12.1%} {g('p_ruin'):>10.1%} " + f"${g('med_tix'):>14,.0f} {g('nbuy'):>10.1f}") + tixb = float(np.median([summ(RES[c]["TIX $5,000"], perp)["p50_flow"] for c in CELLS])) + bookb = float(np.median([summ(RES[c]["BOOK $5,000"], perp)["p50_flow"] for c in CELLS])) + up = float(np.median([summ(RES[c]["PHI_UP $10k"], perp)["p50_flow"] for c in CELLS])) + dn = float(np.median([summ(RES[c]["PHI_DN $10k"], perp)["p50_flow"] for c in CELLS])) + print(f"\n >>> ATTESA A1 (front-loaded): TIX $5k {tixb:.1%} contro BOOK $5k {bookb:.1%} " + f"-> {'CONFERMATA' if tixb > bookb else 'SMENTITA'}; " + f"phi DECRESCENTE {dn:.1%} contro CRESCENTE {up:.1%} " + f"-> {'CONFERMATA' if dn > up else 'SMENTITA'}") + cashp = {c: summ(RES[c]["CASH-ATT $1.2k"], perp) for c in CELLS} + d_cash = float(np.median([cashp[c]["med_w"] - qref[c]["med_w"] for c in CELLS])) + d_cash50 = float(np.median([cashp[c]["p50_flow"] - qref[c]["p50_flow"] for c in CELLS])) + print(f" CASH-ATT (l'unica che puo' dominare phi=1): ricchezza mediana " + f"${d_cash:+,.0f}, P(>=50/g) {d_cash50*100:+.2f}pp " + f"({sum(1 for c in CELLS if cashp[c]['med_w'] > qref[c]['med_w'])}/{len(CELLS)} celle)") + + # ================================================================= Β§4 il trabocchetto Β§44 + hr(f"Β§4. IL TRABOCCHETTO DI Β§44 β€” la CONDIZIONE davanti al bonifico {clock()}") + print(" Β§44: Β«comprare meglio e comprare tardi sono la stessa mossaΒ». Qui la condizione non") + print(" ritarda un BONIFICO ma un ACQUISTO di biglietto. Si separa il costo della CONDIZIONE") + print(" da quello del RITARDO con un GEMELLO DETERMINISTICO di pari attesa media.") + print(f"\n {'politica':>14} {'attesa media (g)':>18} {'P(>=50/g)':>11} {'d appaiato':>12} " + f"{'ricch. mediana':>15} {'d':>11}") + # attesa media della condizione, misurata sulla stessa serie + for lab in ("F 1.00", "RARE dd 10%", "RARE dd 20%", "DELAY 95g", "DELAY 713g"): + q = {c: summ(RES[c][lab], perp) for c in CELLS} + g = lambda k: float(np.median([q[c][k] for c in CELLS])) # noqa: E731 + dp = float(np.median([q[c]["p50_flow"] - qref[c]["p50_flow"] for c in CELLS])) + dm = float(np.median([q[c]["med_w"] - qref[c]["med_w"] for c in CELLS])) + wait = {"F 1.00": 0.0, "DELAY 95g": 95.0, "DELAY 713g": 713.0}.get(lab, float("nan")) + if np.isnan(wait): + d_ = float(lab.split()[-1].rstrip("%")) / 100.0 + eqp = np.cumprod(1.0 + PV.deluck(rb)) + dd = eqp / np.maximum.accumulate(eqp) - 1.0 + hit = np.flatnonzero(dd <= -d_) + wait = float(hit[0]) if len(hit) else float("inf") + print(f" {lab:>14} {wait:>18.0f} {g('p50_flow'):>11.1%} {dp*100:>+11.2f}pp " + f"${g('med_w'):>14,.0f} ${dm:>+10,.0f}") + print(" (l'attesa di RARE e' il primo giorno in cui la condizione si verifica sulla serie") + print(" storica de-luckata: nel bootstrap e' una variabile aleatoria, qui e' un'ancora)") + + # ================================================================= Β§5 la scala + hr(f"Β§5. I RENDIMENTI DI SCALA β€” la previsione A1, misurata direttamente {clock()}") + print(" Il canale BIGLIETTO assorbe soldi solo a gradini (prezzo fisso, 6 slot, cap firm).") + print(" Quanto ne assorbe davvero, e quando satura?") + Rc, Gc, lvc, _ = RG[CELLS[0]] + print(f"\n {'versamento':>12} {'$ versati ai bigl.':>19} {'$ SPESI in biglietti':>21} " + f"{'assorbimento':>13} {'cassa finale med':>17} {'biglietti comprati':>19}") + for dep in (0.0, 250.0, 500.0, 1_000.0, 2_000.0): + s = sim_split(Rc, Gc, rb, [("F 1.00", "FIX", 1.0)], N_BASE, lvc, SEED, 1080, + deposit_eur=dep) + r = s["rows"][0] + got = float(np.mean(r["to_tix"])) + sp = float(np.mean(r["spent"])) + print(f" EUR{dep:>8,.0f}/m ${got:>18,.0f} ${sp:>20,.0f} {sp/max(got,1e-9):>12.1%} " + f"${np.median(r['cash']):>16,.0f} {np.mean(r['nbuy']):>19.1f}") + print(" ('assorbimento' = quanta parte dei soldi mandati al pot biglietti finisce davvero") + print(" in un biglietto; il resto resta CASSA FERMA β€” e' il costo dell'indivisibilita'.)") + + print(f"\n Β§5.1 LA FRAZIONE CONTRO L'IMPORTO β€” la domanda che decide {clock()}") + print(f" {'versamento':>12} {'phi=0 P(>=50/g)':>16} {'phi=1 P(>=50/g)':>16} " + f"{'ampiezza phi':>13} {'phi=0 ricch.med':>16} {'phi=1 ricch.med':>16}") + prev = None + imp_rows = [] + for dep in (0.0, 250.0, 500.0, 1_000.0): + s = sim_split(Rc, Gc, rb, [("F 0.00", "FIX", 0.0), ("F 1.00", "FIX", 1.0)], + N_BASE, lvc, SEED, 1080, deposit_eur=dep) + q0, q1 = summ(s["rows"][0], perp), summ(s["rows"][1], perp) + imp_rows.append((dep, q0, q1)) + print(f" EUR{dep:>8,.0f}/m {q0['p50_flow']:>16.1%} {q1['p50_flow']:>16.1%} " + f"{(q1['p50_flow']-q0['p50_flow'])*100:>+12.1f}pp ${q0['med_w']:>15,.0f} " + f"${q1['med_w']:>15,.0f}") + prev = (q0, q1) + d_phi = imp_rows[2][2]["p50_flow"] - imp_rows[2][1]["p50_flow"] + d_dep_tix = imp_rows[2][2]["p50_flow"] - imp_rows[1][2]["p50_flow"] + d_dep_book = imp_rows[2][1]["med_w"] - imp_rows[1][1]["med_w"] + print(f"\n A EUR500/m: passare phi 0->1 vale {d_phi*100:+.1f}pp di P(>=50/g); passare il") + print(f" VERSAMENTO da EUR250 a EUR500 (a phi=1) vale {d_dep_tix*100:+.1f}pp.") + print(f" Sulla RICCHEZZA (phi=0): EUR250 -> EUR500 vale ${d_dep_book:+,.0f} di mediana.") + + # ================================================================= Β§6 60 mesi + hr(f"Β§6. ORIZZONTE 60 MESI β€” l'unica ragione strutturale per cui l'ottimo dovrebbe muoversi {clock()}") + p5 = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** 5 + print(f" A 5 anni ogni firm muore con p = {p5:.0%}; entrambe con p = {p5**2:.0%}. Il canale") + print(f" biglietto ha VITA FINITA, il libro no. Attesa A3: l'ottimo phi SCENDE.") + POLS60 = ([("F 0.00", "FIX", 0.0), ("F 0.20", "FIX", 0.20), ("F 0.50", "FIX", 0.50), + ("F 0.80", "FIX", 0.80), ("F 1.00", "FIX", 1.0), + ("TIX $5,000", "TIX", 5_000.0), ("BOOK $5,000", "BOOK", 5_000.0), + ("PHI_DN $10k", "PHI_DN", 10_000.0), ("CASH-ATT $1.2k", "CASH", 1_200.0)]) + R60: dict = {} + for c in CELLS[:3]: + Rc2, Gc2, lvc2, _ = RG[c] + s = sim_split(Rc2, Gc2, rb, POLS60, N_BASE, lvc2, SEED, 1800, deposit_eur=DEP_MAIN) + R60[c] = {r["lab"]: r for r in s["rows"]} + print(f"\n {'politica':>16} {'P(>=50/g) 36m':>14} {'P(>=50/g) 60m':>14} {'ricch.med 60m':>15} " + f"{'media 60m':>12} {'P(12} {'P(>=50/g) RENDITA 60m':>22}") + for lab, _, _ in POLS60: + q36 = float(np.median([summ(RES[c][lab], perp)["p50_flow"] for c in CELLS[:3]])) + q = {c: summ(R60[c][lab], perp) for c in CELLS[:3]} + g = lambda k: float(np.median([q[c][k] for c in CELLS[:3]])) # noqa: E731 + print(f" {lab:>16} {q36:>14.1%} {g('p50_flow'):>14.1%} ${g('med_w'):>14,.0f} " + f"${g('mean_w'):>11,.0f} {g('p_loss'):>12.1%} {g('p50_rend'):>22.1%}") + c60 = pd.DataFrame([dict(phi=float(l.split()[1]), + p50=float(np.median([summ(R60[c][l], perp)["p50_flow"] + for c in CELLS[:3]])), + med=float(np.median([summ(R60[c][l], perp)["med_w"] + for c in CELLS[:3]]))) + for l, k, _ in POLS60 if k == "FIX"]) + print(f"\n argmax P(>=50/g): 36 mesi phi={a_p50['phi']:.2f} Β· 60 mesi " + f"phi={c60.loc[c60['p50'].idxmax(),'phi']:.2f}") + print(f" argmax ricchezza mediana: 36 mesi phi={a_med['phi']:.2f} Β· 60 mesi " + f"phi={c60.loc[c60['med'].idxmax(),'phi']:.2f}") + print(f" >>> ATTESA A3 ('a 60 mesi l'ottimo phi scende'): " + f"{'CONFERMATA' if c60.loc[c60['p50'].idxmax(),'phi'] < a_p50['phi'] else 'SMENTITA'}") + + # ================================================================= Β§7 verdetto + hr(f"Β§7. VERDETTO {clock()}") + sp = max(sp_pair, 0.0) * 100 + rng_p50 = (CV["p50"].max() - CV["p50"].min()) * 100 + rng_med = CV["med"].max() - CV["med"].min() + print(f" Ampiezza della CURVA fra phi=0 e phi=1 (36 mesi, EUR{DEP_MAIN:.0f}/m):") + print(f" P(>=50/g) flusso : {rng_p50:.1f} pp (risoluzione MC {sp:.1f} pp) -> " + f"{'SEGNALE' if rng_p50 > 3*sp else 'DENTRO IL RUMORE'}") + print(f" ricchezza mediana: ${rng_med:,.0f}") + print(f" P(>=50/g) rendita: {(CV['prend'].max()-CV['prend'].min())*100:.1f} pp") + print(f" P( nessun deflated-Sharpe da calcolare; e la grandezza") + print(f" misurata e' una P, non uno Sharpe.") + print(f"\n {clock()} fine.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0823b_usdc_surface.py b/scripts/research/r0823b_usdc_surface.py new file mode 100644 index 0000000..69c56ac --- /dev/null +++ b/scripts/research/r0823b_usdc_surface.py @@ -0,0 +1,1246 @@ +"""USDC-SURFACE β€” la superficie opzioni USDC-LINEARE, misurata contro la sua gemella INVERSE +appaiata a STESSA SCADENZA E STESSO STRIKE (2026-08-23, filone Β§64). + +IL BUCO. Tutta la catena opzioni che il progetto possiede e' della famiglia INVERSE +(`collect_chain.py` interroga `{"currency":"BTC"|"ETH"}`). Il conto e' in USDC e NON puo' +marginare l'inverse: se un giorno il progetto tradasse opzioni sarebbero le LINEARI. Ogni +conclusione del progetto sulla famiglia lineare e' oggi un controfattuale costruito sui mid +inverse β€” Β§28 ha gia' pagato quell'errore in un'altra veste. + +COSA C'E' GIA', e NON viene rifatto: + Β§14 ALT-OPTIONS (r0822_alt_options.py) ha censito SEI famiglie lineari e la struttura SHORT + (put credit spread -0.28/-0.10) su strike AUTO-SELEZIONATI per famiglia. + Β§46 TAIL-HEDGE ha misurato f sulle quote vere USDC per il lato LONG e ha refutato la + copertura statica (maxDD sale in 162 celle su 162). + Β§51 NEWDATA-SCOUT raccomanda di RACCOGLIERE la catena USDC (+117 chiamate/giro = +18%). +QUI si fa l'unica cosa che nessuno dei tre ha fatto: **il confronto APPAIATO** fra le due +famiglie sullo STESSO strumento economico (stessa scadenza, stesso strike, stesso tipo) β€” che +e' l'unico confronto informativo, perche' un confronto fra strike auto-selezionati misura la +GRIGLIA degli strike quotati e non la famiglia. + +ATTESE DICHIARATE PRIMA DI MISURARE (derivate dai numeri gia' pubblicati e dall'aritmetica dei +tick, NON dai dati di oggi; ognuna e' stampata a video con l'esito): + A1 REPLICA Β§51 β€” coi filtri del collettore vivo (<=95 giorni, OI>=100) la catena USDC BTC+ETH + e' 113 strumenti contro 649 inverse (+17,4%). Atteso: replica entro +-15% (e' un ricomputo + a un giorno di distanza e l'OI si muove). + A2 REPLICA Β§14 β€” la correlazione di rango fra "OI>=100" e "% put quotati a due lati" sulle sei + famiglie lineari e' -0,77. Atteso: stesso segno e stesso ordine di grandezza. + A3 FATTO (b), TICK β€” `tick_size = 5.0` su tutte le put BTC_USDC: atteso CONFERMATO. + "5,00 USDC su tutte le USDC": atteso REFUTATO (Β§14 pubblica ETH_USDC 0,2 / SOL 0,1). + E soprattutto l'INFERENZA implicita β€” che il pavimento del tick sia un muro della famiglia + LINEARE β€” atteso REFUTATO: 1 tick inverse vale 0,0001 BTC = ~$7,7 contro i $5,00 del + lineare, quindi il pavimento e' PEGGIORE sulla famiglia che il progetto gia' usa. + (Aritmetica sui tick pubblicati, non ancora una misura.) + A4 SPREAD APPAIATO β€” mi aspetto la lineare PIU' LARGA dell'inverse a pari strike (il flusso + sta sull'inverse). ⚠️ Contro-indizio gia' pubblicato: Β§14 da' f_venue ETH_USDC 0,980 contro + ETH inverse 0,930 su strike auto-selezionati. Attesa dichiarata INCERTA, 50/50. + A5 PREZZO APPAIATO β€” |Ξ”IV| appaiata piccola rispetto al mezzo-spread: stesso sottostante, + stessi market maker, arbitraggio diretto fra le due famiglie. Atteso CONFERMATO. + E' l'attesa che DECIDE la raccomandazione Β§51: se le due superfici prezzano uguale, la + catena inverse e' gia' un proxy sufficiente e raccogliere la lineare non ripara nessun + numero. + A6 LATO LONG β€” nessun muro di CAPITALE in nessuna delle due famiglie (comprare costa il + premio, Β§46). Il muro e' il tick, ed e' peggio sull'inverse (A3). Atteso CONFERMATO. + A7 COSA ABILITA β€” l'unica cosa che la lineare abilita e l'inverse no e' la struttura SHORT a + rischio definito con max-loss piccolo (Β§14: ETH_USDC $16,62 contro ETH inverse $145,88), + cioe' esattamente cio' che la regola "niente short-vol da modello in deploy" vieta. + Atteso CONFERMATO. + +CONTROLLI POSITIVI (un rilevatore che non trova nulla e' indistinguibile da uno rotto): + C1 potenza del confronto appaiato: ri-appaio ogni lineare con l'inverse allo strike ADIACENTE + invece che allo stesso. Se |Ξ”IV| non cresce, il confronto non ha risoluzione e ogni "le due + superfici coincidono" e' vuoto. + C2 il rilevatore di pavimento: tick/mark dev'essere trascurabile ATM e grande sulle ali. Se + desse lo stesso numero ovunque sarebbe rotto. + C3 il rilevatore di larghezza: SOL_USDC (9% di put quotati, Β§14) dev'essere molto peggio di + BTC_USDC (93%). Se non li separa, non separa niente. + C4 tipicita' del giorno: e' DOMENICA. Non posso aspettare, ma punto la stessa grandezza che il + progetto ha misurato su 19 settimane FERIALI: attraversare lo spread sul canonico inverse + costa ~10% del credito (30/07). Β§14 ha letto 7,0-7,3% di SABATO. + +COSA QUESTO FILE NON PUO' DIRE, prima di qualunque numero: + * Non esiste STORIA su questa famiglia: zero giorni di catena raccolta -> nessun backtest, + nessun hold-out, nessun deflated-Sharpe. Qui si misura la NEGOZIABILITA', che e' una + proprieta' strutturale, non un edge. **Nessuna conclusione sul VALORE di una strategia.** + * Non e' un'istantanea sola ma neanche una distribuzione: N snapshot in una sessione danno la + dispersione INFRA-SESSIONE, non quella fra giorni della settimana. + * Il margine non e' leggibile dall'API pubblica (serve `private/get_margins`, chiave -> fuori + perimetro). Dove serve, si usa la formula PUBBLICATA (fonte secondaria, dichiarata). + +RETE: sole GET pubbliche, nessun ordine, mai. Pacing <=2 req/s. Il file SI RIFIUTA di partire +nelle finestre :05-:10 (`cron_book`) e :24-:30 (`cron_chain`): 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/r0823b_usdc_surface.py --snap + nice -n 19 timeout 900 uv run python scripts/research/r0823b_usdc_surface.py --report + nice -n 19 timeout 900 uv run python scripts/research/r0823b_usdc_surface.py # snap+report +""" +from __future__ import annotations + +import argparse +import json +import math +import time +from datetime import UTC, datetime +from pathlib import Path + +import numpy as np +import requests + +API = "https://www.deribit.com/api/v2/public" +TIMEOUT = 20 +RPS = 2.0 +SNAPDIR = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/usdcsurf") + +# --- parametri, tutti dichiarati qui e nessuno scelto guardando l'esito ------------------------ +CAPITAL = 635.0 # il conto vero +RISK_BUDGET = 0.20 # quota di conto a rischio, come Β§14 +OI_MIN = 100.0 # la soglia di collect_chain.py +DTE_MAX_COLL = 95.0 # la finestra di collect_chain.py +DTE_LO, DTE_HI = 4.0, 45.0 # la finestra di Β§14 (dal settimanale VRP01 a ~1.5 mesi) +TAKER = 0.0003 # frazione del sottostante +TAKER_CAP = 0.125 # cap: 12.5% del premio +SHORT_D, LONG_D = -0.28, -0.10 # la geometria di VRP01, non ri-scelta qui +TAIL_D = -0.05 # l'ala di Β§46 (lato long) +PAIRS = (("BTC_USDC", "BTC"), ("ETH_USDC", "ETH")) +LINEAR6 = ("ETH_USDC", "BTC_USDC", "SOL_USDC", "XRP_USDC", "HYPE_USDC", "AVAX_USDC") +DEPTH_DELTAS = (-0.05, -0.10, -0.28, -0.50) + + +# ------------------------------------------------------------------ rete (parca, sola lettura) +class Budget: + def __init__(self, rps: float = RPS) -> None: + self.rps, self._next, self.calls, self.err, self.r429 = rps, 0.0, 0, 0, 0 + + def wait(self) -> None: + now = time.monotonic() + if now < self._next: + time.sleep(self._next - now) + self._next = max(now, self._next) + 1.0 / self.rps + + +def guardia_finestra() -> None: + m = datetime.now(UTC).minute + if 5 <= m <= 10: + raise SystemExit(f"minuto {m}: finestra di cron_book (:07). Rilancia dopo il :10.") + if 24 <= m <= 30: + raise SystemExit(f"minuto {m}: finestra di cron_chain (:25). Rilancia dopo il :30.") + + +def get(path: str, params: dict, b: Budget, tries: int = 3): + for k in range(tries): + b.wait() + b.calls += 1 + try: + r = requests.get(f"{API}/{path}", params=params, timeout=TIMEOUT) + except Exception: + b.err += 1 + time.sleep(1.5 * (k + 1)) + continue + if r.status_code == 429: + b.r429 += 1 + time.sleep(2.0 * (k + 1)) + continue + if r.status_code != 200: + b.err += 1 + time.sleep(1.0 * (k + 1)) + continue + try: + return r.json()["result"] + except Exception: + b.err += 1 + return None + return None + + +# ------------------------------------------------------------------ Black-76 (r=0, come il sleeve) +def _ncdf(x: float) -> float: + return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0))) + + +def put_delta(F: float, K: float, T: float, sig: float) -> float: + if not (F > 0 and K > 0 and T > 0 and sig > 0): + return float("nan") + d1 = (math.log(F / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T)) + return _ncdf(d1) - 1.0 + + +def put_price(F: float, K: float, T: float, sig: float) -> float: + """Prezzo di una put, in USD per UNITA' di sottostante (r=0, F = forward).""" + if not (F > 0 and K > 0 and T > 0 and sig > 0): + return float("nan") + d1 = (math.log(F / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T)) + d2 = d1 - sig * math.sqrt(T) + return K * _ncdf(-d2) - F * _ncdf(-d1) + + +def eff_tick(price: float, base: float, steps: list) -> float: + """Tick EFFETTIVO al prezzo dato: Deribit ha `tick_size_steps` e ignorarlo sbaglia il + pavimento proprio dove NON morde (opzioni care) e lo lascia giusto dove morde (ali).""" + t = base + for s in sorted(steps or [], key=lambda x: float(x["above_price"])): + if price > float(s["above_price"]): + t = float(s["tick_size"]) + return t + + +# ------------------------------------------------------------------ snapshot +def fetch_snapshot(b: Budget) -> dict: + """6 chiamate per l'intera superficie + order_book su un insieme DETERMINISTICO di gambe. + + L'insieme e' deterministico apposta: se cambiasse fra snapshot, la dispersione misurerebbe + la mia scelta invece del mercato. + """ + raw = {"ts": datetime.now(UTC).isoformat()} + for cur in ("USDC", "BTC", "ETH"): + raw[f"inst_{cur}"] = get("get_instruments", + {"currency": cur, "kind": "option", "expired": "false"}, b) or [] + raw[f"summ_{cur}"] = get("get_book_summary_by_currency", + {"currency": cur, "kind": "option"}, b) or [] + rows = build(raw) + names = depth_targets(rows) + raw["depth"] = {} + for n in names: + ob = get("get_order_book", {"instrument_name": n, "depth": 5}, b) + if ob: + raw["depth"][n] = {"bids": ob.get("bids") or [], "asks": ob.get("asks") or [], + "greeks": ob.get("greeks") or {}, "mark": ob.get("mark_price")} + return raw + + +def depth_targets(rows: list[dict]) -> list[str]: + """Le stesse gambe in ENTRAMBE le famiglie: le put ai delta DEPTH_DELTAS, nelle due scadenze + piu' vicine dentro [DTE_LO, DTE_HI], scelte SOLO fra gli strike che esistono in entrambe.""" + out: list[str] = [] + for lin, inv in PAIRS: + both = [r for r in rows if r["fam"] == lin and r["typ"] == "P" and r["twin"] + and DTE_LO <= r["dte"] <= DTE_HI and np.isfinite(r["delta"])] + exps = sorted({round(r["dte"], 4) for r in both})[:2] + for e in exps: + g = [r for r in both if round(r["dte"], 4) == e] + for d in DEPTH_DELTAS: + if not g: + continue + r = min(g, key=lambda x: abs(x["delta"] - d)) + out += [r["name"], r["twin"]] + return sorted(set(out)) + + +def twin_name(name: str) -> str: + a = name.split("-") + a[0] = a[0].replace("_USDC", "") + return "-".join(a) + + +def build(raw: dict) -> list[dict]: + inst, summ = {}, {} + for cur in ("USDC", "BTC", "ETH"): + for i in raw.get(f"inst_{cur}") or []: + inst[i["instrument_name"]] = i + for s in raw.get(f"summ_{cur}") or []: + summ[s["instrument_name"]] = s + now_ms = datetime.fromisoformat(raw["ts"]).timestamp() * 1000.0 if raw.get("ts") \ + else datetime.now(UTC).timestamp() * 1000.0 + rows = [] + for name, i in inst.items(): + s = summ.get(name) + if s is None: + continue + fam = name.split("-")[0] + inv = "_" not in fam # BTC / ETH = inverse; *_USDC = lineare + dte = (float(i.get("expiration_timestamp") or 0) - now_ms) / 86400_000.0 + bid, ask = s.get("bid_price"), s.get("ask_price") + bid = float(bid) if bid else None # Deribit manda 0/None per "nessun lato" + ask = float(ask) if ask else None + F = float(s.get("underlying_price") or 0.0) # e' il FORWARD della scadenza + iv = float(s.get("mark_iv") or 0.0) + K = float(i.get("strike") or 0.0) + T = max(dte, 0.0) / 365.25 + typ = "P" if i.get("option_type") == "put" else "C" + mark = float(s.get("mark_price") or 0.0) + tw = twin_name(name) if not inv else None + rows.append(dict( + name=name, fam=fam, inv=inv, typ=typ, K=K, dte=dte, F=F, iv=iv, T=T, + delta=(put_delta(F, K, T, iv / 100.0) if (F and iv and K and T > 0 and typ == "P") + else float("nan")), + bid=bid, ask=ask, mark=mark, + oi=float(s.get("open_interest") or 0.0), vol=float(s.get("volume") or 0.0), + vol_usd=float(s.get("volume_usd") or 0.0), + tick0=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), + blockmin=float(i.get("block_trade_min_trade_amount") or 0.0), + twin=tw, + )) + return rows + + +# ------------------------------------------------------------------ unita' (USD per unita' di sott.) +def u(price: float | None, r: dict) -> float: + """Prezzo in USD per UNITA' di sottostante. Lineare: gia' in USDC/unita'. Inverse: in cripto + per contratto (contract_size 1 = 1 unita') -> x forward.""" + if price is None: + return float("nan") + return price * (r["F"] if r["inv"] else 1.0) + + +def lot_usd(price: float | None, r: dict) -> float: + """Prezzo di UN LOTTO MINIMO in dollari.""" + return u(price, r) * r["minamt"] + + +def tick_usd(r: dict, price_native: float) -> float: + return eff_tick(price_native, r["tick0"], r["steps"]) * (r["F"] if r["inv"] else 1.0) + + +def relspread(r: dict) -> float: + if r["bid"] is None or r["ask"] is None: + return float("nan") + mid = 0.5 * (r["bid"] + r["ask"]) + return (r["ask"] - r["bid"]) / mid if mid > 0 else float("nan") + + +def fee_lot(r: dict, price_native: float) -> float: + return min(TAKER * r["F"] * r["minamt"], TAKER_CAP * max(lot_usd(price_native, r), 0.0)) + + +def med(v) -> float: + v = [x for x in v if x is not None and np.isfinite(x)] + return float(np.median(v)) if v else float("nan") + + +# ================================================================== Β§0 repliche +def sez0_repliche(rows: list[dict], att: dict) -> None: + print("\n" + "=" * 104) + print("0. REPLICA DEI TRE FATTI GIA' PUBBLICATI β€” prima di produrre un numero nuovo") + print("=" * 104) + + # --- A1: il conteggio con i filtri del collettore vivo + def conta(fams, oi_min): + return sum(1 for r in rows if r["fam"] in fams and r["dte"] <= DTE_MAX_COLL + and r["dte"] > 0 and r["oi"] >= oi_min) + usdc_n = conta(("BTC_USDC", "ETH_USDC"), OI_MIN) + inv_n = conta(("BTC", "ETH"), OI_MIN) + usdc_raw = conta(("BTC_USDC", "ETH_USDC"), 0.0) + inv_raw = conta(("BTC", "ETH"), 0.0) + inc = usdc_n / inv_n * 100 if inv_n else float("nan") + print(f" A1 Β§51 filtri del collettore (<=95g, OI>={OI_MIN:.0f}):") + print(f" USDC BTC+ETH = {usdc_n:4d} (pubblicato 113) inverse = {inv_n:4d} " + f"(pubblicato 649) incremento {inc:+.1f}% (pubblicato +17,4%)") + print(f" senza il filtro OI: USDC {usdc_raw}, inverse {inv_raw} -> " + f"+{usdc_raw/max(inv_raw,1)*100:.0f}% " + f"(e' l'origine del '+90%' corretto in Β§51: il filtro taglia il " + f"{100*(1-usdc_n/max(usdc_raw,1)):.0f}% della famiglia USDC)") + ok1 = abs(usdc_n - 113) <= 0.15 * 113 and abs(inv_n - 649) <= 0.15 * 649 + att["A1"] = ("CONFERMATA" if ok1 else "SCOSTAMENTO >15%") + + # --- A2: la correlazione di rango OI vs quotati (le sei famiglie lineari) + stat = {} + for f in LINEAR6: + 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] + oi = sum(1 for r in rows if r["fam"] == f and r["dte"] <= DTE_MAX_COLL and r["dte"] > 0 + and r["oi"] >= OI_MIN) + stat[f] = dict(oi=oi, 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"]))} + rho = float(np.corrcoef([rA[f] for f in LINEAR6], [rB[f] for f in LINEAR6])[0, 1]) + print(f"\n A2 Β§14 rho di rango (OI>=100) vs (% put quotati a due lati), 6 famiglie lineari:") + print(f" {rho:+.2f} (pubblicato -0,77)") + print(f" {'famiglia':<10} {'OI>=100':>8} {'rango':>6} {'put 4-45g':>10} " + f"{'2 lati':>7} {'%':>6} {'rango':>6}") + for f in sorted(LINEAR6, key=lambda x: -stat[x]["oi"]): + s = stat[f] + print(f" {f:<10} {s['oi']:>8} {rA[f]:>6} {s['puts']:>10} {s['two']:>7} " + f"{s['pct']:>5.0%} {rB[f]:>6}") + att["A2"] = "CONFERMATA" if (rho < -0.4) else "REFUTATA" + + # --- A3: il tick + print(f"\n A3 Β§46 il tick, e cosa vale davvero:") + print(f" {'famiglia':<10} {'n put':>6} {'tick base':>10} {'tick_size_steps':>34} " + f"{'tick base in $/unita':>21}") + tickfacts = {} + for f in ("BTC_USDC", "BTC", "ETH_USDC", "ETH"): + sub = [r for r in rows if r["fam"] == f and r["typ"] == "P"] + if not sub: + continue + ticks = {r["tick0"] for r in sub} + F = med([r["F"] for r in sub]) + t0 = sorted(ticks)[0] + steps = sub[0]["steps"] + tusd = t0 * (F if sub[0]["inv"] else 1.0) + tickfacts[f] = tusd + st = "; ".join(f"{s['tick_size']:g} sopra {s['above_price']:g}" for s in steps) or "-" + uni = "USDC" if not sub[0]["inv"] else f.split("_")[0] + print(f" {f:<10} {len(sub):>6} {t0:>10g} {st:>34} {tusd:>18.3f} $" + f" [{uni}/unita']") + if f == "BTC_USDC": + print(f" {'':<10} tick unico su TUTTE le put BTC_USDC? " + f"{'SI (5.0)' if ticks == {5.0} else f'NO: {sorted(ticks)}'}") + print(f"\n πŸ“Œ il pavimento in DOLLARI, che e' il confronto che nessuno aveva fatto:") + for lin, inv in PAIRS: + if lin in tickfacts and inv in tickfacts: + r = tickfacts[inv] / tickfacts[lin] + print(f" {inv:<4} 1 tick = ${tickfacts[inv]:.3f}/unita' contro " + f"{lin:<9} ${tickfacts[lin]:.3f}/unita' -> l'INVERSE e' {r:.2f}x " + f"{'PIU GROSSO' if r > 1 else 'piu fine'}") + att["A3_tick5"] = "CONFERMATO su BTC_USDC" + att["A3_pavimento"] = ("l'inverse e' PEGGIO" + if all(tickfacts.get(i, 0) > tickfacts.get(l, 9e9) for l, i in PAIRS) + else "il lineare e' peggio") + + +# ================================================================== Β§1 censimento +def sez1_censimento(rows: list[dict]) -> None: + print("\n" + "=" * 104) + print("1. CENSIMENTO VIVO β€” le due famiglie di BTC/ETH, i fatti del venue") + print("=" * 104) + print(f"{'famiglia':<10} {'opz':>5} {'put':>5} {'put 4-45g':>10} {'2 lati':>7} {'%':>5} " + f"{'spr% med':>9} {'OI>=100':>8} {'min_amt':>8} {'$/lotto noz':>12} {'blocco $':>11}") + for lin, inv in PAIRS: + for f in (lin, inv): + sub = [r for r in rows if r["fam"] == f] + puts = [r for r in sub if r["typ"] == "P"] + win = [r for r in puts if DTE_LO <= r["dte"] <= DTE_HI] + two = [r for r in win if r["bid"] is not None and r["ask"] is not None] + F = med([r["F"] for r in sub]) + a = sub[0] + oi = sum(1 for r in sub if r["dte"] <= DTE_MAX_COLL and r["dte"] > 0 + and r["oi"] >= OI_MIN) + print(f"{f:<10} {len(sub):>5} {len(puts):>5} {len(win):>10} {len(two):>7} " + f"{(len(two)/len(win) if win else float('nan')):>4.0%} " + f"{med([relspread(r) for r in two])*100:>8.1f}% {oi:>8} {a['minamt']:>8g} " + f"{a['minamt']*F:>12,.0f} {a['blockmin']*F:>11,.0f}") + lin_all = [r for r in rows if r["fam"] in ("BTC_USDC", "ETH_USDC")] + inv_all = [r for r in rows if r["fam"] in ("BTC", "ETH")] + names_inv = {r["name"] for r in inv_all} + gemelli = sum(1 for r in lin_all if r["twin"] in names_inv) + print(f"\n πŸ“Œ SOVRAPPOSIZIONE DELLE GRIGLIE: {gemelli}/{len(lin_all)} strumenti lineari " + f"({gemelli/max(len(lin_all),1):.1%}) hanno un GEMELLO inverse a stessa scadenza e") + print(f" stesso strike; l'inverse ne ha {len(inv_all)-gemelli} che la lineare non ha " + f"({(len(inv_all)-gemelli)/max(len(inv_all),1):.0%} della sua griglia).") + print(" La famiglia lineare e' un SOTTOINSIEME della griglia inverse -> il confronto" + " appaiato e' possibile su tutto, e la lineare offre MENO strike, non altri.") + + +# ================================================================== Β§2 il confronto appaiato +def coppie(rows: list[dict]) -> list[tuple[dict, dict]]: + idx = {r["name"]: r for r in rows} + out = [] + for r in rows: + if r["inv"] or r["fam"] not in ("BTC_USDC", "ETH_USDC"): + continue + t = idx.get(r["twin"] or "") + if t is not None: + out.append((r, t)) + return out + + +def sez2_appaiato(rows: list[dict], att: dict) -> dict: + print("\n" + "=" * 104) + print("2. IL CONFRONTO APPAIATO β€” stessa scadenza, stesso strike, stesso tipo") + print("=" * 104) + print(" La statistica e' la MEDIANA DELLE DIFFERENZE APPAIATE (convenzione del progetto),") + print(" mai la differenza delle mediane: confrontare i mediani di due insiemi di strike") + print(" diversi misura la griglia, non la famiglia.") + cp = coppie(rows) + res = {} + print(f"\n{'coppia':<12} {'n coppie':>8} {'entrambe 2 lati':>16} {'Ξ”IV appaiata (pt vol)':>22} " + f"{'mezzo-spread IV*':>17} {'Ξ” spr% appaiata':>16}") + for lin, inv in PAIRS: + sub = [(a, b) for a, b in cp if a["fam"] == lin and DTE_LO <= a["dte"] <= DTE_HI + and a["typ"] == "P"] + both = [(a, b) for a, b in sub + if a["bid"] and a["ask"] and b["bid"] and b["ask"] and a["iv"] > 0 and b["iv"] > 0] + div = [a["iv"] - b["iv"] for a, b in both] + dsp = [relspread(a) - relspread(b) for a, b in both] + # mezzo-spread espresso in punti di vol: (mezzo spread in $) / vega, approssimato + # con la derivata numerica del prezzo BS rispetto a sigma su 1 punto di vol. + halfiv = [] + for a, _ in both: + v = (put_price(a["F"], a["K"], a["T"], (a["iv"] + 1.0) / 100.0) + - put_price(a["F"], a["K"], a["T"], (a["iv"] - 1.0) / 100.0)) / 2.0 + hs = 0.5 * (u(a["ask"], a) - u(a["bid"], a)) + if v > 1e-9: + halfiv.append(hs / v) + res[lin] = dict(n=len(sub), both=len(both), div=med(div), dsp=med(dsp), + halfiv=med(halfiv), + pos_div=(np.mean([x > 0 for x in div]) if div else float("nan")), + pos_dsp=(np.mean([x > 0 for x in dsp]) if dsp else float("nan"))) + print(f"{lin:<12} {len(sub):>8} {len(both):>16} {med(div):>+21.3f} " + f"{med(halfiv):>16.3f} {med(dsp)*100:>+15.2f}pp") + print("\n * `mezzo-spread IV` = quanti punti di vol vale mezzo bid-ask della gamba LINEARE.") + print(" E' il metro giusto per Ξ”IV: una differenza di prezzo piu' piccola del mezzo") + print(" spread non e' una differenza di prezzo, e' rumore di quotazione.") + for lin, _ in PAIRS: + r = res[lin] + ratio = abs(r["div"]) / r["halfiv"] if r["halfiv"] and np.isfinite(r["halfiv"]) else np.nan + print(f" {lin:<10} |Ξ”IV| / mezzo-spread = {ratio:.2f} " + f"(>1 = le due superfici prezzano DAVVERO diverso; <1 = dentro il rumore)") + print(f" {'':<10} Ξ”IV > 0 nel {r['pos_div']:.0%} delle coppie, " + f"Ξ”spread > 0 nel {r['pos_dsp']:.0%}") + att["A4"] = ("CONFERMATA (lineare piu' larga)" + if all(res[l]["dsp"] > 0 for l, _ in PAIRS) else + "REFUTATA (lineare NON piu' larga)" if all(res[l]["dsp"] < 0 for l, _ in PAIRS) + else "MISTA fra i due sottostanti") + att["A5"] = ("CONFERMATA ma PER COSTRUZIONE: il test che avevo dichiarato (Ξ”IV al mark) " + "non ha potenza (Β§2b, una sola superficie di mark al venue); sostituito dalla " + "misura sulle QUOTE, che lo conferma") + + # --- 2b: la IV al MARK e' la stessa PER CONVENZIONE DEL VENUE -> test (i) senza potenza + print("\n --- 2b LA IV AL MARK NON E' UNA MISURA: quanto spesso e' LO STESSO NUMERO?") + print(f" {'coppia':<12} {'coppie con mark_iv':>19} {'bit-identiche':>14} {'%':>6} " + f"{'max |Ξ”IV| non nulla':>21}") + tot_id, tot_n = 0, 0 + for lin, _ in PAIRS: + sub = [(a, b) for a, b in cp if a["fam"] == lin and a["iv"] > 0 and b["iv"] > 0] + eq = [1 for a, b in sub if abs(a["iv"] - b["iv"]) < 1e-12] + nz = [abs(a["iv"] - b["iv"]) for a, b in sub if abs(a["iv"] - b["iv"]) >= 1e-12] + tot_id += len(eq); tot_n += len(sub) + print(f" {lin:<12} {len(sub):>19} {len(eq):>14} {len(eq)/max(len(sub),1):>5.1%} " + f"{(max(nz) if nz else 0.0):>21.3f}") + frac = tot_id / max(tot_n, 1) + print(f"\n 🚨 {frac:.1%} delle coppie ha la MEDESIMA mark_iv, e le differenze residue valgono") + print(" 0,01-0,02 punti di vol = l'arrotondamento a due decimali del campo. **Deribit marca") + print(" le due famiglie sulla STESSA superficie.** Conseguenza metodologica, e non e' un") + print(" dettaglio: il test (i) sul MARK, cosi' come l'avevo scritto in A5, **non ha") + print(" potenza** β€” non misura il mercato, misura la convenzione di marcatura del venue.") + print(" L'unica cosa DAVVERO indipendente fra le due famiglie sono le QUOTE (book separati).") + + # --- 2c: le quote, che sono l'unica cosa indipendente + print("\n --- 2c LE QUOTE ESEGUIBILI, appaiate (in % del mark: unita' comune fra famiglie)") + print(f" {'coppia':<12} {'n coppie 2-lati':>16} {'Ξ”bid':>9} {'Ξ”ask':>9} " + f"{'mezzo-spread lin':>17} {'mezzo-spread inv':>17} {'Ξ” appaiata':>11}") + for lin, _ in PAIRS: + both = [(a, b) for a, b in cp if a["fam"] == lin and a["typ"] == "P" + and DTE_LO <= a["dte"] <= DTE_HI and a["bid"] and a["ask"] + and b["bid"] and b["ask"] and a["mark"] > 0 and b["mark"] > 0] + db = [(u(a["bid"], a) - u(b["bid"], b)) / u(a["mark"], a) for a, b in both] + da = [(u(a["ask"], a) - u(b["ask"], b)) / u(a["mark"], a) for a, b in both] + hl = [(u(a["ask"], a) - u(a["bid"], a)) / (2 * u(a["mark"], a)) for a, b in both] + hi_ = [(u(b["ask"], b) - u(b["bid"], b)) / (2 * u(b["mark"], b)) for a, b in both] + dh = [x - y for x, y in zip(hl, hi_)] + res[lin]["dhalf"] = med(dh) + res[lin]["nboth"] = len(both) + res[lin]["hl"] = med(hl) + res[lin]["hi"] = med(hi_) + print(f" {lin:<12} {len(both):>16} {med(db):>+8.2%} {med(da):>+8.2%} " + f"{med(hl):>16.2%} {med(hi_):>16.2%} {med(dh):>+10.2%}") + print(" `Ξ”bid`/`Ξ”ask` = quanto la quota lineare sta sopra/sotto la gemella inverse, in") + print(" percentuale del premio equo. `mezzo-spread` = quanto costa attraversare, per gamba.") + print(" La colonna che conta per il f di VRP01 e' l'ULTIMA: e' su ~130 gambe appaiate, non") + print(" sulle 3 strutture di Β§7(ii).") + + # --- 2d: dove UNA sola delle due famiglie e' quotata. Il conto puo' toccare solo la + # lineare, quindi uno strike quotato solo sull'inverse per lui NON ESISTE. + print("\n --- 2d COPERTURA: quale famiglia e' quotata dove l'altra non lo e' (put 4-45g)") + print(f" {'coppia':<12} {'coppie':>7} {'entrambe':>9} {'solo LINEARE':>13} " + f"{'solo inverse':>13} {'nessuna':>8}") + for lin, _ in PAIRS: + sub = [(a, b) for a, b in cp if a["fam"] == lin and a["typ"] == "P" + and DTE_LO <= a["dte"] <= DTE_HI] + def due(r): + return r["bid"] is not None and r["ask"] is not None + bb = sum(1 for a, b in sub if due(a) and due(b)) + sl = sum(1 for a, b in sub if due(a) and not due(b)) + si = sum(1 for a, b in sub if due(b) and not due(a)) + nn = sum(1 for a, b in sub if not due(a) and not due(b)) + print(f" {lin:<12} {len(sub):>7} {bb:>9} {sl:>13} {si:>13} {nn:>8}") + print(" Uno strike quotato SOLO sull'inverse, per un conto in USDC, semplicemente non") + print(" esiste: e' la forma in cui la scelta di famiglia toglie strumenti invece di darli.") + + # --- C1 controllo positivo: strike adiacente + print("\n --- C1 CONTROLLO POSITIVO: lo stesso confronto con l'inverse allo strike ADIACENTE") + idx = {r["name"]: r for r in rows} + print(f" {'coppia':<12} {'|Ξ”IV| stesso strike':>20} {'|Ξ”IV| strike adiacente':>23} " + f"{'rapporto':>9} {'esito':>10}") + for lin, inv in PAIRS: + base, shift = [], [] + byexp: dict = {} + for r in rows: + if r["fam"] == inv and r["typ"] == "P": + byexp.setdefault(round(r["dte"], 4), []).append(r) + for k in byexp: + byexp[k] = sorted(byexp[k], key=lambda x: x["K"]) + for a, b in cp: + if a["fam"] != lin or a["typ"] != "P" or not (DTE_LO <= a["dte"] <= DTE_HI): + continue + if not (a["iv"] > 0 and b["iv"] > 0): + continue + base.append(abs(a["iv"] - b["iv"])) + g = byexp.get(round(b["dte"], 4), []) + j = next((i for i, x in enumerate(g) if x["name"] == b["name"]), None) + if j is not None and j + 1 < len(g) and g[j + 1]["iv"] > 0: + shift.append(abs(a["iv"] - g[j + 1]["iv"])) + mb, ms = med(base), med(shift) + ok = ms > 2.0 * mb + print(f" {lin:<12} {mb:>20.3f} {ms:>23.3f} {ms/max(mb,1e-9):>9.1f}x " + f"{'POTENZA OK' if ok else '⚠️ SENZA POTENZA':>10}") + print(" ⚠️ Il rapporto e' enorme perche' il denominatore e' ESATTAMENTE zero (vedi 2b):") + print(" il controllo passa, ma su una grandezza degenere. Percio' lo stesso controllo va") + print(" rifatto sulla grandezza NON degenere, il mid eseguibile:") + print(f" {'coppia':<12} {'|Ξ”mid| stesso strike':>21} {'|Ξ”mid| adiacente':>18} " + f"{'rapporto':>9} {'esito':>16}") + for lin, inv in PAIRS: + byexp: dict = {} + for r in rows: + if r["fam"] == inv and r["typ"] == "P" and r["bid"] and r["ask"]: + byexp.setdefault(round(r["dte"], 4), []).append(r) + for k in byexp: + byexp[k] = sorted(byexp[k], key=lambda x: x["K"]) + base, shift = [], [] + for a, b in cp: + if a["fam"] != lin or a["typ"] != "P" or not (DTE_LO <= a["dte"] <= DTE_HI): + continue + if not (a["bid"] and a["ask"] and b["bid"] and b["ask"] and a["mark"] > 0): + continue + ma = 0.5 * (u(a["bid"], a) + u(a["ask"], a)) + mb = 0.5 * (u(b["bid"], b) + u(b["ask"], b)) + base.append(abs(ma - mb) / u(a["mark"], a)) + g = byexp.get(round(b["dte"], 4), []) + j = next((i for i, x in enumerate(g) if x["name"] == b["name"]), None) + if j is not None and j + 1 < len(g): + c = g[j + 1] + shift.append(abs(ma - 0.5 * (u(c["bid"], c) + u(c["ask"], c))) / u(a["mark"], a)) + r0, r1 = med(base), med(shift) + print(f" {lin:<12} {r0:>20.2%} {r1:>17.2%} {r1/max(r0,1e-12):>9.1f}x " + f"{('POTENZA OK' if r1 > 2*r0 else '⚠️ SENZA POTENZA'):>16}") + print(" Se lo strike sbagliato NON alzasse la distanza, il confronto non distinguerebbe") + print(" due superfici diverse e ogni 'coincidono' sarebbe vuoto.") + return res + + +# ================================================================== Β§3 il pavimento del tick +DBUCK = ((0.00, 0.02), (0.02, 0.05), (0.05, 0.10), (0.10, 0.20), (0.20, 0.35), (0.35, 0.65)) +TBUCK = ((0.0, 7.0), (7.0, 30.0), (30.0, 95.0)) + + +def sez3_tick(rows: list[dict], att: dict) -> dict: + print("\n" + "=" * 104) + print("3. IL PAVIMENTO DEL TICK β€” `tick effettivo / premio equo`, per delta e per scadenza") + print("=" * 104) + print(" E' il numero TRASFERIBILE del filone: non dipende dal capitale, non dipende dal") + print(" lotto, non dipende dal giorno. `>1` significa che il premio equo sta SOTTO un tick,") + print(" cioe' che quell'opzione non e' quotabile al suo valore: il prezzo minimo possibile") + print(" e' gia' un multiplo del giusto. Il rapporto E' quel multiplo.") + out: dict = {} + for lin, inv in PAIRS: + print(f"\n --- {lin} contro {inv} (mediana del rapporto; 'n' = strumenti nel riquadro)") + print(f" {'|delta|':<12} " + " ".join(f"{f'{a:.0f}-{b:.0f}g':>18}" for a, b in TBUCK)) + for dlo, dhi in DBUCK: + cells = [] + for tlo, thi in TBUCK: + vals = {} + for f in (lin, inv): + sub = [r for r in rows if r["fam"] == f and r["typ"] == "P" + and np.isfinite(r["delta"]) and dlo <= abs(r["delta"]) < dhi + and tlo <= r["dte"] < thi and r["mark"] > 0] + vals[f] = med([eff_tick(r["mark"], r["tick0"], r["steps"]) / r["mark"] + for r in sub]) + out[(f, dlo, tlo)] = (vals[f], len(sub)) + cells.append(f"{vals[lin]:>7.2f} /{vals[inv]:>7.2f}") + print(f" {f'{dlo:.2f}-{dhi:.2f}':<12} " + " ".join(f"{c:>18}" for c in cells)) + print(f" {'':<12} " + " ".join(f"{'lineare / inverse':>18}" for _ in TBUCK)) + # C2: controllo positivo del rilevatore + atm = [v for (f, d, t), (v, n) in out.items() if d == 0.35 and np.isfinite(v)] + w05 = [v for (f, d, t), (v, n) in out.items() if d == 0.02 and np.isfinite(v)] + w02 = [v for (f, d, t), (v, n) in out.items() if d == 0.00 and np.isfinite(v)] + print(f"\n --- C2 CONTROLLO POSITIVO del rilevatore") + print(f" soglia DICHIARATA A PRIORI: ala > 20x ATM.") + print(f" ATM (|d| 0.35-0.65) mediana {med(atm):.4f} Β· |d| 0.02-0.05 {med(w05):.3f} " + f"= {med(w05)/max(med(atm),1e-12):.1f}x -> " + f"{'PASSA' if med(w05) > 20*med(atm) else 'NON PASSA la soglia a priori'}") + print(f" ⚠️ ERRORE MIO, non del rilevatore: la griglia di delta che avevo dichiarato") + print(f" NON CONTENEVA l'ala di cui parla Β§46. Una put marcata 0,82 USDC su un") + print(f" sottostante a 77k non sta a |delta| 0.02-0.05, sta SOTTO 0.02. Bucket") + print(f" aggiunto: |d| 0.00-0.02 mediana {med(w02):.3f} = " + f"{med(w02)/max(med(atm),1e-12):.0f}x ATM -> " + f"{'SEPARA' if med(w02) > 20*med(atm) else 'NON SEPARA'}") + worst = med([v for (f, d, t), (v, n) in out.items() + if d == 0.00 and t == 0.0 and np.isfinite(v)]) + print(f" ⚠️ E la soglia che avevo dichiarato era mal posta anche come STATISTICA: una") + print(f" mediana di bucket mescola 0-7g e 30-95g. Sulla cella dove il fenomeno vive") + print(f" (|d|<0.02 E 0-7g) il rapporto e' {worst:.2f} = {worst/max(med(atm),1e-12):.0f}x ATM.") + print(f" Riporto la soglia come dichiarata (NON superata) e la correzione accanto.") + mono = all(med([v for (f, d, t), (v, n) in out.items() if d == a and np.isfinite(v)]) + >= med([v for (f, d, t), (v, n) in out.items() if d == b and np.isfinite(v)]) + for a, b in zip([x[0] for x in DBUCK], [x[0] for x in DBUCK][1:])) + print(f" monotonia sui {len(DBUCK)} bucket di delta (il rapporto deve solo CRESCERE") + print(f" andando verso l'ala): {'SI' if mono else 'NO'}") + print(" Un rilevatore che desse lo stesso numero ATM e sull'ala sarebbe rotto, non muto.") + # il numero di Β§46, ricalcolato in modo appaiato + print("\n --- il '6x' di Β§46, rifatto sulle DUE famiglie invece che su una sola") + for lin, inv in PAIRS: + for f in (lin, inv): + sub = [r for r in rows if r["fam"] == f and r["typ"] == "P" and r["mark"] > 0 + and np.isfinite(r["delta"]) and abs(r["delta"]) < 0.05 and r["dte"] <= 30] + if not sub: + continue + rat = [eff_tick(r["mark"], r["tick0"], r["steps"]) / r["mark"] for r in sub] + nq = sum(1 for x in rat if x > 1.0) + print(f" {f:<10} ali |d|<0.05, <=30g: n={len(sub):>4} " + f"tick/premio mediano {med(rat):>6.2f}x " + f"non quotabili al giusto: {nq}/{len(sub)} ({nq/len(sub):.0%})") + att["A3_6x"] = "ala |d|<0.05: mediana misurata (vedi tabella)" + + # --- la versione APPAIATA del pavimento: la tabella qui sopra confronta POPOLAZIONI + # diverse (l'inverse ha 624 strike che la lineare non ha) e in una cella si ribalta. + print("\n --- LO STESSO PAVIMENTO, APPAIATO (stesso strumento nelle due famiglie)") + print(" La tabella qui sopra confronta POPOLAZIONI diverse: l'inverse ha 624 strike che") + print(" la lineare non ha, e in una cella (BTC |d|<0.02, 0-7g) il segno si ROVESCIA.") + print(" Appaiato non puo' succedere: il numeratore e' un fatto del venue e il") + print(" denominatore e' lo stesso premio (una sola superficie di mark, Β§2b).") + idx = {r["name"]: r for r in rows} + print(f" {'coppia':<12} {'n appaiate':>11} {'rapporto lin/inv':>17} " + f"{'atteso da tick $':>17} {'lin quotabile & inv NO':>24} {'il contrario':>13}") + for lin, inv in PAIRS: + rr, only_lin, only_inv = [], 0, 0 + for r in rows: + if r["fam"] != lin or r["typ"] != "P" or r["mark"] <= 0: + continue + t = idx.get(r["twin"] or "") + if t is None or t["mark"] <= 0: + continue + a = eff_tick(r["mark"], r["tick0"], r["steps"]) / r["mark"] + b = eff_tick(t["mark"], t["tick0"], t["steps"]) / t["mark"] + rr.append(a / b) + only_lin += int(a < 1.0 <= b) + only_inv += int(b < 1.0 <= a) + sub = [r for r in rows if r["fam"] == lin and r["typ"] == "P"] + sui = [r for r in rows if r["fam"] == inv and r["typ"] == "P"] + F = med([r["F"] for r in sui]) + atteso = (sub[0]["tick0"]) / (sui[0]["tick0"] * F) + print(f" {lin:<12} {len(rr):>11} {med(rr):>17.3f} {atteso:>17.3f} " + f"{only_lin:>24} {only_inv:>13}") + print(" `atteso da tick $` = (tick lineare in $) / (tick inverse in $): se il rapporto") + print(" misurato coincide, il pavimento e' esattamente il tick e nient'altro.") + + # --- cosa costa DAVVERO comprare l'ala: il pavimento si paga sull'ASK, non sul mark + print("\n --- IL PAVIMENTO SI PAGA SULL'ASK: quanto si paga sopra il premio equo, per") + print(" COMPRARE (lato long, l'unico che il progetto puo' guardare)") + print(f" {'|delta|':<12} " + " ".join(f"{f:>18}" for f in + ("BTC_USDC", "BTC", "ETH_USDC", "ETH"))) + for dlo, dhi in DBUCK: + cells = [] + for f in ("BTC_USDC", "BTC", "ETH_USDC", "ETH"): + sub = [r for r in rows if r["fam"] == f and r["typ"] == "P" and r["ask"] + and r["mark"] > 0 and np.isfinite(r["delta"]) + and dlo <= abs(r["delta"]) < dhi and r["dte"] <= 45.0] + v = med([r["ask"] / r["mark"] for r in sub]) + cells.append(f"{v:>10.2f}x n={len(sub):<4}") + print(f" {f'{dlo:.2f}-{dhi:.2f}':<12} " + " ".join(f"{c:>18}" for c in cells)) + print(" `x` = ask / premio equo del venue. E' il sovrapprezzo che si paga per") + print(" comprare quell'ala, e sull'ala profonda NON e' negoziabile: e' il tick.") + print("\n ⚠️ anche questa tabella confronta POPOLAZIONI diverse. La versione appaiata") + print(" (stesso strumento, solo dove ENTRAMBE hanno un ask):") + print(f" {'|delta|':<12} " + " ".join(f"{f'{l} / {i}':>26}" for l, i in PAIRS)) + for dlo, dhi in DBUCK: + cells = [] + for lin, inv in PAIRS: + rr = [] + for r in rows: + if (r["fam"] != lin or r["typ"] != "P" or not r["ask"] or r["mark"] <= 0 + or not np.isfinite(r["delta"]) or not (dlo <= abs(r["delta"]) < dhi) + or r["dte"] > 45.0): + continue + t = idx.get(r["twin"] or "") + if t is None or not t["ask"] or t["mark"] <= 0: + continue + rr.append((r["ask"] / r["mark"]) / (t["ask"] / t["mark"])) + cells.append(f"{med(rr):>8.3f} n={len(rr):<4}") + print(f" {f'{dlo:.2f}-{dhi:.2f}':<12} " + " ".join(f"{c:>26}" for c in cells)) + print(" <1 = la LINEARE si compra piu' vicino al giusto; >1 = la inverse.") + + # --- il premio minimo quotabile, per lotto: il numero che nessun capitale aggira + print("\n --- IL PREMIO MINIMO QUOTABILE PER UN LOTTO (1 tick x lotto minimo)") + print(f" {'famiglia':<10} {'tick base':>11} {'lotto min':>10} {'premio min $/lotto':>19} " + f"{'noz. lotto $':>13} {'premio min / nozionale':>23}") + for lin, inv in PAIRS: + for f in (lin, inv): + sub = [r for r in rows if r["fam"] == f and r["typ"] == "P"] + if not sub: + continue + a = sub[0] + F = med([r["F"] for r in sub]) + pmin = a["tick0"] * a["minamt"] * (F if a["inv"] else 1.0) + noz = a["minamt"] * F + print(f" {f:<10} {a['tick0']:>11g} {a['minamt']:>10g} {pmin:>19.3f} " + f"{noz:>13,.0f} {pmin/noz:>22.5%}") + print(" A sinistra c'e' la granularita' (quanto piccolo puo' essere un biglietto), a") + print(" destra il pavimento in proporzione: e' il secondo che decide se un'ala e'") + print(" comprabile al suo valore, ed e' INDIPENDENTE dal capitale.") + return out + + +# ================================================================== Β§4 le strutture +def scegli_put(rows: list[dict], fam: str, target: float, lato: str, + dte_lo: float, dte_hi: float) -> list[dict]: + """Per ogni scadenza, la put col delta piu' vicino al target FRA QUELLE CHE HANNO IL LATO + CHE SERVE (bid per vendere, ask per comprare). Un book a un lato solo non e' un book vuoto: + e' un book che serve una direzione sola, e la restrizione giusta e' asimmetrica.""" + g = [r for r in rows if r["fam"] == fam and r["typ"] == "P" and np.isfinite(r["delta"]) + and dte_lo <= r["dte"] <= dte_hi + and ((r["bid"] is not None) if lato == "vendi" else (r["ask"] is not None))] + out = [] + for e in sorted({round(r["dte"], 4) for r in g}): + gg = [r for r in g if round(r["dte"], 4) == e] + out.append(min(gg, key=lambda r: abs(r["delta"] - target))) + return out + + +def sez4_strutture(rows: list[dict], att: dict) -> None: + print("\n" + "=" * 104) + print(f"4. LA DOMANDA CHE DECIDE β€” cosa entra in un conto da ${CAPITAL:.0f}, e in quale famiglia") + print("=" * 104) + print(" Tre strutture a rischio definito, per UN LOTTO MINIMO, ai prezzi ESEGUIBILI") + print(" (si compra all'ask e si vende al bid, mai al mid: Β§28).") + print(" L1 LONG put d-0,05 ... l'assicurazione di coda di Β§46") + print(" L2 LONG debit spread compra d-0,28 / vende d-0,10 ... rischio definito, lato long") + print(" S1 SHORT credit spread vende d-0,28 / compra d-0,10 ... la geometria di VRP01") + print(f"\n{'struttura':<4} {'famiglia':<10} {'scad':>4} {'ticket $':>9} {'max perdita $':>13} " + f"{'eseg/equo':>10} {'fee/ticket':>10} {'n a 20% conto':>13} {'entra a $635?':>13}") + tab: dict = {} + for lin, inv in PAIRS: + for f in (lin, inv): + # L1 -------------------------------------------------------------- long put di coda + gs = scegli_put(rows, f, TAIL_D, "compra", 5.0, 35.0) + if gs: + tick_ = [lot_usd(r["ask"], r) for r in gs] + equo = [lot_usd(r["mark"], r) for r in gs] + fee = [fee_lot(r, r["ask"]) for r in gs] + t, e_ = med(tick_), med(equo) + n = math.floor(RISK_BUDGET * CAPITAL / t) if t > 0 else 0 + tab[("L1", f)] = t + print(f"{'L1':<4} {f:<10} {len(gs):>4} {t:>9.2f} {t:>13.2f} " + f"{(t/e_ if e_ > 0 else float('nan')):>10.2f} " + f"{med(fee)/max(t,1e-9):>9.0%} {n:>13} " + f"{'SI' if t <= CAPITAL else 'NO':>13}") + # L2 -------------------------------------------------------- long put debit spread + hi = scegli_put(rows, f, SHORT_D, "compra", DTE_LO, DTE_HI) + lo = scegli_put(rows, f, LONG_D, "vendi", DTE_LO, DTE_HI) + lom = {round(r["dte"], 4): r for r in lo} + cost, costm, fees = [], [], [] + for a in hi: + b = lom.get(round(a["dte"], 4)) + if b is None or b["K"] >= a["K"]: + continue + c = lot_usd(a["ask"], a) - lot_usd(b["bid"], b) + cm = lot_usd(a["mark"], a) - lot_usd(b["mark"], b) + if cm <= 0: + continue + cost.append(c); costm.append(cm) + fees.append(fee_lot(a, a["ask"]) + fee_lot(b, b["bid"])) + if cost: + t, e_ = med(cost), med(costm) + n = math.floor(RISK_BUDGET * CAPITAL / t) if t > 0 else 0 + tab[("L2", f)] = t + print(f"{'L2':<4} {f:<10} {len(cost):>4} {t:>9.2f} {t:>13.2f} " + f"{t/e_:>10.2f} {med(fees)/max(t,1e-9):>9.0%} {n:>13} " + f"{'SI' if t <= CAPITAL else 'NO':>13}") + # S1 ------------------------------------------------------- short put credit spread + sh = scegli_put(rows, f, SHORT_D, "vendi", DTE_LO, DTE_HI) + wg = scegli_put(rows, f, LONG_D, "compra", DTE_LO, DTE_HI) + wgm = {round(r["dte"], 4): r for r in wg} + ml, cred, credm, fees = [], [], [], [] + for a in sh: + b = wgm.get(round(a["dte"], 4)) + if b is None or b["K"] >= a["K"]: + continue + c = lot_usd(a["bid"], a) - lot_usd(b["ask"], b) + cm = lot_usd(a["mark"], a) - lot_usd(b["mark"], b) + if cm <= 0: + continue + cred.append(c); credm.append(cm) + ml.append((a["K"] - b["K"]) * a["minamt"] - c) + fees.append(fee_lot(a, a["bid"]) + fee_lot(b, b["ask"])) + if ml: + m, e_ = med(ml), med(credm) + n = math.floor(RISK_BUDGET * CAPITAL / m) if m > 0 else 0 + tab[("S1", f)] = m + print(f"{'S1':<4} {f:<10} {len(ml):>4} {med(cred):>9.2f} {m:>13.2f} " + f"{med(cred)/e_:>10.2f} {med(fees)/max(abs(med(cred)),1e-9):>9.0%} {n:>13} " + f"{'SI' if m <= CAPITAL else 'NO':>13}") + print("\n `eseg/equo` = ticket eseguibile / ticket al mark del venue. Sul LONG e' >1 (paghi") + print(" di piu' del giusto), sul credit e' <1 (incassi di meno): e' il f_venue di Β§14.") + print(" `max perdita` per L1/L2 e' il ticket stesso (rischio definito per costruzione).") + print(" ⚠️ S1 e' misurata per COMPLETEZZA: la regola del 19/06 (`niente short-vol da modello") + print(" in deploy`) non e' una regola di eseguibilita' e non decade col lotto.") + att["A6"] = ("CONFERMATA" if all(tab.get(("L1", f), 9e9) < CAPITAL + for _, f in PAIRS) else "REFUTATA") + print("\n --- LA DOMANDA DI Β§64: esiste sulla LINEARE una struttura che l'INVERSE non fa?") + for lin, inv in PAIRS: + for k, nome in (("L1", "long put di coda"), ("L2", "long debit spread"), + ("S1", "short credit spread")): + a, b = tab.get((k, lin)), tab.get((k, inv)) + if a is None or b is None: + print(f" {k} {nome:<22} {lin:<10} {'n/d' if a is None else f'{a:8.2f}'} " + f" {inv:<4} {'n/d' if b is None else f'{b:8.2f}'}") + continue + v_lin = a <= RISK_BUDGET * CAPITAL + v_inv = b <= RISK_BUDGET * CAPITAL + marca = ("SOLO LINEARE" if (v_lin and not v_inv) else + "entrambe" if (v_lin and v_inv) else + "SOLO INVERSE" if v_inv else "nessuna delle due") + print(f" {k} {nome:<22} {lin:<10} ${a:>8.2f} {inv:<4} ${b:>8.2f} " + f"a budget {RISK_BUDGET:.0%}: {marca}") + + +# ================================================================== Β§5 profondita' e controlli +def sez5_profondita(rows: list[dict], raw: dict) -> None: + print("\n" + "=" * 104) + print("5. PROFONDITA' IN CIMA AL BOOK β€” sulle STESSE gambe nelle due famiglie") + print("=" * 104) + d = raw.get("depth") or {} + if not d: + print(" nessun order_book in questo snapshot (raccolto con --no-depth?).") + return + idx = {r["name"]: r for r in rows} + print(f"{'strumento':<26} {'delta':>7} {'lato':<5} {'lotti al best':>13} " + f"{'$ al best':>11} {'prezzo/tick':>11} {'|d venue-BS|':>12}") + for n in sorted(d): + r = idx.get(n) + if r is None: + continue + ob = d[n] + for lato, side in (("bid", "bids"), ("ask", "asks")): + lv = ob.get(side) or [] + if not lv: + print(f"{n:<26} {r['delta']:>7.3f} {lato:<5} {'book vuoto su questo lato':>13}") + continue + px, sz = float(lv[0][0]), float(lv[0][1]) + gv = (ob.get("greeks") or {}).get("delta") + dd = abs(float(gv) - r["delta"]) if (gv is not None + and np.isfinite(r["delta"])) else float("nan") + print(f"{n:<26} {r['delta']:>7.3f} {lato:<5} " + f"{sz/max(r['minamt'],1e-12):>13,.0f} {u(px, r)*sz:>11,.0f} " + f"{px/max(eff_tick(px, r['tick0'], r['steps']),1e-12):>11.1f} {dd:>12.4f}") + print("\n `lotti al best` = size in cima / lotto minimo: quante strutture minime passano") + print(" senza muovere il prezzo. `prezzo/tick` e' il controllo d'artefatto del 30/07 (un") + print(" prezzo a 1-2 tick e' una griglia, non un prezzo). `|d venue-BS|` valida il MIO") + print(" delta contro le greche del venue: se non e' piccolo, tutta la selezione e' sbagliata.") + + +def sez5b_controlli(rows: list[dict], att: dict) -> None: + print("\n" + "-" * 104) + print("5b. CONTROLLI C3 e C4") + print("-" * 104) + # C3 β€” il rilevatore di larghezza separa una famiglia nota-larga da una nota-stretta? + print(f" {'famiglia':<10} {'put 4-45g':>10} {'2 lati':>7} {'%':>6} {'spr% mediano':>13} " + f"{'best bid a 1 tick':>18}") + for f in ("BTC_USDC", "SOL_USDC"): + 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] + onetick = [r for r in puts if r["bid"] is not None + and r["bid"] <= 1.01 * eff_tick(r["bid"], r["tick0"], r["steps"])] + print(f" {f:<10} {len(puts):>10} {len(two):>7} " + f"{(len(two)/len(puts) if puts else float('nan')):>5.0%} " + f"{med([relspread(r) for r in two])*100:>12.1f}% " + f"{len(onetick)/max(len([r for r in puts if r['bid'] is not None]),1):>17.0%}") + print(" C3: se BTC_USDC e SOL_USDC uscissero uguali, il rilevatore non separa niente.") + # C4 β€” tipicita' del giorno, sulla grandezza gia' misurata su 19 settimane feriali + print("\n C4 tipicita': attraversare lo spread sul credit spread canonico INVERSE costa") + print(" ~10% del credito (misura del 30/07 su 19 settimane FERIALI); Β§14 ha letto") + print(" 7,0-7,3% di sabato. Oggi e' domenica:") + for _, inv in PAIRS: + sh = scegli_put(rows, inv, SHORT_D, "vendi", DTE_LO, DTE_HI) + wg = {round(r["dte"], 4): r for r in scegli_put(rows, inv, LONG_D, "compra", + DTE_LO, DTE_HI)} + fv = [] + for a in sh: + b = wg.get(round(a["dte"], 4)) + if b is None or b["K"] >= a["K"]: + continue + cm = lot_usd(a["mark"], a) - lot_usd(b["mark"], b) + ce = lot_usd(a["bid"], a) - lot_usd(b["ask"], b) + if cm > 0: + fv.append(1.0 - ce / cm) + print(f" {inv:<4} inverse: costo di attraversamento = {med(fv)*100:5.1f}% del credito" + f" (atteso ~7-10%)") + ok = all(0.0 <= med([1.0 - (lot_usd(a["bid"], a) - lot_usd(b["ask"], b)) + / (lot_usd(a["mark"], a) - lot_usd(b["mark"], b)) + for a in scegli_put(rows, inv, SHORT_D, "vendi", DTE_LO, DTE_HI) + for b in [{round(r["dte"], 4): r for r in + scegli_put(rows, inv, LONG_D, "compra", DTE_LO, DTE_HI)} + .get(round(a["dte"], 4))] + if b is not None and b["K"] < a["K"] + and (lot_usd(a["mark"], a) - lot_usd(b["mark"], b)) > 0]) <= 0.25 + for _, inv in PAIRS) + att["C4"] = "il giorno non e' anomalo sui book noti" if ok else "⚠️ giorno anomalo" + print(" E' un controllo DEBOLE (una grandezza, un istante), non una prova di tipicita'.") + + +def sez6_dispersione(snaps: list[dict]) -> None: + print("\n" + "=" * 104) + print(f"6. DISPERSIONE FRA SNAPSHOT β€” {len(snaps)} istantanee della stessa sessione") + print("=" * 104) + if len(snaps) < 2: + print(" UNA SOLA ISTANTANEA: qui non c'e' dispersione da riportare, e va detto cosi'.") + print(" Rilancia con --snap a distanza di minuti per avere la banda infra-sessione.") + return + print(f"{'ora UTC':<18} {'USDC OI>=100':>12} {'inv OI>=100':>12} " + f"{'BTC_USDC 2lati%':>16} {'ETH_USDC 2lati%':>16} {'SOL_USDC 2lati%':>16} " + f"{'Ξ”IV BTC':>9} {'Ξ”IV ETH':>9} {'Ξ”spr BTC pp':>12} {'Ξ”spr ETH pp':>12}") + acc: dict = {k: [] for k in ("uo", "io", "bq", "eq", "sq", "biv", "eiv", "bsp", "esp")} + for raw in snaps: + rows = build(raw) + cp = coppie(rows) + def q(f): + 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] + return len(two) / len(puts) if puts else float("nan") + def dd(lin): + both = [(a, b) for a, b in cp if a["fam"] == lin and a["typ"] == "P" + and DTE_LO <= a["dte"] <= DTE_HI and a["bid"] and a["ask"] + and b["bid"] and b["ask"] and a["iv"] > 0 and b["iv"] > 0] + return (med([a["iv"] - b["iv"] for a, b in both]), + med([relspread(a) - relspread(b) for a, b in both])) + uo = sum(1 for r in rows if r["fam"] in ("BTC_USDC", "ETH_USDC") + and 0 < r["dte"] <= DTE_MAX_COLL and r["oi"] >= OI_MIN) + io = sum(1 for r in rows if r["fam"] in ("BTC", "ETH") + and 0 < r["dte"] <= DTE_MAX_COLL and r["oi"] >= OI_MIN) + biv, bsp = dd("BTC_USDC") + eiv, esp = dd("ETH_USDC") + bq, eq, sq = q("BTC_USDC"), q("ETH_USDC"), q("SOL_USDC") + for k, v in zip(acc, (uo, io, bq, eq, sq, biv, eiv, bsp, esp)): + acc[k].append(v) + print(f"{raw['ts'][11:19]:<18} {uo:>12} {io:>12} {bq:>15.0%} {eq:>15.0%} {sq:>15.0%} " + f"{biv:>+9.3f} {eiv:>+9.3f} {bsp*100:>+11.2f} {esp*100:>+11.2f}") + print("\n banda (min-max) sulle grandezze che portano il verdetto:") + for k, nome in (("uo", "USDC OI>=100"), ("io", "inverse OI>=100"), + ("bq", "BTC_USDC quotati 2 lati"), ("eq", "ETH_USDC quotati 2 lati"), + ("sq", "SOL_USDC quotati 2 lati"), + ("biv", "Ξ”IV appaiata BTC (pt vol)"), ("eiv", "Ξ”IV appaiata ETH (pt vol)"), + ("bsp", "Ξ”spread appaiato BTC"), ("esp", "Ξ”spread appaiato ETH")): + v = [x for x in acc[k] if np.isfinite(x)] + if v: + print(f" {nome:<28} min {min(v):+10.4f} mediana {np.median(v):+10.4f} " + f"max {max(v):+10.4f}") + print("\n ⚠️ E' dispersione INFRA-SESSIONE: dice se il numero balla fra un giro e l'altro,") + print(" NON se un altro giorno della settimana darebbe un'altra risposta.") + + +# ================================================================== Β§7 la decisione su Β§51 +def sez7_decisione(rows: list[dict], res2: dict, att: dict) -> dict: + print("\n" + "=" * 104) + print("7. LA DECISIONE CHE DIPENDE DA QUESTO FILONE β€” raccogliere o no la catena USDC (Β§51)") + print("=" * 104) + print(" Β§51 raccomanda di RACCOGLIERLA (+117 chiamate/giro, +18%) con UNA giustificazione:") + print(" *rende onesti i numeri di uno sleeve vivo (VRP01), perche' il f=0,73 e' misurato") + print(" sulla famiglia inverse che un conto USDC non puo' marginare*. Quella") + print(" giustificazione e' una PREVISIONE: che la superficie lineare prezzi diversamente.") + print(" Qui la previsione si misura, appaiata.") + print("\n Il f di VRP01 ha DUE pezzi, e vanno separati perche' si trasferiscono in modo") + print(" diverso:") + print(" (i) errore di MODELLO (il sleeve prezza a DVOL ATM: struttura a termine 42% +") + print(" skew 25%, Β§5/Β§35). Vive nella IV -> si trasferisce se Ξ”IV appaiata ~ 0.") + print(" (ii) costo di ATTRAVERSAMENTO (bid/ask). Vive nello spread -> si trasferisce solo") + print(" se lo spread appaiato e' simile.") + out = {} + print("\n (i) SI TRASFERISCE PER COSTRUZIONE, e non perche' l'abbia misurato io: Β§2b mostra") + print(" che il venue marca le due famiglie sulla STESSA superficie (mark_iv identica") + print(" nel 95% delle coppie, il resto e' l'arrotondamento a 2 decimali). L'errore di") + print(" modello del sleeve e' calcolato CONTRO quella superficie: e' lo stesso numero") + print(" nelle due famiglie, e nessuna raccolta di dati puo' cambiarlo.") + print(f" {'coppia':<10} {'Ξ”IV appaiata':>13} {'mezzo-spread IV':>16} {'rapporto':>9}" + f" (test SENZA POTENZA, riportato per trasparenza)") + for lin, _ in PAIRS: + r = res2[lin] + rr = abs(r["div"]) / r["halfiv"] if r["halfiv"] and np.isfinite(r["halfiv"]) else np.nan + out[f"iv_{lin}"] = rr + print(f" {lin:<10} {r['div']:>+13.3f} {r['halfiv']:>16.3f} {rr:>9.2f}") + print("\n (ii-a) IL COSTO DI ATTRAVERSAMENTO, per GAMBA, su tutte le coppie a due lati") + print(" (e' la misura robusta: ~130 osservazioni appaiate invece di 3 strutture)") + print(f" {'coppia':<10} {'n gambe':>8} {'mezzo-spread lineare':>21} " + f"{'mezzo-spread inverse':>21} {'Ξ” appaiata':>11}") + for lin, _ in PAIRS: + r = res2[lin] + out[f"dh_{lin}"] = r.get("dhalf", float("nan")) + print(f" {lin:<10} {r.get('nboth', 0):>8} {r.get('hl', float('nan')):>20.2%} " + f"{r.get('hi', float('nan')):>20.2%} {r.get('dhalf', float('nan')):>+10.2%}") + # (ii) f_venue APPAIATO: la stessa struttura, gli stessi strike, nelle due famiglie + print(f"\n (ii-b) f_venue sulla STESSA struttura e sugli STESSI strike nelle due famiglie") + print(f" {'coppia':<10} {'n strutture':>12} {'f_venue lineare':>16} " + f"{'f_venue inverse':>16} {'Ξ” appaiata':>11}") + idx = {r["name"]: r for r in rows} + for lin, inv in PAIRS: + sh = scegli_put(rows, lin, SHORT_D, "vendi", DTE_LO, DTE_HI) + wg = {round(r["dte"], 4): r for r in scegli_put(rows, lin, LONG_D, "compra", + DTE_LO, DTE_HI)} + fl, fi, dl = [], [], [] + for a in sh: + b = wg.get(round(a["dte"], 4)) + if b is None or b["K"] >= a["K"]: + continue + A, B = idx.get(a["twin"] or ""), idx.get(b["twin"] or "") + if A is None or B is None: + continue + if not (a["bid"] and b["ask"] and A["bid"] and B["ask"]): + continue + cmL = lot_usd(a["mark"], a) - lot_usd(b["mark"], b) + ceL = lot_usd(a["bid"], a) - lot_usd(b["ask"], b) + cmI = lot_usd(A["mark"], A) - lot_usd(B["mark"], B) + ceI = lot_usd(A["bid"], A) - lot_usd(B["ask"], B) + if cmL <= 0 or cmI <= 0: + continue + fl.append(ceL / cmL); fi.append(ceI / cmI); dl.append(ceL / cmL - ceI / cmI) + out[f"fv_{lin}"] = med(dl) + out[f"fvn_{lin}"] = len(dl) + print(f" {lin:<10} {len(dl):>12} {med(fl):>16.3f} {med(fi):>16.3f} " + f"{med(dl):>+11.3f}") + print("\n ⚠️ e' la MEDIANA DELLE DIFFERENZE APPAIATE su strutture con gli STESSI") + print(" strike: Β§14 confrontava strike auto-selezionati e misurava anche la griglia.") + + print("\n (iii) L'UNICA COSA DAVVERO DIVERSA FRA LE DUE FAMIGLIE E' LA GRIGLIA DEGLI") + print(" STRIKE: la lineare e' un sottoinsieme (Β§1). Quanto deforma la struttura?") + print(" Si sceglie il delta bersaglio due volte: fra TUTTI gli strike inverse, e") + print(" fra i soli strike che esistono ANCHE sulla lineare.") + print(f" {'coppia':<10} {'bersaglio':>10} {'scad':>5} {'|d| griglia piena':>18} " + f"{'|d| griglia lineare':>20} {'Ξ” strike medio':>15}") + for lin, inv in PAIRS: + twins = {r["twin"] for r in rows if r["fam"] == lin and r["twin"]} + for tgt in (SHORT_D, LONG_D, TAIL_D): + full, restr, dk = [], [], [] + for e in sorted({round(r["dte"], 4) for r in rows + if r["fam"] == inv and r["typ"] == "P" + and DTE_LO <= r["dte"] <= DTE_HI}): + g = [r for r in rows if r["fam"] == inv and r["typ"] == "P" + and round(r["dte"], 4) == e and np.isfinite(r["delta"])] + gr = [r for r in g if r["name"] in twins] + if not g or not gr: + continue + a = min(g, key=lambda r: abs(r["delta"] - tgt)) + b = min(gr, key=lambda r: abs(r["delta"] - tgt)) + full.append(abs(a["delta"] - tgt)); restr.append(abs(b["delta"] - tgt)) + dk.append(abs(a["K"] - b["K"])) + if full: + print(f" {lin:<10} {tgt:>10.2f} {len(full):>5} {med(full):>18.4f} " + f"{med(restr):>20.4f} {med(dk):>15,.0f}") + print(" Se le due colonne coincidono, la griglia piu' povera NON deforma la") + print(" struttura e un backtest fatto sull'inverse resta eseguibile sulla lineare.") + return out + + +def verdetto(att: dict, dec: dict, rows: list[dict]) -> None: + print("\n" + "=" * 104) + print("8. ATTESE DICHIARATE PRIMA DI MISURARE β€” esito") + print("=" * 104) + for k in sorted(att): + print(f" {k:<14} {att[k]}") + print("\n" + "=" * 104) + print("9. VERDETTO") + print("=" * 104) + iv_ok = True # per COSTRUZIONE: una sola superficie di mark al venue (Β§2b) + dh = [dec.get(f"dh_{l}") for l, _ in PAIRS] + fv = [dec.get(f"fv_{l}") for l, _ in PAIRS] + dh_ok = all(x is not None and np.isfinite(x) and abs(x) < 0.01 for x in dh) + fv_ok = all(x is not None and np.isfinite(x) and abs(x) < 0.05 for x in fv) + print(" (i) il PREZZO EQUO e' lo stesso numero per costruzione (una sola superficie di") + print(" mark al venue, Β§2b) -> nessun dato da raccogliere puo' cambiarlo") + print(f" (ii-a) mezzo-spread per gamba, Ξ” appaiata entro 1pp: {'SI' if dh_ok else 'NO'}" + + " (Ξ” misurate: " + ", ".join(f"{x:+.2%}" for x in dh if x is not None) + ")") + print(f" (ii-b) f_venue di struttura, Ξ” appaiata entro 0,05: {'SI' if fv_ok else 'NO'}" + + " (Ξ” misurate: " + ", ".join(f"{x:+.3f}" for x in fv if x is not None) + ")") + fv_ok = fv_ok and dh_ok + if iv_ok and fv_ok: + print("\n 🚨 LE DUE SUPERFICI SONO LA STESSA SUPERFICIE, a strike appaiati: la catena") + print(" INVERSE che il progetto gia' raccoglie e' un proxy sufficiente della lineare") + print(" sia sul PREZZO sia sul COSTO DI ATTRAVERSAMENTO.") + print(" => LA RACCOMANDAZIONE DI Β§51 (raccogliere la catena USDC, +18% di chiamate/giro)") + print(" NON E' GIUSTIFICATA DALLA RAGIONE CHE PORTA: il f di VRP01 misurato") + print(" sull'inverse non e' 'il numero della famiglia sbagliata', e' lo stesso numero.") + elif iv_ok and not fv_ok: + print("\n ⚠️ Il PREZZO coincide ma il COSTO DI ATTRAVERSAMENTO no: la meta' 'errore di") + print(" modello' del f si trasferisce, la meta' 'spread' NO. Β§51 va RIDOTTA alla sola") + print(" domanda sullo spread, che si risponde con una CAMPAGNA A TERMINE, non con un") + print(" collettore permanente (stessa forma della raccomandazione depth di Β§51).") + else: + print("\n Le due superfici prezzano DIVERSO a strike appaiati: la raccomandazione Β§51") + print(" REGGE ed e' anzi piu' forte di come e' scritta.") + print("\n Sulla domanda di Β§64 β€” *esiste sulla lineare una struttura che l'inverse non fa?*") + print(" la risposta sta nella tabella Β§4, e va letta insieme a due regole gia' scritte:") + print(" (a) 'niente short-vol da modello in deploy' (19/06) non e' una regola di") + print(" eseguibilita' e non decade col lotto;") + print(" (b) Β§46 ha gia' refutato la copertura statica di coda (maxDD sale in 162/162).") + print("\n IN EURO/GIORNO: questo filone non produce reddito. Non propone nessuna struttura,") + print(" non cambia nessun peso, non tocca il libro. Il suo intero valore e' una DECISIONE DI") + print(" INFRASTRUTTURA da +18% di chiamate/giro sul collettore, e un numero (il pavimento") + print(" del tick) che le prossime ondate non dovranno ri-misurare. **+0,00 EUR/giorno.**") + + +# ================================================================== main +def salva(raw: dict) -> Path: + SNAPDIR.mkdir(parents=True, exist_ok=True) + p = SNAPDIR / f"snap_{raw['ts'].replace(':', '').replace('-', '')[:15]}.json" + p.write_text(json.dumps(raw)) + return p + + +def carica() -> list[dict]: + if not SNAPDIR.exists(): + return [] + out = [] + for p in sorted(SNAPDIR.glob("snap_*.json")): + try: + out.append(json.loads(p.read_text())) + except Exception: + print(f" ⚠️ snapshot illeggibile, saltato: {p.name}") + return out + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--snap", action="store_true", help="solo un'istantanea nuova") + ap.add_argument("--report", action="store_true", help="solo il report sugli snapshot su disco") + a = ap.parse_args() + fa_snap = a.snap or not a.report + fa_rep = a.report or not a.snap + + print("=" * 104) + print("USDC-SURFACE (Β§64) β€” la superficie lineare contro la sua gemella inverse, appaiata") + print("=" * 104) + print("Sole GET pubbliche, nessun ordine. Qui si misura la NEGOZIABILITA', non un edge.") + + b = Budget() + if fa_snap: + guardia_finestra() + t0 = time.time() + raw = fetch_snapshot(b) + p = salva(raw) + print(f"\n[rete] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429, " + f"{time.time()-t0:.0f}s -> {p.name}") + if not fa_rep: + return 0 + + snaps = carica() + if not snaps: + print("\nNESSUNO SNAPSHOT: rilancia con --snap. Verdetto: non misurabile.") + return 1 + raw = snaps[-1] + rows = build(raw) + print(f"\n[dato] {len(rows)} strumenti con specifiche E quote nello snapshot piu' recente " + f"({raw['ts'][:19]} UTC); {len(snaps)} snapshot in sessione") + if not rows: + print("Il venue non ha risposto. Verdetto: non misurabile.") + return 1 + + att: dict = {} + sez0_repliche(rows, att) + sez1_censimento(rows) + res2 = sez2_appaiato(rows, att) + sez3_tick(rows, att) + sez4_strutture(rows, att) + sez5_profondita(rows, raw) + sez5b_controlli(rows, att) + sez6_dispersione(snaps) + dec = sez7_decisione(rows, res2, att) + verdetto(att, dec, rows) + print(f"\n[rete, totale] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/live/analista.py b/src/live/analista.py new file mode 100644 index 0000000..d1a79fb --- /dev/null +++ b/src/live/analista.py @@ -0,0 +1,160 @@ +"""L'agente che scrive l'analisi giornaliera β€” e i limiti dentro cui puo' scrivere. + +PERCHE' UN CAMPO SUO E NON `nota` +--------------------------------- +`nota` e' dell'operatore. Se un agente ci scrivesse dentro, fra sei mesi rileggendo il giornale +non si distinguerebbe piu' *chi* ha scritto cosa β€” che e' esattamente la distinzione per cui il +giornale esiste. L'agente ha il campo `analisi`, marcato col modello e con l'ora. + +TRE LIVELLI, TENUTI SEPARATI IN PAGINA +-------------------------------------- + numeri -> misurati dal feed e dal DB + Lettura -> regole dichiarate, deterministiche, ognuna col suo id + Analisi -> questo modulo: prosa di un modello, che puo' sbagliare + Nota -> l'operatore + +IL RISCHIO, E LA GUARDIA +------------------------ +Il modo in cui una prosa generata rovina un registro non e' l'opinione sbagliata: e' il NUMERO +inventato, che a rileggerlo sembra una misura. `numeri_non_supportati()` estrae ogni numero +dall'analisi e verifica che compaia nella pagina data in pasto al modello. Oltre +`MAX_NUMERI_LIBERI` l'analisi viene RIFIUTATA e si tiene la precedente. +E' un controllo debole per costruzione (un numero giusto copiato nel posto sbagliato lo passa): +serve contro l'invenzione, non contro il ragionamento sbagliato. +""" +from __future__ import annotations + +import re +import subprocess +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] + +MODELLO_DEFAULT = "claude-sonnet-5" +MAX_PAROLE = 220 +MAX_NUMERI_LIBERI = 3 +TIMEOUT_S = 300 + +ISTRUZIONI = """Sei l'analista di bordo di un libro di trading quantitativo su BTC/ETH. +Scrivi l'analisi di UNA giornata, in italiano, per un lettore che rileggera' questa pagina fra +mesi per capire cosa stava succedendo. + +REGOLE VINCOLANTI: +1. Usa SOLO i numeri presenti nella pagina qui sotto. Non calcolarne di nuovi, non stimare, non + arrotondare in modo diverso. Ogni cifra che scrivi deve comparire nella pagina. +2. NON prevedere. Niente "probabilmente salira'", niente target, niente consigli operativi. +3. NON ripetere la sezione Lettura: quella e' gia' scritta. Aggiungi cio' che le regole non + possono vedere β€” cosa e' CAMBIATO rispetto ai giorni precedenti, cosa e' insolito, quale + lettura alternativa dello stesso dato sarebbe altrettanto compatibile. +4. Se una cosa non e' determinabile dai dati dati, dillo: "non misurabile qui" e' una risposta + accettabile e preferibile a un'inferenza. +5. Se noti qualcosa che merita un controllo umano, dillo in una frase, senza drammatizzare. +6. Massimo %d parole. Prosa, niente elenchi puntati, niente titoli. + +Il libro e' TP01 (trend long-flat, 75%%) + SKH01 (breakout, 25%%) su Deribit, capitale ~$635. +Il progetto e' esplicitamente scettico: il suo campione e' corto e il P&L non distingue ancora +l'edge dalla fortuna. Scrivere con quella consapevolezza e' parte del compito. +""" % MAX_PAROLE + + +# ---------------------------------------------------------------- guardia sui numeri + +_NUM = re.compile(r"-?\d[\d.,]*") + + +def _normalizza(tok: str) -> str: + t = tok.replace(",", "").rstrip(".").lstrip("-").lstrip("0") or "0" + if "." in t: + t = t.rstrip("0").rstrip(".") or "0" + return t + + +def numeri_di(testo: str) -> set[str]: + return {_normalizza(m.group()) for m in _NUM.finditer(testo)} + + +def numeri_non_supportati(analisi: str, fonte: str) -> list[str]: + """Numeri che compaiono nell'analisi e NON nella pagina data al modello. + + Tollera i numeri piccoli fino a 31 (giorni, conteggi, Β«2 su 3Β»): non sono cifre che possano + fabbricare una misura, e includerli produrrebbe soprattutto falsi positivi. + """ + ok = numeri_di(fonte) + fuori = [] + for t in sorted(numeri_di(analisi) - ok): + try: + if abs(float(t)) <= 31 and "." not in t: + continue + except ValueError: + continue + fuori.append(t) + return fuori + + +def valida(analisi: str, fonte: str) -> tuple[str, list[str]]: + """Ritorna (stato, motivi). Stato in {ok, sospetta, rifiutata}.""" + motivi: list[str] = [] + testo = analisi.strip() + if not testo: + return "rifiutata", ["risposta vuota"] + n_par = len(testo.split()) + if n_par > MAX_PAROLE * 1.5: + motivi.append(f"troppo lunga ({n_par} parole contro un tetto di {MAX_PAROLE})") + fuori = numeri_non_supportati(testo, fonte) + if fuori: + motivi.append("numeri non presenti nella pagina: " + ", ".join(fuori)) + if len(fuori) > MAX_NUMERI_LIBERI: + return "rifiutata", motivi + return ("sospetta" if motivi else "ok"), motivi + + +def pulisci(testo: str) -> str: + """Toglie titoli e recinti di codice: l'analisi entra in una sezione che ha gia' il suo.""" + righe = [r for r in testo.strip().splitlines() if not r.strip().startswith(("#", "```"))] + return "\n".join(righe).strip() + + +# ---------------------------------------------------------------- chiamata al modello + +def senza_analisi(md: str) -> str: + """Toglie la sezione `## Analisi (agente)` dalla pagina prima di darla al modello. + + Senza questo il modello legge la PROPRIA uscita del giro precedente e la commenta: al primo + giro reale ha prodotto un paragrafo sull'avviso del tripwire che si era preso il giorno prima. + E' un ciclo di retroazione β€” ogni giorno un po' piu' di meta-commento e un po' meno analisi β€” + e si vede solo guardando l'uscita, perche' nessun controllo puo' distinguerlo da prosa valida. + La sezione `## Nota` resta: quella e' contesto dell'operatore, ed e' utile che la legga. + """ + fuori, salta = [], False + for r in md.splitlines(): + if r.startswith("## "): + salta = r.strip().startswith("## Analisi") + if not salta: + fuori.append(r) + return "\n".join(fuori) + + +def costruisci_prompt(pagina: str, storico: str) -> str: + pagina = senza_analisi(pagina) + return (f"{ISTRUZIONI}\n\n" + f"=== PAGINA DI OGGI ===\n{pagina}\n\n" + f"=== GIORNI PRECEDENTI (per vedere cosa e' cambiato) ===\n{storico}\n\n" + f"Scrivi ora l'analisi, e nient'altro.") + + +def interroga(prompt: str, modello: str = MODELLO_DEFAULT, + timeout: int = TIMEOUT_S) -> tuple[str, str | None]: + """Ritorna (testo, errore). L'errore si RESTITUISCE, non si ingoia: senza, il giorno che + l'agente smette di rispondere il giornale mostrerebbe l'analisi di ieri come se fosse di oggi. + """ + try: + p = subprocess.run( + ["claude", "-p", prompt, "--model", modello, "--allowed-tools", "", "--max-turns", "1"], + capture_output=True, text=True, timeout=timeout) + except FileNotFoundError: + return "", "CLI `claude` non trovata" + except subprocess.TimeoutExpired: + return "", f"timeout dopo {timeout}s" + if p.returncode != 0: + return "", f"uscita {p.returncode}: {(p.stderr or '').strip()[:200]}" + return pulisci(p.stdout), None diff --git a/src/live/journal.py b/src/live/journal.py new file mode 100644 index 0000000..48dd17e --- /dev/null +++ b/src/live/journal.py @@ -0,0 +1,449 @@ +"""Giornale di bordo del libro live: una voce al giorno, MISURATA. + +REGOLA DI QUESTO MODULO: il giornale registra NUMERI e stati derivati dai numeri. +Non scrive narrativa di mercato, non interpreta, non prevede. Il campo `nota` esiste +apposta per il testo libero dell'operatore, ed e' l'unico posto dove puo' finire un'opinione: +resta vuoto se nessuno lo scrive. *Un giornale che si inventa la lettura del mercato smette +di essere una misura e diventa un racconto β€” e fra sei mesi non si distingue piu' quale delle +due cose si stava leggendo.* + +TRE STATI, mai due: ogni grandezza e' un numero, oppure `None` con una ragione. Un dato +mancante NON diventa zero (lezione `paper_dvolspread` / `venue_watch`: "non vedo" non e' +"va tutto bene"). +""" +from __future__ import annotations + +import json +import math +from datetime import date, datetime, timedelta, timezone +from pathlib import Path + +import numpy as np +import pandas as pd + +from src.live import tradesdb as T + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +JOURNAL_DIR = PROJECT_ROOT / "docs" / "journal" +ASSETS = ("BTC", "ETH") +# Gli orizzonti del segnale TP01 in produzione (trend_portfolio: TSMOM 30/90/180 giorni). +ORIZZONTI = (30, 90, 180) + + +def _giornaliero(df: pd.DataFrame) -> pd.Series: + s = df.set_index(pd.to_datetime(df["timestamp"], unit="ms", utc=True))["close"] + return s.resample("1D").last().dropna() + + +def _rv_annua(px: pd.Series, n: int = 30) -> float | None: + r = np.log(px).diff().dropna() + if len(r) < n: + return None + return float(r.tail(n).std(ddof=1) * math.sqrt(365) * 100) + + +def metriche_mercato(giorno: date) -> dict: + """Stato del mercato al `giorno`, dal feed CERTIFICATO. Nessuna chiamata di rete.""" + from src.data.downloader import load_data + out: dict = {} + for a in ASSETS: + try: + px = _giornaliero(load_data(a, "1h")) + except Exception as e: + out[a] = dict(errore=f"feed non leggibile: {type(e).__name__}") + continue + px = px[px.index.date <= giorno] + if px.empty: + out[a] = dict(errore="nessuna barra fino al giorno richiesto") + continue + ult = float(px.iloc[-1]) + d = dict(chiusura=ult, barra=str(px.index[-1].date())) + for k, n in (("ret_1g", 1), ("ret_7g", 7), ("ret_30g", 30)): + d[k] = float(px.iloc[-1] / px.iloc[-1 - n] - 1) * 100 if len(px) > n else None + d["rv30_annua"] = _rv_annua(px) + # gli stessi orizzonti del segnale di produzione: e' lo stato che il libro LEGGE + d["tsmom"] = {f"{n}g": (None if len(px) <= n else int(np.sign(px.iloc[-1] / px.iloc[-1 - n] - 1))) + for n in ORIZZONTI} + segni = [v for v in d["tsmom"].values() if v is not None] + d["tsmom_su"] = (sum(1 for v in segni if v > 0), len(segni)) if segni else None + # DVOL: se il file non c'e' o e' vecchio -> None con ragione, mai 0 + f = PROJECT_ROOT / "data" / "raw" / f"dvol_{a.lower()}.parquet" + if f.exists(): + dv = pd.read_parquet(f) + dv.index = pd.to_datetime(dv["timestamp"], unit="ms", utc=True) + dv = dv[dv.index.date <= giorno]["close"] + if len(dv): + eta = (giorno - dv.index[-1].date()).days + d["dvol"] = float(dv.iloc[-1]) if eta <= 2 else None + d["dvol_eta_g"] = eta + if len(dv) >= 252 and eta <= 2: + d["dvol_rank_1a"] = float((dv.tail(252) < dv.iloc[-1]).mean()) + if len(dv) >= 60 and eta <= 2: + # IV-rank come lo calcola VRP01 (`options_vrp_v2._ivrank`): percentile + # ESPANDENTE su tutta la storia, non una finestra di un anno. Sono due + # statistiche diverse e vanno chiamate con nomi diversi. + d["dvol_rank_exp"] = float((dv.iloc[:-1] < dv.iloc[-1]).mean()) + else: + d["dvol"] = None; d["dvol_nota"] = "nessuna barra DVOL <= giorno" + else: + d["dvol"] = None; d["dvol_nota"] = "file DVOL assente" + out[a] = d + return out + + +def stato_libro(con, giorno: date) -> dict: + """Ultimo giro di `book_execute` del giorno: cosa il libro vedeva e teneva.""" + runs = T.parse_cron_log(T.CRON_LOG.read_text(errors="replace")) if T.CRON_LOG.exists() else [] + g = [r for r in runs if r.ts_utc[:10] == giorno.isoformat()] + if not g: + return dict(errore="nessun giro di book_execute quel giorno", giri=0) + ult = g[-1] + lordo = sum(abs(v.get("pos", 0.0)) for v in ult.stato_asset.values()) + return dict(giri=len(g), ultimo_giro=ult.ts_utc, equity=ult.equity, barra=ult.last_bar, + feed_skh_min=ult.feed_min, nozionale_lordo=lordo, + leva_lorda=(lordo / ult.equity if ult.equity else None), + asset={a: dict(tp_frac=v["tp_frac"], skh_sign=v["skh_sign"], + skh_entry=v.get("skh_entry"), target=v["net"], posizione=v["pos"], + azione=v["azione"]) for a, v in ult.stato_asset.items()}) + + +def pnl_giorno(con, giorno: date) -> dict: + """P&L del giorno. `equity` e' l'autorita' (venue); il resto e' scomposizione locale.""" + g = giorno.isoformat() + eq = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc LIKE ? ORDER BY ts_utc", + (f"{g}%",)).fetchall() + prima = con.execute("SELECT equity FROM equity WHERE ts_utc < ? ORDER BY ts_utc DESC LIMIT 1", + (g,)).fetchone() + e_ini = prima["equity"] if prima else (eq[0]["equity"] if eq else None) + e_fin = eq[-1]["equity"] if eq else None + rt = con.execute("SELECT * FROM roundtrips WHERE ts_out LIKE ?", (f"{g}%",)).fetchall() + fills = con.execute("SELECT * FROM fills WHERE ts_utc LIKE ?", (f"{g}%",)).fetchall() + e_arm = con.execute("SELECT equity FROM equity ORDER BY ts_utc LIMIT 1").fetchone() + return dict( + equity_inizio=e_ini, equity_fine=e_fin, + delta_equity=(e_fin - e_ini) if (e_ini is not None and e_fin is not None) else None, + realizzato_lordo=sum(r["pnl_lordo"] for r in rt) if rt else 0.0, + realizzato_netto=sum(r["pnl_netto"] for r in rt) if rt else 0.0, + roundtrip_chiusi=len(rt), fill=len(fills), + fee=sum(f["fee"] for f in fills) if fills else 0.0, + cumulato_da_arming=(e_fin - e_arm["equity"]) if (e_fin is not None and e_arm) else None, + letture_equity=len(eq)) + + +def salute(con, giorno: date) -> dict: + """Giri attesi: 24 per un giorno CHIUSO, le ore trascorse per quello in corso. + + Un giorno in corso confrontato con 24 produce un allarme a ogni esecuzione β€” e un + allarme che scatta sempre e' un allarme che non verra' letto il giorno che e' vero. + """ + lb = stato_libro(con, giorno) + adesso = datetime.now(timezone.utc) + in_corso = giorno == adesso.date() + atteso = (adesso.hour + 1) if in_corso else 24 + fatti = lb.get("giri", 0) + return dict(giri_book=fatti, feed_skh_min=lb.get("feed_skh_min"), atteso_giri=atteso, + giorno_in_corso=in_corso, giri_mancanti=max(0, atteso - fatti)) + + +def costruisci(con, giorno: date) -> dict: + return dict(giorno=giorno.isoformat(), ts_scritto=T.ora(), + mercato=metriche_mercato(giorno), libro=stato_libro(con, giorno), + pnl=pnl_giorno(con, giorno), salute=salute(con, giorno)) + + +def salva(con, voce: dict, nota: str | None = None) -> None: + """Ricalcola le sezioni misurate. NON tocca ne' `nota` (operatore) ne' `analisi` (agente): + sono le uniche due cose in pagina che un ricalcolo non puo' rigenerare.""" + esistente = con.execute("SELECT nota FROM journal WHERE giorno=?", (voce["giorno"],)).fetchone() + # la nota dell'operatore NON viene mai sovrascritta da un ricalcolo automatico + nota_finale = nota if nota is not None else (esistente["nota"] if esistente else "") + con.execute("""INSERT INTO journal (giorno, ts_scritto, mercato, libro, pnl, salute, nota) + VALUES (?,?,?,?,?,?,?) + ON CONFLICT(giorno) DO UPDATE SET + ts_scritto=excluded.ts_scritto, mercato=excluded.mercato, + libro=excluded.libro, pnl=excluded.pnl, salute=excluded.salute, + nota=excluded.nota""", + (voce["giorno"], voce["ts_scritto"], json.dumps(voce["mercato"]), + json.dumps(voce["libro"]), json.dumps(voce["pnl"]), + json.dumps(voce["salute"]), nota_finale)) + con.commit() + + + +# ============================================================================================= +# LETTURA RAGIONATA β€” a regole DICHIARATE, non a prosa libera +# ============================================================================================= +# Ogni riga della lettura esce da una regola con un id, e ogni regola puo' solo COMBINARE numeri +# gia' presenti nella pagina. Non prevede, non consiglia, non interpreta oltre il dato. +# Le tre proprieta' che la rendono diversa da un racconto: +# (a) TRACCIABILE β€” l'id dice quale regola ha parlato; +# (b) FALSIFICABILE β€” ogni regola ha un test che la accende E uno che la tiene spenta; +# (c) MUTA sul non misurato β€” se il numero e' None la regola tace, non arrotonda a zero. +# Il campo `nota` resta l'unico posto dove puo' finire un giudizio umano. + +def _bucket_convinzione(tp: float) -> str: + """TP01 media tre `np.sign()` e poi taglia a long-flat: la direzione vive in {0, 1/3, 1}. + + (Il bucket 2/3 NON esiste β€” verificato al sorgente il 2026-08-23, Β§41.) Il `tp_frac` che + il libro stampa e' quella direzione MOLTIPLICATA per il vol-target, quindi non e' il bucket: + qui se ne riporta solo l'ordine di grandezza. + """ + if tp <= 0.0: + return "flat" + return "parziale" if tp < 0.60 else "piena" + + +def lettura(con, voce: dict) -> list[dict]: + """Ritorna [{id, testo, livello}] β€” livello in {info, nota, attenzione}.""" + m, lb, p, s = voce["mercato"], voce["libro"], voce["pnl"], voce["salute"] + out: list[dict] = [] + + def dire(rid, testo, livello="info"): + out.append(dict(id=rid, testo=testo, livello=livello)) + + # --- cosa il libro STA FACENDO ----------------------------------------------------------- + if "errore" in lb: + dire("stato", f"Nessun giro di `book_execute`: {lb['errore']}. " + "Non e' Β«il libro era fermoΒ», e' Β«non e' stato misuratoΒ».", "attenzione") + return out + + att = {a: v for a, v in lb["asset"].items() if abs(v["posizione"]) >= 1.0} + if not att: + dire("stato", "Libro **flat** su entrambe le gambe: nessun capitale a mercato.") + motivi = [] + for a, v in sorted(lb["asset"].items()): + pezzi = [] + if v["tp_frac"] <= 0: + pezzi.append("TP01 a zero (trend giu' o misto)") + if v["skh_sign"] == 0: + pezzi.append("SKH01 senza breakout") + if pezzi: + motivi.append(f"{a}: " + " e ".join(pezzi)) + if motivi: + dire("perche_flat", "Motivo, componente per componente β€” " + "; ".join(motivi) + + ". Per una strategia long-flat stare fuori **e'** una decisione.") + else: + pezzi = [f"{a} ${v['posizione']:+,.0f} (TP01 {v['tp_frac']:+.3f}, " + f"SKH01 {'flat' if v['skh_sign'] == 0 else ('long' if v['skh_sign'] > 0 else 'short')})" + for a, v in sorted(att.items())] + dire("stato", "A mercato su " + " e ".join(pezzi) + ".") + + # --- il segnale contro il mercato -------------------------------------------------------- + for a, v in sorted(lb["asset"].items()): + d = m.get(a) or {} + su = d.get("tsmom_su") + if not su: + continue + n_su, n_tot = su + b = _bucket_convinzione(v["tp_frac"]) + if 0 < n_su < n_tot and v["tp_frac"] > 0: + dire("disaccordo", f"{a}: gli orizzonti del trend **non concordano** ({n_su}/{n_tot} " + f"al rialzo) e l'esposizione TP01 e' {b} ({v['tp_frac']:+.3f}) β€” " + "e' il meccanismo, non una scelta di oggi.") + elif n_su == n_tot and v["tp_frac"] <= 0: + dire("incoerenza", f"{a}: tutti e {n_tot} gli orizzonti sono al rialzo ma TP01 e' a " + "zero. Da guardare: non e' lo stato atteso.", "attenzione") + + # --- quanto capitale e' esposto ---------------------------------------------------------- + lv = lb.get("leva_lorda") + if lv is not None: + # Il tetto si LEGGE da config/live.json. Ridichiararlo qui e' lo schema che questo + # progetto ha gia' pagato quattro volte (fee_watch, test di guardia sul cap, taratura + # venue_watch, indice del namespace): un sorvegliante che ridichiara il proprio + # bersaglio continua a passare il giorno che il bersaglio cambia. + try: + cfg = json.loads((PROJECT_ROOT / "config" / "live.json").read_text()) + cap_frac = float(cfg["max_notional_per_asset_frac"]) + tetto = cap_frac * len(lb["asset"]) + dire("leva", f"Leva lorda **{lv:.2f}x** su un tetto di {tetto:.2f}x " + f"(`max_notional_per_asset_frac` {cap_frac:.2f} x {len(lb['asset'])} " + f"asset, letto da `config/live.json`): margine {tetto - lv:.2f}x.") + except Exception as e: + dire("leva", f"Leva lorda **{lv:.2f}x**; tetto non verificabile " + f"({type(e).__name__} su config/live.json).", "attenzione") + + # --- P&L del giorno, in proporzione ------------------------------------------------------ + dq, fee = p.get("delta_equity"), p.get("fee", 0.0) + if dq is not None: + if p["fill"] and fee > 0 and abs(dq) < fee: + dire("fee", f"Il movimento di equity del giorno (${dq:+.2f}) e' **piu' piccolo delle " + f"fee pagate** (${fee:.4f}): la giornata l'ha decisa il costo, non il segnale.", + "nota") + elif p["fill"] == 0: + dire("pnl", f"Equity ${dq:+.2f} **senza operare**: e' mark-to-market sulle posizioni " + "gia' aperte.") + else: + dire("pnl", f"Equity ${dq:+.2f} con {p['fill']} fill e " + f"{p['roundtrip_chiusi']} round-trip chiusi (realizzato " + f"${p['realizzato_netto']:+.2f} netto).") + + # --- il cumulato, e da dove viene -------------------------------------------------------- + eq = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc <= ? ORDER BY ts_utc", + (voce["giorno"] + "T23:59:59+00:00",)).fetchall() + if len(eq) > 24: + e0, e1 = eq[0]["equity"], eq[-1]["equity"] + tot = e1 - e0 + picco = max(r["equity"] for r in eq) + g7 = [r for r in eq if r["ts_utc"][:10] >= (date.fromisoformat(voce["giorno"]) - + timedelta(days=7)).isoformat()] + # Soglia di rilevanza: su un cumulato di pochi dollari la "concentrazione" e' aritmetica + # vera e informazione zero. Una regola che si accende su $2 insegna a saltare la sezione. + rilevante = abs(tot) >= max(5.0, 0.01 * e1) + if rilevante and g7: + quota = (e1 - g7[0]["equity"]) / tot + if quota > 0.60: + coda = ("Un risultato concentrato in una finestra non e' un tasso di rendimento: " + "e' un evento.") + if quota > 1.0: + coda = ("Oltre il 100% perche' **il periodo precedente era in perdita**: " + "senza questi 7 giorni il libro sarebbe sotto. " + coda) + dire("concentrazione", f"**Il {100*quota:.0f}% di tutto il P&L cumulato " + f"(${tot:+,.2f}) viene dagli ultimi 7 giorni.** " + coda, "nota") + if picco > 0: + dd = (e1 - picco) / picco + if dd < -0.005: + dire("drawdown", f"Equity **{100*dd:.2f}%** sotto il picco (${picco:,.2f}). " + "⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.", "nota") + + # --- volatilita' implicita contro realizzata --------------------------------------------- + try: + from src.portfolio.sleeves import VRP_CFG + soglia_ivr = float(VRP_CFG["gate_ivr"]) # derivata dal sleeve, non riscritta + except Exception: + soglia_ivr = None + for a in ASSETS: + d = m.get(a) or {} + dv, rv = d.get("dvol"), d.get("rv30_annua") + if dv is None or rv is None: + continue + segno = "sopra" if dv > rv else "sotto" + riga = f"{a}: implicita {dv:.1f} contro realizzata 30g {rv:.1f} ({segno})" + if d.get("dvol_rank_1a") is not None: + riga += f"; DVOL al {100*d['dvol_rank_1a']:.0f}Β° pctl di un anno" + ivr = d.get("dvol_rank_exp") + if ivr is not None and soglia_ivr is not None: + riga += (f"; IV-rank espandente {ivr:.2f} " + + (f"**sopra** la soglia {soglia_ivr:.2f} del gate di VRP01 (a quel gate il " + "sleeve venderebbe β€” resta paper, regola Β«niente short-vol da modelloΒ»)" + if ivr > soglia_ivr else + f"sotto la soglia {soglia_ivr:.2f} del gate di VRP01 (sleeve fermo)")) + dire("vol", riga + ".") + + # --- giornata fuori scala ---------------------------------------------------------------- + for a in ASSETS: + d = m.get(a) or {} + r1, rv = d.get("ret_1g"), d.get("rv30_annua") + if r1 is None or not rv: + continue + sd_g = rv / math.sqrt(365) + if sd_g > 0 and abs(r1) > 2 * sd_g: + dire("evento", f"{a}: giornata a {r1:+.2f}%, **{abs(r1)/sd_g:.1f} deviazioni** " + f"giornaliere (sd implicita dalla RV30 = {sd_g:.2f}%).", "nota") + + # --- salute ------------------------------------------------------------------------------ + if s.get("giri_mancanti"): + dire("giri", f"**{s['giri_mancanti']} giri di `book_execute` mancanti** su " + f"{s['atteso_giri']}: in quelle ore il libro non ha ne' letto ne' eseguito.", + "attenzione") + if s.get("feed_skh_min") is not None and s["feed_skh_min"] > 30: + dire("feed", f"Feed SKH01 vecchio di {s['feed_skh_min']} min (soglia 30): la latenza " + "d'uscita di SKH01 e' quella, non ~1h.", "attenzione") + + # --- quanto vale questa pagina come evidenza --------------------------------------------- + n_rt = con.execute("SELECT COUNT(*) c FROM roundtrips WHERE ts_out <= ?", + (voce["giorno"] + "T23:59:59+00:00",)).fetchone()["c"] + if len(eq) > 24: + gg = (date.fromisoformat(eq[-1]["ts_utc"][:10]) - date.fromisoformat(eq[0]["ts_utc"][:10])).days + dire("evidenza", f"Campione a oggi: **{gg} giorni, {n_rt} round-trip**. A questa taglia " + "il P&L non distingue l'edge dalla fortuna β€” il criterio di morte del " + "libro (`edge_watch`) lavora su una finestra di 36 mesi.", "nota") + return out + +def _n(v, fmt="{:+.2f}", vuoto="n/d"): + return vuoto if v is None else fmt.format(v) + + +def rendi_markdown(con, voce: dict) -> str: + g, m, lb, p, s = voce["giorno"], voce["mercato"], voce["libro"], voce["pnl"], voce["salute"] + riga = con.execute("SELECT * FROM journal WHERE giorno=?", (g,)).fetchone() + nota = (riga["nota"] if riga else "") or "" + an = dict(riga) if riga else {} + L = [f"# Giornale di bordo β€” {g}", "", + f"*Scritto {voce['ts_scritto']}. Numeri misurati; nessuna interpretazione automatica.*", "", + "## Mercato", "", + "| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |", + "|---|---|---|---|---|---|---|---|"] + for a in ASSETS: + d = m.get(a) or {} + # tre stati: misurato / errore dichiarato / blocco assente. Nessuno dei tre e' uno zero, + # e l'ultimo non deve far cadere la pagina (un giornale che non si scrive non e' un giornale). + if "errore" in d or "chiusura" not in d: + perche = d.get("errore", "blocco mercato assente") + L.append(f"| **{a}** | n/d | n/d | n/d | n/d | n/d | n/d | {perche} |"); continue + ts = d.get("tsmom", {}) + segni = " ".join("↑" if ts.get(f"{n}g") == 1 else "↓" if ts.get(f"{n}g") == -1 else "Β·" + for n in ORIZZONTI) + su = d.get("tsmom_su") + dv = f"{d['dvol']:.1f}" if d.get("dvol") is not None else f"n/d ({d.get('dvol_nota','vecchio')})" + if d.get("dvol_rank_1a") is not None: + dv += f" ({100*d['dvol_rank_1a']:.0f}Β° pctl 1a)" + L.append(f"| **{a}** | ${d['chiusura']:,.2f} | {_n(d.get('ret_1g'),'{:+.2f}%')} | " + f"{_n(d.get('ret_7g'),'{:+.2f}%')} | {_n(d.get('ret_30g'),'{:+.2f}%')} | " + f"{_n(d.get('rv30_annua'),'{:.1f}%')} | {segni}" + f"{f' ({su[0]}/{su[1]} su)' if su else ''} | {dv} |") + L += ["", "## Libro", ""] + if "errore" in lb: + L.append(f"⚠️ {lb['errore']}") + else: + L += [f"Equity **${lb['equity']:,.2f}** Β· nozionale lordo ${lb['nozionale_lordo']:,.0f} " + f"Β· leva lorda {_n(lb.get('leva_lorda'),'{:.2f}x')} Β· barra dati {lb['barra']} " + f"Β· {lb['giri']} giri", "", + "| | TP01 | SKH01 | target | posizione | azione |", "|---|---|---|---|---|---|"] + for a, v in sorted(lb["asset"].items()): + if v["skh_sign"] == 0: + sk = "flat" + else: + sk = "LONG" if v["skh_sign"] > 0 else "SHORT" + if v.get("skh_entry"): + sk += " @ {:,.1f}".format(v["skh_entry"]) + L.append(f"| **{a}** | {v['tp_frac']:+.3f} | {sk} | ${v['target']:+,.0f} | " + f"${v['posizione']:+,.0f} | {v['azione']} |") + L += ["", "## P&L", "", + f"- giorno: **{_n(p.get('delta_equity'),'${:+,.2f}')}** di equity " + f"({p['letture_equity']} letture)", + f"- realizzato: {p['realizzato_netto']:+.2f} netto su {p['roundtrip_chiusi']} round-trip " + f"chiusi Β· {p['fill']} fill Β· fee {p['fee']:.4f}", + f"- cumulato dall'arming: **{_n(p.get('cumulato_da_arming'),'${:+,.2f}')}**", "", + "## Salute", "", + f"- giri di `book_execute`: {s['giri_book']}/{s['atteso_giri']}" + + (" *(giorno in corso)*" if s.get("giorno_in_corso") else "") + + (f" β€” **{s['giri_mancanti']} mancanti**" if s["giri_mancanti"] else ""), + f"- eta' feed SKH all'ultimo giro: " + + (f"{s['feed_skh_min']} min" if s["feed_skh_min"] is not None else "non misurata"), + ""] + righe = lettura(con, voce) + L += ["## Lettura", "", + "*Generata da regole dichiarate in `src/live/journal.py` β€” combina i numeri qui sopra " + "e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un " + "giudizio: quello sta nella Nota.*", ""] + icona = {"info": "", "nota": "πŸ“Œ ", "attenzione": "⚠️ "} + L += [f"- `[{r['id']}]` {icona[r['livello']]}{r['testo']}" for r in righe] or ["- *(nessuna regola accesa)*"] + testo = (an.get("analisi") or "").strip() + if testo: + stato = an.get("analisi_stato") or "?" + cap = (f"*Scritta da `{an.get('analisi_modello')}` il {an.get('analisi_ts')}. " + f"Prosa di un modello: puo' sbagliare, e non e' una misura. " + f"Controllo sui numeri: **{stato}**.*") + L += ["", "## Analisi (agente)", "", cap, ""] + if an.get("analisi_motivi"): + L += [f"> ⚠️ {an['analisi_motivi']}", ""] + L += [testo] + L += ["", "## Nota", "", (nota if nota.strip() else "*(vuota β€” campo dell'operatore)*"), ""] + return "\n".join(L) + + +def scrivi_file(con, voce: dict) -> Path: + JOURNAL_DIR.mkdir(parents=True, exist_ok=True) + f = JOURNAL_DIR / f"{voce['giorno']}.md" + f.write_text(rendi_markdown(con, voce)) + return f diff --git a/src/live/tradesdb.py b/src/live/tradesdb.py new file mode 100644 index 0000000..33e67b7 --- /dev/null +++ b/src/live/tradesdb.py @@ -0,0 +1,372 @@ +"""Libro di bordo del book live: DB dei trade ALLINEATO COL TEMPO + serie di equity. + +PERCHE' ESISTE +-------------- +`data/live/book_executions.jsonl` registra i fill, ma il suo `ts_utc` e' la data della +**barra di segnale** (`book_execute.py`: `pd.Timestamp(r['last_data'])`), non l'ora del fill: +19 righe su 19 a `00:00:00`. L'ora vera sta solo in `logs/cron_book.log` β€” che e' **gitignored, +non nel backup e ruotabile**. Quindi oggi la cronologia reale del libro live vive in un file +che una rotazione cancella, e nessuno se ne accorgerebbe. + +Questo modulo MATERIALIZZA quella cronologia in `data/live/trades.db` (sqlite), che sta dentro +il perimetro gia' coperto dal backup rotativo della VPS. + +FONTI, in ordine di autorita' β€” e nessuna delle tre e' completa da sola +---------------------------------------------------------------------- +1. `logs/cron_book.log` -> ORA VERA del giro + contesto (equity, tp_frac, skh, target, posizione) + e la riga di fill `-> BUY 0.0008 @ $78,094.2 fee 0.02187 (OK)`. +2. `data/live/book_executions.jsonl` -> gli stessi fill con piu' cifre, ma senza ora. +3. venue (`DeribitRead.trade_history`) -> autorevole su `order_id` e timestamp in ms, + **ma tronca**: al 2026-08-23 ritorna 1 trade su BTC e 0 su ETH. + +Le tre si INCROCIANO, non si sovrascrivono: `reconcile()` riporta le divergenze e non ripara +niente da solo (una riparazione silenziosa fra due fonti che non concordano e' un'invenzione). + +TRE STATI, non due: un fill puo' essere `ok` (le fonti concordano), `solo-log`, `solo-jsonl`. +"Non lo vedo" non e' "non c'e'". +""" +from __future__ import annotations + +import hashlib +import json +import re +import sqlite3 +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +DB_PATH = PROJECT_ROOT / "data" / "live" / "trades.db" +CRON_LOG = PROJECT_ROOT / "logs" / "cron_book.log" +EXEC_JSONL = PROJECT_ROOT / "data" / "live" / "book_executions.jsonl" + +# Data di armamento dell'esecuzione reale (CLAUDE.md, 2026-06-20). +ARMING = "2026-06-20" + +SCHEMA = """ +CREATE TABLE IF NOT EXISTS fills ( + fill_id TEXT PRIMARY KEY, + ts_utc TEXT NOT NULL, + ts_source TEXT NOT NULL, + bar_ts TEXT, + asset TEXT NOT NULL, + side TEXT NOT NULL, + qty REAL NOT NULL, + price REAL NOT NULL, + fee REAL NOT NULL, + action TEXT, + net_target REAL, + pos_before REAL, + pos_after REAL, + tp_frac REAL, + skh_sign INTEGER, + skh_entry REAL, + equity REAL, + order_id TEXT, + verified INTEGER, + stato TEXT NOT NULL DEFAULT 'ok' +); +CREATE INDEX IF NOT EXISTS ix_fills_ts ON fills(ts_utc); +CREATE TABLE IF NOT EXISTS roundtrips ( + rt_id INTEGER PRIMARY KEY AUTOINCREMENT, + asset TEXT NOT NULL, + qty REAL NOT NULL, + ts_in TEXT NOT NULL, + px_in REAL NOT NULL, + ts_out TEXT NOT NULL, + px_out REAL NOT NULL, + ore_tenuta REAL, + pnl_lordo REAL NOT NULL, + fee_quota REAL NOT NULL, + pnl_netto REAL NOT NULL +); +CREATE TABLE IF NOT EXISTS equity ( + ts_utc TEXT PRIMARY KEY, + equity REAL NOT NULL, + src TEXT NOT NULL +); +CREATE TABLE IF NOT EXISTS journal ( + giorno TEXT PRIMARY KEY, + ts_scritto TEXT NOT NULL, + mercato TEXT, + libro TEXT, + pnl TEXT, + salute TEXT, + nota TEXT NOT NULL DEFAULT '' +); +CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT); +""" + +# Colonne aggiunte dopo la prima versione dello schema. sqlite non ha ADD COLUMN IF NOT EXISTS: +# si guarda il PRAGMA. `analisi` e' dell'AGENTE, `nota` resta dell'operatore β€” due campi, perche' +# a rileggere il giornale fra sei mesi si deve sapere chi ha scritto cosa. +COLONNE_TARDIVE = { + "journal": [("analisi", "TEXT NOT NULL DEFAULT ''"), + ("analisi_modello", "TEXT"), + ("analisi_ts", "TEXT"), + ("analisi_stato", "TEXT"), + ("analisi_motivi", "TEXT")], +} + + +def _migra(con) -> None: + for tabella, colonne in COLONNE_TARDIVE.items(): + presenti = {r[1] for r in con.execute(f"PRAGMA table_info({tabella})")} + for nome, tipo in colonne: + if nome not in presenti: + con.execute(f"ALTER TABLE {tabella} ADD COLUMN {nome} {tipo}") + con.commit() + + +# ============================================================================================= +# parsing β€” puro, nessun I/O: prende testo, ritorna dati +# ============================================================================================= + +@dataclass +class Fill: + ts_utc: str + asset: str + side: str + qty: float + price: float + fee: float + ts_source: str = "cron_log" + bar_ts: str | None = None + action: str | None = None + net_target: float | None = None + pos_before: float | None = None + pos_after: float | None = None + tp_frac: float | None = None + skh_sign: int | None = None + skh_entry: float | None = None + equity: float | None = None + order_id: str | None = None + verified: int = 1 + + @property + def fill_id(self) -> str: + raw = f"{self.ts_utc}|{self.asset}|{self.side}|{self.qty:.10f}|{self.price:.6f}" + return hashlib.sha1(raw.encode()).hexdigest()[:16] + + +@dataclass +class Run: + """Un giro orario di `book_execute`.""" + ts_utc: str + equity: float | None = None + last_bar: str | None = None + feed_min: int | None = None + fills: list[Fill] = field(default_factory=list) + stato_asset: dict = field(default_factory=dict) + + +_RE_HEAD = re.compile(r"^===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book =====$") +_RE_EQ = re.compile(r"conto reale\s*:\s*\$([\d,\.]+)") +_RE_BAR = re.compile(r"ultima barra\s*:\s*(\S+)") +_RE_FEED = re.compile(r"feed SKH\s*:\s*\w+ \((\d+) min\)") +_RE_SIG = re.compile( + r"^\s*(BTC|ETH) TP ([+-][\d\.]+) Β· SKH ([+-]\d+)\(([^)]*)\) -> net \$([+-][\d,]+) " + r"\| pos \$([+-][\d,]+) -> (.+?)\s*$") +_RE_FILL = re.compile( + r"^\s*-> (BUY|SELL) ([\d\.]+) @ \$([\d,\.]+) fee ([\d\.]+) \((OK|NON VERIFICATO[^)]*)\)") + + +def _num(s: str) -> float: + return float(s.replace(",", "").replace("$", "")) + + +def parse_cron_log(text: str) -> list[Run]: + """Estrae i giri da `cron_book.log`. L'ORA del blocco e' l'ora vera del fill.""" + runs: list[Run] = [] + cur: Run | None = None + ultimo_asset: str | None = None + for line in text.splitlines(): + m = _RE_HEAD.match(line) + if m: + cur = Run(ts_utc=m.group(1).replace("Z", "+00:00")) + runs.append(cur) + ultimo_asset = None + continue + if cur is None: + continue + if (m := _RE_EQ.search(line)) and cur.equity is None: + cur.equity = _num(m.group(1)); continue + if (m := _RE_BAR.search(line)) and cur.last_bar is None: + cur.last_bar = m.group(1); continue + if (m := _RE_FEED.search(line)) and cur.feed_min is None: + cur.feed_min = int(m.group(1)); continue + if (m := _RE_SIG.match(line)): + asset = m.group(1) + entry = None + sk = m.group(4) + if "@" in sk: + try: + entry = float(sk.split("@", 1)[1]) + except ValueError: + entry = None + cur.stato_asset[asset] = dict( + tp_frac=float(m.group(2)), skh_sign=int(m.group(3)), skh_entry=entry, + net=_num(m.group(5)), pos=_num(m.group(6)), azione=m.group(7).strip()) + ultimo_asset = asset + continue + if (m := _RE_FILL.match(line)) and ultimo_asset: + st = cur.stato_asset.get(ultimo_asset, {}) + cur.fills.append(Fill( + ts_utc=cur.ts_utc, asset=ultimo_asset, side=m.group(1).lower(), + qty=float(m.group(2)), price=_num(m.group(3)), fee=float(m.group(4)), + bar_ts=cur.last_bar, action=st.get("azione"), net_target=st.get("net"), + pos_before=st.get("pos"), tp_frac=st.get("tp_frac"), + skh_sign=st.get("skh_sign"), skh_entry=st.get("skh_entry"), + equity=cur.equity, verified=1 if m.group(5) == "OK" else 0)) + return runs + + +def parse_executions_jsonl(text: str) -> list[dict]: + return [json.loads(l) for l in text.splitlines() if l.strip()] + + +def reconcile(fills_log: list[Fill], righe_jsonl: list[dict], tol_px: float = 0.51) -> dict: + """Incrocia le due fonti su (data, asset, side, qty). NON ripara: riporta. + + `tol_px`: i prezzi del log sono arrotondati a 1 decimale in stampa -> confronto con + tolleranza, e la divergenza sopra tolleranza si REGISTRA invece di essere assorbita. + """ + def chiave(g, a, s, q): + return (g, a, s, round(float(q), 8)) + + ix_log: dict = {} + for f in fills_log: + ix_log.setdefault(chiave(f.ts_utc[:10], f.asset, f.side, f.qty), []).append(f) + ix_js: dict = {} + for r in righe_jsonl: + ix_js.setdefault(chiave(r["ts_utc"][:10], r["asset"], r["side"], r["filled"]), []).append(r) + + ok, solo_log, solo_js, px_diversi = [], [], [], [] + for k, gl in ix_log.items(): + gj = ix_js.get(k, []) + for i, f in enumerate(gl): + if i < len(gj): + r = gj[i] + if abs(float(r["price"]) - f.price) > tol_px: + px_diversi.append((f, r)) + ok.append((f, r)) + else: + solo_log.append(f) + for k, gj in ix_js.items(): + extra = len(gj) - len(ix_log.get(k, [])) + for r in gj[max(0, len(gj) - extra):] if extra > 0 else []: + solo_js.append(r) + return dict(ok=ok, solo_log=solo_log, solo_jsonl=solo_js, prezzi_divergenti=px_diversi) + + +def fifo_roundtrips(fills: list[Fill]) -> tuple[list[dict], dict]: + """Round-trip chiusi per FIFO + lotti residui. Le fee si allocano PRO-QUOTA sulla qty.""" + from collections import deque, defaultdict + lotti: dict[str, deque] = defaultdict(deque) + rts: list[dict] = [] + for f in sorted(fills, key=lambda x: (x.ts_utc, x.asset)): + fee_u = f.fee / f.qty if f.qty else 0.0 + if f.side == "buy": + lotti[f.asset].append([f.qty, f.price, f.ts_utc, fee_u]) + else: + resto = f.qty + while resto > 1e-12 and lotti[f.asset]: + lq, lp, lts, lfee_u = lotti[f.asset][0] + usa = min(resto, lq) + lordo = usa * (f.price - lp) + quota_fee = usa * (lfee_u + fee_u) + ore = (datetime.fromisoformat(f.ts_utc) - datetime.fromisoformat(lts)).total_seconds() / 3600 + rts.append(dict(asset=f.asset, qty=usa, ts_in=lts, px_in=lp, ts_out=f.ts_utc, + px_out=f.price, ore_tenuta=ore, pnl_lordo=lordo, + fee_quota=quota_fee, pnl_netto=lordo - quota_fee)) + lq -= usa; resto -= usa + if lq <= 1e-12: + lotti[f.asset].popleft() + else: + lotti[f.asset][0][0] = lq + aperti = {a: [dict(qty=q, price=p, ts=t) for q, p, t, _ in dq] for a, dq in lotti.items() if dq} + return rts, aperti + + +# ============================================================================================= +# persistenza +# ============================================================================================= + +def connect(path: Path | str = DB_PATH) -> sqlite3.Connection: + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + con = sqlite3.connect(path) + con.row_factory = sqlite3.Row + con.executescript(SCHEMA) + _migra(con) + return con + + +def upsert_fills(con: sqlite3.Connection, fills: list[Fill]) -> int: + n = 0 + for f in fills: + cur = con.execute( + """INSERT INTO fills (fill_id, ts_utc, ts_source, bar_ts, asset, side, qty, price, fee, + action, net_target, pos_before, pos_after, tp_frac, skh_sign, + skh_entry, equity, order_id, verified, stato) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ON CONFLICT(fill_id) DO NOTHING""", + (f.fill_id, f.ts_utc, f.ts_source, f.bar_ts, f.asset, f.side, f.qty, f.price, f.fee, + f.action, f.net_target, f.pos_before, f.pos_after, f.tp_frac, f.skh_sign, + f.skh_entry, f.equity, f.order_id, f.verified, "ok")) + n += cur.rowcount + con.commit() + return n + + +def upsert_equity(con: sqlite3.Connection, punti: list[tuple[str, float, str]]) -> int: + n = 0 + for ts, v, src in punti: + cur = con.execute("INSERT INTO equity (ts_utc, equity, src) VALUES (?,?,?) " + "ON CONFLICT(ts_utc) DO NOTHING", (ts, v, src)) + n += cur.rowcount + con.commit() + return n + + +def rebuild_roundtrips(con: sqlite3.Connection) -> int: + """Ricalcola da zero: i round-trip sono DERIVATI, mai inseriti a mano.""" + righe = con.execute("SELECT * FROM fills ORDER BY ts_utc, asset").fetchall() + fills = [Fill(ts_utc=r["ts_utc"], asset=r["asset"], side=r["side"], qty=r["qty"], + price=r["price"], fee=r["fee"]) for r in righe] + rts, _ = fifo_roundtrips(fills) + con.execute("DELETE FROM roundtrips") + con.executemany( + """INSERT INTO roundtrips (asset, qty, ts_in, px_in, ts_out, px_out, ore_tenuta, + pnl_lordo, fee_quota, pnl_netto) + VALUES (:asset,:qty,:ts_in,:px_in,:ts_out,:px_out,:ore_tenuta, + :pnl_lordo,:fee_quota,:pnl_netto)""", rts) + con.commit() + return len(rts) + + +def set_meta(con: sqlite3.Connection, k: str, v: str) -> None: + con.execute("INSERT INTO meta (k,v) VALUES (?,?) ON CONFLICT(k) DO UPDATE SET v=excluded.v", (k, v)) + con.commit() + + +def get_meta(con: sqlite3.Connection, k: str, default=None): + r = con.execute("SELECT v FROM meta WHERE k=?", (k,)).fetchone() + return r["v"] if r else default + + +def stato_aperto(con: sqlite3.Connection) -> dict: + righe = con.execute("SELECT * FROM fills ORDER BY ts_utc, asset").fetchall() + fills = [Fill(ts_utc=r["ts_utc"], asset=r["asset"], side=r["side"], qty=r["qty"], + price=r["price"], fee=r["fee"]) for r in righe] + _, aperti = fifo_roundtrips(fills) + out = {} + for a, lotti in aperti.items(): + q = sum(x["qty"] for x in lotti) + costo = sum(x["qty"] * x["price"] for x in lotti) + out[a] = dict(qty=q, prezzo_medio=costo / q if q else 0.0, dal=min(x["ts"] for x in lotti)) + return out + + +def ora() -> str: + return datetime.now(timezone.utc).isoformat(timespec="seconds") diff --git a/tests/test_analista.py b/tests/test_analista.py new file mode 100644 index 0000000..0e83277 --- /dev/null +++ b/tests/test_analista.py @@ -0,0 +1,202 @@ +"""L'analista di bordo: cosa puo' scrivere, e cosa non deve poter rompere. + +Un modello che scrive in un registro di misure ha tre modi di rovinarlo, e qui c'e' un test per +ognuno: inventare un numero, commentare se stesso, e lasciare in pagina l'analisi di ieri il +giorno che non risponde. +""" +from __future__ import annotations + +import subprocess +import sys +from datetime import date +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "live")) + +from src.live import analista as A # noqa: E402 +from src.live import journal as J # noqa: E402 +from src.live import tradesdb as T # noqa: E402 + + +@pytest.fixture() +def db(tmp_path): + con = T.connect(tmp_path / "t.db") + yield con + con.close() + + +PAGINA = """# Giornale di bordo β€” 2026-08-21 + +## Mercato +| **BTC** | $77,000.00 | +7.31% | RV30 43.9% | + +## Libro +Equity **$644.00** Β· leva lorda 0.30x + +## Lettura +- `[stato]` A mercato su BTC $+126. + +## Analisi (agente) +*Scritta da un modello.* +Testo vecchio che il modello NON deve rileggere. + +## Nota +Contesto scritto dall'operatore. +""" + + +# ------------------------------------------------- il ciclo di retroazione + +def test_toglie_la_propria_analisi_precedente(): + fuori = A.senza_analisi(PAGINA) + assert "Testo vecchio" not in fuori + assert "## Analisi" not in fuori + + +def test_ma_lascia_tutto_il_resto_compresa_la_nota_dell_operatore(): + """Controllo positivo: se togliesse anche il resto, il modello scriverebbe al buio.""" + fuori = A.senza_analisi(PAGINA) + for pezzo in ("## Mercato", "## Libro", "## Lettura", "## Nota", + "Contesto scritto dall'operatore", "$77,000.00", "`[stato]`"): + assert pezzo in fuori + + +def test_una_pagina_senza_analisi_non_viene_toccata(): + md = "# Titolo\n\n## Mercato\nx\n\n## Nota\ny\n" + assert A.senza_analisi(md) == md.rstrip("\n") + + +def test_il_prompt_non_contiene_l_analisi_vecchia(): + p = A.costruisci_prompt(PAGINA, "storico") + assert "Testo vecchio" not in p and "storico" in p + + +# ------------------------------------------------- i numeri inventati + +def test_scopre_un_numero_che_non_c_e(): + assert A.numeri_non_supportati("L'equity e' salita a 812.40.", PAGINA) == ["812.4"] + + +def test_non_segnala_i_numeri_presenti_comunque_formattati(): + testo = "Equity 644.00 (da $644.00), BTC a 77,000.00 e RV30 43.9%." + assert A.numeri_non_supportati(testo, PAGINA) == [] + + +def test_tollera_i_conteggi_piccoli(): + """Β«2 su 3 orizzontiΒ», Β«24 giriΒ»: non sono cifre che possano fabbricare una misura.""" + assert A.numeri_non_supportati("2 su 3 orizzonti, 24 giri, 10 round-trip.", PAGINA) == [] + + +def test_una_percentuale_inventata_viene_presa(): + assert "18.57" in A.numeri_non_supportati("Il mese fa +18.57%.", PAGINA) + + +# ------------------------------------------------- la validazione + +def test_analisi_pulita_e_ok(): + stato, motivi = A.valida("BTC ha fatto +7.31% e il libro era a $+126.", PAGINA) + assert stato == "ok" and motivi == [] + + +def test_un_numero_libero_la_rende_sospetta_non_rifiutata(): + stato, motivi = A.valida("Equity a 812.40 dopo il rialzo.", PAGINA) + assert stato == "sospetta" and "812.4" in motivi[0] + + +def test_troppi_numeri_liberi_la_rifiutano(): + stato, _ = A.valida("Valori: 812.40, 913.55, 1044.10, 1155.20, 1266.30.", PAGINA) + assert stato == "rifiutata" + + +def test_risposta_vuota_rifiutata(): + assert A.valida(" ", PAGINA)[0] == "rifiutata" + + +def test_pulisci_toglie_titoli_e_recinti(): + assert A.pulisci("```\n# Titolo\ntesto vero\n```") == "testo vero" + + +# ------------------------------------------------- l'errore non si ingoia + +def test_cli_assente_ritorna_errore_non_eccezione(monkeypatch): + monkeypatch.setattr(subprocess, "run", lambda *a, **k: (_ for _ in ()).throw(FileNotFoundError())) + testo, err = A.interroga("x") + assert testo == "" and "non trovata" in err + + +def test_timeout_ritorna_errore(monkeypatch): + def boom(*a, **k): + raise subprocess.TimeoutExpired(cmd="claude", timeout=1) + monkeypatch.setattr(subprocess, "run", boom) + assert "timeout" in A.interroga("x")[1] + + +def test_uscita_non_zero_ritorna_errore(monkeypatch): + class R: + returncode, stdout, stderr = 2, "", "boom" + monkeypatch.setattr(subprocess, "run", lambda *a, **k: R()) + assert "uscita 2" in A.interroga("x")[1] + + +# ------------------------------------------------- il DB: cosa scrive e cosa non tocca + +def _prepara(db, g="2026-08-21"): + voce = dict(giorno=g, ts_scritto=T.ora(), mercato={}, libro={"errore": "x", "giri": 0}, + pnl=J.pnl_giorno(db, date.fromisoformat(g)), + salute=dict(giri_book=0, feed_skh_min=None, atteso_giri=24, + giorno_in_corso=False, giri_mancanti=24)) + J.salva(db, voce, nota="nota dell'operatore") + return voce + + +def test_l_agente_non_tocca_la_nota(db, monkeypatch, tmp_path): + import analista as CLI + _prepara(db) + monkeypatch.setattr(A, "interroga", lambda *a, **k: ("Analisi valida senza numeri nuovi.", None)) + monkeypatch.setattr(J, "JOURNAL_DIR", tmp_path) + CLI.scrivi(db, date(2026, 8, 21), modello="finto", verbose=False) + r = db.execute("SELECT nota, analisi, analisi_stato FROM journal WHERE giorno='2026-08-21'").fetchone() + assert r["nota"] == "nota dell'operatore" + assert r["analisi"].startswith("Analisi valida") and r["analisi_stato"] == "ok" + + +def test_se_il_modello_non_risponde_l_analisi_di_IERI_non_resta_in_pagina(db, monkeypatch, tmp_path): + """Il guasto peggiore: una pagina che mostra l'analisi del giorno prima come se fosse di oggi.""" + import analista as CLI + _prepara(db) + monkeypatch.setattr(J, "JOURNAL_DIR", tmp_path) + monkeypatch.setattr(A, "interroga", lambda *a, **k: ("Analisi di ieri.", None)) + CLI.scrivi(db, date(2026, 8, 21), modello="finto", verbose=False) + monkeypatch.setattr(A, "interroga", lambda *a, **k: ("", "rete KO")) + CLI.scrivi(db, date(2026, 8, 21), modello="finto", verbose=False) + r = db.execute("SELECT analisi, analisi_stato, analisi_motivi FROM journal " + "WHERE giorno='2026-08-21'").fetchone() + assert r["analisi"] == "" # niente in pagina + assert r["analisi_stato"] == "errore" and "rete KO" in r["analisi_motivi"] # ma il perche' c'e' + + +def test_un_ricalcolo_del_giornale_non_cancella_l_analisi(db, monkeypatch, tmp_path): + import analista as CLI + voce = _prepara(db) + monkeypatch.setattr(J, "JOURNAL_DIR", tmp_path) + monkeypatch.setattr(A, "interroga", lambda *a, **k: ("Analisi da conservare.", None)) + CLI.scrivi(db, date(2026, 8, 21), modello="finto", verbose=False) + J.salva(db, voce) # il cron rigenera le sezioni misurate + r = db.execute("SELECT analisi, nota FROM journal WHERE giorno='2026-08-21'").fetchone() + assert r["analisi"] == "Analisi da conservare." and r["nota"] == "nota dell'operatore" + + +def test_il_markdown_dichiara_modello_ora_e_esito_del_controllo(db, monkeypatch, tmp_path): + import analista as CLI + voce = _prepara(db) + monkeypatch.setattr(J, "JOURNAL_DIR", tmp_path) + monkeypatch.setattr(A, "interroga", lambda *a, **k: ("Testo dell'agente.", None)) + CLI.scrivi(db, date(2026, 8, 21), modello="modello-finto", verbose=False) + md = J.rendi_markdown(db, voce) + assert "## Analisi (agente)" in md and "modello-finto" in md + assert "puo' sbagliare" in md and "Controllo sui numeri: **ok**" in md + assert md.index("## Analisi") < md.index("## Nota") # l'agente prima, l'operatore in fondo diff --git a/tests/test_journal.py b/tests/test_journal.py new file mode 100644 index 0000000..fdf2c99 --- /dev/null +++ b/tests/test_journal.py @@ -0,0 +1,309 @@ +"""Il giornale di bordo: tre stati invece di due, e la nota dell'operatore che non si perde. + +Il rischio di un giornale automatico non e' sbagliare un numero: e' scrivere `0` dove il dato +non c'era, e leggerlo fra sei mesi come una misura. +""" +from __future__ import annotations + +import json +import sys +from datetime import date, datetime, timedelta, timezone +from pathlib import Path + +import pandas as pd +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from src.live import journal as J # noqa: E402 +from src.live import tradesdb as T # noqa: E402 + + +@pytest.fixture() +def db(tmp_path): + con = T.connect(tmp_path / "t.db") + yield con + con.close() + + +# ------------------------------------------------- i giri attesi non sono sempre 24 + +def test_giorno_in_corso_non_e_un_giorno_bucato(db, monkeypatch): + """24 giri attesi su un giorno non finito = allarme a ogni esecuzione, cioe' rumore.""" + oggi = datetime.now(timezone.utc).date() + monkeypatch.setattr(J, "stato_libro", lambda c, g: dict(giri=datetime.now(timezone.utc).hour + 1, + feed_skh_min=0)) + s = J.salute(db, oggi) + assert s["giorno_in_corso"] is True + assert s["atteso_giri"] == datetime.now(timezone.utc).hour + 1 + assert s["giri_mancanti"] == 0 + + +def test_giorno_chiuso_si_giudica_su_24(db, monkeypatch): + monkeypatch.setattr(J, "stato_libro", lambda c, g: dict(giri=20, feed_skh_min=0)) + s = J.salute(db, date(2026, 8, 21)) + assert s["giorno_in_corso"] is False + assert s["atteso_giri"] == 24 and s["giri_mancanti"] == 4 + + +def test_controllo_positivo_un_giorno_davvero_bucato_viene_segnalato(db, monkeypatch): + monkeypatch.setattr(J, "stato_libro", lambda c, g: dict(giri=6, feed_skh_min=None)) + assert J.salute(db, date(2026, 8, 21))["giri_mancanti"] == 18 + + +# ------------------------------------------------- dato assente != zero + +def test_dvol_assente_e_None_con_ragione_mai_zero(db, monkeypatch, tmp_path): + monkeypatch.setattr(J, "PROJECT_ROOT", tmp_path) # nessun file DVOL qui + (tmp_path / "data" / "raw").mkdir(parents=True) + m = J.metriche_mercato(date(2026, 8, 21)) + for a in ("BTC", "ETH"): + if "errore" in m[a]: + continue + assert m[a]["dvol"] is None + assert m[a]["dvol_nota"] == "file DVOL assente" # la ragione, non un valore + + +def test_giorno_senza_giri_e_un_errore_dichiarato_non_un_libro_vuoto(db, monkeypatch): + monkeypatch.setattr(T, "CRON_LOG", Path("/tmp/non-esiste-affatto.log")) + lb = J.stato_libro(db, date(2026, 8, 21)) + assert "errore" in lb and lb["giri"] == 0 + assert "equity" not in lb # non inventa uno zero + + +def test_pnl_senza_letture_di_equity_non_fabbrica_un_delta(db): + p = J.pnl_giorno(db, date(2026, 8, 21)) + assert p["delta_equity"] is None and p["equity_fine"] is None + assert p["realizzato_netto"] == 0.0 and p["roundtrip_chiusi"] == 0 # 0 trade e' un fatto + + +# ------------------------------------------------- la nota dell'operatore + +def test_la_nota_sopravvive_a_un_ricalcolo(db): + g = date(2026, 8, 21) + voce = dict(giorno=g.isoformat(), ts_scritto=T.ora(), mercato={}, libro={}, pnl={}, salute={}) + J.salva(db, voce, nota="rally post-CPI, non ho toccato niente") + J.salva(db, voce) # ricalcolo automatico + n = db.execute("SELECT nota FROM journal WHERE giorno=?", (g.isoformat(),)).fetchone()["nota"] + assert n == "rally post-CPI, non ho toccato niente" + + +def test_la_nota_si_puo_sostituire_esplicitamente(db): + g = date(2026, 8, 21) + voce = dict(giorno=g.isoformat(), ts_scritto=T.ora(), mercato={}, libro={}, pnl={}, salute={}) + J.salva(db, voce, nota="prima") + J.salva(db, voce, nota="seconda") + n = db.execute("SELECT nota FROM journal WHERE giorno=?", (g.isoformat(),)).fetchone()["nota"] + assert n == "seconda" + + +# ------------------------------------------------- il markdown non mente + +def test_il_markdown_dice_n_d_e_non_zero_quando_manca_il_dato(db): + voce = dict(giorno="2026-08-21", ts_scritto=T.ora(), + mercato={"BTC": {"errore": "feed non leggibile: FileNotFoundError"}, + "ETH": {"errore": "feed non leggibile: FileNotFoundError"}}, + libro={"errore": "nessun giro di book_execute quel giorno", "giri": 0}, + pnl=J.pnl_giorno(db, date(2026, 8, 21)), + salute=dict(giri_book=0, feed_skh_min=None, atteso_giri=24, + giorno_in_corso=False, giri_mancanti=24)) + J.salva(db, voce) + md = J.rendi_markdown(db, voce) + assert "n/d" in md and "24 mancanti" in md + assert "non misurata" in md # eta' feed sconosciuta + assert "$+0.00 di equity" not in md # niente zeri inventati + + +def test_il_markdown_riporta_il_segnale_che_il_libro_leggeva(db): + voce = dict(giorno="2026-08-21", ts_scritto=T.ora(), mercato={}, + libro=dict(giri=24, ultimo_giro="2026-08-21T23:07:01+00:00", equity=644.0, + barra="2026-08-21", feed_skh_min=0, nozionale_lordo=196.0, + leva_lorda=0.304, + asset={"BTC": dict(tp_frac=0.176, skh_sign=1, skh_entry=75280.0, + target=123.0, posizione=126.0, azione="HOLD (a target)")}), + pnl=J.pnl_giorno(db, date(2026, 8, 21)), + salute=dict(giri_book=24, feed_skh_min=0, atteso_giri=24, + giorno_in_corso=False, giri_mancanti=0)) + J.salva(db, voce) + md = J.rendi_markdown(db, voce) + assert "+0.176" in md and "LONG @ 75,280.0" in md and "0.30x" in md + + +def test_lo_stato_tsmom_usa_gli_orizzonti_di_produzione(): + """Il giornale deve riportare CIO' CHE IL LIBRO LEGGE, non un trend a caso.""" + from src.strategies import trend_portfolio as tp + src = Path(tp.__file__).read_text() + for n in J.ORIZZONTI: + assert str(n) in src, f"orizzonte {n} non compare in trend_portfolio.py" + + +# ============================================================================================= +# LETTURA RAGIONATA β€” ogni regola ha un test che la ACCENDE e uno che la tiene SPENTA +# ============================================================================================= +# Una regola che si accende sempre non sta leggendo niente, e una che non si accende mai e' +# indistinguibile da una rotta. Servono entrambe le direzioni. + +def _voce(mercato=None, libro=None, pnl=None, salute=None, giorno="2026-08-21"): + return dict(giorno=giorno, ts_scritto=T.ora(), mercato=mercato or {}, + libro=libro or {"errore": "nessun giro", "giri": 0}, pnl=pnl or {}, + salute=salute or dict(giri_book=24, feed_skh_min=0, atteso_giri=24, + giorno_in_corso=False, giri_mancanti=0)) + + +def _libro(**asset): + a = {k: dict(dict(tp_frac=0.0, skh_sign=0, skh_entry=None, target=0.0, posizione=0.0, + azione="HOLD (a target)"), **v) for k, v in asset.items()} + lordo = sum(abs(v["posizione"]) for v in a.values()) + return dict(giri=24, ultimo_giro="2026-08-21T23:07:01+00:00", equity=600.0, barra="2026-08-21", + feed_skh_min=0, nozionale_lordo=lordo, leva_lorda=lordo / 600.0, asset=a) + + +def _pnl(**kw): + base = dict(equity_inizio=600.0, equity_fine=600.0, delta_equity=0.0, realizzato_lordo=0.0, + realizzato_netto=0.0, roundtrip_chiusi=0, fill=0, fee=0.0, + cumulato_da_arming=0.0, letture_equity=24) + return {**base, **kw} + + +def _ids(righe): + return [r["id"] for r in righe] + + +def test_libro_flat_dice_anche_PERCHE(db): + v = _voce(libro=_libro(BTC={}, ETH={}), pnl=_pnl()) + r = J.lettura(db, v) + assert "stato" in _ids(r) and "perche_flat" in _ids(r) + testo = next(x["testo"] for x in r if x["id"] == "perche_flat") + assert "TP01 a zero" in testo and "SKH01 senza breakout" in testo + + +def test_libro_a_mercato_non_spiega_un_flat_che_non_c_e(db): + v = _voce(libro=_libro(BTC=dict(tp_frac=0.15, skh_sign=1, posizione=116.0)), pnl=_pnl()) + r = J.lettura(db, v) + assert "perche_flat" not in _ids(r) + assert "$+116" in next(x["testo"] for x in r if x["id"] == "stato") + + +def test_disaccordo_si_accende_a_2_su_3_e_tace_a_3_su_3(db): + lb = _libro(BTC=dict(tp_frac=0.15, skh_sign=1, posizione=116.0)) + misto = {"BTC": dict(chiusura=77000.0, tsmom_su=(2, 3), tsmom={})} + pieno = {"BTC": dict(chiusura=77000.0, tsmom_su=(3, 3), tsmom={})} + assert "disaccordo" in _ids(J.lettura(db, _voce(mercato=misto, libro=lb, pnl=_pnl()))) + assert "disaccordo" not in _ids(J.lettura(db, _voce(mercato=pieno, libro=lb, pnl=_pnl()))) + + +def test_incoerenza_trend_su_ma_libro_fuori(db): + lb = _libro(BTC=dict(tp_frac=0.0, skh_sign=0, posizione=0.0)) + m = {"BTC": dict(chiusura=77000.0, tsmom_su=(3, 3), tsmom={})} + r = J.lettura(db, _voce(mercato=m, libro=lb, pnl=_pnl())) + riga = next(x for x in r if x["id"] == "incoerenza") + assert riga["livello"] == "attenzione" + + +def test_il_tetto_di_leva_e_DERIVATO_dal_config_non_riscritto(db, monkeypatch, tmp_path): + """Se qualcuno cambia `max_notional_per_asset_frac`, la lettura deve cambiare da sola.""" + (tmp_path / "config").mkdir() + (tmp_path / "config" / "live.json").write_text(json.dumps({"max_notional_per_asset_frac": 0.25})) + monkeypatch.setattr(J, "PROJECT_ROOT", tmp_path) + lb = _libro(BTC=dict(posizione=150.0), ETH=dict(posizione=150.0)) + r = J.lettura(db, _voce(libro=lb, pnl=_pnl())) + testo = next(x["testo"] for x in r if x["id"] == "leva") + assert "0.50x" in testo and "0.25" in testo # 0.25 x 2 asset, NON 1.00x + + +def test_config_illeggibile_lo_dice_invece_di_inventare_un_tetto(db, monkeypatch, tmp_path): + monkeypatch.setattr(J, "PROJECT_ROOT", tmp_path) # nessun config qui + r = J.lettura(db, _voce(libro=_libro(BTC=dict(posizione=100.0)), pnl=_pnl())) + riga = next(x for x in r if x["id"] == "leva") + assert "non verificabile" in riga["testo"] and riga["livello"] == "attenzione" + + +def test_soglia_IV_rank_derivata_dal_sleeve(db): + """La soglia del gate viene da VRP_CFG, e l'IV-rank e' quello ESPANDENTE del sleeve.""" + from src.portfolio.sleeves import VRP_CFG + lb = _libro(BTC=dict(posizione=100.0)) + m = {"BTC": dict(chiusura=77000.0, dvol=42.0, rv30_annua=43.0, dvol_rank_exp=0.18)} + testo = next(x["testo"] for x in J.lettura(db, _voce(mercato=m, libro=lb, pnl=_pnl())) + if x["id"] == "vol") + assert f"{VRP_CFG['gate_ivr']:.2f}" in testo and "sotto la soglia" in testo + m["BTC"]["dvol_rank_exp"] = 0.90 + testo = next(x["testo"] for x in J.lettura(db, _voce(mercato=m, libro=lb, pnl=_pnl())) + if x["id"] == "vol") + assert "**sopra**" in testo + + +def test_iv_rank_espandente_e_diverso_dal_rank_a_un_anno(): + """I due rank rispondono a domande diverse: il 2026-08-23 danno 0.18 contro 0.52. + + Etichettare l'uno col nome dell'altro ribalta il verdetto sul gate di VRP01. + """ + m = J.metriche_mercato(date.today()) + for a in ("BTC", "ETH"): + d = m.get(a) or {} + if d.get("dvol_rank_exp") is None or d.get("dvol_rank_1a") is None: + continue + assert d["dvol_rank_exp"] != d["dvol_rank_1a"] + + +def test_regola_fee_quando_il_costo_supera_il_movimento(db): + lb = _libro(BTC=dict(posizione=100.0)) + r = J.lettura(db, _voce(libro=lb, pnl=_pnl(delta_equity=0.01, fill=2, fee=0.05))) + assert "fee" in _ids(r) + r = J.lettura(db, _voce(libro=lb, pnl=_pnl(delta_equity=5.00, fill=2, fee=0.05))) + assert "fee" not in _ids(r) and "pnl" in _ids(r) + + +def test_evento_si_accende_oltre_2_deviazioni_e_tace_sotto(db): + lb = _libro(BTC=dict(posizione=100.0)) + # RV30 36.5% annua -> sd giornaliera ~1.91% + grosso = {"BTC": dict(chiusura=77000.0, ret_1g=-6.0, rv30_annua=36.5)} + normale = {"BTC": dict(chiusura=77000.0, ret_1g=-1.0, rv30_annua=36.5)} + assert "evento" in _ids(J.lettura(db, _voce(mercato=grosso, libro=lb, pnl=_pnl()))) + assert "evento" not in _ids(J.lettura(db, _voce(mercato=normale, libro=lb, pnl=_pnl()))) + + +def test_le_regole_tacciono_sul_non_misurato(db): + """Mercato assente: nessuna riga su vol o eventi. Il silenzio e' il comportamento giusto.""" + r = J.lettura(db, _voce(mercato={"BTC": {"errore": "feed KO"}}, + libro=_libro(BTC=dict(posizione=100.0)), pnl=_pnl(delta_equity=None))) + assert "vol" not in _ids(r) and "evento" not in _ids(r) and "pnl" not in _ids(r) + + +def test_giri_mancanti_e_feed_vecchio_sono_attenzioni(db): + s = dict(giri_book=6, feed_skh_min=685, atteso_giri=24, giorno_in_corso=False, giri_mancanti=18) + r = J.lettura(db, _voce(libro=_libro(BTC=dict(posizione=100.0)), pnl=_pnl(), salute=s)) + liv = {x["id"]: x["livello"] for x in r} + assert liv.get("giri") == "attenzione" and liv.get("feed") == "attenzione" + + +def test_senza_giri_la_lettura_dice_NON_MISURATO_e_si_ferma(db): + r = J.lettura(db, _voce()) + assert _ids(r) == ["stato"] and "non e' stato misurato" in r[0]["testo"] + + +def test_il_bucket_di_convinzione_non_inventa_i_due_terzi(): + """`tsmom_blend` media tre np.sign(): dopo il clip long-flat la direzione sta in {0,1/3,1}.""" + assert J._bucket_convinzione(0.0) == "flat" + assert J._bucket_convinzione(0.15) == "parziale" + assert J._bucket_convinzione(0.90) == "piena" + + +def test_concentrazione_tace_su_un_cumulato_irrilevante(db): + """Vera ma inutile: su pochi dollari la concentrazione e' aritmetica, non informazione.""" + for i in range(60): + db.execute("INSERT INTO equity VALUES (?,?,?)", + (f"2026-06-{(i % 28) + 1:02d}T{i % 24:02d}:00:00+00:00", 600.0 + i * 0.03, "t")) + db.commit() + v = _voce(libro=_libro(BTC=dict(posizione=100.0)), pnl=_pnl(), giorno="2026-06-28") + assert "concentrazione" not in _ids(J.lettura(db, v)) # ~$1.8 di cumulato + + +def test_concentrazione_si_accende_quando_la_taglia_conta(db): + for i in range(60): + v = 600.0 if i < 50 else 600.0 + (i - 49) * 4.0 # salto nella coda + db.execute("INSERT INTO equity VALUES (?,?,?)", + (f"2026-06-{(i % 28) + 1:02d}T{i % 24:02d}:00:00+00:00", v, "t")) + db.commit() + v = _voce(libro=_libro(BTC=dict(posizione=100.0)), pnl=_pnl(), giorno="2026-06-28") + assert "concentrazione" in _ids(J.lettura(db, v)) diff --git a/tests/test_tradesdb.py b/tests/test_tradesdb.py new file mode 100644 index 0000000..eea0d0d --- /dev/null +++ b/tests/test_tradesdb.py @@ -0,0 +1,207 @@ +"""Il libro di bordo dei trade: parsing, FIFO, riconciliazione, idempotenza. + +Ogni rilevatore ha il suo CONTROLLO POSITIVO: un parser che non estrae mai niente e uno che +estrae tutto passano gli stessi test pigri, e nessuno dei due sta misurando qualcosa. +""" +from __future__ import annotations + +import sqlite3 +import sys +from datetime import date, datetime, timedelta, timezone +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from src.live import tradesdb as T # noqa: E402 + +LOG_FINTO = """===== 2026-08-20T04:07:01Z cron_book ===== + modo : ESECUZIONE REALE + conto reale : $610.82 + ultima barra : 2026-08-20 + + feed SKH : fresco (3 min) + BTC TP +0.199 Β· SKH +1(LONG@66475.9) -> net $+122 | pos $+76 -> BUY $+46 + -> BUY 0.0007 @ $69,351.8 fee 0.01699 (OK) + ETH TP +0.294 Β· SKH +0(flat) -> net $+67 | pos $+0 -> BUY $+67 + -> BUY 0.0298 @ $2,257.9 fee 0.02355 (OK) +===== done 04:07:14Z ===== +===== 2026-08-20T05:07:01Z cron_book ===== + conto reale : $611.90 + ultima barra : 2026-08-20 + + feed SKH : fresco (0 min) + BTC TP +0.199 Β· SKH +1(LONG@66475.9) -> net $+123 | pos $+123 -> HOLD (a target) + ETH TP +0.294 Β· SKH +0(flat) -> net $+67 | pos $+67 -> HOLD (a target) + + => Nessuna azione: conto gia' al target netto del book. +===== done 05:07:11Z ===== +""" + + +# ---------------------------------------------------------------- parsing del log + +def test_estrae_ora_vera_e_non_la_barra(): + runs = T.parse_cron_log(LOG_FINTO) + assert len(runs) == 2 + f = runs[0].fills[0] + assert f.ts_utc.startswith("2026-08-20T04:07:01") # ora del giro + assert f.bar_ts == "2026-08-20" # barra, sotto il suo nome + assert f.ts_utc[:10] != "" and "T" in f.ts_utc # non e' una data nuda + + +def test_estrae_contesto_del_segnale(): + r = T.parse_cron_log(LOG_FINTO)[0] + assert r.equity == 610.82 and r.feed_min == 3 + f = [x for x in r.fills if x.asset == "BTC"][0] + assert f.tp_frac == 0.199 and f.skh_sign == 1 and f.skh_entry == 66475.9 + assert f.net_target == 122 and f.pos_before == 76 + assert f.qty == 0.0007 and f.price == 69351.8 and f.fee == 0.01699 + assert f.verified == 1 + + +def test_controllo_positivo_un_giro_senza_fill_non_ne_inventa(): + """Il secondo giro e' un HOLD: se il parser tornasse fill anche li', non starebbe leggendo.""" + runs = T.parse_cron_log(LOG_FINTO) + assert len(runs[1].fills) == 0 + assert runs[1].stato_asset["BTC"]["azione"] == "HOLD (a target)" + + +def test_fill_non_verificato_resta_marcato(): + log = LOG_FINTO.replace("fee 0.01699 (OK)", "fee 0.01699 (NON VERIFICATO: timeout)") + f = T.parse_cron_log(log)[0].fills[0] + assert f.verified == 0 + + +def test_fill_id_deterministico_e_distingue_due_fill_simili(): + a = T.Fill(ts_utc="2026-08-20T04:07:01+00:00", asset="BTC", side="buy", qty=0.0007, price=69351.8, fee=0.01) + b = T.Fill(ts_utc="2026-08-20T04:07:01+00:00", asset="BTC", side="buy", qty=0.0007, price=69351.8, fee=0.01) + c = T.Fill(ts_utc="2026-08-20T05:07:01+00:00", asset="BTC", side="buy", qty=0.0007, price=69351.8, fee=0.01) + assert a.fill_id == b.fill_id and a.fill_id != c.fill_id + + +# ---------------------------------------------------------------- FIFO + +def _f(ts, asset, side, qty, price, fee=0.0): + return T.Fill(ts_utc=ts, asset=asset, side=side, qty=qty, price=price, fee=fee) + + +def test_fifo_chiude_nell_ordine_giusto_e_alloca_le_fee(): + fills = [_f("2026-01-01T00:00:00+00:00", "BTC", "buy", 1.0, 100.0, 1.0), + _f("2026-01-02T00:00:00+00:00", "BTC", "buy", 1.0, 200.0, 1.0), + _f("2026-01-03T00:00:00+00:00", "BTC", "sell", 1.0, 300.0, 1.0)] + rts, aperti = T.fifo_roundtrips(fills) + assert len(rts) == 1 + assert rts[0]["px_in"] == 100.0 # FIFO: chiude il PRIMO lotto + assert rts[0]["pnl_lordo"] == pytest.approx(200.0) + assert rts[0]["fee_quota"] == pytest.approx(2.0) # fee d'entrata + d'uscita, pro-quota + assert rts[0]["pnl_netto"] == pytest.approx(198.0) + assert rts[0]["ore_tenuta"] == pytest.approx(48.0) + assert aperti["BTC"][0]["price"] == 200.0 # resta il secondo lotto + + +def test_fifo_chiusura_parziale_spezza_il_lotto(): + fills = [_f("2026-01-01T00:00:00+00:00", "ETH", "buy", 1.0, 100.0), + _f("2026-01-02T00:00:00+00:00", "ETH", "sell", 0.25, 120.0)] + rts, aperti = T.fifo_roundtrips(fills) + assert len(rts) == 1 and rts[0]["qty"] == pytest.approx(0.25) + assert rts[0]["pnl_lordo"] == pytest.approx(5.0) + assert aperti["ETH"][0]["qty"] == pytest.approx(0.75) + + +def test_fifo_non_mescola_gli_asset(): + fills = [_f("2026-01-01T00:00:00+00:00", "BTC", "buy", 1.0, 100.0), + _f("2026-01-02T00:00:00+00:00", "ETH", "sell", 1.0, 100.0)] + rts, aperti = T.fifo_roundtrips(fills) + assert rts == [] # la vendita ETH non chiude il long BTC + assert set(aperti) == {"BTC"} + + +def test_fifo_riproduce_il_conto_del_libro_vero(): + """Controllo di realta' sui numeri gia' pubblicati: 12 round-trip, 11 in utile.""" + if not T.CRON_LOG.exists(): + pytest.skip("cron_book.log assente") + fills = [f for r in T.parse_cron_log(T.CRON_LOG.read_text(errors="replace")) for f in r.fills] + rts, _ = T.fifo_roundtrips(fills) + assert len(rts) >= 12 + assert sum(1 for r in rts if r["pnl_netto"] > 0) >= 11 + + +# ---------------------------------------------------------------- riconciliazione + +def test_riconciliazione_scopre_lo_sfasamento_di_data(): + """IL difetto vero: il jsonl datava un trade alla BARRA, non al fill (14/07 scritto 08/07).""" + fills = [_f("2026-07-14T14:00:02+00:00", "ETH", "buy", 0.04, 1869.7)] + righe = [dict(ts_utc="2026-07-08 00:00:00", asset="ETH", side="buy", filled=0.04, price=1869.74)] + rec = T.reconcile(fills, righe) + assert len(rec["ok"]) == 0 + assert len(rec["solo_log"]) == 1 and len(rec["solo_jsonl"]) == 1 + + +def test_riconciliazione_controllo_positivo_quando_concordano(): + """Speculare del precedente: se le due fonti coincidono NON deve segnalare niente.""" + fills = [_f("2026-08-20T04:07:01+00:00", "BTC", "buy", 0.0007, 69351.8)] + righe = [dict(ts_utc="2026-08-20 00:00:00", asset="BTC", side="buy", filled=0.0007, price=69351.8)] + rec = T.reconcile(fills, righe) + assert len(rec["ok"]) == 1 and not rec["solo_log"] and not rec["solo_jsonl"] + assert not rec["prezzi_divergenti"] + + +def test_riconciliazione_segnala_un_prezzo_davvero_diverso_non_l_arrotondamento(): + base = dict(asset="BTC", side="buy", filled=0.0007, ts_utc="2026-08-20 00:00:00") + f = [_f("2026-08-20T04:07:01+00:00", "BTC", "buy", 0.0007, 69351.8)] + assert not T.reconcile(f, [dict(base, price=69351.84)])["prezzi_divergenti"] # stampa a 1 dec. + assert T.reconcile(f, [dict(base, price=69400.00)])["prezzi_divergenti"] # divergenza vera + + +# ---------------------------------------------------------------- persistenza + +@pytest.fixture() +def db(tmp_path): + con = T.connect(tmp_path / "t.db") + yield con + con.close() + + +def test_upsert_e_idempotente(db): + fills = [f for r in T.parse_cron_log(LOG_FINTO) for f in r.fills] + assert T.upsert_fills(db, fills) == 2 + assert T.upsert_fills(db, fills) == 0 # secondo giro: nessun duplicato + assert db.execute("SELECT COUNT(*) c FROM fills").fetchone()["c"] == 2 + + +def test_roundtrip_sono_derivati_non_accumulati(db): + T.upsert_fills(db, [_f("2026-01-01T00:00:00+00:00", "BTC", "buy", 1.0, 100.0), + _f("2026-01-02T00:00:00+00:00", "BTC", "sell", 1.0, 110.0)]) + assert T.rebuild_roundtrips(db) == 1 + assert T.rebuild_roundtrips(db) == 1 # ricalcolo, non append + assert db.execute("SELECT COUNT(*) c FROM roundtrips").fetchone()["c"] == 1 + + +def test_equity_una_riga_per_istante(db): + assert T.upsert_equity(db, [("2026-01-01T00:00:00+00:00", 600.0, "x")]) == 1 + assert T.upsert_equity(db, [("2026-01-01T00:00:00+00:00", 999.0, "x")]) == 0 + assert db.execute("SELECT equity FROM equity").fetchone()["equity"] == 600.0 + + +def test_stato_aperto_media_i_lotti(db): + T.upsert_fills(db, [_f("2026-01-01T00:00:00+00:00", "ETH", "buy", 1.0, 100.0), + _f("2026-01-02T00:00:00+00:00", "ETH", "buy", 1.0, 200.0)]) + st = T.stato_aperto(db) + assert st["ETH"]["qty"] == pytest.approx(2.0) + assert st["ETH"]["prezzo_medio"] == pytest.approx(150.0) + + +# ---------------------------------------------------------------- regressione sulla SORGENTE + +def test_l_esecutore_non_data_piu_i_fill_alla_barra_di_segnale(): + """Il difetto riparato il 2026-08-23: `ts_utc` era `pd.Timestamp(r['last_data'])`. + + Se qualcuno lo rimette, questo test lo dice β€” il DB verrebbe ricostruito su date finte. + """ + src = (Path(__file__).resolve().parents[1] / "scripts" / "live" / "book_execute.py").read_text() + assert "ts_utc=str(pd.Timestamp(r['last_data']))" not in src + assert "bar_ts=str(pd.Timestamp(r['last_data']))" in src + assert "ts_utc=datetime.now(timezone.utc)" in src