Непрерывная скользящая оптимизация (Часть 5): Обзор проекта автооптимизатора, а также создание графического интерфейса

19 февраля 2020, 17:07
Andrey Azatskiy
25
2 722

Введение

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

Ознакомиться со статьями данной серии можно по следующим ссылкам:

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

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

  1. Управление оптимизацией (Часть I): Создание графического интерфейса
  2. Управление оптимизацией (Часть 2): Создание ключевых объектов и логики приложения

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


Обзор структуры проекта

Так как данная часть статьи вновь посвящена языку C#, то для лучшего ориентирования по проекту стоит рассмотреть структуру его файлов:

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


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

  • Классы, отвечающие за ориентацию в управляемой директории и  директориях терминала находятся в папке Model/DirectoryManagers
  • Классы, отвечающие за управление файлами — в директории Model/FileReaders.
  • Объекты с логикой оптимизаций и класс инстанцирующий их — в директории Model/OptimisationManagers
  • А уже знакомые нам по прошлому циклу статей объекты, управляющие терминалом и его конфигурационными файлами — в директории Model/Terminal 

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


Создание графической части приложения

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

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


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

Заголовочная часть окна создается следующей XALM-разметкой:

<Grid>
        <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <WrapPanel Margin="2">
            <Label Content="Optimisation:"/>
            <ComboBox Width="200"
                      ItemsSource="{Binding SelectedOptimisationNames,UpdateSourceTrigger=PropertyChanged}"
                      SelectedItem="{Binding SelectedOptimisation}"
                      SelectedIndex="0"
                      IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/>
            <Button Content="Load" 
                    Margin="2,0,0,0"
                    Width="34"
                    Command="{Binding LoadResults}"
                    IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/>
        </WrapPanel>

        <WrapPanel HorizontalAlignment="Right" 
                   Margin="2"
                   Grid.Column="1">
            <Label Content="Terminal:"/>
            <ComboBox Width="200"
                      SelectedIndex="{Binding SelectedTerminalIndex}"
                      ItemsSource="{Binding Terminals,UpdateSourceTrigger=LostFocus}"
                      IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/>
        </WrapPanel>
</Grid>


Контейнер Grid, в который помещены элементы управления, представленные в рассматриваемой области, поделен на 2 колонки. В первую колонку  добавляется: наименование параметра (Optimisation), выпадающий список  со списком доступных оптимизаций, а так же кнопка загрузки оптимизации. Вторая колонка содержит имя параметра и выпадающий список с Id доступных терминалов. 

Контейнер Grid, представляющий подвал графического окна (c ProgressBar), имеет схожую структуру:

<Grid Grid.Row="2">
        <Grid.ColumnDefinitions>
                <ColumnDefinition Width="150"/>
                <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <Label Content="{Binding Status, UpdateSourceTrigger=PropertyChanged}"/>
        <ProgressBar Grid.Column="1"
                     Value="{Binding Progress, UpdateSourceTrigger=PropertyChanged}"
                     Minimum="0"
                     Maximum="100"/>
</Grid>

Он просто делит свою область на 2 части, ограничивая размер первой их них. Как результат, мы отдаем большую часть данного контейнера под ProgressBar, и вдобавок ширина ProgressBar будет изменяться вместе с изменением ширины всего окна в целом. Все 3 озвученные составляющие окна, согласно правилам XALM-разметки, помещаются в контейнер <Window/>.

<Window x:Class="Metatrader_Auto_Optimiser.AutoOptimiser"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:vm="clr-namespace:Metatrader_Auto_Optimiser.View_Model"
        xmlns:v="clr-namespace:Metatrader_Auto_Optimiser.View"
        mc:Ignorable="d"
        Title="Auto Optimiser" Height="500" Width="1200"
        MinHeight="500" MinWidth="1200">

    <Window.DataContext>
        <vm:AutoOptimiserVM/>
    </Window.DataContext>


    ...


</Window>

 В данном контейнере  определяются ссылки на пространства имен:

  • Metatrader_Auto_Optimiser.View_Model, где хранятся коллбэки, описывающие реакцию программы на действия пользователя (ViewModel).
  • Metatrader_Auto_Optimiser.View, где хранится класс , преобразующий событие двойного клика объекта ListView в команду. Это требуется для того, чтобы данное событие можно было использовать совместно с интерфейсом ICommand из ViewModel.

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

Центральная часть панели состоит из элемента TabControl, содержащем 2 вкладки. По сути это основная часть — "Тело" нашего графического интерфейса. Структура вкладки "Settings" следующая:


