8c18e82f1a
Prima integrazione della catena opzioni Deribit mainnet accumulata da cerbero-bite (/opt/docker/cerbero-bite, dal 2026-06-09: entrambe le ali, 1g-3mesi, oraria, con book_depth). E' l'unica fonte di prezzi opzioni VERI del progetto, e non e' ricostruibile a posteriori: Deribit non serve book storici, un'ora non raccolta e' persa. VRP01 prezza entrambe le gambe con BS su DVOL ATM (VRP_CFG f=1.0 sul credito NETTO). Misurato agli STESSI strike su 8/8 scadenze settimanali con entrambe le gambe quotate (delta -0.270/-0.099 contro target -0.280/-0.100): f gamba corta 1.02 <- replica la calibrazione del 20/06 f gamba lunga 2.30 <- l'ala che si COMPRA f credito NETTO 0.73 IC95% [0.698, 0.780], 0/15 osservazioni >= 1.0 Meccanismo, non rumore: IV(corta)-DVOL +0.8pp ma IV(lunga)-DVOL +7.5pp -> il modello prezza a vol ATM anche l'ala comprata. Il difetto non e' nel premio incassato ma nella protezione comprata, cioe' proprio il "defined-risk" per cui v2 fu promosso. Conseguenza standalone (solo f): 1.00 -> FULL 1.08 / HOLD +0.58; 0.80 -> 0.51 / -0.02; 0.73 -> 0.31 / -0.23. Book 5-sleeve: FULL -0.069, HOLD -0.103, DD invariato = dentro la banda d'ancora, ma ~meta' del contributo LOO di VRP01 era il prezzo che il modello si faceva da solo. VRP_CFG["f"] NON cambiato: 15 osservazioni, 7 settimane, e 0/8 passano il gate IV-rank>0.30 -> il f e' misurato nel regime in cui il sleeve sta FLAT. Caveat quantificato, non nuovo parametro. Il criterio del 19/06 (rivalutare quando cerbero-bite cattura un crash) e' intatto. Book, pesi, cron, config INVARIATI. Regole nuove congelate nei test: - il f di una struttura multi-gamba non e' il f di una sua gamba (misurare la sola gamba venduta da' la risposta sbagliata con segno rassicurante); - un guasto IN CORSO si misura al GIORNO PEGGIORE, non in media (la prima stesura della certificazione diluiva un guasto di 2 giorni da 51.7% a 13.5%); - una riga presente non e' un dato presente. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
177 lines
8.7 KiB
Python
177 lines
8.7 KiB
Python
"""VRP01 CONTRO LE QUOTE REALI — il f del credito NETTO, misurato invece che assunto (2026-07-30).
|
|
|
|
Il sleeve VRP01 (`src/portfolio/sleeves.VRP_CFG`) usa **f=1.0** applicato al credito NETTO:
|
|
net_prem = (bs(Ks) - bs(Kl)) * f
|
|
cioe' assume che il mercato paghi esattamente cio' che BS-su-DVOL-ATM dice, per ENTRAMBE le gambe.
|
|
Il caveat di ammissione ("premio MODELLATO su DVOL ATM, skew non esplicito") era dichiarato ma mai
|
|
quantificato, per mancanza di quote vere. cerbero-bite le accumula dal 2026-06-09 (catena piena,
|
|
entrambe le ali, orarie) -> qui si misura.
|
|
|
|
Quattro domande, nell'ordine in cui contano:
|
|
D1 COPERTURA quante settimane hanno ENTRAMBE le gambe (-0.28/-0.10) sulla stessa scadenza?
|
|
D2 f REALE credito reale / credito modellato, agli STESSI strike (isola il prezzo, non lo strike)
|
|
D3 GESTIONE le stesse gambe sono riquotate fino a scadenza? (50%-profit-take e DD infra-settimana)
|
|
D4 REGIME la finestra contiene il regime in cui VRP01 TRADA davvero (gate IV-rank>0.30)?
|
|
|
|
uv run python scripts/research/r0730_vrp_real_quotes.py
|
|
uv run python scripts/research/r0730_vrp_real_quotes.py --book # + delta sul book (lento)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
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"))
|
|
|
|
from cblib import ( # noqa: E402
|
|
DTE_HI, DTE_LO, close_cost_path, dvol_series, load_chain, puts, weekly_entries,
|
|
)
|
|
|
|
ASSETS = ("ETH", "BTC")
|
|
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
|
|
GATE_IVR = 0.30 # il gate di VRP01: sotto questa soglia il sleeve sta FLAT
|
|
FEE_FRAC = 0.125 # stessa fee del modello (cap Deribit, worst-case)
|
|
|
|
|
|
def _met(r: pd.Series) -> tuple[float, float, float]:
|
|
r = r.dropna()
|
|
if len(r) < 10 or r.std() == 0:
|
|
return float("nan"), float("nan"), float("nan")
|
|
sh = float(r.mean() / r.std() * np.sqrt(365.25))
|
|
eq = (1 + r).cumprod()
|
|
return sh, float((eq / eq.cummax() - 1).min()), float(eq.iloc[-1] ** (365.25 / len(r)) - 1)
|
|
|
|
|
|
def boot_ci_median(x: np.ndarray, n: int = 20000, seed: int = 7) -> tuple[float, float]:
|
|
rng = np.random.default_rng(seed)
|
|
med = [np.median(rng.choice(x, size=len(x), replace=True)) for _ in range(n)]
|
|
return float(np.percentile(med, 2.5)), float(np.percentile(med, 97.5))
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--book", action="store_true", help="ricalcola anche il book a 5 sleeve (lento)")
|
|
args = ap.parse_args()
|
|
|
|
chain = load_chain()
|
|
E = pd.concat([weekly_entries(a, chain) for a in ASSETS], ignore_index=True)
|
|
|
|
print("=" * 100)
|
|
print(" VRP01 CONTRO LE QUOTE REALI (cerbero-bite mainnet)")
|
|
print("=" * 100)
|
|
|
|
# ---------------- D1 ----------------
|
|
print("\n [D1] COPERTURA")
|
|
for a in ASSETS:
|
|
p = puts(a, chain)
|
|
wk = p[(p["dte"] >= DTE_LO) & (p["dte"] <= DTE_HI)]
|
|
n = int((E["asset"] == a).sum())
|
|
print(f" {a}: {len(p):,} righe put ({p['ts'].min():%Y-%m-%d}->{p['ts'].max():%Y-%m-%d}), "
|
|
f"{100*(1-p['bid'].isna().mean()):.1f}% quotate | finestra 4-10 DTE: {wk['exp'].nunique()} "
|
|
f"scadenze -> {n} ingressi con ENTRAMBE le gambe")
|
|
|
|
# ---------------- D2 ----------------
|
|
ok = E[E["cred_real"] > 0].copy() # credito negativo = struttura non eseguibile a credito
|
|
scart = len(E) - len(ok)
|
|
lo, hi = boot_ci_median(ok["f_net"].to_numpy())
|
|
nota_scarti = f", {scart} scartati per credito negativo" if scart else ""
|
|
print(f"\n [D2] f = credito reale / credito modellato (N={len(ok)}{nota_scarti})")
|
|
print(f" {'grandezza':44s} {'mediana':>9s} {'p25':>8s} {'p75':>8s}")
|
|
for lab, col in (("f gamba CORTA (venduta al bid)", "f_short"),
|
|
("f gamba LUNGA (comprata all'ask)", "f_long"),
|
|
("f CREDITO NETTO <- quello che entra nel book", "f_net"),
|
|
("f credito netto, a mid", "f_net_mid")):
|
|
v = ok[col].replace([np.inf, -np.inf], np.nan).dropna()
|
|
print(f" {lab:44s} {v.median():9.2f} {v.quantile(.25):8.2f} {v.quantile(.75):8.2f}")
|
|
print(f" f netto: IC95% bootstrap [{lo:.3f}, {hi:.3f}] "
|
|
f"osservazioni >= 1.0: {int((ok['f_net'] >= 1).sum())}/{len(ok)}")
|
|
|
|
ok["skew_short"] = ok["iv_short"] - ok["dvol_pct"]
|
|
ok["skew_long"] = ok["iv_long"] - ok["dvol_pct"]
|
|
print(f"\n MECCANISMO (senza il quale il numero sopra e' un artefatto):")
|
|
print(f" IV(corta) - DVOL = {ok['skew_short'].median():+.1f} pp "
|
|
f"IV(lunga) - DVOL = {ok['skew_long'].median():+.1f} pp")
|
|
print(f" -> il modello prezza entrambe le gambe a vol ATM; l'ala che si COMPRA sta "
|
|
f"{ok['skew_long'].median():.0f} pp sopra")
|
|
print(f" quindi costa ~{ok['f_long'].median():.1f}x il modello e il credito netto si comprime.")
|
|
print(f" delta realizzati: corta {ok['d_short'].median():.3f} (target -0.280), "
|
|
f"lunga {ok['d_long'].median():.3f} (target -0.100) -> non e' un artefatto di strike")
|
|
|
|
# ---------------- D3 ----------------
|
|
print(f"\n [D3] GESTIONE — path a quote reali (il modello tiene a scadenza e non lo vede)")
|
|
marks, tocca50, worst = [], 0, []
|
|
for _, e in ok.iterrows():
|
|
P = close_cost_path(e, chain)
|
|
marks.append(len(P))
|
|
if P.empty:
|
|
continue
|
|
if (P["pnl_aperto"] >= 0.5 * e["cred_real"]).any():
|
|
tocca50 += 1
|
|
worst.append(P["pnl_aperto"].min() / e["width"])
|
|
marks = np.array(marks)
|
|
print(f" riquotazioni con ENTRAMBE le gambe: mediana {np.median(marks):.0f} per trade "
|
|
f"(min {marks.min()}, max {marks.max()})")
|
|
print(f" settimane in cui il 50%-profit-take sarebbe scattato: {tocca50}/{len(ok)}")
|
|
if worst:
|
|
w = np.array(worst)
|
|
# N < len(ok): gli ingressi piu' recenti non hanno ancora un path (trade in corso)
|
|
print(f" peggior mark INFRA-settimana (% del capitale=width), su N={len(w)} trade con "
|
|
f"path: mediana {100*np.median(w):.1f}%, minimo {100*w.min():.1f}%")
|
|
print(f" -> il sleeve marca solo a scadenza: quel minimo non esiste nei suoi numeri")
|
|
|
|
# ---------------- D4 ----------------
|
|
print(f"\n [D4] REGIME — la finestra contiene i trade che VRP01 farebbe?")
|
|
for a in ASSETS:
|
|
V = dvol_series(a)
|
|
g = E[E["asset"] == a]
|
|
win = V[(V.index >= g["ts"].min()) & (V.index <= g["ts"].max())]
|
|
pas = int((g["ivrank"] > GATE_IVR).sum())
|
|
print(f" {a}: DVOL finestra [{win.min():.1f}, {win.max():.1f}] vs storia "
|
|
f"[{V.min():.1f}, {V.max():.1f}] — il MAX raccolto sta al {100*(V < win.max()).mean():.0f}° pctl")
|
|
print(f" IV-rank mediano {g['ivrank'].median():.2f} — settimane che passano il gate "
|
|
f">{GATE_IVR}: {pas}/{len(g)}")
|
|
print(f" -> il f e' misurato nel regime in cui il sleeve sta FLAT. Lo skew e' strutturale")
|
|
print(f" (in stress si irripidisce), ma la TAGLIA in stress resta non misurata.")
|
|
|
|
# ---------------- conseguenza sul modello ----------------
|
|
from src.portfolio import sleeves as SL
|
|
print(f"\n CONSEGUENZA — VRP01 standalone sostituendo SOLO f (f=1.0 = cio' che il book usa oggi)")
|
|
print(f" {'f':>6s} {'Sh FULL':>9s} {'DD FULL':>9s} {'CAGR':>8s} {'Sh HOLD-OUT':>12s}")
|
|
f_mis = float(ok["f_net"].median())
|
|
f_mid = float(ok["f_net_mid"].median())
|
|
for f in (1.00, round(f_mid, 2), round(f_mis, 2)):
|
|
SL.VRP_CFG["f"] = f
|
|
s = SL._vrp_combo_returns()
|
|
shF, ddF, cg = _met(s)
|
|
shH, _, _ = _met(s[s.index >= HOLDOUT])
|
|
tag = " <- assunto" if f == 1.0 else (" <- misurato a mid" if f == round(f_mid, 2)
|
|
else " <- misurato, fill conservativo")
|
|
print(f" {f:6.2f} {shF:9.2f} {100*ddF:8.1f}% {100*cg:7.1f}% {shH:12.2f}{tag}")
|
|
SL.VRP_CFG["f"] = 1.0
|
|
|
|
if args.book:
|
|
from src.portfolio.portfolio import StrategyPortfolio
|
|
print(f"\n BOOK a 5 sleeve (VRP01 al 12%) — livelli ancora-canonici, conta il DELTA")
|
|
base = None
|
|
for f in (1.00, round(f_mis, 2)):
|
|
SL.VRP_CFG["f"] = f
|
|
r = StrategyPortfolio(SL.active_sleeves()).combined_daily()
|
|
shF, ddF, _ = _met(r)
|
|
shH, _, _ = _met(r[r.index >= HOLDOUT])
|
|
if base is None:
|
|
base = (shF, shH)
|
|
d = "" if f == 1.0 else f" (Δ {shF-base[0]:+.3f} FULL / {shH-base[1]:+.3f} HOLD)"
|
|
print(f" f={f:.2f} FULL {shF:.3f} HOLD {shH:.3f} DD {100*ddF:.2f}%{d}")
|
|
SL.VRP_CFG["f"] = 1.0
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|