Flaggenformation

Dmitry Fedoseev | 15 August, 2017


Inhaltsverzeichnis

Einleitung

Die Flaggenformation hat ihren Namen wegen einer deutlichen vertikalen Preisbewegung ("Fahnenmast") und einer anschließenden horizontalen Bewegung (rechteckige "Bahn") bekommen (Abb. 1).


Abb. 1. Flagge

In Büchern und auf Webseiten über die technische Analyse werden häufig die Formationen Flagge und Wimpel parallel betrachtet. Der Wimpel hat im Vergleich zur Flagge eine dreieckige Bahn (Abb. 2), deswegen wird die Flaggenformation in einigen Büchern über die technische Analyse zusammen mit der Dreieck-Formation betrachtet.


Abb. 2. Wimpel

Man könnte denken, Wimpel und Dreieck wären verschiedene Namen eines und desselben Musters. Aber in einigen Büchern über die technische Analyse, unter anderem im Buch "Encyclopedie of Chart Patterns" von Thomas N. Bulkowsi, werden diese Muster einzeln erklärt. Darüber hinaus wird in diesem Buch die Keil-Formation beschrieben, die ähnlich dem Dreieck ist, aber horizontal dargestellt wird. Links befindet sich ein schmaler Teil, und rechts bewegt sich der Preis ansteigend (Abb. 3).


Abb. 3. Keil

Neben dem Keil-Chartmuster ist auch das Steigende Dreieck bekannt sowie verschiedene viereckige Formationen, die der Flagge ähnlich sind. Dies bedeutet, dass es strikte Regeln geben soll, nach welchen sich der Wimpel vom Dreieck, der Keil vom Steigenden Dreieck und die Flagge von der Horizontalen Formation unterscheiden. Diese Frage wird im Artikel betrachtet. Darüber hinaus werden Indikatoren für die Erkennung dieser Muster erstellt. 

Unterschiede zwischen den Formationen Wimpel und Dreieck

Betrachten wir die Unterschiede zwischen dem Wimpel und Dreieck sowie aller anderen in der Einleitung erwähnten Formationen, die eine ähnliche Form haben:

  • Viereckige Formation — Flagge;
  • Dreieck — Wimpel;
  • Steigendes Dreieck — Keil.

Die Muster Rechteckige Formation, Dreieck und Steigendes Dreieck gehören zu einer Kategorie, die Muster Flagge, Wimpel und Keil - zu der anderen.

Die Muster der ersten Kategorie bilden ihre Form anhand der Umkehrpunkte des Kurses (Abb. 4), für ihre Erkennung kann man den ZigZag-Indikator verwenden.


Abb. 4. Die Muster: a — Horizontale Formation, b — Dreieck, c — Steigendes Dreieck.  
Die Muster sind für eine vermutliche Aufwärtsbewegung nach oben (für den Kauf) dargestellt.

Die Muster der zweiten Kategorie bilden eine Form, indem sie die Fläche der einer mit Balken füllen (Abb. 5). Natürlich, erscheinen sie wohl kaum auf einem Chart wie auf dem Bild. Die Grundidee besteht darin, dass ein Balken durch den benachbarten bedeckt wird, und dadurch bilden sich verschiedene Figuren. 


Abb. 5. Die Muster: a — Flagge, b — Wimpel, c — Keil.
Die Muster sind für eine vermutliche Aufwärtsbewegung (für den Kauf) dargestellt.

Nachdem wir die Kategorien und deren grundlegende Unterschiede festgelegt haben, gehen wir auf jede Formation einzeln ein. 

Horizontale Formation

Fangen wir mit den Mustern der ersten Kategorie an. Für ihre Erkennung ist der ZigZag Indikator sehr praktisch. Das erste Muster dieser Kategorie ist die horizontale Formation, der die Flaggenformation in der zweiten Kategorie entspricht. In den meisten Anleitungen zur technischen Analyse wird gerade die horizontale Formation als Flaggenformation bezeichnet.

Damit die Horizontale Formation sich auf dem Chart erkennen lässt, muss der Preis eine deutliche vertikale Bewegung aufweisen und danach mindestens zwei Hochs auf einer Ebene und zwei Tiefs (auch auf einer Preisebene) bilden. Der Preis kann je drei Hochs und Tiefs (Abb. 6), oder auch mehr bilden. Deswegen gibt es im Indikator einen Parameter, der die Anzahl der Hochs und Tiefs in einem Muster festlegt.


Abb. 6. Horizontale Formationen: a — von zwei Hochs/Tiefs, b — von drei Hochs/Tiefs.
Die Muster sind für eine vermutliche Aufwärtsbewegung (für den Kauf) dargestellt 

Der obere und untere Rand des Musters müssen nicht unbedingt horizontal sein. Es wichtig, dass sie parallel sind, deswegen wird der Indikator einen weiteren Parameter für die Auswahl der Steigung haben: horizontal, mit Steigung nach oben, mit Steigung nach unten (Abb. 7).


Abb. 7. a — Horizontale Formation, b — Formation mit der Steigung nach unten, c — Formation mit der Steigung nach oben. 
Die Muster für eine vermutliche Aufwärtsbewegung (für den Kauf) dargestellt

Natürlich kann man Formationen mit einer Steigung nach oben und unten nicht mehr als horizontal bezeichnen, nichtsdestotrotz sind sie der Horizontalen Formation sehr ähnlich.

Die Bildung eines Musters ist dann abgeschlossen, wenn der Preis den von den Spitzen gebildeten Level durchbricht (Abb. 8).

 
Abb. 8. Ende der Formation und der Einstieg
Position buy: a — für die horizontale Formation,
b — für die Formation mit der Steigung nach unten 

Der Einstiegs für eine Formation mit Steigung nach oben wird ohne Berücksichtigung der Steigung von Spitzen festgelegt — einfach die horizontale Ebene vom letzten Hoch (Abb. 9).


Abb. 9. Erkennung des Einstiegs (Kauf) für die Formation mit der Steigung nach oben

Eine einfache Variante mit der horizontalen Ebene kann auch für eine Formation mit Steigung nach unten verwendet werden, deswegen wird eine Variable für die Auswahl der Ebene unabhängig vom Formationstyp im Indikator vorhanden sein.

Das Ziel für die horizontale Formation wird nach der Größe der vertikalen Bewegung bis zum Moment der Bildung des Musters bestimmt. Der Preis muss die gleich lange Strecke vor einer Formation und nach nach der Formation gehen (Abb. 10).


Abb. 10. Festlegung des Ziels. Die Strecke L1 vor dem Beginn
der Formation ist gleich der Strecke L2 nach der Formation.  

Da die obere und die untere Grenzen der Formation parallel sind, kann man eine einfachere Methode zur Bestimmung des Ziels verwenden — den Abstand bis zur ersten Spitze der Formation messen und ihn nach oben bis zum letzten Tief zeichnen (Abb. 11).


Abb. 11. Eine einfache Methode für die Festlegung des Ziels. Die Strecke L1, die der Preis bis zur Bildung
des ersten Hochs hinter sich lässt,
ist gleich der Strecke L2 vom letzten Tief bis zum Ziel


Fallendes Dreieck

Das Muster "Fallendes Dreieck" unterscheidet sich geringfügig von der Horizontalen Formation. Der einzige Unterschied besteht darin, dass die Strecken des ZigZags, die das Muster bilden, immer kürzer werden sollen (Abb. 12).


 Abb. 12. Fallendes Dreieck. Die Strecke 3-4 muss
kürzer als die Strecke 1-2 sein, und die Strecke 5-6 muss kürzer als die Strecke 3-4 sein
 

Im Übrigen ist alles genauso wie bei der Horizontalen Formation: die horizontale Platzierung des Dreieck oder mit Steigung nach oben/nach unten, Einstieg beim Ausbruch der Widerstandslinie, die von den zwei letzten Hochs gebildet wurde, die gleiche Berechnung des Ziels.

Steigendes Dreieck

Alles was für das Fallende Dreieck galt, trifft auch für das Steigende Dreieck zu. Der einzige Unterschied besteht darin, dass die Strecken des ZigZags, die das Muster bilden, nun länger werden (Abb. 13).


 Abb. 13. Steigendes Dreieck. Die Strecke 3-4 muss 
länger als die Strecke 1-2 sein, und die Strecke 5-6 muss länger als die Strecke 3-4 sein
 

Diese Ähnlichkeit aller drei Formationen schafft Voraussetzungen für die Erstellung eines universellen Indikators für deren Erkennung.  

Universeller Indikator für die Erkennung der horizontalen Formation und der Dreieck-Formationen

Für die Erstellung des Indikators wird der Indikator iUniZigZagSW aus dem Artikel "Universeller ZigZag" benötigt. Für seine Arbeit brauchen wir die folgenden zusätzlichen Dateien: CSorceData.mqh, CZZDirection.mqh und CZZDraw.mqh. Diese Dateien sowie die Datei  iUniZigZagSW.mq5 finden Sie im Anhang zum Artikel "Universeller Zigzag". Laden Sie ihn herunter, entpacken Sie die Zip-Datei und kopieren Sie den Ordner MQL5 in den Ordner des Terminals. Nach dem Kopieren erscheint der Ordner ZigZag mit mehreren Dateien (darunter die Datei iUniZigZagSW.mq5) im Ordner MQL5/Indicators, und im Ordner MQL5/Includes — der Ordner ZigZag mit den Dateien CSorceData.mqh, CZZDirection.mqh und CZZDraw.mqh. Nach dem Kopieren der Dateien starten Sie das Terminal erneut, damit die Indikatoren kompiliert werden oder kompilieren Sie sie einzeln im MetaEditor. Bitte vergewissern Sie sich, dass der iUniZigZagSW Indikator funktionsfähig ist, indem Sie ihn auf einen Chart im Terminal ziehen.

Im Artikel "Wolfe Wellen" wurde während einer Zwischenphase der Erstellung des Indikators die DateiiWolfeWaves_Step_1.mq5 gespeichert. In der Datei wird der Indikator iUniZigZagSW.mq5 über die Funktion iCustom() aufgerufen, und es wird ein Array mit allen Hochs und Tiefs des ZigZags erstellt. Laden Sie den Anhang zum Artikel "Wolfe Wellen" herunter, entpacken Sie ihn, kopieren Sie die Datei iWolfeWaves_Step_1.mq5 in den Ordner MQL5/Indicators, benennen Sie sie in "iHorizontalFormation" um und öffnen Sie sie im MetaEditor. Die weitere Arbeit an der Erkennung des Musters der Horizontalen Formation erfolgt in dieser Datei. Wahrscheinlich muss der Pfad zum iUniZigZagSW Indikator in der Datei geändert werden. Um dies zu prüfen, kompilieren Sie den Indikator und versuchen Sie ihn dem Chart hinzuzufügen. Wenn sich dabei ein Fenster mit der Meldung "Error load indicator" öffnet, finden Sie den Aufruf von iCustom() in der Funktion OnInit() und ändern Sie den Namen des Indikators von "iUniZigZagSW" in "ZigZags\\iUniZigZagSW". Nachdem die Änderung vorgenommen wurde, kompilieren Sie den Indikator erneut und vergewissern Sie sich, dass er ohne Fehlermeldungen dem Chart hinzugefügt werden kann. Der Indikator zeichnet noch nichts.

