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