"""EXIT-18 — SL STRUTTURALE su swing low/high (structural / chandelier-by-structure). Idea: invece di uno stop FISSO a sl0 (ATR dall'entrata), uno stop ancorato alla STRUTTURA recente del prezzo. Per un long: il livello "naturale" sotto cui la tesi mean-reversion e' invalidata e' il minimo dello swing recente; se il prezzo rompe sotto il minimo degli ultimi n bar la fade ha torto. Simmetrico per lo short sul massimo recente. long (d=1): level(j) = min(low[j-n .. j-1]) - buf short (d=-1): level(j) = max(high[j-n .. j-1]) + buf buf = 0.25 * atr14[j-1] SOLO-STRINGENTE: lo stop parte da sl0 (protezione iniziale invariata) e si aggiorna SOLO se il livello swing e' PIU' PROTETTIVO (cricchetto): long : cur_stop = max(cur_stop, level) (lo stop puo' solo SALIRE) short: cur_stop = min(cur_stop, level) (lo stop puo' solo SCENDERE) Cosi' man mano che il prezzo (per una fade vincente) torna verso la media, lo swing low/high sale/scende e lo stop segue, bloccando profitto. Non si allenta MAI. TP fisso (tp0) e horizon=max_bars invariati: questa famiglia cambia SOLO lo stop. ANTI-LOOK-AHEAD (contratto): levels(j) usa SOLO dati con indice <= j-1: - massimi/minimi sullo slice [j-n .. j-1] (lookback chiuso a j-1); - buffer da atr14[j-1] (indicatore causale, valore del bar precedente). Mantengo cur_stop in modo incrementale ma lo aggiorno con i bar fino a j-1, mai j. Nessun dato del bar j o successivi entra nel livello attivo nel bar j. MECCANISMO ATTESO: lo stop strutturale e' tipicamente PIU' LASCO di sl0 all'inizio (il minimo recente puo' stare oltre sl0): in quel caso il cricchetto lo IGNORA e resta a sl0 (non allentiamo mai). Quando invece la struttura si stringe (lo swing low risale verso il prezzo dopo che la fade comincia a funzionare) lo stop SALE, proteggendo i guadagni di un trade che poi rintraccia prima del TP. Prior dal repo (ladder scartato): il TP della fade sta alla MEDIA, dove il movimento e' esaurito -> oltre il TP non c'e' runner. Qui non tocchiamo il TP, quindi non puntiamo a cavalcare oltre: cerchiamo SOLO di tagliare meglio i loser/ristagni proteggendo profitto. RISCHIO (fade = mean-reversion): stringere lo stop su un pullback transitorio stoppa fuori trade che sarebbero rientrati al TP -> winner tagliati e stop-rate su. Lo misuriamo. GRID: n in {5, 10, 20} (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 SwingStop(ExitPolicy): name = "swing_stop" def __init__(self, ctx, i, d, entry, tp0, sl0, mb, n=10, **params): super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params) self.n = int(n) self.low = ctx["low"] self.high = ctx["high"] self.atr14 = ctx["atr14"] # stop a cricchetto: parte dalla protezione iniziale, puo' solo stringersi self.cur_stop = sl0 # indice fino a cui ho gia' incorporato i bar nel cricchetto (escluso) self._last_seen = i def _swing_level(self, j: int): """Livello swing causale usando SOLO low/high su [j-n .. j-1] e atr14[j-1].""" lo = max(self.i, j - self.n) # non andare prima dell'entrata hi = j # slice [lo:hi] => indici <= j-1 if hi <= lo: return None a = self.atr14[j - 1] if not np.isfinite(a): a = 0.0 buf = 0.25 * a if self.d == 1: return float(np.min(self.low[lo:hi])) - buf else: return float(np.max(self.high[lo:hi])) + buf def levels(self, j: int): d = self.d # aggiorna il cricchetto bar-per-bar fino a j-1 (causale). Per ogni bar k # passato calcolo il livello swing attivo "a quel momento" e stringo lo stop. while self._last_seen < j: self._last_seen += 1 k = self._last_seen lvl = self._swing_level(k) if lvl is None: continue if d == 1: if lvl > self.cur_stop: # cricchetto long: lo stop solo SALE self.cur_stop = lvl else: if lvl < self.cur_stop: # cricchetto short: lo stop solo SCENDE self.cur_stop = lvl return self.tp0, self.cur_stop, 1.0 GRID = [ {"n": 5}, {"n": 10}, {"n": 20}, ] if __name__ == "__main__": evaluate(SwingStop, GRID)