Der ganze Prozess der Erkennung der Formationen kann in mehrere voneinander fast unabhängige Aufgaben geteilt werden:

  1. Bestimmung der Größe der Kursbewegung vor der Formation.
  2. Erkennung der Form der Formation.
  3. Erkennung der Steigung der Formation.
  4. Abschluss der Formation: direkt nach der Formation oder die Erwartung des Ausbruchs des Levels.
  5. Berechnung des Ziels.

Die Lösung jeder Aufgabe (außer der ersten) wird auf verschiedene Weisen dargestellt. So erreichen wir, dass der Indikator universell ist und für die Erkennung aller drei Formationen angewandt werden kann. Der Wechsel zwischen den Varianten erfolgt im Fenster der Eigenschaften des Indikators über Dropdown-Listen der Aufzählungen. 

Die Aufzählung für die Auswahl der Form (des Typs der Formation):

enum EPatternType{
   PatternTapered,
   PatternRectangular,
   PatternExpanding
};

Die entsprechende Variable im Fenster der Eigenschaften:

input EPatternType         Pattern        =  PatternRectangular;

Dieser Parameter erlaubt es, die Form des Musters auszuwählen: PatternTapered — fallendes Dreieck, PatternRectangular — Rechteck, PatternExpanding — steigendes Dreieck.

Die Aufzählung für die Auswahl der Steigung des Musters:

enum EInclineType{
   InclineAlong,
   InclineHorizontally,
   InclineAgainst
};

Die entsprechende Variable im Fenster der Eigenschaften:

input EInclineType         Incline        =  InclineHorizontally; 

Dieser Parameter erlaubt es, die Steigung des Musters auszuwählen: InclineAlong — Steigung in Richtung der vermutlichen Bewegung (für Buy nach oben, für Sell — nach unten), InclineHorizontally — ohne Steigung, InclineAgainst — Steigung gegen die Richtung der vermutlichen Kursbewegung.

Die Aufzählung für die Auswahl der Methode zum Abschluss der Formation des Musters:

enum EEndType{
   Immediately,
   OneLastVertex,
   TwoLastVertices
};

Die entsprechende Variable im Fenster der Eigenschaften:

input EEndType             CompletionType =  Immediately;

Dieser Parameter erlaubt es, die folgenden Varianten auszuwählen: Immediately — direkt nach der Bildung des Musters, OneLastVertex — nach dem Ausbruch des horizontalen Levels, der durch das letzte Hoch des Musters gebildet wurde, TwoLastVertices — nach dem Ausbruch des Levels, der durch die zwei letzten Hochs des Musters gebildet wurde.

Die Aufzählung für die Auswahl einer Variante für die Berechnung des Ziels:

enum ETargetType{
   FromVertexToVertex,
   OneVertex,
   TwoVertices
};

Die entsprechende Variable im Fenster der Eigenschaften:

input ETargetType          Target         =  OneVertex;

Dieser Parameter erlaubt es, zwischen den folgenden Varianten zu wählen: FromVertexToVertex — von Hoch zu Hoch (Abb. 11), OneVertex — von einem Hoch (Abb. 10), TwoVertices — von zwei Hochs (es werden zwei erste Tiefs des Musters verwendet, s. Abb. 14).


Abb. 14. Das Muster von drei Hochs. Die Variante für die Festlegung des Ziels — TwoVertices,
der Abschluss des Musters — OneLastVertex.

Bei der Beendigung Immediately gilt der Parameter Target nicht, denn es gibt nur eine Variante für die Festlegung des Ziels — FromVertexToVertex. Für die zwei weiteren Varianten der Beendigung des Musters (OneLastVertex und TwoLastVertices), sind verschiedene Kombinationen mit allen drei Varianten des Parameters CompletionType möglich. Bitte beachten Sie eine Besonderheit: in den Varianten OneVertex und TwoVertices werden für die Bestimmung des Ziels das erste oder die ersten zwei Tiefs (Punkt 2 oder die Punkte 2 und 4 auf der Abb. 14) verwendet, und die Ermittlung des Ausbruchslevels werden das erste oder die zwei ersten Hochs (Punkt 5 oder die Punkt 3 und 5 auf der Abb. 14) verwendet. Beim Muster aus zwei Hochs hätten wir den Punkt 3 oder die Punkte 1 und 3 verwendet.

Für die Lösung der ersten Aufgabe wird ein Parameter benötigt, der Größe der Kursbewegung vor der Formationen festlegt:

input double               K1             =  1.5;

Die Höhe der Strecke 1-2 (s. Abb. 14) gilt als Basis des Musters (Referenz-Größe), alle Prüfungen der Größen werden hinsichtlich dieser Größe durchgeführt. Der Parameter K1 definiert, um welchen Faktor die Strecke 0-1 länger als die Strecke 1-2 sein soll.

Um die zweite Aufgabe zu lösen (Bestimmung der Form des Musters) wird der Parameter K verwendet:

input double               K2             =  0.25;

Je kleiner der Wert des Parameters ist, desto stabiler wird die Höhe des Musters in seiner Kontinuität sein. Für Dreieck-Formationen (steigende und fallende) bedeutet die Erhöhung des Parameters die Suche nach Muster mit einer stark ausgeprägten dreieckigen Form.

Für die Lösung der dritten Aufgabe (Bestimmung der Steigung des Musters) wird der Parameter K3 verwendet:

input double               K3             =  0.25;

Je kleiner dieser Parameter ist, desto gleichmäßiger muss das Muster ausgerichtet sein. Bei der Suche nach Mustern mit Steigung erlaubt die Erhöhung des Paramerters K2, Muster mit einer stark ausgeprägten Steigung zu ermitteln.

Schließlich, einer der wichtigsten Parameter:

input int                  N              =  2;

Der Parameter N bestimmt die Anzahl der Spitzen des Musters. 

Als Ergebnis erhalten wir die folgenden externen Parameter (außer den Parametern des ZigZags):

input EPatternType         Pattern        =  PatternRectangular;
input EInclineType         Incline        =  InclineHorizontally;     
input double               K1             =  1.5;
input double               K2             =  0.25;
input double               K3             =  0.25;
input int                  N              =  2;
input EEndType             CompletionType =  Immediately;
input ETargetType          Target         =  OneVertex;

Verwenden wir den Parameter N und berechnen, wie viele Punkte des ZigZahs für die Erkennung eines Musters benötigt werden. Deklarieren wir zuerst eine globale Variable:

int RequiredCount;

Berechnen wir ihren Wert in der Funktion OnInit():

RequiredCount=N*2+2;

2*N — Anzahl der Spitzen, die das Muster bilden (N obere und N untere). Eine weitere Spitze bestimmt die vorherige Strecke der Kursbewegung hat und eine weitere stellt den letzten Punkt der neuen Strecke des ZigZags dar (wird bei der Berechnung nicht verwendet).

Die Arbeit wird in der Funktion OnTick() weitergeführt. Der neue Code wird am Ende der Hauptschleife des Indikators hinzugefügt. Die Prüfung der Bedingungen der Formation erfolgt, wenn es genug Punkte des ZigZags gibt und nur wenn er seine Richtung ändert. Der Preis und der Level werden bei jeder Änderung des ZigZags verfolgt:

if(CurCount>=RequiredCount){
   if(CurDir!=PreDir){      
      // Prüfung der Bedingungen

   }
   // Verfolgung des Preises und Levels

} 

Zuerst wird die Basisgröße berechnet — die Höhe der Strecke 1-2 (s. Abb. 14). Dieser Wert wird bei der Prüfung aller Bedingungen für die Formation verwendet. Dach wird die Bedingung der ersten Aufgabe geprüft — der Größe der vorherigen Kursbewegung:

int li=CurCount-RequiredCount;                                            // Index des ersten Punktes der Formation im Array PeackTrough
double base=MathAbs(PeackTrough[li+1].Val-PeackTrough[li+2].Val);        // Basisgröße
double l1=MathAbs(PeackTrough[li+1].Val-PeackTrough[li].Val);             // die Höhe der Strecke 1-2
   if(l1>=base*K1){                                                       // Prüfung der Größe der vorherigen Kursbewegung
        // weitere Prüfungen

   }

Die weiteren Überprüfungen werden davon abhängen, ob die letzte Strecke des ZigZags nach oben oder nach unten gerichtet ist.

if(CurDir==1){              // die letzte Strecke des ZigZags ist nach oben gerichtet
   // Prüfung der Bedingungen für die Richtung nach oben  
             
}
else if(CurDir==-1){        // die letzte Strecke des ZigZags ist nach unten gerichtet
   // Prüfung der Bedingungen für die Richtung nach unten

}

Gehen wir auf die Prüfung der Bedingung für die Richtung nach oben ein:

if(CheckForm(li,base) && CheckInclineForBuy(li,base)){      // Prüfung der Form und der Richtung
   if(CompletionType==Immediately){ 
      // setzen wir direkt einen Pfeil
      UpArrowBuffer[i]=low[i];
      // das Ziel setzen
      UpDotBuffer[i]=PeackTrough[CurCount-1].Val+l1;
   }
   else{
      // Parameter des Levels setzen, der durchbrochen wird
      SetLevelParameters(1);
      // Parameter des Ziels setzen
      SetTarget(1,li);
   }
} 

Die Bedingung der Formation wird durch zwei Funktionen geprüft: CheckForm() — Prüfung der Form (Aufgabe 2) und CheckInclineForBuy() — Prüfung der Steigung (Aufgabe 3). Wenn die Form und Steigung geprüft wurden, wird je nach Typ des Abschlusses der Formation entweder ein Pfeil und ein Zielpunkt auf den Chart gesetzt, oder es werden die Parameter des durchbrochenen Levels gesetzt, danach verfolgt der Indikator den Level.

Die Funktion CheckForm(). Der Funktion wird der Index des ersten Punktes der Formation im Array PeackTrough und die Basisgröße base übergeben. Betrachten wir den Code dieser Funktion:

bool CheckForm(int li,double base){               
   switch(Pattern){
      case PatternTapered: 
         // fallend
         return(CheckFormTapered(li,base));
      break;               
      case PatternRectangular: 
         // rechteckig
         return(CheckFormRectangular(li,base));
      break;
      case PatternExpanding: 
         // steigend
         return(CheckFormExpanding(li,base));
      break;
   }
   return(true);
}

Je nach dem Wert des Parameters Pattern werden die folgenden Funktionen aufgerufen: CheckFormTapered() — fallendes Dreieck, CheckFormRectangular() — rechteckige Formation, CheckFormExpanding() — steigendes Dreieck.

Die Funktion CheckFormTapered():

bool CheckFormTapered(int li,double base){
   // Schleife von 1, die erste Strecke wird nicht überprüft,
   // aber alle weiteren werden hinsichtlich dieser Strecke geprüft  
   for(int i=1;i<N;i++){ 
      // Berechnung des Index des nächsten Hochs des Musters 
      int j=li+1+i*2;
      // Größe der nächsten Strecke 
      double lv=MathAbs(PeackTrough[j].Val-PeackTrough[j+1].Val);
      // Wert der vorherigen Strecke
      double lp=MathAbs(PeackTrough[j-2].Val-PeackTrough[j-1].Val);
      // die vorherige Strecke muss länger sein, andernfalls
      // gibt die Funktion false zurück   
      if(!(lp-lv>K2*base)){
         return(false);
      }
   } 
   return(true);
}

In der Funktion durchläuft die Schleife die Strecken des ZigZags, die eine Formation bilden. Jede nächste Strecke muss kleiner als die vorherige sein.

Die Funktion CheckFormExpanding() ist gleich, sie unterscheidet sich nur durch eine Bedingung:

if(!(lv-lp>K2*base)){
   return(false);
}

Für die Erfüllung dieser Bedingung muss jede weitere Strecke größer als die vorherige sein.

Die Funktion CheckFormRectangular():

bool CheckFormRectangular(int li,double base){   
   // Durchlauf aller Hochs, außer dem ersten      
   for(int i=1;i<N;i++){
      // Berechnung des Index des nächsten Hochs  
      int j=li+1+i*2; 
      // Berechnung der Größe der nächsten Strecke
      double lv=MathAbs(PeackTrough[j].Val-PeackTrough[j+1].Val);
      // die Strecke darf sich nicht stark von der Basisgröße unterscheiden 
      if(MathAbs(lv-base)>K2*base){
         return(false); 
      }
   }
   return(true);
}

In dieser Funktion wird jede Strecke mit der Basisgröße verglichen. Wenn der Unterschied zwischen ihnen groß ist, gibt die Funktion false zurück.

Wenn die Prüfung der Form erfolgreich abgeschlossen wurde, wird die Steigung überprüft. Die Funktion CheckInclineForBuy():

bool CheckInclineForBuy(int li,double base){                 
   switch(Incline){
      case InclineAlong:
         // Steigung in Trendrichtung
         return(CheckInclineUp(li,base));
      break;
      case InclineHorizontally:
         // ohne Steigung
         return(CheckInclineHorizontally(li,base));
      break;                     
      case InclineAgainst:
         // Steigung gegen Trendrichtung
         return(CheckInclineDn(li,base));
      break;
   } 
   return(true);
}  

Die Funktion für die Prüfung der Steigung für Sell unterscheidet sich nur durch zwei Zeilen:

bool CheckInclineForSell(int li,double base){                 
   switch(Incline){
      case InclineAlong:
         // Steigung in Trendrichtung
         return(CheckInclineDn(li,base));
      break;
      case InclineHorizontally:
         // ohne Steigung
         return(CheckInclineHorizontally(li,base));
      break;                     
      case InclineAgainst:
         // Steigung gegen Trendrichtung
         return(CheckInclineUp(li,base));
      break;
   } 
   return(true);
} 

Wenn Incline gleich InclineAlong ist, wird für den Kauf die Funktion CheckInclineUp() und für den Verkauf - die Funktion CheckInclineDn() aufgerufen. Für den Fall wenn Incline gleich InclineAgainst ist, gilt das Gegenteil.

Die Funktion für die Prüfung der Steigung des Musters nach oben — CheckInclineUp():

bool CheckInclineUp(int li,double base){   
   // Durchlauf aller Hochs, außer dem ersten      
   for(int v=1;v<N;v++){
      // Berechnung des Index des nächsten Hochs
      int vi=li+1+v*2;
      // Berechnung der Mitte der nächsten Strecke des ZigZags
      double mc=(PeackTrough[vi].Val+PeackTrough[vi+1].Val)/2;
      // Berechnung der Mitte der vorherigen Strecke des ZigZags
      double mp=(PeackTrough[vi-2].Val+PeackTrough[vi-1].Val)/2;
      // die nächste Strecke muss größer als die vorherige sein 
      if(!(mc>mp+base*K3)){
         return(false);
      }
   }
   return(true);
} 

In der Funktion werden alle Strecken des ZigZags durchlaufen, für jede wird die Mitte berechnet und mit der Mitte der vorherigen Strecke verglichen. Jede Strecke muss um base*K3 höher als die vorherige sein.

Die Funktion für die Prüfung des Musters nach unten, CheckInclineDn(), unterscheidet sich nur durch eine Bedingung:

if(!(mc<mp-base*K3)){
   return(false);
}

Für die Erfüllung dieser Bedingung muss jede weitere Strecke tiefer als die vorherige liegen.

Die Funktion CheckInclineHorizontally():

bool CheckInclineHorizontally(int li,double base){ 
   // die Mitte der Basis-Strecke
   double mb=(PeackTrough[li+1].Val+PeackTrough[li+2].Val)/2;        
   for(int v=1;v<N;v++){
      // Index des nächsten Hochs
      int vi=li+1+v*2;
      // die Mitte der nächsten Strecke
      double mc=(PeackTrough[vi].Val+PeackTrough[vi+1].Val)/2;
      // die Mitte der nächsten Strecke darf sich nicht stark 
      // von der Mitte der Basis-Strecke unterscheiden
      if(MathAbs(mc-mb)>base*K3){
         return(false);
      }
   }                  
   return(true);
}

Wenn die Prüfungen der Bedingungen für die Form und Steigung abgeschlossen sind, wird der nächste Code-Teil ausgeführt:

if(CompletionType==Immediately){                    // Einstieg sofort
   UpArrowBuffer[i]=low[i];
   UpDotBuffer[i]=PeackTrough[CurCount-1].Val+l1;
}
else{                                               // auf den Ausbruch des Levels warten
   SetLevelParameters(1);
   SetTarget(1,li);
}

Bei der Variante Immediately zeichnet der Indikator einen Pfeil und setzt einen Zielpunkt, in anderen Fällen werden der Ausbruchslevel durch die Funktion SetLevelParameters() und das Ziel - durch die Funktion SetTarget() gesetzt.

Die Funktion SetLevelParameters():

void SetLevelParameters(int dir){
   CurLevel.dir=dir;   
   switch(CompletionType){
      case OneLastVertex:                            // nach einem Punkt
          CurLevel.v=PeackTrough[CurCount-3].Val;
      break;
      case TwoLastVertices:                          // nach zwei Punkten
         CurLevel.x1=PeackTrough[CurCount-5].Bar;
         CurLevel.y1=PeackTrough[CurCount-5].Val;
         CurLevel.x2=PeackTrough[CurCount-3].Bar;
         CurLevel.y2=PeackTrough[CurCount-3].Val;
      break;
   }
} 

In der Funktion SetLevelParameters() wird die Struktur SLevelParameters für das Speichern der Parameter des Levels verwendet:

struct SLevelParameters{
   int x1;
   double y1;
   int x2;
   double y2;       // von x1 bis y2 - Parameter des Steigungslevels
   double v;        // Wert des horizontalen Levels
   int dir;         // Richtung
   double target;   // Ziel
   // Methode für die Berechnung des Wertes des Steigungslevels
   double y3(int x3){
      if(CompletionType==TwoLastVertices){
            return(y1+(x3-x1)*(y2-y1)/(x2-x1));
      }
      else{
         return(v);
      }
   }
   // Methode zur Initialisierung oder Zurücksetzung der Parameter
   void Init(){
      x1=0;
      y1=0;
      x2=0;
      y2=0;
      v=0;
      dir=0;   
   }
};

Die Struktur beinhaltet die Felder für die Parameter der Linie: x1, y1, x2, y2; das Feld v für den horizontalen Level; d - Richtung des Musters; target - das Ziel. Das Ziel kann sowohl durch den Preislevel (bei der Variante FromVertexToVertex), als auch durch die Größe vom Ausbruchslevel (für die Varianten OneVertex und TwoVertices) gesetzt werden. Die y3() wird für die Berechnung des Steigungslevels verwendet. Die Methode Init() wird für die Initialisierung oder Zurücksetzung der Werte genutzt.

Wenn alle Bedingungen für die Formation erfüllt sind, wird die Funktion SetLevelParameter() aufgerufen. In dieser Funktion werden je nach ausgewähltem Leveltyp (horizontal oder mit Steigung) werden entweder die Parameter des Steigungslevels (Feld x1, y1, x2, y2) oder der Wert des horizontalen Levels - v - gesetzt. In der Methode y3() wird der Level unter Verwendung der Felder x1, y1, x2, y2 berechnet oder der Wert des Feldes v zurückgegeben.

Im Indikator werden zwei Variablen vom Typ SLevelParameters deklariert:

SLevelParameters CurLevel;
SLevelParameters PreLevel;

Dieses Variablenpaar wird analog zu den Variablenpaaren CurCount-PreCount und CurDir-PreDir verwendet - vor der ersten Berechnung des Indikators werden die Variablen auf Null gesetzt (der Code-Teil befindet sich ganz am Anfang der Funktion OnTick()):

int start;

if(prev_calculated==0){           // die erste Berechnung nach allen Balken
   start=1;      
   CurCount=0;
   PreCount=0;
   CurDir=0;
   PreDir=0;  
   CurLevel.Init();    
   CurLevel.Init();
   LastTime=0;
}
else{                           // Berechnung neuer Balken und des sich bildenden Balkens 
   start=prev_calculated-1;
}

Bei der Berechnung jedes Balkens werden die Werte aus einer Variablen in die andere Variablen übertragen bzw. kopiert (der Code befindet sich am Anfang der Schleife des Indikators):

