MQL5 Cookbook: Trading strategy stress testing using custom symbols

Denis Kirichenko | 30 October, 2019

Introduction

Some time ago a new feature was added in the MetaTrader 5 terminal: the possibility to create custom symbols. Now algo traders can act as brokers for themselves in some cases, while they do not need trade servers and can totally control the trading environment. However, "some cases" here only refer to the testing mode. Of course, orders for custom symbols cannot be executed by brokers. Nevertheless, this functionality enables algo traders to test their trading strategies more carefully.

In this article we will create conditions for such testing. Let us start with the custom symbol class.


1. The custom symbol class CiCustomSymbol

The Standard Library provides a class for simplified access to symbol properties. It is the CSymbolInfo class. In fact, the class executes an intermediary function: upon a user request, the class communicates with the server and receives from it a response in the form of a value of the requested property.

Our purpose is to create a similar class for a custom symbol. Moreover, this class functionality will be even wider, because we need to add creation, deletion and some other methods. On the other hand, we will not use methods associate with connection to the server. These include Refresh(), IsSynchronized() etc.

This class for creating a trading environment encapsulates standard features for working with custom symbols.

The class declaration structure is shown below.

//+------------------------------------------------------------------+
//| Class CiCustomSymbol.                                            |
//| Purpose: Base class for a custom symbol.                         |
//+------------------------------------------------------------------+
class CiCustomSymbol : public CObject
  {
   //--- === Data members === ---
private:
   string            m_name;
   string            m_path;
   MqlTick           m_tick;
   ulong             m_from_msc;
   ulong             m_to_msc;
   uint              m_batch_size;
   bool              m_is_selected;
   //--- === Methods === ---
public:
   //--- constructor/destructor
   void              CiCustomSymbol(void);
   void             ~CiCustomSymbol(void) {};
   //--- create/delete
   int               Create(const string _name,const string _path="",const string _origin_name=NULL,
                            const uint _batch_size=1e6,const bool _is_selected=false);
   bool              Delete(void);
   //--- methods of access to protected data
   string            Name(void) const { return(m_name); }
   bool              RefreshRates(void);
   //--- fast access methods to the integer symbol properties
   bool              Select(void) const;
   bool              Select(const bool select);
   //--- service methods
   bool              Clone(const string _origin_symbol,const ulong _from_msc=0,const ulong _to_msc=0);
   bool              LoadTicks(const string _src_file_name);
   bool              ChangeSpread(const uint _spread_size,const uint _spread_markup=0,
                                  const ENUM_SPREAD_BASE _spread_base=SPREAD_BASE_BID);
   //--- API
   bool              SetProperty(ENUM_SYMBOL_INFO_DOUBLE _property,double _val) const;
   bool              SetProperty(ENUM_SYMBOL_INFO_INTEGER _property,long _val) const;
   bool              SetProperty(ENUM_SYMBOL_INFO_STRING _property,string _val) const;
   double            GetProperty(ENUM_SYMBOL_INFO_DOUBLE _property) const;
   long              GetProperty(ENUM_SYMBOL_INFO_INTEGER _property) const;
   string            GetProperty(ENUM_SYMBOL_INFO_STRING _property) const;
   bool              SetSessionQuote(const ENUM_DAY_OF_WEEK _day_of_week,const uint _session_index,
                                     const datetime _from,const datetime _to);
   bool              SetSessionTrade(const ENUM_DAY_OF_WEEK _day_of_week,const uint _session_index,
                                     const datetime _from,const datetime _to);
   int               RatesDelete(const datetime _from,const datetime _to);
   int               RatesReplace(const datetime _from,const datetime _to,const MqlRates &_rates[]);
   int               RatesUpdate(const MqlRates &_rates[]) const;
   int               TicksAdd(const MqlTick &_ticks[]) const;
   int               TicksDelete(const long _from_msc,long _to_msc) const;
   int               TicksReplace(const MqlTick &_ticks[]) const;
   //---
private:
   template<typename PT>
   bool              CloneProperty(const string _origin_symbol,const PT _prop_type) const;
   int               CloneTicks(const MqlTick &_ticks[]) const;
   int               CloneTicks(const string _origin_symbol) const;
  };
//+------------------------------------------------------------------+

Let us begin with the methods which will make a full-featured custom symbol out of a selected symbol.


1.1 The CiCustomSymbol::Create() method

In order to use all custom symbol possibilities, we need to create it or make sure it has been created. 

//+------------------------------------------------------------------+
//| Create a custom symbol                                           |
//| Codes:                                                           |
//|       -1 - failed to create;                                     |
//|        0 - a symbol exists, no need to create;                   |
//|        1 - successfully created.                                 |
//+------------------------------------------------------------------+
int CiCustomSymbol::Create(const string _name,const string _path="",const string _origin_name=NULL,
                           const uint _batch_size=1e6,const bool _is_selected=false)
  {
   int res_code=-1;
   m_name=m_path=NULL;
   if(_batch_size<1e2)
     {
      ::Print(__FUNCTION__+": a batch size must be greater than 100!");
     }
   else
     {
      ::ResetLastError();
      //--- attempt to create a custom symbol
      if(!::CustomSymbolCreate(_name,_path,_origin_name))
        {
         if(::SymbolInfoInteger(_name,SYMBOL_CUSTOM))
           {
            ::PrintFormat(__FUNCTION__+": a custom symbol \"%s\" already exists!",_name);
            res_code=0;
           }
         else
           {
            ::PrintFormat(__FUNCTION__+": failed to create a custom symbol. Error code: %d",::GetLastError());
           }
        }
      else
         res_code=1;
      if(res_code>=0)
        {
         m_name=_name;
         m_path=_path;
         m_batch_size=_batch_size;
         //--- if the custom symbol must be selected in the "Market Watch"
         if(_is_selected)
           {
            if(!this.Select())
               if(!this.Select(true))
                 {
                  ::PrintFormat(__FUNCTION__+": failed to set the \"Market Watch\" symbol flag. Error code: %d",::GetLastError());
                  return false;
                 }
           }
         else
           {
            if(this.Select())
               if(!this.Select(false))
                 {
                  ::PrintFormat(__FUNCTION__+": failed to unset the \"Market Watch\" symbol flag. Error code: %d",::GetLastError());
                  return false;
                 }
           }
         m_is_selected=_is_selected;
        }
     }
//---
   return res_code;
  }
//+------------------------------------------------------------------+

The method will return a value as a numerical code:

Here are a few words about the _batch_size and _is_selected parameters.

The first parameter (_batch_size) sets the size of the batch, which will be used to load ticks. Ticks will be loaded in batches: data is first read to an auxiliary array; once the array has been filled, the data is loaded to the tick database of a custom symbol (tick history). On the one hand, with this approach you do not have to create a huge array, on the other hand, there is no need to update the tick database too often. The default size of the auxiliary tick array is 1 million.

The second parameter (_is_selected) determines whether we will write the ticks directly to the database, or they will first be added to the Market Watch window.

As an example, let us run the TestCreation.mql5 script, which creates a custom symbol.

The code returned by this method call will be shown in the Journal.

2019.08.11 12:34:08.055 TestCreation (EURUSD,M1) A custom symbol "EURUSD_1" creation has returned the code: 1

For more details related to custom symbol creation, please read the Documentation.


1.2 The CiCustomSymbol::Delete() method

This method will try to delete a custom symbol. Before deleting the symbol, the method will try to remove it from the Market Watch window. In case of failure, the deletion procedure will be interrupted.

//+------------------------------------------------------------------+
//| Delete                                                           |
//+------------------------------------------------------------------+
bool CiCustomSymbol::Delete(void)
  {
   ::ResetLastError();
   if(this.Select())
      if(!this.Select(false))
        {
         ::PrintFormat(__FUNCTION__+": failed to set the \"Market Watch\" symbol flag. Error code: %d",::GetLastError());
         return false;
        }
   if(!::CustomSymbolDelete(m_name))
     {
      ::PrintFormat(__FUNCTION__+": failed to delete the custom symbol \"%s\". Error code: %d",m_name,::GetLastError());
      return false;
     }
//---
   return true;
  }
//+------------------------------------------------------------------+

As an example, let us start a simple script TestDeletion.mql5, which deletes a custom symbol. If successful, a corresponding log will be added to the Journal.

2019.08.11 19:13:59.276 TestDeletion (EURUSD,M1) A custom symbol "EURUSD_1" has been successfully deleted.


1.3 The CiCustomSymbol::Clone() method

This method performs the cloning: based on the selected symbol, it determines properties for the current custom symbol. Simply put, we receive the original symbol's property value and copy it to another symbol. The user can also set the cloning of a tick history. To do this, you need to define the time interval.

