Otimização Walk Forward Contínua (Parte 7): Vinculação da parte lógica do Otimizador Automático com a parte gráfica e o controle do mesmo no programa

17 agosto 2020, 08:54
Andrey Azatskiy
0
1 982

Neste artigo, nós consideraremos como a parte lógica do programa está conectada com sua representação gráfica. Nós veremos todo o processo de execução da otimização, desde o seu início, e analisaremos todas as etapas até a classe do otimizador automático. Nós também vamos ver como a parte lógica do programa está conectada com a sua exibição, bem como considerar os métodos de gerenciamento gráfico a partir do código do aplicativo. 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
  6. Otimização Walk Forward contínua (parte 6): Lógica e estrutura do otimizador automático

Classe ViewModel e interação com a camada gráfica

Como já mencionado anteriormente, a ViewModel é o conector entre a parte gráfica do aplicativo e a implementação de software da parte lógica. É a representação gráfica do programa, que implementa as chamadas lógicas do aplicativo e as reações gráficas aos callbacks (retornos de chamada) da parte lógica do aplicativo. Assim, uma propriedade pública da parte da ViewModel corresponde a cada campo editável na parte gráfica do aplicativo. Essas propriedades podem ser os getters, que não podem ser alterados nos gráficos, ou os setters, que permitem sobrescrever o objeto oculto por trás dessa propriedade. Nas partes anteriores, nós já consideramos em detalhes a tecnologia de vinculação de dados. Portanto, fornecerei apenas alguns exemplos aqui. 

Os campos de texto são conectados por meio de propriedades que têm acesso de gravação e leitura. Como exemplo, considere um campo que indica o nome de um ativo no qual a otimização será realizada. A marcação XAML para este campo é extremamente simples.

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

Além de definir a largura da janela de texto, ele também possui os campos IsEnabled e Text. O primeiro define se o campo está disponível para edição. Se for definido como verdadeiro, o campo ficará disponível para edição. Se for falso, o campo estará bloqueado. O campo "Text" contém o texto inserido neste campo. Em seguida, há uma construção entre chaves para cada uma delas. Seu conteúdo define a conexão do objeto com uma determinada propriedade pública da classe ViewModel especificada após o parâmetro "Binding".

Isso pode ser seguido por vários parâmetros. Por exemplo, o parâmetro UpdateSourceTrigger indica o método de atualização da parte gráfica deste aplicativo. O valor usado em nosso exemplo (PropertyChanged), indica que a parte gráfica só será atualizada após o acionamento do evento OnPropertyChanged da classe ViewModel com um nome passado e especificado após os parâmetros "Binding" (que é o "EnableMainTogles" em nosso exemplo).

Se o parâmetro "Text" não estiver vinculado a uma string, mas digamos que a um parâmetro do tipo double, apenas números serão permitidos neste campo. Se estiver vinculado a um tipo int, apenas números inteiros serão permitidos. Em outras palavras, esta implementação permite definir os requisitos para o tipo de valor inserido.

Na parte ViewModel, os campos são apresentados da seguinte forma:

O parâmetro IsEnabled:

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

e o parâmetro Text:

/// <summary> /// Name of the asset selected for tests / optimization /// </summary> public string AssetName { get; set; }

Como você pode ver, ambos têm acesso aos dados para gravação e leitura. A única diferença é que a propriedade EnableMainTogles fornece acesso de gravação apenas a partir da classe AutoOptimiserVM (ou seja, a partir dela mesma), não podendo ser editada externamente.

Se nós considerarmos qualquer coleção de dados, como, por exemplo, uma lista de resultados de otimização direta, então ela corresponde a uma propriedade que contém uma lista de valores. Vamos considerar uma tabela com os resultados dos passes de forward:

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

