English Русский 中文 Español Deutsch 日本語
preview
Desenvolvendo um EA multimoeda (Parte 16): Influência de diferentes históricos de cotações nos resultados de testes

Desenvolvendo um EA multimoeda (Parte 16): Influência de diferentes históricos de cotações nos resultados de testes

MetaTrader 5Testador |
146 4
Yuriy Bykov
Yuriy Bykov

Introdução

Como de costume, lembramos que, na parte anterior, começamos a preparar o EA multimoeda em desenvolvimento para operar em uma conta real. Nesse processo, adicionamos suporte a diferentes nomes de instrumentos financeiros, encerramento automático das operações ao ajustar as configurações de estratégias de negociação e retomada adequada das operações do EA após reinicializações por diversos motivos.

Contudo, as etapas de preparação não se encerram aqui. Identificamos outras ações necessárias, mas abordaremos o assunto mais tarde. Por ora, vamos explorar um aspecto importante: garantir resultados consistentes do EA em desenvolvimento entre diferentes corretoras. Sabe-se que, embora semelhantes, as cotações dos instrumentos financeiros não são idênticas entre corretoras. Portanto, ao testar e otimizar com base em certas cotações, ajustamos os parâmetros ideais especificamente para essas condições. Espera-se que, ao operar com cotações diferentes, as variações sejam mínimas, resultando em pequenas diferenças nos resultados operacionais. 

Entretanto, essa questão é importante demais para ser ignorada sem uma verificação detalhada. Por isso, analisaremos o comportamento do nosso EA ao ser testado com cotações de diferentes corretoras.


Comparação dos resultados

Primeiramente, executaremos nosso EA com cotações do servidor MetaQuotes-Demo. O primeiro teste foi realizado com o gerenciamento de risco ativado. No entanto, adiantando os fatos, observamos que, com outras cotações, o gerenciamento de risco encerrou as operações muito antes do fim do intervalo de teste, assim, desativaremos essa função para obter uma visão mais completa dos resultados. Isso permitirá uma comparação mais justa dos resultados. Eis o que obtivemos:


Figura 1. Resultados dos testes com cotações do servidor MetaQuotes-Demo sem o gerenciamento de risco

Agora, conectaremos o terminal a um servidor real de outra corretora e repetiremos o teste do EA com os mesmos parâmetros:

Figura 2. Resultados dos testes com cotações de um servidor real de outra corretora sem o gerenciamento de risco

Essa foi uma reviravolta inesperada. A conta foi completamente zerada em menos de um ano dos dois previstos. Vamos investigar as razões desse comportamento para determinar se é possível corrigir a situação.


Investigando a causa

Salvaremos os relatórios do testador no formato de arquivos XML. Abriremos os arquivos e identificaremos a seção onde começa a lista de operações realizadas. Organizaremos as janelas dos arquivos abertos de modo que seja possível visualizar simultaneamente as partes superiores das listas de operações dos dois relatórios.

Figura 3. Partes superiores das listas de operações realizadas pelo EA durante os testes com cotações de diferentes servidores

Mesmo pelas primeiras linhas dos relatórios, fica claro que as posições foram abertas em momentos distintos. Assim, se houve diferenças nas cotações para os mesmos instantes em servidores distintos, provavelmente elas não tiveram um impacto tão destrutivo quanto os diferentes momentos de abertura.

Vamos analisar onde os momentos de abertura de posições são definidos nas estratégias, para isso devemos verificar o arquivo que implementa a classe da estratégia de negociação simples, SimpleVolumesStrategy.mqh. Ao observarmos o código, rapidamente encontramos o método SignalForOpen(), que retorna o sinal para abertura:

//+------------------------------------------------------------------+
//| Signal for opening pending orders                                |
//+------------------------------------------------------------------+
int CSimpleVolumesStrategy::SignalForOpen() {
// By default, there is no signal
   int signal = 0;

// Copy volume values from the indicator buffer to the receiving array
   int res = CopyBuffer(m_iVolumesHandle, 0, 0, m_signalPeriod, m_volumes);

// If the required amount of numbers have been copied
   if(res == m_signalPeriod) {
      // Calculate their average value
      double avrVolume = ArrayAverage(m_volumes);

      // If the current volume exceeds the specified level, then
      if(m_volumes[0] > avrVolume * (1 + m_signalDeviation + m_ordersTotal * m_signaAddlDeviation)) {
         // if the opening price of the candle is less than the current (closing) price, then 
         if(iOpen(m_symbol, m_timeframe, 0) < iClose(m_symbol, m_timeframe, 0)) {
            signal = 1; // buy signal
         } else {
            signal = -1; // otherwise, sell signal
         }
      }
   }

   return signal;
}

Vemos que o sinal de abertura é baseado nos valores dos volumes de ticks para o instrumento financeiro atual. Os preços (atuais ou anteriores) não são considerados diretamente na geração desse sinal. Na verdade, eles são considerados apenas após a decisão de abrir a posição, influenciando unicamente a direção da abertura. Portanto, parece que a questão reside nas grandes diferenças nos valores dos volumes de ticks provenientes de servidores distintos.

Isso é perfeitamente plausível, já que, para que os gráficos de velas coincidam visualmente entre corretoras, basta fornecer apenas quatro ticks corretos por minuto para construir os preços de Open, Close, High e Low da menor vela no período M1. A quantidade de ticks intermediários dentro dos limites entre Low e High não é relevante. Assim, o número de ticks armazenados no histórico e sua distribuição temporal dentro de uma única vela dependem exclusivamente da corretora, que possui liberdade para definir os parâmetros mais convenientes. Além disso, é importante lembrar que até mesmo servidores de contas demo e reais de uma mesma corretora podem apresentar diferenças.

Se o problema for realmente esse, será possível contorná-lo. Porém, antes de implementarmos essa solução, é essencial garantir que a causa das divergências observadas tenha sido identificada corretamente, para evitar esforços desperdiçados.


Definindo o caminho

