preview
Building Your Personal Expert Advisor (Part 1): From Fragile Script to Working EA

Building Your Personal Expert Advisor (Part 1): From Fragile Script to Working EA

MetaTrader 5Examples |
333 0
Solomon Anietie Sunday
Solomon Anietie Sunday

Table of Contents

  1. Introduction
  2. Building Our First Attempt: The Flawed Script
  3. Fix 1: New-Bar Detection
  4. Fix 2: Position Awareness and the Magic Number
  5. Fix 3: Dynamic Stops with ATR
  6. Fix 4: Error Handling and Validation
  7. Additional Production Safeguards
  8. Conclusion


Introduction

The road from a trading idea to a fully automated Expert Advisor is one that many traders attempt, but few complete successfully. The challenge is rarely the trading idea itself. More often, the problem is the architecture surrounding it. A script may compile, generate trades, and backtest well. However, it can still fail in live trading if it lacks safeguards for execution, position management, risk control, and validation. In this article, we will transform a deliberately flawed Moving Average crossover EA into a more reliable foundation for automated trading. Along the way, we will address several common weaknesses found in beginner EAs:

  • Repeated entries caused by processing signals on every tick.
  • No position awareness or trade isolation.
  • Fixed stop-loss and take-profit levels that ignore market volatility.
  • Missing validation and error handling.

The article assumes basic familiarity with MQL5 and MetaTrader 5. Rather than focusing on trading strategy design, we will focus on the architectural improvements that help turn a functional script into a more reliable Expert Advisor.

By the end of this part, we will have:

  • Built a basic EA skeleton.
  • Implemented new-bar detection.
  • Added Magic Number isolation and position awareness.
  • Replaced fixed stops with ATR-based stops.
  • Introduced validation and trade execution checks.
  • Recommended some additional production safeguards.

The examples are based on a simple Moving Average crossover strategy, but the techniques presented can be applied to many other automated trading systems.


Building Our First Attempt: The Flawed Script

Let us begin with a simple Moving Average crossover strategy. When the fast MA crosses above the slow MA, the EA buys; when it crosses below, the EA sells. A trader with basic MQL5 knowledge could implement the idea quickly. The resulting EA compiles, opens trades, and appears functional, but it contains several architectural flaws that will become apparent in live trading.

Here is the skeleton of an EA when a trader starts to code:

//+------------------------------------------------------------------+
//|                                            FlawedMACrossover.mq5 |
//|                                    Copyright 2026, soloharbinger |
//|                      https://www.mql5.com/en/users/soloharbinger |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, soloharbinger"
#property link      "https://www.mql5.com/en/users/soloharbinger"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }

They proceed to include the trading library and add the input that the user can adjust:

//+------------------------------------------------------------------+
//|                                            FlawedMACrossover.mq5 |
//|                                    Copyright 2026, soloharbinger |
//|                      https://www.mql5.com/en/users/soloharbinger |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, soloharbinger"
#property link      "https://www.mql5.com/en/users/soloharbinger"
#property version   "1.00"
#include <Trade/Trade.mqh>

//--- Input Parameters
input int    FastMA = 10;    // Fast MA period
input int    SlowMA = 20;    // Slow MA period
input double LotSize = 0.1;  // Fixed lot size
input int    StopLoss = 200; // Fixed stop loss in points
input int    TakeProfit = 400; // Fixed take profit in points

So far, this looks reasonable. The inputs are clear, and the user can tweak them from the EA's properties window.

Global Handles and Initialization

Next, they declare the global variables just below the inputs to hold the indicator handles, call the CTrade class for trading functions, and initialize the indicator handles in OnInit():

//--- Global Variables
CTrade trade;
int fastHandle, slowHandle;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize Handles
   fastHandle = iMA(_Symbol, _Period, FastMA, 0, MODE_SMA, PRICE_CLOSE);
   slowHandle = iMA(_Symbol, _Period, SlowMA, 0, MODE_SMA, PRICE_CLOSE);
   if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE)
     {
      return INIT_FAILED;
     }
//--- EA initialization successful
   return(INIT_SUCCEEDED);
  }

They create two handles for the moving averages, check that they are valid, and return INIT_SUCCEEDED. They also release the handles in OnDeinit():

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Deinitialize indicator
   IndicatorRelease(fastHandle);
   IndicatorRelease(slowHandle);
  }

The Trading Logic in OnTick()

The trader knows that OnTick() is called on every price tick, and they want to check for crossovers on the most recent completed bar (index 1). They copy the MA values, compare them, and execute orders using the trading functions from the CTrade class:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Populate arrays for both fast and slow indicators
   double fast[], slow[];
   ArraySetAsSeries(fast, true);
   ArraySetAsSeries(slow, true);
   CopyBuffer(fastHandle, 0, 0, 3, fast);
   CopyBuffer(slowHandle, 0, 0, 3, slow);

