- Общие принципы работы с локальными проектами
- План проекта веб-сервиса копирования сделок и сигналов
- Веб-сервер на основе nodejs
- Теоретические основы протокола WebSockets
- Серверная часть веб-сервисов на базе WebSocket-протокола
- Протокол WebSocket-ов на MQL5
- Клиентские программы эхо и чат-сервисов на MQL5
- Сервис торговых сигналов и тестовая веб-страница
- Клиентская программа сигнального сервиса на MQL5
Клиентская программа сигнального сервиса на MQL5
Итак, мы решили, что текст в сообщениях сервиса будет в формате JSON.
В наиболее распространенном варианте, JSON представляет собой текстовое описание объекта, похожее на то, как это делается для структур в MQL5. Объект заключен в фигурные скобки, внутри которых через запятую пишутся его свойства: каждое свойство имеет идентификатор в кавычках, после чего идет двоеточие и значение свойства. Поддерживаются свойства нескольких примитивных типов: строки, целые и вещественные числа, логические true/false и пустое значение null. Кроме того, значением свойства может быть, в свою очередь, объект или массив. Массивы описываются с помощью квадратных скобок, внутри которых элементы перечисляются через запятую. Например,
{
|
В принципе, массив на верхнем уровне также является валидным JSON. Например,
[
|
Для сокращения трафика в прикладных протоколах, использующих JSON, принято сокращать названия полей до нескольких букв (часто — до одной).
Названия свойств и строковые значения заключаются в двойные кавычки. Если требуется указать кавычку внутри строки, её следует экранировать обратной косой чертой.
Применение JSON делает протокол универсальным и расширяемым. Например, для проектируемого сервиса (торговых сигналов и, в более общем случае, копирования состояния счета) можно чисто теоретически предположить следующую структуру сообщения:
{
|
Какие-то из этих возможностей могут поддерживать, а могут не поддерживать конкретные реализации клиентских программ (всё, что им не "понятно", они просто проигнорируют). Кроме того, при соблюдении условия отсутствия конфликтов в названиях свойств на одном уровне, каждый поставщик информации может добавлять в 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.mqh — JsParser — позволяет выполнять обратную операцию: превращать текст с описанием 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 MqlTradeRequestWeb: public MqlTradeRequest
|
Мы переносим в JSON все свойства (это подойдет для сервиса мониторинга счета), но вы можете оставить только ограниченный набор.
Для свойств, которые являются перечислениями, мы предусмотрели 2 способа представления в JSON: как целое число и как строковое имя элемента перечисления. Выбор способа производится с помощью входного параметра VerboseJson (в идеале, он должен прописываться в коде структур не напрямую, а через параметр конструктора).
input bool VerboseJson = false; |
Передача только чисел упростила бы кодирование, потому что на приемной стороне достаточно привести их к нужному типу перечисления, чтобы выполнить "зеркальные" действия. Однако числа затрудняют восприятие информации человеком, а ему может потребоваться проанализировать ситуацию (сообщение). Поэтому имеет смысл поддержать опцию для строкового представления, как более "дружественного", хотя оно и требует дополнительных операций в приемном алгоритме.
Во входных параметрах также указывается адрес сервера, роль программы и реквизиты подключения — раздельно для поставщика и подписчика.
enum TRADE_ROLE
|
Параметры SymbolFilter и MagicFilter в группе поставщика позволяют ограничить отслеживаемую торговую активность заданным символом и магическим номером. Пустое значение в SymbolFilter означает контроль только текущего символа графика, для перехвата любых сделок введите символ '*'. В поставщике сигнала для этой цели будет применяться функция FilterMatched, принимающая символ и "магик" сделки.
bool FilterMatched(const string s, const ulong m)
|
Параметр SymbolSubstitute во входной группе подписчика позволяет подменить приходящий в сообщениях символ на другой, по которому и будет вестись копирующая торговля. Эта возможность пригодится, если названия тикеров одного и того же финансового инструмента отличаются у брокеров. Но данный параметр выполняет также и функцию разрешающего фильтра для повторения сигналов: только указанные здесь символы будут торговаться. Например, чтобы разрешить торговлю сигналом по символу EURUSD (даже без подмены тикеров), нужно задать в параметре строку "EURUSD=EURUSD". Слева от знака '=' указывается символ из сообщений сигнала, справа — символ для торговли.
Список подстановки символов обрабатывается функцией FillSubstitutes во время инициализации и затем используется для подстановки и разрешения торговли функцией FindSubstitute.
string Substitutes[][2];
|
Для общения с сервисом определен производный класс от WebSocketClient. Он нужен, в первую очередь, для запуска торговли по сигналу — по приходу сообщения в обработчик onMessage. Мы вернемся к этому вопросу чуть позже, после того как рассмотрим формирование и отправку сигналов на стороне поставщика.
class MyWebSocket: public WebSocketClient<Hybi>
|
Инициализация в OnInit включает запуск таймера (для периодического вызова wss.checkMessages(false)) и подготовку кастом-заголовков с реквизитами пользователя, в зависимости от выбранной роли. Затем открываем соединение с помощью вызова wss.open(custom).
int OnInit()
|
Механизм копирования, то есть перехвата сделок и отправки информации о них на веб-сервис, запускается в обработчике OnTradeTransaction. Как мы знаем, это не единственный способ — можно было бы анализировать "слепок" состояния счета в OnTrade.
void OnTradeTransaction(const MqlTradeTransaction &transaction,
|
Мы отслеживаем события об успешно выполненных торговых запросах, которые удовлетворяют условиям заданных фильтров. Далее структуры запроса, результата запроса и сделки превращаются в объекты JSON. Все они помещаются в один общий контейнер msg под названиями "req", "res" и "deal", соответственно. Напомним, что сам контейнер попадёт в сообщение веб-сервиса как свойство "msg".
// контейнер для вложения в сообщение сервиса будет виден как свойство "msg": // {"origin" : "this_publisher_id", "msg" : { наши данные здесь }}
|
После заполнения контейнер выводится в виде строки в buffer, печатается в журнал и отправляется на сервер.
По идее, мы можем добавлять в этот контейнер другую информацию: статус счета (просадка, загрузка), количество и свойства отложенных ордеров и так далее. Так, просто для демонстрации возможностей по расширению содержимого сообщений, мы выше добавили количество открытых позиций. Для отбора позиций согласно фильтрам мы использовали объект класса PositionFilter (PositionFilter.mqh):
PositionFilter Positions;
|
В принципе, для повышения надежности "копировщикам" имеет смысл анализировать состояние позиций, а не просто перехватывать сделки.
На этом рассмотрение той части эксперта, которая задействована в роли поставщика сигналов, завершено.
В роли подписчика, как мы уже анонсировали, эксперт получает сообщения в методе MyWebSocket::onMessage. Здесь входящее сообщение разбирается с помощью JsParser::jsonify, и тот контейнер, который был сформирован передающей стороной, извлекается из свойства obj["msg"].
class MyWebSocket: public WebSocketClient<Hybi>
|
Непосредственно анализом сигнала и торговыми операциями занимается функция RemoteTrade. Здесь она приводится с сокращениями, без обработки потенциальных ошибок. В функции обеспечена поддержка обоих способов представления перечислений: как целочисленных значений или как строковых названий элементов. Входящий JSON-объект "исследуется" на наличие необходимых свойств (команд и атрибутов сигнала) путем применения оператора [], в том числе последовательно по несколько раз (для доступа во вложенные JSON-объекты).
bool RemoteTrade(JsValue *obj)
|
В данной реализации не анализируется цена сделки, возможные ограничения на лот, стоп-уровни и прочие нюансы. Мы просто повторяем торговлю по текущей локальной цене. Также при закрытии позиции делается проверка на точное равенство объема, что подходит для счетов с хеджинговым учетом, но не для неттинга, где возможно частичное закрытие, если объем сделки меньше позиции (а может быть и больше, в случае переворота, но вариант 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.