Back to MetaTrader 5 MQL5 for CFD Traders
Part 4

The Complete Expert Advisor

Everything in one EA: regime filter, ATR stops, risk sizing, validation, and clean shutdown.

Advanced15 min readBeginner → AdvancedLesson 17 / 17
Try it in MetaEditor
The complete trend-following EA

Save as MQL5/Experts/Lesson17CompleteEA.mq5. DEMO FIRST. Daily regime filter, EMA cross entry, ATR bracket, 1% risk sizing, full validation, and clean shutdown.

//+------------------------------------------------------------------+
//| Lesson 17 - Complete trend-following EA for CFD                  |
//| WARNING: sends real orders. Test on DEMO and understand the risk |
//| lesson before any live use.                                      |
//+------------------------------------------------------------------+
#property copyright "Strategist Academy"
#property version   "1.00"
#property strict

#include <Trade/Trade.mqh>

input group "Signal"
input int FastMAPeriod   = 20;              // Fast EMA
input int SlowMAPeriod   = 50;              // Slow EMA
input int TrendMAPeriod  = 200;             // Regime EMA (higher TF)

input group "Risk"
input double RiskPercent    = 1.0;          // Risk % of equity per trade
input double AtrSLMult      = 2.0;          // Stop = ATR x
input double AtrTPMult      = 4.0;          // Target = ATR x
input int    AtrPeriod      = 14;           // ATR period
input int    MaxSpreadPoints= 30;           // Skip if spread wider than this

input group "Execution"
input ulong MagicNumber     = 17091709;     // Magic number
input int   DeviationPoints = 10;           // Slippage tolerance (points)

CTrade   trade;
int      fastHandle  = INVALID_HANDLE;
int      slowHandle  = INVALID_HANDLE;
int      trendHandle = INVALID_HANDLE;
int      atrHandle   = INVALID_HANDLE;
datetime lastBarTime = 0;

//+------------------------------------------------------------------+
int OnInit()
{
   if(FastMAPeriod >= SlowMAPeriod)
   {
      Print("FastMA must be shorter than SlowMA");
      return(INIT_FAILED);
   }

   // Create handles ONCE. Doing this in OnTick leaks one per tick.
   fastHandle  = iMA(_Symbol, _Period, FastMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   slowHandle  = iMA(_Symbol, _Period, SlowMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   trendHandle = iMA(_Symbol, PERIOD_D1, TrendMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   atrHandle   = iATR(_Symbol, _Period, AtrPeriod);

   if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE ||
      trendHandle == INVALID_HANDLE || atrHandle == INVALID_HANDLE)
   {
      Print("Indicator handle creation failed. Error: ", GetLastError());
      return(INIT_FAILED);
   }

   trade.SetExpertMagicNumber(MagicNumber);
   trade.SetDeviationInPoints(DeviationPoints);
   trade.SetTypeFillingBySymbol(_Symbol);

   Print("Complete EA ready on ", _Symbol, " / ", EnumToString((ENUM_TIMEFRAMES)_Period));
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   if(fastHandle  != INVALID_HANDLE) IndicatorRelease(fastHandle);
   if(slowHandle  != INVALID_HANDLE) IndicatorRelease(slowHandle);
   if(trendHandle != INVALID_HANDLE) IndicatorRelease(trendHandle);
   if(atrHandle   != INVALID_HANDLE) IndicatorRelease(atrHandle);
}

//+------------------------------------------------------------------+
//| Read one value from a handle at a given shift                    |
//+------------------------------------------------------------------+
bool ReadAt(const int handle, const int shift, double &out)
{
   double buf[];
   ArraySetAsSeries(buf, true);
   if(CopyBuffer(handle, 0, 0, shift + 3, buf) < shift + 3)
      return false;
   double v = buf[shift];
   if(v == EMPTY_VALUE || v <= 0.0)
      return false;
   out = v;
   return true;
}

//+------------------------------------------------------------------+
//| Volume rounded DOWN onto the broker's step                       |
//+------------------------------------------------------------------+
double NormalizeVolumeDown(const string sym, double vol)
{
   double vMin  = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
   double vMax  = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
   double vStep = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
   vol = MathMax(vMin, MathMin(vMax, vol));
   if(vStep > 0)
      vol = vMin + MathFloor((vol - vMin) / vStep + 0.0000001) * vStep;
   return NormalizeDouble(vol, 2);
}

//+------------------------------------------------------------------+
//| Lots so a stop of stopPoints costs at most riskMoney             |
//+------------------------------------------------------------------+
double CalcLots(const string sym, const double riskMoney, const double stopPoints)
{
   double tickValue = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE);
   double tickSize  = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);
   if(tickValue <= 0 || tickSize <= 0 || stopPoints <= 0)
      return 0.0;
   double stopTicks  = (stopPoints * _Point) / tickSize;
   double riskPerLot = stopTicks * tickValue;
   if(riskPerLot <= 0)
      return 0.0;
   return NormalizeVolumeDown(sym, riskMoney / riskPerLot);
}

