English Русский Deutsch 日本語
preview
Desenvolvendo um EA multimoeda (Parte 8): Realizando testes de carga e processando um novo candle

Desenvolvendo um EA multimoeda (Parte 8): Realizando testes de carga e processando um novo candle

MetaTrader 5Testador | 11 setembro 2024, 08:55
36 0
Yuriy Bykov
Yuriy Bykov

Introdução

No primeiro artigo, desenvolvemos um EA com duas instâncias de estratégias de negociação. No segundo artigo, utilizamos nove instâncias, e no último, 32 instâncias. Até agora, não enfrentamos problemas com o tempo excessivo de testes. É claro que quanto menor for o tempo de uma única execução no testador, melhor. No entanto, se o processo de otimização levar algumas horas, isso é aceitável, em comparação a um processo que poderia durar dias ou semanas. Da mesma forma, ao combinar várias instâncias de estratégias em um único EA e visualizar seus resultados, a execução deve ser concluída em segundos ou minutos, não em horas ou dias.

Quando realizamos uma otimização para ajustar grupos de instâncias de estratégias, várias instâncias já participam de cada execução de otimização. Isso aumenta tanto o tempo de execução individual quanto o tempo total de otimização. Por isso, limitamos a otimização a grupos de no máximo oito instâncias.

Agora, tentaremos descobrir como o tempo de execução no testador varia conforme o número de instâncias de estratégias de negociação para diferentes períodos de teste. Também analisaremos o uso de memória. E, claro, veremos como os EAs com diferentes números de instâncias se comportam ao serem executados no gráfico do terminal.


Vários números de instâncias no testador

Para realizar esse experimento, precisaremos criar um novo EA com base em um já existente. Usaremos como base o EA OptGroupExpert.mq5 e realizaremos as seguintes alterações:

  • Removeremos os parâmetros de entrada que definem os índices de oito conjuntos de parâmetros, que eram retirados de uma matriz completa carregada a partir de um arquivo. Manteremos o parâmetro count_, que agora determinará quantos conjuntos serão carregados da matriz completa de parâmetros.
  • Removeremos a verificação de unicidade dos índices, já que eles não existirão mais. Adicionaremos novas estratégias à matriz de estratégias, com conjuntos de parâmetros retirados dos primeiros count_ elementos da matriz de parâmetros params. Se faltar alguma instância nessa matriz, reiniciaremos a contagem do início.
  • Removeremos as funções OnTesterInit() e OntesterDeinit(), pois este EA não será utilizado para otimização por enquanto.

Obtivemos o seguinte código:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "::: Money management"
sinput double expectedDrawdown_ = 10;  // - Maximum risk (%)
sinput double fixedBalance_ = 10000;   // - Used deposit (0 - use all) in the account currency
sinput double scale_ = 1.00;           // - Group scaling multiplier

input group "::: Selection for the group"
sinput string fileName_ = "Params_SV_EURGBP_H1.csv";  // - File with strategy parameters (*.csv)
input int     count_ = 8;              // - Number of strategies in the group (1 .. 8)

input group "::: Other parameters"
sinput ulong  magic_        = 27183;   // - Magic

...

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Load strategy parameter sets
   int totalParams = LoadParams(fileName_, params);

// If nothing is loaded, report an error
   if(totalParams == 0) {
      PrintFormat(__FUNCTION__" | ERROR: Can't load data from file %s.\n"
                  "Check that it exists in data folder or in common data folder.",
                  fileName_);
      return(INIT_PARAMETERS_INCORRECT);
   }

// Report an error if
   if(count_ < 1) { // number of instances is less than 1
      return INIT_PARAMETERS_INCORRECT;
   }
   
   ArrayResize(params, count_);

// Set parameters in the money management class
   CMoney::DepoPart(expectedDrawdown_ / 10.0);
   CMoney::FixedBalance(fixedBalance_);

// Create an EA handling virtual positions
   expert = new CVirtualAdvisor(magic_, "SimpleVolumes_BenchmarkInstances");

// Create and fill the array of all strategy instances
   CVirtualStrategy *strategies[];

   FOREACH(params, APPEND(strategies, new CSimpleVolumesStrategy(params[i % totalParams])));

// Form and add a group of strategies to the EA
   expert.Add(CVirtualStrategyGroup(strategies, scale_));

   return(INIT_SUCCEEDED);
}

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

Agora, faremos vários testes deste EA no testador, com diferentes números de instâncias de estratégias de negociação e diferentes modos de modelagem de ticks.


