"""IB01 — Inside Bar Breakout. Pattern di compressione a singola candela: quando una barra ha high < prev high E low > prev low, il prezzo si sta comprimendo. Al breakout del range della inside bar, segui la direzione. 17% delle candele 15m sono inside bars → frequenza altissima. IN: - OHLCV DataFrame - Parametri: min_consecutive (N inside bars consecutivi), volume_filter, breakout_confirm OUT: - Signal al breakout del range dell'inside bar - BacktestResult Logica: 1. Identifica N inside bars consecutivi (compressione) 2. Quando il prezzo rompe il range → entra nella direzione del breakout 3. Filtro: volume al breakout > media 4. Hold fisso """ 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 InsideBarBreakout(Strategy): name = "IB01_inside_bar" description = "Inside bar breakout — compressione a singola candela" 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) min_consec = params.get("min_consecutive", 2) use_vol = params.get("vol_filter", False) min_range_pct = params.get("min_range_pct", 0.002) # 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 = [] consec = 0 mother_high = 0.0 mother_low = 0.0 for i in range(1, n - 1): is_inside = h[i] <= h[i - 1] and l[i] >= l[i - 1] if is_inside: if consec == 0: mother_high = h[i - 1] mother_low = l[i - 1] consec += 1 else: if consec >= min_consec: range_pct = (mother_high - mother_low) / mother_low if mother_low > 0 else 0 if range_pct < min_range_pct: consec = 0 continue # Breakout detection sulla barra corrente if c[i] > mother_high: direction = 1 elif c[i] < mother_low: direction = -1 else: consec = 0 continue # Volume filter if use_vol and not np.isnan(vol_ma[i]): if v[i] < vol_ma[i] * 1.2: consec = 0 continue signals.append(Signal( idx=i, direction=direction, entry_price=c[i], metadata={"consec": consec, "range_pct": round(range_pct * 100, 3)}, )) consec = 0 return signals if __name__ == "__main__": strategy = InsideBarBreakout() configs = [ ("2ib", {"min_consecutive": 2}), ("3ib", {"min_consecutive": 3}), ("4ib", {"min_consecutive": 4}), ("2ib+vol", {"min_consecutive": 2, "vol_filter": True}), ("3ib+vol", {"min_consecutive": 3, "vol_filter": True}), ("2ib r>0.3%", {"min_consecutive": 2, "min_range_pct": 0.003}), ("3ib r>0.3%", {"min_consecutive": 3, "min_range_pct": 0.003}), ] 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"IB01 {label} h={hold}" all_results.append(r) all_results.sort(key=lambda r: r.accuracy, reverse=True) print(f"\n{'=' * 120}") print(f" IB01 INSIDE BAR BREAKOUT — TOP 20") print(f"{'=' * 120}") for r in all_results[:20]: r.print_summary() if all_results: all_results[0].print_yearly()