"""EXIT-05 — ratchet: stop a cricchetto sul profitto su close (high-water-mark). Idea: una volta che il trade ha messo a segno profitto (su CLOSE), proteggiamo una FRAZIONE f del profitto migliore raggiunto. hwm = miglior close a favore visto finora; quando hwm e' a favore dell'entrata, lo stop si porta a long : stop = entry + f*(hwm - entry) short: stop = entry - f*(entry - hwm) Lo stop e' SOLO-STRINGENTE (cricchetto): puo' solo avvicinarsi al prezzo, mai allentarsi. Parte da sl0 (protezione iniziale invariata finche' il ratchet non supera sl0). Differenza dal trailing chandelier (EXIT-01): qui lo stop e' ancorato all'ENTRY + frazione del profitto su close (non al massimo high - k*ATR). Cattura profitto in % del guadagno realizzato; con f<1 lascia un cuscinetto sotto l'hwm. VARIANTI: - keep_tp=True : il TP fisso tp0 resta (exit standard alla media) + ratchet di backup sullo SL. horizon = max_bars. - keep_tp=False: TP RIMOSSO -> ride puro protetto dal cricchetto, horizon=2*mb (cap HARD_CAP). Qui il ratchet e' l'unica uscita oltre l'orizzonte. ANTI-LOOK-AHEAD: levels(j) usa SOLO dati <= j-1: - hwm calcolato sui close[i .. j-1] (mantenuto incrementalmente, aggiornato col bar j-1 prima di calcolare lo stop attivo nel bar j; close[i]=entry e' incluso cosi' hwm>=entry sempre, lo stop non scende mai sotto entry una volta armato). Nessun TP nella variante ride -> nessun fill parziale. after_bar non usato. GRID: f in {0.3, 0.5, 0.7} 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 Ratchet(ExitPolicy): name = "ratchet" def __init__(self, ctx, i, d, entry, tp0, sl0, mb, f=0.5, keep_tp=True, **params): super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params) self.f = float(f) self.keep_tp = bool(keep_tp) self.close = ctx["close"] if not self.keep_tp: self.horizon = min(2 * mb, HARD_CAP) # high-water-mark del close a favore sullo slice [i..j-1]; il primo bar # valutato e' j=i+1 -> slice [i..i] = close[i] = entry (gia' incorporato). self.hwm = self.close[i] # = entry self._last_seen = i self.armed = False # diventa True quando hwm > entry (a favore) # stop a cricchetto: parte da sl0, puo' solo stringersi self.cur_stop = sl0 def levels(self, j: int): c = self.close d = self.d # incorpora i close fino a j-1 (causali, gia' chiusi al poll del bar j) while self._last_seen < j - 1: self._last_seen += 1 cv = c[self._last_seen] if d == 1: if cv > self.hwm: self.hwm = cv else: if cv < self.hwm: self.hwm = cv tp = self.tp0 if self.keep_tp else None # profitto su close del miglior bar a favore (>=0 perche' hwm parte da entry) prof = (self.hwm - self.entry) * d if prof > 0: # il trade e' andato a favore: arma il ratchet self.armed = True if d == 1: cand = self.entry + self.f * (self.hwm - self.entry) if cand > self.cur_stop: # cricchetto: lo stop long solo SALE self.cur_stop = cand else: cand = self.entry - self.f * (self.entry - self.hwm) if cand < self.cur_stop: # cricchetto: lo stop short solo SCENDE self.cur_stop = cand return tp, self.cur_stop, 1.0 GRID = [ {"f": f, "keep_tp": keep_tp} for f in (0.3, 0.5, 0.7) for keep_tp in (True, False) ] if __name__ == "__main__": evaluate(Ratchet, GRID)