Otimização Walk Forward Contínua (parte 5): Panorama do Projeto Otimizador Automático e Criação da Interface Gráfica

Andrey Azatskiy | 1 julho, 2020

Introdução

Nos artigos anteriores, nós consideramos a parte do projeto diretamente relacionada à plataforma, bem como a parte que descreve a aplicação geral do projeto. O artigo anterior estava à frente do resto da série. Isso foi feito por duas razões. Em primeiro lugar, ele serve como uma instrução para usar o aplicativo. Em segundo lugar, ele ilustra a ideia e a lógica de criação de aplicativos, que ajuda a entender o código.

Os artigos estão disponíveis nos seguintes links:

  1. Otimização Walk Forward Contínua (parte 1): Trabalhando com os Relatórios de Otimização
  2. Otimização Walk Forward Contínua (Parte 2): Mecanismo para a criação de um relatório de otimização para qualquer robô
  3. Otimização Walk Forward Contínua (Parte 3): Método de Adaptação de um Robô ao Otimizador Automático
  4. Otimização Walk Forward Contínua (Parte 4): Gerenciamento de Otimização (Otimizador Automático)

Os artigos anteriores, dos quais foram emprestados alguns materiais, podem ser encontrados nos seguintes links:

  1. Gerenciando Otimizações (Parte I): Criando uma Interface Gráfica do Usuário
  2. Gerenciando Otimizações (Parte II): Criando a Lógica do Aplicativo e Objetos Chave

O artigo atual fornece uma descrição da estrutura do projeto no IDE do Visual Studio e seus componentes. Esta parte é dedicada à criação da Interface Gráfica do Usuário (GUI) do aplicativo. Ela também considera a estrutura do diretório gerenciado na qual as otimizações são armazenadas e as alterações nas classes de gerenciamento do processo de otimização extraídas do projeto anterior.


Visão geral da estrutura do projeto

Como esta parte do artigo também é dedicada ao C#, vamos começar com a consideração de sua estrutura de arquivos:

A solução anexada abaixo contém dois projetos. Um deles foi considerado no primeiro artigo, o segundo foi analisado nos artigos posteriores. Este projeto é o Otimizador Automático.


Como o projeto possui uma GUI, desta vez o MVVM (ModelViewViewModel) é usado novamente. O modelo do projeto é dividido em seções apropriadas. Como a lógica do projeto deve ser implementada na parte do modelo, as classes que não estão relacionadas à parte gráfica do projeto estão localizadas no diretório subdiretório Model e são divididos em diretórios.

Vamos começar com os objetos da série de artigos anteriores que foram modificados. Esta descrição também será útil para todos aqueles que não estão familiarizados com a parte anterior do artigo. 


Criação da parte gráfica do aplicativo

Vamos para a interface gráfica. Anteriormente, nós consideramos um método para criar um complemento para a MetaTrader 5 na linguagem C# e maneiras de combinar suas funções com um Expert Advisor usando uma DLL e o retorno de chamada da OnTimer. Na implementação atual, o otimizador automático será implementado fora da plataforma. Agora, ele é executado como um gerenciador de otimizações externo e imita o trabalho de um trader que lança as otimizações e processa os resultados. Além disso, evitando otimizações simultâneas em várias plataformas executando no mesmo computador e implementando o otimizador automático como um aplicativo separado, nós podemos acessar absolutamente todos as plataformas que estão instalados no computador, incluindo o computador em que o otimizador está sendo executado. Este computador não pôde ser usado no projeto anterior.

É por isso que o projeto atual não é implementado inteiramente como uma DLL, mas agora ele é dividido em uma DLL e um arquivo executável do projeto otimizador automático.


Como pode ser visto na captura de tela acima, a janela do projeto consiste em um Cabeçalho, um Rodapé e um TabControl com duas guias: Settings e Reports. A parte do cabeçalho e o rodapé da janela não são alterados, independentemente da guia selecionada na parte central, proporcionando fácil acesso a todos os elementos de controle localizados nessas partes.

A parte do cabeçalho da janela é criada pela seguinte marcação XAML:

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

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


O container Grid, que contém todos os elementos de controle disponíveis na área considerada, é dividido em 2 colunas. Os seguintes elementos são adicionados à primeira coluna: nome do parâmetro (Otimização), caixa de combinação com uma lista de otimizações disponíveis, bem como o botão para carregar as otimizações. A segunda coluna contém o nome do parâmetro e uma lista suspensa com os IDs das plataformas disponíveis. 