Данная вкладка также поделена на 3 части. В верхней части данной вкладки находится панель с настройкой параметров сохраняемого отчета автооптимизатора, а также выбор имени актива и клавиша обновления (*set) файла. Средняя часть вкладки "Settings" содержит настройки самого оптимизатора, а также выбор показателей для фильтрации и сортировки данных во время процесса автооптимизации. Завершающая часть содержит настройку параметров эксперта и выбор дат оптимизации и форвардных тестов. Для большей удобности заполнения настроек между двумя главными частями выше упомянутой вкладки располагается элемент GridSplitter, потянув за который можно с легкостью изменять размеры двух озвученных вкладок. Особенно удобно это бывает, когда требуется заполнить параметры оптимизации для робота с большим списком входных данных.

Рассмотрим поподробнее код разметки первой из трех составляющей вкладки "Settings":

<Grid>
        <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>

        <WrapPanel HorizontalAlignment="Left"
                VerticalAlignment="Bottom">
        <Label Content="Select Optimiser:"/>
        <ComboBox Width="150"
                IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"
                ItemsSource="{Binding Optimisers}"
                SelectedIndex="{Binding SelectedOptimiserIndex}"/>
        <Button Content="GUI"
                Command="{Binding ShowOptimiserGUI}"
                IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/>
        <Label Content="Directory prefix:"/>
        <TextBox Width="150"
                IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"
                Text="{Binding DirPrefix}"/>
        <ComboBox Width="100" 
                Margin="2,0,0,0"
                SelectedIndex="0"
                ItemsSource="{Binding FileFillingType}"
                IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"
                SelectedItem="{Binding FileWritingMode}"/>
        <Label Content="Asset name:"/>
        <TextBox Width="100"
                IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"
                Text="{Binding AssetName}"/>
        <Button Content="Update (*.set) file"
                Margin="2,0,0,0"
                IsEnabled="{Binding EnableMainTogles}"
                Command="{Binding UpdateSetFile}"/>
        </WrapPanel>
        <Button Content="Start/Stop"
                Grid.Column="2"
                Margin="2"
                Command="{Binding StartStopOptimisation}"/>

</Grid>

Описываемая часть имеет разделение на две колонки, первая из которых  динамически изменяет свою ширину, а другая фиксирована на 100 пикселей. В первую колонку входят все элементы управления, размещенные в рассматриваемой панели, и все они занесены в обертку WrapPanel, позволяющую располагать элементы друг за другом. Первыми идут элементы управления, отвечающие за выбор автооптимизатора и его настройки. Далее располагаются параметры именования папки с отчетом оптимизации и способ создания данного отчета (Rewrite, Append). В завершении следует задание имени актива, на котором производится оптимизация, и кнопка обновления (*set) файла с параметрами робота. Тот столбец, что имеет фиксированную ширину, целиком отдан под кнопку "Start/Stop" — именно она служит отправной точкой запуска оптимизации и останавливающим ее извне элементом управления. 

Вторая часть вкладки "Settings" разделена на 2 части.


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

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


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

Нижняя часть описываемой вкладки содержит два поля разделенные вертикальным GridSplitter. Первая из них отведена под задание дат и режимов теста выбранного в одной из таблиц верхней части, а другая — под отображение ряда показателей, сведенных в таблицы для удобства отображения и их чтения. Также здесь можно увидеть список параметров выбранного оптимизационного прохода (вкладка "Bot Params").

Сопоставление элементов разметки с ее результатами на вкладке "Optimisations" имеет следующую структуру:


Данная вкладка, аналогично соседней "Selected pass", имеет клавишу "Save to (*csv)", которая сохраняет в файл результаты всех проделанных оптимизаций за выбранную дату. Однако к упомянутой кнопке добавляются новые кнопки сортировки и фильтрации данных, что занесены в таблицу, отображающую результаты всех оптимизаций. По своей структуре таблица с результатами оптимизаций аналогична тем же таблицам, что расположены на вкладках "Selected pass.History" и "Selected pass.Forward". Сам же фрагмент разметки, создающий данные таблицы, выглядит следующим образом:

<ListView ItemsSource="{Binding AllOptimisations}"
          SelectedIndex="{Binding SelecterReportItem}"
          v:ListViewExtention.DoubleClickCommand="{Binding StartTestReport}">
        <ListView.View>
                <GridView>
                        <GridViewColumn Header="Date From" DisplayMemberBinding="{Binding From}"/>
                        <GridViewColumn Header="Date Till" DisplayMemberBinding="{Binding Till}"/>
                        <GridViewColumn Header="Sort by" DisplayMemberBinding="{Binding SortBy}"/>
                        <GridViewColumn Header="Payoff" DisplayMemberBinding="{Binding Payoff}"/>
                        <GridViewColumn Header="Profit pactor" DisplayMemberBinding="{Binding ProfitFactor}"/>
                        <GridViewColumn Header="Average Profit Factor" DisplayMemberBinding="{Binding AverageProfitFactor}"/>
                        <GridViewColumn Header="Recovery factor" DisplayMemberBinding="{Binding RecoveryFactor}"/>
                        <GridViewColumn Header="Average Recovery Factor" DisplayMemberBinding="{Binding AverageRecoveryFactor}"/>
                        <GridViewColumn Header="PL" DisplayMemberBinding="{Binding PL}"/>
                        <GridViewColumn Header="DD" DisplayMemberBinding="{Binding DD}"/>
                        <GridViewColumn Header="Altman Z score" DisplayMemberBinding="{Binding AltmanZScore}"/>
                        <GridViewColumn Header="Total trades" DisplayMemberBinding="{Binding TotalTrades}"/>
                        <GridViewColumn Header="VaR 90" DisplayMemberBinding="{Binding VaR90}"/>
                        <GridViewColumn Header="VaR 95" DisplayMemberBinding="{Binding VaR95}"/>
                        <GridViewColumn Header="VaR 99" DisplayMemberBinding="{Binding VaR99}"/>
                        <GridViewColumn Header="Mx" DisplayMemberBinding="{Binding Mx}"/>
                        <GridViewColumn Header="Std" DisplayMemberBinding="{Binding Std}"/>
                </GridView>
        </ListView.View>
</ListView>

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

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


Классы, заимствованные из прошлого цикла статей "Управление оптимизацией", и их модификации

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

  • RelayCommand — реализует интерфейс ICommand, через который все действия осуществляемые пользователем с графическим интерфейсом передаются во ViewModel часть проекта.  
  • ListViewExtention — переводит событие двойного клика по элементу ListView к интерфейсу ICommand, благодаря чему мы получаем возможность работать с контейнерами ListView, не отходя от используемого шаблона MVVM. По сути данный класс является оберткой для события двойного клика.
  • DirectoryInfoExtention — класс, содержащий метод расширения для класса DirectoryInfo.
  • Config — класс, служащий оберткой для конфигурационных файлов терминала. Он позволяет работать с файлом, как будто мы работаем с объектом типа Config, что сокращает количество строк кода и нивелирует возможность ошибки.
  • TerminalManager — класс, отвечающий за пуск и остановку терминала из стороннего приложения. Он создан специально под терминал MetaTrader 5 и поддерживает все методы конфигурирования терминала перед запуском.
  • SetFileManager — класс для работы с (*.set) файлами конфигурации тестера.
  • TerminalDirectory — класс, являющийся менеджером директории терминалов. Он предоставляет доступ к ключевым директориям и убирает необходимость написания полного пути к требуемой папке.

Четыре последних объекта из приведенного списка можно расценивать как самописное API для работы с терминалом из C# кода. Стоит сказать, что изменения, описываемые в данной части статьи, хоть и коснулись ряда перечисленных объектов, но были лишь внутренними. Иначе говоря, внешний интерфейс работы с данными классами (публичные методы и свойства) остались неизменны по своей сигнатуре. Соответственно, даже если Вы замените прошлые реализации данных объектов в прошлом проекте на новые, то он скомпилируется и заработает. 

Первым из объектов, структура которого подверглась переменам, стал класс Config. Данный класс представляет таблицу, описанную в соответствующем разделе документации к терминалу. Он содержит все поля данной таблицы в своих свойствах, и при изменении того или иного свойства вы меняете значение для конкретного ключа определенной секции иницилизационного файла терминала.  Сами иницилизационные файлы (*.ini), являются довольно долго используемым форматом, и для работы с ними в ядре OS Windows существует ряд функций, две из которых мы экспортировали в наш C# код. В прошлой реализации данного класса используемые методы импортировались напрямую в класс Config. В текущей реализации эти методы были вынесены в отдельный класс "IniFileManager".

class IniFileManager
{
    private const int SIZE = 1024; //Максимальный размер (для чтения значения из файла)
        
