merge: ondata rendita 2026-07-25 — curva capitale, muro della rendita, asimmetria funded, HyroTrader
This commit is contained in:
@@ -417,6 +417,103 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
**`SPLIT-NON-AGG`** in `fetch_ib_equities.certify()`, test `tests/test_eq_splits.py` (8 casi).
|
**`SPLIT-NON-AGG`** in `fetch_ib_equities.certify()`, test `tests/test_eq_splits.py` (8 casi).
|
||||||
**Regola nuova: ogni soglia di certificazione tarata su un valore tondo va controllata contro il
|
**Regola nuova: ogni soglia di certificazione tarata su un valore tondo va controllata contro il
|
||||||
difetto che genera esattamente quel valore** (una soglia a 50% non puo' sorvegliare gli split 2:1).
|
difetto che genera esattamente quel valore** (una soglia a 50% non puo' sorvegliare gli split 2:1).
|
||||||
|
- ⚠ **RENDITA / CURVA CAPITALE (2026-07-25, 2° filone del giorno) — il muro del 24/07 era ottimista
|
||||||
|
2-4x, e 1 DIFETTO DI ESEGUIBILITA' su GTAA01.** Script `r0725_capcurve.py` + `r0725_prop_config.py`,
|
||||||
|
test `tests/test_capcurve.py` (10 casi); diario `2026-07-25-rendita-capcurve-prop.md`. Book/pesi/cron
|
||||||
|
**INVARIATI**.
|
||||||
|
(1) **GTAA01 non e' eseguibile come codificato.** `gtaa.py` modella 2bps proporzionali e lo sleeve e'
|
||||||
|
documentato "switch mensile/basso turnover": FALSO — il vol-target e' **continuo giornaliero su 6
|
||||||
|
gambe**, e IB ha un **pavimento FISSO** `min(max($0.35, $0.0035/az), 1% valore)` per ordine =
|
||||||
|
~$530/anno indipendenti dal capitale. A $600: **CAGR −3.5% / Sh −0.55** (vs modellato +5.5%/0.77);
|
||||||
|
negativo fino a ~$3k; a $50k arriva solo a 0.59. **La banda lo salva**: plateau robusto
|
||||||
|
**weekly x banda $25-100** → Sh 0.45-0.64 a ogni capitale (a $600: Sh 0.52 / CAGR 3.0%). Due
|
||||||
|
conseguenze: (a) se GTAA01 va live su IB **deve** avere banda+cadenza cablate; (b) il suo Sharpe
|
||||||
|
onesto e' **~0.55-0.64, non 0.77** → i numeri headline del book a 5 sleeve vanno citati con questo
|
||||||
|
haircut (in aggiunta all'anchor-luck). **REGOLA NUOVA: il costo di un venue va modellato nella sua
|
||||||
|
FORMA (fisso vs proporzionale), non solo nel livello** — due venue a pari "costo medio" danno esiti
|
||||||
|
OPPOSTI al variare del capitale (il pavimento fisso e' una tassa regressiva). Controprova: su Deribit
|
||||||
|
(proporzionale, senza pavimento) lo Sharpe realistico di TP01 = modellato da ~$500 in su → conferma
|
||||||
|
indipendente di "a $600 il min-order $5 e' gia' la banda ottimale".
|
||||||
|
(2) **La rendita NON e' `capitale x CAGR`.** Il 24/07 calcolava capitale = target/CAGR (€122k a CAGR
|
||||||
|
15%), ignorando il rischio di sequenza. Bootstrap a blocchi sui ritorni REALI (SKH01 su **path live**):
|
||||||
|
rendita **perpetua** (= prelievo con P(cap a 20a >= cap iniziale) >= 90%) del book de-luckato ×0.6 =
|
||||||
|
**5.98% → $497k**; SWR-20a 7.31% → $406k; lente modellata 13.57% → $219k. **Il muro vero per 50 EUR/g
|
||||||
|
perpetui e' una BANDA $232k-$497k** (path live nudo → path live + de-luck ×0.6; il ×0.6 sopra il path
|
||||||
|
live rischia di **contare due volte** la degradazione SKH01 → stima centrale ~$300-400k), non €122k.
|
||||||
|
Cio' che NON dipende dalla lente: **la rendita perpetua vale ~45-55% del CAGR**, quindi ogni muro
|
||||||
|
calcolato come `target/CAGR` sbaglia di un fattore ~2. A $600 il book rende **~€0.11/giorno**. Traiettoria (bootstrap,
|
||||||
|
de-luck): **0 depositi → 0% dei path in 20 anni**; €500/m mediana 19.6a; €1000/m mediana 14.8a.
|
||||||
|
(3) **Diversificare NON crea reddito a pari nozionale — libera BUDGET DI RISCHIO.** Per tier:
|
||||||
|
iso-nozionale 2 sleeve 5.98% → 4 sleeve 6.35% (nulla, il diversificatore a basso CAGR toglie vol e
|
||||||
|
ritorno insieme); a **ISO-RISCHIO** (vol 15%) 7.51%/$395k → **9.25%/$321k (−19% di muro)**.
|
||||||
|
**REGOLA: un diversificatore a basso CAGR si giudica a ISO-RISCHIO, mai a iso-nozionale** (e' il
|
||||||
|
null de-levering al contrario). L'ipotesi "piu' capitale → piu' sleeve → CAGR super-lineare" e'
|
||||||
|
**REFUTATA nella forma forte**: la curva Sharpe del book eseguibile e' PIATTA da $600 a $200k.
|
||||||
|
(4) ✅ **ASIMMETRIA CAPITALE-PROPRIO vs FUNDED (risultato strategico).** Il 24/07 valuto' il fronte
|
||||||
|
prop **solo col book a 2 sleeve**; ma su un conto funded il capitale e' $100k → **gli sleeve STAT-MODE
|
||||||
|
per taglia (XS01, ~$20k) diventano eseguibili**, e le regole prop passano sul **DRAWDOWN, non sul
|
||||||
|
CAGR**. Finestra comune 2024+, de-luck, crypto-only: P(pass) HYRO 1.0x **44.4%→54.7%**, FTMO 1.5x
|
||||||
|
**51.9%→70.0%**; **funded HYRO 1.0x P(vivo 1a) 18.9%→57.6% (3x)**, FTMO 1.0x 58.8%→**92.0%**,
|
||||||
|
E[payout] $8.7k→$9.3k (~€15.7/g atteso). **Si RIBALTA la raccomandazione "funded a 0.75x"**: col book
|
||||||
|
diversificato 1.0x e' sostenibile. **In una riga: sul capitale proprio diversificare non aumenta il
|
||||||
|
reddito; su un conto funded si', perche' li' il vincolo binding e' la regola di DD, non il capitale
|
||||||
|
→ gli sleeve "inutili a $600" sono gli asset di maggior valore sull'unico canale che scala.**
|
||||||
|
⚠ CAVEAT: MC **close-only** (C-bis 24/07: i wick tagliano 6-37pp) → livelli assoluti = TETTO, il
|
||||||
|
DELTA e' la misura onesta ed e' conservativo (B ha vol minore); la finestra comune 2024+ e' quella in
|
||||||
|
cui XS01 e' stato scoperto/affinato → la TAGLIA del guadagno e' ottimista (finestra piena: 56.9%→58.5%),
|
||||||
|
il MECCANISMO (corr bassa → meno DD → piu' sopravvivenza sotto vincolo di DD) e' robusto.
|
||||||
|
(5) ✅ **LA VIA: i 600 euro come BIGLIETTO, e il problema della correlazione fra conti**
|
||||||
|
(`r0725_prop_ladder.py`). I €600 come capitale sono refutati; come **biglietto** no: si compra
|
||||||
|
un'eval, il funded moltiplica il NOZIONALE senza possedere capitale, i payout comprano altri
|
||||||
|
biglietti. Il 24/07 si fermava a UN conto (cap $200k/firm → ~€15-30/g); **50 EUR/g richiede piu'
|
||||||
|
conti**, e li' il problema mai studiato: **N conti sullo STESSO book bustano INSIEME** (corr 1.0)
|
||||||
|
→ la diversificazione fra conti e' illusoria salvo **sleeve diversi su conti diversi**. E' un
|
||||||
|
problema di portafoglio sotto **barriera di rovina PER-CONTO**. Sim 36 mesi, €600, max 6 conti,
|
||||||
|
2 firm, regole vere, payout mensile prelevato subito, fisco 33%, **morte-firm 10%/anno**,
|
||||||
|
bootstrap CONGIUNTO (corr reali). **Vince il MISTO, non gli estremi** (close-only, leva 1.0x):
|
||||||
|
MISTO mediana €10.70/g, P(≥50/g) 20.7%, P(zero) 33.4% > CONC-DIV 5.70/17.8%/33.4% > SPARSO
|
||||||
|
0.00/16.8%/**66.0%** > **CONC-2SL (= il book live attuale) 0.00/13.1%/55.6% = la PEGGIORE**.
|
||||||
|
Logica: ogni conto serve Sharpe per sopravvivere alla PROPRIA barriera (uccide SPARSO), ma i
|
||||||
|
conti servono decorrelati fra loro (penalizza CONC). **Con lente INTRADAY (wick)** tutto crolla:
|
||||||
|
P(≥50/g) 1.6-2.5%, P(zero) 78-90%. ⚠ Il mio wick e' un **PAVIMENTO**: gap estratto INDIPENDENTE
|
||||||
|
dal rendimento del giorno (i wick veri sono accoppiati alle giornate brutte) → breach spuri; il
|
||||||
|
modo giusto e' il bootstrap delle TUPLE (ritorno,gap) di `r0724_goal50_intraday_mc.py`, che pero'
|
||||||
|
ha il recon solo per TP01+SKH01 → **follow-up dichiarato**. **VERDETTO IN BANDA (risposta al goal):
|
||||||
|
una via da €600 a €50/g ESISTE (scala di conti funded) con P ~2-21% in 3 anni e P(bruciare i €600)
|
||||||
|
~33-78% → scommessa a coda destra, NON una rendita.** Il confronto fra POLITICHE e' robusto (stessa
|
||||||
|
lente per tutte): **se si apre il fronte prop, NON mandarci il book live attuale**.
|
||||||
|
(6) **ADDENDUM "e se metto 10K su IB?"** (`r0725_ib10k.py`): allocazione fra venue, non domanda su
|
||||||
|
GTAA01. Deribit = **motore** (CAGR ~11% de-luck), IB/GTAA01 = **diversificatore** (CAGR ~3.9%).
|
||||||
|
A $11.5k totali: 0% su IB → **€2.16/g**; 50% → €1.54/g (Sharpe migliore 1.13, DD 15.3→11.2%);
|
||||||
|
**94% (= i 10k su IB) → €0.93/g = REDDITO PIU' CHE DIMEZZATO**. La via d'uscita (levare per
|
||||||
|
convertire lo Sharpe in reddito) e' **chiusa dai costi**: su ETF a IB con €10k c'e' Reg-T **2x max**
|
||||||
|
(portfolio margin da ~$110k) e il margine costa **~5.5%/anno** → contando il finanziamento vince
|
||||||
|
**tutto-Deribit a 1.33x** (rendita €1.33/g vs €0.92/g a 50/50). **REGOLA: l'argomento iso-rischio
|
||||||
|
(punto 3) vale solo se la leva e' (a) disponibile e (b) a costo < uplift — su un retail da €10k su
|
||||||
|
ETF non lo e'.** Confronto che ridimensiona GTAA01: €10k nello sleeve = €0.93/g con maxDD 10.5%,
|
||||||
|
gli stessi €10k FERMI a ~2% = €0.54/g con maxDD ~0% → **premio ~€0.50/g pagato con un DD del 10%**.
|
||||||
|
Raccomandazione: i depositi vanno su **Deribit**; al massimo **25% su IB** (costa ~€0.08/g di
|
||||||
|
rendita, taglia il maxDD 15.3→12.0%, unico split che regge coi costi). ⚠ Il confronto FAVORISCE il
|
||||||
|
crypto per costruzione (7 anni con 2 bull vs 30 anni di GTAA); tassi e aliquote (26%/33%) sono
|
||||||
|
assunzioni dichiarate, da confermare col commercialista (non un parere fiscale).
|
||||||
|
(7) **HYROTRADER nello specifico** (`r0725_hyro.py`): la firm meglio classificata per questo book
|
||||||
|
(API reale da funded, nessun limite di tempo, **weekend consentito** — e il weekend porta il 38%
|
||||||
|
del gross di TP01). **Ipotesi REFUTATA: la regola di consistency 40% NON morde** (costo ≈0pp a
|
||||||
|
ogni leva/lente) perche' il book accumula il 10% in 130-300 giorni → nessun giorno si avvicina al
|
||||||
|
40% del cumulato; ⚠ prima di dichiararlo ho dovuto **provare che il controllo funziona** (un
|
||||||
|
"costo 0" puo' essere un bug): su un book con profitto concentrato in 1-2 giorni la regola porta
|
||||||
|
P(pass) da >90% a <5% (test cablato). **Il vincolo vero e' il maxDD 6% STATICO**: config A (book
|
||||||
|
live) ha maxDD 9.5% > 6% = strutturalmente incompatibile a leva piena — intraday a 1.0x
|
||||||
|
sopravvive l'**1.4%**; config B (+XS01) ha maxDD 6.2%, Sharpe 1.13. **Funded consigliato: config B
|
||||||
|
a 0.50x** (intraday P(vivo) 71% vs 32% a 0.75x, E[payout] €7.69 vs €8.66/g — la sopravvivenza
|
||||||
|
COMPONE su piu' anni). EV del biglietto $100k **positivo in entrambe le lenti** (+$6.789
|
||||||
|
close-only, +$1.166 intraday), ma ~69% di perdere la fee nella lente pessimista. Cap $200k/trader
|
||||||
|
⇒ ~€15-30/g max → per €50/g servono piu' firm (punto 5). ⚠ Discrepanza col 24/07 (P(vivo) A@0.75x
|
||||||
|
58% vs 10% mio): mio wick indipendente = piu' severo + finestra 2024+ dove A e' debole; **la
|
||||||
|
direzione concorda, sul LIVELLO il numero del 24/07 (recon MTM vero) e' piu' affidabile**.
|
||||||
|
⚠ Bug catturato in sessione: base di prelievo funded aggiornata giornalmente come HWM mobile →
|
||||||
|
guadagno al checkpoint ≈ 0 → **payout $230/anno invece di ~$7.000**; regole vere = max-loss STATICO
|
||||||
|
dal saldo iniziale + base ripristinata dopo il prelievo. Test di regressione cablato.
|
||||||
- **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso:
|
- **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso:
|
||||||
cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55.
|
cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55.
|
||||||
- **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.**
|
- **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.**
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
# 2026-07-25 — Rendita passiva: la curva capitale→book, il muro vero, e l'asimmetria del funded
|
||||||
|
|
||||||
|
**Contesto.** Goal: "altra sessione di ricerca per arrivare allo scopo (rendita passiva, 50 EUR/g),
|
||||||
|
i 600 euro si possono usare come si vuole". La roadmap del 24/07 aveva già chiuso i *canali*
|
||||||
|
(prop / vault / copy / legale). Questa ondata attacca i due punti che quella roadmap **assumeva**
|
||||||
|
invece di calcolare: (a) il tasso di crescita del book è preso come **CAGR fisso 15%**, (b) il
|
||||||
|
capitale-rendita è calcolato come **target/CAGR**, cioè ignorando il rischio di sequenza.
|
||||||
|
Entrambe le assunzioni sono risultate sbagliate, e nella stessa direzione: ottimista.
|
||||||
|
|
||||||
|
Script: `scripts/research/r0725_capcurve.py`, `scripts/research/r0725_prop_config.py`.
|
||||||
|
Test: `tests/test_capcurve.py` (10 casi). **Book live, pesi e config: INVARIATI.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) Difetto di eseguibilità su GTAA01 — il 5° sleeve del book attivo
|
||||||
|
|
||||||
|
`src/portfolio/gtaa.py` modella il costo come **2bps proporzionali** e lo sleeve è documentato
|
||||||
|
come *"eseguibile su IB a basso capitale (ETF frazionabili, switch mensile/basso turnover)"*.
|
||||||
|
Entrambe le cose sono false come codificate:
|
||||||
|
|
||||||
|
- il segnale è un vol-target **continuo**: l'esposizione cambia **ogni giorno** su **6 gambe**;
|
||||||
|
- IB non ha una fee proporzionale ma un **pavimento FISSO per ordine**:
|
||||||
|
`min(max($0.35, $0.0035/azione), 1% del controvalore)` → ~**$530/anno di commissioni
|
||||||
|
indipendenti dal capitale**.
|
||||||
|
|
||||||
|
| capitale allocato a GTAA | CAGR reale | Sharpe reale | drag commissioni |
|
||||||
|
|---|---|---|---|
|
||||||
|
| $600 | **−3.5%** | **−0.55** | 9.0%/anno |
|
||||||
|
| $2.000 | −0.7% | −0.09 | 6.2%/anno |
|
||||||
|
| $5.000 | +1.5% | 0.28 | 4.0%/anno |
|
||||||
|
| $50.000 | +3.4% | 0.59 | 2.1%/anno |
|
||||||
|
| *modellato (2bps)* | *+5.5%* | *0.77* | — |
|
||||||
|
|
||||||
|
**La banda lo salva.** Con costo fisso la banda ottimale non è il minimo eseguibile (come su
|
||||||
|
Deribit) ma un valore molto più alto. Plateau robusto = **cadenza settimanale × banda $25-100**
|
||||||
|
→ Sharpe 0.45-0.64 a *ogni* capitale; a $600 la config scelta sul plateau (banda $50, weekly)
|
||||||
|
dà **Sh 0.52 / CAGR 3.0%** invece di −0.55/−3.5%. (La cella argmax a $600 era banda $50 *daily*
|
||||||
|
a 0.57, ma è isolata — crolla a 0.28 a banda 100 → scartata: il parametro è di costo, si sceglie
|
||||||
|
sul plateau, non sull'argmax in-sample.)
|
||||||
|
|
||||||
|
**Due conseguenze.**
|
||||||
|
1. Se GTAA01 va live su IB **deve** avere banda+cadenza cablate. Come sta ora, a $600-2k è un
|
||||||
|
distruttore di capitale.
|
||||||
|
2. Lo Sharpe **onesto** di GTAA01 è **~0.55-0.64, non 0.77** → i numeri headline del book a 5
|
||||||
|
sleeve vanno citati con questo haircut (in aggiunta all'anchor-luck già documentato).
|
||||||
|
|
||||||
|
**Regola nuova (generale): il costo di un venue va modellato nella sua FORMA — fisso vs
|
||||||
|
proporzionale — non solo nel suo livello.** Due venue con lo stesso "costo medio" danno
|
||||||
|
risultati **opposti** al variare del capitale: la fee proporzionale è neutrale alla taglia, il
|
||||||
|
pavimento fisso è una tassa regressiva che si mangia i piccoli. Ogni sleeve nuovo va incrociato
|
||||||
|
col modello di costo del suo venue *prima* di dichiararlo eseguibile.
|
||||||
|
|
||||||
|
**Controprova su Deribit (fee proporzionale, nessun pavimento):** lo Sharpe realistico di TP01
|
||||||
|
coincide col modellato da ~$500 in su (1.29 a $1k = 1.29 a $50k) → conferma indipendente della
|
||||||
|
lezione già a libro *"a $600 il min-order $5 è già la banda ottimale"*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) La rendita NON è `capitale × CAGR` — il muro del 24/07 era ottimista di 2-4×
|
||||||
|
|
||||||
|
Una rendita passiva si preleva **mentre** il book è in drawdown: è rischio di sequenza, e
|
||||||
|
`target/CAGR` non lo vede. Calcolato col bootstrap a blocchi (20g) dei ritorni **reali** del book
|
||||||
|
(TP01 75 / SKH01 25, SKH01 sul **path live** orario, non sul backtest):
|
||||||
|
|
||||||
|
| lente | Sharpe | CAGR | SWR 20a (95% sopravv.) | **rendita perpetua** | capitale per 50 EUR/g |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| modellato (backtest) | 1.75 | 20.5% | 13.10% | 13.57% | $219k |
|
||||||
|
| path live | 1.64 | 19.7% | 12.48% | 12.78% | $232k |
|
||||||
|
| **path live de-luck ×0.6** | **0.98** | **11.1%** | **7.31%** | **5.98%** | **$497k** |
|
||||||
|
|
||||||
|
*"Rendita perpetua"* = prelievo massimo con **P(capitale a 20 anni ≥ capitale iniziale) ≥ 90%**.
|
||||||
|
È il criterio giusto: "sopravvivere 20 anni" non è una rendita, è un piano di consumo del capitale.
|
||||||
|
|
||||||
|
**Il muro vero per 50 EUR/g perpetui è $300k-$500k**, non i €122k del diario 24/07. L'errore si
|
||||||
|
componeva di due fattori nella stessa direzione: (a) `capital×CAGR` ignora la sequenza (−45%
|
||||||
|
circa sul tasso), (b) CAGR 15% invece del de-luckato 11%.
|
||||||
|
|
||||||
|
⚠️ **Sul de-luck, onestà metodologica:** il ×0.6 sul drift è la convenzione ereditata dal 24/07,
|
||||||
|
ma qui è applicato **sopra** il path live orario, che *già* incorpora la degradazione di SKH01
|
||||||
|
(fill al livello → fill orario). C'è quindi un rischio concreto di **doppio conteggio** della
|
||||||
|
stessa penalità. Il muro va perciò letto come una **banda**, non come un punto:
|
||||||
|
**$232k (path live, nessun de-luck ulteriore) — $497k (path live + de-luck ×0.6)**, con la stima
|
||||||
|
centrale ragionevolmente attorno a **$300-400k**. Ciò che NON dipende dal de-luck è il risultato
|
||||||
|
strutturale: la rendita perpetua vale ~**45-55% del CAGR**, non il 100% — quindi qualunque muro
|
||||||
|
calcolato come `target/CAGR` è sbagliato per un fattore ~2, indipendentemente dalla lente.
|
||||||
|
|
||||||
|
Al capitale attuale: **$600 → ~€0.11/giorno** di rendimento atteso, ~€0.06/g come rendita perpetua.
|
||||||
|
|
||||||
|
*(Fisco incluso: 50 EUR/g netti = €18.250/anno netti = **$29.690/anno lordi** al 33%.)*
|
||||||
|
|
||||||
|
**Validazione della macchineria** (obbligatoria, 3 casi a risposta nota): asset deterministico
|
||||||
|
al 10%/anno → rendita perpetua fra 9% e 10% ✓; random walk a drift zero → rendita 0 ✓; book a
|
||||||
|
prelievo nullo → P(rovina)=0 e mediana 37.6× in 20 anni ≈ 1.197²⁰ ✓.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) La diversificazione non crea reddito a pari nozionale — libera budget di rischio
|
||||||
|
|
||||||
|
Il test che conta, per tier di sblocco (tutti de-luck ×0.6, GTAA01 a costo IB reale con banda):
|
||||||
|
|
||||||
|
| tier | Sharpe | vol | rendita @iso-nozionale | capitale | @ISO-RISCHIO (vol 15%) | capitale |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| $600 — 2 sleeve | 0.98 | 11.4% | 5.98% | $497k | 1.32× → **7.51%** | $395k |
|
||||||
|
| $2k — + GTAA01 | 1.02 | 9.2% | 5.39% | $551k | 1.64× → 7.92% | $375k |
|
||||||
|
| $20k — + XS01 | 1.11 | 9.3% | 6.28% | $472k | 1.61× → **9.25%** | **$321k** |
|
||||||
|
| $20k — + XS01 + VRP01 (contro-fattuale) | 1.15 | 8.8% | 6.35% | $467k | 1.70× → 9.82% | $302k |
|
||||||
|
|
||||||
|
**A pari nozionale il muro NON si abbassa** (5.98% → 6.35%: nulla). Un diversificatore a basso
|
||||||
|
CAGR toglie vol e ritorno insieme. **A iso-rischio sì**: −19% di muro ($395k → $321k).
|
||||||
|
|
||||||
|
**Regola: un diversificatore a basso CAGR si giudica a ISO-RISCHIO, mai a iso-nozionale.** È il
|
||||||
|
"null de-levering" del progetto applicato al contrario — lì si smascherava un finto taglio di DD
|
||||||
|
comprato con la size, qui si smaschera un finto *mancato* guadagno per non aver rilevato la size.
|
||||||
|
|
||||||
|
*(L'ipotesi di partenza — "più capitale → più sleeve → CAGR super-lineare" — è quindi **refutata**
|
||||||
|
nella forma forte: la curva Sharpe del book eseguibile è **piatta** da $600 a $200k, perché TP01 su
|
||||||
|
Deribit non ha pavimento di costo e SKH01 nemmeno. Sopravvive solo nella forma debole, via iso-rischio.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) Traiettoria onesta (bootstrap, de-luck, target $497k)
|
||||||
|
|
||||||
|
| deposito | p10 | mediana | p90 | P(entro 10a) | P(entro 20a) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| €0/mese | — | — | — | 0% | **0%** |
|
||||||
|
| €250/mese | 18.9a | 22.4a | 24.6a | 0% | 8% |
|
||||||
|
| €500/mese | 15.6a | 19.6a | 23.4a | 0% | 48% |
|
||||||
|
| €1.000/mese | 11.6a | 14.8a | 18.7a | 2% | 95% |
|
||||||
|
| €2.000/mese | 8.1a | 10.1a | 12.8a | 48% | 100% |
|
||||||
|
|
||||||
|
Senza depositi, da $600, la rendita da 50 EUR/g **non arriva mai** (0% dei path in 20 anni).
|
||||||
|
Il moltiplicatore è il deposito, non il rendimento: è la stessa conclusione del 24/07, ma su un
|
||||||
|
target 4× più alto e quindi con tempi ~2× più lunghi.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) L'ASIMMETRIA — il risultato strategico della sessione
|
||||||
|
|
||||||
|
Il 24/07 ha valutato il fronte prop **solo col book a 2 sleeve** (quello eseguibile a $600). Ma su
|
||||||
|
un conto funded il capitale non è $600, è **$100k** — e gli sleeve che a casa nostra sono STAT-MODE
|
||||||
|
*per taglia* (XS01 serve ~$20k) **diventano eseguibili**. Le regole prop, inoltre, passano sul
|
||||||
|
**drawdown**, non sul CAGR: premiano esattamente ciò che la diversificazione produce.
|
||||||
|
|
||||||
|
Finestra comune 2024+, de-luck ×0.6, crypto-only (niente GTAA01 che è equity su IB, niente VRP01):
|
||||||
|
|
||||||
|
| | P(pass) HYRO 1.0× | P(pass) FTMO 1.5× | **funded HYRO 1.0×: P(vivo 1a)** | funded FTMO 1.0×: P(vivo) | E[payout] FTMO | EUR/g atteso |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| A: TP01/SKH01 (live oggi) | 44.4% | 51.9% | **18.9%** | 58.8% | $8.655 | 14.58 |
|
||||||
|
| B: + XS01 (19 alt perp) | **54.7%** | **70.0%** | **57.6%** | **92.0%** | **$9.333** | **15.72** |
|
||||||
|
|
||||||
|
**Il salto è nella sopravvivenza: 18.9% → 57.6% (3×) sul conto funded HYRO a leva 1.0×.** E la
|
||||||
|
raccomandazione del 24/07 ("funded a 0.75×, mai 1.0×") si **ribalta** col book diversificato: su
|
||||||
|
FTMO a 1.0× la sopravvivenza è 92%.
|
||||||
|
|
||||||
|
**L'asimmetria in una riga:** sul **capitale proprio** diversificare non aumenta il reddito (il
|
||||||
|
reddito è `C × tasso`, e il tasso sale poco); su un **conto funded** lo aumenta molto, perché lì
|
||||||
|
il vincolo binding non è il capitale ma la **regola di drawdown**. → **Gli sleeve "inutili a $600
|
||||||
|
perché STAT-MODE" sono esattamente gli asset di maggior valore sull'unico canale che scala in mesi.**
|
||||||
|
Questo inverte la priorità implicita di dieci ondate di ricerca.
|
||||||
|
|
||||||
|
**Caveat forte, dichiarato.** (1) Il MC è **close-only**: C-bis del 24/07 ha dimostrato che i wick
|
||||||
|
intraday tagliano 6-37pp → **i livelli assoluti qui sono un TETTO**. Il ricostruttore MTM intraday
|
||||||
|
esiste solo per TP01+SKH01 (feed 5m); XS01 è 1d nativo su Hyperliquid e il suo intraday non è
|
||||||
|
ricostruibile dai nostri dati → non l'ho inventato. Il **delta** fra config è la misura onesta ed
|
||||||
|
è **conservativo** (B ha vol minore → wick minori → haircut minore). (2) La finestra comune 2024+
|
||||||
|
è **quella in cui XS01 è stato scoperto e affinato** → la *taglia* del guadagno è ottimista; sulla
|
||||||
|
finestra piena il delta si riduce (P(pass) 56.9%→58.5%, P(vivo) 14.9%→21.8%) ma quella è
|
||||||
|
sfavorevole a B per costruzione (pre-2024 B degenera in A). Il **meccanismo** (corr bassa → meno
|
||||||
|
DD → più sopravvivenza sotto vincolo di DD) è robusto; la sua taglia no.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6) LA VIA — i 600 euro come BIGLIETTO, non come capitale (`r0725_prop_ladder.py`)
|
||||||
|
|
||||||
|
Se i €600 come *capitale* sono refutati (§2, §4), restano i €600 come **biglietto**: si compra
|
||||||
|
un'evaluation, il conto funded moltiplica il **nozionale** senza possedere capitale, i payout
|
||||||
|
comprano altri biglietti. È l'unica leva che non richiede depositi. Il 24/07 si era fermato a
|
||||||
|
**un** conto ($100k → ~€14.5/g, cap $200k/firm). Ma €50/g richiede **più conti in parallelo** —
|
||||||
|
e qui c'è il problema che nessuno aveva studiato:
|
||||||
|
|
||||||
|
> **N conti che girano lo stesso book bustano INSIEME.** La diversificazione fra conti è
|
||||||
|
> illusoria salvo assegnare **sleeve diversi a conti diversi**.
|
||||||
|
|
||||||
|
È un problema di costruzione di portafoglio sotto **barriera di rovina per-conto**, e non è ovvio
|
||||||
|
quale verso vinca: concentrare alza lo Sharpe *per conto* (sopravvivenza individuale) ma rende le
|
||||||
|
morti perfettamente correlate; sparpagliare decorrela le morti ma ogni conto ha Sharpe più basso
|
||||||
|
e quindi buca più spesso la propria barriera.
|
||||||
|
|
||||||
|
Simulazione: 36 mesi, cassa iniziale €600, max 6 conti, 2 firm (cap $200k/$400k), regole vere,
|
||||||
|
payout mensile 80% prelevato subito, fisco 33%, **rischio-morte firm 10%/anno**, bootstrap a
|
||||||
|
blocchi **congiunto** dei 3 sleeve (la correlazione fra conti è quella *reale*: TP01–SKH01 +0.10,
|
||||||
|
TP01–XS01 −0.12, SKH01–XS01 −0.00).
|
||||||
|
|
||||||
|
| politica (leva 1.0×) | EUR/g mediano | p90 | P(≥10/g) | **P(≥50/g)** | P(zero) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| CONC-2SL — ogni conto = book live attuale | 0.00 | 63.2 | 26.1% | 13.1% | 55.6% |
|
||||||
|
| CONC-DIV — ogni conto = book diversificato | 5.70 | 70.6 | 44.9% | 17.8% | 33.4% |
|
||||||
|
| SPARSO — 1 sleeve per conto | 0.00 | 76.3 | 26.2% | 16.8% | **66.0%** |
|
||||||
|
| **MISTO — metà diversificati, metà single** | **10.70** | 75.1 | **50.6%** | **20.7%** | 33.4% |
|
||||||
|
|
||||||
|
*(lente close-only)*
|
||||||
|
|
||||||
|
**Risultato non ovvio: vince il MISTO, non gli estremi.** Ogni conto ha bisogno di abbastanza
|
||||||
|
Sharpe per sopravvivere alla *propria* barriera (questo uccide SPARSO: P(zero) 66%), ma i conti
|
||||||
|
hanno anche bisogno di essere decorrelati fra loro (questo penalizza CONC). E il **book live
|
||||||
|
attuale a 2 sleeve è la politica peggiore** di tutte.
|
||||||
|
|
||||||
|
**La lente onesta però è più dura.** Le regole prop scattano sull'equity *intraday*. Con i wick
|
||||||
|
(lognormale calibrato sui percentili del recon MTM 1h del 24/07: p50 −0.17pp, p90 −0.90pp,
|
||||||
|
p99 −3.50pp, scalati sulla vol di ogni config):
|
||||||
|
|
||||||
|
| politica (leva 1.0×, INTRADAY) | EUR/g mediano | p90 | P(≥10/g) | **P(≥50/g)** | P(zero) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| CONC-2SL | 0.00 | 2.18 | 4.8% | 1.7% | 85.1% |
|
||||||
|
| CONC-DIV | 0.00 | 7.34 | 9.0% | **2.5%** | 78.1% |
|
||||||
|
| SPARSO | 0.00 | 0.02 | 1.3% | 0.3% | 89.9% |
|
||||||
|
| MISTO | 0.00 | 4.89 | 6.6% | 1.6% | 78.1% |
|
||||||
|
|
||||||
|
⚠️ **Il mio modello di wick è un PAVIMENTO, non una stima puntuale**: il gap è estratto
|
||||||
|
**indipendente dal rendimento del giorno**, mentre i wick veri sono *accoppiati* alle giornate
|
||||||
|
brutte → estrarli indipendenti aggiunge finti tuffi anche nelle giornate buone, cioè breach spuri.
|
||||||
|
Il modo corretto è il bootstrap delle **tuple (ritorno, gap)** come in `r0724_goal50_intraday_mc.py`,
|
||||||
|
che però ha il recon solo per TP01+SKH01 (XS01 è 1d nativo su Hyperliquid, intraday non
|
||||||
|
ricostruibile dai nostri dati) → **follow-up dichiarato, non simulato**.
|
||||||
|
|
||||||
|
**Verdetto in banda (ed è la risposta al goal):**
|
||||||
|
> Una via da €600 a €50/giorno **esiste** ed è la scala di conti funded. In 3 anni ha una
|
||||||
|
> probabilità **fra il 2% e il 21%** di arrivarci, e una probabilità **fra il 33% e il 78%** di
|
||||||
|
> bruciare interamente i €600. Il valore atteso è positivo rispetto ai €600 tenuti come capitale
|
||||||
|
> (~€0.11/g ⇒ ~€120 in 3 anni), ma è una **scommessa a coda destra, non una rendita**.
|
||||||
|
|
||||||
|
Il confronto fra *politiche* è invece robusto (stessa lente per tutte) e ha un'implicazione
|
||||||
|
diretta: **se si apre il fronte prop, non mandarci il book live attuale** — è l'ultima delle
|
||||||
|
quattro politiche in entrambe le lenti.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7) ADDENDUM — "e se metto 10K su IB?" (`r0725_ib10k.py`)
|
||||||
|
|
||||||
|
Domanda dell'operatore a valle dell'ondata. Non è una domanda su GTAA01: è **allocazione fra due
|
||||||
|
venue** con profili opposti — Deribit (TP01+SKH01) è il **motore** (CAGR ~11% de-luck, vol 11.3%),
|
||||||
|
IB (GTAA01) è un **diversificatore** (CAGR ~3.9%, vol ~5%, corr ~+0.10). Tre effetti da contare
|
||||||
|
insieme: (a) il deposito **sblocca** GTAA01 (il pavimento di commissione passa dall'1-3% per ordine
|
||||||
|
a ~0.02% → §1 risolto); (b) sposta il baricentro su un venue a CAGR basso; (c) cambia l'**aliquota**
|
||||||
|
(ETF 26% vs crypto 33%, assunzione dichiarata).
|
||||||
|
|
||||||
|
Capitale totale $11.500 ($600 Deribit + €10k), de-luck ×0.6, SKH01 path live:
|
||||||
|
|
||||||
|
| quota su IB | Sharpe | CAGR | maxDD | €/g atteso | rendita perpetua |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 0% (tutto Deribit) | 0.99 | 11.2% | 15.3% | **2.16** | €1.16/g |
|
||||||
|
| 25% | 1.06 | 9.3% | 12.0% | 1.85 | €1.08/g |
|
||||||
|
| 50% | **1.13** | 7.5% | 11.2% | 1.54 | €0.98/g |
|
||||||
|
| 94% (= i 10k su IB) | 0.84 | 4.4% | 10.5% | **0.93** | €0.61/g |
|
||||||
|
|
||||||
|
**Mettere i 10k su IB più che dimezza il reddito** (€0.93 vs €2.16/g): è la §3 di questo stesso
|
||||||
|
diario vista dal lato pratico — un diversificatore a basso CAGR diluisce l'income a pari nozionale.
|
||||||
|
|
||||||
|
**E la via d'uscita (levare) è chiusa dai costi.** Lo Sharpe migliore (1.13 a 50/50) si converte in
|
||||||
|
reddito solo con leva; ma su ETF a IB con €10k c'è la **Reg-T (2× max**, il portfolio margin parte
|
||||||
|
da ~$110k) e il margine costa **~5.5%/anno**. Contando il finanziamento:
|
||||||
|
|
||||||
|
| quota IB | leva attuabile | CAGR lordo | costo leva | CAGR netto | rendita |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **0%** | 1.33× | 14.8% | 0.7% | **14.1%** | **€1.33/g** |
|
||||||
|
| 25% | 1.71× | 15.9% | 2.0% | 13.9% | €1.25/g |
|
||||||
|
| 50% | 2.27× | 17.1% | 4.8% | 12.4% | €0.92/g |
|
||||||
|
| 94% | 2.06× | 9.0% | 5.6% | 3.4% | €0.00/g |
|
||||||
|
|
||||||
|
→ **con leva attuabile E costo del margine, vince tutto-Deribit a 1.33×.** Il diversificatore
|
||||||
|
permetterebbe di levare di più, ma il finanziamento costa più di quanto aggiunge.
|
||||||
|
**Regola: l'argomento iso-rischio (§3) vale solo se la leva è (a) disponibile e (b) a costo
|
||||||
|
inferiore all'uplift — su un conto retail da €10k su ETF non lo è.**
|
||||||
|
|
||||||
|
**Il confronto che ridimensiona GTAA01:** €10k in GTAA01 = **€0.93/g con maxDD 10.5%**; gli stessi
|
||||||
|
€10k **fermi** a interesse ~2% = **€0.54/g con maxDD ~0%**. Il premio dello sleeve sopra la
|
||||||
|
liquidità è **~€0.50/g pagato con un drawdown del 10%** — in isolamento un cattivo affare (e un
|
||||||
|
monetario/BTP EUR renderebbe più del 2% assunto).
|
||||||
|
|
||||||
|
**Raccomandazione:** se arrivano €10k, il posto produttivo è **Deribit**. Chi vuole meno drawdown
|
||||||
|
può tenere **25% su IB**: costa ~€0.08/g di rendita e taglia il maxDD dal 15.3% al 12.0% — è
|
||||||
|
l'unico split che regge anche coi costi di finanziamento. Il 94% su IB no.
|
||||||
|
|
||||||
|
⚠️ **Caveat che rende il confronto meno netto di come appare:** il CAGR crypto (11%) viene da ~7
|
||||||
|
anni che contengono **due bull market**; il 3.9% di GTAA01 da ~30 anni con dentro più orsi. Il
|
||||||
|
confronto **favorisce il crypto per costruzione** — il numero crypto ha più probabilità di deludere.
|
||||||
|
Tassi (margine 5.5%, liquidità 2%) e aliquote (26%/33%) sono assunzioni dichiarate; le aliquote
|
||||||
|
vanno confermate col commercialista (**non è un parere fiscale**).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8) HYROTRADER nello specifico (`r0725_hyro.py`)
|
||||||
|
|
||||||
|
La firm che il 24/07 aveva messo prima in classifica per *questo* book: unica con **API reale** da
|
||||||
|
funded (sub-account Bybit) → il book gira automatico; **nessun limite di tempo** sull'eval (una
|
||||||
|
strategia lenta non è penalizzata); **weekend consentito** — non banale, l'ondata 2026-07-17 ha
|
||||||
|
misurato che il weekend porta il **38% del gross di TP01**. Biglietti $25k/$249, $50k/$379,
|
||||||
|
$100k/$579, cap $200k, split 80%, fee rimborsata al pass.
|
||||||
|
|
||||||
|
Due regole mordono proprio noi, e nessuna analisi le aveva modellate: **maxDD 6% STATICO** e
|
||||||
|
**consistency 40%** (nessun giorno > 40% del profitto totale, in eval).
|
||||||
|
|
||||||
|
**Ipotesi refutata: la consistency NON morde.** Avevo previsto una tagliola (SKH01 ha P&L a
|
||||||
|
scalino, TP01 fa strappi di trend). Misurato: **costo ≈ 0pp** a ogni leva e in entrambe le lenti.
|
||||||
|
Ragione strutturale: il book accumula il 10% in **130-300 giorni**, quindi nessun singolo giorno
|
||||||
|
si avvicina al 40% del cumulato. ⚠️ Prima di dichiararlo ho dovuto **provare che il controllo
|
||||||
|
funziona** (un "costo 0" può essere un bug): `test_consistency_MORDE_su_un_book_grumoso` mostra
|
||||||
|
che su un book con profitto concentrato in 1-2 giorni la regola porta P(pass) da >90% a <5%. Il
|
||||||
|
meccanismo c'è; è il book a non essere grumoso abbastanza. *(Il primo probe era sbagliato — strappi
|
||||||
|
ripetuti si diluiscono a vicenda, e sim_eval fa block-bootstrap, non replay: due iterazioni di test
|
||||||
|
prima di avere un discriminante valido.)*
|
||||||
|
|
||||||
|
**Il vincolo vero è il DD 6% statico.** Finestra comune 2024+, de-luck ×0.6:
|
||||||
|
|
||||||
|
| | Sharpe | CAGR | vol | maxDD |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| A — book live (TP01/SKH01) | 0.74 | 7.2% | 10.1% | 9.5% |
|
||||||
|
| B — + XS01 | **1.13** | 10.0% | 8.8% | **6.2%** |
|
||||||
|
|
||||||
|
Config A ha maxDD 9.5% contro un limite di 6%: **strutturalmente incompatibile a leva piena**.
|
||||||
|
|
||||||
|
**Funded $100k, P(vivo 1 anno) / €-giorno:**
|
||||||
|
|
||||||
|
| config | leva | close-only (tetto) | intraday (pavimento) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| A | 0.50× | 80.2% / €8.33 | 44.0% / €6.86 |
|
||||||
|
| A | 0.75× | 51.1% / €12.21 | **10.1%** / — |
|
||||||
|
| A | 1.00× | 27.1% / €13.89 | **1.4%** / — |
|
||||||
|
| B | 0.50× | 98.5% / €8.69 | **71.0%** / €7.69 |
|
||||||
|
| B | 0.75× | 85.4% / €12.78 | 31.6% / €8.66 |
|
||||||
|
| B | 1.00× | 66.0% / €16.20 | 9.9% / — |
|
||||||
|
|
||||||
|
**EV del biglietto** (config B, eval 1.0×, funded a leva ottima, 1 anno): close-only P(pass) 73.0%
|
||||||
|
→ EV **+$6.789** sul biglietto $100k; intraday P(pass) 31.1% → EV **+$1.166**. **Positivo in
|
||||||
|
entrambe le lenti** — ma con ~69% di probabilità di perdere la fee nella lente pessimista.
|
||||||
|
|
||||||
|
**Raccomandazione operativa:**
|
||||||
|
1. **Config B (con XS01), non il book live attuale.** A 1.0× intraday config A sopravvive l'1.4%:
|
||||||
|
mandarci il book a 2 sleeve a leva piena è morte quasi certa.
|
||||||
|
2. **Funded a 0.50×** (non 0.75× come indicava il 24/07 per la config A): E[payout] scende poco
|
||||||
|
(€7.69 vs €8.66/g) ma la sopravvivenza raddoppia (71% vs 32%) — e su orizzonti pluriennali la
|
||||||
|
sopravvivenza **compone**, un conto bustato smette di produrre per sempre.
|
||||||
|
3. Eval a 0.75-1.0×; biglietto $25-50k come test economico — l'EV scala **linearmente** con la
|
||||||
|
taglia, quindi la ragione per partire piccoli è varianza e apprendimento, non EV.
|
||||||
|
4. **Cap $200k/trader ⇒ ~€15-30/g anche negli scenari buoni.** Per €50/g servono più firm → §6.
|
||||||
|
|
||||||
|
⚠️ **Discrepanza dichiarata col 24/07:** lì il funded di config A a 0.75× dava P(vivo) 58%, qui
|
||||||
|
10%. Due cause note: (a) il mio wick è a estrazione indipendente (più severo — §6); (b) finestra
|
||||||
|
2024+, dove config A è debole (Sharpe 0.74 vs 0.99 sulla finestra piena). **La direzione concorda
|
||||||
|
(meno leva è meglio), il livello no** — e il numero del 24/07 usa il recon MTM vero, quindi sul
|
||||||
|
livello è più affidabile del mio.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug catturato in sessione
|
||||||
|
|
||||||
|
**Modello funded, base di prelievo aggiornata ogni giorno come un HWM mobile** → al checkpoint
|
||||||
|
mensile `eq > hwm` è quasi sempre falso → guadagno misurato ≈ 0 → **payout $230/anno invece di
|
||||||
|
~$7.000**. Le regole vere sono a max-loss **statico dal saldo iniziale**, con la base ripristinata
|
||||||
|
dopo ogni prelievo. Corretto e bloccato da `test_funded_payout_non_e_zero_regressione_hwm`.
|
||||||
|
Famiglia nota: numero plausibile, meccanismo sbagliato — si prende solo con una sanity a risposta nota.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisioni e non-decisioni
|
||||||
|
|
||||||
|
**Cablato:** nulla sul path live. Book, pesi, config, cron: **invariati**.
|
||||||
|
|
||||||
|
**Raccomandazioni all'operatore (non automatizzabili):**
|
||||||
|
1. **Se si apre il fronte prop** (l'unico canale che scala in mesi): mandarci il book
|
||||||
|
**diversificato** TP01+SKH01+XS01, non il book a 2 sleeve. È la differenza fra P(vivo) 19% e
|
||||||
|
58% a leva 1.0×. Richiede una venue con i 19 alt perp (Bybit sub-account HyroTrader li ha).
|
||||||
|
E se si arriva a più conti, **politica MISTA** (§6): metà conti col book diversificato, metà
|
||||||
|
con singoli sleeve decorrelati. Budget mentale onesto: **P(perdere tutto) 33-78%**.
|
||||||
|
2. **GTAA01 su IB**: non andare live senza banda+cadenza (weekly, banda ~$50/gamba). È un cambio
|
||||||
|
al modello dello sleeve → passa dai gate, non lo cablo in questa sessione.
|
||||||
|
3. **Aspettativa onesta ricalibrata**: rendita perpetua da 50 EUR/g = **$300k-$500k** di capitale
|
||||||
|
(non €122k). A $600 il book rende ~**€0.11/giorno**.
|
||||||
|
|
||||||
|
**Cosa NON fare (confermato):** niente cambio pesi senza `weights_tilt_null`; niente leva "per
|
||||||
|
accelerare" sul capitale proprio (il muro si sposta del 20%, non dell'ordine di grandezza).
|
||||||
@@ -0,0 +1,646 @@
|
|||||||
|
"""r0725_capcurve — LA CURVA CAPITALE -> BOOK ESEGUIBILE (2026-07-25).
|
||||||
|
|
||||||
|
Perche' questo script esiste. Il modello di crescita del 2026-07-24 (`r0724_goal50_math.py`
|
||||||
|
parte A) usa un **CAGR FISSO 15%** per stimare il tempo da $600 a 50 EUR/g. Ma il CAGR del
|
||||||
|
book NON e' una costante: a $600 girano 2 sleeve su 6 (TP01+SKH01 su Deribit), a $5k si
|
||||||
|
aggiunge XSR01, a ~$20k XS01, e GTAA01 (6 ETF su IB) ha un COSTO FISSO PER ORDINE che a
|
||||||
|
capitale piccolo se lo mangia. Il tasso di crescita e' quindi **funzione dello stato**
|
||||||
|
(equity), e la traiettoria vera e' super-lineare: piu' capitale -> piu' diversificazione ->
|
||||||
|
piu' Sharpe -> piu' CAGR.
|
||||||
|
|
||||||
|
Tre parti:
|
||||||
|
A) COST-FLOOR PER SLEEVE — Sharpe/CAGR realistici di ogni sleeve in funzione dei dollari
|
||||||
|
ALLOCATI a quello sleeve, con il modello di costo del SUO venue:
|
||||||
|
- Deribit (TP01, SKH01): fee proporzionale 0.05%/lato, min order $5 (skip, non trade).
|
||||||
|
- IB (GTAA01): commissione = min(max($0.35, $0.0035/azione), 1% del controvalore)
|
||||||
|
-> **pavimento FISSO per ordine**, mai modellato prima (gtaa.py usa 2bps proporzionali).
|
||||||
|
- Hyperliquid (XSR01): fee 0.045%/lato, min order $5, 50 gambe.
|
||||||
|
Novita' vs tutto il lavoro precedente: la curva GTAA, cioe' a che capitale il 3o sleeve
|
||||||
|
smette di essere un costo.
|
||||||
|
B) FRONTIERA — per ogni capitale totale C, il MIGLIOR sottoinsieme di sleeve eseguibili
|
||||||
|
(pesi canonici rinormalizzati), con Sharpe/CAGR/DD del book risultante. Da qui la
|
||||||
|
funzione CAGR(C) che sostituisce il 15% fisso.
|
||||||
|
C) CRESCITA STATE-DEPENDENT — traiettoria da $600 con CAGR(C), depositi mensili e fisco
|
||||||
|
italiano 2026 (33% sulle plusvalenze), vs il modello a CAGR fisso. Piu' il "premio di
|
||||||
|
sblocco": quanto vale anticipare un deposito che fa scattare una soglia.
|
||||||
|
|
||||||
|
ONESTA': (1) i rendimenti di SKH01 sono modellati equity-daily-step; il path live e' peggiore
|
||||||
|
(FULL 1.19/HOLD 1.15/DD 25% vs 1.46/1.64/18%) -> parte B gira anche in modalita' de-luck.
|
||||||
|
(2) Le soglie XS01/VRP01 sono quelle gia' documentate (non ricalcolate qui). (3) Il fisco e'
|
||||||
|
applicato come 33% sul realizzato annuo: e' l'ipotesi peggiore (nessuna compensazione).
|
||||||
|
|
||||||
|
Uso: `uv run python scripts/research/r0725_capcurve.py`
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from src.data.downloader import load_data # noqa: E402
|
||||||
|
from src.portfolio.gtaa import EQ_UNIVERSE, _close, _exposure # noqa: E402
|
||||||
|
from src.portfolio.portfolio import combine_outer, metrics # noqa: E402
|
||||||
|
from src.portfolio.sleeves import _skyhook_returns, _tp01_returns # noqa: E402
|
||||||
|
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d, simple_returns # noqa: E402
|
||||||
|
|
||||||
|
EURUSD = 1.09
|
||||||
|
TARGET_EUR_DAY = 50.0
|
||||||
|
TAX_RATE = 0.33 # L.199/2025: capital gain crypto 33% dal 2026, esenzione abolita
|
||||||
|
ASSETS = ("BTC", "ETH")
|
||||||
|
|
||||||
|
# --- modelli di costo per VENUE -------------------------------------------------
|
||||||
|
DERIBIT_FEE_SIDE = 0.0005 # taker 0.05%/lato (0.10% RT) - gia' il default del progetto
|
||||||
|
DERIBIT_MIN_ORDER = 5.0
|
||||||
|
IB_MIN_COMM = 0.35 # USD per ordine (IBKR Pro tiered, US stocks/ETF)
|
||||||
|
IB_PER_SHARE = 0.0035 # USD per azione
|
||||||
|
IB_MAX_FRAC = 0.01 # cap: 1% del controvalore dell'ordine
|
||||||
|
IB_MIN_TRADE = 1.0 # ordine frazionale minimo sensato in USD
|
||||||
|
HL_FEE_SIDE = 0.00045
|
||||||
|
HL_MIN_ORDER = 5.0
|
||||||
|
VOL_BUDGET = 0.15 # budget di rischio del confronto ISO-RISCHIO (vol annua)
|
||||||
|
|
||||||
|
CAP_GRID = (100.0, 200.0, 400.0, 600.0, 1_000.0, 2_000.0, 5_000.0, 10_000.0,
|
||||||
|
20_000.0, 50_000.0, 100_000.0, 200_000.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================== A) COST FLOOR
|
||||||
|
@lru_cache(maxsize=64)
|
||||||
|
def _tp01_realistic_cached(alloc: float) -> pd.Series:
|
||||||
|
"""TP01 con capitale ALLOCATO `alloc` (meta' per asset): un ribilanciamento il cui
|
||||||
|
nozionale |Dw|*cap_asset e' sotto min order NON si esegue (si tiene la posizione)."""
|
||||||
|
cap_asset = alloc / len(ASSETS)
|
||||||
|
tp = TrendPortfolio(**CANONICAL)
|
||||||
|
series = {}
|
||||||
|
for a in ASSETS:
|
||||||
|
df = resample_1d(load_data(a, "1h"))
|
||||||
|
r = simple_returns(df["close"].values.astype(float))
|
||||||
|
tgt = np.nan_to_num(np.asarray(tp.target_series(df), float))
|
||||||
|
held_tgt = np.empty(len(tgt))
|
||||||
|
cur = 0.0
|
||||||
|
for i in range(len(tgt)):
|
||||||
|
if abs(tgt[i] - cur) * cap_asset >= DERIBIT_MIN_ORDER:
|
||||||
|
cur = tgt[i]
|
||||||
|
held_tgt[i] = cur
|
||||||
|
pos = np.zeros(len(held_tgt))
|
||||||
|
pos[1:] = held_tgt[:-1] # causale: deciso a i-1, tenuto in i
|
||||||
|
net = pos * r - DERIBIT_FEE_SIDE * np.abs(np.diff(pos, prepend=0.0))
|
||||||
|
net[0] = 0.0
|
||||||
|
series[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"]))
|
||||||
|
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
|
||||||
|
out = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
|
||||||
|
if out.index.tz is None:
|
||||||
|
out.index = out.index.tz_localize("UTC")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=64)
|
||||||
|
def _gtaa_realistic_cached(alloc: float, universe=EQ_UNIVERSE) -> pd.Series:
|
||||||
|
"""GTAA con capitale ALLOCATO `alloc`, costo IB REALE (pavimento fisso per ordine).
|
||||||
|
|
||||||
|
gtaa.py modella 2bps proporzionali: a $600 su 6 gambe l'ordine tipico e' ~$10-30 e la
|
||||||
|
commissione IB minima ($0.35) e' l'**1-3%** del controvalore, cioe' 15-150x il modello.
|
||||||
|
Qui ogni gamba e' un conto separato: si ribilancia solo se il nozionale supera il minimo
|
||||||
|
frazionale, e si paga min(max(0.35, 0.0035*azioni), 1%*valore) A OGNI ordine.
|
||||||
|
"""
|
||||||
|
n = len(universe)
|
||||||
|
cap_leg = alloc / n
|
||||||
|
legs = {}
|
||||||
|
for sym in universe:
|
||||||
|
close = _close(sym)
|
||||||
|
ex = np.nan_to_num(np.asarray(_exposure(close).values, float))
|
||||||
|
px = close.values.astype(float)
|
||||||
|
ret = np.zeros(len(px))
|
||||||
|
ret[1:] = px[1:] / px[:-1] - 1.0
|
||||||
|
held = np.empty(len(ex))
|
||||||
|
cur = 0.0
|
||||||
|
comm = np.zeros(len(ex))
|
||||||
|
for i in range(len(ex)):
|
||||||
|
notional = abs(ex[i] - cur) * cap_leg
|
||||||
|
if notional >= IB_MIN_TRADE:
|
||||||
|
shares = notional / px[i]
|
||||||
|
c = min(max(IB_MIN_COMM, IB_PER_SHARE * shares), IB_MAX_FRAC * notional)
|
||||||
|
comm[i] = c / cap_leg # commissione in frazione del capitale-gamba
|
||||||
|
cur = ex[i]
|
||||||
|
held[i] = cur
|
||||||
|
pos = np.zeros(len(held))
|
||||||
|
pos[1:] = held[:-1]
|
||||||
|
net = pos * ret - comm
|
||||||
|
net[0] = 0.0
|
||||||
|
legs[sym] = pd.Series(net, index=close.index)
|
||||||
|
r = pd.concat(legs, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna()
|
||||||
|
if r.index.tz is None:
|
||||||
|
r.index = r.index.tz_localize("UTC")
|
||||||
|
days = pd.date_range(r.index.min().normalize(), r.index.max().normalize(), freq="1D", tz="UTC")
|
||||||
|
return r.reindex(days).fillna(0.0)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=256)
|
||||||
|
def _gtaa_banded_cached(alloc: float, band_usd: float, every: int = 1,
|
||||||
|
universe=EQ_UNIVERSE) -> pd.Series:
|
||||||
|
"""GTAA con BANDA di ribilanciamento e cadenza: si guarda il target solo ogni `every`
|
||||||
|
barre, e si esegue solo se il nozionale supera `band_usd`. Con un costo FISSO per ordine
|
||||||
|
la banda ottimale non e' il minimo eseguibile (come su Deribit) ma un valore molto piu'
|
||||||
|
alto: ogni ordine costa $0.35 a prescindere dalla taglia."""
|
||||||
|
n = len(universe)
|
||||||
|
cap_leg = alloc / n
|
||||||
|
legs = {}
|
||||||
|
for sym in universe:
|
||||||
|
close = _close(sym)
|
||||||
|
ex = np.nan_to_num(np.asarray(_exposure(close).values, float))
|
||||||
|
px = close.values.astype(float)
|
||||||
|
ret = np.zeros(len(px))
|
||||||
|
ret[1:] = px[1:] / px[:-1] - 1.0
|
||||||
|
held = np.empty(len(ex))
|
||||||
|
cur = 0.0
|
||||||
|
comm = np.zeros(len(ex))
|
||||||
|
for i in range(len(ex)):
|
||||||
|
if i % every == 0:
|
||||||
|
notional = abs(ex[i] - cur) * cap_leg
|
||||||
|
if notional >= max(band_usd, IB_MIN_TRADE):
|
||||||
|
shares = notional / px[i]
|
||||||
|
c = min(max(IB_MIN_COMM, IB_PER_SHARE * shares), IB_MAX_FRAC * notional)
|
||||||
|
comm[i] = c / cap_leg
|
||||||
|
cur = ex[i]
|
||||||
|
held[i] = cur
|
||||||
|
pos = np.zeros(len(held))
|
||||||
|
pos[1:] = held[:-1]
|
||||||
|
net = pos * ret - comm
|
||||||
|
net[0] = 0.0
|
||||||
|
legs[sym] = pd.Series(net, index=close.index)
|
||||||
|
r = pd.concat(legs, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna()
|
||||||
|
if r.index.tz is None:
|
||||||
|
r.index = r.index.tz_localize("UTC")
|
||||||
|
days = pd.date_range(r.index.min().normalize(), r.index.max().normalize(), freq="1D", tz="UTC")
|
||||||
|
return r.reindex(days).fillna(0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def part_a2() -> dict:
|
||||||
|
"""La banda salva GTAA? Sweep banda x cadenza per capitale. Il parametro e' di COSTO
|
||||||
|
(non di segnale): la scelta si fa sul costo atteso, non sullo Sharpe in-sample."""
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print(" A2) GTAA01 — la BANDA di ribilanciamento puo' salvarlo? (costo fisso IB $0.35/ordine)")
|
||||||
|
print("=" * 100)
|
||||||
|
from src.portfolio.gtaa import gtaa_returns
|
||||||
|
g_model = gtaa_returns()
|
||||||
|
if g_model.index.tz is None:
|
||||||
|
g_model.index = g_model.index.tz_localize("UTC")
|
||||||
|
print(f" riferimento MODELLATO (2bps propor., ribil. continuo): "
|
||||||
|
f"Sh {metrics(g_model)['sharpe']:.2f} CAGR {metrics(g_model)['cagr']:.1%}")
|
||||||
|
|
||||||
|
best = {}
|
||||||
|
for alloc in (600.0, 2_000.0, 5_000.0, 20_000.0):
|
||||||
|
print(f"\n --- capitale allocato a GTAA: ${alloc:,.0f} (per gamba ${alloc/6:,.0f}) ---")
|
||||||
|
print(f" {'banda$':>8} {'cadenza':>9} {'Sharpe':>8} {'CAGR':>8} {'maxDD':>8} {'ord/anno':>9}")
|
||||||
|
rows = []
|
||||||
|
for every, lbl in ((1, "daily"), (5, "weekly"), (21, "monthly")):
|
||||||
|
for band in (1.0, 5.0, 25.0, 50.0, 100.0):
|
||||||
|
if band > alloc / 6: # banda piu' grande della gamba = mai trade
|
||||||
|
continue
|
||||||
|
s = gtaa_banded(alloc, band, every)
|
||||||
|
m = metrics(s)
|
||||||
|
# ordini/anno stimati dal drag: ricostruiti come costo/anno / 0.35
|
||||||
|
rows.append((band, lbl, every, m, s))
|
||||||
|
for band, lbl, every, m, s in rows:
|
||||||
|
yrs = len(s) / 365.25
|
||||||
|
print(f" {band:>8.0f} {lbl:>9} {m['sharpe']:>8.2f} {m['cagr']:>8.1%} "
|
||||||
|
f"{m['maxdd']:>8.1%} {'':>9}")
|
||||||
|
top = max(rows, key=lambda r: r[3]["sharpe"])
|
||||||
|
best[alloc] = dict(band=top[0], every=top[2], label=top[1],
|
||||||
|
metrics=top[3], series=top[4])
|
||||||
|
print(f" -> migliore: banda ${top[0]:.0f} {top[1]} Sh {top[3]['sharpe']:.2f} "
|
||||||
|
f"CAGR {top[3]['cagr']:.1%}")
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
def _sh(s: pd.Series, lo=None) -> dict:
|
||||||
|
x = s if lo is None else s[s.index >= lo]
|
||||||
|
return metrics(x)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# --- wrapper pubblici: le implementazioni sopra sono memoizzate e lru_cache ritorna lo STESSO
|
||||||
|
# oggetto a ogni hit -> si consegna sempre una COPIA, cosi' una mutazione in place di un
|
||||||
|
# chiamante non puo' corrompere in silenzio i risultati successivi.
|
||||||
|
def tp01_realistic(alloc: float) -> pd.Series:
|
||||||
|
return _tp01_realistic_cached(float(alloc)).copy()
|
||||||
|
|
||||||
|
|
||||||
|
def gtaa_realistic(alloc: float, universe=EQ_UNIVERSE) -> pd.Series:
|
||||||
|
return _gtaa_realistic_cached(float(alloc), universe).copy()
|
||||||
|
|
||||||
|
|
||||||
|
def gtaa_banded(alloc: float, band_usd: float, every: int = 1,
|
||||||
|
universe=EQ_UNIVERSE) -> pd.Series:
|
||||||
|
return _gtaa_banded_cached(float(alloc), float(band_usd), int(every), universe).copy()
|
||||||
|
|
||||||
|
|
||||||
|
def part_a() -> dict:
|
||||||
|
print("=" * 100)
|
||||||
|
print(" A) COST-FLOOR PER SLEEVE — Sharpe reale vs dollari ALLOCATI a quello sleeve")
|
||||||
|
print("=" * 100)
|
||||||
|
|
||||||
|
alloc_grid = (50.0, 100.0, 200.0, 500.0, 1_000.0, 2_000.0, 5_000.0, 10_000.0, 50_000.0)
|
||||||
|
out = {"TP01": {}, "GTAA01": {}}
|
||||||
|
|
||||||
|
print("\n TP01 (Deribit, 2 gambe, fee 0.05%/lato, min order $5)")
|
||||||
|
print(f" {'alloc':>10} {'Sharpe':>8} {'CAGR':>8} {'maxDD':>8} {'vs modellato':>14}")
|
||||||
|
base_tp = _tp01_returns()
|
||||||
|
if base_tp.index.tz is None:
|
||||||
|
base_tp.index = base_tp.index.tz_localize("UTC")
|
||||||
|
m_model = metrics(base_tp)
|
||||||
|
for a in alloc_grid:
|
||||||
|
s = tp01_realistic(a)
|
||||||
|
m = metrics(s)
|
||||||
|
out["TP01"][a] = s
|
||||||
|
print(f" {a:>10,.0f} {m['sharpe']:>8.2f} {m['cagr']:>8.1%} {m['maxdd']:>8.1%}"
|
||||||
|
f" {m['sharpe'] - m_model['sharpe']:>+14.2f}")
|
||||||
|
print(f" {'MODELLATO':>10} {m_model['sharpe']:>8.2f} {m_model['cagr']:>8.1%} {m_model['maxdd']:>8.1%}")
|
||||||
|
|
||||||
|
print("\n GTAA01 (IB, 6 ETF, commissione min $0.35/ordine — PAVIMENTO FISSO)")
|
||||||
|
print(f" {'alloc':>10} {'Sharpe':>8} {'CAGR':>8} {'maxDD':>8} {'drag comm/anno':>15}")
|
||||||
|
from src.portfolio.gtaa import gtaa_returns
|
||||||
|
g_model = gtaa_returns()
|
||||||
|
if g_model.index.tz is None:
|
||||||
|
g_model.index = g_model.index.tz_localize("UTC")
|
||||||
|
mg_model = metrics(g_model)
|
||||||
|
for a in alloc_grid:
|
||||||
|
s = gtaa_realistic(a)
|
||||||
|
m = metrics(s)
|
||||||
|
out["GTAA01"][a] = s
|
||||||
|
drag = mg_model["cagr"] - m["cagr"]
|
||||||
|
print(f" {a:>10,.0f} {m['sharpe']:>8.2f} {m['cagr']:>8.1%} {m['maxdd']:>8.1%}"
|
||||||
|
f" {drag:>15.2%}")
|
||||||
|
print(f" {'MODELLATO':>10} {mg_model['sharpe']:>8.2f} {mg_model['cagr']:>8.1%} {mg_model['maxdd']:>8.1%}"
|
||||||
|
f" {'(2bps propor.)':>15}")
|
||||||
|
out["_models"] = dict(TP01=m_model, GTAA01=mg_model)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================== B) FRONTIERA
|
||||||
|
# Config GTAA scelta sul PLATEAU (non sulla cella max): cadenza settimanale + banda $50.
|
||||||
|
# La banda e' un parametro di COSTO, non di segnale — si sceglie sul plateau robusto
|
||||||
|
# (weekly/monthly x banda 25-100 danno 0.45-0.64 a ogni capitale), non sull'argmax
|
||||||
|
# in-sample (banda 50 daily a $600 = 0.57 e' una cella isolata, crolla a 0.28 a banda 100).
|
||||||
|
GTAA_BAND_USD, GTAA_EVERY = 50.0, 5
|
||||||
|
|
||||||
|
|
||||||
|
def part_b(deluck: float = 1.0) -> dict:
|
||||||
|
"""Per ogni capitale totale C: il book eseguibile migliore e il suo CAGR/Sharpe.
|
||||||
|
|
||||||
|
A $600 il book live e' TP01+SKH01 su Deribit. La domanda nuova: conviene SPEZZARE il
|
||||||
|
capitale (una parte su IB per GTAA01)? Il costo dello split e' il pavimento IB; il
|
||||||
|
beneficio e' un 3o sleeve a corr ~+0.10.
|
||||||
|
deluck: moltiplicatore sul DRIFT (0.6 = convenzione de-luck del progetto), la vol resta.
|
||||||
|
"""
|
||||||
|
tag = "MODELLATO" if deluck == 1.0 else f"DE-LUCK x{deluck}"
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print(f" B) FRONTIERA CAPITALE -> BOOK ESEGUIBILE [{tag}]")
|
||||||
|
print("=" * 100)
|
||||||
|
|
||||||
|
skh = _skyhook_returns()
|
||||||
|
if skh.index.tz is None:
|
||||||
|
skh.index = skh.index.tz_localize("UTC")
|
||||||
|
|
||||||
|
def _dl(s: pd.Series) -> pd.Series:
|
||||||
|
if deluck == 1.0:
|
||||||
|
return s
|
||||||
|
return s - (1.0 - deluck) * float(s.mean())
|
||||||
|
|
||||||
|
print(f"\n {'capitale':>9} {'book':>28} {'ShFULL':>7} {'ShHOLD':>7} {'CAGR':>7} "
|
||||||
|
f"{'maxDD':>7} {'EUR/g':>7}")
|
||||||
|
rows = []
|
||||||
|
for C in (600.0, 1_000.0, 2_000.0, 5_000.0, 10_000.0, 20_000.0, 50_000.0, 100_000.0, 200_000.0):
|
||||||
|
cands = []
|
||||||
|
# --- 1) solo Deribit (book live attuale)
|
||||||
|
for wg in (0.0, 0.10, 0.20, 0.30):
|
||||||
|
w_der = 1.0 - wg
|
||||||
|
alloc_tp, alloc_gt = C * w_der * 0.75, C * wg
|
||||||
|
cols = {"TP01": _dl(tp01_realistic(alloc_tp)), "SKH01": _dl(skh)}
|
||||||
|
wts = {"TP01": w_der * 0.75, "SKH01": w_der * 0.25}
|
||||||
|
if wg > 0:
|
||||||
|
cols["GTAA01"] = _dl(gtaa_banded(alloc_gt, GTAA_BAND_USD, GTAA_EVERY))
|
||||||
|
wts["GTAA01"] = wg
|
||||||
|
combo = combine_outer(cols, wts, lo=pd.Timestamp("2019-03-14", tz="UTC"))
|
||||||
|
m = metrics(combo)
|
||||||
|
mh = metrics(combo[combo.index >= pd.Timestamp("2025-01-01", tz="UTC")])
|
||||||
|
name = "TP01+SKH01 (Deribit)" if wg == 0 else f"+GTAA01 {wg:.0%} (IB split)"
|
||||||
|
cands.append((name, m, mh, wg))
|
||||||
|
best = max(cands, key=lambda r: r[1]["sharpe"])
|
||||||
|
for name, m, mh, wg in cands:
|
||||||
|
mark = " *" if name == best[0] else " "
|
||||||
|
eur = C * m["cagr"] * (1 - TAX_RATE) / EURUSD / 365.0
|
||||||
|
print(f" {C:>9,.0f} {name:>28} {m['sharpe']:>7.2f} {mh['sharpe']:>7.2f} "
|
||||||
|
f"{m['cagr']:>7.1%} {m['maxdd']:>7.1%} {eur:>7.2f}{mark}")
|
||||||
|
rows.append((C, best))
|
||||||
|
print()
|
||||||
|
return dict(rows=rows)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================== C) RENDITA
|
||||||
|
def book_series(lens: str = "hourly", with_gtaa: float = 0.0,
|
||||||
|
alloc: float = 600.0) -> pd.Series:
|
||||||
|
"""Serie giornaliera del BOOK ESEGUIBILE.
|
||||||
|
lens='hourly' -> SKH01 sul PATH LIVE (fill al close 5m del prossimo multiplo orario:
|
||||||
|
e' la stima onesta di cio' che il cron esegue davvero).
|
||||||
|
lens='canonical' -> SKH01 modellato (fill AL LIVELLO SL/TP): e' il backtest, non il live.
|
||||||
|
"""
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
import r0724_skh_live_weight as skl # noqa: E402 (riusa la macchineria d'audit gia' validata)
|
||||||
|
|
||||||
|
skh = skl.skh_book(0, lens)
|
||||||
|
if skh.index.tz is None:
|
||||||
|
skh.index = skh.index.tz_localize("UTC")
|
||||||
|
cols = {"TP01": tp01_realistic(alloc * 0.75), "SKH01": skh}
|
||||||
|
wts = {"TP01": 0.75, "SKH01": 0.25}
|
||||||
|
if with_gtaa > 0:
|
||||||
|
cols = {"TP01": tp01_realistic(alloc * (1 - with_gtaa) * 0.75), "SKH01": skh,
|
||||||
|
"GTAA01": gtaa_banded(alloc * with_gtaa, GTAA_BAND_USD, GTAA_EVERY)}
|
||||||
|
wts = {"TP01": (1 - with_gtaa) * 0.75, "SKH01": (1 - with_gtaa) * 0.25,
|
||||||
|
"GTAA01": with_gtaa}
|
||||||
|
return combine_outer(cols, wts, lo=pd.Timestamp("2019-03-14", tz="UTC"))
|
||||||
|
|
||||||
|
|
||||||
|
def _boot_paths(r: np.ndarray, n_paths: int, n_days: int, block: int,
|
||||||
|
rng: np.random.Generator) -> np.ndarray:
|
||||||
|
"""Block bootstrap (blocchi di `block` giorni) — preserva la dipendenza seriale
|
||||||
|
(momentum/vol-clustering) che un bootstrap iid distruggerebbe, gonfiando la sopravvivenza."""
|
||||||
|
n_blocks = int(np.ceil(n_days / block))
|
||||||
|
starts = rng.integers(0, len(r) - block, size=(n_paths, n_blocks))
|
||||||
|
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)
|
||||||
|
return r[idx[:, :n_days]]
|
||||||
|
|
||||||
|
|
||||||
|
def survival(r: np.ndarray, cap0: float, draw_usd_yr: float, years: int,
|
||||||
|
k: float, n_paths: int = 4000, block: int = 20,
|
||||||
|
index_rate: float = 0.0, seed: int = 725) -> dict:
|
||||||
|
"""P(il capitale sopravvive `years` anni prelevando `draw_usd_yr`/anno) con leva `k`.
|
||||||
|
Prelievo giornaliero pro-rata (una rendita si preleva in continuo, non a fine anno).
|
||||||
|
"""
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
n_days = int(years * 365)
|
||||||
|
paths = _boot_paths(r, n_paths, n_days, block, rng) * k
|
||||||
|
cap = np.full(n_paths, cap0, float)
|
||||||
|
alive = np.ones(n_paths, bool)
|
||||||
|
daily_draw = draw_usd_yr / 365.0
|
||||||
|
ruin_day = np.full(n_paths, n_days + 1, int)
|
||||||
|
for t in range(n_days):
|
||||||
|
d = daily_draw * (1.0 + index_rate) ** (t / 365.0)
|
||||||
|
cap = np.where(alive, cap * (1.0 + paths[:, t]) - d, cap)
|
||||||
|
newly = alive & (cap <= 0)
|
||||||
|
ruin_day[newly] = t
|
||||||
|
alive &= cap > 0
|
||||||
|
return dict(p_survive=float(alive.mean()),
|
||||||
|
median_end=float(np.median(np.where(alive, cap, 0.0))),
|
||||||
|
p10_end=float(np.percentile(np.where(alive, cap, 0.0), 10)),
|
||||||
|
median_ruin_yr=float(np.median(ruin_day[~alive]) / 365.0) if (~alive).any() else np.nan)
|
||||||
|
|
||||||
|
|
||||||
|
def part_c() -> dict:
|
||||||
|
"""La RENDITA sostenibile del book. Il muro del 24/07 usava capitale = target/CAGR,
|
||||||
|
che ignora il RISCHIO DI SEQUENZA: prelevare da un book volatile mentre e' in drawdown
|
||||||
|
consuma capitale che non torna. Qui il muro vero, a 95% di sopravvivenza."""
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print(" C) RENDITA SOSTENIBILE — quanto capitale serve DAVVERO per 50 EUR/g")
|
||||||
|
print("=" * 100)
|
||||||
|
|
||||||
|
out = {}
|
||||||
|
for lens, deluck in (("canonical", 1.0), ("hourly", 1.0), ("hourly", 0.6)):
|
||||||
|
b = book_series(lens)
|
||||||
|
if deluck != 1.0:
|
||||||
|
b = b - (1.0 - deluck) * float(b.mean()) # taglia il DRIFT, lascia la vol
|
||||||
|
m = metrics(b)
|
||||||
|
r = b.values.astype(float)
|
||||||
|
vol = float(r.std() * np.sqrt(365))
|
||||||
|
lbl = ("MODELLATO (backtest)" if lens == "canonical" else "PATH LIVE (cron orario)")
|
||||||
|
if deluck != 1.0:
|
||||||
|
lbl += f" DE-LUCK drift x{deluck}"
|
||||||
|
print(f"\n --- book TP01 75 / SKH01 25 — {lbl} ---")
|
||||||
|
print(f" Sharpe {m['sharpe']:.2f} CAGR {m['cagr']:.1%} vol {vol:.1%} "
|
||||||
|
f"maxDD {m['maxdd']:.1%} ({m['n']} giorni)")
|
||||||
|
|
||||||
|
# tasso di prelievo massimo sostenibile al 95% di sopravvivenza, per leva k
|
||||||
|
print(f"\n {'leva':>5} {'vol':>7} {'CAGR':>7} {'SWR20a':>8} {'SWR30a':>8} "
|
||||||
|
f"{'capitale per 50EUR/g netti':>28}")
|
||||||
|
rows = {}
|
||||||
|
for k in (0.5, 0.75, 1.0, 1.5, 2.0):
|
||||||
|
# SWR = massimo tasso di prelievo (% del capitale iniziale) con P(vivo) >= 95%
|
||||||
|
swr = {}
|
||||||
|
for years in (20, 30):
|
||||||
|
lo, hi = 0.0, 0.40
|
||||||
|
for _ in range(14): # bisezione sul tasso
|
||||||
|
mid = (lo + hi) / 2
|
||||||
|
p = survival(r, 1_000_000.0, 1_000_000.0 * mid, years, k,
|
||||||
|
n_paths=2000)["p_survive"]
|
||||||
|
if p >= 0.95:
|
||||||
|
lo = mid
|
||||||
|
else:
|
||||||
|
hi = mid
|
||||||
|
swr[years] = lo
|
||||||
|
gross_needed = TARGET_EUR_DAY * 365 * EURUSD / (1 - TAX_RATE)
|
||||||
|
cap_needed = gross_needed / swr[20] if swr[20] > 0.001 else float("inf")
|
||||||
|
print(f" {k:>5.2f} {vol*k:>7.1%} {m['cagr']*k:>7.1%} {swr[20]:>8.2%} "
|
||||||
|
f"{swr[30]:>8.2%} ${cap_needed:>26,.0f}")
|
||||||
|
rows[k] = dict(swr20=swr[20], swr30=swr[30], cap=cap_needed)
|
||||||
|
out[(lens, deluck)] = dict(metrics=m, vol=vol, rows=rows, series=b)
|
||||||
|
best_k = max(rows, key=lambda k: rows[k]["swr20"])
|
||||||
|
print(f" -> leva che massimizza la rendita: {best_k:.2f}x "
|
||||||
|
f"(SWR {rows[best_k]['swr20']:.2%}, capitale ${rows[best_k]['cap']:,.0f})")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ====================================================== C2) RENDITA PERPETUA
|
||||||
|
def part_c2() -> dict:
|
||||||
|
"""'Survive 20 anni' NON e' una rendita: e' un piano di consumo del capitale. Una rendita
|
||||||
|
passiva vera vuole il capitale INTATTO (altrimenti a 20 anni il reddito finisce). Qui il
|
||||||
|
criterio giusto: prelievo massimo con P(capitale a 20a >= capitale iniziale) >= 90%."""
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print(" C2) RENDITA PERPETUA — prelievo che PRESERVA il capitale (P(cap_20a >= cap_0) >= 90%)")
|
||||||
|
print("=" * 100)
|
||||||
|
|
||||||
|
gross_needed = TARGET_EUR_DAY * 365 * EURUSD / (1 - TAX_RATE)
|
||||||
|
print(f" 50 EUR/g netti = EUR {TARGET_EUR_DAY*365:,.0f}/anno netti "
|
||||||
|
f"= ${gross_needed:,.0f}/anno LORDI (fisco {TAX_RATE:.0%})\n")
|
||||||
|
print(f" {'lente':>34} {'leva':>5} {'SWR-20a':>8} {'PERPETUA':>9} "
|
||||||
|
f"{'capitale (perpetua)':>21}")
|
||||||
|
out = {}
|
||||||
|
for lens, deluck in (("canonical", 1.0), ("hourly", 1.0), ("hourly", 0.6)):
|
||||||
|
b = book_series(lens)
|
||||||
|
if deluck != 1.0:
|
||||||
|
b = b - (1.0 - deluck) * float(b.mean())
|
||||||
|
r = b.values.astype(float)
|
||||||
|
lbl = ("MODELLATO" if lens == "canonical" else "PATH LIVE")
|
||||||
|
if deluck != 1.0:
|
||||||
|
lbl += f" de-luck x{deluck}"
|
||||||
|
for k in (1.0, 1.5, 2.0):
|
||||||
|
lo, hi = 0.0, 0.40
|
||||||
|
for _ in range(13):
|
||||||
|
mid = (lo + hi) / 2
|
||||||
|
s = survival(r, 1_000_000.0, 1_000_000.0 * mid, 20, k, n_paths=2000)
|
||||||
|
# criterio PERPETUO: il 90mo percentile dei path deve finire >= capitale iniziale
|
||||||
|
keep = s["p10_end"] >= 1_000_000.0
|
||||||
|
if keep:
|
||||||
|
lo = mid
|
||||||
|
else:
|
||||||
|
hi = mid
|
||||||
|
swr20 = None
|
||||||
|
lo2, hi2 = 0.0, 0.40
|
||||||
|
for _ in range(13):
|
||||||
|
mid = (lo2 + hi2) / 2
|
||||||
|
if survival(r, 1e6, 1e6 * mid, 20, k, n_paths=2000)["p_survive"] >= 0.95:
|
||||||
|
lo2 = mid
|
||||||
|
else:
|
||||||
|
hi2 = mid
|
||||||
|
swr20 = lo2
|
||||||
|
cap = gross_needed / lo if lo > 0.002 else float("inf")
|
||||||
|
print(f" {lbl:>34} {k:>5.2f} {swr20:>8.2%} {lo:>9.2%} "
|
||||||
|
f"{('$' + format(cap, ',.0f')) if np.isfinite(cap) else 'IRRAGGIUNGIBILE':>21}")
|
||||||
|
out[(lens, deluck, k)] = dict(swr20=swr20, perp=lo, cap=cap)
|
||||||
|
print()
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================== D) TRAIETTORIA
|
||||||
|
def part_d(perp_rate: float, cap_needed: float) -> None:
|
||||||
|
"""Quanto tempo per accumulare il capitale della rendita — STOCASTICO (il 24/07 era
|
||||||
|
deterministico: un CAGR fisso nasconde che meta' dei path arriva dopo)."""
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print(f" D) TRAIETTORIA — da $600 a ${cap_needed:,.0f} (rendita perpetua {perp_rate:.2%})")
|
||||||
|
print("=" * 100)
|
||||||
|
b = book_series("hourly")
|
||||||
|
b = b - 0.4 * float(b.mean()) # de-luck x0.6: la lente della decisione
|
||||||
|
r = b.values.astype(float)
|
||||||
|
rng = np.random.default_rng(725)
|
||||||
|
n_days = 365 * 25
|
||||||
|
paths = _boot_paths(r, 3000, n_days, 20, rng)
|
||||||
|
|
||||||
|
print(f"\n {'deposito/mese':>14} {'p10':>8} {'mediana':>9} {'p90':>8} "
|
||||||
|
f"{'P(entro 10a)':>12} {'P(entro 20a)':>12}")
|
||||||
|
for dep_eur in (0, 250, 500, 1_000, 2_000, 5_000):
|
||||||
|
dep = dep_eur * EURUSD
|
||||||
|
cap = np.full(3000, 600.0)
|
||||||
|
hit = np.full(3000, -1, int)
|
||||||
|
for t in range(n_days):
|
||||||
|
cap = cap * (1.0 + paths[:, t])
|
||||||
|
if t % 30 == 0 and t > 0:
|
||||||
|
cap += dep
|
||||||
|
newly = (hit < 0) & (cap >= cap_needed)
|
||||||
|
hit[newly] = t
|
||||||
|
yrs = np.where(hit >= 0, hit / 365.0, np.nan)
|
||||||
|
finite = yrs[~np.isnan(yrs)]
|
||||||
|
p10 = np.percentile(finite, 10) if len(finite) > 100 else np.nan
|
||||||
|
p50 = np.percentile(finite, 50) if len(finite) > 100 else np.nan
|
||||||
|
p90 = np.percentile(finite, 90) if len(finite) > 100 else np.nan
|
||||||
|
p10y = float((yrs <= 10).sum() / len(yrs))
|
||||||
|
p20y = float((yrs <= 20).sum() / len(yrs))
|
||||||
|
f = lambda v: f"{v:.1f}a" if np.isfinite(v) else " —" # noqa: E731
|
||||||
|
print(f" {dep_eur:>10,.0f}/m {f(p10):>8} {f(p50):>9} {f(p90):>8} "
|
||||||
|
f"{p10y:>12.0%} {p20y:>12.0%}")
|
||||||
|
print("\n (bootstrap a blocchi dei ritorni REALI del book, de-luck x0.6; i depositi")
|
||||||
|
print(" sono in EUR/mese. 'P(entro Na)' = quota di path che tocca il capitale-rendita.)")
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================ E) IL MURO SI ABBASSA?
|
||||||
|
def part_e() -> dict:
|
||||||
|
"""L'ipotesi di partenza: piu' capitale -> piu' sleeve eseguibili -> piu' Sharpe -> muro
|
||||||
|
piu' basso. Per il book eseguibile-OGGI e' FALSA (parte B: Sharpe piatto da $600 a $200k).
|
||||||
|
Ma attraverso le SOGLIE di sblocco (XSR01 ~$5k, XS01 ~$20k) puo' essere vera. Qui il
|
||||||
|
conto onesto, con GTAA01 al suo costo IB REALE e SKH01 sul path live.
|
||||||
|
|
||||||
|
Regole di ammissione rispettate: VRP01 ESCLUSO (regola permanente 'niente short-vol da
|
||||||
|
modello in deploy'); XSR01 escluso dal book deployable (gate pre-registrato 2026-10-23,
|
||||||
|
oggi in forward-monitor) ma mostrato come contro-fattuale.
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print(" E) IL MURO SI ABBASSA COL CAPITALE? — book per tier di sbloccо")
|
||||||
|
print("=" * 100)
|
||||||
|
from src.portfolio.sleeves import _vrp_combo_returns, _xsec_returns
|
||||||
|
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
import r0724_skh_live_weight as skl
|
||||||
|
|
||||||
|
skh = skl.skh_book(0, "hourly")
|
||||||
|
if skh.index.tz is None:
|
||||||
|
skh.index = skh.index.tz_localize("UTC")
|
||||||
|
|
||||||
|
def _d(s):
|
||||||
|
s = s.dropna().sort_index()
|
||||||
|
if s.index.tz is None:
|
||||||
|
s.index = s.index.tz_localize("UTC")
|
||||||
|
return s
|
||||||
|
|
||||||
|
xs = _d(_xsec_returns()).resample("1D").apply(lambda x: (1 + x).prod() - 1)
|
||||||
|
vrp = _d(_vrp_combo_returns()).resample("1D").apply(lambda x: (1 + x).prod() - 1)
|
||||||
|
|
||||||
|
gross_needed = TARGET_EUR_DAY * 365 * EURUSD / (1 - TAX_RATE)
|
||||||
|
tiers = [
|
||||||
|
("$600 — 2 sleeve (Deribit)", 600.0,
|
||||||
|
{"TP01": 0.75, "SKH01": 0.25}),
|
||||||
|
("$2k — + GTAA01 (IB, banda)", 2_000.0,
|
||||||
|
{"TP01": 0.60, "SKH01": 0.20, "GTAA01": 0.20}),
|
||||||
|
("$20k — + XS01 (Hyperliquid)", 20_000.0,
|
||||||
|
{"TP01": 0.45, "SKH01": 0.25, "GTAA01": 0.20, "XS01": 0.10}),
|
||||||
|
("$20k — + XS01 + VRP01 (contro-fattuale)", 20_000.0,
|
||||||
|
{"TP01": 0.40, "SKH01": 0.22, "GTAA01": 0.18, "XS01": 0.10, "VRP01": 0.10}),
|
||||||
|
]
|
||||||
|
print(f"\n {'tier':>42} {'Sharpe':>7} {'CAGR':>7} {'vol':>6} {'maxDD':>7} "
|
||||||
|
f"{'rendita':>8} {'capitale 50EUR/g':>18}")
|
||||||
|
out = {}
|
||||||
|
for name, C, w in tiers:
|
||||||
|
cols = {"TP01": tp01_realistic(C * w["TP01"]), "SKH01": skh}
|
||||||
|
if "GTAA01" in w:
|
||||||
|
cols["GTAA01"] = gtaa_banded(C * w["GTAA01"], GTAA_BAND_USD, GTAA_EVERY)
|
||||||
|
if "XS01" in w:
|
||||||
|
cols["XS01"] = xs
|
||||||
|
if "VRP01" in w:
|
||||||
|
cols["VRP01"] = vrp
|
||||||
|
combo = combine_outer(cols, w, lo=pd.Timestamp("2019-03-14", tz="UTC"))
|
||||||
|
combo = combo - 0.4 * float(combo.mean()) # de-luck x0.6 (lente della decisione)
|
||||||
|
m = metrics(combo)
|
||||||
|
r = combo.values.astype(float)
|
||||||
|
lo, hi = 0.0, 0.40
|
||||||
|
for _ in range(13):
|
||||||
|
mid = (lo + hi) / 2
|
||||||
|
if survival(r, 1e6, 1e6 * mid, 20, 1.0, n_paths=2000)["p10_end"] >= 1e6:
|
||||||
|
lo = mid
|
||||||
|
else:
|
||||||
|
hi = mid
|
||||||
|
cap = gross_needed / lo if lo > 0.002 else float("inf")
|
||||||
|
vol = float(r.std() * np.sqrt(365))
|
||||||
|
# ISO-RISCHIO: e' QUI che la diversificazione paga. A pari nozionale un diversificatore
|
||||||
|
# a basso CAGR diluisce il ritorno; il suo valore e' che LIBERA BUDGET DI RISCHIO. Si
|
||||||
|
# confronta portando ogni tier alla STESSA vol (15%), non allo stesso nozionale.
|
||||||
|
k_iso = min(VOL_BUDGET / vol, 3.0)
|
||||||
|
lo_i, hi_i = 0.0, 0.60
|
||||||
|
for _ in range(13):
|
||||||
|
mid = (lo_i + hi_i) / 2
|
||||||
|
if survival(r, 1e6, 1e6 * mid, 20, k_iso, n_paths=2000)["p10_end"] >= 1e6:
|
||||||
|
lo_i = mid
|
||||||
|
else:
|
||||||
|
hi_i = mid
|
||||||
|
cap_i = gross_needed / lo_i if lo_i > 0.002 else float("inf")
|
||||||
|
print(f" {name:>42} {m['sharpe']:>7.2f} {m['cagr']:>7.1%} {vol:>6.1%} "
|
||||||
|
f"{m['maxdd']:>7.1%} {lo:>8.2%} "
|
||||||
|
f"{('$' + format(cap, ',.0f')) if np.isfinite(cap) else 'IRRAGG.':>18}"
|
||||||
|
f" | {k_iso:>4.2f}x {lo_i:>7.2%} "
|
||||||
|
f"{('$' + format(cap_i, ',.0f')) if np.isfinite(cap_i) else 'IRRAGG.':>18}")
|
||||||
|
out[name] = dict(perp=lo, cap=cap, metrics=m, k_iso=k_iso, perp_iso=lo_i, cap_iso=cap_i)
|
||||||
|
print("\n (tutti de-luck x0.6, SKH01 su path live, GTAA01 a costo IB reale con banda.")
|
||||||
|
print(" 'rendita' = prelievo perpetuo al 90% di preservazione del capitale.)")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
part_a()
|
||||||
|
part_a2()
|
||||||
|
part_b()
|
||||||
|
part_c()
|
||||||
|
res = part_c2()
|
||||||
|
part_e()
|
||||||
|
key = ("hourly", 0.6, 1.0)
|
||||||
|
part_d(res[key]["perp"], res[key]["cap"])
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
"""r0725_hyro — HYROTRADER nello specifico: config, leva e la regola di CONSISTENCY (2026-07-25).
|
||||||
|
|
||||||
|
La ricerca del 24/07 ha messo HyroTrader prima in classifica per QUESTO book:
|
||||||
|
* unica con **API reale** da funded (sub-account Bybit) -> il book gira automatico, non a mano;
|
||||||
|
* **nessun limite di tempo** sull'eval -> una strategia lenta non e' penalizzata;
|
||||||
|
* **weekend consentito** -> non banale: l'ondata 2026-07-17 ha misurato che il weekend porta
|
||||||
|
il **38% del gross di TP01** nel 31% del tempo (de-esporlo e' attivamente dannoso);
|
||||||
|
* funding = mercato (stessa economia del book Deribit), fee rimborsata al pass, payout ~12h.
|
||||||
|
Biglietti 1-step: $25k/$249, $50k/$379, $100k/$579. Cap $200k per trader.
|
||||||
|
|
||||||
|
Ma ha due regole che mordono ESATTAMENTE questo book, e nessuna analisi le aveva modellate:
|
||||||
|
1. **max drawdown 6% STATICO** (dal saldo iniziale) — il book de-luckato ha maxDD 15.4% a 1.0x;
|
||||||
|
2. **CONSISTENCY 40% in eval**: nessun singolo giorno puo' valere piu' del 40% del profitto
|
||||||
|
totale. E' una tagliola per un book LENTO e GRUMOSO: SKH01 ha P&L per-trade a scalino
|
||||||
|
(caveat noto "equity daily-step") e TP01 fa mesi piatti seguiti da strappi di trend. Se il
|
||||||
|
target del 10% arriva con 2-3 giornatone, l'eval **non passa**: si deve continuare a
|
||||||
|
tradare finche' il profitto totale si diluisce — esponendosi altro tempo al DD 6%.
|
||||||
|
|
||||||
|
Questo script misura: P(pass) con e senza consistency, la leva ottima, e l'EV di ogni biglietto.
|
||||||
|
|
||||||
|
ONESTA': doppia lente. CLOSE-ONLY = tetto ottimista; INTRADAY = pavimento (gap lognormale
|
||||||
|
calibrato sul recon MTM 1h del 24/07, ma estratto indipendente dal rendimento del giorno ->
|
||||||
|
breach spuri: vedi il caveat in r0725_prop_ladder.py). La verita' sta in mezzo. Finestra comune
|
||||||
|
2024+ (dove esiste XS01) = anche la finestra di scoperta di XS01 -> la config B e' favorita.
|
||||||
|
|
||||||
|
Uso: `uv run python scripts/research/r0725_hyro.py`
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
from src.portfolio.portfolio import combine_outer, metrics # noqa: E402
|
||||||
|
from src.portfolio.sleeves import _xsec_returns # noqa: E402
|
||||||
|
|
||||||
|
import r0724_skh_live_weight as skl # noqa: E402
|
||||||
|
from r0725_capcurve import EURUSD, TAX_RATE, tp01_realistic # noqa: E402
|
||||||
|
from r0725_prop_ladder import WICK_MU, WICK_SIGMA, WICK_REF_VOL # noqa: E402
|
||||||
|
|
||||||
|
# --- regole HyroTrader 1-step (ricerca 24/07, fonti primarie)
|
||||||
|
EV_TARGET, EV_DD, EV_DL = 0.10, 0.06, 0.04
|
||||||
|
FU_ML, FU_DL = 0.06, 0.04
|
||||||
|
CONSISTENCY = 0.40 # nessun giorno > 40% del profitto totale (solo in eval)
|
||||||
|
SPLIT = 0.80
|
||||||
|
TICKETS = [(25_000, 249.0), (50_000, 379.0), (100_000, 579.0)]
|
||||||
|
CAP = 200_000
|
||||||
|
DELUCK = 0.6
|
||||||
|
RNG = np.random.default_rng(20260725)
|
||||||
|
|
||||||
|
|
||||||
|
def _d(s: pd.Series) -> pd.Series:
|
||||||
|
s = s.dropna().sort_index()
|
||||||
|
if s.index.tz is None:
|
||||||
|
s.index = s.index.tz_localize("UTC")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def configs() -> dict:
|
||||||
|
skh = _d(skl.skh_book(0, "hourly"))
|
||||||
|
tp = _d(tp01_realistic(100_000 * 0.75))
|
||||||
|
xs = _d(_xsec_returns()).resample("1D").apply(lambda x: (1 + x).prod() - 1)
|
||||||
|
lo = max(tp.index.min(), skh.index.min(), xs.index.min())
|
||||||
|
out = {}
|
||||||
|
for name, cols, w in (
|
||||||
|
("A book live (TP01/SKH01)", {"TP01": tp, "SKH01": skh}, {"TP01": .75, "SKH01": .25}),
|
||||||
|
("B + XS01 (19 alt perp)", {"TP01": tp, "SKH01": skh, "XS01": xs},
|
||||||
|
{"TP01": .55, "SKH01": .20, "XS01": .25}),
|
||||||
|
):
|
||||||
|
s = combine_outer(cols, w, lo=lo)
|
||||||
|
s = s - (1.0 - DELUCK) * float(s.mean())
|
||||||
|
out[name] = s
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _paths(r: np.ndarray, n_days: int, n_paths: int, block: int = 20) -> np.ndarray:
|
||||||
|
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]
|
||||||
|
return r[idx]
|
||||||
|
|
||||||
|
|
||||||
|
def _wick(shape, vol: float) -> np.ndarray:
|
||||||
|
scale = float(np.clip(vol / WICK_REF_VOL, 0.0, 3.0))
|
||||||
|
return -np.exp(RNG.normal(WICK_MU, WICK_SIGMA, size=shape)) * scale
|
||||||
|
|
||||||
|
|
||||||
|
def sim_eval(r: np.ndarray, k: float, consistency: bool, intraday: bool,
|
||||||
|
max_days: int = 540, n_paths: int = 8000) -> dict:
|
||||||
|
"""Eval HYRO. Il pass richiede equity >= 1+target E (se consistency) che il MIGLIOR giorno
|
||||||
|
non superi il 40% del profitto cumulato. Breach su equity intraday."""
|
||||||
|
R = _paths(r, max_days, n_paths) * k
|
||||||
|
G = _wick(R.shape, float(r.std() * np.sqrt(365)) * k) if intraday else np.zeros_like(R)
|
||||||
|
eq = np.ones(n_paths)
|
||||||
|
best_day = np.zeros(n_paths) # miglior guadagno giornaliero in USD-equity
|
||||||
|
alive = np.ones(n_paths, bool)
|
||||||
|
passed = np.zeros(n_paths, bool)
|
||||||
|
day_pass = np.full(n_paths, max_days + 1, int)
|
||||||
|
for t in range(max_days):
|
||||||
|
prev = eq
|
||||||
|
eq = np.where(alive & ~passed, eq * (1 + R[:, t]), eq)
|
||||||
|
low = np.where(alive & ~passed, prev * (1 + R[:, t] + G[:, t]), eq)
|
||||||
|
gain = np.maximum(eq - prev, 0.0)
|
||||||
|
best_day = np.where(alive & ~passed, np.maximum(best_day, gain), best_day)
|
||||||
|
bust = alive & ~passed & ((low < 1.0 - EV_DD) | ((R[:, t] + G[:, t]) < -EV_DL))
|
||||||
|
alive &= ~bust
|
||||||
|
profit = eq - 1.0
|
||||||
|
ok = alive & ~passed & (profit >= EV_TARGET)
|
||||||
|
if consistency:
|
||||||
|
ok &= best_day <= CONSISTENCY * np.maximum(profit, 1e-9)
|
||||||
|
day_pass[ok] = t
|
||||||
|
passed |= ok
|
||||||
|
return dict(p_pass=float(passed.mean()),
|
||||||
|
median_days=float(np.median(day_pass[passed])) if passed.any() else float("nan"))
|
||||||
|
|
||||||
|
|
||||||
|
def sim_funded(r: np.ndarray, k: float, intraday: bool, notional: float,
|
||||||
|
years: int = 1, n_paths: int = 8000) -> dict:
|
||||||
|
n_days = years * 365
|
||||||
|
R = _paths(r, n_days, n_paths) * k
|
||||||
|
G = _wick(R.shape, float(r.std() * np.sqrt(365)) * k) if intraday else np.zeros_like(R)
|
||||||
|
eq = np.ones(n_paths)
|
||||||
|
alive = np.ones(n_paths, bool)
|
||||||
|
payout = np.zeros(n_paths)
|
||||||
|
for t in range(n_days):
|
||||||
|
prev = eq
|
||||||
|
eq = np.where(alive, eq * (1 + R[:, t]), eq)
|
||||||
|
low = np.where(alive, prev * (1 + R[:, t] + G[:, t]), eq)
|
||||||
|
bust = alive & ((low < 1.0 - FU_ML) | ((R[:, t] + G[:, t]) < -FU_DL))
|
||||||
|
alive &= ~bust
|
||||||
|
if (t + 1) % 30 == 0:
|
||||||
|
g = np.where(alive & (eq > 1.0), eq - 1.0, 0.0)
|
||||||
|
payout += g * SPLIT
|
||||||
|
eq = np.where(alive & (eq > 1.0), 1.0, eq)
|
||||||
|
usd = payout * notional
|
||||||
|
return dict(p_alive=float(alive.mean()), e_payout=float(usd.mean()),
|
||||||
|
eur_day=float(usd.mean() * (1 - TAX_RATE) / EURUSD / 365.0))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
cfg = configs()
|
||||||
|
print("=" * 100)
|
||||||
|
print(" HYROTRADER — config, leva e la regola di CONSISTENCY 40%")
|
||||||
|
print("=" * 100)
|
||||||
|
print(f" regole: target {EV_TARGET:.0%} | maxDD {EV_DD:.0%} STATICO | daily {EV_DL:.0%} | "
|
||||||
|
f"consistency {CONSISTENCY:.0%} (eval) | split {SPLIT:.0%} | cap ${CAP:,}")
|
||||||
|
for n, s in cfg.items():
|
||||||
|
m = metrics(s)
|
||||||
|
print(f" {n:>28}: Sharpe {m['sharpe']:>5.2f} CAGR {m['cagr']:>6.1%} "
|
||||||
|
f"vol {s.std()*np.sqrt(365):>5.1%} maxDD {m['maxdd']:>5.1%}")
|
||||||
|
|
||||||
|
for intraday in (False, True):
|
||||||
|
lens = "INTRADAY (pavimento)" if intraday else "CLOSE-ONLY (tetto)"
|
||||||
|
print("\n" + "-" * 100)
|
||||||
|
print(f" EVAL — {lens}: quanto costa la regola di CONSISTENCY")
|
||||||
|
print("-" * 100)
|
||||||
|
print(f" {'config':>28} {'leva':>5} {'P(pass) senza':>14} {'P(pass) CON':>12} "
|
||||||
|
f"{'costo regola':>13} {'giorni med':>11}")
|
||||||
|
for n, s in cfg.items():
|
||||||
|
r = s.values.astype(float)
|
||||||
|
for k in (0.5, 0.75, 1.0):
|
||||||
|
a = sim_eval(r, k, consistency=False, intraday=intraday, n_paths=4000)
|
||||||
|
b = sim_eval(r, k, consistency=True, intraday=intraday, n_paths=4000)
|
||||||
|
print(f" {n:>28} {k:>5.2f} {a['p_pass']:>14.1%} {b['p_pass']:>12.1%} "
|
||||||
|
f"{b['p_pass']-a['p_pass']:>+13.1f}pp {b['median_days']:>11.0f}")
|
||||||
|
|
||||||
|
print(f"\n FUNDED $100k — {lens}")
|
||||||
|
print(f" {'config':>28} {'leva':>5} {'P(vivo 1a)':>11} {'E[payout]':>11} {'EUR/g':>7}")
|
||||||
|
for n, s in cfg.items():
|
||||||
|
r = s.values.astype(float)
|
||||||
|
for k in (0.5, 0.75, 1.0):
|
||||||
|
f = sim_funded(r, k, intraday, 100_000.0, n_paths=4000)
|
||||||
|
print(f" {n:>28} {k:>5.2f} {f['p_alive']:>11.1%} ${f['e_payout']:>10,.0f} "
|
||||||
|
f"{f['eur_day']:>7.2f}")
|
||||||
|
|
||||||
|
# --- EV del biglietto (lente intraday = quella su cui si decide, piu' close-only come tetto)
|
||||||
|
print("\n" + "-" * 100)
|
||||||
|
print(" EV DEL BIGLIETTO — config B, eval a leva 1.0x, funded a leva ottima, 1 anno")
|
||||||
|
print(" (la fee e' rimborsata al pass: il costo atteso e' fee x P(fallire l'eval))")
|
||||||
|
print("-" * 100)
|
||||||
|
r = cfg["B + XS01 (19 alt perp)"].values.astype(float)
|
||||||
|
for intraday in (False, True):
|
||||||
|
lens = "intraday" if intraday else "close-only"
|
||||||
|
pe = sim_eval(r, 1.0, consistency=True, intraday=intraday, n_paths=4000)["p_pass"]
|
||||||
|
best = None
|
||||||
|
for k in (0.5, 0.75, 1.0):
|
||||||
|
f = sim_funded(r, k, intraday, 100_000.0, n_paths=4000)
|
||||||
|
if best is None or f["e_payout"] > best[1]["e_payout"]:
|
||||||
|
best = (k, f)
|
||||||
|
print(f"\n [{lens}] P(pass eval) {pe:.1%} | leva funded ottima {best[0]:.2f}x "
|
||||||
|
f"(P(vivo) {best[1]['p_alive']:.1%})")
|
||||||
|
print(f" {'biglietto':>12} {'prezzo':>8} {'E[payout] 1a':>14} {'costo atteso':>13} "
|
||||||
|
f"{'EV':>10} {'EUR/g atteso':>13}")
|
||||||
|
for size, price in TICKETS:
|
||||||
|
scale = size / 100_000.0
|
||||||
|
ev_payout = pe * best[1]["e_payout"] * scale
|
||||||
|
cost = price * (1.0 - pe) # la fee e' rimborsata al pass -> si perde solo se si fallisce
|
||||||
|
ev = ev_payout - cost
|
||||||
|
print(f" ${size:>11,} ${price:>7,.0f} ${ev_payout:>13,.0f} "
|
||||||
|
f"${cost:>12,.0f} ${ev:>9,.0f} {ev*(1-TAX_RATE)/EURUSD/365:>13.2f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
"""r0725_ib10k — E SE METTO 10K SU IB? Lo split ottimale Deribit/IB (2026-07-25).
|
||||||
|
|
||||||
|
Domanda dell'operatore, posta dopo l'ondata rendita: "se metto 10K in IB?".
|
||||||
|
Non e' una domanda su GTAA01: e' una domanda di ALLOCAZIONE FRA DUE VENUE, e le due
|
||||||
|
gambe hanno profili opposti:
|
||||||
|
|
||||||
|
* DERIBIT (TP01+SKH01): Sharpe ~0.98 de-luck, CAGR ~11%, vol 11.4% — e' il MOTORE di reddito.
|
||||||
|
* IB (GTAA01): Sharpe ~0.6, CAGR ~3.5-5%, vol ~7% — e' un DIVERSIFICATORE (corr ~+0.10),
|
||||||
|
e a $600 era negativo per il pavimento di commissione; a $10k il pavimento diventa
|
||||||
|
trascurabile (0.02% per ordine invece dell'1-3%) -> **il deposito SBLOCCA lo sleeve**.
|
||||||
|
|
||||||
|
Tre effetti che si sommano e che vanno contati INSIEME:
|
||||||
|
1. sblocco di GTAA01 (il pavimento IB smette di mordere);
|
||||||
|
2. spostamento del baricentro su un venue a CAGR basso (se ci va TUTTO, il reddito CALA);
|
||||||
|
3. **fisco diverso**: plusvalenze ETF 26% vs crypto 33% (L.199/2025) -> un euro lordo su IB
|
||||||
|
vale il 10% in piu' netto di un euro lordo su Deribit.
|
||||||
|
(+ pavimento alternativo: la liquidita' IB non investita rende interesse ~2% EUR / ~4% USD.)
|
||||||
|
|
||||||
|
Output: la curva dello split, giudicata su TRE metriche diverse perche' danno risposte diverse —
|
||||||
|
reddito atteso netto, rendita perpetua (preservazione del capitale), e reddito a ISO-RISCHIO.
|
||||||
|
|
||||||
|
ONESTA': (a) SKH01 sul path live, de-luck x0.6 (lente di decisione del progetto);
|
||||||
|
(b) GTAA01 col costo IB REALE (pavimento fisso) e banda/cadenza scelte sul plateau;
|
||||||
|
(c) le aliquote sono l'assunzione dichiarata 26%/33% — da verificare col commercialista,
|
||||||
|
NON un parere fiscale; (d) i numeri di GTAA01 sono su ~30 anni, quelli crypto su ~7.
|
||||||
|
|
||||||
|
Uso: `uv run python scripts/research/r0725_ib10k.py`
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
from src.portfolio.portfolio import combine_outer, metrics # noqa: E402
|
||||||
|
|
||||||
|
import r0724_skh_live_weight as skl # noqa: E402
|
||||||
|
from r0725_capcurve import (EURUSD, gtaa_banded, metrics as _m, survival, # noqa: E402
|
||||||
|
tp01_realistic)
|
||||||
|
|
||||||
|
TAX_CRYPTO = 0.33 # L.199/2025 dal 1/1/2026
|
||||||
|
TAX_EQUITY = 0.26 # plusvalenze su ETF (assunzione dichiarata)
|
||||||
|
IB_CASH_RATE = 0.02 # interesse su liquidita EUR non investita (IBKR, sopra $10k)
|
||||||
|
IB_MARGIN_RATE = 0.055 # costo del margine IB in USD (benchmark + spread) — assunzione dichiarata
|
||||||
|
DERIBIT_FUNDING = 0.02 # funding medio dei perp Deribit (ricerca 24/07: ciclo 0-4%/anno)
|
||||||
|
DELUCK = 0.6
|
||||||
|
DERIBIT_NOW = 600.0 # cio' che c'e' oggi sul conto Deribit
|
||||||
|
BOOK_START = pd.Timestamp("2019-03-14", tz="UTC")
|
||||||
|
|
||||||
|
|
||||||
|
def _d(s: pd.Series) -> pd.Series:
|
||||||
|
s = s.dropna().sort_index()
|
||||||
|
if s.index.tz is None:
|
||||||
|
s.index = s.index.tz_localize("UTC")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def _deluck(s: pd.Series) -> pd.Series:
|
||||||
|
return s - (1.0 - DELUCK) * float(s.mean())
|
||||||
|
|
||||||
|
|
||||||
|
def gtaa_best_band(alloc: float) -> tuple[float, int, dict]:
|
||||||
|
"""Config di costo per GTAA al capitale dato, scelta sul PLATEAU (weekly, banda 25-100):
|
||||||
|
non e' un parametro di segnale, si sceglie sul costo e si dichiara."""
|
||||||
|
best = None
|
||||||
|
for every in (5, 21):
|
||||||
|
for band in (25.0, 50.0, 100.0):
|
||||||
|
if band > alloc / 6:
|
||||||
|
continue
|
||||||
|
m = metrics(gtaa_banded(alloc, band, every))
|
||||||
|
if best is None or m["sharpe"] > best[2]["sharpe"]:
|
||||||
|
best = (band, every, m)
|
||||||
|
if best is None: # capitale troppo piccolo per una banda
|
||||||
|
best = (1.0, 21, metrics(gtaa_banded(alloc, 1.0, 21)))
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
def build(total: float, f_ib: float) -> dict:
|
||||||
|
"""Book con frazione f_ib del capitale su IB (GTAA01) e il resto su Deribit (TP01/SKH01)."""
|
||||||
|
a_ib, a_der = total * f_ib, total * (1.0 - f_ib)
|
||||||
|
skh = _d(skl.skh_book(0, "hourly"))
|
||||||
|
cols, w = {}, {}
|
||||||
|
if a_der > 1.0:
|
||||||
|
cols["TP01"] = _deluck(_d(tp01_realistic(a_der * 0.75)))
|
||||||
|
cols["SKH01"] = _deluck(skh)
|
||||||
|
w["TP01"] = (1.0 - f_ib) * 0.75
|
||||||
|
w["SKH01"] = (1.0 - f_ib) * 0.25
|
||||||
|
band = every = None
|
||||||
|
if a_ib > 1.0:
|
||||||
|
band, every, _ = gtaa_best_band(a_ib)
|
||||||
|
cols["GTAA01"] = _deluck(_d(gtaa_banded(a_ib, band, every)))
|
||||||
|
w["GTAA01"] = f_ib
|
||||||
|
combo = combine_outer(cols, w, lo=BOOK_START)
|
||||||
|
m = metrics(combo)
|
||||||
|
vol = float(combo.std() * np.sqrt(365))
|
||||||
|
# fisco misto: la quota IB e' tassata al 26%, la quota Deribit al 33%
|
||||||
|
tax_blend = f_ib * TAX_EQUITY + (1.0 - f_ib) * TAX_CRYPTO
|
||||||
|
eur_day = total * m["cagr"] * (1.0 - tax_blend) / EURUSD / 365.0
|
||||||
|
return dict(metrics=m, vol=vol, combo=combo, tax=tax_blend, eur_day=eur_day,
|
||||||
|
band=band, every=every, a_ib=a_ib, a_der=a_der)
|
||||||
|
|
||||||
|
|
||||||
|
def perp_rate(combo: pd.Series, k: float = 1.0, n: int = 1500) -> float:
|
||||||
|
"""Prelievo perpetuo (P(cap a 20a >= cap iniziale) >= 90%)."""
|
||||||
|
r = combo.values.astype(float)
|
||||||
|
lo, hi = 0.0, 0.35
|
||||||
|
for _ in range(12):
|
||||||
|
mid = (lo + hi) / 2
|
||||||
|
if survival(r, 1e6, 1e6 * mid, 20, k, n_paths=n)["p10_end"] >= 1e6:
|
||||||
|
lo = mid
|
||||||
|
else:
|
||||||
|
hi = mid
|
||||||
|
return lo
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
total = DERIBIT_NOW + 10_000.0 * EURUSD # $600 + EUR 10k
|
||||||
|
print("=" * 100)
|
||||||
|
print(f" SE METTO 10K SU IB — capitale totale ${total:,.0f} "
|
||||||
|
f"(= $600 Deribit + EUR 10.000)")
|
||||||
|
print("=" * 100)
|
||||||
|
print(f" fisco assunto: ETF {TAX_EQUITY:.0%} / crypto {TAX_CRYPTO:.0%} | de-luck x{DELUCK} | "
|
||||||
|
f"SKH01 path live")
|
||||||
|
print(f" pavimento alternativo: liquidita' IB non investita ~{IB_CASH_RATE:.0%}/anno EUR "
|
||||||
|
f"= EUR {total/EURUSD*IB_CASH_RATE*(1-TAX_EQUITY)/365:.2f}/g\n")
|
||||||
|
|
||||||
|
print(f" {'quota su IB':>12} {'GTAA banda':>12} {'Sharpe':>7} {'CAGR':>7} {'vol':>6} "
|
||||||
|
f"{'maxDD':>7} {'fisco':>6} {'EUR/g':>7} {'rendita':>8} {'EUR/g rend.':>12}")
|
||||||
|
rows = []
|
||||||
|
for f_ib in (0.0, 0.25, 0.50, 0.75, 0.90, 0.94, 1.0):
|
||||||
|
b = build(total, f_ib)
|
||||||
|
pr = perp_rate(b["combo"])
|
||||||
|
eur_perp = total * pr * (1.0 - b["tax"]) / EURUSD / 365.0
|
||||||
|
lbl = "—" if b["band"] is None else f"${b['band']:.0f}/{ 'sett' if b['every']==5 else 'mens'}"
|
||||||
|
print(f" {f_ib:>11.0%} {lbl:>12} {b['metrics']['sharpe']:>7.2f} "
|
||||||
|
f"{b['metrics']['cagr']:>7.1%} {b['vol']:>6.1%} {b['metrics']['maxdd']:>7.1%} "
|
||||||
|
f"{b['tax']:>6.0%} {b['eur_day']:>7.2f} {pr:>8.2%} {eur_perp:>12.2f}")
|
||||||
|
rows.append((f_ib, b, pr))
|
||||||
|
print("\n ('quota su IB' 94% = mettere i 10k su IB e lasciare i $600 dove sono;")
|
||||||
|
print(" 'rendita' = prelievo perpetuo al 90% di preservazione del capitale.)")
|
||||||
|
|
||||||
|
# --- ISO-RISCHIO: e' qui che un diversificatore a basso CAGR si giudica
|
||||||
|
print("\n" + "-" * 100)
|
||||||
|
print(" A ISO-RISCHIO (ogni split portato a vol 15% con la leva) — il confronto corretto")
|
||||||
|
print("-" * 100)
|
||||||
|
print(f" {'quota su IB':>12} {'leva':>6} {'CAGR iso':>9} {'EUR/g iso':>10} "
|
||||||
|
f"{'rendita iso':>12} {'EUR/g rend.iso':>15}")
|
||||||
|
best = None
|
||||||
|
for f_ib, b, _ in rows:
|
||||||
|
k = min(0.15 / b["vol"], 3.0)
|
||||||
|
pr = perp_rate(b["combo"], k=k)
|
||||||
|
eur = total * b["metrics"]["cagr"] * k * (1.0 - b["tax"]) / EURUSD / 365.0
|
||||||
|
eur_p = total * pr * (1.0 - b["tax"]) / EURUSD / 365.0
|
||||||
|
print(f" {f_ib:>11.0%} {k:>6.2f}x {b['metrics']['cagr']*k:>9.1%} {eur:>10.2f} "
|
||||||
|
f"{pr:>12.2%} {eur_p:>15.2f}")
|
||||||
|
if best is None or eur_p > best[1]:
|
||||||
|
best = (f_ib, eur_p, k, pr)
|
||||||
|
print(f"\n -> split che massimizza la RENDITA a iso-rischio: {best[0]:.0%} su IB "
|
||||||
|
f"(leva {best[2]:.2f}x, rendita {best[3]:.2%} = EUR {best[1]:.2f}/g)")
|
||||||
|
|
||||||
|
# --- ISO-RISCHIO CON VINCOLI DI LEVA REALI -------------------------------
|
||||||
|
# La leva che l'iso-rischio chiede non e' sempre ottenibile: su IB un conto da EUR 10k ha
|
||||||
|
# Reg-T (2x max sugli ETF); il portfolio margin parte da ~$110k. Su Deribit i perp danno
|
||||||
|
# leva alta. La leva ATTUABILE del book misto e' quindi limitata dalla gamba peggiore.
|
||||||
|
print("\n" + "-" * 100)
|
||||||
|
print(" ISO-RISCHIO CON LEVA ATTUABILE (IB Reg-T 2.0x su ETF; Deribit perp 3.0x)")
|
||||||
|
print("-" * 100)
|
||||||
|
print(f" {'quota su IB':>12} {'leva max':>9} {'leva usata':>11} {'CAGR':>7} "
|
||||||
|
f"{'EUR/g':>7} {'rendita':>9} {'EUR/g rend.':>12}")
|
||||||
|
print(" (COSTO DEL MARGINE INCLUSO: la parte presa a prestito si paga — IB ~5.5%/anno sul")
|
||||||
|
print(" margine USD, Deribit funding perp ~2%/anno medio. Ignorarlo gonfia ogni riga levata.)")
|
||||||
|
print(f" {'quota su IB':>12} {'leva max':>9} {'leva usata':>11} {'CAGR lordo':>11} "
|
||||||
|
f"{'costo leva':>11} {'CAGR netto':>11} {'EUR/g':>7} {'rendita':>9} {'EUR/g rend.':>12}")
|
||||||
|
best2 = None
|
||||||
|
for f_ib, b, _ in rows:
|
||||||
|
k_want = min(0.15 / b["vol"], 3.0)
|
||||||
|
k_max = f_ib * 2.0 + (1.0 - f_ib) * 3.0 # vincolo pesato per venue
|
||||||
|
k = min(k_want, k_max)
|
||||||
|
borrow_rate = f_ib * IB_MARGIN_RATE + (1.0 - f_ib) * DERIBIT_FUNDING
|
||||||
|
fin = max(k - 1.0, 0.0) * borrow_rate # costo sulla quota presa a prestito
|
||||||
|
cagr_gross = b["metrics"]["cagr"] * k
|
||||||
|
cagr_net = cagr_gross - fin
|
||||||
|
pr = max(perp_rate(b["combo"], k=k) - fin, 0.0) # il costo grava anche sul prelievo
|
||||||
|
eur_p = total * pr * (1.0 - b["tax"]) / EURUSD / 365.0
|
||||||
|
eur = total * cagr_net * (1.0 - b["tax"]) / EURUSD / 365.0
|
||||||
|
flag = " (leva TAGLIATA)" if k < k_want - 1e-9 else ""
|
||||||
|
print(f" {f_ib:>11.0%} {k_max:>8.2f}x {k:>10.2f}x {cagr_gross:>11.1%} "
|
||||||
|
f"{fin:>11.1%} {cagr_net:>11.1%} {eur:>7.2f} {pr:>9.2%} {eur_p:>12.2f}{flag}")
|
||||||
|
if best2 is None or eur_p > best2[1]:
|
||||||
|
best2 = (f_ib, eur_p, k, pr)
|
||||||
|
print(f"\n -> con leva ATTUABILE E costo del margine il migliore e' {best2[0]:.0%} su IB "
|
||||||
|
f"(leva {best2[2]:.2f}x, rendita {best2[3]:.2%} = EUR {best2[1]:.2f}/g)")
|
||||||
|
|
||||||
|
# --- il confronto che conta: GTAA vs LIQUIDITA' FERMA ---------------------
|
||||||
|
print("\n" + "-" * 100)
|
||||||
|
print(" IL CONFRONTO ONESTO: GTAA01 sui 10k vs la stessa liquidita' FERMA che prende interesse")
|
||||||
|
print("-" * 100)
|
||||||
|
b94 = [b for f, b, _ in rows if abs(f - 0.94) < 1e-9][0]
|
||||||
|
cash_eur_day = total / EURUSD * IB_CASH_RATE * (1 - TAX_EQUITY) / 365
|
||||||
|
print(f" 10k investiti in GTAA01 (+ $600 su Deribit) : EUR {b94['eur_day']:.2f}/g "
|
||||||
|
f"maxDD {b94['metrics']['maxdd']:.1%}")
|
||||||
|
print(f" 10k FERMI a interesse ~{IB_CASH_RATE:.0%} (+ $600 Deribit): EUR "
|
||||||
|
f"{cash_eur_day + rows[0][1]['eur_day']*DERIBIT_NOW/total:.2f}/g maxDD ~0%")
|
||||||
|
print(f" -> il premio di GTAA01 sopra la liquidita' e' ~EUR "
|
||||||
|
f"{b94['eur_day'] - cash_eur_day:.2f}/g, pagato con un maxDD del "
|
||||||
|
f"{b94['metrics']['maxdd']:.0%}.")
|
||||||
|
print(" (tasso liquidita' = assunzione dichiarata; un BTP/monetario EUR renderebbe di piu'.)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
"""r0725_prop_config — QUALE BOOK mandare su un conto funded (2026-07-25).
|
||||||
|
|
||||||
|
Il 24/07 (`r0724_goal50_math.py` parte C + `r0724_goal50_intraday_mc.py`) ha stabilito che il
|
||||||
|
conto funded e' l'unico canale che moltiplica il NOZIONALE senza capitale proprio, e ha
|
||||||
|
quantificato P(pass) e payout — ma sempre e solo per il **book a 2 sleeve** (TP01+SKH01),
|
||||||
|
perche' e' quello eseguibile a $600 sul conto Deribit.
|
||||||
|
|
||||||
|
Il punto che quell'analisi non ha visto: **su un conto funded il capitale non e' piu' $600, e'
|
||||||
|
$100k** — quindi gli sleeve che a casa nostra sono STAT-MODE per taglia (XS01 a ~$20k) DIVENTANO
|
||||||
|
eseguibili. E le regole prop premiano esattamente cio' che la diversificazione produce: passano
|
||||||
|
sul DRAWDOWN e sulla perdita giornaliera, NON sul CAGR. Un book a Sharpe piu' alto e vol piu'
|
||||||
|
bassa dovrebbe passare molto piu' spesso, a parita' di tutto il resto.
|
||||||
|
|
||||||
|
Confronta tre configurazioni CRYPTO-ONLY (cio' che un sub-account di prop firm puo' davvero
|
||||||
|
eseguire — niente GTAA01 che e' equity su IB, niente VRP01 che e' opzioni + regola permanente):
|
||||||
|
A) TP01 75 / SKH01 25 — il book live attuale (quello valutato il 24/07)
|
||||||
|
B) A + XS01 — cross-sectional 19 alt perp: eseguibile a $100k, NON a $600
|
||||||
|
C) B + XSR01 — contro-fattuale (gate pre-registrato 2026-10-23, non deployato)
|
||||||
|
|
||||||
|
ONESTA' (limiti dichiarati, ereditati e nuovi):
|
||||||
|
1. **Close-only**: il MC gira sulle CHIUSURE giornaliere. C-bis del 24/07 ha dimostrato che
|
||||||
|
i wick intraday tagliano 6-37pp di P(pass) -> **i livelli ASSOLUTI qui sono un TETTO**.
|
||||||
|
Il ricostruttore MTM intraday esiste solo per TP01+SKH01 (feed 5m); XS01/XSR01 sono 1d
|
||||||
|
nativi su Hyperliquid e il loro intraday NON e' ricostruibile dai nostri dati -> non lo
|
||||||
|
invento. Cio' che questo script misura onestamente e' il **DELTA fra configurazioni**,
|
||||||
|
e il delta e' CONSERVATIVO: i book B/C hanno vol piu' bassa, quindi wick piu' piccoli,
|
||||||
|
quindi subirebbero un haircut intraday MINORE di A.
|
||||||
|
2. **Finestra comune**: XS01 parte dal 2024 -> il confronto principale gira sui giorni in cui
|
||||||
|
TUTTI gli sleeve esistono (2024+, ~2.5 anni). Riportata anche la finestra piena (outer-join)
|
||||||
|
dove pre-2024 i book B/C degradano su A.
|
||||||
|
3. De-luck x0.6 sul drift (convenzione del progetto) come caso di decisione.
|
||||||
|
4. SKH01 sul PATH LIVE (fill orario), non sul backtest.
|
||||||
|
|
||||||
|
Uso: `uv run python scripts/research/r0725_prop_config.py`
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
from src.portfolio.portfolio import combine_outer, metrics # noqa: E402
|
||||||
|
from src.portfolio.sleeves import _xsec_returns # noqa: E402
|
||||||
|
|
||||||
|
import r0724_skh_live_weight as skl # noqa: E402
|
||||||
|
from r0725_capcurve import EURUSD, TAX_RATE, tp01_realistic # noqa: E402
|
||||||
|
|
||||||
|
RNG = np.random.default_rng(20260725)
|
||||||
|
PROP_NOTIONAL = 100_000.0
|
||||||
|
SPLIT = 0.80 # profit split tipico (80% al trader)
|
||||||
|
|
||||||
|
RULES = [
|
||||||
|
("HYRO 1-step tgt10% DD6% dl4%", 0.10, 0.06, 0.04),
|
||||||
|
("FTMO Swing tgt10% DD10% dl5%", 0.10, 0.10, 0.05),
|
||||||
|
]
|
||||||
|
FUNDED_RULES = [("HYRO max-loss 6%", 0.06, 0.04),
|
||||||
|
("FTMO max-loss 10%", 0.10, 0.05)]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- serie book
|
||||||
|
def _d(s: pd.Series) -> pd.Series:
|
||||||
|
s = s.dropna().sort_index()
|
||||||
|
if s.index.tz is None:
|
||||||
|
s.index = s.index.tz_localize("UTC")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def build_configs(deluck: float = 0.6) -> dict:
|
||||||
|
"""Le tre configurazioni, su griglia giornaliera, de-luckate sul drift."""
|
||||||
|
skh = _d(skl.skh_book(0, "hourly"))
|
||||||
|
tp = _d(tp01_realistic(PROP_NOTIONAL * 0.75))
|
||||||
|
xs = _d(_xsec_returns()).resample("1D").apply(lambda x: (1 + x).prod() - 1)
|
||||||
|
|
||||||
|
cfgs = {
|
||||||
|
"A TP01/SKH01 (live oggi)": ({"TP01": tp, "SKH01": skh},
|
||||||
|
{"TP01": 0.75, "SKH01": 0.25}),
|
||||||
|
"B + XS01 (19 alt perp)": ({"TP01": tp, "SKH01": skh, "XS01": xs},
|
||||||
|
{"TP01": 0.55, "SKH01": 0.20, "XS01": 0.25}),
|
||||||
|
}
|
||||||
|
out = {}
|
||||||
|
for name, (cols, w) in cfgs.items():
|
||||||
|
s = combine_outer(cols, w, lo=pd.Timestamp("2019-03-14", tz="UTC"))
|
||||||
|
s = s - (1.0 - deluck) * float(s.mean())
|
||||||
|
out[name] = dict(full=s, cols=cols, w=w)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def common_window(cfgs: dict) -> pd.Timestamp:
|
||||||
|
"""Primo giorno in cui TUTTI gli sleeve di TUTTE le config hanno dato."""
|
||||||
|
starts = []
|
||||||
|
for v in cfgs.values():
|
||||||
|
for s in v["cols"].values():
|
||||||
|
starts.append(s.index.min())
|
||||||
|
return max(starts)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ MC eval
|
||||||
|
def _paths(r: np.ndarray, n_days: int, n_paths: int, block: int = 20) -> np.ndarray:
|
||||||
|
n_blocks = int(np.ceil(n_days / block))
|
||||||
|
starts = RNG.integers(0, len(r) - block, size=(n_paths, n_blocks))
|
||||||
|
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
|
||||||
|
return r[idx]
|
||||||
|
|
||||||
|
|
||||||
|
def sim_eval(r: np.ndarray, target: float, max_dd: float, daily_loss: float,
|
||||||
|
k: float, max_days: int = 365, n_paths: int = 8000) -> dict:
|
||||||
|
R = _paths(r, max_days, n_paths) * k
|
||||||
|
eq = np.cumprod(1 + R, axis=1)
|
||||||
|
eq_start = eq / (1 + R)
|
||||||
|
fail = (eq_start * (1 + R) < (1 - max_dd)) | (R < -daily_loss)
|
||||||
|
passed = eq >= 1 + target
|
||||||
|
ff = np.where(fail.any(axis=1), fail.argmax(axis=1), max_days + 1)
|
||||||
|
fp = np.where(passed.any(axis=1), passed.argmax(axis=1), max_days + 1)
|
||||||
|
ok = fp < ff
|
||||||
|
return dict(p_pass=float(ok.mean()),
|
||||||
|
median_days=float(np.median(fp[ok])) if ok.any() else float("nan"))
|
||||||
|
|
||||||
|
|
||||||
|
def sim_funded(r: np.ndarray, max_loss: float, daily_loss: float, k: float,
|
||||||
|
years: int = 1, n_paths: int = 8000) -> dict:
|
||||||
|
"""Fase funded con regole REALI: max-loss **STATICO** (misurato dal saldo iniziale del
|
||||||
|
conto, non da un picco mobile) + daily-loss; prelievo mensile dell'80% del profitto,
|
||||||
|
dopo il quale il saldo torna alla base (e con esso il buffer di perdita).
|
||||||
|
|
||||||
|
NB: la base di prelievo si aggiorna SOLO al prelievo. Aggiornarla giorno per giorno
|
||||||
|
(come un HWM) azzera per costruzione il guadagno misurato al checkpoint -> payout ~0.
|
||||||
|
E' l'errore che questo modello ha commesso al primo giro ed e' stato corretto.
|
||||||
|
"""
|
||||||
|
n_days = years * 365
|
||||||
|
R = _paths(r, n_days, n_paths) * k
|
||||||
|
eq = np.ones(n_paths) # equity normalizzata alla BASE del conto
|
||||||
|
alive = np.ones(n_paths, bool)
|
||||||
|
payout = np.zeros(n_paths)
|
||||||
|
for t in range(n_days):
|
||||||
|
eq = np.where(alive, eq * (1 + R[:, t]), eq)
|
||||||
|
bust = alive & ((eq < 1.0 - max_loss) | (R[:, t] < -daily_loss))
|
||||||
|
alive &= ~bust
|
||||||
|
if (t + 1) % 30 == 0:
|
||||||
|
gain = np.where(alive & (eq > 1.0), eq - 1.0, 0.0)
|
||||||
|
payout += gain * SPLIT
|
||||||
|
eq = np.where(alive & (eq > 1.0), 1.0, eq) # profitto prelevato, base ripristinata
|
||||||
|
usd = payout * PROP_NOTIONAL
|
||||||
|
return dict(p_alive=float(alive.mean()),
|
||||||
|
e_payout=float(usd.mean()),
|
||||||
|
med_payout=float(np.median(usd)),
|
||||||
|
eur_day_mean=float(usd.mean() * (1 - TAX_RATE) / EURUSD / 365.0),
|
||||||
|
eur_day_med=float(np.median(usd) * (1 - TAX_RATE) / EURUSD / 365.0))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("=" * 104)
|
||||||
|
print(" QUALE BOOK su un conto FUNDED — il 24/07 ha valutato solo il book a 2 sleeve")
|
||||||
|
print("=" * 104)
|
||||||
|
cfgs = build_configs()
|
||||||
|
lo = common_window(cfgs)
|
||||||
|
print(f"\n finestra comune (tutti gli sleeve esistono): dal {lo.date()}")
|
||||||
|
print(" NB: livelli ASSOLUTI = TETTO (close-only; C-bis 24/07: i wick tagliano 6-37pp).")
|
||||||
|
print(" Il DELTA fra config e' la misura onesta, ed e' CONSERVATIVO (B ha vol minore).\n")
|
||||||
|
|
||||||
|
for win, wlabel in ((lo, "FINESTRA COMUNE (2024+)"), (None, "FINESTRA PIENA (outer-join)")):
|
||||||
|
print("-" * 104)
|
||||||
|
print(f" {wlabel}")
|
||||||
|
print("-" * 104)
|
||||||
|
stats = {}
|
||||||
|
for name, v in cfgs.items():
|
||||||
|
s = v["full"] if win is None else v["full"][v["full"].index >= win]
|
||||||
|
m = metrics(s)
|
||||||
|
vol = float(s.std() * np.sqrt(365))
|
||||||
|
stats[name] = (s.values.astype(float), m, vol)
|
||||||
|
print(f" {name:>28} Sharpe {m['sharpe']:>5.2f} CAGR {m['cagr']:>6.1%} "
|
||||||
|
f"vol {vol:>5.1%} maxDD {m['maxdd']:>5.1%}")
|
||||||
|
|
||||||
|
for rname, tgt, dd, dl in RULES:
|
||||||
|
print(f"\n EVAL — {rname}")
|
||||||
|
print(f" {'config':>28} {'leva':>5} {'P(pass)':>8} {'giorni med':>11}")
|
||||||
|
for name, (r, m, vol) in stats.items():
|
||||||
|
for k in (0.75, 1.0, 1.5):
|
||||||
|
e = sim_eval(r, tgt, dd, dl, k)
|
||||||
|
print(f" {name:>28} {k:>5.2f} {e['p_pass']:>8.1%} "
|
||||||
|
f"{e['median_days']:>11.0f}")
|
||||||
|
|
||||||
|
print(f"\n FUNDED ${PROP_NOTIONAL:,.0f} — 1 anno, split {SPLIT:.0%}, prelievi mensili")
|
||||||
|
print(f" {'config':>28} {'regola':>20} {'leva':>5} {'P(vivo)':>8} "
|
||||||
|
f"{'E[payout]':>11} {'EUR/g atteso':>13}")
|
||||||
|
for name, (r, m, vol) in stats.items():
|
||||||
|
for fname, ml, dl in FUNDED_RULES:
|
||||||
|
for k in (0.5, 0.75, 1.0):
|
||||||
|
f = sim_funded(r, ml, dl, k)
|
||||||
|
print(f" {name:>28} {fname:>20} {k:>5.2f} {f['p_alive']:>8.1%} "
|
||||||
|
f"${f['e_payout']:>10,.0f} {f['eur_day_mean']:>13.2f}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
"""r0725_prop_ladder — I 600 EURO COME BIGLIETTO: la scala di conti funded (2026-07-25).
|
||||||
|
|
||||||
|
Domanda del goal: "puoi usare i 600 euro come vuoi, puoi ipotizzare di creare qualcosa".
|
||||||
|
`r0725_capcurve.py` ha refutato i 600 euro come CAPITALE (rendita 50 EUR/g -> muro $300-500k,
|
||||||
|
0% dei path in 20 anni senza depositi). Qui i 600 euro sono usati come **biglietto**: si compra
|
||||||
|
un'evaluation, il conto funded moltiplica il NOZIONALE senza possedere capitale, i payout
|
||||||
|
comprano altri biglietti. E' l'unica leva che non richiede depositi.
|
||||||
|
|
||||||
|
Il 24/07 si era fermato a UN conto ($100k -> ~EUR 14.5/g atteso, cap $200k per firm). Ma
|
||||||
|
**50 EUR/g richiede piu' conti in parallelo**, e qui c'e' il problema che nessuno ha ancora
|
||||||
|
studiato: **N conti che girano lo STESSO book bustano INSIEME** (correlazione 1.0). La
|
||||||
|
diversificazione fra conti e' illusoria salvo assegnare **sleeve DIVERSI a conti diversi**.
|
||||||
|
|
||||||
|
Da cui la domanda di ricerca vera, che e' un problema di costruzione di portafoglio sotto
|
||||||
|
barriera di rovina PER-CONTO:
|
||||||
|
- CONCENTRARE: ogni conto gira il book diversificato (Sharpe alto per conto -> P(vivo) alta
|
||||||
|
per conto, ma i conti sono perfettamente correlati -> o vivono tutti o muoiono tutti);
|
||||||
|
- SPARPAGLIARE: ogni conto gira UNO sleeve (Sharpe basso per conto -> P(vivo) piu' bassa,
|
||||||
|
ma le morti sono INDIPENDENTI -> il reddito aggregato e' molto piu' stabile).
|
||||||
|
Non e' ovvio quale vinca: la barriera di rovina e' convessa, il reddito e' lineare.
|
||||||
|
|
||||||
|
Modello (tutto dichiarato):
|
||||||
|
* bootstrap a blocchi CONGIUNTO dei 3 sleeve (stessi indici di blocco per tutti -> la
|
||||||
|
correlazione fra sleeve e' quella REALE, non imposta);
|
||||||
|
* finestra comune 2024+ (XS01 nasce li'); de-luck x0.6 sul drift;
|
||||||
|
* eval -> funded con le regole vere (max-loss STATICO, daily-loss), payout mensile 80%,
|
||||||
|
prelievo immediato (mai lasciare credito alla firm: regola del 24/07);
|
||||||
|
* bust = biglietto perso; la cassa ricompra se se lo puo' permettere;
|
||||||
|
* cap per firm; **rischio controparte**: ogni anno una firm puo' sparire (il settore ha perso
|
||||||
|
il 13% delle firm nel 2024) -> i conti aperti su quella firm valgono zero;
|
||||||
|
* fisco 33% sui payout.
|
||||||
|
|
||||||
|
ONESTA' — il risultato va letto come una BANDA, non come un punto. Si gira con due lenti:
|
||||||
|
* CLOSE-ONLY = **TETTO OTTIMISTA**: le regole prop scattano sull'equity intraday, non sulla
|
||||||
|
chiusura (C-bis del 24/07: i wick tagliano fino a 37pp di P(pass) e 48pp di P(vivo)).
|
||||||
|
* INTRADAY = **PAVIMENTO PESSIMISTA**: il gap e' lognormale calibrato sui percentili del recon
|
||||||
|
MTM 1h, ma estratto **INDIPENDENTE dal rendimento del giorno**. Nella realta' i wick profondi
|
||||||
|
stanno sulle giornate brutte (sono ACCOPPIATI): estrarli indipendenti aggiunge finti tuffi
|
||||||
|
anche nelle giornate buone -> breach spuri -> stima troppo severa. Il modo giusto e' il
|
||||||
|
bootstrap delle TUPLE (ritorno, gap) come in `r0724_goal50_intraday_mc.py`, che pero' ha il
|
||||||
|
recon solo per TP01+SKH01 (XS01 e' 1d nativo su Hyperliquid, intraday non ricostruibile).
|
||||||
|
-> FOLLOW-UP dichiarato, non simulato qui.
|
||||||
|
La verita' sta FRA le due lenti. Il confronto fra POLITICHE, invece, e' robusto: e' la stessa
|
||||||
|
lente applicata a tutte. La finestra 2024+ e' anche quella di scoperta di XS01 -> le config che
|
||||||
|
lo contengono sono favorite; "CONC-2SL" (solo TP01/SKH01) e' il controllo pulito.
|
||||||
|
|
||||||
|
Uso: `uv run python scripts/research/r0725_prop_ladder.py`
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
from src.portfolio.portfolio import metrics # noqa: E402
|
||||||
|
from src.portfolio.sleeves import _xsec_returns # noqa: E402
|
||||||
|
|
||||||
|
import r0724_skh_live_weight as skl # noqa: E402
|
||||||
|
from r0725_capcurve import EURUSD, TAX_RATE, tp01_realistic # noqa: E402
|
||||||
|
|
||||||
|
TARGET_EUR_DAY = 50.0
|
||||||
|
DELUCK = 0.6
|
||||||
|
SPLIT = 0.80
|
||||||
|
FIRM_DEATH_PER_YEAR = 0.10 # il settore ha perso il 13% delle firm nel 2024 (ricerca 24/07)
|
||||||
|
START_EUR = 600.0
|
||||||
|
HORIZON_MONTHS = 36
|
||||||
|
MAX_CONCURRENT = 6
|
||||||
|
|
||||||
|
# (nome, [(nozionale, prezzo biglietto)], eval(target,dd,daily), funded(max_loss,daily), cap)
|
||||||
|
FIRMS = [
|
||||||
|
dict(name="HYRO", tickets=[(25_000, 249.0), (50_000, 379.0), (100_000, 579.0)],
|
||||||
|
ev=(0.10, 0.06, 0.04), fu=(0.06, 0.04), cap=200_000),
|
||||||
|
dict(name="FTMO", tickets=[(50_000, 345.0), (100_000, 540.0), (200_000, 1_080.0)],
|
||||||
|
ev=(0.10, 0.10, 0.05), fu=(0.10, 0.05), cap=400_000),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ sleeve
|
||||||
|
def _d(s: pd.Series) -> pd.Series:
|
||||||
|
s = s.dropna().sort_index()
|
||||||
|
if s.index.tz is None:
|
||||||
|
s.index = s.index.tz_localize("UTC")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def sleeve_panel() -> tuple[np.ndarray, list[str]]:
|
||||||
|
"""Matrice (giorni x sleeve) sulla finestra comune, de-luckata sul drift."""
|
||||||
|
tp = _d(tp01_realistic(100_000 * 0.75))
|
||||||
|
skh = _d(skl.skh_book(0, "hourly"))
|
||||||
|
xs = _d(_xsec_returns()).resample("1D").apply(lambda x: (1 + x).prod() - 1)
|
||||||
|
J = pd.concat({"TP01": tp, "SKH01": skh, "XS01": xs}, axis=1, join="inner").fillna(0.0)
|
||||||
|
M = J.to_numpy(float)
|
||||||
|
M = M - (1.0 - DELUCK) * M.mean(axis=0, keepdims=True) # de-luck: taglia il drift
|
||||||
|
return M, list(J.columns)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- politiche
|
||||||
|
def policies(names: list[str]) -> dict:
|
||||||
|
"""Ogni politica = lista di vettori-peso sugli sleeve, uno per 'slot' di conto.
|
||||||
|
Gli slot si riempiono in ordine man mano che la cassa permette biglietti."""
|
||||||
|
i = {n: k for k, n in enumerate(names)}
|
||||||
|
n = len(names)
|
||||||
|
|
||||||
|
def w(**kw):
|
||||||
|
v = np.zeros(n)
|
||||||
|
for k, x in kw.items():
|
||||||
|
v[i[k]] = x
|
||||||
|
return v
|
||||||
|
|
||||||
|
book_div = w(TP01=0.55, SKH01=0.20, XS01=0.25)
|
||||||
|
book_2sl = w(TP01=0.75, SKH01=0.25)
|
||||||
|
return {
|
||||||
|
"CONC-2SL ogni conto = book live (TP01/SKH01)": [book_2sl] * MAX_CONCURRENT,
|
||||||
|
"CONC-DIV ogni conto = book diversificato": [book_div] * MAX_CONCURRENT,
|
||||||
|
"SPARSO 1 sleeve per conto (a rotazione)": [w(TP01=1.0), w(XS01=1.0), w(SKH01=1.0),
|
||||||
|
w(TP01=1.0), w(XS01=1.0), w(SKH01=1.0)],
|
||||||
|
"MISTO meta' diversificati, meta' single": [book_div, w(XS01=1.0), book_div,
|
||||||
|
w(TP01=1.0), book_div, w(XS01=1.0)],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------- sim
|
||||||
|
# --- geometria dei WICK (dal recon MTM 1h del book, r0724_goal50_intraday_mc.py):
|
||||||
|
# gap = (minimo intraday - chiusura), in punti di equity: p50 -0.17pp, p10 -1.03pp, p1 -2.7pp.
|
||||||
|
# Le regole prop scattano sull'equity INTRADAY, non sulla chiusura -> ignorarli gonfia P(pass)
|
||||||
|
# e P(vivo) (C-bis 24/07: fino a -37pp e -48pp). Qui il gap e' lognormale calibrato su quei
|
||||||
|
# percentili e SCALATO sulla vol della configurazione (un book meno volatile ha wick minori).
|
||||||
|
WICK_MU, WICK_SIGMA = np.log(0.0017), 1.30
|
||||||
|
WICK_REF_VOL = 0.114 # vol annua del book TP01/SKH01 su cui la geometria e' misurata
|
||||||
|
|
||||||
|
|
||||||
|
def wick_percentiles() -> dict:
|
||||||
|
"""Percentili impliciti della calibrazione — stampati per essere verificabili."""
|
||||||
|
q = {p: float(np.exp(WICK_MU + WICK_SIGMA * np.sqrt(2) * _erfinv(2 * p - 1)))
|
||||||
|
for p in (0.50, 0.90, 0.99)}
|
||||||
|
return q
|
||||||
|
|
||||||
|
|
||||||
|
def _erfinv(x: float) -> float:
|
||||||
|
from math import sqrt
|
||||||
|
from statistics import NormalDist
|
||||||
|
return NormalDist().inv_cdf((x + 1) / 2) / sqrt(2)
|
||||||
|
|
||||||
|
|
||||||
|
def simulate(M: np.ndarray, slots: list[np.ndarray], n_paths: int, lev: float,
|
||||||
|
seed: int, block: int = 20, intraday: bool = False) -> dict:
|
||||||
|
"""Una politica, n_paths traiettorie di HORIZON_MONTHS mesi.
|
||||||
|
|
||||||
|
Stato per conto: fase (0=eval, 1=funded), equity normalizzata alla base, firm, nozionale.
|
||||||
|
Il bootstrap dei ritorni degli sleeve e' CONGIUNTO (stesso blocco per tutti gli sleeve e
|
||||||
|
per tutti i conti dello stesso path) -> due conti che girano lo stesso book sono
|
||||||
|
perfettamente correlati, due conti su sleeve diversi hanno la correlazione REALE.
|
||||||
|
"""
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
n_days = HORIZON_MONTHS * 30
|
||||||
|
n_sl = M.shape[1]
|
||||||
|
n_blocks = int(np.ceil(n_days / block))
|
||||||
|
starts = rng.integers(0, len(M) - block, size=(n_paths, n_blocks))
|
||||||
|
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
|
||||||
|
R = M[idx] # (paths, days, sleeve)
|
||||||
|
|
||||||
|
n_slot = len(slots)
|
||||||
|
W = np.stack(slots) # (slot, sleeve)
|
||||||
|
# ritorni per (path, day, slot) = R . W^T, con leva
|
||||||
|
Rs = np.einsum("pds,ks->pdk", R, W) * lev
|
||||||
|
# gap intraday per (path, day, slot), scalato sulla vol della config: un book meno volatile
|
||||||
|
# ha wick proporzionalmente piu' piccoli. Il breach si valuta sul MINIMO (chiusura + gap).
|
||||||
|
if intraday:
|
||||||
|
vol_k = Rs.reshape(-1, n_slot).std(axis=0) * np.sqrt(365.0)
|
||||||
|
scale = np.clip(vol_k / WICK_REF_VOL, 0.0, 3.0)
|
||||||
|
G = -np.exp(rng.normal(WICK_MU, WICK_SIGMA, size=Rs.shape)) * scale[None, None, :]
|
||||||
|
else:
|
||||||
|
G = None
|
||||||
|
|
||||||
|
cash = np.full(n_paths, START_EUR * EURUSD)
|
||||||
|
phase = np.full((n_paths, n_slot), -1, np.int8) # -1 vuoto, 0 eval, 1 funded
|
||||||
|
eq = np.ones((n_paths, n_slot))
|
||||||
|
firm = np.full((n_paths, n_slot), -1, np.int8)
|
||||||
|
notional = np.zeros((n_paths, n_slot))
|
||||||
|
days_in = np.zeros((n_paths, n_slot), int)
|
||||||
|
firm_alive = np.ones((n_paths, len(FIRMS)), bool)
|
||||||
|
cum_payout = np.zeros(n_paths) # netto fisco, USD
|
||||||
|
payout_last_year = np.zeros(n_paths)
|
||||||
|
spent = np.zeros(n_paths)
|
||||||
|
|
||||||
|
p_death_day = 1.0 - (1.0 - FIRM_DEATH_PER_YEAR) ** (1 / 365.0)
|
||||||
|
|
||||||
|
for t in range(n_days):
|
||||||
|
# --- morte di una firm (rischio controparte)
|
||||||
|
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
|
||||||
|
|
||||||
|
# --- acquisto biglietti negli slot liberi
|
||||||
|
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
|
||||||
|
days_in[buy, k] = 0
|
||||||
|
free = free & ~buy
|
||||||
|
if not free.any():
|
||||||
|
break
|
||||||
|
if not free.any():
|
||||||
|
break
|
||||||
|
|
||||||
|
# --- dinamica dei conti
|
||||||
|
r = Rs[:, t, :] # (paths, slot)
|
||||||
|
act = phase >= 0
|
||||||
|
eq_prev = eq
|
||||||
|
eq = np.where(act, eq * (1.0 + r), eq)
|
||||||
|
# equity peggiore TOCCATA nella giornata: e' su questa che scattano le regole prop
|
||||||
|
if G is None:
|
||||||
|
eq_low, r_low = eq, r
|
||||||
|
else:
|
||||||
|
eq_low = np.where(act, eq_prev * (1.0 + r + G[:, t, :]), eq)
|
||||||
|
r_low = r + G[:, t, :]
|
||||||
|
days_in += act
|
||||||
|
|
||||||
|
for fi, F in enumerate(FIRMS):
|
||||||
|
sel = act & (firm == fi)
|
||||||
|
if not sel.any():
|
||||||
|
continue
|
||||||
|
ev_t, ev_dd, ev_dl = F["ev"]
|
||||||
|
fu_ml, fu_dl = F["fu"]
|
||||||
|
in_ev = sel & (phase == 0)
|
||||||
|
in_fu = sel & (phase == 1)
|
||||||
|
# breach — valutato sull'equity INTRADAY (eq_low), il target sulla CHIUSURA
|
||||||
|
bust_ev = in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl))
|
||||||
|
bust_fu = in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl))
|
||||||
|
bust = bust_ev | bust_fu
|
||||||
|
phase[bust] = -1
|
||||||
|
notional[bust] = 0.0
|
||||||
|
# pass eval
|
||||||
|
passed = in_ev & (eq >= 1.0 + ev_t) & ~bust
|
||||||
|
phase[passed] = 1
|
||||||
|
eq[passed] = 1.0
|
||||||
|
|
||||||
|
# --- payout mensile (prelievo immediato, base ripristinata)
|
||||||
|
if (t + 1) % 30 == 0:
|
||||||
|
fu = phase == 1
|
||||||
|
gain = np.where(fu & (eq > 1.0), (eq - 1.0) * notional, 0.0)
|
||||||
|
gross = gain.sum(axis=1) * SPLIT
|
||||||
|
net = gross * (1.0 - TAX_RATE)
|
||||||
|
cum_payout += net
|
||||||
|
payout_last_year += net
|
||||||
|
cash += net
|
||||||
|
eq = np.where(fu & (eq > 1.0), 1.0, eq)
|
||||||
|
if (t + 1) % 365 == 0 and t + 1 < n_days:
|
||||||
|
payout_last_year[:] = 0.0
|
||||||
|
|
||||||
|
eur_day = cum_payout / EURUSD / (n_days / 365.0) / 365.0
|
||||||
|
live_notional = np.where(phase == 1, notional, 0.0).sum(axis=1)
|
||||||
|
return dict(eur_day=eur_day,
|
||||||
|
p_target=float((eur_day >= TARGET_EUR_DAY).mean()),
|
||||||
|
p_ge10=float((eur_day >= 10.0).mean()),
|
||||||
|
p_zero=float((cum_payout <= 0).mean()),
|
||||||
|
med=float(np.median(eur_day)),
|
||||||
|
p90=float(np.percentile(eur_day, 90)),
|
||||||
|
p10=float(np.percentile(eur_day, 10)),
|
||||||
|
med_notional=float(np.median(live_notional)),
|
||||||
|
med_spent=float(np.median(spent)))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
M, names = sleeve_panel()
|
||||||
|
print("=" * 104)
|
||||||
|
print(" I 600 EURO COME BIGLIETTO — scala di conti funded, e il problema della correlazione")
|
||||||
|
print("=" * 104)
|
||||||
|
print(f"\n finestra comune: {len(M)} giorni | sleeve: {names} | de-luck x{DELUCK}")
|
||||||
|
print(" correlazioni fra sleeve (le morti dei conti sono correlate quanto lo sono queste):")
|
||||||
|
C = np.corrcoef(M.T)
|
||||||
|
for i, n in enumerate(names):
|
||||||
|
print(" " + f"{n:>7} " + " ".join(f"{C[i, j]:>6.2f}" for j in range(len(names))))
|
||||||
|
for i, n in enumerate(names):
|
||||||
|
m = metrics(pd.Series(M[:, i], index=pd.date_range("2024-01-01", periods=len(M), tz="UTC")))
|
||||||
|
print(f" {n:>7}: Sharpe {m['sharpe']:>5.2f} CAGR {m['cagr']:>6.1%} maxDD {m['maxdd']:>5.1%}")
|
||||||
|
|
||||||
|
print(f"\n orizzonte {HORIZON_MONTHS} mesi | cassa iniziale EUR {START_EUR:.0f} | "
|
||||||
|
f"max {MAX_CONCURRENT} conti | rischio-morte firm {FIRM_DEATH_PER_YEAR:.0%}/anno")
|
||||||
|
print(" NB: livelli ASSOLUTI = TETTO (close-only). Il confronto fra POLITICHE e' la misura onesta.\n")
|
||||||
|
|
||||||
|
q = wick_percentiles()
|
||||||
|
print(f" wick calibrati: p50 -{q[0.50]*100:.2f}pp p90 -{q[0.90]*100:.2f}pp "
|
||||||
|
f"p99 -{q[0.99]*100:.2f}pp (target dal recon 24/07: -0.17 / -1.03 / -2.70)")
|
||||||
|
|
||||||
|
pols = policies(names)
|
||||||
|
for intraday in (False, True):
|
||||||
|
lens = "INTRADAY (wick: la lente onesta)" if intraday else "CLOSE-ONLY (tetto ottimista)"
|
||||||
|
for lev in (0.75, 1.0):
|
||||||
|
print("-" * 104)
|
||||||
|
print(f" {lens} — LEVA {lev:.2f}x")
|
||||||
|
print("-" * 104)
|
||||||
|
print(f" {'politica':>46} {'EUR/g med':>10} {'p10':>7} {'p90':>7} "
|
||||||
|
f"{'P(>=10/g)':>10} {'P(>=50/g)':>10} {'P(zero)':>8}")
|
||||||
|
for pname, slots in pols.items():
|
||||||
|
r = simulate(M, slots, n_paths=3000, lev=lev, seed=20260725, intraday=intraday)
|
||||||
|
print(f" {pname:>46} {r['med']:>10.2f} {r['p10']:>7.2f} {r['p90']:>7.2f} "
|
||||||
|
f"{r['p_ge10']:>10.1%} {r['p_target']:>10.1%} {r['p_zero']:>8.1%}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
"""Test della macchineria RENDITA / COST-FLOOR (ondata 2026-07-25).
|
||||||
|
|
||||||
|
Blocca tre cose che, se si rompono in silenzio, producono numeri credibili e sbagliati:
|
||||||
|
1. il simulatore di prelievo (survival) — validato contro casi con risposta NOTA;
|
||||||
|
2. il modello di costo IB a pavimento fisso — deve penalizzare il capitale piccolo e
|
||||||
|
deve essere salvato dalla banda (altrimenti GTAA01 sembra eseguibile quando non lo e');
|
||||||
|
3. la regressione del bug HWM nel modello funded (base di prelievo aggiornata ogni giorno
|
||||||
|
-> payout ~0): e' l'errore realmente commesso in sessione.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
def _load(name: str, rel: str):
|
||||||
|
spec = importlib.util.spec_from_file_location(name, ROOT / rel)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[name] = mod
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def cc():
|
||||||
|
return _load("r0725_capcurve", "scripts/research/r0725_capcurve.py")
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- 1) survival
|
||||||
|
def test_survival_asset_deterministico(cc):
|
||||||
|
"""Un asset che rende esattamente 10%/anno senza vol sostiene un prelievo perpetuo
|
||||||
|
appena sotto il 10% e NON sostiene il 15%."""
|
||||||
|
r = np.full(2000, 1.10 ** (1 / 365) - 1.0)
|
||||||
|
assert cc.survival(r, 1e6, 1e6 * 0.05, 20, 1.0, n_paths=100)["p10_end"] > 1e6
|
||||||
|
assert cc.survival(r, 1e6, 1e6 * 0.09, 20, 1.0, n_paths=100)["p10_end"] > 1e6
|
||||||
|
# sopra il rendimento dell'asset il capitale si erode, e molto sopra si va in rovina
|
||||||
|
assert cc.survival(r, 1e6, 1e6 * 0.11, 20, 1.0, n_paths=100)["p10_end"] < 1e6
|
||||||
|
assert cc.survival(r, 1e6, 1e6 * 0.15, 20, 1.0, n_paths=100)["p_survive"] < 0.05
|
||||||
|
|
||||||
|
|
||||||
|
def test_survival_drift_zero_non_sostiene_rendita(cc):
|
||||||
|
"""Random walk a drift ZERO: nessun prelievo perpetuo e' sostenibile (il criterio
|
||||||
|
p10 >= capitale iniziale deve fallire gia' a prelievo ~0)."""
|
||||||
|
rng = np.random.default_rng(0)
|
||||||
|
r = rng.normal(0.0, 0.15 / np.sqrt(365), 4000)
|
||||||
|
assert cc.survival(r, 1e6, 0.0, 20, 1.0, n_paths=500)["p10_end"] < 1e6
|
||||||
|
assert cc.survival(r, 1e6, 1e6 * 0.03, 20, 1.0, n_paths=500)["p_survive"] < 0.95
|
||||||
|
|
||||||
|
|
||||||
|
def test_survival_prelievo_nullo_non_fallisce_mai(cc):
|
||||||
|
"""Senza prelievi un book a rendimenti limitati non puo' andare in rovina."""
|
||||||
|
rng = np.random.default_rng(1)
|
||||||
|
r = rng.normal(0.0005, 0.01, 3000)
|
||||||
|
assert cc.survival(r, 1e6, 0.0, 20, 1.0, n_paths=300)["p_survive"] == 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_leva_aumenta_la_vol_dei_path(cc):
|
||||||
|
"""La leva k scala i ritorni: e' l'assunzione dichiarata del confronto iso-rischio."""
|
||||||
|
rng = np.random.default_rng(2)
|
||||||
|
r = rng.normal(0.0003, 0.01, 2000)
|
||||||
|
p1 = cc._boot_paths(r, 200, 365, 20, np.random.default_rng(3))
|
||||||
|
p2 = cc._boot_paths(r, 200, 365, 20, np.random.default_rng(3)) * 2.0
|
||||||
|
assert np.isclose(p2.std(), 2.0 * p1.std(), rtol=1e-9)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------ 2) pavimento IB / banda
|
||||||
|
def test_ib_pavimento_penalizza_il_capitale_piccolo(cc):
|
||||||
|
"""Il costo IB e' FISSO per ordine: a capitale piccolo il drag esplode. Se questo test
|
||||||
|
fallisce, il modello e' tornato proporzionale e GTAA01 sembrera' eseguibile a $600."""
|
||||||
|
small = cc.metrics(cc.gtaa_realistic(600.0))
|
||||||
|
big = cc.metrics(cc.gtaa_realistic(50_000.0))
|
||||||
|
assert small["cagr"] < big["cagr"], "il pavimento fisso deve penalizzare il piccolo"
|
||||||
|
assert small["cagr"] < 0.0, "a $600 il ribilanciamento continuo su IB deve essere negativo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_banda_salva_gtaa_a_capitale_piccolo(cc):
|
||||||
|
"""Con banda + cadenza settimanale, GTAA01 a $600 torna positivo. E' la condizione che
|
||||||
|
rende il 3o sleeve eseguibile a basso capitale."""
|
||||||
|
nudo = cc.metrics(cc.gtaa_realistic(600.0))
|
||||||
|
banda = cc.metrics(cc.gtaa_banded(600.0, cc.GTAA_BAND_USD, cc.GTAA_EVERY))
|
||||||
|
assert banda["sharpe"] > nudo["sharpe"] + 0.5
|
||||||
|
assert banda["cagr"] > 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_tp01_deribit_insensibile_al_capitale_sopra_soglia(cc):
|
||||||
|
"""Su Deribit la fee e' proporzionale e non c'e' pavimento: sopra ~$500 lo Sharpe
|
||||||
|
realistico coincide col modellato (conferma della lezione 'min-order = banda ottimale')."""
|
||||||
|
a = cc.metrics(cc.tp01_realistic(1_000.0))["sharpe"]
|
||||||
|
b = cc.metrics(cc.tp01_realistic(50_000.0))["sharpe"]
|
||||||
|
assert abs(a - b) < 0.02
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------- 3) regressione bug HWM funded
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def pc():
|
||||||
|
return _load("r0725_prop_config", "scripts/research/r0725_prop_config.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_funded_payout_non_e_zero_regressione_hwm(pc):
|
||||||
|
"""REGRESSIONE: aggiornare la base di prelievo ogni giorno (come un HWM mobile) azzera
|
||||||
|
per costruzione il guadagno misurato al checkpoint mensile -> payout ~0. Con un book
|
||||||
|
che rende ~20%/anno e split 80% il payout atteso su $100k e' dell'ordine di $10k, NON
|
||||||
|
di $200. Questo test e' il guardiano di quell'errore."""
|
||||||
|
r = np.full(3000, 1.20 ** (1 / 365) - 1.0) # +20%/anno deterministico, mai in perdita
|
||||||
|
f = pc.sim_funded(r, max_loss=0.10, daily_loss=0.05, k=1.0, years=1, n_paths=200)
|
||||||
|
assert f["p_alive"] == 1.0, "un asset che sale ogni giorno non puo' bustare"
|
||||||
|
assert f["e_payout"] > 8_000.0, f"payout implausibilmente basso: {f['e_payout']:.0f}"
|
||||||
|
assert f["e_payout"] < 20_000.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_funded_bust_su_max_loss_statico(pc):
|
||||||
|
"""Il max-loss e' STATICO dal saldo iniziale: un book che perde costantemente deve
|
||||||
|
bustare, e il payout deve essere zero."""
|
||||||
|
r = np.full(3000, -0.002)
|
||||||
|
f = pc.sim_funded(r, max_loss=0.06, daily_loss=0.05, k=1.0, years=1, n_paths=200)
|
||||||
|
assert f["p_alive"] == 0.0
|
||||||
|
assert f["e_payout"] == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_eval_target_irraggiungibile_non_passa(pc):
|
||||||
|
"""Sanity dell'eval: un book piatto non raggiunge mai il target di profitto."""
|
||||||
|
r = np.zeros(3000)
|
||||||
|
e = pc.sim_eval(r, target=0.10, max_dd=0.06, daily_loss=0.04, k=1.0, n_paths=200)
|
||||||
|
assert e["p_pass"] == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------ 4) scala di conti funded (ladder)
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def pl():
|
||||||
|
return _load("r0725_prop_ladder", "scripts/research/r0725_prop_ladder.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_wick_calibrato_sui_percentili_del_recon(pl):
|
||||||
|
"""Il gap intraday deve riprodurre l'ordine di grandezza misurato dal recon MTM 1h del
|
||||||
|
24/07 (p50 ~-0.17pp, p90 ~-1pp, p99 ~-3pp). Se la calibrazione scivola, i breach diventano
|
||||||
|
finzione in una direzione o nell'altra."""
|
||||||
|
q = pl.wick_percentiles()
|
||||||
|
assert 0.0010 < q[0.50] < 0.0025
|
||||||
|
assert 0.005 < q[0.90] < 0.015
|
||||||
|
assert 0.015 < q[0.99] < 0.050
|
||||||
|
|
||||||
|
|
||||||
|
def test_lente_intraday_e_sempre_piu_severa_del_close_only(pl):
|
||||||
|
"""I wick possono solo AGGIUNGERE breach: la lente intraday non puo' produrre piu' reddito
|
||||||
|
di quella close-only. Se questo si inverte, il gap sta entrando col segno sbagliato."""
|
||||||
|
M, names = pl.sleeve_panel()
|
||||||
|
slots = list(pl.policies(names).values())[1]
|
||||||
|
close = pl.simulate(M, slots, n_paths=400, lev=1.0, seed=1, intraday=False)
|
||||||
|
intra = pl.simulate(M, slots, n_paths=400, lev=1.0, seed=1, intraday=True)
|
||||||
|
assert intra["p_target"] <= close["p_target"]
|
||||||
|
assert intra["p_zero"] >= close["p_zero"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_conti_sullo_stesso_book_sono_perfettamente_correlati(pl):
|
||||||
|
"""Il punto centrale dello studio: N conti che girano lo STESSO book non diversificano.
|
||||||
|
Verifica strutturale sui pesi, non sull'output del MC."""
|
||||||
|
M, names = pl.sleeve_panel()
|
||||||
|
pols = pl.policies(names)
|
||||||
|
conc = pols["CONC-DIV ogni conto = book diversificato"]
|
||||||
|
assert all(np.allclose(s, conc[0]) for s in conc), "CONC deve avere slot identici"
|
||||||
|
sparso = pols["SPARSO 1 sleeve per conto (a rotazione)"]
|
||||||
|
assert not np.allclose(sparso[0], sparso[1]), "SPARSO deve avere slot diversi"
|
||||||
|
|
||||||
|
|
||||||
|
def test_senza_cassa_nessun_conto_e_nessun_reddito(pl):
|
||||||
|
"""Controllo di sanita' del meccanismo cassa->biglietto: a cassa zero non si compra nulla."""
|
||||||
|
M, names = pl.sleeve_panel()
|
||||||
|
slots = list(pl.policies(names).values())[0]
|
||||||
|
old = pl.START_EUR
|
||||||
|
try:
|
||||||
|
pl.START_EUR = 0.0
|
||||||
|
r = pl.simulate(M, slots, n_paths=200, lev=1.0, seed=2)
|
||||||
|
assert r["p_zero"] == 1.0
|
||||||
|
finally:
|
||||||
|
pl.START_EUR = old
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------- 5) regola di CONSISTENCY HyroTrader
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def hy():
|
||||||
|
return _load("r0725_hyro", "scripts/research/r0725_hyro.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_consistency_MORDE_su_un_book_grumoso(hy):
|
||||||
|
"""PROVA CHE IL CONTROLLO FUNZIONA. Un UNICO giorno da +12% porta da solo tutto il profitto
|
||||||
|
e non arriva mai altro: con la regola 40% quel path non deve passare MAI (0.12 > 0.40*0.12),
|
||||||
|
senza regola passa subito. Se questo test non discrimina, il 'costo ~0pp' misurato sul book
|
||||||
|
vero sarebbe un bug, non un risultato.
|
||||||
|
|
||||||
|
NB sul probe: sim_eval fa BLOCK BOOTSTRAP (blocchi di 20g), non replay -> uno strappo isolato
|
||||||
|
finirebbe solo in alcuni path. Qui gli strappi sono spaziati di 20 giorni = esattamente uno per
|
||||||
|
blocco, cosi' OGNI path ne vede uno ogni 20 giorni in modo deterministico. In 40 giorni:
|
||||||
|
senza regola si passa al 1o strappo (+12% >= target 10%); con la regola servono 3 strappi
|
||||||
|
perche' il giorno migliore scenda sotto il 40% del profitto cumulato -> oltre la finestra.
|
||||||
|
"""
|
||||||
|
r = np.zeros(200)
|
||||||
|
r[::20] = 0.12
|
||||||
|
senza = hy.sim_eval(r, 1.0, consistency=False, intraday=False, max_days=40, n_paths=200)
|
||||||
|
con = hy.sim_eval(r, 1.0, consistency=True, intraday=False, max_days=40, n_paths=200)
|
||||||
|
assert senza["p_pass"] > 0.9, "senza regola un +12% secco deve passare il target 10%"
|
||||||
|
assert con["p_pass"] < 0.05, "la regola 40% deve bloccare un profitto concentrato in 1-2 giorni"
|
||||||
|
|
||||||
|
|
||||||
|
def test_consistency_non_morde_su_un_book_liscio(hy):
|
||||||
|
"""Controprova: un book che accumula il target in tanti giorni piccoli non e' toccato
|
||||||
|
dalla regola. E' il motivo per cui sul book vero il costo misurato e' ~0."""
|
||||||
|
r = np.full(400, 0.0008) # ~+0.08%/g: il 10% arriva in ~120 giorni
|
||||||
|
senza = hy.sim_eval(r, 1.0, consistency=False, intraday=False, n_paths=200)
|
||||||
|
con = hy.sim_eval(r, 1.0, consistency=True, intraday=False, n_paths=200)
|
||||||
|
assert abs(con["p_pass"] - senza["p_pass"]) < 0.02
|
||||||
|
|
||||||
|
|
||||||
|
def test_dd_statico_6pct_e_binding(hy):
|
||||||
|
"""Il maxDD 6% STATICO di HYRO e' il vincolo vero: un book con DD storico sopra il 6%
|
||||||
|
non puo' girare a leva piena. Sanity: alzando la leva la sopravvivenza funded scende."""
|
||||||
|
cfg = hy.configs()
|
||||||
|
r = list(cfg.values())[0].values.astype(float)
|
||||||
|
lo = hy.sim_funded(r, 0.5, False, 100_000.0, n_paths=400)["p_alive"]
|
||||||
|
hi = hy.sim_funded(r, 1.0, False, 100_000.0, n_paths=400)["p_alive"]
|
||||||
|
assert lo > hi
|
||||||
Reference in New Issue
Block a user