O contêiner Grid que representa o rodapé da janela gráfica (com o ProgressBar) possui uma estrutura semelhante:

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

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

Ele divide sua área em 2 partes, limitando o tamanho da primeira parte. Como resultado, a maior parte do contêiner é usada pelo ProgressBar. Além disso, a largura do ProgressBar será adaptada caso a largura da janela inteira seja alterada. Todos os três componentes são colocados no contêiner <Window/>, de acordo com as regras de marcação XAML.

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

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


    ...


</Window>

 Este contêiner define as referências ao espaço de nomes (namespace):

Além disso, os seguintes tamanhos de janela são definidos: o tamanho mínimo e o tamanho inicial com o qual a janela será aberta no início do programa. Então, o DataContext é instalado para a interface gráfica, usando o alias mencionado acima para o espaço de nomes que contém o ViewModel. 

A parte central do painel consiste em um elemento TabControl contendo 2 guias. Ele serve como a parte principal, como o "Corpo" do nosso elemento gráfico. A estrutura da guia "Settings" é a seguinte:


Essa guia também é dividida em três partes. A parte superior da guia possui um painel que permite configurar os parâmetros do relatório do otimizador automático a serem salvos. Ele também inclui a seleção do nome do ativo e um botão para atualizar o arquivo *set. A parte do meio da guia "Settings" contém as configurações e opções do otimizador para selecionar os parâmetros de filtragem e classificação durante o processo de otimização automática. A última parte permite definir os parâmetros do Expert Advisor e selecionar as datas de otimização e encaminhamento. Para maior comodidade, o elemento GridSplitter está localizado entre as duas primeiras partes. Ao arrastá-lo, você pode redimensionar essas guias. Isso é especialmente conveniente quando você precisa preencher os parâmetros de otimização para um robô com uma lista longa de entradas.

Vamos ver, detendo o código de marcação da primeira parte da guia "Settings":

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

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

</Grid>

A parte descrita inclui a divisão em duas colunas. A largura da primeira coluna pode ser alterada dinamicamente; a segunda largura da coluna é fixa e é igual a 100 pixels. A primeira coluna inclui todos os controles localizados no painel. Todos eles são fornecidos no WrapPanel, permitindo organizar elementos um após o outro. Primeiro vem os controles responsáveis pela seleção e configuração do otimizador automático. Estes são seguidos por parâmetros relacionados à nomeação da pasta com o relatório de otimizações, bem como pelas formas de geração de relatórios (Rewrite, Append). A última parte é a especificação do nome do ativo, usado para a otimização, e um botão para atualizar o arquivo *set com os parâmetros do robô. A coluna com largura fixa é ocupada pelo botão "Start/Stop" que serve como ponto de partida para iniciar a otimização e pará-la. 

A segunda parte da guia "Settings" é dividida em 2 partes.


A primeira contém a ListView com a lista de parâmetros de configuração do otimizador. Aqui, os nomes e os valores dos parâmetros correspondem aos campos de configuração do otimizador na plataforma. A segunda parte contém a especificação de classificação de dados e coeficientes de filtro. As colunas também têm o elemento GridSplitter que separa as áreas descritas. O código que cria os elementos é simples, portanto, eu não fornecerei aqui. O código completo está anexado abaixo. A parte inferior da guia é totalmente semelhante à parte superior, com a única exceção sendo que a parte correta contendo datas de otimização é dividida em duas partes. O primeiro possui controles para adicionar os dados a uma lista. O segundo é usado para mostrar a lista criada.

O elemento final da interface gráfica é a guia "Results", projetada para exibir o resultado das otimizações, bem como os resultados dos testes do histórico e de forward.  


Como pode ser visto na figura em anexo, a guia possui uma estrutura mais interessante que a anterior. Ela é dividida em duas partes e é separado pelo elemento GridSplitter, devido ao qual as partes podem ser redimensionadas, permitindo um estudo mais detalhado dos resultados da otimização. A parte superior contém os dois elementos do agrupamento TabItem, um aninhado no outro. A guia "Selected pass" na qual os testes de forward e do histórico estão localizados, não é tão interessante quanto a guia "Optimisations", no entanto, nós voltaremos a isso mais tarde.

