连续前行优化 (第六部分): 自动优化器的逻辑部分和结构

Andrey Azatskiy | 31 七月, 2020

概述

我们继续讲述实现连续前行优化的自动优化器的创建。 在上一篇文章中,我们分析了所得应用程序的图形界面,但尚未研究其逻辑部分和内部结构。 这就是本文将要讲述的内容。 本系列的前几篇文章:

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

我们将用 UML 示意图描述应用程序的内部结构,以及应用程序在操作过程中所要执行的调用。 请注意,这些示意图的目的是为了提供主要对象及其之间相互关系的示意图,而不是挨个讲述现有对象。

应用程序内部结构,其描述和关键对象的生成

如前一篇文章所述,所得程序里使用的主要模式是 MVVM。 根此模式,整个程序的逻辑应在数据模型类中实现,其与图形的连接是通过单独实现的 ViewModel 对象角色类。 程序逻辑进一步按基本实体划分为多个类。 以下的类 UML 示意图展示了主要程序实体的逻辑,以及登录和 UI 之间关系的。    


在研究该示意图之前,我们先查看不同对象类型的颜色指示。 蓝色表示图形层。 这些是表示 XAML 标记的对象,所有 WPF 机制都隐藏在其内,这些对最终用户和开发人员都不可见。 紫色表示应用程序图形与其逻辑的连接层。 换言之,它是来自所用 MVVM 模型的 ViewModel 层。 粉色表示接口,这些接口是隐藏其后数据的抽象表示。

它们中的第一个(IMainModel)隐藏了数据模型的特定实现。 根据 MVVM 模式的主要思想,数据模型必须尽可能独立,而 ViewModel 不应依赖于此模型的特定实现。 第二个(IOptimiser)是优化逻辑的接口,因为根据程序思路,可以运行多个优化并选择逻辑,且用户可以从组合框中选择相应的优化器来将之更改。

棕色表示图形界面中的数据模型层。 如您所见,图中有两个数据模型:第一个数据模型是指自动优化器本身,第二个数据模型是指优化器的图形界面。 黄色表示当前存在的唯一优化管理器。 不过,可以有多个优化管理器。 您还可以实现自己的优化逻辑(实现机制的方法将在后续文章中介绍)。绿色表示辅助对象,这些辅助对象是作为工厂,并实现创建当前所需的对象。

进一步,我们来研究对象之间的关系,及其在应用程序启动期间的创建过程。 在此之前,我们需要考虑图形层及其组成部分:


这些是示意图中显示的前五个对象。 在应用程序启动期间,将首先实例化 AutoOptimiser 类。 该类会创建图形界面。 图形界面的 XAML 标记包含充当 ViewModel 的 AutoOptimiserVM 对象引用。 所以,在创建图形层期间,还将创建 AutoOptimiserVM 类,而图形层则完全拥有它。 该对象一直存在,直到图形界面被销毁才会被注销。 它经由 “Composition” 与 AutoOptimiser 类(我们的窗口)连接,这意味着该对象的完全所有权和控制权。  

ViewModel 类必须有权访问 Model 类,但数据模型类必须要保持独立于 ViewModel。 换言之,它不需要知道哪个类提供数据模型。 代之,ViewModel 类知道模型接口,它包含中介层可使用的一套公开方法、事件和属性。 这就是为什么该类不直接连接到 MainModel 类,而是根据所分析类的所属关系,通过“聚合”关系与调用它的类相连接。

然而,“聚合”和“组合”之间的区别之一是,所分析的类一次可能属于多个对象,且其生命周期处理不受容器对象的控制。 该语句对于 MainModel 类完全正确,因为它是在其静态构造函数(MainModelCreator 类)中创建的,并同时存储在其内部和 AutoOptimiserVM 类中。 当应用程序完成其操作时,该对象将被销毁。 这是因为它最初是在静态属性中实现的,只有在应用程序完成时才将其清除。   

我们已研究了三个关键对象之间的关系:模型—视图—ViewModel。 示意图的其余部分专门针对我们应用程序的主要业务逻辑。 它体现了负责优化过程的对象,与数据模型对象之间的关系。 负责优化控制过程的对象充当一种控制器,它启动所需的过程,并将其执行委派给独立的程序对象。 其中之一是优化器。 优化器也是一个管理器,将任务的执行委派给面向任务的对象,例如终端启动,或终端启动所需的配置文件的生成。 


