Files
PythagorasGoal/scripts/research/r0909_crash_catturato.py
T
Adriano Dal Pastro 0a2f780d3c cblib causale: spot +1h e DVOL +1 giorno (debito §5.18); §11 rimisurato 0,714 → 0,712, verdetti invariati
- cblib.causale(s, cadenza): spot_series e dvol_series escono causali (asof = ultima chiusura NOTA);
  il DVOL giornaliero era una seconda serie con lo stesso difetto (fino a 24h avanti), verificato contro
  l'API pubblica a 1h. Regolamento ST alle 08:00 invece delle 09:00.
- §11 riprodotto al millesimo su worktree HEAD (0,714 [0,690-0,779], n=19, flag --al) e rimisurato:
  0,712 [0,664-0,732]; solo spot 0,721, solo DVOL 0,693, DVOL orario di controllo 0,717. Mediana onesta
  BTC 1,45 → 2,12, fee 1,51x/1,87x. r0730: 0,73 → 0,71. §75: ETH pre 0,76, BTC durante 0,89 (orario 0,83).
  skew panel ≤0,005. vrp_f_watch: f canonico 0,732 → 0,706, differenza +0,097 [+0,042, +0,140].
- r0909 senza doppio shift, contesto sul DVOL del giorno; r0822 regime su date di calendario; r0901
  join riallineato (bit-exact). Aperti con costo: DVOL orario (−0,04/+0,06 nel crollo), spot 5m (≤25 min).
