
Визуальная оценка и корректировка торговли в MetaTrader 5
Содержание
- Введение
- Сохраняем историю сделок в файл
- Разбираем в тестере историю сделок из файла
- Скорректируем стоп-приказы
- Заключение
Введение
Представим ситуацию: на каком-либо счёте достаточно долгое время ведётся более-менее активная торговля на разных инструментах разными советниками и где-то даже вручную. И вот, по истечении некоторого времени, мы хотим видеть результаты работы. Естественно, можно посмотреть стандартные отчёты по торговле в терминале, нажав сочетание клавиш Alt+E. Можно загрузить значки сделок на график и посмотреть входы и выходы позиций. Но что, если хочется в динамике посмотреть как велась торговля, где и как открывались и закрывались позиции? Посмотреть раздельно по каждому символу, либо сразу все вместе, открытие и закрытие позиций, на каких уровнях ставились стоп-приказы, и обоснован ли был их размер. А что если далее мы зададимся вопросом "что бы было, если бы ..." (и тут много вариантов — другие стопы, по иным алгоритмам и критериям, использование трейлинга позиций, либо перенос стопов в безубыток и т.д.); а потом ещё и протестировать все свои "если" с наглядным видимым результатом. Как бы могла измениться торговля, если бы...
Для решения таких вопросов, оказывается, всё уже есть. Нам достаточно лишь загрузить историю счёта в файл — все проведённые сделки, а затем в тестере стратегий прогнать советник, который читает сделки из файла и открывает/закрывает позиции в тестере стратегий клиентского терминала. При наличии такого советника, мы сможем добавить в него код для изменения условий выхода из позиций и сравнить, как при этом изменилась торговля, и что бы было, если бы...
Что нам это даст? Ещё один инструмент для поиска лучших результатов, для внесения корректировок в торговлю, которая некоторое время уже велась на счёте; визуальное тестирование позволит в динамике видеть верно ли открывались позиции на том, или ином инструменте, и в нужное ли время они закрывались, и т.д. И главное — какой-то новый алгоритм можно просто добавить в код советника, протестировать, получить результат и внести корректировки в советники, работающие на этом счёте.
Сделаем такую логику поведения советника:
- если советник запущен на графике любого инструмента, то он соберёт всю историю сделок на текущем счёте, сохранит все сделки в один файл и далее ничего делать не будет;
- если советник запущен в тестере, то он прочитает записанную в файл историю сделок и, во время прохождения теста, будет повторять все сделки из файла, открывая и закрывая позиции.
Таким образом, советник сначала подготавливает файл истории сделок (при запуске его на графике), а затем выполняет сделки из файла, полностью повторяя торговлю на счёте (при запуске его в тестере стратегий).
Далее, внесём доработки в советник, чтобы иметь возможность выставить иные размеры StopLoss и TakeProfit для открываемых в тестере позиций.
Сохраняем историю сделок в файл
В каталоге терминала \MQL5\Experts\ создадим новую папку TradingByHistoryDeals, а в ней — новый файл советника с именем TradingByHistoryDeals.mq5.
В советнике должна быть возможность выбора по какому символу и по какому магику проводить тестирование. В случае, если на счёте работало несколько советников по нескольким символам или магикам, то можно будет выбрать в настройках какой символ или магик нас интересует, ну либо сразу все вместе.
//+------------------------------------------------------------------+ //| 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 */ // Показать собранные данные сделок в журнале
Значения по умолчанию для символа и магика — пустая строка и -1. При этих значениях советник не будет фильтровать историю торговли ни по символу, ни по магику — тестироваться будет полностью вся торговая история. Третьей строкой мы указываем советнику вывести (либо нет) в журнал описания всех сделок, сохранённых в файле — чтобы можно было наглядно проконтролировать верность сохранённых данных.
Каждая сделка — это целый набор различных параметров, описываемых разными свойствами сделки. Самое простое — записать все свойства сделки в структуру. Для записи в файл большого количества сделок, необходимо использовать массив структур. И далее этот массив сохранять в файл. В языке MQL5 для этого всё есть. Логика сохранения истории сделок в файл будет такой:
- идём в цикле по историческим сделкам;
- получаем очередную сделку и записываем её данные в структуру;
- созданную структуру сделки сохраняем в массив сделок;
- по окончании цикла подготовленный массив структур сохраняем в файл.
Все дополнительные коды — структуры, классы, перечисления, будем писать в отдельном файле. Назовём его по имени будущего класса торгового объекта символа.
В той же папке создадим новый подключаемый файл с именем SymbolTrade.mqh.
Сразу же напишем макроподстановки для имени каталога, в который будет записываться файл истории, имя файла и путь к файлу, и подключим к созданному файлу все необходимые для разработки файлы Стандартной Библиотеки:
//+------------------------------------------------------------------+ //| 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>
Далее напишем структуру сделки:
//+------------------------------------------------------------------+ //| Структура сделки. Используется для создания файла истории сделок| //+------------------------------------------------------------------+ 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)); } };
Так как структуры сделок будем сохранять в файл, а в файл можно записывать только структуры простых типов (см. FileWriteArray()), то все строковые переменные необходимо заменить на ushort-массивы и создать методы для записи и возврата строковых свойств структуры.
Созданная структура необходима только для сохранения истории сделок в файл и чтения записанной истории из файла. В самом же советнике далее будет создан список объектов, в котором будут храниться объекты класса сделки. Для поиска нужной сделки в списке и сортировки массива, необходимо будет указывать свойство сделки, по которому она будет искаться в списке. Для поиска сделки список объектов должен быть отсортирован по искомому свойству.
Напишем перечисление всех свойств объекта сделки, по которым возможно будет осуществлять поиск:
//--- Типы сортировки сделок 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, // Режим сравнения/сортировки по идентификатору позиции в тестере };
Здесь, помимо стандартных свойств сделки, есть ещё два: тикет сделки в тестере и идентификатор позиции в тестере. Дело в том, что мы будем торговать в тестере на основе данных реальных сделок, а открываемые в тестере позиции и, соответственно, те сделки, на основе которых были позиции открыты, в тестере имеют совсем иные тикет и идентификатор. Для того, чтобы мы могли сопоставить реальную сделку со сделкой в тестере (так же и идентификатор), нам нужно будет тикет и идентификатор позиции в тестере сохранять в свойствах объекта сделки, и уже по этим сохранённым данным сопоставлять сделку в тестере с реальной сделкой в истории.
На этом пока остановимся с этим файлом и перейдём к файлу советника, созданного чуть ранее. Добавим массив структур, куда будем добавлять структуры всех сделок истории:
//--- 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[]={};
И напишем функции для работы с историческими сделками.
Функция, сохраняющая историю сделок в массив:
//+------------------------------------------------------------------+ //| Сохраняет сделки из истории в массив | //+------------------------------------------------------------------+ 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(); }
Код функции подробно прокомментирован. Выбираем всю торговую историю от начала и до текущего времени, получаем каждую очередную историческую сделку, сохраняем её свойства в полях структуры и переменную структуры сохраняем в массиве. По окончании цикла по истории сделок, возвращаем размер полученного массива сделок. Для контроля за ходом записи сделок в массив, можно каждую обработанную сделку распечатать в журнале. Для этого, при вызове функции нужно указать флаг logs в формальных параметрах функции равным true.
Функция, распечатывающая в журнале все сделки из массива сделок:
//+------------------------------------------------------------------+ //| Выводит сделки из массива в журнал | //+------------------------------------------------------------------+ 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); } }
Для вывода описания сделки в журнал, сделаем несколько функций.
Функция, возвращающая описание типа сделки:
//+------------------------------------------------------------------+ //| Возвращает описание типа сделки | //+------------------------------------------------------------------+ 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; } }
В зависимости от переданного в функцию типа сделки, выводится соответствующая строка.
Функция, возвращающая описание способа изменения позиции:
//+------------------------------------------------------------------+ //| Возвращает описание способа изменения позиции | //+------------------------------------------------------------------+ 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; } }
В зависимости от переданного в функцию способа изменения позиции, выводится соответствующая строка.
Функция, возвращающая описание сделки:
//+------------------------------------------------------------------+ //| Возвращает описание сделки | //+------------------------------------------------------------------+ 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))); }
Если это балансовая сделка, то выводится описание в виде
0: deal #190715988 Entry In, type Balance 3000.00 USD at 2024.09.13 21:48
Иначе, выводится описание сделки в ином формате:
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
Функция, распечатывающая в журнале описание сделки:
//+------------------------------------------------------------------+ //| Распечатывает в журнале данные сделки | //+------------------------------------------------------------------+ void DealPrint(SDeal &deal, const int index) { Print(DealDescription(deal, index)); }
Здесь всё наглядно — просто распечатываем строку, полученную из функции DealDescription().
Напишем функции для записи и чтения массива сделок в файл/из файла.
Функция, открывающая файл для записи:
//+------------------------------------------------------------------+ //| Открывает файл для записи, возвращает хэндл | //+------------------------------------------------------------------+ 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; }
Функция, открывающая файл для чтения:
//+------------------------------------------------------------------+ //| Открывает файл для чтения, возвращает хэндл | //+------------------------------------------------------------------+ 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; }
Функции открывают файл для чтения/записи. В формальных параметрах по ссылке передаётся переменная, в которую записывается хэндл файла. Возвращают true при успешном открытии файла и false — при ошибке.
Функция, сохраняющая в файл данные сделок из массива:
//+------------------------------------------------------------------+ //| Сохраняет в файл данные сделок из массива | //+------------------------------------------------------------------+ 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; }
В функцию передаётся массив структур, который необходимо сохранить в файле. Переменная для получения размера созданного файла передаётся по ссылке в формальных параметрах функции. Открываем файл, перемещаем файловый указатель в конец файла и записываем данные из массива структур в файл, начиная от указателя. По окончании записи файл закрывается.
После того, как массив структур сделок сохранён в файл, из этого файла можно обратно считать все сделки в массив, и далее его использовать для создания списков сделок и работы с ними в тестере.
Функция, загружающая в массив данные сделок из файла:
//+------------------------------------------------------------------+ //| Загружает в массив данные сделок из файла | //+------------------------------------------------------------------+ 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; }
На основе созданных выше функций напишем функцию для чтения истории сделок и записи их в файл.
Функция, подготавливающая файл со сделками истории:
//+------------------------------------------------------------------+ //| Подготавливает файл со сделками истории | //+------------------------------------------------------------------+ 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; }
По комментариям в коде вся логика здесь понятна. Функция запускается в обработчике OnInit() и подготавливает файл со сделками для дальнейшей работы с ним:
//+------------------------------------------------------------------+ //| 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); }
Помимо сохранения всех исторических сделок в файл, здесь ещё выводится алерт с сообщением о рекомендуемых настройках тестера — начальный баланс, плечо и время начала тестирования, соответствующее дате первой балансовой сделки. Например так:
Alert: Now you can run testing Interval: 2024.09.13 - current date Initial deposit: 3000.00, leverage 1:500
Такие настройки тестера дадут итоговый результат в тестере наиболее близким к тому, который получен в реальности.
Структура сделки, написанная в файле \MQL5\Experts\TradingByHistoryDeals\SymbolTrade.mqh, предназначена только для сохранения истории сделок в файл и для чтения сохранённой истории из файла. Для дальнейшей работы нам необходимо создать класс сделки, объекты которого будут храниться в списках. А сами списки — в объектах торгового класса для тестера. В свою очередь, торговые объекты — это тоже объекты класса, которые будут храниться в своём списке. Каждый торговый объект будет определяться по принадлежности его к определённому своему символу — сколько символов участвовало в торговле, столько будет и торговых объектов. Сами торговые объекты будут содержать в себе список сделок только по своему символу и свои объекты класса CTrade Стандартной Библиотеки. Это позволит каждый торговый объект класса CTrade настроить в соответствии с условиями торгуемого символа.
Напишем класс сделки в файле \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) {} };
Класс практически повторяет ранее созданную структуру сделки. Кроме свойств сделки добавлены дополнительные свойства — Digits и Point символа, по которому была проведена сделка. Это упрощает вывод описания сделки, так как эти данные устанавливаются в конструкторе сделки сразу при создании объекта, что освобождает от необходимости получать эти свойства для каждой сделки (если они нужны) при обращении к ней.
Также здесь создан виртуальный метод Compare() для сравнения двух объектов сделок — он будет использоваться при сортировке списков сделок для поиска нужной сделки по указанному свойству.
Теперь создадим торговый класс символа. В классе будет храниться список сделок, проведённых по символу, записанному в свойствах объекта, из него же эти сделки будут запрашиваться тестером для их копирования. В общем, этот класс будет являться основой для копирования сделок, проведённых на счёте по символу, в тестере стратегий:
//+------------------------------------------------------------------+ //| Класс торговли по символу | //+------------------------------------------------------------------+ 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) {} };
Рассмотрим некоторые методы.
- SetNumProcessedDeals() и NumProcessedDeals() устанавливают и возвращают количество уже обработанных тестером исторических сделок из полученного из файла списка сделок. Необходимы для контроля правильности обработки исторических сделок и получения итоговой статистики количества обработанных сделок тестером;
- GetDealCurrent() возвращает указатель на текущую историческую сделку, которую необходимо обработать тестером и пометить её обработанной;
- DealCurrentIndex() возвращает индекс исторической сделки, выбранной для обработки тестером на текущий момент;
- SetNextDealIndex() после завершения обработки текущей исторической сделки, устанавливает индекс следующей сделки для обработки её тестером. Так как все исторические сделки в списке отсортированы по времени в миллисекундах, то это будет установка индекса следующей сделки после завершения обработки тестером предыдущей. Таким образом мы будем выбирать последовательно все сделки истории, которые будут обрабатываться тестером в момент наступления времени, записанного в свойствах текущей выбранной сделки;
- CheckTime() проверяет момент наступления в тестере времени, записанного в свойствах текущей исторической сделки. Логика такова: есть выбранная сделка, которую нужно обработать в тестере. До тех пор, пока время в тестере меньше времени, записанного в сделке, ничего не делаем — просто идём на следующий тик. Как только время в тестере становится равным, либо больше времени в текущей выбранной сделке (время в тестере может не совпасть со временем в сделке, поэтому время проверяется и на "больше"), сделка обрабатывается тестером в зависимости от её типа и способа изменения позиции сделкой. Далее эта сделка помечается обработанной, устанавливается индекс следующей сделки, и процесс ожидания времени, контролируемый этим методом, продолжается, но уже для следующей сделки:
- Обработчик OnTester() вызывается из стандартного обработчика OnTester() советника, выводит в журнал имя символа, количество исторических и обработанных тестером сделок, и возвращает количество обработанных сделок по символу торгового объекта;
Класс имеет два конструктора — по умолчанию и параметрический.
В параметрическом конструкторе, в формальных параметрах, передаётся наименование символа торгового объекта, которое устанавливается при его создании, и в торговый объект класса CTrade устанавливаются режим расчёта маржи,в соответствии с настройками текущего счета, и тип ордера по исполнению, согласно настройкам символа торгового объекта:
//--- Конструкторы/деструктор 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::Добавляет сделку в массив сделок | //+------------------------------------------------------------------+ 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; }
В метод передаётся указатель на объект сделки. Если сделка с таким тикетом уже есть в списке — просто возвращается true. Иначе — список сортируется по времени сделок в миллисекундах, и сделка добавляется в список в порядке сортировки по времени в мс.
Метод, возвращающий указатель на объект сделки по времени в секундах:
//+------------------------------------------------------------------+ //| 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); }
В метод передаётся искомое время. Временному объекту сделки устанавливаем переданное в метод время, список сортируется по времени в миллисекундах, и в нём ищется индекс сделки, у которой время равно переданному в метод (установленному во временный объект). Далее возвращается указатель на сделку в списке по найденному индексу. Если сделки с таким временем нет в списке, то индекс будет равен -1, и из списка будет возвращён NULL.
Интересно, что сделка ищется по времени в секундах, но список мы сортируем по времени в миллисекундах. Тесты показали, что если список сортировать тоже в секундах, то некоторые сделки в нём не находятся, хотя они точно есть. Скорее всего, это связано с тем, что в одной секунде есть несколько сделок со временем в миллисекундах. И возвращается указатель на уже обработанную ранее сделку, так как несколько сделок имеют одинаковое время в секундах.
Метод, возвращающий указатель на сделку открытия по идентификатору позиции:
//+------------------------------------------------------------------+ //|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; }
В метод передаётся идентификатор позиции, открывающую сделку которой необходимо найти. Далее в цикле по списку сделок получаем сделку, у которой идентификатор позиции равен переданному в метод, и возвращаем указатель на сделку, у которой способ изменения позиции равен "Вход в рынок" (DEAL_ENTRY_IN).
Метод, возвращающий указатель на объект сделки по индексу в списке:
//+------------------------------------------------------------------+ //| CSymbolTrade::Возвращает объект сделки по индексу в списке | //+------------------------------------------------------------------+ CDeal *CSymbolTrade::GetDealByIndex(const int index) { return this.m_list_deals.At(index); }
Просто возвращаем указатель на объект в списке по переданному в метод индексу. Если индекс некорректен, то будет возвращён NULL.
Метод, возвращающий указатель на сделку, на которую указывает индекс текущей сделки:
//+------------------------------------------------------------------+ //| Возвращает сделку, на которую указывает индекс текущей сделки | //+------------------------------------------------------------------+ CDeal *CSymbolTrade::GetDealCurrent(void) { this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC); return this.GetDealByIndex(this.m_index_next_deal); }
Список сделок сортируется по времени в миллисекундах, и возвращается указатель на сделку, индекс которой записан в переменную класса m_index_next_deal.
Метод, возвращающий текущую цену 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; }
Получаем данные последнего тика в структуру цен m_tick и возвращаем из неё цену Bid.
Метод, возвращающий текущую цену 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; }
Получаем данные последнего тика в структуру цен m_tick и возвращаем из неё цену Ask.
Метод, возвращающий текущее время в секундах:
//+------------------------------------------------------------------+ //| 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; }
Получаем данные последнего тика в структуру цен m_tick и возвращаем из неё время.
Метод, возвращающий текущее время в миллисекундах:
//+------------------------------------------------------------------+ //| 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; }
Получаем данные последнего тика в структуру цен m_tick и возвращаем из неё время в миллисекундах.
Метод, открывающий длинную позицию:
//+------------------------------------------------------------------+ //| 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(); }
В метод передаются параметры открываемой длинной позиции, в торговый объект устанавливается требуемый магик позиции и отсылается приказ на открытие длинной позиции с указанными параметрами. При ошибке открытия позиции, возвращается ноль, в случае успеха — тикет ордера, на основе которого была открыта позиция.
Метод, открывающий короткую позицию:
//+------------------------------------------------------------------+ //| 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(); }
Аналогично предыдущему методу, но открывается короткая позиция.
Метод, закрывающий позицию по тикету:
//+------------------------------------------------------------------+ //| CSymbolTrade::Закрывает позицию по тикету | //+------------------------------------------------------------------+ bool CSymbolTrade::ClosePos(const ulong ticket) { return this.m_trade.PositionClose(ticket); }
Возвращает результат вызова метода PositionClose() торгового объекта класса CTrade.
Торговый класс символа готов. Теперь реализуем его в советнике для работы с историческими сделками, сохранёнными в файле.
Разбираем в тестере историю сделок из файла
Перейдём к файлу советника \MQL5\Experts\TradingByHistoryDeals\TradingByHistoryDeals.mq5 и впишем временный объект только что созданного торгового класса символа — он потребуется для поиска нужного объекта в списке, в котором будут храниться указатели на такие объекты:
//+------------------------------------------------------------------+ //| 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;
У нас есть массив исторических сделок, на основе которого мы можем создать список торговых объектов, внутри которых будут списки сделок, принадлежащих символу объекта. В массиве сделок хранятся структуры, описывающие сделки. Так как в торговом объекте будут содержаться списки объектов сделок, нам нужно создать функцию, создающую новый объект сделки, и заполняющий свойства сделки из полей структуры, описывающей сделку:
//+------------------------------------------------------------------+ //| Создаёт объект сделки из структуры | //+------------------------------------------------------------------+ 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; }
В функцию передаётся структура сделки, создаётся новый объект сделки, и его свойства заполняются значениями из полей структуры.
Функция возвращает указатель на вновь созданный объект. При ошибке создания объекта, возвращается NULL.
Напишем функцию, создающую список торговых объектов символов:
//+------------------------------------------------------------------+ //| Создаёт массив используемых символов | //+------------------------------------------------------------------+ 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; }
Логика функции подробно расписана в комментариях. В цикле по списку исторических сделок анализируем каждую очередную сделку. Проверяем её символ и, если торгового объекта для этого символа ещё нет, — создаём новый торговый объект и сохраняем его в списке. Если уже есть — просто получаем указатель на торговый объект символа сделки из списка. Далее, точно так же проверяем наличие такой сделки в списке сделок торгового объекта и добавляем её в список при отсутствии. В результате прохода в цикле по всем историческим сделкам, получаем список торговых объектов по символам, в которых содержатся списки сделок, принадлежащих символу объекта.
Список торговых объектов можно вывести в журнал при помощи функции:
//+------------------------------------------------------------------+ //| Выводит в журнал список торговых объектов символов | //+------------------------------------------------------------------+ 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()); } }
В цикле по списку торговых объектов символов получаем очередной объект и выводим в журнал его описание. Выглядит в журнале это примерно так:
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
У нас появился объект класса сделки. Добавим функцию, возвращающую описание сделки:
//+------------------------------------------------------------------+ //| Возвращает описание сделки | //+------------------------------------------------------------------+ 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()))); }
Эта функция полностью повторяет логику точно такой же функции, возвращающей описание структуры сделки. Но здесь вместо структуры сделки в функцию передаётся указатель на объект сделки.
Теперь оформим обработчик OnInit() до логического завершения.
Добавим обработку запуска советника в тестере, создадим список торговых объектов и обратимся к каждому символу, которые использовались в торговле для подгрузки их истории и открытия окон графиков этих символов в тестере:
//+------------------------------------------------------------------+ //| 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); }
В обработчике OnDeinit() советника очистим созданные массивы и списки:
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- очищаем созданные списки и массивы ExtListSymbols.Clear(); ArrayFree(ExtArrayDeals); }
В обработчике OnTick() советника в тестере обрабатываем список сделок из файла:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- работаем только в тестере стратегий if(!MQLInfoInteger(MQL_TESTER)) return; //--- Обрабатываем в тестере список сделок из файла TradeByHistory(InpTestedSymbol, InpTestedMagic); }
Рассмотрим эту функцию подробнее. Вообще, логика обработки сделок истории изначально представлялась такой:
- получили время тика,
- получили сделку с таким временем,
- обрабатываем сделку в тестере.
На первый взгляд, простая и логичная схема при реализации потерпела полное фиаско. Дело в том, что в тестере не всегда время тика совпадает с временем сделки. Даже в миллисекундах. И при тестировании по всем тикам, на основе реальных тиков, взятых с того же самого сервера, сделки терялись. Время тика знаем, точно знаем, что на этом самом времени есть сделка, но тестер её не видит — нет тика с таким временем, как у сделки. Но есть тик со временем до-, и со временем после времени сделки. Соответственно, логика может быть построена не на тиках с их временем, а на сделках:
- сделки отсортированы в списке по времени их появления в миллисекундах. Устанавливаем индекс самой первой сделки как индекс текущей,
- выбираем сделку по индексу текущей сделки и получаем её время;
- ждём тик с таким временем:
- если время тика меньше времени сделки — ждём следующий тик,
- если время тика равно или больше времени сделки — обрабатываем сделку, записываем в неё факт того, что она уже обработана, и устанавливаем индекс следующей сделки как индекс текущей;
- пока тест не окончен, повторяем с пункта 2.
Такая схема позволяет дождаться времени каждой последующей сделки и исполнить её в тестере. При этом, на цену сделки внимания не обращаем — просто копируем сделки по наступлению их времени. Даже если время тика в тестере уже чуть позже, чем у реальной сделки — ничего страшного. Главное — скопировать торговлю. Фактом того, что сделка уже обработана тестером, будет ненулевое значение свойства сделки "тикет позиции в тестере". Если это значение равно нулю — значит, эта сделка ещё не обрабатывалась в тестере. После исполнения этой сделки в тестере, в это её свойство вписывается тикет позиции, к которой относится эта сделка в тестере.
Напишем функцию, в которой реализована описанная выше логика:
//+------------------------------------------------------------------+ //| Торговля по истории | //+------------------------------------------------------------------+ 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(); } } }
Этот код полностью копирует оригинальную торговлю, которая велась на счёте, и сделки которой записаны в файл. Все позиции открываются без стоп-приказов. То есть, значения для StopLoss и TakeProfit не копируются из реальных сделок в методы открытия позиций. Это упрощает отслеживание сделок, так как сделки закрытия тоже есть в списке, и тестер их отрабатывает, независимо от того, как была закрыта позиция — по StopLoss или по TakeProfit.
Скомпилируем советник и запустим его на графике. В итоге, будет создан файл HistoryDealsData.bin, в общей папке клиентских терминалов, по пути типа "C:\Users\UserName\AppData\Roaming\MetaQuotes\Terminal\Common\Files", в подпапке TradingByHistoryDeals, и будет выведен на график алерт с сообщением о желательных настройках тестера:
Давайте теперь запустим советник в тестере, выбрав в настройках тестера указанный диапазон дат, начальный депозит и плечо:
Тест запустим по всем проторгованным символам и магикам:
Получается, что вся торговля принесла нам минус 550 долларов. Интересно, а если бы мы ставили иные стоп-приказы?
Давайте проверим.
Скорректируем стоп-приказы
Сохраним советник в той же папке \MQL5\Experts\TradingByHistoryDeals\ под новым именем TradingByHistoryDeals_SLTP.mq5.
Добавим перечисление методов тестирования, входные параметры разобьём по группам, добавив группу для установки параметров стоп-приказов, и две новые переменные глобального уровня для передачи через них значений StopLoss и TakeProfit в торговые объекты:
//+------------------------------------------------------------------+ //| 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;
В обработчике OnInit() скорректируем и запишем в переменные значения стоп-приказов, устанавливаемые входными параметрами:
int OnInit() { //--- Корректируем размеры стопов ExtStopLoss =(InpStopLoss<1 ? 0 : InpStopLoss); ExtTakeProfit=(InpTakeProfit<1 ? 0 : InpTakeProfit); //--- Если советник запущен не в тестере
Добавим функции, рассчитывающие корректные значения для цен StopLoss и TakeProfit относительно уровня StopLevel, установленного для символа:
//+------------------------------------------------------------------+ //| Возвращает корректный 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); }
Для установки уровней StopLoss и TakeProfit, цена стоп-приказа не должна находиться ближе к текущей цене на расстояние StopLevel. Если уровень StopLevel для символа имеет нулевое значение, значит используется размер StopLevel, равный двум, иногда трём размерам спреда, установленного для символа. В этих функциях используется двойной множитель спреда. Эта величина передаётся в формальных параметрах функций, и имеет значение по умолчанию 2. При необходимости изменения значения множителя, нужно передать в функции, при их вызове, иное требуемое значение. Функции возвращают корректные цены для StopLoss и TakeProfit.
В функции торговли по истории сделок TradeByHistory() впишем новые блоки кода, где учитывается режим торговли в тестере и установка значений StopLoss и TakeProfit в случае, если выбрано тестирование с указанными значениями стоп-приказов. В блоке закрытия позиций закрывать их нужно только при типе тестирования "оригинальная торговля". Если выбрано тестирование с указанными значениями стоп-приказов, то сделки закрытия должны игнорироваться — тестер сам будет закрывать позиции по выставленным значениям StopLoss и TakeProfit. Единственное, что нужно делать при торговле со стоп-приказами в случае обработки сделок закрытия — это помечать их обработанными и переходить к следующей сделке.
//+------------------------------------------------------------------+ //| Торговля по истории | //+------------------------------------------------------------------+ 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(); } } }
В обработчике OnTester() советника посчитаем и вернём общее количество обработанных в тестере сделок:
//+------------------------------------------------------------------+ //| 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); }
Кроме того, здесь для каждого торгового объекта символа вызывается его собственный обработчик OnTester(), которые распечатывают в журнале свои данные. По окончании тестирования, в журнале тестера получим такие сообщения:
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
Скомпилируем советник и запустим его с теми же настройками тестирования, но указав тип тестирования как "Specified StopLoss and TakeProfit values", установив размеры StopLoss и TakeProfit равными 100 и 500 пунктов соответственно:
В прошлом тесте, при тестировании оригинальной торговли, мы получили убыток в 550 долларов. Сейчас, заменив StopLoss всех позиций на значение в 100 пунктов, а TakeProfit — на 500 пунктов, мы получили прибыль в 590 пунктов. И это при простой замене стоп-приказов, не глядя на специфику разных торгуемых символов. Если для каждого из проторгованных символов подобрать свои размеры стоп-приказов, то график тестирования, вполне вероятно, можно и выровнять.
Заключение
Сегодня мы провели небольшой эксперимент с торговой историей в стиле "А что бы было, если...". Думаю, такие эксперименты вполне могут натолкнуть на интересные решения по изменению своей торговли. И в следующей статье мы проведём ещё один такой эксперимент — подключим сюда различные трейлинги позиций. Будет интересно.
К статье прикреплены файлы всех рассмотренных сегодня советников и классов. Вы можете их скачать, изучить и самостоятельно поэкспериментировать со своими торговыми счетами. Папку с архивом можно сразу распаковать в свой каталог MQL5 клиентского терминала, и все файлы будут размещены в нужных подпапках.
Программы, используемые в статье:
# | Имя | Тип | Описание |
---|---|---|---|
1 | SymbolTrade.mqh | Библиотека класса | Библиотека структуры и класса сделки, торговый класс символа |
2 | TradingByHistoryDeals.mq5 | Советник | Советник для просмотра в тестере сделок и трейдов, осуществляемых на счёте |
3 | TradingByHistoryDeals_SLTP.mq5 | Советник | Советник для просмотра и изменения при помощи StopLoss и TakeProfit в тестере сделок и трейдов, осуществляемых на счёте |
4 | MQL5.zip | Архив | Архив файлов, представленных выше, для распаковки в каталог MQL5 клиентского терминала |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
В чём ошибка?
Если файл меньше, чем массив до чтения, то размер массива не изменится.
На подобном можно попасть и при использовании ArrayCopy.Хорошую функцию игнорируете
В чём преимущество?
В чём преимущество?
В лаконичности и быстроте выполнения (полностью на стороне MQ).
Покажите стандартные самораспечатывающиеся и самозаполняющиеся структуры, пожалуйста.
В лаконичности и быстроте выполнения (полностью на стороне MQ).