Edgoro
Platforms

How to convert Pine Script to MQL5: a practical guide for TradingView traders

Port a TradingView Pine Script strategy to an MT5 Expert Advisor: how bars, series, indicators and orders map to MQL5, with a worked EMA crossover example.

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 v6MQL5
Script runs once per barOnTick() 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
varA 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_sizeLoop 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()
naEMPTY_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 differenceWhyWhat to do
ATRPine's ta.atr() uses Wilder's smoothing; MT5's iATR uses a simple average of true rangeAccept small differences, or code Wilder's ATR yourself in MQL5
EMA startDifferent seeding of the first valuesLoad enough history; differences fade after a few hundred bars
DataTradingView's feed and your broker's prices differ slightlyCompare signals, not exact prices
Session timesTradingView charts use the exchange or chosen time zone; MT5 uses the broker's server timeRe-express every time filter in server time
Daily barsBrokers on different server times build different daily candlesCheck daily-bar logic on the broker you'll trade
FillsSimulated fills versus real spread, slippage and requotesForward-test on a demo account

Checking the conversion

  1. Pick five or six recent signals on the Pine chart and note their bar times.
  2. 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.
  3. Print the indicator values from the EA at those bars and compare them with the Pine data window.
  4. 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

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.

Build it with Edgoro

Describe your strategy in plain English and get a TradingView indicator in Pine Script v6 or a MetaTrader 5 Expert Advisor in minutes. Every first purchase is covered by a 7-day money-back guarantee.

Build your indicator with Edgoro