Como pode ser visto na marcação, a tabela do tipo ListView é uma referência da própria classe da tabela. Isso é seguido pela criação de uma grade em que os dados serão armazenados, e uma coluna com os dados. Ao mencionar a criação de uma referência de classe, eu estava me referindo à classe ListView. Essa marcação XAML aparentemente simples representa um mecanismo bastante complexo e bem pensado, que permite descrever classes e trabalhar com os objetos de classe usando a linguagem de marcação. Todos os campos que nós associamos à classe AutoOptimiserVM são exatamente as propriedades dessas classes. No exemplo acima com a tabela, nós lidamos com as três classes:

  • ListView — System.Windows.Controls.ListView.
  • GridView — System.Windows.Controls.GridView, que é derivado de System.Windows.Controls.ViewBase e, portanto, pode ser usado como uma classe inicializando a propriedade View da classe ListView.
  • GridViewColumn — System.Windows.Controls.GridViewColumn.

A propriedade ItemsSource da classe ListView indica uma coleção de elementos que consiste na tabela. Depois de conectar esta propriedade com uma coleção da ViewModel, nós temos uma espécie de DataContext para a classe Window, que opera dentro de nossa tabela. Como nós estamos falando de uma tabela, a tabela que representa a coleção deve consistir de classes que possuem propriedades públicas para cada uma das tabelas. Depois de associar a propriedade ItemsSource a uma propriedade da ViewModel, que representa uma tabela com os dados, nós podemos vincular cada uma das colunas com o valor da coluna desejada da tabela fornecida. Além disso, a tabela tem uma conexão com as propriedades SelectedIndex e SelectedForwardItem da ViewModel. Isso é necessário para a ViewModel saber qual linha o usuário selecionou nesta tabela.

Na parte da ViewModel, a propriedade com a qual a tabela apresentada está associada é implementada da seguinte maneira:

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

A classe ObservableCollection da biblioteca padrão C# é um objeto que notifica os gráficos sobre as modificações. Isso porque a classe já possui o evento mencionado e o chama toda vez que for atualizar a lista de seus elementos. Quanto ao resto, ela é uma coleção padrão de dados.

A propriedade SelectedForwardItem desempenhava várias funções: ela armazena os dados na linha da tabela selecionada e serve como um callback da seleção da linha.

/// <summary> /// Selected forward pass /// </summary> private int _selectedForwardItem; public int SelectedForwardItem {     get => _selectedForwardItem;     set     {         _selectedForwardItem = value;         if (value > -1)         {             FillInBotParams(model.ForwardOptimisations[value]);             FillInDailyPL(model.ForwardOptimisations[value]);             FillInMaxPLDD(model.ForwardOptimisations[value]);         }     } } 

Como a propriedade é utilizada como uma callback, devido ao qual se espera a especificação da reação ao ajuste de um valor (em nosso exemplo), o setter deve conter a implementação dessa reação e servir como uma função. Devido a isso, o valor da propriedade é armazenado na variável privada. Para receber um valor desta variável, nós a acessamos diretamente do getter. Para definir um valor nós definimos no setter um valor armazenado como 'value'. A variável 'value' não tem título e serve como um certo alias para o valor definido, fornecido pela linguagem C#. Se 'value' for maior que -1, preenchemos outras tabelas relacionadas na guia Results, que são atualizadas de acordo com a linha selecionada. Estas são as tabelas com os parâmetros do robô de negociação, lucro médio, prejuízo para um dia da semana e o maior/menor valor do PL. A verificação realizada na condição 'if' é necessária porque se o índice do elemento da tabela selecionado for -1, isso significa que a tabela está vazia e, portanto, não há necessidade de preencher as tabelas relacionadas. A implementação dos métodos chamados está disponível no código da classe AutoOptimiserVM.

Aqui está a implementação da classe que descreve a linha com o resultado da otimização.

