English Русский Deutsch 日本語
preview
Anotação de dados na análise de série temporal (Parte 5): Aplicação e teste de um EA usando Socket

Anotação de dados na análise de série temporal (Parte 5): Aplicação e teste de um EA usando Socket

MetaTrader 5Experts | 20 junho 2024, 14:49
29 0
Yuqiang Pan
Yuqiang Pan

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:

  1. Introdução
  2. Princípio de implementação
  3. Implementação das funções do servidor Python
  4. Implementação das funções do cliente MQL5
  5. Teste histórico
  6. 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.

f0

Você pode pensar que essa não é uma boa maneira. Por que não simplesmente converter o modelo em ONNX, que é suportado pelo MQL5, e depois adicioná-lo ao EA? Sim, isso faz sentido, mas não se esqueça de que alguns modelos específicos são enormes, e o processo de inferência é otimizado por vários métodos, o que pode exigir a migração conjunta da lógica de inferência e a implementação entre linguagens. Como resultado, o projeto pode se tornar imenso. Esse método pode combinar sistemas e linguagens para alcançar várias combinações de funcionalidades. Por exemplo, se o seu cliente MetaTrader 5 estiver rodando no Windows, você pode até mesmo implementar a parte do servidor em um servidor remoto. O servidor pode ser qualquer sistema operacional que suporte a inferência do modelo, então você não precisa instalar máquinas virtuais adicionais. Claro, você também pode implementar o servidor no WSL ou Docker. Assim, não ficamos limitados a um único sistema operacional ou linguagem de programação. Esse método é muito comum e podemos expandir seu uso livremente.

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 model
    Depois, 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);
      }


    Não se esqueça de liberar os recursos ao final do EA.

    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:



    Mas ainda não podemos usar o teste histórico, pois o SocketCreate() e uma série de operações com sockets não são permitidos durante o teste. Vamos então discutir a solução para esse problema.



    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.

    f1

    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:

      Neste momento, a coluna do painel de ferramentas do gráfico de informações de teste mostrará as informações correspondentes:  f2


    Os resultados da execução do backtest são os seguintes:

      f3

    Podemos ver que todo o nosso sistema está funcionando perfeitamente e pode executar operações de ordens de acordo com as previsões do modelo.


    Notas:

    1. 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().
    2. 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:

    WebSocket para MetaTrader 5 — Uso da API do Windows

    Traduzido do Inglês pela MetaQuotes Ltd.
    Artigo original: https://www.mql5.com/en/articles/13254

    Arquivos anexados |
    winhttp.mqh (8.13 KB)
    socket_test.mq5 (21.34 KB)
    server.py (4 KB)
    Desenvolvimento de um Cliente MQTT para o MetaTrader 5: Metodologia TDD (Parte 5) Desenvolvimento de um Cliente MQTT para o MetaTrader 5: Metodologia TDD (Parte 5)
    Este artigo é a quinta parte de uma série que descreve as etapas de desenvolvimento de um cliente MQL5 nativo para o protocolo MQTT 5.0. Nesta parte, vamos detalhar a estrutura dos pacotes PUBLISH, configuraremos seus flags de publicação, codificaremos os nomes dos tópicos e estabeleceremos identificadores de pacotes quando necessário.
    Rede neural na prática: Pseudo Inversa (I) Rede neural na prática: Pseudo Inversa (I)
    Aqui, vamos começar a ver como podermos implementar, usando MQL5 puro, o cálculo de pseudo inversa. Apesar do código que será visto, será de fato bem mais complicado, para os iniciantes, do que eu de fato gostaria de apresentar. Ainda estou pensando em como o explicar de forma simples. Veja isto como uma oportunidade de estudar um o código pouco comum. Então vá com calma. Sem pressa e correria. Mesmo que ele não vise ser eficiente e de rápida execução. O objetivo é ser o mais didático possível.
    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 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
    A decomposição em valores singulares truncada (TruncatedSVD) e a fatoração de matriz não negativa (NMF) são métodos de redução de dimensionalidade. Ambos podem ser bastante úteis ao trabalhar com estratégias de negociação baseadas na análise de dados. Neste artigo, analisamos a aplicabilidade desses métodos no processamento de dados complexos de mercado, incluindo suas capacidades de redução de dimensionalidade para otimizar a análise quantitativa nos mercados financeiros.
    Implementação do teste aumentado de Dickey-Fuller no MQL5 Implementação do teste aumentado de Dickey-Fuller no MQL5
    Neste artigo, vamos mostrar como implementar o teste aumentado de Dickey-Fuller e sua aplicação para realizar testes de cointegração usando o método de Engle-Granger.