English 中文 Español Deutsch 日本語 Português
preview
Непрерывная скользящая оптимизация (Часть 8): Доработка программы и исправление найденных недочетов

Непрерывная скользящая оптимизация (Часть 8): Доработка программы и исправление найденных недочетов

MetaTrader 5Тестер | 11 августа 2020, 13:51
2 926 28
Andrey Azatskiy
Andrey Azatskiy

Введение

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

Предшествующие статьи могут быть просмотрены по ссылкам, приведённым ниже: 

  1. Непрерывная скользящая оптимизация (Часть 1): Механизм работы с отчетами оптимизациий 
  2. Непрерывная скользящая оптимизация (Часть 2): Механизм создания отчета оптимизации для любого робота
  3. Непрерывная скользящая оптимизация (Часть 3): Способ адаптации робота к автооптимизатору
  4. Непрерывная скользящая оптимизация (Часть 4): Программа управляющая процессом оптимизации (автооптимизатор)
  5. Непрерывная скользящая оптимизация (Часть 5): Обзор проекта автооптимизатора, а также создание графического интерфейса
  6. Непрерывная скользящая оптимизация (Часть 6): Логическая часть автооптимизатора и его структура 
  7. Непрерывная скользящая оптимизация (Часть 7): Стыковка логической части автооптимизатора с графикой и управление графикой из программы


Добавление автоматического заполнения дат

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

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

 


Рассмотрим полученную стыковку, параллельно приводя примеры её реализации. Начнем наше рассмотрение с графического интерфейса созданного расширения, т.е. все что находится на диаграмме от объекта AutoFillInDateBorders, который представляет графическое окно, и ниже. На изображении указано сопоставление элементов графического интерфейса, его XAML разметки и полей из части ViewModel, представленной классом AutoFillInDateBordersVM. 

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

Значение Value из рассматриваемой таблицы — это шаг соответствующего типа оптимизации в днях. К примеру, если Value исторического интервала равно 360 дней, а форвардного 90, это означает, что выделенный нами в календарях интервал времени будет разбит на ряд исторических оптимизационных интервалов длительностью по 360 дней и форвардных длительностью по 90 дней. Начало каждого следующего окна исторического прохода оптимизации будет смещено на шаг форвардного интервала.  

class AutoFillInDateBordersM : IAutoFillInDateBordersM {     private AutoFillInDateBordersM() { }     private static AutoFillInDateBordersM instance;     public static AutoFillInDateBordersM Instance()     {         if (instance == null)             instance = new AutoFillInDateBordersM();         return instance;     }     public event Action<List<KeyValuePair<OptimisationType, DateTime[]>>> DateBorders;     public void Calculate(DateTime From, DateTime Till, uint history, uint forward)     {         if (From >= Till)             throw new ArgumentException("Date From must be less then date Till");         List<KeyValuePair<OptimisationType, DateTime[]>> data = new List<KeyValuePair<OptimisationType, DateTime[]>>();         OptimisationType type = OptimisationType.History;         DateTime _history = From;         DateTime _forward = From.AddDays(history + 1);         DateTime CalcEndDate()         {             return type == OptimisationType.History ? _history.AddDays(history) : _forward.AddDays(forward);         }            while (CalcEndDate() <= Till)         {             DateTime from = type == OptimisationType.History ? _history : _forward;             data.Add(new KeyValuePair<OptimisationType, DateTime[]>(type, new DateTime[2] { from, CalcEndDate() }));             if (type == OptimisationType.History)                 _history = _history.AddDays(forward + 1);             else                 _forward = _forward.AddDays(forward + 1);             type = type == OptimisationType.History ? OptimisationType.Forward : OptimisationType.History;         }         if (data.Count == 0)             throw new ArgumentException("Can`t create any date borders with setted In sample (History) step");         DateBorders?.Invoke(data);     } }

Класс модели данных рассматриваемого окна является объектом, написанным с применением паттерна Singletone. Это требуется для того, чтобы часть ViewModel основного окна могла взаимодействовать с моделью данных, минуя графическое окно рассматриваемой надстройки. Из интересных методов данный объект содержит лишь метод "Calculate", задача которого в подсчете самих диапазонов дат, и событие, вызываемое после завершения упомянутой процедуры. Событие принимает в качестве параметра коллекцию парных значений, где ключом является тип рассматриваемого интервала (форвардная или же историческая оптимизация), а значением массив из двух значений типа DateTime. Первая дата указывает на начало выбранного интервала, а вторая на дату его завершения.

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

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

Фабрика модели данных нашего дополнения, реализован наипростейшим образом:

class AutoFillInDateBordersCreator {     public static IAutoFillInDateBordersM Model => AutoFillInDateBordersM.Instance(); }

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

public AutoOptimiserVM() {     ...     AutoFillInDateBordersCreator.Model.DateBorders += Model_DateBorders;     .... } ~AutoOptimiserVM() {     ...     AutoFillInDateBordersCreator.Model.DateBorders -= Model_DateBorders;     .... }

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

void _AddDateBorder(DateTime From, DateTime Till, OptimisationType DateBorderType) {         try     {         DateBorders border = new DateBorders(From, Till);         if (!DateBorders.Where(x => x.BorderType == DateBorderType).Any(y => y.DateBorders == border))         {             DateBorders.Add(new DateBordersItem(border, _DeleteDateBorder, DateBorderType));         }     }     catch (Exception e)     {         System.Windows.MessageBox.Show(e.Message);     } }

Создание объекта DateBorder обернуто конструкцию try - catch, это сделано от того, что в конструкторе данного объекта может возникнуть исключение и посему его нужно как-то обрабатывать. Также из нововведений был добавлен метод ClearDateBorders, реализованный следующим образом: 

ClearDateBorders = new RelayCommand((object o) => {     DateBorders.Clear(); });

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

При клике на кнопку Autoset срабатывает коллбек, вызывающий метод Open на экземпляре класса SubFormKeeper. Данный класс был написан как обертка, инкапсулирующая процесс создания вложенных окон нашего приложения. Это нужно для того, чтобы разгрузить ViewModel основного окна от лишних свойств и полей, а также избавить нас от лишнего соблазна напрямую обращаться к создаваемому вспомогательному окну, ведь мы не должны взаимодействовать с ним напрямую. 

class SubFormKeeper {     public SubFormKeeper(Func<Window> createWindow, Action<Window> subscribe_events = null, Action<Window> unSubscribe_events = null);     public void Open();     public void Close(); }

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


Нововведения и исправление ошибок в библиотеке работы с результатами оптимизаций

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

  • Введение пользовательского коэффициента оптимизаций

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

Первым делом в перечисление "SortBy" был добавлен новый параметр "Custom", а также в структуру "Coefficients" было занесено соответствующее поле. Сделав это, мы добавили пользовательский коэффициент в объекты, отвечающие за хранение данных, однако не добавили его в объекты, занимающиеся выгрузкой данных и их чтением. За запись данных отвечают два метода и один класс со статическими методами, который используется из MQL5 для сохранения отчетов.

public static void AppendMainCoef(double customCoef,                                   double payoff,                                   double profitFactor,                                   double averageProfitFactor,                                   double recoveryFactor,                                   double averageRecoveryFactor,                                   int totalTrades,                                   double pl,                                   double dd,                                   double altmanZScore) {     ReportItem.OptimisationCoefficients.Custom = customCoef;     ... }

Первое что было сделано, это в метод AppendMainCoef был добавлен новый параметр, идентифицирующий пользовательский коэффициент. Далее он, как и другие переданные коэффициенты, добавляется в структуру ReportWriter.ReportItem. Теперь при попытке скомпилировать старый проект с новой библиотекой "ReportManager.dll", мы получим исключение, ведь мы изменили сигнатуру метода AppendMainCoef. Однако немного подредактировав объект, занимающийся выгрузкой данных, мы исправили эту ошибку, к коду на MQL5 мы перейдем несколько позже.

