Quick Answer
ta.bb(close, 20, 2.0) in Pine Script maps to iBands(_Symbol, _Period, 20, 0, 2.0, PRICE_CLOSE) in MQL5. iBands returns a handle, and you read the values with CopyBuffer. It has three buffers: 0 is the middle line, 1 is the upper band and 2 is the lower band.
For a standalone ta.stdev, you can use iStdDev, or calculate it in a short loop. The detail to watch is the formula. Pine's ta.stdev is biased by default, which means it divides by the number of bars, not by the number of bars minus one. If the Pine code sets biased = false, the plain indicator does not match, and you need the sample version.
Key Facts
| Pine Script | MQL5 |
|---|---|
| [basis, upper, lower] = ta.bb(close, 20, 2.0) | iBands(_Symbol, _Period, 20, 0, 2.0, PRICE_CLOSE) plus CopyBuffer |
| basis | Buffer 0 (BASE_LINE) |
| upper | Buffer 1 (UPPER_BAND) |
| lower | Buffer 2 (LOWER_BAND) |
| ta.stdev(close, 20) | iStdDev(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE) or a manual loop |
| ta.stdev(close, 20, false) | Manual loop that divides by length - 1 |
| ta.crossunder(close, lower) | close1 < lower1 && close2 >= lower2 on closed bars |
| Shift 0 is the live bar | Use shift 1 for closed-bar logic |
The Pine Script Version
//@version=5
strategy("Bollinger touch", overlay = true)
[basis, upper, lower] = ta.bb(close, 20, 2.0)
if ta.crossunder(close, lower)
strategy.entry("Long", strategy.long)
The strategy enters long when the close crosses below the lower band. In ta.bb, the middle line is a simple moving average of the source, and each band is the middle line plus or minus the multiplier times ta.stdev.
iBands and CopyBuffer
The parameters are the symbol, the timeframe, the period, the band shift, the deviation multiplier and the applied price. Use a band shift of 0, because Pine's ta.bb has no shift:
int bandsHandle = INVALID_HANDLE;
int OnInit()
{
bandsHandle = iBands(_Symbol, _Period, 20, 0, 2.0, PRICE_CLOSE);
if(bandsHandle == INVALID_HANDLE)
return INIT_FAILED;
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
if(bandsHandle != INVALID_HANDLE)
IndicatorRelease(bandsHandle);
}
bool GetBands(int shift, double &middle, double &upper, double &lower)
{
double m[1], u[1], l[1];
if(CopyBuffer(bandsHandle, 0, shift, 1, m) != 1) return false;
if(CopyBuffer(bandsHandle, 1, shift, 1, u) != 1) return false;
if(CopyBuffer(bandsHandle, 2, shift, 1, l) != 1) return false;
middle = m[0];
upper = u[0];
lower = l[0];
return true;
}
Create the handle once, not on every tick. For the details of CopyBuffer and what to do when it returns -1, see MQL5 CopyBuffer guide.
ta.stdev: Biased or Sample
The standard deviation has two common versions:
- Biased (population): divide the sum of squared differences by
length. This is the default in Pine'sta.stdev. - Unbiased (sample): divide by
length - 1. In Pine, you get it withbiased = false.
The built-in MT5 indicators use the divide-by-length version, which matches Pine's default. If your Pine code uses biased = false, calculate it yourself:
// Standard deviation of the close, like ta.stdev(close, length, biased)
double StdDevClose(int length, int shift, bool biased = true)
{
if(length < 2)
return EMPTY_VALUE;
double sum = 0.0;
for(int i = shift; i < shift + length; i++)
{
double c = iClose(_Symbol, _Period, i);
if(c == 0.0)
return EMPTY_VALUE; // history not ready
sum += c;
}
double mean = sum / length;
double sq = 0.0;
for(int i = shift; i < shift + length; i++)
{
double d = iClose(_Symbol, _Period, i) - mean;
sq += d * d;
}
double divisor = biased ? (double)length : (double)(length - 1);
return MathSqrt(sq / divisor);
}
With biased = true, mean + 2.0 * StdDevClose(20, shift) should give the same upper band as ta.bb(close, 20, 2.0). Check a few bars against TradingView before you rely on it.
Full MQL5 Example
This EA detects the close crossing below the lower band on the last closed bar, once per bar, and prints it. It does not place trades.
input int BandsPeriod = 20;
input double BandsDev = 2.0;
int bandsHandle = INVALID_HANDLE;
datetime g_lastBarTime = 0;
int OnInit()
{
bandsHandle = iBands(_Symbol, _Period, BandsPeriod, 0, BandsDev, PRICE_CLOSE);
if(bandsHandle == INVALID_HANDLE)
return INIT_FAILED;
g_lastBarTime = iTime(_Symbol, _Period, 0);
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
if(bandsHandle != INVALID_HANDLE)
IndicatorRelease(bandsHandle);
}
bool IsNewBar()
{
datetime currentBarTime = iTime(_Symbol, _Period, 0);
if(currentBarTime == 0 || currentBarTime == g_lastBarTime)
return false;
g_lastBarTime = currentBarTime;
return true;
}
bool GetBands(int shift, double &middle, double &upper, double &lower)
{
double m[1], u[1], l[1];
if(CopyBuffer(bandsHandle, 0, shift, 1, m) != 1) return false;
if(CopyBuffer(bandsHandle, 1, shift, 1, u) != 1) return false;
if(CopyBuffer(bandsHandle, 2, shift, 1, l) != 1) return false;
middle = m[0];
upper = u[0];
lower = l[0];
return true;
}
void OnTick()
{
if(!IsNewBar())
return;
double mid1, up1, low1;
double mid2, up2, low2;
// shift 1 = last closed bar, shift 2 = the bar before it
if(!GetBands(1, mid1, up1, low1)) return;
if(!GetBands(2, mid2, up2, low2)) return;
double close1 = iClose(_Symbol, _Period, 1);
double close2 = iClose(_Symbol, _Period, 2);
if(close1 == 0.0 || close2 == 0.0)
return;
// ta.crossunder(close, lower) on the closed bar
if(close1 < low1 && close2 >= low2)
PrintFormat("Close crossed below the lower band. Close %.5f, lower %.5f, middle %.5f",
close1, low1, mid1);
}
The cross rule is the mirror of ta.crossover, which is explained in Pine Script ta.crossover in MQL5. The new-bar check and the shift 1 rule are explained in Pine Script close[1] in MQL5.
Common Mistakes
- Using the wrong buffer. 0 is the middle line, 1 is the upper band, 2 is the lower band.
- Setting a band shift. The second number after the period is the shift of the lines on the chart. Keep it 0 to match
ta.bb. - Ignoring
biased = false. If Pine uses the sample standard deviation, calculate it yourself. - Mixing the source.
ta.bb(close, ...)maps toPRICE_CLOSE. A different source, such ashl2, needs a different applied price (PRICE_MEDIANforhl2). - Reading shift 0. The live bar changes on every tick, and so do the bands. Use closed bars.
- Comparing the close with the band of a different bar. Compare the close at shift 1 with the band at shift 1.
- Creating the handle on every tick. Create it once in
OnInit().
FAQ
Related Guides
- Pine Script ta.macd in MQL5: another multi-buffer indicator, with a signal line that differs from Pine.
- Pine Script ta.rsi and ta.ema vs MQL5: why indicator values can differ.
- Pine Script ta.crossover in MQL5: detect a cross on closed bars.
- MQL5 CopyBuffer guide: read indicator buffers safely.
- Pine Script close[1] in MQL5: closed-bar values and the new-bar check.
- Pine Script ta.atr, ta.highest and ta.lowest in MQL5: ATR and channel levels with correct bar indexing.
- Why a Pine Script Strategy Gives Different Results in MQL5: indicator differences are one of the causes.
Convert Indicator Logic Without Surprises
A biased or sample standard deviation, a wrong buffer, or a band compared on the wrong bar can change when a strategy enters. CodeFlowOS translates Pine Script into MQL5 and lets you review the generated code before you test it in the MT5 Strategy Tester. The platform compile check is optional, and outputs are not guaranteed, so always review the result. For the full process, see the Pine Script to MQL5 guide, or try the converter page. If the code does not compile, the Error Fixer can suggest a fix, and the free early-bird Code Review checks an EA for common mistakes.
This article is educational. It does not describe trading performance and is not financial advice. Always compile and test code in MetaEditor and the Strategy Tester before using it on a live account.