🚀 AI One-Click Generation of quantconnect Strategy Code

How to Code a Donchian Channel Breakout Trading Strategy in Python with QuantConnect

quantconnect | Published on: 2025/12/11
The following content is generated by EasyQuant .

Problem Description

Please write a trading bot that uses Donchian Channels for breakouts.

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.

  1. Indicator: 20-Period Donchian Channel (High, Low, and Mean).
  2. Long Entry: When the price closes above the Upper Band (20-day High).
  3. Short Entry: When the price closes below the Lower Band (20-day Low).
  4. 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

  1. self.dch(...): This helper method creates the Donchian Channel indicator. In QuantConnect, the main value of the DonchianChannel object is the mean (middle band). You access the top and bottom via .upper_band and .lower_band.
  2. set_warm_up: This is crucial. It feeds historical data into the algorithm before the StartDate so that on day 1 of the simulation, the 20-day channel is already calculated and ready to trade.
  3. Resolution.DAILY: Breakout strategies generally produce fewer false signals on higher timeframes (Daily or Weekly) compared to Minute data.
  4. 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).