Analisando resultados de negociação usando relatórios HTML

Dmitry Fedoseev | 25 fevereiro, 2019

Introdução

Se um trader está tentando atrair investidores, é muito provável que eles desejem verificar seus resultados de negociação. Assim, um trader deve apresentar seu histórico de negociação para demonstrar os resultados. A MetaTrader 5 permite salvar o histórico de negociação em um arquivo (Caixa de ferramentas - aba Negociação - menu de contexto - Relatório). Um relatório pode ser salvo como XLSX (para ser analisado no Microsoft Excel) e como arquivos HTML que podem ser visualizados em qualquer navegador. A segunda opção é obviamente mais popular, já que alguns usuários podem não ter o Excel, enquanto todos têm um navegador. Portanto, um relatório HTML com resultados de negociação seria mais adequado para um potencial investidor.

Além das métricas padrão disponíveis em tais relatórios, você pode querer calcular seus próprios valores usando dados de negociação, que podem ser extraídos de um relatório. Neste artigo, vamos considerar os métodos para extrair dados de relatórios HTML. Primeiro, vamos examinar a estrutura dos relatórios e depois escreveremos uma função para analisá-los, o nome do relatório será colocado na função. Em seguida, a função retornará uma estrutura com os dados importantes, essa estrutura permitirá o acesso direto a qualquer relatório. Ao utilizar essa estrutura, você poderá gerar com facilidade e rapidez seus próprios relatórios com as métricas desejadas.

Além dos relatórios de negociação, a MetaTrader 5 permite salvar relatórios de testes e otimizações de um Expert Advisor. Similarmente ao histórico de negociação, um relatório de teste pode ser salvo em dois formatos: XLSX e HTML, enquanto o relatório de otimização é salvo em XML.

Neste artigo, analisamos o relatório de teste HTML, o relatório de otimização XML e o relatório de histórico de negociação HTML.

Arquivos HTML e XML

Um arquivo HTML é na verdade um arquivo de texto composto de texto (dados exibidos) e tags, que indicam como os dados devem ser exibidos (Fig. 1), qualquer tag começa com o caractere "<" e termina com ">". Por exemplo, a tag <br> significa que o texto seguinte deve começar com uma nova linha, enquanto que <p> significa que o texto deve começar com um novo parágrafo (não apenas uma nova linha, mas depois uma linha vazia). Atributos adicionais podem ser localizados dentro das tags, por exemplo, <p color="red"> significa que, após a tag, o texto deve começar com um novo parágrafo e será escrito em vermelho. 

Arquivo HTML no bloco de notas
Fig. 1. Um fragmento do arquivo HTML aberto no bloco de notas

Para cancelar a ação da tag, uma tag de fechamento é usada, que é semelhante à de abertura, sendo acrescentado uma barra "/" no início. Por exemplo, </p> é a tag de fechamento de um parágrafo. Algumas tags são usadas sem o fechamento de contrapartes, como a <br>. Algumas tags podem ser usadas opcionalmente com uma tag de fechamento. Um novo parágrafo pode ser iniciado sem fechar o anterior. No entanto, se um atributo de cor for usado dentro da tag, como no exemplo acima com <p>, uma tag de fechamento deve ser usada para cancelar a coloração adicional do texto. Existem tags para as quais é necessário o fechamento, como a <table>, uma tabela deve sempre terminar com uma tag de fechamento </table>. A tabela consiste em linhas indicadas pelas tags de abertura <tr> e de fechamento </tr>. Células dentro das linhas são definidas com <td> e </td>, às vezes, as células são marcadas com <th> (célula de cabeçalho). Uma mesma tabela pode ter ambas as células marcadas com <td> e com <th>. As tags de fechamento de linha e célula são obrigatórias.

Atualmente, a maioria dos atributos html praticamente não são usados, ao invés destes, é utilizado um atributo "style", na qual a aparência do elemento é descrita. Por exemplo: <p style="color: red"> denota um parágrafo em vermelho, mas este também não é um método muito popular. A maneira mais comum de utilização é o atributo "class", no qual é especificado apenas o nome da classe de estilo, como <p class="small">, enquanto a própria classe (descrição de estilo) está localizada no início do HTML documento ou em um arquivo CSS separado (Planilha em Estilo Cascata). 

Arquivos XML (Fig. 2) são muito semelhantes ao HTML, a principal diferença é que o HTML suporta um conjunto estritamente limitado de tags, enquanto o XML permite a expansão do conjunto de tags e a introdução de tags personalizadas.

Arquivo XML no bloco de notas
Fig. 2. Um fragmento do arquivo XML aberto no bloco de notas

O principal objetivo do HTML é a exibição de dados, é por isso que ele tem um conjunto padrão de tags, devido a isso, os documentos HTML têm quase a mesma aparência em diferentes navegadores. O objetivo principal do XML é salvar e transmitir dados, portanto, a possibilidade de estruturação arbitrária de dados é importante, o usuário que trabalha com esse arquivo deve entender o propósito de utilizá-lo e os dados que precisam ser extraídos.

Funções de string ou expressões regulares

Geralmente, expressões regulares são usadas para extrair dados do texto. No CodeBase, você pode encontrar a biblioteca RegularExpressions para trabalhar com as expressões, você também pode verificar o artigo: "Expressões regulares para traders". Naturalmente, expressões regulares constituem uma ferramenta muito poderosa e adequada para analisar dados de texto. Se você costuma lidar com a análise de dados, enquanto executa tarefas diferentes, isso definitivamente requer o uso de expressões regulares.

No entanto, existem algumas desvantagens das expressões regulares: você precisa estudar as expressões corretamente antes de usá-las. Todas ideias relacionadas a expressões regulares são bastante extensas, Você não pode usá-las apenas com a intenção de "verificar a referência quando necessário", você deve estudar minuciosamente a teoria e ter habilidades práticas. Eu penso que a maneira necessária para o uso das expressões regulares difere muito do necessidade das resoluções comuns de tarefas de programação. Mesmo que alguém tenha a capacidade de usar expressões regulares, podem surgir dificuldades na alternância de ida e volta no trabalho com expressões regulares. 

