Рецепты MQL5 – Стресс-тестирование торговой стратегии с помощью пользовательских символов

16 сентября 2019, 20:25
Denis Kirichenko
0
1 684

Введение

Относительно недавно в терминале MetaTrader 5  появилась возможность создавать пользовательские символы. И теперь алготрейдеры могут в какой-то степени стать сами себе брокером: ведь торговые серверы не нужны, и торговое окружение полностью под контролем. Правда поспешу оговориться, что это касается режима тестирования. Естественно, что реального исполнения торговых приказов для пользовательских символов обычный брокер не даст. Тем не менее алготрейдер имеет инструмент, который позволит проверить свою стратегию более тщательно.

В данной статье займемся созданием условий для такой проверки. А начнём с класса пользовательского символа.


1. Класс пользовательского символа CiCustomSymbol

В Стандартной библиотеке уже давно существует класс для упрощенного доступа к свойствам символа. Таковым является класс CSymbolInfo. По сути этот класс выполняет посредническую функцию, т.к. по запросу пользователя он обращается к серверу и получает от него ответ в виде значения запрашиваемого свойства.

Наша задача создать аналогичный класс, но уже для пользовательского символа. Представляется, что функционал данного класса будет с одной стороны шире — ведь нужно написать методы создания, удаления и прочие, а с другой стороны будут отсутствовать методы, выполняющие связь с сервером. К ним относятся Refresh(), IsSynchronized() и пр.

Данный класс для создания торгового окружения инкапсулирует стандартные функции для работы с пользовательскими символами.

Структура объявления класса представлена ниже.

//+------------------------------------------------------------------+
//| 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;
  };
//+------------------------------------------------------------------+

Начнём с методов, которые должны сделать наш символ полноценным пользовательским (кастомным) символом.


1.1 Метод CiCustomSymbol::Create()

Для того, чтобы воспользоваться возможностями кастомного символа нужно его создать или проверить, что он был создан ранее. 

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

Здесь отмечу, что метод вернёт значение в виде числового кода:

  • «-1» — ошибка создания символа;
  • «0»  — символ был создан ранее;
  • «1»  — символ успешно создан в текущем вызове метода.

И несколько слов о параметрах _batch_size и _is_selected.

Первый (_batch_size) задаёт размер пакета, с помощью которого будут загружаться тики. Тики будут загружаться пачками: сначала данные считываются во вспомогательный тиковый массив, а потом, когда массив заполнен, заливаются в базу тиков пользовательского символа (история тиков). С одной стороны такой подход позволяет не создавать массив огромного размера, а с другой стороны не нужно часто обновлять тиковую базу. Размер вспомогательного тикового массива по умолчанию равен 1 млн.

Второй параметр (_is_selected) определяет, будем ли мы записывать тики в базу напрямую, или они сначала будут попадать  в окно "Обзор рынка".

Для примера запустим простой скрипт TestCreation.mql5, создающий пользовательский символ.

В журнале увидим какой код возвращает вызов данного метода.

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

Более подробную информацию по созданию  пользовательского символа можно найти в Документации.


1.2 Метод CiCustomSymbol::Delete()

Данный метод будет пытаться удалить пользовательский символ. Предварительно он попробует убрать символ из «Обзора рынка». В случае неудачи процедура удаления будет досрочно прервана.

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

Для примера запустим простой скрипт TestDeletion.mql5, удаляющий пользовательский символ. В случае успеха в журнале появится запись.

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


1.3 Метод CiCustomSymbol::Clone()

Этот метод занимается клонированием: на основании выбранного символа он определяет свойства для текущего пользовательского символа. Проще говоря, получаем значение свойства исходного символа и копируем его для другого. Также пользователь может задать возможность клонировать и тиковую историю. Для этого достаточно определить временной интервал.

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

Замечу, что не все свойства можно копировать, т.к. некоторые задаются только на уровне терминала. Их значения можно только получить, но не контролировать самостоятельно ( get-свойство).

Если попытаться задать get-свойство для пользовательского символа, то получим ошибку 5307 (ERR_CUSTOM_SYMBOL_PROPERTY_WRONG). Да, тут стоит сказать, что разработчик определил целую группу ошибок для пользовательских символов в разделе «Ошибки времени выполнения».

Для примера запустим простой скрипт TestClone.mql5, клонирующий базовый символ. В журнале появится сообщение, если попытка клонирования была удачной.

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


1.4 Метод CiCustomSymbol::LoadTicks()

Этот метод считывает тики из файла и загружает их для последующего использования. Стоит отметить, что метод предварительно удаляет имеющуюся базу тиков для данного пользовательского символа. 

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

В данном варианте заполняются следующие поля структуры тиков:

struct MqlTick 
  { 
   datetime     time;          // Время последнего обновления цен 
   double       bid;           // Текущая цена Bid 
   double       ask;           // Текущая цена Ask 
   double       last;          // Текущая цена последней сделки (Last) 
   ulong        volume;        // Объем для текущей цены Last 
   long         time_msc;      // Время последнего обновления цен в миллисекундах 
   uint         flags;         // Флаги тиков 
   double       volume_real;   // Объем для текущей цены Last c повышенной точностью 
  };

Для флага первого тика задаётся значение "TICK_FLAG_BID|TICK_FLAG_ASK". Последующее значение зависит от того, какая цена (bid или ask) изменилась. Если обе цены не менялись, то обрабатываем как первый тик.

Начиная с версии 2085 для формирования баровой истории достаточно загрузить историю тиков. Как только она загружена, можно программым образом запрашивать уже саму историю баров.

Для примера запустим простой скрипт TestLoad.mql5, загружающий тики из файла. Сам файл с данными должен находится в папке % MQL5/Files. В примере таким файлом является EURUSD1_tick.csv. Он содержит тики по символу EURUSD за 1 и 2 августа 2019 г. Позже рассмотрим источники тиковых данных.

Итак, после запуска скрипта в журнале появится сообщение, сколько тиков было загружено. Кроме того, перепроверим, запросив данные из тиковой базы терминала, сколько тиков имеется в нашем распоряжении. Было скопировано 354 400 тиков. Всё совпадает. Также получили 2 697 минутных бара. 

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


Остальные методы относятся к группе API-методов.


2. Тиковые данные. Источники

Тиковые данные образуют ценовой ряд, который живёт веселой жизнью: здесь как на фронте «воюют» между собой спрос и предложение.

Над природой ценового ряда спорили, спорят и будут спорить трейдеры и эксперты. Этот ряд является предметом всякого вида анализа,   основой принятия торговых решений и т.д.

В контексте нашей статьи отмечу пару базовых  моментов.

Как известно, рынок Forex является внебиржевым, поэтому не существует эталонных котировок. И, как следствие, нет тиковых архивов (истории тиков), которые можно взять в качестве эталона. Правда, есть валютные фьючерсы, большинство из которых торгуется на Чикагской товарной бирже. И котировки по ним наверное имеют какую-то ценность ориентира. Получить исторические данные бесплатно не получится. В лучшем случае даётся некоторый пробный период. Но и для этого придётся пройти этап регистрации на сайте биржи и пообщаться с менеджером по продажам. С другой стороны конкуренция между брокерами делает своё дело. Поэтому осмелюсь предположить, что у разных брокеров котировки примерно одинаковые, плюс-минус некоторое число пунктов. Другое дело, что брокеры как правило не хранят тиковые архивы и не предоставляют их для закачки.

Среди бесплатных источников отметил бы сайт брокера Dukascopy Bank.

После простой регистрации там можно скачивать исторические данные, в том числе и тики.

Строки в файле состоят из 5 столбцов:

  • Время;
  • Цена аск;
  • Цена бид;
  • Купленный объём;
  • Проданный объём.
Неудобство скачивания тиков вручную состоит в том, что можно выбрать только 1 день. Если нужна история тиков за несколько лет, то придётся повозиться с закачкой.


Quant Data Manager

Рис.1 Символы для закачки на вкладке "Data" в программе Quant Data Manager


Но есть вспомогательные программы, скачивающие архивы тиков с сайта брокера Dukascopy Bank. К таковым можно отнести Quant Data Manager. На Рис.1 представлена одна из вкладок окна программы. 


Именно под формат csv-файла указанной программы настроен метод CiCustomSymbol::LoadTicks().


