Die Grenzen des maschinellen Lernens überwinden (Teil 7): Automatische Strategieauswahl
Wenn die meisten Händler ihre Reise beginnen, wird ihnen geraten, Strategien zu wählen, die ihrem Risikoprofil entsprechen. Dies ist zwar vernünftig, aber dieser Artikel appelliert an den Leser, dass Handelsstrategien zunächst auf der Grundlage der erwarteten Performance und nicht auf der Grundlage idealisierter Präferenzen ermittelt werden sollten. Die Identifizierung profitabler Strategien ist eine universelle Herausforderung für algorithmische Händler, unabhängig von ihrer Erfahrung. Erschwerend kommt hinzu, dass unsere schnell wachsende globale Gemeinschaft von algorithmischen Händlern ständig neue Strategien, Indikatoren und Expert Advisors entwickelt.
Wir leben in einem Zeitalter beispielloser Interkonnektivität – einer informationellen Revolution. Doch was passiert, wenn neue Ideen auftauchen und sich schneller verbreiten, als ein Händler sie bewerten kann? Wie können wir angesichts unzähliger möglicher Strategien automatisch eine Auswahlliste ermitteln, die wir für prüfenswert halten? Können wir potenziell profitable Strategiekonfigurationen entdecken, ohne jede mögliche Kombination zu erzwingen?
In diesem Artikel wird ein Rahmen vorgeschlagen, um diese Fragen durch zwei sich ergänzende Ansätze zu beantworten:
- White Box-Lösung: Verwendung der Matrixfaktorisierung – insbesondere der Singulärwertzerlegung (SVD) – auf die erwarteten Renditen, um Strategiekombinationen zu identifizieren, die von den aktuellen Marktbedingungen positiv beeinflusst werden.
- Black-Box-Lösung: Einsatz von tiefen neuronalen Netzen zur dynamischen Auswahl von Strategien auf der Grundlage des beobachteten Marktverhaltens.
Unsere Lösung beruht auf unserer Fähigkeit, die Renditen zu schätzen, die mit den vorhandenen Handelsstrategien erzielt worden wären. Anschließend nutzen wir unser Wissen über numerische Berechnungen, um die erwarteten Ertragsströme aus unseren Strategien zu ermitteln. Eine Annäherung an die mit einer bestimmten Strategie erzielten Renditen kann wertvolle Erkenntnisse liefern.
Abrufen der benötigten Daten
Um loszulegen, schreiben wir zunächst ein MQL5-Skript, um die wichtigen Marktdaten abzurufen, die wir benötigen. Wir werden gewöhnliche Marktdaten abrufen und auch Daten im Zusammenhang mit den Indikator-Inputs abrufen, um sicherzustellen, dass unsere ONNX-Modelle auf denselben Indikatorberechnungen trainiert werden, die sie in der Produktion beobachten werden.
//+------------------------------------------------------------------+ //| ProjectName | //| Copyright 2020, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs //--- Define our moving average indicator #define MA_PERIOD 5 //--- Moving Average Period #define MA_TYPE MODE_SMA //--- Type of moving average we have #define RSI_PERIOD 15 //--- RSI Period #define STOCH_K 5 //--- Stochastich K Period #define STOCH_D 3 //--- Stochastich D Period #define STOCH_SLOWING 3 //--- Stochastic slowing #define STOCH_MODE MODE_EMA //--- Stochastic mode #define STOCH_PRICE STO_LOWHIGH //--- Stochastic price feeds #define HORIZON 5 //--- Forecast horizon //--- Our handlers for our indicators int ma_handle,ma_o_handle,ma_h_handle,ma_l_handle,rsi_handle,stoch_handle; //--- Data structures to store the readings from our indicators double ma_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[],rsi_reading[],sto_reading_main[],sto_reading_signal[]; //--- File name string file_name = Symbol() + " Market Data As Series Indicators.csv"; //--- Amount of data requested input int size = 3000; //+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { int fetch = size + (HORIZON * 2); //---Setup our technical indicators ma_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_OPEN); ma_h_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_HIGH); ma_l_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_LOW); rsi_handle = iRSI(_Symbol,PERIOD_CURRENT,RSI_PERIOD,PRICE_CLOSE); stoch_handle = iStochastic(_Symbol,PERIOD_CURRENT,STOCH_K,STOCH_D,STOCH_SLOWING,STOCH_MODE,STOCH_PRICE); //---Set the values as series CopyBuffer(ma_handle,0,0,fetch,ma_reading); ArraySetAsSeries(ma_reading,true); CopyBuffer(ma_o_handle,0,0,fetch,ma_o_reading); ArraySetAsSeries(ma_o_reading,true); CopyBuffer(ma_h_handle,0,0,fetch,ma_h_reading); ArraySetAsSeries(ma_h_reading,true); CopyBuffer(ma_l_handle,0,0,fetch,ma_l_reading); ArraySetAsSeries(ma_l_reading,true); CopyBuffer(rsi_handle,0,0,fetch,rsi_reading); ArraySetAsSeries(rsi_reading,true); CopyBuffer(stoch_handle,0,0,fetch,sto_reading_main); ArraySetAsSeries(sto_reading_main,true); CopyBuffer(stoch_handle,0,0,fetch,sto_reading_signal); ArraySetAsSeries(sto_reading_signal,true); //---Write to file int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(int i=size;i>=1;i--) { if(i == size) { FileWrite(file_handle, //--- Time "Time", //--- OHLC "Open", "High", "Low", "Close", //--- MA OHLC "MA O", "MA H", "MA L", "MA C", //--- RSI "RSI", //--- Stochastic Oscilator "Stoch Main", "Stoch Signal" ); } else { FileWrite(file_handle, iTime(_Symbol,PERIOD_CURRENT,i), //--- OHLC iOpen(_Symbol,PERIOD_CURRENT,i), iHigh(_Symbol,PERIOD_CURRENT,i), iLow(_Symbol,PERIOD_CURRENT,i), iClose(_Symbol,PERIOD_CURRENT,i), //--- MA OHLC ma_o_reading[i], ma_h_reading[i], ma_l_reading[i], ma_reading[i], //--- RSI rsi_reading[i], //--- Stochastic Oscilator sto_reading_main[i], sto_reading_signal[i] ); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef HORIZON #undef MA_PERIOD #undef MA_TYPE //+------------------------------------------------------------------+
Die Analyse der benötigten Daten
Als Nächstes importieren wir die Standard-Python-Bibliotheken, die wir benötigen.
#Import the standard libraries import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns
Zunächst laden wir den Datensatz, den wir mit unserem MQL5-Skript erstellt haben.
data = pd.read_csv("../EURUSD Market Data As Series Indicators.csv")
Als Nächstes partitionieren wir den Datensatz, um alle Zeiträume auszuschließen, die sich mit dem geplanten Backtest-Fenster überschneiden. Die Einbeziehung sich überschneidender Daten würde die Gültigkeit unserer Ergebnisse untergraben.
#Drop the last 3 years of historical data data = data.iloc[:-(365*3),:] _ = data.iloc[-(365*3):,:]
Stellen wir sicher, dass der Prognosehorizont mit dem im Skript definierten Prognosezeitraum übereinstimmt.
HORIZON = 5Dann berechnen wir die realisierte Marktrendite.
data['Return'] = data['Close'].shift(-HORIZON) - data['Close'] data.dropna(inplace=True)
Um die Rendite jeder Strategie zu schätzen, müssen wir zunächst die Richtung bestimmen, in die sich der Markt voraussichtlich bewegen wird. Wenn die Strategie eine Hausse vorausgesagt hat, wird eine Rendite von 1 zugewiesen; bei einer Baisse wird -1 zugewiesen. Wir multiplizieren diese erwartete Rendite mit der tatsächlichen Rendite, um die Performance der Strategie zu ermitteln. Die Rendite wird nur dann positiv sein, wenn die Strategie die Marktrichtung richtig vorweggenommen hat.
data['MA OC Strategy'] = 0 data['MA HL Strategy'] = 0 data['RSI Strategy'] = 0 data['Stochastic Strategy'] = 0 #Moving Average Open and Close strategy data.loc[data['MA O']<data['MA C'],'MA OC Strategy'] = 1 data.loc[data['MA O']>data['MA C'],'MA OC Strategy'] = -1 #Moving average High Low Strategy data.loc[data['Close']>data['MA H'],'MA HL Strategy'] = 1 data.loc[data['Close']<data['MA L'],'MA HL Strategy'] = -1 #RSI Strategy data.loc[data['RSI']>50,'RSI Strategy'] = 1 data.loc[data['RSI']<50,'RSI Strategy'] = -1 #Stoch Main Strategy data.loc[data['Stoch Main']>80,'Stochastic Strategy'] = 1 data.loc[data['Stoch Main']<30,'Stochastic Strategy'] = -1 #Strategy Returns for i in np.arange(4): data.iloc[:,-1*(i+1)]= data.iloc[:,-1*(i+1)] * data['Return'] data.iloc[:,-1*(i+1)]= data.iloc[:,-1*(i+1)].cumsum() data['Return'] = data['Return'].cumsum()
Wir wollen auch untersuchen, wie sich die Marktrenditen über mehrere Zeitschritte hinweg entwickeln.
data['MA OC 1'] = data['MA OC Strategy'].shift(-1) data['MA OC 2'] = data['MA OC Strategy'].shift(-HORIZON) data['MA HL 1'] = data['MA HL Strategy'].shift(-1) data['MA HL 2'] = data['MA HL Strategy'].shift(-HORIZON) data['RSI 1'] = data['RSI Strategy'].shift(-1) data['RSI 2'] = data['RSI Strategy'].shift(-HORIZON) data['Stochastic 1'] = data['Stochastic Strategy'].shift(-1) data['Stochastic 2'] = data['Stochastic Strategy'].shift(-HORIZON) data.dropna(inplace=True) data
Trennen wir nun die Eingaben von den Zielen.
X = data.iloc[:,1:12] y = data.iloc[:,-8:]
Als Nächstes wollen wir uns die erwarteten Renditen der Strategie ansehen. Wie in Abbildung 1 dargestellt, scheinen alle vier Strategien von Anfang an unrentabel zu sein. Dennoch sind diese Informationen wertvoll.
plt.plot(data.iloc[:,-12:-8]) plt.legend(data.columns[-12:-8]) plt.grid() plt.title('Estimating The Effectiveness of Different Strategies') plt.ylabel('Estimated Profit Level') plt.xlabel('Historical Training Epochs')

Abbildung 1: Visualisierung der Renditen unserer unabhängigen Strategien in ihrer jetzigen Form
Wir werden nun eine Singulärwertzerlegung (SVD) für die Strategieerträge durchführen.
#Analyze the returns U,S,VT = np.linalg.svd(data.iloc[:,-12:-8])
Die SVD offenbart die den Daten zugrunde liegende Struktur, und für diese Diskussion sind wir besonders an der Anzahl der einzigartigen Variationsmodi der Strategien interessiert. Jede Art der Variation spiegelt ein bestimmtes Verhaltensmuster wider, das der Markt annehmen kann.
Im Wesentlichen liefert SVD eine Reihe unabhängiger Kombinationen von Strategierenditen, von denen jede die Portfoliorendite bei einem bestimmten Marktverhalten maximiert. Im Allgemeinen sind wir an der kleinsten Anzahl von dominanten Modi interessiert, die mindestens 80 % der Gesamtvariation ausmachen.
Die S-Matrix (Sigma) aus der SVD-Funktion von Numpy enthält die Singulärwerte. Diese geben an, wie viel Variation jede Hauptkomponente erklärt. In Abbildung 2 ist die kumulative Summe der Singulärwerte, skaliert durch ihre L1-Norm, dargestellt. Die Darstellung zeigt, dass die ersten beiden Singulärwerte mehr als 80 % der Gesamtvariation ausmachen, was bedeutet, dass die ersten beiden Hauptkomponenten dominieren.
#Standardize and scale the singular values sigma_scaled = S / np.linalg.norm(S,1) sns.barplot(np.cumsum(sigma_scaled),color='black') plt.axhline(0.8,linestyle='--',color='red') plt.title('Number of Singular Values Needed To Capture 80% of Variance') plt.ylabel('Proportion of Variance Explained') plt.xticks([0,1,2,3],['First Total','Second Total','Third Total','Total']) plt.xlabel('Number of Singular Values Needed To Recreate The Original Dataset')

