Donchian Channel Breakout EA: Code Dynamic Trailing Stop

Build a Donchian Channel breakout EA in MQL5 with an adaptive ATR-based trailing stop. Full code, input tables, backtest tips, and real gotchas from live.

donchian-channel-breakout-ea-code-dynamic

Why Donchian Channels Still Matter in 2025

I've coded a lot of breakout systems over the years — Keltner, Bollinger, even some weird adaptive stuff I'd rather forget. But when I need something that just works on trending markets, I keep coming back to Donchian Channels. They're brutally simple: take the highest high and lowest low over N periods, draw two lines, and trade the break. No smoothing, no weighting, no voodoo.

Most traders overlook the real power here. Donchian doesn't try to predict — it reacts. That lag is actually a feature, not a bug, because it keeps you in trends until they genuinely exhaust. Richard Donchian figured this out in the 1960s, and it's still the backbone of many institutional trend-following systems. I've seen hedge funds run variants of this on everything from currency futures to cocoa.

In this post, I'll walk you through building a complete Donchian Channel breakout EA in MQL5, with a dynamic trailing stop that adapts to volatility. You'll get the full code structure, input parameter tables, and the gotchas I've hit in live trading. If you're expecting a "copy-paste and get rich" script, you'll be disappointed. If you want something you can actually trust in the Strategy Tester and forward test, keep reading.

What Makes a Donchian Breakout Strategy Tick

The core logic is dead simple: price breaks above the upper channel = go long. Price breaks below the lower channel = go short. But the devil is in the details — specifically, how you confirm the breakout, where you place the stop, and how you trail it.

A typical Donchian Channel has three lines:

  • Upper Channel — highest high of the last N bars
  • Lower Channel — lowest low of the last N bars
  • Middle Line — average of upper and lower (optional, often ignored)

Most traders set N to 20 for daily charts, but I've seen everything from 10 to 50 work depending on the instrument. The key insight: Donchian works best when the market has directional bias. In sideways chop, you'll get whipsawed to death. That's why we'll add a trend filter later.

One thing that trips up newcomers: Donchian Channels repaint by definition, because the highest high and lowest low shift as new bars form. This isn't a bug — it's how the channel adapts. But it means you should never base an entry on the current bar's channel values alone. Always check that the breakout happened on a completed bar, or at least use the previous bar's close as I do in the code below.

The Dynamic Trailing Stop Problem

Standard trailing stops are static — you set a fixed distance in points or pips, and the stop moves up as price moves in your favor. That's fine for ranging markets, but in strong trends, a fixed trailing stop is either too tight (gets you stopped out early) or too loose (gives back most of the profit). I've watched traders give back 80% of a 200-pip move because their 50-pip trail was way too narrow for that week's volatility.

Dynamic trailing means the stop distance adapts to recent volatility. When volatility expands, the trail widens; when it contracts, the trail tightens. I use the Average True Range (ATR) for this because it's simple, standard, and available in MQL5 out of the box. No external libraries, no custom indicators — just iATR().

Building the EA: MQL5 Implementation Step by Step

Let's get into the code. I'll assume you have basic MQL5 familiarity — you know how to create an Expert Advisor, compile it, and run it in the Strategy Tester. If not, MetaQuotes has decent documentation on the basics. Open MetaEditor, create a new Expert Advisor, and paste the snippets as we go. I'll structure this as a single-file EA for simplicity, but you can split it into include files if you're managing multiple strategies.

Step 1: Input Parameters

Every good EA starts with clean, well-documented inputs. Here's what we'll expose to the user. These go at the top of the file, outside any functions:

ParameterTypeDefaultDescription
DonchianPeriodint20Number of bars for channel calculation
ATRPeriodint14ATR period for dynamic trail calculation
TrailMultiplierdouble2.0Multiplier for ATR to set trail distance
LotSizedouble0.1Fixed lot size for each trade
TrendFilterMAint50Period for SMA trend filter (0 = disabled)
MagicNumberint123456Unique identifier to manage EA's own orders
InitialSLBufferdouble0.5ATR multiplier buffer below channel for initial stop

Notice the InitialSLBuffer parameter — that's a lesson from live trading. Placing the stop exactly at the Donchian low gets you stopped out on minor wicks. Adding a 0.5 ATR buffer gives breathing room. You'll want to optimize this per symbol.

