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
+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()