//+------------------------------------------------------------------+
//| Clone a symbol                                                   |
//+------------------------------------------------------------------+
bool CiCustomSymbol::Clone(const string _origin_symbol,const ulong _from_msc=0,const ulong _to_msc=0)
  {
   if(!::StringCompare(m_name,_origin_symbol))
     {
      ::Print(__FUNCTION__+": the origin symbol name must be different!");
      return false;
     }
   ::ResetLastError();
//--- if to load history
   if(_to_msc>0)
     {
      if(_to_msc<_from_msc)
        {
         ::Print(__FUNCTION__+": wrong settings for a time interval!");
         return false;
        }
      m_from_msc=_from_msc;
      m_to_msc=_to_msc;
     }
   else
      m_from_msc=m_to_msc=0;
//--- double
   ENUM_SYMBOL_INFO_DOUBLE dbl_props[]=
     {
      SYMBOL_MARGIN_HEDGED,
      SYMBOL_MARGIN_INITIAL,
      SYMBOL_MARGIN_MAINTENANCE,
      SYMBOL_OPTION_STRIKE,
      SYMBOL_POINT,
      SYMBOL_SESSION_PRICE_LIMIT_MAX,
      SYMBOL_SESSION_PRICE_LIMIT_MIN,
      SYMBOL_SESSION_PRICE_SETTLEMENT,
      SYMBOL_SWAP_LONG,
      SYMBOL_SWAP_SHORT,
      SYMBOL_TRADE_ACCRUED_INTEREST,
      SYMBOL_TRADE_CONTRACT_SIZE,
      SYMBOL_TRADE_FACE_VALUE,
      SYMBOL_TRADE_LIQUIDITY_RATE,
      SYMBOL_TRADE_TICK_SIZE,
      SYMBOL_TRADE_TICK_VALUE,
      SYMBOL_VOLUME_LIMIT,
      SYMBOL_VOLUME_MAX,
      SYMBOL_VOLUME_MIN,
      SYMBOL_VOLUME_STEP
     };
   for(int prop_idx=0; prop_idx<::ArraySize(dbl_props); prop_idx++)
     {
      ENUM_SYMBOL_INFO_DOUBLE curr_property=dbl_props[prop_idx];
      if(!this.CloneProperty(_origin_symbol,curr_property))
         return false;
     }
//--- integer
   ENUM_SYMBOL_INFO_INTEGER int_props[]=
     {
      SYMBOL_BACKGROUND_COLOR,
      SYMBOL_CHART_MODE,
      SYMBOL_DIGITS,
      SYMBOL_EXPIRATION_MODE,
      SYMBOL_EXPIRATION_TIME,
      SYMBOL_FILLING_MODE,
      SYMBOL_MARGIN_HEDGED_USE_LEG,
      SYMBOL_OPTION_MODE,
      SYMBOL_OPTION_RIGHT,
      SYMBOL_ORDER_GTC_MODE,
      SYMBOL_ORDER_MODE,
      SYMBOL_SPREAD,
      SYMBOL_SPREAD_FLOAT,
      SYMBOL_START_TIME,
      SYMBOL_SWAP_MODE,
      SYMBOL_SWAP_ROLLOVER3DAYS,
      SYMBOL_TICKS_BOOKDEPTH,
      SYMBOL_TRADE_CALC_MODE,
      SYMBOL_TRADE_EXEMODE,
      SYMBOL_TRADE_FREEZE_LEVEL,
      SYMBOL_TRADE_MODE,
      SYMBOL_TRADE_STOPS_LEVEL
     };
   for(int prop_idx=0; prop_idx<::ArraySize(int_props); prop_idx++)
     {
      ENUM_SYMBOL_INFO_INTEGER curr_property=int_props[prop_idx];
      if(!this.CloneProperty(_origin_symbol,curr_property))
         return false;
     }
//--- string
   ENUM_SYMBOL_INFO_STRING str_props[]=
     {
      SYMBOL_BASIS,
      SYMBOL_CURRENCY_BASE,
      SYMBOL_CURRENCY_MARGIN,
      SYMBOL_CURRENCY_PROFIT,
      SYMBOL_DESCRIPTION,
      SYMBOL_FORMULA,
      SYMBOL_ISIN,
      SYMBOL_PAGE,
      SYMBOL_PATH
     };
   for(int prop_idx=0; prop_idx<::ArraySize(str_props); prop_idx++)
     {
      ENUM_SYMBOL_INFO_STRING curr_property=str_props[prop_idx];
      if(!this.CloneProperty(_origin_symbol,curr_property))
         return false;
     }
//--- history
   if(_to_msc>0)
     {
      if(this.CloneTicks(_origin_symbol)==-1)
         return false;
     }
//---
   return true;
  }
