Русский 中文 Español Deutsch 日本語 Português
preview
Post-Factum trading analysis: Selecting trailing stops and new stop levels in the strategy tester

Post-Factum trading analysis: Selecting trailing stops and new stop levels in the strategy tester

MetaTrader 5Examples |
1 657 4
Artyom Trishkin
Artyom Trishkin

Contents


Introduction

In the previous article, we created an EA that trades in the client terminal's strategy tester based on the results of trading on a real account. We have added the ability to set new StopLoss and TakeProfit sizes to test our trading in the tester with different stop order sizes. The result was unexpected: instead of a loss, we received a profit commensurate with the loss incurred in real trading. This means that if we had used the StopLoss and TakeProfit levels in our trading account that allowed us to make a profit in the tester, then trading on a real account would also be profitable. And this is just a simple change in the stop sizes. I wonder what would happen if we added a trailing StopLoss? How would this change things?

Today we will connect several different trailing stops to the EA and test our trading in the strategy tester. Let's see what result will be obtained.


Selecting and refining a class of position trailing stops

Let's try to connect trailing by Parabolic SAR and moving average. The Parabolic SAR indicator, judging by its description and name (SAR = Stop And Reverse), should be perfect for moving the stop line according to its values:

Parabolic SAR

Parabolic SAR Technical Indicator was developed for analyzing the trending markets. The indicator is constructed on the price chart. This indicator is similar to Moving Average with the only difference that Parabolic SAR moves with higher acceleration and may change its position in terms of the price. The indicator is below the prices on the bull market (Up Trend), when the market is bearish (Down Trend), it is above the prices.

If the price crosses Parabolic SAR line, the indicator turns, and its further values are situated on the other side of the price. When such an indicator turn does take place, the maximum or the minimum price for the previous period would serve as the starting point. When the indicator makes a turn, it gives a signal of the trend end (correction stage or flat), or of its turn.

The Parabolic SAR is an outstanding indicator for providing exit points. Long positions should be closed when the price sinks below the SAR line, short positions should be closed when the price rises above the SAR line. That is, it is necessary to track the direction of Parabolic SAR and hold open only the positions that are in the same direction as the Parabolic SAR. The indicator is often used as the trailing stop line.

If the long position is open (i.e., the price is above the SAR line), the Parabolic SAR line will go up, regardless of what direction the prices take. The amount of movement of the Parabolic SAR line depends on the value of the price movement.

The moving average, due to its natural lag, is also suitable for trailing the stop line following the price. For a long position, if the indicator line is below the price, then the stop can be trailed following the price using the moving average value. The price will cross the moving average naturally and the stop will be triggered. For a short position, the stop should follow the moving average line if the price is below it.

I previously wrote an article about connecting any trailing to EAs: "How to develop any type of Trailing Stop and connect it to an EA". In that article, I suggest creating trailing classes and simply connecting them to the EA.

Let's take advantage of this opportunity. The only thing that does not suit us very well is that each symbol or magic number applies its own loop of iterating through open positions. This means that we will have to refine the class from the article. Fortunately, it is not difficult.

Let's download the Trailings.mqh file from the ones attached to the article. This is the only file we currently need. In the previous article, we created a folder containing class and EA files for trading by deal history: \MQL5\Experts\TradingByHistoryDeals. Save the downloaded Trailings.mqh file and open it in MetaEditor.

The main and primary method called from the program is Run(). If we look at its code, we can see that it implements the iteration of open positions:

//+------------------------------------------------------------------+
//| Launch simple trailing with StopLoss offset from the price       |
//+------------------------------------------------------------------+
bool CSimpleTrailing::Run(void)
  {
//--- if disabled, leave
   if(!this.m_active)
      return false;
//--- trailing variables
   MqlTick tick = {};                           // price structure
   bool    res  = true;                         // result of modification of all positions
//--- check the correctness of the data by symbol
   if(this.m_point==0)
     {
      //--- let's try to get the data again
      ::ResetLastError();
      this.SetSymbol(this.m_symbol);
      if(this.m_point==0)
        {
         ::PrintFormat("%s: Correct data was not received for the %s symbol. Error %d",__FUNCTION__, this.m_symbol, ::GetLastError());
         return false;
        }
     }
     
//--- in a loop by the total number of open positions
   int total =::PositionsTotal();
   for(int i = total - 1; i >= 0; i--)
     {
      //--- get the ticket of the next position
      ulong  pos_ticket =::PositionGetTicket(i);
      if(pos_ticket == 0)
         continue;
         
      //--- get the symbol and position magic
      string pos_symbol = ::PositionGetString(POSITION_SYMBOL);
      long   pos_magic  = ::PositionGetInteger(POSITION_MAGIC);
      
      //--- if the position does not match the filter by symbol and magic number, leave
      if((this.m_magic != -1 && pos_magic != this.m_magic) || (pos_symbol != this.m_symbol))
         continue;
      
      //--- if failed to get the prices, move on
      if(!::SymbolInfoTick(this.m_symbol, tick))
         continue;

      //--- get the position type, its opening price and StopLoss level
      ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)::PositionGetInteger(POSITION_TYPE);
      double             pos_open =::PositionGetDouble(POSITION_PRICE_OPEN);
      double             pos_sl   =::PositionGetDouble(POSITION_SL);
      
      //--- get the calculated StopLoss level
      double value_sl = this.GetStopLossValue(pos_type, tick);
      
      //--- if StopLoss modification conditions are suitable, modify the position stop level and add the result to the res variable
      if(this.CheckCriterion(pos_type, pos_open, pos_sl, value_sl, tick))
         res &=this.ModifySL(pos_ticket, value_sl);
     }
//--- at the end of the loop, return the result of modifying each position that matches the "symbol/magic" filter
   return res;
  }

If we call the method for each symbol trading object (see the first article), we will have the number of open position iteration loops equal to the number of symbols used in trading. This is not optimal. Let's make it so that the program itself will have the loop for iterating through all open positions, and these methods will be called from this loop. But the method also has a built-in loop... This means that we need to create another Run() method in the trailing class, but specifying the ticket of the position whose stop needs to be moved, and call it from the main loop.

In the simple trailing class, declare a new method to get the ticket of the necessary position:

