How to Code a Bollinger Bands EA in MQL5 (Mean-Reversion Example + Real Backtest)

Published 2026-09-29 · Ranar Algo

Bollinger Bands are one of the most common building blocks for a mean-reversion strategy: price

tends to snap back toward the middle band after it stretches to an extreme. That idea is easy to

draw on a chart and surprisingly easy to get *wrong* in code. This guide walks through a complete,

compiling MQL5 Expert Advisor that trades it — and then shows a real MetaTrader 5 backtest of the

exact source, so you can see what the rules actually do rather than what they promise.

The strategy in one sentence

When a candle closes back above the lower band after closing below it, go long. When a candle

closes back below the upper band after closing above it, go short. Exit when price returns to

the middle (base) line. That "close back inside" filter matters — entering the moment price merely

touches a band is how you end up buying straight into a trend that keeps running.

The iBands handle (and the buffer order people miss)

In MQL5 you create an indicator once with a handle, then read its values with CopyBuffer. The

signature is:

int iBands(string symbol, ENUM_TIMEFRAMES period, int bands_period,
           int bands_shift, double deviation, ENUM_APPLIED_PRICE applied_price);

The single detail that trips people up is the buffer numbering. For Bollinger Bands it is not

upper/middle/lower in visual order — it is:

Get that wrong and your "buy below the band" logic silently reads the middle line. Here is the setup:

#include <Trade/Trade.mqh>
input int    InpBandsPeriod = 20;
input double InpDeviation   = 2.0;
input double InpLots        = 0.10;
input int    InpStopLossPts = 600;
input int    InpMagic       = 770020;

CTrade c_Trade;
int    h_Bands = INVALID_HANDLE;
double g_upper[], g_lower[], g_base[], g_close[];

int OnInit()
{
   h_Bands = iBands(_Symbol, _Period, InpBandsPeriod, 0, InpDeviation, PRICE_CLOSE);
   if(h_Bands == INVALID_HANDLE) { Print("iBands failed, err=", GetLastError()); return(INIT_FAILED); }
   ArraySetAsSeries(g_upper, true); ArraySetAsSeries(g_lower, true);
   ArraySetAsSeries(g_base, true);  ArraySetAsSeries(g_close, true);
   c_Trade.SetExpertMagicNumber(InpMagic);
   c_Trade.SetTypeFillingBySymbol(_Symbol);
   return(INIT_SUCCEEDED);
}

ArraySetAsSeries(..., true) makes index [0] the current (still-forming) bar, [1] the last

closed bar, [2] the one before it — so signal logic reads left-to-right in time like you'd expect.

Act on closed bars only

A mean-reversion rule that fires mid-bar will re-trigger on every tick until the bar closes,

firing a burst of orders. The fix is to do the work once per new bar:

bool IsNewBar()
{
   datetime t = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE);
   if(t == g_lastBar) return(false);
   g_lastBar = t;
   return(true);
}

The entry and exit logic

Read three values of each buffer ([0] forming, [1] and [2] closed), compare the two closed

bars, and act:

void OnTick()
{
   if(!IsNewBar()) return;
   if(CopyBuffer(h_Bands, 1, 0, 3, g_upper) < 3) return;   // upper
   if(CopyBuffer(h_Bands, 2, 0, 3, g_lower) < 3) return;   // lower
   if(CopyBuffer(h_Bands, 0, 0, 3, g_base)  < 3) return;   // middle
   if(CopyClose(_Symbol, _Period, 0, 3, g_close) < 3) return;

   double closePrev = g_close[2], closeNow = g_close[1];

   if(HasPosition())
   {
      long type = PositionGetInteger(POSITION_TYPE);
      if(type == POSITION_TYPE_BUY  && closeNow >= g_base[1]) c_Trade.PositionClose(_Symbol);
      if(type == POSITION_TYPE_SELL && closeNow <= g_base[1]) c_Trade.PositionClose(_Symbol);
      return;
   }

   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

   if(closePrev < g_lower[2] && closeNow > g_lower[1])          // closed back above lower band
   {
      double sl = InpStopLossPts > 0 ? NormalizeDouble(ask - InpStopLossPts * _Point, _Digits) : 0;
      c_Trade.Buy(InpLots, _Symbol, ask, sl, 0);
   }
   else if(closePrev > g_upper[2] && closeNow < g_upper[1])     // closed back below upper band
   {
      double sl = InpStopLossPts > 0 ? NormalizeDouble(bid + InpStopLossPts * _Point, _Digits) : 0;
      c_Trade.Sell(InpLots, _Symbol, bid, sl, 0);
   }
}

A few things worth calling out. HasPosition() checks PositionsTotal() filtered by symbol and

magic number, so the EA holds at most one position and won't fight a trade opened by something else.

CTrade handles the order request plumbing, and SetTypeFillingBySymbol picks a filling mode the

broker actually accepts — a frequent cause of "order send failed" on otherwise-correct code. The

exit here is the middle band; a fixed stop-loss in points is the only hard risk cap, and there is no

take-profit because the whole premise is "ride it back to the mean," not to a fixed target.

A real backtest of this exact code

Rules that look clean can still behave differently than you assume, so here is the genuine

MetaTrader 5 Strategy Tester result for the source above — no tuning, default 20/2.0 bands, 0.10

lots, 600-point stop:

That is a textbook example of *why you backtest before you trust a strategy*: a 62% win rate sounds

great, but a profit factor barely above 1.0 means the average loss nearly cancels the average win —

the edge is thin and would likely be eaten by wider spreads or a different period. It's a starting

point to iterate on (different symbol, band period, a trend filter), not a finished system. Past

performance does not guarantee future results.

Let the tool write and backtest it for you

If you'd rather describe the strategy than hand-write the handle bookkeeping, that's exactly what

Ranar Algo does: you type the rules in plain English, it generates the MQL5, compile-checks it

against real MetaEditor, and runs the same MetaTrader 5 Strategy Tester backtest you saw above —

returning the net profit, win rate, profit factor, drawdown and trade count. You keep your IP and

can download the full .mq5 source; nothing is locked to us. It's still improving and feedback is

welcome. It won't invent an edge that isn't there — but it removes the boilerplate between an idea

and an honest backtest, which is usually where a good strategy dies of a silent bug.

Takeaways

Build & backtest your own EA free →

Ranar Algo turns a plain-English strategy into a compiled MQL5 Expert Advisor with a real MetaTrader 5 backtest. You keep your IP. Past performance does not guarantee future results.