A parte de baixo da guia contém dois campos separados por uma GridSplitter vertical. A primeira destina-se a especificar as datas e modos para um teste selecionado em uma das tabelas na parte de cima, e a outra mostra um número de variáveis resumidas nas tabelas para facilitar a exibição e a leitura. Ela também apresenta a lista de parâmetros do passe de otimização selecionado (guia "Bot Params").

Comparação de elementos de marcação com seus resultados na guia "Optimisations", que possui a seguinte estrutura:


Da mesma forma que a "Selected pass", essa guia tem o botão "Save to (*csv)" que salva em um arquivo os resultados de todas as otimizações concluídas para a data selecionada. Existem mais dois botões para classificar e filtrar os dados na tabela que mostram os resultados de todas as otimizações. A estrutura dos resultados da tabela é semelhante às tabelas localizadas nas guias "Selected pass.History" e "Selected pass.Forward". A parte da marcação que cria os dados da tabela é mostrada abaixo:

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

A TabItem que contém os filtros dos resultados de otimização e os parâmetros de classificação é totalmente idêntico ao mesmo item na guia "Settings". Embora estejam separados na marcação, o ViewModel é organizado para que as alterações em um deles sejam renderizadas instantaneamente no outro. O mecanismo de renderização de alterações será considerado no próximo artigo.  

Como pode ser visto nesta seção, a marcação da interface gráfica é bem simples. Eu não forneci os efeitos visuais apropriados no programa, porque a principal tarefa era a funcionalidade. Se você deseja embelezar o aplicativo, edite o arquivo App.xaml, que serve como armazenamento centralizado do projeto. 


Classes emprestadas da série de artigos "Gerenciamento de otimização" e suas modificações

Neste projeto, eu usei os objetos criados anteriormente para a série "Gerenciamento de Otimização". Eu não fornecerei uma descrição detalhada de cada um dos objetos, pois eles estão disponíveis nos artigos acima. No entanto, vamos nos aprofundar em alguns deles, especialmente naqueles que foram modificados dentro desses projetos. A lista completa dos objetos emprestados é a seguinte:

Os últimos quatro objetos da lista podem ser considerados como uma API proprietária para trabalhar com a plataforma a partir do código em C#. As alterações descritas nesta parte do artigo foram apenas internas. Em outras palavras, a interface externa para trabalhar com essas classes (métodos e propriedades públicas) permaneceram inalteradas em sua assinatura. Portanto, mesmo se você substituir as implementações anteriores desses objetos no projeto anterior por implementações novas, o projeto será compilado e funcionará. 

O primeiro dos objetos com a estrutura modificada é a classe Config. A classe apresenta uma tabela descrita no manual correspondente a seção da documentação da plataforma. Ela contém todos os campos da tabela em suas propriedades. Ao alterar uma propriedade, você altera o valor de uma chave específica em uma seção específica do arquivo de inicialização da plataforma. Os arquivos de inicialização *.ini representam um formato usado, que é comum. O kernel do SO Windows fornece funções para trabalhar com este formato. Nós importamos dois deles para o nosso código C#. Na implementação anterior dessa classe, os métodos usados foram importados diretamente para a classe Config. Na implementação atual, os métodos são implementados em uma classe separada chamada IniFileManager.

class IniFileManager
{
    private const int SIZE = 1024; //Maximum size (for reading the value from the file)
        
    public static string GetParam(string section, string key, string path)
    {
        //To get the value
        StringBuilder buffer = new StringBuilder(SIZE);

        //Get value to buffer
        if (GetPrivateProfileString(section, key, null, buffer, SIZE, path) == 0)
            ThrowCErrorMeneger("GetPrivateProfileStrin", Marshal.GetLastWin32Error(), path);

        //Return the received value
        return buffer.Length == 0 ? null : buffer.ToString();
    }
    /// <summary>
    /// Return error
    /// </summary>
    /// <param name="methodName">Method name</param>
    /// <param name="er">Error code</param>
    private static void ThrowCErrorMeneger(string methodName, int er, string path)
    {
        if (er > 0)
        {
            if (er == 2)
            {
                if (!File.Exists(path))
                    throw new Exception($"{path} - File doesn1t exist");
            }
            else
            {
                throw new Exception($"{methodName} error {er} " +
                    $"See System Error Codes (https://docs.microsoft.com/en-us/windows/desktop/Debug/system-error-codes) for details");
            }
        }
    }

    public static void WriteParam(string section, string key, string value, string path)
    {
        //Write value to the INI-file
        if (WritePrivateProfileString(section, key, value, path) == 0)
            ThrowCErrorMeneger("WritePrivateProfileString", Marshal.GetLastWin32Error(), path);
    }
}

O arquivo resultante Config apresenta apenas os campos contidos no arquivo de configuração. A descrição completa desse objeto foi fornecida em artigos anteriores relacionados ao gerenciamento de otimização.  

A próxima classe modificada é a TerminalManager. O conteúdo da classe permanece inalterado. Nós não consideraremos o método e os componentes de operação da classe, pois ela é uma classe emprestada. No entanto, a classe desempenha um papel importante no aplicativo ao iniciar e parar a operação da plataforma. Então, aqui está o código completo da implementação da classe.   

class TerminalManager
{
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="TerminalChangeableDirectory">
    /// Path to the directory with mutable files (the one in AppData)
    /// </param>
    public TerminalManager(DirectoryInfo TerminalChangeableDirectory) :
        this(TerminalChangeableDirectory, new DirectoryInfo(File.ReadAllText(TerminalChangeableDirectory.GetFiles().First(x => x.Name == "origin.txt").FullName)), false)
    {
    }
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="TerminalChangeableDirectory">
    /// Path to the directory with mutable files
    /// </param>
    /// <param name="TerminalInstallationDirectory">
    /// Path to the terminal folder
    /// </param>
    public TerminalManager(DirectoryInfo TerminalChangeableDirectory, DirectoryInfo TerminalInstallationDirectory, bool isPortable)
    {
        this.TerminalInstallationDirectory = TerminalInstallationDirectory;
        this.TerminalChangeableDirectory = TerminalChangeableDirectory;

        TerminalID = TerminalChangeableDirectory.Name;

        CheckDirectories();

        Process.Exited += Process_Exited;

        Portable = isPortable;
    }
    /// <summary>
    /// Destructor
    /// </summary>
    ~TerminalManager()
    {
        Close();
        Process.Exited -= Process_Exited;
    }
    /// <summary>
    /// Terminal startup process
    /// </summary>
    private readonly System.Diagnostics.Process Process = new System.Diagnostics.Process();
    /// <summary>
    /// Running process completion event
    /// </summary>
    public event Action<TerminalManager> TerminalClosed;

    #region Terminal start Arguments
    /// <summary>
    /// Login for start - flag /Login
    /// </summary>
    public uint? Login { get; set; } = null;
    /// <summary>
    /// Platform launch under a certain profile. 
    /// The profile must be created in advance and located in the /profiles/charts/ folder of the trading platform
    /// </summary>
    public string Profile { get; set; } = null;
    /// <summary>
    /// Config file as a /Config object
    /// </summary>
    public Config Config { get; set; } = null;
    /// <summary>
    /// Flag of terminal launch in /portable mode
    /// </summary>
    private bool _portable;
    public bool Portable
    {
        get => _portable;
        set
        {
            _portable = value;
            if (value && !TerminalInstallationDirectory.GetDirectories().Any(x => x.Name == "MQL5"))
            {
                WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized;

                if (Run())
                {
                    System.Threading.Thread.Sleep(1000);
                    Close();
                }
                WaitForStop();
                WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
            }
        }
    }
    /// <summary>
    /// window style of the launched process
    /// </summary>
    public System.Diagnostics.ProcessWindowStyle WindowStyle { get; set; } = System.Diagnostics.ProcessWindowStyle.Normal;
    #endregion

    #region Terminal directories
    /// <summary>
    /// Path to terminal installation folder
    /// </summary>
    public DirectoryInfo TerminalInstallationDirectory { get; }
    /// <summary>
    /// Path to terminal folder with variable files
    /// </summary>
    public DirectoryInfo TerminalChangeableDirectory { get; }
    /// <summary>
    /// Path to the MQL5 folder
    /// </summary>
    public DirectoryInfo MQL5Directory => (Portable ? TerminalInstallationDirectory : TerminalChangeableDirectory).GetDirectory("MQL5");
    #endregion

    /// <summary>
    /// Terminal ID folder name in AppData directory
    /// </summary>
    public string TerminalID { get; }
    /// <summary>
    /// Flag of whether the terminal is currently running or not
    /// </summary>
    public bool IsActive => Process.StartInfo.FileName != "" && !Process.HasExited;

    #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

    /// <summary>
    /// Terminal launch
    /// </summary>
    public bool Run()
    {
        if (IsActive)
            return false;
        // Set path to the terminal
        Process.StartInfo.FileName = Path.Combine(TerminalInstallationDirectory.FullName, "terminal64.exe");
        Process.StartInfo.WindowStyle = WindowStyle;
        // Set data for terminal launch (if any data were set)
        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";

        // Notify the process of the need to call an Exit event after closing the terminal
        Process.EnableRaisingEvents = true;

        // Run the process and save the launch status to the IsActive variable
        return Process.Start();
    }
    /// <summary>
    /// Wait for the terminal operation to complete
    /// </summary>
    public void WaitForStop()
    {
        if (IsActive)
            Process.WaitForExit();
    }
    /// <summary>
    /// Stop the process
    /// </summary>
    public void Close()
    {
        if (IsActive)
            Process.Kill();
    }
    /// <summary>
    /// Wait for the terminal operation to complete for a certain time
    /// </summary>
    public bool WaitForStop(int miliseconds)
    {
        if (IsActive)
            return Process.WaitForExit(miliseconds);
        return true;
    }
    /// <summary>
    /// Search for files with the Ex5 extension 
    /// Search is performed recursively - files are searched in the specified folder and in all subfolders
    /// </summary>
    /// <param name="path">Path to the folder where search begins</param>
    /// <param name="RelativeDirectory">Folder relative to which oath is returned</param>
    /// <returns>List of paths to the found files</returns>
    private List<string> GetEX5FilesR(DirectoryInfo path, string RelativeDirectory = null)
    {
        if (RelativeDirectory == null)
            RelativeDirectory = path.Name;
        string GetRelevantPath(string pathToFile)
        {
            string[] path_parts = pathToFile.Split('\\');
            int i = path_parts.ToList().IndexOf(RelativeDirectory) + 1;
            string ans = path_parts[i];

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

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

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

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

        return files;
    }
    /// <summary>
    /// Terminal closing event
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Process_Exited(object sender, EventArgs e)
    {
       TerminalClosed?.Invoke(this);
    }
    /// <summary>
    /// Check the correctness of the passed terminal path
    /// </summary>
    private void CheckDirectories()
    {
        if (!TerminalInstallationDirectory.Exists)
            throw new ArgumentException("PathToTerminalInstallationDirectory doesn`t exists");
        if (!TerminalChangeableDirectory.Exists)
            throw new ArgumentException("PathToTerminalChangeableDirectory doesn`t exists");
        if (!TerminalInstallationDirectory.GetFiles().Any(x => x.Name == "terminal64.exe"))
            throw new ArgumentException($"Can`t find terminal (terminal64.exe) in the instalation folder {TerminalInstallationDirectory.FullName}");
    }
}

Agora, a classe não implementa a interface ITerminalManager (como foi na última vez). Eu decidi não usar testes unitários ao implementar o aplicativo descrito, em um esforço para acelerar o processo de desenvolvimento e minimizar o número de projetos. Como resultado, nenhuma interface é necessária para este objeto.

A próxima modificação refere-se a uma nova maneira de determinar se a plataforma está funcionando ou não. Na versão anterior, a propriedade recebeu um valor dos métodos Run (nos quais o valor false foi atribuído) e do retorno da conclusão da otimização. No entanto, ela não foi uma decisão muito boa, podendo não funcionar às vezes. Portanto, eu revisei a propriedade getter IsActive. Agora, o getter acessa diretamente a propriedade HasExited do objeto Process. No entanto, uma tentativa de acessar a propriedade antes de sua primeira execução gerará uma mensagem de erro. Eu estudei detalhes da classe Process e observei que quando você inicia o processo por meio do objeto descrito, sua propriedade StartInfo.FileName é preenchida com um caminho para o arquivo executável. Antes da primeira execução, ela é igual ao valor vazio (""). É por isso que o getter IsActive parece estar estranho. Primeiro, o getter verifica se o nome existe e, em seguida, ele verifica a propriedade Process.HasExited. Em outras palavras, nós assumimos por padrão que a plataforma está fechada e só pode ser iniciada através da nossa classe TerminalManager. Portanto, se StartInfo.FileName == "", retornar false (ela significa que a plataforma não está sendo executada). Se a plataforma já foi iniciada, comparamos o valor da propriedade HasExited. O valor da propriedade muda toda vez que a plataforma é iniciada, se ela é iniciada a partir do nosso objeto e quando ela é desligada. Devido a esse recurso, você deve sempre fechar a plataforma ao usar o otimizador automático. 

A descrição é concluída pelo último objeto com uma estrutura interna modificada. Esta é a classe SetFileManager e o seu método UpdateParams.

/// <summary>
/// Clear all recorded data in Params and load data from the required file
/// </summary>
public virtual void UpdateParams()
{
    _params.Clear();

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

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

                _params.Add(item);
            }
        }
    }
}

