Files
PythagorasGoal/Old/scripts/analysis/exit_policies/18_swing_stop.py
T
Adriano Dal Pastro 14522262e6 chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
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>
2026-06-19 15:20:59 +00:00

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)