Para verificar nossa hipótese, utilizaremos os seguintes instrumentos:

  • Armazenamento do histórico. Adicionaremos ao nosso EA a capacidade de salvar o histórico de operações (abertura e fechamento de posições) ao final de um teste no testador. Esse armazenamento poderá ser feito em arquivo ou em banco de dados. Como inicialmente esse recurso será usado apenas como ferramenta auxiliar, parece mais simples optar pelo armazenamento em arquivo. Caso decidamos utilizá-lo de forma mais permanente no futuro, poderemos expandi-lo para incluir o suporte ao salvamento em banco de dados.

  • Reprodução de negociações. Desenvolveremos um novo EA que não terá nenhuma regra para abertura de posições, mas apenas reproduzirá as operações de abertura e fechamento, lendo-as do histórico salvo pelo outro EA. Como decidimos, por enquanto, salvar o histórico em arquivo, esse novo EA receberá como parâmetro de entrada o nome do arquivo contendo o histórico de negociações, lendo e executando as operações nele registradas.

Após criarmos essas ferramentas, faremos dois testes distintos. No primeiro, executaremos nosso EA no testador utilizando cotações do servidor MetaQuotes-Demo e salvaremos o histórico de operações desse teste em um arquivo. Este será o primeiro teste. Em seguida, utilizaremos o novo EA de reprodução de negociações para repetir o teste com cotações de outro servidor, utilizando o arquivo de histórico salvo anteriormente. Este será o segundo teste. Se as diferenças observadas anteriormente nos resultados das operações forem realmente causadas por grandes variações nos dados de volume de ticks, enquanto os preços permanecerem semelhantes, o segundo teste deverá apresentar resultados semelhantes aos do primeiro.


Salvando o histórico

Existem diversas maneiras de implementar o salvamento do histórico. Por exemplo, podemos adicionar um método à classe CVirtualAdvisor, que seria chamado pelo manipulador de eventos OnTester(). Contudo, essa abordagem amplia a classe existente, adicionando funcionalidades que, em princípio, não são essenciais para o funcionamento principal. Por isso, optaremos por criar uma classe separada, chamada CExpertHistory, dedicada exclusivamente a essa tarefa. Não será necessário instanciar múltiplos objetos dessa classe, podemos torná-la estática, contendo apenas propriedades e métodos estáticos.

O principal método público da classe será Export(), enquanto os demais métodos desempenharão funções auxiliares. O método Export() receberá dois parâmetros: o nome do arquivo onde o histórico será salvo e uma flag para indicar o uso da pasta de dados compartilhada do terminal. O nome do arquivo pode, por padrão, ser uma string vazia. Nesse caso, será gerado automaticamente por um método auxiliar chamado GetHistoryFileName(). Com a flag para salvar na pasta compartilhada, podemos decidir se o arquivo será armazenado na pasta compartilhada ou na pasta local do terminal. Por padrão, o valor estará configurado para salvar na pasta compartilhada, já que abrir a pasta local do agente de teste é mais complicado durante a execução no testador.

Como propriedades da classe, utilizaremos um símbolo delimitador para a abertura do arquivo CSV em modo de gravação, um manipulador para o arquivo aberto, que será utilizado nos métodos auxiliares, e um array com os nomes das colunas dos dados a serem salvos.

//+------------------------------------------------------------------+
//| Export trade history to file                                     |
//+------------------------------------------------------------------+
class CExpertHistory {
private:
   static string     s_sep;            // Separator character
   static int        s_file;           // File handle for writing
   static string     s_columnNames[];  // Array of column names

   // Write deal history to file
   static void       WriteDealsHistory();

   // Write one row of deal history to file 
   static void       WriteDealsHistoryRow(const string &fields[]);

   // Get the first deal date
   static datetime   GetStartDate();

   // Form a file name
   static string     GetHistoryFileName();

public:
   // Export deal history
   static void       Export(
      string exportFileName = "",   // File name for export. If empty, the name is generated
      int commonFlag = FILE_COMMON  // Save the file in shared data folder
   );
};

// Static class variables
string CExpertHistory::s_sep = ",";
int    CExpertHistory::s_file;
string CExpertHistory::s_columnNames[] = {"DATE", "TICKET", "TYPE",
                                          "SYMBOL", "VOLUME", "ENTRY", "PRICE",
                                          "STOPLOSS", "TAKEPROFIT", "PROFIT",
                                          "COMMISSION", "FEE", "SWAP",
                                          "MAGIC", "COMMENT"
                                         };

No método principal Export(), o arquivo será criado e aberto para gravação com o nome especificado ou gerado. Se o arquivo for aberto com sucesso, o método de gravação do histórico de operações será chamado, e o arquivo será fechado.

//+------------------------------------------------------------------+
//| Export deal history                                              |
//+------------------------------------------------------------------+
void CExpertHistory::Export(string exportFileName = "", int commonFlag = FILE_COMMON) {
   // If the file name is not specified, then generate it
   if(exportFileName == "") {
      exportFileName = GetHistoryFileName();
   }

   // Open the file for writing in the desired data folder
   s_file = FileOpen(exportFileName, commonFlag | FILE_WRITE | FILE_CSV | FILE_ANSI, s_sep);

   // If the file is open,
   if(s_file > 0) {
      // Set the deal history
      WriteDealsHistory();

      // Close the file
      FileClose(s_file);
   } else {
      PrintFormat(__FUNCTION__" | ERROR: Can't open file [%s]. Last error: %d",  exportFileName, GetLastError());
   }
}

No método GetHistoryFileName(), o nome do arquivo será composto de vários fragmentos. Primeiro, será adicionado o nome do EA e sua versão, caso especificada na constante __VERSION__. Em segundo lugar, incluirá as datas de início e término do histórico de operações. A data de início será determinada pela data da primeira operação no histórico, obtida pelo método GetStartDate(). A data de término será baseada no horário atual, pois o histórico será exportado após a conclusão do teste. Ou seja, o horário atual no momento da chamada do método de salvamento representará o fim do teste. Por fim, ao nome do arquivo serão adicionados valores de algumas métricas do teste: saldo inicial, saldo final, rebaixamento e o índice de Sharpe.

Se o nome for muito longo, ele será encurtado para o tamanho permitido e será adicionado o sufixo ".history.csv".

