"""EXIT-03 — trail_pct: trailing percentuale dall'high-water-mark favorevole (sui CLOSE). Idea: invece di un trail ATR (EXIT-01/02), lo stop segue l'estremo favorevole RAGGIUNTO dai CLOSE a una distanza percentuale fissa p. Lo stop puo' solo stringersi (mai allentarsi sotto sl0 lato rischio). Long : hwm = max(close[i..j-1]); stop(j) = max(sl0, hwm*(1-p)) (sale, mai scende) Short: lwm = min(close[i..j-1]); stop(j) = min(sl0, lwm*(1+p)) (scende, mai sale) Due varianti: keep_tp=True -> il TP fisso tp0 dall'entrata RESTA attivo (esci al primo dei due), horizon = max_bars (invariato). Il trail morde solo se il TP non viene raggiunto e il prezzo ha prima corso a favore e poi ritracciato. keep_tp=False -> TP RIMOSSO (cavalcata pura), horizon = 2*max_bars (cap HARD_CAP). Si esce solo sul trailing-stop o a orizzonte. ANTI-LOOK-AHEAD: levels(j) usa SOLO dati <= j-1: - hwm/lwm sullo slice [i..j-1] dei CLOSE (mantenuto incrementalmente col bar j-1); - p e' una costante -> nessuna dipendenza da indicatori del bar j. L'hwm sui close (non sugli high) e' deliberato: il close e' il dato su cui il worker live decide al poll del tick; un hwm sugli high anticiperebbe un livello che il close non ha ancora confermato. GRID: p in {0.005, 0.01, 0.02} x keep_tp in {True, False} (6 celle). """ import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from exit_lab import ExitPolicy, evaluate, HARD_CAP # noqa: E402 class TrailPct(ExitPolicy): name = "trail_pct" def __init__(self, ctx, i, d, entry, tp0, sl0, mb, p=0.01, keep_tp=True, **params): super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params) self.p = float(p) self.keep_tp = bool(keep_tp) if not self.keep_tp: self.horizon = min(2 * mb, HARD_CAP) self.close = ctx["close"] # high/low-water-mark sui CLOSE, solo barre <= j-1; init = close d'entrata i self.hwm = self.close[i] self.lwm = self.close[i] self.last_seen = i # ultimo indice gia' incorporato # stop trailing monotono: parte da sl0 e puo' solo stringersi self.cur_stop = sl0 def _update_wm(self, upto: int) -> None: """Incorpora le barre (last_seen, upto] nell'estremo sui close. upto = j-1 -> NON tocca il bar j (anti-look-ahead).""" while self.last_seen < upto: self.last_seen += 1 cv = self.close[self.last_seen] if cv > self.hwm: self.hwm = cv if cv < self.lwm: self.lwm = cv def levels(self, j: int): self._update_wm(j - 1) # solo dati <= j-1 if self.d == 1: cand = self.hwm * (1.0 - self.p) if cand > self.cur_stop: # lo stop long puo' solo SALIRE self.cur_stop = cand else: cand = self.lwm * (1.0 + self.p) if cand < self.cur_stop: # lo stop short puo' solo SCENDERE self.cur_stop = cand tp = self.tp0 if self.keep_tp else None return tp, self.cur_stop, 1.0 GRID = [ {"p": p, "keep_tp": kt} for p in (0.005, 0.01, 0.02) for kt in (True, False) ] if __name__ == "__main__": evaluate(TrailPct, GRID)