- test: +3 causalita' in test_cb_chain_vrp, anti-doppio-shift in test_r0909; suite 1011/1011.
- revisione fable: 16 segnalazioni, tutte applicate (fra cui un «−1,8% in un'ora» che era +3,9%).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjj5vPEBoAJrB23P6RjKzs
2026-09-09 21:41:23 +00:00

385 lines
25 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.
"""r0909 — IL CROLLO CATTURATO: il `f` di stress delle opzioni, misurato su quote VERE.
PERCHE' ESISTE. La decisione vincolante del 19/06 ("niente short-vol da modello in deploy", e la
gemella "niente long-vol scalp") dichiara UNA condizione che la riapre: *un `f` di stress reale
misurato su un crash catturato*. COLLAR01 (§71) e PAVIMENTO-LEVA (§72) chiudono con la stessa
condizione. Il 22/08 (§11) il campione con quote vere era "la strategia mentre STA FERMA":
19 settimane di rally, "l'unico episodio ad alta vol del campione e' un rally, non un crash".
FATTO NUOVO (2026-09-09): l'archivio ereditato `bite_archive.parquet` copre con bid E ask
il crollo del 1-5 giugno 2026 — BTC 25% e ETH 30% in 5 giorni (peggior giorno ETH 10,6%
il 05/06), su UNA scadenza (19 giugno, 14-21 DTE agli ingressi). Il campione del 22/08
partiva DOPO, dal fondo. Nessuno lo aveva usato: e' il crash catturato che la regola aspettava.
COSA MISURA (a ogni ora d'ingresso dal 30/05 al 04/06, BTC e ETH — l'ora e' un'ancora, si
riportano mediana e banda, mai il massimo):
A. la geometria di VRP01 (vendi put δ−0,28, compra put δ−0,10, stessa scadenza) a quote REALI:
credito al bid/ask, mark-to-market ora per ora dalle standing quotes, regolamento a scadenza
sullo spot certificato. Contro lo STESSO trade prezzato dal modello del sleeve (BS su DVOL,
`sleeves._bs_put`). f_stress = perdita reale / perdita modellata, a scadenza e al peggior MTM.
B. lo specchio LONG (compra la put δ−0,10 all'ask; e il debit spread 0,28/0,10): quanto del
payoff modellato si incassa davvero al bid — a scadenza (uscita passiva) e al miglior punto
del percorso (limite superiore con look-ahead, dichiarato).
C. il contesto: DVOL, RV realizzata, IV-rank causale (il gate di VRP01 era aperto o chiuso?),
e cosa dice il modello del sleeve (`_vrp_weekly_asset`) per le settimane di giugno.
LIMITI DICHIARATI PRIMA DEI NUMERI: n = 1 crollo, 1 scadenza per asset, tenor 14-21 giorni
(VRP01 e' a 7), 2-5 mila righe/giorno (40-125 strumenti/ora), `underlying_price` assente
nell'archivio (spot dal feed 1h certificato, D1), regolamento approssimato allo spot dell'ora
di scadenza (Deribit usa una TWAP di 30 min). La famiglia dell'archivio e' INVERSE (premi in
BTC/ETH; `ETH-19JUN26-1900-P`): VRP01 opererebbe sulla USDC-lineare, che il conto puo' marginare
e che NON e' nell'archivio (§11) — il f qui misurato e' della famiglia che il conto non negozia.
Un episodio e' un episodio: da' la TAGLIA di f nello stress, non la sua distribuzione.
DUE CORREZIONI DELLA REVISIONE (fable, 09/09), applicate:
* il feed 1h e' etichettato all'APERTURA (la barra 20:00 chiude alle 21:00): `S.asof(ts)`
guardava un'ora AVANTI. Corretto la mattina del 09/09 qui con uno `spot_causale` locale;
la sera dello stesso giorno la riparazione e' entrata in `cblib` (`causale`, debito §5.18) e
copre anche il DVOL giornaliero (fino a 24 ore avanti): questo script usa le serie di `cblib`
COME SONO, senza ri-spostarle.
* il costo di chiusura `(ask corta bid lunga)·spot` puo' superare la LARGHEZZA dello spread
(una quota da 158% di spread relativo a T→0): uno spread a rischio definito non costa mai piu'
della larghezza per chiudere (si lascia regolare). Il MTM e' tagliato alla larghezza e le ore
tagliate sono contate. Senza il taglio il "peggior MTM" era un artefatto di quote (2,6×).
* `f_exp` e' TAUTOLOGICO quando entrambe le gambe finiscono ITM (payoff = larghezza per reale
e modello): si riduce a una funzione di f_net. Si stampa la quota di trade in cui succede.
Nessun file di produzione toccato. Nessun ordine.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
import cblib as CB # noqa: E402
from src.portfolio.sleeves import VRP_CFG, _vrp_weekly_asset # noqa: E402
ARCHIVIO = ROOT / "data" / "raw" / "cb_chain" / "bite_archive.parquet"
ASSETS = ("BTC", "ETH")
EXP = pd.Timestamp("2026-06-19 08:00", tz="UTC")
INGRESSI = (pd.Timestamp("2026-05-30", tz="UTC"), pd.Timestamp("2026-06-05", tz="UTC")) # [da, a)
CROLLO = (pd.Timestamp("2026-06-01", tz="UTC"), pd.Timestamp("2026-06-06", tz="UTC"))
PRE = pd.Timestamp("2026-06-02", tz="UTC") # ingressi "prima del crollo" = prima del primo giorno a 7%/8% (il 01/06 BTC fece 3,1%: e' DENTRO "pre", dichiarato)
TOL_D = (0.08, 0.06) # geometria VRP01: |δ corta + 0,28| ≤ 0,08 e |δ lunga + 0,10| ≤ 0,06, altrimenti "fuori geometria"
SHORT_D, LONG_D = VRP_CFG["short_delta"], VRP_CFG["long_delta"] # P1: dal sleeve, non ridichiarati
FEE_FRAC = VRP_CFG["fee_frac"]
# listino Deribit opzioni (stesse costanti di r0822_vrp_real_quotes, verificate al venue il 22/08)
FEE_UNDERLYING, FEE_PREMIUM_CAP, FEE_DELIVERY = 0.0003, 0.125, 0.00015
def leg_fee(prem_base: float, spot: float) -> float:
return min(FEE_UNDERLYING * spot, FEE_PREMIUM_CAP * prem_base * spot)
def carica() -> pd.DataFrame:
a = pd.read_parquet(ARCHIVIO)
a["ts"] = pd.to_datetime(a["ts"], utc=True)
a["exp"] = pd.to_datetime(a["exp"], utc=True)
a = a[(a["option_type"] == "P") & (a["exp"] == EXP) & (a["ts"] >= INGRESSI[0]) & (a["ts"] <= EXP)]
a = a.drop_duplicates(subset=["ts", "instrument_name"], keep="last")
a["dte"] = (a["exp"] - a["ts"]).dt.total_seconds() / 86400.0
return a.reset_index(drop=True)
def bs_spread(S: float, Ks: float, Kl: float, T: float, sig: float) -> float:
return CB.bs_put(S, Ks, T, sig) - CB.bs_put(S, Kl, T, sig)
def bs_put_vec(S, K, T, sig):
"""`cblib.bs_put` vettorizzata (stessa formula, r=0); T≤0 → intrinseco."""
from scipy.stats import norm
S = np.asarray(S, float); T = np.asarray(T, float); sig = np.asarray(sig, float)
out = np.maximum(K - S, 0.0)
ok = (T > 0) & (sig > 0)
d1 = (np.log(S[ok] / K) + 0.5 * sig[ok] ** 2 * T[ok]) / (sig[ok] * np.sqrt(T[ok]))
out[ok] = K * norm.cdf(-(d1 - sig[ok] * np.sqrt(T[ok]))) - S[ok] * norm.cdf(-d1)
return out
class Quote:
"""Tabella ora × strumento (bid, ask, iv) costruita UNA volta per asset, con spot e DVOL per ora."""
def __init__(self, seg: pd.DataFrame, S: pd.Series, V: pd.Series):
q = seg.dropna(subset=["bid", "ask"]); q = q[(q["bid"] > 0) & (q["ask"] > 0)]
self.bid = q.pivot_table(index="ts", columns="instrument_name", values="bid")
self.ask = q.pivot_table(index="ts", columns="instrument_name", values="ask")
self.iv = q.pivot_table(index="ts", columns="instrument_name", values="iv")
idx = self.bid.index
self.spot = pd.Series([float(S.asof(t)) for t in idx], index=idx)
self.dvol = pd.Series([float(V.asof(t)) for t in idx], index=idx) / 100.0
self.T = pd.Series(np.maximum((EXP - idx).total_seconds().values / 86400.0, 0.0) / 365.25, index=idx)
def percorso(Q: Quote, dopo: pd.Timestamp, inst_s: str, inst_l: str, Ks: float, Kl: float) -> pd.DataFrame:
"""Ora per ora dopo l'ingresso: costo REALE di chiudere lo short spread (ricompra la corta
all'ask, rivende la lunga al bid), valore REALE di liquidare il debit spread (vende la corta
al bid, ricompra la lunga all'ask) e la put lunga (bid); piu' il valore MODELLATO (BS a DVOL_t)."""
if inst_s not in Q.bid.columns or inst_l not in Q.bid.columns:
return pd.DataFrame()
m = (Q.bid.index > dopo) & Q.bid[inst_s].notna() & Q.bid[inst_l].notna()
if not m.any():
return pd.DataFrame()
idx = Q.bid.index[m]
sp = Q.spot.loc[idx].values; dv = Q.dvol.loc[idx].values; T = Q.T.loc[idx].values
bs_, as_ = Q.bid.loc[idx, inst_s].values, Q.ask.loc[idx, inst_s].values
bl, al = Q.bid.loc[idx, inst_l].values, Q.ask.loc[idx, inst_l].values
mod_s, mod_l = bs_put_vec(sp, Ks, T, dv), bs_put_vec(sp, Kl, T, dv)
grezzo = (as_ - bl) * sp
return pd.DataFrame(dict(ts=idx, spot=sp, dvol=dv,
chiudi_short=np.minimum(grezzo, Ks - Kl), chiudi_grezzo=grezzo,
liquida_long=np.clip((bs_ - al) * sp, 0.0, Ks - Kl), liquida_put=bl * sp,
mod_spread=mod_s - mod_l, mod_put=mod_l,
iv_s=Q.iv.loc[idx, inst_s].values, iv_l=Q.iv.loc[idx, inst_l].values))
def trade(asset: str, ts: pd.Timestamp, snap: pd.DataFrame, Q: Quote,
S: pd.Series, V: pd.Series) -> dict | None:
legs = CB.pick_legs(snap, SHORT_D, LONG_D)
if legs is None:
return None
spot = float(S.asof(ts)); dvol = float(V.asof(ts)); T = (EXP - ts).total_seconds() / 86400.0 / 365.25
hist = V[V.index < ts]; ivr = float((hist < dvol).mean()) if len(hist) else np.nan
Ks, Kl = legs["k_short"], legs["k_long"]
# --- A. short spread (VRP01) — reale al bid/ask, modello a DVOL
cred = (legs["bid_short"] - legs["ask_long"]) * spot
if cred <= 0:
return None
cred_mod = bs_spread(spot, Ks, Kl, T, dvol / 100.0)
ST = float(S.asof(EXP))
payoff = max(Ks - ST, 0.0) - max(Kl - ST, 0.0)
fee_in = leg_fee(legs["bid_short"], spot) + leg_fee(legs["ask_long"], spot)
fee_out = sum(min(FEE_DELIVERY * ST, FEE_PREMIUM_CAP * max(K - ST, 0.0)) for K in (Ks, Kl) if K > ST)
pnl_real = cred - payoff - fee_in - fee_out
pnl_mod = cred_mod - payoff - FEE_FRAC * cred_mod
p = percorso(Q, ts, legs["inst_short"], legs["inst_long"], Ks, Kl)
if p.empty:
return None
mtm_real = cred - p["chiudi_short"] # P&L aperto se si chiude ORA a quote vere (tagliato alla larghezza)
mtm_mod = cred_mod - p["mod_spread"] # lo stesso trade riprezzato a BS/DVOL_t (costruzione nostra: il sleeve tiene a scadenza)
i_min = int(mtm_real.idxmin())
n_tagli = int((p["chiudi_grezzo"] > Ks - Kl).sum())
# --- B. specchio long: put δ−0,10 comprata all'ask; debit spread 0,28/0,10
costo_put = legs["ask_long"] * spot
mod_put = CB.bs_put(spot, Kl, T, dvol / 100.0)
costo_deb = (legs["ask_short"] - legs["bid_long"]) * spot
mod_deb = cred_mod
pay_put_exp = max(Kl - ST, 0.0)
pay_deb_exp = payoff
# il picco del percorso e' un limite SUPERIORE con look-ahead: si dichiara, non si vende
i_put = int(p["liquida_put"].idxmax()); i_deb = int(p["liquida_long"].idxmax())
risk = (Ks - Kl) - cred
in_geom = abs(legs["d_short"] - SHORT_D) <= TOL_D[0] and abs(legs["d_long"] - LONG_D) <= TOL_D[1]
return dict(
in_geom=bool(in_geom),
asset=asset, ts=ts, dte=(EXP - ts).total_seconds() / 86400.0, spot=spot, dvol=dvol, ivrank=ivr,
k_short=Ks, k_long=Kl, d_short=legs["d_short"], d_long=legs["d_long"], width=Ks - Kl,
iv_s0=legs["iv_short"], iv_l0=legs["iv_long"], ST=ST, und_ret=ST / spot - 1.0,
cred_real=cred, cred_mod=cred_mod, f_net=cred / cred_mod if cred_mod > 0 else np.nan,
risk=risk, pnl_real=pnl_real, pnl_mod=pnl_mod,
perdita_su_risk=-pnl_real / risk, perdita_su_coll=-pnl_real / Ks,
f_exp=(-pnl_real / -pnl_mod) if pnl_mod < 0 and pnl_real < 0 else np.nan,
mtm_real_min=float(mtm_real.min()), mtm_mod_min=float(mtm_mod.min()),
mtm_min_ts=p["ts"].iloc[i_min], n_tagli=n_tagli, n_ore=int(len(p)),
iv_s_at_min=float(p["iv_s"].iloc[i_min]), iv_l_at_min=float(p["iv_l"].iloc[i_min]), dvol_at_min=float(p["dvol"].iloc[i_min]) * 100,
itm_pieno=bool(ST <= Kl), # entrambe le gambe ITM: payoff = larghezza, f_exp si riduce a f_net
f_mtm=(mtm_real.min() / mtm_mod.min()) if mtm_mod.min() < 0 and mtm_real.min() < 0 else np.nan,
spot_min=float(p["spot"].min()), iv_s_max=float(p["iv_s"].max()), iv_l_max=float(p["iv_l"].max()),
n_ore_percorso=int(len(p)),
# long
costo_put=costo_put, mod_put=mod_put, f_put_in=costo_put / mod_put if mod_put > 0 else np.nan,
put_exp=pay_put_exp - costo_put, put_exp_mod=pay_put_exp - mod_put,
put_best=float(p["liquida_put"].iloc[i_put]) - costo_put, put_best_mod=float(p["mod_put"].iloc[i_put]) - mod_put,
put_best_ts=p["ts"].iloc[i_put], bleed_put=costo_put / spot,
costo_deb=costo_deb, mod_deb=mod_deb,
deb_exp=pay_deb_exp - costo_deb, deb_exp_mod=pay_deb_exp - mod_deb,
deb_best=float(p["liquida_long"].iloc[i_deb]) - costo_deb, deb_best_mod=float(p["mod_spread"].iloc[i_deb]) - mod_deb,
)
def misura() -> dict:
a = carica()
out = {"trades": {}, "contesto": {}, "sleeve": {}}
for asset in ASSETS:
seg = a[a["asset"] == asset]
S, V = CB.spot_series(asset), CB.dvol_series(asset) # gia' causali (cblib, dal 09/09 sera)
Q = Quote(seg, S, V)
rows = []
for ts in sorted(seg["ts"].unique()):
ts = pd.Timestamp(ts)
if not (INGRESSI[0] <= ts < INGRESSI[1]):
continue
t = trade(asset, ts, seg[seg["ts"] == ts], Q, S, V)
if t:
rows.append(t)
out["trades"][asset] = pd.DataFrame(rows)
# contesto: DVOL, RV5 e RV1 sul crollo
px = S[(S.index >= pd.Timestamp("2026-05-20", tz="UTC")) & (S.index <= EXP)]
pd_ = px.resample("1D").last().dropna() # chiusura del giorno = ultima chiusura nota entro le 24:00
# CONTESTO per giorno: la serie di cblib e' causale (la chiusura del giorno D sta a D+1);
# qui si vuole "il DVOL DEL giorno D" accanto al prezzo del giorno D, quindi si toglie lo
# spostamento SOLO per la tabella e per il picco del crollo. `dvol_pre` e `ivr_pre` restano
# sulla serie causale: sono cio' che si sapeva alle 00:00 del primo giorno di crollo.
dvg = CB.causale(V, "-1D")
hist_all = dvg[dvg.index < CROLLO[0]]
lr = np.log(pd_).diff()
rv5 = lr.rolling(5).std() * np.sqrt(365.25) * 100
dv = dvg[(dvg.index >= pd.Timestamp("2026-05-20", tz="UTC")) & (dvg.index <= EXP)]
out["contesto"][asset] = dict(
spot=pd_, ret=lr, rv5=rv5, dvol=dv,
dvol_max_crollo=float(dv[(dv.index >= CROLLO[0]) & (dv.index <= CROLLO[1])].max()),
dvol_pre=float(V.asof(CROLLO[0])),
pctl_picco=float((hist_all < dv[(dv.index >= CROLLO[0]) & (dv.index <= CROLLO[1])].max()).mean()),
ivr_pre=float((hist_all < V.asof(CROLLO[0])).mean()),
rv5_max=float(rv5[(rv5.index >= CROLLO[0]) & (rv5.index <= CROLLO[1] + pd.Timedelta(days=3))].max()),
caduta=float(pd_[CROLLO[0]:CROLLO[1]].min() / pd_[:CROLLO[0]].iloc[-1] - 1.0),
)
wk = _vrp_weekly_asset(asset)
out["sleeve"][asset] = wk[(wk.index >= pd.Timestamp("2026-05-10", tz="UTC")) & (wk.index <= pd.Timestamp("2026-07-05", tz="UTC"))]
return out
def sintesi(m: dict) -> dict:
s = {}
for asset in ASSETS:
t = m["trades"][asset]
if t.empty:
s[asset] = None; continue
n_fuori = int((~t["in_geom"]).sum())
t = t[t["in_geom"]] # la griglia di strike dell'archivio e' rada: fuori geometria = altra struttura
if t.empty:
s[asset] = dict(n=0, n_fuori=n_fuori); continue
pre = t[t["ts"] < PRE] # ingressi PRIMA del crollo: il caso della regola
dur = t[(t["ts"] >= PRE) & (t["ts"] < CROLLO[1] - pd.Timedelta(days=1))] # DURANTE (02-04/06): lente secondaria
med = lambda c, d=t: float(np.nanmedian(d[c])) if len(d) else np.nan
b = lambda c, d=t: (float(np.nanpercentile(d[c], 10)), float(np.nanpercentile(d[c], 90))) if len(d) else (np.nan, np.nan)
s[asset] = dict(
n=int(len(t)), n_pre=int(len(pre)), n_fuori=n_fuori,
d_short=med("d_short", pre), d_long=med("d_long", pre), width=med("width", pre), ST=float(t["ST"].iloc[0]),
spot_pre=med("spot", pre),
dte=(float(t["dte"].min()), float(t["dte"].max())),
f_net=med("f_net"), f_net_b=b("f_net"),
ivrank=(float(t["ivrank"].min()), float(t["ivrank"].max())),
gate_aperto=float((t["ivrank"] >= VRP_CFG["gate_ivr"]).mean()),
perdita_su_risk=med("perdita_su_risk", pre), perdita_su_risk_b=b("perdita_su_risk", pre),
perdita_su_coll=med("perdita_su_coll", pre),
pnl_real=med("pnl_real", pre), pnl_mod=med("pnl_mod", pre),
f_exp=med("f_exp", pre), f_exp_b=b("f_exp", pre), n_f_exp=int(pre["f_exp"].notna().sum()),
f_mtm=med("f_mtm", pre), f_mtm_b=b("f_mtm", pre),
mtm_real_min=med("mtm_real_min", pre), mtm_mod_min=med("mtm_mod_min", pre),
n_tagli=int(pre["n_tagli"].sum()) if len(pre) else 0, n_ore=int(pre["n_ore"].sum()) if len(pre) else 0,
iv_s_at_min=med("iv_s_at_min", pre), dvol_at_min=med("dvol_at_min", pre),
q_itm_pieno=float(pre["itm_pieno"].mean()) if len(pre) else np.nan,
n_strutture=int(pre.groupby(["k_short", "k_long"]).ngroups) if len(pre) else 0,
f_put_in=med("f_put_in", pre), bleed_put=med("bleed_put", pre),
put_exp=med("put_exp", pre), put_exp_mod=med("put_exp_mod", pre),
put_best=med("put_best", pre), put_best_mod=med("put_best_mod", pre),
f_put_exp=med_ratio(pre, "put_exp", "put_exp_mod"), f_put_best=med_ratio(pre, "put_best", "put_best_mod"),
deb_exp=med("deb_exp", pre), deb_exp_mod=med("deb_exp_mod", pre),
deb_best=med("deb_best", pre), deb_best_mod=med("deb_best_mod", pre),
f_deb_exp=med_ratio(pre, "deb_exp", "deb_exp_mod"), f_deb_best=med_ratio(pre, "deb_best", "deb_best_mod"),
iv_s0=med("iv_s0", pre), iv_s_max=med("iv_s_max", pre), iv_l0=med("iv_l0", pre), iv_l_max=med("iv_l_max", pre),
und_ret=med("und_ret", pre), spot_min=med("spot_min", pre),
durante=dict(n=int(len(dur)), f_exp=med("f_exp", dur), f_mtm=med("f_mtm", dur),
perdita_su_risk=med("perdita_su_risk", dur), f_net=med("f_net", dur),
q_perde=float((dur["pnl_real"] < 0).mean()) if len(dur) else np.nan,
pnl_p10=float(np.nanpercentile(dur["pnl_real"], 10)) if len(dur) else np.nan,
pnl_p90=float(np.nanpercentile(dur["pnl_real"], 90)) if len(dur) else np.nan,
d_short=med("d_short", dur), d_long=med("d_long", dur), width=med("width", dur),
pnl_real=med("pnl_real", dur), pnl_mod=med("pnl_mod", dur)),
)
return s
def med_ratio(d: pd.DataFrame, a: str, b: str, minimo: float = 5.0) -> float:
"""Mediana del rapporto reale/modello sulle righe in cui il modello vale almeno `minimo` $
(un payoff modellato ≈ 0 non da' un rapporto, da' un'altra domanda: si stampano i dollari)."""
x = d[(d[b] >= minimo)]
return float(np.nanmedian(x[a] / x[b])) if len(x) else np.nan
def verdetto(s: dict) -> str:
parti = []
for asset in ASSETS:
r = s.get(asset)
if not r or r.get("n", 0) == 0:
parti.append(f"{asset}: NON misurabile — {r.get('n_fuori', 0) if r else 0} snapshot tutti FUORI geometria (griglia di strike rada)"); continue
if not np.isfinite(r["f_exp"]):
parti.append(f"{asset}: nessun ingresso pre-crollo in geometria (solo DURANTE: f_net {r['durante']['f_net']:.2f}, perde il {r['durante']['q_perde']*100:.0f}%)"); continue
parti.append(f"{asset}: f_net {r['f_net']:.2f} (22/08 in rally 0,714); a scadenza f {r['f_exp']:.2f} ma TAUTOLOGICO nel "
f"{r['q_itm_pieno']*100:.0f}% dei trade (entrambe le gambe ITM ⇒ dipende solo da f_net; {r['n_strutture']} strutture, non {r['n_pre']} osservazioni); "
f"peggior MTM tagliato alla larghezza f {r['f_mtm']:.2f} (quote oltre la larghezza nel {r['n_tagli']/max(1, r['n_ore'])*100:.1f}% delle ore); "
f"gate IV-rank {'APERTO' if r['gate_aperto'] > 0.5 else 'CHIUSO'} nel {r['gate_aperto']*100:.0f}% delle ore")
esito = ("IL CROLLO E' CATTURATO MA A VOL BASSA E FUORI DAL GATE DI VRP01: il f di stress che la regola del 19/06 aspettava "
"NON e' misurato nella sostanza — cio' che e' misurato e' che il rapporto di credito f_net non cambia col regime")
return esito + " — " + "; ".join(parti) + " — n = 1 crollo, 1 scadenza per asset, famiglia INVERSE"
def pct(x): return f"{x*100:+.1f}%"
def main() -> None:
print("=" * 104)
print(" r0909 — IL CROLLO CATTURATO (1-5 giugno 2026): f di stress su quote VERE, scadenza 19/06")
print("=" * 104)
m = misura(); s = sintesi(m)
for asset in ASSETS:
c = m["contesto"][asset]; r = s[asset]
print(f"\n [{asset}] CONTESTO — caduta {pct(c['caduta'])} dal 31/05 al minimo di 1-5/06; "
f"DVOL pre {c['dvol_pre']:.1f} (IV-rank causale {c['ivr_pre']:.2f}) → max {c['dvol_max_crollo']:.1f} "
f"(= {c['pctl_picco']*100:.0f}° percentile storico: il PICCO del crollo sta sotto la mediana); RV5 max {c['rv5_max']:.0f}")
print(" giorno spot ret DVOL RV5")
for d in pd.date_range("2026-05-28", "2026-06-08", tz="UTC"):
if d in c["spot"].index:
dv = c["dvol"].asof(d)
print(f" {d.date()} {c['spot'][d]:>8.0f} {pct(c['ret'].get(d, np.nan)):>7} {dv:>5.1f} {c['rv5'].get(d, np.nan):>5.0f}")
if r is None or r.get("n", 0) == 0:
print(f" nessun trade in geometria VRP01: {r.get('n_fuori', 0) if r else 0} snapshot fuori geometria "
f"(la griglia di strike dell'archivio in quei giorni non ha le δ−0,28/0,10)"); continue
t = m["trades"][asset]; t = t[t["in_geom"]]
print(f"\n [{asset}] A. VRP01 a quote vere — {r['n']} snapshot d'ingresso in geometria ({r['n_pre']} prima del {PRE.date()}; "
f"{r['n_fuori']} scartati fuori geometria), DTE {r['dte'][0]:.0f}-{r['dte'][1]:.0f}, IV-rank {r['ivrank'][0]:.2f}-{r['ivrank'][1]:.2f}")
print(f" geometria mediana pre-crollo: δ corta {r['d_short']:+.2f} / δ lunga {r['d_long']:+.2f}, larghezza {r['width']:.0f}, "
f"spot d'ingresso {r['spot_pre']:.0f}, regolamento {EXP.date()} a {r['ST']:.0f}")
print(f" credito reale/modello f_net {r['f_net']:.2f} [{r['f_net_b'][0]:.2f}, {r['f_net_b'][1]:.2f}] "
f"(22/08: 0,714 in rally)")
print(f" ingressi PRE-crollo, mediana: P&L reale ${r['pnl_real']:+.0f} vs modello ${r['pnl_mod']:+.0f} per unita' "
f"→ perdita {r['perdita_su_risk']*100:.0f}% del rischio [{r['perdita_su_risk_b'][0]*100:.0f}, {r['perdita_su_risk_b'][1]*100:.0f}] "
f"(100% = perdita MASSIMA dello spread; oltre 100 sono le fee), "
f"{r['perdita_su_coll']*100:.1f}% del collaterale")
print(f" f a SCADENZA {r['f_exp']:.2f} [{r['f_exp_b'][0]:.2f}, {r['f_exp_b'][1]:.2f}] su {r['n_f_exp']} snapshot = {r['n_strutture']} strutture "
f"— TAUTOLOGICO nel {r['q_itm_pieno']*100:.0f}% (entrambe le gambe ITM: payoff = larghezza per reale e modello, resta solo f_net)")
print(f" peggior MTM (chiusura tagliata alla larghezza; quote oltre la larghezza nel {r['n_tagli']/max(1, r['n_ore'])*100:.1f}% delle ore-percorso): "
f"f {r['f_mtm']:.2f} [{r['f_mtm_b'][0]:.2f}, {r['f_mtm_b'][1]:.2f}] (reale ${r['mtm_real_min']:+.0f} / modello ${r['mtm_mod_min']:+.0f}); "
f"a quell'ora IV corta {r['iv_s_at_min']:.0f} vs DVOL {r['dvol_at_min']:.0f}")
print(f" IV gamba corta {r['iv_s0']:.0f} → max {r['iv_s_max']:.0f} (il max e' a T→0, non nel crollo); gamba lunga {r['iv_l0']:.0f} → max {r['iv_l_max']:.0f}; "
f"sottostante a scadenza {pct(r['und_ret'])}, minimo {r['spot_min']:.0f}")
du = r["durante"]
print(f" lente secondaria — ingressi DURANTE il crollo (02-04/06, {du['n']} snapshot): δ {du['d_short']:+.2f}/{du['d_long']:+.2f}, "
f"larghezza {du['width']:.0f}, f_net {du['f_net']:.2f}, P&L reale ${du['pnl_real']:+.0f} vs modello ${du['pnl_mod']:+.0f}, "
f"perde nel {du['q_perde']*100:.0f}% (p10 ${du['pnl_p10']:+.0f} / p90 ${du['pnl_p90']:+.0f}: bimodale, la mediana nasconde le perdite), "
f"f scadenza {du['f_exp']:.2f} / peggior MTM {du['f_mtm']:.2f}")
print(f"\n [{asset}] B. LO SPECCHIO LONG (ingressi PRE-crollo, mediana per unita')")
print(f" put δ−0,10 all'ask: costo reale/modello {r['f_put_in']:.2f}, bleed {r['bleed_put']*100:.2f}% dello spot")
print(f" a scadenza: reale ${r['put_exp']:+.0f} vs modello ${r['put_exp_mod']:+.0f} → f_payoff {r['f_put_exp']:.2f}")
print(f" al PICCO del percorso (look-ahead, limite sup.): reale ${r['put_best']:+.0f} vs modello ${r['put_best_mod']:+.0f} → f {r['f_put_best']:.2f}")
print(f" debit spread 0,28/0,10: a scadenza reale ${r['deb_exp']:+.0f} vs modello ${r['deb_exp_mod']:+.0f} → f {r['f_deb_exp']:.2f}; "
f"al picco ${r['deb_best']:+.0f} vs ${r['deb_best_mod']:+.0f} → f {r['f_deb_best']:.2f}")
print(f"\n [{asset}] C. IL MODELLO DEL SLEEVE (`_vrp_weekly_asset`, 0.0 = settimana saltata dai gate) — rendimento settimanale su collaterale:")
print(" " + " ".join(f"{d.date()} {v*100:+.2f}%" for d, v in m["sleeve"][asset].items()))
print(f"\n snapshot d'ingresso (primi 6): " + " | ".join(
f"{row.ts.strftime('%m-%d %H')}h K{row.k_short:.0f}/{row.k_long:.0f} cred ${row.cred_real:.0f} pnl ${row.pnl_real:+.0f} mtm ${row.mtm_real_min:+.0f}"
for row in t.head(6).itertuples()))
print("\n VERDETTO: " + verdetto(s))
print("=" * 104)
if __name__ == "__main__":
main()