//+------------------------------------------------------------------+
//| Class of the position StopLoss simple trailing                   |
//+------------------------------------------------------------------+
class CSimpleTrailing : public CObject
  {
private:
//--- check the criteria for modifying the StopLoss position and return the flag
   bool              CheckCriterion(ENUM_POSITION_TYPE pos_type, double pos_open, double pos_sl, double value_sl, MqlTick &tick);

//--- modify StopLoss of a position by its ticket
   bool              ModifySL(const ulong ticket, const double stop_loss);

//--- return StopLevel in points
   int               StopLevel(void);

protected: 
   string            m_symbol;                        // trading symbol
   long              m_magic;                         // EA ID
   double            m_point;                         // Symbol Point
   int               m_digits;                        // Symbol digits
   int               m_offset;                        // stop distance from price
   int               m_trail_start;                   // profit in points for launching trailing
   uint              m_trail_step;                    // trailing step
   uint              m_spread_mlt;                    // spread multiplier for returning StopLevel value
   bool              m_active;                        // trailing activity flag

//--- calculate and return the StopLoss level of the selected position
   virtual double    GetStopLossValue(ENUM_POSITION_TYPE pos_type, MqlTick &tick);

public:
//--- set trailing parameters
   void              SetSymbol(const string symbol)
                       {
                        this.m_symbol = (symbol==NULL || symbol=="" ? ::Symbol() : symbol);
                        this.m_point  =::SymbolInfoDouble(this.m_symbol, SYMBOL_POINT);
                        this.m_digits = (int)::SymbolInfoInteger(this.m_symbol, SYMBOL_DIGITS);
                       }
   void              SetMagicNumber(const long magic)       { this.m_magic = magic;       }
   void              SetStopLossOffset(const int offset)    { this.m_offset = offset;     }
   void              SetTrailingStart(const int start)      { this.m_trail_start = start; }
   void              SetTrailingStep(const uint step)       { this.m_trail_step = step;   }
   void              SetSpreadMultiplier(const uint value)  { this.m_spread_mlt = value;  }
   void              SetActive(const bool flag)             { this.m_active = flag;       }

//--- return trailing parameters
   string            Symbol(void)                     const { return this.m_symbol;       }
   long              MagicNumber(void)                const { return this.m_magic;        }
   int               StopLossOffset(void)             const { return this.m_offset;       }
   int               TrailingStart(void)              const { return this.m_trail_start;  }
   uint              TrailingStep(void)               const { return this.m_trail_step;   }
   uint              SpreadMultiplier(void)           const { return this.m_spread_mlt;   }
   bool              IsActive(void)                   const { return this.m_active;       }

//--- launch trailing with StopLoss offset from the price
   bool              Run(void);
   bool              Run(const ulong pos_ticket);

//--- constructors
                     CSimpleTrailing() : m_symbol(::Symbol()), m_point(::Point()), m_digits(::Digits()),
                                         m_magic(-1), m_trail_start(0), m_trail_step(0), m_offset(0), m_spread_mlt(2) {}
                     CSimpleTrailing(const string symbol, const long magic,
                                     const int trailing_start, const uint trailing_step, const int offset);
//--- destructor
                    ~CSimpleTrailing() {}
  };

Let's write its implementation outside the class body:

//+------------------------------------------------------------------+
//| Launch simple trailing with StopLoss offset from the price       |
//+------------------------------------------------------------------+
bool CSimpleTrailing::Run(const ulong pos_ticket)
  {
//--- if trailing is disabled, or the ticket is invalid, we leave
   if(!this.m_active || pos_ticket==0)
      return false;
      
//--- trailing variables
   MqlTick tick = {};   // price structure
   
//--- check the correctness of the data by symbol
   ::ResetLastError();
   if(this.m_point==0)
     {
      //--- let's try to get the data again
      this.SetSymbol(this.m_symbol);
      if(this.m_point==0)
        {
         ::PrintFormat("%s: Correct data was not received for the %s symbol. Error %d",__FUNCTION__, this.m_symbol, ::GetLastError());
         return false;
        }
     }
//--- select a position by ticket
   if(!::PositionSelectByTicket(pos_ticket))
     {
      ::PrintFormat("%s: PositionSelectByTicket(%I64u) failed. Error %d",__FUNCTION__, pos_ticket, ::GetLastError());
      return false;
     }
     
//--- if prices could not be obtained, return 'false'
   if(!::SymbolInfoTick(this.m_symbol, tick))
      return false;

//--- get the position type, its opening price and StopLoss level
   ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)::PositionGetInteger(POSITION_TYPE);
   double             pos_open =::PositionGetDouble(POSITION_PRICE_OPEN);
   double             pos_sl   =::PositionGetDouble(POSITION_SL);
   
//--- get the calculated StopLoss level
   double value_sl = this.GetStopLossValue(pos_type, tick);
   
   //--- if the conditions for modifying StopLoss are suitable, return the result of modifying the position stop
   if(this.CheckCriterion(pos_type, pos_open, pos_sl, value_sl, tick))
      return(this.ModifySL(pos_ticket, value_sl));
//--- conditions for modification are not suitable
   return false;
  }

Now this method will be called either from the Run() method, which has no formal parameters, but in which a loop is organized through all open positions, or directly from the program with the required ticket passed to it.

Let's refine the Run() method without formal parameters. Remove the code block from the loop:

   for(int i = total - 1; i >= 0; i--)
     {
      //--- get the ticket of the next position
      ulong  pos_ticket =::PositionGetTicket(i);
      if(pos_ticket == 0)
         continue;
         
      //--- get the symbol and position magic
      string pos_symbol = ::PositionGetString(POSITION_SYMBOL);
      long   pos_magic  = ::PositionGetInteger(POSITION_MAGIC);
      
      //--- if the position does not match the filter by symbol and magic number, leave
      if((this.m_magic != -1 && pos_magic != this.m_magic) || (pos_symbol != this.m_symbol))
         continue;
      
      //--- if failed to get the prices, move on
      if(!::SymbolInfoTick(this.m_symbol, tick))
         continue;

      //--- get the position type, its opening price and StopLoss level
      ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)::PositionGetInteger(POSITION_TYPE);
      double             pos_open =::PositionGetDouble(POSITION_PRICE_OPEN);
      double             pos_sl   =::PositionGetDouble(POSITION_SL);
      
      //--- get the calculated StopLoss level
      double value_sl = this.GetStopLossValue(pos_type, tick);
      
      //--- if StopLoss modification conditions are suitable, modify the position stop level and add the result to the res variable
      if(this.CheckCriterion(pos_type, pos_open, pos_sl, value_sl, tick))
         res &=this.ModifySL(pos_ticket, value_sl);
     }

Add calling the new method instead:

//+------------------------------------------------------------------+
//| Launch simple trailing with StopLoss offset from the price       |
//+------------------------------------------------------------------+
bool CSimpleTrailing::Run(void)
  {
//--- if disabled, leave
   if(!this.m_active)
      return false;
//--- trailing variables
   bool res  = true; // result of modification of all positions
   
//--- check the correctness of the data by symbol
   if(this.m_point==0)
     {
      //--- let's try to get the data again
      ::ResetLastError();
      this.SetSymbol(this.m_symbol);
      if(this.m_point==0)
        {
         ::PrintFormat("%s: Correct data was not received for the %s symbol. Error %d",__FUNCTION__, this.m_symbol, ::GetLastError());
         return false;
        }
     }
     
//--- in a loop by the total number of open positions
   int total =::PositionsTotal();
   for(int i = total - 1; i >= 0; i--)
     {
      //--- get the ticket of the next position
      ulong  pos_ticket =::PositionGetTicket(i);
      if(pos_ticket == 0)
         continue;
         
      //--- get the symbol and position magic
      string pos_symbol = ::PositionGetString(POSITION_SYMBOL);
      long   pos_magic  = ::PositionGetInteger(POSITION_MAGIC);
      
      //--- if the position does not match the filter by symbol and magic number, leave
      if((this.m_magic != -1 && pos_magic != this.m_magic) || (pos_symbol != this.m_symbol))
         continue;
      
      res &=this.Run(pos_ticket);
     }
//--- at the end of the loop, return the result of modifying each position that matches the "symbol/magic" filter
   return res;
  }

In the trailing class by the specified value, also declare the new Run() method specifying the position ticket:

//+------------------------------------------------------------------+
//| Trailing class based on a specified value                        |
//+------------------------------------------------------------------+
class CTrailingByValue : public CSimpleTrailing
  {
protected:
   double            m_value_sl_long;     // StopLoss level for long positions
   double            m_value_sl_short;    // StopLoss level for short positions

//--- calculate and return the StopLoss level of the selected position
   virtual double    GetStopLossValue(ENUM_POSITION_TYPE pos_type, MqlTick &tick);

public:
//--- return StopLoss level for (2) long and (2) short positions
   double            StopLossValueLong(void)    const { return this.m_value_sl_long;   }
   double            StopLossValueShort(void)   const { return this.m_value_sl_short;  }

//--- launch trailing with the specified StopLoss offset from the price
   bool              Run(const double value_sl_long, double value_sl_short);
   bool              Run(const ulong pos_ticket, const double value_sl_long, double value_sl_short);

//--- constructors
                     CTrailingByValue(void) : CSimpleTrailing(::Symbol(), -1, 0, 0, 0), m_value_sl_long(0), m_value_sl_short(0) {}
                     
                     CTrailingByValue(const string symbol, const long magic, const int trail_start, const uint trail_step, const int trail_offset) :
                                      CSimpleTrailing(symbol, magic, trail_start, trail_step, trail_offset), m_value_sl_long(0), m_value_sl_short(0) {}
//--- destructor
                    ~CTrailingByValue(void){}
  };

Write its implementation outside the class body:

//+------------------------------------------------------------------+
//| Launch trailing with the specified StopLoss offset from the price|
//+------------------------------------------------------------------+
bool CTrailingByValue::Run(const ulong pos_ticket,const double value_sl_long,double value_sl_short)
  {
   this.m_value_sl_long =value_sl_long;
   this.m_value_sl_short=value_sl_short;
   return CSimpleTrailing::Run(pos_ticket);
  }

Here we call the Run() method added to the simple trailing class along with the specified position ticket.

Find the details about the trailing classes in the article we took the file with trailing stops from.

Symbol trading class created in the previous article stores the lists of deals and the CTrade trading class of the Standard Library. To avoid modifying it to include trailing classes, we will create a new class based on it we will add handling trailing classes to.

In the \MQL5\Experts\TradingByHistoryDeals\ terminal folder, create a new file SymbolTradeExt.mqh of the CSymbolTradeExt class. The file should have the file of trailing classes and CSymbolTrade class file included. The newly created class should be inherited from the latter:

//+------------------------------------------------------------------+
//|                                               SymbolTradeExt.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include "Trailings.mqh"
#include "SymbolTrade.mqh"

class CSymbolTradeExt : public CSymbolTrade
  {
  }

When using the trailing stop class taken from the article, we are provided with trailing stops for parabolic and all types of standard moving averages. The class also has a trailing function based on the specified values, but we will not use it here as it requires calculating stop levels in its own program and passing them in the parameters of the called Run() method of the trailing class. This could be, for example, the calculation of ATR indicator and passing the calculated values to the trailing class.

Let's implement the enumeration of trailing modes:

//+------------------------------------------------------------------+
//|                                               SymbolTradeExt.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include "Trailings.mqh"
#include "SymbolTrade.mqh"

enum ENUM_TRAILING_MODE    // Enumeration of trailing modes
  {
   TRAILING_MODE_SIMPLE=2, // Simple trailing
   TRAILING_MODE_SAR,      // Trailing by Parabolic SAR
   TRAILING_MODE_AMA,      // Trailing by adjustable moving average
   TRAILING_MODE_DEMA,     // Trailing by double exponential moving average
   TRAILING_MODE_FRAMA,    // Trailing by fractal adaptive moving average 
   TRAILING_MODE_MA,       // Trailing by simple moving average
   TRAILING_MODE_TEMA,     // Trailing by triple exponential moving average
   TRAILING_MODE_VIDYA,    // Trailing by moving average with dynamic averaging period
  };

class CSymbolTradeExt : public CSymbolTrade
  {
  }

Why do enumeration constant values start with 2 instead of zero? The EA we will be using to create the new one also lists testing modes:

enum ENUM_TESTING_MODE
  {
   TESTING_MODE_ORIGIN,    /* Original trading                          */ 
   TESTING_MODE_SLTP,      /* Specified StopLoss and TakeProfit values  */ 
  };

There are two constants here: the original trading (0) and trading with the specified stop order values (1). Later, we will add here new constants corresponding to the trailing enumeration ones. It is for this reason that the values of the trailing enumeration constants start with the value of 2.

In the private section of the class, declare a pointer to the trailing stops class object and a variable for storing the chart period for calculating the indicators used in trailing stops. In the public section, we declare a method for setting trailing parameters, a method for starting position trailing, constructors, and a destructor:

class CSymbolTradeExt : public CSymbolTrade
  {
private:
   CSimpleTrailing  *m_trailing;    // Trailing class object
   ENUM_TIMEFRAMES   m_timeframe;   // Timeframe for calculating the indicator for trailing
public:
//--- Set trailing and its parameters
   bool              SetTrailing(const ENUM_TRAILING_MODE trailing_mode, const int data_index, const long magic, const int start, const int step, const int offset, const MqlParam &param[]);
//--- Start a trail of the position specified by the ticket
   void              Trailing(const ulong pos_ticket);

//--- Constructor/destructor
                     CSymbolTradeExt() : m_trailing(NULL), m_timeframe(::Period()) { this.SetSymbol(::Symbol()); }
                     CSymbolTradeExt(const string symbol, const ENUM_TIMEFRAMES timeframe);
                    ~CSymbolTradeExt();
  };