//--- Check crossover on the latest closed bar (index 1) and execute trade
   if(fast[1] > slow[1] && fast[2] <= slow[2])
     {
      // Buy signal
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      double sl = ask - StopLoss * _Point;
      double tp = ask + TakeProfit * _Point;
      trade.Buy(LotSize, _Symbol, 0.0, sl, tp, "Flawed Buy");
     }
   else
      if(fast[1] < slow[1] && fast[2] >= slow[2])
        {
         // Sell signal
         double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
         double sl = bid + StopLoss * _Point;
         double tp = bid - TakeProfit * _Point;
         trade.Sell(LotSize, _Symbol, 0.0, sl, tp, "Flawed Sell");
        }
  }

The Complete Flawed Expert Advisor

Here is the entire script as our trader wrote it. It compiles, attaches to a chart, and even appears to work—at least for a few minutes:

//+------------------------------------------------------------------+
//|                                            FlawedMACrossover.mq5 |
//|                                    Copyright 2026, soloharbinger |
//|                      https://www.mql5.com/en/users/soloharbinger |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, soloharbinger"
#property link      "https://www.mql5.com/en/users/soloharbinger"
#property version   "1.00"
#include <Trade/Trade.mqh>

//--- Input Parameters
input int    FastMA = 10;    // Fast MA period
input int    SlowMA = 20;    // Slow MA period
input double LotSize = 0.1;  // Fixed lot size
input int    StopLoss = 200; // Fixed stop loss in points
input int    TakeProfit = 400; // Fixed take profit in points

//--- Global Variables
CTrade trade;
int fastHandle, slowHandle;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize Handles
   fastHandle = iMA(_Symbol, _Period, FastMA, 0, MODE_SMA, PRICE_CLOSE);
   slowHandle = iMA(_Symbol, _Period, SlowMA, 0, MODE_SMA, PRICE_CLOSE);
   if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE)
     {
      return INIT_FAILED;
     }
//--- EA initialization successful
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Deinitialize indicator
   IndicatorRelease(fastHandle);
   IndicatorRelease(slowHandle);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Populate arrays for both fast and slow indicators
   double fast[], slow[];
   ArraySetAsSeries(fast, true);
   ArraySetAsSeries(slow, true);
   CopyBuffer(fastHandle, 0, 0, 3, fast);
   CopyBuffer(slowHandle, 0, 0, 3, slow);

//--- Check crossover on the latest closed bar (index 1) and execute trade
   if(fast[1] > slow[1] && fast[2] <= slow[2])
     {
      // Buy signal
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      double sl = ask - StopLoss * _Point;
      double tp = ask + TakeProfit * _Point;
      trade.Buy(LotSize, _Symbol, 0.0, sl, tp, "Flawed Buy");
     }
   else
      if(fast[1] < slow[1] && fast[2] >= slow[2])
        {
         // Sell signal
         double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
         double sl = bid + StopLoss * _Point;
         double tp = bid - TakeProfit * _Point;
         trade.Sell(LotSize, _Symbol, 0.0, sl, tp, "Flawed Sell");
        }
  }

Why This EA Will Fail in Production

Although the EA compiles, generates trades, and calculates stop-loss and take-profit, it contains four major problems:

Problem 1: Repeated Entries

The crossover condition is evaluated on every tick, allowing multiple identical trades to be opened from the same signal. Traders can use the Strategy Tester to visualize this problem:

Animation 1.

Problem 2: No Position Awareness

The EA never checks whether a position already exists, making over-trading possible. This can lead to overleveraging, margin calls, and a blown account.

Problem 3: Fixed Stops

Static stop-loss and take-profit levels do not adapt to changing market volatility.

Problem 4: No Validation or Error Handling

The code uses the trade execution function and does not check the result. If the broker rejects the order (for example, because the stop level is too close or the lot size is invalid), the EA may log a generic error and continue running. It will not know that the action failedThere is no validation of indicator data, no verification that trade requests succeeded, and no meaningful diagnostic information to help identify execution problems.

These issues are common in beginner EAs and can lead to unreliable behavior in live trading. In the following sections, we will address each problem and gradually transform the script into a more robust foundation.


Fix 1: New-Bar Detection

The first major flaw in our EA is repeated trade execution. Since OnTick() runs whenever a new price tick arrives, the crossover condition can remain true for an entire candle. Without additional control, the EA may open multiple trades from a single signal. The solution is to process trading decisions only once per bar.

The Solution: Process Signals Only Once Per Bar

We can achieve this by tracking the opening time of the current bar. If the EA has already processed that bar, it exits immediately. Otherwise, it updates the stored timestamp and continues with the trading logic.

The implementation is as follows:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Get the opening time of the current bar
   datetime currentBarTime = iTime(_Symbol, _Period, 0);
   if(currentBarTime == 0)
      return;

//--- Static variable to remember the last bar we processed
   static datetime lastProcessedBarTime = 0;

//--- If this bar has already been processed, exit immediately
   if(currentBarTime == lastProcessedBarTime)
      return;

//--- Mark this bar as processed
   lastProcessedBarTime = currentBarTime;

//--- Now we can safely execute our trading logic
   double fast[], slow[];
   ArraySetAsSeries(fast, true);
   ArraySetAsSeries(slow, true);
   CopyBuffer(fastHandle, 0, 0, 3, fast);
   CopyBuffer(slowHandle, 0, 0, 3, slow);

//--- Check crossover on the latest closed bar (index 1) and execute trade
   if(fast[1] > slow[1] && fast[2] <= slow[2])
     {
      // Buy signal
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      double sl = ask - StopLoss * _Point;
      double tp = ask + TakeProfit * _Point;
      trade.Buy(LotSize, _Symbol, 0.0, sl, tp, "Flawed Buy");
     }
   else
      if(fast[1] < slow[1] && fast[2] >= slow[2])
        {
         // Sell signal
         double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
         double sl = bid + StopLoss * _Point;
         double tp = bid - TakeProfit * _Point;
         trade.Sell(LotSize, _Symbol, 0.0, sl, tp, "Flawed Sell");
        }
  }

The static keyword allows a variable to retain its value between function calls. Without it, lastProcessedBarTime would be reset on every tick, and the new-bar detection mechanism would not work. As an additional safeguard, we verify that iTime() returns a valid timestamp before processing the new bar, preventing execution during temporary data synchronization.

Why This Works

Consider what happens when a new bar opens:

  • On the first tick of the new bar, currentBarTime becomes the opening time of the new bar.
  • lastProcessedBarTime still holds the opening time of the previous bar.
  • Since they differ, the condition fails, and we proceed to execute our trading logic.
  • Immediately after, we set lastProcessedBarTime to the current bar's time.

On every subsequent tick of the same bar:

  • currentBarTime remains the same.
  • lastProcessedBarTime now equals currentBarTime.
  • The condition if(currentBarTime == lastProcessedBarTime) becomes true, and we return early.

The result can be verified easily in the Strategy Tester:

Animation 2.

With this addition, the EA evaluates signals only once per candle, eliminating duplicate entries caused by repeated tick processing. The next improvement is position awareness, allowing the EA to identify and manage its own trades correctly.


Fix 2: Position Awareness and the Magic Number

With new-bar detection in place, the EA no longer opens multiple trades on the same candle. However, it still lacks position awareness. The EA does not know whether it already has an open position and may continue opening trades without considering existing exposure. To solve this, we will introduce Magic Number isolation and position-counting functions.

The Solution: Position Counting and Magic Number Isolation

To fix this, we need 2 components:

  1. Magic Number: A unique identifier that marks every trade opened by our EA. This allows us to distinguish our trades from manual trades or trades opened by other EAs.
  2. Position Counting Functions: Functions that count how many positions our EA has open and in which direction (buy or sell).

Adding the Magic Number

The magic number is a simple integer input. We add it to our input parameters:

//--- Input Parameters
input int    FastMA = 10;         // Fast MA period
input int    SlowMA = 20;         // Slow MA period
input double LotSize = 0.1;       // Fixed lot size
input int    StopLoss = 200;      // Fixed stop loss in points
input int    TakeProfit = 400;    // Fixed take profit in points
input int    MagicNumber = 12345; // EA Magic Number

We then set this number for our trade object in OnInit():

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Set the Magic Number for all trades
   trade.SetExpertMagicNumber(MagicNumber);
   
//--- Initialize Handles
   fastHandle = iMA(_Symbol, _Period, FastMA, 0, MODE_SMA, PRICE_CLOSE);
   slowHandle = iMA(_Symbol, _Period, SlowMA, 0, MODE_SMA, PRICE_CLOSE);
   if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE)
     {
      return INIT_FAILED;
     }
//--- EA initialization successful
   return(INIT_SUCCEEDED);
  }

Every position opened by the EA now carries a unique identifier, allowing us to distinguish it from manual trades or other Expert Advisors.


Building the Position Count Functions

Now we need functions that can tell us how many positions our EA has open. We will write 2 helper functions:

  1. CountOpenPositions() returns the total number of positions our EA has open.
  2. CountOpenPositionsByDirection() returns the number of buy or sell positions our EA has open.

Here is the implementation:

//+------------------------------------------------------------------+
//| Count total open positions for this EA                           |
//+------------------------------------------------------------------+
int CountOpenPositions()
  {
   int count = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket))
        {
         if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
            PositionGetInteger(POSITION_MAGIC) == MagicNumber)
           {
            count++;
           }
        }
     }
   return count;
  }

//+------------------------------------------------------------------+
//| Count open positions by direction (1 = Buy, -1 = Sell)           |
//+------------------------------------------------------------------+
int CountOpenPositionsByDirection(int direction)
  {
   int count = 0;
   ENUM_POSITION_TYPE posType = (direction == 1) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket))
        {
         if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
            PositionGetInteger(POSITION_MAGIC) == MagicNumber &&
            PositionGetInteger(POSITION_TYPE) == posType)
           {
            count++;
           }
        }
     }
   return count;
  }

These helper functions count all open positions belonging to the current symbol and Magic Number. The direction-specific function allows us to check how many buy positions or sell positions we currently have.

Integrating Position Awareness into OnTick()

Now we can use these functions to prevent over-trading. We will add a simple rule: only open a new trade if there is no existing position in that direction. In our OnTick() function, after the new-bar detection, we will check whether we already have a position in the direction we are about to trade:

//--- Populate MA array
   double fast[], slow[];
   ArraySetAsSeries(fast, true);
   ArraySetAsSeries(slow, true);
   CopyBuffer(fastHandle, 0, 0, 3, fast);
   CopyBuffer(slowHandle, 0, 0, 3, slow);

//--- Check crossover on the latest closed bar (index 1) and execute trade
   if(fast[1] > slow[1] && fast[2] <= slow[2])
     {
      //--- Buy signal: Only open a trade if we don't already have a buy position
      if(CountOpenPositionsByDirection(1) == 0)
        {
         double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
         double sl = ask - StopLoss * _Point;
         double tp = ask + TakeProfit * _Point;
         trade.Buy(LotSize, _Symbol, 0.0, sl, tp, "Fixed Buy");
        }
     }
   else
      if(fast[1] < slow[1] && fast[2] >= slow[2])
        {
         //--- Sell signal: Only open a trade if we don't already have a sell position
         if(CountOpenPositionsByDirection(-1) == 0)
           {
            double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
            double sl = bid + StopLoss * _Point;
            double tp = bid - TakeProfit * _Point;
            trade.Sell(LotSize, _Symbol, 0.0, sl, tp, "Fixed Sell");
           }
        }
  }
  

With these changes, the EA processes signals once per bar. It also checks whether a position already exists in the signal direction and opens a trade only if none exists. This reduces duplicate entries and prevents multiple trades from being opened from the same signal on a single bar. Although this improves trade discipline considerably, it should not be viewed as complete position management. Factors such as position sizing, account risk limits, and execution rules still influence overall trading risk.

A Note on Maximum Positions

Some traders may want to allow more than one position in a direction, for example, to scale into a trend. We will cover that in a later part of this series (when we discuss add-on systems and pyramiding). For now, the "one position per direction" rule is a safe and conservative default.


The Non-Hedging Option

Some traders allow opposing positions on the same symbol. Others prefer a strictly directional approach. To support both styles, we can introduce an optional hedging parameter.
Note: The hedging option described here is primarily relevant for hedging accounts, where independent buy and sell positions can coexist on the same symbol. On netting accounts, MetaTrader 5 maintains a single aggregated position per symbol. Opening a trade in the opposite direction reduces, closes, or reverses the existing position instead of creating a separate one. Consequently, the position-management examples presented in this section behave differently depending on the account type.

Adding the Input

We need to add a simple boolean input below others:

//--- Input Parameters
input int    FastMA = 10;          // Fast MA period
input int    SlowMA = 20;          // Slow MA period
input double LotSize = 0.1;        // Fixed lot size
input int    StopLoss = 200;       // Fixed stop loss in points
input int    TakeProfit = 400;     // Fixed take profit in points
input int    MagicNumber = 12345;  // EA Magic Number
input bool   AllowHedging = false; // Allow opposing positions (hedging)

Modify the position check once more

When the AllowHedging input parameter is false, we should block any trade if there is already any position open in either direction. We will modify our OnTick() accordingly:

