English 中文 Español Deutsch 日本語 Português
preview
WebSocket для MetaTrader 5 — Использование Windows API

WebSocket для MetaTrader 5 — Использование Windows API

MetaTrader 5Примеры | 5 апреля 2022, 16:01
2 078 0
Francis Dube
Francis Dube

Введение

В статье WebSocket для MetaTrader 5 мы рассмотрели основы протокола WebSocket и реализовали клиент в MQL5. На этот раз мы используем Windows API, чтобы создать клиент WebSocket для MetaTrader 5-программ. Это второй по удобству вариант – он не предусматривает использования сторонних программ, все нужные компоненты есть в операционной системе. Мы выполним клиент в виде класса и проведем тесты, отправив текущие тиковые данные в MetaTrader 5 с помощью WebSocket API от Binary.com.


WebSocket в Windows

Говоря о Windows API и Интернете, следует отметить, что разработчики MQL5 наиболее знакомы с библиотекой Windows Internet (WinINeT). Она поддерживает межсетевые протоколы, включая File transfer protocol (FTP) и HTTP. Библиотека Windows HTTP Services (WinHTTP) – специальная библиотека для протокола HTTP с функциями для разработки серверных приложений. Некоторые из функций WinHTTP предназначены для обработки WebSocket-соединений.

Протокол WebSocket поддерживается в операционных системах Windows, начиная с Windows 8.1 и Windows Server 2012 R2. У Windows 7 и более ранних версий нативной поддержки протокола нет, поэтому программы, описанные в статье, не будут работать на машинах с этими операционными системами.

Библиотека Winhttp

Для создания клиентского WebSocket-соединения нам понадобятся следующие функции:

WinHttpOpen
Инициализирует библиотеку, подготавливая ее к использованию приложением
WinHttpConnect
Устанавливает доменное имя сервера, с которым необходимо взаимодействовать приложению
WinHttpOpenRequest
Создает хэндл HTTP-запроса
WinHttpSetOption
Устанавливает различные варианты конфигурации для HTTP-соединения
WinHttpSendRequest
Отправляет запрос на сервер
WinHttpReceiveResponse
Получает ответ от сервера после отправки запроса
WinHttpWebSocketCompleteUpgrade
Подтверждает, что ответ от сервера соответствует протоколу WebSocket
WinHttpCloseHandle
Отключает любые дескрипторы ресурсов, использовавшиеся ранее
WinHttpWebSocketSend
Отправляет данные через WebSocket-соединение
WinHttpWebSocketReceive
Получает данные, используя WebSocket-соединение
WinHttpWebSocketClose
Закрывает WebSocket-соединение
WinHttpWebSocketQueryCloseStatus
Проверяет сообщение о статусе закрытия, отправленное с сервера

Все функции библиотеки задокументированы Microsoft. Подробное описание всех функций, их входных параметров и типов возвращаемого значения можно найти по ссылкам выше.

Создаваемый нами клиент для MetaTrader 5 работает в синхронном режиме. Это означает, что вызов функции блокирует исполнение до возврата значения. Например, вызов WinHttpWebSocketReceive() блокирует выполняемый поток до тех пор, пока данные не будут доступны для чтения. Помните об этом, создавая приложения для MetaTrader 5.

Функции winhttp объявляются и импортируются во включаемом файле winhttp.mqh.

#include <WinAPI\errhandlingapi.mqh>



#define WORD  ushort
#define DWORD ulong
#define BYTE  uchar
#define INTERNET_PORT WORD
#define HINTERNET long
#define LPVOID uint&

#define WINHTTP_ERROR_BASE                     12000

#define ERROR_WINHTTP_OUT_OF_HANDLES           (WINHTTP_ERROR_BASE + 1)
#define ERROR_WINHTTP_TIMEOUT                  (WINHTTP_ERROR_BASE + 2)
#define ERROR_WINHTTP_INTERNAL_ERROR           (WINHTTP_ERROR_BASE + 4)
#define ERROR_WINHTTP_INVALID_URL              (WINHTTP_ERROR_BASE + 5)
#define ERROR_WINHTTP_UNRECOGNIZED_SCHEME      (WINHTTP_ERROR_BASE + 6)
#define ERROR_WINHTTP_NAME_NOT_RESOLVED        (WINHTTP_ERROR_BASE + 7)
#define ERROR_WINHTTP_INVALID_OPTION           (WINHTTP_ERROR_BASE + 9)
#define ERROR_WINHTTP_OPTION_NOT_SETTABLE      (WINHTTP_ERROR_BASE + 11)
#define ERROR_WINHTTP_SHUTDOWN                 (WINHTTP_ERROR_BASE + 12)


#define ERROR_WINHTTP_LOGIN_FAILURE            (WINHTTP_ERROR_BASE + 15)
#define ERROR_WINHTTP_OPERATION_CANCELLED      (WINHTTP_ERROR_BASE + 17)
#define ERROR_WINHTTP_INCORRECT_HANDLE_TYPE    (WINHTTP_ERROR_BASE + 18)
#define ERROR_WINHTTP_INCORRECT_HANDLE_STATE   (WINHTTP_ERROR_BASE + 19)
#define ERROR_WINHTTP_CANNOT_CONNECT           (WINHTTP_ERROR_BASE + 29)
#define ERROR_WINHTTP_CONNECTION_ERROR         (WINHTTP_ERROR_BASE + 30)
#define ERROR_WINHTTP_RESEND_REQUEST           (WINHTTP_ERROR_BASE + 32)

#define ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED  (WINHTTP_ERROR_BASE + 44)


#define ERROR_WINHTTP_CANNOT_CALL_BEFORE_OPEN   (WINHTTP_ERROR_BASE + 100)
#define ERROR_WINHTTP_CANNOT_CALL_BEFORE_SEND   (WINHTTP_ERROR_BASE + 101)
#define ERROR_WINHTTP_CANNOT_CALL_AFTER_SEND (WINHTTP_ERROR_BASE + 102)
#define ERROR_WINHTTP_CANNOT_CALL_AFTER_OPEN (WINHTTP_ERROR_BASE + 103)

#define ERROR_WINHTTP_HEADER_NOT_FOUND             (WINHTTP_ERROR_BASE + 150)
#define ERROR_WINHTTP_INVALID_SERVER_RESPONSE      (WINHTTP_ERROR_BASE + 152)
#define ERROR_WINHTTP_INVALID_HEADER               (WINHTTP_ERROR_BASE + 153)
#define ERROR_WINHTTP_INVALID_QUERY_REQUEST        (WINHTTP_ERROR_BASE + 154)
#define ERROR_WINHTTP_HEADER_ALREADY_EXISTS        (WINHTTP_ERROR_BASE + 155)
#define ERROR_WINHTTP_REDIRECT_FAILED              (WINHTTP_ERROR_BASE + 156)


#define ERROR_WINHTTP_AUTO_PROXY_SERVICE_ERROR  (WINHTTP_ERROR_BASE + 178)
#define ERROR_WINHTTP_BAD_AUTO_PROXY_SCRIPT     (WINHTTP_ERROR_BASE + 166)
#define ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT (WINHTTP_ERROR_BASE + 167)
#define ERROR_WINHTTP_UNHANDLED_SCRIPT_TYPE     (WINHTTP_ERROR_BASE + 176)
#define ERROR_WINHTTP_SCRIPT_EXECUTION_ERROR    (WINHTTP_ERROR_BASE + 177)
#define ERROR_WINHTTP_NOT_INITIALIZED          (WINHTTP_ERROR_BASE + 172)
#define ERROR_WINHTTP_SECURE_FAILURE           (WINHTTP_ERROR_BASE + 175)


#define ERROR_WINHTTP_SECURE_CERT_DATE_INVALID    (WINHTTP_ERROR_BASE + 37)
#define ERROR_WINHTTP_SECURE_CERT_CN_INVALID      (WINHTTP_ERROR_BASE + 38)
#define ERROR_WINHTTP_SECURE_INVALID_CA           (WINHTTP_ERROR_BASE + 45)
#define ERROR_WINHTTP_SECURE_CERT_REV_FAILED      (WINHTTP_ERROR_BASE + 57)
#define ERROR_WINHTTP_SECURE_CHANNEL_ERROR        (WINHTTP_ERROR_BASE + 157)
#define ERROR_WINHTTP_SECURE_INVALID_CERT         (WINHTTP_ERROR_BASE + 169)
#define ERROR_WINHTTP_SECURE_CERT_REVOKED         (WINHTTP_ERROR_BASE + 170)
#define ERROR_WINHTTP_SECURE_CERT_WRONG_USAGE     (WINHTTP_ERROR_BASE + 179)


#define ERROR_WINHTTP_AUTODETECTION_FAILED                  (WINHTTP_ERROR_BASE + 180)
#define ERROR_WINHTTP_HEADER_COUNT_EXCEEDED                 (WINHTTP_ERROR_BASE + 181)
#define ERROR_WINHTTP_HEADER_SIZE_OVERFLOW                  (WINHTTP_ERROR_BASE + 182)
#define ERROR_WINHTTP_CHUNKED_ENCODING_HEADER_SIZE_OVERFLOW (WINHTTP_ERROR_BASE + 183)
#define ERROR_WINHTTP_RESPONSE_DRAIN_OVERFLOW               (WINHTTP_ERROR_BASE + 184)
#define ERROR_WINHTTP_CLIENT_CERT_NO_PRIVATE_KEY            (WINHTTP_ERROR_BASE + 185)
#define ERROR_WINHTTP_CLIENT_CERT_NO_ACCESS_PRIVATE_KEY     (WINHTTP_ERROR_BASE + 186)

#define ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED_PROXY         (WINHTTP_ERROR_BASE + 187)
#define ERROR_WINHTTP_SECURE_FAILURE_PROXY                  (WINHTTP_ERROR_BASE + 188)
#define ERROR_WINHTTP_RESERVED_189                          (WINHTTP_ERROR_BASE + 189)
#define ERROR_WINHTTP_HTTP_PROTOCOL_MISMATCH                (WINHTTP_ERROR_BASE + 190)

#define WINHTTP_ERROR_LAST                                  (WINHTTP_ERROR_BASE + 188)

enum WINHTTP_WEB_SOCKET_BUFFER_TYPE
  {
   WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE       = 0,
   WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE      = 1,
   WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE         = 2,
   WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE        = 3,
   WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE                = 4
  };

enum _WINHTTP_WEB_SOCKET_CLOSE_STATUS
  {
   WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS                = 1000,
   WINHTTP_WEB_SOCKET_ENDPOINT_TERMINATED_CLOSE_STATUS    = 1001,
   WINHTTP_WEB_SOCKET_PROTOCOL_ERROR_CLOSE_STATUS         = 1002,
   WINHTTP_WEB_SOCKET_INVALID_DATA_TYPE_CLOSE_STATUS      = 1003,
   WINHTTP_WEB_SOCKET_EMPTY_CLOSE_STATUS                  = 1005,
   WINHTTP_WEB_SOCKET_ABORTED_CLOSE_STATUS                = 1006,
   WINHTTP_WEB_SOCKET_INVALID_PAYLOAD_CLOSE_STATUS        = 1007,
   WINHTTP_WEB_SOCKET_POLICY_VIOLATION_CLOSE_STATUS       = 1008,
   WINHTTP_WEB_SOCKET_MESSAGE_TOO_BIG_CLOSE_STATUS        = 1009,
   WINHTTP_WEB_SOCKET_UNSUPPORTED_EXTENSIONS_CLOSE_STATUS = 1010,
   WINHTTP_WEB_SOCKET_SERVER_ERROR_CLOSE_STATUS           = 1011,
   WINHTTP_WEB_SOCKET_SECURE_HANDSHAKE_ERROR_CLOSE_STATUS = 1015
  };

#define WINHTTP_WEB_SOCKET_MAX_CLOSE_REASON_LENGTH 123
#define WINHTTP_FLAG_SECURE                0x00800000

#define WINHTTP_ACCESS_TYPE_DEFAULT_PROXY               0

#define WINHTTP_OPTION_SECURITY_FLAGS                   31
#define WINHTTP_OPTION_SECURE_PROTOCOLS                 84
#define WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET            114
#define WINHTTP_OPTION_WEB_SOCKET_CLOSE_TIMEOUT         115
#define WINHTTP_OPTION_WEB_SOCKET_KEEPALIVE_INTERVAL    116
#define WINHTTP_OPTION_WEB_SOCKET_RECEIVE_BUFFER_SIZE   122
#define WINHTTP_OPTION_WEB_SOCKET_SEND_BUFFER_SIZE      123


#define SECURITY_FLAG_IGNORE_UNKNOWN_CA         0x00000100
#define SECURITY_FLAG_IGNORE_CERT_DATE_INVALID  0x00002000
#define SECURITY_FLAG_IGNORE_CERT_CN_INVALID    0x00001000
#define SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE   0x00000200


#define ERROR_INVALID_PARAMETER          87L
#define ERROR_INVALID_OPERATION          4317L

#import "winhttp.dll"
HINTERNET WinHttpOpen(string,DWORD,string,string,DWORD);
HINTERNET WinHttpConnect(HINTERNET,string,INTERNET_PORT,DWORD);
HINTERNET WinHttpOpenRequest(HINTERNET,string,string,string,string,string,DWORD);
bool WinHttpSetOption(HINTERNET,DWORD,LPVOID[],DWORD);
bool WinHttpQueryOption(HINTERNET,DWORD,LPVOID[],DWORD&);
bool WinHttpSetTimeouts(HINTERNET,int,int,int,int);
HINTERNET WinHttpSendRequest(HINTERNET,string,DWORD,LPVOID[],DWORD,DWORD,DWORD);
bool WinHttpReceiveResponse(HINTERNET,LPVOID[]);
HINTERNET WinHttpWebSocketCompleteUpgrade(HINTERNET,DWORD&);
bool WinHttpCloseHandle(HINTERNET);
DWORD WinHttpWebSocketSend(HINTERNET,WINHTTP_WEB_SOCKET_BUFFER_TYPE,BYTE&[],DWORD);
DWORD WinHttpWebSocketReceive(HINTERNET,BYTE&[],DWORD,DWORD&,WINHTTP_WEB_SOCKET_BUFFER_TYPE&);
DWORD WinHttpWebSocketClose(HINTERNET,ushort,BYTE&[],DWORD);
DWORD WinHttpWebSocketQueryCloseStatus(HINTERNET,ushort&,BYTE&[],DWORD,DWORD&);
#import
//+------------------------------------------------------------------+


Использование функций winhttp

Для установки клиента WebSocket с необходимыми функциями нам в первую очередь необходимо вызывать WinHttpOpen(), чтобы инициализировать библиотеку. Функция возвращает хэндл сессии для последующего вызова других функций winhttp.

#include<winhttp.mqh>

HINTERNET sessionhandle,connectionhandle,requesthandle,websockethandle;

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   sessionhandle=connectionhandle=requesthandle=websockethandle=NULL;

   sessionhandle=WinHttpOpen("MT5 app",WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,NULL,NULL,0);

   if(sessionhandle==NULL)
     {
      Print("WinHttpOpen error" +string(kernel32::GetLastError()));
      return;
     }

Второй шаг – создание хэндла соединения с помощью WinHttpConnect(). Здесь мы указываем адрес сервера и номер порта. Обратите внимание, что на этом этапе для сервера требуется указать доменное имя без схемы и пути. Также можно использовать публичный IP-адрес, если он известен. Большая часть ошибок, возникающих при работе с winhttp, связаны с передачей неправильно отформатированного адреса сервера. Например, если полный адрес сервера выглядит как wss://ws.example.com/path, WinHttpConnect() ожидает только ws.example.com.

connectionhandle=WinHttpConnect(sessionhandle,server,Port,0);

   if(connectionhandle==NULL)
     {
      Print("WinHttpConnect error "+string(kernel32::GetLastError()));

      if(sessionhandle!=NULL)
         WinHttpCloseHandle(sessionhandle);

      return;
     }

После успешного создания хэндла соединения мы используем его для установки хэндла запроса путем вызова WinHttpOpenRequest(). Здесь мы указываем путь (если есть) от адреса сервера, а также устанавливаем возможность сделать соединение защищенным.

requesthandle=WinHttpOpenRequest(connectionhandle,"GET",path,NULL,NULL,NULL,(ExtTLS)?WINHTTP_FLAG_SECURE:0);

   if(requesthandle==NULL)
     {
      Print("WinHttpOpenRequest error "+string(kernel32::GetLastError()));


      if(connectionhandle!=NULL)
         WinHttpCloseHandle(connectionhandle);

      if(sessionhandle!=NULL)
         WinHttpCloseHandle(sessionhandle);

      return;
     }

Теперь, когда у нас есть правильный хэндл запроса, необходимо приготовиться к рукопожатию путем вызова WinHttpSetOption().

uint nullpointer[]= {};
   if(!WinHttpSetOption(requesthandle,WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,nullpointer,0))
     {
      Print("WinHttpSetOption upgrade error "+string(kernel32::GetLastError()));
      if(requesthandle!=NULL)
         WinHttpCloseHandle(requesthandle);

      if(connectionhandle!=NULL)
         WinHttpCloseHandle(connectionhandle);

      if(sessionhandle!=NULL)
         WinHttpCloseHandle(sessionhandle);

      return;
     }

