chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
Motore del gioco-OPZIONI: prezza e backtesta strutture in opzioni proposte dagli
|
||||
agenti ciechi, sui prezzi REALI ETH/BTC, con Black-Scholes + skew fittato + DVOL storica.
|
||||
|
||||
NON usa la chain reale (solo 6 settimane, un regime): prezza sinteticamente con la
|
||||
vol implicita storica (DVOL Deribit, dal 2021-03) e la curva di skew fittata sulle IV
|
||||
reali della ricerca credit-spread (iv/atm = 1 - 0.664*k + 3.494*k^2, k=ln(K/S)). Costi:
|
||||
haircut bid/ask sulle opzioni (il fill reale e' peggiore del mid). Roll giornaliero,
|
||||
hold-to-expiry (terminale model-free dai prezzi reali). PnL per-trade ADDITIVO.
|
||||
|
||||
Caveat onesto (dalla ricerca del progetto): il premium-selling a skew negativo vince nei
|
||||
campioni calmi e restituisce tutto nei crash -> il gioco lo mostrera'.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
import json as _json
|
||||
|
||||
from src.data.downloader import load_data
|
||||
from scripts.analysis.option_overlay_lab import bs_put, bs_call, dvol_for
|
||||
|
||||
# skew fittato (fallback se manca la calibrazione reale): iv/atm in funzione di k=ln(K/S).
|
||||
SKEW_A, SKEW_B = -0.664, 3.494
|
||||
MIN_TRADES_PER_MONTH = 10.0
|
||||
TRADING_DAYS_MONTH = 30.0
|
||||
|
||||
# --- pricing REALE: superficie premi/spread da cerbero-bite (scripts/games/opt_calibrate.py) ---
|
||||
_CALIB_DIR = PROJECT_ROOT / "data" / "games"
|
||||
_CALIB = {}
|
||||
|
||||
|
||||
def _load_calib(asset):
|
||||
if asset not in _CALIB:
|
||||
f = _CALIB_DIR / f"opt_calib_{asset.lower()}.json"
|
||||
_CALIB[asset] = _json.loads(f.read_text()) if f.exists() else None
|
||||
return _CALIB[asset]
|
||||
|
||||
|
||||
def _surf_lookup(cal, typ, otm_signed, dte):
|
||||
"""Premio% e spread reali per (otm firmato, dte): punto di griglia piu' vicino.
|
||||
Ritorna (prem_pct, spread, sellable) o None se fuori dalla superficie liquida."""
|
||||
s = cal["surface"][typ]
|
||||
og = cal["otm_grid"]; tg = cal["ten_grid"]
|
||||
o = min(og, key=lambda x: abs(x - otm_signed))
|
||||
t = min(tg, key=lambda x: abs(x - dte))
|
||||
if abs(o - otm_signed) > 0.06: # troppo lontano dagli strike reali -> illiquido
|
||||
return None
|
||||
v = s.get(f"{o:+.2f}|{t}")
|
||||
if not v or v["sellable"] < 0.5:
|
||||
return None
|
||||
return v["prem"], v["spread"], v["sellable"]
|
||||
|
||||
|
||||
def iv_skew(k: float, atm: float) -> float:
|
||||
"""IV per moneyness k=ln(K/S) dato l'ATM vol. Clamp a [0.3x, 3x] atm."""
|
||||
mult = 1.0 + SKEW_A * k + SKEW_B * k * k
|
||||
mult = min(max(mult, 0.3), 3.0)
|
||||
return atm * mult
|
||||
|
||||
|
||||
def load_opt(asset: str = "ETH"):
|
||||
"""Prezzi GIORNALIERI (resample 1h->1d) + DVOL allineata. asset reale nascosto."""
|
||||
df = load_data(asset, "1h").copy()
|
||||
df["dt"] = pd.to_datetime(df["datetime"])
|
||||
g = df.set_index("dt").resample("1D").agg(
|
||||
{"timestamp": "first", "open": "first", "high": "max", "low": "min",
|
||||
"close": "last"}).dropna(subset=["close"]).reset_index(drop=True)
|
||||
g["timestamp"] = g["timestamp"].astype("int64")
|
||||
dv = dvol_for(g, asset)
|
||||
cal = _load_calib(asset)
|
||||
dvol_chain = (cal["dvol_chain"] / 100.0) if cal else float(np.nanmedian(dv))
|
||||
return {"close": g["close"].to_numpy(float), "high": g["high"].to_numpy(float),
|
||||
"low": g["low"].to_numpy(float), "dvol": dv, "asset": asset,
|
||||
"dvol_chain": dvol_chain, "real": cal is not None,
|
||||
"dt": pd.to_datetime(g["timestamp"], unit="ms", utc=True).to_numpy(),
|
||||
"n": len(g)}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Pricing di una struttura: ritorna (premio_netto_incassato, funzione_payoff(ST))
|
||||
# premio>0 = struttura a CREDITO (vendi); payoff e' il valore terminale (>=0 per long opt).
|
||||
# Convenzione PnL trade: net = (premio_incassato - payoff_terminale)/S0 - costi (per credito)
|
||||
# Tutto normalizzato sul SPOT (frazione), cosi' e' confrontabile fra asset/epoche.
|
||||
# --------------------------------------------------------------------------
|
||||
STRUCTURES = ["short_put", "short_call", "short_strangle", "put_spread",
|
||||
"call_spread", "iron_condor", "long_put", "long_call", "long_straddle"]
|
||||
|
||||
|
||||
def _legs_for(struct, S, otm, width):
|
||||
kp = S * (1 - otm); kc = S * (1 + otm)
|
||||
kp2 = S * (1 - otm - width); kc2 = S * (1 + otm + width)
|
||||
return {
|
||||
"short_put": [("P", kp, -1)], "short_call": [("C", kc, -1)],
|
||||
"short_strangle": [("P", kp, -1), ("C", kc, -1)],
|
||||
"put_spread": [("P", kp, -1), ("P", kp2, +1)],
|
||||
"call_spread": [("C", kc, -1), ("C", kc2, +1)],
|
||||
"iron_condor": [("P", kp, -1), ("P", kp2, +1), ("C", kc, -1), ("C", kc2, +1)],
|
||||
"long_put": [("P", kp, +1)], "long_call": [("C", kc, +1)],
|
||||
"long_straddle": [("P", S, +1), ("C", S, +1)],
|
||||
}[struct]
|
||||
|
||||
|
||||
def _price_real(struct, S, dte, scale, otm, width, cal):
|
||||
"""Pricing REALE dalla superficie cerbero-bite. Ritorna (entry_cf_frac, legs, ok).
|
||||
entry_cf_frac = cassa d'ingresso in frazione di spot (>0 = incassi); side-aware bid/ask;
|
||||
ok=False se una gamba e' fuori dagli strike liquidi reali."""
|
||||
legs = _legs_for(struct, S, otm, width)
|
||||
entry = 0.0
|
||||
for typ, K, sgn in legs:
|
||||
q = _surf_lookup(cal, typ, K / S - 1.0, dte)
|
||||
if q is None:
|
||||
return 0.0, legs, False
|
||||
prem, spread, _ = q
|
||||
pf = prem / 100.0 * scale # premio frazione di spot, scalato a DVOL del giorno
|
||||
if sgn < 0: # short: incassi il BID (~ ask*(1-spread))
|
||||
entry += pf * (1 - spread)
|
||||
else: # long: paghi l'ASK
|
||||
entry -= pf
|
||||
return entry, legs, True
|
||||
|
||||
|
||||
def _price(struct, S, T, atm, otm, width):
|
||||
"""Fallback SINTETICO (BS+skew). Usato solo se manca la calibrazione reale."""
|
||||
legs = _legs_for(struct, S, otm, width)
|
||||
prem = gross = 0.0
|
||||
for typ, K, sgn in legs:
|
||||
px = bs_put(S, K, T, iv_skew(np.log(K / S), atm)) if typ == "P" \
|
||||
else bs_call(S, K, T, iv_skew(np.log(K / S), atm))
|
||||
prem += -sgn * px / S
|
||||
gross += abs(px) / S
|
||||
return prem - 0.06 * gross, legs, True
|
||||
|
||||
|
||||
def _payoff(legs, ST):
|
||||
v = 0.0
|
||||
for typ, K, sgn in legs:
|
||||
intr = max(K - ST, 0.0) if typ == "P" else max(ST - K, 0.0)
|
||||
v += sgn * intr # valore terminale delle opzioni che POSSIEDI/devi
|
||||
return v # per le short questo e' cio' che PAGHI (sgn<0 -> negativo = debito)
|
||||
|
||||
|
||||
def evaluate(data, spec, sl=None):
|
||||
"""Backtest della struttura: roll giornaliero, hold dte giorni, PnL additivo.
|
||||
spec = {structure, otm, width, dte}. Ritorna metriche con scoring PNL+%win, >=10 tr/mese.
|
||||
"""
|
||||
c, dv = data["close"], data["dvol"]
|
||||
n = data["n"]
|
||||
s, e = (sl if sl else (0, n))
|
||||
struct = spec["structure"]
|
||||
otm = float(spec["otm"]); width = float(spec.get("width", 0.05))
|
||||
dte = int(spec["dte"])
|
||||
T = dte / 365.0
|
||||
cal = _load_calib(data["asset"]); dvol_chain = data["dvol_chain"]
|
||||
rets = []
|
||||
i = s
|
||||
while i < e - dte:
|
||||
S0 = c[i]; atm = dv[i]
|
||||
if S0 <= 0 or atm <= 0:
|
||||
i += 1; continue
|
||||
if cal is not None: # PRICING REALE (cerbero-bite), scalato a DVOL del giorno
|
||||
scale = min(max(atm / dvol_chain, 0.3), 4.0)
|
||||
entry, legs, ok = _price_real(struct, S0, dte, scale, otm, width, cal)
|
||||
if not ok: # strike fuori dalla superficie liquida reale -> non eseguibile
|
||||
i += 1; continue
|
||||
net = entry + _payoff(legs, c[i + dte]) / S0
|
||||
else: # fallback sintetico
|
||||
prem, legs, _ = _price(struct, S0, T, atm, otm, width)
|
||||
net = prem + _payoff(legs, c[i + dte]) / S0
|
||||
rets.append(net)
|
||||
i += 1 # roll giornaliero (posizioni sovrapposte)
|
||||
rets = np.array(rets)
|
||||
nbars = e - s
|
||||
months = nbars / TRADING_DAYS_MONTH
|
||||
n_tr = len(rets)
|
||||
tpm = n_tr / months if months > 0 else 0.0
|
||||
if n_tr == 0:
|
||||
return dict(n_trades=0, win_rate=0.0, pnl_pct=0.0, tpm=0.0, sharpe=0.0,
|
||||
avg_ret=0.0, qualified=False, fitness=-1e6)
|
||||
win = float(np.mean(rets > 0))
|
||||
pnl = float(np.sum(rets)) * 100
|
||||
avg = float(np.mean(rets)) * 100
|
||||
sharpe = float(np.mean(rets) / (np.std(rets) + 1e-12) * np.sqrt(tpm * 12)) \
|
||||
if np.std(rets) > 0 else 0.0
|
||||
qualified = tpm >= MIN_TRADES_PER_MONTH
|
||||
fitness = pnl + 50.0 * win
|
||||
if not qualified:
|
||||
fitness = -1e6 + pnl
|
||||
return dict(n_trades=n_tr, win_rate=win, pnl_pct=pnl, tpm=tpm, sharpe=sharpe,
|
||||
avg_ret=avg, qualified=qualified, fitness=fitness)
|
||||
|
||||
|
||||
def splits3(data, train_frac=0.60, valid_frac=0.20):
|
||||
n = data["n"]
|
||||
c1 = int(n * train_frac); c2 = int(n * (train_frac + valid_frac))
|
||||
return (0, c1), (c1, c2), (c2, n)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
d = load_opt("ETH")
|
||||
print("loaded", d["n"], "giorni", str(d["dt"][0])[:10], "->", str(d["dt"][-1])[:10],
|
||||
"| dvol", round(float(np.nanmean(d["dvol"])), 2))
|
||||
tr, va, te = splits3(d)
|
||||
for st in ["short_put", "short_strangle", "iron_condor", "long_straddle", "put_spread"]:
|
||||
sp = {"structure": st, "otm": 0.05, "width": 0.05, "dte": 14}
|
||||
f = evaluate(d, sp, None); o = evaluate(d, sp, te)
|
||||
print(f"{st:14} FULL pnl{f['pnl_pct']:8.0f} win{f['win_rate']*100:4.0f} tpm{f['tpm']:5.0f} "
|
||||
f"Sh{f['sharpe']:6.1f} | OOS pnl{o['pnl_pct']:8.0f} win{o['win_rate']*100:4.0f} Sh{o['sharpe']:6.1f}")
|
||||
Reference in New Issue
Block a user