Files
PythagorasGoal/scripts/analysis/exit_policies/verify_16_leakage.py
T
Adriano Dal Pastro ad65a0b344 research(exit-lab): 34 agenti su exit dinamiche → EXIT-16 close-confirm SL PROMOSSO a livello PORT06
23 famiglie esplorate (harness condiviso exit_lab, train/OOS embargo nov-2023,
tutto lo storico 1h 2018-2026) + 10 verifiche avversariali + test PORT06.
'Cavalcare il prezzo' non esiste (4a conferma: oltre il TP=media non c'e' coda).
Scoperta: lo SL intrabar fisso e' il distruttore di valore n.1 delle fade
(stop da wick = falsi negativi). Forma robusta: SL solo su CHIUSURA oltre
sl0±0.5·ATR14 — PORT06 FULL Sharpe 6.47→7.84 DD 4.10→2.60, OOS 8.82→10.06.
Collaterali: bias gap-through dell'engine sugli stop stretti; ramo -2% del
worker morto con sl=0. Diario: docs/diary/2026-06-04-exit-lab.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 21:16:58 +00:00

212 lines
8.5 KiB
Python

"""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()