Alterações nesta classe se refere apenas a um método e, portanto, não fornecerei o código completo da classe aqui. Eu descobri durante o teste do aplicativo que, às vezes, o arquivo *.set com os parâmetros do robô gerados pela plataforma e que executam no otimizador pode estar meio vazio para alguns dos parâmetros. Por exemplo, a plataforma pode preencher o campo Value e falhar ao preencher o valor inicial ou final da otimização. Isso depende do tipo do parâmetro. Por exemplo, os parâmetros de string preenchem apenas os campos Value. O propósito de implementar as alterações no código abaixo foram para evitar o problema acima.


A estrutura de diretório de dados

Nos artigos anteriores, nós já mencionamos o diretório local "Data" que armazena os relatórios de otimização e outros arquivos de trabalho criados pelo otimizador automático. Agora, está na hora de considerar o diretório com mais detalhes. O diretório Data é criado próximo do arquivo executável durante o início da plataforma. O diretório é criado apenas se ele não existir no momento de inicialização do otimizador automático. Caso contrário, o seu caminho será salvo na propriedade apropriada da classe abaixo. O diretório serve como uma pasta de trabalho e armazenamento ao mesmo tempo. Se você precisar acessar os arquivos e armazená-los, faça-o dentro deste diretório. O objeto a seguir cria e armazena o diretório:

/// <summary>
/// The object describing the Data directory with the auto optimizer's mutable files.
/// </summary>
class WorkingDirectory
{
    /// <summary>
    /// Default constructor
    /// </summary>
    public WorkingDirectory()
    {
        // Create a root directory with mutable files
        WDRoot = new DirectoryInfo("Data");
        if (!WDRoot.Exists)
            WDRoot.Create();
        // Create a subdirectory with optimization reports
        Reports = WDRoot.GetDirectory("Reports", true);
    }
    /// <summary>
    /// Nested directory with optimization reports
    /// </summary>
    public DirectoryInfo Reports { get; }
    /// <summary>
    /// Root directory with mutable files and folders
    /// </summary>
    public DirectoryInfo WDRoot { get; }

    /// <summary>
    /// Get or create (if not previously created) a directory nested inside the Reports directory.
    /// The resulting directory stores the results of a particular optimization pass.
    /// </summary>
    /// <param name="Symbol">The symbol on which the optimization was performed</param>
    /// <param name="ExpertName">Robot name</param>
    /// <param name="DirectoryPrefix">Prefix added to the directory name</param>
    /// <param name="OptimiserName">The name of the use optimizer</param>
    /// <returns>
    /// Path to the directory with the optimization results.
    /// The name of the directory is formed as follows: public DirectoryInfo WDRoot { get; }
    /// {DirectoryPrefix} {OptimiserName} {ExpertName} {Symbol}
    /// </returns>
    public DirectoryInfo GetOptimisationDirectory(string Symbol, string ExpertName,
                                                  string DirectoryPrefix, string OptimiserName)
    {
        return Reports.GetDirectory($"{DirectoryPrefix} {OptimiserName} {ExpertName} {Symbol}", true);
    }

    /// <summary>
    /// Path to Data/Tester 
    /// Needed to temporarily move files from the terminal directory of the same name
    /// </summary>
    public DirectoryInfo Tester => WDRoot.GetDirectory("Tester", true);

}

