"""DC01 — Donchian Channel Breakout con filtri. Trend-following classico: quando il prezzo rompe il massimo/minimo degli ultimi N periodi, entra nella direzione del breakout. Completamente diverso dallo squeeze (che usa Bollinger/Keltner). Donchian cattura breakout di RANGE, non di VOLATILITÀ. IN: - OHLCV DataFrame - Parametri: channel_period, volume_filter, atr_stop, trend_filter OUT: - Signal al breakout del canale Donchian - BacktestResult Logica: 1. Donchian upper = max(high, N periodi), lower = min(low, N periodi) 2. Close > upper → LONG (breakout rialzista) 3. Close < lower → SHORT (breakout ribassista) 4. Filtri: volume, trend EMA, ATR minimo """ from __future__ import annotations import sys sys.path.insert(0, ".") import numpy as np import pandas as pd from src.strategies.base import Strategy, Signal class DonchianBreakout(Strategy): name = "DC01_donchian" description = "Donchian Channel breakout — trend-following su range" default_assets = ["BTC", "ETH"] default_timeframes = ["15m", "1h"] fee_rt = 0.002 def generate_signals(self, df, ts, **params): c = df["close"].values h = df["high"].values l = df["low"].values v = df["volume"].values n = len(c) period = params.get("channel_period", 48) use_vol = params.get("vol_filter", False) use_trend = params.get("trend_filter", False) cooldown = params.get("cooldown", 6) # EMA per trend filter ema_50 = np.full(n, np.nan) k = 2 / 51 ema_50[49] = np.mean(c[:50]) for i in range(50, n): ema_50[i] = c[i] * k + ema_50[i - 1] * (1 - k) # Volume media vol_ma = np.full(n, np.nan) for i in range(20, n): vol_ma[i] = np.mean(v[i - 20:i]) signals = [] last_signal_idx = -cooldown for i in range(period + 1, n): if i - last_signal_idx < cooldown: continue upper = np.max(h[i - period:i]) lower = np.min(l[i - period:i]) direction = 0 if c[i] > upper: direction = 1 elif c[i] < lower: direction = -1 if direction == 0: continue # Trend filter: breakout must align with EMA trend if use_trend and not np.isnan(ema_50[i]): if direction == 1 and c[i] < ema_50[i]: continue if direction == -1 and c[i] > ema_50[i]: continue # Volume filter if use_vol and not np.isnan(vol_ma[i]): if v[i] < vol_ma[i] * 1.3: continue signals.append(Signal( idx=i, direction=direction, entry_price=c[i], metadata={"upper": float(upper), "lower": float(lower)}, )) last_signal_idx = i return signals if __name__ == "__main__": strategy = DonchianBreakout() configs = [ ("p=24", {"channel_period": 24}), ("p=48", {"channel_period": 48}), ("p=96", {"channel_period": 96}), ("p=48+trend", {"channel_period": 48, "trend_filter": True}), ("p=48+vol", {"channel_period": 48, "vol_filter": True}), ("p=48+t+v", {"channel_period": 48, "trend_filter": True, "vol_filter": True}), ("p=96+t+v", {"channel_period": 96, "trend_filter": True, "vol_filter": True}), ] all_results = [] for label, params in configs: for asset in ["BTC", "ETH"]: for tf in ["15m", "1h"]: for hold in [3, 6, 12]: r = strategy.backtest(asset, tf, hold=hold, **params) if r and r.trades >= 30: r.strategy_name = f"DC01 {label} h={hold}" all_results.append(r) all_results.sort(key=lambda r: r.accuracy, reverse=True) print(f"\n{'=' * 120}") print(f" DC01 DONCHIAN BREAKOUT — TOP 20") print(f"{'=' * 120}") for r in all_results[:20]: r.print_summary() if all_results: all_results[0].print_yearly()