Post-Factum trading analysis: Selecting trailing stops and new stop levels in the strategy tester
Contents
- Introduction
- Selecting and refining a class of position trailing stops
- Testing different types of TrailingStop
- Conclusion
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 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 ¶m[]); //--- 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 ¶m[]) { //--- 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 ¶m[]) { //--- 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
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
From Novice to Expert: Demystifying Hidden Fibonacci Retracement Levels
Market Simulation (Part 02): Cross Orders (II)
Neural Networks in Trading: Models Using Wavelet Transform and Multi-Task Attention
MetaTrader 5 Machine Learning Blueprint (Part 3): Trend-Scanning Labeling Method
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
Трал по VIDYA:
Profit - 283.3 dollars.Error: Loss - 283.3 dollars.
Error: Loss- $283.3.
In the article, the negative value of the profit is written.
However, the space after the minus was accidentally added.