21d3ba609d
Trovate e promosse 3 strategie con edge netto distinto da MR01, stessa metodologia (ingresso close[i], netto fee 0.10% RT + leva 3x, OOS ultimo 30%, robustezza su griglia + sweep fee 0.00-0.20%): - MR02 Donchian Fade: fade rottura canale H/L, TP al centro. BTC +172% OOS. - MR03 Keltner Fade: canale ATR su EMA (indipendente da Bollinger). BTC +112%. - MR07 Return Reversal: fade movimento di barra estremo (z dei rendimenti). BTC +105%. Tutte positive netto OOS su entrambi gli asset e su tutto lo sweep fee, anche 0.20% RT pessimista (validate anche via oos_validation live-path). Scartate MR04 (= MR01 riparametrizzato), MR05 (ADX non robusto), MR06 (RSI2 ETH neg). Base condivisa fade_base.FadeStrategy (backtest intrabar TP/SL/max_bars). Aggiunte a strategy_loader e strategies.yml (BTC+ETH 1h). Ricerca in strategy_research_v2.py. Diario e CLAUDE.md aggiornati. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
78 lines
3.1 KiB
Python
78 lines
3.1 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
|
|
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
signals: list[Signal] = []
|
|
for i in range(n + 14, len(c)):
|
|
if np.isnan(hh[i]) or np.isnan(a[i]):
|
|
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()
|