
Рецепты MQL5 – Стресс-тестирование торговой стратегии с помощью пользовательских символов
Введение
Относительно недавно в терминале 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 Символы для закачки на вкладке "Data" в программе Quant Data Manager
Именно под формат 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 за интересное обсуждение темы в ветке "Пользовательские символы. Ошибки, баги, вопросы, предложения".





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования