Edgoro
Expert Advisor · MQL5

MQL5 moving average crossover EA (complete code)

Short answer

Create two iMA() handles in OnInit(), and once per bar copy the last two closed values of each with CopyBuffer(). A cross up is fast[1] <= slow[1] && fast[0] > slow[0] (bars 2 and 1 as series arrays); close an opposite position first, then open the new one with CTrade.

UsesiMACopyBufferCTrade::BuyCTrade::SellCTrade::PositionCloseINIT_PARAMETERS_INCORRECT

The code

A complete Expert Advisor that opens and closes real positions on its own. Test it in the Strategy Tester and on a demo account: a crossover rule is an example, not a recommendation.

Save it as MQL5/Experts/moving-average-crossover-ea.mq5 and compile it in MetaEditor (F7): 0 errors, 0 warnings.

MQL5
#include <Trade/Trade.mqh>

input int            InpFastPeriod = 20;       // Fast MA period
input int            InpSlowPeriod = 50;       // Slow MA period
input ENUM_MA_METHOD InpMaMethod   = MODE_EMA; // MA method
input double         InpLots       = 0.10;     // Volume, lots
input int            InpStopPoints = 500;      // Stop-loss, points (0 = none)
input int            InpTakePoints = 1000;     // Take-profit, points (0 = none)
input bool           InpReverse    = true;     // Close the opposite position on a new cross
input ulong          InpMagic      = 20261112; // Magic number

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

int OnInit()
  {
   if(InpFastPeriod <= 0 || InpFastPeriod >= InpSlowPeriod)
     {
      Print("The fast period must be positive and shorter than the slow period");
      return INIT_PARAMETERS_INCORRECT;
     }
   fastHandle = iMA(_Symbol, PERIOD_CURRENT, InpFastPeriod, 0, InpMaMethod, PRICE_CLOSE);
   slowHandle = iMA(_Symbol, PERIOD_CURRENT, InpSlowPeriod, 0, InpMaMethod, PRICE_CLOSE);
   if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE)
     {
      PrintFormat("iMA failed: error %d", GetLastError());
      return INIT_FAILED;
     }
   trade.SetExpertMagicNumber(InpMagic);
   trade.SetDeviationInPoints(10);
   trade.SetTypeFillingBySymbol(_Symbol);
   return INIT_SUCCEEDED;
  }

void OnDeinit(const int reason)
  {
   if(fastHandle != INVALID_HANDLE)
      IndicatorRelease(fastHandle);
   if(slowHandle != INVALID_HANDLE)
      IndicatorRelease(slowHandle);
  }

// Cross on the last closed bar: +1 fast crossed above slow, -1 below, 0 none. False if the values aren't ready.
bool ReadCross(int &signal)
  {
   double fast[], slow[];
   ArraySetAsSeries(fast, true);
   ArraySetAsSeries(slow, true);
   if(CopyBuffer(fastHandle, 0, 1, 2, fast) != 2 || CopyBuffer(slowHandle, 0, 1, 2, slow) != 2)
      return false;
   signal = 0;
   if(fast[1] <= slow[1] && fast[0] > slow[0])
      signal = 1;
   else if(fast[1] >= slow[1] && fast[0] < slow[0])
      signal = -1;
   return true;
  }

// This EA's position on this symbol: +1 long, -1 short, 0 none
int PositionDirection(ulong &ticket)
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong t = PositionGetTicket(i);
      if(t == 0 || PositionGetString(POSITION_SYMBOL) != _Symbol || PositionGetInteger(POSITION_MAGIC) != (long)InpMagic)
         continue;
      ticket = t;
      return PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY ? 1 : -1;
     }
   ticket = 0;
   return 0;
  }

double RoundToTick(const double price)
  {
   double tick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   if(tick <= 0.0)
      return NormalizeDouble(price, _Digits);
   return NormalizeDouble(MathRound(price / tick) * tick, _Digits);
  }

bool Open(const int direction)
  {
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick))
      return false;
   bool   buy  = direction > 0;
   double exit = buy ? tick.bid : tick.ask;   // stops are checked against the closing price
   double sl   = InpStopPoints > 0 ? RoundToTick(buy ? exit - InpStopPoints * _Point : exit + InpStopPoints * _Point) : 0.0;
   double tp   = InpTakePoints > 0 ? RoundToTick(buy ? exit + InpTakePoints * _Point : exit - InpTakePoints * _Point) : 0.0;
   bool   ok   = buy ? trade.Buy(InpLots, _Symbol, tick.ask, sl, tp, "MA cross")
                     : trade.Sell(InpLots, _Symbol, tick.bid, sl, tp, "MA cross");
   if(!ok || trade.ResultRetcode() != TRADE_RETCODE_DONE)
      PrintFormat("%s failed: retcode %u (%s)", buy ? "Buy" : "Sell", trade.ResultRetcode(), trade.ResultRetcodeDescription());
   return ok;
  }

void OnTick()
  {
   datetime bar = iTime(_Symbol, PERIOD_CURRENT, 0);
   if(bar == 0 || bar == lastBar)
      return;                       // once per bar
   int signal;
   if(!ReadCross(signal))
      return;                       // indicator not ready: retry on the next tick
   lastBar = bar;
   if(signal == 0)
      return;

   ulong ticket;
   int   current = PositionDirection(ticket);
   if(current == signal)
      return;                       // already in the direction of the cross
   if(current != 0)
     {
      if(!InpReverse)
         return;
      if(!trade.PositionClose(ticket) || trade.ResultRetcode() != TRADE_RETCODE_DONE)
        {
         PrintFormat("Close failed: retcode %u (%s)", trade.ResultRetcode(), trade.ResultRetcodeDescription());
         return;                    // don't open the new trade while the old one is still there
        }
     }
   Open(signal);
  }

How it works

  • INIT_PARAMETERS_INCORRECT. A fast period equal to or longer than the slow one makes no sense. Returning this code from OnInit() also tells the optimizer to skip that combination instead of running it.
  • ReadCross(). With series arrays, index 0 is bar 1 (the last closed bar) and index 1 is bar 2. <= on the earlier bar and > on the later one count a cross once, including a touch followed by a cross; equal values on bar 1 are no cross yet.
  • lastBar after a successful read. If the averages aren't ready, the bar isn't marked as done and the next tick tries again.
  • PositionDirection(). Only positions with this EA's magic number on this symbol count, so manual trades and other EAs on the same account are left alone.
  • Close, then open. When the cross goes against the open position, the EA closes it and opens the new trade only if the close succeeded. On a hedging account that avoids a buy and a sell open together; on a netting account it avoids a reversal of the wrong size.
  • Stops from the closing price. The stop and target are measured from the Bid for a buy and the Ask for a sell, then rounded to the tick size.

Pitfalls

  • Trading the cross on bar 0. The averages of the forming bar move with every tick, so a cross can appear and vanish within the bar. The EA then trades signals that are no longer on the chart, and the Strategy Tester's modelling modes disagree.
  • Points, not pips. InpStopPoints = 500 is 50 pips on a five-digit EUR/USD quote, but 5.00 on gold quoted to two digits. Check the distance on each symbol; stops inside the broker's minimum fail with 10016 invalid stops.
  • Not enough history. A 200-period average needs at least 200 bars before it has a value; until then CopyBuffer() fails with 4806, and the EA waits.
  • Whipsaws. In a range, the averages cross back and forth and each cross is a trade with costs. That's the rule's nature, not a bug. A trend filter or fewer trades per day changes it, and only testing tells whether it helps.
  • The wrong filling policy on some brokers. SetTypeFillingBySymbol() avoids hard-coding one; a policy the symbol doesn't accept is refused with 10030.

Variations

Testing it

Because the EA only reads closed bars, the Strategy Tester's fast "Open prices only" mode gives the same signals as the slower modes: use it to explore settings. Then run the settings you keep in "Every tick based on real ticks" to check how the stops and targets behave inside the bars, where the modelling mode matters.

Optimizing two periods on one stretch of history will always find a pair that looks good. Keep a later period aside and check the chosen settings there, as described in walk-forward analysis, before drawing conclusions.

Questions

Which moving average periods work best for a crossover EA?

There's no universal answer: it depends on the symbol, the timeframe and the costs, and the best pair on past data is usually partly luck. Choose a few sensible pairs, test them on data you didn't optimize on, and prefer settings whose neighbours also do reasonably well.

Why does my crossover EA open trades where the chart shows no cross?

Usually because it reads bar 0, where the averages are still moving, or mixes up the order of the copied values (ArraySetAsSeries missing). Read bars 1 and 2, as series arrays, once per bar.

Can I run the same EA on several charts?

Yes. It filters positions by symbol and magic number, so copies on different symbols can share a magic number. Two copies on the same symbol, for example with different periods, each need their own magic number.

This program is a minimal example written to show one technique, not a trading system, and none of it 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.

All MQL5 errors

Want it inside a complete EA?

Want this EA with risk-based sizing, a session filter and a daily loss limit? Describe it in plain English and Edgoro's MT5 EA generator writes it and compiles it in MetaEditor. A clean build proves the code compiles, not that a strategy works: test it on a demo account.