English Русский 中文 Español Deutsch 日本語
preview
Desenvolvendo um EA multimoeda (Parte 15): Preparando o EA para o trading real

Desenvolvendo um EA multimoeda (Parte 15): Preparando o EA para o trading real

MetaTrader 5Testador |
180 2
Yuriy Bykov
Yuriy Bykov

Introdução

Embora já tenhamos alcançado certos resultados nos artigos anteriores, o trabalho não pode ser considerado concluído. O objetivo final é desenvolver um EA multimoeda que possa ser utilizado em uma ou mais contas reais de diferentes corretoras. Até agora, nossos esforços se concentraram em obter bons resultados de trading nos testes, pois sem isso não é possível garantir um bom desempenho do EA em uma conta real. Agora, com resultados razoáveis de testes em mãos, podemos começar a olhar para aspectos que garantam o funcionamento correto no ambiente real.

Em parte, já abordamos esse aspecto do desenvolvimento do EA. Especificamente, o desenvolvimento do gerenciador de riscos foi um passo para atender às exigências que podem surgir no processo de trading real. Para testar ideias de trading, o gerenciador de riscos não é necessário, pois, apesar de importante, ele é uma ferramenta auxiliar.

Neste artigo, tentaremos prever outros mecanismos importantes, sem os quais não é recomendável iniciar o trading em contas reais. Como esses aspectos envolvem situações que não ocorrem ao executar o EA no testador de estratégias, provavelmente será necessário desenvolver EAs adicionais para depurá-los e verificar se estão funcionando corretamente.


Definindo o caminho 

Há muitos detalhes que exigem atenção no trading em contas reais. Vamos nos concentrar por enquanto em alguns deles, listados abaixo:

  • Substituição de símbolos. Realizamos a otimização e configuramos os parâmetros de inicialização do EA utilizando nomes específicos de instrumentos de trading (símbolos). No entanto, pode ocorrer que, em uma conta real, os nomes dos instrumentos sejam diferentes dos que utilizamos. As diferenças podem incluir sufixos ou prefixos nos nomes (EURGBP.x ou xEURGBP em vez de EURGBP) ou o uso de registros diferentes (eurgbp em vez de EURGBP). No futuro, é possível expandir a lista de instrumentos de trading para incluir aqueles com diferenças ainda maiores nos nomes. Por isso, é essencial implementar regras de substituição de nomes de instrumentos, para que o EA consiga operar nos símbolos utilizados pela corretora específica.

  • Modo de encerramento de trading. Como planejamos atualizar periodicamente a composição e as configurações das estratégias de trading que operam simultaneamente dentro do EA, é desejável implementar a capacidade de colocar o EA já em operação em um modo especial, no qual ele opere "apenas para fechamento", ou seja, ele deverá se concentrar em encerrar as operações, fechando (de preferência com lucro total) todas as posições abertas. Esse processo pode levar algum tempo, especialmente se decidirmos encerrar o trading com o EA em momentos de perdas nas posições abertas.

  • Recuperação após a reinicialização. Isso envolve a capacidade do EA de continuar seu trabalho após uma reinicialização do terminal, que pode ser causada por diversos motivos. Algumas dessas causas não podem ser evitadas. No entanto, o EA deve não apenas retomar a operação, mas fazê-lo exatamente como se a reinicialização não tivesse ocorrido. Por isso, é necessário garantir que o EA salve todas as informações necessárias para restaurar seu estado após uma reinicialização.

Vamos começar a implementar o planejado.


Substituição de símbolos

Começaremos pelo mais simples — adicionaremos a capacidade de definir nas configurações do EA as regras para substituir os nomes dos instrumentos de trading. Normalmente, as diferenças estarão na presença de sufixos e/ou prefixos adicionais. Assim, à primeira vista, podemos adicionar dois novos parâmetros de entrada, nos quais definiremos os sufixos e prefixos.

Contudo, esse método possui menor flexibilidade, já que pressupõe o uso de um algoritmo fixo para obter o nome correto do símbolo a partir dos nomes originais extraídos da string de inicialização. Além disso, converter os nomes para minúsculas exigirá outro parâmetro. Por isso, implementaremos outro método.

Adicionaremos um parâmetro ao EA que conterá uma string no seguinte formato:

<Symbol1>=<TargetSymbol1>;<Symbol2>=<TargetSymbol2>;...<SymbolN>=<TargetSymbolN>

Aqui <Symbol[i]> representa os nomes originais dos instrumentos de trading usados na string de inicialização, enquanto <TargetSymbol[i]> são os nomes-alvo dos instrumentos que serão usados para o trading real. Por exemplo:

EURGBP=EURGBPx;EURUSD=EURUSDx;GBPUSD=GBPUSDx

Esse valor será passado a um método específico do objeto especialista (da classe CVirtualAdvisor), que realizará todas as ações subsequentes necessárias. Se uma string vazia for passada a esse método, não haverá modificação nos nomes dos instrumentos.

Chamaremos esse método de SymbolsReplace, e adicionaremos sua chamada ao código da função de inicialização do EA:

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

input string   symbolsReplace_   = "";       // - Symbol replacement rules


datetime fromDate = TimeCurrent();


CVirtualAdvisor     *expert;             // EA object

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

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

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

// If an error occurred while replacing symbols, then return an error
   if(!expert.SymbolsReplace(symbolsReplace_)) return INIT_FAILED;

// Successful initialization
   return(INIT_SUCCEEDED);
}

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

Adicionaremos a descrição do método especialista, que realiza a substituição de nomes de símbolos, à classe do especialista e sua implementação. Dentro desse método, analisaremos a string de substituições passada, dividindo-a primeiro pelos caracteres de ponto e vírgula ';', e depois pelos caracteres de sinal de igual '='. A partir das partes obtidas, formaremos um dicionário que vincula os nomes dos símbolos originais aos nomes dos símbolos-alvo. Este dicionário será então transmitido a cada instância da estratégia de trading, para que possam realizar a substituição necessária, caso seus símbolos estejam presentes como chaves no dicionário.

A cada etapa onde houver possibilidade de erro, atualizaremos a variável de resultado, permitindo que a função superior saiba sobre a falha na execução das substituições de nomes dos símbolos. Nesse caso, o EA reportará o fracasso na inicialização.

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...

public:
   ...

   bool SymbolsReplace(const string p_symbolsReplace); // Replace symbol names
};

...

//+------------------------------------------------------------------+
//| Replace symbol names                                             |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::SymbolsReplace(string p_symbolsReplace) {
   // If the replacement string is empty, then do nothing
   if(p_symbolsReplace == "") {
      return true;
   }

   // Variable for the result
   bool res = true;

   string symbolKeyValuePairs[]; // Array for individual replacements
   string symbolPair[];          // Array for two names in one replacement

   // Split the replacement string into parts representing one separate replacement
   StringSplit(p_symbolsReplace, ';', symbolKeyValuePairs);

   // Glossary for mapping target symbol to source symbol
   CHashMap<string, string> symbolsMap;

   // For all individual replacements
   FOREACH(symbolKeyValuePairs, {
      // Get the source and target symbols as two array elements 
      StringSplit(symbolKeyValuePairs[i], '=', symbolPair);

      // Check if the target symbol is in the list of available non-custom symbols
      bool custom = false;
      res &= SymbolExist(symbolPair[1], custom);

      // If the target symbol is not found, then report an error and exit
      if(!res) {
         PrintFormat(__FUNCTION__" | ERROR: Target symbol %s for mapping %s not found", symbolPair[1], symbolKeyValuePairs[i]);
         return res;
      }
      
      // Add a new element to the glossary: key is the source symbol, while value is the target symbol
      res &= symbolsMap.Add(symbolPair[0], symbolPair[1]);
   });

   // If no errors occurred, then for all strategies we call the corresponding replacement method
   if(res) {
      FOREACH(m_strategies, res &= ((CVirtualStrategy*) m_strategies[i]).SymbolsReplace(symbolsMap));
   }

   return res;
}
//+------------------------------------------------------------------+

Salvaremos as alterações feitas no arquivo VirtualAdvisor.mqhna pasta atual.

Na classe de estratégia de trading, adicionaremos um método com o mesmo nome, mas que receberá um dicionário de substituições como argumento, e não uma string. Na classe CVirtualStrategy, infelizmente, não podemos implementar esse método, pois nesse nível ainda não temos conhecimento sobre os instrumentos de trading utilizados. Por isso, faremos com que ele seja virtual, delegando sua implementação para níveis inferiores, ou seja, nas classes derivadas.

//+------------------------------------------------------------------+
//| Class of a trading strategy with virtual positions               |
//+------------------------------------------------------------------+
class CVirtualStrategy : public CStrategy {
   ...
public:
   ...
   // Replace symbol names
   virtual bool      SymbolsReplace(CHashMap<string, string> &p_symbolsMap) { 
      return true;
   }
};

Salvaremos as alterações feitas no arquivo VirtualStrategy.mqhna pasta atual.