//+------------------------------------------------------------------+
//| Form the file name                                               |
//+------------------------------------------------------------------+
string CExpertHistory::GetHistoryFileName() {
   // Take the EA name
   string fileName = MQLInfoString(MQL_PROGRAM_NAME);

   // If a version is specified, add it
#ifdef __VERSION__
   fileName += "." + __VERSION__;
#endif

   fileName += " ";

   // Add the history start and end date
   fileName += "[" + TimeToString(GetStartDate(), TIME_DATE);
   fileName += " - " + TimeToString(TimeCurrent(), TIME_DATE) + "]";

   fileName += " ";

   // Add some statistical characteristics
   fileName += "[" + DoubleToString(TesterStatistics(STAT_INITIAL_DEPOSIT), 0);
   fileName += ", " + DoubleToString(TesterStatistics(STAT_INITIAL_DEPOSIT) + TesterStatistics(STAT_PROFIT), 0);
   fileName += ", " + DoubleToString(TesterStatistics(STAT_EQUITY_DD_RELATIVE), 0);
   fileName += ", " + DoubleToString(TesterStatistics(STAT_SHARPE_RATIO), 2);
   fileName += "]";

   // If the name is too long, shorten it
   if(StringLen(fileName) > 255 - 13) {
      fileName = StringSubstr(fileName, 0, 255 - 13);
   }

   // Add extension
   fileName += ".history.csv";

   return fileName;
}

No método de gravação do histórico em arquivo, primeiro gravamos o cabeçalho, ou seja, uma linha com os nomes das colunas de dados. Em seguida, selecionamos todo o histórico disponível e iteramos sobre todas as operações. Para cada operação, obtemos suas propriedades. Se a operação for de abertura de posição ou uma operação de balanço, formamos um array com os valores de todas as propriedades da operação e o passamos para o método de gravação de uma operação WriteDealsHistoryRow().

//+------------------------------------------------------------------+
//| Write deal history to file                                       |
//+------------------------------------------------------------------+
void CExpertHistory::WriteDealsHistory() {
   // Write a header with column names
   WriteDealsHistoryRow(s_columnNames);

   // Variables for each deal properties
   uint     total;
   ulong    ticket = 0;
   long     entry;
   double   price;
   double   sl, tp;
   double   profit, commission, fee, swap;
   double   volume;
   datetime time;
   string   symbol;
   long     type, magic;
   string   comment;

   // Take the entire history
   HistorySelect(0, TimeCurrent());
   total = HistoryDealsTotal();

   // For all deals
   for(uint i = 0; i < total; i++) {
      // If the deal is successfully selected,
      if((ticket = HistoryDealGetTicket(i)) > 0) {
         // Get the values of its properties
         time  = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
         type  = HistoryDealGetInteger(ticket, DEAL_TYPE);
         symbol = HistoryDealGetString(ticket, DEAL_SYMBOL);
         volume = HistoryDealGetDouble(ticket, DEAL_VOLUME);
         entry = HistoryDealGetInteger(ticket, DEAL_ENTRY);
         price = HistoryDealGetDouble(ticket, DEAL_PRICE);
         sl = HistoryDealGetDouble(ticket, DEAL_SL);
         tp = HistoryDealGetDouble(ticket, DEAL_TP);
         profit = HistoryDealGetDouble(ticket, DEAL_PROFIT);
         commission = HistoryDealGetDouble(ticket, DEAL_COMMISSION);
         fee = HistoryDealGetDouble(ticket, DEAL_FEE);
         swap = HistoryDealGetDouble(ticket, DEAL_SWAP);
         magic = HistoryDealGetInteger(ticket, DEAL_MAGIC);
         comment = HistoryDealGetString(ticket, DEAL_COMMENT);

         if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL || type == DEAL_TYPE_BALANCE) {
            // Replace the separator characters in the comment with a space
            StringReplace(comment, s_sep, " ");

            // Form an array of values for writing one deal to the file string
            string fields[] = {TimeToString(time, TIME_DATE | TIME_MINUTES | TIME_SECONDS),
                               IntegerToString(ticket), IntegerToString(type), symbol, DoubleToString(volume), IntegerToString(entry),
                               DoubleToString(price, 5), DoubleToString(sl, 5), DoubleToString(tp, 5), DoubleToString(profit),
                               DoubleToString(commission), DoubleToString(fee), DoubleToString(swap), IntegerToString(magic), comment
                              };

            // Set the values of a single deal to the file
            WriteDealsHistoryRow(fields);
         }
      }
   }
}

No método WriteDealsHistoryRow(), simplesmente concatenamos todos os valores do array recebido em uma única linha, separados por um delimitador especificado, e gravamos no arquivo CSV aberto. Para concatenar os valores, utilizamos o novo macro JOIN, que foi adicionado à nossa coleção de macros no arquivo Macros.mqh.

//+------------------------------------------------------------------+
//| Write one row of deal history to the file                        |
//+------------------------------------------------------------------+
void CExpertHistory::WriteDealsHistoryRow(const string &fields[]) {
   // Row to be set
   string row = "";

   // Concatenate all array values into one row using a separator
   JOIN(fields, row, ",");

   // Write a row to the file
   FileWrite(s_file, row);
}

Salvaremos as alterações realizadas no arquivo ExpertHistory.mqh na pasta atual.

Agora resta pouco: conectar esse arquivo ao arquivo do EA e adicionar a chamada do método CExpertHistory::Export() no manipulador de eventos OnTester().

...

#include "ExpertHistory.mqh"

...

//+------------------------------------------------------------------+
//| Test results                                                     |
//+------------------------------------------------------------------+
double OnTester(void) {
   CExpertHistory::Export();
   return expert.Tester();
}

Salvaremos as alterações realizadas no arquivo SimpleVolumesExpert.mq5 na pasta atual.

Executaremos o teste do EA. Após a conclusão, na pasta comum de dados, surgiu um arquivo com o nome:

SimpleVolumesExpert.1.19 [2021.01.01 - 2022.12.30] [10000, 34518, 1294, 3.75].history.csv

Pelo nome, fica claro que o histórico de operações cobre dois anos (2021 e 2022), o saldo inicial da conta era de $10.000, e o saldo final foi de $34.518. Durante o intervalo de teste, a máxima redução relativa nos fundos foi de $1.294, e o índice de Sharpe foi de 3,75. Se abrirmos o arquivo gerado no Excel, veremos o seguinte:

Figura 4. Resultados da exportação do histórico de operações para o arquivo CSV