/// <summary> /// Class - a wrapper for a report item (for a graphical interval) /// </summary> class ReportItem {     /// <summary>     /// Constructor     /// </summary>     /// <param name="item">Item</param>     public ReportItem(OptimisationResult item)     {         result = item;     }     /// <summary>     /// Report item     /// </summary>     private readonly OptimisationResult result;     public DateTime From => result.report.DateBorders.From;     public DateTime Till => result.report.DateBorders.Till;     public double SortBy => result.SortBy;     public double Payoff => result.report.OptimisationCoefficients.Payoff;     public double ProfitFactor => result.report.OptimisationCoefficients.ProfitFactor;     public double AverageProfitFactor => result.report.OptimisationCoefficients.AverageProfitFactor;     public double RecoveryFactor => result.report.OptimisationCoefficients.RecoveryFactor;     public double AverageRecoveryFactor => result.report.OptimisationCoefficients.AverageRecoveryFactor;     public double PL => result.report.OptimisationCoefficients.PL;     public double DD => result.report.OptimisationCoefficients.DD;     public double AltmanZScore => result.report.OptimisationCoefficients.AltmanZScore;     public int TotalTrades => result.report.OptimisationCoefficients.TotalTrades;     public double VaR90 => result.report.OptimisationCoefficients.VaR.Q_90;     public double VaR95 => result.report.OptimisationCoefficients.VaR.Q_95;     public double VaR99 => result.report.OptimisationCoefficients.VaR.Q_99;     public double Mx => result.report.OptimisationCoefficients.VaR.Mx;     public double Std => result.report.OptimisationCoefficients.VaR.Std; }

A classe é fornecida aqui para demonstrar a aparência de uma string de um passe de otimização em um código. Cada coluna da tabela está associada a uma propriedade apropriada da instância de classe específica. A própria classe é um wrapper para a estrutura OptimisationResult que foi considerada no primeiro artigo.

Todos os botões ou cliques duplos em uma linha da tabela são conectados pela propriedade Command da ViewModel, cujo tipo básico é ICommand. Nós já consideramos essa tecnologia em artigos anteriores sobre a criação da interface gráfica. 

A classe ViewModel e interação com o modelo de dados

Vamos começar este capítulo com os callbacks de inicialização e parada de otimização que são combinados no mesmo botão. 


O clique no botão StartStop chama o método _StartStopOptimisation da classe AutoOptimiserVM. Além disso, existem duas alternativas: interromper a otimização e iniciar a otimização. Como pode ser visto no diagrama, quando a propriedade IsOptimisationInProcess da classe do otimizador retorna true, nós executamos a primeira parte da lógica e solicitamos o método StopOptimisation da classe do modelo de dados. O método então redireciona essa chamada para o otimizador. Se a otimização não foi iniciada, o método StartOptimisation da classe do modelo de dados é chamado. O método é assíncrono e, portanto, o método Start chamado continuará a operação mesmo após a operação _StartStopOptimisation ser concluída. 

Nós consideramos a cadeia de chamadas realizadas na chamada do método. Agora, vamos ver o bloco de código que descreve a conexão dessas chamadas de método com a parte gráfica e da Model com o ViewModel. As marcações gráficas XAML não são difíceis e, portanto, não são apresentadas aqui. Quanto à parte ViewModel, as propriedades e o método responsáveis pelo início da otimização são os seguintes:

private void _StartStopOptimisation(object o) {     if (model.Optimiser.IsOptimisationInProcess)     {         model.StopOptimisation();     }     else     {         EnableMainTogles = false;         OnPropertyChanged("EnableMainTogles");         Model.OptimisationManagers.OptimiserInputData optimiserInputData = new Model.OptimisationManagers.OptimiserInputData         {             Balance = Convert.ToDouble(OptimiserSettings.Find(x => x.Name == "Deposit").SelectedParam),             BotParams = BotParams?.Select(x => x.Param).ToList(),             CompareData = FilterItems.ToDictionary(x => x.Sorter, x => new KeyValuePair<CompareType, double>(x.CompareType, x.Border)),             Currency = OptimiserSettings.Find(x => x.Name == "Currency").SelectedParam,             ExecutionDelay = GetEnum<ENUM_ExecutionDelay>(OptimiserSettings.Find(x => x.Name == "Execution Mode").SelectedParam),             Laverage = Convert.ToInt32(OptimiserSettings.Find(x => x.Name == "Laverage").SelectedParam),             Model = GetEnum<ENUM_Model>(OptimiserSettings.Find(x => x.Name == "Optimisation model").SelectedParam),             OptimisationMode = GetEnum<ENUM_OptimisationMode>(OptimiserSettings.Find(x => x.Name == "Optimisation mode").SelectedParam),             RelativePathToBot = OptimiserSettings.Find(x => x.Name == "Available experts").SelectedParam,             Symb = AssetName,             TF = GetEnum<ENUM_Timeframes>(OptimiserSettings.Find(x => x.Name == "TF").SelectedParam),             HistoryBorders = (DateBorders.Any(x => x.BorderType == OptimisationType.History) ?                             DateBorders.Where(x => x.BorderType == OptimisationType.History)                             .Select(x => x.DateBorders).ToList() :                             new List<DateBorders>()),             ForwardBorders = (DateBorders.Any(x => x.BorderType == OptimisationType.Forward) ?                             DateBorders.Where(x => x.BorderType == OptimisationType.Forward)                             .Select(x => x.DateBorders).ToList() :                             new List<DateBorders>()),             SortingFlags = SorterItems.Select(x => x.Sorter)         };         model.StartOptimisation(optimiserInputData, FileWritingMode == "Append", DirPrefix);     } } /// <summary> /// Callback for the graphical interface - run optimization / test /// </summary> public ICommand StartStopOptimisation { get; }

