Umkehrmuster: Testen des Musters Doppelspitze/Doppelboden

Dmitriy Gizlyk | 28 Dezember, 2018

Inhalt

Einführung

Die im Artikel "Wie lang ist der Trend?" durchgeführte Analyse zeigt, dass der Preis 60% der Zeit im Trend bleibt. Das bedeutet, dass das Eröffnen einer Position zu Beginn eines Trends die besten Ergebnisse liefert. Die Suche nach Trendumkehrpunkten hat zu einer großen Anzahl von Umkehrmustern geführt. Doppelspitze/Doppelboden ist einer der bekanntesten und am häufigsten verwendeten. 

1. Theoretische Aspekte des Musters

Das Muster von Doppelspitze/Doppelboden ist häufig auf einem Preischart zu finden. Seine Entstehung ist eng mit der Theorie der Handelsstufen verbunden. Das Muster bildet sich am Ende eines Trends, wenn der Preis ein Unterstützungs- oder Widerstandsniveau erreicht (abhängig von der vorherigen Bewegung). Nach einer Korrektur während einer wiederholten Prüfung des Niveaus rollt er wieder zurück, anstatt ihn zu durchbrechen.

An dieser Stelle kommen Gegentrendhändler ins Spiel, die einen Rollback vom Niveau handeln und den Preis in Richtung Korrektur treiben. Während die Korrekturbewegung an Dynamik gewinnt, beginnen Händler, die dem Trend folgen, den Markt zu verlassen, indem sie entweder die Gewinne fixieren oder Verlustpositionen schließen, die darauf abzielten, das Niveau zu durchbrechen. Das stärkt die Bewegung noch weiter und führt zur Entstehung eines neuen Trends.

Das Muster der Doppelspitze

Bei der Suche nach einem Muster in einem Diagramm macht es keinen Sinn, nach der genauen Übereinstimmung der Spitze bzw. des Bodens zu suchen. Abweichungen von Spitze/Boden gilt als normal. Stellen Sie einfach sicher, dass die Extrema innerhalb des gleichen Support-/Widerstandsniveaus liegen. Die Zuverlässigkeit der Muster hängt von der Stärke eines Niveaus ab, auf dem sie basiert.


2. Strategie das Muster zu handeln

Die Popularität der Muster befeuert viele Strategien, die sie verwenden. Im Internet, gibt es zumindest drei unterschiedliche Einstiegspunkt, dieses Muster zu handeln.

2.1. Fall 1

Der erste Einstiegspunkt basiert auf dem Durchbruch der Nackenlinie. Ein Stop-Loss wird jenseits der oberen und unteren Linie gesetzt. Es gibt verschiedene Ansätze, um den "Durchbruch der Nackenlinie" zu definieren. Händler können einen Balken verwenden, der unter der Nackenlinie schließt, sowie einen Balken, der die Nackenlinie um eine bestimmte Distanz durchbricht. Beide Ansätze haben ihre Vor- und Nachteile. Im Falle einer starken Bewegung kann eine Kerze in ausreichendem Abstand zur Nackenlinie geschlossen werden, was das Muster ineffizient macht.

Der Erste Einstiegspunkt

Der Nachteil dieses Ansatzes ist der relativ hohe Stop-Loss, der das Verhältnis von Gewinn zu Risiko der Strategie vermindert.

2.2. Fall 2

Der zweite Einstiegspunkt basiert auf der Theorie der Spiegelniveaus, wenn die Nackenlinie von einer Unterstützungs- in eine Widerstandslinie übergeht und umgekehrt. Hier erfolgt die Eröffnung, wenn der Preis nach dem Durchbruch wieder die Nackenlinie kreuzt. In diesem Fall wird Stop-Loss über das Extremem der letzten Korrektur hinaus gesetzt, wodurch das Stop-Loss-Level deutlich reduziert wird. Leider testet der Preis nicht immer die Nackenlinie nach dem Durchbruch, was die Anzahl der Eröffnungen reduziert.

Der zweite Einstiegspunkt 


2.3. Fall 3

Der dritte Einstiegspunkt basiert auf der Trendtheorie. Er wird durch einen Durchbruch der Trendlinie vom Bewegungsstartpunkt bis zur Nackenlinie definiert. Wie im ersten Fall wird ein Stop-Loss jenseits der oberen bzw. untere Linie gesetzt. Ein früher Einstieg ermöglicht ein niedrigeres Stop-Loss-Level im Vergleich zum ersten Einstiegspunkt. Er liefert auch mehr Signale als im zweiten Fall. Gleichzeitig erzeugt ein solcher Einstiegspunkt mehr falsche Signale, da sich zwischen den Linien der Extrema und dem Nacken ein Kanal bilden kann, oder es kann das Muster des Wimpels entstehen. Beide Fälle deuten auf eine Fortsetzung des Trends hin.

Der dritte Einstiegspunkt 