3. Стресс-тестирование торговых стратегий

Процедура тестирования торговой стратегии носит многоаспектный характер. И хотя чаще всего под тестированием подразумевается прогон торгового алгоритма на истории котировок (бэктестинг), всё же есть и другие способы проверки торговой стратегии. 

Одним из них может выступать стресс-тестирование.

Стресс-тестирование (stress testing) — одна из форм тестирования, которая используется для определения устойчивости системы или модуля в условиях превышения пределов нормального функционирования.

Идея очень простая: создаем такие условия для работы торговой стратегии, которые ухудшают нормальные или типичные при прочих равных. Конечная цель таких действий — проверить степень надёжности торговой системы и её устойчивости к условиям, которые изменились или могут измениться в худшую сторону.


3.1 Изменение спреда

Фактор спреда имеет большое значение для торговой стратегии, т.к. он определяет размер накладных расходов. Особенно чувствительны к размеру спреда стратегии, нацеленные на краткосрочные сделки. В этих случаях отношение размера спреда к доходу может превышать 100%. 

Давайте попробуем создать пользовательский символ, который будет отличаться от базового размером спреда. Для этих целей напишем новый метод 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;
  }
//+------------------------------------------------------------------+

Как можно поменять величину спреда для пользовательского символа?

Во-первых, спред можно сделать фиксированным. Достаточно задать некоторое положительное значение для параметра _spread_size. Тут замечу, что такая конструкция будет работать и в Тестере, несмотря на следующее правило:

В тестере спред всегда считается плавающим. То есть SymbolInfoInteger(symbol, SYMBOL_SPREAD_FLOAT) всегда возвращает true.

Во-вторых, к уже имеющемуся значению спреда можно сделать добавку (маркап). Для этого определим параметр _spread_markup. 

И метод ещё позволяет указывать ту цену, которая будет отправной точкой отсчёта значения спреда. За это отвечает перечисление ENUM_SPREAD_BASE.

//+------------------------------------------------------------------+
//| 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
  };
//+------------------------------------------------------------------+

Если взять цену bid (SPREAD_BASE_BID), то цена ask = цена bid + рассчитанный спред. Если взять цену ask (SPREAD_BASE_ASK), то цена bid = цена ask - рассчитанный спред. И если возьмём среднюю цену (SPREAD_BASE_AVERAGE), то цена bid = средняя цена - рассчитанный спред/2.

Метод CiCustomSymbol::ChangeSpread() меняет значение не какого-то свойства символа, а значение спреда на каждом тике. Обновлённые тики сохраняются в базе тиков.


Проверим работу метода с помощью скрипта TestChangeSpread.mq5. Если скрипт отработает нормально, то в Журнале появится запись:

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

Т.е. для всех ранее загруженных тиков размер спреда был заменён. 


Представлю результаты тестирования своей стратегии для разных значений спреда на паре EURUSD (Табл.1).

Показатель Спред 1 (12-17 пп) Спред 2 (25 пп) Спред 2 (50 пп)
Всего трейдов
172 156 145
Чистая прибыль, $
4 018.27 3 877.58 3 574.1
Макс. просадка по средствам, %
11.79 9.65 8.29
 Прибыль на трейд, $
 23.36  24.86  24.65

Табл.1 Значения показателей тестирования при меняющемся спреде

Столбец "Спред 1" отражает результат для реального плавающего спреда (12-17 пп в пятом знаке).

Вполне естественно, что при росте значения спреда было совершено меньше сделок. Это повлекло уменьшение просадки. Ещё интересно, что при этом прибыльность трейда выросла.


3.2 Изменение уровня стопа и уровня "заморозки"

Некоторые стратегии могут зависеть от стоп-левела (уровень стопа) и фриз-левела (уровень "заморозки"). У многих брокеров первый равен величине спреда, а второй — нулю. Но бывают моменты, когда брокер может увеличить данные уровни. Это случается в периоды высокой волатильности или низкой ликвидности. Более точную информацию можно получить из Торговых правил брокера. 

Приведу в качестве примера выдержку из Торговых правил одного брокера:

