Edgoro
Worked port · Pine Script v6 → MQL5 expert advisor

Pine strategy to MT5 Expert Advisor: a complete worked port

Short answer

Port it block by block: inputs become input variables, ta.ema() becomes iMA() handles, ta.atr() a Wilder ATR computed from CopyRates(), the risk-based qty a lot size from the symbol's tick value, and strategy.entry() with strategy.exit(stop, limit) a single trade.Buy() or trade.Sell() that carries the stop and target, sent on the first tick after the signal bar.

Pinestrategy()ta.emata.atrstrategy.entrystrategy.exitstrategy.equitysyminfo.pointvalue

MQL5CTrade::BuyCTrade::SelliMACopyBufferCopyRatesSYMBOL_TRADE_TICK_VALUE

  • MQL5: compiled in MetaEditor, 0 errors, 0 warnings
  • Pine: checked by Edgoro Pine Check, not run in TradingView

The Pine Script v6 code

A long-and-short strategy: it enters on an EMA crossover, puts the stop at 2 ATR from the signal bar's close and the target at twice that distance, and sizes each trade to risk 1% of equity.

Checked by Edgoro Pine Check with no errors and no warnings. Pine Check is a static checker, not TradingView's compiler, and this script was not run in TradingView.

Pine Script v6
//@version=6
strategy("EMA cross with ATR stop", overlay = true, initial_capital = 10000, commission_type = strategy.commission.percent, commission_value = 0.01, slippage = 2, pyramiding = 0)

fastLen = input.int(9, "Fast EMA", minval = 1)
slowLen = input.int(21, "Slow EMA", minval = 1)
atrLen = input.int(14, "ATR length", minval = 1)
atrMult = input.float(2.0, "Stop distance (x ATR)", minval = 0.1, step = 0.1)
rewardRisk = input.float(2.0, "Target (x stop distance)", minval = 0.1, step = 0.1)
riskPct = input.float(1.0, "Risk per trade (% of equity)", minval = 0.1, step = 0.1)

fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
atr = ta.atr(atrLen)
longSignal = ta.crossover(fast, slow)
shortSignal = ta.crossunder(fast, slow)

// Size so that a stopped-out trade loses riskPct of equity
stopDist = atr * atrMult
qty = stopDist > 0 ? strategy.equity * riskPct / 100 / (stopDist * syminfo.pointvalue) : 0.0

if longSignal and qty > 0 and strategy.position_size <= 0
    strategy.entry("Long", strategy.long, qty = qty)
    strategy.exit("Long exit", "Long", stop = close - stopDist, limit = close + stopDist * rewardRisk)
if shortSignal and qty > 0 and strategy.position_size >= 0
    strategy.entry("Short", strategy.short, qty = qty)
    strategy.exit("Short exit", "Short", stop = close + stopDist, limit = close - stopDist * rewardRisk)

plot(fast, "Fast EMA", color.teal, 2)
plot(slow, "Slow EMA", color.orange, 2)

The MQL5 program

The Expert Advisor with the same rules: same signals on closed bars, same stop and target prices, a lot size that risks the same share of equity. It sends real orders, so run it in the Strategy Tester or on a demo account first.

Save it as MQL5/Experts/ema-cross-atr-stop-strategy.mq5 and compile it in MetaEditor (F7): 0 errors, 0 warnings.

MQL5
//+------------------------------------------------------------------+
//| EMA cross with ATR stop: MQL5 port of the Pine Script v6         |
//| strategy on edgoro.com/pine-script-to-mql5.                      |
//| Expert Advisor: save in MQL5/Experts and compile (F7).           |
//| It sends real orders: run it in the Strategy Tester or on a demo |
//| account first.                                                   |
//+------------------------------------------------------------------+
#include <Trade/Trade.mqh>

//--- input.int / input.float
input int    InpFastLen    = 9;        // Fast EMA
input int    InpSlowLen    = 21;       // Slow EMA
input int    InpAtrLen     = 14;       // ATR length
input double InpAtrMult    = 2.0;      // Stop distance (x ATR)
input double InpRewardRisk = 2.0;      // Target (x stop distance)
input double InpRiskPct    = 1.0;      // Risk per trade (% of equity)
input ulong  InpMagic      = 20261112; // Magic number
input ulong  InpSlippage   = 20;       // Maximum slippage, points (strategy(slippage = ...))

CTrade   trade;
int      fastHandle  = INVALID_HANDLE;
int      slowHandle  = INVALID_HANDLE;
datetime lastBarTime = 0;

