
Desenvolvendo um EA multimoeda (Parte 15): Preparando o EA para o trading real
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:
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
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.





- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
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.