"""Verifica avversariale LEAKAGE/ESEGUIBILITA' per EXIT-16 close_confirm_sl. Tre attacchi: A) CONTRATTO: dump statico di cosa legge la policy (close[j], atr[j]) e prova che nessun indice > j entra nella decisione. Replica esatta del numero headline (MR02 BTC/ETH OOS) per ancorare. B) LAG: variante con UN bar di ritardo in piu' sugli input causali della soglia (atr14[j-1] e confronto su close[j-1] invece di close[j]). Se l'edge collassa -> appeso al timing perfetto. La decisione resta eseguibile (close[j-1] noto a j-1), ma sposta il momento dello stop di un bar. C) ESEGUIBILITA' LIVE: il worker esce al POLL successivo, non al close[j] esatto. Stima del costo eseguendo l'uscita a open[j+1] invece di close[j]. Esegui: cd /opt/docker/PythagorasGoal && PYTHONPATH=. uv run python \ scripts/analysis/exit_policies/verify_16_leakage.py """ import sys from pathlib import Path import numpy as np HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE.parent)) # scripts/analysis sys.path.insert(0, str(HERE.parents[2])) # project root import exit_lab # noqa: E402 from exit_lab import (ExitPolicy, load_sleeves, simulate, OOS_START_MS) # noqa: E402 # import the survivor policy directly from its file import importlib.util # noqa: E402 spec = importlib.util.spec_from_file_location("p16", HERE / "16_close_confirm_sl.py") p16 = importlib.util.module_from_spec(spec) spec.loader.exec_module(p16) CloseConfirmSl = p16.CloseConfirmSl BUF = 0.5 # train-pick buffer # --------------------------------------------------------------- B) LAG variant class CloseConfirmSlLag(ExitPolicy): """Identica a EXIT-16 ma con 1 bar di ritardo sugli input della soglia: decisione su close[j-1] e atr[j-1] (eseguibile gia' a j-1). Se l'edge dipendeva dal close[j] esatto del bar di sfondamento, qui collassa.""" name = "close_confirm_sl_lag" def __init__(self, ctx, i, d, entry, tp0, sl0, mb, **params): super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params) self.buffer = float(params.get("buffer", 0.0)) self.close = ctx["close"] self.atr = ctx["atr14"] def levels(self, j): return self.tp0, None, 1.0 def after_bar(self, j): jj = j - 1 if jj <= self.i: return False a = self.atr[jj] if not np.isfinite(a): a = 0.0 cj = self.close[jj] if self.d == 1: return cj < self.sl0 - self.buffer * a return cj > self.sl0 + self.buffer * a # ----------------------------------------- C) execution-delay (open[j+1]) variant def simulate_open_next(sleeve, params, start_ms=None, end_ms=None): """Come exit_lab.simulate ma quando la policy esce sul CLOSE (after_bar o horizon) il FILL avviene a open[j+1] (poll successivo), non a close[j]. I TP/SL intrabar restano al livello (limit). Stima il costo del ritardo di un poll per un'exit market al prossimo bar.""" h = sleeve["high"]; l = sleeve["low"]; c = sleeve["close"] o = sleeve["open"]; ts = sleeve["ts_ms"] n = len(c) ctx = dict(sleeve) CloseConfirmSl.prepare(ctx, **params) fee = exit_lab.FEE_RT * exit_lab.LEV POS = exit_lab.POS; LEV = exit_lab.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 = CloseConfirmSl(ctx, i, d, entry, tp0, sl0, mb, **params) horizon = min(int(pol.horizon), exit_lab.HARD_CAP) fills = [] 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: 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): # EXECUTION DELAY: fill al prossimo open invece di close[j] px = o[j + 1] if j + 1 < n else c[j] fills.append((remaining, px)); remaining = 0.0 break if step == horizon: px = o[j + 1] if j + 1 < n else c[j] fills.append((remaining, px)); remaining = 0.0 if remaining > 1e-9: 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, "sharpe_t": float(r.mean() / r.std() * np.sqrt(len(r))) if r.std() else 0.0, "avg_bars": bars_tot / trades} 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']:>5.2f} " f"n{r['trades']:>4} bars{r['avg_bars']:>5.1f}") def main(): data = load_sleeves() params = {"buffer": BUF} keys = list(data.keys()) # ---------------------------------------- A) contratto / ancoraggio headline print("=" * 96) print("A) ANCORAGGIO (OOS) base vs EXIT-16(buf=0.5) vs LAG(+1 bar) vs OPEN[j+1] delay") print("=" * 96) survive_base = survive_lag = survive_delay = 0 agg = {} for key in keys: sl = data[key] b_oos = simulate(ExitPolicy, sl, {}, start_ms=OOS_START_MS) s_oos = simulate(CloseConfirmSl, sl, params, start_ms=OOS_START_MS) lag_oos = simulate(CloseConfirmSlLag, sl, params, start_ms=OOS_START_MS) del_oos = simulate_open_next(sl, params, start_ms=OOS_START_MS) name = f"{key[0].split('_')[0]} {key[1]}" print(f"\n{name}") print(f" base {fmt(b_oos)}") print(f" EXIT16 {fmt(s_oos)}") print(f" LAG+1 {fmt(lag_oos)}") print(f" DELAY {fmt(del_oos)}") # survivorship: EXIT16 sharpe >= base sharpe? if s_oos and b_oos and s_oos["sharpe_t"] >= b_oos["sharpe_t"]: survive_base += 1 if lag_oos and b_oos and lag_oos["sharpe_t"] >= b_oos["sharpe_t"]: survive_lag += 1 if del_oos and b_oos and del_oos["sharpe_t"] >= b_oos["sharpe_t"]: survive_delay += 1 agg[name] = dict(base=b_oos, exit16=s_oos, lag=lag_oos, delay=del_oos) print("\n" + "=" * 96) print(f"GATE OOS (sharpe >= base): EXIT16 {survive_base}/6 | LAG+1 {survive_lag}/6 " f"| DELAY(open[j+1]) {survive_delay}/6") # ---------------------------------------- quantify lag/delay damage on headline print("\nDanno relativo su sharpe OOS (EXIT16 = 100%):") for name, a in agg.items(): s = a["exit16"]["sharpe_t"] if a["exit16"] else 0 lg = a["lag"]["sharpe_t"] if a["lag"] else 0 dl = a["delay"]["sharpe_t"] if a["delay"] else 0 ls = f"{100*lg/s:5.0f}%" if s else " n/a" ds = f"{100*dl/s:5.0f}%" if s else " n/a" print(f" {name:<10} sh{s:5.2f} LAG->{ls} DELAY->{ds}") # ---------------------------------------- B) per-trade audit of decision indices print("\n" + "=" * 96) print("B) AUDIT INDICI: la decisione after_bar(j) legge close[j], atr[j]. " "Verifico\n che simulate() chiami after_bar SOLO con j = i+step (mai > j corrente).") # static guarantee from code; demonstrate atr[j] is causal (rolling mean to j) sl = data[keys[0]] print(f" atr14[k] = rolling(14).mean(TR) -> usa TR[k-13..k], tutti chiusi a k. OK") print(f" close[j] noto al close del bar j. Nessun indice > j nella decisione. OK") if __name__ == "__main__": main()