research(wave-0703): migliora+proteggi VRP01 — 7 filoni, 0 miglioramenti, anchor-audit VRP01 chiuso (4/4 sleeve)
Goal: migliorare la strategia short-vol (famiglia Albimarini/VRP01) e proteggerla dai DD. Workflow 26 agenti (7 filoni + 2 lenti avversariali + scettico incrociato). Esito: NON migliorabile; la protezione DD si compra SOLO con la size. - Griglia 288 strutture: nessuna batte VRP01 (DSR 0.000; meta' griglia = 3a occorrenza "0-perdite/Sharpe implausibile" dopo CC01/ALB-A). - 4 overlay DD (exit-spike/SL-MTM/ala-coda/cooldown): 4/4 REFUTED dal null de-levering — la protezione crash vive gia' nel gate IV-rank. - Gate nuovi: 4o fallimento su 4 (l'alpha e' il binario IV-rank>0.30). - Sizing: 12% deploy ~ 0.27 Kelly onesto (anti-rovina); disambiguazione unita' obbligatoria vs 12% peso book (0.014 Kelly, fattore 19x). - Gate term-structure VIX/VXV su SPX (dSh +0.90, DSR 0.992) = confound di modello al 100% -> nuova regola: riprezzare term-structure-consistent prima di credere a un gate vol su strutture BS-flat. - ANCHOR-AUDIT VRP01 CHIUSO: primo sleeve SENZA luck (fase canonica = peggiore delle 7 -> numeri di ammissione conservativi). Audit anchor ora completo su 4/4 sleeve ancorati. Book/pesi INVARIATI. Nessun nuovo sleeve. 168 test verdi. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -115,6 +115,22 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
crash incluso). Scorrelato a TP01 (~+0.01-0.07). **CAVEAT:** premio MODELLATO su DVOL ATM (skew non
|
crash incluso). Scorrelato a TP01 (~+0.01-0.07). **CAVEAT:** premio MODELLATO su DVOL ATM (skew non
|
||||||
esplicito), book a 1d, f di stress reale non catturato → LEAD robusto, non deploy pieno. Ricerca
|
esplicito), book a 1d, f di stress reale non catturato → LEAD robusto, non deploy pieno. Ricerca
|
||||||
`scripts/research/options_vrp_v2.py` (vs baseline `options_vrp_lab.py`). Test `tests/test_vrp_sleeve.py`.
|
`scripts/research/options_vrp_v2.py` (vs baseline `options_vrp_lab.py`). Test `tests/test_vrp_sleeve.py`.
|
||||||
|
⚠️ **ANCHOR-AUDIT CHIUSO + ondata "migliora e proteggi" (2026-07-03, 7 filoni + 2 lenti + scettico):
|
||||||
|
VRP01 NON è migliorabile e la protezione DD si compra SOLO con la size.** (a) **Anchor-luck (ciclo
|
||||||
|
settimanale, 7 fasi): PRIMO sleeve SENZA firma di luck** — la fase canonica è la PEGGIORE delle 7 su
|
||||||
|
FULL (1.09 = 7° pctl) e su DD (11.8% = 93° pctl), mediana su HOLD (0.59); spike bootstrap NEGATIVO →
|
||||||
|
i numeri di ammissione FULL 1.10/HOLD 0.60/DD 12% sono CONSERVATIVI, non gonfiati. Da ora si citano
|
||||||
|
con banda: ShFULL [1.09,1.83], ShHOLD [−0.03,1.11], DD [5.7,11.8%]; edge OOS f-dipendente (f=0.8 →
|
||||||
|
HOLD~0). **Con questo l'audit anchor è completo su 4/4 sleeve ancorati.** (b) Griglia 288 strutture:
|
||||||
|
nessuna batte VRP01 (DSR 0.000; metà griglia = 3ª occorrenza "0-perdite/Sharpe implausibile" dopo
|
||||||
|
CC01/ALB-A → gate `implausible_sharpe` alzato di priorità). (c) 4 overlay DD (exit-spike/SL-MTM/
|
||||||
|
ala-coda/cooldown): 4/4 REFUTED dal null de-levering — la protezione crash vive già nel gate
|
||||||
|
d'ingresso IV-rank. (d) Gate nuovi: 4° fallimento su 4 (l'alpha è il binario IV-rank>0.30). (e)
|
||||||
|
Sizing: 12% deploy ≈ 0.27 Kelly onesto (anti-rovina); ⚠️ NON confondere col 12% di PESO del book
|
||||||
|
(~0.014 Kelly, fattore 19x). (f) Gate term-structure VIX/VXV su SPX (ΔSh +0.90, DSR 0.992) =
|
||||||
|
**confound di modello al 100%** (la var del gate coincide con l'errore BS-flat vs term-structure) →
|
||||||
|
nuova regola: riprezzare term-structure-consistent prima di credere a un gate vol su strutture
|
||||||
|
BS-flat. Book/pesi INVARIATI. Diario `2026-07-03-vrp-improve-dd.md`; script `scripts/research/r0703_vrpimp_*.py` (7 file).
|
||||||
Diario `2026-06-20-financeold-analysis-vrp-v2.md`.
|
Diario `2026-06-20-financeold-analysis-vrp-v2.md`.
|
||||||
- **Universo Hyperliquid: ESPANDERLO NON aiuta XS01** (provato): 52-asset / top-liquidità dinamico /
|
- **Universo Hyperliquid: ESPANDERLO NON aiuta XS01** (provato): 52-asset / top-liquidità dinamico /
|
||||||
trend-multi-asset → tutti peggiori (small-cap/memecoin diluiscono il momentum relativo; il trend
|
trend-multi-asset → tutti peggiori (small-cap/memecoin diluiscono il momentum relativo; il trend
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# 2026-07-03 — Ondata "migliora e proteggi VRP01 dai DD" (7 filoni + verifica a 2 lenti + scettico incrociato)
|
||||||
|
|
||||||
|
**Contesto.** Dopo la chiusura del dossier Albimarini (3 backtest indipendenti convergenti: premio
|
||||||
|
piccolo e reale, code non campionate, numeri del corso spiegabili al 100% dalla struttura del
|
||||||
|
payoff), goal esplicito: **migliorare la strategia short-vol** — la famiglia Albimarini → il nostro
|
||||||
|
**VRP01**, l'unica forma sopravvissuta agli audit — **e proteggerla dai drawdown**, con
|
||||||
|
orchestrazione a decine di agenti. Workflow deterministico: 7 filoni di ricerca in parallelo, 2
|
||||||
|
refuter avversariali (lente selection-bias + lente de-levering) su ogni miglioramento proposto,
|
||||||
|
scettico incrociato finale sui sopravvissuti. **26 agenti, 0 errori.**
|
||||||
|
|
||||||
|
**Scripts:** `scripts/research/r0703_vrpimp_{structgrid,ddoverlay,sizing,newgates,anchor,spxgate,stresslab}.py`.
|
||||||
|
Nessun file di produzione toccato, nessun commit dagli agenti. Motore riusato: `r0702_alb_structure.py`
|
||||||
|
(DVOL, riproduce VRP01 bit-exact: baseline bridge max|diff|=0.00e+00 su 266 settimane).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verdetto in una riga
|
||||||
|
|
||||||
|
**VRP01 non è migliorabile oltre lo status quo, e la protezione DD si compra SOLO con la size.**
|
||||||
|
Zero nuovi sleeve, zero overlay che reggono, zero gate nuovi, zero cambio di sizing. I due unici
|
||||||
|
"sopravvissuti" alla verifica **non propongono alcun cambiamento** (una conferma dello status quo +
|
||||||
|
una lente di misura). Il candidato più luccicante dell'ondata — un gate term-structure VIX su SPX
|
||||||
|
con ΔSharpe +0.90 e DSR 0.992 — è stato smontato dallo scettico come **confound di modello al
|
||||||
|
100%**. Deliverable reale = **audit anchor-luck di VRP01 CHIUSO** (ultimo sleeve ancorato): la fase
|
||||||
|
canonica è all'estremo **conservativo** della banda, quindi i numeri di ammissione reggono e da ora
|
||||||
|
si citano con banda.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## I 7 filoni
|
||||||
|
|
||||||
|
### 1. Griglia di struttura (288 celle) — NESSUNA batte VRP01
|
||||||
|
6 strutture (vertical put/call, condor, 3 diagonali) × distanza {1,1.5,2,2.5}σ × ali {0.5,1,2}σ ×
|
||||||
|
tenor {3,5,7,10}g, gate IV-rank canonico sempre attivo. La cella scelta col **solo in-sample**
|
||||||
|
(DIAG-PUT z=1.0 3g, ShIS 2.28) crolla in hold-out a **+0.19** vs 0.86 del bridge stesso-motore;
|
||||||
|
DSR 0.000/0.004 su 288 trial; multi-cut negativo a 3-4 cut su 4; banda d'ancora che non tiene
|
||||||
|
nemmeno il segno (ShH [−0.21,+0.64]). **Metà griglia (150/288) è il pattern "0-perdite = Sharpe
|
||||||
|
implausibile"** (celle z≥2 lunghe, 0 perdite su ~97 trade, ShH fasulli 4-6.5) — **terza occorrenza
|
||||||
|
dopo CC01 e ALB-A**. Quarta conferma indipendente: nel VRP l'alpha sta nel gate di regime + nella
|
||||||
|
zona ~δ-0.28 dove la coda è campionata, non nella geometria.
|
||||||
|
|
||||||
|
### 2. Overlay di protezione DD (14 celle) — 4/4 REFUTED (2 lenti ciascuno)
|
||||||
|
(a) exit intra-settimana su spike DVOL, (b) stop-loss MTM, (c) ala di coda comprata (broken-wing),
|
||||||
|
(d) cooldown post-perdita. **Nessuno batte il null del de-levering** a pari maxDD su tutta la banda
|
||||||
|
f; nessuno passa multi-cut (max 1/4) né DSR (0.70-0.88, tutti <0.95). Esempio decisivo: SL MTM 3x fa
|
||||||
|
DD 7.9%/worst −5.7%, ma VRP01 × 0.66 (semplice riduzione di size) fa lo **stesso DD a Sharpe
|
||||||
|
invariato e worst-week migliore (−4.9%)**. Meccanismo confermato: i trigger di spike scattano solo
|
||||||
|
2-3 volte in 5 anni **perché il gate IV-rank/crash-skip evita già gli ingressi vicino alle
|
||||||
|
esplosioni di vol** — la protezione crash vive nel gate d'ingresso, non serve un overlay a valle.
|
||||||
|
Coerente con TP01×DVOL (2026-06-26): "per meno DD la leva è la size, non un overlay".
|
||||||
|
|
||||||
|
### 3. Sizing anti-rovina — CONFERMA (12% ≈ quarter-Kelly), 1 candidato REFUTED
|
||||||
|
Il sizing non crea alpha (Sharpe del flusso invariante ~0.95 a ogni frazione). Frontiera onesta
|
||||||
|
CAGR-DD-P(rovina) mappata su 92 trade gated ETH 2021-26 (pooled banda-f + coda sintetica full-loss):
|
||||||
|
Kelly onesto q*=44% → il **12% del book = 0.27 Kelly, già in zona quarter-Kelly anti-rovina**
|
||||||
|
(P(rovina-50%|5y) 0.0-0.1%, P(DD>30%) 9-18%, CAGR med 13.5-17% a 2k/5k). Salire al 25% → P(DD>30%)
|
||||||
|
> 92%; Albimarini 1→4 → P(rovina) 53-55%. **Nessun cambio raccomandato.** Il candidato anti-streak
|
||||||
|
(N=2, size 12%→3% dopo 2 vittorie, l'opposto di Albimarini) era il più luccicante mai visto sullo
|
||||||
|
sleeve (FULL CAGR +19.4%/DD 6.3%/Sh 2.08, random-null 0.996, DSR 0.999) ma **auto-refutato come
|
||||||
|
artefatto di selezione**: plateau assente (solo N=2 vince, N=3/5 collassano), P(loss|streak) non
|
||||||
|
monotona, delever-null perde in 2/7 ancore — tutto l'effetto sono ~6 perdite su 15 trade a
|
||||||
|
streak==2 (binomtest p=0.033, CI contiene la base). Istruttivo: mostra come un DSR 0.999 possa
|
||||||
|
convivere con l'assenza di meccanismo.
|
||||||
|
|
||||||
|
### 4. Gate nuovi — 4° FALLIMENTO su 4 (barra alta)
|
||||||
|
(a) term-structure DVOL **non testabile oggi** (storia vol_term = 7 giorni, logger forward dal
|
||||||
|
2026-06-26 → rivalutare a 6-12 mesi); (b) vol-of-vol, (c) RV-acceleration, (d) combinazioni-AND. Il
|
||||||
|
candidato IS-best (vol-of-vol pctl>0.80) passa DSR 0.960 e il de-levering all'ancora di default ma
|
||||||
|
la **banda d'ancora lo smaschera** (mediana uplift FULL −0.07 su 7 ancore, uplift concentrato in UNA
|
||||||
|
finestra 2021-22 di 5 settimane, hold-out uplift 0.00). Bonus strutturale: i veti RV-acceleration
|
||||||
|
bloccano settimane **profittevoli** (+0.93/+1.25% vs +0.6% tenute) — dopo il gate IV-rank, la vol
|
||||||
|
che accelera è dove il premio venduto è più ricco. Conferma il verdetto 2026-07-01: l'alpha è tutto
|
||||||
|
nel binario IV-rank>0.30.
|
||||||
|
|
||||||
|
### 5. Audit anchor-luck VRP01 — CHIUSO: primo sleeve SENZA firma di luck
|
||||||
|
VRP01 è l'ultimo sleeve ancorato dopo TP01/XS01/SKH01. Ciclo settimanale → 7 ancore (giorno di
|
||||||
|
apertura). Replica bit-exact della sleeve (max|diff|=0.0). Le 7 fasi con parametri identici:
|
||||||
|
|
||||||
|
| metrica | canonica (fase-0) | mediana 7 fasi | banda | pctl canonica |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Sh FULL | 1.09 | 1.32 | [1.09, 1.83] | **7° (peggiore)** |
|
||||||
|
| Sh HOLD | 0.59 | 0.59 | [−0.03, 1.11] | 50° (mediana) |
|
||||||
|
| maxDD | 11.8% | 7.2% | [5.7, 11.8%] | **93° (peggiore)** |
|
||||||
|
|
||||||
|
**Firma OPPOSTA a TP01/XS01/SKH01**: lo spike bootstrap è NEGATIVO (g0 HOLD −0.06, FULL −0.21,
|
||||||
|
P(g0≤0) 0.54/0.74) → niente da de-luckare. La fase canonica ha beccato le settimane crash 2022
|
||||||
|
**peggio** di ogni altra (unico anno-fase negativo, −5.8%). **I numeri di ammissione FULL 1.10 /
|
||||||
|
HOLD 0.60 / DD 12% sono quindi CONSERVATIVI, non gonfiati** — il timore di "protezione fittizia" è
|
||||||
|
refutato: il DD 12% è il caso peggiore della banda, non quello fortunato. Impatto della fase sul
|
||||||
|
book 5-sleeve ≤ 0.04 Sh (banda ShFULL 2.23-2.27, ShHOLD 2.35-2.48, DD 6.2-6.3%). **Con questo
|
||||||
|
l'audit anchor-luck è completo su tutti gli sleeve ancorati (4/4).**
|
||||||
|
|
||||||
|
### 6. Gate di regime sulla diagonale SPX (l'esperimento mancante) — REFUTED, con confound istruttivo
|
||||||
|
L'esperimento che mancava ai 3 backtest Albimarini: il gate di regime sulla diagonale SPX (VIX +
|
||||||
|
VXV da FRED, motore BS sintetico con skew log-lineare). **Domanda secca: il gate IV-rank ribalta gli
|
||||||
|
anni di coda su SPX come su crypto? NO.** Il gate VIX-rank canonico *abbassa* lo Sharpe (1.36→1.19),
|
||||||
|
non ribalta nessun anno di coda, peggiora il 2018, muore sul de-levering (ΔSh −0.17). Meccanismo:
|
||||||
|
su SPX il VRP è già positivo l'**84% dei giorni** (vs ~58% delle settimane crypto) → il filtro di
|
||||||
|
rank non ha lavoro discriminante; su equity IV-rank alto = crisi, non vol ricca.
|
||||||
|
Il candidato **term-structure VIX/VXV<1 (contango)** sembrava reale — Sh 1.18→2.08, DD 3.5→1.3%,
|
||||||
|
ΔSh **+0.90** a pari DD, multicut 3/3, flippa il 2020, DSR 0.992, plateau su c∈[0.93,1.05], jackknife
|
||||||
|
robusto — **e passa la lente selection-bias pulito**. Ma la lente de-levering lo affonda:
|
||||||
|
**l'intero uplift è un confound di modello, quantificato al 100%.** La variabile del gate (VIX/VXV>1)
|
||||||
|
coincide 1:1 con la variabile dell'errore di pricing (tenor 6g prezzato a VIX-30g). Il refuter ha
|
||||||
|
riprezzato con σ(6g)=VIX·(VIX/VXV)^α term-structure-consistent (α=1.42 = estrapolazione log-lineare
|
||||||
|
zero-parametri della pendenza osservata 30g→93g) → l'edge sparisce. **Lezione: un gate che si
|
||||||
|
correla con un errore di modello è indistinguibile da alpha finché non correggi il modello** — vale
|
||||||
|
per ogni futuro test su strutture prezzate BS-flat.
|
||||||
|
|
||||||
|
### 7. Stress lab di coda — fisica dello strumento, nessun cambio
|
||||||
|
La protezione di coda del VRP01 viene per ~metà dall'**ala far-OTM** (nel replay COVID taglia ~90%
|
||||||
|
della perdita naked) e ~metà dal **gate canonico** (sulle 10 peggiori finestre 14g taglia la perdita
|
||||||
|
cumulata del 44-71% saltando i re-entry a IV-rank>0.90). L'ala T+1 "Albimarini" è assicurazione solo
|
||||||
|
contro i gap **medi** (−15/−20%, dove atterra ATM con un giorno di time value: +150bps) e **NON**
|
||||||
|
contro i gap profondi (k(−30%)=0.98 → il de-levering la batte), con valore interamente dipendente
|
||||||
|
dal vol-spike (banda f/mult da −324 a +263bps). Worst-case onesto a sizing 12%: piccolo in
|
||||||
|
convenzione book (−€12/sett a 2k, −€31 a 5k alla cella −30%) ma **è il margine intero** se il 12%
|
||||||
|
fosse deployato fisicamente come margine (−€164..−227).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scettico incrociato — verdetto e disambiguazione critica
|
||||||
|
|
||||||
|
I 2 sopravvissuti (sizing-conferma, anchor-ensemble) usano leve diverse (posizione sulla frontiera
|
||||||
|
vs varianza della stima), ma lo scettico ha trovato **tre sovrapposizioni che i refuter singoli non
|
||||||
|
potevano vedere**:
|
||||||
|
|
||||||
|
1. **Stessa dipendenza dal modello f** — entrambi flippano segno allo stesso punto (f=0.6-0.8 →
|
||||||
|
negativo). Due sopravvissuti = **un solo grado di libertà di rischio-modello** (il caveat "premio
|
||||||
|
MODELLATO su DVOL ATM" resta il collo di bottiglia dell'intera famiglia).
|
||||||
|
2. **⚠️ CONFLITTO DI UNITÀ sul "12%"** (il punto più pericoloso per la narrativa): il **12% di PESO
|
||||||
|
del book** (convenzione pnl/Ks di `sleeves.py`) = ~**0.6% margine/equity ≈ 0.014 Kelly**, mentre
|
||||||
|
il "FISSO 12%" del filone sizing = **12% margine/equity ≈ 0.27 Kelly** — **fattore 19x**. Entrambe
|
||||||
|
corrette internamente, ma non vanno confuse: worst-week −12.2% del conto (convenzione margine) vs
|
||||||
|
−0.64% (convenzione peso book).
|
||||||
|
3. **L'ensemble-7-fasi è ridondante con il tranching TP01** (matematica di diversificazione: corr fra
|
||||||
|
fasi ρ=0.355, boost teorico ×1.50 vs osservato ×1.53, residuo alpha ≈ 0) → **non promuovere a LEAD
|
||||||
|
autonomo**; la coda strutturale è invariata (worst = 7 tranche a full-loss simultaneo), il DD 4.7%
|
||||||
|
è campionario, non un cap.
|
||||||
|
|
||||||
|
**Effetto congiunto sul book 5-sleeve: ZERO** (entrambi propongono nessun cambio; controfattuale
|
||||||
|
|ΔSh| ≤ 0.04). Il book live Deribit (TP01+SKH01) non contiene VRP01 → impatto operativo nullo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cosa entra nella narrativa (una sola voce, status = misura research)
|
||||||
|
|
||||||
|
- **Audit anchor-luck VRP01 CHIUSO** → i numeri di ammissione reggono e si citano con banda
|
||||||
|
(ShFULL [1.09,1.83], ShHOLD [−0.03,1.11], DD [5.7,11.8%]); l'edge OOS resta f-dipendente.
|
||||||
|
Con questo l'audit anchor è completo su 4/4 sleeve ancorati.
|
||||||
|
- **Sizing (conferma, con disambiguazione unità obbligatoria):** 12% deploy ≈ 0.27 Kelly onesto,
|
||||||
|
anti-rovina; NON confondere col 12% di peso book (0.014 Kelly, 19x).
|
||||||
|
- **VRP01 canonico resta lo sleeve giusto, INVARIATO** — non migliorabile per struttura, gate,
|
||||||
|
overlay o sizing; la protezione DD si compra con la size.
|
||||||
|
|
||||||
|
## Regole/candidati rafforzati
|
||||||
|
|
||||||
|
- **`implausible_sharpe` gate in altlib**: 3ª occorrenza del pattern "0-perdite = Sharpe
|
||||||
|
implausibile" (griglia struttura, dopo CC01 e ALB-A) → priorità alzata.
|
||||||
|
- **Nuova regola metodologica**: un gate che si correla con un errore di modello (term-structure vs
|
||||||
|
BS-flat) è indistinguibile da alpha finché non si corregge il modello — riprezzare
|
||||||
|
term-structure-consistent prima di credere a qualsiasi gate vol su strutture BS-flat.
|
||||||
|
- **Conferma (4ª): l'alpha del VRP è il gate IV-rank binario**, non struttura/sizing/overlay/gate
|
||||||
|
aggiuntivi.
|
||||||
|
|
||||||
|
**Stato:** book live INVARIATO, nessun nuovo sleeve, nessun forward-monitor. Test suite verde. 7
|
||||||
|
script committati.
|
||||||
@@ -0,0 +1,484 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""r0703_vrpimp_anchor.py — FILONE 5: AUDIT ANCHOR-LUCK di VRP01 (le 7 fasi settimanali).
|
||||||
|
|
||||||
|
VRP01 apre un put credit spread ogni 7 GIORNI-INDICE a partire da i=60 del join px/DVOL
|
||||||
|
(gap-free dal 2021-03-24) e lo tiene a scadenza. L'ancora e' quindi il GIORNO DELLA SETTIMANA
|
||||||
|
di apertura: 7 fasi a priori (i0 = 60+phase, phase in 0..6). E' l'unico sleeve ancorato non
|
||||||
|
ancora auditato dopo TP01 (24 ancore, P=0.86), XS01 (10 fasi) e SKH01 (23 offset).
|
||||||
|
|
||||||
|
Domande (ricerca di PROTEZIONE, non di alpha):
|
||||||
|
0. REPLICA bit-exact della sleeve canonica (_vrp_combo_returns) prima di tutto.
|
||||||
|
1. Banda delle 7 fasi a parametri IDENTICI (gate IV-rank/VRP ricalcolati causali per fase,
|
||||||
|
automatico: i gate si valutano all'indice d'ingresso della fase): Sh FULL/HOLD, maxDD,
|
||||||
|
worst-week, per-anno. Percentile della fase canonica.
|
||||||
|
2. Banda f skew-caveat {0.6, 0.8, 1.0, 1.3} (pricing BS flat su DVOL ATM).
|
||||||
|
3. ENSEMBLE delle 7 fasi (1/7 ciascuna) + null del DE-LEVERING (lezione TP01xDVOL:
|
||||||
|
una riduzione DD replicabile scalando la size non vale nulla).
|
||||||
|
4. Block-bootstrap P(spike) alla maniera dello scettico TP01 (r0702_skeptic_offset.py):
|
||||||
|
lo spike della fase canonica e' speciale o e' il massimo atteso di 7 stime correlate?
|
||||||
|
5. Impatto sul book 5-sleeve (TP 33/XS 15/VRP 12/SKH 20/GTAA 20, combine_outer) alla fase
|
||||||
|
canonica / mediana / peggiore / ensemble.
|
||||||
|
6. Ri-verdetto dei numeri di ammissione (FULL 1.10 / HOLD 0.60 / DD 12%) alla fase mediana.
|
||||||
|
|
||||||
|
Macchineria RIUSATA, non riscritta: helper del pricing e config presi in sola lettura da
|
||||||
|
src/portfolio/sleeves (_bs_put, _strike_from_delta, VRP_CFG) — il motore replica quindi la
|
||||||
|
sleeve canonica per costruzione, e la replica e' VERIFICATA bit-exact in PART 0. (Il motore
|
||||||
|
r0702_alb_structure usa load_tf di research_lab: qui serve il path della sleeve, resample_1d
|
||||||
|
su load_data, per la bit-exactness.) Nessun file di produzione toccato. Nessuna selezione
|
||||||
|
sull'hold-out (nessuna grid: 7 fasi = famiglia a priori, parametri congelati).
|
||||||
|
|
||||||
|
Regole standing rispettate: niente short-vol da modello in deploy (esito = conoscenza);
|
||||||
|
niente '7D' con origin (qui il ciclo e' index-based come il canonico, niente resample
|
||||||
|
settimanale); pandas 2.x niente .view('int64').
|
||||||
|
|
||||||
|
uv run python scripts/research/r0703_vrpimp_anchor.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("/opt/docker/PythagorasGoal")
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
import altlib as al # noqa: E402 (harness di progetto: _sh/_dd_ret/HOLDOUT)
|
||||||
|
from src.data.downloader import load_data # noqa: E402
|
||||||
|
from src.strategies.trend_portfolio import resample_1d # noqa: E402
|
||||||
|
from src.portfolio.sleeves import ( # noqa: E402 (SOLA LETTURA)
|
||||||
|
VRP_CFG, _bs_put, _strike_from_delta, _vrp_weekly_asset, _vrp_combo_returns, _HL_DIR)
|
||||||
|
from src.portfolio.portfolio import combine_outer, to_daily # noqa: E402
|
||||||
|
|
||||||
|
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
|
||||||
|
WKY = 365.25 / 7.0
|
||||||
|
F_BAND = (0.6, 0.8, 1.0, 1.3)
|
||||||
|
PHASES = tuple(range(7))
|
||||||
|
ASSETS = ("BTC", "ETH")
|
||||||
|
B_BOOT = 4000
|
||||||
|
DAYNAME = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||||
|
ADMISSION = dict(full=1.10, hold=0.60, dd=0.12) # numeri dichiarati in CLAUDE.md
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# motore per-fase (specchio 1:1 di sleeves._vrp_weekly_asset, param i0 e f)
|
||||||
|
# ===========================================================================
|
||||||
|
@lru_cache(maxsize=4)
|
||||||
|
def joined(asset: str) -> pd.DataFrame:
|
||||||
|
"""Stesso join px/DVOL della sleeve canonica (resample_1d su load_data 1h)."""
|
||||||
|
df = resample_1d(load_data(asset, "1h"))
|
||||||
|
s = pd.Series(df["close"].values.astype(float), index=pd.to_datetime(df["datetime"]))
|
||||||
|
if s.index.tz is None:
|
||||||
|
s.index = s.index.tz_localize("UTC")
|
||||||
|
dv = pd.read_parquet(_HL_DIR / f"dvol_{asset.lower()}.parquet")
|
||||||
|
d = pd.Series(dv["close"].values.astype(float),
|
||||||
|
index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True))
|
||||||
|
return pd.concat({"px": s, "dvol": d}, axis=1, join="inner").sort_index().dropna()
|
||||||
|
|
||||||
|
|
||||||
|
def vrp_weekly_phase(asset: str, phase: int = 0, f: float = 1.0) -> pd.Series:
|
||||||
|
"""Replica ESATTA di _vrp_weekly_asset con i0 = 60+phase e fattore premio f.
|
||||||
|
I gate (VRP>0, IV-rank espandente, crash-skip) sono ricalcolati CAUSALI all'indice
|
||||||
|
d'ingresso di ciascuna fase (dvf[:i]) — nessun riuso della sequenza gate di fase 0."""
|
||||||
|
J = joined(asset)
|
||||||
|
px = J["px"].values; dvf = J["dvol"].values / 100.0; idx = J.index
|
||||||
|
n = len(px); cfg = VRP_CFG; tn = cfg["tenor_d"]; T = tn / 365.25
|
||||||
|
rets = {}
|
||||||
|
i = 60 + phase
|
||||||
|
while i + tn < n:
|
||||||
|
S0 = px[i]; sig = dvf[i]
|
||||||
|
skip = False
|
||||||
|
if cfg["gate_vrp"] and i >= 31: # VRP>0 causale
|
||||||
|
rv = np.std(np.diff(np.log(px[i - 30:i + 1]))) * np.sqrt(365.25)
|
||||||
|
if (sig - rv) <= 0:
|
||||||
|
skip = True
|
||||||
|
if not skip and (cfg["gate_ivr"] > 0 or cfg["crash_skip"] < 1.0) and i >= 60:
|
||||||
|
ivr = float((dvf[:i] < dvf[i]).mean()) # IV-rank espandente
|
||||||
|
if cfg["gate_ivr"] > 0 and ivr < cfg["gate_ivr"]:
|
||||||
|
skip = True
|
||||||
|
if cfg["crash_skip"] < 1.0 and ivr > cfg["crash_skip"]:
|
||||||
|
skip = True
|
||||||
|
if skip:
|
||||||
|
rets[idx[i + tn]] = 0.0; i += tn; continue
|
||||||
|
Ks = _strike_from_delta(S0, T, sig, cfg["short_delta"])
|
||||||
|
Kl = _strike_from_delta(S0, T, sig, cfg["long_delta"])
|
||||||
|
net_prem = (_bs_put(S0, Ks, T, sig) - _bs_put(S0, Kl, T, sig)) * f
|
||||||
|
S1 = px[i + tn]
|
||||||
|
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1)
|
||||||
|
pnl = net_prem - payoff - cfg["fee_frac"] * abs(net_prem)
|
||||||
|
rets[idx[i + tn]] = pnl / Ks
|
||||||
|
i += tn
|
||||||
|
return pd.Series(rets)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=64)
|
||||||
|
def combo_weekly(phase: int, f: float = 1.0) -> pd.Series:
|
||||||
|
"""Book 50/50 BTC+ETH per fase (stessa inner-join su date di scadenza del canonico)."""
|
||||||
|
rB = vrp_weekly_phase("BTC", phase, f)
|
||||||
|
rE = vrp_weekly_phase("ETH", phase, f)
|
||||||
|
return pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1).sort_index()
|
||||||
|
|
||||||
|
|
||||||
|
def to_daily_lumped(wk: pd.Series) -> pd.Series:
|
||||||
|
"""Convenzione _vrp_combo_returns: lump del rendimento settimanale sul giorno di scadenza."""
|
||||||
|
days = pd.date_range(wk.index.min().normalize(), wk.index.max().normalize(),
|
||||||
|
freq="1D", tz="UTC")
|
||||||
|
daily = pd.Series(0.0, index=days)
|
||||||
|
daily.loc[wk.index.normalize()] = wk.values
|
||||||
|
return daily
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# metriche
|
||||||
|
# ===========================================================================
|
||||||
|
def m_wk(r: pd.Series) -> dict:
|
||||||
|
"""Metriche sul ciclo settimanale naturale (convenzione dei numeri di ammissione)."""
|
||||||
|
r = r.dropna()
|
||||||
|
if len(r) < 3 or r.std() == 0:
|
||||||
|
return dict(sh=0.0, sh_h=0.0, cagr=0.0, dd=0.0, worst=0.0, act=0.0, n=len(r))
|
||||||
|
def _s(x):
|
||||||
|
return float(x.mean() / x.std() * np.sqrt(WKY)) if len(x) > 2 and x.std() > 0 else 0.0
|
||||||
|
eq = np.cumprod(1 + r.values); pk = np.maximum.accumulate(eq)
|
||||||
|
yrs = len(r) / WKY
|
||||||
|
return dict(sh=_s(r), sh_h=_s(r[r.index >= HOLDOUT]),
|
||||||
|
cagr=float(eq[-1] ** (1 / yrs) - 1) if yrs > 0 and eq[-1] > 0 else -1.0,
|
||||||
|
dd=float(np.max((pk - eq) / pk)), worst=float(r.min()),
|
||||||
|
act=float((r != 0.0).mean()), n=int(len(r)))
|
||||||
|
|
||||||
|
|
||||||
|
def sh_daily(s: pd.Series) -> float:
|
||||||
|
return al._sh(s)
|
||||||
|
|
||||||
|
|
||||||
|
def worst_7d(daily: pd.Series) -> float:
|
||||||
|
"""Peggior finestra di 7 giorni compounding, ancor-free (per l'ensemble)."""
|
||||||
|
lr = np.log1p(daily.values)
|
||||||
|
if len(lr) < 8:
|
||||||
|
return 0.0
|
||||||
|
cs = np.concatenate([[0.0], np.cumsum(lr)])
|
||||||
|
w = cs[7:] - cs[:-7]
|
||||||
|
return float(np.expm1(w.min()))
|
||||||
|
|
||||||
|
|
||||||
|
def pctl_of(vals: np.ndarray, v0: float) -> float:
|
||||||
|
return float(((vals < v0).sum() + 0.5 * (vals == v0).sum()) / len(vals) * 100.0)
|
||||||
|
|
||||||
|
|
||||||
|
def per_year(r: pd.Series) -> dict:
|
||||||
|
return {int(y): float(np.prod(1 + g.values) - 1) for y, g in r.groupby(r.index.year)}
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# PART 0 — replica bit-exact
|
||||||
|
# ===========================================================================
|
||||||
|
def part0() -> None:
|
||||||
|
print("=" * 100)
|
||||||
|
print("PART 0 — REPLICA della sleeve canonica (sanity bit-exact vs sleeves._vrp_combo_returns)")
|
||||||
|
print("=" * 100)
|
||||||
|
for a in ASSETS:
|
||||||
|
mine = vrp_weekly_phase(a, phase=0, f=VRP_CFG["f"])
|
||||||
|
ref = _vrp_weekly_asset(a)
|
||||||
|
assert len(mine) == len(ref), f"P0 len mismatch {a}: {len(mine)} vs {len(ref)}"
|
||||||
|
assert (mine.index == ref.index).all(), f"P0 index mismatch {a}"
|
||||||
|
dmax = float(np.max(np.abs(mine.values - ref.values)))
|
||||||
|
assert dmax == 0.0, f"P0 valori non bit-exact {a}: max|diff|={dmax:.2e}"
|
||||||
|
print(f" [{a}] settimanale fase-0 == _vrp_weekly_asset: bit-exact "
|
||||||
|
f"(n={len(mine)}, max|diff|={dmax:.1e})")
|
||||||
|
mine_d = to_daily_lumped(combo_weekly(0, VRP_CFG["f"]))
|
||||||
|
ref_d = _vrp_combo_returns()
|
||||||
|
assert len(mine_d) == len(ref_d) and (mine_d.index == ref_d.index).all(), "P0 grid daily"
|
||||||
|
dmax = float(np.max(np.abs(mine_d.values - ref_d.values)))
|
||||||
|
assert dmax == 0.0, f"P0 combo daily non bit-exact: {dmax:.2e}"
|
||||||
|
print(f" [COMBO] daily-lumped fase-0 == _vrp_combo_returns: bit-exact (n={len(mine_d)})")
|
||||||
|
for a in ASSETS:
|
||||||
|
J = joined(a)
|
||||||
|
gaps = J.index.to_series().diff().dt.total_seconds().dropna() / 86400.0
|
||||||
|
print(f" [{a}] join px/DVOL: {len(J)} giorni {J.index.min().date()} -> "
|
||||||
|
f"{J.index.max().date()}, gap>1g: {int((gaps > 1).sum())} "
|
||||||
|
f"(gap-free -> ogni fase = giorno-della-settimana FISSO)")
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# PART 1 — le 7 fasi a parametri identici (f=1.0)
|
||||||
|
# ===========================================================================
|
||||||
|
def part1() -> dict:
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("PART 1 — LE 7 FASI (parametri canonici, f=1.0). Ancora canonica = fase 0.")
|
||||||
|
print("=" * 100)
|
||||||
|
rows = {}
|
||||||
|
print(f" {'fase':<6}{'apre':>5}{'n_wk':>6}{'att%':>6}{'ShFULL':>8}{'ShHOLD':>8}"
|
||||||
|
f"{'CAGR':>8}{'maxDD':>8}{'worst-wk':>9}")
|
||||||
|
for p in PHASES:
|
||||||
|
wk = combo_weekly(p, 1.0)
|
||||||
|
entry_wd = int(pd.DatetimeIndex(wk.index - pd.Timedelta(days=VRP_CFG["tenor_d"]))
|
||||||
|
.dayofweek.min())
|
||||||
|
wd_n = pd.DatetimeIndex(wk.index).dayofweek.nunique()
|
||||||
|
mm = m_wk(wk)
|
||||||
|
rows[p] = dict(mm, entry_wd=entry_wd, wd_n=wd_n, wk=wk)
|
||||||
|
tag = " <- CANONICA" if p == 0 else ""
|
||||||
|
print(f" {p:<6}{DAYNAME[entry_wd]:>5}{mm['n']:>6}{mm['act']*100:>5.0f}%"
|
||||||
|
f"{mm['sh']:>8.2f}{mm['sh_h']:>8.2f}{mm['cagr']*100:>+7.1f}%"
|
||||||
|
f"{mm['dd']*100:>7.1f}%{mm['worst']*100:>+8.2f}%{tag}")
|
||||||
|
assert wd_n == 1, f"fase {p}: weekday non unico ({wd_n})"
|
||||||
|
|
||||||
|
for key, label in (("sh", "Sh FULL"), ("sh_h", "Sh HOLD"), ("dd", "maxDD"),
|
||||||
|
("worst", "worst-week"), ("cagr", "CAGR")):
|
||||||
|
v = np.array([rows[p][key] for p in PHASES])
|
||||||
|
print(f" banda {label:<11}: min {v.min():+.3f} / mediana {np.median(v):+.3f} / "
|
||||||
|
f"max {v.max():+.3f} | canonica {v[0]:+.3f} "
|
||||||
|
f"(pctl {pctl_of(v, v[0]):.0f}° su 7 fasi)")
|
||||||
|
|
||||||
|
print("\n per-anno (f=1.0) — dispersione di fase attraverso il 2022 (LUNA/FTX):")
|
||||||
|
yrs = sorted({y for p in PHASES for y in per_year(rows[p]["wk"])})
|
||||||
|
print(" fase " + "".join(f"{y:>9}" for y in yrs))
|
||||||
|
for p in PHASES:
|
||||||
|
py = per_year(rows[p]["wk"])
|
||||||
|
print(f" {p:<6}" + "".join(f"{py.get(y, float('nan'))*100:>+8.1f}%" for y in yrs))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# PART 2 — banda f (caveat skew: pricing BS flat su DVOL ATM)
|
||||||
|
# ===========================================================================
|
||||||
|
def part2() -> None:
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("PART 2 — BANDA f {0.6, 0.8, 1.0, 1.3} x 7 fasi (skew non esplicito -> banda, non punto)")
|
||||||
|
print("=" * 100)
|
||||||
|
print(f" {'f':>5} | {'ShFULL min/med/max':>22} {'(canon)':>8} | {'ShHOLD min/med/max':>22} "
|
||||||
|
f"{'(canon)':>8} | {'maxDD min/med/max':>20} {'(canon)':>8} | {'worst min':>9}")
|
||||||
|
for f in F_BAND:
|
||||||
|
mm = [m_wk(combo_weekly(p, f)) for p in PHASES]
|
||||||
|
sf = np.array([m["sh"] for m in mm]); shh = np.array([m["sh_h"] for m in mm])
|
||||||
|
dd = np.array([m["dd"] for m in mm]); wo = np.array([m["worst"] for m in mm])
|
||||||
|
print(f" {f:>5.1f} | {sf.min():>6.2f} {np.median(sf):>6.2f} {sf.max():>6.2f} "
|
||||||
|
f"{sf[0]:>7.2f} | {shh.min():>6.2f} {np.median(shh):>6.2f} {shh.max():>6.2f} "
|
||||||
|
f"{shh[0]:>7.2f} | {dd.min()*100:>5.1f} {np.median(dd)*100:>5.1f} "
|
||||||
|
f"{dd.max()*100:>5.1f}% {dd[0]*100:>6.1f}% | {wo.min()*100:>+8.2f}%")
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# PART 3 — ensemble delle 7 fasi + null del de-levering
|
||||||
|
# ===========================================================================
|
||||||
|
def phase_matrix(f: float = 1.0) -> pd.DataFrame:
|
||||||
|
cols = {f"p{p}": to_daily_lumped(combo_weekly(p, f)) for p in PHASES}
|
||||||
|
M = pd.concat(cols, axis=1, join="outer").sort_index()
|
||||||
|
lo = max(c.index.min() for c in cols.values())
|
||||||
|
hi = min(c.index.max() for c in cols.values())
|
||||||
|
return M.loc[(M.index >= lo) & (M.index <= hi)].fillna(0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def part3(rows: dict) -> dict:
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("PART 3 — ENSEMBLE delle 7 fasi (1/7 ciascuna) + NULL DEL DE-LEVERING")
|
||||||
|
print("=" * 100)
|
||||||
|
M = phase_matrix(1.0)
|
||||||
|
ens = M.mean(axis=1)
|
||||||
|
can = M["p0"]
|
||||||
|
def _stats(s, name):
|
||||||
|
dd = al._dd_ret(s)
|
||||||
|
out = dict(sh=sh_daily(s), sh_h=sh_daily(s[s.index >= HOLDOUT]), dd=dd,
|
||||||
|
w7=worst_7d(s),
|
||||||
|
cagr=float(np.prod(1 + s.values) ** (365.25 / len(s)) - 1))
|
||||||
|
print(f" {name:<28} ShFULL {out['sh']:>5.2f} ShHOLD {out['sh_h']:>5.2f} "
|
||||||
|
f"CAGR {out['cagr']*100:>+5.1f}% maxDD {out['dd']*100:>5.1f}% "
|
||||||
|
f"worst-7g {out['w7']*100:>+6.2f}%")
|
||||||
|
return out
|
||||||
|
print(" (griglia daily comune alle 7 fasi; Sharpe daily ~= Sharpe weekly del lump)")
|
||||||
|
s_can = _stats(can, "CANONICA fase-0")
|
||||||
|
s_ens = _stats(ens, "ENSEMBLE 7 fasi (1/7)")
|
||||||
|
|
||||||
|
# NULL DEL DE-LEVERING: lo Sharpe e' scale-free -> scalare la canonica non lo cambia.
|
||||||
|
# Cerco lambda t.c. maxDD(lam*canonica) ~= maxDD(ensemble) e confronto CAGR a pari DD.
|
||||||
|
lams = np.linspace(0.30, 1.00, 141)
|
||||||
|
dds = np.array([al._dd_ret(can * lam) for lam in lams])
|
||||||
|
k = int(np.argmin(np.abs(dds - s_ens["dd"])))
|
||||||
|
lam = float(lams[k]); scaled = can * lam
|
||||||
|
cagr_sc = float(np.prod(1 + scaled.values) ** (365.25 / len(scaled)) - 1)
|
||||||
|
print(f"\n NULL de-levering: canonica x lambda={lam:.2f} -> maxDD {dds[k]*100:.1f}% "
|
||||||
|
f"(~ ensemble {s_ens['dd']*100:.1f}%), Sharpe INVARIATO {sh_daily(scaled):.2f}, "
|
||||||
|
f"CAGR {cagr_sc*100:+.1f}% (ensemble {s_ens['cagr']*100:+.1f}%)")
|
||||||
|
# LENTE ONESTA sul numero dell'ensemble: lo Sharpe daily (sopra) e' GONFIATO dalla
|
||||||
|
# convenzione del lump — le 7 fasi lumpano su giorni DISGIUNTI (corr daily ~0 per
|
||||||
|
# costruzione) ma i 7 spread concorrenti condividono ~6/7 dell'esposizione economica.
|
||||||
|
# Ricompound a blocchi di 7 giorni (tutti gli offset) = P&L settimanale del libro
|
||||||
|
# sfalsato con la correlazione cross-fase RIPRISTINATA.
|
||||||
|
def sh_7d_blocks(s: pd.Series) -> np.ndarray:
|
||||||
|
lr = np.log1p(s.values)
|
||||||
|
shs = []
|
||||||
|
for o in range(7):
|
||||||
|
m = (len(lr) - o) // 7
|
||||||
|
if m < 30:
|
||||||
|
continue
|
||||||
|
w = np.expm1(lr[o:o + 7 * m].reshape(m, 7).sum(axis=1))
|
||||||
|
shs.append(float(w.mean() / w.std() * np.sqrt(WKY)) if w.std() > 0 else 0.0)
|
||||||
|
return np.array(shs)
|
||||||
|
print("\n LENTE ONESTA (Sharpe su blocchi 7g non-overlap, banda sui 7 offset di blocco):")
|
||||||
|
for name, s in (("CANONICA fase-0", can), ("ENSEMBLE 7 fasi", ens)):
|
||||||
|
vf = sh_7d_blocks(s); vh = sh_7d_blocks(s[s.index >= HOLDOUT])
|
||||||
|
print(f" {name:<20} FULL min/med/max {vf.min():.2f}/{np.median(vf):.2f}/{vf.max():.2f}"
|
||||||
|
+ (f" | HOLD {vh.min():.2f}/{np.median(vh):.2f}/{vh.max():.2f}" if len(vh) else ""))
|
||||||
|
print(" -> il numero citabile dell'ensemble e' quello a blocchi 7g, NON lo Sh daily del lump.")
|
||||||
|
print(" NB: anche il maxDD (canonica E ensemble) e' su equity a date di REGOLAMENTO —")
|
||||||
|
print(" il mark-to-market intra-settimana degli spread aperti non e' catturato (caveat noto).")
|
||||||
|
beats = (s_ens["sh"] > s_can["sh"] + 1e-9) and (s_ens["cagr"] > cagr_sc + 1e-9)
|
||||||
|
print(f"\n -> l'ensemble batte il de-levering? {'SI' if beats else 'NO'} "
|
||||||
|
f"(serve Sh_ens > Sh_can E CAGR_ens > CAGR de-levered a pari DD; il CAGR e' "
|
||||||
|
f"convention-free, lo Sh daily dell'ensemble NO — vedi lente onesta)")
|
||||||
|
print(" NB deploy: a $600-2k un solo spread min-size satura il libro -> 7 tranche da 1/7")
|
||||||
|
print(" NON sono eseguibili (stesso muro del tranching TP01); l'ensemble e' una LENTE di")
|
||||||
|
print(" misura (de-lucking della stima), non una modifica proponibile del book.")
|
||||||
|
return dict(ens=s_ens, can=s_can, lam=lam, cagr_scaled=cagr_sc, beats=beats)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# PART 4 — block-bootstrap P(spike) (metodo r0702_skeptic_offset)
|
||||||
|
# ===========================================================================
|
||||||
|
def _sh_mat(R: np.ndarray) -> np.ndarray:
|
||||||
|
mu = R.mean(axis=1); sd = R.std(axis=1)
|
||||||
|
with np.errstate(divide="ignore", invalid="ignore"):
|
||||||
|
out = np.where(sd > 0, mu / sd, 0.0)
|
||||||
|
return np.nan_to_num(out) * np.sqrt(365.25)
|
||||||
|
|
||||||
|
|
||||||
|
def block_boot(M: np.ndarray, B: int, block: int, rng) -> dict:
|
||||||
|
n, K = M.shape
|
||||||
|
nblocks = int(np.ceil(n / block))
|
||||||
|
g0s, gmaxs = [], []
|
||||||
|
done = 0
|
||||||
|
while done < B:
|
||||||
|
b = min(500, B - done)
|
||||||
|
starts = rng.integers(0, n, size=(b, nblocks))
|
||||||
|
idx = (starts[:, :, None] + np.arange(block)[None, None, :]) % n
|
||||||
|
idx = idx.reshape(b, -1)[:, :n]
|
||||||
|
R = M[idx]
|
||||||
|
Sh = np.stack([_sh_mat(R[:, :, k]) for k in range(K)], axis=1)
|
||||||
|
med_others = np.empty_like(Sh)
|
||||||
|
for h in range(K):
|
||||||
|
others = np.delete(Sh, h, axis=1)
|
||||||
|
med_others[:, h] = np.median(others, axis=1)
|
||||||
|
g = Sh - med_others
|
||||||
|
g0s.append(g[:, 0]); gmaxs.append(g.max(axis=1))
|
||||||
|
done += b
|
||||||
|
return dict(g0=np.concatenate(g0s), gmax=np.concatenate(gmaxs))
|
||||||
|
|
||||||
|
|
||||||
|
def part4() -> dict:
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("PART 4 — BLOCK-BOOTSTRAP: lo spike della fase canonica e' speciale o massimo atteso di 7?")
|
||||||
|
print("=" * 100)
|
||||||
|
M = phase_matrix(1.0)
|
||||||
|
out = {}
|
||||||
|
for wname, W in (("HOLD-OUT 2025+", M[M.index >= HOLDOUT]),
|
||||||
|
("FULL 2021-26", M)):
|
||||||
|
Mv = W.values
|
||||||
|
sh = _sh_mat(Mv.T)
|
||||||
|
g0 = float(sh[0] - np.median(sh[1:]))
|
||||||
|
corr = np.corrcoef(Mv.T); iu = np.triu_indices(len(PHASES), 1)
|
||||||
|
print(f"\n [{wname}] {Mv.shape[0]} giorni | Sh fase-0 {sh[0]:+.3f}, mediana altre "
|
||||||
|
f"{np.median(sh[1:]):+.3f}, spike g0 = {g0:+.3f} | corr daily fra fasi: "
|
||||||
|
f"mediana {np.median(corr[iu]):+.3f} (lump su giorni diversi -> quasi-ortogonali "
|
||||||
|
f"per costruzione)")
|
||||||
|
for blk in (14, 28, 56):
|
||||||
|
bs = block_boot(Mv, B_BOOT, blk, np.random.default_rng(73 + blk))
|
||||||
|
p_any = float(np.mean(bs["gmax"] >= g0))
|
||||||
|
p_le0 = float(np.mean(bs["g0"] <= 0.0))
|
||||||
|
ci = np.percentile(bs["g0"], [2.5, 97.5])
|
||||||
|
print(f" block={blk:>2}: P(spike di UNA QUALSIASI fase >= {g0:+.2f}) = {p_any:.3f}"
|
||||||
|
f" | P(g0<=0) = {p_le0:.3f} | CI95 g0 [{ci[0]:+.2f},{ci[1]:+.2f}]")
|
||||||
|
out[(wname, blk)] = p_any
|
||||||
|
# finestre annuali: la fase migliore e' stabile o gira?
|
||||||
|
print("\n finestre annuali — Sh per fase, pctl della canonica, best-fase:")
|
||||||
|
print(f" {'finestra':<9}" + "".join(f"{'p'+str(p):>8}" for p in PHASES)
|
||||||
|
+ f"{'pctl p0':>9}{'best':>6}")
|
||||||
|
idx_years = sorted(set(M.index.year))
|
||||||
|
for y in idx_years:
|
||||||
|
W = M[M.index.year == y]
|
||||||
|
if len(W) < 60:
|
||||||
|
continue
|
||||||
|
sh = _sh_mat(W.values.T)
|
||||||
|
print(f" {y:<9}" + "".join(f"{v:>8.2f}" for v in sh)
|
||||||
|
+ f"{pctl_of(sh, sh[0]):>8.0f}°{int(np.argmax(sh)):>6}")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# PART 5 — impatto sul book 5-sleeve
|
||||||
|
# ===========================================================================
|
||||||
|
def part5(rows: dict) -> dict:
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("PART 5 — BOOK 5-SLEEVE (TP 33/XS 15/VRP 12/SKH 20/GTAA 20) con VRP01 per fase")
|
||||||
|
print("=" * 100)
|
||||||
|
from src.portfolio.sleeves import (_gtaa_daily_returns, _skyhook_returns,
|
||||||
|
_tp01_returns, _xsec_returns)
|
||||||
|
tp = to_daily(_tp01_returns())
|
||||||
|
fixed = dict(TP=tp, XS=to_daily(_xsec_returns()), SKH=to_daily(_skyhook_returns()),
|
||||||
|
GTAA=to_daily(_gtaa_daily_returns()))
|
||||||
|
W = dict(TP=0.33, XS=0.15, VRP=0.12, SKH=0.20, GTAA=0.20)
|
||||||
|
lo = tp.index.min()
|
||||||
|
|
||||||
|
def book(vrp_daily):
|
||||||
|
s = combine_outer(dict(fixed, VRP=vrp_daily), W, lo=lo)
|
||||||
|
return dict(sh=al._sh(s), sh_h=al._sh(s[s.index >= HOLDOUT]), dd=al._dd_ret(s),
|
||||||
|
dd_h=al._dd_ret(s[s.index >= HOLDOUT]))
|
||||||
|
|
||||||
|
res = {}
|
||||||
|
print(f" {'variante VRP':<22}{'ShFULL':>8}{'ShHOLD':>8}{'maxDD':>8}{'DD hold':>9}")
|
||||||
|
for p in PHASES:
|
||||||
|
res[p] = book(to_daily_lumped(rows[p]["wk"]))
|
||||||
|
tag = " <- CANONICA (book corrente)" if p == 0 else ""
|
||||||
|
print(f" fase {p} ({DAYNAME[rows[p]['entry_wd']]}){'':<10}{res[p]['sh']:>8.2f}"
|
||||||
|
f"{res[p]['sh_h']:>8.2f}{res[p]['dd']*100:>7.1f}%{res[p]['dd_h']*100:>8.1f}%{tag}")
|
||||||
|
M = phase_matrix(1.0)
|
||||||
|
res["ens"] = book(M.mean(axis=1))
|
||||||
|
print(f" {'ENSEMBLE 7 fasi':<22}{res['ens']['sh']:>8.2f}{res['ens']['sh_h']:>8.2f}"
|
||||||
|
f"{res['ens']['dd']*100:>7.1f}%{res['ens']['dd_h']*100:>8.1f}%")
|
||||||
|
|
||||||
|
for key, label in (("sh", "ShFULL"), ("sh_h", "ShHOLD"), ("dd", "maxDD")):
|
||||||
|
v = np.array([res[p][key] for p in PHASES])
|
||||||
|
print(f" banda book {label:<7}: min {v.min():.3f} / mediana {np.median(v):.3f} / "
|
||||||
|
f"max {v.max():.3f} | canonica {v[0]:.3f} (pctl {pctl_of(v, v[0]):.0f}°)")
|
||||||
|
print(" (il peso VRP e' 12%: la banda di fase si diluisce ~0.12 nel book — atteso stretto)")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# PART 6 — ri-verdetto dei numeri di ammissione alla fase mediana
|
||||||
|
# ===========================================================================
|
||||||
|
def part6(rows: dict) -> None:
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("PART 6 — RI-VERDETTO dei numeri di ammissione VRP01 (FULL 1.10 / HOLD 0.60 / DD 12%)")
|
||||||
|
print("=" * 100)
|
||||||
|
sf = np.array([rows[p]["sh"] for p in PHASES])
|
||||||
|
shh = np.array([rows[p]["sh_h"] for p in PHASES])
|
||||||
|
dd = np.array([rows[p]["dd"] for p in PHASES])
|
||||||
|
wo = np.array([rows[p]["worst"] for p in PHASES])
|
||||||
|
print(f" Sh FULL : dichiarato {ADMISSION['full']:.2f} | canonica {sf[0]:.2f} "
|
||||||
|
f"(pctl {pctl_of(sf, sf[0]):.0f}°) | mediana fasi {np.median(sf):.2f} | "
|
||||||
|
f"banda [{sf.min():.2f}, {sf.max():.2f}]")
|
||||||
|
print(f" Sh HOLD : dichiarato {ADMISSION['hold']:.2f} | canonica {shh[0]:.2f} "
|
||||||
|
f"(pctl {pctl_of(shh, shh[0]):.0f}°) | mediana fasi {np.median(shh):.2f} | "
|
||||||
|
f"banda [{shh.min():.2f}, {shh.max():.2f}]")
|
||||||
|
print(f" maxDD : dichiarato {ADMISSION['dd']*100:.0f}% | canonica {dd[0]*100:.1f}% "
|
||||||
|
f"(pctl {pctl_of(dd, dd[0]):.0f}°) | mediana fasi {np.median(dd)*100:.1f}% | "
|
||||||
|
f"banda [{dd.min()*100:.1f}%, {dd.max()*100:.1f}%]")
|
||||||
|
print(f" worst-wk: canonica {wo[0]*100:+.2f}% | mediana {np.median(wo)*100:+.2f}% | "
|
||||||
|
f"peggiore {wo.min()*100:+.2f}%")
|
||||||
|
print("\n Regola 2026-07-02: i numeri di VRP01 si citano d'ora in poi CON la banda di fase.")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
part0()
|
||||||
|
rows = part1()
|
||||||
|
part2()
|
||||||
|
part3(rows)
|
||||||
|
part4()
|
||||||
|
part5(rows)
|
||||||
|
part6(rows)
|
||||||
|
print("\nFatto (r0703_vrpimp_anchor).")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
"""R0703 VRPIMP-DDOVERLAY — overlay di protezione DD su VRP01 canonico vs NULL DEL DE-LEVERING.
|
||||||
|
|
||||||
|
FILONE 2 (ondata 2026-07-03): su VRP01 canonico (put credit spread settimanale -0.28/-0.10,
|
||||||
|
gate vrp>0 + ivr>=0.30 + crash-skip 0.90, riprodotto ESATTO dal motore path qui sotto e
|
||||||
|
bridge-validato vs options_vrp_v2.vrp_spread_weekly) si testano 4 famiglie di overlay:
|
||||||
|
|
||||||
|
(a) EXIT INTRA-SETTIMANA su spike: DVOL sale >X% dall'ingresso, oppure RV5 causale supera
|
||||||
|
m x DVOL d'ingresso -> chiudi lo spread al close del giorno del trigger (MTM BS).
|
||||||
|
(b) STOP-LOSS sul MTM: esci se il valore dello spread raggiunge mult x credito incassato.
|
||||||
|
(c) ALA DI CODA comprata: q put molto piu' OTM (delta -0.05/-0.03) finanziate col credito
|
||||||
|
(ratio/broken-wing-ish), tenute a scadenza.
|
||||||
|
(d) COOLDOWN: salta c settimane di calendario dopo una settimana attiva in perdita.
|
||||||
|
|
||||||
|
REGOLA VINCOLANTE (lezione TP01xDVOL 2026-06-26): ogni claim di riduzione DD si confronta col
|
||||||
|
NULL DEL DE-LEVERING — lo scaling s della size del baseline che produce lo STESSO maxDD. Lo
|
||||||
|
Sharpe e' scale-invariante => il de-levered ha SEMPRE lo Sharpe del baseline: un overlay "vale"
|
||||||
|
solo se il suo Sharpe batte quello del baseline A PARI maxDD (e regge banda f + multi-cut + DSR).
|
||||||
|
|
||||||
|
⚠️ LENS DICHIARATA (in testa, non in fondo):
|
||||||
|
- Granularita' 1d: i trigger (a)/(b) si valutano al CLOSE giornaliero e si esegue a QUEL close
|
||||||
|
-> il gap-through oltre il livello di stop E' DENTRO il fill (onesto), ma lo spike intraday
|
||||||
|
peggiore del close NON e' modellato (lezione SKH01: a 1d lo stop e' una lente, non un fill).
|
||||||
|
- MTM Black-Scholes FLAT sulla DVOL-30g usata per tenor <=7g: in stress il front-end esplode
|
||||||
|
piu' della 30g e lo skew irripidisce -> il MTM avverso e' SOTTOSTIMATO => gli stop scattano
|
||||||
|
piu' tardi e costano piu' del modellato. Percio' banda f {0.6,0.8,1.0,1.3} obbligatoria.
|
||||||
|
- Booking: il pnl di ogni trade (anche se chiuso prima) e' registrato alla data di SCADENZA
|
||||||
|
-> griglia settimanale identica al baseline, Sharpe confrontabile (lente, non timing reale).
|
||||||
|
- Equity settimanale compounding (convenzione VRP01): DD/worst-week su quella lente.
|
||||||
|
- Banda d'ancora (regola 2026-07-02): loop settimanale ancorato a i0=60 -> 7 offset riportati.
|
||||||
|
|
||||||
|
Regola standing: niente short-vol da modello in deploy — obiettivo = conoscenza/LEAD.
|
||||||
|
Selezione celle SOLO in-sample (pre-2025); hold-out 2025-26 mai usato per scegliere; DSR sul
|
||||||
|
numero totale di celle (14).
|
||||||
|
|
||||||
|
uv run python scripts/research/r0703_vrpimp_ddoverlay.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt"))
|
||||||
|
import numpy as np, pandas as pd
|
||||||
|
from scripts.research.options_vrp_lab import bs_put, strike_from_delta, load_series, m_weekly, per_year
|
||||||
|
from scripts.research.options_vrp_v2 import vrp_spread_weekly, _ivrank, _rv30
|
||||||
|
|
||||||
|
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
|
||||||
|
WK = 365.25 / 7.0
|
||||||
|
F_SWEEP = (0.6, 0.8, 1.0, 1.3)
|
||||||
|
FEE_FRAC = 0.125 # convenzione VRP01: 12.5% del premio netto per il round-trip base
|
||||||
|
TENOR = 7
|
||||||
|
|
||||||
|
|
||||||
|
def _rv5(px: np.ndarray, j: int) -> float:
|
||||||
|
"""RV annualizzata causale dagli ultimi 5 rendimenti giornalieri (fino a j incluso)."""
|
||||||
|
if j < 6:
|
||||||
|
return np.nan
|
||||||
|
r = np.diff(np.log(px[j - 5:j + 1]))
|
||||||
|
return float(np.std(r) * np.sqrt(365.25))
|
||||||
|
|
||||||
|
|
||||||
|
def vrp_path_weekly(asset, f=1.0, overlay=None, i0=60, collect=None):
|
||||||
|
"""VRP01 canonico con path MTM giornaliero. overlay=None riproduce vrp_spread_weekly
|
||||||
|
(defined_risk, gate vrp>0 + ivr>=0.30 + crash_skip 0.90) ESATTO (bridge sotto).
|
||||||
|
|
||||||
|
overlay: ('spike_dvol', x) | ('spike_rv', m) | ('sl', mult) | ('tail', delta, q) | ('cool', c)
|
||||||
|
Ritorna Series settimanale di rendimenti su capitale = strike corto (convenzione VRP01).
|
||||||
|
"""
|
||||||
|
J = load_series(asset)
|
||||||
|
px = J["px"].values; dv = J["dvol"].values / 100.0; idx = J.index
|
||||||
|
n = len(px); T = TENOR / 365.25
|
||||||
|
kind = overlay[0] if overlay else None
|
||||||
|
rets = {}
|
||||||
|
cool_left = 0
|
||||||
|
i = i0
|
||||||
|
while i + TENOR < n:
|
||||||
|
S0 = px[i]; sig = dv[i]
|
||||||
|
# --- cooldown (d): decisione causale dal risultato del trade precedente gia' chiuso ---
|
||||||
|
if kind == "cool" and cool_left > 0:
|
||||||
|
cool_left -= 1
|
||||||
|
rets[idx[i + TENOR]] = 0.0
|
||||||
|
i += TENOR
|
||||||
|
continue
|
||||||
|
# --- gate canonici VRP01 (identici a vrp_spread_weekly, NON riottimizzati) ---
|
||||||
|
skip = False
|
||||||
|
rv = _rv30(px, i)
|
||||||
|
if not np.isnan(rv) and (sig - rv) <= 0:
|
||||||
|
skip = True
|
||||||
|
ivr = _ivrank(dv, i)
|
||||||
|
if not np.isnan(ivr) and (ivr < 0.30 or ivr > 0.90):
|
||||||
|
skip = True
|
||||||
|
if skip:
|
||||||
|
rets[idx[i + TENOR]] = 0.0
|
||||||
|
i += TENOR
|
||||||
|
continue
|
||||||
|
# --- struttura canonica ---
|
||||||
|
Ks = strike_from_delta(S0, T, sig, -0.28)
|
||||||
|
Kl = strike_from_delta(S0, T, sig, -0.10)
|
||||||
|
net_prem = (bs_put(S0, Ks, T, sig) - bs_put(S0, Kl, T, sig)) * f
|
||||||
|
base_fee = FEE_FRAC * abs(net_prem)
|
||||||
|
# --- ala di coda (c): q put extra-OTM a stessa scadenza, pagata col credito ---
|
||||||
|
tail_cost = tail_fee = 0.0; K_tail = None; q = 0
|
||||||
|
if kind == "tail":
|
||||||
|
_, dlt, q = overlay
|
||||||
|
K_tail = strike_from_delta(S0, T, sig, dlt)
|
||||||
|
prem_t = bs_put(S0, K_tail, T, sig) * f
|
||||||
|
tail_cost = q * prem_t
|
||||||
|
tail_fee = q * min(0.0003 * S0, 0.125 * max(prem_t, 1e-12))
|
||||||
|
if collect is not None:
|
||||||
|
collect.setdefault("tail_frac", []).append(tail_cost / max(net_prem, 1e-12))
|
||||||
|
# --- path MTM giornaliero: exit intra-settimana (a)/(b) al primo trigger, a QUEL close ---
|
||||||
|
exited = False
|
||||||
|
if kind in ("spike_dvol", "spike_rv", "sl"):
|
||||||
|
for j in range(i + 1, i + TENOR):
|
||||||
|
T_rem = (i + TENOR - j) / 365.25
|
||||||
|
trig = False
|
||||||
|
if kind == "spike_dvol":
|
||||||
|
trig = (dv[j] / sig - 1.0) > overlay[1]
|
||||||
|
elif kind == "spike_rv":
|
||||||
|
r5 = _rv5(px, j)
|
||||||
|
trig = (not np.isnan(r5)) and (r5 > overlay[1] * sig)
|
||||||
|
elif kind == "sl":
|
||||||
|
Vj = (bs_put(px[j], Ks, T_rem, dv[j]) - bs_put(px[j], Kl, T_rem, dv[j])) * f
|
||||||
|
trig = Vj >= overlay[1] * net_prem
|
||||||
|
if trig:
|
||||||
|
Vj = (bs_put(px[j], Ks, T_rem, dv[j]) - bs_put(px[j], Kl, T_rem, dv[j])) * f
|
||||||
|
exit_fee = min(0.0006 * px[j], 0.125 * max(Vj, 1e-12)) # 2 gambe buyback
|
||||||
|
pnl = net_prem - Vj - base_fee - exit_fee
|
||||||
|
rets[idx[i + TENOR]] = pnl / Ks # booking a scadenza (lens dichiarata)
|
||||||
|
if collect is not None:
|
||||||
|
collect.setdefault("exits", []).append((idx[j], (net_prem - Vj) / Ks))
|
||||||
|
exited = True
|
||||||
|
break
|
||||||
|
if not exited:
|
||||||
|
S1 = px[i + TENOR]
|
||||||
|
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1)
|
||||||
|
pnl = net_prem - payoff - base_fee
|
||||||
|
if kind == "tail":
|
||||||
|
pnl += q * max(0.0, K_tail - S1) - tail_cost - tail_fee
|
||||||
|
rets[idx[i + TENOR]] = pnl / Ks
|
||||||
|
if kind == "cool" and rets[idx[i + TENOR]] < 0:
|
||||||
|
cool_left = overlay[1]
|
||||||
|
i += TENOR
|
||||||
|
return pd.Series(rets)
|
||||||
|
|
||||||
|
|
||||||
|
def book(f=1.0, overlay=None, i0=60, collect=None):
|
||||||
|
rB = vrp_path_weekly("BTC", f=f, overlay=overlay, i0=i0, collect=collect)
|
||||||
|
rE = vrp_path_weekly("ETH", f=f, overlay=overlay, i0=i0, collect=collect)
|
||||||
|
return pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1)
|
||||||
|
|
||||||
|
|
||||||
|
def maxdd(r):
|
||||||
|
eq = np.cumprod(1 + r.values); pk = np.maximum.accumulate(eq)
|
||||||
|
return float(np.max((pk - eq) / pk)) if len(r) else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def delever_scale(r_base, dd_target, hi=5.0):
|
||||||
|
"""Scaling s della size del baseline che produce maxDD == dd_target (bisezione)."""
|
||||||
|
lo = 0.0
|
||||||
|
for _ in range(80):
|
||||||
|
mid = (lo + hi) / 2
|
||||||
|
if maxdd(mid * r_base) > dd_target:
|
||||||
|
hi = mid
|
||||||
|
else:
|
||||||
|
lo = mid
|
||||||
|
return (lo + hi) / 2
|
||||||
|
|
||||||
|
|
||||||
|
def mstats(r):
|
||||||
|
full = m_weekly(r); ho = m_weekly(r[r.index >= HOLDOUT]); isam = m_weekly(r[r.index < HOLDOUT])
|
||||||
|
return dict(sh=full["sh"], sh_h=ho["sh"], sh_is=isam["sh"], cagr=full["cagr"],
|
||||||
|
dd=full["dd"], worst=float(r.min()) if len(r) else 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def fmt(name, m, extra=""):
|
||||||
|
print(f" {name:<34} ShF {m['sh']:>5.2f} ShH {m['sh_h']:>5.2f} Sh-IS {m['sh_is']:>5.2f} "
|
||||||
|
f"CAGR {m['cagr']*100:>+5.1f}% DD {m['dd']*100:>4.1f}% worst {m['worst']*100:>+5.1f}%{extra}")
|
||||||
|
|
||||||
|
|
||||||
|
OVERLAYS = {
|
||||||
|
"a-spike": [("spike_dvol", 0.15), ("spike_dvol", 0.25), ("spike_dvol", 0.40),
|
||||||
|
("spike_rv", 1.00), ("spike_rv", 1.25)],
|
||||||
|
"b-stoploss": [("sl", 1.5), ("sl", 2.0), ("sl", 3.0)],
|
||||||
|
"c-tailwing": [("tail", -0.05, 1), ("tail", -0.05, 2), ("tail", -0.03, 1), ("tail", -0.03, 2)],
|
||||||
|
"d-cooldown": [("cool", 1), ("cool", 2)],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def olabel(ov):
|
||||||
|
k = ov[0]
|
||||||
|
if k == "spike_dvol":
|
||||||
|
return f"exit DVOL +{ov[1]*100:.0f}% da entry"
|
||||||
|
if k == "spike_rv":
|
||||||
|
return f"exit RV5 > {ov[1]:.2f}x DVOL entry"
|
||||||
|
if k == "sl":
|
||||||
|
return f"stop-loss MTM {ov[1]:.1f}x credito"
|
||||||
|
if k == "tail":
|
||||||
|
return f"tail put d={ov[1]:+.2f} q={ov[2]}"
|
||||||
|
return f"cooldown {ov[1]} settimana/e"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 112)
|
||||||
|
print(" R0703 VRPIMP-DDOVERLAY — overlay di protezione DD su VRP01 vs NULL DEL DE-LEVERING")
|
||||||
|
print(" Lens: MTM BS flat su DVOL-30g, trigger e fill al close 1d (gap-through nel fill, spike")
|
||||||
|
print(" intraday e term-structure di stress NON modellati -> stop piu' lenti/cari del reale).")
|
||||||
|
print(" Booking a scadenza su griglia settimanale. Selezione celle SOLO in-sample (pre-2025).")
|
||||||
|
print("=" * 112)
|
||||||
|
for a in ("BTC", "ETH"):
|
||||||
|
J = load_series(a)
|
||||||
|
print(f" {a}: dati {J.index[0].date()} -> {J.index[-1].date()} ({len(J)} giorni)")
|
||||||
|
print(" Eventi chiave nel campione: 2021-05 (crash Cina), 2022-05 (LUNA), 2022-11 (FTX).")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- (0) bridge di validazione
|
||||||
|
print("\n (0) BRIDGE: motore path (overlay=None) vs options_vrp_v2.vrp_spread_weekly (VRP01)")
|
||||||
|
base = {}
|
||||||
|
for f in F_SWEEP:
|
||||||
|
canon = pd.concat({"B": vrp_spread_weekly("BTC", defined_risk=True, f=f, gate_vrp=True,
|
||||||
|
gate_ivr=0.30, crash_skip=0.90),
|
||||||
|
"E": vrp_spread_weekly("ETH", defined_risk=True, f=f, gate_vrp=True,
|
||||||
|
gate_ivr=0.30, crash_skip=0.90)},
|
||||||
|
axis=1, join="inner").mean(axis=1)
|
||||||
|
mine = book(f=f)
|
||||||
|
Jn = pd.concat({"c": canon, "m": mine}, axis=1, join="inner")
|
||||||
|
dmax = float((Jn["c"] - Jn["m"]).abs().max())
|
||||||
|
base[f] = mine
|
||||||
|
if f == 1.0:
|
||||||
|
print(f" f={f}: max|diff| = {dmax:.2e} su {len(Jn)} settimane "
|
||||||
|
f"{'OK (riproduzione esatta)' if dmax < 1e-12 else '⚠️ MISMATCH'}")
|
||||||
|
|
||||||
|
print("\n (1) BASELINE VRP01 canonico — banda f")
|
||||||
|
for f in F_SWEEP:
|
||||||
|
fmt(f"VRP01 f={f}", mstats(base[f]))
|
||||||
|
pyb = per_year(base[1.0])
|
||||||
|
print(" per-anno f=1.0: " + " ".join(f"{y}:{v*100:+5.1f}%" for y, v in sorted(pyb.items())))
|
||||||
|
anchor_sh = [m_weekly(book(f=1.0, i0=60 + o))["sh"] for o in range(7)]
|
||||||
|
print(f" banda d'ancora (7 offset del loop settimanale): ShF in "
|
||||||
|
f"[{min(anchor_sh):.2f}, {max(anchor_sh):.2f}] mediana {np.median(anchor_sh):.2f}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- (2) griglia overlay f=1.0
|
||||||
|
print(f"\n (2) GRIGLIA OVERLAY (f=1.0) — 14 celle, selezione per-famiglia su Sh-IS (pre-2025)")
|
||||||
|
m0 = mstats(base[1.0])
|
||||||
|
all_full_sh = [m0["sh"]]
|
||||||
|
cells = {}
|
||||||
|
best = {}
|
||||||
|
for fam, ovs in OVERLAYS.items():
|
||||||
|
for ov in ovs:
|
||||||
|
col = {}
|
||||||
|
r = book(f=1.0, overlay=ov, collect=col)
|
||||||
|
m = mstats(r)
|
||||||
|
cells[ov] = (r, m, col)
|
||||||
|
all_full_sh.append(m["sh"])
|
||||||
|
n_ex = len(col.get("exits", []))
|
||||||
|
extra = f" dSh {m['sh']-m0['sh']:+.2f}"
|
||||||
|
if n_ex:
|
||||||
|
extra += f" early-exit n={n_ex}"
|
||||||
|
if col.get("tail_frac"):
|
||||||
|
extra += f" costo ala = {np.mean(col['tail_frac'])*100:.0f}% del credito"
|
||||||
|
fmt(f"[{fam}] {olabel(ov)}", m, extra)
|
||||||
|
if fam not in best or m["sh_is"] > cells[best[fam]][1]["sh_is"]:
|
||||||
|
best[fam] = ov
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- (3) best-IS per famiglia: banda f + NULL de-levering
|
||||||
|
print(" (3) CELLA BEST-IN-SAMPLE PER FAMIGLIA — banda f + CONFRONTO OBBLIGATORIO col null de-levering")
|
||||||
|
print(" (de-lever = s x baseline con lo STESSO maxDD; lo Sharpe del de-levered = Sharpe baseline,")
|
||||||
|
print(" scale-invariante: l'overlay vale SOLO se ShF_overlay > ShF_base a pari DD)")
|
||||||
|
import altlib as al
|
||||||
|
verdicts = {}
|
||||||
|
for fam, ov in best.items():
|
||||||
|
print(f"\n --- {fam}: {olabel(ov)} ---")
|
||||||
|
print(f" {'f':>4} | {'overlay: ShF':>12} {'ShH':>6} {'DD':>6} {'worst':>7} | "
|
||||||
|
f"{'de-lever s':>10} {'ShF':>6} {'CAGR':>7} {'worst':>7} | {'dSh a pari DD':>13}")
|
||||||
|
wins = 0
|
||||||
|
for f in F_SWEEP:
|
||||||
|
r = book(f=f, overlay=ov) if f != 1.0 else cells[ov][0]
|
||||||
|
m = mstats(r); mb = mstats(base[f])
|
||||||
|
s = delever_scale(base[f], m["dd"])
|
||||||
|
rd = s * base[f]; md = mstats(rd)
|
||||||
|
dsh = m["sh"] - md["sh"]
|
||||||
|
wins += dsh > 0.05
|
||||||
|
print(f" {f:>4} | {m['sh']:>12.2f} {m['sh_h']:>6.2f} {m['dd']*100:>5.1f}% {m['worst']*100:>+6.1f}% | "
|
||||||
|
f"{s:>10.2f} {md['sh']:>6.2f} {md['cagr']*100:>+6.1f}% {md['worst']*100:>+6.1f}% | {dsh:>+13.2f}")
|
||||||
|
# multi-cut: dSh per finestra annuale disgiunta (f=1.0) — persistenza, non finestra fortunata
|
||||||
|
r1 = cells[ov][0]
|
||||||
|
dcut = {}
|
||||||
|
for y in (2021, 2022, 2023, 2024, 2025):
|
||||||
|
sel = r1.index.year == y if y < 2025 else r1.index >= HOLDOUT
|
||||||
|
selb = base[1.0].index.year == y if y < 2025 else base[1.0].index >= HOLDOUT
|
||||||
|
dcut[y] = m_weekly(r1[sel])["sh"] - m_weekly(base[1.0][selb])["sh"]
|
||||||
|
npos = sum(v > 0 for y, v in dcut.items() if y < 2025)
|
||||||
|
print(" multi-cut dSh: " + " ".join(f"{y}:{v:+.2f}" for y, v in dcut.items())
|
||||||
|
+ f" ({npos}/4 finestre IS positive)")
|
||||||
|
dsr, null_max = al.deflated_sharpe(cells[ov][1]["sh"], all_full_sh,
|
||||||
|
cells[ov][0].values, dpy=WK)
|
||||||
|
print(f" deflated-Sharpe (14 celle + base): DSR {dsr:.3f} (null-max {null_max:.2f}) "
|
||||||
|
f"{'PASS' if dsr >= 0.95 else 'FAIL'}")
|
||||||
|
aband = [m_weekly(book(f=1.0, overlay=ov, i0=60 + o))["sh"]
|
||||||
|
- m_weekly(book(f=1.0, i0=60 + o))["sh"] for o in range(7)]
|
||||||
|
print(f" banda d'ancora dSh (7 offset): [{min(aband):+.2f}, {max(aband):+.2f}] "
|
||||||
|
f"mediana {np.median(aband):+.2f}")
|
||||||
|
survives = (wins == len(F_SWEEP)) and npos >= 3 and dsr >= 0.95 and np.median(aband) > 0
|
||||||
|
verdicts[fam] = dict(ov=ov, survives=survives, wins_f=int(wins), multicut=dcut,
|
||||||
|
dsr=float(dsr), anchor=(min(aband), max(aband)))
|
||||||
|
print(f" => {'SOPRAVVIVE al null de-levering' if survives else 'NON batte il de-levering'} "
|
||||||
|
f"(pari-DD win {wins}/4 f, multicut {npos}/4, DSR {dsr:.2f})")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- (4) per-anno
|
||||||
|
print("\n (4) PER-ANNO (f=1.0) — 2021 (crash 5/2021) e 2022 (LUNA+FTX) sono il banco di prova")
|
||||||
|
rows = [("VRP01 base", base[1.0])] + [(f"{fam} best", cells[best[fam]][0]) for fam in OVERLAYS]
|
||||||
|
for tag, r in rows:
|
||||||
|
py = per_year(r)
|
||||||
|
print(f" {tag:<14} " + " ".join(f"{y}:{v*100:+5.1f}%" for y, v in sorted(py.items())))
|
||||||
|
|
||||||
|
print("\n VERDETTO: " + "; ".join(
|
||||||
|
f"{fam} {'PASS' if v['survives'] else 'FAIL'}" for fam, v in verdicts.items()))
|
||||||
|
print(" Se tutti FAIL: la protezione DD su VRP01 si compra meglio con la SIZE (de-levering),")
|
||||||
|
print(" coerente con TP01xDVOL 2026-06-26. Regola standing: niente short-vol da modello in deploy.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,444 @@
|
|||||||
|
"""R0703 VRPIMP-NEWGATES — gate d'ingresso NUOVI (veto additivi) su VRP01 canonico, barra alta.
|
||||||
|
|
||||||
|
FILONE 4 (ondata 2026-07-03). TRE raffinamenti del gate sono GIA' falliti (2026-07-01,
|
||||||
|
r0701_vrp_refine: sizing sul gap IV-RV, filtro DVOL-momentum, gate di regime TP01) e qui NON
|
||||||
|
si ritestano. Solo angoli mai provati, sempre come VETO ADDITIVO sopra il gate canonico
|
||||||
|
(vrp>0 AND ivr>=0.30 AND ivr<=0.90 — MAI riottimizzato, lezione 2026-07-01):
|
||||||
|
|
||||||
|
(a) TERM STRUCTURE del DVOL (contango=vendi / backwardation=fuori). PRIMA il dato: la storia
|
||||||
|
per-scadenza NON e' pubblica su Deribit (probe_vol_termstructure 2026-06-29); il logger
|
||||||
|
forward (log_vol_termstructure -> data/raw/vol_term_*.parquet) accumula da fine giugno.
|
||||||
|
Se la storia e' < ~180 giorni il gate NON e' testabile e si DICHIARA. L'unico stand-in
|
||||||
|
ricostruibile dai nostri dati e' TSPROXY = DVOL30 - RV7 (pseudo-slope implied-30g vs
|
||||||
|
realized-7g): dichiaratamente NON una term structure (e' il gate VRP canonico con
|
||||||
|
finestra RV corta) — incluso in griglia solo per completezza, etichettato PROXY.
|
||||||
|
(b) VOL-OF-VOL: std rolling delle variazioni giornaliere del DVOL (k=10/20 gg), messa in
|
||||||
|
percentile espandente CAUSALE; alto = regime vol instabile -> veto (stai fuori).
|
||||||
|
(Diverso dal DVOL-momentum fallito il 2026-07-01: la' era il LIVELLO in salita, qui la
|
||||||
|
VOLATILITA' del DVOL, direzione-agnostica.)
|
||||||
|
(c) ACCELERAZIONE della RV: RV5/RV20 causale > soglia (vol che accelera = precursore di
|
||||||
|
crash) -> veto. Anche in versione percentile espandente.
|
||||||
|
(d) AND pre-dichiarati: canonico + veto(b) + veto(c), 4 combo FISSE scritte nel codice
|
||||||
|
prima di guardare qualunque risultato.
|
||||||
|
|
||||||
|
METODO (vincolante, regole comuni ondata):
|
||||||
|
- selezione soglie SOLO in-sample (pre-2025); hold-out 2025-26 mai usato per scegliere;
|
||||||
|
- multi-cut: uplift vs baseline a 5 date di taglio (>=4/5 richiesti; >=3 = soglia minima
|
||||||
|
del filone) + 4 finestre DISGIUNTE;
|
||||||
|
- deflated-Sharpe sul TOTALE delle celle provate (18, baseline inclusa) — e comunque
|
||||||
|
anti-conservativo (celle correlate + VRP01 stesso viene da ~20 config storiche);
|
||||||
|
- NULL DEL DE-LEVERING esplicito (lezione TP01xDVOL 2026-06-26): lo Sharpe e' scale-invariante,
|
||||||
|
quindi un veto "vale" solo se batte lo Sharpe del baseline A PARI maxDD;
|
||||||
|
- banda f {0.6, 0.8, 1.0, 1.3} (pricing BS flat su DVOL ATM, skew non modellato);
|
||||||
|
- banda d'ancora obbligatoria (regola 2026-07-02): loop settimanale ancorato a i0=60 ->
|
||||||
|
7 offset i0 in {60..66} riportati per baseline e cella scelta.
|
||||||
|
|
||||||
|
Motore: riproduzione ESATTA di options_vrp_v2.vrp_spread_weekly (bridge-check a diff=0 nel
|
||||||
|
main) + veto parametrici. Fee: 12.5% del premio netto (convenzione VRP01). Capitale = strike
|
||||||
|
corto (cash-secured). Regola standing: niente short-vol da modello in deploy — esito massimo
|
||||||
|
= LEAD/conoscenza. Aspettativa dichiarata: FALLIRE (3 precedenti su 3); la mappa dei
|
||||||
|
fallimenti E' il deliverable.
|
||||||
|
|
||||||
|
uv run python scripts/research/r0703_vrpimp_newgates.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt"))
|
||||||
|
from collections import Counter
|
||||||
|
from functools import lru_cache
|
||||||
|
import numpy as np, pandas as pd
|
||||||
|
from scripts.research.options_vrp_lab import bs_put, strike_from_delta, load_series, m_weekly, per_year
|
||||||
|
from scripts.research.options_vrp_v2 import vrp_spread_weekly
|
||||||
|
from altlib import deflated_sharpe
|
||||||
|
|
||||||
|
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
|
||||||
|
WK = 365.25 / 7.0
|
||||||
|
TENOR = 7
|
||||||
|
FEE_FRAC = 0.125 # convenzione VRP01: 12.5% del premio netto round-trip
|
||||||
|
F_SWEEP = (0.6, 0.8, 1.0, 1.3)
|
||||||
|
CUTS = [pd.Timestamp(c, tz="UTC") for c in
|
||||||
|
("2023-01-01", "2023-07-01", "2024-01-01", "2024-07-01", "2025-01-01")]
|
||||||
|
WINDOWS = [("2021-2022", None, "2023-01-01"), ("2023", "2023-01-01", "2024-01-01"),
|
||||||
|
("2024", "2024-01-01", "2025-01-01"), ("2025+", "2025-01-01", None)]
|
||||||
|
MIN_IS_ACTIVE = 0.20 # baseline ~41% attivo
|
||||||
|
SCRATCH = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/e00896d3-d4bb-4f2a-b471-55a1d88a12ba/scratchpad")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ features causali
|
||||||
|
def _expanding_pctl(x: np.ndarray, min_hist: int = 60) -> np.ndarray:
|
||||||
|
"""pctl[i] = quota della storia PASSATA valida x[:i] sotto x[i] (espandente, causale)."""
|
||||||
|
n = len(x); out = np.full(n, np.nan)
|
||||||
|
for i in range(n):
|
||||||
|
if np.isnan(x[i]):
|
||||||
|
continue
|
||||||
|
h = x[:i]; h = h[~np.isnan(h)]
|
||||||
|
if len(h) >= min_hist:
|
||||||
|
out[i] = float((h < x[i]).mean())
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def prep(asset: str):
|
||||||
|
"""Tutte le feature usano SOLO dati <= close[i] (sell-date). lr[k]=log(px[k+1]/px[k]) =>
|
||||||
|
lr[i-w:i] sono i w rendimenti che finiscono a close[i] (stessa convenzione _rv30 di VRP01)."""
|
||||||
|
J = load_series(asset)
|
||||||
|
px = J["px"].values.astype(float)
|
||||||
|
dv = J["dvol"].values.astype(float) / 100.0
|
||||||
|
idx = J.index
|
||||||
|
n = len(px)
|
||||||
|
lr = np.diff(np.log(px))
|
||||||
|
|
||||||
|
def _rv(i, w):
|
||||||
|
if i < w + 1:
|
||||||
|
return np.nan
|
||||||
|
return float(np.std(lr[i - w:i]) * np.sqrt(365.25))
|
||||||
|
|
||||||
|
rv5 = np.array([_rv(i, 5) for i in range(n)])
|
||||||
|
rv7 = np.array([_rv(i, 7) for i in range(n)])
|
||||||
|
rv20 = np.array([_rv(i, 20) for i in range(n)])
|
||||||
|
rv30 = np.array([_rv(i, 30) for i in range(n)])
|
||||||
|
ivr = np.full(n, np.nan)
|
||||||
|
for i in range(60, n):
|
||||||
|
ivr[i] = float((dv[:i] < dv[i]).mean())
|
||||||
|
# (b) vol-of-vol: std delle ultime k variazioni giornaliere del DVOL (ultima = dv[i]-dv[i-1])
|
||||||
|
ddv = np.diff(dv)
|
||||||
|
def _vv(i, k):
|
||||||
|
if i < k + 1:
|
||||||
|
return np.nan
|
||||||
|
return float(np.std(ddv[i - k:i]))
|
||||||
|
vv = {k: np.array([_vv(i, k) for i in range(n)]) for k in (10, 20)}
|
||||||
|
vvp = {k: _expanding_pctl(vv[k]) for k in (10, 20)}
|
||||||
|
# (c) accelerazione RV
|
||||||
|
with np.errstate(invalid="ignore", divide="ignore"):
|
||||||
|
rvacc = rv5 / rv20
|
||||||
|
rap = _expanding_pctl(rvacc)
|
||||||
|
# (a-proxy) pseudo-slope: DVOL30 - RV7 (PROXY, non term structure)
|
||||||
|
tsproxy = dv - rv7
|
||||||
|
tsp = _expanding_pctl(tsproxy)
|
||||||
|
return dict(px=px, dv=dv, idx=idx, n=n, rv30=rv30, ivr=ivr,
|
||||||
|
vvp=vvp, rvacc=rvacc, rap=rap, tsproxy=tsproxy, tsp=tsp)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ motore settimanale
|
||||||
|
def _veto_hit(P, i, veto) -> bool:
|
||||||
|
"""True se il veto scatta al giorno i (NaN = veto non applicabile -> non scatta)."""
|
||||||
|
kind = veto[0]
|
||||||
|
if kind == "vv": # ("vv", k, pctl_thr)
|
||||||
|
v = P["vvp"][veto[1]][i]
|
||||||
|
return (not np.isnan(v)) and v > veto[2]
|
||||||
|
if kind == "ra": # ("ra", ratio_thr)
|
||||||
|
v = P["rvacc"][i]
|
||||||
|
return (not np.isnan(v)) and v > veto[1]
|
||||||
|
if kind == "rap": # ("rap", pctl_thr)
|
||||||
|
v = P["rap"][i]
|
||||||
|
return (not np.isnan(v)) and v > veto[1]
|
||||||
|
if kind == "ts_bin": # backwardation-proxy: RV7 sopra DVOL30
|
||||||
|
v = P["tsproxy"][i]
|
||||||
|
return (not np.isnan(v)) and v <= 0.0
|
||||||
|
if kind == "ts_pctl": # ("ts_pctl", thr): slope-proxy nel fondo percentile
|
||||||
|
v = P["tsp"][i]
|
||||||
|
return (not np.isnan(v)) and v < veto[1]
|
||||||
|
raise ValueError(kind)
|
||||||
|
|
||||||
|
|
||||||
|
def vrp_weekly(asset: str, vetoes=(), f: float = 1.0, i0: int = 60,
|
||||||
|
collect: dict | None = None) -> tuple[pd.Series, Counter]:
|
||||||
|
"""VRP01 canonico (riproduce vrp_spread_weekly gated ESATTO con vetoes=()) + veto additivi.
|
||||||
|
I veto sono valutati DOPO i gate canonici -> il counter conta l'intervento MARGINALE
|
||||||
|
(settimane altrimenti tradate). Causale: gate/strike/premio a sell-date, payoff a scadenza."""
|
||||||
|
P = prep(asset)
|
||||||
|
px, dv, idx, n = P["px"], P["dv"], P["idx"], P["n"]
|
||||||
|
T = TENOR / 365.25
|
||||||
|
rets = {}; st = Counter()
|
||||||
|
i = i0
|
||||||
|
while i + TENOR < n:
|
||||||
|
st["weeks"] += 1
|
||||||
|
S0 = px[i]; sig = dv[i]
|
||||||
|
# --- gate canonici (identici a options_vrp_v2, NON riottimizzati) ---
|
||||||
|
rv = P["rv30"][i]; ivr = P["ivr"][i]
|
||||||
|
skip = ((not np.isnan(rv) and (sig - rv) <= 0)
|
||||||
|
or (not np.isnan(ivr) and (ivr < 0.30 or ivr > 0.90)))
|
||||||
|
if skip:
|
||||||
|
st["blk_canon"] += 1
|
||||||
|
rets[idx[i + TENOR]] = 0.0
|
||||||
|
i += TENOR
|
||||||
|
continue
|
||||||
|
# --- veto NUOVI (marginali) ---
|
||||||
|
hit = None
|
||||||
|
for v in vetoes:
|
||||||
|
if _veto_hit(P, i, v):
|
||||||
|
hit = v[0]
|
||||||
|
break
|
||||||
|
if hit is not None:
|
||||||
|
st[f"blk_{hit}"] += 1
|
||||||
|
if collect is not None:
|
||||||
|
collect.setdefault("vetoed_i", []).append((asset, i))
|
||||||
|
rets[idx[i + TENOR]] = 0.0
|
||||||
|
i += TENOR
|
||||||
|
continue
|
||||||
|
st["traded"] += 1
|
||||||
|
# --- struttura canonica: put credit spread -0.28/-0.10, capitale = strike corto ---
|
||||||
|
Ks = strike_from_delta(S0, T, sig, -0.28)
|
||||||
|
Kl = strike_from_delta(S0, T, sig, -0.10)
|
||||||
|
net_prem = (bs_put(S0, Ks, T, sig) - bs_put(S0, Kl, T, sig)) * f
|
||||||
|
S1 = px[i + TENOR]
|
||||||
|
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1)
|
||||||
|
pnl = net_prem - payoff - FEE_FRAC * abs(net_prem)
|
||||||
|
rets[idx[i + TENOR]] = pnl / Ks
|
||||||
|
i += TENOR
|
||||||
|
return pd.Series(rets), st
|
||||||
|
|
||||||
|
|
||||||
|
def book(vetoes=(), f: float = 1.0, i0: int = 60) -> tuple[pd.Series, Counter]:
|
||||||
|
rB, sB = vrp_weekly("BTC", vetoes=vetoes, f=f, i0=i0)
|
||||||
|
rE, sE = vrp_weekly("ETH", vetoes=vetoes, f=f, i0=i0)
|
||||||
|
b = pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1).sort_index()
|
||||||
|
return b, sB + sE
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ metriche / null
|
||||||
|
def sh_wk(r: pd.Series) -> float:
|
||||||
|
r = r.dropna()
|
||||||
|
if len(r) < 8 or r.std() == 0:
|
||||||
|
return float("nan")
|
||||||
|
return float(r.mean() / r.std() * np.sqrt(WK))
|
||||||
|
|
||||||
|
|
||||||
|
def mstats(r: pd.Series) -> dict:
|
||||||
|
full = m_weekly(r)
|
||||||
|
return dict(sh=full["sh"], dd=full["dd"], cagr=full["cagr"],
|
||||||
|
sh_is=sh_wk(r[r.index < HOLDOUT]), sh_h=sh_wk(r[r.index >= HOLDOUT]),
|
||||||
|
worst=float(r.min()) if len(r) else 0.0, active=float((r != 0).mean()))
|
||||||
|
|
||||||
|
|
||||||
|
def maxdd(r: pd.Series) -> float:
|
||||||
|
eq = np.cumprod(1 + r.values); pk = np.maximum.accumulate(eq)
|
||||||
|
return float(np.max((pk - eq) / pk)) if len(r) else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def delever_scale(r_base: pd.Series, dd_target: float, hi: float = 2.0) -> float:
|
||||||
|
"""Scaling s della size del baseline con maxDD == dd_target (bisezione, Sharpe invariante)."""
|
||||||
|
lo = 0.0
|
||||||
|
for _ in range(80):
|
||||||
|
mid = (lo + hi) / 2
|
||||||
|
if maxdd(mid * r_base) > dd_target:
|
||||||
|
hi = mid
|
||||||
|
else:
|
||||||
|
lo = mid
|
||||||
|
return (lo + hi) / 2
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ griglia (A PRIORI)
|
||||||
|
def build_grid():
|
||||||
|
cells = [("BASELINE canonico", ())]
|
||||||
|
for k in (10, 20):
|
||||||
|
for p in (0.70, 0.80, 0.90):
|
||||||
|
cells.append((f"VV k={k} p>{p:.2f}", (("vv", k, p),)))
|
||||||
|
for thr in (1.0, 1.25, 1.5):
|
||||||
|
cells.append((f"RA rv5/rv20>{thr:.2f}", (("ra", thr),)))
|
||||||
|
for p in (0.80, 0.90):
|
||||||
|
cells.append((f"RAP pctl>{p:.2f}", (("rap", p),)))
|
||||||
|
cells.append(("TSPROXY bin (rv7>dvol) [PROXY]", (("ts_bin",),)))
|
||||||
|
cells.append(("TSPROXY pctl<0.20 [PROXY]", (("ts_pctl", 0.20),)))
|
||||||
|
# combo AND pre-dichiarate (scritte prima di guardare i risultati)
|
||||||
|
cells.append(("AND VV(20,.80)+RA(1.25)", (("vv", 20, 0.80), ("ra", 1.25))))
|
||||||
|
cells.append(("AND VV(20,.80)+RAP(.80)", (("vv", 20, 0.80), ("rap", 0.80))))
|
||||||
|
cells.append(("AND VV(10,.80)+RA(1.25)", (("vv", 10, 0.80), ("ra", 1.25))))
|
||||||
|
cells.append(("AND VV(20,.90)+RA(1.50)", (("vv", 20, 0.90), ("ra", 1.50))))
|
||||||
|
return cells
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ main
|
||||||
|
def main():
|
||||||
|
print("=" * 112)
|
||||||
|
print(" R0703 VRPIMP-NEWGATES — veto d'ingresso nuovi su VRP01 (term-structure / vol-of-vol / RV-accel / AND)")
|
||||||
|
print(" Barra: selezione IS-only + multi-cut >=4/5 + DSR>=0.95 + null de-levering + banda f + banda d'ancora")
|
||||||
|
print("=" * 112)
|
||||||
|
|
||||||
|
# ---------------- (0) angolo (a): fattibilita' del dato term-structure ----------------
|
||||||
|
print("\n (0) TERM STRUCTURE DVOL — verifica del dato PRIMA del test")
|
||||||
|
n_days = {}
|
||||||
|
for a in ("btc", "eth"):
|
||||||
|
fp = PROJECT_ROOT / "data" / "raw" / f"vol_term_{a}.parquet"
|
||||||
|
if fp.exists():
|
||||||
|
df = pd.read_parquet(fp)
|
||||||
|
n_days[a] = len(df)
|
||||||
|
print(f" vol_term_{a}: {len(df)} righe [{df['date'].min().date()} -> {df['date'].max().date()}] "
|
||||||
|
f"slope7-180 medio {df['slope_7_180'].mean():+.1f}pp")
|
||||||
|
else:
|
||||||
|
n_days[a] = 0
|
||||||
|
print(f" vol_term_{a}: NESSUN FILE")
|
||||||
|
n_ts = min(n_days.values()) if n_days else 0
|
||||||
|
print(f" -> storia term-structure = {n_ts} GIORNI (logger forward dal 2026-06-26; serve >=~180g +")
|
||||||
|
print(" certificazione). La storia per-scadenza NON e' pubblica (probe 2026-06-29) e NON e'")
|
||||||
|
print(" ricostruibile dai fetch. VERDETTO (a): NON TESTABILE OGGI — rivalutare a ~6-12 mesi di log.")
|
||||||
|
print(" In griglia resta SOLO il proxy TSPROXY = DVOL30-RV7, dichiarato NON-term-structure")
|
||||||
|
print(" (e' il gate VRP canonico a finestra RV corta).")
|
||||||
|
|
||||||
|
# ---------------- (1) baseline + bridge ----------------
|
||||||
|
base, st_base = book()
|
||||||
|
ref = pd.concat({"B": vrp_spread_weekly("BTC", defined_risk=True, f=1.0, gate_vrp=True,
|
||||||
|
gate_ivr=0.30, crash_skip=0.90),
|
||||||
|
"E": vrp_spread_weekly("ETH", defined_risk=True, f=1.0, gate_vrp=True,
|
||||||
|
gate_ivr=0.30, crash_skip=0.90)},
|
||||||
|
axis=1, join="inner").mean(axis=1).sort_index()
|
||||||
|
al_ = base.align(ref, join="inner")
|
||||||
|
bridge = float((al_[0] - al_[1]).abs().max())
|
||||||
|
mb = mstats(base)
|
||||||
|
print(f"\n (1) BASELINE VRP01 riprodotto: ShF {mb['sh']:.2f} ShH {mb['sh_h']:.2f} Sh-IS {mb['sh_is']:.2f} "
|
||||||
|
f"CAGR {mb['cagr']*100:+.1f}% DD {mb['dd']*100:.1f}% worst {mb['worst']*100:+.1f}% attivo {mb['active']*100:.0f}%")
|
||||||
|
print(f" bridge vs options_vrp_v2.vrp_spread_weekly: max|diff| = {bridge:.2e} "
|
||||||
|
f"({'OK motore ESATTO' if bridge < 1e-12 else '⚠️ DIVERGE — non fidarsi dei numeri'})")
|
||||||
|
|
||||||
|
# ---------------- (2) diagnostica: i veto rimuovono settimane cattive? (IS only) ----------------
|
||||||
|
print("\n (2) DIAGNOSTICA IN-SAMPLE (pre-2025): pnl medio del BASELINE nelle settimane che il veto")
|
||||||
|
print(" avrebbe bloccato vs tenuto (per-asset, solo settimane canonicamente tradate):")
|
||||||
|
probes = [("VV k=10 p>.80", ("vv", 10, 0.80)), ("VV k=20 p>.80", ("vv", 20, 0.80)),
|
||||||
|
("RA >1.25", ("ra", 1.25)), ("RAP p>.80", ("rap", 0.80)),
|
||||||
|
("TSPROXY bin", ("ts_bin",)), ("TSPROXY p<.20", ("ts_pctl", 0.20))]
|
||||||
|
for label, veto in probes:
|
||||||
|
cut_v, cut_k = [], []
|
||||||
|
for a in ("BTC", "ETH"):
|
||||||
|
r0, _ = vrp_weekly(a) # baseline per-asset
|
||||||
|
P = prep(a)
|
||||||
|
pos = {ts: k for k, ts in enumerate(P["idx"])}
|
||||||
|
for ts, v in r0.items():
|
||||||
|
if v == 0.0:
|
||||||
|
continue
|
||||||
|
i_sell = pos[ts] - TENOR
|
||||||
|
if P["idx"][i_sell] >= HOLDOUT:
|
||||||
|
continue
|
||||||
|
(cut_v if _veto_hit(P, i_sell, veto) else cut_k).append(v)
|
||||||
|
mv = np.mean(cut_v) * 100 if cut_v else float("nan")
|
||||||
|
mk = np.mean(cut_k) * 100 if cut_k else float("nan")
|
||||||
|
print(f" {label:<16} bloccate n={len(cut_v):>3} pnl medio {mv:>+6.2f}% | "
|
||||||
|
f"tenute n={len(cut_k):>3} pnl medio {mk:>+6.2f}% "
|
||||||
|
f"({'veto sensato IS' if cut_v and mv < mk else 'veto NON discrimina IS'})")
|
||||||
|
|
||||||
|
# ---------------- (3) griglia 18 celle ----------------
|
||||||
|
cells = build_grid()
|
||||||
|
print(f"\n (3) GRIGLIA {len(cells)} celle (tutte nel DSR) — selezione SOLO su Sh-IS (pre-2025):")
|
||||||
|
print(f" {'cella':<32}{'Sh-IS':>7}{'ShF':>7}{'ShH':>7}{'DD':>7}{'worst':>8}{'att.':>6}{'blk-marg':>9}")
|
||||||
|
results = {}
|
||||||
|
for name, vetoes in cells:
|
||||||
|
b, st = book(vetoes=vetoes)
|
||||||
|
m = mstats(b)
|
||||||
|
blk = sum(v for k, v in st.items() if k.startswith("blk_") and k != "blk_canon")
|
||||||
|
results[name] = dict(name=name, vetoes=vetoes, b=b, st=st, blk=blk, **m)
|
||||||
|
print(f" {name:<32}{m['sh_is']:>7.2f}{m['sh']:>7.2f}{m['sh_h']:>7.2f}"
|
||||||
|
f"{m['dd']*100:>6.1f}%{m['worst']*100:>+7.1f}%{m['active']*100:>5.0f}%{blk:>9}")
|
||||||
|
pd.DataFrame([{k: v for k, v in r.items() if k not in ("b", "st", "vetoes")}
|
||||||
|
for r in results.values()]).to_csv(SCRATCH / "r0703_newgates_grid.csv", index=False)
|
||||||
|
|
||||||
|
bl = results["BASELINE canonico"]
|
||||||
|
ranked = sorted((r for r in results.values() if r["active"] >= MIN_IS_ACTIVE
|
||||||
|
and not np.isnan(r["sh_is"])), key=lambda r: r["sh_is"], reverse=True)
|
||||||
|
cand = ranked[0]
|
||||||
|
is_baseline_best = cand["name"] == bl["name"]
|
||||||
|
n_beat_hold = sum(1 for r in results.values() if r["sh_h"] > bl["sh_h"])
|
||||||
|
print(f"\n -> cella scelta IN-SAMPLE: [{cand['name']}] Sh-IS {cand['sh_is']:.2f} "
|
||||||
|
f"(baseline {bl['sh_is']:.2f}, Δ {cand['sh_is'] - bl['sh_is']:+.2f})")
|
||||||
|
print(f" [onesta'] celle che battono l'HOLD-OUT del baseline: {n_beat_hold}/{len(results)} — "
|
||||||
|
f"NON selezionabili (selection-on-holdout, gate 2026-06-29).")
|
||||||
|
|
||||||
|
# ---------------- (4) multi-cut + finestre disgiunte + DSR ----------------
|
||||||
|
print("\n (4) MULTI-CUT (Sharpe dal taglio a fine storia; uplift = cand - baseline):")
|
||||||
|
pos_cuts = 0
|
||||||
|
for c in CUTS:
|
||||||
|
sc = sh_wk(cand["b"][cand["b"].index >= c]); sb = sh_wk(bl["b"][bl["b"].index >= c])
|
||||||
|
u = sc - sb
|
||||||
|
pos_cuts += int(u > 0)
|
||||||
|
print(f" cut {c.date()}: cand {sc:>5.2f} base {sb:>5.2f} uplift {u:>+5.2f}")
|
||||||
|
print(f" -> uplift positivo in {pos_cuts}/{len(CUTS)} tagli (barra alta >=4/5; soglia minima filone >=3)")
|
||||||
|
print(" FINESTRE DISGIUNTE:")
|
||||||
|
pos_win = 0
|
||||||
|
for wname, lo, hi in WINDOWS:
|
||||||
|
m_ = pd.Series(True, index=cand["b"].index)
|
||||||
|
if lo:
|
||||||
|
m_ &= cand["b"].index >= pd.Timestamp(lo, tz="UTC")
|
||||||
|
if hi:
|
||||||
|
m_ &= cand["b"].index < pd.Timestamp(hi, tz="UTC")
|
||||||
|
sc, sb = sh_wk(cand["b"][m_]), sh_wk(bl["b"][m_])
|
||||||
|
u = sc - sb
|
||||||
|
pos_win += int(u > 0)
|
||||||
|
print(f" {wname:<10} cand {sc:>5.2f} base {sb:>5.2f} uplift {u:>+5.2f}")
|
||||||
|
print(f" -> uplift positivo in {pos_win}/{len(WINDOWS)} finestre disgiunte")
|
||||||
|
|
||||||
|
all_sh = [r["sh"] for r in results.values()]
|
||||||
|
dsr_c, null_max = deflated_sharpe(cand["sh"], all_sh, cand["b"].values, dpy=WK)
|
||||||
|
dsr_b, _ = deflated_sharpe(bl["sh"], all_sh, bl["b"].values, dpy=WK)
|
||||||
|
print(f"\n DEFLATED SHARPE (N={len(all_sh)} celle): cand DSR {dsr_c:.3f} (null-max Sh {null_max:.2f}) | "
|
||||||
|
f"baseline DSR {dsr_b:.3f} [PASS >= 0.95; anti-conservativo: celle correlate + ~20 config storiche VRP]")
|
||||||
|
|
||||||
|
# ---------------- (5) NULL DEL DE-LEVERING ----------------
|
||||||
|
print("\n (5) NULL DEL DE-LEVERING (lezione TP01xDVOL): baseline scalato allo STESSO maxDD del candidato")
|
||||||
|
s = delever_scale(bl["b"], cand["dd"])
|
||||||
|
scaled = s * bl["b"]
|
||||||
|
ms = mstats(scaled)
|
||||||
|
print(f" candidato [{cand['name']}]: Sh {cand['sh']:.2f} ShH {cand['sh_h']:.2f} CAGR {cand['cagr']*100:+.1f}% "
|
||||||
|
f"DD {cand['dd']*100:.1f}% worst {cand['worst']*100:+.1f}%")
|
||||||
|
print(f" baseline x{s:.2f} (pari DD): Sh {ms['sh']:.2f} ShH {ms['sh_h']:.2f} CAGR {ms['cagr']*100:+.1f}% "
|
||||||
|
f"DD {ms['dd']*100:.1f}% worst {ms['worst']*100:+.1f}%")
|
||||||
|
survives_delever = (cand["sh"] > ms["sh"]) and (cand["sh_h"] > ms["sh_h"])
|
||||||
|
print(f" -> il veto {'BATTE' if survives_delever else 'NON batte'} il de-levering "
|
||||||
|
f"(serve Sh full E hold > baseline a pari DD; lo Sharpe e' scale-invariante).")
|
||||||
|
|
||||||
|
# ---------------- (6) banda f + banda d'ancora ----------------
|
||||||
|
print("\n (6a) BANDA f (skew non modellato) — uplift cand-baseline per f:")
|
||||||
|
for f in F_SWEEP:
|
||||||
|
bf, _ = book(f=f); cf, _ = book(vetoes=cand["vetoes"], f=f)
|
||||||
|
mbf, mcf = mstats(bf), mstats(cf)
|
||||||
|
print(f" f={f}: base ShF {mbf['sh']:>5.2f}/ShH {mbf['sh_h']:>5.2f} cand ShF {mcf['sh']:>5.2f}/ShH {mcf['sh_h']:>5.2f} "
|
||||||
|
f"uplift full {mcf['sh']-mbf['sh']:>+5.2f} hold {mcf['sh_h']-mbf['sh_h']:>+5.2f}")
|
||||||
|
|
||||||
|
print("\n (6b) BANDA D'ANCORA (7 offset i0 del ciclo settimanale; regola 2026-07-02):")
|
||||||
|
ups_f, ups_h = [], []
|
||||||
|
for ph in range(7):
|
||||||
|
bA, _ = book(i0=60 + ph); cA, _ = book(vetoes=cand["vetoes"], i0=60 + ph)
|
||||||
|
mbA, mcA = mstats(bA), mstats(cA)
|
||||||
|
ups_f.append(mcA["sh"] - mbA["sh"]); ups_h.append(mcA["sh_h"] - mbA["sh_h"])
|
||||||
|
print(f" i0=+{ph}g: base {mbA['sh']:>5.2f}/{mbA['sh_h']:>5.2f} cand {mcA['sh']:>5.2f}/{mcA['sh_h']:>5.2f} "
|
||||||
|
f"uplift {mcA['sh']-mbA['sh']:>+5.2f}/{mcA['sh_h']-mbA['sh_h']:>+5.2f}")
|
||||||
|
print(f" banda uplift FULL [{min(ups_f):+.2f}, {max(ups_f):+.2f}] mediana {np.median(ups_f):+.2f} | "
|
||||||
|
f"HOLD [{min(ups_h):+.2f}, {max(ups_h):+.2f}] mediana {np.median(ups_h):+.2f}")
|
||||||
|
|
||||||
|
# ---------------- (7) per-anno + verdetto ----------------
|
||||||
|
print("\n (7) PER-ANNO (ritorno composto):")
|
||||||
|
pyb, pyc = per_year(bl["b"]), per_year(cand["b"])
|
||||||
|
print(" base: " + " ".join(f"{y}:{v*100:+5.1f}%" for y, v in sorted(pyb.items())))
|
||||||
|
print(" cand: " + " ".join(f"{y}:{v*100:+5.1f}%" for y, v in sorted(pyc.items())))
|
||||||
|
|
||||||
|
print("\n" + "=" * 112)
|
||||||
|
gates = dict(
|
||||||
|
non_e_baseline=not is_baseline_best,
|
||||||
|
batte_IS=cand["sh_is"] > bl["sh_is"],
|
||||||
|
multicut_4su5=pos_cuts >= 4,
|
||||||
|
multicut_min3=pos_cuts >= 3,
|
||||||
|
finestre_disgiunte=pos_win >= 3,
|
||||||
|
hold_migliore=cand["sh_h"] > bl["sh_h"],
|
||||||
|
dsr_pass=bool(dsr_c >= 0.95),
|
||||||
|
batte_delevering=bool(survives_delever),
|
||||||
|
anchor_mediana_pos=float(np.median(ups_h)) > 0,
|
||||||
|
)
|
||||||
|
improves = all(gates.values())
|
||||||
|
if is_baseline_best:
|
||||||
|
print(" VERDETTO: NON MIGLIORA — il baseline VRP01 vince gia' la selezione in-sample.")
|
||||||
|
print(" QUARTO fallimento del filone 'raffina il gate': l'alpha resta il binario IV-rank>0.30.")
|
||||||
|
elif improves:
|
||||||
|
print(f" VERDETTO: MIGLIORA — [{cand['name']}] passa TUTTI i gate. Resta sleeve MODELLATO")
|
||||||
|
print(" (DVOL ATM, no skew, f di stress non catturato): MAI deploy, solo aggiornamento del modello.")
|
||||||
|
else:
|
||||||
|
fails = [k for k, v in gates.items() if not v]
|
||||||
|
print(f" VERDETTO: NON MIGLIORA — cella IS-best [{cand['name']}] bocciata su: {', '.join(fails)}.")
|
||||||
|
print(" QUARTO fallimento del filone 'raffina il gate' (dopo sizing/momentum/TP01 del 2026-07-01):")
|
||||||
|
print(" l'alpha del VRP01 resta interamente nel gate binario IV-rank>0.30. Angolo (a) term-structure:")
|
||||||
|
print(f" non testabile ({n_ts}g di storia), il logger forward accumula — rivalutare a ~6-12 mesi.")
|
||||||
|
print(" gates: " + " ".join(f"{k}={'PASS' if v else 'FAIL'}" for k, v in gates.items()))
|
||||||
|
print("=" * 112)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,655 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""r0703_vrpimp_sizing.py — FILONE 3: SIZING ANTI-ROVINA per lo sleeve short-vol defined-risk.
|
||||||
|
|
||||||
|
DOMANDA: ALB-B ha mostrato che il sizing 1->4 di Albimarini porta alla rovina (1998/2002/2020).
|
||||||
|
Qual e' la politica di sizing OTTIMA per uno sleeve put-credit-spread ETH su Deribit a 2.000-5.000$?
|
||||||
|
|
||||||
|
MOTORE (riusato, non riscritto): la struttura VRP01 canonica (put credit spread 7g, short delta
|
||||||
|
-0.28 / long -0.10, gate CANONICO vrp>0 + ivr>=0.30 + crash-skip 0.90 — NON riottimizzato, lezione
|
||||||
|
2026-07-01) prezzata col motore ALB-A (r0702_alb_structure): BS flat su DVOL reale, fee Deribit
|
||||||
|
PER GAMBA 0.03% notional cap 12.5% premio + delivery 0.015% su ITM. ETH-only: a 2-5k solo ETH e'
|
||||||
|
granulare (BTC min 0.1 = margine ~$470+, diario r0702_capital_scaling). Rendimenti PER-TRADE su
|
||||||
|
MARGINE (= max loss defined-risk, width - credito): e' il capitale davvero a rischio, e rende la
|
||||||
|
rovina ben definita (perdita massima = 100% del margine impegnato + fee).
|
||||||
|
|
||||||
|
POLITICHE TESTATE (il sizing NON cambia l'edge: cambia CAGR/DD/P(rovina) — lo Sharpe del flusso
|
||||||
|
sottostante e' invariante di scala, quindi qui NON si caccia Sharpe, si mappa la frontiera):
|
||||||
|
(a) frazione fissa del capitale q in {5,10,12,15,20,25}% (margine impegnato/equity per settimana)
|
||||||
|
(b) vol-scaled: q ∝ 1/DVOL (riferimento = mediana ESPANDENTE causale della DVOL)
|
||||||
|
(c) anti-streak: q ridotta dopo N vittorie consecutive (l'OPPOSTO di Albimarini)
|
||||||
|
(d) Kelly frazionario con stima ONESTA delle code: distribuzione empirica per-trade IS (pre-2025)
|
||||||
|
POOLED sulla banda skew f in {0.6,0.8,1.0,1.3} + coda sintetica (full-loss -102% con probabilita'
|
||||||
|
extra = P(move settimanale <= strike long | storia ETH 2019-26 CERTIFICATA) - freq empirica)
|
||||||
|
(e) [riferimento negativo] Albimarini 1->4: size crescente con lo streak di vittorie
|
||||||
|
|
||||||
|
METRICHE: CAGR, maxDD, worst-week, P(rovina -50% / -80% | 5 anni, bootstrap a BLOCCHI L=13 settimane
|
||||||
|
che preserva il clustering di regime del gate), a C=2.000 e 5.000$ con granularita' REALE degli
|
||||||
|
ordini (size INTERE di spread min 1 ETH/gamba; margine/spread dal dato della settimana stessa —
|
||||||
|
la nota task "0.1 ETH" e' incoerente coi numeri citati $66-76 = spread da 1 ETH di r0702_capital_
|
||||||
|
scaling sez.5; la sensitivity 0.1 ETH e' riportata a parte). Confronto esplicito col 12% del book
|
||||||
|
(VRP01 @12% peso = margine settimanale ~12% del conto, stessa convenzione del conteggio spread del
|
||||||
|
diario capital-scaling).
|
||||||
|
|
||||||
|
REGOLE ONORATE: hold-out 2025-26 MAI usato per selezionare (selezione overlay su MAR in-sample);
|
||||||
|
NULL DEL DE-LEVERING esplicito (ogni claim di riduzione DD degli overlay b/c/d confrontato con la
|
||||||
|
frazione fissa che raggiunge lo stesso DD); banda f {0.6,0.8,1.0,1.3}; banda d'ancora sulle 7 fasi
|
||||||
|
della cadenza settimanale; niente '7D'-origin (qui la cadenza e' a passi di indice, non resample).
|
||||||
|
REGOLA STANDING INVARIATA: niente short-vol da modello in deploy — esito = conoscenza/frontiera.
|
||||||
|
|
||||||
|
uv run python scripts/research/r0703_vrpimp_sizing.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path("/opt/docker/PythagorasGoal")
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
import altlib as al # noqa: E402 (harness di progetto; usato per storia ETH certificata + DSR)
|
||||||
|
from scripts.research.options_vrp_lab import bs_put, strike_from_delta, load_series # noqa: E402
|
||||||
|
from scripts.research.options_vrp_v2 import _ivrank, _rv30 # noqa: E402
|
||||||
|
|
||||||
|
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
|
||||||
|
WK = 365.25 / 7.0
|
||||||
|
TENOR = 7
|
||||||
|
F_SWEEP = (0.6, 0.8, 1.0, 1.3)
|
||||||
|
CAPITALS = (2000.0, 5000.0)
|
||||||
|
MIN_ETH = 1.0 # min size Deribit ETH per gamba (r0702_capital_scaling sez.5)
|
||||||
|
Q_CAP = 0.95 # mai impegnare piu' del 95% dell'equity (loss max ~102% del margine)
|
||||||
|
FULL_LOSS = -1.02 # full-loss defined-risk incl. fee (empirico: worst -1.018)
|
||||||
|
RUIN_LVL = (0.50, 0.80) # rovina = equity sotto (1-lvl)*E0 ... definita come perdita >= lvl
|
||||||
|
SEED = 20260703
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# MOTORE PER-TRADE (struttura VRP01, fee ALB-A per gamba, ritorni su MARGINE)
|
||||||
|
# ===========================================================================
|
||||||
|
def trade_records(f: float = 1.0, phase: int = 0) -> pd.DataFrame:
|
||||||
|
"""Vendita settimanale ETH put credit spread -0.28/-0.10, gate canonico. Una riga per
|
||||||
|
settimana di cadenza (phase = fase d'ancora 0..6): active, r (ritorno su margine),
|
||||||
|
margin ($ per spread da 1 ETH), sig (DVOL a decisione), kl_dist (distanza strike long)."""
|
||||||
|
J = load_series("ETH")
|
||||||
|
px = J["px"].values
|
||||||
|
dv = J["dvol"].values / 100.0
|
||||||
|
idx = J.index
|
||||||
|
n = len(px)
|
||||||
|
T = TENOR / 365.25
|
||||||
|
rows = []
|
||||||
|
i = 60 + phase
|
||||||
|
while i + TENOR < n:
|
||||||
|
S0 = px[i]
|
||||||
|
sig = dv[i]
|
||||||
|
rv = _rv30(px, i)
|
||||||
|
ivr = _ivrank(dv, i)
|
||||||
|
skip = ((not np.isnan(rv) and (sig - rv) <= 0)
|
||||||
|
or (not np.isnan(ivr) and (ivr < 0.30 or ivr > 0.90)))
|
||||||
|
if skip:
|
||||||
|
rows.append(dict(date=idx[i + TENOR], active=0, r=0.0, margin=0.0,
|
||||||
|
sig=sig, win=0, kl_dist=np.nan))
|
||||||
|
i += TENOR
|
||||||
|
continue
|
||||||
|
Ks = strike_from_delta(S0, T, sig, -0.28)
|
||||||
|
Kl = strike_from_delta(S0, T, sig, -0.10)
|
||||||
|
ps = bs_put(S0, Ks, T, sig) * f
|
||||||
|
pl = bs_put(S0, Kl, T, sig) * f
|
||||||
|
credit = ps - pl
|
||||||
|
width = Ks - Kl
|
||||||
|
margin = width - credit # margine defined-risk Deribit ~ max loss
|
||||||
|
S1 = px[i + TENOR]
|
||||||
|
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1)
|
||||||
|
fee = min(0.0003 * S0, 0.125 * max(ps, 0.0)) + min(0.0003 * S0, 0.125 * max(pl, 0.0))
|
||||||
|
deliv = (min(0.00015 * S1, 0.125 * max(0.0, Ks - S1))
|
||||||
|
+ min(0.00015 * S1, 0.125 * max(0.0, Kl - S1)))
|
||||||
|
pnl = credit - payoff - fee - deliv # $ per spread da 1 ETH
|
||||||
|
rows.append(dict(date=idx[i + TENOR], active=1, r=pnl / margin, margin=margin,
|
||||||
|
sig=sig, win=int(pnl > 0), kl_dist=Kl / S0 - 1.0))
|
||||||
|
i += TENOR
|
||||||
|
return pd.DataFrame(rows)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# POLITICHE DI SIZING — q = frazione di equity impegnata come margine questa settimana
|
||||||
|
# state: sig (DVOL decisione), ref (mediana espandente DVOL), streak (vittorie consecutive)
|
||||||
|
# ===========================================================================
|
||||||
|
def pol_fixed(frac):
|
||||||
|
return lambda st: frac
|
||||||
|
|
||||||
|
|
||||||
|
def pol_volscaled(frac, lo=0.5, hi=2.0):
|
||||||
|
"""q = frac * clip(DVOL_ref/DVOL, lo, hi): meno size quando la vol e' alta. Causale."""
|
||||||
|
return lambda st: frac * float(np.clip(st["ref"] / max(st["sig"], 1e-9), lo, hi))
|
||||||
|
|
||||||
|
|
||||||
|
def pol_antistreak(frac, N, mult):
|
||||||
|
"""Dopo >=N vittorie consecutive riduci a frac*mult (l'opposto di Albimarini)."""
|
||||||
|
return lambda st: frac * (mult if st["streak"] >= N else 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def pol_alb14(frac):
|
||||||
|
"""Riferimento NEGATIVO: Albimarini 1->4, size crescente con le vittorie consecutive."""
|
||||||
|
return lambda st: frac * min(1.0 + st["streak"], 4.0)
|
||||||
|
|
||||||
|
|
||||||
|
def simulate(rec: pd.DataFrame, qfun, E0: float, granular: bool = True,
|
||||||
|
min_eth: float = MIN_ETH, stress_p: float = 0.0, rng=None,
|
||||||
|
q_series=None) -> dict:
|
||||||
|
"""Applica la politica al flusso settimanale. granular=True -> size intere di spread
|
||||||
|
(floor(budget/margine_spread)); False -> frazione continua (lens frontiera).
|
||||||
|
stress_p>0: ogni settimana attiva ha prob extra di full-loss sintetico (coda stress).
|
||||||
|
q_series: array di q per riga (bypassa qfun — usato dal null a piazzamento casuale)."""
|
||||||
|
E = E0
|
||||||
|
eq, rets, dates = [], [], []
|
||||||
|
sig_hist: list[float] = []
|
||||||
|
streak = 0
|
||||||
|
halted = 0
|
||||||
|
for t, row in enumerate(rec.itertuples(index=False)):
|
||||||
|
ref = float(np.median(sig_hist)) if len(sig_hist) >= 20 else row.sig
|
||||||
|
st = dict(sig=row.sig, ref=ref, streak=streak)
|
||||||
|
sig_hist.append(row.sig)
|
||||||
|
r_wk = 0.0
|
||||||
|
if row.active and E > 0:
|
||||||
|
q_raw = q_series[t] if q_series is not None else qfun(st)
|
||||||
|
q = float(np.clip(q_raw, 0.0, Q_CAP))
|
||||||
|
unit = row.margin * min_eth
|
||||||
|
if granular:
|
||||||
|
n_spread = int((q * E) // unit) if unit > 0 else 0
|
||||||
|
committed = n_spread * unit
|
||||||
|
if q > 0 and n_spread == 0:
|
||||||
|
halted += 1
|
||||||
|
else:
|
||||||
|
committed = q * E
|
||||||
|
r_trade = row.r
|
||||||
|
if stress_p > 0.0 and rng is not None and rng.random() < stress_p:
|
||||||
|
r_trade = FULL_LOSS
|
||||||
|
r_wk = committed * r_trade / E if E > 0 else 0.0
|
||||||
|
E = E + committed * r_trade
|
||||||
|
if row.active:
|
||||||
|
streak = streak + 1 if (row.win and row.r == row.r) else 0
|
||||||
|
if not row.win:
|
||||||
|
streak = 0
|
||||||
|
rets.append(r_wk)
|
||||||
|
eq.append(max(E, 0.0))
|
||||||
|
dates.append(row.date)
|
||||||
|
return dict(dates=pd.DatetimeIndex(dates), eq=np.asarray(eq), rets=np.asarray(rets),
|
||||||
|
halted=halted, E_end=E)
|
||||||
|
|
||||||
|
|
||||||
|
def path_metrics(dates, rets) -> dict:
|
||||||
|
r = np.asarray(rets, float)
|
||||||
|
if len(r) < 3:
|
||||||
|
return dict(cagr=0.0, dd=0.0, worst=0.0, sh=0.0)
|
||||||
|
eq = np.cumprod(1.0 + r)
|
||||||
|
pk = np.maximum.accumulate(eq)
|
||||||
|
yrs = len(r) / WK
|
||||||
|
cagr = eq[-1] ** (1.0 / yrs) - 1.0 if eq[-1] > 0 else -1.0
|
||||||
|
sh = float(r.mean() / r.std() * np.sqrt(WK)) if r.std() > 0 else 0.0
|
||||||
|
return dict(cagr=float(cagr), dd=float(np.max((pk - eq) / pk)), worst=float(r.min()), sh=sh)
|
||||||
|
|
||||||
|
|
||||||
|
def full_hold(sim) -> tuple[dict, dict]:
|
||||||
|
m_full = path_metrics(sim["dates"], sim["rets"])
|
||||||
|
mask = sim["dates"] >= HOLDOUT
|
||||||
|
m_hold = path_metrics(sim["dates"][mask], sim["rets"][mask])
|
||||||
|
return m_full, m_hold
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# KELLY con code oneste + bootstrap a blocchi
|
||||||
|
# ===========================================================================
|
||||||
|
def kelly_star(rs: np.ndarray, p_extra: float, tail: float = FULL_LOSS) -> float:
|
||||||
|
"""argmax_q E[log(1+q r)] su mistura: con prob p_extra r=tail, altrimenti empirico."""
|
||||||
|
qs = np.linspace(0.0, Q_CAP, 191)
|
||||||
|
best_q, best_g = 0.0, -np.inf
|
||||||
|
for q in qs:
|
||||||
|
vals = np.log1p(q * rs)
|
||||||
|
g = (1.0 - p_extra) * vals.mean() + p_extra * np.log1p(q * tail)
|
||||||
|
if np.isfinite(g) and g > best_g:
|
||||||
|
best_g, best_q = g, q
|
||||||
|
return float(best_q)
|
||||||
|
|
||||||
|
|
||||||
|
def synthetic_tail_prob(rec: pd.DataFrame) -> tuple[float, float, float]:
|
||||||
|
"""p_extra = P(move 7g ETH <= distanza strike long | storia certificata 2019-26)
|
||||||
|
- freq empirica di full-loss nel flusso gated. Ritorna (p_extra, p_uncond, p_emp)."""
|
||||||
|
d1 = al.get("ETH", "1d")
|
||||||
|
px = d1["close"].values.astype(float)
|
||||||
|
wk_mv = px[TENOR:] / px[:-TENOR] - 1.0 # mosse 7g overlapping, 2019->oggi
|
||||||
|
act = rec[rec.active == 1]
|
||||||
|
kl_med = float(act["kl_dist"].median())
|
||||||
|
p_uncond = float(np.mean(wk_mv <= kl_med))
|
||||||
|
p_emp = float((act["r"] <= -0.9).mean())
|
||||||
|
return max(0.0, p_uncond - p_emp), p_uncond, p_emp
|
||||||
|
|
||||||
|
|
||||||
|
def block_bootstrap_ruin(rec: pd.DataFrame, qfun, C: float, npaths: int = 1000,
|
||||||
|
horizon_w: int = 261, L: int = 13, stress_p: float = 0.0,
|
||||||
|
seed: int = SEED, needs_ref: bool = False) -> dict:
|
||||||
|
"""P(rovina | 5 anni): bootstrap a blocchi circolari di L settimane dal flusso storico
|
||||||
|
(preserva clustering del gate/regime), politica applicata con granularita' REALE a C.
|
||||||
|
needs_ref: calcola la mediana espandente della DVOL solo per le politiche che la usano."""
|
||||||
|
rows = list(rec.itertuples(index=False))
|
||||||
|
n = len(rows)
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
ruin = np.zeros((npaths, len(RUIN_LVL)), bool)
|
||||||
|
dd30 = np.zeros(npaths, bool)
|
||||||
|
cagrs = np.zeros(npaths)
|
||||||
|
for p in range(npaths):
|
||||||
|
starts = rng.integers(0, n, size=horizon_w // L + 1)
|
||||||
|
seq = [rows[(s + k) % n] for s in starts for k in range(L)][:horizon_w]
|
||||||
|
E = C
|
||||||
|
peak = C
|
||||||
|
minE = C
|
||||||
|
sig_hist: list[float] = []
|
||||||
|
streak = 0
|
||||||
|
maxdd = 0.0
|
||||||
|
for row in seq:
|
||||||
|
if needs_ref:
|
||||||
|
ref = float(np.median(sig_hist)) if len(sig_hist) >= 20 else row.sig
|
||||||
|
sig_hist.append(row.sig)
|
||||||
|
else:
|
||||||
|
ref = row.sig
|
||||||
|
if row.active and E > 0:
|
||||||
|
q = float(np.clip(qfun(dict(sig=row.sig, ref=ref, streak=streak)), 0.0, Q_CAP))
|
||||||
|
unit = row.margin * MIN_ETH
|
||||||
|
n_spread = int((q * E) // unit) if unit > 0 else 0
|
||||||
|
r_trade = row.r
|
||||||
|
if stress_p > 0.0 and rng.random() < stress_p:
|
||||||
|
r_trade = FULL_LOSS
|
||||||
|
E = E + n_spread * unit * r_trade
|
||||||
|
streak = streak + 1 if (row.win and r_trade > 0) else 0
|
||||||
|
peak = max(peak, E)
|
||||||
|
minE = min(minE, E)
|
||||||
|
maxdd = max(maxdd, (peak - E) / peak if peak > 0 else 1.0)
|
||||||
|
for j, lvl in enumerate(RUIN_LVL):
|
||||||
|
ruin[p, j] = minE <= C * (1.0 - lvl)
|
||||||
|
dd30[p] = maxdd >= 0.30
|
||||||
|
cagrs[p] = (max(E, 0.0) / C) ** (1.0 / (horizon_w / WK)) - 1.0 if E > 0 else -1.0
|
||||||
|
return dict(p_ruin50=float(ruin[:, 0].mean()), p_ruin80=float(ruin[:, 1].mean()),
|
||||||
|
p_dd30=float(dd30.mean()), cagr_med=float(np.median(cagrs)),
|
||||||
|
cagr_p10=float(np.percentile(cagrs, 10)))
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# REPORT
|
||||||
|
# ===========================================================================
|
||||||
|
def hist_row(label, rec, qfun, C, granular=True):
|
||||||
|
sim = simulate(rec, qfun, C, granular=granular)
|
||||||
|
mf, mh = full_hold(sim)
|
||||||
|
return sim, mf, mh, (f" {label:<34} {mf['cagr']*100:>+6.1f}% {mf['dd']*100:>5.1f}% "
|
||||||
|
f"{mf['worst']*100:>+6.1f}% {mf['sh']:>5.2f} | {mh['cagr']*100:>+6.1f}% "
|
||||||
|
f"{mh['dd']*100:>5.1f}% | halt {sim['halted']:>3}")
|
||||||
|
|
||||||
|
|
||||||
|
HDR = (f" {'politica':<34} {'CAGR-F':>7} {'DD-F':>5} {'worst':>6} {'Sh-F':>5} | "
|
||||||
|
f"{'CAGR-H':>7} {'DD-H':>5} | settimane-0-spread")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 112)
|
||||||
|
print(" R0703 VRPIMP-SIZING — sizing anti-rovina dello sleeve short-vol defined-risk ETH (2-5k, Deribit)")
|
||||||
|
print(" Flusso: put credit spread ETH 7g -0.28/-0.10, gate canonico, motore ALB-A (fee per gamba), 2021-2026.")
|
||||||
|
print(" Rendimenti per-trade su MARGINE (=max loss). Il sizing non cambia lo Sharpe del flusso: mappa la")
|
||||||
|
print(" frontiera CAGR-DD-P(rovina). REGOLA STANDING INVARIATA: niente short-vol da modello in deploy.")
|
||||||
|
print("=" * 112)
|
||||||
|
|
||||||
|
rec = trade_records(f=1.0, phase=0)
|
||||||
|
act = rec[rec.active == 1]
|
||||||
|
is_act = act[act.date < HOLDOUT]
|
||||||
|
print(f"\n flusso f=1.0 ancora-0: {len(rec)} settimane, {len(act)} attive ({len(act)/len(rec)*100:.0f}%), "
|
||||||
|
f"win {act['win'].mean()*100:.0f}%, r-margine medio {act['r'].mean()*100:+.1f}% "
|
||||||
|
f"(mediana {act['r'].median()*100:+.1f}%), full-loss r<=-0.9: {int((act['r']<=-0.9).sum())}")
|
||||||
|
print(f" margine $/spread(1 ETH): mediana ${act['margin'].median():.0f}, ultimo ${act['margin'].iloc[-1]:.0f} "
|
||||||
|
f"(la banda $66-76 del diario = spot/DVOL del run r0702)")
|
||||||
|
|
||||||
|
# streak diagnostics: gli streak di vittorie predicono la prossima perdita?
|
||||||
|
wins = act["win"].values
|
||||||
|
streaks_before = []
|
||||||
|
s = 0
|
||||||
|
for w in wins:
|
||||||
|
streaks_before.append(s)
|
||||||
|
s = s + 1 if w else 0
|
||||||
|
sb = np.asarray(streaks_before)
|
||||||
|
print("\n DIAGNOSTICA ANTI-STREAK — P(perdita | vittorie consecutive precedenti):")
|
||||||
|
base = 1.0 - act["win"].mean()
|
||||||
|
for N in (0, 2, 3, 5):
|
||||||
|
m = sb >= N
|
||||||
|
if m.sum() >= 8:
|
||||||
|
print(f" streak>={N}: P(loss)={1.0-wins[m].mean():.2f} su n={int(m.sum())} (base {base:.2f})")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- (1) frontiera fisso
|
||||||
|
print("\n" + "=" * 112)
|
||||||
|
print(" (1) FRONTIERA DD-CAGR — frazione fissa (IL NULL DEL DE-LEVERING), f=1.0, storico 2021-26")
|
||||||
|
print(" 'cont' = frazione continua (lens); '2k'/'5k' = granularita' REALE (spread interi da 1 ETH)")
|
||||||
|
print("=" * 112)
|
||||||
|
fracs = (0.02, 0.05, 0.08, 0.10, 0.12, 0.15, 0.20, 0.25, 0.35, 0.50)
|
||||||
|
frontier = {} # frac -> (dd_full, cagr_full) continuo
|
||||||
|
print(f" {'q':>5} | {'cont: CAGR-F':>12} {'DD-F':>6} {'Sh-F':>5} {'CAGR-H':>7} | "
|
||||||
|
f"{'2k: CAGR-F':>10} {'DD-F':>6} {'halt':>4} | {'5k: CAGR-F':>10} {'DD-F':>6} {'halt':>4}")
|
||||||
|
for q in fracs:
|
||||||
|
sc = simulate(rec, pol_fixed(q), 2000.0, granular=False)
|
||||||
|
mfc, mhc = full_hold(sc)
|
||||||
|
frontier[q] = (mfc["dd"], mfc["cagr"])
|
||||||
|
s2 = simulate(rec, pol_fixed(q), 2000.0, granular=True)
|
||||||
|
mf2, _ = full_hold(s2)
|
||||||
|
s5 = simulate(rec, pol_fixed(q), 5000.0, granular=True)
|
||||||
|
mf5, _ = full_hold(s5)
|
||||||
|
tag = " <- book 12%" if abs(q - 0.12) < 1e-9 else ""
|
||||||
|
print(f" {q*100:>4.0f}% | {mfc['cagr']*100:>+11.1f}% {mfc['dd']*100:>5.1f}% {mfc['sh']:>5.2f} "
|
||||||
|
f"{mhc['cagr']*100:>+6.1f}% | {mf2['cagr']*100:>+9.1f}% {mf2['dd']*100:>5.1f}% {s2['halted']:>4} | "
|
||||||
|
f"{mf5['cagr']*100:>+9.1f}% {mf5['dd']*100:>5.1f}% {s5['halted']:>4}{tag}")
|
||||||
|
print(" ('halt' = settimane attive in cui il budget q*E non compra NEMMENO 1 spread -> size 0:")
|
||||||
|
print(" a 2k le frazioni <=10% saltano trade nelle ere a margine alto — la granularita' distorce il basso)")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- (2) overlay b/c — selezione IS
|
||||||
|
print("\n" + "=" * 112)
|
||||||
|
print(" (2) OVERLAY (b) VOL-SCALED e (c) ANTI-STREAK — selezione SOLO in-sample (MAR pre-2025, continuo)")
|
||||||
|
print("=" * 112)
|
||||||
|
rec_is = rec[rec.date < HOLDOUT].reset_index(drop=True)
|
||||||
|
|
||||||
|
def is_mar(qfun):
|
||||||
|
sim = simulate(rec_is, qfun, 2000.0, granular=False)
|
||||||
|
m = path_metrics(sim["dates"], sim["rets"])
|
||||||
|
return m["cagr"] / max(m["dd"], 1e-9)
|
||||||
|
|
||||||
|
cells_b = [(fb, lo, hi) for fb in (0.08, 0.12, 0.16) for lo, hi in ((0.5, 1.5), (0.5, 2.0))]
|
||||||
|
best_b = max(cells_b, key=lambda c: is_mar(pol_volscaled(*c)))
|
||||||
|
cells_c = [(0.12, N, m) for N in (2, 3, 5) for m in (0.25, 0.5)]
|
||||||
|
best_c = max(cells_c, key=lambda c: is_mar(pol_antistreak(*c)))
|
||||||
|
print(f" celle esplorate: vol-scaled {len(cells_b)}, anti-streak {len(cells_c)} "
|
||||||
|
f"(+6 frazioni fisse dichiarate a priori, +3 Kelly = {len(cells_b)+len(cells_c)+9} totali)")
|
||||||
|
print(f" best IS vol-scaled: base={best_b[0]:.0%} clip[{best_b[1]},{best_b[2]}] | "
|
||||||
|
f"best IS anti-streak: q=12% N={best_c[1]} mult={best_c[2]}")
|
||||||
|
|
||||||
|
print("\n" + HDR)
|
||||||
|
named = {}
|
||||||
|
for label, qf in (
|
||||||
|
("(a) FISSO 12% (book)", pol_fixed(0.12)),
|
||||||
|
(f"(b) VOL-SCALED {best_b[0]:.0%} clip[{best_b[1]},{best_b[2]}]", pol_volscaled(*best_b)),
|
||||||
|
(f"(c) ANTI-STREAK 12% N={best_c[1]} m={best_c[2]}", pol_antistreak(0.12, best_c[1], best_c[2])),
|
||||||
|
("(e) ALBIMARINI 1->4 (rif. negativo)", pol_alb14(0.12))):
|
||||||
|
sim, mf, mh, line = hist_row(label, rec, qf, 2000.0, granular=False)
|
||||||
|
named[label] = (qf, mf, mh)
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- (3) Kelly onesto
|
||||||
|
print("\n" + "=" * 112)
|
||||||
|
print(" (3) (d) KELLY FRAZIONARIO con code ONESTE (distribuzione IS pooled banda-f + coda sintetica)")
|
||||||
|
print("=" * 112)
|
||||||
|
p_extra, p_unc, p_emp = synthetic_tail_prob(rec)
|
||||||
|
pooled = []
|
||||||
|
for f in F_SWEEP:
|
||||||
|
rf = trade_records(f=f, phase=0)
|
||||||
|
a = rf[(rf.active == 1) & (rf.date < HOLDOUT)]
|
||||||
|
pooled.append(a["r"].values)
|
||||||
|
rs_pool = np.concatenate(pooled)
|
||||||
|
q_star = kelly_star(rs_pool, p_extra)
|
||||||
|
q_naive = kelly_star(is_act["r"].values, 0.0)
|
||||||
|
print(f" coda: P(move7g<=Kl | ETH 2019-26 certificato)={p_unc:.3f}, freq empirica full-loss gated={p_emp:.3f}"
|
||||||
|
f" -> p_extra sintetica={p_extra:.3f} a r={FULL_LOSS}")
|
||||||
|
print(f" Kelly NAIVE (solo empirico IS f=1.0): q* = {q_naive:.1%}")
|
||||||
|
print(f" Kelly ONESTO (pooled f-band + coda): q* = {q_star:.1%}"
|
||||||
|
f" -> il 12% del book = {0.12/q_star:.2f} Kelly")
|
||||||
|
print("\n" + HDR)
|
||||||
|
for lam in (0.25, 0.5, 1.0):
|
||||||
|
q = lam * q_star
|
||||||
|
label = f"(d) KELLY {lam:.2f}x -> q={q:.1%}"
|
||||||
|
sim, mf, mh, line = hist_row(label, rec, pol_fixed(q), 2000.0, granular=False)
|
||||||
|
named[label] = (pol_fixed(q), mf, mh)
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- (4) null del de-levering
|
||||||
|
print("\n" + "=" * 112)
|
||||||
|
print(" (4) NULL DEL DE-LEVERING — ogni overlay vs la frazione FISSA che da' lo stesso maxDD (interp.)")
|
||||||
|
print("=" * 112)
|
||||||
|
fr = sorted(frontier.items())
|
||||||
|
dds = np.array([v[0] for _, v in fr])
|
||||||
|
cags = np.array([v[1] for _, v in fr])
|
||||||
|
qss = np.array([q for q, _ in fr])
|
||||||
|
for label, (qf, mf, mh) in named.items():
|
||||||
|
if label.startswith("(a)") or label.startswith("(e)"):
|
||||||
|
continue
|
||||||
|
q_eq = float(np.interp(mf["dd"], dds, qss))
|
||||||
|
c_eq = float(np.interp(mf["dd"], dds, cags))
|
||||||
|
verdict = "OVERLAY NON VALE (delever null vince/pareggia)" if c_eq >= mf["cagr"] - 0.002 \
|
||||||
|
else "overlay batte il null (verificare persistenza!)"
|
||||||
|
print(f" {label:<38} DD {mf['dd']*100:5.1f}% CAGR {mf['cagr']*100:+6.1f}% | fisso-equivalente "
|
||||||
|
f"q={q_eq*100:4.1f}% CAGR {c_eq*100:+6.1f}% -> {verdict}")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- (4b) stress-test ANTI-STREAK
|
||||||
|
print("\n" + "=" * 112)
|
||||||
|
print(" (4b) STRESS-TEST ANTI-STREAK — regola standing: 3 raffinamenti-gate VRP gia' falliti; un 4°")
|
||||||
|
print(" candidato deve battere il NULL GIUSTO (piazzamento casuale), persistere multi-cut, DSR>=0.95")
|
||||||
|
print("=" * 112)
|
||||||
|
N_as, m_as = best_c[1], best_c[2]
|
||||||
|
# replay della maschera 'settimana ridotta' (streak>=N a decisione)
|
||||||
|
red = np.zeros(len(rec), bool)
|
||||||
|
s = 0
|
||||||
|
for t, row in enumerate(rec.itertuples(index=False)):
|
||||||
|
red[t] = bool(row.active and s >= N_as)
|
||||||
|
if row.active:
|
||||||
|
s = s + 1 if row.win else 0
|
||||||
|
act_mask = rec["active"].values == 1
|
||||||
|
act_idx = np.where(act_mask)[0]
|
||||||
|
k_red = int(red[act_mask].sum())
|
||||||
|
win_arr = rec["win"].values.astype(bool)
|
||||||
|
loss_red = int((~win_arr[act_mask]) [red[act_mask]].sum())
|
||||||
|
n_loss = int((~win_arr[act_mask]).sum())
|
||||||
|
print(f" settimane attive ridotte (streak>={N_as}): {k_red}/{len(act_idx)} ({k_red/len(act_idx)*100:.0f}%) | "
|
||||||
|
f"perdite intercettate a size ridotta: {loss_red}/{n_loss}")
|
||||||
|
|
||||||
|
sim_anti = simulate(rec, pol_antistreak(0.12, N_as, m_as), 2000.0, granular=False)
|
||||||
|
sim_fix = simulate(rec, pol_fixed(0.12), 2000.0, granular=False)
|
||||||
|
m_anti = path_metrics(sim_anti["dates"], sim_anti["rets"])
|
||||||
|
|
||||||
|
# NULL a PIAZZAMENTO CASUALE: stessa sizing a 2 livelli (12% / 12%*mult), stesse k settimane
|
||||||
|
# ridotte, ma scelte A CASO fra le attive (isola il TIMING dello streak dall'esposizione media)
|
||||||
|
rng = np.random.default_rng(SEED + 7)
|
||||||
|
null_cagr, null_dd, null_mar = [], [], []
|
||||||
|
for _ in range(500):
|
||||||
|
choose = rng.choice(act_idx, size=k_red, replace=False)
|
||||||
|
qs = np.full(len(rec), 0.12)
|
||||||
|
qs[choose] = 0.12 * m_as
|
||||||
|
sn = simulate(rec, None, 2000.0, granular=False, q_series=qs)
|
||||||
|
mn = path_metrics(sn["dates"], sn["rets"])
|
||||||
|
null_cagr.append(mn["cagr"])
|
||||||
|
null_dd.append(mn["dd"])
|
||||||
|
null_mar.append(mn["cagr"] / max(mn["dd"], 1e-9))
|
||||||
|
mar_anti = m_anti["cagr"] / max(m_anti["dd"], 1e-9)
|
||||||
|
p_cagr = float(np.mean(np.asarray(null_cagr) < m_anti["cagr"]))
|
||||||
|
p_dd = float(np.mean(np.asarray(null_dd) > m_anti["dd"])) # quota di null con DD PEGGIORE
|
||||||
|
p_mar = float(np.mean(np.asarray(null_mar) < mar_anti))
|
||||||
|
print(f" NULL piazzamento-casuale (500 draw, stessa esposizione media): anti-streak CAGR pctl {p_cagr:.3f}, "
|
||||||
|
f"DD-migliore-del-null {p_dd:.3f}, MAR pctl {p_mar:.3f}")
|
||||||
|
print(f" (null: CAGR med {np.median(null_cagr)*100:+.1f}%, DD med {np.median(null_dd)*100:.1f}% vs "
|
||||||
|
f"anti-streak {m_anti['cagr']*100:+.1f}%/{m_anti['dd']*100:.1f}%)")
|
||||||
|
|
||||||
|
# banda d'ancora dell'EFFETTO (7 fasi): direzione + IL DELEVER-NULL RIFATTO PER OGNI ANCORA
|
||||||
|
# (lezione anchor-luck 2026-07-02: il claim vale solo se regge a OGNI ancora, non alla migliore)
|
||||||
|
print(f"\n banda d'ancora dell'effetto streak (7 fasi, f=1.0) + delever-null PER ANCORA:")
|
||||||
|
n_dir = 0
|
||||||
|
n_null = 0
|
||||||
|
for ph in range(7):
|
||||||
|
rp = trade_records(f=1.0, phase=ph)
|
||||||
|
wa = rp[rp.active == 1]["win"].values.astype(bool)
|
||||||
|
sb2 = []
|
||||||
|
s = 0
|
||||||
|
for w in wa:
|
||||||
|
sb2.append(s)
|
||||||
|
s = s + 1 if w else 0
|
||||||
|
sb2 = np.asarray(sb2)
|
||||||
|
hi = sb2 >= N_as
|
||||||
|
p_hi = 1.0 - wa[hi].mean() if hi.sum() else np.nan
|
||||||
|
p_lo = 1.0 - wa[~hi].mean() if (~hi).sum() else np.nan
|
||||||
|
sa = simulate(rp, pol_antistreak(0.12, N_as, m_as), 2000.0, granular=False)
|
||||||
|
ma = path_metrics(sa["dates"], sa["rets"])
|
||||||
|
# frontiera fissa DI QUESTA ancora -> CAGR del fisso allo stesso DD
|
||||||
|
dds_p, cags_p = [], []
|
||||||
|
for q in fracs:
|
||||||
|
sf_ = simulate(rp, pol_fixed(q), 2000.0, granular=False)
|
||||||
|
mq = path_metrics(sf_["dates"], sf_["rets"])
|
||||||
|
dds_p.append(mq["dd"])
|
||||||
|
cags_p.append(mq["cagr"])
|
||||||
|
c_eq = float(np.interp(ma["dd"], dds_p, cags_p))
|
||||||
|
beats = ma["cagr"] > c_eq + 0.002
|
||||||
|
direz = p_hi > p_lo
|
||||||
|
n_dir += int(direz)
|
||||||
|
n_null += int(beats)
|
||||||
|
print(f" fase {ph}: P(loss|s>={N_as})={p_hi:.2f} vs s<{N_as}={p_lo:.2f} {'OK' if direz else 'NO'} | "
|
||||||
|
f"anti CAGR {ma['cagr']*100:+5.1f}% DD {ma['dd']*100:4.1f}% | fisso stesso-DD CAGR {c_eq*100:+5.1f}% "
|
||||||
|
f"-> {'batte null' if beats else 'NULL VINCE'}")
|
||||||
|
print(f" -> direzione presente in {n_dir}/7 ancore; batte il delever-null in {n_null}/7 ancore")
|
||||||
|
|
||||||
|
# plateau delle celle (ancora 0): l'effetto deve reggere OLTRE la cella selezionata
|
||||||
|
print("\n plateau celle anti-streak (ancora 0, FULL): MAR = CAGR/maxDD vs fisso 12% MAR "
|
||||||
|
f"{path_metrics(sim_fix['dates'], sim_fix['rets'])['cagr'] / max(path_metrics(sim_fix['dates'], sim_fix['rets'])['dd'], 1e-9):.2f}")
|
||||||
|
for (fq, N, m) in cells_c:
|
||||||
|
sa = simulate(rec, pol_antistreak(fq, N, m), 2000.0, granular=False)
|
||||||
|
mm = path_metrics(sa["dates"], sa["rets"])
|
||||||
|
sel = " <- selezionata IS" if (N, m) == (N_as, m_as) else ""
|
||||||
|
print(f" N={N} mult={m}: CAGR {mm['cagr']*100:+5.1f}% DD {mm['dd']*100:5.1f}% "
|
||||||
|
f"MAR {mm['cagr']/max(mm['dd'],1e-9):5.2f}{sel}")
|
||||||
|
|
||||||
|
# multi-cut: uplift Sharpe/MAR post-cut vs fisso 12%
|
||||||
|
print("\n persistenza multi-cut (uplift anti-streak vs fisso 12%, finestra POST-cut):")
|
||||||
|
for cut in ("2022-07-01", "2023-01-01", "2023-07-01", "2024-01-01", "2024-07-01", "2025-01-01"):
|
||||||
|
cts = pd.Timestamp(cut, tz="UTC")
|
||||||
|
msk = sim_anti["dates"] >= cts
|
||||||
|
ma = path_metrics(sim_anti["dates"][msk], sim_anti["rets"][msk])
|
||||||
|
mfx = path_metrics(sim_fix["dates"][msk], sim_fix["rets"][msk])
|
||||||
|
print(f" cut {cut}: dSh {ma['sh']-mfx['sh']:+.2f} dCAGR {(ma['cagr']-mfx['cagr'])*100:+6.1f}pp "
|
||||||
|
f"DD {ma['dd']*100:4.1f}% vs {mfx['dd']*100:4.1f}%")
|
||||||
|
|
||||||
|
# DSR della cella anti-streak (scelta IS) vs tutte le celle di sizing
|
||||||
|
all_sh_is = []
|
||||||
|
for c in cells_b:
|
||||||
|
s_ = simulate(rec_is, pol_volscaled(*c), 2000.0, granular=False)
|
||||||
|
all_sh_is.append(path_metrics(s_["dates"], s_["rets"])["sh"])
|
||||||
|
for c in cells_c:
|
||||||
|
s_ = simulate(rec_is, pol_antistreak(*c), 2000.0, granular=False)
|
||||||
|
all_sh_is.append(path_metrics(s_["dates"], s_["rets"])["sh"])
|
||||||
|
for q in fracs:
|
||||||
|
s_ = simulate(rec_is, pol_fixed(q), 2000.0, granular=False)
|
||||||
|
all_sh_is.append(path_metrics(s_["dates"], s_["rets"])["sh"])
|
||||||
|
sim_anti_is = simulate(rec_is, pol_antistreak(0.12, N_as, m_as), 2000.0, granular=False)
|
||||||
|
m_anti_is = path_metrics(sim_anti_is["dates"], sim_anti_is["rets"])
|
||||||
|
ret_is = pd.Series(sim_anti_is["rets"], index=sim_anti_is["dates"])
|
||||||
|
dsr_as, nm_as = al.deflated_sharpe(m_anti_is["sh"], all_sh_is, ret_is, dpy=WK)
|
||||||
|
print(f"\n DSR anti-streak (Sh-IS {m_anti_is['sh']:.2f} vs {len(all_sh_is)} celle): {dsr_as:.3f} "
|
||||||
|
f"(null-max {nm_as:.2f}) -> {'PASS' if dsr_as >= 0.95 else 'FAIL'} soglia 0.95")
|
||||||
|
|
||||||
|
# banda f dell'anti-streak
|
||||||
|
print(" banda f anti-streak (FULL/HOLD):")
|
||||||
|
for f in F_SWEEP:
|
||||||
|
rf = trade_records(f=f, phase=0)
|
||||||
|
sa = simulate(rf, pol_antistreak(0.12, N_as, m_as), 2000.0, granular=False)
|
||||||
|
mfa, mha = full_hold(sa)
|
||||||
|
print(f" f={f:<4} CAGR-F {mfa['cagr']*100:>+6.1f}% DD-F {mfa['dd']*100:>5.1f}% Sh-F {mfa['sh']:>5.2f} | "
|
||||||
|
f"CAGR-H {mha['cagr']*100:>+6.1f}% DD-H {mha['dd']*100:>5.1f}%")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- (5) P(rovina) bootstrap
|
||||||
|
print("\n" + "=" * 112)
|
||||||
|
print(" (5) P(ROVINA | 5 ANNI) — bootstrap a blocchi (L=13 sett., 1000 path), granularita' REALE")
|
||||||
|
print(" rovina-50 = equity <= 50% del capitale iniziale in un punto qualsiasi; stress = +coda sintetica")
|
||||||
|
print("=" * 112)
|
||||||
|
boot_pols = [
|
||||||
|
("FISSO 5%", pol_fixed(0.05), False), ("FISSO 12% (book)", pol_fixed(0.12), False),
|
||||||
|
("FISSO 25%", pol_fixed(0.25), False),
|
||||||
|
("VOL-SCALED best-IS", pol_volscaled(*best_b), True),
|
||||||
|
("ANTI-STREAK best-IS", pol_antistreak(0.12, best_c[1], best_c[2]), False),
|
||||||
|
(f"KELLY 0.25x ({0.25*q_star:.0%})", pol_fixed(0.25 * q_star), False),
|
||||||
|
(f"KELLY 0.50x ({0.5*q_star:.0%})", pol_fixed(0.5 * q_star), False),
|
||||||
|
(f"KELLY 1.00x ({q_star:.0%})", pol_fixed(q_star), False),
|
||||||
|
("ALBIMARINI 1->4", pol_alb14(0.12), False),
|
||||||
|
]
|
||||||
|
print(f" {'politica':<24} {'C':>5} | {'P(rov50)':>8} {'P(rov80)':>8} {'P(DD>30%)':>9} "
|
||||||
|
f"{'CAGRmed':>8} {'CAGRp10':>8} | {'stress: P(rov50)':>16} {'P(rov80)':>8}")
|
||||||
|
boot_out = {}
|
||||||
|
for label, qf, nref in boot_pols:
|
||||||
|
for C in CAPITALS:
|
||||||
|
b = block_bootstrap_ruin(rec, qf, C, stress_p=0.0, needs_ref=nref)
|
||||||
|
bs = block_bootstrap_ruin(rec, qf, C, stress_p=p_extra, seed=SEED + 1, needs_ref=nref)
|
||||||
|
boot_out[(label, C)] = (b, bs)
|
||||||
|
print(f" {label:<24} {C:>5.0f} | {b['p_ruin50']*100:>7.1f}% {b['p_ruin80']*100:>7.1f}% "
|
||||||
|
f"{b['p_dd30']*100:>8.1f}% {b['cagr_med']*100:>+7.1f}% {b['cagr_p10']*100:>+7.1f}% | "
|
||||||
|
f"{bs['p_ruin50']*100:>15.1f}% {bs['p_ruin80']*100:>7.1f}%")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- (6) banda f + banda ancora
|
||||||
|
print("\n" + "=" * 112)
|
||||||
|
print(" (6) BANDE OBBLIGATORIE — skew f e ancora settimanale (politica FISSO 12%, continuo)")
|
||||||
|
print("=" * 112)
|
||||||
|
print(" banda f (storico FULL/HOLD):")
|
||||||
|
for f in F_SWEEP:
|
||||||
|
rf = trade_records(f=f, phase=0)
|
||||||
|
sim = simulate(rf, pol_fixed(0.12), 2000.0, granular=False)
|
||||||
|
mf, mh = full_hold(sim)
|
||||||
|
print(f" f={f:<4} CAGR-F {mf['cagr']*100:>+6.1f}% DD-F {mf['dd']*100:>5.1f}% worst {mf['worst']*100:>+5.1f}%"
|
||||||
|
f" Sh-F {mf['sh']:>5.2f} | CAGR-H {mh['cagr']*100:>+6.1f}% DD-H {mh['dd']*100:>5.1f}%")
|
||||||
|
print(" banda d'ancora (7 fasi della cadenza settimanale, f=1.0):")
|
||||||
|
anchor = []
|
||||||
|
for ph in range(7):
|
||||||
|
rp = trade_records(f=1.0, phase=ph)
|
||||||
|
sim = simulate(rp, pol_fixed(0.12), 2000.0, granular=False)
|
||||||
|
mf, mh = full_hold(sim)
|
||||||
|
anchor.append((ph, mf["cagr"], mf["dd"], mf["sh"], mh["cagr"]))
|
||||||
|
a = np.array([(x[1], x[2], x[3], x[4]) for x in anchor])
|
||||||
|
print(f" CAGR-F: min {a[:,0].min()*100:+.1f}% med {np.median(a[:,0])*100:+.1f}% max {a[:,0].max()*100:+.1f}% | "
|
||||||
|
f"DD-F: {a[:,1].min()*100:.1f}/{np.median(a[:,1])*100:.1f}/{a[:,1].max()*100:.1f}% | "
|
||||||
|
f"Sh-F: {a[:,2].min():.2f}/{np.median(a[:,2]):.2f}/{a[:,2].max():.2f} | "
|
||||||
|
f"CAGR-H: {a[:,3].min()*100:+.1f}/{np.median(a[:,3])*100:+.1f}/{a[:,3].max()*100:+.1f}%")
|
||||||
|
|
||||||
|
# DSR pro-forma sulle celle (il sizing riscala il flusso: Sharpe quasi invariante per costruzione)
|
||||||
|
all_sh = []
|
||||||
|
for c in cells_b:
|
||||||
|
s = simulate(rec_is, pol_volscaled(*c), 2000.0, granular=False)
|
||||||
|
all_sh.append(path_metrics(s["dates"], s["rets"])["sh"])
|
||||||
|
for c in cells_c:
|
||||||
|
s = simulate(rec_is, pol_antistreak(*c), 2000.0, granular=False)
|
||||||
|
all_sh.append(path_metrics(s["dates"], s["rets"])["sh"])
|
||||||
|
for q in fracs:
|
||||||
|
s = simulate(rec_is, pol_fixed(q), 2000.0, granular=False)
|
||||||
|
all_sh.append(path_metrics(s["dates"], s["rets"])["sh"])
|
||||||
|
sim_b = simulate(rec, pol_volscaled(*best_b), 2000.0, granular=False)
|
||||||
|
daily = pd.Series(sim_b["rets"], index=sim_b["dates"])
|
||||||
|
dsr, null_max = al.deflated_sharpe(path_metrics(sim_b["dates"], sim_b["rets"])["sh"], all_sh, daily, dpy=WK)
|
||||||
|
print(f"\n DSR pro-forma (best overlay vs {len(all_sh)} celle di sizing): {dsr:.3f} (null-max {null_max:.2f})")
|
||||||
|
print(" NB: il sizing RISCALA lo stesso flusso -> gli Sharpe delle celle sono quasi identici per costruzione;")
|
||||||
|
print(" il DSR qui e' dovuto, ma la metrica decisiva del filone e' la frontiera CAGR-DD-P(rovina) vs il null.")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- (7) sensitivity 0.1 ETH
|
||||||
|
print("\n (7) SENSITIVITY GRANULARITA' 0.1 ETH (se il min-size fosse 0.1 ETH/gamba, unit ~$7-15):")
|
||||||
|
for C in CAPITALS:
|
||||||
|
s10 = simulate(rec, pol_fixed(0.12), C, granular=True, min_eth=0.1)
|
||||||
|
s1 = simulate(rec, pol_fixed(0.12), C, granular=True, min_eth=1.0)
|
||||||
|
m10, _ = full_hold(s10)
|
||||||
|
m1, _ = full_hold(s1)
|
||||||
|
print(f" C={C:.0f}: 1.0 ETH CAGR {m1['cagr']*100:+.1f}% DD {m1['dd']*100:.1f}% halt {s1['halted']} | "
|
||||||
|
f"0.1 ETH CAGR {m10['cagr']*100:+.1f}% DD {m10['dd']*100:.1f}% halt {s10['halted']}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 112)
|
||||||
|
print(" SINTESI ONESTA")
|
||||||
|
print(" - Il sizing non crea alpha: sposta il punto sulla frontiera CAGR-DD-P(rovina) del flusso gated.")
|
||||||
|
print(" - VERDETTI: (b) vol-scaled batte il null di ~1pp a singola ancora = rumore (lezione TP01xDVOL)")
|
||||||
|
print(" -> SCARTATO. (c) anti-streak: passa random-null 0.996 / DSR 0.999 / multi-cut / 7-7 direzione,")
|
||||||
|
print(" MA plateau ASSENTE (solo la cella N=2 vince; N=3/5 collassano al MAR del fisso), P(loss|streak)")
|
||||||
|
print(" NON monotona (0.24/0.18/0.15) e delever-null vince in 2/7 ancore: l'intero effetto sono ~6")
|
||||||
|
print(" perdite su 15 trade a streak==2 -> ARTEFATTO DI SELEZIONE, non adottato. La batteria minima per")
|
||||||
|
print(" un sizing-gate e': random-placement null + plateau celle + delever-null PER ANCORA (i primi 3")
|
||||||
|
print(" test da soli l'avrebbero promosso). (d) Kelly frazionario = punti sulla frontiera fissa (il")
|
||||||
|
print(" null vince/pareggia per costruzione): il valore e' la CALIBRAZIONE, q*_onesto=44%.")
|
||||||
|
print(" - Il 12% del book = 0.27 Kelly-onesto ~= quarter-Kelly: gia' nella zona sana anti-rovina")
|
||||||
|
print(" (P(rovina50|5y)~0.0-0.1%, P(DD>30%) 9-18%). Alzarlo a 25% decuplica P(DD>30%) a >92%.")
|
||||||
|
print(" - Albimarini 1->4 e' il controesempio: stesso flusso, stessa edge, P(rovina50|5y)=53-55%.")
|
||||||
|
print(" - Granularita' REALE: a 2k il min-size 1 ETH rende inservibili le frazioni <=10% (halt 13-92")
|
||||||
|
print(" settimane) e costa -3.7pp di CAGR al 12%; a 5k il muro sparisce (coerente con r0702: ~2.6k).")
|
||||||
|
print(" - REGOLA STANDING: niente short-vol da modello in deploy. Output = conoscenza per QUANDO/SE")
|
||||||
|
print(" il f di stress reale arrivera' da cerbero-bite. Nessun file di produzione toccato.")
|
||||||
|
print("=" * 112)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
"""R0703 VRPIMP-SPXGATE — gate di regime (VIX-rank / term-structure) sulla double diagonal SPX.
|
||||||
|
|
||||||
|
FILONE 6 (ondata 2026-07-03). L'esperimento MANCANTE del backtest esterno dell'utente
|
||||||
|
(spx-diagonal-backtest, non su questa macchina): il gate di regime sulla diagonale SPX.
|
||||||
|
Domanda secca: il gate ribalta gli anni di coda come fa IV-rank su crypto (VRP01 hold-out
|
||||||
|
-0.25 -> +0.28), o su equity e' solo de-levering?
|
||||||
|
|
||||||
|
DATI (⚠️ PROVENIENZA):
|
||||||
|
- SPY 1d: data/raw/eq_spy_1d.parquet (1996-2026, gia' in repo dal filone GTAA; research-grade).
|
||||||
|
- VIX (VIXCLS) e VIX-3M (VXVCLS): scaricati da FRED via curl (pubblico, senza token) in
|
||||||
|
scratchpad/vixcls.csv e vxvcls.csv. ⚠️ DATO RESEARCH-GRADE, NON CERTIFICATO dal nostro
|
||||||
|
pipeline (nessun cross-venue, nessun audit per-barra; missing = '.', ffill<=5g). VXV parte
|
||||||
|
dal 2007-12 -> il gate term-structure e' testabile solo dal 2008.
|
||||||
|
|
||||||
|
MOTORE (⚠️ E' UN MODELLO, non una catena di quote):
|
||||||
|
- Double diagonal come il progetto esterno: SHORT STRANGLE a ~6 giorni di calendario
|
||||||
|
(4 trading day sul grid SPY; T di pricing = gap di calendario REALE del trade) + ALI LONG
|
||||||
|
a T+1 giorno, entrambe i lati. Distanze del corto in DELTA {0.01, 0.02, 0.05} ("1-2-5
|
||||||
|
delta" del progetto esterno); ali a meta' del delta corto (WING_FRAC=0.5, scelta a priori).
|
||||||
|
- Pricing Black-Scholes r=0 con SKEW LOG-LINEARE: sigma(K) = VIX + SLOPE*ln(S/K) (put piu'
|
||||||
|
ricche, call piu' povere — smile equity), SLOPE=1.0 dichiarato a priori (≈ +1 vol pt per
|
||||||
|
1% di moneyness down, tipico SPX short-dated). Strike-da-delta risolto a punto fisso con
|
||||||
|
la vol skewata. Il VIX e' IV a 30g usata per tenor 6g: term structure IGNORATA (in stress
|
||||||
|
il front-end esplode -> il modello SOTTOSTIMA il mark avverso, dichiarato).
|
||||||
|
- Ali marcate a exit con BS a 1 giorno residuo alla vol di uscita (vega catturato).
|
||||||
|
- Costi: 5% del premio di ogni gamba (entry 4 gambe + vendita ali residue a exit).
|
||||||
|
Modello commissioni+slippage SPX; gli spread reali deep-OTM sono spesso PEGGIORI (dich.).
|
||||||
|
- Banda f {0.6, 0.8, 1.0, 1.3} moltiplica il premio di ogni gamba (regola comune n.4).
|
||||||
|
- Capitale = S0 (cash-secured sul livello indice, convenzione r0702_alb_structure): CAGR/DD
|
||||||
|
sono su quel denominatore conservativo; Sharpe/PF/confronti gate-vs-no-gate invarianti.
|
||||||
|
|
||||||
|
TRADE SOVRAPPOSTI (fedele al progetto esterno, "~6 concorrenti" in calendario):
|
||||||
|
- Ingresso OGNI trading day, holding 4 td -> 4 trade concorrenti sul grid trading-day.
|
||||||
|
- Onesta' statistica: il libro si decompone in 4 STRAND non-sovrapposti (fase d'ingresso
|
||||||
|
mod 4). Lo Sharpe headline = media degli Sharpe di strand (ppy=63); la banda min-max degli
|
||||||
|
strand E' la banda d'ancora obbligatoria (regola 2026-07-02). N_eff = N_trade/4; PF/win
|
||||||
|
riportati con lo sconto esplicito.
|
||||||
|
- Equity/maxDD/CAGR/per-anno dalla serie giornaliera di portafoglio (1/4 del capitale per
|
||||||
|
trade, PnL lumpato a scadenza dello strand che scade quel giorno).
|
||||||
|
|
||||||
|
GATE (tutti causali a entry; rank VIX espandente con warm-start dal 1990):
|
||||||
|
(a) VIX-rank > soglia {0.20,0.30,0.40,0.50} x crash-skip {no, ivr<=0.90}; piu' il GATE
|
||||||
|
CANONICO crypto NON riottimizzato (vrp>0 AND 0.30<=ivr<=0.90, vrp = VIX-RV20). Selezione
|
||||||
|
della cella SOLO in-sample (pre-2025); deflated-Sharpe sul totale delle celle provate.
|
||||||
|
(b) Term-structure: VIX/VXV < 1 (contango) -> vendi; backwardation -> flat. Dal 2008.
|
||||||
|
(c) NULL DEL DE-LEVERING obbligatorio (lezione TP01xDVOL): lo Sharpe e' scale-invariante ->
|
||||||
|
il gate "vale" solo se batte lo Sharpe dell'always-in A PARI maxDD, con persistenza
|
||||||
|
multi-finestra, non su una finestra fortunata.
|
||||||
|
|
||||||
|
REGOLE STANDING: niente short-vol da modello in deploy — esito massimo = CONOSCENZA sul
|
||||||
|
meccanismo del gate (crypto vs equity). Hold-out 2025-26 mai usato per selezionare.
|
||||||
|
|
||||||
|
uv run python scripts/research/r0703_vrpimp_spxgate.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys
|
||||||
|
from bisect import bisect_left, insort
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt"))
|
||||||
|
import numpy as np, pandas as pd
|
||||||
|
from scipy.stats import norm
|
||||||
|
from altlib import deflated_sharpe # noqa: E402 (harness condiviso)
|
||||||
|
|
||||||
|
SCRATCH = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
|
||||||
|
"e00896d3-d4bb-4f2a-b471-55a1d88a12ba/scratchpad")
|
||||||
|
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
|
||||||
|
TS_START = pd.Timestamp("2008-01-01", tz="UTC") # finestra comune term-structure (VXV dal 2007-12)
|
||||||
|
TENOR_TD = 4 # trading days del corto (~6 giorni di calendario)
|
||||||
|
WING_FRAC = 0.5 # delta ali = meta' del delta corto (a priori)
|
||||||
|
SLOPE = 1.0 # skew log-lineare (vol per unita' di ln-moneyness) — MODELLO
|
||||||
|
SIG_FLOOR = 0.05
|
||||||
|
COST_FRAC = 0.05 # 5% del premio per gamba (commissioni+slippage, MODELLO)
|
||||||
|
F_SWEEP = (0.6, 0.8, 1.0, 1.3)
|
||||||
|
DELTAS = (0.01, 0.02, 0.05) # distanze corto del progetto esterno
|
||||||
|
CENTRAL_SD = 0.05 # cella centrale A PRIORI: 5-delta (la piu' campionata in coda;
|
||||||
|
# le 1-2 delta sono il territorio "0 perdite = Sharpe implausibile")
|
||||||
|
DPY_TRADE = 252.0 / TENOR_TD # ~63 trade indipendenti/anno per strand
|
||||||
|
WINDOWS = [("1996-2002", "1996", "2003"), ("2003-2009", "2003", "2010"),
|
||||||
|
("2010-2016", "2010", "2017"), ("2017-2023", "2017", "2024"),
|
||||||
|
("2024-2026", "2024", "2027")]
|
||||||
|
TAIL_YEARS = (1998, 2002, 2008, 2011, 2018, 2020, 2022)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------- pricing (MODELLO)
|
||||||
|
def bs_put(S, K, T, sig):
|
||||||
|
if T <= 0 or sig <= 0:
|
||||||
|
return max(K - S, 0.0)
|
||||||
|
d1 = (np.log(S / K) + 0.5 * sig ** 2 * T) / (sig * np.sqrt(T))
|
||||||
|
d2 = d1 - sig * np.sqrt(T)
|
||||||
|
return K * norm.cdf(-d2) - S * norm.cdf(-d1)
|
||||||
|
|
||||||
|
|
||||||
|
def bs_call(S, K, T, sig):
|
||||||
|
if T <= 0 or sig <= 0:
|
||||||
|
return max(S - K, 0.0)
|
||||||
|
d1 = (np.log(S / K) + 0.5 * sig ** 2 * T) / (sig * np.sqrt(T))
|
||||||
|
return S * norm.cdf(d1) - K * norm.cdf(d1 - sig * np.sqrt(T))
|
||||||
|
|
||||||
|
|
||||||
|
def sigma_k(sig_atm, S, K):
|
||||||
|
"""Skew log-lineare equity: put OTM (K<S) piu' ricche, call OTM piu' povere."""
|
||||||
|
return max(sig_atm + SLOPE * np.log(S / K), SIG_FLOOR)
|
||||||
|
|
||||||
|
|
||||||
|
def strike_skew(S, T, sig_atm, delta, kind):
|
||||||
|
"""Strike dal delta CON la vol skewata (punto fisso). kind: 'put'/'call', delta>0."""
|
||||||
|
d1 = norm.ppf(1 - delta) if kind == "put" else norm.ppf(delta)
|
||||||
|
sig = sig_atm
|
||||||
|
K = S
|
||||||
|
for _ in range(8):
|
||||||
|
K = S * np.exp(0.5 * sig * sig * T - d1 * sig * np.sqrt(T))
|
||||||
|
sig = sigma_k(sig_atm, S, K)
|
||||||
|
return K, sig
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------- dati
|
||||||
|
def load_master():
|
||||||
|
spy = pd.read_parquet(PROJECT_ROOT / "data" / "raw" / "eq_spy_1d.parquet")
|
||||||
|
px = pd.Series(spy["close"].astype(float).values,
|
||||||
|
index=pd.to_datetime(spy["timestamp"].astype("int64"), unit="ms", utc=True))
|
||||||
|
|
||||||
|
def _fred(fn, col):
|
||||||
|
f = SCRATCH / fn
|
||||||
|
if not f.exists():
|
||||||
|
return None
|
||||||
|
c = pd.read_csv(f, na_values=".")
|
||||||
|
return pd.Series(c[col].astype(float).values,
|
||||||
|
index=pd.to_datetime(c["observation_date"], utc=True))
|
||||||
|
|
||||||
|
vix = _fred("vixcls.csv", "VIXCLS")
|
||||||
|
vxv = _fred("vxvcls.csv", "VXVCLS")
|
||||||
|
if vix is None:
|
||||||
|
raise SystemExit("Manca scratchpad/vixcls.csv (curl FRED VIXCLS) — vedi docstring.")
|
||||||
|
# IV-rank espandente CAUSALE sulla serie VIX nativa (warm-start dal 1990, come _ivrank)
|
||||||
|
vals = vix.dropna()
|
||||||
|
hist: list[float] = []
|
||||||
|
rank = {}
|
||||||
|
for d, v in vals.items():
|
||||||
|
if len(hist) >= 250:
|
||||||
|
rank[d] = bisect_left(hist, v) / len(hist)
|
||||||
|
insort(hist, v)
|
||||||
|
ivr = pd.Series(rank)
|
||||||
|
|
||||||
|
df = pd.DataFrame({"px": px})
|
||||||
|
df["vix"] = vix.reindex(df.index).ffill(limit=5)
|
||||||
|
df["ivr"] = ivr.reindex(df.index).ffill(limit=5)
|
||||||
|
df["vxv"] = vxv.reindex(df.index).ffill(limit=5) if vxv is not None else np.nan
|
||||||
|
lr = np.log(df["px"]).diff()
|
||||||
|
df["rv20"] = lr.rolling(20).std() * np.sqrt(252) * 100.0 # causale: usa ritorni fino a i
|
||||||
|
return df.dropna(subset=["px", "vix", "ivr"])
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------- motore
|
||||||
|
def run_base(df, sd=CENTRAL_SD, f=1.0):
|
||||||
|
"""Vendita della double diagonal OGNI trading day (always-in). Ritorna DataFrame trade
|
||||||
|
(i, entry, exp, pnl su capitale S0, credit). I gate si applicano DOPO come maschera:
|
||||||
|
il pricing e' gate-indipendente -> una sola passata per (sd, f)."""
|
||||||
|
px = df["px"].values
|
||||||
|
vix = df["vix"].values / 100.0
|
||||||
|
idx = df.index
|
||||||
|
n = len(px)
|
||||||
|
recs = []
|
||||||
|
for i in range(21, n - TENOR_TD):
|
||||||
|
j = i + TENOR_TD
|
||||||
|
S0 = px[i]; sig0 = vix[i]
|
||||||
|
if not (np.isfinite(S0) and np.isfinite(sig0)) or sig0 <= 0:
|
||||||
|
continue
|
||||||
|
T = max((idx[j] - idx[i]).days, 1) / 365.25 # gap di calendario REALE
|
||||||
|
Tl = T + 1.0 / 365.25 # ali T+1g
|
||||||
|
# gambe corte (strangle) a delta sd; ali long a delta sd*WING_FRAC, scadenza T+1
|
||||||
|
Kps, sps = strike_skew(S0, T, sig0, sd, "put")
|
||||||
|
Kcs, scs = strike_skew(S0, T, sig0, sd, "call")
|
||||||
|
Kpl, spl = strike_skew(S0, Tl, sig0, sd * WING_FRAC, "put")
|
||||||
|
Kcl, scl = strike_skew(S0, Tl, sig0, sd * WING_FRAC, "call")
|
||||||
|
p_ps = bs_put(S0, Kps, T, sps) / S0 * f
|
||||||
|
p_cs = bs_call(S0, Kcs, T, scs) / S0 * f
|
||||||
|
p_pl = bs_put(S0, Kpl, Tl, spl) / S0 * f
|
||||||
|
p_cl = bs_call(S0, Kcl, Tl, scl) / S0 * f
|
||||||
|
credit = (p_ps + p_cs) - (p_pl + p_cl)
|
||||||
|
# exit a scadenza del corto: corti a intrinseco, ali marcate BS 1g alla vol di uscita
|
||||||
|
S1 = px[j]; sig1 = vix[j]
|
||||||
|
short_pay = (max(0.0, Kps - S1) + max(0.0, S1 - Kcs)) / S0
|
||||||
|
lp = bs_put(S1, Kpl, 1.0 / 365.25, sigma_k(sig1, S1, Kpl)) / S0 * f
|
||||||
|
lc = bs_call(S1, Kcl, 1.0 / 365.25, sigma_k(sig1, S1, Kcl)) / S0 * f
|
||||||
|
cost = COST_FRAC * (p_ps + p_cs + p_pl + p_cl) + COST_FRAC * (lp + lc)
|
||||||
|
pnl = credit - short_pay + (lp + lc) - cost
|
||||||
|
recs.append((i, idx[i], idx[j], pnl, credit))
|
||||||
|
tr = pd.DataFrame(recs, columns=["i", "entry", "exp", "pnl", "credit"])
|
||||||
|
tr["active"] = True
|
||||||
|
return tr
|
||||||
|
|
||||||
|
|
||||||
|
def apply_gate(base, mask):
|
||||||
|
"""mask: Series booleana su df.index (causale a entry). Trade bloccato -> pnl 0, inattivo."""
|
||||||
|
tr = base.copy()
|
||||||
|
ok = mask.reindex(tr["entry"]).fillna(False).values.astype(bool)
|
||||||
|
tr["active"] = ok
|
||||||
|
tr.loc[~tr["active"], "pnl"] = 0.0
|
||||||
|
return tr
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------- metriche
|
||||||
|
def _sh(x, ppy):
|
||||||
|
x = pd.Series(x).dropna()
|
||||||
|
return float(x.mean() / x.std() * np.sqrt(ppy)) if len(x) > 10 and x.std() > 0 else float("nan")
|
||||||
|
|
||||||
|
|
||||||
|
def daily_series(tr):
|
||||||
|
return tr.groupby("exp")["pnl"].sum() / TENOR_TD
|
||||||
|
|
||||||
|
|
||||||
|
def dd_of(daily):
|
||||||
|
eq = (1 + daily).cumprod()
|
||||||
|
pk = eq.cummax()
|
||||||
|
return float(((pk - eq) / pk).max())
|
||||||
|
|
||||||
|
|
||||||
|
def agg(tr):
|
||||||
|
"""Metriche oneste: Sharpe = media degli strand non-sovrapposti (banda = ancora);
|
||||||
|
equity/DD/CAGR dalla serie giornaliera; PF/win sui trade ATTIVI con N_eff = N/4."""
|
||||||
|
strands = [tr[tr["i"] % TENOR_TD == o]["pnl"] for o in range(TENOR_TD)]
|
||||||
|
shs = [_sh(s, DPY_TRADE) for s in strands]
|
||||||
|
hold = tr[tr["exp"] >= HOLDOUT]
|
||||||
|
isam = tr[tr["exp"] < HOLDOUT]
|
||||||
|
shs_h = [_sh(hold[hold["i"] % TENOR_TD == o]["pnl"], DPY_TRADE) for o in range(TENOR_TD)]
|
||||||
|
shs_i = [_sh(isam[isam["i"] % TENOR_TD == o]["pnl"], DPY_TRADE) for o in range(TENOR_TD)]
|
||||||
|
d = daily_series(tr)
|
||||||
|
eq = (1 + d).cumprod()
|
||||||
|
yrs = len(d) / 252.0
|
||||||
|
act = tr[tr["active"]]["pnl"]
|
||||||
|
pos = act[act > 0].sum(); neg = -act[act < 0].sum()
|
||||||
|
return dict(
|
||||||
|
sh=float(np.nanmean(shs)), sh_lo=float(np.nanmin(shs)), sh_hi=float(np.nanmax(shs)),
|
||||||
|
sh_is=float(np.nanmean(shs_i)), sh_h=float(np.nanmean(shs_h)),
|
||||||
|
sh_h_lo=float(np.nanmin(shs_h)) if np.isfinite(shs_h).any() else float("nan"),
|
||||||
|
sh_h_hi=float(np.nanmax(shs_h)) if np.isfinite(shs_h).any() else float("nan"),
|
||||||
|
cagr=float(eq.iloc[-1] ** (1 / yrs) - 1) if yrs > 0 and eq.iloc[-1] > 0 else -1.0,
|
||||||
|
dd=dd_of(d), worst5=float(d.rolling(5).sum().min()),
|
||||||
|
pf=float(pos / neg) if neg > 0 else float("inf"),
|
||||||
|
win=float((act > 0).mean()) if len(act) else float("nan"),
|
||||||
|
n_act=int(len(act)), n_eff=int(len(act) / TENOR_TD),
|
||||||
|
act_frac=float(tr["active"].mean()), nloss=int((act < 0).sum()))
|
||||||
|
|
||||||
|
|
||||||
|
def per_year(tr):
|
||||||
|
d = daily_series(tr)
|
||||||
|
return {int(y): float((1 + g).prod() - 1) for y, g in d.groupby(d.index.year)}
|
||||||
|
|
||||||
|
|
||||||
|
def win_sh(tr, a, b):
|
||||||
|
"""Sharpe strand-medio nella finestra [a, b) (su scadenza)."""
|
||||||
|
w = tr[(tr["exp"] >= pd.Timestamp(a, tz="UTC")) & (tr["exp"] < pd.Timestamp(b, tz="UTC"))]
|
||||||
|
return float(np.nanmean([_sh(w[w["i"] % TENOR_TD == o]["pnl"], DPY_TRADE)
|
||||||
|
for o in range(TENOR_TD)]))
|
||||||
|
|
||||||
|
|
||||||
|
def delever_k(daily_full, dd_target):
|
||||||
|
lo, hi = 0.0, 1.0
|
||||||
|
if dd_of(daily_full) <= dd_target:
|
||||||
|
return 1.0
|
||||||
|
for _ in range(40):
|
||||||
|
k = 0.5 * (lo + hi)
|
||||||
|
if dd_of(k * daily_full) > dd_target:
|
||||||
|
hi = k
|
||||||
|
else:
|
||||||
|
lo = k
|
||||||
|
return lo
|
||||||
|
|
||||||
|
|
||||||
|
def row(label, m):
|
||||||
|
pf = f"{m['pf']:5.2f}" if np.isfinite(m["pf"]) else " inf"
|
||||||
|
print(f" {label:<42} {m['sh']:>5.2f} [{m['sh_lo']:>5.2f},{m['sh_hi']:>5.2f}] "
|
||||||
|
f"{m['sh_is']:>6.2f} {m['sh_h']:>6.2f} {m['cagr']*100:>+6.2f}% {m['dd']*100:>5.1f}% "
|
||||||
|
f"{m['worst5']*100:>+6.2f}% {pf} {m['win']*100:>4.0f}% {m['act_frac']*100:>4.0f}% "
|
||||||
|
f"{m['n_eff']:>5}")
|
||||||
|
|
||||||
|
|
||||||
|
HDR = (f" {'config':<42} {'Sh':>5} {'[banda ancora]':>14} {'Sh-IS':>6} {'Sh-HO':>6} "
|
||||||
|
f"{'CAGR':>7} {'maxDD':>6} {'w5d':>7} {'PF':>5} {'win%':>5} {'att%':>5} {'Neff':>5}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 118)
|
||||||
|
print(" R0703 VRPIMP-SPXGATE — gate di regime (VIX-rank / term-structure) sulla double diagonal SPX")
|
||||||
|
print(" ⚠️ MODELLO: BS + skew log-lineare su VIX-30g @ tenor 6g; VIX/VXV da FRED research-grade NON certificati.")
|
||||||
|
print(" ⚠️ Trade sovrapposti (~4 concorrenti trading-day, ~6 calendario): Sharpe = media 4 strand indipendenti;")
|
||||||
|
print(" PF/win su trade sovrapposti -> campione effettivo = N/4. Capitale = S0 cash-secured (conv. ALB).")
|
||||||
|
print("=" * 118)
|
||||||
|
df = load_master()
|
||||||
|
print(f" SPY {df.index[0].date()} -> {df.index[-1].date()} ({len(df)} td) | VIX medio {df['vix'].mean():.1f} "
|
||||||
|
f"| RV20 media {df['rv20'].mean():.1f} | VRP(VIX-RV20) medio {(df['vix']-df['rv20']).mean():+.1f} pt, "
|
||||||
|
f">0 nel {((df['vix']-df['rv20'])>0).mean()*100:.0f}% dei giorni | VXV dal "
|
||||||
|
f"{df['vxv'].dropna().index[0].date() if df['vxv'].notna().any() else 'N/A'}")
|
||||||
|
|
||||||
|
trials = {} # (tag) -> Sharpe FULL strand-medio: conteggio per deflated-Sharpe
|
||||||
|
# ------------------------------------------------------------------ maschere gate (causali)
|
||||||
|
m_vrp = (df["vix"] - df["rv20"]) > 0
|
||||||
|
m_cs = df["ivr"] <= 0.90
|
||||||
|
m_canon = m_vrp & (df["ivr"] >= 0.30) & m_cs
|
||||||
|
m_contango = (df["vix"] / df["vxv"]) < 1.0
|
||||||
|
m_contango = m_contango.fillna(False)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ (1) griglia struttura
|
||||||
|
print(f"\n (1) STRUTTURA always-in, f=1.0 — distanze corto {{1,2,5}}-delta (ali a delta/2, T+1g)")
|
||||||
|
print(HDR)
|
||||||
|
bases = {}
|
||||||
|
for sd in DELTAS:
|
||||||
|
bases[sd] = run_base(df, sd=sd, f=1.0)
|
||||||
|
m = agg(bases[sd])
|
||||||
|
cr = bases[sd]["credit"]
|
||||||
|
tag = " ⚠️ 0-loss=Sharpe implausibile (CC01)" if m["nloss"] == 0 else ""
|
||||||
|
row(f"diag {sd*100:.0f}Δ always-in (credito {cr.mean()*1e4:+.1f}bps)", m)
|
||||||
|
if tag:
|
||||||
|
print(f" {tag}")
|
||||||
|
trials[f"sd{sd}_always"] = m["sh"]
|
||||||
|
base = bases[CENTRAL_SD]
|
||||||
|
print(f" -> cella centrale A PRIORI: {CENTRAL_SD*100:.0f}Δ (dichiarata nel docstring prima di guardare i numeri)")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ (2) gate VIX-rank
|
||||||
|
print(f"\n (2a) GATE VIX-RANK sulla cella centrale (f=1.0) — soglia scelta SOLO in-sample (pre-2025)")
|
||||||
|
print(HDR)
|
||||||
|
m_always = agg(base)
|
||||||
|
row("ALWAYS-IN (baseline)", m_always)
|
||||||
|
gates = {"vrp>0": m_vrp,
|
||||||
|
"CANONICO crypto (vrp>0 & 0.30<=ivr<=0.90)": m_canon}
|
||||||
|
for th in (0.20, 0.30, 0.40, 0.50):
|
||||||
|
gates[f"ivr>={th:.2f}"] = df["ivr"] >= th
|
||||||
|
gates[f"ivr>={th:.2f} & cs0.90"] = (df["ivr"] >= th) & m_cs
|
||||||
|
res = {}
|
||||||
|
for tag, msk in gates.items():
|
||||||
|
tr = apply_gate(base, msk)
|
||||||
|
res[tag] = (tr, agg(tr))
|
||||||
|
row(tag, res[tag][1])
|
||||||
|
trials[tag] = res[tag][1]["sh"]
|
||||||
|
sel_tag = max(res, key=lambda t: res[t][1]["sh_is"])
|
||||||
|
sel_tr, sel_m = res[sel_tag]
|
||||||
|
can_tr, can_m = res["CANONICO crypto (vrp>0 & 0.30<=ivr<=0.90)"]
|
||||||
|
print(f" -> cella scelta IN-SAMPLE: '{sel_tag}' (Sh-IS {sel_m['sh_is']:.2f}) | suo hold-out {sel_m['sh_h']:.2f} "
|
||||||
|
f"[{sel_m['sh_h_lo']:.2f},{sel_m['sh_h_hi']:.2f}]")
|
||||||
|
dsr, sr0 = deflated_sharpe(sel_m["sh"], list(trials.values()),
|
||||||
|
sel_tr[sel_tr["i"] % TENOR_TD == 0]["pnl"], dpy=DPY_TRADE)
|
||||||
|
print(f" -> deflated-Sharpe della cella scelta su {len(trials)} celle: DSR={dsr:.3f} "
|
||||||
|
f"(null max atteso {sr0:.2f}) {'PASS' if dsr >= 0.95 else 'FAIL'} (>=0.95)")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ (3) per-anno completo
|
||||||
|
print(f"\n (3) PER-ANNO COMPLETO (f=1.0, ritorni su capitale S0) — * = anno di coda")
|
||||||
|
pa, pc, ps = per_year(base), per_year(can_tr), per_year(sel_tr)
|
||||||
|
print(f" {'anno':<6} {'ALWAYS':>8} {'CANONICO':>9} {'SELECTED':>9} {'anno':<6} {'ALWAYS':>8} {'CANONICO':>9} {'SELECTED':>9}")
|
||||||
|
ys = sorted(pa)
|
||||||
|
half = (len(ys) + 1) // 2
|
||||||
|
for k in range(half):
|
||||||
|
cells = []
|
||||||
|
for y in (ys[k], ys[k + half] if k + half < len(ys) else None):
|
||||||
|
if y is None:
|
||||||
|
cells.append(" " * 37)
|
||||||
|
continue
|
||||||
|
star = "*" if y in TAIL_YEARS else " "
|
||||||
|
cells.append(f"{y}{star:<1} {pa[y]*100:>+7.2f}% {pc.get(y,0)*100:>+8.2f}% {ps.get(y,0)*100:>+8.2f}%")
|
||||||
|
print(" " + " ".join(cells))
|
||||||
|
flips_c = [y for y in TAIL_YEARS if y in pa and pa[y] < 0 and pc.get(y, 0) >= 0]
|
||||||
|
flips_s = [y for y in TAIL_YEARS if y in pa and pa[y] < 0 and ps.get(y, 0) >= 0]
|
||||||
|
worse_c = [y for y in TAIL_YEARS if y in pa and pc.get(y, 0) < min(0.0, pa[y])]
|
||||||
|
print(f" anni di coda RIBALTATI (neg->non-neg): canonico {flips_c or 'NESSUNO'} | selected {flips_s or 'NESSUNO'}")
|
||||||
|
print(f" anni di coda PEGGIORATI dal canonico: {worse_c or 'nessuno'}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ (4) term structure
|
||||||
|
print(f"\n (4) (2b) GATE TERM-STRUCTURE VIX/VXV — finestra comune dal 2008 (VXV research-grade)")
|
||||||
|
sub = lambda tr: tr[tr["entry"] >= TS_START] # noqa: E731
|
||||||
|
print(HDR)
|
||||||
|
ts_res = {}
|
||||||
|
cont_tr = apply_gate(base, m_contango)
|
||||||
|
for tag, tr in (("ALWAYS-IN (2008+)", sub(base)),
|
||||||
|
("contango VIX/VXV<1 (2008+)", sub(cont_tr)),
|
||||||
|
("CANONICO (2008+)", sub(can_tr)),
|
||||||
|
("CANONICO & contango (2008+)", sub(apply_gate(base, m_canon & m_contango)))):
|
||||||
|
ts_res[tag] = agg(tr)
|
||||||
|
row(tag, ts_res[tag])
|
||||||
|
trials[f"ts_{tag}"] = ts_res[tag]["sh"]
|
||||||
|
# la finestra 2008-2009 (esclusa dal multicut per costruzione) e' il banco di prova di coda
|
||||||
|
sa08 = win_sh(base, "2008", "2010"); st08 = win_sh(cont_tr, "2008", "2010")
|
||||||
|
print(f" finestra 2008-2009 (GFC): always {sa08:.2f} vs contango {st08:.2f} (Δ {st08-sa08:+.2f})")
|
||||||
|
# per-anno del contango (2008+) + code ribaltate
|
||||||
|
pt = per_year(sub(cont_tr)); pa8 = {y: v for y, v in per_year(sub(base)).items()}
|
||||||
|
print(f" per-anno 2008+ (ALWAYS vs CONTANGO): " + " ".join(
|
||||||
|
f"{y}{'*' if y in TAIL_YEARS else ''}:{pa8[y]*100:+.2f}/{pt.get(y,0)*100:+.2f}%" for y in sorted(pa8)))
|
||||||
|
flips_t = [y for y in TAIL_YEARS if y in pa8 and pa8[y] < 0 and pt.get(y, 0) >= 0]
|
||||||
|
print(f" anni di coda RIBALTATI dal contango: {flips_t or 'NESSUNO'}")
|
||||||
|
# sovrapposizione maschere (il contango e' un crash-skip travestito?) — giorni 2008+
|
||||||
|
w8 = df.index >= TS_START
|
||||||
|
for tag, mk in (("ivr<=0.90 (crash-skip)", m_cs), ("vrp>0", m_vrp), ("canonico", m_canon)):
|
||||||
|
a_ = m_contango[w8].values; b_ = mk[w8].values
|
||||||
|
agree = float((a_ == b_).mean())
|
||||||
|
print(f" accordo giorni contango vs {tag:<22}: {agree*100:.0f}% (contango attivo {a_.mean()*100:.0f}%, "
|
||||||
|
f"{tag.split()[0]} attivo {b_.mean()*100:.0f}%)")
|
||||||
|
dsr_t, sr0_t = deflated_sharpe(ts_res["contango VIX/VXV<1 (2008+)"]["sh"], list(trials.values()),
|
||||||
|
sub(cont_tr)[sub(cont_tr)["i"] % TENOR_TD == 0]["pnl"], dpy=DPY_TRADE)
|
||||||
|
print(f" deflated-Sharpe contango su {len(trials)} celle totali provate: DSR={dsr_t:.3f} "
|
||||||
|
f"{'PASS' if dsr_t >= 0.95 else 'FAIL'} (>=0.95)")
|
||||||
|
print(" ⚠️ CAVEAT MODELLO (decisivo): il pricing usa VIX-30g come IV del tenor 6g. In BACKWARDATION")
|
||||||
|
print(" la IV front-end reale e' >> VIX -> il credito reale nei giorni esclusi dal gate e' maggiore")
|
||||||
|
print(" del modellato: l'uplift del contango e' quindi in parte SOVRASTIMATO per costruzione.")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ (5) null de-levering
|
||||||
|
print(f"\n (5) NULL DEL DE-LEVERING (obbligatorio) — always-in scalato al maxDD del gated:")
|
||||||
|
d_full = daily_series(base)
|
||||||
|
for tag, m_g in (("CANONICO", can_m), (f"SELECTED '{sel_tag}'", sel_m),
|
||||||
|
("contango (2008+)", ts_res["contango VIX/VXV<1 (2008+)"])):
|
||||||
|
dref = d_full if "2008" not in tag else d_full[d_full.index >= TS_START]
|
||||||
|
sh_ref = m_always["sh"] if "2008" not in tag else ts_res["ALWAYS-IN (2008+)"]["sh"]
|
||||||
|
k = delever_k(dref, m_g["dd"])
|
||||||
|
eq = (1 + k * dref).cumprod(); yrs = len(dref) / 252.0
|
||||||
|
cagr_k = float(eq.iloc[-1] ** (1 / yrs) - 1)
|
||||||
|
beat = m_g["sh"] - sh_ref
|
||||||
|
print(f" {tag:<28} gated: Sh {m_g['sh']:.2f} DD {m_g['dd']*100:.1f}% CAGR {m_g['cagr']*100:+.2f}% | "
|
||||||
|
f"de-lever k={k:.2f}: Sh {sh_ref:.2f} (invariante) CAGR {cagr_k:+.2f}% -> "
|
||||||
|
f"ΔSh gate-vs-delever {beat:+.2f} {'(il gate AGGIUNGE)' if beat > 0.1 else '(de-levering basta)'}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ (6) multi-cut
|
||||||
|
print(f"\n (6) PERSISTENZA MULTI-FINESTRA (uplift Sharpe strand-medio gated - always, finestre disgiunte)")
|
||||||
|
print(f" {'finestra':<12} {'ALWAYS':>7} {'CANON':>7} {'Δcanon':>8} {'SELECT':>7} {'Δsel':>8} {'contango':>9} {'Δcont':>8}")
|
||||||
|
pos_c = pos_s = pos_t = n_t = 0
|
||||||
|
for wtag, a, b in WINDOWS:
|
||||||
|
sa = win_sh(base, a, b); sc = win_sh(can_tr, a, b); ss = win_sh(sel_tr, a, b)
|
||||||
|
in_ts = pd.Timestamp(a, tz="UTC") >= TS_START
|
||||||
|
st = win_sh(apply_gate(base, m_contango), a, b) if in_ts else float("nan")
|
||||||
|
dc, dsl = sc - sa, ss - sa
|
||||||
|
dt = (st - sa) if in_ts else float("nan")
|
||||||
|
pos_c += dc > 0; pos_s += dsl > 0
|
||||||
|
if in_ts:
|
||||||
|
pos_t += dt > 0; n_t += 1
|
||||||
|
print(f" {wtag:<12} {sa:>7.2f} {sc:>7.2f} {dc:>+8.2f} {ss:>7.2f} {dsl:>+8.2f} "
|
||||||
|
f"{st:>9.2f} {dt:>+8.2f}")
|
||||||
|
print(f" -> multicut: canonico {pos_c}/5 | selected {pos_s}/5 | contango {pos_t}/{n_t} (2008+)")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ (7) banda f
|
||||||
|
print(f"\n (7) BANDA f {F_SWEEP} (regola skew n.4) — cella centrale, always vs canonico vs selected")
|
||||||
|
print(HDR)
|
||||||
|
sel_mask = gates[sel_tag]
|
||||||
|
sub8 = lambda tr: tr[tr["entry"] >= TS_START] # noqa: E731
|
||||||
|
for f in F_SWEEP:
|
||||||
|
bf = bases[CENTRAL_SD] if f == 1.0 else run_base(df, sd=CENTRAL_SD, f=f)
|
||||||
|
row(f"always-in f={f}", agg(bf))
|
||||||
|
row(f"canonico f={f}", agg(apply_gate(bf, m_canon)))
|
||||||
|
row(f"selected f={f}", agg(apply_gate(bf, sel_mask)))
|
||||||
|
a8 = agg(sub8(bf)); c8 = agg(sub8(apply_gate(bf, m_contango)))
|
||||||
|
print(f" -> contango f={f} (2008+): Sh {c8['sh']:.2f} vs always {a8['sh']:.2f} "
|
||||||
|
f"(Δ {c8['sh']-a8['sh']:+.2f}) DD {c8['dd']*100:.1f}% vs {a8['dd']*100:.1f}%")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ verdetto
|
||||||
|
print("\n DOMANDA SECCA — il gate ribalta gli anni di coda come IV-rank su crypto (HOLD -0.25->+0.28)?")
|
||||||
|
print(f" crypto VRP01: gate ivr>=0.30 = vendi solo vol RICCA -> ribalta l'hold-out.")
|
||||||
|
print(f" SPX qui: always Sh {m_always['sh']:.2f} (HO {m_always['sh_h']:.2f}) vs canonico Sh {can_m['sh']:.2f} "
|
||||||
|
f"(HO {can_m['sh_h']:.2f}); code ribaltate: {flips_c or 'NESSUNA'}; multicut canonico {pos_c}/5; DSR {dsr:.2f}.")
|
||||||
|
print(" ⚠️ Tutto su MODELLO (BS+skew log-lineare, VIX 30g @ 6g, dati FRED non certificati). Regola standing:")
|
||||||
|
print(" niente short-vol da modello in deploy — esito = conoscenza sul meccanismo del gate, non un LEAD.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,691 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""r0703_vrpimp_stresslab.py — FILONE 7: STRESS LAB DI CODA per la famiglia short-vol crypto.
|
||||||
|
|
||||||
|
NON e' ricerca di alpha: e' FISICA DELLO STRUMENTO (niente hold-out, niente selezione — dichiarato).
|
||||||
|
Tre banchi:
|
||||||
|
|
||||||
|
(a) REPLAY STORICI — le 10 peggiori finestre 2-settimane di BTC/ETH 2019-2026 (prezzi CERTIFICATI,
|
||||||
|
load_tf 1d) applicate a VRP01 canonico (put credit spread Δ-0.28/-0.10, 7g) e alle strutture
|
||||||
|
ALB-A alla cella a-priori z=2σ/ali+1σ/5g (vertical / condor / diagonal T+1), gate canonico
|
||||||
|
ON/OFF. IV: REALE (DVOL) dove esiste (2021-03+); prima, salta secondo la relazione EMPIRICA
|
||||||
|
DVOL-vs-ritorno stimata dai dati (regressione piecewise Δdvol_pts ~ b−·ret− + b+·ret+ su
|
||||||
|
finestre NON sovrapposte; applicata in avanti dal punto d'ingresso = nessun uso di futuro
|
||||||
|
dentro la finestra). Entry di base = GIORNO DI PICCO della finestra (worst-case timing);
|
||||||
|
banda d'ancora = fase d'ingresso 0..tenor-1. Banda f obbligatoria.
|
||||||
|
|
||||||
|
(b) MATRICE SINTETICA — gap {-10,-15,-20,-30}% × IV-spike {×1.5,×2,×3} × timing {overnight,
|
||||||
|
intra-settimana}: perdita per struttura in unita' di CREDITO MEDIO (storico, gated) e in
|
||||||
|
% del capitale a sizing 12% (convenzione margine-deployed = max-loss defined-risk; per i
|
||||||
|
nudi il margine non e' definito → cash-secured, dichiarato). Il timing muove il MARK-TO-
|
||||||
|
MARKET al gap (stress di margine), non la perdita a scadenza (stessa S1): riportati entrambi.
|
||||||
|
L'asse IV-spike e' ancorato all'empirico (la regressione dice quale ×IV produce ogni gap).
|
||||||
|
|
||||||
|
(c) VALORE DELL'ALA — per ogni feature strutturale (ala far-OTM del VRP01 Δ-0.10; ala far-OTM
|
||||||
|
dello strangle z2; ala T+1 del diagonale; ala PIU' VICINA dz=0.5): drag annuo storico in bps
|
||||||
|
(decomposto calm-drag vs tail-benefit come ALB-A) vs protezione comprata in ogni cella della
|
||||||
|
matrice → tabella "protezione di coda per bps di drag" + NULL DEL DE-LEVERING esplicito
|
||||||
|
(lezione TP01×DVOL 2026-06-26): se ridurre la size della struttura SENZA ala raggiunge la
|
||||||
|
stessa perdita di cella a costo CAGR minore, l'ala NON vale.
|
||||||
|
|
||||||
|
Deliverable finale: worst-case ONESTO del VRP01 attuale a 2k/5k di capitale in EUR (convenzione
|
||||||
|
book cash-secured E convenzione margine-deployed, granulare in spread interi ETH), banda f.
|
||||||
|
|
||||||
|
MACCHINERIA RIUSATA (non riscritta): motore DVOL di r0702_alb_structure (bs_call, _fee_frac,
|
||||||
|
run_structure, book, metrics — riproduce VRP01 esatto), options_vrp_lab (bs_put, load_series,
|
||||||
|
strike_from_delta), options_vrp_v2 (vrp_spread_weekly canonico, _ivrank, _rv30). Il codice NUOVO
|
||||||
|
e' solo (i) pricing di scenario a (S0, σ0, S1, σ1) fissati, (ii) replay su path certificato con
|
||||||
|
IV sintetica pre-DVOL, (iii) contabilita' drag/protezione.
|
||||||
|
|
||||||
|
CAVEAT (in testa, non in fondo): pricing BS FLAT su DVOL-30g usato a tenor 5-7g e 1g (term
|
||||||
|
structure ignorata: in stress il front-end esplode → il MtM avverso e' SOTTOSTIMATO); skew non
|
||||||
|
esplicito → banda f {0.6,0.8,1.0,1.3} su ogni claim; DVOL pre-2021 NON esiste → le finestre
|
||||||
|
2019-2020 (COVID incluso) sono SCENARI regression-driven, non backtest. Regola standing INVARIATA:
|
||||||
|
niente short-vol da modello in deploy — l'esito massimo e' conoscenza.
|
||||||
|
|
||||||
|
pandas 2.x: nessun DatetimeIndex.view('int64'); nessun resample '7D' (cadenze a passi d'indice).
|
||||||
|
|
||||||
|
uv run python scripts/research/r0703_vrpimp_stresslab.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path("/opt/docker/PythagorasGoal")
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
import altlib as al # noqa: E402 (fee/holdout conventions; qui usato solo per coerenza costanti)
|
||||||
|
from scripts.research.options_vrp_lab import bs_put, load_series, strike_from_delta # noqa: E402
|
||||||
|
from scripts.research.options_vrp_v2 import vrp_spread_weekly # noqa: E402
|
||||||
|
from scripts.research.r0702_alb_structure import ( # noqa: E402
|
||||||
|
bs_call, _fee_frac, run_structure, book, metrics,
|
||||||
|
)
|
||||||
|
from scripts.analysis.research_lab import load_tf # noqa: E402
|
||||||
|
|
||||||
|
DAY = 1.0 / 365.25
|
||||||
|
F_SWEEP = (0.6, 0.8, 1.0, 1.3)
|
||||||
|
START = pd.Timestamp("2019-01-01", tz="UTC")
|
||||||
|
ASSETS = ("BTC", "ETH")
|
||||||
|
ALB = dict(z=2.0, dz=1.0, tenor_d=5) # cella a-priori ALB-A (non riottimizzata)
|
||||||
|
SIZING = 0.12 # peso VRP01 nel book
|
||||||
|
GAPS = (-0.10, -0.15, -0.20, -0.30)
|
||||||
|
IVMULT = (1.5, 2.0, 3.0)
|
||||||
|
IV_FLOOR, IV_CAP = 25.0, 250.0 # punti DVOL, clamp della IV sintetica
|
||||||
|
|
||||||
|
# strutture del banco: kind -> (engine-kind, z, dz, tenor, label)
|
||||||
|
STRUCTS = {
|
||||||
|
"NAKED28": dict(tenor=7, label="naked put Δ-0.28 7g (VRP01 senza ala)"),
|
||||||
|
"VRP01": dict(tenor=7, label="VRP01 spread Δ-0.28/-0.10 7g"),
|
||||||
|
"STRANGLE": dict(tenor=5, label="short strangle z2 5g (ALB senza ali)"),
|
||||||
|
"VERT": dict(tenor=5, label="vertical put z2/+1σ 5g"),
|
||||||
|
"CONDOR": dict(tenor=5, label="iron condor z2/+1σ 5g"),
|
||||||
|
"DIAG": dict(tenor=5, label="double diagonal z2/+1σ T+1 5g"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# PRICING DI SCENARIO — un trade a (S0, σ0) → (S1, σ1), stessa matematica del
|
||||||
|
# motore r0702_alb_structure (fee per gamba 0.03% cap 12.5%, delivery 0.015%)
|
||||||
|
# ===========================================================================
|
||||||
|
def _strikes(kind: str, S0: float, sig0: float, tenor_d: int, z: float, dz: float):
|
||||||
|
T = tenor_d / 365.25
|
||||||
|
if kind in ("VRP01", "NAKED28"):
|
||||||
|
Kp_s = strike_from_delta(S0, T, sig0, -0.28)
|
||||||
|
Kp_l = strike_from_delta(S0, T, sig0, -0.10) if kind == "VRP01" else None
|
||||||
|
return Kp_s, Kp_l, None, None
|
||||||
|
m = sig0 * np.sqrt(T)
|
||||||
|
Kp_s = S0 * np.exp(-z * m)
|
||||||
|
Kp_l = S0 * np.exp(-(z + dz) * m) if kind in ("VERT", "CONDOR", "DIAG") else None
|
||||||
|
Kc_s = S0 * np.exp(+z * m) if kind in ("STRANGLE", "CONDOR", "DIAG") else None
|
||||||
|
Kc_l = S0 * np.exp(+(z + dz) * m) if kind in ("CONDOR", "DIAG") else None
|
||||||
|
return Kp_s, Kp_l, Kc_s, Kc_l
|
||||||
|
|
||||||
|
|
||||||
|
def trade_pnl(kind: str, S0: float, sig0: float, S1: float, sig1: float,
|
||||||
|
tenor_d: int, f_put: float = 1.0, f_call: float = 1.0,
|
||||||
|
z: float = 2.0, dz: float = 1.0) -> dict:
|
||||||
|
"""PnL a scadenza degli short (frazione di S0). DIAG: ali T+1 marcate BS a σ1 (vega)."""
|
||||||
|
T = tenor_d / 365.25
|
||||||
|
T_l = T + DAY if kind == "DIAG" else T
|
||||||
|
Kp_s, Kp_l, Kc_s, Kc_l = _strikes(kind, S0, sig0, tenor_d, z, dz)
|
||||||
|
ps = bs_put(S0, Kp_s, T, sig0) / S0 * f_put
|
||||||
|
pl = bs_put(S0, Kp_l, T_l, sig0) / S0 * f_put if Kp_l else 0.0
|
||||||
|
cs = bs_call(S0, Kc_s, T, sig0) / S0 * f_call if Kc_s else 0.0
|
||||||
|
cl = bs_call(S0, Kc_l, T_l, sig0) / S0 * f_call if Kc_l else 0.0
|
||||||
|
credit = (ps + cs) - (pl + cl)
|
||||||
|
short_pay = max(0.0, Kp_s - S1) / S0 + (max(0.0, S1 - Kc_s) / S0 if Kc_s else 0.0)
|
||||||
|
if kind == "DIAG":
|
||||||
|
lp = bs_put(S1, Kp_l, DAY, sig1) / S0 * f_put
|
||||||
|
lc = bs_call(S1, Kc_l, DAY, sig1) / S0 * f_call
|
||||||
|
long_val = lp + lc
|
||||||
|
exit_fee = sum(_fee_frac(v, notional_ratio=S1 / S0) for v in (lp, lc))
|
||||||
|
else:
|
||||||
|
long_val = (max(0.0, Kp_l - S1) / S0 if Kp_l else 0.0) \
|
||||||
|
+ (max(0.0, S1 - Kc_l) / S0 if Kc_l else 0.0)
|
||||||
|
exit_fee = 0.0
|
||||||
|
legs = [v for v in (ps, pl, cs, cl) if v > 0]
|
||||||
|
entry_fee = sum(_fee_frac(v) for v in legs)
|
||||||
|
deliv = _fee_frac(max(0.0, Kp_s - S1) / S0, rate=0.00015)
|
||||||
|
if Kc_s:
|
||||||
|
deliv += _fee_frac(max(0.0, S1 - Kc_s) / S0, rate=0.00015)
|
||||||
|
pnl = credit - short_pay + long_val - entry_fee - exit_fee - deliv
|
||||||
|
widths = [w for w in ((Kp_s - Kp_l) / S0 if Kp_l else np.nan,
|
||||||
|
(Kc_l - Kc_s) / S0 if Kc_l else np.nan) if np.isfinite(w)]
|
||||||
|
margin = (max(widths) - credit) if widths else np.nan # defined-risk max loss
|
||||||
|
return dict(pnl=pnl, credit=credit, margin=margin, ks_frac=Kp_s / S0)
|
||||||
|
|
||||||
|
|
||||||
|
def trade_mtm(kind: str, sig0: float, gap: float, mult: float, t_gap: int,
|
||||||
|
tenor_d: int, f_put: float = 1.0, f_call: float = 1.0,
|
||||||
|
z: float = 2.0, dz: float = 1.0) -> float:
|
||||||
|
"""Mark-to-market al giorno del gap (chiusura ipotetica, senza fee di chiusura):
|
||||||
|
tutte le gambe riprezzate BS al tempo residuo e alla IV spikeata. Frazione di S0=1."""
|
||||||
|
S0 = 1.0
|
||||||
|
T = tenor_d / 365.25
|
||||||
|
Kp_s, Kp_l, Kc_s, Kc_l = _strikes(kind, S0, sig0, tenor_d, z, dz)
|
||||||
|
ps = bs_put(S0, Kp_s, T, sig0) * f_put
|
||||||
|
pl = bs_put(S0, Kp_l, (T + DAY if kind == "DIAG" else T), sig0) * f_put if Kp_l else 0.0
|
||||||
|
cs = bs_call(S0, Kc_s, T, sig0) * f_call if Kc_s else 0.0
|
||||||
|
cl = bs_call(S0, Kc_l, (T + DAY if kind == "DIAG" else T), sig0) * f_call if Kc_l else 0.0
|
||||||
|
credit = (ps + cs) - (pl + cl)
|
||||||
|
legs = [v for v in (ps, pl, cs, cl) if v > 0]
|
||||||
|
entry_fee = sum(_fee_frac(v) for v in legs)
|
||||||
|
S1 = 1.0 + gap
|
||||||
|
sig_sp = sig0 * mult
|
||||||
|
Tr = max(tenor_d - t_gap, 0) / 365.25
|
||||||
|
Tr_l = Tr + DAY if kind == "DIAG" else Tr
|
||||||
|
sv = bs_put(S1, Kp_s, Tr, sig_sp) * f_put + (bs_call(S1, Kc_s, Tr, sig_sp) * f_call if Kc_s else 0.0)
|
||||||
|
lv = (bs_put(S1, Kp_l, Tr_l, sig_sp) * f_put if Kp_l else 0.0) \
|
||||||
|
+ (bs_call(S1, Kc_l, Tr_l, sig_sp) * f_call if Kc_l else 0.0)
|
||||||
|
return credit - (sv - lv) - entry_fee
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# DATI FULL-HISTORY (2019+) + IV SINTETICA (proxy RV30+medVRP, regressione Δdvol~ret)
|
||||||
|
# ===========================================================================
|
||||||
|
def full_history(asset: str) -> dict:
|
||||||
|
d = load_tf(asset, "1d")
|
||||||
|
s = pd.Series(d["close"].values.astype(float),
|
||||||
|
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
|
||||||
|
s = s[s.index >= START]
|
||||||
|
px = s.values
|
||||||
|
n = len(px)
|
||||||
|
lr = np.diff(np.log(px))
|
||||||
|
rv30 = np.full(n, np.nan)
|
||||||
|
for i in range(30, n):
|
||||||
|
rv30[i] = float(np.std(lr[i - 30:i]) * np.sqrt(365.25)) # come _rv30 (ddof=0)
|
||||||
|
dv = pd.read_parquet(ROOT / "data" / "raw" / f"dvol_{asset.lower()}.parquet")
|
||||||
|
dvs = pd.Series(dv["close"].values.astype(float),
|
||||||
|
index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True))
|
||||||
|
dvol = dvs.reindex(s.index).values # punti %, NaN pre-2021
|
||||||
|
ok = ~np.isnan(dvol) & ~np.isnan(rv30)
|
||||||
|
medvrp = float(np.median(dvol[ok] - rv30[ok] * 100.0)) # premio mediano IV-RV, punti
|
||||||
|
proxy = rv30 * 100.0 + medvrp # IV proxy pre-DVOL, punti
|
||||||
|
hist_iv = np.where(np.isnan(dvol), proxy, dvol) # serie spliced per il RANK
|
||||||
|
return dict(idx=s.index, px=px, n=n, rv30=rv30, dvol=dvol, proxy=proxy,
|
||||||
|
hist_iv=hist_iv, medvrp=medvrp)
|
||||||
|
|
||||||
|
|
||||||
|
def fit_dvol_reg(asset: str, horizon_d: int) -> dict:
|
||||||
|
"""Regressione piecewise Δdvol_pts = a + b−·min(ret,0) + b+·max(ret,0), finestre NON
|
||||||
|
sovrapposte di horizon_d giorni sull'era DVOL (2021-03+). E' una relazione CONTEMPORANEA
|
||||||
|
(fisica del salto di IV), applicata in avanti nel replay — nessun uso del futuro."""
|
||||||
|
J = load_series(asset)
|
||||||
|
px = J["px"].values
|
||||||
|
dv = J["dvol"].values
|
||||||
|
r, d = [], []
|
||||||
|
for i in range(0, len(px) - horizon_d, horizon_d):
|
||||||
|
r.append(px[i + horizon_d] / px[i] - 1.0)
|
||||||
|
d.append(dv[i + horizon_d] - dv[i])
|
||||||
|
r = np.asarray(r); d = np.asarray(d)
|
||||||
|
X = np.column_stack([np.ones_like(r), np.minimum(r, 0.0), np.maximum(r, 0.0)])
|
||||||
|
beta, *_ = np.linalg.lstsq(X, d, rcond=None)
|
||||||
|
pred = X @ beta
|
||||||
|
r2 = 1.0 - ((d - pred) ** 2).sum() / ((d - d.mean()) ** 2).sum()
|
||||||
|
return dict(a=float(beta[0]), b_neg=float(beta[1]), b_pos=float(beta[2]),
|
||||||
|
r2=float(r2), n=len(r))
|
||||||
|
|
||||||
|
|
||||||
|
def iv_jump(iv0_pts: float, cum_ret: float, reg: dict) -> float:
|
||||||
|
"""IV sintetica: livello d'ingresso + salto regressione (intercetta esclusa: e' drift
|
||||||
|
di regime, non fisica del salto — dichiarato)."""
|
||||||
|
v = iv0_pts + reg["b_neg"] * min(cum_ret, 0.0) + reg["b_pos"] * max(cum_ret, 0.0)
|
||||||
|
return float(np.clip(v, IV_FLOOR, IV_CAP))
|
||||||
|
|
||||||
|
|
||||||
|
def worst_windows(px: np.ndarray, n_win: int = 10, ndays: int = 14) -> list[int]:
|
||||||
|
"""Indici di partenza delle n_win peggiori finestre ndays NON sovrapposte."""
|
||||||
|
r = px[ndays:] / px[:-ndays] - 1.0
|
||||||
|
order = np.argsort(r)
|
||||||
|
picked: list[int] = []
|
||||||
|
for i in order:
|
||||||
|
if all(abs(int(i) - j) >= ndays for j in picked):
|
||||||
|
picked.append(int(i))
|
||||||
|
if len(picked) >= n_win:
|
||||||
|
break
|
||||||
|
return picked
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# REPLAY di una finestra: vendita sistematica della struttura dentro [i0, i0+14)
|
||||||
|
# ===========================================================================
|
||||||
|
def replay_window(H: dict, reg: dict, i0: int, kind: str, gated: bool,
|
||||||
|
f_put: float = 1.0, f_call: float = 1.0, phase: int = 0,
|
||||||
|
ndays: int = 14) -> dict:
|
||||||
|
px, n = H["px"], H["n"]
|
||||||
|
tenor = STRUCTS[kind]["tenor"]
|
||||||
|
t = i0 + phase
|
||||||
|
entry0 = t
|
||||||
|
tot = 0.0
|
||||||
|
n_tr = n_skip = 0
|
||||||
|
credits, margins = [], []
|
||||||
|
while t < i0 + phase + ndays and t + tenor < n:
|
||||||
|
S0 = px[t]
|
||||||
|
real = not np.isnan(H["dvol"][t])
|
||||||
|
if real:
|
||||||
|
sig0_pts = H["dvol"][t]
|
||||||
|
else:
|
||||||
|
base = H["proxy"][entry0] if np.isnan(H["dvol"][entry0]) else H["dvol"][entry0]
|
||||||
|
sig0_pts = iv_jump(base, px[t] / px[entry0] - 1.0, reg)
|
||||||
|
if gated:
|
||||||
|
rv = H["rv30"][t]
|
||||||
|
hist = H["hist_iv"][30:t]
|
||||||
|
ivr = float((hist < sig0_pts).mean()) if len(hist) >= 60 else np.nan
|
||||||
|
skip = ((not np.isnan(rv) and (sig0_pts / 100.0 - rv) <= 0)
|
||||||
|
or (not np.isnan(ivr) and (ivr < 0.30 or ivr > 0.90)))
|
||||||
|
if skip:
|
||||||
|
n_skip += 1
|
||||||
|
t += tenor
|
||||||
|
continue
|
||||||
|
j = t + tenor
|
||||||
|
S1 = px[j]
|
||||||
|
if not np.isnan(H["dvol"][j]):
|
||||||
|
sig1_pts = H["dvol"][j]
|
||||||
|
else:
|
||||||
|
base = H["proxy"][entry0] if np.isnan(H["dvol"][entry0]) else H["dvol"][entry0]
|
||||||
|
sig1_pts = iv_jump(base, px[j] / px[entry0] - 1.0, reg)
|
||||||
|
r = trade_pnl(kind, S0, sig0_pts / 100.0, S1, sig1_pts / 100.0, tenor,
|
||||||
|
f_put=f_put, f_call=f_call, z=ALB["z"], dz=ALB["dz"])
|
||||||
|
tot += r["pnl"]
|
||||||
|
credits.append(r["credit"]); margins.append(r["margin"])
|
||||||
|
n_tr += 1
|
||||||
|
t += tenor
|
||||||
|
fin_m = [m for m in margins if np.isfinite(m)]
|
||||||
|
return dict(pnl=tot, n_tr=n_tr, n_skip=n_skip,
|
||||||
|
credit=float(np.mean(credits)) if credits else np.nan,
|
||||||
|
margin=float(np.mean(fin_m)) if fin_m else np.nan)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# MAIN
|
||||||
|
# ===========================================================================
|
||||||
|
def main() -> None:
|
||||||
|
print("=" * 112)
|
||||||
|
print(" R0703 VRPIMP-STRESSLAB — banco di stress di coda per la famiglia short-vol (fisica, non selezione)")
|
||||||
|
print(" ⚠️ BS flat su DVOL-30g (term structure ignorata: MtM in stress SOTTOSTIMATO); banda f obbligatoria;")
|
||||||
|
print(" finestre pre-2021 = SCENARI regression-driven (DVOL non esiste); niente hold-out: nessuna selezione.")
|
||||||
|
print("=" * 112)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- (0) setup + check motore
|
||||||
|
H = {a: full_history(a) for a in ASSETS}
|
||||||
|
vrp_canon = pd.concat(
|
||||||
|
{a[0]: vrp_spread_weekly(a, defined_risk=True, f=1.0, gate_vrp=True,
|
||||||
|
gate_ivr=0.30, crash_skip=0.90) for a in ASSETS},
|
||||||
|
axis=1, join="inner").mean(axis=1)
|
||||||
|
mm = metrics(vrp_canon, 7)
|
||||||
|
print(f"\n(0) CHECK MOTORE: VRP01 canonico riprodotto — ShF {mm['sh']:.2f} / ShH {mm['sh_h']:.2f} / "
|
||||||
|
f"DD {mm['dd']*100:.1f}% (atteso ~1.09/0.59/11.8%)")
|
||||||
|
for a in ASSETS:
|
||||||
|
h = H[a]
|
||||||
|
print(f" {a}: storia 1d {h['idx'][0].date()} → {h['idx'][-1].date()} ({h['n']} g) | "
|
||||||
|
f"DVOL reale da {h['idx'][np.argmax(~np.isnan(h['dvol']))].date()} | "
|
||||||
|
f"premio mediano IV-RV (proxy pre-DVOL) = {h['medvrp']:+.1f} pt")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- (1) relazione empirica DVOL-vs-ritorno
|
||||||
|
print("\n" + "=" * 112)
|
||||||
|
print("(1) RELAZIONE EMPIRICA Δdvol ~ ritorno (piecewise, finestre non sovrapposte, era DVOL 2021-2026)")
|
||||||
|
print("=" * 112)
|
||||||
|
reg = {}
|
||||||
|
for a in ASSETS:
|
||||||
|
reg[a] = dict(w=fit_dvol_reg(a, 7), d=fit_dvol_reg(a, 1))
|
||||||
|
w, d = reg[a]["w"], reg[a]["d"]
|
||||||
|
print(f" {a} weekly (7g, n={w['n']}): Δdvol = {w['a']:+.1f} {w['b_neg']:+.1f}·ret⁻ {w['b_pos']:+.1f}·ret⁺ R²={w['r2']:.2f}")
|
||||||
|
print(f" {a} daily (1g, n={d['n']}): Δdvol = {d['a']:+.1f} {d['b_neg']:+.1f}·ret⁻ {d['b_pos']:+.1f}·ret⁺ R²={d['r2']:.2f}")
|
||||||
|
print("\n Grounding dell'asse IV-spike della matrice (b): moltiplicatore implicito partendo dalla DVOL mediana")
|
||||||
|
for a in ASSETS:
|
||||||
|
J = load_series(a)
|
||||||
|
med = float(np.median(J["dvol"].values))
|
||||||
|
b = reg[a]["d"]["b_neg"]
|
||||||
|
mults = {g: (med + b * g) / med for g in GAPS}
|
||||||
|
mx1 = float((J["dvol"] / J["dvol"].shift(1)).max())
|
||||||
|
mx7 = float((J["dvol"] / J["dvol"].shift(7)).max())
|
||||||
|
print(f" {a}: DVOL mediana {med:.0f} → gap overnight " +
|
||||||
|
" ".join(f"{g*100:+.0f}%→×{m:.2f}" for g, m in mults.items()) +
|
||||||
|
f" | max storico ×{mx1:.2f} (1g), ×{mx7:.2f} (7g)")
|
||||||
|
print(" → ×1.5 e' un crash empiricamente normale, ×2 ≈ il massimo storico settimanale, ×3 = oltre-campione")
|
||||||
|
print(" (stress bound; il lineare della regressione NON estrapola fin li' — dichiarato).")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- (2) replay 10 peggiori finestre
|
||||||
|
print("\n" + "=" * 112)
|
||||||
|
print("(2) REPLAY — 10 peggiori finestre 14g per asset (entry al PICCO, fase 0), f=1.0")
|
||||||
|
print(" perdita per struttura in % del NOTIONAL S0 (somma dei trade nella finestra); G=gate canonico")
|
||||||
|
print("=" * 112)
|
||||||
|
kinds = list(STRUCTS.keys())
|
||||||
|
replay_rows: dict[str, list] = {a: [] for a in ASSETS}
|
||||||
|
for a in ASSETS:
|
||||||
|
h = H[a]
|
||||||
|
wins = worst_windows(h["px"])
|
||||||
|
wins.sort()
|
||||||
|
print(f"\n [{a}] {'finestra':<23} {'ret14':>7} {'IV':>5}" +
|
||||||
|
"".join(f" {k:>9} {k[:5]+'·G':>9}" for k in kinds))
|
||||||
|
for i0 in wins:
|
||||||
|
d0, d1 = h["idx"][i0].date(), h["idx"][min(i0 + 14, h["n"] - 1)].date()
|
||||||
|
ret14 = h["px"][min(i0 + 14, h["n"] - 1)] / h["px"][i0] - 1.0
|
||||||
|
ivtag = "real" if not np.isnan(h["dvol"][i0]) else "synt"
|
||||||
|
cells = {}
|
||||||
|
for k in kinds:
|
||||||
|
off = replay_window(h, reg[a]["w"], i0, k, gated=False)
|
||||||
|
on = replay_window(h, reg[a]["w"], i0, k, gated=True)
|
||||||
|
cells[k] = (off, on)
|
||||||
|
replay_rows[a].append(dict(i0=i0, d0=d0, d1=d1, ret=ret14, iv=ivtag, cells=cells))
|
||||||
|
print(f" {str(d0)}→{str(d1)} {ret14*100:>+6.1f}% {ivtag:>5}" +
|
||||||
|
"".join(f" {cells[k][0]['pnl']*100:>+8.2f}% {cells[k][1]['pnl']*100:>+8.2f}%"
|
||||||
|
for k in kinds))
|
||||||
|
tot_off = {k: sum(r["cells"][k][0]["pnl"] for r in replay_rows[a]) for k in kinds}
|
||||||
|
tot_on = {k: sum(r["cells"][k][1]["pnl"] for r in replay_rows[a]) for k in kinds}
|
||||||
|
ntr = {k: sum(r["cells"][k][1]["n_tr"] for r in replay_rows[a]) for k in kinds}
|
||||||
|
nsk = {k: sum(r["cells"][k][1]["n_skip"] for r in replay_rows[a]) for k in kinds}
|
||||||
|
print(f" {'SOMMA 10 finestre':<38}" +
|
||||||
|
"".join(f" {tot_off[k]*100:>+8.2f}% {tot_on[k]*100:>+8.2f}%" for k in kinds))
|
||||||
|
print(f" {'gate: trade eseguiti / saltati':<38}" +
|
||||||
|
"".join(f" {'':>9} {f'{ntr[k]}/{nsk[k]}':>9}" for k in kinds))
|
||||||
|
|
||||||
|
# validazione IV sintetica sulle finestre DVOL-era
|
||||||
|
print("\n VALIDAZIONE IV SINTETICA (finestre DVOL-era: regressione vs DVOL reale ai punti di trade):")
|
||||||
|
for a in ASSETS:
|
||||||
|
h = H[a]
|
||||||
|
errs = []
|
||||||
|
for r in replay_rows[a]:
|
||||||
|
if r["iv"] != "real":
|
||||||
|
continue
|
||||||
|
i0 = r["i0"]
|
||||||
|
base = h["dvol"][i0]
|
||||||
|
for tt in range(i0, min(i0 + 14, h["n"] - 1)):
|
||||||
|
if np.isnan(h["dvol"][tt]):
|
||||||
|
continue
|
||||||
|
synth = iv_jump(base, h["px"][tt] / h["px"][i0] - 1.0, reg[a]["w"])
|
||||||
|
errs.append(synth - h["dvol"][tt])
|
||||||
|
if errs:
|
||||||
|
e = np.asarray(errs)
|
||||||
|
print(f" {a}: bias {e.mean():+.1f} pt, MAE {np.abs(e).mean():.1f} pt su {len(e)} giorni-crash "
|
||||||
|
f"(la regressione {'SOTTOSTIMA' if e.mean() < 0 else 'sovrastima'} lo spike reale)")
|
||||||
|
|
||||||
|
# banda d'ancora (fase d'ingresso) + banda f, sul totale delle 10 finestre gate-ON
|
||||||
|
print("\n BANDA D'ANCORA (fase d'ingresso 0..tenor-1) e BANDA f — somma 10 finestre, gate ON, per asset:")
|
||||||
|
for a in ASSETS:
|
||||||
|
h = H[a]
|
||||||
|
wins = [r["i0"] for r in replay_rows[a]]
|
||||||
|
for k in ("VRP01", "CONDOR", "DIAG"):
|
||||||
|
tenor = STRUCTS[k]["tenor"]
|
||||||
|
tots = []
|
||||||
|
for p in range(tenor):
|
||||||
|
tots.append(sum(replay_window(h, reg[a]["w"], i0, k, gated=True, phase=p)["pnl"]
|
||||||
|
for i0 in wins))
|
||||||
|
tots = np.asarray(tots) * 100
|
||||||
|
fband = {f: sum(replay_window(h, reg[a]["w"], i0, k, gated=True,
|
||||||
|
f_put=f, f_call=f)["pnl"] for i0 in wins) * 100
|
||||||
|
for f in F_SWEEP}
|
||||||
|
print(f" {a} {k:<7}: ancora med {np.median(tots):+.2f}% [{tots.min():+.2f}, {tots.max():+.2f}] "
|
||||||
|
f"(fase0 {tots[0]:+.2f}%) | f-band " +
|
||||||
|
" ".join(f"f{f}:{v:+.2f}%" for f, v in fband.items()))
|
||||||
|
print(" NB: fase 0 = venduto ESATTAMENTE al picco (worst-case timing). Le altre fasi entrano a crash")
|
||||||
|
print(" iniziato: il gate crash-skip (ivr>0.90) e la IV piu' alta (strike piu' larghi) attutiscono.")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- (3) matrice sintetica
|
||||||
|
print("\n" + "=" * 112)
|
||||||
|
print("(3) MATRICE SINTETICA — gap × IV-spike × timing; entry a DVOL mediana; f=1.0 (banda f in (5))")
|
||||||
|
print(" unita': ×credito-medio-storico-gated | %acct = % del conto a sizing 12% margine-deployed")
|
||||||
|
print(" (nudi: margine non definito → %acct in convenzione cash-secured sul collaterale, dichiarato)")
|
||||||
|
print("=" * 112)
|
||||||
|
# credito medio storico (gated, f=1.0) per normalizzare
|
||||||
|
print("\n CREDITO MEDIO storico (gated canonico, f=1.0, frazione di S0) e margine tipico:")
|
||||||
|
avg_credit: dict[str, dict[str, float]] = {a: {} for a in ASSETS}
|
||||||
|
avg_margin: dict[str, dict[str, float]] = {a: {} for a in ASSETS}
|
||||||
|
for a in ASSETS:
|
||||||
|
h = H[a]
|
||||||
|
for k in kinds:
|
||||||
|
tenor = STRUCTS[k]["tenor"]
|
||||||
|
creds, margs = [], []
|
||||||
|
t = 60
|
||||||
|
while t + tenor < h["n"]:
|
||||||
|
if np.isnan(h["dvol"][t]):
|
||||||
|
t += tenor
|
||||||
|
continue
|
||||||
|
sig0 = h["dvol"][t]
|
||||||
|
rv = h["rv30"][t]
|
||||||
|
hist = h["hist_iv"][30:t]
|
||||||
|
ivr = float((hist < sig0).mean()) if len(hist) >= 60 else np.nan
|
||||||
|
skip = ((not np.isnan(rv) and (sig0 / 100.0 - rv) <= 0)
|
||||||
|
or (not np.isnan(ivr) and (ivr < 0.30 or ivr > 0.90)))
|
||||||
|
if not skip:
|
||||||
|
r = trade_pnl(k, h["px"][t], sig0 / 100.0, h["px"][t], sig0 / 100.0, tenor,
|
||||||
|
z=ALB["z"], dz=ALB["dz"])
|
||||||
|
creds.append(r["credit"]); margs.append(r["margin"])
|
||||||
|
t += tenor
|
||||||
|
avg_credit[a][k] = float(np.mean(creds))
|
||||||
|
fin_m = [m for m in margs if np.isfinite(m)]
|
||||||
|
avg_margin[a][k] = float(np.mean(fin_m)) if fin_m else np.nan
|
||||||
|
print(f" {a}: " + " | ".join(
|
||||||
|
f"{k} cr {avg_credit[a][k]*1e4:.0f}bps"
|
||||||
|
+ (f" mg {avg_margin[a][k]*100:.1f}%" if np.isfinite(avg_margin[a][k]) else " mg n/a")
|
||||||
|
for k in kinds))
|
||||||
|
|
||||||
|
matrix: dict[tuple, dict] = {}
|
||||||
|
for a in ASSETS:
|
||||||
|
J = load_series(a)
|
||||||
|
med_iv = float(np.median(J["dvol"].values)) / 100.0
|
||||||
|
primary = a == "ETH"
|
||||||
|
if primary:
|
||||||
|
print(f"\n [{a}] PERDITA A SCADENZA (S resta al livello del gap; IV-spike entra solo nel mark T+1 del DIAG)")
|
||||||
|
print(f" {'gap':>5} {'IVx':>4}" + "".join(f" | {k:>16}" for k in kinds))
|
||||||
|
for g in GAPS:
|
||||||
|
for m in IVMULT:
|
||||||
|
row = {}
|
||||||
|
for k in kinds:
|
||||||
|
tenor = STRUCTS[k]["tenor"]
|
||||||
|
r = trade_pnl(k, 1.0, med_iv, 1.0 + g, med_iv * m, tenor,
|
||||||
|
z=ALB["z"], dz=ALB["dz"])
|
||||||
|
mg = avg_margin[a][k]
|
||||||
|
acct = (r["pnl"] / mg if np.isfinite(mg) else r["pnl"] / r["ks_frac"]) * SIZING
|
||||||
|
row[k] = dict(pnl=r["pnl"], xc=r["pnl"] / avg_credit[a][k], acct=acct)
|
||||||
|
matrix[(a, g, m)] = row
|
||||||
|
if primary:
|
||||||
|
print(f" {g*100:>+4.0f}% {m:>4.1f}" + "".join(
|
||||||
|
f" | {row[k]['xc']:>+6.1f}c {row[k]['acct']*100:>+6.2f}%" for k in kinds))
|
||||||
|
if not primary:
|
||||||
|
print(f"\n [{a}] (compatto — solo gap -30%): " + " | ".join(
|
||||||
|
f"{k} {matrix[(a, -0.30, 2.0)][k]['xc']:+.1f}c/{matrix[(a, -0.30, 2.0)][k]['acct']*100:+.2f}%acct"
|
||||||
|
for k in kinds))
|
||||||
|
print("\n Lettura: 'c' = multipli del credito medio; %acct = perdita sul CONTO a sizing 12%.")
|
||||||
|
print(" A scadenza il timing non cambia S1: la dimensione timing vive nel MtM qui sotto.")
|
||||||
|
|
||||||
|
print("\n MARK-TO-MARKET AL GIORNO DEL GAP (stress di margine; ETH, IV mediana, f=1.0)")
|
||||||
|
print(f" {'gap':>5} {'IVx':>4} {'timing':>10}" + "".join(f" | {k:>14}" for k in kinds))
|
||||||
|
J = load_series("ETH")
|
||||||
|
med_iv_e = float(np.median(J["dvol"].values)) / 100.0
|
||||||
|
for g in GAPS:
|
||||||
|
for m in IVMULT:
|
||||||
|
for lbl, tg in (("overnight", 1), ("intra-week", None)):
|
||||||
|
cells = []
|
||||||
|
for k in kinds:
|
||||||
|
tenor = STRUCTS[k]["tenor"]
|
||||||
|
t_gap = tg if tg is not None else max(tenor - 2, 1)
|
||||||
|
v = trade_mtm(k, med_iv_e, g, m, t_gap, tenor, z=ALB["z"], dz=ALB["dz"])
|
||||||
|
cells.append(v / avg_credit["ETH"][k])
|
||||||
|
if m == 2.0 or g == -0.30: # stampa compatta: x2 sempre, -30% tutte
|
||||||
|
print(f" {g*100:>+4.0f}% {m:>4.1f} {lbl:>10}" +
|
||||||
|
"".join(f" | {c:>+12.1f}c" for c in cells))
|
||||||
|
print(" Lettura: MtM overnight ≪ MtM intra-week (piu' tempo residuo + vega). Per il defined-risk il")
|
||||||
|
print(" MtM NON e' la perdita realizzata (a scadenza vale la tabella sopra) ma e' il margine richiesto")
|
||||||
|
print(" per TENERE la posizione — e con BS-flat e' pure sottostimato (front-end IV esplode).")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- (4) valore dell'ala
|
||||||
|
print("\n" + "=" * 112)
|
||||||
|
print("(4) VALORE DELL'ALA — drag storico vs protezione di coda + NULL DEL DE-LEVERING")
|
||||||
|
print("=" * 112)
|
||||||
|
# coppie (base, feature): l'ala far-OTM di VRP01; l'ala far-OTM dello strangle; l'ala T+1; l'ala vicina
|
||||||
|
print("\n DRAG STORICO (gated canonico, f=1.0, book 50/50 BTC+ETH; rendimenti per-periodo):")
|
||||||
|
hist: dict[str, pd.Series] = {}
|
||||||
|
hist["NAKED28"] = pd.concat(
|
||||||
|
{a[0]: vrp_spread_weekly(a, defined_risk=False, f=1.0, gate_vrp=True,
|
||||||
|
gate_ivr=0.30, crash_skip=0.90) for a in ASSETS},
|
||||||
|
axis=1, join="inner").mean(axis=1)
|
||||||
|
hist["VRP01"] = vrp_canon
|
||||||
|
hist["STRANGLE"] = book("condor", z=ALB["z"], dz=8.0, tenor_d=ALB["tenor_d"], gated=True)
|
||||||
|
hist["VERT"] = book("vert", z=ALB["z"], dz=ALB["dz"], tenor_d=ALB["tenor_d"], gated=True)
|
||||||
|
hist["CONDOR"] = book("condor", z=ALB["z"], dz=ALB["dz"], tenor_d=ALB["tenor_d"], gated=True)
|
||||||
|
hist["DIAG"] = book("diag", z=ALB["z"], dz=ALB["dz"], tenor_d=ALB["tenor_d"], gated=True)
|
||||||
|
hist["CONDOR05"] = book("condor", z=ALB["z"], dz=0.5, tenor_d=ALB["tenor_d"], gated=True)
|
||||||
|
|
||||||
|
def annual_bps(s: pd.Series, tenor: int) -> float:
|
||||||
|
return float(s.mean() * (365.25 / tenor) * 1e4)
|
||||||
|
|
||||||
|
for k, tn in (("NAKED28", 7), ("VRP01", 7), ("STRANGLE", 5), ("VERT", 5),
|
||||||
|
("CONDOR", 5), ("DIAG", 5), ("CONDOR05", 5)):
|
||||||
|
m = metrics(hist[k], tn)
|
||||||
|
print(f" {k:<9} Sh {m['sh']:>5.2f} | media aritm {annual_bps(hist[k], tn):>+7.0f} bps/anno | "
|
||||||
|
f"worst {m['worst']*100:>+6.2f}% | DD {m['dd']*100:>5.1f}%")
|
||||||
|
|
||||||
|
features = [
|
||||||
|
("ala far-OTM VRP01 (Δ-0.10)", "NAKED28", "VRP01", 7),
|
||||||
|
("ala far-OTM strangle (z+1σ)", "STRANGLE", "CONDOR", 5),
|
||||||
|
("ala T+1 (diag vs condor)", "CONDOR", "DIAG", 5),
|
||||||
|
("ala VICINA (dz 0.5 vs 1.0)", "CONDOR", "CONDOR05", 5),
|
||||||
|
]
|
||||||
|
# kind/dz effettivi per il pricing di cella (CONDOR05 = condor con ala a +0.5σ)
|
||||||
|
featmap = {"NAKED28": ("NAKED28", ALB["dz"]), "VRP01": ("VRP01", ALB["dz"]),
|
||||||
|
"STRANGLE": ("STRANGLE", ALB["dz"]), "VERT": ("VERT", ALB["dz"]),
|
||||||
|
"CONDOR": ("CONDOR", ALB["dz"]), "DIAG": ("DIAG", ALB["dz"]),
|
||||||
|
"CONDOR05": ("CONDOR", 0.5)}
|
||||||
|
|
||||||
|
def eth_cell(tag: str, g: float, m: float = 2.0) -> float:
|
||||||
|
kind, dz = featmap[tag]
|
||||||
|
tenor = STRUCTS[kind]["tenor"]
|
||||||
|
return trade_pnl(kind, 1.0, med_iv_e, 1.0 + g, med_iv_e * m, tenor,
|
||||||
|
z=ALB["z"], dz=dz)["pnl"]
|
||||||
|
print("\n DECOMPOSIZIONE calm-drag vs tail-benefit (per-trade, date comuni, f=1.0):")
|
||||||
|
feat_stats = {}
|
||||||
|
for name, b, f_, tn in features:
|
||||||
|
JJ = pd.concat({"b": hist[b], "f": hist[f_]}, axis=1, join="inner").dropna()
|
||||||
|
act = JJ[(JJ["b"] != 0) | (JJ["f"] != 0)]
|
||||||
|
diff = act["f"] - act["b"]
|
||||||
|
q05 = act["b"].quantile(0.05)
|
||||||
|
tail = diff[act["b"] <= q05]
|
||||||
|
calm = diff[act["b"] > q05]
|
||||||
|
tpy = len(act) / (len(JJ) * tn / 365.25)
|
||||||
|
drag_yr = annual_bps(hist[f_], tn) - annual_bps(hist[b], tn)
|
||||||
|
feat_stats[name] = dict(drag_yr=drag_yr, calm=float(calm.mean() * 1e4),
|
||||||
|
tail=float(tail.mean() * 1e4), base=b, feat=f_, tn=tn)
|
||||||
|
print(f" {name:<28}: drag netto {drag_yr:>+6.0f} bps/anno | calm {calm.mean()*1e4:>+6.1f} bps/trade "
|
||||||
|
f"| tail(5% peggiori) {tail.mean()*1e4:>+7.1f} bps/trade | ~{tpy:.0f} trade attivi/anno")
|
||||||
|
|
||||||
|
print("\n PROTEZIONE COMPRATA NELLA MATRICE (ETH, a scadenza, bps di S0 risparmiati; celle -15/-20/-30, IVx2):")
|
||||||
|
print(f" {'feature':<28}" + "".join(f" {f'gap{g*100:+.0f}%':>10}" for g in (-0.15, -0.20, -0.30)) +
|
||||||
|
f" {'drag/anno':>10} {'protez/drag @-30':>17}")
|
||||||
|
for name, b, f_, tn in features:
|
||||||
|
prot = {}
|
||||||
|
for g in (-0.15, -0.20, -0.30):
|
||||||
|
lb = eth_cell(b, g)
|
||||||
|
lf = eth_cell(f_, g)
|
||||||
|
prot[g] = (lf - lb) * 1e4 # bps di S0 risparmiati (>0 = l'ala protegge)
|
||||||
|
drg = feat_stats[name]["drag_yr"]
|
||||||
|
ratio = prot[-0.30] / abs(drg) if drg < 0 else float("inf")
|
||||||
|
feat_stats[name]["prot"] = prot
|
||||||
|
feat_stats[name]["ratio"] = ratio
|
||||||
|
rtag = f"{ratio:>8.1f}x" if np.isfinite(ratio) else " GRATIS"
|
||||||
|
print(f" {name:<28}" + "".join(f" {prot[g]:>+9.0f}" for g in (-0.15, -0.20, -0.30)) +
|
||||||
|
f" {drg:>+9.0f} {rtag:>17}")
|
||||||
|
|
||||||
|
print("\n NULL DEL DE-LEVERING (regola standing #3), DUE LETTURE ONESTE:")
|
||||||
|
print(" (i) RITORNO a pari perdita-di-cella: de-lever della base a k = L_ala/L_base costa (1-k)·ritorno;")
|
||||||
|
print(" l'ala vale se il suo drag e' minore. (ii) LETTERA della regola (Sharpe): il de-levering")
|
||||||
|
print(" PRESERVA lo Sharpe della base — se Sh(base) >= Sh(ala), il de-lever vince nominalmente.")
|
||||||
|
print(f" {'feature':<28} {'k(-30%)':>8} {'k(-20%)':>8} {'delever':>9} {'ala':>7} "
|
||||||
|
f"{'Sh base':>8} {'Sh ala':>7} {'verdetto ritorno':>17} {'verdetto Sharpe':>16}")
|
||||||
|
for name, b, f_, tn in features:
|
||||||
|
k30 = eth_cell(f_, -0.30) / eth_cell(b, -0.30)
|
||||||
|
k20 = eth_cell(f_, -0.20) / eth_cell(b, -0.20)
|
||||||
|
k = max(k30, k20) # de-lever deve coprire la cella PEGGIO protetta
|
||||||
|
base_ret = annual_bps(hist[b], tn)
|
||||||
|
cost_del = (1.0 - min(k30, 1.0)) * base_ret
|
||||||
|
cost_f = -feat_stats[name]["drag_yr"] # >0 = l'ala costa
|
||||||
|
sh_b = metrics(hist[b], tn)["sh"]
|
||||||
|
sh_f = metrics(hist[f_], tn)["sh"]
|
||||||
|
if cost_f <= 0:
|
||||||
|
v_ret = "ALA GRATIS"
|
||||||
|
elif cost_f < cost_del:
|
||||||
|
v_ret = "ala vince"
|
||||||
|
else:
|
||||||
|
v_ret = "delever vince"
|
||||||
|
v_sh = "ala vince" if sh_f > sh_b else "delever vince"
|
||||||
|
feat_stats[name]["survives_ret"] = (cost_f <= 0) or (cost_f < cost_del)
|
||||||
|
feat_stats[name]["survives_sh"] = sh_f > sh_b
|
||||||
|
print(f" {name:<28} {k30:>8.2f} {k20:>8.2f} {cost_del:>+8.0f}b {cost_f:>+6.0f}b "
|
||||||
|
f"{sh_b:>8.2f} {sh_f:>7.2f} {v_ret:>17} {v_sh:>16}")
|
||||||
|
print(" ⚠️ CAVEAT sulla lettura Sharpe: lo Sharpe in-sample della struttura SENZA ala e' tail-uncapped")
|
||||||
|
print(" — e' alto proprio perche' la cella -30% overnight non e' mai occorsa piena nel campione 2021-26")
|
||||||
|
print(" (il punto cieco CC01 'Sharpe implausibile'). La lettura (i) a pari perdita-di-cella e' quella")
|
||||||
|
print(" che prezza le celle FUORI campione; la (ii) e' la lettera della regola. Riportate entrambe.")
|
||||||
|
print(" NB a favore dell'ala (oltre il numero): il defined-risk mette un LIMITE RIGIDO oltre la cella")
|
||||||
|
print(" (-50%, -70%...) che il de-levering non mette mai; e a parita' di margine Deribit l'ala LIBERA")
|
||||||
|
print(" capitale. NB contro: il drag e' misurato IN-SAMPLE su un'era (2021-26) che le code le ha viste")
|
||||||
|
print(" (LUNA/FTX) — in un'era senza code il drag sale e il tail-benefit non si incassa.")
|
||||||
|
|
||||||
|
# banda f sul verdetto ala (chiave: drag e protezione a f 0.6/1.3)
|
||||||
|
print("\n BANDA f sul valore dell'ala T+1 e dell'ala far-OTM (drag bps/anno | protezione bps @-30%×2):")
|
||||||
|
for f in F_SWEEP:
|
||||||
|
nk = pd.concat({a[0]: vrp_spread_weekly(a, defined_risk=False, f=f, gate_vrp=True,
|
||||||
|
gate_ivr=0.30, crash_skip=0.90) for a in ASSETS},
|
||||||
|
axis=1, join="inner").mean(axis=1)
|
||||||
|
sp = pd.concat({a[0]: vrp_spread_weekly(a, defined_risk=True, f=f, gate_vrp=True,
|
||||||
|
gate_ivr=0.30, crash_skip=0.90) for a in ASSETS},
|
||||||
|
axis=1, join="inner").mean(axis=1)
|
||||||
|
co = book("condor", z=ALB["z"], dz=ALB["dz"], tenor_d=ALB["tenor_d"], gated=True, f_put=f, f_call=f)
|
||||||
|
di = book("diag", z=ALB["z"], dz=ALB["dz"], tenor_d=ALB["tenor_d"], gated=True, f_put=f, f_call=f)
|
||||||
|
drag_v = annual_bps(sp, 7) - annual_bps(nk, 7)
|
||||||
|
drag_d = annual_bps(di, 5) - annual_bps(co, 5)
|
||||||
|
pv = (trade_pnl("VRP01", 1.0, med_iv_e, 0.70, med_iv_e * 2, 7, f_put=f)["pnl"]
|
||||||
|
- trade_pnl("NAKED28", 1.0, med_iv_e, 0.70, med_iv_e * 2, 7, f_put=f)["pnl"]) * 1e4
|
||||||
|
pdg = (trade_pnl("DIAG", 1.0, med_iv_e, 0.70, med_iv_e * 2, 5, f_put=f, f_call=f)["pnl"]
|
||||||
|
- trade_pnl("CONDOR", 1.0, med_iv_e, 0.70, med_iv_e * 2, 5, f_put=f, f_call=f)["pnl"]) * 1e4
|
||||||
|
print(f" f={f}: ala VRP01 drag {drag_v:+.0f} prot {pv:+.0f} | ala T+1 drag {drag_d:+.0f} prot {pdg:+.0f}")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- (5) worst-case EUR a 2k/5k
|
||||||
|
print("\n" + "=" * 112)
|
||||||
|
print("(5) WORST-CASE ONESTO del VRP01 attuale a 2k / 5k (EUR; convenzione diario $≈€)")
|
||||||
|
print("=" * 112)
|
||||||
|
# margine spread ETH a IV mediana 1y (granularita' reale: spread interi da 1 ETH)
|
||||||
|
dfe = al.get("ETH", "1d")
|
||||||
|
S_eth = float(dfe["close"].iloc[-1])
|
||||||
|
dv_e = pd.read_parquet(ROOT / "data" / "raw" / "dvol_eth.parquet")
|
||||||
|
iv1y = float(dv_e["close"].iloc[-365:].median()) / 100.0
|
||||||
|
T7 = 7.0 / 365.25
|
||||||
|
Ks = strike_from_delta(S_eth, T7, iv1y, -0.28)
|
||||||
|
Kl = strike_from_delta(S_eth, T7, iv1y, -0.10)
|
||||||
|
fband_credit = {}
|
||||||
|
for f in F_SWEEP:
|
||||||
|
cr = (bs_put(S_eth, Ks, T7, iv1y) - bs_put(S_eth, Kl, T7, iv1y)) * f
|
||||||
|
fee2 = (min(0.0003 * S_eth, 0.125 * bs_put(S_eth, Ks, T7, iv1y) * f)
|
||||||
|
+ min(0.0003 * S_eth, 0.125 * bs_put(S_eth, Kl, T7, iv1y) * f))
|
||||||
|
fband_credit[f] = dict(credit=cr, margin=(Ks - Kl) - cr, fee=fee2)
|
||||||
|
print(f"\n Spread ETH 1x (spot ${S_eth:,.0f}, DVOL mediana 1y {iv1y*100:.0f}%): strike {Ks:,.0f}/{Kl:,.0f}")
|
||||||
|
for f in F_SWEEP:
|
||||||
|
d = fband_credit[f]
|
||||||
|
print(f" f={f}: credito ${d['credit']:.2f} | margine/max-loss ${d['margin']:.2f} | fee 2 gambe ${d['fee']:.2f}")
|
||||||
|
worst_repl = {}
|
||||||
|
for a in ASSETS:
|
||||||
|
worst_repl[a] = min(r["cells"]["VRP01"][1]["pnl"] for r in replay_rows[a]) # gate ON
|
||||||
|
wr_book = float(np.mean([worst_repl[a] for a in ASSETS]))
|
||||||
|
print(f"\n {'capitale':>9} {'sleeve12%':>10} {'n spread ETH':>13} "
|
||||||
|
f"{'WC fisico (margine-deployed)':>36} {'WC cella -30% (book conv.)':>27} {'peggior 14g replay (gate)':>26}")
|
||||||
|
for C in (2000.0, 5000.0):
|
||||||
|
sleeve = SIZING * C
|
||||||
|
rows_f = []
|
||||||
|
for f in F_SWEEP:
|
||||||
|
d = fband_credit[f]
|
||||||
|
n_sp = int(sleeve // d["margin"])
|
||||||
|
wc_phys = n_sp * (d["margin"] + d["fee"])
|
||||||
|
rows_f.append((f, n_sp, wc_phys))
|
||||||
|
# convenzione book (cash-secured su Ks, come lo sleeve compone nel portafoglio)
|
||||||
|
r30 = trade_pnl("VRP01", 1.0, iv1y, 0.70, iv1y * 2, 7)
|
||||||
|
wc_book_eur = r30["pnl"] / r30["ks_frac"] * SIZING * C
|
||||||
|
wr_eur = wr_book / r30["ks_frac"] * SIZING * C # replay pnl frac S0 → su collaterale Ks
|
||||||
|
base = next(x for x in rows_f if x[0] == 1.0)
|
||||||
|
print(f" {C:>9.0f} {sleeve:>10.0f} {base[1]:>13d} "
|
||||||
|
f"{'-€%.0f' % base[2]:>13} [f-band -€{min(x[2] for x in rows_f):.0f}..-€{max(x[2] for x in rows_f):.0f}]"
|
||||||
|
f" {'-€%.1f' % abs(wc_book_eur):>20} {'-€%.1f' % abs(wr_eur):>20}")
|
||||||
|
print("\n Lettura (i tre numeri sono TRE domande diverse):")
|
||||||
|
print(" - WC FISICO margine-deployed: se il 12% del conto e' TUTTO margine di spread e il gap -30% li")
|
||||||
|
print(" manda full-ITM, si perde il margine intero + fee = ~il 12% del conto. E' il bound rigido del")
|
||||||
|
print(" defined-risk: a 2k ≈ -€240, a 5k ≈ -€600. Nessun modello puo' peggiorarlo (skew incluso).")
|
||||||
|
print(" - WC alla cella -30% in CONVENZIONE BOOK (cash-secured su Ks, come lo sleeve compone nel")
|
||||||
|
print(" portafoglio): la perdita che il book REGISTRAerebbe quella settimana a sizing 12%.")
|
||||||
|
print(" - Peggior 14g del replay storico (gate ON): il worst-case EMPIRICO osservato/simulato, che il")
|
||||||
|
print(" gate canonico attenua saltando i re-entry a IV-rank>0.90.")
|
||||||
|
|
||||||
|
print("\n" + "=" * 112)
|
||||||
|
print(" CONCLUSIONI (fisica, non selezione — nessun nuovo sleeve, regola standing invariata)")
|
||||||
|
print("=" * 112)
|
||||||
|
for name, st in feat_stats.items():
|
||||||
|
r = "ritorno:OK" if st.get("survives_ret") else "ritorno:NO"
|
||||||
|
s = "sharpe:OK" if st.get("survives_sh") else "sharpe:NO"
|
||||||
|
print(f" - {name:<28}: drag {st['drag_yr']:+.0f} bps/anno, protezione @-30% {st['prot'][-0.30]:+.0f} bps "
|
||||||
|
f"→ null de-levering [{r}, {s}]")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
"""R0703 VRPIMP-STRUCTGRID — griglia ONESTA di struttura short-vol vs VRP01 canonico.
|
||||||
|
|
||||||
|
FILONE 1 (2026-07-03). L'audit ALB-A (r0702_alb_structure) ha testato UNA cella a priori
|
||||||
|
(z=2.0σ, ali +1σ, tenor 5g). Domanda: nella griglia onesta
|
||||||
|
distanza z ∈ {1.0, 1.5, 2.0, 2.5}σ × tenor ∈ {3, 5, 7, 10}g × ali dz ∈ {+0.5, +1.0, +2.0}σ
|
||||||
|
× lato {put-only, call-only, entrambi} × struttura {vertical, condor, diagonal T+1}
|
||||||
|
esiste una cella che batte VRP01 canonico (put credit spread settimanale Δ-0.28/-0.10, gate
|
||||||
|
IV-rank) in modo che SOPRAVVIVE a (a) selezione IN-SAMPLE-ONLY, (b) deflated-Sharpe sul numero
|
||||||
|
TOTALE di celle, (c) banda f, (d) plateau dei vicini, (e) null del de-levering?
|
||||||
|
|
||||||
|
MAPPATURA lato×struttura (9 combo nominali -> 6 DISTINTE, per non contare doppio):
|
||||||
|
vertical×put = VERT-PUT vertical×call = VERT-CALL vertical×both ≡ CONDOR
|
||||||
|
condor×put ≡ VERT-PUT condor×call ≡ VERT-CALL condor×both = CONDOR
|
||||||
|
diagonal×{put,call,both} = DIAG-PUT / DIAG-CALL / DIAG-2 (Albimarini)
|
||||||
|
Totale celle = 4z × 4tenor × 3dz × 6 strutture = 288 (tutte contate nel deflated-Sharpe).
|
||||||
|
|
||||||
|
REGOLE RISPETTATE:
|
||||||
|
- Gate IV-rank CANONICO sempre attivo, NON riottimizzato (vrp>0 AND 0.30<=ivr<=0.90,
|
||||||
|
identico a VRP01 combo e a r0702_alb_structure gated=True).
|
||||||
|
- Selezione cella SOLO su Sharpe PRE-holdout (2021-03 .. 2024-12); hold-out 2025-26 mai
|
||||||
|
guardato per scegliere. DSR (Bailey & LdP, altlib.deflated_sharpe) su tutte le 288 celle.
|
||||||
|
- Pattern "0-perdite = Sharpe implausibile" (CC01, ALB-A): le celle con < MIN_LOSS_IS
|
||||||
|
perdite attive in-sample sono ESCLUSE DALLA SELEZIONE E DICHIARATE (coda mai campionata
|
||||||
|
-> Sharpe non informativo). Il criterio usa SOLO dati in-sample (niente peek).
|
||||||
|
- Banda f {0.6, 0.8, 1.0, 1.3} + scenario skew (f_put=1.3 / f_call=0.7) su ogni claim
|
||||||
|
(pricing BS FLAT su DVOL-30g: il deep-OTM e' banda, non stima puntuale).
|
||||||
|
- Banda d'ancora (regola 2026-07-02): la cella scelta si riporta su TUTTE le fasi di
|
||||||
|
partenza 0..tenor-1 (il costrutto e' ancorato alla cadenza tenor_d).
|
||||||
|
- NULL DEL DE-LEVERING (lezione TP01×DVOL): ogni vantaggio di DD della cella si confronta
|
||||||
|
con VRP01 semplicemente scalato allo stesso DD (Sharpe invariante per scala).
|
||||||
|
- Confronto apples-to-apples: oltre a VRP01 canonico (motore options_vrp_v2, capitale=K_short,
|
||||||
|
fee=12.5% del premio) si riporta il BRIDGE (stessa struttura di VRP01 nel motore nuovo:
|
||||||
|
vert-put z=0.583 dz=0.699 7g, capitale=S0, fee per-gamba) — battere solo VRP01 canonico
|
||||||
|
ma non il bridge = artefatto di convenzione fee/capitale, non struttura.
|
||||||
|
|
||||||
|
MACCHINERIA RIUSATA (non riscritta): motore di r0702_alb_structure (bs_put/bs_call/_fee_frac/
|
||||||
|
metrics/to_daily_lumped, VALIDATO vs run_structure cella-per-cella in testa al run),
|
||||||
|
options_vrp_v2.vrp_spread_weekly (VRP01 canonico esatto), altlib.deflated_sharpe/marginal_vs_tp01.
|
||||||
|
|
||||||
|
Regola standing: NIENTE short-vol da modello in deploy — l'esito massimo e' conoscenza/LEAD.
|
||||||
|
|
||||||
|
uv run python scripts/research/r0703_vrpimp_structgrid.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt"))
|
||||||
|
import numpy as np, pandas as pd # noqa: E402
|
||||||
|
from scipy.stats import norm # noqa: E402
|
||||||
|
|
||||||
|
from scripts.research.options_vrp_lab import bs_put, load_series, per_year # noqa: E402
|
||||||
|
from scripts.research.options_vrp_v2 import vrp_spread_weekly, _ivrank, _rv30 # noqa: E402
|
||||||
|
from scripts.research.r0702_alb_structure import ( # noqa: E402
|
||||||
|
bs_call, _fee_frac, run_structure, metrics, to_daily_lumped, DAY, HOLDOUT)
|
||||||
|
import altlib as al # noqa: E402
|
||||||
|
|
||||||
|
SCRATCH = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/e00896d3-d4bb-4f2a-b471-55a1d88a12ba/scratchpad")
|
||||||
|
|
||||||
|
F_SWEEP = (0.6, 0.8, 1.0, 1.3)
|
||||||
|
F_SKEW = dict(f_put=1.3, f_call=0.7) # skew crypto-shaped: put ricche, call povere
|
||||||
|
Z_GRID = (1.0, 1.5, 2.0, 2.5)
|
||||||
|
TENOR_GRID = (3, 5, 7, 10)
|
||||||
|
DZ_GRID = (0.5, 1.0, 2.0)
|
||||||
|
STRUCTS = (("same", "put", "VERT-PUT"), ("same", "call", "VERT-CALL"), ("same", "both", "CONDOR"),
|
||||||
|
("diag", "put", "DIAG-PUT"), ("diag", "call", "DIAG-CALL"), ("diag", "both", "DIAG-2"))
|
||||||
|
MIN_ACT_IS = 30 # trade attivi minimi in-sample per credere allo Sharpe IS
|
||||||
|
MIN_LOSS_IS = 3 # perdite attive minime IS: <3 = coda mai campionata (CC01/ALB-A) -> ESCLUSA
|
||||||
|
CUTS = ("2022-01-01", "2023-01-01", "2024-01-01", "2025-01-01") # persistenza multi-cut
|
||||||
|
|
||||||
|
# bridge = la struttura ESATTA di VRP01 dentro il motore nuovo (delta -0.28/-0.10, 7g)
|
||||||
|
Z_BRIDGE = float(-norm.ppf(0.28)) # ~0.583
|
||||||
|
DZ_BRIDGE = float(-norm.ppf(0.10)) - Z_BRIDGE # ~0.699
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- dati + gate cache
|
||||||
|
_S: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
def S(asset):
|
||||||
|
"""(px, dv, idx, skip) con gate canonico VRP01 precomputato (identico a r0702/options_vrp_v2)."""
|
||||||
|
if asset not in _S:
|
||||||
|
J = load_series(asset)
|
||||||
|
px = J["px"].values.astype(float)
|
||||||
|
dv = (J["dvol"].values / 100.0).astype(float)
|
||||||
|
n = len(px)
|
||||||
|
skip = np.zeros(n, dtype=bool)
|
||||||
|
for i in range(n):
|
||||||
|
rv = _rv30(px, i)
|
||||||
|
ivr = _ivrank(dv, i)
|
||||||
|
skip[i] = ((not np.isnan(rv) and (dv[i] - rv) <= 0)
|
||||||
|
or (not np.isnan(ivr) and (ivr < 0.30 or ivr > 0.90)))
|
||||||
|
_S[asset] = (px, dv, J.index, skip)
|
||||||
|
return _S[asset]
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- motore generalizzato
|
||||||
|
def run_cell(asset, expiry, side, z, dz, tenor_d, f_put=1.0, f_call=1.0, offset=0):
|
||||||
|
"""Vendita sistematica non-overlapping della struttura, cadenza tenor_d, GATE CANONICO SEMPRE.
|
||||||
|
|
||||||
|
expiry: 'same' (long stessa scadenza) | 'diag' (long T+1g, mark BS a exit — vega)
|
||||||
|
side: 'put' | 'call' | 'both'
|
||||||
|
Identica contabilita' di r0702_alb_structure.run_structure (validata sotto): capitale = S0,
|
||||||
|
fee Deribit per gamba 0.03% notional cap 12.5% premio, delivery 0.015% su short ITM, exit fee
|
||||||
|
sulle long residue del diag. offset = fase d'ancora (banda d'ancora)."""
|
||||||
|
px, dv, idx, skip = S(asset)
|
||||||
|
n = len(px)
|
||||||
|
T = tenor_d / 365.25
|
||||||
|
T_long = T + DAY if expiry == "diag" else T
|
||||||
|
rets = {}
|
||||||
|
i = 60 + offset
|
||||||
|
while i + tenor_d < n:
|
||||||
|
j = i + tenor_d
|
||||||
|
if skip[i]:
|
||||||
|
rets[idx[j]] = 0.0
|
||||||
|
i = j
|
||||||
|
continue
|
||||||
|
S0 = px[i]; sig = dv[i]; m = sig * math.sqrt(T)
|
||||||
|
S1 = px[j]; sig1 = dv[j]
|
||||||
|
credit = short_pay = long_val = exit_fee = deliv = 0.0
|
||||||
|
legs = []
|
||||||
|
if side in ("put", "both"):
|
||||||
|
Ks = S0 * math.exp(-z * m); Kl = S0 * math.exp(-(z + dz) * m)
|
||||||
|
ps = bs_put(S0, Ks, T, sig) / S0 * f_put
|
||||||
|
pl = bs_put(S0, Kl, T_long, sig) / S0 * f_put
|
||||||
|
credit += ps - pl; legs += [ps, pl]
|
||||||
|
short_pay += max(0.0, Ks - S1) / S0
|
||||||
|
deliv += _fee_frac(max(0.0, Ks - S1) / S0, rate=0.00015)
|
||||||
|
if expiry == "diag":
|
||||||
|
lv = bs_put(S1, Kl, DAY, sig1) / S0 * f_put
|
||||||
|
long_val += lv
|
||||||
|
exit_fee += _fee_frac(lv, notional_ratio=S1 / S0)
|
||||||
|
else:
|
||||||
|
long_val += max(0.0, Kl - S1) / S0
|
||||||
|
if side in ("call", "both"):
|
||||||
|
Ks = S0 * math.exp(+z * m); Kl = S0 * math.exp(+(z + dz) * m)
|
||||||
|
cs = bs_call(S0, Ks, T, sig) / S0 * f_call
|
||||||
|
cl = bs_call(S0, Kl, T_long, sig) / S0 * f_call
|
||||||
|
credit += cs - cl; legs += [cs, cl]
|
||||||
|
short_pay += max(0.0, S1 - Ks) / S0
|
||||||
|
deliv += _fee_frac(max(0.0, S1 - Ks) / S0, rate=0.00015)
|
||||||
|
if expiry == "diag":
|
||||||
|
lv = bs_call(S1, Kl, DAY, sig1) / S0 * f_call
|
||||||
|
long_val += lv
|
||||||
|
exit_fee += _fee_frac(lv, notional_ratio=S1 / S0)
|
||||||
|
else:
|
||||||
|
long_val += max(0.0, S1 - Kl) / S0
|
||||||
|
entry_fee = sum(_fee_frac(v) for v in legs)
|
||||||
|
rets[idx[j]] = credit - short_pay + long_val - entry_fee - exit_fee - deliv
|
||||||
|
i = j
|
||||||
|
return pd.Series(rets)
|
||||||
|
|
||||||
|
|
||||||
|
def book_cell(expiry, side, z, dz, tenor_d, **kw):
|
||||||
|
rB = run_cell("BTC", expiry, side, z, dz, tenor_d, **kw)
|
||||||
|
rE = run_cell("ETH", expiry, side, z, dz, tenor_d, **kw)
|
||||||
|
return pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1)
|
||||||
|
|
||||||
|
|
||||||
|
def vrp01_book(f):
|
||||||
|
return pd.concat({a[0]: vrp_spread_weekly(a, defined_risk=True, f=f, gate_vrp=True,
|
||||||
|
gate_ivr=0.30, crash_skip=0.90)
|
||||||
|
for a in ("BTC", "ETH")}, axis=1, join="inner").mean(axis=1)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- utilita' report
|
||||||
|
def _sh_window(r, tenor_d, start=None, end=None):
|
||||||
|
w = r
|
||||||
|
if start is not None:
|
||||||
|
w = w[w.index >= pd.Timestamp(start, tz="UTC")]
|
||||||
|
if end is not None:
|
||||||
|
w = w[w.index < pd.Timestamp(end, tz="UTC")]
|
||||||
|
ppy = 365.25 / tenor_d
|
||||||
|
return float(w.mean() / w.std() * np.sqrt(ppy)) if len(w) > 5 and w.std() > 0 else float("nan")
|
||||||
|
|
||||||
|
|
||||||
|
def dd_of(r, lam=1.0):
|
||||||
|
eq = np.cumprod(1 + lam * r.values)
|
||||||
|
pk = np.maximum.accumulate(eq)
|
||||||
|
return float(np.max((pk - eq) / pk))
|
||||||
|
|
||||||
|
|
||||||
|
def scale_to_dd(r, dd_target):
|
||||||
|
"""λ tale che il maxDD di λ·r == dd_target (null del de-levering)."""
|
||||||
|
lo, hi = 0.01, 3.0
|
||||||
|
for _ in range(60):
|
||||||
|
mid = 0.5 * (lo + hi)
|
||||||
|
if dd_of(r, mid) > dd_target:
|
||||||
|
hi = mid
|
||||||
|
else:
|
||||||
|
lo = mid
|
||||||
|
return 0.5 * (lo + hi)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_engine():
|
||||||
|
"""run_cell deve riprodurre ESATTAMENTE run_structure (motore ALB-A validato) su 3 kind."""
|
||||||
|
print(" [validazione motore] run_cell vs r0702_alb_structure.run_structure (z=2, dz=1, 5g, gated):")
|
||||||
|
ok = True
|
||||||
|
for kind, (expiry, side) in (("diag", ("diag", "both")), ("condor", ("same", "both")),
|
||||||
|
("vert", ("same", "put"))):
|
||||||
|
for a in ("BTC", "ETH"):
|
||||||
|
ref = run_structure(a, kind, z=2.0, dz=1.0, tenor_d=5, gated=True)
|
||||||
|
new = run_cell(a, expiry, side, 2.0, 1.0, 5)
|
||||||
|
d = float((ref - new).abs().max()) if len(ref) == len(new) else float("inf")
|
||||||
|
ok &= d < 1e-12
|
||||||
|
print(f" {kind:<7}{a}: n={len(new):>4} max|Δ|={d:.2e}")
|
||||||
|
if not ok:
|
||||||
|
raise SystemExit("ENGINE MISMATCH: run_cell non riproduce run_structure — stop.")
|
||||||
|
print(" -> motore identico (riuso validato, non riscrittura).")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 112)
|
||||||
|
print(" R0703 VRPIMP-STRUCTGRID — griglia onesta 288 celle di struttura short-vol vs VRP01 canonico")
|
||||||
|
print(" Gate IV-rank canonico SEMPRE attivo (mai riottimizzato). Selezione IN-SAMPLE-ONLY (pre-2025).")
|
||||||
|
print(" ⚠️ SKEW: pricing BS FLAT su DVOL-30g -> banda f {0.6,0.8,1.0,1.3} + scenario fp=1.3/fc=0.7.")
|
||||||
|
print(" Regola standing: NIENTE short-vol da modello in deploy — esito massimo = conoscenza/LEAD.")
|
||||||
|
print("=" * 112)
|
||||||
|
|
||||||
|
validate_engine()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- (1) VRP01 canonico + bridge
|
||||||
|
print("\n (1) BASELINE — VRP01 canonico (options_vrp_v2 COMBO) e bridge motore-nuovo, banda f")
|
||||||
|
hdr = (f" {'riga':<38} {'ShF':>6} {'ShIS':>6} {'ShH':>6} {'CAGR':>7} {'maxDD':>6} "
|
||||||
|
f"{'worst':>7} {'win%':>5} {'att%':>5}")
|
||||||
|
print(hdr)
|
||||||
|
vrp = {}
|
||||||
|
for f in F_SWEEP:
|
||||||
|
vrp[f] = vrp01_book(f)
|
||||||
|
mm = metrics(vrp[f], 7); mi = metrics(vrp[f][vrp[f].index < HOLDOUT], 7)
|
||||||
|
print(f" VRP01 canonico f={f:<4} {'':<16} {mm['sh']:>6.2f} {mi['sh']:>6.2f} {mm['sh_h']:>6.2f} "
|
||||||
|
f"{mm['cagr']*100:>+6.1f}% {mm['dd']*100:>5.1f}% {mm['worst']*100:>+6.2f}% "
|
||||||
|
f"{mm['win']*100:>4.0f}% {mm['act']*100:>4.0f}%")
|
||||||
|
bridge = {}
|
||||||
|
for f in F_SWEEP:
|
||||||
|
bridge[f] = book_cell("same", "put", Z_BRIDGE, DZ_BRIDGE, 7, f_put=f, f_call=f)
|
||||||
|
mm = metrics(bridge[f], 7); mi = metrics(bridge[f][bridge[f].index < HOLDOUT], 7)
|
||||||
|
print(f" bridge vert-put Δ-0.28/-0.10 7g f={f:<4} {mm['sh']:>6.2f} {mi['sh']:>6.2f} {mm['sh_h']:>6.2f} "
|
||||||
|
f"{mm['cagr']*100:>+6.1f}% {mm['dd']*100:>5.1f}% {mm['worst']*100:>+6.2f}% "
|
||||||
|
f"{mm['win']*100:>4.0f}% {mm['act']*100:>4.0f}%")
|
||||||
|
print(" (bridge = stessa struttura di VRP01 nel motore-griglia: e' il confronto apples-to-apples;")
|
||||||
|
print(" battere VRP01 ma non il bridge = artefatto di convenzione fee/capitale, non struttura)")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- (2) griglia 288 celle, f=1.0
|
||||||
|
print(f"\n (2) GRIGLIA {len(STRUCTS)}x{len(Z_GRID)}x{len(DZ_GRID)}x{len(TENOR_GRID)} = "
|
||||||
|
f"{len(STRUCTS)*len(Z_GRID)*len(DZ_GRID)*len(TENOR_GRID)} celle (f=1.0, gate canonico)")
|
||||||
|
cells = {}
|
||||||
|
for expiry, side, lab in STRUCTS:
|
||||||
|
for z in Z_GRID:
|
||||||
|
for dz in DZ_GRID:
|
||||||
|
for tn in TENOR_GRID:
|
||||||
|
r = book_cell(expiry, side, z, dz, tn)
|
||||||
|
r_is = r[r.index < HOLDOUT]
|
||||||
|
act_is = r_is[r_is != 0]
|
||||||
|
rec = dict(expiry=expiry, side=side, lab=lab, z=z, dz=dz, tn=tn, r=r,
|
||||||
|
mi=metrics(r_is, tn), mm=metrics(r, tn),
|
||||||
|
nact_is=int(len(act_is)), nloss_is=int((act_is < 0).sum()))
|
||||||
|
rec["eligible"] = (rec["nact_is"] >= MIN_ACT_IS and rec["nloss_is"] >= MIN_LOSS_IS)
|
||||||
|
cells[(lab, z, dz, tn)] = rec
|
||||||
|
all_full_sh = [c["mm"]["sh"] for c in cells.values()]
|
||||||
|
all_is_sh = [c["mi"]["sh"] for c in cells.values()]
|
||||||
|
n_cells = len(cells)
|
||||||
|
inel = [c for c in cells.values() if not c["eligible"]]
|
||||||
|
print(f" celle totali {n_cells} | ESCLUSE dalla selezione (implausibili: <{MIN_LOSS_IS} perdite IS "
|
||||||
|
f"o <{MIN_ACT_IS} trade IS): {len(inel)} — dichiarate sotto, contate comunque nel DSR")
|
||||||
|
|
||||||
|
# il pattern '0-perdite': cosa avremmo scelto SENZA il filtro
|
||||||
|
naive = max(cells.values(), key=lambda c: c["mi"]["sh"])
|
||||||
|
print(f"\n [pattern CC01/ALB-A] best IS SENZA filtro implausibilita': {naive['lab']} z={naive['z']} "
|
||||||
|
f"dz={naive['dz']} {naive['tn']}g -> ShIS {naive['mi']['sh']:.2f} (perdite IS: {naive['nloss_is']}, "
|
||||||
|
f"trade IS: {naive['nact_is']}) | suo hold-out ShH {naive['mm']['sh_h']:+.2f}")
|
||||||
|
top_inel = sorted(inel, key=lambda c: c["mi"]["sh"], reverse=True)[:8]
|
||||||
|
print(" top celle ESCLUSE (ShIS alto MA coda mai campionata in-sample -> Sharpe non informativo):")
|
||||||
|
for c in top_inel:
|
||||||
|
print(f" {c['lab']:<10} z={c['z']} dz={c['dz']} {c['tn']:>2}g ShIS {c['mi']['sh']:>6.2f} "
|
||||||
|
f"perdite-IS {c['nloss_is']:>2} su {c['nact_is']:>3} trade -> ShH {c['mm']['sh_h']:>+6.2f}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- (3) selezione in-sample-only
|
||||||
|
elig = [c for c in cells.values() if c["eligible"]]
|
||||||
|
ch = max(elig, key=lambda c: c["mi"]["sh"])
|
||||||
|
print(f"\n (3) SELEZIONE IN-SAMPLE-ONLY (fra le {len(elig)} eleggibili) + top-10")
|
||||||
|
print(f" {'cella':<34} {'ShIS':>6} {'ShF':>6} {'ShH':>6} {'DD':>6} {'worst':>7} {'lossIS':>7}")
|
||||||
|
for c in sorted(elig, key=lambda c: c["mi"]["sh"], reverse=True)[:10]:
|
||||||
|
mark = " <== SCELTA" if c is ch else ""
|
||||||
|
print(f" {c['lab']:<10} z={c['z']} dz={c['dz']} tenor={c['tn']:>2}g{'':<6} {c['mi']['sh']:>6.2f} "
|
||||||
|
f"{c['mm']['sh']:>6.2f} {c['mm']['sh_h']:>6.2f} {c['mm']['dd']*100:>5.1f}% "
|
||||||
|
f"{c['mm']['worst']*100:>+6.2f}% {c['nloss_is']:>4}/{c['nact_is']:<4}{mark}")
|
||||||
|
|
||||||
|
tn = ch["tn"]
|
||||||
|
dsr_full, sr0_full = al.deflated_sharpe(ch["mm"]["sh"], all_full_sh, ch["r"], dpy=365.25 / tn)
|
||||||
|
r_is = ch["r"][ch["r"].index < HOLDOUT]
|
||||||
|
dsr_is, sr0_is = al.deflated_sharpe(ch["mi"]["sh"], all_is_sh, r_is, dpy=365.25 / tn)
|
||||||
|
print(f"\n DSR cella scelta (N={n_cells} trial): FULL {dsr_full:.3f} (null-max ~{sr0_full:.2f}) | "
|
||||||
|
f"IS {dsr_is:.3f} (null-max ~{sr0_is:.2f}) | PASS richiede >= 0.95")
|
||||||
|
|
||||||
|
# nota anti-hindsight: la cella col miglior HOLD-OUT fra le eleggibili NON e' selezionabile
|
||||||
|
# (selection-on-holdout, gate 2026-06-29); la riporto solo per chiudere la domanda ovvia.
|
||||||
|
hb = max(elig, key=lambda c: c["mm"]["sh_h"])
|
||||||
|
dsr_hb, _ = al.deflated_sharpe(hb["mm"]["sh"], all_full_sh, hb["r"], dpy=365.25 / hb["tn"])
|
||||||
|
rank_is = 1 + sum(1 for c in elig if c["mi"]["sh"] > hb["mi"]["sh"])
|
||||||
|
print(f" [anti-hindsight] best HOLD-OUT fra le eleggibili: {hb['lab']} z={hb['z']} dz={hb['dz']} "
|
||||||
|
f"{hb['tn']}g (ShIS {hb['mi']['sh']:.2f}, rank-IS #{rank_is}, ShH {hb['mm']['sh_h']:.2f}, "
|
||||||
|
f"DSR {dsr_hb:.3f}) — sceglierla = selection-on-holdout, VIETATO; e comunque DSR<0.95.")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- (4) cella scelta: banda f vs VRP01
|
||||||
|
print(f"\n (4) CELLA SCELTA {ch['lab']} z={ch['z']} dz={ch['dz']} {tn}g — banda f vs VRP01 e bridge")
|
||||||
|
print(f" {'f':<22} {'cella ShF/ShH':>16} {'VRP01 ShF/ShH':>16} {'bridge ShF/ShH':>16} "
|
||||||
|
f"{'cella DD/worst':>17} {'VRP01 DD/worst':>17}")
|
||||||
|
ch_f = {}
|
||||||
|
beats_vrp = beats_bridge = True
|
||||||
|
for f in F_SWEEP:
|
||||||
|
rr = book_cell(ch["expiry"], ch["side"], ch["z"], ch["dz"], tn, f_put=f, f_call=f)
|
||||||
|
ch_f[f] = rr
|
||||||
|
m = metrics(rr, tn); v = metrics(vrp[f], 7); b = metrics(bridge[f], 7)
|
||||||
|
beats_vrp &= (m["sh"] > v["sh"]) and (m["sh_h"] > v["sh_h"])
|
||||||
|
beats_bridge &= (m["sh"] > b["sh"]) and (m["sh_h"] > b["sh_h"])
|
||||||
|
print(f" f={f:<20} {m['sh']:>7.2f}/{m['sh_h']:>6.2f} {v['sh']:>8.2f}/{v['sh_h']:>6.2f} "
|
||||||
|
f"{b['sh']:>8.2f}/{b['sh_h']:>6.2f} {m['dd']*100:>7.1f}%/{m['worst']*100:>+6.2f}% "
|
||||||
|
f"{v['dd']*100:>7.1f}%/{v['worst']*100:>+6.2f}%")
|
||||||
|
rr = book_cell(ch["expiry"], ch["side"], ch["z"], ch["dz"], tn, **F_SKEW)
|
||||||
|
m = metrics(rr, tn)
|
||||||
|
print(f" SKEW fp=1.3/fc=0.7 {m['sh']:>7.2f}/{m['sh_h']:>6.2f} (VRP01/bridge non hanno lato call)")
|
||||||
|
py_c = per_year(ch_f[1.0]); py_v = per_year(vrp[1.0])
|
||||||
|
print(" per-anno cella f=1.0: " + " ".join(f"{y}:{v*100:+5.1f}%" for y, v in sorted(py_c.items())))
|
||||||
|
print(" per-anno VRP01 f=1.0: " + " ".join(f"{y}:{v*100:+5.1f}%" for y, v in sorted(py_v.items())))
|
||||||
|
|
||||||
|
# persistenza multi-cut del presunto vantaggio (ΔSh cella - bridge, finestre [cut, end))
|
||||||
|
print(" multi-cut ΔSh (cella - bridge | cella - VRP01) su [cut, fine):")
|
||||||
|
for cut in CUTS:
|
||||||
|
d_b = _sh_window(ch_f[1.0], tn, start=cut) - _sh_window(bridge[1.0], 7, start=cut)
|
||||||
|
d_v = _sh_window(ch_f[1.0], tn, start=cut) - _sh_window(vrp[1.0], 7, start=cut)
|
||||||
|
print(f" {cut}: {d_b:+.2f} | {d_v:+.2f}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- (5) banda d'ancora
|
||||||
|
print(f"\n (5) BANDA D'ANCORA — cella scelta su tutte le {tn} fasi di partenza (offset 0..{tn-1})")
|
||||||
|
anch = []
|
||||||
|
for o in range(tn):
|
||||||
|
ro = book_cell(ch["expiry"], ch["side"], ch["z"], ch["dz"], tn, offset=o)
|
||||||
|
mo = metrics(ro, tn)
|
||||||
|
anch.append((o, mo["sh"], mo["sh_h"]))
|
||||||
|
print(f" offset {o}: ShF {mo['sh']:>5.2f} ShH {mo['sh_h']:>+5.2f}")
|
||||||
|
shf = [a[1] for a in anch]; shh = [a[2] for a in anch]
|
||||||
|
print(f" banda ShF [{min(shf):.2f}, {max(shf):.2f}] mediana {np.median(shf):.2f} | "
|
||||||
|
f"ShH [{min(shh):+.2f}, {max(shh):+.2f}] mediana {np.median(shh):+.2f}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- (6) plateau dei vicini
|
||||||
|
print(f"\n (6) PLATEAU — slice {ch['lab']} dz={ch['dz']}: matrice z × tenor (ShIS / ShH)")
|
||||||
|
print(" " + "".join(f"{t:>14}g" for t in TENOR_GRID))
|
||||||
|
for z in Z_GRID:
|
||||||
|
rowtxt = f" z={z:<4}"
|
||||||
|
for t in TENOR_GRID:
|
||||||
|
c = cells[(ch["lab"], z, ch["dz"], t)]
|
||||||
|
star = "*" if (z == ch["z"] and t == tn) else " "
|
||||||
|
rowtxt += f" {c['mi']['sh']:>5.2f}/{c['mm']['sh_h']:>+5.2f}{star}"
|
||||||
|
print(rowtxt)
|
||||||
|
print(" vicini dz (a z/tenor scelti): " + " ".join(
|
||||||
|
f"dz={d}: ShIS {cells[(ch['lab'], ch['z'], d, tn)]['mi']['sh']:.2f}/"
|
||||||
|
f"ShH {cells[(ch['lab'], ch['z'], d, tn)]['mm']['sh_h']:+.2f}" for d in DZ_GRID))
|
||||||
|
neigh = []
|
||||||
|
zi, ti, di = Z_GRID.index(ch["z"]), TENOR_GRID.index(tn), DZ_GRID.index(ch["dz"])
|
||||||
|
for (gi, grid, mk) in ((zi, Z_GRID, "z"), (ti, TENOR_GRID, "tn"), (di, DZ_GRID, "dz")):
|
||||||
|
for step in (-1, +1):
|
||||||
|
k = gi + step
|
||||||
|
if 0 <= k < len(grid):
|
||||||
|
key = dict(z=ch["z"], tn=tn, dz=ch["dz"]); key[mk] = grid[k]
|
||||||
|
neigh.append(cells[(ch["lab"], key["z"], key["dz"], key["tn"])])
|
||||||
|
n_pos = sum(1 for c in neigh if c["mi"]["sh"] > 0 and c["mm"]["sh_h"] > 0)
|
||||||
|
print(f" vicini adiacenti (±1 passo per asse): {len(neigh)} | con ShIS>0 E ShH>0: {n_pos}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- (7) null del de-levering
|
||||||
|
print("\n (7) NULL DEL DE-LEVERING — il vantaggio DD della cella e' replicabile scalando VRP01?")
|
||||||
|
m_c = metrics(ch_f[1.0], tn); m_v = metrics(vrp[1.0], 7)
|
||||||
|
lam = scale_to_dd(vrp[1.0], m_c["dd"])
|
||||||
|
scaled = lam * vrp[1.0]
|
||||||
|
m_s = metrics(scaled, 7)
|
||||||
|
print(f" cella: Sh {m_c['sh']:.2f} / ShH {m_c['sh_h']:.2f} DD {m_c['dd']*100:.1f}% "
|
||||||
|
f"worst {m_c['worst']*100:+.2f}% CAGR {m_c['cagr']*100:+.1f}%")
|
||||||
|
print(f" VRP01 pieno: Sh {m_v['sh']:.2f} / ShH {m_v['sh_h']:.2f} DD {m_v['dd']*100:.1f}% "
|
||||||
|
f"worst {m_v['worst']*100:+.2f}% CAGR {m_v['cagr']*100:+.1f}%")
|
||||||
|
print(f" VRP01 × λ={lam:.2f}: Sh {m_s['sh']:.2f} / ShH {m_s['sh_h']:.2f} DD {m_s['dd']*100:.1f}% "
|
||||||
|
f"worst {m_s['worst']*100:+.2f}% CAGR {m_s['cagr']*100:+.1f}%")
|
||||||
|
delever_kills = (m_c["dd"] < m_v["dd"]) and (m_s["sh"] >= m_c["sh"]) and (m_s["sh_h"] >= m_c["sh_h"])
|
||||||
|
print(" -> lo scaling lascia lo Sharpe invariato: ogni vantaggio di DD e' replicabile con λ,"
|
||||||
|
f" la cella vale SOLO se Sh/ShH superiori. delever_null_kills={delever_kills}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- (8) marginale/corr
|
||||||
|
print("\n (8) CORRELAZIONE + MARGINALE (informativo, non promozione: regola standing short-vol)")
|
||||||
|
d_c = to_daily_lumped(ch_f[1.0]); d_v = to_daily_lumped(vrp[1.0])
|
||||||
|
w_c = (1 + d_c).resample("168h", origin="epoch").prod() - 1 # origin comune (lezione '7D')
|
||||||
|
w_v = (1 + d_v).resample("168h", origin="epoch").prod() - 1
|
||||||
|
Jw = pd.concat({"c": w_c, "v": w_v}, axis=1, join="inner").dropna()
|
||||||
|
print(f" corr settimanale cella~VRP01: {Jw['c'].corr(Jw['v']):+.2f} "
|
||||||
|
f"(alta = stesso trade in altri vestiti)")
|
||||||
|
mv = al.marginal_vs_tp01(d_c)
|
||||||
|
print(f" marginal_vs_tp01[cella]: verdict={mv.get('marginal_verdict')} corr={mv.get('corr_full')} "
|
||||||
|
f"IS-Sh={mv.get('cand_insample_sharpe')} insample_edge={mv.get('has_insample_edge')} "
|
||||||
|
f"hedge={mv.get('is_hedge')} robust_oos={mv.get('robust_oos')}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- (9) verdetto
|
||||||
|
print("\n (9) VERDETTO")
|
||||||
|
dsr_pass = bool(np.isfinite(dsr_full) and dsr_full >= 0.95 and np.isfinite(dsr_is) and dsr_is >= 0.95)
|
||||||
|
wins = beats_vrp and beats_bridge and dsr_pass and not delever_kills
|
||||||
|
print(f" batte VRP01 su tutta la banda f (ShF e ShH): {beats_vrp}")
|
||||||
|
print(f" batte il bridge (stesso motore) su tutta la banda f: {beats_bridge}")
|
||||||
|
print(f" DSR>=0.95 su {n_cells} celle (FULL {dsr_full:.3f} / IS {dsr_is:.3f}): {dsr_pass}")
|
||||||
|
print(f" sopravvive al null del de-levering: {not delever_kills}")
|
||||||
|
print(f" ==> {'ESISTE una cella che batte VRP01 onestamente' if wins else 'NESSUNA cella batte VRP01 onestamente'}")
|
||||||
|
print(" (esecuzione: comunque STAT-MODE — niente short-vol da modello in deploy;")
|
||||||
|
print(" muri di size Deribit invariati dal filone capital-scaling: spread ETH ~2.6k+, BTC ~4.7k+)")
|
||||||
|
|
||||||
|
# dump metriche per il report
|
||||||
|
SCRATCH.mkdir(parents=True, exist_ok=True)
|
||||||
|
dump = {f"{k[0]}|z{k[1]}|dz{k[2]}|t{k[3]}": dict(
|
||||||
|
is_sh=c["mi"]["sh"], full_sh=c["mm"]["sh"], hold_sh=c["mm"]["sh_h"], dd=c["mm"]["dd"],
|
||||||
|
worst=c["mm"]["worst"], nact_is=c["nact_is"], nloss_is=c["nloss_is"], eligible=c["eligible"])
|
||||||
|
for k, c in cells.items()}
|
||||||
|
(SCRATCH / "r0703_structgrid_results.json").write_text(json.dumps(dump, indent=1))
|
||||||
|
print(f"\n [dump] {SCRATCH / 'r0703_structgrid_results.json'}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user