English Русский 中文 Deutsch 日本語 Português
preview
Marcado de datos en el análisis de series temporales (Parte 5): Aplicación y comprobación de asesores usando Socket

Marcado de datos en el análisis de series temporales (Parte 5): Aplicación y comprobación de asesores usando Socket

MetaTrader 5Asesores Expertos | 10 julio 2024, 16:17
406 1
Yuqiang Pan
Yuqiang Pan

Introducción

En artículos anteriores, hemos visto cómo marcar los datos según nuestras propias necesidades y cómo utilizar estos para entrenar modelos de previsión de series temporales. Pero, ¿cuál es la mejor forma de utilizar estos modelos? Ha llegado el momento de discutir cómo validar nuestros modelos creados en las pruebas históricas de MetaTrader 5 e incorporarlos a nuestro asesor. En el asesor necesitaremos una estrategia como lógica clave, y una estrategia real y utilizable requiere un marco teórico específico y muchas pruebas y ajustes para garantizar su fiabilidad.

La estrategia del artículo es muy sencilla y solo sirve como demostración. No la utilice en el comercio real. Obviamente, con el apoyo de muchas bibliotecas diferentes, también podemos hacer este trabajo usando Python, pero MetaTrader 5 ofrece una herramienta cómoda y completa para la prueba histórica, que permite modelar con mayor precisión nuestro entorno comercial, por lo que aun así elegiremos el cliente MetaTrader 5 como nuestra plataforma de prueba para la historia. Pero como nuestro entorno de creación de modelos es Python, las pruebas en la historia de MetaTrader 5 tienen que implementarse utilizando MQL5, lo que complica un poco la implementación de las pruebas, pero tenemos una solución. Vamos a discutir el uso de tres métodos distintos para probar con la historia de nuestros modelos en el entorno MetaTrader 5 para ayudarnos a mejorar y aumentar su calidad. Este artículo tratará sobre el método WebSocket. Los demás se tratarán en los artículos siguientes.

Contenido:

  1. Introducción
  2. Principio de realización
  3. Implementación de funciones de servidor Python
  4. Implementación de funciones de cliente MQL5
  5. Pruebas históricas
  6. Conclusión


Principio de realización

En primer lugar, añadiremos un ejemplar de servidor web a nuestro script Python y le añadiremos la muestra de nuestro modelo. A continuación, utilizaremos MQL5 para crear un cliente web que consulte el servicio de inferencia en el servidor.

f0

Puede que piense que no es una buena forma de hacerlo. ¿Por qué no convertir el modelo a ONNX, que inicialmente es compatible con MQL5, y luego añadir la llamada al asesor? Sí, esto es razonable, pero debemos considerar que algunos modelos concretos son enormes y que el proceso de inferencia se optimiza mediante métodos diferentes, lo que puede requerir una migración conjunta de la lógica de inferencia y la implementación entre lenguajes. Como resultado, el proyecto alcanzará un tamaño gigantesco. El método puede combinar sistemas y lenguajes para lograr distintas combinaciones de funciones. Por ejemplo, si su cliente MetaTrader 5 se ejecuta en Windows, su parte de servidor puede incluso desplegarse en un servidor remoto. Su servidor puede ser cualquier sistema operativo compatible con la muestra de modelos, por lo que no tiene que instalar máquinas virtuales adicionales. Obviamente, también puede desplegar el servidor en wsl o docker. De esta forma, no estaremos limitados a un sistema operativo o un lenguaje de programación. Este método es muy común, y somos libres de ampliar su uso.

Suponemos que la lógica del asesor es la siguiente:

  • En primer lugar, cada vez que se activa el evento OnTick(), los últimos 300 datos del histograma se enviarán al servidor a través del cliente.
  • Tras recibir la información, el servidor enviará la tendencia prevista de los 6 histogramas siguientes al cliente del asesor a través de la muestra del modelo. Aquí utilizaremos el modelo Nbeats mencionado en el artículo anterior porque puede descomponer la previsión en tendencias.
  • Si la tendencia es bajista, venderemos, si la tendencia es alcista, compraremos.

Implementación de funciones de servidor Python