//+------------------------------------------------------------------+

Please note that not all properties can be copied, because some of them are set at the terminal level. Their values can be retrieved, but they cannot be controlled ( get-properties).

An attempt to set a get-property to a custom symbol will return error 5307 (ERR_CUSTOM_SYMBOL_PROPERTY_WRONG). Moreover, there is a separate group of errors for custom symbols under the "Runtime errors" section.

As an example, let us run a simple script TestClone.mql5, which clones a basic symbol. If the cloning attempt is successful, the following log will appear in the Journal.

2019.08.11 19:21:06.402 TestClone (EURUSD,M1) A base symbol "EURUSD" has been successfully cloned.


1.4 The CiCustomSymbol::LoadTicks() method

This method reads ticks from the file and loads them for further use. Note that the method previously deletes the existing tick database for this custom symbol. 

//+------------------------------------------------------------------+
//| Load ticks                                                       |
//+------------------------------------------------------------------+
bool CiCustomSymbol::LoadTicks(const string _src_file_name)
  {
   int symbol_digs=(int)this.GetProperty(SYMBOL_DIGITS);;
//--- delete ticks
   if(this.TicksDelete(0,LONG_MAX)<0)
      return false;
//--- open a file
   CFile curr_file;
   ::ResetLastError();
   int file_ha=curr_file.Open(_src_file_name,FILE_READ|FILE_CSV,',');
   if(file_ha==INVALID_HANDLE)
     {
      ::PrintFormat(__FUNCTION__+": failed to open a %s file!",_src_file_name);
      return false;
     }
   curr_file.Seek(0,SEEK_SET);
//--- read data from a file
   MqlTick batch_arr[];
   if(::ArrayResize(batch_arr,m_batch_size)!=m_batch_size)
     {
      ::Print(__FUNCTION__+": failed to allocate memory for a batch array!");
      return false;
     }
   ::ZeroMemory(batch_arr);
   uint tick_idx=0;
   bool is_file_ending=false;
   uint tick_cnt=0;
   do
     {
      is_file_ending=curr_file.IsEnding();
      string dates_str[2];
      if(!is_file_ending)
        {
         //--- time
         string time_str=::FileReadString(file_ha);
         if(::StringLen(time_str)<1)
           {
            ::Print(__FUNCTION__+": no datetime string - the current tick skipped!");
            ::PrintFormat("The unprocessed string: %s",time_str);
            continue;
           }
         string sep=".";
         ushort u_sep;
         string result[];
         u_sep=::StringGetCharacter(sep,0);
         int str_num=::StringSplit(time_str,u_sep,result);
         if(str_num!=4)
           {
            ::Print(__FUNCTION__+": no substrings - the current tick skipped!");
            ::PrintFormat("The unprocessed string: %s",time_str);
            continue;
           }
         //--- datetime
         datetime date_time=::StringToTime(result[0]+"."+result[1]+"."+result[2]);
         long time_msc=(long)(1e3*date_time+::StringToInteger(result[3]));
         //--- bid
         double bid_val=::FileReadNumber(file_ha);
         if(bid_val<.0)
           {
            ::Print(__FUNCTION__+": no bid price - the current tick skipped!");
            continue;
           }
         //--- ask
         double ask_val=::FileReadNumber(file_ha);
         if(ask_val<.0)
           {
            ::Print(__FUNCTION__+": no ask price - the current tick skipped!");
            continue;
           }
         //--- volumes
         for(int jtx=0; jtx<2; jtx++)
            ::FileReadNumber(file_ha);
         //--- fill in the current tick
         MqlTick curr_tick= {0};
         curr_tick.time=date_time;
         curr_tick.time_msc=(long)(1e3*date_time+::StringToInteger(result[3]));
         curr_tick.bid=::NormalizeDouble(bid_val,symbol_digs);
         curr_tick.ask=::NormalizeDouble(ask_val,symbol_digs);
         //--- flags
         if(m_tick.bid!=curr_tick.bid)
            curr_tick.flags|=TICK_FLAG_BID;
         if(m_tick.ask!=curr_tick.ask)
            curr_tick.flags|=TICK_FLAG_ASK;
         if(curr_tick.flags==0)
            curr_tick.flags=TICK_FLAG_BID|TICK_FLAG_ASK;;
         if(tick_idx==m_batch_size)
           {
            //--- add ticks to the custom symbol
            if(m_is_selected)
              {
               if(this.TicksAdd(batch_arr)!=m_batch_size)
                  return false;
              }
            else
              {
               if(this.TicksReplace(batch_arr)!=m_batch_size)
                  return false;
              }
            tick_cnt+=m_batch_size;
            //--- log
            for(uint idx=0,batch_idx=0; idx<::ArraySize(dates_str); idx++,batch_idx+=(m_batch_size-1))
               dates_str[idx]=::TimeToString(batch_arr[batch_idx].time,TIME_DATE|TIME_SECONDS);
            ::PrintFormat("\nTicks loaded from %s to %s.",dates_str[0],dates_str[1]);
            //--- reset
            ::ZeroMemory(batch_arr);
            tick_idx=0;
           }
         batch_arr[tick_idx]=curr_tick;
         m_tick=curr_tick;
         tick_idx++;
        }
      //--- end of file
      else
        {
         uint new_size=tick_idx;
         if(new_size>0)
           {
            MqlTick last_batch_arr[];
            if(::ArrayCopy(last_batch_arr,batch_arr,0,0,new_size)!=new_size)
              {
               ::Print(__FUNCTION__+": failed to copy a batch array!");
               return false;
              }
            //--- add ticks to the custom symbol
            if(m_is_selected)
              {
               if(this.TicksAdd(last_batch_arr)!=new_size)
                  return false;
              }
            else
              {
               if(this.TicksReplace(last_batch_arr)!=new_size)
                  return false;
              }
            tick_cnt+=new_size;
            //--- log
            for(uint idx=0,batch_idx=0; idx<::ArraySize(dates_str); idx++,batch_idx+=(tick_idx-1))
               dates_str[idx]=::TimeToString(batch_arr[batch_idx].time,TIME_DATE|TIME_SECONDS);
            ::PrintFormat("\nTicks loaded from %s to %s.",dates_str[0],dates_str[1]);
           }
        }
     }
   while(!is_file_ending && !::IsStopped());
   ::PrintFormat("\nLoaded ticks number: %I32u",tick_cnt);
   curr_file.Close();
//---
   MqlTick ticks_arr[];
   if(::CopyTicks(m_name,ticks_arr,COPY_TICKS_INFO,1,1)!=1)
     {
      ::Print(__FUNCTION__+": failed to copy the first tick!");
      return false;
     }
   m_from_msc=ticks_arr[0].time_msc;
   if(::CopyTicks(m_name,ticks_arr,COPY_TICKS_INFO,0,1)!=1)
     {
      ::Print(__FUNCTION__+": failed to copy the last tick!");
      return false;
     }
   m_to_msc=ticks_arr[0].time_msc;
//---
   return true;
  }
