Files
PythagorasGoal/scripts/analysis/exit_policies/08_sl_tighten.py
T
Adriano Dal Pastro ad65a0b344 research(exit-lab): 34 agenti su exit dinamiche → EXIT-16 close-confirm SL PROMOSSO a livello PORT06
23 famiglie esplorate (harness condiviso exit_lab, train/OOS embargo nov-2023,
tutto lo storico 1h 2018-2026) + 10 verifiche avversariali + test PORT06.
'Cavalcare il prezzo' non esiste (4a conferma: oltre il TP=media non c'e' coda).
Scoperta: lo SL intrabar fisso e' il distruttore di valore n.1 delle fade
(stop da wick = falsi negativi). Forma robusta: SL solo su CHIUSURA oltre
sl0±0.5·ATR14 — PORT06 FULL Sharpe 6.47→7.84 DD 4.10→2.60, OOS 8.82→10.06.
Collaterali: bias gap-through dell'engine sugli stop stretti; ramo -2% del
worker morto con sl=0. Diario: docs/diary/2026-06-04-exit-lab.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 21:16:58 +00:00

78 lines
3.0 KiB
Python

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