优化管理(第二部分):创建按键对象和附加逻辑

4 十月 2019, 12:18
Andrey Azatskiy
0
1 387

目录

简介

本文提供了一种方便的GUI创建过程的进一步描述,其目的是同时管理多个终端中的优化。前面的文章探讨了从控制台启动终端的方法,并包含了配置文件的描述。在本文中,我们开始为终端创建一个C#包装,它将使优化管理成为第三方进程。先前探讨过的 GUI 没有逻辑,无法执行任何操作。它只能响应按键,按键将按下的键的文本输出到控制台(从控制台启动)。在这一部分中,将添加一个逻辑,它将处理GUI事件,并将实现嵌入式逻辑。将创建多个与文件一起工作的对象,这将允许通过该对象实现程序操作的逻辑部分,而不是使用文件——这将简化操作并使代码更具信息性。在本文中,应用程序将最终采用视频中演示的形式。



外部终端管理器 (ITerminalManager 和 Config)

早些时候,我们检查了为我们的插件创建一个图形层,这一部分探讨了逻辑组件的创建方法。利用 OOP (面向对象编程)的优点,将逻辑部分分成若干类,每个类负责其特定的区域。让我们从执行与文件和终端相关的特定操作的类开始。在此之后,我们将继续使用由此产生的 ExtentionGUI_M 类,其中描述了最终的逻辑。让我们从逻辑实现中使用的类开始。 

本章介绍终端操作,将探讨以下组成部分:

  1. 操作配置文件
  2. 与第三方进程类似的终端工作

让我们从配置文件开始,在终端参考部分提供了详细信息。首先,我们需要创建所有需要的变量,这些变量将在配置文件中应用:这些变量的数值可以在终端中查看,而它们的实现在文件 Config.cs 中。让我们来看看一个传递服务器地址的方便方法的实现。请注意,它应该以特定格式传递,并带有额外指定的端口号。这个问题可以通过创建一个类来解决,该类存储通过构造函数接收的服务器地址,并在安装之前检查其正确性。

/// <summary>
/// IPv4 服务器地址和端口
/// </summary>
class ServerAddressKeeper
{
    public ServerAddressKeeper(IPv4Adress ip, uint port)
    {
        IP = ip;
        Port = port;
    }
    public ServerAddressKeeper(string adress)
    {
        if (string.IsNullOrEmpty(adress) || string.IsNullOrWhiteSpace(adress))
            throw new ArgumentException("地址不正确");

        string[] data = adress.Split(':');

        if (data.Length != 2)
            throw new ArgumentException("地址不正确");

        IP = new IPv4Adress(data[0]);
        Port = Convert.ToUInt32(data[1]);
    }

    public IPv4Adress IP { get; }
    public uint Port { get; }

    public string Address => $"{IP.ToString()}:{Port}";
}

/// <summary>
/// IPv4 服务器地址
/// </summary>
struct IPv4Adress
{
    public IPv4Adress(string adress)
    {
        string[] ip = adress.Split('.');
        if (ip.Length != 4)
            throw new ArgumentException("ip 不正确");

        part_1 = (char)Convert.ToInt32(ip[0]);
        part_2 = (char)Convert.ToInt32(ip[1]);
        part_3 = (char)Convert.ToInt32(ip[2]);
        part_4 = (char)Convert.ToInt32(ip[3]);
    }

    public char part_1;
    public char part_2;
    public char part_3;
    public char part_4;

    public new string ToString()
    {
        return $"{(int)part_1}.{(int)part_2}.{(int)part_3}.{(int)part_4}";
    }
}

这个类包含了保存服务器IP地址的 IPv4Adress 结构。在为本文准备数据时,我没有遇到与 IPv4 格式不同的单个服务器地址,因此正好实现了这种格式。在其构造函数中,结构接受带有地址的字符串,然后对其进行解析并保存到适当的字段中。如果地址中的位数小于4,则返回错误。主类构造函数有两个重载,其中一个重载接受带有服务器地址的字符串,另一个重载接受已形成的IP地址和端口号。此外,IPv4Adress 结构具有重载的 ToString方法,该方法派生自基本对象类,所有C#对象都从该类隐式继承。ServerAddressKeeper 类含有Address属性, 实现了相同的对象。因此,我们有一个包装类,它将服务器地址存储在一个方便的表单中,并可以将其组装成配置文件所需的表单。  

现在我们需要考虑使用 *.ini 格式配置文件的方法。如前所述,这种文件格式被认为是过时的,现在很少使用。C# 没有用于处理这些文件的内置接口,类似于在前一篇文章中考虑的使用 XML 标记的那些接口。但是,WinApi 仍然支持使用此文件格式的WritePrivateProfileStringGetPrivateProfileString函数。以下是Microsoft的说明:

此函数仅用于与基于16位Windows的应用程序兼容。应用程序应在注册表中存储初始化信息。

我们可以使用它,避免需要开发自己的解决方案。为此,我们必须将 C 函数的数据导入到 C# 代码中。在 C# 中,这可以类似于 MQL5:

[DllImport("kernel32.dll", SetLastError = true)]
private extern static int GetPrivateProfileString(string AppName, string KeyName, string Default, StringBuilder ReturnedString, int Size, string FileName);

[DllImport("kernel32.dll", SetLastError = true)]
private extern static int WritePrivateProfileString(string AppName, string KeyName, string Str, string FileName);

和 #import 相反,这里,我们必须指定 DLLImport 属性,并传递给它的 DLL 名称,从中导入函数,以及其他可选参数。特别的是,在引入时,我指定了参数 SetLastErro = true, 这使得可以 在我们的C#代码中取得使用 C++ 代码调用 GetLastError() 取得的错误,以便控制这些方法的正确执行。由于 C# 和 C 具有不同的字符串操作方法,所以让我们使用包装器方法,这些方法可以方便地处理导出的函数,并且可以处理可能的错误。我以如下方式实现它们:   

/// <summary>
/// WinAPI 函数 GetPrivateProfileString 的方便包装
/// </summary>
/// <param name="section">section name</param>
/// <param name="key">key</param>
/// <returns>the requested parameter or null if the key was not found</returns>
protected virtual string GetParam(string section, string key)
{
    //取得数值
    StringBuilder buffer = new StringBuilder(SIZE);
 
   //把数值放入缓冲区
    if (GetPrivateProfileString(section, key, null, buffer, SIZE, Path) == 0)
        ThrowCErrorMeneger("GetPrivateProfileStrin", Marshal.GetLastWin32Error());

    //返回收到的值
    return buffer.Length == 0 ? null : buffer.ToString();
}

/// <summary>
/// WinAPI WritePrivateProfileString的方便包装
/// </summary>
/// <param name="section">Section</param>
/// <param name="key">Key</param>
/// <param name="value">Value</param>
protected virtual void WriteParam(string section, string key, string value)
{
    //把值写到 INI 文件中
    if (WritePrivateProfileString(section, key, value, Path) == 0)
        ThrowCErrorMeneger("WritePrivateProfileString", Marshal.GetLastWin32Error());
}

/// <summary>
/// Return error
/// </summary>
/// <param name="methodName">Method name</param>
/// <param name="er">Error code</param>
private void ThrowCErrorMeneger(string methodName, int er)
{
    if (er > 0)
    {
        if (er == 2)
        {
            if (!File.Exists(Path))
                throw new Exception($"{Path} - File doesn1t exist");
        }
        else
        {
            throw new Exception($"{methodName} error {er} " +
                $"See System Error Codes (https://docs.microsoft.com/ru-ru/windows/desktop/Debug/system-error-codes) for detales");
        }
    }
}

当使用这些方法时,我遇到了一个有趣的特征。在寻找其他信息后,我确定这不仅仅是我的问题。其特点是 GetPrivateProfileString 方法不仅在找不到文件的情况下返回ERROR_FILE_NOT_FOUND(错误代码=2),而且在以下情况下返回:

  1. 该片段部分不存在于读取文件中。
  2. 所请求的键名不存在。

由于这个特定的特性,在错误的情况下,我们 ThrowCErrorMeneger方法中进行检查文件是否存在。为了取得最近的错误 (GetLastError 方法), C# 的 Marshal 类有一个静态方法 ( Marshal.GetLastWin32Error()), 我们可以使用它在调用文件读取或写入后取得错误信息。为了方便起见,导入了只读取和写入字符串的方法,因为任何数据类型都可以强制转换为字符串。 

另一个有趣的函数操作方面是从文件中删除数据的方式。例如,为了删除整个段,有必要传递到 WriteParam方法,键名等于NULL。使用这种功能,我创建了一个对应的方法。所有片段的名称加到 ENUM_SectionType 中:

/// <summary>
/// 删除片段
/// </summary>
/// <param name="section">section selected for deletion</param>
public void DeleteSection(ENUM_SectionType section)
{
    WriteParam(section.ToString(), null, null);
}

还有一种删除特定键的方法,必须为其指定键名,但是键值必须为空。在该方法实现中,传递的键的名称被保留为字符串字段,因为每个部分都具有唯一的键。

/// <summary>
/// 删除键
/// </summary>
/// <param name="section">section from which key should be deleted</param>
/// <param name="key">Key to delete</param>
public void DeleteKey(ENUM_SectionType section, string key)
{
    if (string.IsNullOrEmpty(key) || string.IsNullOrWhiteSpace(key))
        throw new ArgumentException("Key is not vailed");

    WriteParam(section.ToString(), key, null);
}

为了方便访问片段,我决定通过属性实现它们,这样Config类实例允许通过点(.)操作符访问任何片段,然后访问该片段的任何键,如下图所示:

Config myConfig = new Config("Path");

myConfig.Tester.Expert = MyExpert;
string MyExpert = myConfig.Tester.Expert; 

显然,要实现这个想法,我们需要为每个部分创建一个类。在每个片段的类中,我们需要指定在文件中写入和读取此特定字符串的属性。因为片段实际上是这个特定初始化文件的组件,而 Config 是这个文件的面向对象表示,一个合理的解决方案是创建将这些部分描述为 Config 类的嵌套类的类,然后在 Config 类中设置只读属性,它们应该是按这些特定类划分的类型。在下面的示例中,省略了不必要的代码部分,因此仅演示上述描述:   

class Config
{
    public Config(string path)
    {
        Path = path;
        CreateFileIfNotExists();

        Common = new CommonSection(this);
        Charts = new ChartsSection(this);
        Experts = new ExpertsSection(this);
        Objects = new ObjectsSection(this);
        Email = new EmailSection(this);
        StartUp = new StartUpSection(this);
        Tester = new TesterSection(this);
    }

    protected virtual void CreateFileIfNotExists()
    {
        if (!File.Exists(Path))
        {
            File.Create(Path).Close();
        }
    }

    public readonly string Path; // path to file

    public virtual Config DublicateFile(string path)
    {
        File.Copy(Path, path, true);
        return new Config(path);
    }

    #region Section managers
    internal class CommonSection
    {
    }
    internal class ChartsSection
    {
    }
    internal class ExpertsSection
    {
    }
    internal class ObjectsSection
    {
    }
    internal class EmailSection
    {
    }
    internal class StartUpSection
    {
    }
    internal class TesterSection
    {
    }
    #endregion

    public CommonSection Common { get; }
    public ChartsSection Charts { get; }
    public ExpertsSection Experts { get; }
    public ObjectsSection Objects { get; }
    public EmailSection Email { get; }
    public StartUpSection StartUp { get; }
    public TesterSection Tester { get; }
}

描述一个特定部分的嵌套类的实现是相似的;考虑使用 Config.ChartsSection 类的例子。

internal class ChartsSection
{
    private readonly Converter converter;
    public ChartsSection(Config parent)
    {
        converter = new Converter(parent, "Charts");
    }

    public string ProfileLast
    {
        get => converter.String("ProfileLast");
        set => converter.String("ProfileLast", value);
    }
    public int? MaxBars
    {
        get => converter.Int("MaxBars");
        set => converter.Int("MaxBars", value);
    }
    public bool? PrintColor
    {
         get => converter.Bool("PrintColor");
         set => converter.Bool("PrintColor", value);
    }
    public bool? SaveDeleted
    {
         get => converter.Bool("SaveDeleted");
         set => converter.Bool("SaveDeleted", value);
    }
 }

这个类描述的片段中包含了一个 Nullable 部分,可以使用其它中介类来读取和写入文件,这个类的实现将会晚些时候探讨,现在我想提醒您注意返回数据:如果类没有写入文件,包装类将返回 null 而不是键值。如果我们将 null 空值传递给任何键属性,则此值将被忽略。如果要删除一个栏位,就使用之前所说过的 DeleteKey 方法。

现在让我们考虑从文件中写入和读取数据的 Converter 类:它也是嵌套类,因此它可以使用主类的 WiteParam 和 GetParam 方法,尽管它们被标记为“protected”的访问修饰符。类具有下列类型的读写方法重载

  • Bool
  • Int
  • Double
  • String
  • DateTime

所有其他类型都转换为最合适的类型之一。类实现如下所示:

private class Converter
{
    private readonly Config parent;
    private readonly string section;
    public Converter(Config parent, string section)
    {
        this.parent = parent;
        this.section = section;
    }

    public bool? Bool(string key)
    {
        string s = parent.GetParam(section, key);
        if (s == null)
            return null;

        int n = Convert.ToInt32(s);
        if (n < 0 || n > 1)
            throw new ArgumentException("string mast be 0 or 1");
        return n == 1;
    }
    public void Bool(string key, bool? val)
    {
        if (val.HasValue)
            parent.WriteParam(section, key, val.Value ? "1" : "0");
    }

    public int? Int(string key)
    {
        string s = parent.GetParam(section, key);
        return s == null ? null : (int?)Convert.ToInt32(s);
    }
    public void Int(string key, int? val)
    {
        if (val.HasValue)
            parent.WriteParam(section, key, val.Value.ToString());
    }

    public double? Double(string key)
    {
        string s = parent.GetParam(section, key);
        return s == null ? null : (double?)Convert.ToDouble(s);
    }
    public void Double(string key, double? val)
    {
        if (val.HasValue)
            parent.WriteParam(section, key, val.Value.ToString());
    }

    public string String(string key) => parent.GetParam(section, key);
    public void String(string key, string value)
    {
        if (value != null)
            parent.WriteParam(section, key, value);
    }

