preview
Разрабатываем мультивалютный советник (Часть 8): Проводим нагрузочное тестирование и обрабатываем новый бар

Разрабатываем мультивалютный советник (Часть 8): Проводим нагрузочное тестирование и обрабатываем новый бар

MetaTrader 5Тестер | 18 апреля 2024, 17:32
264 0
Yuriy Bykov
Yuriy Bykov

Введение

В первой статье мы разработали советник, в котором было два экземпляра торговых стратегий. Во второй статье использовали уже девять экземпляров, а в последней — 32 экземпляра. При этом каких-либо проблем со слишком долгим временем тестирования не было. Понятно, что чем меньше будет время одиночного прохода тестера, тем лучше. Но если в целом процесс оптимизации занимает время порядка нескольких часов, то это приемлемо по сравнению с ситуацией, когда этот процесс занимал бы несколько дней или недель. Точно также, если мы объединили в одном советнике несколько экземпляров стратегий и хотим посмотреть его результаты, то одиночный проход должен завершаться за секунды или минуты, а не за часы или дни.

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

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


Разное количество экземпляров в тестере

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

  • Уберём входные параметры, задающие индексы восьми наборов параметров, которые брались из полного массива наборов, загружаемого из файла. Оставим параметр count_, который теперь будет задавать количество наборов, загружаемых из полного массива наборов.
  • Уберём проверку на уникальность индексов, которых теперь уже нет. Будем добавлять в массив стратегий новые стратегии, с наборами параметров, взятых из первых count_ элементов массива наборов параметров params. Если в этом массиве будет не хватать экземпляров, то будем брать по следующему кругу с начала массива.
  • Уберём функции OnTesterInit() и OntesterDeinit(), поскольку не будем пока использовать данный эксперт для оптимизации чего-либо.

Получим такой код:

//+------------------------------------------------------------------+
//| Входные параметры                                                |
//+------------------------------------------------------------------+
input group "::: Управление капиталом"
sinput double expectedDrawdown_ = 10;  // - Максимальный риск (%)
sinput double fixedBalance_ = 10000;   // - Используемый депозит (0 - использовать весь) в валюте счета
sinput double scale_ = 1.00;           // - Масштабирующий множитель для группы

input group "::: Отбор в группу"
sinput string fileName_ = "Params_SV_EURGBP_H1.csv";  // - Файл с параметрами стратегий (*.csv)
input int     count_ = 8;              // - Количество стратегий в группе (1 .. 8)

input group "::: Прочие параметры"
sinput ulong  magic_        = 27183;   // - Magic

...

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Загружаем наборы параметров стратегий
   int totalParams = LoadParams(fileName_, params);

// Если ничего не загрузили, то сообщим об ошибке
   if(totalParams == 0) {
      PrintFormat(__FUNCTION__" | ERROR: Can't load data from file %s.\n"
                  "Check that it exists in data folder or in common data folder.",
                  fileName_);
      return(INIT_PARAMETERS_INCORRECT);
   }

// Сообщаем об ошибке, если
   if(count_ < 1) { // количество экземпляров меньше 1
      return INIT_PARAMETERS_INCORRECT;
   }
   
   ArrayResize(params, count_);

// Устанавливаем параметры в классе управления капиталом
   CMoney::DepoPart(expectedDrawdown_ / 10.0);
   CMoney::FixedBalance(fixedBalance_);

// Создаем эксперта, работающего с виртуальными позициями
   expert = new CVirtualAdvisor(magic_, "SimpleVolumes_BenchmarkInstances");

// Создаем и наполняем массив из всех экземпляров стратегий
   CVirtualStrategy *strategies[];

   FOREACH(params, APPEND(strategies, new CSimpleVolumesStrategy(params[i % totalParams])));

// Формируем и добавляем к эксперту группу стратегий
   expert.Add(CVirtualStrategyGroup(strategies, scale_));

   return(INIT_SUCCEEDED);
}

Сохраним полученный код в файле BenchmarkInstancesExpert.mq5 в текущей папке.

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