A classe serve como um gerenciador para o diretório descrito. Ela é muito conveniente, porque não importa onde esteja o executável do otimizador automático, nós sempre podemos obter o caminho correto para o diretório desejado acessando a propriedade WDRoot deste objeto. Nesse construtor, nós criamos o diretório Data se ele ainda não existir. De outra forma, nós salvamos o seu endereço na propriedade acima. Além disso, salvamos o caminho no subdiretório "Reports". O parâmetro booleano igual a true passado, indica que, se o diretório não existir, ele deverá ser criado. 


Como resultado, o diretório Data é criado imediatamente após a primeira execução. Após a criação, o diretório possui apenas um subdiretório "Reports" que está vazio. Durante o primeiro lançamento de uma otimização ou teste, o subdiretório Tester é criado por uma chamada para a propriedade apropriada do objeto descrito. O arquivo de configuração {ID da Plataforma}.ini é criado copiando o arquivo de configuração que você selecionou por padrão. Portanto, você evita a substituição do arquivo de configuração de origem. O diretório do Tester é criado para a cópia temporária do cache das otimizações executadas anteriormente. Ele se assemelha parcialmente ao diretório Tester disponível entre os diretórios das plataformas mutáveis.

O diretório contém apenas a pasta "cache". Todos os arquivos do mesmo diretório da plataforma selecionado são movidos para esta pasta de cache. Após o final do processo de otimização, os arquivos são retornados ao local anterior. Esta operação garante a execução do processo de otimização. De acordo com a lógica do otimizador, se o diretório da plataforma tiver arquivos que descrevem o processo de otimização, o otimizador carregará as otimizações executadas anteriormente em vez de iniciar um novo processo. Esta é uma ótima solução que economiza muito tempo. Mas ela é completamente inadequada para nossos propósitos. Como armazenamos nossa própria cópia do relatório de otimizações ajustado pelo otimizador automático (artigos 3 e 1 da série atual), nós precisamos criar um relatório. Para gerar um relatório, nós precisamos iniciar o processo de otimização. É por isso que emulamos a ausência desses arquivos. Para fazer isso, nós movemos temporariamente esses arquivos para o diretório local. Após a conclusão bem-sucedida do processo de otimização, um subdiretório é criado no diretório Reports usando o método GetOptimisationDirectory.

 

Na captura de tela acima, destacamos a cor que mostra o prefixo do diretório especificado nas configurações do otimizador automático antes do início da otimização. Ela permite distinguir entre diferentes otimizações do mesmo Expert Advisor. Cada diretório armazena três arquivos com os resultados das otimizações executadas:

Os arquivos têm uma estrutura semelhante, descrita no primeiro artigo desta série. Quando você clicar no botão Load na GUI, o otimizador automático carregará todos os três arquivos do diretório selecionado nas tabelas apropriadas. Se algum dos três arquivos não for encontrado ou todos os arquivos não existirem, uma mensagem apropriada será gerada. As tabelas correspondentes aos arquivos ausentes são exibidas em branco. 

Se você precisar mover os resultados de otimização do otimizador automático localizado em um computador para outro computador, basta copiar o diretório Reports e movê-lo para o diretório relevante no segundo computador. Após o lançamento, o otimizador automático acessará os diretórios desejados com os resultados. Assim, os resultados estarão disponíveis para download e posterior análise.

Conclusão

Nos primeiros artigos desta série, nós examinamos a criação e o upload de relatórios de otimização. Em seguida, nós passamos a considerar o projeto do otimizador automático. No artigo anterior, nós analisamos o projeto pronto. A ideia era apresentar o objetivo final desta série. Além disso, o artigo anterior fornece instruções para o uso do otimizador automático finalizado. Neste artigo, nós consideramos o aspecto técnico na implementação do otimizador automático. Antes de prosseguirmos com a análise da parte lógica do projeto, nós consideramos a interface gráfica e as modificações nos arquivos emprestados da série anterior de artigos. Os links para a série anterior de artigos são fornecidos na introdução deste artigo. No próximo artigo, nós consideraremos a implementação da parte lógica do programa.

O anexo contém o projeto do otimizador automático com um robô de negociação analisado no artigo 4. Para usar o projeto, compile o arquivo do projeto otimizador automático e o arquivo do robô de teste. Em seguida, copie o ReportManager.dll (descrito no primeiro artigo) para o diretório MQL5/Libraries e você poderá começar a testar o EA. Consulte os artigos 3 e 4 desta série para obter os detalhes sobre como conectar o otimizador automático aos seus Expert Advisors.