Os dados parecem estar corretos. Agora, vamos para a segunda etapa: escrever um EA que possa reproduzir operações em outra conta com base nesse arquivo CSV.


Reprodução de operações

Começaremos a implementação do novo EA criando a estratégia de negociação. Afinal, seguir as instruções de outro sobre quando e quais posições abrir também pode ser chamado de estratégia de negociação. Se a fonte dos sinais for confiável, por que não utilizá-los? Assim, criaremos uma nova classe CHistoryStrategy, herdando-a de CVirtualStrategy. Os métodos que precisaremos implementar obrigatoriamente nessa classe incluem o construtor, o método de processamento de tick e o método de conversão para string. Embora o último método não seja útil neste caso, sua presença é obrigatória devido à herança, já que na classe pai ele é abstrato.

Como ficará claro adiante, na nova classe será suficiente adicionar as seguintes propriedades:

  • m_symbols — array com os nomes dos símbolos (instrumentos financeiros);
  • m_history — matriz bidimensional para leitura do histórico de operações do arquivo (N linhas x 15 colunas);
  • m_totalDeals — número total de operações no histórico;
  • m_currentDeal — número atual da operação;
  • m_symbolInfo — objeto para obter informações sobre as propriedades do símbolo.
Os valores iniciais dessas propriedades serão definidos no construtor.
//+------------------------------------------------------------------+
//| Trading strategy for reproducing the history of deals            |
//+------------------------------------------------------------------+
class CHistoryStrategy : public CVirtualStrategy {
protected:
   string            m_symbols[];            // Symbols (trading instruments)
   string            m_history[][15];        // Array of deal history (N rows * 15 columns)
   int               m_totalDeals;           // Number of deals in history
   int               m_currentDeal;          // Current deal index

   CSymbolInfo       m_symbolInfo;           // Object for getting information about the symbol properties

public:
                     CHistoryStrategy(string p_params);        // Constructor
   virtual void      Tick() override;        // OnTick event handler
   virtual string    operator~() override;   // Convert object to string
};

O construtor da estratégia deve aceitar um argumento: uma string de inicialização. Esse requisito também decorre da herança. A string de inicialização deve conter todos os valores necessários, e o construtor será responsável por lê-los e utilizá-los de forma apropriada. Para esta estratégia simples, basta que a string de inicialização contenha apenas um valor: o nome do arquivo com o histórico. Todos os demais dados necessários para o funcionamento da estratégia serão extraídos desse arquivo. O construtor pode ser implementado da seguinte maneira:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CHistoryStrategy::CHistoryStrategy(string p_params) {
   m_params = p_params;

// Read the file name from the parameters
   string fileName = ReadString(p_params);

// If the name is read, then
   if(IsValid()) {
      // Attempting to open a file in the data folder
      int f = FileOpen(fileName, FILE_READ | FILE_CSV | FILE_ANSI | FILE_SHARE_READ, ',');

      // If failed to open a file, then try to open the file from the shared folder
      if(f == INVALID_HANDLE) {
         f = FileOpen(fileName, FILE_COMMON | FILE_READ | FILE_CSV | FILE_ANSI | FILE_SHARE_READ, ',');
      }

      // If this does not work, report an error and exit
      if(f == INVALID_HANDLE) {
         SetInvalid(__FUNCTION__,
                    StringFormat("ERROR: Can't open file %s from common folder %s, error code: %d",
                                 fileName, TerminalInfoString(TERMINAL_COMMONDATA_PATH), GetLastError()));
         return;
      }

      // Read the file up to the header string (usually it comes first)
      while(!FileIsEnding(f)) {
         string s = FileReadString(f);
         // If we find a header string, read the names of all columns without saving them
         if(s == "DATE") {
            FORI(14, FileReadString(f));
            break;
         }
      }

      // Read the remaining rows until the end of the file
      while(!FileIsEnding(f)) {
         // If the array for storing the read history is filled, increase its size
         if(m_totalDeals == ArraySize(m_history)) {

            ArrayResize(m_history, ArraySize(m_history) + 10000, 100000);
         }

         // Read 15 values from the next file string into the array string
         FORI(15, m_history[m_totalDeals][i] = FileReadString(f));

         // If the deal symbol is not empty,
         if(m_history[m_totalDeals][SYMBOL] != "") {
            // Add it to the symbol array if there is no such symbol there yet
            ADD(m_symbols, m_history[m_totalDeals][SYMBOL]);
         }

         // Increase the counter of read deals
         m_totalDeals++;
      }

      // Close the file
      FileClose(f);

      PrintFormat(__FUNCTION__" | OK: Found %d rows in %s", m_totalDeals, fileName);

      // If there are read deals except for the very first one (account top-up), then
      if(m_totalDeals > 1) {
         // Set the exact size for the history array
         ArrayResize(m_history, m_totalDeals);

         // Current time
         datetime ct = TimeCurrent();

         PrintFormat(__FUNCTION__" |\n"
                     "Start time in tester:  %s\n"
                     "Start time in history: %s",
                     TimeToString(ct, TIME_DATE), m_history[0][DATE]);

         // If the test start date is greater than the history start date, then report an error
         if(StringToTime(m_history[0][DATE]) < ct) {
            SetInvalid(__FUNCTION__,
                       StringFormat("ERROR: For this history file [%s] set start date less than %s",
                                    fileName, m_history[0][DATE]));
         }
      }

      // Create virtual positions for each symbol
      CVirtualReceiver::Get(GetPointer(this), m_orders, ArraySize(m_symbols));

      // Register the event handler for a new bar on the minimum timeframe
      FOREACH(m_symbols, IsNewBar(m_symbols[i], PERIOD_M1));
   }
}

Nele, lemos o nome do arquivo a partir da string de inicialização e tentamos abri-lo. Se o arquivo for aberto com sucesso, seja da pasta local ou da pasta comum de dados, lemos seu conteúdo e preenchemos o array m_history. Durante a leitura, também adicionamos os nomes dos símbolos ao array m_symbols sempre que um novo nome é encontrado. Essa tarefa é realizada pelo macro ADD().

Simultaneamente, contamos o número de registros de operações lidos, armazenando-o na propriedade m_totalDeals. Este número é usado como índice da primeira dimensão do array m_history, onde as informações sobre cada operação são registradas. Após ler todo o conteúdo do arquivo, fechamos o arquivo.