//+------------------------------------------------------------------+
void OnTick()
{
   // --- cheap checks first -------------------------------------------
   datetime t = iTime(_Symbol, _Period, 0);
   if(t == lastBarTime) return;
   lastBarTime = t;

   if(PositionSelect(_Symbol) &&
      (ulong)PositionGetInteger(POSITION_MAGIC) == MagicNumber)
      return;                                    // already in our position

   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

   if((ask - bid) / _Point > MaxSpreadPoints)
   {
      Print("Spread too wide - skipping. Spread=",
            DoubleToString((ask - bid) / _Point, 1), " points");
      return;
   }

   // --- read indicators (shift 1 = last closed bar) -------------------
   double fast[1], slow[1], fastPrev[1], slowPrev[1], trend[1], atr[1];

   if(!ReadAt(fastHandle, 1, fast[0]) || !ReadAt(slowHandle, 1, slow[0]) ||
      !ReadAt(fastHandle, 2, fastPrev[0]) || !ReadAt(slowHandle, 2, slowPrev[0]) ||
      !ReadAt(trendHandle, 1, trend[0]) || !ReadAt(atrHandle, 1, atr[0]))
      return;                                    // not enough data yet

   // --- the signal ----------------------------------------------------
   bool bullRegime = (bid > trend[0]);           // daily trend agrees
   bool crossUp    = (fastPrev[0] <= slowPrev[0] && fast[0] > slow[0]);

   if(!(bullRegime && crossUp))
      return;

   // --- levels --------------------------------------------------------
   double atrVal    = atr[0];
   double stopPrice = NormalizeDouble(bid - atrVal * AtrSLMult, _Digits);
   double tpPrice   = NormalizeDouble(ask + atrVal * AtrTPMult, _Digits);
   double stopPts   = (bid - stopPrice) / _Point;

   long stopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   if(stopPts < (double)stopsLevel)
   {
      Print("Stop ", DoubleToString(stopPts, 0), " pts is inside broker minimum ",
            stopsLevel, " - skipping rather than widening the stop.");
      return;
   }

   // --- sizing --------------------------------------------------------
   double riskMoney = AccountInfoDouble(ACCOUNT_EQUITY) * RiskPercent / 100.0;
   double lots      = CalcLots(_Symbol, riskMoney, stopPts);

   if(lots <= 0)
   {
      Print("Calculated size is zero - symbol limits make this unriskable at ",
            DoubleToString(riskMoney, 2));
      return;
   }

   double margin = 0.0;
   if(OrderCalcMargin(ORDER_TYPE_BUY, _Symbol, lots, ask, margin))
   {
      if(margin > AccountInfoDouble(ACCOUNT_MARGIN_FREE))
      {
         Print("Not enough free margin. Need ", DoubleToString(margin, 2),
               " have ", DoubleToString(AccountInfoDouble(ACCOUNT_MARGIN_FREE), 2));
         return;
      }
   }

   // --- send ----------------------------------------------------------
   bool ok = trade.Buy(lots, _Symbol, 0.0, stopPrice, tpPrice, "lesson17");

   Print("BUY ", ok ? "sent" : "FAILED",
         "  lots=", DoubleToString(lots, 2),
         "  sl=", DoubleToString(stopPrice, _Digits),
         "  tp=", DoubleToString(tpPrice, _Digits),
         "  retcode=", trade.ResultRetcode(),
         " (", trade.ResultRetcodeDescription(), ")");
}
To run it
  1. 1. Open MetaEditor from MT5 (press F4), and pick the folder this program belongs in — Experts, Indicators, or Scripts.
  2. 2. Create a new file, paste this over it, then press F7 to compile. Fix anything the Errors tab reports.
  3. 3. Back in MT5, drag it onto a chart — or open the Strategy Tester if it is an Expert Advisor.

What we are assembling

A long-only trend-following EA for CFD instruments. A daily EMA defines the regime, an EMA cross on the working timeframe triggers entry, ATR sets the stop and target, position size comes from a fixed risk percentage, and every order is validated against the broker's own limits before it leaves the building.

The order of operations matters

Notice the sequence in the code. Cheap checks first — new bar, existing position, spread. Expensive checks next — read indicators. Then validation — stops level, volume, margin. Only then send. This ordering means the common case (do nothing) costs almost nothing, which matters when you are running on every tick.

Every guard is there because something broke

  • New-bar gate — because without it the EA opened positions on every tick.
  • Position check with magic — because an EA that manages another EA's trades is a disaster.
  • Spread filter — because entries during a spread spike are pure cost.
  • Stops-level validation — because "invalid stops" is the most common rejection and it is silent unless you log it.
  • Volume normalisation — because 0.137 lots is not a valid order.
  • Margin check — because a correct size is not always an affordable one.
  • Handle release — because leaks stall the tester and eventually the terminal.

What is deliberately missing

No martingale, no grid, no averaging down, no recovery zone. Those are ways to manufacture a smooth equity curve while building a position that will eventually kill the account. If a system needs them, the system does not work.

Before you go live

Test on every regime in your data — trending, choppy, and crisis. Run a walk-forward. Check swap and commission are realistic. Forward-test on demo for weeks, not hours. And keep the position size small enough that being wrong about all of this is survivable.

What you now know

You can read and write MQL5: indicators, EAs, scripts, handles, events, order management, and risk-based sizing. Every line in this EA is something Parts 1 through 3 covered. The systems you write next will be variations on this template — and now you know which guards to keep.

What you just did

Lesson 17 of 17 in MetaTrader 5 MQL5 for CFD Traders. When you have run the examples or read the section, tick it off and move to the next lesson.