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>
333 lines
14 KiB
Python
333 lines
14 KiB
Python
"""GATE PORT06 — griglia ETH (vincitore gioco "Grid Traders", sessione 3).
|
|
|
|
Il gioco (scripts/games/grid_*, regola STRATEGIA_GRIGLIA.md) ha promosso una
|
|
griglia geometrica asimmetrica su ETH: range profondo sotto, corto sopra,
|
|
4 livelli (passo ~5%), SL catastrofale. Ma il motore del gioco somma i PnL
|
|
REALIZZATI per trade e NON misura l'equity mark-to-market: l'inventario a
|
|
tranche dentro un drawdown e' rischio vero che il fitness non vede.
|
|
|
|
Questo gate risponde alla domanda "si puo' inserire?" con il metodo del progetto:
|
|
[1] STANDALONE mark-to-market (engine MTM dedicato, fill onesti):
|
|
equity per barra = capitale + inventario valutato al close; fee 0.10% RT
|
|
(taker; i fill ai livelli sarebbero LIMIT->maker, quindi conservativo);
|
|
SL gap-aware (gap sotto lo stop -> fill all'open, non al livello);
|
|
flat-skip (nessun fill sulle candele O=H=L=C di ETH 15m, live-realizable).
|
|
Metriche FULL/OOS con le stesse funzioni degli altri gate + stress fee 2x.
|
|
[2] CORRELAZIONE coi 19 sleeve PORT06 (il sospetto: e' la stessa reversione
|
|
ETH delle fade MR, incassata con inventory risk).
|
|
[3] ROBUSTEZZA: plateau range_down x range_up attorno al vincitore.
|
|
[4] GATE PORT06: baseline vs +GRID (full e half size). Promosso solo se
|
|
OOS Sharpe non peggiora E DD non sale (criterio standard).
|
|
|
|
uv run python scripts/analysis/grid_game_gate.py
|
|
"""
|
|
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))
|
|
|
|
from scripts.analysis.combine_portfolio import port_returns, metrics, SPLIT, OOS_DATE, IDX
|
|
from scripts.portfolios._defs import PORTFOLIOS
|
|
from src.portfolio import weighting as W
|
|
from src.data.downloader import load_data
|
|
|
|
POS, LEV = 0.15, 3.0 # config canonica sleeve (== build_everything)
|
|
FEE_SIDE = 0.0005 # 0.05%/lato = 0.10% RT
|
|
|
|
# top-3 del torneo (data/games/grid_result.json)
|
|
WINNER_15M = dict(tf="15m", range_down=0.171, range_up=0.046, levels=4,
|
|
sl_buf=0.124, tp_buf=0.048, max_bars=2143)
|
|
TOP2_30M = dict(tf="30m", range_down=0.158, range_up=0.048, levels=4,
|
|
sl_buf=0.081, tp_buf=0.044, max_bars=613)
|
|
TOP3_1H = dict(tf="1h", range_down=0.134, range_up=0.053, levels=4,
|
|
sl_buf=0.150, tp_buf=0.063, max_bars=562)
|
|
|
|
_RESAMPLE = {"30m": ("15m", "30min")}
|
|
|
|
|
|
def _load(asset, tf):
|
|
if tf in _RESAMPLE:
|
|
base, rule = _RESAMPLE[tf]
|
|
d = load_data(asset, base).copy()
|
|
d["dt"] = pd.to_datetime(d["datetime"])
|
|
g = d.set_index("dt").resample(rule).agg(
|
|
{"open": "first", "high": "max", "low": "min", "close": "last",
|
|
"volume": "sum"}).dropna(subset=["open", "close"]).reset_index()
|
|
g["datetime"] = g["dt"]
|
|
return g
|
|
d = load_data(asset, tf).copy()
|
|
d["dt"] = pd.to_datetime(d["datetime"])
|
|
return d
|
|
|
|
|
|
def grid_mtm(asset="ETH", *, tf, range_down, range_up, levels, sl_buf, tp_buf,
|
|
max_bars, pos=POS, lev=LEV, fee_side=FEE_SIDE, flat_skip=True,
|
|
close_only=False, deploy_mask=None, df=None):
|
|
"""Griglia STRATEGIA_GRIGLIA.md con contabilita' mark-to-market.
|
|
|
|
Ritorna (equity daily Series base 1.0, stats dict). Causale: deploy sul
|
|
close, fill dalle barre successive lungo il percorso O->L->H->C / O->H->L->C.
|
|
`deploy_mask` (opzionale, np.bool array lungo come la serie, causale): se
|
|
fornito, una NUOVA griglia si deploya SOLO dove mask[j]=True (regime-gate);
|
|
None = comportamento storico (deploy sempre). Una griglia gia' attiva non
|
|
viene interrotta dal mask (gestisce il suo episodio fino a SL/TP/timeout).
|
|
`df` (opzionale): OHLCV gia' caricato (per il feed LIVE del GridWorker); None
|
|
= carica da _load(asset, tf) (comportamento storico, parita' col gate).
|
|
"""
|
|
df = _load(asset, tf) if df is None else df
|
|
op = df["open"].to_numpy(float)
|
|
hi = df["high"].to_numpy(float)
|
|
lo = df["low"].to_numpy(float)
|
|
cl = df["close"].to_numpy(float)
|
|
dt = (pd.to_datetime(df["datetime"]) if "datetime" in df.columns
|
|
else pd.to_datetime(df["timestamp"], unit="ms", utc=True)).to_numpy()
|
|
n = len(cl)
|
|
ratio = ((1 + range_up) / (1 - range_down)) ** (1.0 / levels)
|
|
if ratio - 1 <= 1.5 * 2 * fee_side: # vincolo break-even §4
|
|
raise ValueError("break-even violato")
|
|
flat = (op == hi) & (op == lo) & (op == cl)
|
|
|
|
capital = 1.0
|
|
eq = np.empty(n)
|
|
eq[:20] = 1.0
|
|
# stato episodio
|
|
active = False
|
|
lv = []; filled = []; tn = 0.0; sl = tp = 0.0; ep_end = 0
|
|
n_open = 0
|
|
trades = wins = stops = 0
|
|
deploy_i = -1
|
|
|
|
def mtm(px):
|
|
u = 0.0
|
|
for k in range(levels):
|
|
if filled[k]:
|
|
u += tn * (px / lv[k] - 1.0)
|
|
return capital + u
|
|
|
|
i = 20
|
|
for j in range(20, n):
|
|
if not active:
|
|
if deploy_mask is not None and not deploy_mask[j]:
|
|
eq[j] = capital # regime-gate: niente deploy, resta in cash
|
|
continue
|
|
# deploy sul close di j (fill da j+1)
|
|
px = cl[j]
|
|
rl_ = px * (1 - range_down)
|
|
lv = [rl_ * ratio ** k for k in range(levels + 1)]
|
|
sl = rl_ * (1 - sl_buf)
|
|
tp = lv[levels] * (1 + tp_buf)
|
|
filled = [False] * levels
|
|
n_open = 0
|
|
tn = capital * pos * lev / levels # notional per tranche
|
|
ep_end = j + max_bars
|
|
active = True
|
|
deploy_i = j
|
|
eq[j] = capital
|
|
continue
|
|
if flat_skip and flat[j]:
|
|
eq[j] = mtm(cl[j])
|
|
continue
|
|
cur = cl[j - 1]
|
|
if close_only:
|
|
# fill solo su attraversamento del CLOSE (le wick non fillano):
|
|
# stress anti-spike-print del feed testnet
|
|
pts = (cl[j],)
|
|
else:
|
|
pts = (op[j], lo[j], hi[j], cl[j]) if cl[j] >= op[j] \
|
|
else (op[j], hi[j], lo[j], cl[j])
|
|
died = False
|
|
for pi, q in enumerate(pts):
|
|
q = float(q)
|
|
if q == cur:
|
|
continue
|
|
if q < cur:
|
|
from bisect import bisect_left
|
|
k1 = bisect_left(lv, q)
|
|
k2 = bisect_left(lv, cur) - 1
|
|
for k in range(min(k2, levels - 1), max(k1, 0) - 1, -1):
|
|
if not filled[k]:
|
|
filled[k] = True
|
|
n_open += 1
|
|
capital -= fee_side * tn # fee ingresso
|
|
if q <= sl:
|
|
# STOP: gap all'open -> fill all'open, altrimenti al livello sl
|
|
fill = q if (pi == 0 and q <= sl) else sl
|
|
for k in range(levels):
|
|
if filled[k]:
|
|
r = fill / lv[k]
|
|
capital += tn * (r - 1.0) - fee_side * tn * r
|
|
filled[k] = False
|
|
trades += 1
|
|
stops += 1
|
|
n_open = 0
|
|
died = True
|
|
cur = q
|
|
break
|
|
else:
|
|
from bisect import bisect_right
|
|
m1 = bisect_right(lv, cur)
|
|
m2 = bisect_right(lv, q) - 1
|
|
for m in range(max(m1, 1), min(m2, levels) + 1):
|
|
k = m - 1
|
|
if filled[k]:
|
|
r = lv[m] / lv[k]
|
|
capital += tn * (r - 1.0) - fee_side * tn * r
|
|
filled[k] = False
|
|
n_open -= 1
|
|
trades += 1
|
|
wins += 1
|
|
if q >= tp:
|
|
for k in range(levels):
|
|
if filled[k]:
|
|
r = tp / lv[k]
|
|
capital += tn * (r - 1.0) - fee_side * tn * r
|
|
filled[k] = False
|
|
trades += 1
|
|
wins += 1
|
|
n_open = 0
|
|
died = True
|
|
cur = q
|
|
break
|
|
cur = q
|
|
if not died and j >= ep_end:
|
|
# timeout: liquida al close
|
|
for k in range(levels):
|
|
if filled[k]:
|
|
r = cl[j] / lv[k]
|
|
capital += tn * (r - 1.0) - fee_side * tn * r
|
|
filled[k] = False
|
|
trades += 1
|
|
wins += r > 1.0 + 2 * fee_side
|
|
n_open = 0
|
|
died = True
|
|
if died:
|
|
active = False
|
|
eq[j] = capital
|
|
else:
|
|
eq[j] = mtm(cl[j])
|
|
|
|
s = pd.Series(eq, index=pd.DatetimeIndex(dt)).resample("1D").last().dropna()
|
|
s = s / s.iloc[0]
|
|
return s, dict(trades=trades, win=100.0 * wins / max(1, trades), stops=stops)
|
|
|
|
|
|
def std(eqd):
|
|
"""Metriche FULL/OOS con le funzioni standard del progetto."""
|
|
e = eqd.reindex(IDX).ffill().bfill()
|
|
dr = e.pct_change().fillna(0.0)
|
|
return metrics(dr), metrics(dr, lo=SPLIT)
|
|
|
|
|
|
def main():
|
|
p = PORTFOLIOS["PORT06"]
|
|
print("=" * 100)
|
|
print(" GATE PORT06 — griglia ETH (vincitore gioco Grid Traders) | "
|
|
f"pos={POS} lev={LEV} | OOS da {OOS_DATE}")
|
|
print("=" * 100)
|
|
|
|
# [1] STANDALONE mark-to-market
|
|
print("\n[1] STANDALONE mark-to-market (fee 0.10% RT, flat-skip, SL gap-aware):")
|
|
print(f" {'cfg':<22s}{'trd':>7s}{'win%':>6s}{'stops':>6s}{'FULL%':>8s}{'CAGR%':>7s}"
|
|
f"{'DD%':>7s}{'Shrp':>6s} | {'OOS%':>7s}{'oDD%':>6s}{'oShrp':>6s}")
|
|
eqs = {}
|
|
for tag, cfg in [("WINNER 15m", WINNER_15M), ("top2 30m", TOP2_30M),
|
|
("top3 1h", TOP3_1H)]:
|
|
eqd, st = grid_mtm("ETH", **cfg)
|
|
f, o = std(eqd)
|
|
eqs[tag] = eqd
|
|
print(f" {tag:<22s}{st['trades']:>7d}{st['win']:>6.1f}{st['stops']:>6d}"
|
|
f"{f['ret']:>+8.0f}{f['cagr']:>7.1f}{f['dd']:>7.2f}{f['sharpe']:>6.2f}"
|
|
f" | {o['ret']:>+7.0f}{o['dd']:>6.2f}{o['sharpe']:>6.2f}")
|
|
# stress: fee 2x e no flat-skip sul winner
|
|
eq2, st2 = grid_mtm("ETH", **WINNER_15M, fee_side=0.001)
|
|
f2, o2 = std(eq2)
|
|
eqnf, _ = grid_mtm("ETH", **WINNER_15M, flat_skip=False)
|
|
fnf, onf = std(eqnf)
|
|
print(f" {'winner fee 2x':<22s}{st2['trades']:>7d}{st2['win']:>6.1f}{'':>6s}"
|
|
f"{f2['ret']:>+8.0f}{f2['cagr']:>7.1f}{f2['dd']:>7.2f}{f2['sharpe']:>6.2f}"
|
|
f" | {o2['ret']:>+7.0f}{o2['dd']:>6.2f}{o2['sharpe']:>6.2f}")
|
|
print(f" {'winner no-flat-skip':<22s}{'':>7s}{'':>6s}{'':>6s}"
|
|
f"{fnf['ret']:>+8.0f}{fnf['cagr']:>7.1f}{fnf['dd']:>7.2f}{fnf['sharpe']:>6.2f}"
|
|
f" | {onf['ret']:>+7.0f}{onf['dd']:>6.2f}{onf['sharpe']:>6.2f}")
|
|
|
|
grid_eq = eqs["WINNER 15m"]
|
|
|
|
# [2] CORRELAZIONI coi sleeve PORT06
|
|
from src.portfolio.sleeves import all_sleeve_equities
|
|
eq_base = dict(all_sleeve_equities())
|
|
gr = grid_eq.reindex(IDX).ffill().bfill().pct_change().fillna(0.0)
|
|
print("\n[2] CORRELAZIONE rendimenti giornalieri GRID_ETH15M vs sleeve PORT06 (top 8):")
|
|
cors = {}
|
|
for sid, e in eq_base.items():
|
|
r = e.reindex(IDX).ffill().bfill().pct_change().fillna(0.0)
|
|
cors[sid] = gr.corr(r)
|
|
for sid, cv in sorted(cors.items(), key=lambda kv: -abs(kv[1]))[:8]:
|
|
print(f" {sid:<16s} {cv:+.3f}")
|
|
|
|
# [3] ROBUSTEZZA: plateau range_down x range_up (15m, levels=4)
|
|
print("\n[3] ROBUSTEZZA 15m (Sharpe FULL mark-to-market, levels=4, "
|
|
"sl_buf=0.12, tp_buf=0.05, max_bars=2000):")
|
|
rds = [0.13, 0.15, 0.17, 0.19]
|
|
rus = [0.04, 0.05, 0.06, 0.08]
|
|
print(" rd\\ru " + "".join(f"{ru:>7.2f}" for ru in rus))
|
|
cells = tot = 0
|
|
for rd in rds:
|
|
row = f" {rd:>5.2f} "
|
|
for ru in rus:
|
|
eqd, _ = grid_mtm("ETH", tf="15m", range_down=rd, range_up=ru,
|
|
levels=4, sl_buf=0.12, tp_buf=0.05, max_bars=2000)
|
|
f, o = std(eqd)
|
|
tot += 1
|
|
cells += (f["sharpe"] > 1) and (o["sharpe"] > 1)
|
|
row += f"{f['sharpe']:>7.2f}"
|
|
print(row)
|
|
print(f" -> {cells}/{tot} celle con Sharpe>1 sia FULL che OOS")
|
|
|
|
# [4] GATE PORT06
|
|
print("\n[4] GATE PORT06 — baseline vs +GRID_ETH15M (full/half size):")
|
|
def port_m(extra=None):
|
|
members = dict(eq_base)
|
|
ids = list(p.sleeve_ids)
|
|
if extra is not None:
|
|
members["GRID_ETH15M"] = extra
|
|
ids = ids + ["GRID_ETH15M"]
|
|
dr = pd.DataFrame({i: members[i].reindex(IDX).ffill().bfill()
|
|
.pct_change().fillna(0.0) for i in ids})
|
|
w = W.weight_vector(p.weighting, ids, dr, weights=p.weights,
|
|
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
|
|
drp = port_returns({i: members[i].reindex(IDX).ffill().bfill()
|
|
for i in ids}, w)
|
|
return metrics(drp), metrics(drp, lo=SPLIT)
|
|
|
|
half = (1 + 0.5 * gr).cumprod()
|
|
res = {"baseline": port_m(None),
|
|
"+GRID full": port_m(grid_eq),
|
|
"+GRID half": port_m(half)}
|
|
print(f" {'variante':<12s} | {'FULL Sh':>8s}{'FULL DD%':>9s}{'CAGR%':>7s}"
|
|
f" | {'OOS Sh':>7s}{'OOS DD%':>8s}")
|
|
for tag, (f, o) in res.items():
|
|
print(f" {tag:<12s} | {f['sharpe']:>8.2f}{f['dd']:>9.2f}{f['cagr']:>7.1f}"
|
|
f" | {o['sharpe']:>7.2f}{o['dd']:>8.2f}")
|
|
|
|
fb, ob = res["baseline"]
|
|
print("\n" + "=" * 100)
|
|
print(" VERDETTO (criterio standard: OOS Sharpe non peggiora E DD non sale)")
|
|
print("=" * 100)
|
|
for tag in ("+GRID full", "+GRID half"):
|
|
f, o = res[tag]
|
|
ok = (o["sharpe"] >= ob["sharpe"] - 0.02 and o["dd"] <= ob["dd"] + 1e-9
|
|
and f["sharpe"] >= fb["sharpe"] - 0.02)
|
|
print(f" {tag:<12s}: OOS Sh {ob['sharpe']:.2f}->{o['sharpe']:.2f} "
|
|
f"DD {ob['dd']:.2f}->{o['dd']:.2f} | FULL Sh {fb['sharpe']:.2f}->{f['sharpe']:.2f} "
|
|
f"DD {fb['dd']:.2f}->{f['dd']:.2f} => {'PROMOSSO' if ok else 'bocciato'}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|