Стоит сказать сразу, что для корректной компиляции с текущей версией dll библиотеки нужно заменить папку "History Manager", располагаемую в директории Include,на новую из архива к текущей статье, этого будет достаточно для компиляции роботов со старым и новым способами выгрузки данных.   

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

Для того чтобы в файле с отчетом появилась новая запись, нам нужно создать новый тег <Item/> с атрибутом Name, равным значению "Custom". 

WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.Custom.ToString(), new Dictionary<string, string> { { "Name", "Custom" } });

Также изменения постигли метод OptimisationResultsExtentions.ReportWriter, где была добавлена схожая строка кода, которая, как и в методе ReportWriter.Write, добавляет тег <Item/> с параметром пользовательского коэффициента. 

Теперь стоит рассмотреть добавление пользовательских коэффициентов в выгрузку и код, работающий на стороне робота, написанный на языке MQL5. Первым делом рассмотрим старый вариант выгрузки данных, где та часть кода, что работает с классом ReportWriter, находится в классе CXmlHistoryWriter в файле XmlHistoryWriter.mqh. Для поддержки пользовательских коэффициентов была создана ссылка на функцию со следующей сигнатурой: 

typedef double(*TCustomFilter)();

А также private поле в упомянутом классе, которое должно хранить эту функцию.

class CXmlHistoryWriter   { private:    const string      _path_to_file,_mutex_name;    CReportCreator    _report_manager;    TCustomFilter     custom_filter;    void              append_bot_params(const BotParams  &params[]);//    void              append_main_coef(PL_detales &pl_detales,                                       TotalResult &totalResult);//    //double            get_average_coef(CoefChartType type);    void              insert_day(PLDrawdown &day,ENUM_DAY_OF_WEEK day);//    void              append_days_pl();// public:                      CXmlHistoryWriter(string file_name,string mutex_name,                      CCCM *_comission_manager, TCustomFilter filter);//                      CXmlHistoryWriter(string mutex_name,CCCM *_comission_manager, TCustomFilter filter);                     ~CXmlHistoryWriter(void) {_report_manager.Clear();} //    void              Write(const BotParams &params[],datetime start_test,datetime end_test);//   };

Значение данного private поля заполняется из конструкторов класса. Далее, в методе append_main_coef, при вызове статического метода "ReportWriter::AppendMainCoef" из dll библиотеки мы вызываем переданную функцию по её указателю и тем самым получаем значение пользовательского коэффициента.

    Рассмотренный класс не используется напрямую, так как для него существует обертка, описанная в третьей статье, это класс CAutoUploader.

class CAutoUploader   { private:    datetime          From,Till; // даты начала и завершения тестирования    CCCM              *comission_manager; // Менеджер комиссий    BotParams         params[]; // Список параметров    string            mutexName; // Имя мьютекса    TCustomFilter     custom_filter; public:                      CAutoUploader(CCCM *comission_manager, string mutexName, BotParams &params[],                                    TCustomFilter filter);                      CAutoUploader(CCCM *comission_manager, string mutexName, BotParams &params[]);    virtual          ~CAutoUploader(void);    virtual void      OnTick(); // Подсчет дат начала и завершения тестирования   };

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

double EmptyCustomCoefCallback() {return 0;} //+------------------------------------------------------------------+ //| Конструктор                                                      | //+------------------------------------------------------------------+ CAutoUploader::CAutoUploader(CCCM *_comission_manager,string _mutexName,BotParams &_params[], TCustomFilter filter) : comission_manager(_comission_manager),    mutexName(_mutexName),    From(0),    Till(0),    custom_filter(filter)   {    CopyBotParams(params,_params);   } //+------------------------------------------------------------------+ //| Конструктор                                                      | //+------------------------------------------------------------------+ CAutoUploader::CAutoUploader(CCCM *_comission_manager,string _mutexName,BotParams &_params[]) : comission_manager(_comission_manager),    mutexName(_mutexName),    From(0),    Till(0),    custom_filter(EmptyCustomCoefCallback)   {    CopyBotParams(params,_params);   }

Для сохранения старой версии конструктора была создана функция "EmptyCustomCoefCallback", которая просто возвращает нуль в качестве пользовательского коэффициента. И в случае, если был вызван старый конструктор данного класса, мы передаем в класс CXmlHistoryWriter именно эту функцию. Теперь, возвращаясь к нашему примеру, приведенному в статье №4, можно добавить в робота пользовательский коэффициент следующим образом:

//+------------------------------------------------------------------+ //|                                                     SimpleMA.mq5 | //|                        Copyright 2019, MetaQuotes Software Corp. | //|                                             https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2019, MetaQuotes Software Corp." #property link      "https://www.mql5.com" #property version   "1.00" #include <Trade/Trade.mqh> #include <History manager/AutoLoader.mqh> // Include CAutoUploader #define TESTER_ONLY input int ma_fast = 10; // MA fast input int ma_slow = 50; // MA slow input int _sl_ = 20; // SL input int _tp_ = 60; // TP input double _lot_ = 1; // Lot size // Comission and price shift (Article 2) input double _comission_ = 0; // Comission input int _shift_ = 0; // Shift int ma_fast_handle,ma_slow_handle; const double tick_size = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE); CTrade trade; CAutoUploader * auto_optimiser;// Pointer to CAutoUploader class (Article 3) CCCM _comission_manager_;// Comission manager (Article 2) double CulculateMyCustomCoef() {    return 0; } //+------------------------------------------------------------------+ //| Expert initialization function                                   | //+------------------------------------------------------------------+ int OnInit()   { //--- ...    // Add Instance CAutoUploader class (Article3)    auto_optimiser = new CAutoUploader(&_comission_manager_,"SimpleMAMutex",params,CulculateMyCustomCoef); //---    return(INIT_SUCCEEDED);   }   double OnTester()   {    return(CulculateMyCustomCoef());   } //+------------------------------------------------------------------+

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

  • Ускорение выгрузки данных проходов оптимизаций, используемая в новом формате выгрузки данных

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

  1. Чтение файла.  
  2. Сохранение прочтенных данных в оперативную память компьютера. 
  3. Добавление нового прохода оптимизации к прочитанным данным в память. 
  4. Удаление старого файла. 
  5. Создание нового, чистого файла на месте старого. 
  6. Сохранение всего массива данных в созданный файл. 

Именно так работает используемый нами класс XmlDocument из стандартной библиотеки языка C#, что отнимает много времени. Причем количество затрачиваемого времени на данные операции увеличивается по мере разрастания файла. В прошлой версии выгрузки данных это было необходимым злом, ведь мы не могли аккумулировать все данные в одном месте, а сохраняли их по мере завершения оптимизаций. Забегая несколько вперед, стоит сказать, что в текущей версии выгрузки данных они аккумулируются посредством фреймов, и посему мы имеем возможность преобразовать все данные разом в требуемый формат. Для реализации задуманного мы воспользовались уже ранее написанным методом "OptimisationResultsExtentions.ReportWriter". Данный метод является методом расширения для массива проходов оптимизаций. Он, в отличии от метода ReportWriter.Write, не добавляет данные в файл, а создает лишь один файл, куда постепенно записывает строка за строкой все проходы оптимизаций. Как результат, тот массив данных, который при помощи метода ReportWriter.Write, записывался бы несколько минут, пишется при помощи нового способа за пару секунд.  

 Для того, чтобы можно было использовать метод OptimisationResultsExtentions.ReportWriter из MQL5, для него была реализована обертка в классе ReportWriter. 

public class ReportWriter
{
    private static ReportItem ReportItem;
    private static List<OptimisationResult> ReportData = new List<OptimisationResult>();
    public static void AppendToReportData(string symbol, int tf,
                                          ulong StartDT, ulong FinishDT)
    {
        ReportItem.Symbol = symbol;
        ReportItem.TF = tf;
        ReportItem.DateBorders = new DateBorders(StartDT.UnixDTToDT(), FinishDT.UnixDTToDT());

        ReportData.Add(ReportItem);
        ClearReportItem();
    }
    public static void ClearReportItem()
    {
        ReportItem = new ReportItem();
    }
    public static void ClearReportData() { ReportData.Clear(); }
    public static string WriteReportData(string pathToBot, string currency, double balance,
                                         int laverage, string pathToFile)
    {
        try
        {
            ReportData.ReportWriter(pathToBot, currency, balance, laverage, pathToFile);
            ClearReportData();
        }
        catch (Exception e)
        {
            return e.Message;
        }
        ClearReportData();
        return "";
    }
}

В классе ReportWriter было создано поле ReportData, где будет храниться коллекция элементов ReportItem, иначе говоря, это будет коллекция оптимизационных проходов робота. Идея состоит в том, чтобы из MQL5 при помощи ранее описанных в первой статье методов, записать в структуру ReportItem все требуемые данные. Затем, вызвав метод AppendToReportData, добавить их к коллекции проходов оптимизации. Тем самым, мы формируем на стороне C# требуемую коллекцию данных. По завершении добавления всех проходов оптимизации в коллекцию мы вызываем метод WriteReportData, который, используя метод OptimisationResultsExtentions.ReportWriter, осуществляет быстрое формирование отчета оптимизации.

  • Исправление бага

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

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