In the class constructor, namely in the initialization string, a symbol is passed to the parent class constructor. The value passed in the formal parameters is set as the indicator calculation timeframe, while the pointer to the trailing class object is initialized by NULL:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSymbolTradeExt::CSymbolTradeExt(const string symbol, const ENUM_TIMEFRAMES timeframe) : CSymbolTrade(symbol)
  {
   this.m_trailing=NULL;
   this.m_timeframe=timeframe;
  }

It is deleted in the class destructor if the trailing object was created:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CSymbolTradeExt::~CSymbolTradeExt()
  {
//--- delete the created trailing object
   if(this.m_trailing!=NULL)
      delete this.m_trailing;
  }

Since different types of trailing stops, working on the basis of different types of indicators, have different setup parameters, we will pass all this set of parameters to the method for setting the trailing stop parameters through the MqlParam structure. It is just that for each trailing the set of parameters will be its own, and each field of the structure will carry the value of its indicator parameter, inherent only to it.

//+------------------------------------------------------------------+
//| Set trailing parameters                                          |
//+------------------------------------------------------------------+
bool CSymbolTradeExt::SetTrailing(const ENUM_TRAILING_MODE trailing_mode, const int data_index, const long magic, const int start, const int step, const int offset, const MqlParam &param[])
  {
//--- Set trailing parameters (only necessary structure fields are used for each indicator type)
   int                ma_period  = (int)param[0].integer_value;
   int                ma_shift   = (int)param[1].integer_value;
   ENUM_APPLIED_PRICE ma_price   = (ENUM_APPLIED_PRICE)param[2].integer_value;
   ENUM_MA_METHOD     ma_method  = (ENUM_MA_METHOD)param[3].integer_value;
   int                fast_ema   = (int)param[4].integer_value;
   int                slow_ema   = (int)param[5].integer_value;
   int                period_cmo = (int)param[6].integer_value;
   double             sar_step   = param[0].double_value;
   double             sar_max    = param[1].double_value;
   
//--- depending on the trailing type, we create a trailing object
//--- if the value passed as the calculation period is less than the allowed value, then each indicator is assigned its own default value
   switch(trailing_mode)
     {
      case TRAILING_MODE_SIMPLE  :
        this.m_trailing=new CSimpleTrailing(this.Symbol(), magic, start, step, offset);
        break;
      case TRAILING_MODE_AMA     :
        this.m_trailing=new CTrailingByAMA(this.Symbol(), this.m_timeframe, magic,
                                           (ma_period<1 ?  9 : ma_period), (fast_ema<1  ?  2 : fast_ema), (slow_ema<1  ? 30 : slow_ema), ma_shift, ma_price, start, step, offset);
        break;
      case TRAILING_MODE_DEMA    :
        this.m_trailing=new CTrailingByDEMA(this.Symbol(), this.m_timeframe, magic, (ma_period==0 ? 14 : ma_period), ma_shift, ma_price, start, step, offset);
        break;
      case TRAILING_MODE_FRAMA   :
        this.m_trailing=new CTrailingByFRAMA(this.Symbol(), this.m_timeframe, magic, (ma_period==0 ? 14 : ma_period), ma_shift, ma_price, start, step, offset);
        break;
      case TRAILING_MODE_MA      :
        this.m_trailing=new CTrailingByMA(this.Symbol(), this.m_timeframe, magic, ma_period, (ma_period==0 ? 10 : ma_period), ma_method, ma_price, start, step, offset);
        break;
      case TRAILING_MODE_TEMA    :
        this.m_trailing=new CTrailingByTEMA(this.Symbol(), this.m_timeframe, magic, (ma_period==0 ? 14 : ma_period), ma_shift, ma_price, start, step, offset);
        break;
      case TRAILING_MODE_VIDYA   :
        this.m_trailing=new CTrailingByVIDYA(this.Symbol(), this.m_timeframe, magic, (period_cmo<1 ? 9 : period_cmo), (ma_period==0 ? 12 : ma_period), ma_shift, ma_price, start, step, offset);
        break;
      case TRAILING_MODE_SAR     :
        this.m_trailing=new CTrailingBySAR(this.Symbol(), this.m_timeframe, magic, (sar_step<0.0001 ? 0.02 : sar_step), (sar_max<0.02 ? 0.2 : sar_max), start, step, offset);
        break;
      default :
        break;
     }
//--- something went wrong - return 'false'
   if(this.m_trailing==NULL)
      return false;
      
//--- all is well - make the trail active and return 'true'
   this.m_trailing.SetActive(true);
   return true;
  }

When calling this method for different trailing types, it is important to fill the MqlParam structure correctly. We will check this when we modify the EA to use trailing.

The method that starts the trailing of the position specified by the ticket:

//+------------------------------------------------------------------+
//| Start trailing of the position specified by the ticket           |
//+------------------------------------------------------------------+
void CSymbolTradeExt::Trailing(const ulong pos_ticket)
  {
    if(this.m_trailing!=NULL)
      this.m_trailing.Run(pos_ticket);
  }

From the EA's loop by open positions (depending on the position symbol), we will get the symbol trading object from the list. From that object, we call the method to trail the position stop whose ticket we will pass to the method. If the trailing object exists, its new Run() method is called with the position ticket specified.


Testing different types of TrailingStop

To perform the test, let's use the TradingByHistoryDeals_SLTP.mq5 EA from the previous article and save it in the same folder \MQL5\Experts\TradingByHistoryDeals\ as TradingByHistoryDeals_Ext.mq5.

Including the CSymbolTrade class file is replaced with including the CSymbolTradeExt class file, include the file of trailing classes and expand the list of constants of test modes enumeration by adding the selection of the necessary trailing:

//+------------------------------------------------------------------+
//|                                    TradingByHistoryDeals_Ext.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include "SymbolTradeExt.mqh"
#include "Trailings.mqh"

