Rezepte MQL5 - Handelssignale der gleitenden Kanäle
Einführung
Der vorherige Artikel «Rezepte MQL5 - Programmierung der gleitenden Kanäle» beschreibt eine Methode zum Zeichnen äquidistanter Kanäle, auch benannt als gleitende Kanälen. Um das Aufgabe zu lösen, verwenden wir «äquidistanter Kanal» und die Möglichkeiten des OOP.
Dieser Artikel konzentriert sich auf Signale, die bei der Verwendung dieser Kanäle identifiziert werden können. Versuchen wir also eine Handelsstrategie aus diesen Signalen zu entwerfen.
Es gibt mehrere Artikel für MQL5, die das Erzeugen von Handelssignalen unter Verwendung der Mittel der Standardbibliothek beschreiben. Hoffentlich ergänzt dieser Artikel die Materialien und erweitert des Nutzers Wissen über die Standardklassen.
Anfänger sind eingeladen, mit dieser Strategie das Vorgehen vom Einfachen zum Komplexen zu lernen. Zuerst eine einfache Strategie erstellen, die dann durch zusätzliche Elemente komplexer wird.
1. Der Indikator eines äquidistanten Kanals
Im vorigen Artikel über gleitende Kanäle zeichnete der Expert Advisor die Kanäle selbst als grafische Objekte auf den Chart. Dessen Ansatz erleichtert auf der einen Seite die Aufgabe für Programmierer, aber auf der anderen Seite macht er einige Dinge unmöglich. Zum Beispiel, wenn der EA optimiert wird, kann er keine grafische Objekte erkennen, da es dann keinen Chart gibt. Gemäß den Beschränkungen während des Testens:
Grafische Objekte beim Testen
Während des Testens/Optimierung wird die Konstruktion von grafischen Objekten nicht ausgeführt. Wenn man auf die Eigenschaften des geschaffenen Objektes während des Testens/Optimierung zugreift, erhält der Expert Advisor eine Null.
Diese Einschränkung gilt nicht für das Testen im visuellen Modus. |
Daher wird ein anderer Ansatz verwendet, um einen Indikator zu erstellen, der sowohl Fraktale wie den Kanal berücksichtigt.
Dieser Indikator heißt EquidistantChannels. Er besteht im Wesentlichen aus zwei Blöcken. Der Erste berechnet den Puffer der Fraktale, der Zweite den des Kanals.
Hier ist der Code der Funktion OnCalculate.
//+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- Wenn es noch keine Bars eines vorherigen Aufrufs gibt if(prev_calculated==0) { //--- Initialisieren der Puffer ArrayInitialize(gUpFractalsBuffer,0.); ArrayInitialize(gDnFractalsBuffer,0.); ArrayInitialize(gUpperBuffer,0.); ArrayInitialize(gLowerBuffer,0.); ArrayInitialize(gNewChannelBuffer,0.); } //--- Berechnung der Fraktale [Start] int startBar,lastBar; //--- if(rates_total<gMinRequiredBars) { Print("Not enough data for calculation"); return 0; } //--- if(prev_calculated<gMinRequiredBars) startBar=gLeftSide; else startBar=rates_total-gMinRequiredBars; //--- lastBar=rates_total-gRightSide; for(int bar_idx=startBar; bar_idx<lastBar && !IsStopped(); bar_idx++) { //--- if(isUpFractal(bar_idx,gMaxSide,high)) gUpFractalsBuffer[bar_idx]=high[bar_idx]; else gUpFractalsBuffer[bar_idx]=0.0; //--- if(isDnFractal(bar_idx,gMaxSide,low)) gDnFractalsBuffer[bar_idx]=low[bar_idx]; else gDnFractalsBuffer[bar_idx]=0.0; } //--- Berechnung der Fraktale [Ende] //--- Berechnung der Kanalgrenzen [Start] if(prev_calculated>0) { //--- Falls noch nicht initialisiert wurde if(!gFracSet.IsInit()) if(!gFracSet.Init( InpPrevFracNum, InpBarsBeside, InpBarsBetween, InpRelevantPoint, InpLineWidth, InpToLog )) { Print("Fractal set initialization error!"); return 0; } //--- Berechnung gFracSet.Calculate(gUpFractalsBuffer,gDnFractalsBuffer,time, gUpperBuffer,gLowerBuffer, gNewChannelBuffer ); } //--- Berechnung der Kanalgrenzen [Ende] //--- Rückgabe des Wertes von prev_calculated für den nächsten Aufruf return rates_total; }
Der Block mit der Berechnung des Fraktalpuffers ist gelb hervorgehoben und der Block mit der Berechnung des Kanalpuffers grün. Es ist leicht zu bemerken, dass der zweite Block nicht sofort durchlaufen wird, sondern erst beim nächsten Aufruf der Funktion. Diese Umsetzung des zweiten Blocks ermöglicht die Abfrage eines gefüllten Fraktalpuffers.
Jetzt ein paar Worte über Fraktale — das CFractalSet Objekt. Wegen der Änderungen der Anzeige des Kanals, war es auch notwendig die Klasse CFractalSet anzupassen. Die wichtigste Methode ist CFractalSet::Calculate, die die Werte des Kanalpuffers berechnet. Der Code befindet sich in der Datei CFractalPoint.mqh.
Jetzt haben wir die Basis — einen Absender der Signale des äquidistanten Kanals. Das Funktionieren des Indikators wird im Video gezeigt.
2. Basisstrategie
Beginnen wir also mit etwas Einfachem, das mit der Hilfe von OOP verbessert und überarbeitet werden kann. Nehmen wir eine ganz einfach Strategie.
Sie hat nur ein paar simple Handelsregeln. Der Markteintritt wird durch die Kanalgrenzen bestimmt. Berührt der Preis die untere Grenze, eröffnen wir eine Kaufposition, die obere - eine Verkaufposition. Fig. 1 zeigt die Berührung der unteren Grenze und der EA kauft eine bestimmte Menge. Die Handelsebenen (StopLoss und TakeProfit) sind fix und werden automatisch platziert. Gibt es eine offene Position, wird ein erneutes Signal ignoriert.
Fig.1Eröffnungssignal
Ich möchte darauf hinweisen, dass die Standardbibliothek stark gewachsen ist. Es gibt bereits viele fertige, verwendbare Klassen. Versuchen wir zuerst eine «Verbindung» zur Signalklasse CExpertSignal. Gemäß der Dokumentation ist es eine Basisklasse zur Erstellung eines Handelssignal Generators.
Diese Klasse trägt ihren Namen zurecht. Es nicht CTradeSignal und nicht CSignal, sondern die Signalklasse, die zur Verwendung in einem EA bestimmt ist — CExpertSignal.
Ich werde ihren Inhalt nicht besprechen. Der Artikel «Der MQL5 Assistent: Wie man ein Modul an Handelssignalen erzeugt» beschreibt die Methoden dieser Signalklassen.
2.1 Die Signalklasse CSignalEquidChannelsignal
Daher ist die abgeleitete Signalklasse wie folgt:
//+------------------------------------------------------------------+ //| Class CSignalEquidChannel | //| Zweck: Klasse der Handelssignale auf Basis äquidistanter | //| Kanäle. | //| Abgeleitet aus der Klasse CExpertSignal. | //+------------------------------------------------------------------+ class CSignalEquidChannel : public CExpertSignal { protected: CiCustom m_equi_chs; // Indikatorobjekt "EquidistantChannels" //--- anpassbare Parameter int m_prev_frac_num; // vorheriges Fraktal bool m_to_plot_fracs; // Fraktal anzeigen? int m_bars_beside; // Bars rechts/links des Fraktals int m_bars_between; // Zwischenbars ENUM_RELEVANT_EXTREMUM m_relevant_pnt; // relevanter Punkt int m_line_width; // Linienstärke bool m_to_log; // Logeinträge? double m_pnt_in; // interne Toleranz, Points double m_pnt_out; // externe Toleranz, Points bool m_on_start; // Signal für den Start //--- Berechnung double m_base_low_price; // Basis Tiefstpreis double m_base_high_price; // Basis Höchstpreis double m_upper_zone[2]; // obere Zone: [0]-interne Toleranz, [1]-externe double m_lower_zone[2]; // untere Zone datetime m_last_ch_time; // Zeitpunkt des Auftretens des letzten Kanals //--- "Wichtungen" des Modells (0-100) int m_pattern_0; // "Berühren des unteren Kanals - kaufen, des oberen - verkaufen" //--- === Methods === --- public: //--- Constructor/Destructor void CSignalEquidChannel(void); void ~CSignalEquidChannel(void){}; //--- Methoden für die regelbaren Parametereinstellungen void PrevFracNum(int _prev_frac_num) {m_prev_frac_num=_prev_frac_num;} void ToPlotFracs(bool _to_plot) {m_to_plot_fracs=_to_plot;} void BarsBeside(int _bars_beside) {m_bars_beside=_bars_beside;} void BarsBetween(int _bars_between) {m_bars_between=_bars_between;} void RelevantPoint(ENUM_RELEVANT_EXTREMUM _pnt) {m_relevant_pnt=_pnt;} void LineWidth(int _line_wid) {m_line_width=_line_wid;} void ToLog(bool _to_log) {m_to_log=_to_log;} void PointsOutside(double _out_pnt) {m_pnt_out=_out_pnt;} void PointsInside(double _in_pnt) {m_pnt_in=_in_pnt;} void SignalOnStart(bool _on_start) {m_on_start=_on_start;} //--- Methode zur Anpassung der "Wichtungen" im Marktmodell void Pattern_0(int _val) {m_pattern_0=_val;} //--- Überprüfung der Einstellung virtual bool ValidationSettings(void); //--- Methode zum Erstellender des Indikators und der Zeitreihen virtual bool InitIndicators(CIndicators *indicators); //--- Prüfmethoden des Marktmodells virtual int LongCondition(void); virtual int ShortCondition(void); virtual double Direction(void); //--- protected: //--- Methode zur Initialisierung des Indikators bool InitCustomIndicator(CIndicators *indicators); //- Abfrage der oberen Kanalgrenze double Upper(int ind) {return(m_equi_chs.GetData(2,ind));} //- Abfrage der unteren Kanalgrenze double Lower(int ind) {return(m_equi_chs.GetData(3,ind));} //- Abfrage nach der Existenz des Kanals double NewChannel(int ind) {return(m_equi_chs.GetData(4,ind));} }; //+------------------------------------------------------------------+
Ein paar Kleinigkeiten noch.
Das Hauptsignal ist der äquidistante Kanal. Und der ist augenblicklich der Einzige. Weitere gibt es nicht. Hier verfügt diese Klasse über eine Klasse, die mit dem technischen Indikator des Typs — CiCustom arbeitet.
Das Basismodell wird als Signalmodell verwendet: "Berühren des unteren Kanals - kaufen, des oberen - verkaufen". Da eine punktgenaue Präzision praktisch unerreichbar ist, wird eine regelbare Toleranz verwendet. Der Parameter der externe Toleranz m_pnt_out bestimmt, wie weit der Preis die Kanalgrenze überschreiten darf und der der internen Toleranz m_pnt_in — wie weit der Preis innerhalb Grenze liegen darf. Die Logik ist ganz einfach. Angenommen der Preis erreicht die Grenze nicht ganz oder ist nur etwas darüber. Fig.2 zeigt schematisch diese Toleranz. Gelangt der Preis von unten dahin, wird das Signal ausgelöst.
Fig.2 Auslösung des Signals
Das Array m_upper_zone[2] enthält die obere und m_lower_zone[2] die untere Toleranzgrenzen.
Der Preis von $1,11552 ist ein Beispiel für eine obere Kanalgrenze (rote Linie). Der Preise $1,11452 ist das untere Limit der Toleranz und $1,11702 das der oberen. Also ist die Größe der externen Toleranz 150 Points und die der internen 100 Points. Der Kurs ist als blaue Linie angezeigt.
Der Parameter m_on_start erlaubt es, das aller erste Signal zu ignorieren, wenn der EA auf dem Chart läuft und der Kanal bereits gezeichnet worden ist. Wenn dieses Signal zurückgesetzt wird, wird der EA nur mit den nächsten Kanal arbeiten und nicht mit den augenblicklichen Handelssignalen.
Die Parameter m_base_low_price und m_base_high_price sichern die Werte des Hochs und Tiefs der aktuellen Bar. Das ist die Bar-Null, wenn der EA auf jedem Tick läuft, oder die vorherige Bar, wenn der Handel nur beim Erscheinen einer neuen Bar erlaubt ist.
Jetzt ein paar Worte über die Methoden. Es sollte angemerkt werden, dass der Entwickler genügend Freiraum für eine Weiterentwicklung ließ, da etwa die Hälfte virtuelle Methoden sind. Das bedeutet, dass das Verhalten der abgeleiteten Klassen ganz nach eigenen Wünschen bestimmt werden kann.
Beginnen wir mit der Methode Direction(), die ein qualitative Abschätzung einer möglichen Handelsrichtung liefert:
//+------------------------------------------------------------------+ //| Bestimmung der "Wichtungen" der Richtung | //+------------------------------------------------------------------+ double CSignalEquidChannel::Direction(void) { double result=0.; //--- Erscheinen eines neuen Kanals datetime last_bar_time=this.Time(0); bool is_new_channel=(this.NewChannel(0)>0.); //--- Wenn das Signal des ersten Kanals ignoriert wird if(!m_on_start) //--- Wenn der erste Kanal ganz normal bei der Initialisierung angezeigt wird if(m_prev_frac_num==3) { static datetime last_ch_time=0; //--- Wenn ein neuer Kanal erscheint if(is_new_channel) { last_ch_time=last_bar_time; //--- Wenn das der erste Start ist if(m_last_ch_time==0) //--- Sichere den Zeitpunkt der Bar des Erscheinens des ersten Kanals m_last_ch_time=last_ch_time; } //--- Wenn die Zeit passt if(m_last_ch_time==last_ch_time) return 0.; else //--- Lösche das Signal m_on_start=true; } //--- Index der aktuellen Bar int actual_bar_idx=this.StartIndex(); //--- Grenzsetzung double upper_vals[2],lower_vals[2]; // [0]-bar preceding the actual, [1]-actual bar ArrayInitialize(upper_vals,0.); ArrayInitialize(lower_vals,0.); for(int idx=ArraySize(upper_vals)-1,jdx=0;idx>=0;idx--,jdx++) { upper_vals[jdx]=this.Upper(actual_bar_idx+idx); lower_vals[jdx]=this.Lower(actual_bar_idx+idx); if((upper_vals[jdx]==0.) || (lower_vals[jdx]==0.)) return 0.; } //--- Preisabfrage double curr_high_pr,curr_low_pr; curr_high_pr=this.High(actual_bar_idx); curr_low_pr=this.Low(actual_bar_idx); //--- Nach Erhalt des Preises if(curr_high_pr!=EMPTY_VALUE) if(curr_low_pr!=EMPTY_VALUE) { //--- sichern des Preises m_base_low_price=curr_low_pr; m_base_high_price=curr_high_pr; //--- Bestimme Preise der Toleranzzone //--- obere Zone: [0]-interne Toleranz, [1]-externe this.m_upper_zone[0]=upper_vals[1]-m_pnt_in; this.m_upper_zone[1]=upper_vals[1]+m_pnt_out; //--- untere Zone: [0]-interne Toleranz, [1]-externe this.m_lower_zone[0]=lower_vals[1]+m_pnt_in; this.m_lower_zone[1]=lower_vals[1]-m_pnt_out; //--- Normalisierung for(int jdx=0;jdx<ArraySize(m_lower_zone);jdx++) { this.m_lower_zone[jdx]=m_symbol.NormalizePrice(m_lower_zone[jdx]); this.m_upper_zone[jdx]=m_symbol.NormalizePrice(m_upper_zone[jdx]); } //--- Prüfung ob die Zonen konvergieren if(this.m_upper_zone[0]<=this.m_lower_zone[0]) return 0.; //--- Ergebnis result=m_weight*(this.LongCondition()-this.ShortCondition()); } //--- return result; } //+------------------------------------------------------------------+
Der erste Block der Methode prüft, ob der erste Kanal auf dem Chart, wenn es ihn denn gibt, ignoriert werden muss.
Der zweit Block fragt nach den Preisen und bestimmt die Toleranzzonen. Das ist die Prüfung der Konvergierung der Zonen. Wenn der Kanal zu eng oder die Toleranz zu groß ist, ist es wahrscheinlich, dass die Preise in beide Zonen gelangen. Daher müssen wir das auch berücksichtigen.
Die Ziellinie ist markiert in blau. Hier machen wir eine quantitative Schätzung der Handelsrichtung, wenn es denn eine gibt.
Betrachten wir jetzt mal die Methode LongCondition().
//+------------------------------------------------------------------+ //| Prüfe die Kaufbedingung | //+------------------------------------------------------------------+ int CSignalEquidChannel::LongCondition(void) { int result=0; //--- Haben wir einen Tiefstpreis if(m_base_low_price>0.) //--- Wenn der Tiefstpreis nahe der unteren Grenze ist if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1])) { if(IS_PATTERN_USAGE(0)) result=m_pattern_0; } //--- return result; } //+------------------------------------------------------------------+
Für einen Kauf, prüfe, ob der Preis in die untere Toleranzzone reicht. Ist das der Fall, prüfe die Erlaubnis das Marktmodell zu aktivieren. Weitere Details über die Struktur des Typs "IS_PATTERN_USAGE(k)" findet sich im Artikel «Handelssignal-Generator auf Grundlage eines angepassten Indikators».
Die Methode ShortCondition() arbeitet ähnlich der obigen. Der Fokus liegt diesmal nur auf der oberen Zone.
//+------------------------------------------------------------------+ //| Prüfe die Verkaufbedingung | //+------------------------------------------------------------------+ int CSignalEquidistantChannel::ShortCondition(void) { int result=0; //--- Haben wir einen Höchstpreis if(m_base_high_price>0.) //--- Wenn der Höchstpreis nahe der oberen Grenze ist if((m_base_high_price>=m_upper_zone[0]) && (m_base_high_price<=m_upper_zone[1])) { if(IS_PATTERN_USAGE(0)) result=m_pattern_0; } //--- return result; } //+------------------------------------------------------------------+
Diese Klasse initialisiert einen Nutzerindikator unter Verwendung der Methode InitCustomIndicator():
//+------------------------------------------------------------------+ //| Initialisierung des Nutzerindikators | //+------------------------------------------------------------------+ bool CSignalEquidChannel::InitCustomIndicator(CIndicators *indicators) { //--- füge ein Objekt hinzu if(!indicators.Add(GetPointer(m_equi_chs))) { PrintFormat(__FUNCTION__+": error adding object"); return false; } //--- bestimme die Parameter des Indikators MqlParam parameters[8]; parameters[0].type=TYPE_STRING; parameters[0].string_value="EquidistantChannels.ex5"; parameters[1].type=TYPE_INT; parameters[1].integer_value=m_prev_frac_num; // 1) previous fractals parameters[2].type=TYPE_BOOL; parameters[2].integer_value=m_to_plot_fracs; // 2) display fractals? parameters[3].type=TYPE_INT; parameters[3].integer_value=m_bars_beside; // 3) bars on the left/right of fractal parameters[4].type=TYPE_INT; parameters[4].integer_value=m_bars_between; // 4) intermediate bars parameters[5].type=TYPE_INT; parameters[5].integer_value=m_relevant_pnt; // 5) relevant point parameters[6].type=TYPE_INT; parameters[6].integer_value=m_line_width; // 6) line width parameters[7].type=TYPE_BOOL; parameters[7].integer_value=m_to_log; // 7) keep the log? //---Initialisierung des Objektes if(!m_equi_chs.Create(m_symbol.Name(),_Period,IND_CUSTOM,8,parameters)) { PrintFormat(__FUNCTION__+": error initializing object"); return false; } //--- Anzahl der Puffer if(!m_equi_chs.NumBuffers(5)) return false; //--- ok return true; } //+------------------------------------------------------------------+
Der erste Wert des Arrays der Parameter muss der Name des Indikators als Zeichenkette sein.
Die Klasse hat auch eine virtuelle Methode ValidationSettings(). Diese ruft eine ähnliche Methode des Vorfahren auf und prüft, ob die Parameter des Kanalindikators korrekt sind. Es gibt auch eine Methode, die die Werte des entsprechenden Puffers des Nutzerindikators zurück liefert.
Soweit ist das alles in Bezug zur abgeleiteten Signalklasse.
2.2 Klasse der Handelsstrategie CEquidChannelExpert
Umsetzung der Basisidee verlangt eine von der Standardklasse CExpert abgeleitete Klasse. Im Augenblick ist der Code so kompakt wie möglich, weil nur das Verhalten der Hauptmethode Processing() geändert werden muss. Sie ist virtuell und garantiert die Möglichkeit, damit jede Strategie schreiben zu können.
//+------------------------------------------------------------------+ //| Klasse CEquidChannelExpert. | //| Zweck: Klasse eines EA der den äquidistanten Kanal handelt. | //| Abgeleitet von der Klasse CExper. | //+------------------------------------------------------------------+ class CEquidChannelExpert : public CExpert { //--- === Data members === --- private: //--- === Methods === --- public: //--- Constructor/Destructor void CEquidChannelExpert(void){}; void ~CEquidChannelExpert(void){}; protected: virtual bool Processing(void); }; //+------------------------------------------------------------------+
Hier ist die Methode selbst:
//+------------------------------------------------------------------+ //| Hauptmodul | //+------------------------------------------------------------------+ bool CEquidChannelExpert::Processing(void) { //--- Berechnung der Richtung m_signal.SetDirection(); //--- Prüfe auf offene Positionen if(!this.SelectPosition()) { //--- Positionseröffnung if(this.CheckOpen()) return true; } //--- wenn es keine Handelsoperationen gibt return false; }
Ist alles ganz einfach. Zuerst schätzt das Signalobjekt die mögliche Handelsrichtung, dann wird die Existenz offener Positionen geprüft. Gibt es keine, ist es möglich eine zu öffnen. Gibt es eine offene Position, Ende der Methode.
Der Code dieser Basisstrategie ist umgesetzt in der Datei BaseChannelsTrader.mq5.
Beispiele dieser Basisstrategie zeigt das folgende Video.
Fig.3 Ergebnisse der Basisstrategie für 2013-2015.
Der EA lief im Strategietester auf EURUSD im Zeitrahmen 1 Stunde. Auf dem Guthabenchart wird deutlich, dass in bestimmten Intervallen die Basisstrategie nach dem Prinzip "Sägezahn" funktioniert: auf einen Verlierer folgt ein Gewinner. Die Parameter dieses Tests wurden in der Datei base_signal.set gesichert. Enthalten sind auch die Parameter des Kanals, die in allen Versionen dieser Strategie unverändert bleiben.
Hier und weiter unten wird die Testmethode "Jeder Tick" verwendet.
Im Wesentlichen, gibt es 2 Möglichkeiten zur Verbesserung der Handelsergebnisse dieser Strategie. Zuerst die Optimierung, die jene Parameterauswahl findet, die den Gewinn maximiert, etc. Als zweites können wir Faktoren finden, die den Erfolg des EA beeinflussen. Die erste Methode belässt die Handelslogik unverändert, die ändert sie.
Im nächsten Teil wird die Basisstrategie verändert, um einen größeren Erfolg zu erreichen.
3. Erfolgsfaktoren
Ein paar Worte zum Ansatz. Es könnte bequem sein, alle Dateien der Strategie in einem einzelnen, eindeutigen Projektverzeichnis zu speichern. Daher ist alles in einem Unterverzeichnis des Basisverzeichnisses eingeordnet (Fig.4) und so weiter.
Fig.4 Beispiel der Verzeichnisstruktur des Projektes der Kanalstrategie
Weiters nehmen wir an, jeder neue Faktor bedeutet eine neue Stufe der veränderten Quelldateien, die den EA ausmachen.
3.1 Verwendung von Trailingstopps
Vor dem Start empfehlen wir, das Trailing der Strategie hinzuzufügen. Es ist ein Objekt der Klasse CTrailingFixedPips, die es erlaubt, die offenen Positionen mit einem fixen "Abstand" (in Points) zu kontrollieren. Die wird sowohl den StopLoss wie den TakeProfit nachziehen. Um das Nachziehen des TakeProfit zu deaktivieren, muss der entsprechende Parameter (InpProfitLevelPips) auf Null gesetzt werden.
Ändern wir den Code wie folgt:
Ergänzen wir eine Reihe von Parameter zum Experten der Datei ChannelsTrader1.mq5:
//--- sinput string Info_trailing="+===-- Trailing --====+"; // +===-- Trailing --====+ input int InpStopLevelPips=30; // Abstand des StopLoss, Pips input int InpProfitLevelPips=50; // Abstand des TakeProfit, Pips
Im Initialisierungsblock erstellen wir das Objekt CTrailingFixedPips und integrieren es in die Strategie und setzen die Parameter des Trailings.
//--- Trailing Objekt CTrailingFixedPips *trailing=new CTrailingFixedPips; if(trailing==NULL) { //--- Fehler printf(__FUNCTION__+": error creating trailing"); myChannelExpert.Deinit(); return(INIT_FAILED); } //--- Hinzufügen des Trailing Objektes if(!myChannelExpert.InitTrailing(trailing)) { //--- Fehler PrintFormat(__FUNCTION__+": error initializing trailing"); myChannelExpert.Deinit(); return INIT_FAILED; } //--- Trailing Parameter trailing.StopLevel(InpStopLevelPips); trailing.ProfitLevel(InpProfitLevelPips);
Da das Trailing verwendet wird, müssen wir auch die Hauptmethode CEquidChannelExpert::Processing() in der Datei EquidistantChannelExpert1.mqh ändern.
//+------------------------------------------------------------------+ //| Hauptmodul | //+------------------------------------------------------------------+ bool CEquidChannelExpert::Processing(void) { //--- Berechnung der Richtung m_signal.SetDirection(); //--- Keine Position if(!this.SelectPosition()) { //--- Positionseröffnung if(this.CheckOpen()) return true; } //--- Es gibt eine Position else { //--- Prüfung auf Änderbarkeit der Position if(this.CheckTrailingStop()) return true; } //--- wenn es keine Handelsoperationen gibt return false; }
Das war's Das Trailing wurde ergänzt. Die Dateien der aktualisierten Strategie werden in einem eigenen Unterverzeichnis ChannelsTrader1 gespeichert.
Überprüfen wir, ob die Änderung die Effektivität beeinflussten.
Daher wurden mehrere Optimierungen im Strategietester durchgeführt, über dasselbe Zeitintervall und den Parameterwerten der Basisstrategie. StopLoss und TakeProfit wurden angepasst:
Variable | Start | Step | Stop |
---|---|---|---|
Abstand des StopLoss, Pips | 0 | 10 | 100 |
Abstand des TakeProfit, Pips | 0 | 10 | 150 |
Die Ergebnisse der Optimierung finden sich in der Datei ReportOptimizer-signal1.xml. Das beste Ergebnis zeigt Fig.5, mit StopLoss = 0, und TakeProfit = 150.
Fig.5 Ergebnisse der Strategie mit Trailing für 2013-2015.
Es ist leicht zu sehen, dass das letzte Bild dem von Fig 3 ähnelt. Daher kann gesagt werden, die Verwendung von Trailing bringt keine Verbesserung.
3.2 Kanaltypen
Nehmen wir an, dass der Kanaltyp den Erfolg beeinflusst. Die allgemeine Idee ist: Es ist besser in einem absteigenden Kanal zu verkaufen und in einem steigenden zu kaufen. Ist der Kanal flach (keine Steigung), ist es möglich auf beiden Grenzen zu handeln.
Die Enumeration ENUM_CHANNEL_TYPE definiert den Kanaltyp:
//+------------------------------------------------------------------+ //| Kanaltyp | //+------------------------------------------------------------------+ enum ENUM_CHANNEL_TYPE { CHANNEL_TYPE_ASCENDING=0, // steigend CHANNEL_TYPE_DESCENDING=1, // fallend CHANNEL_TYPE_FLAT=2, // flach }; //+------------------------------------------------------------------+
Bestimmen wir Toleranzparameter für die Suche nach Kanaltypen im Initialisierungsblock der Quelldatei ChannelsTrader2.mq5 des EA.
//--- Filter Parameter filter0.PointsInside(_Point*InpPipsInside); filter0.PointsOutside(_Point*InpPipsOutside); filter0.TypeTolerance(_Point*InpTypePips); filter0.PrevFracNum(InpPrevFracNum); ...
Diese Parameter kontrollieren das Ausmaß der Preisänderung in Points. Angenommen das wären 7 Pips. Und wächst dann der Kanal um 6 Pip mit jeder Bar, reicht das nicht für einen steigenden Kanal. Er wird daher als flach klassifiziert (ohne Steigung).
Wir ergänzen diese Identifikation zur Methode Direction() des Quellcodes der Datei SignalEquidChannel2.mqh.
//--- Ist der Kanal neu if(is_new_channel) { m_ch_type=CHANNEL_TYPE_FLAT; // flat (not inclined) channel //--- Wenn die Toleranz gesetzt ist if(m_ch_type_tol!=EMPTY_VALUE) { //--- Kanaltyp //--- Ausmaß der Änderung double pr_speed_pnt=m_symbol.NormalizePrice(upper_vals[1]-upper_vals[0]); //--- Ist das Ausmaß groß genug if(MathAbs(pr_speed_pnt)>m_ch_type_tol) { if(pr_speed_pnt>0.) m_ch_type=CHANNEL_TYPE_ASCENDING; // ascending channel else m_ch_type=CHANNEL_TYPE_DESCENDING; // descending channel } } }
Zunächst wird ein Kanal als flach klassifiziert - weder steigend noch fallend. Wenn der Wert des Toleranz Parameters zur Identifizierung des Kanaltyps nicht bestimmt wurde, können wir auch das Ausmaß der Änderung nicht bestimmen.
Die Bedingung für einen Kauf beinhaltet auch die Prüfung, dass der Kanal nicht fällt.
//+------------------------------------------------------------------+ //| Prüfe die Kaufbedingung | //+------------------------------------------------------------------+ int CSignalEquidChannel::LongCondition(void) { int result=0; //--- Haben wir einen Tiefstpreis if(m_base_low_price>0.) //--- Wenn der Kanal nicht fällt if(m_ch_type!=CHANNEL_TYPE_DESCENDING) //--- Wenn der Tiefstpreis nahe der unteren Grenze ist if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1])) { if(IS_PATTERN_USAGE(0)) result=m_pattern_0; } //--- return result; } //+------------------------------------------------------------------+
Eine vergleichbare Prüfung wird für einen Verkauf durchgeführt, der Kanal steigt nicht.
Die Hauptmethode CEquidChannelExpert::Processing() der Datei EquidistantChannelExpert2.mqh ist die gleiche wie in der Basisversion, da ja kein Trailing stattfindet.
Prüfung der Effektivität dieses Faktors. Nur ein Parameter wird optimiert.
Variable | Start | Step | Stop |
---|---|---|---|
Toleranz des Typs, Pips | 0 | 5 | 150 |
Die Ergebnisse der Optimierung finden sich in der Datei ReportOptimizer-signal2.xml. Das beste Ergebnis zeigt Fig.6.
Fig.6 Results of the strategy with the use of channel type for 2013-2015.
Leicht zu sehen, dass die Ergebnisse der Strategie etwas besser sind als die der Basisstrategie. Es zeigt sich, dass bei einem bestimmten Parameterwert, ein Filter wie der Kanaltyp das Endergebnis beeinflussen kann.
3.3 Kanalbreite
Es könnte sein, dass die Kanalbreite die Strategie selbst beeinflussen könnte. Wenn der Kanal eng ist, dann könnte man vielleicht in die Richtung der Preisbewegung handeln und nicht gegen sie. Das Ergebnis wäre eine Ausbruchsstrategie. Ist die Kanalbreite groß, könnte man wie gehabt handeln. Das ist dann die Abprallstrategie. Das ist die augenblickliche Strategie — Handel auf der Basis der Kanalgrenzen.
Natürlich benötige wir ein Kriterium, um breite oder enge Kanäle zu bestimmen. Um nicht die Extrema definieren zu müssen, bestimmen wir den Kanal, der weder zu breit noch zu eng ist. Wir benötigen dazu 2 Kriterien:
- ausreichende Breite des engen Kanals;
- ausreichende Breite des breiten Kanals.
Ist der Kanal keines von beiden, sollte man vielleicht keine Position eröffnen.
Fig.7 Kanalbreite, Diagramm
Es muss auf das geometrische Problem der Bestimmung der Kanalbreite hingewiesen werden. Die Achsen des Charts haben unterschiedliche Dimensionen. Es ist sehr einfach die Längen der Senkrechten AB und CD zu messen. Aber die Berechnung der Länge von CE macht Probleme (Fig.7).
Entscheiden wir uns für die einfachste Methode, durchaus diskussionswürdig, weil nicht die Allergenaueste. Die Formel ist wie folgt:
Länge von CE ≃ Länge von CD / (1.0 + Ausmaß der Änderung des Kanals)
Kanalbreite gemessen unter Verwendung der Enumeration ENUM_CHANNEL_WIDTH_TYPE:
//+------------------------------------------------------------------+ //| Kanalbreite | //+------------------------------------------------------------------+ enum ENUM_CHANNEL_WIDTH_TYPE { CHANNEL_WIDTH_NARROW=0, // eng CHANNEL_WIDTH_MID=1, // mittel CHANNEL_WIDTH_BROAD=2, // weit };
Ergänzen wir das Kriterium der Kanalbreite zur Gruppe der Parameter des Quellcodes des Experten in der Datei ChannelsTrader3.mq5.
//--- sinput string Info_channels="+===-- Channels --====+"; // +===-- Channels --====+ input int InpPipsInside=100; // interne Toleranz, Pips input int InpPipsOutside=150; // externe Toleranz, Pips input int InpNarrowPips=250; // enger Kanal, Pips input int InpBroadPips=1200; // breiter Kanal, Pips ...
Wenn das Kriteriums eines schmalen Kanals größer ist als der Wert eines breiten Kanals, gibt es einen Fehler bei der Initialisierung.
//--- Filter Parameter filter0.PointsInside(_Point*InpPipsInside); filter0.PointsOutside(_Point*InpPipsOutside); if(InpNarrowPips>=InpBroadPips) { PrintFormat(__FUNCTION__+": error specifying narrow and broad values"); return INIT_FAILED; } filter0.NarrowTolerance(_Point*InpNarrowPips); filter0.BroadTolerance(_Point*InpBroadPips);
Die Bestimmung des Ausmaßes der Kanalbreite geschieht in der Methode Direction().
//--- Kanalbreite m_ch_width=CHANNEL_WIDTH_MID; // mittel double ch_width_pnt=((upper_vals[1]-lower_vals[1])/(1.0+pr_speed_pnt)); //--- das Kriterium eines engen Kanals if(m_ch_narrow_tol!=EMPTY_VALUE) if(ch_width_pnt<=m_ch_narrow_tol) m_ch_width=CHANNEL_WIDTH_NARROW; // eng //--- das Kriterium eines bereiten Kanals if(m_ch_narrow_tol!=EMPTY_VALUE) if(ch_width_pnt>=m_ch_broad_tol) m_ch_width=CHANNEL_WIDTH_BROAD; // breit
Anfänglich wird eine mittlere Kanalbreite angenommen. Danach wird auf eng oder breit geprüft.
Es müssen aber auch die Methoden zur Bestimmung der Handelsrichtung geändert werden. Daher, die Bedingung zum Kauf ist jetzt wie folgt:
//+------------------------------------------------------------------+ //| Prüfe die Kaufbedingung | //+------------------------------------------------------------------+ int CSignalEquidChannel::LongCondition(void) { int result=0; //--- Ist der Kanal eng - handele den Ausbruch aus der oberen Grenze if(m_ch_width==CHANNEL_WIDTH_NARROW) { //--- Haben wir einen Höchstpreis if(m_base_high_price>0.) //--- Wenn der Höchstpreis nahe der oberen Grenze ist if(m_base_high_price>=m_upper_zone[1]) { if(IS_PATTERN_USAGE(0)) result=m_pattern_0; } } //--- oder ist der Kanal breit - handele den Abprall von der unteren Grenze else if(m_ch_width==CHANNEL_WIDTH_BROAD) { //--- Haben wir einen Tiefstpreis if(m_base_low_price>0.) //--- Wenn der Tiefstpreis nahe der unteren Grenze ist if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1])) { if(IS_PATTERN_USAGE(0)) result=m_pattern_0; } } //--- return result; } //+------------------------------------------------------------------+
Die Methode hat zwei Blöcke. Der Ersteprüft die Möglichkeit den Ausbruch im engen Kanal zu handeln. Beachten Sie, dass die aktuelle Variante den Ausbruch beim Erreichen der Preise der oberen Toleranzzone handelt. Der zweite Block prüft, ob der Preis bereits in der unteren Toleranzzone ist, um in der Folge den Abprall zu handeln.
Die Methode, die die Gelegenheit zum Verkauf prüft, — ShortCondition() — ist analog.
Die Hauptmethode CEquidChannelExpert::Processing() in der Datei EquidistantChannelExpert3.mqh bleibt unverändert.
Es gibt 2 Parameter zu optimieren.
Variable | Start | Step | Stop |
---|---|---|---|
Enger Kanal, Pips | 100 | 20 | 250 |
Breiter Kanal, Pips | 350 | 50 | 1250 |
Die Ergebnisse der Optimierung finden sich in der Datei ReportOptimizer-signal3.xml. Das beste Ergebnis sieht man in Fig.8.
Fig.8 Ergebnisse der Strategie mit Kanalbreite für 2013-2015.
Vielleicht ist das der Faktor mit dem größten Einfluss von allen oben beschriebenen. Die Guthabenskurve hat jetzt eine deutlichere Richtung.
3.4 StopLoss und TakeProfit relativ zu den Kanalgrenzen
Wenn die Ausstiegsziele der Positionen ursprünglich in Form von StopLoss und TakeProfit gegeben waren, sollte es möglich sein, diese Werte von der augenblicklichen Strategie beeinflussen zu lassen. Einfach ausgedrückt, wenn der Kanal einen gewissen Winkel aufweist, sollten sich StopLoss und TakeProfit gemäß der Kanalgrenzen bewegen.
Ein paar Modelle wurden außerdem hinzugefügt. Nun Sie sehen so aus:
//--- "Wichtungen" des Modells (0-100) int m_pattern_0; // Modell "Abprall von der Kanalgrenze" int m_pattern_1; // Modell "Ausbruch aus der Kanalgrenze" int m_pattern_2; // Modell "Neuer Kanal"
Die bisherigen Versionen hatten jeweils nur ein Modell, die in Relation zu den Kanalgrenzen handelten. Jetzt unterscheiden wir zwischen den Abprall und Ausbruch. Und jetzt gibt es auch ein drittes Modell — der neue Kanal. Das ist notwendig für den Fall, dass es einen neuen Kanal gibt und noch Positionen des alten Kanals offen sind. Wenn das Modell ausgelöst wurde, wird so eine Position geschlossen.
Die Bedingungen für den Kauf sind die Folgenden:
//+------------------------------------------------------------------+ //| Prüfe die Kaufbedingung | //+------------------------------------------------------------------+ int CSignalEquidChannel::LongCondition(void) { int result=0; bool is_position=PositionSelect(m_symbol.Name()); //--- Ist der Kanal eng - handele den Ausbruch aus der oberen Grenze if(m_ch_width_type==CHANNEL_WIDTH_NARROW) { //--- Haben wir einen Höchstpreis if(m_base_high_price>0.) //--- Wenn der Höchstpreis nahe der oberen Grenze ist if(m_base_high_price>=m_upper_zone[1]) { if(IS_PATTERN_USAGE(1)) { result=m_pattern_1; //--- Keine Position if(!is_position) //--- Eintrag im Log if(m_to_log) { Print("\nTriggered the \"Breakout of channel border\" model for buying."); PrintFormat("High price: %0."+IntegerToString(m_symbol.Digits())+"f",m_base_high_price); PrintFormat("Trigger price: %0."+IntegerToString(m_symbol.Digits())+"f",m_upper_zone[1]); } } } } //--- oder wenn der Kanal breit oder mittel ist - handele den Abprall von der unteren Grenze else { //--- Haben wir einen Tiefstpreis if(m_base_low_price>0.) //--- Wenn der Tiefstpreis nahe der unteren Grenze ist if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1])) { if(IS_PATTERN_USAGE(0)) { result=m_pattern_0; //--- Keine Position if(!is_position) //--- Eintrag im Log if(m_to_log) { Print("\nTriggered the \"Rebound of channel border\" model for buying."); PrintFormat("Low price: %0."+IntegerToString(m_symbol.Digits())+"f",m_base_low_price); PrintFormat("Zone up: %0."+IntegerToString(m_symbol.Digits())+"f",m_upper_zone[0]); PrintFormat("Zone down: %0."+IntegerToString(m_symbol.Digits())+"f",m_upper_zone[1]); } } } } //--- return result; } //+------------------------------------------------------------------+
Dazu die Prüfung für einen Verkauf:
//+------------------------------------------------------------------+ //| Prüfe die Bedingung zum Schließen einer Kaufposition | //+------------------------------------------------------------------+ bool CSignalEquidChannel::CheckCloseLong(double &price) const { bool to_close_long=true; int result=0; if(IS_PATTERN_USAGE(2)) result=m_pattern_2; if(result>=m_threshold_close) { if(m_is_new_channel) //--- Wenn eine Kaufposition zu schließen ist if(to_close_long) { price=NormalizeDouble(m_symbol.Bid(),m_symbol.Digits()); //--- Eintrag im Log if(m_to_log) { Print("\nTriggered the \"New channel\" model for closing buy."); PrintFormat("Close price: %0."+IntegerToString(m_symbol.Digits())+"f",price); } } } //--- return to_close_long; } //+------------------------------------------------------------------+Für Verkaufspositionen sind die Bedingungen identisch.
Jetzt noch ein paar Worte zum Trailing. Eine eigene Klasse CTrailingEquidChannel wurde dafür geschrieben, abgeleitet von der Klasse CExpertTrailing.
//+------------------------------------------------------------------+ //| Class CTrailingEquidChannel. | //| Zweck: Klasse der Trailingstopps auf Basis des äquidist. Kanals. | //| Abgleitet von der Klasse CExpertTrailing. | //+------------------------------------------------------------------+ class CTrailingEquidChannel : public CExpertTrailing { protected: double m_sl_distance; // distance to stop loss double m_tp_distance; // distance to take profit double m_upper_val; // upper border double m_lower_val; // lower border ENUM_CHANNEL_WIDTH_TYPE m_ch_wid_type; // channel type by width //--- public: void CTrailingEquidChannel(void); void ~CTrailingEquidChannel(void){}; //--- Methoden zur Initialisierung der "protected" Daten void SetTradeLevels(double _sl_distance,double _tp_distance); //--- virtual bool CheckTrailingStopLong(CPositionInfo *position,double &sl,double &tp); virtual bool CheckTrailingStopShort(CPositionInfo *position,double &sl,double &tp); //--- bool RefreshData(const CSignalEquidChannel *_ptr_ch_signal); }; //+------------------------------------------------------------------+
Die Methode für die Signale von den Kanälen ist rot markiert.
Die Methoden zur Prüfung des Trailings von Kauf- oder Verkaufpositionen des Vorfahren wurde mittels des Polymorphismus neu definiert - das Grundprinzip der OOP.
Damit die Trailing-Klasse Zeit und Preise der Ziele von dem aktuellen Kanal erhalten kann, muss sie mit der Klasse für die Signale CSignalEquidChannel verbunden werden. Es wurde durch einen "constant" Pointer in der Klasse CEquidChannelExpert realisiert. Dieser Ansatz erlaubt es, alle notwendigen Informationen von den Signalen zu erhalten, ohne dass die Parameter der Signale selbst verändert werden.
//+------------------------------------------------------------------+ //| Klasse CEquidChannelExpert. | //| Zweck: Klasse eines EA der den äquidistanten Kanal handelt. | //| Abgeleitet von der Klasse CExper. | //+------------------------------------------------------------------+ class CEquidChannelExpert : public CExpert { //--- === Data members === --- private: const CSignalEquidChannel *m_ptr_ch_signal; //--- === Methods === --- public: //--- Constructor/Destructor void CEquidChannelExpert(void); void ~CEquidChannelExpert(void); //--- Pointer auf das Objekt der Kanalsignale void EquidChannelSignal(const CSignalEquidChannel *_ptr_ch_signal){m_ptr_ch_signal=_ptr_ch_signal;}; const CSignalEquidChannel *EquidChannelSignal(void) const {return m_ptr_ch_signal;}; protected: virtual bool Processing(void); //--- trade close positions check virtual bool CheckClose(void); virtual bool CheckCloseLong(void); virtual bool CheckCloseShort(void); //--- trailing stop check virtual bool CheckTrailingStop(void); virtual bool CheckTrailingStopLong(void); virtual bool CheckTrailingStopShort(void); }; //+------------------------------------------------------------------+
Auch die Methoden für das Schließen und Trailing von Positionen wurden in der Expertklasse angepasst.
Die Hauptmethode CEquidChannelExpert::Processing() in der Datei EquidistantChannelExpert4.mqh schaut jetzt wie folgt aus:
//+------------------------------------------------------------------+ //| Hauptmodul | //+------------------------------------------------------------------+ bool CEquidChannelExpert::Processing(void) { //--- Berechnung der Richtung m_signal.SetDirection(); //--- Keine Position if(!this.SelectPosition()) { //--- Positionseröffnung if(this.CheckOpen()) return true; } //--- Es gibt eine Position else { if(!this.CheckClose()) { //--- Prüfung auf Änderbarkeit der Position if(this.CheckTrailingStop()) return true; //--- return false; } else { return true; } } //--- wenn es keine Handelsoperationen gibt return false; } //+------------------------------------------------------------------+Diese Parameter werden optimiert:
Variable | Start | Step | Stop |
---|---|---|---|
StopLoss, Points | 25 | 5 | 75 |
TakeProfit, Points | 50 | 5 | 200 |
Die Ergebnisse der Optimierung finden sich in der Datei ReportOptimizer-signal4.xml. Das beste Resultat zeigt Fig.9.
Fig.9 Ergebnisse der Strategie unter Berücksichtigung der Kanalgrenzen für 2013-2015.
Es ist ganz deutlich, dass dieser Faktor — grenzbezogene Preislevel — keine Verbesserung der Ergebnisse darstellt.
Schlussfolgerung
Der Artikel erläutert den Prozess der Entwicklung und Umsetzung einer Klasse der Signale eines gleitenden Kanals. Jede Version der Signale wurde in einer Handelsstrategie realisiert und getestet.
Ich möchte betonen, dass für alles in diesem Artikel die verwendeten Werte des äquidistanten Kanals fix waren. Daher müssen alle Schlussfolgerungen, ob dieser oder jener Faktor entscheidend ist, unter Berücksichtigung dieses Faktums erfolgen.
Es bleiben noch andere Wege die Ergebnisse weiter zu verbessern. Dieser Artikel beschäftigt sich mit einem Teilaspekt dieser Möglichkeiten.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/1863
- 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.