int OnInit()
  {
   if(InpFastLen < 1 || InpSlowLen < 1 || InpAtrLen < 1 || InpAtrMult <= 0.0 || InpRewardRisk <= 0.0 || InpRiskPct <= 0.0)
      return INIT_PARAMETERS_INCORRECT;
   // ta.ema(close, len): built-in iMA handles, created once
   fastHandle = iMA(_Symbol, _Period, InpFastLen, 0, MODE_EMA, PRICE_CLOSE);
   slowHandle = iMA(_Symbol, _Period, InpSlowLen, 0, MODE_EMA, PRICE_CLOSE);
   if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE)
      return INIT_FAILED;
   trade.SetExpertMagicNumber(InpMagic);
   trade.SetDeviationInPoints(InpSlippage);
   trade.SetTypeFillingBySymbol(_Symbol);
   return INIT_SUCCEEDED;
  }

void OnDeinit(const int reason)
  {
   IndicatorRelease(fastHandle);
   IndicatorRelease(slowHandle);
  }

//+------------------------------------------------------------------+
//| ta.atr(len) at bar `shift`: Wilder's RMA of the true range,      |
//| seeded with a simple average. iATR() would give a simple average |
//| of the true range instead, a different number.                   |
//+------------------------------------------------------------------+
double WilderAtr(const int len, const int shift)
  {
   int count = len * 10 + 1;  // enough history for the seed to stop mattering
   MqlRates r[];
   ArraySetAsSeries(r, false);  // oldest first
   if(CopyRates(_Symbol, _Period, shift, count, r) != count)
      return 0.0;
   double atr = 0.0;
   for(int i = 1; i < count; i++)
     {
      double tr = MathMax(r[i].high, r[i - 1].close) - MathMin(r[i].low, r[i - 1].close);
      if(i <= len)
        {
         atr += tr / len;  // the first `len` true ranges: a simple average
         continue;
        }
      atr = (atr * (len - 1) + tr) / len;
     }
   return atr;
  }

double TickSize()
  {
   double t = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   return t > 0.0 ? t : _Point;
  }

double RoundToTick(const double price)
  {
   double t = TickSize();
   return NormalizeDouble(MathRound(price / t) * t, _Digits);
  }

//+------------------------------------------------------------------+
//| qty = equity * risk% / (stopDist * syminfo.pointvalue), in lots: |
//| money lost per lot if the stop is hit = ticks to stop x tick     |
//| value of one lot.                                                |
//+------------------------------------------------------------------+
double LotsForRisk(const double stopDist)
  {
   double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double step      = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   double minV      = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxV      = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   if(stopDist <= 0.0 || tickValue <= 0.0 || step <= 0.0)
      return 0.0;
   double riskMoney   = AccountInfoDouble(ACCOUNT_EQUITY) * InpRiskPct / 100.0;
   double lossPerLot  = stopDist / TickSize() * tickValue;
   double lots        = MathFloor(riskMoney / lossPerLot / step + 1e-9) * step;
   if(lots < minV)
      return 0.0;  // the risk is too small for the minimum volume: skip rather than over-risk
   return NormalizeDouble(MathMin(lots, maxV), 8);
  }

//--- strategy.position_size for this EA: + long, - short, 0 flat
double PositionSize()
  {
   double size = 0.0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || PositionGetString(POSITION_SYMBOL) != _Symbol || (ulong)PositionGetInteger(POSITION_MAGIC) != InpMagic)
         continue;
      double vol = PositionGetDouble(POSITION_VOLUME);
      size += PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY ? vol : -vol;
     }
   return size;
  }

//--- Closes this EA's positions of one side; false if one stays open
bool CloseSide(const ENUM_POSITION_TYPE type)
  {
   bool ok = true;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || PositionGetString(POSITION_SYMBOL) != _Symbol || (ulong)PositionGetInteger(POSITION_MAGIC) != InpMagic)
         continue;
      if(PositionGetInteger(POSITION_TYPE) != type)
         continue;
      if(!trade.PositionClose(ticket) || trade.ResultRetcode() != TRADE_RETCODE_DONE)
        {
         PrintFormat("Close of #%I64u failed: retcode %u (%s)", ticket, trade.ResultRetcode(), trade.ResultRetcodeDescription());
         ok = false;
        }
     }
   return ok;
  }

