English 日本語
preview
Klassische Strategien neu interpretieren (Teil 14): Analyse mehrerer Strategien

Klassische Strategien neu interpretieren (Teil 14): Analyse mehrerer Strategien

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

In unserer vorangegangenen Diskussion aus unserer Artikelserie über selbstoptimierende Expert Advisors haben wir uns der Herausforderung gestellt, ein Ensemble aus mehreren Strategien zu erstellen und sie zu einer einzigen, leistungsfähigeren Strategie zu verschmelzen, als wir ursprünglich hatten.

Wir beschlossen, unsere Strategien demokratisch zusammenarbeiten zu lassen, bei der jede Strategie eine einzige Stimme haben sollte. Das Gewicht jeder Stimme wurde zu einem Tuning-Parameter, den wir, wie bereits erwähnt, vom genetischen Optimierer so einstellen ließen, dass die Rentabilität unserer Handelsstrategie maximiert wurde. Wir haben dann die Strategie eliminiert, der der genetische Optimierer das geringste Gewicht zugewiesen hat, sodass die beiden Strategien übrig blieben, die wir nun analysieren und um die herum wir statistische Modelle erstellen werden.

In dieser Diskussion haben wir die Marktdaten mit Hilfe eines MQL5-Skripts extrahiert, basierend auf den besten Ergebnissen, die unser genetischer Optimierer ermittelt hat. Denken Sie daran, dass wir die Ergebnisse ausgewählt haben, die sowohl im Backtest als auch im Vorwärtstest stabil waren, und dies als Entscheidungsfaktor verwendet haben.

Bei näherer Betrachtung der Renditen, die durch die beiden vom genetischen Optimierer ausgewählten Strategien erzielt wurden, stellten wir jedoch fest, dass die Strategien stark miteinander korreliert waren. Mit anderen Worten, beide Strategien neigten dazu, zu etwa denselben Zeitpunkten zu gewinnen und zu verlieren. Zwei stark korrelierte Handelsstrategien sind nicht besser als nur eine Strategie, und mit nur einer Strategie wird der gesamte Zweck der Analyse mehrerer Strategien verfehlt.

Bei dem Versuch, künstliche Intelligenz zur Entwicklung von Handelsstrategien einzusetzen, können viele Dinge schief gehen, und es scheint, dass der genetische Optimierer den von uns vorgegebenen Rahmen ausgenutzt und die am stärksten korrelierten Strategien ausgewählt hat. Aus rein mathematischer Sicht ist dies ein kluger Schachzug: Es wird für den genetischen Optimierer einfacher, den Gesamtsaldo des Kontos zu antizipieren, wenn die dominanten Strategien korreliert sind.

Ursprünglich hatte ich erwartet, dass der genetische Optimierer den profitabelsten Strategien eine höhere und den weniger profitablen eine geringere Gewichtung zuweisen würde. Da wir jedoch nur 3 Strategien zur Auswahl hatten und dieses Optimierungsverfahren nur einmal durchgeführt wurde, können wir nicht ausschließen, dass dies alles zufällig geschehen ist. Das heißt, wenn wir die Optimierung der Stimmgewichte mit einem langsamen und vollständigen Optimierungsalgorithmus wiederholen würden, dann hätte unser Optimierer vielleicht keine korrelierten Strategien ausgewählt. 

Diese Erkenntnis hat mich dazu veranlasst, den Ansatz, den wir zur Auswahl der optimalen Einstellungen für unsere Strategien verwenden, zu überarbeiten. Es scheint, dass wir zunächst alle Gewichte der einzelnen Stimmen auf eins festlegen sollten. Dies zwingt den genetischen Optimierer, sich ausschließlich darauf zu konzentrieren, die profitabelsten Einstellungen für jeden von uns verwendeten Indikator zu finden. Wie wir auf unserer gemeinsamen Reise sehen werden, erweist sich dieser überarbeitete Ansatz als besser als unser ursprünglicher Plan. Wenn zwei korrelierte Strategien für die Analyse mehrerer Strategien verwendet werden, werden keine wirklichen Fortschritte erzielt. Daher haben wir gelernt, das objektive Problem der Analyse mehrerer Strategien besser zu formulieren: „Wie können wir am besten mehrere Strategien auswählen, die unkorrelierte Renditen erzielen und die Rentabilität unseres Kontos maximieren?“. 



Erste Schritte in MQL5

Zunächst schreiben wir ein Skript, um historische Marktdaten mit den Einstellungen abzurufen, mit denen wir bei unserem vorherigen Test, bei dem wir die beiden bisher verwendeten Strategien ausgewählt haben, unsere Rendite maximiert haben. Unser System stützt sich auf einige feste Parameter, die wir aus den zuvor besprochenen Tests zur genetischen Optimierung gelernt haben. Diese Parameter unserer Traumstrategie bleiben fest, während wir die Daten abrufen.

//+------------------------------------------------------------------+
//|                                                      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  100               //--- Period for our moving average
#define MA_TYPE    MODE_EMA          //--- Type of moving average we have 
#define RSI_PERIOD 24                //--- Period For Our RSI Indicator
#define RSI_PRICE  PRICE_CLOSE       //--- Applied Price For our RSI Indicator
#define HORIZON    38                //--- Holding period
#define TF PERIOD_H3                 //--- Time Frame

Unser System hängt von mehreren wichtigen globalen Variablen ab, die für die Überwachung unserer technischen Indikatoren verantwortlich sind und in den entsprechenden Handles und Puffern gespeichert sind, die wir während der Ausführung unseres Skripts aufrufen werden. Darüber hinaus werden wir weitere Variablen definieren, wie den Namen der Ausgabedatei und die abzufragende Datenmenge.

//--- Our handlers for our indicators
int ma_handle,ma_o_handle,rsi_handle;

//--- Data structures to store the readings from our indicators
double ma_reading[],ma_o_reading[],rsi_reading[];