Resultados do teste para diferentes modos

Vamos começar com o familiar modo de modelagem de ticks "OHLC em M1", que utilizamos em todos os artigos anteriores. O número de instâncias será duplicado a cada nova execução. Começaremos com 8 instâncias. Se o tempo de teste se tornar excessivamente longo, reduziremos o período de teste. 


Fig. 1. Resultados das execuções únicas no modo "OHLC em M1"


Como pode ser observado, ao testar com até 512 instâncias, usamos um período de teste de 6 anos, depois mudamos para 1 ano, e nas duas últimas execuções utilizamos apenas 3 meses. 

Para comparar o tempo gasto em diferentes períodos de teste, calcularemos uma métrica específica: o tempo de modelagem de uma instância de uma estratégia por dia. Para isso, dividimos o tempo total pelo número de instâncias e pela duração do período de teste em dias. Para facilitar, converteremos esse tempo para nanossegundos, multiplicando por 10^9.

Nos logs, o testador fornece informações sobre o uso de memória durante a execução, indicando o volume total e os volumes usados para dados históricos e de ticks. Subtraindo esses valores do volume total de memória, obtemos a quantidade de memória usada pelo próprio EA (provavelmente é isso).

Com base nos resultados, podemos dizer que mesmo o número máximo de instâncias (16384) não exige um tempo extremamente longo no testador. Na prática, essa quantidade de instâncias é suficiente para organizar a operação conjunta, por exemplo, de quinze símbolos com cem instâncias cada. Portanto, isso já é bastante significativo. Além disso, o consumo de memória não aumenta drasticamente com o crescimento do número de instâncias. Há, por algum motivo, um pico no consumo de memória para o próprio EA com 8192 instâncias, mas depois a memória necessária voltou a diminuir.

Para obter resultados mais precisos, seria possível realizar várias execuções para cada quantidade de instâncias e calcular os tempos médios e os volumes médios de memória, já que em diferentes execuções com o mesmo número de instâncias os resultados variaram um pouco. No entanto, essas variações não foram significativas, então não há muita necessidade de testes mais extensos. Nosso objetivo era apenas verificar se encontraríamos limitações com números relativamente pequenos de instâncias.

Agora, proponho analisarmos os resultados das execuções do EA no testador no modo de modelagem "Todos os ticks".


Fig. 2. Resultados das execuções únicas no modo "Todos os ticks"


O tempo de uma única execução aumentou cerca de 10 vezes, então reduzimos o período de teste para as mesmas quantidades de instâncias em comparação com o modo anterior. O tamanho da memória para os ticks aumentou conforme esperado, o que levou a um aumento no volume total de memória alocada. No entanto, a quantidade de memória destinada ao EA permaneceu quase constante para todas as quantidades de instâncias. Houve um pequeno aumento, mas ele foi suficientemente lento.

Foi observado um tempo de execução anormalmente baixo para 512 e 1024 instâncias – quase duas vezes mais rápido do que para outras quantidades de instâncias. A possível causa está provavelmente relacionada à ordem dos conjuntos de parâmetros das instâncias de estratégias de negociação no arquivo CSV com os dados.

O último modo de modelagem investigado foi "Todos os ticks com base em ticks reais". Para esse modo, realizamos um pouco mais de execuções do que para o modo "Todos os ticks".


Fig. 3. Resultados dos testes individuais no modo "Todos os ticks com base em ticks reais"


Comparado ao modo anterior, o tempo aumentou em cerca de 30%, e o uso de memória cresceu aproximadamente 20%.

Vale destacar que, ao mesmo tempo em que o teste era realizado no terminal, havia uma instância desse EA em execução, anexada a um gráfico. Nessa instância, eram utilizadas 8192 instâncias. Com consumo de memória do terminal em torno de 200 MB e uso de CPU variando entre 0% e 4%.

Em geral, o experimento mostrou que temos uma margem bastante ampla quanto ao número possível de instâncias de estratégias de negociação que podem trabalhar juntas em um único EA. Claro, esse número dependerá muito da complexidade interna das estratégias de negociação. Quanto mais cálculos uma única instância precisar realizar, menor será a quantidade de instâncias que poderemos integrar.

Agora, refletimos sobre quais passos simples podemos tomar para acelerar o processo de teste.


Desativação de saída de logs

Na implementação atual, estamos gerando uma quantidade considerável de informações no log durante a execução do EA. Quando se trata de otimização de instâncias únicas, isso não causa problemas, pois as funções de log simplesmente não são executadas. No entanto, ao rodar um único teste do EA, todas as mensagens são registradas no log. Por exemplo, na biblioteca VirtualOrder.mqh, é gerada uma mensagem para cada evento processado por um pedido virtual. Quando há poucos pedidos virtuais, isso tem pouco impacto no tempo de teste, mas, quando o número começa a atingir dezenas de milhares, o efeito pode ser mais perceptível.

Tentemos medir isso. Para desativar todas as nossas mensagens de log, basta adicionar a seguinte linha no início do arquivo do EA:

#define PrintFormat StringFormat

Devido à relação dessas funções, é possível substituir todas as chamadas de PrintFormat() por chamadas de StringFormat(), que formam a string, mas não a exibem no log.

Após realizar alguns testes, notamos uma redução no tempo de execução de 5 a 10% em alguns casos, enquanto em outros o tempo até aumentou ligeiramente. De qualquer forma, foi uma tentativa válida. Aliás, essa técnica de substituir PrintFormat() pode nos ser útil no futuro.

 

Migração para OHLC em M1

Outro método para acelerar tanto os testes únicos quanto a otimização é abandonar o uso dos modos de modelagem "Todos os ticks" e "Todos os ticks com base em ticks reais".

É claro que nem todas as estratégias de negociação podem permitir isso. Se a estratégia envolve a abertura/fechamento de posições com muita frequência (mais de uma vez por minuto), não é possível evitar o teste com todos os ticks. Mesmo o trading de alta frequência não ocorre o tempo todo, apenas em momentos específicos. Mas se a estratégia não exige abertura/fechamento frequente e não é tão sensível à perda de alguns pontos devido à imprecisão na execução de Stop Loss e Take Profit, por que não aproveitar essa oportunidade?

A estratégia de negociação considerada aqui é uma daquelas que permitem evitar o uso do modo "todos os ticks". No entanto, isso traz outro problema. Se otimizarmos os parâmetros das instâncias no modo "OHLC em M1" e, em seguida, colocarmos o EA para operar no terminal,ele terá que trabalhar no modo "todos os ticks". Nesse caso, o EA receberá muito mais do que os 4 ticks fixos por minuto, e a função OnTick() será chamada com mais frequência, processando uma variedade maior de preços.

Essa diferença pode alterar os resultados apresentados pelo EA. Para verificar quão realista é esse cenário, comparamos os resultados de negociação obtidos ao testar o EA, com os mesmos parâmetros de entrada nos modos "OHLC em M1" e "Todos os ticks com base em ticks reais".

Fig. 4. Comparação dos resultados dos testes nos modos
"Todos os ticks com base em ticks reais" (à esquerda) e "OHLC em M1" (à direita)


Pode-se notar que os horários de abertura, fechamento e os preços são ligeiramente diferentes entre os modos. Inicialmente, a diferença está apenas nisso, mas, em um certo ponto, observamos que no modo "OHLC em M1" há uma abertura de negociação, enquanto no modo "Todos os ticks com base em ticks reais" não: veja a linha referente à negociação #25. Assim, os resultados no modo "OHLC em M1" contêm menos negociações em comparação ao modo "Todos os ticks com base em ticks reais". 

No modo "todos os ticks", o lucro foi um pouco maior. Se olharmos para o crescimento da curva de saldo, não há diferenças significativas entre os dois modos:


Fig. 5. Resultados dos testes no modo "OHLC em M1" (acima) e "Todos os ticks com base em ticks reais" (abaixo)


Portanto, podemos concluir que, ao executar esse EA no terminal, obteremos provavelmente resultados que não serão piores do que aqueles obtidos no modo de teste "OHLC em M1". Isso significa que podemos usar o modo de modelagem de ticks mais rápido para otimização. Se a estratégia exigir cálculos apenas no início de um novo candle, podemos acelerar ainda mais o EA eliminando cálculos em cada tick. Para isso, precisaremos de uma forma de detectar um novo candle no EA.

Se os resultados no modo "todos os ticks" forem piores do que no modo "OHLC em M1", podemos tentar proibir o EA de realizar operações fora do início de um novo candle. Nesse caso, os resultados devem ser o mais semelhantes possível em todos os modos de modelagem de ticks. Mais uma vez, precisaremos de um método para detectar um novo candle no EA. 


Detecção de um novo candle

