Testador de estratégia personalizada com base em cálculos matemáticos rápido

Vasiliy Sokolov | 20 fevereiro, 2018


Sumário



Introdução

O testador de estratégias, fornecido pelo MetaTrader 5, tem características poderosas para resolver diversos problemas. Ele permite testar estratégias de negociação complexas, usando cestas de instrumentos, e estratégias únicas, com regras simples de entradas e saídas. No entanto, nem sempre uma funcionalidade tão extensa vem ao caso. Muitas vezes, nós simplesmente precisamos verificar rapidamente alguma idéia de negociação simples ou fazer cálculos aproximados cuja precisão será compensada pela sua velocidade. O testador padrão do MetaTrader 5 tem uma funcionalidade interessante, mas pouco usada, que pode fazer cálculos no modo de cálculo matemático. Trata-se de um modo restrito de arranque do testador de estratégias, que, no entanto, tem todas as vantagens de uma otimização completa: é visível a computação em nuvem pode ser usado e o otimizador genético pode coletar tipos de dados personalizados.

Não só aqueles que precisam de velocidade absoluta podem necessitar um testador de estratégias. O teste em modo de cálculos matemáticos abre o caminho para os pesquisadores. O testador de estratégias pode simular operações de negociação tão próximas da realidade quanto possível. No modo de pesquisa, esse requisito não é sempre útil. Por exemplo, às vezes é preciso obter uma estimativa da eficiência líquida do sistema de negociação - sem levar em conta a derrapagem, spreads e comissões. O testador de cálculos matemáticos, desenvolvido neste artigo, dá essa oportunidade.

Claro, quem tudo quer nada tem. Este artigo não é a excepção. Escrever um testador de estratégias próprio exige um trabalho sério e meticuloso. Nosso objetivo é mais modesto, quer dizer, nós mostramos que, usando as bibliotecas apropriadas, criar o testador não é tão difícil como pode parecer à primeira vista.

Se o tema for interessante para os meus colegas, este artigo verá sua continuação aplicando as idéias que sejam propostas para frente.


Introdução ao modo de cálculos matemáticos

Modo de cálculos matemáticos é iniciado na janela de do testador de estratégias. Para fazer isso, selecione, no menu suspenso, o item do mesmo nome:

Fig. 1. Selecionando os cálculos matemáticos no testador de estratégias

Neste modo, é chamado apenas um conjunto limitado de funções, enquanto não está disponível o ambiente de negociação (símbolos, informações de conta, propriedades do servidor de negociação). A OnTester() vira a função de chamada principal, graças a ela o usuário pode definir certo critério personalizado de otimização especial. Ele será usado juntamente com outros critérios de otimização padrão e está disponíveis para exibição no relatório padrão de estratégias. Na imagem abaixo, ele é rodeado por uma borda vermelha:

 

Fig. 2. O critério de otimização personalizado é calculado na função OnTester

Os valores devolvidos pela função OnTester se prestam a uma pesquisa detalhada e otimização. Demonstramos isso num EA simples:

//+--------------------------------------------------------------------+
//|                                                 OnTesterCheck.mq5  |
//|                       Copyright 2017, MetaQuotes Software Corp.    |
//|                                               http://www.mql5.com  |
//+--------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
input double x = 0.01;
//+-------------------------------------------------------------------+
//| Tester function                                                   |
//+-------------------------------------------------------------------+
double OnTester()
{
   double ret = MathSin(x);
   return(ret);
}
//+------------------------------------------------------------------+

Seu código não tem nada além do parâmetro de entrada x a da função OnTester, que calcula o valor do seno do argumento carregado nela, neste caso, x. Vamos agora tentar otimizar essa função. Para fazer isso no testador de estratégias, selecionamos o tipo de otimização "Lenta (busca exaustiva de parâmetros)", enquanto o modo de simulação é o mesmo: "cálculo matemático".

Nos parâmetros de optimização, definimos a zona de alteração x: valor inicial - 0,01, passo - 0,01, stop - 10. Depois que tudo estiver pronto, iniciamos o testador de estratégias. Ele completará seu trabalho quase instantaneamente, após o qual abriremos o gráfico de otimização e, no menu de contexto, selecionaremos o "Gráfico de linhas". Aparecerá a função seno:

Fig. 3. Representação gráfica da função seno

Uma característica distintiva deste modo é o reduzido consumo de recursos. As operações de leitura e escrita da unidade de disco rígido são minimizadas, os agentes de teste não carregam cotações dos símbolos solicitados, não há cálculos de acompanhamento, todos os cálculos estão concentrados na função OnTester. 

Devido a uma boa velocidade da OnTester, podemos criar um módulo de computação independente que permite simulações simples. Aqui estão os elementos deste módulo:

  • O histórico do instrumento em que será realizado o teste
  • Sistema de posições virtuais
  • Sistema de negociação dedicada ao gerenciamento de posições virtuais
  • Sistema de análise de resultados

A auto-suficiência do módulo significa que, num único EA, estarão tanto todos os dados necessários para testes quanto o sistema de teste em si que irá utilizá-los. Um EA desse tipo pode ser facilmente distribuído numa rede de computação distribuída, se necessário.

Avançamos para a descrição da primeira parte do sistema, ou seja, a como armazenar o histórico para teste.


Armazenamento do histórico do instrumento para o testador de cálculos matemáticos

O modo de cálculo matemático não inclui o acesso a ferramentas de negociação. Aqui não faz sentido a chamada da funções como CopyRates (Symbol(), ...). No entanto, para simular, são necessários dados históricos. Para fazer isto, podemos armazenar o histórico de cotações, do símbolo desejado, numa matriz pré-comprimida do tipo uchar []:

uchar symbol[128394] = {0x98,0x32,0xa6,0xf7,0x64,0xbc...};

Qualquer tipo de dados - som, imagens, números e cadeias de caracteres - podem ser representados como um simples conjunto de bytes. Byte é um bloco curto constituído por oito bits. Qualquer informação é armazenada em "pacotes" numa sequência composta por esses bytes. Em linguagem MQL5, existe um tipo de dados especial, isto é, uchar. Cada um dos seus valores pode representar um byte. Desse modo, uma matriz uchar de 100 elementos é capaz de armazenar 100 bytes.

As cotações do símbolo são compostas por muitas barras. Cada uma das barras inclui informações sobre a hora de abertura da barra, seus preços (máximo, mínimo, abertura e fechamento) e volume. Cada um desses valores é armazenado numa variável com o respectivo comprimento. Aqui está uma tabela:

Valor Tipo de dados Tamanho em bytes
Tempo de abertura datetime 8
Preço de abertura double 8
Preço máximo double 8
Preço mínimo double 8
Preço de fechamento double 8
Volume de ticks long  8
Spread   int 4
Volume real   long 8

É fácil calcular que, para o armazenamento de uma barra, são necessários 60 bytes, ou uma matriz uchar constituída por 60 elementos. Para o mercado de moedas, um dia de negociação é composto de 1 440 bares de minuto. Consequentemente, o histórico de minuto de um ano consiste em aproximadamente 391 680 barras. Multiplicando esse número por 60 bytes, descobrimos que um ano do histórico de minuto, em sua forma descompactada, tem cerca de 23 MB. Isso é muito ou pouco? Segundo os padrões modernos, isso é pouco, mas imagine o que aconteceria se nós decidimos testar um EA em 10 anos de dados. Seria necessário armazenar 230 MB de dados, e, talvez, até distribuí-los em toda a rede. Isso é muito mesmo para os padrões de hoje.

Por isso, precisamos comprimir de alguma forma esta informação. Por sorte, há muito foi desenvolvida uma biblioteca especial para trabalhar com arquivos Zip. Além de uma funcionalidade diversificada, a biblioteca permite converter o resultado da compactação numa matriz de bytes, o que facilita muito o nosso trabalho.

Assim, nosso algoritmo irá carregar a matriz de barras MqlRates, convertê-la numa representação de bytes, comprimir com o arquivador Zip, e armazenar os dados compactados como uma matriz uchar, definida num arquivo mqh.

Para converter cotações numa matriz de bytes, usa-se a conversão de sistema por meio da combinação dos tipos union. Este sistema permite que colcar vários tipos de dados num único campo de armazenamento. Assim, acessando o mesmo tipo, podemos receber os dados de outro. Nesta associação, serão armazenados dois tipos: a estrutura MqlRates e a matriz uchar, o número de elementos, que será igual ao tamanho da MqlRates. Para entender como funciona este sistema, vamos nos voltar para a primeira versão do nosso script SaveRates.mq5 que converte o histórico do instrumento numa matriz de bytes uchar:

//+------------------------------------------------------------------+
//|                                                     SaveRates.mq5 |
//|                                  Copyright 2016, Vasiliy Sokolov. |
//|                                               http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include <Zip\Zip.mqh>
#include <ResourceCreator.mqh>
input ENUM_TIMEFRAMES MainPeriod;

union URateToByte
{
   MqlRates bar;
   uchar    bar_array[sizeof(MqlRates)];
}RateToByte;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   //-- Carregamos as cotações
   MqlRates rates[];
   int total = CopyRates(Symbol(), Period(), 0, 20000, rates);
   uchar symbol_array[];
   //-- Convertemo-las em bytes
   ArrayResize(symbol_array, sizeof(MqlRates)*total);
   for(int i = 0, dst = 0; i < total; i++, dst +=sizeof(MqlRates))
   {
      RateToByte.bar = rates[i];
      ArrayCopy(symbol_array, RateToByte.bar_array, dst, 0, WHOLE_ARRAY);
   }
   //-- Compactamo-las num arquivo zip
   CZip Zip;
   CZipFile* file = new CZipFile(Symbol(), symbol_array);
   Zip.AddFile(file);
   uchar zip_symbol[];
   //-- Obtemos os bytes do arquivo compactado
   Zip.ToCharArray(zip_symbol);
   //-- Registramo-lo como um arquivo anexado mqh
   CCreator creator;
   creator.ByteArrayToMqhArray(zip_symbol, "rates.mqh", "rates");
}
//+------------------------------------------------------------------+

Após esse código ser executado, na matriz zip_symbol estará a matriz compactada da estrutura MqlRates, isto é, o histórico de cotações compactado. Em seguida, a matriz compactada é armazenada como um arquiva mqh, no disco rígido do computador. A seguir, vamos discutir sobre como isso é feito e por que ela é necessária. 

Não basta obter a representação de bytes das cotações e comprimi-las. É preciso escrever esta representação como uma matriz uchar. Esta matriz deve ser carregada como um recurso, isto é, ela deve ser compilada juntamente com o programa. Para este fim, criamos um arquivo de cabeçalho mqh especial que contém nossa matriz como um conjunto de caracteres ASCII simples. Para fazer isso, usamos a classe especial CResourceCreator:

//+-------------------------------------------------------------------+
//|                                               ResourceCreator.mqh |
//|                                  Copyright 2017, Vasiliy Sokolov. |
//|                                               http://www.mql5.com |
//+-------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include <Arrays\ArrayObj.mqh>
//+------------------------------------------------------------------+
//| Contém os identificadores de cadeia da matriz de recursos gerada |
//+------------------------------------------------------------------+
class CResInfo : public CObject
{
public:
   string FileName;
   string MqhFileName;
   string ArrayName;
};
//+------------------------------------------------------------------+
//| Cria recurso MQL como uma matriz de bytes.                       |
//+------------------------------------------------------------------+
class CCreator
{
private:
   int      m_common;
   bool     m_ch[256];
   string   ToMqhName(string name);
   void     CreateInclude(CArrayObj* list_info, string file_name);
public:
            CCreator(void);
   void     SetCommonDirectory(bool common);
   bool     FileToByteArray(string file_name, uchar& byte_array[]);
   bool     ByteArrayToMqhArray(uchar& byte_array[], string file_name, string array_name);
   void     DirectoryToMqhArray(string src_dir, string dst_dir, bool create_include = false);
};
//+------------------------------------------------------------------+
//| Construtor por padrão                                            |
//+------------------------------------------------------------------+
CCreator::CCreator(void) : m_common(FILE_COMMON)
{
   ArrayInitialize(m_ch, false);
   for(uchar i = '0'; i < '9'; i++)
      m_ch[i] = true;
   for(uchar i = 'A'; i < 'Z'; i++)
      m_ch[i] = true;
}
//+------------------------------------------------------------------+
//| Define ou remove o sinalizador FILE_COMMON                       |
//+------------------------------------------------------------------+
CCreator::SetCommonDirectory(bool common)
{
   m_common = common ? FILE_COMMON : 0;   
}

//+-----------------------------------------------------------------------------+
//| Transfere todos os arquivos no diretório src_dir para arquivos mqh contendo |
//| a representação destes arquivos                                             |
//+-----------------------------------------------------------------------------+
void CCreator::DirectoryToMqhArray(string src_dir,string dst_dir, bool create_include = false)
{
   string file_name;
   string file_mqh;
   CArrayObj list_info;
   long h = FileFindFirst(src_dir+"\\*", file_name, m_common);
   if(h == INVALID_HANDLE)
   {
      printf("Diretório" + src_dir + " não pode ser encontrado ou não contém arquivos");
      return;
   }
   do
   {
      uchar array[];
      if(FileToByteArray(src_dir+file_name, array))
      {
         string norm_name = ToMqhName(file_name);
         file_mqh = dst_dir + norm_name + ".mqh";
         ByteArrayToMqhArray(array, file_mqh, "m_"+norm_name);
         printf("Create resource: " + file_mqh);
         //Adicionamos informações sobre o recurso criado
         CResInfo* info = new CResInfo();
         list_info.Add(info);
         info.FileName = file_name;
         info.MqhFileName = norm_name + ".mqh";
         info.ArrayName = "m_"+norm_name;
      }
   }while(FileFindNext(h, file_name));
   if(create_include)
      CreateInclude(&list_info, dst_dir+"include.mqh");
}
//+----------------------------------------------------------------------+
//| Cria um arquivo mqh com a inclusão de todos os arquivo gerados       |
//+----------------------------------------------------------------------+
void CCreator::CreateInclude(CArrayObj *list_info, string file_name)
{
   int handle = FileOpen(file_name, FILE_WRITE|FILE_TXT|m_common);
   if(handle == INVALID_HANDLE)
   {
      printf("Falha ao criar o arquivo include" + file_name);
      return;
   }
   //Criamos o cabeçalho include
   for(int i = 0; i < list_info.Total(); i++)
   {
      CResInfo* info = list_info.At(i);
      string line = "#include \"" + info.MqhFileName + "\"\n";
      FileWriteString(handle, line);
   }
   //Criamos a função que copia a matriz de recursos para o código que faz a chamada
   FileWriteString(handle, "\n");
   FileWriteString(handle, "void CopyResource(string file_name, uchar &array[])\n");
   FileWriteString(handle, "{\n");
   for(int i = 0; i < list_info.Total(); i++)
   {
      CResInfo* info = list_info.At(i);
      if(i == 0)
         FileWriteString(handle, "   if(file_name == \"" + info.FileName + "\")\n");
      else
         FileWriteString(handle, "   else if(file_name == \"" + info.FileName + "\")\n");
      FileWriteString(handle,    "      ArrayCopy(array, " + info.ArrayName + ");\n");
   }
   FileWriteString(handle, "}\n");
   FileClose(handle);
}
//+------------------------------------------------------------------+
//| converte o nome passado no nome correto da variável MQL          |
//+------------------------------------------------------------------+
string CCreator::ToMqhName(string name)
{
   uchar in_array[];
   uchar out_array[];
   int total = StringToCharArray(name, in_array);
   ArrayResize(out_array, total);
   int t = 0;
   for(int i = 0; i < total; i++)
   {
      uchar ch = in_array[i];
      if(m_ch[ch])
         out_array[t++] = ch;
      else if(ch == ' ')
         out_array[t++] = '_';
      uchar d = out_array[t-1];
      int dbg = 4;
   }
   string line = CharArrayToString(out_array, 0, t);
   return line;
}
//+------------------------------------------------------------------+
//| Retorna a representação de bytes do arquivo passado como         |
//| uma matriz byte_array                                            |
//+------------------------------------------------------------------+
bool CCreator::FileToByteArray(string file_name, uchar& byte_array[])
{
   int handle = FileOpen(file_name, FILE_READ|FILE_BIN|m_common);
   if(handle == -1)
   {
      printf("Filed open file " + file_name + ". Reason: " + (string)GetLastError());
      return false;
   }
   FileReadArray(handle, byte_array, WHOLE_ARRAY);
   FileClose(handle);
   return true;
}
//+------------------------------------------------------------------+
//| Converte a matriz de bytes passada byte_array num arquivo mqh    |
//| file_name em que há uma descrição da matriz com o nome           |
//|  array_name                                                      |
//+------------------------------------------------------------------+
bool CCreator::ByteArrayToMqhArray(uchar& byte_array[], string file_name, string array_name)
{
   int size = ArraySize(byte_array);
   if(size == 0)
      return false;
   int handle = FileOpen(file_name, FILE_WRITE|FILE_TXT|m_common, "");
   if(handle == -1)
      return false;
   string strSize = (string)size;
   string strArray = "uchar " +array_name + "[" + strSize + "] = \n{\n";
   FileWriteString(handle, strArray);
   string line = "   ";
   int chaptersLine = 32;
   for(int i = 0; i < size; i++)
   {
      ushort ch = byte_array[i];
      line += (string)ch;
      if(i == size - 1)
         line += "\n";
      if(i>0 && i%chaptersLine == 0)
      {
         if(i < size-1)
            line += ",\n";
         FileWriteString(handle, line);
         line = "   ";
      }
      else if(i < size - 1)
         line += ",";
   }
   if(line != "")
      FileWriteString(handle, line);
   FileWriteString(handle, "};");
   FileClose(handle);
   return true;
}

Nós não vamos nos debruçar sobre seu trabalho, vou descrevê-lo apenas em termos gerais listando suas capacidades.

  • Lê qualquer arquivo arbitrário no disco rígido e armazena sua representação de bytes como uma matriz uchar num arquivo mqh;
  • Lê qualquer diretório arbitrário no disco rígido e salva a representação de bytes de todos os arquivos nesse diretório. A representação de bytes para cada ficheiro desse tipo está localizada num arquivo mqh separado contendo uma matriz uchar;
  • Recebe, para entrada, uma matriz de bytes uchar e armazena-a na forma de uma matriz de símbolos num arquivo separado mqh;
  • Cria um arquivo de cabeçalho especial que contém links para todos os arquivos mqh criados durante a geração. Além disso, cria-se uma função especial que recebe, para entrada, o nome da matriz e retorna sua representação em bytes. Este algoritmo utiliza a geração dinâmica de código. 

A classe descrita é uma poderosa alternativa para o sistema padrão de alocação de recursos em programas mql.

Por padrão, todas as operações de arquivo são mantidas num diretório de arquivos comuns (FILE_COMMON). Se você executar o script a partir da listagem anterior, um novo arquivo rates.mqh será exibido nele (o nome do arquivo é definido pelo segundo parâmetro do método ByteArrayToMqhArray). Ele conterá uma matriz enorme rates[] (o nome da matriz é especificado pelo terceiro parâmetro deste método). Aqui está um excerto desse arquivo:


Fig. 4. Cotações MqlRates como uma matriz de bytes compactada

A compactação de dados funciona bem. Um ano de um histórico de minuto descompactado do par EURUSD é de aproximadamente 20 MB, já após a compressão - apenas 5 MB. No entanto, é melhor não abrir o próprio arquivo rates.mqh no MetaEditor, porque seu tamanho é muito maior do que este valor, e o editor pode não responder. Mas não se preocupe. Após a compilação, o texto é convertido em bytes, e o tamanho real do programa é aumentado apenas o valor real das informações armazenadas, ou seja, 5 megabytes neste caso.

A propósito, usando esta técnica em programas ex5, pode-se armazenar qualquer tipo de informação, e não só o histórico de cotações.


Carregamento de cotações MqlRates de uma matriz compactada de bytes

Agora que o histórico está armazenado, podemos conectá-lo a qualquer programa MQL, para fazer isso, basta escrever uma diretiva include no seu início:

...
#include "rates.mqh"
...

Adicionalmente, o arquivo rates.mqh deve ser movido para o diretório de textos de origem do programa.

Ligar os dados não é suficiente. Além disso, é necessário escrever um bloco de procedimentos para sua conversão inversa numa matriz regular MqlRates. Para fazer isso, realizamos a função especial LoadRates. Na entrada, ela vai aceitar a matriz vazia MqlRates. Após ela concluir su trabalho, a matriz conterá as habituais cotações MqlRates carregadas a partir da matriz compactada. Aqui está o código desta função:

//+---------------------------------------------------------------------+
//|                                                       Mtester.mqh   |
//|                       Copyright 2017, MetaQuotes Software Corp.     |
//|                                               http://www.mql5.com   |
//+---------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#include <Zip\Zip.mqh>
#include "rates.mqh"
//+------------------------------------------------------------------+
//|  Casting MqlRates to uchar[]                                     |
//+------------------------------------------------------------------+
union URateToByte
{
   MqlRates bar;
   uchar    bar_array[sizeof(MqlRates)];
};
//+---------------------------------------------------------------------+
//| Converte os arquivos compactados numa matriz de cotações MqlRates   |
//| Retorna o número de barras obtidas, retorna -1 se for               |
//| mal-sucedido                                                        |
//+---------------------------------------------------------------------+
int LoadRates(string symbol_name, MqlRates &mql_rates[])
{
   CZip Zip;
   Zip.CreateFromCharArray(rates);
   CZipFile* file = dynamic_cast<CZipFile*>(Zip.ElementByName(symbol_name));
   if(file == NULL)
      return -1;
   uchar array_rates[];
   file.GetUnpackFile(array_rates);
   URateToByte RateToBar;
   ArrayResize(mql_rates, ArraySize(array_rates)/sizeof(MqlRates));
   for(int start = 0, i = 0; start < ArraySize(array_rates); start += sizeof(MqlRates), i++)
   {
      ArrayCopy(RateToBar.bar_array, array_rates, 0, start, sizeof(MqlRates));
      mql_rates[i] = RateToBar.bar;
   }
   return ArraySize(mql_rates);
}
//+------------------------------------------------------------------+

A função está no arquivo Mtester.mqh. Esta será nossa primeira função para trabalhar no modo de cálculos matemáticos. Mais tarde, ao arquivo Mtester.mqh será adicionado um novo recurso, que talvez vire um motor completo de teste matemático de estratégias.

Escreveremos uma estratégia fundamental para o modo de cálculos matemáticos. Ela vai realizar apenas duas funções: carregar as cotações na função OnInit e calcular a média de todos os preços de fechamento na função OnTester. O resultado do cálculo retornará no MetaTrader:

//+-------------------------------------------------------------------+
//|                                                       MExpert.mq5 |
//|                       Copyright 2017, MetaQuotes Software Corp.   |
//|                                               http://www.mql5.com |
//+-------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include "Mtester.mqh"
//+------------------------------------------------------------------+
//| Cotações em que será realizado o teste                           |
//+------------------------------------------------------------------+
MqlRates Rates[];
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   //-- Carregamos as cotações para o símbolo especificado.
   if(LoadRates(Symbol(), Rates)==-1)
   {
      printf("Cotações para os símbolo " + Symbol() + " não encontradas. Crie o recurso de cotações adequado.");
      return INIT_PARAMETERS_INCORRECT;
   }
   printf("Carregadas " + (string)ArraySize(Rates) + " barras do símbolo " + Symbol());
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester()
{
   double mean = 0.0;
   for(int i = 0; i < ArraySize(Rates); i++)
      mean += Rates[i].close;
   mean /= ArraySize(Rates);
   return mean;
}
//+------------------------------------------------------------------+

Após o EA compilado, iremos carregá-lo no testador de estratégias e selecionar o modo "Cálculos matemáticos". Executamos seu teste e acessamos o log:

2017.12.13 15:12:25.127 Core 2  math calculations test of Experts\MTester\MExpert.ex5 started
2017.12.13 15:12:25.127 Core 2  Carregadas 354159 barras do símbolo EURUSD
2017.12.13 15:12:25.127 Core 2  OnTester result 1.126596405653942
2017.12.13 15:12:25.127 Core 2  EURUSD,M15: mathematical test passed in 0:00:00.733 (total tester working time 0:00:01.342)
2017.12.13 15:12:25.127 Core 2  217 Mb memory used

Como pode ser visto, o EA trabalhou como planejado. Todas as cotações foram carregadas corretamente, o que é evidenciado pelo registro do número de barras carregadas. Além disso, foi feita corretamente a pesquisa detalhada de todas as barras para calcular o valor médio, o que foi devolvido para o fluxo de chamada. Como você pode ver, o preço médio de todas as cotações sobre EURUSD durante o último ano foi de 1,12660.


Protótipo de estratégia na média móvel

Conseguimos resultados impressionantes: obtivemos, compactamos e armazenamos os dados como uma matriz estática uchar que carregamos no EA, em seguida, extraímos, convertemos novamente os dados numa matriz de cotações. Agora é hora de escrever a primeira estratégia útil. Veremos a versão clássica com base no cruzamento de duas médias móveis. Esta estratégia é fácil de implementar. Como não temos acesso ao ambiente de negociação no modo de cálculos matemáticos, não podemos chamar indicadores como o IMA diretamente. Em vez disso, temos que calcular o valor da média manualmente. Nosso principal objetivo, neste modo de teste, é a aceleração máxima. Portanto, todos os algoritmos que vamos usar tem que trabalhar rápido. Sabe-se que o cálculo da média tem a ver com uma classe de tarefas simples com a complexidade da realização O(1). Isto significa que a velocidade de cálculo do valor médio não depende do período da média. Para estes fins, usamos a biblioteca do buffer circular pronta para usar. Para mais informações sobre este algoritmo pode ver este outro artigo.

Para começar, escrevemos o modelo de nosso primeiro EA:

//+--------------------------------------------------------------------+
//|                                                       MExpert.mq5  |
//|                       Copyright 2017, MetaQuotes Software Corp.    |
//|                                               http://www.mql5.com  |
//+--------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include "Mtester.mqh" 
#include <RingBuffer\RiSma.mqh>

input int PeriodFastMa = 9;
input int PeriodSlowMa = 16;

CRiSMA FastMA;    // Buffer circular para cálculo da média móvel rápida
CRiSMA SlowMA;    // Buffer circular para cálculo da média móvel lenta
//+------------------------------------------------------------------+
//| Cotações em que será realizado o teste                           |
//+------------------------------------------------------------------+
MqlRates Rates[];
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   //-- Verificamos se a combinação de parâmetros é correta
   //-- A média rápida não pode ser superior à lenta
   if(PeriodFastMa >= PeriodSlowMa)
      return INIT_PARAMETERS_INCORRECT;
   //-- Inicializamos o período dos buffers circulares
   FastMA.SetMaxTotal(PeriodFastMa);
   SlowMA.SetMaxTotal(PeriodSlowMa);
   //-- Carregamos as cotações para o símbolo especificado.
   if(LoadRates(Symbol(), Rates)==-1)
   {
      printf("Cotações para os símbolo " + Symbol() + " não encontradas. Crie o recurso de cotações adequado.");
      return INIT_FAILED;
   }
   printf("Carregadas " + (string)ArraySize(Rates) + " barras do símbolo " + Symbol());
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Descrevemos a estratégia                                         |
//+------------------------------------------------------------------+
double OnTester()
{
   for(int i = 1; i < ArraySize(Rates); i++)
   {
      FastMA.AddValue(Rates[i].close);
      SlowMA.AddValue(Rates[i].close);
      //Esta será a lógica de negociação de nosso EA
   }
   return 0.0;
}
//+------------------------------------------------------------------+

Ele define dois parâmetros para os períodos de média da MA rápida e lenta. Em seguida, são declarados dois buffers circulares que calculam o valor destas médias. No bloco de inicialização, verifica-se se os parâmetros inseridos estão corretos. Como os parâmetros são definidos pelo testador de estratégias automaticamente - e não pelo usuário - no modo de otimização, muitas vezes, os parâmetros são combinados de maneira incorreta. Nesse caso, nossa MA rápida pode ser mais lenta. Para evitar esta confusão e economizar tempo na otimização, vamos concluir esta corrida antes de sua inicialização. Para estes fins, no bloco da OnInit, é retornada a constante INIT_PARAMETERS_INCORRECT.

Após inicializados os buffers, os parâmetros verificados e as cotações carregadas, é inicializada a função OnTester. Nela, o teste básico estará dentro do bloco for. O código permite inferir que se o valor médio do buffer circular FastMA é maior do que o valor médio do SlowMA, será necessário abrir uma posição longa, e vice-versa. Porém, agora não temos um módulo de negociação para abrir quer posições longas quer curtas. Temos que escrevê-lo. 


Classe da posição virtual

Como já foi referido, o modo de cálculos matemáticos não serve para o cálculo de estratégias. Portanto, nele não há funções de negociação. Também não podemos tirar vantagem do ambiente de negociação do MetaTrader. Neste modo, o conceito de "posição" simplesmente não existe. Por isso, precisamos criar um análogo simplificado da posição do MetaTrader. Ele conterá apenas as informações mais relevantes. Para fazer isso, escrevemos uma classe com estes campos: 

  • hora de abertura de posição;
  • preço de abertura da posição;
  • hora de fechamento da posição;
  • preço de fechamento da posição;
  • volume da posição;
  • spread ao abrir a posição;
  • direção da posição.

Talvez no futuro vamos completá-lo com informações adicionais, mas agora só precisamos destes campos.

//+--------------------------------------------------------------------+
//|                                                       Mtester.mqh  |
//|                       Copyright 2017, MetaQuotes Software Corp.    |
//|                                               http://www.mql5.com  |
//+--------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#include <Object.mqh>
#include "rates.mqh"
#include "Type2Char.mqh"
//+--------------------------------------------------------------------+
//| Classe da posição virtual para o testador de cálculos matemáticos  |
//+--------------------------------------------------------------------+
class CMposition : public CObject
{
private:
   datetime    m_time_open;
   datetime    m_time_close;
   double      m_price_open;
   double      m_price_close;
   double      m_volume;
   int         m_spread;
   ENUM_POSITION_TYPE m_type;
public:
               CMposition(void);
   static int  Sizeof(void);
   bool        IsActive(void);
   datetime    TimeOpen(void);
   datetime    TimeClose(void);
   double      PriceOpen(void);
   double      PriceClose(void);
   double      Volume(void);
   double      Profit(void);
   ENUM_POSITION_TYPE PositionType(void);
   static CMposition*  CreateOnBarOpen(MqlRates& bar, ENUM_POSITION_TYPE pos_type, double vol);
   void        CloseOnBarOpen(MqlRates& bar);
};
//+------------------------------------------------------------------+
//| Uma posição CMposition ocupa 45 bytes                            |
//+------------------------------------------------------------------+
int CMposition::Sizeof(void)
{
   return 48;
}
CMposition::CMposition(void):m_time_open(0),
                             m_time_close(0),
                             m_price_open(0.0),
                             m_price_close(0.0),
                             m_volume(0.0)
{
}
//+------------------------------------------------------------------+
//| True, se a posição é fechada                                     |
//+------------------------------------------------------------------+
bool CMposition::IsActive()
{
   return m_time_close == 0;
}
//+------------------------------------------------------------------+
//| Hora da abertura da posição                                      |
//+------------------------------------------------------------------+
datetime CMposition::TimeOpen(void)
{
   return m_time_open;
}
//+------------------------------------------------------------------+
//| Hora do fechamento da posição                                    |
//+------------------------------------------------------------------+
datetime CMposition::TimeClose(void)
{
   return m_time_close;
}
//+------------------------------------------------------------------+
// Preço de abertura da posição                                      |
//+------------------------------------------------------------------+
double CMposition::PriceOpen(void)
{
   return m_price_open;
}
//+------------------------------------------------------------------+
//| Preço de fechamento da posição                                   |
//+------------------------------------------------------------------+
double CMposition::PriceClose(void)
{
   return m_price_close;
}
//+------------------------------------------------------------------+
//| Volume da posição                                                |
//+------------------------------------------------------------------+
double CMposition::Volume(void)
{
   return m_volume;
}
//+------------------------------------------------------------------+
//| Retorna o tipo da posição                                        |
//+------------------------------------------------------------------+
ENUM_POSITION_TYPE CMposition::PositionType(void)
{
   return m_type;
}
//+------------------------------------------------------------------+
//| Lucro da posição                                                 |
//+------------------------------------------------------------------+
double CMposition::Profit(void)
{
   if(IsActive())
      return 0.0;
   int sign = m_type == POSITION_TYPE_BUY ? 1 : -1;
   double pips = (m_price_close - m_price_open)*sign;
   double profit = pips*m_volume;
   return profit;
}
//+------------------------------------------------------------------+
//| Cria uma posição a partir doa parâmetros carregados              |
//+------------------------------------------------------------------+
static CMposition* CMposition::CreateOnBarOpen(MqlRates &bar, ENUM_POSITION_TYPE pos_type, double volume)
{
   CMposition* position = new CMposition();
   position.m_time_open = bar.time;
   position.m_price_open = bar.open;
   position.m_volume = volume;
   position.m_type = pos_type;
   return position;
}
//+-------------------------------------------------------------------------+
//| Fecha a posição de acordo com o preço de abertura da barra carregada    |
//+-------------------------------------------------------------------------+
void CMposition::CloseOnBarOpen(MqlRates &bar)
{
   m_price_close = bar.open;
   m_time_close = bar.time;
}
//+-------------------------------------------------------------------------+

Por enquanto, o mais importante nesta implementação é a hora de criação da posição Seus campos são protegidos contra as mudanças externas, no entanto o método estático CreateOnBarOpen retorna um objeto de classe com os parâmetros que foram definidos corretamente. Não se pode criar um objeto desta classe, exceto recorrendo a este método. Desta forma, é realizada a segurança de dados contra as mudanças não premeditadas.


Classe do bloco de negociação

Agora é preciso criar uma classe que gerencie essas posições. Ela será um análogo das funções de negociação do MetaTrader. Obviamente, neste módulo, é preciso salvar as posições também. Para fazer isso, existem duas coleções CArrayObj: a primeira -  Active - é necessária para armazenar as posições ativas da estratégia, a outra - History - conterá as posições históricas.

Também, na classe, haverá métodos especiais para abertura e fechamento de posições:

  • EntryAtOpenBar - abre a posição com uma direção e volume certos;
  • CloseAtOpenBar - fecha a posição de acordo com o índice selecionado.

As posições serão abertas e fechadas segundo o preço da barra carregada. Embora esta abordagem não preveja o futuro, é simples de implementar e muito rápida.

Nossa classe CMtrade (como a chamamos) resultou bastante simples:

//+--------------------------------------------------------------------+
//|                                                       Mtester.mqh  |
//|                       Copyright 2017, MetaQuotes Software Corp.    |
//|                                               http://www.mql5.com  |
//+--------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#include <Object.mqh>
#include <Arrays\ArrayObj.mqh>
#include "Mposition.mqh"
//+------------------------------------------------------------------+
//| Módulo de negociação de abertura de posições virtuais            |
//+------------------------------------------------------------------+
class CMtrade
{
public:
               CMtrade(void);
               ~CMtrade();
   CArrayObj   Active;
   CArrayObj   History;
   void        EntryAtOpenBar(MqlRates &bar, ENUM_POSITION_TYPE pos_type, double volume);
   void        CloseAtOpenBar(MqlRates &bar, int pos_index);
};
//+------------------------------------------------------------------+
//| Construtor por padrão                                            |
//+------------------------------------------------------------------+
CMtrade::CMtrade(void)
{
   Active.FreeMode(false);
}
//+------------------------------------------------------------------+
//| Remoção de todas as posições restantes                           |
//+------------------------------------------------------------------+
CMtrade::~CMtrade()
{
   Active.FreeMode(true);
   Active.Clear();
}
//+------------------------------------------------------------------+
//| Cria uma nova posição e a adiciona à lisa de posições            |
//| ativas.                                                          |
//+------------------------------------------------------------------+
void CMtrade::EntryAtOpenBar(MqlRates &bar, ENUM_POSITION_TYPE pos_type, double volume)
{
   CMposition* pos = CMposition::CreateOnBarOpen(bar, pos_type, volume);
   Active.Add(pos);
}
//+------------------------------------------------------------------+
//| Fecha a posição virtual do índice pos_index segundo o preço      |
//| de abertura da barra carregada                                   |
//+------------------------------------------------------------------+
void CMtrade::CloseAtOpenBar(MqlRates &bar, int pos_index)
{
   CMposition* pos = Active.At(pos_index);
   pos.CloseOnBarOpen(bar);
   Active.Delete(pos_index);
   History.Add(pos);
}
//+------------------------------------------------------------------+

 Praticamente toda a sua funcionalidade é reduzida a dois recursos:

  1. Obter a nova posição do método estático CMposition::CreateOnBarOpen e adicioná-la na lista Active (método EntryOnOpenBar);
  2. Colocar a posição selecionada - da lista de posições ativas - na lista das posições históricas, levando em consideração que a posição móvel é fechada pelo método estático CMposition::CLoseOnBarOpen.

Nós criamos uma classe de negociação, e agora temos todos os componentes para testar o EA.


O primeiro teste do EA. Trabalhando no otimizador

Reunimos todos os componentes. Realizamos o código-fonte da estratégia em duas médias móveis para o funcionamento do otimizador matemático. 

//+---------------------------------------------------------------------+
//|                                                       MExpert.mq5   |
//|                       Copyright 2017, MetaQuotes Software Corp.     |
//|                                               http://www.mql5.com   |
//+---------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include <RingBuffer\RiSma.mqh>
#include "Mtester.mqh"

input int PeriodFastMa = 9;
input int PeriodSlowMa = 16;

CRiSMA FastMA;    // Buffer circular para cálculo da média móvel rápida
CRiSMA SlowMA;    // Buffer circular para cálculo da média móvel lenta
CMtrade Trade;    // Módulo de negociação para cálculos virtuais

//+------------------------------------------------------------------+
//| Cotações em que será realizado o teste                           |
//+------------------------------------------------------------------+
MqlRates Rates[];
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   //-- Verificamos se a combinação de parâmetros é correta
   //-- A média rápida não pode ser superior à lenta
   //if(PeriodFastMa >= PeriodSlowMa)
   //   return INIT_PARAMETERS_INCORRECT;
   //-- Inicializamos o período dos buffers circulares
   FastMA.SetMaxTotal(PeriodFastMa);
   SlowMA.SetMaxTotal(PeriodSlowMa);
   //-- Carregamos as cotações para o símbolo especificado.
   if(LoadRates(Symbol(), Rates)==-1)
   {
      printf("Cotações para os símbolo " + Symbol() + " não encontradas. Crie o recurso de cotações adequado.");
      return INIT_FAILED;
   }
   printf("Carregadas " + (string)ArraySize(Rates) + " barras do símbolo " + Symbol());
   return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Descrevemos a estratégia                                         |
//+------------------------------------------------------------------+
double OnTester()
{
   for(int i = 1; i < ArraySize(Rates)-1; i++)
   {
      MqlRates bar = Rates[i];
      FastMA.AddValue(Rates[i].close);
      SlowMA.AddValue(Rates[i].close);
      ENUM_POSITION_TYPE pos_type = FastMA.SMA() > SlowMA.SMA() ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
      //-- Fechamos a posição oposta em relação ao sinal atual
      for(int k = Trade.Active.Total()-1; k >= 0 ; k--)
      {
         CMposition* pos = Trade.Active.At(k);
         if(pos.PositionType() != pos_type)
            Trade.CloseAtOpenBar(Rates[i+1], k);   
      }
      //-- Se não houver posições, fechamos a nova direção definida.
      if(Trade.Active.Total() == 0)
         Trade.EntryAtOpenBar(Rates[i+1], pos_type, 1.0);
   }
   double profit = 0.0;
   for(int i = 0; i < Trade.History.Total(); i++)
   {
      CMposition* pos = Trade.History.At(i);
      profit += pos.Profit();
   }
   return profit;
}
//+------------------------------------------------------------------+

Agora a função OnTester está preenchida completamente. O código está organizado de maneira muito simples. Descreveremos seu funcionamento por etapas.

  1. São examinadas todas as cotações no ciclo for.
  2. Dentro do ciclo, é determinada a direção atual da transação: compra, se a SMA rápida estiver acima da SMA lenta, e venda - no caso oposto.
  3. São examinadas todas as transações ativas, e se a direção não coincidir com a tendência atual, elas serão fechadas.
  4. Se não houver nenhuma posição, será aberta uma nova posição na direção especificada.
  5. No final da pesquisa detalhada, novamente são procuradas todas as posições fechadas e é calculado seu lucro total, lucro esse que é devolvido pelo testador de estratégias.

Nosso EA está pronto para teste no otimizador. Basta executá-lo em modo de cálculos matemáticos. Para verificar que a otimização está funcionando, realizamos uma pesquisa detalhada dos parâmetros da média móvel, como mostrado na figura abaixo: 

Fig. 5. Seleção do campo otimizado de parâmetros

No nosso exemplo, há 1 000 corridas da otimização, cada uma das quais processa 1 ano do histórico de minuto. No entanto, neste modo, o cálculo não leva muito tempo. Num computador com um processador i7, toda a otimização leva cerca de 1 minuto, após o qual o gráfico é construído:

Fig. 6 Gráfico de 1 000 corridas no modo "otimização lenta".

Mas, por enquanto, temos poucas ferramentas para analisar os resultados obtidos. Na verdade, tudo o que temos agora é um único número que representa um lucro virtual. Para remediar esta situação, é necessário não só idear um mecanismo de geração e carregamento de dados de otimização, mas também desenvolver um formato para esses dados. Falaremos sobre isso mais tarde.

Salvando resultados da otimização usando o mecanismo de quadros

No MetaTrader 5, é implementado um método de trabalho muito avançado com dados personalizados. Ele se baseia no mecanismo de geração e a obtenção de quadros. Eles, de facto, são dados binários comuns dispostos quer como valores individuais quer como uma matriz com esses valores. Por exemplo, no momento da otimização, na função OnTester, pode-se gerar uma matriz de dados de tamanho arbitrário e enviá-la para o testador de estratégias do MetaTrader 5. Os dados contidos na matriz podem ser lidos usando a função FrameNext, e, mais para frente, podem ser processados, por exemplo, para visualizar a informação na tela. O trabalho em si com quadros só é possível no modo de otimização, e só dentro de três funções: OnTesterInit(), OnTesterDeinit() e OnTesterPass(). Todos eles não têm parâmetros e não retornam nenhum valor. Mas tudo é mais fácil do que pode parecer. Para ilustrar isso, podemos escrever um script simples mostrando o algoritmo geral de trabalho com quadros:

//+--------------------------------------------------------------------+
//|                                                OnTesterSample.mq5  |
//|                       Copyright 2017, MetaQuotes Software Corp.    |
//|                                               http://www.mql5.com  |
//+--------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
input int Param = 1;
//+------------------------------------------------------------------+
//| OnTesterInit function                                            |
//+------------------------------------------------------------------+
void OnTesterInit()
{
   printf("Começamos a otimização");      
}
//+------------------------------------------------------------------+
//| Aqui ocorre a execução da estratégia                             |
//+------------------------------------------------------------------+
double OnTester()
{
   uchar data[5] = {1,2,3,4,5};        // Geramos os dados para osso quadro
   FrameAdd("sample", 1, Param, data); // Criamos um novo quadro com nossos dados
   return 3.0;
}
//+------------------------------------------------------------------+
//| Aqui é possível obter o último quadro de otimização adicionado   |
//+------------------------------------------------------------------+
void OnTesterPass()
{
   ulong pass = 0;
   string name = "";
   ulong id = 0;
   double value = 0.0;
   uchar data[];
   FrameNext(pass, name, id, value, data);
   //-- formamos o arquivo de execução e o adicionamos a um arquivo zip
   printf("Obtido o novo quadro # " + (string)pass + ". Nome " + (string)name + " ID: " + (string)id + " Nome do sample ID: 1 Valor do parâmetro Param:: " + DoubleToString(value, 0));
}
//+------------------------------------------------------------------+
//| OnTesterDeinit function                                          |
//+------------------------------------------------------------------+
void OnTesterDeinit()
{
   printf("Concluímos a otimização");
}
//+------------------------------------------------------------------+

Executamos este código no testador de estratégias usando o modo de cálculos matemáticos. Definimos o modo de otimização lenta. Mudaremos o parâmetro único Param de 10 para 90, em incrementos de 10.

Imediatamente após a execução da otimização, começam a chegar mensagens informando-nos sobre a obtenção de novos quadros. O inicio e o final da otimização também são monitorados através de eventos especiais. Log de funcionamento do aplicativo:

2017.12.19 16:58:08.101 OnTesterSample (EURUSD,M15)     Começamos a otimização
2017.12.19 16:58:08.389 OnTesterSample (EURUSD,M15)     Obtido o novo quadro # 1. Nome sample ID: 1 Valor do parâmetro Param: 20
2017.12.19 16:58:08.396 OnTesterSample (EURUSD,M15)     Obtido o novo quadro # 0. Nome do sample ID: 1 Valor do parâmetro Param: 10
2017.12.19 16:58:08.408 OnTesterSample (EURUSD,M15)     Obtido o novo quadro # 4. Nome do sample ID: 1 Valor do parâmetro Param: 50
2017.12.19 16:58:08.426 OnTesterSample (EURUSD,M15)     Obtido o novo quadro # 5. Nome do sample ID: 1 Valor do parâmetro Param: 60
2017.12.19 16:58:08.426 OnTesterSample (EURUSD,M15)     Obtido o novo quadro # 2. Nome do sample ID: 1 Valor do parâmetro Param: 30
2017.12.19 16:58:08.432 OnTesterSample (EURUSD,M15)     Obtido o novo quadro # 3. Nome do sample ID: 1 Valor do parâmetro Param: 40
2017.12.19 16:58:08.443 OnTesterSample (EURUSD,M15)     Obtido o novo quadro # 6. Nome do sample ID: 1 Valor do parâmetro Param: 70
2017.12.19 16:58:08.444 OnTesterSample (EURUSD,M15)     Obtido o novo quadro # 7. Nome do sample ID: 1 Valor do parâmetro Param: 80
2017.12.19 16:58:08.450 OnTesterSample (EURUSD,M15)     Obtido o novo quadro # 8. Nome do sample ID: 1 Valor do parâmetro Param: 90
2017.12.19 16:58:08.794 OnTesterSample (EURUSD,M15)     Concluímos a otimização

Para nós, as mensagens mais interessantes são aquelas que apresentam informações sobre o número do quadro, seu identificador e valor do parâmetro Param. Todas essas informações valiosas podem ser conhecidas com a ajuda da função FrameNext. 

Uma característica interessante deste modo é a dupla inicialização do EA. O EA, cujo código tem dados do manipulador de eventos, é executado duas vezes: a primeira vez, no otimizador de estratégias, enquanto a segunda vez - no gráfico, em tempo real. No otimizador, o EA gera novos dados, enquanto, o Expert, carregado no gráfico, os recebe. Assim, embora os códigos fonte do EA estejam num lugar, eles são processados ​​por diferentes instâncias do EA. 

Recebidos os dados na função OnTesterPass, podemos processá-los de quaisquer maneiras. No caso do teste, estes dados são simplesmente exibidos no console usando a função printf. Porém, o processamento de dados que precisamos organizar pode ser muito mais complicado. Falaremos sobre isto na próxima seção.


Obtendo a representação de bytes do histórico de posições. Armazenamento de dados nos quadros

O mecanismo de quadros proporciona uma maneira conveniente de armazenamento, processamento e divulgação de informações. No entanto, precisamos gerar essa informação em si. No exemplo acima, isto se tratava de uma simples matriz estática uchar contendo os valores 1, 2, 3, 4, 5. São poucos os beneficios desse tipo de dados. Mas a matriz de bytes pode ser de qualquer comprimento e armazenar quaisquer dados. Para isto, os tipos de dados personalizados devem se converter numa matriz de bytes uchar. Nós já fizemos com a MqlRates algo parecido, quando armazenamos cotações numa matriz de bytes. Realizaremos a mesma abordagem para nossos dados.

O testador de estratégias personalizado consiste em duas partes. A primeira parte gera os dados, a segunda os analisa e exibe numa forma conveniente para o usuário. É também evidente que a informação básica para a análise da estratégia pode ser obtida analisando todas as transações do histórico. Portanto, no final de cada corrida converteremos todas as transações históricas numa matriz de bytes e, depois, adicioná-la a um novo quadro. Após obter esse quadro na função OnTesterPass(), podemos adicioná-lo ao obtido anteriormente por meio da criação de toda uma coleção de quadros. 

Os dados ~sobre as posições deverão ser não apenas convertidos numa matriz de bytes, mas também descarregados a partir dela. Para fazer isso, é preciso escrever dois procedimentos para cada tipo de dados: 

  • Procedimento para converter o tipo personalizado numa matriz de bytes;
  • Procedimento para a converter a matriz de bytes num tipo personalizado.

Nós já escrevemos o módulo CMtrade com duas coleções de posições, isto é, ativas e históricas. Nós nos concentramos apenas em posições históricas. Os procedimentos para a conversão de posições virtuais devem ser escritos em conformidade com os métodos apropriados.

Método que converte a posição uma matriz de bytes:

//+------------------------------------------------------------------+
//| Converte a posição em bytes sob a forma de uma matriz            |
//+------------------------------------------------------------------+
int CMposition::ToCharArray(int dst_start, uchar &array[])
{
   int offset = dst_start;
   //-- Copy time open position
   type2char.time_value = m_time_open;
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(datetime));
   offset += sizeof(datetime);
   //-- Copy time close position
   type2char.time_value = m_time_close;
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(datetime));
   offset += sizeof(datetime);
   //-- Copy price open position
   type2char.double_value = m_price_open;
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double));
   offset += sizeof(double);
   //-- Copy price close position
   type2char.double_value = m_price_close;  
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double));
   offset += sizeof(double);
   //-- Copy volume position
   type2char.double_value = m_volume; 
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double));
   offset += sizeof(double);
   //-- Copy spread symbol
   type2char.int_value = m_spread;
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(int));
   offset += sizeof(int);
   //-- Copy type of position
   type2char.int_value = m_type;
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(char));
   offset += sizeof(int);
   //-- return last offset
   return offset;
}

 Procedimento inverso:

//+------------------------------------------------------------------+
//| Carrega a posição a partir da matriz de bytes                    |
//+------------------------------------------------------------------+
int CMposition::FromCharArray(int dst_start, uchar &array[])
{
   int offset = dst_start;
   //-- Copy time open position
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(datetime));
   m_time_open = type2char.time_value;
   offset += sizeof(datetime);
   //-- Copy time close position
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(datetime));
   m_time_close = type2char.time_value;
   offset += sizeof(datetime);
   //-- Copy price open position
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(double));
   m_price_open = type2char.double_value;
   offset += sizeof(double);
   //-- Copy price close position
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(double));
   m_price_close = type2char.double_value;
   offset += sizeof(double);
   //-- Copy volume position
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(double));
   m_volume = type2char.double_value;
   offset += sizeof(double);
   //-- Copy spread symbol
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(int));
   m_spread = type2char.int_value;
   offset += sizeof(int);
   //-- Copy type of position
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(int));
   m_type = (ENUM_POSITION_TYPE)type2char.int_value;
   offset += sizeof(int);
   //-- return last offset
   return offset;
}

O união de TypeToChar (é usada sua instancia type2char) é o núcleo de ambos os procedimentos de conversão:

//+------------------------------------------------------------------+
//| Conversão de tipos simples numa matriz de bytes                  |
//+------------------------------------------------------------------+
union TypeToChar
{
   uchar    char_array[128];
   int      int_value;
   double   double_value;
   float    float_value;
   long     long_value;
   short    short_value;
   bool     bool_value;
   datetime time_value;
   char     char_value;
};

Tudo é organizado como a declaração de RateToByte discutida na seção sobre a conversão de cotações. 

Ambos os procedimentos são projetados para permitir baixar os dados de uma matriz global contendo dados sobre todas as posições virtuais fechadas. Isso permite formar um algoritmo de pesquisa detalhada altamente eficiente que não requer cópia adicional de memória.

A pesquisa detalhada de todas as posições históricas é realizada pela classe CMTrade. Isso é lógico, dado que é nele é armazenada a coleção de posições históricas. Uma classe, como CMposition, funciona em duas direções: permite converter uma coleção de posições históricas numa matriz uchar e, realizar o procedimento inverso, ou seja, carregar as posições históricas a partir da matriz de bytes.

Procedimento para a conversão de uma coleção numa matriz de bytes:

//+------------------------------------------------------------------+
//| Converte a lista de posições históricas numa arquivo zip         |
//| como uma matriz de bytes . Retorna True, se for bem-sucedido     |
//| caso contrário, False.                                           |
//+------------------------------------------------------------------+
bool CMtrade::ToCharArray(uchar &array[])
{
   int total_size = CMposition::Sizeof()*History.Total();
   if(total_size == 0)
   {
      printf(__FUNCTION__ +  ": Received  array is empty");
      return false;
   }
   if(ArraySize(array) != total_size && ArrayResize(array, total_size) != total_size)
   {
      printf(__FUNCTION__ +  ": failed resized received array");
      return false;
   }
   //-- Armazenamos as posições no fluxo de bytes
   for(int offset = 0, i = 0; offset < total_size; i++)
   {
      CMposition* pos = History.At(i);
      offset = pos.ToCharArray(offset, array);
   }
   return true;
}

Procedimento inverso:

//+--------------------------------------------------------------------------+
//| Carrega a lista de posições do histórico a partir de um arquivo zip      |
//| carregado como uma matriz de bytes. Retorna True, se for bem-sucedid     |
//| caso contrário, False.                                                   |
//+--------------------------------------------------------------------------+
bool CMtrade::FromCharArray(uchar &array[], bool erase_prev_pos = true)
{
   if(ArraySize(array) == 0)
   {
      printf(__FUNCTION__ +  ": Received  array is empty");
      return false;
   }
   //-- O tamanho do fluxo de bytes deve corresponder exatamente à representação de bytes das posições
   int pos_total = ArraySize(array)/CMposition::Sizeof();
   if(ArraySize(array)%CMposition::Sizeof() != 0)
   {
      printf(__FUNCTION__ +  ": Wrong size of received  array");
      return false;
   }
   if(erase_prev_pos)
      History.Clear();
   //-- Restauramos todas as posições a partir do fluxo de bytes
   for(int offset = 0; offset < ArraySize(array);)
   {
      CMposition* pos = new CMposition();
      offset = pos.FromCharArray(offset, array);
      History.Add(pos);
   }
   return History.Total() > 0;
}

Para coletar todos os itens, basta obter a representação de bytes das posições históricas no final da corrida e mantê-las num quadro:

//+------------------------------------------------------------------+
//| Descrevemos a estratégia                                         |
//+------------------------------------------------------------------+
double OnTester()
{
   for(int i = 1; i < ArraySize(Rates)-1; i++)
   {
      MqlRates bar = Rates[i];
      FastMA.AddValue(Rates[i].close);
      SlowMA.AddValue(Rates[i].close);
      ENUM_POSITION_TYPE pos_type = FastMA.SMA() > SlowMA.SMA() ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
      //-- Fechamos a posição oposta em relação ao sinal atual
      for(int k = Trade.Active.Total()-1; k >= 0 ; k--)
      {
         CMposition* pos = Trade.Active.At(k);
         if(pos.PositionType() != pos_type)
            Trade.CloseAtOpenBar(Rates[i+1], k);   
      }
      //-- Se não houver posições, fechamos a nova direção definida.
      if(Trade.Active.Total() == 0)
         Trade.EntryAtOpenBar(Rates[i+1], pos_type, 1.0);
   }
   uchar array[];
   //-- Obtemos a representação de bytes das posições de histórico
   Trade.ToCharArray(array); 
   //-- Carregamos a representação de bytes no quadro e o carregamos para processamento adicional
   FrameAdd(MTESTER_STR, MTESTER_ID, 0.0, array);  
   return Trade.History.Total();
}

Uma vez que o quadro é formado e enviado para o procedimento de processamento OnTesterPass(), é preciso pensar sobre o que fazer com ele. Já dissemos que nosso testador de estratégias consiste em duas partes: um bloco de geração de dados e outro de análise de informações coletadas. Para esta análise, precisamos preservar todos os quadros gerados num formato conveniente e econômico, para, mais tarde, poder analisar facilmente esse formato. Para fazer isso, pode-se usar um arquivo zip. Em primeiro lugar, ele efetivamente comprime os dados e, portanto, mesmo milhares informações de transações não ocuparão muito espaço. Em segundo lugar, ele fornece um sistema de arquivos conveniente. Cada corrida pode ser armazenada num arquivo separado dentro de um único arquivo zip.

Assim, podemos escrever o processo de conversão do conteúdo de bytes do quadro numa arquivo zip.

//+------------------------------------------------------------------+
//| Adicionamos cada nova execução num arquivo zip                   |
//+------------------------------------------------------------------+
void OnTesterPass()
{
   ulong pass = 0;
   string name = "";
   ulong id = 0;
   double value = 0.0;
   uchar data[];
   FrameNext(pass, name, id, value, data);
   //-- formamos o arquivo de execução e o adicionamos a um arquivo zip
   printf("Obtido um novo quadro com um tamanho de " + (string)ArraySize(data));
   string file_name = name + "_" + (string)id + "_" + (string)pass + "_" + DoubleToString(value, 5)+".hps";
   CZipFile* zip_file = new CZipFile(file_name, data);
   Zip.AddFile(zip_file);
}

Graças ao fato da classe, para trabalhar com arquivos zip, ser poderosa o suficiente e ter métodos universais, é muito simples adicionar no arquivo uma nova corrida como um arquivo separado. De fato, na OnTesterPass, é adicionado um novo arquivo zip num arquivo zip declarado a nível global:

CZip     Zip;     // Arquivo zim que vamos preencher com as execuções da otimização

Este procedimento é chamado em paralelo no final de cada passagem de otimização e requer grandes recursos computacionais.

Após o fim da otimização, é preciso armazenar o arquivo zip formado como o arquivo zip adequado. Fazer isso também é muito simples. Isso é feito no procedimento OnTesterDeinit():

//+--------------------------------------------------------------------------------------+
//| Armazenamos o arquivo zip das execuções na unidade de disco rígido do computador     |
//+--------------------------------------------------------------------------------------+
void OnTesterDeinit()
{
   Zip.SaveZipToFile(OptimizationFile, FILE_COMMON);
   string f_totals = (string)Zip.TotalElements();
   printf("Otimização concluída. Total de corridas de otimização armazenado: " + f_totals);
}

Aqui OptimizationFile é um parâmetro personalizado de cadeia de caracteres que especifica o nome da otimização. Por padrão é "Optimization.zip". Assim, o arquivo zip apropriado será gravado após a conclusão da otimização de nossa estratégia atualizada SmaSample. Encontramo-lo na pasta Files e abrimos usando ferramentas padrão:

Fig. 7. Conteúdo interno do arquivo de otimização

Como pode ser visto, todas as execuções armazenados estão excelentemente armazenadas, com um elevado grau de compactação, de 3 a 5 vezes. 

Coletados e armazenados todos os dados no disco rígido, é preciso carregá-los em outro programa e analisar. Lidaremos com isso na próxima seção.


Criando um analisador de estratégias

Na seção anterior, nós criamos um arquivo zip incluindo informações sobre todas as execuções. Agora é necessário processar essa informação. Para fazer isso, criamos o programa especial H-Tester Analyzer. Ele carregará o arquivo gerado e exibirá cada corrida como um gráfico de saldo. Além disso, o M-Tester Analyzer calculará estatísticas de resumo para a execução selecionada.

Uma das principais características de todo o nosso complexo de teste criado é a capacidade de armazenar informações sobre todas as passagens, ao mesmo tempo. Isto significa que a otimização pode ser realizada apenas uma vez. Todas suas corridas serão armazenadas num arquivo e transferidas para o usuário. Depois, é necessário baixar qualquer corrida desta otimização e ver suas estatísticas, sem perder tempo chamando repetidamente o testador de estratégias.

Fluxo de ações do analisador:

  1. Carregamento do arquivo de otimização selecionado
  2. Seleção de uma das corridas de otimização neste arquivo
  3. Construção do gráfico do saldo virtual com base nas transações existentes
  4. Cálculo das estatísticas da corrida incluindo parâmetros como número de transações, o lucro total, perda total, fator de lucro, o valor esperado e assim por diante.
  5. Exibição das estatísticas calculadas na forma de uma tabela na janela principal.

É necessário fornecer ao usuário com as ferramentas para selecionar uma corrida aleatória a partir do arquivo, para fazer isso, proporcionamos uma fácil transição entre a execução atual e a seguinte ou anterior, e daremos a oportunidade de definir qualquer número de corrida.

O programa será baseado no motor gráfico CPanel. Até o momento, não há nenhum artigo separado sobre esta biblioteca, mas é fácil de aprender, compacta e tem sido repetidamente usada em vários projetos e artigos.

O código principal do nosso analisador está localizado na classe CAnalizePanel, derivada da CElChart. O analisador é realizado na forma de um EA. O arquivo principal do EA inicia a janela gráfica do analisador. Este é o arquivo principal do EA:

//+--------------------------------------------------------------------+
//|                                                     mAnalizer.mq5  |
//|                       Copyright 2017, MetaQuotes Software Corp.    |
//|                                               http://www.mql5.com  |
//+--------------------------------------------------------------------+
#include "mAnalizerPanel.mqh"
CAnalyzePanel Panel;
input string FilePasses = "Optimization.zip";
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   Panel.Width(800);
   Panel.Height(630);
   Panel.XCoord(10);
   Panel.YCoord(20);
   Panel.LoadFilePasses(FilePasses);
   Panel.Show();
   ChartRedraw();
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   Panel.Hide();
}

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   switch(id)
   {
      case CHARTEVENT_OBJECT_ENDEDIT:
      {
         CEventChartEndEdit event(sparam);
         Panel.Event(&event);
         break;
      }
      case CHARTEVENT_OBJECT_CLICK:
      {
         CEventChartObjClick event(sparam);
         Panel.Event(&event);
         break;
      }
   }
   ChartRedraw();
}
//+------------------------------------------------------------------+

Como se pode ver, o código é muito simples. Cria-se um objeto do tipo CAnalyzePanel. Em seguida, no método OnInit, define-se seu tamanho, após o que será exibido no gráfico atual (método Show). De todos os eventos que vêm do gráfico, estamos interessados ​​em apenas dois: o fim do texto e clique no objeto gráfico. Estes eventos são convertidos num objeto especial do tipo CEvent e são enviados por nosso painel (Panel.Event(...)). O painel do analisador recebe esses eventos e os processa.

Agora descrevamos o painel do analisador. Ele consiste na classe CAnalyzePanel, que é grande, por isso não vamos publicar o conteúdo aqui totalmente. Quem quiser pode analisá-la no código completo em anexo ao artigo. Nós nos limitaremos a uma breve descrição de como ela funciona, para o qual apresentaremos o protótipo da classe:

//+--------------------------------------------------------------------+
//|                                                     mAnalizer.mq5  |
//|                       Copyright 2017, MetaQuotes Software Corp.    |
//|                                               http://www.mql5.com  |
//+--------------------------------------------------------------------+
#include <Panel\ElChart.mqh>
#include <Panel\ElButton.mqh>
#include <Graphics\Graphic.mqh>
#include "ListPass.mqh"
#include "TradeAnalyze.mqh"
//+------------------------------------------------------------------+
//| Painel do analisador de execuções do analisador matemático       |
//+------------------------------------------------------------------+
class CAnalizePanel : public CElChart
{
private:
   // - Matrizes de elementos e suas estatísticas
   CArrayObj      m_stat_descr;     // Descrição de estatísticas
   CArrayObj      m_stat_all;       // Valor de estatísticas para todas as transações
   CArrayObj      m_stat_long;      // Valor de estatísticas para transações longas
   CArrayObj      m_stat_short;     // Valor de estatísticas para transações curtas
   CTradeAnalize  m_analize;        // Módulo de cálculo de estatística
   //-- Elementos gráficos
   CElChart       m_name_analyze;   // Nome da janela principal
   CElChart       m_np;             // Rótulo "Pass #"
   CElChart       m_of_pass;        // Rótulo "of ### passes"
   CElChart       m_pass_index;     // Janela de entrada do número da execução
   CElButton      m_btn_next;       // Botão "seguinte execução"
   CElButton      m_btn_prev;       // Botão "anterior execução"
   CGraphic       m_graphic;        // Gráfico da dinâmica do saldo
   //-- Infra-estrutura
   CListPass      m_passes;         // Lista de execuções
   int            m_curr_pass;      // Índice da execução atual
   CCurve*        m_balance_hist;   // Linha da dinâmica do saldo no gráfico
   bool           IsEndEditPass(CEvent* event);
   bool           IsClick(CEvent* event, CElChart* el);
   void           NextPass(void);
   void           PrevPass(void);
   int            GetCorrectPass(string text);
   void           RedrawGraphic(void);
   void           RedrawCurrPass(void);
   void           PlotStatistic(void);
   string         TypeStatToString(ENUM_MSTAT_TYPE type);
   void           CreateStatElements(void);
   string         ValueToString(double value, ENUM_MSTAT_TYPE type);
public:
                  CAnalizePanel(void);
   bool           LoadFilePasses(string file_name, int file_common = FILE_COMMON);
   virtual void   OnShow();
   virtual void   OnHide();
   virtual void   Event(CEvent *event);
};

Como pode ser visto, nesta classe, a maioria do trabalho está escondido, e o método público principal é o de carregamento do arquivo zip que contém as corridas da otimização. Todo o trabalho da classe pode ser dividido em três partes:

  1. Criação do gráfico e adicionamento a ele da linha de saldo.
  2. Criação de rótulos de texto na forma de elementos CElChart, que exibem as estatísticas de teste.
  3. Em suma, crescem as estatísticas de corridas.

Descreveremos brevemente cada uma dessas seções. 

Precisamos criar uma série de elementos para exibir todas as estatísticas coletadas para cada corrida. Nosso analisador exibe dez parâmetros estatísticos básicos. Além disso, cada parâmetro é calculado separadamente para todas as transações, apenas para compras e vendas. Adicionalmente, precisamos de mais 10 rótulos de texto, exibe o nome da performance. Assim, é requerido criar 40 rótulos de texto. Para evitar criar cada elemento manualmente, escreveremos o procedimento de automação. Para fazer isso, cada um dos parâmetros estatístico recebe seu próprio identificador numa listagem especial:

//+------------------------------------------------------------------+
//| Identificadores do tipo de valor estatístico                     |
//+------------------------------------------------------------------+
enum ENUM_MSTAT_TYPE
{
   MSTAT_PROFIT,
   MSTAT_ALL_WINS_MONEY,
   MSTAT_ALL_LOSS_MONEY,
   MSTAT_TRADERS_TOTAL,
   MSTAT_WIN_TRADERS,
   MSTAT_LOSS_TRADERS,   
   MSTAT_MAX_PROFIT,
   MSTAT_MAX_LOSS,
   MSTAT_PROFIT_FACTOR,
   MSTAT_MATH_EXP,   
};
#define MSTAT_ELEMENTS_TOTAL 10

Além disso, definimos o identificador para a direção do cálculo:

/+-------------------------------------------------------------------+
//| A estatística pode ser calculada para uma das três direções      |
//+------------------------------------------------------------------+
enum ENUM_MSTATE_DIRECT
{
   MSTATE_DIRECT_ALL,      // Para todas as transações
   MSTATE_DIRECT_LONG,     // Apenas para compras
   MSTATE_DIRECT_SHORT,    // Apenas para vendas
};

O painel consiste em quatro grupos de elementos, cada um dos quais se encontra na sua própria matriz:

  • Elementos que exibem o nome da estatística (matriz m_stat_descr)
  • Elementos que exibem os valores da estatística para todas as transações (matriz m_stat_all)
  • Elementos que exibem os valores da estatística para transações longas (matriz m_stat_long)
  • Elementos que exibem os valores da estatística para transações curtas (matriz m_stat_short)

Todos estes elementos são criados após a primeira inicialização no método CAnalyzePanel::CreateStatElements(void).

Depois que os elementos são criados, eles devem ser preenchidos com os valores corretos. O cálculo destes valores é delegado à classe externa CTradeAnalize:

#include <Arrays\ArrayObj.mqh>
#include <Dictionary.mqh>
#include "..\MTester\Mposition.mqh"
//+------------------------------------------------------------------+
//| Elemento auxiliar contento os campos necessários                 |
//+------------------------------------------------------------------+
class CDiffValues : public CObject
{
public:
   double all;
   double sell;
   double buy;
   CDiffValues(void) : all(0), buy(0), sell(0)
   {
   }
};
//+------------------------------------------------------------------+
//| Classe da análise estatística                                    |
//+------------------------------------------------------------------+
class CTradeAnalize
{
private:
   CDictionary m_values;
   
public:
   void     CalculateValues(CArrayObj* history);
   double   GetStatistic(ENUM_MSTAT_TYPE type, ENUM_MSTATE_DIRECT direct);
};
//+------------------------------------------------------------------+
//| Calcula o valor da estatística                                   |
//+------------------------------------------------------------------+
double CTradeAnalize::GetStatistic(ENUM_MSTAT_TYPE type, ENUM_MSTATE_DIRECT direct)
{
   CDiffValues* value = m_values.GetObjectByKey(type);
   switch(direct)
   {
      case MSTATE_DIRECT_ALL:
         return value.all;
      case MSTATE_DIRECT_LONG:
         return value.buy;
      case MSTATE_DIRECT_SHORT:
         return value.sell;
   }
   return EMPTY_VALUE;
}
//+------------------------------------------------------------------+
//| Calcula o número de transações para cada uma das transações      |
//+------------------------------------------------------------------+
void CTradeAnalize::CalculateValues(CArrayObj *history)
{
   m_values.Clear();
   for(int i = 0; i < MSTAT_ELEMENTS_TOTAL; i++)
      m_values.AddObject(i, new CDiffValues());
   CDiffValues* profit = m_values.GetObjectByKey(MSTAT_PROFIT);
   CDiffValues* wins_money = m_values.GetObjectByKey(MSTAT_ALL_WINS_MONEY);
   CDiffValues* loss_money = m_values.GetObjectByKey(MSTAT_ALL_LOSS_MONEY);
   CDiffValues* total_traders = m_values.GetObjectByKey(MSTAT_TRADERS_TOTAL);
   CDiffValues* win_traders = m_values.GetObjectByKey(MSTAT_WIN_TRADERS);
   CDiffValues* loss_traders = m_values.GetObjectByKey(MSTAT_LOSS_TRADERS);
   CDiffValues* max_profit = m_values.GetObjectByKey(MSTAT_MAX_PROFIT);
   CDiffValues* max_loss = m_values.GetObjectByKey(MSTAT_MAX_LOSS);
   CDiffValues* pf = m_values.GetObjectByKey(MSTAT_PROFIT_FACTOR);
   CDiffValues* mexp = m_values.GetObjectByKey(MSTAT_MATH_EXP);
   total_traders.all = history.Total();
   for(int i = 0; i < history.Total(); i++)
   {
      CMposition* pos = history.At(i);
      profit.all += pos.Profit();
      if(pos.PositionType() == POSITION_TYPE_BUY)
      {
         if(pos.Profit() > 0)
         {
            win_traders.buy++;
            wins_money.buy += pos.Profit();
         }
         else
         {
            loss_traders.buy++;
            loss_money.buy += pos.Profit();
         }
         total_traders.buy++;
         profit.buy += pos.Profit();
      }
      else
      {
         if(pos.Profit() > 0)
         {
            win_traders.sell++;
            wins_money.sell += pos.Profit();
         }
         else
         {
            loss_traders.sell++;
            loss_money.sell += pos.Profit();
         }
         total_traders.sell++;
         profit.sell += pos.Profit();
      }
      if(pos.Profit() > 0)
      {
         win_traders.all++;
         wins_money.all += pos.Profit();
      }
      else
      {
         loss_traders.all++;
         loss_money.all += pos.Profit();
      }
      if(pos.Profit() > 0 && max_profit.all < pos.Profit())
         max_profit.all = pos.Profit();
      if(pos.Profit() < 0 && max_loss.all > pos.Profit())
         max_loss.all = pos.Profit();
   }
   mexp.all = profit.all/total_traders.all;
   mexp.buy = profit.buy/total_traders.buy;
   mexp.sell = profit.sell/total_traders.sell;
   pf.all = wins_money.all/loss_money.all;
   pf.buy = wins_money.buy/loss_money.buy;
   pf.sell = wins_money.sell/loss_money.sell;
}

O cálculo é realizado pelo método CalculateValues. Para que ele funcione, é preciso carregar a matriz CArrayObj contendo elementos CMposition. Mas de onde aparece a matriz dessas posições virtuais?

Na verdade, a classe CAnalyzePanel tem outra classe, nomeadamente, CListPass. É ela que carrega o arquivo zip e cria uma coleção de corridas. Essa classe tem uma estrutura muito simples:

//+--------------------------------------------------------------------+
//|                                                     Optimazer.mq5  |
//|                       Copyright 2017, MetaQuotes Software Corp.    |
//|                                               http://www.mql5.com  |
//+--------------------------------------------------------------------+
#include <Zip\Zip.mqh>
#include <Dictionary.mqh>
#include "..\MTester\MTrade.mqh"
//+------------------------------------------------------------------+
//| Armazena a lista de otimizações                                  |
//+------------------------------------------------------------------+
class CListPass
{
private:
   CZip        m_zip_passes;  // Arquivo de todas as execuções das otimizações
   CDictionary m_passes;      // Posições já carregadas do histórico   
public:
   bool        LoadOptimazeFile(string file_name, int file_common = FILE_COMMON);
   int         PassTotal(void);
   CArrayObj*  PassAt(int index);
};
//+------------------------------------------------------------------+
//| Carrega a lista de otimizações a partir do arquivo zip           |
//+------------------------------------------------------------------+
bool CListPass::LoadOptimazeFile(string file_name,int file_common=FILE_COMMON)
{
   m_zip_passes.Clear();
   if(!m_zip_passes.LoadZipFromFile(file_name, file_common))
   {     
      printf("Failed load optimization file. Last Error");
      return false;
   }
   return true;
}
//+------------------------------------------------------------------+
//| Número de execuções                                              |
//+------------------------------------------------------------------+
int CListPass::PassTotal(void)
{
   return m_zip_passes.TotalElements();
}
//+------------------------------------------------------------------+
//| Retorna a lista de transações executadas com número index        |
//+------------------------------------------------------------------+
CArrayObj* CListPass::PassAt(int index)
{
   if(!m_passes.ContainsKey(index))
   {
      CZipFile* zip_file = m_zip_passes.ElementAt(index);
      uchar array[];
      zip_file.GetUnpackFile(array);
      CMtrade* trade = new CMtrade();
      trade.FromCharArray(array);
      m_passes.AddObject(index, trade);
   }
   CMtrade* trade = m_passes.GetObjectByKey(index);
   //printf("Total Traders: " + (string)trade.History.Total());
   return &trade.History;
}

Como pode ser visto, a classe CListPass carrega o arquivo de otimização, mas não o extrai. Isto significa que, mesmo na memória do computador, todos os dados de otimização não utilizados são armazenados em forma compactada, o que economiza a memória RAM do computador. A corrida solicitada é descompactada e convertida no objeto CMtrade, após o que já é armazenada no repositório interno na forma não compactada. Ao acessar este elemento, da próxima vez, não será necessária outra descompactação.

Novamente consideremos o classe CAnalyzePanel. Nós aprendemos como são carregadas as posições (classe CListPass) e como são calculadas as estatísticas sobre elas (classe CTradeAnalyze). Após criar os elementos gráficos, resta preenchê-los com os valores corretos. O método CAnalyzePanel::PlotStatistic(void) se encarrega disso:

//+------------------------------------------------------------------+
//| Exibe a estatística                                              |
//+------------------------------------------------------------------+
void CAnalyzePanel::PlotStatistic(void)
{
   if(m_stat_descr.Total() == 0)
      CreateStatElements();
   CArrayObj* history = m_passes.PassAt(m_curr_pass-1);
   m_analize.CalculateValues(history);
   for(int i = 0; i < MSTAT_ELEMENTS_TOTAL; i++)
   {
      ENUM_MSTAT_TYPE stat_type = (ENUM_MSTAT_TYPE)i;
      //-- all traders
      CElChart* el = m_stat_all.At(i);
      string v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_ALL), stat_type);
      el.Text(v);
      //-- long traders
      el = m_stat_long.At(i);
      v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_LONG), stat_type);
      el.Text(v);
      //-- short traders
      el = m_stat_short.At(i);
      v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_SHORT), stat_type);
      el.Text(v);
   }
}

Nós consideramos todos os elementos necessários para o funcionamento do painel de nosso analisador. A descrição resultou inconsistente, mas essa é a essência da programação: todos os elementos estão interligados, e, às vezes, é necessário descrevê-los simultaneamente.

É hora de executar o analisador no gráfico. Antes disso, verificamos que o arquivo zip de otimização está presente no diretório FILE_COMMON. Por padrão, o analisador carrega o arquivo "Optimization.zip", e é ele que deve estar no diretório comum.

O maior efeito das funcionalidades realizadas pode ser visto no momento de alternar as corridas. Ao fazer isso, o gráfico e as estatísticas são atualizados automaticamente. A imagem abaixo mostra esse momento:


Fig. 8. Alternância de corridas no analisador de cálculos matemáticos

Para uma melhor compreensão dos painéis, realizamos um esquema gráfico com dicas: contorneamos os principais elementos de interface usando bordas indicando quais as classes e métodos são responsáveis ​​por cada grupo de elementos:


Fig. 9. Principais elementos da interface

No final, descrevemos a estrutura de nosso projeto resultante. Todos os códigos fonte estão no arquivo MTester.zip. O projeto em si está localizado na pasta MQL5\Experts\MTester. No entanto, o projeto precisa da adição de bibliotecas adicionais. Aquelas que não vêm na distribuição padrão do MetaTrader 5, estão presentes neste arquivo na pasta MQL5\Include. Primeiro de tudo, trata-se da biblioteca gráfica CPanel (localização: MQL5\Include\Panel). Também estão a biblioteca para trabalhar com arquivos zip (MQL5\Include\Zip) e a classe para formar a matriz de associação (MQL5\Include\Dictionary). Adicionalmente, para a conveniência dos usuários foram criados dois projetos MQL5. Esse recurso apareceu recentemente, no MetaTrader 5. O primeiro projeto é chamado MTester e contém o testador estratégia real e da própria estratégia, com base no cruzamento de médias móveis (SmaSample.mq5). O segundo projeto é chamado MAnalyzer e contém o código fonte do painel do analisador.

Além dos códigos fonte, o arquivo contém o ficheiro Optimization.zip contendo aproximadamente 160 corridas da estratégia, em dados de teste. Isso ajudará a testar rapidamente a funcionalidade do analisador de corridas, sem a necessidade de uma nova otimização. O arquivo está localizado na pasta MQL5\Files.


Fim do artigo

Por último, podemos dar um breve resumo dos materiais descritos no artigo.

  • O testador de cálculos matemáticos tem uma alta velocidade devido à ausência da simulação do ambiente de negociação. Graças a isso, com base nele, é possível escrever sei próprio algoritmo de alto desempenho para testar estratégias de negociação simples. No entanto, devido à falta de controle sobre a correta execução das operações, pode-se olhar para o futuro acessando as cotações que ainda não têm chegado. Pode ser difícil identificar esse tipo de erros em "Santos Graais prometendo estratégias de negociação sem perdas", mas este é o preço que se paga pelo alto desempenho.
  • Estando no testador de cálculos matemáticos, é impossível ter acesso ao ambiente de negociação. Consequentemente, é impossível receber cotações do instrumento desejado. Portanto, neste modo, você tem que avançar sozinho e carregar os dados necessários a tempo, bem como utilizar suas próprias bibliotecas para calcular os indicadores. O artigo mostrou como preparar, compactar e inserir efetivamente os dados num módulo que execute o programa. Esta técnica também pode ser útil a todos aqueles que desejam distribuir, com seu próprio programa, dados adicionais para seu trabalho.
  • Acesso aos indicadores padrão no modo de cálculos matemático também está fechado. Por isso é necessário para executar o cálculo apenas dos indicadores necessários. No entanto, a velocidade também é muito importante. Portanto, o cálculo independente dos indicadores dentro do EA não é apenas a única, mas também a melhor solução em termos de velocidade. Por fortuna, a biblioteca de buffers circulares pode proporcionar um cálculo eficaz dos indicadores necessários por um tempo constante.
  • No MetaTrader 5, embora o modo de geração de quadros seja um mecanismo complicado, ele é poderoso e dá ao usuário uma enorme oportunidade ao escrever seus algoritmos de análise. Por exemplo, pode-se escrever um testador de estratégias pessoal, o que já foi feito neste artigo. Para usar plenamente os recursos do modo de geração de quadros, é necessário saber trabalhar com dados binários. É a capacidade de trabalhar com este tipo de dados que torna possível gerar dados de tipo complexo, por exemplo, uma lista de posições. Relativamente ao tipo de dados personalizado, o artigo mostrou como criá-lo (classe da posição CMPosition); como convertê-lo numa representação de bytes e adicioná-lo ao quadro; como obter a partir do quadro uma matriz de bytes e convertê-la de volta numa lista personalizada de posições.
  • Uma das partes mais importantes do testador de estratégias é o sistema de armazenamento de dados. Obviamente, durante o teste, são obtidos muitos dados: cada teste pode incluir centenas ou mesmo milhares de corridas, enquanto cada corrida inclui muitas transações, o que pode chegar facilmente a várias dezenas de milhares. O sucesso do projeto depende de quão efetivamente é armazenada e distribuída esta informação. Por isso, foi selecionado o arquivo zip. Como a linguagem MQL5 possui uma poderosa e rápida biblioteca para trabalhar com este tipo de arquivos, torna-se fácil organizar repositórios pessoais contendo os arquivos das corridas das otimizações. Cada otimização é um arquivo zip contendo todas as corridas. Cada corrida é representado por um único arquivo compactado. Devido à alta compactação dos dados, mesmo uma otimização de grandes proporções tem um tamanho muito modesto.
  • Não é suficiente escrever um testador de estratégias pessoal. Também é necessário um subsistema separado que analisa os resultados da otimização. Nós realizamos esse subsistema na forma do programa M-Tester Analyzer. Trata-se de um módulo de software separado que carrega os resultados de otimização num arquivo zip e os exibe num gráfico, mostrando as estatísticas básicas para cada corrida. M-Tester Analyzer é baseado em várias classes e na biblioteca gráfica CPanel. Ele é uma biblioteca simples e conveniente que permite criar uma poderosa interface gráfica, rapidamente. Graças à biblioteca de sistema CGraphic, o analisador exibe um gráfico informativo da dinâmica do saldo. 

Embora nosso resultado tenha sido impressionante e o testador tenha conseguido realizar cálculos matemáticos, ainda faltam muitas coisas úteis. Aqui estão alguns dos componentes prioritários que precisam ser adicionados na próxima versão.

  • Informações sobre o símbolo (nome, valor de um tick, spreads e swap, etc.). Esta informação é necessária para o cálculo de possíveis comissões, spreads e swaps. Também para calcular os lucros em moeda depósito (agora o lucro é considerado em pontos).
  • Informações sobre a estratégia e seus parâmetros para cada corrida. É preciso saber não só o resultado da estratégia, mas também o valor de todos os seus parâmetros. Para fazer isso, no relatório gerado, também é necessário inserir um tipo adicional de dados.
  • Controle das ações realizadas. Nesta fase, é bastante fácil "olhar para o futuro" e se enganar vendo um Graal que nada tem a ver com a realidade. Em futuras versões, é necessário um mecanismo, pelo menos, de controle mínimo. No entanto, é difícil determinar qual deve ser sua aparência.
  • Integração do mecanismo de geração de relatórios com o testador de estratégia real. Nada impede converter o resultado obtido - no testador de estratégias convencional - no formato de relatório desenvolvido por nós. Isso será uma oportunidade para analisar os resultados do teste usando o M-Trade Analyzer. Assim, teremos alguns sistemas de teste e um sistema de análise.
  • Desenvolvimento futuro do M-Trade Analayzer. Agora programa de análise tem apenas funcionalidades básicas. Ele não é suficiente para um processamento de dados completo. É preciso adicionar mais estatísticas e gráficos de saldo separados tanto para compras como para vendas. Também seria bom aprender a armazenar o histórico de transações num arquivo de texto, e depois carregá-lo em Excel.

Examinamos todos os aspectos principais do M-Tester, bem como perspectivas futuras para seu desenvolvimento. Se o tema proposto resultar interessante o suficiente, este artigo vai ver sua continuação. Muito tem sido feito, mas há muito mais trabalho a fazer. Esperemos que uma nova versão do M-Tester apareça em breve!