"""ADVERSARIAL VERIFY — EXIT-02 trail_atr_keep_tp, LENTE OVERFIT/ROBUSTEZZA. Tesi del sopravvissuto: lo SL intrabar fisso distrugge valore nelle fade; il Chandelier trail (k=1.5) + TP fisso migliora Sharpe/DD ovunque (6/6 train, 5/6 OOS). Ipotesi nulla del verificatore: e' un artefatto. Tre attacchi: (1) JITTER parametri: k vicini non provati (1.25/1.75) + ponte SL fisso a 3x/4x ATR (no_sl). Il plateau tiene o e' una cresta? (2) STABILITA' TEMPORALE: train 2018-20 vs 21-22, OOS 23-11/25-01 vs 25-01/26-05. Il miglioramento c'e' in OGNI finestra o concentrato in un regime? (3) DIPENDENZA HURST (decisivo): i segnali in cache hanno hurst_max=0.55 (toglie il regime trending). Rigenero i segnali SENZA hurst (hurst_max=None) IN MEMORIA (non tocco la cache) e ripeto base-vs-policy: la tesi "SL dannoso" regge anche dove gli stop servivano (regime persistente)? cd /opt/docker/PythagorasGoal && PYTHONPATH=. uv run python \ scripts/analysis/exit_policies/verify_02_overfit.py """ import sys from pathlib import Path import numpy as np import pandas as pd HERE = Path(__file__).resolve() sys.path.insert(0, str(HERE.parents[1])) # scripts/analysis sys.path.insert(0, str(HERE.parents[3])) # project root import exit_lab # noqa: E402 from exit_lab import (ExitPolicy, simulate, load_sleeves, OOS_START_MS, # noqa: E402 CODES, ASSETS, LIVE_PARAMS, _atr14) from importlib import import_module # noqa: E402 mod = import_module("exit_policies.02_trail_atr_keep_tp") TrailATRKeepTP = mod.TrailATRKeepTP from src.data.downloader import load_data # noqa: E402 from src.live.strategy_loader import load_strategy # noqa: E402 SLEEVE_KEYS = [(c, a) for c in CODES for a in ASSETS] # --------------------------------------------------------------- fixed-SL bridge class FixedSLmultATR(ExitPolicy): """Ponte fra base (SL=sl0) e no_sl: SL fisso a m*ATR(entry) dall'entrata, TP fisso. Se il trail (k piccolo) batte uno SL fisso GIA' largo (3x/4x), allora il guadagno e' nel trailing, non solo nell'allontanare lo SL.""" name = "fixed_sl_mult_atr" def __init__(self, ctx, i, d, entry, tp0, sl0, mb, **params): super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params) m = float(params.get("m", 3.0)) a = ctx["atr14"][i] if a is None or a != a: self.sl = sl0 else: self.sl = entry - m * a if d == 1 else entry + m * a def levels(self, j: int): return self.tp0, self.sl, 1.0 # --------------------------------------------------------------- no-SL bridge class NoSL(ExitPolicy): """Solo TP fisso + horizon, NESSUNO stop. Isola: il valore e' nel TOGLIERE lo stop (qualsiasi) o nel TRAIL dinamico? Se NoSL ~ trail, il driver e' 'niente SL'; se il trail batte NoSL, il trail aggiunge.""" name = "no_sl" def levels(self, j: int): return self.tp0, None, 1.0 def _fmt(r): if not r: return " (no trades)" return (f"ret{r['ret_pct']:>7.0f}% dd{r['dd_pct']:>5.1f} sh{r['sharpe_t']:>6.2f} " f"n{r['trades']:>4} bars{r['avg_bars']:>5.1f}") def _summary(rows): """rows: list of (sleeve_key, base_dict, pol_dict). Ritorna conteggi miglioramento.""" sh_up = dd_dn = ret_up = n = 0 for _, b, p in rows: if not b or not p: continue n += 1 sh_up += p["sharpe_t"] > b["sharpe_t"] dd_dn += p["dd_pct"] < b["dd_pct"] ret_up += p["ret_pct"] > b["ret_pct"] return sh_up, dd_dn, ret_up, n # ============================================================= TEST 1: JITTER def test_jitter(data): print("\n" + "=" * 78) print("TEST 1 — JITTER: k vicini (1.25/1.75) + ponte SL fisso 3x/4x ATR + NoSL") print("=" * 78) print("\n[1a] Trail k in {1.25, 1.5, 1.75} — plateau o cresta? (OOS)") for k in (1.25, 1.5, 1.75): rows = [] for key in SLEEVE_KEYS: sl = data[key] b = simulate(ExitPolicy, sl, start_ms=OOS_START_MS) p = simulate(TrailATRKeepTP, sl, {"k": k}, start_ms=OOS_START_MS) rows.append((key, b, p)) sh, dd, ret, n = _summary(rows) print(f" k={k:<5} OOS: Sharpe-up {sh}/{n} DD-down {dd}/{n} ret-up {ret}/{n}") print("\n[1b] SL fisso a m*ATR dall'entrata (m=3,4) — uno stop largo basta? (OOS)") for m in (3.0, 4.0): rows = [] for key in SLEEVE_KEYS: sl = data[key] b = simulate(ExitPolicy, sl, start_ms=OOS_START_MS) p = simulate(FixedSLmultATR, sl, {"m": m}, start_ms=OOS_START_MS) rows.append((key, b, p)) sh, dd, ret, n = _summary(rows) print(f" m={m:<5} OOS: Sharpe-up {sh}/{n} DD-down {dd}/{n} ret-up {ret}/{n}") print("\n[1c] NoSL (solo TP+horizon) — il driver e' 'togliere lo SL'? (OOS)") rows = [] for key in SLEEVE_KEYS: sl = data[key] b = simulate(ExitPolicy, sl, start_ms=OOS_START_MS) p = simulate(NoSL, sl, start_ms=OOS_START_MS) rows.append((key, b, p)) sh, dd, ret, n = _summary(rows) print(f" NoSL OOS: Sharpe-up {sh}/{n} DD-down {dd}/{n} ret-up {ret}/{n}") print("\n[1d] dettaglio per sleeve: base vs k=1.5 vs NoSL vs SLx3 (OOS)") for key in SLEEVE_KEYS: sl = data[key] b = simulate(ExitPolicy, sl, start_ms=OOS_START_MS) t = simulate(TrailATRKeepTP, sl, {"k": 1.5}, start_ms=OOS_START_MS) ns = simulate(NoSL, sl, start_ms=OOS_START_MS) f3 = simulate(FixedSLmultATR, sl, {"m": 3.0}, start_ms=OOS_START_MS) tag = f"{key[0].split('_')[0]} {key[1]}" print(f" {tag:<10} base sh{b.get('sharpe_t',0):>6.2f} | trail1.5 sh{t.get('sharpe_t',0):>6.2f} " f"| NoSL sh{ns.get('sharpe_t',0):>6.2f} | SLx3 sh{f3.get('sharpe_t',0):>6.2f}") # ============================================================= TEST 2: TEMPORAL def test_temporal(data): print("\n" + "=" * 78) print("TEST 2 — STABILITA' TEMPORALE (Sharpe base -> trail k=1.5)") print("=" * 78) W = [ ("train 2018-20", None, int(pd.Timestamp("2021-01-01", tz="UTC").value // 1e6)), ("train 2021-22", int(pd.Timestamp("2021-01-01", tz="UTC").value // 1e6), OOS_START_MS), ("OOS 23-11/25-01", OOS_START_MS, int(pd.Timestamp("2025-01-01", tz="UTC").value // 1e6)), ("OOS 25-01/26-05", int(pd.Timestamp("2025-01-01", tz="UTC").value // 1e6), None), ] for label, s, e in W: rows = [] for key in SLEEVE_KEYS: sl = data[key] b = simulate(ExitPolicy, sl, start_ms=s, end_ms=e) p = simulate(TrailATRKeepTP, sl, {"k": 1.5}, start_ms=s, end_ms=e) rows.append((key, b, p)) sh, dd, ret, n = _summary(rows) # mediana del delta-Sharpe deltas = [p["sharpe_t"] - b["sharpe_t"] for _, b, p in rows if b and p] med = float(np.median(deltas)) if deltas else 0.0 print(f" {label:<20} Sharpe-up {sh}/{n} DD-down {dd}/{n} ret-up {ret}/{n} " f"median dSharpe {med:+.2f}") # ============================================================= TEST 3: HURST def _build_sleeves_no_hurst(): """Rigenera i segnali SENZA il loss-guard Hurst (hurst_max=None), IN MEMORIA. Replica esattamente load_sleeves() ma con LIVE_PARAMS modificati.""" params = dict(LIVE_PARAMS) params["hurst_max"] = None 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, **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), } return out def test_hurst(data): print("\n" + "=" * 78) print("TEST 3 — DIPENDENZA DAL FILTRO HURST (decisivo)") print("=" * 78) print("Rigenero segnali con hurst_max=None (loss-guard OFF -> include il regime") print("trending/persistente dove gli stop dovrebbero servire). Confronto base->trail.") nh = _build_sleeves_no_hurst() # quanti segnali in piu' (il guard ne toglieva) print("\n segnali: con-guard -> senza-guard") for key in SLEEVE_KEYS: ng = len(data[key]["signals"]) nn = len(nh[key]["signals"]) tag = f"{key[0].split('_')[0]} {key[1]}" print(f" {tag:<10} {ng:>4} -> {nn:>4} (+{nn-ng})") for scope, s, e in [("TRAIN", None, OOS_START_MS), ("OOS", OOS_START_MS, None)]: print(f"\n [{scope}] base vs trail k=1.5 — SENZA hurst guard") rows = [] for key in SLEEVE_KEYS: sl = nh[key] b = simulate(ExitPolicy, sl, start_ms=s, end_ms=e) p = simulate(TrailATRKeepTP, sl, {"k": 1.5}, start_ms=s, end_ms=e) rows.append((key, b, p)) tag = f"{key[0].split('_')[0]} {key[1]}" print(f" {tag:<10} base {_fmt(b)}") print(f" {'':<10} trail{_fmt(p)}") sh, dd, ret, n = _summary(rows) print(f" --> Sharpe-up {sh}/{n} DD-down {dd}/{n} ret-up {ret}/{n}") # contro-prova: con-guard sugli STESSI scope, per isolare l'effetto guard print("\n [CONTROLLO] stesso confronto CON hurst guard (cache):") for scope, s, e in [("TRAIN", None, OOS_START_MS), ("OOS", OOS_START_MS, None)]: rows = [] for key in SLEEVE_KEYS: sl = data[key] b = simulate(ExitPolicy, sl, start_ms=s, end_ms=e) p = simulate(TrailATRKeepTP, sl, {"k": 1.5}, start_ms=s, end_ms=e) rows.append((key, b, p)) sh, dd, ret, n = _summary(rows) print(f" [{scope}] con-guard --> Sharpe-up {sh}/{n} DD-down {dd}/{n} ret-up {ret}/{n}") if __name__ == "__main__": data = load_sleeves() test_jitter(data) test_temporal(data) test_hurst(data) print("\nDONE")