Abbildung 2: Wir benötigen nur die ersten 2 Hauptkomponenten, um 80% der im Datensatz beobachteten Variation zu erfassen
Wir können auch die Korrelationen zwischen den Renditen der Strategien untersuchen. Vor allem der gleitende Durchschnitt und der RSI weisen eine starke positive Korrelation auf, die möglicherweise nutzbare Erkenntnisse liefert.
data.iloc[:,-12:-8].corr()

Abbildung 3: Visualisierung der Korrelationsmatrix der uns zur Verfügung stehenden Marktdaten
Nachdem wir die dominierenden Hauptkomponenten ermittelt haben, müssen wir noch bestimmen, welche Strategien positiv zu den einzelnen Komponenten beitragen. Diese Beiträge werden als Hauptkomponentenladungen bezeichnet. Wir konzentrieren uns auf Strategien mit positiven Ladungen auf den dominanten Komponenten, da sie voraussichtlich gut abschneiden werden, wenn der Markt das entsprechende Verhalten zeigt.
VT
array([[ 0.64587337, 0.37029478, 0.63092801, 0.21830991],
[ 0.10444288, -0.33948578, 0.38679319, -0.85101828],
[ 0.64575265, 0.19765237, -0.67157641, -0.30483139],
[ 0.39362773, -0.84176287, -0.03613857, 0.36767716]])
Schließlich werden in Abbildung 4 die mit diesen ausgewählten Kombinationen erzielten Strategieerträge dargestellt.
plt.plot(data.iloc[:,13]+data.iloc[:,14]+data.iloc[:,15]+data.iloc[:,16],color='red') plt.plot(data.iloc[:,13]+data.iloc[:,15],color='Orange') plt.plot(data.iloc[:,13]+data.iloc[:,14],color='Green') plt.plot(data.iloc[:,13]+data.iloc[:,16],color='Blue') plt.legend(['High Risk','Medium Risk','Low Risk','Minimal Risk']) plt.grid() plt.title('Estimating The Returns Produced by Each of Our Risk Settings') plt.ylabel('Estimated Profit') plt.xlabel('Historical Epochs')

