How to Code a MACD EA in MQL5 (Signal Line Crossover Example + Real Backtest)

Published 2026-10-06 · Ranar Algo

MACD is one of the most widely used momentum indicators. The standard interpretation is simple: when the MACD line crosses above the signal line, momentum is turning bullish; when it crosses below, momentum is turning bearish. That simplicity makes it a good candidate for an Expert Advisor — but the difference between "looks good on the chart" and "executes correctly" comes down to handling indicator buffers correctly, acting on closed bars only, and being explicit about trade management.

The strategy in one sentence

Buy when the MACD line crosses above the signal line on a closed bar. Sell when it crosses below the signal line on a closed bar. Exit the existing position when the opposite cross occurs (one position at a time). This is the classic signal-line crossover, with a hard stop loss and optional take profit.

Getting MACD values in MQL5 (buffers matter)

In MQL5 you get MACD values via iMACD(), which returns a handle. You then read values with CopyBuffer(). For MACD, the buffer numbering is:

Many people confuse these or assume the order is different. Getting the buffer order wrong is one of the silent compile/runtime bugs that makes an EA never trade. Here is the correct handle creation:

h_Macd = iMACD(_Symbol, _Period, 12, 26, 9, PRICE_CLOSE);

Then read the last three closed bars (so you can detect a cross between bar[2] and bar[1]):

ArraySetAsSeries(g_main, true);
ArraySetAsSeries(g_sig,  true);
if(CopyBuffer(h_Macd, 0, 0, 3, g_main) < 3) return; // main line
if(CopyBuffer(h_Macd, 1, 0, 3, g_sig)  < 3) return; // signal line

With ArraySetAsSeries(..., true), index 0 is the current (forming) bar, index 1 is the last closed bar, and index 2 is the bar before it.

Act on closed bars only (new-bar detection)

If you evaluate on every tick, a crossover that forms mid-bar can trigger repeatedly, creating duplicate orders or churn. Gate entries/exits to once per new closed bar:

bool IsNewBar()
  {
   datetime t = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE);
   if(t == g_lastBar) return(false);
   g_lastBar = t;
   return(true);
  }

void OnTick()
  {
   if(!IsNewBar()) return;
   // ... read buffers and trade
  }

This keeps the logic deterministic and matches what you'd visually validate on a chart.

Detecting a clean crossover

A crossover means the lines cross: the main line goes from below/at to above the signal line (or vice versa). Using closed bars avoids false positives from intra-bar flicker:

bool crossUp = (g_main[2] <= g_sig[2] && g_main[1] > g_sig[1]); // below/flat -> above
bool crossDn = (g_main[2] >= g_sig[2] && g_main[1] < g_sig[1]); // above/flat -> below

The <=/>= handles the rare flat case without missing a proper cross.

Trade execution with CTrade

Use CTrade for clean order placement and filling. Always set the magic number and let the filling mode follow the symbol's rules (FOK/IOC) to avoid common "invalid trade request" errors:

#include <Trade/Trade.mqh>
CTrade c_Trade;
...
c_Trade.SetExpertMagicNumber(InpMagic);
c_Trade.SetTypeFillingBySymbol(_Symbol);

Entries and exits:

if(HasPosition())
  {
   long type = PositionGetInteger(POSITION_TYPE);
   if(type == POSITION_TYPE_BUY  && crossDn) c_Trade.PositionClose(_Symbol);
   if(type == POSITION_TYPE_SELL && crossUp) c_Trade.PositionClose(_Symbol);
   return;
  }
if(crossUp)   c_Trade.Buy(InpLots, _Symbol, ask, sl, tp);
if(crossDn)   c_Trade.Sell(InpLots, _Symbol, bid, sl, tp);

One position at a time keeps the system simple and avoids pyramiding on the same signal. The exit triggers on the opposite cross — so you stay in the trade as long as momentum continues in that direction.

Risk management (minimal but correct)

The example uses a fixed lot size and explicit SL/TP in points. You can also compute lots by risk percent (balance × risk%), but the key is: always define your risk. Check your broker's point value (5/4 digit symbols) — using _Point and normalizing with _Digits is correct for MT5. Also verify spread is acceptable before entering (especially on lower timeframes) — a simple spread filter avoids entering right as spreads widen.

A real backtest of this exact code

The following numbers come from running the exact compiled EA through the MetaTrader 5 Strategy Tester (no parameter tuning, default 12/26/9 MACD, fixed 0.10 lots, SL 600 pts, TP 900 pts):

This is a useful, honest result: MACD crossovers alone are choppy in ranging periods on EURUSD H1. The high trade count with sub-1.0 profit factor shows the edge is weak for these default parameters — it's a starting hypothesis to test on different symbols/timeframes or to add a trend filter (e.g., price above/below a moving average), not a "set and forget" edge. Past performance does not guarantee future results.

Let the tool write and backtest it for you

If you'd rather describe the rules in plain English than track indicator handles and buffer indices, Ranar Algo does exactly that: you describe the strategy, it generates the MQL5, compile-checks it against real MetaEditor, and runs the same MT5 Strategy Tester backtest — returning the net profit, win rate, profit factor, drawdown and trade count. You keep your IP and can download the full .mq5 source. It's still improving and feedback is welcome — the goal is to remove the boilerplate between an idea and an honest backtest, so bugs don't hide in the glue code.

Takeaways

[Build & backtest your own EA free →](/app)

Build & backtest your own EA free →

Ranar Algo turns a plain-English strategy into a compiled MQL5 Expert Advisor with a real MetaTrader 5 backtest. You keep your IP. Past performance does not guarantee future results.