![Anotação de dados na análise de série temporal (Parte 5): Aplicação e teste de um EA usando Socket](https://c.mql5.com/2/64/Data_label_for_time_series_miningcPart_5c_Apply_and_Test_in_EA_Using_Socket_600x314.jpg)
Anotação de dados na análise de série temporal (Parte 5): Aplicação e teste de um EA usando Socket
Introdução
Nos artigos anteriores, discutimos como anotar dados de acordo com nossas próprias necessidades e usá-los para treinar modelos de previsão de séries temporais. Mas como utilizar esses modelos da melhor forma? É hora de discutir como testar nossos modelos criados durante o teste histórico no MetaTrader 5 e incluí-los no nosso EA. No EA, precisamos de uma estratégia como lógica principal, e uma estratégia real e utilizável requer uma base teórica concreta, além de muitas verificações e ajustes para garantir sua confiabilidade.
A estratégia apresentada no artigo é muito simples e serve apenas como exemplo demonstrativo. Não a utilize em negociações reais! Claro, com o suporte de muitas bibliotecas diferentes, você também pode fazer esse trabalho com Python, mas o MetaTrader 5 oferece uma ferramenta prática e abrangente para testes históricos e pode modelar com mais precisão nosso ambiente de negociação, então ainda escolhemos o cliente MetaTrader 5 como a plataforma para testes históricos. Porém, como nosso ambiente de criação de modelos é o Python, os testes históricos no MetaTrader 5 precisam ser implementados com MQL5, o que complica um pouco a implementação dos testes, mas temos uma solução. Vamos discutir três métodos diferentes para testar nossos modelos históricos no ambiente MetaTrader 5, para nos ajudar a melhorar e aprimorar sua qualidade. Neste artigo, discutiremos o método WebSocket. Os outros serão abordados em artigos seguintes.
Conteúdo:
- Introdução
- Princípio de implementação
- Implementação das funções do servidor Python
- Implementação das funções do cliente MQL5
- Teste histórico
- Considerações finais
Princípio de implementação
Primeiro, adicionamos uma instância de servidor web ao nosso script Python e adicionamos a ele a saída do nosso modelo. Em seguida, usamos o MQL5 para criar um cliente web para solicitar o serviço de inferência no servidor.
Vamos supor que a lógica do EA funcione da seguinte maneira:
- Primeiro, toda vez que o evento OnTick() for acionado, os últimos 300 dados do histograma são enviados para o servidor através do cliente.
- Após receber a informação, o servidor envia a previsão da tendência dos próximos 6 histogramas para o cliente do EA usando a inferência do modelo. Aqui, utilizamos o modelo Nbeats, mencionado no artigo anterior, pois ele pode decompor a previsão em tendências.
- Se a tendência for de queda, vende-se; se a tendência for de alta, compra-se.
Implementação das funções do servidor Python
O socket em Python inclui basicamente as seguintes funções:
- socket.bind(): associa um endereço (host, porta) ao socket. No AF_INET, o endereço é representado por uma tupla (host, porta).
- socket.listen(): inicia a escuta TCP. backlog indica o número máximo de conexões que o sistema operacional pode suspender antes de rejeitar a conexão. O valor é pelo menos 1, a maioria dos aplicativos define como 5.
- socket.accept(): aceita passivamente a conexão do cliente TCP, (bloqueando) espera pela conexão.
- socket.connect(): inicia ativamente uma conexão com o servidor TCP. Normalmente, o formato do endereço é uma tupla (nome do host, porta). Se a conexão falhar, retorna o erro socket.error.
- socket.connect_ex(): versão expandida da função connect(). Retorna o código de erro em caso de falha, em vez de gerar uma exceção. socket.recv(): recebe dados TCP, os dados são retornados como uma string, bufsize define o máximo volume de dados a ser recebido. A flag fornece informações adicionais sobre a mensagem, que geralmente podem ser ignoradas.
- socket.send(): envia dados TCP, envia os dados na forma de uma string para o socket conectado. O valor retornado é a quantidade de bytes enviados, que pode ser menor que o tamanho da string em bytes.
- socket.sendall(): envia completamente os dados TCP. Os dados são enviados para o socket conectado como uma string, tentando enviar todos os dados antes de retornar. Retorna None em caso de sucesso ou gera uma exceção em caso de falha.
- socket.recvfrom(): recebe dados UDP, semelhante ao recv(), mas o valor retornado é (data, address). Aqui, data é uma string contendo os dados recebidos, e address é o endereço do socket que enviou os dados.
- socket.sendto(): envia dados UDP, envia dados para um socket, o endereço é uma tupla (ipaddr, port), definindo o endereço remoto. O valor retornado é a quantidade de bytes enviados.
- socket.close(): fecha o socket.
- socket.getpeername(): retorna o endereço remoto ao qual o socket está conectado. O valor retornado geralmente é uma tupla (ipaddr, port).
- socket.getsockname(): retorna o próprio endereço do socket. Geralmente na forma de uma tupla (ipaddr, port).
- socket.setsockopt(level, optname, value): define o valor da opção do socket.
- socket.getsockopt(level, optname[.buflen]): retorna o valor do parâmetro do socket.
- socket.settimeout(timeout): define o tempo limite para operações com sockets, o tempo limite é um número de ponto flutuante em segundos. O valor None significa que não há tempo limite. Geralmente, o tempo limite deve ser definido se o socket foi criado recentemente, pois ele pode ser usado para operações de conexão (por exemplo, Connect()).
- socket.gettimeout(): retorna o valor atual do tempo limite em segundos ou None se o tempo limite não estiver definido.
- socket.fileno(): retorna o descritor de arquivo do socket.
- socket.setblocking(flag): se a flag for 0, define o socket para o modo não bloqueante; caso contrário, define o socket para o modo bloqueante (valor padrão). No modo não bloqueante, se ao chamar recv() não houver dados ou se a chamada send() não puder enviar dados imediatamente, isso gerará uma exceção socket.error.
- socket.makefile(): cria um arquivo associado ao socket.
1. Importação dos pacotes necessários
A implementação da classe não requer a instalação de pacotes adicionais, pois a biblioteca de sockets geralmente é incluída por padrão (no ambiente conda). Se parecer que avisos de mensagens são muitos, você pode adicionar o módulo de avisos e usar o comando warnings.filterwarnings("ignore"). Ao mesmo tempo, também precisamos definir algumas variáveis globais necessárias:
- max_encoder_length=96
- max_prediction_length=20
- info_file="results.json"
Essas variáveis globais são definidas com base no modelo que treinamos no artigo anterior.
O código em si:
import socket import json from time import sleep import pandas as pd import numpy as np import warnings from pytorch_forecasting import NBeats warnings.filterwarnings("ignore") max_encoder_length=96 max_prediction_length=20 info_file="results.json"
2. Criação da classe do servidor
Crie uma classe de servidor, na qual inicializamos algumas configurações básicas do socket, incluindo as seguintes funções:
socket.socket(): definimos dois parâmetros como socket.AF_INET e socket.SOCK_STREAM.
Método bind() do socket.socket(): a função define o parâmetro host como "127.0.0.1" e o parâmetro port como "8989". O host não é recomendado mudar, mas o porto pode ser ajustado para outro valor se 8989 estiver ocupado.
O modelo será apresentado mais tarde, por isso o inicializamos temporariamente como None.
Precisamos escutar a porta do servidor: self.sk.listen(1), aceitar passivamente as conexões TCP do cliente e esperar pelas conexões: self.sk_, self.ad_ = self.sock.accept(). Executamos essas tarefas durante a inicialização da classe para evitar reinicializações ao receber informações repetidamente.
class server_: def __init__(self, host = '127.0.0.1', port = 8989): self.sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.host = host self.port = port self.sk.bind((self.host, self.port)) self.re = '' self.model=None self.stop=None self.sk.listen(1) self.sk_, self.ad_ = self.sk.accept() print('server running:',self.sk_, self.ad_)
Nota: Se você implementar o servidor no Docker ou em um contêiner similar ao Docker, talvez precise definir o host como "0.0.0.0", caso contrário, seu cliente não encontrará o servidor.
3. Lógica de processamento da informação recebida
Definimos o método da classe msg() para processar a informação recebida, usando um loop while para lidar com os dados recebidos. Aqui, vale notar uma coisa: os dados recebidos precisam ser decodificados usando decode("utf-8") e, em seguida, a informação processada é enviada para a função de processamento de inferência self.sk_.send(bytes(eva(self.re), "utf-8")), onde a função de inferência é definida como eva(), e o parâmetro é a informação recebida, que implementaremos mais tarde. Precisamos garantir que nosso servidor também pare quando o teste do EA no histórico for interrompido, caso contrário, ele continuará consumindo recursos em segundo plano. Podemos fazer isso enviando a string "stop" para o servidor após o EA terminar seu trabalho e, se recebermos essa string, permitimos que o servidor pare o loop e encerre o processo. Já adicionamos esse atributo na inicialização da classe do servidor, e só precisamos definir seu valor como true quando recebermos esse sinal.
def msg(self): self.re = '' while True: data = self.sk_.recv(2374) if not data: break data=data.decode("utf-8") # print(len(data)) if data=="stop": self.stop=True break self.re+=data bt=eva(self.re, self.model) bt=bytes(bt, "utf-8") self.sk_.send(bt) return self.re
Nota: Neste exemplo, definimos o parâmetro self.sk_.recv(2374) como 2374, que corresponde ao comprimento de 300 números de ponto flutuante. Se você notar que os dados recebidos estão incompletos, pode alterar esse valor.
4. Devolução de recursos
Após parar o servidor, precisamos liberar os recursos.
def __del__(self):
print("server closed!")
self.sk_.close()
self.ad_.close()
self.sock.close()
5. Definição da lógica de inferência
A lógica de inferência deste exemplo é bem simples. Nós apenas carregamos o modelo e usamos o histograma fornecido pelo cliente para prever os resultados, depois os dividimos em tendências e enviamos os resultados de volta para o cliente. Aqui, precisamos nos atentar ao fato de que podemos inicializar o modelo ao inicializar a classe do servidor, para que o modelo esteja pré-carregado e pronto para inferência a qualquer momento.
Primeiro, definimos uma função para carregar o modelo e depois chamamos essa função na inicialização da classe do servidor para obter uma instância do modelo. No artigo anterior, apresentamos como salvar e carregar o modelo. Após o treinamento, o modelo salva as informações em um arquivo JSON, results.json, no diretório raiz da pasta. Podemos ler e carregar o modelo. Naturalmente, nosso arquivo server.py também deve estar no diretório raiz da pasta.def load_model(): with open(info_file) as f: m_p=json.load(fp=f)['last_best_model'] model = NBeats.load_from_checkpoint(m_p) return modelDepois, adicionamos a função init() à classe server_(): self.model=load_model() para inicializar, e em seguida passamos o modelo inicializado para a função de inferência.
def __init__(self, host = '127.0.0.1', port = 8989): self.sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.host = host self.port = port self.sk.bind((self.host, self.port)) self.re = '' self.model=load_model() self.stop=None self.sk.listen(1) self.sk_, self.ad_ = self.sk.accept() print('server running:',self.sk_, self.ad_)
Vamos então finalizar nossa função de inferência.
Aqui, é importante observar que o formato dos dados que precisam ser inseridos no modelo deve ser um DataFrame, então primeiro precisamos converter os dados recebidos em um array numpy: msg=np.fromstring(msg, dtype=float, sep=','), e depois para DataFrame: dt=pd.DataFrame(msg). Após concluir a inferência, o resultado é retornado. Definimos que, se o último valor da tendência for maior que a média, isso indica uma tendência de alta; caso contrário, uma tendência de baixa. Se a tendência for de alta, retornamos "buy", se for de baixa, "sell". O processo de inferência não é abordado neste artigo. Você pode ler sobre isso nas partes anteriores da série. Aqui, precisamos destacar mais um ponto. Como definimos o preditor do modelo como a coluna "close" do DataFrame, precisamos adicionar a coluna "close" aos dados convertidos em DataFrame: dt['close']=dt.
def eva(msg,model): offset=1 msg=np.fromstring(msg, dtype=float, sep= ',') # print(msg) dt=pd.DataFrame(msg) dt=dt.iloc[-max_encoder_length-offset:-offset,:] last_=dt.iloc[-1] for i in range(1,max_prediction_length+1): dt.loc[dt.index[-1]+1]=last_ dt['close']=dt dt['series']=0 dt['time_idx']=dt.index-dt.index[0] print(dt) predictions = model.predict(dt, mode='raw',trainer_kwargs=dict(accelerator="cpu",logger=False),return_x=True) trend =predictions.output["trend"][0].detach().cpu() if (trend[-1]-trend.mean()) >= 0: return "buy" else: return "sell"
Em seguida, precisamos adicionar o loop principal.
Primeiro, inicializamos a classe de serviço e depois adicionamos a função de processamento de informações no loop while. Finalizamos o loop e saímos do programa quando recebemos o sinal de parada. Não queremos que o loop execute muito rápido, então adicionamos Sleep(0.5) para limitar a velocidade do loop e evitar alta carga de CPU.
while True: rem=sv.msg() if sv.stop: break sleep(0.5)Criamos um servidor simples, agora precisamos implementar o cliente no EA.
Implementação das funções do cliente MQL5
1. Funções do socket no MQL5
O módulo socket atualmente inclui as seguintes funções:
- SocketCreate: cria um socket com o identificador especificado e retorna seu handle.
- SocketClose: fecha o socket.
- SocketConnect: realiza a conexão com o servidor controlando o timeout.
- SocketIsConnected: verifica se o socket está conectado no momento.
- SocketIsReadable: obtém a quantidade de bytes que podem ser lidos do socket.
- SocketIsWritable: verifica se é possível escrever dados no socket no momento.
- SocketTimeouts: define os timeouts de recebimento e envio de dados para o objeto de sistema do socket.
- SocketRead: lê dados do socket.
- SocketSend: grava dados no socket.
- SocketTlsHandshake: inicia uma conexão segura TLS (SSL) com o host especificado usando o protocolo TLS Handshake.
- SocketTlsCertificate: obtém dados sobre o certificado usado para proteger a conexão de rede.
- SocketTlsRead: lê dados de uma conexão TLS segura.
- SocketTlsReadAvailable: lê todos os dados disponíveis de uma conexão TLS segura.
- SocketTlsSend: envia dados através de uma conexão TLS segura.
Utilizando esses métodos, podemos facilmente adicionar funcionalidades adicionais do lado do cliente.
2. Implementação das funções do EA
Primeiro, vamos considerar a lógica funcional do EA:
Inicializamos o socket no "int OnInit()".
Em seguida, no "void OnTick()", implementamos a recepção de dados do cliente e o envio dos dados atuais do histograma para o cliente, bem como a lógica de teste histórico do nosso EA.
No "void OnDeinit(const int Reason)" precisamos enviar a mensagem "stop" para o servidor e fechar o socket.3. Inicialização do EA
Primeiro, precisamos definir uma variável global "int sk", que é usada para obter o handle após a criação do socket.
Na função OnInit(), usamos SocketCreate() para criar o cliente: int sk=SocketCreate().
Em seguida, definimos o endereço do nosso servidor: string host="127.0.0.1";Porta do servidor: int port=8989;
Já mencionamos o valor 300 como o comprimento dos dados enviados: int data_len=300;
Na função OnInit(), precisamos avaliar a situação de inicialização. Se a criação falhar, a inicialização também falhará.
Depois, criamos a conexão com o servidor SocketConnect(sk, host, port, 1000), onde a porta deve corresponder ao lado do servidor. Se a conexão falhar, a inicialização terminará sem sucesso.
int sk=-1; string host="127.0.0.1"; int port= 8989; int OnInit() { //--- sk=SocketCreate(); Print(sk); Print(GetLastError()); if (sk==INVALID_HANDLE) { Print("Failed to create socket"); return INIT_FAILED; } if (!SocketConnect(sk,host, port,1000)) { Print("Failed to connect to server"); return INIT_FAILED; } //--- return(INIT_SUCCEEDED); }
void OnDeinit(const int reason) { socket.Disconnect(); }
4. Lógica de negociação
Aqui, precisamos definir a lógica principal de processamento de dados e a lógica de negociação no void OnTick().
Crie as variáveis "MqlTradeRequest request" e "MqlTradeResult result" para executar as tarefas de ordem;
Crie a variável de array de caracteres "char Recv_data[]" para receber informações do servidor;
Crie a variável de array duplo "double priceData[300]" para copiar os dados do gráfico;
Crie as variáveis "string dataToSend" e "char ds[]" para converter o array duplo em um array de caracteres que pode ser usado pelo socket;
Primeiro, precisamos copiar os dados do gráfico para envio: int nc=CopyClose(Symbol(),0,0,data_len,priceData);
Depois, convertemos os dados para o formato string: for(int i=0;i<ArraySize(priceData);i++) dataToSend+=(string)priceData[i]+",", usamos "," para separar os dados;
Usamos "int dsl=StringToCharArray(dataToSend,ds)" para converter os dados em string para um array de caracteres que pode ser usado pelo socket.
Após converter os dados, precisamos usar SocketIsWritable(sk) para determinar se nosso socket pode enviar dados. Se puder, use SocketSend(sk, ds, dsl) para enviar os dados.
Também precisamos ler as informações do servidor. Usamos "uint len=SocketIsReadable(sk)" para verificar se há dados disponíveis na porta atual. Se a informação não estiver vazia, executamos a lógica de negociação: int rsp_len=SocketRead(sk, recv_data, len, 500), "len" é o tamanho do buffer, "500" é o timeout (em milissegundos).
Se recebermos "buy", abrimos uma ordem de compra, configurando a solicitação da seguinte forma:
- Zerar a estrutura da solicitação de negociação: ZeroMemory(request)
- Definir a execução imediata do comando de negociação: request.action = TRADE_ACTION_DEAL
- Definir o par de moedas da negociação: request.symbol = Symbol()
- Volume da ordem: request.volume = 0.1
- Tipo de ordem: request.type = ORDER_TYPE_BUY
- Para a função SymbolInfoDouble, são necessários 2 parâmetros de entrada: o primeiro é a string do par de moedas, o segundo é o tipo na enumeração NUM_SYMBOL_INFO_DOUBLE: request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK)
- Desvio permitido da negociação: request.deviation = 5
- Em seguida, enviamos a ordem de negociação: OrderSend(request, result)
Se recebermos "sell", abrimos uma ordem de venda, configurando a solicitação da seguinte forma (configurações semelhantes às da ordem de compra, aqui não são detalhadas):
- ZeroMemory(request)
- request.action = TRADE_ACTION_DEAL
- request.symbol = Symbol()
- request.volume = 0.1
- request.type = ORDER_TYPE_SELL
- request.price = SymbolInfoDouble(Symbol(), SYMBOL_BID)
- request.deviation = 5
- Em seguida, enviamos a ordem de negociação: OrderSend(request, result)
Aqui, para evitar problemas com o código de teste, comentamos a função real de envio de ordens e a ativamos no backtest.
Código completo:
void OnTick() { MqlTradeRequest request; MqlTradeResult result; char recv_data[]; double priceData[300]; string dataToSend; char ds[]; int nc=CopyClose(Symbol(),0,0,300,priceData); for(int i=0;i<ArraySize(priceData);i++) dataToSend+=(string)priceData[i]+","; int dsl=StringToCharArray(dataToSend,ds); if (SocketIsWritable(sk)) { Print("Send data:",dsl); int ssl=SocketSend(sk,ds,dsl); } uint len=SocketIsReadable(sk); if (len) { int rsp_len=SocketRead(sk,recv_data,len,500); if(rsp_len>0) { string result; result+=CharArrayToString(recv_data,0,rsp_len); Print("The predicted value is:",result); if (StringFind(result,"buy")) { ZeroMemory(request); request.action = TRADE_ACTION_DEAL; request.symbol = Symbol(); request.volume = 0.1; request.type = ORDER_TYPE_BUY; request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK); request.deviation = 5; //OrderSend(request, result); } else{ ZeroMemory(request); request.action = TRADE_ACTION_DEAL; request.symbol = Symbol(); request.volume = 0.1; request.type = ORDER_TYPE_SELL; request.price = SymbolInfoDouble(Symbol(), SYMBOL_BID); request.deviation = 5; //OrderSend(request, result); } } } }
Nota: O parâmetro buffer_maxlen na função SocketSend() deve corresponder à configuração do servidor. Esse valor será automaticamente calculado e retornado ao executar a função StringToCharArray().
Agora, primeiro executamos o server.py, e depois adicionamos o EA ao gráfico no cliente MetaTrader 5. Os resultados são os seguintes:
Teste histórico
Mencionamos anteriormente as limitações dos sockets no MQL5, e agora precisamos adicionar suporte a WebSockets tanto no arquivo MQL5 quanto no arquivo Python.
1. Adicionando suporte a WebSockets no cliente
Durante o teste histórico, podemos usar winhttp.mqh no Windows API para alcançar a funcionalidade desejada. Uma introdução detalhada ao API está aqui:
Documentação oficial da Microsoft: https://docs.microsoft.com/pt-br/windows/win32/winhttp/winhttp-functions. Aqui listarei apenas as funções principais:
- WinHttpOpen(): inicializa a biblioteca e a prepara para uso pela aplicação
- WinHttpConnect(): define o nome de domínio do servidor com o qual a aplicação precisa interagir
- WinHttpOpenRequest(): cria um handle para a solicitação HTTP
- WinHttpSetOption: define várias opções de configuração para a conexão HTTP
- WinHttpSendRequest: envia a solicitação ao servidor
- WinHttpReceiveResponse: recebe a resposta do servidor após enviar a solicitação
- WinHttpWebSocketCompleteUpgrade: confirma que a resposta do servidor está de acordo com o protocolo WebSocket
- WinHttpCloseHandle: Desconecta quaisquer descritores de recursos usados 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 pelo servidor
Baixe o arquivo winhttp.mqh e copie-o para a pasta Include\WinAPI\ no diretório de dados do cliente. Agora, vamos finalizar o código.
Adicione as variáveis handle que precisamos usar nas variáveis globais "HINTERNET ses_h, cnt_h, re_h, ws_h" e inicializá-las no OnInit():
- Primeiro, evitaremos números aleatórios, definindo-os como NULL: ses_h=cnt_h=re_h=ws_h=NULL;
- Depois, iniciaremos a sessão HTTP: ses_h=WinHttpOpen("MT5", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, NULL, NULL, 0). Se falhar, a inicialização também falhará;
- Conecte-se ao servidor: cnt_h=WinHttpConnect(ses_h, host, port, 0). Se falhar, a inicialização falhará;
- Inicialize a solicitação: re_h=WinHttpOpenRequest(cnt_h, "GET", NULL, NULL, NULL, NULL, 0). Se houver erro, a inicialização falhará;
- Configuramos o WebSocket: WinHttpSetOption(re_h, WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, nullpointer, 0). Se houver erro, a inicialização falhará;
- Envie a solicitação para estabelecer a conexão via WebSocket: WinHttpSendRequest(re_h, NULL, 0, nullpointer, 0, 0, 0). Se houver erro, a inicialização falhará;
- Receba a resposta do handshake do servidor: WinHttpReceiveResponse(re_h, nullpointer). Se houver erro, a inicialização falhará;
- Atualizamos o WebSocket, obtemos o handle após a inicialização: WinHttpWebSocketCompleteUpgrade(re_h, nv). Se houver erro, a inicialização falhará;
- Após concluir a atualização, o descritor original da solicitação não é mais necessário, então o fechamos: WinHttpCloseHandle(re_h);
Assim, completamos todo o processo de conexão entre o cliente e o servidor. Esses processos devem ser executados em ordem estrita, e precisamos comentar as configurações originais do operador de falha de inicialização, pois sempre estarão ativas durante o teste histórico e resultarão em erro de inicialização.
int sk=-1; string host="127.0.0.1"; int port= 8989; int data_len=300; HINTERNET ses_h,cnt_h,re_h,ws_h; int OnInit() { //--- ses_h=cnt_h=re_h=ws_h=NULL; ses_h=WinHttpOpen("MT5", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, NULL, NULL, 0); Print(ses_h); if (ses_h==NULL){ Print("Http open failed!"); return INIT_FAILED; } cnt_h=WinHttpConnect(ses_h, host, port, 0); Print(cnt_h); if (cnt_h==-1){ Print("Http connect failed!"); return INIT_FAILED; } re_h=WinHttpOpenRequest(cnt_h, "GET", NULL, NULL, NULL, NULL, 0); if(re_h==NULL){ Print("Request open failed!"); return INIT_FAILED; } uchar nullpointer[]= {}; if(!WinHttpSetOption(re_h,WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,nullpointer,0)) { Print("Set web socket failed!"); return INIT_FAILED; } bool br; br = WinHttpSendRequest( re_h, NULL, 0, nullpointer, 0, 0, 0); if (!br) { Print("send request failed!"); return INIT_FAILED; } br=WinHttpReceiveResponse(re_h,nullpointer); if (!br) { Print("receive response failed!",string(kernel32::GetLastError())); return INIT_FAILED; } ulong nv=0; ws_h=WinHttpWebSocketCompleteUpgrade(re_h,nv); if (!ws_h) { Print("Web socket upgrade failed!",string(kernel32::GetLastError())); return INIT_FAILED; } WinHttpCloseHandle(re_h); re_h=NULL; sk=SocketCreate(); Print(sk); Print(GetLastError()); if (sk==INVALID_HANDLE) { Print("Failed to create socket"); //return INIT_FAILED; } if (!SocketConnect(sk,host, port,1000)) { Print("Failed to connect to server"); //return INIT_FAILED; } //--- return(INIT_SUCCEEDED); }
Depois, adicionamos o código lógico necessário à função OnTick().
Primeiro, precisamos determinar em qual ambiente estamos trabalhando, já que definimos uma variável global de descritor de socket. Podemos distinguir se estamos em condições normais ou em estado de teste avaliando se o socket foi inicializado com sucesso. Portanto, a mensagem "sk! =-1" como true significa que a inicialização do socket foi bem-sucedida, e essa parte do código não precisa ser alterada. Se "sk!=-1" não for true, precisamos ajustar a lógica do WebSocket:
- Primeiro, enviamos dados ao servidor: WinHttpWebSocketSend(ws_h, WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE, ds, dsl). Se esse processo for bem-sucedido, o valor retornado da função será 0; caso contrário, retornará o código de erro correspondente.
- Em caso de sucesso, limpe a variável de dados recebidos: ZeroMemory(recv_data).
- Recebemos os dados: get=WinHttpWebSocketReceive(ws_h, recv_data, ArraySize(recv_data), rb, st). Se os dados forem recebidos com sucesso, o valor retornado será 0; caso contrário, retornará um código de erro.
- Se os dados forem recebidos, decodificamos: pre+=CharArrayToString(recv_data,0)
Se o servidor nos enviar "buy", abrimos uma ordem de compra, caso contrário, abrimos uma ordem de venda. A diferença é que também adicionamos lógica adicional de avaliação: se a ordem já existir, primeiro determinamos se há uma ordem pendente "numt=PositionsTotal()>0". Se sim, obtemos o tipo de ordem: tpt=OrderGetInteger(ORDER_TYPE), então verificamos se o tipo de ordem é ORDER_TYPE_SELL ou ORDER_TYPE_BUY. Se o tipo de ordem corresponder à tendência enviada pelo servidor, não precisamos fazer nada. Se o tipo de ordem for contrário à tendência, fechamos a ordem atual e abrimos uma nova ordem correspondente à tendência.
Usamos os dados do servidor "buy" como exemplo para mostrar esse processo.
Se tpt==ORDER_TYPE_BUY, retornamos diretamente; se tpt==ORDER_TYPE_SELL, significa que há uma ordem de venda, então configuramos: request.order=tik, set: request.action=TRADE_ACTION_REMOVE, ao executar OrderSend(request, result), a ordem de venda é fechada.
Se não houver ordem, configuramos:
- request.action = TRADE_ACTION_DEAL;
- request.action = TRADE_ACTION_DEAL;
- request.symbol = Symbol();
- request.volume = 0.1;
- request.type = ORDER_TYPE_BUY;
- request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
- request.deviation = 5;
- request.type_filling=ORDER_FILLING_IOC;
Ao executar OrderSend(request, result), uma ordem de compra será aberta. Da mesma forma, se for "sell", a configuração da ordem ocorre da mesma forma e não será detalhada nesta parte.
void OnTick() { //--- MqlTradeRequest request; MqlTradeResult result; char recv_data[5]; double priceData[300]; string dataToSend; char ds[]; int nc=CopyClose(Symbol(),0,0,data_len,priceData); for(int i=0;i<ArraySize(priceData);i++) dataToSend+=(string)priceData[i]+","; int dsl=StringToCharArray(dataToSend,ds); if (sk!=-1) { if (SocketIsWritable(sk)) { Print("Send data:",dsl); int ssl=SocketSend(sk,ds,dsl); } uint len=SocketIsReadable(sk); if (len) { int rsp_len=SocketRead(sk,recv_data,len,500); if(rsp_len>0) { string result=NULL; result+=CharArrayToString(recv_data,0,rsp_len); Print("The predicted value is:",result); if (StringFind(result,"buy")) { ZeroMemory(request); request.action = TRADE_ACTION_DEAL; request.symbol = "EURUSD"; request.volume = 0.1; request.type = ORDER_TYPE_BUY; request.price = SymbolInfoDouble("EURUSD", SYMBOL_ASK); request.deviation = 5; //OrderSend(request, result); } else{ ZeroMemory(request); request.action = TRADE_ACTION_DEAL; request.symbol = "EURUSD"; request.volume = 0.1; request.type = ORDER_TYPE_SELL; request.price = SymbolInfoDouble("EURUSD", SYMBOL_BID); request.deviation = 5; //OrderSend(request, result); } } } } else { ulong send=0; if (ws_h) { send=WinHttpWebSocketSend(ws_h, WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE, ds, dsl); //Print("Send data failed!",string(kernel32::GetLastError())); if(!send) { ZeroMemory(recv_data); ulong rb=0; WINHTTP_WEB_SOCKET_BUFFER_TYPE st=-1; ulong get=WinHttpWebSocketReceive(ws_h,recv_data,ArraySize(recv_data),rb,st); if (!get) { string pre=NULL; pre+=CharArrayToString(recv_data,0); Print("The predicted value is:",pre); ulong numt=0; ulong tik=0; bool sod=false; ulong tpt=-1; numt=PositionsTotal(); if (numt>0) { tik=OrderGetTicket(numt-1); sod=OrderSelect(tik); tpt=OrderGetInteger(ORDER_TYPE);//ORDER_TYPE_BUY or ORDER_TYPE_SELL } if (pre=="buy") { if (tpt==ORDER_TYPE_BUY) return; else if(tpt==ORDER_TYPE_SELL) { request.order=tik; request.action=TRADE_ACTION_REMOVE; Print("Close sell order."); } else{ ZeroMemory(request); request.action = TRADE_ACTION_DEAL; request.symbol = Symbol(); request.volume = 1; request.type = ORDER_TYPE_BUY; request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK); request.deviation = 5; request.type_filling=ORDER_FILLING_IOC; Print("Open buy order."); } OrderSend(request, result); } else{ if (tpt==ORDER_TYPE_SELL) return; else if(tpt==ORDER_TYPE_BUY) { request.order=tik; request.action=TRADE_ACTION_REMOVE; Print("Close buy order."); } else{ ZeroMemory(request); request.action = TRADE_ACTION_DEAL; request.symbol = Symbol(); request.volume = 1; request.type = ORDER_TYPE_SELL; request.price = SymbolInfoDouble(Symbol(), SYMBOL_BID); request.deviation = 5; request.type_filling=ORDER_FILLING_IOC; Print("OPen sell order."); } OrderSend(request, result); } } } } } }
Neste ponto, concluímos a configuração do nosso cliente WebSocket MQL5.
2. Configuração no lado do servidor
Precisamos adicionar suporte a WebSockets no server.py.
Primeiro, precisamos importar as bibliotecas necessárias.import base64
import hashlib
import struct
O trabalho principal é realizado pela função msg(self) da classe de servidor: Primeiro, adicionamos a variável de flag de WebSocket wsk=False e, em seguida, determinamos se os dados que recebemos são mascarados ou não.
Se forem mascarados, o bit mais significativo do segundo byte dos dados será 1, e precisamos determinar apenas o valor (data[1] & 0x80) >> 7.
Se não forem mascarados, basta usar data.decode("utf-8").
Se os dados forem mascarados, precisamos encontrar a chave de máscara: mask = data[4:8] e os dados de payload: payload = data[8:], e então desmascarar os dados: for i in range(len(payload)):message += chr(payload[i] ^ mask[i % 4]) e definir a variável de flag wsk como true.
Após resolver o problema de mascaramento dos dados, também precisamos adicionar a configuração da conexão via WebSocket:
Primeiro, vamos garantir que isso seja realmente uma configuração de conexão: if '\r\n\r\n' in data;
Se for, obtenha o valor da chave: data.split("\r\n")[4].split(": ")[1];
Calcule o valor de Sec-WebSocket-Accept: base64.b64encode(hashlib.sha1((key + GUID).encode('utf-8')).digest()), onde GUID é um valor fixo "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".
Depois, defina o cabeçalho de resposta do handshake:
response_tpl="HTTP/1.1 101 Switching Protocols\r\n" \ "Upgrade:websocket\r\n" \ "Connection: Upgrade\r\n" \ "Sec-WebSocket-Accept: %s\r\n" \ "WebSocket-Location: ws://%s/\r\n\r\n"
Preencha o cabeçalho da resposta: response_str = response_tpl % (ac.decode('utf-8'), "127.0.0.1:8989").
Finalmente, envie a resposta do handshake: self.sk_.send(bytes(response_str, encoding='utf-8')).
Há mais uma coisa que precisamos adicionar: tratar a informação que será enviada de forma aceitável para o WebSocket:
if wsk: tk=b'\x81' lgt=len(bt) tk+=struct.pack('B',lgt) bt=tk+bt
Agora a parte que precisa ser aprimorada no lado do servidor está praticamente concluída.
def msg(self): self.re = '' wsk=False while True: data = self.sk_.recv(2500) if not data: break if (data[1] & 0x80) >> 7: fin = (data[0] & 0x80) >> 7 # FIN bit opcode = data[0] & 0x0f # opcode masked = (data[1] & 0x80) >> 7 # mask bit mask = data[4:8] # masking key payload = data[8:] # payload data print('fin is:{},opcode is:{},mask:{}'.format(fin,opcode,masked)) message = "" for i in range(len(payload)): message += chr(payload[i] ^ mask[i % 4]) data=message wsk=True else: data=data.decode("utf-8") if '\r\n\r\n' in data: key = data.split("\r\n")[4].split(": ")[1] print(key) GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ac = base64.b64encode(hashlib.sha1((key+GUID).encode('utf-8')).digest()) response_tpl="HTTP/1.1 101 Switching Protocols\r\n" \ "Upgrade:websocket\r\n" \ "Connection: Upgrade\r\n" \ "Sec-WebSocket-Accept: %s\r\n" \ "WebSocket-Location: ws://%s/\r\n\r\n" response_str = response_tpl % (ac.decode('utf-8'), "127.0.0.1:8989") self.sk_.send(bytes(response_str, encoding='utf-8')) data=data.split('\r\n\r\n',1)[1] if "stop" in data: self.stop=True break if len(data)<200: break self.re+=data bt=eva(self.re, self.model) bt=bytes(bt, "utf-8") if wsk: tk=b'\x81' lgt=len(bt) tk+=struct.pack('B',lgt) bt=tk+bt self.sk_.sendall(bt) return self.re
3. Aplicação
Primeiro, precisamos iniciar o lado do servidor, encontrar o diretório do arquivo server.py na linha de comando e executar python server.py para ativar o serviço.
Depois, retornamos ao cliente MetaTrader 5, abrimos o código-fonte e pressionamos Ctrl+F5 ou clicamos no botão de teste para iniciar o teste:
![](https://c.mql5.com/2/63/f4.png)
![f2 f2](https://c.mql5.com/2/63/f2.png)
Os resultados da execução do backtest são os seguintes:
Notas:
- Se você quiser realizar um teste diretamente no gráfico, observe: até agora, nosso código inicializou tanto o WebSocket quanto o socket simultaneamente. Claro, se a inicialização do socket for bem-sucedida, a lógica de execução não seguirá a parte lógica do WebSocket, mas para evitar problemas desnecessários neste caso, recomenda-se comentar a parte da inicialização do WebSocket no OnInit().
- Além de usar o OnTick() para concluir nossa lógica principal, também podemos considerar implementar a lógica no OnTimer(), permitindo definir um horário específico para o envio dos dados, por exemplo, a cada 15 minutos. Isso evitará o envio frequente de dados ao receber cotações. Esta seção não fornece um código de implementação específico. Os leitores podem referir-se ao método de implementação descrito neste artigo para escrever seu próprio código.
Considerações finais
Examinamos o método servidor-cliente para testar o modelo previamente treinado. Também mostrei como testar nosso sistema em cenários de teste histórico e fora deles. O artigo exige um grande volume de conhecimento sobre a interação entre diferentes linguagens e disciplinas. A parte mais difícil de entender é o conceito de WebSockets, que representa um projeto de engenharia complexo. Mas se você seguir os passos descritos neste artigo, conseguirá. Deve-se enfatizar que este artigo apresenta apenas um exemplo que permite testar nosso modelo com uma estratégia bastante simples. Por favor, não use este exemplo para negociações reais! A negociação real requer a otimização de cada parte deste sistema para um funcionamento estável, então, mais uma vez: não use este exemplo em sua negociação real! No próximo artigo, discutiremos como eliminar a dependência de sockets e usar nosso modelo diretamente no EA.
Espero que as informações tenham sido úteis para você.
Referências:
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/13254
![Desenvolvimento de um Cliente MQTT para o MetaTrader 5: Metodologia TDD (Parte 5)](https://c.mql5.com/2/64/Developing_an_MQTT_client_for_Metatrader_5___Part_5___LOGO__1.png)
![Rede neural na prática: Pseudo Inversa (I)](https://c.mql5.com/2/81/Rede_neural_na_prztica__Pseudo_Inversa___LOGO.png)
![Ciência de dados e aprendizado de máquina (Parte 18): Comparando a eficácia do TruncatedSVD e NMF no tratamento de dados complexos de mercado](https://c.mql5.com/2/64/Data_Science_and_Machine_Learning_pPart_183_Truncated_SVD_Versus_NMF__LOGO.png)
![Implementação do teste aumentado de Dickey-Fuller no MQL5](https://c.mql5.com/2/64/Implementation_of_the_Augmented_Dickey_Fuller_test_in_MQL5__LOGO.png)
![MQL5 - Linguagem para estratégias de negociação inseridas no terminal do cliente MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- 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