    public DateTime? DT(string key)
    {
        string s = parent.GetParam(section, key);
        return s == null ? null : (DateTime?)DateTime.ParseExact(s, "yyyy.MM.dd", null);
    }
    public void DT(string key, DateTime? val)
    {
        if (val.HasValue)
            parent.WriteParam(section, key, val.Value.ToString("yyyy.MM.dd"));
    }
}

类将传递的数据转换为文件中预期的格式,并将数据写入该文件。从文件读取时,它将字符串转换为返回文件,并将结果传递给类,该类描述特定节,然后将值转换为预期格式。请注意,在访问任何属性的情况下,该类直接在文件中写入或读取数据。这确保了与文件一起工作时的实际数据,但与内存访问相比,可以占用更多的时间。由于读取和写入需要微秒级的时间,因此在程序运行期间,这种延迟是不明显的。   

接下来,让我们考虑一下终端运行管理器,这个类的用途:终端启动和停止,获取终端是否正在运行的数据的可能性,设置配置文件和启动标志。换句话说,类必须理解终端手册中描述的所有终端运行方法,并且它应该能够管理终端操作过程。基于这些需求,编写了以下接口,它们描述了所需的属性和方法。与终端的进一步操作将通过下面的接口实现。

interface ITerminalManager
{
    uint? Login { get; set; }
    string Profile { get; set; }
    Config Config { get; set; }
    bool Portable { get; set; }
    System.Diagnostics.ProcessWindowStyle WindowStyle { get; set; }
    DirectoryInfo TerminalInstallationDirectory { get; }
    DirectoryInfo TerminalChangeableDirectory { get; }
    DirectoryInfo MQL5Directory { get; }
    List<string> Experts { get; }
    List<string> Indicators { get; }
    List<string> Scripts { get; }
    string TerminalID { get; }
    bool IsActive { get; }

    bool Run();
    void Close();
    void WaitForStop();
    bool WaitForStop(int miliseconds);

    event Action<ITerminalManager> TerminalClosed;
}

从界面可以看出,前4个属性接受指令中提供的标志值,并在GUI创建部分中讨论。第五个标志在开始时设置终端窗口大小-它可以最小化终端,以完全模式或小窗口模式启动。但如果选择了Hidden值(以隐藏窗口),则不会执行预期的行为。要隐藏终端,我们需要编辑另一个初始化文件。然而,由于行为是至关重要的,我决定不使代码复杂化。

继承此接口的类有两个构造函数重载。

public TerminalManager(DirectoryInfo TerminalChangeableDirectory) :
    this(TerminalChangeableDirectory, new DirectoryInfo(File.ReadAllText(TerminalChangeableDirectory.GetFiles().First(x => x.Name == "origin.txt").FullName)), false)
{
}

public TerminalManager(DirectoryInfo TerminalChangeableDirectory, DirectoryInfo TerminalInstallationDirectory, bool isPortable)
{
    this.TerminalInstallationDirectory = TerminalInstallationDirectory;
    this.TerminalChangeableDirectory = TerminalChangeableDirectory;

    TerminalID = TerminalChangeableDirectory.Name;

    CheckDirectories();

    Process.Exited += Process_Exited;

    Portable = isPortable;
}

根据 Vladimir Karputov 的文章, 变量终端目录的origin.txt文件中存储了安装目录的路径。此事实用于第一个构造函数重载。此重载搜索文件origin.txt读取整个文件并创建 DirectiryInfo类,该类通过将从文件中读取的数据传递给它来描述该目录。还要注意,与准备操作类相关的所有动作都是由第二个构造函数执行的,该构造函数接受三个参数:

  • 变量目录的路径(AppData中的路径)。
  • 安装目录的路径。
  • 在便携式模式下终端运行的标志。  

为了便于配置,添加了此构造函数中的最后一个参数。它应该在构造函数的末尾被有意地分配。当终端以便携模式启动时,其MQL5目录(其中所有专家顾问和指示器都是存储的)在终端安装目录中被创建(如果之前没有创建)。最初,如果终端从未以可移植模式运行,则该目录不存在,因此,当设置此标志时,必须检查该目录是否存在。设置并读取此标志的属性描述如下。

/// <summary>
/// 在便携式模式下的终端运行标志
/// </summary>
private bool _portable;
public bool Portable
{
    get => _portable;
    set
    {
        _portable = value;
        if (value && !TerminalInstallationDirectory.GetDirectories().Any(x => x.Name == "MQL5"))
        {
            WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized;
            if (Run())
            {
                System.Threading.Thread.Sleep(100);
                Close();
            }
	    WaitForStop();
            WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
        }
    }
} 

在分配传递值时,检查MQL5目录的存在。如果没有这样的目录,启动终端并保持线程直到终端启动。根据先前设置的终端运行标志,终端将在便携式模式下启动,并且在第一次发射时将创建所需的目录。一旦终端启动,我们使用包装器中的 Close 命令关闭它,以便与终端一起工作,等待终端关闭。之后,将创建所需的MQL5目录,前提是访问没有问题。返回到终端 MQL5 文件夹的路径的属性通过一个条件构造工作:它根据上述标志,从安装目录或具有可变文件的目录返回到所需目录的路径。

/// <summary>
/// MQL5 文件夹的路径
/// </summary>
public DirectoryInfo MQL5Directory => (Portable ? TerminalInstallationDirectory : TerminalChangeableDirectory).GetDirectory("MQL5");

另一个需要注意的是构造函数重载。如果不是变量目录,您会突然将路径传递给安装目录,那么,如果在便携模式中至少有一个启动(或者当设置了 isPortable = true 标志)时,这个类应该正确工作。但是,它将只看到终端的安装目录,在这种情况下,TerminalID将不是一组数字和拉丁字符(在终端的变量目录中指示),而是等于安装终端的文件夹的名称,即安装目录的名称。  
还要注意提供关于交易机器人、指示器和脚本在终端中可用的信息的属性。这些属性是通过私有的 GetEX5FilesR 方法实现的。

#region .ex5 files relative paths
/// <summary>
/// List of full EA names
/// </summary>
public List<string> Experts => GetEX5FilesR(MQL5Directory.GetDirectory("Experts"));
/// <summary>
/// List of full indicator names
/// </summary>
public List<string> Indicators => GetEX5FilesR(MQL5Directory.GetDirectory("Indicators"));
/// <summary>
/// List of full script names
/// </summary>
public List<string> Scripts => GetEX5FilesR(MQL5Directory.GetDirectory("Scripts"));
#endregion

private List<string> GetEX5FilesR(DirectoryInfo path, string RelativeDirectory = null)
{
    if (RelativeDirectory == null)
        RelativeDirectory = path.Name;
    string GetRelevantPath(string pathToFile)
    {
        string[] path_parts = pathToFile.Split('\\');
        int i = path_parts.ToList().IndexOf(RelativeDirectory) + 1;
        string ans = path_parts[i];
        for (i++; i < path_parts.Length; i++)
        {
            ans = Path.Combine(ans, path_parts[i]);
        }

        return ans;
    }

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

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

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

    return files;
}

在这些属性中,我们不尝试获取指向 EA 文件的可能路径。相反,我们得到相对于专家文件夹(或者相对于指标文件夹和脚本文件夹)的 EA 路径。在选择期间,类检查文件扩展名(仅搜索ex5文件)。

返回已找到的ex5文件列表的方法使用递归。让我们详细探讨一下这个方法。首先,它检查其第二个参数的值,这是可选的:如果参数没有设置,则将当前通过的目录的名称分配给它。因此,我们可以了解应该生成哪些目录文件路径。使用的另一种C#语言结构是嵌套函数。这些函数只存在于当前方法中,我们使用这个结构是因为函数将不再使用。它的函数体不太大,因此可以适合在这种方法。此函数接受ex5文件的路径作为输入,并使用“\\”符号作为分隔符将其拆分。因此,我们得到一个目录名数组,并且ex5文件的名称在此数组的末尾可用。在下一步中,将目录的索引指定给i变量,该索引与搜索文件的路径相关。将其增加1并将指针移到下一个目录或文件。“ans”变量将存储找到的地址:为此,将指定目录的值分配给它,然后在循环中添加新的目录或文件,直到退出循环(即,添加所需文件的名称)。GetEX5FilesR 方法如下运行:

  1. 接收到所有嵌套目录的路径。
  2. 在当前目录中搜索ex5文件并保存它们的相对路径。
  3. 在循环中,为每个嵌套目录启动递归,同时传递需要接收路径的目录的名称。添加到 EX5文件的相对路径列表。 
  4. 返回找到的文件路径。

因此,此方法执行完整的文件搜索,并返回找到的所有专家顾问和其他 MQL5 可执行文件。

现在让我们考虑如何在C#语言中启动第三方应用程序。它具有非常便利的功能,用于启动和处理其他应用程序:Process 类,它是启动任何外部进程的包装器。例如,要从 C# 启动记事本,只需编写3行代码: 

System.Diagnostics.Process Process = new System.Diagnostics.Process();
Process.StartInfo.FileName = "Notepad.exe";
Process.Start();

我们将使用这个类来实现从附件中管理第三方终端的过程。以下是启动终端的方法:

public bool Run()
{
    if (IsActive)
        return false;
    // 设置终端的路径
    Process.StartInfo.FileName = Path.Combine(TerminalInstallationDirectory.FullName, "terminal64.exe");
    Process.StartInfo.WindowStyle = WindowStyle;
    // 设置运行终端的数据 (如果已经安装)
    if (Config != null && File.Exists(Config.Path))
        Process.StartInfo.Arguments = $"/config:{Config.Path} ";
    if (Login.HasValue)
        Process.StartInfo.Arguments += $"/login:{Login.Value} ";
    if (Profile != null)
        Process.StartInfo.Arguments += $"/profile:{Profile} ";
    if (Portable)
        Process.StartInfo.Arguments += "/portable";

    // 通知在关闭终端后需要呼叫退出事件的过程
    Process.EnableRaisingEvents = true;

    // 运行进程并将启动状态保存到 IsActive 变量
    return (IsActive = Process.Start());
}

在启动前配置终端时,应执行以下操作:

  1. 指定要启动的可执行文件的路径。
  2. 设置进程的窗口类型。
  3. 设置键值(在控制台应用程序中,这些都是在要启动的文件名之后指定的值)。
  4. 设置标志 Process.EnableRaisingEvents= true. 如果未设置此标志,进程终止事件将不触发。
  5. 启动进程并将启动状态保存到 IsActive 变量。

IsActive属性在终端关闭后触发的回调时再次变为false。TerminalClosed事件也在此回调中调用。

/// <summary>
/// 终端关闭事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Process_Exited(object sender, EventArgs e)
{
    IsActive = false;
    TerminalClosed?.Invoke(this);
}

其他终端管理方法(等待终端停止和关闭)是对 Process 类的标准方法的包装。

public void WaitForStop()
{
    if (IsActive)
        Process.WaitForExit();
}
/// <summary>
/// 停止进程
/// </summary>
public void Close()
{
    if (IsActive && !Process.HasExited)
        Process.Kill();
}
/// <summary>
/// 在指定时间内等待终端终止
/// </summary>
public bool WaitForStop(int miliseconds)
{
    if (IsActive)
        return Process.WaitForExit(miliseconds);
   return true;
}

因此,使用标准 Process 类,我们创建了一个与 MetaTrader 5 终端一起工作的方便包装器。这使得使用终端的操作更方便,而不是直接使用 Process类。

目录结构对象

在本文的第一部分中,我们已经探讨过使用目录,现在让我们讨论访问文件系统的方法。让我们从创建文件和目录路径的方法开始。为此,C# 提供了一个方便的类Path。它可以安全地创建到文件和目录的路径,因此也可以消除可能的错误。目录是使用DirectoryInfo类显示的,因此我们可以快速获取有关嵌套目录、父目录、目录名及其完整路径的数据,以及访问许多其他有用的属性。例如,这个类只允许调用一个方法来获取这个目录中的所有文件。FileInfo类用于面向对象的任何文件的呈现,这个类在功能上类似于DirectoryInfo。因此,文件和目录的整个操作都被实现为具有呈现类的操作,这样就可以专注于主要问题,几乎不需要创建中间函数和方法。

在前面描述的 TerminalManager 类中,经常使用DirectoryInfo类实例上的 GetDirectory 方法,此方法不包含在标准DirectoryInfo类布局中,添加此方法是为了方便起见。C# 提供了一种通过向标准类和自定义类添加扩展方法来扩展其功能的方法。我们使用 C# 语言的这个功能添加 GetDirectory 扩展方法。

static class DirectoryInfoExtention
{
    public static DirectoryInfo GetDirectory(this DirectoryInfo directory, string Name, bool createIfNotExists = false)
    {
        DirectoryInfo ans = new DirectoryInfo(Path.Combine(directory.FullName, Name));
        if (!ans.Exists)
        {
            if (!createIfNotExists)
                return null;
            ans.Create();
        }
        return ans;
    }
}

要创建一个扩展方法,必须创建静态类,在其中创建一个“公共静态方法”。必须使用正在创建扩展名的类型来键入其第一个参数。'this' 关键字必须在它前面。这个参数是在自动扩展方法调用中指定的,不需要显式传递给函数,而这正是扩展创建的类实例。不需要创建存储扩展方法的类的示例,而所有扩展方法都会自动添加到它们创建的类的方法集。具体方法按以下算法进行操作:

  1. 如果参数 createIfNotExists = false(或未指定),它返回一个带有传递名的子文件夹,传递给DirectoyInpe类型(如果存在该文件夹),或NULL。
  2. 如果 createIfNotExists=true, 如果尚未创建文件夹,则将创建该文件夹,并返回转换为DirectoryInfo类型的文件夹。 

为了方便地使用可变终端目录的文件夹,我们创建了一个类,它是面向对象的目录呈现。 

~\AppData\Roaming\MetaQuotes\Terminal

这个类按照下面的方式实现。

