English 日本語
preview
Integrieren Sie Ihr eigenes LLM in einen EA (Teil 5): Handelsstrategie mit LLMs(IV) entwickeln und testen - Test der Handelsstrategie

Integrieren Sie Ihr eigenes LLM in einen EA (Teil 5): Handelsstrategie mit LLMs(IV) entwickeln und testen - Test der Handelsstrategie

MetaTrader 5Handel | 3 Juni 2025, 07:49
146 1
Yuqiang Pan
Yuqiang Pan

Inhaltsverzeichnis

  1. Inhaltsverzeichnis
  2. Einführung
  3. Entwicklungsumgebung für das Beispiel in diesem Artikel
  4. Methoden zum Laden von LLMs in MQL5
  5. Konvertierung des GPT-2-Modells in das ONNX-Modell
  6. Formulierung der EA-Strategie und Server-Funktionalität
  7. Erstellen des Inferenzdienstes
  8. Der Client-EA
  9. Backtests
  10. Schlussfolgerung



Einführung

In früheren Artikeln haben wir vorgestellt, wie man vortrainierte GPT-2-Modelle mit verschiedenen Methoden feinabstimmen kann, damit GPT-2 Aufgaben nach unseren Wünschen ausführt, und wir haben diese Methoden über mehrere Dimensionen hinweg verglichen. Natürlich haben wir nur einige häufig verwendete Methoden vorgestellt, was nicht bedeutet, dass nur diese Methoden zur Feinabstimmung von GPT-2-Modellen verwendet werden können. Sie können versuchen, GPT-2 mit anderen Methoden, die auf unserem Beispielimplementierungsprozess basieren, fein abzustimmen, sie zu vergleichen und ein besseres Modell zu wählen. Wenn Sie während dieses Prozesses auf Probleme stoßen, können Sie am Ende des Artikels einen Kommentar hinterlassen.

Unser fein abgestimmtes GPT-2-Modell verfügt nun über die anfängliche Fähigkeit, einfache, quantitative Handelsstrategien auszuführen. Daher wird in diesem Artikel erläutert, wie wir unser fein abgestimmtes Modell in unsere quantitative Handelsstrategie integrieren können. Das im Beispiel verwendete Modell ist das GPT-2-Modell, das mit Adapter-Tuning feinabgestimmt wurde (spezifischer Artikellink: „Integrieren Sie Ihr eigenes LLM in einen EA (Teil 5): Handelsstrategie mit LLMs entwickeln und testen (III) – Adapter-Tuning“. Sofern nicht anders angegeben, beziehen sich daher alle Verweise auf GPT-2 in diesem Artikel auf dieses Modell.

Es ist jedoch zu beachten, dass das von uns feinabgestimmte Modell auf begrenzten Daten zu Demonstrationszwecken basiert und nicht für reale Handelsumgebungen geeignet ist. Ohne Tests und Optimierungen dürfen sie nicht direkt im realen Handel eingesetzt werden, was von größter Bedeutung ist. Unser früherer Vorhersagecode wurde in der Python-Umgebung erstellt, aber MQL5, als hochintegrierte Programmiersprache für die MetaTrader 5-Plattform, bietet leistungsstarke Werkzeuge zur Entwicklung von Expert Advisors (EAs). Um automatisierte quantitative Handelsstrategien zu implementieren, müssen wir daher zur MQL5-Umgebung zurückkehren. In diesem Artikel wird dieser Prozess Schritt für Schritt erläutert.

Schauen wir uns an, wie man dieses trainierte Modell aus der Python-Umgebung in den MQL5 EA migriert, sodass es direkt auf der MetaTrader 5-Plattform läuft und Echtzeit-Handelsentscheidungen unterstützt.


Entwicklungsumgebung für das Beispiel in diesem Artikel

Lassen Sie uns die Betriebsumgebung für die Code-Beispiele in diesem Artikel vorstellen. Das bedeutet natürlich nicht, dass Ihre Code-Umgebung die gleiche sein muss wie meine, aber wenn Sie beim Ausführen des Codes auf Probleme stoßen, können Sie sich an meiner Umgebungskonfiguration orientieren.

  • Betriebssystem: Ubuntu 22.04.5 LTS (oder die entsprechende Version von WSL)
  • Python-Version: 3.10.14
  • Erforderliche Python-Bibliotheken:

    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

Bevor Sie mit dem nächsten Schritt fortfahren beachten Sie bitte, dass Sie ein Modell mit Adapter-Tuning trainiert haben, wie im vorherigen Artikel beschrieben (da die Größe des Modells die Plattformgrenze überschreitet, können die bereits trainierten Gewichte nicht hochgeladen werden).


Methoden zum Laden von LLMs in MQL5

Um das trainierte GPT-2-Modell in den MQL5 EA zu integrieren, ist das erste Problem, das wir lösen müssen, wie wir dieses Modell, das im Wesentlichen ein in Python trainiertes Modell ist, in die MQL5-Umgebung laden und ausführen können. Hier sind mehrere Methoden denkbar:

1. Konvertieren Sie das Modell nach ONNX und fügen Sie es dem EA hinzu

ONNX (Open Neural Network Exchange) ist ein offenes Format zur Darstellung neuronaler Netze, das die Interoperabilität zwischen verschiedenen Deep-Learning-Frameworks ermöglicht. In meinem vorigen Artikel (Datenkennzeichnung für Zeitreihenanalyse (Teil 6): Anwendung und Test des EAs, der ONNX verwendet) habe ich die Integration einfacher Modelle in EAs mit Hilfe von ONNX vorgestellt. Wir können das GPT-2-Modell auch in das ONNX-Format konvertieren, es in den EA importieren und die integrierte ONNX-Laufzeitbibliothek in MQL5 verwenden, um die Modellinferenz durchzuführen. Für die MQL5-Unterstützung von ONNX siehe die Hilfedatei „MQL5 Reference / ONNX models“ oder die offizielle MQL5-Dokumentation (https://www.mql5.com/de/docs/onnx).

  • Vorteile:

  1. Hohe Leistung: Die ONNX-Laufzeit ist in der Regel auf Leistung optimiert, was eine relativ effiziente Inferenz im EA ermöglicht.
  2. Hohe Integration: MQL5 verfügt über integrierte Unterstützung für ONNX, sodass keine externen Programme oder Bibliotheken erforderlich sind.
  3. Unabhängigkeit: Das konvertierte ONNX-Modell kann unabhängig von der Python-Umgebung ausgeführt werden.

  • Nachteile:

  1. Komplexität der Umwandlung: Die Konvertierung komplexer Sprachmodelle in das ONNX-Format kann eine Herausforderung darstellen und erfordert die Behandlung von Kompatibilitätsproblemen der Operatoren.
  2. Schwierigkeiten bei der Fehlersuche: Das Debuggen von ONNX-Modellen ist weniger komfortabel als das Debuggen von Python-Modellen.

2. Direkte Ausführung von Python-Inferenzskripten mit Winapi

MQL5 bietet Zugang zu Winapi, sodass wir die Funktion „WinExec()“ von „kernel32.dll“ aufrufen können, um externe Programme auszuführen. Auf diese Weise können wir vorhandene Python-Skripte verwenden, um das GPT-2-Modell zu laden und Inferenzen durchzuführen, und dann das Skript im EA mit „WinExec()“ aufrufen und seine Ausgabeergebnisse analysieren (alternativ kann diese Funktionalität auch mit der Funktion „ShellExecuteW()“ aus „shell32.dll“ erreicht werden). Diese Methode erfordert eine gewisse Entwicklungserfahrung und Vertrautheit mit der Windows-Entwicklung, um sie zu implementieren.

  • Vorteile:
  1. Einfach und direkt: Das Modell muss nicht konvertiert werden, sondern nutzt direkt den vorhandenen Python-Code.
  2. Flexibilität: Sie können die umfangreichen Bibliotheken und Werkzeuge des Python-Ökosystems problemlos nutzen.
  • Nachteile:
  1. Mehrkosten für die Leistung: Für jede Schlussfolgerung muss ein neuer Python-Prozess gestartet werden, was zu erheblichem Leistungsmehraufwand und Ineffizienz führt.
  2. Abhängigkeit: Der EA hängt von der externen Python-Umgebung und den Skripten ab.
  3. Datenaustausch: Es erfordert den Datenaustausch zwischen MQL5 und Python, was die Komplexität erhöht.
  4. Sicherheit: Es können verschiedene unerwartete Situationen auftreten, die zu unkontrollierbaren Abstürzen führen können.

Anmerkung: Von dieser Methode ist dringend abzuraten! Ich stelle diese Lösung nur vor, um zu zeigen, dass sie machbar ist und in Tests oder unter kontrollierten Bedingungen verwendet werden kann. Verwenden Sie es nicht ohne ausreichendes Vertrauen.

3. Abrufen von Python-Inferenzergebnissen durch Socket-Kommunikation

Das ist ähnlich wie die zweite Methode, aber unter Verwendung von Socket-Kommunikation anstelle von Winapi (tatsächlich kann auch das HTTP-Protokoll verwendet werden, ähnlich wie die HTTP-Dienste, die von gängigen Inferenz-Frameworks bereitgestellt werden, die im Wesentlichen dasselbe wie Socket sind, und dieser Artikel wird nicht weiter darauf eingehen). Die spezifische Implementierungsmethode besteht darin, einen Socket-Server in Python laufen zu lassen, um das Modell zu laden und Inferenzen durchzuführen, wobei der EA als Client fungiert, der sich mit dem Server verbindet, Eingabedaten sendet und Inferenzergebnisse empfängt.

  • Vorteile:

  1. Bessere Leistung: Die Socket-Kommunikation kann den Overhead des Prozessstarts reduzieren und ist viel sicherer.
  2. Flexibilität: Dennoch können Sie die Vorteile von Python nutzen.

  • Nachteile:

  1. Komplexität: Sie müssen die Kommunikationslogik zwischen dem Socket-Server und dem Client implementieren.
  2. Abhängigkeit: Der EA hängt von der externen Python-Umgebung und dem Socket-Server ab, was einige Kenntnisse zur Einrichtung des Dienstes erfordert.
  3. Stabilität: Die Stabilität der Sockelverbindungen kann den Betrieb des EA beeinträchtigen.

Hinweis:Für diese Methode gibt es eine spezielle Implementierung in meinem früheren Artikel. Wenn Sie daran interessiert sind, können Sie den Artikel „Datenkennzeichnung für Zeitreihenanalyse (Teil 5): Anwendung und Test in einem EA, der Socket verwendet“.

Wir haben verschiedene Umrechnungsmethoden diskutiert. Derzeit tendiere ich weiterhin dazu, das GPT-2-Modell in das ONNX-Format zu konvertieren und in den EA zu integrieren, da dies auch plattformübergreifende Probleme lösen kann und der EA eine höhere Integration und Stabilität aufweist. Wenn die Parameter des ONNX-Modells jedoch zu groß sind, können sie nicht in MQL5 ausgeführt werden (zum Beispiel überschreitet unser aktuelles GPT-2-Modell die MQL5-Dateiladegrenze).

Eine weitere Herausforderung ist die Lösung des Tokenizer-Problems im Transformermodell, da Modelle wie GPT-2 mit einem Tokenizer ausgestattet sind, um Eingabeinformationen zu verarbeiten, und um das GPT-2-Modell in MQL5 auszuführen, müssen wir den GPT-2-Tokenizer in MQL5 erstellen, was ein bedeutendes Projekt ist. Das ist schwierig, aber nicht unmöglich. Die MQL5-Dateigrößenbeschränkung ist jedoch ein schwer zu lösendes Problem.

Obwohl ich versucht habe, sie auf das INT8-Format zu quantisieren, überschritt sie immer noch die Grenze und konnte nicht geladen werden. Wenn im INT4-Format quantisiert wird, unterstützt MQL5 keine quantisierten Modelle im INT4-Format, obwohl die Modellgröße den Anforderungen entspricht! Daher können wir diese Methode nur bedauernd aufgeben. Dennoch werde ich in diesem Artikel ein Beispiel für die Konvertierung unseres auf den Adapter abgestimmten GPT-2-Modells in das ONNX-Format geben, in der Hoffnung, dieses Problem bald lösen zu können!

In diesem Artikel habe ich mich entschieden, die Socket-Kommunikation mit dem Python-Inferenzdienst zu diskutieren. Der Vorteil dieser Methode besteht darin, dass sie die Datensicherheit gewährleisten und unsere EA-Implementierung vereinfachen kann. Im EA brauchen wir uns nur auf unsere Strategie und Handelslogik zu konzentrieren und müssen uns nicht um zusätzliche Fragen der Modulintegration kümmern. Ein weiterer Vorteil dieser Methode besteht darin, dass diese Methode auch dann für die Entwicklung des EA verwendet werden kann, wenn es vor Ort keine entsprechende Modellentwicklungsumgebung gibt, z. B. wenn das Modell auf einem entfernten Gerät entwickelt und trainiert wird, selbst wenn die Entwicklungsumgebung nicht mit der lokalen Umgebung kompatibel ist.

Auch wenn diese Methode technisch kompliziert erscheint und mehr Wissen erfordert, kann sie eine hohe operative Effizienz erreichen und die Unabhängigkeit des EA gewährleisten, was für ein effizientes Echtzeit-Handelsumfeld entscheidend ist. Da wir dies bereits in einem früheren Artikel ausführlich erörtert haben, wird in diesem Artikel nicht weiter auf die Einzelheiten eingegangen. Wenn Sie Fragen zu den Codebeispielen haben, können Sie die ausführliche Einführung im vorherigen Artikel nachlesen.


Konvertierung des GPT-2-Modells in das ONNX-Modell

Im vorherigen Abschnitt habe ich die verschiedenen Herausforderungen beschrieben, die bei der Konvertierung des fein abgestimmten GPT-2-Modells in das ONNX-Format und dessen Verwendung in MQL5 aufgetreten sind. Ich glaube aber immer noch, dass dies eine Richtung ist, die es wert ist, ausprobiert zu werden. Deshalb werde ich in diesem Artikel einen zusätzlichen Abschnitt verwenden, um vorzustellen, wie man dieses personalisierte, fein abgestimmte Modell in das ONNX-Format konvertiert, in der Hoffnung, dass jeder eine Lösung für die aktuelle Misere finden kann. Wenn Sie an diesem Teil nicht interessiert sind, können Sie diesen Abschnitt auslassen.

1. Modellumwandlungsmethoden

Ⅰ. Direkte Umwandlung (https://github.com/rayhern/convert-gpt2-xl-to-onnx)

Dieses GitHub-Repository bietet ein Skript zur direkten Konvertierung von GPT-2-Modellen, das auf der Bibliothek „Transformers“ von Hugging Face und dem Exporter „torch.onnx“ basiert. Aufgrund der langjährigen mangelnden Wartung durch den Autor kann es jedoch einige Einschränkungen aufweisen und ist möglicherweise nicht mit den neuesten Versionen der „Transformers“-Bibliothek kompatibel.

  • Vorteile: Bietet ein relativ einfaches Skript, das direkt verwendet werden kann; speziell für die Konvertierung von GPT-2-Modellen optimiert.
  • Nachteile: Der Wartungsstatus dieses Repositorys ist möglicherweise unklar, und es ist möglicherweise nicht mit den neuesten „Transformers“-Versionen kompatibel und nur auf bestimmte Versionen von GPT-2-Modellen anwendbar.

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

Microsofts Bibliothek „onnxruntime-genai“ bietet eine Reihe von ONNX-Konvertierungs- und Optimierungs-APIs für generative KI-Modelle.

  • Vorteile: Optimiert für die ONNX-Laufzeit, verbessert die Inferenzleistung und wird von Microsoft unterstützt und gepflegt.
  • Nachteile: Sie müssen die API der Bibliothek „onnxruntime-genai“ erlernen, die im Vergleich zu anderen Methoden komplexer sein kann.

Ⅲ. Verwendung von „torch.onnx“ zum Exportieren des Modells

PyTorch bietet eine eingebaute ONNX-Exportfunktion „torch.onnx“, die PyTorch-Modelle in das ONNX-Format exportieren kann.

  • Vorteile: Eng mit dem PyTorch-Framework integriert und einfach zu nutzen, ist „torch.onnx“ ein weit verbreitetes ONNX-Exportwerkzeug.
  • Nachteile: Möglicherweise müssen Sie einige Kompatibilitätsprobleme mit Operatoren lösen, insbesondere bei neueren oder nutzerdefinierten Operatoren, und einige Exportparameter manuell anpassen, um die Korrektheit und Leistung des Modells sicherzustellen.

Ⅳ. Konvertierung des Modells mit Hilfe von „transformers.onnx“

Hugging Face Bibliothek „transformers“ bietet ein eigenes ONNX-Konvertierungswerkzeug „transformers.onnx“, das Modelle aus der „transformers“-Bibliothek einfach in das ONNX-Format konvertieren kann.

  • Vorteile: Es ist einfach und leicht zu bedienen, bietet eine einfache Befehlszeilenschnittstelle zur einfachen Konvertierung von Modellen, ist eng mit der „transformers“-Bibliothek integriert, unterstützt mehrere vortrainierte Modelle und wird vom Hugging Face-Team aktiv gewartet und aktualisiert.
  • Nachteile: Im Vergleich zu „torch.onnx“ ist „transformers.onnx“ ein relativ neues Werkzeug und kann Kompatibilitätsprobleme haben.

Ⅴ. Optimum verwenden

Optimum ist eine von Hugging Face eingeführte Werkzeugbibliothek zur Modelloptimierung und -beschleunigung, die auch ONNX-Konvertierungsfunktionen bietet.

  • Vorteile: Optimierte Integration, kann ONNX-Konvertierung mit anderen Optimierungstechniken (wie Quantisierung, Pruning) kombinieren und wird vom Team Hugging Face unterstützt und gepflegt.
  • Nachteile: Sie müssen die Verwendung der Optimum-Bibliothek erlernen, was einige technische Grundlagen erfordert.

Diese Umrechnungsmethoden haben ihre eigenen Vor- und Nachteile. Sie müssen sich nicht auf die in diesem Artikel verwendete Methode beschränken, sondern können je nach Ihren Bedürfnissen eine geeignete Methode wählen. In unserem Beispiel wird die Bibliothek „transformers.onnx“ verwendet, um das GPT-2-Modell zu konvertieren.

2. Konvertierung des GPT-2-Modells in das ONNX-Modell

Nachdem wir uns für die Verwendung von „transformers.onnx für die Modellkonvertierung entschieden haben, werden wir nun einen detaillierten Konvertierungsprozess beschreiben.

Ⅰ. Abhängigkeiten installieren

Stellen Sie zunächst sicher, dass die Bibliotheken „transformers“ und „onnx“ installiert sind. Wenn sie nicht installiert sind, können Sie den folgenden Befehl verwenden, um sie zu installieren:

pip install transformers onnx

Wenn Sie für bestimmte Hardware optimieren müssen, wie z.B. die Verwendung von GPU-Beschleunigung, müssen Sie auch „onnxruntime-gpu“ installieren:

pip install onnxruntime-gpu

Ⅱ. Konvertierung der Befehl

„transformers.onnx“ bietet ein einfaches Kommandozeilenwerkzeug. Ohne besondere Anforderungen ist die Verwendung dieses Tools für die Modellkonvertierung einfach: Führen Sie einfach den folgenden Befehl aus:

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

Die Parameter dieses Befehls:

  • „python -m transformers.onnx“: Rufen Sie das Werkzeug „Transformers.onnx“ auf.
  • „--model=pfad/zu/ihrem/tuned_model“: Geben Sie den Pfad des feinabgestimmten GPT-2-Modells an. In unserem Beispiel lautet dieser Pfad „gpt2_Adapter-tuning“.
  • „--feature=causal-lm-with-past“: Geben Sie die Art der Modellfunktionalität an. Da wir ein kausales Sprachmodell verwenden und „past_key_values unterstützen müssen, um die Generierungseffizienz zu verbessern, wählen wir „causal-lm-with-past“.
  • „path/to/save/onnx_model“: Geben Sie den Pfad zum Speichern des ONNX-Modells an. Wir können ihn zum Beispiel auf „gpt2_onnx“ setzen.

Vollständiges Befehlsbeispiel:

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

Führen Sie den obigen Befehl in der Befehlszeile aus. „transformers.onnx“ wird automatisch die erforderlichen Konfigurationsdateien herunterladen und das Modell in das ONNX-Format konvertieren. Nach Abschluss der Konvertierung sehen Sie eine Datei namens „model.onnx“ im angegebenen Ausgabeverzeichnis (in diesem Fall „gpt2_onnx“), zusammen mit einigen möglichen JSON-Dateien wie „config.json“.

Wenn Sie jedoch während der Modellkonvertierung einige Einstellungen anpassen müssen, um dem aktuellen Anwendungsfall besser gerecht zu werden, kann dieses Tool unsere Anforderungen eindeutig nicht erfüllen. Für komplexe Anwendungsszenarien ist es daher nach wie vor notwendig, geeignete Skripte für die Konvertierung zu schreiben, um eine genauere Kontrolle über die exportierte Modellform zu haben.

Ⅲ. Konvertierungs-Skript

Um das mit Adapter-Tuning feinabgestimmte GPT-2-Modell zu konvertieren, muss der Konvertierungsprozess das Adapter-Modul und die Einstellungen laden und auch die ONNX OP-Version einstellen, um Kompatibilitätsprobleme zu vermeiden. Anschließend werden wir die entsprechenden Funktionen Schritt für Schritt nach unseren Bedürfnissen implementieren.

Zunächst importieren wir die erforderlichen Python-Bibliotheken sowie die Klassen Adapter() und GPT2LMHeadModelWithAdapters(). Diese Klassen wurden im vorherigen Artikel (Integrieren Sie Ihr eigenes LLM in einen EA (Teil 5): Handelsstrategie mit LLMs entwickeln und testen (III) – Adapter-Tuning) ausführlich vorgestellt. Sie können direkt aus dem vorhandenen Skript importieren, und hier kopieren wir diese Klassen zum besseren Verständnis in das Konvertierungsskript:

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

Als Nächstes müssen wir das feinabgestimmte GPT-2-Modell laden und den Modellumwandlungsprozess steuern. Wir verwenden die Funktion „load_model_and_tokenizer()“, um das fein abgestimmte GPT-2-Modell zu laden, die Funktion „export_model_to_onnx()“, um das Modell in das ONNX-Format zu konvertieren, und die Funktion „main()“, um den gesamten Prozess und die Eingabe-/Ausgabepfade zu steuern. Schließlich definieren wir eine Funktion „check_onnx()“ zur Überprüfung der Exportergebnisse und eine Funktion „quantization()“ zur Quantisierung. Hier ist ein Beispiel:

# 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}")

Die Implementierung dieses Teils des Codes bereitet keine Schwierigkeiten, und es gibt ausführliche Kommentare im Code, sodass wir nicht im Detail darauf eingehen werden. Wir werden nur die wichtigsten Teile des Codes besprechen:

  • Es muss die Modellklasse mit dem Adaptermodul verwendet werden, um das feinabgestimmte Modell zu laden:

model = GPT2LMHeadModelWithAdapters.from_pretrained(model_id)

  • Dateipfade müssen in das Pfadformat konvertiert werden, das von „transformers.onnx.export()“ unterstützt wird, und können nicht direkt als Stringpfade verwendet werden. Wir verwenden die Klasse „Path“ aus der Bibliothek „pathlib“ zur Konvertierung:

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

  • Die Parameter „tokenizer“ und „preprocessor“ der Funktion „export()“ können nur einen setzen. Andernfalls wird ein Fehler gemeldet. Es wird empfohlen, „preprocessor“ zu verwenden:

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

  • Wir müssen die Opset-Version bestimmen, die mit der von MQL5 unterstützten Opset-Version übereinstimmen muss, um korrekt geladen zu werden. Wir wählen opset=14:

opset = 14

  • Der Eingabepfad des Modells (d.h. der Ordner, der das mit Adapter-Tuning feinabgestimmte GPT-2-Modell enthält) wird auf den Ordner „gpt2_Adapter-tuning“ unter dem aktuellen Projektpfad gesetzt, und der Ausgabepfad wird auf den Ordner „gpt2_onnx“ unter dem aktuellen Projektpfad gesetzt:

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

  • Die Funktionen „check_onnx()“ und „quantization()“ sind nicht obligatorisch und werden nur als Referenz angegeben.

Natürlich ist dies nur ein einfaches Beispiel für ein Konvertierungsskript. Wir haben keine weiteren Details festgelegt, wie z. B. die Unterstützung dynamischer Eingaben für Sequenzen. Wenn Sie die entsprechende Funktionsweise benötigen, fügen Sie bitte die entsprechenden Funktionen auf der Grundlage des Beispielskripts hinzu.

Das vollständige Konvertierungsskript ist ebenfalls im Anhang enthalten und trägt den Namen „torch2onnx.py“.


Formulierung der EA-Strategie und Server-Funktionalität

Wir haben die Arbeitsweise des EA bestimmt. Als Nächstes müssen wir einen Plan festlegen, um zu bestimmen, welche Dienste der Server bereitstellt und welche Funktionen der Client integriert: Der Client-EA ist hauptsächlich für die Datenerfassung und die Durchführung von Transaktionen zuständig; der Python-Server empfängt die vom Client gesendeten Daten, berechnet die Inferenzergebnisse und sendet die Ergebnisse zurück an den Client; der Client-EA und der Python-Server kommunizieren über Socket.

1. EA-Strategie

Als Nächstes werden wir eine Handelsstrategie entwickeln, die auf den Ergebnissen der GPT-2-Prognose basiert. Da der Schwerpunkt dieses Artikels auf der Demonstration der Integration des GPT-2-Modells in den MQL5 EA liegt, werden wir als Beispiel eine einfache Handelsstrategie erstellen. Es sollte betont werden, dass es sich hierbei um eine einfache Beispielstrategie handelt, die nur zu Demonstrationszwecken dient und keine tatsächliche Handelsberatung darstellt. In der Praxis müssen umfassendere und robustere Handelsstrategien entwickelt und gründliche Backtests und Risikobewertungen durchgeführt werden.

EA-Strategie-Logik:

  • Abrufen der Schlusskurse der letzten 20 Zeitpunkte für jede 1-Minute.
  • Übertragen der Daten an den Server und warten darauf, dass der Server die Berechnungsergebnisse zurücksendet.
  • Senden der Handelsaufträge auf der Grundlage der vom Server zurückgesendeten Handelssignale, ohne Stop Loss oder Take Profit zu setzen, und es soll immer nur eine offene Position geben.

2. Server Funktionsdesign

Auf der Serverseite müssen wir die Hauptfunktionen implementieren, die darin bestehen, Daten vom Client-EA zu empfangen, Modellinferenzen durchzuführen, um Ergebnisse zu erhalten, und die Handelssignale zu berechnen, die auf der Grundlage der Inferenzergebnisse an den Client zurückgesendet werden.

Server-seitige Funktionen:

  • Empfangen von Daten vom Client.
  • Laden des GPT-2-Modell und des Tokenizers, und jederzeit das Modell bereithalten.
  • Ausführen der Inferenz und Berechnung der Differenz zwischen dem aktuellen, tatsächlichen Preis und dem Mittelwert des vorhergesagten Preises auf der Grundlage der Inferenzergebnisse. Wenn die Differenz größer als 0 ist, wird ein Kaufsignal gesendet; wenn sie kleiner als 0 ist, wird ein Verkaufssignal gesendet; wenn sie gleich 0 ist, wird kein Signal gesendet.
  • Prüfen und entscheiden, ob CPU oder GPU für die Modellinferenz verwendet werden soll (je nach dem vom aktuellen Gerät unterstützten Modus).

Als Nächstes werden wir die entsprechende Funktionsweisen implementieren.


Erstellen des Inferenzdienstes

Wie man den Inferenzdienst erstellt, habe ich in einem früheren Artikel („Datenkennzeichnung für Zeitreihenanalyse (Teil 5):Anwendung und Test in einem EA, der Socket verwendet“ ausführlich beschrieben, wo das hier verwendete Skript „server.py“ zur Verfügung gestellt wird). Der Code-Teil folgt immer noch der Hauptlogik des Skripts „server.py“aus dem vorigen Artikel, nur dass wir es an unser fein abgestimmtes GPT-2-Modell anpassen und einige andere Optimierungen und Verbesserungen vornehmen.

Der geänderte Code enthält im Wesentlichen die folgenden Änderungen:

  • Anpassung der Modellinferenz an das GPT-2-Modell, mit wesentlichen Änderungen in der Funktion „eva()“.
  • Optimierung der Socket-Handshake-Logik durch Hinzufügen der Möglichkeit, den Client wieder zu verbinden, ohne den Server nach der Trennung neu zu starten, was für Backtests bequemer ist und keinen Neustart des Servers nach den Backtests erfordert.
  • Hinzufügen der Erkennung des Client-Verbindungsstatus, um unnötige Ressourcenverschwendung zu vermeiden.
  • Vermeiden Sie redundantes Ausdrucken von Ergebnissen, drucken Sie nur Ergebnisse, wenn sich die Vorhersageergebnisse ändern.
  • Hinzufügen einer Fehlerbehandlungslogik zur Vermeidung von Serverabstürzen.
  • Optimieren Sie die gesamte Codelogik.

Was den Code betrifft, so wird in diesem Artikel nicht weiter auf die Einzelheiten eingegangen, sondern nur auf die Teile, die geändert werden müssen.

1. Erforderliche Bibliotheken importieren

Zusätzlich zum Import der normalen benötigten Bibliotheken müssen wir auch die Klassen Adapter und GPT2LMHeadModelWithAdapters importieren, die wir in das Skript eingebaut haben. Sie können diese Klassen aus meinem früheren Artikel über die Feinabstimmung von GPT-2 beziehen oder sie direkt aus der in diesem Artikel bereitgestellten „torch2onnx.py“ importieren. Der Beispielcode importiert die beiden Klassen direkt aus „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. Hinzufügen einer Logik zum Laden von GPT-2-Modellen in der Funktion „load_model()“.

Im Originalskript „server.py“ wird die Funktion „load_model()“ zum Laden des Modells verwendet. Beachten Sie, dass wir hier die Ladelogik für das GPT-2-Modell sowie die Ladelogik für den GPT-2-Tokenizer hinzufügen müssen.

# 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. Hinzufügung der GPT-2-Modell-Inferenzlogik in der Funktion „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"

Beachten Sie, dass die Eingabelänge mit dem Datenformat übereinstimmen muss, das beim Training des GPT-2-Modells mit Adapter-Tuning verwendet wird:

  • input_data = msg[-encoder_length:]: Verwenden der letzten 20 vom Client gesendeten Datenpunkte als Modelleingabe.
  • prompt = ' '.join(map(str, input_data)): Konvertierung der Daten in das String-Format und Umwandlung in eine Eingabeaufforderung.
  • token = tokenizer.encode(prompt, return_tensors='pt').to(dvc): Verwenden des Tokenizer vom vorab trainierten GPT-2-Modell, um die Eingabeaufforderung zu kodieren und auf das aktuell unterstützte Gerät zu übertragen (das dem für die Modellinferenz verwendeten Gerät entspricht).
  • attention_mask = torch.ones_like(token).to(dvc): Definieren der Aufmerksamkeitsmaske für die Modellinferenz.
  • model.generate(token, attention_mask=attention_mask, pad_token_id=tokenizer.eos_token_id, do_sample=True, max_length=200)[0]: Durchführen der Modellinferenz.
  • generated = tokenizer.decode(model.generate(...), skip_special_tokens=True): Dekodieren der Vorhersageergebnisse und Überspringen spezieller Token.
  • generated_prices = generated.split('\n')[0]: Aufteilung der dekodierten Inferenzergebnisse.
  • try_float(s)  : Diese Funktion wird verwendet, um festzustellen, ob es Elemente in den Schlussfolgerungsergebnissen gibt, die nicht in das Float-Format umgewandelt werden können.
  • generated_prices = generated_prices.split(): Trennen der Vorhersageergebnisse durch Leerzeichen und entfernen der Trennzeichen, die nicht in Zahlen umgewandelt werden können.
  • generated_prices = list(map(try_float, generated_prices)): Alle Elemente in generated_prices in Zahlen im Float-Format umwandeln; wenn es Elemente gibt, die nicht in Zahlen umgewandelt werden können, die Funktion try_float(s) verwenden, um sie auf None zu setzen.
  • generated_prices = [f for f in generated_prices if f is not None]: Durchlaufen aller Elemente in generated_prices und Entfernen von Elementen, die keine sind.
  • generated_prices = generated_prices[0:prediction_length]: Nur die ersten 10 vorhergesagten Werte als Referenz erhalten.
  • if (last_price - prediction_mean) >= 0: Berechnen der Differenz zwischen den letzten vom Client gesendeten Daten und dem Mittelwert der vorhergesagten Werte. Wenn größer oder gleich 0, wird ein Verkaufssignal gesendet; wenn kleiner als 0, wird ein Kaufsignal gesendet.

Wir haben uns für die Verwendung der Bibliothek „transformers“ für die Inferenz entschieden. Sie können auch das bereits erwähnte Skript „torch2onnx.py“ verwenden, um das Modell in das ONNX-Format zu konvertieren und die Bibliothek „onnxruntime zur Inferenz zu verwenden. Auf diese Methode wird in diesem Artikel nicht eingegangen.

4. Server

Die gesamte Funktionsweise des Servers ist in die Klasse „server_()“ integriert, und die Änderungen am Gesamtcode sind nicht signifikant. Wir werden ihn hier nicht im Detail interpretieren und nur auf die geänderten Teile eingehen.

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

  • Hinzufügen der Klassenfunktion „is_connected(self)“, um festzustellen, ob der Client online ist.
  • Hinzufügen der Klassenfunktion „handshake(self, data)“, um die Handshake-Logik zu integrieren und zu vermeiden, dass die Haupt-Parsing-Logik überladen wird.
  • Hinzufügen des Klassenmitglieds „self.last_action“, um zu erkennen, ob sich das Handelssignal geändert hat. Drucken Sie die Ergebnisse nur, wenn sich das Handelssignal ändert, um häufiges Drucken zu vermeiden. Wenn der Client die Verbindung trennt, setzen Sie sie auf „None“ zurück, um zu vermeiden, dass falsche Signale gesendet werden, wenn der Client die Verbindung wieder aufnimmt.

Anmerkung: Unsere Host-Adresse ist auf „0.0.0.0“ eingestellt, denn wenn sie auf „127.0.0.1“ eingestellt ist, können entfernte Clients, die auf einem anderen Host laufen, keine Verbindung herstellen. Das bedeutet, dass durch die Einstellung auf „0.0.0.0“ auch dann eine Verbindung hergestellt werden kann, wenn sich Server und Client nicht auf demselben Host befinden (der Client-EA muss die richtige Host-IP-Adresse einstellen).

Der gesamte Code befindet sich in der beigefügten Datei „server.py“. Wenn der Server läuft, zeigt das Terminal die entsprechenden Laufinformationen an.

Server



Der Client-EA

Der Client folgt im Wesentlichen der Logik aus einem früheren Artikel (der in einem früheren Artikel ausdrücklich erwähnt wurde), mit entsprechenden Änderungen in der Logik. Es gibt weiterhin zwei Socket-Kompatibilitätsmethoden (eine mit Winapi zur Implementierung von WebSocket, die andere mit dem eingebauten Socket-Modul in MQL5), um Signalunterbrechungen zu vermeiden, die dadurch entstehen, dass das in MQL5 eingebaute Socket unter bestimmten Umständen keine Verbindung herstellen kann. Die Hauptbetriebslogik besteht darin, den Socket in der Funktion „OnInit()“ zu initialisieren, die Handelslogik in der Funktion „OnTick()“zu behandeln und das Senden von Daten an den Server und das Empfangen von Schlussfolgerungsergebnissen zu jeder festgelegten Zeit in der Funktion „OnTimer()“ zu behandeln.

1. Definition der Konstanten

#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“: Socket-Handle.
  • „host“ und „port“: Adresse und Port des Servers, mit dem eine Verbindung hergestellt werden soll.
  • „data_len“: Anzahl der zu sendenden Preisdatenpunkte.
  • „pre“: Zeichenkette zum Speichern der Vorhersageergebnisse.
  • „ses_h“, „cnt_h“, „re_h“, „ws_h“: Sitzungshandle, Verbindungshandle, Anfragehandle und WebSocket-Handle für WinHttp.

2. Initialisierung des 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);
  }

Im Initialisierungsteil implementieren wir hauptsächlich die Initialisierung von Winapi WebSocket und dem eingebauten Socket in MQL5. Dieser Teil hat sich im Vergleich zum vorherigen Artikel nicht wesentlich verändert und wird daher in diesem Artikel nicht weiter behandelt.

3. Handelsstrategie

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

Wir integrieren die gesamte Handelsstrategie in die Funktion „OnTick()“, um die Logik zu verdeutlichen. Wenn die Funktion OnTick() ausgeführt wird, ist zu prüfen, ob die globale Variable „pre“ leer ist. Wenn sie nicht leer ist, bedeutet dies, dass vom Client Vorhersageergebnisse gesendet wurden.

Dann senden wir auf der Grundlage der Vorhersageergebnisse Handelsaufträge („buy“ oder „sell“):

  • Bei der Auswahl von „buy“, eröffnen wir eine Position, wenn noch keine Position vorhanden ist, oder schließen einen bestehenden Verkaufsauftrag.
  • Bei einem „sell“, eröffnen wir eine Position, wenn keine Position vorhanden ist, oder schließen wir einen bestehenden Kaufauftrag.

Wir haben immer nur einen Auftrag offen, ohne Take Profit oder Stop Loss zu setzen, und kontrollieren die Position nur durch Handelssignale.

4. Interaktion mit dem Server

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

Die Hauptfunktion des Servers besteht darin, 100 Datenpunkte aus dem aktuellen Chart zu sammeln, sie an den Server zu senden, die Ergebnisse vom Server zu erhalten und dann die globale Variable auf der Grundlage der Ergebnisse zu ändern, um sicherzustellen, dass die Handelsstrategie entsprechend den vom Server gelieferten Ergebnissen ausgeführt wird. Hier verwenden wir zwei Socket-Verbindungsmethoden, um die Dateninteraktionslogik zu implementieren, und wählen automatisch die geeignete Methode auf der Grundlage des aktuell verbundenen Socket-Typs aus.

5. Freigabe von Ressourcen

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

In der Funktion „OnDeinit()“ werden die entsprechenden Systemressourcen freigegeben und die Wiederherstellung der Ressourcen durchgeführt.

Da der GPT-2-Modellinferenzprozess nicht im EA implementiert ist, macht dies unsere EA-Logik viel einfacher und prägnanter. Beachten Sie, dass wir dem EA keine Logik zur Risikokontrolle hinzugefügt haben und dass es sehr riskant ist, sich ausschließlich auf die GPT-2-Inferenzergebnisse zu verlassen, um zu entscheiden, ob eine Position gehalten oder eröffnet werden soll. Bitte beachten Sie nochmals, dass dieses EA-Beispiel nicht für den realen Handel verwendet werden sollte!

Der vollständige Code ist im Anhang des Artikels unter dem Namen „gpt2_EA.mql5“ zu finden.


Backtests

Um die Leistung des EAs zu bewerten, können wir ein Backtest im Strategietester des MetaTrader 5-Clients durchführen. Wir wählen den entsprechenden Bereich historischer Daten aus, legen die Backtest-Parameter fest und führen dann den Backtest durch (da unser gpt2-Modell auf dem Währungspaar NZDUSD trainiert wurde, können wir für den Backtest nur das Währungspaar NZDUSD auswählen).

Einstellungen

Der Backtest läuft:

Backtest

Nach Abschluss des Backtests sind die Ergebnisse wie folgt:

Ergebnis

Sie können die Rentabilität des EA, den maximalen Drawdown, die Gewinnrate und andere Metriken analysieren, indem Sie den Backtest-Bericht überprüfen. Bedenken Sie, dass unsere Handelsstrategie einfach ist und die Backtest-Ergebnisse daher nicht ideal sind. Dies liegt vor allem daran, dass unsere Strategie keiner Parameteroptimierung oder Risikokontrolle unterzogen wurde und der Trainingsprozess und die Datenaufbereitung für das Modell ein erhebliches Optimierungspotenzial aufweisen. Alles in allem erfordert dies eine Menge Geduld. Es ist wichtig zu beachten, dass aufgrund von Änderungen der Marktbedingungen und der Einschränkungen des Modells die Backtest-Ergebnisse keine Garantie für die Leistung des EA im zukünftigen Live-Handel sind und die Einschränkungen des Modells auch zu instabilen Vorhersageergebnissen führen können.



Schlussfolgerung

In diesem Artikel haben wir gezeigt, wie man ein mit spezifischen Finanzdaten (des Währungspaares NZDUSD) feinabgestimmtes GPT-2-Modell in ein EA-Programm integriert. Dabei haben wir den gesamten Prozess von der Feinabstimmung des Modells und der Implementierung der Inferenzlogik über die Einrichtung von Server und Client bis hin zur Integration von Handelsstrategien systematisch erläutert.

Es ist wichtig zu betonen, dass die von uns entwickelte Handelsstrategie relativ einfach ist und nur zu Demonstrationszwecken dient. In der Praxis müssen umfassendere und robustere Strategien entwickelt werden, z. B. die Kombination mehrerer technischer Indikatoren, die Berücksichtigung der Marktstimmung, die Festlegung von Stop Loss und Take Profit usw.

Darüber hinaus bergen der Trainingsprozess und die Datenaufbereitung für das Modell ein erhebliches Optimierungspotenzial, und Änderungen der Marktbedingungen und Modellbeschränkungen können zu instabilen Vorhersageergebnissen führen. Dennoch liegt die Bedeutung dieser Arbeit darin, dass sie das Potenzial großer Sprachmodelle für den quantitativen Handel demonstriert. Modelle wie GPT-2 können herkömmliche Marktdaten analysieren und mit Nachrichtendaten, Daten aus sozialen Medien und anderen textreichen Daten umgehen, was eine umfassendere Analyse der Marktstimmung ermöglicht und Händlern hilft, klügere Entscheidungen zu treffen. Diese verkehrsträgerübergreifende Fähigkeit ist etwas, was die traditionellen Finanzmodelle nicht haben, und ist ein Bereich, den wir weiter erforschen müssen.

Im nächsten Artikel werden wir anhand eines Beispiels zeigen, wie sich die Anwendung großer Sprachmodelle im quantitativen Handel optimieren lässt.


Anhang:

Dateien
Beschreibung
torch2onnx.pyDas Python-Skript zur Konvertierung des GPT-2-Modells in das ONNX-Format
server.pyDas Python-Skript zur Bereitstellung von GPT2-Modellinferenzdiensten und Ergebnissen
gpt2_EA.mq5EA-Programm zum Testen der Inferenzergebnisse von GPT2-Modellen

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/13506

Beigefügte Dateien |
server.py (7.5 KB)
torch2onnx.py (7.61 KB)
gpt2_EA.mq5 (11.49 KB)
Letzte Kommentare | Zur Diskussion im Händlerforum (1)
Alpha Dolcy
Alpha Dolcy | 29 Jan. 2025 in 13:46
Großartig.....wird es später in der Tiefe überprüfen. Ich freue mich auf den nächsten Artikel
Implementierung des kryptografischen SHA-256-Algorithmus von Grund auf in MQL5 Implementierung des kryptografischen SHA-256-Algorithmus von Grund auf in MQL5
Die Entwicklung DLL-freier Integrationen von Kryptowährungsbörsen war lange Zeit eine Herausforderung, aber diese Lösung bietet ein komplettes Framework für die direkte Marktanbindung.
Meistern der Log-Einträge (Teil 4): Speichern der Protokolle in Dateien Meistern der Log-Einträge (Teil 4): Speichern der Protokolle in Dateien
In diesem Artikel zeige ich Ihnen die grundlegenden Dateioperationen und wie Sie einen flexiblen Handler zur Anpassung konfigurieren. Wir werden die Klasse CLogifyHandlerFile aktualisieren, um Protokolle direkt in die Datei zu schreiben. Wir werden einen Leistungstest durchführen, indem wir eine Strategie für EURUSD eine Woche lang simulieren und bei jedem Tick Protokolle erstellen, mit einer Gesamtzeit von 5 Minuten und 11 Sekunden. Das Ergebnis wird in einem zukünftigen Artikel verglichen, in dem wir ein Caching-System zur Verbesserung der Leistung implementieren werden.
Neudefinition der Indikatoren von MQL5 und dem MetaTrader 5 Neudefinition der Indikatoren von MQL5 und dem MetaTrader 5
Ein innovativer Ansatz zur Erfassung von Indikatorinformationen in MQL5 ermöglicht eine flexiblere und rationalisierte Datenanalyse, indem Entwickler nutzerdefinierte Eingaben an Indikatoren für sofortige Berechnungen weitergeben können. Dieser Ansatz ist besonders nützlich für den algorithmischen Handel, da er eine bessere Kontrolle über die von den Indikatoren verarbeiteten Informationen ermöglicht und über die traditionellen Beschränkungen hinausgeht.
Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 9): External Flow Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 9): External Flow
In diesem Artikel wird eine neue Dimension der Analyse unter Verwendung externer Bibliotheken untersucht, die speziell für fortgeschrittene Analysen entwickelt wurden. Diese Bibliotheken, wie z. B. Pandas, bieten leistungsstarke Werkzeuge für die Verarbeitung und Interpretation komplexer Daten, die es Händlern ermöglichen, tiefere Einblicke in die Marktdynamik zu gewinnen. Durch die Integration solcher Technologien können wir die Lücke zwischen Rohdaten und umsetzbaren Strategien schließen. Begleiten Sie uns, wenn wir den Grundstein für diesen innovativen Ansatz legen und das Potenzial der Kombination von Technologie und Handelskompetenz erschließen.