enum ENUM_TESTING_MODE
  {
   TESTING_MODE_ORIGIN,       /* Original trading                          */ 
   TESTING_MODE_SLTP,         /* Specified StopLoss and TakeProfit values  */ 
   TESTING_MODE_TRAIL_SIMPLE, /* Simple Trailing                           */ 
   TESTING_MODE_TRAIL_SAR,    /* Trailing by Parabolic SAR indicator       */ 
   TESTING_MODE_TRAIL_AMA,    /* Trailing by AMA indicator                 */ 
   TESTING_MODE_TRAIL_DEMA,   /* Trailing by DEMA indicator                */ 
   TESTING_MODE_TRAIL_FRAMA,  /* Trailing by FRAMA indicator               */ 
   TESTING_MODE_TRAIL_MA,     /* Trailing by MA indicator                  */ 
   TESTING_MODE_TRAIL_TEMA,   /* Trailing by TEMA indicator                */ 
   TESTING_MODE_TRAIL_VIDYA,  /* Trailing by VIDYA indicator               */ 
  };

//+------------------------------------------------------------------+
//| Expert                                                           |
//+------------------------------------------------------------------+

In the EA inputs, add the variables for selecting trailing parameters:

//+------------------------------------------------------------------+
//| Expert                                                           |
//+------------------------------------------------------------------+
//--- input parameters
input    group                " - Strategy parameters - "
input    string               InpTestedSymbol   =  "";                  /* The symbol being tested in the tester        */ 
input    long                 InpTestedMagic    =  -1;                  /* The magic number being tested in the tester  */ 
sinput   bool                 InpShowDataInLog  =  false;               /* Show collected data in the log               */ 

input    group                " - Stops parameters - "
input    ENUM_TESTING_MODE    InpTestingMode    =  TESTING_MODE_ORIGIN; /* Testing Mode                                 */ 
input    int                  InpStopLoss       =  300;                 /* StopLoss in points                           */ 
input    int                  InpTakeProfit     =  500;                 /* TakeProfit in points                         */ 

input    group                " - Trailing Parameters -"
input    bool                 InpSetStopLoss    =  true;                /* Set Initial StopLoss                         */ 
input    bool                 InpSetTakeProfit  =  true;                /* Set Initial TakeProfit                       */ 
input    int                  InpTrailingStart  =  150;                 /* Trailing start                               */ // Profit in points to start trailing
input    int                  InpTrailingStep   =  50;                  /* Trailing step in points                      */ 
input    int                  InpTrailingOffset =  0;                   /* Trailing offset in points                    */ 

input    group                " - Indicator Parameters -"
input    ENUM_TIMEFRAMES      InpIndTimeframe   =  PERIOD_CURRENT;      /* Indicator's timeframe                        */ // Timeframe of the indicator used in trailing calculation
input    int                  InpMAPeriod       =  0;                   /* MA Period                                    */ 
input    int                  InpMAShift        =  0;                   /* MA Shift                                     */ 
input    int                  InpFastEMAPeriod  =  2;                   /* AMA Fast EMA Period                          */ 
input    int                  InpSlowEMAPeriod  =  30;                  /* AMA Slow EMA Period                          */ 
input    int                  InpCMOPeriod      =  9;                   /* VIDYA CMO Period                             */ 
input    double               InpSARStep        =  0.02;                /* Parabolic SAR Step                           */ 
input    double               InpSARMax         =  0.2;                 /* Parabolic SAR Max                            */ 
input    ENUM_APPLIED_PRICE   InpAppliedPrice   =  PRICE_CLOSE;         /* MA Applied Price                             */ 
input    ENUM_MA_METHOD       InpMAMethod       =  MODE_SMA;            /* MA Smoothing Method                          */ 
input    int                  InpDataIndex      =  1;                   /* Indicator data index                         */ // Bar of data received frrom the indicator


InpMAPeriod is set to zero by default here. The reason for this is that each type of moving average has its own default period value. When this parameter is set to zero, the default value set for the indicator is passed to the trailing class, all data will be correct if simple default values are required for the indicator used in the trailing.

In the entire code, replace all occurrences of the CSymbolTrade with CSymbolTradeExt. Now we have a new symbol trading object class CSymbolTradeExt inherited from the previous CSymbolTrade class implemented in the previous article. This is the class, in which the trailing class object is declared. Therefore, we replaced the previous class type with a new one. In fact, it is not necessary to do this everywhere, but only where trailing is required. Let's not delve into into the details of class inheritance here, but simply replace the old class with the new one.

Trailings will be called for each position according to its ticket. In order to access the position trailing, we need to get the trading object of the symbol the position is open for from the list. To do this, we will implement the function that returns the pointer to the trading object from the list by symbol name:

//+------------------------------------------------------------------+
//| Return the pointer to the symbol trading object by name          |
//+------------------------------------------------------------------+
CSymbolTrade *GetSymbolTrade(const string symbol, CArrayObj *list)
  {
   SymbTradeTmp.SetSymbol(symbol);
   list.Sort();
   int index=list.Search(&SymbTradeTmp);
   return list.At(index);
  }

The function receives the name of the symbol a trading object must be returned for and the pointer to the list that stores pointers to the symbols' trading objects. The symbol name passed to the function is set to the temporary trading object and the index of the object with that symbol name is searched for in the list. As a result, the pointer to the desired object is returned from the list by index. If such an object is not in the list, the index is equal to -1 and NULL is returned from the list.

When creating an array of symbols used, symbol trading objects are created, and the trailing stops specified in the inputs should be initialized in them. Let's refine the function that creates an array of used symbols:

