Торговый инструментарий MQL5 (Часть 4): Разработка EX5-библиотеки для управления историей
Введение
В этой увлекательной серии статей мы разработали две комплексные EX5-библиотеки: PositionsManager.ex5, который обрабатывает и управляет позициями, и PendingOrdersManager.ex5, которая обрабатывает отложенные ордера. Наряду с этим мы создали практические примеры, в том числе с графическими пользовательскими интерфейсами, чтобы продемонстрировать эффективную реализацию этих библиотек.
В этой статье мы представим еще одну важную EX5-библиотеку, предназначенную для извлечения и обработки истории исполненных ордеров, сделок и позиций. Кроме того, мы разработаем аналитические модули для создания торговых отчетов, которые оценивают эффективность торговых систем, советников или конкретных символов на основе различных гибких критериев.
Данная статья представляет собой практическое руководство для начинающих разработчиков MQL5, которым может быть сложно работать с позициями, ордерами и историями сделок. Он также станет ценным ресурсом для любого программиста MQL5, которому нужна библиотека для упрощения и повышения эффективности обработки торговой истории.
Для начала мы рассмотрим несколько важных вопросов, которые многим программистам MQL5, особенно новичкам в обработке истории сделок в MetaTrader 5, часто сложно понять.
Каков жизненный цикл сделки в MQL5?
В MQL5 жизненный цикл сделки начинается с исполнения ордера. Ордера могут быть прямыми рыночными или отложенными.
Рыночный ордер
Прямой рыночный ордер — это запрос в режиме реального времени на покупку или продажу актива по текущей рыночной цене (Ask или Bid). Ранее мы рассмотрели, как обрабатывать эти ордера в первой и второй статьях при разработке библиотеки Positions Manager.

Прямой рыночный ордер исполняется немедленно, что делает его идеальным для ручных и автоматизированных торговых стратегий. После исполнения ордер переходит в активную открытую позицию и ему присваивается уникальный номер тикета и отдельный идентификатор позиции (POSITION_ID), что более надежно для отслеживания и управления различными этапами позиции на протяжении ее жизненного цикла.
Отложенный ордер
Отложенный ордер (BUY STOP, BUY LIMIT, SELL STOP, SELL LIMIT, BUY STOP LIMIT и SELL STOP LIMIT), напротив, представляет собой отложенный ордер, срабатывающий при достижении определенного уровня цены. Подробное руководство по обработке таких ордеров приведено в третьей статье серии, в которой мы разрабатываем библиотеку Pending Orders Manager.

До тех пор, пока рыночная цена не совпадет с предопределенной ценой срабатывания отложенного ордера, он останется неактивным. После срабатывания он превращается в рыночный и выполняется, получая уникальный номер тикета и идентификатор позиции (POSITION_ID) аналогично прямому рыночному.
Как статус позиции может измениться в течение ее срока действия?
На протяжении всего срока существования позиции ее статус может меняться из-за различных факторов:
- Частичное закрытие: Если часть позиции закрыта, в торговой истории будет зарегистрирована соответствующая сделка exit (out) deal (сделка выхода).
- Разворот позиции: Транзакция close by также записывается как exit deal.
- Полное закрытие: Когда вся позиция закрывается либо вручную, либо автоматически с помощью тейк-профита, стоп-лосса или стоп-аута (при маржин-колле), в торговой истории регистрируется окончательная сделка выхода.
Понимание жизненного цикла торговой операции в MQL5 имеет ключевое значение. Каждая сделка начинается с ордера, отправляемого на торговый сервер, будь то запрос на открытие отложенного ордера, исполнение прямого рыночного ордера на покупку или продажу или частичное закрытие существующей позиции. Независимо от типа все торговые операции сначала регистрируются как ордера.
Если ордер успешно выполнен, он переходит на следующий этап и сохраняется в базе данных истории как сделка. Используя различные свойства и функции, доступные для ордеров и сделок, мы можем отследить каждую сделку до ее исходного ордера и связать ее с соответствующей позицией. Это позволяет получить четкое и систематическое представление о жизненном цикле сделки.
Такой подход позволяет отслеживать происхождение, развитие и результат каждой сделки в среде MQL5. Он предоставляет полную раскладку, включая ордер, инициировавший сделку, точное время исполнения, любые изменения, внесенные в процессе, и конечный результат торговли (позицию). Такое отслеживание не только повышает прозрачность, но и дает вам как MQL5-программисту возможность разрабатывать алгоритмы для анализа торговых стратегий, выявления областей для улучшения и оптимизации производительности.
В MQL5 позиция представляет собой активную на данный момент сделку (position), которые вы в настоящее время держите на рынке. Она находится в открытом состоянии. Позиция может быть на покупку или продажу на конкретном символе. С другой стороны, сделка представляет собой завершенную транзакцию — полностью закрытую позицию. Активные открытые позиции и отложенные ордера отображаются в окне "Инструменты" на вкладке "Торговля".

Закрытые позиции , наряду с ордерами и сделками, отображаются на вкладке "История" окна "Инструменты".

Чтобы получить доступ к полной истории позиций, вы можете воспользоваться меню платформы и выбрать пункт "Позиции". Доступ к истории ордеров и сделок также можно получить с помощью тех же пунктов меню.

Различие между позициями и сделками может сбивать с толку начинающих MQL5-программистов, особенно при использовании стандартных функций истории платформы. Эта статья, а также подробный код в библиотеке, которую мы собираемся создать, дадут вам четкое представление о том, как позиции и сделки классифицируются и отслеживаются в MQL5. Если у вас мало времени и вам нужна готовая к использованию библиотека истории, вы можете просто следовать подробной документации в следующей статье о том, как внедрить ее непосредственно в свой проект.
Создание файла исходного кода библиотеки History Manager (.mq5)
Откройте MetaEditor IDE и запустите Мастер MQL, выбрав "Создать" в меню. В Мастере выберите создание нового исходного файла библиотеки, который мы назовем HistoryManager.mq5. Этот файл станет основой для наших базовых функций, которые направлены на управление и анализ истории торговли на счете. При создании новой библиотеки HistoryManager.mq5, сохраните ее в папку Libraries\Toolkit, созданную в первой статье. Сохранив этот новый файл в том же каталоге, что и EX5-библиотеки Positions Manager и Pending Orders Manager, мы сможем поддерживать четкую и последовательную организационную структуру нашего проекта. Такой подход облегчит обнаружение и управление каждым компонентом по мере расширения нашего инструментария.

