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:
@@ -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.
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user