#!/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()