Como pode ser visto no código e no diagrama, o método é dividido em dois ramos da condição 'If Else'. O primeiro interrompe o processo de otimização se ele estiver em execução. O segundo inicia o processo de outra forma.

No momento do lançamento da otimização, nós bloqueamos os campos principais da interface gráfica definindo EnableMainTogles = false e, em seguida, prosseguimos para formar os parâmetros de entrada. Para iniciar uma otimização, nós precisamos criar a estrutura OptimistionInputData que é preenchida a partir das coleções OptimiserSettings, BotParams, FilterItems, SorterItems e DateBorders. Os valores chegam a essas estruturas diretamente da interface gráfica, usando o mecanismo de vinculação de dados já mencionado. Após a conclusão da formação desta estrutura, nós executamos o método anteriormente discutido StartOptimisation para uma instância da classe de modelo de dados. A propriedade StartStopOptimisation no construtor.

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

Ele é instanciado pela instância da classe RelayCommand que implementa a interface ICommand, que é necessária para vincular os comandos da ViewModel com a propriedade Command da parte gráfica do aplicativo.

Uma vez que todas as otimizações tenham sido realizadas e as tabelas na guia Results tenham sido formadas (ou uma vez que tenham sido carregadas usando o botão Load e selecionando uma otimização da lista), você pode iniciar um teste de um passe de otimização selecionada em qualquer um dos intervalos de tempo necessários clicando duas vezes no passe de otimização desejado. 

private void _StartTest(List<OptimisationResult> results, int ind) {     try     {         Model.OptimisationManagers.OptimiserInputData optimiserInputData = new Model.OptimisationManagers.OptimiserInputData         {             Balance = Convert.ToDouble(OptimiserSettingsForResults_fixed.First(x => x.Key == "Deposit").Value),             Currency = OptimiserSettingsForResults_fixed.First(x => x.Key == "Currency").Value,             ExecutionDelay = GetEnum<ENUM_ExecutionDelay>(OptimiserSettingsForResults_changing.First(x => x.Name == "Execution Mode").SelectedParam),             Laverage = Convert.ToInt32(OptimiserSettingsForResults_fixed.First(x => x.Key == "Laverage").Value),             Model = GetEnum<ENUM_Model>(OptimiserSettingsForResults_changing.First(x => x.Name == "Optimisation model").SelectedParam),             OptimisationMode = ENUM_OptimisationMode.Disabled,             RelativePathToBot = OptimiserSettingsForResults_fixed.First(x => x.Key == "Expert").Value,             ForwardBorders = new List<DateBorders>(),             HistoryBorders = new List<DateBorders> { new DateBorders(TestFrom, TestTill) },             Symb = OptimiserSettingsForResults_fixed.First(x => x.Key == "Symbol").Value,             TF = (ENUM_Timeframes)Enum.Parse(typeof(ENUM_Timeframes), OptimiserSettingsForResults_fixed.First(x => x.Key == "TF").Value),             SortingFlags = null,             CompareData = null,             BotParams = results[ind].report.BotParams.Select(x => new ParamsItem { Variable = x.Key, Value = x.Value }).ToList()         };         model.StartTest(optimiserInputData);     }     catch (Exception e)     {         System.Windows.MessageBox.Show(e.Message);     } }

