14522262e6
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
109 lines
4.4 KiB
Python
109 lines
4.4 KiB
Python
"""EXIT-18 — SL STRUTTURALE su swing low/high (structural / chandelier-by-structure).
|
|
|
|
Idea: invece di uno stop FISSO a sl0 (ATR dall'entrata), uno stop ancorato alla
|
|
STRUTTURA recente del prezzo. Per un long: il livello "naturale" sotto cui la tesi
|
|
mean-reversion e' invalidata e' il minimo dello swing recente; se il prezzo rompe
|
|
sotto il minimo degli ultimi n bar la fade ha torto. Simmetrico per lo short sul
|
|
massimo recente.
|
|
|
|
long (d=1): level(j) = min(low[j-n .. j-1]) - buf
|
|
short (d=-1): level(j) = max(high[j-n .. j-1]) + buf
|
|
buf = 0.25 * atr14[j-1]
|
|
|
|
SOLO-STRINGENTE: lo stop parte da sl0 (protezione iniziale invariata) e si aggiorna
|
|
SOLO se il livello swing e' PIU' PROTETTIVO (cricchetto):
|
|
long : cur_stop = max(cur_stop, level) (lo stop puo' solo SALIRE)
|
|
short: cur_stop = min(cur_stop, level) (lo stop puo' solo SCENDERE)
|
|
Cosi' man mano che il prezzo (per una fade vincente) torna verso la media, lo swing
|
|
low/high sale/scende e lo stop segue, bloccando profitto. Non si allenta MAI.
|
|
|
|
TP fisso (tp0) e horizon=max_bars invariati: questa famiglia cambia SOLO lo stop.
|
|
|
|
ANTI-LOOK-AHEAD (contratto): levels(j) usa SOLO dati con indice <= j-1:
|
|
- massimi/minimi sullo slice [j-n .. j-1] (lookback chiuso a j-1);
|
|
- buffer da atr14[j-1] (indicatore causale, valore del bar precedente).
|
|
Mantengo cur_stop in modo incrementale ma lo aggiorno con i bar fino a j-1, mai j.
|
|
Nessun dato del bar j o successivi entra nel livello attivo nel bar j.
|
|
|
|
MECCANISMO ATTESO: lo stop strutturale e' tipicamente PIU' LASCO di sl0 all'inizio
|
|
(il minimo recente puo' stare oltre sl0): in quel caso il cricchetto lo IGNORA e
|
|
resta a sl0 (non allentiamo mai). Quando invece la struttura si stringe (lo swing
|
|
low risale verso il prezzo dopo che la fade comincia a funzionare) lo stop SALE,
|
|
proteggendo i guadagni di un trade che poi rintraccia prima del TP.
|
|
Prior dal repo (ladder scartato): il TP della fade sta alla MEDIA, dove il movimento
|
|
e' esaurito -> oltre il TP non c'e' runner. Qui non tocchiamo il TP, quindi non
|
|
puntiamo a cavalcare oltre: cerchiamo SOLO di tagliare meglio i loser/ristagni
|
|
proteggendo profitto. RISCHIO (fade = mean-reversion): stringere lo stop su un
|
|
pullback transitorio stoppa fuori trade che sarebbero rientrati al TP -> winner
|
|
tagliati e stop-rate su. Lo misuriamo.
|
|
|
|
GRID: n in {5, 10, 20} (3 celle).
|
|
"""
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
from exit_lab import ExitPolicy, evaluate # noqa: E402
|
|
|
|
|
|
class SwingStop(ExitPolicy):
|
|
name = "swing_stop"
|
|
|
|
def __init__(self, ctx, i, d, entry, tp0, sl0, mb, n=10, **params):
|
|
super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params)
|
|
self.n = int(n)
|
|
self.low = ctx["low"]
|
|
self.high = ctx["high"]
|
|
self.atr14 = ctx["atr14"]
|
|
# stop a cricchetto: parte dalla protezione iniziale, puo' solo stringersi
|
|
self.cur_stop = sl0
|
|
# indice fino a cui ho gia' incorporato i bar nel cricchetto (escluso)
|
|
self._last_seen = i
|
|
|
|
def _swing_level(self, j: int):
|
|
"""Livello swing causale usando SOLO low/high su [j-n .. j-1] e atr14[j-1]."""
|
|
lo = max(self.i, j - self.n) # non andare prima dell'entrata
|
|
hi = j # slice [lo:hi] => indici <= j-1
|
|
if hi <= lo:
|
|
return None
|
|
a = self.atr14[j - 1]
|
|
if not np.isfinite(a):
|
|
a = 0.0
|
|
buf = 0.25 * a
|
|
if self.d == 1:
|
|
return float(np.min(self.low[lo:hi])) - buf
|
|
else:
|
|
return float(np.max(self.high[lo:hi])) + buf
|
|
|
|
def levels(self, j: int):
|
|
d = self.d
|
|
# aggiorna il cricchetto bar-per-bar fino a j-1 (causale). Per ogni bar k
|
|
# passato calcolo il livello swing attivo "a quel momento" e stringo lo stop.
|
|
while self._last_seen < j:
|
|
self._last_seen += 1
|
|
k = self._last_seen
|
|
lvl = self._swing_level(k)
|
|
if lvl is None:
|
|
continue
|
|
if d == 1:
|
|
if lvl > self.cur_stop: # cricchetto long: lo stop solo SALE
|
|
self.cur_stop = lvl
|
|
else:
|
|
if lvl < self.cur_stop: # cricchetto short: lo stop solo SCENDE
|
|
self.cur_stop = lvl
|
|
return self.tp0, self.cur_stop, 1.0
|
|
|
|
|
|
GRID = [
|
|
{"n": 5},
|
|
{"n": 10},
|
|
{"n": 20},
|
|
]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
evaluate(SwingStop, GRID)
|