"""SB01 — Squeeze Breakout con Retest. Il problema di SQ01/SQ02: entri al breakout, ma molti breakout sono fakeout. Soluzione: aspetta il RETEST. Dopo il breakout, il prezzo spesso torna a testare il livello di breakout prima di continuare. Più selettivo di SQ02 → meno trade ma più accurati. Anti-overfitting: meccanismo strutturale (retest è fenomeno di mercato reale). IN: - OHLCV DataFrame - Parametri: bb_window, sq_threshold, retest_window (quante barre aspettare il retest), retest_tolerance (quanto può tornare indietro) OUT: - Signal al retest confermato (non al breakout iniziale) - BacktestResult Logica: 1. Rileva squeeze release (come SQ01) 2. NON entrare subito — segna direzione e livello di breakout 3. Nelle N barre successive, aspetta che il prezzo torni verso il livello 4. Se il prezzo torna nel range di tolleranza e poi rimbalza → ENTRA 5. Se il prezzo non torna → skip (momentum troppo forte, entry persa) 6. Se il prezzo sfonda il livello → fakeout confermato, skip """ 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 from src.strategies.indicators import keltner_ratio, detect_squeezes class SqueezeBreakoutRetest(Strategy): name = "SB01_squeeze_retest" description = "Squeeze breakout con retest — entra solo dopo pullback confermato" 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) bb_w = params.get("bb_window", 14) sq_thr = params.get("sq_threshold", 0.8) retest_window = params.get("retest_window", 8) retest_tol = params.get("retest_tolerance", 0.5) use_vol = params.get("vol_filter", False) kcr = keltner_ratio(c, h, l, bb_w) events = detect_squeezes(c, h, l, kcr, sq_thr) vol_ma = np.full(n, np.nan) for i in range(20, n): vol_ma[i] = np.mean(v[i - 20:i]) signals = [] for ev in events: brk_idx = ev["idx"] if brk_idx + retest_window + 3 >= n or brk_idx < 1: continue # Direzione breakout first_ret = (c[brk_idx] - c[brk_idx - 1]) / c[brk_idx - 1] if abs(first_ret) < 0.001: continue direction = 1 if first_ret > 0 else -1 breakout_level = c[brk_idx - 1] breakout_move = abs(first_ret) # Aspetta retest nelle prossime N barre retest_found = False retest_idx = -1 for j in range(brk_idx + 1, min(brk_idx + retest_window + 1, n)): if direction == 1: # Long: il prezzo deve tornare GIÙ verso breakout_level pullback = (h[brk_idx] - l[j]) / (h[brk_idx] - breakout_level) if h[brk_idx] > breakout_level else 0 if pullback >= retest_tol: # Tornato abbastanza — ora deve rimbalzare if c[j] > breakout_level: retest_found = True retest_idx = j break elif c[j] < breakout_level * 0.998: # Sfondato sotto → fakeout break else: # Short: il prezzo deve tornare SU verso breakout_level pullback = (h[j] - l[brk_idx]) / (breakout_level - l[brk_idx]) if breakout_level > l[brk_idx] else 0 if pullback >= retest_tol: if c[j] < breakout_level: retest_found = True retest_idx = j break elif c[j] > breakout_level * 1.002: break if not retest_found or retest_idx < 0: continue # Volume filter al retest if use_vol and not np.isnan(vol_ma[retest_idx]): if v[retest_idx] < vol_ma[retest_idx] * 0.8: continue signals.append(Signal( idx=retest_idx, direction=direction, entry_price=c[retest_idx], metadata={ "breakout_idx": brk_idx, "retest_bars": retest_idx - brk_idx, "breakout_move": round(breakout_move * 100, 3), }, )) return signals if __name__ == "__main__": strategy = SqueezeBreakoutRetest() configs = [ ("rt8 tol50%", {"retest_window": 8, "retest_tolerance": 0.5}), ("rt6 tol50%", {"retest_window": 6, "retest_tolerance": 0.5}), ("rt10 tol50%", {"retest_window": 10, "retest_tolerance": 0.5}), ("rt8 tol30%", {"retest_window": 8, "retest_tolerance": 0.3}), ("rt8 tol70%", {"retest_window": 8, "retest_tolerance": 0.7}), ("rt8 tol50%+vol", {"retest_window": 8, "retest_tolerance": 0.5, "vol_filter": True}), ("rt6 tol30%", {"retest_window": 6, "retest_tolerance": 0.3}), ("rt12 tol50%", {"retest_window": 12, "retest_tolerance": 0.5}), ] all_results = [] for label, params in configs: for asset in ["BTC", "ETH"]: for tf in ["15m", "1h"]: for hold in [3, 6]: r = strategy.backtest(asset, tf, hold=hold, **params) if r and r.trades >= 30: r.strategy_name = f"SB01 {label} h={hold}" all_results.append(r) all_results.sort(key=lambda r: r.accuracy, reverse=True) print(f"\n{'=' * 130}") print(f" SB01 SQUEEZE BREAKOUT RETEST — TOP 25") print(f"{'=' * 130}") for r in all_results[:25]: r.print_summary() if all_results: all_results[0].print_yearly() # Confronto con benchmark print(f"\n BENCHMARK SQ02: 79.7% acc, 1250 trades, DD 6.5%, 9/9 anni")