Abbildung 4: Visualisierung der neuen Rückgabeströme, die uns durch die SVD-Faktorisierung vorgeschlagen werden
Die Umsetzung unserer Strategie in MQL5
Wir sind nun bereit, unsere Handelsanwendung in MQL5 zu implementieren. Wie in unseren Artikeln üblich, beginnen wir mit der Definition von Systemkonstanten, um sicherzustellen, dass sich die Anwendung konsistent zu den Erwartungen verhält, die in der Modellierungsphase festgelegt wurden. //+------------------------------------------------------------------+ //| Automatic Strategy Selection.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System definiyions | //+------------------------------------------------------------------+ #define MA_PERIOD 5 //--- Moving Average Period #define MA_TYPE MODE_SMA //--- Type of moving average #define RSI_PERIOD 15 //--- RSI Period #define STOCH_K 5 //--- Stochastich K Period #define STOCH_D 3 //--- Stochastich D Period #define STOCH_SLOWING 3 //--- Stochastic slowing #define STOCH_MODE MODE_EMA //--- Stochastic mode #define STOCH_PRICE STO_LOWHIGH //--- Stochastic price feeds #define TOTAL_STRATEGIES 4 //--- Total strategies we have to choose from
Als Nächstes laden wir die Handelsbibliothek, um die Marktpositionen zu verwalten und die globalen Variablen zu definieren, die während des gesamten Lebenszyklus der Anwendung verwendet werden.
//+------------------------------------------------------------------+ //| System libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade;
Zunächst legen wir Variablen für unsere technischen Indikatoren und deren Outputs fest. Dann definieren wir Mql-Objekte, um Zeit- und Tickdaten zu speichern. Schließlich deklarieren wir Arrays für die Gewichtung der Hauptkomponenten – erinnern Sie sich daran, dass Strategien mit positiven Reaktionen auf identifizierte Modi ein Gewicht von 1 zugewiesen wurde und andere ein Gewicht von 0.
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ma_c_handle,ma_o_handle,ma_h_handle,ma_l_handle,rsi_handle,stoch_handle,atr_handle; double ma_c_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[],rsi_reading[],sto_reading_main[],sto_reading_signal[],atr_reading[]; double long_vote,short_vote; MqlDateTime ts,tc; MqlTick current_tick; double const weights_1 [] = {1,1,1,1}; double const weights_2 [] = {1,0,1,0}; double const weights_3 [] = {1,1,0,0}; double const weights_4 [] = {1,0,0,1}; double selected_weights[] = {0,0,0,0};
Anschließend definieren wir eine nutzerdefinierte Enumeration, mit der der Nutzer auswählen kann, in welchem Modus die Anwendung arbeiten soll. Da wir neue Strategien erforschen wollen, ist es einfacher, die vier von SVD vorgeschlagenen Strategien zu testen, als jede mögliche Kombination manuell zu bewerten.
//+------------------------------------------------------------------+ //| Custom enumrations | //+------------------------------------------------------------------+ enum operation_modes { HIGH=0, //High Risk MID=1, //Medium Risk LOW=2, //Low Risk MINIMUM=3 //Minimum Risk };
Wir definieren auch einen Eingabeparameter, mit dem wir über diese vier Strategiekonfigurationen hinweggehen können.
//+------------------------------------------------------------------+ //| User inputs | //+------------------------------------------------------------------+ input group "User Risk Settings" input operation_modes user_mode = 1;//Define Your Risk Settings
Beim Start der Anwendung werden die vom Nutzer ausgewählten Gewichte mit der Anweisung „switch“ in das (mit Nullen initialisierte) Array der Gewichte geladen. Wir stellen dann die zeitlichen und technischen Indikatoren entsprechend ein.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Setup our risk settings switch(user_mode) { case(0): Print("High risk mode selected"); ArrayCopy(selected_weights,weights_1,0,0,WHOLE_ARRAY); break; case(1): Print("Medium risk mode selected"); ArrayCopy(selected_weights,weights_2,0,0,WHOLE_ARRAY); break; case(2): Print("Low risk mode selected"); ArrayCopy(selected_weights,weights_3,0,0,WHOLE_ARRAY); break; case(3): Print("Minimum risk mode selected"); ArrayCopy(selected_weights,weights_4,0,0,WHOLE_ARRAY); break; default: Print("No risk mode selected! No Trades will be placed"); break; } //--- Setup the time TimeLocal(tc); TimeLocal(ts); //---Setup our technical indicators ma_c_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_OPEN); ma_h_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_HIGH); ma_l_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_LOW); atr_handle = iATR(_Symbol,PERIOD_CURRENT,14); rsi_handle = iRSI(_Symbol,PERIOD_CURRENT,RSI_PERIOD,PRICE_CLOSE); stoch_handle = iStochastic(_Symbol,PERIOD_CURRENT,STOCH_K,STOCH_D,STOCH_SLOWING,STOCH_MODE,STOCH_PRICE); //--- return(INIT_SUCCEEDED); }
Wenn die Anwendung nicht mehr genutzt wird, geben wir alle von uns zugewiesenen technischen Indikatoren frei.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(ma_c_handle); IndicatorRelease(ma_o_handle); IndicatorRelease(ma_h_handle); IndicatorRelease(ma_l_handle); IndicatorRelease(rsi_handle); IndicatorRelease(stoch_handle); IndicatorRelease(atr_handle); }
Wenn neue Kursdaten eintreffen und ein neuer Tag beginnt, aktualisieren wir die Zeit- und Indikatorwerte. Wenn keine Positionen offen sind, führt das System eine Abstimmung unter allen Strategien mit einer Gewichtung von 1 durch. Die Mehrheit entscheidet, ob gekauft oder verkauft wird.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- TimeLocal(ts); if(ts.day != tc.day) { //--- Update the time TimeLocal(tc); //--- Update Our indicator readings CopyBuffer(ma_c_handle,0,0,1,ma_c_reading); CopyBuffer(ma_o_handle,0,0,1,ma_o_reading); CopyBuffer(ma_h_handle,0,0,1,ma_h_reading); CopyBuffer(ma_l_handle,0,0,1,ma_l_reading); CopyBuffer(rsi_handle,0,0,1,rsi_reading); CopyBuffer(stoch_handle,0,0,1,sto_reading_main); CopyBuffer(stoch_handle,0,0,1,sto_reading_signal); CopyBuffer(atr_handle,0,0,1,atr_reading); //--- Copy Market Data double close = iClose(Symbol(),PERIOD_CURRENT,0); SymbolInfoTick(Symbol(),current_tick); //--- Place a position if(PositionsTotal() ==0) { //--- Our strategies will vote on what should be done long_vote = 0; short_vote = 0; for(int i =0; i<TOTAL_STRATEGIES;i++) { //--- Is the strategy's vote valid? if(selected_weights[i] > 0) { //--- Moving average open close strategy if(i == 0) { if(ma_o_reading[0] > ma_c_reading[0]) long_vote += selected_weights[0]; else if(ma_o_reading[0] < ma_c_reading[0]) short_vote += selected_weights[0]; } //--- Moving average high low strategy if(i == 1) { if(close > ma_h_reading[0]) long_vote += selected_weights[1]; else if(close < ma_l_reading[0]) short_vote += selected_weights[1]; } //--- RSI Strategy if(i == 2) { if(rsi_reading[0] > 50) long_vote += selected_weights[2]; else if(rsi_reading[0] < 50) short_vote += selected_weights[2]; //--- Stochastic Strategy if(i == 3) { if(sto_reading_main[0] > 50) long_vote += selected_weights[3]; else if(sto_reading_main[0] < 50) short_vote += selected_weights[3]; } } } } if(long_vote > short_vote) Trade.Buy(0.01,Symbol(),current_tick.ask,current_tick.ask-(1.5*atr_reading[0]),current_tick.ask+(1.5*atr_reading[0])); if(long_vote < short_vote) Trade.Sell(0.01,Symbol(),current_tick.bid,current_tick.bid+(1.5*atr_reading[0]),current_tick.bid-(1.5*atr_reading[0])); } } } //+------------------------------------------------------------------+
Am Ende der Anwendung werden alle Systemkonstanten, die nicht mehr benötigt werden, zurückgesetzt.
//+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef MA_PERIOD #undef MA_TYPE #undef RSI_PERIOD #undef STOCH_K #undef STOCH_D #undef STOCH_SLOWING #undef STOCH_MODE #undef STOCH_PRICE #undef TOTAL_STRATEGIES //+------------------------------------------------------------------+
Analyse unserer Ergebnisse
Wir können nun die Ergebnisse unserer Anwendung analysieren. Zunächst wählen wir Datumsbereiche außerhalb der für die Modellentwicklung verwendeten historischen Daten aus.

Abbildung 5: Auswahl der Backtest-Tage für unsere Basisversion der Handelsanwendung
Als Nächstes konfigurieren wir die Modellierung so, dass echte Ticks mit zufälliger Verzögerung verwendet werden, um reale Marktbedingungen zu simulieren.

Abbildung 6: Durch die Auswahl der Einstellung „Zufällige Verzögerung“ wird sichergestellt, dass unser Backtest reale Marktbedingungen nachbildet.
Dann geben wir die Eingaben an, die durchsucht werden sollen. Da wir nur vier Kandidatenstrategien haben, lassen wir den genetischen Optimierer eine Zeilensuche über diese vier Eingaben durchführen.

Abbildung 7: Nach der Auswahl des Minimal- und Maximalwertes lassen wir den genetischen Optimierer laufen
In Abbildung 8 sehen wir, dass die ersten beiden von SVD vorgeschlagenen Strategien profitabel waren, während die beiden anderen unzuverlässig waren. Aus Abbildung 2 geht hervor, dass diese beiden Hauptkomponenten über 80 % der Variation in den Trainingsdaten ausmachen. Dies deutet darauf hin, dass der Markt von zwei stabilen Verhaltensmustern bestimmt wird, während die anderen schwach und instabil sind.

Abbildung 8: Analyse der Ergebnisse aus unserem historischen Backtest der Marktdaten
Verbesserung unserer Ergebnisse
Wenden wir uns nun der zweiten, der Blackbox-Lösung zu. Mit diesem Ansatz wird versucht, die optimale Strategie auf der Grundlage der aktuellen Marktbedingungen zu ermitteln.
import onnx from sklearn.linear_model import Ridge from sklearn.neural_network import MLPRegressor from skl2onnx.convert import convert_sklearn from skl2onnx.common.data_types import FloatTensorType from sklearn.model_selection import RandomizedSearchCV,TimeSeriesSplit
Wir beginnen mit dem Laden der erforderlichen Bibliotheken und der Definition eines nutzerdefinierten Zeitreihenvalidierungsobjekts, um beim Training tiefer neuronaler Netze sorgfältig eine Kreuzvalidierung durchzuführen.
tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)Als Nächstes geben wir die Parameter des neuronalen Netzes an, die durchsucht werden sollen. Da der Parameterraum groß ist, untersuchen wir nur eine Teilmenge, von der wir erwarten, dass sie eine mögliche Lösung enthält. Dies macht die Konfiguration schwieriger als bei unserem White-Box-Ansatz.
dist = {
'max_iter':[10,50,100,500,1000,5000,10000,50000,100000],
'activation':['tanh','relu','identity','logistic'],
'alpha':[10e0,10e-1,10e-2,10e-3,10e-4,10-5,10e-6],
'solver':['lbfgs','adam','sgd'],
'learning_rate':['constant','invscaling','adaptive'],
'hidden_layer_sizes':[(11,1),(11,11),(11,11,11),(11,11,11,11),(11,22,33,44),(11,22,55,22,11),(11,100,11),(11,5,2,5,11),(11,3,9,18,9,3)]
}Anschließend legen wir die grundlegenden Parameter des neuronalen Netzes fest, die in allen Experimenten gleich bleiben.
model = MLPRegressor(shuffle=False,early_stopping=False,random_state=0,verbose=True)
Wenn die Konfiguration abgeschlossen ist, suchen wir nach den optimalen Parametern. Beachten Sie die Bedeutung des Parameters „n_iter“ bei der randomisierten Suche – mehr Iterationen verbessern normalerweise die Suchqualität.
rscv = RandomizedSearchCV(model,dist,random_state=0,n_iter=20,scoring='neg_mean_squared_error',cv=tscv,n_jobs=-1,refit=True)
Beginnen Sie die Suche.
res = rscv.fit(X,y)
Iteration 1, loss = 0.21844802
Iteration 2, loss = 0.13287107
Iteration 3, loss = 0.08159530
Iteration 4, loss = 0.07053761
Iteration 5, loss = 0.07051259
Sobald die Suche abgeschlossen ist, wird das beste Modell im Attribut „best_estimator_“ gespeichert.
res.best_estimator_

