Step-by-step: Writing your own Expert Advisor in MQL5

Self-coded Expert Advisor “Moving Average Crossover”

Introduction

This Expert Advisor (EA) “MA_Cross” for MetaTrader 5 automates the 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 do the following in MQL5:

  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, running EA.


Code Overview

Libraries and trade object

#include <Trade\Trade.mqh>   // Include CTrade class for order functions
CTrade trade;               // Trade object for sending and modifying 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;            // ATR period
input double RiskPercent     = 1.0;           // Max risk per trade in % of the 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 all 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 integers store references that are later used to read values using CopyBuffer().


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);
   // Amount we are allowed to risk at most
   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 calculation: 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 computes the lot size so that, if the stop-loss is hit, the maximum risk amount is not exceeded.


OnTick()

void OnTick()
  {
   // 1) Current Bid/Ask prices
   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 whether a position is already open
   bool positionExists = PositionSelect(_Symbol);

   // — Enter only if no position is open —
   if(!positionExists)
     {
      // Long signal: EMA10 crosses EMA50 up + ATR rising
      bool longSignal  = (fastEMAold < slowEMAold)
                         && (fastEMA > slowEMA)
                         && (atrNow > atrOld);
      // Short signal: EMA10 crosses EMA50 down
      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("Opened long 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("Opened short 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 dynamically trailed when 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. Creating handles and reading values
    • iMA() / iATR() each return a handle in OnInit(), i.e., a numeric reference to the indicator.
    • In OnTick(), CopyBuffer(handle, 0, shift, count, buffer) fetches current and previous values into a local array.
    • Advantage: more efficient than repeated direct calls and allows 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 EMA values of the previous bar; fastEMA and slowEMA are those of the current bar.
    • A long signal occurs when the fast EMA crosses above the slow EMA and ATR shows rising volatility.
    • A short signal occurs when crossing from above to below, without an 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 so that the risk-reward ratio is 1:2:
      • TP distance = 2 × SL distance
    • In CalculateLot(), the lot size ensures that, if the stop-loss is hit, at most RiskPercent% of the account balance is lost.
    • SymbolInfoDouble(..., SYMBOL_TRADE_TICK_VALUE) and SYMBOL_TRADE_TICK_SIZE are used to correctly account for the cash value per point.
  4. Order placement
    • With trade.Buy(lot, NULL, price, sl, tp, CommentText) and
      trade.Sell(...) a market order is sent with defined SL and TP.
    • CTrade handles error processing, slippage, and returns the order ticket.
  5. Trailing stop (optional)
    • Once a position is open and UseTrailingStop == true, the EA moves the stop-loss by 1 × ATR behind current price on every tick:
      • Long: new SL = Bid – ATR
      • Short: new SL = Ask + ATR
    • This provides a dynamic lock-in of accrued profit.
  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
  • Implement dynamic stop-loss strategies

Summary

This EA demonstrates a complete MQL5 workflow:

  • Clean separation between initialization (OnInit), main logic (OnTick), and cleanup (OnDeinit).
  • Handle-based indicator usage with CopyBuffer().
  • Simple trend-following strategy based on 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 get a solid foundation to develop your own strategies in MQL5 and understand how to implement automated trading systems professionally. 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;                                              // Trade object
//— Input parameters ——————————————————————————————————————————————
input int    FastEMAPeriod    = 10;                       // Period for fast EMA
input int    SlowEMAPeriod    = 50;                       // Period for slow EMA
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";          // Comment text for orders
//— Global indicator handles ——————————————————————————————————————
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
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);
// Check if handles are valid
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);                                // All good
}
//+------------------------------------------------------------------+
//| CalculateLot: Lot size based on SL distance                      |
//+------------------------------------------------------------------+
double CalculateLot(bool isBuy, double stopLossPrice)
{
// 1) Determine 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 step
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 on every tick                                 |
//+------------------------------------------------------------------+
void OnTick()
{
// 1) Get 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);
// — Entry if no position is open ————————————————————————————————
if(!positionExists)
{
// Long if EMA cross up + ATR rising
bool longSignal  = (fastEMAold < slowEMAold) && (fastEMA > slowEMA) && (atrNow > atrOld);
// Short if 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);                                    // Compute lot
if(trade.Buy(lot, NULL, ask, sl, tp, CommentText))                      // Send buy order
Print("Opened long 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);                                   // Compute lot
if(trade.Sell(lot, NULL, bid, sl, tp, CommentText))                     // Send sell order
Print("Opened short 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)                                             // If long
newSL = bid - atrNow;                                                   // SL = Bid − 1×ATR
else                                                                       // If short
newSL = ask + atrNow;                                                   // SL = Ask + 1×ATR
trade.PositionModify(ticket, newSL, 0);                                   // Adjust 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
}
//+------------------------------------------------------------------+
Scroll to Top