//+------------------------------------------------------------------+

In this variant, the following tick structure fields are filled:

struct MqlTick 
  { 
   datetime     time;          // Last price update time 
   double       bid;           // Current Bid price 
   double       ask;           // Current Ask price 
   double       last;          // Current price of the last trade (Last)
   ulong        volume;        // Volume for the current Last price
   long         time_msc;      // Last price update time in milliseconds 
   uint         flags;         // Tick flags 
   double       volume_real;   // Volume for the current Last price
  };

The TICK_FLAG_BID|TICK_FLAG_ASK value is set for the first tick flag. Further value depends on which price (bid or ask) has changed. If none of the prices has changed, then they shall be processed as the first tick.

Starting with build 2085, a bar history can be formed by simply loading the tick history. If the history has been loaded, we can programmatically request the bar history .

As an example, let us run the simple script TestLoad.mql5, which loads ticks from a file. The data file must be located under the % MQL5/Files folder. In this example, the file is EURUSD1_tick.csv. It contains EURUSD ticks for August 1 and 2, 2019. Further we will consider tick data sources.

After running the script, the number of loaded ticks will be shown in the journal. In addition, we will double-check the number of available ticks by requesting data from the tick database of the terminal. 354,400 ticks have been copied. Thus, the numbers are equal. We also received 2,697 1-minute bars. 