class TerminalDirectory
{
    public TerminalDirectory() :
        this(Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), "MetaQuotes", "Terminal"))
    {
    }

    public TerminalDirectory(string path)
    {
        pathToTerminal = path;
    }

    private readonly string pathToTerminal;

    public List<DirectoryInfo> Terminals
    {
        get
        {
            List<DirectoryInfo> ans = new List<DirectoryInfo>();
            string[] dir_array = Directory.GetDirectories(pathToTerminal);
            foreach (var item in dir_array)
            {
                string pathToOrigin = Path.Combine(pathToTerminal, item, "origin.txt");
                if (!File.Exists(pathToOrigin))
                    continue;
                if (!File.Exists(Path.Combine(File.ReadAllText(pathToOrigin), "terminal64.exe")))
                    continue;
                ans.Add(new DirectoryInfo(Path.Combine(pathToTerminal, item)));
            }

            return ans;
        }
    }
    public DirectoryInfo Common => new DirectoryInfo(Path.Combine(pathToTerminal, "Common"));
    public DirectoryInfo Community => new DirectoryInfo(Path.Combine(pathToTerminal, "Community"));
}

它有三个字段:

  1. Terminals
  2. Common
  3. Community

这些字段对应于此目录中子目录的名称。Terminals属性返回属于终端文件系统的目录列表。通常,在删除终端之后,终端文件夹仍保留在该目录中,因此我决定添加目录相关性检查。根据以下标准进行检查:

  1. 分析目录根目录中存在“origin.txt”文件:该文件允许访问终端目录路径。
  2. 在适当的目录中存在一个可执行的终端文件。 

请注意,扩展是为64位终端版本设计的。为了使用32位版本,“terminal64.exe”应在程序中的任何位置(即 TerminalManager 和当前讨论的类)重命名为 “terminal.exe”。因此,它忽略了找不到可执行终端文件的目录。

转到探讨第一个构造函数。此构造函数允许自动生成到终端文件目录的路径:

System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData)

Environment 类允许自动获取“AppData”的路径,因此我们不必指定用户名。多亏了这一行,扩展将能够找到使用标准安装方法安装在您电脑上的所有终端的列表。 

除了描述具有终端数据目录的文件夹之外,扩展还有自己的目录,其中存储临时文件和优化报告。下面是描述这个目录的类。

class OptimisationExtentionWorkingDirectory
{
    public OptimisationExtentionWorkingDirectory(string DirectoryName)
    {
        DirectoryRoot = CreateIfNotExists(DirectoryName);
        Configs = CreateIfNotExists(Path.Combine(DirectoryName, "Configs"));
        Reports = CreateIfNotExists(Path.Combine(DirectoryName, "Reports"));
    }

    public DirectoryInfo DirectoryRoot { get; }

    public DirectoryInfo Configs { get; }

    public DirectoryInfo Reports { get; }

    protected DirectoryInfo CreateIfNotExists(string path)
    {
        DirectoryInfo ans = new DirectoryInfo(path);
        if (!ans.Exists)
            ans.Create();
        return ans;
    }
}

从类构造函数可以看出,在创建过程中,我们检查根目录和嵌套目录的存在性。如果这些目录不可用,将创建它们。

  • “DirectoryRoot”是加载项存储其文件和目录的主目录。 
  • “Configs” 是我们将复制配置文件、更改它们并在终端启动时设置为输入参数的目录。
  • “Reports”目录将存储文件和文件夹的结构,并在每次测试后加载报告和优化设置。

报表目录的内部结构是在“OptimisationManager”类中创建的,并在完成后为每个优化而形成。它包括以下几点:

  1. 名称等于终端ID的目录。 
  2. 名称等于robot名称的目录。包含以下内容:
    • Settings.xml — 具有优化设置的文件(在程序内部形成)
    • History.xml — 复制的历史优化文件(由终端形成)
    • Forward.xml — 复制前向优化文件(由终端形成)

因此,我们创建了两个类,它们是使用文件系统的起点。在代码中使用文件系统的进一步工作是使用标准的C#类来实现的,这样可以避免文件路径中的错误,并显著加快编程速度。

使用报表和EA设置文件的对象 (OptimisatorSettingsManager, ReportReader, SetFileManager)

本章讨论文件操作。加载项必须能够处理以下文件:

  • EA设置选项卡
  • 交易报告文件
  • 优化设置文件,这是保存在报告中的“Reports”目录的附加。

让我们从包含EA参数的文件开始进行优化。EA设置文件的扩展名为(*set)。但是,有几个安装文件,其中包含在图表上启动的设置和在测试人员中运行的设置。我们对第二种文件格式感兴趣:这些文件存储在终端的变量目录 

~\MQL5\Profiles\Tester

请注意,有时在纯安装期间,此目录不存在,因此您应该检查并在必要时创建。如果该目录不存在,则终端将无法保存优化设置。这是以下问题的常见原因:在这种纯终端安装的情况下,每次新的测试或优化运行之后,优化设置标签仍然具有默认设置。所描述的文件结构与ini文件有些相似,如下所示:

Variable_name=Value||Start||Step||Stop||(Y/N)

换句话说,这些文件中的密钥是EA参数的名称,并且键值可以取其值的列表,在给定示例中的名称与策略测试器中的适当列相同。最后一个变量可以取两个值中的一个(Y/N),它允许/禁用对这个特定的EA参数的优化。这个规则的一个例外是写字符串参数,它的格式类似于INI文件:

Variable_name=Value

与初始化文件类似,SET 文件也有注释。注释行总是以“;”(分号)开头。下面是一些简单的示例:

; saved automatically on 2019.05.19 09:04:18

; this file contains last used input parameters for testing/optimizing 2MA_Martin expert advisor

;

Fast=12||12||1||120||N

Slow=50||50||1||500||N

maxLot=1||1||0.100000||10.000000||N

pathToDB=C:\Users\Administrator\Desktop\test_2MA_8

要处理这些文件,我们需要创建一个允许读取这些文件的包装类,以及一个存储每个读取字符串的值的类。这个类是在本文的“视图”描述部分中提到的,因此我们这里不考虑它。让我们考虑从图形接口——SetFileManager 读写参数集的主要类。这里是这个类的实现:

class SetFileManager
{
    public SetFileManager(string filePath, bool createIfNotExists)
    {
        if ((FileInfo = new FileInfo(filePath)).Extension.CompareTo(".set") != 0)
            throw new ArgumentException("File mast have '.set' extention!");
        if (createIfNotExists)
            File.Create(filePath).Close();
        if (!File.Exists(filePath))
            throw new ArgumentException("File doesn`t exists");

    }

    public FileInfo FileInfo { get; }

    #region File data
        
    private List<ParamsItem> _params = new List<ParamsItem>();
    public List<ParamsItem> Params
    {
        get
        {
            if (_params.Count == 0)
                UpdateParams();
            return _params;
        }
        set
        {
            if (value != null && value.Count != 0)
                _params = value;
        }
    }
    #endregion

    public virtual void SaveParams()
    {
        if (_params.Count == 0)
            return;

        using (var file = new StreamWriter(FileInfo.FullName, false))
        {
            file.WriteLine(@"; saved by OptimisationManagerExtention program");
            file.WriteLine(";");
            foreach (var item in _params)
            {
                file.WriteLine($"{item.Variable}={item.Value}||{item.Start}||{item.Step}||{item.Stop}||{(item.IsOptimize ? "Y" : "N")}");
            }
        }
    }

    public virtual SetFileManager DublicateFile(string pathToFile)
    {
        if (new FileInfo(pathToFile).Extension.CompareTo(".set") != 0)
            throw new ArgumentException("File mast have '.set' extention!");

        File.Copy(FileInfo.FullName, pathToFile, true);
        return new SetFileManager(pathToFile, false);
    }
        
    public virtual void UpdateParams()
    {
        _params.Clear();

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

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

                    _params.Add(item);
                }
            }
        }
    }
}

首先要注意的是文件格式检查,它在类构造函数中提供。如果文件格式与 SET 的文件格式不同,则此类将返回错误,因为我们将尝试处理终端未知的文件。文件存储在公共只读属性 FileInfo中。文件读取是在 UpdateParams 方法中进行的,在“使用构造”中,从第一行到最后一行读取文件,而忽略注释文件。还要注意读取文件参数的设置。读取行首先被分成两行,等号(“=”)用作分隔符。因此变量名与其值是分开的。在下一步中,变量值被分成4个元素的数组[数值、起点、步长、停止、是否优化]。如果是字符串,数组将不会被分成这些字符串,因为将找不到分隔符号的两行(“”)。为了避免字符串出现错误,我们不建议在字符串中使用此字符。如果数组中没有每个新元素的数据,则为其分配空值,否则将使用数组中的值。

保存值的方法是SaveParams,请注意文件中的数据写入格式。这是在以下代码行中完成的:

file.WriteLine($"{item.Variable}={item.Value}||{item.Start}||{item.Step}||{item.Stop}||{(item.IsOptimize ? "Y" : "N")}");

无论是哪种类型的数据,数据都将以非字符串类型写入。终端可以理解它是否是字符串,因此选择了唯一的数据写入类型。这个类的缺点之一是无法找到数据类型。我们无法知道它的格式,因为文件结构不提供此类信息。终端直接从专家顾问处接收这些信息。  

通过params属性访问读取文件并设置其参数。由于使用文件数据的操作是通过描述的属性实现的,为了方便检查文件是否已被读取。如果尚未读取文件,则调用UpdateParams方法。一般来说,使用这个类的过程如下:

  1. 实例化,从而获得文件的 OOP 表示;
  2. 通过调用 “Params”方法读取(或根据需要UpdateParams,例如从外部更改文件)
  3. 使用“Setter”设置自定义值,或通过“Getter”使用接收的数组更改数据。
  4. 使用 SaveParams 方法保存更改 

与 INI 文件相比,它的主要缺点是在读写之间,数据存储在程序内存中。但是,如果意外或故意排除该文件,则可以从外部更改该文件。该类还具有DuplicateFile方法,该方法的目的是在传递路径上复制文件(如果在该路径上存在同名文件,则通过替换完成)。

进一步的类 ReportReader 读取终端生成的优化报告,并将这些报告解析为表的准备数据。优化历史文件以为MS Excel创建的XML格式提供。它的根节点(第一个标记)<Workbook/>描述了这本书。下一个节点<DocumentProperties/>描述用于执行优化的参数。此节点包含以下有用信息:

  1. 由robot名称、资产名称、时间范围和优化周期组成的头。
  2. 创建日期
  3. 执行优化的服务器的名称
  4. 存款和存款货币
  5. 杠杆

节点<Styles/>对我们没有用处-它主要是为Excel创建的。下一个节点<Worksheet/>使用优化过程描述工作表。此节点包含存储搜索数据的节点:优化结果列表,分为多个列,如在Strategy Tester中测试完所有参数后。请注意,表的第一行包含列标题,而其他行包含值。每个<Row/>节点都包含<Cell/>标记内的表值列表。此外,每个<Cell/>标记都包含type属性,该属性指示此单元格中的值类型。因为这个文件太大了,我不在这里提供。您可以通过优化任何专家顾问来查看整个文件,并从我们的插件的报表文件夹中打开其优化结果。现在让我们继续考虑所描述的类:首先回顾描述优化文件的属性。 

#region DocumentProperties and column names
/// <summary>
/// Document column names
/// </summary>
protected List<string> columns = new List<string>();
/// <summary>
/// Access to the collection of document columns from outside, collection copy is returned 
/// to protect the initial collection from modification 
/// </summary>
public List<string> ColumnNames => new List<string>(columns);
/// <summary>
/// Document header
/// </summary>
public string Title { get; protected set; }
/// <summary>
/// Document author
/// </summary>
public string Author { get; protected set; }
/// <summary>
/// Document creation date
/// </summary>
public DateTime Created { get; protected set; }
/// <summary>
/// Server on which optimization was performed
/// </summary>
public string Server { get; protected set; }
/// <summary>
/// Initial deposit 
/// </summary>
public Deposit InitialDeposit { get; protected set; }
/// <summary>
/// Leverage
/// </summary>
public int Leverage { get; protected set; }
#endregion

这些属性在<DocumentProperties/>中,属性用以下方法填充

protected virtual void GetDocumentProperties(string path)
{
    document.Load(path);

    Title = document["Workbook"]["DocumentProperties"]["Title"].InnerText;
    Author = document["Workbook"]["DocumentProperties"]["Author"].InnerText;
    string DT = document["Workbook"]["DocumentProperties"]["Created"].InnerText;
    Created = Convert.ToDateTime(DT.Replace("Z", ""));
    Server = document["Workbook"]["DocumentProperties"]["Server"].InnerText;
    string[] deposit = document["Workbook"]["DocumentProperties"]["Deposit"].InnerText.Split(' ');
    Deposit = new Deposit(Convert.ToDouble(deposit[0]), deposit[1]);
    Leverage = Convert.ToInt32(document["Workbook"]["DocumentProperties"]["Leverage"].InnerText);

    enumerator = document["Workbook"]["Worksheet"]["Table"].ChildNodes.GetEnumerator();
    enumerator.MoveNext();

    foreach (XmlElement item in (XmlElement)enumerator.Current)
    {
        columns.Add(item["Data"].InnerText);
    }
}

因此,使用 C# 工具处理(*.xml)文件几乎和处理数组一样简单。"document”对象是XmlDocument类的一个实例,它存储读取的文件并提供方便的使用。在 Read 方法中使用赋值的枚举器字段,该方法逐行读取文档。在类声明期间,使用IDisposable接口: 

class ReportReader : IDisposable

这个接口只包含一个Dispose()方法,它允许在“using”构造中使用这个类。“using”结构确保了正确的操作,这意味着每次读取文件时都不需要关闭文件。相反,在 Dispose()方法中关闭文件,该方法在退出对文件执行操作的花括号块后自动调用。在这种情况下,我们将清除“Dispose”方法中的 document 字段,以避免在读取的文件中存储大量不必要的信息。此方法的实现如下所示:

public void Dispose()
{
    document.RemoveAll();
}

现在让我们查看IEnumerator接口,它是一个标准的C#接口。具体如下:

//
// 概述:
//     Supports a simple iteration over a non-generic collection.
[ComVisible(true)]
[Guid("496B0ABF-CDEE-11d3-88E8-00902754C43A")]
public interface IEnumerator
{
    object Current { get; }

    bool MoveNext();
    
    void Reset();
}

