Непрерывная скользящая оптимизация (Часть 6): Логическая часть автооптимизатора и его структура

Andrey Azatskiy | 31 марта, 2020

Введение

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

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

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

Внутренняя структура приложения, её описание и порождение ключевых объектов

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


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

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

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

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


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

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

Однако одним из отличий данных связей является то, что исследуемый класс может принадлежать более чем одному объекту за раз, и его процесс жизнедеятельности не управляется ни одним из объектов контейнеров. Данное утверждение полностью справедливо для класса MainModel, ведь он создается в своем статическом конструкторе (класс MainModelCreator) и хранится как в нем, так и в классе AutoOptimiserVM одновременно. Уничтожается же объект в момент завершения приложением своей работы, это происходит от того, что он изначально был занесен в статическое свойство, которое очищается лишь при завершении приложения.   

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


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

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

Созданный оптимизатор, в свою очередь, приводится к своему интерфейсному типу, сохраняясь в соответствующем поле класса MainModel. Тем самым, используя абстракции как в процессе порождения объекта (конструкторы объектов), так и его экземпляров (оптимизаторы), мы достигаем возможности динамической подстановки оптимизаторов в процессе выполнения программы. Используемый нами подход в инстанцировании оптимизаторов носит имя "Абстрактная фабрика". Его смысл в том, что как продукт (класс, реализующий логику оптимизаций), так и его фабрики (классы, создающие продукт) имеют свою абстракцию. Класс пользователь же, в свою очередь, не должен знать ничего касательно конкретной реализации логики, обоих составляющих, однако должен иметь возможность пользоваться разными их реализациями.

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

В нашей программе пользователем является класс MainModel.


Рассматривая же реализацию оптимизатора по умолчанию, можно видеть, что он имеет также графический интерфейс с настройками (тот самый, что вызывается по клику на кнопку "GUI" рядом с ComboBox, где перечислены все оптимизаторы). На диаграмме классов (и в коде) графическая часть настроек оптимизатора называется "SimpleOptimiserSettings", а ViewModel и View — "SimpleOptimiserVM" и "SimpleOptimiserM" соответственно. Как видно из диаграммы классов, ViewModel настроек оптимизатора безраздельно принадлежит графической части и потому связана с ней связью "Композиция". Сама View часть безраздельно принадлежит оптимизатору, и потому связана связью "Композиция" с классом "Manager". Часть модели данных настроек оптимизатора же принадлежит как оптимизатору, так и ViewModel, и потому имеет связь "Агрегация" как с первым, так и со вторым. Это сделано намеренно, для того чтобы оптимизатор имел доступ к настройкам ? хранимым в модели данных графики настроек оптимизатора.      

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


Данная диаграмма читается сверху вниз и стартовой точкой отображенного процесса является точка Instance, показывающая момент запуска приложения и инстанцирование графического слоя основного окна оптимизатора. В момент инстанцирования графический интерфейс инстанцирует класс "SimpleOptimiserVM", так как он объявлен как DataContext основного окна. В момент инстанцирования класса "SimpleOptimiserVM", вызывает статическое свойство "MainModelCreator.Model", которое в свою очередь порождает объект "MainModel" и приводит его к интерфейсному типу IMainModel.

В момент инстанцирования класса MainModel создается список конструкторов оптимизаторов. Именно этот список мы видим в ComboBox выбора нужного оптимизатора. После инстанцирования модели данных вызывается конструктор класса SimpleOptimiserVM, который вызывает метод "ChangeOptimiser" из модели данных, представленной интерфейсным типом IMainModel. Метод "ChangeOptimiser" вызывает метод Create() на выбранном конструкторе оптимизаторов. Так как мы рассматриваем запуск приложения, то выбранный конструктор оптимизатора является первым из указанного списка. Вызывая метод Create на интересующем нас конструкторе оптимизатора, мы делегируем конструктору создание конкретного выбранного типа оптимизатора, что он и делает, возвращая приведенный к интерфейсному типу объект оптимизатора и передавая его в модель данных, где он и сохраняется в соответствующем свойстве. После этого завершается работа метода ChangeOptimiser, мы переходим назад в конструктор класса SimpleOptimiserVM.

Класс Model и логическая часть программы

