bb40e87d13
Quattro misure (r0909_*), nessun cambio a libro/pesi/config: - libro nei crolli: −0,48%/g nei 160 giorni ≤ −5%, positivo per finestra dal 2022 per la gamba short di SKH01 (108% dei guadagni); beta 0,0769 riprodotto (§46); il peso di SKH01 resta chiuso dal gate (in-sample). - crollo catturato 1-5/06/2026 a quote vere: f_net 0,74 = rally; put δ−0,10 1,92× il modello; a vol bassa e fuori dal gate di VRP01 → §3 non si riapre. - XRP terza gamba (harness r0822_sol_leg): hold-out −0,169 in 0/24, un anno buono. - universo Deribit: liquidi solo BTC/ETH/XRP/SOL; XS01 13/19; BTCDVOL future non negoziabile; PAXG non misurato. Revisione fable: due conclusioni smontate (MTM 2,6× era un artefatto di quote; «8/12 guadagna» era l'ordine delle classi). Debito §5.18: cblib.spot_series + asof guarda un'ora avanti (feed 1h etichettato all'apertura). Test Opus: 92 nuovi, suite 1008/1008; due difetti del verdetto XRP corretti. Docs: diario, RESULTS §74-77, CLAUDE.md, memoria 20/50, README; journal 07-08/09. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017zJjHUS7mf4pnGE6pq9RTt
386 lines
25 KiB
Python
386 lines
25 KiB
Python
"""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. Qui lo spot e' `spot_causale` = chiusura dell'ultima barra chiusa a ts.
|
||
⚠️ `cblib.spot_series` ha lo stesso difetto (e quindi il f 0,714 di §11): registrato, non toccato.
|
||
* 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 spot_causale(S: pd.Series) -> pd.Series:
|
||
"""Il feed 1h e' etichettato all'apertura: la chiusura della barra T e' nota a T+1h. Spostando
|
||
l'indice di +1h, `asof(ts)` restituisce l'ultima chiusura CONOSCIUTA a ts, mai una futura."""
|
||
out = S.copy(); out.index = out.index + pd.Timedelta(hours=1)
|
||
return out
|
||
|
||
|
||
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 = spot_causale(CB.spot_series(asset)), CB.dvol_series(asset)
|
||
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
|
||
hist_all = V[V.index < CROLLO[0]]
|
||
lr = np.log(pd_).diff()
|
||
rv5 = lr.rolling(5).std() * np.sqrt(365.25) * 100
|
||
dv = V[(V.index >= pd.Timestamp("2026-05-20", tz="UTC")) & (V.index <= EXP)]
|
||
hist = V[V.index < CROLLO[0]]
|
||
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 < V[(V.index >= CROLLO[0]) & (V.index <= CROLLO[1])].max()).mean()),
|
||
ivr_pre=float((hist < 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()
|