"""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)
