Создание системы торговой аналитики (Часть 2): Как фиксировать закрытые сделки и передавать данные в формате JSON с помощью MQL5
Введение
После настройки внешнего бэкенда становится очевидной следующая проблема: MetaTrader 5 не может автоматически фиксировать закрытые сделки и отправлять данные о них во внешнюю систему. Данные о сделках остаются внутри терминала, если не экспортировать их вручную и не извлекать с помощью скриптов. Это затрудняет создание целостного пайплайна обработки данных, работающего на основе событий.
В части 1 заложили основу бэкенда: определили архитектуру системы, создали модель данных и подготовили API с поддержкой версионирования для приема торговых данных. Однако бэкенд остается без данных, пока в терминале не появится механизм передачи структурированной информации о торговых операциях.
В этой статье мы устраним этот пробел. Мы разрабатываем легковесный советник, который отслеживает закрытие сделок. Он извлекает нужные данные из MetaTrader 5, представляет их в формате JSON и отправляет в бэкенд по HTTP. Основное внимание уделяется исключительно надежной и воспроизводимой фиксации и передаче данных о закрытых сделках.
К концу статьи у вас будет работающий мост между MetaTrader 5 и внешним бэкендом. Система сможет обнаруживать закрытие позиции и передавать сведения о ней в API, создавая основу для сохранения и анализа данных в следующей части серии.
Предварительные требования
Прежде чем продолжить, убедитесь, что обладаете следующими знаниями и навыками:
- Основы программирования на языке MQL5. Вам нужно разбираться в переменных и управлении потоком выполнения программы, а также уметь писать и организовывать функции.
- Работа с MetaTrader 5. Вы должны уметь ориентироваться на платформе, добавлять советники на график и удалять их, а также просматривать журналы в панели "Инструменты".
- Использование MetaEditor. Вы должны уметь создавать исходные файлы, писать код, компилировать программы и выявлять базовые ошибки.
- Знакомство с контекстом серии статей. Рекомендуется прочитать часть 1, чтобы разобраться в настройке бэкенда и структуре API, но выполнять описанные здесь шаги можно и без этого.
Где мы сейчас и что дальше
В части 1 мы заложили основу бэкенда системы. Мы определили четкую модель данных, создали легковесный API и убедились, что сервер может работать локально и принимать запросы. На этом этапе бэкенд готов, но пока не получает данных из MetaTrader 5.
В этой части мы завершим подключение терминала к бэкенду. Мы разработаем советник, который будет отслеживать события закрытия сделок в MetaTrader 5, извлекать нужные сведения и отправлять их в бэкенд в структурированном формате. Цель — создать простой и надежный мост для передачи данных между торговым терминалом и внешней системой.
К концу статьи система сможет обнаруживать закрытие позиции и передавать сведения о ней в API. На этом будет завершено создание основного пайплайна передачи торговых данных из MetaTrader 5 во внешнюю среду.
Пошаговое создание моста
На этом этапе мы переходим от проектирования к реализации. Лучший способ освоить этот раздел — создавать советник параллельно с чтением пояснений. Практическая работа поможет лучше разобраться в этих понятиях и запомнить их. Для этого к разделу приложен полный исходный файл tradeAnalyticsBridge.mq5. Рекомендуем скачать его и оставить открытым в отдельной вкладке. Если при разработке возникнут проблемы, вы сможете обратиться к этому файлу и сравнить с ним свой код.
Для начала запустите MetaEditor и создайте новый файл советника. Вы можете использовать любое имя, хотя единообразное именование помогает лучше организовать файлы. После создания файла вставьте следующий начальный код.
//+------------------------------------------------------------------+ //| tradeAnalyticsBridge.mq5 | //| Copyright 2026, MetaQuotes Ltd. Developer is Chacha Ian | //| https://www.mql5.com/ru/users/chachaian | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, MetaQuotes Ltd. Developer is Chacha Ian" #property link "https://www.mql5.com/ru/users/chachaian" #property version "1.00" input string apiEndpointUrl = "http://127.0.0.1:5000/api/v1/trades"; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Notify why the program stopped running Print("Program terminated! Reason code: ", reason); } //+------------------------------------------------------------------+ //| OnTradeTransaction function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { //--- If position closed } //+------------------------------------------------------------------+
Этот код служит основой моста. Каждая часть имеет свое назначение и подготавливает программу к реализации последующей логики.
Разбираем основу
Заголовочная часть задает основные свойства советника. Она содержит метаданные, например сведения о версии и авторе. Это не влияет на торговую логику, но облегчает сопровождение программы и позволяет отслеживать ее версию и авторство.
//+------------------------------------------------------------------+ //| tradeAnalyticsBridge.mq5 | //| Copyright 2026, MetaQuotes Ltd. Developer is Chacha Ian | //| https://www.mql5.com/ru/users/chachaian | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, MetaQuotes Ltd. Developer is Chacha Ian" #property link "https://www.mql5.com/ru/users/chachaian" #property version "1.00"
apiEndpointUrl задает эндпоинт сервера, на который будут отправляться данные о сделках. Этот входной параметр позволяет пользователю менять адрес назначения без изменения кода.
input string apiEndpointUrl = "http://127.0.0.1:5000/api/v1/trades";
Функция OnInit выполняется при добавлении советника на график. В этой реализации она просто подтверждает успешную инициализацию.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { return(INIT_SUCCEEDED); }
Функция OnDeinit вызывается при удалении программы. Она выводит сообщение с указанием причины остановки программы. Это облегчает отладку и наблюдение за работой программы при тестировании.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Notify why the program stopped running Print("Program terminated! Reason code: ", reason); }
Функция OnTradeTransaction — основной обработчик событий. Она срабатывает при каждом событии в терминале, связанном с торговлей. На этом этапе она пуста: мы будем постепенно добавлять в нее логику по мере разработки системы.
//+------------------------------------------------------------------+ //| OnTradeTransaction function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { //--- If position closed }
Мы используем обработчик событий OnTradeTransaction, поскольку он реагирует на торговые события по мере их возникновения. Этот обработчик событий срабатывает только при событиях, связанных с торговлей. Постоянные проверки на каждом тике неэффективны и могут быть ненадежными. Такой подход точнее и лучше подходит для фиксации значимых изменений состояния счета.
Опрос в OnTick требует перебора позиций и сравнения их состояний с предыдущими. Это усложняет обработку и может приводить к пропуску граничных случаев. Фиксируя только закрытые сделки, мы работаем уже с окончательными и полными данными. Это позволяет избежать частичных обновлений: каждая запись, отправляемая в бэкенд, содержит данные о полностью завершенной сделке и готова к сохранению или дальнейшей обработке.
Обнаружение закрытых сделок
Процесс обработки в этой системе начинается при закрытии сделки. Поэтому сначала нам нужен надежный способ обнаружения таких событий. Определим функцию IsClosedTradeDeal.
//+------------------------------------------------------------------+ //| Returns true if this transaction is a closed trade deal | //+------------------------------------------------------------------+ bool IsClosedTradeDeal(const MqlTradeTransaction &trans) { if(trans.type != TRADE_TRANSACTION_DEAL_ADD) { return false; } if(trans.deal == 0) { return false; } if(!HistoryDealSelect(trans.deal)) { return false; } long deal_entry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY); return (deal_entry == DEAL_ENTRY_OUT || deal_entry == DEAL_ENTRY_OUT_BY); }
Эта функция фильтрует транзакции, отбирая только те, которые соответствуют закрытым сделкам. Сначала проверяется, соответствует ли тип транзакции добавлению сделки в историю. Для наших целей имеют значение только такие события. Если это условие не выполняется, функция сразу завершает работу.
//+------------------------------------------------------------------+ //| Returns true if this transaction is a closed trade deal | //+------------------------------------------------------------------+ bool IsClosedTradeDeal(const MqlTradeTransaction &trans) { if(trans.type != TRADE_TRANSACTION_DEAL_ADD) { return false; } }
Затем функция проверяет наличие действительного тикета сделки. Без действительного тикета нет данных для проверки.
//+------------------------------------------------------------------+ //| Returns true if this transaction is a closed trade deal | //+------------------------------------------------------------------+ bool IsClosedTradeDeal(const MqlTradeTransaction &trans) { ... if(trans.deal == 0) { return false; } }
Затем функция выбирает сделку из истории по ее тикету. Этот шаг необходим для доступа к ее свойствам.
//+------------------------------------------------------------------+ //| Returns true if this transaction is a closed trade deal | //+------------------------------------------------------------------+ bool IsClosedTradeDeal(const MqlTradeTransaction &trans) { ... if(!HistoryDealSelect(trans.deal)) { return false; } }
Наконец, она считывает тип входа/выхода сделки. Если этот тип указывает на выход с рынка, функция возвращает true. В противном случае функция возвращает false.
//+------------------------------------------------------------------+ //| Returns true if this transaction is a closed trade deal | //+------------------------------------------------------------------+ bool IsClosedTradeDeal(const MqlTradeTransaction &trans) { ... long deal_entry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY); return (deal_entry == DEAL_ENTRY_OUT || deal_entry == DEAL_ENTRY_OUT_BY); }
Такой подход обеспечивает реакцию системы только на завершенные сделки, отсекая остальной шум торговых транзакций.
Представление причин сделок в читаемом виде
После обнаружения закрытой сделки нужно интерпретировать ее свойства. Причина сделки показывает, почему она была совершена (вручную, советником, по стоп-лоссу, по тейк-профиту, при стоп-ауте, с мобильного устройства или через веб-интерфейс). В таком виде эти значения неудобны для передачи и интерпретации во внешних системах. Чтобы решить эту проблему, мы определяем функцию DealReasonToString.
//+------------------------------------------------------------------+ //| Convert deal reason to readable text | //+------------------------------------------------------------------+ string DealReasonToString(const ENUM_DEAL_REASON reason) { switch(reason) { case DEAL_REASON_CLIENT: return "CLIENT"; case DEAL_REASON_MOBILE: return "MOBILE"; case DEAL_REASON_WEB: return "WEB"; case DEAL_REASON_EXPERT: return "EXPERT"; case DEAL_REASON_SL: return "STOP_LOSS"; case DEAL_REASON_TP: return "TAKE_PROFIT"; case DEAL_REASON_SO: return "STOP_OUT"; case DEAL_REASON_ROLLOVER: return "ROLLOVER"; case DEAL_REASON_VMARGIN: return "VMARGIN"; case DEAL_REASON_SPLIT: return "SPLIT"; default: return "UNKNOWN"; } }
Эта функция сопоставляет каждую константу причины сделки с понятной меткой, например STOP_LOSS или TAKE_PROFIT. Сопоставление выполняется с помощью оператора switch. Задача этой функции — подготовить данные для внешнего использования. Вместо исходных числовых кодов мы отправляем понятные значения, которые легко интерпретировать в бэкенде.
Определение направления сделки
Аналогичным образом необходимо стандартизировать обозначение направления сделки. Функция DealTypeToPositionType преобразует константы типов сделок в простые метки. Сделке на покупку соответствует BUY, а сделке на продажу — SELL.
//+------------------------------------------------------------------+ //| Convert deal type to BUY / SELL | //+------------------------------------------------------------------+ string DealTypeToPositionType(const ENUM_DEAL_TYPE deal_type) { switch(deal_type) { case DEAL_TYPE_BUY: return "BUY"; case DEAL_TYPE_SELL: return "SELL"; default: return "UNKNOWN"; } }
Это обеспечивает согласованность при передаче торговых данных. Кроме того, это соответствует структуре, заданной в модели бэкенда.
Поиск открывающей сделки
Закрытая сделка содержит лишь часть необходимой информации. Чтобы получить такие данные, как цена входа и время открытия, необходимо найти сделку, открывшую позицию. Эту задачу выполняет функция FindOpeningDealTicket.
//+------------------------------------------------------------------+ //| Find the opening deal ticket for a given position | //+------------------------------------------------------------------+ ulong FindOpeningDealTicket(const ulong position_ticket) { if(position_ticket == 0) { return 0; } if(!HistorySelectByPosition(position_ticket)) { return 0; } int total_deals = HistoryDealsTotal(); for(int i = 0; i < total_deals; i++) { ulong deal_ticket = HistoryDealGetTicket(i); if(deal_ticket == 0) { continue; } long entry_type = HistoryDealGetInteger(deal_ticket, DEAL_ENTRY); if(entry_type == DEAL_ENTRY_IN || entry_type == DEAL_ENTRY_INOUT) { return deal_ticket; } } return 0; }
Сначала она проверяет тикет позиции. Если тикет недействителен, функция немедленно завершает работу.
//+------------------------------------------------------------------+ //| Find the opening deal ticket for a given position | //+------------------------------------------------------------------+ ulong FindOpeningDealTicket(const ulong position_ticket) { if(position_ticket == 0) { return 0; } }
Затем она выбирает историю сделок по этой позиции. Это позволяет получить доступ ко всем сделкам, связанным с ней.
//+------------------------------------------------------------------+ //| Find the opening deal ticket for a given position | //+------------------------------------------------------------------+ ulong FindOpeningDealTicket(const ulong position_ticket) { ... if(!HistorySelectByPosition(position_ticket)) { return 0; } }
Затем функция перебирает доступные сделки. Для каждой сделки проверяется тип входа/выхода. Когда функция находит сделку с типом входа, она возвращает ее тикет.
//+------------------------------------------------------------------+ //| Find the opening deal ticket for a given position | //+------------------------------------------------------------------+ ulong FindOpeningDealTicket(const ulong position_ticket) { ... int total_deals = HistoryDealsTotal(); for(int i = 0; i < total_deals; i++) { ulong deal_ticket = HistoryDealGetTicket(i); if(deal_ticket == 0) { continue; } long entry_type = HistoryDealGetInteger(deal_ticket, DEAL_ENTRY); if(entry_type == DEAL_ENTRY_IN || entry_type == DEAL_ENTRY_INOUT) { return deal_ticket; } } return 0; }
Эта функция позволяет связать закрывающую сделку с соответствующей открывающей сделкой. Этот шаг необходим для формирования полной записи о сделке.
Структурирование данных в формате JSON
Теперь у нас есть все необходимые поля данных. Следующий шаг — подготовить их к передаче. Все сообщения в этой системе представлены в формате JSON. Это обеспечивает совместимость с API бэкенда. Поскольку в языке MQL5 нет встроенной поддержки JSON, мы вручную формируем строку JSON. Это выполняется в функции BuildTradeJson.
//+------------------------------------------------------------------+ //| Build JSON string from trade data | //+------------------------------------------------------------------+ string BuildTradeJson( string symbol, long order_ticket, long deal_ticket, long position_ticket, string position_type, string position_reason, double lot_size, double entry_price, double stop_loss, double take_profit, double profit, datetime open_time, datetime close_time, long magic_number ) { string json = "{"; json += "\"symbol\":\"" + symbol + "\","; json += "\"order_ticket\":" + IntegerToString(order_ticket) + ","; json += "\"deal_ticket\":" + IntegerToString(deal_ticket) + ","; json += "\"position_ticket\":" + IntegerToString(position_ticket) + ","; json += "\"position_type\":\"" + position_type + "\","; json += "\"position_reason\":\"" + position_reason + "\","; json += "\"lot_size\":" + DoubleToString(lot_size, 2) + ","; json += "\"entry_price\":" + DoubleToString(entry_price, _Digits) + ","; json += "\"stop_loss\":" + DoubleToString(stop_loss, _Digits) + ","; json += "\"take_profit\":" + DoubleToString(take_profit, _Digits) + ","; json += "\"profit\":" + DoubleToString(profit, 2) + ","; json += "\"open_time\":\"" + TimeToString(open_time, TIME_DATE | TIME_SECONDS) + "\","; json += "\"close_time\":\"" + TimeToString(close_time, TIME_DATE | TIME_SECONDS) + "\","; json += "\"magic_number\":" + IntegerToString(magic_number); json += "}"; return json; }
Функция принимает в качестве входных параметров значения всех необходимых полей сделки. Затем она формирует структурированную строку, последовательно добавляя пары "ключ — значение". Каждое значение форматируется в соответствии с его типом. Числовые значения преобразуются с помощью соответствующих функций форматирования, а значения времени — в удобочитаемые строки. В результате получаются структурированные данные в формате JSON, которого и ожидает сервер.
Отправка данных на сервер
После подготовки данных JSON можно переходить к их отправке. За отправку отвечает функция SendTradeToServer.
//+------------------------------------------------------------------+ //| Send JSON trade data to backend | //+------------------------------------------------------------------+ bool SendTradeToServer(const string json_payload) { string url = apiEndpointUrl; string headers = "Content-Type: application/json\r\n"; char data[]; StringToCharArray(json_payload, data); char result[]; string result_headers; ResetLastError(); int response = WebRequest( "POST", url, headers, 5000, data, result, result_headers ); if(response == -1) { Print("WebRequest failed. Error: ", GetLastError()); return false; } Print("Server response code: ", response); Print("Server response body: ", CharArrayToString(result)); return (response == 200 || response == 201); }
Сначала из входного параметра извлекается адрес эндпоинта API. Это обеспечивает гибкость при работе в разных средах. Функция задает заголовки запроса, указывающие, что содержимое передается в формате JSON. Далее строка JSON преобразуется в массив символов. Это необходимо, поскольку функция WebRequest работает с байтовыми данными. Затем запрос отправляется HTTP-методом POST. Задается таймаут, чтобы избежать бесконечного ожидания. Если запрос завершается сбоем, функция записывает ошибку в журнал. В случае успеха она выводит ответ сервера для проверки. Эта функция образует слой взаимодействия между MetaTrader и бэкендом.
Обработка закрытой сделки
Теперь объединим все компоненты в единый процесс обработки. Функция ProcessClosedTrade отвечает за координацию этого процесса.
//+------------------------------------------------------------------+ //| Extract, build JSON and send trade | //+------------------------------------------------------------------+ void ProcessClosedTrade(const MqlTradeTransaction &trans) { ulong close_deal_ticket = trans.deal; if(close_deal_ticket == 0) return; if(!HistoryDealSelect(close_deal_ticket)) return; // --- close-side string symbol = HistoryDealGetString(close_deal_ticket, DEAL_SYMBOL); long order_ticket = HistoryDealGetInteger(close_deal_ticket, DEAL_ORDER); long deal_ticket = (long)close_deal_ticket; long position_ticket = HistoryDealGetInteger(close_deal_ticket, DEAL_POSITION_ID); long deal_type = HistoryDealGetInteger(close_deal_ticket, DEAL_TYPE); long deal_reason = HistoryDealGetInteger(close_deal_ticket, DEAL_REASON); double lot_size = HistoryDealGetDouble(close_deal_ticket, DEAL_VOLUME); double close_price = HistoryDealGetDouble(close_deal_ticket, DEAL_PRICE); double stop_loss = HistoryDealGetDouble(close_deal_ticket, DEAL_SL); double take_profit = HistoryDealGetDouble(close_deal_ticket, DEAL_TP); double profit = HistoryDealGetDouble(close_deal_ticket, DEAL_PROFIT); long magic_number = HistoryDealGetInteger(close_deal_ticket, DEAL_MAGIC); datetime close_time = (datetime)HistoryDealGetInteger(close_deal_ticket, DEAL_TIME); // --- opening side ulong open_deal_ticket = FindOpeningDealTicket((ulong)position_ticket); double entry_price = 0.0; datetime open_time = 0; if(open_deal_ticket != 0 && HistoryDealSelect(open_deal_ticket)) { entry_price = HistoryDealGetDouble(open_deal_ticket, DEAL_PRICE); open_time = (datetime)HistoryDealGetInteger(open_deal_ticket, DEAL_TIME); } // --- enums string position_type = DealTypeToPositionType((ENUM_DEAL_TYPE)deal_type); string position_reason = DealReasonToString((ENUM_DEAL_REASON)deal_reason); // --- build JSON string json = BuildTradeJson( symbol, order_ticket, deal_ticket, position_ticket, position_type, position_reason, lot_size, entry_price, stop_loss, take_profit, profit, open_time, close_time, magic_number ); Print("JSON Payload: ", json); // --- send SendTradeToServer(json); }
Она выбирает закрывающую сделку и извлекает нужные свойства. К ним относятся символ, объем, цена, прибыль и идентификаторы. Затем она получает открывающую сделку с помощью ранее определенной вспомогательной функции. Это позволяет получить цену входа и время открытия. Затем функция преобразует значения перечислений в удобочитаемые строки. Благодаря этому итоговый набор данных становится понятным и единообразным. После подготовки всех данных вызывается функция формирования JSON для создания передаваемого сообщения. В заключение сформированные данные выводятся для проверки и отправляются на сервер. Эта функция реализует полный пайплайн обработки одной закрытой сделки.
Предотвращение повторной обработки
Прежде чем перейти к тестированию, нам нужно добавить небольшой защитный механизм, чтобы предотвратить повторную обработку одной и той же сделки.
Советник-мост использует обработчик событий OnTradeTransaction, отслеживающий торговые события на уровне счета. Это означает, что советник реагирует на торговую активность всего счета, а не только на активность, связанную с графиком, на который он добавлен. Поэтому советник-мост следует добавлять только на один график. Если он добавлен на несколько графиков, каждый экземпляр может получить одно и то же событие по счету и отправить одинаковые данные о сделке в бэкенд.
Даже при соблюдении этого правила полезно добавить в советник простую проверку на повторную обработку. Для этого мы ведем список тикетов уже обработанных сделок.
Сначала объявим динамический массив рядом с блоком входных параметров:
ulong processedDeals[]; В этом массиве хранятся тикеты сделок, уже обработанных мостом. Перед отправкой данных о закрытой сделке в бэкенд мы проверяем, есть ли ее тикет в этом списке.
Чтобы выполнить эту проверку, мы определяем следующую функцию:
//+------------------------------------------------------------------+ //| Check if deal has already been processed | //+------------------------------------------------------------------+ bool IsDealAlreadyProcessed(ulong deal) { //--- Iterate through processed deals for(int i = 0; i < ArraySize(processedDeals); i++) { if(processedDeals[i] == deal) return true; } return false; }
Функция перебирает массив processedDeals и сравнивает каждый сохраненный тикет с тикетом текущей сделки. Если найдено совпадение, функция возвращает true. Если совпадение не найдено, функция возвращает false.
Затем изменим обработчик событий OnTradeTransaction так, чтобы закрытая сделка обрабатывалась только один раз:
//+------------------------------------------------------------------+ //| Trade transaction handler | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { //--- Check if this is a closed trade event if(IsClosedTradeDeal(trans)) { ulong deal = trans.deal; //--- Prevent duplicate processing if(IsDealAlreadyProcessed(deal)) return; //--- Mark deal as processed ArrayResize(processedDeals, ArraySize(processedDeals) + 1); processedDeals[ArraySize(processedDeals) - 1] = deal; } }
Логика здесь проста. При обнаружении закрытой сделки советник сначала проверяет, обрабатывалась ли уже сделка с таким тикетом. Если сделка уже обработана, функция немедленно завершает работу. Если нет, тикет добавляется в массив, после чего сделка обрабатывается.
Это не отменяет необходимости правильно развертывать советник. Советник-мост по-прежнему следует добавлять только на один график, поскольку он отслеживает торговые события на уровне счета. Проверка повторной обработки лишь добавляет защитный слой внутри программы.
С этим дополнением советник лучше подготовлен к тестированию: одна и та же сделка не обрабатывается повторно в пределах одного запущенного экземпляра.
Объединяем все компоненты
Теперь, когда все вспомогательные функции определены, мы изменим обработчик событий.
//+------------------------------------------------------------------+ //| Trade transaction handler | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { //--- Check if this is a closed trade event if(IsClosedTradeDeal(trans)) { ulong deal = trans.deal; //--- Prevent duplicate processing if(IsDealAlreadyProcessed(deal)) return; //--- Mark deal as processed ArrayResize(processedDeals, ArraySize(processedDeals) + 1); processedDeals[ArraySize(processedDeals) - 1] = deal; //--- Process the closed trade ProcessClosedTrade(trans); } }
Функция OnTradeTransaction изменяется так, чтобы проверять, произошло ли закрытие сделки. Если условие выполняется, вызывается ProcessClosedTrade. Это связывает логику обнаружения с пайплайном обработки. С этого момента каждая закрытая сделка будет запускать полную последовательность извлечения, форматирования и передачи данных.
На этом этапе мост готов. Советник может обнаружить закрытие сделки, извлечь сведения о ней, преобразовать данные в формат JSON и отправить их в бэкенд. В следующем разделе мы протестируем эту реализацию и убедимся, что система работает ожидаемым образом.
Тестирование моста в локальной среде
Перед тестированием советника сервер бэкенда должен быть запущен и готов к приему запросов. Этот бэкенд был разработан в части 1. Если вы не знакомы с настройкой, рекомендуется еще раз просмотреть соответствующий раздел. Тот же исходный файл app.py снова приложен для справки.
Мы проведем это тестирование в локальной среде разработки. Для начала откройте терминал и перейдите в каталог проекта бэкенда.
cd mt5TradeAnalyticsBackend
Перейдя в каталог проекта, активируйте виртуальную среду.
. .venv/bin/activate
После активации среды запустите сервер Flask с помощью следующей команды:
flask --app app run --host 0.0.0.0 --debug
Эта команда запускает сервер бэкенда и позволяет ему принимать входящие HTTP-запросы. Параметр host обеспечивает доступ с локального компьютера, а режим отладки выводит полезные сообщения при разработке. После успешного запуска сервера в терминале должно появиться сообщение о том, что приложение работает. Ниже для справки приведен скриншот этого вывода.

