Self-Programmed Expert Advisor “Moving Average Crossover”

Introduction

This Expert Advisor (EA) “MA_Cross” for MetaTrader 5 automates detection and trading of a trend-following setup based on two Exponential Moving Averages (EMA) and the Average True Range (ATR).

It demonstrates how to use MQL5 to:

  1. Create and read indicators
  2. Detect signals (crossover and volatility filter)
  3. Implement risk management via stop-loss, take-profit, and lot sizing
  4. Place and monitor orders
  5. Dynamically adjust a trailing stop

The goal is to show how to work cleanly with indicator handles in MQL5 and build a complete, runnable EA.


Code Overview

Libraries and Trading Object

#include <Trade\Trade.mqh>   // include CTrade class for order functions
CTrade trade;               // trading object to send and manage orders

Trade.mqh provides the CTrade class, which makes it easy to place market orders (buy/sell) and modify existing positions.


Input Parameters

input int    FastEMAPeriod   = 10;            // period for fast EMA
input int    SlowEMAPeriod   = 50;            // period for slow EMA
input int    ATRPeriod       = 14;            // period for ATR
input double RiskPercent     = 1.0;           // max risk per trade in % of account
input double RewardRiskRatio = 2.0;           // TP to SL ratio (e.g. 1:2)
input bool   UseTrailingStop = true;          // enable trailing stop?
input string CommentText     = "EMA_Cross_ATR"; // comment for all orders

These inputs let you adjust key parameters (EMA periods, ATR, risk, R:R ratio, trailing stop) directly in the tester or EA properties.


Global Handles for Indicators

int fastHandle;    // handle for fast EMA (FastEMAPeriod)
int slowHandle;    // handle for slow EMA (SlowEMAPeriod)
int atrHandle;     // handle for ATR (ATRPeriod)

Indicators in MQL5 are created via handles. These integer variables store references and are later used with CopyBuffer() to read values.


OnInit()

