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:
Adriano Dal Pastro
2026-07-03 07:05:19 +00:00
parent 76120b59c2
commit 26f8d27a61
9 changed files with 3668 additions and 0 deletions
+16
View File
@@ -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
+167
View File
@@ -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.
+484
View File
@@ -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()
+323
View File
@@ -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()
+444
View File
@@ -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()
+655
View File
@@ -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()
+462
View File
@@ -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()
+691
View File
@@ -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()
+426
View File
@@ -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()