"""EXIT-09 — tp_extend_momentum: cavalca il momentum OLTRE il TP. Idea: le fade escono al TP fisso (alla media). Ma quando il movimento NON e' esaurito e sfonda il TP in chiusura, vogliamo provare a cavalcarlo invece di prendere solo il TP. State machine a 2 fasi: FASE A (default, TP intatto): tp = tp0, sl = sl0. Exit standard alla media. Finche' close[j-1] NON ha superato tp0 in chiusura, ci si comporta come base. FASE B (armata quando close[j-1] supera tp0 a favore): - TP RIMOSSO (None): non si esce piu' al livello fisso; - attiva un trail "chandelier" a k*ATR ancorato all'ESTREMO favorevole visto DAL bar di superamento in poi (high per long, low per short); - FLOOR a tp0: lo stop trailing non scende mai sotto tp0 (long) / non sale mai sopra tp0 (short), cosi' NON si esce MAI peggio del TP originale. Lo stop e' inoltre solo-stringente (cricchetto): puo' solo avvicinarsi al prezzo. Trigger di arma (su close[j-1], a favore): long : close[j-1] > tp0 -> arma short: close[j-1] < tp0 -> arma Trail attivo (FASE B), con floor a tp0: long : stop = max( tp0, fav_high_post - k*atr14[j-1] ) (cricchetto: solo sale) short: stop = min( tp0, fav_low_post + k*atr14[j-1] ) (cricchetto: solo scende) Orizzonte esteso a 3*mb (cap HARD_CAP=240) per dare spazio al runner post-TP. ANTI-LOOK-AHEAD: levels(j) usa SOLO dati <= j-1: - il trigger di arma legge close[j-1] (gia' chiuso al poll del bar j); - l'estremo favorevole post-superamento e' sullo slice [arm_idx .. j-1] (mantenuto incrementalmente, incorporando i bar fino a j-1); - atr14[j-1] (indice causale). Nessun fill parziale (tp_frac sempre 1.0). after_bar non usato. GRID: k in {1.0, 2.0, 3.0} (3 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 TpExtendMomentum(ExitPolicy): name = "tp_extend_momentum" def __init__(self, ctx, i, d, entry, tp0, sl0, mb, k=2.0, **params): super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params) self.k = float(k) self.horizon = min(3 * mb, HARD_CAP) self.close = ctx["close"] self.high = ctx["high"] self.low = ctx["low"] self.atr = ctx["atr14"] # FASE A finche' armed=False. Si arma quando close[j-1] supera tp0 a favore. self.armed = False self.arm_idx = None # bar dal quale si ancora l'estremo favorevole self.fav_high = None # estremo favorevole post-superamento (long) self.fav_low = None # estremo favorevole post-superamento (short) self._last_seen = i # ultimo close gia' esaminato per il trigger self._fav_seen = None # ultimo bar gia' incorporato nell'estremo # stop trailing monotono in FASE B: parte dal floor tp0, solo si stringe self.cur_stop = None def levels(self, j: int): c = self.close d = self.d # ---- FASE A -> controlla l'arma sui close fino a j-1 (causali) ----------- if not self.armed: while self._last_seen < j - 1: self._last_seen += 1 cv = c[self._last_seen] crossed = (cv > self.tp0) if d == 1 else (cv < self.tp0) if crossed: # arma: ancora l'estremo favorevole DAL bar di superamento self.armed = True self.arm_idx = self._last_seen self.fav_high = self.high[self.arm_idx] self.fav_low = self.low[self.arm_idx] self._fav_seen = self.arm_idx self.cur_stop = self.tp0 # floor di partenza = tp0 break if not self.armed: return self.tp0, self.sl0, 1.0 # ancora FASE A: TP/SL fissi # ---- FASE B -> TP rimosso, trail chandelier ancorato post-superamento ----- # incorpora i bar [arm_idx .. j-1] nell'estremo favorevole (dati causali) while self._fav_seen < j - 1: self._fav_seen += 1 if self.high[self._fav_seen] > self.fav_high: self.fav_high = self.high[self._fav_seen] if self.low[self._fav_seen] < self.fav_low: self.fav_low = self.low[self._fav_seen] a = self.atr[j - 1] if a == a: # non-NaN if d == 1: cand = self.fav_high - self.k * a cand = max(cand, self.tp0) # floor: mai sotto il TP if cand > self.cur_stop: # cricchetto: solo sale self.cur_stop = cand else: cand = self.fav_low + self.k * a cand = min(cand, self.tp0) # floor: mai sopra il TP if cand < self.cur_stop: # cricchetto: solo scende self.cur_stop = cand return None, self.cur_stop, 1.0 # TP rimosso in FASE B GRID = [{"k": k} for k in (1.0, 2.0, 3.0)] if __name__ == "__main__": evaluate(TpExtendMomentum, GRID)