Тестирование торговых стратегий

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

  • эксперт совершает торговые операции в соответствии с правилами торговой системы;
  • торговая стратегия, реализованная в эксперте, показывает прибыль на истории.

Для получения ответов на эти вопросы предназначен тестер стратегий, входящий в состав клиентского терминала MetaTrader 5.

В данном разделе рассмотрены все особенности тестирования и оптимизации программ в тестере стратегий:

 

Ограничения на память и дисковое пространство в MQL5 Cloud Network

При запуске оптимизации в MQL5 Cloud Network существует ограничение: тестируемый советник не может записывать на диск более 4ГБ информации и использовать более 4ГБ оперативной памяти. При превышении лимита агент сети не сможет корректно завершить расчет, и вы не получите результат тестирования. При этом с вас будет удержана оплата за уже затраченное на расчеты время.

Если вам необходимо получать информацию с каждого прохода оптимизации, используйте для этого отправку фреймов без записи на диск. То есть при вычислениях в MQL5 Cloud Network не используйте файловые операции в советниках при оптимизации, можно применять такую проверку:

   int handle=INVALID_HANDLE;
   bool file_operations_allowed=true;
   if(MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD))
      file_operations_allowed=false;
 
   if(file_operations_allowed)
     {
      ...
      handle=FileOpen(...);
      ...
     }

 

 

Ограничения работы функций в тестере торговых стратегий #

Существуют ограничения работы некоторых функций в тестере стратегий клиентского терминала.

Функции Comment(), Print() и PrintFormat() #

Для увеличения быстродействия при оптимизации параметров советника функции Comment(), Print() и PrintFormat() не выполняются. Исключением является использование этих функций внутри обработчика OnInit(). Это позволяет облегчить поиск причин ошибок при их возникновении.

Функции Alert(), MessageBox(), PlaySound(), SendFTP, SendMail(), SendNotification(), WebRequest() #

Функции взаимодействия с "внешним миром" Alert(), MessageBox(), PlaySound(), SendFTP(), SendMail(), SendNotification() и WebRequest() в тестере стратегий не выполняются.

 

Режимы генерации тиков #

Эксперт на языке MQL5 представляет из себя программу, которая запускается каждый раз в ответ на некое внешнее воздействие - событие. Для каждого предопределенного события в эксперте есть соответствующая этому событию функция - обработчик события.  

Основным событием для эксперта является изменение цены - NewTick, и поэтому для тестирования экспертов необходимо генерировать тиковые последовательности. В тестере клиентского терминала MetaTrader 5 реализовано 3 режима генерации тиков:

  • Все тики
  • Цены OHLC с минутных баров (1 Minute OHLC)
  • Только цены открытия

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

Все тики

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

Минимальным элементом ценовой истории является минутный бар, из которого можно получить информацию о четырех значениях цены:

  • Open - цена, по которой открылся минутный бар;
  • High - максимум, который достигался в течение этого минутного бара;
  • Low - минимум, который достигался в течение этого минутного бара;
  • Close - цена закрытия бара.

Новый минутный бар открывается не в тот момент, когда начинается новая минута (количество секунд становится равным 0), а когда приходит тик -  изменение цены хотя бы на один пункт. На рисунке показан первый минутный бар новой торговой недели, который имеет время открытия 2011.01.10. 00:00. Ценовой разрыв между пятницей и понедельником, который мы видим на графике, является обычным явлением, так как даже в выходные дни валютные курсы изменяются в ответ на поступающие новости.

new_bar_of_week

Для этого бара нам только известно, что данный минутный бар открыт 10 января 2011 года в 00 часов 00 минут, но ничего неизвестно о секундах. Это могло быть время 00:00:12 или 00:00:36 (12 или 36 секунд после начала нового дня) или  любое другое время в пределах этой минуты. Но мы знаем точно, что в момент открытия нового минутного бара цена  Open по EURUSD была на уровне 1.28940.

Точно также мы не знаем с точностью до секунды, когда пришел тик, соответствующий цене закрытия рассматриваемого минутного бара, известно только одно - это последняя цена на минутном баре, которая и была записана  как цена Close. Для данной минуты это оказалась цена 1.28958. Время появления цен High и Low также неизвестно, но мы знаем, что максимальная и минимальная цена точно побывала на уровнях 1.28958 и 1.28940 соответственно.

Для тестирования торговой стратегии нам необходима тиковая последовательность, на которой будет эмулироваться работа эксперта. Таким образом, для каждого минутного бара нам известны 4 контрольные точки, о которых мы точно можем сказать, что цена там побывала. Если бар имеет только 4 тика, то для тестирования этой информации достаточно, но обычно тиковый объем больше 4. Значит, необходимо сгенерировать дополнительные контрольные точки для тиков, которые приходили между ценами Open, High, Low и Close. Принцип генерации тиков в режиме "Все тики" описан в статье Алгоритм генерации тиков в тестере стратегий терминала MetaTrader 5, рисунок из которой представлен ниже.

ideal_white_MQL5_2