Proponho formularmos primeiro nossos requisitos. Gostaríamos de ter uma função que retornasse verdadeiro se um novo candle tivesse sido formado para o símbolo e o time frame em questão. Quando se desenvolve um EA que implementa uma única instância de uma estratégia de negociação, geralmente essa função é escrita para um único símbolo e time frame, usando variáveis para armazenar o tempo de formação do último candle. Ou, às vezes, para um único símbolo e vários timeframes. Muitas vezes, o código que implementa essa funcionalidade não é separado em uma função específica, sendo realizado apenas no ponto onde é necessário.

Essa abordagem se torna bastante inconveniente quando é necessário realizar múltiplas verificações de novos candles para diferentes instâncias de estratégias de negociação. Claro, podemos incorporar esse código diretamente na implementação da instância da estratégia, mas faremos de outra forma.

Criaremos uma função pública IsNewBar(symbol, timeframe), que deve poder informar sobre a formação de um novo candle no tick atual para o símbolo symbol e o timeframe em questão. De preferência, além da chamada a essa função, não serão necessárias variáveis ou ações adicionais no código lógico das estratégias. Além disso, se no tick atual houver a formação de um novo candle e a função for chamada várias vezes (por exemplo, a partir de diferentes instâncias de estratégias de negociação), ela deverá retornar verdadeiro em cada chamada, e não apenas na primeira.

Para isso, precisaremos armazenar informações sobre os horários de formação do último candle para cada símbolo e timeframe. Por "cada", entendemos apenas os necessários para as instâncias das estratégias de negociação em execução, e não todos os disponíveis no terminal. ara definir esse conjunto de símbolos e timeframes necessários, expandiremos a lista de ações realizadas pela função IsNewBar(symbol, timeframe). Ela verificará primeiro se existe algum registro do horário do último candle para o símbolo e timeframe atuais. Se não houver, ela criará esse registro. Se houver, retornará o resultado da verificação de um novo candle.

Para permitir que a função IsNewBar() seja chamada várias vezes no mesmo tick, precisaremos dividi-la em duas funções separadas. Uma verificará a formação de novos candles no início de cada tick para todos os símbolos e timeframes de interesse e armazenará essa informação. A outra apenas retornará o resultado da verificação de um novo candle. Chamaremos a primeira função de UpdateNewBar(), e ela também retornará um valor lógico indicando se houve a formação de um novo candle para pelo menos um símbolo e timeframe. 

A função UpdateNewBar() deverá ser chamada uma vez no início do processamento de um novo tick. Por exemplo, sua chamada pode ser colocada no início do método CVirtualAdvisor::Tick():

void CVirtualAdvisor::Tick(void) {
// Define a new bar for all required symbols and timeframes
   UpdateNewBar();

   ...
// Start handling in strategies where IsNewBar(...) can already be used
   CAdvisor::Tick();

   ...
}

Para organizar o armazenamento dos horários dos últimos candles, primeiro criaremos uma classe estática CNewBarEvent. Isso significa que não criaremos objetos dessa classe, mas usaremos apenas suas propriedades e métodos estáticos. Basicamente, isso é equivalente a criar as variáveis e funções globais necessárias em um espaço de nomes dedicado.

Essa classe conterá dois arrays: um array de nomes de símbolos (m_symbols) e um array de ponteiros para objetos da nova classe (m_symbolNewBarEvent). O primeiro armazenará os símbolos para os quais monitoraremos os eventos de novos candles. O segundo armazenará ponteiros para objetos da nova classe CSymbolNewBarEvent, que manterá os horários dos candles para um símbolo, mas para diferentes timeframes.

Nestas duas classes, haverá três métodos:

  • O método para registrar um novo símbolo ou timeframe a ser monitorado: Register(...)
  • O método para atualizar os sinais de um novo candle: Update()
  • O método para obter o sinal de um novo candle: IsNewBar(...)

Quando for necessário registrar o monitoramento de um evento de novo candle para um novo símbolo, será criado um novo objeto da classe CSymbolNewBarEvent. Portanto, é necessário cuidar da liberação da memória ocupada por esses objetos ao encerrar o EA. Para isso, foi adicionado o método estático CNewBarEvent::Destroy() e a função global DestroyNewBar(). A chamada a essa função será adicionada no destrutor do EA:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   delete m_receiver;         // Remove the recipient
   delete m_interface;        // Remove the interface
   DestroyNewBar();           // Remove the new bar tracking objects 
}