    public static string GetParam(string section, string key, string path)
    {
        //Для получения значения
        StringBuilder buffer = new StringBuilder(SIZE);

        //Получить значение в buffer
        if (GetPrivateProfileString(section, key, null, buffer, SIZE, path) == 0)
            ThrowCErrorMeneger("GetPrivateProfileStrin", Marshal.GetLastWin32Error(), path);

        //Вернуть полученное значение
        return buffer.Length == 0 ? null : buffer.ToString();
    }
    /// <summary>
    /// Выброс ошибки
    /// </summary>
    /// <param name="methodName">Имя метода</param>
    /// <param name="er">Код ошибки</param>
    private static void ThrowCErrorMeneger(string methodName, int er, string path)
    {
        if (er > 0)
        {
            if (er == 2)
            {
                if (!File.Exists(path))
                    throw new Exception($"{path} - File doesn1t exist");
            }
            else
            {
                throw new Exception($"{methodName} error {er} " +
                    $"See System Error Codes (https://docs.microsoft.com/ru-ru/windows/desktop/Debug/system-error-codes) for detales");
            }
        }
    }

    public static void WriteParam(string section, string key, string value, string path)
    {
        //Записать значение в INI-файл
        if (WritePrivateProfileString(section, key, value, path) == 0)
            ThrowCErrorMeneger("WritePrivateProfileString", Marshal.GetLastWin32Error(), path);
    }
}

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

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

class TerminalManager
{
    /// <summary>
    /// Конструктор
    /// </summary>
    /// <param name="TerminalChangeableDirectory">
    /// Путь к директории с изменяемыми файлами (та что в AppData)
    /// </param>
    public TerminalManager(DirectoryInfo TerminalChangeableDirectory) :
        this(TerminalChangeableDirectory, new DirectoryInfo(File.ReadAllText(TerminalChangeableDirectory.GetFiles().First(x => x.Name == "origin.txt").FullName)), false)
    {
    }
    /// <summary>
    /// Конструктор
    /// </summary>
    /// <param name="TerminalChangeableDirectory">
    /// Путь к директории с изменяемыми файлами
    /// </param>
    /// <param name="TerminalInstallationDirectory">
    /// Путь к папке с терминалом
    /// </param>
    public TerminalManager(DirectoryInfo TerminalChangeableDirectory, DirectoryInfo TerminalInstallationDirectory, bool isPortable)
    {
        this.TerminalInstallationDirectory = TerminalInstallationDirectory;
        this.TerminalChangeableDirectory = TerminalChangeableDirectory;

        TerminalID = TerminalChangeableDirectory.Name;

        CheckDirectories();

        Process.Exited += Process_Exited;

        Portable = isPortable;
    }
    /// <summary>
    /// Деструктор
    /// </summary>
    ~TerminalManager()
    {
        Close();
        Process.Exited -= Process_Exited;
    }
    /// <summary>
    /// Процесс запуска терминала
    /// </summary>
    private readonly System.Diagnostics.Process Process = new System.Diagnostics.Process();
    /// <summary>
    /// Событие завершения запущенного процесса
    /// </summary>
    public event Action<TerminalManager> TerminalClosed;

    #region Terminal start Arguments
    /// <summary>
    /// Login для старта - флаг /Login
    /// </summary>
    public uint? Login { get; set; } = null;
    /// <summary>
    /// запуск платформы под определенным профилем. 
    /// Профиль должен быть заранее создан и находится в папке /profiles/charts/ торговой платформы
    /// </summary>
    public string Profile { get; set; } = null;
    /// <summary>
    /// Конфиг файл в виде объекта /Config
    /// </summary>
    public Config Config { get; set; } = null;
    /// <summary>
    /// Флаг запуска терминала в режиме /portable
    /// </summary>
    private bool _portable;
    public bool Portable
    {
        get => _portable;
        set
        {
            _portable = value;
            if (value && !TerminalInstallationDirectory.GetDirectories().Any(x => x.Name == "MQL5"))
            {
                WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized;

                if (Run())
                {
                    System.Threading.Thread.Sleep(1000);
                    Close();
                }
                WaitForStop();
                WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
            }
        }
    }
    /// <summary>
    /// стиль окна запускаемого процесса
    /// </summary>
    public System.Diagnostics.ProcessWindowStyle WindowStyle { get; set; } = System.Diagnostics.ProcessWindowStyle.Normal;
    #endregion

    #region Terminal directories
    /// <summary>
    /// Путь к папке где установлен терминал
    /// </summary>
    public DirectoryInfo TerminalInstallationDirectory { get; }
    /// <summary>
    /// Путь к папке терминала с изменяемыми файлами
    /// </summary>
    public DirectoryInfo TerminalChangeableDirectory { get; }
    /// <summary>
    /// Путь к папке MQL5
    /// </summary>
    public DirectoryInfo MQL5Directory => (Portable ? TerminalInstallationDirectory : TerminalChangeableDirectory).GetDirectory("MQL5");
    #endregion

    /// <summary>
    /// ID терминала - имя папки в директории AppData
    /// </summary>
    public string TerminalID { get; }
    /// <summary>
    /// Признак того запущен ли терминал в данный момент или же нет
    /// </summary>
    public bool IsActive => Process.StartInfo.FileName != "" && !Process.HasExited;

    #region .ex5 files relative paths
    /// <summary>
    /// Список полных имен экспертов
    /// </summary>
    public List<string> Experts => GetEX5FilesR(MQL5Directory.GetDirectory("Experts"));
    /// <summary>
    /// Список полных имен индикаторов
    /// </summary>
    public List<string> Indicators => GetEX5FilesR(MQL5Directory.GetDirectory("Indicators"));
    /// <summary>
    /// Список полных имен скриптов
    /// </summary>
    public List<string> Scripts => GetEX5FilesR(MQL5Directory.GetDirectory("Scripts"));
    #endregion

    /// <summary>
    /// Запуск терминала
    /// </summary>
    public bool Run()
    {
        if (IsActive)
            return false;
        // Задаем путь к терминалу
        Process.StartInfo.FileName = Path.Combine(TerminalInstallationDirectory.FullName, "terminal64.exe");
        Process.StartInfo.WindowStyle = WindowStyle;
        // Задаем данные для запуска терминала (если таковые были установлены)
        if (Config != null && File.Exists(Config.Path))
            Process.StartInfo.Arguments = $"/config:{Config.Path} ";
        if (Login.HasValue)
            Process.StartInfo.Arguments += $"/login:{Login.Value} ";
        if (Profile != null)
            Process.StartInfo.Arguments += $"/profile:{Profile} ";
        if (Portable)
            Process.StartInfo.Arguments += "/portable";

        // Уведомляем процесс о необходимости вызывать событие Exit после закрытия терминала
        Process.EnableRaisingEvents = true;

        //Запускаем процесс и сохраняем статус запуска в переменную IsActive
        return Process.Start();
    }
    /// <summary>
    /// Дождаться завершения работы терминала
    /// </summary>
    public void WaitForStop()
    {
        if (IsActive)
            Process.WaitForExit();
    }
    /// <summary>
    /// Остановка процесса
    /// </summary>
    public void Close()
    {
        if (IsActive)
            Process.Kill();
    }
    /// <summary>
    /// Дождаться завершения работы терминала определенное время
    /// </summary>
    public bool WaitForStop(int miliseconds)
    {
        if (IsActive)
            return Process.WaitForExit(miliseconds);
        return true;
    }
    /// <summary>
    /// Поиск файлов с расширением Ex5 
    /// Поиск выполняется рекурсивно - т.е. файлы ищутся в указанной папке и во всех вложенных папках
    /// </summary>
    /// <param name="path">Путь к папке с которой начинается поиск</param>
    /// <param name="RelativeDirectory">Указание папки относительно которой возвращается путь</param>
    /// <returns>Список путей к найденным файлам</returns>
    private List<string> GetEX5FilesR(DirectoryInfo path, string RelativeDirectory = null)
    {
        if (RelativeDirectory == null)
            RelativeDirectory = path.Name;
        string GetRelevantPath(string pathToFile)
        {
            string[] path_parts = pathToFile.Split('\\');
            int i = path_parts.ToList().IndexOf(RelativeDirectory) + 1;
            string ans = path_parts[i];

            for (i++; i < path_parts.Length; i++)
            {
                ans = Path.Combine(ans, path_parts[i]);
            }

            return ans;
        }
    
        List<string> files = new List<string>();
        IEnumerable<DirectoryInfo> directories = path.GetDirectories();

        files.AddRange(path.GetFiles("*.ex5").Select(x => GetRelevantPath(x.FullName)));

        foreach (var item in directories)
            files.AddRange(GetEX5FilesR(item, RelativeDirectory));

        return files;
    }
    /// <summary>
    /// Событие закрытия терминала
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Process_Exited(object sender, EventArgs e)
    {
       TerminalClosed?.Invoke(this);
    }
    /// <summary>
    /// Проверка переданного пути к терминалу на корректность
    /// </summary>
    private void CheckDirectories()
    {
        if (!TerminalInstallationDirectory.Exists)
            throw new ArgumentException("PathToTerminalInstallationDirectory doesn`t exists");
        if (!TerminalChangeableDirectory.Exists)
            throw new ArgumentException("PathToTerminalChangeableDirectory doesn`t exists");
        if (!TerminalInstallationDirectory.GetFiles().Any(x => x.Name == "terminal64.exe"))
            throw new ArgumentException($"Can`t find terminal (terminal64.exe) in the instalation folder {TerminalInstallationDirectory.FullName}");
    }
}

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

