Клиентская программа сигнального сервиса на MQL5

Итак, мы решили, что текст в сообщениях сервиса будет в формате JSON.

В наиболее распространенном варианте, JSON представляет собой текстовое описание объекта, похожее на то, как это делается для структур в MQL5. Объект заключен в фигурные скобки, внутри которых через запятую пишутся его свойства: каждое свойство имеет идентификатор в кавычках, после чего идет двоеточие и значение свойства. Поддерживаются свойства нескольких примитивных типов: строки, целые и вещественные числа, логические true/false и пустое значение null. Кроме того, значением свойства может быть, в свою очередь, объект или массив. Массивы описываются с помощью квадратных скобок, внутри которых элементы перечисляются через запятую. Например,

{
   "string": "this is a text",
   "number": 0.1,
   "integer": 789735095,
   "enabled": true,
   "subobject" :
   {
      "option": null
   },
   "array":
   [
      1, 2, 3, 5, 8
   ]
}

В принципе, массив на верхнем уровне также является валидным JSON. Например,

[
   {
      "command": "buy",
      "volume": 0.1,
      "symbol": "EURUSD",
      "price": 1.0
   },
   {
      "command": "sell",
      "volume": 0.01,
      "symbol": "GBPUSD",
      "price": 1.5
   }
]

Для сокращения трафика в прикладных протоколах, использующих JSON, принято сокращать названия полей до нескольких букв (часто — до одной).

Названия свойств и строковые значения заключаются в двойные кавычки. Если требуется указать кавычку внутри строки, её следует экранировать обратной косой чертой.

Применение JSON делает протокол универсальным и расширяемым. Например, для проектируемого сервиса (торговых сигналов и, в более общем случае, копирования состояния счета) можно чисто теоретически предположить следующую структуру сообщения:

{
   "origin": "publisher_id",    // отправитель сообщения ("Server" в техническом сообщении)
   "msg" :                      // сообщение (текст или JSON), как поступило от отправителя
   {
      "trade" :                 // текущие торговые команды (если есть сигнал)
      {
         "operation": ...,      // покупка/продажа/закрытие
         "symbol": "ticker",
         "volume": 0.1,
         ... // другие параметры сигнала
      },
      "account":                // статус счета
      {
         "positions":           // позиции
         {
            "n": 10,            // количество открытых позиций
            [ { ... },{ ... } ] // массив свойств открытых позиций
         },
         "pending_orders":      // отложенные ордера
         {
            "n": ...
            [ { ... } ]
         }
         "drawdown": 2.56,
         "margin_level": 12345,
         ... // другие параметры статуса
      },
      "hardware":               // удаленный контроль "здоровья" ПК
      {
         "memory": ...,
         "ping_to_broker": ...
      }
   }
}

Какие-то из этих возможностей могут поддерживать, а могут не поддерживать конкретные реализации клиентских программ (всё, что им не "понятно", они просто проигнорируют). Кроме того, при соблюдении условия отсутствия конфликтов в названиях свойств на одном уровне, каждый поставщик информации может добавлять в JSON свои специфические данные. Сервис обмена сообщениями просто будет пересылать эту информацию. Разумеется, программа на приемной стороне должна уметь интерпретировать эти специфические данные.

К книге прилагается парсер JSON под названием ToyJson ("игрушечный" JSON, файл toyjson.mqh) — маленький, неэффективный, и без поддержки полных возможностей спецификации формата (например, в части обработки escape-последовательностей). Он был написан специально для данного демо-сервиса, с поправкой на предполагаемую, не особо сложную, структуру информации о торговых сигналах. Мы не станем его подробно здесь описывать, а принципы его использования станут ясны из исходного кода MQL-клиента сигнального сервиса.

Для своих проектов и в случае развития данного проекта вы можете выбрать другие парсеры JSON, доступные в кодовой базе на сайте mql5.com.

