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>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""EXIT-12 — partial_tp_trail: partial AL TP PIENO, poi il runner CORRE col trail.
|
||||
|
||||
Idea (diversa dal ladder 80/20 gia' SCARTATO, che metteva uno stop FISSO alla
|
||||
soglia 80% del TP): qui il partial avviene AL TP PIENO (tp0, alla media), esce una
|
||||
frazione q del trade, e il RESIDUO resta SENZA TP, protetto da un trailing
|
||||
chandelier k*ATR ancorato all'estremo favorevole raggiunto DOPO il partial; il
|
||||
floor dello stop e' tp0 -> il profitto al livello TP e' LOCKATO (il runner non
|
||||
puo' mai chiudere peggio di tp0). horizon esteso a 3*mb (cap HARD_CAP) per dare
|
||||
spazio al runner.
|
||||
|
||||
Fase 1 (pre-partial): exit standard = (tp0, sl0). Al tocco di tp0 esce q.
|
||||
Fase 2 (post-partial): TP rimosso. Trail sul residuo:
|
||||
Long : stop(j) = max( tp0, max(high[part..j-1]) - k*atr14[j-1] ) (solo sale)
|
||||
Short: stop(j) = min( tp0, min(low [part..j-1]) + k*atr14[j-1] ) (solo scende)
|
||||
floor = tp0 -> profitto lockato al livello TP, MAI peggio.
|
||||
|
||||
ANTI-LOOK-AHEAD: levels(j) usa SOLO dati <= j-1:
|
||||
- estremo favorevole post-partial sullo slice [part .. j-1] (mantenuto
|
||||
incrementalmente, aggiornato col bar j-1 prima di calcolare lo stop in j);
|
||||
- atr14[j-1] (indice causale).
|
||||
on_partial(j) registra solo l'indice del bar di partial (j) e il prezzo (tp0):
|
||||
l'estremo della fase 2 parte da high/low del bar di partial j (<= j, ma il primo
|
||||
bar valutato in fase 2 e' j+1, slice [j..j] noto al poll di j+1 -> causale).
|
||||
|
||||
GRID: q in {0.5, 0.7} x k in {2.0, 3.0} (4 celle). tp_frac=q.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from exit_lab import ExitPolicy, evaluate, HARD_CAP # noqa: E402
|
||||
|
||||
|
||||
class PartialTpTrail(ExitPolicy):
|
||||
name = "partial_tp_trail"
|
||||
|
||||
def __init__(self, ctx, i, d, entry, tp0, sl0, mb, q=0.5, k=3.0, **params):
|
||||
super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params)
|
||||
self.q = float(q)
|
||||
self.k = float(k)
|
||||
self.horizon = min(3 * mb, HARD_CAP)
|
||||
self.high = ctx["high"]
|
||||
self.low = ctx["low"]
|
||||
self.atr = ctx["atr14"]
|
||||
# stato fase 2 (post-partial)
|
||||
self.partial_idx = None # bar in cui e' avvenuto il partial (None = fase 1)
|
||||
self.fav_high = None # estremo favorevole sullo slice [partial..j-1]
|
||||
self.fav_low = None
|
||||
self._last_seen = None # ultimo indice incorporato nell'estremo
|
||||
self.cur_stop = None # stop trailing fase 2, floor=tp0, monotono
|
||||
|
||||
def levels(self, j):
|
||||
# ---- Fase 1: pre-partial -> exit standard (tp0, sl0), esce frazione q al TP
|
||||
if self.partial_idx is None:
|
||||
return self.tp0, self.sl0, self.q
|
||||
|
||||
# ---- Fase 2: post-partial -> TP rimosso, trail chandelier floor=tp0
|
||||
h, l, atr, d = self.high, self.low, self.atr, self.d
|
||||
# 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 -> resta sullo stop corrente
|
||||
return None, self.cur_stop, 1.0
|
||||
if 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
|
||||
self.cur_stop = cand
|
||||
return None, self.cur_stop, 1.0
|
||||
|
||||
def on_partial(self, j, price, remaining):
|
||||
# entra in fase 2: ancora l'estremo al bar di partial j (high/low[j] sono
|
||||
# noti al poll del bar j+1, primo bar valutato in fase 2). floor=tp0.
|
||||
self.partial_idx = j
|
||||
self.fav_high = self.high[j]
|
||||
self.fav_low = self.low[j]
|
||||
self._last_seen = j
|
||||
self.cur_stop = self.tp0 # profitto lockato al livello TP, MAI peggio
|
||||
|
||||
|
||||
GRID = [
|
||||
{"q": q, "k": k}
|
||||
for q in (0.5, 0.7)
|
||||
for k in (2.0, 3.0)
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
evaluate(PartialTpTrail, GRID)
|
||||
Reference in New Issue
Block a user