Это позволяет добавить необходимые заголовки к http-запросу в соответствии с протоколом WebSocket. Рукопожатие инициируется вызовом WinHttpSendRequest(), а затем и WinHttpReceiveResponse() для подтверждения того, что ответ на наш запрос получен.

if(!WinHttpSendRequest(requesthandle,NULL,0,nullpointer,0,0,0))
     {
      Print("WinHttpSendRequest error "+string(kernel32::GetLastError()));
      if(requesthandle!=NULL)
         WinHttpCloseHandle(requesthandle);

      if(connectionhandle!=NULL)
         WinHttpCloseHandle(connectionhandle);

      if(sessionhandle!=NULL)
         WinHttpCloseHandle(sessionhandle);

      return;
     }



   if(!WinHttpReceiveResponse(requesthandle,nullpointer))
     {
      Print("WinHttpRecieveResponse no response "+string(kernel32::GetLastError()));
      if(requesthandle!=NULL)
         WinHttpCloseHandle(requesthandle);

      if(connectionhandle!=NULL)
         WinHttpCloseHandle(connectionhandle);

      if(sessionhandle!=NULL)
         WinHttpCloseHandle(sessionhandle);

      return;
     }

WinHttpWebSocketCompleteUpgrade() проверяет ответ на соответствие протоколу WebSocket. Если всё в порядке, функция возвращает необходимый хэндл WebSocket.

ulong nv=0;
   websockethandle=WinHttpWebSocketCompleteUpgrade(requesthandle,nv);
   if(websockethandle==NULL)
     {
      Print("WinHttpWebSocketCompleteUpgrade error "+string(kernel32::GetLastError()));
      if(requesthandle!=NULL)
         WinHttpCloseHandle(requesthandle);

      if(connectionhandle!=NULL)
         WinHttpCloseHandle(connectionhandle);

      if(sessionhandle!=NULL)
         WinHttpCloseHandle(sessionhandle);

      return;
     }

   WinHttpCloseHandle(requesthandle);
   requesthandle=NULL;

Наш WebSocket-клиент готов к работе. Мы можем отправлять данные с помощью WinHttpWebSocketSend() и получать их с помощью WinHttpWebSocketReceive(). Хэндл запроса больше не нужен, так как хэндл WebSocket уже создан и наше http-соединение обновлено до WebSocket-соединения. Мы можем освободить все ресурсы, связанные с хэндлом запроса, вызвав WinHttpCloseHandle().

bool WebsocketSend(const string message)
  {
   BYTE msg_array[];

   StringToCharArray(message,msg_array,0,WHOLE_ARRAY);

   ArrayRemove(msg_array,ArraySize(msg_array)-1,1);

   DWORD len=(ArraySize(msg_array));

   ulong send=WinHttpWebSocketSend(websockethandle,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE,msg_array,len);

   if(send)
      return(false);


   return(true);
  }
//+------------------------------------------------------------------+
bool WebSocketRecv(uchar &rxbuffer[],ulong &bytes_read)
  {
   WINHTTP_WEB_SOCKET_BUFFER_TYPE rbuffertype=-1;

   BYTE rbuffer[65539];

   ulong rbuffersize=ulong(ArraySize(rbuffer));

   ulong done=0;
   ulong transferred=0;
   ZeroMemory(rxbuffer);
   ZeroMemory(rbuffer);
   bytes_read=0;
   int called=0;

   do
     {
      called++;
      ulong get=WinHttpWebSocketReceive(websockethandle,rbuffer,rbuffersize,transferred,rbuffertype);
      if(get)
        {
         return(false);
        }

      ArrayCopy(rxbuffer,rbuffer,(int)done,0,(int)transferred);

      done+=transferred;

      transferred=0;

      ZeroMemory(rbuffer);

     }
   while(rbuffertype==WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE || rbuffertype==WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE);

   Print("Buffer type is "+EnumToString(rbuffertype)+" bytes read "+IntegerToString(done)+" looped "+IntegerToString(called));

   bytes_read=done;

   return(true);

  }

//+------------------------------------------------------------------+

Вызов WinHttpWebSocketClose() закрывает WebSocket-соединение. После закрытия соединения все связанные с ним хэндлы должны быть деинициализированы путем вызова
WinHttpCloseHandle() для каждого из них.

BYTE closearray[]= {};

   ulong close=WinHttpWebSocketClose(websockethandle,WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS,closearray,0);
   if(close)
     {
      Print("websocket close error "+string(kernel32::GetLastError()));
      if(requesthandle!=NULL)
         WinHttpCloseHandle(requesthandle);

      if(websockethandle!=NULL)
         WinHttpCloseHandle(websockethandle);

      if(connectionhandle!=NULL)
         WinHttpCloseHandle(connectionhandle);

      if(sessionhandle!=NULL)
         WinHttpCloseHandle(sessionhandle);

      return;
     }

Класс CWebsocket

Файл websocket.mqh будет содержать класс CWebsocket, который будет служить оберткой для функций библиотеки winhttp, необходимых для активации клиента WebSocket.
Файл начинается с директивы include для включения всех функций и объявлений, импортированных из библиотек Windows API.

#include<winhttp.mqh>

#define WEBSOCKET_ERROR_FIRST              WINHTTP_ERROR_LAST+1000
#define WEBSOCKET_ERROR_NOT_INITIALIZED    WEBSOCKET_ERROR_FIRST+1
#define WEBSOCKET_ERROR_EMPTY_SEND_BUFFER  WEBSOCKET_ERROR_FIRST+2
#define WEBSOCKET_ERROR_NOT_CONNECTED      WEBSOCKET_ERROR_FIRST+3
//+------------------------------------------------------------------+
//| websocket state enumeration                                      |
//+------------------------------------------------------------------+

enum ENUM_WEBSOCKET_STATE
  {
   CLOSED = 0,
   CLOSING,
   CONNECTING,
   CONNECTED
  };

Первым для начала соединения с WebSocket-сервером вызывается метод Connect().

Параметры Connect():

  • _serveraddress — полный адрес сервера (type:string)
  • _port — номер порта сервера (type:ushort)
  • _appname — строковый параметр для идентификации приложения, использующего WebSocket-клиент. Он будет отправляться в качестве одного из заголовков в начальном http-запросе (type:string)
  • _secure — булево значение, устанавливающее, должно ли использоваться защищенное соединение (type:boolean)

Метод Connect() вызывает приватные методы initialize() и upgrade(). Приватный метод initialize() обрабатывает полный адрес сервера и делит его на доменное имя и путь. Наконец createSessionConnection() создает сессию и хэндлы соединения. Метод upgrade() создает запрос и WebSocket-хэндлы перед установкой нового состояния клиентского соединения.

bool CWebsocket::Connect(const string _serveraddress, const INTERNET_PORT _port=443, const string _appname=NULL,bool _secure=true)
  {
   if(clientState==CONNECTED)
     {
      if(StringCompare(_serveraddress,serveraddress,false))
         Abort();
      else
         return(true);
     }

   if(!initialize(_serveraddress,_port,appname,_secure))
      return(false);

   return(upgrade());
  }




bool CWebsocket::initialize(const string _serveraddress,const ushort _port,const string _appname,bool _secure)
  {
   if(initialized)
      return(true);

   if(_secure)
      isSecure=true;

   if(_port==0)
     {
      if(isSecure)
         serverPort=443;
      else
         serverPort=80;
     }
   else
     {
      serverPort=_port;
      isSecure=_secure;

      if(serverPort==443 && !isSecure)
         isSecure=true;
     }



   if(_appname!=NULL)
      appname=_appname;
   else
      appname="Mt5 app";

   serveraddress=_serveraddress;

   int dot=StringFind(serveraddress,".");

   int ss=(dot>0)?StringFind(serveraddress,"/",dot):-1;

   serverPath=(ss>0)?StringSubstr(serveraddress,ss+1):"/";

   int sss=StringFind(serveraddress,"://");

   if(sss<0)
      sss=-3;

   serverName=StringSubstr(serveraddress,sss+3,ss);

   initialized=createSessionConnection();

   return(initialized);
  }



bool CWebsocket::createSessionConnection(void)
  {
   hSession=WinHttpOpen(appname,WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,NULL,NULL,0);

   if(hSession==NULL)
     {
      setErrorDescription();
      return(false);
     }


   hConnection=WinHttpConnect(hSession,serverName,serverPort,0);

   if(hSession==NULL)
     {
      setErrorDescription();
      reset();
      return(false);
     }

   return(true);

  }

bool CWebsocket::upgrade(void)
  {
   clientState=CONNECTING;

   hRequest=WinHttpOpenRequest(hConnection,"GET",serverPath,NULL,NULL,NULL,(isSecure)?WINHTTP_FLAG_SECURE:0);

   if(hRequest==NULL)
     {
      setErrorDescription();
      reset();
      return(false);
     }

   uint nullpointer[]= {};
   if(!WinHttpSetOption(hRequest,WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,nullpointer,0))
     {
      setErrorDescription();
      reset();
      return(false);
     }

   if(!WinHttpSendRequest(hRequest,NULL,0,nullpointer,0,0,0))
     {
      setErrorDescription();
      reset();
      return(false);
     }

   if(!WinHttpReceiveResponse(hRequest,nullpointer))
     {
      setErrorDescription();
      reset();
      return(false);
     }

   ulong nv=0;
   hWebSocket=WinHttpWebSocketCompleteUpgrade(hRequest,nv);
   if(hWebSocket==NULL)
     {
      setErrorDescription();
      reset();
      return(false);
     }

   WinHttpCloseHandle(hRequest);
   hRequest=NULL;
   clientState=CONNECTED;

   return(true);

  }

Если метод Connect() возвращает true, мы можем начать отправку данных через WebSocket-клиент. Процесс упрощается благодаря использованию двух методов.
Метод SendString() принимает строку в качестве входных данных, в то время как метод Send() принимает массив беззнаковых символов в качестве единственного параметра функции. При успехе оба возвращают true и вызывают приватный метод clientsend(), который обрабатывает все операции отправки для класса.

//+------------------------------------------------------------------+
//| helper method for sending data to the server                     |
//+------------------------------------------------------------------+
bool CWebsocket::clientsend(BYTE &txbuffer[],WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype)
  {
   DWORD len=(ArraySize(txbuffer));

   if(len<=0)
     {
      setErrorDescription(WEBSOCKET_ERROR_EMPTY_SEND_BUFFER);
      return(false);
     }

   ulong send=WinHttpWebSocketSend(hWebSocket,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE,txbuffer,len);

   if(send)
     {
      setErrorDescription();
      return(false);
     }

   return(true);

  }

//+------------------------------------------------------------------+
//|public method for sending raw string messages                     |
//+------------------------------------------------------------------+
bool CWebsocket::SendString(const string msg)
  {
   if(!initialized)
     {
      setErrorDescription(WEBSOCKET_ERROR_NOT_INITIALIZED);
      return(false);
     }

   if(clientState!=CONNECTED)
     {
      setErrorDescription(WEBSOCKET_ERROR_NOT_CONNECTED);
      return(false);
     }

   if(StringLen(msg)<=0)
     {
      setErrorDescription(WEBSOCKET_ERROR_EMPTY_SEND_BUFFER);
      return(false);
     }

   BYTE msg_array[];

   StringToCharArray(msg,msg_array,0,WHOLE_ARRAY);

   ArrayRemove(msg_array,ArraySize(msg_array)-1,1);

   DWORD len=(ArraySize(msg_array));

   return(clientsend(msg_array,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE));
  }

//+------------------------------------------------------------------+
//|Public method for sending data prepackaged in an array            |
//+------------------------------------------------------------------+
bool CWebsocket::Send(BYTE &buffer[])
  {
   if(!initialized)
     {
      setErrorDescription(WEBSOCKET_ERROR_NOT_INITIALIZED);
      return(false);
     }

   if(clientState!=CONNECTED)
     {
      setErrorDescription(WEBSOCKET_ERROR_NOT_CONNECTED);
      return(false);
     }

   return(clientsend(buffer,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE));
  }

Для чтения данных, отправленных с сервера, мы можем использовать Read() или ReadString(). Методы возвращают размер полученных данных. ReadString() требует строку, передаваемую по ссылке, в которую будут записаны полученные данные, в то время как Read() записывает в массив беззнаковых символов.

//+------------------------------------------------------------------+
//|helper method for reading received messages from the server       |
//+------------------------------------------------------------------+
void CWebsocket::clientread(BYTE &rbuffer[],ulong &bytes)
  {

   WINHTTP_WEB_SOCKET_BUFFER_TYPE rbuffertype=-1;

   ulong done=0;
   ulong transferred=0;
   ZeroMemory(rbuffer);
   ZeroMemory(rxbuffer);
   bytes=0;

   do
     {
      ulong get=WinHttpWebSocketReceive(hWebSocket,rxbuffer,rxsize,transferred,rbuffertype);
      if(get)
        {
         setErrorDescription();
         return;
        }

      ArrayCopy(rbuffer,rxbuffer,(int)done,0,(int)transferred);

      done+=transferred;

      ZeroMemory(rxbuffer);

      transferred=0;

     }
   while(rbuffertype==WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE || rbuffertype==WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE);

   bytes=done;

   return;

  }

//+------------------------------------------------------------------+
//|public method for reading data sent from the server               |
//+------------------------------------------------------------------+
ulong CWebsocket::Read(BYTE &buffer[])
  {
   if(!initialized)
     {
      setErrorDescription(WEBSOCKET_ERROR_NOT_INITIALIZED);
      return(false);
     }

   if(clientState!=CONNECTED)
     {
      setErrorDescription(WEBSOCKET_ERROR_NOT_CONNECTED);
      return(false);
     }

   ulong bytes_read_from_socket=0;

   clientread(buffer,bytes_read_from_socket);

   return(bytes_read_from_socket);

  }
//+------------------------------------------------------------------+
//|public method for reading data sent from the server               |
//+------------------------------------------------------------------+
ulong CWebsocket::ReadString(string &_response)
  {
   if(!initialized)
     {
      setErrorDescription(WEBSOCKET_ERROR_NOT_INITIALIZED);
      return(false);
     }

   if(clientState!=CONNECTED)
     {
      setErrorDescription(WEBSOCKET_ERROR_NOT_CONNECTED);
      return(false);
     }

   ulong bytes_read_from_socket=0;
   BYTE buffer[];

   clientread(buffer,bytes_read_from_socket);

   _response=(bytes_read_from_socket)?CharArrayToString(buffer):NULL;

   return(bytes_read_from_socket);

  }

Как только необходимость в WebSocket-клиенте отпадает, соединение с сервером может быть закрыто с помощью Close() или Abort(). Метод Abort() отличается от Close() тем, что он не только закрывает WebSocket-соединение, но также сбрасывает значения некоторых свойств класса, устанавливая их в состояние по умолчанию.

//+------------------------------------------------------------------+
//| Closes a websocket client connection                             |
//+------------------------------------------------------------------+
void CWebsocket::Close(void)
  {
   if(clientState==CLOSED)
      return;

   clientState=CLOSING;

   BYTE nullpointer[]= {};

   ulong result=WinHttpWebSocketClose(hWebSocket,WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS,nullpointer,0);
   if(result)
      setErrorDescription();

   reset();

   return;
  }


//+--------------------------------------------------------------------------+
//|method for abandoning a client connection. All previous server connection |
//|   parameters are reset to their default state                            |
//+--------------------------------------------------------------------------+
void CWebsocket::Abort(void)
  {
   Close();
//---
   serveraddress=serverName=serverPath=NULL;
   serverPort=0;
   isSecure=false;
   last_error=0;
   StringFill(errormsg,0);
//---
   return;
  }

ClientState() опрашивает текущее состояние WebSocket-клиента.

DomainName(), Port() и ServerPath() возвращают, соответственно, доменное имя, порт и путь для текущего соединения.

LastErrorMessage() можно использовать для получения последней ошибки в виде детализированной строки, в то время как вызов LastError() возвращает код ошибки в виде целого значения.

//public getter methods
   string            LastErrorMessage(void)          {  return(errormsg);    }
   uint              LastError(void)      {  return(last_error);  }
   ENUM_WEBSOCKET_STATE ClientState(void) {  return(clientState); }
   string            DomainName(void)                {  return(serverName);  }
   INTERNET_PORT     Port(void)               {  return(serverPort);  }
   string            ServerPath(void)                {  return(serverPath);  }

Ниже приведен весь класс.

//+------------------------------------------------------------------+
//|Class CWebsocket                                                  |
//| Purpose: class for websocket client                              |
//+------------------------------------------------------------------+