Вот как выглядит наш недавно созданный исходный файл HistoryManager.mq5. Начнем с удаления комментариев в разделе My function под директивами property. Директивы свойств copyright и link в вашем файле могут отличаться, но это не повлияет на поведение или производительность кода. Вы можете настроить директивы copyright и link, добавив любую необходимую информацию, однако свойство library менять нельзя.
//+------------------------------------------------------------------+ //| HistoryManager.mq5 | //| Copyright 2024, Wanateki Solutions Ltd. | //| https://www.wanateki.com | //+------------------------------------------------------------------+ #property library #property copyright "Copyright 2024, Wanateki Solutions Ltd." #property link "https://www.wanateki.com" #property version "1.00" //+------------------------------------------------------------------+ //| My function | //+------------------------------------------------------------------+ // int MyCalculator(int value,int value2) export // { // return(value+value2); // } //+------------------------------------------------------------------+
Структуры данных, директивы препроцессора и глобальные переменные
Определим следующие компоненты в нашем только что созданном исходном файле библиотеки HistoryManager.mq5:
- Директивы препроцессора: Помогут сортировать и запрашивать различные типы истории торговли.
- Структуры данных: Будут хранить исторические данные по ордерам, сделкам, позициям и отложенным ордерам.
- Глобальные динамические структурные массивы: Будут содержать все соответствующие исторические данные в библиотеке.
Определение этих компонентов в глобальной области действия гарантирует, что они будут доступны во всей библиотеке и могут использоваться всеми ее различными модулями или функциями.
Директивы препроцессора
Поскольку наша библиотека управления историей будет обрабатывать различные типы запросов, крайне важно спроектировать ее таким образом, чтобы она извлекала только конкретные данные истории, необходимые для каждого запроса. Такой модульный и целенаправленный подход повысит производительность нашей библиотеки, сохраняя при этом гибкость для различных вариантов использования.
Для этого мы определим целочисленные константы, которые будут служить идентификаторами для определенных типов исторических данных. Эти константы позволят библиотеке выбирать только необходимые данные, обеспечивая минимальное потребление ресурсов и более быструю обработку.
Организуем исторические данные по пяти основным категориям:
- История ордеров.
- История сделок.
- История позиций.
- История отложенных ордеров.
- Все исторические данные.
Используя эти константы, функции в библиотеке могут указывать тип истории, которую они хотят обрабатывать. Основная функция извлечения истории будет запрашивать и возвращать только запрошенные данные, экономя время и вычислительные ресурсы. Начнем с определения целочисленных констант, разместив их непосредственно под последними директивами #property в нашем коде.
#define GET_ORDERS_HISTORY_DATA 1001 #define GET_DEALS_HISTORY_DATA 1002 #define GET_POSITIONS_HISTORY_DATA 1003 #define GET_PENDING_ORDERS_HISTORY_DATA 1004 #define GET_ALL_HISTORY_DATA 1005
Структуры данных
Наша EX5-библиотека будет хранить различные исторические данные в глобально объявленных структурах данных. Эти структуры будут эффективно хранить историю сделок, ордеров, позиций и отложенных ордеров при каждом запросе.
//- Data structure to store deal properties struct DealData { ulong ticket; ulong magic; ENUM_DEAL_ENTRY entry; ENUM_DEAL_TYPE type; ENUM_DEAL_REASON reason; ulong positionId; ulong order; string symbol; string comment; double volume; double price; datetime time; double tpPrice; double slPrice; double commission; double swap; double profit; }; //- Data structure to store order properties struct OrderData { datetime timeSetup; datetime timeDone; datetime expirationTime; ulong ticket; ulong magic; ENUM_ORDER_REASON reason; ENUM_ORDER_TYPE type; ENUM_ORDER_TYPE_FILLING typeFilling; ENUM_ORDER_STATE state; ENUM_ORDER_TYPE_TIME typeTime; ulong positionId; ulong positionById; string symbol; string comment; double volumeInitial; double priceOpen; double priceStopLimit; double tpPrice; double slPrice; }; //- Data structure to store closed position/trade properties struct PositionData { ENUM_POSITION_TYPE type; ulong ticket; ENUM_ORDER_TYPE initiatingOrderType; ulong positionId; bool initiatedByPendingOrder; ulong openingOrderTicket; ulong openingDealTicket; ulong closingDealTicket; string symbol; double volume; double openPrice; double closePrice; datetime openTime; datetime closeTime; long duration; double commission; double swap; double profit; double tpPrice; double slPrice; int tpPips; int slPips; int pipProfit; double netProfit; ulong magic; string comment; }; //- Data structure to store executed or canceled pending order properties struct PendingOrderData { string symbol; ENUM_ORDER_TYPE type; ENUM_ORDER_STATE state; double priceOpen; double tpPrice; double slPrice; int tpPips; int slPips; ulong positionId; ulong ticket; datetime timeSetup; datetime expirationTime; datetime timeDone; ENUM_ORDER_TYPE_TIME typeTime; ulong magic; ENUM_ORDER_REASON reason; ENUM_ORDER_TYPE_FILLING typeFilling; string comment; double volumeInitial; double priceStopLimit; };
Глобальные динамические структурные массивы
Окончательные объявления в глобальной области видимости будут состоять из определенных ранее динамических массивов структур данных. Эти массивы будут служить основным хранилищем всех основных данных, управляемых нашей библиотекой.
OrderData orderInfo[]; DealData dealInfo[]; PositionData positionInfo[]; PendingOrderData pendingOrderInfo[];
Функция получения исторических данных
GetHistoryDataFunction() будет служить ядром нашей EX5-библиотеки, формируя ее функционал. Большинство других функций библиотеки будут зависеть от нее при извлечении истории торговли на основе указанных периодов и типов истории. Поскольку эта функция предназначена только для внутреннего использования, она не будет определена как экспортируемая.
Эта функция предназначена для извлечения запрошенных исторических данных за заданный период и тип истории. Это булева функция, то есть она будет возвращать true при успешном извлечении истории и false, - если что-то пойдет не так.
GetHistoryDataFunction() принимает три входных параметра:
- Две переменные datetime, fromDateTime и toDateTime, указывающее начало и конец желаемого периода.
- Беззнаковое целое число dataToGet, соответствующее одной из предопределенных констант в верхней части файла.
Объединяя эти входные данные, функция может эффективно запрашивать и обрабатывать необходимые исторические данные. Начнем с определения функции.
bool GetHistoryData(datetime fromDateTime, datetime toDateTime, uint dataToGet) { return(true); //-- Our function's code will go here }
Первой задачей нашей функции будет проверка корректности указанного диапазона дат. Так как тип данных datetime в MQL5 - по сути, целое число типа long, представляющее формат эпохи Unix (количество секунд, прошедших с 1 января 1970 года, 00:00:00 UTC), мы можем напрямую сравнить эти значения, чтобы убедиться в их правильности. Также обратите внимание, что при запросе исторических данных в MQL5 время рассчитывается на основе времени торгового сервера, а не вашего локального компьютера.
Чтобы проверить диапазон дат, убедимся, что значение fromDateTime меньше toDateTime. Если fromDateTime превышает или равно toDateTime, это указывает на недопустимый период, поскольку дата начала не может быть позже или равной дате окончания. Если указанный период не проходит проверку, возвращаем false и выходим из функции.
if(fromDateTime >= toDateTime) { //- Invalid time period selected Print("Invalid time period provided. Can't load history!"); return(false); }
После проверки дат и периода сбросим кэш ошибок MQL5, чтобы обеспечить точность кодов ошибок в случае возникновения каких-либо проблем. Далее вызовем функцию HistorySelect() в пределах оператора if-else, передав проверенные значения datetime для извлечения истории сделок и ордеров для тестируемого периода. Так как HistorySelect() возвращает булево значение, она вернет true, если успешно найдет историю для обработки или false, если обнаружит ошибку или не сможет получить данные.
ResetLastError(); if(HistorySelect(fromDateTime, toDateTime)) //- History selected ok { //-- Code to process the history data will go here } else //- History selecting failed { Print("Selecting the history failed. Error code = ", GetLastError()); return(false); }
В части else оператора if-else мы добавили код для регистрации сообщения, указывающего на то, что выбор истории не удался, вместе с кодом ошибки, перед выходом из функции и возвратом булева значения false. В части if мы используем оператор switch для вызова соответствующих функций для обработки загруженных данных истории торговли на основе значения dataToGet.
switch(dataToGet) { case GET_DEALS_HISTORY_DATA: //- Get and save only the deals history data SaveDealsData(); break; case GET_ORDERS_HISTORY_DATA: //- Get and save only the orders history data SaveOrdersData(); break; case GET_POSITIONS_HISTORY_DATA: //- Get and save only the positions history data SaveDealsData(); //- Needed to generate the positions history data SaveOrdersData(); //- Needed to generate the positions history data SavePositionsData(); break; case GET_PENDING_ORDERS_HISTORY_DATA: //- Get and save only the pending orders history data SaveOrdersData(); //- Needed to generate the pending orders history data SavePendingOrdersData(); break; case GET_ALL_HISTORY_DATA: //- Get and save all the history data SaveDealsData(); SaveOrdersData(); SavePositionsData(); SavePendingOrdersData(); break; default: //-- Unknown entry Print("-----------------------------------------------------------------------------------------"); Print(__FUNCTION__, ": Can't fetch the historical data you need."); Print("*** Please specify the historical data you need in the (dataToGet) parameter."); break; }
Ниже приведен полный код GetHistoryDataFunction().
bool GetHistoryData(datetime fromDateTime, datetime toDateTime, uint dataToGet) { //- Check if the provided period of dates are valid if(fromDateTime >= toDateTime) { //- Invalid time period selected Print("Invalid time period provided. Can't load history!"); return(false); } //- Reset last error and get the history ResetLastError(); if(HistorySelect(fromDateTime, toDateTime)) //- History selected ok { //- Get the history data switch(dataToGet) { case GET_DEALS_HISTORY_DATA: //- Get and save only the deals history data SaveDealsData(); break; case GET_ORDERS_HISTORY_DATA: //- Get and save only the orders history data SaveOrdersData(); break; case GET_POSITIONS_HISTORY_DATA: //- Get and save only the positions history data SaveDealsData(); //- Needed to generate the positions history data SaveOrdersData(); //- Needed to generate the positions history data SavePositionsData(); break; case GET_PENDING_ORDERS_HISTORY_DATA: //- Get and save only the pending orders history data SaveOrdersData(); //- Needed to generate the pending orders history data SavePendingOrdersData(); break; case GET_ALL_HISTORY_DATA: //- Get and save all the history data SaveDealsData(); SaveOrdersData(); SavePositionsData(); SavePendingOrdersData(); break; default: //-- Unknown entry Print("-----------------------------------------------------------------------------------------"); Print(__FUNCTION__, ": Can't fetch the historical data you need."); Print("*** Please specify the historical data you need in the (dataToGet) parameter."); break; } } else { Print(__FUNCTION__, ": Selecting the history failed. Error code = ", GetLastError()); return(false); } return(true); }
Если вы сохраните и попытаетесь скомпилировать наш файл с исходным кодом на этом этапе, вы столкнетесь с многочисленными ошибками компиляции и предупреждениями. Это связано с тем, что многие функции, упомянутые в коде, еще не созданы. Мы все еще находимся на ранней стадии разработки нашей EX5-библиотеки. Как только все недостающие функции будут реализованы, файл будет компилироваться без каких-либо ошибок или предупреждений.
Функция сохранения данных о сделках
Функция SaveDealsData() будет отвечать за извлечение и сохранение всей истории сделок, доступной в данный момент в кэше истории торговли за различные периоды, которые будут запрошены различными функциями в библиотеке. Он не возвращает никаких данных и не определен как экспортируемый, поскольку вызывается внутри библиотеки, а именно - из функции GetHistoryData(). Функция будет использовать стандартные функции MQL5 HistoryDealGet... для извлечения различных свойств сделки и сохранения их в динамическом массиве структуры данных dealInfo.
Для начала давайте создадим определение, или сигнатуру функции.
void SaveDealsData() { //-- Our function's code will go here }
Так как SaveDealsData() вызывается в пределах функции GetHistoryData(), нет необходимости снова вызывать HistorySelect() перед обработкой торговой истории. Первый шаг в функции SaveDealsData() - проверка наличия истории сделок для обработки. Введем ее с помощью функции HistoryDealsTotal(), которая возвращает общее количество сделок, доступных в кэше истории. Для эффективности мы создадим целое число и назовем его totalDeals для хранения общей истории сделок и беззнакового dealTicket long-типа для хранения идентификаторов тикетов сделок.
int totalDeals = HistoryDealsTotal(); ulong dealTicket;
Если сделки не доступны или не найдены (totalDeals равен 0 или меньше), запишем сообщение, указывающее на это, а затем выйдем из функции раньше времени, чтобы избежать ненужной обработки.
if(totalDeals > 0) { //-- Code to process deal goes here } else { Print(__FUNCTION__, ": No deals available to be processed, totalDeals = ", totalDeals); }
Если история сделок существует, следующим шагом будет подготовка массива для хранения извлеченных данных. Для этой задачи мы используем динамический массив dealInfo и начнем с изменения его размера, чтобы он соответствовал общему количеству сделок, используя функцию ArrayResize(), гарантирующую наличие достаточного объема для хранения всех соответствующих свойств сделки.
ArrayResize(dealInfo, totalDeals); Затем мы пройдемся по сделкам в обратном порядке, начиная с самой последней, используя цикл for. Для каждой сделки мы используем функцию HistoryDealGetTicket() для извлечения уникального тикета. Если извлечение тикетов прошло успешно, мы извлечем и сохраним различные свойства сделки. Мы будем хранить каждое свойство в соответствующем поле в массиве dealInfo по индексу, соответствующему текущей итерации цикла.
Если функция HistoryDealGetTicket() не сможет получить действительный тикет для какой-либо сделки, мы зарегистрируем сообщение об ошибке, включая код ошибки, для отладки. Это обеспечит прозрачность в случае возникновения непредвиденных проблем при возврате свойств.
for(int x = totalDeals - 1; x >= 0; x--) { ResetLastError(); dealTicket = HistoryDealGetTicket(x); if(dealTicket > 0) { //- Deal ticket selected ok, we can now save the deals properties dealInfo[x].ticket = dealTicket; dealInfo[x].entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(dealTicket, DEAL_ENTRY); dealInfo[x].type = (ENUM_DEAL_TYPE)HistoryDealGetInteger(dealTicket, DEAL_TYPE); dealInfo[x].magic = HistoryDealGetInteger(dealTicket, DEAL_MAGIC); dealInfo[x].positionId = HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID); dealInfo[x].order = HistoryDealGetInteger(dealTicket, DEAL_ORDER); dealInfo[x].symbol = HistoryDealGetString(dealTicket, DEAL_SYMBOL); dealInfo[x].comment = HistoryDealGetString(dealTicket, DEAL_COMMENT); dealInfo[x].volume = HistoryDealGetDouble(dealTicket, DEAL_VOLUME); dealInfo[x].price = HistoryDealGetDouble(dealTicket, DEAL_PRICE); dealInfo[x].time = (datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME); dealInfo[x].tpPrice = HistoryDealGetDouble(dealTicket, DEAL_TP); dealInfo[x].slPrice = HistoryDealGetDouble(dealTicket, DEAL_SL); dealInfo[x].commission = HistoryDealGetDouble(dealTicket, DEAL_COMMISSION); dealInfo[x].swap = HistoryDealGetDouble(dealTicket, DEAL_SWAP); dealInfo[x].reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(dealTicket, DEAL_REASON); dealInfo[x].profit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT); } else { Print( __FUNCTION__, " HistoryDealGetTicket(", x, ") failed. (dealTicket = ", dealTicket, ") *** Error Code: ", GetLastError() ); } }
Ниже приведен полный код функции SaveDealsData().
void SaveDealsData() { //- Get the number of loaded history deals int totalDeals = HistoryDealsTotal(); ulong dealTicket; //- //- Check if we have any deals to be worked on if(totalDeals > 0) { //- Resize the dynamic array that stores the deals ArrayResize(dealInfo, totalDeals); //- Let us loop through the deals and save them one by one for(int x = totalDeals - 1; x >= 0; x--) { ResetLastError(); dealTicket = HistoryDealGetTicket(x); if(dealTicket > 0) { //- Deal ticket selected ok, we can now save the deals properties dealInfo[x].ticket = dealTicket; dealInfo[x].entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(dealTicket, DEAL_ENTRY); dealInfo[x].type = (ENUM_DEAL_TYPE)HistoryDealGetInteger(dealTicket, DEAL_TYPE); dealInfo[x].magic = HistoryDealGetInteger(dealTicket, DEAL_MAGIC); dealInfo[x].positionId = HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID); dealInfo[x].order = HistoryDealGetInteger(dealTicket, DEAL_ORDER); dealInfo[x].symbol = HistoryDealGetString(dealTicket, DEAL_SYMBOL); dealInfo[x].comment = HistoryDealGetString(dealTicket, DEAL_COMMENT); dealInfo[x].volume = HistoryDealGetDouble(dealTicket, DEAL_VOLUME); dealInfo[x].price = HistoryDealGetDouble(dealTicket, DEAL_PRICE); dealInfo[x].time = (datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME); dealInfo[x].tpPrice = HistoryDealGetDouble(dealTicket, DEAL_TP); dealInfo[x].slPrice = HistoryDealGetDouble(dealTicket, DEAL_SL); dealInfo[x].commission = HistoryDealGetDouble(dealTicket, DEAL_COMMISSION); dealInfo[x].swap = HistoryDealGetDouble(dealTicket, DEAL_SWAP); dealInfo[x].reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(dealTicket, DEAL_REASON); dealInfo[x].profit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT); } else { Print( __FUNCTION__, " HistoryDealGetTicket(", x, ") failed. (dealTicket = ", dealTicket, ") *** Error Code: ", GetLastError() ); } } } else { Print(__FUNCTION__, ": No deals available to be processed, totalDeals = ", totalDeals); } }
Функция отображения истории сделок
Функция PrintDealsHistory() предназначена для извлечения и отображения исторических данных о сделках за указанный период. Эта функция будет полезна в ситуациях, когда вам необходимо проанализировать ряд торговых данных за определенный период времени. Она не возвращает никаких данных, а вместо этого выводит информацию о сделке в журнал MetaTrader 5 для проверки. Эту функцию можно вызвать извне, чтобы предоставить пользователям информацию о прошлых сделках, используя функцию GetHistoryData() для извлечения соответствующих данных.
Начнем с определения функции PrintDealsHistory(). Функция требует два параметра: fromDateTime и toDateTime, которые представляют время начала и окончания периода, в котором мы хотим выполнить поиск. Функция извлечет сделки, которые были заключены в течение этого периода времени. Обратите внимание, что функция отмечена как export (экспортируемая), то есть ее можно вызывать из других программ или библиотек, что делает ее легко доступной для внешнего использования.
void PrintDealsHistory(datetime fromDateTime, datetime toDateTime) export { //-- Our function's code will go here }
Далее вызываем функцию GetHistoryData(), передающую fromDateTime, toDateTime и дополнительную константу GET_DEALS_HISTORY_DATA. Это указывает функции на необходимость извлечения соответствующих торговых данных между указанными начальным и конечным временем. Этот вызов функции гарантирует, что информация о сделке за желаемый период будет извлечена и сохранена в массиве dealInfo.
GetHistoryData(fromDateTime, toDateTime, GET_DEALS_HISTORY_DATA);
После того как данные о сделке извлечены, нам нужно проверить, доступны ли какие-либо данные. Используем функцию ArraySize() для получения общего количества сделок, хранящихся в массиве dealInfo. Если сделки не найдены (то есть totalDeals равен 0), мы регистрируем сообщение, информирующее пользователя, и выходим из функции. Если сделок для отображения нет, функция завершается раньше времени, что экономит время и предотвращает ненужные операции.
int totalDeals = ArraySize(dealInfo); if(totalDeals <= 0) { Print(""); Print(__FUNCTION__, ": No deals history found for the specified period."); return; //-- Exit the function }
Если данные по сделке найдены, приступаем к отображению деталей. Первый шаг — распечатать сводное сообщение с указанием общего количества найденных сделок и диапазона дат, в течение которых они были исполнены.
Print(""); Print(__FUNCTION__, "-------------------------------------------------------------------------------"); Print( "Found a total of ", totalDeals, " deals executed between (", fromDateTime, ") and (", toDateTime, ")." );
Далее, используем цикл for для перебора всех сделок в массиве dealInfo . Для каждой сделки мы печатаем соответствующие данные, такие как символ сделки, номер тикета, идентификатор позиции, тип входа, цена, уровни стоп-лосса (SL), тейк-профита (TP), своп, комиссия, прибыль и многое другое. Подробная информация о каждой сделке снабжена пояснительными надписями, что позволяет пользователю легко понять историю транзакций.
for(int r = 0; r < totalDeals; r++) { Print("---------------------------------------------------------------------------------------------------"); Print("Deal #", (r + 1)); Print("Symbol: ", dealInfo[r].symbol); Print("Time Executed: ", dealInfo[r].time); Print("Ticket: ", dealInfo[r].ticket); Print("Position ID: ", dealInfo[r].positionId); Print("Order Ticket: ", dealInfo[r].order); Print("Type: ", EnumToString(dealInfo[r].type)); Print("Entry: ", EnumToString(dealInfo[r].entry)); Print("Reason: ", EnumToString(dealInfo[r].reason)); Print("Volume: ", dealInfo[r].volume); Print("Price: ", dealInfo[r].price); Print("SL Price: ", dealInfo[r].slPrice); Print("TP Price: ", dealInfo[r].tpPrice); Print("Swap: ", dealInfo[r].swap, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Commission: ", dealInfo[r].commission, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Profit: ", dealInfo[r].profit, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Comment: ", dealInfo[r].comment); Print("Magic: ", dealInfo[r].magic); Print(""); }
Ниже приведен полный код функции PrintDealsHistory().
void PrintDealsHistory(datetime fromDateTime, datetime toDateTime) export { //- Get and save the deals history for the specified period GetHistoryData(fromDateTime, toDateTime, GET_DEALS_HISTORY_DATA); int totalDeals = ArraySize(dealInfo); if(totalDeals <= 0) { Print(""); Print(__FUNCTION__, ": No deals history found for the specified period."); return; //-- Exit the function } Print(""); Print(__FUNCTION__, "-------------------------------------------------------------------------------"); Print( "Found a total of ", totalDeals, " deals executed between (", fromDateTime, ") and (", toDateTime, ")." ); for(int r = 0; r < totalDeals; r++) { Print("---------------------------------------------------------------------------------------------------"); Print("Deal #", (r + 1)); Print("Symbol: ", dealInfo[r].symbol); Print("Time Executed: ", dealInfo[r].time); Print("Ticket: ", dealInfo[r].ticket); Print("Position ID: ", dealInfo[r].positionId); Print("Order Ticket: ", dealInfo[r].order); Print("Type: ", EnumToString(dealInfo[r].type)); Print("Entry: ", EnumToString(dealInfo[r].entry)); Print("Reason: ", EnumToString(dealInfo[r].reason)); Print("Volume: ", dealInfo[r].volume); Print("Price: ", dealInfo[r].price); Print("SL Price: ", dealInfo[r].slPrice); Print("TP Price: ", dealInfo[r].tpPrice); Print("Swap: ", dealInfo[r].swap, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Commission: ", dealInfo[r].commission, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Profit: ", dealInfo[r].profit, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Comment: ", dealInfo[r].comment); Print("Magic: ", dealInfo[r].magic); Print(""); } }
Функция сохранения данных ордеров
Функция SaveOrdersData() будет отвечать за извлечение и сохранение исторических данных об ордерах, доступных в кэше истории торговли. функция обрабатывает заказы один за другим, извлекает их ключевые свойства с помощью функций MQL5 HistoryOrderGet... и сохраняет их в динамическом массиве orderInfo. Этот массив затем будет использоваться другими частями библиотеки для анализа и обработки данных по мере необходимости. Эта функция не возвращает никаких данных, не будет определена как экспортируемая, поскольку она используется внутри библиотеки, будет корректно обрабатывать ошибки и регистрировать любые проблемы для отладки.
Начнем с определения сигнатуры функции.
void SaveOrdersData() { //-- Our function's code will go here }
Далее мы определяем, сколько исторических ордеров доступно. Это достигается с помощью функции HistoryOrdersTotal(), которая возвращает общее количество исторических ордеров в кэше. Результат сохраняется в переменной totalOrdersHistory. Кроме того, мы объявляем беззнаковую long-переменную orderTicket для хранения тикета каждого ордера по мере его обработки.
int totalOrdersHistory = HistoryOrdersTotal(); ulong orderTicket;
Если нет исторических ордеров (totalOrdersHistory <= 0), функция регистрирует сообщение, указывающее на это, и завершает работу раньше времени, чтобы избежать ненужной обработки.
if(totalOrdersHistory > 0) { //-- Code to process orders goes here } else { Print(__FUNCTION__, ": No order history available to be processed, totalOrdersHistory = ", totalOrdersHistory); return; }
При наличии исторических ордеров, мы готовим массив orderInfo для хранения полученных данных. Это делается путем изменения размера массива с помощью функции ArrayResize() для сопоставления общего количества исторических ордеров.
ArrayResize(orderInfo, totalOrdersHistory); Мы циклически просматриваем ордера в обратном порядке (начиная с самого последнего) с использованием цикла for. Для каждого ордера. Начинаем с получения тикета ордера с помощью функции HistoryOrderGetTicket(). Если извлечение тикета прошло успешно, извлекаем различные свойства ордера, используя функции HistoryOrderGet..., и сохраняем их в соответствующих полях массива orderInfo. Если извлечение тикета не удлось, функция регистрирует сообщение об ошибке вместе с кодом ошибки для отладки.
for(int x = totalOrdersHistory - 1; x >= 0; x--) { ResetLastError(); orderTicket = HistoryOrderGetTicket(x); if(orderTicket > 0) { //- Order ticket selected ok, we can now save the order properties orderInfo[x].ticket = orderTicket; orderInfo[x].timeSetup = (datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_SETUP); orderInfo[x].timeDone = (datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_DONE); orderInfo[x].expirationTime = (datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_EXPIRATION); orderInfo[x].typeTime = (ENUM_ORDER_TYPE_TIME)HistoryOrderGetInteger(orderTicket, ORDER_TYPE_TIME); orderInfo[x].magic = HistoryOrderGetInteger(orderTicket, ORDER_MAGIC); orderInfo[x].reason = (ENUM_ORDER_REASON)HistoryOrderGetInteger(orderTicket, ORDER_REASON); orderInfo[x].type = (ENUM_ORDER_TYPE)HistoryOrderGetInteger(orderTicket, ORDER_TYPE); orderInfo[x].state = (ENUM_ORDER_STATE)HistoryOrderGetInteger(orderTicket, ORDER_STATE); orderInfo[x].typeFilling = (ENUM_ORDER_TYPE_FILLING)HistoryOrderGetInteger(orderTicket, ORDER_TYPE_FILLING); orderInfo[x].positionId = HistoryOrderGetInteger(orderTicket, ORDER_POSITION_ID); orderInfo[x].positionById = HistoryOrderGetInteger(orderTicket, ORDER_POSITION_BY_ID); orderInfo[x].symbol = HistoryOrderGetString(orderTicket, ORDER_SYMBOL); orderInfo[x].comment = HistoryOrderGetString(orderTicket, ORDER_COMMENT); orderInfo[x].volumeInitial = HistoryOrderGetDouble(orderTicket, ORDER_VOLUME_INITIAL); orderInfo[x].priceOpen = HistoryOrderGetDouble(orderTicket, ORDER_PRICE_OPEN); orderInfo[x].priceStopLimit = HistoryOrderGetDouble(orderTicket, ORDER_PRICE_STOPLIMIT); orderInfo[x].tpPrice = HistoryOrderGetDouble(orderTicket, ORDER_TP); orderInfo[x].slPrice = HistoryOrderGetDouble(orderTicket, ORDER_SL); } else { Print( __FUNCTION__, " HistoryOrderGetTicket(", x, ") failed. (orderTicket = ", orderTicket, ") *** Error Code: ", GetLastError() ); } }
После обработки всех ордеров функция корректно завершает работу. Вот полная реализация функции SaveOrdersData() в коде.
void SaveOrdersData() { //- Get the number of loaded history orders int totalOrdersHistory = HistoryOrdersTotal(); ulong orderTicket; //- //- Check if we have any orders in the history to be worked on if(totalOrdersHistory > 0) { //- Resize the dynamic array that stores the history orders ArrayResize(orderInfo, totalOrdersHistory); //- Let us loop through the order history and save them one by one for(int x = totalOrdersHistory - 1; x >= 0; x--) { ResetLastError(); orderTicket = HistoryOrderGetTicket(x); if(orderTicket > 0) { //- Order ticket selected ok, we can now save the order properties orderInfo[x].ticket = orderTicket; orderInfo[x].timeSetup = (datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_SETUP); orderInfo[x].timeDone = (datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_DONE); orderInfo[x].expirationTime = (datetime)HistoryOrderGetInteger(orderTicket, ORDER_TIME_EXPIRATION); orderInfo[x].typeTime = (ENUM_ORDER_TYPE_TIME)HistoryOrderGetInteger(orderTicket, ORDER_TYPE_TIME); orderInfo[x].magic = HistoryOrderGetInteger(orderTicket, ORDER_MAGIC); orderInfo[x].reason = (ENUM_ORDER_REASON)HistoryOrderGetInteger(orderTicket, ORDER_REASON); orderInfo[x].type = (ENUM_ORDER_TYPE)HistoryOrderGetInteger(orderTicket, ORDER_TYPE); orderInfo[x].state = (ENUM_ORDER_STATE)HistoryOrderGetInteger(orderTicket, ORDER_STATE); orderInfo[x].typeFilling = (ENUM_ORDER_TYPE_FILLING)HistoryOrderGetInteger(orderTicket, ORDER_TYPE_FILLING); orderInfo[x].positionId = HistoryOrderGetInteger(orderTicket, ORDER_POSITION_ID); orderInfo[x].positionById = HistoryOrderGetInteger(orderTicket, ORDER_POSITION_BY_ID); orderInfo[x].symbol = HistoryOrderGetString(orderTicket, ORDER_SYMBOL); orderInfo[x].comment = HistoryOrderGetString(orderTicket, ORDER_COMMENT); orderInfo[x].volumeInitial = HistoryOrderGetDouble(orderTicket, ORDER_VOLUME_INITIAL); orderInfo[x].priceOpen = HistoryOrderGetDouble(orderTicket, ORDER_PRICE_OPEN); orderInfo[x].priceStopLimit = HistoryOrderGetDouble(orderTicket, ORDER_PRICE_STOPLIMIT); orderInfo[x].tpPrice = HistoryOrderGetDouble(orderTicket, ORDER_TP); orderInfo[x].slPrice = HistoryOrderGetDouble(orderTicket, ORDER_SL); } else { Print( __FUNCTION__, " HistoryOrderGetTicket(", x, ") failed. (orderTicket = ", orderTicket, ") *** Error Code: ", GetLastError() ); } } } else { Print(__FUNCTION__, ": No order history available to be processed, totalOrdersHistory = ", totalOrdersHistory); } }
Функция отображения истории ордеров
Функция PrintOrdersHistory() обеспечивает важную возможность отображения подробностей истории ордеров за указанный период. Она запрашивает ранее сохраненные данные из массива orderInfo и выводит все соответствующие данные ордеров. Эта функция определена как экспортируемая, поскольку она должна быть доступна внешним модулям или приложениям MQL5, использующим эту библиотеку. Она следует аналогичному подходу функции PrintDealsHistory(). Вот полная реализация функции PrintOrdersHistory() с пояснительными комментариями, которые помогут вам лучше понять, как работает каждая часть кода.
void PrintOrdersHistory(datetime fromDateTime, datetime toDateTime) export { //- Get and save the orders history for the specified period GetHistoryData(fromDateTime, toDateTime, GET_ORDERS_HISTORY_DATA); int totalOrders = ArraySize(orderInfo); if(totalOrders <= 0) { Print(""); Print(__FUNCTION__, ": No orders history found for the specified period."); return; //-- Exit the function } Print(""); Print(__FUNCTION__, "-------------------------------------------------------------------------------"); Print( "Found a total of ", totalOrders, " orders filled or cancelled between (", fromDateTime, ") and (", toDateTime, ")." ); for(int r = 0; r < totalOrders; r++) { Print("---------------------------------------------------------------------------------------------------"); Print("Order #", (r + 1)); Print("Symbol: ", orderInfo[r].symbol); Print("Time Setup: ", orderInfo[r].timeSetup); Print("Type: ", EnumToString(orderInfo[r].type)); Print("Ticket: ", orderInfo[r].ticket); Print("Position ID: ", orderInfo[r].positionId); Print("State: ", EnumToString(orderInfo[r].state)); Print("Type Filling: ", EnumToString(orderInfo[r].typeFilling)); Print("Type Time: ", EnumToString(orderInfo[r].typeTime)); Print("Reason: ", EnumToString(orderInfo[r].reason)); Print("Volume Initial: ", orderInfo[r].volumeInitial); Print("Price Open: ", orderInfo[r].priceOpen); Print("Price Stop Limit: ", orderInfo[r].priceStopLimit); Print("SL Price: ", orderInfo[r].slPrice); Print("TP Price: ", orderInfo[r].tpPrice); Print("Time Done: ", orderInfo[r].timeDone); Print("Expiration Time: ", orderInfo[r].expirationTime); Print("Comment: ", orderInfo[r].comment); Print("Magic: ", orderInfo[r].magic); Print(""); } }
Функция сохранения данных позиций
Функция SavePositionsData() организует историю сделок и ордеров для реконструкции жизненного цикла каждой позиции, играя центральную роль в создании истории позиций путем синтеза информации из доступных данных. В документации MQL5 вы заметите отсутствие стандартных функций (таких как HistoryPositionSelect() или HistoryPositionsTotal()) для прямого доступа к историческим данным о местоположении. Поэтому нам нужно создать пользовательскую функцию, которая объединяет данные об ордерах и сделках, используя Position ID как связующий ключ для связи с исходными ордерами.
Мы начнем с рассмотрения сделок для поиска всех сделок выхода, которые указывают на то, что позиция закрыта. Далее мы проследуем обратно до сделки открытия для сбора информации об открытии позиции. Наконец, мы будем использовать историю ордеров для обогащения информации об истории позиций дополнительным контекстом, например, типом исходного ордера или тем, была ли позиция инициирована отложенным ордером. Этот пошаговый процесс обеспечит точную реконструкцию жизненного цикла каждой позиции — от открытия до закрытия — и предоставит простой документальный след.
Начнем с определения сигнатуры функции. Поскольку эта функция будет использоваться только внутренними модулями ядра EX5-библиотеки, ее нельзя будет экспортировать.
void SavePositionsData() { //-- Our function's code will go here }
Далее мы подсчитаем общее количество сделок в массиве dealInfo, содержащем все данные о сделках. После этого мы изменим размер массива positionInfo, который мы будем использовать для сохранения всех данных истории позиций. Подготовим его для размещения ожидаемого количества позиций.
int totalDealInfo = ArraySize(dealInfo); ArrayResize(positionInfo, totalDealInfo); int totalPositionsFound = 0, posIndex = 0;
Если в массиве dealInfo нет доступных сделок (то есть totalDealInfo == 0), мы выходим из функции раньше времени, поскольку нет данных для обработки.
if(totalDealInfo == 0) { return; }
Далее мы проходим по сделкам в обратном порядке (начиная с самой последней), чтобы мы могли сопоставить сделки выхода с соответствующими им сделками входа. Мы проверяем, является ли текущая сделка сделкой выхода, оценивая ее свойство entry. (dealInfo[x].entry == DEAL_ENTRY_OUT). Крайне важно начинать с поиска сделок выхода, поскольку это подтверждает, что позиция закрыта и больше не активна. Нам нужно регистрировать только закрытые позиции.
for(int x = totalDealInfo - 1; x >= 0; x--) { if(dealInfo[x].entry == DEAL_ENTRY_OUT) { // Process exit deal } }
Если найдена сделка выхода, мы ищем соответствующую сделку входа, сопоставляя POSITION_ID. Если найдена сделка на вход, начинаем сохранять соответствующую информацию в массив positionInfo.
for(int k = ArraySize(dealInfo) - 1; k >= 0; k--) { if(dealInfo[k].positionId == positionId) { if(dealInfo[k].entry == DEAL_ENTRY_IN) { exitDealFound = true; totalPositionsFound++; posIndex = totalPositionsFound - 1; // Save the entry deal data positionInfo[posIndex].openingDealTicket = dealInfo[k].ticket; positionInfo[posIndex].openTime = dealInfo[k].time; positionInfo[posIndex].openPrice = dealInfo[k].price; positionInfo[posIndex].volume = dealInfo[k].volume; positionInfo[posIndex].magic = dealInfo[k].magic; positionInfo[posIndex].comment = dealInfo[k].comment; } } }
После того как сделка выхода сопоставлена со сделкой входа, мы приступаем к сохранению свойств сделки выхода, таких как цена закрытия, время закрытия, прибыль, своп и комиссия. Мы также рассчитываем длительность сделки и чистую прибыль, учитывая своп и комиссию.
if(exitDealFound) { if(dealInfo[x].type == DEAL_TYPE_BUY) { positionInfo[posIndex].type = POSITION_TYPE_SELL; } else { positionInfo[posIndex].type = POSITION_TYPE_BUY; } positionInfo[posIndex].positionId = dealInfo[x].positionId; positionInfo[posIndex].symbol = dealInfo[x].symbol; positionInfo[posIndex].profit = dealInfo[x].profit; positionInfo[posIndex].closingDealTicket = dealInfo[x].ticket; positionInfo[posIndex].closePrice = dealInfo[x].price; positionInfo[posIndex].closeTime = dealInfo[x].time; positionInfo[posIndex].swap = dealInfo[x].swap; positionInfo[posIndex].commission = dealInfo[x].commission; positionInfo[posIndex].duration = MathAbs((long)positionInfo[posIndex].closeTime - (long)positionInfo[posIndex].openTime); positionInfo[posIndex].netProfit = positionInfo[posIndex].profit + positionInfo[posIndex].swap - positionInfo[posIndex].commission; }
Для каждой позиции мы рассчитываем значения пипсов для уровней стоп-лосса (SL) и тейк-профита (TP) в зависимости от направления сделки - покупка или продажа. Мы используем значение точки символа для определения количества пипсов.
if(positionInfo[posIndex].type == POSITION_TYPE_BUY) { // Calculate TP and SL pip values for buy position if(positionInfo[posIndex].tpPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].tpPips = int((positionInfo[posIndex].tpPrice - positionInfo[posIndex].openPrice) / symbolPoint); } if(positionInfo[posIndex].slPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].slPips = int((positionInfo[posIndex].openPrice - positionInfo[posIndex].slPrice) / symbolPoint); } // Calculate pip profit for buy position double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].pipProfit = int((positionInfo[posIndex].closePrice - positionInfo[posIndex].openPrice) / symbolPoint); } else { // Calculate TP and SL pip values for sell position if(positionInfo[posIndex].tpPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].tpPips = int((positionInfo[posIndex].openPrice - positionInfo[posIndex].tpPrice) / symbolPoint); } if(positionInfo[posIndex].slPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].slPips = int((positionInfo[posIndex].slPrice - positionInfo[posIndex].openPrice) / symbolPoint); } // Calculate pip profit for sell position double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].pipProfit = int((positionInfo[posIndex].openPrice - positionInfo[posIndex].closePrice) / symbolPoint); }
Наконец, мы просматриваем массив orderInfo для поиска ордера, инициировавшего позицию. Сопоставляем POSITION_ID и проверяем, что ордер в состоянии ORDER_STATE_FILLED. Далее мы сохраняем тикет и тип открывающего ордера, что поможет определить, была ли позиция инициирована отложенным или прямым рыночным ордером.
for(int k = 0; k < ArraySize(orderInfo); k++) { if( orderInfo[k].positionId == positionInfo[posIndex].positionId && orderInfo[k].state == ORDER_STATE_FILLED ) { positionInfo[posIndex].openingOrderTicket = orderInfo[k].ticket; positionInfo[posIndex].ticket = positionInfo[posIndex].openingOrderTicket; //- Determine if the position was initiated by a pending order or direct market entry switch(orderInfo[k].type) { case ORDER_TYPE_BUY_LIMIT: case ORDER_TYPE_BUY_STOP: case ORDER_TYPE_SELL_LIMIT: case ORDER_TYPE_SELL_STOP: case ORDER_TYPE_BUY_STOP_LIMIT: case ORDER_TYPE_SELL_STOP_LIMIT: positionInfo[posIndex].initiatedByPendingOrder = true; positionInfo[posIndex].initiatingOrderType = orderInfo[k].type; break; default: positionInfo[posIndex].initiatedByPendingOrder = false; positionInfo[posIndex].initiatingOrderType = orderInfo[k].type; break; } break; //- Exit the orderInfo loop once the required data is found } }
Наконец, чтобы очистить массив positionInfo, мы изменяем их размер, чтобы удалить все пустые или неиспользуемые элементы после обработки всех позиций.
ArrayResize(positionInfo, totalPositionsFound); Ниже представлена полная реализация функции SavePositionsData() со всеми сегментами кода.
void SavePositionsData() { //- Since every transaction is recorded as a deal, we will begin by scanning the deals and link them //- to different orders and generate the positions data using the POSITION_ID as the primary and foreign key int totalDealInfo = ArraySize(dealInfo); ArrayResize(positionInfo, totalDealInfo); //- Resize the position array to match the deals array int totalPositionsFound = 0, posIndex = 0; if(totalDealInfo == 0) //- Check if we have any deal history available for processing { return; //- No deal data to process found, we can't go on. exit the function } //- Let us loop through the deals array for(int x = totalDealInfo - 1; x >= 0; x--) { //- First we check if it is an exit deal to close a position if(dealInfo[x].entry == DEAL_ENTRY_OUT) { //- We begin by saving the position id ulong positionId = dealInfo[x].positionId; bool exitDealFound = false; //- Now we check if we have an exit deal from this position and save it's properties for(int k = ArraySize(dealInfo) - 1; k >= 0; k--) { if(dealInfo[k].positionId == positionId) { if(dealInfo[k].entry == DEAL_ENTRY_IN) { exitDealFound = true; totalPositionsFound++; posIndex = totalPositionsFound - 1; positionInfo[posIndex].openingDealTicket = dealInfo[k].ticket; positionInfo[posIndex].openTime = dealInfo[k].time; positionInfo[posIndex].openPrice = dealInfo[k].price; positionInfo[posIndex].volume = dealInfo[k].volume; positionInfo[posIndex].magic = dealInfo[k].magic; positionInfo[posIndex].comment = dealInfo[k].comment; } } } if(exitDealFound) //- Continue saving the exit deal data { //- Save the position type if(dealInfo[x].type == DEAL_TYPE_BUY) { //- If the exit deal is a buy, then the position was a sell trade positionInfo[posIndex].type = POSITION_TYPE_SELL; } else { //- If the exit deal is a sell, then the position was a buy trade positionInfo[posIndex].type = POSITION_TYPE_BUY; } positionInfo[posIndex].positionId = dealInfo[x].positionId; positionInfo[posIndex].symbol = dealInfo[x].symbol; positionInfo[posIndex].profit = dealInfo[x].profit; positionInfo[posIndex].closingDealTicket = dealInfo[x].ticket; positionInfo[posIndex].closePrice = dealInfo[x].price; positionInfo[posIndex].closeTime = dealInfo[x].time; positionInfo[posIndex].swap = dealInfo[x].swap; positionInfo[posIndex].commission = dealInfo[x].commission; positionInfo[posIndex].tpPrice = dealInfo[x].tpPrice; positionInfo[posIndex].tpPips = 0; positionInfo[posIndex].slPrice = dealInfo[x].slPrice; positionInfo[posIndex].slPips = 0; //- Calculate the trade duration in seconds positionInfo[posIndex].duration = MathAbs((long)positionInfo[posIndex].closeTime - (long)positionInfo[posIndex].openTime); //- Calculate the net profit after swap and commission positionInfo[posIndex].netProfit = positionInfo[posIndex].profit + positionInfo[posIndex].swap - positionInfo[posIndex].commission; //- Get pip values for the position if(positionInfo[posIndex].type == POSITION_TYPE_BUY) //- Buy position { //- Get sl and tp pip values if(positionInfo[posIndex].tpPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].tpPips = int((positionInfo[posIndex].tpPrice - positionInfo[posIndex].openPrice) / symbolPoint); } if(positionInfo[posIndex].slPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].slPips = int((positionInfo[posIndex].openPrice - positionInfo[posIndex].slPrice) / symbolPoint); } //- Get the buy profit in pip value double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].pipProfit = int((positionInfo[posIndex].closePrice - positionInfo[posIndex].openPrice) / symbolPoint); } else //- Sell position { //- Get sl and tp pip values if(positionInfo[posIndex].tpPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].tpPips = int((positionInfo[posIndex].openPrice - positionInfo[posIndex].tpPrice) / symbolPoint); } if(positionInfo[posIndex].slPrice > 0) { double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].slPips = int((positionInfo[posIndex].slPrice - positionInfo[posIndex].openPrice) / symbolPoint); } //- Get the sell profit in pip value double symbolPoint = SymbolInfoDouble(positionInfo[posIndex].symbol, SYMBOL_POINT); positionInfo[posIndex].pipProfit = int((positionInfo[posIndex].openPrice - positionInfo[posIndex].closePrice) / symbolPoint); } //- Now we scan and get the opening order ticket in the orderInfo array for(int k = 0; k < ArraySize(orderInfo); k++) //- Search from the oldest to newest order { if( orderInfo[k].positionId == positionInfo[posIndex].positionId && orderInfo[k].state == ORDER_STATE_FILLED ) { //- Save the order ticket that intiated the position positionInfo[posIndex].openingOrderTicket = orderInfo[k].ticket; positionInfo[posIndex].ticket = positionInfo[posIndex].openingOrderTicket; //- Determine if the position was initiated by a pending order or direct market entry switch(orderInfo[k].type) { //- Pending order entry case ORDER_TYPE_BUY_LIMIT: case ORDER_TYPE_BUY_STOP: case ORDER_TYPE_SELL_LIMIT: case ORDER_TYPE_SELL_STOP: case ORDER_TYPE_BUY_STOP_LIMIT: case ORDER_TYPE_SELL_STOP_LIMIT: positionInfo[posIndex].initiatedByPendingOrder = true; positionInfo[posIndex].initiatingOrderType = orderInfo[k].type; break; //- Direct market entry default: positionInfo[posIndex].initiatedByPendingOrder = false; positionInfo[posIndex].initiatingOrderType = orderInfo[k].type; break; } break; //--- We have everything we need, exit the orderInfo loop } } } } else //--- Position id not found { continue;//- skip to the next iteration } } //- Resize the positionInfo array and delete all the indexes that have zero values ArrayResize(positionInfo, totalPositionsFound); }
Функция отображения истории позиций
Функция PrintPositionsHistory() предназначена для отображения подробной истории закрытых позиций за указанный период времени. Он получает доступ к ранее сохраненным данным из массива positionInfo и выводит соответствующие данные для каждой позиции. Эту функцию можно экспортировать, что делает ее доступной для внешних модулей или приложений MQL5, использующих эту библиотеку. Его реализация будет иметь ту же структуру, что и другие разработанные нами функции отображения. Вот полная реализация с подробными комментариями для ясности.
void PrintPositionsHistory(datetime fromDateTime, datetime toDateTime) export { //- Get and save the deals, orders, positions history for the specified period GetHistoryData(fromDateTime, toDateTime, GET_POSITIONS_HISTORY_DATA); int totalPositionsClosed = ArraySize(positionInfo); if(totalPositionsClosed <= 0) { Print(""); Print(__FUNCTION__, ": No position history found for the specified period."); return; //- Exit the function } Print(""); Print(__FUNCTION__, "-------------------------------------------------------------------------------"); Print( "Found a total of ", totalPositionsClosed, " positions closed between (", fromDateTime, ") and (", toDateTime, ")." ); for(int r = 0; r < totalPositionsClosed; r++) { Print("---------------------------------------------------------------------------------------------------"); Print("Position #", (r + 1)); Print("Symbol: ", positionInfo[r].symbol); Print("Time Open: ", positionInfo[r].openTime); Print("Ticket: ", positionInfo[r].ticket); Print("Type: ", EnumToString(positionInfo[r].type)); Print("Volume: ", positionInfo[r].volume); Print("0pen Price: ", positionInfo[r].openPrice); Print("SL Price: ", positionInfo[r].slPrice, " (slPips: ", positionInfo[r].slPips, ")"); Print("TP Price: ", positionInfo[r].tpPrice, " (tpPips: ", positionInfo[r].tpPips, ")"); Print("Close Price: ", positionInfo[r].closePrice); Print("Close Time: ", positionInfo[r].closeTime); Print("Trade Duration: ", positionInfo[r].duration); Print("Swap: ", positionInfo[r].swap, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Commission: ", positionInfo[r].commission, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Profit: ", positionInfo[r].profit, " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("Net profit: ", DoubleToString(positionInfo[r].netProfit, 2), " ", AccountInfoString(ACCOUNT_CURRENCY)); Print("pipProfit: ", positionInfo[r].pipProfit); Print("Initiating Order Type: ", EnumToString(positionInfo[r].initiatingOrderType)); Print("Initiated By Pending Order: ", positionInfo[r].initiatedByPendingOrder); Print("Comment: ", positionInfo[r].comment); Print("Magic: ", positionInfo[r].magic); Print(""); } }
Функция сохранения данных отложенных ордеров
Функция SavePendingOrdersData() обрабатывает данные из истории ордеров для формирования и сохранения истории отложенных ордеров. Функция сортирует отложенные ордера из истории ордеров, сохраняет ключевые данные и вычисляет конкретные значения, такие как количество пипсов для тейа-профита (TP) и стоп-лосса (SL) levels. Она играет важную роль в отслеживании жизненного цикла отложенных ордеров, помогая формировать точную историю ордеров и дополняя систему данными о том, как был структурирован и исполнен каждый отложенный ордер.
В настоящее время в MQL5 нет стандартных функций, таких как HistoryPendingOrderSelect() или HistoryPendingOrdersTotal() для прямого доступа к историческим данным отложенных ордеров. В результате нам необходимо создать пользовательскую функцию для сканирования истории ордеров и построения источника данных, содержащего все исполненные или отмененные отложенные ордера за заданный исторический период времени.
Начнем с определения сигнатуры функции. Поскольку эта функция будет использоваться только внутренними модулями ядра EX5-библиотеки, ее нельзя будет экспортировать.
void SavePendingOrdersData() { //-- Function's code will go here }
Далее мы подсчитываем общее количество ордеров массива orderInfo, в котором хранится информация обо всех ордерах. Изменим размер массива pendingOrderInfo для первоначального размещения общего количества ордеров, обеспечивая достаточно места для хранения отсортированных отложенных ордеров.
int totalOrderInfo = ArraySize(orderInfo); ArrayResize(pendingOrderInfo, totalOrderInfo); int totalPendingOrdersFound = 0, pendingIndex = 0;
Если нет ордеров для обработки (то есть totalOrderInfo == 0), мы немедленно выходим из функции, поскольку нет данных об отложенных ордерах для обработки.
if(totalOrderInfo == 0) { return; }
Теперь мы просматриваем ордера в обратном порядке, чтобы гарантировать, что мы обрабатываем сначала самые последние ордера. Внутри цикла мы проверяем, является ли текущий ордер отложенным, оценивая его тип. История сохраненных заказов будет включать отложенные ордера (такие как лимитные ордера на покупку, стоп-ордера на продажу, и т.д.), которые были либо исполнены и превращены в позиции или отменены.
for(int x = totalOrderInfo - 1; x >= 0; x--) { if( orderInfo[x].type == ORDER_TYPE_BUY_LIMIT || orderInfo[x].type == ORDER_TYPE_BUY_STOP || orderInfo[x].type == ORDER_TYPE_SELL_LIMIT || orderInfo[x].type == ORDER_TYPE_SELL_STOP || orderInfo[x].type == ORDER_TYPE_BUY_STOP_LIMIT || orderInfo[x].type == ORDER_TYPE_SELL_STOP_LIMIT ) { totalPendingOrdersFound++; pendingIndex = totalPendingOrdersFound - 1; //-- Save the pending order properties into the pendingOrderInfo array }
Если ордер отложенный, мы сохраняем его свойства (например, тип, состояние, идентификатор позиции, тикет, символ, время и т.д.) в массиве pendingOrderInfo.
pendingOrderInfo[pendingIndex].type = orderInfo[x].type; pendingOrderInfo[pendingIndex].state = orderInfo[x].state; pendingOrderInfo[pendingIndex].positionId = orderInfo[x].positionId; pendingOrderInfo[pendingIndex].ticket = orderInfo[x].ticket; pendingOrderInfo[pendingIndex].symbol = orderInfo[x].symbol; pendingOrderInfo[pendingIndex].timeSetup = orderInfo[x].timeSetup; pendingOrderInfo[pendingIndex].expirationTime = orderInfo[x].expirationTime; pendingOrderInfo[pendingIndex].timeDone = orderInfo[x].timeDone; pendingOrderInfo[pendingIndex].typeTime = orderInfo[x].typeTime; pendingOrderInfo[pendingIndex].priceOpen = orderInfo[x].priceOpen; pendingOrderInfo[pendingIndex].tpPrice = orderInfo[x].tpPrice; pendingOrderInfo[pendingIndex].slPrice = orderInfo[x].slPrice;
Затем мы подсчитаем количество пипсов как для уровней тейк-профита (TP), так и стоп-лосса (SL), еслт они указаны. Для этого мы используем значение точки символа для определения количества пипсов.
if(pendingOrderInfo[pendingIndex].tpPrice > 0) { double symbolPoint = SymbolInfoDouble(pendingOrderInfo[pendingIndex].symbol, SYMBOL_POINT); pendingOrderInfo[pendingIndex].tpPips = (int)MathAbs((pendingOrderInfo[pendingIndex].tpPrice - pendingOrderInfo[pendingIndex].priceOpen) / symbolPoint); } if(pendingOrderInfo[pendingIndex].slPrice > 0) { double symbolPoint = SymbolInfoDouble(pendingOrderInfo[pendingIndex].symbol, SYMBOL_POINT); pendingOrderInfo[pendingIndex].slPips = (int)MathAbs((pendingOrderInfo[pendingIndex].slPrice - pendingOrderInfo[pendingIndex].priceOpen) / symbolPoint); }
Мы также сохраняем дополнительные свойства, такие как магическое число ордера, причина, тип исполнения, комментарий, начальный объем и цена stop limit.
pendingOrderInfo[pendingIndex].magic = orderInfo[x].magic; pendingOrderInfo[pendingIndex].reason = orderInfo[x].reason; pendingOrderInfo[pendingIndex].typeFilling = orderInfo[x].typeFilling; pendingOrderInfo[pendingIndex].comment = orderInfo[x].comment; pendingOrderInfo[pendingIndex].volumeInitial = orderInfo[x].volumeInitial; pendingOrderInfo[pendingIndex].priceStopLimit = orderInfo[x].priceStopLimit;
После обработки всех ордеров мы изменяем размер pendingOrderInfo для удаления всех пустых или неиспользуемых элементов, гарантируя, что массив будет содержать только актуальные данные отложенного ордера.
ArrayResize(pendingOrderInfo, totalPendingOrdersFound); Ниже представлена полная реализация функции SavePendingOrdersData() со всеми сегментами кода.
void SavePendingOrdersData() { //- Let us begin by scanning the orders and link them to different deals int totalOrderInfo = ArraySize(orderInfo); ArrayResize(pendingOrderInfo, totalOrderInfo); int totalPendingOrdersFound = 0, pendingIndex = 0; if(totalOrderInfo == 0) { return; //- No order data to process found, we can't go on. exit the function } for(int x = totalOrderInfo - 1; x >= 0; x--) { //- Check if it is a pending order and save its properties if( orderInfo[x].type == ORDER_TYPE_BUY_LIMIT || orderInfo[x].type == ORDER_TYPE_BUY_STOP || orderInfo[x].type == ORDER_TYPE_SELL_LIMIT || orderInfo[x].type == ORDER_TYPE_SELL_STOP || orderInfo[x].type == ORDER_TYPE_BUY_STOP_LIMIT || orderInfo[x].type == ORDER_TYPE_SELL_STOP_LIMIT ) { totalPendingOrdersFound++; pendingIndex = totalPendingOrdersFound - 1; pendingOrderInfo[pendingIndex].type = orderInfo[x].type; pendingOrderInfo[pendingIndex].state = orderInfo[x].state; pendingOrderInfo[pendingIndex].positionId = orderInfo[x].positionId; pendingOrderInfo[pendingIndex].ticket = orderInfo[x].ticket; pendingOrderInfo[pendingIndex].symbol = orderInfo[x].symbol; pendingOrderInfo[pendingIndex].timeSetup = orderInfo[x].timeSetup; pendingOrderInfo[pendingIndex].expirationTime = orderInfo[x].expirationTime; pendingOrderInfo[pendingIndex].timeDone = orderInfo[x].timeDone; pendingOrderInfo[pendingIndex].typeTime = orderInfo[x].typeTime; pendingOrderInfo[pendingIndex].priceOpen = orderInfo[x].priceOpen; pendingOrderInfo[pendingIndex].tpPrice = orderInfo[x].tpPrice; pendingOrderInfo[pendingIndex].slPrice = orderInfo[x].slPrice; if(pendingOrderInfo[pendingIndex].tpPrice > 0) { double symbolPoint = SymbolInfoDouble(pendingOrderInfo[pendingIndex].symbol, SYMBOL_POINT); pendingOrderInfo[pendingIndex].tpPips = (int)MathAbs((pendingOrderInfo[pendingIndex].tpPrice - pendingOrderInfo[pendingIndex].priceOpen) / symbolPoint); } if(pendingOrderInfo[pendingIndex].slPrice > 0) { double symbolPoint = SymbolInfoDouble(pendingOrderInfo[pendingIndex].symbol, SYMBOL_POINT); pendingOrderInfo[pendingIndex].slPips = (int)MathAbs((pendingOrderInfo[pendingIndex].slPrice - pendingOrderInfo[pendingIndex].priceOpen) / symbolPoint); } pendingOrderInfo[pendingIndex].magic = orderInfo[x].magic; pendingOrderInfo[pendingIndex].reason = orderInfo[x].reason; pendingOrderInfo[pendingIndex].typeFilling = orderInfo[x].typeFilling; pendingOrderInfo[pendingIndex].comment = orderInfo[x].comment; pendingOrderInfo[pendingIndex].volumeInitial = orderInfo[x].volumeInitial; pendingOrderInfo[pendingIndex].priceStopLimit = orderInfo[x].priceStopLimit; } } //--Resize the pendingOrderInfo array and delete all the indexes that have zero values ArrayResize(pendingOrderInfo, totalPendingOrdersFound); }
Функция отображения истории отложенных ордеров
Функция PrintPendingOrdersHistory() предназначена для отображения подробной истории исполненных или отмененных отложенных ордеров за указанный период времени. Она получает доступ к ранее сохраненным данным из массива pendingOrderInfo и выводит соответствующие данные для каждого отложенного ордера. Эту функцию можно экспортировать, что делает ее доступной для внешних модулей или приложений MQL5, использующих эту EX5-библиотеку. Его реализация будет иметь ту же структуру, что и другие разработанные нами функции отображения. Вот полная реализация с подробными комментариями для ясности.
void PrintPendingOrdersHistory(datetime fromDateTime, datetime toDateTime) export { //- Get and save the pending orders history for the specified period GetHistoryData(fromDateTime, toDateTime, GET_PENDING_ORDERS_HISTORY_DATA); int totalPendingOrders = ArraySize(pendingOrderInfo); if(totalPendingOrders <= 0) { Print(""); Print(__FUNCTION__, ": No pending orders history found for the specified period."); return; //- Exit the function } Print(""); Print(__FUNCTION__, "-------------------------------------------------------------------------------"); Print( "Found a total of ", totalPendingOrders, " pending orders filled or cancelled between (", fromDateTime, ") and (", toDateTime, ")." ); for(int r = 0; r < totalPendingOrders; r++) { Print("---------------------------------------------------------------------------------------------------"); Print("Pending Order #", (r + 1)); Print("Symbol: ", pendingOrderInfo[r].symbol); Print("Time Setup: ", pendingOrderInfo[r].timeSetup); Print("Type: ", EnumToString(pendingOrderInfo[r].type)); Print("Ticket: ", pendingOrderInfo[r].ticket); Print("State: ", EnumToString(pendingOrderInfo[r].state)); Print("Time Done: ", pendingOrderInfo[r].timeDone); Print("Volume Initial: ", pendingOrderInfo[r].volumeInitial); Print("Price Open: ", pendingOrderInfo[r].priceOpen); Print("SL Price: ", pendingOrderInfo[r].slPrice, " (slPips: ", pendingOrderInfo[r].slPips, ")"); Print("TP Price: ", pendingOrderInfo[r].tpPrice, " (slPips: ", pendingOrderInfo[r].slPips, ")"); Print("Expiration Time: ", pendingOrderInfo[r].expirationTime); Print("Position ID: ", pendingOrderInfo[r].positionId); Print("Price Stop Limit: ", pendingOrderInfo[r].priceStopLimit); Print("Type Filling: ", EnumToString(pendingOrderInfo[r].typeFilling)); Print("Type Time: ", EnumToString(pendingOrderInfo[r].typeTime)); Print("Reason: ", EnumToString(pendingOrderInfo[r].reason)); Print("Comment: ", pendingOrderInfo[r].comment); Print("Magic: ", pendingOrderInfo[r].magic); Print(""); } }
Заключение
Мы рассмотрели, как использовать MQL5 для извлечения исторических данных ордеров и сделок. Мы узнали, как использовать эти данные для создания истории закрытых позиций и отложенных ордеров с документальным следом, который отслеживает жизненный цикл каждой закрытой позиции. Это включает в себя его источник, как он был закрыт, и другие ценные детали, такие как чистая прибыль, прибыль в пипсах, значение пипса для стоп-лосса и тейк-профита, длительность сделки и многое другое.
Мы также разработали основные функции EX5-библиотеки History Manager, позволяющие нам запрашивать, сохранять и классифицировать различные типы исторических данных. Эти основополагающие функции являются частью библиотечного движка, который управляет его внутренней работой. Однако сделать предстоит еще больше. Большинство функций, созданных нами в этой статье, являются подготовительными и закладывают основу для библиотеки, более ориентированной на пользователя.
В следующей статье мы расширим EX5-библиотеку History Manager путем внедрения экспортируемых функций, предназначенных для сортировки и анализа исторических данных на основе общих требований пользователей. Например, вы сможете получить свойства последних закрытых позиций, проанализировать последние исполненные или отмененные отложенные ордера, проверить последнюю закрытую позицию по определенному символу, рассчитать прибыль текущего дня и определить еженедельную прибыль в пунктах, а также воспользоваться другими функциями.
Кроме того, мы включим расширенные модули сортировки и аналитики для создания подробных торговых отчетов, аналогичных тем, которые создаются тестером стратегий MetaTrader 5. Эти отчеты будут анализировать реальные данные истории торговли, предоставляя информацию об эффективности советника или торговой стратегии. Вы также сможете программно сортировать эти данные по таким параметрам, как символы или магические числа.
Чтобы внедрение прошло гладко, мы предоставим полную документацию для EX5-библиотеки History Manager, а также примеры практического использования. Эти примеры покажут, как интегрировать библиотеку в ваши проекты и проводить эффективный торговый анализ. Кроме того, мы включим простые примеры советников и пошаговые демонстрации, которые помогут вам оптимизировать свои торговые стратегии и в полной мере использовать возможности библиотеки.
Файл с исходным кодом HistoryManager.mq5 приложен внизу. Спасибо за внимание и успехов в торговле и программировании на MQL5!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/16528
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Нейросети в трейдинге: Распутывание структурных компонентов (SCNN)
Преодоление ограничений машинного обучения (Часть 1): Нехватка совместимых метрик
Квантовая нейросеть на MQL5 (Часть II): Обучаем нейросеть с обратным распространением ошибки на марковских матрицах ALGLIB
Трейдинг с экономическим календарем MQL5 (Часть 5): Добавление в панель адаптивных элементов управления и кнопок сортировки
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования