English Русский Español Deutsch 日本語
preview
Desenvolvendo um EA multimoeda (Parte 4): Ordens virtuais pendentes e salvamento de estado

Desenvolvendo um EA multimoeda (Parte 4): Ordens virtuais pendentes e salvamento de estado

MetaTrader 5Sistemas de negociação | 26 julho 2024, 13:37
267 0
Yuriy Bykov
Yuriy Bykov

Introdução

No artigo anterior, fizemos uma reestruturação significativa da arquitetura do código para construir um EA multimoeda com várias estratégias operando em paralelo. Para buscar simplicidade e clareza, até agora focamos apenas em um conjunto mínimo de funcionalidades. E mesmo com essa abordagem bastante limitada, alteramos consideravelmente o código dos artigos anteriores.

Agora esperamos que o trabalho realizado seja suficiente para aumentar a funcionalidade sem grandes refatorações no código já escrito. Tentaremos fazer o mínimo de mudanças possíveis, apenas onde for estritamente necessário.

Como desenvolvimento adicional neste artigo, tentaremos fazer o seguinte:

  • adicionar a capacidade de abrir ordens pendentes virtuais (Buy Stop, Sell Stop, Buy Limit, Sell Limit), e não apenas posições virtuais (Buy, Sell);
  • adicionar uma maneira simples de visualizar as ordens e posições virtuais definidas, para que possamos controlar visualmente durante os testes a correta implementação das regras de abertura de posições/ordens nas estratégias de negociação utilizadas;
  • implementar a salvaguarda de informações sobre o estado atual pelo EA, para que ao reiniciar o terminal ou ao transferir o EA para outro terminal, ele possa continuar a partir do estado em que estava no momento da interrupção.

Começaremos com o mais simples, isto é, trabalhar com ordens pendentes virtuais.


Ordens pendentes virtuais

Para lidar com posições virtuais, criamos a classe CVirtualOrder. Para ordens pendentes virtuais, podemos criar uma classe separada semelhante. No entanto, vamos ver se trabalhar com posições é significativamente diferente de lidar com ordens.

As propriedades são bastante similares, exceto pelo fato de que as ordens pendentes incluem uma propriedade adicional para armazenar o tempo de expiração. Por causa disso, elas têm um novo motivo para fechar quando o tempo de expiração é alcançado. Consequentemente, será preciso verificar a cada tick se a ordem pendente virtual foi encerrada por essa causa, enquanto deve permanecer aberta quando atingir os níveis de Stop Loss e Take Profit.

Ao atingir o preço de ativação da ordem pendente virtual, ela deve se transformar em uma posição virtual aberta. Aqui já fica claro que, ao implementar em diferentes classes, teremos que fazer um trabalho adicional para criar uma posição virtual aberta e remover a ordem pendente virtual ativada. Mas se implementarmos em uma única classe, só precisaremos mudar uma propriedade do objeto dessa classe, que é seu tipo. Portanto, vamos implementar exatamente essa abordagem.

O que mais diferenciará uma ordem de uma posição? Também precisaremos indicar o preço de abertura da ordem pendente virtual. Para a posição virtual, a indicação do preço de abertura não era necessária, pois era automaticamente determinada a partir dos dados de mercado atuais. Agora faremos dela um parâmetro obrigatório da função de abertura.

Vamos às adições na classe. Adicionaremos:

  • a propriedade m_expiration para armazenar o tempo de expiração;
  • a propriedade lógica m_isExpired para indicar o tempo de expiração da ordem pendente;
  • o método CheckTrigger() para verificar a ativação da ordem pendente, vários métodos de verificação se o objeto pertence a um tipo específico (se é uma ordem pendente, uma ordem limite, etc.);
  • nos métodos de verificação se o objeto é do tipo BUY ou SELL condições, para que retorne verdadeiro também nos casos em que se trate de uma ordem pendente do tipo desejado, independentemente de ser limite ou stop. 
  • na lista de parâmetros do método Open(), o preço de abertura e o tempo de expiração.

//+------------------------------------------------------------------+
//| Class of virtual orders and positions                            |
//+------------------------------------------------------------------+
class CVirtualOrder {
private:
   ...
//--- Order (position) properties
   ...
   datetime          m_expiration;     // Expiration time
   ...
   bool              m_isExpired;      // Expiration flag
   ...
//--- Private methods
   ...
   bool              CheckTrigger();   // Check for pending order trigger

public:
    ...

//--- Methods for checking the position (order) status
   ...
   bool              IsPendingOrder() {// Is it a pending order?
      return IsOpen() && (m_type == ORDER_TYPE_BUY_LIMIT
                          || m_type == ORDER_TYPE_BUY_STOP
                          || m_type == ORDER_TYPE_SELL_LIMIT
                          || m_type == ORDER_TYPE_SELL_STOP);
   }
   bool              IsBuyOrder() {    // Is it an open BUY position?
      return IsOpen() && (m_type == ORDER_TYPE_BUY
                          || m_type == ORDER_TYPE_BUY_LIMIT
                          || m_type == ORDER_TYPE_BUY_STOP);
   }
   bool              IsSellOrder() {   // Is it an open SELL position?
      return IsOpen() && (m_type == ORDER_TYPE_SELL 
                          || m_type == ORDER_TYPE_SELL_LIMIT
                          || m_type == ORDER_TYPE_SELL_STOP);
   }
   bool              IsStopOrder() {   // Is it a pending STOP order?
      return IsOpen() && (m_type == ORDER_TYPE_BUY_STOP || m_type == ORDER_TYPE_SELL_STOP);
   }
   bool              IsLimitOrder() {  // is it a pending LIMIT order?
      return IsOpen() && (m_type == ORDER_TYPE_BUY_LIMIT || m_type == ORDER_TYPE_SELL_LIMIT);
   }
   ...

//--- Methods for handling positions (orders)
   bool              CVirtualOrder::Open(string symbol,
                                         ENUM_ORDER_TYPE type,
                                         double lot,
                                         double price,
                                         double sl = 0,
                                         double tp = 0,
                                         string comment = "",
                                         datetime expiration = 0,
                                         bool inPoints = false); // Opening a position (order)

   ...
};

No método de abertura da posição virtual Open(), que agora também abrirá ordens pendentes virtuais, primeiro adicionaremos a atribuição da propriedade m_openPrice com o valor do preço de abertura passado, e depois, se for uma posição, a atribuição dessa propriedade com o preço de abertura de mercado atual:

bool CVirtualOrder::Open(string symbol,         // Symbol
                         ENUM_ORDER_TYPE type,  // Type (BUY or SELL)
                         double lot,            // Volume
                         double price = 0,      // Open price
                         double sl = 0,         // StopLoss level (price or points)
                         double tp = 0,         // TakeProfit level (price or points)
                         string comment = "",   // Comment
                         datetime expiration = 0,  // Expiration time
                         bool inPoints = false  // Are the SL and TP levels set in points?
                        ) {
   ...

   if(s_symbolInfo.Name(symbol)) {  // Select the desired symbol
      s_symbolInfo.RefreshRates();  // Update information about current prices

      // Initialize position properties
      m_openPrice = price;
     ...
      m_expiration = expiration;

      // The position (order) being opened is not closed by SL, TP or expiration
      ...
      m_isExpired = false;

      ...
      // Depending on the direction, set the opening price, as well as the SL and TP levels.
      // If SL and TP are specified in points, then we first calculate their price levels
      // relative to the open price
      if(IsBuyOrder()) {
         if(type == ORDER_TYPE_BUY) {
            m_openPrice = s_symbolInfo.Ask();
         }
         ...
      } else if(IsSellOrder()) {
         if(type == ORDER_TYPE_SELL) {
            m_openPrice = s_symbolInfo.Bid();
         }
         ...
      }

      ...

      return true;
   }
   return false;
}

No método de verificação de ativação da ordem pendente virtual CheckTrigger(), obteremos o preço de mercado atual Bid ou Ask dependendo da direção da ordem e verificaremos se alcançou o preço de abertura do lado correto. Se alcançar, alteramos a propriedade m_type do objeto atual para o valor correspondente à posição na direção desejada e notificamos os objetos receptores e a estratégia que uma nova posição virtual foi aberta.

//+------------------------------------------------------------------+
//| Check whether a pending order is triggered                       |
//+------------------------------------------------------------------+
bool CVirtualOrder::CheckTrigger() {
   if(IsPendingOrder()) {
      s_symbolInfo.Name(m_symbol);     // Select the desired symbol
      s_symbolInfo.RefreshRates();     // Update information about current prices
      double price = (IsBuyOrder()) ? s_symbolInfo.Ask() : s_symbolInfo.Bid();
      int spread = s_symbolInfo.Spread();

      // If the price has reached the opening levels, turn the order into a position
      if(false
            || (m_type == ORDER_TYPE_BUY_LIMIT && price <= m_openPrice)
            || (m_type == ORDER_TYPE_BUY_STOP  && price >= m_openPrice)
        ) {
         m_type = ORDER_TYPE_BUY;
      } else if(false
                || (m_type == ORDER_TYPE_SELL_LIMIT && price >= m_openPrice)
                || (m_type == ORDER_TYPE_SELL_STOP  && price <= m_openPrice)
               ) {
         m_type = ORDER_TYPE_SELL;
      }

      // If the order turned into a position 
      if(IsMarketOrder()) {
         m_openPrice = price; // Remember the open price

         // Notify the recipient and the strategy of the position opening 
         m_receiver.OnOpen(GetPointer(this));
         m_strategy.OnOpen();
         return true;
      }
   }
   return false;
}

Este método será chamado ao processar um novo tick no método Tick(), se for realmente uma ordem pendente virtual:

//+------------------------------------------------------------------+
//| Handle a tick of a single virtual order (position)               |
//+------------------------------------------------------------------+
void CVirtualOrder::Tick() {
   if(IsOpen()) {  // If this is an open virtual position or order
      if(CheckClose()) {  // Check if SL or TP levels have been reached
         Close();         // Close when reached
      } else if (IsPendingOrder()) {   // If this is a pending order
         CheckTrigger();  // Check if it is triggered
      }
   }
}

No método Tick(), chama-se o método CheckClose(), no qual também precisamos adicionar o código que verifica o fechamento da ordem pendente virtual pelo tempo de expiração:

//+------------------------------------------------------------------+
//| Check the need to close by SL, TP or EX                          |
//+------------------------------------------------------------------+
bool CVirtualOrder::CheckClose() {
   if(IsMarketOrder()) {               // If this is a market virtual position,
      ...
      // Check that the price has reached SL or TP
      ...
   } else if(IsPendingOrder()) {    // If this is a pending order
      // Check if the expiration time has been reached, if one is specified 
      if(m_expiration > 0 && m_expiration < TimeCurrent()) {
         m_isExpired = true;
         return true;
      }
   }
   return false;
}

Salvamos as alterações no arquivo VirtualOrder.mqh na pasta atual.

Agora vamos voltar à classe da nossa estratégia de negociação CSimpleVolumesStrategy. Nela deixamos espaços para futuras modificações, onde precisamos adicionar suporte para trabalhar com ordens pendentes virtuais. Esses espaços estavam em dois métodos OpenBuyOrder() e OpenSellOrder(). Adicionaremos nesses locais a chamada do método Open() com parâmetros que levem à abertura de ordens pendentes virtuais. Calcularemos previamente o preço de abertura a partir do preço atual, afastando-se na direção necessária pelo número de pontos especificado pelo parâmetro m_openDistance. Fornecemos o código apenas para o método OpenBuyOrder(), sendo as alterações análogas no outro método.

//+------------------------------------------------------------------+
//| Open BUY order                                                   |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::OpenBuyOrder() {
// Update symbol current price data
   ...
// Retrieve the necessary symbol and price data
   ...

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

// Opening price
   double price = ask + distance * point;

// StopLoss and TakeProfit levels
   ...

// Expiration time
   datetime expiration = TimeCurrent() + m_ordersExpiration * 60;

   ...
   for(int i = 0; i < m_maxCountOfOrders; i++) {   // Iterate through all virtual positions
      if(!m_orders[i].IsOpen()) {                  // If we find one that is not open, then open it
         if(m_openDistance > 0) {
            // Set SELL STOP pending order
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY_STOP, m_fixedLot,
                                   NormalizeDouble(price, digits),
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits),
                                   "", expiration);

         } else if(m_openDistance < 0) {
            // Set SELL LIMIT pending order
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY_LIMIT, m_fixedLot,
                                   NormalizeDouble(price, digits),
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits),
                                   "", expiration);

         } else {
            // Open a virtual SELL position
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot,
                                   0,
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits));

         }
         break; // and exit
      }
   }
  ...
}

Salvamos as alterações no arquivo SimpleVolumesStrategy.mqh na pasta atual.

Com isso, as mudanças necessárias para suportar estratégias com ordens pendentes virtuais estão concluídas. Fizemos alterações em apenas dois arquivos e agora podemos compilar o arquivo do EA SimpleVolumesExpertSingle.mq5. Ao definir o parâmetro openDistance_ como diferente de zero, o EA deve abrir ordens pendentes virtuais em vez de posições virtuais. No entanto, o momento de abertura não será visível no gráfico. Apenas no log poderemos ver mensagens indicando que uma ordem pendente virtual foi aberta. No gráfico, poderemos vê-las apenas após se transformarem em uma posição virtual aberta, que será exibida pelo objeto receptor dos volumes de negociação virtuais.