class CWebsocket
  {
private:
   ENUM_WEBSOCKET_STATE clientState;            //websocket state
   HINTERNET            hSession;               //winhttp session handle
   HINTERNET            hConnection;            //winhttp connection handle
   HINTERNET            hWebSocket;             //winhttp websocket handle
   HINTERNET            hRequest;               //winhtttp request handle
   string               appname;                //optional application name sent as one of the headers in initial http request
   string               serveraddress;          //full server address
   string               serverName;             //server domain name
   INTERNET_PORT        serverPort;             //port number
   string               serverPath;             //server path
   bool                 initialized;            //boolean flag that denotes the state of underlying winhttp infrastruture required for client
   BYTE                 rxbuffer[];             //internal buffer for reading from the socket
   bool                 isSecure;               //secure connection flag
   ulong                rxsize;                 //rxbuffer arraysize
   string               errormsg;               //internal buffer for error messages
   uint                 last_error;             //last winhttp/win32/class specific error
   // private methods
   bool              initialize(const string _serveraddress, const INTERNET_PORT _port, const string _appname,bool _secure);
   bool              createSessionConnection(void);
   bool              upgrade(void);
   void              reset(void);
   bool              clientsend(BYTE &txbuffer[],WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype);
   void              clientread(BYTE &rxbuffer[],ulong &bytes);
   void              setErrorDescription(uint error=0);

public:
                     CWebsocket(void):clientState(0),
                     hSession(NULL),
                     hConnection(NULL),
                     hWebSocket(NULL),
                     hRequest(NULL),
                     serveraddress(NULL),
                     serverName(NULL),
                     serverPort(0),
                     initialized(false),
                     isSecure(false),
                     rxsize(65539),
                     errormsg(NULL),
                     last_error(0)
     {
      ArrayResize(rxbuffer,(int)rxsize);
      ArrayFill(rxbuffer,0,rxsize,0);
      StringInit(errormsg,1000);
     }

                    ~CWebsocket(void)
     {
      Close();
      ArrayFree(rxbuffer);
     }
   //public methods

   bool              Connect(const string _serveraddress, const INTERNET_PORT _port=443, const string _appname=NULL,bool _secure=true);
   void              Close(void);
   bool              SendString(const string msg);
   bool              Send(BYTE &buffer[]);
   ulong             ReadString(string &response);
   ulong             Read(BYTE &buffer[]);
   void              Abort(void);
   void              ResetLastError(void)
     {
      last_error=0;
      StringFill(errormsg,0);
      ::ResetLastError();
     }
   //public getter methods
   string            LastErrorMessage(void)          {  return(errormsg);    }
   uint              LastError(void)      {  return(last_error);  }
   ENUM_WEBSOCKET_STATE ClientState(void) {  return(clientState); }
   string            DomainName(void)                {  return(serverName);  }
   INTERNET_PORT     Port(void)               {  return(serverPort);  }
   string            ServerPath(void)                {  return(serverPath);  }



  };

Теперь, когда у нас есть класс WebSocket, мы можем рассмотреть пример его использования.


Тестирование класса CWebsocket

Для тестирования я создам приложение для MetaTrader 5, добавляющее пользовательский символ с ресурса Binary.com. При запуске на графике приложение загружает историю и открывает новый график пользовательского символа, который обновляется в реальном времени при каждом тике.

Предполагаются две версии приложения. BinaryCustomSymboWithTickHistory.ex5 будет использовать тиковую историю, в то время как BinaryCustomSymbolWithBarHistory.ex5 будет загружать историю бара в формате OHLC. Код обоих приложений почти идентичен.

Binary.com располагает хорошо задокументированным API, позволяющим разработчикам создавать интерфейсы, взаимодействующие с их системами. API полагается на веб-сокеты с запросами и ответами в формате JSON.

Веб-страница Binary.com



Приложение будет выполнено в виде советника, использующего три важные библиотеки:

  • Первая – websocket.mqh для обработки WebSocket-соединений,
  • вторая – JAson.mqh для работы с данными в формате JSON за авторством Алексея Сергеева. Ее можно найти в GitHub-репозитории vivazzi,
  • третья – FileTxt.mqh для обработки файловых операций.

Советник имеет следующие настраиваемые входные параметры:

  • binary_appid - строковый параметр для доступа приложения к API. Идентификатор приложения можно получить, следуя инструкциям на портале разработчиков. Подписка на тиковые данные по символу не требует авторизации на Binary.com, поэтому указывать токен API не нужно.
  • binary_symbol - перечисление, позволяющее пользователям выбрать символ, который необходимо импортировать в MetaTrader 5.
  • binary_timeframe - таймфрейм графика, который открывается после загрузки и добавления истории в MetaTrader 5.
#include<websocket.mqh>
#include<JAson.mqh>
#include<Files/FileTxt.mqh>

#define BINARY_URL "ws.binaryws.com/websockets/v3?app_id="
#define BINARY_SYMBOL_SETTINGS "binarysymbolset.json"
#define BINARY_SYMBOL_BASE_PATH "Binary.com\\"

enum ENUM_BINARY_SYMBOL
{
 BINARY_1HZ10V=0,//Volatility 10 (1s)
 BINARY_1HZ25V,//Volatility 25 (1s)
 BINARY_1HZ50V,//Volatility 50 (1s)
 BINARY_1HZ75V,//Volatility 75 (1s)
 BINARY_1HZ100V,//Volatility 100 (1s)
 BINARY_1HZ200V,//Volatility 200 (1s)
 BINARY_1HZ300V,//Volatility 300 (1s)
 BINARY_BOOM300N,//BOOM 300
 BINARY_BOOM500,//BOOM 500
 BINARY_BOOM1000,//BOOM 1000
 BINARY_CRASH300N,//CRASH 300
 BINARY_CRASH500,//CRASH 500
 BINARY_CRASH1000,//CRASH 1000
 BINARY_cryBTCUSD,//BTCUSD
 BINARY_cryETHUSD,//ETHUSD
 BINARY_frxAUDCAD,//AUDCAD
 BINARY_frxAUDCHF,//AUDCHF
 BINARY_frxAUDJPY,//AUDJPY
 BINARY_frxAUDNZD,//AUDNZD
 BINARY_frxAUDUSD,//AUDUSD
 BINARY_frxBROUSD,//BROUSD
 BINARY_frxEURAUD,//EURAUD
 BINARY_frxEURCAD,//EURCAD
 BINARY_frxEURCHF,//EURCHF
 BINARY_frxEURGBP,//EURGBP
 BINARY_frxEURJPY,//EURJPY
 BINARY_frxEURNZD,//EURNZD
 BINARY_frxEURUSD,//EURUSD
 BINARY_frxGBPAUD,//GBPAUD
 BINARY_frxGBPCAD,//GBPCAD
 BINARY_frxGBPCHF,//GBPCHF
 BINARY_frxGBPJPY,//GBPJPY
 BINARY_frxGBPNOK,//GBPNOK
 BINARY_frxGBPNZD,//GBPNZD
 BINARY_frxGBPUSD,//GBPUSD
 BINARY_frxNZDJPY,//NZDJPY
 BINARY_frxNZDUSD,//NZDUSD
 BINARY_frxUSDCAD,//USDCAD
 BINARY_frxUSDCHF,//USDCHF
 BINARY_frxUSDJPY,//USDJPY
 BINARY_frxUSDMXN,//USDMXN
 BINARY_frxUSDNOK,//USDNOK
 BINARY_frxUSDPLN,//USDPLN
 BINARY_frxUSDSEK,//USDSEK
 BINARY_frxXAUUSD,//XAUUSD
 BINARY_frxXAGUSD,//XAGUSD
 BINARY_frxXPDUSD,//XPDUSD
 BINARY_frxXPTUSD,//XPTUSD
 BINARY_JD10,//Jump 10 Index
 BINARY_JD25,//Jump 25 Index
 BINARY_JD50,//Jump 50 Index
 BINARY_JD75,//Jump 75 Index
 BINARY_JD100,//Jump 100 Index
 BINARY_OTC_AEX,//Dutch Index
 BINARY_OTC_AS51,//Australian Index
 BINARY_OTC_DJI,//Wall Street Index
 BINARY_OTC_FCHI,//French Index 
 BINARY_OTC_FTSE,//UK Index
 BINARY_OTC_GDAXI,//German Index
 BINARY_OTC_HSI,//Hong Kong Index
 BINARY_OTC_N225,//Japanese Index
 BINARY_OTC_NDX,//US Tech Index
 BINARY_OTC_SPC,//US Index
 BINARY_OTC_SSMI,//Swiss Index 
 BINARY_OTC_SX5E,//Euro 50 Index
 BINARY_R_10,//Volatility 10 Index
 BINARY_R_25,//Volatility 25 Index
 BINARY_R_50,//Volatility 50 Index
 BINARY_R_75,//Volatility 75 Index
 BINARY_R_100,//Volatility 100 Index
 BINARY_RDBEAR,//Bear Market Index
 BINARY_RDBULL,//Bull Market Index
 BINARY_stpRNG,//Step Index
 BINARY_WLDAUD,//AUD Index
 BINARY_WLDEUR,//EUR Index
 BINARY_WLDGBP,//GBP Index
 BINARY_WLDUSD,//USD Index
 BINARY_WLDXAU//Gold Index
};

input string binary_appid="";//Binary.com registered application ID
input ENUM_BINARY_SYMBOL binary_symbol=BINARY_R_100;//Binary.com symbol
input ENUM_TIMEFRAMES binary_timeframe=PERIOD_M1;//Chart period


Советник будет состоять из двух классов – CCustomSymbol и CBinarySymbol.


Класс CCustomSymbol

