Problem Description
Solution
Here is a complete, PEP8-compliant Mean Reversion trading strategy using Bollinger Bands on the QuantConnect (Lean) platform.
Strategy Logic
This algorithm implements a classic Mean Reversion strategy:
- Long Entry: When the price closes below the Lower Bollinger Band (indicating the asset is oversold).
- Short Entry: When the price closes above the Upper Bollinger Band (indicating the asset is overbought).
- Exit: Positions are closed (liquidated) when the price returns to the Middle Band (the Moving Average), which represents the "mean."
Python Code
# region imports
from AlgorithmImports import *
# endregion
class BollingerMeanReversionStrategy(QCAlgorithm):
def initialize(self):
# 1. Set Strategy Settings
self.set_start_date(2020, 1, 1)
self.set_end_date(2023, 12, 31)
self.set_cash(100000) # Set Strategy Cash
# 2. Add Universe (Using SPY as the target asset)
self.ticker = "SPY"
self.equity = self.add_equity(self.ticker, Resolution.DAILY)
self.symbol = self.equity.symbol
# 3. Define Bollinger Bands Indicator
# Parameters: Period=20, Standard Deviation=2, Moving Average Type=Simple
self.bb = self.bb(self.symbol, 20, 2, MovingAverageType.SIMPLE, Resolution.DAILY)
# 4. Set WarmUp period to ensure indicators are ready immediately
self.set_warm_up(20)
def on_data(self, data: Slice):
# Ensure data exists and algorithm is done warming up
if self.is_warming_up or not self.bb.is_ready or self.symbol not in data:
return
# Get current price and indicator values
price = self.securities[self.symbol].close
upper_band = self.bb.upper_band.current.value
lower_band = self.bb.lower_band.current.value
middle_band = self.bb.middle_band.current.value
# Plotting for visual analysis
self.plot("Trade Plot", "Price", price)
self.plot("Trade Plot", "Upper Band", upper_band)
self.plot("Trade Plot", "Lower Band", lower_band)
self.plot("Trade Plot", "Middle Band", middle_band)
# --- Strategy Logic ---
# Check if we are currently invested
if not self.portfolio.invested:
# ENTRY LOGIC
# If price falls below lower band, buy (Long)
if price < lower_band:
self.set_holdings(self.symbol, 1.0)
self.debug(f"Buy Signal at {price} (Lower Band: {lower_band})")
# If price rises above upper band, sell short (Short)
elif price > upper_band:
self.set_holdings(self.symbol, -1.0)
self.debug(f"Short Signal at {price} (Upper Band: {upper_band})")
else:
# EXIT LOGIC
# If we are Long and price recovers to the mean (Middle Band)
if self.portfolio[self.symbol].is_long and price >= middle_band:
self.liquidate(self.symbol)
self.debug(f"Take Profit (Long) at {price} (Mean Reversion)")
# If we are Short and price falls back to the mean (Middle Band)
elif self.portfolio[self.symbol].is_short and price <= middle_band:
self.liquidate(self.symbol)
self.debug(f"Take Profit (Short) at {price} (Mean Reversion)")
Key Implementation Details
self.bb(...): This helper method creates the Bollinger Band indicator and automatically registers it to receive data updates.self.set_warm_up(20): This ensures the algorithm processes 20 bars of data before the start date so the indicator is ready to trade immediately on day 1.self.plot(...): This generates a custom chart in the backtest results, allowing you to visually verify if the trades align with the bands.- PEP8 Compliance: The code uses snake_case for method names (e.g.,
set_holdings,on_data) as per the latest QuantConnect API standards.
Q&A
Q: How can I change the sensitivity of the bands?
A: You can adjust the standard deviation parameter in the initialize method. Changing self.bb(self.symbol, 20, 2, ...) to self.bb(self.symbol, 20, 2.5, ...) will make the bands wider, resulting in fewer but potentially higher-quality signals.
Q: Can I use this for intraday trading?
A: Yes. Change Resolution.DAILY to Resolution.MINUTE or Resolution.HOUR in the add_equity and self.bb calls. Note that intraday data is noisier, so you might need to increase the period (e.g., from 20 to 50).
Q: How do I stop the strategy from Shorting?
A: To make this a "Long Only" strategy, simply remove the elif price > upper_band: block inside the Entry Logic and the elif self.portfolio[self.symbol].is_short block inside the Exit Logic.
Q: Why do we exit at the Middle Band instead of the opposite band?
A: Exiting at the Middle Band (Mean) increases the win rate (probability of profit) because the price touches the average more often than it swings from one extreme to the other. Exiting at the opposite band attempts to capture larger trends but often results in giving back profits if the trend reverses halfway.