Em seguida, verificamos se a data de início do teste é posterior à data de início do histórico. Essa situação não é permitida, pois tornaria impossível modelar parte das operações presentes no início do histórico, o que poderia levar a distorções nos resultados durante o teste. Portanto, o construtor só criará um objeto válido se o histórico de operações começar antes ou ao mesmo tempo que a data de início do teste.

Um aspecto essencial no construtor é a alocação de posições virtuais exclusivamente baseada na quantidade de nomes de símbolos únicos encontrados no histórico. Como a tarefa da estratégia é garantir o volume adequado de posições abertas para cada símbolo, isso pode ser alcançado utilizando apenas uma posição virtual por símbolo.

O método de processamento de ticks operará exclusivamente com o array de operações lidas. Como aberturas ou fechamentos de posições podem ocorrer para vários símbolos ao mesmo tempo, organizaremos um loop que processará todas as linhas do histórico de operações cujo tempo seja menor ou igual ao tempo atual. As operações restantes serão processadas em ticks futuros, à medida que o tempo atual avançar e novas operações se tornarem relevantes.

Se for encontrada pelo menos uma operação a ser processada, identificaremos seu símbolo e o índice correspondente no array m_symbols. Com base nesse índice, determinaremos qual posição virtual no array m_orders é responsável por esse símbolo. Caso o índice não seja encontrado (o que não deveria ocorrer durante uma operação correta), simplesmente ignoraremos essa operação. Também ignoraremos operações que representem transações de balanço na conta.

Agora começa a parte mais interessante. Precisamos processar a operação lida. Aqui, há duas situações possíveis: não existe uma posição virtual aberta para o símbolo em questão ou já há uma posição virtual aberta.

No primeiro caso, é simples: abrimos uma posição na direção da operação, com o volume correspondente. No segundo caso, pode ser necessário aumentar o volume da posição atual para o símbolo ou reduzi-lo. E essa redução pode ser tão grande que a direção da posição aberta se inverta.

Para simplificar os cálculos, procederemos da seguinte forma:

  • Converteremos o volume da nova operação para um formato "com sinal". Ou seja, se a operação for na direção SELL, atribuímos a ela um volume negativo.
  • Obteremos o volume da posição aberta para o mesmo símbolo da nova operação. O método CVirtualOrder::Volume() já retorna o volume nesse formato "com sinal".
  • Somaremos o volume da nova operação ao volume da posição já aberta. O resultado será o novo volume que deve permanecer aberto após considerar a nova operação. Este valor também estará no formato "com sinal".
  • Fecharemos a posição virtual atualmente aberta.
  • Se o novo volume não for zero, abrimos uma nova posição virtual para o símbolo. A direção será determinada pelo sinal do novo volume (positivo para BUY, negativo para SELL), e o módulo do novo volume será passado como argumento ao método de abertura de posição virtual.

Após esse procedimento, incrementamos o contador de operações processadas no histórico e avançamos para a próxima iteração do laço. Se não houver mais operações para processar atualmente ou se o histórico tiver terminado, a execução do tick é encerrada.

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CHistoryStrategy::Tick() override {
//---
   while(m_currentDeal < m_totalDeals && StringToTime(m_history[m_currentDeal][DATE]) <= TimeCurrent()) {
      // Deal symbol
      string symbol = m_history[m_currentDeal][SYMBOL];
      
      // Find the index of the current deal symbol in the array of symbols
      int index;
      FIND(m_symbols, symbol, index);

      // If not found, then skip the current deal
      if(index == -1) {
         m_currentDeal++;
         continue;
      }
      
      // Deal type
      ENUM_DEAL_TYPE type = (ENUM_DEAL_TYPE) StringToInteger(m_history[m_currentDeal][TYPE]);

      // Current deal volume
      double volume = NormalizeDouble(StringToDouble(m_history[m_currentDeal][VOLUME]), 2);

      // If this is a top-up/withdrawal, skip the deal
      if(volume == 0) {
         m_currentDeal++;
         continue;
      }

      // Report information about the read deal
      PrintFormat(__FUNCTION__" | Process deal #%d: %s %.2f %s",
                  m_currentDeal, (type == DEAL_TYPE_BUY ? "BUY" : (type == DEAL_TYPE_SELL ? "SELL" : EnumToString(type))),
                  volume, symbol);

      // If this is a sell deal, then make the volume negative
      if(type == DEAL_TYPE_SELL) {
         volume *= -1;
      }

      // If the virtual position for the current deal symbol is open,
      if(m_orders[index].IsOpen()) {
         // Add its volume to the volume of the current trade
         volume += m_orders[index].Volume();
         
         // Close the virtual position
         m_orders[index].Close();
      }

      // If the volume for the current symbol is not 0,
      if(MathAbs(volume) > 0.00001) {
         // Open a virtual position of the required volume and direction
         m_orders[index].Open(symbol, (volume > 0 ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), MathAbs(volume));
      }

      // Increase the counter of handled deals
      m_currentDeal++;
   }
}

Salvaremos o código obtido no arquivo HistoryStrategy.mqh na pasta atual.

Agora, criaremos um novo arquivo de EA baseado no já existente SimpleVolumesExpert.mq5. Para alcançar o resultado desejado, precisamos adicionar um parâmetro de entrada ao EA, no qual será possível especificar o nome do arquivo com o histórico.

input group "::: Testing the deal history"
input string historyFileName_    = "";    // File with history

A parte do código responsável por carregar as strings de inicialização das estratégias a partir do banco de dados agora é desnecessária, então a removeremos.

Na string de inicialização do EA, incluiremos a criação de uma instância da estratégia da classe CHistoryStrategy, passando o nome do arquivo com o histórico como argumento:

// Prepare the initialization string for an EA with a group of several strategies
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    class CVirtualStrategyGroup(\n"
                            "       [\n"
                            "        class CHistoryStrategy(\"%s\")\n"
                            "       ],%f\n"
                            "    ),\n"
                            "    class CVirtualRiskManager(\n"
                            "       %d,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%.2f,%d,%.2f,%.2f"
                            "    )\n"
                            "    ,%d,%s,%d\n"
                            ")",
                            historyFileName_, scale_,
                            rmIsActive_, rmStartBaseBalance_,
                            rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCloseDailyPart_,
                            rmCalcOverallLossLimit_, rmMaxOverallLossLimit_, rmCloseOverallPart_,
                            rmCalcOverallProfitLimit_, rmMaxOverallProfitLimit_, rmMaxOverallProfitDate_,
                            rmMaxRestoreTime_, rmLastVirtualProfitFactor_,
                            magic_, "HistoryReceiver", useOnlyNewBars_
                         );

Com isso, as alterações no arquivo do EA estão concluídas. Salvaremos o arquivo sob o nome HistoryReceiverExpert.mq5 na pasta atual.

Agora temos um EA funcional, capaz de reproduzir o histórico de operações. Na verdade, suas capacidades vão além. Podemos verificar facilmente como os resultados da negociação se comportam ao aumentarmos o volume das posições abertas proporcionalmente ao crescimento do saldo da conta, mesmo que o histórico tenha sido registrado com base em operações fixas. Podemos aplicar diferentes parâmetros de gerenciamento de risco para avaliar seu impacto nas negociações, mesmo que o histórico tenha sido gravado com outros parâmetros ou até mesmo com o gerenciamento de risco desativado. Após cada execução no testador, o histórico de operações é salvo automaticamente em um novo arquivo.

No entanto, se não precisamos, por enquanto, de todas essas funcionalidades adicionais, se não queremos ativar o gerenciamento de risco e se preferimos evitar uma longa lista de parâmetros de entrada não utilizados relacionados a ele, podemos criar uma nova classe de EA simplificada, sem nada "supérfluo". Nesta classe, podemos também eliminar o salvamento do estado, o desenho das posições virtuais nos gráficos e outras funcionalidades pouco utilizadas.

A implementação dessa classe pode ser algo assim:

//+------------------------------------------------------------------+
//| Trade history replay EA class                                    |
//+------------------------------------------------------------------+
class CVirtualHistoryAdvisor : public CAdvisor {
protected:
   CVirtualReceiver *m_receiver;       // Receiver object that brings positions to the market
   bool              m_useOnlyNewBar;  // Handle only new bar ticks
   datetime          m_fromDate;       // Test start time

public:
   CVirtualHistoryAdvisor(string p_param);   // Constructor
   ~CVirtualHistoryAdvisor();                // Destructor

   virtual void      Tick() override;        // OnTick event handler
   virtual double    Tester() override;      // OnTester event handler

   virtual string    operator~() override;   // Convert object to string
};


//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualHistoryAdvisor::CVirtualHistoryAdvisor(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the file name from the initialization string
   string fileName = ReadString(p_params);

// Read the work flag only at the bar opening
   m_useOnlyNewBar = (bool) ReadLong(p_params);

// If there are no read errors,
   if(IsValid()) {
      if(!MQLInfoInteger(MQL_TESTER)) {
         // Otherwise, set the object state to invalid
         SetInvalid(__FUNCTION__, "ERROR: This expert can run only in tester");
         return;
      }

      if(fileName == "") {
         // Otherwise, set the object state to invalid
         SetInvalid(__FUNCTION__, "ERROR: Set file name with deals history in ");
         return;
      }

      string strategyParams = StringFormat("class CHistoryStrategy(\"%s\")", fileName);

      CREATE(CHistoryStrategy, strategy, strategyParams);

      Add(strategy);

      // Initialize the receiver with the static receiver
      m_receiver = CVirtualReceiver::Instance(65677);

      // Save the work (test) start time
      m_fromDate = TimeCurrent();
   }
}

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualHistoryAdvisor::~CVirtualHistoryAdvisor() {
   if(!!m_receiver)     delete m_receiver;      // Remove the recipient
   DestroyNewBar();           // Remove the new bar tracking objects 
}


//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualHistoryAdvisor::Tick(void) {
// Define a new bar for all required symbols and timeframes
   bool isNewBar = UpdateNewBar();

// If there is no new bar anywhere, and we only work on new bars, then exit
   if(!isNewBar && m_useOnlyNewBar) {
      return;
   }

// Start handling in strategies
   CAdvisor::Tick();

// Receiver handles virtual positions
   m_receiver.Tick();

// Adjusting market volumes
   m_receiver.Correct();
}

//+------------------------------------------------------------------+
//| OnTester event handler                                           |
//+------------------------------------------------------------------+
double CVirtualHistoryAdvisor::Tester() {
// Maximum absolute drawdown
   double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD);

// Profit
   double profit = TesterStatistics(STAT_PROFIT);

// Fixed balance for trading from settings
   double fixedBalance = CMoney::FixedBalance();

// The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_
   double coeff = fixedBalance * 0.1 / MathMax(1, balanceDrawdown);

// Calculate the profit in annual terms
   long totalSeconds = TimeCurrent() - m_fromDate;
   double totalYears = totalSeconds / (365.0 * 24 * 3600);
   double fittedProfit = profit * coeff / totalYears;

// If it is not specified, then take the initial balance (although this will give a distorted result)
   if(fixedBalance < 1) {
      fixedBalance = TesterStatistics(STAT_INITIAL_DEPOSIT);
      balanceDrawdown = TesterStatistics(STAT_EQUITY_DDREL_PERCENT);
      coeff = 0.1 / balanceDrawdown;
      fittedProfit = fixedBalance * MathPow(1 + profit * coeff / fixedBalance, 1 / totalYears);
   }

   return fittedProfit;
}

//+------------------------------------------------------------------+
//| Convert an object to a string                                    |
//+------------------------------------------------------------------+
string CVirtualHistoryAdvisor::operator~() {
   return StringFormat("%s(%s)", typename(this), m_params);
}
//+------------------------------------------------------------------+

O EA desta classe aceitará apenas dois parâmetros na string de inicialização: o nome do arquivo com o histórico e uma flag para operar apenas na abertura de um candle de minuto. Salvaremos este código no arquivo VirtualHistoryAdvisor.mqh na pasta atual.

O arquivo do EA que utiliza essa classe também pode ser simplificado em relação ao exemplo anterior:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "::: Testing the deal history"
input string historyFileName_    = "";    // File with history
input group "::: Money management"
sinput double fixedBalance_      = 10000; // - Used deposit (0 - use all) in the account currency
input  double scale_             = 1.00;  // - Group scaling multiplier