Aqui enfrentamos a necessidade de visualizar as ordens pendentes virtuais no gráfico. Voltaremos a esse ponto mais tarde, mas agora vamos abordar uma questão mais importante: garantir a salvaguarda e o carregamento do estado do EA após reiniciar.


Salvaguarda de Estado

Com base nas classes desenvolvidas, criamos dois EAs. O primeiro (SimpleVolumesExpertSingle.mq5) foi projetado para otimizar os parâmetros de uma única instância da estratégia de negociação, enquanto o segundo (SimpleVolumesExpert.mq5) incluía várias instâncias da estratégia com os melhores parâmetros, selecionados pelo primeiro EA. No futuro, apenas o segundo EA tem perspectiva de uso em contas reais, enquanto o primeiro é destinado apenas ao testador de estratégias. Por esse motivo, precisamos salvar e carregar o estado apenas no segundo EA ou em outros que também incluam várias instâncias de estratégias de negociação.

É importante esclarecer que estamos falando sobre a salvaguarda e o carregamento do estado do EA, não dos diferentes conjuntos de estratégias a serem usadas no EA. Ou seja, o conjunto de instâncias de estratégias com parâmetros definidos no EA é fixo e o mesmo a cada execução. Após a primeira execução, o EA abre posições virtuais e reais, e talvez calcule alguns indicadores a partir dos dados de preços. Essas informações representam o estado geral do EA. Se reiniciarmos o terminal, o EA deve não só reconhecer as posições abertas como suas, mas também restaurar todas as suas posições virtuais e valores de cálculo necessários. Se as informações sobre posições abertas podem ser obtidas do terminal, as informações sobre posições virtuais e valores de cálculo precisam ser salvas pelo próprio EA.

Para a estratégia de negociação simples que consideramos, não é necessário acumular dados de cálculo. Dessa forma, o estado do EA será determinado apenas pelo conjunto de posições virtuais e ordens pendentes. No entanto, apenas salvar o array de todos os objetos da classe CVirtualOrder em um arquivo não será suficiente.

Suponhamos que temos vários EAs com diferentes conjuntos de estratégias de negociação. Mas o número total de objetos da classe CVirtualOrder criados por cada EA foi o mesmo. Por exemplo, em cada um, utilizamos 9 instâncias de estratégias de negociação, solicitando 3 objetos de posições virtuais. Então, cada EA irá salvar informações sobre 27 objetos da classe CVirtualOrder. Nesse caso, precisamos garantir que nenhum EA carregue informações sobre 27 posições virtuais que não sejam suas. 

Para isso, podemos adicionar no arquivo salvo informações sobre os parâmetros das estratégias que operam dentro deste EA, e possivelmente informações sobre os parâmetros do próprio EA.

Agora pensemos sobre os momentos em que a salvaguarda de estado deve ocorrer. Se os parâmetros das estratégias são fixos, então eles são iguais em qualquer momento. Mas os objetos de posições virtuais podem mudar os valores de suas propriedades durante as operações de abertura e fechamento. Por isso, faz sentido salvar o estado logo após essas operações. Salvar com mais frequência, por exemplo, a cada tick ou por timer, ainda parece excessivo.

Vamos começar a implementação. Como temos uma estrutura hierárquica de objetos do tipo:

  • EA
    • estratégias
      • posições virtuais

delegaremos parte do trabalho de salvaguarda a cada nível, ao invés de concentrá-lo todo no nível superior, embora isso seja possível.

Em cada nível, adicionaremos dois métodos às classes: Save() e Load(), responsáveis por salvar e carregar, respectivamente. No nível superior, esses métodos abrirão o arquivo, e nos níveis inferiores, o descritor do arquivo já aberto será passado como parâmetro para esses métodos. Assim, a questão da escolha do nome do arquivo para salvamento será resolvida apenas no nível do EA, e nos níveis das estratégias e posições virtuais essa questão não surgirá.


Modificação do EA

Adicionaremos ao classe CVirtualAdvisor o campo m_name para armazenar o nome do EA. Como ele não será alterado durante a operação, faremos sua atribuição no construtor. Também faz sentido expandir o nome adicionando um número mágico e um sufixo opcional ".test" no caso de o EA ser executado no modo de teste visual.

Para realizar a salvaguarda apenas quando houver mudanças nas posições virtuais, adicionaremos o campo m_lastSaveTime, que armazenará o horário da última salvaguarda. 

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...
   
   string            m_name;           // EA name
   datetime          m_lastSaveTime;   // Last save time

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

   virtual bool      Save();           // Save status
   virtual bool      Load();           // Load status
};

Ao criar o EA, atribuiremos valores iniciais às duas novas propriedades da seguinte forma:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1, string p_name = "") :
   ...
   m_lastSaveTime(0) {
   m_name = StringFormat("%s-%d%s.csv",
                         (p_name != "" ? p_name : "Expert"),
                         p_magic,
                         (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                        );
};

A lógica para verificar a necessidade de realizar a salvaguarda será colocada dentro do método Save(). Portanto, a cada tick, podemos simplesmente adicionar a chamada desse método após a execução das outras ações do tick:

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Receiver handles virtual positions
   m_receiver.Tick();

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

// Adjusting market volumes
   m_receiver.Correct();

// Save status
   Save();
}

No método de salvaguarda, primeiro devemos verificar se precisamos executá-la. Para isso, precisaremos combinar previamente que adicionaremos ao objeto receptor uma nova propriedade que armazenará o horário das últimas alterações nas posições virtuais abertas ou o horário da última correção dos volumes abertos reais. Assim, se o horário da última salvaguarda for anterior ao horário da última correção, ocorreram alterações e precisamos salvá-las.

Além disso, não salvaremos mudanças se estiver ocorrendo otimização ou teste individual sem uso do modo visual. O salvamento será realizado apenas quando o teste estiver em modo visual. Isso permitirá verificar o processo de salvamento tanto no testador de estratégias quanto quando o EA estiver rodando no terminal.

Aqui está como o método de salvamento Save() pode ser implementado no EA: primeiro, verificamos se é necessário salvar. Em seguida, registramos o horário atual e o número de estratégias. Depois disso, usamos um loop para chamar o método de salvamento de cada estratégia.

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Save() {
   bool res = true;

   // Save status if:
   if(true
         // later changes appeared
         && m_lastSaveTime < CVirtualReceiver::s_lastChangeTime
         // currently, there is no optimization
         && !MQLInfoInteger(MQL_OPTIMIZATION)
         // and there is no testing at the moment or there is a visual test at the moment
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      int f = FileOpen(m_name, FILE_CSV | FILE_WRITE, '\t');

      if(f != INVALID_HANDLE) {  // If file is open, save
         FileWrite(f, CVirtualReceiver::s_lastChangeTime);  // Time of last changes
         FileWrite(f, ArraySize(m_strategies));             // Number of strategies

         // All strategies
         FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).Save(f));

         FileClose(f);

         // Update the last save time
         m_lastSaveTime = CVirtualReceiver::s_lastChangeTime;
         PrintFormat(__FUNCTION__" | OK at %s to %s",
                     TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS), m_name);
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d",
                     m_name, GetLastError());
         res = false;
      }
   }
   return res;
}

Após salvar as estratégias, atualizamos o horário do último salvamento de acordo com o horário das últimas mudanças nas posições virtuais. Agora, até a próxima alteração, o método de salvamento não gravará nada no arquivo.

O método de carregamento de estado Load() deve realizar um trabalho similar, apenas em vez de gravar, ele lerá os dados do arquivo. Primeiro, lemos o horário gravado e a quantidade de estratégias. Podemos, por precaução, verificar se a quantidade de estratégias lida corresponde à quantidade de estratégias adicionadas a este EA. Se não corresponder, não faz sentido continuar a leitura, pois é um arquivo incorreto. Se corresponder, está tudo certo, podemos continuar a leitura. Em seguida, delegamos o trabalho subsequente aos objetos do próximo nível hierárquico: iteramos sobre todas as estratégias adicionadas e chamamos seu método de leitura do arquivo aberto.

No código, isso pode se parecer com:

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Load() {
   bool res = true;

   // Load status if:
   if(true
         // file exists
         && FileIsExist(m_name)
         // currently, there is no optimization
         && !MQLInfoInteger(MQL_OPTIMIZATION)
         // and there is no testing at the moment or there is a visual test at the moment
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      int f = FileOpen(m_name, FILE_CSV | FILE_READ, '\t');

      if(f != INVALID_HANDLE) {  // If the file is open, then load
         m_lastSaveTime = FileReadDatetime(f);     // Last save time
         PrintFormat(__FUNCTION__" | LAST SAVE at %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS));

         // Number of strategies
         long f_strategiesCount = StringToInteger(FileReadString(f));

         // Does the loaded number of strategies match the current one?
         res = (ArraySize(m_strategies) == f_strategiesCount);

         if(res) {
            // Load all strategies
            FOREACH(m_strategies, res &= ((CVirtualStrategy*) m_strategies[i]).Load(f));

            if(!res) {
               PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_name);
            }
         } else {
            PrintFormat(__FUNCTION__" | ERROR: Wrong strategies count (%d expected but %d found in file %s)",
                        ArraySize(m_strategies), f_strategiesCount, m_name);
         }
         FileClose(f);
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d", m_name, GetLastError());
         res = false;
      }
   }
   return res;
}

Salvamos as alterações feitas no arquivo VirtualAdvisor.mqh na pasta atual.

Devemos chamar o método de carregamento de estado apenas uma vez ao iniciar o EA, mas não podemos fazer isso no construtor do objeto do EA, pois, neste momento, as estratégias ainda não foram adicionadas ao EA. Desse modo, faremos isso na função OnInit() no arquivo do EA, e faremos isso depois de adicionar todas as instâncias de estratégias ao objeto do EA:

CVirtualAdvisor     *expert;                  // EA object

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Create and fill the array of strategy instances
   CStrategy *strategies[9];
   strategies[0] = ...
   ...
   strategies[8] = ...

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

// Add strategies to the EA
   FOREACH(strategies, expert.Add(strategies[i]));

// Load the previous state if available   
   expert.Load();

   return(INIT_SUCCEEDED);
}

Salvamos essas alterações no arquivo SimpleVolumesExpert.mq5 na pasta atual.


Modificação da estratégia básica

Adicionamos ao classe base da estratégia de negociação que utiliza posições virtuais, os métodos Save() e Load(), além do método de conversão do objeto atual da estratégia para uma string. Para simplificar, implementaremos este método como um operador unário sobrecarregado ~ (til).

//+------------------------------------------------------------------+
//| Class of a trading strategy with virtual positions               |
//+------------------------------------------------------------------+
class CVirtualStrategy : public CStrategy {
   ...

public:
   ...

   virtual bool      Load(const int f);   // Load status
   virtual bool      Save(const int f);   // Save status

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

O método de conversão do objeto para string retornará uma string com o nome da classe atual e a quantidade de elementos da matriz de posições virtuais:

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

O método Save() receberá um descritor de arquivo aberto para gravação. Este arquivo primeiro gravará a string obtida da conversão do objeto para string. Então, em um loop, chamará o método de salvamento de cada posição virtual.

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CVirtualStrategy::Save(const int f) {
   bool res = true;
   FileWrite(f, ~this); // Save parameters

   // Save virtual positions (orders) of the strategy
   FOREACH(m_orders, res &= m_orders[i].Save(f));

   return res;
}

O método de carregamento Load() apenas lerá os dados na mesma ordem em que foram gravados, verificando a correspondência entre a string de parâmetros no arquivo e na estratégia:

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualStrategy::Load(const int f) {
   bool res = true;
   // Current parameters are equal to read parameters   
   res = (~this == FileReadString(f));
   
   // If yes, then load the virtual positions (orders) of the strategy
   if(res) {
      FOREACH(m_orders, res &= m_orders[i].Load(f));
   }

   return res;
}

Salvamos as alterações feitas no arquivo VirtualStrategy.mqh na pasta atual.


Modificação da estratégia de negociação

Na classe da estratégia de negociação específica CSimpleVolumesStrategy, precisaremos adicionar o mesmo conjunto de métodos que na classe base:

//+------------------------------------------------------------------+
//| Trading strategy using tick volumes                              |
//+------------------------------------------------------------------+
class CSimpleVolumesStrategy : public CVirtualStrategy {
   ...

public:
   ...