Один элемент (контейнер или свойство) в ToyJson описывается объектом класса JsValue. В нем определено несколько перегрузок метода put(key, value) для добавления именованных внутренних свойств как в JSON-объект или put(value) для добавления значения как в JSON-массив. Также данный объект может представлять и отдельное значение примитивного типа. Для чтения свойств JSON-объекта можно применить к JsValue нотацию оператора [] с указанием имени требуемого свойства в скобках. Очевидно, что целочисленные индексы поддерживаются для доступа внутрь JSON-массива.

Сформировав требуемую конфигурацию связанных объектов JsValue, можно её сериализовать в текст JSON-формата с помощью метода stringify(string &buffer).

Второй класс в toyjson.mqhJsParser — позволяет выполнять обратную операцию: превращать текст с описанием JSON в иерархическую структуру JsValue-объектов.

С учетом классов для работы с JSON приступим к написанию эксперта MQL5/Experts/MQL5Book/p7/wsTradeCopier/wstradecopier.mq5, который сможет выполнять обе роли в сервисе копирования сделок: поставщика информации о трейдах, совершаемых на счету, или получателя этой информации от сервиса для воспроизведения этих трейдов.

Объем и содержание пересылаемой информации остается, с политической точки зрения, на усмотрении поставщика и может существенно отличаться в зависимости от сценария (назначения) использования сервиса. В частности, можно копировать только совершаемые сделки или полностью состояние счета вместе с отложенными ордерами и защитными уровнями. В нашем примере мы лишь обозначим техническую реализацию передачи информации, а конкретный набор объектов и свойств можно затем подобрать по своему усмотрению.

Опишем в коде 3 структуры, унаследованные от встроенных структур и обеспечивающие "упаковку" информации в JSON:

  • MqlTradeRequestWeb — MqlTradeRequest;
  • MqlTradeResultWeb — MqlTradeResult;
  • DealMonitorWeb — DealMonitor*.

Последняя в списке структура, строго говоря, не является встроенной, а определена нами в файле DealMonitor.mqh, но она заполняется на стандартном наборе свойств сделок.

Конструктор каждой из производных структур заполняет поля на основе переданного первоисточника (торгового запроса, его результата или сделки). В каждой структуре реализован метод asJsValue, возвращающий указатель на объект JsValue, в котором отражены все свойства структуры: они добавляются в JSON-объект с помощью метода JsValue::put. Вот, например, как это сделано в случае MqlTradeRequest:

struct MqlTradeRequestWebpublic MqlTradeRequest
{
   MqlTradeRequestWeb(const MqlTradeRequest &r)
   {
      ZeroMemory(this);
      action = r.action;
      magic = r.magic;
      order = r.order;
      symbol = r.symbol;
      volume = r.volume;
      price = r.price;
      stoplimit = r.stoplimit;
      sl = r.sl;
      tp = r.tp;
      type = r.type;
      type_filling = r.type_filling;
      type_time = r.type_time;
      expiration = r.expiration;
      comment = r.comment;
      position = r.position;
      position_by = r.position_by;
   }
   
