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>
83 lines
3.1 KiB
Python
83 lines
3.1 KiB
Python
"""MR03 — Keltner Fade (mean-reversion sul canale ATR).
|
|
|
|
Stessa tesi di MR01 (i breakout rientrano) ma con banda costruita su ATR
|
|
attorno a una EMA, invece che su deviazione standard attorno a una SMA.
|
|
Reagisce diversamente a gap e code: edge indipendente, non ridondante con MR01.
|
|
|
|
Logica:
|
|
1. Canale di Keltner: EMA(n) +/- k*ATR(n)
|
|
2. ENTRY: close esce sotto la banda inferiore -> LONG (o sopra la superiore -> SHORT)
|
|
Ingresso a close[i] (eseguibile dal vivo, nessun look-ahead).
|
|
3. EXIT: take-profit alla EMA centrale (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=30 k=2.0: +112% OOS, DD 20%
|
|
ETH 1h n=50 k=1.5: +1426% OOS, DD 20%
|
|
Robusto su TUTTA la griglia n in {14,20,30,50} x k in {1.5,2.0,2.5}
|
|
(BTC+ETH 1h sempre positivo OOS).
|
|
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 KeltnerFade(FadeStrategy):
|
|
name = "MR03_keltner_fade"
|
|
description = "Mean-reversion: fada il canale di Keltner (ATR), TP alla EMA"
|
|
default_assets = ["BTC", "ETH"]
|
|
default_timeframes = ["1h"]
|
|
|
|
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
|
**params) -> list[Signal]:
|
|
n = params.get("n", 30)
|
|
k = params.get("k", 2.0)
|
|
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)
|
|
|
|
c = df["close"].values
|
|
e = pd.Series(c).ewm(span=n, adjust=False).mean().values
|
|
a = atr(df, n)
|
|
up, lo = e + k * a, e - k * a
|
|
td = trend_distance(df, ema_long) if trend_max is not None else None
|
|
|
|
signals: list[Signal] = []
|
|
for i in range(n + 1, len(c)):
|
|
if np.isnan(up[i]) or np.isnan(a[i]):
|
|
continue
|
|
if td is not None and (np.isnan(td[i]) or td[i] > trend_max):
|
|
continue
|
|
if c[i] < lo[i] and c[i - 1] >= lo[i - 1]:
|
|
d, sl = 1, c[i] - sl_atr * a[i]
|
|
elif c[i] > up[i] and c[i - 1] <= up[i - 1]:
|
|
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(e[i]), "sl": float(sl), "max_bars": max_bars},
|
|
))
|
|
return signals
|
|
|
|
|
|
if __name__ == "__main__":
|
|
strat = KeltnerFade()
|
|
print("=" * 110)
|
|
print(f" MR03 KELTNER FADE — netto fee {strat.fee_rt*100:.2f}% RT, leva {strat.leverage:.0f}x")
|
|
print("=" * 110)
|
|
for asset, n, k in [("BTC", 30, 2.0), ("ETH", 50, 1.5)]:
|
|
r = strat.backtest(asset, "1h", n=n, k=k, sl_atr=2.0, max_bars=24)
|
|
if r:
|
|
r.strategy_name = f"MR03 {asset} 1h n{n} k{k}"
|
|
r.print_summary()
|
|
r.print_yearly()
|