Результаты тестирования для разных режимов

Начнём с привычного уже режима моделирования тиков "OHLC на M1", который мы использовали во всех предыдущих статьях. Количество экземпляров будем увеличивать в два раза на следующем запуске. Начнём с 8 экземпляров. Если время тестирования будет становиться уже слишком большим, то уменьшим период тестирования. 


Рис. 1. Результаты одиночных прогонов в режиме "OHLC на M1"


Как видно, при тестировании с количеством экземпляров до 512 штук, мы использовали период тестирования протяженностью 6 лет, затем перешли на период 1 год и для последних двух проходов использовали только 3 месяца. 

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

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

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

Для более точных результатов можно повторить по несколько проходов для каждого количества экземпляров и вычислить средние времена и средние объёмы памяти, так как на разных проходах с одинаковым количеством экземпляров результаты всё равно отличались. Но эти отличия были не очень большими, поэтому в проведении более объёмного тестирования особого смысла нет. Мы ведь хотели только убедиться, что мы не столкнёмся с ограничениями уже на относительно небольших количествах экземпляров.

Попробуем теперь посмотреть на результаты запусков советника в тестере в режиме моделирования "Все тики".

Рис. 2. Результаты одиночных прогонов в режиме "Все тики"


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

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

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


Рис. 3. Результаты одиночных прогонов в режиме "Все тики на основе реальных тиков"


По сравнению с предыдущим режимом время ещё возросло примерно на 30%, увеличилась примерно на 20% используемая память.

Стоит отметить, что одновременно с тестированием в терминале работал один экземпляр этого советника, прикреплённый к графику. В нём использовалось 8192 экземпляра. При этом потребление памяти терминалом составляло около 200 Мб, а ресурсов процессора - от 0% до 4%.

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

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


Отключение вывода

В текущей реализации мы выводим довольно много информации в лог в процессе работы советника. Когда речь идёт об оптимизации одиночных экземпляров, то это не создаёт проблемы, так как функции вывода просто не выполняются. Если же мы запускаем в тестере одиночный проход советника, то в лог будут выводиться все сообщения. В используемой библиотеке VirtualOrder.mqh, например, выводится сообщение об обработке событий каждого виртуального ордера. Когда виртуальных ордеров мало, то это слабо влияет на время тестирования, но когда их количество начинает составлять десятки тысяч, то это может оказывать уже заметный эффект.

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

#define PrintFormat StringFormat

Благодаря родственности этих функций, можно заменить все вызовы PrintFormat() на вызовы StringFormat(), которые будут формировать строку, но не будут её выводить в лог.

Проведя несколько запусков на некоторых было замечено уменьшение времени на 5 - 10%, а в других время могло и немного увеличиться. Что же, мы попробовали. Кстати, подобный способ замены PrintFormat() нам, возможно ещё пригодится в будущем.

 

Миграция на OHLC на M1

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

Понятно, что не все торговые стратегии могут себе позволить подобное. Если стратегия предполагает очень частое открытие/закрытие позиций (чаще одного раза в минуту), то отказаться от тестирования на всех тиках невозможно. Даже высокочастотная торговля длится не всё время, а только в выделенные промежутки времени. Но если стратегия не нуждается в частом открытии/закрытии и не столь чувствительна к потере нескольких пунктов из-за недостаточно точного срабатывания уровней Stop Loss и Take Profit, то почему бы воспользоваться такой возможностью?

Рассматриваемая в качестве примера торговая стратегия как раз относится к таким, которые позволяют уйти от использования режима всех тиков. Однако тут возникает ещё одна проблема. Если просто оптимизировать параметры одиночных экземпляров в режиме "OHLC на M1", а затем поставить собранного советника на работу в терминале, то там советнику придётся работать в режиме всех тиков. Он будет получать не фиксированные 4 тика в минуту, а гораздо больше. Поэтому функция OnTick() будет вызываться чаще, и набор цен, которые будет обрабатывать советник будет немного разнообразнее.

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

Рис. 4. Сравнение результатов одиночных прогонов в режиме
"Все тики на основе реальных тиков" (слева) и "OHLC на M1" (справа)


Можно заметить, что в для разных режимов время открытия, закрытия и цена немного отличаются. Поначалу отличие только в этом, но потом наступает момент, когда слева видим открытие сделки, а справа в это же время открытия нет: посмотрите на строки со сделкой #25. Таким образом, результаты для режима "OHLC на M1" содержат меньше сделок, чем для режима "Все тики на основе реальных тиков". 

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


Рис. 5. Результаты тестирования в режиме "OHLC на M1" (сверху) и "Все тики на основе реальных тиков" (снизу)


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

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


Определение нового бара

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

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

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

Тогда нам понадобится хранить информацию о временах наступления последнего бара для каждого символа и таймфрейма. Но под "каждым" мы подразумеваем не все, которые доступны в терминале, а только реально требуемые для работы конкретных экземпляров торговых стратегий. Чтобы определить круг этих необходимых символов и таймфреймов, мы расширим список действий, выполняемый функцией IsNewBar(symbol, timeframe). Пусть она сначала проверяет, существует ли в каком-то виде запомненное время для текущего бара на данном символе и таймфрейме. Если не существует, то она будет выполнять действия по созданию такого места хранения времени. Если существует, то будет возвращать результат проверки наступления нового бара.

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

Функция UpdateNewBar() должна будет вызываться один раз в начале обработки нового тика. Например, её вызов можно поставить в начале метода CVirtualAdvisor::Tick():

void CVirtualAdvisor::Tick(void) {
// Определяем новый бар по всем нужным символам и таймфреймам
   UpdateNewBar();

   ...
// Запуск обработки в стратегиях, где можно уже использовать IsNewBar(...)
   CAdvisor::Tick();

   ...
}

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

В этом классе у нас будет два массива: массив названий символов (m_symbols) и массив указателей на объекты нового класса (m_symbolNewBarEvent). Первый будет содержать символы, по которым мы будем отслеживать события нового бара. Второй — указатели на объекты нового класса CSymbolNewBarEvent, которые будут хранить времена баров для одного символа, но для разных таймфреймов.

В этих двух классах будет три метода:

  • Метод регистрации нового отслеживаемого символа или таймфрейма для символа Register(...)
  • Метод обновления признаков нового бара Update()
  • Метод получения признака нового бара IsNewBar(...)

При необходимости регистрации отслеживания события нового бара по новому символу будет создаваться новый объект класса CSymbolNewBarEvent. Поэтому необходимо позаботиться об очистке памяти, занятой этими объектами при завершении работы советника. Для этого добавлен статический метод CNewBarEvent::Destroy() и глобальная функция DestroyNewBar(). Вызов этой функции мы добавим в деструктор эксперта:

//+------------------------------------------------------------------+
//| Деструктор                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   delete m_receiver;         // Удаляем получатель
   delete m_interface;        // Удаляем интерфейс
   DestroyNewBar();           // Удаляем объекты отслеживания нового бара
}

Полная реализация этих классов может выглядеть примерно так:

//+------------------------------------------------------------------+
//| Класс определения нового бара для конкретного символа            |
//+------------------------------------------------------------------+
class CSymbolNewBarEvent {
private:
   string            m_symbol;         // Отслеживаемый символ
   long              m_timeFrames[];   // Массив отслеживаемых таймфреймов для символа
   long              m_timeLast[];     // Массив времен наступления последних баров для таймфреймов
   bool              m_res[];          // Массив признаков наступления нового бара для таймфреймов

   // Метод регистрации нового отслеживаемого таймфрейма для символа
   int               Register(ENUM_TIMEFRAMES p_timeframe) {
      APPEND(m_timeFrames, p_timeframe);  // Добавляем его в массив таймфреймов
      APPEND(m_timeLast, 0);              // Время последнего бара по нему пока неизвестно
      APPEND(m_res, false);               // Нового бара по нему пока нет
      Update();                           // Обновляем признаки нового бара
      return ArraySize(m_timeFrames) - 1;
   }

public:
   // Конструктор
                     CSymbolNewBarEvent(string p_symbol) :
                     m_symbol(p_symbol) // Устанавливаем символ
   {}

   // Метод обновления признаков нового бара
   bool              Update() {
      bool res = (ArraySize(m_res) == 0);
      FOREACH(m_timeFrames, {
         // Получаем время текущего бара
         long time = iTime(m_symbol, (ENUM_TIMEFRAMES) m_timeFrames[i], 0);
         // Если не совпадает с запомненным - то это новый бар
         m_res[i] = (time != m_timeLast[i]);
         res |= m_res[i];
         // Запоминаем новое время
         m_timeLast[i] = time;
      });
      return res;
   }

   // Метод получения признака нового бара
   bool              IsNewBar(ENUM_TIMEFRAMES p_timeframe) {
      int index;
      // Ищем индекс нужного таймфрейма
      FIND(m_timeFrames, p_timeframe, index);

      // Если не найден, то зарегистрируем новый таймфрейм
      if(index == -1) {
         PrintFormat(__FUNCTION__" | Register new event handler for %s %s", m_symbol, EnumToString(p_timeframe));
         index = Register(p_timeframe);
      }

      // Возвращаем признак нового бара для нужного таймфрейма
      return m_res[index];
   }
};


//+------------------------------------------------------------------+
//| Статический класс определения нового бара для всех               |
//| символов и таймфреймов                                           |
//+------------------------------------------------------------------+
class CNewBarEvent {
private:
   // Массив объектов для определения нового бара для одного символа
   static   CSymbolNewBarEvent     *m_symbolNewBarEvent[];

   // Массив нужных символов
   static   string                  m_symbols[];

   // Метод регистрации нового символа и таймфрейма для отслеживания нового бара
   static   int                     Register(string p_symbol)  {
      APPEND(m_symbols, p_symbol);
      APPEND(m_symbolNewBarEvent, new CSymbolNewBarEvent(p_symbol));
      return ArraySize(m_symbols) - 1;
   }

public:
   // Объекты этого класса создавать не понадобится - удаляем конструктор
                            CNewBarEvent() = delete; 

   // Метод обновления признаков нового бара
   static bool              Update() {
      bool res = (ArraySize(m_symbolNewBarEvent) == 0);
      FOREACH(m_symbols, res |= m_symbolNewBarEvent[i].Update());
      return res;
   }

   // Метод освобождения памяти для автоматически созданных объектов
   static void              Destroy() {
      FOREACH(m_symbols, delete m_symbolNewBarEvent[i]);
      ArrayResize(m_symbols, 0);
      ArrayResize(m_symbolNewBarEvent, 0);
   }

   // Метод получения признака нового бара
   static bool              IsNewBar(string p_symbol, ENUM_TIMEFRAMES p_timeframe) {
      int index;
      // Ищем индекс нужного символа
      FIND(m_symbols, p_symbol, index);
      
      // Если не найден, то зарегистрируем новый символ
      if(index == -1) index = Register(p_symbol);
      
      // Возвращаем признак нового бара для нужного символа и таймфрейма
      return m_symbolNewBarEvent[index].IsNewBar(p_timeframe);
   }
};

// Инициализация статических членов класса CSymbolNewBarEvent;
CSymbolNewBarEvent* CNewBarEvent::m_symbolNewBarEvent[];
string CNewBarEvent::m_symbols[];


//+------------------------------------------------------------------+
//| Функция проверки наступления нового бара                         |
//+------------------------------------------------------------------+
bool IsNewBar(string p_symbol, ENUM_TIMEFRAMES p_timeframe) {
   return CNewBarEvent::IsNewBar(p_symbol, p_timeframe);
}

//+------------------------------------------------------------------+
//| Функция обновления информации о новых барах                      |
//+------------------------------------------------------------------+
bool UpdateNewBar() {
   return CNewBarEvent::Update();
}

//+------------------------------------------------------------------+
//| Функция удаления объектов отслеживания нового бара               |
//+------------------------------------------------------------------+
void DestroyNewBar() {
   CNewBarEvent::Destroy();
}
//+------------------------------------------------------------------+

Сохраним этот код в файле NewBarEvent.mqh в текущей папке.

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


Исправления торговой стратегии

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

Первая ошибка приводила к тому, что при задании в параметрах отрицательного значения параметра openDistance_, его значение в стратегии сбрасывалось в небольшое положительное число, равное спреду по текущему символу. То есть вместо открытия отложенных ордеров BUY STOP и SELL_STOP шло открытие рыночных позиций. Это привело к тому, что при оптимизации мы не видели результатов, которые могли бы быть получены при торговле такими отложенными ордерами. То есть упустили часть потенциально прибыльных наборов параметров.

Ошибка возникала в этой строке кода в файле SimpleVolumesStrategy.mqh в функциях открытия отложенных ордеров:

// Сделаем, чтобы расстояние открытия было не меньше спреда
   int distance = MathMax(m_openDistance, spread);

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

// Сделаем, чтобы расстояние открытия было не меньше спреда
   int distance = MathMax(MathAbs(m_openDistance), spread) * (m_openDistance < 0 ? -1 : 1);

Вторая ошибка состояла в том, что при расчёте среднего объёма за несколько последних баров в этот расчёт включался объём и текущего бара. Хотя по описанию стратегии мы не должны использовать его для расчёта среднего. Однако влияние этой ошибки, вероятно, тоже достаточно мало. Чем больше период усреднения объёмов, тем меньший вклад в среднее вносил последний бар. 

Чтобы её исправить, достаточно слегка изменить функцию расчёта среднего, исключив самый первый элемент передаваемого массива:

//+------------------------------------------------------------------+
//| Среднее значение массива чисел со второго элемента               |
//+------------------------------------------------------------------+
double CSimpleVolumesStrategy::ArrayAverage(const double &array[]) {
   double s = 0;
   int total = ArraySize(array) - 1;
   for(int i = 1; i <= total; i++) {
      s += array[i];
   }

   return s / MathMax(1, total);
}

Сохраним эти изменения в файле SimpleVolumesStrategy.mqh в текущей папке.


Учёт нового бара в стратегии

Для того, чтобы в торговой стратегии какие-то действия выполнялись только при наступлении нового бара, нам достаточно поместить этот блок кода в такой условный оператор:

// Если наступил новый бар на H1 по текущему символу стратегии, то
if(IsNewBar(m_symbol, PERIOD_H1)) {

       // выполняем необходимые действия
   ...
}

Наличие такого кода в стратегии автоматически приведёт к регистрации отслеживания события наступления нового бара на таймфрейме H1 и символе стратегии m_symbol.

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

// Если наступил новый бар на D1 по текущему символу стратегии, то
if(IsNewBar(m_symbol, PERIOD_H1)) {
   CalcATR(); // вызываем свою функцию расчёта ATR
}

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

//+------------------------------------------------------------------+
//| "Tick" event handler function                                    |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::Tick() override {
// Если нет нового бара на M1, 
   if(!IsNewBar(m_symbol, PERIOD_M1)) return;

// Если их количество меньше допустимого
   if(m_ordersTotal < m_maxCountOfOrders) {
      // Получаем сигнал на открытие
      int signal = SignalForOpen();

      if(signal == 1 /* || m_ordersTotal < 1 */) {          // Если сигнал на покупку, то
         OpenBuyOrder();         // открываем ордер BUY_STOP
      } else if(signal == -1) {  // Если сигнал на продажу, то
         OpenSellOrder();        // открываем ордер SELL_STOP
      }
   }
}

Мы можем также ввести запрет на любую обработку в обработчике события OnTick эксперта в моменты времени тех тиков, которые не совпадают с началом нового бара по какому-либо из используемых символов или таймфреймов. Чтобы этого добиться можно внести такие изменения в код метода CVirtualAdvisor::Tick():

//+------------------------------------------------------------------+
//| Обработчик события OnTick                                        |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Определяем новый бар по всем нужным символам и таймфреймам
   bool isNewBar = UpdateNewBar();

// Если нигде нового бара нет, а мы работаем только по новым барам, то выходим
   if(!isNewBar && m_useOnlyNewBar) {
      return;
   }

// Получатель обрабатывает виртуальные позиции
   m_receiver.Tick();

// Запуск обработки в стратегиях
   CAdvisor::Tick();

// Корректировка рыночных объемов
   m_receiver.Correct();

// Сохранение состояния
   Save();

// Отрисовка интерфейса
   m_interface.Redraw();
}

В этом коде мы добавили новое свойство эксперта m_useOnlyNewBar, которое можно установить при создании объекта эксперта:

//+------------------------------------------------------------------+
//| Класс эксперта, работающего с виртуальными позициями (ордерами)  |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...
   bool              m_useOnlyNewBar;  // Обрабатывать только тики нового бара

public:
                     CVirtualAdvisor(ulong p_magic = 1, string p_name = "",
                                     bool p_useOnlyNewBar = false); // Конструктор
    ...
};


//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1,
                                 string p_name = "",
                                 bool p_useOnlyNewBar = false) :
// Инициализируем получателя статическим получателем
   m_receiver(CVirtualReceiver::Instance(p_magic)),
// Инициализируем интерфейс статическим интерфейсом
   m_interface(CVirtualInterface::Instance(p_magic)),
   m_lastSaveTime(0),
   m_useOnlyNewBar(p_useOnlyNewBar) {
   m_name = StringFormat("%s-%d%s.csv",
                         (p_name != "" ? p_name : "Expert"),
                         p_magic,
                         (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                        );
};

В принципе, для добавления возможности можно было создать новый класс эксперта, унаследовав его от CVirtualAdvisor, и добавив новое свойство и проверку наличия нового бара в него. Но можно оставить всё как есть, так как при значении по умолчанию для свойства m_useOnlyNewBar = false, всё будет работать как и без добавления этой функциональности в класс эксперта.

Если мы так расширили класс эксперта, то внутри класса торговой стратегии можно обойтись без проверки события нового минутного бара внутри метода Tick(). Достаточно один раз в конструкторе стратегии вызвать функцию IsNewBar() с текущим символом и таймфреймом M1, чтобы событие нового бара с таким символом и таймфреймом начало отслеживаться. Тогда эксперт с установленным значением свойства m_useOnlyNewBar = true просто не будет запускать обработку тика для экземпляров стратегий, если не наступил новый бар на M1:

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CSimpleVolumesStrategy::CSimpleVolumesStrategy(
   ...) :
// Список инициализации
   ... {
   CVirtualReceiver::Get(GetPointer(this), m_orders, m_maxCountOfOrders);

// Загружаем индикатор для получения тиковых объемов
   m_iVolumesHandle = iVolumes(m_symbol, m_timeframe, VOLUME_TICK);

// Устанавливаем размер массива-приемника тиковых объемов и нужную адресацию
   ArrayResize(m_volumes, m_signalPeriod);
   ArraySetAsSeries(m_volumes, true);

// Регистрируем обработчик события нового бара на минимальном таймфрейме
   IsNewBar(m_symbol, PERIOD_M1);
}


//+------------------------------------------------------------------+
//| "Tick" event handler function                                    |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::Tick() override {
// Если их количество меньше допустимого
   if(m_ordersTotal < m_maxCountOfOrders) {
      // Получаем сигнал на открытие
      int signal = SignalForOpen();

      if(signal == 1 /* || m_ordersTotal < 1 */) {          // Если сигнал на покупку, то
         OpenBuyOrder();         // открываем ордер BUY_STOP
      } else if(signal == -1) {  // Если сигнал на продажу, то
         OpenSellOrder();        // открываем ордер SELL_STOP
      }
   }
}

Сохраним эти изменения в файле SimpleVolumesStrategy.mqh в текущей папке. 


Проверка результатов

Добавим в советник BenchmarkInstancesExpert.mq5 новый входной параметр useOnlyNewBars_, в котором будем устанавливать, должен ли он обрабатывать тики, которые не совпали с началом нового бара. При инициализации советника передадим значение этого параметра в конструктор эксперта:

//+------------------------------------------------------------------+
//| Входные параметры                                                |
//+------------------------------------------------------------------+
...

input group "::: Прочие параметры"
sinput ulong  magic_          = 27183;   // - Magic
input bool    useOnlyNewBars_ = true;    // - Работать только на открытии бара

...

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   ...

// Создаем эксперта, работающего с виртуальными позициями
   expert = new CVirtualAdvisor(magic_, "SimpleVolumes_BenchmarkInstances", useOnlyNewBars_);

   ...
}

Запустим тестирование на небольшом периоде этого советника с 256 экземплярами торговых стратегий в режиме "Все тики на основе реальных тиков" сначала со значением параметра useOnlyNewBars_ = false, а затем со значением useOnlyNewBars_ = true.

В первом случае, то есть когда советники отрабатывал каждый тик, прибыль составила $296, проход завершился за время 04:15. Во втором случае, когда советник пропускал все тики, кроме тех, которые приходились на начало нового бара, прибыль составила $434, проход завершился за 00:25. То есть мы не только сократили затраты на вычисления в 10 раз, но и получили немного большую прибыль во втором случае.

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


Заключение

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

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

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

Спасибо за внимание, до новых встреч!



Прикрепленные файлы |
NewBarEvent.mqh (11.7 KB)
VirtualAdvisor.mqh (14.9 KB)
VirtualOrder.mqh (39.52 KB)
Алгоритм оптимизации на основе мозгового штурма — Brain Storm Optimization (Часть II): Многомодальность Алгоритм оптимизации на основе мозгового штурма — Brain Storm Optimization (Часть II): Многомодальность
Во второй части статьи перейдем к практической реализации алгоритма BSO, проведем тесты на тестовых функциях и сравним эффективность BSO с другими методами оптимизации.
Разработка системы репликации (Часть 33): Система ордеров (II) Разработка системы репликации (Часть 33): Система ордеров (II)
Сегодня мы продолжим разработку системы ордеров, но вы увидите, что мы будем массово использовать заново то, что уже было показано в других статьях. Тем не менее, в этой статье мы получим небольшое вознаграждение. Сначала мы разработаем систему, которую можно будет использовать вместе с реальным торговым сервером, либо с помощью демо-счета, либо реального счета. Мы будем широко использовать платформу MetaTrader 5, которая обеспечит нам всю необходимую поддержку в начале данного пути.
Как разработать агент обучения с подкреплением на MQL5 с интеграцией RestAPI (Часть 1): Как использовать RestAPI в MQL5 Как разработать агент обучения с подкреплением на MQL5 с интеграцией RestAPI (Часть 1): Как использовать RestAPI в MQL5
В этой статье мы расскажем о важности интерфейсов программирования API для взаимодействия между различными приложениями и программными системами. В ней подчеркивается роль API в упрощении взаимодействия между приложениями, позволяя им эффективно обмениваться данными и функциональными возможностями.
Алгоритм оптимизации на основе мозгового штурма — Brain Storm Optimization (Часть I): Кластеризация Алгоритм оптимизации на основе мозгового штурма — Brain Storm Optimization (Часть I): Кластеризация
В данной статье мы рассмотрим инновационный метод оптимизации, названный BSO (Brain Storm Optimization), который вдохновлен природным явлением - "мозговым штурмом". Мы также обсудим новый подход к решению многомодальных задач оптимизации, который использует метод BSO и позволяет находить несколько оптимальных решений без необходимости заранее определять количество подпопуляций. В статье мы также рассмотрим методы кластеризации K-Means и K-Means++.