English 日本語
preview
Selbstoptimierende Expert Advisors in MQL5 (Teil 16): Überwachte lineare Systemidentifikation

Selbstoptimierende Expert Advisors in MQL5 (Teil 16): Überwachte lineare Systemidentifikation

MetaTrader 5Beispiele |
25 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

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:

  1. Der Nettogewinn stieg von 56 US-Dollar im Basissystem auf 170 US-Dollar – eine Verbesserung um fast 200 %.
  2. Der Bruttoverlust sank von 333 $ auf 143 $, was einer Verringerung des Verlustrisikos um 57 % entspricht.
  3. Die Genauigkeit verbesserte sich von 52,9 % auf 72 %, was einer Steigerung der Präzision um 37 % entspricht.
  4. 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.
  5. 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.

DateinameBeschreibung 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.ipynbDas 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

Die Übertragung der Trading-Signale in einem universalen Expert Advisor. Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
In diesem Artikel wurden die verschiedenen Möglichkeiten beschrieben, um die Trading-Signale von einem Signalmodul des universalen EAs zum Steuermodul der Positionen und Orders zu übertragen. Es wurden die seriellen und parallelen Interfaces betrachtet.
Vom Neuling zum Experten: Hilfsprogramm zur Parametersteuerung Vom Neuling zum Experten: Hilfsprogramm zur Parametersteuerung
Stellen Sie sich vor, Sie verwandeln die traditionellen EA- oder Indikator-Eingabeeigenschaften in eine Echtzeit-Kontrollschnittstelle auf dem Chart. Diese Diskussion baut auf unserer grundlegenden Arbeit am Market Period Synchronizer-Indikator auf und stellt eine bedeutende Entwicklung in der Art und Weise dar, wie wir Higher-Timeframe (HTF)-Marktstrukturen visualisieren und verwalten. Hier setzen wir dieses Konzept in ein vollständig interaktives Hilfsprogramm um – ein Dashboard, das eine dynamische Steuerung und eine verbesserte Visualisierung von mehrperiodigen Preisaktionen direkt auf dem Chart ermöglicht. Erkunden Sie mit uns, wie diese Innovation die Art und Weise, wie Händler mit ihren Tools interagieren, neu gestaltet.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Einführung in MQL5 (Teil 24): Erstellen eines EAs, der mit Chart-Objekten handelt Einführung in MQL5 (Teil 24): Erstellen eines EAs, der mit Chart-Objekten handelt
In diesem Artikel erfahren Sie, wie Sie einen Expert Advisor erstellen, der auf dem Chart eingezeichnete Unterstützungs- und Widerstandszonen erkennt und darauf basierend automatisch Handelsgeschäfte ausführt.