"""SH01 EXIT POLICY 07 — LOSER time-stop condizionale (after_bar). Idea: SH01 esce a orizzonte fisso H=12. Se a un check-point intermedio j=i+m il trade e' GIA' in perdita oltre una soglia, esci subito al close invece di tenere fino a H. L'ipotesi (lezione exit-lab fade: tagliare i loser presto rischia di tagliare anche i winner in drawdown temporaneo): su un orizzonte FISSO di 12 bar forse un loser conclamato a meta' corsa raramente recupera, mentre i winner del modello partono subito (asimmetria). Il time-stop e' UNA volta sola (al bar m), non un trailing: non insegue il prezzo, condiziona solo l'uscita a un istante. Regola (after_bar): al bar j == i + m: se (close[j]-entry)/entry * d < -x * ATR14[i] / entry esci al close del bar j. Equivalente: directional_move[j] < -x*ATR14[i]. x=0.0 => esci se in QUALSIASI perdita direzionale al bar m. Griglia m in {2, 3, 4, 6} x x in {0.0, 0.5, 1.0}. ANTI-LOOK-AHEAD: ATR14[i] e entry=close[i] fissati all'ingresso (dati <= i); after_bar decide sul close del bar j (dati <= j, eseguibile al poll del tick). Nessun indicatore al bar j stesso. cd /opt/docker/PythagorasGoal && uv run python scripts/analysis/sh01_exit_policies/07_loser_timestop.py """ from __future__ import annotations import sys sys.path.insert(0, "/opt/docker/PythagorasGoal") from scripts.analysis.sh01_exit_lab import ( # noqa: E402 ExitPolicy, evaluate, load_sleeves, simulate, OOS_START_MS, ) class LoserTimestop(ExitPolicy): def __init__(self, m: int, x: float): self.m = int(m) self.x = float(x) self.name = f"loser_timestop m={m} x={x:g}" def open_trade(self, ctx: dict, i: int, d: int) -> dict: return { "entry": float(ctx["close"][i]), "atr": float(ctx["atr14"][i]), } def after_bar(self, ctx: dict, i: int, d: int, j: int, st: dict) -> bool: if j != i + self.m: return False move = (ctx["close"][j] - st["entry"]) * d # directional, in price thresh = -self.x * st["atr"] return move < thresh GRID_M = [2, 3, 4, 6] GRID_X = [0.0, 0.5, 1.0] def _fmt(m): return (f"ret={m['ret']:>+7.0f}% dd={m['dd']:>4.0f}% shrp={m['sharpe']:>5.2f} " f"worst={m['worst']:>+5.1f}% stop={m['stop_rate']:>4.1f}% n={m['trades']}") def main(): sleeves = load_sleeves() base = {} for a in ("BTC", "ETH"): ctx = sleeves[a] base[a] = { "train": simulate(ctx, ExitPolicy(), t_hi=OOS_START_MS), "oos": simulate(ctx, ExitPolicy(), t_lo=OOS_START_MS), } print("=" * 110) print("BASELINE (exit orizzonte puro):") for a in ("BTC", "ETH"): print(f" {a} TRAIN {_fmt(base[a]['train'])}") print(f" {a} OOS {_fmt(base[a]['oos'])}") print("=" * 110) print("GRID — TRAIN ONLY (selezione parametri): m x rows") train_res = {} for mm in GRID_M: for xx in GRID_X: pol = LoserTimestop(mm, xx) row = {} for a in ("BTC", "ETH"): row[a] = simulate(sleeves[a], pol, t_hi=OOS_START_MS) train_res[(mm, xx)] = row print(f" m={mm} x={xx:g} | BTC {_fmt(row['BTC'])}") print(f" | ETH {_fmt(row['ETH'])}") print("=" * 110) print("OOS (verdetto, intera griglia per ispezione plateau):") for mm in GRID_M: for xx in GRID_X: pol = LoserTimestop(mm, xx) line_b = simulate(sleeves["BTC"], pol, t_lo=OOS_START_MS) line_e = simulate(sleeves["ETH"], pol, t_lo=OOS_START_MS) print(f" m={mm} x={xx:g} | BTC OOS {_fmt(line_b)}") print(f" | ETH OOS {_fmt(line_e)}") if __name__ == "__main__": main()