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