在 MainModel 类的实例化过程中,我们还会用到已经熟悉的静态构造函数机制来实例化优化器类。 正如图中所见,优化器类应该实现 IOptimiser 接口,并且应该含有派生自 OptimiserCreator 的构造函数类 - 它会创建优化器的特定实例。 这是在程序执行模式下实现动态优化程序替代所必需的。

每个优化器都可拥有独立的优化逻辑。 当前优化程序的逻辑和优化程序的实现将在以后的文章中详细介绍。 现在,我们回到架构。 数据模型类通过关联关系与所有模型构造函数的基类相连,这意味着数据模型类利用优化器的构造函数强制转换为其基类,并创建相应优化器的实例。

所创建优化器被强制转换为其接口类型,并保存在 MainModel 类的相应字段中。 因此,在对象创建(对象构造函数)和实例创建(优化器)期间利用抽象,我们提供了在程序执行过程中动态替换优化器的可能性。 所使用的方式称为“抽象工厂”。 其思路是产品(实现优化逻辑的类)及其工厂(创建产品的类)都拥有自己的抽象。 用户类不需要了解两个组件的逻辑及特定实现,但是它必须能够调用它们的不同实现。

作为现实生活中的一个例子,我们可以使用苏打水、茶、咖啡或类似产品,以及生产它们的工厂。 一个人不需要知道这些饮料的具体生产方法就可以饮用。 还有,人们不需要知道生产饮料的工厂,或出售它们的商店的确定内部结构。 在此示例中:

在我们的程序中,用户是 MainModel 类。


如果您查看默认的优化器实现,您将看到它也有带设置的图形界面(单击列举出的所有优化器的 ComboBox 旁边的 “GUI” 按钮来调用)。 在类的示意图(和代码)中,优化器设置的图形部分称为 “SimpleOptimiserSettings”,而 ViewModel 和 View 分别称为 “SimpleOptimiserVM” 和 “SimpleOptimiserM”。 正如类图中所见,优化器设置的 ViewModel 完全归图形部分所有,因此通过“组合”关系进行连接。 视图部分由优化器完全拥有,并通过“组合”关系与 Manager 类连接。 优化器设置数据模型的一部分既属于优化器,又属于 ViewModel,这就是为什么它与两者都具有“聚合”关系。 故意这样做是为了允许优化器访问存储在优化器设置图形数据模型中的设置。      

为了完成本章节,我在这里提供了一个序列图,展示上述对象的实例化过程。


该示意图应从上至下阅读。 所显示过程的起点是 “Instance”,它以优化器主窗口的图形层的实例显示应用程序的开始时刻。 在实例化期间,图形界面实例化 SimpleOptimiserVM 类,因为它被声明为主窗口的 DataContext。 在实例化期间, SimpleOptimiserVM 调用 MainModelCreator.Model 静态属性,该静态属性又生成 MainModel 对象,并将其强制转换为 IMainModel 接口类型。

MainModel 类实例化之时,会创建一个优化器构造函数的列表。 这是 ComboBox 中显示的列表,可选择所需的优化器。 数据模型实例化之后,调用 SimpleOptimiserVM 类的构造函数,该构造函数从 IMainModel 接口类型的数据模型中调用 ChangeOptimiser 方法。 ChangeOptimiser 方法依据所选优化器的构造函数调用 Create() 方法。 由于我们正在观察应用程序的启动,因此选择的优化器构造函数来自指定列表的第一个。 依据所需优化器构造函数调用 Create 方法,我们将特定优化器类型的创建委托给构造函数。 它创建优化器,将优化器对象强制转换为接口类型,然后将其传递给数据模型,在该处将其保存在相应的属性之中。 在此之后,ChangeOptimiser 方法操作完成,我们可以回到 SimpleOptimiserVM 类的构造函数。

Model 类和逻辑程序部分

我们已研究了所得应用程序的一般结构,以及在应用程序启动时创建主要对象的过程。 现在,我们继续研究其逻辑实现细节。 描述所创建应用程序逻辑的所有对象都位于 “Model” 目录当中。 根目录含有 “MainModel.cs” 文件,该文件包含数据模型类,其为启动应用程序的整个业务逻辑的起点。 它的实现包含 1000 多行代码,故在此我会不提供整个类的代码,而仅提供单一方法的实现。 该类继承自 IMainModel 接口。 此处的接口代码展现其结构。

/// <summary>
/// Data model interface of the main optimizer window
/// </summary>    
interface IMainModel : INotifyPropertyChanged
{
    #region Getters
    /// <summary>
    /// Selected optimizer
    /// </summary>
    IOptimiser Optimiser { get; }
    /// <summary>
    /// The list of names of terminals installed on the computer
    /// </summary>
    IEnumerable<string> TerminalNames { get; }
    /// <summary>
    /// The list of names of optimizers available for usage
    /// </summary>
    IEnumerable<string> OptimisatorNames { get; }
    /// <summary>
    /// The list of names of directories with saved optimizations (Data/Reports/*)
    /// </summary>
    IEnumerable<string> SavedOptimisations { get; }
    /// <summary>
    /// Structure with all passes of optimization results
    /// </summary>
    ReportData AllOptimisationResults { get; }
    /// <summary>
    /// Forward tests
    /// </summary>
    List<OptimisationResult> ForwardOptimisations { get; }
    /// <summary>
    /// Historical tests
    /// </summary>
    List<OptimisationResult> HistoryOptimisations { get; }
    #endregion

    #region Events
    /// <summary>
    /// Event of exception throw form the data model
    /// </summary>
    event Action<string> ThrowException;
    /// <summary>
    /// Optimization stop error
    /// </summary>
    event Action OptimisationStoped;
    /// <summary>
    /// Event of progress bar update form the data model
    /// </summary>
    event Action<string, double> PBUpdate;
    #endregion

    #region Methods
    /// <summary>
    /// Method loading previously saved optimization results
    /// </summary>
    /// <param name="optimisationName">The name of the required report</param>
    void LoadSavedOptimisation(string optimisationName);
    /// <summary>
    /// Method changing the previously selected terminal
    /// </summary>
    /// <param name="terminalName">ID of the requested terminal</param>
    /// <returns></returns>
    bool ChangeTerminal(string terminalName);
    /// <summary>
    /// Optimizer change method
    /// </summary>
    /// <param name="optimiserName">Optimizer name</param>
    /// <param name="terminalName">Terminal name</param>
    /// <returns></returns>
    bool ChangeOptimiser(string optimiserName, string terminalName = null);
    /// <summary>
    /// Optimization start
    /// </summary>
    /// <param name="optimiserInputData">Input data to launch optimization</param>
    /// <param name="IsAppend">Flag showing whether to add to existing data (if any) or overwrite them</param>
    /// <param name="dirPrefix">Prefix of the directory with optimizations</param>
    void StartOptimisation(OptimiserInputData optimiserInputData, bool IsAppend, string dirPrefix);
    /// <summary>
    /// Optimization stop from outside (by user)
    /// </summary>
    void StopOptimisation();
    /// <summary>
    /// Get robot parameters
    /// </summary>
    /// <param name="botName">Expert name</param>
    /// <param name="isUpdate">Flag whether file needs to be updated before reading</param>
    /// <returns>List of parameters</returns>
    IEnumerable<ParamsItem> GetBotParams(string botName, bool isUpdate);
    /// <summary>
    /// Saving selected optimizations to the (* .csv) file 
    /// </summary>
    /// <param name="pathToSavingFile">Path to the file to be saved</param>
    void SaveToCSVSelectedOptimisations(string pathToSavingFile);
    /// <summary>
    /// Saving optimizations for the transferred date to the (* csv) file 
    /// </summary>
    /// <param name="dateBorders">Date range borders</param>
    /// <param name="pathToSavingFile">Path to the file to be saved</param>
    void SaveToCSVOptimisations(DateBorders dateBorders, string pathToSavingFile);
    /// <summary>
    /// Start the testing process
    /// </summary>
    /// <param name="optimiserInputData">List of tester setup parameters</param>
    void StartTest(OptimiserInputData optimiserInputData);
    /// <summary>
    /// Start the sorting process
    /// </summary>
    /// <param name="borders">Date range borders</param>
    /// <param name="sortingFlags">Array of parameter names for sorting</param>
    void SortResults(DateBorders borders, IEnumerable<SortBy> sortingFlags);
    /// <summary>
    /// Filtering optimization results
    /// </summary>
    /// <param name="borders">Date range borders</param>
    /// <param name="compareData">Data filtering flags</param>
    void FilterResults(DateBorders borders, IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData);
    #endregion
}

接口的组件由 #region 指令分隔。 因此,接口成员被划分成典型的组件。 如您所见,它拥有许多属性,可为图形界面从数据模型的常规字段提取各种信息。 然而,这些只是限制访问数据的取值器,仅允许读取它们,而不能重写正在读取的对象。 这样做是为了防止 ViewModel 逻辑中的意外数据模型损坏。 关于接口属性的趣事之一就是优化结果列表:

这些字段包含优化列表,能够显示在 GUI 的 “Results” 选项卡的表格里。 所有的优化通关列表都包含在一个特殊创建的结构 “ReportData” 之中:

/// <summary>
/// Structure describing optimization results
/// </summary>
struct ReportData
{
    /// <summary>
    /// Dictionary with optimization passes
    /// key - date range
    /// value - list of optimization passes for the given range
    /// </summary>
    public Dictionary<DateBorders, List<OptimisationResult>> AllOptimisationResults;
    /// <summary>
    /// Expert and Currency
    /// </summary>
    public string Expert, Currency;
    /// <summary>
    /// Deposits
    /// </summary>
    public double Deposit;
    /// <summary>
    /// Leverage
    /// </summary>
    public int Laverage;
}

除了优化数据外,该结构还描述了主要的优化器设置,这些设置是启动测试(双击所选优化通关项),以及为比较优化结果而将新数据添加到先前的优化数据之中时所需的。

此外,数据模型包含计算机上已安装的所有终端列表,可供选择的优化器名称(由这些优化器的构造函数创建),和之前保存的优化列表(位于目录名称 “Data/Reports” 当中)。 还提供了针对优化器本身的访问。

信息的逆向交换(从模型到 View 模型)是利用实例化数据模型后 ViewModel 订阅的事件来执行的。 有 4 个此类事件,其中 3 个是自定义的,一个是从 INotifyPropertyChanged 接口继承而来的。 数据模型不需要从 INotifyPropertyChanged 接口继承。 但这对我来说好似很方便,这就是为什么在此程序中要用继承的原因。

事件之一是 ThrowException。 最初,创建它是为了将错误消息发送到应用程序的图形部分,之后显示它,因为您不应直接从数据模型控制图形。 然而,现在该事件还用于将大量文本消息从数据模型传递至图形。 这些不是错误,而是文本警报。 因此请注意,该事件将来传递的不是错误消息。 

为了研究数据模型的方法,我们来查看实现该程序部分的类。 

选择新机器人时,优化器要做的第一件事就是加载其参数。 这是靠实现了两种潜在逻辑的 “GetBotParams” 方法完成的。 它可以用机器人参数更新配置文件,并可以简单地读取它。 它也可以是递归的。 

/// <summary>
/// Get parameters for the selected EA
/// </summary>
/// <param name="botName">Expert name</param>
/// <param name="terminalName">Terminal name</param>
/// <returns>Expert parameters</returns>
public IEnumerable<ParamsItem> GetBotParams(string botName, bool isUpdate)
{
    if (botName == null)
        return null;

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


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

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

            Config config = new Config(iniFile.FullName);

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

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

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

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

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

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

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

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

在方法伊始,我们用 C# 标准库中提供的 FileInfo 类依据机器人参数来创建文件的面向对象表示。 根据标准终端设置,该文件保存在目录 MQL5/Profiles/Tester/{所选机器人名称}.set 之中。 这是在创建面向对象的文件表示形式时设置的路径。 更多的动作包装在 try-catch 构造中,因为在文件操作过程中存在抛出错误的风险。 现在,根据所传递 isUpdate 参数执行可能的逻辑分支之一。 如果 isUpdate = true,则必须用默认值更新文件,然后读取其参数。 单击应用程序图形部分中的 “Update(*.set) file” 时,将执行此逻辑分支。 采用智能系统的设定来更新文件的最便捷途径是重新生成文件。

如果在测试器中选择机器人时该文件不存在,则由策略测试器生成该文件。 所以,我们要做的就是删除文件后重新启动测试器,然后等待文件生成,再关闭测试器,并返回其默认值。 首先,检查终端是否正在运行。 如果它正在运行,则显示相应的消息,并等待其结束。 然后检查含参数的文件是否存在。 如果有这样的文件,要将其删除。

然后,为了启动终端,用先前文章中已研究过的 Config 填充配置文件。 注意在配置文件写入日期。 我们在终端中启动测试,但是测试开始日期指定为结束日期的前 1 天。 由此,测试器开始并生成含有所需设置的文件。 然后它无法启动测试并完成其操作,此后我们就可以读取文件。 一旦创建并准备好配置文件,将用 TerminalManager 类启动设置文件生成的过程(该过程之前已研究过)。 文件生成完成后,我们利用 SetFileManager 类读取含有设置的文件,并返回其内容。

如果需要其它逻辑分支,若该分支不需要显式生成设置文件,则选择条件的第二部分。 该方法读取 EA 设置文件,并返回其内容,或用参数 isUpdate = true 递归启动该方法,即执行早前研究的逻辑部分。

另一个有趣的方法是 “StartOptimisation”:

/// <summary>
/// Start optimizations
/// </summary>
/// <param name="optimiserInputData">Input data for the optimizer</param>
/// <param name="isAppend">Flag whether data should be added to a file?</param>
/// <param name="dirPrefix">Directory prefix</param>
public async void StartOptimisation(OptimiserInputData optimiserInputData, bool isAppend, string dirPrefix)
{
    if (string.IsNullOrEmpty(optimiserInputData.Symb) ||
        string.IsNullOrWhiteSpace(optimiserInputData.Symb) ||
        (optimiserInputData.HistoryBorders.Count == 0 && optimiserInputData.ForwardBorders.Count == 0))
    {
        ThrowException("Fill in asset name and date borders");
        OnPropertyChanged("ResumeEnablingTogle");
        return;
    }

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

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

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

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

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

该方法是异步的,编写时采用异步等待技术,它提供了一种最简单的异步方法声明。 首先,检查所传递品种名称和优化范围。如果其中任何一个缺失,则解锁被冻结的 GUI(优化开始时某些 GUI 按钮被冻结),并显示错误消息,之后 函数执行应完成。 如果终端已经在运行,则执行完全相同的操作。 如果选择了测试模式来替代优化,则将进程的执行重定向到启动测试的方法

如果选择了追加模式,则利用优化功能删除目录中的所有文件,以及所有子目录。 然后继续运行优化。 优化过程异步启动,因此在执行此任务时它不会阻塞 GUI。 它也被包装到 try-catch 结构中,以便应对错误发生。 在开始处理之前,我们将所有先前执行的优化缓存文件复制到自动优化器在 Data 工作目录中创建的临时目录当中。 这样可确保即使早前曾启动过优化,也能正确启动它们。 然后,清除所有先前写入优化器局部变量的数据,并启动优化过程。 优化启动参数之一是机器人生成的报告文件的路径。 如早前第三篇文章所述,该报告的名称为 {robot name}_Report.xml。 在自动优化器中,此名称由以下行指定:

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

这是通过字符串串联完成的,其中机器人名称是由机器人的路径形成的,该路径在优化文件里是指定参数之一。 优化停止过程已完全转移到优化器类。 实现它的方法只是在优化器类的实例上调用 StopOptimisation 方法。

/// <summary>
/// Complete optimization from outside the optimizer
/// </summary>
public void StopOptimisation()
{
    Optimiser.Stop();
}

测试由数据模型类中实现的方法启动,并不是优化器。

/// <summary>
/// Run tests
/// </summary>
/// <param name="optimiserInputData">Input data for the tester</param>
public async void StartTest(OptimiserInputData optimiserInputData)
{
    // Check if the terminal is running
    if (Optimiser.TerminalManager.IsActive)
    {
        ThrowException("Terminal already running");
        return;
    }

    // Set the date range
    #region From/Forward/To
    DateTime Forward = new DateTime();
    DateTime ToDate = Forward;
    DateTime FromDate = Forward;

    // Check the number of passed dates. Maximum one historical and one forward
    if (optimiserInputData.HistoryBorders.Count > 1 ||
        optimiserInputData.ForwardBorders.Count > 1)
    {
        ThrowException("For test there must be from 1 to 2 date borders");
        OnPropertyChanged("ResumeEnablingTogle");
        return;
    }

    // If both historical and forward dates are passed
    if (optimiserInputData.HistoryBorders.Count == 1 &&
        optimiserInputData.ForwardBorders.Count == 1)
    {
        // Test the correctness of the specified interval
        DateBorders _Forward = optimiserInputData.ForwardBorders[0];
        DateBorders _History = optimiserInputData.HistoryBorders[0];

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

        // Remember the dates
        Forward = _Forward.From;
        FromDate = _History.From;
        ToDate = (_History.Till < _Forward.Till ? _Forward.Till : _History.Till);
    }
    else // If only forward or only historical data is passed
    {
        // Save and consider it a historical date (even if forward was passed)
        if (optimiserInputData.HistoryBorders.Count > 0)
        {
            FromDate = optimiserInputData.HistoryBorders[0].From;
            ToDate = optimiserInputData.HistoryBorders[0].Till;
        }
        else
        {
            FromDate = optimiserInputData.ForwardBorders[0].From;
            ToDate = optimiserInputData.ForwardBorders[0].Till;
        }
    }
    #endregion

    PBUpdate("Start test", 100);

    // Run test in the secondary thread
    await Task.Run(() =>
    {
        try
        {
            // Create a file with EA settings
            #region Create (*.set) file
            FileInfo file = new FileInfo(Path.Combine(Optimiser
                                             .TerminalManager
                                             .TerminalChangeableDirectory
                                             .GetDirectory("MQL5")
                                             .GetDirectory("Profiles")
                                             .GetDirectory("Tester")
                                             .FullName, $"{Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot)}.set"));

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

            // Fill the expert settings with those that were specified in the graphical interface
            for (int i = 0; i < optimiserInputData.BotParams.Count; i++)
            {
                var item = optimiserInputData.BotParams[i];

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

            // Save settings to a file
            SetFileManager setFile = new SetFileManager(file.FullName, false)
            {
                Params = botParams
            };
            setFile.SaveParams();
            #endregion

            // Create terminal config
            #region Create config file
            Config config = new Config(Optimiser.TerminalManager
                                                .TerminalChangeableDirectory
                                                .GetDirectory("config")
                                                .GetFiles("common.ini")
                                                .First().FullName);
            config = config.DublicateFile(Path.Combine(workingDirectory.WDRoot.FullName, $"{Optimiser.TerminalManager.TerminalID}.ini"));

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

            // Configure the terminal and launch it
            Optimiser.TerminalManager.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
            Optimiser.TerminalManager.Config = config;
            Optimiser.TerminalManager.Run();

            // Wait for the terminal to close
            Optimiser.TerminalManager.WaitForStop();
        }
        catch (Exception e)
        {
            ThrowException(e.Message);
        }

        OnPropertyChanged("StopTest");
    });
}

在检查知晓终端是否正在运行之后,继续设置历史和前向验证测试的日期。 您可以设置一个历史范围,也可以设置一个历史范围和一个前向验证范围。 如果在设置中仅指定了前向验证区间,则它将视为历史间隔。 首先,我们声明存储测试日期的变量(前向验证,最后测试日期,测试开始日期)。 然后检查该方法 — 如果历史范围边界或前向验证测试边界多于一个,则显示错误消息。 然后,设置边界 — 这个条件的想法是,在三个声明的变量之间设置四个通关日期(如果只应设置历史区间,则设置两个)。

测试开始也包装在 try-catch 构造中。 首先,生成带有机器人参数的文件,并取所传递机器人的参数填充。 这是通过调用早前研究的 SetFileManager 对象完成的。 然后根据指令创建配置文件,并启动测试过程。 之后,等待终端关闭。 一旦方法操作完成后,通知图形测试已完成。 这必须通过事件来完成,因为此方法是异步的,且程序操作在被调用之后可继续执行,而不必等待所调用方法完成。

对于优化过程,优化器还将利用优化过程完成事件,通知数据模型优化过程即将结束。 最后一篇文章将对此进行更详细的讨论。

结束语

在以前的文章中,我们详细分析了将算法与所创建自动优化器,及其某些部分组合的过程。 我们已研究了优化报告的逻辑,并已看到其在交易算法中的应用。 在前一篇文章当中,我们研究了图形界面(程序的 View 部分),和项目文件的结构。

我们还从程序的视角分析了项目的内部结构、类之间的交互,以及优化过程的启动。 由于该程序支持多种优化逻辑,因此我们没有详细研究所实现逻辑 — 最好在单独文章里阐述该逻辑,并作为优化器实现的示例。 我们还会有两篇文章,其中我们将分析逻辑部分与图形的联系,以及讨论优化器实现算法,并研究优化器实现的示例。

附件包含第四篇文章中所分析的拥有交易机器人的自动优化器项目。 若要使用该项目,请编译自动优化器项目文件,和测试机器人文件。 然后将 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 之内。