Atualmente, temos apenas uma classe derivada, que possui a propriedade m_symbol, responsável por armazenar o nome do instrumento de trading. Adicionaremos a ela o método SymbolsReplace(), que verificará se existe no dicionário passado uma chave correspondente ao nome do instrumento de trading atual e realizará a substituição do instrumento, quando necessário:

//+------------------------------------------------------------------+
//| Trading strategy using tick volumes                              |
//+------------------------------------------------------------------+
class CSimpleVolumesStrategy : public CVirtualStrategy {
protected:
   string            m_symbol;         // Symbol (trading instrument)
   ...

public:
   ...

   // Replace symbol names
   virtual bool      SymbolsReplace(CHashMap<string, string> &p_symbolsMap);
};

...

//+------------------------------------------------------------------+
//| Replace symbol names                                             |
//+------------------------------------------------------------------+
bool CSimpleVolumesStrategy::SymbolsReplace(CHashMap<string, string> &p_symbolsMap) {
   // If there is a key in the glossary that matches the current symbol
   if(p_symbolsMap.ContainsKey(m_symbol)) {
      string targetSymbol; // Target symbol
      
      // If the target symbol for the current one is successfully retrieved from the glossary
      if(p_symbolsMap.TryGetValue(m_symbol, targetSymbol)) {
         // Update the current symbol
         m_symbol = targetSymbol;
      }
   }
   
   return true;
}

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

Com isso, as alterações relativas a essa sub-tarefa estão concluídas. Os testes realizados no testador mostraram que o EA inicia o trading com sucesso utilizando os novos símbolos conforme as regras de substituição. Vale destacar que, como usamos o método CHashMap::Add() para preencher o dicionário de substituições, uma tentativa de adicionar um novo elemento (símbolo-alvo) com uma chave já existente (símbolo original) resultará em erro.

Isso significa que, se na string de substituições houver duas regras para o mesmo símbolo, o EA não passará na inicialização. Será necessário corrigir a string de substituições, removendo as duplicatas das regras para os mesmos instrumentos de trading.


Modo de encerramento de trading

O próximo passo é adicionar um modo especial de operação do EA, o modo de encerramento de trading. Primeiramente, precisamos definir o que isso significa. Como pretendemos ativá-lo apenas quando quisermos substituir o EA atual por um novo, com outros parâmetros, temos dois objetivos conflitantes: por um lado, queremos fechar todas as posições abertas pelo EA antigo o mais rápido possível. Por outro lado, não queremos fechar posições se o lucro flutuante for negativo no momento. Nesse caso, pode ser melhor aguardar um pouco até que o EA saia do rebaixamento.

Portanto, formularemos a tarefa da seguinte maneira: ao ativar o modo de encerramento de trading, o EA deve fechar todas as posições abertas e não abrir novas, assim que o lucro flutuante se tornar não negativo. Se o lucro já for não negativo no momento da ativação, não será necessário esperar: o EA fechará imediatamente todas as posições. Caso contrário, será necessário aguardar.

O próximo ponto a considerar é: quanto tempo devemos esperar? Ao observar os resultados de testes históricos, notamos que os rebaixamentos podem durar vários meses. Assim, se simplesmente esperarmos, uma ativação em um momento desfavorável pode resultar em uma espera muito longa. Talvez seja mais vantajoso fechar todas as posições do EA antigo sem esperar a recuperação para um saldo positivo, aceitando as perdas atuais. Isso permitiria introduzir a nova versão mais rapidamente, que, possivelmente, durante o tempo de espera pela recuperação da versão antiga, poderia gerar lucros suficientes para compensar as perdas aceitas ao interromper a versão anterior.

No entanto, não podemos saber antecipadamente nem o tempo necessário para a antiga versão sair do rebaixamento, nem os potenciais lucros que a nova versão poderia gerar nesse período, pois, no momento da decisão, esses resultados ainda estão no futuro. 

Uma possível solução de compromisso para essa situação seria introduzir um tempo limite de espera, após o qual todas as posições da versão antiga seriam forçosamente encerradas, independentemente do rebaixamento atual. Podem ser elaboradas opções mais complexas. Por exemplo, usar esse tempo limite como um parâmetro de uma função linear ou não linear do tempo, que retornaria um valor de patrimônio para o qual estaríamos dispostos a fechar todas as posições naquele momento. No caso mais simples, seria uma função de limiar, que retorna 0 até o tempo limite e, após ele, um valor inferior ao patrimônio atual da conta. Dessa forma, todas as posições seriam encerradas após o tempo estipulado.

