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