它由两个方法和一个属性组成。此接口用作集合的包装器,一次迭代一个值。MoveNext方法将光标向前移动一个值,依此类推,直到集合结束。如果在传递整个集合之后尝试调用此方法,则它将返回false,这意味着迭代结束。Reset方法允许重新启动迭代,即将光标移动到集合的零索引。"Current”属性包含在MoveNext中移动后收到的索引的当前选定集合元素。此接口在C#中广泛使用,因此,“foreach”循环是基于它的。但是,如果 Read 方法的实现需要这个。 

/// <summary>
/// Command to read a row from the optimizations table
/// </summary>
/// <param name="row">
/// Read row key - column header; value - cell value</param>
/// <returns>
/// true - if the row has been read
/// false - if the row has not been read
/// </returns>
public virtual bool Read(out List<KeyValuePair<string, object>> row)
{
    row = new List<KeyValuePair<string, object>>();

    if (enumerator == null)
        return false;

    bool ans = enumerator.MoveNext();
    if (ans)
    {
        XmlNodeList nodes = ((XmlElement)enumerator.Current).ChildNodes;

        for (int i = 0; i < columns.Count; i++)
        {
            string value = nodes[i]["Data"].InnerText;
            string type = nodes[i]["Data"].Attributes["ss:Type"].Value;
            KeyValuePair<string, object> item = new KeyValuePair<string, object>(columns[i], ConvertToType(value, type));
            row.Add(item);
        }
    }
    return ans;
}

方法“Read”的用途与MoveNext()类似。此外,它还通过传递给它的参数返回操作结果。因为它应该只返回带值的行,所以当设置枚举器变量的值时,我们调用 MoveNext方法一次,从而将光标从零位置(表列标题)移动到索引1(带值的第一行)。在读取数据时,我们还使用 ConvertToType,它将读取的值从字符串格式转换为“Type”属性设置的格式。这就是为什么在返回列表中指定类型“object”-这样我们可以将任何类型转换为返回类型。ConvertToType方法的实现如下所示。

private object ConvertToType(string value, string type)
{
    object ans;
    switch (type)
    {
        case "Number":
            {
                System.Globalization.NumberFormatInfo provider = new System.Globalization.NumberFormatInfo()
                {
                    NumberDecimalSeparator = ","
                };

                ans = Convert.ToDouble(value.Replace('.', ','), provider);
            }
            break;
        case "DateTime": ans = Convert.ToDateTime(value); break;
        case "Boolean":
            {
                try
                {
                    ans = Convert.ToBoolean(value.ToLower());
                }
                catch (Exception)
                {
                    ans = Convert.ToInt32(value) == 1;
                }
            }
            break;
        default: ans = value; break; // String
    }

    return ans;
}

在此方法中,字符串将转换为数字格式。由于不同国家有不同的数据和时间表示格式,因此有必要显式指定小数分隔符

读取器重新启动是通过“ResetReader”方法启用的,该方法是IEnumerator.Reset方法的包装。具体实现如下:

public void ResetReader()
{
    if (enumerator != null)
    {
        enumerator.Reset(); // Reset
        enumerator.MoveNext(); // Skip the headers
    }
}

这样,使用C#中提供的用于解析XML文件的方便包装器,我们可以轻松地编写一个包装器类来解析报表文件、读取它们并获取其他数据。

下一个类处理优化器设置文件,这些文件是由加载项本身生成的,而不是由终端直接生成的。目标特性之一是通过双击优化参数在测试仪中启动ea的可能性。但是我们在哪里设置测试器(日期范围、符号名称和其他参数)?优化报告只存储这些数据的一部分,但不存储所有数据。显然,要解决此问题,需要将这些设置保存到文件中。选择XML标记是为了方便数据存储格式。在上面的类中显示了XML文件读取示例。除了读取,我们还将写入文件。首先我们需要确定要保存在设置文件中的信息。

要保存的第一个对象是存储优化器设置数据(在主设置选项卡的下部区域的设置选项卡中可用)的结构。这个结构实现如下。

struct OptimisationInputData
{
    public void Copy(OptimisationInputData data)
    {
        Login = data.Login;
        ForvardDate = data.ForvardDate;
        IsVisual = data.IsVisual;
        Deposit = data.Deposit;
        Laverage = data.Laverage;
        Currency = data.Currency;
        DepositIndex = data.DepositIndex;
        ExecutionDelayIndex = data.ExecutionDelayIndex;
        ModelIndex = data.ModelIndex;
        CurrencyIndex = data.CurrencyIndex;
        LaverageIndex = data.LaverageIndex;
        OptimisationCriteriaIndex = data.OptimisationCriteriaIndex;
    }
        
    public uint? Login;
    public DateTime ForvardDate;
    public bool IsVisual;
    public int Deposit;
    public string Laverage;
    public string Currency;
    public int DepositIndex, ExecutionDelayIndex, ModelIndex,
               CurrencyIndex, LaverageIndex, OptimisationCriteriaIndex;
    public ENUM_Model Model => GetEnum<ENUM_Model>(ModelIndex);
    public ENUM_OptimisationCriteria GetOptimisationCriteria => GetEnum<ENUM_OptimisationCriteria>(OptimisationCriteriaIndex);
    public ENUM_ExecutionDelay ExecutionDelay => GetEnum<ENUM_ExecutionDelay>(ExecutionDelayIndex);
    private T GetEnum<T>(int ind)
    {
        Type type = typeof(T);
        string[] arr = Enum.GetNames(type);
        return (T)Enum.Parse(type, arr[ind]);
    }
}

该结构最初是作为一个容器创建的,用于将数据从视图传递到模型,因此除了数据之外,它还包含组合框的索引。为了有效地操作模型和其他类中的结构,我创建了方法来转换枚举(enum)的值,这些值是通过索引号存储在结构中的所需枚举类型。枚举操作如下:要将这些列表的值输出到组合框,它们将以方便的字符串格式存储。方法GetEnum<T>用于反向转换。它是一种类似于C++模板的通用方法。若要在此方法中查找所需的枚举,请查找为其使用存储类型值的类型类的传递类型的特定值。然后将此枚举类型分解为行列表,然后使用从字符串到枚举的反向转换-以获取特定枚举的值(不在字符串视图中,而是作为所需的枚举)。

下一个包含保存数据的对象是ConfigCreator_InputData,此结构包含来自具有所选终端的表的数据,并使用 OptimisationManager类创建配置文件。这个结构看起来如下:

struct ConfigCreator_inputData
{
    public ENUM_Timeframes TF;
    public uint? Login;
    public string TerminalID, pathToBot, setFileName,
           Pass, CertPass, Server, Symbol, ReportName;
    public DateTime From, Till;
    public ENUM_OptimisationMode OptimisationMode;
}

所有保存的数据中的第三个也是最后一个是按列表元素 ParamItem(List<ParamsItem>)列出的 EA 参数类型列表。现在让我们看看在类操作期间创建的文件:


<Settings>
        <OptimisationInputData>
                <Item Name="Login" />
                <Item Name="ForvardDate">2019.04.01</Item>
                <Item Name="IsVisual">False</Item>
                <Item Name="Deposit">10000</Item>
                <Item Name="Laverage">1:1</Item>
                <Item Name="Currency">USD</Item>
                <Item Name="DepositIndex">2</Item>
                <Item Name="ExecutionDelayIndex">0</Item>
                <Item Name="ModelIndex">1</Item>
                <Item Name="CurrencyIndex">1</Item>
                <Item Name="LaverageIndex">0</Item>
                <Item Name="OptimisationCriteriaIndex">0</Item>
        </OptimisationInputData>
        <ConfigCreator_inputData>
                <Item Name="TF">16386</Item>
                <Item Name="Login">18420888</Item>
                <Item Name="TerminalID">0CFEFA8410765D70FC53545BFEFB44F4</Item>
                <Item Name="pathToBot">Examples\MACD\MACD Sample.ex5</Item>
                <Item Name="setFileName">MACD Sample.set</Item>
                <Item Name="Pass" />
                <Item Name="CertPass" />
                <Item Name="Server" />
                <Item Name="Symbol">EURUSD</Item>
                <Item Name="ReportName">MACD Sample</Item>
                <Item Name="From">2019.01.01</Item>
                <Item Name="Till">2019.06.18</Item>
                <Item Name="OptimisationMode">2</Item>
        </ConfigCreator_inputData>
        <SetFileParams>
                <Variable Name="InpLots">
                        <Value>0.1</Value>
                        <Start>0.1</Start>
                        <Step>0.010000</Step>
                        <Stop>1.000000</Stop>
                        <IsOptimize>False</IsOptimize>
                </Variable>
                <Variable Name="InpTakeProfit">
                        <Value>50</Value>
                        <Start>50</Start>
                        <Step>1</Step>
                        <Stop>500</Stop>
                        <IsOptimize>False</IsOptimize>
                </Variable>
                <Variable Name="InpTrailingStop">
                        <Value>30</Value>
                        <Start>30</Start>
                        <Step>1</Step>
                        <Stop>300</Stop>
                        <IsOptimize>False</IsOptimize>
                </Variable>
                <Variable Name="InpMACDOpenLevel">
                        <Value>3</Value>
                        <Start>3</Start>
                        <Step>1</Step>
                        <Stop>30</Stop>
                        <IsOptimize>True</IsOptimize>
                </Variable>
                <Variable Name="InpMACDCloseLevel">
                        <Value>2</Value>
                        <Start>2</Start>
                        <Step>1</Step>
                        <Stop>20</Stop>
                        <IsOptimize>True</IsOptimize>
                </Variable>
                <Variable Name="InpMATrendPeriod">
                        <Value>26</Value>
                        <Start>26</Start>
                        <Step>1</Step>
                        <Stop>260</Stop>
                        <IsOptimize>False</IsOptimize>
                </Variable>
        </SetFileParams>
</Settings>

文件是在视频中显示的 EA 操作期间创建的。从其结构上可以看出,根文件节点是<settings/>,其中还有三个节点:   <OptimisationInputData/><ConfigCreator_inputData/><SetFileParams/>. 这些节点中的数据类型与其名称相对应。存储测试仪设置数据的节点中的最后一个元素是包含“Name”属性的“Item”标记,通过它我们可以设置保存的参数的名称。标签 <Variable/>用于EA参数列表。“Name”属性存储参数的名称,相应的优化参数值保存在嵌套标记中。要创建此文件,OptimisatorSettingsManager 类将从IDisposable接口继承,指定的值将保存到Dispose方法中的文件中。相应属性的Getter用于从文件中读取数据。

#region OptimisationInputData
/// <summary>
/// The OptimisationInputData structure for saving data
/// </summary>
private OptimisationInputData? _optimisationInputData = null;
/// <summary>
/// Get and save the OptimisationInputData structure
/// </summary>
public virtual OptimisationInputData OptimisationInputData
{
    get
    {
        return new OptimisationInputData
        {
            Login = StrToUintNullable(GetItem(NodeType.OptimisationInputData, "Login")),
            ForvardDate = DateTime.ParseExact(GetItem(NodeType.OptimisationInputData, "ForvardDate"), DTFormat, null),
            IsVisual = Convert.ToBoolean(GetItem(NodeType.OptimisationInputData, "IsVisual")),
            Deposit = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "Deposit")),
            Laverage = GetItem(NodeType.OptimisationInputData, "Laverage"),
            Currency = GetItem(NodeType.OptimisationInputData, "Currency"),
            DepositIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "DepositIndex")),
            ExecutionDelayIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "ExecutionDelayIndex")),
            ModelIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "ModelIndex")),
            CurrencyIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "CurrencyIndex")),
            LaverageIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "LaverageIndex")),
            OptimisationCriteriaIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "OptimisationCriteriaIndex"))
        };
    }
    set => _optimisationInputData = value;
}
#endregion

在这个具体的例子中,在Getter中获得了OptimisationInputData结构。结构的值取自上述文件。Getter中的 GetItem方法用于从文件接收数据。该方法有两个参数:

  1. 使用数据的节点类型 
  2. 在“Name”属性中指定的参数的名称。 

方法实现如下:

/// <summary>
/// Get element from a settings file
/// </summary>
/// <param name="NodeName">Structure type</param>
/// <param name="Name">Field name</param>
/// <returns>
/// Field value
/// </returns>
public string GetItem(NodeType NodeName, string Name)
{
    if (!document.HasChildNodes)
        document.Load(Path.Combine(PathToReportDataDirectory, SettingsFileName));

    return document.SelectSingleNode($"/Settings/{NodeName.ToString()}/Item[@Name='{Name}']").InnerText;
}

此数据获取方法使用与 SQL 类似但应用于 XML 格式的Xpath语言。若要从指定属性值处的所需节点获取数据,请指定该节点的完整路径,然后在最终项节点中,我们需要指示以下条件:Name属性必须等于传递的名称。因此,所有结构都从文件中读取。另一种方法用于参数列表,因为此节点结构更复杂。

#region SetFileParams
/// <summary>
/// List of parameters to save
/// </summary>
private List<ParamsItem> _setFileParams = new List<ParamsItem>();
/// <summary>
/// Get and set (.set) file parameters to save
/// </summary>
public List<ParamsItem> SetFileParams
{
    get
    {
        if (!document.HasChildNodes)
            document.Load(Path.Combine(PathToReportDataDirectory, SettingsFileName));
        var data = document["Settings"]["SetFileParams"];

        List<ParamsItem> ans = new List<ParamsItem>();
        foreach (XmlNode item in data.ChildNodes)
        {
            ans.Add(new ParamsItem(item.Attributes["Name"].Value)
            {
                Value = item["Value"].InnerText,
                Start = item["Start"].InnerText,
                Step = item["Step"].InnerText,
                Stop = item["Stop"].InnerText,
                IsOptimize = Convert.ToBoolean(item["IsOptimize"].InnerText)
            });
        }

        return ans;
    }
    set { if (value.Count > 0) _setFileParams = value; }
}
#endregion

在本例中,我们遍历所有<Variable/>节点。从每个属性中,我们获取 Name 属性值,并用这个特定的 ParamsItem节点中包含的数据填充 ParamItem类。

决定将数据保存到文件中的完整的 Dispose()方法由以下实现表示:

public virtual void Dispose()
{
    // Nested method which assists in writing of structure elements
    void WriteItem(XmlTextWriter writer, string Name, string Value)
    {
        writer.WriteStartElement("Item");

        writer.WriteStartAttribute("Name");
        writer.WriteString(Name);
        writer.WriteEndAttribute();

        writer.WriteString(Value);

        writer.WriteEndElement();
    }
    void WriteElement(XmlTextWriter writer, string Node, string Value)
    {
        writer.WriteStartElement(Node);
        writer.WriteString(Value);
        writer.WriteEndElement();
    }

    // firstly clean the file storing xml markup of the settings file
    if (document != null)
        document.RemoveAll();

    // then check if the results can be saved
    if (!_configInputData.HasValue ||
        !_optimisationInputData.HasValue ||
        _setFileParams.Count == 0)
    {
        return;
    }

    using (var xmlWriter = new XmlTextWriter(Path.Combine(PathToReportDataDirectory, SettingsFileName), null))
    {
        xmlWriter.Formatting = Formatting.Indented;
        xmlWriter.IndentChar = '\t';
        xmlWriter.Indentation = 1;

        xmlWriter.WriteStartDocument();

        xmlWriter.WriteStartElement("Settings");

        xmlWriter.WriteStartElement("OptimisationInputData");
        WriteItem(xmlWriter, "Login", _optimisationInputData.Value.Login.ToString());
        WriteItem(xmlWriter, "ForvardDate", _optimisationInputData.Value.ForvardDate.ToString(DTFormat));
        WriteItem(xmlWriter, "IsVisual", _optimisationInputData.Value.IsVisual.ToString());
        WriteItem(xmlWriter, "Deposit", _optimisationInputData.Value.Deposit.ToString());
        WriteItem(xmlWriter, "Laverage", _optimisationInputData.Value.Laverage);
        WriteItem(xmlWriter, "Currency", _optimisationInputData.Value.Currency);
        WriteItem(xmlWriter, "DepositIndex", _optimisationInputData.Value.DepositIndex.ToString());
        WriteItem(xmlWriter, "ExecutionDelayIndex", _optimisationInputData.Value.ExecutionDelayIndex.ToString());
        WriteItem(xmlWriter, "ModelIndex", _optimisationInputData.Value.ModelIndex.ToString());
        WriteItem(xmlWriter, "CurrencyIndex", _optimisationInputData.Value.CurrencyIndex.ToString());
        WriteItem(xmlWriter, "LaverageIndex", _optimisationInputData.Value.LaverageIndex.ToString());
        WriteItem(xmlWriter, "OptimisationCriteriaIndex", _optimisationInputData.Value.OptimisationCriteriaIndex.ToString());
        xmlWriter.WriteEndElement();

        xmlWriter.WriteStartElement("ConfigCreator_inputData");
        WriteItem(xmlWriter, "TF", ((int)_configInputData.Value.TF).ToString());
        WriteItem(xmlWriter, "Login", _configInputData.Value.Login.ToString());
        WriteItem(xmlWriter, "TerminalID", _configInputData.Value.TerminalID.ToString());
        WriteItem(xmlWriter, "pathToBot", _configInputData.Value.pathToBot);
        WriteItem(xmlWriter, "setFileName", _configInputData.Value.setFileName);
        WriteItem(xmlWriter, "Pass", _configInputData.Value.Pass);
        WriteItem(xmlWriter, "CertPass", _configInputData.Value.CertPass);
        WriteItem(xmlWriter, "Server", _configInputData.Value.Server);
        WriteItem(xmlWriter, "Symbol", _configInputData.Value.Symbol);
        WriteItem(xmlWriter, "ReportName", _configInputData.Value.ReportName);
        WriteItem(xmlWriter, "From", _configInputData.Value.From.ToString(DTFormat));
        WriteItem(xmlWriter, "Till", _configInputData.Value.Till.ToString(DTFormat));
        WriteItem(xmlWriter, "OptimisationMode", ((int)_configInputData.Value.OptimisationMode).ToString());
        xmlWriter.WriteEndElement();

        xmlWriter.WriteStartElement("SetFileParams");
        foreach (var item in _setFileParams)
        {
            xmlWriter.WriteStartElement("Variable");

            xmlWriter.WriteStartAttribute("Name");
            xmlWriter.WriteString(item.Variable);
            xmlWriter.WriteEndAttribute();

            WriteElement(xmlWriter, "Value", item.Value);
            WriteElement(xmlWriter, "Start", item.Start);
            WriteElement(xmlWriter, "Step", item.Step);
            WriteElement(xmlWriter, "Stop", item.Stop);
            WriteElement(xmlWriter, "IsOptimize", item.IsOptimize.ToString());

            xmlWriter.WriteEndElement();
        }
        xmlWriter.WriteEndElement();

        xmlWriter.WriteEndElement();
        xmlWriter.WriteEndDocument();
        xmlWriter.Close();
    }
}

在该方法的开头创建两个嵌套函数。WriteItem 函数允许分离用于编写结构元素的重复代码块。 WriteElement函数用于保存优化参数的值,例如 Start、Step、Stop、IsOptimize。这三个标记都必须在设置文件中可用。因此,我们在写入之前添加一个 检查单元,其目的是在未通过所有必需参数的情况下防止文件写入。接下来,将数据写入前面描述的“using”构造中的文件。使用嵌套函数可以将与数据写入文件相关的代码减少三倍以上。  

测试按键对象

最后,我想添加一些关于应用程序测试的注释。由于插件将进一步扩展和修改,我决定编写测试来检查键对象。稍后,如果有必要修改它们,我们将能够轻松地检查它们的性能。当前测试部分包括以下类:

  • Config
  • ReportReader
  • OptimisationSettingsManager
  • SetFileManager
  • TerminalManager

在第一篇文章中,将修改进一步章节中描述的类。这些更改涉及一些方法的逻辑和执行结果,这就是为什么这些类不被单元测试覆盖的原因。这些类的测试将在下一篇文章中实现。还要注意,虽然这些测试是作为单元测试实现的,但目前它们都是集成测试,因为它们与外部对象(终端、文件系统等)交互。计划在不依赖于上述对象的情况下测试更多的对象,即纯单元测试。为此,上述每个对象前面都有创建结构。这种结构的一个例子是创建 ReportReader类:

#region ReportReaderFabric
abstract class ReportReaderCreator
{
    public abstract ReportReader Create(string path);
}

class MainReportReaderCreator : ReportReaderCreator
{
    public override ReportReader Create(string path)
    {
        return new ReportReader(path);
    }
}
#endregion

它的代码很简单:我们实际上将 ReportReader 类型对象的创建包装到MainReportReaderCreator类,该类派生自ReportReaderFabric类。这种方法允许将对象类型作为 ReportReaderFabric传递给关键对象(在后面的章节中描述)。在这里,特定结构的实现可能不同。因此,在单元测试中,使用文件和终端的类可以被关键对象替换。类之间的依赖性也降低了。这种形成对象的方法称为工厂方法。

下一篇文章将详细讨论未来测试的实现。在以后的章节中将考虑使用fabric方法创建对象的示例。现在让我们考虑测试一个使用配置文件的类。当前项目中的所有测试都应包含在单独的项目“单元测试项目”中


让我们将其命名为“OptimisationManagerExtentionTests”,因为测试将为“OptimisationManagerExtention”项目编写。下一步是添加到“OptimisationManagerExtention”项目的链接,即添加到带有图形界面和逻辑的 DLL。我们需要测试没有用“public”访问修饰符标记的对象。有两种方法可用于我们的测试项目:

  1. 公开它们(这是错误的,因为它们只在项目中使用)
  2. 添加在特定项目中查看内部类的可能性(这是一种更可取的方法)

我使用了第二种方法来解决此问题,并在主项目代码中添加了以下属性:

[assembly: InternalsVisibleTo("OptimisationManagerExtentionTests")]

下一步是为所选类编写测试。由于测试项目只是一个辅助项目,因此我们不会考虑每个测试类。这里是一个例子,为了方便起见,这里是测试 Config 类的完整类。进行此类测试的第一个条件是添加属性[TestClass]。测试类也需要是公共的,其测试方法应该具有[TestMethod]属性,而整个测试过程都将在其中实现。每次开始测试之前,都会启动标记为[TestInitialize]的方法。有一个类似的[ClassInitialize]属性,此属性不在此测试中使用,但在其他测试中使用。与标记为[TestInitialize]的方法不同,它只在第一次测试开始之前启动。在每个测试方法的末尾,有一个Assert类方法的调用,它将测试值与所需值进行比较。因此,这个测试要么被证实,要么被否定。        

[TestClass]
public class ConfigTests
{
    private string ConfigName = $"{Environment.CurrentDirectory}\\MyTestConfig.ini";
    private string first_excention = "first getters call mast be null because file doesn't contain this key";
    Config config;

    [TestInitialize]
    public void TestInitialize()
    {
        if (File.Exists(ConfigName))
            File.Delete(ConfigName);
        config = new Config(ConfigName);
    }
    [TestMethod]
    public void StringConverter_GetSetTest()
    {
        string expected = null;
 
        // first get
        string s = config.Common.Password;
        Assert.AreEqual(expected, s, first_excention);

        // set
        expected = "MyTestPassward";
        config.Common.Password = expected;
        s = config.Common.Password;
        Assert.AreEqual(expected, s, "Login mast be equal to MyTestLogin");

        // set null
        config.Common.Login = null;
        s = config.Common.Password;
        Assert.AreEqual(expected, s, "Login mast be equal to MyTestLogin");
    }
    [TestMethod]
    public void ServerConverter_GetSetTest()
    {
        ServerAddressKeeper expected = null;

        // first get;
        ServerAddressKeeper server = config.Common.Server;
        Assert.AreEqual(expected, server);

        // set
        expected = new ServerAddressKeeper("193.219.127.76:4443"); // Open broker demo server
        config.Common.Server = expected;
        server = config.Common.Server;
        Assert.AreEqual(server.Address, expected.Address, $"Address must be {expected.Address}");
    }
    [TestMethod]
    public void BoolConverter_GetSetTest()
    {
        bool? expected = null;

        // first get
        bool? b = config.Common.ProxyEnable;
        Assert.AreEqual(expected, b, first_excention);

        // set
        Random gen = new Random();
        int prob = gen.Next(100);
        expected = prob <= 50;
        config.Common.ProxyEnable = expected;
        b = config.Common.ProxyEnable;
        Assert.AreEqual(expected.Value, b.Value, "ProxyEnables must be equal to true");

        // set null
        config.Common.ProxyEnable = null;
        b = config.Common.ProxyEnable;
        Assert.AreEqual(expected.Value, b.Value, "ProxyEnables must be equal to true");

    }
    [TestMethod]
    public void ENUMConverter_GetSetTest()
    {
        ENUM_ProxyType? expected = null;

        // first get
        ENUM_ProxyType? p = config.Common.ProxyType;
        Assert.AreEqual(expected, p, first_excention);

        // set
        Random gen = new Random();
        int prob = gen.Next(300);
        int n = prob <= 100 ? 0 : (prob > 100 && prob <= 200 ? 1 : 2);
        expected = (ENUM_ProxyType)n;

        config.Common.ProxyType = expected;
        p = config.Common.ProxyType;
        Assert.AreEqual(expected.Value, p.Value, $"ProxyType must be equal to {expected.Value}");

        // set null
        config.Common.ProxyEnable = null;
        p = config.Common.ProxyType;
        Assert.AreEqual(expected.Value, p.Value, $"ProxyType must be equal to {expected.Value}");
    }
    [TestMethod]
    public void DTConverter_GetSetTest()
    {
        DateTime? expected = null;

        // first get
        DateTime? p = config.Tester.FromDate;
        Assert.AreEqual(expected, p, first_excention);

        // set
        expected = DateTime.Now;

        config.Tester.FromDate = expected;
        p = config.Tester.FromDate;
        Assert.AreEqual(expected.Value.Date, p.Value.Date, $"ProxyType must be equal to {expected.Value}");

        // set null
        config.Common.ProxyEnable = null;
        p = config.Tester.FromDate;
        Assert.AreEqual(expected.Value.Date, p.Value.Date, $"ProxyType must be equal to {expected.Value}");
    }
    [TestMethod]
    public void DoubleConverter_GetSetTest()
    {
        double? expected = null;

        // first get
        double? p = config.Tester.Deposit;
        Assert.AreEqual(expected, p, first_excention);

        // set
        Random rnd = new Random();
        expected = rnd.NextDouble();

        config.Tester.Deposit = expected;
        p = config.Tester.Deposit;
        Assert.AreEqual(Math.Round(expected.Value, 6), Math.Round(p.Value, 6), $"Deposit must be equal to {expected.Value}");

        // set null
        config.Common.ProxyEnable = null;
        p = config.Tester.Deposit;
        Assert.AreEqual(Math.Round(expected.Value, 6), Math.Round(p.Value, 6), $"Deposit must be equal to {expected.Value}");
    }
    [TestMethod]
    public void DeleteKeyTest()
    {
        config.Common.Login = 12345;
        config.DeleteKey(ENUM_SectionType.Common, "Login");

        Assert.AreEqual(null, config.Common.Login, "Key must be deleted");
    }
    [TestMethod]
    public void DeleteSectionTest()
    {
        config.Common.Login = 12345;
        config.DeleteSection(ENUM_SectionType.Common);

        Assert.AreEqual(null, config.Common.Login, "Key must be deleted");
    }
}

如果我们考虑这个特定的测试类,应该注意的是它并没有覆盖所有必需的方法,而是测试Config.Converter类,它基本上用配置文件执行整个操作逻辑。但是,由于它是私有类,我们需要编写测试,而不是为类本身,而是为使用该类的属性。例如,DoubleConverter_GetSetTest()通过config.Tester.Deposit属性测试“string”到“double”的转换是否正确。此特定测试由三部分组成:

  1. 从尚未创建的字段中请求double类型的参数-应返回null
  2. 将随机值写入文件并读取
  3. Null 项目应当被忽略

如果在任何阶段检测到错误,则可以很容易地检测和纠正错误。因此,这些测试对于应用程序开发是有用的。创建完所有测试后,可以直接从visualstudio运行它们,在路径 Test=>Run=>All Tests执行启动


它们对于具有不同计算机区域标准的读者也很有用:通过运行这些测试,您可以检测出可能的错误(例如,与十进制分隔符相关的错误)并修复它们。

优化管理器 (OptimissationManager)

应用程序的标准之一是可扩展性。优化过程将在下一篇文章中进行更改,而主附加UI不需要进行重大更改。这就是为什么我决定实现优化过程不是作为一个模型类,而是作为一个抽象类,它的实现可以依赖于所请求的优化方法。这个类是根据抽象类工厂模板编写的。让我们从工厂开始:

/// <summary>
/// Factory for creating classes that manage the optimization process
/// </summary>
abstract class OptimisationManagerFabric
{
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="ManagerName">The name of the created optimization manager</param>
    public OptimisationManagerFabric(string ManagerName)
    {
        this.ManagerName = ManagerName;
    }
    /// <summary>
    /// Name reflecting the type of the created optimization manager (its features)
    /// </summary>
    public string ManagerName { get; }
    /// <summary>
    /// Method creating the optimization manager
    /// </summary>
    /// <returns>Optimization manager</returns>
    public abstract OptimisationManager Create(Dictionary<string, BotParamKeeper> botParamsKeeper,
                                               List<ViewModel.TerminalAndBotItem> selectedTerminals);
}

从抽象工厂类可以看出,它包含实现类的名称,将在以后的文章中使用,以及创建优化管理器的方法。假设优化管理器是在每次优化之前创建的,然后使用终端进行操作。因此,参数(例如具有专家顾问列表的字典和终端列表(即在不同优化中不同的参数)将传递给对象创建方法。所有其他必需的参数都将从构造函数传递到特定工厂的类。现在让我们考虑一下 OptimisationManager 类。这个类是为管理优化而设计的。此外,它还负责启动测试。由于测试的启动几乎总是按照相同的算法执行,因此该功能直接在所考虑的抽象类中实现。我们将考虑下面的类实现。对于优化启动和停止,这个功能是用两个抽象方法实现的,这两个方法需要在子类中实现。类构造函数接受工厂的多余数量,因此可以使用上面考虑的所有对象进行操作。

public OptimisationManager(TerminalDirectory terminalDirectory,
                                   TerminalCreator terminalCreator,
                                   ConfigCreator configCreator,
                                   ReportReaderCreator reportReaderCreator,
                                   SetFileManagerCreator setFileManagerCreator,
                                   OptimisationExtentionWorkingDirectory currentWorkingDirectory,
                                   Dictionary<string, BotParamKeeper> botParamsKeeper,
                                   Action<double, string, bool> pbUpdate,
                                   List<ViewModel.TerminalAndBotItem> selectedTerminals,
                                   OptimisatorSettingsManagerCreator optimisatorSettingsManagerCreator)

AllOptimisationsFinished事件用于通知模型类优化完成。以下属性允许从模型类访问此优化管理器中包含的有关终端和机器人的数据。

/// <summary>
/// Dictionary where:
/// key - terminal ID
/// value - full path to the robot
/// </summary>
public virtual Dictionary<string, string> TerminalAndBotPairs
{
    get
    {
        Dictionary<string, string> ans = new Dictionary<string, string>();
        foreach (var item in botParamsKeeper)
        {
            ans.Add(item.Key, item.Value.BotName);
        }
        return ans;
    }
}

此属性是在抽象类中实现的,但它可以重写,因为它用“virtual”关键字标记。为了使模型类能够查明优化/测试过程是否已启动,创建了适当的属性。属性的值是从启动优化/测试过程的方法中设置的。

public bool IsOptimisationOrTestInProcess { get; private set; } = false;

为了方便起见,在大多数情况下优化类和测试启动类中保持不变的长类直接在抽象类中实现。这是形成配置文件的方法。 

protected virtual Config CreateConfig(ConfigCreator_inputData data,
                                      OptimisationInputData optData)
{
    DirectoryInfo termonalChangableFolder = terminalDirectory.Terminals.Find(x => x.Name == data.TerminalID);

    Config config = configCreator.Create(Path.Combine(termonalChangableFolder.GetDirectory("config").FullName, "common.ini"))
                                         .DublicateFile(Path.Combine(currentWorkingDirectory.Configs.FullName, $"{data.TerminalID}.ini"));

    // Fill the configuration file
    config.Common.Login = data.Login;
    config.Common.Password = data.Pass;
    config.Common.CertPassword = data.CertPass;
    if (!string.IsNullOrEmpty(data.Server) || !string.IsNullOrWhiteSpace(data.Server))
    {
        try
        {
            config.Common.Server = new ServerAddressKeeper(data.Server);
        }
        catch (Exception e)
        {
            System.Windows.MessageBox.Show($"Server address was incorrect. Your adress is '{data.Server}' but mast have following type 'IPv4:Port'" +
                                           $"\nError message:\n{e.Message}\n\nStack Trace is {e.StackTrace}");
            return null;
        }
    }

    bool IsOptimisation = (data.OptimisationMode == ENUM_OptimisationMode.Fast_genetic_based_algorithm ||
                           data.OptimisationMode == ENUM_OptimisationMode.Slow_complete_algorithm);

    config.Tester.Expert = data.pathToBot;
    config.Tester.ExpertParameters = data.setFileName;
    сonfig.Tester.Symbol = data.Symbol;
    config.Tester.Period = data.TF;
    config.Tester.Login = optData.Login;
    config.Tester.Model = optData.Model;
    config.Tester.ExecutionMode = optData.ExecutionDelay;
    config.Tester.Optimization = data.OptimisationMode;
    с data.From;
    config.Tester.ToDate = data.Till;
    config.Tester.ForwardMode = ENUM_ForvardMode.Custom;
    config.Tester.ForwardDate = optData.ForvardDate;
    config.Tester.ShutdownTerminal = IsOptimisation;
    config.Tester.Deposit = optData.Deposit;
    config.Tester.Currency = optData.Currency;
    config.Tester.Leverage = optData.Laverage;
    config.Tester.OptimizationCriterion = optData.GetOptimisationCriteria;
    config.Tester.Visual = optData.IsVisual;

    if (IsOptimisation)
    {
        config.Tester.Report = data.ReportName;
        config.Tester.ReplaceReport = true;
    }

    return config;
}

首先,使用描述终端变量目录的类和配置类型的工厂创建对象,我们创建一个配置文件对象并将其复制到插件的相应目录。将其名称设置为原始配置文件所属的终端的ID。然后填写复制的配置文件的[Tester]部分。要填充此部分的所有数据都直接从传递的结构中获取,这些结构要么是在代码中形成的(在优化的情况下),要么是从文件中获取的(在测试开始的情况下)。如果服务器的传递不正确,则会将适当的消息输出为MessageBox,而返回null而不是配置文件。出于同样的目的,为了分离重复代码,在抽象类中实现创建终端管理器的方法。代码在这里:

protected virtual ITerminalManager GetTerminal(Config config, string TerminalID)
{
    DirectoryInfo TerminalChangebleFolder = terminalDirectory.Terminals.Find(x => x.Name == TerminalID);

    ITerminalManager terminal = terminalCreator.Create(TerminalChangebleFolder);
    terminal.Config = config;

    if (MQL5Connector.MainTerminalID == terminal.TerminalID)
        terminal.Portable = true;

     return terminal;
}

如果所需终端的ID与启动加载项的终端的ID匹配,则该终端配置为以便携模式启动,但应用程序只能在以标准模式启动终端时才能正常运行。因此,它有一个忽略当前终端的筛选器,并且不会将其添加到可用终端列表中。

在选定终端中双击事件时启动测试的方法也在抽象类中实现:

/// <summary>
/// Method for launching a test upon a double-click event 
/// </summary>
/// <param name="TerminalID">ID of the selected terminal</param>
/// <param name="pathToBot">Path to the robot relative to the experts tab</param>
/// <param name="row">Row from the optimizations table</param>
public virtual void StartTest(ConfigCreator_inputData data,
                              OptimisationInputData optData)
{
    pbUpdate(0, "Start Test", true);

    double pb_step = 100.0 / 3;

    IsOptimisationOrTestInProcess = true;

    pbUpdate(pb_step, "Create Config File", false);
    Config config = CreateConfig(data, optData);
    config.Tester.Optimization = ENUM_OptimisationMode.Disabled;
    config.Tester.ShutdownTerminal = false;
    config.DeleteKey(ENUM_SectionType.Tester, "ReplaceReport");
    config.DeleteKey(ENUM_SectionType.Tester, "Report");

    pbUpdate(pb_step, "Create TerminalManager", false);
    ITerminalManager terminal = GetTerminal(config, data.TerminalID);

    pbUpdate(pb_step, "Testing", false);
    terminal.Run();
    terminal.WaitForStop();
    IsOptimisationOrTestInProcess = false;
    pbUpdate(0, null, true);
}

在其输入参数中,它接受在描述模型的类中从保存了设置的文件接收的数据。在方法内部,进度条值和操作状态也通过传递的委托进行设置。生成的配置文件被调整为运行测试器:描述优化器报告的键被删除,并且在测试器结束后自动关闭终端。终端启动后,启动终端的线程将冻结并等待其操作完成,从而通知表单测试结束。为了避免在优化/测试启动时冻结表单,这些进程将在次线程的上下文中启动。至于优化,如前所述,这个过程是用受保护的抽象方法实现的。但是,在抽象类中实现了一个公共方法,这是类的正确操作所必需的,并且不能重写。

/// <summary>
/// Launching optimization/testing for all planned terminals
/// </summary>
/// <param name="BotParamsKeeper">List of terminals, robots and robot parameters</param>
/// <param name="PBUpdate">The delegate editing the values of the progress bar and the status</param>
/// <param name="sturtup_status">Response from the function - only used if optimization/test could not be started 
/// reason for that is written here</param>
/// <returns>true - if successful</returns>
public void StartOptimisation()
{
    pbUpdate(0, "Start Optimisation", true);
    IsOptimisationOrTestInProcess = true;
 
    DoOptimisation();
    OnAllOptimisationsFinished();
    IsOptimisationOrTestInProcess = false;
    pbUpdate(0, null, true);
}
protected abstract void DoOptimisation();

/// <summary>
/// The method interrupting optimizations
/// </summary>
public abstract void BreakOptimisation();

此方法根据进度条的更新、优化开始和完成标志的设置以及优化传递完成事件的调用,调整优化过程触发的顺序。

抽象类中实现的最后一个方法是将报表移动到加载项的工作目录的方法。除了报表移动之外,还应创建具有优化设置的文件,因此这些操作将在单独的方法中实现。

protected virtual void MoveReportToWorkingDirectery(ITerminalManager terminalManager,
                                                    string FileName,
                                                    ConfigCreator_inputData ConfigCreator_inputData,
                                                    OptimisationInputData OptimisationInputData)
{
    FileInfo pathToFile_history = new FileInfo(Path.Combine(terminalManager.TerminalChangeableDirectory.FullName, $"{FileName}.xml"));
    FileInfo pathToFile_forward = new FileInfo(Path.Combine(terminalManager.TerminalChangeableDirectory.FullName, $"{FileName}.forward.xml"));
    int _i = 0;
    while (_i <= 100 && (!pathToFile_history.Exists && !pathToFile_forward.Exists))
    {
        _i++;
        System.Threading.Thread.Sleep(500);
    }

    string botName = new FileInfo(terminalManager.Config.Tester.Expert).Name.Split('.')[0];
    DirectoryInfo terminalReportDirectory = currentWorkingDirectory.Reports.GetDirectory(terminalManager.TerminalID, true);
    if (terminalReportDirectory == null)
        throw new Exception("Can`t create directory");
    DirectoryInfo botReportDir = terminalReportDirectory.GetDirectory(botName, true);
    if (botReportDir == null)
        throw new Exception("Can`t create directory");

    FileInfo _history = new FileInfo(Path.Combine(botReportDir.FullName, "History.xml"));
    FileInfo _forward = new FileInfo(Path.Combine(botReportDir.FullName, "Forward.xml"));

    if (_history.Exists)
        _history.Delete();
    if (_forward.Exists)
        _forward.Delete();

    if (pathToFile_history.Exists)
    {
        pathToFile_history.CopyTo(_history.FullName, true);
        pathToFile_history.Delete();
    }
    if (pathToFile_forward.Exists)
    {
        pathToFile_forward.CopyTo(_forward.FullName, true);
        pathToFile_forward.Delete();
    }

    string pathToSetFile = Path.Combine(terminalManager.TerminalChangeableDirectory
                                        .GetDirectory("MQL5")
                                        .GetDirectory("Profiles")
                                        .GetDirectory("Tester").FullName,
                                        ConfigCreator_inputData.setFileName);

    using (OptimisatorSettingsManager manager =
           optimisatorSettingsManagerCreator.Create(botReportDir.FullName))
    {
        manager.OptimisationInputData = OptimisationInputData;
        manager.ConfigCreator_inputData = ConfigCreator_inputData;
        manager.SetFileParams = setFileManagerCreator.Create(pathToSetFile, false).Params;
    }
}

首先,在这个方法中,我们获得到带有报告的文件的路径。然后循环等待,直到创建一个所需的文件(仅创建一个,因为并不总是会生成这两个文件-这可能发生在历史优化过程中,没有转发周期)。然后形成指向存储带有报告的文件的目录的路径。实际上,此代码段包含 Reports 目录子文件夹的布局接下来,我们创建指向未来文件的路径,并删除旧文件(如果有)。之后,将 报告复制到加载项目录。最后,我们使用优化启动期间使用的设置创建一个*.xml文件。由于这个过程应该分阶段执行,而且不太可能更改,所以它被移到了一个抽象类中,要启动它,我们可以简单地从子类调用这个方法。

现在让我们考虑实现的优化过程。目前,这是一个常见的终端启动,与使用选择的优化参数在一个标准测试器类似。它实现的最有趣的方面是启动过程和优化完成事件的处理程序。

private readonly List<ITerminalManager> terminals = new List<ITerminalManager>();
/// <summary>
/// The method interrupts the optimization process and forcibly closes the terminals
/// </summary>
public override void BreakOptimisation()
{
    foreach (var item in terminals)
    {
        if (item.IsActive)
            item.Close();
    }
}
private void UnsubscribeTerminals()
{
    if (terminals.Count > 0)
    {
        foreach (var item in terminals)
        {
            item.TerminalClosed -= Terminal_TerminalClosed;
        }
        terminals.Clear();
    }
}

protected override void DoOptimisation()
{
    UnsubscribeTerminals();

    double pb_step = 100.0 / (botParamsKeeper.Count + 1);

    foreach (var item in botParamsKeeper)
    {
        pbUpdate(pb_step, item.Key, false);

        ConfigCreator_inputData configInputData = GetConfigCreator_inputData(item.Key);
        OptimisationInputData optData = item.Value.OptimisationData;

        Config config = CreateConfig(configInputData, optData);

        ITerminalManager terminal = GetTerminal(config, item.Key);
        terminal.TerminalClosed += Terminal_TerminalClosed;
        terminal.Run();

        terminals.Add(terminal);
    }

    pbUpdate(pb_step, "Waiting for Results", false);

    foreach (var item in terminals)
    {
        if (item.IsActive)
            item.WaitForStop();
    }
}

终端管理器列表是在一个字段中实现的,可以通过不同的方法访问。这也实现了BreakOptimisations 方法。在优化过程启动方法中,在创建终端后,我们订阅终端关闭事件,从而跟踪优化完成情况。优化启动后,我们将th线程保持在一个循环中,直到所有启动的终端都关闭。UnsubscribeTerminals 方法用于在优化重新启动时从以前订阅的所有事件中取消订阅。方法在类析构函数中调用。优化停止事件处理程序的实现如下:

protected virtual void Terminal_TerminalClosed(ITerminalManager terminalManager)
{
    string FileName = new FileInfo(terminalManager.Config.Tester.Expert).Name.Split('.')[0];

    ConfigCreator_inputData ConfigCreator_inputDat = GetConfigCreator_inputData(terminalManager.TerminalID);
    OptimisationInputData optData = botParamsKeeper[terminalManager.TerminalID].OptimisationData;

    MoveReportToWorkingDirectery(terminalManager, FileName, ConfigCreator_inputDat, optData);
}

private ConfigCreator_inputData GetConfigCreator_inputData(string TerminalID)
{
    ViewModel.TerminalAndBotItem settingsData = selectedTerminals.Find(x => x.TerminalID == TerminalID);
    BotParamKeeper ParamKeeper = botParamsKeeper[TerminalID];

    ConfigCreator_inputData ConfigCreator_inputDat = new ConfigCreator_inputData
    {
        TerminalID = TerminalID,
        pathToBot = ParamKeeper.BotName,
        CertPass = settingsData.CertPass,
        From = settingsData.From,
        Till = settingsData.Till,
        Login = settingsData.Login,
        OptimisationMode = settingsData.GetOptimisationMode,
        Pass = settingsData.Pass,
        Server = settingsData.Server,
        setFileName = botParamsKeeper[TerminalID].BotParams.FileInfo.Name,
        Symbol = settingsData.AssetName,
        TF = settingsData.GetTF,
        ReportName = new FileInfo(ParamKeeper.BotName).Name.Split('.')[0]
    };

    return ConfigCreator_inputDat;
}

其主要目的是将带有优化报告的文件移动到适当的目录。从而实现了运行逻辑的优化和测试。我们将在下一篇文章中执行的操作之一是根据所描述的示例实现额外的优化方法。我们几乎检查了整个创建的应用程序,现在让我们看看主要的结果类,它描述了从 ViewModel 引用的模型。

结果模式类 (IExtentionGUI_M 以及它的实现)

所描述项目的这一部分实现了IExtentionGUI_M 接口,并且是实现所描述表单的逻辑的起点。图形部分和 ViewModel 引用这个类来接收数据并委托各种命令的执行,让我们从下面实现的接口开始。

/// <summary>
/// Model interface
/// </summary>
interface IExtentionGUI_M : INotifyPropertyChanged
{
    #region Properties

    bool IsTerminalsLVEnabled { get; }
    List<FileReaders.ParamsItem> BotParams { get; }
    VarKeeper<string> Status { get; }
    VarKeeper<double> PB_Value { get; }
    ObservableCollection<string> TerminalsID { get; }
    DataTable HistoryOptimisationResults { get; }
    DataTable ForvardOptimisationResults { get; }
    ObservableCollection<ViewExtention.ColumnDescriptor> OptimisationResultsColumnHeadders { get; }
    ObservableCollection<string> TerminalsAfterOptimisation { get; }
    VarKeeper<int> TerminalsAfterOptimisation_Selected { get; set; }
    ObservableCollection<string> BotsAfterOptimisation { get; }
    VarKeeper<int> BotsAfterOptimisation_Selected { get; set; }

    #endregion

    void LoadOptimisations();
    void LoadBotParams(string fullExpertName,
        string TerminalID,
        out OptimisationInputData? optimisationInputData);
    List<string> GetBotNamesList(int terminalIndex);
    uint? GetCurrentLogin(int terminalIndex);
    void StartOptimisationOrTest(List<ViewModel.TerminalAndBotItem> SelectedTerminals);
    void StartTest(ENUM_TableType TableType, int rowIndex);
    bool RemoveBotParams(string TerminalID);
    bool IsEnableToAddNewTerminal();
    void SelectNewBotsAfterOptimisation_forNewTerminal();
    void UpdateTerminalOptimisationsParams(OptimisationInputData optimisationInputData);
}

#region Accessory objects 

/// <summary>
/// Enum characterizing the type of tables with optimization results
/// </summary>
enum ENUM_TableType
{
    History,
    Forvard
}

这是 ViewModel 操作的接口,如有必要,其实现可以替换为任何其他实现。在这种情况下,不需要更改程序的图形部分。另一方面,我们可以在不改变逻辑的情况下改变图形部分。接口是从INotifyPropertyChanged接口继承的,因此我们有机会通知 ViewModel 并查看在此数据模型中实现的任何属性是否已更改。为了方便起见,我添加了一个通用包装类 VarKeeper,它除了可以存储任何类型值之外,还可以隐式转换为存储的类型,如果存储的值发生更改,还可以通知 ViewModel。下面是类实现:

/// <summary>
/// Class storing the variable _Var of type T_keeper.
/// We can implicitly cast to type T_keeper and also change the value of the stored variable
/// At the time of changing the value it notifies all those which have subscribed
/// </summary>
/// <typeparam name="T_keeper">Type of stored variable</typeparam>
class VarKeeper<T_keeper>
{
    /// <summary>
    /// Constructor specifying the variable identification name
    /// </summary>
    /// <param name="propertyName">Variable identification name</param>
    public VarKeeper(string propertyName)
    {
        this.propertyName = propertyName;
    }
    /// <summary>
    /// Constructor specifying the variable identification name 
    /// and the initial value of the variable
    /// </summary>
    /// <param name="PropertyName">Identification name of the variable</param>
    /// <param name="Var">initial value of the variable</param>
    public VarKeeper(string PropertyName, T_keeper Var) : this(PropertyName)
    {
        _Var = Var;
    }
    /// <summary>
    /// Overloading the implicit type conversion operator.
    /// Converts this type to T_keeper
    /// </summary>
    /// <param name="obj"></param>
    public static implicit operator T_keeper(VarKeeper<T_keeper> obj)
    {
        return obj._Var;
    }
    /// <summary>
    /// stored variable 
    /// </summary>
    protected T_keeper _Var;
    /// <summary>
    /// Identification name of the variable
    /// </summary>
    public readonly string propertyName;
    #region Event 
    /// <summary>
    /// Event notifying about the change of the stored variable
    /// </summary>
    public event Action<string> PropertyChanged;
    /// <summary>
    /// Method that calls the event notifying about the change of the stored variable
    /// </summary>
    protected void OnPropertyChanged()
    {
        PropertyChanged?.Invoke(propertyName);
    }
    #endregion
    /// <summary>
    /// Method which sets the value of a variable with the 'value' value
    /// </summary>
    /// <param name="value">new value of the variable</param>
    public void SetVar(T_keeper value)
    {
        SetVarSilently(value);
        OnPropertyChanged();
    }
    public void SetVarSilently(T_keeper value)
    {
        _Var = value;
    }
}

类构造函数中,我们传递存储变量的初始值和将用于通知值更改的变量的名称。变量存储在此类的受保护字段中。用于通知值更改的变量的名称存储在公共只读字段ropertyName中。变量值设置方法分为设置其值并调用事件以通知所有订阅者此更改的方法和仅设置变量值的方法。要启用到存储值类型的隐式类转换,其中使用类型转换运算符的重载。此类使我们能够存储变量值,在不使用显式类型转换的情况下读取它们,并将变量值的更改通知环境。在实现 IExtentionGUI_M接口的类的构造函数中,将值设置为属性并订阅这些属性的更新通知。在这个类析构函数中,取消订阅属性事件。

public ExtentionGUI_M(TerminalCreator TerminalCreator,
                      ConfigCreator ConfigCreator,
                      ReportReaderCreator ReportReaderCreator,
                      SetFileManagerCreator SetFileManagerCreator,
                      OptimisationExtentionWorkingDirectory CurrentWorkingDirectory,
                      OptimisatorSettingsManagerCreator SettingsManagerCreator,
                      TerminalDirectory terminalDirectory)
{
    // Assign the current working directory
    this.CurrentWorkingDirectory = CurrentWorkingDirectory;
    this.terminalDirectory = terminalDirectory;
    //Create factories
    this.TerminalCreator = TerminalCreator;
    this.ReportReaderCreator = ReportReaderCreator;
    this.ConfigCreator = ConfigCreator;
    this.SetFileManagerCreator = SetFileManagerCreator;
    this.SettingsManagerCreator = SettingsManagerCreator;
    CreateOptimisationManagerFabrics();

    // subscribe to the event of a change in columns of the historic optimizations table
    HistoryOptimisationResults.Columns.CollectionChanged += Columns_CollectionChanged;

    // Assign initial status
    Status = new VarKeeper<string>("Status", "Wait for the operation");
    Status.PropertyChanged += OnPropertyChanged;
    // Assign initial values for the progress bar
    PB_Value = new VarKeeper<double>("PB_Value", 0);
    PB_Value.PropertyChanged += OnPropertyChanged;
    // Create a variable storing the index of terminal selected from the list of available terminals for which optimization was done
    TerminalsAfterOptimisation_Selected = new VarKeeper<int>("TerminalsAfterOptimisation_Selected", 0);
    TerminalsAfterOptimisation_Selected.PropertyChanged += OnPropertyChanged;
    // Create a variable storing the index of robot selected from the list of available robots for which optimization was done
    BotsAfterOptimisation_Selected = new VarKeeper<int>("BotsAfterOptimisation_Selected", -1);
    BotsAfterOptimisation_Selected.PropertyChanged += OnPropertyChanged;

    _isTerminalsEnabled = new VarKeeper<bool>("IsTerminalsLVEnabled", true);
    _isTerminalsEnabled.PropertyChanged += OnPropertyChanged;

    // Load data on terminals installed on the computer
    FillInTerminalsID();
    FillInTerminalsAfterOptimisation();
    LoadOptimisations();
}

在构造函数中调用以下方法:

  • CreateOptimisationManagerFabrics — 创建优化管理器的工厂:它们被添加到数组中;稍后将根据我们的特定条件从中选择所需的优化管理器。
  • FillInTerminalsID — 填充终端 ID的列表,这些ID在优化之前显示在下拉式终端选择列表中。所有找到的终端,除了当前添加到列表中的终端。
  • FillInTerminalsAfterOptimisation — 填充已执行任何优化且有数据要加载到优化数据的终端列表。
  • LoadOptimiations — 根据所选终端和robot填充优化表(这两个参数当前的索引均为零)。

这样,我们实现了构造函数的主要任务:为操作准备程序,用初始值填充所有表和变量。下一个阶段涉及到对选定的终端表进行优化的操作。所有选定的终端都存储在一个类字段的词汇表中。

/// <summary>
/// Presenting the table of selected terminals at the start tab of the add-on
/// key - Terminal ID
/// value - bot params
/// </summary>
private readonly Dictionary<string, BotParamKeeper> BotParamsKeeper = new Dictionary<string, BotParamKeeper>();
/// <summary>
/// Currently selected terminal
/// </summary>
private string selectedTerminalID = null;
/// <summary>
/// List of robot parameters to be edited
/// </summary>
List<ParamsItem> IExtentionGUI_M.BotParams
{
    get
    {
        return (BotParamsKeeper.Count > 0 && selectedTerminalID != null) ?
               BotParamsKeeper[selectedTerminalID].BotParams.Params :
               new List<ParamsItem>();
    }
}

BotParams从该词汇表中接收 EA 参数列表,当所选机器人发生更改(将进一步描述该机制)时,我们将访问该词汇表中的新键。词汇表内容由 LoadBotParam方法控制,该方法在单击用于添加新终端的按钮后立即调用,该按钮是从该加载项的第一个选项卡的下拉列表中选择的。该方法实现如下:

void IExtentionGUI_M.LoadBotParams(string fullExpertName,
            string TerminalID,
            out OptimisationInputData? optimisationInputData)
{
    PBUpdate(0, "Loading params", true);
    optimisationInputData = null;

    if (!IsTerminalsLVEnabled)
        return;

    _isTerminalsEnabled.SetVar(false);

    if (!BotParamsKeeper.Keys.Contains(TerminalID))
    {
        PBUpdate(100, "Add New Terminal", false);
        AddNewTerminalIntoBotParamsKeeper(fullExpertName, TerminalID);
    }
    else
    {
        if (selectedTerminalID != null)
            BotParamsKeeper[selectedTerminalID].BotParams.SaveParams();
        else
        {
            foreach (var item in BotParamsKeeper)
            {
                item.Value.BotParams.SaveParams();
            }
        }
    }

    selectedTerminalID = TerminalID;
    optimisationInputData = BotParamsKeeper[selectedTerminalID].OptimisationData;

    if (BotParamsKeeper[selectedTerminalID].BotName != fullExpertName)
    {
        PBUpdate(100, "Load new params", false);
        BotParamKeeper param = BotParamsKeeper[selectedTerminalID];
        param.BotName = fullExpertName;
        param.BotParams = GetSetFile(fullExpertName, TerminalID);
        BotParamsKeeper[selectedTerminalID] = param;
    }
    PBUpdate(0, null, true);
    _isTerminalsEnabled.SetVar(true);
}

从代码中可以看出,除了在优化测试期间阻塞用户界面(如视频中所示),代码还包括是否可以更新机器人(可能还有终端)参数列表的检查。如果机器人或终端参数可以更新,则阻止图形界面。然后添加一个新的机器人,或者保存先前通过GUI输入的参数。之后,选定的终端 ID被保存(词汇表中的一个键),新选定的机器人的参数被传回 ViewModel。如果我们将选定的机器人与先前选定的机器人相比进行了更改,请通过GetSetFile方法上载该机器人的参数。添加新终端的方法非常简单,几乎完全重复了所考虑方法的最后一个条件构造。主要工作由 GetSetFile 方法执行。

private SetFileManager GetSetFile(string fullExpertName, string TerminalID)
{
    DirectoryInfo terminalChangableFolder = terminalDirectory.Terminals.Find(x => x.Name == TerminalID);

    // Creating a manager for working with the terminal
    ITerminalManager terminalManager = TerminalCreator.Create(terminalChangableFolder);

    // Creating path to the Tester folder (which is under ~/MQL5/Profiles) 
    // If there is no such folder, create it yourself
    // Files with optimization parameter settings are stored in it
    DirectoryInfo pathToMqlTesterFolder = terminalManager.MQL5Directory.GetDirectory("Profiles").GetDirectory("Tester", true);
    if (pathToMqlTesterFolder == null)
        throw new Exception("Can`t find (or create) ~/MQL5/Profiles/Tester directory");

    // Create a configuration file and copy it to the Configs folder of the current working add-on directory
    Config config = ConfigCreator.Create(Path.Combine(terminalChangableFolder.GetDirectory("config").FullName, "common.ini"))
                                 .DublicateFile(Path.Combine(CurrentWorkingDirectory.Configs.FullName, $"{TerminalID}.ini"));
    // Configure the terminal so that it launches the selected robot test and immediately shuts down
    // Thus the terminal will create a .set file with this Expert Advisor settings.
    // To have it immediately shut down, specify the test end one day lower than the start date.
    config.Tester.Expert = fullExpertName;
    config.Tester.Model = ENUM_Model.OHLC_1_minute;
    config.Tester.Optimization = ENUM_OptimisationMode.Disabled;
    config.Tester.Period = ENUM_Timeframes.D1;
    config.Tester.ShutdownTerminal = true;
    config.Tester.FromDate = DateTime.Now.Date;
    config.Tester.ToDate = config.Tester.FromDate.Value.AddDays(-1);

    // Set configuration file to the terminal manager, launch it and wait for he terminal to close
    // To enable automatic terminal shut down after testing completion, 
    // assign the true value to field config.Tester.ShutdownTerminal
    terminalManager.Config = config;
    terminalManager.WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized;
    string fileName = $"{new FileInfo(fullExpertName).Name.Split('.')[0]}.set";

    while (!terminalManager.Run())
    {
        System.Windows.MessageBoxResult mb_ans =
            System.Windows.MessageBox.Show(@"Can`t start terminal 
Close manually all MetaTrader terminals that are running now (except main terminal)",
"Can`t start terminal", System.Windows.MessageBoxButton.OKCancel);
        if (mb_ans == System.Windows.MessageBoxResult.Cancel)
            break;
    }
    terminalManager.WaitForStop();

    bool isSetFileWasCreated = pathToMqlTesterFolder.GetFiles().Any(x => x.Name == fileName);

    return SetFileManagerCreator.Create(Path.Combine(pathToMqlTesterFolder.FullName, fileName), !isSetFileWasCreated);
}

这种方法有很好的注释,让我们解释一下它的主要目的。该方法接收所选机器人的参数,即其 SET 文件。一旦机器人在测试器中启动,终端就会创建该文件,因此生成该文件的唯一方法是在测试器中运行选定的算法。为了不显式地执行此操作,带有正在运行的测试器的终端将以最小化模式启动。为了让测试器快速完成操作并关机,我们将测试结束日期设置为比测试开始日期早一天。如果终端已在运行,则会在循环中尝试打开它,并显示相应的消息。操作后,返回 SET 文件的面向对象表示形式。

这个类中的下一个有趣的点是优化启动过程,它由异步 StartOptimistianOrTest 方法执行。

async void IExtentionGUI_M.StartOptimisationOrTest(List<ViewModel.TerminalAndBotItem> SelectedTerminals)
{
    if (BotParamsKeeper.Count == 0)
       return;
    foreach (var item in BotParamsKeeper)
    {
        item.Value.BotParams.SaveParams();
    }

    SetOptimisationManager(SelectedTerminals);

    // Run the optimization and wait for it to finish
    _isTerminalsEnabled.SetVar(false);
    await System.Threading.Tasks.Task.Run(() => selectedOptimisationManager.StartOptimisation());
    _isTerminalsEnabled.SetVar(true);
}

private void SetOptimisationManager(List<ViewModel.TerminalAndBotItem> SelectedTerminals)
{
    // Select a factory to create an optimization manager from the list
    OptimisationManagerFabric OMFabric = optimisationManagerFabrics[0];
    // Unsubscribe from a previously used optimization manager
    if (selectedOptimisationManager != null)
    {
        // Check if optimization is running at the moment
        if (selectedOptimisationManager.IsOptimisationOrTestInProcess)
            return;

        selectedOptimisationManager.AllOptimisationsFinished -= SelectedOptimisationManager_AllOptimisationsFinished;
    }

    // Create an optimization manager and subscribe it to the optimization completion event
    selectedOptimisationManager = OMFabric.Create(BotParamsKeeper, SelectedTerminals);
    selectedOptimisationManager.AllOptimisationsFinished += SelectedOptimisationManager_AllOptimisationsFinished;
}

该实现演示了优化管理器的使用:它是在每次优化开始之前重新创建的。在这个实现中,只对对应数组中的第一个管理器执行创建。下一篇文章将展示一个更复杂的过程。测试启动类似于优化启动。但是,这里的robot参数将替换为双击选择的参数。 

async void IExtentionGUI_M.StartTest(ENUM_TableType TableType, int rowIndex)
{
    if (!IsTerminalsLVEnabled)
        return;

    string TerminalID = TerminalsAfterOptimisation[TerminalsAfterOptimisation_Selected];
    string pathToBot = BotsAfterOptimisation[BotsAfterOptimisation_Selected];
    DirectoryInfo terminalChangableFolder = terminalDirectory.Terminals.Find(x => x.Name == TerminalID);

    DataRow row = (TableType == ENUM_TableType.History ? HistoryOptimisationResults : ForvardOptimisationResults).Rows[rowIndex];

    ConfigCreator_inputData configInputData;
    OptimisationInputData OptimisatorSettings;

    DirectoryInfo BotReportDirectory = CurrentWorkingDirectory.Reports.GetDirectory(TerminalID).GetDirectory(pathToBot);
    using (OptimisatorSettingsManager settingsManager = SettingsManagerCreator.Create(BotReportDirectory.FullName))
    {
        configInputData = settingsManager.ConfigCreator_inputData;
        OptimisatorSettings = settingsManager.OptimisationInputData;

        string setFilePath = Path.Combine(terminalChangableFolder
                                          .GetDirectory("MQL5")
                                          .GetDirectory("Profiles")
                                          .GetDirectory("Tester", true).FullName,
                                           configInputData.setFileName);

        SetFileManager setFile = SetFileManagerCreator.Create(setFilePath, true);
        setFile.Params = settingsManager.SetFileParams;

        foreach (var item in setFile.Params)
        {
            if (row.Table.Columns.Contains(item.Variable))
                item.Value = row[item.Variable].ToString();
        }
        setFile.SaveParams();
    }

    _isTerminalsEnabled.SetVar(false);
    if (selectedOptimisationManager == null)
        SetOptimisationManager(new List<ViewModel.TerminalAndBotItem>());

    await System.Threading.Tasks.Task.Run(() =>
    {
        selectedOptimisationManager.StartTest(configInputData, OptimisatorSettings);
    });
    _isTerminalsEnabled.SetVar(true);
}

这个方法也是异步的,它还涉及到优化管理器的创建,前提是它以前没有创建过。要获取测试输入,调用位于选定机器人优化报告旁边的设置文件。创建robot设置文件后,找到优化报告中指定的那些参数,并使用“Value”参数中的值设置选中的优化行进行设置值。保存参数后,继续执行测试启动。 

要将优化结果上载到适当的表中,将使用以下包含嵌套方法的方法。

public void LoadOptimisations()
{
    // Internal method filling the table with data
    void SetData(bool isForvard, DataTable tb)
    {
        // Clear the table from previously added data
        tb.Clear();
        tb.Columns.Clear();

        // Get data
        string TerminalID = TerminalsAfterOptimisation[TerminalsAfterOptimisation_Selected];
        string botName = BotsAfterOptimisation[BotsAfterOptimisation_Selected];
        string path = Path.Combine(CurrentWorkingDirectory.Reports
                                                          .GetDirectory(TerminalID)
                                                          .GetDirectory(botName)
                                                          .FullName,
                                                          $"{(isForvard ? "Forward" : "History")}.xml");
        if (!File.Exists(path))
            return;

        using (ReportReader reader = ReportReaderCreator.Create(path))
        {
            if (reader.ColumnNames.Count == 0)
                return;

            // Fill the columns
            foreach (var item in reader.ColumnNames)
            {
                tb.Columns.Add(item);
            }

            // Fill the rows
            while (reader.Read(out List<KeyValuePair<string, object>> data))
            {
                DataRow row = tb.NewRow();
                foreach (var item in data)
                {
                    row[item.Key] = item.Value;
                }
                tb.Rows.Add(row);
            }
        }
    }

    if (TerminalsAfterOptimisation.Count == 0 && BotsAfterOptimisation.Count == 0)
    {
        return;
    }

    // Fill historic optimization data first, then add forward test results
    SetData(false, HistoryOptimisationResults);
    SetData(true, ForvardOptimisationResults);
}

该方法执行执行操作的嵌套函数的双调用。在嵌套函数中执行以下操作:

  1. 清除传递的表(及其列) 
  2. 设置报表文件的路径
  3. 使用 ReportReader类读取报表并将数据加载到表中。

以下代码行包含在构造函数中:

// subscribe to the event of a change in columns of the historic optimizations table
HistoryOptimisationResults.Columns.CollectionChanged += Columns_CollectionChanged;

它将 Columns_CollectionChanged 方法订阅为与历史优化表相关的列更新事件。使用此方法跟踪列的添加。在Subscribed方法(请参阅附件中的代码)中,列名将从 OptimizationResultsColumnHeaders 集合中自动添加或删除,并从中传递到 ViewModel和 View,然后使用上述扩展名将列名添加到 ListView中,以便自动加载列。这样,当在历史优化表中编辑列列表时,将自动编辑两个表中视图中的列。  

在本章中,我们研究了优化启动、程序加载和加载具有历史和正向优化过程的文件的实现细节,并分析了在双击事件时启动测试过程的方法。因此,视频中显示的应用程序几乎准备就绪,而只需要从终端启动。这将由以下作为专家顾问实现的包装器完成。

//+------------------------------------------------------------------+
//|                                 OptimisationManagerExtention.mq5 |
//|                        Copyright 2019, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"

#import "OptimisationManagerExtention.dll"
//+------------------------------------------------------------------+
//| EA 交易初始化函数                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---   

   string data[];
   StringSplit(TerminalInfoString(TERMINAL_DATA_PATH),'\\',data);
   MQL5Connector::Instance(data[ArraySize(data)-1]);

   while(!MQL5Connector::IsWindowActive())
     {
      Sleep(500);
     }

   EventSetMillisecondTimer(500);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnTimer()
  {
   if(!MQL5Connector::IsWindowActive())
      ExpertRemove();
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
  }
//+------------------------------------------------------------------+

编译 C# 项目(发布模式)后,将其添加到相应的目录(~/Libraries)并将连接到 robot。要获取当前终端ID,请找到 其变量目录的路径,然后使用StringSplit方法将其划分为组件部分。最后一个目录将包含终端ID。在图形启动之后,当前线程延迟将启用,直到加载窗口。然后运行计时器。计时器启用窗口关闭事件的跟踪。关闭窗口后,需要从图表中删除专家顾问。这样就实现了视频中所示的行为。

结论和附件

在本研究的开始,我们设定了一个目标,用GUI创建一个灵活的可扩展的插件,以管理优化过程。C# 语言用于实现,因为它为开发图形应用程序提供了方便的接口,以及许多额外的令人惊叹的选项,大大简化了编程过程。在本文中,我们考虑了创建应用程序的整个过程,从控制台程序的启动基础开始,使用C# 技术从另一个终端运行 MetaTrader 的包装器。我希望读者会觉得这项研究有趣而有用。在我看来,本文最后几章描述的类可以改进,因此在下一篇文章中可能会介绍代码重构。

附加的存档包含两个文件夹:

  • MQL5 是为主要的 MetaTrader 5 终端而设计的,在该终端中要启动附加组件。它包含一个运行加载项的文件。 
  • Visual Studio 包含了 Visual Studio 所描述的三个项目。在使用之前编译它们,将编译 OptimisationManagerExtention 后的*DLL库添加到终端的 Libraries 目录中,从该库中启动项目。 


本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/7059

附加的文件 |
美林(Merrill)形态 美林(Merrill)形态

在本文中,我们将研究美林形态的模型,并尝试评估它们与当前行情的相关性。 为此,我们将开发一种工具来测试形态,并将其模型应用在各种数据类型,例如收盘价、最高价和最低价,以及震荡指标。

轻松快捷开发 MetaTrader 程序的函数库(第十四部分):品种对象 轻松快捷开发 MetaTrader 程序的函数库(第十四部分):品种对象

在本文中,我们将创建品种对象类,该类将成为创建品种集合的基本对象。 该类可令我们获取必要品种的数据,以便进一步进行分析和比较。

付款和付款方式 付款和付款方式

MQL5.community 内置服务为MQL5开发人员和普通的无编程技巧的交易者们提供了巨大了机遇。但是,所有这些功能的实现都离不开安全的内部支付系统,为买家和卖家之间的结算提供了方便的基础。在本文中,我们将展示MQL5.community支付系统的工作方式。

MQL5.community - 用户手册 MQL5.community - 用户手册

如果你已经在本社区成功注册,那么你很可能会问:怎样在我发送的消息中插入图片?怎样格式化MQL5源代码?我的私信保存在哪?诸如此类的很多问题。本文我们为您准备了一些实用技巧,帮助你熟悉MQL5.community,并充分利用其提供的功能。