"""EXIT-08 — SL che si STRINGE linearmente col tempo (time-based stop tighten). Lo stop iniziale sl0 si avvicina linearmente a un livello d'arrivo `target` man mano che il trade invecchia. TP fisso (tp0) e horizon=max_bars invariati. frac(j) = clamp( (j - i) / (mb * stretch), 0, 1 ) sl(j) = sl0 + frac(j) * (target - sl0) target ("arrivo"): - "entry" -> entry (a fine corsa lo stop e' a breakeven) - "midpoint" -> (entry + sl0)/2 (a fine corsa lo stop e' a meta' strada) Direzioni: il segno e' gestito dalla geometria di sl0 vs entry. long (d=1): sl0 < entry -> target >= sl0 -> lo stop SALE verso entry short (d=-1): sl0 > entry -> target <= sl0 -> lo stop SCENDE verso entry In entrambi i casi lo stop si stringe (avvicina al prezzo d'ingresso) col tempo. stretch: - 1.0 : lo stop raggiunge `target` a max_bars (fine horizon). - 2.0 : lo stop raggiunge `target` a 2*max_bars -> stringe a META' velocita', quindi a max_bars e' solo a meta' del cammino sl0->target (piu' lasco). ANTI-LOOK-AHEAD: sl(j) dipende SOLO da {i, j, mb, stretch, entry, sl0}, tutti noti all'ENTRATA (close[i]). Nessun dato del bar j o successivi entra nel livello attivo nel bar j. Il TP resta tp0. -> contratto rispettato per costruzione. MECCANISMO ATTESO: stringere lo stop col tempo taglia prima i trade che ristagnano (non vanno al TP ne' allo SL iniziale e scadrebbero a max_bars vicino al BE/in perdita). Rischio: stoppa fuori trade che sarebbero rientrati verso il TP (la fade e' mean-reversion: il movimento contrario e' spesso transitorio) -> puo' alzare lo stop-rate effettivo e tagliare winner ritardatari. GRID: stretch in {1.0, 2.0} x target in {entry, midpoint} (4 celle). """ import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from exit_lab import ExitPolicy, evaluate # noqa: E402 class SlTighten(ExitPolicy): name = "sl_tighten" def __init__(self, ctx, i, d, entry, tp0, sl0, mb, **params): super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params) self.stretch = float(params.get("stretch", 1.0)) self.target_kind = str(params.get("target", "entry")) if self.target_kind == "entry": self.target = entry elif self.target_kind == "midpoint": self.target = 0.5 * (entry + sl0) else: raise ValueError(f"target sconosciuto: {self.target_kind}") self.denom = max(self.mb * self.stretch, 1e-9) def levels(self, j: int): # frac dipende solo da i, j, mb, stretch -> noti all'entrata. No look-ahead. frac = (j - self.i) / self.denom if frac < 0.0: frac = 0.0 elif frac > 1.0: frac = 1.0 sl = self.sl0 + frac * (self.target - self.sl0) return self.tp0, sl, 1.0 GRID = [ {"stretch": 1.0, "target": "entry"}, {"stretch": 1.0, "target": "midpoint"}, {"stretch": 2.0, "target": "entry"}, {"stretch": 2.0, "target": "midpoint"}, ] if __name__ == "__main__": evaluate(SlTighten, GRID)