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,259 @@
|
||||
"""EXIT LAB — harness onesto e CONDIVISO per la ricerca di policy di uscita
|
||||
(TP dinamico, SL dinamico/trailing, partial, ride) sulle fade attive.
|
||||
|
||||
Ricerca 2026-06-04 (>=20 agenti): ogni agente implementa una ExitPolicy in
|
||||
scripts/analysis/exit_policies/<id>_<nome>.py e la valuta QUI, sugli STESSI
|
||||
segnali (cache su disco) e con lo stesso engine intrabar di fade_base.
|
||||
|
||||
CONTRATTO ANTI-LOOK-AHEAD (vincolante, verra' verificato da agenti avversari):
|
||||
- i livelli attivi nel bar j (`levels(j)`) possono usare SOLO dati <= j-1
|
||||
(il worker live li fissa al close del bar precedente, poi il bar j li tocca);
|
||||
- `after_bar(j)` decide sul CLOSE del bar j (eseguibile al poll del tick);
|
||||
- indicatori: usare l'indice j-1 degli array causali (es. ctx["atr14"][j-1]).
|
||||
|
||||
PROTOCOLLO ANTI-OVERFIT (vincolante):
|
||||
- TRAIN = storico fino al 2023-11-01, OOS = dopo. La SELEZIONE dei parametri
|
||||
si fa SOLO sul train; l'OOS si guarda una volta, per il verdetto.
|
||||
- gate: il miglioramento deve tenere su ENTRAMBI gli asset e su TUTTE e 3 le
|
||||
strategie (train E oos), con plateau sulla griglia (non una cella isolata).
|
||||
- fee 0.10% RT x leva su tutto il notional; nessuna fee scontata sui limit.
|
||||
|
||||
Baseline = exit attuale (TP/SL fissi dall'entrata + max_bars): la parita' con
|
||||
`partial_tp_ladder.py --base` e' verificata da `parity_check()`.
|
||||
|
||||
uv run python scripts/analysis/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))
|
||||
|
||||
from src.data.downloader import load_data # noqa: E402
|
||||
from src.live.strategy_loader import load_strategy # noqa: E402
|
||||
|
||||
LIVE_PARAMS = dict(trend_max=3.0, ema_long=200, hurst_max=0.55, min_tp_frac=0.0015)
|
||||
OOS_START_MS = int(pd.Timestamp("2023-11-01", tz="UTC").value // 1e6)
|
||||
LEV, POS, FEE_RT = 3.0, 0.15, 0.001
|
||||
CODES = ["MR01_bollinger_fade", "MR02_donchian_fade", "MR07_return_reversal"]
|
||||
ASSETS = ("BTC", "ETH")
|
||||
CACHE = PROJECT_ROOT / "data" / "cache" / "exit_lab_signals.pkl"
|
||||
HARD_CAP = 240 # bound assoluto ai bar in posizione (policy "ride" comprese)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- dati
|
||||
|
||||
def _atr14(h: np.ndarray, l: np.ndarray, c: np.ndarray) -> np.ndarray:
|
||||
pc = np.roll(c, 1); pc[0] = c[0]
|
||||
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
|
||||
return pd.Series(tr).rolling(14).mean().values
|
||||
|
||||
|
||||
def load_sleeves(refresh: bool = False) -> dict:
|
||||
"""{(code, asset): sleeve} con cache. sleeve = {signals, open, high, low,
|
||||
close, ts_ms, atr14}. signals = [(i, d, tp0, sl0, mb), ...] dai params LIVE."""
|
||||
if CACHE.exists() and not refresh:
|
||||
with open(CACHE, "rb") as f:
|
||||
return pickle.load(f)
|
||||
out = {}
|
||||
for code in CODES:
|
||||
strat = load_strategy(code)
|
||||
for asset in ASSETS:
|
||||
df = load_data(asset, "1h")
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
sigs = strat.generate_signals(df, ts, **LIVE_PARAMS)
|
||||
h = df["high"].values.astype(float)
|
||||
l = df["low"].values.astype(float)
|
||||
c = df["close"].values.astype(float)
|
||||
out[(code, asset)] = {
|
||||
"signals": [(int(s.idx), int(s.direction), float(s.metadata["tp"]),
|
||||
float(s.metadata["sl"]), int(s.metadata["max_bars"]))
|
||||
for s in sigs],
|
||||
"open": df["open"].values.astype(float),
|
||||
"high": h, "low": l, "close": c,
|
||||
"ts_ms": df["timestamp"].values.astype(np.int64),
|
||||
"atr14": _atr14(h, l, c),
|
||||
}
|
||||
print(f" cache {code} {asset}: {len(sigs)} segnali, {len(c)} barre "
|
||||
f"({ts.iloc[0].date()} -> {ts.iloc[-1].date()})")
|
||||
CACHE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(CACHE, "wb") as f:
|
||||
pickle.dump(out, f)
|
||||
return out
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- policy
|
||||
|
||||
class ExitPolicy:
|
||||
"""Baseline = exit live attuale. Le sottoclassi ridefinisco levels/after_bar.
|
||||
|
||||
Una ISTANZA per trade. `ctx` e' il dict sleeve (array completi + indicatori
|
||||
aggiunti da prepare()): per contratto si legge SOLO fino a j-1 in levels(j)
|
||||
e fino a j in after_bar(j)/on_partial(j).
|
||||
"""
|
||||
name = "base"
|
||||
|
||||
@classmethod
|
||||
def prepare(cls, ctx: dict, **params) -> None:
|
||||
"""Pre-calcola array causali per-sleeve (una volta), es. SMA/EMA."""
|
||||
|
||||
def __init__(self, ctx: dict, i: int, d: int, entry: float,
|
||||
tp0: float, sl0: float, mb: int, **params):
|
||||
self.ctx, self.i, self.d, self.entry = ctx, i, d, entry
|
||||
self.tp0, self.sl0, self.mb = tp0, sl0, mb
|
||||
self.horizon = mb # le sottoclassi possono estendere (cap HARD_CAP)
|
||||
|
||||
def levels(self, j: int):
|
||||
"""Livelli ATTIVI nel bar j -> (tp, sl, tp_frac). None = livello assente.
|
||||
tp_frac = quota del RESIDUO che esce al tocco del TP (1.0 = tutta)."""
|
||||
return self.tp0, self.sl0, 1.0
|
||||
|
||||
def on_partial(self, j: int, price: float, remaining: float) -> None:
|
||||
"""Notifica del fill parziale al TP nel bar j (aggiorna lo stato qui)."""
|
||||
|
||||
def after_bar(self, j: int) -> bool:
|
||||
"""True = chiudi il residuo al close[j] (decisione sul close, eseguibile)."""
|
||||
return False
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- engine
|
||||
|
||||
def simulate(policy_cls, sleeve: dict, params: dict | None = None,
|
||||
start_ms: int | None = None, end_ms: int | None = None) -> dict:
|
||||
"""Replay intrabar dei segnali dello sleeve con la policy. SL prioritario
|
||||
sul TP nello stesso bar (conservativo); fill parziali pesati; max_bars/
|
||||
horizon esce al close; non-overlap (una posizione per volta)."""
|
||||
params = params or {}
|
||||
h, l, c, ts = sleeve["high"], sleeve["low"], sleeve["close"], sleeve["ts_ms"]
|
||||
n = len(c)
|
||||
ctx = dict(sleeve)
|
||||
policy_cls.prepare(ctx, **params)
|
||||
fee = FEE_RT * LEV
|
||||
capital = peak = 1000.0
|
||||
max_dd = 0.0
|
||||
last_exit = -1
|
||||
trades = wins = 0
|
||||
bars_tot = 0
|
||||
rets = []
|
||||
|
||||
for (i, d, tp0, sl0, mb) in sleeve["signals"]:
|
||||
if start_ms is not None and ts[i] < start_ms:
|
||||
continue
|
||||
if end_ms is not None and ts[i] >= end_ms:
|
||||
continue
|
||||
if i <= last_exit or i + 1 >= n:
|
||||
continue
|
||||
entry = c[i]
|
||||
pol = policy_cls(ctx, i, d, entry, tp0, sl0, mb, **params)
|
||||
horizon = min(int(pol.horizon), HARD_CAP)
|
||||
fills: list[tuple[float, float]] = []
|
||||
remaining = 1.0
|
||||
j = i
|
||||
for step in range(1, horizon + 1):
|
||||
j = i + step
|
||||
if j >= n:
|
||||
j = n - 1
|
||||
fills.append((remaining, c[j])); remaining = 0.0
|
||||
break
|
||||
tp, sl, tpfrac = pol.levels(j)
|
||||
hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
|
||||
hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
|
||||
if hit_sl: # conservativo: SL prima del TP
|
||||
fills.append((remaining, sl)); remaining = 0.0
|
||||
break
|
||||
if hit_tp:
|
||||
f = min(max(tpfrac, 0.0), 1.0) * remaining
|
||||
if f > 0:
|
||||
fills.append((f, tp)); remaining -= f
|
||||
if remaining <= 1e-9:
|
||||
break
|
||||
pol.on_partial(j, tp, remaining)
|
||||
if pol.after_bar(j):
|
||||
fills.append((remaining, c[j])); remaining = 0.0
|
||||
break
|
||||
if step == horizon:
|
||||
fills.append((remaining, c[j])); remaining = 0.0
|
||||
if remaining > 1e-9: # safety (non dovrebbe accadere)
|
||||
fills.append((remaining, c[j]))
|
||||
|
||||
ret = sum(f * (p - entry) for f, p in fills) / entry * d * LEV - fee
|
||||
capital = max(capital + capital * POS * ret, 10.0)
|
||||
peak = max(peak, capital)
|
||||
max_dd = max(max_dd, (peak - capital) / peak)
|
||||
last_exit = j
|
||||
trades += 1
|
||||
wins += ret > 0
|
||||
bars_tot += j - i
|
||||
rets.append(ret)
|
||||
|
||||
if trades == 0:
|
||||
return {}
|
||||
r = np.array(rets)
|
||||
return {
|
||||
"ret_pct": (capital / 1000.0 - 1) * 100,
|
||||
"dd_pct": max_dd * 100,
|
||||
"trades": trades,
|
||||
"win_pct": wins / trades * 100,
|
||||
"avg_ret_bps": r.mean() * 1e4,
|
||||
"sharpe_t": float(r.mean() / r.std() * np.sqrt(len(r))) if r.std() else 0.0,
|
||||
"avg_bars": bars_tot / trades,
|
||||
}
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- report
|
||||
|
||||
def evaluate(policy_cls, grid: list[dict], data: dict | None = None,
|
||||
quiet: bool = False) -> dict:
|
||||
"""Protocollo train/OOS su tutta la griglia. La selezione dei parametri va
|
||||
fatta SUL TRAIN (l'OOS si riporta, non si ottimizza). Ritorna dict
|
||||
{params_str: {sleeve: {train: {...}, oos: {...}}}} + baseline."""
|
||||
data = data or load_sleeves()
|
||||
out: dict = {}
|
||||
rows = [("base", ExitPolicy, {})] + [
|
||||
(", ".join(f"{k}={v}" for k, v in g.items()) or "default", policy_cls, g)
|
||||
for g in grid]
|
||||
for tag, cls, g in rows:
|
||||
out[tag] = {}
|
||||
for (code, asset), sleeve in data.items():
|
||||
key = f"{code.split('_')[0]} {asset}"
|
||||
tr = simulate(cls, sleeve, g, end_ms=OOS_START_MS)
|
||||
oo = simulate(cls, sleeve, g, start_ms=OOS_START_MS)
|
||||
out[tag][key] = {"train": tr, "oos": oo}
|
||||
if not quiet:
|
||||
print(f"{tag:<28}{key:<10}"
|
||||
f"TRAIN ret{tr.get('ret_pct', 0):>7.0f}% dd{tr.get('dd_pct', 0):>5.1f} "
|
||||
f"sh{tr.get('sharpe_t', 0):>5.2f} n{tr.get('trades', 0):>4} | "
|
||||
f"OOS ret{oo.get('ret_pct', 0):>6.0f}% dd{oo.get('dd_pct', 0):>5.1f} "
|
||||
f"sh{oo.get('sharpe_t', 0):>5.2f} n{oo.get('trades', 0):>4} "
|
||||
f"bars{oo.get('avg_bars', 0):>5.1f}")
|
||||
return out
|
||||
|
||||
|
||||
def parity_check() -> None:
|
||||
"""La baseline qui deve riprodurre i numeri FULL di partial_tp_ladder (base):
|
||||
MR01 BTC ~92%/13.8dd, MR01 ETH ~194%/16.5dd, MR02 ETH ~2135%/16.2dd..."""
|
||||
data = load_sleeves()
|
||||
print("\nParity check baseline (FULL, atteso = partial_tp_ladder base):")
|
||||
expected = {("MR01_bollinger_fade", "BTC"): 92, ("MR01_bollinger_fade", "ETH"): 194,
|
||||
("MR02_donchian_fade", "BTC"): 129, ("MR02_donchian_fade", "ETH"): 2135,
|
||||
("MR07_return_reversal", "BTC"): 78, ("MR07_return_reversal", "ETH"): 115}
|
||||
ok = True
|
||||
for key, sleeve in data.items():
|
||||
r = simulate(ExitPolicy, sleeve)
|
||||
exp = expected[key]
|
||||
match = abs(r["ret_pct"] - exp) < 1.0
|
||||
ok &= match
|
||||
print(f" {key[0].split('_')[0]} {key[1]}: ret {r['ret_pct']:.0f}% "
|
||||
f"(atteso ~{exp}) {'OK' if match else 'MISMATCH'}")
|
||||
print("PARITY", "OK" if ok else "FAILED")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
load_sleeves(refresh="--refresh" in sys.argv)
|
||||
parity_check()
|
||||
Reference in New Issue
Block a user