Problem Description
Solution
Here is a complete, robust trading bot implemented in Python for the QuantConnect (Lean) platform.
Strategy Overview
This strategy utilizes Donchian Channels to identify and trade breakouts. It follows a classic Trend Following approach similar to the "Turtle Trading" system.
- Indicator: 20-Period Donchian Channel (High, Low, and Mean).
- Long Entry: When the price closes above the Upper Band (20-day High).
- Short Entry: When the price closes below the Lower Band (20-day Low).
- Exit Strategy:
- Exit Long: When price crosses below the Middle Band.
- Exit Short: When price crosses above the Middle Band.
- Note: Using the Middle Band for exits often preserves more profit than waiting for the opposite band to be hit.
QuantConnect Python Code
# region imports
from AlgorithmImports import *
# endregion
class DonchianChannelBreakout(QCAlgorithm):
def initialize(self):
"""
Initialize the algorithm settings, data subscriptions, and indicators.
"""
# 1. Set Strategy Settings
self.set_start_date(2018, 1, 1) # Backtest Start Date
self.set_end_date(2023, 1, 1) # Backtest End Date
self.set_cash(100000) # Initial Capital
# 2. Add Security
# We use SPY (S&P 500 ETF) for this example, but this works on Forex/Crypto too.
self.ticker = "SPY"
self.equity = self.add_equity(self.ticker, Resolution.DAILY)
self.symbol = self.equity.symbol
# Data normalization mode (Raw is often better for technical analysis on price,
# but Adjusted is standard for ETFs to account for dividends).
self.equity.set_data_normalization_mode(DataNormalizationMode.ADJUSTED)
# 3. Define Parameters
self.period = 20 # Lookback period for the Donchian Channel
# 4. Initialize Indicators
# DCH returns a DonchianChannel object containing UpperBand, LowerBand, and Current (Middle)
self.donchian = self.dch(self.symbol, self.period, Resolution.DAILY)
# 5. Set WarmUp
# Automatically process data before the start date to ready the indicators
self.set_warm_up(self.period)
# 6. Plotting
# Create a custom chart to visualize the bands vs price
stock_plot = Chart("Trade Plot")
stock_plot.add_series(Series("Price", SeriesType.LINE, 0))
stock_plot.add_series(Series("Upper Band", SeriesType.LINE, 0))
stock_plot.add_series(Series("Middle Band", SeriesType.LINE, 0))
stock_plot.add_series(Series("Lower Band", SeriesType.LINE, 0))
self.add_chart(stock_plot)
def on_data(self, data: Slice):
"""
Event handler for new data.
"""
# Ensure data exists for our symbol and indicators are ready
if not self.donchian.is_ready or self.symbol not in data:
return
# Get current price (Close price of the daily bar)
price = data[self.symbol].close
# Get Indicator Values
upper_band = self.donchian.upper_band.current.value
lower_band = self.donchian.lower_band.current.value
# The main .current.value of the DCH indicator is the Middle Band (Average of High and Low)
middle_band = self.donchian.current.value
# Plotting for visualization
self.plot("Trade Plot", "Price", price)
self.plot("Trade Plot", "Upper Band", upper_band)
self.plot("Trade Plot", "Middle Band", middle_band)
self.plot("Trade Plot", "Lower Band", lower_band)
# --- Trading Logic ---
# Check current holdings
holdings = self.portfolio[self.symbol].quantity
# 1. Entry Logic: Breakouts
if holdings == 0:
# Long Breakout: Price closes above the Upper Band
# Note: In a strict breakout, price > previous high.
# If price is currently pushing the band up, it is a breakout.
if price >= upper_band:
self.set_holdings(self.symbol, 1.0)
self.debug(f"LONG Breakout at {price} (Upper: {upper_band})")
# Short Breakout: Price closes below the Lower Band
elif price <= lower_band:
self.set_holdings(self.symbol, -1.0)
self.debug(f"SHORT Breakout at {price} (Lower: {lower_band})")
# 2. Exit Logic: Mean Reversion / Trailing Stop
# We use the Middle Band as a dynamic trailing stop.
# If Long, liquidate if price crosses below Middle Band
elif holdings > 0:
if price < middle_band:
self.liquidate(self.symbol)
self.debug(f"Exit LONG (Crossed Middle Band) at {price}")
# If Short, liquidate if price crosses above Middle Band
elif holdings < 0:
if price > middle_band:
self.liquidate(self.symbol)
self.debug(f"Exit SHORT (Crossed Middle Band) at {price}")
def on_end_of_algorithm(self):
self.debug(f"Final Portfolio Value: {self.portfolio.total_portfolio_value}")
Key Implementation Details
self.dch(...): This helper method creates the Donchian Channel indicator. In QuantConnect, the main value of theDonchianChannelobject is the mean (middle band). You access the top and bottom via.upper_bandand.lower_band.set_warm_up: This is crucial. It feeds historical data into the algorithm before theStartDateso that on day 1 of the simulation, the 20-day channel is already calculated and ready to trade.- Resolution.DAILY: Breakout strategies generally produce fewer false signals on higher timeframes (Daily or Weekly) compared to Minute data.
- Exit Logic: Instead of waiting for the price to hit the opposite band (which would require a massive reversal and loss of open profits), this code uses the Middle Band as a trailing stop. This locks in profits earlier while still allowing the trend to run.
Q&A: Donchian Channel Strategy
Q: How can I make the strategy more sensitive to price changes?
A: You can reduce the self.period variable (e.g., change it from 20 to 10). A shorter period makes the bands tighter, resulting in more frequent trades but potentially more "whipsaws" (false signals).
Q: Can I apply this to Bitcoin or Forex?
A: Yes. Simply change self.ticker in the initialize method.
- For Forex:
self.add_forex("EURUSD", Resolution.DAILY) - For Crypto:
self.add_crypto("BTCUSD", Resolution.DAILY)
Q: Why do we use set_holdings(..., 1.0)?
A: set_holdings calculates the number of shares required to allocate a specific percentage of your portfolio (1.0 = 100%). This automatically handles position sizing based on your current equity, making the code cleaner than calculating share counts manually.
Q: What is the difference between Raw and Adjusted data normalization?
A: DataNormalizationMode.ADJUSTED (default for equities in backtesting) accounts for stock splits and dividend payments by adjusting historical prices. This is essential for accurate P&L calculation in long-term backtests. RAW uses the actual price seen on the tape, which is better if your logic depends on specific psychological price levels (like $100.00).