"""EXIT-12 — partial_tp_trail: partial AL TP PIENO, poi il runner CORRE col trail. Idea (diversa dal ladder 80/20 gia' SCARTATO, che metteva uno stop FISSO alla soglia 80% del TP): qui il partial avviene AL TP PIENO (tp0, alla media), esce una frazione q del trade, e il RESIDUO resta SENZA TP, protetto da un trailing chandelier k*ATR ancorato all'estremo favorevole raggiunto DOPO il partial; il floor dello stop e' tp0 -> il profitto al livello TP e' LOCKATO (il runner non puo' mai chiudere peggio di tp0). horizon esteso a 3*mb (cap HARD_CAP) per dare spazio al runner. Fase 1 (pre-partial): exit standard = (tp0, sl0). Al tocco di tp0 esce q. Fase 2 (post-partial): TP rimosso. Trail sul residuo: Long : stop(j) = max( tp0, max(high[part..j-1]) - k*atr14[j-1] ) (solo sale) Short: stop(j) = min( tp0, min(low [part..j-1]) + k*atr14[j-1] ) (solo scende) floor = tp0 -> profitto lockato al livello TP, MAI peggio. ANTI-LOOK-AHEAD: levels(j) usa SOLO dati <= j-1: - estremo favorevole post-partial sullo slice [part .. j-1] (mantenuto incrementalmente, aggiornato col bar j-1 prima di calcolare lo stop in j); - atr14[j-1] (indice causale). on_partial(j) registra solo l'indice del bar di partial (j) e il prezzo (tp0): l'estremo della fase 2 parte da high/low del bar di partial j (<= j, ma il primo bar valutato in fase 2 e' j+1, slice [j..j] noto al poll di j+1 -> causale). GRID: q in {0.5, 0.7} x k in {2.0, 3.0} (4 celle). tp_frac=q. """ 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 PartialTpTrail(ExitPolicy): name = "partial_tp_trail" def __init__(self, ctx, i, d, entry, tp0, sl0, mb, q=0.5, k=3.0, **params): super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params) self.q = float(q) self.k = float(k) self.horizon = min(3 * mb, HARD_CAP) self.high = ctx["high"] self.low = ctx["low"] self.atr = ctx["atr14"] # stato fase 2 (post-partial) self.partial_idx = None # bar in cui e' avvenuto il partial (None = fase 1) self.fav_high = None # estremo favorevole sullo slice [partial..j-1] self.fav_low = None self._last_seen = None # ultimo indice incorporato nell'estremo self.cur_stop = None # stop trailing fase 2, floor=tp0, monotono def levels(self, j): # ---- Fase 1: pre-partial -> exit standard (tp0, sl0), esce frazione q al TP if self.partial_idx is None: return self.tp0, self.sl0, self.q # ---- Fase 2: post-partial -> TP rimosso, trail chandelier floor=tp0 h, l, atr, d = self.high, self.low, self.atr, self.d # incorpora i bar fino a j-1 (dati causali, gia' chiusi al poll del bar j) while self._last_seen < j - 1: self._last_seen += 1 if h[self._last_seen] > self.fav_high: self.fav_high = h[self._last_seen] if l[self._last_seen] < self.fav_low: self.fav_low = l[self._last_seen] a = atr[j - 1] if a != a: # NaN -> resta sullo stop corrente return None, self.cur_stop, 1.0 if d == 1: cand = self.fav_high - self.k * a if cand > self.cur_stop: # lo stop long puo' solo SALIRE (stringersi) self.cur_stop = cand else: cand = self.fav_low + self.k * a if cand < self.cur_stop: # lo stop short puo' solo SCENDERE self.cur_stop = cand return None, self.cur_stop, 1.0 def on_partial(self, j, price, remaining): # entra in fase 2: ancora l'estremo al bar di partial j (high/low[j] sono # noti al poll del bar j+1, primo bar valutato in fase 2). floor=tp0. self.partial_idx = j self.fav_high = self.high[j] self.fav_low = self.low[j] self._last_seen = j self.cur_stop = self.tp0 # profitto lockato al livello TP, MAI peggio GRID = [ {"q": q, "k": k} for q in (0.5, 0.7) for k in (2.0, 3.0) ] if __name__ == "__main__": evaluate(PartialTpTrail, GRID)