""" Test Bollinger Band Width Filter on 15-Minute Strategy ------------------------------------------------------ Goal: Avoid trading during "Squeezes" (low volatility) to reduce whipsaws. Hypothesis: Only entering when Band Width > Threshold (expansion) will improve performance. """ import pandas as pd import numpy as np import sys import os from typing import Dict, Any sys.path.append(os.path.dirname(os.path.abspath(__file__))) import config from core.backtester import Backtester DATA_FILE = os.path.join(config.DATA_DIR, "btc_15min_binance.csv") DF_15M = pd.read_csv(DATA_FILE) DF_15M['timestamp'] = pd.to_datetime(DF_15M['timestamp']) DF_15M.set_index('timestamp', inplace=True) def calculate_bb_width(df, period=20, std_dev=2.0): """Calculate Bollinger Band Width.""" df = df.copy() sma = df['close'].rolling(period).mean() std = df['close'].rolling(period).std() upper = sma + (std * std_dev) lower = sma - (std * std_dev) # Width normalized by price level df['bb_width'] = (upper - lower) / sma return df['bb_width'] def run_strategy_with_bb_width(width_threshold): df = DF_15M.copy() # Indicators (MA 10/100 Baseline) df['fast'] = df['close'].ewm(span=10).mean() df['slow'] = df['close'].ewm(span=100).mean() df['bb_width'] = calculate_bb_width(df) # ATR for Stops tr = (df['high'] - df['low']).rolling(14).mean() df['atr'] = tr # Signals # Only signal if Crossover AND BB Width > Threshold signals = pd.Series(0, index=df.index) long_cond = (df['fast'] > df['slow']) & (df['bb_width'] > width_threshold) short_cond = (df['fast'] < df['slow']) & (df['bb_width'] > width_threshold) signals[long_cond] = 1 signals[short_cond] = -1 # Stops (Same as Baseline) avg_atr_pct = (df['atr'] / df['close']).mean() if pd.isna(avg_atr_pct): avg_atr_pct = 0.005 stop_dist = avg_atr_pct * 3.0 class DummySignal: def get_params(self): return {"model": f"BBW_{width_threshold}"} backtester = Backtester( data=df, features=df, signals=signals, signal_model=DummySignal(), e_base=1000.0, leverage=2.0, risk_fraction=0.02, stop_distance=stop_dist, tp_distance=stop_dist * 3.0, d_max=0.40, commission=0.0, slippage=0.0001, verbose=False ) results = backtester.run() m = results['metrics'] total_days = (df.index[-1] - df.index[0]).days trades_per_day = m['num_trades'] / total_days if total_days > 0 else 0 return { 'threshold': width_threshold, 'return': m['total_return_pct'], 'trades': m['num_trades'], 'trades_per_day': trades_per_day, 'win_rate': m['win_rate'], 'sharpe': m['sharpe_ratio'], 'drawdown': m['max_drawdown_pct'] } def test_bb_width(): print("\n" + "="*70) print("TESTING BOLLINGER BAND WIDTH FILTER (15-Minute)") print("="*70) print("Baseline: MA 10/100 | Stop 3.0ATR | 2x Leverage") print("-" * 70) print(f"{'BB Width >':<15} {'Return':<10} {'Trades':<10} {'Win Rate':<10} {'Drawdown':<10} {'Sharpe':<10}") print("-" * 70) # Width usually ranges from 0.005 (squeeze) to 0.05+ (expansion) on 15m thresholds = [0.0, 0.005, 0.01, 0.015, 0.02] for t in thresholds: r = run_strategy_with_bb_width(t) print(f"{t:<15} {r['return']:>8.2f}% {r['trades']:>9} {r['win_rate']:>9.1f}% {r['drawdown']:>9.1f}% {r['sharpe']:>9.2f}") print("-" * 70) if __name__ == "__main__": test_bb_width()