Problem Description
Solution
Here is a complete, robust Mean Reversion trading strategy implemented in Python for the QuantConnect Lean engine.
Strategy Logic
This strategy utilizes Bollinger Bands to identify overbought and oversold conditions.
- Indicator: 20-period Simple Moving Average (SMA) with 2 Standard Deviation bands.
- Entry Long: When the price crosses below the Lower Bollinger Band (indicating the asset is oversold).
- Entry Short: When the price crosses above the Upper Bollinger Band (indicating the asset is overbought).
- Exit: When the price reverts to the Middle Band (the mean).
Python Code
# region imports
from AlgorithmImports import *
# endregion
class BollingerMeanReversion(QCAlgorithm):
def initialize(self):
# 1. Set Strategy Settings
self.set_start_date(2018, 1, 1)
self.set_end_date(2023, 1, 1)
self.set_cash(100000)
# 2. Add Security
# We use SPY (S&P 500 ETF) for this example
self.ticker = "SPY"
self.equity = self.add_equity(self.ticker, Resolution.DAILY)
self.symbol = self.equity.symbol
# 3. Define Parameters
self.period = 20
self.k = 2 # Standard Deviations
# 4. Initialize Indicators
# Create a Bollinger Band indicator
self.bb = self.bb(self.symbol, self.period, self.k, MovingAverageType.SIMPLE, Resolution.DAILY)
# 5. Set WarmUp
# Ensure indicators have enough data before the algorithm starts trading
self.set_warm_up(self.period)
def on_data(self, data: Slice):
# Check if the algorithm is warming up or if data is missing
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
middle_band = self.bb.middle_band.current.value
lower_band = self.bb.lower_band.current.value
# Current position quantity
holdings = self.portfolio[self.symbol].quantity
# --- Entry Logic ---
if holdings == 0:
# Long Condition: Price falls below the lower band (Oversold)
if price < lower_band:
self.set_holdings(self.symbol, 1.0)
self.plot("Trade Plot", "Price", price)
self.plot("Trade Plot", "Lower Band", lower_band)
self.debug(f"Buy Signal at {self.time}: Price {price} < Lower Band {lower_band}")
# Short Condition: Price rises above the upper band (Overbought)
elif price > upper_band:
self.set_holdings(self.symbol, -1.0)
self.plot("Trade Plot", "Price", price)
self.plot("Trade Plot", "Upper Band", upper_band)
self.debug(f"Sell Signal at {self.time}: Price {price} > Upper Band {upper_band}")
# --- Exit Logic ---
else:
# Exit Long: Price returns to the mean (Middle Band)
if holdings > 0 and price >= middle_band:
self.liquidate(self.symbol)
self.debug(f"Take Profit (Long) at {self.time}: Price {price} reached Mean {middle_band}")
# Exit Short: Price returns to the mean (Middle Band)
elif holdings < 0 and price <= middle_band:
self.liquidate(self.symbol)
self.debug(f"Take Profit (Short) at {self.time}: Price {price} reached Mean {middle_band}")
def on_end_of_algorithm(self):
self.log(f"Final Portfolio Value: {self.portfolio.total_portfolio_value}")
Key Implementation Details
initialize: Sets up the backtest timeframe, cash, and initializes the Bollinger Band indicator usingself.bb. We useself.set_warm_upto prevent the algorithm from trading before the indicator has calculated valid values.on_data: This is the event handler for market data. It checks if the indicators are ready, retrieves the current price, and compares it against the bands.set_holdings: Used to allocate portfolio weight.1.0means 100% long,-1.0means 100% short.liquidate: Closes all positions for the specific symbol immediately.
Q&A: Mean Reversion Strategies
Q: Why use Bollinger Bands for mean reversion?
A: Bollinger Bands dynamically adjust to market volatility. In high volatility, the bands widen, requiring a more significant price deviation to trigger a trade. In low volatility, they contract. This makes them more adaptive than static price thresholds.
Q: What are the risks of this strategy?
A: The primary risk is "momentum" or "trending" markets. If the price crosses the lower band and continues to crash (a strong downtrend), a mean reversion strategy will buy too early and suffer losses. To mitigate this, traders often add a Stop Loss or filter trades using a trend indicator (like ADX) to avoid trading against strong trends.
Q: Can I use this on minute resolution data?
A: Yes. You can change Resolution.DAILY to Resolution.MINUTE in the add_equity and bb indicator calls. However, lower timeframes often have more "noise," which may result in more false signals and higher transaction costs.
Q: How do I handle short selling restrictions?
A: The code above assumes a margin account that allows shorting. If you are trading in a cash account or an asset class that cannot be shorted easily (like some crypto spot pairs without margin), you should remove the elif price > upper_band: block in the Entry Logic.