Un socket en Python incluye básicamente las siguientes funciones:

  • socket.bind(): vincula una dirección (host, puerto) a un socket. En AF_INET, una dirección se representa usando una tupla (host, puerto).
  • socket.listen(): inicia la escucha de TCP. backlog especifica el número máximo de conexiones que el sistema operativo puede suspender antes de rechazar una conexión. El valor es como mínimo 1, la mayoría de las aplicaciones lo fijan en 5.
  • socket.accept(): acepta pasivamente la conexión del cliente TCP, (blocking) espera la conexión.
  • socket.connect(): inicializa activamente la conexión con el servidor TCP. Normalmente, el formato de la dirección es una tupla (nombre de host, puerto). Si la conexión falla, retornará el error socket.error.
  • socket.connect_ex(): versión extendida de connect(). Retorna un código de error cuando se produce un error en lugar de emitir una excepción. socket.recv(): Recibe datos TCP, los datos se retornan como una cadena, bufsize especifica la cantidad máxima de datos a recibir. flag ofrece información adicional sobre el mensaje que normalmente puede ignorarse.
  • socket.send(): envía datos TCP, envía datos como una cadena al socket conectado. El valor de retorno será el número de bytes a enviar, que puede ser menor que el tamaño de la cadena en bytes.
  • socket.sendall(): envía datos TCP completamente. Los datos se envían al socket conectado como una cadena, intentando enviar todos los datos antes del retorno. Retorna None si tiene éxito o se llama una excepción si no tiene éxito.
  • socket.recvfrom(): obtiene datos UDP, similar a recv() pero el valor de retorno es (data,address). Aquí data será la cadena que contiene los datos recibidos y address serás la dirección del socket que envía los datos.
  • socket.sendto(): envía datos UDP, envía datos al socket, la dirección es una tupla (ipaddr,port) que define la dirección remota. El valor retornado es el número de bytes enviados.
  • socket.close(): cierra el socket
  • socket.getpeername(): retorna la dirección remota a la que está conectado el socket. El valor de retorno suele ser una tupla (ipaddr,puerto).
  • socket.getsockname(): retorna la dirección propia del socket, normalmente como una tupla (ipaddr,port)
  • socket.setsockopt(level,optname,value): establece el valor de esta opción de socket.
  • socket.getsockopt(level,optname[.buflen]): retorna el valor del parámetro del socket.
  • Socket.settimeout(timeout): establece el tiempo de espera para las operaciones de socket, timeout es un número de coma flotante en segundos. El valor None indica que no hay tiempo de espera. Generalmente, el tiempo de espera debe establecerse si el socket acaba de ser creado, ya que puede ser utilizado para operaciones de conexión (como Connect()).
  • socket.gettimeout(): retorna el valor actual del tiempo de espera en segundos o None si no se ha establecido ningún tiempo de espera.
  • socket.fileno(): retorna el descriptor de archivo del socket.
  • socket.setblocking(flag): si la bandera es igual a 0, pondremos el socket en modo de no-bloqueo, en caso contrario pondremos el socket en modo de bloqueo (valor por defecto). En modo de no-bloqueo, si no se encuentran datos al llamar a recv() o la llamada a send() no podrá enviar los datos de inmediato, llamará una excepción socket.error.
  • socket.makefile(): crea un archivo asociado al socket.


    1. Importamos los paquetes necesarios

    La implementación de la clase no requiere la instalación de paquetes adicionales, y la biblioteca de sockets suele incluirse por defecto (en un entorno conda). Si cree que hay demasiados mensajes de advertencia, puedes añadir un módulo warnings y la sentencia warnings.filterwarnings("ignore"). Al mismo tiempo, también deberemos definir las variables globales que necesitamos:

    • max_encoder_length=96
    • max_prediction_length=20
    • info_file=“results.json”

    Estas variables globales se definen partiendo del modelo que entrenamos en el artículo anterior.

    El código en sí:

    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. Creamos una clase de servidor

    Cree una clase de servidor donde inicializar algunas configuraciones básicas de socket, incluyendo las siguientes funciones:

    socket.socket(): establecemos dos parámetros a socket.AF_INET y socket.SOCK_STREAM.

    Método bind() socket.socket(): la función establece el parámetro host a "127.0.0.1" y el parámetro port a "8989", no recomendamos cambiar el host, y el port puede establecerse a otro valor si 8989 está ocupado.

    El modelo se enviará más tarde, por lo que lo inicializaremos temporalmente con el valor None.

    Necesitamos escuchar en el puerto del servidor: self.sk.listen(1), aceptar pasivamente las conexiones TCP de los clientes y esperar las conexiones: self.sk_, self.ad_ = self.sock.accept(). Realizaremos estas tareas al inicializar la clase para evitar la reinicialización al obtener la información de forma cíclica.


    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: Si estamos desplegando el servidor en un docker o un contenedor similar, puede que necesitemos establecer el host a "0.0.0.0.0" o nuestro cliente no podrá encontrar el servidor.


    3. Lógica de procesamiento de la información obtenida

    Definiremos un método de la clase msg() para procesar la información obtenida, usando un ciclo while para procesar la información recibida. Una cosa a considerar aquí es que debemos decodificar los datos recibidos usando decode("utf-8"), para luego enviar la información procesada a la función de procesamiento lógico de muestra self.sk_.send(bytes(eva(self.re), "utf-8")) donde la función lógica de muestra se define como eva() y el parámetro será la información que recibimos, que implementaremos más adelante. A continuación, deberemos asegurarnos de que nuestro servidor también se detiene cuando dejamos de probar el asesor con la historia, de lo contrario ocupará recursos en segundo plano. Podemos hacer esto enviando una cadena "stop" al servidor después de que el asesor termine, y si recibimos esta cadena, permitiremos que el servidor detenga el ciclo y termine el proceso. Ya hemos añadido este atributo de clase al inicializar la clase servidor, y solo tendremos que establecerlo en true cuando recibamos esta señal.

    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: En este ejemplo, hemos establecido self.sk_.recv(2374) en 2374, que se corresponde con una longitud de 300 números en coma flotante. Si ve que los datos obtenidos están incompletos, podrá cambiar este valor.


    4. Recuperación de recursos

    Después de detener el servidor, necesitaremos recuperar los recursos.

    def __del__(self):
            print("server closed!")
            self.sk_.close()
            self.ad_.close()
            self.sock.close()


    5. Definición de lógica de muestra

    La lógica de la muestra del ejemplo es muy sencilla. Simplemente cargaremos el modelo y utilizaremos el histograma proporcionado por el cliente para predecir los resultados, luego los dividiremos en tendencias y retornaremos los resultados al cliente. Aquí deberemos considerar que podemos inicializar el modelo al inicializar la clase del servidor, de modo que el modelo esté precargado y listo para la muestra en cualquier momento.

    Primero definiremos una función para cargar el modelo, y luego llamaremos a esta función al inicializar la clase servidor para obtener un ejemplar del modelo. En el artículo anterior, presentamos el proceso de almacenamiento y carga de un modelo. Después del entrenamiento, el modelo guardará la información en un archivo json results.json en el directorio raíz de la carpeta. Podremos leer y descargar el modelo. Obviamente, nuestro archivo server.py también deberá estar en el directorio raíz de la carpeta.


    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
    A continuación, añadiremos la función init() de la clase server_(): self.model=load_model() para inicializarlo, y luego transmitiremos el modelo inicializado a la función de muestra.

        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_) 

    A continuación, completaremos nuestra función de muestra.

    Aquí deberemos prestar especial atención al hecho de que el formato de los datos a introducir en el modelo debe ser un formato DataFrame, por lo que tendremos que convertir primero los datos recibidos en un array numpy: msg=np.fromstring(msg, dtype=float , sep= ',') y luego en DataFrame: dt=pd.DataFrame(msg). Cuando se complete la muestra, se retornará el resultado. Hemos establecido que si el último valor de una tendencia es superior al valor medio, se tratará de una tendencia alcista; en caso contrario, se tratará de una tendencia bajista. Si la tendencia es alcista, se retornará "buy", si es bajista, se retornará un "sell" descendente. El proceso de muestra no lo expondremos en este artículo. Podrá leer sobre él en anteriores partes de la serie. Existe otro punto que conviene subrayar aquí. Puesto que establecemos el predictor del modelo como la columna "close" de DataFrame, necesitaremos añadir la columna "close" a los datos transformados en 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"

    A continuación, tendremos que añadir el ciclo principal.

    En primer lugar, inicializaremos la clase de servicio y, a continuación, añadiremos una función de procesamiento de la información al ciclo while. Terminaremos el ciclo y saldremos del programa cuando recibamos una señal de parada. No queremos que el ciclo se ejecute demasiado rápido, por lo que añadiremos Sleep(0.5) para limitar la velocidad del ciclo y evitar una alta utilización de la CPU.

    while True:
         rem=sv.msg()
         if sv.stop:
              break
        sleep(0.5)

    Ya hemos creado un servidor simple, ahora necesitaremos implementar el cliente en el asesor.


    Implementación de funciones de cliente MQL5

    1. Funciones de socket en MQL5

    El módulo de socket incluye actualmente las siguientes funciones:

    • SocketCreate: crea un socket con el identificador especificado y retorna su manejador
    • SocketClose: cierra el socket
    • SocketConnect: realiza una conexión al servidor con tiempo de espera controlado
    • SocketIsConnected: comprueba si un socket está actualmente conectado
    • SocketIsReadable: obtiene el número de bytes que se puedan leer de un socket
    • SocketIsWritable: comprueba si se pueden escribir datos en el socket en el momento actual
    • SocketTimeouts: establece los tiempos de espera de recepción y transmisión para el objeto de sistema de socket
    • SocketRead: lee datos de un socket
    • SocketSend: escribe datos en un socket
    • SocketTlsHandshake: inicia una conexión segura TLS (SSL) con el host especificado usando el protocolo TLS Handshake
    • SocketTlsCertificate: recupera datos sobre el certificado usado para asegurar la conexión de red
    • SocketTlsRead: lee datos de una conexión TLS segura
    • SocketTlsReadAvailable: lee todos los datos disponibles de una conexión TLS segura
    • SocketTlsSend: envía datos a través de una conexión TLS segura

    Llamando a estos métodos, podremos añadir fácilmente funciones adicionales en el lado del cliente.


    2. Implementación de las funciones de asesor

    En primer lugar, echaremos un vistazo a la lógica funcional del asesor:

    Para ello, inicializaremos el socket en "int OnInit()".

    Luego en "void OnTick()" implementaremos la obtención de datos desde el cliente y el envío de los datos del histograma actual al cliente, así como la lógica de prueba de nuestro asesor usando datos históricos.

    En "void OnDeinit(const int Reason)" necesitaremos enviar un mensaje "stop" al servidor y cerrar el socket.


    3. Inicialización del asesor

    En primer lugar, deberemos definir una variable global "int sk" que se utilice para obtener el manejador después de crear el socket.

    En OnInit(), utilizaremos SocketCreate() para crear el cliente: int sk=SocketCreate().

    A continuación, definiremos la dirección de nuestro servidor: string host="127.0.0.1";
    Puerto del servidor: int port= 8989;
    Ya hemos mencionado el valor 300 como longitud de los datos a enviar: int data_len=300;

    En la función OnInit(), deberemos evaluar la situación de inicialización. Si la creación no tiene éxito, también fallará la inicialización.

    A continuación, crearemos una conexión con el servidor SocketConnect(sk,host, port,1000) donde el puerto deberá coincidir con el lado del servidor. Si la creación no tiene éxito, también fallará la inicialización.

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


    No olvide liberar recursos al final del asesor.

    void OnDeinit(const int reason) {
        socket.Disconnect();
    }


    4. Lógica comercial

    Aquí necesitaremos definir el procesamiento básico de datos y la lógica comercial en void OnTick().

    Para ejecutar las tareas de la orden, crearemos las variables "MqlTradeRequest request" y "MqlTradeResult result";

    Luego crearemos una variable de array de caracteres "char Recv_data[]" para recuperar la información del servidor;

    Después crearemos una variable de array double "doublepriceData[300]" para copiar los datos del gráfico;

    Y crearemos las variables "string dataToSend" y "char ds[]" para convertir el array double en un array de caracteres que pueda ser utilizado por el socket;

    Primero tendremos que copiar los datos del gráfico para enviarlos: int nc=CopyClose(Symbol(),0,0,data_len,priceData);

    A continuación, convertiremos los datos al formato de cadena: for(int i=0;i<ArraySize(priceData);i++) dataToSend+=(string)priceData[i]+",", utilizaremos "," para separar los datos;

    Usaremos "int dsl=StringToCharArray(dataToSend,ds)" para convertir los datos de cadena en una array de caracteres que pueda ser utilizado por el socket.

    Después de convertir los datos, necesitaremos usar SocketIsWritable(sk) para determinar si nuestro socket puede enviar datos. De ser así, utilizaremos SocketSend(sk,ds,dsl) para enviar los datos.

    También necesitaremos leer información del servidor. Usaremos "uint len=SocketIsReadable(sk)" para comprobar si hay datos disponibles en el puerto actual. Si la información no está vacía, ejecutaremos la lógica comercial: int rsp_len=SocketRead( sk,recv_data,len,500), "len" será el tamaño del búfer, "500" será el tiempo de espera (en milisegundos).

    Si hemos recibido "buy", abriremos una orden de compra, y configuraremos la solicitud de la siguiente manera:

    • Restableceremos la estructura de la solicitud comercial request: ZeroMemory(request)
    • Estableceremos la ejecución inmediata de la orden de operación: request.action = TRADE_ACTION_DEAL
    • Estableceremos el par de divisas de la transacción: request.symbol = Symbol()
    • Volumen de la orden: request.volume = 0.1
    • Tipo de orden: request.type = ORDER_TYPE_BUY
    • La función SymbolInfoDouble necesitará 2 parámetros de entrada: el primero es la cadena del par de divisas, el segundo es el tipo en la enumeración ENUM_SYMBOL_INFO_DOUBLE: request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK)
    • Desviación admisible de una operación: request.deviation = 5
    • A continuación, enviaremos una orden comercial: OrderSend(request, result)

    Si recibimos "sell", abriremos una orden de venta, y configuraremos la solicitud de la siguiente manera (los ajustes se refieren a una orden de compra, no se explican en detalle aquí):

    • 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
    • A continuación, enviaremos una orden comercial: OrderSend(request, result)

    Aquí, para evitar problemas con el código de prueba, hemos comentado la función real de envío de órdenes y la hemos abierto en el 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: El parámetro buffer_maxlen de la función SocketSend() deberá coincidir con la configuración del servidor. Este valor se calculará automáticamente y se retornará cuando se ejecute StringToCharArray().

    Ahora primero ejecutaremos server.py y luego añadiremos el asesor al gráfico en el cliente MetaTrader 5. Los resultados serán los siguientes:



    Sin embargo, por ahora no podremos usar la prueba de la historia, porque SocketCreate() y una serie de operaciones de socket no están permitidas en las pruebas. Veremos la solución a este problema posteriormente.



    Pruebas históricas


    Antes hemos mencionado las limitaciones de los sockets en MQL5, y ahora necesitaremos añadir soporte para WebSockets tanto en MQL5 como en Python.


    1. Añadimos soporte de WebSockets al cliente

    Al realizar pruebas en la historia, podemos usar winhttp.mqh en la API de Windows para conseguir la funcionalidad deseada. Encontrará una introducción detallada a la API en este enlace:

    Documentación oficial de Microsoft: https://learn.microsoft.com/es-es/windows/win32/winhttp/winhttp-functions. Aquí solo enumeraremos las características principales:

    • WinHttpOpen(): inicializa la biblioteca y la prepara para ser utilizada por la aplicación
    • WinHttpConnect(): establece el nombre de dominio del servidor con el que la aplicación necesita comunicarse
    • WinHttpOpenRequest(): crea el manejador de la solicitud HTTP
    • WinHttpSetOption: establece diferentes opciones de configuración para la conexión HTTP
    • WinHttpSendRequest: envía una solicitud al servidor
    • WinHttpReceiveResponse: recibe una respuesta del servidor tras enviar una solicitud.
    • WinHttpWebSocketCompleteUpgrade: confirma que la respuesta del servidor cumple el protocolo WebSocket
    • WinHttpCloseHandle: Desactiva los descriptores de recursos usados anteriormente
    • WinHttpWebSocketSend: Envía datos a través de una conexión WebSocket
    • WinHttpWebSocketReceive: Recibe datos usando una conexión WebSocket
    • WinHttpWebSocketClose: Cierra una conexión WebSocket
    • WinHttpWebSocketQueryCloseStatus: Comprueba el mensaje de estado de cierre enviado desde el servidor

    Ahora descargaremos el archivo winhttp.mqh y lo copiaremos en la carpeta de datos del cliente Include\WinAPI\. Ahora vamos a finalizar el código.

    Luego añadiremos las variables descriptoras que necesitamos utilizar a las variables globales "HINTERNET ses_h,cnt_h,re_h,ws_h" y las inicializaremos en OnInit():

    • Primero evitaremos los números aleatorios estableciéndolos en NULL:ses_h=cnt_h=re_h=ws_h=NULL;
    • A continuación, iniciaremos la sesión http: ses_h=WinHttpOpen("MT5",WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,NULL,NULL,0), si falla, la inicialización también fallará;
    • Después nos conectaremos al servidor: cnt_h=WinHttpConnect(ses_h,host,port,0), si hay un fallo, la inicialización fallará;
    • A continuación, inicializaremos la solicitud: re_h=WinHttpOpenRequest(cnt_h, "GET",NULL,NULL,NULL,NULL,NULL,0), en caso de error, la inicialización fallará;
    • Luego crearemos un proyecto web: WinHttpSetOption(re_h,WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,nullpointer,0), la inicialización finalizará con error;
    • Vamos a ejecutar una solicitud para establecer una conexión a través de un WebSocket: WinHttpSendRequest( re_h,NULL, 0,nullpointer, 0, 0, 0, 0), en caso de error, la inicialización fallará;
    • Ahora obtendremos la respuesta al handshake del servidor: WinHttpReceiveResponse(re_h,nullpointer), en caso de error, la inicialización fallará;
    • Luego actualizaremos el WebSocket, obtendremos el manejador después de la inicialización:WinHttpWebSocketCompleteUpgrade(re_h,nv), en caso de error, la inicialización fallará;
    • Una vez completada la actualización, ya no necesitaremos el descriptor original de la solicitud, así que lo cerraremos: WinHttpCloseHandle(re_h);

    De esta forma hemos completado todo el proceso de conexión entre el cliente y el servidor. Estos procesos deberán ejecutarse en riguroso orden. Ahora necesitaremos comentar las configuraciones iniciales del operador de fallo de inicialización porque siempre estarán en efecto durante las pruebas de la historia y causarán un error de inicialización.

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

    A continuación añadiremos el código lógico necesario a la función OnTick().

    En primer lugar, determinaremos en qué entorno estamos trabajando, ya que hemos definido la variable global del socket del descriptor. ¡Podemos distinguir si estamos trabajando en condiciones normales o en un estado de prueba evaluando si el socket se ha inicializado con éxito, por lo que el mensaje "sk! =-1" cuando es true significará que la inicialización del socket se ha realizado correctamente, no necesitaremos cambiar esta parte del código. Si "sk!=-1" no es igual a true, entonces necesitaremos mejorar la lógica del WebSocket:

    • Primero, enviaremos los datos al servidor: WinHttpWebSocketSend(ws_h,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE,ds,dsl). Si este proceso tiene éxito, el valor de retorno de la función será 0, en caso contrario retornará el código de error correspondiente
    • Si tiene éxito, borraremos la variable de datos resultante: ZeroMemory(recv_data)
    • Luego obtendremos los datos: get=WinHttpWebSocketReceive(ws_h,recv_data,ArraySize(recv_data),rb,st). Si los datos se obtienen correctamente, el valor retornado será 0, en caso contrario se retornará un código de error
    • Si obtenemos los datos, los descodificaremos: pre+=CharArrayToString(recv_data,0)

    Si el servidor nos envía "buy", abriremos una orden de compra, de lo contrario abriremos una orden de venta. La diferencia es que también hemos añadido una lógica de evaluación adicional: si ya existe una orden, primero determinaremos si existe una orden no ejecutada "numt=PositionsTotal()>0". En caso afirmativo, obtendremos el tipo de orden: tpt=OrderGetInteger( ORDER_TYPE), luego veremos si el tipo de orden es ORDER_TYPE_SELL o ORDER_TYPE_BUY. Si el tipo de orden coincide con la tendencia enviada por el servidor, no necesitaremos realizar ninguna operación. Si el tipo de orden es opuesto a la tendencia, cerraremos la orden actual y abriremos una orden correspondiente a la tendencia.

    Utilizaremos los datos del servidor "buy" como ejemplo para mostrar este proceso.

    Si tpt==ORDER_TYPE_BUY, retornamos directamente, si tpt==ORDER_TYPE_SELL, significa que hay una orden de venta, entonces estableceremos: request.order=tik, set: request.action=TRADE_ACTION_REMOVE, cuando OrderSend(request, result) se ejecuta, la orden de venta se cerrará.

    Si no hay orden, la estableceremos:

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

    Cuando se ejecute OrderSend(request, result), se abrirá una orden de compra. De forma análoga, si la información del servidor es "sell", la orden se establecerá de la misma manera, así que no se discutirá con detalle este artículo.

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

    En este punto, hemos completado la configuración de nuestro cliente WebSocket MQL5.


    2. Configuración del servidor

    Necesitaremos añadir soporte para WebSockets a server.py.

    Primero deberemos importar las bibliotecas necesarias.

    import base64
    import hashlib
    import struct
    El trabajo principal lo realizará la función msg(self) de la clase de servidor:

    En primer lugar, añadiremos la variable de bandera de WebSocket wsk=False, y luego determinaremos si los datos obtenidos están enmascarados o no.

    Si están enmascarados, el bit alto del segundo byte de datos será 1 y solo necesitaremos determinar el valor (data[1] & 0x80) >> 7.

    Si no está enmascarado, utilizaremos data.decode("utf-8").

    Si los datos están enmascarados, necesitaremos encontrar la clave de enmascaramiento: mask = data[4:8] y los datos de carga útil: payload = data[8:], luego revelar los datos: for i in range(len(payload)):message += chr(payload[i] ^ mask[i % 4]) y establecer la variable de bandera wsk como true.

    Después de resolver el problema de la ocultación de datos, también tendremos que añadir la configuración de la comunicación del WebSocket:

    En primer lugar, nos aseguraremos de que se trate de un enlace real: if '\r\n\r\n\r\n' in data;

    En caso afirmativo, obtendremos el valor de la clave: data.split("\r\n")[4].split(": ")[1];

    Luego calcularemos el valor Sec-WebSocket-Accept: base64.b64encode(hashlib.sha1((key+GUID).encode('utf-8'))).digest()), donde GUID será el valor fijo "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".

    A continuación, definiremos el encabezado de la respuesta del 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"


    Ahora rellenaremos el encabezado de la respuesta: response_str = response_tpl % (ac.decode('utf-8'), "127.0.0.0.1:8989").

    Por último, enviaremos una respuesta al handshake: self.sk_.send(bytes(response_str, encoding='utf-8')).


    Hay una cosa más que añadir: tenemos que procesar la información que se enviará como información aceptable para el WebSocket:

    if wsk:
       tk=b'\x81'
       lgt=len(bt)
       tk+=struct.pack('B',lgt)
       bt=tk+bt

    Ahora ya está casi terminada la parte que hay que finalizar en el lado del servidor.


    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. Aplicación

    En primer lugar, tendremos que iniciar la parte del servidor, encontrar el directorio de archivos server.py en la línea de comandos y ejecutar python server.py para habilitar el servicio.

    f1

    A continuación, regresaremos al cliente MetaTrader 5, abriremos el código fuente y pulsaremos Ctrl+F5 o clicaremos en el botón de prueba para iniciar la prueba:

      En este momento, la columna de la barra de herramientas del diagrama de prueba de información mostrará la siguiente información: f2


    Los resultados del backtest son los siguientes:

      f3

    Podemos ver que todo nuestro sistema funciona perfectamente y puede realizar las operaciones debidas con órdenes tal y como predice el modelo.


    Notas:

    1. Si quiere realizar una prueba directamente en el gráfico, tenga en cuenta que hasta ahora nuestro código ha inicializado el WebSocket y el socket al mismo tiempo. Obviamente, si la inicialización del socket tiene éxito, la lógica de ejecución no ejecutará la parte lógica del WebSocket, pero para evitar problemas innecesarios en este caso le recomendamos comentar la parte de inicialización del WebSocket en OnInit().
    2. Además de usar OnTick() para completar nuestra lógica básica, también podríamos considerar implementar la lógica en OnTimer() para que podamos establecer un tiempo específico para el envío de datos, por ejemplo, cada 15 minutos. Esto evitará el envío frecuente de datos cuando se reciban las cotizaciones. En este artículo no ofrecemos ningún código de aplicación específico. Los lectores podrán consultar el método de implementación de este artículo para escribir su propio código de implementación.


    Conclusión

    En el material de hoy, hemos analizado un método servidor-cliente para realizar pruebas sobre la historia de un modelo previamente entrenado. También hemos mostrado cómo probar nuestro sistema dentro y fuera de los escenarios de pruebas históricas. El artículo requiere un gran conocimiento sobre la interacción entre distintos lenguajes y disciplinas. La parte más difícil de entender es el concepto de WebSockets, que es un complejo proyecto de ingeniería. Pero si seguimos los pasos descritos en este artículo, lo conseguiremos. Cabe destacar que este artículo solo presenta un ejemplo que nos permite probar nuestro modelo con una estrategia bastante sencilla. Por favor, ¡no lo utilice para comerciar de verdad! El trading real requiere optimizar cada parte de este sistema para lograr un rendimiento estable, así que una vez más: ¡no utilice este ejemplo en su trading real! En el próximo artículo discutiremos cómo deshacernos de la dependencia del socket y utilizar nuestro modelo directamente en un asesor.

    Espero que la información le haya resultado útil.


    Enlaces:

    Websockets para MetaTrader 5 — Usando la API de Windows

    Traducción del inglés realizada por MetaQuotes Ltd.
    Artículo original: https://www.mql5.com/en/articles/13254

    Archivos adjuntos |
    winhttp.mqh (8.13 KB)
    socket_test.mq5 (21.34 KB)
    server.py (4 KB)
    Hilario Miguel Ofarril Gonzalez
    Hilario Miguel Ofarril Gonzalez | 11 jul 2024 en 00:59
    Muy importante este articulo .serios anilicis de tu parte me gusta .sin palabras me kedo .bale me gustó muchísimo .gracias 
    Desarrollando un cliente MQTT para MetaTrader 5: metodología de TDD (Parte 5) Desarrollando un cliente MQTT para MetaTrader 5: metodología de TDD (Parte 5)
    El presente artículo supone la quinta parte de la serie que describe las etapas de desarrollo de un cliente MQL5 nativo para el protocolo MQTT 5.0. Hoy describiremos la estructura de los paquetes PUBLISH: cómo establecemos sus banderas de publicación (Publish Flags), codificamos cadenas de nombres de temas y establecemos IDs de paquetes cuando es necesario.
    Introducción a MQL5 (Parte 2): Variables predefinidas, funciones comunes y operadores de flujo de control Introducción a MQL5 (Parte 2): Variables predefinidas, funciones comunes y operadores de flujo de control
    En este artículo, seguiremos familiarizándonos con el lenguaje de programación MQL5. Esta serie de artículos no es solo un tutorial, sino también una puerta de entrada al mundo de la programación. ¿Qué hace especiales a estos artículos? Hemos procurado que las explicaciones sean sencillas para que los conceptos complejos resulten accesibles a todos. Aunque el material es accesible, para obtener los mejores resultados será necesario reproducir activamente todo lo que vamos a tratar. Solo así obtendremos el máximo beneficio de estos artículos.
    Algoritmos de optimización de la población: Algoritmo genético binario (Binary Genetic Algorithm, BGA). Parte I Algoritmos de optimización de la población: Algoritmo genético binario (Binary Genetic Algorithm, BGA). Parte I
    En este artículo, analizaremos varios métodos utilizados en algoritmos genéticos binarios y otros algoritmos poblacionales. Asimismo, repasaremos los principales componentes del algoritmo, como la selección, el cruce y la mutación, así como su impacto en el proceso de optimización. Además, estudiaremos las formas de presentar la información y su repercusión en los resultados de la optimización.
    Algoritmos de optimización de la población: microsistema inmune artificial (Micro Artificial immune system, Micro-AIS) Algoritmos de optimización de la población: microsistema inmune artificial (Micro Artificial immune system, Micro-AIS)
    El artículo habla de un método de optimización basado en los principios del sistema inmune del organismo -Micro Artificial immune system, (Micro-AIS)-, una modificación del AIS. El Micro-AIS usa un modelo más simple del sistema inmunitario y operaciones sencillas de procesamiento de la información inmunitaria. El artículo también analizará las ventajas e inconvenientes del Micro-AIS en comparación con el AIS convencional.