research: porta artefatti da strategy-research-calendar (tracks F-I + eval crypto_backtest + lead OPZIONI/VRP)
Dal branch parallelo strategy-research-calendar (continuazione della linea TP01). Porta su main il record di ricerca + la fondazione del lead opzioni (NIENTE blob dati, niente codice in conflitto): - Tracks F/G/H/I (seasonality/calendar, prior-levels, volume-vol, momentum-reversal): tutti NEGATIVI/spurii -> confermano il soffitto Sharpe ~1.3 su BTC/ETH direzionale (calendar = buy&hold travestito; mean-reversion morta anche a fee 0). Diari + script. - trackD_lookahead_audit.py: audit anti-look-ahead (stesso esito del nostro fix >=12h). - eval-crypto-backtest-options.md: valutazione strategia esterna crypto_backtest. Cross-valida TP01 (il loro sleeve spot 12h ~ TP01: due ricerche indipendenti, stessa conclusione). Identifica il LEAD: sleeve income OPZIONI (vendita put settimanali delta-0.28, VRP IV>RV), scorrelato ~0.22 al trend -> via per superare il soffitto ~1.3. - options_real_quote_check.py + cerbero-bite-mainnet-verified.md: VERIFICATO su QUOTE REALI Deribit mainnet (cerbero-bite/MCP = mainnet, bit-identico a ccxt.deribit). Premio reale (BID, con skew) = 1.29x il modellato -> il backtest SOTTOSTIMA il premio; il rischio vero e' la CODA (short-vol) + liquidita' di roll in stress, non la magnitudine. NB: lo sleeve opzioni e' un LEAD, NON deployato: prezzato da modello (BS su DVOL) + 1 snapshot in regime calmo. Serve validazione real-chain multi-regime + stress crash + paper su testnet prima di aggiungerlo al portafoglio. Portafoglio attivo invariato: TP01 70% + XS01 30%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
"""VERIFICA SLEEVE OPZIONI su QUOTE REALI Deribit — quanto Sharpe sopravvive a bid/ask + skew.
|
||||
|
||||
Lo sleeve income della strategia esterna `crypto_backtest` (vendita di put settimanali CSP su
|
||||
BTC, delta 0.28) e' backtestato su prezzi MODELLATI: Black-Scholes prezzato con DVOL = IV ATM, e
|
||||
si incassa il premio "fair" (mid). Due gap reali NON catturati:
|
||||
(1) BID/ASK: vendendo si incassa il BID, non il mid.
|
||||
(2) SKEW: una put OTM (delta 0.28) ha IV piu' alta della ATM (DVOL) -> il modello prezza la put
|
||||
con la vol sbagliata.
|
||||
|
||||
Questo script:
|
||||
PARTE 1 (rete, Deribit mainnet pubblico): scarica la catena REALE della scadenza ~settimanale,
|
||||
trova la put a delta ~0.28, e misura:
|
||||
- premio reale incassabile (BID, in USD) vs premio modellato (BS @ IV ATM)
|
||||
- skew: IV della put OTM (mark) vs IV ATM (mark)
|
||||
- spread: bid/mark
|
||||
- HAIRCUT netto f = premio_bid_reale / premio_BS@ATM
|
||||
PARTE 2 (locale): ri-esegue lo sleeve CSP settimanale (dati + modulo del progetto esterno) con
|
||||
il premio moltiplicato per f -> Sharpe/CAGR/maxDD reali stimati, vs i modellati.
|
||||
|
||||
NB ONESTO: e' UNO SNAPSHOT (la catena di oggi). Lo spread si allarga nello stress; lo skew varia.
|
||||
Va ripetuto nel tempo per robustezza. Ma misura direttamente i due gap col mercato vero.
|
||||
|
||||
uv run python scripts/research/options_real_quote_check.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
EXT = Path("/home/adriano/crypto_backtest")
|
||||
sys.path.insert(0, str(EXT))
|
||||
|
||||
PUT_DELTA = 0.28
|
||||
CYCLE_DAYS = 7
|
||||
ANN = 365
|
||||
|
||||
|
||||
def fetch_real_chain():
|
||||
import ccxt
|
||||
ex = ccxt.deribit({"enableRateLimit": True})
|
||||
ex.load_markets()
|
||||
puts = [m for m in ex.markets.values()
|
||||
if m.get("option") and m["base"] == "BTC" and m["optionType"] == "put"]
|
||||
calls = [m for m in ex.markets.values()
|
||||
if m.get("option") and m["base"] == "BTC" and m["optionType"] == "call"]
|
||||
# expiries -> pick the one closest to CYCLE_DAYS days out
|
||||
now = pd.Timestamp.utcnow().tz_localize(None)
|
||||
def exp_dt(m):
|
||||
return pd.to_datetime(m["symbol"].split("-")[1], format="%y%m%d")
|
||||
exps = sorted(set(exp_dt(m) for m in puts))
|
||||
target = now + pd.Timedelta(days=CYCLE_DAYS)
|
||||
expiry = min(exps, key=lambda e: abs((e - target).days))
|
||||
dte = (expiry - now).days + (expiry - now).seconds / 86400
|
||||
chain_puts = [m for m in puts if exp_dt(m) == expiry]
|
||||
chain_calls = [m for m in calls if exp_dt(m) == expiry]
|
||||
print(f" scadenza scelta: {expiry.date()} (DTE ~{dte:.1f}g, target {CYCLE_DAYS}g) "
|
||||
f"strikes put: {len(chain_puts)}")
|
||||
|
||||
def tick(m):
|
||||
try:
|
||||
t = ex.fetch_ticker(m["symbol"])
|
||||
i = t["info"]
|
||||
g = i.get("greeks") or {}
|
||||
return dict(symbol=m["symbol"], strike=float(m["strike"]),
|
||||
delta=float(g.get("delta", "nan")), mark_iv=float(i.get("mark_iv", "nan")),
|
||||
bid=float(i.get("best_bid_price") or 0), ask=float(i.get("best_ask_price") or 0),
|
||||
mark=float(i.get("mark_price") or 0),
|
||||
S=float(i.get("underlying_price") or i.get("index_price") or 0))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
rows = [r for r in (tick(m) for m in chain_puts) if r and np.isfinite(r["delta"])]
|
||||
callrows = [r for r in (tick(m) for m in chain_calls) if r and np.isfinite(r["delta"])]
|
||||
return expiry, dte, pd.DataFrame(rows), pd.DataFrame(callrows)
|
||||
|
||||
|
||||
def bs_put(S, K, T, sigma):
|
||||
from scipy.stats import norm
|
||||
if T <= 0 or sigma <= 0:
|
||||
return max(0.0, K - S)
|
||||
d1 = (np.log(S / K) + 0.5 * sigma ** 2 * T) / (sigma * np.sqrt(T))
|
||||
d2 = d1 - sigma * np.sqrt(T)
|
||||
return K * norm.cdf(-d2) - S * norm.cdf(-d1)
|
||||
|
||||
|
||||
def measure_haircut(dte, puts, calls):
|
||||
S = puts["S"].iloc[0]
|
||||
T = dte / ANN
|
||||
# ATM IV: option with |delta| closest to 0.5 (use calls+puts mark_iv near ATM)
|
||||
allo = pd.concat([puts.assign(typ="P"), calls.assign(typ="C")], ignore_index=True)
|
||||
atm = allo.iloc[(allo["delta"].abs() - 0.5).abs().argsort()[:4]]
|
||||
atm_iv = atm["mark_iv"].mean() / 100.0
|
||||
# delta-0.28 put (delta negative)
|
||||
p = puts.iloc[(puts["delta"] - (-PUT_DELTA)).abs().argsort()[:1]].iloc[0]
|
||||
K = p["strike"]
|
||||
put_iv = p["mark_iv"] / 100.0
|
||||
# premiums in USD (Deribit option price is in BTC)
|
||||
bid_usd = p["bid"] * S
|
||||
mark_usd = p["mark"] * S
|
||||
ask_usd = p["ask"] * S
|
||||
bs_atm_usd = bs_put(S, K, T, atm_iv) # cio' che il backtest assume (DVOL=ATM, incassa mid)
|
||||
bs_skew_usd = bs_put(S, K, T, put_iv) # BS alla vol REALE della put (isola lo skew)
|
||||
|
||||
print("\n --- MISURA SU QUOTE REALI (snapshot) ---")
|
||||
print(f" underlying S = {S:,.0f} strike(delta~-0.28) K = {K:,.0f} ({(1-K/S)*100:.1f}% OTM) delta {p['delta']:.3f}")
|
||||
print(f" IV ATM (DVOL-equivalente) = {atm_iv*100:.1f}% IV put OTM (mark) = {put_iv*100:.1f}% "
|
||||
f"skew +{(put_iv-atm_iv)*100:.1f} pt")
|
||||
print(f" premio put (USD): BID {bid_usd:,.1f} mark {mark_usd:,.1f} ask {ask_usd:,.1f}")
|
||||
print(f" spread bid/mark = {(p['bid']/p['mark']) if p['mark']>0 else float('nan'):.3f} "
|
||||
f"(ask-bid)/mark = {((p['ask']-p['bid'])/p['mark']) if p['mark']>0 else float('nan'):.3f}")
|
||||
print(f" modellato dal backtest BS@IV-ATM = {bs_atm_usd:,.1f} USD (BS@IV-put-reale = {bs_skew_usd:,.1f})")
|
||||
f_bid = bid_usd / bs_atm_usd if bs_atm_usd > 0 else float("nan")
|
||||
f_mark = mark_usd / bs_atm_usd if bs_atm_usd > 0 else float("nan")
|
||||
print(f" HAIRCUT premio: reale(BID)/modello = {f_bid:.3f} | mark/modello = {f_mark:.3f}")
|
||||
print(f" -> lo skew ALZA il premio lordo (+{(bs_skew_usd/bs_atm_usd-1)*100:.0f}% vs ATM), ma il "
|
||||
f"BID/ask lo riporta a {f_bid*100:.0f}% del modello.")
|
||||
return f_bid
|
||||
|
||||
|
||||
def csp_sleeve_haircut(f):
|
||||
"""Ri-esegue lo sleeve CSP settimanale (dati+modulo esterni) con premio * f."""
|
||||
import options_deribit as od
|
||||
px = pd.read_csv(EXT / "data/BTCUSDT.csv", parse_dates=["date"]).set_index("date")["close"]
|
||||
dvol = pd.read_csv(EXT / "data/DVOL_BTC.csv", parse_dates=["date"]).set_index("date")["close"]
|
||||
iv = od.build_iv(px, "BTC", dvol)
|
||||
d0 = dvol.index[0]
|
||||
px, iv = px[px.index >= d0], iv[iv.index >= d0]
|
||||
|
||||
def sim(prem_mult, m=0.63):
|
||||
idx = px.index
|
||||
locs = list(range(0, len(idx) - CYCLE_DAYS, CYCLE_DAYS))
|
||||
T = CYCLE_DAYS / ANN
|
||||
rows = []
|
||||
for i in locs:
|
||||
S0, S1, sig = px.iloc[i], px.iloc[i + CYCLE_DAYS], iv.iloc[i]
|
||||
if not (np.isfinite(S0) and np.isfinite(S1) and np.isfinite(sig)):
|
||||
continue
|
||||
Kp = od.strike_for_delta(S0, T, sig, PUT_DELTA, call=False)
|
||||
pp = od.bs_price(S0, Kp, T, sig, call=False) * prem_mult # <-- haircut sul premio
|
||||
fee = od.option_fee(S0, pp) + (od.SETTLE_FEE * S0 if S1 < Kp else 0)
|
||||
pnl = pp - max(Kp - S1, 0.0) - fee
|
||||
rows.append((idx[i + CYCLE_DAYS], m * pnl / S0))
|
||||
s = pd.Series({d: r for d, r in rows}).sort_index()
|
||||
return s
|
||||
|
||||
def met(s, name):
|
||||
eq = (1 + s).cumprod()
|
||||
cpy = ANN / CYCLE_DAYS
|
||||
yrs = len(s) / cpy
|
||||
cagr = eq.iloc[-1] ** (1 / yrs) - 1 if eq.iloc[-1] > 0 else -1
|
||||
sh = s.mean() / s.std() * np.sqrt(cpy)
|
||||
dd = (eq / eq.cummax() - 1).min()
|
||||
print(f" {name:<34s} CAGR {cagr*100:>+6.1f}% Sharpe {sh:>5.2f} maxDD {dd*100:>6.1f}% win {(s>0).mean()*100:>3.0f}%")
|
||||
return sh
|
||||
|
||||
print("\n --- RI-ESECUZIONE SLEEVE CSP con HAIRCUT REALE (m=0.63, hold-to-expiry) ---")
|
||||
print(f" finestra {px.index[0].date()} -> {px.index[-1].date()} (DVOL reale)")
|
||||
sh_model = met(sim(1.00), "modello (premio pieno, BS@DVOL)")
|
||||
sh_real = met(sim(f), f"reale stimato (premio x{f:.2f} = BID)")
|
||||
# sensitivity
|
||||
for ff in (0.85, 0.70, 0.55):
|
||||
met(sim(ff), f"sensitivity premio x{ff:.2f}")
|
||||
print(f"\n => con haircut reale f={f:.2f}: Sharpe sleeve {sh_model:.2f} -> {sh_real:.2f}")
|
||||
return sh_model, sh_real
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 92)
|
||||
print("# VERIFICA SLEEVE OPZIONI su QUOTE REALI DERIBIT — quanto Sharpe sopravvive")
|
||||
print("=" * 92)
|
||||
try:
|
||||
expiry, dte, puts, calls = fetch_real_chain()
|
||||
f = measure_haircut(dte, puts, calls)
|
||||
except Exception as e:
|
||||
print(f" [rete] impossibile scaricare la catena reale ({type(e).__name__}: {e})")
|
||||
print(" uso haircut di letteratura f=0.70 (spread+skew tipici su put OTM settimanali)")
|
||||
f = 0.70
|
||||
f = float(np.clip(f, 0.3, 1.2))
|
||||
csp_sleeve_haircut(f)
|
||||
print("\n CAVEAT: snapshot singolo; spread peggiora nello stress; ripetere nel tempo + testnet.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user