Em seguida, nós criamos uma estrutura com os parâmetros de entrada e iniciamos um teste. Caso ocorra algum erro durante o processo de execução do método, exibimos uma mensagem de erro em uma MessageBox. A implementação do método já foi discutida. No entanto, vamos dar uma olhada na instanciação das propriedades que contêm esse callback. Nós temos três tabelas diferentes:

  • Testes de Forward,
  • Testes do Histórico,
  • Lista de otimizações para o intervalo de datas selecionado.

Portanto, três callbacks foram criados. Isso é necessário para um processamento correto dos dados de cada tabela. 

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

Sua implementação é realizada por meio da configuração das funções lambda:

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

Esta abordagem permite a criação da lista necessária com os resultados de otimização que é usada para obter os parâmetros do robô, que o algoritmo passa para um arquivo (para obter os detalhes, consulte a Parte 3 desta série de artigos). 

Depois que o processo de otimização termina e os melhores resultados são selecionados e testados usando os dados do histórico e de forward, é salvo uma lista com todos os passes de otimização. Devido a esse processo, o usuário pode verificar a lógica de operação do otimizador selecionado, bem como selecionar outros passes manualmente, alterando os fatores de filtragem e classificação. Portanto, existe a possibilidade de usar o mecanismo embutido para filtrar os resultados da otimização e classificá-los de acordo com vários critérios simultaneamente. Esse mecanismo é implementado no modelo de dados, mas os parâmetros de entrada para este mecanismo são gerados na classe ViewModel.

/// <summary> /// Sort reports /// </summary> /// <param name="o"></param> private void _SortResults(object o) {     if (ReportDateBorders.Count == 0)         return;     IEnumerable<SortBy> sortFlags = SorterItems.Select(x => x.Sorter);     if (sortFlags.Count() == 0)         return;     if (AllOptimisations.Count == 0)         return;     model.SortResults(ReportDateBorders[SelectedReportDateBorder], sortFlags); } public ICommand SortResults { get; } /// <summary> /// Filtering reports /// </summary> /// <param name="o"></param> private void _FilterResults(object o) {     if (ReportDateBorders.Count == 0)         return;     IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData =         FilterItems.ToDictionary(x => x.Sorter, x => new KeyValuePair<CompareType, double>(x.CompareType, x.Border));     if (compareData.Count() == 0)         return;     if (AllOptimisations.Count == 0)         return;     model.FilterResults(ReportDateBorders[SelectedReportDateBorder], compareData); } public ICommand FilterResults { get; }

Esses dois métodos têm implementações semelhantes. Eles verificam a presença dos parâmetros de filtragem dos dados (ou seja, se a tabela não está vazia) e redirecionam sua execução para a classe do modelo de dados. Ambos os métodos da classe do modelo de dados redirecionam a execução para o método de extensão apropriado descrito no primeiro artigo.

O método de classificação possui as seguintes assinaturas:

public static IEnumerable<OptimisationResult> SortOptimisations(this IEnumerable<OptimisationResult> results,                                                                         OrderBy order, IEnumerable<SortBy> sortingFlags,                                                                         Func<SortBy, SortMethod> sortMethod = null)

O método de filtragem:

public static IEnumerable<OptimisationResult> FiltreOptimisations(this IEnumerable<OptimisationResult> results,                                                                   IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData)

Isso é executado em um modo assíncrono, para evitar o bloqueio dos gráficos durante a classificação (que pode levar mais de um segundo, dependendo da quantidade de dados).

Falando sobre a classificação de dados, vamos ver a implementação da conexão entre duas tabelas de classificação e filtragem de dados. No otimizador automático, tanto a guia de resultados quanto a guia de configurações (principal) têm uma área com os dados da tabela de classificação e filtragem de dados — é disso que estamos falando.

   