A implementação completa dessas classes pode ser algo assim:

//+------------------------------------------------------------------+
//| Class for defining a new bar for a specific symbol               |
//+------------------------------------------------------------------+
class CSymbolNewBarEvent {
private:
   string            m_symbol;         // Tracked symbol
   long              m_timeFrames[];   // Array of tracked symbol timeframes
   long              m_timeLast[];     // Array of times of the last bars for timeframes
   bool              m_res[];          // Array of flags of a new bar occurrence for timeframes

   // The method for registering a new tracked timeframe for a symbol
   int               Register(ENUM_TIMEFRAMES p_timeframe) {
      APPEND(m_timeFrames, p_timeframe);  // Add it to the timeframe array
      APPEND(m_timeLast, 0);              // The last time bar for it is still unknown
      APPEND(m_res, false);               // No new bar for it yet
      Update();                           // Update new bar flags
      return ArraySize(m_timeFrames) - 1;
   }

public:
   // Constructor
                     CSymbolNewBarEvent(string p_symbol) :
                     m_symbol(p_symbol) // Set a symbol
   {}

   // Method for updating new bar flags
   bool              Update() {
      bool res = (ArraySize(m_res) == 0);
      FOREACH(m_timeFrames, {
         // Get the current bar time
         long time = iTime(m_symbol, (ENUM_TIMEFRAMES) m_timeFrames[i], 0);
         // If it does not match the saved one, it is a new bar 
         m_res[i] = (time != m_timeLast[i]);
         res |= m_res[i];
         // Save the new time
         m_timeLast[i] = time;
      });
      return res;
   }

   // Method for getting the new bar flag
   bool              IsNewBar(ENUM_TIMEFRAMES p_timeframe) {
      int index;
      // Search for the required timeframe index
      FIND(m_timeFrames, p_timeframe, index);

      // If not found, then register a new timeframe
      if(index == -1) {
         PrintFormat(__FUNCTION__" | Register new event handler for %s %s", m_symbol, EnumToString(p_timeframe));
         index = Register(p_timeframe);
      }

      // Return the new bar flag for the necessary timeframe
      return m_res[index];
   }
};


//+------------------------------------------------------------------+
//| Static class for defining a new bar for all                      |
//| symbols and timeframes                                           |
//+------------------------------------------------------------------+
class CNewBarEvent {
private:
   // Array of objects to define a new bar for one symbol
   static   CSymbolNewBarEvent     *m_symbolNewBarEvent[];

   // Array of required symbols
   static   string                  m_symbols[];

   // Method to register new symbol and timeframe to track a new bar
   static   int                     Register(string p_symbol)  {
      APPEND(m_symbols, p_symbol);
      APPEND(m_symbolNewBarEvent, new CSymbolNewBarEvent(p_symbol));
      return ArraySize(m_symbols) - 1;
   }

public:
   // There is no need to create objects of this class - delete the constructor
                            CNewBarEvent() = delete; 

   // Method for updating new bar flags
   static bool              Update() {
      bool res = (ArraySize(m_symbolNewBarEvent) == 0);
      FOREACH(m_symbols, res |= m_symbolNewBarEvent[i].Update());
      return res;
   }

   // Method to free memory for automatically created objects
   static void              Destroy() {
      FOREACH(m_symbols, delete m_symbolNewBarEvent[i]);
      ArrayResize(m_symbols, 0);
      ArrayResize(m_symbolNewBarEvent, 0);
   }

   // Method for getting the new bar flag
   static bool              IsNewBar(string p_symbol, ENUM_TIMEFRAMES p_timeframe) {
      int index;
      // Search for the required symbol index
      FIND(m_symbols, p_symbol, index);
      
      // If not found, then register a new symbol
      if(index == -1) index = Register(p_symbol);
      
      // Return the new bar flag for the necessary symbol and timeframe
      return m_symbolNewBarEvent[index].IsNewBar(p_timeframe);
   }
};

// Initialize static members of the CSymbolNewBarEvent class members;
CSymbolNewBarEvent* CNewBarEvent::m_symbolNewBarEvent[];
string CNewBarEvent::m_symbols[];


//+------------------------------------------------------------------+
//| Function for checking a new bar occurrence                       |
//+------------------------------------------------------------------+
bool IsNewBar(string p_symbol, ENUM_TIMEFRAMES p_timeframe) {
   return CNewBarEvent::IsNewBar(p_symbol, p_timeframe);
}

//+------------------------------------------------------------------+
//| Function for updating information about new bars                 |
//+------------------------------------------------------------------+
bool UpdateNewBar() {
   return CNewBarEvent::Update();
}

//+------------------------------------------------------------------+
//| Function for removing new bar tracking objects                   |
//+------------------------------------------------------------------+
void DestroyNewBar() {
   CNewBarEvent::Destroy();
}
//+------------------------------------------------------------------+

Salvaremos este código no arquivo NewBarEvent.mqh na pasta atual.

Agora vejamos como aplicar essa biblioteca na estratégia de negociação e no EA. Mas primeiro, faremos pequenas correções na estratégia de negociação, que não estão relacionadas ao processamento de um novo candle.


Correções na estratégia de negociação

Infelizmente, durante a escrita deste artigo, foram encontradas duas falhas na estratégia utilizada. Elas não tiveram um impacto significativo nos resultados anteriores, mas, já que as encontramos, vamos corrigi-las.

A primeira falha fazia com que, quando um valor negativo era definido no parâmetro openDistance_, ele fosse redefinido para um pequeno valor positivo, igual ao spread do símbolo atual. Isso fazia com que, em vez de abrir ordens pendentes BUY STOP e SELL STOP, fossem abertas posições de mercado. Com isso, durante a otimização, não víamos os resultados que poderiam ser obtidos ao negociar com essas ordens pendentes, perdendo assim conjuntos de parâmetros potencialmente lucrativos.

O erro ocorria nesta linha de código no arquivo SimpleVolumesStrategy.mqh, nas funções de abertura de ordens pendentes:

// Let's make sure that the opening distance is not less than the spread
   int distance = MathMax(m_openDistance, spread);

Se o valor de m_openDistance fosse negativo, o valor de desvio da abertura em relação ao preço atual, distance, era garantido como positivo. Para manter o mesmo sinal em distance que em m_openDistance, basta multiplicar a expressão pelo próprio sinal:

// Let's make sure that the opening distance is not less than the spread
   int distance = MathMax(MathAbs(m_openDistance), spread) * (m_openDistance < 0 ? -1 : 1);

A segunda falha era que, ao calcular o volume médio dos últimos candles, o volume do candle atual também era incluído no cálculo, embora, consoante a descrição da estratégia, ele não devesse ser considerado. No entanto, o impacto dessa falha provavelmente é pequeno. Quanto maior o período de média dos volumes, menor a influência do último candle na média. 

Para corrigir isso, basta modificar levemente a função de cálculo da média, excluindo o primeiro elemento do array passado:

//+------------------------------------------------------------------+
//| Average value of the array of numbers from the second element    |
//+------------------------------------------------------------------+
double CSimpleVolumesStrategy::ArrayAverage(const double &array[]) {
   double s = 0;
   int total = ArraySize(array) - 1;
   for(int i = 1; i <= total; i++) {
      s += array[i];
   }

   return s / MathMax(1, total);
}

Salvaremos essas alterações no arquivo SimpleVolumesStrategy.mqh na pasta atual.


Consideração de um novo candle na estratégia

Para algumas ações serem executadas na estratégia de negociação apenas ao surgir um novo candle, basta colocar este bloco de código dentro de uma estrutura condicional:

// If a new bar arrived on H1 for the current strategy symbol, then
if(IsNewBar(m_symbol, PERIOD_H1)) {

       // perform the necessary actions
   ...
}

A presença desse código na estratégia registrará automaticamente o monitoramento do evento de novo candle no timeframe H1 e no símbolo da estratégia m_symbol.

Podemos adicionar tranquilamente a verificação da formação de novos candles em outros timeframes adicionais. Por exemplo, se a estratégia utilizar valores de algum intervalo médio de preços (ATR ou ADR), o recálculo pode ser facilmente organizado para ocorrer apenas uma vez por dia da seguinte forma:

// If a new bar arrived on D1 for the current strategy symbol, then
if(IsNewBar(m_symbol, PERIOD_H1)) {
   CalcATR(); // call our ATR calculation function
}

Na estratégia de negociação que estamos analisando neste ciclo de artigos, todas as ações fora do momento de formação de um novo candle podem ser completamente eliminadas:

//+------------------------------------------------------------------+
//| "Tick" event handler function                                    |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::Tick() override {
// If there is no new bar on M1, 
   if(!IsNewBar(m_symbol, PERIOD_M1)) return;

