WebSocket para o MetaTrader 5: Usando a API do Windows
Introdução
Em nosso artigo WebSocket para MetaTrader 5, cobrimos os conceitos básicos do protocolo WebSocket e implementamos o cliente em MQL5. Desta vez vamos usar a API do Windows para criar um cliente WebSocket para os programas MetaTrader 5. Esta é a segunda melhor alternativa, pois não requer o uso de programas de terceiros, todos os componentes necessários estão disponíveis no sistema operacional. Executaremos o cliente como uma classe e realizaremos testes enviando os dados de tick atuais ao MetaTrader 5 usando o WebSocket API da Binary.com.
WebSocket no Windows
Ao falar em API do Windows e em Internet, convém notar que os desenvolvedores da MQL5 estão mais familiarizados com a biblioteca Windows Internet (WinINeT). Ela suporta protocolos de Internet, incluindo protocolo de transferência de arquivos (FTP) e HTTP. A biblioteca Windows HTTP Services (WinHTTP) é uma biblioteca especial para o protocolo HTTP, com funções para o desenvolvimento de aplicativos de servidor. Algumas das funções WinHTTP são projetadas para manusear conexões WebSocket.
O protocolo WebSocket é suportado nos sistemas operacionais Windows a partir do Windows 8.1 e Windows Server 2012 R2. O Windows 7 e versões anteriores não têm suporte nativo para o protocolo, por isso os programas deste artigo não funcionarão em máquinas com estes sistemas operacionais.
Biblioteca Winhttp
Para criar uma conexão cliente WebSocket, precisaremos das seguintes funções:
WinHttpOpen | Inicializa a biblioteca, preparando-a para ser utilizada pelo aplicativo |
WinHttpConnect | Define o nome de domínio do servidor com o qual o aplicativo precisa interagir. |
WinHttpOpenRequest | Cria o identificador da solicitação HTTP |
WinHttpSetOption | Define várias alternativas de configuração para conexão HTTP |
WinHttpSendRequest | Envia a solicitação para o servidor |
WinHttpReceiveResponse | Recebe uma resposta do servidor depois que a solicitação é enviada |
WinHttpWebSocketCompleteUpgrade | Confirma que a resposta do servidor corresponde ao protocolo WebSocket |
WinHttpCloseHandle | Desativa quaisquer descritores de recursos utilizados anteriormente |
WinHttpWebSocketSend | Envia dados através de uma conexão WebSocket |
WinHttpWebSocketReceive | Recebe dados usando uma conexão WebSocket |
WinHttpWebSocketClose | Fecha a conexão WebSocket |
WinHttpWebSocketQueryCloseStatus | Verifica a mensagem de status de fechamento enviada do servidor |
Todas as funções da biblioteca estão documentadas pela Microsoft. Uma descrição detalhada de todas as funções, seus parâmetros de entrada e tipos de valores de retorno pode ser encontrada nos links acima.
O cliente que estamos criando para o MetaTrader 5 funciona em modo síncrono. Isso significa que a chamada de função bloqueia a execução até que o valor retorne. Por exemplo, uma chamada do WinHttpWebSocketReceive() irá bloquear a execução até que os dados estejam disponíveis para serem lidos. Tenha isto em mente ao criar aplicativos para o MetaTrader 5.
As funções winhttp são declaradas e importadas no arquivo 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 //+------------------------------------------------------------------+
Usando as funções Winhttp
Para instalar o cliente WebSocket com as funções necessárias, primeiro precisamos chamar WinHttpOpen() para inicializar a biblioteca. Esta função retorna um identificador de sessão para chamadas posteriores a outras funções 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; }
O segundo passo é criar um identificador de conexão usando WinHttpConnect(). Aqui especificamos o endereço do servidor e o número da porta. Note que nesta etapa precisamos especificar o nome de domínio para o servidor sem esquema e caminho. Também é possível usar um endereço IP público, se for conhecido. A maioria dos erros que ocorrem com winhttp está relacionada à transmissão de um endereço de servidor formatado incorretamente. Por exemplo, se o endereço completo do servidor é como wss://ws.example.com/path, WinHttpConnect() espera apenas ws.example.com.
connectionhandle=WinHttpConnect(sessionhandle,server,Port,0); if(connectionhandle==NULL) { Print("WinHttpConnect error "+string(kernel32::GetLastError())); if(sessionhandle!=NULL) WinHttpCloseHandle(sessionhandle); return; }
Depois de criar com sucesso o identificador de conexão, nós o usamos para definir o identificador de solicitação através de WinHttpOpenRequest(). Aqui especificamos o caminho (se houver) a partir do endereço do servidor e também definimos a opção para tornar a conexão segura.
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; }
Agora que temos o identificador de solicitação correto, precisamos nos preparar para o aperto de mão, chamando 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; }
Isto permite que os cabeçalhos necessários sejam adicionados à solicitação http de acordo com o protocolo WebSocket. O aperto de mão é iniciado por meio da chamada de WinHttpSendRequest() e, em seguida, de WinHttpReceiveResponse() para confirmar que a resposta à nossa solicitação foi recebida.
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() verifica a resposta em relação ao protocolo WebSocket. Se tudo estiver bem, ele devolve o identificador WebSocket necessário.
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;
Nosso cliente WebSocket está agora pronto para operar. Podemos enviar dados com WinHttpWebSocketSend() e recebê-los com WinHttpWebSocketReceive(). O identificador da solicitação não é mais necessária porque o identificador do WebSocket já foi criado e nossa conexão http foi atualizada para uma conexão WebSocket. Podemos liberar todos os recursos associados ao idetificador da solicitação por meio da chamada de 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); } //+------------------------------------------------------------------+
A chamada de WinHttpWebSocketClose() fecha a conexão WebSocket. Após o fechamento da conexão, todos os identificadores associados devem ser desinicializados por meio da chamada de
WinHttpCloseHandle() para cada um deles.
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; }
Classe CWebsocket
O arquivo websocket.mqh conterá a classe CWebsocket, que servirá como wrapper para as funções da biblioteca winhttp necessárias para ativar o cliente WebSocket.
O arquivo começa com uma diretiva include para incluir todas as funções e declarações importadas das bibliotecas API do Windows.
#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 };
O método Connect() é chamado primeiro para iniciar uma conexão com o servidor WebSocket.
Parâmetros Connect():
- _serveraddress — endereço completo do servidor (type:string)
- _port — número da porta do servidor (type:ushort)
- _appname — parâmetro de string para identificar o aplicativo usando o cliente WebSocket. Será enviado como um dos cabeçalhos na solicitação http inicial (type:string)
- _secure — valor booleano que especifica se deve ou não ser utilizada uma conexão segura (type:boolean)
O método Connect() chama os métodos privados initialize() e upgrade(). O método privado initialize() processa o endereço completo do servidor e o divide em um nome de domínio e um caminho. Finalmente, createSessionConnection() cria uma sessão e os identificadores de conexão. O método upgrade() cria uma solicitação e os identificadores WebSocket antes de definir o novo estado de conexão de cliente.
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); }
Se o método Connect() retornar true, podemos começar a enviar dados através do cliente WebSocket. O processo é simplificado através de dois métodos.
O método SendString() aceita uma string como entrada, enquanto o método Send() aceita um array de caracteres não assinados como o único parâmetro de função. No caso de sucesso, ambos retornam true e chamam o método private clientend(), que trata de todas as operações de envio para a classe.
//+------------------------------------------------------------------+ //| 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)); }
Podemos usar Read() ou ReadString() para ler os dados enviados a partir do servidor. Os métodos retornam o tamanho dos dados recebidos. ReadString() requer uma string, passada por referência, na qual os dados recebidos serão escritos, enquanto Read() escreve em um array de caracteres não assinados.
//+------------------------------------------------------------------+ //|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); }
Quando o cliente WebSocket não for mais necessário, a conexão com o servidor pode ser fechada usando Close() ou Abort(). O método Abort() difere do Close() na medida em que não apenas fecha a conexão WebSocket, mas também redefine algumas propriedades de classe para seu estado padrão.
//+------------------------------------------------------------------+ //| 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() consulta o estado atual do cliente WebSocket
DomainName(), Port() e ServerPath() retornam, respectivamente, o nome de domínio, a porta e o caminho para a conexão atual.
LastErrorMessage() pode ser usado para recuperar o último erro como uma string detalhada, enquanto que chamar LastError() retorna o código de erro como um valor inteiro.
//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); }
Abaixo está a classe inteira.
//+------------------------------------------------------------------+ //|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); } };
Agora que temos a classe WebSocket, podemos olhar para um exemplo de como usá-la.
Testando a classe CWebsocket
Para testes, criarei um aplicativo para MetaTrader 5 que adicionará um símbolo personalizado da Binary.com. Ao ser lançado em um gráfico, o aplicativo carrega o histórico e abre um novo gráfico do símbolo personalizado, que é atualizado em tempo real a cada tick.
Estão previstas duas versões do aplicativo. BinaryCustomSymboWithTickHistory.ex5 utilizará o histórico de tick, enquanto BinaryCustomSymboWithBarHistory.ex5 carregará o histórico de barras no formato OHLC. O código de ambos os aplicativos é quase idêntico.
Binary.com tem uma API bem documentada que permite aos desenvolvedores criar interfaces que interagem com seus sistemas. A API se baseia em soquete web com solicitações e respostas em formato JSON.
O aplicativo será realizado como um EA que usa três bibliotecas:
- A primeira é websocket.mqh para processamento das conexões WebSocket,
- A segunda é JAson.mqh por trabalhar com dados em formato JSON, ela foi desenvolvida por Alexey Sergeev. Pode ser encontrada no repositório vivazzi do GitHub,
- A terceira é FileTxt.mqh para operações de arquivo.
O EA tem os seguintes parâmetros de entrada configuráveis:
- binary_appid é um parâmetro de string para acesso de aplicativo à API. O ID do aplicativo pode ser obtido seguindo as instruções no portal do desenvolvedor. A assinatura dos dados por símbolo não requer autorização no Binary.com, portanto não é necessário nenhum token API.
- binary_symbol é uma enumeração que permite aos usuários selecionar o símbolo a ser importado para o MetaTrader 5.
- binary_timeframe é o período do gráfico que se abre após o carregamento e a adição do histórico no 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
O EA será composto por duas classes - CCustomSymbol e CBinarySymbol.
Classe CCustomSymbol
CCustomSymbol é uma classe para manipulação de símbolos personalizados de fontes externas. A classe é baseada na biblioteca SYMBOL elaborada por fxsaber. Entre outras coisas, a classe fornece métodos para recuperar e manipular as propriedades dos símbolos, e para abrir e fechar os gráficos correspondentes. O mais importante é que ela dispõe de três métodos virtuais que podem ser substituídos por classes filhas para poder acrescentar um símbolo personalizado de forma mais flexível.
//+------------------------------------------------------------------+ //|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); } };
Parâmetros do método Initialize():
- sy - este parâmetro de string define o nome do símbolo personalizado
- sy_path - parâmetro de string que define a propriedade de caminho do símbolo
- chart_tf - este parâmetro define o período gráfico a ser aberto após o histórico de símbolos ser carregado
Os dois métodos virtuais restantes, UpdateHistory() e AddTick(), não são adicionados ao CCustomSymbol. Qualquer classe derivada precisaria substituir estes métodos.
Classe CBinarySymbol
É aqui que entra em jogo a classe CBinarySymbol. Ela é herdada do CCustomSymbol e fornece métodos que substituem os métodos virtuais de sua classe mãe. É aqui que utilizaremos o cliente WebSocket para aplicar a API do Binary.com.
//+------------------------------------------------------------------+ //|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); };
Depois de criar uma cópia da classe CBinarySymbol, precisamos definir o identificador app_id correto usando o método SetAppID(). Somente então poderemos inicializar o símbolo personalizado.
//+------------------------------------------------------------------+ //|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; }
O método Initialize() usa o método privado getSymbolSpecs() para obter as propriedades do símbolo selecionado. As informações necessárias são então utilizadas para definir as propriedades do novo símbolo personalizado.
//+------------------------------------------------------------------+ //|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); }
Depois de inicializar um símbolo, precisamos obter preços ou dados para o gráfico. O método UpdateHistory() é responsável por isso. Após o histórico ter sido carregado no terminal, um novo gráfico do símbolo personalizado é aberto, se ele ainda não existir. O código abaixo mostra duas variantes do método UpdateHistory(): a primeira utiliza dados de barras para preencher o histórico, a segunda se baseia em dados de tick.
//+------------------------------------------------------------------+ //|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); }
Após atualizar o histórico e abrir o gráfico, você precisa assinar e receber os dados de tick da Binary.com. O método StartTicksStream() envia uma solicitação correspondente. Se tiver sucesso, o servidor começa a enviar a cotação em tempo real, que é então processada usando o método AddTick(). O método StopTicksStream(), por sua vez, notifica o servidor para parar de enviar as cotações.
//+---------------------------------------------------------------------+ //|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; } } } } //--- }
Abaixo está o código do Expert Advisor.
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(); } //+------------------------------------------------------------------+
Ambos EAs têm código similar, exceto pelo método UpdateHistory().
Conclusão
Usamos o Win32 API para criar um cliente WebSocket para o MetaTrader 5. Criamos uma classe que encapsula esta funcionalidade e demonstramos seu uso em um EA que interage com o WebSockets API da Binary.com.
Pasta | Sumário | Descrição |
---|---|---|
MT5zip\Mt5zip\Mql5\include | JAson.mqh, websocket.mqh, winhttp.mqh | Os arquivos Include contêm o código para o analisador JSON (classe CJAval), o cliente WebSocket (classe CWebsocket), a função de importação WinHttp e as declarações de tipos |
MT5zip\ Mt5zip\Mql5\ Experts | BinaryCustomSymboWithTickHistory.mq5, BinaryCustomSymbolWithBarHistory.mq5 | Experts que usam a classe CWebsocket para criar símbolos personalizados usando o WebSocket API da Binary.com |
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/10275
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
Eu usei typedef para definir o ponteiro para a função desta maneira
então eu importei o WinHttpSetStatusCallBack() da winhttp.dll de acordo com a msdn
Sim, boa tentativa :)
Eu também esperava que isso fosse possível, mas se você pesquisar no fórum, verá que a função no MQL é um cabo, não um endereço de memória, o que é exigido pelo "C/C++" calllback-API.
Talvez um dia a MQL acrescentaria o "real" Ponteiro de Função.
Sim, boa tentativa :)
Eu também esperava que isso fosse possível, mas se você pesquisar no fórum, verá que a função no MQL é um cabo, não um endereço de memória, o que é exigido pelo "C/C++" calllback-API.
Talvez um dia a MQL acrescentaria o "real" Ponteiro de Função.
Sim, espero que em breve isto seja suportado nativamente.
@Francis Dube É possível criar um serviço MQL5 que atua como servidor WebSocket? Você tem alguns exemplos?
@Francis Dube É possível criar um serviço MQL5 que atua como servidor WebSocket? Você tem alguns exemplos?
é um cliente websocket e não um servidor.