Связь между терминалами MT5 через Named Pipes на MQL5

Связь между терминалами MT5 через Named Pipes на MQL5 MQL4 / MQL5 (MetaTrader)
Пошаговое руководство по реализации сверхбыстрого межпроцессного взаимодействия (IPC) между терминалами MetaTrader 5 с помощью именованных каналов (Named Pipes) на MQL5.
Суть: Использование Windows API функций (CreateNamedPipeW, ReadFile, WriteFile) через импорт kernel32.dll позволяет организовать сверхбыстрый (sub-millisecond) локальный обмен данными между несколькими терминалами MT5 без использования сторонних DLL и сетевых сокетов.

Зачем нужны Named Pipes в алготрейдинге?

При построении сложных распределенных торговых систем часто возникает необходимость мгновенной передачи сигналов между терминалами. Например, когда ведущий советник, использующий панель управления советником с кнопками Buy/Sell, должен мгновенно дублировать сделки на ведомые терминалы (копирование сделок).

В отличие от сетевых сокетов или отправки уведомлений через внешние API (как описано в статье про критические PUSH-уведомления через WebRequest), именованные каналы (Named Pipes) работают локально на уровне ядра Windows. Это обеспечивает субмиллисекундную задержку (latency), что критически важно для высокочастотного трейдинга и синхронизации сложных стратегий, таких как сетка ордеров (Grid) с мартингейлом.

Исходный код

Ниже представлен готовый к использованию советник (Expert Advisor), который может работать как в режиме Сервера (Master), так и в режиме Клиента (Slave). Код написан с использованием неблокирующего режима чтения, что предотвращает зависание интерфейса MetaTrader 5.

//+------------------------------------------------------------------+
//|                                                    MT5_Pipes.mq5 |
//|                                      Copyright 2024, Finfluct    |
//|                                             https://finfluct.com |
//+------------------------------------------------------------------+
#property property_strict
#property description "Named Pipes IPC for MT5 Terminals"

// Импорт функций Windows API из kernel32.dll
#import "kernel32.dll"
int CreateNamedPipeW(string lpName, uint dwOpenMode, uint dwPipeMode, uint nMaxInstances, uint nOutBufferSize, uint nInBufferSize, uint nDefaultTimeOut, ulong lpSecurityAttributes);
int ConnectNamedPipe(int hNamedPipe, ulong lpOverlapped);
int DisconnectNamedPipe(int hNamedPipe);
int CreateFileW(string lpFileName, uint dwDesiredAccess, uint dwShareMode, ulong lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, int hTemplateFile);
int ReadFile(int hFile, uchar &lpBuffer[], uint nNumberOfBytesToRead, uint &lpNumberOfBytesRead, ulong lpOverlapped);
int WriteFile(int hFile, uchar &lpBuffer[], uint nNumberOfBytesToWrite, uint &lpNumberOfBytesWritten, ulong lpOverlapped);
int CloseHandle(int hObject);
int PeekNamedPipe(int hNamedPipe, uchar &lpBuffer[], uint nBufferSize, uint &lpBytesRead, uint &lpTotalBytesAvail, uint &lpBytesLeftThisMessage);
#import

// Константы WinAPI
#define INVALID_HANDLE_VALUE -1
#define PIPE_ACCESS_DUPLEX 0x00000003
#define PIPE_TYPE_BYTE 0x00000000
#define PIPE_READMODE_BYTE 0x00000000
#define PIPE_NOWAIT 0x00000001
#define OPEN_EXISTING 3
#define GENERIC_READ 0x80000000
#define GENERIC_WRITE 0x40000000
#define FILE_ATTRIBUTE_NORMAL 0x00000080

enum ENUM_ROLE {
   ROLE_SERVER, // Master (Сервер)
   ROLE_CLIENT  // Slave (Клиент)
};

input ENUM_ROLE InputRole = ROLE_SERVER;
input string InputPipeName = "MT5_Easy_Pipe";

int hPipe = INVALID_HANDLE_VALUE;
string fullPipePath = "";

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   fullPipePath = "\\\\.\\pipe\\" + InputPipeName;
   EventSetMillisecondTimer(100);

   if(InputRole == ROLE_SERVER)
   {
      // Создаем именованный канал в неблокирующем режиме (PIPE_NOWAIT)
      hPipe = CreateNamedPipeW(fullPipePath, 
                               PIPE_ACCESS_DUPLEX, 
                               PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_NOWAIT, 
                               1, 1024, 1024, 0, 0);
      if(hPipe == INVALID_HANDLE_VALUE)
      {
         Print("Ошибка создания Named Pipe. Код ошибки: ", GetLastError());
         return(INIT_FAILED);
      }
      Print("Сервер Named Pipe запущен: ", fullPipePath);
   }
   else
   {
      Print("Клиент инициализирован. Ожидание подключения к: ", fullPipePath);
   }

   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   EventKillTimer();
   if(hPipe != INVALID_HANDLE_VALUE)
   {
      if(InputRole == ROLE_SERVER)
      {
         DisconnectNamedPipe(hPipe);
      }
      CloseHandle(hPipe);
      hPipe = INVALID_HANDLE_VALUE;
   }
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   // Пример: Сервер транслирует изменение цены Bid в канал
   if(InputRole == ROLE_SERVER && hPipe != INVALID_HANDLE_VALUE)
   {
      static double lastBid = 0;
      double currentBid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      if(currentBid != lastBid)
      {
         lastBid = currentBid;
         string message = _Symbol + "|" + DoubleToString(lastBid, _Digits);
         SendMessage(message);
      }
   }
}

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
{
   if(InputRole == ROLE_CLIENT)
   {
      // Попытка подключения, если канал еще не открыт
      if(hPipe == INVALID_HANDLE_VALUE)
      {
         hPipe = CreateFileW(fullPipePath, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
         if(hPipe != INVALID_HANDLE_VALUE)
         {
            Print("Успешное подключение к Серверу Pipe!");
         }
      }
      else
      {
         // Чтение входящих сообщений
         string msg = ReceiveMessage();
         if(msg != "")
         {
            Print("Получено сообщение: ", msg);
         }
      }
   }
   else if(InputRole == ROLE_SERVER)
   {
      // Сервер проверяет входящие подключения в неблокирующем режиме
      ConnectNamedPipe(hPipe, 0);
   }
}

//+------------------------------------------------------------------+
//| Отправка сообщения в канал                                       |
//+------------------------------------------------------------------+
void SendMessage(string text)
{
   if(hPipe == INVALID_HANDLE_VALUE) return;

   uchar buffer[];
   StringToCharArray(text, buffer, 0, WHOLE_ARRAY, CP_UTF8);
   uint bytesWritten = 0;
   
   int res = WriteFile(hPipe, buffer, ArraySize(buffer), bytesWritten, 0);
   if(res == 0)
   {
      // Если запись не удалась, возможно клиент отключился
      Print("Ошибка записи в Pipe. Клиент отключен?");
   }
}

//+------------------------------------------------------------------+
//| Безопасное неблокирующее чтение из канала                        |
//+------------------------------------------------------------------+
string ReceiveMessage()
{
   if(hPipe == INVALID_HANDLE_VALUE) return "";

   uint bytesAvail = 0;
   uint bytesRead = 0;
   uint lpLeft = 0;
   uchar peekBuf[1];

   // Проверяем наличие данных в канале без блокировки потока
   int peekRes = PeekNamedPipe(hPipe, peekBuf, 1, bytesRead, bytesAvail, lpLeft);
   if(peekRes == 0 || bytesAvail == 0)
   {
      if(peekRes == 0)
      {
         CloseHandle(hPipe);
         hPipe = INVALID_HANDLE_VALUE;
         Print("Соединение с Pipe потеряно.");
      }
      return "";
   }

   uchar buffer[];
   ArrayResize(buffer, bytesAvail);
   
   int readRes = ReadFile(hPipe, buffer, bytesAvail, bytesRead, 0);
   if(readRes != 0 && bytesRead > 0)
   {
      return CharArrayToString(buffer, 0, bytesRead, CP_UTF8);
   }
   
   return "";
}

Разбор параметров

  • InputRole: Определяет режим работы советника. ROLE_SERVER создает именованный канал и выступает в роли источника сигналов (Master). ROLE_CLIENT подключается к существующему каналу для приема данных (Slave).
  • InputPipeName: Имя создаваемого канала. Должно быть абсолютно одинаковым на стороне Сервера и Клиента. Полный системный путь формируется автоматически как \\.\pipe\имя_канала.
  • PIPE_NOWAIT: Важнейший флаг WinAPI, переводящий канал в неблокирующий режим. Без него вызовы функций чтения/записи заморозят поток терминала MT5 до тех пор, пока на другом конце не появится активность.
  • PeekNamedPipe: Функция предварительного анализа буфера. Позволяет узнать, сколько байт готово к чтению, предотвращая зависание функции ReadFile.

Как запустить

Для демонстрации работы межпроцессного взаимодействия выполните следующие шаги:

1. Откройте первый терминал MetaTrader 5. Скомпилируйте код выше и запустите советник на любом графике с параметром InputRole = ROLE_SERVER. В журнале появится запись о запуске сервера.

2. Откройте второй терминал MetaTrader 5 на этом же компьютере. Запустите этот же советник, но с параметром InputRole = ROLE_CLIENT.

3. В журнале клиентского терминала появится сообщение Успешное подключение к Серверу Pipe!. При каждом изменении цены Bid на сервере, клиент будет мгновенно получать и выводить в лог сообщения вида EURUSD|1.08450.

Оцените статью
FinFluct