  • По убыванию — стоит понимать как "Лучшие параметры сверху, худшие снизу"
  • По возрастанию — стоит понимать как "Худшие параметры сверху, а лучшие снизу"

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

private static SortMethod GetSortMethod(SortBy sortBy) {     switch (sortBy)     {         case SortBy.Payoff: return SortMethod.Increasing;         case SortBy.ProfitFactor: return SortMethod.Increasing;         case SortBy.AverageProfitFactor: return SortMethod.Increasing;         case SortBy.RecoveryFactor: return SortMethod.Increasing;         case SortBy.AverageRecoveryFactor: return SortMethod.Increasing;         case SortBy.PL: return SortMethod.Increasing;         case SortBy.DD: return SortMethod.Decreasing;         case SortBy.AltmanZScore: return SortMethod.Decreasing;         case SortBy.TotalTrades: return SortMethod.Increasing;         case SortBy.Q_90: return SortMethod.Decreasing;         case SortBy.Q_95: return SortMethod.Decreasing;         case SortBy.Q_99: return SortMethod.Decreasing;         case SortBy.Mx: return SortMethod.Increasing;         case SortBy.Std: return SortMethod.Decreasing;         case SortBy.MaxProfit: return SortMethod.Increasing;         case SortBy.MaxDD: return SortMethod.Decreasing;         case SortBy.MaxProfitTotalTrades: return SortMethod.Increasing;         case SortBy.MaxDDTotalTrades: return SortMethod.Decreasing;         case SortBy.MaxProfitConsecutivesTrades: return SortMethod.Increasing;         case SortBy.MaxDDConsecutivesTrades: return SortMethod.Decreasing;         case SortBy.AverageDailyProfit_Mn: return SortMethod.Increasing;         case SortBy.AverageDailyDD_Mn: return SortMethod.Decreasing;         case SortBy.AverageDailyProfitTrades_Mn: return SortMethod.Increasing;         case SortBy.AverageDailyDDTrades_Mn: return SortMethod.Decreasing;         case SortBy.AverageDailyProfit_Tu: return SortMethod.Increasing;         case SortBy.AverageDailyDD_Tu: return SortMethod.Decreasing;         case SortBy.AverageDailyProfitTrades_Tu: return SortMethod.Increasing;         case SortBy.AverageDailyDDTrades_Tu: return SortMethod.Decreasing;         case SortBy.AverageDailyProfit_We: return SortMethod.Increasing;         case SortBy.AverageDailyDD_We: return SortMethod.Decreasing;         case SortBy.AverageDailyProfitTrades_We: return SortMethod.Increasing;         case SortBy.AverageDailyDDTrades_We: return SortMethod.Decreasing;         case SortBy.AverageDailyProfit_Th: return SortMethod.Increasing;         case SortBy.AverageDailyDD_Th: return SortMethod.Decreasing;         case SortBy.AverageDailyProfitTrades_Th: return SortMethod.Increasing;         case SortBy.AverageDailyDDTrades_Th: return SortMethod.Decreasing;         case SortBy.AverageDailyProfit_Fr: return SortMethod.Increasing;         case SortBy.AverageDailyDD_Fr: return SortMethod.Decreasing;         case SortBy.AverageDailyProfitTrades_Fr: return SortMethod.Increasing;         case SortBy.AverageDailyDDTrades_Fr: return SortMethod.Decreasing;         default: throw new ArgumentException($"Unaxpected Sortby variable {sortBy}");     } }

Нынешняя реализация, следующая:

private static OrderBy GetSortingDirection(SortBy sortBy) {     switch (sortBy)     {         case SortBy.Custom: return OrderBy.Ascending;         case SortBy.Payoff: return OrderBy.Ascending;         case SortBy.ProfitFactor: return OrderBy.Ascending;        case SortBy.AverageProfitFactor: return OrderBy.Ascending;         case SortBy.RecoveryFactor: return OrderBy.Ascending;         case SortBy.AverageRecoveryFactor: return Or-derBy.Ascending;         case SortBy.PL: return OrderBy.Ascending;         case SortBy.DD: return OrderBy.Ascending;         case SortBy.AltmanZScore: return OrderBy.Descending;         case SortBy.TotalTrades: return OrderBy.Ascending;         case SortBy.Q_90: return OrderBy.Ascending;         case SortBy.Q_95: return OrderBy.Ascending;         case SortBy.Q_99: return OrderBy.Ascending;         case SortBy.Mx: return OrderBy.Ascending;         case SortBy.Std: return OrderBy.Descending;         case SortBy.MaxProfit: return OrderBy.Ascending;         case SortBy.MaxDD: return OrderBy.Ascending;         case SortBy.MaxProfitTotalTrades: return OrderBy.Ascending;         case SortBy.MaxDDTotalTrades: return OrderBy.Descending;         case SortBy.MaxProfitConsecutivesTrades: return OrderBy.Ascending;         case SortBy.MaxDDConsecutivesTrades: return OrderBy.Descending;         case SortBy.AverageDailyProfit_Mn: return OrderBy.Ascending;         case SortBy.AverageDailyDD_Mn: return OrderBy.Descending;         case SortBy.AverageDailyProfitTrades_Mn: return OrderBy.Ascending;         case SortBy.AverageDailyDDTrades_Mn: return OrderBy.Descending;         case SortBy.AverageDailyProfit_Tu: return OrderBy.Ascending;         case SortBy.AverageDailyDD_Tu: return OrderBy.Descending;         case SortBy.AverageDailyProfitTrades_Tu: return OrderBy.Ascending;         case SortBy.AverageDailyDDTrades_Tu: return OrderBy.Descending;         case SortBy.AverageDailyProfit_We: return OrderBy.Ascending;         case SortBy.AverageDailyDD_We: return OrderBy.Descending;         case SortBy.AverageDailyProfitTrades_We: return OrderBy.Ascending;         case SortBy.AverageDailyDDTrades_We: return OrderBy.Descending;         case SortBy.AverageDailyProfit_Th: return OrderBy.Ascending;         case SortBy.AverageDailyDD_Th: return OrderBy.Descending;         case SortBy.AverageDailyProfitTrades_Th: return OrderBy.Ascending;         case SortBy.AverageDailyDDTrades_Th: return OrderBy.Descending;         case SortBy.AverageDailyProfit_Fr: return OrderBy.Ascending;         case SortBy.AverageDailyDD_Fr: return OrderBy.Descending;         case SortBy.AverageDailyProfitTrades_Fr: return OrderBy.Ascending;         case SortBy.AverageDailyDDTrades_Fr: return OrderBy.Descending;         default: throw new ArgumentException($"Unaxpected Sortby variable {sortBy}");     } }

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

// Если минимум меньше нуля - сдвигаемвсе данные на велечину отрицательного минимума
if (mm.Min < 0) {     value += Math.Abs(mm.Min);     mm.Max += Math.Abs(mm.Min); } // Если максимум больше нуля - делаем подсччеты if (mm.Max > 0) {     // В зависимости от метода сортировки - высчитываем коэффициент     if (GetSortingDirection(item.Key) == OrderBy.Descending)     {         // высчитываем коэффициент для сортировки по убыванию         data.SortBy += (1 - value / mm.Max) * coef;     }     else     {         // Высчитываем коэффициент для сортировки по возрастанию         data.SortBy += value / mm.Max * coef;     } }

Значение value — это числовое значение определенного коэффициента. Перед тем как сортировать данные, мы проверяем — не является ли минимальное значение из массива выбранного для сортировки коэффициента отрицательным. Если оно таковым является, переводим все сортируемые значение в положительную плоскость, сдвигая их вверх на величину минимального коэффициента. Таким образом, на выходе имеем массив, колеблющийся в диапазоне [0 ; (Max + |Min|)]. Когда мы рассчитываем результирующий коэффициент, по которому будет производиться итоговая сортировка, мы переводим наш массив данных в диапазон [0 ; 1] путем деления каждого i-того значения на максимальное значение из массива сортируемых данных. Если выбран метод сортировки по убыванию, то мы отнимаем полученное значение от единицы, тем самым переворачивая массив образовавшихся весов. Поэтому прошлый вариант сортировки данных неверен, ведь из-за реализуемой логики мультифакторной сортировки мы просто переворачивали массив весов в обратную последовательность, что не нужно было делать для коэффициентов, отмеченных на прошлых фрагментах кода. Более подробно метод сортировки описан в первой статье. Также для удобства были изменены наименование метода и тип возвращаемого значения на более подходящий, однако это никак не влияет на логику приложения.  

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

if (order == OrderBy.Ascending)     return results.OrderBy(x => x.GetResult(sortingFlags.ElementAt(0))); else     return results.OrderByDescending(x => x.GetResult(sortingFlags.ElementAt(0)));

Текущая выглядит следующим образом:

if (order == GetSortingDirection(sortingFlags.ElementAt(0)))     return results.OrderBy(x => x.GetResult(sortingFlags.ElementAt(0))); else     return results.OrderByDescending(x => x.GetResult(sortingFlags.ElementAt(0)));

Прошлый вариант сортировок не учитывал направления указываемое методом GetSortingDirection. Новая же сортирует, учитывая данный критерий. И к примеру, если мы выбираем сортировку по убыванию (лучшие результаты сверху), то для SortBy.PL, будет производиться сортировка по убыванию, как и было запрошено, и сверху будет наибольшее значение, а вот для параметра SortBy.MaxDDTotalTrades (общее количество убыточных сделок) сверху будет самое малое значение и сортироваться массив будет соответственно не по убыванию, а по возрастанию. Это нужно для того, чтобы сохранить логическую структуру. К примеру, выбери мы в качестве критерия один лишь SortBy.MaxDDTotalTrades  то, при прошлом методе сортировки мы получили бы не самые оптимальные параметры (на которые рассчитывали), а напротив — самые худшие из найденных. 

Автоматизация выгрузки параметров робота и новые правила написания советников

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

class CAutoUploader2   { private:                      CAutoUploader2() {}    static CCCM       comission_manager;    static datetime   From,Till;    static TCustomFilter on_tester;    static TCallback on_tick,           on_tester_deinit;    static TOnTesterInit on_tester_init;    static string     frame_name;    static long       frame_id;    static string     file_name;    static bool       FillInData(Data &data);    static void       UploadData(const Data &data, double custom_coef, const BotParams &params[]); public:    static void       OnTick();    static double     OnTester();    static int        OnTesterInit();    static void       OnTesterDeinit();    static void       SetUploadingFileName(string name);    static void       SetCallback(TCallback callback, ENUM_CALLBACK_TYPE type);    static void       SetCustomCoefCallback(TCustomFilter custom_filter_callback);    static void       SetOnTesterInit(TOnTesterInit on_tester_init_callback);    static void       AddComission(string symbol,double comission,double shift);    static double     GetComission(string symbol,double price,double volume);    static void       RemoveComission(string symbol);   }; datetime CAutoUploader2::From = 0; datetime CAutoUploader2::Till = 0; TCustomFilter CAutoUploader2:: EmptyCustomCoefCallback; TCallback CAutoUploader2:: EmptyCallback; TOnTesterInit CAutoUploader2:: EmptyOnTesterInit; TCallback CAutoUploader2:: EmptyCallback; CCCM CAutoUploader2::comission_manager; string CAutoUploader2::frame_name = "AutoOptomiserFrame"; long CAutoUploader2::frame_id = 1; string CAutoUploader2::file_name = MQLInfoString(MQL_PROGRAM_NAME)+"_Report.xml";  

Новый выгружающий отчеты класс имеет лишь статические методы. Это требуется для того, чтобы не было необходимости его инстанцировать, и тем самым облегчить написание эксперта, убрав лишний код из его реализации. Данный класс имеет ряд статических полей, среди которых границы дат (по аналогии с ранее используемым классом, подробности в статье №3), ссылки на функции для коллбэков окончания тестирования, фреймов оптимизаций, а также коллбэк прихода нового тика, класс менеджера комиссий (подробности в статье #2), имя и id фреймов и? в заключениe, имя файла с выгрузкой оптимизационных результатов.     

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

#ifndef CUSTOM_ON_TESTER double OnTester() { return CAutoUploader2::OnTester(); } #endif #ifndef CUSTOM_ON_TESTER_INIT int OnTesterInit() { return CAutoUploader2::OnTesterInit(); } #endif #ifndef CUSTOM_ON_TESTER_DEINIT void OnTesterDeinit() { CAutoUploader2::OnTesterDeinit(); } #endif #ifndef CUSTOM_ON_TICK void OnTick() { CAutoUploader2::OnTick(); } #endif

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

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

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

static void       AddComission(string symbol,double comission,double shift); static double     GetComission(string symbol,double price,double volume); static void       RemoveComission(string symbol);

Теперь, закончив с презентацией функционала, стоит сказать пару слов о том, как все это работает.

int CAutoUploader2::OnTesterInit(void) { return on_tester_init(); }

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

void CAutoUploader2::OnTick(void)   {    if(MQLInfoInteger(MQL_OPTIMIZATION)==1 ||       MQLInfoInteger(MQL_TESTER)==1)      {       if(From == 0)          From = iTime(_Symbol,PERIOD_M1,0);       Till=iTime(_Symbol,PERIOD_M1,0);      }    on_tick();   }

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

double CAutoUploader2::OnTester(void)   {    double ret = on_tester();    Data data[1];    if(!FillInData(data[0]))       return ret;    if(MQLInfoInteger(MQL_OPTIMIZATION)==1)      {       if(!FrameAdd(frame_name, frame_id, ret, data))          Print(GetLastError());      }    else       if(MQLInfoInteger(MQL_TESTER)==1)         {          BotParams params[];          UploadData(data[0], ret, params, false);         }    return ret;   }

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

input bool close_terminal_after_finishing_optimisation = false; // MetaTrader Auto Optimiser param (must be false if you run it  from terminal) void CAutoUploader2::OnTesterDeinit(void)   {    ResetLastError();    if(FrameFilter(frame_name,frame_id))      {       ulong pass;       string name;       long id;       double coef_value;       Data data[];       while(FrameNext(pass,name,id,coef_value,data))         {          string parameters_list[];          uint params_count;          BotParams params[];          if(FrameInputs(pass,parameters_list,params_count))            {             for(uint i=0; i<params_count; i++)               {                string arr[];                StringSplit(parameters_list[i],'=',arr);                BotParams item;                item.name = arr[0];                item.value = arr[1];                ADD_TO_ARR(params,item);               }            }          else             Print("Can`t get params");          UploadData(data[0], coef_value, params, true);         }       CheckRetMessage(ReportWriter::WriteReportData(get_path_to_expert(),                       CharArrayToString(data[0].currency),                       data[0].balance,                       data[0].laverage,                       TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+file_name));      }    else      {       Print("Can`t select apropriate frames. Error code = " + IntegerToString(GetLastError()));       ResetLastError();      }    on_tester_deinit();    if(close_terminal_after_finishing_optimisation)      {       if(!TerminalClose(0))         {          Print("===================================");          Print("Can`t close terminal from OnTesterDeinit error number: " +                IntegerToString(GetLastError()) +                " Close it by hands");          Print("===================================");         }      }    ExpertRemove();   }

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

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

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

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

void CAutoUploader2::UploadData(const Data &data, double custom_coef, const BotParams &params[], bool is_appent_to_collection)   {    int total = ArraySize(params);    for(int i=0; i<total; i++)       ReportWriter::AppendBotParam(params[i].name,params[i].value);    ReportWriter::AppendMainCoef(custom_coef,data.payoff,data.profitFactor,data.averageProfitFactor,                                 data.recoveryFactor,data.averageRecoveryFactor,data.totalTrades,                                 data.pl,data.dd,data.altmanZScore);    ReportWriter::AppendVaR(data.var_90,data.var_95,data.var_99,data.mx,data.std);    ReportWriter::AppendMaxPLDD(data.max_profit,data.max_dd,                                data.totalProfitTrades,data.totalLooseTrades,                                data.consecutiveWins,data.consequtiveLoose);    ReportWriter::AppendDay(MONDAY,data.averagePl_mn,data.averageDd_mn,                            data.numberProfitTrades_mn,data.numberLooseTrades_mn);    ReportWriter::AppendDay(TUESDAY,data.averagePl_tu,data.averageDd_tu,                            data.numberProfitTrades_tu,data.numberLooseTrades_tu);    ReportWriter::AppendDay(WEDNESDAY,data.averagePl_we,data.averageDd_we,                            data.numberProfitTrades_we,data.numberLooseTrades_we);    ReportWriter::AppendDay(THURSDAY,data.averagePl_th,data.averageDd_th,                            data.numberProfitTrades_th,data.numberLooseTrades_th);    ReportWriter::AppendDay(FRIDAY,data.averagePl_fr,data.averageDd_fr,                            data.numberProfitTrades_fr,data.numberLooseTrades_fr);    if(is_appent_to_collection)      {       ReportWriter::AppendToReportData(_Symbol,                                        data.tf,                                        data.startDT,                                        data.finishDT);       return;      }    CheckRetMessage(ReportWriter::Write(get_path_to_expert(),                                        CharArrayToString(data.currency),                                        data.balance,                                        data.laverage,                                        TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+file_name,                                        _Symbol,                                        data.tf,                                        data.startDT,                                        data.finishDT));   }

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

Переходя к конкретному примеру добавления ссылки на выгрузку отчетов оптимизации при помощи новой логики, рассмотрим ранее созданный файл с тестовым экспертом из четвертой статьи. Если не считать ссылки на подключаемый файл, то подключение нового метода выгрузки занимает всего 3 строки кода, вместо 16 строк из примера, приведенного в статье №4. Если говорить о коллбэках, используемых нами при выгрузке данных, то в текущем примере в самом эксперте осталась реализация коллбэка "OnTick", а остальные коллбеки ("OnTester", "OnTesterInit", "OnTesterDeinit"), остались реализованными в подключаемом файле. 

//+------------------------------------------------------------------+ //|                                                     SimpleMA.mq5 | //|                        Copyright 2019, MetaQuotes Software Corp. | //|                                             https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2019, MetaQuotes Software Corp." #property link      "https://www.mql5.com" #property version   "1.00" #include <Trade/Trade.mqh> #define CUSTOM_ON_TICK // Tell to uploading system that we implement OnTick callback ourself #include <History manager/AutoUpLoader2.mqh> // Include CAutoUploader #define TESTER_ONLY input int ma_fast = 10; // MA fast input int ma_slow = 50; // MA slow input int _sl_ = 20; // SL input int _tp_ = 60; // TP input double _lot_ = 1; // Lot size // Comission and price shift (Article 2) input double _comission_ = 0; // Comission input int _shift_ = 0; // Shift int ma_fast_handle,ma_slow_handle; const double tick_size = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE); CTrade trade; //+------------------------------------------------------------------+ //| Custom coeffifient`s creator                                     | //+------------------------------------------------------------------+ double CulculateMyCustomCoef()   {    return 0;   } //+------------------------------------------------------------------+ //| Expert initialization function                                   | //+------------------------------------------------------------------+ int OnInit()   { //--- ...    CAutoUploader2::SetCustomCoefCallback(CulculateMyCustomCoef);    CAutoUploader2::AddComission(_Symbol,_comission_,_shift_); //---    return(INIT_SUCCEEDED);   } //+------------------------------------------------------------------+ //| Expert tick function                                             | //+------------------------------------------------------------------+ void OnTick()   {    CAutoUploader2::OnTick(); // If CUSTOM_ON_TICK was defined    ...   } //+------------------------------------------------------------------+

Красным цветом выделено добавление интерфейса выгрузки данных из эксперта в файл по новому типу выгрузки. Как видно из примера, коллбэк OnTester остался реализованным в файле выгружающем данные, и для того чтобы производился расчет нашего пользовательского коэффициента, в него был передан метод "CulculateMyCustomCoef", где должна содержаться пользовательская логика реализации данного коллбэка. Коллбэк OnTick для примера был оставлен реализованным в роботе. Именно для этого перед ссылкой на файл, где описана процедура выгрузки данных, определена переменная CUSTOM_ON_TICK. Более подробно изучить реализацию данного робота, а также сравнить его с дефолтной реализацией (где не подключена выгрузка для автооптимизатора) и с вариантом реализации с прошлым способом выгрузки данных, можно изучив соответствующие файлы из приложенного к статье архива. 

Изменение способа запуска оптимизаций и иные улучшения

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

  • Планирование оптимизации на переданном списке активов

 

Данное улучшение позволяет экономить время, производя оптимизации на ряде активов, ведь запланированные задачи будут работать и днем, и ночью, пока не закончится заданный список. Однако для его создания пришлось несколько видоизменить способ запуска процесса оптимизаций, описанный в прошлых статьях. Ранее, после нажатия на кнопку "Start/Stop" ViewModel сразу же переадресовывала задание в метод модели данных, вызывающий полный цикл от запуска оптимизации, до сохранения полученных результатов. Теперь же, мы сперва вызываем метод, который в цикле перебирает переданный список параметров, а затем уже запускает оптимизации и последующее их сохранение в соответствующую директорию.    

public async void StartOptimisation(OptimiserInputData optimiserInputData, bool isAppend, string dirPrefix, List<string> assets) {     if (assets.Count == 0)     {         ThrowException("Fill in asset name");         OnPropertyChanged("ResumeEnablingTogle");         return;     }     await Task.Run(() =>     {         try         {             if (optimiserInputData.OptimisationMode == ENUM_OptimisationMode.Disabled &&                assets.Count > 1)             {                 throw new Exception("For test there mast be selected only one asset");             }             StopOptimisationTougle = false;             bool doWhile()             {                 if (assets.Count == 0 || StopOptimisationTougle)                     return false;                 optimiserInputData.Symb = assets.First();                 LoadingOptimisationTougle = assets.Count == 1;                 assets.Remove(assets.First());                 return true;             }             while (doWhile())             {                 var data = optimiserInputData; // Copy input data                 StartOptimisation(data, isAppend, dirPrefix);             }         }         catch (Exception e)         {             LoadingOptimisationTougle = true;             OnPropertyChanged("ResumeEnablingTogle");м             ThrowException?.Invoke(e.Message);         }     }); }

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

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

/// <summary> /// Завершаем оптимизацию извне оптимизатора /// </summary> public void StopOptimisation() {     StopOptimisationTougle = true;     LoadingOptimisationTougle = true;     Optimiser.Stop();     var processes = System.Diagnostics.Process.GetProcesses().Where(x => x.ProcessName == "metatester64");     foreach (var item in processes)         item.Kill(); } bool StopOptimisationTougle = false;

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

Как можно заметить из приведенного фрагмента кода, был введен еще один флаг "LoadingOptimisationTougle". Данный флаг указывает на то, нужно ли загружать текущую произведенную оптимизацию в графический интерфейс, как это было реализовано ранее. Для ускорения процесса данный флаг всегда равняется “false” до тех пор, пока не процесс не будет остановлен силой, либо пока не будет достигнут последний элемент из переданного списка активов. И лишь после этого, в момент выхода из процесса оптимизации, будут загружены данные в графический интерфейс. 

  • Сохранение файла конфигураций с параметрами запуска оптимизации и очистка памяти загруженных оптимизаций

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

После клика на данную кнопку срабатывает следующий метод класса AutoOptimiserVM:

private void SetBotParams()
{
    if (string.IsNullOrEmpty(SelectedOptimisation))
        return;

    try
    {
        Status = "Filling bot params";
        OnPropertyChanged("Status");
        Progress = 100;
        OnPropertyChanged("Progress");

        var botParams = model.GetBotParamsFromOptimisationPass(OptimiserSettings.First(x => x.Name == "Available experts").SelectedParam,
                                                                       SelectedOptimisation);
        for (int i = 0; i < BotParams.Count; i++)
        {
            if (!botParams.Any(x => x.Variable == BotParams[i].Vriable))
                continue;

            BotParams[i] = new BotParamsData(botParams.First(x => x.Variable == BotParams[i].Vriable));
        }
    }
    catch (Exception e)
    {
        MessageBox.Show(e.Message);
    }

    Status = null;
    OnPropertyChanged("Status");
    Progress = 0;
    OnPropertyChanged("Progress")
}

Сперва мы запрашиваем у модели данных, список параметров робота. Затем в цикле пробегаемся по всем параметрам, загруженным в графический интерфейс и проверяем, содержится ли этот параметр в списке полученных параметров. Если параметр был найден, то он замещается в списке текущих параметров на новый. Метод из модели данных, возвращающий корректные параметры файла настроек, читает их из директории выбранной в ComboBox из списка оптимизаций, где хранится файл с именем "OptimisationSettings.set". Данный файл формируется методом, запускающим оптимизации, по завершении данного процесса. 

  • Очистка памяти от ранее загруженных оптимизационных проходов

Также была добавлена опция очистки проходов оптимизаций после их загрузки. Дело в том, что они занимают много места в оперативной памяти и, если компьютер обладает низким объемом оперативной памяти, порой, при большом объеме форвардных и исторических тестов, она может забиться так, что компьютер начнет заметно зависать. Для минимизации занимаемых ресурсов убрано дублирование данных о форвардных и исторических проходах оптимизации. Теперь они хранятся только лишь в модели данных. Затем была добавлена кнопка в графическом интерфейсе "Clear loaded results", которая ссылается на метод "ClearResults" из модели данных. 

void ClearOptimisationFields()
{
    if (HistoryOptimisations.Count > 0)
        dispatcher.Invoke(() => HistoryOptimisations.Clear());
    if (ForwardOptimisations.Count > 0)
        dispatcher.Invoke(() => ForwardOptimisations.Clear());
    if (AllOptimisationResults.AllOptimisationResults.Count > 0)
    {
        AllOptimisationResults.AllOptimisationResults.Clear();
        AllOptimisationResults = new ReportData
        {
            AllOptimisationResults = new Dictionary<DateBorders, List<OptimisationResult>>()
        };
    }

    GC.Collect();
}
public void ClearResults()
{
    ClearOptimisationFields();
    OnPropertyChanged("AllOptimisationResults");
    OnPropertyChanged("ClearResults");
}

Упомянутый метод ссылается на private метод "ClearOptimisationFields", который чистит коллекции в классе AutoOptimiserM, что содержат загруженные отчеты оптимизаций. Однако, так как мы имеем дело с C#, где управление памятью осуществляется не вручную, а автоматически, для применения очистки массива и удаления данных из памяти сразу же после очистки нам нужно очистить и память от всех удаленных объектов. Для этого мы вызываем статический метод "Collect" класса Garbige Collector (GC). После произведенных действий оперативная память очищается от ранее занимаемых её объектов.

  • Формирование (*.set) файла интересующего оптимизационного прохода.

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

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

public void SaveBotParams(IEnumerable<KeyValuePair<string, string>> data, string path)
{
    SetFileManager setFileManager = new SetFileManager(path, true)
    {
        Params = data.Select(x => new ParamsItem { Variable = x.Key, Value = x.Value }).ToList()
    };

    setFileManager.SaveParams();
}

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


Заключение

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


В приложении находится полный проект автооптимизатора с тестовым роботом, рассмотренным в статье №4. Все, что необходимо сделать для его использования — это скомпилировать файлы проекта автооптимизатора и тестового робота. Затем нужно скопировать ReportManager.dll (реализация которой описывается в первой статье) в директорию MQL5/Libraries, и можно приступать к тестам полученной стыковки. О том, как подключить втооптимизацию ваших экспертов, уже рассказано в 3 и 4 частях данной серии статей.

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

  1. Самый простой — нажать сочетание клавиш CTRL+SHIFT+B,

  1. Более визуальный — нажать на зеленую стрелочку в редакторе, произойдет запуск приложения в режиме отладки кода, но компиляция пройдет тоже (сработает без проблем, только если будет выбран режим компиляции Debug), 

  1. Еще один вариант — из выпадающего меню пункт Build.


Прикрепленные файлы |
Data.zip (144.68 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (28)
EDUARDO RODRIGUES NASCIMENTO
EDUARDO RODRIGUES NASCIMENTO | 27 апр. 2022 в 21:21
If i compile the .mq5 files you sent from your expert advisor named "New uploading variant" its showing a lot of errors as you can see in the image below. Ive just downloaded the lastest version posted (8th article).

What shoul i do to solve this?





Regards
Good Beer
Good Beer | 14 мая 2023 в 19:27

Здравствуйте, Андрей. Я решил вернуться к поискам грааля и был рад найти ваш оптимизатор работающим и доработанным. Решения, собранные полностью на Mql5 оказались требовательны к поддержке автора и сошли с дистанции. А ваш оптимизатор будет работать пока есть С# и окно тестера стратегий в МТ5 будет неизменным. Странно, что нет массового спроса на бэк-форвард оптимизацию. В любом случае, огромное вам спасибо за труд!


Тестер уже рабочий, но я озвучу несколько пожеланий. Может даже кто другой продолжит доработку проекта и выложит в кодебазу.
1. Хотелось бы всплывающие подсказки с пояснениями, в заголовке таблиц с результатами. Что такое: var 90, var 95, mx....
2. Значения PL и DD в окнах 1 и 3 вкладки "results" не совпадают. Ну, и единицы измерения?
3. Функция OnTester() не компилируется, потому что определена в файлах оптимизатора, так что совсем кастомные критерии отменяются. Из стандартных программа позволяет набрать любой набор.
4. Как же без графиков баланса? Можно же склеить все форварды и запустить на тестере, напр. А лучше - грубый график в окне оптимизатора сразу, для оценки достойности советника дальнейшей траты времени. Как минимум, в оптимизаторе должен отряжаться итоговый результат всех форвард-проходов.
5. Расчёт прибыли в пипсах нужен. Особенно учитывая, как тестер работает с криптой. Добавить чекбокс.
6. Ну и напоследок, помечтаю о возможности добавления нескольких ТФ. Подобно возможности добавления нескольких активов.
Ну, и много, много денег....

Andrey Azatskiy
Andrey Azatskiy | 8 июн. 2023 в 22:44
Good Beer #:

Здравствуйте, Андрей. Я решил вернуться к поискам грааля и был рад найти ваш оптимизатор работающим и доработанным. Решения, собранные полностью на Mql5 оказались требовательны к поддержке автора и сошли с дистанции. А ваш оптимизатор будет работать пока есть С# и окно тестера стратегий в МТ5 будет неизменным. Странно, что нет массового спроса на бэк-форвард оптимизацию. В любом случае, огромное вам спасибо за труд!


Тестер уже рабочий, но я озвучу несколько пожеланий. Может даже кто другой продолжит доработку проекта и выложит в кодебазу.
1. Хотелось бы всплывающие подсказки с пояснениями, в заголовке таблиц с результатами. Что такое: var 90, var 95, mx....
2. Значения PL и DD в окнах 1 и 3 вкладки "results" не совпадают. Ну, и единицы измерения?
3. Функция OnTester() не компилируется, потому что определена в файлах оптимизатора, так что совсем кастомные критерии отменяются. Из стандартных программа позволяет набрать любой набор.
4. Как же без графиков баланса? Можно же склеить все форварды и запустить на тестере, напр. А лучше - грубый график в окне оптимизатора сразу, для оценки достойности советника дальнейшей траты времени. Как минимум, в оптимизаторе должен отряжаться итоговый результат всех форвард-проходов.
5. Расчёт прибыли в пипсах нужен. Особенно учитывая, как тестер работает с криптой. Добавить чекбокс.
6. Ну и напоследок, помечтаю о возможности добавления нескольких ТФ. Подобно возможности добавления нескольких активов.
Ну, и много, много денег....

Благодарю за отзыв, я не поддерживаю данный проект, но да он должен быть рабочим долгое время. 

Если у кого либо будет желание доработать, подправить  проект - то занимайтесь)
https://github.com/AndreyKrivcov/MetaTrader-Auto-Optimiser

Artyom Trishkin
Artyom Trishkin | 9 июн. 2023 в 19:08
EDUARDO RODRIGUES NASCIMENTO #:
If i compile the .mq5 files you sent from your expert advisor named "New uploading variant" its showing a lot of errors as you can see in the image below. Ive just downloaded the lastest version posted (8th article).

What shoul i do to solve this?





Regards

Итак. Загружаем себе архив, прилагаемый к статье и видим в нём две папки:

So. We download the archive attached to the article and see two folders in it:


Папку MetaTrader-Auto-Optimiser переносим из архива в корневой каталог, где расположен MetaTrader 5:

Move the MetaTrader-Auto-Optimiser folder from the archive to the root directory where MetaTrader 5 is located:


В архиве, в папке MQL5 лежат две папки - их копируем в папку MQL5 Вашего терминала. Соответственно - в папку MQL5\Experts будет скопирована папка Test Expert, а в папку MQL\Include будут скопированы две папки: CustomGeneric и History manager.

Компилируем файл SimpleMA.mq5, лежащий в папке Experts\Test Expert\New uploading variant:

There are two folders in the archive, in the MQL5 folder - copy them to the MQL5 folder of your terminal. Accordingly, the Test Expert folder will be copied to the MQL5\Experts folder, and two folders will be copied to the MQL\Include folder: CustomGeneric and History manager.

Compile the SimpleMA.mq5 file located in the Experts\Test Expert\New uploading variant folder:


Получаем 100 ошибок и 60 предупреждений:

We get 100 errors and 60 warnings:


Переходим к самой первой ошибке и видим, что это не закрытый импорт:

Let's move on to the very first error and see that this is not a closed import:


Дважды щёлкаем по надписи об ошибке и попадаем в файл UploadersEntities.mqh на строку с ошибкой:

Double-click on the inscription about the error and get into the UploadersEntities.mqh file on the line with the error:


Что видим? А видим действительно не закрытый импорт. Исправляем:

What do we see? And we see really not closed import. We fix:

//+------------------------------------------------------------------+
//|                                            UploadersEntities.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"

#include "ReportCreator.mqh"
#import "ReportManager.dll"
#import
//+------------------------------------------------------------------+
//| Структура хранящая данные по входным параметрам                  |
//+------------------------------------------------------------------+
struct BotParams
  {
   string            name,value;
  };

// Добавление нового значения к динамичесому массиву
#define ADD_TO_ARR(arr, value) \
{\
   int s = ArraySize(arr);\
   ArrayResize(arr,s+1,s+1);\
   arr[s] = value;\
}

// добавление нового параметра робота к динамическому массиву параметрв
#define APPEND_BOT_PARAM(Var,BotParamArr) \
{\
   BotParams param;\
   param.name = #Var;\
   param.value = (string)Var;\
   \
   ADD_TO_ARR(BotParamArr,param);\
}

//+------------------------------------------------------------------+
//| Функция копирующая список массивов                               |
//+------------------------------------------------------------------+
void CopyBotParams(BotParams &dest[], const BotParams &src[])
  {
   int total = ArraySize(src);
   for(int i=0; i<total; i++)
     {
      ADD_TO_ARR(dest,src[i]);
     }
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
double GetAverageCoef(CoefChartType type, CReportCreator &report_manager)
  {
   CoefChart_item coef_chart[];
   report_manager.GetCoefChart(false,type,coef_chart);

   double ans= 0;
   int total = ArraySize(coef_chart);
   for(int i=0; i<total; i++)
      ans+=coef_chart[i].coef;

   ArrayFree(coef_chart);
   return (ans/(double)total);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
string get_path_to_expert(void)
  {
   string arr[];
   StringSplit(MQLInfoString(MQL_PROGRAM_PATH),'\\',arr);
   string relative_dir=NULL;

   int total= ArraySize(arr);
   bool save= false;
   for(int i=0; i<total; i++)
     {
      if(save)
        {
         if(relative_dir== NULL)
            relative_dir=arr[i];
         else
            relative_dir+="\\"+arr[i];
        }

      if(StringCompare("Experts",arr[i])==0)
         save=true;
     }

   return relative_dir;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
typedef void(*TCallback)();
typedef double(*TCustomFilter)();
typedef int (*TOnTesterInit)();


//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void EmptyCallback() {}
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
double EmptyCustomCoefCallback() {return 0;}
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int EmptyOnTesterInit() {return(INIT_SUCCEEDED);}

enum ENUM_CALLBACK_TYPE
  {
   CB_ON_TICK,
   CB_ON_TESTER_DEINIT
  };

struct Data
  {
   int tf, // ReportItem.TF
       laverage, // ReportReader.Laverage
       totalTrades, // ReportItem.OptimisationCoefficients.TotalTrades
       totalProfitTrades, // ReportItem.OptimisationCoefficients.MaxPLDD.Profit.TotalTrades
       totalLooseTrades, // ReportItem.OptimisationCoefficients.MaxPLDD.DD.TotalTrades
       consecutiveWins, // ReportItem.OptimisationCoefficients.MaxPLDD.Profit.ConsecutivesTrades
       consequtiveLoose, // ReportItem.OptimisationCoefficients.MaxPLDD.DD.ConsecutivesTrades
       numberProfitTrades_mn, // ReportItem.OptimisationCoefficients.TradingDays[Mn].Profit.Trades
       numberProfitTrades_tu, // ReportItem.OptimisationCoefficients.TradingDays[Tu].Profit.Trades
       numberProfitTrades_we, // ReportItem.OptimisationCoefficients.TradingDays[We].Profit.Trades
       numberProfitTrades_th, // ReportItem.OptimisationCoefficients.TradingDays[Th].Profit.Trades
       numberProfitTrades_fr, // ReportItem.OptimisationCoefficients.TradingDays[Fr].Profit.Trades
       numberLooseTrades_mn, // ReportItem.OptimisationCoefficients.TradingDays[Mn].DD.Trades
       numberLooseTrades_tu, // ReportItem.OptimisationCoefficients.TradingDays[Tu].DD.Trades
       numberLooseTrades_we, // ReportItem.OptimisationCoefficients.TradingDays[We].DD.Trades
       numberLooseTrades_th, // ReportItem.OptimisationCoefficients.TradingDays[Th].DD.Trades
       numberLooseTrades_fr; // ReportItem.OptimisationCoefficients.TradingDays[Fr].DD.Trades
   ulong startDT, // ReportItem.DateBorders.From
         finishDT; // ReportItem.DateBorders.Till
   double payoff, // ReportItem.OptimisationCoefficients.Payoff
          profitFactor, // ReportItem.OptimisationCoefficients.ProfitFactor
          averageProfitFactor, // ReportItem.OptimisationCoefficients.AverageProfitFactor
          recoveryFactor, // ReportItem.OptimisationCoefficients.RecoveryFactor
          averageRecoveryFactor, // ReportItem.OptimisationCoefficients.AverageRecoveryFactor
          pl, // ReportItem.OptimisationCoefficients.PL
          dd, // ReportItem.OptimisationCoefficients.DD
          altmanZScore, // ReportItem.OptimisationCoefficients.AltmanZScore
          var_90, // ReportItem.OptimisationCoefficients.VaR.Q_90
          var_95, // ReportItem.OptimisationCoefficients.VaR.Q_95
          var_99, // ReportItem.OptimisationCoefficients.VaR.Q_99
          mx, // ReportItem.OptimisationCoefficients.VaR.Mx
          std, // ReportItem.OptimisationCoefficients.VaR.Std
          max_profit, // ReportItem.OptimisationCoefficients.MaxPLDD.Profit.Value
          max_dd, // ReportItem.OptimisationCoefficients.MaxPLDD.DD.Value
          averagePl_mn, // ReportItem.OptimisationCoefficients.TradingDays[Mn].Profit.Value
          averagePl_tu, // ReportItem.OptimisationCoefficients.TradingDays[Tu].Profit.Value
          averagePl_we, // ReportItem.OptimisationCoefficients.TradingDays[We].Profit.Value
          averagePl_th, // ReportItem.OptimisationCoefficients.TradingDays[Th].Profit.Value
          averagePl_fr, // ReportItem.OptimisationCoefficients.TradingDays[Fr].Profit.Value
          averageDd_mn, // ReportItem.OptimisationCoefficients.TradingDays[Mn].DD.Value
          averageDd_tu, // ReportItem.OptimisationCoefficients.TradingDays[Tu].DD.Value
          averageDd_we, // ReportItem.OptimisationCoefficients.TradingDays[We].DD.Value
          averageDd_th, // ReportItem.OptimisationCoefficients.TradingDays[Th].DD.Value
          averageDd_fr, // ReportItem.OptimisationCoefficients.TradingDays[Fr].DD.Value
          balance; // ReportReader.Balance
   char              currency[100];
  };
//+------------------------------------------------------------------+

Компилируем опять. Ошибка импорта ушла, но теперь компилятор не видит функций и методов из импортированного файла:

Let's compile again. The import error is gone, but now the compiler does not see the functions and methods from the imported file:


Вспоминаем, что мы импортируем методы и классы из сторонней dll. И она должна лежать в папке MQL5\Libraries.

Открываем папку MetaTrader-Auto-Optimiser, скопированную из архива в корневой каталог MetaTrader 5. Видим в ней файл Metatrader Auto Optimiser.sln:

We recall that we are importing methods and classes from a third-party dll. And it should be in the MQL5\Libraries folder.

Open the MetaTrader-Auto-Optimiser folder copied from the archive to the MetaTrader 5 root directory. We see the file Metatrader Auto Optimiser.sln in it:


Дважды щёлкаем по этому файлу, чтобы открыть проект в MS Visual Studio.

При открытии проекта видим, что он для устаревшей платформы:

Double click on this file to open the project in MS Visual Studio.

When opening the project, we see that it is for an outdated platform:


Оставляем флажок на "Обновить цель до платформы .NET Framefork 4.8" и жмём кнопку "Продолжить".

Далее ещё раз для второго проекта:

Leave the checkbox "Upgrade target to .NET Framefork 4.8 platform" and click the "Continue" button.

Then again for the second project:


После загрузки проектов выбираем "Релиз" и Any CPU:

After loading the projects, select "Release" and Any CPU:


И нажимаем Ctrl+F5 для компиляции и сборки проектов.

После компиляции проекта в MS Visual Studio идём в корневой каталог терминала и в нём в папку \MetaTrader-Auto-Optimiser\ReportManager\bin\Release. Копируем из этой папки файл собранной библиотеки ReportManager.dll в каталог терминала MQL5\Libraries.

Теперь опять компилируем файл SimpleMA.mq5 из папки MQL5\Experts\Test Expert\New uploading variant.

Готово, ошибок нет:

And press Ctrl+F5 to compile and build projects.

After compiling the project in MS Visual Studio, go to the root directory of the terminal and in it to the \MetaTrader-Auto-Optimiser\ReportManager\bin\Release folder. Copy the compiled library file ReportManager.dll from this folder to the MQL5\Libraries directory of the terminal.

Now let's compile the SimpleMA.mq5 file from the MQL5\Experts\Test Expert\New uploading variant folder again.

Done, no errors:


Enjoy

Rashid Umarov
Rashid Umarov | 12 июн. 2023 в 12:21
Artyom Trishkin #:

Итак. Загружаем себе архив, прилагаемый к статье и видим в нём две папки:

So. We download the archive attached to the article and see two folders in it:


Папку MetaTrader-Auto-Optimiser переносим из архива в корневой каталог, где расположен MetaTrader 5:

Move the MetaTrader-Auto-Optimiser folder from the archive to the root directory where MetaTrader 5 is located:


В архиве, в папке MQL5 лежат две папки - их копируем в папку MQL5 Вашего терминала. Соответственно - в папку MQL5\Experts будет скопирована папка Test Expert, а в папку MQL\Include будут скопированы две папки: CustomGeneric и History manager.

Компилируем файл SimpleMA.mq5, лежащий в папке Experts\Test Expert\New uploading variant:

There are two folders in the archive, in the MQL5 folder - copy them to the MQL5 folder of your terminal. Accordingly, the Test Expert folder will be copied to the MQL5\Experts folder, and two folders will be copied to the MQL\Include folder: CustomGeneric and History manager.

Compile the SimpleMA.mq5 file located in the Experts\Test Expert\New uploading variant folder:


Получаем 100 ошибок и 60 предупреждений:

We get 100 errors and 60 warnings:


Переходим к самой первой ошибке и видим, что это не закрытый импорт:

Let's move on to the very first error and see that this is not a closed import:


Спасибо, обновил исходники, приложенные к статье

Работа с таймсериями в библиотеке DoEasy (Часть 49): Мультипериодные мультисимвольные многобуферные стандартные индикаторы Работа с таймсериями в библиотеке DoEasy (Часть 49): Мультипериодные мультисимвольные многобуферные стандартные индикаторы
В статье доработаем классы библиотеки для возможности создания мультисимвольных мультипериодных стандартных индикаторов, требующих для отображения своих данных несколько индикаторных буферов.
Научный подход к разработке торговых алгоритмов Научный подход к разработке торговых алгоритмов
В статье на примере будет рассмотрена методика разработки торговых алгоритмов при использовании последовательного научного подхода к анализу возможных закономерностей ценообразования и построения на основе этих закономерностей торговых алгоритмов.
Параллельная оптимизация методом роя частиц (Particle Swarm Optimization) Параллельная оптимизация методом роя частиц (Particle Swarm Optimization)
В статье описан способ быстрой оптимизиции методом роя частиц, представлена его реализация на MQL, готовая к применению как в однопоточном режиме внутри эксперта, так и в параллельном многопоточном режиме в качестве надстройки, выполняющейся на локальных агентах тестера.
Работа с таймсериями в библиотеке DoEasy (Часть 48): Мультипериодные мультисимвольные индикаторы на одном буфере в подокне Работа с таймсериями в библиотеке DoEasy (Часть 48): Мультипериодные мультисимвольные индикаторы на одном буфере в подокне
В статье рассмотрим пример создания мультисимвольных мультипериодных стандартных индикаторов, использующих для своих построений один индикаторный буфер, и работающих в подокне графика. Подготовим классы библиотеки для работы со стандартными индикаторами, работающими в основном окне программы, или имеющими более одного буфера для вывода своих данных.