← Back to Home
Filtered Squeeze Breakout Trading Strategy with Long-Term Trend and Volume Filters

Filtered Squeeze Breakout Trading Strategy with Long-Term Trend and Volume Filters

This article describes an advanced breakout trading strategy implemented in Backtrader that identifies low-volatility “squeeze” periods using Bollinger Bands and Keltner Channels, enhanced with a long-term trend filter and volume confirmation. The strategy trades breakouts with momentum confirmation and manages risk with ATR-based trailing stops.

Strategy Overview

The Filtered Squeeze Breakout Trading Strategy integrates the following components:

Code Implementation

Below is the complete Backtrader code for the strategy, including the custom Keltner Channel indicator:

import backtrader as bt

class CustomKeltnerChannel(bt.Indicator):
    alias = ('KeltnerChannel',)
    lines = ('mid', 'top', 'bot',)
    plotinfo = dict(subplot=False)
    params = (('period', 20), ('devfactor', 1.5), ('movav', bt.indicators.SimpleMovingAverage),)
    def __init__(self):
        self.lines.mid = self.p.movav(self.data, period=self.p.period)
        atr = self.p.devfactor * bt.indicators.AverageTrueRange(self.data, period=self.p.period)
        self.lines.top = self.lines.mid + atr
        self.lines.bot = self.lines.mid - atr

class FilteredSqueezeStrategy(bt.Strategy):
    """
    An advanced breakout strategy using a single timeframe. It combines a
    Squeeze, a long-term trend filter, and a volume confirmation filter.
    """
    params = (
        # Squeeze detection
        ('bb_period', 7), ('bb_devfactor', 1.0),
        ('kc_period', 30), ('kc_devfactor', 1.0),
        # Momentum confirmation
        ('macd_fast', 7), ('macd_slow', 30), ('macd_signal', 14),
        # Volume Filter
        ('vol_ma_period', 7),
        ('vol_multiplier', 1.2),
        # Long-term trend filter
        ('long_term_ma_period', 30),
        # Risk Management
        ('atr_period', 7), ('atr_stop_multiplier', 3.0),
    )

    def __init__(self):
        self.order = None
        
        # --- All indicators now run on self.data (datas[0]) ---
        self.bband = bt.indicators.BollingerBands(self.data, period=self.p.bb_period, devfactor=self.p.bb_devfactor)
        self.keltner = CustomKeltnerChannel(self.data, period=self.p.kc_period, devfactor=self.p.kc_devfactor)
        self.macd = bt.indicators.MACD(self.data, period_me1=self.p.macd_fast, period_me2=self.p.macd_slow, period_signal=self.p.macd_signal)
        self.atr = bt.indicators.AverageTrueRange(self.data, period=self.p.atr_period)
        self.vol_ma = bt.indicators.SimpleMovingAverage(self.data.volume, period=self.p.vol_ma_period)
        
        # Long-term MA to replace the HTF filter
        self.long_term_ma = bt.indicators.SimpleMovingAverage(self.data, period=self.p.long_term_ma_period)

        # --- Trailing Stop State ---
        self.stop_price = None
        self.highest_price_since_entry = None
        self.lowest_price_since_entry = None

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]: 
            return
        if order.status in [order.Completed]:
            if self.position and self.stop_price is None:
                if order.isbuy():
                    self.highest_price_since_entry = self.data.high[0]
                    self.stop_price = self.highest_price_since_entry - (self.atr[0] * self.p.atr_stop_multiplier)
                elif order.issell():
                    self.lowest_price_since_entry = self.data.low[0]
                    self.stop_price = self.lowest_price_since_entry + (self.atr[0] * self.p.atr_stop_multiplier)
            elif not self.position:
                self.stop_price = None
                self.highest_price_since_entry = None
                self.lowest_price_since_entry = None
        self.order = None

    def next(self):
        if self.order: 
            return

        # --- Filter Conditions ---
        is_squeeze = (self.bband.top < self.keltner.top and self.bband.bot > self.keltner.bot)
        has_volume_spike = self.data.volume[0] > (self.vol_ma[0] * self.p.vol_multiplier)
        
        # Trend filter using long-term MA
        is_long_term_uptrend = self.data.close[0] > self.long_term_ma[0]
        is_long_term_downtrend = self.data.close[0] < self.long_term_ma[0]

        if not self.position and is_squeeze and has_volume_spike:
            price_breaks_up = self.data.close[0] > self.bband.top[0]
            price_breaks_down = self.data.close[0] < self.bband.bot[0]
            macd_is_bullish = self.macd.macd[0] > self.macd.signal[0]
            macd_is_bearish = self.macd.macd[0] < self.macd.signal[0]

            # Entry requires alignment with the long-term MA
            if price_breaks_up and macd_is_bullish and is_long_term_uptrend:
                self.order = self.buy()
            elif price_breaks_down and macd_is_bearish and is_long_term_downtrend:
                self.order = self.sell()
                
        elif self.position:
            # --- Manual ATR Trailing Stop Logic ---
            if self.position.size > 0: # Long
                self.highest_price_since_entry = max(self.highest_price_since_entry, self.data.high[0])
                new_stop = self.highest_price_since_entry - (self.atr[0] * self.p.atr_stop_multiplier)
                self.stop_price = max(self.stop_price, new_stop)
                if self.data.close[0] < self.stop_price: 
                    self.order = self.close()
            elif self.position.size < 0: # Short
                self.lowest_price_since_entry = min(self.lowest_price_since_entry, self.data.low[0])
                new_stop = self.lowest_price_since_entry + (self.atr[0] * self.p.atr_stop_multiplier)
                self.stop_price = min(self.stop_price, new_stop)
                if self.data.close[0] > self.stop_price: 
                    self.order = self.close()

Strategy Explanation

1. CustomKeltnerChannel Indicator

The custom Keltner Channel indicator defines a volatility-based channel:

2. FilteredSqueezeStrategy

The strategy combines squeeze detection, trend alignment, volume confirmation, and momentum to trade breakouts:

Key Features

Pasted image 20250717122053.png Pasted image 20250717122107.png

Potential Improvements

This strategy is designed for markets with periodic low-volatility consolidations followed by strong, trend-aligned breakouts, suitable for assets like forex, stocks, or cryptocurrencies, and can be backtested to evaluate its effectiveness across various timeframes and assets.