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

10 апреля 2020, 11:00
Andrey Azatskiy
2
2 143

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

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

Класс ViewModel и взаимодействие c графическим слоем

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

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

<TextBox Width="100"          IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"          Text="{Binding AssetName}"/>

Помимо задания ширины текстового окна мы видим поля IsEnabled и Text. Первое из них отвечает за доступность поля для редактирования. Если оно выставлено в режим true, то поле становится доступным для редактирования, если же false, то соответственно заблокировано. Свойство Text содержит сам текст, введенный в данное поле. Далее напротив каждого из них мы видим конструкцию в фигурных скобках. Все что находится в ней, задает связь объекта с определенным публичным свойством из класса ViewModel, указанным после параметра "Binding".

Затем может следовать еще ряд параметров, к примеру, параметр UpdateSourceTrigger указывает на способ обновления графической части данного приложения, и он может равняться ряду значений. Конкретно то значение, что указано в нашем примере (PropertyChanged), говорит о том, что графическая часть будет обновляться лишь при срабатывании события OnPropertyChanged из класса ViewModel с переданным именем указанным после параметра Binding (в нашем примере это "EnableMainTogles").

Также стоит сказать, что если связать параметр Text связан не со строкой, а, к примеру, с параметром double, то мы не сможем ввести ничего кроме цифр. Если же связать данный параметр с типом int, то соответственно не сможем ввести ничего кроме целочисленных чисел. Иными словами, данная технология также помогает накладывать ограничения на тип вводимого значения.

Если же рассматривать часть ViewModel, то интересующие нас поля представлены следующим образом:

Параметр IsEnabled:

/// <summary> /// Если этот переключатель = false, то наиболее важные поля недоступны /// </summary> public bool EnableMainTogles { get; private set; } = true;

и параметр Text:

/// <summary> /// Имя актива выбранного для тестов / оптимизации /// </summary> public string AssetName { get; set; }

Как видно, оба из них имеют доступ как на запись, так и на чтение данных. Отличием является лишь то, что свойство EnableMainTogles предоставляет доступ на запись лишь из класса AutoOptimiserVM (т.е. из самого себя), поэтому никак не получится его изменить извне.

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

<ListView ItemsSource="{Binding ForwardOptimisations}"           SelectedIndex="{Binding SelectedForwardItem}"           v:ListViewExtention.DoubleClickCommand="{Binding StartTestForward}">     <ListView.View>         <GridView>             <GridViewColumn Header="Date From"                             DisplayMemberBinding="{Binding From}"/>             <GridViewColumn Header="Date Till"                             DisplayMemberBinding="{Binding Till}"/>             <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>

Как видно из разметки, таблица типа ListView представляет собой указание самого класса таблицы. Затем создание сетки, в которой будут храниться данные и наконец, колонок с данными. Когда я сказал, что создается указание класса, я имел в виду именно класс ListView. Ведь столь простая, на первый взгляд, разметка XAML скрывает в себе достаточно сложный и продуманный механизм, который позволяет языком разметки описывать классы и оперировать объектами этих классов. Все поля, которые мы связываем с классом AutoOptimiserVM, на самом деле являются ничем иным, как свойствами данных классов. Даже в приведенном примере с таблицей мы имеем дело с 3-мя классами:

  • ListView — System.Windows.Controls.ListView.
  • GridView — System.Windows.Controls.GridView, который является наследником класса System.Windows.Controls.ViewBase, что позволяет использовать его в качестве класса инициализирующего свойство View из класса ListView.
  • GridViewColumn — System.Windows.Controls.GridViewColumn.

Свойство ItemsSource класса ListView указывает на коллекцию элементов, из которых состоит таблица. После связки данного свойства с коллекцией из ViewModel мы имеем подобие DataContext для класса Window, однако в рамках рассматриваемой таблицы. Так как рассматривается таблица, то коллекция, представляющая ее, должна состоять из классов, имеющих свои публичные свойства для каждой из колонок. Теперь, связав свойство ItemsSource со свойством из ViewModel, которое представляет таблицу с данными, мы можем связать каждую из колонок с искомым значением колонки из данной таблицы. Также таблица имеет связь свойства SelectedIndex со свойством SelectedForwardItem из ViewModel. Это необходимо для того, чтобы ViewModel знал о том, какая именно строка выбрана пользователем в представленной таблице.

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

