English
preview
Создание системы торговой аналитики (Часть 2): Как фиксировать закрытые сделки и передавать данные в формате JSON с помощью MQL5

Создание системы торговой аналитики (Часть 2): Как фиксировать закрытые сделки и передавать данные в формате JSON с помощью MQL5

MetaTrader 5 — Торговые системы |
37 0
Chacha Ian Maroa
Chacha Ian Maroa

Введение

После настройки внешнего бэкенда становится очевидной следующая проблема: 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 обеспечивает доступ с локального компьютера, а режим отладки выводит полезные сообщения при разработке. После успешного запуска сервера в терминале должно появиться сообщение о том, что приложение работает. Ниже для справки приведен скриншот этого вывода.

Server Listening

Чтобы убедиться, что сервер готов отвечать на запросы, откройте браузер и введите следующий адрес:

http://127.0.0.1:5000/api/v1/health

Если все работает правильно, браузер отобразит ответ JSON, подтверждающий, что бэкенд запущен. Это подтверждает, что сервер работает и готов принимать данные.

API Health Check

После настройки бэкенда вернитесь в 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. После добавления на график советник начинает отслеживать торговые события.

EA attached to chart

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

Long position opend

Оставьте позицию открытой ненадолго, затем закройте ее вручную. Это действие вызывает событие торговой транзакции в терминале. После закрытия позиции откройте панель "Инструменты" и перейдите на вкладку "Эксперты". Здесь должен появиться вывод, похожий на приведенный ниже.

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 означает, что запрос был успешно получен. Сервер также возвращает сообщение, подтверждающее работу эндпоинта. Ниже приведен скриншот вкладки "Эксперты" с этим выводом.

Experts Journal Success

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

API Access Success

На этом этапе бэкенд не обрабатывает и не сохраняет полученные данные. Тем не менее этот тест подтверждает, что основная цель достигнута. Советник успешно обнаруживает закрытую сделку, извлекает данные о ней и передает их в бэкенд в структурированном формате.

На этом проверка моста завершена. Теперь система может передавать торговые данные из 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

Прикрепленные файлы |
app.py (3.22 KB)
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
От новичка до эксперта: Автоматизация распознавания геометрических паттернов внутри базовых свечей зон ликвидности в MQL5 От новичка до эксперта: Автоматизация распознавания геометрических паттернов внутри базовых свечей зон ликвидности в MQL5
В статье описывается реализация модуля на MQL5, который анализирует бары младшего таймфрейма внутри каждой базовой свечи зоны ликвидности. Он обнаруживает свинговые точки и по объективным правилам классифицирует внутреннюю структуру: восходящий, нисходящий или симметричный треугольник, прямоугольник, M, W либо неопределенная структура. Индикатор отображает на графике метки геометрических фигур и добавляет название паттерна в алерты, сокращая необходимость ручной проверки на младшем таймфрейме.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Создание традиционного индикатора Ренко с наложением на график в MQL5 Создание традиционного индикатора Ренко с наложением на график в MQL5
Создайте в MQL5 традиционный индикатор Ренко, который преобразует цены закрытия свечей в кирпичи фиксированного размера и отображает их на основном графике. Мы рассчитываем движение от цены закрытия последнего кирпича, создаем новые кирпичи заданного пользователем размера, подтверждаем развороты с помощью правила двух кирпичей, храним цены закрытия кирпичей в динамическом массиве и отображаем прямоугольники для визуализации тренда в режиме реального времени.