You've built and tested an idea in Pine Script and now you want it to trade on its own in MetaTrader 5. There's no converter that turns one into the other reliably, because the two languages don't just use different words: they run differently. A Pine Script strategy is a simulation that steps through bars; an MQL5 Expert Advisor is a program reacting to live ticks and sending real orders.
This guide maps the concepts from one to the other, converts a complete example line by line, and lists the places where the two versions will disagree even when the code is right. The example is for learning, not a strategy recommendation.
The big difference: bars versus ticks
A Pine Script runs once on every historical bar, then again on each update of the live bar. Values like close refer to "the bar being processed", and close[1] reaches back one bar. A strategy's orders fill in a simulated broker.
An MQL5 Expert Advisor has no bar loop. OnTick() is called whenever a new price arrives for the chart's symbol. To get bar-based behaviour you detect a new bar yourself, then read the values of the bars that just closed. Indicators come from handles that you copy values out of, and orders go to the real trade server.
Concept map
| Pine Script v6 | MQL5 |
|---|---|
| Script runs once per bar | OnTick() on every tick, plus your own new-bar check |
close, close[1] | iClose(_Symbol, PERIOD_CURRENT, 1), iClose(..., 2) once the bar has closed (index 0 is the forming bar) |
ta.ema(close, 20) | iMA(_Symbol, PERIOD_CURRENT, 20, 0, MODE_EMA, PRICE_CLOSE) handle, read with CopyBuffer() |
ta.atr(14) | iATR(_Symbol, PERIOD_CURRENT, 14) handle (different smoothing, see below) |
ta.crossover(a, b) | Compare the last two closed values: a[0] > b[0] && a[1] <= b[1] |
input.int(), input.float() | input int, input double |
var | A global or static variable |
strategy.entry() | CTrade::Buy() or CTrade::Sell() |
strategy.exit(stop = ..., limit = ...) | Stop-loss and take-profit on the order, or CTrade::PositionModify() |
strategy.close() | CTrade::PositionClose(ticket) |
strategy.position_size | Loop over PositionsTotal(), filtered by symbol and magic number |
request.security() | A handle created on another timeframe, e.g. iMA(_Symbol, PERIOD_H4, ...) |
alert(), alertcondition() | Alert(), SendNotification(), Print() |
na | EMPTY_VALUE in indicator buffers; check return values of functions |
The Pine Script strategy we'll convert
A long-only EMA crossover with an ATR stop, closed on the opposite cross:
//@version=6
strategy("EMA cross with ATR stop (to convert)", overlay = true, initial_capital = 10000,
default_qty_type = strategy.fixed, default_qty_value = 1)
fastLen = input.int(20, "Fast EMA", minval = 1)
slowLen = input.int(50, "Slow EMA", minval = 2)
atrMult = input.float(2.0, "Stop (x ATR)", minval = 0.5, step = 0.25)
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
atr = ta.atr(14)
crossUp = ta.crossover(fast, slow)
crossDown = ta.crossunder(fast, slow)
var float stopPrice = na
if crossUp and strategy.position_size == 0
stopPrice := close - atrMult * atr
strategy.entry("Long", strategy.long)
if crossDown and strategy.position_size > 0
strategy.close("Long")
strategy.exit("Stop", "Long", stop = stopPrice)
plot(fast, "Fast EMA", color = color.new(color.teal, 0))
plot(slow, "Slow EMA", color = color.new(color.orange, 0))
The MQL5 version
#include <Trade/Trade.mqh>
input int InpFastLen = 20; // Fast EMA
input int InpSlowLen = 50; // Slow EMA
input double InpAtrMult = 2.0; // Stop (x ATR)
input double InpLots = 0.10; // Fixed volume (replace with risk-based sizing)
input ulong InpMagic = 261105; // Magic number
CTrade trade;
int fastHandle = INVALID_HANDLE, slowHandle = INVALID_HANDLE, atrHandle = INVALID_HANDLE;
int OnInit()
{
fastHandle = iMA(_Symbol, PERIOD_CURRENT, InpFastLen, 0, MODE_EMA, PRICE_CLOSE);
slowHandle = iMA(_Symbol, PERIOD_CURRENT, InpSlowLen, 0, MODE_EMA, PRICE_CLOSE);
atrHandle = iATR(_Symbol, PERIOD_CURRENT, 14);
if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE || atrHandle == INVALID_HANDLE)
return INIT_FAILED;
trade.SetExpertMagicNumber(InpMagic);
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
IndicatorRelease(fastHandle);
IndicatorRelease(slowHandle);
IndicatorRelease(atrHandle);
}
// Ticket of this EA's open position on this symbol, or 0 (Pine: strategy.position_size).
ulong OwnPosition()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket != 0 && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == (long)InpMagic)
return ticket;
}
return 0;
}
void OnTick()
{
// Pine evaluates once per closed bar: do the same with a new-bar check.
static datetime lastBar = 0;
datetime barTime = iTime(_Symbol, PERIOD_CURRENT, 0);
if(barTime == lastBar)
return;
lastBar = barTime;
// Index 0 = the bar that just closed (Pine's current bar), index 1 = the one before (Pine's [1]).
double fast[], slow[], atr[];
ArraySetAsSeries(fast, true);
ArraySetAsSeries(slow, true);
ArraySetAsSeries(atr, true);
if(CopyBuffer(fastHandle, 0, 1, 2, fast) != 2 || CopyBuffer(slowHandle, 0, 1, 2, slow) != 2 || CopyBuffer(atrHandle, 0, 1, 1, atr) != 1)
return;
bool crossUp = fast[0] > slow[0] && fast[1] <= slow[1]; // ta.crossover(fast, slow)
bool crossDown = fast[0] < slow[0] && fast[1] >= slow[1]; // ta.crossunder(fast, slow)
ulong ticket = OwnPosition();
if(crossUp && ticket == 0)
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = NormalizeDouble(ask - InpAtrMult * atr[0], _Digits); // strategy.exit(stop = ...)
if(!trade.Buy(InpLots, _Symbol, ask, sl, 0.0, "EMA cross"))
PrintFormat("Buy failed: %u %s", trade.ResultRetcode(), trade.ResultRetcodeDescription());
}
else if(crossDown && ticket != 0)
{
if(!trade.PositionClose(ticket)) // strategy.close("Long")
PrintFormat("Close failed: %u %s", trade.ResultRetcode(), trade.ResultRetcodeDescription());
}
}
What changed, and why
The new-bar check. Without it, OnTick() would evaluate the cross on every tick of the forming bar, which is how EAs end up with five entries in one candle.
Shifted indexes. CopyBuffer(handle, 0, 1, 2, buf) starts at bar 1, the last closed bar. With ArraySetAsSeries, buf[0] is that bar (Pine's current bar at its close) and buf[1] the bar before it (Pine's [1]). Reading bar 0 instead would use the forming bar and reintroduce the live-bar flicker Pine users know as repainting.
Handles. MQL5 indicators are created once in OnInit() and read with CopyBuffer(). Creating them inside OnTick() is a common, slow mistake.
The fill. Pine fills the entry at the next bar's open. The EA sends its order on the first tick of the new bar, which is close to the same moment, but at the ask, with real spread and possible slippage.
The stop. Pine measured the stop from the signal bar's close; the EA measures it from the actual ask. Slightly different numbers, and arguably more realistic. The stop goes with the order, on the server, so it still works if the terminal disconnects.
Position tracking. strategy.position_size becomes a loop over positions filtered by symbol and magic number; see MQL5 magic numbers explained.
Sizing. The fixed InpLots mirrors the Pine example's fixed quantity. For real use, size from risk with a lot-size function.
Values that won't match exactly
| Source of difference | Why | What to do |
|---|---|---|
| ATR | Pine's ta.atr() uses Wilder's smoothing; MT5's iATR uses a simple average of true range | Accept small differences, or code Wilder's ATR yourself in MQL5 |
| EMA start | Different seeding of the first values | Load enough history; differences fade after a few hundred bars |
| Data | TradingView's feed and your broker's prices differ slightly | Compare signals, not exact prices |
| Session times | TradingView charts use the exchange or chosen time zone; MT5 uses the broker's server time | Re-express every time filter in server time |
| Daily bars | Brokers on different server times build different daily candles | Check daily-bar logic on the broker you'll trade |
| Fills | Simulated fills versus real spread, slippage and requotes | Forward-test on a demo account |
Checking the conversion
- Pick five or six recent signals on the Pine chart and note their bar times.
- Run the EA in the Strategy Tester on the same symbol and period, with visual mode on, and check that it trades on the same bars, allowing for the time-zone difference.
- Print the indicator values from the EA at those bars and compare them with the Pine data window.
- Investigate any trade one version takes and the other doesn't. It's usually a session filter, a data difference or an off-by-one bar index.
If you're new to MT5, how to install an Expert Advisor on MT5 covers compiling and attaching the EA, and the EMA crossover indicator is the visual version of the signal.
Sources and further reading
- Pine Script User Manual: Execution model — how a Pine script runs bar by bar.
- MQL5 Reference: Event handling functions —
OnInit(),OnTick()and the other entry points of an EA. - MQL5 Reference: iMA() and CopyBuffer() — indicator handles and buffers, the MQL5 counterpart of
ta.ema(). - MQL5 Reference: CTrade class — sending and closing orders.
Try it with Edgoro
Paste your Pine Script into Edgoro and ask for the MT5 version, or describe the strategy once and get both: a Pine Script v6 strategy to backtest on TradingView and an MQL5 Expert Advisor with the same rules, risk inputs and a magic number. Convert your strategy with Edgoro.
Converted code can behave differently from the original; test both versions before relying on either. Trading involves substantial risk of loss.