"Минимальное расстояние, на котором можно устанавливать ждущие ордера или устанавливать ордера T/P и S/L, равно спреду по инструменту. За 10 минут до выхода значимых показателей макроэкономической статистики и важных политических или экономических новостей минимальное расстояние для ордеров S/L может быть увеличено до 10 спредов. За 30 минут до закрытия торгов данный уровень для ордеров S/L увеличивается до 25 спредов."

Настраиваются указанные уровни (SYMBOL_TRADE_STOPS_LEVEL и SYMBOL_TRADE_FREEZE_LEVEL) с помощью метода CiCustomSymbol::SetProperty(). 

Правда, динамически менять свойства символа в Тестере не получится. В текущей версии Тестер работает с предварительно заданными параметрами пользовательского символа. Разработчик ведёт большую работу по модернизации Тестера. Возможно, что в скором будущем данная возможность появится.


3.3 Изменение уровня маржинальных требований

Для пользовательского символа также можно определить свои значения маржинальных требований. На сегодняшний день программно можно задать значения для следующих параметров: SYMBOL_MARGIN_INITIAL, SYMBOL_MARGIN_MAINTENANCE, SYMBOL_MARGIN_HEDGED. По сути, таким образом мы определяем размер маржинального плеча для торгуемого инструмента. 

Ещё есть идентификаторы маржи для отдельных типов позиций и ордеров (SYMBOL_MARGIN_LONG,SYMBOL_MARGIN_SHORT и т.д.). Но они задаются в ручном режиме

Вариация значений плеча позволяет протестировать торговую стратегию на предмет устойчивости к просадкам и способности избежать стоп-аут.


Заключение

В рамках данной статьи были освещены некоторые аспекты стресс-тестирования торговой стратегии.

Отмечу, что настройки пользовательского символа позволяют задавать многие параметры для собственного символа. И каждый алготрейдер может выбрать из перечня именно те, которые представляют интерес для его стратегии. Достаточно интересно было бы, к примеру, поработать со стаканом пользовательского символа.

В архиве находится файл с тиками, которые обрабатывались в рамках примеров, исходные файлы скриптов и класса пользовательского символа. 

Особую благодарность хочу выразить пользователю fxsaber, модераторам  Artyom Trishkin и Slava за интересное обсуждение темы в ветке "Пользовательские символы. Ошибки, баги, вопросы, предложения".

Прикрепленные файлы |
EURUSD1_tick.zip (3087.31 KB)
Custom.zip (10.37 KB)
Библиотека для простого и быстрого создания программ для MetaTrader (Часть XX): Создание и хранение ресурсов программы Библиотека для простого и быстрого создания программ для MetaTrader (Часть XX): Создание и хранение ресурсов программы

В статье рассмотрим способ хранения данных в исходниках программы и создание из них звуковых и графических файлов. Часто при создании программы, нам требуется использовать звуки и изображения. В языке MQL есть несколько возможностей использования таких данных.

Библиотека для простого и быстрого создания программ для MetaTrader (Часть XIX): Класс сообщений библиотеки Библиотека для простого и быстрого создания программ для MetaTrader (Часть XIX): Класс сообщений библиотеки

В статье рассмотрим класс вывода текстовых сообщений. Сейчас у нас имеется достаточное количество различных текстовых сообщений, и уже стоит подумать о реорганизации способа их хранения, вывода и удобства правки русских сообщений на иной язык, а так же об удобном способе добавления новых языков в библиотеку и быстром переключении между ними.

Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXI): Торговые классы - Базовый кроссплатформенный торговый объект Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXI): Торговые классы - Базовый кроссплатформенный торговый объект

В статье начнём новый раздел библиотеки - торговые классы, и рассмотрим создание единого базового торгового объекта для платформ MetaTrader 5 и MetaTrader 4. Такой торговый объект будет подразумевать при отправке запроса на сервер, что в него переданы уже проверенные и корректные параметры торгового запроса.

Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXII): Торговые классы - Основной торговый класс, контроль ограничений Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXII): Торговые классы - Основной торговый класс, контроль ограничений

В статье начнём создавать основной торговый класс библиотеки и наделим его первую версию функционалом первичной проверки разрешений на проведение торговых операций. Также немного расширим возможности и содержание базового торгового класса.