English Русский Español Deutsch 日本語
preview
Desenvolvendo um EA multimoeda (Parte 2): Transição para posições virtuais de estratégias de trading

Desenvolvendo um EA multimoeda (Parte 2): Transição para posições virtuais de estratégias de trading

MetaTrader 5Negociação | 9 julho 2024, 15:57
135 0
Yuriy Bykov
Yuriy Bykov

Introdução

No artigo anterior, começamos a desenvolver o EA multimoeda, operando simultaneamente com diferentes estratégias de trading. Na primeira etapa, havia apenas duas estratégias diferentes. Elas representavam a implementação da mesma ideia de trading, operando no mesmo instrumento de trading (símbolo) e período de gráfico (timeframe). A única diferença entre elas eram os valores numéricos dos parâmetros.

Também determinamos o tamanho ideal das posições a serem abertas, com base no nível máximo desejado de rebaixamento (10% do depósito). Fizemos isso para cada estratégia separadamente. Quando combinamos as duas estratégias, para manter o nível de rebaixamento desejado, tivemos que reduzir o tamanho das posições abertas. Para duas estratégias, a redução foi pequena. Mas e se quisermos combinar dezenas ou centenas de estratégias? Pode acontecer que algumas estratégias precisem reduzir o tamanho das posições para um valor menor do que o mínimo permitido pelo corretor. Nesse caso, essas estratégias simplesmente não poderão participar do trading. Como fazer com que elas funcionem?

Para isso, vamos retirar das estratégias o direito de abrir posições e colocar ordens pendentes de forma independente. As estratégias devem negociar apenas virtualmente, ou seja, memorizar os níveis em que as posições de determinado tamanho devem ser abertas e, sob demanda, informar qual volume deve estar aberto no momento. Abriremos posições a mercado reais somente após consultar todas as estratégias e calcular o volume total necessário, levando em consideração a escala para manter o rebaixamento desejado.

No momento, estaremos interessados apenas em testar a viabilidade dessa abordagem, e não na eficiência de sua implementação. Portanto, neste artigo, tentaremos escrever uma implementação funcional dessa abordagem, que nos ajudará a construir uma mais elegante do ponto de vista arquitetônico no futuro. Isso nos ajudará não no sentido de melhorar a existente, mas de entender o que não devemos fazer e como podemos fazer de outra forma, de maneira melhor.

Vamos tentar implementar isso.


Revisando o que foi feito

Desenvolvemos a classe expert CAdvisor, que armazena um array de instâncias de estratégias de trading (mais precisamente, ponteiros para as instâncias). Isso permite criar uma instância do expert no programa principal e adicionar várias instâncias das classes de estratégias a ele. Além disso, como o array armazena ponteiros para objetos da classe base CStrategy, nela podem ser armazenados ponteiros para objetos de quaisquer classes derivadas, herdadas de CStrategy. No nosso caso, criamos uma classe derivada CSimpleVolumesStrategy, dois objetos dos quais foram adicionados a esse array no expert.

Vamos estabelecer nomes convenientes para facilitar a explicação:

  • EA — nosso arquivo final com a extensão mq5, que após a compilação gera um arquivo executável ex5, adequado para execução no testador e no terminal.
  • Expert — objeto da classe CAdvisor, declarado no programa. Usaremos apenas uma instância do expert em um único programa.
  • Estratégia — objeto da classe derivada, herdada da classe base de estratégias CStrategy

Também lembramos que um ponteiro para um objeto (estratégia ou qualquer outra classe) é uma informação sobre a localização na memória do objeto criado anteriormente (simplificadamente). Permite, ao passar para funções, atribuir a novas variáveis ou elementos de array, evitar a criação repetida do mesmo objeto em outro local da memória.

É por isso que no expert armazenamos no array ponteiros para os objetos das estratégias, para que ao preencher esse array, não sejam criadas cópias dos objetos das estratégias. Assim, ao acessar os elementos do array de estratégias, acessaremos os objetos originais das estratégias.

O trabalho do EA consistia nas seguintes etapas:

  • O expert era criado na área de memória estática.
  • Ao inicializar o programa, duas estratégias eram criadas na memória dinâmica e seus ponteiros eram armazenados no expert.
  • Durante a execução do programa, o expert chamava cada estratégia alternadamente para executar as ações de trading necessárias, chamando o método CStrategy::Tick().
  • Ao desinicializar o programa, o expert removia os objetos das estratégias da memória dinâmica.

Antes de prosseguir com a tarefa proposta, faremos algumas correções no EA e na classe expert. No EA, faremos com que o expert seja criado na área de memória dinâmica.

CAdvisor     expert;          // EA object
CAdvisor     *expert;         // Pointer to the EA object

int OnInit() {
   expert = new CAdvisor();   // Create EA object
   
   // The rest of the code from OnInit() ...
}

Na classe expert, criaremos um destrutor, ou seja, uma função que será chamada automaticamente ao excluir o objeto expert da memória. Transferiremos para o destrutor as operações de exclusão dos objetos das estratégias da memória dinâmica, que atualmente estão no método CAdvisor::Deinit(). Esse método não será mais necessário, então o removeremos. Também removeremos a variável de classe que armazena o número de estratégias m_strategiesCount. Nos locais onde ela é necessária, podemos usar ArraySize().

class CAdvisor {
protected:
   CStrategy         *m_strategies[];  // Array of trading strategies
public:
   ~CAdvisor();                        // Destructor

   // ...
};

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CAdvisor::~CAdvisor() {
// Delete all strategy objects
   for(int i = 0; i < ArraySize(m_strategies); i++) {
      delete m_strategies[i];
   }
}

Agora, substituiremos na função OnDeinit() a chamada do método CAdvisor::Deinit pelo comando de exclusão do objeto expert.

void OnDeinit(const int reason) {
   expert.Deinit();
   delete expert;
}


Vamos traçar o caminho

Se as estratégias de trading agora não puderem abrir posições a mercado por conta própria, então 

  • são necessários objetos que armazenem informações sobre as posições virtuais das estratégias;
  • são necessários objetos que traduzam as informações das posições virtuais em posições reais de mercado.

Os objetos para posições virtuais devem ser parte integrante da estratégia e deve haver vários deles. Portanto, chamaremos a primeira nova classe de CVirtualOrder, e adicionaremos na classe estratégia CStrategy um array desses objetos. Também adicionaremos na CStrategy uma propriedade que armazene o indicador de mudanças na composição das posições virtuais abertas e métodos para obter e definir seu valor. Esse indicador determinará de fato em qual dos dois estados a estratégia se encontra:

  • não há mudanças — todo o volume aberto virtual foi transferido para o mercado;
  • há mudanças — o volume virtual não corresponde ao de mercado, portanto, é necessário ajustar os volumes das posições reais de mercado.
Por enquanto, parece que esses dois estados serão suficientes, então limitaremos o modelo a isso.

Como agora outra entidade será responsável pela abertura de posições reais, podemos remover a propriedade do número mágico m_magic da classe base da estratégia. No futuro, vamos limpar ainda mais a classe base da estratégia, mas, por enquanto, faremos apenas uma limpeza parcial.

Considerando o que foi dito, a classe base da estratégia ficará assim.

#include "VirtualOrder.mqh"

//+------------------------------------------------------------------+
//| Base class of the trading strategy                               |
//+------------------------------------------------------------------+
class CStrategy {
protected:
   string            m_symbol;         // Symbol (trading instrument)
   ENUM_TIMEFRAMES   m_timeframe;      // Chart period (timeframe)
   double            m_fixedLot;       // Size of opened positions (fixed)

   CVirtualOrder     m_orders[];       // Array of virtual positions (orders)
   int               m_ordersTotal;    // Total number of open positions and orders
   double            m_volumeTotal;    // Total volume of open positions and orders

   bool              m_isChanged;      // Sign of changes in open virtual positions
   void              CountOrders();    // Calculate the number and volumes of open positions and orders

public:
   // Constructor
   CStrategy(string p_symbol = "",
             ENUM_TIMEFRAMES p_timeframe = PERIOD_CURRENT,
             double p_fixedLot = 0.01);

   virtual void      Tick();           // Main method - handling OnTick events
   virtual double    Volume();         // Total volume of virtual positions
   virtual string    Symbol();         // Strategy symbol (only one for a single strategy so far)
   virtual bool      IsChanged();      // Are there any changes in open virtual positions?
   virtual void      ResetChanges();   // Reset the sign of changes in open virtual positions
};

Os métodos Symbol(), IsChanged() e ResetChanges() já podem ser implementados.

//+------------------------------------------------------------------+
//| Strategy symbol                                                  |
//+------------------------------------------------------------------+
string CStrategy::Symbol() {
   return m_symbol;
}

//+------------------------------------------------------------------+
//| Are there any changes to open virtual positions?                 |
//+------------------------------------------------------------------+
bool CStrategy::IsChanged() {
   return m_isChanged;
}

//+------------------------------------------------------------------+
//| Reset the flag for changes in virtual positions                  |
//+------------------------------------------------------------------+
void CStrategy::ResetChanges() {
   m_isChanged = false;
}

Os outros métodos — Tick(), Volume() e CountOrders() — serão implementados ou nos descendentes da classe base ou na própria classe base, mas posteriormente.

A segunda nova classe, cujos objetos serão responsáveis por converter as posições virtuais das estratégias em posições reais no mercado, será chamada de CReceiver. Para sua operação, este objeto precisa ter acesso a todas as estratégias do EA, para poder saber em que símbolos e com que volume precisam ser abertas as posições reais. Para um EA, será suficiente um único objeto deste tipo. O objeto CReceiver deve ter um número mágico, que será atribuído às posições a mercado abertas.

#include "Strategy.mqh"

//+------------------------------------------------------------------+
//| Base class for converting open volumes into market positions     |
//+------------------------------------------------------------------+
class CReceiver {
protected:
   CStrategy         *m_strategies[];  // Array of strategies
   ulong             m_magic;          // Magic

public:
   CReceiver(ulong p_magic = 0);                // Constructor
   virtual void      Add(CStrategy *strategy);  // Adding strategy
   virtual bool      Correct();                 // Adjustment of open volumes
};

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CReceiver::CReceiver(ulong p_magic) : m_magic(p_magic) {
   ArrayResize(m_strategies, 0, 128);
}

//+------------------------------------------------------------------+
//| Add strategy                                                     |
//+------------------------------------------------------------------+
void CReceiver::Add(CStrategy *strategy) {
   APPEND(m_strategies, strategy);
}

//+------------------------------------------------------------------+
//| Adjust open volumes                                              |
//+------------------------------------------------------------------+
bool CReceiver::Correct() {
   return true;
}

Esta classe base não contém a implementação do mecanismo específico de ajuste de volumes. Portanto, em diferentes descendentes desta classe, poderemos fazer várias implementações de ajuste. E um objeto dessa classe pode servir como um substituto para aquelas estratégias que ainda abrirão posições a mercado por conta própria. Isso será necessário para depurar o mecanismo de ajuste: será preciso comparar quais posições são abertas pelo EA em que as estratégias realizam a negociação real e quais são abertas pelo EA em que as estratégias realizam apenas negociação virtual.

Portanto, prepararemos dois EAs, que ainda operam no mercado real com base nas estratégias do artigo anterior.

No primeiro EA, haverá uma única instância da estratégia, e em seus parâmetros poderemos especificar os parâmetros dessa única instância da estratégia para otimização.

No segundo EA, haverá várias instâncias de estratégias de trading com parâmetros predeterminados, obtidos como resultado da otimização do primeiro EA.


EA para otimização dos parâmetros da estratégia

Na última vez, realizamos a otimização dos parâmetros da estratégia, usando a implementação da estratégia não como um objeto da classe CStrategy. Mas agora já temos a classe CSimpleVolumesStrategy pronta, então vamos criar um programa separado, no qual o expert conterá uma única instância dessa estratégia. Só vamos nomear essa classe um pouco diferente, para destacar no nome que a estratégia abrirá posições a mercado por conta própria: em vez de CSimpleVolumesStrategy, usaremos CSimpleVolumesMarketStrategy e a salvaremos no arquivo SimpleVolumesMarketStrategy.mqh na pasta atual.

No arquivo do EA, os parâmetros da estratégia serão carregados a partir de variáveis de input do EA e adicionaremos uma instância da estratégia ao objeto expert. Obtivemos um EA com o qual podemos otimizar os parâmetros da estratégia. 

#include "Advisor.mqh"
#include "SimpleVolumesMarketStrategy.mqh"
#include "VolumeReceiver.mqh"

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
input string      symbol_              = "EURGBP";    // Trading instrument (symbol)
input ENUM_TIMEFRAMES
timeframe_           = PERIOD_H1;   // Chart period

input group "===  Opening signal parameters"
input int         signalPeriod_        = 130;   // Number of candles for volume averaging
input double      signalDeviation_     = 0.9;   // Relative deviation from the average to open the first order 
input double      signaAddlDeviation_  = 1.4;   // Relative deviation from the average for opening the second and subsequent orders

input group "===  Pending order parameters"
input int         openDistance_        = 0;     // Distance from price to pending order
input double      stopLevel_           = 2000;  // Stop Loss (in points)
input double      takeLevel_           = 475;   // Take Profit (in points)
input int         ordersExpiration_    = 6000;  // Pending order expiration time (in minutes)

input group "===  Money management parameters"
input int         maxCountOfOrders_    = 3;     // Maximum number of simultaneously open orders
input double      fixedLot_            = 0.01;  // Single order volume

input group "===  EA parameters"
input ulong       magic_              = 27181; // Magic

CAdvisor     *expert;         // Pointer to the EA object

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   expert = new CAdvisor();
   expert.Add(new CSimpleVolumesMarketStrategy(
                 magic_, symbol_, timeframe_,
                 fixedLot_,
                 signalPeriod_, signalDeviation_, signaAddlDeviation_,
                 openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
                 maxCountOfOrders_)
             );       // Add one strategy instance

   return(INIT_SUCCEEDED);
}

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

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

Vamos salvá-lo no arquivo SimpleVolumesMarketExpertSingle.mq5 na pasta atual.

Agora, vamos complicar um pouco a estratégia de trading para simplificar a realização da tarefa proposta. Será mais fácil migrar para trading virtual uma estratégia que utiliza posições a mercado, em vez de ordens pendentes. A versão atual da estratégia trabalha apenas com ordens pendentes. Vamos adicionar à estratégia a análise do valor do parâmetro openDistance_. Se ele for maior que zero, a estratégia abrirá ordens pendentes BUY_STOP e SELL_STOP. Se for menor que zero, a estratégia abrirá ordens pendentes BUY_LIMIT e SELL_LIMIT. Se for igual a zero, abrirá posições a mercado. 

Para isso, basta fazer alterações no código dos métodos CSimpleVolumesMarketStrategy::OpenBuyOrder() e CSimpleVolumesMarketStrategy::OpenSellOrder().

void CSimpleVolumesMarketStrategy::OpenBuyOrder() {
// Previous code in the method ...

// Order volume
   double lot = m_fixedLot;

// Set a pending order
   bool res = false;
   if(openDistance_ > 0) {
      res = trade.BuyStop(lot, ...);
   } else if(openDistance_ < 0) {
      res = trade.BuyLimit(lot, ...);
   } else {
      res = trade.Buy(lot, ...);
   }

   if(!res) {
      Print("Error opening order");
   }
}

Outra alteração necessária na estratégia é transferir o código de inicialização do método Init() para o construtor da estratégia. Isso é necessário porque agora o EA não chamará o método de inicialização das estratégias, presumindo que seu código está dentro do construtor das estratégias.

Vamos compilar o novo EA e colocá-lo para otimizar no timeframe H1 em três símbolos: EURGBP, GBPUSD e EURUSD.

Figura 1. Resultados do teste com os parâmetros [EURGBP, ]]

Figura. 1. Resultados do teste para [EURGBP, H1, 17, 1.7, 0.5, 0, 16500, 100, 52000, 3, 0.01]

Escolheremos alguns bons parâmetros dos resultados da otimização, por exemplo, três variantes para cada símbolo, e criaremos um segundo EA, no qual serão criadas nove instâncias da estratégia com os parâmetros escolhidos. Para cada instância, calcularemos o tamanho ótimo das posições abertas, onde a retração de uma estratégia não exceda 10%. O método de cálculo foi descrito no artigo anterior.

Para demonstrar a alteração dos indicadores de desempenho do EA, faremos possível definir quais estratégias serão ativadas. Para isso, colocaremos primeiro todas as instâncias de estratégias em um array de nove elementos. Adicionaremos o parâmetro de entrada startIndex_, que determinará a partir de qual índice no array de estratégias começaremos a ativá-las. Outro parâmetro totalStrategies_ determinará quantas estratégias consecutivas do array, a partir do índice startIndex_, serão ativadas. No final da inicialização, adicionamos as estratégias correspondentes do array ao objeto expert.

#include "Advisor.mqh"
#include "SimpleVolumesMarketStrategy.mqh"

input int startIndex_      = 0;        // Starting index
input int totalStrategies_ = 1;        // Number of strategies
input double depoPart_     = 1.0;      // Part of the deposit for one strategy
input ulong  magic_        = 27182;    // Magic

CAdvisor     *expert;                  // EA object

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Check if the parameters are correct
   if(startIndex_ < 0 || startIndex_ + totalStrategies_ > 9) {
      return INIT_PARAMETERS_INCORRECT;
   }

// Create and fill the array of strategy instances
   CStrategy *strategies[9];
   strategies[0] = new CSimpleVolumesMarketStrategy(
      magic_ + 0, "EURGBP", PERIOD_H1,
      NormalizeDouble(0.01 / 0.16 * depoPart_, 2),
      13, 0.3, 1.0, 0, 10500, 465, 1000, 3);
   strategies[1] = new CSimpleVolumesMarketStrategy(
      magic_ + 1, "EURGBP", PERIOD_H1,
      NormalizeDouble(0.01 / 0.09 * depoPart_, 2),
      17, 1.7, 0.5, 0, 16500, 220, 1000, 3);
   strategies[2] = new CSimpleVolumesMarketStrategy(
      magic_ + 2, "EURGBP", PERIOD_H1,
      NormalizeDouble(0.01 / 0.16 * depoPart_, 2),
      51, 0.5, 1.1, 0, 19500, 370, 22000, 3);
   strategies[3] = new CSimpleVolumesMarketStrategy(
      magic_ + 3, "GBPUSD", PERIOD_H1,
      NormalizeDouble(0.01 / 0.25 * depoPart_, 2),
      80, 1.1, 0.2, 0, 6000, 1190, 1000, 3);
   strategies[4] = new CSimpleVolumesMarketStrategy(
      magic_ + 4, "GBPUSD", PERIOD_H1,
      NormalizeDouble(0.01 / 0.09 * depoPart_, 2),
      128, 2.0, 0.9, 0, 2000, 1170, 1000, 3);
   strategies[5] = new CSimpleVolumesMarketStrategy(
      magic_ + 5, "GBPUSD", PERIOD_H1,
      NormalizeDouble(0.01 / 0.14 * depoPart_, 2),
      13, 1.5, 0.8, 0, 2500, 1375, 1000, 3);
   strategies[6] = new CSimpleVolumesMarketStrategy(
      magic_ + 6, "EURUSD", PERIOD_H1,
      NormalizeDouble(0.01 / 0.23 * depoPart_, 2),
      24, 0.1, 0.3, 0, 7500, 2400, 24000, 3);
   strategies[7] = new CSimpleVolumesMarketStrategy(
      magic_ + 7, "EURUSD", PERIOD_H1,
      NormalizeDouble(0.01 / 0.20 * depoPart_, 2),
      18, 0.2, 0.4, 0, 19500, 1480, 6000, 3);
   strategies[8] = new CSimpleVolumesMarketStrategy(
      magic_ + 8, "EURUSD", PERIOD_H1,
      NormalizeDouble(0.01 / 0.22 * depoPart_, 2),
      128, 0.7, 0.3, 0, 3000, 170, 42000, 3);

   expert = new CAdvisor();

// Add the necessary strategies to the EA
   for(int i = startIndex_; i < startIndex_ + totalStrategies_; i++) {
      expert.Add(strategies[i]);
   }

   return(INIT_SUCCEEDED);
}

void OnTick() {
   expert.Tick();
}

void OnDeinit(const int reason) {
   delete expert;
}

Com isso, podemos usar a otimização pelo parâmetro de índice inicial das estratégias no array de estratégias para obter resultados para cada instância de estratégia. Executaremos isso com um depósito inicial de $100,000 e obteremos os seguintes resultados.


Figura. 2. Resultados das execuções únicas de nove instâncias da estratégia

Desses resultados, é possível ver que a retração é de cerca de 1% do depósito inicial, ou seja, aproximadamente $1,000, conforme planejado, ao determinar o tamanho ótimo das posições abertas. O índice de Sharpe é em média 1,3.

Agora, vamos ativar todas as instâncias e ajustar o multiplicador depoPart_ para manter a retração em $1,000. Descobrimos que com depoPart_ = 0.38, a retração permanece dentro do limite aceitável.


Figura. 3. Resultados do teste com a operação simultânea de nove instâncias de estratégias

Comparando os resultados de instâncias únicas das estratégias e os resultados da operação simultânea de todas as instâncias, pode-se notar que, com a mesma retração, obtivemos um aumento no lucro de aproximadamente 3 vezes, além de um aumento no índice de Sharpe de 1,3 para 2,84.

Agora vamos começar a implementar a tarefa principal.


Classe de posições (ordens) virtuais

Vamos criar a classe prometida CVirtualOrder e adicionar campos para armazenar todas as propriedades das posições abertas.

class CVirtualOrder {
private:
//--- Order (position) properties
   ulong             m_id;          // Unique ID 

   string            m_symbol;      // Symbol
   double            m_lot;         // Volume
   ENUM_ORDER_TYPE   m_type;        // Type
   double            m_openPrice;   // Open price
   double            m_stopLoss;    // StopLoss level
   double            m_takeProfit;  // TakeProfit level
   string            m_comment;     // Comment

   datetime          m_openTime;    // Open time

//--- Closed order (position) properties
   double            m_closePrice;  // Close price
   datetime          m_closeTime;   // Close time
   string            m_closeReason; // Closure reason

   double            m_point;       // Point value

   bool              m_isStopLoss;  // StopLoss activation property
   bool              m_isTakeProfit;// TakeProfit activation property
};

Cada posição virtual deve ter um identificador único. Portanto, adicionaremos uma variável estática da classe s_count para contar o número de todos os objetos de posição criados no programa. Ao criar um novo objeto de posição, esse contador aumenta em 1 e esse valor se torna o número único da posição. O valor inicial de s_count será 0.

Também precisaremos de um objeto da classe CSymbolInfo para obter informações sobre preços. Faremos dele também um membro estático da classe. 

class CVirtualOrder {
private:
   static int        s_count;
   static
   CSymbolInfo       s_symbolInfo;

//--- Order (position) properties ...

};

int               CVirtualOrder::s_count = 0;
CSymbolInfo       CVirtualOrder::s_symbolInfo;

Vale destacar que a criação do objeto de posição virtual e a "abertura" da posição virtual serão operações diferentes. O objeto de posição pode ser criado antecipadamente e aguardar o momento em que a estratégia desejar abrir a posição virtual. Nesse momento, as propriedades da posição serão preenchidas com valores atualizados do símbolo, volume, preço de abertura e outros. Quando a estratégia decidir fechar a posição, os valores das propriedades de fechamento serão salvos no objeto: preço, tempo e motivo do fechamento. na função OnDeinit() Na próxima operação de abertura de posição virtual, podemos usar essa mesma instância do objeto, limpando suas propriedades de fechamento e preenchendo-as novamente com novos valores de símbolo, volume, preço de abertura e outros.

Adicionaremos métodos a essa classe. Precisaremos de métodos públicos para abrir e fechar a posição virtual e de um construtor. Também serão úteis métodos para verificar o estado da posição (se está aberta, em qual direção) e suas propriedades mais importantes — volume e lucro atual.

class CVirtualOrder {
//--- Previous code...

public:
                     CVirtualOrder();  // Constructor
                     
//--- Methods for checking the order (position) status
   bool              IsOpen();         // Is the order open?
   bool              IsMarketOrder();  // Is this a market position?
   bool              IsBuyOrder();     // Is this an open BUY position?
   bool              IsSellOrder();    // Is this an open SELL position?
  
//--- Methods for obtaining order (position) properties
   double            Volume();         // Volume with direction 
   double            Profit();         // Current profit 

//--- Methods for handling orders (positions)
   bool              Open(string symbol,
                          ENUM_ORDER_TYPE type,
                          double lot,
                          double sl = 0,
                          double tp = 0,
                          string comment = "",
                          bool inPoints = true);   // Opening an order (position)
   bool              Close();                      // Closing an order (position)
};

A implementação de alguns desses métodos é muito simples e curta, portanto, pode ser incluída diretamente na descrição da classe, por exemplo:

class CVirtualOrder : public CObject {
// ...

//--- Methods for checking the order (position) status
   bool              IsOpen() {        // Is the order open?
      return(this.m_openTime > 0 && this.m_closeTime == 0);
   };
   bool              IsMarketOrder() { // Is this a market position?
      return IsOpen() && (m_type == ORDER_TYPE_BUY || m_type == ORDER_TYPE_SELL);
   }
   
// ...
};

O construtor, através da lista de inicialização, atribuirá valores vazios (no sentido de deliberadamente inaceitáveis) a todas as propriedades da posição virtual, exceto uma — o identificador único. Esse identificador, como mencionado anteriormente, será atribuído a partir do valor do contador de objetos criados anteriormente desta classe e permanecerá assim durante toda a operação do EA. Antes de atribuir, aumentaremos o contador de objetos criados.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualOrder::CVirtualOrder() :
// Initialization list
   m_id(++s_count),  // New ID = object counter + 1
   m_symbol(""),
   m_lot(0),
   m_type(-1),
   m_openPrice(0),
   m_stopLoss(0),
   m_takeProfit(0),
   m_openTime(0),
   m_comment(""),
   m_closePrice(0),
   m_closeTime(0),
   m_closeReason(""),
   m_point(0) {
}

Antes de continuar com a implementação dos métodos da classe CVirtualOrder, vamos olhar um pouco à frente e pensar como iremos utilizá-la. Temos objetos de estratégias que atualmente abrem posições reais de mercado. Além disso, o número máximo de posições a mercado abertas é conhecido (definido nos parâmetros das estratégias). Queremos migrar para posições virtuais. Então, seu número também será limitado. Portanto, podemos criar um array de objetos de posições virtuais na estratégia, preenchê-lo na inicialização da estratégia com o número necessário de posições virtuais e trabalhar apenas com esse array.

Quando surgirem condições para abrir uma nova posição, pegaremos uma posição virtual ainda não aberta do array e a transformaremos em aberta. Quando surgirem condições para fechamento forçado de posições, transformaremos em fechada.

Enquanto houver posições virtuais abertas, qualquer estratégia deve, a cada tick, processar esses objetos da mesma maneira: iterando por todos em sequência, verificando se o nível de StopLoss ou TakeProfit foi alcançado e, se sim, fechando essa posição. Essa uniformidade permite transferir a implementação do processamento de posições virtuais abertas para sua própria classe, e a estratégia apenas chamará o método correspondente.

Adicionaremos à classe CVirtualOrder o método Tick(), que verificará as condições de fechamento da posição e, se cumpridas, a posição será convertida para o estado fechado. Se houve alteração no estado da posição, o método retornará true.

Também adicionaremos um método estático Tick(), que processa vários objetos de posições virtuais ao mesmo tempo. Ele aceitará como parâmetro uma referência ao array desses objetos. Para cada objeto desse array, seu método Tick() será chamado. Se pelo menos uma posição virtual for fechada, o método retornará true.

class CVirtualOrder {
private:
   //...
public:
   //...
   
//--- Methods for handling orders (positions)
   bool              Open(string symbol,
                          ENUM_ORDER_TYPE type,
                          double lot,
                          double sl = 0,
                          double tp = 0,
                          string comment = "",
                          bool inPoints = false
                         );      // Open order (position)

   bool              Tick();     // Handle tick for an order (position)
   bool              Close();    // Close an order (position)

   static bool       Tick(CVirtualOrder &orders[]);   // Handle a tick for the array of virtual orders
};               

//...

//+------------------------------------------------------------------+
//| Handle a tick of a single virtual order (position)               |
//+------------------------------------------------------------------+
bool CVirtualOrder::Tick() {
   if(IsMarketOrder()) {  // If this is a market virtual position
      if(CheckClose()) {  // Check if SL or TP levels have been reached
         Close();         // Close when reached
         return true;     // Return the fact that there are changes in open positions
      }
   }

   return false;
}

//+------------------------------------------------------------------+
//| Handle a tick for the array of virtual orders (positions)        |
//+------------------------------------------------------------------+
bool CVirtualOrder::Tick(CVirtualOrder &orders[]) {
   bool isChanged = false;                      // We assume that there will be no changes
   for(int i = 0; i < ArraySize(orders); i++) { // For all orders (positions)
      isChanged |= orders[i].Tick();            // Check and close if necessary
   }
   return isChanged;
}
//+------------------------------------------------------------------+

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


Aprimorando a classe de estratégia de trading simples

Agora podemos voltar à classe de estratégia de trading e fazer as alterações necessárias para trabalhar com posições virtuais. Como já acordamos, na classe base CStrategy já temos um array m_orders[] para armazenar objetos de posições virtuais. Portanto, na classe CSimpleVolumesStrategy ele também estará disponível. A estratégia em questão tem um parâmetro m_maxCountOfOrders, que define o número máximo de posições abertas simultaneamente. Então, definiremos no construtor o tamanho do array de posições virtuais igual a esse parâmetro.

Depois, precisamos apenas substituir a abertura de posições reais nos métodos OpenBuyOrder() e OpenSellOrder() pela abertura de posições virtuais. A abertura de ordens pendentes reais, por enquanto, não pode ser substituída, então simplesmente comentaremos essas operações.

//+------------------------------------------------------------------+
//| Open BUY order                                                   |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::OpenBuyOrder() {
// ...
   
   if(m_openDistance > 0) {
      /* // Set BUY STOP pending order
         res = trade.BuyStop(lot, ...);  */
   } else if(m_openDistance < 0) {
      /* // Set BUY LIMIT pending order
         res = trade.BuyLimit(lot, ...); */
   } else {
      // Open a virtual BUY position
      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
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot,
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits));
            break;                                    // and exit
         }
      }
   } 

   ... 
}

//+------------------------------------------------------------------+
//| Open SELL order                                                  |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::OpenSellOrder() {
// ...
   
   if(m_openDistance > 0) {
      /* // Set SELL STOP pending order
      res = trade.SellStop(lot, ...);          */
   } else if(m_openDistance < 0) {
      /* // Set SELL LIMIT pending order
      res = trade.SellLimit(lot, ...);         */
   } else {
      // Open a virtual SELL position
      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
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_SELL, m_fixedLot,
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits));
            break;                                    // and exit
         }
      }
   }

   ... 
}

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


Criando a classe de conversão de volumes abertos em posições a mercado

Já criamos a classe base para objetos de conversão de volumes abertos em posições a mercado, que por enquanto não faz nada além de preencher o array de estratégias usadas. Agora precisamos escrever uma classe derivada que contenha a implementação específica das operações de saída de posições para o mercado. Vamos criar a classe CVolumeReceiver. Precisaremos adicionar bastante código para implementar o método Correct() na classe CVolumeReceiver. Vamos dividi-lo em vários métodos protegidos da classe.

#include "Receiver.mqh"

//+------------------------------------------------------------------+
//| Class for converting open volumes into market positions          |
//+------------------------------------------------------------------+
class CVolumeReceiver : public CReceiver {
protected:
   bool              m_isNetting;      // Is this a netting account?
   string            m_symbols[];      // Array of used symbols

   double            m_minMargin;      // Minimum margin for opening

   CPositionInfo     m_position;
   CSymbolInfo       m_symbolInfo;
   CTrade            m_trade;

