Selbstoptimierende Expert Advisors mit MQL5 und Python erstellen (Teil II): Abstimmung tiefer neuronaler Netze
Einführung
Die Mitglieder unserer Community sind sehr daran interessiert, KI in ihre Handelsstrategien zu integrieren, was die Abstimmung von KI-Modellen auf bestimmte Märkte erfordert. Jedes KI-Modell verfügt über anpassbare Parameter, die seine Leistung erheblich beeinflussen; optimale Einstellungen für einen Markt funktionieren möglicherweise nicht für einen anderen. In diesem Artikel wird gezeigt, wie KI-Modelle mithilfe von Optimierungsalgorithmen, insbesondere dem Nelder-Mead-Algorithmus, so angepasst werden können, dass sie die Standardeinstellungen übertreffen. Wir werden diesen Algorithmus zur Feinabstimmung eines tiefen neuronalen Netzwerks mit Daten aus dem MetaTrader5-Terminal anwenden und dann das optimierte Modell im ONNX-Format zur Verwendung in einem Expert Advisor exportieren. Für diejenigen, die mit diesen Begriffen nicht vertraut sind, werden wir im Laufe des Artikels ausführliche Erläuterungen geben.Nelder-Mead-Optimierungsalgorithmus
Der Nelder-Mead-Algorithmus ist eine beliebte Wahl für verrauschte, nicht-differenzierbare und nicht-lineare multimodale Optimierungsprobleme. Der nach seinen Erfindern John Nelder und Roger Mead benannte Algorithmus wurde 1965 in ihrem Papier „A Simplex Method for Function Minimization“ vorgestellt. Es kann sowohl für univariate als auch für multivariate Optimierungsprobleme verwendet werden.
Der Nelder-Mead-Algorithmus stützt sich nicht auf abgeleitete Informationen, sondern ist ein Optimierungsalgorithmus für die Mustersuche. Es erfordert, dass der Nutzer einen Ausgangspunkt angibt. Je nach gewähltem Startpunkt kann der Algorithmus in einem trügerischen lokalen Optimum stecken bleiben. Daher kann es von Vorteil sein, die Optimierung mehrmals mit unterschiedlichen Ausgangspunkten durchzuführen, um die Chancen auf ein globales Optimum zu erhöhen.
Der Algorithmus arbeitet mit einer geometrischen Form, die Simplex genannt wird. Das Simplex hat einen Scheitelpunkt für jede Eingangsvariable und einen zusätzlichen Scheitelpunkt. Die Punkte (Scheitelpunkte) des Simplex werden ausgewertet, und anhand einfacher Regeln werden die Punkte auf der Grundlage ihrer Auswertungen verschoben. Der Algorithmus hat bestimmte Abbruchbedingungen, wie z. B. das Erreichen der maximalen Anzahl von Iterationen oder das Erreichen einer minimalen Änderung der Bewertungswerte. Wenn keine Verbesserungen erzielt werden oder die zulässige Anzahl von Iterationen überschritten wird, wird das Optimierungsverfahren abgebrochen.
Abb. 1: Roger Mead
Abb. 2: John Nelder
Fangen wir an
Wir beginnen damit, die benötigten Daten von unserem MetaTrader 5-Terminal zu holen. Zunächst öffnen wir unser MetaTrader 5-Terminal und klicken im Kontextmenü auf das Symbol-Symbol. Von dort aus wählen wir Balken aus und suchen nach dem gewünschten Symbol. Sobald wir die Daten angefordert haben, klicken wir einfach auf „Exportieren“ und die Daten stehen uns im CSV-Format zur Verfügung.
Abb. 3: Suche nach den benötigten Daten
Da unsere Daten fertig sind, können wir damit beginnen, die benötigten Bibliotheken zu importieren.
#import libraries we need import pandas as pd import numpy as np from numpy.random import randn,rand import seaborn as sns
Dann lesen wir die von uns vorbereiteten Daten ein.
#Read in our market data brent = pd.read_csv("/home/volatily/market_data/Market Data UK Brent Oil.csv", sep="\t")
Wir müssen unsere Daten kennzeichnen.
#Preparing to label the data look_ahead = 20 #Defining the target brent["Target"] = brent["Close"].shift(-look_ahead) #Drop missing values brent.dropna(inplace=True)
Importieren wir nun die Bibliotheken, die wir für die Optimierung benötigen.
#In this article we will cover some techniques for hyper-parameter tuning from scipy.optimize import minimize from sklearn.neural_network import MLPRegressor from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import root_mean_squared_error import time
Wir werden nun unser Zeitreihen-Kreuzvalidierungsobjekt erstellen.
#Define the time series split parameters splits = 5 gap = look_ahead #Create the time series split object tscv = TimeSeriesSplit(n_splits=splits,gap=gap) #Create a dataframe to store our accuracy current_error_rate = pd.DataFrame(index = np.arange(0,splits),columns=["Current Error"])
Definieren wir die Prädiktoren und Ziele für unser Modell.
#Define the predictors and the target predictors = ["Open","High","Low","Close"] target = "Target"
Wir definieren nun die Funktion, die wir minimieren wollen: den Kreuzvalidierungsfehler des Modells. Bitte beachten Sie, dass dies nur zu Demonstrationszwecken dient. Idealerweise würden wir den Datensatz in zwei Hälften aufteilen, die Optimierung auf einer Hälfte durchführen und die Genauigkeit auf der anderen Hälfte messen. Für diese Demonstration optimieren wir jedoch das Modell und messen seine Genauigkeit anhand desselben Datensatzes.
#Define the objective function def objective(x): #The parameter x represents a new value for our neural network's settings #In order to find optimal settings, we will perform 10 fold cross validation using the new setting #And return the average RMSE from all 10 tests #We will first turn the model's Alpha parameter, which controls the amount of L2 regularization model = MLPRegressor(hidden_layer_sizes=(5,2),alpha=x[0],early_stopping=True,shuffle=False,learning_rate_init=x[1],tol=x[2]) #Now we will cross validate the model for i,(train,test) in enumerate(tscv.split(brent)): #The data X_train = brent.loc[train[0]:train[-1],predictors] y_train = brent.loc[train[0]:train[-1],target] X_test = brent.loc[test[0]:test[-1],predictors] y_test = brent.loc[test[0]:test[-1],target] #Train the model model.fit(X_train,y_train) #Measure the RMSE current_error_rate.iloc[i,0] = root_mean_squared_error(y_test,model.predict(X_test)) #Return the Mean CV RMSE return(current_error_rate.iloc[:,0].mean())
Es sei daran erinnert, dass der Nelder-Mead-Algorithmus einen Ausgangspunkt erfordert. Um einen guten Ausgangspunkt zu finden, werden wir eine Zeilensuche über die fraglichen Parameter durchführen. Wir werden eine for-Schleife verwenden, um unsere Genauigkeit zu messen, wobei die Parameter auf 0,1, dann auf 0,01 und so weiter eingestellt werden. Dies wird uns helfen, einen potenziell guten Ausgangspunkt für den Algorithmus zu finden.
#Let us measure how much time this takes. start = time.time() #Create a dataframe to measure the error rates starting_point_error = pd.DataFrame(index=np.arange(0,21),columns=["Average CV RMSE"]) starting_point_error["Iteration"] = np.arange(0,21) #Let us first find a good starting point for our optimization algorithm for i in np.arange(0,21): #Set a new starting point new_starting_point = (10.0 ** -i) #Store error rates starting_point_error.iloc[i,0] = objective([new_starting_point,new_starting_point,new_starting_point]) #Record the time stamp at the end stop = time.time() #Report the amount of time taken print(f"Completed in {stop - start} seconds")
Betrachten wir nun unsere Fehlerquoten.
Durchschnittlicher CV RMSE | Iteration |
---|---|
0.91546 | 0 |
0.267167 | 1 |
14.846035 | 2 |
15.763264 | 3 |
56.820397 | 4 |
75.202923 | 5 |
72.562681 | 6 |
64.33746 | 7 |
88.980977 | 8 |
83.791834 | 9 |
82.871215 | 10 |
88.031151 | 11 |
65.532539 | 12 |
78.177191 | 13 |
85.063947 | 14 |
88.631589 | 15 |
74.369735 | 16 |
86.133656 | 17 |
90.482654 | 18 |
102.803612 | 19 |
74.636781 | 20 |
Wie wir sehen können, scheint es, dass wir zwischen Iteration 0 und 2 eine optimale Region überquert haben. Von da an wurde unser Fehler immer größer. Wir können die gleichen Informationen visuell beobachten.
sns.lineplot(data=starting_point_error,x="Iteration",y="Average CV RMSE")
Abb. 4: Visualisierung der Ergebnisse unserer Zeilensuche
Da wir nun eine Vorstellung davon haben, was ein guter Ausgangspunkt sein könnte, wollen wir eine Funktion definieren, die uns zufällige Punkte innerhalb des Bereichs liefert, in dem wir das Optimum vermuten.
pt = abs(((10 ** -1) + rand(3) * ((10 ** 0) - (10 ** -1)))) pt
Beachten Sie, dass wir ein Array mit 3 zufälligen Werten abrufen, weil wir 3 verschiedene Parameter für unser neuronales Netz optimieren. Führen wir nun die Abstimmung der Hyperparameter durch.
start = time.time() result = minimize(objective,pt,method="nelder-mead") stop = time.time() print(f"Task completed in {stop - start} seconds")
Lassen Sie uns das Ergebnis der Optimierung interpretieren
result
success: False
status: 1
fun: 0.12022686955703668
x: [ 7.575e-01 3.577e-01 2.621e-01]
nit: 225
nfev: 600
final_simplex: (array([[ 7.575e-01, 3.577e-01, 2.621e-01],
[ 7.575e-01, 3.577e-01, 2.621e-01],
[ 7.575e-01, 3.577e-01, 2.621e-01],
[ 7.575e-01, 3.577e-01, 2.621e-01]]), array([ 1.202e-01, 2.393e-01, 2.625e-01, 8.978e-01])
Achten Sie zunächst auf die nutzerfreundliche Meldung, die am oberen Rand angezeigt wird. Die Meldung zeigt an, dass der Algorithmus die maximale Anzahl von Funktionsbewertungen überschritten hat. Erinnern Sie sich an die Bedingungen, die wir zuvor in Bezug auf die Szenarien festgelegt haben, die zum Anhalten der Optimierung führen würden. Wir können zwar versuchen, die Anzahl der zulässigen Iterationen zu erhöhen, aber das ist keine Garantie für eine bessere Leistung.
Wir können den Schlüssel „fun“ sehen, der die optimale Ausgabe angibt, die der Algorithmus mit der Funktion erreicht hat. Danach folgt der Schlüssel „x“, der die Werte von x anzeigt, die zum optimalen Ergebnis geführt haben.
Wir können auch die Taste „nit“ beobachten, die uns die Anzahl der von der Funktion durchgeführten Iterationen angibt. Der Schlüssel „nfev“ schließlich gibt an, wie oft der Algorithmus die Zielfunktion aufgerufen hat, um ihre Ausgabe zu bewerten. Erinnern Sie sich daran, dass unsere Zielfunktion eine 5-fache Kreuzvalidierung durchführt und die durchschnittliche Fehlerrate liefert. Das bedeutet, dass wir bei jedem Aufruf der Funktion unser neuronales Netz 5 Mal anpassen. 600 Funktionsbewertungen bedeuten also, dass wir unser neuronales Netz 3000 Mal anpassen!
Lassen Sie uns nun das Standardmodell und das von uns erstellte angepasste Modell vergleichen.
#Let us compare our customised model and the defualt model custom_model = MLPRegressor(hidden_layer_sizes=(5,2),alpha=result.x[0],early_stopping=True,shuffle=False,learning_rate_init=result.x[1],tol=result.x[2]) #Default model default_model = MLPRegressor(hidden_layer_sizes=(5,2))
Wir bereiten das geteilte Zeitreihenobjekt vor.
#Define the time series split parameters splits = 10 gap = look_ahead #Create the time series split object tscv = TimeSeriesSplit(n_splits=splits,gap=gap) #Create a dataframe to store our accuracy model_error_rate = pd.DataFrame(index = np.arange(0,splits),columns=["Default Model","Custom Model"])
Wir werden nun jedes Modell einer Kreuzvalidierung unterziehen.
#Now we will cross validate the model for i,(train,test) in enumerate(tscv.split(brent)): #The data X_train = brent.loc[train[0]:train[-1],predictors] y_train = brent.loc[train[0]:train[-1],target] X_test = brent.loc[test[0]:test[-1],predictors] y_test = brent.loc[test[0]:test[-1],target] #Our model model = MLPRegressor(hidden_layer_sizes=(5,2),alpha=result.x[0],early_stopping=True,shuffle=False,learning_rate_init=result.x[1],tol=result.x[2]) #Train the model model.fit(X_train,y_train) #Measure the RMSE model_error_rate.iloc[i,1] = root_mean_squared_error(y_test,model.predict(X_test))
Betrachten wir unsere Fehlermetriken.
model_error_rate
Standardmodell | Kundenspezifisches Modell |
---|---|
0.153904 | 0.550214 |
0.113818 | 0.501043 |
82.188345 | 0.52897 |
0.114108 | 0.117466 |
0.114718 | 0.112892 |
77.508403 | 0.258558 |
0.109191 | 0.304262 |
0.142143 | 0.363774 |
0.163161 | 0.153202 |
0.120068 | 2.20102 |
Lassen Sie uns die Ergebnisse auch visualisieren.
model_error_rate["Default Model"].plot(legend=True) model_error_rate["Custom Model"].plot(legend=True)
Abb. 5: Visualisierung der Leistung unseres angepassten Modells
Wie wir feststellen können, hat das angepasste Modell das Standardmodell übertroffen. Unser Test wäre jedoch überzeugender gewesen, wenn wir separate Datensätze für das Training der Modelle und die Bewertung ihrer Genauigkeit verwendet hätten. Die Verwendung desselben Datensatzes für beide Zwecke ist nicht das ideale Verfahren.
Als Nächstes bereiten wir die Konvertierung unseres tiefen neuronalen Netzes in seine ONNX-Darstellung vor. ONNX, die Abkürzung für Open Neural Network Exchange, ist ein standardisiertes Format, mit dem KI-Modelle, die in einem beliebigen konformen Framework trainiert wurden, in verschiedenen Programmen verwendet werden können. ONNX ermöglicht es uns zum Beispiel, ein KI-Modell in Python zu trainieren und es dann in MQL5 oder sogar in einem Java-Programm zu verwenden (vorausgesetzt, die Java-API unterstützt ONNX).
Zunächst importieren wir die benötigten Bibliotheken.
#Now we will prepare to export our neural network into ONNX format from skl2onnx.common.data_types import FloatTensorType from skl2onnx import convert_sklearn import onnxruntime as ort import netron
Definieren wir die Eingabeform für unser Modell, denn unser Modell benötigt 4 Eingaben.
#Define the input types initial_type = [("float_input",FloatTensorType([1,4]))]
Anpassung an unser maßgeschneidertes Modell.
#Fit our custom model custom_model.fit(brent.loc[:,["Open","High","Low","Close"]],brent.loc[:,"Target"])
Die Erstellung der ONNX-Darstellung unseres tiefen neuronalen Netzes ist dank der skl2onnx-Bibliothek einfach.
#Create the onnx represantation onnx = convert_sklearn(custom_model,initial_types=initial_type,target_opset=12)
Definieren wir den Namen unserer ONNX-Datei.
#The name of our ONNX file onnx_filename = "Brent_M1.onnx"
Jetzt werden wir die ONNX-Datei schreiben.
#Write out the ONNX file with open(onnx_filename,"wb") as f: f.write(onnx.SerializeToString())
Schauen wir uns nun die Parameter unseres ONNX-Modells an.
#Now let us inspect our ONNX model onnx_session = ort.InferenceSession(onnx_filename) input_name = onnx_session.get_inputs()[0].name output_name = onnx_session.get_outputs()[0].name
Schauen wir uns die Eingabeform an.
for i, input_tensor in enumerate(onnx_session.get_inputs()): print(f"{i + 1}. Name: {input_tensor.name}, Data Type: {input_tensor.type}, Shape: {input_tensor.shape}")
Beobachten Sie die Form der Ausgabe unseres Modells.
for i, output_tensor in enumerate(onnx_session.get_outputs()): print(f"{i + 1}. Name: {output_tensor.name}, Data Type: {output_tensor.type}, Shape: {output_tensor.shape}")
Jetzt können wir unser Modell mit netron visuell darstellen. Diese Schritte helfen uns sicherzustellen, dass unsere ONNX-Eingabe- und Ausgabeformen unseren Erwartungen entsprechen.
#We can also inspect our model visually using netron.
netron.start(onnx_filename)
Abb. 6: Die ONNX-Darstellung unseres neuronalen Netzes
Abb. 7: Meta-Details zu unserem ONNX-Modell
Netron ist eine Open-Source-Python-Bibliothek, die es uns ermöglicht, ONNX-Modelle visuell zu inspizieren, ihre Parameter zu überprüfen und Metadaten zu kontrollieren. Für diejenigen, die mehr über die Verwendung von ONNX-Modellen in MetaTrader 5 erfahren möchten, gibt es viele gut geschriebene Artikel. Einer meiner Lieblingsautoren zu diesem Thema ist Omega.
Implementierung in MQL5
Nachdem die Konfiguration unseres ONNX-Modells abgeschlossen ist, können wir mit der Erstellung unseres Expert Advisors in MQL5 beginnen.
Abb. 8: Ein schematischer Plan unseres Expert Advisors
Unser Expert Advisor wird das angepasste ONNX-Modell verwenden, um Einstiegssignale zu generieren. Alle guten Händler lassen jedoch Vorsicht walten und führen nicht jedes Einstiegssignal aus, das sie erhalten. Um unserem Expert Advisor diese Disziplin beizubringen, werden wir ihn so programmieren, dass er auf die Bestätigung durch technische Indikatoren wartet, bevor er eine Position eröffnet.
Diese technischen Indikatoren helfen uns, den Zeitpunkt für unsere Einstiege effektiv zu bestimmen. Sobald Positionen geöffnet sind, werden sie durch nutzerdefinierte Stop-Loss-Levels geschlossen. Der erste Schritt besteht darin, das ONNX-Modell als Ressource für unsere Anwendung zu spezifizieren.
//+------------------------------------------------------------------+ //| Custom Deep Neural Network.mq5 | //| Gamuchirai Zororo Ndawana | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Zororo Ndawana" #property link "https://www.mql5.com/en/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| Load the ONNX model | //+------------------------------------------------------------------+ #resource "\\Files\\Brent_M1.onnx" as const uchar ModelBuffer[];
Als Nächstes werden wir die Handelsbibliothek laden, die für die Verwaltung unserer Positionen unerlässlich ist.
//+------------------------------------------------------------------+ //| Libraries we need | //+------------------------------------------------------------------+ #include <Trade/Trade.mqh> CTrade Trade;
Nun können wir globale Variablen für unser Programm erstellen.
//+------------------------------------------------------------------+ //| Gloabal variables | //+------------------------------------------------------------------+ long model; //The handler for our ONNX model vector forecast = vector::Zeros(1); //Our model's forecast const int states = 3; //The total number of states the system can be in vector state = vector::Zeros(states); //The state of our system int mfi_handler,wpr_handler; //Handlers for our technical indicators vector mfi_reading,wpr_reading; //The values of our indicators will be kept in vectors double minimum_volume, trading_volume; //Smallest lot size allowed & our calculated lotsize double ask_price, bid_price; //Market rates
Lassen Sie uns Nutzereingaben definieren, mit denen wir das Verhalten des Expert Advisors ändern können.
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input int mfi_period = 20; //Money Flow Index Period input int wpr_period = 30; //Williams Percent Range Period input int lot_multiple = 20; //How big should our lot sizes be? input double sl_width = 2; //How tight should the stop loss be? input double max_profit = 10; //Close the position when this profit level is reached. input double max_loss = 10; //Close the position when this loss level is reached.
Unsere Anwendung benötigt Hilfsfunktionen, um bestimmte Routinen auszuführen. Wir beginnen mit der Definition einer Funktion, die den Zustand der Anwendung verwaltet. Diese Anwendung wird drei Zustände haben: Zustand 0 bedeutet, dass wir keine Positionen haben, während die Zustände 1 und 2 eine Kauf- bzw. Verkaufsposition anzeigen.
Je nach aktuellem Zustand hat die Anwendung Zugriff auf unterschiedliche Funktionen.
//+------------------------------------------------------------------+ //| This function is responsible for updating the system state | //+------------------------------------------------------------------+ void update_state(int index) { //--- Reset the system state state = vector::Zeros(states); //--- Now update the current state state[index] = 1; }
Als Nächstes benötigen wir eine Funktion, die für die Überprüfung der Nutzereingaben beim Starten der Anwendung verantwortlich ist. Diese Funktion stellt zum Beispiel sicher, dass alle Perioden der technischen Indikatoren größer als 0 sind.
//+------------------------------------------------------------------+ //| This function will ensure that user inputs are valid | //+------------------------------------------------------------------+ bool valid_inputs(void) { //--- Let us validate the inputs the user passed return((mfi_period > 0)&&(wpr_period > 0) && (max_profit >= 0) && (max_loss >= 0) && (lot_multiple >= 0) && (sl_width >= 0)); }
Unser Expert Advisor prüft laufend, ob die Gewinnniveaus den Vorgaben des Nutzers entsprechen. Wenn der Nutzer beispielsweise ein maximales Gewinnziel von $1 festlegt, wird die Position automatisch geschlossen, sobald sie einen Gewinn von $1 erreicht hat, auch wenn das Take-Profit-Niveau noch nicht erreicht wurde. Die gleiche Logik gilt für den Stop-Loss: Die Position wird geschlossen, je nachdem, welcher Schwellenwert zuerst erreicht wird, sei es das Stop-Loss-Niveau oder das maximale Verlustniveau. Diese Funktion soll Flexibilität bei der Festlegung akzeptabler Risikoniveaus bieten.
//+------------------------------------------------------------------+ //| This function will check our profit levels | //+------------------------------------------------------------------+ void check_profit_level(void) { //--- Let us check if the user set a max profit/loss limit if(max_loss > 0 || max_profit > 0) { //--- If true, let us inspect whether we have passed the limit. if((PositionGetDouble(POSITION_PROFIT) > max_profit) || (PositionGetDouble(POSITION_PROFIT) < (max_loss * -1))) { //--- Close the position Trade.PositionClose(Symbol()); } } }
Da wir über ein KI-basiertes System verfügen, sollten wir eine Funktion entwickeln, die überprüft, ob unser Modell eine Marktbewegung vorhersagt, die sich negativ auf unsere offene Position auswirken könnte. Solche Signale können als Frühindikatoren für eine veränderte Marktstimmung dienen.
//+------------------------------------------------------------------+ //| If we predict a reversal, let's close our positions | //+------------------------------------------------------------------+ void find_reversal(void) { //--- We have a position if(((state[1] == 1) && (forecast[0] < iClose(Symbol(),PERIOD_CURRENT,0))) || ((state[2] == 1) && (forecast[0] > iClose(Symbol(),PERIOD_CURRENT,0)))) { Trade.PositionClose(Symbol()); } }
Als Nächstes definieren wir eine Funktion, die auf gültige Einstiegssignale prüft. Ein Einstiegssignal wird als gültig angesehen, wenn es zwei Bedingungen erfüllt: Erstens muss es durch Kursänderungen auf höheren Zeitrahmen unterstützt werden; zweitens muss unser KI-Modell eine Kursbewegung vorhersagen, die mit diesem höheren Trend übereinstimmt. Wenn beide Bedingungen erfüllt sind, werden wir unsere technischen Indikatoren überprüfen, um die endgültige Bestätigung zu erhalten.
//+------------------------------------------------------------------+ //| This function will determine if we have a valid entry | //+------------------------------------------------------------------+ void find_entry(void) { //--- First we want to know if the higher timeframes are moving in the same direction we want to go double higher_time_frame_trend = iClose(Symbol(),PERIOD_W1,16) - iClose(Symbol(),PERIOD_W1,0); //--- If price levels appreciated, the difference will be negative if(higher_time_frame_trend < 0) { //--- We may be better off only taking buy opportunities //--- Buy opportunities are triggered when the model's prediction is greater than the current price if(forecast[0] > iClose(Symbol(),PERIOD_CURRENT,0)) { //--- We will use technical indicators to time our entries bullish_sentiment(); } } //--- If price levels depreciated, the difference will be positive if(higher_time_frame_trend > 0) { //--- We may be better off only taking sell opportunities //--- Sell opportunities are triggered when the model's prediction is less than the current price if(forecast[0] < iClose(Symbol(),PERIOD_CURRENT,0)) { //--- We will use technical indicators to time our entries bearish_sentiment(); } } }
Nun sind wir bei der Funktion angelangt, die für die Interpretation unserer technischen Indikatoren zuständig ist. Es gibt verschiedene Möglichkeiten, diese Indikatoren zu interpretieren; ich ziehe es jedoch vor, sie auf 50 zu zentrieren. Dabei bestätigen Werte über 50 eine Aufwärts-Stimmung, während Werte unter 50 eine Abwärts-Stimmung anzeigen. Wir werden den Money Flow Index (MFI) als Volumenindikator und den Williams Percent Range (WPR) als Trendstärkeindikator verwenden.
//+------------------------------------------------------------------+ //| This function will interpret our indicators for buy signals | //+------------------------------------------------------------------+ void bullish_sentiment(void) { //--- For bullish entries we want strong volume readings from our MFI //--- And confirmation from our WPR indicator wpr_reading.CopyIndicatorBuffer(wpr_handler,0,0,1); mfi_reading.CopyIndicatorBuffer(mfi_handler,0,0,1); if((wpr_reading[0] > -50) && (mfi_reading[0] > 50)) { //--- Get the ask price ask_price = SymbolInfoDouble(Symbol(),SYMBOL_ASK); //--- Make sure we have no open positions if(PositionsTotal() == 0) Trade.Buy(trading_volume,Symbol(),ask_price,(ask_price - sl_width),(ask_price + sl_width),"Custom Deep Neural Network"); update_state(1); } } //+------------------------------------------------------------------+ //| This function will interpret our indicators for sell signals | //+------------------------------------------------------------------+ void bearish_sentiment(void) { //--- For bearish entries we want strong volume readings from our MFI //--- And confirmation from our WPR indicator wpr_reading.CopyIndicatorBuffer(wpr_handler,0,0,1); mfi_reading.CopyIndicatorBuffer(mfi_handler,0,0,1); if((wpr_reading[0] < -50) && (mfi_reading[0] < 50)) { //--- Get the bid price bid_price = SymbolInfoDouble(Symbol(),SYMBOL_BID); if(PositionsTotal() == 0) Trade.Sell(trading_volume,Symbol(),bid_price,(bid_price + sl_width),(bid_price - sl_width),"Custom Deep Neural Network"); //--- Update the state update_state(2); } }
Als Nächstes konzentrieren wir uns auf die Vorhersagen unseres ONNX-Modells. Zur Erinnerung: Unser Modell erwartet Eingaben der Form [1,4] und liefert Ausgaben der Form [1,1]. Wir definieren Vektoren, um die Eingaben und Ausgaben entsprechend zu speichern, und verwenden dann die Funktion OnnxRun, um die Prognose des Modells zu erhalten.
//+------------------------------------------------------------------+ //| This function will fetch forecasts from our model | //+------------------------------------------------------------------+ void model_predict(void) { //--- First we get the input data ready vector input_data = {iOpen(_Symbol,PERIOD_CURRENT,0),iHigh(_Symbol,PERIOD_CURRENT,0),iLow(_Symbol,PERIOD_CURRENT,0),iClose(_Symbol,PERIOD_CURRENT,0)}; //--- Now we need to perform inferencing if(!OnnxRun(model,ONNX_DATA_TYPE_FLOAT,input_data,forecast)) { Comment("Failed to obtain a forecast from the model: ",GetLastError()); forecast[0] = 0; return; } //--- We succeded! Comment("Model forecast: ",forecast[0]); }
Jetzt können wir mit der Erstellung einer Ereignisbehandlung für unsere Anwendung beginnen, der bei der Initialisierung des Expert Advisors aufgerufen wird. Unser Verfahren wird zunächst die Nutzereingaben validieren und dann die Eingabe- und Ausgabeformen unseres ONNX-Modells definieren. Als Nächstes werden wir unsere technischen Indikatoren einrichten, Marktdaten abrufen und schließlich den Status unseres Systems auf 0 setzen.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Make sure user inputs are valid if(!valid_inputs()) { Comment("Invalid inputs were passed to the application."); return(INIT_FAILED); } //--- Create the ONNX model from the buffer model = OnnxCreateFromBuffer(ModelBuffer,ONNX_DEFAULT); //--- Check if we were succesfull if(model == INVALID_HANDLE) { Comment("[ERROR] Failed to create the ONNX model from the buffer: ",GetLastError()); return(INIT_FAILED); } //--- Set the input shape of the model ulong input_shape[] = {1,4}; //--- Check if we were succesfull if(!OnnxSetInputShape(model,0,input_shape)) { Comment("[ERROR] Failed to set the ONNX model input shape: ",GetLastError()); return(INIT_FAILED); } //--- Set the output shape of the model ulong output_shape[] = {1,1}; //--- Check if we were succesfull if(!OnnxSetOutputShape(model,0,output_shape)) { Comment("[ERROR] Failed to set the ONNX model output shape: ",GetLastError()); return(INIT_FAILED); } //--- Setup the technical indicators wpr_handler = iWPR(Symbol(),PERIOD_CURRENT,wpr_period); mfi_handler = iMFI(Symbol(),PERIOD_CURRENT,mfi_period,VOLUME_TICK); //--- Fetch market data minimum_volume = SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN); trading_volume = minimum_volume * lot_multiple; //--- Set the system to state 0, indicating we have no open positions update_state(0); //--- Everything went fine return(INIT_SUCCEEDED); }
Ein entscheidender Teil unserer Anwendung ist die Deinitialisierungsprozedur. In dieser Ereignisbehandlung werden alle Ressourcen freigegeben, die nicht mehr benötigt werden, wenn der Expert Advisor nicht in Gebrauch ist.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Free the onnx resources OnnxRelease(model); //--- Free the indicator resources IndicatorRelease(wpr_handler); IndicatorRelease(mfi_handler); //--- Detach the expert advisor ExpertRemove(); }
Schließlich müssen wir unsere Ereignishandler von OnTick definieren. Welche Maßnahmen ergriffen werden, hängt vom Zustand des Systems ab. Wenn wir keine offenen Positionen haben (Zustand 0), besteht unsere Priorität darin, eine Prognose von unserem Modell zu erhalten und einen möglichen Einstieg zu identifizieren. Wenn wir eine offene Position haben (Status 1 für Kauf oder Status 2 für Verkauf), liegt unser Schwerpunkt auf der Verwaltung der Position. Dazu gehören die Überwachung möglicher Umkehrungen und die Überprüfung von Risikoniveaus, Gewinnzielen und maximalen Gewinnhöhen.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Which state is the system in? if(state[0] == 1) { //--- Being in this state means we have no open positions, let's analyse the market to try find one model_predict(); find_entry(); } if((state[1] == 1) || (state[2] == 1)) { //--- Being in this state means we have an position open, if our model forecasts a reversal move we will close model_predict(); find_reversal(); check_profit_level(); } } //+------------------------------------------------------------------+
Abb. 9: Testen unseres Expert Advisors
Schlussfolgerung
Dieser Artikel bot eine leichte Einführung in die Verwendung von Optimierungsalgorithmen für die Auswahl von Modellhyperparametern. In künftigen Artikeln werden wir eine robustere Methodik anwenden und zwei spezielle Datensätze verwenden: einen für die Optimierung des Modells und den anderen für die Kreuzvalidierung und den Vergleich der Leistung mit einem Modell mit Standardeinstellungen.
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/15413
- 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.