if(time[i]>LastTime){
   // die erste Berechnung eines neuen Balkens
   LastTime=time[i];
   PreCount=CurCount;
   PreDir=CurDir;
   PreLevel=CurLevel;
}
else{
   // erneute Berechnung des Balkens
   CurCount=PreCount;
   CurDir=PreDir;
   CurLevel=PreLevel;
}

Durch den Aufruf der Funktion SetTarget() werden die Parameter des Ziels gesetzt:

void SetTarget(int dir,int li){
   switch(Target){
      case FromVertexToVertex:
         // Variante "von Hoch bis Hoch"
         if(dir==1){
            CurLevel.target=PeackTrough[CurCount-1].Val+(PeackTrough[li+1].Val-PeackTrough[li].Val);
         }
         else if(dir==-1){
            CurLevel.target=PeackTrough[CurCount-1].Val-(PeackTrough[li].Val-PeackTrough[li+1].Val);
         }
      break;
      case OneVertex:
         // von einer Spitze
         CurLevel.target=MathAbs(PeackTrough[li].Val-PeackTrough[li+2].Val);
      break;
      case TwoVertices:
         // von zwei Spitzen
         SetTwoVerticesTarget(dir,li);
      break;
   }
}

Für die Variante FromVertexToVertex wird der Preiswert berechnet. Für die Variante OneVertex wird dem Feld target der Wert der Preisbewegung vom Ausdruchslevels bis zum Ziel zugewiesen. Die Berechnung für die Variante SetTwoVerticesTarget wird in der Funktion SetTwoVerticesTarget() durchgeführt:

void SetTwoVerticesTarget(int dir,int li){
   // die Koordinaten der ersten (langen) Linie
   // der Formation - vom Tief zum Hoch  
   double x11=PeackTrough[li].Bar;
   double y11=PeackTrough[li].Val;
   double x12=PeackTrough[li+1].Bar;
   double y12=PeackTrough[li+1].Val;
   // die Koordinaten der Linie nach zwei Tiefs für den Kauf
   // oder nach zwei Hochs für den Verkauf
   double x21=PeackTrough[li+2].Bar;
   double y21=PeackTrough[li+2].Val;
   double x22=PeackTrough[li+4].Bar;
   double y22=PeackTrough[li+4].Val;
   // Wert im Schnittpunkt der Linien
   double t=TwoLinesCrossY(x11,y11,x12,y12,x21,y21,x22,y22);
   // Setzen des Ziels je nach Richtung
   if(dir==1){
      CurLevel.target=t-PeackTrough[li].Val;
   }
   else if(dir==-1){
      CurLevel.target=PeackTrough[li].Val-t;         
   }
}

Für die Variante SetTwoVerticesTarget erhält das Feld target den Wert der Preisbewegung vom durchbrochenen Level bis zum Ziel, genauso wie für die Variante OneVertex.

Betrachten wir, wie der Kurs und der Level verfolgt werden (CompletionType ist nicht gleich Immediately):

// unter Verwendung des Levels
if(CompletionType!=Immediately){
   // es gibt Änderungen des ZigZags
   if(PeackTrough[CurCount-1].Bar==i){
      if(CurLevel.dir==1){                // warten wir auf einen Ausbruch nach oben
         // Erhalten des Wertes des Levels in die Variable cl
         double cl=CurLevel.y3(i); 
         // ZigZag durchbricht den Level
         if(PeackTrough[CurCount-1].Val>cl){
            // Setzen eines Pfeils nach oben
            UpArrowBuffer[i]=low[i];
            // Setzen des Ziels
            if(Target==FromVertexToVertex){
               // Preis im Feld target
               UpDotBuffer[i]=CurLevel.target;                        
            }
            else{
               // Abstand vom Level im Feld target
               UpDotBuffer[i]=cl+CurLevel.target;
            }
            // das Feld dir auf Null setzten, damit der Level nicht mehr verfolgt wird
            CurLevel.dir=0;
         }
      }
      else if(CurLevel.dir==-1){         // auf einen Ausbruch nach unten warten
         // Erhalten des Wertes des Levels in die Variable cl
         double cl=CurLevel.y3(i);
         // ZigZag durchbricht den Level
         if(PeackTrough[CurCount-1].Val<cl){
            // Setzen eines Pfeils nach unten
            DnArrowBuffer[i]=low[i];
            // Setzen des Ziels
            if(Target==FromVertexToVertex){
               // Preis im Feld target
               DnDotBuffer[i]=CurLevel.target;
            }
            else{                     
               // Abstand vom Level im Feld target
               DnDotBuffer[i]=cl-CurLevel.target;
            }
            // das Feld dir auf Null setzten, damit der Level nicht mehr verfolgt wird
            CurLevel.dir=0;
         }         
      }         
   }
} 

Diese Prüfung wird bei jeder Änderung des ZigZags durchgeführt. Alle Spitzen des ZigZags werden im Array PeackTrough gespeichert, die Änderung im ZigZag wird nach der Entsprechung des letzten Punkten des ZigZags dem Index des aktuellen Balkens festgelegt:

if(PeackTrough[CurCount-1].Bar==i){

Mithilfe der Methode y3() wird der aktuelle Wert des Levels berechnet:

double cl=CurLevel.y3(i); 

es wird geprüft, ob die letzte Strecke des ZigZags diesen Level durchbrochen hat:

if(PeackTrough[CurCount-1].Val>cl){

Wenn ein Level durchbrochen wurde, zeichnet der Indikator einen Pfeil und setzt einen Zielpunkt. Das Feld target kann den Kurswert des Ziels beinhalten, in diesem Fall wird dieser Wert verwendet, oder es kann den Wert der Preisbewegung bis zum Ziel beinhalten, in diesem Fall wird der Zielwert unter Berücksichtigung des aktuellen Wertes des Levels berechnet: 

if(Target==FromVertexToVertex){
   UpDotBuffer[i]=CurLevel.target;                        
}
else{
   UpDotBuffer[i]=cl+CurLevel.target;
}

Am Ende wird das Feld dir auf Null gesetzt, um den Kurs bis zum Auftreten des nächsten Musters nicht mehr zu verfolgen:

CurLevel.dir=0;

Somit ist die Erstellung des Indikators abgeschlossen, einige Fragmente des Codes sind auf dem Bild 15 dargestellt.


Abb. 15 Einige Signale des Indikators iHorizontalFormation

Dem Indikator wurde eine zusätzliche Alert-Funktion hinzugefügt. Der fertige Indikator ist im Anhang unter dem Namen iHorizontalFormation zu finden.

Universeller Indikator für die Erkennung der Formationen Flagge, Wimpel und Keil

Nun erstellen wir einen Indikator für die Erkennung der Muster der zweiten Kategorie. Sie bilden eine Form, indem sie die Fläche einer Figur mit Balken füllen. Die Formation beginnt mit einer starken Kursbewegung, in diesem Fall — mit einem langen Balken. Für die Erkennung der langen Balken verwenden wir den ATR Indikator mit einer langen Periode. Ein Balken gilt als lang, wenn der Wert seines Schattens den Wert des ATR, multipliziert mit einem Koeffizienten, übersteigt. Das heißt es werden externe Parameter für die Periode des ATR und der Koeffizient benötigt:

input int                  ATRPeriod            =  50;
input double               K1                   =  3;

Deklarieren wir eine globale Variable des Indikators für den Handle des ATR:

int h;

In der Funktion OnInit() laden wir den Indikator ATR und erhalten den Wert des Handles:

h=iATR(Symbol(),Period(),ATRPeriod);
if(h==INVALID_HANDLE){
   Alert("Error load indicator");
   return(INIT_FAILED);
}

In der Hauptschleife des Indikators erhalten wir den Wert des ATR:

double atr[1];
if(CopyBuffer(h,0,rates_total-i-1,1,atr)==-1){
   return(0);
}

Verwenden wir den erhaltenen Wert des ATR und überprüfen wir die Größe des Balkens. Wenn die Größe des Schattens des Balkens den Schwellenwert, der durch den Koeffizienten gesetzt wird, übersteigt, werden wir die Richtung einer vermutlichen Kursbewegung ermitteln. Die Richtung wird nach der Farbe des Balkens festgelegt (nach Open und Close). Wenn der Schlusskurs größer als der Eröffnungskurs ist, wird eine weitere Aufwärtsbewegung vermutet. Wenn der Schlusskurs kleiner als der Eröffnungskurs wird, wird eine Abwärtsbewegung vermutet:

if(high[i]-low[i]>atr[0]*K1){    // langer Balken
   if(close[i]>open[i]){         // der Balken ist nach oben gerichtet
      Cur.Whait=1;
      Cur.Count=0;
      Cur.Bar=i;
   }
   else if(close[i]<open[i]){    // der Balken ist nach unten gerichtet
      Cur.Whait=-1;   
      Cur.Count=0;
      Cur.Bar=i;
   }
}

Bei der Erfüllung der Bedingungen hinsichtlich der Größe und Richtung des Balkens werden der Cur Struktur die folgenden Werte zugewiesen: im Feld Whait wird die vermutliche Richtung angegeben (1 — nach oben, -1 — nach unten), das Feld Count wird zurückgesetzt. Ihm wird der Wert 0 zugewiesen, dieses Feld wird für die Berechnung der Anzahl der Balken im Muster verwendet. Im Balken Bar wird der Index des ersten (langen) Balkens des Musters gespeichert.

Gehen wir ausführlicher auf die Struktur Cur ein. Die Struktur beinhaltet drei Felder und die Methode Init(), um alle Felder schnell auf Null zu setzen:

struct SCurPre{
   int Whait;
   int Count;
   int Bar;
   void Init(){
      Whait=0;
      Count=0;
      Bar=0;
   }
};

Am Anfang der Funktion OnTick() wurden zwei statische Variablen von diesem Typ und eine Variable vom Typ datetime deklariert:

static datetime LastTime=0;   
static SCurPre Cur;          
static SCurPre Pre;

Danach wird der Index des ersten Balkens berechnet, mit welchem die Berechnung des Indikators beginnt, und die Variablen Cur und Pre werden initialisiert:

int start=0;

if(prev_calculated==0){           // die erste Berechnung des Indikators
   
   start=1;      
   
   Cur.Init();
   Pre.Init();             
     
   LastTime=0;      
}
else{                             // Berechnung der neuen Balken und des sich bildenden Balkens
   start=prev_calculated-1;
}

Am Anfang der Hauptschleife des Indikators werden die Werte in den Variablen Cur und Pre verschoben:

if(time[i]>LastTime){       // die erste Berechnung des Balkens
   LastTime=time[i];
   Pre=Cur;              
}
else{                      // erneute Berechnung des Balkens
   Cur=Pre;
}  

Dieser Ansatz mit den Variablen wurde im Artikel "Wolfe Wellen" (die Variablen PreCount und CurCount) ausführlich beschrieben. In diesem Artikel wurde er bei der Erstellung des Indikators iHorizontalFormation (Variablen mit den Präfixen Cur und Pre) verwendet.

Wenn die Variable Cur.Count nicht gleich Null ist, heißt das, dass der Indikator die Bedingungen für die Erkennung des Musters verifiziert. Dabei wird die Anzahl der Balken berechnet, die das Muster bilden — die Variable CurCount wird vergrößert. Der erste Balken nach dem langen Balken wird ausgelassen, ab dem dritten Balken werden die Überprüfungen durchgeführt:

if(Cur.Whait!=0){
   Cur.Count++;            // Berechnung der Anzahl der Balken
   if(Cur.Count>=3){
      // Prüfungen

   }
}

Der wichtigste Wert der Prüfungen ist die Überlappung der Balken (Abb. 16).

Abb. 16. Die Überlappung von zwei Balken L wird als Differenz
zwischen dem minimalen Preis high und dem maximalen Preis low definiert.

Dieser Wert wird unter Verwendung zweier Balken als die Differenz zwischen dem kleinsten Hoch und dem größten Tief berechnet: 

Overlapping=MathMin(high[i],high[i-1])-MathMax(low[i],low[i-1]);

Die Überlappung mit dem ersten Balken wird nicht überprüft, deswegen beginnt die Prüfung mit dem dritten Balken und nicht mit dem zweitem.

Der Wert darf nicht größer als Schwellenwert sein. Wenn man diesen Wert in Punkten angibt, wird der Indikator stark vom Zeitrahmen abhängig sein, denn die Werte des Parameters werden sich auf verschiedenen Zeitrahmen stark voneinander unterscheiden. Um vom Zeitrahmen nicht abzuhängen, legen wir den Basis-Wert nach den zu prüfenden Balken fest — verwenden wir den größten Schatten von den zwei Balken:

double PreSize=MathMax(high[i-1]-low[i-1],high[i]-low[i]);

Überprüfen wir den Wert der Überlappung von Balken:

if(!(Overlapping>=PreSize*MinOverlapping))

Wenn sich zwei aufeinanderfolgende Balken nicht überlappen, heißt das, dass die Reihe aufeinanderfolgenden sich überlappenden Balken zu Ende ist. In diesem Fall überprüfen wir die Anzahl der Balken in der Reihe:

if(Cur.Count-2>=MinCount){
   // Prüfungen
}
Cur.Whait=0;

Wenn die Anzahl der Balken in der Reihe den Wert der Variablen MinCount überschreitet, werden zusätzliche Prüfungen durchgeführt, sonst wird das Warten auf die Bildung des Musters abgebrochen, indem die Variable CurCount auf Null gesetzt wird. Im oben angeführten Code wird 2 von der Variablen CurCount subtrahiert. Das sind der erste lange Balken und der abschließende Balken, auf welchem die Bedingungen der Überlappung nicht erfüllt sind.

Die Variablen MinOverlapping und MinCount stellen die externen Variablen des Indikators dar:

input double               MinOverlapping       =  0.4;
input int                  MinCount             =  5;

Nach der Erfüllung der Bedingungen der Anzahl der sich überlappenden Balken überprüfen wir die Bedingungen der Form und Steigung des Musters. Zuerst werden dafür die Parameter der festgestellten Reihe der sich überlappenden Balken definiert:  

double AverSize,AverBias,AverSizeDif;
PatternParameters(high,low,i-1,Cur.Count-2,AverSize,AverBias,AverSizeDif);

Die Parameter werden in der Funktion PatternParameters() definiert und nach Links in den Variablen AverSize, AverBias und AverSizeDif zurückgegeben. Die durchschnittliche Größe des Balkens wird in die Variable AverSize zurückgegeben, die durchschnittliche Verschiebung des Balkenzentrums - in die Variable AverBias, und die durchschnittliche Differenz der Größe von zwei benachbarten Balken - in AverSizeDif. Um zu verstehen, wie diese Parameter definiert werden, gehen wir auf die Funktion PatternParameters() ein:

void PatternParameters( const double & high[],
                        const double & low[],
                        int i,
                        int CurCnt,
                        double & AverSize,
                        double & AverBias,
                        double & AverSizeDif
){
            
   // durchschnittliche Größe des Balkens            
   AverSize=high[i-CurCnt]-low[i-CurCnt];
   // durchschnittliche Verschiebung des Balkens
   AverBias=0;
   // durchschnittliche Differenz zwischen der Größe zweier benachbarten Balken
   AverSizeDif=0;
   
   for(int k=i-CurCnt+1;k<i;k++){      // nach allen Balken der Reihe außer dem ersten
      // durchschnittliche Größe
      AverSize+=high[k]-low[k];
      // durchschnittliche Verschiebung
      double mc=(high[k]+low[k])/2;
      double mp=(high[k-1]+low[k-1])/2;
      AverBias+=(mc-mp);
      // durchschnittliche Differenz
      double sc=(high[k]-low[k]);
      double sp=(high[k-1]-low[k-1]);
      AverSizeDif+=(sc-sp);               
      
   }
   
   // Division der Summen durch die Anzahl
   AverSize/=CurCnt;
   AverBias/=(CurCnt-1);
   AverSizeDif/=(CurCnt-1); 
} 

Der Funktion werden zwei Arrays high und low, Index des Balkens, mit welchem die Reihe der sich überlappenden Balken endet, die Länge der Reihe und drei Variablen für Rückgabewerte übergeben. Die Berechnung der Werte erfolgt in der Schleife for, aber da die Werte AverBias und AverDiff anhand zweier benachbarten Balken berechnet werden, der erste Balken der Reihe wird ausgelassen:

for(int k=i-CurCnt+1;k<i;k++)

Deswegen werden die Variablen AverBias und AverDiff vor der Schleife auf Null gesetzt und der Variablen AverSize wird der Wert zugewiesen, der nach dem in der Schleife ausgelassenen Balken berechnet wurde.

In der Schleife werden der Variablen AverSize die Balkengrößen hinzugefügt:

AverSize+=high[k]-low[k];

Für den Indikator AverBias (Verschiebung) werden die Mittelpunkte der Balken und deren Differenz berechnet, die resultierende Differenz summiert sich:

double mc=(high[k]+low[k])/2;
double mp=(high[k-1]+low[k-1])/2;
AverBias+=(mc-mp);

Für den Wert AverSizeDif wird die Größe zweier benachbarten Balken und deren Differenz berechnet, die resultierende Differenz summiert sich:

double sc=(high[k]-low[k]);
double sp=(high[k-1]-low[k-1]);
AverSizeDif+=(sc-sp);    

Nach der Schleife werden alle Summen durch die Zahl der summierten Werte dividiert:

AverSize/=CurCnt;
AverBias/=(CurCnt-1);
AverSizeDif/=(CurCnt-1); 

Nach der Berechnung der Parameter wird die Form des Musters geprüft. Diese Prüfung hängt nicht von der Richtung der vermutlichen Kursbewegung ab. Für die Prüfung der Form werden drei Funktionen verwendet FormTapered() — fallend (Wimpel), FormHorizontal() — rechteckig(Flagge), FormExpanding() — steigend (Keil):

if(   FormTapered(AverSizeDif,AverSize) ||
      FormHorizontal(AverSizeDif,AverSize) ||
      FormExpanding(AverSizeDif,AverSize)
){ 
   // Prüfung der Richtung
}

Im Indikator iHorizontalFormation konnte man nur eine der drei Formen in den Einstellungen auswählen, hier werden alle drei Formen unabhängig aktiviert. Dies ist mit einer seltenen Erfüllung der Bedingungen und dementsprechend seltenen Handelssignalen verbunden. Für das Aktivieren/ Deaktivieren jeder der Formen wurden den Parametern des Indikators drei Variablen hinzugefügt. Darüber hinaus wurde ein Koeffizient für jede Form im Fenster der Eigenschaften hinzugefügt:

input bool                 FormTapered          =  true;
input double               FormTaperedK         =  0.05;
input bool                 FormRectangular      =  true;
input double               FormRectangularK     =  0.33;
input bool                 FormExpanding        =  true;
input double               FormExpandingK       =  0.05;

 Gehen wir auf die Funktionen der Prüfung der Form ein. Die Funktion FormTapered():

bool FormTapered(double AverDif, double AverSize){
   return(FormTapered && AverDif<-FormTaperedK*AverSize);
}

Wenn die durchschnittliche Differenz zwischen der Größe der Balken kleiner als der negative Schwellenwert ist, heißt das, dass die Balken kleiner werden, was einer spitzen Form des Musters entspricht:

Die Funktion FormHorizontal():

bool FormHorizontal(double AverDif, double AverSize){
   return(FormRectangular && MathAbs(AverDif)<FormRectangularK*AverSize);
}

Wenn der absolute Wert der durchschnittlichen Differenz zwischen der Größe der Balken kleiner als der Schwellenwert ist, bedeutet das, dass alle Balken ungefähr gleich groß sind, was einer rechteckigen Form entspricht:

Die Funktion FormExpanding():

bool FormExpanding(double AverDif, double AverSize){
   return(FormExpanding && AverDif>FormExpandingK*AverSize);
}

In dieser Funktion muss, im Gegensatz zum spitzen Muster, die durchschnittliche Differenz zwischen der Größe der Balken den positiven Schwellenwert übersteigen, was länger werdenden Balken und einer breiter werdenden Form entspricht.

Nachdem die Prüfung der Form abgeschlossen wurde, wird die Steigung des Musters überprüft. Diese Prüfung hängt von der vermutlichen Richtung der Preisbewegung ab. Für die Richtung nach oben wird die Funktion CheckInclineForBuy() verwendet, für die Richtung nach unten — CheckInclineForSell():

if(Cur.Whait==1){
   if(CheckInclineForBuy(AverBias/AverSize)){
      // zusätzliche Prüfungen für die Richtung nach oben

   }
}
else if(Cur.Whait==-1){
   if(CheckInclineForSell(AverBias/AverSize)){   
      // zusätzliche Prüfungen für die Richtung nach unten

   }
}

Die Varianten für die Prüfung der Steigung werden wie die Varianten für die Prüfung der Form separat aktiviert. Dafür sind entsprechende Variablen im Fenster der Eigenschaften vorhanden. Für jede Variante der Steigung gibt es im Fenster der Eigenschaften einen eigenen Koeffizienten:

input bool                 InclineAlong         =  true;
input double               InclineAlongK        =  0.1;
input bool                 InclineHorizontal    =  true;
input double               InclineHorizontalK   =  0.1;
input bool                 InclineAgainst       =  true;
input double               InclineAgainstK      =  0.1;

Die Funktion CheckInclineForBuy():

bool CheckInclineForBuy(double Val){
   return(  (InclineAlong && Val>InclineAlongK) || 
            (InclineHorizontal && MathAbs(Val)<InclineHorizontalK) || 
            (InclineAgainst && Val<-InclineAgainstK)
   );
}   

Der Funktion wird der Wert der relativen Verschiebung der Balken AverBias/AverSize übergeben. Wenn er über dem positiven Schwellenwert liegt, bedeutet das, dass die Formation eine Steigung nach oben aufweist, wenn unter dem negativen — Steigung nach unten. Wenn der Wert im Bereich des Schwellenwertes ohne Berücksichtigung des Vorzeichens ist, liegt das Muster horizontal:

bool CheckInclineForBuy(double Val){
   return(  (InclineAlong && Val>InclineAlongK) || 
            (InclineHorizontal && MathAbs(Val)<InclineHorizontalK) || 
            (InclineAgainst && Val<-InclineAgainstK)
   );
}   

Gleich für die Richtung nach unten:

bool CheckInclineForSell(double Val){
   return(  (InclineAlong && Val<-InclineAlongK) || 
            (InclineHorizontal && MathAbs(Val)<InclineHorizontalK) || 
            (InclineAgainst && Val>InclineAgainstK)
   );
}  

Nun entspricht aber die Steigung nach unten der Bewegungsrichtung, und die Steigung nach oben — der Bewegung gegen die Richtung.

Es bleibt nur übrig, die Richtung des abschließenden Balkens zu prüfen. Es kann zwei Varianten der letzten Prüfung geben: entweder ist der letzte Balken in die Richtung des Musters oder gegen diese Richtung gerichtet. Für die Aktivierung der Varianten der abschließenden Prüfungen werden die folgenden Parameter in das Fenster der Eigenschaften hinzugefügt:

input bool                 EnterAlong           =  true;
input bool                 EnterAgainst         =  true;

Die Richtung nach oben wird wie folgt geprüft:

if((EnterAlong && close[i]>open[i]) || (EnterAgainst && close[i]<open[i])){
   Label1Buffer[i]=low[i];
   Label3Buffer[i]=close[i]+(high[Cur.Bar]-low[Cur.Bar]);
}

Wenn die Variante EnterAlong ausgewählt wurde, und der Balken nach oben gerichtet ist, oder wenn die Variante EnterAgainst ausgewählt wurde und der Balken nach unten gerichtet ist, zeichnet der Indikator einen Pfeil und einen Zielpunkt. Das Ziel liegt in einem Abstand, der gleich der Größe des ersten großen Balkens ist.

Gleich für die Richtung nach unten:

if((EnterAlong && close[i]<open[i]) || (EnterAgainst && close[i]>open[i])){
   Label2Buffer[i]=high[i];                  
   Label4Buffer[i]=close[i]-(high[Cur.Bar]-low[Cur.Bar]);
}

Damit ist die Erstellung des Indikators abgeschlossen. Der Indikator mit der Alert-Funktion ist im Anhang zum Artikel zu finden, Dateiname — iFlag. 


Tester-Indikator

Die einfachste Weise, die Effizient eines Indikators zu testen, ist die Verwendung eines Expert Advisors und des Strategietesters im Terminal. Im Artikel "Wolfe Wellen" wurde ein einfacher Expert Advisor erstellt, der nach einer kleinen Modifizierung auch für das Testen der Indikatoren aus dem vorliegenden Artikel verwendet werden kann. Die Indexierung der Puffer der Indikatoren iHorizontalFormation und iFlag entspricht der Indexierung der Puffer des Indikators iWolfeWaves, deswegen kann man nur die externen Parameter des Expert Advisors und den Aufruf der Funktion iCustom() ändern.

Es gibt auch eine weitere Testmethode, dank der man die Effizienz von Indikatoren ganz schnell einschätzen kann. Es geht um den Tester-Indikator. In einem zusätzlichen Indikator wird der Handel nach den Pfeilen des Hauptindikators modelliert, die Equity- und Kontostand-Linien werden auf dem Chart angezeigt.

Die einfachste und offensichtliche Methode bei der Erstellung des Tester-Indikators ist die Verwendung der Funktion iCustom(), aber diese Vorgehensweise hat wesentliche Nachteile: der Hauptindikator, der Pfeile zeichnet, wird auf dem Preischart angezeigt und der Tester-Indikator, der die Equity- bzw. Kontostand-Linie zeichnet, muss in einem Unterfenster angezeigt werden. Es sind zwei Indikatoren hinzuzufügen; für beide sind die gleichen Parameter zu setzen. Später kann man je nach Wunsch die Parameter ändern, sie müssen allerdings bei beiden Indikatoren geändert werden, und dies ist nicht immer praktisch.

Eine weitere Variante wäre so zu machen, dass der Tester-Indikator Pfeile durch grafische Objekte auf dem Chart zeichnen.

Die dritte Variante wäre die Funktion ChartIndicatorAdd() zu verwenden. Diese Funktion erlaubt es, dem Chart einen anderen Indikator durch das Programm hinzuzufügen. In diesem Fall muss man bei jeder Änderung der Parameter des Hauptindikators den zusätzlichen Tester-Indikator auf dem Chart suchen, ihn löschen und dann ihn mit neuen Parametern hinzufügen. Das ist eine passende und sogar praktische Variante.

Es gibt auch eine vierte Variante, die genauso praktisch wie die dritte, aber einfacher hinsichtlich der Implementierung ist. Darüber hinaus kann man einen universellen Tester-Indikator erstellen, der sowohl mit dem Indikator iHorizontalFormation, als auch mit dem Indikator iFlag nach deren kleiner Nacharbeitung verwendet werden kann.

Die Nacharbeitung von iHorizontalFormation und iFlag besteht in der Erstellung einer externen Variablen ID: 

input int                  ID             =  1;

Danach wird dem Indikator OnInit unter Verwendung des Wertes dieser Variablen ein kurzer Name gegeben:

string ShortName=MQLInfoString(MQL_PROGRAM_NAME)+"-"+IntegerToString(ID);
IndicatorSetString(INDICATOR_SHORTNAME,ShortName);

Der kurze Name stellt den Dateinamen des Indikators dar, zu welchem das Vorzeichen "-" und der Wert der ID-Variablen hinzugefügt werden. Nach diesem Namen wird der Tester-Indikator den Hauptindikator finden (seinen Handle erhalten).

Der Indikator iHorizontalFormation basiert auf dem ZigZag Indikator, der anhand der high-low Kursen, Schlusskursen und anderen Indikatoren berechnet werden kann. Der Pfeil, der bei der Berechnung nach high-low erscheint, verschwindet nicht mehr vom Chart. Das heißt, wenn man den Indikator für den Handel verwendet, kann man ihn auf dem sich bildenden Balken verfolgen. In den anderen Fällen muss man bei der Berechnung des ZigZags nach close und anderen Indikatoren das Auftreten eines Pfeils auf einem vollständigen Balken verfolgen. Das heißt, der Tester-Indikator muss auf irgendeine Weise benachrichtigt werden, auf welchem Balken man das Erscheinen von Pfeilen verfolgen muss.

Die Indikatoren iHorizontalFormation und iFlag zeichnen Zielpunkte, die man beim Setzen von Take-Profit verwenden kann. Für den Indikator iHorizontalFormationist ist das aber nur in dem Fall möglich, wenn der ZigZag nach Preis berechnet wird. Man muss angeben, ob der Tester-Indikator Zielpunkte oder zusätzliche Parameter von Stop-Loss und Take-Profit verwenden muss. Die erste Idee ist, eine globale Variable für die Übertragung von Daten zu verwenden. Das MetaTrader 5 Terminal hat aber eine Besonderheit: bei der Änderung von Parametern des Indikators wird eine neue Instanz des Indikators mit einem neuen Handle geladen, und die vorherige Instanz wird nicht direkt aus dem Speicher gelöscht. Deswegen wenn man die externen Parameter des Indikators zurücksetzt, wird der Indikator nicht geladen und nicht berechnet, das heißt die Funktion OnInit() wird nicht ausgeführt und die Variable prev_calculated wird nicht auf Null gesetzt. Auf diese Weise erhalten die globalen Variablen den neuen Wert nicht. 

Die notwendigen Parameter werden über einen Puffer des Indikators übergeben. Dafür reicht ein Element des Puffers. Verwenden wir den vorhandenen — den ersten Puffer und sein erstes (ganz links) Element. Insgesamt müssen zwei Werte übergeben werden. Der eine bestimmt, ob der Hauptindikator auf dem vollständigen Balken verfolgt werden muss, oder ob man den sich bildenden Balken verwenden kann, und der zweite — ob man den Ziel-Punkt für den Take-Profit Level einer Position verwenden kann. Wenn prev_calculate=0, wird der Funktion OnCalculate() der folgende Code hinzugefügt:

int ForTester=0;       // Variable für den Wert
if(!(SrcSelect==Src_HighLow)){
   // Arbeit nach dem vollständigen Balken
   ForTester+=10;
}   
if(!(SrcSelect==Src_HighLow || SrcSelect==Src_Close)){
   // man kann den Ziel-Punkt verwenden
   ForTester+=1;
}     
UpArrowBuffer[0]=ForTester;  

Über ein Element des Arrays muss man zwei Zahlen kleiner als 10 übergeben, deswegen wird die eine mit 10 multipliziert, und die zweite Zahl wird dazu addiert. Da die Werte entweder gleich 0 oder 1 sein können, könnte man eine Zahl mit der Basis 2 verwenden, aber da das Volumen der Daten nicht so groß, muss man hier nicht unbedingt an Bytes sparen.

Auf die gleiche Weise muss auch der Indikator iFlag nachgearbeitet werden. In der Funktion OnInit():

string ShortName=MQLInfoString(MQL_PROGRAM_NAME)+"-"+IntegerToString(ID);
IndicatorSetString(INDICATOR_SHORTNAME,ShortName);

In der OnCalculate() Funktion:

Label1Buffer[0]=11;

Man sollte den Indikator iFlag immer auf einem vollständigen Balken verfolgen, und man kann immer einen Ziel-Punkt verwenden. Deswegen wird der Wert 11 ohne Berechnungen zugewiesen.

Bei der Verwendung eines vollständigen Balkens ist es offensichtlich, dass die Position bei der Eröffnung eines neuen Balkens eröffnet wird, aber beim Einstieg auf einem sich bildenden Balken ist der Eröffnungspreis nicht bekannt. Deswegen muss der Indikator iHorizontalFormation noch einmal nachgearbeitet werden: es wurde ein weiterer Puffer hinzugefügt, der Punkte auf den Balken mit Pfeilen zeichnet. Mit diesem Puffer wird der Eröffnungslevel für eine Position festgelegt.

Nun gehen wir auf den Tester-Indikator ein. Erstellen wir einen neuen Indikator mit dem Namen iTester, fügen wir externe Parameter hinzu:

input int                  ID             =  1;
input double               StopLoss_K     =  1;
input bool                 FixedSLTP      =  false;
input int                  StopLoss       =  50;
input int                  TakeProfit     =  50;

Wobei:

  • ID — Identifikator des Hauptindikators
  • StopLoss_K — Berechnungskoeffizient von Stop-Loss hinsichtlich Take-Profit bei der Verwendung des Zielpunktes
  • FixedSLTP — die Variablen StopLoss und TakeProfit oder den Punkt-Ziel und die Variable StopLoss_K verwenden.

Es wird sehr praktisch sein, wenn der Tester-Indikator nicht nur seinen Namen in der rechten oberen Ecke anzeigen wird, sondern auch den Namen des Indikators, nach dessen Pfeilen es funktioniert. Aber bis der zu testende Indikator noch nicht dem Chart hingefügt ist, wird der Name des Tester-Indikators angezeigt. Deklarieren wir dafür die globale Variable:

string IndName;

Weisen wir ihr den Namen des Tester-Indikators in der OnInit() Funktion zu:

IndName=MQLInfoString(MQL_PROGRAM_NAME);

Die Information über jede vom Tester-Indikator eröffnete Position wird im Array der Struktur SPos gespeichert, die Variable PosCnt wird für die Abrechnung der Anzahl offener Positionen verwendet:

struct SPos{
   int dir;
   double price;
   double sl;
   double tp;
   datetime time; 
};
SPos Pos[];
int PosCnt;

Der Abschluss wird dem Array nur einmal hinzugefügt, dafür wird die folgende Variable verwendet:

datetime LastPosTime;

Beim Hinzufügen dem Array einer Position werden die Zeit aus der Variablen LastPosTime und die Zeit des Balkens, auf welchem die Position eröffnet wird, überprüft. Wenn die Position hinzugefügt ist, wird der Variablen LastPosTime die neue Zeit zugewiesen. Wenn die Zeit des Balkens gleich der Zeit LastPosTime ist, heißt das, dass die Position bereits eröffnet wurde.

Für das Schließen der Positionen, genauer gesagt, für die Berechnung des Profits, werden zwei weitere Variablen benötigt:

int Closed;
datetime CloseTime;

Der Variablen Closed wird der Profit von den auf einem Balken geschlossenen Positionen zugewiesen, und der Variablen CloseTime — die Zeit dieses Balkens. Unten wird darauf ausführlicher eingegangen.

Wir haben alle Hilfsvariablen und die Funktion OnInit() betrachtet, kommen wir nun zur Funktion OnCalculate(). Zuerst werden einige Hilfsvariablen deklariert:

string name;
static int last_handle=-1;
static int shift=0;
static int use_target=0;
int handle=-1;     
int start=2;  

Beschreiben wir diese Variablen:

  • name wird für das Erhalten des Namens des Indikators über die Funktion ChartIndicatorName() verwendet;
  • statische Variable last_handle — für das Speichern des zu testenden Indikators;
  • die statischen shift und use_target — für die Parameter, die aus dem zu testenden Indikator übergeben wurden;
  • handle — für das Erhalten des Handles über die Funktion ChartIndicatorGet();
  • start — für den Anfang der Berechnung des Indikators.

Beschäftigen wir uns mit dem Code für die Suche des zu testenden Indikators. Bestimmen wir zuerst die Anzahl der Indikatoren, die an den Preischart angehängt sind:

int it=ChartIndicatorsTotal(0,0);

Durchlaufen wir sie in der Schleife:

for(int i=0;i<it;i++){        // nach allen Indikatoren des Charts
   // Erhalten des Namens des weiteren Indikators
   name=ChartIndicatorName(0,0,i);
   // Suche des Substrings  "-"
   int p=StringFindRev(name,"-");
   if(p!=-1){
      // Substring gefunden, der Wert des Identifikators wird überprüft
      if(StringSubstr(name,p+1,StringLen(name)-p-1)==IntegerToString(ID)){
         // der Identifikator stimmt überein, den Handle erhalten
         handle=ChartIndicatorGet(0,0,name);
      }
   }
} 

Gehen wir auf den oben angeführten Code-Teil ausführlicher ein. Der Variablen name wird der Name des Indikators zugewiesen, der mithilfe der Funktion ChartIndicatorName() erhalten wurde. Diese Funktion gibt den Namen des Indikators nach seinem Index zurück. Es wird geprüft, ob der erhaltene Name dem Identifikator entspricht. Dafür wird nach dem letzten "-" des Substrings gesucht. Wenn das Zeichen "-" gefunden wurde, wird der Teil des Strings nach ihm exportiert. Wenn er dem Identifikator entspricht, wird der Variablen handle der Wert des Handles zugewiesen, der mithilfe der Funktion ChartIndicatorGet() erhalten wurde. Diese Funktion gibt den Handle nach dem Namen des Indikators zurück. 

Nachdem wir den Handle erhalten haben, vergleichen wir ihn mit dem bereits bekannten Handle aus der Variablen last_handle (statische Variable, die ihren Wert nach dem Ende der Funktion OnCalculate() speichert):

if(handle!=last_handle){
   if(handle==-1){                    // kein Handle
      // den ursprünglichen Namen setzen
      IndicatorSetString(INDICATOR_SHORTNAME,IndName);
      ChartRedraw(0);
      return(0);
   }
   // Prüfung, ob die Berechnung des zu testenden Indikators abgeschlossen ist
   int bc=BarsCalculated(handle);
   if(bc<=0)return(0);                // wenn nicht abgeschlossen, wird die Funktion abgebrochen
   // Kopieren von Daten mit den Testparametern
   double sh[1];
   if(CopyBuffer(handle,0,rates_total-1,1,sh)==-1){
      // wenn das Kopieren fehlgeschlagen ist, wird die Funktion bis zum
      // nächsten Tick abgebrochen
      return(0);
   }
   // einzelne Parameter importieren
   shift=((int)sh[0])/10;              // vollständiger oder sich bildender Balken
   use_target=((int)sh[0])%10;         // ob der Zielpunkt verwendet werden darf
   last_handle=handle;                 // den Handle speichern
   // den Namen des Indikators setzen
   IndicatorSetString(INDICATOR_SHORTNAME,name);
   ChartRedraw(0);
}
else if(prev_calculated!=0){
   // wenn kein neuer Handle vorhanden ist, werden nur neue Balken 
   // und der sich bildende Balken berechnet
   start=prev_calculated-1;
}

Wenn der Wert der Variablen handle nicht gleich dem Wert der Variablen last_handle ist, heißt das, dass es Änderungen im zu testenden Indikator gab. Wahrscheinlich wurde er gerade eben dem Chart hinzugefügt, oder seine Parameter wurden geändert. Oder er wurde gar vom Chart gelöscht. Wenn der Indikator vom Chart gelöscht wurde, wird die handle Variablen gleich -1 sein, dabei wird dem Tester-Indikator der standardmäßige Name zugewiesen, und die Funktion OnCalculate() wird beendet. Wenn die Variable hadle den aktuellen Wert des Handles hat, werden die Testparameter erhalten. Dazu zählen der sich bildende/vollständige Balken und die Erlaubnis für die Verwendung des Zielpunktes. Bei der weiteren Ausführung der Funktion OnCalculate() sind handle und last_handle gleich, dabei wird die Variable start berechnet — der erste Balken, mit dem die Berechnung des Indikators beginnt.

Die Variable start ist standardmäßig auf 2 gesetzt. Wenn der Indikator komplett neu gerechnet werden muss (und dies ist notwendig bei der Änderung des Handles oder wenn prev_calculated gleich Null ist), müssen in diesem Fall einige zusätzliche Variablen zurückgesetzt werden:

if(start==2){
   PosCnt=0;
   BalanceBuffer[1]=0;
   EquityBuffer[1]=0;
   LastPosTime=0;
   Closed=0;
   CloseTime=0;      
}

Es werden auf Null gesetzt: die Anzahl der offenen Positionen — PosCnt, die ersten Elemente der Puffer für Kontostand und Equity — BalanceBuffer[1] und EquityBuffer[1], die Zeit der letzten Position — LastPosTime, Profit der auf einem Balken geschlossenen Positionen — Closed sowie Zeit des Schlussbalkens — ClosedTime.

Nun gehen wir auf die Hauptschleife des Indikators ein. Zuerst wird ihr kompletter Code angeführt, danach wird er zeilenweise betrachtet:

for(int i=start;i<rates_total;i++){

   // Verschiebung der früher bekannten Werte des Kontostandes und der Equity
   BalanceBuffer[i]=BalanceBuffer[i-1];
   EquityBuffer[i]=EquityBuffer[i-1];

   if(CloseTime!=time[i]){ 
      // Anfang der Berechnung eines neuen Balkens
      Closed=0; // die Variable des Profits auf Null setzen
      CloseTime=time[i];          
   }

   // Erhalten der Daten des zu testenden Indikators
   double buy[1],sell[1],buy_target[1],sell_target[1],enter[1];
   int ind=rates_total-i-1+shift;
   if(CopyBuffer(last_handle,0,ind,1,buy)==-1 || 
      CopyBuffer(last_handle,1,ind,1,sell)==-1 ||
      CopyBuffer(last_handle,2,ind,1,buy_target)==-1 || 
      CopyBuffer(last_handle,3,ind,1,sell_target)==-1        
   ){
      return(0);
   }
  
   if(shift==0){
      // beim Testen anhand der sich bildenden Balkens erhalten wir den 
      // Eröffnungskurs aus dem zusätzlichen Puffer des zu testenden Indikator       
      if(CopyBuffer(last_handle,4,ind,1,enter)==-1){
         return(0);
      } 
   }
   else{
      // beim Testen anhand eines vollständigen Balkens wird 
      // der Eröffnungskurs des Balkens verwendet
      enter[0]=open[i];
   }

   // Kaufpfeil
   if(buy[0]!=EMPTY_VALUE){
      AddPos(1,enter[0],buy_target[0],spread[i],time[i],use_target);      
   }
   // Verkaufspfeil
   if(sell[0]!=EMPTY_VALUE){
      AddPos(-1,enter[0],sell_target[0],spread[i],time[i],use_target);       
   }

   // Prüfung, ob die Positionen geschlossen werden müssen
   CheckClose(i,high,low,close,spread);

   // Kontostand-Linie
   BalanceBuffer[i]+=Closed;
   
   // Equity-Linie
   EquityBuffer[i]=BalanceBuffer[i]+SolveEquity(i,close,spread);

}

Der Kontostand setzt sich aus dem früher bekannten Wert des Kontostandes und des Profits der geschlossenen Positionen, deswegen wird der früher bekannte Wert des Kontostandes vom vorherigen Element des Puffers übertragen:

// Verschiebung der früher bekannten Werte des Kontostandes und der Equity
BalanceBuffer[i]=BalanceBuffer[i-1];

Bei der ersten Berechnung jedes Balkens wird die Variable des Profits von den auf diesem Balken geschlossenen Positionen auf Null gesetzt:

if(CloseTime!=time[i]){ 
   Closed=0;
   CloseTime=time[i];          
}

Die Daten des zu testenden Indikators werden kopiert:

// Erhalten der Daten des zu testenden Indikators
double buy[1],sell[1],buy_target[1],sell_target[1],enter[1];
int ind=rates_total-i-1+shift;
if(CopyBuffer(last_handle,0,ind,1,buy)==-1 || 
   CopyBuffer(last_handle,1,ind,1,sell)==-1 ||
   CopyBuffer(last_handle,2,ind,1,buy_target)==-1 || 
   CopyBuffer(last_handle,3,ind,1,sell_target)==-1        
){
   return(0);
}

In den Arrays buy und sell werden die Daten der Puffer mit Pfeilen kopiert, und in die Arrays buy_target und sell_target — die Daten der Zielpunkte. Vor dem Kopieren wird der Index des Balkens ind unter Berücksichtigung der Variablen shift berechnet.

Je nach Wert von shift wird entweder der zusätzliche Puffer kopiert oder der Eröffnungskurs des Balkens verwendet:

if(shift==0){
   // beim Testen anhand der sich bildenden Balkens erhalten wir den 
   // Eröffnungskurs aus dem zusätzlichen Puffer des zu testenden Indikator       
   if(CopyBuffer(last_handle,4,ind,1,enter)==-1){
      return(0);
   } 
}
else{
   // beim Testen anhand eines vollständigen Balkens wird 
   // der Eröffnungskurs des Balkens verwendet
   enter[0]=open[i];
}

Wenn auf dem zu berechnenden Balken Pfeile festgelegt wurden, wird eine Position durch den Aufruf der Funktion AddPos() eröffnet:

// Kaufpfeil
if(buy[0]!=EMPTY_VALUE){
   AddPos(1,enter[0],buy_target[0],spread[i],time[i],use_target);      
}
// Verkaufspfeil
if(sell[0]!=EMPTY_VALUE){
   AddPos(-1,enter[0],sell_target[0],spread[i],time[i],use_target);       
}

In der Funktion CheckClose() wird überprüft, ob die Funktion geschlossen werden muss. Wenn die Funktion geschlossen wird, wird der erhaltene Profit in der Variablen Closed sein:

// Prüfung, ob die Positionen geschlossen werden müssen
CheckClose(i,high,low,close,spread);

Der Profit aus der Variablen Closed wird zum Kontostand addiert:

// Kontostand-Linie
BalanceBuffer[i]+=Closed;

Das Eigenkapital setzt sich aus dem Kontostand und dem nicht geschlossenen Profits zusammen, der durch die Funktion SolveEquity() berechnet wird:

EquityBuffer[i]=BalanceBuffer[i]+SolveEquity(i,close,spread);

Betrachten wir die Funktionen AddPos(), CheckClose() und SolveEquity(). Unten ist der Code jeder Funktion mit ausführlichen Kommentaren angeführt.  

Die Funktion AddPos():

void AddPos(int dir, double price,double target,int spread,datetime time,bool use_target){

   if(time<=LastPosTime){
      // Position mit der Zeit time bereits hinzugefügt
      return;
   }
   
   // kein Platz im Array
   if(PosCnt>=ArraySize(Pos)){
      // das Array wird um 32 Elemente vergrößert
      ArrayResize(Pos,ArraySize(Pos)+32);
   }
   
   // die Richtung der Position wird gespeichert
   Pos[PosCnt].dir=dir;
   // die Zeit der Eröffnung der Position
   Pos[PosCnt].time=time;
   // Eröffnungskurs der Position
   if(dir==1){
      // Ask-Preis für den Kauf
      Pos[PosCnt].price=price+Point()*spread;  
   }
   else{
      // Bid-Kurs für den Verkauf
      Pos[PosCnt].price=price;  
   }

   // Stoploss und Takeprofit werden berechnet
   if(use_target && !FixedSLTP){ 
      // Zielpunkt verwenden und Stop Loss berechnen
      if(dir==1){
         Pos[PosCnt].tp=target;
         Pos[PosCnt].sl=NormalizeDouble(Pos[PosCnt].price-StopLoss_K*(Pos[PosCnt].tp-Pos[PosCnt].price),Digits());
      }
      else{
         Pos[PosCnt].tp=target+Point()*spread;
         Pos[PosCnt].sl=NormalizeDouble(Pos[PosCnt].price+StopLoss_K*(Pos[PosCnt].price-Pos[PosCnt].tp),Digits());
      }   
   }
   else{
      // es werden die Variablen StopLoss und TakeProfit verwendet
      if(dir==1){
         Pos[PosCnt].tp=Pos[PosCnt].price+Point()*TakeProfit;
         Pos[PosCnt].sl=Pos[PosCnt].price-Point()*StopLoss;
      }
      else{
         Pos[PosCnt].tp=Pos[PosCnt].price-Point()*TakeProfit;
         Pos[PosCnt].sl=Pos[PosCnt].price+Point()*StopLoss;
      }     
   }
   
   PosCnt++;
   
}  

Die CheckClose() Funktion:

void CheckClose(int i,const double & high[],const double & low[],const double & close[],const int & spread[]){
   for(int j=PosCnt-1;j>=0;j--){                                       // nach allen Positionen
      bool closed=false;                                               // der Wert false bedeutet, dass die Position noch offen ist  
      if(Pos[j].dir==1){                                               // Kauf
         if(low[i]<=Pos[j].sl){                                        // der Preis ist kleiner oder gleich dem Stop Loss Level
            // Profit in Punkten
            Closed+=(int)((Pos[j].sl-Pos[j].price)/Point());
            closed=true;                                               // Position mit dem Index j geschlossen
         }
         else if(high[i]>=Pos[j].tp){                                  // Take Profit erreicht
            // Profit in Punkten
            Closed+=(int)((Pos[j].tp-Pos[j].price)/Point());    
            closed=true;                                               // Position mit dem Index j geschlossen        
         }
      }
      else{ // Verkauf
         if(high[i]+Point()*spread[i]>=Pos[j].sl){                     // Stop-Loss erreicht
            // Profit in Punkten
            Closed+=(int)((Pos[j].price-Pos[j].sl)/Point());
            closed=true;                                               // Position mit dem Index j geschlossen
         }
         else if(low[i]+Point()*spread[i]<=Pos[j].tp){                 // der Preis ist kleiner oder gleich dem Take Profit
            // Profit in Punkten
            Closed+=(int)((Pos[j].price-Pos[j].tp)/Point());              
            closed=true;                                               // Position mit dem Index j geschlossen
         }         
      }
      // Position geschlossen, sie muss aus dem Array gelöscht werden
      if(closed){ 
         int ccnt=PosCnt-j-1;
         if(ccnt>0){
            ArrayCopy(Pos,Pos,j,j+1,ccnt);
         }
         PosCnt--;
      }
   }
}

In der Funktion CheckClose() werden alle im Array Pos gespeicherten Positionen durchlaufen, Pos, und die Werte ihrer Stop Loss und Take Profits werden mit den aktuellen Preisen high und low verglichen. Wenn die Position geschlossen wird, wird ihr Profit in Punkten zur Variablen Closed hinzugefügt, danach wird die Position aus dem Array gelöscht.

Die SolveEquity() Funktion:

int SolveEquity(int i,const double & close[],const int & spread[]){
   int rv=0;                                // Variable für das Ergebnis
   for(int j=PosCnt-1;j>=0;j--){            // nach allen Positionen
      if(Pos[j].dir==1){                    // Kauf
                                            // Profit
         rv+=(int)((close[i]-Pos[j].price)/Point());
      }
      else{                                // Verkauf
         // Profit
         rv+=(int)((Pos[j].price+Point()*spread[i]-close[i])/Point());         
      }
   }
   return(rv);
}  

In der Funktion SolveEquity() werden alle offenen Positionen aus dem Array Pos durchlaufen, unter Berücksichtigung des aktuellen Close wird der Profit in Punkten berechnet.

dWir haben den Indikator iTester betrachtet. Der fertige Indikator ist im Anhang zum Artikel unter dem Namen iTester zu finden.  Auf dem Abb. 17 ist eine Grafik mit den Indikatoren iHorizontalFormation (Pfeile) und iTester im Unterfenster dargestellt. Die grüne Linie — Equity, die rote Linie — Kontostand.


Abb. 17. Der Indikator iTester (im Unterfenster) basierend auf dem Indikator iHorizontalFormation (Pfeile auf der Grafik)


Fazit

Die im Artikel betrachteten Methoden können die aufgestellte Aufgabe lösen: auf dem Chart sind ganz deutlich Flaggen, Wimpel, Dreiecke und Keile zu erkennen. Die betrachteten Methoden gelten nicht als einzig möglich und absolut richtig. Man kann auch andere Methoden zur Erkennung der gleichen Muster entwickeln. Man kann, zum Beispiel, die lineare Regression verwenden: einzelne Berechnungen nach High und Low Preisen durchführen, danach die Steigung und Konvergenz/Divergenz dieser Linien prüfen. Noch mehr Ideen können bei der Lösung einzelner Aufgaben entstehen, die zur Gesamtaufgabe der Erkennung von Mustern gehören. Die Methoden zur Preisanalyse können hilfreich bei der Lösung weiterer Aufgaben der technischen Analyse sein. 

Anhang

Alle Indikatoren, die im Artikel erstellt wurden, finden Sie im Anhang:

  • iHorizontalFormation
  • iFlag
  • iTester