Alle drei Strategien empfehlen das Schließen im Abstand der Differenz von Extremum und Nackenlinie.

Take-Profit

Bei der Bestimmung des Musters auf dem Chart sollten Sie außerdem beachten, dass sich Doppelspitze/Doppelboden deutlich von der Kursbewegung abheben sollte. Bei der Beschreibung des Musters wird oft eine Einschränkung hinzugefügt: Zwischen zwei Ober- und Unterseiten sollten mindestens sechs Balken liegen.

Da die Musterbildung auf der Theorie des Preisniveaus basiert, sollte der Händler des Musters das nicht ignorieren. Daher sollte, je nach Verwendungszweck, die Nackenlinie nicht weniger sein als der Fibo Level 50 der Anfangsbewegung. Um Fehlsignale herauszufiltern, können wir außerdem einen Mindestwert der ersten Korrektur (die die Nackenlinie bildet) als Indikator für die Stärke des Preisniveaus hinzufügen.


3. Erstellen des EAs

3.1. Suche nach den Extrema

Wir werden die Entwicklung des EAs mit dem Block der Mustersuche beginnen. Verwenden wir den ZigZag-Indikator aus dem Standardpaket des MetaTrader 5, um nach den Preisextrema zu suchen. Wir verschieben die Berechnung des Indikators in die Klasse, wie es im Artikel[1] beschrieben wurde. Der Indikator enthält zwei Indikatorpuffer, die die Preise der Extrema enthalten. Die Indikatorpuffer enthalten "Nichts" zwischen den Extrema. Um keine zwei Indikatorpuffer mit mehreren leeren Werten zu erstellen, wurden sie durch ein Array von Strukturen ersetzt, die Informationen über das Extremum enthalten. Die Struktur zum Speichern von Informationen über das Extremum sieht wie folgt aus.

   struct s_Extremum
     {
      datetime          TimeStartBar;
      double            Price;
      
      s_Extremum(void)  :  TimeStartBar(0),
                           Price(0)
         {
         }
      void Clear(void)
        {
         TimeStartBar=0;
         Price=0;
        }
     };

Wenn Sie den ZigZag-Indikator mindestens einmal verwendet haben, wissen Sie, wie viele Kompromisse Sie bei der Suche nach optimalen Parametern eingehen müssen. Zu kleine Parameterwerte teilen eine große Bewegung in kleine Teile, während zu große Parameterwerte kurze Bewegungen überspringen. Der Algorithmus zur Suche nach grafischen Mustern ist sehr anspruchsvoll in Bezug auf die Qualität der Extrema. Während ich versuchte, einen Mittelweg zu finden, entschied ich mich, den Indikator mit kleinen Parameterwerten zu verwenden und einen zusätzlichen Überbau zu schaffen, der unidirektionale Bewegungen mit kurzen Korrekturen in einer Bewegung kombiniert.

Um dieses Problem zu lösen, wurde die CTrends-Klasse entwickelt. Der Header der Klasse ist unten aufgeführt. Bei der Initialisierung wird eine Referenz auf das Klassenobjekt des Indikators erstellt und der Mindestwert der Bewegung, der als Trendfortsetzung gilt, der Klasse übergeben.

class CTrends : public CObject
  {
private:
   CZigZag          *C_ZigZag;         // Link zum Objekt des ZigZag-Indikators
   s_Extremum        Trends[];         // Array der Extrema
   int               i_total;          // Gesamtzahl der gespeicherten Extrema
   double            d_MinCorrection;  // Minimalbewegung für eine Trendfortsetzung

public:
                     CTrends();
                    ~CTrends();
//--- Initialisierungsmethode der Klasse
   virtual bool      Create(CZigZag *pointer, double min_correction);
//--- Abfragen der Informationen des Extremums
   virtual bool      IsHigh(s_Extremum &pointer) const;
   virtual bool      Extremum(s_Extremum &pointer, const int position=0);
   virtual int       ExtremumByTime(datetime time);
//--- Allgemeine Information
   virtual int       Total(void)          {  Calculate(); return i_total;   }
   virtual string    Symbol(void) const   {  if(CheckPointer(C_ZigZag)==POINTER_INVALID) return "Not Initilized"; return C_ZigZag.Symbol();  }
   virtual ENUM_TIMEFRAMES Timeframe(void) const   {  if(CheckPointer(C_ZigZag)==POINTER_INVALID) return PERIOD_CURRENT; return C_ZigZag.Timeframe();  }
   
protected:
   virtual bool      Calculate(void);
   virtual bool      AddTrendPoint(s_Extremum &pointer);
  };

Um Daten über die Extrema zu erhalten, werden die folgenden Methoden von der Klasse bereitgestellt:

Der allgemeine Informationsblock enthält Methoden, die die Gesamtzahl der gespeicherten Extreme, das verwendete Symbol und den Zeitrahmen zurückgeben.