   JsValue *asJsValue() const
   {
      JsValue *req = new JsValue();
      // главный блок: action, symbol, type
      req.put("a"VerboseJson ? EnumToString(action) : (string)action);
      if(StringLen(symbol) != 0req.put("s"symbol);
      req.put("t"VerboseJson ? EnumToString(type) : (string)type);
      
      // объемы
      if(volume != 0req.put("v"TU::StringOf(volume));
      req.put("f"VerboseJson ? EnumToString(type_filling) : (string)type_filling);
      
      // блок с ценами
      if(price != 0req.put("p"TU::StringOf(price));
      if(stoplimit != 0req.put("x"TU::StringOf(stoplimit));
      if(sl != 0req.put("sl"TU::StringOf(sl));
      if(tp != 0req.put("tp"TU::StringOf(tp));
   
      // блок отложенных ордеров
      if(TU::IsPendingType(type))
      {
         req.put("t"VerboseJson ? EnumToString(type_time) : (string)type_time);
         if(expiration != 0req.put("d"TimeToString(expiration));
      }
   
      // блок модификации
      if(order != 0req.put("o"order);
      if(position != 0req.put("q"position);
      if(position_by != 0req.put("b"position_by);
      
      // вспомогательный блок
      if(magic != 0req.put("m"magic);
      if(StringLen(comment)) req.put("c"comment);
   
      return req;
   }
};

Мы переносим в JSON все свойства (это подойдет для сервиса мониторинга счета), но вы можете оставить только ограниченный набор.

Для свойств, которые являются перечислениями, мы предусмотрели 2 способа представления в JSON: как целое число и как строковое имя элемента перечисления. Выбор способа производится с помощью входного параметра VerboseJson (в идеале, он должен прописываться в коде структур не напрямую, а через параметр конструктора).

input bool VerboseJson = false;

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

Во входных параметрах также указывается адрес сервера, роль программы и реквизиты подключения — раздельно для поставщика и подписчика.

enum TRADE_ROLE
{
   TRADE_PUBLISHER,  // Trade Publisher
   TRADE_SUBSCRIBER  // Trade Subscriber
};
   
input string Server = "ws://localhost:9000/";
input TRADE_ROLE Role = TRADE_PUBLISHER;
input bool VerboseJson = false;
input group "Publisher";
input string PublisherID = "PUB_ID_001";
input string PublisherPrivateKey = "PUB_KEY_FFF";
input string SymbolFilter = ""// SymbolFilter (empty - current, '*' - any)
input ulong MagicFilter = 0;    // MagicFilter (0 - any)
input group "Subscriber";
input string SubscriberID = "SUB_ID_100";
input string SubscribeToPublisherID = "PUB_ID_001";
input string SubscriberAccessKey = "fd3f7a105eae8c2d9afce0a7a4e11bf267a40f04b7c216dd01cf78c7165a2a5a";
input string SymbolSubstitute = "EURUSD=GBPUSD"// SymbolSubstitute (<from>=<to>,...)
input ulong SubscriberMagic = 0;

Параметры SymbolFilter и MagicFilter в группе поставщика позволяют ограничить отслеживаемую торговую активность заданным символом и магическим номером. Пустое значение в SymbolFilter означает контроль только текущего символа графика, для перехвата любых сделок введите символ '*'. В поставщике сигнала для этой цели будет применяться функция FilterMatched, принимающая символ и "магик" сделки.

bool FilterMatched(const string sconst ulong m)
{
   if(MagicFilter != 0 && MagicFilter != m)
   {
      return false;
   }
   
   if(StringLen(SymbolFilter) == 0)
   {
      if(s != _Symbol)
      {
         return false;
      }
   }
   else if(SymbolFilter != s && SymbolFilter != "*")
   {
      return false;
   }
   
   return true;
}

Параметр SymbolSubstitute во входной группе подписчика позволяет подменить приходящий в сообщениях символ на другой, по которому и будет вестись копирующая торговля. Эта возможность пригодится, если названия тикеров одного и того же финансового инструмента отличаются у брокеров. Но данный параметр выполняет также и функцию разрешающего фильтра для повторения сигналов: только указанные здесь символы будут торговаться. Например, чтобы разрешить торговлю сигналом по символу EURUSD (даже без подмены тикеров), нужно задать в параметре строку "EURUSD=EURUSD". Слева от знака '=' указывается символ из сообщений сигнала, справа — символ для торговли.

Список подстановки символов обрабатывается функцией FillSubstitutes во время инициализации и затем используется для подстановки и разрешения торговли функцией FindSubstitute.

string Substitutes[][2];
   
void FillSubstitutes()
{
   string list[];
   const int n = StringSplit(SymbolSubstitute, ',', list);
   ArrayResize(Substitutesn);
   for(int i = 0i < n; ++i)
   {
      string pair[];
      if(StringSplit(list[i], '=', pair) == 2)
      {
         Substitutes[i][0] = pair[0];
         Substitutes[i][1] = pair[1];
      }
      else
      {
         Print("Wrong substitute: "list[i]);
      }
   }
}
   
string FindSubstitute(const string s)
{
   for(int i = 0i < ArrayRange(Substitutes0); ++i)
   {
      if(Substitutes[i][0] == sreturn Substitutes[i][1];
   }
   return NULL;
}

Для общения с сервисом определен производный класс от WebSocketClient. Он нужен, в первую очередь, для запуска торговли по сигналу — по приходу сообщения в обработчик onMessage. Мы вернемся к этому вопросу чуть позже, после того как рассмотрим формирование и отправку сигналов на стороне поставщика.

class MyWebSocketpublic WebSocketClient<Hybi>
{
public:
   MyWebSocket(const string address): WebSocketClient(address) { }
   
   void onMessage(IWebSocketMessage *msgoverride
   {
      ...
   }
};
   
MyWebSocket wss(Server);

Инициализация в OnInit включает запуск таймера (для периодического вызова wss.checkMessages(false)) и подготовку кастом-заголовков с реквизитами пользователя, в зависимости от выбранной роли. Затем открываем соединение с помощью вызова wss.open(custom).

int OnInit()
{
   FillSubstitutes();
   EventSetTimer(1);
   wss.setTimeOut(1000);
   Print("Opening...");
   string custom;
   if(Role == TRADE_PUBLISHER)
   {
      custom = "Sec-Websocket-Protocol: X-MQL5-publisher-"
         + PublisherID + "-" + PublisherPrivateKey + "\r\n";
   }
   else
   {
      custom = "Sec-Websocket-Protocol: X-MQL5-subscriber-"
         + SubscriberID + "-" + SubscribeToPublisherID
         + "-" + SubscriberAccessKey + "\r\n";
   }
   return wss.open(custom) ? INIT_SUCCEEDED : INIT_FAILED;
}

Механизм копирования, то есть перехвата сделок и отправки информации о них на веб-сервис, запускается в обработчике OnTradeTransaction. Как мы знаем, это не единственный способ — можно было бы анализировать "слепок" состояния счета в OnTrade.

void OnTradeTransaction(const MqlTradeTransaction &transaction,
   const MqlTradeRequest &request,
   const MqlTradeResult &result)
{
   if(transaction.type == TRADE_TRANSACTION_REQUEST)
   {
      Print(TU::StringOf(request));
      Print(TU::StringOf(result));
      if(result.retcode == TRADE_RETCODE_PLACED           // успешное действие
         || result.retcode == TRADE_RETCODE_DONE
         || result.retcode == TRADE_RETCODE_DONE_PARTIAL)
      {
         if(FilterMatched(request.symbolrequest.magic))
         {
            ... // см. следующий блок кода
         }
      }
   }
}

Мы отслеживаем события об успешно выполненных торговых запросах, которые удовлетворяют условиям заданных фильтров. Далее структуры запроса, результата запроса и сделки превращаются в объекты JSON. Все они помещаются в один общий контейнер msg под названиями "req", "res" и "deal", соответственно. Напомним, что сам контейнер попадёт в сообщение веб-сервиса как свойство "msg".

            // контейнер для вложения в сообщение сервиса будет виден как свойство "msg":

              // {"origin" : "this_publisher_id", "msg" : { наши данные здесь }}
            JsValue msg;
            MqlTradeRequestWeb req(request);
            msg.put("req"req.asJsValue());
            
            MqlTradeResultWeb res(result);
            msg.put("res"res.asJsValue());
            
            if(result.deal != 0)
            {
               DealMonitorWeb deal(result.deal);
               msg.put("deal"deal.asJsValue());
            }
            ulong tickets[];
            Positions.select(tickets);
            JsValue pos;
            pos.put("n"ArraySize(tickets));
            msg.put("pos", &pos);
            string buffer;
            msg.stringify(buffer);
            
            Print(buffer);
            
            wss.send(buffer);

После заполнения контейнер выводится в виде строки в buffer, печатается в журнал и отправляется на сервер.

По идее, мы можем добавлять в этот контейнер другую информацию: статус счета (просадка, загрузка), количество и свойства отложенных ордеров и так далее. Так, просто для демонстрации возможностей по расширению содержимого сообщений, мы выше добавили количество открытых позиций. Для отбора позиций согласно фильтрам мы использовали объект класса PositionFilter (PositionFilter.mqh):

PositionFilter Positions;
   
int OnInit()
{
   ...
   if(MagicFilterPositions.let(POSITION_MAGICMagicFilter);
   if(SymbolFilter == ""Positions.let(POSITION_SYMBOL_Symbol);
   else if(SymbolFilter != "*"Positions.let(POSITION_SYMBOLSymbolFilter);
   ...
}

В принципе, для повышения надежности "копировщикам" имеет смысл анализировать состояние позиций, а не просто перехватывать сделки.

На этом рассмотрение той части эксперта, которая задействована в роли поставщика сигналов, завершено.

В роли подписчика, как мы уже анонсировали, эксперт получает сообщения в методе MyWebSocket::onMessage. Здесь входящее сообщение разбирается с помощью JsParser::jsonify, и тот контейнер, который был сформирован передающей стороной, извлекается из свойства obj["msg"].

class MyWebSocketpublic WebSocketClient<Hybi>
{
public:
   void onMessage(IWebSocketMessage *msgoverride
   {
      Alert(msg.getString());
      JsValue *obj = JsParser::jsonify(msg.getString());
      if(obj && obj["msg"])
      {
         obj["msg"].print();
         if(!RemoteTrade(obj["msg"])) { /* обработка ошибок */ }
         delete obj;
      }
      delete msg;
   }
};

Непосредственно анализом сигнала и торговыми операциями занимается функция RemoteTrade. Здесь она приводится с сокращениями, без обработки потенциальных ошибок. В функции обеспечена поддержка обоих способов представления перечислений: как целочисленных значений или как строковых названий элементов. Входящий JSON-объект "исследуется" на наличие необходимых свойств (команд и атрибутов сигнала) путем применения оператора [], в том числе последовательно по несколько раз (для доступа во вложенные JSON-объекты).

bool RemoteTrade(JsValue *obj)
{
   bool success = false;
   
   if(obj["req"]["a"] == TRADE_ACTION_DEAL
      || obj["req"]["a"] == "TRADE_ACTION_DEAL")
   {
      const string symbol = FindSubstitute(obj["req"]["s"].s);
      if(symbol == NULL)
      {
         Print("Suitable symbol not found for "obj["req"]["s"].s);
         return false// не найден или запрещен
      }
      
      JsValue *pType = obj["req"]["t"];
      if(pType == ORDER_TYPE_BUY || pType == ORDER_TYPE_SELL
         || pType == "ORDER_TYPE_BUY" || pType == "ORDER_TYPE_SELL")
      {
         ENUM_ORDER_TYPE type;
         if(pType.detect() >= JS_STRING)
         {
            if(pType == "ORDER_TYPE_BUY"type = ORDER_TYPE_BUY;
            else type = ORDER_TYPE_SELL;
         }
         else
         {
            type = obj["req"]["t"].get<ENUM_ORDER_TYPE>();
         }
         
         MqlTradeRequestSync request;
         request.deviation = 10;
         request.magic = SubscriberMagic;
         request.type = type;
         
         const double lot = obj["req"]["v"].get<double>();
         JsValue *pDir = obj["deal"]["entry"];
         if(pDir == DEAL_ENTRY_IN || pDir == "DEAL_ENTRY_IN")
         {
            success = request._market(symbollot) && request.completed();
            Alert(StringFormat("Trade by subscription: market entry %s %s %s - %s",
               EnumToString(type), TU::StringOf(lot), symbol,
               success ? "Successful" : "Failed"));
         }
         else if(pDir == DEAL_ENTRY_OUT || pDir == "DEAL_ENTRY_OUT")
         {
            // действие закрытия предполагает наличие подходящей позиции, ищем её
            PositionFilter filter;
            int props[] = {POSITION_TICKETPOSITION_TYPEPOSITION_VOLUME};
            Tuple3<long,long,doublevalues[];
            filter.let(POSITION_SYMBOLsymbol).let(POSITION_MAGIC,
               SubscriberMagic).select(propsvalues);
            for(int i = 0i < ArraySize(values); ++i)
            {
               // нужна позиция, противоположная по направлению сделке
               if(!TU::IsSameType((ENUM_ORDER_TYPE)values[i]._2type))
               {
                  // нужен достаточный объем (здесь точно равный!)
                  if(TU::Equal(values[i]._3lot))
                  {
                     success = request.close(values[i]._1lot) && request.completed();
                     Alert(StringFormat("Trade by subscription: market exit %s %s %s - %s",
                        EnumToString(type), TU::StringOf(lot), symbol,
                        success ? "Successful" : "Failed"));
                  }
               }
            }
            
            if(!success)
            {
               Print("No suitable position to close");
            }
         }
      }
   }
   return success;
}

В данной реализации не анализируется цена сделки, возможные ограничения на лот, стоп-уровни и прочие нюансы. Мы просто повторяем торговлю по текущей локальной цене. Также при закрытии позиции делается проверка на точное равенство объема, что подходит для счетов с хеджинговым учетом, но не для неттинга, где возможно частичное закрытие, если объем сделки меньше позиции (а может быть и больше, в случае переворота, но вариант DEAL_ENTRY_INOUT здесь не поддержан). Все эти моменты следует доработать для реального применения.

Давайте запустим сервер node.exe wspubsub.js и две копии эксперта wstradecopier.mq5 на разных графиках, в одном и том же терминале. Обычный сценарий предполагает, что эксперт нужно запустить на разных счетах, но для проверки работоспособности подойдет и "парадоксальный" вариант: будем копировать сигналы от одного символа на другой.

В одной копии эксперта оставим настройки по умолчанию, с ролью публикатора. Его следует разместить на графике EURUSD. Во второй копии, на графике GBPUSD, сменим роль на подписчика. Строка "EURUSD=GBPUSD" во входном параметре SymbolSubstitute как раз разрешает торговлю GBPUSD по сигналам о EURUSD.

В журнал будут выведены строки о подключении, с HTTP-заголовками и приветствиями, которые мы уже видели, и потому опустим их.

Совершим покупку EURUSD и убедимся, что она "продублировалась" в том же объеме по GBPUSD.

Далее приведены фрагменты журнала (имейте в виду, что из-за того, что оба эксперта работают в одной копии терминала, сообщения о транзакциях будут поступать в оба чарта, в связи с чем для облегчения анализа журнала можно попеременно устанавливать фильтры "EURUSD" и "GBPUSD"):

(EURUSD,H1) TRADE_ACTION_DEAL, EURUSD, ORDER_TYPE_BUY, V=0.01, ORDER_FILLING_FOK, @ 0.99886, #=1461313378

(EURUSD,H1) DONE, D=1439023682, #=1461313378, V=0.01, @ 0.99886, Bid=0.99886, Ask=0.99886, Req=2

(EURUSD,H1) {"req" : {"a" : "TRADE_ACTION_DEAL", "s" : "EURUSD", "t" : "ORDER_TYPE_BUY", "v" : 0.01,

»   "f" : "ORDER_FILLING_FOK", "p" : 0.99886, "o" : 1461313378}, "res" : {"code" : 10009, "d" : 1439023682,

»   "o" : 1461313378, "v" : 0.01, "p" : 0.99886, "b" : 0.99886, "a" : 0.99886}, "deal" : {"d" : 1439023682,

»   "o" : 1461313378, "t" : "2022.09.19 16:45:50", "tmsc" : 1663605950086, "type" : "DEAL_TYPE_BUY",

»   "entry" : "DEAL_ENTRY_IN", "pid" : 1461313378, "r" : "DEAL_REASON_CLIENT", "v" : 0.01, "p" : 0.99886,

»   "s" : "EURUSD"}, "pos" : {"n" : 1}}

 

Здесь показано содержание выполненного запроса и его результат, а также буфер с JSON-строкой, отправленной на сервер.

Почти моментально на приемной стороне, на графике GBPUSD, выводится алерт с сообщением от сервера: в "сыром" виде и отформатированный после успешного парсинга в JsParser. В "сыром" виде сохранено свойство "origin", в котором сервер дает нам знать, кто является источником сигнала.

(GBPUSD,H1) Alert: {"origin":"publisher PUB_ID_001", "msg":{"req" : {"a" : "TRADE_ACTION_DEAL",

»   "s" : "EURUSD", "t" : "ORDER_TYPE_BUY", "v" : 0.01, "f" : "ORDER_FILLING_FOK", "p" : 0.99886,

»   "o" : 1461313378}, "res" : {"code" : 10009, "d" : 1439023682, "o" : 1461313378, "v" : 0.01,

»   "p" : 0.99886, "b" : 0.99886, "a" : 0.99886}, "deal" : {"d" : 1439023682, "o" : 1461313378,

»   "t" : "2022.09.19 16:45:50", "tmsc" : 1663605950086, "type" : "DEAL_TYPE_BUY",

»   "entry" : "DEAL_ENTRY_IN", "pid" : 1461313378, "r" : "DEAL_REASON_CLIENT", "v" : 0.01,

»   "p" : 0.99886, "s" : "EURUSD"}, "pos" : {"n" : 1}}}

(GBPUSD,H1)        {

(GBPUSD,H1)          req = 

(GBPUSD,H1)          {

(GBPUSD,H1)            a = TRADE_ACTION_DEAL

(GBPUSD,H1)            s = EURUSD

(GBPUSD,H1)            t = ORDER_TYPE_BUY

(GBPUSD,H1)            v =  0.01

(GBPUSD,H1)            f = ORDER_FILLING_FOK

(GBPUSD,H1)            p =  0.99886

(GBPUSD,H1)            o =  1461313378

(GBPUSD,H1)          }

(GBPUSD,H1)          res = 

(GBPUSD,H1)          {

(GBPUSD,H1)            code =  10009

(GBPUSD,H1)            d =  1439023682

(GBPUSD,H1)            o =  1461313378

(GBPUSD,H1)            v =  0.01

(GBPUSD,H1)            p =  0.99886

(GBPUSD,H1)            b =  0.99886

(GBPUSD,H1)            a =  0.99886

(GBPUSD,H1)          }

(GBPUSD,H1)          deal = 

(GBPUSD,H1)          {

(GBPUSD,H1)            d =  1439023682

(GBPUSD,H1)            o =  1461313378

(GBPUSD,H1)            t = 2022.09.19 16:45:50

(GBPUSD,H1)            tmsc =  1663605950086

(GBPUSD,H1)            type = DEAL_TYPE_BUY

(GBPUSD,H1)            entry = DEAL_ENTRY_IN

(GBPUSD,H1)            pid =  1461313378

(GBPUSD,H1)            r = DEAL_REASON_CLIENT

(GBPUSD,H1)            v =  0.01

(GBPUSD,H1)            p =  0.99886

(GBPUSD,H1)            s = EURUSD

(GBPUSD,H1)          }

(GBPUSD,H1)          pos = 

(GBPUSD,H1)          {

(GBPUSD,H1)            n =  1

(GBPUSD,H1)          }

(GBPUSD,H1)        }

(GBPUSD,H1)        Alert: Trade by subscription: market entry ORDER_TYPE_BUY 0.01 GBPUSD - Successful

Последняя из приведенных записей сигнализирует об успешно выполненной сделке по GBPUSD. На торговой закладке счета должны отображаться 2 позиции.

Спустя некоторое время закроем позицию EURUSD — позиция GBPUSD должна закрыться автоматически.

(EURUSD,H1) TRADE_ACTION_DEAL, EURUSD, ORDER_TYPE_SELL, V=0.01, ORDER_FILLING_FOK, @ 0.99881, #=1461315206, P=1461313378

(EURUSD,H1) DONE, D=1439025490, #=1461315206, V=0.01, @ 0.99881, Bid=0.99881, Ask=0.99881, Req=4

(EURUSD,H1) {"req" : {"a" : "TRADE_ACTION_DEAL", "s" : "EURUSD", "t" : "ORDER_TYPE_SELL", "v" : 0.01,

»   "f" : "ORDER_FILLING_FOK", "p" : 0.99881, "o" : 1461315206, "q" : 1461313378}, "res" : {"code" : 10009,

»   "d" : 1439025490, "o" : 1461315206, "v" : 0.01, "p" : 0.99881, "b" : 0.99881, "a" : 0.99881},

»   "deal" : {"d" : 1439025490, "o" : 1461315206, "t" : "2022.09.19 16:46:52", "tmsc" : 1663606012990,

»   "type" : "DEAL_TYPE_SELL", "entry" : "DEAL_ENTRY_OUT", "pid" : 1461313378, "r" : "DEAL_REASON_CLIENT",

»   "v" : 0.01, "p" : 0.99881, "m" : -0.05, "s" : "EURUSD"}, "pos" : {"n" : 0}}

Если в первый раз сделка имела тип DEAL_ENTRY_IN, то теперь — DEAL_ENTRY_OUT. Алерт подтверждает прием сообщения и успешное закрытие дублирующей позиции.

(GBPUSD,H1) Alert: {"origin":"publisher PUB_ID_001", "msg":{"req" : {"a" : "TRADE_ACTION_DEAL",

»   "s" : "EURUSD", "t" : "ORDER_TYPE_SELL", "v" : 0.01, "f" : "ORDER_FILLING_FOK", "p" : 0.99881,

»   "o" : 1461315206, "q" : 1461313378}, "res" : {"code" : 10009, "d" : 1439025490, "o" : 1461315206,

»   "v" : 0.01, "p" : 0.99881, "b" : 0.99881, "a" : 0.99881}, "deal" : {"d" : 1439025490,

»   "o" : 1461315206, "t" : "2022.09.19 16:46:52", "tmsc" : 1663606012990, "type" : "DEAL_TYPE_SELL",

»   "entry" : "DEAL_ENTRY_OUT", "pid" : 1461313378, "r" : "DEAL_REASON_CLIENT", "v" : 0.01,

»   "p" : 0.99881, "m" : -0.05, "s" : "EURUSD"}, "pos" : {"n" : 0}}}

...

(GBPUSD,H1)        Alert: Trade by subscription: market exit ORDER_TYPE_SELL 0.01 GBPUSD - Successful

Напоследок создадим рядом с экспертом wstradecopier.mq5 файл проекта wstradecopier.mqproj, чтобы добавить в него описание и необходимые серверные файлы (в прежнем каталоге MQL5/Experts/p7/MQL5Book/Web/).

Подведем итог: мы организовали технически расширяемую, многопользовательскую систему для обмена торговой информацией через сокет-сервер. В силу технических особенностей веб-сокетов (постоянное открытое соединение), данная реализация сервиса сигналов больше подходит для краткосрочной и высокочастотной торговли, а также для контроля арбитражных ситуаций в котировках.

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

Правда, в случае MetaEditor, при разработке проекта в папке MQL5/Shared Projects следует учитывать, что заголовочные файлы из стандартного каталога MQL5/Include не попадают в общее хранилище. С другой стороны, создание выделенной папки Include внутри вашего проекта и перенос в неё необходимых стандартных mqh-файлов приведет к дублированию информации и потенциальным расхождениям в версиях заголовочных файлов. Это поведение, вероятно, будет совершенствоваться в MetaEditor.

Еще одним моментом для проектов, выходящих на уровень публичного, сетевого сервиса, является необходимость администрирования пользователей и их авторизация. В нашем последнем примере этот вопрос был только обозначен, но не прорабатывался. Однако сайт mql5.com предоставляет готовое решение на базе широко известного протокола OAuth. Ознакомиться с принципом работы OAuth и настроить его для своего веб-сервиса может каждый, у кого есть учетная запись mql5.com: достаточно найти раздел Приложения (ссылка вида https://www.mql5.com/en/users/<login>/apps) в своем профиле. За счет регистрации веб-сервиса в приложениях mql5.com, вы получите возможность авторизовывать пользователей через сайт mql5.com.