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,332 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user