Чтобы убедиться, что сервер готов отвечать на запросы, откройте браузер и введите следующий адрес:
http://127.0.0.1:5000/api/v1/health Если все работает правильно, браузер отобразит ответ JSON, подтверждающий, что бэкенд запущен. Это подтверждает, что сервер работает и готов принимать данные.

После настройки бэкенда вернитесь в MetaTrader 5 и добавьте советник на график. Но сначала необходимо разрешить MetaTrader 5 отправлять HTTP-запросы на сервер бэкенда. По умолчанию MetaTrader 5 блокирует внешние веб-запросы из соображений безопасности. Если URL бэкенда не добавлен в список разрешенных адресов, функция WebRequest завершится с ошибкой, даже если код советника написан правильно.
Выполните следующие действия:
- Откройте MetaTrader 5.
- Выберите "Сервис" в главном меню.
- В раскрывающемся меню выберите "Настройки".
- В окне "Настройки" откройте вкладку "Советники".
- Найдите и установите флажок "Разрешить WebRequest для следующих URL".
- В список URL добавьте корневой адрес локального бэкенда:
http://127.0.0.1:5000 - Если сервер Flask работает на другом компьютере в той же локальной сети, добавьте и его адрес. Например:
http://192.168.1.20:5000 - Нажмите "OK", чтобы сохранить изменения.
После этого советник-мост сможет отправлять запросы к эндпоинту бэкенда, заданному входным параметром apiEndpointUrl. Без этой настройки MetaTrader 5 заблокирует запрос до того, как он достигнет сервера Flask.
В этом примере советник добавлен на часовой график XAUUSD. После добавления на график советник начинает отслеживать торговые события.