//+------------------------------------------------------------------+
//| strategy.entry() + strategy.exit(stop, limit) in one call:       |
//| close the opposite side, then open with the stop and target     |
//| computed from the signal bar's close, as in the Pine script.     |
//+------------------------------------------------------------------+
void Enter(const bool isLong, const double signalClose, const double stopDist)
  {
   if(!CloseSide(isLong ? POSITION_TYPE_SELL : POSITION_TYPE_BUY))
      return;
   double lots = LotsForRisk(stopDist);
   if(lots <= 0.0)
     {
      Print("Volume for this risk is below the symbol's minimum: no trade");
      return;
     }
   double sl = RoundToTick(isLong ? signalClose - stopDist : signalClose + stopDist);
   double tp = RoundToTick(isLong ? signalClose + stopDist * InpRewardRisk : signalClose - stopDist * InpRewardRisk);

   // The market may have gapped past the stop or target since the close: the server would refuse them
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick))
      return;
   double minDist = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point;
   bool valid = isLong ? (tick.bid - sl > minDist && tp - tick.bid > minDist)
                       : (sl - tick.ask > minDist && tick.ask - tp > minDist);
   if(!valid)
     {
      Print("Price already beyond the stop or target computed on the close: no trade");
      return;
     }

   bool sent = isLong ? trade.Buy(lots, _Symbol, tick.ask, sl, tp, "Long")
                      : trade.Sell(lots, _Symbol, tick.bid, sl, tp, "Short");
   if(!sent || trade.ResultRetcode() != TRADE_RETCODE_DONE)
      PrintFormat("%s failed: retcode %u (%s)", isLong ? "Buy" : "Sell", trade.ResultRetcode(), trade.ResultRetcodeDescription());
  }

void OnTick()
  {
   // Pine evaluates on each bar's close: act on the first tick of the next bar, on closed bars only
   datetime barTime = iTime(_Symbol, _Period, 0);
   if(barTime == 0 || barTime == lastBarTime)
      return;

   double fast[], slow[];
   ArraySetAsSeries(fast, true);  // [0] = bar 1 (the closed signal bar), [1] = bar 2
   ArraySetAsSeries(slow, true);
   if(CopyBuffer(fastHandle, 0, 1, 2, fast) != 2 || CopyBuffer(slowHandle, 0, 1, 2, slow) != 2)
      return;
   double atr = WilderAtr(InpAtrLen, 1);
   double signalClose = iClose(_Symbol, _Period, 1);
   if(atr <= 0.0 || signalClose <= 0.0)
      return;  // history not ready: try again on the next tick
   lastBarTime = barTime;

   bool longSignal  = fast[0] > slow[0] && fast[1] <= slow[1];  // ta.crossover(fast, slow)
   bool shortSignal = fast[0] < slow[0] && fast[1] >= slow[1];  // ta.crossunder(fast, slow)
   double stopDist  = atr * InpAtrMult;
   double size      = PositionSize();

   if(longSignal && size <= 0.0)
      Enter(true, signalClose, stopDist);
   if(shortSignal && size >= 0.0)
      Enter(false, signalClose, stopDist);
  }
//+------------------------------------------------------------------+

Side by side, block by block

Pine Script v6
strategy("EMA cross with ATR stop", overlay = true,
  initial_capital = 10000, commission_value = 0.01,
  slippage = 2, pyramiding = 0)
fastLen = input.int(9, "Fast EMA", minval = 1)
MQL5
#include <Trade/Trade.mqh>
input int   InpFastLen  = 9;   // Fast EMA
input ulong InpSlippage = 20;  // Maximum slippage, points
...
if(InpFastLen < 1 || ...) return INIT_PARAMETERS_INCORRECT;

The strategy becomes an Expert Advisor. Initial capital and commission are set in the Strategy Tester, slippage becomes the deviation CTrade accepts, and minval becomes a check in OnInit().

Pine Script v6
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
MQL5
fastHandle = iMA(_Symbol, _Period, InpFastLen, 0,
                 MODE_EMA, PRICE_CLOSE);   // OnInit
CopyBuffer(fastHandle, 0, 1, 2, fast);     // OnTick

Handles are created once. CopyBuffer() from shift 1 reads the closed signal bar (fast[0]) and the bar before it (fast[1]).

Pine Script v6
atr = ta.atr(atrLen)
MQL5
double atr = WilderAtr(InpAtrLen, 1);

iATR() would return a simple average of the true range, a different number from ta.atr(). WilderAtr() copies 10 × len bars with CopyRates() and applies Wilder's smoothing, seeded with a simple average as ta.rma() is.

Pine Script v6
longSignal = ta.crossover(fast, slow)
shortSignal = ta.crossunder(fast, slow)
MQL5
bool longSignal  = fast[0] > slow[0] && fast[1] <= slow[1];
bool shortSignal = fast[0] < slow[0] && fast[1] >= slow[1];

Pine's definition, written out, on the bar that just closed.