NO      0       15:52:50.149    TestLoad (EURUSD,H1)    
LN      0       15:52:50.150    TestLoad (EURUSD,H1)    Ticks loaded from 2019.08.01 00:00:00 to 2019.08.02 20:59:56.
FM      0       15:52:50.152    TestLoad (EURUSD,H1)    
RM      0       15:52:50.152    TestLoad (EURUSD,H1)    Loaded ticks number: 354400
EJ      0       15:52:50.160    TestLoad (EURUSD,H1)    Ticks from the file "EURUSD1_tick.csv" have been successfully loaded.
DD      0       15:52:50.170    TestLoad (EURUSD,H1)    Copied 1-minute rates number: 2697
GL      0       15:52:50.170    TestLoad (EURUSD,H1)    The 1st rate time: 2019.08.01 00:00
EQ      0       15:52:50.170    TestLoad (EURUSD,H1)    The last rate time: 2019.08.02 20:56
DJ      0       15:52:50.351    TestLoad (EURUSD,H1)    Copied ticks number: 354400


Other methods belong to the group of API methods.


2. Tick data. Sources

Tick data form a price series, which has a very active life, in which supply and demand are fighting.

The nature of the price series has long been the subject of discussions of traders and experts. This series is the subject of analysis, the underlying basis for making decisions, etc.

The following ideas are applicable within the article context.

As you know, Forex is the over-the-counter market. Therefore, there are no benchmark quotes. As a result, there are no tick archives (tick history), which can be used for reference. But there are currency futures, most of which are traded on the Chicago Mercantile Exchange. Probably these quotes can serve as some kind of benchmark. However, the historic data cannot be obtained for free. In some cases, you can have a free testing period. But even in this case you will have to register on the exchange website and communicate with the sales manager. On the other hand there is competition between brokers. Therefore , quotes should not differ much between brokers. But usually brokers do not store tick archives and do not make them available for download.

One of the free sources is the Dukascopy Bank website.

After a simple registration, it allows downloading historical data, including ticks.

Rows in the file consist of 5 columns:

The inconvenience of manual downloading of ticks is that you can only select 1 day. If you need a several-year history of ticks, downloading will take much time and effort.


Quant Data Manager

Fig.1 Symbols for downloading on the Data tab of the Quant Data Manager app


There are auxiliary apps that download archives of ticks from the Dukascopy Bank site. One of them is Quant Data Manager. Figure 1 shows one of the tabs of the application window. 


The CiCustomSymbol::LoadTicks() method is adjusted to the csv file format used in the above app.


3. Stress testing of trading strategies

Trading strategy testing is a multi-aspect process. Most often "testing" refers to the execution of a trading algorithm using historic quotes (back-testing). But there are other methods to test a trading strategy. 

One of them is Stress Testing.

Stress testing is a form of deliberately intense or thorough testing used to determine the stability of a given system or entity.

The idea is simple: create specific conditions for trading strategy operation, which will provide accelerated stress for the strategy. The ultimate goal of such conditions is to check the degree of trading system reliability and its resistance to conditions that have become or may become worse.


3.1 Spread change

The spread factor is extremely important for a trading strategy, as it determines the amount of additional costs. The strategies which aim short-term deals are particularly sensitive to the spread size. In some cases, the ratio of spread to return may exceed 100%. 

Let's try to create a custom symbol, which will differ from the basic spread size. For this purpose, let's create a new method CiCustomSymbol::ChangeSpread().

