How to Develop a Bollinger Bands Breakout and Retest Indicator
Last checked: 12 October 2026. This guide draws on the MQL5 reference for iBands and CopyBuffer. The rule is a teaching example and has not been backtested for this article. Test on a demo account.
A Bollinger Bands breakout happens when a candle closes outside the upper or lower band. A retest happens when price comes back to the band or the middle line and holds. Many traders prefer to wait for the retest rather than chase the first move. This guide turns that idea into a small state machine you can code in MQL5.
Define the events precisely
Words like “breakout” and “retest” are vague until you convert them into conditions on closed candles:
- Breakout up: a candle closes above the upper band.
- Retest up: within the next N candles, a candle’s low trades down to or below the upper band (or the middle line, if you choose that version) but the candle still closes above the band.
- Failure: a candle closes back inside the bands before any valid retest, which cancels the setup.
- The downside is the mirror image.
Every word in those definitions maps to a comparison between a closed candle’s values and the band values on the same candle. That is what makes the logic testable.

Why a state machine
The indicator has to remember what happened on earlier bars: a breakout occurred two candles ago, and we are waiting. A state machine stores that memory in a small number of named states: IDLE, WAIT_RETEST_UP, WAIT_RETEST_DOWN. Each new closed bar can move the state forward, back to IDLE or fire a signal.
The indicator
This indicator draws an up arrow when a valid upward retest is confirmed and a down arrow for a valid downward retest. It processes bars in order and stores arrows in buffers, so past arrows never move.
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots 2
#property indicator_label1 "Retest Buy"
#property indicator_type1 DRAW_ARROW
#property indicator_color1 clrLimeGreen
#property indicator_label2 "Retest Sell"
#property indicator_type2 DRAW_ARROW
#property indicator_color2 clrTomato
input int InpPeriod = 20;
input double InpDev = 2.0;
input int InpMaxWait = 5; // candles allowed for the retest
double BuyBuf[], SellBuf[];
int hBB = INVALID_HANDLE;
int OnInit()
{
SetIndexBuffer(0, BuyBuf, INDICATOR_DATA);
SetIndexBuffer(1, SellBuf, INDICATOR_DATA);
PlotIndexSetInteger(0, PLOT_ARROW, 233); // up arrow
PlotIndexSetInteger(1, PLOT_ARROW, 234); // down arrow
PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
hBB = iBands(_Symbol, _Period, InpPeriod, 0, InpDev, PRICE_CLOSE);
return(hBB == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED);
}
void OnDeinit(const int reason) { IndicatorRelease(hBB); }
int OnCalculate(const int rates_total, const int prev_calculated,
const datetime &time[], const double &open[],
const double &high[], const double &low[],
const double &close[], const long &tick_volume[],
const long &volume[], const int &spread[])
{
if(rates_total < InpPeriod + 10) return(0);
double up[], lo[];
if(CopyBuffer(hBB, 1, 0, rates_total, up) != rates_total) return(0);
if(CopyBuffer(hBB, 2, 0, rates_total, lo) != rates_total) return(0);
ArraySetAsSeries(up, false);
ArraySetAsSeries(lo, false);
int start = (prev_calculated > 1) ? prev_calculated - 1 : InpPeriod;
int last = rates_total - 1; // forming bar: never evaluated
// rebuild state by scanning a short window before 'start'
int from = MathMax(InpPeriod, start - InpMaxWait - 2);
int state = 0, waited = 0; // 0 idle, 1 wait up, -1 wait down
for(int i = from; i < last; i++)
{
if(i >= start) { BuyBuf[i] = EMPTY_VALUE; SellBuf[i] = EMPTY_VALUE; }
if(state == 0)
{
if(close[i] > up[i]) { state = 1; waited = 0; }
else if(close[i] < lo[i]) { state = -1; waited = 0; }
continue;
}
waited++;
if(state == 1)
{
if(close[i] <= up[i]) { state = 0; } // failed
else if(low[i] <= up[i]) // touched band, held
{
if(i >= start) BuyBuf[i] = low[i] - 10 * _Point;
state = 0;
}
else if(waited >= InpMaxWait) state = 0;
}
else
{
if(close[i] >= lo[i]) { state = 0; }
else if(high[i] >= lo[i])
{
if(i >= start) SellBuf[i] = high[i] + 10 * _Point;
state = 0;
}
else if(waited >= InpMaxWait) state = 0;
}
}
BuyBuf[last] = EMPTY_VALUE;
SellBuf[last] = EMPTY_VALUE;
return(rates_total);
}
Understanding the design
The loop stops one bar before the newest, so the forming candle is never evaluated and no arrow can appear and vanish within a candle. Because OnCalculate may be called with only a few new bars, the code re-scans a short window before the starting bar to rebuild the state. Without that, a breakout that happened just before the window would be forgotten.
The BuyBuf and SellBuf arrays are filled with EMPTY_VALUE where there is no arrow. Setting PLOT_EMPTY_VALUE tells the terminal not to draw those points.
Testing it properly
- Attach it to a chart and scroll back. Check each arrow by eye: was there a close outside the band before it, and a touch followed by a close outside?
- Watch it live on a demo chart for several days. Arrows should appear only after candles close and should never disappear.
- Count how many arrows were followed by continuation. A handful of examples proves nothing, so test across many months and several pairs before drawing conclusions.
Honest limits
- Bollinger Bands widen when price moves, so a strong trend can keep price outside the band for many candles with no retest at all.
- In sideways markets, closes outside the band often fail. The ranging filter article in this series helps with that.
- The arrow marks a pattern, not a prediction.
Installing and adjusting it
Create a custom indicator in MetaEditor named BBRetest, paste the code and compile. After attaching it, the Inputs tab lets you change the Bollinger period and deviation, and the number of candles you allow for the retest. Standard Bollinger settings are a period of 20 and a deviation of 2, but they are conventions rather than laws, and some traders prefer a deviation of 2.5 for fewer signals. Change one input at a time so you can see what each does.
Arrow codes and offsets
PLOT_ARROW takes a Wingdings character code. Code 233 draws an upward arrow and 234 a downward one in the standard Wingdings font. The arrows are placed ten points below the candle’s low or above its high so they do not sit on the candle itself. On instruments with a very small point size, ten points may be too tight; try a multiple of ATR for the offset instead.
What a good manual check looks like
Pick three recent arrows and walk through them by eye. For each, find the candle that closed outside the band, count the candles until the arrow, and confirm that none of the candles in between closed back inside the band. Then ask what price did next, but do not draw conclusions from three cases. The goal of this check is to prove the code does what the definition says, not to prove the pattern makes money.
What we could not confirm
- Any profitability of the retest rule. We did not run a backtest.
- The best period, deviation or wait length for your market.
Frequently asked questions
Should I retest the band or the middle line? Both are used. The middle line retest is deeper and less frequent. Try each and compare.
Why arrows in buffers instead of objects? Buffers are tied to bars and cleaned up by the terminal, and they work with iCustom if you later build an Expert Advisor.
Key takeaways
Convert vocabulary into closed-candle comparisons, use a state machine for multi-bar patterns, never evaluate the forming bar and verify arrows against the chart by hand.
Sources checked
- MQL5 Reference, iBands, CopyBuffer, PlotIndexSetInteger and PLOT_ARROW, mql5.com
Educational content only — not financial advice. Income and savings results vary and are never guaranteed. How we write
Enjoyed this story?
Get the best stories delivered weekly.


