Selbstoptimierende Expert Advisors in MQL5 (Teil 16): Überwachte lineare Systemidentifikation
In unserer vorangegangenen Diskussion über Feedback-Controller haben wir gelernt, dass diese Systeme die Leistung von Handelsstrategien stabilisieren können, indem sie zunächst ihr Verhalten in Aktion beobachten. Wir haben hier einen kurzen Link zu der früheren Diskussion bereitgestellt. Dieses Anwendungsdesign ermöglichte es uns, die vorherrschenden Korrelationsstrukturen zu erfassen, die sowohl bei Gewinn- als auch bei Verlustgeschäften bestehen blieben. Im Wesentlichen halfen Feedback-Controller unserer Handelsanwendung zu lernen, wie man sich unter den aktuellen Marktbedingungen optimal verhält – ähnlich wie menschliche Händler, die sich weniger auf die Vorhersage der Zukunft als vielmehr auf eine intelligente Reaktion auf die Gegenwart konzentrieren.
Der Leser sollte wissen, dass wir uns bisher auf rückgekoppelte Regler konzentriert haben, die einfache, regelbasierte Strategien korrigieren. Dieser einfache Ansatz ermöglichte es dem Leser, die Wirkung des Feedback-Controllers sofort zu erkennen, selbst wenn er zum ersten Mal mit der Materie in Berührung kam. In der folgenden Abbildung 1 haben wir ein schematisches Diagramm der Anwendungskonfiguration erstellt, um dem Leser die Änderungen, die wir heute vornehmen, zu verdeutlichen.

Abbildung 1: Visualisierung des Entwurfsmusters, das wir ursprünglich für unseren Rückkopplungsregler ausgewählt haben
In dieser Diskussion gehen wir über diese Grenze hinaus und stellen eine tiefer gehende Frage: Können wir lernen, eine Handelsstrategie, die selbst durch ein statistisches Modell des Marktes definiert ist, optimal zu steuern? Dies bedeutet einen Wandel in der Art und Weise, wie wir maschinelles Lernen im algorithmischen Handel anwenden. Anstatt Modelle nur für Vorhersagen zu verwenden, untersuchen wir, wie statistische Modelle einander überwachen oder korrigieren können – eine potenziell neue Klasse von Aufgaben für maschinelle Lernsysteme.

Abbildung 2: Wir werden die festgelegte Handelsstrategie durch ein statistisches Modell ersetzen, das anhand der Marktdaten geschätzt wird
Unser Ziel ist es, herauszufinden, ob der Start mit einer ausgefeilteren, datengesteuerten Handelsstrategie dem Feedback-Controller eine reichhaltigere Struktur bietet, aus der er lernen kann, und letztlich zu besseren Ergebnissen führt. Um dies zu untersuchen, griffen wir auf unsere früheren Arbeiten zur Rückkopplungssteuerung und Identifizierung linearer Systeme zurück, bei denen wir eine einfache Strategie des gleitenden Mittelwerts entwickelt und einen Rückkopplungsregler eingebaut hatten, um eine Grundlage zu schaffen. Anschließend ersetzten wir die Komponente des gleitenden Durchschnitts durch ein überwachtes statistisches Modell des EUR/USD-Marktes und bewerteten die Leistung unter identischen Testbedingungen. Die Ergebnisse waren:
- Der Nettogewinn stieg von 56 US-Dollar im Basissystem auf 170 US-Dollar – eine Verbesserung um fast 200 %.
- Der Bruttoverlust sank von 333 $ auf 143 $, was einer Verringerung des Verlustrisikos um 57 % entspricht.
- Die Genauigkeit verbesserte sich von 52,9 % auf 72 %, was einer Steigerung der Präzision um 37 % entspricht.
- Die Zahl der Abschlüsse sank von 51 auf 33, was einer Effizienzsteigerung von 35 % entspricht und zeigt, dass das System unnötige Abschlüsse herausgefiltert hat.
- Der Gewinnfaktor verbesserte sich von 1,17 auf 2,18, was einer Steigerung der Rentabilität pro Risikoeinheit um 86 % entspricht.
Zusammengenommen zeigen diese Ergebnisse, dass die Kopplung eines rückgekoppelten Reglers mit einem gut geeigneten statistischen Modell zu wesentlichen Verbesserungen sowohl der Effizienz als auch der Stabilität führen kann. Die Synergie zwischen geschlossenem Regelkreis und überwachtem Lernen ermöglicht eine Form der intelligenten Anpassung. Ein solches System kann an Algorithmen des Reinforcement Learning erinnern, allerdings aus einer überwachten Perspektive.
Am Ende wird dieser Artikel die Design-Entscheidungen umreißen, die dieses verbesserte System geformt haben, und den Lesern einen strukturierten Ansatz für die Verbesserung der Leistung ihrer eigenen MetaTrader 5-Anwendungen unter Verwendung von Feedback-Control-Prinzipien zur Unterstützung ihrer statistischen Modelle bieten.
Erste Schritte mit unserer Analyse in Python
Der erste Schritt in unserer Python-basierten Analyse von MetaTrader 5 Marktdaten ist der Import der notwendigen Bibliotheken.
#Import the standard python libraries import numpy as np import pandas as pd import seaborn as sns import matplotlib.pyplot as plt import MetaTrader5 as mt5
Sobald unsere Abhängigkeiten geladen sind, fahren wir mit der Initialisierung des MetaTrader 5 Terminals fort.
#Check if we have started the terminal if(mt5.initialize()): print("Failed To Startup") else: print("Logged In")
Eingeloggt
In diesem Stadium wählen wir das Marktsymbol aus, das wir analysieren möchten.
if(mt5.symbol_select("EURUSD")): print("Found EURUSD Market") else: print("Failed To Find EURUSD Market")
EURUSD-Markt gefunden
Wenn Sie bis hierhin durchgehalten haben, können Sie nun historische Marktdaten direkt von Ihrem MetaTrader 5-Terminal abrufen. Stellen Sie sicher, dass Sie die Zeitstempel von Sekunden in ein für Menschen lesbares Format umwandeln, da MetaTrader 5 die Zeitdaten standardmäßig in Sekunden zurückgibt.
#Read in the market data data = pd.DataFrame(mt5.copy_rates_from_pos("EURUSD",mt5.TIMEFRAME_D1,0,4000)) data['time'] = pd.to_datetime(data['time'],unit='s') data

Abbildung 3: Die Marktdaten, die wir vom MetaTrader 5-Terminal abgerufen haben
Das Terminal liefert einen detaillierten Datensatz mit mehreren Marktattributen. Für diese Diskussion werden wir uns jedoch nur auf die vier wichtigsten Kursniveaus konzentrieren: Open, High, Low und Close. Daher werden alle anderen Spalten aus dem Datensatz entfernt.
#Focus on the major price levels data = data.iloc[:,:5] data

Abbildung 4: Wir konzentrieren uns in dieser Übung auf die vier grundsätzlichen Preisniveaus.
Als Nächstes werden alle Beobachtungen entfernt, die sich mit dem geplanten Backtest-Zeitraum überschneiden. In unserer vorangegangenen Diskussion über die Identifizierung linearer Systeme haben wir einen Backtest vom 1. Januar 2023 bis zum Oktober 2025 (dem aktuellen Zeitraum zum Zeitpunkt der Erstellung dieses Artikels) durchgeführt. Um die Konsistenz zu wahren, werden wir hier das gleiche Backtest-Fenster beibehalten. Es ist eine gute Praxis, alle Daten zu eliminieren, die Informationen aus dem Testzeitraum in den Trainingssatz einfließen lassen könnten.
#Drop off the test period data = data.iloc[:-(370*2),:] data

Abbildung 5: Es ist eine gute Praxis, alle Beobachtungen, die sich mit dem geplanten Backtest überschneiden, zu streichen.
Nachdem wir unseren Datensatz bereinigt haben, legen wir nun den Prognosehorizont fest – wie weit in die Zukunft unser Modell versuchen wird, Prognosen zu erstellen – und beschriften den Datensatz entsprechend mit den Zielwerten.
#Define the new horizon HORIZON = 10
Schließlich werden alle fehlenden Zeilen gelöscht, um die Datenintegrität zu gewährleisten. Sobald der Datensatz vollständig und richtig formatiert ist, können wir die Bibliotheken für maschinelles Lernen laden und mit dem Modelltraining beginnen.
#Label the data data['Target 1'] = data['close'].shift(-HORIZON) data['Target 2'] = data['high'].shift(-HORIZON) data['Target 3'] = data['low'].shift(-HORIZON)
Dann werden alle Zeilen mit fehlenden Daten gelöscht.
#Drop missing rows data.dropna(inplace=True)
Nun können wir mit der Anpassung unserer Modelle für maschinelles Lernen beginnen. Da wir nicht wissen, welches Modell am besten geeignet ist, importieren wir verschiedene Modelle, um den Anfang zu machen.
#Import cross validation tools from sklearn.linear_model import Ridge,LinearRegression from sklearn.model_selection import TimeSeriesSplit,cross_val_score from sklearn.metrics import root_mean_squared_error from sklearn.neural_network import MLPRegressor from sklearn.ensemble import RandomForestRegressor,GradientBoostingRegressor from sklearn.neighbors import KNeighborsRegressor,RadiusNeighborsRegressor from sklearn.svm import LinearSVR
Erzeugt neue Instanzen der einzelnen Modelle.
models = [LinearRegression(), Ridge(alpha=10e-3), RandomForestRegressor(random_state=0), GradientBoostingRegressor(random_state=0), KNeighborsRegressor(n_jobs=-1,n_neighbors=5), RadiusNeighborsRegressor(n_jobs=-1), LinearSVR(random_state=0), MLPRegressor(random_state=0,hidden_layer_sizes=(4,10,40,10),solver='lbfgs')]
Aufteilung der Daten in gleiche Hälften. Eine für die Ausbildung und die zweite für die Prüfung.
#The big picture of what we want to test train , test = data.iloc[:data.shape[0]//2,:] , data.iloc[data.shape[0]//2:,:]
Nun, da unser Datensatz fertig ist, können wir die Eingaben und Ziele für unser maschinelles Lernmodell definieren.
#Define inputs and target X = data.columns[1:-3] y = data.columns[-3:]
Wir beginnen mit der Erstellung einer speziellen Funktion, die bei jedem Aufruf eine neue Modellinstanz zurückgibt.
#Fetch a new copy of the model def get_model(): return(LinearRegression())
Wie wir in der vorangegangenen Diskussion gelernt haben, sind nicht alle historischen Daten immer für aktuelle Prognosen geeignet. Leser, die unsere frühere Diskussion über das Marktgedächtnis nicht gelesen haben, finden hier einen hilfreichen Link. Um festzustellen, wie viel Historie wir tatsächlich benötigen, führen wir erneut eine Kreuzvalidierung durch, wobei wir dieses Mal testen, wie gut die erste Hälfte unserer Trainingsdaten die zweite Hälfte vorhersagen kann. Unsere Ergebnisse zeigen, dass nur etwa 60 % der ersten Hälfte erforderlich sind, um die verbleibende Hälfte genau vorherzusagen. Das bedeutet, dass wir unsere Trainingsmenge sicher reduzieren können, um uns nur auf die kohärenteste Partition zu konzentrieren – den Teil der Daten, der intern konsistent erscheint.
#Store our performance error = [] #Define the total number of iterations we wish to perform ITERATIONS = 10 #Let us perform the line search for i in np.arange(ITERATIONS): #Training fraction fraction =((i+1)/10) #Partition the data to select the most recent information partition_index = train.shape[0] - int(train.shape[0]*fraction) train_X_partition = train.loc[partition_index:,X] train_y_partition = train.loc[partition_index:,y[0]] #Fit a model model = get_model() #Fit the model model.fit(train_X_partition,train_y_partition) #Cross validate the model out of sample score = root_mean_squared_error(test.loc[:,y[0]],model.predict(test.loc[:,X])) #Append the error levels error.append(score) #Plot the results plt.title('Improvements Made By Historical Data') plt.plot(error,color='black') plt.grid() plt.ylabel('Out of Sample RMSE') plt.xlabel('Progressivley Fitting On All Historical Data') plt.scatter(np.argmin(error),np.min(error),color='red')

Abbildung 6: Wie wir in der vorangegangenen Diskussion über die effektive Kreuzvalidierung des Gedächtnisses gelernt haben, sind nicht alle vorhandenen historischen Daten hilfreich
Identifizieren wir den kohärenten Index.
#Let us select the partition of interest partition_index = train.shape[0] - int(train.shape[0]*(0.6))
Die Trainingsdaten umgestalten und ältere, weniger relevante Beobachtungen entfernen.
train = train.loc[partition_index:,:] train.reset_index(inplace=True,drop=True) train

Abbildung 7: Wir haben unseren Datensatz auf die Beobachtungen beschränkt, die unserer Meinung nach am besten mit der aktuellen Situation übereinstimmen
Zu einem früheren Zeitpunkt in unserem Entwurfsprozess haben wir eine Liste von möglichen Modelltypen definiert. Wir werden nun jede einzelne durchgehen und ihre Leistung in der Testmenge bewerten. Beachten Sie, dass wir die Modelle zwar auf den Testdaten evaluieren, sie aber nie auf diesen Testsatz anpassen, da dieser für unseren abschließenden Backtest reserviert ist.
#Store each model's error levels error = [] #Fit each model for m in models: m.fit(train.loc[:,X],train.loc[:,y[0]]) #Store our error levels error.append(root_mean_squared_error(test.loc[:,y[0]],m.predict(test.loc[:,X])))
Als Nächstes wird die Leistung jedes Modells in einem Balkendiagramm dargestellt. Wie gezeigt, schneidet das Ridge-Regressionsmodell am besten ab, obwohl das Deep Neural Network (DNN) dicht dahinter liegt. Dies deutet darauf hin, dass das DNN von einer Parameteroptimierung profitieren könnte.
sns.barplot(error,color='black') plt.axhline(np.min(error),color='red',linestyle=':') plt.scatter(np.argmin(error),np.min(error),color='red') plt.ylabel('Out of Sample RMSE') plt.title('Model Selection For EURUSD Market') plt.xticks([0,1,2,3,4,5,6,7],['OLS','Ridge','RF','GBR','KNR','RNR','LSVR','DNN'])

Abbildung 8: Wir haben ein gutes Benchmark-Modell identifiziert, das besser abschneidet
Um optimale Parameter für das DNN zu finden, verwenden wir eine Zeitreihen-Kreuzvalidierung mit scikit-learn.
from sklearn.model_selection import RandomizedSearchCV,TimeSeriesSplit
Wir definieren die Anzahl der Aufteilungen und den zeitlichen Abstand zwischen den einzelnen Faltungen und legen dann ein Parameterraster fest, das alle zu untersuchenden Werte umfasst. Anschließend definieren wir eine Grundkonfiguration des neuronalen Netzes mit festen Parametern, um die Reproduzierbarkeit zu gewährleisten. Zum Beispiel deaktivieren wir shuffle=True (da Zeitreihendaten die Reihenfolge beibehalten müssen) und setzen den Zufallsstatus auf 0, damit die Initialisierung der Gewichte über alle Läufe hinweg konsistent bleibt. Außerdem deaktivieren wir das frühzeitige Stoppen und setzen die maximale Anzahl der Iterationen auf 1000.
#Define the time series cross validation tool tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON) #Define the parameter values we want to search over dist = dict( loss=['squared_error','poisson'], activation = ['identity','relu','tanh','logistic'], solver=['adam','lbfgs','sgd'], learning_rate=['constant','invscaling','adaptive'], learning_rate_init=[1,0,10e-1,10e-2,10e-3], hidden_layer_sizes=[(4,10,4),(4,4,4,4),(4,1,8,2),(4,2,6,3),(4,2,1,4),(4,2,8,16,2)], alpha=[1,0,10e-1,10e-2,10e-3] ) #Define basic model parameters we want to keep fixed model = MLPRegressor(shuffle=False,random_state=0,early_stopping=False,max_iter=1000) #Define the randomized search object rscv = RandomizedSearchCV(model,cv=tscv,param_distributions=dist,random_state=0,n_iter=50) #Perform the search rscv.fit(train.loc[:,X],train.loc[:,y[0]]) #Retreive the best parameters we found rscv.best_params_
{'solver': 'lbfgs',
'loss': 'squared_error',
'learning_rate_init': 0.1,
'learning_rate': 'adaptive',
'hidden_layer_sizes': (4, 2, 1, 4),
'alpha': 0.01,
'activation': 'identity'}
Nach Durchführung der Rastersuche liefert das Modell die beste Parameterkombination, die wir mit früheren Ergebnissen vergleichen. Interessanterweise übertrifft unser optimiertes DNN – sichtbar ganz rechts im Leistungsdiagramm – immer noch nicht den Ridge-Regressions-Benchmark.
sns.barplot(error,color='black') plt.scatter(x=np.argmin(error),y=np.min(error),color='red') plt.axhline(np.min(error),color='red',linestyle=':') plt.xticks([0,1,2,3,4,5,6,7,8],['OLS','Ridge','RF','GBR','KNR','RNR','LSVR','DNN','ODNN']) plt.ylabel('Out of Sample RMSE') plt.title('Final Model Selection For EURUSD 2023-2025 Backtest')

