English Русский 中文 Deutsch 日本語
preview
Añadimos un LLM personalizado a un robot comercial (Parte 5): Desarrollar y probar la estrategia de negociación con LLMs (IV) - Probar la estrategia de trading

Añadimos un LLM personalizado a un robot comercial (Parte 5): Desarrollar y probar la estrategia de negociación con LLMs (IV) - Probar la estrategia de trading

MetaTrader 5Trading |
196 1
Yuqiang Pan
Yuqiang Pan

Tabla de contenido

  1. Tabla de contenido
  2. Introducción
  3. Entorno de desarrollo para el ejemplo de este artículo
  4. Métodos para cargar LLM en MQL5
  5. Conversión del modelo GPT-2 al modelo ONNX
  6. Formulación de la estrategia del EA y la funcionalidad del servidor
  7. Creación del servicio de inferencia
  8. Cliente del EA
  9. Pruebas retrospectivas
  10. Conclusión



Introducción

En artículos anteriores, presentamos cómo ajustar los modelos GPT-2 preentrenados utilizando diferentes métodos para que GPT-2 realice tareas de acuerdo con nuestros deseos, y comparamos estos métodos en varias dimensiones. Por supuesto, solo hemos presentado algunos métodos de uso común, lo que no significa que solo estos métodos puedan utilizarse para ajustar con precisión los modelos GPT-2. Puedes intentar ajustar GPT-2 utilizando otros métodos basados en nuestro proceso de implementación de ejemplo, compararlos y elegir un modelo mejor. Si encuentra algún problema durante este proceso, puede dejar un comentario al final del artículo.

Ahora, nuestro modelo GPT-2 perfeccionado tiene la capacidad inicial de ejecutar estrategias de negociación cuantitativas simples. Por lo tanto, este artículo presentará cómo integrar nuestro modelo perfeccionado en nuestra estrategia de negociación cuantitativa. El modelo utilizado en el ejemplo es el modelo GPT-2 ajustado con Adapter-tuning (enlace al artículo específico: Añadimos un LLM personalizado a un robot comercial (Parte 5): Desarrollar y probar la estrategia de negociación con LLMs (III) Ajuste del adaptador). Así pues, a menos que se especifique lo contrario, todas las referencias a GPT-2 en este artículo se refieren a este modelo.

Sin embargo, cabe señalar que el modelo que ajustamos se basa en datos limitados con fines demostrativos y no puede manejar entornos de negociación reales. Sin realizar pruebas y optimización, no las utilice directamente en operaciones reales, lo cual es de suma importancia. Nuestro código de predicción anterior se completó en el entorno Python, pero MQL5, como lenguaje de programación altamente integrado para la plataforma MetaTrader 5, proporciona herramientas potentes para desarrollar Asesores Expertos (EA). Por lo tanto, para implementar estrategias de negociación cuantitativa automatizadas, necesitamos volver al entorno MQL5. Este artículo explicará paso a paso cómo lograr este proceso.

Veamos cómo migrar este modelo entrenado del entorno Python al EA MQL5, para que se ejecute directamente en la plataforma MetaTrader 5 y así respaldar las decisiones de negociación en tiempo real.


Entorno de desarrollo para el ejemplo de este artículo

Vamos a presentar el entorno de ejecución de los ejemplos de código proporcionados en este artículo. Por supuesto, esto no significa que tu entorno de código deba ser el mismo que el mío, pero si encuentras problemas al ejecutar el código, puedes consultar la configuración de mi entorno.

  • Sistema operativo: Ubuntu 22.04.5 LTS (o la versión correspondiente de WSL)
  • Versión de Python: 3.10.14
  • Bibliotecas Python necesarias:

    1. torch-2.4.1
    2. numpy-1.26.3
    3. pandas-2.2.3
    4. transformers-4.45.1
    5. petf-0.13.0
    6. matplotlib-3.9.2
    7. onnx-1.17.0
    8. onnxconverter-common-1.14.0
    9. onnxruntime-1.20.1
    10. onnxruntime-tools-1.7.0

Tenga en cuenta que, antes de continuar con el siguiente paso, debe asegurarse de haber entrenado un modelo utilizando el ajuste del adaptador, tal y como se describe en el artículo anterior (debido a que el tamaño del modelo supera el límite de la plataforma, no se pueden cargar los pesos que ya he entrenado).


Métodos para cargar LLM en MQL5

Para integrar el modelo GPT-2 entrenado en el EA MQL5, el primer problema que debemos resolver es cómo cargar y ejecutar este modelo, que es esencialmente un modelo entrenado en Python, en el entorno MQL5. Aquí hay varios métodos viables:

1. Convierta el modelo a ONNX y agréguelo al EA.

ONNX (Open Neural Network Exchange) es un formato abierto para representar redes neuronales, lo que permite la interoperabilidad entre diferentes marcos de aprendizaje profundo. En mi artículo anterior, presenté cómo integrar modelos simples en algoritmos evolutivos utilizando ONNX (Marcado de datos en el análisis de series temporales (Parte 6): Aplicación y prueba en EA utilizando ONNX). También podemos convertir el modelo GPT-2 al formato ONNX, importarlo al EA y utilizar la biblioteca de tiempo de ejecución ONNX integrada en MQL5 para ejecutar la inferencia del modelo. Para obtener información sobre la compatibilidad de MQL5 con ONNX, consulte el archivo de ayuda «Manual de referencia de MQL5 / Modelos ONNX» o la documentación oficial de MQL5 (https://www.mql5.com/es/docs/onnx).

  • Ventajas:

  1. Alto rendimiento: El entorno de ejecución de ONNX suele estar optimizado para el rendimiento, lo que permite una inferencia relativamente eficiente en el EA.
  2. Alta integración: MQL5 tiene soporte incorporado para ONNX, eliminando la necesidad de programas o bibliotecas externas.
  3. Independencia: El modelo ONNX convertido puede ejecutarse independientemente del entorno Python.

  • Desventajas:

  1. Complejidad de la conversión: Convertir modelos de lenguaje complejos al formato ONNX puede ser un desafío, ya que requiere abordar problemas de compatibilidad de operadores.
  2. Dificultad de depuración: Depurar modelos ONNX es menos conveniente que depurar modelos Python.

2. Ejecuta directamente scripts de inferencia de Python usando WinAPI.

MQL5 proporciona acceso a WinAPI, lo que nos permite llamar a la función WinExec() desde kernel32.dll para ejecutar programas externos. De esta manera, podemos utilizar scripts Python existentes para cargar el modelo GPT-2 y realizar inferencias, y luego llamar al script en el EA utilizando WinExec() y analizar sus resultados de salida (alternativamente, la función ShellExecuteW() de shell32.dll también puede lograr esta funcionalidad). Este método requiere cierta experiencia en desarrollo y familiaridad con el desarrollo para Windows para su implementación.

  • Ventajas:
  1. Sencillo y directo: No es necesario convertir el modelo, utilizando directamente el código Python existente.
  2. Flexibilidad: Permite utilizar fácilmente las numerosas bibliotecas y herramientas del ecosistema de Python.
  • Desventajas:
  1. Sobrecarga de rendimiento: Cada inferencia requiere iniciar un nuevo proceso de Python, lo que genera una sobrecarga de rendimiento e ineficiencia significativas.
  2. Dependencia: El EA depende del entorno y los scripts de Python externos.
  3. Intercambio de datos: Requiere gestionar el intercambio de datos entre MQL5 y Python, lo que aumenta la complejidad.
  4. Seguridad: Pueden producirse diversas situaciones imprevistas que podrían causar colisiones incontrolables.

Nota: ¡Este método no es nada recomendable! Proporciono esta solución únicamente para indicar que es factible y puede utilizarse en pruebas o bajo condiciones controladas. No lo utilice sin tener suficiente confianza.

3. Obtenga resultados de inferencia de Python a través de la comunicación de sockets.

Similar al segundo método, pero utilizando comunicación Socket en lugar de WinAPI (en realidad, también se puede utilizar el protocolo HTTP, similar a los servicios HTTP proporcionados por los marcos de inferencia convencionales, que son esencialmente iguales a Socket, y este artículo no lo tratará más a fondo). El método de implementación específico consiste en ejecutar un servidor Socket en Python para cargar el modelo y realizar la inferencia, con el EA actuando como cliente que se conecta al servidor, envía datos de entrada y recibe los resultados de la inferencia.

  • Ventajas:

  1. Mejor rendimiento: La comunicación por sockets puede reducir la sobrecarga del inicio del proceso y es mucho más segura.
  2. Flexibilidad: Aún puede aprovechar las ventajas de Python.

  • Desventajas:

  1. Complejidad: Es necesario implementar la lógica de comunicación entre el servidor y el cliente Socket.
  2. Dependencia: El EA depende del entorno Python externo y del servidor Socket, lo que requiere algunos conocimientos para configurar el servicio.
  3. Estabilidad: La estabilidad de las conexiones de socket puede afectar al funcionamiento del EA.

Nota: Este método tiene una implementación específica en mi artículo anterior. Si está interesado, puede consultar el artículo: Marcado de datos en el análisis de series temporales (Parte 5): Aplicación y comprobación de asesores usando Socket.

Hemos analizado varios métodos de conversión diferentes. Actualmente, sigo prefiriendo convertir el modelo GPT-2 al formato ONNX e integrarlo en el EA porque esto también puede resolver problemas de compatibilidad entre plataformas y el EA tiene mayor integración y estabilidad. Sin embargo, si los parámetros del modelo ONNX son demasiado grandes, no se pueden ejecutar en MQL5 (por ejemplo, nuestro modelo GPT-2 actual excede el límite de carga de archivos de MQL5).

Otro desafío es resolver el problema del tokenizador en el modelo de transformadores, ya que modelos como GPT-2 vienen con un tokenizador para manejar la información de entrada, y para ejecutar el modelo GPT-2 en MQL5, debemos construir el tokenizador GPT-2 en MQL5, lo cual es un proyecto significativo. Esto es difícil, pero no imposible. Sin embargo, el límite de tamaño de carga de archivos MQL5 es un problema difícil de resolver.

Aunque intenté cuantificarlo al formato INT8, aún así excedió el límite y no se pudo cargar. Si se cuantifica al formato INT4, aunque el tamaño del modelo cumpla los requisitos, ¡MQL5 no admite modelos cuantificados en formato INT4! Por lo tanto, lamentablemente no nos queda más remedio que abandonar este método. Sin embargo, en este artículo proporcionaré un ejemplo de cómo convertir nuestro modelo GPT-2 adaptado al formato ONNX, ¡con la esperanza de resolver este problema pronto!

En este artículo, finalmente decidí abordar el uso de la comunicación Socket con el servicio de inferencia de Python. La ventaja de este método es que puede garantizar la seguridad de los datos y simplificar nuestra implementación del EA. En el EA, solo necesitamos centrarnos en nuestra estrategia y lógica de negociación, y no necesitamos considerar problemas de integración de módulos adicionales. Otra ventaja de este método es que, si no existe un entorno de desarrollo de modelos relevante a nivel local, como por ejemplo si el modelo se desarrolla y entrena en un dispositivo remoto, incluso si el entorno de desarrollo es incompatible con el entorno local, este método aún puede utilizarse para desarrollar el EA.

En general, aunque este método pueda parecer técnicamente complejo de implementar y requiera más conocimientos, puede lograr una alta eficiencia operativa y garantizar la independencia del EA, lo cual es crucial para un entorno de negociación en tiempo real eficiente. Dado que ya hemos tratado este tema en detalle en un artículo anterior, este artículo no describirá los detalles con más detalle. Si tiene alguna pregunta sobre los ejemplos de código, puede consultar la introducción detallada del artículo anterior.


Conversión del modelo GPT-2 al modelo ONNX

En la sección anterior, describí los diversos desafíos encontrados al convertir el modelo GPT-2 ajustado al formato ONNX y usarlo en MQL5. Sin embargo, sigo creyendo que esta es una dirección que vale la pena intentar, por lo que en este artículo utilizaré una sección adicional para presentar cómo convertir este modelo personalizado y ajustado al formato ONNX, con la esperanza de que todos puedan encontrar una solución a la situación actual. Si no le interesa esta parte, puede omitir esta sección.

1. Métodos de conversión de modelos

Ⅰ. Conversión directa (https://github.com/rayhern/convert-gpt2-xl-to-onnx)

Este repositorio de GitHub proporciona un script para convertir directamente modelos GPT-2, basado en la biblioteca "transformers" de Hugging Face y el exportador "torch.onnx". Sin embargo, debido a la falta de mantenimiento por parte del autor durante un largo período, puede tener algunas limitaciones y puede no ser compatible con las últimas versiones de la biblioteca "transformers".

  • Ventajas: Proporciona un script relativamente sencillo que se puede utilizar directamente; optimizado específicamente para la conversión del modelo GPT-2.
  • Desventajas: El estado de mantenimiento de este repositorio puede no estar claro, y es posible que no sea compatible con las últimas versiones de "transformers" y solo sea aplicable a versiones específicas de los modelos GPT-2.

Ⅱ. API ONNX de Microsoft (https://github.com/microsoft/onnxruntime-genai)

La biblioteca "onnxruntime-genai" de Microsoft proporciona un conjunto de API de conversión y optimización ONNX para modelos de IA generativa.

  • Ventajas: Optimizado para el tiempo de ejecución de ONNX, mejora el rendimiento de la inferencia y cuenta con el soporte y mantenimiento de Microsoft.
  • Desventajas: Es necesario aprender la API de la biblioteca "onnxruntime-genai", que puede ser más compleja en comparación con otros métodos.

Ⅲ. Uso de "torch.onnx" para exportar el modelo

PyTorch proporciona una función integrada de exportación ONNX (torch.onnx), que permite exportar modelos PyTorch al formato ONNX.

  • Ventajas: Estrechamente integrado con el marco PyTorch, fácil de usar, "torch.onnx" es una herramienta de exportación ONNX muy utilizada.
  • Desventajas: Es posible que sea necesario resolver algunos problemas de compatibilidad de operadores, especialmente en el caso de operadores más nuevos o personalizados, y que sea necesario ajustar manualmente algunos parámetros de exportación para garantizar la corrección y el rendimiento del modelo.

Ⅳ. Uso de "transformers.onnx" para convertir el modelo

La biblioteca "transformers" de Hugging Face proporciona su propia herramienta de conversión ONNX (transformers.onnx), que puede convertir fácilmente los modelos de la biblioteca "transformers" al formato ONNX.

  • Ventajas: Sencillo y fácil de usar, proporciona una interfaz de línea de comandos sencilla para convertir modelos fácilmente, está estrechamente integrado con la biblioteca "transformers", admite múltiples modelos preentrenados y es mantenido y actualizado activamente por el equipo de Hugging Face.
  • Desventajas: En comparación con "torch.onnx", "transformers.onnx" es una herramienta relativamente nueva y puede presentar problemas de compatibilidad.

Ⅴ. Usando Optimum

Optimum es una biblioteca de herramientas lanzada por Hugging Face para la optimización y aceleración de modelos, que también ofrece la funcionalidad de conversión ONNX.

  • Ventajas: Integración optimizada, puede combinar la conversión ONNX con otras técnicas de optimización (como cuantificación y la poda), y cuenta con el soporte y mantenimiento del equipo de Hugging Face.
  • Desventajas: Es necesario aprender a utilizar la biblioteca Optimum, lo que requiere ciertos conocimientos técnicos.

Estos métodos de conversión tienen sus propias ventajas y desventajas. No es necesario que se limite al método utilizado en este artículo, sino que puede elegir el método más adecuado en función de sus necesidades. Nuestro ejemplo utilizará la biblioteca "transformers.onnx" para convertir el modelo GPT-2.

2. Conversión del modelo GPT-2 al modelo ONNX

Después de decidir utilizar "transformers.onnx" para la conversión del modelo, a continuación proporcionaremos un proceso de conversión detallado.

Ⅰ. Instalar dependencias

Primero, asegúrese de que las bibliotecas "transformers" y "onnx" estén instaladas. Si no están instalados, puede utilizar el siguiente comando para instalarlos:

pip install transformers onnx

Si necesitas optimizar para hardware específico, como por ejemplo usar la aceleración por GPU, también necesitas instalar "onnxruntime-gpu":

pip install onnxruntime-gpu

Ⅱ. Comando de conversión

"transformers.onnx" proporciona una herramienta de línea de comandos sencilla. Sin requisitos especiales, usar esta herramienta para la conversión de modelos es sencillo; basta con ejecutar el siguiente comando:

python -m transformers.onnx --model=path/to/your/tuned_model --feature=causal-lm-with-past path/to/save/onnx_model

Los parámetros de este comando:

  • python -m transformers.onnx: Llama a la herramienta "transformers.onnx".
  • --model=path/to/your/tuned_model: Especifica la ruta del modelo GPT-2 ajustado. En nuestro ejemplo, esta ruta es "gpt2_Adapter-tuning".
  • --feature=causal-lm-with-past: Especifica el tipo de funcionalidad del modelo. Dado que estamos utilizando un modelo de lenguaje causal y necesitamos admitir "past_key_values" para mejorar la eficiencia de generación, elegimos "causal-lm-with-past".
  • path/to/save/onnx_model: Especifica la ruta para guardar el modelo ONNX. Por ejemplo, podemos configurarlo como "gpt2_onnx".

Comando de ejemplo completo:

python -m transformers.onnx --model=gpt2_Adapter-tuning --feature=causal-lm-with-past gpt2_onnx

Ejecute el comando anterior en la línea de comandos, "transformers.onnx" descargará automáticamente los archivos de configuración necesarios y convertirá el modelo al formato ONNX. Una vez completada la conversión, verá un archivo llamado "model.onnx" en el directorio de salida especificado (en este caso, "gpt2_onnx"), junto con algunos posibles archivos JSON, como "config.json".

Sin embargo, si necesitas ajustar algunos parámetros durante el proceso de conversión del modelo para adaptarlo mejor al caso de uso actual, esta herramienta claramente no puede satisfacer nuestras necesidades. Por lo tanto, para escenarios de aplicación complejos, sigue siendo necesario escribir scripts adecuados para la conversión con el fin de tener un control más preciso sobre el formato del modelo exportado.

Ⅲ. Script de conversión

Para convertir el modelo GPT-2 ajustado con Adapter-Tuning, el proceso de conversión necesita cargar el módulo Adapter y su configuración, y también establecer la versión de ONNX OP para evitar problemas de compatibilidad. A continuación, implementaremos las funciones relevantes paso a paso según nuestras necesidades.

Primero, importamos las bibliotecas de Python necesarias y las clases Adapter() y GPT2LMHeadModelWithAdapters(). Estas clases se presentaron en detalle en el artículo anterior (Añadimos un LLM personalizado a un robot comercial (Parte 5): Desarrollar y probar la estrategia de negociación con LLMs (III) Ajuste del adaptador). Puedes optar por importar directamente desde el script existente, y aquí copiamos estas clases al script de conversión para una mejor comprensión:

import os
import logging
from pathlib import Path
from transformers.onnx import export, FeaturesManager
from transformers import AutoConfig, AutoTokenizer, GPT2LMHeadModel, modeling_outputs
from torch import nn
import torch.nn.functional as F
import onnx
# Set up basic configuration for logging
logging.basicConfig(level=logging.INFO)
tokenizer = AutoTokenizer.from_pretrained('gpt2')
# Define the Adapter class, which is a simple feed-forward network with dropout
class Adapter(nn.Module):
    def __init__(self, in_features, bottleneck_features=64):
        super(Adapter, self).__init__()
        # Down projection layer
        self.down_project = nn.Linear(in_features, bottleneck_features)
        # Up projection layer
        self.up_project = nn.Linear(bottleneck_features, in_features)
        # Dropout layer for regularization
        self.dropout = nn.Dropout(0.1)
        # Initialize weights of the layers
        self.init_weights()

    def init_weights(self):
        # Initialize weights for down projection layer
        nn.init.normal_(self.down_project.weight, mean=0.0, std=0.02)
        nn.init.constant_(self.down_project.bias, 0)
        # Initialize weights for up projection layer
        nn.init.normal_(self.up_project.weight, mean=0.0, std=0.02)
        nn.init.constant_(self.up_project.bias, 0)

    def forward(self, hidden_states):
        # Apply down projection and ReLU activation
        hidden_states = self.down_project(hidden_states)
        hidden_states = F.relu(hidden_states)
        # Apply dropout
        hidden_states = self.dropout(hidden_states)
        # Apply up projection
        hidden_states = self.up_project(hidden_states)
        # Apply dropout again
        hidden_states = self.dropout(hidden_states)
        return hidden_states

# Define the GPT2LMHeadModelWithAdapters class, which inherits from GPT2LMHeadModel
# and adds adapter layers to each transformer layer
class GPT2LMHeadModelWithAdapters(GPT2LMHeadModel):
    def __init__(self, config):
        super().__init__(config)
        # Create a list of adapter modules, one for each transformer layer
        self.adapters = nn.ModuleList([Adapter(config.n_embd) for _ in range(config.n_layer)])

    def forward(
        self,
        input_ids=None,
        past_key_values=None,
        attention_mask=None,
        token_type_ids=None,
        position_ids=None,
        head_mask=None,
        inputs_embeds=None,
        encoder_hidden_states=None,
        encoder_attention_mask=None,
        labels=None,
        use_cache=None,
        output_attentions=None,
        output_hidden_states=None,
        return_dict=None,
    ):
        # Get the outputs from the transformer
        transformer_outputs = self.transformer(
            input_ids,
            past_key_values=past_key_values,
            attention_mask=attention_mask,
            token_type_ids=token_type_ids,
            position_ids=position_ids,
            head_mask=head_mask,
            inputs_embeds=inputs_embeds,
            encoder_hidden_states=encoder_hidden_states,
            encoder_attention_mask=encoder_attention_mask,
            use_cache=use_cache,
            output_attentions=output_attentions,
            output_hidden_states=output_hidden_states,
            return_dict=return_dict,
        )
        hidden_states = transformer_outputs[0]

        # Apply each adapter to the hidden states
        for i, adapter in enumerate(self.adapters):
            hidden_states = hidden_states + adapter(hidden_states)

        # Get the logits for the language modeling head
        lm_logits = self.lm_head(hidden_states)

        # Compute loss if labels are provided
        loss = None
        if labels is not None:
            # Shift logits and labels for loss computation
            shift_logits = lm_logits[..., :-1, :].contiguous()
            shift_labels = labels[..., 1:].contiguous()
            # Flatten the logits and labels for cross-entropy loss
            loss_fct = nn.CrossEntropyLoss()
            loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))

        # Return the outputs in the appropriate format
        if not return_dict:
            output = (lm_logits,) + transformer_outputs[1:]
            return ((loss,) + output) if loss is not None else output

        return modeling_outputs.CausalLMOutputWithCrossAttentions(
            loss=loss,
            logits=lm_logits,
            past_key_values=transformer_outputs.past_key_values,
            hidden_states=transformer_outputs.hidden_states,
            attentions=transformer_outputs.attentions,
            cross_attentions=transformer_outputs.cross_attentions,
        )

A continuación, necesitamos cargar el modelo GPT-2 ajustado y controlar el proceso de conversión del modelo. Utilizamos la función "load_model_and_tokenizer()" para cargar el modelo GPT-2 ajustado, la función "export_model_to_onnx()" para convertir el modelo al formato ONNX y la función "main()" para controlar todo el proceso y las rutas de entrada/salida. Finalmente, definimos una función "check_onnx()" para verificar los resultados de la exportación y una función "quantization()" para la cuantización. Aquí tenéis un ejemplo:

# Function to load the model and tokenizer
def load_model_and_tokenizer(model_id):
    try:
        # Load the model configuration
        config = AutoConfig.from_pretrained(model_id)
        # Load the model
        model = GPT2LMHeadModelWithAdapters.from_pretrained(model_id)
        # Load the tokenizer
        # tokenizer = AutoTokenizer.from_pretrained('gpt2')
        return config, model,tokenizer
    except Exception as e:
        # Log any errors that occur during loading
        logging.error(f"Error loading model and tokenizer: {e}")
        raise

# Function to export the model to ONNX format
def export_model_to_onnx(model, config, tokenizer, output_path, opset):
    try:
        # Get the appropriate feature for the model
        model_type = config.model_type.replace("-", "_")
        feature = "causal-lm-with-past"
        # Get the ONNX configuration
        onnx_config_constructor = FeaturesManager.get_config(model_type, feature=feature)
        onnx_config = onnx_config_constructor(config)
        # Create the output directory if it doesn't exist
        if not os.path.exists(output_path.parent):
            os.makedirs(output_path.parent)

        # Export the model to ONNX
        export(
            model=model,
            config=onnx_config,
            opset=opset,
            output=output_path,
            preprocessor=tokenizer,
        )
        # Log success message
        logging.info(f"Model successfully converted to ONNX and saved in {output_path}")
    except Exception as e:
        # Log any errors that occur during export
        logging.error(f"Error exporting model to ONNX: {e}")
        raise

# Main function to orchestrate the process
def main():
    # Define the model ID, output path, and ONNX opset version
    model_id = "gpt2_Adapter-tuning"
    onnx_path = "./gpt2_onnx"
    out_path = Path(os.path.join(onnx_path, "gpt2_adapter_tuning.onnx"))
    opset = 14

    # Load the model and tokenizer
    config, model, tokenizer = load_model_and_tokenizer(model_id)
    # Export the model to ONNX
    export_model_to_onnx(model, config, tokenizer, out_path, opset)

def check_onnx():

    # Check the ONNX model
    onnx_model = onnx.load("gpt2_onnx/gpt2_adapter_tuning.onnx")
    onnx.checker.check_model(onnx_model)
    print("ONNX model check passed!")

def quantization():

    from onnxruntime.quantization import quantize_dynamic, QuantType

    # load model
    model_path = "gpt2_onnx/gpt2_adapter_tuning.onnx"
    onnx_model = onnx.load(model_path)

    #dynamic quantize INT4
    quantized_model_path = "gpt2_onnx/quantized_gpt2.onnx"
    quantize_dynamic(model_path, quantized_model_path, weight_type=QuantType.QUInt4)

    print(f"Save the quantized model to: {quantized_model_path}")

La implementación de esta parte del código no presenta ninguna dificultad, y el código contiene comentarios detallados, por lo que no la discutiremos en detalle. Solo analizaremos las partes clave del código:

  • Debe utilizar la clase de modelo con el módulo Adapter para cargar el modelo ajustado:

model = GPT2LMHeadModelWithAdapters.from_pretrained(model_id)

  • Las rutas de archivo deben convertirse al formato de ruta compatible con "transformers.onnx.export()" y no pueden usarse directamente como rutas de cadena. Utilizamos la clase "Path" de la biblioteca "pathlib" para convertir:

out_path = Path(os.path.join(onnx_path, "gpt2_adapter_tuning.onnx"))

  • Los parámetros "tokenizer" y "preprocessor" de la función "export()" solo pueden establecerse uno. De lo contrario, mostrará un error. Se recomienda utilizar "preprocessor":

export(model=model, config=onnx_config, opset=opset, output=output_path, preprocessor=tokenizer)

  • Determine la versión del conjunto de operaciones, que debe corresponder a la versión del conjunto de operaciones compatible con MQL5 para cargarse correctamente. Elegimos opset=14:

opset = 14

  • La ruta de entrada del modelo (es decir, la carpeta que contiene el modelo GPT-2 ajustado con Adapter-Tuning) se establece en la carpeta "gpt2_Adapter-tuning" dentro de la ruta del proyecto actual, y la ruta de salida se establece en la carpeta "gpt2_onnx" dentro de la ruta del proyecto actual:

model_id = "gpt2_Adapter-tuning"
onnx_path = "./gpt2_onnx"

  • Las funciones "check_onnx()" y "quantization()" no son obligatorias y se proporcionan como referencia.

Por supuesto, este es solo un ejemplo básico de script de conversión. No hemos establecido más detalles, como el soporte de entrada dinámica para secuencias. Si necesita la funcionalidad correspondiente, agregue las características relevantes basándose en el script de ejemplo.

El script de conversión completo también se proporciona en el archivo adjunto, llamado "torch2onnx.py".


Formulación de la estrategia del EA y la funcionalidad del servidor

Hemos determinado el modo de funcionamiento del EA. A continuación, debemos especificar un plan para determinar qué servicios proporciona el servidor y qué características integra el cliente: El cliente EA es el principal responsable de la recopilación de datos y la implementación de transacciones; el servidor Python recibe los datos enviados por el cliente, calcula los resultados de la inferencia y envía los resultados de vuelta al cliente; el cliente EA y el servidor Python se comunican a través de Socket.

1. Estrategia del EA

A continuación, diseñaremos una estrategia de negociación basada en los resultados de predicción de GPT-2. Dado que el objetivo de este artículo es demostrar cómo integrar el modelo GPT-2 en el EA MQL5, crearemos una estrategia de negociación simple como ejemplo. Cabe destacar que esta es una estrategia de ejemplo simple con fines demostrativos únicamente y no constituye ningún consejo de inversión real. En las aplicaciones prácticas, es necesario desarrollar estrategias de negociación más completas y robustas, y llevar a cabo pruebas retrospectivas exhaustivas y evaluaciones de riesgos.

Lógica de la estrategia del EA:

  • Obtenga los datos del precio de cierre de los últimos 20 puntos temporales cada minuto.
  • Transmite los datos al servidor y espera a que el servidor devuelva los resultados del cálculo.
  • Enviar órdenes de negociación basándose en las señales de negociación devueltas por el servidor, sin establecer stop loss ni take profit, y manteniendo siempre una sola orden.

2. Diseño de funciones del servidor

En el lado del servidor, tenemos que implementar las funciones principales de recibir datos del cliente del EA, ejecutar la inferencia del modelo para obtener resultados y calcular las señales de negociación que se enviarán de vuelta al cliente en función de los resultados de la inferencia.

Funciones del lado del servidor:

  • Recibir datos del cliente.
  • Cargue el modelo GPT-2 y el tokenizador y mantenga el modelo listo en todo momento.
  • Realice la inferencia y calcule la diferencia entre el precio actual real y la media del precio previsto en función de los resultados de la inferencia. Si la diferencia supera 0, envíe una señal de "compra"; si es menor que 0, envíe una señal de "venta"; si es igual a 0, no envíe ninguna señal.
  • Verifique y decida si utilizar la CPU o la GPU para la inferencia del modelo (dependiendo del modo admitido por el dispositivo actual).

A continuación, completaremos la implementación de la funcionalidad correspondiente.


Creación del servicio de inferencia

En cuanto a cómo crear el servicio de inferencia, he proporcionado una descripción detallada en un artículo anterior (Marcado de datos en el análisis de series temporales (Parte 5): Aplicación y comprobación de asesores usando Socket), donde se proporciona el script "server.py" utilizado aquí). La parte del código seguirá la lógica principal del script "server.py" del artículo anterior, solo que adaptándola a nuestro modelo GPT-2 ajustado y realizando algunas otras optimizaciones y mejoras.

El código modificado presenta principalmente los siguientes cambios:

  • Adaptar la inferencia del modelo al modelo GPT-2, con cambios significativos en la función "eva()".
  • Optimizar la lógica de negociación de sockets, añadiendo la capacidad de reconectar al cliente sin reiniciar el servidor tras la desconexión, lo que resulta más conveniente para las pruebas retrospectivas y no requiere reiniciar el servidor después de dichas pruebas.
  • Añade la detección del estado de la conexión del cliente para evitar el desperdicio innecesario de recursos.
  • Evite imprimir resultados de forma redundante; imprima los resultados solo cuando cambien los resultados de la predicción.
  • Agregue lógica de manejo de errores para evitar fallos del servidor.
  • Optimice la lógica general del código.

En lo que respecta al código, este artículo no profundizará en los detalles y solo abordará las partes que deben modificarse.

1. Importar las bibliotecas necesarias

Además de importar las bibliotecas normales necesarias, también necesitamos importar las clases Adapter y GPT2LMHeadModelWithAdapters que hemos incorporado al script. Puedes obtener estas clases de mi artículo anterior sobre el ajuste fino de GPT-2, o importarlas directamente desde el archivo "torch2onnx.py" proporcionado en este artículo. El código de ejemplo opta por importar las dos clases directamente desde "torch2onnx.py".

import socket
from time import sleep
import pandas as pd
import numpy as np
import warnings
import base64
import hashlib
import struct
from torch2onnx import GPT2LMHeadModelWithAdapters,Adapter
from transformers import AutoTokenizer
import logging
import torch
from statistics import mean
# Set logging and warning
logging.basicConfig(level=logging.INFO)
warnings.filterwarnings("ignore")
# Set device
dvc='cuda' if torch.cuda.is_available() else 'cpu'

# Global 
model_id = "gpt2_Adapter-tuning"
encoder_length=20
prediction_length=10
info_file="results.json"
host="0.0.0.0"
port=10055

2. Agregue la lógica de carga del modelo GPT-2 en la función "load_model()".

En el script original (server.py), la función "load_model()" se utiliza para cargar el modelo. Tenga en cuenta que debemos agregar aquí la lógica de carga del modelo GPT-2, así como la lógica de carga para el tokenizador GPT-2.

# Function to loda model
def load_model():
    try:
        # Load the model
        model = GPT2LMHeadModelWithAdapters.from_pretrained(model_id).to(dvc)
        # Load the tokenizer
        tokenizer = AutoTokenizer.from_pretrained('gpt2')
        print("Model loaded!")
        return  model,tokenizer
    except Exception as e:
        # Log any errors that occur during loading
        logging.error(f"Error loading model and tokenizer: {e}")
        raise

3. Agregar lógica de inferencia del modelo GPT-2 en la función "eva()"

def eva(msg,model,tokenizer):
        
        # Get the data
        msg=np.fromstring(msg, dtype=float, sep= ',').tolist()
        # Parse the data
        input_data=msg[-encoder_length:]
        # Create the prompt
        prompt = ' '.join(map(str, input_data))
        # Generate the predication
        token=tokenizer.encode(prompt, return_tensors='pt').to(dvc)
        attention_mask = torch.ones_like(token).to(dvc)
        model.eval() 
        generated = tokenizer.decode(
            model.generate(
                token, 
                attention_mask=attention_mask,
                pad_token_id=tokenizer.eos_token_id,
                do_sample=True, 
                max_length=200)[0], 
            skip_special_tokens=True)
        generated_prices=generated.split('\n')[0]

        # Remove non-numeric formats
        def try_float(s):
            try:
                return float(s)
            except ValueError:
                return None
        generated_prices=generated_prices.split()
        generated_prices=list(map(try_float,generated_prices))
        generated_prices = [f for f in generated_prices if f is not None]

        generated_prices=generated_prices[0:prediction_length]
        
        # Calculate and send the results
        last_price=input_data[-1]
        prediction_mean=mean(generated_prices)
        if (last_price-prediction_mean) >= 0:
            # print('Send sell.')
            return "sell" 
        else:
            # print("Send buy.")
            return "buy"

Tenga en cuenta que la longitud de entrada debe coincidir con el formato de datos utilizado al entrenar el modelo GPT-2 con Adapter-Tuning:

  • input_data = msg[-encoder_length:]: Toma los últimos 20 puntos de datos enviados por el cliente como entrada del modelo.
  • prompt = ' '.join(map(str, input_data)): Convierta los datos a formato de cadena y conviértalos a un mensaje.
  • token = tokenizer.encode(prompt, return_tensors='pt').to(dvc): Utilice el tokenizador del modelo GPT-2 preentrenado para codificar la indicación y transferirla al dispositivo compatible actual (que coincida con el dispositivo utilizado para la inferencia del modelo).
  • attention_mask = torch.ones_like(token).to(dvc): Defina la máscara de atención para la inferencia del modelo.
  • model.generate(token, attention_mask=attention_mask, pad_token_id=tokenizer.eos_token_id, do_sample=True, max_length=200)[0]: Ejecuta la inferencia del modelo.
  • generated = tokenizer.decode(model.generate(...), skip_special_tokens=True): Descodificar los resultados de la predicción y configurar para omitir tokens especiales.
  • generated_prices = generated.split('\n')[0]: Dividir los resultados de inferencia decodificados.
  • try_float(s): Esta función se utiliza para detectar si hay elementos en los resultados de la inferencia que no se pueden convertir al formato flotante.
  • generated_prices = generated_prices.split(): Separe los resultados de la predicción con espacios y elimine los separadores que no se puedan convertir en números.
  • generated_prices = list(map(try_float, generated_prices)): Convierte todos los elementos de generated_prices a números en formato flotante. Si hay elementos que no se pueden convertir a números, utiliza la función try_float(s) para establecerlos como None.
  • generated_prices = [f for f in generated_prices if f is not None]: Recorre todos los elementos de generated_prices y elimina los elementos que sean None.
  • generated_prices = generated_prices[0:prediction_length]: Obtenga solo los primeros 10 valores predichos como referencia.
  • if (last_price - prediction_mean) >= 0: Calcula la diferencia entre los últimos datos enviados por el cliente y la media de los valores previstos. Si es mayor o igual a 0, envía una señal de «venta»; si es menor que 0, envía una señal de «compra».

Hemos optado por utilizar la biblioteca "transformers" para la inferencia. También puede utilizar el script "torch2onnx.py" mencionado anteriormente para convertir el modelo al formato ONNX y utilizar la biblioteca "onnxruntime" para la inferencia. Este método no se tratará en este artículo.

4. Servidor

Toda la funcionalidad del servidor está integrada en la clase "server_()", y los cambios generales en el código no son significativos. Aquí no lo interpretaremos en detalle y solo hablaremos de las partes que han sido modificadas.

class server_:
    def __init__(self, host = host, port = port):
        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,self.tokenizer=load_model()
        self.stop=None
        self.sk.listen(1)
        self.sk_, self.ad_ = self.sk.accept()
        self.last_action=None
        print('server running:',self.sk_, self.ad_)  

    def msg(self):
        self.re = ''
        wsk=False
        while True:
            sleep(0.5)
            if self.is_connected():
                try:
                    data = self.sk_.recv(2500)
                except Exception as e:
                    break
                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: 
                    self.handshake(data)
                    data=data.split('\r\n\r\n',1)[1]
                if "stop" in data:
                    self.stop=True
                    break
                if len(data)<50:
                    break
                self.re+=data
                bt=eva(self.re, self.model,self.tokenizer)
                bt=bytes(bt, "utf-8")
                # If the signal changes,then print the information
                if bt != self.last_action:
                    if bt == b'buy':
                        print('Send buy.')
                    elif bt == b'sell':
                        print('Send sell.')
                    self.last_action = bt 
                if wsk:
                    tk=b'\x81'
                    lgt=len(bt)
                    tk+=struct.pack('B',lgt)
                    bt=tk+bt
                self.sk_.sendall(bt)
            else:
                print("Disconnected!Try to connect the client...")
                try:
                    # reconnect
                    self.sk_.close()
                    self.sk.listen(1)
                    self.sk_, self.ad_ = self.sk.accept()
                    print('Reconnected:', self.sk_, self.ad_)
                    # handshake
                    while True:
                        sleep(0.5)
                        data = self.sk_.recv(2500)
                        data=data.decode("utf-8")
                        if '\r\n\r\n' in data:
                            self.handshake(data)
                            break
                    print("Reconnection succeed!")
                    # # clean the socket
                    # while True:
                    #     if not self.sk_.recv(2500):
                    #         break
                except Exception as e:
                    print(f"Reconnection failed: {e}")
        return self.re
        
    def __del__(self):
        print("server closed!")
        self.sk.close()
        if self.sk_ is not None:
            self.sk_.close()
            self.ad_.close()
    def handshake(self,data):
        try:           
            # Handshake
            key = data.split("\r\n")[4].split(": ")[1]
            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:10055")
            self.sk_.send(bytes(response_str, encoding='utf-8'))
            print('Handshake succeed!')
        except Exception as e:
            print(f"Connection failed: {e}")
            return None
        
    def is_connected(self):
        try:
        # Check remote 
            # remote_addr = self.sk_.getpeername()
            data = self.sk_.recv(1, socket.MSG_PEEK)
            return True
        except socket.error:
            self.last_action=None
            return False

  • Agregue la función de clase "is_connected(self)" para detectar si el cliente está en línea.
  • Agregue la función de clase "handshake(self, data)" para integrar la lógica de handshake y evitar sobrecargar la lógica de análisis principal.
  • Agregue el miembro de clase "self.last_action" para detectar si la señal de negociación ha cambiado. Imprima los resultados solo cuando cambie la señal de negociación para evitar impresiones frecuentes. Cuando el cliente se desconecte, restablezca el valor a Ninguno para evitar enviar señales incorrectas cuando el cliente se vuelva a conectar.

Nota: Nuestra dirección de host está configurada en "0.0.0.0" porque si está configurada en "127.0.0.1", los clientes remotos que se ejecutan en un host diferente no podrán conectarse. Esto significa que al configurarlo en "0.0.0.0", incluso si el servidor y el cliente no están en el mismo host, aún pueden conectarse (el cliente EA necesita configurar la dirección IP del host correcta).

El código completo se encuentra en el archivo "server.py" adjunto. Cuando el servidor esté en funcionamiento, la terminal mostrará la información correspondiente.

Servidor



Cliente del EA

El cliente sigue principalmente la lógica de un artículo anterior (mencionada específicamente en un artículo anterior), con las modificaciones lógicas correspondientes. Aún conserva dos métodos de compatibilidad de sockets (uno que utiliza WinAPI para implementar WebSocket y el otro que utiliza el módulo Socket integrado en MQL5) para evitar interrupciones de señal causadas por la incapacidad del Socket integrado de MQL5 para conectarse en circunstancias especiales. La lógica operativa principal consiste en inicializar el Socket en la función "OnInit()", manejar la lógica de negociación en la función "OnTick()" y manejar el envío de datos al servidor y la recepción de resultados de inferencia cada tiempo fijo en la función "OnTimer()".

1. Definir constantes

#include <WinAPI\winhttp.mqh>

int sk=-1;
string host="127.0.0.1";
int port= 10055;
int data_len=100;
string pre=NULL;
HINTERNET ses_h,cnt_h,re_h,ws_h;

  • `sk`: Identificador de socket.
  • `host` y `port`: Dirección del servidor y puerto al que conectarse.
  • `data_len`: Número de puntos de datos de precios a enviar.
  • `pre`: Cadena para almacenar los resultados de la predicción.
  • `ses_h`, `cnt_h`, `re_h`, `ws_h`: Identificador de sesión, identificador de conexión, identificador de solicitud e identificador de WebSocket para WinHttp, respectivamente.

2. Inicializar Socket

int OnInit()
  {
//--- create timer
   EventSetTimer(60);
   ses_h=cnt_h=re_h=ws_h=NULL;
//handshake
   ses_h=WinHttpOpen("MT5",
                     WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                     NULL,
                     NULL,
                     0);
   //Print(ses_h);
   if (ses_h==NULL){
      Print("Http open failed!",string(kernel32::GetLastError()));
      return INIT_FAILED;
      }
   cnt_h=WinHttpConnect(ses_h,
                        host,
                        port,
                        0);
   //Print(cnt_h);
   if (cnt_h==NULL){
      Print("Http connect failed!",string(kernel32::GetLastError()));
      return INIT_FAILED;
      }
   re_h=WinHttpOpenRequest(cnt_h,
                           "GET",
                           NULL,
                           NULL,
                           NULL,
                           NULL,
                           0);
   if(re_h==NULL){
      Print("Request open failed!",string(kernel32::GetLastError()));
      return INIT_FAILED;
   }
   uchar nullpointer[]= {};
   if(!WinHttpSetOption(re_h,WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,nullpointer,0))
     {
          Print("Set web socket failed!",string(kernel32::GetLastError()));
          return INIT_FAILED;
       }
   bool br;   
   br = WinHttpSendRequest( re_h,
                             NULL, 
                             0,
                             nullpointer, 
                             0, 
                             0, 
                             0);
   if (!br)
      {
         Print("send request failed!",string(kernel32::GetLastError()));
         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;
         }
    else{
      Print("Web socket connected!");
    }   
  
   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 built-in socket");
        //return INIT_FAILED;
    }
//---
   return(INIT_SUCCEEDED);
  }

En la parte de inicialización, implementamos principalmente la inicialización de WinAPI WebSocket y el Socket integrado en MQL5. Esta parte no ha cambiado mucho en comparación con el contenido del artículo anterior, por lo que no se tratará en este artículo.

3. Estrategia de trading

void OnTick()
  {
//---
   MqlTradeRequest request;
   MqlTradeResult result;
   //int x=SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE);

    if (pre!=NULL)
    {
        //Print("The predicted value is:",pre);
        ulong numt=0;
        ulong tik=0;
        bool sod=false;
        ulong tpt=-1;
        ZeroMemory(request); 
        numt=PositionsTotal();
        //Print("All tickets: ",numt);
        if (numt>0)
         {  tik=PositionGetTicket(numt-1);    
            sod=PositionSelectByTicket(tik);
            tpt=PositionGetInteger(POSITION_TYPE);//ORDER_TYPE_BUY or ORDER_TYPE_SELL
            if (tik==0 || sod==false || tpt==0) return; 
            }
        if (pre=="buy")
        {  
           
           if (tpt==POSITION_TYPE_BUY)
               return;
               
            request.action=TRADE_ACTION_DEAL;
            request.symbol=Symbol();
            request.volume=0.1;
            request.deviation=5;
            request.type_filling=ORDER_FILLING_IOC;
            request.type = ORDER_TYPE_BUY;  
            request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
           if(tpt==POSITION_TYPE_SELL)
             {
               request.position=tik;
               Print("Close sell order.");
                    }
           else{     
  
            Print("Open buy order.");
                     }
            OrderSend(request, result);
               }
        else{
           if (tpt==POSITION_TYPE_SELL)
               return;
               
            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; 
            //request.type_filling=SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE);
            request.type_filling=ORDER_FILLING_IOC;
           if(tpt==POSITION_TYPE_BUY)
               {
               request.position=tik;
               Print("Close buy order.");
                    }
           else{

               Print("OPen sell order.");
                    }
            
            OrderSend(request, result);
              }
        //is_pre=false;
        }
    pre=NULL;
  }

Integramos toda la estrategia de negociación en la función "OnTick()" para que la lógica sea más clara. Cuando se ejecute la función OnTick(), compruebe si la variable global "pre" está vacía. Si no está vacío, indica que se han enviado resultados de predicción desde el cliente.

A continuación, envíe solicitudes de negociación basadas en los resultados de la predicción ("compra" o "venta"):

  • Si es "compra", abra una posición si no existe ninguna o cierre una orden de venta existente.
  • Si se selecciona "vender", abra una posición si no existe ninguna o cierre una orden de compra existente.

Mantenga una sola orden durante toda la operación, sin establecer objetivos de ganancias ni límites de pérdidas, y controle la posición únicamente mediante señales de negociación.

4. Interacción con el servidor

void OnTimer()
  {
//---
    MqlTradeRequest request;
    MqlTradeResult result;
    char recv_data[5];
    double priceData[100];
    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"))
           {
            pre="buy";
           }
           if (StringFind(result,"sell")){
             pre="sell";

               }
            }
          }
     }
    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)
                {
                    pre=NULL; 
                    pre+=CharArrayToString(recv_data,0);
                    Print("The predicted value is:",pre);
                     }
                 }
            }
        }           
  }

La función principal del servidor es recopilar 100 puntos de datos del gráfico actual, enviarlos al servidor y recibir los resultados de inferencia del mismo, para luego modificar la variable global en función de dichos resultados y así garantizar que la estrategia de negociación se ejecute de acuerdo con los resultados proporcionados por el servidor. Aquí utilizamos dos métodos de conexión de sockets para implementar la lógica de interacción de datos y seleccionamos automáticamente el método apropiado en función del tipo de socket conectado actualmente.

5. Lanzamiento de recursos

void OnDeinit(const int reason)
  {
//--- destroy timer
   EventKillTimer();
   uchar stop[];
   
   int ls=StringToCharArray("stop",stop);
   
   SocketSend(sk,stop,ls);
   SocketClose(sk);
 // close the websocket
   WinHttpSendRequest(re_h,NULL,0,stop,0,0,0);
   BYTE closearray[]= {};
   ulong close=WinHttpWebSocketClose(ws_h,
                                    WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS,
                                    closearray,
                                    0);
   if(close)
     {
      Print("websocket close error "+string(kernel32::GetLastError()));
      if(re_h!=NULL)
         WinHttpCloseHandle(re_h);

      if(ws_h!=NULL)
         WinHttpCloseHandle(ws_h);

      if(cnt_h!=NULL)
         WinHttpCloseHandle(cnt_h);

      if(ses_h!=NULL)
         WinHttpCloseHandle(ses_h);
     }   
  }

En la función "OnDeinit()", libere los recursos del sistema relacionados y realice la recuperación de recursos.

Como el proceso de inferencia del modelo GPT-2 no está implementado en el EA, esto hace que nuestra lógica del EA sea mucho más simple y concisa. Tenga en cuenta que no hemos añadido lógica de control de riesgos en el EA, y confiar únicamente en los resultados de la inferencia GPT-2 para decidir si mantener o abrir una posición es un enfoque muy arriesgado. Por favor, tenga en cuenta nuevamente que este ejemplo de EA no debe utilizarse para operaciones reales.

El código completo se proporciona en el archivo adjunto del artículo, llamado "gpt2_EA.mql5".


Pruebas retrospectivas

Para evaluar el rendimiento del EA, podemos realizar pruebas retrospectivas en el probador de estrategias del cliente MetaTrader 5. Seleccionamos el rango apropiado de datos históricos, configuramos los parámetros de la prueba retrospectiva y luego ejecutamos la prueba (dado que nuestro modelo gpt2 está entrenado con el par de divisas NZDUSD, solo podemos seleccionar el par de divisas NZDUSD para realizar la prueba retrospectiva).

Conjunto

Ejecutando la prueba retrospectiva:

Ejecutandolo

Una vez completada la prueba retrospectiva, los resultados son los siguientes:

Resultado

Puede analizar la rentabilidad, la caída máxima, la tasa de ganancias y otras métricas del EA revisando el informe de la prueba retrospectiva. Recuerde que nuestra estrategia de trading es sencilla, por lo que los resultados de las pruebas retrospectivas no son ideales. Esto se debe principalmente a que nuestra estrategia no ha sido sometida a ninguna optimización de parámetros ni control de riesgos, y el proceso de entrenamiento y la preparación de datos para el modelo tienen un importante potencial de optimización. En general, hacer esto requiere mucha paciencia. Es importante señalar que, debido a los cambios en las condiciones del mercado y a las limitaciones del modelo, los resultados de las pruebas retrospectivas no pueden garantizar el rendimiento del EA en futuras operaciones reales, y las limitaciones del modelo también pueden dar lugar a resultados de predicción inestables.



Conclusión

En este artículo, demostramos cómo integrar un modelo GPT-2 ajustado con datos financieros específicos (del par de divisas NZDUSD) en un programa EA, explicando sistemáticamente todo el proceso, desde el ajuste del modelo y la implementación de la lógica de inferencia hasta la configuración del servidor y del cliente, y finalmente la integración de estrategias de negociación.

Es importante destacar que el diseño de nuestra estrategia de negociación es relativamente simple y tiene únicamente fines demostrativos. En las aplicaciones prácticas, es necesario desarrollar estrategias más completas y robustas, como la combinación de múltiples indicadores técnicos, la consideración del sentimiento del mercado, el establecimiento de límites de pérdidas y de ganancias, etc.

Además, el proceso de entrenamiento y la preparación de datos para el modelo tienen un potencial de optimización significativo, y los cambios en las condiciones del mercado y las limitaciones del modelo pueden conducir a resultados de predicción inestables. A pesar de ello, la importancia de este trabajo radica en demostrar el potencial de los grandes modelos de lenguaje en el trading cuantitativo. Modelos como GPT-2 pueden analizar datos de mercado tradicionales y manejar datos de noticias, datos de redes sociales y otros datos ricos en texto, proporcionando un análisis de sentimiento de mercado más completo para ayudar a los operadores a tomar decisiones más acertadas. Esta capacidad intermodal es algo que los modelos financieros tradicionales no poseen y es un área que necesitamos explorar más a fondo.

En el próximo artículo, utilizaremos un ejemplo para demostrar cómo optimizar la aplicación de grandes modelos de lenguaje en el trading cuantitativo.


Apéndice:

Archivos
Descripción
torch2onnx.py Script de Python para convertir un modelo GPT-2 a formato ONNX.
server.py El script de Python para proporcionar servicios y resultados de inferencia del modelo GPT-2.
gpt2_EA.mq5 EA para probar los resultados de inferencia de los modelos GPT-2.

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

Archivos adjuntos |
server.py (7.5 KB)
torch2onnx.py (7.61 KB)
gpt2_EA.mq5 (11.49 KB)
Alpha Dolcy
Alpha Dolcy | 29 ene 2025 en 13:46
Genial.....lo revisaré en profundidad más tarde. Espero con impaciencia el próximo artículo
Desarrollamos un asesor experto multidivisas (Parte 23): Ordenando la cadena de etapas de optimización automática de proyectos (II) Desarrollamos un asesor experto multidivisas (Parte 23): Ordenando la cadena de etapas de optimización automática de proyectos (II)
Hoy nuestro objetivo consiste en crear un sistema de optimización periódica automática de las estrategias comerciales utilizadas en un asesor experto final. El sistema se vuelve más complejo a medida que se desarrolla, por lo que de vez en cuando debemos examinarlo en su conjunto para detectar cuellos de botella y soluciones subóptimas.
Simulación de mercado (Parte 15): Sockets (IX) Simulación de mercado (Parte 15): Sockets (IX)
En este artículo, explicaré una de las posibles soluciones a lo que he estado intentando mostrar. Es decir, cómo permitir que un usuario de Excel realice una acción en MetaTrader 5 sin enviar órdenes ni abrir o cerrar una posición. La idea es que el usuario utilice Excel para realizar un análisis fundamental de algún símbolo. Y que, usando únicamente Excel, pueda indicar a un Asesor Experto que se esté ejecutando en MetaTrader 5 que debe abrir o cerrar una posición determinada.
Simulación de mercado (Parte 16): Sockets (X) Simulación de mercado (Parte 16): Sockets (X)
Estamos a punto de concluir este desafío. Sin embargo, antes de pasar al siguiente, quiero que tú, querido lector, procures comprender estos dos artículos, tanto este como el anterior. Así podrás entender realmente el próximo artículo, en el que abordaré exclusivamente la parte referente a la programación en MQL5. Aunque en él también procuraré que sea fácil de entender. Si no comprendes estos dos últimos artículos, con toda seguridad tendrás grandes dificultades para entender el siguiente. El motivo es simple: los contenidos se van acumulando. Cuantas más cosas haya que hacer, más cosas será necesario crear y comprender para alcanzar el objetivo.
Simulación de mercado (Parte 14): Sockets (VIII) Simulación de mercado (Parte 14): Sockets (VIII)
Muchos podrían sugerir que deberíamos dejar de usar Excel y pasar a Python directamente, haciendo uso de algunos paquetes que permitirían a Python crear un archivo de Excel para poder analizar los resultados después. Pero, como se mencionó en el artículo anterior, aunque esta solución sea la más sencilla para muchos programadores, no será bien recibida por algunos usuarios. Y, en este asunto, el usuario siempre tiene la razón. Tú, como programador, debes encontrar la forma de hacer que las cosas funcionen.