Vamos passar para a implementação. A primeira ideia consistiu em adicionar dois parâmetros de entrada (ativação do modo de encerramento e tempo limite em dias) ao arquivo do EA e utilizá-los na função de inicialização e em seguida:

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

input bool     useOnlyCloseMode_ = false;    // - Enable close mode
input double   onlyCloseDays_    = 0;        // - Maximum time of closing mode (days)

...

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

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

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

   ...

// Successful initialization
   return(INIT_SUCCEEDED);
}

Contudo, à medida que o desenvolvimento avançava, ficou claro que teríamos que escrever um código muito semelhante ao que havíamos feito recentemente. Descobriu-se que o comportamento necessário no modo de encerramento é muito semelhante ao de um EA com um gerenciador de riscos configurado para atingir um valor de lucro-alvo igual à diferença entre o saldo atual no início do modo de encerramento e o saldo inicial. Portanto, por que não ajustar um pouco o gerenciador de riscos para que o modo de encerramento possa ser implementado simplesmente configurando os parâmetros adequados nele?

Vamos pensar no que falta ao gerenciador de riscos para implementar o modo de encerramento. No caso mais simples, se não lidarmos com o tempo limite, não será necessário realizar ajustes no gerenciador de riscos. Basta configurar o parâmetro de lucro-alvo na versão antiga para o valor igual à diferença entre o saldo atual da conta e o saldo inicial e aguardar que esse valor seja alcançado. Podemos até ajustá-lo periodicamente ao longo do tempo. Afinal, espera-se que esse mecanismo seja utilizado raramente. Contudo, seria preferível que o encerramento automático ocorresse após o tempo estipulado. Por isso, vamos adicionar ao gerenciador de riscos a capacidade de definir não apenas o lucro-alvo, mas também o tempo máximo permitido para alcançá-lo. Esse tempo será o limite para o fechamento das posições.

Esse tempo será transmitido como uma data e horário específicos, eliminando a necessidade de registrar a data de início das operações, a partir da qual seria necessário calcular o intervalo estipulado. Adicionaremos esse parâmetro ao conjunto de parâmetros de entrada relacionados ao gerenciador de riscos. Também incluiremos a substituição de seu valor na string de inicialização:

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

input group ":::  Risk manager"

...

input ENUM_RM_CALC_OVERALL_PROFIT
rmCalcOverallProfitLimit_                    = RM_CALC_OVERALL_PROFIT_MONEY_BB;  // - Method for calculating total profit
input double      rmMaxOverallProfitLimit_   = 1000000;                          // - Overall profit
input datetime    rmMaxOverallProfitDate_    = 0;                                // - Maximum time of waiting for the total profit (days)

...

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

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

...

// Successful initialization
   return(INIT_SUCCEEDED);
}

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

Na classe do gerenciador de riscos, começaremos adicionando uma nova propriedade para o tempo limite de espera pelo lucro estipulado e configuraremos seu valor no construtor a partir da string de inicialização. Além disso, criaremos um novo método OverallProfit(), que retornará o valor do lucro desejado para o encerramento:

//+------------------------------------------------------------------+
//| Risk management class (risk manager)                             |
//+------------------------------------------------------------------+
class CVirtualRiskManager : public CFactorable {
protected:
// Main constructor parameters
   ...
   ENUM_RM_CALC_OVERALL_PROFIT m_calcOverallProfitLimit; // Method for calculating maximum overall profit
   double            m_maxOverallProfitLimit;            // Parameter for calculating the maximum overall profit
   datetime          m_maxOverallProfitDate;             // Maximum time of reaching the total profit

   ...

// Protected methods
   double            DailyLoss();            // Maximum daily loss
   double            OverallLoss();          // Maximum total loss
   double            OverallProfit();        // Maximum profit

   ...
};


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

// Read the initialization string and set the property values
   m_isActive = (bool) ReadLong(p_params);
   m_baseBalance = ReadDouble(p_params);
   m_calcDailyLossLimit = (ENUM_RM_CALC_DAILY_LOSS) ReadLong(p_params);
   m_maxDailyLossLimit = ReadDouble(p_params);
   m_closeDailyPart = ReadDouble(p_params);
   m_calcOverallLossLimit = (ENUM_RM_CALC_OVERALL_LOSS) ReadLong(p_params);
   m_maxOverallLossLimit = ReadDouble(p_params);
   m_closeOverallPart = ReadDouble(p_params);
   m_calcOverallProfitLimit = (ENUM_RM_CALC_OVERALL_PROFIT) ReadLong(p_params);
   m_maxOverallProfitLimit = ReadDouble(p_params);
   m_maxOverallProfitDate  = (datetime) ReadLong(p_params);
   m_maxRestoreTime = ReadDouble(p_params);
   m_lastVirtualProfitFactor = ReadDouble(p_params);

   ...
}