Следующим изменением является иной способ определения признака запуска терминала. В прошлом варианте данное свойство получало свое значение из методов Run (где присваивалось значение false) и коллбэка окончания оптимизации, однако это было не очень красивым решением и порой могло не сработать. Посему геттер свойства IsActive был переписан на обращение напрямую к свойству HasExited объекта Process. Однако, если до первого запуска обратиться к искомому свойству, мы получим сообщение об ошибке. Изучая специфику работы класса Process, было замечено, что при старте процесса через описываемый объект его свойство StartInfo.FileName заполняется путем до исполняемого файла, а до старта равняется пустому значению (""). Именно поэтому геттер IsActive имеет столь странный вид. Он сперва проверяет наличие имени до запускаемого объекта, а уже после свойство Process.HasExited. Иначе говоря, по умолчанию мы предполагаем, что терминал закрыт и запущен может быть лишь через наш класс TerminalManager, соответственно, если StartInfo.FileName == "", мы возвращаем false (говорим что терминал не запущен). Далее если хоть раз терминал был запущен, мы попросту сравниваем значение свойства HasExited. Данное свойство меняет свое значение каждый раз при старте терминала, если старт был произведен из описываемого объекта, и при завершении его работы. Именно из-за этой особенности приходится держать терминал в закрытом состоянии при использовании автооптимизатора. 

Завершает данное описание последний объект, подвергшийся изменениям во внутренней структуре. Это класс SetFileManager и его метод  UpdateParams.

/// <summary>
/// Очистка всех записанных данных в Params и загрузка данных из требуемого файла
/// </summary>
public virtual void UpdateParams()
{
    _params.Clear();

    using (var file = FileInfo.OpenText())
    {
        string line;
        while ((line = file.ReadLine()) != null)
        {
            if (line[0].CompareTo(';') != 0 && line[0].CompareTo('#') != 0)
            {
                string[] key_value = line.Replace(" ", "").Split('=');
                string[] value_data = key_value[1].Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);

                ParamsItem item = new ParamsItem
                {
                    Variable = key_value[0],
                    Value = (value_data.Length > 0 ? value_data[0] : null),
                    Start = (value_data.Length > 1 ? value_data[1] : null),
                    Step = (value_data.Length > 2 ? value_data[2] : null),
                    Stop = (value_data.Length > 3 ? value_data[3] : null),
                    IsOptimize = (value_data.Length > 4 ? value_data[4].CompareTo("Y") == 0 : false)
                };

                _params.Add(item);
            }
        }
    }
}

В данном классе изменения коснулись лишь одного метода, посему нет необходимости приводить весь код класса целиком. Дело в том, что как выяснилось в процессе тестирования приложения, терминал порой может сгенерировать (*.set) файл с параметрами робота для оптимизатора пустым наполовину для какого-либо параметра. К примеру, заполнить поле Value, но не заполнить поле начального значения для оптимизации, либо его завершающее значение. Подобное зависит от типа параметра, к примеру, строковые параметры заполняют лишь поле Value. Именно для избежания подобной проблемы были добавлены те изменения, что можно наблюдать в представленном фрагменте кода.


Разбор структуры директории Data

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

/// <summary>
/// Объект описывающий подконтрольную директорию Data с изменяемыми файлами автооптимизатора.
/// </summary>
class WorkingDirectory
{
    /// <summary>
    /// Конструктор по умолчанию
    /// </summary>
    public WorkingDirectory()
    {
        // Создание корневой директории с изменяемыми файлами
        WDRoot = new DirectoryInfo("Data");
        if (!WDRoot.Exists)
            WDRoot.Create();
        // Создание вложенной директории с отчетами оптимизаций
        Reports = WDRoot.GetDirectory("Reports", true);
    }
    /// <summary>
    /// Вложенная директория с отчетами оптимизаций
    /// </summary>
    public DirectoryInfo Reports { get; }
    /// <summary>
    /// Корневая директория с изменяемыми файлами и папками
    /// </summary>
    public DirectoryInfo WDRoot { get; }

