Files
PythagorasGoal/scripts/strategies/MR02_donchian_fade.py
Adriano cff0d08fca feat(risk): filtro trend per alzare Acc e ridurre DD + modello portafoglio
Filtro opzionale trend_max/ema_long su tutte le fade (MR01/MR02/MR03/MR07):
salta i segnali quando |close-EMA200|/ATR supera la soglia (non fadare un trend
o crollo estremo). Con trend_max=3.0 (default in strategies.yml): accuratezza su
e DD giu' su 7/8 sleeve, drastico su ETH (MR01 71->26%, MR02 42->25%,
MR03 66->34%, MR07 46->21%); edge OOS confermato. MR03 BTC: filtro disattivo
(unico sleeve dove peggiora entrambe).

Scartate come non robuste: vol-target sizing e skip-alta-volatilita' (peggiorano
sia Acc che DD). Aggiunto modello di portafoglio equipesato su sotto-conti
indipendenti: DD aggregato ~14% full / ~10% OOS sul paniere di 8 sleeve, contro
20-70% del singolo -> vera leva anti-drawdown.

Banco di prova: scripts/analysis/risk_improvements.py, risk_portfolio.py.
Helper trend_distance() in fade_base. CLAUDE.md e diario aggiornati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:47:52 +02:00

83 lines
3.4 KiB
Python

"""MR02 — Donchian Fade (mean-reversion sugli estremi del canale).
L'opposto esatto del trend-following Donchian (che PERDE netto: vedi
scripts/analysis/strategy_research.py). Coerente con la lezione squeeze:
i breakout RIENTRANO, quindi si fada la rottura del canale verso il centro.
Logica:
1. Canale Donchian: massimo/minimo delle ultime n barre (escludendo la corrente)
2. ENTRY: close rompe SOPRA il massimo del canale -> SHORT (fade);
close rompe SOTTO il minimo -> LONG. Ingresso a close[i] (eseguibile).
3. EXIT: take-profit al centro del canale (il rientro atteso),
stop-loss a sl_atr*ATR oltre l'estremo, time-limit max_bars.
Validazione (netto, fee 0.10% RT reale Deribit, leva 3x, OOS = ultimo 30%):
BTC 1h n=20: +879% FULL / +171% OOS, DD 30%, 8/9 anni positivi
ETH 1h n=20: enorme FULL / +8452% OOS, DD 42%
Robusto su TUTTA la griglia n in {10,20,30,50} x sl_atr in {1.5,2.0,3.0}
(BTC+ETH 1h sempre positivo OOS) e su tutte le fee 0.00-0.20% RT.
Ricerca completa: scripts/analysis/strategy_research_v2.py.
"""
from __future__ import annotations
import sys
sys.path.insert(0, ".")
import numpy as np
import pandas as pd
from src.strategies.base import Signal
from src.strategies.fade_base import FadeStrategy, atr, trend_distance
class DonchianFade(FadeStrategy):
name = "MR02_donchian_fade"
description = "Mean-reversion: fada la rottura del canale Donchian, TP al centro"
default_assets = ["BTC", "ETH"]
default_timeframes = ["1h"]
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
**params) -> list[Signal]:
n = params.get("n", 20)
sl_atr = params.get("sl_atr", 2.0)
max_bars = params.get("max_bars", 24)
trend_max = params.get("trend_max") # None = filtro disattivo
ema_long = params.get("ema_long", 200)
h, l, c = df["high"].values, df["low"].values, df["close"].values
hh = pd.Series(h).rolling(n).max().shift(1).values
ll = pd.Series(l).rolling(n).min().shift(1).values
a = atr(df, 14)
td = trend_distance(df, ema_long) if trend_max is not None else None
signals: list[Signal] = []
for i in range(n + 14, len(c)):
if np.isnan(hh[i]) or np.isnan(a[i]):
continue
if td is not None and (np.isnan(td[i]) or td[i] > trend_max):
continue
mid = (hh[i] + ll[i]) / 2.0
if c[i] > hh[i] and c[i - 1] <= hh[i - 1]: # rottura rialzista -> fade short
d, sl = -1, c[i] + sl_atr * a[i]
elif c[i] < ll[i] and c[i - 1] >= ll[i - 1]: # rottura ribassista -> fade long
d, sl = 1, c[i] - sl_atr * a[i]
else:
continue
signals.append(Signal(
idx=i, direction=d, entry_price=c[i],
metadata={"tp": float(mid), "sl": float(sl), "max_bars": max_bars},
))
return signals
if __name__ == "__main__":
strat = DonchianFade()
print("=" * 110)
print(f" MR02 DONCHIAN FADE — netto fee {strat.fee_rt*100:.2f}% RT, leva {strat.leverage:.0f}x")
print("=" * 110)
for asset in ["BTC", "ETH"]:
r = strat.backtest(asset, "1h", n=20, sl_atr=2.0, max_bars=24)
if r:
r.strategy_name = f"MR02 {asset} 1h n20"
r.print_summary()
r.print_yearly()