"""EXIT-10 — tp_moving_mean: il TP INSEGUE la media corrente (non quello fisso).

PRIOR (vincolante): le fade puntano alla MEDIA. Oggi il TP e' CONGELATO all'entrata
(tp0 = SMA_n(close) al close[i-1]). Ma la media SI MUOVE durante il trade: se il
prezzo ci mette qualche barra a rientrare, la media si e' gia' spostata. Idea:
ridefinire il TP come la SMA_n corrente -> tp(j) = SMA_n(close)[j-1]. Il target
"segue" la media verso cui la fade punta, invece di mirare a una media stantia.

  long  (d=+1): si compra sotto la media -> tp(j) = SMA_n[j-1] (sopra entry, di norma).
  short (d=-1): si vende sopra la media  -> tp(j) = SMA_n[j-1] (sotto entry, di norma).

CAP ONESTO (anti exit-in-perdita): se la media si muove CONTRO (long: media scende
sotto entry; short: media sale sopra entry), il TP diventerebbe un'uscita in perdita
mascherata. Lo evitiamo:
    long : tp(j) = max(tp(j), entry*(1+0.002))
    short: tp(j) = min(tp(j), entry*(1-0.002))
0.002 (~ 0.10% RT fee + margine) garantisce che il tocco del TP sia >= breakeven.

SL FISSO (sl0) invariato; horizon = max_bars invariato.

ANTI-LOOK-AHEAD: prepare() precalcola sma_n = rolling(n).mean() su close (causale,
ogni valore dipende solo da close <= quel-indice). In levels(j) si legge sma_n[j-1]
-> solo dati <= j-1, mai il bar j. SL e horizon invariati. OK per costruzione.

GRID: n in {20, 50, 100} (3 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  # noqa: E402


class TpMovingMean(ExitPolicy):
    name = "tp_moving_mean"

    @classmethod
    def prepare(cls, ctx, **params):
        n = int(params.get("n", 50))
        key = f"sma_{n}"
        if key not in ctx:
            c = ctx["close"]
            ctx[key] = pd.Series(c).rolling(n).mean().values

    def __init__(self, ctx, i, d, entry, tp0, sl0, mb, **params):
        super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params)
        n = int(params.get("n", 50))
        self.sma = ctx[f"sma_{n}"]
        # cap onesto: il TP non puo' diventare un'uscita in perdita
        if d == 1:
            self.cap = entry * (1.0 + 0.002)
        else:
            self.cap = entry * (1.0 - 0.002)

    def levels(self, j: int):
        # SOLO dati <= j-1
        m = self.sma[j - 1]
        if not np.isfinite(m):
            # warmup della SMA non ancora pronto -> ricadi sul TP fisso d'entrata
            return self.tp0, self.sl0, 1.0
        if self.d == 1:
            tp = max(m, self.cap)
        else:
            tp = min(m, self.cap)
        return tp, self.sl0, 1.0


GRID = [
    {"n": 20},
    {"n": 50},
    {"n": 100},
]


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