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

Andrey Azatskiy | 11 августа, 2020

Введение

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

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

  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) файла с его настройками. Ранее приходилось вручную выписывать найденные параметры, либо же формировать его из тестера, запуская его по событию двойного клика на выбранной строке с оптимизацией.

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

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.