Learning Center

Convert an EMA crossover strategy from Pine Script to an MQL5 EA

A full worked example: ta.ema and ta.crossover become indicator handles, CopyBuffer and CTrade orders.

The Pine Script original

This strategy buys when the 9 EMA crosses above the 21 EMA and closes when it crosses back below. Pine calculates both averages for you on every bar.

Pine Script

//@version=5
strategy("EMA Cross", overlay=true)
fast = ta.ema(close, 9)
slow = ta.ema(close, 21)
if ta.crossover(fast, slow)
    strategy.entry("Long", strategy.long)
if ta.crossunder(fast, slow)
    strategy.close("Long")

Step 1: create the indicator handles once

In MQL5 you do not call an EMA function on every bar. You create a handle with iMA in OnInit, read values with CopyBuffer, and release the handle in OnDeinit. Creating handles inside OnTick slows the terminal and can run out of resources.

Step 2: detect the cross on closed bars

Pine strategies normally act when a bar closes. To match that, compare bar 1 (last closed bar) with bar 2 (the one before) and act only once per new bar. Reading bar 0 would fire and un-fire signals while the candle is still moving.

MQL5

#include <Trade/Trade.mqh>
CTrade trade;
input int FastLen = 9, SlowLen = 21;
input double Lots = 0.10;
int hFast, hSlow;
datetime lastBar = 0;

int OnInit(){
   hFast = iMA(_Symbol, _Period, FastLen, 0, MODE_EMA, PRICE_CLOSE);
   hSlow = iMA(_Symbol, _Period, SlowLen, 0, MODE_EMA, PRICE_CLOSE);
   if(hFast == INVALID_HANDLE || hSlow == INVALID_HANDLE) return INIT_FAILED;
   return INIT_SUCCEEDED;
}
void OnDeinit(const int reason){ IndicatorRelease(hFast); IndicatorRelease(hSlow); }

void OnTick(){
   datetime t = iTime(_Symbol, _Period, 0);
   if(t == lastBar) return;          // once per bar, like Pine
   lastBar = t;
   double f[], s[];
   ArraySetAsSeries(f, true); ArraySetAsSeries(s, true);
   if(CopyBuffer(hFast, 0, 1, 2, f) < 2 || CopyBuffer(hSlow, 0, 1, 2, s) < 2) return;
   bool crossUp   = f[1] <= s[1] && f[0] > s[0];   // f[0] = bar 1, f[1] = bar 2
   bool crossDown = f[1] >= s[1] && f[0] < s[0];
   bool hasLong = PositionSelect(_Symbol) && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
   if(crossUp && !hasLong) trade.Buy(Lots, _Symbol);
   if(crossDown && hasLong) trade.PositionClose(_Symbol);
}

What to check after converting

Results will not match TradingView exactly. Pine fills strategy orders at the next bar open by default; MetaTrader fills at the live price with your broker's spread. Compare signal times first, then trades. Run the EA in the Strategy Tester on the same symbol and timeframe before going near a live account.

Related code snippets