
WebSockets for MetaTrader 5 — Using the Windows API
Introduction
In the article WebSockets for MetaTrader 5, we discussed the basics of the WebSocket protocol and created a client that relied on MQL5 implemented sockets. This time around we will leverage the Windows API to build a WebSocket client for MetaTrader 5 programs. It is the next best option as no extra software is required, everything is provided by the operating system. We will implement the client as a class and conduct tests by consuming the Deriv.com WebSocket API to feed live tick data into MetaTrader 5.
WebSockets in Windows
When it comes to Windows API and the Internet, MQL5 developers are most familiar with the Windows Internet (WinINeT) library. It implements Internet protocols such as File transfer protocol (FTP) and HTTP among others. Similar to it is the Windows HTTP Services (WinHTTP) library. It is a dedicated library for the HTTP protocol with features that are useful for server side development. Some of the features exposed by WinHTTP are utilities for handling WebSocket connections.
The WebSocket protocol was introduced into Windows operating systems from Windows 8.1 and Windows Server 2012 R2 onwards. Windows 7 and older operating systems do not have native support for it. The programs described in this article will not work on machines running these older operating systems.
The Winhttp library
To create a WebSocket client connection using winhttp, we will need the functions listed below:
WinHttpOpen | Initializes the library preparing it for use by an application |
WinHttpConnect | Sets the domain name of the server the application wants to communicate with |
WinHttpOpenRequest | Creates an HTTP request handle |
WinHttpSetOption | Sets various configuration options for an HTTP connection |
WinHttpSendRequest | Sends a request to a server |
WinHttpReceiveResponse | Receives the response from a server after sending a request |
WinHttpWebSocketCompleteUpgrade | Confirms the response received from the server satisfies the WebSocket protocol |
WinHttpCloseHandle | Used to discard any resource descriptors previously in use |
WinHttpWebSocketSend | Used to send data via a WebSocket connection |
WinHttpWebSocketReceive | Receives data using a WebSocket connection |
WinHttpWebSocketClose | Closes a WebSocket connection |
WinHttpWebSocketQueryCloseStatus | Checks the close status message sent from the server |
All the functions available in the library are documented by Microsoft. Also, a detailed description of all the functions, their input parameters and return types, can be viewed by following the corresponding links above.
The client we will create for MetaTrader 5 works in synchronous mode. That means function calls will block execution until they return. For example, a call to WinHttpWebSocketReceive() will block the executing thread until data is available for reading. Keep this in mind when creating MetaTrader 5 applications.
The winhttp functions are declared and imported in the winhttp.mqh include file.
#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 //+------------------------------------------------------------------+
Using the winhttp functions
To establish a WebSocket client with these functions, first we have have to call WinHttpOpen() to initialize the library. The function returns a session handle to be used in subsequent calls to other winhttp library functions.
#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; }
The second step is the creation of a connection handle which is done with the help of WinHttpConnect(). It is here where we specify the server address and port number. It is important to note that at this point what is required is the domain name for the server without the scheme nor the path. The public IP address can also be used if it is known. Most errors that occur when using winhttp relate to passing an incorrectly formatted server address. For example, if the full server address is wss://ws.example.com/path, WinHttpConnect() expects only ws.example.com.
connectionhandle=WinHttpConnect(sessionhandle,server,Port,0); if(connectionhandle==NULL) { Print("WinHttpConnect error "+string(kernel32::GetLastError())); if(sessionhandle!=NULL) WinHttpCloseHandle(sessionhandle); return; }
With the connection handle successfully created, we will use it to establish a request handle by calling WinHttpOpenRequest(). Here we specify the path component if any from the server's address and also set the option to make the connection secure or not.
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; }
Once that is done and we have a valid request handle, we prepare for the WebSocket handshake process by calling 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; }
This adds the required headers to an http request as specified by the WebSocket protocol. The WebSocket handshake is initiated by calling WinHttpSendRequest() and then WinHttpReceiveResponse() to confirm receipt of a response to our request.
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() checks the response and ensures its adherence to the WebSocket protocol. If satisfied, the function returns the coveted WebSocket handle.
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;
From henceforth, our WebSocket client is fully functional and we can use WinHttpWebSocketSend() to send and WinHttpWebSocketReceive() to receive data. Since the WebSocket handle has been created, the request handle is no longer required because our http connection has been upgraded to a WebSocket connection. We then can release any resources associated with the request handle by calling 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); } //+------------------------------------------------------------------+
Calling WinHttpWebSocketClose() closes a WebSocket connection. Once a connection is closed, all the handles associated with it should be deinitilized by calling
WinHttpCloseHandle() for each.
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; }
The CWebsocket class
The websocket.mqh file will contain the CWebsocket class which will be a wrapper for the winhttp library functions needed to enable a WebSocket client.
The file begins with an include directive to admit all the functions and declarations imported from Windows API libraries.
#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 };
To start the process of connecting to a websocket server, Connect() would be the first method to call.
Connect() parameters:
- _serveraddress — server full address (type:string)
- _port — server port number (type:ushort)
- _appname — this is a string parameter that can be set to uniquely identify an application using the WebSocket client. It will be sent as one of the headers in the initial http request (type:string)
- _secure — a boolean value that sets whether a secure connection should be used or not (type:boolean)
The Connect() method calls the private methods initialize() and upgrade() respectively. The private method initialize() processes the full server address and splits it into domain name and path components. Lastly, createSessionConnection() creates the session and connection handles. The upgrade() method comes into play to create the request and WebSocket handles before setting the new state of the client connection.
//+------------------------------------------------------------------------------------------------------+ //|Connect method used to set server parameters and establish client connection | //+------------------------------------------------------------------------------------------------------+ 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()); } //+---------------------------------------------------------------------------------+ //| private method used to set the server parameters. | //+---------------------------------------------------------------------------------+ 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-(sss+3)); initialized=createSessionConnection(); return(initialized); } //+------------------------------------------------------------------+ //|creates the session and connection handles for the client | //+------------------------------------------------------------------+ 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(hConnection==NULL) { setErrorDescription(); reset(); return(false); } return(true); } //+---------------------------------------------------------------------+ //|helper method that sets up the required request and websocket handles| //+---------------------------------------------------------------------+ 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); }
If the Connect() method returns 'true', we can begin sending data via the WebSocket client. To facilitate this, there are two methods that can be used.
SendString() method takes a string as an input and the Send() method takes an unsigned character array as its sole function parameter. Both return 'true' on success and call the private method clientsend() which handles all send operations for the class.
//+------------------------------------------------------------------+ //| 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)); }
To read data sent from the server, we can use either Read() or ReadString(). The methods return the size of the data received. ReadString() requires a string passed by reference to which data received will be written to, whilst Read() writes to an unsigned character array.
//+------------------------------------------------------------------+ //|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); }
When the WebSocket client is no longer needed, the connection to the server can be closed with either Close() or Abort(). The Abort() method differs from the Close() method in that it not only closes a WebSocket connection but also goes further to reset the values of some class properties setting them to their default state.
//+------------------------------------------------------------------+ //| 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() queries the current state of the WebSocket client.
DomainName(), Port() and ServerPath() return the domain name, port and path component respectively for the current connection.
LastErrorMessage() can be used to get the last error as a detailed string, whereas a call to LastError() retrieves the error code as an integral value.
//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); }
The whole class is shown below.
//+------------------------------------------------------------------+ //|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); } };
Now that we have our WebSocket class, we can consider an example of its use.
Testing the CWebsocket class
For testing, we will create a MetaTrader 5 application that adds a custom symbol from Deriv.com. When loaded onto a chart, it will download history and open up a new chart for the custom symbol that will be updated with live tick data.
There will be two versions. DerivCustomSymboWithTickHistory.ex5 will use tick history, whilst the other DerivCustomSymbolWithBarHistory.ex5 will download OHLC bar history. Both will have similar code.
Deriv.com provides a well documented API that enables developers to build interfaces that interact with their systems. The API relies on websockets with queries and responses provided in JSON format.
The application will be implemented as an Expert Advisor that enlists the help of three important libraries:
- The first is websocket.mqh to handle WebSocket connections,
- the second is JAson.mqh for working with JSON formatted data authored by Alexey Sergeev and obtainable from vivazzi's github repository,
- the third library we will need is FileTxt.mqh for handling file operations.
The EA will have the following user adjustable inputs:
- DERIV_appid - this is a string parameter required to grant our application access to the API, an app ID can be acquired by following instructions provided on the developers' portal. Subscribing to a symbol tickstream does not require user authentication on Deriv.com, that is why there is no need to specify an API token.
- DERIV_symbol - this is an enumeration that allows users to select a symbol they want imported into MetaTrader 5.
- DERIV_timeframe - this is the timeframe of the chart that will be opened once history data has been downloaded and added to MetaTrader 5.
//+------------------------------------------------------------------+ //| DerivCustomSymbolWithTickHistory.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include<websocket.mqh> #include<JAson.mqh> #include<Files/FileTxt.mqh> #define DERIV_URL "wss://ws.derivws.com/websockets/v3?app_id=" #define DERIV_SYMBOL_SETTINGS "derivsymbolset.json" #define DERIV_SYMBOL_BASE_PATH "Deriv.com\\" enum ENUM_DERIV_SYMBOL { DERIV_1HZ10V=0,//Volatility 10 (1s) DERIV_1HZ25V,//Volatility 25 (1s) DERIV_1HZ50V,//Volatility 50 (1s) DERIV_1HZ75V,//Volatility 75 (1s) DERIV_1HZ100V,//Volatility 100 (1s) DERIV_1HZ200V,//Volatility 200 (1s) DERIV_1HZ300V,//Volatility 300 (1s) DERIV_BOOM300N,//BOOM 300 DERIV_BOOM500,//BOOM 500 DERIV_BOOM1000,//BOOM 1000 DERIV_CRASH300N,//CRASH 300 DERIV_CRASH500,//CRASH 500 DERIV_CRASH1000,//CRASH 1000 DERIV_cryBTCUSD,//BTCUSD DERIV_cryETHUSD,//ETHUSD DERIV_frxAUDCAD,//AUDCAD DERIV_frxAUDCHF,//AUDCHF DERIV_frxAUDJPY,//AUDJPY DERIV_frxAUDNZD,//AUDNZD DERIV_frxAUDUSD,//AUDUSD DERIV_frxBROUSD,//BROUSD DERIV_frxEURAUD,//EURAUD DERIV_frxEURCAD,//EURCAD DERIV_frxEURCHF,//EURCHF DERIV_frxEURGBP,//EURGBP DERIV_frxEURJPY,//EURJPY DERIV_frxEURNZD,//EURNZD DERIV_frxEURUSD,//EURUSD DERIV_frxGBPAUD,//GBPAUD DERIV_frxGBPCAD,//GBPCAD DERIV_frxGBPCHF,//GBPCHF DERIV_frxGBPJPY,//GBPJPY DERIV_frxGBPNOK,//GBPNOK DERIV_frxGBPNZD,//GBPNZD DERIV_frxGBPUSD,//GBPUSD DERIV_frxNZDJPY,//NZDJPY DERIV_frxNZDUSD,//NZDUSD DERIV_frxUSDCAD,//USDCAD DERIV_frxUSDCHF,//USDCHF DERIV_frxUSDJPY,//USDJPY DERIV_frxUSDMXN,//USDMXN DERIV_frxUSDNOK,//USDNOK DERIV_frxUSDPLN,//USDPLN DERIV_frxUSDSEK,//USDSEK DERIV_frxXAUUSD,//XAUUSD DERIV_frxXAGUSD,//XAGUSD DERIV_frxXPDUSD,//XPDUSD DERIV_frxXPTUSD,//XPTUSD DERIV_JD10,//Jump 10 Index DERIV_JD25,//Jump 25 Index DERIV_JD50,//Jump 50 Index DERIV_JD75,//Jump 75 Index DERIV_JD100,//Jump 100 Index DERIV_OTC_AEX,//Dutch Index DERIV_OTC_AS51,//Australian Index DERIV_OTC_DJI,//Wall Street Index DERIV_OTC_FCHI,//French Index DERIV_OTC_FTSE,//UK Index DERIV_OTC_GDAXI,//German Index DERIV_OTC_HSI,//Hong Kong Index DERIV_OTC_N225,//Japanese Index DERIV_OTC_NDX,//US Tech Index DERIV_OTC_SPC,//US Index DERIV_OTC_SSMI,//Swiss Index DERIV_OTC_SX5E,//Euro 50 Index DERIV_R_10,//Volatility 10 Index DERIV_R_25,//Volatility 25 Index DERIV_R_50,//Volatility 50 Index DERIV_R_75,//Volatility 75 Index DERIV_R_100,//Volatility 100 Index DERIV_RDBEAR,//Bear Market Index DERIV_RDBULL,//Bull Market Index DERIV_stpRNG,//Step Index DERIV_WLDAUD,//AUD Index DERIV_WLDEUR,//EUR Index DERIV_WLDGBP,//GBP Index DERIV_WLDUSD,//USD Index DERIV_WLDXAU//Gold Index }; input string DERIV_appid="";//Deriv.com registered application ID input ENUM_DERIV_SYMBOL DERIV_symbol=DERIV_cryBTCUSD;//Deriv.com symbol input ENUM_TIMEFRAMES DERIV_timeframe=PERIOD_M1;//Chart period
The EA will be made up of two classes – CCustomSymbol and CDerivSymbol.
The CCustomSymbol class
The CCustomSymbol is a class for working with custom symbols from external sources. It is inspired by fxsaber's SYMBOL library. It provides methods for manipulating and retrieving the properties of symbols as well as opening and closing their corresponding charts amongst other features. More importantly, it provides three virtual methods that child classes can override to allow for variations in the implementation of a custom symbol.
//+------------------------------------------------------------------+ //|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); } };
The Initialize() method parameters:
- sy - this string parameter sets the symbol name for a custom symbol
- sy_path - string parameter that sets the symbol path property
- chart_tf - the parameter sets a period of the chart that will be opened when the symbols history has been loaded
The other two virtual methods, UpdateHistory() and AddTick(), are not implemented in CCustomSymbol. Any derived class will have to override these methods.
The CDerivSymbol class
This is where the CDerivSymbol class comes in. It inherits from CCustomSymbol and provides methods that override all the virtual methods of its parent class. Its here where we will use our WebSocket client to consume the Deriv.com API.
//+------------------------------------------------------------------+ //|Class for creating custom Deriv.com specific symbols | //+------------------------------------------------------------------+ class CDerivSymbol:public CCustomSymbol { private: //private properties string m_appID; //app id string issued by Deriv.com string m_url; //final url string m_stream_id; //stream identifier for a symbol int m_index; //array index int m_max_ticks; //max number of ticks downloadable from Deriv.com CWebsocket* websocket; //websocket client CJAVal* json; //utility json object CJAVal* symbolSpecs; //json object storing symbol specification //private methods bool CheckDerivError(CJAVal &j); bool GetSymbolSettings(void); public: //Constructor CDerivSymbol(void):m_appID(NULL), m_url(NULL), m_stream_id(NULL), m_index(-1), m_max_ticks(86400) { json=new CJAVal(); symbolSpecs=new CJAVal(); websocket=new CWebsocket(); } //Destructor ~CDerivSymbol(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 SetMaximumHistoryTicks(const int max) { m_max_ticks=(max>=5000)?max:86400; } void SetAppID(const string id); bool StartTicksStream(void); bool StopTicksStream(void); };
After an instance of the CDerivSymbol class is created, a valid application identifier app_id should be set using the SetAppID() method. Only then can we move on to initialize a custom symbol.
//+------------------------------------------------------------------+ //|sets the the application id used to consume Deriv.com api | //+------------------------------------------------------------------+ void CDerivSymbol::SetAppID(const string id) { if(m_appID!=NULL && StringCompare(id,m_appID,false)) websocket.Abort(); m_appID=id; m_url=DERIV_URL+m_appID; }
The Initialize() method uses the getSymbolSpecs() private method to get the properties of a selected symbol. The relevant information is then used to set the symbol properties for a new custom symbol.
//+------------------------------------------------------------------+ //|Begins process of creating custom symbol | //+------------------------------------------------------------------+ bool CDerivSymbol::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,"DERIV_")>=0)?StringSubstr(sy,6):sy; m_chart_tf=chart_tf; Comment("Initializing Symbol "+m_symbol_name+"......."); if(!GetSymbolSettings()) return(false); string s_path=DERIV_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); }
When the symbol has been initialized, we will need to acquire either rates or tick data to build the chart. This is done by the UpdateHistory() method. After loading the history to the terminal, a new chart for the custom symbol will be opened if it does not already exist. In the code shown below there are two versions of the UpdateHistory() method, the first one uses bar data to fill the history, whilst the second relies on tick data.
//+------------------------------------------------------------------+ //|method for updating the tick history for a particular symbol | //+------------------------------------------------------------------+ bool CDerivSymbol::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(CheckDerivError(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); } //+------------------------------------------------------------------+ //|method for updating the tick history for a particular symbol | //+------------------------------------------------------------------+ bool CDerivSymbol::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(CheckDerivError(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); }
Since the history has been updated and the chart is open, the next step is to subcribe to a tick data stream from Binary.com. The StartTicksStream() method sends the corresponding query and if successful, the server will begin streaming live quotes that will be processed by the AddTick() method. The StopTicksStream() method on the other hand is used to notify the server to stop sending live quotes.
//+---------------------------------------------------------------------+ //|method that enables the reciept of new ticks as they become available| //+---------------------------------------------------------------------+ bool CDerivSymbol::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())); } //+------------------------------------------------------------------+ //|Overridden method that handles new ticks streamed from Deriv.com | //+------------------------------------------------------------------+ void CDerivSymbol::AddTick(void) { string str_tick; MqlTick current_tick[1]; json.Clear(); if(websocket.ReadString(str_tick)) { json.Deserialize(str_tick); ZeroMemory(current_tick); if(CheckDerivError(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; } } } } //--- }
//+------------------------------------------------------------------+ //|Used to cancel all tick streams that may have been initiated | //+------------------------------------------------------------------+ bool CDerivSymbol::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); }
The code for the EA is shown below.
CDerivSymbol b_symbol; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { b_symbol.SetAppID(DERIV_appid); //--- if(!b_symbol.Initialize(EnumToString(DERIV_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(); } //+------------------------------------------------------------------+
Both EAs have the same code with the exception of the UpdateHistory() method.
Conclusion
We explored how to use the Win32 API to create a WebSocket client for MetaTrader 5. We created a class that encapsulates this functionality and demonstrated its use in the EA that interacts with the Deriv.com WebSockets API.
Folder | Contents | Description |
---|---|---|
Mql5.zip\Mql5\include | JAson.mqh, websocket.mqh, winhttp.mqh | Include files contain the code for the JSON parser (CJAval class), WebSocket client (CWebsocket class), WinHttp imported function and type declarations respectively |
Mt5zip\Mql5\experts | DerivCustomSymboWithTickHistory.mq5, DerivCustomSymbolWithBarHistory.mq5 | Sample EAs that use the CWebsocket class to create custom symbols by leveraging the Deriv.com WebSocket API |





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
Yeah, nice try :)
I was hoping this would be possible too, but if you search the forum, you'll find that function in MQL is a handle, not a memory address, which is required by "C/C++" callback-API.
Maybe someday MQL would add "real" Function Pointer.
yea i hope soon this will be natively supported
@Francis Dube Is it possible to create a MQL5 service that acts as the WebSocket server? Do you have some examples?
@Francis Dube Is it possible to create a MQL5 service that acts as the WebSocket server? Do you have some examples?
its a websocket client not server.
Hello Francisco. Super grateful for the valuable code provided. I have found that when requesting the EURUSD symbol, that is: "frxEURUSD" to DERIV, it throws the following error:
* "Matching symbol name not found, Check the symbol name on Binary.com"
Any idea why DERIV doesn't seem to recognize the symbol we requested? I got stuck in development/testing here. Thank you so much. :)