Edgoro
Pine Script v6 → MQL5

Pine Script to MQL5: the complete translation guide

How to port a TradingView indicator or strategy to MetaTrader 5 by hand, construct by construct: what changes in the way the code runs, a table of 64 Pine constructs and their MQL5 equivalents, and 11 pages that put a Pine script next to the complete MQL5 program that reproduces it.

Start with a whole strategy

Worked port · Expert Advisor

Pine strategy to MT5 Expert Advisor: a complete worked port

A Pine Script v6 EMA crossover strategy with ATR stop, target and risk sizing, ported block by block to an MQL5 Expert Advisor that compiles in MetaEditor.

Pine and MQL5 side by side

How Pine Script and MQL5 differ

The two languages don't just use different words, they run differently. Most ports that compile but behave wrongly trip over one of these ten differences, not over syntax.

Bars versus events

A Pine script runs once per bar, from the oldest loaded bar to the newest, then again on every price update of the real-time bar. You write what happens on one bar and Pine does the looping.

MQL5 has no bar loop. The program reacts to events: OnInit() when it starts, OnDeinit() when it stops, and in between either OnCalculate() (a custom indicator, called on every tick with the number of bars and how many were already calculated) or OnTick() (an Expert Advisor, called on every new tick of its symbol). An indicator loops over the bars it hasn't calculated yet; an Expert Advisor reads the bars it needs by index.

The rule of thumb: a Pine indicator() becomes an MQL5 custom indicator, and a Pine strategy() becomes an Expert Advisor.

In practice: barstate.isconfirmed and new bars

Series and indexing: close[1] versus arrays

In Pine, close is the bar being executed and close[1] the bar before; the history operator always counts back from the current bar.

In MQL5 you index arrays yourself, and two conventions coexist. The price arrays passed to OnCalculate() are not series by default: index 0 is the oldest bar and rates_total - 1 the newest, so Pine's close[1] on bar i is close[i - 1]. Functions such as iClose(_Symbol, _Period, shift), CopyBuffer() and arrays flipped with ArraySetAsSeries(arr, true) count the other way: 0 is the newest (forming) bar and 1 the last closed one.

Mixing the two is the most common porting bug: the signal lands one bar off, or reads a bar from the future.

In practice: ta.crossover and ta.crossunder

Repainting: barstate.isconfirmed versus the new-bar check

On historical bars Pine only sees final values; on the real-time bar the values change with every tick, so a condition can turn true and then false before the bar closes. barstate.isconfirmed, or reading [1], keeps signals to closed bars.

MQL5 has the same forming bar: index rates_total - 1 in an indicator, shift 0 in an Expert Advisor. A signal that must not flicker reads rates_total - 2 in an indicator; an Expert Advisor acts on the first tick of a new bar and reads shift 1. The MT5 Strategy Tester's "Open prices only" mode runs only that first tick of each bar, so an EA that reads bar 0 tests one way and trades another, the MT5 version of a repainting Pine backtest.

In practice: barstate.isconfirmed and new bars

Other timeframes: request.security versus iClose and handles

request.security() evaluates an expression on another timeframe or symbol and lines the result up with the chart's bars, and its lookahead argument plus a [1] offset decide whether history can see the future.

In MQL5 every series function takes a symbol and a timeframe: iClose(_Symbol, PERIOD_D1, 1) is yesterday's completed daily close, and an indicator handle can be created on any timeframe, iMA(_Symbol, PERIOD_H4, ...). On historical bars of an indicator, iBarShift() finds which higher-timeframe bar a chart bar belongs to; reading the bar before it is the non-repainting [1] + lookahead_on form. The other timeframe may not be loaded yet: copies then fail (error 4806) and the program tries again on the next tick.

In practice: request.security on a higher timeframe

Strategies: a simulated broker versus real orders

strategy.* calls go to TradingView's broker emulator: by default an order decided on a bar's close fills at the next bar's open, quantities are units or contracts, and the strategy holds one net position, which strategy.entry() reverses when the signal flips.

An Expert Advisor sends real requests through CTrade (from <Trade/Trade.mqh>), in lots, and the trade server can refuse them: volume steps, minimum stop distances, filling modes, margin. Every result has a return code to check. The account type matters too: on a netting account there is one position per symbol, as in Pine; on a hedging account, common with forex brokers, a buy doesn't close an open sell, so a reversal means closing the short yourself. A magic number tells the EA's positions from your manual trades.

In practice: strategy.entry to CTrade

var and varip versus globals and static

A plain Pine variable is recalculated on every bar. var keeps its value from bar to bar, and on the real-time bar Pine rolls it back to the last closed bar's value before each tick, so the forming bar counts once. varip skips the rollback and keeps every tick's change.

MQL5 globals and static locals persist between calls and are never rolled back: they behave like varip. To get var in an indicator, derive each bar's value from the previous bar's buffer element; the forming bar is then recomputed from a fixed starting point on every tick. Anything that must survive a restart of the terminal belongs in the deal history or a terminal global variable, not in a variable.

In practice: var and varip

na versus EMPTY_VALUE

Pine has na for "no value", with na(), nz() and fixnan() to test and replace it.

A double in MQL5 always holds a number. In indicator buffers, EMPTY_VALUE (the largest double) means "draw nothing here", and PLOT_EMPTY_VALUE tells the terminal which value to skip. Functions report failure through their return value (-1, 0, false) and GetLastError(), not through na. Built-in indicators need a warm-up period like Pine's: skip the first bars of their period instead of testing for a special value, and use MathIsValidNumber() to catch the NaN or infinity a division by zero can produce.

plot() versus indicator buffers

plot() draws a series in one call. In MQL5 a custom indicator declares how many buffers (arrays of values) and plots (things drawn) it has with #property indicator_buffers and indicator_plots, binds each array with SetIndexBuffer(), picks a drawing type (DRAW_LINE, DRAW_ARROW, DRAW_FILLING, DRAW_COLOR_LINE, DRAW_HISTOGRAM, DRAW_NONE) and writes one value per bar.

An Expert Advisor has no buffers: it draws chart objects (ObjectCreate()), which is also what label.new(), line.new(), box.new() and table.new() become.

In practice: plot, fill and plotshape

alertcondition() versus Alert() and SendNotification()

TradingView evaluates alerts on its own servers: alertcondition() adds a condition you pick in the "Create alert" dialog, alert() fires from the code, and both keep running with your browser closed.

MetaTrader 5 has no dialog for an indicator's conditions. The program raises the alert itself while it runs in your terminal: Alert() for a pop-up with a sound, SendNotification() for a push to the MetaTrader mobile app, SendMail() for e-mail, and WebRequest() (Expert Advisors only) for a webhook. For alerts around the clock the terminal has to run around the clock, usually on a VPS.

In practice: alertcondition and alert

input.*() versus input variables

input.int(9, "Fast EMA", minval = 1) becomes input int InpFastLen = 9; // Fast EMA: the comment after the declaration is the label in the settings dialog. MQL5 inputs have no minval, maxval or step: check the values in OnInit() and return INIT_PARAMETERS_INCORRECT. A list of options becomes an enum, input.timeframe() an ENUM_TIMEFRAMES input, input.source() an ENUM_APPLIED_PRICE input, and group an input group "Name" line. Inputs are constants: the code can't assign to them.

Pine Script to MQL5 mapping table

64 Pine Script v6 constructs and how to write them in MQL5 for MetaTrader 5. Built-in indicators are handles read with CopyBuffer(), never the MQL4-style functions that return a value directly. Where a row links to a guide, that page has a complete program.

Bars, series and execution
Pine Script v6MQL5Note
The script runs once per bar, then on every real-time tickIndicator: OnCalculate(), looping from prev_calculated - 1. Expert Advisor: OnTick() plus a new-bar checkOnCalculate() is called on every tick too; recalculating only the new bars is your job.
Full example: barstate.isconfirmed and new bars
open, high, low, closeThe open[], high[], low[], close[] arrays of OnCalculate(); in an EA iClose(_Symbol, _Period, shift) or CopyRates()In an EA, shift 0 is the forming bar and shift 1 the last closed one.
volumetick_volume[] (EA: iTickVolume()); volume[] only where the broker sends real volumeOn most forex and CFD symbols real volume is 0. TradingView's forex volume is a tick count from another feed, so the numbers differ.
close[1]close[i - 1] in OnCalculate(); iClose(_Symbol, _Period, 1) in an EAOnCalculate() arrays count from the oldest bar unless you call ArraySetAsSeries().
Full example: ta.crossover and ta.crossunder
bar_indexi in an OnCalculate() loop; Bars(_Symbol, _Period) - 1 - shift in an EABoth count from 0 on the oldest loaded bar, but the oldest loaded bar differs between platforms.
barstate.isconfirmedIndicator: decide on bar rates_total - 2. EA: on the first tick of a new bar, read shift 1MQL5 has no last-tick-of-the-bar event: a bar is known to be closed when the next one starts.
Full example: barstate.isconfirmed and new bars
barstate.isnewiTime(_Symbol, _Period, 0) != lastBarTime, with lastBarTime kept in a globalrates_total > prev_calculated is not a new-bar test: it is also true on the first call and after history loads.
Full example: barstate.isconfirmed and new bars
barstate.islast, last_bar_indexi == rates_total - 1, rates_total - 1The last bar is the forming one.
na, na(x), nz(x)EMPTY_VALUE in buffers, x == EMPTY_VALUE, x == EMPTY_VALUE ? 0.0 : xSet PLOT_EMPTY_VALUE so the terminal skips it. Functions signal failure by their return value instead.
Built-in indicators (ta.*)
Pine Script v6MQL5Note
ta.sma(close, n)iMA(_Symbol, _Period, n, 0, MODE_SMA, PRICE_CLOSE) read with CopyBuffer()Same formula. Create the handle once, in OnInit().
Full example: ta.sma, ta.ema, ta.rsi and ta.atr
ta.ema(close, n)iMA(_Symbol, _Period, n, 0, MODE_EMA, PRICE_CLOSE)Same weight, 2 / (n + 1); the first values are seeded differently, so compare away from the start of history.
Full example: ta.sma, ta.ema, ta.rsi and ta.atr
ta.rma(src, n), ta.wma(src, n)iMA(..., n, 0, MODE_SMMA, price), iMA(..., n, 0, MODE_LWMA, price)MT5's smoothed moving average is Wilder's average, the one ta.rma() computes.
ta.rsi(close, n)iRSI(_Symbol, _Period, n, PRICE_CLOSE)Both use Wilder's smoothing.
Full example: ta.sma, ta.ema, ta.rsi and ta.atr
ta.atr(n)Wilder's average of the true range, computed in your code. iATR() is a different ATRiATR() takes a simple average of the true range; ta.atr() uses ta.rma(). The most common "my values don't match".
Full example: ta.sma, ta.ema, ta.rsi and ta.atr
ta.tr(true)MathMax(high[i], close[i - 1]) - MathMin(low[i], close[i - 1])On the first bar, where there is no previous close, use high[i] - low[i].
[macd, signal, hist] = ta.macd(close, 12, 26, 9)iMACD(_Symbol, _Period, 12, 26, 9, PRICE_CLOSE): buffer 0 = MACD, buffer 1 = signalMT5's signal line is a simple average of the MACD (Pine: an EMA), and there is no histogram buffer: compute MACD minus signal.
[mid, up, lo] = ta.bb(close, 20, 2)iBands(_Symbol, _Period, 20, 0, 2.0, PRICE_CLOSE): buffers BASE_LINE, UPPER_BAND, LOWER_BANDBoth: a simple average plus or minus the population standard deviation.
Full example: plot, fill and plotshape
k = ta.sma(ta.stoch(close, high, low, 14), 3), d = ta.sma(k, 3)iStochastic(_Symbol, _Period, 14, 3, 3, MODE_SMA, STO_LOWHIGH): buffer 0 = %K, buffer 1 = %DMT5's slowing divides sums, Σ(close − lowest) / Σ(highest − lowest), instead of averaging %K: close, not identical.
ta.highest(high, n), ta.lowest(low, n)iHigh(_Symbol, _Period, iHighest(_Symbol, _Period, MODE_HIGH, n, shift)); in OnCalculate() high[ArrayMaximum(high, i - n + 1, n)]Both include the current bar. Start at shift 1 to leave the forming bar out.
ta.crossover(a, b), ta.crossunder(a, b)a[i] > b[i] && a[i - 1] <= b[i - 1], a[i] < b[i] && a[i - 1] >= b[i - 1]No built-in function: write the comparison, on closed bars for signals.
Full example: ta.crossover and ta.crossunder
ta.change(x, n)x[i] - x[i - n]Guard i - n >= 0: a negative index stops the program with "array out of range".
ta.valuewhen(cond, src, k), ta.barssince(cond)A loop back from bar i that counts the bars where the condition heldStore the condition in a calculation buffer (INDICATOR_CALCULATIONS) so the loop can read past bars.
ta.pivothigh(high, l, r), ta.pivotlow(low, l, r)A loop: bar i - r is a pivot if it beats the l bars before it and the r bars after itKnown only r bars after the pivot, in both languages.
Full example: ta.pivothigh and ta.pivotlow
ta.vwap(hlc3)No built-in: add up typical price × volume and volume since the day started, then divideUse tick_volume[] on forex and CFDs. MT5's day starts at the broker's server midnight.
math.abs(), math.max(), math.min(), math.pow(), math.sqrt(), math.log()MathAbs(), MathMax(), MathMin(), MathPow(), MathSqrt(), MathLog()MathLog() is the natural log, like math.log(). math.round(x, 2) becomes NormalizeDouble(x, 2).
Time, symbol, timeframe and other data
Pine Script v6MQL5Note
timetime[i] in OnCalculate(), iTime(_Symbol, _Period, shift)Pine: milliseconds since 1970, in UTC. MQL5 datetime: seconds, in the broker's server time.
hour, minute, dayofweekMqlDateTime dt; TimeToStruct(time[i], dt); then dt.hour, dt.min, dt.day_of_weekPine numbers days 1 (Sunday) to 7; MQL5 0 (Sunday) to 6. Pine uses the exchange time zone, MQL5 server time.
Full example: strategy.entry to CTrade
syminfo.ticker, syminfo.tickerid_SymbolBroker symbol names vary: EURUSD, EURUSD.m, EURUSDpro.
syminfo.mintickSymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE)Often equal to _Point, not always (indices, metals): round prices to the tick size.
Full example: strategy.exit stop loss and take profit
syminfo.pointvalueSymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE) / SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE)MT5's tick value is for one lot, in the account currency.
Full example: Worked port: EMA cross with ATR stop
timeframe.period, timeframe.in_seconds()_Period, PeriodSeconds()_Period is an ENUM_TIMEFRAMES value such as PERIOD_H1.
request.security(syminfo.tickerid, "D", close[1], lookahead = barmerge.lookahead_on)EA: iClose(_Symbol, PERIOD_D1, 1). Indicator: the daily close at iBarShift(_Symbol, PERIOD_D1, time[i]) + 1The last completed daily bar, the same on history and in real time.
Full example: request.security on a higher timeframe
request.security(syminfo.tickerid, "D", ta.ema(close, 20)[1], lookahead = barmerge.lookahead_on)iMA(_Symbol, PERIOD_D1, 20, 0, MODE_EMA, PRICE_CLOSE) read with CopyBuffer() at the same shiftA handle can be created on any timeframe; its buffer is indexed by that timeframe's bars.
Full example: request.security on a higher timeframe
request.security("FX:GBPUSD", timeframe.period, close)iClose("GBPUSD", _Period, shift) after SymbolSelect("GBPUSD", true)The symbol must be in Market Watch, under your broker's name for it.
Strategies and orders
Pine Script v6MQL5Note
strategy(...)An Expert Advisor with #include <Trade/Trade.mqh> and a CTrade objectInitial capital and commission are Strategy Tester settings in MT5, not code.
Full example: strategy.entry to CTrade
strategy.entry("Long", strategy.long)trade.Buy(lots, _Symbol) after closing any open sellPine reverses an open short; on a hedging account MT5 opens a second position instead.
Full example: strategy.entry to CTrade
strategy.exit("x", "Long", stop = s, limit = l)trade.Buy(lots, _Symbol, price, sl, tp), or trade.PositionModify(ticket, sl, tp) laterStops sit on the broker's server and must respect SYMBOL_TRADE_STOPS_LEVEL.
Full example: strategy.exit stop loss and take profit
strategy.exit("x", "Long", loss = n, profit = m)sl = fill - n * tickSize, tp = fill + m * tickSize, rounded to the tick sizeTicks from the fill price, in both.
Full example: strategy.exit stop loss and take profit
strategy.exit(..., trail_points = p, trail_offset = o)Move the stop yourself in OnTick() with trade.PositionModify()MT5's trailing stop in the terminal's menu is not available to EAs.
strategy.close("Long"), strategy.close_all()trade.PositionClose(ticket) for each position with your symbol and magic numberPositionClose(_Symbol) closes one position and ignores the magic number.
Full example: strategy.entry to CTrade
strategy.position_size, strategy.position_avg_priceSum of POSITION_VOLUME (buys +, sells −) and POSITION_PRICE_OPEN over your positionsLoop with PositionGetTicket(), filter by POSITION_SYMBOL and POSITION_MAGIC.
Full example: strategy.entry to CTrade
strategy.entry(..., limit = p), strategy.entry(..., stop = p)trade.BuyLimit(), trade.BuyStop() (pending orders)Pending orders also need the stops-level distance from the price.
qty, default_qty_value, strategy.equityLots rounded to SYMBOL_VOLUME_STEP within SYMBOL_VOLUME_MIN and SYMBOL_VOLUME_MAX; AccountInfoDouble(ACCOUNT_EQUITY)On TradingView forex 1 is one unit; one MT5 lot is usually 100,000 units. Equity is the whole account's.
Full example: Worked port: EMA cross with ATR stop
Inputs
Pine Script v6MQL5Note
input.int(9, "Fast EMA", minval = 1), input.float(), input.bool(), input.color()input int InpFastLen = 9; // Fast EMA, input double, input bool, input colorThe comment is the label. No minval: check in OnInit() and return INIT_PARAMETERS_INCORRECT.
Full example: ta.crossover and ta.crossunder
input.string("EMA", options = ["EMA", "SMA"])An enum and an input of that typeA comment after each enum value becomes its label in the dropdown.
Full example: alertcondition and alert
input.timeframe("D"), input.source(close)input ENUM_TIMEFRAMES InpTf = PERIOD_D1;, input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE;Built-in indicator functions take both directly.
Full example: request.security on a higher timeframe
Drawing and alerts
Pine Script v6MQL5Note
plot(x)An indicator buffer: SetIndexBuffer() with a DRAW_LINE plotDeclare the counts with #property indicator_buffers and indicator_plots.
Full example: plot, fill and plotshape
plot(x, color = cond ? c1 : c2)DRAW_COLOR_LINE plus an INDICATOR_COLOR_INDEX bufferThe colors are listed once; the second buffer holds each bar's color number.
Full example: plot, fill and plotshape
plot(x, display = display.data_window)A DRAW_NONE plotShows the value in the Data Window without drawing it: handy to compare numbers.
Full example: ta.sma, ta.ema, ta.rsi and ta.atr
plotshape(), plotchar()DRAW_ARROW with PLOT_ARROW (a Wingdings code)Store a price where the symbol goes and EMPTY_VALUE elsewhere.
Full example: plot, fill and plotshape
fill(p1, p2, color)DRAW_FILLING, which takes two buffers of its ownMT5 has no transparency: choose a pale color.
Full example: plot, fill and plotshape
hline(70)#property indicator_level1 70, or IndicatorSetDouble(INDICATOR_LEVELVALUE, 0, 70) for an inputFull example: alertcondition and alert
label.new(), line.new(), box.new(), table.new()ObjectCreate() with OBJ_TEXT, OBJ_TREND, OBJ_RECTANGLE; a panel of OBJ_LABEL objects or Comment()Objects need unique names and should be deleted in OnDeinit().
alertcondition(cond, title, message)No equivalent: code the alert with Alert(), SendNotification() or SendMail()The terminal must be running for the alert to fire.
Full example: alertcondition and alert
alert(msg, alert.freq_once_per_bar_close)Alert(msg) once per bar, judged on the bar that just closedRemember the bar's time so later ticks don't repeat it.
Full example: alertcondition and alert
log.info(), runtime.error()Print(), PrintFormat(); return INIT_FAILED or ExpertRemove()Output goes to the Experts tab (live) or the Journal (Strategy Tester).
Variables, types and functions
Pine Script v6MQL5Note
varIndicator: a value carried from the previous bar's buffer element. EA: a global or static variablePine rolls var back on each real-time tick; MQL5 variables never roll back.
Full example: var and varip
varipA global variableEvery MQL5 global keeps each tick's change, like varip.
Full example: var and varip
f(x) => x * 2double f(const double x) { return x * 2; }Types are explicit, and a function sees no hidden series history: pass arrays and indexes.
type (user-defined type), methodstruct or class, with member functions
array.new<float>(), array.push(), array.get(), array.size()double a[];, ArrayResize() then assign, a[i], ArraySize()array.shift() becomes ArrayRemove(a, 0, 1).
matrix.new<float>(2, 3)matrix m(2, 3); (and vector)Native types with methods such as m.Transpose() and v.Sum().
map.new<string, float>()CHashMap<string, double> from <Generic/HashMap.mqh>
str.tostring(x, "#.##"), str.format()DoubleToString(x, 2), StringFormat()Full example: alertcondition and alert

Construct by construct

The constructs that are hardest to port, each with a Pine Script v6 script, the complete MQL5 program, a line-by-line mapping, the pitfalls and how to check that both give the same values.

Is there a Pine Script to MQL5 converter?

Tools that translate Pine Script to MQL5 line by line exist, and they can get the syntax right. What they can't translate is the execution model: a series that becomes an array counted the other way, a var that loses its rollback, a strategy that reverses on TradingView and locks two positions on a hedging account, an iATR() that isn't ta.atr(). Those are the bugs that compile. Porting construct by construct, then comparing values bar by bar, is the reliable way, and it's what these pages show.

If you'd rather not port by hand, Edgoro's MT5 EA generator can write the MQL5 version from a plain-English description of your rules, compiled in MetaEditor before you get it; test it on a demo account like any port. For a shorter walk-through of one conversion, read how to convert Pine Script to MQL5.

How the code on these pages was checked

  • Every MQL5 program was compiled in MetaEditor 5 on November 12, 2026 with 0 errors and 0 warnings. A clean build shows that the code compiles, not that a strategy makes money: the Expert Advisors that send orders say so, and belong on a demo account first.
  • Every Pine Script was checked by Edgoro Pine Check, our static checker for Pine Script v6, with no errors and no warnings. Pine Check is not TradingView's compiler, and we have not run these scripts in TradingView: if the Pine Editor reports an error, the Pine Script error library explains it.
  • Statements about how MetaTrader 5's built-in indicators calculate come from the indicator sources shipped with the terminal and the MQL5 Reference; statements about Pine come from TradingView's Pine Script v6 manual and reference.

More complete programs: the MQL5 examples (orders, new bars, alerts, chart objects) and the Pine Script examples. When MQL5 code fails, the MQL5 error library has the cause and a fix for each return code and compiler message.

Want the MQL5 version without porting it yourself?

Describe your strategy in plain English and Edgoro writes the MetaTrader 5 Expert Advisor, compiled in MetaEditor, or the TradingView version in Pine Script v6, checked by Edgoro Pine Check. A clean build isn't proof that a strategy works: test it on a demo account.