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:
@@ -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()
|
||||
Reference in New Issue
Block a user