Abbildung 9: Das optimale neuronale Netz, das durch ein randomisiertes Suchverfahren während der für unsere Diskussion zulässigen Iterationen gefunden wurde
Vor dem Export in ONNX definieren wir die Eingabe- und Ausgabeform unseres neuronalen Netzes.
initial_types = [('float_input',FloatTensorType([1,X.shape[1]]))] final_types = [('float_output',FloatTensorType([y.shape[1],1]))]
Anschließend speichern wir das Modell als ONNX-Prototyp.
onnx_proto = convert_sklearn(model=res.best_estimator_,initial_types=initial_types,final_types=final_types,target_opset=12)Wir schreiben das ONNX-Modell mit der Erweiterung „.onnx“ auf die Festplatte.
onnx.save(onnx_proto,'Unsupervised Strategy Selection MLP.onnx')
Die Umsetzung unserer Verbesserungen
Wir laden nun das ONNX-Modell.
//+------------------------------------------------------------------+ //| System resources | //+------------------------------------------------------------------+ #resource "\\Files\\USS\\Unsupervised Strategy Selection MLP.onnx" as const uchar onnx_buffer[];
Wir definieren die Eingangs- und Ausgangsformen des ONNX-Modells.
#define ONNX_INPUTS 11 //--- Total inputs needed by our ONNX model #define ONNX_OUTPUTS 8 //--- Total outputs needed by our ONNX model
Einige zusätzliche globale Variablen sind erforderlich, um die Eingaben und Ausgaben des Modells zu verwalten.
long onnx_model; vectorf onnx_features,onnx_targets;
Während der Anwendungsinitialisierung laden wir das ONNX-Modell aus dem Puffer und setzen seine Eingabe- und Ausgabeformen wie in Python definiert. Nachdem wir diese überprüft haben und sichergestellt haben, dass das Modell gültig ist, fahren wir fort.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Prepare the model's inputs and outputs onnx_features = vectorf::Zeros(ONNX_INPUTS); onnx_targets = vectorf::Zeros(ONNX_OUTPUTS); //--- Create the ONNX model onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DATA_TYPE_FLOAT); //--- Define the I/O shape ulong input_shape[] = {1,ONNX_INPUTS}; ulong output_shape[] = {ONNX_OUTPUTS,1}; if(!OnnxSetInputShape(onnx_model,0,input_shape)) { Print("Failed to define ONNX input shape"); return(INIT_FAILED); } if(!OnnxSetOutputShape(onnx_model,0,output_shape)) { Print("Failed to define ONNX output shape"); return(INIT_FAILED); } //--- Check if the model is valid if(onnx_model == INVALID_HANDLE) { Print("Failed to create our ONNX model from buffer"); return(INIT_FAILED); } //--- Setup the time TimeLocal(tc); TimeLocal(ts); //---Setup our technical indicators ma_c_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_OPEN); ma_h_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_HIGH); ma_l_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_LOW); atr_handle = iATR(_Symbol,PERIOD_CURRENT,14); rsi_handle = iRSI(_Symbol,PERIOD_CURRENT,RSI_PERIOD,PRICE_CLOSE); stoch_handle = iStochastic(_Symbol,PERIOD_CURRENT,STOCH_K,STOCH_D,STOCH_SLOWING,STOCH_MODE,STOCH_PRICE); //--- return(INIT_SUCCEEDED); }
Wenn das ONNX-Modell nicht mehr benötigt wird, geben wir es frei und setzen seine Ressourcen frei.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- OnnxRelease(onnx_model); }
Wenn wir neue Preisniveaus erhalten, aktualisieren wir unsere Indikatorpuffer und setzen alle Eingaben auf float, wie von ONNX gefordert. Das Modell sagt die erwartete kumulierte Rendite zwei Schritte im Voraus voraus, sodass wir prüfen, ob die Steigung der kumulierten Bilanz positiv ist. Ist dies der Fall, platzieren wir Handelsgeschäfte, die auf die Strategie mit der größten, erwarteten Steigung ausgerichtet sind.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- TimeLocal(ts); if(ts.day != tc.day) { //--- Update the time TimeLocal(tc); //--- Update Our indicator readings CopyBuffer(ma_c_handle,0,0,1,ma_c_reading); CopyBuffer(ma_o_handle,0,0,1,ma_o_reading); CopyBuffer(ma_h_handle,0,0,1,ma_h_reading); CopyBuffer(ma_l_handle,0,0,1,ma_l_reading); CopyBuffer(rsi_handle,0,0,1,rsi_reading); CopyBuffer(stoch_handle,0,0,1,sto_reading_main); CopyBuffer(stoch_handle,0,0,1,sto_reading_signal); CopyBuffer(atr_handle,0,0,1,atr_reading); //--- Set our model inputs onnx_features[0] = (float) iOpen(Symbol(),PERIOD_CURRENT,0); onnx_features[1] = (float) iHigh(Symbol(),PERIOD_CURRENT,0); onnx_features[2] = (float) iLow(Symbol(),PERIOD_CURRENT,0); onnx_features[3] = (float) iClose(Symbol(),PERIOD_CURRENT,0); onnx_features[4] = (float) ma_o_reading[0]; onnx_features[5] = (float) ma_h_reading[0]; onnx_features[6] = (float) ma_l_reading[0]; onnx_features[7] = (float) ma_c_reading[0]; onnx_features[8] = (float) rsi_reading[0]; onnx_features[9] = (float) sto_reading_main[0]; onnx_features[10] = (float) sto_reading_signal[0]; //--- Copy Market Data double close = iClose(Symbol(),PERIOD_CURRENT,0); SymbolInfoTick(Symbol(),current_tick); //--- Place a position if(PositionsTotal() ==0) { if(OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_features,onnx_targets)) { Comment("Onnx Model Prediction: \n",onnx_targets); //--- Store our result vectorf res = {onnx_targets[1]-onnx_targets[0],onnx_targets[3]-onnx_targets[2],onnx_targets[5]-onnx_targets[4],onnx_targets[7]-onnx_targets[6]}; if(res.Max() > 0) { Print("Trading oppurtunity found"); Print(res); if(res.ArgMax()==0) { if(ma_o_reading[0]<ma_c_reading[0]) Buy(); if(ma_o_reading[0]>ma_c_reading[0]) Sell(); } if(res.ArgMax()==1) { if(close>ma_h_reading[0]) Buy(); if(close<ma_l_reading[0]) Sell(); } if(res.ArgMax()==2) { if(rsi_reading[0]>50) Buy(); if(rsi_reading[0]<50) Sell(); } if(res.ArgMax()==3) { if(sto_reading_main[0]>50) Buy(); if(sto_reading_main[0]<50) Sell(); } } else { Print("No trading oppurtunities expected."); } } } } } //+------------------------------------------------------------------+
Schließlich definieren wir aus Gründen der Wartungsfreundlichkeit separate Methoden für die Eingabe von Kauf- und Verkaufspositionen, um Wiederholungen in unserem Code zu vermeiden.
//+------------------------------------------------------------------+ //| Enter a long position | //+------------------------------------------------------------------+ void Buy(void) { Trade.Buy(0.01,Symbol(),current_tick.ask,current_tick.ask-(1.5*atr_reading[0]),current_tick.ask+(1.5*atr_reading[0])); } //+------------------------------------------------------------------+ //| Enter a short position | //+------------------------------------------------------------------+ void Sell(void) { Trade.Sell(0.01,Symbol(),current_tick.bid,current_tick.bid+(1.5*atr_reading[0]),current_tick.bid-(1.5*atr_reading[0])); } //+------------------------------------------------------------------+
Wir sind nun bereit, die Blackbox-Version unserer Handelsanwendung einem Backtest zu unterziehen.

Abbildung 10: Wir wählen die richtige Version der Anwendung für unseren zweiten Test
Beginnen wir damit, die richtige Version der Anwendung auszuwählen und die entsprechenden Testdaten zu laden – diese müssen mit dem Rest unserer Diskussion übereinstimmen.

Abbildung 11: Vergewissern Sie sich, dass Sie die richtigen Backtest-Daten für unseren Test ausgewählt haben.
Die Kapitalkurve zeigt den von uns erwarteten positiven Kontostandstrend. Es sei jedoch daran erinnert, dass die Strategie automatisch ausgewählt wurde – das tiefe neuronale Netz wählte die einzige Strategie, die es für optimal hielt.

Abbildung 12: Die von der von uns verfolgten Handelsstrategie erzeugte Kapitalkurve deutet darauf hin, dass unsere Blackbox-Lösung die gestellte Aufgabe verstanden hat
Ein Blick auf die Leistungsstatistiken zeigt, dass der Black-Box-Ansatz im Vergleich zur White-Box-Lösung unterdurchschnittlich abschneidet. Dieses Ergebnis war zu erwarten, da das Blackbox-Setup zeitaufwändiger war und wahrscheinlich mit einer größeren Anzahl von Suchiterationen verbessert werden könnte.

Abbildung 14: Die detaillierten Ergebnisse unserer Blackbox-Lösung
Schlussfolgerung
Insgesamt wurde in diesem Artikel gezeigt, wie man mit Hilfe des MetaTrader 5-Toolkits automatisch Handelsstrategien identifizieren kann. Wir haben gezeigt, wie ein Computer schnell Strategien aufdecken kann, die dem Menschen sonst entgehen würden – die Daten offenbaren Muster, ob wir sie sehen oder nicht. In unserer Diskussion wurden die Vorteile von White-Box-Lösungen hervorgehoben, die auf unüberwachter Matrixfaktorisierung beruhen: Sie erfordern weniger Zeit für die Konfiguration, bieten eine klarere Interpretierbarkeit und geben explizite Hinweise darauf, welche Strategien beibehalten werden sollen, was letztendlich Zeit spart und einen zusätzlichen diagnostischen Wert darstellt. Im Gegensatz dazu werden Black-Box-Lösungen unter komplexen Marktbedingungen wertvoller, wo White-Box-Ansätze möglicherweise nicht ausreichen.
| Dateiname | Beschreibung der Datei |
|---|---|
| Automatic Strategy Selection Baseline.mq5 | Unsere White-Box-Lösung mit 4 einzigartigen Strategien, die durch die SVD-Faktorisierung erzeugt werden. |
| Automatic Strategy Selection.mq5 | Unsere Blackbox-Lösung, generiert durch unser tiefes neuronales Netzwerk des EURUSD-Marktes. |
| Fetch Data Indicators.mq5 | Das MQL5-Skript, das wir erstellt haben, um die benötigten Marktdaten abzurufen und unsere Analyse der Strategierenditen zu beginnen. |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/20256
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.
Entwicklung einer Handelsstrategie: Der Flower-Volatilitäts-Index als Trendfolgemethode
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Aufbau von KI-gestützten Handelssystemen in MQL5 (Teil 6): Einführung der Chat-Lösch- und Suchfunktionalität
- 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.