From 8c0b5f97ee4064d2ccb49ddd070260bc636114c7 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 12:40:30 +0000 Subject: [PATCH] =?UTF-8?q?registro:=20chiusi=20=C2=A758-69=20=E2=80=94=20?= =?UTF-8?q?dodici=20verdetti,=200=20candidati,=20e=20due=20appoggi=20tolti?= =?UTF-8?q?=20a=20decisioni=20gia'=20prese?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I dodici filoni della sesta ondata erano rimasti `_in corso_`: la sessione si e' chiusa dopo che gli agenti avevano scritto gli script e prima che i verdetti fossero consolidati, e i loro messaggi finali sono persi. I numeri qui NON vengono da quei messaggi: vengono dalla riesecuzione dei dodici script (07:38-08:06 UTC, sequenziale, log in logs/r0823b/ che e' gitignored). E' il motivo per cui l'ondata non e' andata persa: ogni script calcola il proprio verdetto a runtime. Cosa tocca decisioni gia' prese: - §67 il gate pre-registrato XSR01 del 23/10 legge un monitor tarato sul pavimento del venue sbagliato (C* $15-20k, non ~$3k) - §64 la raccomandazione di §51 (raccogliere la catena USDC) non e' giustificata dalla ragione che porta: le due superfici sono la stessa - §60 la politica MISTO scelta il 25/07 non e' piu' l'ottimo (oggi MISTO-A) - §63 domanda fiscale NUOVA, diversa da quella aperta il 07/08 Il risultato piu' grande e' di §58: l'obiettivo del progetto ha DUE definizioni operative in uso che danno 33,9% contro 0,33% sulla stessa domanda, e non era mai stato detto quale si stesse ottimizzando. r0823b_quasi_passati.py (§68) terminava con IndexError: Griglia.combo() indicizzava con self.idx (2720 giorni) un sottoinsieme di 958 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. La correzione e' del coordinatore, non dell'autore, ed e' dichiarata nel registro. Libro, pesi, cron, config INVARIATI. Nessun ordine. Co-Authored-By: Claude Opus 5 (1M context) --- docs/research/RESULTS-0822.md | 71 +- scripts/research/r0823b_crossvenue.py | 969 +++++++++++++ scripts/research/r0823b_curva_datati.py | 896 ++++++++++++ scripts/research/r0823b_fisco_payout.py | 890 ++++++++++++ scripts/research/r0823b_ladder_redo.py | 794 +++++++++++ scripts/research/r0823b_obiettivo_diretto.py | 877 ++++++++++++ scripts/research/r0823b_quasi_passati.py | 964 +++++++++++++ scripts/research/r0823b_regime_capitale.py | 785 +++++++++++ scripts/research/r0823b_retry.py | 909 ++++++++++++ scripts/research/r0823b_scala_prop.py | 894 ++++++++++++ .../research/r0823b_scettico_conversione.py | 731 ++++++++++ scripts/research/r0823b_split_deposito.py | 784 +++++++++++ scripts/research/r0823b_usdc_surface.py | 1246 +++++++++++++++++ 13 files changed, 10798 insertions(+), 12 deletions(-) create mode 100644 scripts/research/r0823b_crossvenue.py create mode 100644 scripts/research/r0823b_curva_datati.py create mode 100644 scripts/research/r0823b_fisco_payout.py create mode 100644 scripts/research/r0823b_ladder_redo.py create mode 100644 scripts/research/r0823b_obiettivo_diretto.py create mode 100644 scripts/research/r0823b_quasi_passati.py create mode 100644 scripts/research/r0823b_regime_capitale.py create mode 100644 scripts/research/r0823b_retry.py create mode 100644 scripts/research/r0823b_scala_prop.py create mode 100644 scripts/research/r0823b_scettico_conversione.py create mode 100644 scripts/research/r0823b_split_deposito.py create mode 100644 scripts/research/r0823b_usdc_surface.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 5066cee..371080e 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -723,18 +723,18 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 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? | _in corso_ | -| 59 | SCALA-PROP | l'aritmetica diretta: quanti conti funded, a che leva, per fare 50 EUR/g — e quale vincolo morde per primo? | _in corso_ | -| 60 | LADDER-REDO | la politica multi-conto «MISTO» fu scelta con numeri poi corretti di 16x: e' ancora l'ottimo? | _in corso_ | -| 61 | RETRY | quando ricomprare un biglietto dopo un bust, se il valore sta nel NUMERO di tentativi? | _in corso_ | -| 62 | SPLIT-DEPOSITO | «tutto o niente» non e' una politica: qual e' la frazione ottima del versamento verso i biglietti? | _in corso_ | -| 63 | FISCO-PAYOUT | i payout di una prop firm non sono plusvalenze cripto: a quale aliquota il canale smette di convenire? | _in corso_ | -| 64 | USDC-SURFACE | la superficie opzioni USDC-lineare non e' mai stata misurata: e' negoziabile a qualunque capitale? | _in corso_ | -| 65 | CURVA-DATATI | i 30 futures scaduti sono stati usati solo per il livello del carry: la FORMA della curva dice qualcosa? | _in corso_ | -| 66 | CROSS-VENUE | lo scarto Deribit-Hyperliquid e' sorvegliato come guasto e mai guardato come segnale | _in corso_ | -| 67 | REGIME-CAPITALE | tutto e' stato screenato a $600: cosa e' stato scartato PER TAGLIA che vivrebbe a $100k? | _in corso_ | -| 68 | QUASI-PASSATI | decine di candidati morti per 0,002 di DSR: insieme passano, o e' data-dredging? | _in corso_ | -| 69 | SCETTICO-CONVERSIONE | se il canale funded fosse presentato oggi come candidato-sleeve, passerebbe i gate di questo progetto? | _in corso_ | +| 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 | @@ -3683,3 +3683,50 @@ ricerca**. **Il suo valore e' DIFENSIVO ed e' reale: ha impedito decisioni sbagl 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/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())