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