Files
PythagorasGoal/scripts/analysis/exit_policies/20_ema_cross_exit.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

91 lines
3.7 KiB
Python

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