Чтобы проверить мост, откройте новую позицию. В этом примере открывается позиция объемом 0,10.

Оставьте позицию открытой ненадолго, затем закройте ее вручную. Это действие вызывает событие торговой транзакции в терминале. После закрытия позиции откройте панель "Инструменты" и перейдите на вкладку "Эксперты". Здесь должен появиться вывод, похожий на приведенный ниже.
2026.04.24 16:31:19.861 tradeAnalyticsBridge (XAUUSD,H1) JSON Payload: {"symbol":"XAUUSD","order_ticket":370171325,"deal_ticket":270524399,"position_ticket":370071966,"position_type":"SELL","position_reason":"CLIENT","lot_size":0.10,"entry_price":4709.96,"stop_loss":0.00,"take_profit":0.00,"profit":-161.90,"open_time":"2026.04.24 15:18:55","close_time":"2026.04.24 16:31:18","magic_number":0} 2026.04.24 16:31:19.944 tradeAnalyticsBridge (XAUUSD,H1) Server response code: 200 2026.04.24 16:31:19.944 tradeAnalyticsBridge (XAUUSD,H1) Server response body: { 2026.04.24 16:31:19.944 tradeAnalyticsBridge (XAUUSD,H1) "message": "Trade endpoint ready. Logic will be implemented later." 2026.04.24 16:31:19.944 tradeAnalyticsBridge (XAUUSD,H1) }
Выводятся данные в формате JSON со сведениями о сделке, зафиксированными и отформатированными советником. Сразу после этого отображается ответ сервера. Код ответа 200 означает, что запрос был успешно получен. Сервер также возвращает сообщение, подтверждающее работу эндпоинта. Ниже приведен скриншот вкладки "Эксперты" с этим выводом.

Затем вернитесь в терминал, где запущен сервер бэкенда. В журнале должна появиться запись о поступлении POST-запроса на указанный эндпоинт. Эта запись в журнале подтверждает, что данные успешно переданы из MetaTrader 5 в бэкенд. Также прилагается скриншот этого вывода.

На этом этапе бэкенд не обрабатывает и не сохраняет полученные данные. Тем не менее этот тест подтверждает, что основная цель достигнута. Советник успешно обнаруживает закрытую сделку, извлекает данные о ней и передает их в бэкенд в структурированном формате.
На этом проверка моста завершена. Теперь система может передавать торговые данные из MetaTrader 5 во внешнюю среду.
Текущие ограничения
Хотя разработанный в этом разделе мост работает, важно понимать границы его функциональности. Советник фиксирует закрытые сделки и отправляет данные о них в бэкенд в структурированном формате. Действуют следующие ограничения:
- Механизм повторных попыток для неудачных запросов отсутствует. Если сервер недоступен или запрос завершается неудачей, данные о сделке могут быть потеряны.
- Перед передачей данные не проверяются. Система исходит из того, что все извлеченные значения корректны и правильно отформатированы.
- Локального хранилища или очереди нет. Данные отправляются сразу после закрытия сделки, что может быть ненадежно при нестабильном сетевом соединении.
- Система обрабатывает только закрытые сделки. Она не отслеживает открытые позиции, изменения или частичные обновления.
Эти ограничения соответствуют задачам этой части серии. Основная цель — показать, как извлекать данные о сделках из MetaTrader 5 и отправлять их во внешнюю систему в структурированном формате. Проверка данных, их долговременное хранение и расширенная обработка ошибок выходят за рамки этой реализации.
Заключение
В этой части мы создали недостающее связующее звено между MetaTrader 5 и бэкендом. Цель — фиксировать закрытые сделки и передавать данные о них в структурированном формате. Теперь эта задача решена: создан рабочий советник, который реагирует на торговые события и взаимодействует с внешним API.
В результате у вас есть:
- работающий советник, отслеживающий торговые события с помощью OnTradeTransaction;
- надежный способ обнаружения закрытия позиции по истории сделок;
- полный процесс извлечения данных о символе, тикетах, ценах, объеме, прибыли и временных метках;
- преобразование внутренних констант в понятные значения, например BUY, SELL, STOP_LOSS и TAKE_PROFIT;
- структурированные данные в формате JSON, представляющие каждую закрытую сделку в единообразном формате;
- настраиваемый эндпоинт API, позволяющий выбирать адрес отправки данных;
- работающий слой HTTP-взаимодействия, использующий WebRequest для отправки данных в бэкенд;
- проведенный тест, подтвердивший успешную передачу торговых данных и их получение сервером.
Эти результаты подтверждают, что система больше не изолирована внутри торгового терминала. Теперь данные о сделках можно передавать за пределы MetaTrader 5 контролируемым и воспроизводимым способом. Это создает полноценный мост между торговой средой и внешним бэкендом.
На этом этапе бэкенд принимает данные, но пока не сохраняет и не обрабатывает их. Следующий шаг — реализовать всю предусмотренную функциональность API и подключить его к базе данных. Это позволит сохранять поступающие данные о сделках и подготавливать их к анализу.
В следующей части серии мы расширим бэкенд, реализовав логику приема поступающих данных и их записи в базу данных. Это завершит пайплайн данных и приблизит систему к готовому решению для торговой аналитики.
Приложения
Ниже приведены основные исходные файлы этой части, чтобы вы могли выполнять описанные шаги и проверять свою реализацию. Эти файлы содержат все работающие компоненты, разработанные в серии к настоящему моменту. Вы можете скачать файлы, изучить их структуру, сравнить со своей реализацией или использовать как справочный материал при возникновении проблем в ходе разработки.
| Имя файла | Описание |
|---|---|
| app.py | Приложение бэкенда на Flask, определяющее эндпоинты API и инициализирующее базу данных SQLite. Оно принимает входящие HTTP-запросы и служит точкой входа для обработки торговых данных. |
| tradeAnalyticsBridge.mq5 | Советник, разработанный в этой части, отслеживает торговые события, обнаруживает закрытые сделки, извлекает сведения о них, формирует данные в формате JSON и отправляет их в бэкенд с помощью HTTP-запросов. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22216
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
От новичка до эксперта: Автоматизация распознавания геометрических паттернов внутри базовых свечей зон ликвидности в MQL5
Создание традиционного индикатора Ренко с наложением на график в MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования