
Разрабатываем мультивалютный советник (Часть 25): Подключаем новую стратегию (II)
Введение
Продолжим очередной этап работы, начало которого было положено в предыдущей статье. Напомним, что после разделения всего кода проекта на библиотечную и проектную часть, мы решили проверить, как можно перейти от используемой уже длительное время модельной торговой стратегии SimpleVolumes к какой-то другой. Что нам потребуется для этого сделать, насколько это будет легко? Само собой было необходимо написать класс новой торговой стратегии. Но дальше возникли сложности, появление которых не было таким уж очевидным.
Они были связаны именно со стремлением сделать так, чтобы библиотечная часть могла быть независимой от проектной. Если бы мы решили нарушить это нововведённое правило, то и сложности бы не возникло. Однако, в итоге был найден способ, позволяющий и сохранить разделение кода, и обеспечить подключение новой торговой стратегии. Это потребовало пусть и не очень больших по объёму, но существенных по смыслу, изменений в файлах библиотечной части проекта.
В итоге, мы смогли скомпилировать и запустить оптимизацию советника первого этапа с новой стратегий, которую назвали SimpleCandles. Дальнейшие намеченные шаги состояли в том, чтобы заставить с ней работать конвейер автоматической оптимизации. Для прошлой стратегии у нас был разработан вспомогательный советник CreateProject.mq5, который позволял сформировать в базе данных оптимизации задания для выполнения на конвейере. В параметрах советника мы могли указать, на каких торговых инструментах (символах) и таймфреймах мы хотим проводить оптимизацию, как называются советники этапов и прочую необходимую информацию. Если базы данных оптимизации прежде не существовало, то она автоматически создавалась.
Посмотрим, как теперь заставить его работать с новой торговой стратегией.
Намечаем путь
Основную работу начнём с того, что проанализируем код советника CreateProject.mq5. Нашей целью станет выявление кода, который будет одинаковым, или почти одинаковым для различных проектов. Этот код можно будет выделить в библиотечную часть, разбив его при необходимости на несколько отдельных файлов. Ту часть кода, которая будет различной для разных проектов, мы оставим в проектной части и опишем, какие изменения в неё потребуется вносить.
Но вначале, давайте исправим обнаруженную ошибку, возникающую при сохранении информации о проходах тестера в базу данных оптимизации, доработаем макросы для организации циклов и посмотрим на то, как можно добавить дополнительные параметры к разработанной ранее торговой стратегии.
Исправления в CDatabase
В последних статьях мы начали использовать для проектов оптимизации относительно небольшие интервалы тестирования. Вместо интервалов длительностью 5 и более лет, мы стали брать интервалы продолжительностью несколько месяцев. Это было связано с тем, что основной задачей у нас была проверка работы механизма работы конвейера автоматической оптимизации, и сокращение интервала позволяло сильно уменьшить время отдельного прохода тестера, а значит, и общее время оптимизации.
Для сохранения информации о проходах в базу данных оптимизации каждый агент тестирования (локальный, удалённый или облачный) направляет её в составе фрейма данных в терминал, в котором запущен процесс оптимизации. В этом терминале, после старта оптимизации, запускается дополнительный экземпляр оптимизируемого советника в специальном режиме — режиме сбора фреймов данных. Этот экземпляр запускается не в тестере, а на отдельном графике терминала. Именно он будет получать и выполнять сохранение всей информации, приходящей от агентов тестирования.
Хотя код обработчика события прихода новых фреймов данных от агентов тестирования не содержит асинхронных операций, при оптимизации стали появляться сообщения об ошибках вставки в базу данных, связанных с блокировкой базы данных другой операцией. Эта ошибка встречалась относительно нечасто. Тем не менее, несколько десятков из нескольких тысяч проходов, в итоге, не могли добавить свои результаты в базу данных оптимизации.
Похоже, что причина возникновения этих ошибок кроется в увеличении количества ситуаций, когда сразу несколько агентов тестирования одновременно заканчивают очередной проход и отправляют фрейм данных советнику в главный терминал. А этот советник пытается вставить новую запись в базу данных быстрее, чем успевает завершиться обработка предыдущей операции вставки на стороне базы данных.
Для исправления добавим отдельный обработчик этой категории ошибок. Если причина возникшей ошибки именно в блокировке базы данных или таблицы другой операцией, то надо просто повторить неудачную операцию спустя некоторое время. Если спустя какое-то количество попыток повторной вставки данных, такая же ошибка получается ещё раз, то тогда попытки следует прекратить.
Для вставки мы используем метод CDatabase::ExecuteTransaction(), поэтому внесём в него следующие изменения. Добавим к аргументам метода счётчик попыток выполнения запроса. При возникновении ошибки такого рода, делаем паузу на случайное количество миллисекунд (0 - 50) и вызываем эту же функцию с увеличенным значением счётчика попыток.
//+------------------------------------------------------------------+ //| Выполнение нескольких запросов к БД в одной транзакции | //+------------------------------------------------------------------+ bool CDatabase::ExecuteTransaction(string &queries[], int attempt = 0) { // Открываем транзакцию DatabaseTransactionBegin(s_db); s_res = true; // Отправляем все запросы на выполнение FOREACH(queries, { s_res &= DatabaseExecute(s_db, queries[i]); if(!s_res) break; }); // Если в каком-то запросе возникла ошибка, то if(!s_res) { // Отменяем транзакцию DatabaseTransactionRollback(s_db); if((_LastError == ERR_DATABASE_LOCKED || _LastError == ERR_DATABASE_BUSY) && attempt < 20) { PrintFormat(__FUNCTION__" | ERROR: ERR_DATABASE_LOCKED. Repeat Transaction in DB [%s]", s_fileName); Sleep(rand() % 50); ExecuteTransaction(queries, attempt + 1); } else { // Сообщаем о ней PrintFormat(__FUNCTION__" | ERROR: Transaction failed in DB [%s], error code=%d", s_fileName, _LastError); } } else { // Иначе - подтверждаем транзакцию DatabaseTransactionCommit(s_db); //PrintFormat(__FUNCTION__" | Transaction done successfully"); } return s_res; }
На всякий случай внесём такие же по смыслу изменения в метод выполнения SQL-запроса без транзакции CDatabase::Execute().
Еще одно небольшое изменение, которое нам пригодится в будущем, состояло в добавлении статической логической переменной к классу CDatabase. В ней будет запоминаться, что при выполнении запросов произошла ошибка:
//+------------------------------------------------------------------+ //| Класс для работы с базой данных | //+------------------------------------------------------------------+ class CDatabase { // ... static bool s_res; // Результат выполнения запросов public: static int Id(); // Хендл соединения с БД static bool Res(); // Результат выполнения запросов // ... }; bool CDatabase::s_res = true;
Сохраним сделанные изменения в файле Database/Database.mqh в папке библиотеки.
Исправления в Macros.h
Упомянем об одном изменении, которое давно напрашивалось, но до него всё никак не доходила очередь. Напомним, что для упрощения записи заголовков циклов, которые должны перебрать все значения в некотором массиве, мы сделали макрос FOREACH(A, D):
#define FOREACH(A, D) { for(int i=0, im=ArraySize(A);i<im;i++) {D;} }
Здесь A— это имя массива, а D— это тело цикла. У такой реализации был недостаток в том, что при отладке нельзя нормально отслеживать в пошаговое выполнение кода внутри тела цикла. Хотя это требовалось весьма редко, было сильно неудобно. Как-то раз, просматривая документацию, мы увидели другой способ реализации подобного макроса. Там макрос задавал только заголовок цикла, а тело было вынесено за пределы макроса. Однако, был ещё один параметр, задающий имя переменной цикла.
В нашей предыдущей реализации имя переменной цикла (индекса элемента массива) было фиксированным (i), и это нигде не вызывало проблем. Даже в том месте, где понадобился двойной цикл, можно было обойтись одинаковыми именами за счёт разных областей видимости этих индексов. Поэтому новая реализация тоже получила фиксированное имя индекса. Единственный передаваемый параметр — это имя массива для перебора в цикле:
#define FOREACH(A) for(int i=0, im=ArraySize(A);i<im;i++)
Для перехода на новый вариант пришлось внести правки во все места, где использовался этот макрос. Например:
//+------------------------------------------------------------------+ //| Обработчик события OnTick | //+------------------------------------------------------------------+ void CAdvisor::Tick(void) { // Для всех стратегий вызываем обработку OnTick //FOREACH(m_strategies, m_strategies[i].Tick();) FOREACH(m_strategies) m_strategies[i].Tick(); }
Вместе с этим макросом мы добавили ещё один, обеспечивающий создание заголовка цикла. В нём каждый элемент массива A по очереди помещается в переменную с именем E, которая должна быть объявлена заранее. Перед заголовком цикла в эту переменную помещается первый элемент массива, если он существует. В качестве переменной цикла мы будем использовать переменную с именем, складывающимся из буквы i и названия переменной E. В третьей части заголовка цикла мы выполняем инкремент переменной цикла и присваиваем переменной E значение элемента массива A с увеличенным индексом. Использование операции взятия индекса по модулю количества элементов массива позволяет избежать выхода за пределы массива на последней итерации цикла:
#define FOREACH_AS(A, E) if(ArraySize(A)) E=A[0]; \ for(int i##E=0, im=ArraySize(A);i##E<im;E=A[++i##E%im])
Сохраним сделанные изменения в файле Utils/Macros.h в папке библиотеки.
Как добавить параметр в торговую стратегию
Как и практически весь программный код, реализация торговой стратегии тоже подвержена изменениям. Если эти изменения касаются изменения состава входных параметров одиночного экземпляра торговой стратегии, то потребуется внести правки не только в класс торговой стратегии, но и ещё в некоторые места. Рассмотрим на примере, что для этого потребуется сделать.
Предположим, что мы решили добавить к торговой стратегии параметр максимального спреда. Его использование будет состоять в том, что если в момент получения сигнала на открытие позиции текущий спред будет превышать установленное в этом параметре значение, то позиция не откроется.
Для начала в советнике первого этапа мы добавим входной параметр, через который можно будет устанавливать это значение при запуске в тестере. Затем, в функции формирования строки инициализации, добавим подстановкузначения добавленного параметра в строку инициализации:
//+------------------------------------------------------------------+ //| 4. Входные параметры для стратегии | //+------------------------------------------------------------------+ sinput string symbol_ = ""; // Символ sinput ENUM_TIMEFRAMES period_ = PERIOD_CURRENT; // Таймфрейм для свечей input group "=== Параметры сигнала к открытию" input int signalSeqLen_ = 6; // Количество однонаправленных свечей input int periodATR_ = 0; // Период ATR (если 0, то TP/SL в пунктах) input group "=== Параметры отложенных ордеров" input double stopLevel_ = 25000; // Stop Loss (в доле ATR или пунктах) input double takeLevel_ = 3630; // Take Profit (в доле ATR или пунктах) input group "=== Параметры управление капиталом" input int maxCountOfOrders_ = 9; // Макс. количество одновременно отрытых ордеров input int maxSpread_ = 10; // Макс. допустимый спред (в пунктах) //+------------------------------------------------------------------+ //| 5. Функция формирования строки инициализации стратегии | //| из входных параметров | //+------------------------------------------------------------------+ string GetStrategyParams() { return StringFormat( "class CSimpleCandlesStrategy(\"%s\",%d,%d,%d,%.3f,%.3f,%d,%d)", (symbol_ == "" ? Symbol() : symbol_), period_, signalSeqLen_, periodATR_, stopLevel_, takeLevel_, maxCountOfOrders_, maxSpread_ ); }
Теперь строка инициализации содержит на один параметр больше, чем было раньше. Поэтому следующее изменение будет состоять в добавлении нового свойства класса и чтении в него значения из строки инициализации в конструкторе:
//+------------------------------------------------------------------+ //| Торговая стратегия c использованием однонаправленных свечей | //+------------------------------------------------------------------+ class CSimpleCandlesStrategy : public CVirtualStrategy { protected: // ... //--- Параметры управление капиталом int m_maxCountOfOrders; // Макс. количество одновременно отрытых позиций int m_maxSpread; // Макс. допустимый спред (в пунктах) // ... }; //+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CSimpleCandlesStrategy::CSimpleCandlesStrategy(string p_params) { // Читаем параметры из строки инициализации m_params = p_params; m_symbol = ReadString(p_params); m_timeframe = (ENUM_TIMEFRAMES) ReadLong(p_params); m_signalSeqLen = (int) ReadLong(p_params); m_periodATR = (int) ReadLong(p_params); m_stopLevel = ReadDouble(p_params); m_takeLevel = ReadDouble(p_params); m_maxCountOfOrders = (int) ReadLong(p_params); m_maxSpread = (int) ReadLong(p_params); // ... }
Всё, теперь новый параметр может использоваться, как мы пожелаем, в методах класса торговой стратегии. Исходя из его назначения, можно добавить следующий код в метод получения сигнала на открытие позиции.
//+------------------------------------------------------------------+ //| Сигнал для открытия отложенных ордеров | //+------------------------------------------------------------------+ int CSimpleCandlesStrategy::SignalForOpen() { // По-умолчанию сигнала на открытие нет int signal = 0; MqlRates rates[]; // Копируем значения котировок (свечей) в массив-приёмник. // Для проверки сигнала нам нужно m_signalSeqLen закрытых свечей и текущая свеча, // поэтому всего m_signalSeqLen + 1 int res = CopyRates(m_symbol, m_timeframe, 0, m_signalSeqLen + 1, rates); // Если скопировалось нужное количество свечей if(res == m_signalSeqLen + 1) { signal = 1; // сигнал на покупку // Перебираем все закрытые свечи for(int i = 1; i <= m_signalSeqLen; i++) { // Если встречается хоть одна свеча вверх, то отменяем сигнал if(rates[i].open < rates[i].close ) { signal = 0; break; } } if(signal == 0) { signal = -1; // иначе - сигнал на продажу // Перебираем все закрытые свечи for(int i = 1; i <= m_signalSeqLen; i++) { // Если встречается хоть одна свеча вниз, то отменяем сигнал if(rates[i].open > rates[i].close ) { signal = 0; break; } } } } // Если сигнал есть, то if(signal != 0) { // Если текущий спред больше максимально разрешённого, то if(rates[0].spread > m_maxSpread) { PrintFormat(__FUNCTION__" | IGNORE %s Signal, spread is too big (%d > %d)", (signal > 0 ? "BUY" : "SELL"), rates[0].spread, m_maxSpread); signal = 0; // Отменяем сигнал } } return signal; }
Аналогично можно добавлять другие новые параметры к торговым стратегиям или избавляться от параметров, ставших ненужными.
Анализ CreateProject.mq5
Приступим к анализу кода советника создания проектов CreateProject.mq5. В его функции инициализации мы уже сделали разделение кода на отдельные функции. Назначение каждой понятно из названия:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Подключаемся к базе данных DB::Connect(fileName_); // Создание проекта CreateProject(projectName_, projectVersion_, StringFormat("%s - %s", TimeToString(fromDate_, TIME_DATE), TimeToString(toDate_, TIME_DATE) ) ); // Создание этапов проекта CreateStages(); // Создание работ и задач CreateJobs(); // Постановка проекта в очередь на выполнение QueueProject(); // Закрываем базу данных DB::Close(); // Успешная инициализация return(INIT_SUCCEEDED); }
Но такое разделение не очень удобно, потому что выделенные функции получились довольно громоздкими и решающими довольно-таки разнородные задачи. Например, в функции CreateJobs() мы занимаемся и предварительной обработкой входных данных, и формированием шаблонов параметров для работ, и вставкой информации в базу данных, а потом ещё и выполняем похожие действия для создания задач оптимизации в базе данных. А желательно, чтобы было наоборот: функции были попроще и решали какую-то одну небольшую задачу.
Для использования новой стратегии в текущей реализации нам надо было бы поменять шаблон параметров первого этапа, а также, возможно, и количество задач с критериями оптимизации для него. Шаблон параметров первого этапа для прошлой торговой стратегии задавался в глобальной переменной paramsTemplate1:
// Шаблон параметров оптимизации на первом этапе string paramsTemplate1 = "; === Параметры сигнала к открытию\n" "signalPeriod_=212||12||40||240||Y\n" "signalDeviation_=0.1||0.1||0.1||2.0||Y\n" "signaAddlDeviation_=0.8||0.1||0.1||2.0||Y\n" "; === Параметры отложенных ордеров\n" "openDistance_=10||0||10||250||Y\n" "stopLevel_=16000||200.0||200.0||20000.0||Y\n" "takeLevel_=240||100||10||2000.0||Y\n" "ordersExpiration_=22000||1000||1000||60000||Y\n" "; === Параметры управление капиталом\n" "maxCountOfOrders_=3||3||1||30||N\n";
Он, к счастью, был одинаковым для всех работ оптимизации первого этапа. Но это может быть не всегда так. Например, в новой стратегии мы включили в состав параметров значения символа и таймфрейма, на котором должна работать стратегия. Это значит, что в разных работах оптимизации первого этапа, создаваемых для разных символов и таймфреймов, в шаблоне параметров появятся изменяемые части. Однако, чтобы задать им значения, понадобится лезть в дебри кода функции создания работ и вносить изменения в неё. Тогда её уже не получится вынести в библиотечную часть.
Кроме этого, сейчас наш советник создания проекта оптимизации создаёт проект с тремя фиксированными этапами. Мы пришли к такому простому составу этапов в процессе разработки, хотя и пробовали добавлять дополнительные этапы (см. например, часть 18 и часть 19). Дополнительные этапы не показали каких-либо существенных улучшений конечного результата, хотя это может оказаться не так для других торговых стратегий. Поэтому, если мы вынесем в библиотечную часть текущий код, то не сможем в дальнейшем, при желании, изменять состав этапов.
Так что, как ни хотелось бы обойтись малыми усилиями, лучше, всё-таки, сейчас провести серьёзную работу по рефакторингу этого кода, чем откладывать это на более позднее время. Попробуем разделить код советника создания проекта на несколько классов. Классы будут вынесены в библиотечную часть, а в проектной части мы будем их использовать для создания проектов с желаемым составом этапов и их начинкой. Одновременно это будет и заготовка на будущее для показа информации о ходе работы конвейера.
Для начала мы попробовали написать, как может выглядеть итоговый код. Эта предварительная версия так и осталась практически без существенных изменений до рабочего варианта. Только добавились конкретные составы параметров в вызовы методов. Поэтому посмотрим, как выглядит новая версия функции инициализации советника создания проекта оптимизации. Чтобы не отвлекаться на мелкие детали, аргументы методов не показаны:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Создаём объект проекта оптимизации для заданной базы данных COptimizationProject p; // Создаём новый проект в базе данных p.Create(...); // Добавляем первый этап p.AddStage(...); // Добавляем работы первого этапа p.AddJobs(...); // Добавляем задачи для работ первого этапа p.AddTasks(...); // Добавляем второй этап p.AddStage(...); // Добавляем работы второго этапа p.AddJobs(...); // Добавляем задачи для работ второго этапа p.AddTasks(...); // Добавляем третий этап p.AddStage(...); // Добавляем работу третьего этапа p.AddJobs(...); // Добавляем задачу для работы третьего этапа p.AddTasks(...); // Ставим проект в очередь на выполнение p.Queue(); // Удаляем советник ExpertRemove(); // Успешная инициализация return(INIT_SUCCEEDED); }
При такой структуре кода мы сможем легко добавлять новые этапы и гибко изменять их параметры. Но пока что мы видим только один новый класс, который нам точно понадобится — это класс проекта оптимизации COptimizationProject. Посмотрим на его код.
Класс COptimizationProject
При разработке этого класса довольно быстро выяснилось, что нам понадобятся отдельные классы для всех видов сущностей, которые мы храним в базе данных оптимизации. То есть, на очереди будут классы COptimizationStage для этапов проекта, COptimizationJob для работ этапов проекта и COptimizationTask для задач каждой работы этапа проекта.
Поскольку объекты этих классов являются, по сути, представлением записей из различных таблиц базы данных оптимизации, то состав полей класса будет повторять состав полей соответствующих таблиц. Помимо этих полей, мы добавим в эти классы и другие поля и методы, необходимые для выполнения возложенных на них задач.
Пока что, для упрощения, сделаем все свойства и методы создаваемых классов публичными. У каждого класса будет свой метод создания новой записи в базе данных оптимизации. В дальнейшем мы добавим методы изменения существующей записи и чтения записи из базы данных, так как при создании проекта он нам не понадобится.
Вместо использованных ранее шаблонов параметров тестера, сделаем отдельные функции, которые будут возвращать уже заполненные параметры по шаблону. Таким образом, шаблоны параметров переместятся внутрь этих функций. В качестве параметра эти функции будут принимать указатель на проект и смогут через него получить доступ к нужной информации проекта для подстановки в шаблон. Объявление этих функций мы вынесем в проектную часть, а в библиотечной объявим только новый тип — указатель на функцию следующего вида:
// Создание нового типа - указателя на функцию генерации строки // параметров работы оптимизации (job), принимающей в качестве // аргумента указатель на объект проекта оптимизации typedef string (*TJobsTemplateFunc)(COptimizationProject*);
Благодаря этому, мы сможем использовать в классе COptimizationProject функции генерации параметров этапов, которых пока нет, но в будущем, в проектной части, мы их обязательно должны будем добавить.
Вот как выглядит описание этого класса:
//+------------------------------------------------------------------+ //| Класс для проекта оптимизации | //+------------------------------------------------------------------+ class COptimizationProject { public: string m_fileName; // Имя базы данных // Свойства, напрямую сохраняемые в базе данных ulong id_project; // Идентификатор проекта string name; // Название string version; // Версия string description; // Описание string status; // Статус // Массивы всех этапов, работ и задач проекта COptimizationStage* m_stages[]; // Этапы проекта COptimizationJob* m_jobs[]; // Работы всех этапов проекта COptimizationTask* m_tasks[]; // Задачи всех работ этапов проекта // Свойства для текущего состояния процесса создания проекта string m_symbol; // Текущий символ string m_timeframe; // Текущий таймфрейм COptimizationStage* m_stage; // Последний созданный этап (текущий этап) COptimizationJob* m_job; // Последняя созданная работа (текущая работа) COptimizationTask* m_task; // Последняя созданная задача (текущая задача) // Методы COptimizationProject(string p_fileName); // Конструктор ~COptimizationProject(); // Дестрктор // Создание нового проекта в базе данных COptimizationProject* COptimizationProject::Create(string p_name, string p_version = "", string p_description = "", string p_status = "Done"); void Insert(); // Вставка записи в базу данных void Update(); // Обновление записи в базе данных // Добавление нового этапа в базу данных COptimizationProject* AddStage(COptimizationStage* parentStage, string stageName, string stageExpertName, string stageSymbol, string stageTimeframe, int stageOptimization, int stageModel, datetime stageFromDate, datetime stageToDate, int stageForwardMode, datetime stageForwardDate, int stageDeposit = 10000, string stageCurrency = "USD", int stageProfitInPips = 0, int stageLeverage = 200, int stageExecutionMode = 0, int stageOptimizationCriterion = 7, string stageStatus = "Done"); // Добавление новых работ в базу данных для заданных символов и таймфреймов COptimizationProject* AddJobs(string p_symbols, string p_timeframes, TJobsTemplateFunc p_templateFunc); COptimizationProject* AddJobs(string &p_symbols[], string &p_timeframes[], TJobsTemplateFunc p_templateFunc); // Добавление новых задач в базу данных для заданных критериев оптимизации COptimizationProject* AddTasks(string p_criterions); COptimizationProject* AddTasks(string &p_criterions[]); void Queue(); // Постановка проекта в очередь на выполненеие // Преобразование строкового названия в таймфрейм static ENUM_TIMEFRAMES StringToTimeframe(string s); };
В начале идут свойства, которые напрямую сохраняются в базе данных оптимизации в таблице projects. Затем идут массивы всех этапов, работ и задач проекта, а далее — свойства для текущего состояния процесса создания проекта.
Поскольку сейчас у этого класса только одна задача — создать проект в базе данных оптимизации, то в конструкторе мы сразу подключаемся к нужной базе данных и открываем транзакцию. Завершение этой транзакции будет происходить в деструкторе. Тут-то нам и пригодиться статическое поле класса CDatabase::s_res, по значению которого можно понять, произошла ли какая-нибудь ошибка при операциях вставки записей в базу данных оптимизации при создании проекта. Если ошибок не было, то транзакция подтверждается, а иначе — отменяется. Также в деструкторе освобождается память под созданные динамические объекты.
//+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ COptimizationProject::COptimizationProject(string p_fileName) : m_fileName(p_fileName), id_project(0) { // Подключаемся к базе данных if (DB::Connect(m_fileName)) { // Начинаем транзакцию DatabaseTransactionBegin(DB::Id()); } } //+------------------------------------------------------------------+ //| Деструктор | //+------------------------------------------------------------------+ COptimizationProject::~COptimizationProject() { // Если не возникло ошибок, то if(DB::Res()) { // Подтверждаем транзакцию DatabaseTransactionCommit(DB::Id()); } else { // Иначе отменяем транзакцию DatabaseTransactionRollback(DB::Id()); } // Закрываем соединение с базой данных DB::Close(); // Удаляем созданные объекты задач, работ и этапов FOREACH(m_tasks) { delete m_tasks[i]; } FOREACH(m_jobs) { delete m_jobs[i]; } FOREACH(m_stages) { delete m_stages[i]; } }
Методы добавления работ и задач объявлены в двух вариантах. В первом варианте списки символов, таймфреймов и критериев им передаются в строковых параметрах, разделённые запятыми. Внутри метода эти строки преобразуются в массивы значений и подставляются в качестве аргументов при вызове второго варианта метода, который принимает как раз массивы.
Вот как выглядят методы добавления работ:
//+------------------------------------------------------------------+ //| Добавление новых работ в базу данных для заданных | //| символов и таймфреймов в строках | //+------------------------------------------------------------------+ COptimizationProject* COptimizationProject::AddJobs(string p_symbols, string p_timeframes, TJobsTemplateFunc p_templateFunc) { // Массив символов для стратегий string symbols[]; StringReplace(p_symbols, ";", ","); StringSplit(p_symbols, ',', symbols); // Массив таймфреймов для стратегий string timeframes[]; StringReplace(p_timeframes, ";", ","); StringSplit(p_timeframes, ',', timeframes); return AddJobs(symbols, timeframes, p_templateFunc); } //+------------------------------------------------------------------+ //| Добавление новых работ в базу данных для заданных | //| символов и таймфреймов в массивах | //+------------------------------------------------------------------+ COptimizationProject* COptimizationProject::AddJobs(string &p_symbols[], string &p_timeframes[], TJobsTemplateFunc p_templateFunc) { // Для каждого символа FOREACH_AS(p_symbols, m_symbol) { // Для каждого таймфрейма FOREACH_AS(p_timeframes, m_timeframe) { // Получаем параметры для работы для данного символа и таймфрейма string params = p_templateFunc(&this); // Создаём новый объект работы m_job = new COptimizationJob(0, m_stage, m_symbol, m_timeframe, params); // Вставляем его в базу данных оптимизации m_job.Insert(); // Добавляем его в массив всех работ APPEND(m_jobs, m_job); // Добавляем его в массив работ текущего этапа APPEND(m_stage.jobs, m_job); } } return &this; }
Третьим аргументом в них передаётся указатель на функцию создания параметров оптимизации советников этапов.
Класс COptimizationStage
В описании этого класса много свойств, по сравнению с другими классами, но это обусловлено лишь тем, что в базе данных оптимизации в таблице этапов stages много полей. Для каждого из них есть соответствующее свойство в этом классе. Также обратите внимание, что в конструктор этапа передаётся указатель на объект проекта, в состав которого входит этот этап, и указатель на объект предыдущего этапа. Для первого этапа предыдущего нет, поэтому для него в этом параметре будем передавать значение NULL.
//+------------------------------------------------------------------+ //| Класс для этапа оптимизации | //+------------------------------------------------------------------+ class COptimizationStage { public: ulong id_stage; ulong id_project; ulong id_parent_stage; string name; string expert; string symbol; string period; int optimization; int model; datetime from_date; datetime to_date; int forward_mode; datetime forward_date; int deposit; string currency; int profit_in_pips; int leverage; int execution_mode; int optimization_criterion; string status; COptimizationProject* project; COptimizationStage* parent_stage; COptimizationJob* jobs[]; COptimizationStage(ulong p_idStage, COptimizationProject* p_project, COptimizationStage* parentStage, string p_name, string p_expertName, string p_symbol = "GBPUSD", string p_timeframe = "H1", int p_optimization = 0, int p_model = 0, datetime p_fromDate = 0, datetime p_toDate = 0, int p_forwardMode = 0, datetime p_forwardDate = 0, int p_deposit = 10000, string p_currency = "USD", int p_profitInPips = 0, int p_leverage = 200, int p_executionMode = 0, int p_optimizationCriterion = 7, string p_status = "Done") : id_stage(p_idStage), project(p_project), id_project(!!p_project ? p_project.id_project : 0), parent_stage(parentStage), id_parent_stage(!!parentStage ? parentStage.id_stage : 0), name(p_name), expert(p_expertName), symbol(p_symbol), period(p_timeframe), optimization(p_optimization), model(p_model), from_date(p_fromDate), to_date(p_toDate), forward_mode(p_forwardMode), forward_date(p_forwardDate), deposit(p_deposit), currency(p_currency), profit_in_pips(p_profitInPips), leverage(p_leverage), execution_mode(p_executionMode), optimization_criterion(p_optimizationCriterion), status(p_status) {} // Создание этапа в базе данных void Insert(); }; //+------------------------------------------------------------------+ //| Создание этапа в базе данных | //+------------------------------------------------------------------+ void COptimizationStage::Insert() { string query = StringFormat("INSERT INTO stages VALUES(" "%s," // id_stage "%I64u," // id_project "%s," // id_parent_stage "'%s'," // name "'%s'," // expert "'%s'," // symbol "'%s'," // period "%d," // optimization "%d," // model "'%s'," // from_date "'%s'," // to_date "%d," // forward_mode "%s," // forward_date "%d," // deposit "'%s'," // currency "%d," // profit_in_pips "%d," // leverage "%d," // execution_mode "%d," // optimization_criterion "'%s'" // status ");", (id_stage == 0 ? "NULL" : (string) id_stage), // id_stage id_project, // id_project (id_parent_stage == 0 ? "NULL" : (string) id_parent_stage), // id_parent_stage name, // name expert, // expert symbol, // symbol period, // period optimization, // optimization model, // model TimeToString(from_date, TIME_DATE), // from_date TimeToString(to_date, TIME_DATE), // to_date forward_mode, // forward_mode (forward_mode == 4 ? "'" + TimeToString(forward_date, TIME_DATE) + "'" : "NULL"), // forward_date deposit, // deposit currency, // currency profit_in_pips, // profit_in_pips leverage, // leverage execution_mode, // execution_mode optimization_criterion, // optimization_criterion status // status ); PrintFormat(__FUNCTION__" | %s", query); id_stage = DB::Insert(query); }
Но состав действий, выполняемых в конструкторе и методе вставки новой записи в таблицу stages, очень прост: запоминаем преданные значения аргументов в свойствах объекта и используем их для формирования SQL-запроса вставки записи в нужную таблицу базы данных оптимизации.
Класс COptimizationJob
Этот класс по структуре идентичен классу COptimizationStage. Конструктор запоминает параметры, а метод Insert() вставляет новую строку в таблицу работ jobs в базе данных оптимизации. Также каждому объекту работы при создании передаётся указатель на объект этапа, в состав которого будет входить данный объект работы.
//+------------------------------------------------------------------+ //| Класс для работы оптимизации | //+------------------------------------------------------------------+ class COptimizationJob { public: ulong id_job; // ID работы ulong id_stage; // ID этапа string symbol; // Символ string timeframe; // Таймфрейм string params; // Параметры работы для оптимизатора string status; // Статус COptimizationStage* stage; // Этап, к которому относится данная работа COptimizationTask* tasks[]; // Массив задач, относящихся к данной работе // Конструктор COptimizationJob(ulong p_jobId, COptimizationStage* p_stage, string p_symbol, string p_timeframe, string p_params, string p_status = "Done"); // Создание работы в базе данных void Insert(); }; //+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ COptimizationJob::COptimizationJob(ulong p_jobId, COptimizationStage* p_stage, string p_symbol, string p_timeframe, string p_params, string p_status = "Done") : id_job(p_jobId), stage(p_stage), id_stage(!!p_stage ? p_stage.id_stage : 0), symbol(p_symbol), timeframe(p_timeframe), params(p_params), status(p_status) {} //+------------------------------------------------------------------+ //| Создание работы в базе данных | //+------------------------------------------------------------------+ void COptimizationJob::Insert() { // Запрос на создание работы второго этапа для данного символа и таймфрейма string query = StringFormat("INSERT INTO jobs " " VALUES (NULL,%I64u,'%s','%s','%s','%s');", id_stage, symbol, timeframe, params, status); id_job = DB::Insert(query); PrintFormat(__FUNCTION__" | %s -> %I64u", query, id_job); }
Таким же образом построен и последний оставшийся класс COptimizationTask, поэтому не будем приводить здесь его код.
Переписываем CreateProject.mq5
Вернёмся к файлу CreateProject.mq5 и посмотрим на то, какие основные параметры там есть. Этот файл располагается в проектной части, поэтому для каждого отдельного проекта мы можем в нём указать нужные значения параметров по умолчанию, чтобы не изменять их при запуске.
Прежде всего, мы указываем название базы данных оптимизации:
input string fileName_ = "article.17328.db.sqlite"; // - Файл базы данных оптимизации
В следующей группе параметров мы указываем через запятую на каких символах и таймфреймах будет проходить оптимизация советника первого и второго этапов:
input string symbols_ = "GBPUSD,EURUSD,EURGBP"; // - Символы input string timeframes_ = "H1,M30"; // - Таймфреймы
При таком выборе будет создано шесть работ для каждой из возможных комбинаций из трёх символов и двух таймфреймов.
Далее идёт выбор интервала, на котором будет проходить оптимизация:
input group "::: Параметры проекта - Интервал оптимизации" input datetime fromDate_ = D'2022-09-01'; // - Дата начала input datetime toDate_ = D'2023-01-01'; // - Дата окончания
В группе параметров для счёта мы выбираем основной символ, который будет использоваться на третьем этапе, когда в тестере будет работать советник уже с несколькими символами. Его выбор становится важным, если среди символов будут такие, по которым торговля продолжается и в выходные дни (например, криптовалюты). В этом случае, в качестве основного надо выбрать именно такой, так как в противном случае, при проходе тестер не будет генерировать тики во все выходные дни.
input group "::: Параметры проекта - Счёт" input string mainSymbol_ = "GBPUSD"; // - Основной символ input int deposit_ = 10000; // - Начальный депозит
В группе параметров первого этапа указывается имя советника первого этапа, но его можно всегда использовать одинаковое. Далее мы указываем критерии оптимизации, которые будут использоваться для каждой работы первого этапа. Это просто числа, разделённые запятыми. Значение 6 соответствует пользовательскому критерию оптимизации.
input group "::: Этап 1. Поиск" input string stage1ExpertName_ = "Stage1.ex5"; // - Советник этапа input string stage1Criterions_ = "6,6,6"; // - Критерии оптимизации для задач
В данном случае, мы указали три раза пользовательский критерий, поэтому каждая работа будет содержать три задачи оптимизации с указанным критерием.
В группе параметров второго этапа мы добавили возможность указать все значения параметров советника второго этапа, а не только имя и количество стратегий в группе. Эти параметры влияют на отбор проходов первого этапа, параметры которых будут участвовать подборе групп на втором этапе.
input group "::: Этап 2. Группировка" input string stage2ExpertName_ = "Stage2.ex5"; // - Советник этапа input string stage2Criterion_ = "6"; // - Критерий оптимизации для задач //input bool stage2UseClusters_= false; // - Использовать кластеризацию? input double stage2MinCustomOntester_ = 500; // - Мин. значение норм. прибыли input uint stage2MinTrades_ = 20; // - Мин. кол-во сделок input double stage2MinSharpeRatio_ = 0.7; // - Мин. коэфф. Шарпа input uint stage2Count_ = 8; // - Кол-во стратегий в группе
Так, например, при значении параметра stage2MinTrades_ =20 в группу смогут попасть только такие одиночные экземпляры торговых стратегий, которые на первом этапе совершили, как минимум, 20 сделок. Параметр stage2UseClusters_ пока что закомментирован, так как мы не используем сейчас кластеризацию результатов второго этапа. Поэтому на его место должно подставляться значение false.
В группе параметров третьего этапа мы тоже кое-что добавили. Помимо имени советника третьего этапа (его также можно не менять при смене проекта), появились два параметра, которые управляют формированием названия базы данных итогового советника. В самом итоговом советнике это название формируется в функции CVirtualAdvisor::FileName() по такому шаблону:
<Название проекта>-<Magic>.test.db.sqlite // Для запуска в тестере <Название проекта>-<Magic>.db.sqlite // Для запуска на торговом счёте
Поэтому в советнике третьего этапа используется такой же шаблон. Для подстановки на место <Название проекта> используется параметр projectName_, а на место <Magic> —stage3Magic_. Параметр stage3Tester_ отвечает за добавление суффикса ".test".
input group "::: Этап 3. Итог" input string stage3ExpertName_ = "Stage3.ex5"; // - Советник этапа input ulong stage3Magic_ = 27183; // - Magic input bool stage3Tester_ = true; // - Для тестера?
В принципе, можно было бы сделать один параметр, в котором указывалось бы просто полное имя базы данных итогового советника. После завершения третьего этапа, полученный файл этой базы данных можно спокойно переименовать как угодно перед дальнейшим использованием.
Теперь нам осталось создать функции генерации параметров советников этапов по заданным шаблонам. Так как мы используем три этапа, то понадобится три функции.
Для первого этапа функция будет выглядеть так:
// Шаблон параметров оптимизации на первом этапе string paramsTemplate1(COptimizationProject *p) { string params = StringFormat( "symbol_=%s\n" "period_=%d\n" "; === Параметры сигнала к открытию\n" "signalSeqLen_=4||2||1||8||Y\n" "periodATR_=21||7||2||48||Y\n" "; === Параметры отложенных ордеров\n" "stopLevel_=2.34||0.01||0.01||5.0||Y\n" "takeLevel_=4.55||0.01||0.01||5.0||Y\n" "; === Параметры управление капиталом\n" "maxCountOfOrders_=15||1||1||30||Y\n", p.m_symbol, p.StringToTimeframe(p.m_timeframe)); return params; }
Её основу составляет скопированные из тестера стратегий параметры оптимизации советника первого этапа с установленными желаемыми диапазонами перебора отдельных входных параметров. В эту строку подставляются значения символа и таймфрейма, для которых в момент вызова этой функции в проекте создаётся объект работы. Если, например, для какого-то таймфрейма надо будет использовать другие диапазоны перебираемых входных параметров, то эту логику можно реализовать именно в этой функции.
При переходе к другому проекту, с другой торговой стратегией, эта функция должна быть заменена на другую, написанную под новую торговую стратегию и её набор входных параметров.
Для второго и третьего этапа мы тоже написали реализацию этих функций в файле CreateProject.mq5, однако, при переходе к другому проекту, их менять, скорее всего, не придётся. Но не будем их сразу выносить в библиотечную часть, пусть пока побудут здесь:
// Шаблон параметров оптимизации на втором этапе string paramsTemplate2(COptimizationProject *p) { // Находим идентификатор родительской работы для текущей работы // по совпадению символа и таймфрейма на текущем и родительском этапе int i; SEARCH(p.m_stage.parent_stage.jobs, (p.m_stage.parent_stage.jobs[i].symbol == p.m_symbol && p.m_stage.parent_stage.jobs[i].timeframe == p.m_timeframe), i); ulong parentJobId = p.m_stage.parent_stage.jobs[i].id_job; string params = StringFormat( "idParentJob_=%I64u\n" "useClusters_=%s\n" "minCustomOntester_=%f\n" "minTrades_=%u\n" "minSharpeRatio_=%.2f\n" "count_=%u\n", parentJobId, (string) false, //(string) stage2UseClusters_, stage2MinCustomOntester_, stage2MinTrades_, stage2MinSharpeRatio_, stage2Count_ ); return params; } // Шаблон параметров оптимизации на третьем этапе string paramsTemplate3(COptimizationProject *p) { string params = StringFormat( "groupName_=%s\n" "advFileName_=%s\n" "passes_=\n", StringFormat("%s_v.%s_%s", p.name, p.version, TimeToString(toDate_, TIME_DATE)), StringFormat("%s-%I64u%s.db.sqlite", p.name, stage3Magic_, (stage3Tester_ ? ".test" : ""))); return params; }
Далее идёт уже код функции инициализации, которая выполняет всю работу и перед завершением удаляет этот советник с графика. Покажем его теперь уже с параметрами вызываемых функций:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Создаём объект проекта оптимизации для заданной базы данных COptimizationProject p(fileName_); // Создаём новый проект в базе данных p.Create(projectName_, projectVersion_, StringFormat("%s - %s", TimeToString(fromDate_, TIME_DATE), TimeToString(toDate_, TIME_DATE))); // Добавляем первый этап p.AddStage(NULL, "First", stage1ExpertName_, mainSymbol_, "H1", 2, 2, fromDate_, toDate_, 0, 0, deposit_); // Добавляем работы первого этапа p.AddJobs(symbols_, timeframes_, paramsTemplate1); // Добавляем задачи для работ первого этапа p.AddTasks(stage1Criterions_); // Добавляем второй этап p.AddStage(p.m_stages[0], "Second", stage2ExpertName_, mainSymbol_, "H1", 2, 2, fromDate_, toDate_, 0, 0, deposit_); // Добавляем работы второго этапа p.AddJobs(symbols_, timeframes_, paramsTemplate2); // Добавляем задачи для работ второго этапа p.AddTasks(stage2Criterion_); // Добавляем третий этап p.AddStage(p.m_stages[1], "Save to library", stage3ExpertName_, mainSymbol_, "H1", 0, 2, fromDate_, toDate_, 0, 0, deposit_); // Добавляем работу третьего этапа p.AddJobs(mainSymbol_, "H1", paramsTemplate3); // Добавляем задачу для работы третьего этапа p.AddTasks("0"); // Ставим проект в очередь на выполнение p.Queue(); // Удаляем советник ExpertRemove(); // Успешная инициализация return(INIT_SUCCEEDED); }
Эту часть кода тоже можно будет не менять при переходе к другому проекту, если мы не захотим изменить состав этапов конвейера автоматической оптимизации. Со временем, мы его тоже улучшим. Например, сейчас в коде присутствуют числовые константы, которые для лучшей читабельности следует заменить на именованные константы. Если окажется, что этот код действительно не нуждается в изменениях, то и вовсе перенесём его в библиотечную часть.
Итак, советник для создания проектов оптимизации в базе данных готов. Создадим теперь советники этапов.
Советники этапов
Советник первого этапа Stage1.mq5 мы уже сделали в прошлой части, поэтому сейчас мы внесли в него изменения, связанные только с добавлением нового параметра maxSpread_ в торговую стратегию. Эти изменения уже были рассмотрены выше.
// 1. Определяем константу с именем советника #define __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME) // 2. Подключаем нужную стратегию #include "Strategies/SimpleCandlesStrategy.mqh"; // 3. Подключаем общую часть советника первого этапа из библиотеки Advisor #include <antekov/Advisor/Experts/Stage1.mqh> //+------------------------------------------------------------------+ //| 4. Входные параметры для стратегии | //+------------------------------------------------------------------+ sinput string symbol_ = ""; // Символ sinput ENUM_TIMEFRAMES period_ = PERIOD_CURRENT; // Таймфрейм для свечей input group "=== Параметры сигнала к открытию" input int signalSeqLen_ = 6; // Количество однонаправленных свечей input int periodATR_ = 0; // Период ATR (если 0, то TP/SL в пунктах) input group "=== Параметры отложенных ордеров" input double stopLevel_ = 25000; // Stop Loss (в доле ATR или пунктах) input double takeLevel_ = 3630; // Take Profit (в доле ATR или пунктах) input group "=== Параметры управление капиталом" input int maxCountOfOrders_ = 9; // Макс. количество одновременно отрытых ордеров input int maxSpread_ = 10; // Макс. допустимый спред (в пунктах) //+------------------------------------------------------------------+ //| 5. Функция формирования строки инициализации стратегии | //| из входных параметров | //+------------------------------------------------------------------+ string GetStrategyParams() { return StringFormat( "class CSimpleCandlesStrategy(\"%s\",%d,%d,%d,%.3f,%.3f,%d,%d)", (symbol_ == "" ? Symbol() : symbol_), period_, signalSeqLen_, periodATR_, stopLevel_, takeLevel_, maxCountOfOrders_, maxSpread_ ); }
В советнике второго и третьего этапа нам достаточно всего лишь определить константу __NAME__ с уникальным именем советника и подключить файл или файлы используемых торговых стратегий. Остальной код будет браться из подключаемого библиотечного файла соответствующего этапа. Вот как может выглядеть код советника второго этапа Stage2.mq5:
// 1. Определяем константу с именем советника #define __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME) // 2. Подключаем нужную стратегию #include "Strategies/SimpleCandlesStrategy.mqh"; #include <antekov/Advisor/Experts/Stage2.mqh>
и третьего этапа Stage3.mq5:
// 1. Определяем константу с именем советника #define __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME) // 2. Подключаем нужную стратегию #include "Strategies/SimpleCandlesStrategy.mqh"; #include <antekov/Advisor/Experts/Stage3.mqh>
Итоговый советник
В итоговом советнике нам надо добавить только подключение используемой стратегии. Константу __NAME__ объявлять здесь не следует, так как в этом случае и она, и функция формирования строки инициализации, будут объявлены в подключаемом файле из библиотечной части. В приведённом ниже коде мы показали в комментариях, какими в этом случае будут имя советника и функция формирования строки инициализации:
// 1. Определяем константу с именем советника //#define __NAME__ MQLInfoString(MQL_PROGRAM_NAME) // 2. Подключаем нужную стратегию #include "Strategies/SimpleCandlesStrategy.mqh"; #include <antekov/Advisor/Experts/Expert.mqh> //+------------------------------------------------------------------+ //| Функция формирования строки инициализации стратегии | //| из входных параметров по умолчанию (если не было задано имя). | //| Импортирует строку инициализации из базы данных советника | //| по идентификатору группы стратегий | //+------------------------------------------------------------------+ //string GetStrategyParams() { //// Берём строку инициализации из новой библиотеки для выбранной группы //// (из базы данных эксперта) // string strategiesParams = CVirtualAdvisor::Import( // CVirtualAdvisor::FileName(__NAME__, magic_), // groupId_ // ); // //// Если группа стратегий из библиотеки не задана, то прерываем работу // if(strategiesParams == NULL && useAutoUpdate_) { // strategiesParams = ""; // } // // return strategiesParams; //}
Если же мы вдруг захотим что-то из этого изменить, то достаточно снять комментарии с данного кода и внести в него необходимые правки.
Таким образом, в проектной части у нас будут находиться следующие файлы:
Скомпилируем все файлы проектной части, чтобы для каждого файла с расширением mq5 создался файл с расширением ex5.
Собираем всё вместе
Шаг 1. Создание проекта
Перетянем советник CreateProject.ex5 на любой график в терминале (этот советник не надо запускать в тестере!). В исходном коде этого советника мы уже постарались указать актуальные значения для всех входных параметров, поэтому в появившемся диалоге можно просто нажать OK.
Рис. 1. Запуск советника создания проекта в базе данных оптимизации
В результате, в общей папке терминалов у нас появится файл article.17328.db.sqlite с базой данных оптимизации.
Шаг 2. Запуск оптимизации
Перетянем на любой график советник Optimization.ex5 (этот советник тоже не надо запускать в тестере!). В открывшемся диалоге разрешим использование DLL, и проверим на вкладке параметров, что у нас указано правильное имя базы данных оптимизации:
Рис. 2. Запуск советника автоматической оптимизации
Если всё в порядке, то мы должны увидеть примерно такую картину: в тестере запустится оптимизация советника первого этапа на первой паре символ-таймфрейм, а на графике с запущенным советником Optimization.ex5 будет написано "Total tasks in queue: ..., Current Task ID: ...".
Рис. 3. Работа советника автоматической оптимизации.
Далее следует подождать некоторое время, пока не завершаться все задачи оптимизации. Это время может оказаться весьма значительным, если интервал тестирования будет длинным, а количество символов и таймфреймов — большим. С текущими параметрами по умолчанию на 33 агентах весь процесс занял около четырёх часов.
На последнем этапе конвейера оптимизация уже не выполняется, а запускается одиночный проход советника третьего этапа. В результате создаётся файл с базой данных итогового советника. Поскольку мы выбрали при создании проекта имя проекта "SimpleCandles", магический номер равный 27183, и значение входного параметра stage3Tester_=true, то в общей папке терминалов будет создан файл с именем SimpleCandles-27183.test.db.sqlite.
Шаг 3. Запуск итогового советника в тестере
Попробуем запустить итоговый советник в тестере. Поскольку сейчас его код полностью берётся из библиотечной части, то значения параметров по умолчанию определены там же. Поэтому когда мы запустим в тестере советник SimpleCandles.ex5 не меняя значений входных параметров, он будет использовать последнюю добавленную группу стратегий (groupId_= 0) с включенным автоматическим обновлением (useAutoUpdate_= true) из файла базы данных с именем SimpleCandles-27183.test.db.sqlite (имя файла советника SimpleCandles, плюс магический номер по умолчанию magic_= 27183 и плюс суффикс ".test" из-за запуска в тестере).
К сожалению, мы пока не сделали ещё каких-либо специальных инструментов, позволяющих посмотреть существующие идентификаторы групп стратегий в базе данных итогового советника. Можно только открыть саму базу данных в любом редакторе SQLite в и посмотреть их в таблице strategy_groups.
Однако, если был создан только один проект оптимизации и запущен один раз, то в базе данных итогового советника появится только одна группа стратегий с идентификатором 1. Поэтому, с точки зрения выбора группы, нет разницы, укажем ли мы сейчас конкретный идентификатор groupId_= 1 или оставим groupId_= 0. В любом случае, будет загружаться единственная существующая группа. Если же мы будем запускать этот же проект повторно (это можно сделать через изменение статуса проекта напрямую в базе данных) или сделаем ещё один такой же и будем запускать его, то у нас в базе данных итогового советника будут появляться новые группы стратегий. Тогда уже, при разных значениях параметра groupId_, будут использоваться разные группы.
Параметр включения автоматического обновления (useAutoUpdate_= true) тоже требует нашего внимания. Несмотря на наличие только одной группы, этот параметр влияет на работу итогового советника. Проявляется это в том, что при включенном автоматическом обновлении могут загружаться для работы только те группы стратегий, дата появления которых меньше текущей моделируемой даты.
Это означает, что если мы запустим итоговый советник на том же интервале, который использовали для оптимизации (2022.09.01 - 2023.01.01), то наша единственная группа стратегий не будет загружена, так как у неё указана дата формирования 2023.01.01. Поэтому нам надо либо выключить автоматическое обновление (useAutoUpdate_= false) и указать конкретный идентификатор используемой группы торговых стратегий (groupId_= 1) во входных параметрах при запуске итогового советника, либо выбрать другой интервал, расположенный после даты окончания интервала оптимизации.
В общем, пока мы ещё окончательно не выбрали, какие стратегии будут использоваться в итоговом советнике, и не задались целью проверить их на целесообразность периодической переоптимизации, этот параметр можно установить равным false и указывать конкретный идентификатор используемой группы торговых стратегий.
Последний набор важных параметров отвечает за то, какое имя базы данных будет использовать итоговый советник. В его настройках по умолчанию магический номер совпадает с тем, который мы указывали в настройках при создании проекта. Имя файла итогового советника мы тоже сделали совпадающим с названием проекта. И значение параметра stage3Tester_ при создании проекта было равно true, поэтому имя файла созданной базы данных итогового советника будет SimpleCandles-27183.test.db.sqlite. Оно полностью совпадает с тем, которое будет использовать итоговый советник SimpleCandles.ex5.
Посмотрим на результаты запуска итогового советника на интервале оптимизации:
Рис. 4. Работа советника автоматической оптимизации на интервале 2022.09.01 - 2023.01.01
Если запустить его на каком-то другом временном интервале, то результаты, скорее всего, будут не такие красивые:
Рис. 5. Работа советника автоматической оптимизации на интервале 2023.01.01 - 2023.02.01
Мы взяли для примера интервал в один месяц сразу после интервала оптимизации. Действительно, просадка немного превысила ожидаемое значение 10%, а нормированная прибыль уменьшилась примерно в пять раз. Можно ли снова провести оптимизацию на последних трёх месяцах и получить похожую картину поведения советника в течение следующего месяца? Этот вопрос остаётся пока открытым.
Шаг 4. Запуск итогового советника на торговом счёте
Для запуска итогового советника на торговом счёте нам понадобится подкорректировать имя полученного файла с базой данных. Из него надо удалить суффикс ".test". То есть, просто переименуем или скопируем SimpleCandles-27183.test.db.sqlite в SimpleCandles-27183.db.sqlite. Его расположение остаётся прежним — в общей папке терминалов.
Перетаскиваем итоговый советник SimpleCandles.ex5 на любой график терминала. Во входных параметрах можно всё оставить со значениями по умолчанию, так как нас вполне устраивает загрузка последней группы стратегий, а текущая дата будет заведомо больше, чем дата создания этой группы.
Рис. 6. Входные параметры по умолчанию для итогового советника
Пока шла работа над текстом статьи, такой итоговый советник успел поработать на демо-счёте примерно неделю и показал такие результаты:
Рис. 7. Результаты работы итогового советника на торговом счёте
Неделя для советника выдалась довольно неплохой. При просадке в 1.27% прибыль составила около 2%. Пару раз советник перезапускался из-за перезагрузки компьютера, но успешно восстанавливал информацию об открытых виртуальных позициях и продолжал работу.
Заключение
Посмотрим, что у нас получилось. Мы наконец-то собрали результаты довольно длительного процесса разработки во что-то, напоминающее целостную систему. Полученный инструмент по организации автоматической оптимизации и тестирования торговых стратегий позволяет довольно сильно улучшить результаты тестирования даже простых торговых стратегий за счёт диверсификации по разным торговым инструментам.
Он также позволяет очень сильно сократить количество операций, требующих ручного вмешательства для достижения тех же целей. Теперь нет необходимости отслеживать момент окончания очередного процесса оптимизации для запуска следующего, не надо думать о том, как сохранять промежуточные результаты оптимизаций и как затем встраивать их в торговый советник. Можно сосредоточиться непосредственно на разработке логики работы торговых стратегий.
Конечно, можно ещё много сделать в плане улучшения и повышения удобства этого инструмента. В отдалённых планах продолжает оставаться мысль о полноценном веб-интерфейсе, управляющем не только созданием, запуском и отслеживанием состояния запущенных проектов оптимизации, но и работой советников, запущенных в различных терминалах, просмотром их статистики. Это очень объёмная задача, но, оглядываясь назад, можно сказать то же самое и про ту задачу, которая сегодня уже получила более-менее законченное решение.
Спасибо за внимание, до встречи!
Важное предупреждение
Все результаты, изложенные в этой статье и всех предшествующих статьях цикла, основываются только на данных тестирования на истории и не являются гарантией получения хоть какой-то прибыли в будущем. Работа в рамках данного проекта носит исследовательский характер. Все опубликованные результаты могут быть использованы всеми желающими на свой страх и риск.
Содержание архива
# | Имя | Версия | Описание | Последние изменения |
---|---|---|---|---|
MQL5/Experts/Article.17328 | Рабочая папка проекта | |||
1 | CreateProject.mq5 | 1.02 | Советник-скрипт создания проекта с этапами, работами и задачами оптимизации. | Часть 25 |
2 | Optimization.mq5 | 1.00 | Советник для автоматической оптимизации проектов | Часть 23 |
3 | SimpleCandles.mq5 | 1.01 | Итоговый советник для параллельной работы нескольких групп модельных стратегий. Параметры будут браться из встроенной библиотеки групп. | Часть 25 |
4 | Stage1.mq5 | 1.02 | Советник оптимизации одиночного экземпляра торговой стратегии (Этап 1) | Часть 25 |
5 | Stage2.mq5 | 1.01 | Советник оптимизации группы экземпляров торговых стратегий (Этап 2) | Часть 25 |
6 | Stage3.mq5 | 1.01 | Советник, сохраняющий сформированную нормированную группу стратегий в базу данных эксперта с заданным именем. | Часть 25 |
MQL5/Experts/Article.17328/Strategies | Папка стратегий проекта | |||
7 | SimpleCandlesStrategy.mqh | 1.01 | Класс торговой стратегии SimpleCandles | Часть 25 |
MQL5/Include/antekov/Advisor/Base | Базовые классы, от которых наследуются другие классы проекта | |||
8 | Advisor.mqh | 1.04 | Базовый класс эксперта | Часть 10 |
9 | Factorable.mqh | 1.05 | Базовый класс объектов, создаваемых из строки | Часть 24 |
10 | FactorableCreator.mqh | 1.00 | Часть 24 | |
11 | Interface.mqh | 1.01 | Базовый класс визуализации различных объектов | Часть 4 |
12 | Receiver.mqh | 1.04 | Базовый класс перевода открытых объемов в рыночные позиции | Часть 12 |
13 | Strategy.mqh | 1.04 | Базовый класс торговой стратегии | Часть 10 |
MQL5/Include/antekov/Advisor/Database | Файлы для работы со всеми типами баз данных, используемых советниками проекта | |||
14 | Database.mqh | 1.12 | Класс для работы с базой данных | Часть 25 |
15 | db.adv.schema.sql | 1.00 | Схема базы данных итогового советника | Часть 22 |
16 | db.cut.schema.sql | 1.00 | Схема урезанной базы данных оптимизации | Часть 22 |
17 | db.opt.schema.sql | 1.05 | Схема базы данных оптимизации | Часть 22 |
18 | Storage.mqh | 1.01 | Класс работы с хранилищем Key-Value для итогового советника в базе данных эксперта | Часть 23 |
MQL5/Include/antekov/Advisor/Experts | Файлы с общими частями используемых советников разного типа | |||
19 | Expert.mqh | 1.22 | Библиотечный файл для итогового советника. Параметры групп могут браться базы данных эксперта | Часть 23 |
20 | Optimization.mqh | 1.04 | Библиотечный файл для советника, управляющего запуском задач оптимизации | Часть 23 |
21 | Stage1.mqh | 1.19 | Библиотечный файл для советника оптимизации одиночного экземпляра торговой стратегии (Этап 1) | Часть 23 |
22 | Stage2.mqh | 1.04 | Библиотечный файл для советника оптимизации группы экземпляров торговых стратегий (Этап 2) | Часть 23 |
23 | Stage3.mqh | 1.04 | Библиотечный файл для советника, сохраняющего сформированную нормированную группу стратегий в базу данных эксперта с заданным именем. | Часть 23 |
MQL5/Include/antekov/Advisor/Optimization | Классы, отвечающие за работу автоматической оптимизации | |||
24 | OptimizationJob.mqh | 1.00 | Класс для работы этапа проекта оптимизации | Часть 25 |
25 | OptimizationProject.mqh | 1.00 | Класс для проекта оптимизации | Часть 25 |
26 | OptimizationStage.mqh | 1.00 | Класс для этапа проекта оптимизации | Часть 25 |
27 | OptimizationTask.mqh | 1.00 | Класс для задачи оптимизации (для создания) | Часть 25 |
28 | Optimizer.mqh | 1.03 | Класс для менеджера автоматической оптимизации проектов | Часть 22 |
29 | OptimizerTask.mqh | 1.03 | Класс для задачи оптимизации (для конвейера) | Часть 22 |
MQL5/Include/antekov/Advisor/Strategies | Примеры торговых стратегий, используемые для демонстрации работы проекта | |||
30 | HistoryStrategy.mqh | 1.00 | Класс торговой стратегии воспроизведения истории сделок | Часть 16 |
31 | SimpleVolumesStrategy.mqh | 1.11 | Класс торговой стратегии с использованием тиковых объемов | Часть 22 |
MQL5/Include/antekov/Advisor/Utils | Вспомогательные утилиты, макросы для сокращения кода | |||
32 | ExpertHistory.mqh | 1.00 | Класс для экспорта истории сделок в файл | Часть 16 |
33 | Macros.mqh | 1.06 | Полезные макросы для операций с массивами | Часть 25 |
34 | NewBarEvent.mqh | 1.00 | Класс определения нового бара для конкретного символа | Часть 8 |
35 | SymbolsMonitor.mqh | 1.00 | Класс получения информации о торговых инструментах (символах) | Часть 21 |
MQL5/Include/antekov/Advisor/Virtual | Классы для создания различных объектов, объединённых использованием системы виртуальных торговых ордеров и позиций | |||
36 | Money.mqh | 1.01 | Базовый класс управления капиталом | Часть 12 |
37 | TesterHandler.mqh | 1.07 | Класс для обработки событий оптимизации | Часть 23 |
38 | VirtualAdvisor.mqh | 1.10 | Класс эксперта, работающего с виртуальными позициями (ордерами) | Часть 24 |
39 | VirtualChartOrder.mqh | 1.01 | Класс графической виртуальной позиции | Часть 18 |
40 | VirtualHistoryAdvisor.mqh | 1.00 | Класс эксперта воспроизведения истории сделок | Часть 16 |
41 | VirtualInterface.mqh | 1.00 | Класс графического интерфейса советника | Часть 4 |
42 | VirtualOrder.mqh | 1.09 | Класс виртуальных ордеров и позиций | Часть 22 |
43 | VirtualReceiver.mqh | 1.04 | Класс перевода открытых объемов в рыночные позиции (получатель) | Часть 23 |
44 | VirtualRiskManager.mqh | 1.05 | Класс управления риском (риск-менеждер) | Часть 24 |
45 | VirtualStrategy.mqh | 1.09 | Класс торговой стратегии с виртуальными позициями | Часть 23 |
46 | VirtualStrategyGroup.mqh | 1.03 | Класс группы торговых стратегий или групп торговых стратегий | Часть 24 |
47 | VirtualSymbolReceiver.mqh | 1.00 | Класс символьного получателя | Часть 3 |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Во первых хотелось бы узнать на каком языке вот это
Это корейский язык. Ваш браузер его не показывает почему-то
Это корейский язык. Ваш браузер его не показывает почему-то
Вот именно. В этой теме я ничего не писал в этот день, 2025.07.08 от слова совсем. Если перейти по этой ссылке в тему, то отобразится сообщение с другой датой. Это наверное тоже мой браузер виноват в том, что ваши оставшиеся программисты не могут справиться с этой задачей.
Вот именно. В этой теме я ничего не писал в этот день, 2025.07.08 от слова совсем. Если перейти по этой ссылке в тему, то отобразится сообщение с другой датой. Это наверное тоже мой браузер виноват в том, что ваши оставшиеся программисты не могут справиться с этой задачей.
Спасибо за настойчивость, исправили.
Спасибо за настойчивость, исправили.
Простите за настойчивость, что-то я не вижу исправлений. По прежнему ссылка ведёт на странное сообщение которое я не писал. Ну даже если допустить, что писал я, то почему рядом нет сообщения на русском? Или вы думаете, что если мне не по силам выучить английский, я выучил корейский и так забавляюсь…
Вот какая разница в одном обсуждении на разных языках
Это по ссылке
Это перевод на русский
И вот что в русскоязычном варианте статьи.
Так на каком языке я пытался писать???
Это всё только одна тема. А если посмотреть другие, то и там найдутся сообщения странного происхождения на языках которые мне и во сне не снились.
Возможно я погорячился. Других подобных сообщений нашёл только одно, на английском и возможно настоящий перевод.
Пожалуйста удалите вышеуказанное сообщение во всех языковых вариантах и наверное действительно будет исправлено. Может не до конца как и в прошлый раз……
Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий
Обсуждение статьи "Разрабатываем мультивалютный советник (Часть 25): Подключаем новую стратегию (II)"
Rashid Umarov, 2025.07.06 14:04
Спасибо, разберемся.
Мы уже решали эту проблему, но похоже, не до конца.