//--- File name
string file_name = Symbol() + " Market Data As Series Multiple Strategy Analysis.csv";

//--- Amount of data requested
input int size = 3000;

Der Hauptteil unseres Skripts umfasst die wichtigsten Aufgaben, die wir heute erledigen wollen. Wir werden unsere Indikatoren initialisieren und die Indikatorwerte als Serien einstellen, wobei wir sicherstellen, dass die Werte chronologisch von den ältesten bis zu den jüngsten Daten geordnet sind. Auf diese Weise wollen wir unsere Daten strukturieren und weitergeben. Von dort aus werden wir alle Marktdaten ausschreiben und einige arithmetische Berechnungen durchführen, um die historischen Veränderungen der Marktdaten zu verfolgen, indem wir den Horizont-Parameter verwenden, der von unserem genetischen Optimierer in unserer vorherigen Diskussion eingestellt wurde.

//+------------------------------------------------------------------+
//| Our script execution                                             |
//+------------------------------------------------------------------+
void OnStart()
  {
  int fetch = size + (HORIZON * 2);
//---Setup our technical indicators
   ma_handle = iMA(_Symbol,TF,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE);
   ma_o_handle = iMA(_Symbol,TF,MA_PERIOD,0,MA_TYPE,PRICE_OPEN);
   rsi_handle = iRSI(_Symbol,TF,RSI_PERIOD,RSI_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(rsi_handle,0,0,fetch,rsi_reading);
   ArraySetAsSeries(rsi_reading,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","True Open","True High","True Low","True Close","True MA C","True MA O","True RSI","Open","High","Low","Close","MA Close","MA Open","RSI");
        }

      else
        {
         FileWrite(file_handle,
                   iTime(_Symbol,TF,i), 
                   iOpen(_Symbol,TF,i),
                   iHigh(_Symbol,TF,i),
                   iLow(_Symbol,TF,i),
                   iClose(_Symbol,TF,i),
                   ma_reading[i],
                   ma_o_reading[i],
                   rsi_reading[i],
                   iOpen(_Symbol,TF,i)   - iOpen(_Symbol,TF,(i + HORIZON)), 
                   iHigh(_Symbol,TF,i)   - iHigh(_Symbol,TF,(i + HORIZON)),
                   iLow(_Symbol,TF,i)    - iLow(_Symbol,TF,(i + HORIZON)),
                   iClose(_Symbol,TF,i)  - iClose(_Symbol,TF,(i + HORIZON)),
                   ma_reading[i] - ma_reading[(i + HORIZON)],
                   ma_o_reading[i] - ma_o_reading[(i + HORIZON)],
                   rsi_reading[i] - rsi_reading[(i + HORIZON)]
                   );
        }
     }
//--- Close the file
   FileClose(file_handle);
  }
//+------------------------------------------------------------------+


Analysieren der Daten in Python

Jetzt können wir mit der Analyse unserer Marktdaten beginnen, indem wir einige in Python verfügbare numerische Bibliotheken verwenden. Zu Beginn werden wir Pandas laden, um die Marktdaten einzulesen.

#Load our libraries
import pandas as pd

Anschließend kennzeichnen wir die Aktionen, die unsere Trainingsstrategie unter den gegebenen Marktbedingungen durchgeführt hätte, und berechnen den Gewinn oder Verlust, den jede Aktion erzeugt hätte.

#Read in the data
data = pd.read_csv("EURUSD Market Data As Series Multiple Strategy Analysis.csv")

#The optimal holding period suggested by our MT5 Genetic optimizer
HORIZON = 38

#Calculate the true market return
data['Return'] = data['True Close'].shift(-HORIZON) - data['True Close']

#The action suggested by our first strategy, MA Cross
data['Action 1'] = 0

#The action suggested by our second strategy, RSI Strategy
data['Action 2'] = 0

#Buy conditions
data.loc[data['True MA C'] > data['True MA O'],'Action 1'] = 1
data.loc[data['True RSI'] > 50,'Action 2'] = 1

#Sell conditions
data.loc[data['True MA C'] < data['True MA O'],'Action 1'] = -1
data.loc[data['True RSI'] < 50,'Action 2'] = -1

#Perform a linear transformation of the true market return, using our trading stragies
data['Return 1'] = data['Return'] * data['Action 1']
data['Return 2'] = data['Return'] * data['Action 2']

data = data.iloc[:-HORIZON,:]

Dies ist ein wesentlicher Schritt in jedem statistischen Modellierungs- und Handelsaufbau. Wir müssen sicherstellen, dass unser Modell nicht zu sehr an alle Daten angepasst ist; andernfalls wird jede Analyse oder Prüfung sinnlos, weil das Modell beeinträchtigt wurde.

#Drop our back test data
_    = data.iloc[-((365 * 2 * 6)):,:]
data = data.iloc[:-((365 * 2 * 6)),:]

Die Kennzeichnung unserer Ziele ist ein wichtiger Bestandteil jedes überwachten maschinellen Lernprojekts. Zur Veranschaulichung werden wir unsere Ziele kennzeichnen, um zu zeigen, ob die von Strategie 1 erzielte Rendite größer war als die von Strategie 2 oder umgekehrt. Unsere Zielvorgabe wird uns darüber informieren, ob Strategie 2 eine höhere Rendite als Strategie 1 erzielt hat. Zum Vergleich werden wir dies mit der Fähigkeit unseres Modells vergleichen, zukünftige Marktrenditen direkt vorherzusagen.

#Gether inputs
X = data.iloc[:,1:15]

#Both Strategies will earn equal reward
data['Target 1'] = 0 
data['Target 2'] = 0 

#Strategy 1 is more profitable
data.loc[data['Return 1'] > data['Return 2'],'Target 1'] = 1

