Files
PythagorasGoal/scripts/research/r0703_vrpimp_stresslab.py
T
Adriano Dal Pastro 26f8d27a61 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>
2026-07-03 07:05:19 +00:00

692 lines
38 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()