Рассмотрев в прошлой главе общую структуру получившегося приложения и процесс порождения основных объектов в момент запуска приложения, стоит перейти к деталям реализации его логики. Все объекты, описывающие логику создаваемого приложения, располагаются в директории "Model". В корне данной директории находится файл "MainModel.cs", в котором и находится класс модели данных, являющийся отправной точкой для запуска всей бизнес логики приложения. Его реализация насчитывает более 1000 строк кода, соответственно в наших разъяснениях мы будем приводить реализации отдельных методов, но не весь класс целиком. Однако, так как он наследуется от интерфейса IMainModel, для демонстрации его структуры приведем код упомянутого интерфейса.

/// <summary>
/// Интерфейс модели данных основного окна оптимизатора
/// </summary>    
interface IMainModel : INotifyPropertyChanged
{
    #region Getters
    /// <summary>
    /// Выбранный оптимизатор
    /// </summary>
    IOptimiser Optimiser { get; }
    /// <summary>
    /// Список имен терминалов установленных на компьютере
    /// </summary>
    IEnumerable<string> TerminalNames { get; }
    /// <summary>
    /// Список имен оптимизаторов доступных для использования
    /// </summary>
    IEnumerable<string> OptimisatorNames { get; }
    /// <summary>
    /// Список имен директорий с сохраненными оптимизациями (Data/Reperts/*)
    /// </summary>
    IEnumerable<string> SavedOptimisations { get; }
    /// <summary>
    /// Структура со всеми проходами результатов оптимизаций
    /// </summary>
    ReportData AllOptimisationResults { get; }
    /// <summary>
    /// Форвардные тесты
    /// </summary>
    List<OptimisationResult> ForwardOptimisations { get; }
    /// <summary>
    /// Исторические тесты
    /// </summary>
    List<OptimisationResult> HistoryOptimisations { get; }
    #endregion

    #region Events
    /// <summary>
    /// Событие выброса ошибки из модели данных
    /// </summary>
    event Action<string> ThrowException;
    /// <summary>
    /// Событие остановки оптимизации
    /// </summary>
    event Action OptimisationStoped;
    /// <summary>
    /// Событие обновление прогресс бара из модели данных
    /// </summary>
    event Action<string, double> PBUpdate;
    #endregion

    #region Methods
    /// <summary>
    /// Метод загружающий ранее сохраненные результаты оптимизаций
    /// </summary>
    /// <param name="optimisationName">Имя требуемого отчета</param>
    void LoadSavedOptimisation(string optimisationName);
    /// <summary>
    /// Метод изменяющий ранее выбранный терминал
    /// </summary>
    /// <param name="terminalName">ID запрашиваемого терминала</param>
    /// <returns></returns>
    bool ChangeTerminal(string terminalName);
    /// <summary>
    /// Метод смены оптимизатора
    /// </summary>
    /// <param name="optimiserName">Имя оптимизатора</param>
    /// <param name="terminalName">Имя терминала</param>
    /// <returns></returns>
    bool ChangeOptimiser(string optimiserName, string terminalName = null);
    /// <summary>
    /// Запуск оптимизации
    /// </summary>
    /// <param name="optimiserInputData">Входные данные для запуска оптимизаций</param>
    /// <param name="IsAppend">Признак дополнить ли существующие выгрузки (если существуют) или же перезаписать их</param>
    /// <param name="dirPrefix">Префикс директории с оптимизациями</param>
    void StartOptimisation(OptimiserInputData optimiserInputData, bool IsAppend, string dirPrefix);
    /// <summary>
    /// Остановка оптимизации извне (пользователем)
    /// </summary>
    void StopOptimisation();
    /// <summary>
    /// Получение параметров робота
    /// </summary>
    /// <param name="botName">Имя эксперта</param>
    /// <param name="isUpdate">Признак нужно ли обновлять файл с параметрами перед его чтением</param>
    /// <returns>Список параметров</returns>
    IEnumerable<ParamsItem> GetBotParams(string botName, bool isUpdate);
    /// <summary>
    /// Сохранение в (*.csv) файл выбранных оптимизаций
    /// </summary>
    /// <param name="pathToSavingFile">Путь к сохраняемому файлу</param>
    void SaveToCSVSelectedOptimisations(string pathToSavingFile);
    /// <summary>
    /// Сохранение в (*csv) файл оптимизаций на переданную дату
    /// </summary>
    /// <param name="dateBorders">Границы дат</param>
    /// <param name="pathToSavingFile">Путь к сохраняемому файлу</param>
    void SaveToCSVOptimisations(DateBorders dateBorders, string pathToSavingFile);
    /// <summary>
    /// Запуск процесса тестирования
    /// </summary>
    /// <param name="optimiserInputData">Список параметров настройки тестера</param>
    void StartTest(OptimiserInputData optimiserInputData);
    /// <summary>
    /// Запуск процесса сортировки результатов
    /// </summary>
    /// <param name="borders">Границы дат</param>
    /// <param name="sortingFlags">Массив имен параметров для сортировки</param>
    void SortResults(DateBorders borders, IEnumerable<SortBy> sortingFlags);
    /// <summary>
    /// Фильтрация результатов оптимизации
    /// </summary>
    /// <param name="borders">Границы дат</param>
    /// <param name="compareData">Флаги фильтрации данных</param>
    void FilterResults(DateBorders borders, IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData);
    #endregion
}

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

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

/// <summary>
/// Структура описывающая результаты оптимизации
/// </summary>
struct ReportData
{
    /// <summary>
    /// Словарь с проходами оптимизаций
    /// key - диапазон дат
    /// value - список проходов оптимизаций за заданный диапазон
    /// </summary>
    public Dictionary<DateBorders, List<OptimisationResult>> AllOptimisationResults;
    /// <summary>
    /// Эксперт и валюта
    /// </summary>
    public string Expert, Currency;
    /// <summary>
    /// Депозит
    /// </summary>
    public double Deposit;
    /// <summary>
    /// Кредитное плечо
    /// </summary>
    public int Laverage;
}

Данная структура, помимо самих данных оптимизаций, описывает основные настройки оптимизатора, что необходимо, во-первых, для запуска тестов (при двойном клике на выбранном проходе оптимизаций), во-вторых, для сверки результатов оптимизации при выбранном режиме добавления новых данных к уже оптимизированным ранее.

Также в модели данных содержится список терминалов установленных на компьютере, имена оптимизаторов , доступных для выбора (создается из конструкторов этих самых оптимизаторов) и список сохраненных ранее оптимизаций (имена директорий, расположенных по пути "Data/Reports"). Также предоставляется доступ к самому оптимизатору.

Обратный обмен информацией (из модели во View модель) осуществляется при помощи событий, на которые подписывается ViewModel после инстанцирования модели данных. Таких событий насчитывается 4, 3 из которых являются пользовательскими, а одно унаследовано от интерфейса INotifyPropertyChanged. Наследование от  интерфейса INotifyPropertyChanged излишне в модели данных. Однако это показалось мне удобным, и поэтому оно присутствует в текущей реализации данной программы.

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

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

Первое, что делает оптимизатор при выборе нового робота, это загрузка его параметров. За данное действо отвечает метод "GetBotParams", данный метод выполняет две возможные логики. Он может либо обновлять файл конфигураций параметров робота, либо просто считывать его. Также он может быть рекурсивным. 

/// <summary>
/// Получение параметров для выбранного эксперта
/// </summary>
/// <param name="botName">Имя эксперта</param>
/// <param name="terminalName">Имя терминала</param>
/// <returns>Параметры эксперта</returns>
public IEnumerable<ParamsItem> GetBotParams(string botName, bool isUpdate)
{
    if (botName == null)
        return null;

    FileInfo setFile = new FileInfo(Path.Combine(Optimiser
                                   .TerminalManager
                                   .TerminalChangeableDirectory
                                   .GetDirectory("MQL5")
                                   .GetDirectory("Profiles")
                                   .GetDirectory("Tester")
                                   .FullName, $"{Path.GetFileNameWithoutExtension(botName)}.set"));


    try
    {
        if (isUpdate)
        {
            if (Optimiser.TerminalManager.IsActive)
            {
                ThrowException("Wating for closing terminal");
                Optimiser.TerminalManager.WaitForStop();
            }
            if (setFile.Exists)
                setFile.Delete();

            FileInfo iniFile = terminalDirectory.Terminals
                                                .First(x => x.Name == Optimiser.TerminalManager.TerminalID)
                                                .GetDirectory("config")
                                                .GetFiles("common.ini").First();

            Config config = new Config(iniFile.FullName);

            config = config.DublicateFile(Path.Combine(workingDirectory.WDRoot.FullName, $"{Optimiser.TerminalManager.TerminalID}.ini"));

            config.Tester.Expert = botName;
            config.Tester.FromDate = DateTime.Now;
            config.Tester.ToDate = config.Tester.FromDate.Value.AddDays(-1);
            config.Tester.Optimization = ENUM_OptimisationMode.Disabled;
            config.Tester.Model = ENUM_Model.OHLC_1_minute;
            config.Tester.Period = ENUM_Timeframes.D1;
            config.Tester.ShutdownTerminal = true;
            config.Tester.UseCloud = false;
            config.Tester.Visual = false;

            Optimiser.TerminalManager.WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized;
            Optimiser.TerminalManager.Config = config;

            if (Optimiser.TerminalManager.Run())
                Optimiser.TerminalManager.WaitForStop();

            if (!File.Exists(setFile.FullName))
                return null;

            SetFileManager setFileManager = new SetFileManager(setFile.FullName, false);
            return setFileManager.Params;
        }
        else
        {
            if (!setFile.Exists)
                return GetBotParams(botName, true);

            SetFileManager setFileManager = new SetFileManager(setFile.FullName, false);
            if (setFileManager.Params.Count == 0)
                return GetBotParams(botName, true);

            return setFileManager.Params;
        }
    }
    catch (Exception e)
    {
        ThrowException(e.Message);
        return null;
    }
}

В начале метода мы создаем объектное-ориентированное представление рассматриваемого файла с параметрами робота, пользуясь классом FileInfo, который входит в стандартную библиотеку языка C#. Согласно стандартным настройкам терминала, данный файл сохраняется в директории MQL5/Profiles/Tester/{имя выбранного робота}.set. Именно данный путь и задается в момент создания объектно-ориентированного представления файла. Дальнейшие действия оборачиваются в конструкцию try-catch из-за того, что есть риск выброса ошибки в процессе операций с файлом. Теперь, в зависимости от переданного параметра isUpdate, выполняется одна из возможных ветвей логики. Если isUpdate = true, значит, мы должны обновить файл с настройками, тем самым скинув его значения до значений по умолчанию и прочесть его параметры. Именно данная ветвь логики выполняется в момент, когда мы нажимаем на кнопку "Update (*.set) file" в графической части нашего приложения. Наиболее удобным способом обновить файл с настройками эксперта будет его повторная генерация.

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

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

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

Следующим интересующим нас методом является "StartOptimisation":

/// <summary>
/// Запуск оптимизаций
/// </summary>
/// <param name="optimiserInputData">Входные данные для оптимизатора</param>
/// <param name="isAppend">Флаг - нужно ли дополнять файл ?</param>
/// <param name="dirPrefix">Префикс директории</param>
public async void StartOptimisation(OptimiserInputData optimiserInputData, bool isAppend, string dirPrefix)
{
    if (string.IsNullOrEmpty(optimiserInputData.Symb) ||
        string.IsNullOrWhiteSpace(optimiserInputData.Symb) ||
        (optimiserInputData.HistoryBorders.Count == 0 && optimiserInputData.ForwardBorders.Count == 0))
    {
        ThrowException("Fill in asset name and date borders");
        OnPropertyChanged("ResumeEnablingTogle");
        return;
    }

    if (Optimiser.TerminalManager.IsActive)
    {
        ThrowException("Terminal already running");
        return;
    }

    if (optimiserInputData.OptimisationMode == ENUM_OptimisationMode.Disabled)
    {
        StartTest(optimiserInputData);
        return;
    }

    if (!isAppend)
    {
        var dir = workingDirectory.GetOptimisationDirectory(optimiserInputData.Symb,
                                                  Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot),
                                                  dirPrefix, Optimiser.Name);
        List<FileInfo> data = dir.GetFiles().ToList();
        data.ForEach(x => x.Delete());
        List<DirectoryInfo> dirData = dir.GetDirectories().ToList();
        dirData.ForEach(x => x.Delete());
    }

    await Task.Run(() =>
    {
        try
        {
            DirectoryInfo cachDir = Optimiser.TerminalManager.TerminalChangeableDirectory
                                                     .GetDirectory("Tester")
                                                     .GetDirectory("cache", true);
            DirectoryInfo cacheCopy = workingDirectory.Tester.GetDirectory("cache", true);
            cacheCopy.GetFiles().ToList().ForEach(x => x.Delete());
            cachDir.GetFiles().ToList()
                   .ForEach(x => x.MoveTo(Path.Combine(cacheCopy.FullName, x.Name)));

            Optimiser.ClearOptimiser();
            Optimiser.Start(optimiserInputData,
                Path.Combine(terminalDirectory.Common.FullName,
                $"{Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot)}_Report.xml"), dirPrefix);
        }
        catch (Exception e)
        {
            Optimiser.Stop();
            ThrowException(e.Message);
        }
    });
}

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

Если выбран режим добавления данных (Append), то удаляем все файлы в директории с оптимизациями, а также все вложенные в нее директории, далее переходим к запуску оптимизации. Процесс оптимизации запускается асинхронно, чтобы не блокировать графический интерфейс на время выполнения данной задачи, а также оборачивается в конструкцию try-catch на случай появления ошибок. Перед стартом процесса, как уже упоминалось ранее, мы копируем все файлы с кэшем ранее произведенных оптимизаций во временную директорию созданную в рабочей директории Data авто оптимизатора. Это необходимо для того, чтобы оптимизации запускались даже в случае если они были произведены ранее. Затем очищаем оптимизатор от всех ранее записанных данных в локальные переменные оптимизатора и запускаем процесс оптимизации. Одним из параметров запуска оптимизаций является путь к формируемому роботом файлу с отчетом. Как уже говорилось ранее в статье № 3, отчет формируется с именем {имя робота}_Report.xml, в авто оптимизаторе данное имя задается следующей строкой:

$"{Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot)}_Report.xml")

Это происходит посредством конкатенации строк, где имя робота, формируется из пути до робота указанного как один из параметров файла оптимизаций. Процесс остановки оптимизации всецело перекладывается на класс оптимизатор. А метод, реализующий его, просто вызывает метод "StopOptimisation" на экземпляре класса оптимизатора.

/// <summary>
/// Завершаем оптимизацию извне оптимизатора
/// </summary>
public void StopOptimisation()
{
    Optimiser.Stop();
}

Упомянутый выше запуск тестов производится методом, реализованным в классе модели данных, а не в оптимизаторе.

/// <summary>
/// Запуск тестов
/// </summary>
/// <param name="optimiserInputData">Входные данные для тестера</param>
public async void StartTest(OptimiserInputData optimiserInputData)
{
    // Проверка не запущен ли терминал
    if (Optimiser.TerminalManager.IsActive)
    {
        ThrowException("Terminal already running");
        return;
    }

    // Задаем диапазон дат
    #region From/Forward/To
    DateTime Forward = new DateTime();
    DateTime ToDate = Forward;
    DateTime FromDate = Forward;

    // Проверка на количество переданных дат. Максимум одна историческая и одна форвардная
    if (optimiserInputData.HistoryBorders.Count > 1 ||
        optimiserInputData.ForwardBorders.Count > 1)
    {
        ThrowException("For test there mast be from 1 to 2 date borders");
        OnPropertyChanged("ResumeEnablingTogle");
        return;
    }

    // Если передана и историческая и форвардная даты
    if (optimiserInputData.HistoryBorders.Count == 1 &&
        optimiserInputData.ForwardBorders.Count == 1)
    {
        // Тестируем на корректность заданного промежутка
        DateBorders _Forward = optimiserInputData.ForwardBorders[0];
        DateBorders _History = optimiserInputData.HistoryBorders[0];

        if (_History > _Forward)
        {
            ThrowException("History optimisation mast be less than Forward");
            OnPropertyChanged("ResumeEnablingTogle");
            return;
        }

        // Запоминаем даты
        Forward = _Forward.From;
        FromDate = _History.From;
        ToDate = (_History.Till < _Forward.Till ? _Forward.Till : _History.Till);
    }
    else // Если передана лишь форвардная или же лишь историческая дата
    {
        // Сохраняем их и считаем что это была историческая дата (даже если была передана форвардная)
        if (optimiserInputData.HistoryBorders.Count > 0)
        {
            FromDate = optimiserInputData.HistoryBorders[0].From;
            ToDate = optimiserInputData.HistoryBorders[0].Till;
        }
        else
        {
            FromDate = optimiserInputData.ForwardBorders[0].From;
            ToDate = optimiserInputData.ForwardBorders[0].Till;
        }
    }
    #endregion

    PBUpdate("Start test", 100);

    // Запускаем тест во вторичном потоке
    await Task.Run(() =>
    {
        try
        {
            // Создаем файл с настройками эксперта
            #region Create (*.set) file
            FileInfo file = new FileInfo(Path.Combine(Optimiser
                                             .TerminalManager
                                             .TerminalChangeableDirectory
                                             .GetDirectory("MQL5")
                                             .GetDirectory("Profiles")
                                             .GetDirectory("Tester")
                                             .FullName, $"{Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot)}.set"));

            List<ParamsItem> botParams = new List<ParamsItem>(GetBotParams(optimiserInputData.RelativePathToBot, false));

            // Заполняем настройки эксперта теми что были введены в графическом интерфейсе
            for (int i = 0; i < optimiserInputData.BotParams.Count; i++)
            {
                var item = optimiserInputData.BotParams[i];

                int ind = botParams.FindIndex(x => x.Variable == item.Variable);
                if (ind != -1)
                {
                    var param = botParams[ind];
                    param.Value = item.Value;
                    botParams[ind] = param;
                }
            }

            // Сохраняем настройки в файл
            SetFileManager setFile = new SetFileManager(file.FullName, false)
            {
                Params = botParams
            };
            setFile.SaveParams();
            #endregion

            // Создаем конфиг терминала
            #region Create config file
            Config config = new Config(Optimiser.TerminalManager
                                                .TerminalChangeableDirectory
                                                .GetDirectory("config")
                                                .GetFiles("common.ini")
                                                .First().FullName);
            config = config.DublicateFile(Path.Combine(workingDirectory.WDRoot.FullName, $"{Optimiser.TerminalManager.TerminalID}.ini"));

            config.Tester.Currency = optimiserInputData.Currency;
            config.Tester.Deposit = optimiserInputData.Balance;
            config.Tester.ExecutionMode = optimiserInputData.ExecutionDelay;
            config.Tester.Expert = optimiserInputData.RelativePathToBot;
            config.Tester.ExpertParameters = setFile.FileInfo.Name;
            config.Tester.ForwardMode = (Forward == new DateTime() ? ENUM_ForvardMode.Disabled : ENUM_ForvardMode.Custom);
            if (config.Tester.ForwardMode == ENUM_ForvardMode.Custom)
                config.Tester.ForwardDate = Forward;OnPropertyChanged("StopTest");
            else
                config.DeleteKey(ENUM_SectionType.Tester, "ForwardDate");
            config.Tester.FromDate = FromDate;
            config.Tester.ToDate = ToDate;
            config.Tester.Leverage = $"1:{optimiserInputData.Laverage}";
            config.Tester.Model = optimiserInputData.Model;
            config.Tester.Optimization = ENUM_OptimisationMode.Disabled;
            config.Tester.Period = optimiserInputData.TF;
            config.Tester.ShutdownTerminal = false;
            config.Tester.Symbol = optimiserInputData.Symb;
            config.Tester.Visual = false;
            #endregion

            // Конфигурируем терминал и запускаем его
            Optimiser.TerminalManager.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
            Optimiser.TerminalManager.Config = config;
            Optimiser.TerminalManager.Run();

            // Ожидаем закрытие терминала
            Optimiser.TerminalManager.WaitForStop();
        }
        catch (Exception e)
        {
            ThrowException(e.Message);
        }

        OnPropertyChanged("StopTest");
    });
}

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

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

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

Заключение

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

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

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

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

  1. Самый простой — нажать сочетание клавиш CTRL+SHIFT+B,
  2. Более визуальный — нажать на зеленую стрелочку в редакторе, произойдет запуск приложения в режиме отладки кода, но компиляция пройдет тоже (сработает без проблем, только если будет выбран режим компиляции Debug),
  3. Еще один вариант — из выпадающего меню пункт Build.

Позже в папке  MetaTrader Auto Optimiser/bin/Debug (или  MetaTrader Auto Optimiser/bin/Release — зависит от выбранного типа сборки) появится скомпилированная программа.