При тестировании в режиме "Все тики" функция OnTick() эксперта будет вызываться на каждой контрольной точке, каждая контрольная точка - это тик из сгенерированной последовательности. Эксперт будет получать время и цену смоделированного тика так же, как и при работе в онлайне.

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

1 minute OHLC

Тестирование в режиме "Все тики" является самым точным из трех режимов, но в  то же время и самым медленным. Запуск обработчика OnTick() происходит на каждом тике, а тиковый объем может быть достаточно большим. Для стратегий, которым не важно, в какой тиковой последовательности развивалась цена в течение бара, существует более быстрый и более грубый режим моделирования - "1 minute OHLC".

В режиме "1 minute OHLC" тиковая последовательность строится только по OHLC ценам минутных баров, количество сгенерированных контрольных точек существенно уменьшается - следовательно, уменьшается время тестирования. Запуск функции OnTick() производится на всех контрольных точках, которые строятся по ценам OHLC минутных баров.

Отказ от генерации дополнительных промежуточных тиков между ценами Open, High, Low и Close приводит к появлению жесткой детерминированности в развитии цены с того момента, как определена цена Open. Это дает возможность для создания "Грааля тестирования", который показывает красивый восходящий график баланса при тестировании. Пример такого Грааля представлен в CodeBase - Grr-al.

graal_OHLC

На рисунке представлен очень привлекательный график тестирования этого эксперта. Как он получен? Для минутного бара известно 4 цены, и для них точно известно, что первой идет цена Open, а последней идет цена Close. Между ними есть цены High и Low, последовательность их наступления неизвестна, но известно, что цена High больше или равна цене Open (цена Low меньше или равна цене Open).

Достаточно определить момент поступления цены Open и затем анализировать следующий тик, чтобы определить что перед нами - High или Low. Если цена ниже цены Open, значит, перед нами цена Low - покупаем на этом тике, следующий тик будет соответствовать цене High, на котором закрываем покупку и открываем продажу. Следующий тик последний, это цена Close, на нем закрываем продажу.

Если после цены пришел тик с ценой больше цены открытия, то последовательность сделок обратная. Отработаем в таком мошенническом режиме минутный бар и ждем следующий. При тестировании такого эксперта на истории все идет хорошо, но стоит запустить его в онлайне, и сказка рассыпается - линия баланса по-прежнему ровная, но идет вниз. Для быстрого разоблачения трюка достаточно прогнать такой советник в режиме "Все тики".

Важно: если результаты тестирования эксперта на грубых режимах тестирования ("1 minute OHLC" и "Только цены открытия") слишком хороши, обязательно протестируйте его в режиме "Все тики".

Только цены открытия

В данном режиме происходит генерация тиков по ценам OHLC таймфрейма, выбранного для тестирования. При этом функция эксперта OnTick() запускается только в начале бара по цене Open. Из-за этой особенности стоп-уровни и отложенные ордера могут срабатывать не по заявленной цене (особенно при тестировании на старших таймфреймах). В обмен за это мы получаем возможность быстро провести оценочное тестирование эксперта.

Исключением при генерации тиков в режиме "Только цены открытия" являются периоды W1 и MN1: для этих таймфреймов тики генерируются для цен OHLC каждого дня, а не для цен OHLC недели или месяца соответственно.

Например, производится тестирование советника на EURUSD H1 в режиме "Только цены открытия". В этом случае общее количество тиков (контрольных точек) будет не больше 4*количество часовых баров, попавших в тестируемый интервал. Но при этом вызов обработчика OnTick() производится только на открытии часового бара. На остальных ("скрытых" от эксперта) тиках происходят проверки, необходимые для корректного тестирования:

  • вычисление маржевых требований;
  • срабатывание Stop Loss и Take Profit;
  • срабатывание отложенных ордеров;
  • удаление отложенных ордеров с истекшим временем.

Если нет открытых позиций или отложенных ордеров, то необходимости в данных проверках на  скрытых тиках нет и прирост скорости может оказаться существенным. Данный режим "Только цены открытия" хорошо подходит для тестирования стратегий, которые совершают сделки только на открытии бара и не используют отложенные ордера, а также не используют ордера StopLoss,. TakeProfit. Для класса таких стратегий сохраняется вся необходимая точность тестирования.

В качестве примера эксперта, которому не важно в каком режиме тестироваться, приведем советник Moving Average из стандартной поставки. Логика этого эксперта построена таким образом, что все решения принимаются на открытии бара и сделки проводятся сразу же без использования отложенных ордеров. Запустим тестирование эксперта на EURUSD H1 на интервале с 2010.09.01 по 2010.12.31 и сравним графики. На рисунке показаны графики баланса из отчета тестера для всех трех режимов.

График тестирования советника Moving Average.mq5 из стандартной поставки не зависит от режима тестирования

Как видите, графики на разных режимах тестирования абсолютно одинаковы для советника Moving Average из стандартной поставки.

Существует ряд ограничений применения режима "Только цены открытия":

  • Нельзя использовать режим торговли "Произвольная задержка";
  • В тестируемом эксперте невозможно обратиться к данным более низкого таймфрейма, чем тот, что используется для тестирования/оптимизации. Например, если тестирование/оптимизация осуществляется на периоде H1, то вы можете обращаться к данным H2, H3, H4 и т.д., но не к данным M30, M20, M10 и т.д. Помимо этого, более старшие таймфреймы, к которым идет обращение, должны быть кратными таймфрейму тестирования. Например, при тестировании на периоде M20 нельзя обратиться к таймфрейму M30, но можно к H1. Эти ограничения обусловлены невозможностью получить данные более низких и не кратных таймфреймов из баров, генерируемых при тестировании/оптимизации.
  • Ограничения по обращению к данным других таймфремов распространяются и на другие символы, чьи данные используются советником. Однако в этом случае ограничением для каждого символа служит первый таймфрейм, к которому произошло обращение во время тестирования/оптимизации. Например, тестирование осуществляется на символе и периоде EURUSD H1, советник в первый раз обратился к символу GBPUSD M20. В этой ситуации советник в дальнейшем может использовать данные EURUSD H1, H2, и т.д., а также GBPUSD M20, H1, H2 и т.д.

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

В завершение раздела о режимах моделирования приведем визуальное сравнение разных режимов генерации тиков для EURUSD для двух баров M15 на интервале с 2011.01.11 21:00:00 - 2011.01.11 21:30:00. Запись тиков произведена с помощью эксперта WriteTicksFromTester.mq5 в разные файлы, окончание имен этих файлов задаются input-параметраx filenameEveryTick, filenameOHLC и filenameOpenPrice.

EA_inputs_in_tester

Чтобы получить три файла с тремя тиковыми последовательностями (для каждого из режимов "Все тики", "OHLC на минутных барах" и "Только цены открытия") советник был запущен трижды в соответствующих режимах на одиночных прогонах. Затем данные из  этих трех файлов с помощью индикатора TicksFromTester.mq5 были выведены на график. Код индикатора прилагается к статье.

three_tick_series

По умолчанию все файловые операции в языке MQL5 производятся в пределах "файловой песочницы" и при тестировании эксперту доступна только собственная "файловая песочница". Для того чтобы индикатор и эксперт при тестировании работали с файлами из одной папки, использовался флаг FILE_COMMON. Пример кода из эксперта:

//--- откроем файл
   file=FileOpen(filename,FILE_WRITE|FILE_CSV|FILE_COMMON,";");
//--- проверим успешность операции
   if(file==INVALID_HANDLE)
     {
      PrintFormat("Не удалось открыть на запись файл %s. Код ошибки=%d",filename,GetLastError());
      return;
     }
   else
     {
      //--- сообщим о записи в общую папку всех клиентских терминалов и ее местоположение
      PrintFormat("Файл будет записан в папке %s",TerminalInfoString(TERMINAL_COMMONDATA_PATH));
     }

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

//--- откроем файл
   int file=FileOpen(fname,FILE_READ|FILE_CSV|FILE_COMMON,";");
//--- проверим успешность операции
   if(file==INVALID_HANDLE)
     {
      PrintFormat("Не удалось открыть на чтение файл %s. Код ошибки=%d",fname,GetLastError());
      return;
     }
   else
     {
      //--- сообщим местонахождение общей папки всех клиентских терминалов
      PrintFormat("Файл будет прочитан из папки %s",TerminalInfoString(TERMINAL_COMMONDATA_PATH));
     }

Моделирование спреда #

Разница между ценами Bid и Ask называется спредом. При тестировании спред не моделируется, а берется из исторических данных. Если в исторических данных спред меньше или равен нулю, то используется последний известный на момент генерации спред.

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

Кроме того, в исторических данных хранятся значения тиковых и торговых объемов. Для хранения и получения данных используется специальная структура MqlRates:

struct MqlRates
  {
   datetime time;         // время открытия бара
   double   open;         // цена открытия Open
   double   high;         // наивысшая цена High
   double   low;          // наименьшая цена Low
   double   close;        // цена закрытия Close
   long     tick_volume;  // тиковый объем
   int      spread;       // спред
   long     real_volume;  // биржевой объем
  };

Использование реальных тиков при тестировании #

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

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

Тестер проверяет соответствие тиковых данных параметрам минутного бара: тик не должен выходить за пределы цен High/Low бара, открывающий и закрывающий минуту тик должен совпадать с ценами Open/Close бара. Также сравнивается объем. При выявлении несовпадения отбрасываются все тики, соответствующие этому минутному бару. Вместо них будут использованы сгенерированные тики (как в режиме "Все тики").

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

Если в истории символа нет минутного бара, но тиковые данные за эту минуту есть, они могут быть использованы в тестере. Например, бары биржевых символов формируются по ценам Last. Если с сервера приходят только тики с ценами Bid/Ask без цены Last, бар не будет сформирован. Тестер будет использовать эти тиковые данные, поскольку они не противоречат минутным.

Тиковые данные могут не совпадать с минутными барами по различным причинам. Например, из-за обрывов связи или иных сбоев при передаче данных от источника в клиентский терминал. При тестировании минутные данные считаются более достоверными.

При тестировании на реальных тиках учитывайте следующие особенности:

  • При запуске тестирования синхронизируются не только тиковые, но и минутные данные по инструменту.
  • Тики хранятся в кэше символа в тестере стратегий. Размер кэша — не более 128 000 тиков. При поступлении новых тиков самые старые данные из него выталкиваются. Однако при помощи функции CopyTicks можно получить тики и за пределами кэша (только при тестировании по реальным тикам). В этом случае данные будут запрошены из базы тиков тестера, которая полностью соответствует аналогичной базе клиентского терминала. В эту базу никакие корректировки по минутным барам не вносятся. Поэтому тики в ней могут отличаться от тиков, находящихся в кэше.

Глобальные переменные клиентского терминала #

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

Расчет индикаторов при тестировании #

В режиме реального времени значения индикаторов вычисляются на каждом тике.

В тестере стратегий индикаторы рассчитываются только при обращении к ним за данными — то есть только в тот момент, когда запрашиваются значения индикаторных буферов. Исключение составляют пользовательские индикаторы с выставленным #property tester_everytick_calculate, в этом случае пересчет идет на каждом тике.

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

Расчет индикатора на каждом тике делается однократно, и все последующие обращения за данными индикатора до поступления нового тика не вызывают пересчета. Поэтому, если в эксперте с помощью функции EventSetTimer() включен таймер, то перед каждым вызовом обработчика OnTimer() будут запрошены данные индикатора с момента последнего тика. Если на последнем тике расчет индикатора еще не производился, то будут запущены вычисления значений индикатора. Если данные уже были подготовлены, то они будут предоставлены без нового пересчета.

Таким образом, все расчеты индикаторов делаются максимально экономно — если на данном тике индикатор уже был рассчитан, то данные индикатора отдаются как есть, повторный расчет индикатора не запускается.

Загрузка истории при тестировании #

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

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

Обращение к дополнительным инструментам происходит и в том случае, когда вычисляется цена кросс-курса при торговых операциях. Например, при тестировании стратегии на EURCHF с валютой депозита в долларах США перед обработкой первой же торговой операцией агент тестирования запрашивает у клиентского терминала историю по EURUSD и USDCHF, хотя в стратегии нет прямого обращения к этим инструментам.

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

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

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

Мультивалютное тестирование #

Тестер позволяет проводить проверку на истории стратегий, торгующих на нескольких инструментах. Такие  эксперты условно называют мультивалютными, так как изначально в предыдущих платформах тестирование проводилось только для одного инструмента. В тестере же терминала MetaTrader 5 можно моделировать торговлю по всем доступным инструментам.

История по используемым инструментам закачивается тестером из клиентского терминала (не с торгового сервера!) автоматически при первом обращении к данному инструменту.

Агент тестирования закачивает только недостающую историю с небольшим запасом, чтобы обеспечить необходимые данные на истории для расчета индикаторов на момент начала тестирования. Минимальный объем истории при скачивании с торгового сервера для таймфреймов D1 и меньше составляет один год. Таким образом, если запускается тестирование на интервале  2010.11.01-2010.12.01 (тестирование на интервале в один месяц)  с периодом M15 (каждый бар равен 15 минутам), то у терминала будет запрошена история по инструменту за весь 2010 год. Для таймфреймов Weekly будет запрошена история в 100 баров, что составляет примерно два года (в году 52 недели). Для тестирования на месячном таймфрейме Monthly агент запросит историю за 8 лет (12 месяцев * 8 лет = 96 месяцев).

Если не удается по каким-либо причинам обеспечить необходимое количество баров перед началом тестирования, то дата начала будет автоматически сдвинута от прошлого к настоящему для того чтобы обеспечить такое количество баров.

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

Перед началом тестирования мультивалютного эксперта необходимо выбрать требуемые для тестирования инструменты в "Обзоре рынка" терминала и подкачать данные на нужную глубину. При первом же обращении к "чужому" символу будет автоматически произведена синхронизация по этому символу между агентом тестирования и клиентским терминалом. "Чужой" символ - это символ, отличающийся от того, на котором запущено тестирование.

Обращение к данным чужого символа происходят в следующих случаях:

  • запрос к таймсерии по паре символ/период функциями:

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

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

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

Моделирование времени в тестере #

При тестировании локальное время TimeLocal() всегда равно серверному времени TimeTradeServer(). В свою очередь, серверное время всегда равно времени, соответствующему времени GMT - TimeGMT(). Таким образом, все эти функции при тестировании выдают одно и то же время.

Отсутствие разницы между GMT, локальным и серверным временем в тестере сделано сознательно по той самой причине, что связь с сервером может быть не всегда. А результаты тестирования должны быть одинаковыми, независимо от наличия связи. Информация о серверном времени не хранится локально, а берётся с сервера.

Графические объекты при тестировании #

Во время тестирования/оптимизации не осуществляется построение графических объектов. Таким образом, при обращении к свойствам созданного объекта во время тестирования/оптимизации эксперт получит нулевые значения.

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

Функция OnTimer() в тестере #

В MQL5 возможна обработка событий таймера. Вызов обработчика OnTimer() производится независимо от режима тестирования. Это означает, что если тестирование запущено в режиме "Только цены открытия" на периоде H4 и внутри эксперта установлен таймер с вызовом каждую секунду, то на открытии каждого H4 бара один раз будет вызван обработчик OnTick() и 14400 раз (3600 секунд * 4 часа)  в течение бара будет вызван обработчик OnTimer(). Насколько при этом увеличится время тестирования эксперта, зависит от логики эксперта.

Для проверки зависимости времени тестирования от заданной периодичности таймера был написан простой эксперт без торговых операций.

//--- input parameters
input int      timer=1;              // значение таймера, сек
input bool     timer_switch_on=true; // таймер включен
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- запустим таймер если  timer_switch_on==true
   if(timer_switch_on)
     {
      EventSetTimer(timer);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- остановим таймер
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//---
// ничего не делаем, тело обработчика пустое
  }
//+------------------------------------------------------------------+

Были сделаны замеры времени тестирования при различных значениях параметра timer (периодичность события Timer). На полученных данных построен график зависимости времени тестирования T от значения периодичности Period.

Time_Dependence_rus

Хорошо видно, чем меньше параметр timer при инициализации таймера функцией EventSetTimer(timer), тем меньше период (Period) между вызовами обработчика OnTimer(), и  тем больше время тестирования T при одних и тех же остальных условиях.

Функция Sleep() в тестере #

Функция Sleep() позволяет в эксперте или скрипте приостановить выполнение mql5-программы на некоторое время при работе на графике. Это может понадобиться при запросе каких-либо данных, которые в момент запроса еще не готовы и необходимо дождаться момента их готовности. Подробный пример использования функции Sleep() можно посмотреть в разделе Организация доступа к данным.

В тестере же вызовы Sleep() не задерживают процесс тестирования. При вызове Sleep() "проигрываются" сгенерированные тики в пределах указанной задержки, в результате чего могут сработать отложенные ордера, стопы и т.д. После вызова Sleep() cмоделированное  в тестере время увеличивается на интервал, указанный в параметре функции Sleep.

Если в результате выполнения функции Sleep() текущее время в тестере вышло за  конец периода тестирования, то  будет получена ошибка "бесконечный цикл в Sleep". При получение такой ошибки результаты тестирования не отбрасываются,  все вычисления производятся в полном объеме (количество сделок, просадка и т.д.) и результаты данного тестирования передаются терминалу.

Функция Sleep() не будет работать в OnDeinit(), так как после ее вызова тестерное время гарантированно окажется за пределами интервала тестирования.

Схема использования функции Sleep() в тестере терминала MetaTrader 5.

Использование тестера для задач оптимизации в математических вычислениях #

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

math_calculations

При выборе режима "Математические вычисления" будет произведен "пустой" прогон агента тестирования. Пустой прогон означает, что не будет производиться генерация тиков и загрузки истории. При таком прогоне будут вызваны  только три функции: OnInit(), OnTester(), OnDeinit().

Если дата окончания тестирования меньше или равна дате начала тестирования, то это также будет означать тестирования в режиме "Математические вычисления".

При использовании тестера для решения математических задач закачка истории и генерация тиков не происходят.

Типичная математическая задача для решения в тестере MetaTrader 5 - поиск экстремума от функции многих переменных. Для ее решения необходимо:

  • Разместить блок вычислений значения функции от многих переменных в OnTester() и вернуть вычисленное значение через return(значение_функции);
  • Вынести параметры функции в глобальную область программы в виде input-переменных;

Компилируем советник, открываем окно "Тестер". На вкладке "Входные параметры"  отмечаем требуемые входные переменные и задаем для них задаем границы в пространстве значений и шаг для перебора.

Выбираем  тип оптимизации - "Медленный (полный перебор параметров) или "Быстрая (генетический алгоритм)". Для простого поиска экстремума функции лучше выбрать быструю оптимизацию, но если нужно вычислить значения на всем пространстве переменных, то подойдет медленная оптимизация.

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

return(1/значение_функции);

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

return(-значение_функции);

В этом варианте не требуется проверять значение_функции на равенство нулю и сама поверхность результатов оптимизации в 3D-представлении имеет ту же форму, только зеркально отраженную от исходной.

В качестве примера приведем функцию sink():

sink_formula

Код советника для поиска экстремума этой функции поместим в OnTester():

//+------------------------------------------------------------------+
//|                                                         Sink.mq5 |
//|                        Copyright 2011, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2000-2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
//--- input parameters
input double   x=-3.0; // start=-3, step=0.05, stop=3
input double   y=-3.0; // start=-3, step=0.05, stop=3
//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester()
  {
//---
   double sink=MathSin(x*x+y*y);
//---
   return(sink);
  }
//+------------------------------------------------------------------+

Проведем оптимизацию и представим результаты оптимизации в виде 2D графика.

Результаты полной оптимизации функции sink(x*x+y*y) в виде 2D-графика.