//+------------------------------------------------------------------+
//| Creates an array of used symbols                                 |
//+------------------------------------------------------------------+
bool CreateListSymbolTrades(SDeal &array_deals[], CArrayObj *list_symbols)
  {
   bool res=true;                      // result
   MqlParam param[7]={};               // trailing parameters
   int total=(int)array_deals.Size();  // total number of deals in the array
   
//--- if the deal array is empty, return 'false'
   if(total==0)
     {
      PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
      return false;
     }
   
//--- in a loop through the deal array
   CSymbolTradeExt *SymbolTrade=NULL;
   for(int i=0; i<total; i++)
     {
      //--- get the next deal and, if it is neither buy nor sell, move on to the next one
      SDeal deal_str=array_deals[i];
      if(deal_str.type!=DEAL_TYPE_BUY && deal_str.type!=DEAL_TYPE_SELL)
         continue;
      
      //--- find a trading object in the list whose symbol is equal to the deal symbol
      string symbol=deal_str.Symbol();
      SymbTradeTmp.SetSymbol(symbol);
      list_symbols.Sort();
      int index=list_symbols.Search(&SymbTradeTmp);
      
      //--- if the index of the desired object in the list is -1, there is no such object in the list
      if(index==WRONG_VALUE)
        {
         //--- we create a new trading symbol object and, if creation fails,
         //--- add 'false' to the result and move on to the next deal
         SymbolTrade=new CSymbolTradeExt(symbol, InpIndTimeframe);
         if(SymbolTrade==NULL)
           {
            res &=false;
            continue;
           }
         //--- if failed to add a symbol trading object to the list,
         //--- delete the newly created object, add 'false' to the result
         //--- and we move on to the next deal
         if(!list_symbols.Add(SymbolTrade))
           {
            delete SymbolTrade;
            res &=false;
            continue;
           }
         //--- initialize trailing specified in the settings in the trading object
         ENUM_TRAILING_MODE mode=(ENUM_TRAILING_MODE)InpTestingMode;
         SetTrailingParams(mode, param);
         SymbolTrade.SetTrailing(mode, InpDataIndex, InpTestedMagic, InpTrailingStart, InpTrailingStep, InpTrailingOffset, param);
         
        }
      //--- otherwise, if the trading object already exists in the list, we get it by index
      else
        {
         SymbolTrade=list_symbols.At(index);
         if(SymbolTrade==NULL)
            continue;
        }
         
      //--- if the current deal is not yet in the list of deals of the symbol trading object
      if(SymbolTrade.GetDealByTime(deal_str.time)==NULL)
        {
         //--- create a deal object according to its sample structure
         CDeal *deal=CreateDeal(deal_str);
         if(deal==NULL)
           {
            res &=false;
            continue;
           }
         //--- add the result of adding the deal object to the list of deals of a symbol trading object to the result value
         res &=SymbolTrade.AddDeal(deal);
        }
     }
//--- return the final result of creating trading objects and adding deals to their lists
   return res;
  }

Here we added the declaration of the indicator's input parameter structure and a small block of code, where trailing in the trading object is initialized.

Implement the special function for setting the parameters of the specified trailing:

//+------------------------------------------------------------------+
//| Set the trailing parameters according to its selected type       |
//+------------------------------------------------------------------+
void SetTrailingParams(const ENUM_TRAILING_MODE mode, MqlParam &param[])
  {
//--- reset all parameters
   ZeroMemory(param);

//--- depending on the selected trailing type, we set the indicator parameters
   switch(mode)
     {
      case TRAILING_MODE_SAR     :
        param[0].type=TYPE_DOUBLE;
        param[0].double_value=InpSARStep;
        
        param[1].type=TYPE_DOUBLE;
        param[1].double_value=InpSARMax;
        break;
      
      case TRAILING_MODE_AMA     :
        param[0].type=TYPE_INT;
        param[0].integer_value=InpMAPeriod;
        
        param[1].type=TYPE_INT;
        param[1].integer_value=InpMAShift;

        param[2].type=TYPE_INT;
        param[2].integer_value=InpAppliedPrice;

        param[4].type=TYPE_INT;
        param[4].integer_value=InpFastEMAPeriod;

        param[5].type=TYPE_INT;
        param[5].integer_value=InpSlowEMAPeriod;
        break;
      
      case TRAILING_MODE_DEMA    :
        param[0].type=TYPE_INT;
        param[0].integer_value=InpMAPeriod;
        
        param[1].type=TYPE_INT;
        param[1].integer_value=InpMAShift;

        param[2].type=TYPE_INT;
        param[2].integer_value=InpAppliedPrice;
        break;
      
      case TRAILING_MODE_FRAMA   :
        param[0].type=TYPE_INT;
        param[0].integer_value=InpMAPeriod;
        
        param[1].type=TYPE_INT;
        param[1].integer_value=InpMAShift;

        param[2].type=TYPE_INT;
        param[2].integer_value=InpAppliedPrice;
        break;
      
      case TRAILING_MODE_MA      :
        param[0].type=TYPE_INT;
        param[0].integer_value=InpMAPeriod;
        
        param[1].type=TYPE_INT;
        param[1].integer_value=InpMAShift;

        param[2].type=TYPE_INT;
        param[2].integer_value=InpAppliedPrice;

        param[3].type=TYPE_INT;
        param[3].integer_value=InpMAMethod;
        break;
      
      case TRAILING_MODE_TEMA    :
        param[0].type=TYPE_INT;
        param[0].integer_value=InpMAPeriod;
        
        param[1].type=TYPE_INT;
        param[1].integer_value=InpMAShift;

        param[2].type=TYPE_INT;
        param[2].integer_value=InpAppliedPrice;
        break;
      
      case TRAILING_MODE_VIDYA   :
        param[0].type=TYPE_INT;
        param[0].integer_value=InpMAPeriod;
        
        param[1].type=TYPE_INT;
        param[1].integer_value=InpMAShift;

        param[2].type=TYPE_INT;
        param[2].integer_value=InpAppliedPrice;

        param[6].type=TYPE_INT;
        param[6].integer_value=InpCMOPeriod;
        break;
         
      case TRAILING_MODE_SIMPLE  :
        break;
      
      default:
        break;
     }
  }

Here, for each of the trailing types, for the indicators used in the trailing, the values specified in the EA inputs are set in the structure. The values assigned to the structure fields are checked for validity and adjusted in the new CSymbolTradeExt class of the symbol trade object discussed above.

Let's improve the function for trading based on deal history. Now we need to clearly distinguish what type of testing is being used.
To test the original trading, there is no need to set stop orders
— all positions are closed based on closing historical deals.

If test trading with different stop order sizes, you need to obtain the correct sizes and set them for the position being opened. When testing trailing stops, it is not necessary to set stops, however, in order to avoid large drawdowns and "overstaying," it is recommended to set initial stops, and then trailing stops will move them according to the specified logic. The EA settings contain parameters that determine whether initial stop orders should be placed:

input    bool                 InpSetStopLoss    =  true;                /* Set Initial StopLoss                         */ 
input    bool                 InpSetTakeProfit  =  true;                /* Set Initial TakeProfit                       */ 

When using them, you also need to get the correct stop sizes and set them for the position being opened:

//+------------------------------------------------------------------+
//| Trading by history                                               |
//+------------------------------------------------------------------+
void TradeByHistory(const string symbol="", const long magic=-1)
  {
   datetime time=0;
   int total=ExtListSymbols.Total();   // number of trading objects in the list
   
//--- in a loop by all symbol trading objects
   for(int i=0; i<total; i++)
     {
      //--- get another trading object
      CSymbolTradeExt *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         continue;
      
      //--- get the current deal pointed to by the deal list index
      CDeal *deal=obj.GetDealCurrent();
      if(deal==NULL)
         continue;
      
      //--- sort the deal by magic number and symbol
      if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol))
         continue;
      
      //--- sort the deal by type (only buy/sell deals)
      ENUM_DEAL_TYPE type=deal.TypeDeal();
      if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL)
         continue;
      
      //--- if this is a deal already handled in the tester, move on to the next one
      if(deal.TicketTester()>0)
         continue;
      
      //--- if the deal time has not yet arrived, move to the next trading object of the next symbol
      if(!obj.CheckTime(deal.Time()))
         continue;

      //--- in case of a market entry deal
      ENUM_DEAL_ENTRY entry=deal.Entry();
      if(entry==DEAL_ENTRY_IN)
        {
         //--- set the sizes of stop orders depending on the stop setting method
         //--- stop orders are not used initially (for original trading)  
         ENUM_ORDER_TYPE order_type=(deal.TypeDeal()==DEAL_TYPE_BUY ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
         double sl=0;
         double tp=0;
         //--- in case of the mode for setting the specified stop order values 
         if(InpTestingMode==TESTING_MODE_SLTP)
           {
            //--- get correct values for StopLoss and TakeProfit
            sl=CorrectStopLoss(deal.Symbol(), order_type, ExtStopLoss);
            tp=CorrectTakeProfit(deal.Symbol(), order_type, ExtTakeProfit);
           }
         //--- otherwise, if testing with trailing stops
         else
           {
            if(InpTestingMode!=TESTING_MODE_ORIGIN)
              {
               //--- if allowed in the settings, we set correct stop orders for the positions being opened
               if(InpSetStopLoss)
                  sl=CorrectStopLoss(deal.Symbol(), order_type, ExtStopLoss);
               if(InpSetTakeProfit)
                  tp=CorrectTakeProfit(deal.Symbol(), order_type, ExtTakeProfit);
              }
           }
         
         //--- open a position by deal type
         ulong ticket=(type==DEAL_TYPE_BUY  ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) : 
                       type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : 0);
         
         //--- if a position is opened (we received its ticket)
         if(ticket>0)
           {
            //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket);
            //--- get the position ID in the tester and write it to the properties of the deal object
            long pos_id_tester=0;
            if(HistoryDealSelect(ticket))
              {
               pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
               deal.SetPosIDTester(pos_id_tester);
              }
           }
        }
      
      //--- in case of a market exit deal
      if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY)
        {
         //--- get a deal a newly opened position is based on
         CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID());
         if(deal_in==NULL)
            continue;

         //--- get the position ticket in the tester from the properties of the opening deal
         //--- if the ticket is zero, then most likely the position in the tester is already closed
         ulong ticket_tester=deal_in.TicketTester();
         if(ticket_tester==0)
           {
            PrintFormat("Could not get position ticket, apparently position #%I64d (#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester());
            obj.SetNextDealIndex();
            continue;
           }
         //--- if we reproduce the original trading history in the tester,
         if(InpTestingMode==TESTING_MODE_ORIGIN)
           {
            //--- if the position is closed by ticket
            if(obj.ClosePos(ticket_tester))
              {
               //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
               obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
               deal.SetTicketTester(ticket_tester);
              }
           }
         //--- otherwise, in the tester we work with stop orders placed according to different algorithms, and closing deals are skipped;
         //--- accordingly, for the closing deal, we simply increase the number of deals handled by the tester and
         //--- write the deal ticket in the tester to the properties of the deal object
         else
           {
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket_tester);
           }
        }
      //--- if a ticket is now set in the deal object, then the deal has been successfully handled -
      //--- set the deal index in the list to the next deal
      if(deal.TicketTester()>0)
        {
         obj.SetNextDealIndex();
        }
     }
  }

Now let's implement the function that trails positions:

//+------------------------------------------------------------------+
//| Trail positions                                                  |
//+------------------------------------------------------------------+
void Trailing(void)
  {
//--- variables for getting position properties
   long   magic=-1;
   string symbol="";
   
//--- in a loop through all positions
   int total=PositionsTotal();
   for(int i=total-1; i>=0; i--)
     {
      //--- get the ticket of the next position
      ulong ticket=PositionGetTicket(i);
      if(ticket==0)
         continue;

      //--- get the magic number and position symbol
      ResetLastError();
      if(!PositionGetInteger(POSITION_MAGIC, magic))
        {
         Print("PositionGetInteger() failed. Error ", GetLastError());
         continue;
        }
      if(!PositionGetString(POSITION_SYMBOL, symbol))
        {
         Print("PositionGetString() failed. Error ", GetLastError());
         continue;
        }
      
      //--- if the position does not meet the specified conditions of the magic number and symbol, we move to the next one
      if((InpTestedMagic>-1 && magic!=InpTestedMagic) || (InpTestedSymbol!="" && symbol!=InpTestedSymbol))
         continue;
      
      //--- get a trading object by a symbol name and call its method for trailing a position by ticket
      CSymbolTradeExt *obj=GetSymbolTrade(symbol, &ExtListSymbols);
      if(obj!=NULL)
         obj.Trailing(ticket);
     }
  }

Here everything is simple: we select each position in the loop, check that its magic number and symbol match those entered in the EA settings and, if this is the desired position, we get the trading object of the symbol of this position and call the trailing from it, indicating the position ticket.

Let's implement the function call to the EA's OnTick() handler:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- work only in the strategy tester
   if(!MQLInfoInteger(MQL_TESTER))
      return;
      
//--- Trail open positions
   Trailing();
   
//---  Handle the list of deals from the file
   TradeByHistory(InpTestedSymbol, InpTestedMagic);
  }

All is set. The EA is ready. The full EA code can be found in the files attached to the article.

Let's test different types of trailing stops and compare the results of original trading and trading using trailing stops using different algorithms.

When launching the EA on a symbol chart, it collects the trading history, writes it to the file, and displays an alert indicating the desired tester settings — the initial test date, the initial deposit, and leverage:

The journal displays symbols used in the trading and the number of deals performed at each symbol:

Alert: Now you can run testing
Interval: 2024.09.13 - current date
Initial deposit: 3000.00, leverage 1:500
Symbols used in trading:
  1. AUDUSD trade object. Total deals: 222
  2. EURJPY trade object. Total deals: 120
  3. EURUSD trade object. Total deals: 526
  4. GBPUSD trade object. Total deals: 352
  5. NZDUSD trade object. Total deals: 182
  6. USDCAD trade object. Total deals: 22
  7. USDCHF trade object. Total deals: 250
  8. USDJPY trade object. Total deals: 150
  9. XAUUSD trade object. Total deals: 118

NOw let's launch the EA in the tester with recommended tester parameters specified in the alert and duplicated in the journal.

Original trading:

We see that the original trading resulted in a loss of USD 658.

Let's try different trailing stops. Allow the initial stop to be set at 100 points and run each trailing order in the tester.
This will be done in turns without changing other parameters. We will see how trading changes.