   // Filling the array of open market volumes by symbols 
   void              FillSymbolVolumes(double &oldVolumes[]);

   // Correction of open volumes using the array of volumes 
   virtual bool      Correct(double &symbolVolumes[]);

   // Volume correction for this symbol
   bool              CorrectPosition(string symbol, double oldVolume, double diffVolume);

   // Auxiliary methods
   bool              ClearOpen(string symbol, double diffVolume);
   bool              AddBuy(string symbol, double volume);
   bool              AddSell(string symbol, double volume);

   bool              CloseBuyPartial(string symbol, double volume);
   bool              CloseSellPartial(string symbol, double volume);
   bool              CloseHedgingPartial(string symbol, double volume, ENUM_POSITION_TYPE type);
   bool              CloseFull(string symbol = "");

   bool              FreeMarginCheck(string symbol, double volume, ENUM_ORDER_TYPE type);

public:
   CVolumeReceiver(ulong p_magic, double p_minMargin = 100);   // Constructor
   virtual void      Add(CStrategy *strategy) override;        // Add strategy
   virtual bool      Correct() override;                       // Adjustment of open volumes
};

O algoritmo geral do método de correção de volumes abertos é o seguinte:

  • Para cada símbolo usado, iteramos por todas as estratégias e calculamos o volume total aberto por símbolo usado. No final, obtemos um array newVolumes, que passamos para o próximo método sobrecarregado Correct().
    //+------------------------------------------------------------------+
    //| Adjustment of open volumes                                       |
    //+------------------------------------------------------------------+
    bool CVolumeReceiver::Correct() {
       int symbolsTotal = ArraySize(m_symbols);
       double newVolumes[];
    
       ArrayResize(newVolumes, symbolsTotal);
       ArrayInitialize(newVolumes, 0);
    
       for(int j = 0; j < symbolsTotal; j++) {  // For each used symbol        
          for(int i = 0; i < ArraySize(m_strategies); i++) { // Iterate through all strategies
             if(m_strategies[i].Symbol() == m_symbols[j]) {  // If the strategy uses this symbol
                newVolumes[j] += m_strategies[i].Volume();   // Add its open volume
             }
          }
       }
       // Call correction of open volumes using the array of volumes
       return Correct(newVolumes);
    }
    
  • Para cada símbolo, determinamos quanto o volume das posições abertas deve ser alterado. Se necessário, chamamos o método de correção de volume para o símbolo em questão.
    //+------------------------------------------------------------------+
    //| Adjusting open volumes using the array of volumes                |
    //+------------------------------------------------------------------+
    bool CVolumeReceiver::Correct(double &newVolumes[]) {
       // ...
       bool res = true;
    
       // For each symbol
       for(int j = 0; j < ArraySize(m_symbols); j++) {
          // ...
          // Define how much the volume of open positions for the symbol should be changed
          double oldVolume = oldVolumes[j];
          double newVolume = newVolumes[j];
          
          // ...
          double diffVolume = newVolume - oldVolume;
          
          // If there is a need to adjust the volume for a given symbol, then do that
          if(MathAbs(diffVolume) > 0.001) {
             res = res && CorrectPosition(m_symbols[j], oldVolume, diffVolume);
          }
       }
    
       return res;
    }
    
  • Para um símbolo, com base nos valores do volume aberto anterior e na alteração necessária, determinamos que tipo de operação de trading precisamos realizar (adicionar, fechar, fechar e reabrir) e chamamos o método auxiliar correspondente.
    //+------------------------------------------------------------------+
    //| Adjust volume by the symbol                                      |
    //+------------------------------------------------------------------+
    bool CVolumeReceiver::CorrectPosition(string symbol, double oldVolume, double diffVolume) {
       bool res = false;
    
       // ...
    
       double volume = MathAbs(diffVolume);
    
       if(oldVolume > 0) { // Have BUY position
          if(diffVolume > 0) { // New BUY position
             res = AddBuy(symbol, volume);
          } else if(diffVolume < 0) { // New SELL position
             if(volume < oldVolume) {
                res = CloseBuyPartial(symbol, volume);
             } else {
                res = CloseFull(symbol);
    
                if(res && volume > oldVolume) {
                   res = AddSell(symbol, volume - oldVolume);
                }
             }
          }
       } else if(oldVolume < 0) { // Have SELL position
          if(diffVolume < 0) { // New SELL position
             res = AddSell(symbol, volume);
          } else if(diffVolume > 0) { // New BUY position
             if(volume < -oldVolume) {
                res = CloseSellPartial(symbol, volume);
             } else {
                res = CloseFull(symbol);
    
                if(res && volume > -oldVolume) {
                   res = AddBuy(symbol, volume + oldVolume);
                }
             }
          }
       } else { // No old position
          res = ClearOpen(symbol, diffVolume);
       }
    
       return res;
    }
    

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


EA com uma estratégia e posições virtuais

Com base no arquivo SimpleVolumesMarketExpertSingle.mq5, criaremos um EA que usará uma instância da estratégia de trading com posições virtuais. Precisaremos incluir os arquivos necessários, passar um novo objeto da classe CVolumeReceiver ao chamar o construtor do expert e substituir a classe da estratégia criada.

#include "Advisor.mqh"
#include "SimpleVolumesStrategy.mqh"
#include "VolumeReceiver.mqh"

// Input parameters...

CAdvisor     *expert;         // Pointer to the EA object

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   expert = new CAdvisor(new CVolumeReceiver(magic_));
   expert.Add(new CSimpleVolumesStrategy(
                         symbol_, timeframe_,
                         fixedLot_,
                         signalPeriod_, signalDeviation_, signaAddlDeviation_,
                         openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
                         maxCountOfOrders_)
                     );       // Add one strategy instance

   return(INIT_SUCCEEDED);
}

void OnTick() {
   expert.Tick();
}

void OnDeinit(const int reason) {
   delete expert;
}

Salvaremos esse código no arquivo SimpleVolumesExpertSingle.mq5 na pasta atual.


Comparação entre trading real e virtual

Executaremos em um pequeno intervalo de tempo EAs com uma estratégia, usando os mesmos parâmetros da estratégia, mas diferentes métodos de abertura de posições reais — direto e através de posições virtuais. Salvaremos esses resultados em relatórios e analisaremos a lista de negociações realizadas por ambos os EAs.

Figura. 4. Negociações realizadas por dois EAs (sem posições virtuais e com elas)

Para reduzir a largura, removemos das tabelas colunas com valores iguais em todas as linhas, como símbolo (sempre EURGBP), volume (sempre 0.01) e outros. Como podemos ver, a abertura das primeiras posições ocorre nos dois casos pelo mesmo preço e nos mesmos momentos. Mas se, com uma posição SELL aberta (2018.03.02 15:46:47 sell in), outra posição oposta BUY é aberta (2018.03.06 13:56:04 buy in), o EA trabalhando com posições virtuais simplesmente fecha a posição SELL aberta anteriormente (2018.03.06 13:56:04 buy out). O resultado geral só melhorou, já que o primeiro EA continuou pagando swaps para posições opostas abertas, enquanto o segundo não.

EA com várias estratégias e posições virtuais

Faremos manipulações semelhantes com o EA do arquivo SimpleVolumesMarketExpert.mq5 — incluiremos os arquivos necessários, passaremos um novo objeto da classe CVolumeReceiver ao chamar o construtor do expert e substituiremos a classe das estratégias criadas. Salvaremos o resultado no arquivo SimpleVolumesExpert.mq5 e analisaremos seus resultados.

Figura. 5. Resultados do EA com nove instâncias da estratégia e posições virtuais

Comparando esses resultados com os de um EA similar, mas que não usa posições virtuais, podemos notar a melhoria em alguns indicadores: o lucro aumentou ligeiramente e a retração diminuiu, o índice de Sharpe e o fator de lucro aumentaram.


Considerações finais

Demos mais um passo em direção ao nosso objetivo. Ao migrar para o uso de posições virtuais relativamente às estratégias, aumentamos a capacidade de várias estratégias de trading operarem juntas sem interferir umas nas outras. Isso também permitirá utilizar um depósito mínimo menor para trading em comparação com o uso de trading independente para cada instância da estratégia. E, como um bônus, teremos a possibilidade de trabalhar em contas de Netting.

No entanto, ainda há muitos passos a serem dados. Por exemplo, atualmente, apenas estratégias que abrem posições a mercado foram implementadas, mas não ordens pendentes. Questões relacionadas à gestão de capital foram deixadas para o futuro, pois atualmente estamos negociando com volume fixo e ajustando manualmente o tamanho ideal das posições. Estratégias que precisam operar com vários símbolos simultaneamente, ou seja, aquelas que não podem ser divididas em estratégias mais simples de um único símbolo, não poderão usar esse esquema de operação.

Em suma, o trabalho continua.


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

Redes neurais de maneira fácil (Parte 76): explorando diversos modos de interação (Multi-future Transformer) Redes neurais de maneira fácil (Parte 76): explorando diversos modos de interação (Multi-future Transformer)
Neste artigo, continuamos o tema de previsão do movimento de preços. E convido você a conhecer a arquitetura do Multi-future Transformer. A ideia principal é decompor a distribuição multimodal do futuro em várias distribuições unimodais, permitindo modelar eficientemente diversos modos de interação entre os agentes na cena.
Redes neurais de maneira fácil (Parte 75): aumentando a produtividade dos modelos de previsão de trajetórias Redes neurais de maneira fácil (Parte 75): aumentando a produtividade dos modelos de previsão de trajetórias
Os modelos que estamos criando estão se tornando cada vez maiores e mais complexos. Com isso, aumentam os custos não apenas para o treinamento, mas também para a operação. Além disso, muitas vezes nos deparamos com situações em que o tempo de tomada de decisão é crítico. E, por isso, voltamos nossa atenção para métodos de otimização de desempenho dos modelos sem perder qualidade.
Desenvolvendo um EA multimoeda (Parte 3): Revisão da arquitetura Desenvolvendo um EA multimoeda (Parte 3): Revisão da arquitetura
Nós já avançamos um pouco no desenvolvimento de um EA multimoeda com várias estratégias funcionando em paralelo. Com base na experiência acumulada, vamos revisar a arquitetura da nossa solução e tentar melhorá-la, antes que avancemos muito.
Modelos de regressão da biblioteca Scikit-learn e sua exportação para ONNX Modelos de regressão da biblioteca Scikit-learn e sua exportação para ONNX
Neste artigo, exploraremos a aplicação de modelos de regressão do pacote Scikit-learn, tentaremos convertê-los para o formato ONNX e usaremos os modelos resultantes em programas MQL5. Além disso, compararemos a precisão dos modelos originais com suas versões ONNX para ambas as precisões float e double. Além disso, examinaremos a representação ONNX dos modelos de regressão, com o objetivo de fornecer uma melhor compreensão de sua estrutura interna e princípios operacionais.