/// <summary> /// Выбранные форвардные тесты /// </summary> public ObservableCollection<ReportItem> ForwardOptimisations { get; } = new ObservableCollection<ReportItem>();

Как уже упоминалось ранее, класс ObservableCollection из стандартной библиотеки языка C#, является объектом, который сам уведомляет графику о произошедших модификациях. Это происходит потому, что данный класс уже имеет упомянутое событие и вызывает его всякий раз при обновлении списка своих элементов. Во всем остальном это довольно стандартная коллекция данных.

Что же касается свойства SelectedForwardItem, то оно выполняет несколько ролей: хранителя данных о выбранной строке в таблице и коллбэка выбора строки.

/// <summary> /// Выбранный форвард проход /// </summary> private int _selectedForwardItem; public int SelectedForwardItem {     get => _selectedForwardItem;     set     {         _selectedForwardItem = value;         if (value > -1)         {             FillInBotParams(model.ForwardOptimisations[value]);             FillInDailyPL(model.ForwardOptimisations[value]);             FillInMaxPLDD(model.ForwardOptimisations[value]);         }     } } 

Так как данное свойство используется как коллбэк, что подразумевает указание реакции на задание значения (в нашем примере), то сеттер должен содержать реализацию данной реакции и выступать так же в роли функции. Из-за этой особенности мы храним значение свойства в private переменной. Для получения из нее значения мы из геттера напрямую обращаемся к ней. Для задания значения, в сеттере, мы приравниваем ей значение, хранимое под именем value. Переменная value нигде не озаглавлена, это некий алиас для задаваемого значения, предусмотренный языком C#. В случае если value больше чем -1, мы заполняем остальные связанные таблицы на вкладке Results, которые обновляются в соответствии с выбранной строкой. Это таблицы с параметрами робота, средней прибылью, убытками за день недели и максимальные/минимальные значения PL. Проверка, производимая в условии if, вызвана тем, что если индекс выбранного элемента в таблице равен -1, то это означает, что таблица пуста и, соответственно, не следует заполнять связанные таблицы. Реализация вызываемых методов довольно тривиальна и поэтому не будет рассмотрена, однако вы в любой момент сможете посмотреть ее в коде класса AutoOptimiserVM.

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

/// <summary> /// Класс - обертка элемента отчета (для графического интерфейса) /// </summary> class ReportItem {     /// <summary>     /// Конструктор     /// </summary>     /// <param name="item">Элемент</param>     public ReportItem(OptimisationResult item)     {         result = item;     }     /// <summary>     /// Элемент отчета     /// </summary>     private readonly OptimisationResult result;     public DateTime From => result.report.DateBorders.From;     public DateTime Till => result.report.DateBorders.Till;     public double SortBy => result.SortBy;     public double Payoff => result.report.OptimisationCoefficients.Payoff;     public double ProfitFactor => result.report.OptimisationCoefficients.ProfitFactor;     public double AverageProfitFactor => result.report.OptimisationCoefficients.AverageProfitFactor;     public double RecoveryFactor => result.report.OptimisationCoefficients.RecoveryFactor;     public double AverageRecoveryFactor => result.report.OptimisationCoefficients.AverageRecoveryFactor;     public double PL => result.report.OptimisationCoefficients.PL;     public double DD => result.report.OptimisationCoefficients.DD;     public double AltmanZScore => result.report.OptimisationCoefficients.AltmanZScore;     public int TotalTrades => result.report.OptimisationCoefficients.TotalTrades;     public double VaR90 => result.report.OptimisationCoefficients.VaR.Q_90;     public double VaR95 => result.report.OptimisationCoefficients.VaR.Q_95;     public double VaR99 => result.report.OptimisationCoefficients.VaR.Q_99;     public double Mx => result.report.OptimisationCoefficients.VaR.Mx;     public double Std => result.report.OptimisationCoefficients.VaR.Std; }

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

Все кнопки или события двойного клика по какой либо строке из таблицы связаны свойством Command из ViewModel, базовым типом которого является тип ICommand. Мы уже рассматривали данную технологию в статьях про создание графического интерфейса по управлению оптимизацией. 