Se as tarefas de análise de dados não são difíceis e não são frequentes, você poderá usar as funções string padrão, também, existe uma opinião de que as funções string operam com rapidez. Muito provavelmente, a velocidade depende do tipo de tarefa e das condições para aplicar expressões regulares, porém as funções de string fornecem uma boa solução para a tarefa de análise de dados.

De todas as funções string disponíveis na referência da linguagem de programação, precisaremos apenas de algumas: Stringring(), StringSubstr() e StringReplace(). também podemos precisar de algumas funções muito simples, como StringLen(), StringTrimLeft() e StringTrimRight().

Relatório do testador

O maior volume de dados está contido no relatório do testador, por isso começaremos com ele, o que nos permitirá entender a tarefa e lidar com todas as dificuldades que possam surgir. Ao criar este artigo, usei o relatório de teste do EA ExpertMACD, com os parâmetros padrão (o relatório anexo no final do artigo). Abra o relatório no navegador para verificar com o que estamos trabalhando (Fig. 3).

Seções de relatório HTML
Fig. 3. Relatório de teste em HTML, aberto no navegador. Linhas vermelhas com texto azul mostram as seções principais do relatório.

Os dados do relatório são divididos em várias seções: nome do relatório, parâmetros, resultados, gráficos, ordens, negociações, total, etc.  

Cada navegador tem um comando para visualizar o código da página: clique com o botão direito do mouse em uma página para abrir o menu de contexto e selecione "Visualizar o código da página" ou um comando semelhante. 

A partir da inspeção visual do código-fonte, podemos descobrir que todos os dados do relatório estão organizados em tabelas (a tag <table>). O relatório do testador tem duas tabelas. A primeira tabela contém dados gerais: do nome do relatório para o valor "Tempo médio de espera de posição", ou seja, até a seção de Ordens. A segunda tabela contém dados sobre ordens, negociações e o estado final do depósito. Dados de tipos diferentes são colocados dentro de uma tabela, usando o atributo "colspan", que une várias células. Os nomes dos parâmetros comuns e seus valores estão localizados em diferentes células da tabela, às vezes essas células aparecem na mesma linha, às vezes elas estão localizadas em linhas diferentes.

Um momento importante é a disponibilidade dos atributos "id", que são identificadores exclusivos de tags. Os valores de id podem ser usados para localizar células contendo os dados necessários, porém os atributos "id" não são usados, é por isso que vamos encontrar parâmetros contando linhas e células. Por exemplo, o nome "Relatório do Testador de Estratégia" está na primeira tabela, na primeira linha e na primeira célula. 

O número de ordens e de negociações na segunda tabela serão diferentes em todos os relatórios, por isso precisamos encontrar o final das ordens e o início das negociações. Uma linha com uma célula segue as negociações e depois vem uma linha com cabeçalhos - isso é uma indicação para separação de dados. Agora não entraremos em detalhes sobre o número de linhas e células, uma maneira apropriada será apresentada mais tarde.

Todos os dados estão localizados em células da tabela, logo, tudo o que precisamos fazer no estágio inicial é extrair tabelas, linhas e células.

Vamos analisar o histórico de negociação e os relatórios de otimização, o relatório de histórico é muito semelhante ao relatório de teste, exceto por uma seção a mais, o resultado final do depósito (Fig. 4).

Relatório de histórico de negociação em HTML, aberto no navegador.

Fig. 4. Relatório de histórico de negociação em HTML, aberto no navegador. Linhas vermelhas com texto azul mostram as seções principais do relatório.

Examinando o código HTML do histórico de negociações, podemos ver que as células não são definidas apenas por tags <td>, mas também por células de cabeçalho <th>, além disso, todos os dados estão localizados em uma tabela.

O relatório de otimização é muito simples - tem uma seção de dados com uma tabela, a partir do seu código-fonte, vemos que ele é criado usando a tag <Table>, as linhas são definidas com <Row> e as células são definidas com <Cell> (todas as tags são usadas junto com tags apropriadas de fechamento).

Download de arquivo

Antes realizarmos quaisquer operações com os dados do relatório, precisamos lê-los a partir do arquivo. Vamos usar a seguinte função que retorna todo o conteúdo do arquivo em uma variável string:

bool FileGetContent(string aFileName,string & aContent){
   int h=FileOpen(aFileName,FILE_READ|FILE_TXT);
   if(h==-1)return(false);
   aContent="";
   while(!FileIsEnding(h)){
      aContent=aContent+FileReadString(h);
   }
   FileClose(h);
   return(true);
}

O primeiro parâmetro transferido para a função é o nome do arquivo do relatório. O segundo parâmetro é a variável que conterá o conteúdo do arquivo. A função retorna verdadeiro/falso dependendo do resultado.

Extraindo as tabelas

Vamos usar duas estruturas para localizar os dados da tabela. A estrutura contendo uma linha da tabela:

struct Str{
   string td[];
};

Cada elemento do array td[] conterá o conteúdo de uma célula.

A estrutura que contém a tabela inteira (todas as linhas):

struct STable{
   Str tr[];
};

Os dados serão extraídos do relatório da seguinte maneira: primeiro, encontraremos a tabela iniciando com a tag de abertura, como as tags podem ter atributos, somente buscaremos o início da tag: "<table". Depois de encontrar o início da tag de abertura, vamos encontrar o seu fim, ">". Para então procurar a extremidade da tabela, ou seja, a tag de fechamento "</table>". Isso é fácil, porque os relatórios não possuem tabelas aninhadas, ou seja, cada tag de abertura é seguida por uma tag de fechamento.

Depois de encontrar a tabela, vamos repetir o mesmo para linhas, usando "<tr" para o início e "</tr" para o fim. Em seguida, em cada linha encontraremos o início ou as células com "<td" e o final com </td>. A tarefa é um pouco mais complicada com as linhas, porque uma célula pode ter as tags <td> e <th>. Por esse motivo, usaremos a delimitação personalizada TagFind(), em vez de StringFind():

int TagFind(string aContent,string & aTags[],int aStart,string & aTag){
   int rp=-1;
   for(int i=0;i<ArraySize(aTags);i++){
      int p=StringFind(aContent,"<"+aTags[i],aStart);
      if(p!=-1){
         if(rp==-1){
            rp=p;
            aTag=aTags[i];
         }
         else{
            if(p<rp){
               rp=p;
               aTag=aTags[i];
            }
         }      
      }
   }
   return(rp);
}

Parâmetros da função:

  • string aContent — a string na qual pesquisamos;
  • string & aTags[] — um array com tags;
  • int aStart — a posição para iniciar a pesquisa;
  • string & aTag — a tag encontrada é retorna pela referência.

Ao contrário da StringFind(), um array é passado para a função TagFind(), em vez da string pesquisada. Abertura "<" é acescentada para pesquisar as strings de caracteres na função. A função retorna a posição da tag mais próxima.

Usando a TagFind(), procuramos consistentemente abrir e fechar tags, tudo encontrado entre eles, então será coletado para um array:

int TagsToArray(string aContent,string & aTags[],string & aArray[]){
   ArrayResize(aArray,0);
   int e,s=0;
   string tag;
   while((s=TagFind(aContent,aTags,s,tag))!=-1 && !IsStopped()){  
      s=StringFind(aContent,">",s);
      if(s==-1)break;
      s++; 
      e=StringFind(aContent,"</"+tag,s);   
      if(e==-1)break;  
      ArrayResize(aArray,ArraySize(aArray)+1);
      aArray[ArraySize(aArray)-1]=StringSubstr(aContent,s,e-s);  
   }
   return(ArraySize(aArray));
}

Parâmetros da função:

  • string aContent — a string na qual pesquisamos;
  • string & aTags[] — um array com tags;
  • string aArray[] — um array com o conteúdo de todas as tags encontradas retorna pela referência.

Um array de tags pesquisadas é transfeida para TagsToArray(), somente ao analisar células, por isso que, para todos os outros casos, escreveremos um análogo da função com um parâmetro de string usual:

int TagsToArray(string aContent,string aTag,string & aArray[]){
   string Tags[1];
   Tags[0]=aTag;
   return(TagsToArray(aContent,Tags,aArray));
}

Parâmetros da função:

  • string aContent — a string na qual pesquisamos;
  • string & aTag — tag pesquisada;
  • string aArray[] — um array com o conteúdo de todas as tags encontradas retorna pela referência.

Agora vamos prosseguir para a função que extrai o conteúdo da tabela, o parâmetro da string aFileName com o nome do arquivo analisado é tranferido para a função. Uma variável de string local para o conteúdo do arquivo e um array local de estruturas STable será usada na função:

STable tables[];
string FileContent;

Obtenha todo o conteúdo do relatório usando a função FileGetContent:

if(!FileGetContent(aFileName,FileContent)){
   return(true);
}

Aqui estão as variáveis auxiliares:

string tags[]={"td","th"};
string ttmp[],trtmp[],tdtmp[];
int tcnt,trcnt,tdcnt;

Possíveis opções para as tags de célula são fornecidas no array 'tags'. O conteúdo das tabelas, linhas e células será temporariamente colocado nos arrays de string ttmp[], trtmp[] e tdtmp[] respectivamente. 

Recuperando dados da tabela:

tcnt=TagsToArray(FileContent,"table",ttmp);
ArrayResize(tables,tcnt);

Faça um loop em todas as tabelas e extraia as linhas, depois extraia as células das linhas:

for(int i=0;i<tcnt;i++){
   trcnt=TagsToArray(ttmp[i],"tr",trtmp);
   ArrayResize(tables[i].tr,trcnt);      
   for(int j=0;j<trcnt;j++){         
      tdcnt=TagsToArray(trtmp[j],tags,tdtmp);
      ArrayResize(tables[i].tr[j].td,tdcnt);
      for(int k=0;k<tdcnt;k++){  
         tables[i].tr[j].td[k]=RemoveTags(tdtmp[k]);
      }
   }
} 

Tabelas e linhas são primeiramente recebidos como arrays temporários e, em seguida, os arrays das células são copiados dentro da estrutura. Durante a cópia, uma célula é filtrada pela função RemoveTags(), as células da tabela podem ter tags aninhadas. O RemoveTags() os exclui deixando apenas as tags necessárias.

A função RemoveTags():

string RemoveTags(string aStr){
   string rstr="";
   int e,s=0;
   while((e=StringFind(aStr,"<",s))!=-1){
      if(e>s){

         rstr=rstr+StringSubstr(aStr,s,e-s);
      }
      s=StringFind(aStr,">",e);
      if(s==-1)break;
      s++;
   }
   if(s!=-1){
      rstr=rstr+StringSubstr(aStr,s,StringLen(aStr)-s);
   }
   StringReplace(rstr,"&nbsp;"," ");
   while(StringReplace(rstr,"  "," ")>0);
   StringTrimLeft(rstr);
   StringTrimRight(rstr);
   return(rstr);
}

Vamos considerar a função RemoveTags(). A variável "s" é usada para o início dos dados usados, os dados são primeiro iguais a 0, porque os dados podem começar a partir do início da linha. O colchete angular de abertura "<" significa que o início da marcação é pesquisado no ciclo "while". Quando o início da tag é encontrado, todos os dados, a partir da posição especificada na variável "s" até a posição encontrada, são copiados para a variável "rstr". Depois que o final da tag for pesquisado, novos dados poderão ser usados, após o loop, se o valor da variável "s" não for igual a -1 (isso significa que a string termina com dados úteis, mas eles não foram copiados), os dados são copiados para a variável "rstr". No final da função, o caractere de espaço &nbsp, é substituído por um espaço simples, enquanto espaços repetidos e espaços no início e no final da string são excluídos.

Nesta etapa, temos o array "tables" de estruturas preenchidas com dados puros da tabela, vamos salvar este array para um arquivo de texto. Ao salvar, definimos números para as tabelas, linhas e células (os dados são salvos no arquivo 1.txt):

int h=FileOpen("1.txt",FILE_TXT|FILE_WRITE);

for(int i=0;i<ArraySize(tables);i++){
   FileWriteString(h,"table "+(string)i+"\n");
   for(int j=0;j<ArraySize(tables[i].tr);j++){      
      FileWriteString(h,"   tr "+(string)j+"\n");         
      for(int k=0;k<ArraySize(tables[i].tr[j].td);k++){
         FileWriteString(h,"      td "+(string)k+": "+tables[i].tr[j].td[k]+"\n");
      }
   }
}

FileClose(h);

A partir desse arquivo, podemos entender facilmente em quais células os dados estão localizados. Abaixo está o fragmento de arquivo:

table 0
   tr 0
      td 0: Strategy Test report
   tr 1
      td 0: IMPACT-Demo (Build 1940)
   tr 2
      td 0: 
   tr 3
      td 0: Settings
   tr 4
      td 0: Expert Advisor:
      td 1: ExpertMACD
   tr 5
      td 0: Symbol:
      td 1: USDCHF
   tr 6
      td 0: Period:
      td 1: H1 (2018.11.01 - 2018.12.01)
   tr 7
      td 0: Parameters:
      td 1: Inp_Expert_Title=ExpertMACD
   tr 8
      td 0: 
      td 1: Inp_Signal_MACD_PeriodFast=12
   tr 9
      td 0: 
      td 1: Inp_Signal_MACD_PeriodSlow=24
   tr 10
      td 0: 
      td 1: Inp_Signal_MACD_PeriodSignal=9
   tr 11

Estrutura para os dados do relatório

Agora temos um pouco de rotina chata: precisamos escrever uma estrutura correspondente aos dados do relatório e preencher essa estrutura com dados do array de tabelas. O relatório está dividido em várias seções. Portanto, usaremos várias estruturas combinadas em uma estrutura geral.

Estrutura para a seção Configurações:

struct SSettings{
   string Expert;
   string Symbol;
   string Period;
   string Inputs;
   string Broker;
   string Currency;
   string InitialDeposit;
   string Leverage;
};

O objetivo dos campos da estrutura é claro a partir de seus nomes. Todos os campos da estrutura conterão os dados exatamente como aparecem no relatório. A lista de parâmetros do Expert Advisor será localizada em uma variável de string "Inputs".

Estrutura para a seção de dados de resultados:

struct SResults{
   string HistoryQuality;
   string Bars;
   string Ticks;
   string Symbols;
   string TotalNetProfit;
   string BalanceDrawdownAbsolute;
   string EquityDrawdownAbsolute;
   string GrossProfit;
   string BalanceDrawdownMaximal;
   string EquityDrawdownMaximal;
   string GrossLoss;
   string BalanceDrawdownRelative;
   string EquityDrawdownRelative;
   string ProfitFactor;
   string ExpectedPayoff;
   string MarginLevel;
   string RecoveryFactor;
   string SharpeRatio;
   string ZScore;
   string AHPR;
   string LRCorrelation;
   string OnTesterResult;
   string GHPR;
   string LRStandardError;
   string TotalTrades;
   string ShortTrades_won_pers;
   string LongTrades_won_perc;
   string TotalDeals;
   string ProfitTrades_perc_of_total;
   string LossTrades_perc_of_total;
   string LargestProfitTrade;
   string LargestLossTrade;
   string AverageProfitTrade;
   string AverageLossTrade;
   string MaximumConsecutiveWins_cur;
   string MaximumConsecutiveLosses_cur;
   string MaximalConsecutiveProfit_count;
   string MaximalConsecutiveLoss_count;
   string AverageConsecutiveWins;
   string AverageConsecutiveLosses;
   string Correlation_Profits_MFE;
   string Correlation_Profits_MAE;
   string Correlation_MFE_MAE;      
   string MinimalPositionHoldingTime;
   string MaximalPositionHoldingTime;
   string AveragePositionHoldingTime;
};

Estrutura para dados relativos a um pedido:

struct SOrder{
   string OpenTime;
   string Order;
   string Symbol;
   string Type;
   string Volume;
   string Price;
   string SL;
   string TP;
   string Time;
   string State;
   string Comment;
};

Estrutura para dados referentes a uma oferta:

struct SDeal{
   string Time;
   string Deal;
   string Symbol;
   string Type;
   string Direction;
   string Volume;
   string Price;
   string Order;
   string Commission;
   string Swap;
   string Profit;
   string Balance;
   string Comment;
};

Estrutura para o resultado final do depósito:

struct SSummary{
   string Commission;
   string Swap;
   string Profit;
   string Balance;
};

Estrutura geral:

struct STestingReport{
   Configurações SSettings;
   Resultados SResults;
   SOrder Orders[];
   SDeal Deals[];
   Sumário SSummary;
};

Os arrays de estrutura SOrder e SDeals são usados para ordens e ofertas.

Preenchendo a estrutura com dados

Este pequeno trabalho de rotina também requer atenção. Visualize o arquivo de texto recebido anteriormente com os dados da tabela e preencha a estrutura:

aTestingReport.Settings.Expert=tables[0].tr[4].td[1];
aTestingReport.Settings.Symbol=tables[0].tr[5].td[1];
aTestingReport.Settings.Period=tables[0].tr[6].td[1];
aTestingReport.Settings.Inputs=tables[0].tr[7].td[1];

Na última linha, no exemplo de código acima, um valor é atribuído ao campo Inputs, mas depois disso, o campo só armazenará dados de um parâmetro, então outros parâmetros são coletados. Esses parâmetros estão localizados iniciando com a linha 8, enquanto a primeira célula em cada linha de parâmetro está vazia. O loop é executado enquanto a primeira célula da linha estiver vazia:

int i=8;
while(i<ArraySize(tables[0].tr) && tables[0].tr[i].td[0]==""){
   aTestingReport.Settings.Inputs=aTestingReport.Settings.Inputs+", "+tables[0].tr[i].td[1];
   i++;
}

Abaixo está a função ompleta de análise do relatório de teste:

bool TestingHTMLReportToStruct(string aFileName,STestingReport & aTestingReport){

   STable tables[];

   string FileContent;
   
   if(!FileGetContent(aFileName,FileContent)){
      return(true);
   }

   string tags[]={"td","th"};
   string ttmp[],trtmp[],tdtmp[];
   int tcnt,trcnt,tdcnt;
   
   tcnt=TagsToArray(FileContent,"table",ttmp);

   ArrayResize(tables,tcnt);
   
   for(int i=0;i<tcnt;i++){
      trcnt=TagsToArray(ttmp[i],"tr",trtmp);
      ArrayResize(tables[i].tr,trcnt);      
      for(int j=0;j<trcnt;j++){         
         tdcnt=TagsToArray(trtmp[j],tags,tdtmp);
         ArrayResize(tables[i].tr[j].td,tdcnt);
         for(int k=0;k<tdcnt;k++){  
            tables[i].tr[j].td[k]=RemoveTags(tdtmp[k]);
         }
      }
   }   
   
   // seção de configurações
   
   aTestingReport.Settings.Expert=tables[0].tr[4].td[1];
   aTestingReport.Settings.Symbol=tables[0].tr[5].td[1];
   aTestingReport.Settings.Period=tables[0].tr[6].td[1];
   aTestingReport.Settings.Inputs=tables[0].tr[7].td[1];
   int i=8;
   while(i<ArraySize(tables[0].tr) && tables[0].tr[i].td[0]==""){
      aTestingReport.Settings.Inputs=aTestingReport.Settings.Inputs+", "+tables[0].tr[i].td[1];
      i++;
   }
   aTestingReport.Settings.Broker=tables[0].tr[i++].td[1];
   aTestingReport.Settings.Currency=tables[0].tr[i++].td[1];  
   aTestingReport.Settings.InitialDeposit=tables[0].tr[i++].td[1];
   aTestingReport.Settings.Leverage=tables[0].tr[i++].td[1];   
   
   // seção de resultados
   
   i+=2;
   aTestingReport.Results.HistoryQuality=tables[0].tr[i++].td[1];
   aTestingReport.Results.Bars=tables[0].tr[i].td[1];
   aTestingReport.Results.Ticks=tables[0].tr[i].td[3];
   aTestingReport.Results.Symbols=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.TotalNetProfit=tables[0].tr[i].td[1];
   aTestingReport.Results.BalanceDrawdownAbsolute=tables[0].tr[i].td[3];
   aTestingReport.Results.EquityDrawdownAbsolute=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.GrossProfit=tables[0].tr[i].td[1];
   aTestingReport.Results.BalanceDrawdownMaximal=tables[0].tr[i].td[3];
   aTestingReport.Results.EquityDrawdownMaximal=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.GrossLoss=tables[0].tr[i].td[1];
   aTestingReport.Results.BalanceDrawdownRelative=tables[0].tr[i].td[3];
   aTestingReport.Results.EquityDrawdownRelative=tables[0].tr[i].td[5];
   i+=2;
   aTestingReport.Results.ProfitFactor=tables[0].tr[i].td[1];
   aTestingReport.Results.ExpectedPayoff=tables[0].tr[i].td[3];
   aTestingReport.Results.MarginLevel=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.RecoveryFactor=tables[0].tr[i].td[1];
   aTestingReport.Results.SharpeRatio=tables[0].tr[i].td[3];
   aTestingReport.Results.ZScore=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.AHPR=tables[0].tr[i].td[1];
   aTestingReport.Results.LRCorrelation=tables[0].tr[i].td[3];
   aTestingReport.Results.tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.GHPR=tables[0].tr[i].td[1];
   aTestingReport.Results.LRStandardError=tables[0].tr[i].td[3];
   i+=2;
   aTestingReport.Results.TotalTrades=tables[0].tr[i].td[1];
   aTestingReport.Results.ShortTrades_won_pers=tables[0].tr[i].td[3];
   aTestingReport.Results.LongTrades_won_perc=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.TotalDeals=tables[0].tr[i].td[1];
   aTestingReport.Results.ProfitTrades_perc_of_total=tables[0].tr[i].td[3];
   aTestingReport.Results.LossTrades_perc_of_total=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.LargestProfitTrade=tables[0].tr[i].td[2];
   aTestingReport.Results.LargestLossTrade=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.AverageProfitTrade=tables[0].tr[i].td[2];
   aTestingReport.Results.AverageLossTrade=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.MaximumConsecutiveWins_cur=tables[0].tr[i].td[2];
   aTestingReport.Results.MaximumConsecutiveLosses_cur=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.MaximalConsecutiveProfit_count=tables[0].tr[i].td[2];
   aTestingReport.Results.MaximalConsecutiveLoss_count=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.AverageConsecutiveWins=tables[0].tr[i].td[2];
   aTestingReport.Results.AverageConsecutiveLosses=tables[0].tr[i].td[4];    
   i+=6;
   aTestingReport.Results.Correlation_Profits_MFE=tables[0].tr[i].td[1];
   aTestingReport.Results.Correlation_Profits_MAE=tables[0].tr[i].td[3];
   aTestingReport.Results.Correlation_MFE_MAE=tables[0].tr[i].td[5];    
   i+=3;
   aTestingReport.Results.MinimalPositionHoldingTime=tables[0].tr[i].td[1];
   aTestingReport.Results.MaximalPositionHoldingTime=tables[0].tr[i].td[3];
   aTestingReport.Results.AveragePositionHoldingTime=tables[0].tr[i].td[5];   
   
   // ordens

   ArrayFree(aTestingReport.Orders);
   int ocnt=0;
   for(i=3;i<ArraySize(tables[1].tr);i++){
      if(ArraySize(tables[1].tr[i].td)==1){
         break;
      }   
      ArrayResize(aTestingReport.Orders,ocnt+1);
      aTestingReport.Orders[ocnt].OpenTime=tables[1].tr[i].td[0];
      aTestingReport.Orders[ocnt].Order=tables[1].tr[i].td[1];
      aTestingReport.Orders[ocnt].Symbol=tables[1].tr[i].td[2];
      aTestingReport.Orders[ocnt].Type=tables[1].tr[i].td[3];
      aTestingReport.Orders[ocnt].Volume=tables[1].tr[i].td[4];
      aTestingReport.Orders[ocnt].Price=tables[1].tr[i].td[5];
      aTestingReport.Orders[ocnt].SL=tables[1].tr[i].td[6];
      aTestingReport.Orders[ocnt].TP=tables[1].tr[i].td[7];
      aTestingReport.Orders[ocnt].Time=tables[1].tr[i].td[8];
      aTestingReport.Orders[ocnt].State=tables[1].tr[i].td[9];
      aTestingReport.Orders[ocnt].Comment=tables[1].tr[i].td[10];      
      ocnt++;
   }
   
   // negociações
   
   i+=3;
   ArrayFree(aTestingReport.Deals);
   int dcnt=0;
   for(;i<ArraySize(tables[1].tr);i++){
      if(ArraySize(tables[1].tr[i].td)!=13){
         if(ArraySize(tables[1].tr[i].td)==6){
            aTestingReport.Summary.Commission=tables[1].tr[i].td[1];
            aTestingReport.Summary.Swap=tables[1].tr[i].td[2];
            aTestingReport.Summary.Profit=tables[1].tr[i].td[3];
            aTestingReport.Summary.Balance=tables[1].tr[i].td[4];            
         }
         break;
      }   
      ArrayResize(aTestingReport.Deals,dcnt+1);   
      aTestingReport.Deals[dcnt].Time=tables[1].tr[i].td[0];
      aTestingReport.Deals[dcnt].Deal=tables[1].tr[i].td[1];
      aTestingReport.Deals[dcnt].Symbol=tables[1].tr[i].td[2];
      aTestingReport.Deals[dcnt].Type=tables[1].tr[i].td[3];
      aTestingReport.Deals[dcnt].Direction=tables[1].tr[i].td[4];
      aTestingReport.Deals[dcnt].Volume=tables[1].tr[i].td[5];
      aTestingReport.Deals[dcnt].Price=tables[1].tr[i].td[6];
      aTestingReport.Deals[dcnt].Order=tables[1].tr[i].td[7];
      aTestingReport.Deals[dcnt].Commission=tables[1].tr[i].td[8];
      aTestingReport.Deals[dcnt].Swap=tables[1].tr[i].td[9];
      aTestingReport.Deals[dcnt].Profit=tables[1].tr[i].td[10];
      aTestingReport.Deals[dcnt].Balance=tables[1].tr[i].td[11];
      aTestingReport.Deals[dcnt].Comment=tables[1].tr[i].td[12];
      dcnt++;
   }
   return(true);
}

O nome do arquivo de relatório é transferido para a função, também a estrutura STestingReport a ser preenchida na função é passada por referência.

Observe as partes do código que começam com comentários "Orders" e "Deals". O número da linha com o início da lista de ordens já está definido, enquanto que o final da lista de ordens é determinado com base numa linha com uma única célula:

if(ArraySize(tables[1].tr[i].td)==1){
   break;
}  

Três linhas são transpostas após as ordens:

// negociações
   
i+=3;

Depois disso, os dados sobre negociações são coletados, o final da lista das negociações é determinado por uma linha com 6 células - essa linha contém dados sobre a condição final do depósito. Antes de sair do loop, a estrutura do resultado final do depósito é preenchida:

if(ArraySize(tables[1].tr[i].td)!=13){
   if(ArraySize(tables[1].tr[i].td)==6){
      aTestingReport.Summary.Commission=tables[1].tr[i].td[1];
      aTestingReport.Summary.Swap=tables[1].tr[i].td[2];
      aTestingReport.Summary.Profit=tables[1].tr[i].td[3];
      aTestingReport.Summary.Balance=tables[1].tr[i].td[4];            
   }
   break;
} 

A estrutura com os dados do relatório está completamente pronta.

Relatório do histórico de negociação

O relatório do histórico de negociação pode ser analisado de forma semelhante ao Relatório do Testador de Estratégia, embora a estrutura de dados final e as estruturas nele contidas sejam diferentes:

struct SHistory{
   SHistoryInfo Info;
   SOrder Orders[];
   SDeal Deals[];
   Sumário SSummary;  
   Depósito SDeposit;
   Resultados SHistoryResults;
};

A estrutura SHistory contém as seguintes estruturas: SHistoryInfo - informações gerais da conta, arrays com ordens e estruturas de dados da negociação, SSummary - resultados da negociação, SDeposit - resultado final do depósito, SHistoryResults - valores gerais (lucro, número de negócios, rebaixamento, etc).

O código completo da função HistoryHTMLReportToStruct() para analisar relatórios de negociação está anexo no final. Dois parâmetros são transferidos para a função:

  • string FileName — o nome do arquivo de relatório
  • SHistory History — a estrutura com os dados do relatório de negociação a serem preenchidos

Relatório de otimização

A primeira diferença em relação ao relatório de otimização é o tipo de arquivo diferente, o relatório é salvo em ANSI, usaremos outra função para ler seu conteúdo:

bool FileGetContentAnsi(string aFileName,string & aContent){
   int h=FileOpen(aFileName,FILE_READ|FILE_TXT|FILE_ANSI);
   if(h==-1)return(false);
   aContent="";
   while(!FileIsEnding(h)){
      aContent=aContent+FileReadString(h);
   }
   FileClose(h);
   return(true);
}

Outra diferença é encontrada nas tags. Em vez de <table>, <tr> e <td>, as seguintes tags são usadas: <Table>, <Row> e <Cell>. A última e principal diferença está associada à estrutura dos dados :

struct SOptimization{
   string ParameterName[];
   SPass Pass[];
};

A estrutura inclui um array de string com os nomes dos parâmetros a otimizar (as colunas do relatório mais à direita) e o array de estruturas SPass:

struct SPass{
   string Pass;
   string Result;
   string Profit;
   string ExpectedPayoff;
   string ProfitFactor;
   string RecoveryFactor;
   string SharpeRatio;
   string Custom;
   string EquityDD_perc;
   string Trades;
   string Parameters[];
};

A estrutura inclui os campos contidos nas colunas do relatório, o último campo é o array de strings, que contém os valores dos parâmetros sob otimização, de acordo com seus nomes no array ParameterName().

O código completo da função OptimizerXMLReportToStruct() para analisar relatórios de otimização está anexo no final. Dois parâmetros são transferidos para a função:

  • string FileName — o nome do arquivo de relatório
  • SOptimization Optimization — a estrutura com os dados do relatório de otimização a serem preenchidos

Funções auxiliares

Se organizarmos todas as estruturas e funções criadas neste artigo em um arquivo de inclusão, a análise do relatório será implementada em três linhas de código. 

1. Incluir o arquivo:

#include <HTMLReport.mqh>

2. Declarar a estrutura:

Histórico SHistory;

3. Executar a função:

HistoryHTMLReportToStruct("ReportHistory-555849.html",History);

Depois disso, todos os dados do relatório serão localizados nos campos da estrutura correspondente, exatamente como estão dispostos no relatório, embora todos os campos sejam do tipo string, eles podem ser facilmente usados para cálculos, precisamos apenas converter a string em um número, porém alguns dos campos do relatório e campos de estrutura apropriados contêm dois valores. Por exemplo, o volume da ordem é escrito por dois valores "0.1 / 0.1", onde o primeiro valor é o volume da ordem e o segundo valor representa o volume preenchido. Alguns totais também têm valores duplos, o principal e um valor adicional entre parênteses, por exemplo, o número de negociações pode ser escrito como "11 (54,55%)" - o número real e o valor percentual relativo, logo vamos escrever funções auxiliares para simplificar o uso destes valores.

Funções para extrair valores de volume individuais:

string Volume1(string aStr){
   int p=StringFind(aStr,"/",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,0,p);
   }
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

string Volume2(string aStr){
   int p=StringFind(aStr,"/",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,p+1);
   }
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

A função Volume1() recupera o valor do primeiro volume de uma string como "0.1 / 0.1", Volume2() recupera o segundo dos valores.

Funções para extrair valores de dados duplos:

string Value1(string aStr){
   int p=StringFind(aStr,"(",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,0,p);
   }
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

string Value2(string aStr){
   int p=StringFind(aStr,"(",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,p+1);
   }
   StringReplace(aStr,")","");
   StringReplace(aStr,"%","");
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

A função Value1() recupera o primeiro valor de uma string do tipo "8.02 (0.08%)", Value2() recupera o segundo valor, exclui o colchete de fechamento e o sinal de porcentagem se houver algum.

Outra função útil que pode ser necessária é a conversão de dados sobre parâmetros da estrutura do relatório de teste. Usaremos a seguinte estrutura para convertê-lo dentro de uma forma adequada:

struct SInput{
   string Name,
   string Value;
}

O campo Name é usado para o nome do parâmetro, Value é usado para valor. O tipo de valor é conhecido antecipadamente, é por isso que o campo Valor é do tipo string.

A função a seguir converte uma string com parâmetros dentro de um array SInputs de estruturas:

void InputsToStruct(string aStr,SInput & aInputs[]){
   string tmp[];
   string tmp2[];
   StringSplit(aStr,',',tmp);
   int sz=ArraySize(tmp);
   ArrayResize(aInputs,sz);
   for(int i=0;i<sz;i++){
      StringSplit(tmp[i],'=',tmp2);
      StringTrimLeft(tmp2[0]);
      StringTrimRight(tmp2[0]);      
      aInputs[i].Name=tmp2[0];
      if(ArraySize(tmp2)>1){
         StringTrimLeft(tmp2[1]);
         StringTrimRight(tmp2[1]);       
         aInputs[i].Value=tmp2[1];
      }
      else{
         aInputs[i].Value="";
      }
   }
}

A string com parâmetros é dividida dentro de um array de acordo com o caractere ",". Então cada elemento do array resultante é dividido em nome e valor com base no caractere "=".

Agora tudo está pronto para analisar os dados extraídos e gerar nossos próprios relatórios.

Criando um relatório personalizado

Agora que temos acesso aos dados do relatório, podemos analisá-los da maneira que preferirmos, existem muitas opções. Usando os resultados da negociação, podemos calcular métricas gerais, como o índice de Sharpe, o fator de recuperação, etc. Relatórios HTML personalizados permitem acesso a todos os recursos HTML, CSS e JavaScript, usando HTML, podemos reorganizar relatórios. Por exemplo, podemos criar uma tabela de ordens e negociações ou separar tabelas para operações de compra e venda e outros tipos de relatórios.

O CSS permite colorir os dados, tornando o relatório mais fácil de analisar. O JavaScript pode ajudar a tornar os relatórios interativos, por exemplo, ao passar o mouse sobre uma linha de negociação, evidencia a linha de ordem escolhida na tabela de ordens. Imagens podem ser geradas e adicionadas ao relatório HTML, opcionalmente, o JavaScript possibilita desenhar imagens diretamente no elemento canvas em html5. Tudo depende de suas necessidades, imaginação e habilidades.

Vamos criar um relatório de negociação em HTML personalizado, combinar ordens e negociações em uma tabela. ordens canceladas serão mostrados em cinza, porque eles não fornecem informações de interesse. A cor cinza também será usado para ordens de mercado, pois elas oferecem as mesmas informações que as negociaçoes executadas como resultado das ordens. Os negócios serão destacados nas cores claras: azul e vermelho. O preechimento das ordens pendentes também terão cores destacadas, como a rosa e a azul. 

A tabela de relatórios personalizados conterá cabeçalhos da tabela de ordens e da tabela de negociações. Vamos preparar os arrays necessários:

   string TableHeader[]={  "Time",
                           "Order",
                           "Deal",
                           "Symbol",
                           "Type",
                           "Direction",
                           "Volume",
                           "Price",
                           "Order",
                           "S/L",
                           "T/P",
                           "Time",
                           "State",
                           "Commission",
                           "Swap",
                           "Profit",
                           "Balance",
                           "Comment"};

Recebemos a estrutura com os dados do relatório:

Histórico SHistory;
HistoryHTMLReportToStruct("ReportHistory-555849.html",History);

Vamos abrir o arquivo para o relatório gerado e escrever o início padrão da página HTML formada pela função HTMLStart():

   int h=FileOpen("Report.htm",FILE_WRITE);
   if(h==-1)return;

   FileWriteString(h,HTMLStart("Report"));
   FileWriteString(h,"<table>\n");
   
   FileWriteString(h,"\t<tr>\n");   
   for(int i=0;i<ArraySize(TableHeader);i++){
      FileWriteString(h,"\t\t<th>"+TableHeader[i]+"</th>\n"); 
   }
   FileWriteString(h,"\t</tr>\n");     

O código da função HTMLStart() é mostrado abaixo:

string HTMLStart(string aTitle,string aCSSFile="style.css"){
   string str="<!DOCTYPE html>\n";
   str=str+"<html>\n";
   str=str+"<head>\n";
   str=str+"<link href=\""+aCSSFile+"\" rel=\"stylesheet\" type=\"text/css\">\n";
   str=str+"<title>"+aTitle+"</title>\n";
   str=str+"</head>\n";  
   str=str+"<body>\n";     
   return str;
}

Uma string com o título da página para a tag <title> e o nome do arquivo com estilos é transferido para a função.

Um loop é realizado em todos os pedidos que define o tipo de exibição de string e a forma:

int j=0;
for(int i=0;i<ArraySize(History.Orders);i++){
   
   string sc="";
   
   if(History.Orders[i].State=="canceled"){
      sc="PendingCancelled";
   }
   else if(History.Orders[i].State=="filled"){
      if(History.Orders[i].Type=="buy"){
         sc="OrderMarketBuy";
      }
      else if(History.Orders[i].Type=="sell"){
         sc="OrderMarketSell";
      }
      if(History.Orders[i].Type=="buy limit" || History.Orders[i].Type=="buy stop"){
         sc="OrderPendingBuy";
      }
      else if(History.Orders[i].Type=="sell limit" || History.Orders[i].Type=="sell stop"){
         sc="OrderMarketSell";
      }         
   }

   FileWriteString(h,"\t<tr class=\""+sc+"\">\n");   
   FileWriteString(h,"\t\t<td>"+History.Orders[i].OpenTime+"</td>\n");  // Tempo
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Order+"</td>\n");     // Ordem 
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Negociação 
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Symbol+"</td>\n");    // Símbolo 
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Type+"</td>\n");      // Tipo 
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Direção       
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Volume+"</td>\n");    // Volume    
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Price+"</td>\n");     // Preço  
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Ordem        
   FileWriteString(h,"\t\t<td>"+History.Orders[i].SL+"</td>\n");        // SL
   FileWriteString(h,"\t\t<td>"+History.Orders[i].TP+"</td>\n");        // TP
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Time+"</td>\n");      // Tempo    
   FileWriteString(h,"\t\t<td>"+History.Orders[i].State+"</td>\n");     // Resultado
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Comissão
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Swap
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Lucro     
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Saldo    
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Comment+"</td>\n");   // Comentário     
   FileWriteString(h,"\t</tr>\n");   
   

Se uma ordem for preenchida, encontre uma oferta adequada, defina o estilo de exibição e a forma o código HTML:  

// Encontrar uma negociação

if(History.Orders[i].State=="filled"){
   for(;j<ArraySize(History.Deals);j++){
      if(History.Deals[j].Order==History.Orders[i].Order){
         
         sc="";
         
         if(History.Deals[j].Type=="buy"){
            sc="DealBuy";
         }
         else if(History.Deals[j].Type=="sell"){
            sc="DealSell";
         }
      
         FileWriteString(h,"\t<tr class=\""+sc+"\">\n");   
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Time+"</td>\n");     // Tempo
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // Ordem 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Deal+"</td>\n");     // Negociação 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Symbol+"</td>\n");     // Símbolo 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Type+"</td>\n");     // Tipo 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Direction+"</td>\n");     // Direção       
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Volume+"</td>\n");     // Volume    
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Price+"</td>\n");     // Preço  
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Order+"</td>\n");     // Ordem        
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // SL
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // TP
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // Tempo    
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // Resultado
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Commission+"</td>\n");                    // Comissão
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Swap+"</td>\n");     // Swap
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Profit+"</td>\n");     // Lucro     
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Balance+"</td>\n");     // Saldo    
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Comment+"</td>\n");     // Comentário     
         FileWriteString(h,"\t</tr>\n"); 
         break;
      }
   }
}

Depois disso, fechamos a tabela e adicionamos o final da página padrão HTML, que é criada usando a função HTMLEnd():

FileWriteString(h,"</table>\n");   
FileWriteString(h,HTMLEnd("Report"));  

Código da função HTMLEnd():

string HTMLEnd(){
   string str="</body>\n";
   str=str+"</html>\n";
   return str;
}

Agora somente precisamos escrever o arquivo de estilos style.css, aprender css está além do escopo deste artigo e, portanto, não vamos considerar isso nos detalhes. Você pode visualizar o arquivo, que está anexo no final do artigo, também contém no anexo, o script que cria o relatório - HTMLReportCreate.mq5.

Aqui está o relatório pronto:

Relatório HTML personalizado
Fig. 5. Fragmento do relatório HTML personalizado

Conclusões

Você pode se perguntar se o uso de expressões regulares seria mais fácil, a estrutura geral e o princípio seriam os mesmos. Nós receberíamos separadamente um array com o conteúdo da tabela, depois linhas e células, em vez de TagsToArray(), usaríamos uma função com a expressão regular. As operações restantes seriam muito semelhantes.

Este relatório personalizado descrito neste artigo é apenas uma das opções para representar relatórios, apenas como um exemplo. Você pode usar seu próprio formulário que lhe seja adequado e compreensível. O resultado mais importante do artigo é o fácil acesso a todos os dados do relatório.

Anexos

  • Include/HTMLReport.mqh - inclui o arquivo com as funções de análise do relatório.
  • Scripts/HTMLReportTest.mq5 — exemplo do uso do HTMLReport.mqh para analisar testes, otimização e relatórios do histórico.
  • Scripts/HTMLReportCreate.mq5 — exemplo da criação de um relatório HTML personalizado.
  • Files/ReportTester-555849.html — relatório do Testador de Estratégia.
  • Files/ReportOptimizer-555849.xml — relatório de otimização.
  • Files/ReportHistory-555849.html — relatório do histórico de negociação.
  • Files/Report.htm — arquivo do relatório usando o script HTMLReportCreate.
  • Files/style.css — estilos para Report.htm.