O método OverallProfit() verificará inicialmente se o tempo para alcançar o lucro desejado foi definido. Se tiver sido definido e o tempo atual já tiver ultrapassado o horário estipulado, o método retornará o valor do lucro atual, pois o lucro disponível no momento será considerado como o lucro desejado. Isso resultará no encerramento de todas as posições e na interrupção das operações. Se o tempo ainda não tiver sido alcançado, o método retornará o valor do lucro desejado, calculado com base nos parâmetros de entrada:

//+------------------------------------------------------------------+
//| Maximum total profit                                             |
//+------------------------------------------------------------------+
double CVirtualRiskManager::OverallProfit() {
   // Current time
   datetime tc = TimeCurrent();
   
   // If the current time is greater than the specified maximum time,
   if(m_maxOverallProfitDate && tc > m_maxOverallProfitDate) {
      // Return the value that guarantees the positions are closed
      return m_overallProfit;
   } else if(m_calcOverallProfitLimit == RM_CALC_OVERALL_PROFIT_PERCENT_BB) {
      // To get a given percentage of the base balance, calculate it 
      return m_baseBalance * m_maxOverallProfitLimit / 100;
   } else {
      // To get a fixed value, just return it 
      // RM_CALC_OVERALL_PROFIT_MONEY_BB
      return m_maxOverallProfitLimit;
   }
}

Utilizaremos este método ao verificarmos a necessidade de encerramento no interior do método CheckOverallProfitLimit():

//+------------------------------------------------------------------+
//| Check if the specified profit has been achieved                  |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckOverallProfitLimit() {
// If overall loss is reached and positions are still open
   if(m_overallProfit >= OverallProfit() && CMoney::DepoPart() > 0) {
      // Reduce the multiplier of the used part of the overall balance by the overall loss
      m_overallDepoPart = 0;

      // Set the risk manager to the achieved overall profit state
      m_state = RM_STATE_OVERALL_PROFIT;

      // Set the value of the used part of the overall balance
      SetDepoPart();

      ...

      return true;
   }

   return false;
}

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

As alterações relativas ao modo de encerramento estão, em sua maior parte, concluídas. As etapas restantes serão implementadas posteriormente, quando trabalharmos na funcionalidade de recuperação das operações após uma reinicialização.


Recuperação após reinicialização

A necessidade de implementar essa funcionalidade foi prevista desde as primeiras partes da série. Muitas das classes que criamos já possuem métodos Save() e Load(), projetados especificamente para salvar e carregar o estado do objeto. Embora alguns desses métodos já contassem com código funcional, eles foram deixados de lado temporariamente enquanto lidávamos com outras tarefas, o que resultou em sua desatualização. Agora é hora de restaurar a funcionalidade completa desses métodos.

As principais alterações serão feitas novamente na classe do gerenciador de riscos, já que os métodos nela permanecem completamente vazios. Além disso, será necessário garantir a invocação dos métodos de salvamento e carregamento do gerenciador de riscos durante as operações de salvar/carregar do EA, já que o gerenciador de riscos foi adicionado posteriormente e não fazia parte das informações salvas anteriormente.

Começaremos adicionando um parâmetro de entrada ao EA que determinará se o estado anterior deve ser restaurado. Por padrão, ele será configurado como True. No entanto, caso seja necessário iniciar as operações do EA do zero, ele poderá ser configurado como False. Nesse caso, todas as informações previamente salvas serão substituídas por novas. Posteriormente, esse parâmetro poderá ser alterado novamente para True. Na função de inicialização do EA, verificaremos se o estado anterior precisa ser carregado e, se sim, faremos a recuperação:

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

input bool     usePrevState_     = true;     // - Load the previous state

...

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

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

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

// If an error occurred while replacing symbols, then return an error
   if(!expert.SymbolsReplace(symbolsReplace_)) return INIT_FAILED;


// If we need to restore the state,
   if(usePrevState_) {
      // Load the previous state if available
      expert.Load();
      expert.Tick();
   }

// Successful initialization
   return(INIT_SUCCEEDED);
}

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