Класс ViewModel, и взаимодействие c моделью данных.

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


Нажатие кнопки "StartStop" вызывает метод _StartStopOptimisation из класса AutoOptimiserVM. Далее мы имеем две альтернативы — остановка оптимизации и запуск оптимизации. Из диаграммы мы видим, что в случае, когда свойство IsOptimisationInProcess класса оптимизатора возвращает true, мы выполняем первую часть логики и запрашиваем метод StopOptimisation у класса модели данных, который, как уже было рассмотрено ранее, переадресовывает данный вызов в сам оптимизатор. Если же оптимизация не была запущена, то мы вызываем метод StartOptimisation из класса модели данных. Данный метод является асинхронным, соответственно вызванный метод Start на классе оптимизатора, продолжает свою работу даже после того, как метод _StartStopOptimisation завершить свою. 

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

private void _StartStopOptimisation(object o) {     if (model.Optimiser.IsOptimisationInProcess)     {         model.StopOptimisation();     }     else     {         EnableMainTogles = false;         OnPropertyChanged("EnableMainTogles");         Model.OptimisationManagers.OptimiserInputData optimiserInputData = new Model.OptimisationManagers.OptimiserInputData         {             Balance = Convert.ToDouble(OptimiserSettings.Find(x => x.Name == "Deposit").SelectedParam),             BotParams = BotParams?.Select(x => x.Param).ToList(),             CompareData = FilterItems.ToDictionary(x => x.Sorter, x => new KeyValuePair<CompareType, double>(x.CompareType, x.Border)),             Currency = OptimiserSettings.Find(x => x.Name == "Currency").SelectedParam,             ExecutionDelay = GetEnum<ENUM_ExecutionDelay>(OptimiserSettings.Find(x => x.Name == "Execution Mode").SelectedParam),             Laverage = Convert.ToInt32(OptimiserSettings.Find(x => x.Name == "Laverage").SelectedParam),             Model = GetEnum<ENUM_Model>(OptimiserSettings.Find(x => x.Name == "Optimisation model").SelectedParam),             OptimisationMode = GetEnum<ENUM_OptimisationMode>(OptimiserSettings.Find(x => x.Name == "Optimisation mode").SelectedParam),             RelativePathToBot = OptimiserSettings.Find(x => x.Name == "Available experts").SelectedParam,             Symb = AssetName,             TF = GetEnum<ENUM_Timeframes>(OptimiserSettings.Find(x => x.Name == "TF").SelectedParam),             HistoryBorders = (DateBorders.Any(x => x.BorderType == OptimisationType.History) ?                             DateBorders.Where(x => x.BorderType == OptimisationType.History)                             .Select(x => x.DateBorders).ToList() :                             new List<DateBorders>()),             ForwardBorders = (DateBorders.Any(x => x.BorderType == OptimisationType.Forward) ?                             DateBorders.Where(x => x.BorderType == OptimisationType.Forward)                             .Select(x => x.DateBorders).ToList() :                             new List<DateBorders>()),             SortingFlags = SorterItems.Select(x => x.Sorter)         };         model.StartOptimisation(optimiserInputData, FileWritingMode == "Append", DirPrefix);     } } /// <summary> /// Коллбек для графического интерфейса запуска оптимизации / теста /// </summary> public ICommand StartStopOptimisation { get; }

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

В момент запуска оптимизации мы блокируем основные поля графического интерфейса путем выставления уже известного нам свойства EnableMainTogles = false, а затем приступаем к формированию входных параметров. Для запуска оптимизации нам требуется создать структуру OptimistionInputData, которую мы и заполняем из коллекций OptimiserSettings, BotParams, FilterItems, SorterItems и DateBorders. Значения в упомянутые структуры попадают напрямую из графического интерфейса посредством уже рассмотренного нами механизма связки данных. По завершении формирования данной структуры мы запускаем уже рассмотренный ранее метод StartOptimisation на экземпляре класса модели данных. Свойство StartStopOptimisation задается в конструкторе.

// Коллбэк кнопки запуска / остановки процесса оптимизации StartStopOptimisation = new RelayCommand(_StartStopOptimisation);

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

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

