"""EXIT-19 — TP RIMOSSO, exit al CANALE DONCHIAN OPPOSTO (donchian_trail). Idea: la fade attuale esce al TP (sulla media) + SL fisso (ATR dall'entrata) + max_bars. Qui TOGLIAMO il TP e usiamo come unica uscita di prezzo uno STOP DINAMICO ancorato al canale Donchian OPPOSTO alla direzione del trade: long (d=1): stop(j) = min(low[j-n .. j-1]) (canale inferiore) short (d=-1): stop(j) = max(high[j-n .. j-1]) (canale superiore) Per un long che funziona (il prezzo risale verso la media) il canale inferiore SALE bar dopo bar -> lo stop segue e blocca profitto; usciamo quando il prezzo ritraccia sotto il minimo recente. Simmetrico short. FLOOR a sl0 (mai PEGGIO dello SL originale): il livello attivo e' floorato alla protezione iniziale -> non si allenta mai oltre sl0. long : stop = max(channel_low, sl0) short: stop = min(channel_high, sl0) HORIZON = 4*mb (cap HARD_CAP=240): senza TP la posizione puo' restare a lungo, quindi diamo molto piu' respiro al time-stop; l'uscita "naturale" e' il canale. DIFFERENZA da EXIT-18 (swing_stop): qui (a) NON c'e' TP affatto (li' tp0 restava), (b) niente cricchetto persistente: lo stop e' il canale RICALCOLATO ogni bar (puo' anche allentarsi rispetto al bar prima, ma mai sotto sl0 grazie al floor), (c) horizon esteso 4x. E' una uscita puramente trend-following/Donchian innestata su un ingresso mean-reversion. ANTI-LOOK-AHEAD (contratto): levels(j) usa SOLO dati con indice <= j-1: - min/max sullo slice [j-n .. j-1] (lookback chiuso a j-1, lo[lo:hi] con hi=j); - nessun dato del bar j entra nel livello attivo nel bar j; - non si guarda mai high/low[j] per decidere lo stop attivo nel bar j. PRIOR dal repo (ladder scartato): il TP della fade sta alla MEDIA, dove il movimento e' esaurito; "il runner non corre". Quindi togliendo il TP rischiamo di restare in posizione MENTRE il prezzo ristagna/rientra, pagando giveback e fee. Il canale opposto dovrebbe limitare il giveback, ma la mean-reversion fa rientrare il prezzo prima che il canale si stringa -> probabile uscita PEGGIORE del TP. Lo misuriamo senza pregiudizio. GRID: n in {10, 20, 30} (3 celle). """ import sys from pathlib import Path import numpy as np sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from exit_lab import ExitPolicy, evaluate # noqa: E402 class DonchianTrail(ExitPolicy): name = "donchian_trail" def __init__(self, ctx, i, d, entry, tp0, sl0, mb, n=20, **params): super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params) self.n = int(n) self.low = ctx["low"] self.high = ctx["high"] # TP rimosso, horizon esteso 4x (il cap a HARD_CAP lo applica l'engine) self.horizon = 4 * mb def levels(self, j: int): d = self.d # canale opposto causale: slice [lo : j] => indici <= j-1 lo = max(self.i, j - self.n) hi = j if hi <= lo: # primo bar dopo l'entrata: nessuna finestra -> usa solo sl0 (no TP) return None, self.sl0, 1.0 if d == 1: ch = float(np.min(self.low[lo:hi])) stop = max(ch, self.sl0) # floor: mai sotto sl0 else: ch = float(np.max(self.high[lo:hi])) stop = min(ch, self.sl0) # floor: mai sopra sl0 return None, stop, 1.0 # TP = None (rimosso) GRID = [ {"n": 10}, {"n": 20}, {"n": 30}, ] if __name__ == "__main__": evaluate(DonchianTrail, GRID)