ad65a0b344
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>
76 lines
3.0 KiB
Python
76 lines
3.0 KiB
Python
"""EXIT-01 — trail_atr_ride: TP RIMOSSO, cavalcata pura con SL trailing chandelier.
|
|
|
|
Idea: le fade mean-reversion escono oggi al TP fisso (alla media) + SL + max_bars.
|
|
Qui togliamo il TP e lasciamo correre il trade, proteggendolo con un SL trailing
|
|
"chandelier" a k*ATR dal massimo favorevole raggiunto. Lo stop puo' solo stringersi
|
|
(mai allargarsi). Orizzonte esteso (cap HARD_CAP=240) per dare spazio al runner.
|
|
|
|
Long: stop(j) = max( sl0, max(high[i..j-1]) - k*atr14[j-1] ) (sale, mai scende)
|
|
Short: stop(j) = min( sl0, min(low[i..j-1]) + k*atr14[j-1] ) (scende, mai sale)
|
|
|
|
ANTI-LOOK-AHEAD: levels(j) usa SOLO dati <= j-1:
|
|
- massimo/minimo favorevole sullo slice [i .. j-1] (mantenuto incrementalmente,
|
|
aggiornato col bar j-1 prima di calcolare lo stop attivo nel bar j);
|
|
- atr14[j-1] (indice causale).
|
|
Nessun TP -> nessun fill parziale. after_bar non usato (chiusura solo a orizzonte/SL).
|
|
|
|
GRID: k in {2.0, 3.0, 4.0} x horizon_mult in {2, 4} (6 celle). horizon = mult*mb cap 240.
|
|
"""
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
from exit_lab import ExitPolicy, evaluate, HARD_CAP
|
|
|
|
|
|
class TrailAtrRide(ExitPolicy):
|
|
name = "trail_atr_ride"
|
|
|
|
def __init__(self, ctx, i, d, entry, tp0, sl0, mb, k=3.0, horizon_mult=4, **params):
|
|
super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params)
|
|
self.k = float(k)
|
|
self.horizon = min(int(horizon_mult) * mb, HARD_CAP)
|
|
# estremo favorevole sullo slice [i..j-1]; inizializzato al bar di entrata i
|
|
# (il primo bar valutato e' j=i+1, dove lo slice [i..j-1]=[i..i] e' noto).
|
|
self.fav_high = ctx["high"][i]
|
|
self.fav_low = ctx["low"][i]
|
|
self._last_seen = i # ultimo indice gia' incorporato nell'estremo
|
|
# stop trailing monotono: parte da sl0 e puo' solo stringersi
|
|
self.cur_stop = sl0
|
|
|
|
def levels(self, j):
|
|
h = self.ctx["high"]
|
|
l = self.ctx["low"]
|
|
atr = self.ctx["atr14"]
|
|
# incorpora i bar fino a j-1 (dati causali, gia' chiusi al poll del bar j)
|
|
while self._last_seen < j - 1:
|
|
self._last_seen += 1
|
|
if h[self._last_seen] > self.fav_high:
|
|
self.fav_high = h[self._last_seen]
|
|
if l[self._last_seen] < self.fav_low:
|
|
self.fav_low = l[self._last_seen]
|
|
a = atr[j - 1]
|
|
if a != a: # NaN nei primi 14 bar -> resta sullo stop corrente
|
|
return None, self.cur_stop, 1.0
|
|
if self.d == 1:
|
|
cand = self.fav_high - self.k * a
|
|
if cand > self.cur_stop: # lo stop long puo' solo SALIRE (stringersi)
|
|
self.cur_stop = cand
|
|
else:
|
|
cand = self.fav_low + self.k * a
|
|
if cand < self.cur_stop: # lo stop short puo' solo SCENDERE (stringersi)
|
|
self.cur_stop = cand
|
|
return None, self.cur_stop, 1.0 # TP rimosso
|
|
|
|
|
|
GRID = [
|
|
{"k": k, "horizon_mult": m}
|
|
for k in (2.0, 3.0, 4.0)
|
|
for m in (2, 4)
|
|
]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
evaluate(TrailAtrRide, GRID)
|