Step 2: The Core Breakout Logic

In the OnTick() function, we first check if we have an open position for the current symbol. If not, we look for breakout signals. Here's the essential code for detecting a long breakout. I use iHighest() and iLowest() rather than custom loops — they're faster and less error-prone:

//+------------------------------------------------------------------+
//| Check for long breakout signal                                   |
//+------------------------------------------------------------------+
bool CheckLongBreakout()
{
   // Find highest high and lowest low over the Donchian period, excluding current bar
   int highBar = iHighest(_Symbol, PERIOD_CURRENT, MODE_HIGH, DonchianPeriod, 1);
   int lowBar  = iLowest(_Symbol, PERIOD_CURRENT, MODE_LOW, DonchianPeriod, 1);
   
   double donchHigh = iHigh(_Symbol, PERIOD_CURRENT, highBar);
   double donchLow  = iLow(_Symbol, PERIOD_CURRENT, lowBar);
   
   double currentClose = iClose(_Symbol, PERIOD_CURRENT, 0);
   double prevClose    = iClose(_Symbol, PERIOD_CURRENT, 1);
   
   // Breakout: current close above upper channel, previous close below
   if(currentClose > donchHigh && prevClose <= donchHigh)
   {
      // Optional trend filter: price above SMA
      if(TrendFilterMA > 0)
      {
         double sma = iMA(_Symbol, PERIOD_CURRENT, TrendFilterMA, 0, MODE_SMA, PRICE_CLOSE, 0);
         if(currentClose < sma) return false;
      }
      return true;
   }
   return false;
}

Notice I check that the previous close was below the channel. This prevents re-entering after price has already been above the channel for several bars. It's a simple confirmation that reduces false signals. I also use index 1 for the range — that means we look back starting from the previous completed bar, ignoring the current one. This avoids repaint issues.

For the short breakout, you'd mirror this: check if current close is below the lower channel and previous close was above it, with the trend filter requiring price below the SMA.

Step 3: Dynamic Trailing Stop Implementation

This is where the EA earns its keep. Instead of a fixed pip distance, we calculate the ATR on every tick and adjust the stop accordingly. The stop moves only when price moves in our favor by more than the trail distance. Here's the full function — I call it from OnTick() after checking for new signals:

//+------------------------------------------------------------------+
//| Update trailing stop for all positions                           |
//+------------------------------------------------------------------+
void UpdateTrailingStop()
{
   double atr = iATR(_Symbol, PERIOD_CURRENT, ATRPeriod, 0);
   double trailDistance = atr * TrailMultiplier;
   
   // Convert to points for SL modification
   double trailPoints = trailDistance / _Point;
   
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      if(PositionSelectByTicket(PositionGetTicket(i)))
      {
         if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
         if(PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue;
         
         double currentSL = PositionGetDouble(POSITION_SL);
         double currentTP = PositionGetDouble(POSITION_TP);
         double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
         double currentPrice = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) ? 
                               SymbolInfoDouble(_Symbol, SYMBOL_BID) : 
                               SymbolInfoDouble(_Symbol, SYMBOL_ASK);
         
         if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
         {
            double newSL = currentPrice - trailPoints * _Point;
            // Only move SL forward if it's an improvement and trade is in profit
            if(newSL > currentSL && newSL > openPrice + trailPoints * _Point)
            {
               MqlTradeRequest request = {};
               MqlTradeResult result = {};
               request.action = TRADE_ACTION_SLTP;
               request.position = PositionGetTicket(i);
               request.sl = NormalizeDouble(newSL, _Digits);
               request.tp = currentTP;
               OrderSend(request, result);
               
               // Check for errors
               if(result.retcode != TRADE_RETCODE_DONE)
               {
                  Print("Trail modify failed for ticket ", PositionGetTicket(i), 
                        " error: ", result.retcode);
               }
            }
         }
         else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
         {
            double newSL = currentPrice + trailPoints * _Point;
            if(newSL < currentSL || currentSL == 0)
            {
               MqlTradeRequest request = {};
               MqlTradeResult result

Want to build your own version?

Recreate similar entry logic, risk rules, and filters in TradingBotMaker—no MQL coding. Start free.

Community

Clap for the article and open comments only when you want to read them.

0 claps0 comments

Related articles