Na imagem acima, esta área é marcada na guia dos resultados de otimização. A ideia é que se adicionarmos qualquer parâmetro de classificação nesta área e, em seguida, mudarmos para outra guia (como a guia de configurações em nosso exemplo), o mesmo valor adicionado aparecerá na mesma área. Agora, se nós removermos este valor desta área na guia de configurações e depois voltarmos para a guia com os resultados da otimização, veremos que o valor também foi removido desta guia. Isso ocorre porque as duas tabelas estão vinculadas à mesma propriedade.

As tabelas de classificação estão vinculadas à seguinte propriedade:

/// <summary> /// Selected sorting options /// </summary> public ObservableCollection<SorterItem> SorterItems { get; } = new ObservableCollection<SorterItem>();

As tabelas de filtro estão vinculadas a:   

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

As classes que descrevem as linhas nessas tabelas possuem alguns campos repetidos e são intituladas no mesmo arquivo da ViewModel.

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

A classe SorterItem é um objeto que apresenta as linhas da tabela dos parâmetros selecionados para classificação. Além do parâmetro de classificação, ele também contém a propriedade que aponta para a callback de exclusão deste parâmetro específico da lista. Observe que este callback é definido externamente por meio de um delegado. A classe de filtro de dados é herdada da classe de classificação: não há necessidade de escrever os campos já implementados duas vezes, pois nós podemos simplesmente herdá-los da classe base. Além do conjunto de parâmetros considerado anteriormente, ele tem um tipo de comparação de dados com um valor limiar e o próprio valor limiar.

A presença dos métodos de remoção na classe, que apresenta uma linha, permite adicionar um botão Delete próximo a cada linha, como é feito na implementação atual. Ele é conveniente para os usuários e possui uma implementação interessante. Os métodos de exclusão são implementados fora das classes. Eles são definidos como delegados porque precisam de acesso a coleções de dados localizadas na classe que representa a ViewModel. Sua implementação é bastante simples e, portanto, não é fornecida aqui. Esses métodos chamam apenas o método Delete para a instância de coleção de dados desejada.

Após a conclusão de alguns dos eventos que requerem a reação da camada gráfica, o evento OnPropertyChanged é chamado. A callback do evento na classe que representa a ViewModel é implementada da seguinte maneira:

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

Todas as condições nesta callback verificam a propriedade PropertyName do parâmetro de entrada "e". A primeira condição é atendida se o teste for concluído e o modelo de dados for solicitado para desbloquear a GUI. Quando essa condição é acionada, nós desbloqueamos a GUI, redefinimos o status da barra de progresso e as barras de progresso para os valores iniciais. Observe que este evento pode ser chamado no contexto da thread secundária, e a notificação gráfica (chamada de evento OnPropertyChanged) deve sempre ser feita no contexto da thread primária, ou seja, na mesma thread com uma GUI. Portanto, para evitar erros, chamamos este evento da classe dispatcher. A Dispatcher permite acessar a GUI a partir do contexto da thread desta janela.

A próxima condição é chamada assim que o modelo de dados atualiza a lista de todas as otimizações realizadas. Para permitir a seleção das listas de otimização por meio de uma combobox, nós precisamos preenchê-la com as datas de otimização apropriadas. Isso é feito por esta parte do código. Ele também preenche os parâmetros fixos do testador:

  • Nome do Expert Advisor
  • Depósito
  • Moeda de depósito
  • Alavancagem

Depois disso, ele mostra uma MessageBox notificando que a atualização dos parâmetros e das tabelas com os relatórios dos passes de otimização foi concluído.

Depois que a filtragem ou a classificação estiver concluída, a condição correspondente é acionada. Porém, para entender sua implementação, consideremos a implementação da propriedade SelectedReportDateBorder.

#region Selected optimisation date border index keeper private int _selectedReportDateBorder; public int SelectedReportDateBorder {     get => _selectedReportDateBorder;     set     {         AllOptimisations.Clear();         if (value == -1)         {             _selectedReportDateBorder = 0;             return;         }         _selectedReportDateBorder = value;         if (ReportDateBorders.Count == 0)             return;         List<OptimisationResult> collection = model.AllOptimisationResults.AllOptimisationResults[ReportDateBorders[value]];         foreach (var item in collection)         {             AllOptimisations.Add(new ReportItem(item));         }     } } #endregion

