English Русский 中文 Español Deutsch 日本語
preview
Otimização Walk Forward contínua (parte 6): Lógica e estrutura do otimizador automático

Otimização Walk Forward contínua (parte 6): Lógica e estrutura do otimizador automático

MetaTrader 5Testador | 4 agosto 2020, 07:50
1 553 1
Andrey Azatskiy
Andrey Azatskiy

Introdução

Nós continuamos a descrever a criação de um otimizador automático implementando a otimização walk forward. No artigo anterior, nós analisamos a interface gráfica do aplicativo resultante, no entanto, nós não consideramos sua parte lógica e estrutura interna. Isso é o que será descrito neste artigo. Os artigos anteriores desta série:

  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)
  5. Otimização Walk Forward Contínua (parte 5): Panorama do Projeto Otimizador Automático e Criação da Interface Gráfica

Nós usaremos os diagramas UML para descrever a estrutura interna do aplicativo e as chamadas realizadas pelo aplicativo durante a operação. Observe que o objetivo dos diagramas é fornecer uma apresentação esquemática dos principais objetos e relacionamentos entre eles, mas não descrever todos os seus objetos existentes.

Estrutura interna da aplicação, sua descrição e geração de objetos-chave

Conforme mencionado no artigo anterior, o padrão principal usado no programa resultante é o MVVM. De acordo com esse padrão, toda a lógica do programa é implementada na classe de modelo de dados, que é conectada aos gráficos por meio de uma classe separada que implementa a função de objeto ViewModel. A lógica do programa é dividida ainda mais entre um número de classes que são entidades elementares. As principais entidades do programa que descrevem sua lógica e o relacionamento entre o login e a interface do usuário são mostradas no diagrama UML das classes abaixo.    


Antes de considerar o diagrama, vamos ver a indicação de cores usada para os diferentes tipos de objetos. A cor azul é usada para a camada gráfica. Esses são os objetos que representam a marcação XAML com todos os mecanismos WPF ocultos, que não são visíveis para o usuário final nem para o desenvolvedor. A cor roxa é usada para a camada que conecta os gráficos do aplicativo com sua lógica. Em outras palavras, é a camada ViewModel do modelo MVVM. A cor rosa é usada para mostrar as interfaces que são representações abstratas dos dados ocultos por trás deles.

O primeiro delas (IMainModel) oculta uma implementação específica do modelo de dados. De acordo com a ideias principal do padrão MVVM, o modelo de dados deve ser o mais independente possível, enquanto o ViewModel não deve depender da implementação concreta desse modelo. O segundo (IOptimiser) é uma interface da lógica de otimização, pois de acordo com uma das ideiass do programa, pode haver várias otimizações executando e selecionando lógicas, e o usuário pode alterá-la selecionando o otimizador apropriado na caixa de combinação.

A cor marrom é usada para a camada que representa o modelo de dados em interfaces gráficas. Como você pode ver, existem dois modelos de dados no diagrama: o primeiro se refere ao próprio otimizador automático e o segundo à interface gráfica do otimizador. A cor amarela é usada para o único gerenciador de otimização que existe no momento. No entanto, pode haver vários gerenciadores de otimização. Você também pode implementar sua própria lógica de otimização (o método de implementação do mecanismo será considerado em outros artigos). A cor verde é usada para os objetos auxiliares que servem como fábricas e que implementam a criação de objetos necessários no momento atual.

Além disso, vamos considerar os relacionamentos entre os objetos e o processo de sua criação durante o lançamento do aplicativo. Antes disso, nós precisamos considerar a camada gráfica e seus componentes:

  • AutoOptimiser (janela principal),
  • AutoOptimiserVM (view model),
  • IMainModel (interface do modelo),
  • MainModel (Model),
  • MainModelCreator (static factory para criar o modelo de dados).


Estes são os cinco primeiros objetos mostrados no diagrama. A classe AutoOptimiser é instanciada primeiro durante o lançamento do aplicativo. Esta classe cria uma interface gráfica. A marcação XAML da interface gráfica contém uma referência ao objeto AutoOptimiserVM, que atua como ViewModel. Portanto, durante a criação da camada gráfica, a classe AutoOptimiserVM também é criada, enquanto a camada gráfica a possui completamente. Este objeto existe até ele ser destruído após a destruição da interface gráfica. Ele é conectado à classe AutoOptimiser (nossa janela) via "Composição", o que implica a propriedade e o controle completo do objeto.  