    /// <summary>
    /// Получение или создание (если не была создана ранее) директории вложенной в директорию Reports.
    /// Полученная директория хранит в себе результаты конкретного прохода оптимизаций.
    /// </summary>
    /// <param name="Symbol">Символ на котором проводилась оптимизация</param>
    /// <param name="ExpertName">Имя робота</param>
    /// <param name="DirectoryPrefix">Префикс добавляемый к имени директории</param>
    /// <param name="OptimiserName">Наименование использованного оптимизатора</param>
    /// <returns>
    /// Путь к директории  с результатами оптимизации.
    /// Имя директории строится последующим образом:public DirectoryInfo WDRoot { get; }
    /// {DirectoryPrefix} {OptimiserName} {ExpertName} {Symbol}
    /// </returns>
    public DirectoryInfo GetOptimisationDirectory(string Symbol, string ExpertName,
                                                  string DirectoryPrefix, string OptimiserName)
    {
        return Reports.GetDirectory($"{DirectoryPrefix} {OptimiserName} {ExpertName} {Symbol}", true);
    }

    /// <summary>
    /// Путь к директории Data/Tester 
    /// Нужна для временного перемещения файлов из одноименной директории терминала
    /// </summary>
    public DirectoryInfo Tester => WDRoot.GetDirectory("Tester", true);

}

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


В итоге, сразу после первого запуска создается директория Data с единственной (пока еще пустой) вложенной директорией "Reports". При первом запуске оптимизации или же теста создается вложенная директория Tester, путем обращения к соответствующему свойству описываемого объекта. А конфигурационный файл по имени {Terminal ID}.ini создается путем копирования конфигурационного файла, по умолчанию выбранного Вами терминала. Это делается для того, чтобы не перезаписывать исходный конфигурационный файл. Директория Tester создается для временного копирования кеша произведенных ранее оптимизаций, и она частично повторяет соответствующую директорию Tester, находящуюся среди изменяемых директорий терминала.

Внутри она содержит лишь папку "cache", куда перед началом оптимизационного процесса перемещаются все файлы из соответствующей директории выбранного терминала. А по окончанию процесса оптимизации данные файлы возвращаются на свое старое место. Эта операция производится для того, чтобы процесс оптимизации гарантированно состоялся. Согласно логике работы оптимизатора, если в данной директории терминала существуют файлы, описывающие запускаемый процесс оптимизации, то вместо запуска нового процесса он подгружает ранее произведенные оптимизации. Это прекрасное и верное решение, экономящее много времени, однако для наших целей оно совершенно непригодно. Так как мы храним собственную копию оптимизационного отчета адаптированную для нашего автооптимизатора (статьи №3 и №1 текущего цикла статей), то мы должны сформировать искомый отчет, а для этого нам требуется именно запуск процесса  оптимизации. Посему мы эмитируем отсутствие данных файлов, временно перемещая их в нашу локальную директорию. При удачном завершении процесса оптимизации мы создаем вложенную директорию в директории Reports при помощи метода GetOptimisationDirectory.

 

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

  • Forward.xml — только форвардные тесты
  • History.xml — только исторические тесты
  • Report.xml — все произведенные оптимизации за все переданные исторические периоды времени.

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

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

Заключение

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

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