CCustomSymbol – класс для работы с пользовательскими символами из внешних источников. Класс разработан по мотивам библиотеки SYMBOL за авторством fxsaber. Помимо прочего класс предоставляет методы для получения и работы со свойствами символов, а также для открытия и закрытия соответствующих графиков. Самое главное заключается в том, что он предоставляет три виртуальных метода, которые могут быть переопределены дочерними классами для более гибкого добавления пользовательского символа.

//+------------------------------------------------------------------+
//|General class for creating custom symbols from external source    |
//+------------------------------------------------------------------+
class CCustomSymbol
  {
protected:
   string            m_symbol_name;       //symbol name
   datetime          m_history_start;     //existing tick history start date
   datetime          m_history_end;       //existing tick history end date
   bool              m_new;               //flag specifying whether a symbol has just been created or already exists in the terminal
   ENUM_TIMEFRAMES   m_chart_tf;          //chart timeframe
public:
   //constructor
                     CCustomSymbol(void)
     {
      m_symbol_name=NULL;
      m_chart_tf=PERIOD_M1;
      m_history_start=0;
      m_history_end=0;
      m_new=false;
     }
   //destructor
                    ~CCustomSymbol(void)
     {

     }
   //method for initializing symbol, sets the symbol name and chart timeframe properties
   virtual bool      Initialize(const string sy,string sy_path=NULL, ENUM_TIMEFRAMES chart_tf=PERIOD_M1)
     {
      m_symbol_name=sy;
      m_chart_tf=chart_tf;
      return(InitSymbol(sy_path));
     }
   //gets the symbol name
   string            Name(void) const
     {
      return(m_symbol_name);
     }
   //sets the history start date
   bool              SetHistoryStartDate(const datetime startime)
     {
      if(startime>=TimeLocal())
        {
         Print("Invalid history start time");
         return(false);
        }

      m_history_start=startime;

      return(true);
     }
   //gets the history start date
   datetime          GetHistoryStartDate(void)
     {
      return(m_history_start);
     }
   //general methods for setting the properties of the custom symbol
   bool              SetProperty(const ENUM_SYMBOL_INFO_DOUBLE Property, double Value) const
     {
      return(::CustomSymbolSetDouble(m_symbol_name, Property, Value));
     }

   bool              SetProperty(const ENUM_SYMBOL_INFO_INTEGER Property, long Value) const
     {
      return(::CustomSymbolSetInteger(m_symbol_name, Property, Value));
     }

   bool              SetProperty(const ENUM_SYMBOL_INFO_STRING Property, string Value) const
     {
      return(::CustomSymbolSetString(m_symbol_name, Property, Value));
     }
   //general methods for getting the symbol properties of the custom symbol
   long              GetProperty(const ENUM_SYMBOL_INFO_INTEGER Property) const
     {
      return(::SymbolInfoInteger(m_symbol_name, Property));
     }

   double            GetProperty(const ENUM_SYMBOL_INFO_DOUBLE Property) const
     {
      return(::SymbolInfoDouble(m_symbol_name, Property));
     }

   string            GetProperty(const ENUM_SYMBOL_INFO_STRING Property) const
     {
      return(::SymbolInfoString(m_symbol_name, Property));
     }
   //method for deleting a custom symbol
   bool              Delete(void)
     {
      return((bool)(GetProperty(SYMBOL_CUSTOM)) && DeleteAllCharts()  && ::CustomSymbolDelete(m_symbol_name) && SymbolSelect(m_symbol_name,false));
     }
   //unimplemented virtual method for adding new ticks
   virtual void      AddTick(void)
     {
      return;
     }
   //unimplemented virtual method for aquiring the either ticks or candle history from an external source
   virtual bool      UpdateHistory(void)
     {
      return(false);
     }

protected:
   //checks if the symbol already exists or not
   bool              SymbolExists(void)
     {
      return(SymbolSelect(m_symbol_name,true));
     }
   //method that opens a new chart according to the m_chart_tf property
   void              OpenChart(void)
     {
      long Chart = ::ChartFirst();

      bool opened=false;

      while(Chart != -1)
        {
         if((::ChartSymbol(Chart) == m_symbol_name))
           {
            ChartRedraw(Chart);
            if(ChartPeriod(Chart)==m_chart_tf)
               opened=true;
           }
         Chart = ::ChartNext(Chart);
        }

      if(!opened)
        {
         long id = ChartOpen(m_symbol_name,m_chart_tf);
         if(id == 0)
           {
            Print("Can't open new chart for " + m_symbol_name + ", code: " + (string)GetLastError());
            return;
           }
         else
           {
            Sleep(1000);
            ChartSetSymbolPeriod(id, m_symbol_name, m_chart_tf);
            ChartSetInteger(id, CHART_MODE,CHART_CANDLES);
           }
        }
     }
   //deletes all charts for the specified symbol
   bool              DeleteAllCharts(void)
     {
      long Chart = ::ChartFirst();

      while(Chart != -1)
        {
         if((Chart != ::ChartID()) && (::ChartSymbol(Chart) == m_symbol_name))
            if(!ChartClose(Chart))
              {
               Print("Error closing chart id ", Chart, m_symbol_name, ChartPeriod(Chart));
               return(false);
              }

         Chart = ::ChartNext(Chart);
        }

      return(true);
     }
   //helper method that initializes a custom symbol
   bool              InitSymbol(const string _path=NULL)
     {
      if(!SymbolExists())
        {
         if(!CustomSymbolCreate(m_symbol_name,_path))
           {
            Print("error creating custom symbol ", ::GetLastError());
            return(false);
           }

         if(!SetProperty(SYMBOL_CHART_MODE,SYMBOL_CHART_MODE_BID)   ||
            !SetProperty(SYMBOL_SWAP_MODE,SYMBOL_SWAP_MODE_DISABLED) ||
            !SetProperty(SYMBOL_TRADE_MODE,SYMBOL_TRADE_MODE_DISABLED))
           {
            Print("error setting symbol properties");
            return(false);
           }

         if(!SymbolSelect(m_symbol_name,true))
           {
            Print("error adding symbol to market watch",::GetLastError());
            return(false);
           }

         m_new=true;

         return(true);
        }
      else
        {
         long custom=GetProperty(SYMBOL_CUSTOM);

         if(!custom)
           {
            Print("Error, symbol is not custom ",m_symbol_name,::GetLastError());
            return(false);
           }

         m_history_end=GetLastBarTime();
         m_history_start=GetFirstBarTime();
         m_new=false;

         return(true);
        }
     }

   //gets the last tick time for an existing custom symbol

   datetime          GetLastTickTime(void)
     {
      MqlTick tick;

      ZeroMemory(tick);

      if(!SymbolInfoTick(m_symbol_name,tick))
        {
         Print("symbol info tick failure ", ::GetLastError());
         return(0);
        }
      else
         return(tick.time);
     }


   //gets the last bar time of the one minute timeframe in candle history
   datetime          GetLastBarTime(void)
     {
      MqlRates candle[1];

      ZeroMemory(candle);

      int bars=iBars(m_symbol_name,PERIOD_M1);

      if(bars<=0)
         return(0);

      if(CopyRates(m_symbol_name,PERIOD_M1,0,1,candle)>0)
         return(candle[0].time);
      else
         return(0);
     }
   //gets the first bar time of the one minute timeframe  in candle  history
   datetime          GetFirstBarTime(void)
     {
      MqlRates candle[1];

      ZeroMemory(candle);

      int bars=iBars(m_symbol_name,PERIOD_M1);

      if(bars<=0)
         return(0);

      if(CopyRates(m_symbol_name,PERIOD_M1,bars-1,1,candle)>0)
         return(candle[0].time);
      else
         return(0);

     }


  };

Параметры метода Initialize():

  •   sy - этот строковый параметр устанавливает наименование пользовательского символа
  •   sy_path - строковый параметр, устанавливающий свойство пути символа 
  •   chart_tf - параметр устанавливает период графика, который будет открыт после загрузки истории символов
Метод вызывает Initsymbol(), который создает новый пользовательский символ, если его еще нет, или загружает свойства истории, если символ существует.

Два оставшихся виртуальных метода, UpdateHistory() и AddTick(), в CCustomSymbol не добавляются. Любому производному классу необходимо будет переопределить эти методы.


Класс CBinarySymbol

Здесь вступает в дело класс CBinarySymbol. Он наследуется от CCustomSymbol и предоставляет методы, которые переопределяют виртуальные методы своего родительского класса. Именно здесь мы будем использовать WebSocket-клиент для применения Binary.com API.