A classe ViewModel deve ter acesso à classe Model, mas a classe do modelo de dados deve permanecer independente do ViewModel. Em outras palavras, não é necessário saber qual classe fornece o modelo de dados. Em vez disso, a classe ViewModel está ciente da interface do modelo que contém um conjunto de métodos públicos, eventos e propriedades que nosso mediador pode usar. É por isso que essa classe não está diretamente conectada à classe MainModel, mas à sua interface por meio da relação de "Agregação", segundo a qual a classe analisada pertence à classe que a utiliza.

No entanto, uma das diferenças entre a Agregação e Composição é que a classe analisada pode pertencer a mais de um objeto por vez e seu processo de vida útil não é controlado por objetos de contêiner. Essa declaração é completamente verdadeira para a classe MainModel porque ela é criada em seu construtor estático (a classe MainModelCreator) e armazenada nela e na classe AutoOptimiserVM ao mesmo tempo. O objeto é destruído quando o aplicativo conclui o seu trabalho. Isso ocorre porque ele foi implementado originalmente em uma propriedade estática, que é limpa somente quando o aplicativo é concluído.   

Nós consideramos o relacionamento entre os três objetos principais: Model - View - ViewModel. O restante do diagrama é dedicado à principal lógica de negócio do nosso aplicativo. Ele apresenta o relacionamento dos objetos responsáveis pelo processo de otimização com o objeto do modelo de dados. Os objetos responsáveis pelo processo de controle de otimização servem como um tipo de controlador que inicia os processos necessários e delega sua execução a objetos do programa individuais. Um deles é o otimizador. O otimizador também é um gerente que delega a execução de tarefas para objetos orientados a tarefas, como inicialização do terminal ou geração do arquivo de configuração necessário para a inicialização da plataforma. 


Durante a instanciação da classe MainModel, também instanciamos a classe do otimizador usando o mecanismo já familiar de construtores estáticos. Como pode ser visto no diagrama, a classe do otimizador deve implementar a interface IOptimiser e deve ter uma classe de construtor derivada do OptimiserCreator - ela criará uma instância específica do otimizador. Isso é necessário para a implementação da substituição dinâmica de otimizadores no modo de execução do programa.

Cada um dos otimizadores pode ter uma lógica de otimização individual. A lógica do otimizador atual e a implementação dos otimizadores serão consideradas em detalhes em artigos futuros. Agora, vamos voltar para a arquitetura. A classe do modelo de dados é conectada à classe base de todos os construtores do modelo por meio da relação de associação, o que significa que a classe do modelo de dados usa os construtores de otimizadores convertidos em sua classe base para criar uma determinada instância do otimizador.

O otimizador criado é convertido em seu tipo de interface e é salvo no campo apropriado da classe MainModel. Assim, usando a abstração durante a criação de objetos (construtores de objetos) e a criação de instâncias (otimizadores), nós fornecemos a possibilidade de substituição dinâmica de otimizadores durante o processo de execução do programa. A abordagem usada é chamada de "Abstract Factory". Sua ideia é que tanto o produto (a classe que implementa a lógica de otimização) quanto suas factories (classes que criam o produto) tenham sua própria abstração. A classe do usuário não precisa saber sobre a implementação em específico da lógica de ambos os componentes, mas ela deve poder usar suas diferentes implementações.

Como exemplo da vida real, nós podemos usar água com gás, chá, café ou produtos similares, bem como as fábricas que as produzem. Uma pessoa não precisa conhecer a metodologia específica de produção das bebidas para beber. Além disso, a pessoa não precisa conhecer uma certa estrutura interna das fábricas que produzem as bebidas ou da loja onde são vendidas. Neste exemplo:

  • Uma pessoa é um usuário,
  • As lojas ou fábricas das quais as bebidas são oferecidas são as fábricas (factories),
  • As bebidas são o produto. 
Em nosso programa, o usuário é a classe MainModel.


Se você observar a implementação padrão do otimizador, você verá que ela também possui uma interface gráfica com as configurações (que é chamado de "por um clique" no botão "GUI" ao lado da caixa de combinação, onde todos os otimizadores são enumerados). No diagrama de classes (e no código), a parte gráfica das configurações do otimizador é chamada de "SimpleOptimiserSettings", enquanto a ViewModel e a View são chamadas de "SimpleOptimiserVM" e "SimpleOptimiserM", respectivamente. Como pode ser visto no diagrama de classes, a ViewModel das configurações do otimizador pertence totalmente à parte gráfica e, portanto, ela é conectada através da relação "Composição". A parte da View é controlada pelo otimizador e é conectada à classe Manager por meio da relação "Composition". Parte do modelo de dados das configurações do otimizador pertence ao otimizador e ao ViewModel, é por isso que ele tem uma relação de "Agregação" com os dois. Isso é feito intencionalmente para permitir que o otimizador acesse as configurações armazenadas no modelo de dados gráficos das configurações do otimizador.      

Para concluir o capítulo, eu forneci aqui um diagrama de sequência que mostra o processo de instanciação dos objetos considerados acima.


O diagrama deve ser lido de cima para baixo. O ponto de partida do processo exibido é Instância que mostra o momento de início do aplicativo com a instanciação da camada gráfica da janela principal do otimizador. Durante a instanciação, a interface gráfica instancia a classe SimpleOptimiserVM porque ela é declarada como DataContext da janela principal. Durante a instanciação, a SimpleOptimiserVM chama a propriedade estática MainModelCreator.Model, que por sua vez gera o objeto MainModel e converte-o para o tipo de interface IMainModel.

Na hora da instanciação da classe MainModel, é criado uma lista de construtores de otimizadores. Isto é a lista mostrada na caixa de combinação, permitindo selecionar o otimizador desejado. Após a instanciação do modelo de dados, o construtor da classe SimpleOptimiserVM é chamado, que chama o método ChangeOptimiser do modelo de dados apresentado pelo tipo de interface IMainModel. O método ChangeOptimiser chama o método Create() no construtor selecionado de otimizadores. Como nós estamos visualizando o início do aplicativo, o construtor do otimizador selecionado será o primeiro da lista especificada. Ao chamar o método Create no construtor do otimizador desejado, nós delegamos ao construtor a criação do tipo específico do otimizador. Ele cria o otimizador, retorna o objeto do otimizador convertido no tipo de interface passando-o para o modelo de dados, onde ele é salvo na propriedade apropriada. Depois disso, a operação do método ChangeOptimiser é concluída e nós podemos voltar ao construtor da classe SimpleOptimiserVM.

A classe Model e a parte lógica do programa

Nós consideramos a estrutura geral do aplicativo resultante e o processo de criação dos principais objetos no momento do lançamento do aplicativo. Agora, vamos considerar os detalhes de sua implementação lógica. Todos os objetos que descrevem a lógica do aplicativo criado estão localizados no diretório "Model". A raiz do diretório possui o arquivo "MainModel.cs", que contém a classe do modelo de dados, que é o ponto de partida para iniciar toda a lógica de negócios do aplicativo. Sua implementação contém mais de 1000 linhas de código, portanto, eu não fornecerei todo o código da classe aqui, mas apenas as implementações dos métodos individuais. A classe é herdada da interface IMainModel. Aqui está o código da interface demonstrando sua estrutura.

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

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

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

Os componentes da interface são delimitados pelas diretivas #region. Assim, os membros da interface são divididos em componentes típicos. Como você pode ver, ele possui várias propriedades que fornecem várias informações, desde os campos regulados pelo modelo de dados até a interface gráfica. No entanto, esses são apenas getters que restringem o acesso aos dados, permitindo apenas lê-los, sem a capacidade de substituir o objeto que está sendo lido. Isso é feito para evitar danos acidentais do modelo de dados e a lógica do ViewModel. Uma das coisas interessantes sobre as propriedades da interface são as listas de resultados de otimização:

  • AllOptimisationResults
  • ForwardOptimisations
  • HistoryOptimisations

Esses campos contêm a lista de otimizações que são mostradas nas tabelas na guia Results da GUI externa. A lista de todas os passes de otimização está contida em uma estrutura especialmente criada "ReportData":

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

Além dos dados de otimização, a estrutura descreve as principais configurações do otimizador necessárias para o início dos testes (com um clique duplo nos passes de otimização selecionado) e para comparar os resultados da otimização ao adicionar novos dados aos que foram otimizados anteriormente.

Além disso, o modelo de dados contém a lista das plataformas instaladas no computador, nomes dos otimizadores disponíveis para seleção (criada pelos construtores desses otimizadores) e a lista de otimizações salvas anteriormente (nomes dos diretórios localizados em "Data/Reports"). O acesso ao otimizador em si também é fornecido.

A troca reversa de informações (do modelo para o modelo View) é realizada usando os eventos que o ViewModel assina após instanciar o modelo de dados. Existem 4 desses eventos, 3 dos quais são personalizados e um é herdado da interface INotifyPropertyChanged. Não é necessário que o modelo de dados herde da interface INotifyPropertyChanged. Mas parece que é conveniente para mim, por isso que a herança é usada neste programa.

Um dos eventos é o ThrowException. Inicialmente, ele foi criado para enviar uma mensagem de erro para a parte gráfica do aplicativo para depois exibi-la, porque você não deve controlar os gráficos diretamente do modelo de dados. No entanto, agora o evento também é usado para passar várias mensagens de texto para os gráficos do modelo de dados. Estes não são erros, mas são alertas de texto. Portanto, lembre-se de que o evento passará mais mensagens que não são erros. 

Para considerar os métodos do modelo de dados, vamos ver a classe que implementa esta parte do programa. 

A primeira coisa que o otimizador faz quando um novo robô é selecionado é carregar os seus parâmetros. Isso é feito pelo método "GetBotParams", que implementa as duas lógicas possíveis. Ele pode atualizar o arquivo de configuração com os parâmetros do robô e fazer a sua leitura. Ele também pode ser recursivo. 

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

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


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

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

            Config config = new Config(iniFile.FullName);

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

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

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

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

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

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

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

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

No início do método, nós criamos uma representação orientada a objeto do arquivo com os parâmetros do robô usando a classe FileInfo, disponível na biblioteca padrão da C#. De acordo com as configurações padrão da plataforma, o arquivo é salvo no diretório MQL5/Profiles/Tester/{nome do robô selecionado}.set. Este é o caminho que foi definido no momento da criação de uma representação de arquivo orientado a objetos. Ações adicionais são agrupadas na construção try-catch porque existe o risco de ocorrer um erro durante as operações de arquivo. Agora, uma das possíveis ramificações lógicas é executada dependendo do parâmetro isUpdate que foi passado. Se isUpdate = true, nós devemos atualizar o arquivo com as configurações na qual os seus valores são redefinidos para o padrão e, em seguida, ler os seus parâmetros. Este ramo lógico é executado quando nós clicamos em "Update (*.set) file" na parte gráfica do aplicativo. A maneira mais conveniente de atualizar o arquivo com as configurações avançadas é gerá-lo novamente.

O arquivo é gerado pelo testador de estratégia se ele não existir quando um robô for selecionado no testador. Portanto, tudo o que nós precisamos fazer é reiniciar o testador após excluir o arquivo, aguardar até que o arquivo seja gerado, fechar o testador e retornar o seu valor padrão. Primeiro vamos verificar se o terminal está funcionando. Se ele estiver em execução, exibimos a mensagem correspondente e aguardamos a sua conclusão. Então verificamos se o arquivo com os parâmetros existe. Se houver esse arquivo, nós o removemos.

Então, preenchemos o arquivo de configuração para o lançamento da plataforma, usando a Config já familiar, considerada nos artigos anteriores. Prestar atenção nas datas gravadas no arquivo de configuração. Nós iniciamos o teste na plataforma, mas a data de início do teste é especificada como 1 dia antes da data da plataforma. Por esse motivo, o testador inicia e gera um arquivo com as configurações necessárias. Então, ele falha ao iniciar o teste e encerra a sua operação, passando para a leitura do arquivo. Depois que o arquivo de configuração for criado e preparado, a classe TerminalManager é usada para iniciar o processo de geração de arquivos de configurações (o processo foi considerado anteriormente). Depois que a geração do arquivo é concluída, nós usamos a classe SetFileManager para ler o arquivo com as configurações e retornar o seu conteúdo.

Se outra ramificação lógica for necessária, que não seja necessário a geração explícita de um arquivo de configurações, recorremos a segunda parte da condição. O método lê o arquivo com as configurações do EA e retorna o seu conteúdo, ou o método é iniciado recursivamente com o parâmetro isUpdate = true e, portanto, a parte lógica considerada anteriormente é executada.

