The EMA crossover is the "hello world" of MetaTrader Expert Advisors: when a fast exponential moving average crosses above a slow one, you go long; when it crosses below, you go short. It is simple to reason about and it exposes almost every MQL5 rule a real EA has to get right — indicator handles, buffer copying, new-bar timing, and safe order placement. This is a complete, compiling example you can drop into MetaEditor.
On each newly closed bar, read a 12-period and 26-period EMA. If the fast EMA was at or below the slow one on the previous bar and is now above it, open a buy with a fixed stop loss and take profit. Mirror it for sells. Only one position at a time.
If you are coming from MQL4 or from a chatbot snippet, these are the parts that usually break:
iMA(...) once in OnInit() to get an int handle, then read values with CopyBuffer(). You never call iMA() inside OnTick().ArraySetAsSeries(buf, true) so index 0 is the most recent bar, then check CopyBuffer() returned the count you asked for.1 and 2 (the last two *closed* bars) avoids acting on a half-formed candle. A new-bar check keeps OnTick() from firing thousands of times.CTrade and set the filling mode. trade.SetTypeFillingBySymbol(_Symbol) avoids the "unsupported filling mode" rejections that kill orders on some brokers.pips * _Point * 10.#property strict
#include <Trade/Trade.mqh>
input int InpFastPeriod = 12;
input int InpSlowPeriod = 26;
input double InpLots = 0.10;
input int InpSLPips = 300;
input int InpTPPips = 600;
input long InpMagic = 20260913;
int h_fast = INVALID_HANDLE, h_slow = INVALID_HANDLE;
datetime g_lastBar = 0;
CTrade c_trade;
int OnInit()
{
h_fast = iMA(_Symbol, _Period, InpFastPeriod, 0, MODE_EMA, PRICE_CLOSE);
h_slow = iMA(_Symbol, _Period, InpSlowPeriod, 0, MODE_EMA, PRICE_CLOSE);
if(h_fast == INVALID_HANDLE || h_slow == INVALID_HANDLE) return(INIT_FAILED);
c_trade.SetExpertMagicNumber(InpMagic);
c_trade.SetTypeFillingBySymbol(_Symbol);
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
if(h_fast != INVALID_HANDLE) { IndicatorRelease(h_fast); h_fast = INVALID_HANDLE; }
if(h_slow != INVALID_HANDLE) { IndicatorRelease(h_slow); h_slow = INVALID_HANDLE; }
}
bool IsNewBar()
{
datetime t = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE);
if(t == g_lastBar) return(false);
g_lastBar = t;
return(true);
}
bool HasPosition()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(PositionSelectByTicket(ticket))
if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
PositionGetInteger(POSITION_MAGIC) == InpMagic)
return(true);
}
return(false);
}
void OnTick()
{
if(!IsNewBar()) return;
double fast[2], slow[2];
ArraySetAsSeries(fast, true);
ArraySetAsSeries(slow, true);
if(CopyBuffer(h_fast, 0, 1, 2, fast) < 2) return;
if(CopyBuffer(h_slow, 0, 1, 2, slow) < 2) return;
if(HasPosition()) return;
double pip = _Point * ((_Digits == 3 || _Digits == 5) ? 10 : 1);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
bool crossUp = (fast[1] <= slow[1]) && (fast[0] > slow[0]);
bool crossDown = (fast[1] >= slow[1]) && (fast[0] < slow[0]);
if(crossUp)
{
double sl = NormalizeDouble(ask - InpSLPips * pip, _Digits);
double tp = NormalizeDouble(ask + InpTPPips * pip, _Digits);
c_trade.Buy(InpLots, _Symbol, ask, sl, tp);
}
else if(crossDown)
{
double sl = NormalizeDouble(bid + InpSLPips * pip, _Digits);
double tp = NormalizeDouble(bid - InpTPPips * pip, _Digits);
c_trade.Sell(InpLots, _Symbol, bid, sl, tp);
}
}
fast[0] and fast[1], not fast[1] and fast[2]CopyBuffer(h_fast, 0, 1, 2, fast) copies two values starting at bar index 1 — the last two closed bars. Because the array is a series, fast[0] is the most recently closed bar and fast[1] is the one before it. Comparing those two is a clean crossover test that never peeks at the still-forming current bar (index 0 in chart terms), which is the classic way backtests end up looking better than reality.
We compiled the EA above and ran it in the genuine MetaTrader 5 Strategy Tester — no tuning, defaults as shown.
Twelve trades in three years is the honest headline here: a plain 12/26 EMA cross that also waits for a flat position fires rarely on H1. That is a *feature* of the example, not a bug — it shows why you backtest before believing anything, and why a low trade count means the result is not statistically strong on its own. Past performance does not guarantee future results.
The example above is what Ranar Algo produces when you describe a strategy in plain English — "EMA 12/26 crossover on EURUSD H1, 300-pip stop, 600-pip target, one position at a time." It writes the MQL5, compile-checks it against real MetaEditor until it builds clean, and runs the same MetaTrader 5 Strategy Tester backtest you saw here, then hands back the report and the full source. You keep your IP and own the code. The tool is still improving and feedback is genuinely welcome — it is a self-serve helper for building and testing ideas, not a signal service or a promise of profit.
Change InpFastPeriod/InpSlowPeriod and re-test — faster pairs trade more often, slower pairs trade less. Add a trend or ATR filter to cut whipsaws, or replace the fixed stop with an ATR-based one. Whatever you change, run it through the Strategy Tester across several years and market regimes before trusting it, and forward-test on a demo account before risking real capital.