Implementado OLAP na negociação (Parte 4): análise quantitativa e visual dos relatórios do testador

Stanislav Korotky | 22 junho, 2020

Neste artigo, continuaremos a estudar a aplicação da tecnologia OLAP (On-Line Analytical Processing) na negociação.

Em artigos anteriores, descrevemos técnicas gerais que permitiam a construção de classes para acumulação e análise de dados multidimensionais, além disso, contamos com detalhe sobre a visualização dos resultados da análise numa interface gráfica. Do ponto de vista prático, os dois primeiros artigos abordavam o processamento de relatórios de negociação vindos de diferentes fontes, como o testador de estratégia, o histórico de negociação on-line, arquivos HTML e CSV (incluindo sinais de negociação MQL5). No terceiro artigo, após uma pequena refatoração de código, usamos o OLAP para analisar cotações e desenvolver estratégias de negociação. Para uma boa compreensão do novo material, é necessário se familiarizar com esses artigos (entre parênteses é indicado o que se deve prestar atenção primeiro):

Hoje, expandiremos área de aplicação do OLAP graças à análise de resultados de otimização do MetaTrader 5.

Para realizar este projeto, primeiro precisamos melhorar um pouco a interface gráfica do usuário discutida no artigo 2. Acontece que no código do artigo 3 melhoramos diretamente apenas o mecanismo OLAP, e não atualizamos a visualização. Vamos emendar esse descuido, usando como tarefa de teste o programa OLAPGUI - visto no segundo artigo - para analisar relatórios de negociação. Ao mesmo tempo, vamos unificar a mesma parte gráfica de forma que ela possa ser encaixada facilmente em qualquer nova área complementar, em particular, com o analisador de resultados de otimização que temos projetado.

Gráfico de trades na mira

Lembremo-nos de que o centro da GUI para OLAP é o componente visual especialmente desenvolvido CGraphicInPlot. Sua primeira implementação, apresentada no artigo 2, teve algumas falhas que diziam respeito à exibição de etiquetas nos eixos. Aprendemos, quando necessário, exibir os nomes das células dos seletoras (como os nomes dos dias da semana ou das moedas) no eixo horizontal X, mas, em todos os outros casos, o gráfico implica a exibição do número "tal qual como está", o que nem sempre é fácil de ler (ou amigável para o usuário). Além disso, para o eixo Y, onde geralmente são exibidos valores agregados, mas, dependendo das configurações, podem ser exibidas as células dos seletores, também é necessária a personalização. Um bom exemplo de exibição inconveniente de etiquetas pode ser una solicitação para retenção média de posição de um símbolo.

Duração média com base nos símbolos (segundos)

Duração média com base nos símbolos (segundos)

Como aqui o eixo não exibe um seletor (que arredonda os valores para o tamanho das células do cubo), mas, sim, um valore de duração agregado em segundos, os números grandes são mal percebidos. Para resolver esse problema, tentaremos dividir os segundos pelo comprimento da barra do período gráfico atual. Em seguida, os valores exibidos mostrarão o número de barras. Isso requer passar um sinalizador para a classe CGraphicInPlot e, em seguida, para a classe interna de processamento de eixo CAxis. Pode haver muitos desses sinalizadores que alteram o modo de operação, portanto para eles reservaremos uma nova classe AxisCustomizer especial no arquivo Plot.mqh.

Talvez possam ser adicionadas a ele muitas nuances para exibição de etiquetas, porém, no momento ele armazena apenas um sinal do tipo de eixo (X ou Y) e várias opções, como periodDivider e hide. O primeiro deles indica que é necessário dividir os valores por PeriodSeconds(), veremos o segundo posteriormente.

Os objetos desta classe entram no CGraphicInPlot usando métodos especiais:

Quando esse objeto não é criado e passado para as classes gráficas, a biblioteca padrão gera os valores da maneira usual, isto é, como um número (AXIS_TYPE_DOUBLE).

Aqui, usamos a abordagem da biblioteca padrão para personalizar os rótulos nos eixos: o tipo de eixo é definido como AXIS_TYPE_CUSTOM e com ajuda de ValuesFunctionFormatCBData é passado o ponteiro para o objeto AxisCustomizer. Posteriormente, ele é passado pela classe base CGraphic para a nossa função de renderização de etiqueta CustomDoubleToStringFunction (ela é atribuída chamando ValuesFunctionFormat no trecho de código acima). Certamente, precisamos da própria função CustomDoubleToStringFunction. Ela já foi implementada anteriormente, mas, de forma simplificada, sem usar os objetos da classe AxisCustomizer selecionada (o próprio gráfico CGraphicInPlot era um objeto de configuração).

