English Русский Español Deutsch 日本語 Português
preview
MetaTrader 5 的 WebSocket — 使用 Windows API

MetaTrader 5 的 WebSocket — 使用 Windows API

MetaTrader 5示例 | 11 四月 2022, 09:30
1 315 0
Francis Dube
Francis Dube

概述

MetaTrader 5 的 WebSocket, 一文中,我们讨论了 WebSocket 协议的基础知识,并创建了一个依赖 MQL5 实现的套接字客户端。 这一次,我们将利用 Windows API 为 MetaTrader 5 程序构建 WebSocket 客户端。 它也许是次一级的最佳选项,因为这不需要额外的软件,一切均由操作系统提供。 我们将把客户端作为一个类来实现,并借助 Binary.com WebSocket API 将实时的即时报价数据投喂给 MetaTrader 5。


Windows 中的 WebSocket

当来到 Windows API 和 Internet,MQL5 开发人员最熟悉 Windows Internet(WinINeT)函数库。 它实现了诸如文件传输协议(FTP)和 HTTP 等众多互联网协议。 与之类似的是 Windows HTTP 服务(WinHTTP)函数库。 它是 HTTP 协议的专用库,拥有服务器端开发的有用功能。 WinHTTP 公开的一些功能是处理 WebSocket 连接的实用程序。

自 Windows 8.1 和 Windows Server 2012 R2 开始,WebSocket 协议就被引入 Windows 操作系统。 Windows 7 和更久以前的操作系统则未提供对它的原生支持。 本文讲述的程序在这些运行旧操作系统的机器上不起作用。

Winhttp 函数库

为了使用 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() 为 WebSocket 握手过程做准备。

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;
     }

这会遵照 WebSocket 协议的规定,将所需的头添加到 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 类,该类将启用 WebSocket 客户端所需的 winhttp 库函数的包装器。
该文件以一个 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 — 服务器完整地址(类型:string)
  • _port — 服务器端口号(类型:ushort)
  • _appname — 这是一个字符串参数,可设置为应用程序使用 WebSocket 客户端的唯一标识。 它将作为初始 http 请求中的一个标头发送(类型:string)
  • _secure — 设置是否应使用安全连接的布尔值(类型: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 依赖于 WebSocket,其查询和响应则以 JSON 格式提供。

Binary 开发者网页



该应用程序将作为智能交易系统实现,并获得三个重要函数库的帮助:

  • 第一个是处理 WebSocket 连接的 websocket.mqh,
  • 第二个是 JAson.mqh,用于处理 Alexey Sergeev 编写的 JSON 格式的数据,可从 vivazzi 的 github 存储库中获取,
  • 我们需要的第三个函数库是 FileTxt.mqh,它用于处理文件操作。

EA 将拥有以下用户可调输入:

  • binary_appid - 这是准予我们的应用程序访问 API 所需的字符串参数,可以按照开发者门户上提供的说明获取应用程序 ID。 订阅某一品种的即时报价流不需要得到 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


EA 将由两个类组成 — CCustomSymbol 和 CBinarySymbol。


CCustomSymbol 类

CCustomSymbol 是一个处理来自外部数据源的自定义品种的类。 它的灵感来源于 fxsaber'sSYMBOL library。 它提供了操作和检索品种属性的方法,以及打开和关闭相应图表的方法。 更重要的是,它提供了三个虚拟方法,子类可以重写这些方法,从而允许自定义品种实现各种变体。

//+------------------------------------------------------------------+
//|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 类的一个实例后,应调用 SetAppID() 方法设置有效的应用程序标识符 app_id。 只有这样,我们才能继续初始化自定义品种。

//+------------------------------------------------------------------+
//|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;
                    }
              }
        }
     }
//---
  }

EA 的代码如下所示。

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 的代码都相同。

运行 EA 会创建一个新的自定义品种,如下所示。

EA 演示

结束语

我们探索了如何使用 Win32 API 为 MetaTrader 5 创建 WebSocket 客户端。 我们创建了一个封装此功能的类,并在 EA 里演示了它如何与 Binary.com WebSockets API 进行交互。

文件夹
内容
说明
MT5zip\Mt5zip\Mql5\include
JAson.mqh, websocket.mqh, winhttp.mqh
包含文件包括 JSON 解析器(CJAval 类)、WebSocket 客户端(CWebsocket 类)、WinHttp 导入函数和类型声明、等等分门别类的代码
MT5zip\ Mt5zip\Mql5\ Experts BinaryCustomSymboWithTickHistory.mq5, BinaryCustomSymbolWithBarHistory.mq5 示例 EA 使用 CWebsocket 类通过利用 Binary.com WebSocket API 创建自定义品种


本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/10275

附加的文件 |
Mt5.zip (23.93 KB)
DoEasy 函数库中的图形(第九十四部分):移动和删除复合图形对象 DoEasy 函数库中的图形(第九十四部分):移动和删除复合图形对象
在本文中,我将启动开发各种复合图形对象事件。 我们还将部分研究移动和删除复合图形对象。 实际上,在此,我还会把前一篇文章中实现的东西进行微调。
DoEasy 函数库中的图形(第九十三部分):准备创建复合图形对象的功能 DoEasy 函数库中的图形(第九十三部分):准备创建复合图形对象的功能
在本文中,我将着手开发用于创建复合图形对象的功能。 该函数库将支持创建复合图形对象,允许这些对象含有任意层次的连接。 我将为这些对象的后续实现准备所有必要的类。
学习如何设计不同的移动平均线系统 学习如何设计不同的移动平均线系统
有众多策略可依据任何规则过滤生成的信号,甚至可采用本文自身所讨论的移动平均值。 因此,本文的目的是与大家分享一些移动平均线策略,以及如何设计一款算法交易系统。
学习为什么、以及如何设计算法交易系统 学习为什么、以及如何设计算法交易系统
本文在介绍了 MQL5 的一些基础知识之后,通过设计一个简单的算法交易系统,向初学者展示了如何运用 MQL 的基础知识设计他们的算法交易系统(智能交易系统)