//+------------------------------------------------------------------+
//|Class for creating custom Binary.com specific symbols             |
//+------------------------------------------------------------------+
class CBinarySymbol:public CCustomSymbol
  {
private:
   //private properties
   string            m_appID;      //app id string issued by Binary.com
   string            m_url;        //final url
   string            m_stream_id;  //stream identifier for a symbol
   int               m_index;      //array index
   CWebsocket*       websocket;    //websocket client
   CJAVal*           json;         //utility json object
   CJAVal*           symbolSpecs;  //json object storing symbol specification
   //private methods
   bool              CheckBinaryError(CJAVal &j);
   bool              GetSymbolSettings(void);
public:
   //Constructor
                     CBinarySymbol(void):m_appID(NULL),
                     m_url(NULL),
                     m_stream_id(NULL),
                     m_index(-1)

     {
      json=new CJAVal();
      symbolSpecs=new CJAVal();
      websocket=new CWebsocket();
     }
   //Destructor
                    ~CBinarySymbol(void)
     {
      if(CheckPointer(websocket)==POINTER_DYNAMIC)
        {
         if(m_stream_id!="")
            StopTicksStream();
         delete websocket;
        }

      if(CheckPointer(json)==POINTER_DYNAMIC)
         delete json;

      if(CheckPointer(symbolSpecs)==POINTER_DYNAMIC)
         delete symbolSpecs;

      Comment("");

     }
   //public methods
   virtual void      AddTick(void) override;
   virtual bool      Initialize(const string sy,string sy_path=NULL,ENUM_TIMEFRAMES chart_tf=PERIOD_M1) override;
   virtual bool      UpdateHistory(void) override;
   void              SetAppID(const string id);
   bool              StartTicksStream(void);
   bool              StopTicksStream(void);
  };

После создания копии класса CBinarySymbol необходимо установить правильный идентификатор приложения app_id, используя метод SetAppID(). Только тогда мы можем инициализировать пользовательский символ.

//+------------------------------------------------------------------+
//|sets the the application id used to consume binary.com api        |
//+------------------------------------------------------------------+
void CBinarySymbol::SetAppID(const string id)
  {
   if(m_appID!=NULL && StringCompare(id,m_appID,false))
      websocket.Abort();

   m_appID=id;
   m_url=BINARY_URL+m_appID;

  }

Метод Initialize() использует приватный метод getSymbolSpecs() для получения свойств выбранного символа. Необходимая информация затем используется для установки свойств нового пользовательского символа.

//+------------------------------------------------------------------+
//|Begins process of creating custom symbol                          |
//+------------------------------------------------------------------+
bool CBinarySymbol::Initialize(const string sy,string sy_path=NULL, ENUM_TIMEFRAMES chart_tf=PERIOD_M1)
  {
   if(CheckPointer(websocket)==POINTER_INVALID || CheckPointer(json)==POINTER_INVALID || CheckPointer(symbolSpecs)==POINTER_INVALID)
     {
      Print("Invalid pointer found ");
      return(false);
     } 


   if(m_appID=="")
     {
      Alert("Application ID has not been set, It is required for the program to work");
      return(false);
     }

   m_symbol_name=(StringFind(sy,"BINARY_")>=0)?StringSubstr(sy,7):sy;
   m_chart_tf=chart_tf;

   Comment("Initializing Symbol "+m_symbol_name+".......");

   if(!GetSymbolSettings())
      return(false);

   string s_path=BINARY_SYMBOL_BASE_PATH+symbolSpecs["active_symbols"][m_index]["market_display_name"].ToStr();
   string symbol_description=symbolSpecs["active_symbols"][m_index]["display_name"].ToStr();
   double s_point=symbolSpecs["active_symbols"][m_index]["pip"].ToDbl();
   int s_digits=(int)MathAbs(MathLog10(s_point));

   if(!InitSymbol(s_path))
      return(false);

   if(m_new)
     {
      if(!SetProperty(SYMBOL_DESCRIPTION,symbol_description) ||
         !SetProperty(SYMBOL_POINT,s_point)                 ||
         !SetProperty(SYMBOL_DIGITS,s_digits))
        {
         Print("error setting symbol properties ", ::GetLastError());
         return(false);
        }
     }

   Comment("Symbol "+m_symbol_name+" initialized.......");

           return(true);
  }

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

//+------------------------------------------------------------------+
//|method for updating the tick history for a particular symbol      |
//+------------------------------------------------------------------+
bool CBinarySymbol::UpdateHistory(void)
  {
   if(websocket.ClientState()!=CONNECTED && !websocket.Connect(m_url))
     {
      Print(websocket.LastErrorMessage()," : ",websocket.LastError());
      return(false);
     }

   Comment("Updating history for "+m_symbol_name+".......");

   MqlRates history_candles[];
   string history=NULL;
   json.Clear();

   json["ticks_history"]=m_symbol_name;

   if(m_new)
     {
      if(m_history_start>0)
        {
         json["start"]=(int)(m_history_start);
        }
     }
   else
      if(m_history_end!=0)
        {
         json["start"]=(int)(m_history_start);
        }


   json["end"]="latest";
   json["style"]="candles";

   if(!websocket.SendString(json.Serialize()))
     {
      Print(websocket.LastErrorMessage());
      return(false);
     }

   if(websocket.ReadString(history))
     {
      json.Deserialize(history);

      if(CheckBinaryError(json))
         return(false);

      int i=0;


      if(ArrayResize(history_candles,(json["candles"].Size()),100)<0)
        {
         Print("Last error is "+IntegerToString(::GetLastError()));
         return(false);
        }
          
      while(json["candles"][i]["open"].ToDbl()!=0.0)
        {
         history_candles[i].close=json["candles"][i]["close"].ToDbl();
         history_candles[i].high=json["candles"][i]["high"].ToDbl();
         history_candles[i].low=json["candles"][i]["low"].ToDbl();
         history_candles[i].open=json["candles"][i]["open"].ToDbl();
         history_candles[i].tick_volume=4;
         history_candles[i].real_volume=0;
         history_candles[i].spread=0;
         history_candles[i].time=(datetime)json["candles"][i]["epoch"].ToInt();
         i++;
        }


      if(ArraySize(history_candles)>0)
        {
         if(CustomRatesUpdate(m_symbol_name,history_candles)<0)
           {
            Print("Error adding history "+IntegerToString(::GetLastError()));
            return(false);
           }
        }
      else
        {
         Print("Received unexpected response from server ",IntegerToString(::GetLastError()), " "+history);
         return(false);
        }
     }
   else
     {
      Print("error reading "," error: ",websocket.LastError(), websocket.LastErrorMessage());
      return(false);
     }

   OpenChart();

   return(true);

  }

//+------------------------------------------------------------------+
//|method for updating the tick history for a particular symbol      |
//+------------------------------------------------------------------+
bool CBinarySymbol::UpdateHistory(void)
  {
   if(websocket.ClientState()!=CONNECTED && !websocket.Connect(m_url))
     {
      Print(websocket.LastErrorMessage()," : ",websocket.LastError());
      return(false);
     }

   Comment("Updating history for "+m_symbol_name+".......");

   MqlTick history_ticks[];
   string history=NULL;
   json.Clear();

   json["ticks_history"]=m_symbol_name;

   if(m_new)
     {
      if(m_history_start>0)
        {
         json["start"]=(int)(m_history_start);
        }
     }
   else
      if(m_history_end!=0)
        {
         json["start"]=(int)(m_history_start);
        }


   json["count"]=m_max_ticks;
   json["end"]="latest";
   json["style"]="ticks";

   if(!websocket.SendString(json.Serialize()))
     {
      Print(websocket.LastErrorMessage());
      return(false);
     }

   if(websocket.ReadString(history))
     {
      json.Deserialize(history);

      if(CheckBinaryError(json))
         return(false);

      int i=0;


      int z=i;
      int diff=0;


      while(json["history"]["prices"][i].ToDbl()!=0.0)
        {

         diff=(i>0)?(int)(json["history"]["times"][i].ToInt() - json["history"]["times"][i-1].ToInt()):0;//((m_history_end>0)?(json["history"]["times"][i].ToInt() - (int)(m_history_end)):0);

         if(diff > 1)
           {
            int k=z+diff;
            int p=1;

            if(ArrayResize(history_ticks,k,100)!=k)
              {
               Print("Memory allocation error,  "+IntegerToString(::GetLastError()));
               return(false);
              }

            while(z<(k-1))
              {
               history_ticks[z].bid=json["history"]["prices"][i-1].ToDbl();
               history_ticks[z].ask=0;
               history_ticks[z].time=(datetime)(json["history"]["times"][i-1].ToInt()+p);
               history_ticks[z].time_msc=(long)((json["history"]["times"][i-1].ToInt()+p)*1000);
               history_ticks[z].last=0;
               history_ticks[z].volume=0;
               history_ticks[z].volume_real=0;
               history_ticks[z].flags=TICK_FLAG_BID;
               z++;
               p++;
              }

            history_ticks[z].bid=json["history"]["prices"][i].ToDbl();
            history_ticks[z].ask=0;
            history_ticks[z].time=(datetime)(json["history"]["times"][i].ToInt());
            history_ticks[z].time_msc=(long)((json["history"]["times"][i].ToInt())*1000);
            history_ticks[z].last=0;
            history_ticks[z].volume=0;
            history_ticks[z].volume_real=0;
            history_ticks[z].flags=TICK_FLAG_BID;

            i++;
            z++;
           }
         else
           {
            if(ArrayResize(history_ticks,z+1,100)==(z+1))
              {
               history_ticks[z].bid=json["history"]["prices"][i].ToDbl();
               history_ticks[z].ask=0;
               history_ticks[z].time=(datetime)json["history"]["times"][i].ToInt();
               history_ticks[z].time_msc=(long)(json["history"]["times"][i].ToInt()*1000);
               history_ticks[z].last=0;
               history_ticks[z].volume=0;
               history_ticks[z].volume_real=0;
               history_ticks[z].flags=TICK_FLAG_BID;
              }
            else
              {
               Print("Memory allocation error,  "+IntegerToString(::GetLastError()));
               return(false);
              }

            i++;
            z++;
           }
        }

      //Print("z is ",z,". Arraysize is ",ArraySize(history_ticks));

      if(m_history_end>0 && z>0)
        {
         DeleteAllCharts();

         if(CustomTicksDelete(m_symbol_name,int(m_history_start)*1000,(history_ticks[0].time_msc-1000))<0)
           {
            Print("error deleting ticks ", ::GetLastError());
            return(false);
           }
         else
           {
            m_history_end=history_ticks[z-1].time;
            m_history_start=history_ticks[0].time;
           }
        }


      if(ArraySize(history_ticks)>0)
        {
         //ArrayPrint(history_ticks);
         if(CustomTicksAdd(m_symbol_name,history_ticks)<0)//CustomTicksReplace(m_symbol_name,history_ticks[0].time_msc,history_ticks[z-1].time_msc,history_ticks)
           {
            Print("Error adding history "+IntegerToString(::GetLastError()));
            return(false);
           }
        }
      else
        {
         Print("Received unexpected response from server ",IntegerToString(::GetLastError()), " "+history);
         return(false);
        }
     }
   else
     {
      Print("error reading "," error: ",websocket.LastError(), websocket.LastErrorMessage());
      return(false);
     }

   OpenChart();

   return(true);

  }