#Strategy 2 is more profitable
data.loc[data['Return 2'] > data['Return 1'],'Target 2'] = 1

#Classical Target
data['Classical Target'] = 0

data.loc[data['Return'] > 0,'Classical Target'] = 1

Wir werden nun unsere Bibliotheken scikit-learn laden, um die numerischen Eigenschaften der gesammelten Marktdaten zu analysieren.

#Loading our scikit learn libraries
from sklearn.model_selection import TimeSeriesSplit,cross_val_score
from sklearn.linear_model import LinearRegression,LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.neural_network import MLPRegressor
from sklearn.model_selection import RandomizedSearchCV

Wir beginnen mit der Erstellung von Zeitreihenvalidierungsobjekten mit fünf Teilen (splits) und stellen sicher, dass die Lücke dem von unserem genetischen Optimierer gefundenen optimalen Horizont entspricht. Als Nächstes berechnen wir die Spaltenmittelwerte und Standardabweichungen, um unseren Datensatz so zu standardisieren, dass er einen Mittelwert von Null und eine Standardabweichung von Eins hat.

#Prepare the data for time series modelling
tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)

Z1 = X.mean()
Z2 = X.std()

X = ((X-X.mean()) / X.std())

Wir werden nun die Genauigkeit der Vorhersage der von uns festgelegten neuen Ziele messen und diese mit der Genauigkeit der direkten Vorhersage des klassischen Ziels der künftigen Kursrenditen vergleichen. Mit den Kreuzvalidierungsobjekten von scikit-learn werden wir die Genauigkeit mit einem linearen Klassifikator bewerten. Diese Ergebnisse werden dann in einem Array gespeichert und ein Balkendiagramm erstellt. Wie zu beobachten ist, liegt unsere Genauigkeit beim klassischen Ziel bei knapp 50 %, während unsere Genauigkeit bei der Vorhersage, welche der beiden Strategien profitabler sein wird, bei etwa 90 % liegt und damit das klassische Ziel deutlich übertrifft.

#Measuring our accuracy on our new target
res = []

model = LinearDiscriminantAnalysis()

res.append(np.mean(np.abs(cross_val_score(model,X,data['Classical Target'],cv=tscv,scoring='accuracy'))))

model = LinearDiscriminantAnalysis()

res.append(np.mean(np.abs(cross_val_score(model,X,data['Target 1'],cv=tscv,scoring='accuracy'))))

model = LinearDiscriminantAnalysis()

res.append(np.mean(np.abs(cross_val_score(model,X,data['Target 2'],cv=tscv,scoring='accuracy',n_jobs=-1))))

sns.barplot(res,color='black')
plt.xticks([0,1,2],['Classical Target','MA Cross Over Target','RSI Target'])
plt.axhline(res[0],linestyle=':',color='red')
plt.ylabel('5-Fold Percentage Accuracy %')
plt.title('Outperforming The Classical Target of Direct Price Prediction')

Abbildung 1: Wir machen Verbesserungen gegenüber der klassischen Aufgabe der direkten Preisvorhersage, indem wir die Beziehung zwischen unserer Strategie und dem Markt modellieren

Schließlich können wir die Bibliothek scikit-learn random search verwenden, um ein neuronales Netzwerk für unsere Marktdaten zu erstellen. Wir beginnen mit der Initialisierung unseres neuronalen Netzes mit Standardeinstellungen, die wir beibehalten wollen, wie z. B. die Shuffle- und Early-Stop-Parameter.

#Use random search to build a neural network for our market data

#Initialize the model
model = MLPRegressor(shuffle=False,early_stopping=False)


distributions = {'solver':['lbfgs','adam','sgd'],
                 'hidden_layer_sizes':[(X.shape[1],2,10,20),(X.shape[1],30,50,10),(X.shape[1],14,14,14),(X.shape[1],5,20,2),(X.shape[1],1,2,3,4,5,6,10),(X.shape[1],1,14,14,1)],
                 'activation':['relu','identity','logistic','tanh']
                 }

rscv = RandomizedSearchCV(model,distributions,n_jobs=-1,n_iter=50)

rscv.fit(X,data.loc[:,['Target 1','Target 2']])
Wir sind nun bereit, unser trainiertes neuronales Netz in das ONNX-Format zu exportieren. Um mit dem Export unseres neuronalen Netzes in unser nächstes Format zu beginnen, laden wir zunächst die ONNX-Bibliothek und dann die erforderlichen Konverter. Denken Sie daran, dass ONNX, die Abkürzung für Open Neural Network Exchange, ein Open-Source-Protokoll ist, das es uns ermöglicht, unsere Modelle für maschinelles Lernen auf einfache Weise zu erstellen und zu exportieren, und zwar modellunabhängig.
#Exporting our model to ONNX 
import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

initial_types = [('float_input',FloatTensorType([1,X.shape[1]]))]
final_types = [('float_output',FloatTensorType([2,1]))]

model = rscv.best_estimator_

model.fit(X,data.loc[:,['Target 1','Target 2']])

onnx_proto = convert_sklearn(model=model,initial_types=initial_types,final_types=final_types,target_opset=12)

onnx.save(onnx_proto,'EURUSD NN MSA.onnx')

Um unser ONNX-Diagramm unseres neuronalen Netzes zu betrachten, importieren wir zunächst die Netron-Bibliothek und verwenden dann einfach die Funktion netron.start und übergeben den Pfad des ONNX-Modells, um es zu betrachten. 

#Viewing our ONNX graph in netron
import netron
netron.start('../EURUSD NN MSA.onnx')

In der folgenden Abbildung 2 sind die Metaeigenschaften unseres ONNX-Modells dargestellt. Wir können sehen, dass unser ONNX-Modell 14 Eingänge und 2 Ausgänge hat, die beide Fließkommazahlen sind, sowie andere wichtige Metadaten wie den Produzenten und die ONNX-Version.