Pine Script v6
stopDist = atr * atrMult
qty = stopDist > 0 ? strategy.equity * riskPct / 100
  / (stopDist * syminfo.pointvalue) : 0.0
MQL5
double riskMoney  = AccountInfoDouble(ACCOUNT_EQUITY) * InpRiskPct / 100.0;
double lossPerLot = stopDist / TickSize() * tickValue;
double lots       = MathFloor(riskMoney / lossPerLot / step + 1e-9) * step;

Same idea, other units: Pine sizes in units with the point value of one unit; MQL5 sizes in lots with the tick value of one lot, then rounds down to the volume step. Below the minimum volume the EA skips the trade rather than risk more.

Pine Script v6
if longSignal and qty > 0 and strategy.position_size <= 0
    strategy.entry("Long", strategy.long, qty = qty)
    strategy.exit("Long exit", "Long",
      stop = close - stopDist,
      limit = close + stopDist * rewardRisk)
MQL5
if(longSignal && size <= 0.0)
   Enter(true, signalClose, stopDist);
// Enter(): CloseSide(POSITION_TYPE_SELL), then
trade.Buy(lots, _Symbol, tick.ask, sl, tp, "Long");

One request opens the position with its stop and target, computed from the signal bar's close as in Pine. Closing the open sell first reproduces Pine's reversal on hedging accounts.

Pine Script v6
plot(fast, "Fast EMA", color.teal, 2)
plot(slow, "Slow EMA", color.orange, 2)
MQL5
// An Expert Advisor draws nothing by itself

To see the EMAs, add the crossover indicator to the chart, or two Moving Average indicators.

Pitfalls

  • Stops from the close, fill at the next tick. Both versions compute the stop and target from the signal bar's close. Pine fills at the next bar's open; the EA buys at the Ask a moment later. After a gap through the stop, Pine enters and exits at once, while the EA's check refuses the trade (the server would reject the stop anyway, with 10016 invalid stops).
  • Same risk, different units. strategy.equity is in the strategy's currency and syminfo.pointvalue is per unit; the EA uses the account's equity and the tick value of one lot in the account currency. On EURUSD with a USD account, Pine's 50,000 units are the EA's 0.5 lot.
  • The minimum volume. With a small account or a wide stop, the lot size can fall below SYMBOL_VOLUME_MIN; rounding it up would risk more than 1%, so the EA skips. A volume that isn't a multiple of the step is refused with 10014 invalid volume.
  • Hedging accounts. Without CloseSide(), a short signal while long would leave both positions open on a hedging account.
  • Filling mode. Some brokers only accept one filling type; SetTypeFillingBySymbol() picks an allowed one and avoids 10030 unsupported filling mode.
  • The ATR seed. WilderAtr() starts its average 10 × len bars back; Pine started at the first bar of the chart. The difference is far below the digits the Data Window shows, but only if both read the same prices.

Checking that both give the same values

  1. In TradingView, run the strategy on a symbol your broker also offers (ideally on the broker's own TradingView feed) and open the List of Trades.
  2. In MetaTrader 5, run the EA in the Strategy Tester on the same symbol, timeframe and dates, with "Every tick based on real ticks", a 10,000 deposit in the same currency as the strategy's, and visual mode on.
  3. Match trades by entry bar, converting TradingView's chart time zone to the broker's server time. Same bar and same direction is the first check.
  4. Compare stop and target prices: both are the signal close plus or minus a multiple of ATR, so they match to a few ticks when the ATR matches. To see the EA's ATR, add Print(atr); in OnTick() and compare it with ta.atr(14) in TradingView's Data Window.
  5. Compare sizes last: Pine's quantity in units divided by the contract size (100,000 on most forex symbols) should be close to the EA's lots.

Why the port is an Expert Advisor, not an indicator

The Pine script is a strategy(): it decides trades and the Strategy Tester simulates them. In MetaTrader 5, only an Expert Advisor can send orders, and the MT5 Strategy Tester runs Expert Advisors, so that's what a strategy becomes. The chart lines of the Pine version belong in a separate custom indicator, as on the ta.crossover page.

These programs show one technique each; they are not trading systems, and none of this is a recommendation to trade. Trading involves substantial risk of loss and is not suitable for every investor. Edgoro is a software tool, not a broker or financial adviser: nothing it produces is investment advice, and past or backtested performance does not guarantee future results.

The same topic in each language

All MQL5 errors

Rather not port it by hand?

Edgoro can generate the MQL5 version for you from a plain-English description of the rules, and compiles it in MetaEditor before you get it. Test it on a demo account: a clean build isn't proof that a strategy works.