14522262e6
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>
216 lines
9.2 KiB
Python
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}")
|