Simple trailing:

We got a loss of USD 746.1. Even more than in the original trading.

Let's check trailing using indicators.

Trailing by Parabolic SAR:

Profit - USD 541.8.

Now let's look at trailing stops based on different types of moving averages.

Trailing by АМА:

Profit - USD 806.5.

Trailing by DEMA:

Profit - USD 1397.1.

Trailing by FRAMA:

Profit - USD 1291.6.

Trailing by MA:

Profit - USD 563.1.

Trailing by TEMA:

Profit - USD 1355.1.

Trailing by VIDYA:

Profit - USD 283.3.

So, the final table shows the results when using different types of StopLoss trailing stops relative to the original trading:

#
Trailing type
StopLoss size
TakeProfit size
 Total profit
1
Original trading
100
500
 - 658.0
2
Simple TrailingStop
Initially 100, then stop at 100 points from the price
0
 - 746.1
3
TrailingStop by Parabolic SAR
Initially 100, then stop based on the indicator value
0
 + 541.8
4
TrailingStop by VIDYA
Initially 100, then stop based on the indicator value
0
 -283.3
5
TrailingStop by MA
Initially 100, then stop based on the indicator value
0
 + 563.1
6
TrailingStop by АМА
Initially 100, then stop based on the indicator value
0
 + 806.5
7
TrailingStop by FRAMA
Initially 100, then stop based on the indicator value
0
 + 1291.6
8
TrailingStop by TEMA
Initially 100, then stop based on the indicator value
0
 + 1355.1
9
TrailingStop by DEMA
Initially 100, then stop based on the indicator value
0
 + 1397.1

As a result, when the original trading is unprofitable, a regular simple trailing roughly equivalent to the trailing in the client terminal results in an even greater loss. Let's look at a trailing indicator based on Parabolic SAR positioned as an indicator displaying reversals and stop levels (Stop And Reverse). Indeed, by following the StopLoss level behind the indicator line on the first bar, we made a profit of USD 540.

Let's now look at the results of moving stop positions based on the values of different types of moving averages on the first bar. Trailing on the VIDYA indicator resulted in a loss of USD 280, but all the others showed a profit. Moreover, following the stop along the simple moving average yielded a greater profit than trailing along the Parabolic SAR. The absolute "champion" in improving trading results was double exponential moving average — + USD 1397 of profit.


Conclusion

We have developed an EA that allows us to test our trading in the strategy tester and choose the most suitable trailing stop for our trading style. Please note that all indicator parameters used in trailing tests had standard values, and the trailing settings were chosen randomly.

The most correct solution would be to conduct testing for each individual symbol and select acceptable trailing settings taking into account the characteristics of the movement and volatility of the currency pair. But this EA does not have separate settings for setting custom parameters for each individual traded symbol. But the main thing was that the possibility of testing, analyzing, and improving trading using the strategy tester was demonstrated. This means that the EA can be further developed to allow us to set our own parameters for each symbol — it is not difficult.

All the reviewed class files and the EA are attached to the article. Also included is an archive. Once unpacked, you can immediately obtain the installed files for testing in the required terminal folders.

Programs used in the article:

#
Name
 Type  Description
1
Trailings.mqh
Class library
Trailing class library
2
SymbolTrade.mqh
Class library
Deal structure and class library, symbol trading class
3
SymbolTradeExt.mqh
Class library
Symbol and trailing trading class
4
TradingByHistoryDeals_Ext.mq5
Expert Advisor
The EA for viewing and modifying StopLoss, TakeProfit and trailing stops in the strategy tester for deals and deals performed on the account.
5
MQL5.zip
ZIP archive
The archive of files presented above can be unpacked into the MQL5 directory of the client terminal

Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/16991

Attached files |
Trailings.mqh (101.41 KB)
SymbolTrade.mqh (53.86 KB)
SymbolTradeExt.mqh (12.95 KB)
MQL5.zip (26.49 KB)
Last comments | Go to discussion (4)
Roman Shiredchenko
Roman Shiredchenko | 30 Jan 2025 at 15:38
Thank you very much for the article. It reads in one breath and everything is clear at once.
I will use the code fragments and f -- ii in my bidding robots. Simple trawl also.
One in some robots another trawl in others.
Yevgeniy Koshtenko
Yevgeniy Koshtenko | 30 Jan 2025 at 23:21
TOP article!
den2008224
den2008224 | 31 Jan 2025 at 05:16

Трал по VIDYA:

Profit - 283.3 dollars.

Error: Loss - 283.3 dollars.

Alexey Viktorov
Alexey Viktorov | 31 Jan 2025 at 06:46
den2008224 #:

Error: Loss- $283.3.

In the article, the negative value of the profit is written.

However, the space after the minus was accidentally added.
From Novice to Expert: Demystifying Hidden Fibonacci Retracement Levels From Novice to Expert: Demystifying Hidden Fibonacci Retracement Levels
In this article, we explore a data-driven approach to discovering and validating non-standard Fibonacci retracement levels that markets may respect. We present a complete workflow tailored for implementation in MQL5, beginning with data collection and bar or swing detection, and extending through clustering, statistical hypothesis testing, backtesting, and integration into an MetaTrader 5 Fibonacci tool. The goal is to create a reproducible pipeline that transforms anecdotal observations into statistically defensible trading signals.
Market Simulation (Part 02): Cross Orders (II) Market Simulation (Part 02): Cross Orders (II)
Unlike what was done in the previous article, here we will test the selection option using an Expert Advisor. Although this is not a final solution yet, it will be enough for now. With the help of this article, you will be able to understand how to implement one of the possible solutions.
Neural Networks in Trading: Models Using Wavelet Transform and Multi-Task Attention Neural Networks in Trading: Models Using Wavelet Transform and Multi-Task Attention
We invite you to explore a framework that combines wavelet transforms and a multi-task self-attention model, aimed at improving the responsiveness and accuracy of forecasting in volatile market conditions. The wavelet transform allows asset returns to be decomposed into high and low frequencies, carefully capturing long-term market trends and short-term fluctuations.
MetaTrader 5 Machine Learning Blueprint (Part 3): Trend-Scanning Labeling Method MetaTrader 5 Machine Learning Blueprint (Part 3): Trend-Scanning Labeling Method
We have built a robust feature engineering pipeline using proper tick-based bars to eliminate data leakage and solved the critical problem of labeling with meta-labeled triple-barrier signals. This installment covers the advanced labeling technique, trend-scanning, for adaptive horizons. After covering the theory, an example shows how trend-scanning labels can be used with meta-labeling to improve on the classic moving average crossover strategy.