intraday-btc-model-11yr / test_bb_width.py
VenuMuvva's picture
Upload folder using huggingface_hub
40d4cd9 verified
Raw History Blame Contribute Delete
3.77 kB
"""
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()