research(cerbero-bite): validazione edge credit-spread su prezzi reali

Backtest dell'edge credit-spread ETH di cerbero-bite con la chain reale
(data/options/). Esito: NON edge robusto su ciclo completo. Entry cw reale
0.106 (short 9.4% OTM, max-loss/credito 8.4x); hold-to-expiry EV -1.0 cr/trade
7/9 anni negativi; managed (skew) EV -0.02 cr win-rate 37%. Il "+0.48%/mese"
era artefatto di finestra calma; coda concentrata col fade ETH. TODO aperto:
calibrazione esatta credito (bid/ask + griglia) per EV managed definitivo.
Script riprendibile + diario.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-09 08:08:53 +00:00
parent cfc48cdef4
commit 5d45f4ef6e
2 changed files with 191 additions and 0 deletions
@@ -0,0 +1,144 @@
"""Validazione dell'edge del credit-spread di cerbero-bite sui PREZZI REALI.
cerbero-bite (container accanto) vende credit spread su ETH (bull-put primario,
short delta ~0.18, DTE 18, PT 50% / stop 2.5x credito / delta-breach 0.30 / vol-stop
+10 DVOL / time-stop 7 DTE). Domanda: l'edge regge su un CICLO ETH completo, o e'
profittevole solo nei campioni calmi?
Tre analisi (riprendibili):
1) entry_economics() -> economia d'ingresso REALE dalla chain (data/options/eth_chain.parquet):
credit/width effettivo a delta 0.18 dai bid/ask veri, eleggibilita' sotto i gate liquidita'.
2) tail_model_free() -> esito terminale dai prezzi ETH reali (2018-2026), cw reale 0.106,
NESSUN modello opzioni (niente errore BS): win-rate, EV, frequenza max-loss.
3) managed_backtest() -> lifecycle CON management; mark con skew calibrato sulle IV reali.
ESITO (2026-06-09):
- cw reale a delta 0.18 = 0.106 (short ~9.4% OTM, NON 18%), max-loss/credito = 8.4x, eleggibilita' 65%.
- hold-to-expiry @0.106: EV -1.0 crediti/trade, 7/9 anni NEGATIVI, max-loss 17.8% delle volte.
- managed (skew): EV -0.02 cr/trade, win-rate 37% (delta-breach esce sul 62% dei trade a piccola perdita).
- VERDETTO: NON edge robusto su ciclo completo. Il "+0.48%/mese" era artefatto di finestra calma
(mag-giu 2026, no crash). Premium-selling a skew negativo: vince nei campioni calmi, restituisce
tutto (o piu') nei crash. Tune "Profilo B" (vendere a 9.4% OTM) PEGGIORA la frequenza di max-loss.
Coda CONCENTRATA col fade ETH di PythagorasGoal (stesso crash colpisce entrambi).
TODO APERTO (per nail-are l'EV managed esatto): la calibrazione non e' ancora perfetta
(mark mid+skew da cw 0.228 vs 0.106 reale -> sovrastima il credito ~2x). Manca: modellare
bid/ask reale incrociato sulle 2 gambe + griglia strike reale (entrambi nella chain) cosi'
l'entry cw scende a 0.106 e l'EV managed diventa esatto. Allora chiudere il sì/no definitivo.
uv run python scripts/analysis/cerbero_bite_credit_spread.py
"""
from __future__ import annotations
import sys, math, collections
from pathlib import Path
import numpy as np, pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.options_chain import OptionChain, load_market
from scripts.analysis.explore_lab import get_df
from scripts.analysis.option_overlay_lab import bs_put, _ncdf, dvol_for
SHORT_OTM, LONG_OTM, DTE = 0.094, 0.134, 17 # da chain reale (delta 0.18, width 4%)
CW_REAL = 0.106
def entry_economics():
oc = OptionChain("ETH"); ch = oc.df
mk = load_market("ETH")[["ts_ms", "spot"]].dropna().sort_values("ts_ms")
p = ch[ch["option_type"] == "P"].copy()
p = pd.merge_asof(p.sort_values("ts_ms"), mk, on="ts_ms", direction="backward")
cand = p[(p["tenor_d"] >= 14) & (p["tenor_d"] <= 21)].dropna(subset=["delta", "bid", "ask", "strike", "spot"])
rows = []
for (ts, exp), g in cand.groupby(["timestamp", "expiry"]):
spot = g["spot"].iloc[0]
sc = g[(g["delta"] <= -0.12) & (g["delta"] >= -0.22)]
if sc.empty: continue
short = sc.iloc[(sc["delta"] + 0.18).abs().argmin()]
Ks = short["strike"]; longc = g[g["strike"] < Ks]
if longc.empty: continue
longp = longc.iloc[(longc["strike"] - (Ks - spot * 0.04)).abs().argmin()]
W = Ks - longp["strike"]
if W <= 0: continue
credit = short["bid"] - longp["ask"]
def ok(o):
sp = (o["ask"] - o["bid"]) / ((o["ask"] + o["bid"]) / 2) if (o["ask"] + o["bid"]) > 0 else 9
return (o["open_interest"] or 0) >= 100 and sp <= 0.15 and o["bid"] > 0
cw = credit / (W / spot)
rows.append(dict(cw=cw, credit=credit, elig=ok(short) and ok(longp) and cw >= 0.08 and credit > 0,
short_otm=(spot - Ks) / spot, delta=short["delta"]))
r = pd.DataFrame(rows)
print(f"[ENTRY] {len(r)} spread | eleggibili {r['elig'].mean()*100:.0f}% | cw mediano {r['cw'].median():.3f} "
f"| short OTM {r['short_otm'].median()*100:.1f}% | max-loss/credito {((1-r['cw'].median())/r['cw'].median()):.1f}x")
def tail_model_free():
df = get_df("ETH", "1h"); c = df["close"].values; n = len(c)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True); H = DTE * 24
res = []
for i in range(200, n - H - 1, 24 * 2):
S0 = c[i]; Ks = S0 * (1 - SHORT_OTM); Kl = S0 * (1 - LONG_OTM); W = Ks - Kl
Sx = c[i + H]; intr = min(max(Ks - Sx, 0.0), W); credit = CW_REAL * W
res.append((ts.iloc[i].year, 1 - intr / credit, Sx < Kl))
R = pd.DataFrame(res, columns=["y", "pnl", "maxloss"]); P = R["pnl"].values
print(f"[TAIL model-free @cw0.106] win {(P>0).mean()*100:.0f}% | EV {P.mean():+.2f}cr | max-loss {R['maxloss'].mean()*100:.0f}% "
f"| anni neg {(R.groupby('y')['pnl'].mean()<0).sum()}/{R['y'].nunique()}")
def _skew_fit():
oc = OptionChain("ETH"); ch = oc.df
mk = load_market("ETH")[["ts_ms", "spot"]].dropna().sort_values("ts_ms")
p = ch[ch["option_type"] == "P"].copy()
p = pd.merge_asof(p.sort_values("ts_ms"), mk, on="ts_ms", direction="backward")
p = p.dropna(subset=["iv", "strike", "spot", "delta", "tenor_d"])
p = p[(p["tenor_d"] >= 7) & (p["tenor_d"] <= 35) & (p["iv"] > 0)]
p["dd"] = (p["delta"] + 0.5).abs()
atm = p.sort_values("dd").groupby("timestamp")["iv"].first()
p["atm_iv"] = p["timestamp"].map(atm); p = p.dropna(subset=["atm_iv"])
p["k"] = np.log(p["strike"] / p["spot"]); p["ratio"] = p["iv"] / p["atm_iv"]
p = p[(p["k"] > -0.35) & (p["k"] < 0.15) & (p["ratio"] > 0.5) & (p["ratio"] < 3)]
coef, *_ = np.linalg.lstsq(np.c_[p["k"], p["k"]**2], p["ratio"] - 1.0, rcond=None)
return coef # a, b
def managed_backtest():
a, b = _skew_fit()
def ivol(S, K, atm):
k = math.log(K / S); return max(atm * (1 + a * k + b * k * k), 0.05)
def put_delta(S, K, T, sig):
if T <= 0 or sig <= 0: return -1.0 if S < K else 0.0
return _ncdf((math.log(S / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T))) - 1.0
def mark(S, Ks, Kl, T, atm):
return bs_put(S, Ks, T, ivol(S, Ks, atm)) - bs_put(S, Kl, T, ivol(S, Kl, atm))
df = get_df("ETH", "1h"); c = df["close"].values; n = len(c)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True); dvol = dvol_for(df, "ETH")
H = DTE * 24; STEP = 6; cw = []; tr = []
for i in range(200, n - H - 1, 24 * 2):
S0 = c[i]; atm0 = dvol[i] if not np.isnan(dvol[i]) else 0.6
Ks = S0 * (1 - SHORT_OTM); Kl = S0 * (1 - LONG_OTM); W = Ks - Kl
credit = mark(S0, Ks, Kl, DTE / 365.0, atm0)
if credit <= 0: continue
cw.append(credit / W); pnl = why = None
for k in range(STEP, H + 1, STEP):
j = i + k; Trem = max((H - k) / (24 * 365.0), 1e-6); Sj = c[j]
atmj = dvol[j] if not np.isnan(dvol[j]) else atm0; mk = mark(Sj, Ks, Kl, Trem, atmj)
if mk <= 0.5 * credit: pnl, why = 1 - mk / credit, "PT"; break
if mk >= 2.5 * credit: pnl, why = 1 - mk / credit, "stop"; break
if put_delta(Sj, Ks, Trem, ivol(Sj, Ks, atmj)) <= -0.30: pnl, why = 1 - mk / credit, "delta"; break
if atmj - atm0 >= 0.10: pnl, why = 1 - mk / credit, "vol"; break
if k >= (DTE - 7) * 24: pnl, why = 1 - mk / credit, "time"; break
if pnl is None:
Sx = c[i + H]; intr = min(max(Ks - Sx, 0), W); pnl, why = 1 - intr / credit, "expiry"
tr.append((ts.iloc[i].year, pnl, why))
P = np.array([t[1] for t in tr])
print(f"[MANAGED skew] cw@entry {np.median(cw):.3f} (vs 0.106 reale: sovrastima ~2x, EV vero <=) | "
f"win {(P>0).mean()*100:.0f}% | EV {P.mean():+.3f}cr | worst {P.min():.1f} | "
f"uscite {dict(collections.Counter(t[2] for t in tr))}")
R = pd.DataFrame({"y": [t[0] for t in tr], "p": P})
print(f" 2021+: EV {R[R.y>=2021]['p'].mean():+.3f}cr/trade")
if __name__ == "__main__":
entry_economics()
tail_model_free()
managed_backtest()