Abbildung 2: Visualisierung der mit unserem ONNX-Modell verbundenen Metadaten, um zu überprüfen, ob die richtigen Eingabe- und Ausgabegrößen angegeben wurden

Unser ONNX-Modell stellt Modelle des maschinellen Lernens als Graphen aus Rechenknoten und Kanten dar, die zeigen, wie Informationen von einem Rechenknoten zum nächsten weitergegeben werden. Auf diese Weise können alle Modelle des maschinellen Lernens in ein universelles Format übersetzt werden, nämlich in den ONNX-Graphen, der in Abbildung 3 dargestellt ist. Dieser Graph stellt unser neuronales Netz dar, das wir mit Hilfe der Zufallssuche der Sklearn-Bibliothek erstellt haben.

Abbildung 3: Visualisierung des Berechnungsgraphen, der unser tiefes neuronales Netz darstellt, mit der Netron-Bibliothek


Aufbau unseres Expertenberaters in MQL5

Der erste Schritt beim Aufbau unseres Expert Advisors besteht darin, das ONNX-Modell zu laden, das wir im vorherigen Schritt erstellt haben.

//+------------------------------------------------------------------+
//|                                                   MSA Test 1.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"

//+------------------------------------------------------------------+
//| ONNX Model                                                       |
//+------------------------------------------------------------------+
#resource "\\Files\\EURUSD NN MSA.onnx" as uchar onnx_buffer[];

Die Spaltenmittelwerte und Standardabweichungen, die wir in Python für jede Spalte gemessen haben, werden in entsprechenden Arrays namens Z1 und Z2 gespeichert. Erinnern Sie sich daran, dass wir diese Werte zur Skalierung und Standardisierung unserer Eingaben verwenden werden, bevor wir Vorhersagen von unserem ONNX-Modell erhalten.

//+------------------------------------------------------------------+
//| ONNX Parameters                                                  |
//+------------------------------------------------------------------+
double Z1[] = { 1.18932220e+00,  1.19077958e+00,  1.18786462e+00,  1.18931542e+00,
                1.18994040e+00,  1.18994674e+00,  4.94395259e+01, -4.99204879e-04,
                -5.00701302e-04, -4.97575935e-04, -4.98995739e-04, -4.70848300e-04,
                -4.70289373e-04, -1.84697724e-02
                };

double Z2[] = {1.09599015e-01, 1.09698934e-01, 1.09479324e-01, 1.09593123e-01,
               1.09413744e-01, 1.09419007e-01, 1.00452009e+01, 1.31269558e-02,
               1.31336302e-02, 1.31513465e-02, 1.31174740e-02, 6.88794916e-03,
               6.89036979e-03, 1.28550006e+01
              };

Wichtige Systemkonstanten werden definiert und während der gesamten Laufzeit unseres Programms beibehalten. Erinnern Sie sich, dass diese Konstanten in der vorangegangenen Diskussion mit Hilfe unseres genetischen Optimierers ausgewählt wurden.

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define MA_SHIFT         0
#define MA_TYPE          MODE_EMA
#define RSI_PRICE        PRICE_CLOSE
#define ONNX_INPUTS      14
#define ONNX_OUTPUTS     2
#define HORIZON 38 

Wichtige Strategieparameter, wie die Periode des gleitenden Durchschnitts und die RSI-Periode, wurden mit Hilfe des genetischen Optimierers für uns ausgewählt, und wir werden sie während unseres Programms konstant halten.

//+------------------------------------------------------------------+
//| Strategy Parameters                                              |
//+------------------------------------------------------------------+
int             MA_PERIOD                       =        100;  //Moving Average Period
int             RSI_PERIOD                      =         24;  //RSI Period
ENUM_TIMEFRAMES STRATEGY_TIME_FRAME             =  PERIOD_H3;  //Strategy Timeframe
int             HOLDING_PERIOD                  =         38;  //Position Maturity Period

Wir benötigen eine ganze Reihe von Abhängigkeiten, damit unsere Anwendung vollständig ist. Einige Abhängigkeiten, wie z. B. die Handelsbibliothek, sollten für den Leser offensichtlich sein. Andere, wie z. B. die Strategien, die wir gemeinsam in unserer Artikelserie entwickelt haben, sollten Ihnen inzwischen ebenfalls bekannt sein, wenn Sie uns weiterverfolgt haben. Andernfalls sind die Strategien, die wir laden, für die Ausführung unserer Handelsanwendung erforderlich.

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Time\Time.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>
#include <VolatilityDoctor\Strategies\OpenCloseMACrossover.mqh>
#include <VolatilityDoctor\Strategies\RSIMidPoint.mqh>

Wir werden wichtige globale Variablen haben, die in unserem Programm verwendet werden, aber glücklicherweise benötigen wir nur eine Handvoll davon. Zum Beispiel benötigen wir globale Variablen für einige der nutzerdefinierten Klassen, die wir erstellt haben, wie die Handels- und Zeitklassen, die RSI-Strategie und die Crossover-Strategieklassen. Andere globale Variablen werden benötigt, um Messwerte von unserem ONNX-Modell zu erhalten und die Vorhersagen zu speichern, die es macht.

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
//--- Custom Types
CTrade               Trade;
Time                 *TradeTime;
TradeInfo            *TradeInformation;
RSIMidPoint          *RSIMid;
OpenCloseMACrossover *MACross;
long                 onnx_model;
vectorf onnx_output;

//--- Our handlers for our indicators
int ma_handle,ma_o_handle,rsi_handle;

//--- Data structures to store the readings from our indicators
double ma_reading[],ma_o_reading[],rsi_reading[];

//--- System Types
int                  position_timer;

Immer wenn unsere Anwendung zum ersten Mal initialisiert wird, erstellen wir neue Instanzen der benötigten dynamischen Objekte. Wir haben zum Beispiel eine Klasse, die sich mit der Zeitmessung und dem Handel von Informationen beschäftigt. Wir werden neue Instanzen dieser Klasse sowie neue Instanzen der entsprechenden Indikator-Handler, die wir benötigen, erstellen. Von dort aus erstellen wir unser ONNX-Modell aus dem gefüllten Puffer und überprüfen, ob das Modell korrekt geladen wurde.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Create dynamic instances of our custom types
   TradeTime        = new Time(Symbol(),STRATEGY_TIME_FRAME);
   TradeInformation = new TradeInfo(Symbol(),STRATEGY_TIME_FRAME);
   MACross          = new OpenCloseMACrossover(Symbol(),STRATEGY_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE);
   RSIMid           = new RSIMidPoint(Symbol(),STRATEGY_TIME_FRAME,RSI_PERIOD,RSI_PRICE);
   onnx_model       = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT);
   onnx_output      = vectorf::Zeros(ONNX_OUTPUTS);

//---Setup our technical indicators
   ma_handle        = iMA(_Symbol,STRATEGY_TIME_FRAME,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE);
   ma_o_handle      = iMA(_Symbol,STRATEGY_TIME_FRAME,MA_PERIOD,0,MA_TYPE,PRICE_OPEN);
   rsi_handle       = iRSI(_Symbol,STRATEGY_TIME_FRAME,RSI_PERIOD,RSI_PRICE);
   if(onnx_model != INVALID_HANDLE)
     {
      Print("Preparing ONNX model");

      ulong input_shape[] = {1,ONNX_INPUTS};

      if(!OnnxSetInputShape(onnx_model,0,input_shape))
        {
         Print("Failed To Specify ONNX model input shape");
         return(INIT_FAILED);
        }

      ulong output_shape[] = {ONNX_OUTPUTS,1};

      if(!OnnxSetOutputShape(onnx_model,0,output_shape))
        {
         Print("Failed To Specify ONNX model output shape");
         return(INIT_FAILED);
        }
     }

//--- Everything was fine
   Print("Successfully loaded all components for our Expert Advisor");
   return(INIT_SUCCEEDED);
  }
//--- End of OnInit Scope

Wenn unsere Anwendung nicht mehr verwendet wird, löschen wir den nicht mehr benötigten Speicher und geben diese Ressourcen für andere Anwendungen frei, um unsere Anwendung sicher zu deaktivieren.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete the dynamic objects
   delete TradeTime;
   delete TradeInformation;
   delete MACross;
   delete RSIMid;
   OnnxRelease(onnx_model);
   IndicatorRelease(ma_handle);
   IndicatorRelease(ma_o_handle);
   IndicatorRelease(rsi_handle);
  }
//--- End of Deinit Scope

Immer wenn neue Kursniveaus in der OnTick- und der OnExpertStart-Funktion empfangen werden, prüfen wir zunächst, ob sich eine neue Tageskerze vollständig gebildet hat, indem wir die Funktion new_candle innerhalb der ChangeTime-Klasse aufrufen. Wenn sich tatsächlich eine Kerze gebildet hat, werden wir die Parameter unserer Strategie aktualisieren, bevor wir nach einer Handelsmöglichkeit suchen. Wenn sich Gelegenheiten zum Handel ergeben, werden wir sie nutzen. Andernfalls warten wir, bis unsere Position fällig ist, bevor wir sie schließen.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new daily candle has formed
   if(TradeTime.NewCandle())
     {
      //--- Update strategy
      Update();

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Reset the position timer
         position_timer = 0;

         //--- Check for a trading signal
         CheckSignal();
        }

      //--- Otherwise
      else
        {
         //--- The position has reached maturity
         if(position_timer == HOLDING_PERIOD)
            Trade.PositionClose(Symbol());
         //--- Otherwise keep holding
         else
            position_timer++;
        }
     }
  }
//--- End of OnTick Scope

Unsere Aktualisierungsmethode berücksichtigt einige wichtige Parameter, wie den Prognosehorizont, den wir mit unserem genetischen Optimierer ausgewählt haben. Von dort aus werden die von uns verwendeten Strategien und die in unseren Puffern gespeicherten technischen Indikatorwerte aktualisiert.

//+------------------------------------------------------------------+
//| Update our technical indicators                                  |
//+------------------------------------------------------------------+
void Update(void)
  {
   int fetch = (HORIZON * 2);

//--- Update the strategy
   RSIMid.Update();
   MACross.Update();

//---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(rsi_handle,0,0,fetch,rsi_reading);
   ArraySetAsSeries(rsi_reading,true);
  }
//--- End of Update Scope

Eine Vorhersage von unserem ONNX-Modell erhalten. Um eine Vorhersage aus dem ONNX-Modell zu erhalten, verwenden wir die ONNX-Run-Funktion. Vor dem Aufruf müssen wir jedoch zunächst die Eingabevariablen aktualisieren, die an das Modell übergeben werden, und dann diese Werte skalieren und standardisieren, indem wir den Mittelwert subtrahieren und durch die Standardabweichung dividieren.

//+------------------------------------------------------------------+
//| Get A Prediction from our ONNX model                             |
//+------------------------------------------------------------------+
void OnnxPredict(void)
  {
   vectorf   input_variables =
     {
      iOpen(_Symbol,STRATEGY_TIME_FRAME,0),
      iHigh(_Symbol,STRATEGY_TIME_FRAME,0),
      iLow(_Symbol,STRATEGY_TIME_FRAME,0),
      iClose(_Symbol,STRATEGY_TIME_FRAME,0),
      ma_reading[0],
      ma_o_reading[0],
      rsi_reading[0],
      iOpen(_Symbol,STRATEGY_TIME_FRAME,0)   - iOpen(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)),
      iHigh(_Symbol,STRATEGY_TIME_FRAME,0)   - iHigh(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)),
      iLow(_Symbol,STRATEGY_TIME_FRAME,0)    - iLow(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)),
      iClose(_Symbol,STRATEGY_TIME_FRAME,0)  - iClose(_Symbol,STRATEGY_TIME_FRAME,(0 + HORIZON)),
      ma_reading[0] - ma_reading[(0 + HORIZON)],
      ma_o_reading[0] - ma_o_reading[(0 + HORIZON)],
      rsi_reading[0] - rsi_reading[(0 + HORIZON)]
     };

   for(int i = 0; i < ONNX_INPUTS;i++)
     {
      input_variables[i] = ((input_variables[i] - Z1[i])/ Z2[i]);
     }

   OnnxRun(onnx_model,ONNX_DEFAULT,input_variables,onnx_output);
  }

Die Prüfung auf ein Handelssignal mit unserer Crossover-Strategie beginnt damit, dass wir zunächst eine Vorhersage des ONNX-Modells erhalten. Das Modell wird vorhersagen, welche Strategie seiner Meinung nach am profitabelsten sein wird. Von dort aus überprüfen wir die jeweilige Strategie, um zu sehen, ob ein entsprechendes Einstiegssignal verfügbar ist. Wir geben nur dann ein Signal ein, wenn das Modell erwartet, dass die Strategie profitabel ist, und die Strategie uns eine gute Handelsmöglichkeit bietet.

//+------------------------------------------------------------------+
//| Check for a trading signal using our cross-over strategy         |
//+------------------------------------------------------------------+
void CheckSignal(void)
  {

   OnnxPredict();

//--- MA Strategy is profitable
   if((onnx_output[0] > 0.5) && (onnx_output[1] < 0.5))
     {
      //--- Long positions when the close moving average is above the open
      if(MACross.BuySignal())
        {
         Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
         return;
        }

      //--- Otherwise short
      else
         if(MACross.SellSignal())
           {
            Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
            return;
           }
     }

//--- RSI strategy is profitable
   else
      if((onnx_output[0] < 0.5) && (onnx_output[1] > 0.5))
        {

         if(RSIMid.BuySignal())
           {
            Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
            return;
           }

         //--- Otherwise short
         else
            if(MACross.SellSignal())
              {
               Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
               return;
              }
        }
  }
//--- End of CheckSignal Scope

Gegen Ende unseres Systems werden wir alle Systemkonstanten, die wir zu Beginn unserer Anwendung definiert haben, zurücksetzen.

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef MA_SHIFT
#undef RSI_PRICE
#undef MA_TYPE
#undef ONNX_INPUTS
#undef ONNX_OUTPUTS
#undef HORIZON
//+------------------------------------------------------------------+

Die Auswahl der geeigneten Backtesting-Tage ist ganz einfach. Erinnern Sie sich, dass wir unsere neue Anwendung während der Vorwärts-Testphase testen wollen, die wir in unserem vorherigen Test verwendet haben. Daher wurden die Prüfungstermine entsprechend angepasst.

Abbildung 4: Auswahl der Backtest-Tage, sodass sie mit dem zuvor durchgeführten Vorwärtstest übereinstimmen

Wie immer wollen wir möglichst realistische Einstellungen verwenden, daher wählen wir die Einstellung Zufallsverzögerung.

Abbildung 5: Auswahl unserer Modellierungsbedingungen auf der Grundlage von „Random Delay“ für möglichst realistische Einstellungen

Zu meiner Überraschung haben die neuen Einstellungen für unsere Anwendung – einschließlich statistischer Modelle – unsere Leistung erheblich verschlechtert. Dies ist normalerweise ein deutlicher Hinweis darauf, dass in unserem Plan etwas schief gelaufen ist.

Abbildung 6: Die neue Kapitalkurve, die wir erstellt haben, ist in einem schlechten Zustand, wenn man sie mit unserer Leistung ohne statistische Modellierung vergleicht

Wenn wir uns die detaillierte Analyse der Performance unserer Anwendung genauer ansehen, sehen wir, dass der Gesamtnettogewinn zusammen mit der Sharpe Ratio gesunken ist, was kein gutes Zeichen ist. Dies zeigt, dass unsere Anwendung nicht den beabsichtigten Nutzen aus den von uns gewählten statistischen Modellierungswerkzeugen zieht.

Abbildung 7: Eine detaillierte Analyse der Leistung unserer neuen Handelsstrategie, die sich auf statistische Modelle stützt


Überarbeitung unserer Marktdaten in Python

Nach einer weiteren Überarbeitung unserer Marktdaten in Python wollte ich mir genauer ansehen, was die Fehlerursache sein könnte. Ich begann also mit dem Import der Standardbibliotheken, die wir zur Visualisierung von Marktdaten verwenden. Ich habe dann die kumulierte Summe einiger der von den beiden Strategien erzielten Renditen aufgezeichnet, und sofort wurde das Problem deutlich.
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

Wie aus der nachstehenden Grafik hervorgeht, weisen unsere beiden Strategien bemerkenswert ähnliche Merkmale und Steigungen auf. Die beiden Strategien tendieren dazu, im Gleichklang zu steigen und zu fallen, fast so, als ob wir eine einzige Strategie verfolgen würden.

fig , axs = plt.subplots(2,1,sharex=True)

fig.suptitle('Visualizing The Individual Cumulative Return of our 2 Strategies')

sns.lineplot(data['Return 1'].cumsum(),ax=axs[0],color='black')

sns.lineplot(data['Return 2'].cumsum(),ax=axs[1],color='black')

Abbildung 8: Unser Genetischer Optimierer scheint hoch korrelierte Strategien ausgewählt und ihnen die größten Stimmgewichte zugewiesen zu haben

