"""EXIT-20 — ema_cross_exit: esci quando il close attraversa contro-posizione la EMA_m.

IDEA. Le fade comprano sotto / vendono sopra la media e oggi escono al TP fisso (la
media congelata all'entrata) o a max_bars/SL. Qui usiamo la EMA_m come "linea della
media corrente": finche' il prezzo non l'ha attraversata, la reversione e' ancora in
corso; quando il close la attraversa CONTRO la posizione, la mean-reversion ha
esaurito il suo percorso -> si chiude al close.

  long  (d=+1): la fade ha comprato sotto la media; esce quando close[j] < ema_m[j]
                (il prezzo e' risalito ABOVE la media e ora la ri-perfora al ribasso,
                 ovvero il close torna sotto la media -> reversione finita/overshoot).
  short (d=-1): esce quando close[j] > ema_m[j].

NB sul segno. La condizione "long: close < ema" e' la cross CONTRO la posizione: la
fade long scommette sul rientro VERSO/OLTRE la media; quando il close ricade sotto la
EMA dopo che la reversione e' avvenuta, il segnale di reversione e' consumato. E' un
exit "alla media mobile" che insegue la media invece del target congelato.

VARIANTI keep_tp:
  - keep_tp=True : il TP fisso tp0 RESTA attivo (si esce al primo fra TP-al-livello,
                   SL, ema-cross, max_bars). horizon = max_bars (invariato).
  - keep_tp=False: si RIMUOVE il TP fisso (tp=None) e si lascia che sia la ema-cross a
                   chiudere il vincente; horizon = 2*max_bars (cap HARD_CAP) per dare
                   spazio alla cross. SL fisso resta SEMPRE.

ANTI-LOOK-AHEAD. prepare() precalcola ema_m = EMA(close, span=m) UNA volta: causale,
ema[k] dipende solo da close <= k. La decisione di uscita e' in after_bar(j), che per
contratto puo' leggere il bar j (close[j], ema_m[j]) ed e' eseguibile al close del
poll. levels(j) usa solo sl0/tp0 (costanti) -> nessun dato > j-1. OK per costruzione.

GRID: m in {5, 10, 20} x keep_tp in {True, False} (6 celle).
"""
import sys
from pathlib import Path

import numpy as np
import pandas as pd

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from exit_lab import ExitPolicy, evaluate, HARD_CAP  # noqa: E402


class EmaCrossExit(ExitPolicy):
    name = "ema_cross_exit"

    @classmethod
    def prepare(cls, ctx, **params):
        m = int(params.get("m", 10))
        key = f"ema_{m}"
        if key not in ctx:
            c = ctx["close"]
            ctx[key] = pd.Series(c).ewm(span=m, adjust=False).mean().values

    def __init__(self, ctx, i, d, entry, tp0, sl0, mb, **params):
        super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params)
        m = int(params.get("m", 10))
        self.ema = ctx[f"ema_{m}"]
        self.keep_tp = bool(params.get("keep_tp", True))
        if not self.keep_tp:
            # senza TP la cross deve avere spazio: raddoppia l'orizzonte (cap HARD_CAP)
            self.horizon = min(2 * mb, HARD_CAP)

    def levels(self, j: int):
        # SL fisso sempre; TP fisso solo se keep_tp (altrimenti None -> lo gestisce la cross)
        tp = self.tp0 if self.keep_tp else None
        return tp, self.sl0, 1.0

    def after_bar(self, j: int) -> bool:
        # after_bar puo' usare il bar j (close[j], ema[j]) -> eseguibile al close del poll
        e = self.ema[j]
        if not np.isfinite(e):
            return False
        c = self.ctx["close"][j]
        if self.d == 1:
            return c < e          # long: close ricade SOTTO la media -> reversione finita
        return c > e              # short: close risale SOPRA la media


GRID = [
    {"m": 5,  "keep_tp": True},
    {"m": 10, "keep_tp": True},
    {"m": 20, "keep_tp": True},
    {"m": 5,  "keep_tp": False},
    {"m": 10, "keep_tp": False},
    {"m": 20, "keep_tp": False},
]


if __name__ == "__main__":
    evaluate(EmaCrossExit, GRID)