private void _StartTest(List<OptimisationResult> results, int ind) {     try     {         Model.OptimisationManagers.OptimiserInputData optimiserInputData = new Model.OptimisationManagers.OptimiserInputData         {             Balance = Convert.ToDouble(OptimiserSettingsForResults_fixed.First(x => x.Key == "Deposit").Value),             Currency = OptimiserSettingsForResults_fixed.First(x => x.Key == "Currency").Value,             ExecutionDelay = GetEnum<ENUM_ExecutionDelay>(OptimiserSettingsForResults_changing.First(x => x.Name == "Execution Mode").SelectedParam),             Laverage = Convert.ToInt32(OptimiserSettingsForResults_fixed.First(x => x.Key == "Laverage").Value),             Model = GetEnum<ENUM_Model>(OptimiserSettingsForResults_changing.First(x => x.Name == "Optimisation model").SelectedParam),             OptimisationMode = ENUM_OptimisationMode.Disabled,             RelativePathToBot = OptimiserSettingsForResults_fixed.First(x => x.Key == "Expert").Value,             ForwardBorders = new List<DateBorders>(),             HistoryBorders = new List<DateBorders> { new DateBorders(TestFrom, TestTill) },             Symb = OptimiserSettingsForResults_fixed.First(x => x.Key == "Symbol").Value,             TF = (ENUM_Timeframes)Enum.Parse(typeof(ENUM_Timeframes), OptimiserSettingsForResults_fixed.First(x => x.Key == "TF").Value),             SortingFlags = null,             CompareData = null,             BotParams = results[ind].report.BotParams.Select(x => new ParamsItem { Variable = x.Key, Value = x.Value }).ToList()         };         model.StartTest(optimiserInputData);     }     catch (Exception e)     {         System.Windows.MessageBox.Show(e.Message);     } }

Мы точно так же создаем структуру с входными параметрами и затем запускаем тест. Если произошла какая-либо ошибка в процессе выполнения данного метода, то мы отображаем сообщение об ошибке в виде MessageBox. Реализация данного метода уже была описана, однако стоит уделить внимание инстанцированию свойств содержащих данный коллбэк. Так как у нас 3 разные таблицы:

  • Форвардные тесты,
  • Исторические тесты,
  • Список оптимизаций за выбранный диапазон дат.

Соответственно было создано 3 коллбэка. Это потребовалось для корректной обработки информации каждой из таблиц. 

/// <summary>
/// Запуск теста из таблицы с форвардными тестами
/// </summary>
public ICommand StartTestForward { get; }
/// <summary>
///Запуск теста из таблицы с историческими тестами
/// </summary>
public ICommand StartTestHistory { get; }
/// <summary>
/// Запуск теста из таблицы с результатами оптимизации
/// </summary>
public ICommand StartTestReport { get; }

И их реализация представлена в виде задания лямбда функций:

StartTestReport = new RelayCommand((object o) => {     _StartTest(model.AllOptimisationResults.AllOptimisationResults[ReportDateBorders[SelectedReportDateBorder]], SelecterReportItem); }); // Коллбэк старта теста по событию двойного клика на таблице с историческими тестами StartTestHistory = new RelayCommand((object o) => {     _StartTest(model.HistoryOptimisations, SelectedHistoryItem); }); // Коллбэк старта теста по событию двойного клика на таблице с форвардными тестами StartTestForward = new RelayCommand((object o) => {     _StartTest(model.ForwardOptimisations, SelectedForwardItem); });

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

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

/// <summary> /// Сортировка отчетов /// </summary> /// <param name="o"></param> private void _SortResults(object o) {     if (ReportDateBorders.Count == 0)         return;     IEnumerable<SortBy> sortFlags = SorterItems.Select(x => x.Sorter);     if (sortFlags.Count() == 0)         return;     if (AllOptimisations.Count == 0)         return;     model.SortResults(ReportDateBorders[SelectedReportDateBorder], sortFlags); } public ICommand SortResults { get; } /// <summary> /// Фильтрация отчетов /// </summary> /// <param name="o"></param> private void _FilterResults(object o) {     if (ReportDateBorders.Count == 0)         return;     IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData =         FilterItems.ToDictionary(x => x.Sorter, x => new KeyValuePair<CompareType, double>(x.CompareType, x.Border));     if (compareData.Count() == 0)         return;     if (AllOptimisations.Count == 0)         return;     model.FilterResults(ReportDateBorders[SelectedReportDateBorder], compareData); } public ICommand FilterResults { get; }

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

Метод сортировки имеет следующую сигнатуру:

public static IEnumerable<OptimisationResult> SortOptimisations(this IEnumerable<OptimisationResult> results,                                                                         OrderBy order, IEnumerable<SortBy> sortingFlags,                                                                         Func<SortBy, SortMethod> sortMethod = null)

И метод фильтрации соответственно:

public static IEnumerable<OptimisationResult> FiltreOptimisations(this IEnumerable<OptimisationResult> results,                                                                   IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData)

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

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

   

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

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

/// <summary> /// Отобранные параметры сортировки /// </summary> public ObservableCollection<SorterItem> SorterItems { get; } = new ObservableCollection<SorterItem>();

Таблицы фильтров со следующим:   

/// <summary> ///Выбранные фильтры /// </summary> public ObservableCollection<FilterItem> FilterItems { get; } = new ObservableCollection<FilterItem>();

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

/// <summary> /// Класс обертка для enum SortBy (для графического интерфейса) /// </summary> class SorterItem {     /// <summary>     /// Конструктор     /// </summary>     /// <param name="sorter">Параметр сортировки</param>     /// <param name="deleteItem">Коллбек удаления из списка</param>     public SorterItem(SortBy sorter, Action<object> deleteItem)     {         Sorter = sorter;         Delete = new RelayCommand((object o) => deleteItem(this));      }      /// <summary>      /// Элемент сортировки      /// </summary>      public SortBy Sorter { get; }      /// <summary>      /// Коллбек удаления элемента      /// </summary>      public ICommand Delete { get; } } /// <summary> /// Класс обертка для enum SortBy и флагов CompareType (для графического интерфейса) /// </summary> class FilterItem : SorterItem {     /// <summary>     /// Конструктор     /// </summary>     /// <param name="sorter">Элемент сортировки</param>     /// <param name="deleteItem">Коллбек удаления</param>     /// <param name="compareType">Способ сопоставления</param>     /// <param name="border">Сопоставляемая величина</param>     public FilterItem(SortBy sorter, Action<object> deleteItem,                       CompareType compareType, double border) : base(sorter, deleteItem)     {         CompareType = compareType;         Border = border;     }     /// <summary>     /// Тип сопоставления     /// </summary>     public CompareType CompareType { get; }     /// <summary>     /// Сопоставляемое значение     /// </summary>     public double Border { get; } }

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

Наличие методов удаления в классе, представляющем строку, сделано для того, чтобы можно было поставить напротив каждой из строк кнопку "Delete", как это сделано в текущей реализации. Это удобно для пользователей и достаточно интересно смотрится в реализации. Сами же методы удаления вынесены из классов и задаются в качестве делегатов от того, что требуется доступ к коллекциям данных, которые находятся в классе, представляющем ViewModel. Их реализация достаточно проста и посему не будет приводиться здесь. Все, что делают эти методы, это вызов метода Delete на экземпляре требуемой коллекции данных.

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

private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e) {     // Завершился тест или же нужно возобновить доступность кнопок заблокированных при старте оптимизации или же теста     if (e.PropertyName == "StopTest" ||         e.PropertyName == "ResumeEnablingTogle")     {         // переключатель доступности кнопок = true         EnableMainTogles = true;         // Скидываем статус и прогресс         Status = "";         Progress = 0;         // Уведомляем графику о произошедших изменениях         dispatcher.Invoke(() =>         {             OnPropertyChanged("EnableMainTogles");             OnPropertyChanged("Status");             OnPropertyChanged("Progress");         });     }     // Изменился список пройденных оптимизационных проходов     if (e.PropertyName == "AllOptimisationResults")     {         dispatcher.Invoke(() =>         {             // Отчищаем ранее сохраненные проходы оптимизаций и добавляем новые             ReportDateBorders.Clear();             foreach (var item in model.AllOptimisationResults.AllOptimisationResults.Keys)             {                 ReportDateBorders.Add(item);             }             // Выбираем самую первую дату             SelectedReportDateBorder = 0;             // Заполняем фиксированные настройки тестера в соответствии с настройками выгруженных результатов             ReplaceBotFixedParam("Expert", model.AllOptimisationResults.Expert);             ReplaceBotFixedParam("Deposit", model.AllOptimisationResults.Deposit.ToString());             ReplaceBotFixedParam("Currency", model.AllOptimisationResults.Currency);             ReplaceBotFixedParam("Laverage", model.AllOptimisationResults.Laverage.ToString());             OnPropertyChanged("OptimiserSettingsForResults_fixed");         });         // Уведомляем о завершении загрузки данных         System.Windows.MessageBox.Show("Report params where updated");     }     // Либо фильтрация либо сортировка проходов оптимизации     if (e.PropertyName == "SortedResults" ||         e.PropertyName == "FilteredResults")     {         dispatcher.Invoke(() =>         {             SelectedReportDateBorder = SelectedReportDateBorder;         });     }     // Обновлены данные по форвардным оптимизациям     if (e.PropertyName == "ForwardOptimisations")     {         dispatcher.Invoke(() =>         {             ForwardOptimisations.Clear();             foreach (var item in model.ForwardOptimisations)             {                 ForwardOptimisations.Add(new ReportItem(item));             }         });     }     // Обновлены данные по историческим оптимизациям     if (e.PropertyName == "HistoryOptimisations")     {         dispatcher.Invoke(() =>         {             HistoryOptimisations.Clear();             foreach (var item in model.HistoryOptimisations)             {                 HistoryOptimisations.Add(new ReportItem(item));             }         });     }     // Сохранен (*.csv) файл с результатами оптимизации / тестов     if (e.PropertyName == "CSV")     {         System.Windows.MessageBox.Show("(*.csv) File saved");     } }

Все условия в данном коллбэке проверяют свойство PropertyName из входного параметра "e". Первое условие выполняется в случае завершения теста и запросе модели данных на разблокировку графического интерфейса. По сути, при срабатывании данного условия мы разблокируем графический интерфейс, а также скидываем статус прогресс бара и сам прогресс бар на изначальные значения. Стоит отметить, что данное событие может вызываться в контексте вторичного потока, а уведомление графики (вызов события OnPropertyChanged) должно всегда совершаться в контексте первичного потока, т.е. в одном потоке с графическим интерфейсом. Поэтому, во избежание ошибок, мы вызываем данное событие из класса dispatcher. Dispatcher позволяет обращаться к графики из контекста потока данного окна.

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

  • Имя эксперта,
  • Депозит,
  • Валюта депозита,
  • Кредитное плечо.

И по завершении выводит MessageBox с текстом, уведомляющем о завершении обновления параметров, и таблиц отчета проходов оптимизации.

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

#region Selected optimisation date border index keeper private int _selectedReportDateBorder; public int SelectedReportDateBorder {     get => _selectedReportDateBorder;     set     {         AllOptimisations.Clear();         if (value == -1)         {             _selectedReportDateBorder = 0;             return;         }         _selectedReportDateBorder = value;         if (ReportDateBorders.Count == 0)             return;         List<OptimisationResult> collection = model.AllOptimisationResults.AllOptimisationResults[ReportDateBorders[value]];         foreach (var item in collection)         {             AllOptimisations.Add(new ReportItem(item));         }     } } #endregion

Интересующая нас часть сеттера обновляет коллекцию AllOptimisations в классе ViewModel, и оттого непонятный в начале код в рассматриваемом нами условии обретает смысл. Иначе говоря, присваивая параметру SelectedReportDateBorder значение само себя, мы просто избегаем дублирования данного цикла. 

Условия обновления данных Форвардных и Исторических таблиц выполняют ту же самую роль, что и прошлое условие, а именно — синхронизацию данных между ViewModel и Model. Данная синхронизация нужна от того, что мы не можем напрямую ссылаться на структуры, которыми оперирует модель данных, так как для описания строк таблиц требуются соответствующие классы, где каждая колонка представлена свойством. Данные классы созданы как обертки для используемых структур в модели данных. Конкретно для таблиц с отчетами оптимизации используется класс "ReportItem", чья реализация была рассмотрена в предшествующей главе.

Заключение

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

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

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

В приложении находится полный проект автооптимизатора с тестовым роботом, рассмотренным в статье №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 — зависит от выбранного типа сборки) появится скомпилированная программа.

Прикрепленные файлы |
Auto_Optimiser.zip (121.84 KB)
Good Beer
Good Beer | 10 апр 2020 в 13:54

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

Пользовательский критерий оптимизации, вижу, пока не появился и высота в календаре.... Раз уж OnTester() используется в обязательном порядке, можно было вообще обойтись только пользовательским критерием. Ну и в очередной раз хочу позудеть насчёт автоматизации ввода периодов тестирования. У меня практика показывает что тестовую форвард-оптимизацию лучше проводить на периодах по 3-5 недель, на нескольких инструментах. Писать вручную все периоды на каждый кусок - занятие не для слабонервных. Не трудно создать скрипт средствами MQL5 для получения и вывода в строку периодов, но из за ограничений песочницы и формата придётся вручную переносить их в Автооптимизатор. В ваших силах сделать это в функционале программы. Пусть лично вам это и не нужно, но такие большие и сложные статьи вы кому писали?

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

Andrey Azatskiy
Andrey Azatskiy | 10 апр 2020 в 14:08
Good Beer:

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

Пользовательский критерий оптимизации, вижу, пока не появился и высота в календаре.... Раз уж OnTester() используется в обязательном порядке, можно было вообще обойтись только пользовательским критерием. Ну и в очередной раз хочу позудеть насчёт автоматизации ввода периодов тестирования. У меня практика показывает что тестовую форвард-оптимизацию лучше проводить на периодах по 3-5 недель, на нескольких инструментах. Писать вручную все периоды на каждый кусок - занятие не для слабонервных. Не трудно создать скрипт средствами MQL5 для получения и вывода в строку периодов, но из за ограничений песочницы и формата придётся вручную переносить их в Автооптимизатор. В ваших силах сделать это в функционале программы. Пусть лично вам это и не нужно, но такие большие и сложные статьи вы кому писали?

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

Благодарю Вас за комментарий. Как я написал в заключении к статье, все дополнения я добавлю в последней статье. Это одна из ранее запланированных. Я постарался описать во всех статьях что опубликованы - всю программу целиком от той части что выгружает отчеты до внутренней структуры самого автооптимизатора. Про обещанные правки помню и уже работаю над ними. Однако опубликую их в следующей статье, так как сперва нужно было завершить описание первой версии программы. Как в университете помнится нам преподаватель один говорил : "Не будет же компания по производству автомобилей менять тормозную систему на новую, на уже выпускаемой версии автомобилей". 
Касательно кода, то он прикладывается к статье и вы можете его менять разбирать и модернизировать если будет такая потребность (обещанные правки внесу, а если потребуется что то более индивидуальное то можете добавить как вариант или же просто поразбираться если интересно будет) от того и пишу так подробно что бы была картина того как все работает. Разбиение периодов автоматическое добавлю.

Работа с таймсериями в библиотеке DoEasy (Часть 41): Пример мультисимвольного мультипериодного индикатора Работа с таймсериями в библиотеке DoEasy (Часть 41): Пример мультисимвольного мультипериодного индикатора

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

Язык MQL как средство разметки графического интерфейса MQL-программ (Часть 3). Дизайнер форм Язык MQL как средство разметки графического интерфейса MQL-программ (Часть 3). Дизайнер форм

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

Работа с таймсериями в библиотеке DoEasy (Часть 42): Класс объекта абстрактного индикаторного буфера Работа с таймсериями в библиотеке DoEasy (Часть 42): Класс объекта абстрактного индикаторного буфера

С данной статьи начнём делать классы индикаторных буферов для библиотеки DoEasy. Сегодня создадим базовый класс абстрактного буфера, который будет являться основой для создания различных типов классов индикаторных буферов.

Как создать 3D-графику на DirectX в MetaTrader 5 Как создать 3D-графику на DirectX в MetaTrader 5

Компьютерная 3D-графика хорошо подходит для анализа больших объемов данных, так как позволяет визуализировать скрытые закономерности. Такие задачи можно решать и напрямую в MQL5 — функции для работы с DireсtX позволяют при желании написать свою трехмерную игру для MetaTrader 5. Начните изучение с рисования простых объемных фигур.