
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:
//+------------------------------------------------------------------+ //| Структура сделки. Используется для создания файла истории сделок| //+------------------------------------------------------------------+ struct SDeal { ulong ticket; // Тикет сделки long order; // Ордер, на основании которого была открыта сделка long pos_id; // Идентификатор позиции long time_msc; // Время в миллисекундах datetime time; // Время double volume; // Объём double price; // Цена double profit; // Прибыль double commission; // Комиссия по сделке double swap; // Накопленный своп при закрытии double fee; // Оплата за проведение сделки, начисляется сразу после совершения сделки double sl; // Уровень Stop Loss double tp; // Уровень Take Profit ENUM_DEAL_TYPE type; // Тип ENUM_DEAL_ENTRY entry; // Способ изменения позиции ENUM_DEAL_REASON reason; // Причина или источник проведения сделки long magic; // Идентификатор эксперта int digits; // Digits символа ushort symbol[16]; // Символ ushort comment[64]; // Комментарий к сделке ushort external_id[256]; // Идентификатор сделки во внешней торговой системе (на бирже) //--- Установка строковых свойств 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()); } //--- Возврат строковых свойств 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:
//--- Типы сортировки сделок enum ENUM_DEAL_SORT_MODE { SORT_MODE_DEAL_TICKET = 0, // Режим сравнения/сортировки по тикету сделки SORT_MODE_DEAL_ORDER, // Режим сравнения/сортировки по ордеру, на основание которого выполнена сделка SORT_MODE_DEAL_TIME, // Режим сравнения/сортировки по времени совершения сделки SORT_MODE_DEAL_TIME_MSC, // Режим сравнения/сортировки по времени совершения сделки в миллисекундах SORT_MODE_DEAL_TYPE, // Режим сравнения/сортировки по типу сделки SORT_MODE_DEAL_ENTRY, // Режим сравнения/сортировки по направлению сделки SORT_MODE_DEAL_MAGIC, // Режим сравнения/сортировки по Magic number сделки SORT_MODE_DEAL_REASON, // Режим сравнения/сортировки по причине или источнику проведения сделки SORT_MODE_DEAL_POSITION_ID, // Режим сравнения/сортировки по идентификатору позиции SORT_MODE_DEAL_VOLUME, // Режим сравнения/сортировки по объему сделки SORT_MODE_DEAL_PRICE, // Режим сравнения/сортировки по цене сделки SORT_MODE_DEAL_COMMISSION, // Режим сравнения/сортировки по комиссии SORT_MODE_DEAL_SWAP, // Режим сравнения/сортировки по накопленному свопу при закрытии SORT_MODE_DEAL_PROFIT, // Режим сравнения/сортировки по финансовому результату сделки SORT_MODE_DEAL_FEE, // Режим сравнения/сортировки по оплате за проведение сделки SORT_MODE_DEAL_SL, // Режим сравнения/сортировки по уровню Stop Loss SORT_MODE_DEAL_TP, // Режим сравнения/сортировки по уровню Take Profit SORT_MODE_DEAL_SYMBOL, // Режим сравнения/сортировки по имени символа, по которому произведена сделка SORT_MODE_DEAL_COMMENT, // Режим сравнения/сортировки по комментарию к сделке SORT_MODE_DEAL_EXTERNAL_ID, // Режим сравнения/сортировки по идентификатору сделки во внешней торговой системе SORT_MODE_DEAL_TICKET_TESTER, // Режим сравнения/сортировки по тикету сделки в тестере SORT_MODE_DEAL_POS_ID_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:
//+------------------------------------------------------------------+ //| Сохраняет сделки из истории в массив | //+------------------------------------------------------------------+ int SaveDealsToArray(SDeal &array[], bool logs=false) { //--- структура сделки SDeal deal={}; //--- запросим историю сделок в интервале с самого начала по текущий момент if(!HistorySelect(0, TimeCurrent())) { Print("HistorySelect() failed. Error ", GetLastError()); return 0; } //--- общее количество сделок в списке int total=HistoryDealsTotal(); //--- обработаем каждую сделку for(int i=0; i<total; i++) { //--- получаем тикет очередной сделки (сделка автоматически выбирается для получения её свойств) ulong ticket=HistoryDealGetTicket(i); if(ticket==0) continue; //--- сохраняем только балансовые и торговые сделки 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; //--- сохраняем свойства сделки в структуре 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); //--- увеличиваем массив и int size=(int)array.Size(); ResetLastError(); if(ArrayResize(array, size+1, total)!=size+1) { Print("ArrayResize() failed. Error ", GetLastError()); continue; } //--- сохраняем в массиве сделку array[size]=deal; //--- если разрешено, выводим описание сохранённой сделки в журнал if(logs) DealPrint(deal, i); } //--- возвращаем количество сохранённых в массиве сделок 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:
//+------------------------------------------------------------------+ //| Выводит сделки из массива в журнал | //+------------------------------------------------------------------+ void DealsArrayPrint(SDeal &array[]) { int total=(int)array.Size(); //--- если передан пустой массив - сообщаем об этом и возвращаем false if(total==0) { PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__); return; } //--- В цикле по массиву сделок распечатаем описание каждой сделки 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:
//+------------------------------------------------------------------+ //| Возвращает описание типа сделки | //+------------------------------------------------------------------+ 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:
//+------------------------------------------------------------------+ //| Возвращает описание способа изменения позиции | //+------------------------------------------------------------------+ 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:
//+------------------------------------------------------------------+ //| Возвращает описание сделки | //+------------------------------------------------------------------+ 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:
//+------------------------------------------------------------------+ //| Распечатывает в журнале данные сделки | //+------------------------------------------------------------------+ 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:
//+------------------------------------------------------------------+ //| Открывает файл для записи, возвращает хэндл | //+------------------------------------------------------------------+ 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; } //--- успешно return true; }
Função que abre o arquivo para leitura:
//+------------------------------------------------------------------+ //| Открывает файл для чтения, возвращает хэндл | //+------------------------------------------------------------------+ 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; } //--- успешно 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:
//+------------------------------------------------------------------+ //| Сохраняет в файл данные сделок из массива | //+------------------------------------------------------------------+ bool FileWriteDealsFromArray(SDeal &array[], ulong &file_size) { //--- если передан пустой массив - сообщаем об этом и возвращаем false if(array.Size()==0) { PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__); return false; } //--- откроем файл для записи, получим его хэндл int handle=INVALID_HANDLE; if(!FileOpenToWrite(handle)) return false; //--- переместим файловый указатель на конец файла bool res=true; ResetLastError(); res&=FileSeek(handle, 0, SEEK_END); if(!res) PrintFormat("%s: FileSeek(SEEK_END) failed. Error %d",__FUNCTION__, GetLastError()); //--- запишем данные массива в конец файла file_size=0; res&=(FileWriteArray(handle, array)==array.Size()); if(!res) PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError()); else file_size=FileSize(handle); //--- закрываем файл 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:
//+------------------------------------------------------------------+ //| Загружает в массив данные сделок из файла | //+------------------------------------------------------------------+ bool FileReadDealsToArray(SDeal &array[], ulong &file_size) { //--- откроем файл для чтения, получим его хэндл int handle=INVALID_HANDLE; if(!FileOpenToRead(handle)) return false; //--- переместим файловый указатель на конец файла bool res=true; ResetLastError(); //--- прочитаем данные из файла в массив file_size=0; res=(FileReadArray(handle, array)>0); if(!res) PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError()); else file_size=FileSize(handle); //--- закрываем файл 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:
//+------------------------------------------------------------------+ //| Подготавливает файл со сделками истории | //+------------------------------------------------------------------+ bool PreparesDealsHistoryFile(SDeal &deals_array[]) { //--- сохраним все сделки счёта в массив сделок int total=SaveDealsToArray(deals_array); if(total==0) return false; //--- запишем данные массива сделок в файл ulong file_size=0; if(!FileWriteDealsFromArray(deals_array, file_size)) return false; //--- распечатаем в журнале сколько сделок было прочитано и сохранено в файл, путь к файлу и его размер 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); //--- теперь для проверки прочитаем данные из файла в массив ArrayResize(deals_array, 0, total); if(!FileReadDealsToArray(deals_array, file_size)) return false; //--- распечатаем в журнале сколько байт было прочитано из файла и количество полученных в массив сделок 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(!MQLInfoInteger(MQL_TESTER)) { //--- подготовим файл со всеми историческими сделками if(!PreparesDealsHistoryFile(ExtArrayDeals)) return(INIT_FAILED); //--- распечатаем в журнале все сделки после загрузки их из файла if(InpShowDataInLog) DealsArrayPrint(ExtArrayDeals); //--- получаем первую балансовую сделку, создаём текст сообщения в выводим его при помощи 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); //--- сообщим алертом рекомендуемые параметры тестера стратегий для запуска тестирования Alert(message); } //--- Всё успешно 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.
//+------------------------------------------------------------------+ //| Класс сделки. Используется для торговли в тестере стратегий | //+------------------------------------------------------------------+ class CDeal : public CObject { protected: //--- Целочисленные свойства ulong m_ticket; // Тикет сделки. Уникальное число, которое присваивается каждой сделке long m_order; // Ордер, на основание которого выполнена сделка datetime m_time; // Время совершения сделки long m_time_msc; // Время совершения сделки в миллисекундах с 01.01.1970 ENUM_DEAL_TYPE m_type; // Тип сделки ENUM_DEAL_ENTRY m_entry; // Направление сделки – вход в рынок, выход из рынка или разворот long m_magic; // Magic number для сделки (смотри ORDER_MAGIC) ENUM_DEAL_REASON m_reason; // Причина или источник проведения сделки long m_pos_id; // Идентификатор позиции, в открытии, изменении или закрытии которой участвовала эта сделка //--- Вещественные свойства double m_volume; // Объем сделки double m_price; // Цена сделки double m_commission; // Комиссия по сделке double m_swap; // Накопленный своп при закрытии double m_profit; // Финансовый результат сделки double m_fee; // Оплата за проведение сделки, начисляется сразу после совершения сделки double m_sl; // Уровень Stop Loss double m_tp; // Уровень Take Profit //--- Строковые свойства string m_symbol; // Имя символа, по которому произведена сделка string m_comment; // Комментарий к сделке string m_external_id; // Идентификатор сделки во внешней торговой системе (на бирже) //--- Дополнительные свойства int m_digits; // Digits символа double m_point; // Point символа ulong m_ticket_tester; // Тикет позиции в тестере long m_pos_id_tester; // Идентификатор позиции в тестере public: //--- Установка свойств сделки 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; } //--- Возврат свойств сделки 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; } //--- Сравнивает два объекта между собой по указанному в 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); } } //--- Конструкторы/деструктор 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:
//+------------------------------------------------------------------+ //| Класс торговли по символу | //+------------------------------------------------------------------+ CDeal DealTmp; // Временный объект сделки для поиска по свойствам class CSymbolTrade : public CObject { private: int m_index_next_deal; // Индекс очередной ещё не обработанной сделки int m_deals_processed; // Количество обработанных сделок protected: MqlTick m_tick; // Структура тика CArrayObj m_list_deals; // Список сделок, проведённых по символу CTrade m_trade; // Торговый класс string m_symbol; // Наименование символа public: //--- Возвращает список сделок CArrayObj *GetListDeals(void) { return(&this.m_list_deals); } //--- Устанавливает символ void SetSymbol(const string symbol) { this.m_symbol=symbol; } //--- (1) Устанавливает, (2) возвращает количество обработанных сделок void SetNumProcessedDeals(const int num) { this.m_deals_processed=num; } int NumProcessedDeals(void) const { return this.m_deals_processed; } //--- Добавляет сделку в массив сделок bool AddDeal(CDeal *deal); //--- Возвращает сделку (1) по времени в секундах, (2) по индексу в списке, //--- (3) сделку открытия по идентификатору позиции, (4) текущую сделку в списке CDeal *GetDealByTime(const datetime time); CDeal *GetDealByIndex(const int index); CDeal *GetDealInByPosID(const long pos_id); CDeal *GetDealCurrent(void); //--- Возвращает (1) количество сделок в списке, (2) индекс текущей сделки в списке int DealsTotal(void) const { return this.m_list_deals.Total(); } int DealCurrentIndex(void) const { return this.m_index_next_deal; } //--- Возвращает (1) символ, (2) описание объекта string Symbol(void) const { return this.m_symbol; } string Description(void) const { return ::StringFormat("%s trade object. Total deals: %d", this.Symbol(), this.DealsTotal() ); } //--- Возвращает текущую цену (1) Bid, (2) Ask, время в (3) секундах, (4) миллисекундах double Bid(void); double Ask(void); datetime Time(void); long TimeMsc(void); //--- Открывает (1) длинную, (2) короткую позицию, (3) закрывает позицию по тикету 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); //--- Возвращает результат сравнения текущего времени с указанным bool CheckTime(const datetime time) { return(this.Time()>=time); } //--- Устанавливает индекс следующей сделки void SetNextDealIndex(void) { this.m_index_next_deal++; } //--- Обработчик OnTester. Возвращает количество обработанных тестером сделок 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; } //--- Сравнивает два объекта между собой (сравнение только по символу) 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); } //--- Конструкторы/деструктор 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:
//--- Конструкторы/деструктор 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::Добавляет сделку в массив сделок | //+------------------------------------------------------------------+ bool CSymbolTrade::AddDeal(CDeal *deal) { //--- Если в списке уже есть сделка с тикетом сделки, переданной в метод - возвращаем true this.m_list_deals.Sort(SORT_MODE_DEAL_TICKET); if(this.m_list_deals.Search(deal)>WRONG_VALUE) return true; //--- Добавляем указатель на сделку в список в порядке сортировки по времени в миллисекундах 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; } //--- Всё успешно 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::Возвращает объект сделки по времени в секундах | //+------------------------------------------------------------------+ 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::Возвращает сделку открытия по идентификатору позиции| //+------------------------------------------------------------------+ 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::Возвращает объект сделки по индексу в списке | //+------------------------------------------------------------------+ 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:
//+------------------------------------------------------------------+ //| Возвращает сделку, на которую указывает индекс текущей сделки | //+------------------------------------------------------------------+ 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::Возвращает текущую цену Bid | //+------------------------------------------------------------------+ 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::Возвращает текущую цену Ask | //+------------------------------------------------------------------+ 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::Возвращает текущее время в секундах | //+------------------------------------------------------------------+ 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::Возвращает текущее время в миллисекундах | //+------------------------------------------------------------------+ 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::Открывает длинную позицию | //+------------------------------------------------------------------+ 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::Открывает короткую позицию | //+------------------------------------------------------------------+ 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::Закрывает позицию по тикету | //+------------------------------------------------------------------+ 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:
//+------------------------------------------------------------------+ //| Создаёт объект сделки из структуры | //+------------------------------------------------------------------+ CDeal *CreateDeal(SDeal &deal_str) { //--- Если объект сделки создать не удалось - сообщаем в журнале об ошибке и возвращаем NULL CDeal *deal=new CDeal(deal_str.ticket, deal_str.Symbol()); if(deal==NULL) { PrintFormat("%s: Error. Failed to create deal object"); return NULL; } //--- заполняем свойства сделки из полей структуры deal.SetOrder(deal_str.order); // Ордер, на основании которого была открыта сделка deal.SetPositionID(deal_str.pos_id); // Идентификатор позиции deal.SetTimeMsc(deal_str.time_msc); // Время в миллисекундах deal.SetTime(deal_str.time); // Время deal.SetVolume(deal_str.volume); // Объём deal.SetPrice(deal_str.price); // Цена deal.SetProfit(deal_str.profit); // Прибыль deal.SetCommission(deal_str.commission); // Комиссия по сделке deal.SetSwap(deal_str.swap); // Накопленный своп при закрытии deal.SetFee(deal_str.fee); // Оплата за проведение сделки, начисляется сразу после совершения сделки deal.SetSL(deal_str.sl); // Уровень Stop Loss deal.SetTP(deal_str.tp); // Уровень Take Profit deal.SetType(deal_str.type); // Тип deal.SetEntry(deal_str.entry); // Способ изменения позиции deal.SetReason(deal_str.reason); // Причина или источник проведения сделки deal.SetMagic(deal_str.magic); // Идентификатор эксперта deal.SetComment(deal_str.Comment()); // Комментарий к сделке deal.SetExternalID(deal_str.ExternalID()); // Идентификатор сделки во внешней торговой системе (на бирже) //--- Возвращаем указатель на созданный объект 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:
//+------------------------------------------------------------------+ //| Создаёт массив используемых символов | //+------------------------------------------------------------------+ bool CreateListSymbolTrades(SDeal &array_deals[], CArrayObj *list_symbols) { bool res=true; // результат int total=(int)array_deals.Size(); // общее количество сделок в массиве //--- если массив сделок пустой - возвращаем false if(total==0) { PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__); return false; } //--- в цикле по массиву сделок CSymbolTrade *SymbolTrade=NULL; for(int i=0; i<total; i++) { //--- получаем очередную сделку и, если это не покупка и не продажа - идём к следующей SDeal deal_str=array_deals[i]; if(deal_str.type!=DEAL_TYPE_BUY && deal_str.type!=DEAL_TYPE_SELL) continue; //--- найдём торговый объект в списке, у которого символ равен символу сделки string symbol=deal_str.Symbol(); SymbTradeTmp.SetSymbol(symbol); list_symbols.Sort(); int index=list_symbols.Search(&SymbTradeTmp); //--- если индекс искомого объекта в списке равен -1 - такого объекта в списке нет if(index==WRONG_VALUE) { //--- создаём новый торговый объект символа и, если создать не получилось - //--- добавляем к результату значение false и идём к следующей сделке SymbolTrade=new CSymbolTrade(symbol); if(SymbolTrade==NULL) { res &=false; continue; } //--- если торговый объект символа не удалось добавить в список - //--- удаляем вновь созданный объект, добавляем к результату значение false //--- и идём к следующей сделке if(!list_symbols.Add(SymbolTrade)) { delete SymbolTrade; res &=false; continue; } } //--- иначе, если торговый объект уже существует в списке - получаем его по индексу else { SymbolTrade=list_symbols.At(index); if(SymbolTrade==NULL) continue; } //--- если текущей сделки ещё нет в списке сделок торгового объекта символа if(SymbolTrade.GetDealByTime(deal_str.time)==NULL) { //--- создаём объект сделки по её образцу-структуре CDeal *deal=CreateDeal(deal_str); if(deal==NULL) { res &=false; continue; } //--- к значению результата добавляем результат добавления объекта сделки в список сделок торгового объекта символа res &=SymbolTrade.AddDeal(deal); } } //--- возвращаем итоговый результат создания торговых объектов и добавления сделок в их списки 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:
//+------------------------------------------------------------------+ //| Выводит в журнал список торговых объектов символов | //+------------------------------------------------------------------+ 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:
//+------------------------------------------------------------------+ //| Возвращает описание сделки | //+------------------------------------------------------------------+ 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(!MQLInfoInteger(MQL_TESTER)) { //--- подготовим файл со всеми историческими сделками if(!PreparesDealsHistoryFile(ExtArrayDeals)) return(INIT_FAILED); //--- распечатаем в журнале все сделки после загрузки их из файла if(InpShowDataInLog) DealsArrayPrint(ExtArrayDeals); //--- получаем первую балансовую сделку, создаём текст сообщения в выводим его при помощи 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); //--- сообщим алертом рекомендуемые параметры тестера стратегий для запуска тестирования Alert(message); } //--- Советник запущен в тестере else { //--- прочитаем данные из файла в массив 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); } //--- сообщим в журнале о количестве прочитанных байт из файла и о записи массива сделок. 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()); } //--- Из массива исторических сделок создаём список торговых объектов по символам if(!CreateListSymbolTrades(ExtArrayDeals, &ExtListSymbols)) { Print("Errors found while creating symbol list"); return(INIT_FAILED); } //--- Распечатаем в журнале созданный список сделок SymbolsArrayPrint(&ExtListSymbols); //--- Обратимся к каждому символу для начала закачки исторических данных //--- и открытия графиков проторгованных символов в тестере стратегий 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); } //--- Всё успешно return(INIT_SUCCEEDED); }
No manipulador OnDeinit() do EA, limpamos os arrays e listas criados:
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- очищаем созданные списки и массивы 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() { //--- работаем только в тестере стратегий if(!MQLInfoInteger(MQL_TESTER)) return; //--- Обрабатываем в тестере список сделок из файла 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:
//+------------------------------------------------------------------+ //| Торговля по истории | //+------------------------------------------------------------------+ void TradeByHistory(const string symbol="", const long magic=-1) { datetime time=0; int total=ExtListSymbols.Total(); // количество торговых объектов в списке //--- в цикле по всем торговым объектам символов for(int i=0; i<total; i++) { //--- получаем очередной торговый объект CSymbolTrade *obj=ExtListSymbols.At(i); if(obj==NULL) continue; //--- получаем текущую сделку, на которую указывает индекс списка сделок CDeal *deal=obj.GetDealCurrent(); if(deal==NULL) continue; //--- фильтруем сделку по магику и символу if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol)) continue; //--- фильтруем сделку по типу (только сделки покупки/продажи) ENUM_DEAL_TYPE type=deal.TypeDeal(); if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL) continue; //--- если это уже обработанная в тестере сделка - идём к следующей if(deal.TicketTester()>0) continue; //--- если время сделки ещё не настало - идём к следующему торговому объекту следующего символа if(!obj.CheckTime(deal.Time())) continue; //--- если сделка входа в рынок ENUM_DEAL_ENTRY entry=deal.Entry(); if(entry==DEAL_ENTRY_IN) { //--- открываем позицию по типу сделки 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(ticket>0) { //--- увеличиваем количество обработанных тестером сделок и записываем тикет сделки в тестере в свойства объекта сделки obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket); //--- получаем идентификатор позиции в тестере и записываем его в свойства объекта сделки long pos_id_tester=0; if(HistoryDealSelect(ticket)) { pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID); deal.SetPosIDTester(pos_id_tester); } } } //--- если сделка выхода из рынка if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY) { //--- получаем сделку, на основании которой была открыта позиция CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID()); if(deal_in==NULL) continue; //--- получаем тикет позиции в тестере из свойств открывающей сделки //--- если тикет равен нулю, значит скорее всего позиция в тестере уже закрыта 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(obj.ClosePos(ticket_tester)) { //--- увеличиваем количество обработанных тестером сделок и записываем тикет сделки в тестере в свойства объекта сделки obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket_tester); } } //--- если теперь в объекте сделки записан тикет - значит сделка успешно обработана - //--- устанавливаем индекс сделки в списке на следующую сделку 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 */ // Указанные значения StopLoss и TakeProfit }; //+------------------------------------------------------------------+ //| 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 */ // Отступ StopLoss в пунктах input int InpTakeProfit = 500; /* TakeProfit in points */ // Отступ TakeProfit в пунктах //--- 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() { //--- Корректируем размеры стопов ExtStopLoss =(InpStopLoss<1 ? 0 : InpStopLoss); ExtTakeProfit=(InpTakeProfit<1 ? 0 : InpTakeProfit); //--- Если советник запущен не в тестере
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:
//+------------------------------------------------------------------+ //| Возвращает корректный StopLoss относительно 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) ); } //+------------------------------------------------------------------+ //| Возвращает корректный TakeProfit относительно 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) ); } //+------------------------------------------------------------------+ //| Возвращает размер StopLevel в пунктах | //+------------------------------------------------------------------+ 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.
//+------------------------------------------------------------------+ //| Торговля по истории | //+------------------------------------------------------------------+ void TradeByHistory(const string symbol="", const long magic=-1) { datetime time=0; int total=ExtListSymbols.Total(); // количество торговых объектов в списке //--- в цикле по всем торговым объектам символов for(int i=0; i<total; i++) { //--- получаем очередной торговый объект CSymbolTrade *obj=ExtListSymbols.At(i); if(obj==NULL) continue; //--- получаем текущую сделку, на которую указывает индекс списка сделок CDeal *deal=obj.GetDealCurrent(); if(deal==NULL) continue; //--- фильтруем сделку по магику и символу if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol)) continue; //--- фильтруем сделку по типу (только сделки покупки/продажи) ENUM_DEAL_TYPE type=deal.TypeDeal(); if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL) continue; //--- если это уже обработанная в тестере сделка - идём к следующей if(deal.TicketTester()>0) continue; //--- если время сделки ещё не настало - идём к следующему торговому объекту следующего символа if(!obj.CheckTime(deal.Time())) continue; //--- если сделка входа в рынок ENUM_DEAL_ENTRY entry=deal.Entry(); if(entry==DEAL_ENTRY_IN) { //--- устанавливаем размеры стоп-приказов в зависимости от метода установки стопов 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); } //--- открываем позицию по типу сделки 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(ticket>0) { //--- увеличиваем количество обработанных тестером сделок и записываем тикет сделки в тестере в свойства объекта сделки obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket); //--- получаем идентификатор позиции в тестере и записываем его в свойства объекта сделки long pos_id_tester=0; if(HistoryDealSelect(ticket)) { pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID); deal.SetPosIDTester(pos_id_tester); } } } //--- если сделка выхода из рынка if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY) { //--- получаем сделку, на основании которой была открыта позиция CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID()); if(deal_in==NULL) continue; //--- получаем тикет позиции в тестере из свойств открывающей сделки //--- если тикет равен нулю, значит скорее всего позиция в тестере уже закрыта 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(InpTestingMode==TESTING_MODE_ORIGIN) { //--- если позиция закрыта по тикету if(obj.ClosePos(ticket_tester)) { //--- увеличиваем количество обработанных тестером сделок и записываем тикет сделки в тестере в свойства объекта сделки obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket_tester); } } //--- иначе - в тестере работаем со стоп-приказами, выставляемыми по различным алгоритмам, и сделки закрытия пропускаются //--- соответственно, просто увеличиваем количество обработанных тестером сделок и записываем тикет сделки в тестере в свойства объекта сделки else { obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket_tester); } } //--- если теперь в объекте сделки записан тикет - значит сделка успешно обработана - //--- устанавливаем индекс сделки в списке на следующую сделку 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) { //--- посчитаем и вернём общее количество обработанных в тестере сделок 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.





- 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).