Os próprios objetos de personalização do AxisCustomizer são armazenados na classe CPlot, que é o controle da interface (o sucessor do CWndClient) e o contêiner do CGraphicInPlot:

Desse modo, as configurações de eixo nos objetos m_customX e m_customY podem ser usadas não apenas no estágio final da formatação de valores na função CustomDoubleToStringFunction, mas também muito antes, isto é, quando as matrizes de dados são passadas para o CPlot usando um dos métodos CurveAdd, por exemplo, assim:

Aqui vemos apenas o uso da opção periodDivider para dividir todos os valores por PeriodSeconds(). Esta operação é executada antes que a biblioteca padrão receba os dados e calcule o tamanho da etapa de grade para eles. Isso é importante, porque depois que a grade já é contada, é tarde demais na função CustomDoubleToStringFunction para executar a personalização relacionada ao dimensionamento.

O código de chamada na caixa de diálogo deve criar e inicializar objetos AxisCustomizer conforme necessário no momento da criação do cubo, por exemplo, da seguinte maneira:

Aqui m_plot é a variável de diálogo na qual está armazenado o controle CPlot. A seguir é apresentado o código completo do método OLAPDialog::process, nele pode ser visto como isso na verdade é feito. Assim fica o exemplo de gráfico - mencionado acima - com o periodDivider ativado automaticamente:

Duração média com base nos símbolos (barras do período atual, D1)

Duração média com base nos símbolos (barras do período atual, D1)

Outra variável no AxisCustomizer - hide - permite ocultar completamente as etiquetas no eixo X. Este modo é necessário quando o usuário seleciona a classificação por valor num cubo multidimensional. Nesse caso, as etuqyetas em cada linha de números terão sua própria ordem e não haverá nada para exibir ao longo do eixo X. A classificação num cubo multidimensional está disponível, porque faz sentido em outros modos, em particular por etiquetas.

A opção hide funciona dentro de CustomDoubleToStringFunction. O comportamento padrão dessa função implica a presença de seletores: suas etiquetas são armazenadas em cache para o eixo X na classe especial CurveSubtitles e retornados ao gráfico pelo índice de divisão da grade. No entanto, o sinalizador hide encerra esse processo no início de qualquer abcissa, enquanto a função retorna NULL (valor não exibido).

O segundo problema que deve ser resolvido nos gráficos de negociação é a renderização de histograma. Quando várias linhas (vetores de dados) são exibidas no gráfico, as barras do histograma em cada uma das amostras são completamente sobrepostas e a maior delas pode se sobrepor às outras.

A classe base CGraphic possui um método HistogramPlot virtual. Ele deve ser redefinido e de alguma forma "separar" visualmente as colunas de uma contagem. Para esse propósito, seria desejável que tivéssemos um campo personalizado no objeto de curva CCurve que armazena dados arbitrários (interpretados pelo código do cliente conforme necessário). Infelizmente, não existe esse campo e, portanto, teremos que usar uma das propriedades padrão que não estavam envolvidas no projeto atual. A escolha recaiu sobre o LinesSmoothStep. Com ajuda do método setter CCurve::LinesSmoothStep, nosso código de chamada gravará o número da sequência e, usando o método getter CCurve::LinesSmoothStep, é fácil obtê-lo na nova implementação do HistogramPlot. Aqui está um exemplo de registro do número da linha em LinesSmoothStep:

Conhecendo o número total de linhas e o número da atual, podemos deslocar cada um de seus pontos um pouco para a esquerda ou direita do ponto zero ao renderizar. Aqui está uma versão adaptada do HistogramPlot. As linhas alteradas são marcadas com comentários com o símbolo "*", enquanto as adicionadas, com o símbolo "+".

Em breve vamos verificar como fica isso.

Outro momento desagradável está associado à implementação padrão da exibição de linha. Se nos dados não houver um número, o CGraphic quebra a linha. Isso é ruim para a nossa tarefa, porque em algumas células do cubo, pode realmente não haver dados e, nesse caso, os agregadores escrevem NaN. Alguns cubos, por exemplo, como o de saldo com total crescente em várias seções, são essencialmente separados em seções, porque para cada transação o valor é alterado em apenas uma seção. Para entender como a quebra de linha afeta negativamente a percepção, basta olhar para a figura “Curvas de saldo para cada símbolo separadamente” no artigo 2.

Para resolver esse problema, foi adicionalmente redefinido o método LinesPlot (veja códigos fonte, arquivo Plot.mqh). O resultado do trabalho é mostrado um pouco mais baixo na seção de processamento dos arquivos regulares do testador.

Finalmente, o último problema gráfico está relacionado à definição de eixos zero na Biblioteca Padrão. No método CGraphic::CreateGrid, a pesquisa de zeros é realizada da seguinte maneira simples (é mostrado o caso do eixo Y, já o eixo X é processado da mesma maneira):

Observe que m_yvalues aqui são etiquetas de string. Obviamente, qualquer etiqueta que não contenha um número dará 0. Isso acontece mesmo se o tipo de gráfico estiver definido como AXIS_TYPE_CUSTOM, como no nosso caso. Como resultado, ao exibir o gráfico segundo moedas, dias da semana, tipos de transações e outros seletores, todas as contagens são tratadas sequencialmente como zero, porque são verificadas num ciclo para toda a grade, mas a palavra permanece atrás da última contagem e é destacada no gráfico como uma linha em negrito (embora não seja zero). Além disso, quando cada contagem se torna, ainda que temporariamente, uma candidata a 0, ela pula o desenho usual da linha de grade, por causa disso, a grade inteira simplesmente desaparece.

Como o método CreateGrid também é virtual, nós o redefiniremos com uma verificação mais inteligente de 0, que colocamos na função auxiliar isZero.

Interface gráfica do usuário OLAP

Com isso concluímos a correção de defeitos nos gráficos, mas a interface da janela ainda precisa de revisão para universalizá-la. No Expert Advisor OLAPGUI não-negociante, visto no segundo artigo, realizamos todo o trabalho com o diálogo no arquivo de cabeçalho OLAPGUI.mqh. Nele armazenamos muitos recursos complementares relativos à tarefa anterior, isto é, à análise de relatórios de negociação. Como vamos usar o mesmo diálogo para dados arbitrários, é necessário dividir esse arquivo em duas partes: numa delas devemos colocar todo o comportamento geral da interface, enquanto na outra parte devemos alocar as configurações de um projeto específico.

Renomeamos a antiga classe OLAPDialog para OLAPDialogBase. Quanto às matrizes estáticas selectors, settings, defaults, que são hard-coded nela e que, em essência, descrevem os controles do diálogo, vamos torná-las excertos de trabalho vazios dinâmicos, e as classes derivadas lidarão com o seu preenchimento. Variáveis:

elas também farão parte dos herdeiros, porque precisarão ser padronizadas pelos tipos de seletores e campos de registro definidos na parte complementar de cada mecanismo OLAP. Lembre-se de que no artigo 3 durante a refatoração a antiga classe OLAPWrapper foi convertida na classe genérica OLAPEngine <S,T>.

Para realizar o trabalho principal, são reservados 2 novos métodos abstratos:

O primeiro (setup) define a interface, o segundo (process) inicia a análise. A chamada de configuração é feita a partir de OLAPDialogBase::Create

O usuário inicia a análise ao pressionar um botão, por isso o método OLAPDialogBase::OnClickButton é o mais modificado, uma vez que grande parte do código foi excluída dele e a respectiva funcionalidade (leitura de propriedades de "controles" e seu inicialização via mecanismo OLAP) foi delegada ao próprio método process.

Note que a classe OLAPDialogBase implementa toda a lógica da interface, começando com a criação de controles e terminando com o processamento de eventos que afetam seu estado. No entanto, ela não sabe nada sobre o conteúdo dos controles.

A classe OLAPDisplay implementa a interface virtual Display a partir de OLAPCommon.mqh (discutida no terceiro artigo). Lembre-se de que a interface Display é um retorno de chamada do kernel OLAP para fornecer resultados de análise (são passados para o primeiro parâmetro, no objeto da classe MetaCube). Graças ao ponteiro para a janela pai parent, na classe OLAPDisplay é criada uma cadeia de transferência de dados de cubo adicional para a caixa de diálogo (esse "encaminhamento" foi necessário porque não há herança múltipla em MQL5).

Aqui é apropriado abordar as nuances associadas à obtenção dos nomes dos campos de usuário "reais" desde as classes de adaptadores derivadas. Acontece que, até agora, adicionamos campos personalizados aos campos padrão da área de assunto, como, por exemplo, MFE e MAE no artigo 2, mas na verdade eles eram conhecidos antecipadamente e foram incorporados ao código. No entanto, quando trabalhemos com relatórios de otimização, será necessário analisá-los como parâmetros de entrada de robôs, e seus parâmetros (seus nomes) podem ser obtidos apenas desde os dados analisados.

O adaptador transfere os nomes dos campos "personalizados" para o agregador com ajuda do método assignCustomFields, e isso acontece em segundo plano, isto é, automaticamente no método Analyst::acquireData. Graças a isso, quando dentro de OLAPDisplay::display é chamado o método metaData.getDimensionTitle para obter a designação das seções ao longo dos eixos e o número de sequência do campo n excede a capacidade de enumeração interna de campos, sabemos que estamos lidando com um campo estendido e podemos solicitar uma descrição do cubo. A estrutura geral do método OLAPDisplay::display continua a mesma, o que é fácil de verificar comparando os códigos fonte complementares com os da versão do artigo 2.

Além disso, os nomes dos campos "personalizados" devem ser conhecidos com antecedência na caixa de diálogo para preencher com eles os elementos da interface. Para isso, também adicionamos um novo método à classe OLAPDialogBase para configurar os campos personalizados setCustomFields.

Certamente, precisaremos "vincular" a caixa de diálogo e o adaptador no EA de teste usando este método (veja mais abaixo). Depois disso, nos controles da caixa de diálogo aparecerão nomes de campos que fazem mais sentido (em vez dos números 1 custom, etc.), sendo uma solução temporária. Este e alguns outros aspectos exigem otimização de código adicional, mas no contexto deste artigo são considerados leves.

A parte complementar das configurações da interface para o programa OLAPGUI a ser modificado "migrou" de OLAPGUI.mqh para o arquivo de cabeçalho OLAPGUI_Trades.mqh. O nome da classe de diálogo permanece o mesmo, isto é, OLAPDialog, mas depende dos parâmetros do modelo, que são usados para especializar o objeto OLAPEngine:

Todo o trabalho é realizado nos métodos setup e process. O método setup preenche as matrizes settings, selectors, defaults com os mesmos valores que vimos no segundo artigo (uma vez a aparência da interface não muda). O método process inicia a análise na seção especificada e quase completamente repete o manipulador OnClickButton anterior.

No final do método, vemos a criação de objetos de configuração de eixos AxisCustomizer, que foram descritos anteriormente. Para os eixos X e Y, a divisão de valores por PeriodSeconds() é ativada ao trabalhar com o campo de duração (no agregador ou no seletor, se o tipo de agregador for AGGREGATOR_IDENTITY, nesse caso, o conteúdo dos campos cai diretamente no cubo, em vez de ser decomposto pelos seletores nas células nomeadas). O eixo X é desativado quando a dimensão do cubo é maior que 1 e é selecionada a classificação de magnitudes.

Resta dar uma olhada no arquivo de programa OLAPGUI.mq5. Entre as diferenças em relação à versão antiga, há uma ordem ligeiramente alterada ao anexar os arquivos de cabeçalho: se no kernel forem incluídos adaptadores anteriores para relatórios (porque ainda não havia outras fontes de dados), agora eles deverão ser escritos explicitamente como HTMLcube.mqh e CSVcube.mqh. Em seguida, no código OnInit, o respectivo tipo de adaptador é preparado dependendo dos dados de entrada e transferido para o mecanismo chamando _defaultEngine.setAdapter. Esse fragmento já apareceu no programa OLAPRPRT.mq5 do artigo 3, artigo esse em que pela primeira vez testamos a abordagem correta com decomposição em partes universais e complementares. No entanto, naquele momento OLAPRPRT ficou sem interface gráfica, e agora nós tentamos criá-la.

Para demonstrar uma separação estrita dos campos padrão e personalizados dos registros, a classe CustomTradeRecord com o cálculo dos campos MFE e MAE foi movida de OLAPTrades.mqh para OLAPTradesCustom.mqh (não mostrado aqui, os códigos fonte estão anexados). Isso facilita a escrita, se necessário, de outros campos personalizados baseados em transações: basta alterar o algoritmo no OLAPTradesCustom.mqh, deixando o kernel OLAP intocado. Todas as "coisas" padrão: campos de registro de negociação, seletores associados a eles, a classe base TradeRecord, o mecanismo OLAPEngineTrade e o adaptador para o histórico da conta permanecem no OLAPTrades.mqh. Obviamente, para incluir tudo isso no projeto, OLAPTradesCustom.mqh possui um link para OLAPTrades.mqh.

Executamos o EA OLAPGUI.mq5 atualizado e criamos várias fatias de dados para garantir, em primeiro lugar, que o novo princípio de introdução dinâmica de dependências do kernel nos adaptadores de aplicativos e tipos de registro funcione como deveria e, em segundo lugar, que as melhorias no código gráfico forneçam os benefícios visuais esperados .

A maneira mais fácil de ver as alterações é comparando com as mesmas capturas de tela do 2º artigo. Assim fica "Dependência dos campos de lucro e duração para cada trade" - agora a duração ao longo do eixo X é expressa em barras do período gráfico atual (neste caso, é D1), e não em segundos.

Dependência do lucro das transações em relação à duração (em barras do período gráfico atual, D1)

Dependência do lucro das transações em relação à duração (em barras do período gráfico atual, D1)

A distribuição dos lucros por símbolos e dias da semana mostra as barras do histograma e a grade separadas.

Lucros por símbolos e dias da semana

Lucros por símbolos e dias da semana

A análise dos lucros por tamanho de lote nas transações é mostrada na captura de tela a seguir. Diferentemente do artigo 2, os valores do lote são exibidos diretamente no eixo X, e não no log.

Lucro por tamanho de lote

Lucro por tamanho de lote

Por fim, vamos ver como fica o "Número de trades por símbolo e por tipo". Na versão anterior, eu tive que usar o estilo de desenho com linhas, porque os histogramas se sobrepunham. Não temos esse problema aqui.

Número de transações por símbolos e tipos (histograma)

Número de transações por símbolos e tipos (histograma)

Nisso poderíamos dar por concluída a segunda imersão na tarefa de analisar relatórios de negociação, mas para ter uma ideia mais clara, é impossível não mencionar outra fonte de dados que se tornou disponível para programadores MQL. Estamos falando de arquivos tst no formato interno do testador.

Anexando os arquivos de relatório do testador padrão (*.tst)

Os desenvolvedores do MetaTrader 5 recentemente criaram os formatos de arquivo salvos pelo testador. Em particular, os dados sobre passagem única, que ainda poderíamos analisar apenas depois de exportar para um relatório HTML, agora estão disponíveis para leitura diretamente do arquivo tst.

Não vamos nos aprofundar na estrutura interna do arquivo. Felizmente, já existe uma biblioteca para a leitura de arquivos SingleTesterCache (autor — fxsaber). Ao usarmos essa biblioteca com base na "caixa preta", é fácil obter um conjunto de registros de transações. A transação é apresentada na biblioteca pela classe TradeDeal. Para obter sua lista, basta anexar a biblioteca, criar o objeto da classe base SINGLETESTERCACHE e carregar o arquivo necessário usando o método load.

A matriz SingleTesterCache.Deals contém todas as transações, cujas informações estão disponíveis nos campos correspondentes, informações essas que o testador tem.

O algoritmo para gerar posições de negociação com base em transações é exatamente o mesmo que ao importar um relatório HTML. Um bom estilo de POO implica que você precisa remover as partes comuns do código para a classe base e, em seguida, herdar dele o HTMLReportAdapter e o novo TesterReportAdapter.

Fazemos com que a classe BaseReportAdapter se torne herdeiro comum dos relatórios (arquivo ReportCubeBase.mqh). Você pode comparar esse arquivo contextualmente com o antigo arquivo HTMLcube.mqh da encarnação para garantir que haja muito poucas diferenças (além de renomear classes). O principal que chama sua atenção é o conteúdo minimalista do método load, que se transformou num esboço virtual:

Os herdeiros devem substituir esse método.

O código no método generate também mudou, o que realmente converte transações em posições. Agora, no início desse método, é chamado um fillDealsArray virtual "fictício".

Vamos ver como parte do código existente para trabalhar com relatórios HTML foi transferida para novos métodos virtuais na classe HTMLReportAdapter. Note que toda a classe HTMLReportAdapter é completamente apresentada abaixo: como a parte principal do código ficou na classe base, aqui apenas é necessário definir 2 métodos virtuais.

Aqui nada precisa ser alterado uma vez que já conhecermos o código de ambos os métodos graças a versão anterior.

Agora, vejamos a implementação do novo adaptador TesterReportAdapter. Antes de tudo, aqui foi necessário adicionar a classe TesterDeal, derivada da classe Deal, definida em ReportCubeBase.mqh (Deal é uma classe antiga, anteriormente localizada no HTMLcube.mqh). TesterDeal tem um construtor com o parâmetro TradeDeal, que não é outra coisa senão uma transação da biblioteca SingleTesterCache. TesterDeal também define um par de métodos auxiliares para converter enumerações de tipo e direção de transações em strings.

A classe TesterReportAdapter, além dos métodos load e fillDealsArray, contém um ponteiro para o objeto SINGLETESTERCACHE, que é a classe principal da biblioteca SingleTesterCache. É esse objeto que carrega o arquivo tst e, se for bem-sucedido, preenche a matriz Deals, com base na qual nosso método fillDealsArray funciona.

No final, uma instância do adaptador padrão é criada para o tipo de modelo RECORD_CLASS. Lembre-se de que nosso projeto inclui o arquivo OLAPTradesCustom.mqh, que define uma classe personalizada de registro CustomTradeRecord, nela ele também é indicado pela diretiva de pré-processador com a macro RECORD_CLASS. Desse modo, assim que o novo adaptador for anexado ao projeto e o usuário especificar um arquivo tst nos parâmetros de entrada, o adaptador começará a gerar objetos da classe CustomTradeRecord e nossos campos MFE e MAE personalizados serão calculados automaticamente.

Vamos ver como o novo adaptador lida com suas tarefas. Aqui está um exemplo de curvas de saldo discriminadas por símbolos num arquivo tst.

Curvas de saldo por símbolos

Curvas de saldo por símbolos

Observe que as linhas são desenhadas sem interrupções, ou seja, nossa implementação do CGraphicInPlot::LinesPlot funciona corretamente. Ao trabalhar com um agregador "progressivo" (cumulativo), o primeiro seletor deve sempre ser o número de série (ou índice) dos registros.

Relatórios de otimização do testador como uma área complementar da análise OLAP

Além dos arquivos de passagem única do testador, o MetaQuotes também criou o formato de arquivo opt com cache de otimização. Para lê-los foi criada a biblioteca Testercache (é lógico que o autor seja fxsaber) Com base nela, é fácil criar uma camada complementar para análise OLAP dos resultados de otimização. Para fazer isso, é necessário: uma classe de registro com campos que armazenam os dados de cada passagem de otimização, um adaptador e, opcionalmente, seletores. Temos implementações desses componentes para outras áreas complementares, o que nos permite usá-las como um guia (plano). Em seguida, adicionamos uma interface gráfica (formalmente, tudo está pronto e só precisamos alterar as configurações).

Criamos um arquivo OLAPOpts.mqh semelhante - quanto a propósito - ao arquivo OLAPTrades.mqh. Incluímos o arquivo de cabeçalho TesterCache.mqh nele.

Definimos uma enumeração com todos os campos do otimizador. Os campos são tomados da estrutura ExpTradeSummary (ela está no arquivo fxsaber/TesterCache/ExpTradeSummary.mqh, o arquivo é anexado automaticamente à biblioteca).

Aqui existem todos os indicadores usuais, como lucro, queda de saldo e patrimônio, número de operações de negociação, índice de Sharpe, etc. O único campo que nos adicionamos é FIELD_INDEX: número de série do registro. Os campos na estrutura são de vários tipos: long, double, int. No nosso caso, tudo isso fará parte da classe de registro OptCacheRecord herdada de Record e será armazenada em sua matriz double.

Vamos nos comunicar com a biblioteca através de uma estrutura especial OptCacheRecordInternal:

Acontece que cada passagem do testador é caracterizada não apenas por indicadores de desempenho, mas também associada a um determinado conjunto de parâmetros de entrada. Nessa estrutura, os parâmetros de entrada são adicionados após ExpTradeSummary como uma matriz MqlParam. Com essa estrutura, é bastante simples escrever uma classe OptCacheRecord preenchida com dados no formato otimizador.

O método fillByTesterPass mostra claramente a correspondência entre os elementos de enumeração e os campos ExpTradeSummary. O construtor aceita como parâmetro uma estrutura OptCacheRecordInternal preenchida.

O intermediário entre a biblioteca TesterCache e o OLAP será um adaptador de dados especializado. Sua tarefa é gerar registros da classe OptCacheRecord.

O campo size é o número total de registros, cursor é o número do registro atual durante a passagem sequencial de cache, paramCount é o número de parâmetros otimizados e seus nomes são armazenados na matriz paramNames. Variável Cache do tipo TESTERCACHE <ExpTradeSummary> é um objeto de trabalho da biblioteca TesterCache.

A inicialização e a leitura do cache de otimização são feitas nos métodos de reset, load e customize.

O arquivo opt é carregado no método load, onde o método Cache.Load da biblioteca é chamado e, se bem-sucedido, os parâmetros especializados são selecionados no cabeçalho do otimizador (no método auxiliar customize). O método reset simplesmente redefine o número do registro atual, o que aumentará na próxima vez que todos os registros do kernel OLAP forem rastreados usando o método getNext. Neste último, a estrutura OptCacheRecordInternal é preenchida com dados do cache de otimização, após o qual com base nela é criada uma nova entrada para a classe-parâmetro do modelo (T).

O parâmetro do modelo, é claro, é a classe OptCacheRecord descrita acima.

Ele também é definido pela macro como RECORD_CLASS, usado em outras partes do kernel OLAP. Assim fica o diagrama de classes com base nos adaptadores de dados novos e suportados anteriormente.

Diagrama de classes dos adaptadores de dados

Diagrama de classes dos adaptadores de dados

É importante entendermos que tipos de seletores podem ser úteis para analisar os resultados da otimização. A enumeração a seguir é proposta como a primeira opção mínima.

De fato, todos os campos do registro são de um de dois tipos: indicadores de negociação (estatísticas) e parâmetros do EA. Faz sentido organizar os parâmetros em células que correspondem exatamente aos valores verificados. Por exemplo, se entre os parâmetros houver um período de média móvel e para ele foram usados 10 valores, no cubo OLAP deve haver 10 células para esse parâmetro. Isso fornece um seletor de quantização (SELECTOR_QUANTS) com um tamanho zero de "cesta".

Para campos que são indicadores, faz sentido fazer uma distribuição em células com uma determinada etapa. Por exemplo, podemos ver a distribuição de passagens por lucro em incrementos de 100 "unidades convencionais". Mas o seletor de quantização é novamente adequado para isso, embora desta vez precisemos definir um tamanho da "cesta" igual à etapa requerida. Outros seletores adicionados executam funções utilitárias. Portanto, o seletor de número de sequência (SELECTOR_INDEX) é requerido para calcular o total acumulado e o escalar (SELECTOR_SCALAR) permite que obtenhamos um número como uma característica de toda a fatia.

As próprias classes do seletor já estão prontas e estão no arquivo OLAPCommon.mqh.

Para os tipos selecionados de seletores, escrevemos o método "factory" createSelector na especialização de modelo da classe do mecanismo OLAPEngine para otimização:

Ao criar um seletor de quantização, dependendo de se o campo é "padrão" (ou seja, armazena as estatísticas padrão da passagem do testador) ou personalizado (parâmetro do EA), configuramos o tamanho da "cesta" para a variável quantGranularity, ou zero. O campo quantGranularity é descrito na classe base OLAPEngine e podemos configurá-lo no construtor do mecanismo e posteriormente, usando o método setQuant selecionado.

OptCacheSelector é um invólucro simples para BaseSelector <OPT_CACHE_RECORD_FIELDS> .

Interface gráfica do usuário para analisar relatórios de otimização do testador

Para visualizar os resultados da análise de otimização, usamos a mesma interface que estava no caso dos relatórios de negociação. De fato, podemos copiar o arquivo OLAPGUI_Trade.mqh com o novo nome OLAPGUI_Opts.mqh e fazer pequenos ajustes nele. Obviamente, eles se relacionam aos métodos virtuais setup e process.

É importante observar aqui que praticamente não há diferença entre campos e seletores, pois qualquer campo implica um seletor de quantização para o mesmo campo. Em outras palavras, o seletor de quantização é responsável por tudo. Anteriormente, em projetos de relatórios e cotações, tínhamos seletores especiais para campos individuais (como o seletor de lucro, o dia da semana, o tipo de candle, etc.).

Os nomes de todos os elementos das listas suspensas com campos (eles também são seletores nos eixos X, Y, Z) são formados a partir dos nomes dos elementos da enumeração OPT_CACHE_RECORD_FIELDS para estatísticas padrão e da matriz customFields para parâmetros de EAs. Anteriormente, examinamos o método setCustomFields na classe base OLAPDialogBase, que permite preencher a matriz customFields com os nomes do adaptador. Podemos vinculá-los no código do EA analítico OLAPGUI_Opts.mq5 (consulte

Os campos padrão são exibidos na ordem dos elementos de enumeração e depois deles são seguidos pelos campos associados aos parâmetros do EA otimizado e na ordem em que eles são gravados no arquivo opt.

A leitura do status de "controles" e o início do processo de análise são feitos no método process.

Análise OLAP e visualização de relatórios de otimização

O testador de modelo do MetaTrader permite analisar os resultados de otimização de várias maneiras, mas ainda assim são limitadas pelo conjunto padrão. O mecanismo OLAP criado permitirá complementar esta caixa de ferramentas. Por exemplo, a visualização integrada no modo 2D sempre mostra o valor máximo de lucro para combinar dois parâmetros do EA, e geralmente há mais parâmetros. Isso significa que, em cada ponto da superfície, vemos resultados para diferentes combinações de outros parâmetros que não caem no eixo. Como resultado, pode ser gerada uma avaliação excessivamente otimista da rentabilidade de valores específicos dos parâmetros exibidos. Para uma avaliação mais equilibrada, faria sentido olhar para o valor médio do lucro e sua amplitude. Com ajuda do OLAP podemos fazer isso e muito mais.

Delegamos a análise OLAP de relatórios de otimização ao novo EA não-negociante OLAPGUI_Opts.mq5. Sua estrutura repete totalmente a do OLAPGUI.mq5, sendo que até é mesmo mais simples, porque não é necessário anexar os diversos adaptadores dependendo do tipo de arquivo fornecido. Para os resultados de otimização, sempre será um arquivo opt.

Nos parâmetros de entrada, indicamos o nome do arquivo para análise, bem como o tamanho do incremento da quantização para os parâmetros estatísticos.

Deve-se notar que é desejável que cada campo tenha sua própria incremento de quantização, mas até agora é definido apenas um e não muda da interface gráfica. Essa é uma das áreas do projeto que queremos melhorar no futuro. Agora, devemos lembrar que o tamanho do incremento pode ser adequado para um campo e não para outro (muito grande ou muito pequeno). Portanto, se necessário, precisamos chamar a caixa de diálogo de propriedades do EA para alterar o quantum antes de selecionar um campo na lista suspensa da interface OLAP.

Depois de incluir os arquivos de cabeçalho em todas as classes, criamos uma instância da caixa de diálogo e a vinculamos ao mecanismo OLAP.

No manipulador OnInit, anexamos o novo adaptador ao mecanismo e iniciamos o carregamento de dados do arquivo.

Vamos tentar criar algumas seções analíticas para o arquivo Integrity.opt e QuantGranularity = 100. Durante a otimização, foram selecionados os três parâmetros PricePeriod, Momentum, Sigma.

Assim fica o lucro médio distribuído de acordo com o valor do parâmetro PricePeriod.

Lucro médio, dependendo do valor do parâmetro do EA

Lucro médio, dependendo do valor do parâmetro do EA

Sem dispersão, isso por si só diz pouco.

Dispersão do lucro, dependendo do valor do parâmetro expert

Dispersão do lucro, dependendo do valor do parâmetro expert

Após compararmos estes dois histogramas, podemos fazer uma estimativa dos valores do parâmetro em que o spread não excede a média, indicando break-even. É recomendável fazer isso automaticamente no mesmo gráfico, mas esta tarefa está além do alcance deste artigo.

Como alternativa, podemos fazer uma agir e ver a lucratividade pelo mesmo parâmetro (taxa de lucro/perda para todas as passagens).

Rentabilidade da estratégia (fator de lucro), dependendo do valor do parâmetro do EA

Rentabilidade da estratégia (fator de lucro), dependendo do valor do parâmetro do EA

O tamanho médio do período distribuído de acordo com os níveis de lucro em incrementos de 100 (que definimos no parâmetro de entrada QuantGranularity) oferece uma visão desconfiável.

O valor médio do parâmetro para obter lucro em vários intervalos (em incrementos de 100 ye)

O valor médio do parâmetro para obter lucro em vários intervalos (em incrementos de 100 ye)

Assim fica a distribuição de lucros, dependendo do período (são mostradas todas as passagens graças ao agregador identity).

Lucro vs valor do parâmetro para todas as posições

Lucro vs valor do parâmetro para todas as posições

A distribuição do lucro pelos parâmetros Momentum e Sigma fica assim.

Lucro médio com base em dois parâmetros

Lucro médio com base em dois parâmetros

Para ver a distribuição total dos lucros por níveis em incrementos de 100, selecionamos o campo profit nas estatísticas ao longo do eixo X e no agregador count.

Distribuição de lucros por intervalos em incrementos de 100 ye

Distribuição dos lucros de todos os passes por intervalos em incrementos de 100 ye

Por fim, o agregador identity permite avaliar o impacto do número de trades no lucro. Em princípio, esse agregador permite ver visualmente muitos outros padrões.

Lucro vs número de transações

Lucro vs número de transações

Fim do artigo

Neste artigo, expandimos o escopo do OLAP MQL para relatórios de testadores em passagens únicas e otimização. A estrutura de classes atualizada nos permite aumentar ainda mais o arsenal de ferramentas OLAP. Certamente, a implementação proposta não é exaustiva e pode ser bastante aprimorada (em particular, em termos de visualização 3D, incluindo configurações de filtragem e quantização em diferentes eixos na GUI interativa), mas ao mesmo tempo serve como um conjunto inicial mínimo, com base no qual é muito mais simples entrar no mundo do OLAP. Com sua ajuda, um trader pode processar grandes volumes de dados brutos e extrair novos conhecimentos para a tomada de decisões.

Arquivos anexados:

Interface gráfica

Patch de biblioteca padrão