Abbildung 9: Wir haben das zuvor ermittelte Kontrollniveau erfolgreich übertroffen
Exportieren nach ONNX
Nachdem die Optimierung abgeschlossen ist, exportieren wir das endgültige Modell in das Open Neural Network Exchange (ONNX)-Format. ONNX bietet eine rahmenunabhängige Schnittstelle, die es ermöglicht, trainierte Modelle gemeinsam zu nutzen und in verschiedenen Programmierumgebungen einzusetzen, ohne dass die ursprünglichen Trainingsabhängigkeiten übernommen werden müssen.
#Fit the baseline model
model = rscv.best_estimator_Um den Export zu beginnen, definieren wir das Modell und importieren die notwendigen ONNX-Bibliotheken.
#Prepare to export to ONNX import onnx from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType
Und wir geben die Eingabeform (1x4, entsprechend den vier Hauptpreisniveaus) und die Ausgabeform (1x1, entsprechend dem vorhergesagten Wert) an.
#Define ONNX model input and output dimensions initial_types = [("FLOAT_INPUT",FloatTensorType([1,4]))] final_types = [("FLOAT_OUTPUT",FloatTensorType([1,1]))]
Anschließend erstellen wir den ONNX-Prototyp, eine Zwischenform des Modells.
#Convert the model to its ONNX prototype onnx_proto = convert_sklearn(model,initial_types=initial_types,final_types=final_types,target_opset=12)
Abschließend speichern wir sie als ONNX-Pufferdatei auf der Festplatte, die wir anschließend in unsere MetaTrader 5-Anwendung importieren werden.
#Save the ONNX model onnx.save(onnx_proto,"EURUSD Improved Baseline LR.onnx")
Aufbau unserer MQL5-Anwendung
Nachdem unser ONNX-Modell definiert und fertig ist, beginnen wir nun mit dem Aufbau der MetaTrader 5-Anwendung. Der erste Schritt besteht darin, die Systemkonstanten zu definieren – die festen Parameter, die unsere Strategie während der gesamten Anwendung leiten. Dazu gehören die Perioden der gleitenden Durchschnitte, die Anzahl der Beobachtungen, die erforderlich sind, bevor der Rückkopplungsregler aktiv wird, und die Anzahl der Eingangs- und Ausgangsvariablen für das ONNX-Modell.//+------------------------------------------------------------------+ //| Feedback Control Benchmark .mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define SYMBOL Symbol() #define MA_PERIOD 42 #define MA_SHIFT 0 #define MA_MODE MODE_EMA #define MA_APPLIED_PRICE PRICE_CLOSE #define SYSTEM_TIME_FRAME PERIOD_D1 #define MIN_VOLUME SymbolInfoDouble(SYMBOL,SYMBOL_VOLUME_MIN) #define OBSERVATIONS 90 #define FEATURES 7 #define MODEL_INPUTS 8 #define TOTAL_MODEL_INPUTS 4 #define TOTAL_MODEL_OUTPUTS 1
Sobald diese Konstanten definiert sind, laden wir das zuvor erstellte ONNX-Modell.
//+------------------------------------------------------------------+ //| System resources we need | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD Improved Baseline LR.onnx" as const uchar onnx_buffer[];
Unsere Anwendung importiert auch mehrere unterstützende Bibliotheken, um gängige Handelsoperationen wie das Öffnen, Schließen und Ändern von Positionen zu vereinfachen.
//+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade;
Als Nächstes definieren wir globale Variablen, um einen gemeinsamen Status für alle Funktionen aufrechtzuerhalten und sicherzustellen, dass dieselben Schlüsselwerte überall zugänglich sind, wo sie benötigt werden.
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ma_handler,atr_handler,scenes; bool forecast; long onnx_model; double ma[],atr[]; double ask,bid,open,high,low,close,padding; matrix snapshots,b,X,y,U,S,VT,current_forecast; vector s; vectorf onnx_inputs,onnx_output;
Während der Initialisierung instanziieren wir das ONNX-Modell aus dem exportierten Puffer und führen eine Integritätsprüfung durch, um sicherzustellen, dass es nicht beschädigt wurde. Wenn dies gelingt, definieren wir die Eingabe- und Ausgabeformen des Modells, die mit denen in Python übereinstimmen müssen. Anschließend laden wir unsere technischen Indikatoren und initialisieren die globalen Variablen mit Standardwerten.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Create the ONNX model from its buffer onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DATA_TYPE_FLOAT); //--- Check for errors if(onnx_model == INVALID_HANDLE) { //--- User feedback Print("An error occured loading the ONNX model:\n",GetLastError()); //--- Abort return(INIT_FAILED); } //--- Setup the ONNX handler input shape else { //--- Define the I/O shapes ulong input_shape[] = {1,4}; ulong output_shape[] = {1,1}; //--- Attempt to set input shape if(!OnnxSetInputShape(onnx_model,0,input_shape)) { //--- User feedback Print("Failed to specify the correct ONNX model input shape:\n",GetLastError()); //--- Abort return(INIT_FAILED); } //--- Attempt to set output shape if(!OnnxSetOutputShape(onnx_model,0,output_shape)) { //--- User feedback Print("Failed to specify the correct ONNX model output shape:\n",GetLastError()); //--- Abort return(INIT_FAILED); } } //--- Initialize the indicator ma_handler = iMA(SYMBOL,SYSTEM_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_MODE,MA_APPLIED_PRICE); atr_handler = iATR(SYMBOL,SYSTEM_TIME_FRAME,14); //--- Prepare global variables forecast = false; snapshots = matrix::Zeros(FEATURES,OBSERVATIONS); scenes = -1; return(INIT_SUCCEEDED); }
Wenn das Programm endet, werden alle zugewiesenen Ressourcen freigegeben, um eine effiziente Speichernutzung zu gewährleisten.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Release the ONNX model OnnxRelease(onnx_model); //--- Release the indicator IndicatorRelease(ma_handler); IndicatorRelease(atr_handler); }
Immer wenn neue Kursdaten eintreffen, prüft das System, ob sich eine neue Kerze gebildet hat. Wenn sich eine neue Kerze gebildet hat, werden sowohl die Kerzenanzahl als auch die Gesamtzahl der vom Feedback-Controller beobachteten „Szenen“ (Episoden) aktualisiert. Sobald der Controller die erforderliche Anzahl von Beobachtungen gesammelt hat, wird er aktiviert – ab diesem Zeitpunkt werden seine Prognosen herangezogen, bevor neue Handelsgeschäfte getätigt werden.
Wenn keine offenen Positionen vorhanden sind, aktualisiert die Anwendung ihre Indikatoren und fordert eine Prognose an – entweder vom Feedback-Controller (falls aktiv) oder vom ONNX-Modell. Das Modell erhält die vier wichtigsten Preisniveaus als Input und gibt einen Prognosewert aus. Das System erstellt dann eine Momentaufnahme von Schlüsselvariablen wie Preisniveau, Kontostand, Kapital und Indikatorwerten.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Check if a new candle has formed datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0); static datetime time_stamp; if(current_time != time_stamp) { //--- Update the time time_stamp = current_time; scenes = scenes+1; //--- Check how many scenes have elapsed if(scenes == (OBSERVATIONS-1)) { forecast = true; } //--- If we have no open positions if(PositionsTotal()==0) { //--- Update indicator buffers CopyBuffer(ma_handler,0,1,1,ma); CopyBuffer(atr_handler,0,0,1,atr); padding = atr[0] * 2; //--- Prepare a prediction from our model onnx_inputs = vectorf::Zeros(TOTAL_MODEL_INPUTS); onnx_inputs[0] = (float) iOpen(Symbol(),SYSTEM_TIME_FRAME,0); onnx_inputs[1] = (float) iHigh(Symbol(),SYSTEM_TIME_FRAME,0); onnx_inputs[2] = (float) iLow(Symbol(),SYSTEM_TIME_FRAME,0); onnx_inputs[3] = (float) iClose(Symbol(),SYSTEM_TIME_FRAME,0); //--- Also prepare the outputs onnx_output = vectorf::Zeros(TOTAL_MODEL_OUTPUTS); //--- Fetch current market prices ask = SymbolInfoDouble(SYMBOL,SYMBOL_ASK); bid = SymbolInfoDouble(SYMBOL,SYMBOL_BID); close = iClose(SYMBOL,SYSTEM_TIME_FRAME,1); //--- Do we need to forecast? if(!forecast) { //--- Check trading signal check_signal(); } //--- We need a forecast else if(forecast) { model_forecast(); } } //--- Take a snapshot if(!forecast) take_snapshot(); //--- Otherwise, we have positions open else { //--- Let the model decide if we should close or hold our position if(forecast) model_forecast(); //--- Otherwise record all observations on the performance of the application else if(!forecast) take_snapshot(); } } } //+------------------------------------------------------------------+
Handelssignale werden nur erzeugt, wenn keine Positionen offen sind. Erwartet das ONNX-Modell steigende Kurse, wird ein Kaufsignal registriert – allerdings nur, wenn der Kurs bereits über seinem gleitenden Durchschnitt liegt. Umgekehrt wird ein Verkaufssignal nur dann registriert, wenn der Schlusskurs unter dem gleitenden Durchschnitt liegt und das Modell einen Rückgang erwartet.
//+------------------------------------------------------------------+ //| Check for our trading signal | //+------------------------------------------------------------------+ void check_signal(void) { if(PositionsTotal() == 0) { //--- Fetch a prediction from our model if(OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_inputs,onnx_output)) { if((close > ma[0]) && (onnx_output[0] > iClose(Symbol(),SYSTEM_TIME_FRAME,0))) { Trade.Buy(MIN_VOLUME,SYMBOL,ask,ask-padding,ask+padding); } if((close < ma[0]) && (onnx_output[0] < iClose(Symbol(),SYSTEM_TIME_FRAME,0))) { Trade.Sell(MIN_VOLUME,SYMBOL,bid,ask+padding,ask-padding); } } } }
Die Vorhersagemethode des rückgekoppelten Reglers beginnt mit dem Kopieren aller zuvor aufgezeichneten Beobachtungen und dem Anhängen der aktuellen Beobachtung. Anschließend werden zwei verschobene Partitionen konstruiert: eine, die die aktuellen Eingaben repräsentiert, und eine andere, die die Ziele des nächsten Schritts (zukünftige Beobachtungen) darstellt. Die Zielvariable in diesem Setup ist der zukünftige Kontostand.
Mit Hilfe der Singulärwertzerlegung (SVD) faktorisiert der Controller die Beobachtungsmatrix in drei unitäre Matrizen. Da zwei dieser Matrizen orthogonal sind, können ihre Inversen einfach durch Transponieren ermittelt werden, sodass nur die diagonale S-Matrix zu invertieren ist. Dieser Ansatz reduziert den Rechenaufwand erheblich.
Sobald die optimalen Koeffizienten abgeleitet sind, multipliziert der Regler sie mit dem aktuellen Eingangsvektor, um ein voraussichtliches zukünftiges Gleichgewicht zu erhalten. Wenn der prognostizierte Saldo den aktuellen übersteigt, wird die Handelserlaubnis erteilt, andernfalls wird sie verweigert. In den seltenen Fällen, in denen die Koeffizientenschätzung fehlschlägt – typischerweise aufgrund einer singulären Diagonalmatrix (S enthält Nullen) – bricht der Regler den Vorhersageprozess ab.
//+------------------------------------------------------------------+ //| Obtain a forecast from our model | //+------------------------------------------------------------------+ void model_forecast(void) { Print(scenes); Print(snapshots); //--- Create a copy of the current snapshots matrix temp; temp.Copy(snapshots); snapshots = matrix::Zeros(FEATURES,scenes+1); for(int i=0;i<FEATURES;i++) { snapshots.Row(temp.Row(i),i); } //--- Attach the latest readings to the end take_snapshot(); //--- Obtain a forecast for our trading signal //--- Define the model inputs and outputs //--- Implement the inputs and outputs X = matrix::Zeros(FEATURES+1,scenes); y = matrix::Zeros(1,scenes); //--- The first row is the intercept. X.Row(vector::Ones(scenes),0); //--- Filling in the remaining rows for(int i =0; i<scenes;i++) { //--- Filling in the inputs X[1,i] = snapshots[0,i]; //Open X[2,i] = snapshots[1,i]; //High X[3,i] = snapshots[2,i]; //Low X[4,i] = snapshots[3,i]; //Close X[5,i] = snapshots[4,i]; //Moving average X[6,i] = snapshots[5,i]; //Account equity X[7,i] = snapshots[6,i]; //Account balance //--- Filling in the target y[0,i] = snapshots[6,i+1];//Future account balance } Print("Finished implementing the inputs and target: "); Print("Snapshots:\n",snapshots); Print("X:\n",X); Print("y:\n",y); //--- Singular value decomposition X.SingularValueDecompositionDC(SVDZ_S,s,U,VT); //--- Transform s to S, that is the vector to a diagonal matrix S = matrix::Zeros(s.Size(),s.Size()); S.Diag(s,0); //--- Done Print("U"); Print(U); Print("S"); Print(s); Print(S); Print("VT"); Print(VT); //--- Learn the system's coefficients //--- Check if S is invertible if(S.Rank() != 0) { //--- Invert S matrix S_Inv = S.Inv(); Print("S Inverse: ",S_Inv); //--- Obtain psuedo inverse solution b = VT.Transpose().MatMul(S_Inv); b = b.MatMul(U.Transpose()); b = y.MatMul(b); //--- Prepare the current inputs matrix inputs = matrix::Ones(MODEL_INPUTS,1); for(int i=1;i<MODEL_INPUTS;i++) { inputs[i,0] = snapshots[i-1,scenes]; } //--- Done Print("Coefficients:\n",b); Print("Inputs:\n",inputs); current_forecast = b.MatMul(inputs); Print("Forecast:\n",current_forecast[0,0]); //--- The next trade may be expected to be profitable if(current_forecast[0,0] > AccountInfoDouble(ACCOUNT_BALANCE)) { //--- Feedback Print("Next trade expected to be profitable. Checking for trading singals."); //--- Check for our trading signal check_signal(); } //--- Next trade may be expected to be unprofitable else { Print("Next trade expected to be unprofitable. Waiting for better market conditions"); } } //--- S is not invertible! else { //--- Error Print("[Critical Error] Singular values are not invertible."); } }
Das System zeichnet während der Handelssitzungen kontinuierlich Schnappschüsse seines Zustands auf. Mit dieser Methode der Aufzeichnungen bauen wir Anwendungen, die aus ihren Erfahrungen auf dem Markt lernen können.
//+------------------------------------------------------------------+ //| Take a snapshot of the market | //+------------------------------------------------------------------+ void take_snapshot(void) { //--- Record system state snapshots[0,scenes]=iOpen(SYMBOL,SYSTEM_TIME_FRAME,1); //Open snapshots[1,scenes]=iHigh(SYMBOL,SYSTEM_TIME_FRAME,1); //High snapshots[2,scenes]=iLow(SYMBOL,SYSTEM_TIME_FRAME,1); //Low snapshots[3,scenes]=iClose(SYMBOL,SYSTEM_TIME_FRAME,1);//Close snapshots[4,scenes]=ma[0]; //Moving average snapshots[5,scenes]=AccountInfoDouble(ACCOUNT_EQUITY); //Equity snapshots[6,scenes]=AccountInfoDouble(ACCOUNT_BALANCE);//Balance Print("Scene: ",scenes); Print(snapshots); } //+------------------------------------------------------------------+
Beim Beenden des Programms werden alle zuvor definierten Konstanten und Variablen gelöscht.
//+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef SYMBOL #undef SYSTEM_TIME_FRAME #undef MA_APPLIED_PRICE #undef MA_MODE #undef MA_SHIFT #undef MIN_VOLUME #undef MODEL_INPUTS #undef FEATURES #undef OBSERVATIONS //+------------------------------------------------------------------+
Anschließend wählen wir unsere Bewerbungs- und Prüfungstermine aus.

Abbildung 10: Auswahl unseres Expert Advisors für seinen 2-Jahres-Backtest
Der Schlüssel zur Nachahmung des realen Marktes liegt darin, sicherzustellen, dass der Bewertungszeitraum realistische Handelsanforderungen stellt. Dazu gehört auch die Aktivierung von Zufallsverzögerungen im Strategietester von MetaTrader 5, um die Unsicherheit beim Live-Handel zu simulieren.

Abbildung 11: Wählen Sie Backtest-Bedingungen, die reale Marktbedingungen nachahmen
Die Leistungskennzahlen unserer verbesserten Anwendung sprechen für sich selbst. Der Gesamtnettogewinn hat sich mehr als verdoppelt, und die Handelsgenauigkeit stieg auf 72 % und nähert sich damit der 80 %-Marke. Die wichtigsten Leistungskennzahlen – einschließlich der erwarteten Auszahlung, der Sharpe-Ratio und des Erholungsfaktors – verbesserten sich alle gegenüber dem Basismodell.

Abbildung 12: Detaillierte Statistiken über die Leistung unserer Handelsanwendung während des 2-jährigen Testzeitraums
Die Aktienkurve dieses überarbeiteten Systems weist einen gleichmäßigeren, konsistenteren Aufwärtstrend über den gleichen Backtestzeitraum auf. Da dieses Backtest-Fenster nicht Teil der Trainingsdaten war, können wir sicher sein, dass die Verbesserungen echt sind und nicht das Ergebnis eines Informationslecks.

Abbildung 13: Die von unserer Handelsanwendung erzeugte Aktienkurve weist einen starken Aufwärtstrend auf, was wir beobachten möchten
Ein besonders beeindruckendes Ergebnis war schließlich die Vorhersagegenauigkeit des Feedback-Reglers. Am Ende des Backtests wurde ein Endsaldo von 270,28 $ prognostiziert, während das tatsächliche Ergebnis innerhalb von 10 Cent von dieser Schätzung lag. Wie wir bereits in früheren Artikeln erörtert haben, ist diese geringfügige Diskrepanz wahrscheinlich auf den inhärenten Unterschied zwischen der mathematischen Vielfalt der Modellvorhersagen und derjenigen der realen Ergebnisse zurückzuführen – was bedeutet, dass eine perfekte Übereinstimmung theoretisch unmöglich ist. Dennoch bestätigt die Nähe dieses Ergebnisses, dass unser Rahmen für die Rückkopplungskontrolle aussagekräftige Prognosen liefert.

Abbildung 14: Der Feedback-Kontrolleur scheint auch vernünftige Erwartungen zu haben, wie sich die Strategie auf den Kontostand auswirkt
Schlussfolgerung
Nach der Lektüre dieses Artikels lernt der Leser einen neuen Rahmen für den Aufbau von sich selbst anpassenden Handelsanwendungen kennen, die ihr Verhalten in Abhängigkeit von den Ergebnissen ihrer Aktionen steuern. Lineare Regelungsalgorithmen ermöglichen es uns, unerwünschtes Verhalten selbst in einem komplexen nichtlinearen System effizient zu erkennen. Ihr Nutzen für den algorithmischen Handel kann nicht ausgeschöpft werden. Diese Algorithmen scheinen gut geeignet, unsere klassischen Marktmodelle zu verbessern. Darüber hinaus hat dieser Artikel dem Leser gezeigt, wie man ein Ensemble intelligenter Systeme aufbaut, die zusammenarbeiten, um Handelsanwendungen zu entwickeln, die versuchen, gutes Verhalten auf dem Markt zu lernen. Es hat den Anschein, dass die Zeitreihenprognose für sich genommen nur eine Komponente einer größeren Lösung ist.
| Dateiname | Beschreibung der Datei |
|---|---|
| Feedback_Control_Benchmark_3.mq5 | Die MetaTrader 5 Handelsanwendung, die wir entwickelt haben, basiert auf einer Kombination aus überwachtem Lernen und Systemidentifikation. |
| Supervised Linear System Identification.ipynb | Das Jupyter-Notebook, das wir geschrieben haben, um die EURUSD-Marktdaten zu analysieren, die wir mithilfe der Python-Integrationsbibliothek vom Terminal abgerufen haben. |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/20023
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
Vom Neuling zum Experten: Hilfsprogramm zur Parametersteuerung
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Einführung in MQL5 (Teil 24): Erstellen eines EAs, der mit Chart-Objekten handelt
- 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.