Antes de prosseguir com os métodos de salvamento/carregamento do estado, vamos analisar um aspecto importante. Na versão anterior, o nome do arquivo para salvar o estado era formado pelo nome do EA, seu número mágico e a palavra ".test" quando executado no modo de teste visual. O nome do EA é uma constante, já que ele está fixado no código-fonte e não pode ser alterado pelos parâmetros de entrada do EA. Já o número mágico pode ser alterado pelos parâmetros de entrada. Isso significa que, se mudarmos o número mágico, o EA não carregará o arquivo criado com o número mágico anterior. No entanto, isso também implica que, se alterarmos a composição das instâncias individuais das estratégias de trading, mas mantivermos o mesmo número mágico, o EA tentará usar o arquivo antigo para carregar o estado.

Isso provavelmente resultará em erros, e precisamos evitar essa situação. Uma solução possível é incluir no nome do arquivo uma parte que dependa das instâncias utilizadas nas estratégias de trading. Assim, se a composição das estratégias mudar, essa parte do nome do arquivo também mudará, impedindo o uso de um arquivo antigo após a atualização das estratégias.

Podemos gerar essa parte variável do nome do arquivo calculando uma função hash com base na string de inicialização do EA ou em parte dela. De fato, mencionamos a necessidade de usar um arquivo diferente apenas quando houver modificação na composição das estratégias de trading. Se alterarmos, por exemplo, as configurações do gerenciador de riscos, a string de inicialização será modificada, mas o nome do arquivo utilizado para salvar o estado não deveria ser afetado. Portanto, calcularemos o hash apenas da parte da string de inicialização que contém informações sobre as instâncias individuais das estratégias de trading.

Para isso, adicionaremos um novo método chamado HashParams() e faremos ajustes no construtor do EA:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...
   virtual string    HashParams(string p_name);   // Hash value of EA parameters 

public:
   ...
};

...

//+------------------------------------------------------------------+
//| Hash value of EA parameters                                      |
//+------------------------------------------------------------------+
string CVirtualAdvisor::HashParams(string p_params) {
   uchar hash[], key[], data[];

   // Calculate the hash from the initialization string  
   StringToCharArray(p_params, data);
   CryptEncode(CRYPT_HASH_MD5, data, key, hash);

   // Convert it from the array of numbers to a string with hexadecimal notation
   string res = "";
   FOREACH(hash, res += StringFormat("%X", hash[i]); if(i % 4 == 3 && i < 15) res += "-");
   
   return res;
}

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(string p_params) {


       ... 

// If there are no read errors,
   if(IsValid()) {

      
    ... 

      // Form the name of the file for saving the state from the EA name and parameters
      m_name = StringFormat("%s-%d-%s%s.csv",
                            (p_name != "" ? p_name : "Expert"),
                            p_magic,
                            HashParams(groupParams),
                            (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                           );;

      
    ... 
   }
}

Incluiremos as chamadas de salvamento/carregamento do gerenciador de riscos nos métodos correspondentes do EA: 

//+------------------------------------------------------------------+
//| 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

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

         m_riskManager.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;
}

//+------------------------------------------------------------------+
//| 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));

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

         if(!res) {
            PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_name);
         }

         res &= m_riskManager.Load(f);
         
         if(!res) {
            PrintFormat(__FUNCTION__" | ERROR loading risk manager from file %s", m_name);
         }

         FileClose(f);
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d", m_name, GetLastError());
         res = false;
      }
   }

   return res;
}

Salvaremos as alterações feitas no arquivo VirtualAdvisor.mq5 na pasta atual.

Agora resta apenas implementar os métodos de salvamento/carregamento do gerenciador de riscos. Vamos avaliar o que é necessário salvar para o gerenciador de riscos. Os parâmetros de entrada do gerenciador de riscos não precisam ser salvos, pois eles são sempre obtidos dos parâmetros de entrada do EA, que podem ter valores alterados a cada nova execução. Também não é necessário salvar valores atualizados pelo próprio gerenciador de riscos, como saldo, patrimônio, lucro diário etc. A única exceção entre esses valores é o nível base diário, pois seu cálculo ocorre apenas uma vez por dia.

Já todas as propriedades relacionadas ao estado atual e à gestão do tamanho das posições abertas (exceto a parte utilizada do saldo total) devem ser obrigatoriamente salvas.

// Current state
   ENUM_RM_STATE     m_state;                // State
   double            m_lastVirtualProfit;    // Profit of open virtual positions at the moment of loss limit 
   datetime          m_startRestoreTime;     // Start time of restoring the size of open positions
   datetime          m_startTime;

// Updated values
   
    ... 

// Managing the size of open positions
   double            m_baseDepoPart;         // Used (original) part of the total balance
   double            m_dailyDepoPart;        // Multiplier of the used part of the total balance by daily loss
   double            m_overallDepoPart;      // Multiplier of the used part of the total balance by overall loss

Com base nisso, a implementação desses métodos pode ser feita da seguinte forma:

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::Save(const int f) {
   FileWrite(f,
             m_state, m_lastVirtualProfit, m_startRestoreTime, m_startTime,
             m_dailyDepoPart, m_overallDepoPart);

   return true;
}

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::Load(const int f) {
   m_state = (ENUM_RM_STATE) FileReadNumber(f);
   m_lastVirtualProfit = FileReadNumber(f);
   m_startRestoreTime = FileReadDatetime(f);
   m_startTime = FileReadDatetime(f);
   m_dailyDepoPart = FileReadNumber(f);
   m_overallDepoPart = FileReadNumber(f);

   return true;
}

Salvaremos as alterações feitas no arquivo VirtualRiskManager.mq5 na pasta atual.


Testes

Para testar a funcionalidade adicionada, seguiremos dois caminhos. Primeiro, colocaremos o EA compilado em um gráfico e verificaremos se o arquivo com os dados de estado é criado. Para continuar os testes, será necessário aguardar até que o EA abra alguma posição. No entanto, isso pode levar bastante tempo, e esperar que o gerenciador de riscos entre em ação para validar a retomada correta do EA após sua interferência pode levar ainda mais tempo. Em segundo lugar, utilizaremos o testador de estratégias para simular a retomada do trabalho do EA após uma interrupção.

Para isso, criaremos um novo EA baseado no existente, ao qual adicionaremos dois novos parâmetros de entrada: o horário de interrupção antes da reinicialização e o horário de início da reinicialização. Esses parâmetros serão tratados da seguinte maneira:

  • Se o horário de interrupção antes da reinicialização não for especificado (igual a zero ou 1970-01-01 00:00:00) ou não estiver dentro do intervalo de teste, o EA funcionará como o original.
  • Se um horário específico de interrupção for definido e estiver dentro do intervalo de teste, ao atingir esse horário, o EA interromperá o processamento de ticks no objeto especialista até que o horário indicado no segundo parâmetro seja alcançado.

No código, esses dois parâmetros serão configurados assim:

input datetime restartStopTime_  = 0;        // - Stop time before restart
input datetime restartStartTime_ = 0;        // - Restart launch time

Alteraremos a função de processamento de ticks no EA. Para registrar o início da pausa, adicionaremos uma variável lógica global chamada isRestarting. Se ela estiver configurada como True, significa que o EA está em pausa. Assim que o horário atual ultrapassar o horário de retomada, o EA carregará o estado anterior do especialista e redefinirá o indicador isRestarting:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
// If the stop time is specified,
   if(restartStopTime_ != 0) {
      // Define the current time
      datetime tc = TimeCurrent();

      // If we are in the interval between stopping and resuming,
      if(tc >= restartStopTime_ && tc <= restartStartTime_) {
         // Save the status and exit
         isRestarting = true;
         return;
      }

      // If we were in a state between stopping and resuming,
      // and it is time to resume work,
      if(isRestarting && tc > restartStartTime_) {
         // Load the EA status
         expert.Load();
         // Reset the status flag between stop and resume
         isRestarting = false;
      }
   }

// Perform tick handling
   expert.Tick();
}

Salvaremos as alterações feitas no arquivo SimpleVolumesTestRestartExpert.mq5 na pasta atual.

Primeiro, analisaremos os resultados sem nenhuma interrupção no intervalo de 2021-2022.

Fig. 1. Resultados dos testes sem interrupção de negociação

Agora simularemos uma pequena interrupção no trabalho do EA em algum momento. Após a execução do teste, os resultados foram os mesmos de quando não houve interrupção. Isso indica que, em casos de pequenas pausas, o EA consegue restaurar seu estado e retomar as operações com sucesso.

Para observar uma diferença mais significativa, simularemos uma pausa maior, de aproximadamente 4 meses. Os resultados foram os seguintes:

Fig. 2. Resultados dos testes com interrupção de negociação de 2021.07.27 a 2021.11.29

No gráfico, o intervalo aproximado da pausa está destacado com um retângulo de borda amarela. Durante esse período, as posições abertas pelo EA ficaram sem supervisão. No entanto, ao retomar as operações, o EA reconectou-se às suas posições e obteve resultados gerais satisfatórios. Podemos concluir que a funcionalidade de salvar e carregar o estado do EA foi implementada com sucesso.