//--- Check crossover on the latest closed bar (index 1) and execute trade
   if(fast[1] > slow[1] && fast[2] <= slow[2])
     {
      //--- Buy signal: Check if we can open a Buy position
      bool canOpenTrade = false;
      if(AllowHedging)
        {
         //--- Hedging is allowed: Only block Buys if there is already a Buy
         canOpenTrade = (CountOpenPositionsByDirection(1) == 0);
        }
      else
        {
         //--- Hedging is not allowed: Block trades if there is any open position
         canOpenTrade = (CountOpenPositions() == 0);
        }

      if(canOpenTrade)
        {
         double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
         double sl = ask - StopLoss * _Point;
         double tp = ask + TakeProfit * _Point;
         trade.Buy(LotSize, _Symbol, 0.0, sl, tp, "Fixed EA Buy");
        }
     }
   else
      if(fast[1] < slow[1] && fast[2] >= slow[2])
        {
         //--- Sell signal: Check if we can open a Sell position
         bool canOpenTrade = false;
         if(AllowHedging)
           {
            //--- Hedging is allowed: Only block Sells if there is already a Sell
            canOpenTrade = (CountOpenPositionsByDirection(-1) == 0);
           }
         else
           {
            //--- Hedging is not allowed: Block trades if there is any open position
            canOpenTrade = (CountOpenPositions() == 0);
           }

         if(canOpenTrade)
           {
            double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
            double sl = bid + StopLoss * _Point;
            double tp = bid - TakeProfit * _Point;
            trade.Sell(LotSize, _Symbol, 0.0, sl, tp, "Fixed EA Sell");
           }
        }
  }

At this stage, the EA can identify its own trades, prevent duplicate directional entries, and optionally enforce non-hedging behavior. This forms the foundation of position management, but it is not yet a complete trade-management system. More advanced EAs may close or reverse existing positions when an opposite signal appears, account for the different behavior of netting accounts, or handle partial fills and other execution scenarios. Those enhancements build upon the same foundation introduced here.


Fix 3: Dynamic Stops with ATR

We have addressed duplicate entries and position management, but our EA still uses fixed stop-loss and take-profit levels. The problem with fixed distances is that market volatility changes constantly. A stop that is appropriate during quiet conditions may be too tight during volatile periods. A common solution is to use the Average True Range (ATR) to calculate stop distances dynamically based on current market volatility. This tool is widely used as a measure of market volatility, and it can therefore be used to dynamically calculate stop-loss and take-profit levels.

Why ATR?

ATR is a volatility indicator. Higher ATR values indicate larger price movements, while lower values indicate quieter market conditions. By basing stop-loss and take-profit calculations on ATR, the EA automatically adapts its risk parameters to current market conditions.

Adding ATR to Our EA

First, we add input parameters for the ATR period, the multiplier, and the take-profit ratio that will be based on the distance of the stop-loss. We will also remove the static stop-loss and take-profit input parameters:

//--- Input Parameters
input int    FastMA = 10;          // Fast MA period
input int    SlowMA = 20;          // Slow MA period
input double LotSize = 0.1;        // Fixed lot size
input int    MagicNumber = 12345;  // EA Magic Number
input bool   AllowHedging = false; // Allow Opposing Position (Hedging)
input int    ATRPeriod = 14;       // ATR Period for Volatility Calculation
input double ATRMultiplier = 1.5;  // ATR Multiplier for Stop-Loss Distance
input double TPRatio = 2.0;        // Take Profit Ratio

Next we create an ATR handle in OnInit() just like we did for the Moving Average indicator:

//--- Global Variables
CTrade trade;
int fastHandle, slowHandle;
int atrHandle;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Set the Magic Number for all trades
   trade.SetExpertMagicNumber(MagicNumber);

//--- Initialize Handles
   fastHandle = iMA(_Symbol, _Period, FastMA, 0, MODE_SMA, PRICE_CLOSE);
   slowHandle = iMA(_Symbol, _Period, SlowMA, 0, MODE_SMA, PRICE_CLOSE);
   atrHandle = iATR(_Symbol, _Period, ATRPeriod);

   if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE || atrHandle == INVALID_HANDLE)
     {
      return INIT_FAILED;
     }
//--- EA initialization successful
   return(INIT_SUCCEEDED);
  }

And we release the ATR handle in OnDeinit():

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Deinitialize indicator
   IndicatorRelease(fastHandle);
   IndicatorRelease(slowHandle);
   IndicatorRelease(atrHandle);
  }

Calculate Dynamic Stop-loss and Take-profit

Now that we have prepared the ATR indicator, we will modify our trading logic to use ATR-based stops. In OnTick(), we will first copy the current ATR value. Calculate the stop distance:

//--- Populate the ATR
   double atrArray[];
   ArraySetAsSeries(atrArray, true);
   CopyBuffer(atrHandle, 0, 0, 1, atrArray);
   double atrValue = atrArray[0];

Notice: We use atrArray[0], which represents the current ATR value rather than the last completed one. Unlike the crossover signal, the ATR is used only to determine the stop distance at the moment the trade is placed. Using the most recent ATR value allows the stop-loss and take-profit to reflect the latest market volatility, preserving the dynamic nature of the ATR-based risk management.

Before calculating the dynamic stop distance, we must validate the ATR. This prevents the EA from attempting to trade when ATR data is not yet available (for example, immediately after attaching the EA to a chart). Without this check, the EA would calculate the stop distance as zero, leading to invalid stop levels and rejected orders. We simply add this check:

//--- Validate that we have valid data
   if(atrValue <= 0)
      return; // No valid ATR yet

Retrieving Market Prices

Before calculating the dynamic stop-loss distance using ATR, we must first retrieve proper market prices. Rather than requesting the Ask and Bid prices separately, we use SymbolInfoTick() to obtain a consistent snapshot of the latest market data. This reduces the chance of reading prices from different ticks. It also follows the recommended approach for obtaining current market prices in MQL5.

//--- Retrieve market prices
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick))
     {
      Print("Failed to obtain market prices.");
      return;
     }
   double ask = tick.ask;
   double bid = tick.bid;

Now we calculate the stop distance, digits, and point value of the traded symbol:

//--- Calculate the dynamic stop distance
   double stopDistance = atrValue * ATRMultiplier;
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);

We then use this dynamic distance for both stop-loss and take-profit. For a buy trade:

double sl = NormalizeDouble(ask - stopDistance, digits);
double tp = NormalizeDouble(ask + (stopDistance * TPRatio), digits);

And for a sell trade:

double sl = NormalizeDouble(bid + stopDistance, digits);
double tp = NormalizeDouble(bid - (stopDistance * TPRatio), digits);

A Note on Price Normalization

NormalizeDouble() ensures that stop-loss and take-profit values match the symbol's required precision. This conversion function is essential because MetaTrader 5 requires the price to be rounded to the correct number of decimal places for the symbol. Without this, the broker may reject the order. The number of digits is retrieved using:
SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);

For most forex pairs, this is 5 digits (e.g., 1.12345), but for some symbols it may be 2, 3, or 4.

With ATR integrated, stop-loss and take-profit levels now adjust automatically to market volatility. This produces more flexible risk management than fixed-distance stops and allows the EA to adapt across different market conditions and timeframes.


Fix 4: Error Handling and Validations

Despite the improvements made so far, our EA still assumes that every operation succeeds. In practice, indicator data may be unavailable, CopyBuffer() calls may fail, and brokers may reject trade requests. A reliable EA should validate its data before trading and verify the result of important operations.

The Problem: Silent Failure

Our current EA executes trades without checking the result. Consider this line from our OnTick() function:

trade.Buy(LotSize, _Symbol, 0.0, sl, tp, "Fixed EA Buy");

If this Buy() call fails (for example, because the stop-loss is too close to the entry price), the EA simply continues. Without result checking, the EA has no way to know whether a trade request was accepted or rejected. From the trader's perspective, signals may appear valid while no trade is actually executed. This makes troubleshooting difficult and can hide problems for long periods.

The Solution: Systematic Validation and Result Checking

To improve reliability, we will:

  • Validate indicator data before using it.
  • Verify trade execution results.
  • Log useful diagnostic information when problems occur.

Data Validation: Ensuring Indicator Readiness

Before copying data from an indicator buffer, it is good practice to verify that enough bars have been calculated. In MQL5, BarsCalculated() function is used to prevent the EA from attempting to read data that has not yet been generated, reducing unnecessary execution errors during startup.

//--- Ensure indicators are ready
   if(BarsCalculated(fastHandle) < 3 ||
      BarsCalculated(slowHandle) < 3 ||
      BarsCalculated(atrHandle) < 1)
     {
      Print("Indicators are not ready yet.");
      return;
     }

Indicator Data Validation

We already added a check for ATR data:

//--- Validate that we have a valid ATR value
   if(atrValue <= 0)
      return; // No valid ATR yet

But we should not only check for valid ATR data. We should also check and log the ATR value and the ATR data so that we know when the EA isn't calculating.

//--- Populate the ATR
   double atrArray[];
   ArraySetAsSeries(atrArray, true);
   if(CopyBuffer(atrHandle, 0, 0, 1, atrArray) < 1)
     {
      Print("Failed to get ATR data");
      return;
     }
   double atrValue = atrArray[0];

//--- Validate that we have a valid ATR value
   if(atrValue <= 0)
     {
      Print("Invalid ATR value: ", atrValue);
      return;
     }

The same principle applies to the moving averages. Before using indicator values, we should confirm that enough data was copied successfully.

//--- Populate the MA buffer
   double fast[], slow[];
   ArraySetAsSeries(fast, true);
   ArraySetAsSeries(slow, true);
   int fastCopied = CopyBuffer(fastHandle, 0, 0, 3, fast);
   int slowCopied = CopyBuffer(slowHandle, 0, 0, 3, slow);

//--- Validate that we have enough MA data
   if(fastCopied < 3 || slowCopied < 3)
     {
      Print("Failed to get enough Moving Average Data");
      return;
     }

With all this data validation in place, we can always ensure that the data we use to process the trading logic is always valid.

Result Checking

Now we need to check whether our trades actually succeeded. The CTrade class returns a boolean result indicating whether the trade request was successfully sent, and it also provides access to additional information through its result methods. We will incorporate this into our trading logic for the buy side:

      if(canOpenTrade)
        {
         double sl = NormalizeDouble(ask - stopDistance, digits);
         double tp = NormalizeDouble(ask + (stopDistance * TPRatio), digits);

         // Execute Buy trade and check result
         if(!trade.Buy(LotSize, _Symbol, 0.0, sl, tp, "Fixed EA Buy"))
           {
            PrintFormat("Buy failed. Retcode: %u (%s)",
                        trade.ResultRetcode(),
                        trade.ResultRetcodeDescription());
           }
        }
     }

And for the sell side:

         if(canOpenTrade)
           {
            double sl = NormalizeDouble(bid + stopDistance, digits);
            double tp = NormalizeDouble(bid - (stopDistance * TPRatio), digits);

            // Execute Sell trade and check result
            if(!trade.Sell(LotSize, _Symbol, 0.0, sl, tp, "Fixed EA Sell"))
              {
               PrintFormat("Sell failed. Retcode: %u (%s)",
                           trade.ResultRetcode(),
                           trade.ResultRetcodeDescription());
              }
           }
        }
The ResultRetcode() and ResultRetcodeDescription() methods provide broker-specific feedback about why an order succeeded or failed, making trade execution problems significantly easier to diagnose.

A Note on Common Error Codes

When an operation fails, it is helpful to understand some of the most common trade server return codes:
Error Code Meaning Likely Cause in an EA
10014 Invalid volume in the request Lot size is outside the allowed range
10015 Invalid price in the request Price is not normalized or out of range
10016 Invalid stops in the request Stop-loss or take-profit are too close to entry

Common runtime errors often relate to invalid parameters, unavailable indicator data, or rejected trade requests. When troubleshooting, the MQL5 documentation remains the best reference for interpreting specific error codes.

Here is a quick visual of how error logging can make traders quickly identify runtime errors:

Animation 3.

The benefits of error logging can be observed easily in the Strategy Tester, where runtime issues become significantly easier to identify and diagnose.



Validating User Inputs

Before completing initialization, it is good practice to validate user-configurable inputs. Invalid values can lead to failed indicator creation, incorrect calculations, or trade requests that never execute. Rather than allowing the EA to continue in an invalid state, we can detect configuration errors during OnInit() and stop the initialization process with an informative message.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Set the Magic Number for all trades
   trade.SetExpertMagicNumber(MagicNumber);

//--- Initialize Handles
   fastHandle = iMA(_Symbol, _Period, FastMA, 0, MODE_SMA, PRICE_CLOSE);
   slowHandle = iMA(_Symbol, _Period, SlowMA, 0, MODE_SMA, PRICE_CLOSE);
   atrHandle = iATR(_Symbol, _Period, ATRPeriod);

   if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE || atrHandle == INVALID_HANDLE)
     {
      return INIT_FAILED;
     }

//--- Validate user inputs
   if(FastMA <= 0 || SlowMA <= 0)
     {
      Print("Moving Average periods must be greater than zero.");
      return INIT_PARAMETERS_INCORRECT;
     }

   if(FastMA >= SlowMA)
     {
      Print("FastMA should be smaller than SlowMA.");
      return INIT_PARAMETERS_INCORRECT;
     }

   if(LotSize <= 0)
     {
      Print("Lot size must be greater than zero.");
      return INIT_PARAMETERS_INCORRECT;
     }

   if(ATRPeriod <= 0 || ATRMultiplier <= 0)
     {
      Print("ATR parameters must be greater than zero.");
      return INIT_PARAMETERS_INCORRECT;
     }
//--- EA initialization successful
   return(INIT_SUCCEEDED);
  }

Returning INIT_PARAMETERS_INCORRECT prevents the EA from loading when invalid settings are detected. This allows configuration problems to be corrected immediately instead of surfacing later during trading or backtesting.


Additional Production Safeguards

By this point, our EA validates indicator data, checks trade execution results, and reports failures through meaningful log messages. Before considering an Expert Advisor ready, however, there are a few additional safeguards worth implementing. These checks are not tied to any particular trading strategy. Instead, they help ensure that trades are only submitted when market conditions and account constraints make execution practical.

1. Spread Validation

The spread represents the cost of entering a trade. During periods of low liquidity or major news releases, spreads can widen significantly, making otherwise valid trading signals uneconomical. Before placing an order, we can verify that the current spread remains below a user-defined threshold.

Input

input double MaxSpreadPips = 2.0;  // Maximum allowed spread

Helper Function

//+------------------------------------------------------------------+
//| Check if current spread is acceptable                            |
//+------------------------------------------------------------------+
bool IsSpreadAcceptable()
  {
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick))
      return false;
   double spreadPoints = (tick.ask - tick.bid) / _Point;

   if(spreadPoints > MaxSpreadPips * 10.0)
     {
      PrintFormat("Spread too high: %.1f points (Maximum %.1f pips)",
                  spreadPoints,
                  MaxSpreadPips);
      return false;
     }
   return true;
  }

Usage

Integration can be done anywhere before the trade execution logic.

//--- Check Spread before trading
   if(!IsSpreadAcceptable())
      return;

2. Broker Trading Constraints

Brokers enforce minimum stop distances through the Stops Level and temporarily restrict order modification near market prices through the Freeze Level. Before submitting or modifying orders, these values can be queried using SYMBOL_TRADE_STOPS_LEVEL and SYMBOL_TRADE_FREEZE_LEVEL to ensure the requested prices satisfy the broker's trading rules. Likewise, production EAs often verify that the requested trade volume satisfies the symbol's minimum, maximum, and step requirements, while also confirming that sufficient free margin is available before attempting to execute an order. Although these checks are beyond the scope of this introductory EA, they are valuable additions when preparing a strategy for live trading.



How to Extend This Foundation

The trading logic used in this article is intentionally simple, but the architecture can support many different strategies. Because signal generation is separated from execution and position management, the same framework can be reused with alternative entry methods such as RSI signals, trend-following systems, breakout strategies, or custom indicators.

As the series progresses, we will continue expanding this foundation with additional components such as filters, session controls, advanced risk management, trade management systems, and performance tracking.

Common Pitfalls and Their Solutions

Pitfall Solution
Processing signals on every tick. Use new-bar detection for bar-based strategies.
Ignoring existing positions. Track positions using Magic Numbers and position counting functions.
Using fixed stop-loss values. Use ATR or other volatility-based calculations.
Trading with invalid indicator data. Validate indicator buffers before use.
Ignoring trade execution results. Check trade operation return values and log failures.
Disregarding spread and broker constraints. Check spreads before trade execution, and modify trade values to fit broker requirements.
Building too many features at once. Build and test one component at a time.

These issues appear frequently in beginner EAs. Addressing them early creates a more reliable foundation for future development.

What Has Not Yet Been Covered

The improvements introduced in this article establish a much stronger architectural foundation, but they do not represent every safeguard used in production Expert Advisors. More sophisticated features can be added as the EA evolves. This article demonstrates the core architectural principles that turn a functional script into a more reliable starting point for automated trading.


Conclusion

A trading strategy alone is not enough to create a reliable Expert Advisor. The architecture surrounding the strategy—position awareness, risk controls, validation, and execution handling—is often what determines whether an EA performs reliably in live trading.

In this article, we transformed a flawed Moving Average crossover EA into a more robust foundation by introducing new-bar detection, Magic Number isolation, position awareness, ATR-based stop management, and basic validation and error handling. While the strategy itself remains simple, the framework can now serve as a starting point for more advanced automated trading systems.

The source files used throughout this article are attached below so you can compare the original implementation with the improved version developed step by step.

Filename Description
FixedMACrossover Improved EA with new-bar detection, position management, ATR stops, and validation.
FlawedMACrossover Original version used to demonstrate common architectural issues.
Attached files |
Features of Custom Indicators Creation Features of Custom Indicators Creation
Creation of Custom Indicators in the MetaTrader trading system has a number of features.
Automated Trade Statement Exporter to Excel-Compatible XLSX in MQL5 Automated Trade Statement Exporter to Excel-Compatible XLSX in MQL5
An MQL5 script reconstructs closed trades from deal history using a two-pass SL/TP lookup and exports them to an Excel-compatible XLSX file without third-party libraries. Four cooperating classes handle trade data, history reconstruction, SpreadsheetML XML generation, and ZIP assembly via .NET's ZipFile class through a direct ShellExecuteW call with marker-file polling. The output opens in Excel and Google Sheets with correct numeric types, formatted date columns, and a bold header row.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Bayesian Online Change-Point Detection (BOCPD) in MQL5: One Regime-Break Signal, Three Ways to Use It Bayesian Online Change-Point Detection (BOCPD) in MQL5: One Regime-Break Signal, Three Ways to Use It
This article delivers Bayesian Online Change-Point Detection as a single, dependency-free MQL5 class that maintains a per-bar, causal probability of a regime break. We use it three ways: a live monitor, a moving average that flushes on breaks, and a risk overlay with a matched-frequency random control. Readers get a reusable primitive to watch structural change, adapt indicators, and gate exposure after detected shifts.