Чем лучше значение для заданной пары параметров (x,y), тем более насыщенный цвет. Как и ожидалось из вида формулы функции sink(), ее значения образуют концентрические круги с центром в точке (0,0). Для функции sink() не существует абсолютного экстремума. Это хорошо видно при просмотре результатов оптимизации в режиме 3D:

visual_3D

Синхронизация баров при тестировании в режиме "Только цены открытия" #

Тестер в клиентском терминале MetaTrader 5 позволяет проверять и, так называемые, "мультивалютные" советники. Мультивалютный советник - это советник, который торгует на двух или более символах.

Тестирование стратегий, торгующих на нескольких инструментах, налагает на тестер несколько дополнительных технических требований:

  • генерации тиков для этих инструментов;
  • расчет значений индикаторов для этих инструментов;
  • расчет маржевых требований по этим инструментам;
  • синхронизация сгенерированных тиковых последовательностей по всем торгуемым инструментам.

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

Такое достоверное моделирование развития истории в тестере не вызывает вопросов до тех пор, пока используются режимы тестирования "Все тики" и "1 minute OHLC". При этих режимах  в пределах одной свечи генерируется достаточное количество тиков, чтобы дождаться момента синхронизации баров с разных символов. Но как тестировать мультивалютные стратегии в режиме "Только цены открытия", если требуется обязательная синхронизация баров на торгуемых инструментах? Ведь в этом режиме эксперт вызывается только на одном тике, который соответствует времени открытия бара.

Поясним на примере: если мы тестируем эксперта на символе EURUSD, и на EURUSD открылась новая часовая свеча, то мы легко узнаем этот факт - при тестировании в режиме "Только цены открытия" событие NewTick соответствует моменту открытия бара на тестируемом периоде. Но при этом нет никакой гарантии, что новая свеча открылась по символу GBPUSD, который используется в эксперте.

В обычных условиях достаточно завершить работу функции OnTick() и проверить появление нового бара на GBPUSD на следующем тике. Но при тестировании в режиме "Только цены открытия" другого тика не будет, и может показаться, что этот режим не годится для тестирования мультивалютных экспертов. Но это не так - не забывайте, что тестер в MetaTrader 5 ведет себя также, как и в жизни. Можно дождаться момента, когда на другом символе откроется новый бар с помощью функции Sleep()!

Код советника Synchronize_Bars_Use_Sleep.mq5, который демонстрирует пример синхронизации баров при тестировании в режиме "Только цены открытия":

//+------------------------------------------------------------------+
//|                                   Synchronize_Bars_Use_Sleep.mq5 |
//|                        Copyright 2011, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2000-2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
//--- input parameters
input string   other_symbol="USDJPY";
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- сверим текущий символ
   if(_Symbol==other_symbol)
     {
      PrintFormat("Необходимо указать другой символ или запустить тестирование на другом символе!");
      //--- принудительно прекращаем тестирование эксперта
      return(INIT_PARAMETERS_INCORRECT);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- статическая переменная для хранения времени открытия последнего бара
   static datetime last_bar_time=0;
//---  признак того, что время открытия последнего бара на разных символах синхронизировано
   static bool synchonized=false;
//--- если статическая переменная еще неинициализирована
   if(last_bar_time==0)
     {
      //--- это первый вызов, запишем время открытия и выйдем
      last_bar_time=(datetime)SeriesInfoInteger(_Symbol,Period(),SERIES_LASTBAR_DATE);
      PrintFormat("Инициализировали переменную last_bar_time значением %s",TimeToString(last_bar_time));
     }
//--- получим время открытия последнего бара по своему символу
   datetime curr_time=(datetime)SeriesInfoInteger(Symbol(),Period(),SERIES_LASTBAR_DATE);
//--- если время открытия текущего бара не совпадает с тем, что хранится в last_bar_time
   if(curr_time!=last_bar_time)
     {
      //--- запомним время открытия нового бара в статической переменной
      last_bar_time=curr_time;
      //--- синхронизация нарушена, сбросим флаг в false
      synchonized=false;
      //--- выведем сообщение об этом событии
      PrintFormat("На символе %s открылся новый бар в %s",_Symbol,TimeToString(TimeCurrent()));
     }
//--- сюда будем сохранять время открытия бара на чужом символе
   datetime other_time;
//--- цикл, пока время открытия последнего бара по другому символу не сравняется с curr_time
   while(!(curr_time==(other_time=(datetime)SeriesInfoInteger(other_symbol,Period(),SERIES_LASTBAR_DATE)) && !synchonized))
     {
      PrintFormat("Подождем 5 секунд..");
      //--- подождем 5 секунд и опять запросим SeriesInfoInteger(other_symbol,Period(),SERIES_LASTBAR_DATE)
      Sleep(5000);
     }
//--- время открытия бара теперь одинаково для обоих символов
   synchonized=true;
   PrintFormat("Время открытия последнего бара на своем символе %s: %s",_Symbol,TimeToString(last_bar_time));
   PrintFormat("Время открытия последнего бара на символе %s: %s",other_symbol,TimeToString(other_time));
//--- TimeCurrent() не подойдет, используем TimeTradeServer() для
   Print("Бары синхронизировались в ",TimeToString(TimeTradeServer(),TIME_DATE|TIME_SECONDS));
  }