Considerações finais

Vamos revisar os resultados alcançados. Iniciamos a preparação séria de nosso EA para operar em contas reais. Para isso, consideramos vários cenários que não aparecem no testador de estratégias, mas que provavelmente surgirão durante a operação em contas reais.

Analisamos a operação do EA em contas nas quais os nomes dos instrumentos de trading diferem daqueles utilizados durante sua otimização. Para isso, implementamos a funcionalidade de substituição de nomes de símbolos. Também implementamos a possibilidade de encerrar as operações gradualmente caso seja necessário colocar o EA em funcionamento com outros parâmetros de entrada. Além disso, um aprimoramento importante foi a adição da funcionalidade de salvar o estado do EA, garantindo a retomada correta das operações após uma reinicialização do terminal.

No entanto, esses não são todos os preparativos necessários para configurar o EA para operar em uma conta real. Seria útil organizar melhor a saída de informações de depuração no log, de modo a permitir a visualização de diferentes tipos de informações auxiliares. Também seria interessante fornecer uma exibição consolidada do estado atual do EA diretamente no gráfico. Ainda mais importante é o aprimoramento que permitirá que o EA funcione mesmo na ausência de uma base de dados de resultados de otimização na pasta de trabalho do terminal. Atualmente, essa base é indispensável, pois é dela que são extraídas as partes necessárias para formar a string de inicialização do especialista. Exploraremos essas melhorias detalhadamente em artigos futuros.

Obrigado por acompanhar até o final, e até a próxima!

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

Últimos Comentários | Ir para discussão (2)
Максим Курбатов
Максим Курбатов | 23 jul. 2024 em 17:26
Olá, o Expert Advisor não está sendo compilado. A princípio, ele exige a inclusão de diferentes arquivos mqh - tentei pegar os arquivos correspondentes de seus artigos anteriores. Ele continua não compilando - aparentemente, carrego as versões erradas dos arquivos.... Você poderia me dizer quais versões dos arquivos de inclusão são relevantes para esse código EA? Muito obrigado!
Yuriy Bykov
Yuriy Bykov | 24 jul. 2024 em 07:43

Olá!

Tentarei verificar isso em breve. Em cada artigo, tento verificar novamente se todos os arquivos que foram modificados estão anexados. Portanto, você fez a coisa certa: os arquivos que não estão anexados ao artigo atual devem ser retirados do artigo mais recente, onde esse arquivo existe.

Teoria do caos no trading (Parte 1): Introdução, aplicação nos mercados financeiros e o indicador de Lyapunov Teoria do caos no trading (Parte 1): Introdução, aplicação nos mercados financeiros e o indicador de Lyapunov
É possível aplicar a teoria do caos nos mercados financeiros? Vamos explorar nesta matéria como a teoria clássica do caos e os sistemas caóticos diferem do conceito proposto por Bill Williams.
Aprendendo MQL5 do iniciante ao profissional (Parte III): Tipos de dados complexos e arquivos inclusos Aprendendo MQL5 do iniciante ao profissional (Parte III): Tipos de dados complexos e arquivos inclusos
O artigo é o terceiro de uma série sobre os aspectos fundamentais da programação em MQL5. Aqui são descritos tipos de dados complexos que não foram abordados no artigo anterior, incluindo estruturas, uniões, classes e o tipo de dado "função". Também é explicado como adicionar modularidade ao programa utilizando a diretiva de pré-processador #include.
Do básico ao intermediário: Definições (I) Do básico ao intermediário: Definições (I)
Neste artigo, faremos diversas coisas, que para muitos irão parecer estranho e totalmente fora de contexto. Mas que ser for bem empregado tornará seu aprendo muito mais divertido e empolgante. Já que podemos construir coisas bem interessantes, com base no que será visto aqui. A ponto de permitir uma melhor assimilação da sintaxe da linguagem MQL5. O conteúdo exposto aqui, visa e tem como objetivo, pura e simplesmente a didática. De modo algum deve ser encarado como sendo, uma aplicação cuja finalidade não venha a ser o aprendizado e estudo dos conceitos mostrados.
Desenvolvendo um EA multimoeda (Parte 14): Alteração adaptativa dos volumes no gerenciador de risco Desenvolvendo um EA multimoeda (Parte 14): Alteração adaptativa dos volumes no gerenciador de risco
O gerenciador de risco anteriormente desenvolvido continha apenas funcionalidades básicas. Vamos explorar caminhos para aprimorá-lo, buscando melhorar os resultados de negociação sem alterar a lógica das estratégias de trading.