Прикрепленные файлы |
Auto_Optimiser.zip (126.38 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (25)
Andrey Azatskiy
Andrey Azatskiy | 22 фев 2020 в 20:16
Good Beer:

Уже нашел формулу payoff, не успел исправить -вы быстро отвечаете. Скрины ошибок взяты из терминала. Он и не запускает тест. Облако мне и не нужно, просто раньше такой ошибки не было. Счёт действительно неттинговый. Но на прошлой версии терминала всё работало.

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

fxsaber
fxsaber | 5 мар 2020 в 20:31

Идея по GUI.

Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий

Новая версия платформы MetaTrader 5 build 1930: Плавающие окна графиков и .Net библиотеки в MQL5

fxsaber, 2019.01.31 06:35

Пытаюсь дружить с ГА. По какой-то причине он всегда делает 10К проходов.

Раз делается новый GUI для Тестера, то прошу помочь сдружить с ГА.


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

Каждый раз вводить диапазоны найденных окрестностей очень тяжко.


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

Прошу разработчиков рассмотреть возможность помощи в новом GUI при задании окрестности вокруг левого столбца.


Например, берем первую строку из скрина


Значение Старт Шаг Стоп
Было 16  1  1  30
Окрестность (N шагов в каждую сторону от Значения) 16 16-N*Step Step 16 + N*Step

Т.е. добавить выбор числа N и по нему формировать поля Старт/Стоп.


Поэтому предлагаю добавить еще один столбец (во вкладке Параметры) с названием "Окрестность", куда вводить число N. Если N отлично от нуля, то формировать автоматически поля Старт/Стоп. Если ноль - не участвовать в формировании полей.

Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий

Новая версия платформы MetaTrader 5 build 1930: Плавающие окна графиков и .Net библиотеки в MQL5

fxsaber, 2019.01.31 07:25

Есть два способа задания диапазона: Начало/Конец и Центр/Ширина. Каждый способ удобен в зависимости от ситуации и предпочтений.

Центр/Ширина прекрасен для задания окрестностей. В частности, после Оптимизации значение Центр выставляется автоматически запуском самого прохода. И далее пользователь может либо по-старинке вычислить начало и конец диапазона и прописать их руками, либо же, ничего не вычисляя и не прописывая, задать только ширину.

Andrey Azatskiy
Andrey Azatskiy | 5 мар 2020 в 20:42
fxsaber:

Идея по GUI.

Идея хорошая, однако как отличить типы параметров? У меня они все как строки задаются, так как в (*.set) файле нету указания на то каков тип параметра. К примеру enum в файле int конвертируются,  затем у меня в строку уже и соответственно если задавать таким образом, то я буду пробовать строку скастовать к double - может быть ошибка, но тут проверку можно поставить. А если я enum приведу к double то все получится, однако в тестере значения не примутся и установятся по дефолту... Если бы получилось вытянуть тип параметра из файла с настройками (*set), то было бы замечательно. 

Michael Williams
Michael Williams | 4 июн 2020 в 01:19

Hi Andrey,

Thank you for sharing your project;   trying to figure out the In Sample vs the Out of Sample window sizes and then analyzing DD, RF, good correlation between IS / OOS sets, etc. is a daunting task.   For me, I'm bewildered as to why MetaTrader hasn't built and included a comprehensive Walk Forward Analysis tool.   It's one of the tools we need to mitigate "curve fitting," so thank you for your efforts! 

I was able to build the project in Visual Studio, However, I'm not able to build your sample EA; I'm getting many compile errors. I was wondering if you could check to see if it's still compatible with the latest MT5 version? Also, have you considered making this an open source project, putting it up on Github?

Michael

Andrey Azatskiy
Andrey Azatskiy | 9 июн 2020 в 14:21
Michael Williams:

Hi Andrey,

Thank you for sharing your project;   trying to figure out the In Sample vs the Out of Sample window sizes and then analyzing DD, RF, good correlation between IS / OOS sets, etc. is a daunting task.   For me, I'm bewildered as to why MetaTrader hasn't built and included a comprehensive Walk Forward Analysis tool.   It's one of the tools we need to mitigate "curve fitting," so thank you for your efforts! 

I was able to build the project in Visual Studio, However, I'm not able to build your sample EA; I'm getting many compile errors. I was wondering if you could check to see if it's still compatible with the latest MT5 version? Also, have you considered making this an open source project, putting it up on Github?

Michael

Hello. Try to change file inside directory MQL5/Include/CustomGeneric/GenericSorter.mqh to the attached file. It must help.

Работа с таймсериями в библиотеке DoEasy (Часть 35): Объект "Бар" и список-таймсерия символа Работа с таймсериями в библиотеке DoEasy (Часть 35): Объект "Бар" и список-таймсерия символа

С этой статьи мы открываем новую серию описания создания библиотеки "DoEasy" для простого и быстрого создания программ. Сегодня начнём подготавливать функционал библиотеки для доступа и работе с данными таймсерий символов. Создадим объект "Бар", хранящий основные и расширенные данные бара таймсерии, и разместим объекты-бары в список-таймсерию для удобного поиска и сортировки этих объектов.

SQLite: нативная работа с базами данных на SQL в MQL5 SQLite: нативная работа с базами данных на SQL в MQL5

Разработка торговых стратегий связана с обработкой больших объемов данных. Теперь прямо в MQL5 вы можете работать с базами данных с помощью SQL-запросов на основе SQLite. Важным преимуществом данного движка является то, что вся база данных содержится в единственном файле, который находится на компьютере пользователя.

Прогнозирование временных рядов (Часть 2): метод наименьших квадратов опорных векторов (LS-SVM) Прогнозирование временных рядов (Часть 2): метод наименьших квадратов опорных векторов (LS-SVM)

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

Работа с таймсериями в библиотеке DoEasy (Часть 36): Объект таймсерий всех используемых периодов символа Работа с таймсериями в библиотеке DoEasy (Часть 36): Объект таймсерий всех используемых периодов символа

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