//+------------------------------------------------------------------+

Обратите внимание на последнюю строчку в советнике, которая выводит текущее время, когда был установлен факт синхронизации:

   Print("Бары синхронизировались в ",TimeToString(TimeTradeServer(),TIME_SECONDS));

Для вывода текущего времени мы использовали функцию TimeTradeServer(), а не TimeCurrent(). Дело в том, что функция TimeCurrent() возвращает время последнего тика, которое никак не изменилось после использования Sleep(). Запустите советник в режиме "Только цены открытия" и увидите сообщения о синхронизации баров.

Synchronize_Bars_Use_Sleep_EA

Используйте функцию TimeTradeServer() вместо TimeCurrent(), если требуется получить текущее серверное время, а не время поступления последнего тика.

Есть и другой способ синхронизации баров  - с помощью таймера. Пример такого эксперта Synchronize_Bars_Use_OnTimer.mq5 приложен к статье.

Функция IndicatorRelease() в тестере #

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

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

Но в некоторых случаях программисту может понадобиться скрыть информацию о том, какие индикаторы задействованы в торговом алгоритме. Например, код эксперта сдается в аренду или продается в виде исполняемого файла без предоставления исходного кода. Для этих целей подойдет функция IndicatorRelease().

Если в терминале задан шаблон с названием tester.tpl в каталоге /profiles/templates клиентского терминала, то именно он будет применен к открываемому графику. При его отсутствии применяется шаблон по умолчанию (default.tpl).

Функция IndicatorRelease() изначально предназначена для освобождения расчетной части индикатора, если он больше не нужен. Это позволяет экономить как память, так и ресурсы процессора, потому что каждый тик вызывает расчет индикатора. Второе ее предназначение - запретить показ индикатора на графике тестирования после окончания одиночного прогона.

Чтобы запретить показ индикатора на графике по окончании тестирования, вызовете IndicatorRelease() с хэндлом индикатора в обработчике OnDeinit(). Функция OnDeinit() всегда вызывается после завершения и перед показом графика тестирования.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   bool hidden=IndicatorRelease(handle_ind);
   if(hidden) 
      Print("IndicatorRelease() выполнена успешно");
   else 
      Print("IndicatorRelease() вернула false. Код ошибки ",GetLastError());
  }

Для того чтобы запретить показ индикатора на графике после завершения одиночного тестирования, используйте функцию IndicatorRelease() в обработчике OnDeinit().

Обработка событий в тестере #

Наличие обработчика OnTick() в эксперте не является обязательным для того чтобы его можно было подвергнуть проверке на исторических данных в тестере терминала MetaTrader 5. Достаточно того, чтобы в советнике была хотя бы одна функция-обработчик из перечисленных:

  • OnTick() - обработчик события прихода нового тика;
  • OnTrade() - обработчик торгового события;
  • OnTimer() - обработчик события прихода сигнала от таймера;
  • OnChartEvent() - обработчик пользовательских событий.

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

Индикатор при тестировании может генерировать пользовательские события с помощью функции EventChartCustom(), а советник может обрабатывать это событие в OnChartEvent().

Помимо вышеуказанных событий в тестере стратегий генерируются специальные события, связанные с процессом тестирования и оптимизации:

  • Tester - данное событие генерируется по окончании тестирования эксперта на исторических данных. Обработка события  Tester производится функцией OnTester(). Эта функция может быть использована только в экспертах при тестировании и предназначена в первую очередь для расчета некоторого значения, используемого в качестве критерия Custom max при генетической оптимизации входных параметров.
  • TesterInit - данное событие генерируется при запуске оптимизации в тестере стратегий перед самым первым проходом. Обработка события TesterInit производится функцией OnTesterInit(). Эксперт, имеющий данный обработчик, при запуске оптимизации автоматически загружается на отдельном графике терминала с указанными в тестере символом и периодом, и получает событие TesterInit. Функция предназначена для инициализации эксперта перед началом оптимизации для последующей обработки результатов оптимизации.
  • TesterPass - данное событие генерируется при поступлении нового фрейма данных. Обработка события TesterPass производится функцией OnTesterPass(). Эксперт с данным обработчиком автоматически загружается на отдельном графике терминала с указанными для тестирования символом/периодом и получает во время оптимизации события TesterPass при получении фрейма. Функция предназначена для динамической обработки результатов оптимизации прямо "на лету", не дожидаясь её окончания. Добавление фреймов производится функцией FrameAdd(), которую можно вызывать по окончании одиночного прохода в обработчике OnTester().
  • TesterDeinit - данное событие генерируется по окончании оптимизации эксперта в тестере стратегий. Обработка события TesterDeinit производится функцией OnTesterDeinit(). Эксперт с данным обработчиком автоматически загружается на график при запуске оптимизации и получает событие TesterDeinit после её завершения. Функция предназначена для финальной обработки всех результатов оптимизации.

Агенты тестирования #