   virtual bool      Load(const int f) override;   // Load status
   virtual bool      Save(const int f) override;   // Save status

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

No método de conversão da estratégia de negociação para string, formaremos o resultado a partir do nome da classe e da enumeração de todos os parâmetros críticos. Para simplificar, adicionaremos os valores de todos os parâmetros, mas, no futuro, pode ser conveniente reduzir essa lista. Isso permitirá reiniciar o EA com parâmetros ligeiramente alterados sem fechar todas as posições de mercado abertas anteriormente. Por exemplo, se aumentarmos o parâmetro TakeProfit, podemos deixar as posições abertas com um nível de TakeProfit menor.

//+------------------------------------------------------------------+
//| Convert an object to a string                                    |
//+------------------------------------------------------------------+
string CSimpleVolumesStrategy::operator~() {
   return StringFormat("%s(%s,%s,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d)",
                       // Strategy instance parameters
                       typename(this), m_symbol, EnumToString(m_timeframe), m_fixedLot,
                       m_signalPeriod, m_signalDeviation, m_signaAddlDeviation,
                       m_openDistance, m_stopLevel, m_takeLevel, m_ordersExpiration,
                       m_maxCountOfOrders
                      );
}

Sim, isso cria mais um lugar onde precisamos escrever todos os parâmetros da estratégia no código. Vamos lembrar disso até começarmos a trabalhar com os parâmetros de entrada.

O método de salvamento Save() fica bastante conciso, pois o trabalho principal será feito pela classe base:

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CSimpleVolumesStrategy::Save(const int f) {
   bool res = true;
   FileWrite(f, ~this);                // Save parameters
   res &= CVirtualStrategy::Save(f);   // Save strategy
   return res;
}

O método de carregamento Load() será um pouco mais extenso, mas principalmente para melhorar a legibilidade do código:

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CSimpleVolumesStrategy::Load(const int f) {
   bool res = true;
   string currentParams = ~this;             // Current parameters
   string loadedParams = FileReadString(f);  // Read parameters

   PrintFormat(__FUNCTION__" | %s", loadedParams);

   res = (currentParams == loadedParams);

   // Load if read parameters match the current ones
   if(res) {
      res &= CVirtualStrategy::Load(f);
   }

   return res;
}

Salvamos as alterações no arquivo SimpleVolumesExpert.mqh na pasta atual.


Modificação das posições virtuais

Para a classe de posições virtuais CVirtualOrder, também precisamos adicionar três métodos:

class CVirtualOrder {
   ...

   virtual bool      Load(const int f);   // Load status
   virtual bool      Save(const int f);   // Save status

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

O método de conversão para string não será usado no salvamento para arquivo, mas será útil para registrar informações sobre o objeto carregado no log:

//+------------------------------------------------------------------+
//| Convert an object to a string                                    |
//+------------------------------------------------------------------+
string CVirtualOrder::operator~() {
   if(IsOpen()) {
      return StringFormat("#%d %s %s %.2f in %s at %.5f (%.5f, %.5f). %s, %f",
                          m_id, TypeName(), m_symbol, m_lot,
                          TimeToString(m_openTime), m_openPrice,
                          m_stopLoss, m_takeProfit,
                          TimeToString(m_closeTime), m_closePrice);
   } else {
      return StringFormat("#%d --- ", m_id);
   }

}

Embora, possivelmente, mudemos isso no futuro, adicionando também um método para ler as propriedades do objeto a partir de uma string.

No método de salvamento Save(), finalmente gravaremos informações mais substanciais no arquivo:

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CVirtualOrder::Save(const int f) {
   FileWrite(f, m_id, m_symbol, m_lot, m_type, m_openPrice,
             m_stopLoss, m_takeProfit,
             m_openTime, m_closePrice, m_closeTime,
             m_expiration, m_comment, m_point);
   return true;
}

O método de carregamento Load() não apenas lerá o que foi gravado, preenchendo as propriedades necessárias com as informações lidas, mas também notificará os objetos de estratégia e receptores associados sobre se essa posição virtual (ordem) está aberta ou fechada:

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualOrder::Load(const int f) {
   m_id = (ulong) FileReadNumber(f);
   m_symbol = FileReadString(f);
   m_lot = FileReadNumber(f);
   m_type = (ENUM_ORDER_TYPE) FileReadNumber(f);
   m_openPrice = FileReadNumber(f);
   m_stopLoss = FileReadNumber(f);
   m_takeProfit = FileReadNumber(f);
   m_openTime = FileReadDatetime(f);
   m_closePrice = FileReadNumber(f);
   m_closeTime = FileReadDatetime(f);
   m_expiration = FileReadDatetime(f);
   m_comment = FileReadString(f);
   m_point = FileReadNumber(f);

   PrintFormat(__FUNCTION__" | %s", ~this);

// Notify the recipient and the strategy that the position (order) is open
   if(IsOpen()) {
      m_receiver.OnOpen(GetPointer(this));
      m_strategy.OnOpen();
   } else {
      m_receiver.OnClose(GetPointer(this));
      m_strategy.OnClose();
   }

   return true;
}

Salvamos o código no arquivo VirtualOrder.mqh na pasta atual.


Testando o salvamento

Agora testaremos o salvamento e carregamento das informações de estado. Para não esperar até que ocorra um momento favorável para abrir posições, faremos alterações temporárias em nossa estratégia de negociação para abrir uma posição ou ordem pendente ao iniciar, se ainda não houver posições/ordens abertas.

Como adicionamos ao código mensagens sobre o andamento do carregamento, ao reiniciar o EA (o mais fácil é recompilar o código), veremos no log algo como:

CVirtualAdvisor::Load | LAST SAVE at 2027.02.23 08:05:33

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,0.06,13,0.30,1.00,0,10500.00,465.00,1000,3)
CVirtualOrder::Load | Order#1 EURGBP 0.06 BUY in 2027.02.23 08:02 at 0.85494 (0.75007, 0.85985). 1970.01.01 00:00, 0.000000
CVirtualReceiver::OnOpen#EURGBP | OPEN VirtualOrder #1
CVirtualOrder::Load | Order#2  ---
CVirtualOrder::Load | Order#3  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,0.11,17,1.70,0.50,210,16500.00,220.00,1000,3)
CVirtualOrder::Load | Order#4 EURGBP 0.11 BUY STOP in 2027.02.23 08:02 at 0.85704 (0.69204, 0.85937). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#5  ---
CVirtualOrder::Load | Order#6  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,0.06,51,0.50,1.10,500,19500.00,370.00,22000,3)
CVirtualOrder::Load | Order#7 EURGBP 0.06 BUY STOP in 2027.02.23 08:02 at 0.85994 (0.66494, 0.86377). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#8  ---
CVirtualOrder::Load | Order#9  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(GBPUSD,PERIOD_H1,0.04,80,1.10,0.20,0,6000.00,1190.00,1000,3)
CVirtualOrder::Load | Order#10 GBPUSD 0.04 BUY in 2027.02.23 08:02 at 1.26632 (1.20638, 1.27834). 1970.01.01 00:00, 0.000000
CVirtualReceiver::OnOpen#GBPUSD | OPEN VirtualOrder #10
CVirtualOrder::Load | Order#11  ---
CVirtualOrder::Load | Order#12  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(GBPUSD,PERIOD_H1,0.11,128,2.00,0.90,220,2000.00,1170.00,1000,3)
CVirtualOrder::Load | Order#13 GBPUSD 0.11 BUY STOP in 2027.02.23 08:02 at 1.26852 (1.24852, 1.28028). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#14  ---
CVirtualOrder::Load | Order#15  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(GBPUSD,PERIOD_H1,0.07,13,1.50,0.80,550,2500.00,1375.00,1000,3)
CVirtualOrder::Load | Order#16 GBPUSD 0.07 BUY STOP in 2027.02.23 08:02 at 1.27182 (1.24682, 1.28563). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#17  ---
CVirtualOrder::Load | Order#18  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURUSD,PERIOD_H1,0.04,24,0.10,0.30,330,7500.00,2400.00,24000,3)
CVirtualOrder::Load | Order#19 EURUSD 0.04 BUY STOP in 2027.02.23 08:02 at 1.08586 (1.01086, 1.10990). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#20  ---
CVirtualOrder::Load | Order#21  ---
CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURUSD,PERIOD_H1,0.05,18,0.20,0.40,220,19500.00,1480.00,6000,3)
CVirtualOrder::Load | Order#22 EURUSD 0.05 BUY STOP in 2027.02.23 08:02 at 1.08476 (0.88976, 1.09960). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#23  ---
CVirtualOrder::Load | Order#24  ---
CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURUSD,PERIOD_H1,0.05,128,0.70,0.30,550,3000.00,170.00,42000,3)
CVirtualOrder::Load | Order#25 EURUSD 0.05 BUY STOP in 2027.02.23 08:02 at 1.08806 (1.05806, 1.08980). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#26  ---
CVirtualOrder::Load | Order#27  ---

