"""EXIT-11 — z_overshoot: TP OLTRE la media (overshoot di z_off deviazioni std). PRIOR (vincolante): le fade puntano alla MEDIA. Oggi il TP e' la SMA_n al close[i-1]. Ipotesi di questa famiglia: la reversione spesso NON si ferma esattamente alla media, ma la attraversa (overshoot). Spostando il TP un po' OLTRE la media nella direzione del trade catturiamo l'overshoot quando c'e', al prezzo di mancare alcuni TP che si fermano alla media (che poi rientrano o vengono presi da max_bars/SL). tp(j) = SMA_n[j-1] + d * z_off * STD_n[j-1] long (d=+1): si compra sotto la media -> TP = media + z_off*std (sopra la media: piu' lontano). short (d=-1): si vende sopra la media -> TP = media - z_off*std (sotto la media: piu' lontano). In ENTRAMBI i casi il TP si allontana di z_off*std OLTRE la media nella direzione del profitto -> target piu' ambizioso. NB: lo z e' un overshoot ADDIZIONALE rispetto alla media corrente; la media stessa si muove (come EXIT-10) quindi il target insegue la media + overshoot. CAP ONESTO (anti exit-in-perdita): se la media + overshoot finisce contro l'entry (media e' gia' scappata oltre entry contro di noi), il TP non deve diventare un'uscita in perdita mascherata. Floor a breakeven+fee come EXIT-10: long : tp(j) = max(tp(j), entry*(1+0.002)) short: tp(j) = min(tp(j), entry*(1-0.002)) SL FISSO (sl0) invariato; horizon = max_bars invariato. ANTI-LOOK-AHEAD: prepare() precalcola sma_n = rolling(n).mean() e std_n = rolling(n).std(ddof=0) su close (causali: ogni valore dipende solo da close <= indice). In levels(j) si legge sma_n[j-1] e std_n[j-1] -> solo dati <= j-1, mai il bar j. SL e horizon invariati. OK per costruzione. GRID: n in {20, 50} x z_off in {0.25, 0.5} (4 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 ZOvershoot(ExitPolicy): name = "z_overshoot" @classmethod def prepare(cls, ctx, **params): n = int(params.get("n", 50)) kmean = f"sma_{n}" kstd = f"std_{n}" if kmean not in ctx or kstd not in ctx: c = pd.Series(ctx["close"]) ctx[kmean] = c.rolling(n).mean().values ctx[kstd] = c.rolling(n).std(ddof=0).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.z_off = float(params.get("z_off", 0.5)) self.sma = ctx[f"sma_{n}"] self.std = ctx[f"std_{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] s = self.std[j - 1] if not (np.isfinite(m) and np.isfinite(s)): # warmup non pronto -> ricadi sul TP fisso d'entrata return self.tp0, self.sl0, 1.0 tp = m + self.d * self.z_off * s if self.d == 1: tp = max(tp, self.cap) else: tp = min(tp, self.cap) return tp, self.sl0, 1.0 GRID = [ {"n": 20, "z_off": 0.25}, {"n": 20, "z_off": 0.5}, {"n": 50, "z_off": 0.25}, {"n": 50, "z_off": 0.5}, ] if __name__ == "__main__": evaluate(ZOvershoot, GRID)