feat(games): sessioni 2-3 Blind Traders (opzioni/session/grid) + gate PORT06 e tooling reset
- Gioco GRID TRADERS (sessione 3, regola STRATEGIA_GRIGLIA.md): grid_engine (backtest causale fee-aware della griglia geometrica), grid_brief (digest anonimo per dimensionare la griglia), grid_arena (torneo 100 agenti); diario docs/diary/2026-06-10-grid-traders-game3.md - Gioco OPZIONI: options_engine (BS + skew fittato + DVOL storica), options_arena, opt_calibrate (superficie premi REALE da cerbero-bite) - Gioco SESSION: session_engine/session_arena (pattern orari intraday) - arena: vincolo GAME_NO_LIVE=1 (vieta pairs e fade zscore/breakout/momentum gia' live, coercizione a trend/ma_cross) + normalize del candidato PRIMA della valutazione nel hill-climb - Gate: grid_game_gate (griglia ETH vincitrice vs PORT06, mark-to-market), pairs30m_gate (ETH/BTC 30m ridondante col 15m gia' deployato?) - reset_flatten: flatten one-shot del conto testnet per il reset portafoglio - .gitignore: data/portfolio_paper_stats/ (stato runtime sleeve paper-only) Co-Authored-By: Claude Fable 5 <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