
Разрабатываем мультивалютный советник (Часть 21): Подготовка к важному эксперименту и оптимизация кода
Введение
В предыдущей части мы начали работу по приведению в порядок конвейера автоматической оптимизации, позволяющего получать новый итоговый советник с учётом накопленных ценовых данных. Однако, до полной автоматизации дело пока не дошло, так как далее надо принять непростые решения о том, как лучше реализовать последние этапы. Непросты они тем, что при неправильном выборе нам придётся многое переделывать. Поэтому очень хочется сэкономить усилия и постараться сделать правильный выбор. А ничто так не помогает в приёме сложных решений, как... отложить их приём! Особенно, если мы можем себе это позволить.
Но откладывать тоже можно по-разному. Вместо простого оттягивания момента выбора, попробуем переключиться на другую задачу, которая вроде бы позволит отвлечься, но на самом деле её решение действительно может помочь, и если не выработать правильный путь, то, по крайней мере, повысить мотивацию сделать выбор.
Интересный вопрос
Камнем преткновения во многих спорах об использовании оптимизации параметров является вопрос о том, насколько долго можно использовать полученные параметры для торговли в будущем периоде с сохранением основных показателей прибыльности и просадки на заданных уровнях. И можно ли вообще это делать?
Хотя часто встречается точка зрения, что в повторяемость в будущем результатов тестирования верить нельзя, и это лишь вопрос везения, когда стратегия "сломается". Наверное, почти все разработчики торговых стратегий очень хотят в это верить, иначе теряется смысл прикладывания огромного количества усилий по разработке и тестированию.
Попытки повысить уверенность в том, что подобрав хорошие параметры, стратегия сможет ещё какое-то время успешно работать, уже неоднократно предпринимались. Есть опубликованные статьи, в которых так или иначе рассматривается тема периодического автоматического подбора лучших параметров для торгующих советников. Отдельно можно отметить советник Validate от @fxsaber, который как раз предназначен для проведения весьма интересного эксперимента.
Этот инструмент позволяет взять произвольный советник (исследуемый) и, выбрав некоторый период времени (например 3 года), запустить такой процесс: исследуемый советник будет оптимизироваться на некотором периоде (например 2 месяца), после чего, на лучших настройках торговать в тестере стратегий на периоде, например, двух недель. По завершении каждого двухнедельного периода, исследуемый советник снова будет оптимизироваться на предшествующих двух месяцах и снова торговать ещё две недели. Так будет продолжаться до тех пор, пока не будет достигнут конец выбранного интервала в 3 года.
В итоге будет получен торговый отчет, показывающий, как исследуемый советник торговал бы на протяжении всех трёх лет, если бы его действительно так периодически заново оптимизировали и запускали с обновлёнными параметрами. Понятно, что можно произвольно выбирать упомянутые временные интервалы по своему усмотрению. Если какой-то советник сможет показать приемлемые результаты при такой переоптимизации, то это будет свидетельствовать о его повышенном потенциале использования при реальной торговле.
Однако, у этого инструмента есть существенное ограничение — исследуемый советник должен иметь открытые входные параметры для проведения оптимизации. Если взять, например, наши итоговые советники, получаемые в предыдущих частях путем объединения многих одиночных экземпляров, то у них нет входных параметров, которые позволяли бы влиять на торговую логику открытия позиций. Параметры управления капиталом и риск-менеджер мы не будем принимать во внимание, так как оптимизация их параметров хоть и возможна, но довольно бессмысленна. Ведь и так понятно, что если мы увеличим размер открываемых позиций, то результат прохода покажет большую прибыль, по сравнению с уже полученной ранее в результате прохода с меньшим размером позиций.
Поэтому попробуем реализовать нечто подобное, но применимое к нашим разрабатываемым советникам.
Намечаем путь
В целом, нам нужен скрипт наполнения базы данных практически одинаковыми проектами. Основное отличие будет только в дате начала и окончания периода оптимизации. Состав этапов, работ этапов и задач в рамках работ может быть полностью одинаковым. Поэтому можно пока сделать сервисный советник с небольшим количеством входных параметров, среди которых будет дата начала и продолжительность периода оптимизации. Запуская его в режиме оптимизации с перебором дат начала, мы сможем заполнить базу данных однотипными проектами. Какие ещё параметры имеет смысл вынести во входные, пока не ясно, определимся с ними по ходу разработки.
Полное выполнение всех задач оптимизации, даже в рамках одного проекта, может занимать продолжительное время. А если таких проектов нужно выполнить не один, а десяток или более, то тут уже речь заходит о довольно объёмных по времени заданиях. Поэтому есть смысл посмотреть, можно ли как-то ускорить работу наших советников этапов. Для обнаружения узких мест, нуждающихся в исправлении, воспользуемся профайлером, входящим в состав MetaEditor.
Далее нам нужно решить, как смоделировать работу из нескольких полученных строк инициализации (каждый проект после завершения своих задач будет давать одну строку инициализации итогового советника). Скорее всего, нам потребуется создание нового тестирующего советника, специально предназначенного для такой работы. Но это мы, наверное, отложим до следующей статьи.
Приступим сначала к оптимизации кода тестирующих советников, а только после этого займёмся созданием скрипта наполнения базы данных.
Оптимизация кода
Прежде чем погрузиться в реализацию основной задачи, посмотрим, нет ли какой-либо возможности ускорить работу кода советников, участвующих в автоматической оптимизации. Для обнаружения возможных узких мест, возьмём для исследования итоговый советник из прошлой части. В нём объединены 32 экземпляра одиночных торговых стратегий (2 символа * 1 таймфрейм * 16 экземпляров = 32). Это, конечно, намного меньше предполагаемого общего количества экземпляров в итоговом советнике, но при оптимизации у нас абсолютное большинство проходов будет использовать, либо один экземпляр (на первом этапе), либо не более 16 экземпляров (на втором этапе). Поэтому такой подопытный советник нас вполне устроит.
Запустим советник в режиме профилирования на исторических данных. При запуске в этом режиме, автоматически будет скомпилирована специальная версия советника для профилирования и запущена в тестере стратегий. Процитируем описание использования профилирования из справки:
Для профилирования используется метод "Sampling". Профилировщик делает паузы в работе MQL-программы (~10 000 раз в секунду) и собирает статистику того, сколько раз пауза пришлась на тот или иной участок кода. В том числе анализируются стеки вызовов, чтобы определить "вклад" каждой функции в общее время работы кода.
Sampling — это легковесный и точный метод. В отличие от других, он не вносит никаких изменений в анализируемый код, которые могли бы повлиять на скорость его работы.
Отчет профилирования представлен в виде функций или строк программы, для каждой из которых доступно два показателя:
- Общая активность ЦП [единица измерения, %] — общее количество "появления" функции в стеке вызовов.
- Собственная активность ЦП [единица измерения, %] — количество "пауз", которые произошли непосредственно внутри указанной функции. Этот счетчик наиболее важен для определения "узких" мест, поскольку по статистике остановка чаще происходит в тех участках программы, которые требуют большего процессорного времени.
Для показателя выводится абсолютное количество и процент от общего количества.
Вот что получилось после завершения прохода:
Рис. 1. Результаты профилирования кода подопытного советника
По умолчанию в списке результатов профилирования показываются крупные функции, расположенные на верхних уровнях. Но кликнув по строке с именем функции, мы можем увидеть вложенный список функций, которые вызывались из данной. Это позволяет более точно установить, какие участки кода занимали большее процессорное время.
В первых двух строках мы ожидаемо увидели обработчик OnTick() и вызываемый из него обработчик CVirtualAdvisor::Tick(). Действительно, помимо инициализации, основную часть времени советник проводит, обрабатывая приходящие тики. А вот третья и четвёртая строки результатов вызывают уже обоснованные вопросы.
Почему у нас происходит так много вызовов метода выбора текущего символа? Почему так много времени тратится на получение каких-то целочисленных свойств символа? Давайте разбираться.
Развернув строку, соответствующую вызову метода CSymbolInfo::Name(string name), мы можем отследить, что практически всё затраченное время приходится на её вызов из функции проверки необходимости закрытия виртуальной позиции.
//+------------------------------------------------------------------+ //| Проверка необходимости закрытия по SL, TP или EX | //+------------------------------------------------------------------+ bool CVirtualOrder::CheckClose() { if(IsMarketOrder()) { // Если это открытая рыночная виртуальная позиция, то s_symbolInfo.Name(m_symbol); // Выбираем нужный символ s_symbolInfo.RefreshRates(); // Обновляем информацию о текущих ценах // ... } return false; }
Этот код был написан уже довольно давно. В тот момент нам было важно, чтобы открытые виртуальные позиции правильно транслировались в реальные позиции. Закрытие виртуальной позиции должно было приводить к немедленному (или почти немедленному) закрытию некоторого объёма реальных позиций. Поэтому эта проверка должна выполняться на каждом тике и для каждой открытой виртуальной позиции.
Для самодостаточности мы снабдили каждый объект класса CVirtualOrder своим экземпляром объекта класса CSymbolInfo, через который мы запрашивали всю необходимую информацию о ценах и спецификации нужного торгового инструмента (символа). Таким образом, для 16 экземпляров торговых стратегий, использующих по три виртуальных позиции каждая, в массиве виртуальных позиций их окажется 16*3 = 48 штук. Если в советнике будет несколько сотен экземпляров торговых стратегий, да ещё и использующих большее количество виртуальных позиций, то количество вызовов метода выбора символа возрастёт многократно. Но является ли это необходимым?
Когда нам обязательно нужно вызывать метод выбора названия символа? Только если символ виртуальной позиции изменился. Если он не менялся с предыдущего тика, то и вызов этого метода символа ни к чему. А меняться символ может только при открытии виртуальной позиции, которая либо ранее не открывалась, либо была открыта по другому символу. Это происходит явно не на каждом тике, а гораздо, гораздо реже. Более того, в используемой модельной стратегии вообще никогда не происходит смены символа для одной виртуальной позиции, так как один экземпляр торговой стратегии работает с единственным символом, который и будет символом для всех виртуальных позиций этого экземпляра стратегии.
Тогда можно вынести объекты класса CSymbolInfo на уровень экземпляра торговой стратегии, но и это может оказаться избыточным, так как разные экземпляры торговой стратегии могут использовать одинаковый символ. Поэтому мы вынесем их ещё выше — на глобальный уровень. На этом уровне нам достаточно иметь столько экземпляров объектов класса CSymbolInfo, сколько есть разных используемых символов в данном советнике. Каждый экземпляр CSymbolInfo будет создаваться только тогда, когда советнику понадобится обратиться к свойствам нового символа. Созданный один раз экземпляр будет навсегда закрепляться за определённым символом.
Вдохновившись этим примером из учебника, создадим свой класс CSymbolsMonitor. В отличие от примера, мы не будем создавать новый класс, который будучи написан гораздо красивее, будет по сути повторять функциональность уже существующего в стандартной библиотеке класса. Наш будет выступать в роли контейнера для нескольких объектов класса CSymbolInfo и заботиться о том, чтобы для каждого используемого символа был создан свой информационный объект этого класса.
Для обеспечения доступности его из любого места кода, снова воспользуемся шаблоном проектирования Singleton при реализации. Основу класса будет составлять массив m_symbols[] для хранения указателей на объекты класса CSymbolInfo.
//+------------------------------------------------------------------+ //| Класс получения информации о торговых инструментах (символах) | //+------------------------------------------------------------------+ class CSymbolsMonitor { protected: // Статический указатель на единственный экземпляр данного класса static CSymbolsMonitor *s_instance; // Массив информационных объектов для разных символов CSymbolInfo *m_symbols[]; //--- Частные методы CSymbolsMonitor() {} // Закрытый конструктор public: ~CSymbolsMonitor(); // Деструктор //--- Статические методы static CSymbolsMonitor *Instance(); // Синглтон - создание и получение единственного экземпляра // Обработка тика для объектов разных символов void Tick(); // Оператор получения объекта с информацией о конкретном символе CSymbolInfo* operator[](const string &symbol); }; // Инициализация статического указателя на единственный экземпляр данного класса CSymbolsMonitor *CSymbolsMonitor::s_instance = NULL;
Реализация статического метода создания единственного экземпляра класса повторяет уже встречавшиеся ранее реализации. А в деструкторе мы разместим цикл удаления созданных информационных объектов.
//+------------------------------------------------------------------+ //| Синглтон - создание и получение единственного экземпляра | //+------------------------------------------------------------------+ CSymbolsMonitor* CSymbolsMonitor::Instance() { if(!s_instance) { s_instance = new CSymbolsMonitor(); } return s_instance; } //+------------------------------------------------------------------+ //| Деструктор | //+------------------------------------------------------------------+ CSymbolsMonitor::~CSymbolsMonitor() { // Удаляем все созданные информационные объекты для символов FOREACH(m_symbols, if(!!m_symbols[i]) delete m_symbols[i]); }
Публичный метод обработки тика будет обеспечивать периодическое обновление информации о спецификации символов и котировок. Спецификация, возможно, вообще не изменяется со временем, но на всякий случай предусмотрим её обновление раз в сутки. Котировки будем обновлять каждую минуту, так как мы используем режим работы советника только по открытию минутных баров (для лучшей повторяемости результатов моделирования в режиме 1 minute OHLC и режиме всех тиков на основе реальных тиков).
//+------------------------------------------------------------------+ //| Обработка тика для массива виртуальных ордеров (позиций) | //+------------------------------------------------------------------+ void CSymbolsMonitor::Tick() { // Обновляем котировки каждую минуту и спецификацию раз в день FOREACH(m_symbols, { if(IsNewBar(m_symbols[i].Name(), PERIOD_D1)) { m_symbols[i].Refresh(); } if(IsNewBar(m_symbols[i].Name(), PERIOD_M1)) { m_symbols[i].RefreshRates(); } }); }
Наконец, добавим перегруженный оператор индексации для получения указателя на нужный объект по заданному имени символа. Именно в этом операторе будет происходить автоматическое создание новых информационных объектов для символов, к которым ранее не было обращения через этот оператор.
//+------------------------------------------------------------------+ //| Оператор получения объекта с информацией о конкретном символе | //+------------------------------------------------------------------+ CSymbolInfo* CSymbolsMonitor::operator[](const string &name) { // Ищем информационный объект для данного символа в массиве int i; SEARCH(m_symbols, m_symbols[i].Name() == name, i); // Если нашли, то возвращаем его if(i != -1) { return m_symbols[i]; } else { // Иначе создаём новый информационный объект CSymbolInfo *s = new CSymbolInfo(); // Выбираем для него нужный символ if(s.Name(name)) { // Если выбрали успешно, то обновляем котировки s.RefreshRates(); // Добавляем в массив информационных объектов и возвращаем его APPEND(m_symbols, s); return s; } else { PrintFormat(__FUNCTION__" | ERROR: can't create symbol with name [%s]", name); } } return NULL; }
Сохраним полученный код в файле SymbolsMonitor.mqh в текущей папке. Теперь наступает очередь кода, который будет использовать созданный класс.
Модификация CVirtualAdvisor
В этом классе у нас уже есть несколько объектов, которые существуют в единственном экземпляре и выполняют какие-то специфические задачи: получатель объёмов виртуальных позиций, риск-менеджер, интерфейс информирования пользователя. Добавим к ним и объект монитора символов. Точнее, создадим поле класса, которое будет хранить указатель на объект монитора символов:
class CVirtualAdvisor : public CAdvisor { protected: CSymbolsMonitor *m_symbols; // Объект монитора символов CVirtualReceiver *m_receiver; // Объект получателя, выводящий позиции на рынок CVirtualInterface *m_interface; // Объект интерфейса для показа состояния пользователю CVirtualRiskManager *m_riskManager; // Объект риск-менеджера ... public: ... };
Создание объекта монитора символов будет инициироваться при вызове конструктора за счёт вызова статического метода CSymbolsMonitor::Instance(), аналогично остальным упомянутым ранее объектам. А в деструкторе добавим удаление этого объекта.
//+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(string p_params) { ... // Если нет ошибок чтения, то if(IsValid()) { // Создаём группу стратегий CREATE(CVirtualStrategyGroup, p_group, groupParams); // Инициализируем монитор символов статическим монитором символов m_symbols = CSymbolsMonitor::Instance(); // Инициализируем получателя статическим получателем m_receiver = CVirtualReceiver::Instance(p_magic); // Инициализируем интерфейс статическим интерфейсом m_interface = CVirtualInterface::Instance(p_magic); ... } } //+------------------------------------------------------------------+ //| Деструктор | //+------------------------------------------------------------------+ void CVirtualAdvisor::~CVirtualAdvisor() { if(!!m_symbols) delete m_symbols; // Удаляем монитор символов if(!!m_receiver) delete m_receiver; // Удаляем получатель if(!!m_interface) delete m_interface; // Удаляем интерфейс if(!!m_riskManager) delete m_riskManager; // Удаляем риск-менеджер DestroyNewBar(); // Удаляем объекты отслеживания нового бара }
В обработчик нового тика добавим вызов метода Tick() для монитора символов. Именно в нём будет происходить обновление котировок всех символов, которые используются в советнике:
//+------------------------------------------------------------------+ //| Обработчик события OnTick | //+------------------------------------------------------------------+ void CVirtualAdvisor::Tick(void) { // Определяем новый бар по всем нужным символам и таймфреймам bool isNewBar = UpdateNewBar(); // Если нигде нового бара нет, а мы работаем только по новым барам, то выходим if(!isNewBar && m_useOnlyNewBar) { return; } // Монитор символов обновляет котировки m_symbols.Tick(); // Получатель обрабатывает виртуальные позиции m_receiver.Tick(); // Запуск обработки в стратегиях CAdvisor::Tick(); // Риск-менеджер обрабатывает виртуальные позиции m_riskManager.Tick(); // Корректировка рыночных объемов m_receiver.Correct(); // Сохранение состояния Save(); // Отрисовка интерфейса m_interface.Redraw(); }
Пользуясь случаем, добавим заодно в этот класс с прицелом на будущее обработчик события ChartEvent. В нём пока будет вызываться одноимённый метод у объекта интерфейса m_interface, который на данном этапе ничего не делает.
Сохраним сделанные изменения в файле VirtualAdvisor.mqh в текущей папке.
Модификация CVirtualOrder
Как уже было сказано, получение информации о символах у нас выполняется в классе виртуальных позиций. Поэтому начнем внесение изменений с него, и, первым делом, добавим в поля класса указатели на монитор (класса CSymbolsMonitor) и информационный объект для символа (класса CSymbolInfo):
class CVirtualOrder { private: //--- Статические поля static ulong s_count; // Счётчик всех созданных объектов CVirtualOrder CSymbolInfo *m_symbolInfo; // Объект для получения свойств символов //--- Связанные объекты получателя и стратегии CSymbolsMonitor *m_symbols; CVirtualReceiver *m_receiver; CVirtualStrategy *m_strategy; ... }
Добавление указателей к составу полей класса подразумевает, что им должны быть присвоены указатели на какие-то созданные объекты. А если эти объекты создаются внутри методов объектов данного класса, то необходимо позаботиться и о корректном их удалении.
Добавим в конструктор инициализацию указателя на монитор символов и очистку указателя на информационный объект для символа. Для получения указателя, на монитор символов вызовем статический метод CSymbolsMonitor::Instance(). Создание единственного объекта монитора (при его отсутствии) будет происходить внутри него. В деструктор добавим удаление информационного объекта, если он всё-таки был создан и пока не удалён:
//+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CVirtualOrder::CVirtualOrder(CVirtualStrategy *p_strategy) : // Список инициализации m_id(++s_count), // Новый идентификатор = счётчик объектов + 1 ... m_point(0) { PrintFormat(__FUNCTION__ + "#%d | CREATED VirtualOrder", m_id); m_symbolInfo = NULL; m_symbols = CSymbolsMonitor::Instance(); } //+------------------------------------------------------------------+ //| Деструктор | //+------------------------------------------------------------------+ CVirtualOrder::~CVirtualOrder() { if(!!m_symbolInfo) delete m_symbolInfo; }
Мы не стали добавлять в конструктор получение указателя на информационный объект для символа m_symbolInfo по той причине, что в момент вызова конструктора не всегда может быть точно известно, какой символ будет использоваться в этой виртуальной позиции. Это становится ясно только при выполнении открытия виртуальной позиции, то есть при вызове метода CVirtualOrder::Open(). В него мы и добавим инициализацию указателя на информационный объект символа:
//+------------------------------------------------------------------+ //| Открытие виртуальной позиции (ордера) | //+------------------------------------------------------------------+ bool CVirtualOrder::Open(string symbol, // Символ ENUM_ORDER_TYPE type, // Тип (BUY или SELL) double lot, // Объём double price = 0, // Цена открытия double sl = 0, // Уровень StopLoss (цена или пункты) double tp = 0, // Уровень TakeProfit (цена или пункты) string comment = "", // Комментарий datetime expiration = 0, // Время истечения bool inPoints = false // Уровни SL и TP заданы в пунктах? ) { if(IsOpen()) { // Если позиция уже открыта, то ничего не делаем PrintFormat(__FUNCTION__ "#%d | ERROR: Order is opened already!", m_id); return false; } // Получаем от монитора символов указатель на информационный объект для нужного символа m_symbolInfo = m_symbols[symbol]; if(!!m_symbolInfo) { // Действия по открытию ... return true; } else { ... return false; } }
Теперь, поскольку за обновление информации о котировках символов отвечает монитор символов, то внутри класса CVirtualOrder мы можем убрать все вызовы методов Name() и RefreshRates() для информационного объекта свойств символа m_symbolInfo. При открытии виртуальной позиции в m_symbolInfo будет запоминаться указатель на объект, для которого уже выбран нужный символ. При сопровождении ранее открытой виртуальной позиции на данном тике уже был один раз вызван метод RefreshRates() — это сделал монитор символов для всех них в методе CSymbolsMonitor::Tick().
Проведем профилирование снова. Картина поменялась в лучшую сторону, но всё равно вызовы функции SymbolInfoDouble() занимает 9%. Недолгий поиск показал, что эти вызовы нужны для получения значения спреда. Но мы можем заменить эту операцию на вычисление разности цен (Ask — Bid), которые уже были получены при вызове метода RefreshRates() и не требуют дополнительных вызовов функции SymbolInfoDouble().
Дополнительно в этот класс были внесены изменения, не связанные непосредственно с увеличением скорости работы и не являющиеся необходимыми для рассматриваемой модельной стратегии:
- в обработчик CVirtualStrategy::OnOpen() и CVirtualStrategy::OnClose() добавили передачу текущего объекта;
- добавили подсчёт прибыли закрытых виртуальных позиций;
- добавили геттеры и сеттеры для уровней StopLoss, TakeProfit;
- добавили уникальный тикет, назначаемый при открытии виртуальной позиции.
Возможно, эту библиотеку ждет более радикальная переделка. Поэтому не будем останавливаться на описании этих изменений.
Сохраним сделанные изменения в файле VirtualOrder.mqh в текущей папке.
Модификация стратегии
Для использования монитора символов, нам понадобилось внести небольшие правки и в класс торговой стратегии. Во-первых, как и в классе для виртуальных позиций, мы сделали так, что член класса m_symbolInfo хранит теперь указатель на объект вместо самого объекта:
//+------------------------------------------------------------------+ //| Торговая стратегия с использованием тиковых объемов | //+------------------------------------------------------------------+ class CSimpleVolumesStrategy : public CVirtualStrategy { protected: ... CSymbolInfo *m_symbolInfo; // Объект для получения информации о свойствах символа ... public: ... };
И добавили его инициализацию в конструкторе:
//+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CSimpleVolumesStrategy::CSimpleVolumesStrategy(string p_params) { ... // Регистрируем обработчик события нового бара на минимальном таймфрейме //IsNewBar(m_symbol, PERIOD_M1); m_symbolInfo = CSymbolsMonitor::Instance()[m_symbol]; ... }
Регистрацию обработчика события нового бара мы закомментировали, так как он теперь будет регистрироваться в мониторе символов.
Во-вторых, убрали обновление текущих цен из кода стратегии (в методах проверки сигнала на открытие и самого открытия позиций), поскольку об этом тоже заботится монитор символов.
Сохраним сделанные изменения в файле SimpleVolumesStrategy.mqh в текущей папке.
Проверка корректности
Сравним результаты тестирования подопытного советника на одинаковом временном интервале до и после внесения изменений, связанных с добавлением монитора символов.
Рис. 2 Сравнение результатов тестирования прошлой версии и текущей версии с монитором символов
Как видно, они в целом совпадают, но есть небольшие отличия. Покажем их для наглядности в виде таблицы.
Версия | Прибыль | Просадка | Нормированная прибыль |
---|---|---|---|
Прошлая версия | 41 990.62 | 1 019.49 (0.10%) | 6 867.78 |
Текущая версия | 42 793.27 | 1 158.38 (0.11%) | 6 159.87 |
Если сравнить первые сделки в отчётах, то можно заметить, что в прошлой версии есть дополнительные открытия позиций, которых нет в текущей и наоборот. Скорее всего, это связано с тем, что при запуске тестера на символе EURGBP, новый бар для EURGBP наступает в момент времени mm:00, а для другого символа, например GBPUSD, он может наступить или в mm:00 или в mm:20.
Для устранения этого эффекта, добавим дополнительную проверку наступления нового бара в стратегию:
//+------------------------------------------------------------------+ //| "Tick" event handler function | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::Tick() override { if(IsNewBar(m_symbol, PERIOD_M1)) { // Если их количество меньше допустимого if(m_ordersTotal < m_maxCountOfOrders) { // Получаем сигнал на открытие int signal = SignalForOpen(); if(signal == 1 /* || m_ordersTotal < 1 */) { // Если сигнал на покупку, то OpenBuyOrder(); // открываем ордер BUY_STOP } else if(signal == -1) { // Если сигнал на продажу, то OpenSellOrder(); // открываем ордер SELL_STOP } } } }
После такой модификации результаты только улучшились. Текущая версия показала самую высокую нормированную прибыль:
Версия | Прибыль | Просадка | Нормированная прибыль |
---|---|---|---|
Прошлая версия | 46 565.39 | 1 079.93 (0.11%) | 7 189.77 |
Текущая версия | 47 897.30 | 1 051.37 (0.10%) | 7 596.31 |
Так что оставим внесённые правки и перейдем к созданию скрипта наполнения базы данных.
Наполнение базы данных проектами
Мы создадим не скрипт, а советник, однако вести себя он будет как скрипт. Вся работа будет выполняться в функции инициализации, после чего на первом тике советник будет выгружаться. Такая реализация позволит запускать его как на графике, так и в оптимизаторе, если мы хотим получить многократные запуски с параметрами, изменяемыми в заданных пределах.
Поскольку это первая реализация, то не будем сильно продумывать на перёд, какой состав входных параметров окажется более удобным, а постараемся сделать просто минимальный рабочий прототип. Вот какой список параметров в итоге получился:
//+------------------------------------------------------------------+ //| Входные параметры | //+------------------------------------------------------------------+ input group "::: База данных" sinput string fileName_ = "article.16373.db.sqlite"; // - Файл с основной базой данных input group "::: Параметры проекта" sinput string projectName_ = "SimpleVolumes"; // - Название sinput string projectVersion_ = "1.20"; // - Версия sinput string symbols_ = "GBPUSD;EURUSD;EURGBP"; // - Символы sinput string timeframes_ = "H1;M30;M15"; // - Таймфреймы input datetime fromDate_ = D'2018-01-01'; // - Дата начала input datetime toDate_ = D'2023-01-01'; // - Дата окончания
С названием и версией проекта всё очевидно, далее идут два параметра, в которых мы будем передавать списки символов и таймфреймов, разделённых точкой с запятой. Они будут использоваться для получения одиночных экземпляров торговой стратегии. Для каждого символа будут браться по очереди все таймфреймы. Таким образом, если мы указали в значениях по умолчанию три символа и три таймфрейм, то это приведёт к созданию девяти одиночных экземпляров.
Каждый одиночный экземпляр должен пройти первый этап оптимизации, на котором подбираются наилучшие комбинации параметров именно для него. Точнее, в процессе оптимизации мы пробуем много комбинаций из которых затем можно выбирать некоторое количество "хороших".
Этот выбор будет осуществляться уже на втором этапе оптимизации. В результате у нас будет получена группа из нескольких "хороших" экземпляров, работающих на определённом символе и таймфрейме. После повторения второго этапа для всех комбинаций символ-таймфрейм у нас получится девять групп одиночных экземпляров для каждой комбинации.
На третьем этапе мы будем объединять эти девять групп, получая и сохраняя в библиотеке строку инициализации, по которой можно создать советник, включающий в себя все одиночные экземпляры из этих групп.
Напомним, что код, отвечающий за последовательное выполнение всех вышеперечисленных этапов у нас уже написан и может работать, если в базе данных сформировать нужные "инструкции". До этого мы добавляли их в базу данных вручную. Теперь мы хотим переложить эту рутинную процедуру на разрабатываемый советник-скрипт.
Оставшиеся два параметра этого советника позволяют задать дату начала и окончания интервала оптимизации. Их мы будем использовать для того, чтобы промоделировать периодическое проведение повторной оптимизации и посмотреть, насколько долго после повторной оптимизации итоговый советник будет торговать с такими же результатами, как и на интервале оптимизации.
С учётом сказанного, код функции инициализации может быть примерно таким:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+ //| Обработка тика | //+------------------------------------------------------------------+ void OnTick() { // Так как вся работа выполняется в OnInit(), то удаляем советник ExpertRemove(); }
То есть мы последовательно создаём запись в таблице проектов, затем в таблицу этапов проекта добавляем этапы и после этого наполняем таблицы работ и задач для каждой работы. В конце мы устанавливаем для проекта статус Queued, то есть поставлен в очередь на выполнение. Благодаря триггерам в базе данных, все этапы, работы и задачи проекта тоже перейдут в статус Queued.
Рассмотрим теперь код из созданных функций более подробно. Самая простая из них — это создание проекта. Она содержит один SQL-запрос на вставку данных и сохранение идентификатора только что созданной записи в глобальной переменной id_project:
//+------------------------------------------------------------------+ //| Создание проекта | //+------------------------------------------------------------------+ void CreateProject(string name, string ver, string desc = "") { string query = StringFormat("INSERT INTO projects " " VALUES (NULL,'%s','%s','%s',NULL,'Done') RETURNING rowid;", name, ver, desc); PrintFormat(__FUNCTION__" | %s", query); id_project = DB::Insert(query); }
В качестве описания проекта мы формируем строку из даты начала и окончания интервала оптимизации. Это позволит нам отличать между собой проекты для одной и той же версии торговой стратегии.
Немного более длинной будет функция создания этапов: там потребуется выполнить уже три SQL-запроса для создания трёх этапов. Конечно, этапов может быть и больше, но пока что ограничимся только теми тремя, которые были упомянуты немного раньше. После создания каждого этапа мы также запоминаем их идентификаторы в глобальных переменных id_stage1, id_stage2, id_stage3.
//+------------------------------------------------------------------+ //| Создание трёх этапов | //+------------------------------------------------------------------+ void CreateStages() { // Этап 1 - оптимизация одиночного экземпляра string query1 = StringFormat("INSERT INTO stages VALUES(" "NULL," // 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 ") RETURNING rowid;", id_project, // id_project "NULL", // id_parent_stage "First", // name "SimpleVolumesStage1.ex5", // expert "GBPUSD", // symbol "H1", // period 2, // optimization 2, // model TimeToString(fromDate_, TIME_DATE), // from_date TimeToString(toDate_, TIME_DATE), // to_date 0, // forward_mode "0", // forward_date 1000000, // deposit "USD", // currency 0, // profit_in_pips 200, // leverage 0, // execution_mode 7, // optimization_criterion "Done" // status ); PrintFormat(__FUNCTION__" | %s", query1); id_stage1 = DB::Insert(query1); // Этап 2 - подбор хорошей группы из одиночных экземпляров string query2 = StringFormat("INSERT INTO stages VALUES(" "NULL," // id_stage "%I64u," // id_project "%d," // 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 ") RETURNING rowid;", id_project, // id_project id_stage1, // id_parent_stage "Second", // name "SimpleVolumesStage2.ex5", // expert "GBPUSD", // symbol "H1", // period 2, // optimization 2, // model TimeToString(fromDate_, TIME_DATE), // from_date TimeToString(toDate_, TIME_DATE), // to_date 0, // forward_mode "0", // forward_date 1000000, // deposit "USD", // currency 0, // profit_in_pips 200, // leverage 0, // execution_mode 7, // optimization_criterion "Done" // status ); PrintFormat(__FUNCTION__" | %s", query2); id_stage2 = DB::Insert(query2); // Этап 3 - сохранение в библиотеку строки инициализации итогового советника string query3 = StringFormat("INSERT INTO stages VALUES(" "NULL," // id_stage "%I64u," // id_project "%d," // 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 ") RETURNING rowid;", id_project, // id_project id_stage2, // id_parent_stage "Save to library", // name "SimpleVolumesStage3.ex5", // expert "GBPUSD", // symbol "H1", // period 0, // optimization 2, // model TimeToString(fromDate_, TIME_DATE), // from_date TimeToString(toDate_, TIME_DATE), // to_date 0, // forward_mode "0", // forward_date 1000000, // deposit "USD", // currency 0, // profit_in_pips 200, // leverage 0, // execution_mode 7, // optimization_criterion "Done" // status ); PrintFormat(__FUNCTION__" | %s", query3); id_stage3 = DB::Insert(query3); }
Для каждого этапа мы указываем своё название, идентификатор родительского этапа и имя советника для этапа. Остальные поля в таблице этапов будут в большинстве своём одинаковы для разных этапов: интервал оптимизации, начальный депозит и так далее.
Основная работа приходится на функцию создания работ и задач CreateJobs(). Каждая работа будет относиться одной комбинации символа и таймфрейма. Поэтому вначале мы создаём массивы для всех используемых символов и таймфреймов, которые перечислены во входных параметрах. Для таймфреймов мы добавили функцию StringToTimeframe(), выполняющую преобразование названия таймфрейма из строки в значение типа ENUM_TIMEFRAMES.
// Массив символов для стратегий string symbols[]; StringSplit(symbols_, ';', symbols); // Массив таймфреймов для стратегий ENUM_TIMEFRAMES timeframes[]; string sTimeframes[]; StringSplit(timeframes_, ';', sTimeframes); FOREACH(sTimeframes, APPEND(timeframes, StringToTimeframe(sTimeframes[i])));
Затем в двойном цикле перебираем все комбинации символов и таймфреймов и создаём по три задачи оптимизации с пользовательским критерием.
// Этап 1 FOREACH(symbols, { for(int j = 0; j < ArraySize(timeframes); j++) { // Используем шаблон параметров оптимизации для первого этапа string params = StringFormat(paramsTemplate1, ""); // Запрос на создание работы первого этапа для данного символа и таймфрейма string query = StringFormat("INSERT INTO jobs " " VALUES (NULL,%I64u,'%s','%s','%s','Done') " " RETURNING rowid;", id_stage1, symbols[i], IntegerToString(timeframes[j]), params); ulong id_job = DB::Insert(query); // Добавляем идентификатор созданной работы в массив APPEND(id_jobs1, id_job); // Создаём три задачи для данной работы for(int i = 0; i < 3; i++) { query = StringFormat("INSERT INTO tasks " " VALUES (NULL,%I64u,%d,NULL,NULL,'Done');", id_job, 6); DB::Execute(query); } } });
Такое количество задач обусловлено с одной стороны тем, чтобы у нас накопилось хотя бы 10 - 20 тысяч проходов при оптимизации на одной комбинации, а с другой стороны — их не было бы настолько много, что занимаемое оптимизацией время будет уже слишком большим. Пользовательский критерий для всех трёх задач выбран из-за того, что при разных запусках генетический алгоритм для этой торговой стратегии почти всегда сходится к разным комбинациям параметров. Поэтому нет необходимости использовать различные критерии для разных запусков, у нас и так получается достаточно богатый выбор разных хороших комбинаций параметров одиночного экземпляра стратегии.
В будущем можно вынести в параметры скрипта количество задач и используемые критерии оптимизации, а сейчас они просто жёстко заданы в коде.
Для каждой работы первого этапа мы используем один и тот же шаблон параметров оптимизации, который задан в глобальной переменной 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";
Идентификаторы добавляемых работ мы сохраняем в массив id_jobs1, для использования их при создании работ второго этапа.
Для создания работ второго этапа также используется шаблон, заданный в глобальной переменной paramsTemplate2, но в нём уже есть изменяемая часть:
// Шаблон параметров оптимизации на втором этапе string paramsTemplate2 = "idParentJob_=%s\n" "useClusters_=false||false||0||true||N\n" "minCustomOntester_=500.0||0.0||0.000000||0.000000||N\n" "minTrades_=40||40||1||400||N\n" "minSharpeRatio_=0.7||0.7||0.070000||7.000000||N\n" "count_=8||8||1||80||N\n";
Значение, которое идёт после "idParentJob_=" представляет собой идентификатор работы первого этапа, использующей определённую комбинацию символа и таймфрейма. До создания работ первого этапа эти значения неизвестны, поэтому подставляться в этот шаблон они будут непосредственно перед созданием каждой работы второго этапа из массива id_jobs1.
Параметр count_ в этом шаблоне равен 8, то есть мы будем собирать группы из восьми одиночных экземпляров торговых стратегий. Наш советник второго этапа позволяет задавать в этом параметре значение от 1 до 16. Мы выбрали значение 8 из тех же соображений, что и количество задач для одной работы на первом этапе — не слишком мало, но и не слишком много. В будущем его тоже можно будет вынести во входные параметры скрипта.
// Этап 2 int k = 0; FOREACH(symbols, { for(int j = 0; j < ArraySize(timeframes); j++) { // Используем шаблон параметров оптимизации для второго этапа string params = StringFormat(paramsTemplate2, IntegerToString(id_jobs1[k])); // Запрос на создание работы второго этапа для данного символа и таймфрейма string query = StringFormat("INSERT INTO jobs " " VALUES (NULL,%I64u,'%s','%s','%s','Done') " " RETURNING rowid;", id_stage2, symbols[i], IntegerToString(timeframes[j]), params); ulong id_job = DB::Insert(query); // Добавляем идентификатор созданной работы в массив APPEND(id_jobs2, id_job); k++; // Создаём одну задачу для данной работы query = StringFormat("INSERT INTO tasks " " VALUES (NULL,%I64u,%d,NULL,NULL,'Done');", id_job, 6); DB::Execute(query); } });
На втором этапе для одной работы мы создаём только одну задачу оптимизации, так как за один цикл оптимизации у нас подбираются достаточно хорошие группы одиночных экземпляров торговой стратегии. В качестве критерия оптимизации будем использовать пользовательский критерий.
Идентификаторы добавляемых работ мы также сохраняем в массив id_jobs2, но в итоге они нам пока не понадобились. Возможно, при добавлении этапов эти идентификаторы пригодятся, поэтому не будем их убирать.
На третьем этапе шаблон параметров содержит только название итоговой группы, под которым она будет добавлена в библиотеку:
// Шаблон параметров оптимизации на третьем этапе string paramsTemplate3 = "groupName_=%s\n" "passes_=";
Формируем название итоговой группы из названия и версии проекта и из даты окончания интервала оптимизации и подставляем его в шаблон, который используется для создания работы третьего этапа. Поскольку на третьем этапе мы как бы собираем вместе результаты всех предыдущих этапов, то работа и задача для этой работы будет создаваться только одна:
// Этап 3 // Используем шаблон параметров оптимизации для третьего этапа string params = StringFormat(paramsTemplate3, projectName_ + "_v." + projectVersion_ + "_" + TimeToString(toDate_, TIME_DATE)); // Запрос на создание работы третьего этапа string query = StringFormat("INSERT INTO jobs " " VALUES (NULL,%I64u,'%s','%s','%s','Done') " " RETURNING rowid;", id_stage3, "GBPUSD", "D1", params); ulong id_job = DB::Insert(query); // Создаём одну задачу для данной работы query = StringFormat("INSERT INTO tasks " " VALUES (NULL,%I64u,%d,NULL,NULL,'Done');", id_job, 0); DB::Execute(query);
После этого остаётся только поменять статус проекта, чтобы он поставился в очередь на выполнение:
//+------------------------------------------------------------------+ //| Постановка проекта в очередь на выполнение | //+------------------------------------------------------------------+ void QueueProject() { string query = StringFormat("UPDATE projects SET status='Queued' WHERE id_project=%d;", id_project); DB::Execute(query); }
Сохраним сделанные изменения в новом файле CreateProject.mq5 в текущей папке.
И ещё один момент. Наверное, можно уже считать, что схема базы данных будет постоянной, поэтому можно её интегрировать в библиотеку. Для этого мы создали файл db.schema.sql со схемой базы данных в виде набора SQL-команд и подключили его в качестве ресурса в файл Database.mqh:
// Импорт sql-файла создания структуры БД #resource "db.schema.sql" as string dbSchema
Также немного изменили логику работы метода Connect() — при отсутствии базы данных с указанным именем она будет автоматически создаваться, используя SQL-команды из загруженного в виде ресурса файла. Заодно избавились от метода ExecuteFile(), так как он больше нигде не используется.
Наконец-то мы подошли к тому, что можем попробовать запустить написанный код.
Заполнение базы данных
Не будем сразу генерировать много проектов, а ограничимся только четырьмя. Для этого нам вполне достаточно четыре раза поместить написанный советник-скрипт на любой график, устанавливая каждый раз нужные параметры. Пусть значения всех параметров, кроме даты окончания, будут оставаться равными значениям по умолчанию. А конечную дату мы будем изменять, добавляя каждый раз дополнительный месяц к интервалу тестирования.
В итоге получим примерно такое содержимое базы данных. В таблице проектов есть четыре проекта:
В таблице этапов создано по четыре этапа для каждого проекта. Дополнительный этап с названием "Single tester pass" автоматически создаётся при создании проекта и используется, когда мы хотим запустить одиночный прогон тестера стратегий вне рамок конвейера автоматической оптимизации:
В таблице работ добавлены соответствующие работы:
После запуска проектов на выполнение результат был получен примерно через четверо суток. Это, конечно, не такое уж и маленькое время, несмотря на усилия по оптимизации производительности. Но и не настолько большое, чтобы нельзя было его выделить. Увидеть его можно в таблице библиотеки групп strategy_groups:
По идентификатору прохода id_pass можно посмотреть строку инициализации в таблице проходов passes, например:
Или можно подставить идентификатор прохода в качестве входного параметра советника третьего этапа SimpleVolumesStage3.ex5 и запустить в тестере на выбранном временном интервале:
Рис. 3. Результаты прохода советника SimpleVolumesStage3.ex5 с id_pass=876663 на промежутке 2018.01.01 - 2023.01.01
На этом мы пока остановимся и проведем более детальный анализ полученных результатов в следующих статьях.
Заключение
Итак, мы получили возможность автоматически создавать задания на запуск конвейера автоматической оптимизации, включающей три этапа. Это пока что не более чем черновик, который позволит выявить предпочтительные направления дальнейшего развития. Вопросы реализации автоматического объединения или замены строк инициализации итоговых советников по завершении этапов конвейера для каждого проекта пока остаются открытыми.
Но одно уже можно сказать точно. Выбранный порядок выполнения задач оптимизации в конвейере не очень удачен. Сейчас нам приходится ждать полного завершения всех работ первого этапа, чтобы приступить ко второму. И точно также, третий этап не начнётся ранее, чем завершатся все работы второго этапа. Если мы планируем каким-то способом реализовывать "горячую" замену строк инициализации итогового советника, который непрерывно работает на счёте параллельно с проводимой оптимизацией, то можно сделать эти обновления более мелкими, но более частыми. Возможно, это позволит улучшить результаты, но это пока лишь гипотеза, нуждающаяся в проверке.
Также стоит отметить, что разработанный советник-скрипт ориентирован на создание проектов оптимизации только рассматриваемой модельной торговой стратегии. Для другой стратегии придется вносить небольшие изменения в исходный код. Как минимум, придётся менять шаблон строки входных параметров первого этапа оптимизации. Мы пока не стали выносить эти шаблоны во входные параметры, так как напрямую задавать их там неудобно. Однако далее мы, наверное, разработаем какой-то формат описания задания на создание проекта, которое советник-скрипт будет загружать из файла. Но это уже в следующий раз.
Спасибо за внимание, до встречи!
Важное предупреждение
Все результаты, изложенные в этой статье и всех предшествующих статьях цикла, основываются только на данных тестирования на истории и не являются гарантией получения хоть какой-то прибыли в будущем. Работа в рамках данного проекта носит исследовательский характер. Все опубликованные результаты могут быть использованы всеми желающими на свой страх и риск.
Содержание архива
# | Имя | Версия | Описание | Последние изменения |
---|---|---|---|---|
MQL5/Experts/Article.16373 | ||||
1 | Advisor.mqh | 1.04 | Базовый класс эксперта | Часть 10 |
2 | ClusteringStage1.py | 1.01 | Программа кластеризации результатов первого этапа оптимизации | Часть 20 |
3 | CreateProject.mq5 | 1.00 | Советник-скрипт создания проекта с этапами, работами и задачами оптимизации. | Часть 21 |
4 | Database.mqh | 1.09 | Класс для работы с базой данных | Часть 21 |
5 | db.schema.sql | 1.05 | Схема базы данных | Часть 20 |
6 | ExpertHistory.mqh | 1.00 | Класс для экспорта истории сделок в файл | Часть 16 |
7 | ExportedGroupsLibrary.mqh | — | Генерируемый файл с перечислением имён групп стратегий и массивом их строк инициализации | Часть 17 |
8 | Factorable.mqh | 1.02 | Базовый класс объектов, создаваемых из строки | Часть 19 |
9 | GroupsLibrary.mqh | 1.01 | Класс для работы с библиотекой отобранных групп стратегий | Часть 18 |
10 | HistoryReceiverExpert.mq5 | 1.00 | Советник воспроизведения истории сделок с риск-менеджером | Часть 16 |
11 | HistoryStrategy.mqh | 1.00 | Класс торговой стратегии воспроизведения истории сделок | Часть 16 |
12 | Interface.mqh | 1.00 | Базовый класс визуализации различных объектов | Часть 4 |
13 | LibraryExport.mq5 | 1.01 | Советник, сохраняющий строки инициализации выбранных проходов из библиотеки в файл ExportedGroupsLibrary.mqh | Часть 18 |
14 | Macros.mqh | 1.02 | Полезные макросы для операций с массивами | Часть 16 |
15 | Money.mqh | 1.01 | Базовый класс управления капиталом | Часть 12 |
16 | NewBarEvent.mqh | 1.00 | Класс определения нового бара для конкретного символа | Часть 8 |
17 | Optimization.mq5 | 1.03 | Советник, управляющей запуском задач оптимизации | Часть 19 |
18 | Optimizer.mqh | 1.01 | Класс для менеджера автоматической оптимизации проектов | Часть 20 |
19 | OptimizerTask.mqh | 1.01 | Класс для задачи оптимизации | Часть 20 |
20 | Receiver.mqh | 1.04 | Базовый класс перевода открытых объемов в рыночные позиции | Часть 12 |
21 | SimpleHistoryReceiverExpert.mq5 | 1.00 | Упрощённый советник воспроизведения истории сделок | Часть 16 |
22 | SimpleVolumesExpert.mq5 | 1.20 | Советник для параллельной работы нескольких групп модельных стратегий. Параметры будут браться из встроенной библиотеки групп. | Часть 17 |
23 | SimpleVolumesStage1.mq5 | 1.18 | Советник оптимизации одиночного экземпляра торговой стратегии (Этап 1) | Часть 19 |
24 | SimpleVolumesStage2.mq5 | 1.02 | Советник оптимизации группы экземпляров торговых стратегий (Этап 2) | Часть 19 |
25 | SimpleVolumesStage3.mq5 | 1.02 | Советник, сохраняющий сформированную нормированную группу стратегий в библиотеку групп с заданным именем. | Часть 20 |
26 | SimpleVolumesStrategy.mqh | 1.10 | Класс торговой стратегии с использованием тиковых объемов | Часть 21 |
27 | Strategy.mqh | 1.04 | Базовый класс торговой стратегии | Часть 10 |
28 | SymbolsMonitor.mqh | 1.00 | Класс получения информации о торговых инструментах (символах) | Часть 21 |
29 | TesterHandler.mqh | 1.05 | Класс для обработки событий оптимизации | Часть 19 |
30 | VirtualAdvisor.mqh | 1.08 | Класс эксперта, работающего с виртуальными позициями (ордерами) | Часть 21 |
31 | VirtualChartOrder.mqh | 1.01 | Класс графической виртуальной позиции | Часть 18 |
32 | VirtualFactory.mqh | 1.04 | Класс фабрики объектов | Часть 16 |
33 | VirtualHistoryAdvisor.mqh | 1.00 | Класс эксперта воспроизведения истории сделок | Часть 16 |
34 | VirtualInterface.mqh | 1.00 | Класс графического интерфейса советника | Часть 4 |
35 | VirtualOrder.mqh | 1.08 | Класс виртуальных ордеров и позиций | Часть 21 |
36 | VirtualReceiver.mqh | 1.03 | Класс перевода открытых объемов в рыночные позиции (получатель) | Часть 12 |
37 | VirtualRiskManager.mqh | 1.02 | Класс управления риском (риск-менеждер) | Часть 15 |
38 | VirtualStrategy.mqh | 1.05 | Класс торговой стратегии с виртуальными позициями | Часть 15 |
39 | VirtualStrategyGroup.mqh | 1.00 | Класс группы торговых стратегий или групп торговых стратегий | Часть 11 |
40 | VirtualSymbolReceiver.mqh | 1.00 | Класс символьного получателя | Часть 3 |





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Опубликована статья Разрабатываем мультивалютный советник (Часть 21): Подготовка к важному эксперименту и оптимизация кода:
Author: Yuriy Bykov
Unfortunately, everything is not as simple as we would like. To be able to launch the Expert Advisor of the third stage, it is necessary to specify the IDs of the passes obtained as a result of the previous stages of the optimization pipeline. How to get them is described in the articles.
Understood. However, since you have taken so much of efforts to describe your work in a simpler manner, it will be even great if you could create a video tutorial to teach the operation/optimization of the set of EAs you are creating. Thanks
Understood. However, since you have taken so much of efforts to describe your work in a simpler manner, it will be even great if you could create a video tutorial to teach the operation/optimization of the set of EAs you are creating. Thanks
Hi, thanks for the suggestion. I can't promise that I'll actually be able to record videos for articles, but I'll think about how and in what form I can make a video that helps readers of articles.
Hi, thanks for the suggestion. I can't promise that I'll actually be able to record videos for articles, but I'll think about how and in what form I can make a video that helps readers of articles.
Thank you. A very simple one lasting a few seconds will be sufficient. Since strategy testing and optimization in MT5 is more complex than what used to be in MT4, people who are transitioning find it difficult sometimes. All you can do is showing the exact settings you use in getting those results which you are posting in the articles.
HI Download Last Part Files (21) How I Can User This Advisor Can u Help me please