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.
#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 fromOnInit()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.lastBarafter 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 = 500is 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
- Size from risk. Replace
InpLotswith a volume computed from the stop distance: MQL5 lot size from risk percent. - Protect open trades. Add break-even or a trailing stop to the management part of
OnTick(). - A trend filter. A third handle, a 200-period average on H4 for example, and a rule that only allows buys above it.
- Fewer trades. Cap entries per day or per session with limit trades per day and a session filter.
- The TradingView version. The EMA crossover indicator shows the same signal in Pine Script, and converting Pine Script to MQL5 explains why the two don't always trade identically.
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.