Edgoro
Pine Script v6 → MQL5 custom indicator

ta.crossover and ta.crossunder in MQL5: EMA cross signals

Short answer

ta.crossover(a, b) is true on the bar where a is above b after being at or below it on the bar before. MQL5 has no such function: read both series (here from iMA() handles with CopyBuffer()) and write the test yourself, a[i] > b[i] && a[i - 1] <= b[i - 1], with i counting from the oldest bar as in OnCalculate().

Pineta.crossoverta.crossunderta.emaplotshapeinput.int

MQL5iMACopyBufferBarsCalculatedSetIndexBufferDRAW_ARROW

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

The Pine Script v6 code

An overlay indicator: a 9 and a 21 EMA, with a triangle under the bar where the fast one crosses above and over the bar where it crosses below.

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
indicator("EMA crossover signals", overlay = true)

fastLen = input.int(9, "Fast EMA", minval = 1)
slowLen = input.int(21, "Slow EMA", minval = 1)

fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)

// true on the bar where fast goes from at-or-below slow to above it
crossUp = ta.crossover(fast, slow)
crossDown = ta.crossunder(fast, slow)

plot(fast, "Fast EMA", color.teal, 2)
plot(slow, "Slow EMA", color.orange, 2)
plotshape(crossUp, "Cross up", shape.triangleup, location.belowbar, color.teal, size = size.small)
plotshape(crossDown, "Cross down", shape.triangledown, location.abovebar, color.red, size = size.small)

The MQL5 program

A custom indicator with the same two EMAs and an arrow on each cross. It only draws; it never trades.

Save it as MQL5/Indicators/ta-crossover.mq5 and compile it in MetaEditor (F7): 0 errors, 0 warnings.

MQL5
//+------------------------------------------------------------------+
//| EMA crossover signals: ta.crossover / ta.crossunder in MQL5       |
//| Custom indicator: save in MQL5/Indicators and compile (F7).      |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property indicator_buffers 4
#property indicator_plots   4

#property indicator_label1  "Fast EMA"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrTeal
#property indicator_width1  2

#property indicator_label2  "Slow EMA"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrOrange
#property indicator_width2  2

#property indicator_label3  "Cross up"
#property indicator_type3   DRAW_ARROW
#property indicator_color3  clrTeal

#property indicator_label4  "Cross down"
#property indicator_type4   DRAW_ARROW
#property indicator_color4  clrRed

input int InpFastLen = 9;   // Fast EMA
input int InpSlowLen = 21;  // Slow EMA

double FastBuf[], SlowBuf[], UpBuf[], DownBuf[];
int    fastHandle = INVALID_HANDLE;
int    slowHandle = INVALID_HANDLE;

int OnInit()
  {
   if(InpFastLen < 1 || InpSlowLen < 1)
      return INIT_PARAMETERS_INCORRECT;

   SetIndexBuffer(0, FastBuf, INDICATOR_DATA);
   SetIndexBuffer(1, SlowBuf, INDICATOR_DATA);
   SetIndexBuffer(2, UpBuf, INDICATOR_DATA);
   SetIndexBuffer(3, DownBuf, INDICATOR_DATA);

   PlotIndexSetInteger(2, PLOT_ARROW, 233);        // Wingdings up arrow
   PlotIndexSetInteger(3, PLOT_ARROW, 234);        // Wingdings down arrow
   PlotIndexSetInteger(2, PLOT_ARROW_SHIFT, 12);   // pixels below the low
   PlotIndexSetInteger(3, PLOT_ARROW_SHIFT, -12);  // pixels above the high
   for(int p = 0; p < 4; p++)
     {
      PlotIndexSetDouble(p, PLOT_EMPTY_VALUE, EMPTY_VALUE); // EMPTY_VALUE plays the role of Pine's na
      PlotIndexSetInteger(p, PLOT_DRAW_BEGIN, InpSlowLen);
     }

   // ta.ema() becomes a handle, 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)
     {
      Print("Could not create the EMA handles, error ", GetLastError());
      return INIT_FAILED;
     }
   return INIT_SUCCEEDED;
  }

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

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   // The EMAs are calculated by the terminal: wait until they cover every bar
   if(BarsCalculated(fastHandle) < rates_total || BarsCalculated(slowHandle) < rates_total)
      return prev_calculated;

   // First call: every bar. Later calls: the last finished bar and the forming one
   int start = prev_calculated > 0 ? prev_calculated - 1 : 0;
   int count = rates_total - start;

   double fast[], slow[];  // not series: [0] is the oldest of the copied values
   if(CopyBuffer(fastHandle, 0, 0, count, fast) != count || CopyBuffer(slowHandle, 0, 0, count, slow) != count)
      return prev_calculated;  // data not ready: try again on the next tick

   for(int k = 0; k < count; k++)
     {
      int i = start + k;  // i counts from the oldest bar, like Pine's bar_index
      FastBuf[i] = fast[k];
      SlowBuf[i] = slow[k];
      UpBuf[i]   = EMPTY_VALUE;
      DownBuf[i] = EMPTY_VALUE;
      if(i < InpSlowLen)
         continue;  // warm-up bars: Pine's values are na or still settling here

      // ta.crossover(fast, slow): above now, at or below on the previous bar
      if(FastBuf[i] > SlowBuf[i] && FastBuf[i - 1] <= SlowBuf[i - 1])
         UpBuf[i] = low[i];
      // ta.crossunder(fast, slow): below now, at or above on the previous bar
      if(FastBuf[i] < SlowBuf[i] && FastBuf[i - 1] >= SlowBuf[i - 1])
         DownBuf[i] = high[i];
     }
   return rates_total;
  }
//+------------------------------------------------------------------+

Line by line

Pine Script v6
indicator("EMA crossover signals", overlay = true)
MQL5
#property indicator_chart_window
#property indicator_buffers 4
#property indicator_plots   4

overlay = true becomes indicator_chart_window. MQL5 also wants the number of buffers (arrays of values) and plots (things drawn) up front: two lines and two arrow series.

Pine Script v6
fastLen = input.int(9, "Fast EMA", minval = 1)
MQL5
input int InpFastLen = 9;   // Fast EMA
...
if(InpFastLen < 1 || InpSlowLen < 1)
   return INIT_PARAMETERS_INCORRECT;

The comment after an input is its label in the settings dialog. There's no minval: the check goes in OnInit().

Pine Script v6
fast = ta.ema(close, fastLen)
MQL5
fastHandle = iMA(_Symbol, _Period, InpFastLen, 0, MODE_EMA, PRICE_CLOSE);
...
CopyBuffer(fastHandle, 0, 0, count, fast)

A built-in indicator is a handle created once in OnInit(); each call copies out the values it needs. Copied into a plain array, they come oldest first, so fast[k] is bar start + k.

Pine Script v6
// Pine loops over the bars for you
MQL5
int start = prev_calculated > 0 ? prev_calculated - 1 : 0;
for(int k = 0; k < count; k++)
  {
   int i = start + k;

All bars on the first call, then only the last finished bar and the forming one. i counts from the oldest bar, like bar_index.

Pine Script v6
crossUp = ta.crossover(fast, slow)
MQL5
if(FastBuf[i] > SlowBuf[i] && FastBuf[i - 1] <= SlowBuf[i - 1])
   UpBuf[i] = low[i];

Pine's definition exactly: strictly above now, at or below one bar earlier. Pine's [1] is i - 1 because these arrays aren't series.

Pine Script v6
plotshape(crossUp, "Cross up", shape.triangleup,
  location.belowbar, color.teal, size = size.small)
MQL5
PlotIndexSetInteger(2, PLOT_ARROW, 233);
PlotIndexSetInteger(2, PLOT_ARROW_SHIFT, 12);
UpBuf[i] = EMPTY_VALUE;  // no arrow on this bar

A DRAW_ARROW plot draws a Wingdings symbol (233 is an up arrow) at the price in the buffer and nothing where the value is EMPTY_VALUE. location.belowbar becomes the bar's low plus a shift in pixels.

Pitfalls

  • Series or not. close[i - 1] is the previous bar only while the array isn't a series. After ArraySetAsSeries(arr, true), index 0 is the newest bar and the previous bar is i + 1: the same test then puts arrows on the wrong bars. On bar 0, i - 1 is -1 and the indicator stops with array out of range: the warm-up skip avoids it.
  • The forming bar. As on Pine's real-time bar, the last bar's arrow can appear and vanish within the bar, because the EMAs move with every tick. For a signal that stays, read the cross on rates_total - 2 (closed bars only).
  • < instead of <=. Writing FastBuf[i - 1] < SlowBuf[i - 1] misses crosses that start from a touch; Pine counts them.
  • The first bars. Pine returns na until an EMA has enough bars, and the two platforms seed the average differently. The program draws nothing for the first InpSlowLen bars; compare signals well inside the history.
  • Data not ready. Just after the indicator loads, the EMA handles may not cover every bar: CopyBuffer() returns fewer values or -1 (error 4806). The program returns prev_calculated and tries again on the next tick rather than drawing from half the data.

Checking that both give the same values

  1. Open the same symbol and timeframe on both platforms, ideally your MT5 broker's own feed on TradingView if it has one.
  2. Pick three recent arrows. Read the two EMA values on that bar in TradingView's Data Window and in MetaTrader 5's (Ctrl+D).
  3. Match bars by time: TradingView shows the chart's time zone (set in the chart settings), MetaTrader 5 the broker's server time.
  4. With the same closes, the EMAs agree to the displayed digits. If they differ by a few points, compare the bar's close first: different prices, not different code, is the usual reason, and a cross close to a touch can then appear on one platform only.

The same test in an Expert Advisor

An Expert Advisor reads bars with shifts, where 0 is the forming bar. Copy two values from shift 1 into a series array and compare: CopyBuffer(fastHandle, 0, 1, 2, fast) with ArraySetAsSeries(fast, true) gives fast[0] (the bar that just closed) and fast[1] (the one before), so the crossover is fast[0] > slow[0] && fast[1] <= slow[1]. Do it once per bar, on the first tick of a new bar, as in the worked port.

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.