//+------------------------------------------------------------------+
//| Change the initial spread                                        |
//| Input parameters:                                                |
//|     1) _spread_size - the new fixed value of the spread, pips.   |
//|        If the value > 0 then the spread value is fixed.          |
//|     2) _spread_markup - a markup for the floating value of the   |
//|        spread, pips. The value is added to the current spread if |
//|        _spread_size=0.                                           |
//|     3) _spread_base - a type of the price to which a markup is   |
//|        added in case of the floating value.                      |
//+------------------------------------------------------------------+
bool CiCustomSymbol::ChangeSpread(const uint _spread_size,const uint _spread_markup=0,
                                  const ENUM_SPREAD_BASE _spread_base=SPREAD_BASE_BID)
  {
   if(_spread_size==0)
      if(_spread_markup==0)
        {
         ::PrintFormat(__FUNCTION__+":  neither the spread size nor the spread markup are set!",
                       m_name,::GetLastError());
         return false;
        }
   int symbol_digs=(int)this.GetProperty(SYMBOL_DIGITS);
   ::ZeroMemory(m_tick);
//--- copy ticks
   int tick_idx=0;
   uint tick_cnt=0;
   ulong from=1;
   double curr_point=this.GetProperty(SYMBOL_POINT);
   int ticks_copied=0;
   MqlDateTime t1_time;
   TimeToStruct((int)(m_from_msc/1e3),t1_time);
   t1_time.hour=t1_time.min=t1_time.sec=0;
   datetime start_datetime,stop_datetime;
   start_datetime=::StructToTime(t1_time);
   stop_datetime=(int)(m_to_msc/1e3);
   do
     {
      MqlTick custom_symbol_ticks[];
      ulong t1,t2;
      t1=(ulong)1e3*start_datetime;
      t2=(ulong)1e3*(start_datetime+PeriodSeconds(PERIOD_D1))-1;
      ::ResetLastError();
      ticks_copied=::CopyTicksRange(m_name,custom_symbol_ticks,COPY_TICKS_INFO,t1,t2);
      if(ticks_copied<0)
        {
         ::PrintFormat(__FUNCTION__+": failed to copy ticks for a %s symbol! Error code: %d",
                       m_name,::GetLastError());
         return false;
        }
      //--- there are some ticks for the current day
      else
         if(ticks_copied>0)
           {
            for(int t_idx=0; t_idx<ticks_copied; t_idx++)
              {
               MqlTick curr_tick=custom_symbol_ticks[t_idx];
               double curr_bid_pr=::NormalizeDouble(curr_tick.bid,symbol_digs);
               double curr_ask_pr=::NormalizeDouble(curr_tick.ask,symbol_digs);
               double curr_spread_pnt=0.;
               //--- if the spread is fixed
               if(_spread_size>0)
                 {
                  if(_spread_size>0)
                     curr_spread_pnt=curr_point*_spread_size;
                 }
               //--- if the spread is floating
               else
                 {
                  double spread_markup_pnt=0.;
                  if(_spread_markup>0)
                     spread_markup_pnt=curr_point*_spread_markup;
                  curr_spread_pnt=curr_ask_pr-curr_bid_pr+spread_markup_pnt;
                 }
               switch(_spread_base)
                 {
                  case SPREAD_BASE_BID:
                    {
                     curr_ask_pr=::NormalizeDouble(curr_bid_pr+curr_spread_pnt,symbol_digs);
                     break;
                    }
                  case SPREAD_BASE_ASK:
                    {
                     curr_bid_pr=::NormalizeDouble(curr_ask_pr-curr_spread_pnt,symbol_digs);
                     break;
                    }
                  case SPREAD_BASE_AVERAGE:
                    {
                     double curr_avg_pr=::NormalizeDouble((curr_bid_pr+curr_ask_pr)/2.,symbol_digs);
                     curr_bid_pr=::NormalizeDouble(curr_avg_pr-curr_spread_pnt/2.,symbol_digs);
                     curr_ask_pr=::NormalizeDouble(curr_bid_pr+curr_spread_pnt,symbol_digs);
                     break;
                    }
                 }
               //--- new ticks
               curr_tick.bid=curr_bid_pr;
               curr_tick.ask=curr_ask_pr;
               //--- flags
               curr_tick.flags=0;
               if(m_tick.bid!=curr_tick.bid)
                  curr_tick.flags|=TICK_FLAG_BID;
               if(m_tick.ask!=curr_tick.ask)
                  curr_tick.flags|=TICK_FLAG_ASK;
               if(curr_tick.flags==0)
                  curr_tick.flags=TICK_FLAG_BID|TICK_FLAG_ASK;
               custom_symbol_ticks[t_idx]=curr_tick;
               m_tick=curr_tick;
              }
            //--- replace ticks
            int ticks_replaced=0;
            for(int att=0; att<ATTEMTS; att++)
              {
               ticks_replaced=this.TicksReplace(custom_symbol_ticks);
               if(ticks_replaced==ticks_copied)
                  break;
               ::Sleep(PAUSE);
              }
            if(ticks_replaced!=ticks_copied)
              {
               ::Print(__FUNCTION__+": failed to replace the refreshed ticks!");
               return false;
              }
            tick_cnt+=ticks_replaced;
           }
      //--- next datetimes
      start_datetime=start_datetime+::PeriodSeconds(PERIOD_D1);
     }
   while(start_datetime<=stop_datetime && !::IsStopped());
   ::PrintFormat("\nReplaced ticks number: %I32u",tick_cnt);
//---
   return true;
  }
//+------------------------------------------------------------------+

How to change the spread size for a custom symbol?

Firstly, a fixed spread can be set. This can be done by specifying a positive value in the _spread_size parameter. Note that this part will work in the Tester, despite the following rule:

In the Strategy Tester, the spread is always considered floating. I.e. SymbolInfoInteger(symbol, SYMBOL_SPREAD_FLOAT) always returns true.

Secondly, a markup can be added to the available spread value. This can be done by defining the _spread_markup parameter. 

Also, the method allows specifying the price, which will serve as the reference spread value. This is done by the ENUM_SPREAD_BASE enumeration.

//+------------------------------------------------------------------+
//| Spread calculation base                                          |
//+------------------------------------------------------------------+
enum ENUM_SPREAD_BASE
  {
   SPREAD_BASE_BID=0,    // bid price
   SPREAD_BASE_ASK=1,    // ask price
   SPREAD_BASE_AVERAGE=2,// average price
  };
//+------------------------------------------------------------------+

If we use the bid price (SPREAD_BASE_BID), then ask = bid + calculated spread. If we use the ask price (SPREAD_BASE_ASK), then bid = ask - calculated spread. If we use the average price (SPREAD_BASE_AVERAGE), then bid = average price - calculated spread/2.

The CiCustomSymbol::ChangeSpread() method does not change the value of a certain symbol property, but it changes the spread value at each tick. The updated ticks are stored in the tick base.


Check the operation of the method using the TestChangeSpread.mq5 spread. If the script runs fine, the following log will be added in the Journal:

2019.08.30 12:49:59.678 TestChangeSpread (EURUSD,M1)    Replaced ticks number: 354400

It means that the tick size has been changed for all previously loaded ticks. 


The below table shows my strategy testing results with different spread values, using EURUSD data (Table 1).

Value Spread 1 (12-17 points) Spread 2 (25 points) Spread 2 (50 points)
Number of trades
172 156 145
Net Profit, $
4 018.27 3 877.58 3 574.1
Max. Equity Drawdown, %
11.79 9.65 8.29
 Profit per Trade, $
 23.36  24.86  24.65

Table 1. Testing results with different spread values

Column "Spread 1" reflects results with a real floating spread (12-17 points with five-digit quotes).

With a higher spread, the number of trades was less. This resulted in a decrease in drawdown. Moreover, the trade profitability has increased in this case.


3.2 Changing the Stop and Freeze levels

Some strategies may depend on the stop level and the freeze level. Many brokers provide the stop level equal to spread and the zero freeze level. However, sometimes brokers can increase these levels. Usually this happens during periods of increased volatility or low liquidity. This information can be obtained from the broker's Trading Rules. 

Here is an example of an extract from such Trading Rules:

"The minimum distance at which pending orders or T/P and S/L orders can be placed, is equal to the symbol spread. 10 minutes before the release of important macroeconomic statistics or economic news, the minimum distance for S/L orders can be increased to 10 spreads. 30 minutes before trading close time, this level for S/L orders increases to 25 spreads."

These levels (SYMBOL_TRADE_STOPS_LEVEL and SYMBOL_TRADE_FREEZE_LEVEL) can be configured using the CiCustomSymbol::SetProperty() method. 

Please note that symbol properties cannot be changed dynamically in the Tester. In the current version, the Tester operates with pre-configured custom symbol parameters. The platform developer introduces significant . Therefore, this feature may possibly be added in future builds.


3.3 Changing margin requirements

Individual margin requirements can also be set for a custom symbol. The values for the following parameters can be set programmatically: SYMBOL_MARGIN_INITIAL, SYMBOL_MARGIN_MAINTENANCE, SYMBOL_MARGIN_HEDGED. Thus, we can define the margin level for the trading instrument. 

There are also margin identifiers for separate types of positions and volumes (SYMBOL_MARGIN_LONG, SYMBOL_MARGIN_SHORT etc.). They are set in manual mode

Leverage variations enable testing of the trading strategy in terms of resistance to drawdowns and ability to avoid stop outs.


Conclusion

This article highlights some aspects of trading strategy stress testing.

Custom symbol settings allow configuring parameters of your own symbols. Every algo trader can select a specific set of parameters, which are required for a specific trading strategy. One of the uncovered interesting options concerns the Market Depth of a custom symbol.

The archive contains a file with ticks, which were processed as part of the examples, as well as the source files of the script and the custom symbol class. 

I would like to express my gratitude to fxsaber, to moderators Artyom Trishkin and Slava for interesting discussions in the "Custom symbols. Errors, bugs, questions & suggestions" thread (in Russian).