The Bollinger Momentum Strategy is a sophisticated trading approach that combines Bollinger Bands with volume analysis and volatility-based position sizing to capture significant price movements in financial markets, particularly in volatile assets like cryptocurrencies. This article explores the strategy’s logic, reasoning, and implementation, focusing on its key components and code structure.
The strategy aims to identify high-probability trading opportunities by combining three key technical indicators:
The strategy employs trailing stops to manage risk and protect profits, and it adjusts position sizes inversely to recent volatility to optimize risk management.
The strategy enters trades when three conditions align:
Additionally, an alternative entry condition allows trades near the bands (within 70% of the band distance from the middle band) with volume and volatility confirmation, capturing early momentum moves.
Position sizing is dynamic, based on recent volatility measured by the ATR normalized by price. Higher volatility leads to smaller positions to reduce risk, while lower volatility allows larger positions. The position size scales between a minimum (25%) and maximum (95%) of available capital.
Trailing stops are implemented using the ATR multiplied by a factor (e.g., 2x ATR) to exit positions when the price reverses significantly, locking in profits or limiting losses.
Below are the main parts of the
BollingerMomentumStrategy
class, focusing on the parameters
and the next
function, which drives the trading logic.
import backtrader as bt
class BollingerMomentumStrategy(bt.Strategy):
= (
params 'bb_period', 7), # Bollinger Bands period
('bb_dev', 1.0), # Bollinger Bands standard deviation
('volume_period', 20), # Volume moving average period
('volume_multiplier', 1.5),# Volume spike threshold
('atr_period', 14), # ATR period
('atr_threshold', 0.02), # ATR threshold (2% of price)
('atr_multiplier', 2.0), # ATR multiplier for trailing stops
('vol_lookback', 20), # Volatility lookback for position sizing
('max_position_pct', 0.95),# Maximum position size
('min_position_pct', 0.25),# Minimum position size
('printlog', True),
(
)
def __init__(self):
self.dataclose = self.datas[0].close
self.datavolume = self.datas[0].volume
self.bb = bt.indicators.BollingerBands(self.dataclose, period=self.params.bb_period, devfactor=self.params.bb_dev)
self.volume_sma = bt.indicators.SMA(self.datavolume, period=self.params.volume_period)
self.atr = bt.indicators.ATR(period=self.params.atr_period)
self.order = None
self.trail_order = None
self.volatility_history = []
def next(self):
if self.order:
return
if len(self.atr) > 0 and self.dataclose[0] > 0:
= self.atr[0] / self.dataclose[0]
normalized_atr self.volatility_history.append(normalized_atr)
if len(self.volatility_history) > self.params.vol_lookback:
self.volatility_history = self.volatility_history[-self.params.vol_lookback:]
if self.position:
if not self.trail_order:
if self.position.size > 0:
self.log(f"Placing trailing stop for long position")
self.trail_order = self.sell(exectype=bt.Order.StopTrail, trailamount=self.atr[0] * self.params.atr_multiplier)
elif self.position.size < 0:
self.log(f"Placing trailing stop for short position")
self.trail_order = self.buy(exectype=bt.Order.StopTrail, trailamount=self.atr[0] * self.params.atr_multiplier)
return
if len(self) < max(self.params.bb_period, self.params.volume_period, self.params.atr_period):
return
= self.dataclose[0]
current_price = self.bb.top[0]
bb_upper = self.bb.bot[0]
bb_lower = self.bb.mid[0]
bb_mid = self.datavolume[0]
current_volume = self.volume_sma[0]
avg_volume = self.atr[0]
current_atr = current_price * self.params.atr_threshold
atr_threshold = current_volume > (avg_volume * self.params.volume_multiplier)
volume_spike = current_atr > atr_threshold
high_volatility = current_price > bb_upper
upper_breakout = current_price < bb_lower
lower_breakout = self.calculate_volatility_position_size()
position_size_pct
if upper_breakout and volume_spike and high_volatility and not self.position:
self.log(f"LONG SIGNAL: BB Upper Breakout with Volume & Volatility")
self.log(f"Price: {current_price:.2f} > BB Upper: {bb_upper:.2f}")
self.log(f"Volume: {current_volume:.0f} vs Avg: {avg_volume:.0f} (x{current_volume/avg_volume:.1f})")
self.log(f"ATR: {current_atr:.4f} vs Threshold: {atr_threshold:.4f}")
self.log(f"Position Size: {position_size_pct:.1%}")
self.cancel_trail()
= self.broker.getcash()
cash = cash * position_size_pct
target_value = target_value / current_price
shares self.order = self.buy(size=shares)
elif lower_breakout and volume_spike and high_volatility and not self.position:
self.log(f"SHORT SIGNAL: BB Lower Breakout with Volume & Volatility")
self.log(f"Price: {current_price:.2f} < BB Lower: {bb_lower:.2f}")
self.log(f"Volume: {current_volume:.0f} vs Avg: {avg_volume:.0f} (x{current_volume/avg_volume:.1f})")
self.log(f"ATR: {current_atr:.4f} vs Threshold: {atr_threshold:.4f}")
self.log(f"Position Size: {position_size_pct:.1%}")
self.cancel_trail()
= self.broker.getcash()
cash = cash * position_size_pct
target_value = target_value / current_price
shares self.order = self.sell(size=shares)
elif not self.position:
= current_price > (bb_mid + 0.7 * (bb_upper - bb_mid))
near_upper = current_price < (bb_mid - 0.7 * (bb_mid - bb_lower))
near_lower if near_upper and volume_spike and high_volatility:
self.log(f"LONG: Strong momentum near BB upper")
= self.broker.getcash()
cash = cash * (position_size_pct * 0.7)
target_value = target_value / current_price
shares self.order = self.buy(size=shares)
elif near_lower and volume_spike and high_volatility:
self.log(f"SHORT: Strong momentum near BB lower")
= self.broker.getcash()
cash = cash * (position_size_pct * 0.7)
target_value = target_value / current_price
shares self.order = self.sell(size=shares)
params
tuple defines
configurable settings, such as the Bollinger Bands period
(bb_period
), standard deviation factor
(bb_dev
), volume threshold
(volume_multiplier
), ATR period, and position sizing
limits. These allow flexibility in tuning the strategy.__init__
method
sets up indicators (Bollinger Bands, volume SMA, ATR) and initializes
variables for order tracking and volatility history.next
method is the
core decision-making loop, executed for each data point (e.g., daily
bar). It:
yfinance
to
fetch historical data (e.g., BTC-USD) and feeds it into Backtrader via a
PandasData
feed.The Bollinger Momentum Strategy combines technical indicators with dynamic risk management to trade volatile markets effectively. By leveraging Bollinger Band breakouts, volume spikes, and ATR-based volatility, it aims to capture significant price movements while controlling risk through adaptive position sizing and trailing stops. The provided code offers a robust framework for traders to test and refine this approach on assets like BTC-USD.