连续前行优化 (第七部分): 将自动优化器的逻辑部分与图形绑定,并通过程序控制图形

Andrey Azatskiy | 14 八月, 2020

在本文中,我们将研究程序的逻辑部分如何与其图形呈现形式联系在一起。 我们将研究整个优化运行过程,从其开始逐层分析,直至自动优化器类的所有阶段。 我们还将看到逻辑程序部分与显示部分如何连接,并研究从应用程序代码管理图形的方法。 本系列的前几篇文章:

  1. 连续前行优化 (第一部分): 操控优化报告
  2. 连续前行优化 (第二部分): 创建优化报告机器人的机理
  3. 连续前行优化 (第三部分): 将机器人适配为自动优化器
  4. 连续前行优化 (第四部分): 优化管理器(自动优化器)
  5. 连续前行优化 (第五部分): 自动优化器项目概述和 GUI 的创建
  6. 连续前行优化 (第六部分): 自动优化器的逻辑部分和结构

ViewModel 类与图形层的交互

如前所述,ViewModel 是应用程序的图形部分与软件逻辑实现之间的连接器。 它是程序图形表述,其实现应用程序逻辑调用,并针对应用程序逻辑部分的回调在图形上做出反应。 相应地,来自 ViewModel 部分的公开属性对应于应用程序图形部分中的每个可编辑字段。 这些属性可以是 getter(只读),在这种情况下不能在图形中更改;也可以是 setter,如此即可覆盖隐藏在此属性后面的对象。 在前面的部分里,我们已经详细研究过数据绑定技术。 故此,我在这里仅提供一些示例。 

文本字段是通过可读写权限的属性进行连接。 举例,考虑一个字段,该字段指示正在执行优化的资产名称。 该字段的 XAML 标记极其简单。

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

除了设置文本窗口的宽度外,它还含有字段 IsEnabled 和 Text。 第一个设置该字段是否可编辑。 如果将其设置为 true,则该字段可编辑。 如果为 false,则该字段被锁定。 “Text” 字段包含在此字段中输入的文本。 然后,每个结构都有一对花括号。 其内容设置对象与特定公共属性的连接,而属性来自在 “Binding” 参数之后指定的 ViewModel 类。

后还可以跟一定数量的参数。 例如,UpdateSourceTrigger 参数指示此应用程序的图形部分的更新方法。 在我们的示例中使用的值(PropertyChanged)表示,仅当触发 ViewModel 类中的 OnPropertyChanged 事件时,图形部分才会更新,并且在 “Binding” 参数之后指定传递的名称(在本例中为 “EnableMainTogles”) 。

如果 “Text” 参数并未与字符串绑定,而是绑定 double 型参数,则此字段中仅允许数字。 如果绑定到 int 类型,则只允许整数型。 换言之,此实现能够依据需求设置输入值的类型。

在 ViewModel 部分里,字段显示如下:

IsEnabled 参数:

/// <summary> /// If the switch = false, then the most important fields are not available /// </summary> public bool EnableMainTogles { get; private set; } = true;

以及 Text 参数:

/// <summary> /// Name of the asset selected for tests / optimization /// </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 类关联的所有字段都是这些类的属性。 在上面的表格示例中,我们处理了三个类:

ListView 类的 ItemsSource属性代表由表格构成的元素集合。 将此属性与 ViewModel 的集合连接后,我们为 Window 类提供了一种 DataContext,该 DataContext 要在表格中操作。 由于我们正在谈论一个表格,因此代表集合的表格必须由含有每个表格公开属性的类构成。 将 ItemsSource 属性与 ViewModel 的属性绑定在一起之后,该属性表示一个带有数据的表格,我们可以将每列与给定表格中的所需列值进行绑定。 此外,该表格还含有 SelectedIndex 属性和 ViewModel 中的 SelectedForwardItem 属性的连接。 ViewModel 需要知道用户在此表格中所选择的行。

在 ViewModel 部分中,与表格呈现绑定的属性实现如下:

/// <summary> /// Selected forward tests /// </summary> public ObservableCollection<ReportItem> ForwardOptimisations { get; } = new ObservableCollection<ReportItem>();

C# 标准库中的 ObservableCollection 类是一个对象,用于通知图形有关修改的信息。 这是因为该类已经含所提到的事件,并在每次更新其元素列表时都会调用它。 至于其余的,它是标准的数据集合。

SelectedForwardItem 属性执行若干角色:它在所选表格行上存储数据,并作为行选择回调。

/// <summary> /// Selected forward pass /// </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]);         }     } } 

