diff --git a/docs/diary/2026-06-09-cerbero-bite-credit-spread.md b/docs/diary/2026-06-09-cerbero-bite-credit-spread.md new file mode 100644 index 0000000..390562e --- /dev/null +++ b/docs/diary/2026-06-09-cerbero-bite-credit-spread.md @@ -0,0 +1,47 @@ +# 2026-06-09 — Validazione edge credit-spread cerbero-bite (prezzi reali) + +## Contesto +cerbero-bite (container accanto, `/opt/docker/cerbero-bite`) vende credit-spread su +ETH (bull-put primario, short delta ~0.18, DTE 18, Quarter-Kelly 13%, PT 50% / stop +2.5x credito / delta-breach 0.30 / vol-stop +10 DVOL / time-stop 7 DTE; testnet, +propose-only). Tune "Profilo B" del 2026-06-09: short delta alzato a 0.18 (da 0.10-0.15) +e `credit_to_width_ratio_min` 0.30->0.08 perche' a delta basso 0 spread erano eleggibili. +Domanda: l'edge regge su un ciclo ETH completo, o e' profittevole solo nei campioni calmi? + +Validato con l'infrastruttura opzioni REALE (data/options/, importata da cerbero-bite). +Script riprendibile: `scripts/analysis/cerbero_bite_credit_spread.py`. + +## Risultati +1. **Economia d'ingresso reale** (chain, 3145 spread): cw a delta 0.18 = **0.106** (p25 0.085), + eleggibilita' **65%**, short strike **~9.4% OTM** (NON 18% — quello era il vecchio delta basso), + **max-loss/credito = 8.4x**. +2. **Tail model-free** (8 anni ETH reali, cw 0.106, hold-to-expiry, niente modello opzioni): + win-rate 74%, **EV -1.0 crediti/trade, 7/9 anni NEGATIVI**, ETH <-13.4% a 17g (max-loss) il + **17.8%** delle volte. Un max-loss (-8.4cr) cancella 17 PT-winner. Gap 1g p5 = -25% (salta lo stop). +3. **Managed (skew calibrato sulle IV reali)**: win-rate **37%** (delta-breach esce sul 62% dei + trade a piccola perdita), **EV -0.02 cr/trade** (gia' a economia favorevole), worst -4.6 (2023). + 2021+ EV -0.022. + +## Caveat di calibrazione (TODO aperto) +Il mark mid+skew da **cw 0.228 vs 0.106 reale** -> sovrastima il credito ~2x (manca bid/ask +incrociato sulle 2 gambe + griglia strike reale). Quindi l'EV managed mostrato (-0.02) e' a +economia **2x troppo favorevole**: l'EV vero e' **<=**. Per il numero esatto: modellare bid/ask +reale + griglia (entrambi nella chain) cosi' entry cw -> 0.106. RIPRENDERE da qui. + +## Verdetto +- **NON edge robusto su ciclo completo.** Il "+0.48%/mese netto" citato era **artefatto di + finestra calma** (mag-giu 2026, nessun crash): li' il body vince (EV +0.6), ma su ciclo + completo (mesi -13% al 18% di frequenza) e' breakeven-to-negativo in 2 stime indipendenti. +- **L'82% PoP e' ingannevole**: o esci presto sul delta-breach (win 37%, grind di piccole + perdite), o tieni e prendi la coda. Non c'e' la "macchina da 82% di vittorie". +- **Il tune Profilo B PEGGIORA la coda** (vendere a 9.4% OTM mette il max-loss in zona di + pullback ordinario). Strutturale: il mercato non paga per vendere lontano, vendere vicino + espone alla coda. +- **Coda CONCENTRATA con PythagorasGoal**: il gap che salta lo stop = il crash ETH, lo stesso + evento che colpisce lo sleeve fade. I due sistemi non diversificano, concentrano. +- **Azione**: cerbero-bite resta testnet/paper finche' non c'e' un campione che include un crash + vero; valutare un long deep-OTM put (0.33%/mese reale) come cap della coda del bull-put-spread. + +## Stato +DA RIPRENDERE: calibrazione esatta credito (bid/ask + griglia) -> EV managed definitivo. +Tutto il resto e' chiuso e documentato. diff --git a/scripts/analysis/cerbero_bite_credit_spread.py b/scripts/analysis/cerbero_bite_credit_spread.py new file mode 100644 index 0000000..cbf73b0 --- /dev/null +++ b/scripts/analysis/cerbero_bite_credit_spread.py @@ -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()