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:
Adriano Dal Pastro
2026-06-19 15:16:03 +00:00
parent 8401a280b9
commit 14522262e6
383 changed files with 1971 additions and 779 deletions
+252
View File
@@ -0,0 +1,252 @@
"""SH01 EXIT LAB — harness onesto e CONDIVISO per la ricerca di STOP-LOSS su SH01.
SH01 (shape-ML, logit walk-forward W24 H12 th0.58) NON ha TP/SL: esce SOLO a
orizzonte H=12 barre. Live (2026-06-05) si è preso il crash ETH intero: 15.6%
in un trade (long 1727.8 → 1594.35, leva 2x). Domanda di ricerca: esiste uno SL
che taglia le code SENZA distruggere l'edge (che vive nell'asimmetria dei
winner, win-rate ~50%)?
CONTRATTO ANTI-LOOK-AHEAD (vincolante, verificato da agenti avversari):
- i livelli attivi nel bar j (`levels(..., j)`) possono usare SOLO dati <= j-1
(il worker live fissa i livelli al close del bar precedente; il bar j li tocca);
- `after_bar(..., j)` decide sul CLOSE del bar j (eseguibile al poll del tick);
- indicatori causali: usare l'indice j-1 (es. ctx["atr14"][j-1]).
FILL GAP-AWARE (lezione exit-lab 2026-06-04 + crash live 2026-06-05): lo stop
intrabar NON filla "al livello" se il bar apre già oltre → fill = worse(level,
open[j]). Senza questo il backtest ha un bias PRO stop-stretti (54% dei fill
era ottimista). Il crash di oggi (feed flat 2h → gap 1655→1600) è il caso reale.
PROTOCOLLO ANTI-OVERFIT (vincolante, = exit_lab):
- TRAIN = storico fino al 2023-11-01, OOS = dopo. SELEZIONE parametri SOLO
sul train; OOS guardato una volta per il verdetto.
- gate: miglioramento su ENTRAMBI gli asset (BTC e ETH), train E oos, con
plateau sulla griglia (non una cella isolata). Metrica primaria: Sharpe e
DD; il return non deve crollare (>= ~80% del baseline).
- fee 0.10% RT × leva su tutto il notional.
Baseline = exit a orizzonte puro (max_bars=H, nessun TP/SL): parità ESATTA con
`explore_lab.simulate` verificata da `parity_check()`.
uv run python scripts/analysis/sh01_exit_lab.py # build cache + parity check
"""
from __future__ import annotations
import pickle
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))
LEV, POS, FEE_RT = 3.0, 0.15, 0.001
OOS_START_MS = int(pd.Timestamp("2023-11-01", tz="UTC").value // 1e6)
ASSETS = ("BTC", "ETH")
CACHE = PROJECT_ROOT / "data" / "cache" / "sh01_exit_lab.pkl"
# ----------------------------------------------------------------------------- cache
def build_cache() -> dict:
"""Walk-forward SH01 (lento, ~minuti) → entries cache su disco."""
from scripts.analysis.explore_lab import get_df # noqa: E402
from scripts.analysis.shape_ml_research import ml_wf_entries, atr # noqa: E402
from scripts.strategies.SH01_shape_ml import CONFIG # noqa: E402
out = {}
for a in ASSETS:
df = get_df(a, "1h")
ents = ml_wf_entries(df, **CONFIG)
out[a] = {
"entries": [(int(e["i"]), int(e["d"]), int(e["max_bars"])) for e in ents],
"open": df["open"].values.astype(float),
"high": df["high"].values.astype(float),
"low": df["low"].values.astype(float),
"close": df["close"].values.astype(float),
"ts_ms": df["timestamp"].values.astype("int64"),
"atr14": atr(df, 14),
}
print(f" {a}: {len(ents)} entries, {len(df)} bars", flush=True)
CACHE.parent.mkdir(parents=True, exist_ok=True)
with open(CACHE, "wb") as f:
pickle.dump(out, f)
return out
def load_sleeves(refresh: bool = False) -> dict:
"""{asset: ctx}. ctx = {entries, open, high, low, close, ts_ms, atr14}."""
if CACHE.exists() and not refresh:
with open(CACHE, "rb") as f:
return pickle.load(f)
return build_cache()
# ----------------------------------------------------------------------------- policy
class ExitPolicy:
"""Contratto per le policy di stop su SH01 (solo SL/uscite anticipate: il
TP non esiste e l'exit a orizzonte max_bars resta SEMPRE il bound).
open_trade(ctx, i, d) -> state : livelli iniziali, SOLO dati <= i
levels(ctx, i, d, j, st) -> (sl, mode) attivi nel bar j, SOLO dati <= j-1.
sl=None → nessuno stop nel bar. mode: "intrabar" (tocco high/low, fill
gap-aware worse(sl, open[j])) o "close" (stop solo se il CLOSE sfonda
sl, uscita al close — stile EXIT-16).
after_bar(ctx, i, d, j, st) -> bool : uscita discrezionale al CLOSE del bar
j (dati <= j). Per giveback/time-stop/regime.
Lo state è un dict mutabile per-trade (trailing ecc.)."""
name = "base"
def open_trade(self, ctx: dict, i: int, d: int) -> dict:
return {}
def levels(self, ctx: dict, i: int, d: int, j: int, st: dict):
return None, "intrabar"
def after_bar(self, ctx: dict, i: int, d: int, j: int, st: dict) -> bool:
return False
# ----------------------------------------------------------------------------- engine
def simulate(ctx: dict, policy: ExitPolicy, fee_rt: float = FEE_RT,
lev: float = LEV, pos: float = POS,
t_lo: int | None = None, t_hi: int | None = None,
gap_fill: bool = True, lag_close_exit: bool = False) -> dict:
"""Engine intrabar con policy di stop. Entries non sovrapposte (come
explore_lab.simulate). t_lo/t_hi: filtro ms-epoch sull'ENTRY (train/oos).
gap_fill: fill stop intrabar a worse(sl, open[j]) — tenere True.
lag_close_exit: stress — le uscite "al close" fillano al close del bar
successivo (poll in ritardo)."""
o, h, l, c = ctx["open"], ctx["high"], ctx["low"], ctx["close"]
ts = ctx["ts_ms"]
n = len(c)
cap = peak = 1000.0
max_dd = 0.0
fee = fee_rt * lev
trades = wins = stops = 0
bars_in = 0
last_exit = -1
yearly: dict[int, float] = {}
rets: list[float] = []
trade_rows: list[dict] = []
for (i, d, mb) in ctx["entries"]:
if i <= last_exit or i + 1 >= n:
continue
if t_lo is not None and ts[i] < t_lo:
continue
if t_hi is not None and ts[i] >= t_hi:
continue
entry = c[i]
st = policy.open_trade(ctx, i, d)
exit_p, j, reason = c[min(i + mb, n - 1)], min(i + mb, n - 1), "time"
for k in range(1, mb + 1):
j = i + k
if j >= n:
j, exit_p, reason = n - 1, c[n - 1], "eod"
break
sl, mode = policy.levels(ctx, i, d, j, st)
if sl is not None and mode == "intrabar":
hit = (l[j] <= sl) if d == 1 else (h[j] >= sl)
if hit:
if gap_fill:
exit_p = min(sl, o[j]) if d == 1 else max(sl, o[j])
else:
exit_p = sl
reason = "stop"
break
if sl is not None and mode == "close":
brk = (c[j] < sl) if d == 1 else (c[j] > sl)
if brk:
jj = min(j + 1, n - 1) if lag_close_exit else j
exit_p, j, reason = c[jj], jj, "stop"
break
if policy.after_bar(ctx, i, d, j, st):
jj = min(j + 1, n - 1) if lag_close_exit else j
exit_p, j, reason = c[jj], jj, "policy"
break
if k == mb:
exit_p, reason = c[j], "time"
ret = (exit_p - entry) / entry * d * lev - fee
cb = cap
cap = max(cb + cb * pos * ret, 10.0)
peak = max(peak, cap)
max_dd = max(max_dd, (peak - cap) / peak)
trades += 1
wins += ret > 0
stops += reason == "stop"
bars_in += (j - i)
last_exit = j
rets.append(ret * pos)
yr = pd.Timestamp(ts[i], unit="ms", tz="UTC").year
yearly[yr] = yearly.get(yr, 0.0) + ret * 100
trade_rows.append({"i": i, "j": j, "d": d, "ret": ret, "reason": reason})
sharpe = (float(np.mean(rets) / np.std(rets) * np.sqrt(len(rets)))
if len(rets) > 1 and np.std(rets) > 0 else 0.0)
return {
"trades": trades,
"win": wins / trades * 100 if trades else 0.0,
"stop_rate": stops / trades * 100 if trades else 0.0,
"ret": (cap / 1000 - 1) * 100,
"dd": max_dd * 100,
"sharpe": sharpe,
"worst": min(rets) * 100 if rets else 0.0, # peggior trade, % equity (ret*pos)
"yearly": yearly,
"_trades": trade_rows,
}
def evaluate(policy: ExitPolicy, sleeves: dict | None = None, **kw) -> dict:
"""train (fino al 2023-11-01) e oos (dopo) per BTC e ETH. Stampa sintesi."""
sleeves = sleeves or load_sleeves()
out = {}
for a in ASSETS:
ctx = sleeves[a]
tr = simulate(ctx, policy, t_hi=OOS_START_MS, **kw)
oo = simulate(ctx, policy, t_lo=OOS_START_MS, **kw)
out[a] = {"train": tr, "oos": oo}
print(f" {policy.name:<28s} {a}: "
f"TRAIN ret={tr['ret']:>+7.0f}% dd={tr['dd']:>4.0f}% shrp={tr['sharpe']:>5.2f} "
f"worst={tr['worst']:>+5.1f}% stop={tr['stop_rate']:>4.1f}% | "
f"OOS ret={oo['ret']:>+6.0f}% dd={oo['dd']:>4.0f}% shrp={oo['sharpe']:>5.2f} "
f"worst={oo['worst']:>+5.1f}%", flush=True)
return out
# ----------------------------------------------------------------------------- parity
def parity_check() -> bool:
"""Baseline (nessuno stop) == explore_lab.simulate sugli stessi entries."""
from scripts.analysis.explore_lab import get_df, simulate as ref_sim # noqa: E402
sleeves = load_sleeves()
ok = True
for a in ASSETS:
ctx = sleeves[a]
mine = simulate(ctx, ExitPolicy())
df = get_df(a, "1h")
ents = [{"i": i, "d": d, "max_bars": mb, "tp": None, "sl": None}
for (i, d, mb) in ctx["entries"]]
ref = ref_sim(ents, df)
same = (abs(mine["ret"] - ref["ret"]) < 1e-6 and mine["trades"] == ref["trades"]
and abs(mine["dd"] - ref["dd"]) < 1e-6)
ok &= same
print(f" parity {a}: mine ret={mine['ret']:+.2f}% trades={mine['trades']} "
f"| ref ret={ref['ret']:+.2f}% trades={ref['trades']} -> {'OK' if same else 'MISMATCH'}")
return ok
if __name__ == "__main__":
print("build cache (walk-forward SH01, puo' richiedere minuti)...")
load_sleeves(refresh="--refresh" in sys.argv)
print("parity check baseline vs explore_lab.simulate:")
ok = parity_check()
print("baseline train/oos:")
evaluate(ExitPolicy())
sys.exit(0 if ok else 1)