Die Logik der Hauptklasse ist in der Methode Calculate implementiert. Schauen wir die uns genauer an.

Wir überprüfen zu Beginn der Methode die Relevanz der Referenz auf das Objekt der Indikatorklasse und das Vorhandensein von Extrema, die der Indikator gefunden hat.

bool CTrends::Calculate(void)
  {
   if(CheckPointer(C_ZigZag)==POINTER_INVALID)
      return false;
//---
   if(C_ZigZag.Total()==0)
      return true;

Danach wird die Nummer der unbearbeiteten Extrema definiert. Sind alle Extrema bearbeitet, wird die Methode mit dem Ergebnis true verlassen.

   int start=(i_total<=0 ? C_ZigZag.Total() : C_ZigZag.ExtremumByTime(Trends[i_total-1].TimeStartBar));
   switch(start)
     {
      case 0:
        return true;
        break;
      case -1:
        start=(i_total<=1 ? C_ZigZag.Total() : C_ZigZag.ExtremumByTime(Trends[i_total-2].TimeStartBar));
        if(start<0 || ArrayResize(Trends,i_total-1)<=0)
          {
           ArrayFree(Trends);
           i_total=0;
           start=C_ZigZag.Total();
          }
        else
           i_total=ArraySize(Trends);
        if(start==0)
           return true;
        break;
     }

Danach fragen wir nach der notwendigen Anzahl von Extrema aus der Klasse des Indikators.

   s_Extremum  base[];
   if(!C_ZigZag.Extremums(base,0,start))
      return false;
   int total=ArraySize(base);
   if(total<=0)
      return true;

Gibt es bis jetzt keine Extrema in den Daten, wird das älteste Extremum der Daten durch den Aufruf der Methode AddTrendPoint hinzugefügt.

   if(i_total==0)
      if(!AddTrendPoint(base[total-1]))
         return false;

Als nächstes iterieren wir über alle geladenen Extrema. Extrema vor dem letzten gesicherten werden ignoriert.

   for(int i=total-1;i>=0;i--)
     {
      int trends_pos=i_total-1;
      if(Trends[trends_pos].TimeStartBar>=base[i].TimeStartBar)
         continue;

Im nächsten Schritt überprüfen wir, ob die Extrema gleichgerichtet sind. Wenn ein neues Extremum das vorherige überschreibt, werden die Daten aktualisiert.

      if(IsHigh(Trends[trends_pos]))
        {
         if(IsHigh(base[i]))
           {
            if(Trends[trends_pos].Price<base[i].Price)
              {
               Trends[trends_pos].Price=base[i].Price;
               Trends[trends_pos].TimeStartBar=base[i].TimeStartBar;
              }
            continue;
           }

Bei entgegengesetzt gerichteten Extrema prüfen wir, ob die neue Bewegung eine Fortsetzung eines früheren Trends ist. Wenn ja, aktualisieren wir die Daten der Extrema. Wenn nicht, fügen wir Daten zum Extremum hinzu, indem wir die Methode AddTrendPoint aufrufen;

         else
           {
            if(trends_pos>1 && Trends[trends_pos-1].Price>base[i].Price  && Trends[trends_pos-2].Price>Trends[trends_pos].Price)
              {
               double trend=fabs(Trends[trends_pos].Price-Trends[trends_pos-1].Price);
               double correction=fabs(Trends[trends_pos].Price-base[i].Price);
               if(fabs(1-correction/trend)>d_MinCorrection)
                 {
                  Trends[trends_pos-1].Price=base[i].Price;
                  Trends[trends_pos-1].TimeStartBar=base[i].TimeStartBar;
                  i_total--;
                  ArrayResize(Trends,i_total);
                  continue;
                 }
              }
            AddTrendPoint(base[i]);
           }
        }

Der gesamte Code aller Klassen und Methoden befindet sich in der Anlage.

3.2. Suche nach dem Muster

Nachdem wir die Extrema definiert haben, erstellen wir den Block für die Suche nach Einstiegspunkten im Markt. Wir teilen diese Arbeit in zwei Teilschritte auf:

  1. Suche nach einem potenziellen Muster für eine Positionseröffnung.
  2. Markteintrittspunkt.

Diese Funktion bietet die Klasse CPttern. Der Header ist unten aufgeführt.

class CPattern : public CObject
  {
private:
   s_Extremum     s_StartTrend;        //Startpunkt des Trends
   s_Extremum     s_StartCorrection;   //Startpunkt der Korrektur
   s_Extremum     s_EndCorrection;     //Endpunkt der Korrektur
   s_Extremum     s_EndTrend;          //Trendende
   double         d_MinCorrection;     //Mindestkorrektur
   double         d_MaxCorrection;     //Maximale Korrektur
//---
   bool           b_found;             //Flag für "Muster erkannt"
//---
   CTrends       *C_Trends;
public:
                     CPattern();
                    ~CPattern();
//--- Initialisierung der Klasse
   virtual bool      Create(CTrends *trends, double min_correction, double max_correction);
//--- Methoden zur Mustersuche und der Einstiegspunkte
   virtual bool      Search(datetime start_time);
   virtual bool      CheckSignal(int &signal, double &sl, double &tp1, double &tp2);
//--- Methoden zum Vergleich von Objekten
   virtual int       Compare(const CPattern *node,const int mode=0) const;
//--- Methoden zur Datenabfrage der Extrema der Muster
   s_Extremum        StartTrend(void)        const {  return s_StartTrend;       }
   s_Extremum        StartCorrection(void)   const {  return s_StartCorrection;  }
   s_Extremum        EndCorrection(void)     const {  return s_EndCorrection;    }
   s_Extremum        EndTrend(void)          const {  return s_EndTrend;         }
   virtual datetime  EndTrendTime(void)            {  return s_EndTrend.TimeStartBar;  }
  };

Das Muster wird durch vier benachbarte Extrema definiert. Die Daten über sie werden in den Strukturelementen s_StartTrend, s_StartCorrection, s_EndCorrection und s_EndTrend gespeichert. Um das Muster zu identifizieren, benötigen wir auch minimale und maximale Korrekturstufen, die in den Variablen d_MinCorrection und d_MaxCorrection gespeichert werden. Wir erhalten die Extrema aus der Instanz der zuvor erstellten Klasse CTrends.

Während der Initialisierung der Klasse übergeben wir den Pointer auf die Objekt- und Grenzkorrekturstufen der Klasse CTrends. Wir überprüfen innerhalb der Methode die Gültigkeit des übergebenen Pointers, speichern die empfangenen Informationen und löschen die Strukturen der Extrema.

bool CPattern::Create(CTrends *trends,double min_correction,double max_correction)
  {
   if(CheckPointer(trends)==POINTER_INVALID)
      return false;
//---
   C_Trends=trends;
   b_found=false;
   s_StartTrend.Clear();
   s_StartCorrection.Clear();
   s_EndCorrection.Clear();
   s_EndTrend.Clear();
   d_MinCorrection=min_correction;
   d_MaxCorrection=max_correction;
//---
   return true;
  }

Die Suche nach potentiellen Mustern wird von der Methode Search() durchgeführt. Diese Methode erhält in den Parametern das Startdatum der Suche und gibt den Logikwert zurück, der über die Suchergebnisse informiert. Betrachten wir den Verfahrensalgorithmus im Detail.

Wir überprüfen zunächst die Relevanz der Pointer für das Objekt der Klasse CTrends und das Vorhandensein von gespeicherten Extrema. Im Falle eines negativen Ergebnisses verlassen wir die Methode mit dem Ergebnis false.

bool CPattern::Search(datetime start_time)
  {
   if(CheckPointer(C_Trends)==POINTER_INVALID || C_Trends.Total()<4)
      return false;

Anschließend definieren wir das Extremum, das dem in den Eingaben angegebenen Zeitpunkt entspricht. Wenn kein Extremum gefunden wurde, verlassen wir die Methode mit dem Ergebnis false.

   int start=C_Trends.ExtremumByTime(start_time);
   if(start<0)
      return false;

Als Nächstes ordnen wir die Schleife so an, dass sie über alle Extreme hinweg iteriert, beginnend mit dem angegebenen Zeitpunkt und bis zum letzten erkannten Zeitpunkt. Zuerst erhalten wir vier aufeinanderfolgende Extrema. Wenn mindestens eines der Extrema nicht erreicht wird, wechseln Sie zum nächsten Extremum.

   b_found=false;
   for(int i=start;i>=0;i--)
     {
      if((i+3)>=C_Trends.Total())
         continue;
      if(!C_Trends.Extremum(s_StartTrend,i+3) || !C_Trends.Extremum(s_StartCorrection,i+2) ||
         !C_Trends.Extremum(s_EndCorrection,i+1) || !C_Trends.Extremum(s_EndTrend,i))
         continue;

Im nächsten Schritt überprüfen wir, ob die Extrema dem erforderlichen Muster entsprechen. Wenn dies nicht der Fall ist, gehen wir zum nächsten Extremum über. Wenn das Muster erkannt wird, setzen wir das Flag auf true und verlassen Sie die Methode mit dem gleichen Ergebnis.

      double trend=s_StartCorrection.Price-s_StartTrend.Price;
      double correction=s_StartCorrection.Price-s_EndCorrection.Price;
      double re_trial=s_EndTrend.Price-s_EndCorrection.Price;
      double koef=correction/trend;
      if(koef<d_MinCorrection || koef>d_MaxCorrection || (1-fmin(correction,re_trial)/fmax(correction,re_trial))>=d_MaxCorrection)
         continue;
      b_found= true; 
//---
      break;
     }
//---
   return b_found;
  }

Der nächste Schritt ist das Erkennen des Eintrittspunktes. Wir werden dafür den zweiten Fall verwenden. Um das Risiko zu verringern, dass der Preis nicht zur Nackenlinie zurückkehrt, werden wir nach einer Signalbestätigung im kleineren Zeitrahmen suchen.

Um diese Funktion zu implementieren, erstellen wir die CheckSignal()-Methode. Abgesehen vom Signal selbst gibt das Verfahren Stop-Loss und Take-Profit. Daher werden wir Pointer auf die Variablen in den Methodenparametern verwenden.

Wir überprüfen zu Beginn des Verfahrens das Flag auf das Vorhandensein eines zuvor erkannten Musters. Wenn das Muster nicht gefunden wird, verlassen wir die Methode mit dem Ergebnis "false".

bool CPattern::CheckSignal(int &signal, double &sl, double &tp1, double &tp2)
  {
   if(!b_found)
      return false;

Dann bestimmen wir den Zeitpunkt des Schließens der Musterbildungskerze und laden die Daten des Zeitrahmens, der uns vom Beginn der Musterbildung bis zum aktuellen Moment interessiert.

   string symbol=C_Trends.Symbol();
   if(symbol=="Not Initilized")
      return false;
   datetime start_time=s_EndTrend.TimeStartBar+PeriodSeconds(C_Trends.Timeframe());
   int shift=iBarShift(symbol,e_ConfirmationTF,start_time);
   if(shift<0)
      return false;
   MqlRates rates[];
   int total=CopyRates(symbol,e_ConfirmationTF,0,shift+1,rates);
   if(total<=0)
      return false;

Danach ordnen wir die Schleife an, in der wir den Durchbruch der Nackenlinie, die Kerzenkorrektur und das Schließen der Kerze über der Nackenlinie hinaus in der erwarteten Bewegungsrichtung Takt für Takt überprüfen.

Ich habe hier noch einige weitere Einschränkungen hinzugefügt:

Wenn eines der Ereignisse, die das Muster zerstören, erkannt wird, verlassen wir das Verfahren mit dem Ergebnis false.

   signal=0;
   sl=tp1=tp2=-1;
   bool up_trend=C_Trends.IsHigh(s_EndTrend);
   double extremum=(up_trend ? fmax(s_StartCorrection.Price,s_EndTrend.Price) : fmin(s_StartCorrection.Price,s_EndTrend.Price));
   double exit_level=2*s_EndCorrection.Price - extremum;
   bool break_neck=false;
   for(int i=0;i<total;i++)
     {
      if(up_trend)
        {
         if(rates[i].low<=exit_level || rates[i].high>extremum)
            return false;
         if(!break_neck)
           {
            if(rates[i].close>s_EndCorrection.Price)
               continue;
            break_neck=true;
            continue;
           }
         if(rates[i].high>s_EndCorrection.Price)
           {
            if(sl==-1)
               sl=rates[i].high;
            else
               sl=fmax(sl,rates[i].high);
           }
         if(rates[i].close<s_EndCorrection.Price || sl==-1)
            continue;
         if((total-i)>2)
            return false;

Nachdem das Markteintrittssignal erkannt worden ist, geben wir den Signaltyp ("-1" - Verkauf, "1" - Kauf) und die Handelsstufen an. Ein Stop-Loss wird auf die maximale Korrektur in Bezug auf die Nackenlinie eingestellt, nachdem er durchbrochen wurde. Wir legen zwei Stufen für einen Take-Profit fest:

1. Bei 90% des Abstandes des Extremums von der Nackenlinie in Positionsrichtung.

2. Auf einem Niveau, das 90% der vorherigen Trendbewegung entspricht.

Wir fügen die Einschränkung hinzu: Die erste Take-Profit-Stufe darf die zweite nicht überschreiten.

         signal=-1;
         double top=fmax(s_StartCorrection.Price,s_EndTrend.Price);
         tp1=s_EndCorrection.Price-(top-s_EndCorrection.Price)*0.9;
         tp2=top-(top-s_StartTrend.Price)*0.9;
         tp1=fmax(tp1,tp2);
         break;
        }

Der gesamte Code aller Klassen und Methoden befindet sich in der Anlage.

3.3. Entwickeln des EAs

Nach den Vorbereitungsarbeiten fassen wir alle Blöcke in einem einzigen EA zusammen. Wir deklarieren die externe Variable und teilen sie in drei Blöcke auf:

sinput   string            s1             =  "---- ZigZag Settings ----";     //---
input    int               i_Depth        =  12;                              // Länge
input    int               i_Deviation    =  100;                             // Abweichung
input    int               i_Backstep     =  3;                               // Rückschritt
input    int               i_MaxHistory   =  1000;                            // Max-Historie , Balken
input    ENUM_TIMEFRAMES   e_TimeFrame    =  PERIOD_M30;                      // Arbeitszeitrahmen
sinput   string            s2             =  "---- Pattern Settings ----";    //---
input    double            d_MinCorrection=  0.118;                           // Mindestkorrektur
input    double            d_MaxCorrection=  0.5;                             // Maximale Korrektur
input    ENUM_TIMEFRAMES   e_ConfirmationTF= PERIOD_M5;                       // Zeitrahmen für die Bestätigung
sinput   string            s3             =  "---- Trade Settings ----";      //---
input    double            d_Lot          =  0.1;                             // Handelslot
input    ulong             l_Slippage     =  10;                              // Schlupf
input    uint              i_SL           =  350;                             // Stop-Loss Backstep, Points

Wir deklarieren in den globalen Variablen das Array zum Speichern der Pointer auf die Objekte der Muster, die Instanz der Klasse für den Handel, die Instanz der Klasse zur Mustersuche, in der der Zeiger auf die verarbeitete Klasseninstanz gespeichert werden soll, und die Variable zum Speichern der nächsten Startzeit der Mustersuche.

CArrayObj         *ar_Objects;
CTrade            *Trade;
CPattern          *Pattern;
datetime           start_search;

Um die Möglichkeit zu haben, zwei Take-Profits gleichzeitig festzulegen, verwenden wir die im Artikel[2] beschriebene Technologie.

Wir initialisieren alle notwendigen Objekte in der Funktion OnInit(). Da wir die Klasseninstanzen CZigZag und CTrends nie deklariert haben, initialisieren wir sie einfach und fügen Zeiger auf diese Objekte zu unserem Array hinzu. Im Falle eines Initialisierungsfehlers verlassen Sie die Funktion mit dem Ergebnis INIT_FAILED an einer der Stufen.

int OnInit()
  {
//--- Initialisierung des Arrays für die Objekte
   ar_Objects=new CArrayObj();
   if(CheckPointer(ar_Objects)==POINTER_INVALID)
      return INIT_FAILED;
//--- Initialisierung der Klasse des ZigZag Indikators
   CZigZag *zig_zag=new CZigZag();
   if(CheckPointer(zig_zag)==POINTER_INVALID)
      return INIT_FAILED;
   if(!ar_Objects.Add(zig_zag))
     {
      delete zig_zag;
      return INIT_FAILED;
     }
   zig_zag.Create(_Symbol,i_Depth,i_Deviation,i_Backstep,e_TimeFrame);
   zig_zag.MaxHistory(i_MaxHistory);
//--- Initialisierung der Klasse zur Suche der Trendbewegung
   CTrends *trends=new CTrends();
   if(CheckPointer(trends)==POINTER_INVALID)
      return INIT_FAILED;
   if(!ar_Objects.Add(trends))
     {
      delete trends;
      return INIT_FAILED;
     }
   if(!trends.Create(zig_zag,d_MinCorrection))
      return INIT_FAILED;
//--- Initialisierung der Klasse der Handelsoperationen
   Trade=new CTrade();
   if(CheckPointer(Trade)==POINTER_INVALID)
      return INIT_FAILED;
   Trade.SetAsyncMode(false);
   Trade.SetDeviationInPoints(l_Slippage);
   Trade.SetTypeFillingBySymbol(_Symbol);
//--- Initialisierung zusätzlicher Variablen
   start_search=0;
   CLimitTakeProfit::OnlyOneSymbol(true);
//---
   return(INIT_SUCCEEDED);
  }

Das Löschen der Instanzen der verwendeten Objekte geschieht in der Funktion OnDeinit().

void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(ar_Objects)!=POINTER_INVALID)
     {
      for(int i=ar_Objects.Total()-1;i>=0;i--)
         delete ar_Objects.At(i);
      delete ar_Objects;
     }
   if(CheckPointer(Trade)!=POINTER_INVALID)
      delete Trade;
   if(CheckPointer(Pattern)!=POINTER_INVALID)
      delete Pattern;
  }

Wie üblich ist die Hauptfunktionalität in der Funktion OnTick() implementiert. Sie kann in zwei Blöcke unterteilt werden:

1. Überprüfen der Markteintrittssignale nach den zuvor erkannten Mustern. Sie wird jedes Mal gestartet, wenn eine neue Kerze in einem kleinen Zeitfenster der Suche nach der Signalbestätigung erscheint.

2. Auf der Suche nach neuen Mustern. Sie wird jedes Mal gestartet, wenn eine neue Kerze innerhalb eines Arbeitszeitrahmens (der des Indikators) erscheint.

Zu Beginn der Funktion überprüfen wir das Vorhandensein eines neuen Balkens in einem Bestätigungszeitraum für den Einstiegspunkt. Wenn der Balken nicht gebildet ist, verlassen wir die Funktion bis zum nächsten Tick. Es ist zu beachten, dass dieser Ansatz nur dann richtig funktioniert, wenn der Zeitrahmen für die Bestätigung eines Einstiegspunkts den Arbeitszeitrahmen nicht überschreitet. Andernfalls müssten wir, anstatt die Funktion zu verlassen, zum Mustersuchblock wechseln.

void OnTick()
  {
//---
   static datetime Last_CfTF=0;
   datetime series=(datetime)SeriesInfoInteger(_Symbol,e_ConfirmationTF,SERIES_LASTBAR_DATE);
   if(Last_CfTF>=series)
      return;
   Last_CfTF=series;

Wenn ein neuer Balken erscheint, ordnen wir die Schleife zur Überprüfung aller zuvor gespeicherten Muster auf das Vorhandensein eines Markteintrittssignals an. Wir werden die ersten beiden Array-Objekte nicht auf Signale überprüfen, da wir Pointer auf Instanzen der Extremum-Suchklassen in diesen Zellen speichern. Wenn der gespeicherte Pointer ungültig ist oder die Signalprüfungsfunktion false zurückgibt, wird der Pointer aus dem Array entfernt. Die Mustersignale werden in der Funktion CheckPattern() überprüft. Der Algorithmus wird im Folgenden vorgestellt.

   int total=ar_Objects.Total();
   for(int i=2;i<total;i++)
     {
      if(CheckPointer(ar_Objects.At(i))==POINTER_INVALID)
         if(ar_Objects.Delete(i))
           {
            i--;
            total--;
            continue;
           }
//---
      if(!CheckPattern(ar_Objects.At(i)))
        {
         if(ar_Objects.Delete(i))
           {
            i--;
            total--;
            continue;
           }
        }
     }

Nach der Überprüfung der zuvor erkannten Muster ist es an der Zeit, zum zweiten Block zu gehen - der Suche nach neuen Mustern. Wir überprüfen dazu die Verfügbarkeit eines neuen Balkens im Arbeitszeitrahmen. Wenn kein neuer Balken gebildet wird, verlassen wir die Funktion und warten auf einen neuen Tick.

   static datetime Last_WT=0;
   series=(datetime)SeriesInfoInteger(_Symbol,e_TimeFrame,SERIES_LASTBAR_DATE);
   if(Last_WT>=series)
      return;

Wenn ein neuer Balken erscheint, definieren wir den Anfangszeitpunkt der Suche nach Mustern (unter Berücksichtigung der Tiefe der in den Parametern angegebenen analysierten Historie). Als Nächstes überprüfen wir die Gültigkeit der Pointer auf das Objekt der Klasse CPattern. Wenn der Pointer ungültig ist, erstellen wir eine neue Klasseninstanz.

   start_search=iTime(_Symbol,e_TimeFrame,fmin(i_MaxHistory,Bars(_Symbol,e_TimeFrame)));
   if(CheckPointer(Pattern)==POINTER_INVALID)
     {
      Pattern=new CPattern();
      if(CheckPointer(Pattern)==POINTER_INVALID)
         return;
      if(!Pattern.Create(ar_Objects.At(1),d_MinCorrection,d_MaxCorrection))
        {
         delete Pattern;
         return;
        }
     }
   Last_WT=series;

Danach rufen wir die Methode zum Suchen nach potenziellen Mustern in einer Schleife auf. Im Falle einer erfolgreichen Suche verschieben wir den Anfangszeitpunkt der Suche nach einem neuen Muster und überprüfen das Vorhandensein des erkannten Musters im Array der zuvor gefundenen. Wenn das Muster bereits im Array vorhanden ist, wechseln wir zu einer erneuten Suche.

   while(!IsStopped() && Pattern.Search(start_search))
     {
      start_search=fmax(start_search,Pattern.EndTrendTime()+PeriodSeconds(e_TimeFrame));
      bool found=false;
      for(int i=2;i<ar_Objects.Total();i++)
         if(Pattern.Compare(ar_Objects.At(i),0)==0)
           {
            found=true;
            break;
           }
      if(found)
         continue;

Wenn ein neues Muster gefunden wird, überprüfen wir das Markteintrittssignal, indem Sie die Funktion CheckPattern() aufrufen. Wir speichern anschließend das Muster bei Bedarf im Array und initialisieren die neue Klasseninstanz für die nächste Suche. Die Schleife wird fortgesetzt, bis die Methode Search() während einer der folgenden Suchen false zurückgibt.

      if(!CheckPattern(Pattern))
         continue;
      if(!ar_Objects.Add(Pattern))
         continue;
      Pattern=new CPattern();
      if(CheckPointer(Pattern)==POINTER_INVALID)
         break;
      if(!Pattern.Create(ar_Objects.At(1),d_MinCorrection,d_MaxCorrection))
        {
         delete Pattern;
         break;
        }
     }
//---
   return;
  }

Werfen wir jetzt einen Blick auf den Funktionsalgorithmus von CheckPattern(), um das Bild zu vervollständigen. Die Methode übernimmt den Pointer auf die Instanz der Klasse CPatern in den Parametern und gibt den logischen Wert des Operationsergebnisses zurück. Wenn die Funktion false zurückgibt, wird das analysierte Muster aus dem Array der gespeicherten Objekte gelöscht.

Zu Beginn der Funktion rufen wir die Methode zur Suche nach Markteintrittssignalen der Klasse CPattern auf. Wenn die Prüfung fehlschlägt, verlassen wir die Funktion mit dem Ergebnis false.

bool CheckPattern(CPattern *pattern)
  {
   int signal=0;
   double sl=-1, tp1=-1, tp2=-1;
   if(!pattern.CheckSignal(signal,sl,tp1,tp2))
      return false;

War die Suche nach einem Markteintrittssignal erfolgreich, bestimmen wir die Level der Position und senden einen Auftrag zur Positionseröffnung entsprechend dem Signal.

   double price=0;
   double to_close=100;
//---
   switch(signal)
     {
      case 1:
        price=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
        CLimitTakeProfit::Clear();
        if((tp1-price)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(CLimitTakeProfit::AddTakeProfit((uint)((tp1-price)/_Point),(fabs(tp1-tp2)>=_Point ? 50 : 100)))
              to_close-=(fabs(tp1-tp2)>=_Point ? 50 : 100);
        if(to_close>0 && (tp2-price)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(!CLimitTakeProfit::AddTakeProfit((uint)((tp2-price)/_Point),to_close))
              return false;
        if(Trade.Buy(d_Lot,_Symbol,price,sl-i_SL*_Point,0,NULL))
           return false;
        break;
      case -1:
        price=SymbolInfoDouble(_Symbol,SYMBOL_BID);
        CLimitTakeProfit::Clear();
        if((price-tp1)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(CLimitTakeProfit::AddTakeProfit((uint)((price-tp1)/_Point),(fabs(tp1-tp2)>=_Point ? 50 : 100)))
              to_close-=(fabs(tp1-tp2)>=_Point ? 50 : 100);
        if(to_close>0 && (price-tp2)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(!CLimitTakeProfit::AddTakeProfit((uint)((price-tp2)/_Point),to_close))
              return false;
        if(Trade.Sell(d_Lot,_Symbol,price,sl+i_SL*_Point,0,NULL))
           return false;
        break;
     }
//---
   return true;
  }

Wenn die Position erfolgreich geöffnet wurde, verlassen wir die Funktion mit dem Ergebnis false. Dies geschieht, um das verwendete Muster aus dem Array zu löschen. Dies ermöglicht es uns, das erneute Öffnen einer Position auf dem gleichen Muster zu vermeiden.

Der vollständige Code aller Methoden und Funktionen findet sich im Anhang.

4. Testen der Strategie

Nun, da das EA entwickelt wurde, ist es an der Zeit, seine Arbeit mit den Daten der Historie zu überprüfen. Der Test wird im Zeitraum von 9 Monaten 2018 für EURUSD durchgeführt. Die Suche nach Mustern soll auf M30 erfolgen, während auf М5 die konkreten Positionseröffnungen ermittelt werden sollen.

Testen des EATesten des EA

Die Testergebnisse zeigten die Fähigkeit des EAs, Gewinne zu generieren. Der EA erzeugte 90 Positionen (70 davon waren profitabel) innerhalb des Testzeitraums. Der Gewinnfaktor ist 2,02, der Wiederherstellungsfaktor ist 4,77, was auf die Möglichkeit der Verwendung des EA auf realen Konten hinweist. Die vollständigen Testergebnisse werden unten angezeigt.

TestergebnisseTestergebnisse

Schlussfolgerung

In diesem Artikel haben wir den EA basierend auf dem Muster von Doppelspitze/Doppelboden entwickelt. Das Testen des EAs mit historischen Daten hat akzeptable Ergebnisse und die Fähigkeit des EA, Gewinne zu generieren, gezeigt, was die Möglichkeit bestätigt, das das Muster von Doppelspitze/Doppelboden als effizientes Trendumkehrsignal bei der Suche nach Markteintrittspunkten anzuwenden.

Referenzen

  1. Wie man den Berechnungsblock eines Indikators in den Code eines Expert Advisors überträgt
  2. Verwendung von Limit-Orders anstelle von Take-Profit, ohne den ursprünglichen Code des EA zu ändern.

Programme, die im diesem Artikel verwendet werden

#
Name
Typ
Beschreibung
1 ZigZag.mqh Klassenbibliothek Klasse des Indikators ZigZag
2 Trends.mqh  Klassenbibliothek Klasse der Trendsuche
3 Pattern.mqh Klassenbibliothek Klasse für die Arbeit mit den Mustern
4 LimitTakeProfit.mqh Klassenbibliothek Klasse, um Take-Profits durch Limit-Orders zu ersetzen
5 Header.mqh Bibliothek Header Datei des Eas
6 DoubleTop.mq5 Expert Advisor Der EA auf Basis der Strategie von Doppelspitze/Doppelboden