Тестирование в клиентском терминале MetaTrader 5 осуществляется с помощью агентов тестирования. Локальные агенты создаются и подключаются автоматически. Количество локальных агентов по умолчанию соответствует количеству ядер на компьютере.

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

Агент хранит полученную от терминала историю в отдельных папках по имени инструмента, то есть история для  EURUSD хранится в папке с именем EURUSD. Кроме того история инструментов разделяется по источникам. Структура для хранения истории выглядит таким образом:

каталог_тестера\Agent-IPaddress-Port\bases\имя_источника\history\имя_инструмента

Например, история по EURUSD с сервера MetaQuotes-Demo может храниться в папке каталог_тестера\Agent-127.0.0.1-3000\bases\MetaQuotes-Demo\EURUSD.

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

При досрочном завершении тестирования со стороны пользователя (кнопка "Отмена"), а также при закрытии клиентского терминала все локальные агенты тут же прекращают свою работу и выгружаются из памяти.

Обмен данными между терминалом и агентом #

При запуске тестирования терминал готовит для отправки агенту несколько блоков параметров:

  • Входные параметры тестирования (режим моделирования, интервал тестирования, инструмент, критерий оптимизации и т.д.)
  • Список выбранных в "Обзоре рынка" инструментов
  • Спецификация тестируемого инструмента (размер контракта, допустимые отступы от рынка для установки StopLoss и Takeprofit, и т.д)
  • Тестируемый эксперт и значения его входных параметров
  • Информация о дополнительных файлах (библиотеки, индикаторы, файлы данных - #property tester_...)

tester_indicator

string

Имя пользовательского индикатора в формате "имя_индикатора.ex5". Необходимые для тестирования индикаторы определяются автоматически из вызова функций iCustom(), если соответствующий параметр задан константной строкой. Для остальных случаев (использование функции IndicatorCreate() или использование неконстантной строки в параметре, задающем имя индикатора) необходимо данное свойство

tester_file

string

Имя файла для тестера с указанием расширения, заключенное в двойные кавычки (как константная строка). Указанный файл будет передан тестеру в работу. Входные файлы для тестирования, если необходимы, должны указываться всегда

tester_library

string

Имя библиотеки с расширением, заключенное в двойные кавычки. Библиотека может быть как с расширением dll, так и с расширением ex5. Необходимые для тестирования библиотеки определяются автоматически. Однако, если какая-либо библиотека используется пользовательским индикатором, то необходимо использовать данное свойство

Для каждого блока параметров создается цифровой отпечаток в виде MD5-хэша, который и посылается агенту. MD5-хэш является уникальным для каждого набора, его объем во много раз меньше объема информации, на основе которой он вычислен.

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

После проведения тестирования агент возвращает терминалу все результаты прогона, показываемые во вкладках "Результаты тестирования" и  "Результаты оптимизации": полученная прибыль, количество сделок,  коэффициент Шарпа, результат функции OnTester() и т.д.

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

Агенты никогда не записывают на жесткий диск полученные от терминала EX5-файлы (эксперт, индикаторы, библиотеки и т.д.) из соображений безопасности, чтобы на компьютере с установленным агентом нельзя было воспользоваться присланными данным. Все остальные файлы, в том числе DLL, записываются в "песочницу". В удалённых агентах нельзя тестировать экспертов с использованием DLL.

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

Весь трафик между терминалом и агентами шифруется.

Тики не пересылаются по сети, они генерируются на тестерных агентах.

Использование общей папки всех клиентских терминалов #

Все тестерные агенты изолированы друг от друга и от клиентского терминала: у каждого  агента есть собственная папка, в которую записываются логи агента. Кроме того, все файловые операции при тестирования агента происходят в папке имя_агента/MQL5/Files. Однако можно реализовать взаимодействие между локальными агентами и клиентским терминалом через общую папку всех клиентских терминалов, если при открытии файла указать флаг FILE_COMMON:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- общая папка всех клиентских терминалов
   common_folder=TerminalInfoString(TERMINAL_COMMONDATA_PATH);
//--- выведем имя этой папки
   PrintFormat("Откроем файл в общей папке клиентских терминалов %s", common_folder);
//--- откроем файл в общей папке (указан флаг FILE_COMMON)
   handle=FileOpen(filename,FILE_WRITE|FILE_READ|FILE_COMMON);
   ... дальнейшие действия
//---
   return(INIT_SUCCEEDED);
  }

Использование DLL #

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

Второе ограничение - запрет на использование DLL при тестировании экспертов. Вызовы DLL безусловно запрещены на удалённых агентах из соображений безопасности. На локальных агентах вызовы dll в тестируемых экспертах разрешены только в том случае, если установлено соответствующее разрешение "Разрешить импорт DLL".

Опция "Разрешить импорт DLL" в mql5-программах

Важно: при использовании полученных со стороны советников (скриптов, индикаторов), которые требуют разрешить вызовы DLL, вы должны осознавать весь риск, который принимаете на себя в случае разрешения этой опции в настройках терминала. Независимо от того, как будет использован советник - для тестирования или для запуска на графике.