input group "::: Other parameters"
input bool     useOnlyNewBars_   = true;  // - Work only at bar opening

datetime fromDate = TimeCurrent();        // Operation start time

CVirtualHistoryAdvisor     *expert;       // EA object

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Set parameters in the money management class
   CMoney::DepoPart(scale_);
   CMoney::FixedBalance(fixedBalance_);

// Prepare the initialization string for the deal history replay EA
   string expertParams = StringFormat(
                            "class CVirtualHistoryAdvisor(\"%s\",%f,%d)",
                            historyFileName_, useOnlyNewBars_
                         );

// Create an EA handling virtual positions
   expert = NEW(expertParams);

// If the EA is not created, then return an error
   if(!expert) return INIT_FAILED;

// Successful initialization
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   expert.Tick();
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   if(!!expert) delete expert;
}

//+------------------------------------------------------------------+
//| Test results                                                     |
//+------------------------------------------------------------------+
double OnTester(void) {
   return expert.Tester();
}
//+------------------------------------------------------------------+

Salvaremos este código no arquivo SimpleHistoryReceiverExpert.mq5 na pasta atual.


Resultados do Teste

Executaremos um dos EAs criados, especificando o nome correto do arquivo com o histórico de operações salvo. Primeiro, rodaremos no mesmo servidor de cotações utilizado para gerar o histórico (MetaQuotes-Demo). Os resultados obtidos coincidiram completamente com os resultados originais! É preciso admitir que isso foi um resultado surpreendentemente bom, demonstrando a correção da implementação planejada. 

Agora, vejamos o que acontece ao executar o EA em outro servidor:


Figura 5. Resultados da reprodução do histórico de operações em cotações de um servidor real de outra corretora.

O gráfico da curva de saldo é, visualmente, quase indistinguível do gráfico dos resultados originais no MetaQuotes-Demo. No entanto, os valores numéricos apresentam pequenas diferenças. Vamos revisar os valores originais para comparação:


Figura 6. Resultados do teste original nas cotações do servidor MetaQuotes-Demo.

Notamos uma leve redução no lucro médio anual geral e normalizado, no índice de Sharpe e um pequeno aumento no rebaixamento. Contudo, esses resultados não se comparam à perda total do depósito que obtivemos inicialmente ao rodar o EA com cotações reais de outro servidor. Isso é bastante encorajador e abre novas possibilidades para questões que talvez precisemos resolver ao preparar o EA para uso em negociações reais.


Considerações finais

É hora de resumir os resultados parciais. Demonstramos que a estratégia de negociação utilizada pode ter consequências muito negativas ao trocar o servidor de cotações. No entanto, ao entender as causas desse comportamento, mostramos que os resultados de negociação voltam a ser comparáveis ao manter a lógica dos sinais de abertura de posições no servidor com as cotações originais e transferir apenas as operações de abertura e fechamento para um novo servidor.

Para isso, desenvolvemos duas ferramentas funcionais: uma para salvar o histórico de operações após a execução do testador e outra para reproduzir as operações com base nesse histórico. No entanto, essas ferramentas são úteis apenas no ambiente de teste, não se aplicando a negociações reais. Na negociação real, elas são irrelevantes. Agora, com os testes confirmando a viabilidade dessa abordagem, podemos começar a implementá-la em operações reais com maior confiança.

Precisaremos dividir o EA em dois EAs distintos. O primeiro será responsável por tomar decisões sobre a abertura de posições e executá-las, operando no servidor de cotações mais conveniente. Simultaneamente, enquanto realiza negociações, ele deverá transmitir a lista de posições abertas em um formato que o segundo EA consiga interpretar. O segundo EA, por sua vez, funcionará em outro terminal, conectado, se necessário, a um servidor de cotações diferente. Sua tarefa será manter o volume de posições abertas constantemente conforme os valores transmitidos pelo primeiro EA. Esse esquema de funcionamento permitirá superar a limitação identificada no início deste artigo.

É possível ir ainda mais além. O esquema mencionado pressupõe, implicitamente, que os dois terminais estejam operando no mesmo computador. No entanto, isso não é obrigatório. Os terminais podem funcionar em computadores diferentes, desde que o primeiro EA consiga transmitir as informações sobre posições ao segundo EA por algum meio de comunicação. É claro que, dessa forma, não será possível implementar com sucesso estratégias de negociação que dependam rigorosamente do tempo e do preço exatos para a abertura de posições. No entanto, nosso trabalho é direcionado para o uso de outras estratégias, para as quais uma precisão extrema nas entradas não é necessária. Assim, eventuais atrasos no canal de comunicação não deverão representar um obstáculo significativo para a implementação dessa abordagem.

Por ora, não nos adiantaremos demais. Continuaremos avançando de forma metódica na direção escolhida nas próximas partes deste artigo.

Obrigado pela atenção e até breve! 


Conteúdo do arquivo

#
 Nome
Versão  Descrição   Últimas alterações
 MQL5/Experts/Article.15330
