Avaliação visual e ajuste da negociação no MetaTrader 5
Conteúdo
- Introdução
- Salvando o histórico de negociações em um arquivo
- Analisando o histórico de negociações a partir de um arquivo no testador
- Vamos ajustar os stop orders
- Conclusão
Introdução
Imagine a seguinte situação: em uma conta específica, há uma negociação relativamente ativa sendo realizada há bastante tempo, com diferentes instrumentos, por vários EAs e até mesmo manualmente. E agora, passado algum tempo, queremos ver os resultados desse trabalho. Claro que é possível conferir os relatórios padrão de negociação no terminal pressionando Alt+E. Também dá para carregar os ícones das negociações no gráfico e visualizar as aberturas e fechamentos das posições. Mas e se quisermos ver de forma dinâmica como foi realizada a negociação, onde e como as posições foram abertas e fechadas? Ver separadamente por cada símbolo, ou todos de uma vez, os momentos de abertura e fechamento das posições, em quais níveis os stop orders foram colocados e se seu tamanho foi justificado. E se depois nos perguntarmos "o que teria acontecido se..." (e aqui cabem várias opções: outros stops, com algoritmos e critérios diferentes, uso de trailing nas posições, ou mover os stops para o ponto de equilíbrio etc.); e depois ainda testar todos esses "e se" com resultados visíveis e claros. Como a negociação poderia ter mudado se...
Para resolver esse tipo de questão, na verdade, já temos tudo o que precisamos. Basta carregar o histórico da conta em um arquivo (todas as negociações realizadas) e, em seguida, executar um EA no testador de estratégias que leia essas negociações do arquivo e abra/feche posições no testador do terminal cliente. Com um EA assim, podemos adicionar código para alterar as condições de saída das posições e comparar como a negociação teria mudado, e o que teria acontecido se...
E o que isso nos oferece? Mais uma ferramenta para buscar melhores resultados, para fazer ajustes na negociação que já estava sendo feita na conta; o teste visual permite ver em tempo real se as posições foram abertas corretamente em determinado instrumento, se foram fechadas no momento certo, e assim por diante. E o mais importante: podemos simplesmente adicionar um novo algoritmo no código do EA, testar, obter um resultado e aplicar as correções nos EAs que estão operando nessa conta.
Vamos programar esse comportamento do EA:
- se o EA for iniciado no gráfico de qualquer instrumento, ele reunirá todo o histórico de negociações da conta atual, salvará todas as negociações em um único arquivo e não fará mais nada a partir daí;
- se o EA for iniciado no testador, ele lerá o histórico de negociações salvo no arquivo e, durante a execução do teste, repetirá todas as negociações do arquivo, abrindo e fechando posições.
Dessa forma, o EA primeiro prepara o arquivo com o histórico de negociações (ao ser executado no gráfico), e depois executa as negociações do arquivo, repetindo integralmente a negociação da conta (quando rodado no testador de estratégias).
Em seguida, faremos melhorias no EA para que seja possível definir outros valores de StopLoss e TakeProfit para as posições abertas no testador.
Salvando o histórico de negociações em um arquivo
No diretório do terminal \MQL5\Experts\ criaremos uma nova pasta chamadaTradingByHistoryDeals, e dentro dela, um novo arquivo de EA com o nome TradingByHistoryDeals.mq5.
No EA, deve haver a possibilidade de selecionar qual símbolo e qual magic number serão utilizados para o teste. Caso haja vários EAs operando em diferentes símbolos ou magic numbers na conta, será possível escolher nas configurações qual símbolo ou magic number nos interessa — ou então considerar todos de uma vez.
//+------------------------------------------------------------------+ //| TradingByHistoryDeals.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Expert | //+------------------------------------------------------------------+ //--- input parameters input string InpTestedSymbol = ""; /* The symbol being tested in the tester */ input long InpTestedMagic = -1; /* The magic number being tested in the tester */ sinput bool InpShowDataInLog = false; /* Show collected data in the log */
Os valores padrão para símbolo e magic number são: string vazia e -1. Com esses valores, o EA não irá filtrar o histórico de negociações nem por símbolo, nem por magic number — todo o histórico de negociação será testado. Uma terceira opção permitirá ao EA exibir (ou não) no log as descrições de todas as negociações salvas no arquivo, possibilitando verificar visualmente se os dados foram salvos corretamente.
Cada negociação é um conjunto completo de diferentes parâmetros, descritos por diversas propriedades da negociação. A forma mais simples de representar isso é armazenar todas as propriedades da negociação em uma estrutura. Para gravar uma grande quantidade de negociações em um arquivo, é necessário usar um array de estruturas. E depois, esse array será salvo em arquivo. A linguagem MQL5 fornece todos os recursos necessários para isso. A lógica para salvar o histórico de negociações em um arquivo será a seguinte:
- percorrer as negociações históricas em um laço;
- obter cada negociação e gravar seus dados em uma estrutura;
- salvar a estrutura da negociação criada no array de negociações;
- ao final do laço, salvar o array de estruturas preparado no arquivo.
Todos os códigos adicionais (estruturas, classes, enumerações) serão escritos em um arquivo separado. Vamos nomeá-lo com o nome da futura classe do objeto de negociação por símbolo.
Na mesma pasta, criaremos um novo arquivo incluível com o nome SymbolTrade.mqh.
Em seguida, escreveremos as definições de macros para o nome da pasta onde será salvo o arquivo de histórico, o nome do arquivo e o caminho completo para ele, e incluir no arquivo criado todos os arquivos necessários da Biblioteca Padrão:
//+------------------------------------------------------------------+ //| SymbolTrade.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #define DIRECTORY "TradingByHistoryDeals" #define FILE_NAME "HistoryDealsData.bin" #define PATH DIRECTORY+"\\"+FILE_NAME #include <Arrays\ArrayObj.mqh> #include <Trade\Trade.mqh>
Agora vamos escrever a estrutura da negociação:
//+------------------------------------------------------------------+ //| Deal structure. Used to create a deal history file | //+------------------------------------------------------------------+ struct SDeal { ulong ticket; // Deal ticket long order; // Order the deal is based on long pos_id; // Position ID long time_msc; // Time in milliseconds datetime time; // Time double volume; // Volume double price; // Price double profit; // Profit double commission; // Deal commission double swap; // Accumulated swap at closing double fee; // Payment for the deal is accrued immediately after the deal is completed double sl; // Stop Loss level double tp; // Take Profit level ENUM_DEAL_TYPE type; // Type ENUM_DEAL_ENTRY entry; // Position change method ENUM_DEAL_REASON reason; // Deal reason or source long magic; // EA ID int digits; // Symbol digits ushort symbol[16]; // Symbol ushort comment[64]; // Deal comment ushort external_id[256]; // Deal ID in an external trading system (on the exchange) //--- Set string properties bool SetSymbol(const string deal_symbol) { return(::StringToShortArray(deal_symbol, symbol)==deal_symbol.Length()); } bool SetComment(const string deal_comment) { return(::StringToShortArray(deal_comment, comment)==deal_comment.Length()); } bool SetExternalID(const string deal_external_id) { return(::StringToShortArray(deal_external_id, external_id)==deal_external_id.Length()); } //--- Return string properties string Symbol(void) { return(::ShortArrayToString(symbol)); } string Comment(void) { return(::ShortArrayToString(comment)); } string ExternalID(void) { return(::ShortArrayToString(external_id)); } };
Como iremos salvar estruturas de negociações em arquivo, e apenas estruturas com tipos simples podem ser gravadas em arquivos (ver FileWriteArray()), todas as variáveis do tipo string precisam ser substituídas por arrays de ushort, além de criarmos métodos para gravar e recuperar as propriedades string da estrutura.
A estrutura criada servirá apenas para salvar o histórico de negociações em arquivo e ler esse histórico do arquivo. No próprio EA, mais adiante, será criada uma lista de objetos onde serão armazenados os objetos da classe de negociação. Para buscar uma negociação específica na lista e ordenar o array, será necessário indicar a propriedade da negociação pela qual a busca será feita. Para isso, a lista de objetos deve estar ordenada conforme a propriedade desejada.
Vamos escrever uma enumeração com todas as propriedades do objeto de negociação que poderão ser utilizadas para buscas:
//--- Deal sorting types enum ENUM_DEAL_SORT_MODE { SORT_MODE_DEAL_TICKET = 0, // Mode of comparing/sorting by a deal ticket SORT_MODE_DEAL_ORDER, // Mode of comparing/sorting by the order a deal is based on SORT_MODE_DEAL_TIME, // Mode of comparing/sorting by a deal time SORT_MODE_DEAL_TIME_MSC, // Mode of comparing/sorting by a deal time in milliseconds SORT_MODE_DEAL_TYPE, // Mode of comparing/sorting by a deal type SORT_MODE_DEAL_ENTRY, // Mode of comparing/sorting by a deal direction SORT_MODE_DEAL_MAGIC, // Mode of comparing/sorting by a deal magic number SORT_MODE_DEAL_REASON, // Mode of comparing/sorting by a deal reason or source SORT_MODE_DEAL_POSITION_ID, // Mode of comparing/sorting by a position ID SORT_MODE_DEAL_VOLUME, // Mode of comparing/sorting by a deal volume SORT_MODE_DEAL_PRICE, // Mode of comparing/sorting by a deal price SORT_MODE_DEAL_COMMISSION, // Mode of comparing/sorting by commission SORT_MODE_DEAL_SWAP, // Mode of comparing/sorting by accumulated swap on close SORT_MODE_DEAL_PROFIT, // Mode of comparing/sorting by a deal financial result SORT_MODE_DEAL_FEE, // Mode of comparing/sorting by a deal fee SORT_MODE_DEAL_SL, // Mode of comparing/sorting by Stop Loss level SORT_MODE_DEAL_TP, // Mode of comparing/sorting by Take Profit level SORT_MODE_DEAL_SYMBOL, // Mode of comparing/sorting by a name of a traded symbol SORT_MODE_DEAL_COMMENT, // Mode of comparing/sorting by a deal comment SORT_MODE_DEAL_EXTERNAL_ID, // Mode of comparing/sorting by a deal ID in an external trading system SORT_MODE_DEAL_TICKET_TESTER, // Mode of comparing/sorting by a deal ticket in the tester SORT_MODE_DEAL_POS_ID_TESTER, // Mode of comparing/sorting by a position ID in the tester };
Aqui, além das propriedades padrão da negociação, temos mais duas: o ticket da negociação no testador e o identificador da posição no testador. Isso porque estaremos negociando no testador com base em dados de negociações reais, e as posições abertas no testador — e, consequentemente, as negociações com base nas quais essas posições foram abertas — terão ticket e identificador totalmente diferentes no testador. Para conseguirmos relacionar uma negociação real com a negociação correspondente no testador (e também o identificador), será necessário armazenar o ticket e o identificador da posição no testador nas propriedades do objeto de negociação, e, a partir desses dados salvos, fazer a correspondência entre a negociação no testador e a negociação real do histórico.
Vamos pausar esse arquivo por enquanto e voltar ao arquivo do EA que criamos anteriormente. Adicionaremos um array de estruturas, onde serão inseridas as estruturas de todas as negociações do histórico:
//--- input parameters input string InpTestedSymbol = ""; /* The symbol being tested in the tester */ input long InpTestedMagic = -1; /* The magic number being tested in the tester */ sinput bool InpShowDataInLog = false; /* Show collected data in the log */ //--- global variables SDeal ExtArrayDeals[]={};
E escreveremos funções para trabalhar com as negociações históricas.
Função que salva o histórico de negociações em um array:
//+------------------------------------------------------------------+ //| Save deals from history into the array | //+------------------------------------------------------------------+ int SaveDealsToArray(SDeal &array[], bool logs=false) { //--- deal structure SDeal deal={}; //--- request the deal history in the interval from the very beginning to the current moment if(!HistorySelect(0, TimeCurrent())) { Print("HistorySelect() failed. Error ", GetLastError()); return 0; } //--- total number of deals in the list int total=HistoryDealsTotal(); //--- handle each deal for(int i=0; i<total; i++) { //--- get the ticket of the next deal (the deal is automatically selected to get its properties) ulong ticket=HistoryDealGetTicket(i); if(ticket==0) continue; //--- save only balance and trading deals ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket, DEAL_TYPE); if(deal_type!=DEAL_TYPE_BUY && deal_type!=DEAL_TYPE_SELL && deal_type!=DEAL_TYPE_BALANCE) continue; //--- save the deal properties in the structure deal.ticket=ticket; deal.type=deal_type; deal.order=HistoryDealGetInteger(ticket, DEAL_ORDER); deal.entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket, DEAL_ENTRY); deal.reason=(ENUM_DEAL_REASON)HistoryDealGetInteger(ticket, DEAL_REASON); deal.time=(datetime)HistoryDealGetInteger(ticket, DEAL_TIME); deal.time_msc=HistoryDealGetInteger(ticket, DEAL_TIME_MSC); deal.pos_id=HistoryDealGetInteger(ticket, DEAL_POSITION_ID); deal.volume=HistoryDealGetDouble(ticket, DEAL_VOLUME); deal.price=HistoryDealGetDouble(ticket, DEAL_PRICE); deal.profit=HistoryDealGetDouble(ticket, DEAL_PROFIT); deal.commission=HistoryDealGetDouble(ticket, DEAL_COMMISSION); deal.swap=HistoryDealGetDouble(ticket, DEAL_SWAP); deal.fee=HistoryDealGetDouble(ticket, DEAL_FEE); deal.sl=HistoryDealGetDouble(ticket, DEAL_SL); deal.tp=HistoryDealGetDouble(ticket, DEAL_TP); deal.magic=HistoryDealGetInteger(ticket, DEAL_MAGIC); deal.SetSymbol(HistoryDealGetString(ticket, DEAL_SYMBOL)); deal.SetComment(HistoryDealGetString(ticket, DEAL_COMMENT)); deal.SetExternalID(HistoryDealGetString(ticket, DEAL_EXTERNAL_ID)); deal.digits=(int)SymbolInfoInteger(deal.Symbol(), SYMBOL_DIGITS); //--- increase the array and int size=(int)array.Size(); ResetLastError(); if(ArrayResize(array, size+1, total)!=size+1) { Print("ArrayResize() failed. Error ", GetLastError()); continue; } //--- save the deal in the array array[size]=deal; //--- if allowed, display the description of the saved deal to the journal if(logs) DealPrint(deal, i); } //--- return the number of deals stored in the array return (int)array.Size(); }
O código da função está comentado em detalhes. Selecionamos todo o histórico de negociações desde o início até o momento atual, pegamos cada negociação histórica uma a uma, salvamos suas propriedades nos campos da estrutura e armazenamos a variável da estrutura no array. Ao final do laço sobre o histórico de negociações, a função retorna o tamanho do array de negociações obtido. Para acompanhar o progresso da gravação das negociações no array, podemos imprimir cada negociação processada no log. Para isso, ao chamar a função, basta definir o parâmetro formal logs como true.
Função que imprime no log todas as negociações do array de negociações:
//+------------------------------------------------------------------+ //| Display deals from the array to the journal | //+------------------------------------------------------------------+ void DealsArrayPrint(SDeal &array[]) { int total=(int)array.Size(); //--- if an empty array is passed, report this and return 'false' if(total==0) { PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__); return; } //--- In a loop through the deal array, print out a description of each deal for(int i=0; i<total; i++) { DealPrint(array[i], i); } }
Para exibir a descrição da negociação no log, criaremos algumas funções.
Função que retorna a descrição do tipo de negociação:
//+------------------------------------------------------------------+ //| Return the deal type description | //+------------------------------------------------------------------+ string DealTypeDescription(const ENUM_DEAL_TYPE type) { switch(type) { case DEAL_TYPE_BUY : return "Buy"; case DEAL_TYPE_SELL : return "Sell"; case DEAL_TYPE_BALANCE : return "Balance"; case DEAL_TYPE_CREDIT : return "Credit"; case DEAL_TYPE_CHARGE : return "Additional charge"; case DEAL_TYPE_CORRECTION : return "Correction"; case DEAL_TYPE_BONUS : return "Bonus"; case DEAL_TYPE_COMMISSION : return "Additional commission"; case DEAL_TYPE_COMMISSION_DAILY : return "Daily commission"; case DEAL_TYPE_COMMISSION_MONTHLY : return "Monthly commission"; case DEAL_TYPE_COMMISSION_AGENT_DAILY : return "Daily agent commission"; case DEAL_TYPE_COMMISSION_AGENT_MONTHLY: return "Monthly agent commission"; case DEAL_TYPE_INTEREST : return "Interest rate"; case DEAL_TYPE_BUY_CANCELED : return "Canceled buy deal"; case DEAL_TYPE_SELL_CANCELED : return "Canceled sell deal"; case DEAL_DIVIDEND : return "Dividend operations"; case DEAL_DIVIDEND_FRANKED : return "Franked (non-taxable) dividend operations"; case DEAL_TAX : return "Tax charges"; default : return "Unknown deal type: "+(string)type; } }
Dependendo do tipo de negociação passado para a função, será exibida a string correspondente.
Função que retorna a descrição da forma de modificação da posição:
//+------------------------------------------------------------------+ //| Return position change method | //+------------------------------------------------------------------+ string DealEntryDescription(const ENUM_DEAL_ENTRY entry) { switch(entry) { case DEAL_ENTRY_IN : return "Entry In"; case DEAL_ENTRY_OUT : return "Entry Out"; case DEAL_ENTRY_INOUT : return "Entry InOut"; case DEAL_ENTRY_OUT_BY : return "Entry OutBy"; default : return "Unknown entry: "+(string)entry; } }
Dependendo da forma de modificação da posição passada para a função, será exibida a string correspondente.
Função que retorna a descrição da negociação:
//+------------------------------------------------------------------+ //| Return deal description | //+------------------------------------------------------------------+ string DealDescription(SDeal &deal, const int index) { string indexs=StringFormat("% 5d", index); if(deal.type!=DEAL_TYPE_BALANCE) return(StringFormat("%s: deal #%I64u %s, type %s, Position #%I64d %s (magic %I64d), Price %.*f at %s, sl %.*f, tp %.*f", indexs, deal.ticket, DealEntryDescription(deal.entry), DealTypeDescription(deal.type), deal.pos_id, deal.Symbol(), deal.magic, deal.digits, deal.price, TimeToString(deal.time, TIME_DATE|TIME_MINUTES|TIME_SECONDS), deal.digits, deal.sl, deal.digits, deal.tp)); else return(StringFormat("%s: deal #%I64u %s, type %s %.2f %s at %s", indexs, deal.ticket, DealEntryDescription(deal.entry), DealTypeDescription(deal.type), deal.profit, AccountInfoString(ACCOUNT_CURRENCY), TimeToString(deal.time))); }
Se for uma negociação de balanço, a descrição será exibida no formato
0: deal #190715988 Entry In, type Balance 3000.00 USD at 2024.09.13 21:48
Caso contrário, a descrição da negociação será exibida em outro formato:
1: deal #190724678 Entry In, type Buy, Position #225824633 USDCHF (magic 600), Price 0.84940 at 2024.09.13 23:49:03, sl 0.84811, tp 0.84983
Função que imprime no log a descrição da negociação:
//+------------------------------------------------------------------+ //| Print deal data in the journal | //+------------------------------------------------------------------+ void DealPrint(SDeal &deal, const int index) { Print(DealDescription(deal, index)); }
Aqui tudo é visual, apenas imprimimos a string obtida da função DealDescription().
Vamos escrever funções para gravar e ler o array de negociações em arquivo / a partir de arquivo.
Função que abre o arquivo para gravação:
//+------------------------------------------------------------------+ //| Open a file for writing, return a handle | //+------------------------------------------------------------------+ bool FileOpenToWrite(int &handle) { ResetLastError(); handle=FileOpen(PATH, FILE_WRITE|FILE_BIN|FILE_COMMON); if(handle==INVALID_HANDLE) { PrintFormat("%s: FileOpen() failed. Error %d",__FUNCTION__, GetLastError()); return false; } //--- successful return true; }
Função que abre o arquivo para leitura:
//+------------------------------------------------------------------+ //| Open a file for reading, return a handle | //+------------------------------------------------------------------+ bool FileOpenToRead(int &handle) { ResetLastError(); handle=FileOpen(PATH, FILE_READ|FILE_BIN|FILE_COMMON); if(handle==INVALID_HANDLE) { PrintFormat("%s: FileOpen() failed. Error %d",__FUNCTION__, GetLastError()); return false; } //--- successful return true; }
As funções abrem o arquivo para leitura/gravação. Como parâmetros formais, por referência, é passada uma variável onde será armazenado o handle do arquivo. Retornam true em caso de abertura bem-sucedida do arquivo e false em caso de erro.
Função que salva no arquivo os dados das negociações do array:
//+------------------------------------------------------------------+ //| Save deal data from the array to the file | //+------------------------------------------------------------------+ bool FileWriteDealsFromArray(SDeal &array[], ulong &file_size) { //--- if an empty array is passed, report this and return 'false' if(array.Size()==0) { PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__); return false; } //--- open the file for writing, get its handle int handle=INVALID_HANDLE; if(!FileOpenToWrite(handle)) return false; //--- move the file pointer to the end of the file bool res=true; ResetLastError(); res&=FileSeek(handle, 0, SEEK_END); if(!res) PrintFormat("%s: FileSeek(SEEK_END) failed. Error %d",__FUNCTION__, GetLastError()); //--- write the array data to the end of the file file_size=0; res&=(FileWriteArray(handle, array)==array.Size()); if(!res) PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError()); else file_size=FileSize(handle); //--- close the file FileClose(handle); return res; }
A função recebe um array de estruturas que precisa ser salvo em arquivo. A variável que receberá o tamanho do arquivo criado é passada por referência nos parâmetros formais da função. O arquivo é aberto, o ponteiro do arquivo é movido para o final e os dados do array de estruturas são gravados no arquivo a partir desse ponto. Após o fim da gravação, o arquivo é fechado.
Depois que o array de estruturas de negociações é salvo no arquivo, todas essas negociações podem ser lidas novamente do arquivo para um array, e então ele poderá ser usado para criar listas de negociações e manipulá-las no testador.
Função que carrega os dados das negociações do arquivo para o array:
//+------------------------------------------------------------------+ //| Load the deal data from the file into the array | //+------------------------------------------------------------------+ bool FileReadDealsToArray(SDeal &array[], ulong &file_size) { //--- open the file for reading, get its handle int handle=INVALID_HANDLE; if(!FileOpenToRead(handle)) return false; //--- move the file pointer to the end of the file bool res=true; ResetLastError(); //--- read data from the file into the array file_size=0; res=(FileReadArray(handle, array)>0); if(!res) PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError()); else file_size=FileSize(handle); //--- close the file FileClose(handle); return res; }
Com base nas funções criadas acima, escreveremos uma função para ler o histórico de negociações e gravá-lo em um arquivo.
Função que prepara o arquivo com as negociações do histórico:
//+------------------------------------------------------------------+ //| Prepare a file with history deals | //+------------------------------------------------------------------+ bool PreparesDealsHistoryFile(SDeal &deals_array[]) { //--- save all the account deals in the deal array int total=SaveDealsToArray(deals_array); if(total==0) return false; //--- write the deal array data to the file ulong file_size=0; if(!FileWriteDealsFromArray(deals_array, file_size)) return false; //--- print in the journal how many deals were read and saved to the file, the path to the file and its size PrintFormat("%u deals were saved in an array and written to a \"%s\" file of %I64u bytes in size", deals_array.Size(), "TERMINAL_COMMONDATA_PATH\\Files\\"+ PATH, file_size); //--- now, to perform a check, we will read the data from the file into the array ArrayResize(deals_array, 0, total); if(!FileReadDealsToArray(deals_array, file_size)) return false; //--- print in the journal how many bytes were read from the file and the number of deals received in the array PrintFormat("%I64u bytes were read from the file \"%s\" and written to the deals array. A total of %u deals were received", file_size, FILE_NAME, deals_array.Size()); return true; }
Os comentários no código tornam toda a lógica aqui compreensível. A função é executada no manipulador OnInit() e prepara o arquivo com as negociações para uso posterior:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- If the EA is not running in the tester if(!MQLInfoInteger(MQL_TESTER)) { //--- prepare a file with all historical deals if(!PreparesDealsHistoryFile(ExtArrayDeals)) return(INIT_FAILED); //--- print all deals in the journal after loading them from the file if(InpShowDataInLog) DealsArrayPrint(ExtArrayDeals); //--- get the first balance deal, create the message text and display it using Alert SDeal deal=ExtArrayDeals[0]; long leverage=AccountInfoInteger(ACCOUNT_LEVERAGE); double start_money=deal.profit; datetime first_time=deal.time; string start_time=TimeToString(deal.time, TIME_DATE); string message=StringFormat("Now you can run testing\nInterval: %s - current date\nInitial deposit: %.2f, leverage 1:%I64u", start_time, start_money, leverage); //--- notify via alert of the recommended parameters of the strategy tester for starting the test Alert(message); } //--- All is successful return(INIT_SUCCEEDED); }
Além de salvar todas as negociações históricas no arquivo, também é exibido um alerta com uma mensagem sobre as configurações recomendadas do testador: saldo inicial, alavancagem e data de início do teste correspondente à data da primeira negociação de balanço. Algo assim:
Alert: Now you can run testing Interval: 2024.09.13 - current date Initial deposit: 3000.00, leverage 1:500
Essas configurações do testador resultarão em um resultado final no testador mais próximo possível daquele obtido na conta real.
A estrutura de negociação, escrita no arquivo \MQL5\Experts\TradingByHistoryDeals\SymbolTrade.mqh, serve apenas para salvar o histórico de negociações em arquivo e para ler esse histórico do arquivo. Para o próximo passo, precisamos criar uma classe de negociação, cujos objetos serão armazenados em listas. E essas listas, por sua vez, estarão nos objetos da classe de negociação usada no testador. Esses objetos de negociação também serão instâncias de uma classe própria, que também serão armazenadas em uma lista. Cada objeto de negociação será identificado por pertencer a um determinado símbolo — ou seja, para cada símbolo envolvido na negociação, haverá um objeto de negociação correspondente. Cada objeto conterá apenas a lista de negociações referentes ao seu símbolo, além de suas próprias instâncias da classe CTrade da Biblioteca Padrão. Isso permitirá configurar cada instância de CTrade de forma individual, de acordo com as condições específicas do símbolo negociado.
Vamos escrever a classe de negociação no arquivo \MQL5\Experts\TradingByHistoryDeals\SymbolTrade.mqh.
//+------------------------------------------------------------------+ //| Deal class. Used for trading in the strategy tester | //+------------------------------------------------------------------+ class CDeal : public CObject { protected: //--- Integer properties ulong m_ticket; // Deal ticket. Unique number assigned to each deal long m_order; // Deal order number datetime m_time; // Deal execution time long m_time_msc; // Deal execution time in milliseconds since 01.01.1970 ENUM_DEAL_TYPE m_type; // Deal type ENUM_DEAL_ENTRY m_entry; // Deal entry - entry in, entry out, reverse long m_magic; // Magic number for a deal (see ORDER_MAGIC) ENUM_DEAL_REASON m_reason; // Deal execution reason or source long m_pos_id; // The ID of the position opened, modified or closed by the deal //--- Real properties double m_volume; // Deal volume double m_price; // Deal price double m_commission; // Deal commission double m_swap; // Accumulated swap when closing double m_profit; // Deal financial result double m_fee; // Fee for making a deal charged immediately after performing a deal double m_sl; // Stop Loss level double m_tp; // Take Profit level //--- String properties string m_symbol; // Name of the symbol for which the deal is executed string m_comment; // Deal comment string m_external_id; // Deal ID in an external trading system (on the exchange) //--- Additional properties int m_digits; // Symbol digits double m_point; // Symbol point ulong m_ticket_tester; // Position ticket in the tester long m_pos_id_tester; // Position ID in the tester public: //--- Set deal propertie void SetTicket(const ulong ticket) { this.m_ticket=ticket; } void SetOrder(const long order) { this.m_order=order; } void SetTime(const datetime time) { this.m_time=time; } void SetTimeMsc(const long value) { this.m_time_msc=value; } void SetType(const ENUM_DEAL_TYPE type) { this.m_type=type; } void SetEntry(const ENUM_DEAL_ENTRY entry) { this.m_entry=entry; } void SetMagic(const long magic) { this.m_magic=magic; } void SetReason(const ENUM_DEAL_REASON reason) { this.m_reason=reason; } void SetPositionID(const long id) { this.m_pos_id=id; } void SetVolume(const double volume) { this.m_volume=volume; } void SetPrice(const double price) { this.m_price=price; } void SetCommission(const double commission) { this.m_commission=commission; } void SetSwap(const double swap) { this.m_swap=swap; } void SetProfit(const double profit) { this.m_profit=profit; } void SetFee(const double fee) { this.m_fee=fee; } void SetSL(const double sl) { this.m_sl=sl; } void SetTP(const double tp) { this.m_tp=tp; } void SetSymbol(const string symbol) { this.m_symbol=symbol; } void SetComment(const string comment) { this.m_comment=comment; } void SetExternalID(const string ext_id) { this.m_external_id=ext_id; } void SetTicketTester(const ulong ticket) { this.m_ticket_tester=ticket; } void SetPosIDTester(const long pos_id) { this.m_pos_id_tester=pos_id; } //--- Return deal properties ulong Ticket(void) const { return this.m_ticket; } long Order(void) const { return this.m_order; } datetime Time(void) const { return this.m_time; } long TimeMsc(void) const { return this.m_time_msc; } ENUM_DEAL_TYPE TypeDeal(void) const { return this.m_type; } ENUM_DEAL_ENTRY Entry(void) const { return this.m_entry; } long Magic(void) const { return this.m_magic; } ENUM_DEAL_REASON Reason(void) const { return this.m_reason; } long PositionID(void) const { return this.m_pos_id; } double Volume(void) const { return this.m_volume; } double Price(void) const { return this.m_price; } double Commission(void) const { return this.m_commission; } double Swap(void) const { return this.m_swap; } double Profit(void) const { return this.m_profit; } double Fee(void) const { return this.m_fee; } double SL(void) const { return this.m_sl; } double TP(void) const { return this.m_tp; } string Symbol(void) const { return this.m_symbol; } string Comment(void) const { return this.m_comment; } string ExternalID(void) const { return this.m_external_id; } int Digits(void) const { return this.m_digits; } double Point(void) const { return this.m_point; } ulong TicketTester(void) const { return this.m_ticket_tester; } long PosIDTester(void) const { return this.m_pos_id_tester; } //--- Compare two objects by the property specified in 'mode' virtual int Compare(const CObject *node, const int mode=0) const { const CDeal *obj=node; switch(mode) { case SORT_MODE_DEAL_TICKET : return(this.Ticket() > obj.Ticket() ? 1 : this.Ticket() < obj.Ticket() ? -1 : 0); case SORT_MODE_DEAL_ORDER : return(this.Order() > obj.Order() ? 1 : this.Order() < obj.Order() ? -1 : 0); case SORT_MODE_DEAL_TIME : return(this.Time() > obj.Time() ? 1 : this.Time() < obj.Time() ? -1 : 0); case SORT_MODE_DEAL_TIME_MSC : return(this.TimeMsc() > obj.TimeMsc() ? 1 : this.TimeMsc() < obj.TimeMsc() ? -1 : 0); case SORT_MODE_DEAL_TYPE : return(this.TypeDeal() > obj.TypeDeal() ? 1 : this.TypeDeal() < obj.TypeDeal() ? -1 : 0); case SORT_MODE_DEAL_ENTRY : return(this.Entry() > obj.Entry() ? 1 : this.Entry() < obj.Entry() ? -1 : 0); case SORT_MODE_DEAL_MAGIC : return(this.Magic() > obj.Magic() ? 1 : this.Magic() < obj.Magic() ? -1 : 0); case SORT_MODE_DEAL_REASON : return(this.Reason() > obj.Reason() ? 1 : this.Reason() < obj.Reason() ? -1 : 0); case SORT_MODE_DEAL_POSITION_ID : return(this.PositionID() > obj.PositionID() ? 1 : this.PositionID() < obj.PositionID() ? -1 : 0); case SORT_MODE_DEAL_VOLUME : return(this.Volume() > obj.Volume() ? 1 : this.Volume() < obj.Volume() ? -1 : 0); case SORT_MODE_DEAL_PRICE : return(this.Price() > obj.Price() ? 1 : this.Price() < obj.Price() ? -1 : 0); case SORT_MODE_DEAL_COMMISSION : return(this.Commission() > obj.Commission() ? 1 : this.Commission() < obj.Commission() ? -1 : 0); case SORT_MODE_DEAL_SWAP : return(this.Swap() > obj.Swap() ? 1 : this.Swap() < obj.Swap() ? -1 : 0); case SORT_MODE_DEAL_PROFIT : return(this.Profit() > obj.Profit() ? 1 : this.Profit() < obj.Profit() ? -1 : 0); case SORT_MODE_DEAL_FEE : return(this.Fee() > obj.Fee() ? 1 : this.Fee() < obj.Fee() ? -1 : 0); case SORT_MODE_DEAL_SL : return(this.SL() > obj.SL() ? 1 : this.SL() < obj.SL() ? -1 : 0); case SORT_MODE_DEAL_TP : return(this.TP() > obj.TP() ? 1 : this.TP() < obj.TP() ? -1 : 0); case SORT_MODE_DEAL_SYMBOL : return(this.Symbol() > obj.Symbol() ? 1 : this.Symbol() < obj.Symbol() ? -1 : 0); case SORT_MODE_DEAL_COMMENT : return(this.Comment() > obj.Comment() ? 1 : this.Comment() < obj.Comment() ? -1 : 0); case SORT_MODE_DEAL_EXTERNAL_ID : return(this.ExternalID() >obj.ExternalID() ? 1 : this.ExternalID() <obj.ExternalID() ? -1 : 0); case SORT_MODE_DEAL_TICKET_TESTER : return(this.TicketTester()>obj.TicketTester()? 1 : this.TicketTester()<obj.TicketTester() ? -1 : 0); case SORT_MODE_DEAL_POS_ID_TESTER : return(this.PosIDTester() >obj.PosIDTester() ? 1 : this.PosIDTester() <obj.PosIDTester() ? -1 : 0); default : return(WRONG_VALUE); } } //--- Constructors/destructor CDeal(const ulong ticket, const string symbol) : m_ticket(ticket), m_symbol(symbol), m_ticket_tester(0), m_pos_id_tester(0) { this.m_digits=(int)::SymbolInfoInteger(symbol, SYMBOL_DIGITS); this.m_point=::SymbolInfoDouble(symbol, SYMBOL_POINT); } CDeal(void) {} ~CDeal(void) {} };
A classe é praticamente idêntica à estrutura de negociação criada anteriormente. Além das propriedades da negociação, foram adicionadas as propriedades Digits e Point do símbolo no qual a negociação foi realizada. Isso facilita a geração da descrição da negociação, pois esses dados são definidos no construtor da negociação logo ao criar o objeto, dispensando a necessidade de buscar essas propriedades para cada negociação individualmente (caso sejam necessárias) sempre que ela for acessada.
Também foi criado aqui um método virtual Compare() para comparar dois objetos de negociação, que será utilizado para ordenar as listas de negociações na busca pela negociação desejada com base em uma propriedade definida.
Agora vamos criar a classe de negociação por símbolo. A classe armazenará uma lista de negociações realizadas para o símbolo especificado nas propriedades do objeto, e será a partir dela que essas negociações serão solicitadas pelo testador para cópia. Em resumo, essa classe servirá como base para copiar no testador as negociações que foram realizadas na conta para o símbolo em questão:
//+------------------------------------------------------------------+ //| Class for trading by symbol | //+------------------------------------------------------------------+ CDeal DealTmp; // Temporary deal object for searching by properties class CSymbolTrade : public CObject { private: int m_index_next_deal; // Index of the next deal that has not yet been handled int m_deals_processed; // Number of handled deals protected: MqlTick m_tick; // Tick structure CArrayObj m_list_deals; // List of deals carried out by symbol CTrade m_trade; // Trading class string m_symbol; // Symbol name public: //--- Return the list of deals CArrayObj *GetListDeals(void) { return(&this.m_list_deals); } //--- Set a symbol void SetSymbol(const string symbol) { this.m_symbol=symbol; } //--- (1) Set and (2) returns the number of handled deals void SetNumProcessedDeals(const int num) { this.m_deals_processed=num; } int NumProcessedDeals(void) const { return this.m_deals_processed; } //--- Add a deal to the deal array bool AddDeal(CDeal *deal); //--- Return the deal (1) by time in seconds, (2) by index in the list, //--- (3) opening deal by position ID, (4) current deal in the list CDeal *GetDealByTime(const datetime time); CDeal *GetDealByIndex(const int index); CDeal *GetDealInByPosID(const long pos_id); CDeal *GetDealCurrent(void); //--- Return (1) the number of deals in the list, (2) the index of the current deal in the list int DealsTotal(void) const { return this.m_list_deals.Total(); } int DealCurrentIndex(void) const { return this.m_index_next_deal; } //--- Return (1) symbol and (2) object description string Symbol(void) const { return this.m_symbol; } string Description(void) const { return ::StringFormat("%s trade object. Total deals: %d", this.Symbol(), this.DealsTotal() ); } //--- Return the current (1) Bid and (2) Ask price, time in (3) seconds, (4) milliseconds double Bid(void); double Ask(void); datetime Time(void); long TimeMsc(void); //--- Open (1) long, (2) short position, (3) close a position by ticket ulong Buy(const double volume, const ulong magic, const double sl, const double tp, const string comment); ulong Sell(const double volume, const ulong magic, const double sl, const double tp, const string comment); bool ClosePos(const ulong ticket); //--- Return the result of comparing the current time with the specified one bool CheckTime(const datetime time) { return(this.Time()>=time); } //--- Sets the index of the next deal void SetNextDealIndex(void) { this.m_index_next_deal++; } //--- OnTester handler. Returns the number of deals processed by the tester. double OnTester(void) { ::PrintFormat("Symbol %s: Total deals: %d, number of processed deals: %d", this.Symbol(), this.DealsTotal(), this.NumProcessedDeals()); return this.m_deals_processed; } //--- Compares two objects to each other (comparison by symbol only) virtual int Compare(const CObject *node, const int mode=0) const { const CSymbolTrade *obj=node; return(this.Symbol()>obj.Symbol() ? 1 : this.Symbol()<obj.Symbol() ? -1 : 0); } //--- Constructors/destructor CSymbolTrade(void) : m_index_next_deal(0), m_deals_processed(0) {} CSymbolTrade(const string symbol) : m_symbol(symbol), m_index_next_deal(0), m_deals_processed(0) { this.m_trade.SetMarginMode(); this.m_trade.SetTypeFillingBySymbol(this.m_symbol); } ~CSymbolTrade(void) {} };
Vamos analisar alguns métodos.
- SetNumProcessedDeals() e NumProcessedDeals() definem e retornam a quantidade de negociações históricas já processadas pelo testador a partir da lista de negociações carregada do arquivo. São necessárias para controlar a correta manipulação das negociações históricas e obter a estatística final da quantidade de negociações processadas pelo testador;
- GetDealCurrent() retorna o ponteiro para a negociação histórica atual, que deve ser processada pelo testador e marcada como processada;
- DealCurrentIndex() retorna o índice da negociação histórica selecionada para ser processada pelo testador no momento atual;
- SetNextDealIndex(), após o término da análise da negociação histórica atual, define o índice da próxima negociação a ser processada pelo testador. Como todas as negociações históricas na lista estão ordenadas por tempo em milissegundos, isso define o índice da negociação seguinte após o processamento da anterior pelo testador. Assim, percorremos sequencialmente todas as negociações do histórico, que serão processadas pelo testador no instante em que o tempo registrado nas propriedades da negociação atual for atingido;
- CheckTime() verifica o momento em que o tempo do testador alcança o valor registrado nas propriedades da negociação histórica atual. A lógica é a seguinte: há uma negociação selecionada que precisa ser processada no testador. Enquanto o tempo no testador for menor que o da negociação, não fazemos nada, apenas avançamos para o próximo tick. Assim que o tempo no testador for igual ou maior que o tempo da negociação atual (o tempo pode não coincidir exatamente, por isso também se verifica "maior que"), a negociação é processada no testador de acordo com seu tipo e forma de modificação da posição. Em seguida, essa negociação é marcada como processada, o índice da próxima negociação é definido, e o processo de espera pelo tempo, controlado por esse método, continua, agora para a próxima negociação:
- O manipulador OnTester() é chamado a partir do manipulador padrão OnTester() do EA, imprime no log o nome do símbolo, a quantidade de negociações históricas e as processadas pelo testador, e retorna a quantidade de negociações processadas para o símbolo do objeto de negociação;
A classe possui dois construtores: o padrão e um parametrizado.
No construtor parametrizado, é passado como parâmetro o nome do símbolo do objeto de negociação, que é definido ao criar o objeto, e no objeto da classe CTrade são configurados o modo de cálculo de margem,de acordo com as configurações da conta atual, e o tipo de ordem por execução,conforme as configurações do símbolo do objeto de negociação:
//--- Constructors/destructor CSymbolTrade(void) : m_index_next_deal(0), m_deals_processed(0) {} CSymbolTrade(const string symbol) : m_symbol(symbol), m_index_next_deal(0), m_deals_processed(0) { this.m_trade.SetMarginMode(); this.m_trade.SetTypeFillingBySymbol(this.m_symbol); }
Método que adiciona uma negociação ao array de negociações:
//+------------------------------------------------------------------+ //| CSymbolTrade::Add a trade to the trades array | //+------------------------------------------------------------------+ bool CSymbolTrade::AddDeal(CDeal *deal) { //--- If the list already contains a deal with the deal ticket passed to the method, return 'true' this.m_list_deals.Sort(SORT_MODE_DEAL_TICKET); if(this.m_list_deals.Search(deal)>WRONG_VALUE) return true; //--- Add a pointer to the deal to the list in sorting order by time in milliseconds this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC); if(!this.m_list_deals.InsertSort(deal)) { ::PrintFormat("%s: Failed to add deal", __FUNCTION__); return false; } //--- All is successful return true; }
O método recebe um ponteiro para o objeto da negociação. Se uma negociação com o mesmo ticket já estiver presente na lista, apenas retorna true. Caso contrário, a lista é ordenada pelo tempo das negociações em milissegundos e a nova negociação é adicionada à lista em ordem cronológica.
Método que retorna o ponteiro para o objeto da negociação com base no tempo em segundos:
//+------------------------------------------------------------------+ //| CSymbolTrade::Return the deal object by time in seconds | //+------------------------------------------------------------------+ CDeal* CSymbolTrade::GetDealByTime(const datetime time) { DealTmp.SetTime(time); this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC); int index=this.m_list_deals.Search(&DealTmp); return this.m_list_deals.At(index); }
O método recebe o tempo desejado. Define esse tempo em um objeto de negociação temporário, ordena a lista pelo tempo em milissegundos e busca o índice da negociação cujo tempo coincide com o fornecido (o tempo configurado no objeto temporário). Em seguida, retorna o ponteiro para a negociação na lista com base no índice encontrado. Se não houver negociação com esse tempo na lista, o índice será -1 e o método retornará NULL.
Curiosamente, a negociação é buscada pelo tempo em segundos, mas a lista é ordenada pelo tempo em milissegundos. Os testes mostraram que, se a lista também for ordenada por segundos, algumas negociações não são encontradas, apesar de estarem presentes. Isso provavelmente ocorre porque há várias negociações em um mesmo segundo, com tempos diferentes em milissegundos. E o ponteiro retornado pode apontar para uma negociação já processada anteriormente, já que várias têm o mesmo tempo em segundos.
Método que retorna o ponteiro para a negociação de abertura com base no identificador da posição:
//+------------------------------------------------------------------+ //|CSymbolTrade::Return the opening trade by position ID | //+------------------------------------------------------------------+ CDeal *CSymbolTrade::GetDealInByPosID(const long pos_id) { int total=this.m_list_deals.Total(); for(int i=0; i<total; i++) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL || deal.PositionID()!=pos_id) continue; if(deal.Entry()==DEAL_ENTRY_IN) return deal; } return NULL; }
O método recebe o identificador da posição cuja negociação de abertura deve ser localizada. Em seguida, percorremos a lista de negociações e obtemos a negociação cujo identificador de posição seja igual ao passado para o método, e retornamos o ponteiro para a negociação cujo modo de modificação da posição seja "Entrada no mercado" (DEAL_ENTRY_IN).
Método que retorna o ponteiro para o objeto da negociação pelo índice na lista:
//+------------------------------------------------------------------+ //| CSymbolTrade::Return the deal object by index in the list | //+------------------------------------------------------------------+ CDeal *CSymbolTrade::GetDealByIndex(const int index) { return this.m_list_deals.At(index); }
Apenas retornamos o ponteiro para o objeto na lista conforme o índice passado ao método. Se o índice for inválido, será retornado NULL.
Método que retorna o ponteiro para a negociação indicada pelo índice da negociação atual:
//+------------------------------------------------------------------+ //| Return the deal pointed to by the current deal index | //+------------------------------------------------------------------+ CDeal *CSymbolTrade::GetDealCurrent(void) { this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC); return this.GetDealByIndex(this.m_index_next_deal); }
A lista de negociações é ordenada por tempo em milissegundos, e é retornado o ponteiro para a negociação cujo índice está armazenado na variável de classe m_index_next_deal.
Método que retorna o preço atual de Bid:
//+------------------------------------------------------------------+ //| CSymbolTrade::Return the current Bid price | //+------------------------------------------------------------------+ double CSymbolTrade::Bid(void) { ::ResetLastError(); if(!::SymbolInfoTick(this.m_symbol, this.m_tick)) { ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError()); return 0; } return this.m_tick.bid; }
Obtém os dados do último tick na estrutura de preços m_tick e retorna o preço Bid a partir dela.
Método que retorna o preço atual de Ask:
//+------------------------------------------------------------------+ //| CSymbolTrade::Return the current Ask price | //+------------------------------------------------------------------+ double CSymbolTrade::Ask(void) { ::ResetLastError(); if(!::SymbolInfoTick(this.m_symbol, this.m_tick)) { ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError()); return 0; } return this.m_tick.ask; }
Obtém os dados do último tick na estrutura de preços m_tick e retorna o preço Ask a partir dela.
Método que retorna o tempo atual em segundos:
//+------------------------------------------------------------------+ //| CSymbolTrade::Return the current time in seconds | //+------------------------------------------------------------------+ datetime CSymbolTrade::Time(void) { ::ResetLastError(); if(!::SymbolInfoTick(this.m_symbol, this.m_tick)) { ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError()); return 0; } return this.m_tick.time; }
Obtém os dados do último tick na estrutura de preços m_tick e retorna a hora em segundos.
Método que retorna o tempo atual em milissegundos:
//+------------------------------------------------------------------+ //| CSymbolTrade::Return the current time in milliseconds | //+------------------------------------------------------------------+ long CSymbolTrade::TimeMsc(void) { ::ResetLastError(); if(!::SymbolInfoTick(this.m_symbol, this.m_tick)) { ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError()); return 0; } return this.m_tick.time_msc; }
Obtém os dados do último tick na estrutura de preços m_tick e retorna a hora em milissegundos.
Método que abre uma posição de compra:
//+------------------------------------------------------------------+ //| CSymbolTrade::Open a long position | //+------------------------------------------------------------------+ ulong CSymbolTrade::Buy(const double volume, const ulong magic, const double sl, const double tp, const string comment) { this.m_trade.SetExpertMagicNumber(magic); if(!this.m_trade.Buy(volume, this.m_symbol, 0, sl, tp, comment)) { return 0; } return this.m_trade.ResultOrder(); }
O método recebe os parâmetros da posição de compra a ser aberta, define o magic number da posição no objeto de negociação e envia uma ordem de compra com os parâmetros indicados. Em caso de erro ao abrir a posição, retorna zero; em caso de sucesso, retorna o ticket da ordem que originou a posição.
Método que abre uma posição de venda:
//+------------------------------------------------------------------+ //| CSymbolTrade::Open a short position | //+------------------------------------------------------------------+ ulong CSymbolTrade::Sell(const double volume, const ulong magic, const double sl, const double tp, const string comment) { this.m_trade.SetExpertMagicNumber(magic); if(!this.m_trade.Sell(volume, this.m_symbol, 0, sl, tp, comment)) { return 0; } return this.m_trade.ResultOrder(); }
Semelhante ao método anterior, mas abre uma posição de venda.
Método que fecha a posição com base no ticket:
//+------------------------------------------------------------------+ //| CSymbolTrade::Close position by ticket | //+------------------------------------------------------------------+ bool CSymbolTrade::ClosePos(const ulong ticket) { return this.m_trade.PositionClose(ticket); }
Retorna o resultado da chamada do método PositionClose() do objeto de negociação da classe CTrade.
A classe de negociação por símbolo está pronta. Agora vamos implementá-la no EA para trabalhar com as negociações históricas salvas no arquivo.
Analisando o histórico de negociações a partir de um arquivo no testador
Vamos ao arquivo do EA \MQL5\Experts\TradingByHistoryDeals\TradingByHistoryDeals.mq5 e declaramos um objeto temporário da classe de negociação por símbolo recém-criada, que será necessário para buscar o objeto correto na lista onde serão armazenados os ponteiros para esses objetos:
//+------------------------------------------------------------------+ //| Expert | //+------------------------------------------------------------------+ //--- input parameters input string InpTestedSymbol = ""; /* The symbol being tested in the tester */ input long InpTestedMagic = -1; /* The magic number being tested in the tester */ sinput bool InpShowDataInLog = false; /* Show collected data in the log */ //--- global variables CSymbolTrade SymbTradeTmp; SDeal ExtArrayDeals[]={}; CArrayObj ExtListSymbols;
Temos um array de negociações históricas, a partir do qual podemos criar uma lista de objetos de negociação, dentro dos quais estarão as listas de negociações pertencentes ao símbolo de cada objeto. No array de negociações estão armazenadas estruturas que descrevem as negociações. Como no objeto de negociação teremos listas de objetos de negociação, precisamos criar uma função que crie um novo objeto de negociação e preencha suas propriedades com os valores dos campos da estrutura que descreve a negociação:
//+------------------------------------------------------------------+ //| Create a deal object from the structure | //+------------------------------------------------------------------+ CDeal *CreateDeal(SDeal &deal_str) { //--- If failed to create an object, inform of the error in the journal and return NULL CDeal *deal=new CDeal(deal_str.ticket, deal_str.Symbol()); if(deal==NULL) { PrintFormat("%s: Error. Failed to create deal object"); return NULL; } //--- fill in the deal properties from the structure fields deal.SetOrder(deal_str.order); // Order the deal was based on deal.SetPositionID(deal_str.pos_id); // Position ID deal.SetTimeMsc(deal_str.time_msc); // Time in milliseconds deal.SetTime(deal_str.time); // Time deal.SetVolume(deal_str.volume); // Volume deal.SetPrice(deal_str.price); // Price deal.SetProfit(deal_str.profit); // Profit deal.SetCommission(deal_str.commission); // Deal commission deal.SetSwap(deal_str.swap); // Accumulated swap when closing deal.SetFee(deal_str.fee); // Fee for making a deal charged immediately after performing a deal deal.SetSL(deal_str.sl); // Stop Loss level deal.SetTP(deal_str.tp); // Take Profit level deal.SetType(deal_str.type); // Type deal.SetEntry(deal_str.entry); // Position change method deal.SetReason(deal_str.reason); // Deal execution reason or source deal.SetMagic(deal_str.magic); // EA ID deal.SetComment(deal_str.Comment()); // Deal comment deal.SetExternalID(deal_str.ExternalID()); // Deal ID in an external trading system (on the exchange) //--- Return the pointer to a created object return deal; }
A estrutura da negociação é passada para a função, um novo objeto de negociação é criado e suas propriedades são preenchidas com os valores da estrutura.
A função retorna o ponteiro para o objeto recém-criado. Em caso de erro na criação do objeto, retorna NULL.
Vamos escrever a função que cria a lista de objetos de negociação por símbolo:
//+------------------------------------------------------------------+ //| Create an array of used symbols | //+------------------------------------------------------------------+ bool CreateListSymbolTrades(SDeal &array_deals[], CArrayObj *list_symbols) { bool res=true; // result int total=(int)array_deals.Size(); // total number of deals in the array //--- if the deal array is empty, return 'false' if(total==0) { PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__); return false; } //--- in a loop through the deal array CSymbolTrade *SymbolTrade=NULL; for(int i=0; i<total; i++) { //--- get the next deal and, if it is neither buy nor sell, move on to the next one SDeal deal_str=array_deals[i]; if(deal_str.type!=DEAL_TYPE_BUY && deal_str.type!=DEAL_TYPE_SELL) continue; //--- find a trading object in the list whose symbol is equal to the deal symbol string symbol=deal_str.Symbol(); SymbTradeTmp.SetSymbol(symbol); list_symbols.Sort(); int index=list_symbols.Search(&SymbTradeTmp); //--- if the index of the desired object in the list is -1, there is no such object in the list if(index==WRONG_VALUE) { //--- we create a new trading symbol object and, if creation fails, //--- add 'false' to the result and move on to the next deal SymbolTrade=new CSymbolTrade(symbol); if(SymbolTrade==NULL) { res &=false; continue; } //--- if failed to add a symbol trading object to the list, //--- delete the newly created object, add 'false' to the result //--- and we move on to the next deal if(!list_symbols.Add(SymbolTrade)) { delete SymbolTrade; res &=false; continue; } } //--- otherwise, if the trading object already exists in the list, we get it by index else { SymbolTrade=list_symbols.At(index); if(SymbolTrade==NULL) continue; } //--- if the current deal is not yet in the list of deals of the symbol trading object if(SymbolTrade.GetDealByTime(deal_str.time)==NULL) { //--- create a deal object according to its sample structure CDeal *deal=CreateDeal(deal_str); if(deal==NULL) { res &=false; continue; } //--- add the result of adding the deal object to the list of deals of a symbol trading object to the result value res &=SymbolTrade.AddDeal(deal); } } //--- return the final result of creating trading objects and adding deals to their lists return res; }
A lógica da função está explicada detalhadamente nos comentários. Em um loop sobre a lista de negociações históricas, analisamos cada negociação. Verificamos seu símbolo e, se ainda não houver um objeto de negociação para esse símbolo, criamos um novo objeto de negociação e o salvamos na lista. Se já houver, apenas obtemos o ponteiro para o objeto de negociação correspondente ao símbolo da negociação a partir da lista. Depois, verificamos se essa negociação já existe na lista de negociações do objeto de negociação e a adicionamos se ela ainda não estiver presente. Como resultado do loop sobre todas as negociações históricas, obtemos uma lista de objetos de negociação por símbolo, dentro dos quais estão listas contendo as negociações do respectivo símbolo.
A lista de objetos de negociação pode ser exibida no log com a ajuda da função:
//+------------------------------------------------------------------+ //| Display a list of symbol trading objects in the journal | //+------------------------------------------------------------------+ void SymbolsArrayPrint(CArrayObj *list_symbols) { int total=list_symbols.Total(); if(total==0) return; Print("Symbols used in trading:"); for(int i=0; i<total; i++) { string index=StringFormat("% 3d", i+1); CSymbolTrade *obj=list_symbols.At(i); if(obj==NULL) continue; PrintFormat("%s. %s",index, obj.Description()); } }
Em um loop sobre a lista de objetos de negociação por símbolo, obtemos cada objeto e imprimimos sua descrição no log. No log, isso aparece mais ou menos assim:
Symbols used in trading: 1. AUDUSD trade object. Total deals: 218 2. EURJPY trade object. Total deals: 116 3. EURUSD trade object. Total deals: 524 4. GBPUSD trade object. Total deals: 352 5. NZDUSD trade object. Total deals: 178 6. USDCAD trade object. Total deals: 22 7. USDCHF trade object. Total deals: 250 8. USDJPY trade object. Total deals: 142 9. XAUUSD trade object. Total deals: 118
Agora temos um objeto da classe de negociação. Vamos adicionar uma função que retorna a descrição da negociação:
//+------------------------------------------------------------------+ //| Return deal description | //+------------------------------------------------------------------+ string DealDescription(CDeal *deal, const int index) { string indexs=StringFormat("% 5d", index); if(deal.TypeDeal()!=DEAL_TYPE_BALANCE) return(StringFormat("%s: deal #%I64u %s, type %s, Position #%I64d %s (magic %I64d), Price %.*f at %s, sl %.*f, tp %.*f", indexs, deal.Ticket(), DealEntryDescription(deal.Entry()), DealTypeDescription(deal.TypeDeal()), deal.PositionID(), deal.Symbol(), deal.Magic(), deal.Digits(), deal.Price(), TimeToString(deal.Time(), TIME_DATE|TIME_MINUTES|TIME_SECONDS), deal.Digits(), deal.SL(), deal.Digits(), deal.TP())); else return(StringFormat("%s: deal #%I64u %s, type %s %.2f %s at %s", indexs, deal.Ticket(), DealEntryDescription(deal.Entry()), DealTypeDescription(deal.TypeDeal()), deal.Profit(), AccountInfoString(ACCOUNT_CURRENCY), TimeToString(deal.Time()))); }
Essa função repete exatamente a lógica da função que retorna a descrição da estrutura de negociação. Mas aqui, em vez da estrutura, é passado um ponteiro para o objeto da negociação.
Agora vamos finalizar logicamente o manipulador OnInit().
Adicionamos o tratamento da inicialização do EA no testador, criamos a lista de objetos de negociação e acessamos cada símbolo utilizado na negociação para carregar seu histórico e abrir os gráficos desses símbolos no testador:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- If the EA is not running in the tester if(!MQLInfoInteger(MQL_TESTER)) { //--- prepare a file with all historical deals if(!PreparesDealsHistoryFile(ExtArrayDeals)) return(INIT_FAILED); //--- print all deals in the journal after loading them from the file if(InpShowDataInLog) DealsArrayPrint(ExtArrayDeals); //--- get the first balance deal, create the message text and display it using Alert SDeal deal=ExtArrayDeals[0]; long leverage=AccountInfoInteger(ACCOUNT_LEVERAGE); double start_money=deal.profit; datetime first_time=deal.time; string start_time=TimeToString(deal.time, TIME_DATE); string message=StringFormat("Now you can run testing\nInterval: %s - current date\nInitial deposit: %.2f, leverage 1:%I64u", start_time, start_money, leverage); //--- notify via alert of the recommended parameters of the strategy tester for starting the test Alert(message); } //--- The EA has been launched in the tester else { //--- read data from the file into the array ulong file_size=0; ArrayResize(ExtArrayDeals, 0); if(!FileReadDealsToArray(ExtArrayDeals, file_size)) { PrintFormat("Failed to read file \"%s\". Error %d", FILE_NAME, GetLastError()); return(INIT_FAILED); } //--- report the number of bytes read from the file and writing the deals array in the journal. PrintFormat("%I64u bytes were read from the file \"%s\" and written to the deals array. A total of %u deals were received", file_size, FILE_NAME, ExtArrayDeals.Size()); } //--- Create a list of trading objects by symbols from the array of historical deals if(!CreateListSymbolTrades(ExtArrayDeals, &ExtListSymbols)) { Print("Errors found while creating symbol list"); return(INIT_FAILED); } //--- Print the created list of deals in the journal SymbolsArrayPrint(&ExtListSymbols); //--- Access each symbol to start downloading historical data //--- and opening charts of traded symbols in the strategy tester datetime array[]; int total=ExtListSymbols.Total(); for(int i=0; i<total; i++) { CSymbolTrade *obj=ExtListSymbols.At(i); if(obj==NULL) continue; CopyTime(obj.Symbol(), PERIOD_CURRENT, 0, 1, array); } //--- All is successful return(INIT_SUCCEEDED); }
No manipulador OnDeinit() do EA, limpamos os arrays e listas criados:
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- clear the created lists and arrays ExtListSymbols.Clear(); ArrayFree(ExtArrayDeals); }
No manipulador OnTick() do EA, no testador, processamos a lista de negociações a partir do arquivo:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- work only in the strategy tester if(!MQLInfoInteger(MQL_TESTER)) return; //--- Handle the list of deals from the file in the tester TradeByHistory(InpTestedSymbol, InpTestedMagic); }
Vamos analisar essa função com mais detalhes. De modo geral, a lógica para o processamento das negociações do histórico foi inicialmente pensada assim:
- obtemos o tempo do tick,
- obtemos a negociação correspondente a esse tempo,
- processamos a negociação no testador.
À primeira vista, o esquema parece simples e lógico, mas sua implementação resultou em um completo fracasso. A razão é que, no testador, o tempo do tick nem sempre coincide com o tempo da negociação. Mesmo em milissegundos. E, durante testes com todos os ticks — baseados em ticks reais obtidos do mesmo servidor — negociações eram perdidas. Sabíamos exatamente o horário do tick, sabíamos com certeza que havia uma negociação nesse exato momento, mas o testador não a encontrava, pois não havia um tick com aquele horário exato da negociação. Em vez disso, havia ticks com tempo anterior e posterior ao tempo da negociação. Portanto, a lógica não pode se basear nos ticks e seus tempos, mas sim nas próprias negociações:
- as negociações estão ordenadas na lista por ordem cronológica em milissegundos. Definimos o índice da primeira negociação como o índice atual;
- selecionamos a negociação pelo índice atual e obtemos seu tempo;
- aguardamos um tick com esse tempo:
- se o tempo do tick for menor que o da negociação — aguardamos o próximo tick,
- se o tempo do tick for igual ou maior que o da negociação — processamos a negociação, registramos nela que já foi processada e definimos o índice da próxima negociação como o índice atual;
- enquanto o teste não terminar, repetimos a partir do passo 2.
Esse esquema permite aguardar o momento de cada negociação subsequente e executá-la no testador. Nesse processo, não damos atenção ao preço da negociação, apenas copiamos as negociações com base na chegada do seu tempo. Mesmo que o tempo do tick no testador já esteja um pouco à frente do da negociação real, isso não é um problema. O importante é replicar a negociação. O indicativo de que a negociação já foi processada pelo testador será um valor diferente de zero na propriedade "ticket da posição no testador". Se esse valor for zero, isso indica que a negociação ainda não foi processada no testador. Após a execução dessa negociação no testador, o ticket da posição correspondente é gravado nesse campo da negociação.
Vamos escrever uma função com a lógica acima descrita:
//+------------------------------------------------------------------+ //| Trading by history | //+------------------------------------------------------------------+ void TradeByHistory(const string symbol="", const long magic=-1) { datetime time=0; int total=ExtListSymbols.Total(); // number of trading objects in the list //--- in a loop by all symbol trading objects for(int i=0; i<total; i++) { //--- get another trading object CSymbolTrade *obj=ExtListSymbols.At(i); if(obj==NULL) continue; //--- get the current deal pointed to by the deal list index CDeal *deal=obj.GetDealCurrent(); if(deal==NULL) continue; //--- sort the deal by magic number and symbol if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol)) continue; //--- sort the deal by type (only buy/sell deals) ENUM_DEAL_TYPE type=deal.TypeDeal(); if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL) continue; //--- if this is a deal already handled in the tester, move on to the next one if(deal.TicketTester()>0) continue; //--- if the deal time has not yet arrived, move to the next trading object of the next symbol if(!obj.CheckTime(deal.Time())) continue; //--- in case of a market entry deal ENUM_DEAL_ENTRY entry=deal.Entry(); if(entry==DEAL_ENTRY_IN) { //--- open a position by deal type double sl=0; double tp=0; ulong ticket=(type==DEAL_TYPE_BUY ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) : type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : 0); //--- if a position is opened (we received its ticket) if(ticket>0) { //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket); //--- get the position ID in the tester and write it to the properties of the deal object long pos_id_tester=0; if(HistoryDealSelect(ticket)) { pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID); deal.SetPosIDTester(pos_id_tester); } } } //--- in case of a market exit deal if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY) { //--- get a deal a newly opened position is based on CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID()); if(deal_in==NULL) continue; //--- get the position ticket in the tester from the properties of the opening deal //--- if the ticket is zero, then most likely the position in the tester is already closed ulong ticket_tester=deal_in.TicketTester(); if(ticket_tester==0) { PrintFormat("Could not get position ticket, apparently position #%I64d (#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester()); obj.SetNextDealIndex(); continue; } //--- if the position is closed by ticket if(obj.ClosePos(ticket_tester)) { //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket_tester); } } //--- if a ticket is now set in the deal object, then the deal has been successfully handled - //--- set the deal index in the list to the next deal if(deal.TicketTester()>0) { obj.SetNextDealIndex(); } } }
Esse código copia fielmente a negociação original que foi realizada na conta e cujas negociações foram salvas no arquivo. Todas as posições são abertas sem ordens de stop. Ou seja, os valores de StopLoss e TakeProfit não são copiados das negociações reais para os métodos de abertura de posições. Isso facilita o rastreamento das negociações, pois as ordens de fechamento também estão presentes na lista, e o testador as executa, independentemente de a posição ter sido encerrada por StopLoss ou por TakeProfit.
Compilamos o EA e o executamos no gráfico. Como resultado, será criado o arquivo HistoryDealsData.bin, na pasta comum dos terminais cliente, no caminho do tipo "C:\Users\UserName\AppData\Roaming\MetaQuotes\Terminal\Common\Files", dentro da subpasta TradingByHistoryDeals, e será exibido no gráfico um alerta com uma mensagem sobre as configurações recomendadas do testador:

Vamos agora executar o EA no testador, escolhendo nas configurações do testador o intervalo de datas especificado, o depósito inicial e a alavancagem:

O teste será executado para todos os símbolos e magic numbers negociados:

Verificamos que toda a negociação resultou em um prejuízo de 550 dólares. Interessante… e se tivéssemos usado outros stop orders?
Vamos verificar.
Vamos ajustar os stop orders
Salvamos o EA na mesma pasta \MQL5\Experts\TradingByHistoryDeals\ com um novo nome TradingByHistoryDeals_SLTP.mq5.
Adicionamos a enumeração dos modos de teste, dividimos os parâmetros de entrada em grupos, acrescentando um grupo para configuração dos parâmetros de stop order, e duas novas variáveis de nível global para transmitir os valores de StopLoss e TakeProfit aos objetos de negociação:
//+------------------------------------------------------------------+ //| TradingByHistoryDeals_SLTP.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include "SymbolTrade.mqh" enum ENUM_TESTING_MODE { TESTING_MODE_ORIGIN, /* Original trading */ TESTING_MODE_SLTP, /* Specified StopLoss and TakeProfit values */ }; //+------------------------------------------------------------------+ //| Expert | //+------------------------------------------------------------------+ //--- input parameters input group "Strategy parameters" input string InpTestedSymbol = ""; /* The symbol being tested in the tester */ input long InpTestedMagic = -1; /* The magic number being tested in the tester */ sinput bool InpShowDataInLog = false; /* Show collected data in the log */ input group "Stops parameters" input ENUM_TESTING_MODE InpTestingMode = TESTING_MODE_ORIGIN; /* Testing Mode */ input int InpStopLoss = 300; /* StopLoss in points */ input int InpTakeProfit = 500; /* TakeProfit in points */ //--- global variables CSymbolTrade SymbTradeTmp; SDeal ExtArrayDeals[]={}; CArrayObj ExtListSymbols; int ExtStopLoss; int ExtTakeProfit;
No manipulador OnInit(), ajustamos e gravamos nas variáveis os valores de stop orders definidos pelos parâmetros de entrada:
int OnInit() { //--- Adjust the stops ExtStopLoss =(InpStopLoss<1 ? 0 : InpStopLoss); ExtTakeProfit=(InpTakeProfit<1 ? 0 : InpTakeProfit); //--- If the EA is not running in the tester
Adicionamos funções que calculam valores corretos para os preços de StopLoss e TakeProfit com base no nível StopLevel definido para o símbolo:
//+------------------------------------------------------------------+ //| Return correct StopLoss relative to StopLevel | //+------------------------------------------------------------------+ double CorrectStopLoss(const string symbol_name, const ENUM_ORDER_TYPE order_type, const int stop_loss, const int spread_multiplier=2) { if(stop_loss==0 || (order_type!=ORDER_TYPE_BUY && order_type!=ORDER_TYPE_SELL)) return 0; int lv=StopLevel(symbol_name, spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name, SYMBOL_DIGITS); double pt=SymbolInfoDouble(symbol_name, SYMBOL_POINT); double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name, SYMBOL_BID) : SymbolInfoDouble(symbol_name, SYMBOL_ASK)); return (order_type==ORDER_TYPE_BUY ? NormalizeDouble(fmin(price-lv*pt, price-stop_loss*pt), dg) : NormalizeDouble(fmax(price+lv*pt, price+stop_loss*pt), dg) ); } //+------------------------------------------------------------------+ //| Return correct TakeProfit relative to StopLevel | //+------------------------------------------------------------------+ double CorrectTakeProfit(const string symbol_name, const ENUM_ORDER_TYPE order_type, const int take_profit, const int spread_multiplier=2) { if(take_profit==0 || (order_type!=ORDER_TYPE_BUY && order_type!=ORDER_TYPE_SELL)) return 0; int lv=StopLevel(symbol_name, spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name, SYMBOL_DIGITS); double pt=SymbolInfoDouble(symbol_name, SYMBOL_POINT); double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name, SYMBOL_BID) : SymbolInfoDouble(symbol_name, SYMBOL_ASK)); return (order_type==ORDER_TYPE_BUY ? NormalizeDouble(fmax(price+lv*pt, price+take_profit*pt), dg) : NormalizeDouble(fmin(price-lv*pt, price-take_profit*pt), dg) ); } //+------------------------------------------------------------------+ //| Return StopLevel in points | //+------------------------------------------------------------------+ int StopLevel(const string symbol_name, const int spread_multiplier) { int spread=(int)SymbolInfoInteger(symbol_name, SYMBOL_SPREAD); int stop_level=(int)SymbolInfoInteger(symbol_name, SYMBOL_TRADE_STOPS_LEVEL); return(stop_level==0 ? spread*spread_multiplier : stop_level); }
Para definir os níveis de StopLoss e TakeProfit, o preço do stop order não pode estar a uma distância inferior ao StopLevel em relação ao preço atual. Se o nível StopLevel do símbolo for zero, isso significa que será usado um valor equivalente a duas, às vezes três vezes o tamanho do spread do símbolo. Nessas funções, é usado um multiplicador duplo do spread. Esse valor é passado como parâmetro formal para as funções, e tem valor padrão de 2. Se for necessário alterar o valor do multiplicador, basta informar outro valor ao chamar a função. As funções retornam preços válidos para StopLoss e TakeProfit.
Na função de negociação pelo histórico TradeByHistory(), incluímos novos blocos de código que consideram o modo de negociação no testador e a definição dos valores de StopLoss e TakeProfit caso tenha sido selecionado o teste com esses valores específicos. No bloco de fechamento de posições, as posições devem ser encerradas apenas no modo de teste "negociação original". Se for selecionado o teste com valores definidos de stop order, as negociações de fechamento devem ser ignoradas — o próprio testador encerrará as posições com base nos valores de StopLoss e TakeProfit configurados. A única ação necessária ao processar uma negociação de fechamento com stop order é marcá-la como processada e passar para a próxima negociação.
//+------------------------------------------------------------------+ //| Trading by history | //+------------------------------------------------------------------+ void TradeByHistory(const string symbol="", const long magic=-1) { datetime time=0; int total=ExtListSymbols.Total(); // number of trading objects in the list //--- in a loop by all symbol trading objects for(int i=0; i<total; i++) { //--- get another trading object CSymbolTrade *obj=ExtListSymbols.At(i); if(obj==NULL) continue; //--- get the current deal pointed to by the deal list index CDeal *deal=obj.GetDealCurrent(); if(deal==NULL) continue; //--- sort the deal by magic number and symbol if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol)) continue; //--- sort the deal by type (only buy/sell deals) ENUM_DEAL_TYPE type=deal.TypeDeal(); if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL) continue; //--- if this is a deal already handled in the tester, move on to the next one if(deal.TicketTester()>0) continue; //--- if the deal time has not yet arrived, move to the next trading object of the next symbol if(!obj.CheckTime(deal.Time())) continue; //--- in case of a market entry deal ENUM_DEAL_ENTRY entry=deal.Entry(); if(entry==DEAL_ENTRY_IN) { //--- set the sizes of stop orders depending on the stop setting method double sl=0; double tp=0; if(InpTestingMode==TESTING_MODE_SLTP) { ENUM_ORDER_TYPE order_type=(deal.TypeDeal()==DEAL_TYPE_BUY ? ORDER_TYPE_BUY : ORDER_TYPE_SELL); sl=CorrectStopLoss(deal.Symbol(), order_type, ExtStopLoss); tp=CorrectTakeProfit(deal.Symbol(), order_type, ExtTakeProfit); } //--- open a position by deal type ulong ticket=(type==DEAL_TYPE_BUY ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) : type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : 0); //--- if a position is opened (we received its ticket) if(ticket>0) { //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket); //--- get the position ID in the tester and write it to the properties of the deal object long pos_id_tester=0; if(HistoryDealSelect(ticket)) { pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID); deal.SetPosIDTester(pos_id_tester); } } } //--- in case of a market exit deal if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY) { //--- get a deal a newly opened position is based on CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID()); if(deal_in==NULL) continue; //--- get the position ticket in the tester from the properties of the opening deal //--- if the ticket is zero, then most likely the position in the tester is already closed ulong ticket_tester=deal_in.TicketTester(); if(ticket_tester==0) { PrintFormat("Could not get position ticket, apparently position #%I64d (#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester()); obj.SetNextDealIndex(); continue; } //--- if we reproduce the original trading history in the tester, if(InpTestingMode==TESTING_MODE_ORIGIN) { //--- if the position is closed by ticket if(obj.ClosePos(ticket_tester)) { //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket_tester); } } //--- otherwise, in the tester we work with stop orders placed according to different algorithms, and closing deals are skipped //--- accordingly, simply increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object else { obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket_tester); } } //--- if a ticket is now set in the deal object, then the deal has been successfully handled - //--- set the deal index in the list to the next deal if(deal.TicketTester()>0) { obj.SetNextDealIndex(); } } }
No manipulador OnTester() do EA, calculamos e retornamos o total de negociações processadas no testador:
//+------------------------------------------------------------------+ //| Tester function | //+------------------------------------------------------------------+ double OnTester(void) { //--- calculate and return the total number of deals handled in the tester double ret=0.0; int total=ExtListSymbols.Total(); for(int i=0; i<total; i++) { CSymbolTrade *obj=ExtListSymbols.At(i); if(obj!=NULL) ret+=obj.OnTester(); } return(ret); }
Além disso, para cada objeto de negociação por símbolo, é chamado seu próprio manipulador OnTester(), que imprime seus dados no log. Ao final do teste, no log do testador, veremos mensagens como estas:
2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol AUDUSD: Total deals: 218, number of processed deals: 216 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol EURJPY: Total deals: 116, number of processed deals: 114 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol EURUSD: Total deals: 524, number of processed deals: 518 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol GBPUSD: Total deals: 352, number of processed deals: 350 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol NZDUSD: Total deals: 178, number of processed deals: 176 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol USDCAD: Total deals: 22, number of processed deals: 22 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol USDCHF: Total deals: 250, number of processed deals: 246 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol USDJPY: Total deals: 142, number of processed deals: 142 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol XAUUSD: Total deals: 118, number of processed deals: 118 2025.01.22 23:49:15.951 Core 1 final balance 3591.70 pips 2025.01.22 23:49:15.951 Core 1 OnTester result 1902
Compilamos o EA e o executamos com as mesmas configurações de teste, mas agora selecionamos o tipo de teste como "Specified StopLoss and TakeProfit values", definindo os valores de StopLoss e TakeProfit como 100 e 500 pontos, respectivamente:

No teste anterior, com a negociação original, tivemos um prejuízo de 550 dólares. Agora, ao substituir o StopLoss de todas as posições por 100 pontos, e o TakeProfit por 500 pontos, tivemos um lucro de 590 pontos. E isso apenas substituindo os stop orders, sem considerar a especificidade de cada símbolo negociado. Se escolhermos valores de stop order individualmente para cada símbolo negociado, é bem possível que o gráfico do teste fique mais equilibrado.
Conclusão
Hoje fizemos um pequeno experimento com o histórico de negociação no estilo "E se...". Acredito que esse tipo de experimento pode realmente nos levar a ideias interessantes para ajustar nossa forma de negociar. E no próximo artigo, faremos mais um desses testes, dessa vez incluindo diferentes formas de trailing de posições. Vai ser interessante.
Acompanhando o artigo, estão os arquivos de todos os EAs e classes que analisamos hoje. Você pode baixá-los, estudá-los e experimentar com suas próprias contas de negociação. A pasta com o arquivo compactado pode ser extraída diretamente para o diretório MQL5 do terminal cliente, e todos os arquivos ficarão nas subpastas apropriadas.
Programas usados neste artigo:
| # | Nome | Tipo | Descrição |
|---|---|---|---|
| 1 | SymbolTrade.mqh | Biblioteca de classe | Biblioteca da estrutura e da classe de negociação, classe de negociação por símbolo |
| 2 | TradingByHistoryDeals.mq5 | Expert Advisor | EA para visualizar no testador as negociações e trades realizadas na conta |
| 3 | TradingByHistoryDeals_SLTP.mq5 | Expert Advisor | EA para visualizar e alterar com StopLoss e TakeProfit, no testador, as negociações e trades realizadas na conta |
| 4 | MQL5.zip | Arquivo | Arquivo compactado contendo os arquivos acima para extração no diretório MQL5 do terminal cliente |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/16952
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.
Métodos de discretização dos movimentos de preço em Python
Do básico ao intermediário: Eventos em Objetos (I)
Como Implementar Otimização Automática em Expert Advisors MQL5
Simulação de mercado: Iniciando o SQL no MQL5 (IV)
- 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
Qual é o erro?
Se o arquivo for menor do que a matriz antes da leitura, o tamanho da matriz não será alterado.
Você pode ter um erro semelhante ao usar o ArrayCopy.Você está ignorando um bom recurso
Qual é a vantagem?
Qual é a vantagem?
Concisão e velocidade de execução (totalmente do lado do MQ).
Mostre as estruturas padrão de auto-impressão e auto-preenchimento, por favor.
Na brevidade e na velocidade de execução (totalmente no lado da MQ).