Klassische Strategien neu interpretieren (Teil II): Bollinger-Bänder Ausbrüche
Einführung
Bollinger-Bänder sind ein vielseitiges Instrument für Handelsstrategien, das sowohl für die Trendverfolgung als auch für die Identifizierung potenzieller Wende- oder Umkehrpunkte geeignet ist. Technisch gesehen besteht der Indikator aus einem exponentiellen gleitenden Durchschnitt (EMA), der den Schlusskurs eines Wertpapiers glättet. Diese zentrale Linie wird von zwei weiteren Linien umschlossen, die in der Regel 2 Standardabweichungen über und unter dem EMA liegen.
In diesem Artikel wollen wir die Vorteile der Strategie von Grund auf empirisch analysieren, um den Lesern, die den Einsatz der Bollinger-Bänder in Erwägung ziehen, die Entscheidung zu erleichtern, ob die Strategie für sie besser geeignet ist. Darüber hinaus werden wir zeigen, wie technische Indikatoren verwendet werden können, um KI-Modelle zu steuern und hoffentlich stabilere Handelsstrategien zu entwickeln.
Dazu haben wir zwei gleichwertige KI-Modelle mit dem Algorithmus der linearen Diskriminanzanalyse trainiert und die Modelle mit Hilfe von Zeitreihen-Kreuzvalidierung verglichen, wobei wir uns für die Tests ausschließlich auf die scikit-learn-Bibliothek verlassen haben. Das erste Modell wurde so trainiert, dass es einfach vorhersagen konnte, ob der Preis steigen oder fallen würde, während das zweite Modell lernte zu prognostizieren, wie sich der Preis zwischen den vier vom Bollinger-Bänder umrissenen Zonen bewegt. Zum Leidwesen der Fans der Bollinger-Bänder haben uns unsere empirischen Beobachtungen zu dem Schluss gebracht, dass eine direkte Preisvorhersage effektiver sein kann als die Vorhersage des Übergangs zwischen den vier von den Bollinger-Bändern gebildeten Zonen. Es ist jedoch anzumerken, dass bei der Festlegung der Parameter des Indikators keine Optimierungstechniken eingesetzt wurden.
Dieser Artikel soll verdeutlichen:
- Wie kann man zwei mögliche Handelsstrategien analytisch vergleichen?
- Wie man die lineare Diskriminanzanalyse von Grund auf in MQL5 implementiert.
- Wie man stabile Handelsstrategien unter Einbezug der KI entwickelt.
Überblick über die Strategie und unsere Beweggründe
Der Begriff „Künstliche Intelligenz“ (KI) ist wohl eine der irreführendsten Namensgebungen der Geschichte. Nachdem Sie diesen Artikel gelesen haben, werden Sie vielleicht zustimmen, dass KI eine falsche Bezeichnung ist. Für mich als Autor liegt das Problem in dem Wort „Intelligenz“. KI-Modelle sind nicht intelligent im menschlichen Sinne. Vielmehr handelt es sich um intelligente Anwendungen von Optimierungsalgorithmen.
KI-Modelle zielen in erster Linie auf die Minimierung von Fehlern oder die Maximierung von Gewinnen innerhalb eines Systems ab. Die aus diesen Modellen abgeleiteten Lösungen sind jedoch nicht immer praktikabel. Ein KI-System, das darauf ausgelegt ist, die Verluste auf einem Handelskonto zu minimieren, könnte beispielsweise zu dem Schluss kommen, dass es am besten ist, keine Geschäfte zu tätigen, da dies keine Verluste garantiert. Auch wenn diese Lösung mathematisch gesehen dem Problem gerecht wird, ist sie für den Handel unpraktisch.
Als intelligente KI-Praktiker müssen wir unsere Modelle mit sorgfältig geplanten Beschränkungen steuern. In diesem Artikel werden wir unsere KI-Modelle mithilfe von Bollinger-Bändern steuern. Wir werden vier mögliche Zonen identifizieren, in denen sich der Preis jederzeit befinden könnte. Beachten Sie, dass sich der Preis zu einem bestimmten Zeitpunkt nur in einer dieser vier Zonen befinden kann:
- Zone 1: Der Preis liegt vollständig oberhalb der Bollinger-Bänder.
- Zone 2: Der Preis liegt oberhalb des mittleren Bandes, aber unterhalb des oberen Bandes.
- Zone 3: Der Preis liegt oberhalb des unteren Bandes, aber unterhalb des mittleren Bandes.
- Zone 4: Preis liegt unter dem unteren Band.
Wir werden ein Modell trainieren, um zu verstehen, wie der Preis zwischen diesen vier Zonen übergeht, und die nächste Zone vorhersagen, in die sich der Preis bewegen wird. Handelssignale werden immer dann erzeugt, wenn sich der Preis von einer Zone in eine andere bewegt. Wenn unser Modell beispielsweise vorhersagt, dass sich der Kurs von Zone 2 nach Zone 1 bewegen wird, interpretieren wir dies als eine Aufwärtsbewegung und lösen einen Kaufauftrag aus. Unser Modell und unser Expert Advisor werden vollständig in nativem MQL5 implementiert sein.
Die Bollinger-Bänder können in einer Reihe von Handelsstrategien eingesetzt werden, von der Trendverfolgung bis hin zur Identifizierung von Wendepunkten oder Umkehrpunkten. Technisch gesehen besteht dieser Indikator aus einem exponentiellen gleitenden Durchschnitt (EMA), der normalerweise den Schlusskurs eines Wertpapiers glättet. Er wird von zwei zusätzlichen Bändern flankiert: eines oberhalb und eines unterhalb des EMA, deren Abstand in der Regel jeweils auf 2 Standardabweichungen festgelegt ist.
Traditionell werden Bollinger-Bänder verwendet, um überkaufte und überverkaufte Kursniveaus zu identifizieren. Wenn die Kurse das obere Bollinger-Bänder erreichen, neigen sie dazu, auf den Mittelwert zurückzufallen, und dieses Verhalten gilt oft auch für das untere Band. Dies kann so interpretiert werden, dass das Wertpapier um 2 Standardabweichungen abgewertet wird, wenn es die untere Bandbreite erreicht, was Anleger dazu verleiten könnte, den Vermögenswert mit einem attraktiven Abschlag zu kaufen. Es kann jedoch vorkommen, dass die Kurse heftig aus den Bollinger-Bändern ausbrechen und sich in einem starken Trend fortsetzen. Leider zeigt unsere statistische Analyse, dass die Vorhersage von Ausbrüchen aus dem Bollinger-Band schwieriger sein kann als die Vorhersage von Preisänderungen.Abrufen der Daten von unserem MetaTrader 5 Terminal
Um zu beginnen, öffnen Sie Ihr MetaTrader5-Terminal und klicken Sie auf das Symbol-Symbol im Kontextmenü. Sie sollten eine Liste der auf Ihrem Terminal verfügbaren Symbole sehen.
Abb. 1: Vorbereitung des Abrufs von Daten von unserem MetaTrader5 Terminal.
Klicken Sie dann auf das Fenster Balken und suchen Sie nach dem Symbol, das Sie modellieren möchten, und wählen Sie den gewünschten Zeitrahmen aus. Für unser Beispiel werde ich den täglichen GBPUSD-Wechselkurs modellieren.
Abb. 2: Wir bereiten den Export unserer Daten vor.
Klicken Sie dann auf die Schaltfläche „Balken exportieren“, und wir werden unsere Analyse in Python fortsetzen.
Explorative Datenanalyse
Veranschaulichen wir uns die Wechselwirkungen zwischen den Bollinger-Bändern und den Veränderungen der Kursniveaus.
Wir beginnen damit, die benötigten Bibliotheken zu importieren.
#Import libraries import pandas as pd import numpy as np import seaborn as sns import pandas_ta as taDann lesen wir die csv-Datei ein, die wir für unseren empirischen Test erstellt haben. Beachten Sie, dass wir den Parameter sep="\t" übergeben haben, um anzugeben, dass unsere csv-Datei durch Tabulatoren getrennt ist. Dies ist die Standardausgabe des MetaTrader5 Terminals.
#Read in the csv file csv = pd.read_csv("/home/volatily/market_data/GBPUSD_Daily_20160103_20240131.csv",sep="\t")Definieren wir nun unseren Prognosehorizont.
#Define how far into the future we should forecast look_ahead = 20Jetzt werden wir die Bollinger-Bänder für die Daten berechnen, die wir mit der Bibliothek Pandas-Ta haben.
#Add the Bollinger bands csv.ta.bbands(length=30,std=2,append=True)Als Nächstes benötigen wir eine Spalte, in der der zukünftige Schlusskurs gespeichert wird.
#Add a column to show the future price csv["Future Close"] = csv["Close"].shift(-look_ahead)
Jetzt werden wir unsere Daten kennzeichnen. Wir werden zwei Kennzeichnungen haben, von denen eines die Preisveränderung und das andere die Preisveränderung zwischen den Bollinger-Band-Zonen angibt. Preisänderungen werden mit 1 für einen Anstieg und mit 0 für einen Rückgang gekennzeichnet. Die Kennzeichnungen der Bollinger-Bänder wurden oben definiert.
#Add the normal target, predicting changes in the close price csv["Price Target"] = 0 csv["Price State"] = 0 #Label the data our conditions #If price depreciated, our label is 0 csv.loc[csv["Close"] < csv["Close"].shift(look_ahead),"Price State"] = 0 csv.loc[csv["Close"] > csv["Future Close"], "Price Target"] = 0 #If price appreciated, our label is 1 csv.loc[csv["Close"] > csv["Close"].shift(look_ahead),"Price State"] = 1 csv.loc[csv["Close"] < csv["Future Close"], "Price Target"] = 1 #Label the Bollinger bands #The label to store the current state of the market csv["Current State"] = -1 #If price is above the upper-band, our label is 1 csv.loc[csv["Close"] > csv["BBU_30_2.0"], "Current State"] = 1 #If price is below the upper-band and still above the mid-band,our label is 2 csv.loc[(csv["Close"] < csv["BBU_30_2.0"]) & (csv["Close"] > csv["BBM_30_2.0"]),"Current State"] = 2 #If price is below the mid-band and still above the low-band,our label is 3 csv.loc[(csv["Close"] < csv["BBM_30_2.0"]) & (csv["Close"] > csv["BBL_30_2.0"]),"Current State"] = 3 #Finally, if price is beneath the low-band our label is 4 csv.loc[csv["Close"] < csv["BBL_30_2.0"], "Current State"] = 4 #Now we can add a column to denote the future state the market will be in csv["State Target"] = csv["Current State"].shift(-look_ahead)
Lassen Sie uns alle ungültigen Einträge löschen.
#Let's drop any NaN values csv.dropna(inplace=True)
Wir können nun mit der Visualisierung unserer Daten beginnen, zunächst mit den Veränderungen der Preisniveaus anhand von Boxplots. Auf der y-Achse werden die Schlusskurse und auf der x-Achse zwei Werte angezeigt. Der erste Wert auf der x-Achse steht für die Fälle in unseren Daten, in denen der Preis fiel, und ist mit 0 gekennzeichnet. Innerhalb des Wertes 0 finden Sie zwei Boxplots. Das erste, blau dargestellte Boxplot zeigt die Fälle, in denen der Kurs 20 Kerzen lang fiel und dann weitere 20 Kerzen lang weiter fiel. Das orangefarbene Kästchen stellt die Fälle dar, in denen der Preis 20 Kerzen lang fiel, dann aber in den nächsten 20 Kerzen anstieg. Die von uns erhobenen Daten zeigen, dass die Preise immer dann, wenn sie unter 1,1 fielen, wieder anstiegen. Umgekehrt hat der Wert 1 auf der x-Achse auch zwei Boxplots darüber. Die erste blaue Box fasst die Fälle zusammen, in denen der Preis anstieg und dann fiel, während die zweite orangefarbene Box die Fälle zusammenfasst, in denen der Preis stieg und weiter stieg.
Beachten Sie, dass für den Wert 1, d. h. wenn der Kurs 20 Kerzen lang steigt, das Ende des blauen Box-Plots größer ist als das des orangen Boxplots. Dies könnte darauf hindeuten, dass der GBPUSD-Wechselkurs bei einem Anstieg in Richtung der 1,5-Marke zu einem Rückgang neigt, während in der 0-Spalte bei einem Rückgang des Wechselkurses auf etwa die 1,1-Marke eine Tendenz zur Umkehr und zum Anstieg zu beobachten ist.
#Notice that the tails of the box plots have regions where they stop overlapping these zones may guide us as boundaries sns.boxplot(data=csv,x="Price State",y="Close",hue="Price Target")
Abb. 3: Visualisierung der Veränderungen im Preisniveau.
Wir können ähnliche Visualisierungen auch mit den durch die Bollinger-Bänder definierten Zuständen durchführen. Wie zuvor wird der Schlusskurs auf der y-Achse angezeigt, und die aktuelle Position des Kurses innerhalb der Bollinger-Bänder wird durch die vier Werte auf der x-Achse markiert. Beachten Sie, dass die Dochte der Boxplots Bereiche aufweisen, in denen sie sich nicht überschneiden. Diese Regionen können möglicherweise als Klassifizierungsgrenzen dienen. Beobachten Sie beispielsweise, dass der Kurs immer dann, wenn er sich im Zustand 4 oder vollständig unter den Bollinger-Bändern befindet und sich der 1,1-Marke nähert, anscheinend immer wieder abprallen wird.
#Notice that the tails of the box plots have regions where they stop overlapping these zones may guide us as boundaries sns.boxplot(data=csv,x="Current State",y="Close",hue="Price Target")
Abb. 4: Visualisierung des Kursverhaltens innerhalb der 4 Zonen der Bollinger-Bänder.
Außerdem können wir mit Hilfe von Boxplots visualisieren, wie der Kurs zwischen den vier Bollinger-Band-Zuständen wechselt. Das nachstehende Boxplot zeigt beispielsweise den Schlusskurs auf der y-Achse und vier Werte auf der x-Achse, die die vier von den Bollinger-Bändern gebildeten Zonen darstellen. Jede Box fasst zusammen, wohin der Preis nach dem Auftauchen in dieser Zone übergegangen ist. Lassen Sie uns die Daten gemeinsam interpretieren. Beachten Sie, dass der erste Wert, Zustand 1, nur drei Boxplots aufweist. Das bedeutet, dass der Preis von Zustand 1 aus nur in drei mögliche Zustände übergeht: Er bleibt entweder in Zustand 1 oder geht in Zustand 2 oder 3 über.
#Notice that the tails of the box plots have regions where they stop overlapping these zones may guide us as boundaries sns.boxplot(data=csv,x="Current State",y="Close",hue="State Target")
Abb. 5: Visualisierung des Preisverhaltens innerhalb der 4 Zonen.
#we have very poor separation in the data sns.catplot(data=csv,x="Price State",y="Close",hue="Price Target")
Abb. 6: Visualisierung der Trennung im Datensatz.
Wir können die gleichen Visualisierungen mit den vier Zuständen durchführen, die durch die Bollinger-Bänder definiert sind. Auch hier ist zu beobachten, dass die blauen und orangenen Punkte am besten bei den extremen Preisniveaus getrennt sind.
#Visualizing the separation of data in the Bollinger band zones sns.catplot(data=csv,x="Current State",y="Close",hue="Price Target")
Abb. 7: Visualisierung der Datentrennung in den Zonen der Bollinger-Bänder.
Wir können nun ein Streudiagramm mit dem Schlusskurs auf der x-Achse und dem zukünftigen Schlusskurs auf der y-Achse erstellen. Wir färben die Punkte entweder orange oder blau, je nachdem, ob der Kurs in den letzten 20 Kerzen gestiegen oder gefallen ist. Stellen Sie sich vor, Sie legen eine goldene Linie von der linken unteren Ecke zur rechten oberen Ecke des Grundstücks. Alle Punkte oberhalb dieser goldenen Linie stellen Fälle dar, in denen der Kurs in den nächsten 20 Kerzen gestiegen ist, unabhängig davon, ob er in den vorangegangenen 20 Kerzen gefallen (blau) oder gestiegen (orange) ist. Beachten Sie, dass auf beiden Seiten der goldenen Linie eine Mischung aus blauen und orangefarbenen Punkten vorhanden ist.
Wenn wir eine imaginäre rote Linie bei dem Schlusswert von 1,3 ziehen würden, gäbe es viele blaue und orangefarbene Punkte, die diese Linie berühren. Dies bedeutet, dass neben dem aktuellen Schlusskurs auch andere Variablen den zukünftigen Schlusskurs beeinflussen. Eine andere Möglichkeit, diese Beobachtungen zu interpretieren, besteht darin, dass derselbe Eingabewert zu unterschiedlichen Ausgabewerten führen kann, was darauf hindeutet, dass unser Datensatz verrauscht ist!
#Notice that using the price target gives us beautiful separation in the data set sns.scatterplot(data=csv,x="Close",y="Future Close",hue="Price Target")
Abb. 8: Unser Datensatz weist eine sehr geringe natürliche Trennung auf.
Wir werden nun dieselbe Visualisierung durchführen, indem wir den Zielzustand der Bollinger-Bänder verwenden, um das Streudiagramm einzufärben. Beachten Sie, dass die Trennung innerhalb unseres Datensatzes bei Verwendung der Bollinger-Bänder sehr gering ist. Optisch sieht es noch schlechter aus als die Trennung, die wir erhalten haben, wenn wir einfach den Preis selbst verwendet haben.
#Using the Bollinger bands to define states, however, gives us rather mixed separation sns.scatterplot(data=csv,x="Close",y="Future Close",hue="Current State")
Abb. 9: Visualisierung der durch die Zonen der Bollinger-Bänder erzeugten Trennung im Datensatz.
Führen wir nun unsere analytischen Tests durch, um festzustellen, ob wir eine größere Genauigkeit bei der Vorhersage von Änderungen der Preisniveaus oder von Änderungen der Bollinger-Band-Zustände erreichen. Zunächst importieren wir die erforderlichen Bibliotheken.
#Now let us compare our accuracy forecasting the original price target and the new Bollinger bands target from sklearn.discriminant_analysis import LinearDiscriminantAnalysis from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import accuracy_score
Als Nächstes werden wir die Parameter für die Kreuzvalidierung der Zeitreihen festlegen. Der erste Parameter, splits, gibt die Anzahl der Partitionen an, die aus unseren Daten erstellt werden sollen. Der zweite Parameter, gap, bestimmt die Größe der Lücke zwischen den einzelnen Partitionen. Diese Lücke sollte mindestens so groß sein wie unser Prognosehorizont.
#Now let us define the cross validation parameters splits = 10 gap = look_ahead
Nun können wir unser Zeitreihenobjekt erstellen, das uns die entsprechenden Indizes für unseren Trainings- und Testsatz liefert. In unserem Beispiel werden 10 Indexpaare erzeugt, um unser Modell zu trainieren und zu bewerten.
#Now create the cross validation object
tscv = TimeSeriesSplit(n_splits=splits,gap=gap)
Als Nächstes erstellen wir einen DataFrame, um die Genauigkeit unseres Modells für die Vorhersage der einzelnen Ziele zu speichern.
#We need a dataframe to store the accuracy associated with each target target_accuracy = pd.DataFrame(index=np.arange(0,splits),columns=["Price Target Accuracy","New Target Accuracy"])
Nun werden wir unsere Modelleingaben definieren.
#Define the inputs predictors = ["Open","High","Low","Close"] target = "Price Target"
Nun werden wir den Kreuzvalidierungstest durchführen.
#Now let us perform the cross validation for i,(train,test) in enumerate(tscv.split(csv)): #First initialize the model model = LinearDiscriminantAnalysis() #Now train the model model.fit(csv.loc[train[0]:train[-1],predictors],csv.loc[train[0]:train[-1],target]) #Now record the accuracy target_accuracy.iloc[i,0] = accuracy_score(csv.loc[test[0]:test[-1],target],model.predict(csv.loc[test[0]:test[-1],predictors]))
Jetzt können wir endlich das Ergebnis der Tests analysieren.
target_accuracy
Abb. 10: Unser Modell schnitt bei der direkten Vorhersage von Preisveränderungen besser ab.
Wie bereits erwähnt, haben unsere Tests gezeigt, dass unser Modell bei der Vorhersage von Kursniveaus effektiver ist als die Übergänge der Bollinger-Bänder. Es ist jedoch zu beachten, dass sich die beiden Strategien im Durchschnitt nicht signifikant unterscheiden.
Als Nächstes werden wir die Strategie in MQL5-Code implementieren, um sie zu testen und zu sehen, wie sie sich auf realen Marktdaten verhält
Die Umsetzung der Strategie
Zunächst importieren wir die notwendigen Bibliotheken, die wir in unserem Programm verwenden werden.
//+------------------------------------------------------------------+ //| Target Engineering.mq5 | //| Gamuchirai Zororo Ndawana | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Zororo Ndawana" #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Libraries we need | //+------------------------------------------------------------------+ /* This Expert Advisor will implement the Linear Discriminant Anlysis algorithm to help us successfully trade Bollinger Band Breakouts. Gamuchirai Zororo Ndawana Selebi Phikwe Botswana Wednesday 10 July 2024 15:42 */ #include <Trade/Trade.mqh>//Trade class CTrade Trade;
Als Nächstes werden wir nutzerkonfigurierbare Eingaben definieren, z. B. die Periode der Bollinger-Bänder und die Standardabweichung.
//+------------------------------------------------------------------+ //| Input variables | //+------------------------------------------------------------------+ input double bband_deviation = 2.0;//Bollinger Bands standard deviation input int bband_period = 60; //Bollinger Bands Period input int look_ahead = 10; //How far into the future should we forecast? int input lot_multiple = 1; //How many times bigger than minimum lot? int input fetch = 200;//How much data should we fetch? input double stop_loss_values = 1;//Stop loss values
Anschließend werden wir die globalen Variablen definieren, die in unserer Anwendung verwendet werden sollen.
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int bband_handler;//Technical Indicator Handlers vector bband_high_reading = vector::Ones(fetch);//Bollinger band high reading vector bband_mid_reading = vector::Ones(fetch);//Bollinger band mid reading vector bband_low_reading = vector::Ones(fetch);//Bollinger band low reading double minimum_volume;//The smallest contract size allowed double ask_price;//Ask double bid_price;//Bid vector input_data = vector::Zeros(fetch);//All our input data will be kept in vectors int training_output_array[];//Our output data will be stored in a vector vector output_data = vector::Zeros(fetch); double variance;//This is the variance of our input data int classes = 4;//The total number of output classes we have vector mean_values = vector::Zeros(classes);//This vector will store the mean value for each class vector probability_values = vector::Zeros(classes);//This vector will store the prior probability the target will belong each class vector total_class_count = vector::Zeros(classes);//This vector will count the number of times each class was the target bool model_trained = false;//Has our model been trained? bool training_procedure_running = false;//Have we started the training process? int forecast = 0;//Our model's forecast double discriminant_values[4];//The discriminant function int current_state = 0;//The current state of the system
Als Nächstes müssen wir die Initialisierungsfunktion unseres Expert Advisors definieren. In dieser Funktion initialisieren wir den Indikator der Bollinger-Bänder und rufen wichtige Marktdaten ab.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Initialize the bollinger bands bband_handler = iBands(_Symbol,PERIOD_CURRENT,bband_period,0,bband_deviation,PRICE_CLOSE); //--- Market data minimum_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); //--- End of initilization return(INIT_SUCCEEDED); }
Im Anschluss daran werden wir wichtige Hilfsfunktionen definieren, um unseren Code in kleinere, besser handhabbare Segmente aufzuteilen. Die erste Funktion, die wir erstellen werden, wird für die Aktualisierung unserer Marktdaten zuständig sein.
//+------------------------------------------------------------------+ //|This function will update the price and other technical data | //+------------------------------------------------------------------+ void update_technical_data(void) { //--- Update the bid and ask prices ask_price = SymbolInfoDouble(_Symbol,SYMBOL_ASK); bid_price = SymbolInfoDouble(_Symbol,SYMBOL_BID); }
Anschließend müssen wir eine Funktion implementieren, die den Initialisierungsvorgang steuert. Diese Funktion stellt sicher, dass wir die Trainingsdaten abrufen, unser Modell anpassen und die Prognosen in der richtigen Reihenfolge erstellen.
//+------------------------------------------------------------------+ //|This function will start training our model | //+------------------------------------------------------------------+ void model_initialize(void) { //--- First we have to fetch the input and output data Print("Initializing the model"); int input_start = 1 + (look_ahead * 2); int output_start = 1+ look_ahead; fetch_input_data(input_start,fetch); fetch_output_data(output_start,fetch); //--- Fit the model fit_lda_model(); }
Anschließend definieren wir die Funktion, die für das Abrufen der Eingabedaten zum Trainieren unseres Modells verantwortlich ist. Es ist wichtig zu beachten, dass der Input des Modells aus dem aktuellen Zustand des Marktes besteht — insbesondere aus der Zone, in der sich der Markt derzeit befindet. Das Modell prognostiziert dann, in welche Zone sich der Markt als Nächstes bewegen wird.
//+------------------------------------------------------------------+ //|This function will fetch the inputs for our model | //+------------------------------------------------------------------+ void fetch_input_data(int f_start,int f_fetch) { //--- This function will fetch input data for our model Print("Fetching input data"); //--- The input for our model will be the current state of the market //--- To know the current state of the market, we have to first update our indicator readings bband_mid_reading.CopyIndicatorBuffer(bband_handler,0,f_start,f_fetch); bband_high_reading.CopyIndicatorBuffer(bband_handler,1,f_start,f_fetch); bband_low_reading.CopyIndicatorBuffer(bband_handler,2,f_start,f_fetch); vector historical_prices; historical_prices.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,f_start,f_fetch); //--- Reshape the input data input_data.Resize(f_fetch); //--- Now we will input the state of the market for(int i = 0; i < f_fetch;i++) { //--- Are we above the bollinger bands entirely? if(historical_prices[i] > bband_high_reading[i]) { input_data[i] = 1; } //--- Are we between the upper and mid band? else if((historical_prices[i] < bband_high_reading[i]) && (historical_prices[i] > bband_mid_reading[i])) { input_data[i] = 2; } //--- Are we between the mid and lower band? else if((historical_prices[i] < bband_mid_reading[i]) && (historical_prices[i] > bband_low_reading[i])) { input_data[i] = 3; } //--- Are we below the bollinger bands entirely? else if(historical_prices[i] < bband_low_reading[i]) { input_data[i] = 4; } } //--- Show the input data Print(input_data); }
Nun benötigen wir eine Funktion zum Abrufen der Ausgabedaten für unser Modell. Diese Aufgabe ist komplizierter als das Abrufen der Eingabedaten. Wir müssen nicht nur die Endzone aufzeichnen, in der der Preis endete, sondern auch verfolgen, wie oft jede Zone der Ausgang war. Diese Zahl ist entscheidend für die spätere Schätzung der Parameter unseres LDA-Modells.
Von diesem Punkt an sind wir bereit, unser LDA-Modell anzupassen. Für die Anpassung des Modells stehen verschiedene Methoden zur Verfügung; wir werden uns heute auf einen bestimmten Ansatz konzentrieren.
//+---------------------------------------------------------------------+ //|Fetch the output data for our model | //+---------------------------------------------------------------------+ void fetch_output_data(int f_start,int f_fetch) { //--- The output for our model will be the state of the market //--- To know the state of the market, we have to first update our indicator readings Print("Fetching output data"); bband_mid_reading.CopyIndicatorBuffer(bband_handler,0,f_start,(f_fetch)); bband_high_reading.CopyIndicatorBuffer(bband_handler,1,f_start,(f_fetch)); bband_low_reading.CopyIndicatorBuffer(bband_handler,2,f_start,(f_fetch)); vector historical_prices; historical_prices.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,f_start,f_fetch); //--- First we have to ensure that the class count has been reset total_class_count[0] = 0; total_class_count[1] = 0; total_class_count[2] = 0; total_class_count[3] = 0; //--- Now we need to resize the matrix ArrayResize(training_output_array,f_fetch); //--- Now we will input the state of the market to our output vector for(int i =0 ; i < f_fetch;i++) { //--- Are we above the bollinger bands entirely? if(historical_prices[i] > bband_high_reading[i]) { training_output_array[i] = 1; total_class_count[0] += 1; } //--- Are we between the upper and mid band? else if((historical_prices[i] < bband_high_reading[i]) && (historical_prices[i] > bband_mid_reading[i])) { training_output_array[i] = 2; total_class_count[1] += 1; } //--- Are we between the mid and lower band? else if((historical_prices[i] < bband_mid_reading[i]) && (historical_prices[i] > bband_low_reading[i])) { training_output_array[i] = 3; total_class_count[2] += 1; } //--- Are we below the bollinger bands entirely? else if(historical_prices[i] < bband_low_reading[i]) { training_output_array[i] = 4; total_class_count[3] += 1; } } //--- Show the output data Print("Final state of output vector"); ArrayPrint(training_output_array); //--- Show the total number of times each class appeared as the target. Print(total_class_count); }
Das Verfahren ist etwas kompliziert und bedarf einer ausführlichen Erklärung. Zunächst berechnen wir die Gesamtsumme aller Eingabewerte, die den einzelnen Klassen in der Ausgabe entsprechen. Zum Beispiel berechnen wir für jeden Fall, bei dem das Ziel 1 war, die Summe aller Eingabewerte, die einer Ausgabe von 1 entsprechen, und so weiter für jede Ausgabeklasse. Anschließend wird der Mittelwert von X für jede Klasse berechnet. Bei mehreren Eingaben würden wir den Mittelwert für jede Eingabe berechnen. Anschließend wird die Wahrscheinlichkeit bestimmt, mit der jede Klasse auf der Grundlage der Trainingsdaten als tatsächliches Ziel erscheint. Anschließend wird die Varianz von X für jede Klasse von y berechnet. Schließlich aktualisieren wir unsere Flags, um den Abschluss des Trainingsverfahrens anzuzeigen.
//+------------------------------------------------------------------+ //|Fit the LDA model | //+------------------------------------------------------------------+ void fit_lda_model(void) { //--- To fit the LDA model, we first need to know the mean value for each our inputs for each of our 4 classes double sum_class_one = 0; double sum_class_two = 0; double sum_class_three = 0; double sum_class_four = 0; //--- In this case we only have 1 input for(int i = 0; i < fetch;i++) { //--- Class 1 if(training_output_array[i] == 1) { sum_class_one += input_data[i]; } //--- Class 2 else if(training_output_array[i] == 2) { sum_class_two += input_data[i]; } //--- Class 3 else if(training_output_array[i] == 3) { sum_class_three += input_data[i]; } //--- Class 4 else if(training_output_array[i] == 4) { sum_class_four += input_data[i]; } } //--- Show the sums Print("Class 1: ",sum_class_one," Class 2: ",sum_class_two," Class 3: ",sum_class_three," Class 4: ",sum_class_four); //--- Calculate the mean value for each class mean_values[0] = sum_class_one / fetch; mean_values[1] = sum_class_two / fetch; mean_values[2] = sum_class_three / fetch; mean_values[3] = sum_class_four / fetch; Print("Mean values"); Print(mean_values); //--- Now we need to calculate class probabilities for(int i=0;i<classes;i++) { probability_values[i] = total_class_count[i] / fetch; } Print("Class probability values"); Print(probability_values); //--- Calculating the variance Print("Calculating the variance"); //--- Next we need to calculate the variance of the inputs within each class of y. //--- This process can be simplified into 2 steps //--- First we calculate the difference of each instance of x from the group mean. double squared_difference[4]; for(int i =0; i < fetch;i++) { //--- If the output value was 1, find the input value that created the output //--- Calculate how far that value is from it's group mean and square the difference if(training_output_array[i] == 1) { squared_difference[0] = MathPow((input_data[i]-mean_values[0]),2); } else if(training_output_array[i] == 2) { squared_difference[1] = MathPow((input_data[i]-mean_values[1]),2); } else if(training_output_array[i] == 3) { squared_difference[2] = MathPow((input_data[i]-mean_values[2]),2); } else if(training_output_array[i] == 4) { squared_difference[3] = MathPow((input_data[i]-mean_values[3]),2); } } //--- Show the squared difference values Print("Squared difference value for each output value of y"); ArrayPrint(squared_difference); //--- Next we calculate the variance as the average squared difference from the mean variance = (1.0/(fetch - 4.0)) * (squared_difference[0] + squared_difference[1] + squared_difference[2] + squared_difference[3]); Print("Variance: ",variance); //--- Update our flags to denote the model has been trained model_trained = true; training_procedure_running = false; } //+------------------------------------------------------------------+
Um mit unserem Modell eine Prognose zu erstellen, holen wir uns zunächst die neuesten Inputdaten vom Markt. Mit diesen Eingabedaten berechnen wir die Diskriminanzfunktion für jede mögliche Klasse. Die Klasse mit dem höchsten Diskriminanzfunktionswert ist die vorhergesagte Klasse.
In MQL5 bieten Arrays eine nützliche Funktion namens ArrayMaximum(), die den Index des größten Wertes in einem 1D-Array zurückgibt. Da Arrays mit einem Null-Index versehen sind, addieren wir 1 zum Ergebnis von ArrayMaximum(), um die vorhergesagte Klasse zu erhalten.
//+------------------------------------------------------------------+ //|This function will obtain forecasts from our model | //+------------------------------------------------------------------+ int model_forecast(void) { //--- First we need to fetch the most recent input data fetch_input_data(0,1); //--- Update the current state of the system current_state = input_data[0]; //--- We need to calculate the discriminant function for each class //--- The predicted class is the one with the largest discriminant function Print("Calculating discriminant values."); for(int i = 0; i < classes; i++) { discriminant_values[i] = (input_data[0] * (mean_values[i]/variance) - (MathPow(mean_values[i],2)/(2*variance)) + (MathLog(probability_values[i]))); } ArrayPrint(discriminant_values); return(ArrayMaximum(discriminant_values) + 1); }
Nachdem wir mit unserem Modell eine Prognose erhalten haben, müssen wir diese interpretieren und entsprechende Entscheidungen treffen. Wie bereits erwähnt, werden unsere Handelssignale generiert, wenn das Modell vorhersagt, dass sich der Preis in eine andere Zone bewegen wird:
- Zeigt die Prognose eine Bewegung von Zone 1 nach Zone 2 an, wird ein Verkaufssignal ausgelöst.
- Umgekehrt bedeutet eine Vorhersage des Übergangs von der Zone 4 zur Zone 3 ein Kaufsignal.
- Deutet die Prognose jedoch darauf hin, dass der Kurs in derselben Zone verbleibt (z. B. von Zone 1 nach Zone 1), so ist dies kein Einstiegssignal.
//+--------------------------------------------------------------------+ //|This function will interpret out model's forecast and execute trades| //+--------------------------------------------------------------------+ void find_entry(void) { //--- If the model's forecast is not equal to the current state then we are interested //--- Otherwise whenever the model forecasts that the state will remain the same //--- We are uncertain whether price levels will rise or fall if(forecast != current_state) { //--- If the model forecasts that we will move from a small state to a greater state //--- That is from 1 to 2 or from 2 to 4 then that is a down move if(forecast > current_state) { Trade.Sell(minimum_volume * lot_multiple,_Symbol,bid_price,(bid_price + stop_loss_values),(bid_price - stop_loss_values)); } //--- Otherwise we have a buy setup else { Trade.Buy(minimum_volume * lot_multiple,_Symbol,ask_price,(ask_price - stop_loss_values),(ask_price +stop_loss_values)); } } //--- Otherwise we do not have an entry signal from our model }
Schließlich ist die Ereignisbehandlung duch OnTick() für die Verwaltung des Ereignisflusses verantwortlich und stellt sicher, dass wir nur handeln, wenn unser Modell trainiert wurde und unsere anderen Handelsbedingungen erfüllt sind.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- We must always update market data update_technical_data(); //--- First we must ensure our model has been trained switch(model_trained) { //--- Our model has been trained case(true): //--- If we have no open positions, let's obtain a forecast from our model if(PositionsTotal() == 0) { //--- Obtaining a forecast forecast = model_forecast(); Comment("Model forecast: ",forecast); //--- Find an entry setup find_entry(); } break; //--- End of case 1 //--- Our model has not been trained default: //--- We haven't started the training procedure! if(!training_procedure_running) { Print("Our model has not been trained, starting the training procedure now."); //--- Initialize the model model_initialize(); } break; //--- End of default case } } //+------------------------------------------------------------------+
Abb. 11: Unser Handelssystem in Aktion.
Beschränkungen
Bis zu diesem Punkt hat unsere Strategie eine wesentliche Einschränkung: Sie kann schwierig zu interpretieren sein. Wenn unser Modell vorhersagt, dass der Preis in der gleichen Zone bleiben wird, ist nicht klar, ob die Preise steigen oder fallen werden. Dieser Kompromiss ergibt sich aus unserer Entscheidung, die Marktzustände in vier verschiedene Zonen einzuteilen, was die Genauigkeit erhöht, aber die Transparenz im Vergleich zur direkten Vorhersage von Preisbewegungen beeinträchtigt. Außerdem erzeugt dieser Ansatz weniger Handelssignale, weil wir warten müssen, bis das Modell eine Zonenänderung vorhersagt, bevor wir handeln.
Schlussfolgerung
Zusammenfassend lässt sich sagen, dass unsere Strategie die Leistungsfähigkeit des maschinellen Lernens, insbesondere der linearen Diskriminanzanalyse (LDA), integriert mit Bollinger-Bändern für Handelssignale nutzt. Unser Ansatz bietet zwar eine höhere Genauigkeit, geht aber zu Lasten der Transparenz. Alles in allem sind Händler besser dran, wenn sie Preisänderungen vorhersagen, als wenn sie Durchbrüche von Bollinger-Bänder vorhersagen.
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/15336
- 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.