Data label for time series mining (Part 5):Apply and Test in EA Using Socket
Einführung
In den vorangegangenen Artikeln habe ich erörtert, wie wir Daten nach unseren eigenen Bedürfnissen kennzeichnen und sie zum Trainieren von Zeitreihenvorhersagemodellen verwenden können, aber Sie haben vielleicht keine gute Vorstellung davon, wie Sie diese Modelle besser nutzen können. Nun ist es an der Zeit zu besprechen, wie wir unsere erstellten Modelle im historischen Backtesting von MetaTrader 5 validieren und in unseren EA einbauen können. Aber Sie müssen wissen, dass Sie in einem EA eine Strategie als Schlüssellogik brauchen, und eine echte und brauchbare Strategie erfordert eine spezifische theoretische Grundlage und eine Menge Überprüfung und Anpassung, um ihre Robustheit zu gewährleisten.
Die Strategie in diesem Artikel ist sehr einfach, sie ist aber nur ein einfaches Demonstrationsbeispiel, verwenden sie sie nicht im realen Handel! Natürlich, mit der Unterstützung einer großen Anzahl von verschiedenen Bibliotheken, können Sie auch diese Arbeit mit nur Python, aber MetaTrader 5 bietet eine solche bequeme und umfassende Backtesting-Tool, und kann genauer unsere Handelsumgebung simulieren, so haben wir uns für den MetaTrader 5 Client als unsere Backtesting-Plattform entschieden. Aber da unsere Modellerstellungsumgebung Python ist, muss das historische Backtesting von MetaTrader 5 mit MQL5 implementiert werden, was die Implementierung der Backtesting-Aufgabe etwas schwierig macht, aber wir sind nicht ohne eine Lösung. In diesem Artikel werden wir drei verschiedene Methoden zum Backtesting unserer Modelle in der MetaTrader 5-Umgebung diskutieren, um die Qualität der Modelle zu verbessern und zu steigern. Ich werde in den nächsten Artikeln verschiedene Methoden vorstellen, und dieser Artikel wird die WebSocket-Methode behandeln.
Inhaltsverzeichnis
- Einführung
- Prinzip der Umsetzung
- Implementierung von Python-Server-Funktionen
- Implementierung der MQL5-Client-Funktion
- Wie man Backtests durchführt
- Schlussfolgerung
Prinzip der Umsetzung
Zunächst fügen wir unserem Python-Skript eine Webserver-Instanz und unsere Modellinferenz, die Schlussfolgerungen des Modells, hinzu. Dann verwenden wir MQL5, um einen Web-Client zu erstellen, der den Inferenzdienst auf dem Server anfordert.
Wir gehen davon aus, dass die Logik des EAs wie folgt ist:
- Zunächst werden jedes Mal, wenn das OnTick() ausgelöst wird, die Chartdaten der letzten 300 Balken über den Client an den Server gesendet.
- Nach Erhalt der Informationen sendet der Server den vorhergesagten Trend der nächsten 6 Balken des Charts über die Modellinferenz an den EA-Client. Hier verwenden wir das im vorigen Artikel erwähnte Nbeats-Modell, weil es die Vorhersage in Trends zerlegen kann.
- Wenn es sich um einen Abwärtstrend handelt, dann verkaufen wir, wenn es sich um einen Aufwärtstrend handelt, dann kaufen wir.
Implementierung von Python-Server-Funktionen
Der von Python bereitgestellte Socket umfasst hauptsächlich die folgenden Funktionen:
- socket.bind(): Bindet die Adresse (Host, Port) an den Socket. In AF_INET wird die Adresse als Tupel (Host, Port) dargestellt.
- socket.listen(): Startet das TCP-Abhören. backlog gibt die maximale Anzahl der Verbindungen an, die das Betriebssystem unterbrechen kann, bevor es die Verbindung zurückweist. Der Wert ist mindestens 1, und die meisten Anwendungen setzen ihn auf 5.
- socket.accept(): Passive Annahme von TCP-Client-Verbindungen, (blockierend) Warten auf das Eintreffen einer Verbindung.
- socket.connect(): Aktive Initialisierung der TCP-Serververbindung. Im Allgemeinen ist das Format der Adresse ein Tupel (Hostname, Port). Wenn die Verbindung fehlschlägt, wird socket.error error zurückgegeben.
- socket.connect_ex(): Eine erweiterte Version der Funktion connect(), die bei einem Fehler einen Fehlercode zurückgibt, anstatt eine Ausnahme socket.recv() auszulösen: Empfängt TCP-Daten, die Daten werden als String zurückgegeben, bufsize gibt die maximale Datenmenge an, die empfangen werden soll und flag liefert zusätzliche Informationen über die Nachricht, die normalerweise ignoriert werden können.
- socket.send(): TCP-Daten senden, die Daten als String an den verbundenen Socket senden. Der Rückgabewert ist die Anzahl der zu sendenden Bytes, die kleiner sein kann als die Bytegröße von string.
- socket.sendall(): Senden aller TCP-Daten. Sendet die Daten als String an den verbundenen Socket, versucht aber, alle Daten zu senden, bevor er zurückkehrt. Gibt im Erfolgsfall keinen Wert zurück oder löst im Fehlerfall eine Ausnahme aus.
- socket.recvfrom(): Empfang von UDP-Daten, ähnlich wie recv(), aber der Rückgabewert ist (Daten, Adresse). Dabei ist data eine Zeichenkette, die die empfangenen Daten enthält, und address ist die Adresse des Sockets, der die Daten sendet.
- socket.sendto(): UDP-Daten senden, Daten an den Socket senden, Die Adresse ist ein Tupel der Form (ipaddr, port), das die entfernte Adresse angibt. Der Rückgabewert ist die Anzahl der gesendeten Bytes.
- socket.close(): Schließen des Sockets.
- socket.getpeername(): Gibt die entfernte Adresse des verbundenen Sockets zurück. Der Rückgabewert ist normalerweise ein Tupel (ipaddr, port).
- socket.getsockname(): Gibt die eigene Adresse des Sockets zurück. Normalerweise ein Tupel (ipaddr, port)
- socket.setsockopt(level,optname,value): Setzt den Wert der angegebenen Socket-Option.
- socket.getsockopt(level,optname[.buflen]): Gibt den Wert der Socket-Option zurück.
- socket.settimeout(timeout): Legt die Timeout-Zeit für Socket-Operationen fest. Timeout ist eine Fließkommazahl in Sekunden. Der Wert „None“ bedeutet, dass keine Zeitüberschreitung vorliegt. Im Allgemeinen sollte die Zeitlänge für den Timeout gesetzt werden, wenn der Socket gerade erstellt wird, da er für Verbindungsoperationen (wie connect()) verwendet werden kann.
- socket.gettimeout(): Gibt den aktuellen Wert der Timeout-Zeit in Sekunden zurück oder None, wenn keine Timeout-Zeit eingestellt ist.
- socket.fileno(): Gibt den Dateideskriptor des Sockets zurück.
- socket.setblocking(flag): Wenn das Flag 0 ist, wird der Socket in den nicht-blockierenden Modus gesetzt, andernfalls in den blockierenden Modus (Standardwert). Wenn im nicht-blockierenden Modus beim Aufruf von recv() keine Daten gefunden werden oder der send()-Aufruf nicht sofort Daten senden kann, wird eine socket.error-Ausnahme ausgelöst.
- socket.makefile(): Erstellt eine Datei, die mit dem Socket verbunden ist.
1. Importieren der erforderlichen Pakete
Für die Implementierung dieser Klasse müssen keine zusätzlichen Pakete installiert werden, und die Socket-Bibliothek ist in der Regel standardmäßig enthalten (unter der conda-Umgebung). Wenn Sie einige Warnmeldungen für zu unübersichtlich halten, können Sie das Modul „warnings“ (Warnungen) hinzufügen und die Anweisung warnings.filterwarnings(“ignore”) hinzufügen. Gleichzeitig müssen wir auch die benötigten globalen Variablen definieren:
- max_encoder_length=96
- max_prediction_length=20
- info_file=“ergebnisse.json“
Diese globalen Variablen werden auf der Grundlage des Modells definiert, das wir im vorherigen Artikel trainiert haben.
Der Code:
import socket import json from time import sleep import pandas as pd import numpy as np import warnings from pytorch_forecasting import NBeats warnings.filterwarnings("ignore") max_encoder_length=96 max_prediction_length=20 info_file="results.json"
2. Serverklasse erstellen
Wir erstellen die Klasse des Servers, in der wir einige Grundeinstellungen für den Socket initialisieren, darunter die folgenden Funktionen:
socket.socket(): Wir setzen die beiden Parameter auf socket.AF_INET und socket.SOCK_STREAM.
bind()-Methode von socket.socket(): Diese Funktion setzt den Host-Parameter auf „127.0.0.1“ und den Port-Parameter auf „8989“, wobei es nicht empfehlenswert ist, den Host zu ändern, und der Port auf andere Werte gesetzt werden kann, wenn 8989 belegt ist.
Das Modell wird später eingeführt, daher initialisieren wir es vorübergehend auf None.
Wir müssen den Server-Port abhören: self.sk.listen(1), passiv TCP-Client-Verbindungen annehmen und auf den Eingang der Verbindungen warten: self.sk_, self.ad_ = self.sock.accept(). Wir erledigen diese Aufgaben in der Klasseninitialisierung, um eine wiederholte Initialisierung zu vermeiden, wenn eine Schleife zum Empfang von Informationen ausgeführt wird.
class server_: def __init__(self, host = '127.0.0.1', port = 8989): self.sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.host = host self.port = port self.sk.bind((self.host, self.port)) self.re = '' self.model=None self.stop=None self.sk.listen(1) self.sk_, self.ad_ = self.sk.accept() print('server running:',self.sk_, self.ad_)
Anmerkung: Wenn Sie den Server in einem Docker oder einem Docker-ähnlichen Container bereitstellen, müssen Sie den Host möglicherweise auf „0.0.0.0“ setzen, da Ihr Client den Server sonst möglicherweise nicht finden kann.
3. Die Logik zur Verarbeitung der empfangenen Informationen
Wir definieren eine Klassenmethode msg(), um die empfangenen Informationen zu verarbeiten, indem wir eine while-Schleife verwenden, um die empfangenen Informationen zu verarbeiten. Dabei ist zu beachten, dass die empfangenen Daten mit decode(“utf-8“) dekodiert werden müssen. Anschließend werden die verarbeiteten Informationen an die Verarbeitungsfunktion der Inferenzlogik self.sk_.send(bytes(eva(self.re), “utf-8”)) gesendet, wobei die Funktion der Inferenzlogik als eva() definiert ist und der Parameter die empfangenen Informationen sind, die wir später implementieren werden. Als Nächstes müssen wir noch sicherstellen, dass unser Server ebenfalls stoppt, wenn der EA-Backtest endet, da er sonst Ressourcen im Hintergrund belegt. Wir können dies tun, indem wir nach Beendigung des EA einen „stop“-String an den Server senden, und wenn wir diesen String erhalten, lassen wir den Server die Schleife anhalten und den Prozess beenden. Wir haben dieses Klassenattribut bereits bei der Initialisierung der Serverklasse hinzugefügt und müssen es nur noch auf true setzen, wenn wir dieses Signal erhalten.
def msg(self): self.re = '' while True: data = self.sk_.recv(2374) if not data: break data=data.decode("utf-8") # print(len(data)) if data=="stop": self.stop=True break self.re+=data bt=eva(self.re, self.model) bt=bytes(bt, "utf-8") self.sk_.send(bt) return self.re
Anmerkung: Im Beispiel setzen wir den Parameter self.sk_.recv(2374) auf 2374, was genau der Länge von 300 Gleitkommazahlen entspricht. Wenn Sie feststellen, dass die Daten, die Sie erhalten, unvollständig sind, können Sie diesen Wert anpassen.
4. Ressourcen zurückgewinnen
Nachdem der Server gestoppt wurde, müssen wir die Ressourcen zurückfordern.
def __del__(self):
print("server closed!")
self.sk_.close()
self.ad_.close()
self.sock.close()
5. Definieren der Inferenzlogik
Die Inferenzlogik dieses Beispiels ist sehr einfach. Wir laden einfach das Modell und verwenden das vom Client vorgegebene Balkenchart, um die Ergebnisse vorherzusagen, und zerlegen sie dann in Trends und senden die Ergebnisse zurück an den Kunden. Was wir hier beachten müssen, ist, dass wir das Modell in der Initialisierung der Serverklasse initialisieren können, anstatt hier, sodass das Modell vorgeladen wird und jederzeit zur Ableitung bereit ist.
Zunächst definieren wir eine Funktion zum Laden des Modells und rufen diese Funktion dann bei der Initialisierung der Serverklasse auf, um das instanziierte Modell zu erhalten. Im vorangegangenen Artikel haben wir den Prozess des Speicherns und Ladens von Modellen vorgestellt. Das Modell speichert die Informationen nach dem Training in der Json-Datei „results.json“ im Stammverzeichnis des Ordners. Wir können das Modell lesen und laden. Natürlich muss sich unsere Datei server.py auch im Stammverzeichnis des Ordners befinden.def load_model(): with open(info_file) as f: m_p=json.load(fp=f)['last_best_model'] model = NBeats.load_from_checkpoint(m_p) return modelDann fügen wir in die Funktion init() der Klasse server_() ein: self.model=load_model() zur Initialisierung, und übergeben dann das initialisierte Modell an die Inferenzfunktion.
def __init__(self, host = '127.0.0.1', port = 8989): self.sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.host = host self.port = port self.sk.bind((self.host, self.port)) self.re = '' self.model=load_model() self.stop=None self.sk.listen(1) self.sk_, self.ad_ = self.sk.accept() print('server running:',self.sk_, self.ad_)
Als Nächstes wollen wir unsere Inferenzfunktion vervollständigen.
Eine Sache, die wir hier besonders beachten müssen, ist, dass das Datenformat, das das Modell eingeben muss, das DataFrame-Format sein muss, also müssen wir die empfangenen Daten zuerst in ein Numpy-Array konvertieren: msg=np.fromstring(msg, dtype=float, sep= ','), und sie dann in ein DataFrame konvertieren: dt=pd.DataFrame(msg). Nach Abschluss der Inferenz wird das Ergebnis zurückgegeben. Wir legen fest, dass es sich um einen Aufwärtstrend handelt, wenn der letzte Trendwert größer ist als der Durchschnitt der Trendwerte, andernfalls handelt es sich um einen Abwärtstrend. Wenn es sich um einen Aufwärtstrend handelt, wird „buy“ (Kaufen) zurückgegeben, wenn es sich um einen Abwärtstrend handelt, wird „sell“ (Verkaufen) zurückgegeben. Der spezifische Inferenzprozess wird in diesem Artikel nicht noch einmal besprochen, bitte lesen Sie den Inferenzprozess in den vorherigen Artikeln dieser Serie. Es gibt noch einen weiteren Punkt, der hier hervorgehoben werden muss. Da wir den Prädiktor des Modells als „close“-Spalte von DataFrame festlegen, müssen wir die „close“-Spalte zu den in DataFrame konvertierten Daten hinzufügen: dt['close']=dt.
def eva(msg,model): offset=1 msg=np.fromstring(msg, dtype=float, sep= ',') # print(msg) dt=pd.DataFrame(msg) dt=dt.iloc[-max_encoder_length-offset:-offset,:] last_=dt.iloc[-1] for i in range(1,max_prediction_length+1): dt.loc[dt.index[-1]+1]=last_ dt['close']=dt dt['series']=0 dt['time_idx']=dt.index-dt.index[0] print(dt) predictions = model.predict(dt, mode='raw',trainer_kwargs=dict(accelerator="cpu",logger=False),return_x=True) trend =predictions.output["trend"][0].detach().cpu() if (trend[-1]-trend.mean()) >= 0: return "buy" else: return "sell"
Als Nächstes müssen wir die Hauptschleife hinzufügen.
Zuerst initialisieren wir die Serviceklasse, dann fügen wir die Informationsverarbeitungsfunktion in der while-Schleife hinzu. Wir beenden die Schleife und verlassen das Programm, wenn wir das Stoppsignal erhalten. Da die Schleife nicht zu schnell laufen soll, fügen wir sleep(0.5) hinzu, um die Geschwindigkeit der Schleife zu begrenzen und eine hohe CPU-Auslastung zu vermeiden.
while True: rem=sv.msg() if sv.stop: break sleep(0.5)Bis jetzt haben wir einen einfachen Server fertiggestellt, als Nächstes müssen wir den Client in EA implementieren.
Implementierung der MQL5-Client-Funktion
1. Socket-Funktionen in MQL5
Das Socket-Modul umfasst derzeit die folgenden Funktionen:
- SocketCreate: Erzeugt einen Socket mit dem angegebenen Identifikator und gibt dessen Handle zurück.
- SocketClose: Schließt den Socket.
- SocketConnect: Verbindet mit dem Server mit Timeout-Kontrolle.
- SocketIsConnected: Prüft, ob der Socket gerade verbunden ist.
- SocketIsReadable: Ermittelt die Anzahl der Bytes, die vom Socket gelesen werden können.
- SocketIsWritable: Prüft, ob zum aktuellen Zeitpunkt Daten in den Socket geschrieben werden können.
- SocketTimeouts: Legt die Timeouts für den Datenempfang und das Senden für das System-Socket-Objekt fest.
- SocketRead: Liest Daten aus dem Socket.
- SocketSend: Schreibt Daten in den Socket.
- SocketTlsHandshake: Initiiert eine sichere TLS (SSL) Verbindung mit dem angegebenen Host unter Verwendung des TLS Handshake Protokolls.
- SocketTlsCertificate: Ruft die Zertifikatsdaten ab, die für eine sichere Netzwerkverbindung verwendet werden.
- SocketTlsRead: Liest Daten aus der sicheren TLS-Verbindung.
- SocketTlsReadAvailable: Liest alle verfügbaren Daten aus der sicheren TLS-Verbindung.
- SocketTlsSend: Sendet Daten über die sichere TLS-Verbindung.
Indem wir auf diese Methoden zurückgreifen, können wir leicht zusätzliche Funktionen auf der Client-Seite hinzufügen.
2. Implementierung der EA-Funktion
Lassen Sie uns zunächst die Funktionslogik von EA erörtern:
Wir initialisieren den Socket in „int OnInit()“.
Dann in der „void OnTick()“, implementieren wir den Empfang von Daten aus dem Client und das Senden der aktuellen Bar Chart-Daten an den Client, sowie unsere EA Backtesting-Logik.
In „void OnDeinit(const int reason)“ müssen wir eine „Stop“-Nachricht an den Server senden und den Socket schließen.3. Den EA initialisieren
Zunächst müssen wir eine globale Variable „int sk“ definieren, die nach der Erstellung des Sockets das Handle erhält.
In der Funktion OnInit() verwenden wir SocketCreate(), um den Client zu erstellen: int sk=SocketCreate().
Dann definieren wir unsere Serveradresse: string host=“127.0.0.1“;Server-Anschluss: int port= 8989;
Länge der zu übertragenden Daten, wir haben bereits besprochen, 300 Daten auf einmal zu senden: int data_len=300;
In der Funktion OnInit() müssen wir die Initialisierungssituation beurteilen. Wenn die Erstellung fehlschlägt, schlägt die Initialisierung fehl.
Dann erstellen wir eine Verbindung mit dem Server SocketConnect(sk,host, port,1000), wobei der Port mit der Serverseite übereinstimmen muss. Wenn die Verbindung fehlschlägt, schlägt die Initialisierung fehl.
int sk=-1; string host="127.0.0.1"; int port= 8989; int OnInit() { //--- sk=SocketCreate(); Print(sk); Print(GetLastError()); if (sk==INVALID_HANDLE) { Print("Failed to create socket"); return INIT_FAILED; } if (!SocketConnect(sk,host, port,1000)) { Print("Failed to connect to server"); return INIT_FAILED; } //--- return(INIT_SUCCEEDED); }
void OnDeinit(const int reason) { socket.Disconnect(); }
4. Handelslogik
Hier müssen wir die Hauptdatenverarbeitungslogik und die Handelslogik in der void OnTick() definieren.
Wir erstellen die Variablen „MqlTradeRequest request“ und „MqlTradeResult result“ für die Ausführung von Auftragsaufgaben.
Wir erstellen eine char-Array-Variable „char recv_data[]“, um Serverinformationen zu empfangen.
Wir erstellen eine Double-Array-Variable „double priceData[300]“ zum Kopieren von Chartdaten.
Wie erstellen die Variablen „string dataToSend“ und „char ds[]“, um das Double-Array in ein Char-Array zu konvertieren, das von Socket verwendet werden kann.
Zunächst müssen wir die Daten kopieren, die vom Chart gesendet werden sollen: int nc=CopyClose(Symbol(),0,0,data_len,priceData).
Dann konvertieren wir die Daten in das String-Format: for(int i=0.i<ArraySize(priceData).i++) dataToSend+=(string)priceData[i]+“,“, wir verwenden „,“ um die einzelnen Daten zu trennen.
Dann verwenden wir „int dsl=StringToCharArray(dataToSend,ds)“, um die String-Daten in ein Char-Array zu konvertieren, das von Socket verwendet werden kann.
Nach der Datenumwandlung müssen wir mit SocketIsWritable(sk) feststellen, ob unser Socket Daten senden kann, und wenn ja, dann mit SocketSend(sk,ds,dsl) Daten senden.
Wir müssen auch Informationen vom Server lesen. Dazu verwenden wir „uint len=SocketIsReadable(sk)“, um zu prüfen, ob Daten auf dem aktuellen Port verfügbar sind. Wenn die Informationen nicht leer sind, dann führen wir die Handelslogik aus: int rsp_len=SocketRead(sk,recv_data,len,500). „len“ ist die Puffergröße, „500“ ist die Timeout-Einstellung (in Millisekunden).
Wenn „Kauf“ empfangen wird, dann eröffnen wir einen Kaufauftrag, legen die Anforderung wie folgt fest:
- Wir setzen die Strukturanforderung für Handelsanfragen zurück: ZeroMemory(Anfrage).
- Einstellen, dass der Handelsbefehl sofort ausgeführt wird: request.action = TRADE_ACTION_DEAL.
- Wir legen das Währungspaar für den Handel fest: request.symbol = Symbol().
- Das Volumen des Auftrags: request.volume = 0.1.
- Der Typ des Auftrags: request.type = ORDER_TYPE_BUY.
- Die Funktion SymbolInfoDouble benötigt 2 Eingaben, die erste ist der String des Währungspaares, die zweite ist der Typ in der Aufzählung ENUM_SYMBOL_INFO_DOUBLE: request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK).
- Die zulässige Slippage des Handels: request.deviation = 5.
- Dann senden wir den Handelsauftrag ab: OrderSend(request, result).
Wenn „sell“ empfangen wird, dann öffnen wir einen Verkaufsauftrag und tragen folgende Werte in „request“ ein (die Einstellungen für einen Kaufauftrag werden hier nicht im Detail erklärt):
- ZeroMemory(Anfrage).
- 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;
- Dann senden wir den Handelsauftrag ab: OrderSend(request, result).
Um Probleme mit dem Testcode zu vermeiden, kommentieren wir hier die eigentliche Order-Sendefunktion aus und öffnen sie im Backtest.
Vollständiger Code:
void OnTick() { MqlTradeRequest request; MqlTradeResult result; char recv_data[]; double priceData[300]; string dataToSend; char ds[]; int nc=CopyClose(Symbol(),0,0,300,priceData); for(int i=0;i<ArraySize(priceData);i++) dataToSend+=(string)priceData[i]+","; int dsl=StringToCharArray(dataToSend,ds); if (SocketIsWritable(sk)) { Print("Send data:",dsl); int ssl=SocketSend(sk,ds,dsl); } uint len=SocketIsReadable(sk); if (len) { int rsp_len=SocketRead(sk,recv_data,len,500); if(rsp_len>0) { string result; result+=CharArrayToString(recv_data,0,rsp_len); Print("The predicted value is:",result); if (StringFind(result,"buy")) { ZeroMemory(request); request.action = TRADE_ACTION_DEAL; request.symbol = Symbol(); request.volume = 0.1; request.type = ORDER_TYPE_BUY; request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK); request.deviation = 5; //OrderSend(request, result); } else{ ZeroMemory(request); request.action = TRADE_ACTION_DEAL; request.symbol = Symbol(); request.volume = 0.1; request.type = ORDER_TYPE_SELL; request.price = SymbolInfoDouble(Symbol(), SYMBOL_BID); request.deviation = 5; //OrderSend(request, result); } } } }
Anmerkung: Der Parameter buffer_maxlen in der Funktion SocketSend() muss mit der Servereinstellung übereinstimmen. Dieser Wert wird automatisch berechnet und zurückgegeben, wenn die Funktion StringToCharArray() ausgeführt wird.
Jetzt führen wir zuerst server.py aus und fügen dann EA zum Chart im MetaTrader 5 Client hinzu, um die folgenden Ergebnisse zu sehen:
Wie man Backtests durchführt
Wir haben bereits die Einschränkungen von Socket in MQL5 erwähnt, und als Nächstes müssen wir Websocket-Unterstützung sowohl in der MQL5-Datei als auch in der Python-Datei hinzufügen.
1. Hinzufügen von Websocket-Unterstützung für den Client
Beim Backtesting können wir die winhttp.mqh in der Windows-Api verwenden, um die gewünschte Funktionalität zu erreichen. Eine ausführliche Einführung in diese Api finden Sie unter
Microsofts offizielle Dokumentation: https://learn.microsoft.com/de-de/windows/win32/winhttp/winhttp-functions. Hier werden nur die wichtigsten Funktionen aufgeführt:
- WinHttpOpen(): Initialisiert die Bibliothek und bereitet sie für die Verwendung durch die Anwendung vor.
- WinHttpConnect(): Legt den Domänennamen des Servers fest, mit dem die Anwendung kommunizieren möchte.
- WinHttpOpenRequest(): Erzeugt ein HTTP-Anfrage-Handle.
- WinHttpSetOption: Legt verschiedene Konfigurationsoptionen für die HTTP-Verbindung fest.
- WinHttpSendRequest: Sendet eine Anfrage an den Server.
- WinHttpReceiveResponse: Empfängt eine Antwort vom Server nach dem Senden einer Anfrage.
- WinHttpWebSocketCompleteUpgrade: Bestätigt, dass die vom Server empfangene Antwort dem WebSocket-Protokoll entspricht.
- WinHttpCloseHandle: Verwendet, um alle zuvor verwendeten Ressourcen-Deskriptoren zu verwerfen.
- WinHttpWebSocketSend: Dient zum Senden von Daten über eine WebSocket-Verbindung.
- WinHttpWebSocketReceive: Empfängt Daten über eine WebSocket-Verbindung.
- WinHttpWebSocketClose: Schließt die WebSocket-Verbindung.
- WinHttpWebSocketQueryCloseStatus: Prüft die vom Server gesendete Meldung über den Abschlussstatus.
Wir laden die Datei „winhttp.mqh“ herunter und kopieren sie in den Client-Datenordner „Include\WinAPI\“-Pfad. Vervollständigen wir nun den Code-Teil.
Wir fügen die Handle-Variablen, die wir verwenden müssen, in die globalen Variablen „HINTERNET ses_h,cnt_h,re_h,ws_h“ ein und initialisieren sie in OnInit():
- Zunächst vermeiden wir Zufallszahlen, indem wir sie auf NULL setzen: ses_h=cnt_h=re_h=ws_h=NULL.
- Dann starten wir die http-Sitzung:ses_h=WinHttpOpen(“MT5“,WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,NULL,NULL,0), wenn dies fehlschlägt, schlägt die Initialisierung fehl.
- Es folgt die Verbindung mit dem Server:cnt_h=WinHttpConnect(ses_h,host,port,0), wenn es fehlschlägt, schlägt die Initialisierung fehl.
- Dann die Anfrageinitialisierung:re_h=WinHttpOpenRequest(cnt_h, „GET“,NULL,NULL,NULL,NULL,0), wenn sie fehlschlägt, schlägt die Initialisierung fehl.
- Jetzt den Websocket einrichten:WinHttpSetOption(re_h,WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,nullpointer,0), wenn dies fehlschlägt, schlägt die Initialisierung fehl.
- Ausführen der Anforderung des Websocket-Handshakes:WinHttpSendRequest( re_h,NULL, 0,nullpointer, 0, 0, 0), wenn es dann fehlschlägt Initialisierung fehlschlägt.
- Empfang der Handshake-Antwort des Servers:WinHttpReceiveResponse(re_h,nullpointer), wenn dies fehlschlägt, schlägt die Initialisierung fehl.
- Upgrade auf Websocket, wir erhalten das Handle nach der Initialisierung:WinHttpWebSocketCompleteUpgrade(re_h,nv), wenn es fehlschlägt, dann schlägt die Initialisierung fehl.
- Nachdem die Aktualisierung abgeschlossen ist, benötigen wir das ursprüngliche Anforderungshandle nicht mehr, wir schließen es:WinHttpCloseHandle(re_h).
Auf diese Weise haben wir den gesamten Verbindungsprozess zwischen dem Client und dem Server abgeschlossen. Diese Prozesse müssen strikt in der richtigen Reihenfolge ausgeführt werden, und wir müssen die ursprünglichen Einstellungen der Initialisierungsfehleranweisung auskommentieren, da sie im Backtesting-Prozess immer wirksam sein werden und dazu führen, dass wir nicht erfolgreich initialisieren können.
int sk=-1; string host="127.0.0.1"; int port= 8989; int data_len=300; HINTERNET ses_h,cnt_h,re_h,ws_h; int OnInit() { //--- ses_h=cnt_h=re_h=ws_h=NULL; ses_h=WinHttpOpen("MT5", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, NULL, NULL, 0); Print(ses_h); if (ses_h==NULL){ Print("Http open failed!"); return INIT_FAILED; } cnt_h=WinHttpConnect(ses_h, host, port, 0); Print(cnt_h); if (cnt_h==-1){ Print("Http connect failed!"); return INIT_FAILED; } re_h=WinHttpOpenRequest(cnt_h, "GET", NULL, NULL, NULL, NULL, 0); if(re_h==NULL){ Print("Request open failed!"); return INIT_FAILED; } uchar nullpointer[]= {}; if(!WinHttpSetOption(re_h,WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,nullpointer,0)) { Print("Set web socket failed!"); return INIT_FAILED; } bool br; br = WinHttpSendRequest( re_h, NULL, 0, nullpointer, 0, 0, 0); if (!br) { Print("send request failed!"); return INIT_FAILED; } br=WinHttpReceiveResponse(re_h,nullpointer); if (!br) { Print("receive response failed!",string(kernel32::GetLastError())); return INIT_FAILED; } ulong nv=0; ws_h=WinHttpWebSocketCompleteUpgrade(re_h,nv); if (!ws_h) { Print("Web socket upgrade failed!",string(kernel32::GetLastError())); return INIT_FAILED; } WinHttpCloseHandle(re_h); re_h=NULL; sk=SocketCreate(); Print(sk); Print(GetLastError()); if (sk==INVALID_HANDLE) { Print("Failed to create socket"); //return INIT_FAILED; } if (!SocketConnect(sk,host, port,1000)) { Print("Failed to connect to server"); //return INIT_FAILED; } //--- return(INIT_SUCCEEDED); }
Dann fügen wir den entsprechenden Code der Logik in die Funktion OnTick() ein.
Zunächst müssen wir feststellen, in welcher Umgebung wir arbeiten. Da wir die globale Variable Socket-Handle definiert haben, können wir unterscheiden, ob wir unter normalen Bedingungen oder im Teststatus arbeiten, indem wir beurteilen, ob der Socket erfolgreich initialisiert wurde, sodass „sk!=-1“ bei true bedeutet, dass die Socket-Initialisierung erfolgreich ist. Wenn „sk!=-1“ nicht wahr ist, müssen wir die Websocket-Arbeitslogik vervollständigen:
- Senden wir zunächst Daten an den Server: WinHttpWebSocketSend(ws_h,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE,ds,dsl); Hier ist besonders zu beachten, dass der Rückgabewert der Funktion 0 ist, wenn dieser Vorgang erfolgreich ist, andernfalls wird der entsprechende Fehlercode zurückgegeben
- Bei Erfolg wird die empfangene Datenvariable geleert: ZeroMemory(recv_data)
- Daten empfangen: get=WinHttpWebSocketReceive(ws_h,recv_data,ArraySize(recv_data),rb,st); Wenn Daten erfolgreich empfangen wurden, ist der Rückgabewert 0, andernfalls wird ein Fehlercode zurückgegeben
- Wenn Daten empfangen werden, werden wir die Daten dekodieren: pre+=CharArrayToString(recv_data,0)
Wenn der Server uns ein „buy“ gesendet hat, eröffnen wir einen Kaufauftrag, andernfalls einen Verkaufsauftrag. Der Unterschied besteht darin, dass wir auch eine zusätzliche Beurteilungslogik hinzugefügt haben. Wenn es bereits einen Auftrag gibt, stellen wir zuerst fest, ob es eine offene Position gibt „numt=PositionsTotal()>0“ und, wenn es eine gibt, holen wir uns den Auftrags-Typ: tpt=OrderGetInteger(ORDER_TYPE), dann sehen wir, ob der Auftrags-Typ ORDER_TYPE_SELL oder ORDER_TYPE_BUY ist. Wenn der Auftrags-Typ derselbe ist wie der vom Server gesendete Trend, brauchen wir keine Operation. Wenn der Auftrags-Typ dem Trend entgegengesetzt ist, wird der aktuelle Auftrag geschlossen und ein Auftrag eröffnet, der dem Trend entspricht.
Wir verwenden die Server-Information „buy“ als Beispiel, um diesen Prozess vorzustellen.
Wenn tpt==ORDER_TYPE_BUY, kehren wir direkt zurück, wenn tpt==ORDER_TYPE_SELL, bedeutet dies, dass es einen Verkaufsauftrag gibt, dann setzen wir: request.order=tik und setzen: request.action=TRADE_ACTION_REMOVE. Wenn wir OrderSend(request, result) ausführen, wird es den Verkaufsauftrag schließen.
Wenn es keinen Auftrag gibt, dann folgt:
- request.action = TRADE_ACTION_DEAL;
- request.action = TRADE_ACTION_DEAL;
- request.symbol = Symbol();
- request.volume = 0.1;
- request.type = ORDER_TYPE_BUY;
- request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
- request.deviation = 5;
- request.type_filling=ORDER_FILLING_IOC;
Durch die Ausführung von OrderSend(request, result) wird eine Kauforder eröffnet. Ähnlich verhält es sich, wenn die Server-Informationen „verkaufen“ lauten, was in diesem Artikel nicht näher erläutert wird.
void OnTick() { //--- MqlTradeRequest request; MqlTradeResult result; char recv_data[5]; double priceData[300]; string dataToSend; char ds[]; int nc=CopyClose(Symbol(),0,0,data_len,priceData); for(int i=0;i<ArraySize(priceData);i++) dataToSend+=(string)priceData[i]+","; int dsl=StringToCharArray(dataToSend,ds); if (sk!=-1) { if (SocketIsWritable(sk)) { Print("Send data:",dsl); int ssl=SocketSend(sk,ds,dsl); } uint len=SocketIsReadable(sk); if (len) { int rsp_len=SocketRead(sk,recv_data,len,500); if(rsp_len>0) { string result=NULL; result+=CharArrayToString(recv_data,0,rsp_len); Print("The predicted value is:",result); if (StringFind(result,"buy")) { ZeroMemory(request); request.action = TRADE_ACTION_DEAL; request.symbol = "EURUSD"; request.volume = 0.1; request.type = ORDER_TYPE_BUY; request.price = SymbolInfoDouble("EURUSD", SYMBOL_ASK); request.deviation = 5; //OrderSend(request, result); } else{ ZeroMemory(request); request.action = TRADE_ACTION_DEAL; request.symbol = "EURUSD"; request.volume = 0.1; request.type = ORDER_TYPE_SELL; request.price = SymbolInfoDouble("EURUSD", SYMBOL_BID); request.deviation = 5; //OrderSend(request, result); } } } } else { ulong send=0; if (ws_h) { send=WinHttpWebSocketSend(ws_h, WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE, ds, dsl); //Print("Send data failed!",string(kernel32::GetLastError())); if(!send) { ZeroMemory(recv_data); ulong rb=0; WINHTTP_WEB_SOCKET_BUFFER_TYPE st=-1; ulong get=WinHttpWebSocketReceive(ws_h,recv_data,ArraySize(recv_data),rb,st); if (!get) { string pre=NULL; pre+=CharArrayToString(recv_data,0); Print("The predicted value is:",pre); ulong numt=0; ulong tik=0; bool sod=false; ulong tpt=-1; numt=PositionsTotal(); if (numt>0) { tik=OrderGetTicket(numt-1); sod=OrderSelect(tik); tpt=OrderGetInteger(ORDER_TYPE);//ORDER_TYPE_BUY or ORDER_TYPE_SELL } if (pre=="buy") { if (tpt==ORDER_TYPE_BUY) return; else if(tpt==ORDER_TYPE_SELL) { request.order=tik; request.action=TRADE_ACTION_REMOVE; Print("Close sell order."); } else{ ZeroMemory(request); request.action = TRADE_ACTION_DEAL; request.symbol = Symbol(); request.volume = 1; request.type = ORDER_TYPE_BUY; request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK); request.deviation = 5; request.type_filling=ORDER_FILLING_IOC; Print("Open buy order."); } OrderSend(request, result); } else{ if (tpt==ORDER_TYPE_SELL) return; else if(tpt==ORDER_TYPE_BUY) { request.order=tik; request.action=TRADE_ACTION_REMOVE; Print("Close buy order."); } else{ ZeroMemory(request); request.action = TRADE_ACTION_DEAL; request.symbol = Symbol(); request.volume = 1; request.type = ORDER_TYPE_SELL; request.price = SymbolInfoDouble(Symbol(), SYMBOL_BID); request.deviation = 5; request.type_filling=ORDER_FILLING_IOC; Print("OPen sell order."); } OrderSend(request, result); } } } } } }
An diesem Punkt haben wir die Konfiguration unseres MQL5-Websocket-Clients abgeschlossen.
2. Server-seitige Konfiguration
Wir müssen die Websocket-Unterstützung in server.py hinzufügen.
Zunächst müssen wir die erforderlichen Bibliotheken importieren.import base64
import hashlib
import struct
Die Hauptarbeit wird in der Funktion msg(self) der Serverklasse erledigt: Zunächst fügen wir das Websocker-Flagvariable wsk=False hinzu und beurteilen dann, ob die empfangenen Daten maskiert sind oder nicht.
Wenn sie maskiert sind, ist das High-Bit des zweiten Bytes der Daten 1, und wir müssen nur den Wert von (data[1] & 0x80) >> 7 beurteilen.
Wenn sie nicht maskiert sind, können wir einfach data.decode(“utf-8“) verwenden.
Falls maskiert, müssen wir den Maskierungsschlüssel finden: mask = data[4:8] und die Nutzdaten: payload = data[8:], und dann demaskieren: for i in range(len(payload)):message += chr(payload[i] ^ mask[i % 4]), und die Flag-Variable wsk auf true setzen.
Nachdem wir das Maskierungsproblem gelöst haben, müssen wir auch den Websocket-Handshake-Prozess hinzufügen:
Zunächst beurteilen wir, ob es sich um einen Handshake-Prozess handelt: Wenn '\r\n\r\n' in den Daten;
Wenn es sich um einen Handshake-Prozess handelt, wir erhalten den Schlüsselwert: data.split(“\r\n“)[4].split(“: „)[1];
Wir berechnen den Sec-WebSocket-Accept-Wert: base64.b64encode(hashlib.sha1((key+GUID).encode('utf-8')).digest()), wobei die GUID ein fester Wert „258EAFA5-E914-47DA-95CA-C5AB0DC85B11“ ist.
Dann definieren wir den Header der Handshake-Antwort:
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"
Wir füllen den Header der Antwort aus: response_str = response_tpl % (ac.decode('utf-8'), „127.0.0.1:8989“).
Schließlich wird die Handshake-Antwort gesendet: self.sk_.send(bytes(response_str, encoding='utf-8')).
Es gibt noch einen weiteren Punkt, der hinzuzufügen ist, nämlich die Verarbeitung der Informationen, die als websocketfähige Informationen gesendet werden sollen:
if wsk: tk=b'\x81' lgt=len(bt) tk+=struct.pack('B',lgt) bt=tk+bt
Nun ist der Teil, der auf der Serverseite geändert werden muss, im Grunde abgeschlossen.
def msg(self): self.re = '' wsk=False while True: data = self.sk_.recv(2500) if not data: break if (data[1] & 0x80) >> 7: fin = (data[0] & 0x80) >> 7 # FIN bit opcode = data[0] & 0x0f # opcode masked = (data[1] & 0x80) >> 7 # mask bit mask = data[4:8] # masking key payload = data[8:] # payload data print('fin is:{},opcode is:{},mask:{}'.format(fin,opcode,masked)) message = "" for i in range(len(payload)): message += chr(payload[i] ^ mask[i % 4]) data=message wsk=True else: data=data.decode("utf-8") if '\r\n\r\n' in data: key = data.split("\r\n")[4].split(": ")[1] print(key) GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ac = base64.b64encode(hashlib.sha1((key+GUID).encode('utf-8')).digest()) response_tpl="HTTP/1.1 101 Switching Protocols\r\n" \ "Upgrade:websocket\r\n" \ "Connection: Upgrade\r\n" \ "Sec-WebSocket-Accept: %s\r\n" \ "WebSocket-Location: ws://%s/\r\n\r\n" response_str = response_tpl % (ac.decode('utf-8'), "127.0.0.1:8989") self.sk_.send(bytes(response_str, encoding='utf-8')) data=data.split('\r\n\r\n',1)[1] if "stop" in data: self.stop=True break if len(data)<200: break self.re+=data bt=eva(self.re, self.model) bt=bytes(bt, "utf-8") if wsk: tk=b'\x81' lgt=len(bt) tk+=struct.pack('B',lgt) bt=tk+bt self.sk_.sendall(bt) return self.re
3. Verwendung
Zunächst müssen wir die Serverseite starten, das Verzeichnis der Datei server.py in der Befehlszeile suchen und python server.py ausführen, um den Dienst zu starten.
Dann gehen wir zurück zum MetaTrader 5-Client, öffnen den Quellcode und drücken direkt Strg+F5 oder klicken auf den Test-Button, um den Test zu starten:
Zu diesem Zeitpunkt werden in der Toolbox-Spalte der Informationstesttabelle die entsprechenden Informationen angezeigt:
Die Ergebnisse des Backtests sind wie folgt:
Wir sehen, dass unser gesamtes System einwandfrei funktioniert und die entsprechenden Bestellvorgänge entsprechend der Vorhersage des Modells durchführen kann.
Anmerkung:
- Wenn Sie direkt auf dem Chart testen wollen, beachten Sie bitte: Wenn die Socket-Initialisierung erfolgreich ist, führt die Ausführungslogik den Websocket-Logikteil natürlich nicht aus. Um unnötige Probleme zu vermeiden, wird empfohlen, in diesem Fall den Websocket-Initialisierungsteil in OnInit() auszukommentieren.
- Zusätzlich zur Verwendung von OnTick() zur Vervollständigung unserer Hauptlogik können wir auch die Implementierung von Logik in OnTimer() in Betracht ziehen, sodass Sie den spezifischen Zeitpunkt für das Senden von Daten festlegen können, z. B. das Senden von Daten alle 15 Minuten. Auf diese Weise kann vermieden werden, dass die Daten häufig gesendet werden, immer dann, wenn neue Preise eintreffen. Dieser Artikel enthält keinen spezifischen Implementierungscode. Die Leser können sich auf die Implementierungsmethode in diesem Artikel beziehen, um ihren eigenen Implementierungscode zu schreiben.
Schlussfolgerung
In diesem Artikel erfahren Sie, wie Sie die Server-Client-Methode zum Backtesting des zuvor trainierten Modells verwenden und wie Sie unser System sowohl in Backtesting- als auch in Nicht-Backtesting-Szenarien testen können. Es ist unbestreitbar, dass dieser Artikel eine Menge sprach- und bereichsübergreifendes Wissen beinhaltet, und der am schwierigsten zu verstehende Teil dürfte der Websocket-Teil sein, der ein komplexes technisches Projekt darstellt. Aber solange Sie die Schritte in diesem Artikel befolgen, werden Sie Erfolg haben. Es muss betont werden, dass dieser Artikel nur ein Beispiel liefert, das es Ihnen ermöglicht, unser Modell mit einer relativ einfachen Strategie zu testen. Bitte verwenden Sie es nicht für den tatsächlichen Handel! Im realen Handel müssen Sie möglicherweise jeden Teil dieses Systems optimieren, damit es stabil läuft. Noch einmal: Verwenden Sie dieses Beispiel nicht direkt für Ihren realen Handel! Dieser Artikel endet hier, und im nächsten Artikel werden wir besprechen, wie wir die Socket-Abhängigkeit loswerden und unser Modell direkt im EA verwenden können.
Ich hoffe, Sie haben etwas gelernt und wünsche Ihnen ein glückliches Leben!
Referenzen:
WebSockets für MetaTrader 5 - Verwendung der Windows API
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/13254
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.