Wenn wir das rollierende Risiko in den von unseren Strategien erzielten Renditen darstellen, sehen wir einen weiteren Grund zur Sorge. Das Risikoprofil unserer beiden Strategien unterscheidet sich kaum von dem des Marktes selbst. Auch hier scheint es, dass wir einfach dieselbe Strategie anwenden. Es ist schwer, zwischen Strategie eins und Strategie zwei zu unterscheiden, wenn ich die Spalten unseres Diagramms nicht beschriftet hätte.

fig , axs = plt.subplots(3,1,sharex=True)

fig.suptitle('Visualizing The Risk In Our 2 Strategies')

sns.lineplot(data['Return'].rolling(window=HORIZON).var(),ax=axs[0],color='black')
axs[0].axhline(data['Return'].var(),color='red',linestyle=':')

sns.lineplot(data['Return 1'].rolling(window=HORIZON).var(),ax=axs[1],color='black')
axs[1].axhline(data['Return 1'].var(),color='red',linestyle=':')

sns.lineplot(data['Return 2'].rolling(window=HORIZON).var(),ax=axs[2],color='black')
axs[2].axhline(data['Return 2'].var(),color='red',linestyle=':')

Abbildung 9: Beide Strategien sind in ihren Risiko- und Ertragsniveaus fast identisch, was die Idee hinter der Analyse mehrerer Strategien untergräbt

Der letzte Sargnagel ist die Berechnung der Korrelationsmatrix, die sich aus den drei Renditen ergibt: der Marktrendite, der Rendite der gleitenden Durchschnitts-Crossover-Strategie und der Rendite unserer RSI-Strategie. Es ist deutlich zu erkennen, dass der gleitende Durchschnitts-Crossover und die RSI-Strategie eine Korrelation von etwa 0,75 aufweisen, was sehr stark ist. Dies war der letzte Vorschlag, der mich davon überzeugte, dass der genetische Optimierer die Stimmengewichtung nicht unbedingt so anpasst, dass die Rentabilität maximiert wird. Vielmehr hat es den Anschein, dass es die Gewichte so einstellt, dass wirklich korrelierte Strategien isoliert werden – denn das macht seine Arbeit einfacher.

plt.title('Correlation Between Market Return And Strategy Returns')
sns.heatmap(data.loc[:,['Return','Return 1','Return 2']].corr(),annot=True)

Abbildung 10: Die Korrelationsmatrix, die sich aus unseren Handelsstrategien und dem EURUSD-Markt ergibt


Verbesserungen vornehmen

Mit dem Wissen, das wir jetzt haben, können wir erneut versuchen, unsere Anwendung zu verbessern. Zunächst kehren wir zu der früheren Version unserer Handelsstrategie zurück, die alle drei Strategien enthielt, die wir verwenden wollen.  

Abbildung 11: Auswahl der Backtest-Tage für unser Optimierungsverfahren

Wie immer werden wir für unseren Backtest zufällige Verzögerungseinstellungen verwenden.

Abbildung 12: Stellen Sie sicher, dass Sie die Option „Zufallsverzögerung“ verwenden, wenn Sie beabsichtigen, dem Geschehen zu folgen.

Im Gegensatz zu früheren Tests setzen wir jetzt jedoch alle Stimmgewichte auf 1, um sicherzustellen, dass der genetische Optimierer nur Änderungen vornehmen kann, die die Gesamtrentabilität der Strategie erhöhen. Darüber hinaus wollen wir den Optimierer dazu zwingen, alle drei Strategien zu verwenden und nicht nur die korrelierten herauszupicken, da dies unserem Ziel zuwiderläuft.

Abbildung 13: Denken Sie daran, dass wir dieses Mal alle Stimmgewichte auf 1 setzen wollen, weil wir vermuten, dass der Genetische Optimierer uns überlistet

Bei der Überprüfung der Optimierungsergebnisse können wir bereits Verbesserungen gegenüber den anfänglichen Vektoren, die wir durchgeführt haben, feststellen. Anfänglich beobachteten wir bei allen drei Strategien Rentabilitätsniveaus zwischen 40 und 50 Dollar. Aber wir sehen jetzt eine deutliche Verbesserung der Rentabilität.

Abbildung 14: Unsere Optimierungsergebnisse haben sich gegenüber der letzten Iteration erheblich verbessert

Wenn wir die Ergebnisse des Forward-Tests betrachten, sehen wir außerdem wieder erhöhte Gewinnraten. Unsere Tests sind jetzt sowohl im Rückwärts- als auch im Vorwärtstest profitabel, was ein starkes Zeichen für die Stabilität der Konfiguration ist. Am wichtigsten ist, dass unser genetischer Optimierer nun in der Lage ist, uns Stapel von Strategiekonfigurationen zu liefern, die in beiden Tests profitabel waren. Dies waren die Ergebnisse, die wir zuvor nicht erzielen konnten, als wir dem Optimierer erlaubten, die Gewichte nach Belieben zu ändern. Dies bestärkt uns in der Annahme, dass unsere Anwendung in dieser Konfiguration stabil ist.

Abbildung 15: Unsere Forward-Ergebnisse zeigen nun deutliche Anzeichen von Stabilität und Rentabilität

Wenn wir schließlich einen Backtest mit den profitabelsten Einstellungen durchführen, die wir aus den Vorwärtsergebnissen ermittelt haben, können wir deutlich sehen, dass unsere neuen Parametereinstellungen sowohl im Backtest als auch im Vorwärtstest profitabel sind, und zwar mit einem Aufwärtstrend – genau das, was wir sehen wollen.

Abbildung 16: Die Kapitalkurve, die sich aus unserer neuen Handelsstrategie mit festen Stimmen ergibt, ist wesentlich profitabler


Schlussfolgerung

Aus dieser Diskussion haben wir einige wertvolle Lehren gezogen. Erstens haben wir gesehen, dass die Herausforderung des Belohnungshackings allgegenwärtig ist und auftreten kann, ob wir uns dessen bewusst sind oder nicht. Unsere KI-Tools – wie z. B. genetische Optimierer – sind schnell und intelligent, manchmal sogar intelligenter als wir. Wir müssen immer wachsam sein, um sicherzustellen, dass diese Tools uns nicht überlisten, indem sie ungültige Lösungen generieren, die lediglich die von uns festgelegten Erfolgsbedingungen erfüllen.

Durch sorgfältige Prüfung der Renditen der vom Optimierer ausgewählten Strategien haben wir gelernt, dass wir die Auswahl korrelierter Strategien vermeiden müssen, da dies den Zweck einer Multi-Strategie-Analyse zunichte macht.

Der Leser sollte beachten, dass genetische Optimierer nur durch die Zeit eingeschränkt werden, die wir bereit sind zu investieren. Andernfalls liefert unsere Diskussion nicht notwendigerweise den Beweis, dass der genetische Optimierer des MetaTrader 5 immer die ihm zugewiesenen Belohnungen hacken wird. Da wir nur eine kleine Anzahl von Strategien ausgewählt und diese Optimierung nur einmal durchgeführt haben, besteht immer die Möglichkeit, dass eine Wiederholung des Prozesses andere Ergebnisse liefern würde.

Vielmehr ist dies ein Beweis dafür, dass ich, der Autor, bei der Formulierung des Problems für den genetischen Optimierer vielleicht nicht sorgfältig genug vorgegangen bin. Ein besserer Ansatz wäre es gewesen, den Optimierer zunächst zu zwingen, alle verfügbaren Strategien zu verwenden, und dann die Abstimmungsgewichte mit mehr Bedacht anzupassen.

Am wichtigsten ist, dass wir diese Übung mit dem Wissen abschließen, dass wir in unserem nächsten Artikel sorgfältig prüfen müssen, wie wir die Rentabilität übertreffen können, die mit einheitlichen Gewichten von 1 erzielt wird. Die Leistung, die wir mit diesen einheitlichen Gewichten erzielt haben, dient als solider Benchmark, den wir in unserer nächsten Diskussion mit den statistischen Modellen, die wir ursprünglich in diesem Artikel anwenden wollten, übertreffen wollen.

DateinameBeschreibung der Datei
Fetch Data MSA.mq5Das MQL5-Skript, das wir verwendet haben, um Daten über die 2 von unserem genetischen Optimierer ausgewählten Strategien abzurufen.
MSA Test 2.1.mq5Der Expert Advisor, den wir unter Verwendung der 2 ausgewählten Strategien und unseres ONNX-Modells erstellt haben.
Analyzing Multiple Strategies I.ipynbDas Jupyter Notebook, das wir gemeinsam geschrieben haben, um die Marktdaten zu analysieren, die wir mit unserem MQL5-Skript abgerufen haben.
EURUSD NN MSA.onnxUnser tiefes neuronales Netzwerk, das wir mit der sklearn random-search-Bibliothek erstellt haben.

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/18847

MQL5-Handelswerkzeuge (Teil 7): Informatives Dashboard für Multi-Symbol-Positionen und Kontoüberwachung MQL5-Handelswerkzeuge (Teil 7): Informatives Dashboard für Multi-Symbol-Positionen und Kontoüberwachung
In diesem Artikel entwickeln wir ein Informations-Dashboard in MQL5 zur Überwachung von Multi-Symbol-Positionen und Kontometriken wie Kontostand, Kapital und freie Marge. Wir implementieren ein sortierbares Raster mit Echtzeit-Updates, CSV-Export und einen leuchtenden Header-Effekt, um die Nutzerfreundlichkeit und den visuellen Reiz zu verbessern.
Entwicklung des Price Action Analysis Toolkit (Teil 34): Umwandlung von Marktrohdaten in Prognosemodellen mithilfe einer fortschrittlichen Pipeline der Datenerfassung Entwicklung des Price Action Analysis Toolkit (Teil 34): Umwandlung von Marktrohdaten in Prognosemodellen mithilfe einer fortschrittlichen Pipeline der Datenerfassung
Haben Sie schon einmal einen plötzlichen Marktanstieg verpasst oder wurden Sie von einem solchen überrascht? Der beste Weg, aktuelle Ereignisse zu antizipieren, besteht darin, aus historischen Mustern zu lernen. Mit dem Ziel, ein ML-Modell zu trainieren, zeigt Ihnen dieser Artikel zunächst, wie Sie ein Skript in MetaTrader 5 erstellen, das historische Daten aufnimmt und sie zur Speicherung an Python sendet. Lesen Sie weiter, um die einzelnen Schritte in Aktion zu sehen.
Entwicklung des Price Action Analysis Toolkit (Teil 35): Training und Einsatz von Vorhersagemodellen Entwicklung des Price Action Analysis Toolkit (Teil 35): Training und Einsatz von Vorhersagemodellen
Historische Daten sind alles andere als „Müll“ – sie sind die Grundlage für jede solide Marktanalyse. In diesem Artikel führen wir Sie Schritt für Schritt von der Erfassung der Historie über die Verwendung zur Erstellung eines Prognosemodells bis hin zum Einsatz dieses Modells für Live-Preisprognosen. Lesen Sie weiter, um zu erfahren, wie!
MQL5-Handelswerkzeuge (Teil 6): Dynamisches holografisches Dashboard mit Impulsanimationen und Steuerelementen MQL5-Handelswerkzeuge (Teil 6): Dynamisches holografisches Dashboard mit Impulsanimationen und Steuerelementen
In diesem Artikel erstellen wir ein dynamisches holografisches Dashboard in MQL5 zur Überwachung von Symbolen und Zeitrahmen mit RSI, Volatilitätswarnungen und Sortieroptionen. Wir fügen eine pulsierende Animationen, interaktive Schaltflächen und holografische Effekte hinzu, um das Tool visuell ansprechend und reaktionsschnell zu gestalten.