После обновления истории и открытия графика необходимо подписаться на получение тиковых данных от Binary.com. Метод StartTicksStream() направляет соответствующий запрос. Если он успешен, сервер начинает передавать котировки в реальном времени, которые затем обрабатываются методом AddTick(). В свою очередь, метод StopTicksStream() уведомляет сервер о необходимости остановить отправку котировок.

//+---------------------------------------------------------------------+
//|method that enables the reciept of new ticks as they become available|
//+---------------------------------------------------------------------+
bool CBinarySymbol::StartTicksStream(void)
  {
   Comment("Starting live ticks stream for "+m_symbol_name+".......");

   if(m_stream_id!="")
      StopTicksStream();

   json.Clear();
   json["subscribe"]=1;
   json["ticks"]=m_symbol_name;

   return(websocket.SendString(json.Serialize()));
  }

//+------------------------------------------------------------------+
//|Used to cancel all tick streams that may have been initiated      |
//+------------------------------------------------------------------+
bool CBinarySymbol::StopTicksStream(void)
  {
   
   json.Clear();
   json["forget_all"]="ticks";

   if(websocket.SendString(json.Serialize()))
     {
      m_stream_id=NULL;
      if(websocket.ReadString(m_stream_id)>0)
        {
         m_stream_id=NULL;
         Comment("Stopping live ticks stream for  "+m_symbol_name+".......");
         return(true);
        }
     }

   return(false);
  }



//+------------------------------------------------------------------+
//|Overridden method that handles new ticks streamed from binary.com |
//+------------------------------------------------------------------+
void CBinarySymbol::AddTick(void)
  {
   string str_tick;

   MqlTick current_tick[1];

   json.Clear();

   if(websocket.ReadString(str_tick))
     {
      json.Deserialize(str_tick);
      ZeroMemory(current_tick);

      if(CheckBinaryError(json))
         return;

      if(!json["tick"]["ask"].ToDbl())
         return;

      current_tick[0].ask=json["tick"]["ask"].ToDbl();
      current_tick[0].bid=json["tick"]["bid"].ToDbl();
      current_tick[0].last=0;
      current_tick[0].time=(datetime)json["tick"]["epoch"].ToInt();
      current_tick[0].time_msc=(long)((json["tick"]["epoch"].ToInt())*1000);
      current_tick[0].volume=0;
      current_tick[0].volume_real=0;

      if(current_tick[0].ask)
         current_tick[0].flags|=TICK_FLAG_ASK;
      if(current_tick[0].bid)
         current_tick[0].flags|=TICK_FLAG_BID;

      if(m_stream_id==NULL)
         m_stream_id=json["tick"]["id"].ToStr();

      if(CustomTicksAdd(m_symbol_name,current_tick)<0)
        {
         Print("failed to add new tick ", ::GetLastError());
         return;
        }
      Comment("New ticks for  "+m_symbol_name+".......");
     }
   else
     {
      Print("read error ",websocket.LastError(), websocket.LastErrorMessage());

      websocket.ResetLastError();

      if(websocket.ClientState()!=CONNECTED && websocket.Connect(m_url))
        {
         if(m_stream_id!=NULL)
            if(StopTicksStream())
              {
               if(InitSymbol())
                  if(UpdateHistory())
                    {
                     StartTicksStream();
                     return;
                    }
              }
        }
     }
//---
  }

Ниже приведен код советника.

CBinarySymbol b_symbol;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   b_symbol.SetAppID(binary_appid);
//---
   if(!b_symbol.Initialize(EnumToString(binary_symbol)))
      return(INIT_FAILED);
//---
   if(!b_symbol.UpdateHistory())
      return(INIT_FAILED);
//---
   if(!b_symbol.StartTicksStream())
      return(INIT_FAILED);
//--- create timer
   EventSetMillisecondTimer(500);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- destroy timer
   EventKillTimer();
//--- stop the ticks stream
   b_symbol.StopTicksStream();

  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---

  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//---
   b_symbol.AddTick();
  }
//+------------------------------------------------------------------+

Оба советника имеют схожий код за исключением метода UpdateHistory().

Ниже показано создание нового пользовательского символа после запуска советника.

EA demo

Заключение

Мы использовали Win32 API, чтобы создать WebSocket-клиент для MetaTrader 5. Мы создали класс, который инкапсулирует этот функционал и продемонстрировали его применение в советнике, взаимодействующем с WebSockets API от Binary.com.

Папка
Содержание
Описание
MT5zip\Mt5zip\Mql5\include
JAson.mqh, websocket.mqh, winhttp.mqh
Include-файлы содержат код парсера JSON (класс CJAval), клиента WebSocket (класс CWebsocket), импортируемой функции WinHttp и объявлений типов
MT5zip\ Mt5zip\Mql5\ Experts BinaryCustomSymboWithTickHistory.mq5, BinaryCustomSymbolWithBarHistory.mq5 Советники, использующие класс CWebsocket для создания пользовательских символов с помощью WebSocket API от Binary.com


Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/10275

Прикрепленные файлы |
Mt5.zip (23.93 KB)
Как разработать торговую систему на основе Bollinger Bands Как разработать торговую систему на основе Bollinger Bands
В этой статье мы поговорим о полосах Боллинджера (Bollinger Bands) — одном из самых популярных индикаторов в мире трейдинга. Мы обсудим технический анализ, а также научимся разрабатывать системы алгоритмической торговли на основе индикатора Bollinger Bands.
Графика в библиотеке DoEasy (Часть 100): Устраняем недочёты при работе с расширенными стандартными графическими объектами Графика в библиотеке DoEasy (Часть 100): Устраняем недочёты при работе с расширенными стандартными графическими объектами
Сегодня мы немного "подчистим хвосты" — устраним явные недоработки при одновременной работе с расширенными (и стандартными) графическими объектами и объектами-формами на канвасе и исправим ошибки, замеченные при тестировании в прошлой статье. И на этом завершим этот раздел описания библиотеки.
Анализируем причины неудач торговых советников Анализируем причины неудач торговых советников
В этой статье мы проанализируем данные по валютам, чтобы понять, почему советники могут показывать хорошие результаты на одних интервалах и при этом плохо работают на других.
Графика в библиотеке DoEasy (Часть 99): Перемещаем расширенный графический объект одной контрольной точкой Графика в библиотеке DoEasy (Часть 99): Перемещаем расширенный графический объект одной контрольной точкой
В прошлой статье мы создали возможность перемещения опорных точек расширенного графического объекта при помощи форм управления. Теперь сделаем перемещение составного графического объекта при помощи одной точки (формы) управления графическим объектом.