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,90 @@
|
||||
"""EXIT-20 — ema_cross_exit: esci quando il close attraversa contro-posizione la EMA_m.
|
||||
|
||||
IDEA. Le fade comprano sotto / vendono sopra la media e oggi escono al TP fisso (la
|
||||
media congelata all'entrata) o a max_bars/SL. Qui usiamo la EMA_m come "linea della
|
||||
media corrente": finche' il prezzo non l'ha attraversata, la reversione e' ancora in
|
||||
corso; quando il close la attraversa CONTRO la posizione, la mean-reversion ha
|
||||
esaurito il suo percorso -> si chiude al close.
|
||||
|
||||
long (d=+1): la fade ha comprato sotto la media; esce quando close[j] < ema_m[j]
|
||||
(il prezzo e' risalito ABOVE la media e ora la ri-perfora al ribasso,
|
||||
ovvero il close torna sotto la media -> reversione finita/overshoot).
|
||||
short (d=-1): esce quando close[j] > ema_m[j].
|
||||
|
||||
NB sul segno. La condizione "long: close < ema" e' la cross CONTRO la posizione: la
|
||||
fade long scommette sul rientro VERSO/OLTRE la media; quando il close ricade sotto la
|
||||
EMA dopo che la reversione e' avvenuta, il segnale di reversione e' consumato. E' un
|
||||
exit "alla media mobile" che insegue la media invece del target congelato.
|
||||
|
||||
VARIANTI keep_tp:
|
||||
- keep_tp=True : il TP fisso tp0 RESTA attivo (si esce al primo fra TP-al-livello,
|
||||
SL, ema-cross, max_bars). horizon = max_bars (invariato).
|
||||
- keep_tp=False: si RIMUOVE il TP fisso (tp=None) e si lascia che sia la ema-cross a
|
||||
chiudere il vincente; horizon = 2*max_bars (cap HARD_CAP) per dare
|
||||
spazio alla cross. SL fisso resta SEMPRE.
|
||||
|
||||
ANTI-LOOK-AHEAD. prepare() precalcola ema_m = EMA(close, span=m) UNA volta: causale,
|
||||
ema[k] dipende solo da close <= k. La decisione di uscita e' in after_bar(j), che per
|
||||
contratto puo' leggere il bar j (close[j], ema_m[j]) ed e' eseguibile al close del
|
||||
poll. levels(j) usa solo sl0/tp0 (costanti) -> nessun dato > j-1. OK per costruzione.
|
||||
|
||||
GRID: m in {5, 10, 20} x keep_tp in {True, False} (6 celle).
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from exit_lab import ExitPolicy, evaluate, HARD_CAP # noqa: E402
|
||||
|
||||
|
||||
class EmaCrossExit(ExitPolicy):
|
||||
name = "ema_cross_exit"
|
||||
|
||||
@classmethod
|
||||
def prepare(cls, ctx, **params):
|
||||
m = int(params.get("m", 10))
|
||||
key = f"ema_{m}"
|
||||
if key not in ctx:
|
||||
c = ctx["close"]
|
||||
ctx[key] = pd.Series(c).ewm(span=m, adjust=False).mean().values
|
||||
|
||||
def __init__(self, ctx, i, d, entry, tp0, sl0, mb, **params):
|
||||
super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params)
|
||||
m = int(params.get("m", 10))
|
||||
self.ema = ctx[f"ema_{m}"]
|
||||
self.keep_tp = bool(params.get("keep_tp", True))
|
||||
if not self.keep_tp:
|
||||
# senza TP la cross deve avere spazio: raddoppia l'orizzonte (cap HARD_CAP)
|
||||
self.horizon = min(2 * mb, HARD_CAP)
|
||||
|
||||
def levels(self, j: int):
|
||||
# SL fisso sempre; TP fisso solo se keep_tp (altrimenti None -> lo gestisce la cross)
|
||||
tp = self.tp0 if self.keep_tp else None
|
||||
return tp, self.sl0, 1.0
|
||||
|
||||
def after_bar(self, j: int) -> bool:
|
||||
# after_bar puo' usare il bar j (close[j], ema[j]) -> eseguibile al close del poll
|
||||
e = self.ema[j]
|
||||
if not np.isfinite(e):
|
||||
return False
|
||||
c = self.ctx["close"][j]
|
||||
if self.d == 1:
|
||||
return c < e # long: close ricade SOTTO la media -> reversione finita
|
||||
return c > e # short: close risale SOPRA la media
|
||||
|
||||
|
||||
GRID = [
|
||||
{"m": 5, "keep_tp": True},
|
||||
{"m": 10, "keep_tp": True},
|
||||
{"m": 20, "keep_tp": True},
|
||||
{"m": 5, "keep_tp": False},
|
||||
{"m": 10, "keep_tp": False},
|
||||
{"m": 20, "keep_tp": False},
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
evaluate(EmaCrossExit, GRID)
|
||||
Reference in New Issue
Block a user