Quick Answer
ta.atr, ta.highest and ta.lowest appear in most breakout and volatility-stop strategies. In MQL5, ta.atr(14) becomes iATR(_Symbol, _Period, 14) plus CopyBuffer, because iATR returns an indicator handle and not a value. ta.highest and ta.lowest become iHighest and iLowest, which return a bar index, so you pass that index to iHigh or iLow to get the price.
Two things cause most conversion bugs. Pine's ta.atr smooths with RMA, while the built-in MT5 ATR uses a simple moving average, so the values can differ. And Pine's ta.highest(high, 20) includes the current bar, so you must choose the right start shift in MQL5.
Key Facts
| Pine Script | MQL5 |
|---|---|
| ta.atr(14) | iATR(_Symbol, _Period, 14) to get a handle, then CopyBuffer |
| ta.highest(high, 20) | iHigh(_Symbol, _Period, iHighest(_Symbol, _Period, MODE_HIGH, 20, 0)) |
| ta.lowest(low, 20) | iLow(_Symbol, _Period, iLowest(_Symbol, _Period, MODE_LOW, 20, 0)) |
| ta.highest(high, 20)[1] | Same call with start = 1 |
| high[1] | iHigh(_Symbol, _Period, 1) |
| Bar 0 is the current bar | Shift 0 is the current, still forming bar. Shift 1 is the last closed bar |
| ATR is RMA smoothed | Built-in ATR is an SMA of true range, so values can differ |
The Pine Script Version
//@version=5
strategy("ATR breakout", overlay = true)
atrVal = ta.atr(14)
hh = ta.highest(high, 20)[1]
ll = ta.lowest(low, 20)[1]
if close > hh
strategy.entry("Long", strategy.long)
strategy.exit("Exit", "Long", stop = close - atrVal * 2)
The strategy enters when the close is above the highest high of the previous 20 bars, and places a stop 2 ATR below the close. The [1] matters: it means the level comes from the bars before the current one.
ta.atr in MQL5: iATR and CopyBuffer
iATR does not return a number. It returns a handle to an indicator that MT5 keeps running for you. Create the handle once in OnInit(), read values with CopyBuffer, and release it in OnDeinit().
int atrHandle = INVALID_HANDLE;
int OnInit()
{
atrHandle = iATR(_Symbol, _Period, 14);
if(atrHandle == INVALID_HANDLE)
return INIT_FAILED;
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
if(atrHandle != INVALID_HANDLE)
IndicatorRelease(atrHandle);
}
double GetATR(int shift)
{
double buf[1];
if(CopyBuffer(atrHandle, 0, shift, 1, buf) != 1)
return EMPTY_VALUE;
return buf[0];
}
Do not call iATR on every tick. Each call to iATR with the same parameters returns a handle, and creating handles repeatedly wastes resources. The details of CopyBuffer, including what to do when it returns -1, are in MQL5 CopyBuffer guide.
Why ATR Values Can Differ From Pine
Pine's ta.atr is the RMA (Wilder's smoothing) of the true range. The built-in MT5 ATR indicator averages the true range with a simple moving average. Both use the same true range, so the values are close, but they are not identical, and the gap is most visible on short lengths and in the first bars of the chart.
If your stop distance or position size depends on the exact Pine value, calculate the RMA version yourself. This function seeds the average with a simple average of the first length true ranges, then applies the Pine RMA formula forward to the shift you ask for:
double TrueRange(int i)
{
double h = iHigh(_Symbol, _Period, i);
double l = iLow(_Symbol, _Period, i);
double prevClose = iClose(_Symbol, _Period, i + 1);
return MathMax(h - l, MathMax(MathAbs(h - prevClose), MathAbs(l - prevClose)));
}
double AtrRma(int length, int shift, int lookback = 500)
{
int oldest = shift + lookback;
if(Bars(_Symbol, _Period) < oldest + length + 2)
return EMPTY_VALUE;
// Seed: simple average of the first 'length' true ranges
double sum = 0.0;
for(int i = oldest; i > oldest - length; i--)
sum += TrueRange(i);
double atr = sum / length;
// RMA: (previous * (length - 1) + current) / length
for(int i = oldest - length; i >= shift; i--)
atr = (atr * (length - 1) + TrueRange(i)) / length;
return atr;
}
The result depends on where the calculation starts, so a longer lookback gets closer to what Pine shows. Compare a few bars of your own chart against TradingView before you rely on it. The same RMA and EMA differences, and how to compare them bar by bar, are covered in Pine Script ta.rsi and ta.ema vs MQL5.
ta.highest and ta.lowest in MQL5
iHighest and iLowest find the position of the extreme value, not the value. They return the bar index, or -1 if the request failed. Use iHigh and iLow to read the price at that index.
double HighestHigh(int count, int start)
{
int idx = iHighest(_Symbol, _Period, MODE_HIGH, count, start);
if(idx < 0)
return EMPTY_VALUE;
return iHigh(_Symbol, _Period, idx);
}
double LowestLow(int count, int start)
{
int idx = iLowest(_Symbol, _Period, MODE_LOW, count, start);
if(idx < 0)
return EMPTY_VALUE;
return iLow(_Symbol, _Period, idx);
}
The arguments are the symbol, the timeframe, what to compare (MODE_HIGH or MODE_LOW), how many bars to look at, and the shift where the search starts. With count = 20 and start = 1, the search covers shifts 1 to 20.
Bar Indexing: Where Most Bugs Come From
Pine's ta.highest(high, 20) looks at the current bar and the 19 bars before it. In MQL5, that is count = 20, start = 0. On a live chart the current bar is still forming, so its high can change and your level can move in the middle of the candle.
ta.highest(high, 20)maps tostart = 0.ta.highest(high, 20)[1], the level made by the bars before the current one, maps tostart = 1.- For rules that should only react to closed bars, use
start = 1, together with a new-bar check. The pattern is explained in Pine Script close[1] in MQL5.
Full MQL5 Example
This EA reads the ATR and the 20-bar channel once per new bar and prints a breakout signal. It does not place trades, so you can check the numbers against your Pine chart first.
input int ATRPeriod = 14;
input int ChannelLength = 20;
int atrHandle = INVALID_HANDLE;
int OnInit()
{
atrHandle = iATR(_Symbol, _Period, ATRPeriod);
if(atrHandle == INVALID_HANDLE)
return INIT_FAILED;
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
if(atrHandle != INVALID_HANDLE)
IndicatorRelease(atrHandle);
}
bool IsNewBar()
{
static datetime lastBarTime = 0;
datetime currentBarTime = iTime(_Symbol, _Period, 0);
if(currentBarTime == 0 || currentBarTime == lastBarTime)
return false;
lastBarTime = currentBarTime;
return true;
}
double GetATR(int shift)
{
double buf[1];
if(CopyBuffer(atrHandle, 0, shift, 1, buf) != 1)
return EMPTY_VALUE;
return buf[0];
}
double HighestHigh(int count, int start)
{
int idx = iHighest(_Symbol, _Period, MODE_HIGH, count, start);
return (idx < 0) ? EMPTY_VALUE : iHigh(_Symbol, _Period, idx);
}
double LowestLow(int count, int start)
{
int idx = iLowest(_Symbol, _Period, MODE_LOW, count, start);
return (idx < 0) ? EMPTY_VALUE : iLow(_Symbol, _Period, idx);
}
void OnTick()
{
if(!IsNewBar())
return;
// The last closed bar is shift 1. The channel is built from the bars before it,
// like Pine ta.highest(high, 20)[1] on the closed bar.
double closePrice = iClose(_Symbol, _Period, 1);
double atrValue = GetATR(1);
double hh = HighestHigh(ChannelLength, 2);
double ll = LowestLow(ChannelLength, 2);
if(closePrice == 0.0 || atrValue == EMPTY_VALUE || hh == EMPTY_VALUE || ll == EMPTY_VALUE)
return;
if(closePrice > hh)
PrintFormat("Breakout up. Close %.5f > high %.5f, ATR %.5f, stop distance %.5f",
closePrice, hh, atrValue, atrValue * 2.0);
else if(closePrice < ll)
PrintFormat("Breakout down. Close %.5f < low %.5f, ATR %.5f, stop distance %.5f",
closePrice, ll, atrValue, atrValue * 2.0);
}
Because the signal is evaluated on the closed bar (shift 1), the channel starts one bar earlier, at shift 2. That is the same relationship as close > ta.highest(high, 20)[1] on that Pine bar. To turn the signal into a stop level, see Pine Script strategy.exit to MQL5 SL and TP.
Common Mistakes
- Expecting
iATRto return a number. It returns a handle. Read the value withCopyBuffer. - Creating the handle on every tick. Create it once in
OnInit()and release it inOnDeinit(). - Treating
iHighestas a price. It is a bar index. Pass it toiHigh. - Using the forming bar by accident.
start = 0includes the live bar. Usestart = 1or higher for closed-bar logic. - Off-by-one on the channel. If the Pine code has
[1]and you compare against the last closed bar, shift the channel one bar further back. - Expecting identical ATR values. Pine uses RMA, the built-in MT5 ATR uses SMA.
- Not checking return values.
CopyBuffercan return -1 andiHighestcan return -1 while history is still loading. Check before you use the value.
FAQ
Related Guides
- MQL5 CopyBuffer guide: read indicator values safely and handle errors.
- Pine Script ta.rsi and ta.ema vs MQL5: why RMA and EMA values differ.
- Pine Script close[1] in MQL5: closed-bar logic and the new-bar check.
- Pine Script strategy.exit to MQL5 SL and TP: turn an ATR distance into a stop and a target.
- Pine Script var and varip in MQL5: keep state between bars.
Convert Indicator Logic Without Surprises
Indicator calls and bar shifts are the easiest places to introduce a one-bar error. 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.
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.