由于该属性用作回调,因此(在我们的示例中)期望针对所设置的数值做出特别反应,故此 setter 必须包含此反应的实现,并作为函数。 由此,属性值存储在私密变量中。 若要从该变量接收数值,我们可从取值器(getter)直接访问它。 若要设置一个值,在赋值器(setter)中将数值存储在 "value" 变量里。'value' 变量没有题标,在 C# 语言里作为设置数值的特定别名。 如果 “value” 大于-1,则在“结果”选项卡中填充其他相关表格,这些表格会根据所选行进行更新。 这些表格包含交易机器人参数,平均利润,交易日的亏损,以及盈亏的最高/最低值。 需要在 “if” 条件下执行检查,如果因为所选表项索引为 -1,则意味着表格为空,因此不需要填充相关表格。 AutoOptimiserVM 类代码中提供了调用方法的实现。

此处是类的实现,优化结果也含有说明行。

/// <summary> /// Class - a wrapper for a report item (for a graphical interval) /// </summary> class ReportItem {     /// <summary>     /// Constructor     /// </summary>     /// <param name="item">Item</param>     public ReportItem(OptimisationResult item)     {         result = item;     }     /// <summary>     /// Report item     /// </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 结构的包装。

表格行上的所有按钮,或在表格行上双击都与 ViewModel 的 Command 属性连接,其基本类型为 ICommand。 在前面有关图形界面创建的文章里,我们已经研究过该技术。 

ViewModel 类以及与数据模型的交互

本章开始,我们先从优化开始和停止回调说起,这两个回调在同一按钮中合并。 


单击 StartStop 按钮从 AutoOptimiserVM 类调用 _StartStopOptimisation 方法。 将来,有两种备选方案:停止优化,和开始优化。 正如您从图中可以看出,当优化器类的 IsOptimisationInProcess 属性返回 true 时,我们将执行逻辑的第一部分,并从数据模型类中请求 StopOptimisation 方法。 然后,该方法将此调用重定向到优化器。 如果尚未启动优化,则将调用数据模型类中的 StartOptimisation 方法。 该方法是异步的,因此即使 _StartStopOptimisation 操作完成,所调用的 Start 方法仍将继续操作。 

我们已经顺着执行的方法调用研究了调用链条。 现在,让我们查看描述这些方法调用与图形部件以及Model with 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> /// Callback for the graphical interface - run optimization / test /// </summary> public ICommand StartStopOptimisation { get; }

从代码和示意图中可以看出,该方法由 “If Else” 条件语句切分为两条分支。 如果正在运行,则第一条停止优化过程。 否则,第二条会启动该进程。

在启动优化的同时,我们通过设置 EnableMainTogles = false 锁定图形界面的主要区域,然后继续处理形成的输入参数。 若要开始优化,我们需要创建一个 OptimistionInputData 结构,该结构由 OptimiserSettings、BotParams、FilterItems、SorterItems 和 DateBorders 集合填充。 利用上述的数据绑定机制,值直接从图形界面填充入这些结构。 直至此结构成形后,我们为数据模型类的实例运行前面讨论的 StartOptimisation 方法。 构造函数中的 StartStopOptimisation 属性。

// Callback of optimization start/stop buttons StartStopOptimisation = new RelayCommand(_StartStopOptimisation);

其实例化经由实现了 ICommand 接口的 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 中显示错误消息。 该方法的实现已经讨论过了。 不过,我们再次查看包含此回调的实例化属性。 我们有三个不同的表格:

因此,需创建三个回调。 这是正确处理每个表格数据所必需的。 

/// <summary>
/// Run a test from a table with forward tests
/// </summary>
public ICommand StartTestForward { get; }
/// <summary>
/// Run a test from a table with historical tests
/// </summary>
public ICommand StartTestHistory { get; }
/// <summary>
/// Run a test from a table with optimization results
/// </summary>
public ICommand StartTestReport { get; }

它们的实现是通过设置 lambda 函数来执行的:

StartTestReport = new RelayCommand((object o) => {     _StartTest(model.AllOptimisationResults.AllOptimisationResults[ReportDateBorders[SelectedReportDateBorder]], SelecterReportItem); }); // Callback for the test start upon the event of double-clicking on the table with historical tests StartTestHistory = new RelayCommand((object o) => {     _StartTest(model.HistoryOptimisations, SelectedHistoryItem); }); // Callback for the test start upon the event of double-clicking on the table with historical tests StartTestForward = new RelayCommand((object o) => {     _StartTest(model.ForwardOptimisations, SelectedForwardItem); });

这种方法可以创建含有所需优化结果的列表,该列表用于获取机器人参数,其算法将参数传递到文件之中(有关详细信息,请参阅本系列文章的第三部分)。 

优化进程结束后,选择其中的最佳结果,再用历史复盘和前向验证测试,保存所有优化通关记录的列表。 由于此过程,用户可以检查所选优化器的操作逻辑,以及通过更改过滤和排序因子手动选择其他通关记录。 所以,有可能使用内置机制来过滤优化结果,并同时根据若干个标准将它们进行排序。 该机制在数据模型中实现,但是该机制的输入参数在 ViewModel 类中生成。

/// <summary> /// Sort reports /// </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> /// Filtering reports /// </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> /// Selected sorting options /// </summary> public ObservableCollection<SorterItem> SorterItems { get; } = new ObservableCollection<SorterItem>();

过滤表格被连接到:   

/// <summary> /// Selected filters /// </summary> public ObservableCollection<FilterItem> FilterItems { get; } = new ObservableCollection<FilterItem>();

描述这些表格数据行的类拥有一些重复的字段,并且在同一文件 ViewModel 中带有相应的标题。

/// <summary> /// Wrapper class for enum SortBy (for graphical interval) /// </summary> class SorterItem {     /// <summary>     /// Constructor     /// </summary>     /// <param name="sorter">Sort parameter</param>     /// <param name="deleteItem">Delete from list callback</param>     public SorterItem(SortBy sorter, Action<object> deleteItem)     {         Sorter = sorter;         Delete = new RelayCommand((object o) => deleteItem(this));      }      /// <summary>      /// Sort element      /// </summary>      public SortBy Sorter { get; }      /// <summary>      /// Item delete callback      /// </summary>      public ICommand Delete { get; } } /// <summary> /// Wrapper class for enum SortBy and CompareType flags (for GUI) /// </summary> class FilterItem : SorterItem {     /// <summary>     /// Constructor     /// </summary>     /// <param name="sorter">Sort element</param>     /// <param name="deleteItem">Deletion callback</param>     /// <param name="compareType">Comparison method</param>     /// <param name="border">Comparable value</param>     public FilterItem(SortBy sorter, Action<object> deleteItem,                       CompareType compareType, double border) : base(sorter, deleteItem)     {         CompareType = compareType;         Border = border;     }     /// <summary>     /// Comparison type     /// </summary>     public CompareType CompareType { get; }     /// <summary>     /// Comparable value     /// </summary>     public double Border { get; } }

SorterItem 类是一个对象,用于显示所选排序参数的表格行。 除了排序参数之外,它还包含指向列表中该特定参数删除回调的属性。 请注意,该回调是通过委派在外部设置的。 数据过滤器类是从 sort 类继承的:无需重复编写已实现的代码,我们可简单地从基类继承它。 除了早先研究的参数集外,它还有带阈值的数据比较类型和阈值本身

如同当前实现那样,在显示每行的类中还有删除方法,能够在每行旁边添加一个 Delete 按钮。 它对用户来说很方便,并且实现很有趣。 删除方法在类之外实现。 之所以将它们设置为委派,是因为它们需要访问位于表达 ViewModel 类中的数据集合。 它们的实现非常简单,故于此不提供。 这些方法仅针对所需的数据集合实例调用 Delete 方法。

完成一些需要图形层做出反应的事件后,将调用 OnPropertyChanged 事件。 表达 ViewModel 类中的事件回调实现如下:

private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e) {     // The test has completed, or you need to resume the availability of the buttons locked at the optimization or test start     if (e.PropertyName == "StopTest" ||         e.PropertyName == "ResumeEnablingTogle")     {         // button accessibility switch = true         EnableMainTogles = true;         // Reset status and progress         Status = "";         Progress = 0;         // Notify the GUI of changes         dispatcher.Invoke(() =>         {             OnPropertyChanged("EnableMainTogles");             OnPropertyChanged("Status");             OnPropertyChanged("Progress");         });     }     // Changed the list of passed optimization passes     if (e.PropertyName == "AllOptimisationResults")     {         dispatcher.Invoke(() =>         {             // Clear the previously saved optimization passes and add new ones             ReportDateBorders.Clear();             foreach (var item in model.AllOptimisationResults.AllOptimisationResults.Keys)             {                 ReportDateBorders.Add(item);             }             // Select the very first date             SelectedReportDateBorder = 0;             // Fill in the fixed settings of the tester in accordance with the settings of the uploaded results             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");         });         // Notify when data loading is complete         System.Windows.MessageBox.Show("Report params where updated");     }     // Filter or sort optimization passes     if (e.PropertyName == "SortedResults" ||         e.PropertyName == "FilteredResults")     {         dispatcher.Invoke(() =>         {             SelectedReportDateBorder = SelectedReportDateBorder;         });     }     // Updated forward optimization data     if (e.PropertyName == "ForwardOptimisations")     {         dispatcher.Invoke(() =>         {             ForwardOptimisations.Clear();             foreach (var item in model.ForwardOptimisations)             {                 ForwardOptimisations.Add(new ReportItem(item));             }         });     }     // Updated historical optimization data     if (e.PropertyName == "HistoryOptimisations")     {         dispatcher.Invoke(() =>         {             HistoryOptimisations.Clear();             foreach (var item in model.HistoryOptimisations)             {                 HistoryOptimisations.Add(new ReportItem(item));             }         });     }     // Save (*.csv) file with optimization/test results     if (e.PropertyName == "CSV")     {         System.Windows.MessageBox.Show("(*.csv) File saved");     } }

此回调中的所有条件都检查来自 “e” 输入参数的 PropertyName 属性。 如果测试完成,且请求数据模型解锁 GUI,则满足第一个条件。 当该情况触发时,我们解锁 GUI,将进度条状态重置,并将进度条重置为初始值。 请注意,可以在辅助线程关联中调用此事件,且图形通知(OnPropertyChanged 事件调用)必须始终在主线程中完成,即在与 GUI 相同的线程中执行。 所以,为了避免错误,要从派发类调用此事件。 派发器允许从此窗口的线程里访问 GUI。

一旦数据模型更新了所有已执行优化的列表,就会调用下一个条件。 若要通过组合框选择优化列表,我们需要在其中填入相应的优化日期。 这是通过这部分代码完成的。 它还为测试器填充了固定的参数:

之后,它显示一个 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

赋值器(setter)部分更新 ViewModel 类中的 AllOptimisations 集合,故令该条件的代码具有了意义。 换言之,把 SelectedReportDateBorder 参数设置为其自身,我们可以简单地避免重复循环。 

更新 Forward Historical 表格相关的条件,与先前条件有相同的作用,即 ViewModel 和 Model 之间的数据同步。 之所以需要这种同步,是因为我们不能直接引用数据模型操作的结构,鉴于需要相应的类来描述表格行,其中每一列均由一个属性表示。 创建这些类在数据模型中用作结构包装器。 ReportItem 类用于优化结果表格,上一章已对此进行了讨论。

结束语

本文是专门介绍前行优化和实现该过程自动优化器系列文章中的倒数第二篇。 我们已经研究了所创建应用程序最重要部分的结构。 第一篇文章介绍了应用程序中负责处理报告,并将其保存在 xml 文件里的部分。 第二部分和第三部分包含如何生成自动优化器报告,以及如何将 EA 与报告加载程序接口相连接的讲述,这在第一篇文章中已有介绍。 第四部分包含程序使用说明:在那时,我们已经研究了将任何机器人连接到自动优化器的必需步骤。

在第五、第六和第七部分里,我们研究了自动优化器程序的控制过程。 我们从其图形部分(第五篇)开始,然后研究其操作逻辑(第六篇),及其之间的联系(本文)。 在第五篇文章的评论中,用户添加了有关应用程序 UI 的一些建议。 它们当中最有趣的已被实现。

当前部分不包含这些改进,因为主要思想是讲述之前的工作。 下一篇文章(将是最后一篇文章)将包含所示意的改进,并将提供有关如何自行创建优化器的讲述。 通过优化器,我的意思是运行优化的逻辑。 当前的优化器逻辑已在前面进行了讨论(主要在第四篇文章当中)。 因此,最后一篇文章将提供有关如何创建类似逻辑的说明。 我们将利用现有的优化逻辑为基础,并研究如何逐步自行创建优化器。

附件包含第四篇文章中所分析的拥有交易机器人的自动优化器项目。 若要使用该项目,请编译自动优化器项目文件,和测试机器人文件。 然后将 ReportManager.dll(在第一篇文章中讲述)复制到 MQL5/Libraries 目录,您便可以开始测试 EA。 有关如何将自动优化器与您的智能交易系统相链接的详细信息,请参阅本系列文章的第三、四篇。

这是针对所有未曾用过 Visual Studio 的人员提供的编译过程说明。 可以在 Visual Studio 中以不同的方式编译项目,以下是其中三种:

  1. 最简单的是按 CTRL+SHIFT+B 组合键。
  2. 一种更直观的方法是在编辑器中单击绿色箭头 — 这将以代码调试模式启动应用程序,并执行编译(如果选择了调试编译模式)。
  3. 另一个选择是利用菜单中的 Build 命令。

然后,取决于所选的编译方法,已编译程序将保存在文件夹 MetaTrader Auto Optimiser/bin/Debug,或 MetaTrader Auto Optimiser/bin/Release 之内。