Outro método interessante é o "StartOptimisation":

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

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

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

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

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

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

Esse método é assíncrono e ele foi escrito usando a tecnologia async await, que fornece uma declaração mais simples dos métodos assíncronos. Primeiramente, nós verificamos o nome do símbolo passado e os intervalos de otimização. Se algum deles estiver faltando, nós desbloqueamos a GUI bloqueada (alguns dos botões da GUI são bloqueados quando a otimização é iniciada) e exibimos uma mensagem de erro, após a qual a execução da função deve ser concluída. Faça exatamente o mesmo se a plataforma já estiver em execução. Se um modo de teste foi selecionado em vez da otimização, redirecionamos a execução do processo para o método que inicia o teste.

E se o Append mode estiver selecionado, excluímos todos os arquivos no diretório com as otimizações, bem como todos os subdiretórios. Em seguida, nós prosseguimos para a execução da otimização. O processo de otimização é iniciado de forma assíncrona e, portanto, ele não bloqueia a GUI enquanto esta tarefa estiver sendo executada. Ela também é envolvida em uma construção try-catch em caso de erros. Antes do início do processo, nós copiamos todos os arquivos da cache das otimizações executadas anteriormente em um diretório temporário criado no diretório Data do otimizador automático. Isso garante que as otimizações sejam iniciadas, mesmo que tenham sido lançadas anteriormente. Em seguida, limpamos o otimizador se todos os dados foram gravados anteriormente em variáveis locais do otimizador e iniciamos o processo de otimização. Um dos parâmetros de inicialização da otimização é o caminho para o arquivo de relatório gerado pelo robô. Conforme mencionado anteriormente no artigo 3, o relatório é gerado com o nome {nome do robô}_Report.xml. No otimizador automático, esse nome é especificado pela seguinte linha:

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

Isso é feito por concatenação de strings, onde o nome do robô é formado pelo caminho do robô especificado como um dos parâmetros do arquivo de otimização. O processo de encerramento da otimização foi transferido completamente para a classe do otimizador. O método que o implementa simplesmente chama o método StopOptimisation em uma instância da classe do otimizador.

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

Os testes são iniciados usando o método implementado na classe do modelo de dados, não no otimizador.

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

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

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

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

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

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

    PBUpdate("Start test", 100);

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

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

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

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

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

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

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

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

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

        OnPropertyChanged("StopTest");
    });
}

Após a verificação se a plataforma está em execução, prosseguimos para definir as datas dos testes do histórico e de forward. Você pode definir um intervalo do histórico ou um intervalo do histórico e de forward. Se apenas um intervalo de forward for especificado nas configurações, ele será tratado como o histórico. Antes de tudo, nós declaramos as variáveis que armazenam as datas do teste (forward, última data do teste, data de início do teste). Em seguida, verificamos o método — se for passado mais de um limite do intervalo do histórico ou mais de um limite do teste de forward, é exibido uma mensagem de erro. Então, definimos os limites — a ideias dessa condição é definir as quatro datas passadas (ou duas se apenas o período do histórico for definido) entre as três variáveis declaradas.

  • FromDate — é igual a menor data passada
  • ToDate — é igual a maior data passada
  • Forward — é igual a menor da data de forward

O início do teste também é envolvido em uma construção try-catch. Primeiramente, um arquivo com os parâmetros do robô é gerado e é preenchido com os parâmetros passados do robô. Isso é feito usando o objeto SetFileManager, que foi considerado anteriormente. Em seguida, um arquivo de configuração é criado de acordo com a instrução, e o processo de teste é iniciado. Depois, aguardamos a plataforma se encerrar. Depois que a operação do método estiver concluída, notificamos os gráficos de que o teste está completo. Isso deve ser feito por meio de um evento, porque esse método é assíncrono e a operação do programa continua após sua chamada, sem aguardar a conclusão do método chamado.

Quanto ao processo de otimização, o otimizador também notifica o modelo de dados sobre o processo de otimização finalizado através do evento de conclusão do processo de otimização. Isso será considerado com mais detalhes no artigo final.

Conclusão

Nos artigos anteriores, nós analisamos em detalhes o processo de combinação de algoritmos com o otimizador automático, que foi criado e algumas de suas partes. Nós já consideramos a lógica dos relatórios de otimização e vimos sua aplicação nos algoritmos de negociação. No artigo anterior, nós consideramos a interface gráfica (a parte View do programa) e a estrutura dos arquivos do projeto.