1 Advisor.mqh 1.04 Classe base do EA Parte 10
2 Database.mqh 1.03 Classe para trabalho com banco de dados Parte 13
3 ExpertHistory.mqh 1.00 Classe para exportar o histórico de operações para um arquivo Parte 16
4 Factorable.mqh 1.01 Classe base para objetos criados a partir de uma string Parte 10
5 HistoryReceiverExpert.mq5 1.00 EA para reprodução de histórico de operações com gerenciamento de risco Parte 16  
6 HistoryStrategy.mqh  1.00 Classe de estratégia de negociação para reprodução de histórico de operações  Parte 16
7 Interface.mqh 1.00 Classe base para visualização de diversos objetos Parte 4
8 Macros.mqh 1.02 Macros úteis para operações com arrays Parte 16  
9 Money.mqh 1.01  Classe base para gerenciamento de capital Parte 12
10 NewBarEvent.mqh 1.00  Classe para identificar novos candles para um símbolo específico  Parte 8
11 Receiver.mqh 1.04  Classe base para conversão de volumes abertos em posições de mercado  Parte 12
12 SimpleHistoryReceiverExpert.mq5 1.00 EA simplificado para reprodução de histórico de operações   Parte 16
13 SimpleVolumesExpert.mq5 1.19 EA para operação paralela de vários grupos de estratégias modelo. Os parâmetros devem ser carregados de um banco de dados de otimização. Parte 16
14 SimpleVolumesStrategy.mqh 1.09  Classe de estratégia de negociação usando volumes tick Parte 15
15 Strategy.mqh 1.04  Classe base para estratégia de negociação Parte 10
16 TesterHandler.mqh  1.02 Classe para processamento de eventos de otimização  Parte 13 
17 VirtualAdvisor.mqh  1.06  Classe de EA que trabalha com posições (ordens) virtuais Parte 15
18 VirtualChartOrder.mqh  1.00  Classe de posição virtual gráfica Parte 4  
19 VirtualFactory.mqh 1.04  Classe de fábrica de objetos  Parte 16
20 VirtualHistoryAdvisor.mqh 1.00  Classe de EA para reprodução de histórico de operações  Parte 16
21 VirtualInterface.mqh  1.00  Classe de interface gráfica do EA  Parte 4  
22 VirtualOrder.mqh 1.04  Classe de ordens e posições virtuais  Parte 8
23 VirtualReceiver.mqh 1.03  Classe para conversão de volumes abertos em posições de mercado (receptor)  Parte 12
24 VirtualRiskManager.mqh  1.02  Classe de gerenciamento de risco (gerenciador de risco)  Parte 15
25 VirtualStrategy.mqh 1.05  Classe de estratégia de negociação com posições virtuais  Parte 15
26 VirtualStrategyGroup.mqh  1.00  Classe de grupo de estratégias de negociação ou grupos de estratégias de negociação Parte 11 
27 VirtualSymbolReceiver.mqh  1.00 Classe de receptor simbólico  Parte 3
MQL5/Files 
1 SimpleVolumesExpert.1.19 [2021.01.01 - 2022.12.30] [10000, 34518, 1294, 3.75].history.csv    Histórico de operações do EA SimpleVolumesExpert.mq5, gerado após exportação. Pode ser utilizado para reprodução no testador de operações com os EAs SimpleHistoryReceiverExpert.mq5 ou HistoryReceiverExpert.mq5.  

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

Arquivos anexados |
MQL5-2.zip (163.78 KB)
Últimos Comentários | Ir para discussão (4)
fxsaber
fxsaber | 31 jul. 2024 em 13:24
É exatamente isso que o Signal Service faz.
Amir Jafary
Amir Jafary | 30 jan. 2025 em 14:00
Fiz o download dos últimos arquivos e como posso executar o advisor em meu meta! Não consigo compilar o arquivo do advisor e tenho um erro
Cristian-bogdan Buzatu
Cristian-bogdan Buzatu | 3 fev. 2025 em 23:13

Recebi este erro quando tentei executar um backtest no EA:


2025.02.04 01:11:13.690 Core 01 2021.01.01 00:00:00 erro de banco de dados, não existe tal tabela: passes

2025.02.04 01:11:13.690 Core 01 tester stopped because OnInit returns non-zero code 1


Alguma ajuda, por favor?

Yuriy Bykov
Yuriy Bykov | 4 fev. 2025 em 11:52
Cristian-bogdan Buzatu #:

Recebi este erro quando tentei executar um backtest no EA

Provavelmente, o motivo é que você não criou um banco de dados e não executou as duas primeiras etapas de otimização, que preencherão o banco de dados com informações sobre as passagens executadas(parte 9, parte 11, parte 13). Infelizmente, no momento em que este artigo foi escrito, ainda não havia uma ferramenta simples para criar um banco de dados, criar um projeto de otimização e exportar seus resultados para o EA final. Revisitamos essa questão na parte 21, mas não terminamos de abordá-la. Ela continuará nas partes 22 e 23 (ainda não prontas para publicação).

Criando uma Interface Gráfica de Usuário Interativa no MQL5 (Parte 1): Criando o Painel Criando uma Interface Gráfica de Usuário Interativa no MQL5 (Parte 1): Criando o Painel
Este artigo explora os passos fundamentais para criar e implementar um painel de Interface Gráfica de Usuário (GUI) utilizando a Linguagem MetaQuotes 5 (MQL5). Painéis utilitários personalizados melhoram a interação do usuário no trading, simplificando tarefas comuns e visualizando informações essenciais de trading. Ao criar painéis personalizados, os traders podem otimizar seu fluxo de trabalho e economizar tempo durante as operações de trading.
Desenvolvendo um sistema de Replay (Parte 76): Um novo Chart Trade (III) Desenvolvendo um sistema de Replay (Parte 76): Um novo Chart Trade (III)
Neste artigo vamos compreender como o código faltante no artigo anterior, DispatchMessage, funciona. Aqui será feita a introdução do que será visto no próximo artigo. Sendo assim é importante compreender o funcionamento deste procedimento antes de ver o próximo artigo. O conteúdo exposto aqui, visa e tem como objetivo, pura e simplesmente a didática. De modo algum deve ser encarado como sendo, uma aplicação cuja finalidade não venha a ser o aprendizado e estudo dos conceitos mostrados.
Técnicas do MQL5 Wizard que você deve conhecer (Parte 25): Testes e Operações em Múltiplos Timeframes Técnicas do MQL5 Wizard que você deve conhecer (Parte 25): Testes e Operações em Múltiplos Timeframes
Por padrão, estratégias baseadas em múltiplos timeframes não podem ser testadas em Expert Advisors montados pelo assistente devido à arquitetura de código MQL5 utilizada nas classes de montagem. Exploramos uma possível solução para essa limitação em estratégias que utilizam múltiplos timeframes em um estudo de caso com a média móvel quadrática.
Do básico ao intermediário: Definições (I) Do básico ao intermediário: Definições (I)
Neste artigo, faremos diversas coisas, que para muitos irão parecer estranho e totalmente fora de contexto. Mas que ser for bem empregado tornará seu aprendo muito mais divertido e empolgante. Já que podemos construir coisas bem interessantes, com base no que será visto aqui. A ponto de permitir uma melhor assimilação da sintaxe da linguagem MQL5. O conteúdo exposto aqui, visa e tem como objetivo, pura e simplesmente a didática. De modo algum deve ser encarado como sendo, uma aplicação cuja finalidade não venha a ser o aprendizado e estudo dos conceitos mostrados.