int OnInit()
  {
   // create EMA handles
   fastHandle = iMA(_Symbol, PERIOD_CURRENT, FastEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   slowHandle = iMA(_Symbol, PERIOD_CURRENT, SlowEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   // create ATR handle
   atrHandle  = iATR(_Symbol, PERIOD_CURRENT, ATRPeriod);

   // check if all handles are valid
   if(fastHandle == INVALID_HANDLE
      || slowHandle == INVALID_HANDLE
      || atrHandle  == INVALID_HANDLE)
     {
      Print("Error creating indicator handles");
      return(INIT_FAILED);
     }

   return(INIT_SUCCEEDED);
  }
  • iMA(...) and iATR(...) return the handles.
  • If a handle is INVALID_HANDLE, initialization fails.

CalculateLot()

double CalculateLot(bool isBuy, double stopLossPrice)
  {
   // entry price (Ask for buy, Bid for sell)
   double entryPrice = isBuy
                       ? SymbolInfoDouble(_Symbol, SYMBOL_ASK)
                       : SymbolInfoDouble(_Symbol, SYMBOL_BID);
   // distance entry ↔ stop-loss in price units
   double distance   = MathAbs(entryPrice - stopLossPrice);
   // maximum amount we are allowed to risk
   double riskAmount = AccountInfoDouble(ACCOUNT_BALANCE) * RiskPercent / 100.0;
   // tick value and tick size for the symbol
   double tickValue  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double tickSize   = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   double pointValue = tickValue / tickSize;
   // raw lot size: risk / (distance × point value)
   double lots       = riskAmount / (distance * pointValue);
   // round to allowed volume steps
   double step       = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   double minLot     = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxLot     = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   lots = MathFloor(lots / step) * step;
   if(lots < minLot) lots = minLot;
   if(lots > maxLot) lots = maxLot;
   return(lots);
  }

This function calculates the lot size so that, if the stop-loss is hit, the maximum risk amount will not be exceeded.


OnTick()

void OnTick()
  {
   // 1) current Bid/Ask
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

   // 2) buffers for the last 2 values of each indicator
   double fastBuf[2], slowBuf[2], atrBuf[2];

   // 3) copy indicator values (current = index 0, previous = index 1)
   if(CopyBuffer(fastHandle, 0, 0, 2, fastBuf) <= 0 ||
      CopyBuffer(slowHandle, 0, 0, 2, slowBuf) <= 0 ||
      CopyBuffer(atrHandle,  0, 0, 2, atrBuf)  <= 0)
     {
      Print("Error copying indicator data");
      return;
     }

   // 4) assign buffer data
   double fastEMA    = fastBuf[0], fastEMAold = fastBuf[1];
   double slowEMA    = slowBuf[0], slowEMAold = slowBuf[1];
   double atrNow     = atrBuf[0],  atrOld     = atrBuf[1];

   // 5) check if a position is already open
   bool positionExists = PositionSelect(_Symbol);

   // — entries only if no position is open —
   if(!positionExists)
     {
      // long signal: EMA10 crosses EMA50 upward + ATR rising
      bool longSignal  = (fastEMAold < slowEMAold)
                         && (fastEMA > slowEMA)
                         && (atrNow > atrOld);
      // short signal: EMA10 crosses EMA50 downward
      bool shortSignal = (fastEMAold > slowEMAold)
                         && (fastEMA < slowEMA);

      // long entry
      if(longSignal)
        {
         double sl  = ask - 1.5 * atrNow;
         double tp  = ask + RewardRiskRatio * (ask - sl);
         double lot = CalculateLot(true, sl);
         if(trade.Buy(lot, NULL, ask, sl, tp, CommentText))
            Print("Long opened at ", ask, " SL=", sl, " TP=", tp);
        }
      // short entry
      else if(shortSignal)
        {
         double sl  = bid + 1.5 * atrNow;
         double tp  = bid - RewardRiskRatio * (sl - bid);
         double lot = CalculateLot(false, sl);
         if(trade.Sell(lot, NULL, bid, sl, tp, CommentText))
            Print("Short opened at ", bid, " SL=", sl, " TP=", tp);
        }
     }
   // — trailing stop if a position is open and enabled —
   else if(UseTrailingStop)
     {
      ulong ticket = PositionGetInteger(POSITION_TICKET);
      ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      double newSL = (type == POSITION_TYPE_BUY)
                     ? bid - atrNow
                     : ask + atrNow;
      trade.PositionModify(ticket, newSL, 0);
     }
  }

In OnTick(), indicators are evaluated, signals are detected, orders are sent, and the stop-loss is trailed dynamically if needed.


OnDeinit()

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

When removing the EA, all indicator handles are released to avoid memory leaks and other issues.


Explanation of the Key Steps

  1. Handle creation and value access
    • iMA() / iATR() each return a handle in OnInit(), a numeric reference to the respective indicator.
    • In OnTick(), CopyBuffer(handle, 0, shift, count, buffer) fetches current and previous values into a local array.
    • Benefit: more efficient than repeated direct calls and enables access to historical bars.
  2. Crossover detection
    bool longSignal = (fastEMAold < slowEMAold) && (fastEMA > slowEMA) && (atrNow > atrOld);
    bool shortSignal = (fastEMAold > slowEMAold) && (fastEMA < slowEMA);
    • fastEMAold and slowEMAold are the previous bar’s EMA values; fastEMA and slowEMA are the current ones.
    • A long signal occurs when the fast EMA crosses above the slow EMA and ATR indicates rising volatility.
    • A short signal occurs when crossing downward, without the ATR filter.
  3. Risk management & lot sizing
    • Stop-loss is set to 1.5 × ATR:
      • Long: SL = Ask – 1.5 × ATR
      • Short: SL = Bid + 1.5 × ATR
    • Take-profit targets a 1:2 risk-reward:
      • TP distance = 2 × SL distance
    • CalculateLot() computes how many lots to place so that, if the SL is hit, at most RiskPercent% of the account is lost.
    • It uses SymbolInfoDouble(..., SYMBOL_TRADE_TICK_VALUE) and SYMBOL_TRADE_TICK_SIZE to correctly account for the money value per point.
  4. Order placement
    • With trade.Buy(lot, NULL, price, sl, tp, CommentText) or trade.Sell(...) a market order is sent with defined SL and TP.
    • CTrade handles internal error processing, slippage, and returns the order ticket.
  5. Trailing stop (optional)
    • Once a position is open and UseTrailingStop == true, the EA shifts the SL on every tick by 1 × ATR behind the current market price:
      • Long: new SL = Bid – ATR
      • Short: new SL = Ask + ATR
    • This provides a dynamic lock-in of accrued profits.
  6. Cleanup in OnDeinit()
    • All created indicator handles (fastHandle, slowHandle, atrHandle) are released with IndicatorRelease(handle) to free resources properly.

With these clearly structured steps you learn how to:

  • Create and read indicators efficiently
  • Detect signals precisely
  • Implement responsible risk management
  • Manage market orders professionally
  • Apply dynamic stop-loss strategies

Summary

This EA demonstrates a complete workflow in MQL5:

  • Clean separation between initialization (OnInit), main logic (OnTick), and cleanup (OnDeinit).
  • Handle-based indicator usage with CopyBuffer().
  • Simple trend-following strategy using two EMAs plus a volatility filter.
  • Careful risk management using stop-loss, take-profit, and dynamic lot sizing.
  • Optional automation of a trailing stop.

With this commented example you have a solid foundation to develop your own strategies in MQL5 and understand how to implement professional automated trading systems. Good luck with your next steps in MQL5 programming!


Source Code MA_Cross.mq5

//+------------------------------------------------------------------+
//|                                                   MA_Cross.mq5   |
//|        EMA Crossover + ATR filter for MetaTrader 5 (MQL5)        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>                                  // include CTrade class for order functions
CTrade trade;                                              // trading object

//—— Input parameters —————————————————————————————————————————————
input int    FastEMAPeriod    = 10;                       // fast EMA period
input int    SlowEMAPeriod    = 50;                       // slow EMA period
input int    ATRPeriod        = 14;                       // ATR period
input double RiskPercent      = 1.0;                      // max risk per trade in % (e.g. 1 = 1%)
input double RewardRiskRatio  = 2.0;                      // risk-reward ratio (e.g. 1:2)
input bool   UseTrailingStop  = true;                     // enable trailing stop?
input string CommentText      = "EMA_Cross_ATR";          // order comment

//—— Global handles for indicators ————————————————————————————————
int fastHandle;                                           // handle for fast EMA
int slowHandle;                                           // handle for slow EMA
int atrHandle;                                            // handle for ATR

//+------------------------------------------------------------------+
//| OnInit: initialization when the EA is loaded                     |
//+------------------------------------------------------------------+
int OnInit()
  {
   // create EMA handles with symbol, timeframe, period, shift, method, price source
   fastHandle = iMA(_Symbol, PERIOD_CURRENT, FastEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   slowHandle = iMA(_Symbol, PERIOD_CURRENT, SlowEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   // create ATR handle with symbol, timeframe, period
   atrHandle  = iATR(_Symbol, PERIOD_CURRENT, ATRPeriod);

   // validate handles
   if(fastHandle==INVALID_HANDLE || slowHandle==INVALID_HANDLE || atrHandle==INVALID_HANDLE)
     {
      Print("Error creating indicator handles");
      return(INIT_FAILED);                               // abort init on error
     }
   return(INIT_SUCCEEDED);                                // ok
  }

//+------------------------------------------------------------------+
//| CalculateLot: lot size based on SL distance                      |
//+------------------------------------------------------------------+
double CalculateLot(bool isBuy, double stopLossPrice)
  {
   // 1) entry price depending on buy/sell
   double entryPrice = isBuy
                       ? SymbolInfoDouble(_Symbol, SYMBOL_ASK)
                       : SymbolInfoDouble(_Symbol, SYMBOL_BID);
   // 2) distance SL ↔ entry
   double distance   = MathAbs(entryPrice - stopLossPrice);
   // 3) risk amount (balance × risk%)
   double riskAmount = AccountInfoDouble(ACCOUNT_BALANCE) * RiskPercent / 100.0;
   // 4) point value for symbol (TickValue / TickSize)
   double tickValue  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double tickSize   = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   double pointValue = tickValue / tickSize;
   // 5) raw lots = risk / (distance × point value)
   double lots       = riskAmount / (distance * pointValue);
   // 6) round to minimum lot steps
   double step       = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   double minLot     = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxLot     = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   lots = MathFloor(lots / step) * step;
   if(lots < minLot) lots = minLot;
   if(lots > maxLot) lots = maxLot;
   return(lots);                                          // return final lot size
  }

//+------------------------------------------------------------------+
//| OnTick: main logic each tick                                     |
//+------------------------------------------------------------------+
void OnTick()
  {
   // 1) current Bid/Ask
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

   // 2) buffers for current and previous indicator values
   double fastBuf[2], slowBuf[2], atrBuf[2];
   // 3) copy data from handles
   if(CopyBuffer(fastHandle, 0, 0, 2, fastBuf) <= 0 ||
      CopyBuffer(slowHandle, 0, 0, 2, slowBuf) <= 0 ||
      CopyBuffer(atrHandle,  0, 0, 2, atrBuf)  <= 0)
     {
      Print("Error copying indicator data");
      return;                                             // abort on error
     }

   // 4) assign values: [0] = current, [1] = previous
   double fastEMA    = fastBuf[0];
   double fastEMAold = fastBuf[1];
   double slowEMA    = slowBuf[0];
   double slowEMAold = slowBuf[1];
   double atrNow     = atrBuf[0];
   double atrOld     = atrBuf[1];

   // 5) check if a position is already open
   bool positionExists = PositionSelect(_Symbol);

   // —— entries when no position is open ————————————————————————————
   if(!positionExists)
     {
      // long when EMA cross up + ATR rising
      bool longSignal  = (fastEMAold < slowEMAold) && (fastEMA > slowEMA) && (atrNow > atrOld);
      // short when EMA cross down
      bool shortSignal = (fastEMAold > slowEMAold) && (fastEMA < slowEMA);

      if(longSignal)  // long entry
        {
         double sl  = ask - 1.5 * atrNow;                                         // SL = entry – 1.5×ATR
         double tp  = ask + RewardRiskRatio * (ask - sl);                         // TP = entry + 2×risk
         double lot = CalculateLot(true, sl);                                     // lot sizing
         if(trade.Buy(lot, NULL, ask, sl, tp, CommentText))                      // send buy order
            Print("Long opened at ", ask, " SL=", sl, " TP=", tp);
        }
      else if(shortSignal) // short entry
        {
         double sl  = bid + 1.5 * atrNow;                                         // SL = entry + 1.5×ATR
         double tp  = bid - RewardRiskRatio * (sl - bid);                         // TP = entry – 2×risk
         double lot = CalculateLot(false, sl);                                    // lot sizing
         if(trade.Sell(lot, NULL, bid, sl, tp, CommentText))                     // send sell order
            Print("Short opened at ", bid, " SL=", sl, " TP=", tp);
        }
     }
   // —— trailing stop if a position is open ——————————————————————————
   else if(UseTrailingStop)
     {
      ulong ticket = PositionGetInteger(POSITION_TICKET);                        // position ticket
      ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      double newSL;
      if(type == POSITION_TYPE_BUY)                                              // for long
         newSL = bid - atrNow;                                                   // SL = Bid – 1×ATR
      else                                                                       // for short
         newSL = ask + atrNow;                                                   // SL = Ask + 1×ATR

      trade.PositionModify(ticket, newSL, 0);                                    // modify SL
     }
  }

//+------------------------------------------------------------------+
//| OnDeinit: release handles                                        |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(fastHandle != INVALID_HANDLE)  IndicatorRelease(fastHandle);              // release fast EMA
   if(slowHandle != INVALID_HANDLE)  IndicatorRelease(slowHandle);              // release slow EMA
   if(atrHandle  != INVALID_HANDLE)  IndicatorRelease(atrHandle);               // release ATR
  }
//+------------------------------------------------------------------+