Files
Adriano Dal Pastro 14522262e6 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>
2026-06-19 15:20:59 +00:00

216 lines
9.2 KiB
Python

"""
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}")