Nós também analisamos a estrutura interna do projeto, a interação entre as classes e o lançamento do processo de otimização do ponto de vista do programa. Como o programa suporta várias lógicas de otimização, nós não consideramos em detalhes a lógica implementada — é melhor descrever a lógica em um artigo separado como um exemplo de implementação do otimizador. Nós teremos mais dois artigos, onde nós vamos analisar a conexão da parte lógica com os gráficos, bem como discutir o algoritmo de implementação do otimizador e considerar um exemplo de implementação do otimizador.

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.

Aqui está a descrição do processo de compilação para todos aqueles que não trabalham com o Visual Studio. O projeto pode ser compilado no VisualStudio de diferentes maneiras, eis três deles:

  1. O mais fácil é pressionar CTRL + SHIFT + B.
  2. Um método mais visual é clicar no array verde no editor — isso iniciará o aplicativo no modo de depuração de código e executará a compilação (se o modo de compilação de depuração estiver selecionado).
  3. Outra opção é usar o comando Build do menu.

O programa compilado dependerá da pasta MetaTrader Auto Optimizer/bin/Debug (ou MetaTrader Auto Optimizer/bin/Release — dependendo do método de compilação selecionado).

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/7718

Arquivos anexados |
Auto_Optimiser.zip (125.7 KB)
Últimos Comentários | Ir para discussão (1)
nuno1234
nuno1234 | 4 ago 2020 em 11:32

PORTUGUESE

BOM DIA E OBRIGADO PELA AJUDA E ESCLARECIMENTO

FIZ BACKTEST COM ESTE EA 

Mr Bee MT5


ELE USA OPTIMIZAÇÃO

MAS COMO GANHA SEMPRE TENHO DUVIDA SE LÊ O HISTORICO

COM OS VOSSOS CONHECIMENTOS PODEM APURAR SE É REAL O SEU TRABALHO?

GRATO

NUNO

PORTUGAL


 
Trabalhando com séries temporais na biblioteca DoEasy (Parte 40): indicadores com base na biblioteca - atualização de dados em tempo real Trabalhando com séries temporais na biblioteca DoEasy (Parte 40): indicadores com base na biblioteca - atualização de dados em tempo real
Neste artigo, consideraremos a criação de um indicador multiperíodo simples com base na biblioteca DoEasy. Modificaremos as classes de séries temporais para receber dados de qualquer timeframe e exibi-los no período gráfico atual.
Linguagem MQL como um meio de marcação da interface gráfica de programas MQL. Parte II Linguagem MQL como um meio de marcação da interface gráfica de programas MQL. Parte II
Neste artigo continuaremos testando um novo conceito, em particular a descrição da interface de programas MQL usando as construções da linguagem MQL. A criação automática de GUIs com base no layout MQL fornece funcionalidade adicional para armazenamento em cache e geração dinâmica de elementos, gerenciamento de estilos e novos esquemas de manipulação de eventos. Incluímos uma versão aprimorada da biblioteca de controles padrão.
Criando um EA gradador multiplataforma: testando um EA multimoeda Criando um EA gradador multiplataforma: testando um EA multimoeda
No mês, os mercados caíram mais de 30%. Estamos no momento oportuno para testar Expert Advisors gradadores e martingale. Este artigo é uma continuação da série de artigos "Criando um EA gradador multiplataforma", cuja publicação não tinha sido planejada. Mas, uma vez que o próprio mercado nós dá uma oportunidade para fazer um teste de estresse do EA gradador, é bom aproveitá-la. Então, vamos direto ao assunto.
Monitoramento de sinais de negociação multimoeda (Parte 4): Aprimoramento das funcionalidades e melhorias no sistema de busca de sinais Monitoramento de sinais de negociação multimoeda (Parte 4): Aprimoramento das funcionalidades e melhorias no sistema de busca de sinais
Nesta parte, nós expandimos o sistema de busca e edição de sinais de negociação, além de apresentar a possibilidade de usar indicadores personalizados e adicionar a localização do programa. Nós criamos anteriormente um sistema básico para busca de sinais, mas ele era baseado em um pequeno conjunto de indicadores e em um conjunto simples de regras de busca.