// If their number is less than allowed
   if(m_ordersTotal < m_maxCountOfOrders) {
      // Get an open signal
      int signal = SignalForOpen();

      if(signal == 1 /* || m_ordersTotal < 1 */) {          // If there is a buy signal, then
         OpenBuyOrder();         // open the BUY_STOP order
      } else if(signal == -1) {  // If there is a sell signal, then
         OpenSellOrder();        // open the SELL_STOP order
      }
   }
}

Também podemos introduzir a restrição de que o manipulador de eventos OnTick do EA não processe os ticks em momentos que não coincidam com o início de um novo candle para algum dos símbolos ou timeframes utilizados. Para conseguir isso, podemos efetuar as seguintes modificações no código do método CVirtualAdvisor::Tick():

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualAdvisor::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;
   }

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

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

// Adjusting market volumes
   m_receiver.Correct();

// Save status
   Save();

// Render the interface
   m_interface.Redraw();
}

Nesse código, adicionamos uma nova propriedade do EA, m_useOnlyNewBar, que pode ser configurada durante a criação do objeto EA:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...
   bool              m_useOnlyNewBar;  // Handle only new bar ticks

public:
                     CVirtualAdvisor(ulong p_magic = 1, string p_name = "",
                                     bool p_useOnlyNewBar = false); // Constructor
    ...
};


//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1,
                                 string p_name = "",
                                 bool p_useOnlyNewBar = false) :
// Initialize the receiver with a static receiver
   m_receiver(CVirtualReceiver::Instance(p_magic)),
// Initialize the interface with the static interface
   m_interface(CVirtualInterface::Instance(p_magic)),
   m_lastSaveTime(0),
   m_useOnlyNewBar(p_useOnlyNewBar) {
   m_name = StringFormat("%s-%d%s.csv",
                         (p_name != "" ? p_name : "Expert"),
                         p_magic,
                         (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                        );
};

Em princípio, para adicionar essa funcionalidade, poderíamos criar uma nova classe EA, herdada de CVirtualAdvisor, e adicionar a nova propriedade e a verificação da formação de um novo candle. Mas podemos deixar tudo como está, pois, com o valor padrão da propriedade m_useOnlyNewBar = false, tudo funcionará como antes, sem adicionar essa funcionalidade à classe do EA.

Se expandirmos a classe do EA dessa forma, dentro da classe da estratégia de negociação podemos omitir a verificação do evento de novo candle de minuto no método Tick(). Basta chamar uma vez a função IsNewBar() no construtor da estratégia, com o símbolo atual e o timeframe M1, para que o evento de novo candle com esse símbolo e timeframe comece a ser monitorado. Então, o EA, com o valor da propriedade m_useOnlyNewBar = true, simplesmente não processará o tick para as instâncias de estratégia, a menos que tenha ocorrido um novo candle no M1:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSimpleVolumesStrategy::CSimpleVolumesStrategy(
   ...) :
// Initialization list
   ... {
   CVirtualReceiver::Get(GetPointer(this), m_orders, m_maxCountOfOrders);

// Load the indicator to get tick volumes
   m_iVolumesHandle = iVolumes(m_symbol, m_timeframe, VOLUME_TICK);

// Set the size of the tick volume receiving array and the required addressing
   ArrayResize(m_volumes, m_signalPeriod);
   ArraySetAsSeries(m_volumes, true);

// Register the event handler for a new bar on the minimum timeframe
   IsNewBar(m_symbol, PERIOD_M1);
}


//+------------------------------------------------------------------+
//| "Tick" event handler function                                    |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::Tick() override {
// If their number is less than allowed
   if(m_ordersTotal < m_maxCountOfOrders) {
      // Get an open signal
      int signal = SignalForOpen();

      if(signal == 1 /* || m_ordersTotal < 1 */) {          // If there is a buy signal, then
         OpenBuyOrder();         // open the BUY_STOP order
      } else if(signal == -1) {  // If there is a sell signal, then
         OpenSellOrder();        // open the SELL_STOP order
      }
   }
}

Salvaremos essas alterações no arquivo SimpleVolumesStrategy.mqh na pasta atual. 


Verificação dos resultados

Adicionaremos ao EA BenchmarkInstancesExpert.mq5 um novo parâmetro de entrada, useOnlyNewBars_, que definirá se ele deve processar os ticks que não coincidam com o início de um novo candle. Na inicialização do EA, passaremos o valor desse parâmetro para o construtor do EA:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
...

input group "::: Other parameters"
sinput ulong  magic_          = 27183;   // - Magic
input bool    useOnlyNewBars_ = true;    // - Work only at bar opening

...

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   ...

// Create an EA handling virtual positions
   expert = new CVirtualAdvisor(magic_, "SimpleVolumes_BenchmarkInstances", useOnlyNewBars_);

   ...
}

Executaremos testes em um pequeno período com este EA e 256 instâncias de estratégias de negociação no modo "Todos os ticks com base em ticks reais", primeiro com o valor do parâmetro useOnlyNewBars_ = false e, depois, com o valor useOnlyNewBars_ = true.

No primeiro caso, ou seja, quando o EA processava cada tick, o lucro foi de $ 296, e o teste foi concluído em 04:15. No segundo caso, quando o EA ignorava todos os ticks, exceto os que coincidiam com o início de um novo candle, o lucro foi de $ 434, e o teste foi concluído em 00:25. Ou seja, não só reduzimos o custo computacional em 10 vezes, mas também obtivemos um lucro um pouco maior no segundo caso.

No entanto, não devemos ser excessivamente otimistas. Para outras estratégias de negociação, a repetição desses resultados não é garantida. Por isso, cada estratégia de negociação deve ser estudada separadamente para determinar se ela permite esse tipo de otimização.


Considerações finais

Proponho revisarmos os resultados alcançados. Verificamos a funcionalidade do EA ao operar simultaneamente com inúmeras instâncias de estratégias de negociação. Isso abre boas perspectivas para diversificar o comércio com diferentes símbolos, timeframes e estratégias, pois poderemos combiná-los em um único EA.

Também adicionamos uma nova funcionalidade à nossa biblioteca de classes — a capacidade de monitorar eventos de formação de novos candles. Embora essa funcionalidade não seja tão necessária para a estratégia em questão, ela pode ser muito útil na implementação de outras estratégias de negociação. Além disso, a possibilidade de limitar o funcionamento do EA apenas ao início de um novo candle pode ajudar a reduzir o consumo de recursos computacionais e obter resultados mais consistentes em diferentes modos de simulação de ticks.

No entanto, mais uma vez, nos desviamos um pouco da trajetória original deste projeto. Bem, isso também pode contribuir para o alcance do objetivo final. Após uma breve pausa, voltaremos com energia renovada para continuar o caminho rumo à automação dos testes dos nossos EAs. Parece que chegou a hora de retornar à questão da inicialização das instâncias de estratégias de negociação utilizando constantes de string e à construção de um sistema para armazenar informações sobre os resultados da otimização.

Obrigado pela atenção, até a próxima!



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

Arquivos anexados |
NewBarEvent.mqh (11.52 KB)
VirtualAdvisor.mqh (14.64 KB)
VirtualOrder.mqh (39.52 KB)
Desenvolvendo um sistema de Replay (Parte 64): Dando play no serviço (V) Desenvolvendo um sistema de Replay (Parte 64): Dando play no serviço (V)
Neste artigo irei mostrar como corrigir duas falhas que se encontram presentes no código. No entanto tais correções foram explicadas para que você, aspirante a programador, consiga entender que nem sempre as coisas irão acontecer como você havia previsto. Mas isto não é motivo para desespero e sim uma oportunidade de aprendizado. 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.
Redes neurais de maneira fácil (Parte 85): previsão multidimensional de séries temporais Redes neurais de maneira fácil (Parte 85): previsão multidimensional de séries temporais
Neste artigo, quero apresentar a vocês um novo método abrangente de previsão de séries temporais, que combina harmoniosamente as vantagens dos modelos lineares e dos transformers.
Redes neurais de maneira fácil (Parte 86): Transformador em forma de U Redes neurais de maneira fácil (Parte 86): Transformador em forma de U
Continuamos a analisar algoritmos de previsão de séries temporais. E neste artigo, proponho que você conheça o método U-shaped Transformer.
Redes neurais de maneira fácil (Parte 84): normalização reversível (RevIN) Redes neurais de maneira fácil (Parte 84): normalização reversível (RevIN)
Há muito já aprendemos que o pré-processamento dos dados brutos desempenha um grande papel na estabilidade do treinamento do modelo. E, para o processamento online de dados "brutos", frequentemente usamos a camada de normalização em lote. No entanto, às vezes surge a necessidade de um procedimento inverso. Um dos possíveis métodos para resolver tais tarefas é discutido neste artigo.