A parte setter atualiza a coleção AllOptimisations na classe ViewModel e, portanto, o código na condição faz sentido agora. Em outras palavras, ao definir o parâmetro SelectedReportDateBorder para si mesmo, nós evitamos a duplicação desse loop. 

As condições relacionadas com a atualização das tabelas de Forward e do Histórico têm a mesma função da condição anterior, ou seja, a sincronização de dados entre a ViewModel e Model. Essa sincronização é necessária porque nós não podemos nos referir diretamente às estruturas nas quais o modelo de dados opera, uma vez que as classes correspondentes são necessárias para descrever as linhas da tabela, onde cada coluna é representada por uma propriedade. Essas classes são criadas como wrappers para as estruturas usadas no modelo de dados. A classe ReportItem é utilizada para as tabelas com os resultados da otimização, o que foi considerado no capítulo anterior.

Conclusão

Este artigo precede o último de uma série de artigos dedicados à otimização direta e um otimizador automático que implementa esse processo. Nós consideramos a estrutura das partes mais significativas do aplicativo criado. O primeiro artigo descreveu a parte do aplicativo responsável por trabalhar com os relatórios e salvá-los em arquivos xml. A segunda e terceira parte continham uma descrição de como um relatório para o otimizador automático é gerado e como um Expert Advisor pode ser conectado com a interface do programa de carregamento de relatório, que foi descrito no primeiro artigo. A quarta parte continha instruções de uso do programa: até então, nós consideramos as etapas necessárias para conectar qualquer robô ao otimizador automático.

Nas partes 5, 6 e 7 nós consideramos o programa do otimizador automático, que controla o processo. Nós começamos com a sua parte gráfica (o quinto artigo), depois examinamos sua lógica de funcionamento (sexto artigo) e a conexão entre elas (o artigo atual). Nos comentários do quinto artigo, os usuários adicionaram algumas sugestões sobre a UI do aplicativo. As mais interessantes delas já foram implementadas.

A parte atual não contém essas melhorias, pois a ideia principal era descrever o trabalho anterior. O próximo artigo (que será o último) conterá as melhorias indicadas e fornecerá uma descrição de como você pode criar o seu próprio otimizador. Por otimizador, eu quero dizer a lógica de executar as otimizações. A lógica do otimizador atual já foi considerada anteriormente (principalmente no quarto artigo). Portanto, o último artigo fornecerá instruções sobre como criar as lógicas semelhantes. Nós usaremos a lógica de otimização existente como base e consideraremos um processo passo a passo de como criar o seu próprio 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 Software Corp.
Artigo original: https://www.mql5.com/ru/articles/7747

Arquivos anexados |
Auto_Optimiser.zip (125.7 KB)
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.

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.

Monitoramento de sinais de negociação multimoeda (Parte 5): Sinais compostos Monitoramento de sinais de negociação multimoeda (Parte 5): Sinais compostos

No quinto artigo relacionado à criação de um monitor de sinal de negociação, nós consideraremos os sinais compostos e implementaremos a funcionalidade necessária. Em versões anteriores, nós usamos os sinais simples, como o RSI, WPR e CCI, e também introduzimos a possibilidade de usar os indicadores personalizados.

Trabalhando com séries temporais na biblioteca DoEasy (Parte 41): exemplo de indicador multissímbolo multiperíodo Trabalhando com séries temporais na biblioteca DoEasy (Parte 41): exemplo de indicador multissímbolo multiperíodo

Neste artigo, veremos um exemplo de como criar um indicador multissímbolo multiperíodo usando as classes das séries temporais da biblioteca DoEasy que exibe numa subjanela um gráfico mostrando o par de moedas selecionado no período gráfico desejado na forma de velas japonesas. Vamos modificar um pouco as classes da biblioteca e criar um arquivo separado para armazenar enumerações dos parâmetros de entrada dos programas e para a escolher da linguagem de compilação.