CVirtualAdvisor::Save | OK at 2027.02.23 08:19:48 to SimpleVolumes-27182.csv

Como podemos ver, as informações sobre as posições virtuais abertas e ordens pendentes são carregadas com sucesso. Para as posições virtuais, é chamado o manipulador de eventos de abertura, que, se necessário, abrirá posições de mercado reais, caso tenham sido fechadas manualmente, por exemplo.

O comportamento do EA ao reiniciar em relação às posições abertas é uma questão bastante complexa. Por exemplo, se quisermos mudar o número mágico, o EA deixará de considerar as negociações anteriormente abertas como suas, e elas podem ficar pendentes. Devemos forçar seu fechamento? Se quisermos substituir a versão do EA, talvez seja necessário ignorar completamente a presença de posições virtuais salvas e fechar todas as posições abertas à força. Precisamos decidir qual cenário nos agrada mais a cada vez. Ainda não chegamos ao ponto em que essas questões se tornarão urgentes, então vamos adiá-las para o futuro.


Visualização

Chegou a vez da visualização das posições virtuais e ordens pendentes. À primeira vista, parece bastante natural implementá-la como uma extensão da classe de posição virtual CVirtualOrder. Aqui já temos todas as informações necessárias sobre o objeto a ser visualizado, ele sabe melhor do que ninguém quando precisa se redesenhar. Na verdade, a primeira implementação preliminar foi feita exatamente assim. Mas então começaram a surgir momentos bastante desagradáveis, acompanhados de cartazes dobrados com listas claramente longas de perguntas, que eles estavam prontos para desenrolar a qualquer momento dizendo: "Ah, é assim? Então nós — assim!"

Uma dessas perguntas inocentes era: "E em qual gráfico você planeja me visualizar?" A resposta simples — no atual — só era adequada para o caso em que o EA operasse em um único símbolo, e esse símbolo coincidisse com o símbolo do gráfico em que o EA estava executado. Assim que os símbolos se tornassem vários, exibir todas as posições virtuais no gráfico de um símbolo se tornaria muito inconveniente. No gráfico, surgiria uma bagunça que não traria nenhuma clareza adicional, apenas confusão.

Ou seja, a questão de escolher o gráfico para exibição exigia uma solução, mas isso já não se relacionava à funcionalidade principal dos objetos da classe CVirtualOrder. Eles funcionavam bem sem a nossa visualização, e aqui estamos sobrecarregando-os com algum tipo de relatório, quase literalmente em formato de papel.

Portanto, deixemos essa classe em paz e olhemos um pouco para frente. Se expandirmos um pouco nossos desejos, podemos imaginar que seria bom ter a capacidade de ligar/desligar seletivamente a exibição de posições virtuais, agrupadas por estratégia ou tipo, habilitar a exibição mais detalhada de informações, por exemplo, com preços de abertura visíveis, StopLoss e TakeProfit, perdas e lucros planejados em caso de acionamento, e talvez até a possibilidade de modificar manualmente esses níveis. Embora o último seria mais útil se estivéssemos desenvolvendo um painel para negociação semi-automatizada, e não um EA que opera com estratégias estritamente algorítmicas. Enfim, mesmo começando com uma implementação simples, podemos olhar um pouco mais à frente, imaginando o desenvolvimento futuro do código. Isso pode ajudar a escolher uma direção em que menos precisaremos voltar para refatorar o código já escrito.

Então, vamos criar uma nova classe base para todos os objetos que de alguma forma estarão relacionados à visualização de algo nos gráficos:

//+------------------------------------------------------------------+
//| Basic class for visualizing various objects                      |
//+------------------------------------------------------------------+
class CInterface {
protected:
   static ulong      s_magic;       // EA magic number
   bool              m_isActive;    // Is the interface active?
   bool              m_isChanged;   // Does the object have any changes?
public:
   CInterface();                    // Constructor
   virtual void      Redraw() = 0;  // Draw changed objects on the chart
   virtual void      Changed() {    // Set the flag for changes
      m_isChanged = true;
   }
};

ulong CInterface::s_magic = 0;

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CInterface::CInterface() :
   m_isActive(!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)),
   m_isChanged(true) {}

Salvaremos esse código no arquivo Interface.mqh na pasta atual.

Com base nessa classe, criaremos duas novas classes:

  • CVirtualChartOrder — representará um objeto que exibe uma posição virtual ou ordem pendente no gráfico do terminal (posição virtual gráfica). Ele poderá desenhar a posição virtual no gráfico se houverem alterações, e o gráfico com o instrumento necessário será aberto automaticamente, se não estiver aberto no terminal.
  • CVirtualInterface — agregador de todos os objetos gráficos da interface do EA. Por enquanto, ele conterá apenas uma matriz de posições virtuais gráficas. Ele criará objetos de posições virtuais gráficas sempre que um objeto de posição virtual for criado. Também receberá mensagens sobre mudanças nas posições virtuais e chamará a reedição das posições virtuais gráficas correspondentes. Esse agregador existirá em uma única instância (implementando o padrão de design Singleton) e estará disponível na classe CVirtualAdvisor.

A composição da classe CVirtualChartOrder será aproximadamente assim:

//+------------------------------------------------------------------+
//| Graphic virtual position class                                   |
//+------------------------------------------------------------------+
class CVirtualChartOrder : public CInterface {
   CVirtualOrder*    m_order;          // Associated virtual position (order)
   CChart            m_chart;          // Chart object to be displayed

   // Objects on the chart to display the virtual position
   CChartObjectHLine m_openLine;       // Open price line

   long              FindChart();      // Search/open the desired chart
public:
   CVirtualChartOrder(CVirtualOrder* p_order);     // Constructor
   ~CVirtualChartOrder();                          // Destructor

   bool              operator==(const ulong id) {  // Comparison operator by Id
      return m_order.Id() == id;
   }

   void              Show();    // Show a virtual position (order)
   void              Hide();    // Hide a virtual position (order) 

   virtual void      Redraw() override;   // Redraw a virtual position (order) 
};

O método de reedição Redraw() verificará se precisa ser executado e, se necessário, chamará métodos para mostrar ou ocultar a posição virtual do gráfico:

//+------------------------------------------------------------------+
//| Redraw a virtual position (order)                                |
//+------------------------------------------------------------------+
void CVirtualChartOrder::Redraw() {
   if(m_isChanged) {
      if(m_order.IsOpen()) {
         Show();
      } else {
         Hide();
      }
      m_isChanged = false;
   }
}

No método de mostrar Show(), primeiramente será chamado o método FindChart() para determinar em qual gráfico exibir a posição virtual. Nesse método, apenas iteramos por todos os gráficos abertos até encontrar um gráfico com o símbolo adequado. Se não for encontrado, abriremos um novo gráfico. O gráfico encontrado (ou aberto) será armazenado na propriedade m_chart.

//+------------------------------------------------------------------+
//| Finding a chart to display                                       |
//+------------------------------------------------------------------+
long CVirtualChartOrder::FindChart() {
   if(m_chart.ChartId() == -1 || m_chart.Symbol() != m_order.Symbol()) {
      long currChart, prevChart = ChartFirst();
      int i = 0, limit = 1000;

      currChart = prevChart;

      while(i < limit) { // we probably have no more than 1000 open charts
         if(ChartSymbol(currChart) == m_order.Symbol()) {
            return currChart;
         }
         currChart = ChartNext(prevChart); // get new chart on the basis of the previous one
         if(currChart < 0)
            break;        // end of chart list is reached
         prevChart = currChart; // memorize identifier of the current chart for ChartNext()
         i++;
      }

      // If a suitable chart is not found, then open a new one
      if(currChart == -1) {
         m_chart.Open(m_order.Symbol(), PERIOD_CURRENT);
      }
   }
   return m_chart.ChartId();
}

O método Show() atualmente desenha apenas uma linha horizontal correspondente ao preço de abertura. Sua cor e aparência serão determinadas com base na direção e tipo da posição (ordem). O método Hide() removerá essa linha. O enriquecimento adicional dessa classe ocorrerá precisamente nesses métodos.

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

A implementação da classe CVirtualInterface pode ser realizada, por exemplo, assim:

//+------------------------------------------------------------------+
//| EA GUI class                                                     |
//+------------------------------------------------------------------+
class CVirtualInterface : public CInterface {
protected:
// Static pointer to a single class instance
   static   CVirtualInterface *s_instance;

   CVirtualChartOrder *m_chartOrders[];   // Array of graphical virtual positions

//--- Private methods
   CVirtualInterface();   // Closed constructor

public:
   ~CVirtualInterface();  // Destructor

//--- Static methods
   static
   CVirtualInterface  *Instance(ulong p_magic = 0);   // Singleton - creating and getting a single instance

//--- Public methods
   void              Changed(CVirtualOrder *p_order); // Handle virtual position changes
   void              Add(CVirtualOrder *p_order);     // Add a virtual position

   virtual void      Redraw() override;   // Draw changed objects on the chart
};

No método estático de criação da única instância Instance(), adicionaremos a inicialização da variável estática s_magic, se um número mágico diferente de zero tiver sido passado:

//+------------------------------------------------------------------+
//| Singleton - creating and getting a single instance               |
//+------------------------------------------------------------------+
CVirtualInterface* CVirtualInterface::Instance(ulong p_magic = 0) {
   if(!s_instance) {
      s_instance = new CVirtualInterface();
   }
   if(s_magic == 0 && p_magic != 0) {
      s_magic = p_magic;
   }
   return s_instance;
}

No método de processamento de eventos de abertura/fechamento de posição virtual, encontraremos o objeto de posição virtual gráfica correspondente e marcaremos que houve mudanças nele:

//+------------------------------------------------------------------+
//| Handle virtual position changes                                  |
//+------------------------------------------------------------------+
void CVirtualInterface::Changed(CVirtualOrder *p_order) {
   // Remember that this position has changes
   int i;
   FIND(m_chartOrders, p_order.Id(), i);
   if(i != -1) {
      m_chartOrders[i].Changed();
      m_isChanged = true;
   }
}

E, finalmente, no método de desenho da interface Redraw(), chamaremos em loop os métodos de desenho de todas as posições virtuais gráficas:

//+------------------------------------------------------------------+
//| Draw changed objects on a chart                                  |
//+------------------------------------------------------------------+
void CVirtualInterface::Redraw() {
   if(m_isActive && m_isChanged) {  // If the interface is active and there are changes
      // Start redrawing graphical virtual positions 
      FOREACH(m_chartOrders, m_chartOrders[i].Redraw());
      m_isChanged = false;          // Reset the changes flag
   }
}

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

Agora resta fazer os ajustes finais para fazer a sub-sistema de exibição de posições virtuais nos gráficos funcionar. Na classe CVirtualAdvisor, adicionaremos a nova propriedade m_interface, que armazenará a única instância do objeto da interface de exibição. Precisamos cuidar da sua inicialização no construtor e da sua exclusão no destrutor:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...
   CVirtualInterface *m_interface;     // Interface object to show the status to the user
   
   ...
};

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1, string p_name = "") :
   ...
// Initialize the interface with the static interface
   m_interface(CVirtualInterface::Instance(p_magic)),
   ... {
   ...
};

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   
... 
   delete m_interface;        // Remove the interface
}

No manipulador de eventos OnTick, adicionaremos após todas as operações a chamada do método de reedição da interface, pois essa é a parte menos importante do processamento do tick: 

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

Na classe CVirtualReceiver, também adicionaremos a nova propriedade m_interface, que armazenará a única instância do objeto da interface de exibição. Precisamos cuidar da sua inicialização no construtor:

//+------------------------------------------------------------------+
//| Class for converting open volumes to market positions (receiver) |
//+------------------------------------------------------------------+
class CVirtualReceiver : public CReceiver {
protected:
   ...
   CVirtualInterface
   *m_interface;                          // Interface object to show the status to the user

   ...
};

//+------------------------------------------------------------------+
//| Closed constructor                                               |
//+------------------------------------------------------------------+
CVirtualReceiver::CVirtualReceiver() :
   m_interface(CVirtualInterface::Instance()),
   ... {}

No método de alocação do número necessário de posições virtuais para as estratégias, paralelamente as adicionaremos à interface:

//+------------------------------------------------------------------+
//| Allocate the necessary amount of virtual positions to strategy   |
//+------------------------------------------------------------------+
static void CVirtualReceiver::Get(CVirtualStrategy *strategy,   // Strategy
                                  CVirtualOrder *&orders[],     // Array of strategy positions
                                  int n                         // Required number
                                 ) {
   CVirtualReceiver *self = Instance();   // Receiver singleton
   CVirtualInterface *draw = CVirtualInterface::Instance();
   ArrayResize(orders, n);                // Expand the array of virtual positions
   FOREACH(orders,
           orders[i] = new CVirtualOrder(strategy); // Fill the array with new objects
           APPEND(self.m_orders, orders[i]);
           draw.Add(orders[i])) // Register the created virtual position
   PrintFormat(__FUNCTION__ + " | OK, Strategy orders: %d from %d total",
               ArraySize(orders),
               ArraySize(self.m_orders));
}

E por último, precisamos adicionar o alerta da interface nos métodos de processamento de abertura/fechamento de posições virtuais nesta classe:

void CVirtualReceiver::OnOpen(CVirtualOrder *p_order) {
   m_interface.Changed(p_order);
   ...
}

//+------------------------------------------------------------------+
//| Handle closing a virtual position                                |
//+------------------------------------------------------------------+
void CVirtualReceiver::OnClose(CVirtualOrder *p_order) {
   m_interface.Changed(p_order);
   ...
   }
}

Salvaremos as mudanças feitas nos arquivos VirtualAdvisor.mqh e VirtualReceiver.mqh na pasta atual.

Se agora compilarmos e executarmos nosso EA, com posições virtuais abertas ou ordens pendentes, poderemos ver algo assim:

Fig. 1. Exibição de ordens e posições virtuais no gráfico

Fig. 1. Exibição de ordens e posições virtuais no gráfico

Aqui, as linhas pontilhadas representam ordens pendentes virtuais, laranjas - SELL STOP, azuis - BUY STOP, e as linhas contínuas - posições virtuais, azul - BUY, vermelha - SELL. Até agora, apenas os níveis de abertura são visíveis, mas no futuro a exibição pode ser mais detalhada.


Gráfico bonito

Nas discussões do fórum, surgiu a ideia de que os leitores dos artigos (pelo menos alguns) primeiramente olham o final do artigo, onde esperam ver um gráfico bonito do crescimento da curva de capital ao testar o EA desenvolvido. E se houver um gráfico, isso se torna um incentivo adicional para voltar ao início do artigo e lê-lo.

Durante o trabalho neste artigo, não fizemos alterações na estratégia de demonstração usada e no conjunto de instâncias de estratégias no EA de demonstração. Tudo permaneceu no mesmo estado desde a publicação do artigo anterior. Portanto, não podemos mostrar novos resultados do EA em questão.

Mas, paralelamente, houve mais otimização e treinamento de outras estratégias, cuja publicação ainda não chegou, mas os resultados dos testes que elas mostram nos permitem dizer que a decepção com a abordagem de combinar muitas instâncias de estratégias em um EA ainda não chegou.

Aqui estão dois exemplos de testes do EA, que usa cerca de 170 instâncias de estratégias, operando em diferentes símbolos e períodos. Período de teste: de 01/01/2023 a 23/02/2024, os dados desse período não foram usados para otimização e treinamento. Nas configurações de gerenciamento de capital, os parâmetros foram definidos supondo uma queda aceitável de cerca de 10% em um caso e cerca de 40% no outro.


Fig. 2. Resultados do teste com uma queda aceitável de 10%


Fig. 3. Resultados do teste com uma queda aceitável de 40%

Claro, a existência desses resultados não garante sua repetição no futuro. Mas podemos trabalhar para aumentar a probabilidade de tal resultado. Vamos nos esforçar para não piorar esses resultados e não nos enganar com overfitting.


Considerações finais

Neste artigo, fizemos um trabalho que nos desvia um pouco da direção principal. Ainda assim, gostaríamos de avançar em direção à otimização automática, que, por exemplo, foi discutida no artigo recentemente publicado Usando algoritmos de otimização para configurar parâmetros de EA em tempo real.

No entanto, a capacidade de salvar e carregar o estado é uma parte importante de qualquer EA que tenha perspectivas de ser executado em uma conta real algum dia. Igualmente importante é a capacidade de trabalhar não apenas com posições de mercado, mas também com ordens pendentes, que são usadas em muitas estratégias de negociação. E alguma visualização do processo de trabalho do EA pode ajudar a identificar erros de implementação na fase de desenvolvimento.

Obrigado pelo interesse e até a próxima parte!

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

Trabalho com modelos ONNX nos formatos float16 e float8 Trabalho com modelos ONNX nos formatos float16 e float8
Os formatos de dados utilizados para representar modelos de aprendizado de máquina desempenham um papel fundamental em sua eficiência. Nos últimos anos, surgiram vários novos tipos de dados desenvolvidos especificamente para trabalhar com modelos de aprendizado profundo. Neste artigo, vamos focar em dois novos formatos de dados que se tornaram amplamente utilizados nos modelos modernos.
Classe base de algoritmos populacionais como alicerce para otimização eficiente Classe base de algoritmos populacionais como alicerce para otimização eficiente
Uma tentativa única de pesquisa para combinar uma série de algoritmos populacionais em uma única classe com o objetivo de simplificar a aplicação dos métodos de otimização. Essa abordagem não apenas abre possibilidades para o desenvolvimento de novos algoritmos, incluindo variantes híbridas, mas também estabelece um banco de testes básico universal. Este banco se torna uma ferramenta chave para a escolha do algoritmo ideal, dependendo da tarefa específica em questão.
Desenvolvendo um cliente MQTT para Metatrader 5: uma abordagem TDD — Parte 6 Desenvolvendo um cliente MQTT para Metatrader 5: uma abordagem TDD — Parte 6
Este artigo é a sexta parte de uma série que descreve nossas etapas de desenvolvimento de um cliente MQL5 nativo para o protocolo MQTT 5.0. Nesta parte, comentamos as principais mudanças em nosso primeiro refatoramento, como chegamos a um modelo viável para nossas classes de construção de pacotes, como estamos construindo pacotes PUBLISH e PUBACK, e a semântica por trás dos Códigos de Motivo PUBACK.
Desenvolvendo um sistema de Replay (Parte 58): Voltando a trabalhar no serviço Desenvolvendo um sistema de Replay (Parte 58): Voltando a trabalhar no serviço
Depois de ter dado um tempo no desenvolvimento e aperfeiçoamento do serviço usado no replay / simulação. Iremos voltar a trabalhar nele. Mas como já não iremos mais usar alguns recursos, como as variáveis globais de terminal, se torna necessário uma completa reestruturação de algumas partes do mesmo. Mas não fiquem aflitos, tal processo será adequadamente explicado, para que todos consigam de fato acompanhar o desenrolar do desenvolvimento do serviço.