Das Handelssystem 'Turtle Soup' und seine Modifikation 'Turtle Soup Plus One'

Alexander Puzanov | 7 Dezember, 2016


  1. Einleitung
  2. Das Handelssystem 'Turtle Soup' und seine Modifikation 'Turtle Soup Plus One'
  3. Definition von Parametern des Kanals
  4. Funktion der Signalgenerierung
  5. Ein Basis-Expert Advisor für das Testen der Handelsstrategie
  6. Testen der Strategie anhand historischer Daten
  7. Fazit

Einleitung

Die Autoren des Buches Street Smarts: High Probability Short-Term Trading Strategies, Laurence Connors und Linda Raschke sind zwei erfolgreiche Trader mit einer reichen Erfahrung von 34 Jahren. Ihre Erfahrung umfasst Börsenahandel, Arbeit in Banken und Hadgefonds, Brokerhäusern und in Beratungsunternehmen. Ihrer Meinung nach ist für einen stabilen profitablen Handel eine einzige Handelsstrategie ausreichend. Nichtsdestotrotz sind ungefähr zwei Dutzend Handelsstrategien, aufgeteilt in vier Gruppen, im Buch vorgestellt. Jede Gruppe gehört zu einer bestimmten Phase der Marktzyklen und benutzt eines der stabilen Muster des Preisverhaltens.

Die im Buch beschriebenen Strategien sind relativ populär, man sollte aber beachten, dass die Autoren diese Strategien anhand eines 15...20 Jahre alten Marktverhaltens entwickelt haben. Deshalb hat der Artikel zwei Ziele: wir fangen mit der Umsetzung der ersten im Buch von L. Raschke und L. Connors beschriebenen Strategie an und versuchen dann ihre Leistungsfähigkeit mit dem MT5 Strategietester auszuwerten. Dabei verwenden wir die über den Demo-Server zugänglichen historischen Daten der letzten Jahre.

Beim Schreiben des Codes richte ich mich nach Nutzern mit MQL5-Basiskenntnissen mit anderen Worten nach fortgeschrittenen Anfängern. Aus diesem Grund werde ich nicht erläutern, wie Standardfunktionen arbeiten, wie Typen der Variablen ausgewählt werden und all das erklären, was man noch vor dem Programmieren von Handelsrobotern üben sollte. Von der anderen Seite orientiere ich mich auch nicht auf erfahrene Programmierer: in der Regel haben sie bereits Bibliotheken mit eigenen Lösungen und werden diese bei der Umsetzung einer neuen Handelsstrategie verwenden.

Für die meisten Programmierer, für welche dieser Artikel interessant sein wird, ist das Aneignen der objektorientierten Programmierung wichtig. Deshalb versuche ich diesen Expert Advisor auch für die Lösung dieser Aufgabe nützlich zu machen. Um den Übergang von der prozeduralen zur objektorientierten Programmierung einfacher zu gestalten, werden wir Klassen - das Komlizierteste in der objektorientierten Programmierung nicht verwenden. Stattdessen benutzen wir ihre einfachere Entsprechung - Strukturen. Strukturen vereinen logisch verbundene Daten von verschiedenen Typen und Funktionen für Operationen mit ihnen und haben fast alle Merkmale von Klassen, inklusive Vererbung. Aber um diese zu verwenden, ist es nicht notwendig, die Regeln der Codegestaltung zu beherrschen.


Das Handelssystem 'Turtle Soup' und seine Modifikation 'Turtle Soup Plus One'


Die Handelsstrategie "Turtle Soup" (Schildkrötensuppe) eröffnet eine Reihe von Strategien mit einem kurzen Namen "Tests". Damit man besser verstehen könnte, nach welchem Merkmal diese Reihe zusammengestellt wurde, sollte sie "Testen der Bereichsgrenzen oder der Unterstützungs-/Wiederstandsebenen anhand des Preises" betitelt werden. Turtle Soup basiert auf der Annahme, dass der Preis einen 20-tägigen Bereich nicht ohne Bounce (Abpraller) durchbrechen kann. Wir werden versuchen, Profite aus einem vorübergehenden Rollback von der Grenze oder aus einem falschen Ausbruch zu erzielen. Die Handelsstrategie wird immer in den Bereich gerichtet sein, deswegen können wir diese in die Kategorie Rollback-Strategien einstufen.

Der Name "Turtle Soup" ähnelt sich dem Namen der bekannten Strategie "Turtles" nicht durch Zufall. Beide verfolgen das Preisverhalten an der Grenze eines 20-tägigen Bereichs. Laut den Autoren des Buches haben sie eine Weile versucht, Ausbruchsstrategien zu nutzen, einschließlich "Turtles", aber die große Anzahl fehlerhaften Ausbrüche und Rollbacks machte einen solchen Handel uneffektiv. Die erkannten Muster waren allerdings sehr hilfreich bei der Erstellung von Regeln, um Profite aus der Preisbewegung in eine gegensätzliche Richtung hinsichtlich des Ausbruchs zu erzielen.

Die Regeln der Handelsstrategie "Turtle Soup" für den Einstieg in einen Buy-Trade lassen sich wie folgt formulieren:

  1. Überprüfen Sie, dass seit dem letzten 20-tägigen Tief mindestens 3 Handelstage vergangen sind
  2. Warten Sie, bis der Symbolpreis unter den 20-tägigen Tief sinkt
  3. Setzen Sie eine Buy Pending Order 5-10 Punkte über dem gerade nach unten durchbrochenen Preistief
  4. Direkt nach der Auslösung der Pending Order setzen Sie den StopLoss Level einen Punkt unterhalb des Tagestiefs
  5. Verwenden Sie Trailing Stop, sobald die Position profitabel wird
  6. Wenn die Position am ersten oder am zweiten Tag mit einem Stop Loss geschlossen wurde, ist ein wiederholter Einstieg an dem ursprünglichen Level erlaubt 

Die Regeln für den Einstieg in einen Sell-Trade sind gleich und müssen, wie Sie schon verstanden haben, an die obere Grenze des Bereichs - den 20-tägigen Hoch angewandt werden.

In der Quellcodebibliothek ist ein Indikator vorhanden, welcher bei bestimmten Einstellungen Grenzen des Kanals auf jedem Balken der Historie anzeigt. Er kann für die Visualisierung im manuellen Handel verwendet werden.

 

Die Beschreibung der Handelsstrategie beinhaltet keine direkte Antwort auf die Frage, wie lange die Pending Order gehalten werden muss, so lassen wir uns von einer einfachen Logik leiten. Und zwar: beim Testen der Grenze des Bereichs erstellt der Preis ein neues Extremum, und die erste der oben beschriebenen Bedingungen wird am nächsten Tag unerfüllbar sein. Da es an diesem Tag kein Signal gibt, müssen wir die Pending Order des vorherigen Tages löschen.

Eine Modifikation dieser Handelsstrategie, genannt 'Turtle Soup Plus One', weist zwei Unterschiede auf:

  1. Statt eine Pending Order direkt nach dem Ausbruch des 20-tägigen Bereichs zu platzieren, muss man auf eine Signalbestätigung warten, und zwar bis der Balken dieses Tages außerhalb des Bereichs geschlossen wird. Uns passt es auch, wenn der Tag genau an der Grenze des horizontalen Kanals geschlossen wird.
  2. Um den Levels des ursprünglichen StopLoss zu ermitteln, wird entsprechendes zweitägiges Extremum (Hoch oder Tief) des Preises verwendet.

  

Definition von Parametern des Kanals


Um zu überprüfen, ob die Bedingungen erfüllt sind, muss man den höchsten und den niedrigsten Preis des Bereichs kennen, und um diese zu berechnen, müssen die Zeitgrenzen definiert werden. Diese vier Variablen definieren den Kanal in jedem konkreten Moment, deswegen ist es logisch, sie in eine gemeinsame Struktur zu vereinen. Fügen wir noch zwei Variablen hinzu, die in die Handelsstrategie auch miteinbezogen sind: Anzahl der Tage (Balken), die vergingen, nachdem der Preis das Hoch und das Tief des Bereichs erreicht hatte:

struct CHANNEL {
  double    d_High;           // Preis der oberen Grenze des Bereichs
  double    d_Low;            // Preis der unteren Grenze des Bereichs
  datetime  t_From;           // Datum/Zeit des ersten (des ältesten) Balkens im Kanal
  datetime  t_To;             // Datum/Zeit des ersten (des ältesten) Balkens im Kanal
  int       i_Highest_Offset; // Anzahl der Balken rechts vom Preishoch
  int       i_Lowest_Offset;  // Anzahl der Balken rechts vom Preistief
};

Die Funktion f_Set wird diese Variablen pünktlich aktualisieren. Dafür muss sie wissen, ab welchem Balken ein virtueller Kanal (i_Newest_Bar_Shift) gezeichnet werden muss und wie tief in die Historie zugegriffen werden muss (i_Bars_Limit):

void f_Set(int i_Bars_Limit, int i_Newest_Bar_Shift = 1) {
  double da_Price_Array[]; // Hilfsarray für High/Low Preise aller Balken im Kanal
  
  // Bestimmung der oberen Grenze des Bereichs:
  
  int i_Price_Bars = CopyHigh(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array);
  int i_Bar = ArrayMaximum(da_Price_Array);
  d_High = da_Price_Array[i_Bar]; // die obere Grenze des Bereichs ist bestimmt
  i_Highest_Offset = i_Price_Bars - i_Bar; // das Alter des Hochs (in Balken)
  
  // Bestimmung der unteren Grenze des Bereichs:
  
  i_Price_Bars = CopyLow(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array);
  i_Bar = ArrayMinimum(da_Price_Array);
  d_Low = da_Price_Array[i_Bar]; // die untere Grenze des Bereichs ist bestimmt
  i_Lowest_Offset = i_Price_Bars - i_Bar; // das Alter des Tiefs (in Balken)
  
  datetime ta_Time_Array[];
  i_Price_Bars = CopyTime(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, ta_Time_Array);
  t_From = ta_Time_Array[0];
  t_To = ta_Time_Array[i_Price_Bars - 1];
}

Diese Funktion hat nur 13 Zeilen, aber wenn Sie die Dokumentation über MQL-Funktionen für den Zugriff auf Daten aus Zeitreihen gelesen haben (CopyHigh, CopyLow, CopyTime u.a.), wissen Sie, dass da nicht alles so einfach ist. In einigen Fällen kann sich die Zahl der gelieferten Werte von der Zahl der abgerufenen unterscheiden, denn die angeforderten Daten können beim ersten Zugriff auf die gewünschte Zeitreihe nicht bereit sein. Aber bei der richtigen Verarbeitung der Ergebnisse funktioniert das Kopieren von Daten aus den Zeitreihen so wie Sie das gedacht haben.

Deswegen werden wir uns an minimale Kriterien einer guten Programmierung halten und fügen wir einfache "Fehlerverarbeiter" in den Code hinzu. Einfachheitshalber geben wir Informationen über Fehler im Journal aus. Die Protokollierung ist auch beim Debuggen sehr hilfreich: so verfügt man über Informationen darüber, wie der Handelsroboter eine Entscheidung getroffen hat. Fügen wir eine neue Variable vom Aufzählungstyp hinzu, die definieren wird, wie ausführlich die Protokollierung sein muss:

enum ENUM_LOG_LEVEL { // Liste von Ebenen der Protokollierung
  LOG_LEVEL_NONE,     // Protokollierung deaktiviert
  LOG_LEVEL_ERR,      // nur Informationen über Fehler
  LOG_LEVEL_INFO,     // Fehler + Kommentare des Roboters
  LOG_LEVEL_DEBUG     // alles ohne Ausnahmen
};

Der Nutzer wird den notwendigen Level auswählen, und die entsprechenden Operatoren für die Ausgabe der Information im Log platzieren wir in vielen Funktionen. Aus diesem Grund müssen sowohl die Liste, als auch die benutzerdefinierte Variable Log_Level nicht im Signalblock, sondern am Anfang des Programms platziert werden.

Zurück zur Funktion f_Set. Mit allen Überprüfungen wird diese wie folgt aussehen (hinzugefügte Zeilen sind hervorgehoben):

void f_Set(int i_Bars_Limit, int i_Newest_Bar_Shift = 1) {
  double da_Price_Array[]; // Hilfsarray für High/Low Preise aller Balken im Kanal
  
  // Bestimmung der oberen Grenze des Bereichs:
  
  int i_Price_Bars = CopyHigh(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array);
  
  if(i_Price_Bars == WRONG_VALUE) {
    // Fehlerbehandlung der CopyHigh Funktion
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: Fehler #%u", __FUNCSIG__, _LastError);
    return;
  }
  
  if(i_Price_Bars < i_Bars_Limit) {
    // die CopyHigh Funktion hat die angeforderten Daten nicht in vollem Umfang abgerufen 
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: kopiert %u Balken aus %u", __FUNCSIG__, i_Price_Bars, i_Bars_Limit);
    return;
  }
  
  int i_Bar = ArrayMaximum(da_Price_Array);
  if(i_Bar == WRONG_VALUE) {
    // Fehlerbehandlung der ArrayMaximum Funktion
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ArrayMaximum: Fehler #%u", __FUNCSIG__, _LastError);
    return;
  }
  
  d_High = da_Price_Array[i_Bar]; // die obere Grenze des Bereichs ist bestimmt
  i_Highest_Offset = i_Price_Bars - i_Bar; // das Alter des Hochs (in Balken)
  
  // Bestimmung der unteren Grenze des Bereichs:
  
  i_Price_Bars = CopyLow(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array);
  
  if(i_Price_Bars == WRONG_VALUE) {
    // Fehlerbehandlung der CopyLow Funktion
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: Fehler #%u", __FUNCSIG__, _LastError);
    return;
  }
  
  if(i_Price_Bars < i_Bars_Limit) {
    // die Funktion CopyLow hat Daten nicht in vollem Umgang  объёме
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: kopiert %u Balken aus %u", __FUNCSIG__, i_Price_Bars, i_Bars_Limit);
    return;
  }
  
  i_Bar = ArrayMinimum(da_Price_Array);
  if(i_Bar == WRONG_VALUE) {
    // Fehlerbehandlung der ArrayMinimum Funktion
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ArrayMinimum: Fehler #%u", __FUNCSIG__, _LastError);
    return;
  }
  d_Low = da_Price_Array[i_Bar]; // die untere Grenze des Bereichs ist bestimmt
  i_Lowest_Offset = i_Price_Bars - i_Bar; // das Alter des Tiefs (in Balken)
  
  datetime ta_Time_Array[];
  i_Price_Bars = CopyTime(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, ta_Time_Array);
  if(i_Price_Bars < 1) t_From = t_To = 0;
  else {
    t_From = ta_Time_Array[0];
    t_To = ta_Time_Array[i_Price_Bars - 1];
  }
}

Wenn ein Fehler aufgetreten ist, unterbrechen wir die Ausführung und gehen davon aus, dass das Terminal bis zum nächstem Tick genug historische Daten für die Kopierfunktion herunterlädt. Damit andere benutzerdefinierte Funktionen den Kanal nicht verwenden, bis der Vorgang vollständig abgeschlossen ist, fügen wir das entsprechende Flag b_Ready (true = Daten vorbereitet, false = Vorgang nicht abgeschlossen) hinzu. Fügen wir auch das Flag der Veränderung der Parameter im Kanal (b_Updated) hinzu: es ist wichtig zu wissen, ob sich die vier Parameter der Handelsstrategie geändert haben. Dafür fügen wir eine weitere Variable hinzu — die Signatur des Kanals (s_Signature). Wir fügen die Funktion f_Set auch in die Struktur hinzu, und sie (Struktur CHANNEL) wird wie folgt aussehen: 

// Informationen über den Kanal und Funktionen für das Sammeln und Aktualisieren dieser Informationen, in einer Struktur
struct CHANNEL {
  // Variablen
  double    d_High;           // Preis der oberen Grenze des Bereichs
  double    d_Low;            // Preis der unteren Grenze des Bereichs
  datetime  t_From;           // Datum/Zeit des ersten (des ältesten) Balkens im Kanal
  datetime  t_To;             // Datum/Zeit des letzten Balkens im Kanal
  int       i_Highest_Offset; // Anzahl der Balken rechts vom Preishoch
  int       i_Lowest_Offset;  // Anzahl der Balken rechts vom Preistief
  bool      b_Ready;          // Aktualisierung der Parameter abgeschlossen?
  bool      b_Updated;        // haben sich die Parameter des Kanals geändert?
  string    s_Signature;      // Signatur des letzten bekannten Datensets
  
  // Funktionen:
  
  CHANNEL() {
    d_High = d_Low = 0;
    t_From = t_To = 0;
    b_Ready = b_Updated = false;
    s_Signature = "-";
    i_Highest_Offset = i_Lowest_Offset = WRONG_VALUE;
  }
  
  void f_Set(int i_Bars_Limit, int i_Newest_Bar_Shift = 1) {
    b_Ready = false; // PitStop: Flag der Wartung setzen
    
    double da_Price_Array[]; // Hilfsarray für High/Low Preise aller Balken im Kanal
    
    // Bestimmung der oberen Grenze des Bereichs:
    
    int i_Price_Bars = CopyHigh(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array);
    if(i_Price_Bars == WRONG_VALUE) {
      // Fehlerbehandlung der CopyHigh Funktion
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: Fehler #%u", __FUNCSIG__, _LastError);
      return;
    }
    
    if(i_Price_Bars < i_Bars_Limit) {
      // die CopyHigh Funktion hat die angeforderten Daten nicht in vollem Umfang abgerufen 
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: kopiert %u Balken aus %u", __FUNCSIG__, i_Price_Bars, i_Bars_Limit);
      return;
    }
    
    int i_Bar = ArrayMaximum(da_Price_Array);
    if(i_Bar == WRONG_VALUE) {
      // Fehlerbehandlung der ArrayMaximum Funktion
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ArrayMaximum: Fehler #%u", __FUNCSIG__, _LastError);
      return;
    }
    
    d_High = da_Price_Array[i_Bar]; // die obere Grenze des Bereichs ist bestimmt
    i_Highest_Offset = i_Price_Bars - i_Bar; // das Alter des Hochs (in Balken)
    
    // Bestimmung der unteren Grenze des Bereichs:
    
    i_Price_Bars = CopyLow(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array);
    
    if(i_Price_Bars == WRONG_VALUE) {
      // Fehlerbehandlung der CopyLow Funktion
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: Fehler #%u", __FUNCSIG__, _LastError);
      return;
    }
    
    if(i_Price_Bars < i_Bars_Limit) {
      // die Funktion CopyLow hat Daten nicht in vollem Umgang  объёме
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: kopiert %u Balken aus %u", __FUNCSIG__, i_Price_Bars, i_Bars_Limit);
      return;
    }
    
    i_Bar = ArrayMinimum(da_Price_Array);
    if(i_Bar == WRONG_VALUE) {
      // Fehlerbehandlung der ArrayMinimum Funktion
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ArrayMinimum: Fehler #%u", __FUNCSIG__, _LastError);
      return;
    }
    d_Low = da_Price_Array[i_Bar]; // die untere Grenze des Bereichs ist bestimmt
    i_Lowest_Offset = i_Price_Bars - i_Bar; // das Alter des Tiefs (in Balken)
    
    datetime ta_Time_Array[];
    i_Price_Bars = CopyTime(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, ta_Time_Array);
    if(i_Price_Bars < 1) t_From = t_To = 0;
    else {
      t_From = ta_Time_Array[0];
      t_To = ta_Time_Array[i_Price_Bars - 1];
    }
    
    string s_New_Signature = StringFormat("%.5f%.5f%u%u", d_Low, d_High, t_From, t_To);
    if(s_Signature != s_New_Signature) {
      // Daten des Kanal haben sich geändert
      b_Updated = true;
      if(Log_Level > LOG_LEVEL_ERR) PrintFormat("%s: Kanal aktualisiert: %s .. %s / %s .. %s, min: %u max: %u ", __FUNCTION__, DoubleToString(d_Low, _Digits), DoubleToString(d_High, _Digits), TimeToString(t_From, TIME_DATE|TIME_MINUTES), TimeToString(t_To, TIME_DATE|TIME_MINUTES), i_Lowest_Offset, i_Highest_Offset);
      s_Signature = s_New_Signature;
    }
    
    b_Ready = true; // Datenaktualisierung erfolgreich abgeschlossen
  }
};
Deklarieren wir einen "Objekt-Kanal" von diesem Typ an der globalen Ebene (damit er aus verschiedenen benutzerdefinierten Funktionen zugänglich ist):

CHANNEL go_Channel;

Funktion der Signalgenerierung


Nach diesem System wird ein Kaufsignal anhand zwei obligatorischen Bedingungen ermittelt:

1. Nach dem letzten 20-tägigen Tief sind mindestens drei Handelstage vergangen

2a. Der Preis des Symbols ist unter den 20-tägigen Tief gesunken (Turtle Soup)

2b. Der Tagesbalken wurde unterhalb des 20-tägigen Tiefs geschlossen (Turtle Soup Plus One)


 

Alle anderen oben aufgezählten Regeln der Handelsstrategie gehören zu den Parametern der Handelsorder und zum Position-Management, wir werden diese nicht in den Signalblock hinzufügen.

Im einem Modul programmieren wir die Erkennung von Signalen nach den Regeln der beiden Versionen der Handelsstrategie Turtle Soup und Turtle Soup Plus One, und in die Einstellungen des Expert Advistors fügen wir die Möglichkeit hinzu, die notwendige Version der Regeln auszuwählen. Nennen wir die entsprechende benutzerdefinierte Variable Strategy_Type. In unserem Fall enthält die Liste der Strategien nur zwei Varianten, deswegen wäre es einfacher, true/false (Variable vom Typ bool) zu verwenden. Aber wir lassen uns die Möglichkeit offen, nach dem Ende dieser Artikelreihe alle in den Code übersetzen Strategien aus dem Buch in einem Expert Advisor zu vereinen, deswegen benutzen wir eine nummerierte Liste:

enum ENUM_STRATEGY {     // Liste der Strategien
  TS_TURTLE_SOUP,        // Turtle Soup
  TS_TURTLE_SOUP_PLUS_1  // Turtle Soup Plus One
};
input ENUM_STRATEGY  Strategy_Type = TS_TURTLE_SOUP;  // Handelsstrategie:

Der Typ der Strategie muss der Funktion für Signalerkennung im Hauptprogramm übergeben werden, d.h., die Funktion muss Bescheid wissen, ob sie warten muss, bis der Balken (Tag) geschlossen wird — die Variable b_Wait_For_Bar_Close vom Typ bool. Die zweite notwendige Variable ist die Dauer der Pause nach dem letzten Extremum i_Extremum_Bars. Die Funktion muss den Signalstatus liefern: ob Kauf- bzw. Verkaufsbedingungen erfüllt sind oder ob man noch abwarten sollte. Die notwendige nummerierte Liste wird auch in der Hauptdatei des Expert Advisors platziert:

enum ENUM_ENTRY_SIGNAL {  // Liste der Einstiegssignale
  ENTRY_BUY,              // Kaufsignal
  ENTRY_SELL,             // Verkaufssignal
  ENTRY_NONE,             // kein Signal
  ENTRY_UNKNOWN           // Status unbekannt
};

Eine weitere Struktur, die sowohl vom Signalmodul, als auch von Funktionen des Haupt-Programms verwendet wird, ist das globale Objekt go_Tick, das Informationen über den allerletzten Tick beinhaltet. Das ist eine Standardfunktion vom Typ MqlTick, die in der Hauptdatei deklariert wird. Ihre Aktualisierung programmieren wir später im Body des Hauptprogramms (in der Funktion OnTick).

MqlTick go_Tick; // Information über den allerletzten bekannten Tick

Nun kommen wir schließlich zur wichtigsten Funktion des Moduls

ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal(
  bool b_Wait_For_Bar_Close = false,
  int i_Extremum_Bars = 3
) {}

Fangen wir mit der Überprüfung der Bedingungen für ein Verkaufssignal an. Überprüfen wir, ob es genug Tage (Balken) seit dem vorherigen Hoch vergangen sind (erste Bedingung) und ob der Preis die obere Grenze des Bereichs durchbrochen hat (zweite Bedingung):

if(go_Channel.i_Highest_Offset > i_Extremum_Bars) // erste Bedingung
  if(go_Channel.d_High < d_Actual_Price) // zweite Bedingung
    return(ENTRY_SELL); // beide Verkaufsbedingungen sind erfüllt

Die Bedingungen für ein Kaufsignal werden gleich überprüft:

if(go_Channel.i_Lowest_Offset > i_Extremum_Bars) // erste Bedingung
  if(go_Channel.d_Low > d_Actual_Price) { // zweite Bedingung
    return(ENTRY_BUY); // beide Kaufbedingungen sind erfüllt

Hier wurde die Variable d_Actual_Price verwendet, die den aktuellen Preis für die entsprechende Version der Handelsstrategie enthält. Für Turtle Soup ist das der letzte bekannte Bid-Preis, für Turtle Soup Plus One — der Schlusskurs des vorherigen Tages (Balkens):

double d_Actual_Price = go_Tick.bid; // der standardmäßige Preis - für die Version Turtle Soup
if(b_Wait_For_Bar_Close) { // für die Version Turtle Soup Plus One
  double da_Price_Array[1]; // Hilfsarray
  CopyClose(_Symbol, PERIOD_CURRENT, 1, 1, da_Price_Array));
  d_Actual_Price = da_Price_Array[0];
}

Eine Funktion, die alles Notwendige umfasst, sieht wie folgt aus:

ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal(bool b_Wait_For_Bar_Close = false, int i_Extremum_Bars = 3) {
  double d_Actual_Price = go_Tick.bid; // der standardmäßige Preis - für die Version Turtle Soup
  if(b_Wait_For_Bar_Close) { // für die Version Turtle Soup Plus One
    double da_Price_Array[1];
    CopyClose(_Symbol, PERIOD_CURRENT, 1, 1, da_Price_Array));
    d_Actual_Price = da_Price_Array[0];
  }
  
  // obere Grenze:
  if(go_Channel.i_Highest_Offset > i_Extremum_Bars) // erste Bedingung
    if(go_Channel.d_High < d_Actual_Price) { // zweite Bedingung
      // der Preis hat die obere Grenze durchbrochen
      return(ENTRY_SELL);
    }
  
  // untere Grenze:
  if(go_Channel.i_Lowest_Offset > i_Extremum_Bars) // erste Bedingung
    if(go_Channel.d_Low > d_Actual_Price) { // zweite Bedingung
      // der Preis hat die untere Grenze durchbrochen
      return(ENTRY_BUY);
    }
  
  return(ENTRY_NONE);
}

Denken Sie daran, dass der Objekt-Kanal nicht zum Lesen seiner Daten bereit sein kann (Flag go_Channel.b_Ready = false). Fügen wir also eine Überprüfung für dieses Flag hinzu. In dieser Funktion verwenden wir eine der Standardfunktionen für das Kopieren von Daten aus einer Zeitreihe (CopyClose), deswegen fügen wir auch die Behandlung eines eventuellen Fehlers. Vergessen wir nicht auch die Protokollierung von Daten, die das Debugging wesentlich erleichtert:

ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal(bool b_Wait_For_Bar_Close = false, int i_Extremum_Bars = 3) {
  if(!go_Channel.b_Ready) {
    // die Daten des Kanals sind nicht vorbereitet
    if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: Parameter des Kanals nicht vorbereitet", __FUNCTION__);
    return(ENTRY_UNKNOWN);
  }
  
  double d_Actual_Price = go_Tick.bid; // der standardmäßige Preis - für die Version Turtle Soup
  if(b_Wait_For_Bar_Close) { // für die Version Turtle Soup Plus One
    double da_Price_Array[1];
    if(WRONG_VALUE == CopyClose(_Symbol, PERIOD_CURRENT, 1, 1, da_Price_Array)) {
      // Fehlerverarbeitung der Funktion CopyClose
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyClose: Fehler #%u", __FUNCSIG__, _LastError);
      return(ENTRY_NONE);
    }
    d_Actual_Price = da_Price_Array[0];
  }
  
  // obere Grenze:
  if(go_Channel.i_Highest_Offset > i_Extremum_Bars) // erste Bedingung
    if(go_Channel.d_High < d_Actual_Price) { // zweite Bedingung
      // der Preis hat die obere Grenze durchbrochen
      if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: der Preis (%s) hat die obere Grenze durchbrochen (%s)", __FUNCTION__, DoubleToString(d_Actual_Price, _Digits), DoubleToString(go_Channel.d_High, _Digits));
      return(ENTRY_SELL);
    }
  
  // untere Grenze:
  if(go_Channel.i_Lowest_Offset > i_Extremum_Bars) // erste Bedingung
    if(go_Channel.d_Low > d_Actual_Price) { // zweite Bedingung
      // der Preis hat die untere Grenze durchbrochen
      if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: der Preis (%s) hat die untere Grenze durchbrochen (%s)", __FUNCTION__, DoubleToString(d_Actual_Price, _Digits), DoubleToString(go_Channel.d_Low, _Digits));
      return(ENTRY_BUY);
    }
  
  // wenn das Programm diese Zeile erreicht hat, liegt der Preis innerhalb des Bereichs, die zweite Bedingung ist nicht erfüllt 
  
  return(ENTRY_NONE);
}

Diese Funktion wird auf jedem Tick aufgerufen, d.h. hunderttausend Mal pro Tag. Aber wenn die erste Bedingung nicht erfüllt ist (mindestens drei Tage seit dem letzten Extremum), dann ist die ganze Arbeit nach der ersten Überprüfung sinnlos. Im Sinne von guten Manieren beim Programmieren, um den Ressourcenverbrauch zu minimieren, lassen wir die Funktion bis zum nächsten Balken (Tag) "einschlafen", d.h. bis zur Aktualisierung der Kanalparameter:

ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal(bool b_Wait_For_Bar_Close = false, int i_Extremum_Bars = 3) {
  static datetime st_Pause_End = 0; // Zeit der nächsten Überprüfung
  if(st_Pause_End > go_Tick.time) return(ENTRY_NONE);
  st_Pause_End = 0;
  
  if(go_Channel.b_In_Process) {
    // die Daten des Kanals sind nicht vorbereitet
    if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: Parameter des Kanals nicht vorbereitet", __FUNCTION__);
    return(ENTRY_UNKNOWN);
  }
  if(go_Channel.i_Lowest_Offset < i_Extremum_Bars && go_Channel.i_Highest_Offset < i_Extremum_Bars) {
    // 1. Bedingung nicht erfüllt
    if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: 1. Bedingung nicht erfüllt", __FUNCTION__);
    
    // stoppen bis zur Aktualisierung des Kanals
    st_Pause_End = go_Tick.time + PeriodSeconds() - go_Tick.time % PeriodSeconds();
    
    return(ENTRY_NONE);
  }
  
  double d_Actual_Price = go_Tick.bid; // der standardmäßige Preis - für die Version Turtle Soup
  if(b_Wait_For_Bar_Close) { // für die Version Turtle Soup Plus One
    double da_Price_Array[1];
    if(WRONG_VALUE == CopyClose(_Symbol, PERIOD_CURRENT, 1, 1, da_Price_Array)) {
      // Fehlerverarbeitung der Funktion CopyClose
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyClose: Fehler #%u", __FUNCSIG__, _LastError);
      return(ENTRY_NONE);
    }
    d_Actual_Price = da_Price_Array[0];
  }
  
  // obere Grenze:
  if(go_Channel.i_Highest_Offset > i_Extremum_Bars) // erste Bedingung
    if(go_Channel.d_High < d_Actual_Price) { // zweite Bedingung
      // der Preis hat die obere Grenze durchbrochen
      if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: der Preis (%s) hat die obere Grenze durchbrochen (%s)", __FUNCTION__, DoubleToString(d_Actual_Price, _Digits), DoubleToString(go_Channel.d_High, _Digits));
      return(ENTRY_SELL);
    }
  
  // untere Grenze:
  if(go_Channel.i_Lowest_Offset > i_Extremum_Bars) // erste Bedingung
    if(go_Channel.d_Low > d_Actual_Price) { // zweite Bedingung
      // der Preis hat die untere Grenze durchbrochen
      if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: der Preis (%s) hat die untere Grenze durchbrochen (%s)", __FUNCTION__, DoubleToString(d_Actual_Price, _Digits), DoubleToString(go_Channel.d_Low, _Digits));
      return(ENTRY_BUY);
    }
  
  // wenn das Programm diese Zeile erreicht hat, liegt der Preis innerhalb des Bereichs, die zweite Bedingung ist nicht erfüllt 
  
  if(b_Wait_For_Bar_Close) // für die Version Turtle Soup Plus One
    // stoppen bis der aktuelle Balken geschlossen wird
    st_Pause_End = go_Tick.time + PeriodSeconds() - go_Tick.time % PeriodSeconds();
  
  return(ENTRY_NONE);
}

So sieht der Code der Funktion aus. Nennen wir das Signalmodul Signal_Turtle_Soup.mqh, fügen wir den Code ins Modul hinzu, der zum Kanal und zu den Signalen gehört, und am Anfang der Datei fügen wir Eingabefelder benutzerdefinierter Einstellungen der Strategie hinzu:

enum ENUM_STRATEGY {     // Variante der Strategie
  TS_TURTLE_SOUP,        // Turtle Soup
  TS_TURTLE_SOUP_PLIS_1  // Turtle Soup Plus One
};

// benutzerdefinierte Einstellungen
input ENUM_STRATEGY  Turtle_Soup_Type = TS_TURTLE_SOUP;  // Turtle Soup: Variante der Strategie
input uint           Turtle_Soup_Period_Length = 20;     // Turtle Soup: Tiefe der Suche nach Extrema (in Balken)
input uint           Turtle_Soup_Extremum_Offset = 3;    // Turtle Soup: Pause nach dem letzten Extremum (in Balken)
input double         Turtle_Soup_Entry_Offset = 10;      // Turtle Soup: Einstieg: Abstand von der Extremum-Ebene (in Punkten)
input double         Turtle_Soup_Exit_Offset = 1;        // Turtle Soup: Ausstieg: Abstand von dem gegensätzlichen Extremum (in Punkten)

Diese Datei muss im Verzeichnis des Terminals gespeichert werden: Signalbibliotheken werden im Ordner MQL5\Include\Expert\Signal gespeichert.

 

Ein Basis-Expert Advisor für das Testen der Handelsstrategie


Am Anfang des Codes platzieren wir Felder benutzerdefinierter Einstellungen, und davor — die in den Einstellungen verwendeten Listen vom Aufzählungstyp enum. Teilen wir das Feld der Einstellungen in zwei Gruppen: "Einstellungen der Strategie" und "Eröffnung und Management von Positionen". Die Einstellungen der ersten Gruppe werden bei der Kompilierung aus einer Datei der Bibliothek der Signale hinzugefügt. Momentan haben wir nur eine solche Datei erstellt, aber in weiteren Artikeln werden auch weitere Strategien aus dem Buch programmiert und es wird möglich sein, Signalmodule mit notwendigen benutzerdefinierten Einstellungen zu ersetzen (oder hinzuzufügen).

Fügen wir eine Datei der MQL5 Standardbibliothek für die Durchführung von Operationen am Anfang des Codes hinzu:

enum ENUM_LOG_LEVEL {  // Liste von Ebenen der Protokollierung
  LOG_LEVEL_NONE,      // Protokollierung deaktiviert
  LOG_LEVEL_ERR,       // nur Informationen über Fehler
  LOG_LEVEL_INFO,      // Fehler + Kommentare des Roboters
  LOG_LEVEL_DEBUG      // alles ohne Ausnahmen
};
enum ENUM_ENTRY_SIGNAL {  // Liste der Einstiegssignale
  ENTRY_BUY,              // Kaufsignal
  ENTRY_SELL,             // Verkaufssignal
  ENTRY_NONE,             // kein Signal
  ENTRY_UNKNOWN           // Status unbekannt
};

#include <Trade\Trade.mqh> // Klasse für die Ausführung von Trades



input string  _ = "** Einstellungen der Strategie:";  // .

#include <Expert\Signal\Signal_Turtle_Soup.mqh> // Signalmodul

                                                        
input string  __ = "** Eröffnung und Management von Positionen:"; // .
input double  Trade_Volume = 0.1;                  // Tradevolumen
input uint    Trail_Trigger = 100;                 // Trailing Stop: Distanz für das Aktivieren von Trailing (in Punkten)
input uint    Trail_Step = 5;                      // Trailing: SL Schritt (in Punkten)
input uint    Trail_Distance = 50;                 // Trailing: Maximale Distanz vom Preis bis SL (in Punkten)
input ENUM_LOG_LEVEL  Log_Level = LOG_LEVEL_INFO;  // Protokollierungsmodus:

Die Autoren erwähnen keine speziellen Methoden des Risiko- oder Kapitalmanagements für diese Strategie, deswegen werden wir eine feste Lotgröße für alle Trades verwenden.

Die Trailing Einstellungen sind in Punkten einzugeben. Die Einführung fünfstelliger Kurse hat Verwirrungen mit diesen Messeinheiten verursacht, deshalb stellen wir das hier noch einmal klar: ein Punkt entspricht der minimalen Preisänderung eines Symbols. Bei fünfstelligen Kursen ist ein Punkt gleich 0.00001, und bei vierstelligen — 0.0001. Punkte sind nicht mit Pips zu verwechseln, denn Pips ignorieren die tatsächliche Genauigkeit der Kurse und wandeln diese in vierstellige um. D.h. wenn die minimale Preisänderung eines Symbols (Punkt) gleich 0.00001 ist, dann ist ein Pip gleich 10 Punkte, und wenn der Punkt gleich 0.0001 ist, sind der Pip-Preis und Punkt-Preis gleich.

Die Trailing Stop Funktion verwendet diese Einstellungen auf jedem Tick, und obwohl die Umrechnung der vom Nutzer eingegebenen Punkte in reale Symbolpreise nicht viele Ressourcen in Anspruch nimmt, wird sie aber hunderttausend Mal pro Tag durchgeführt. Richtiger wird es die vom Nutzer eingegebenen Werte bei der Initialisierung des Expert Advisors einmal zu berechnen, und diese in die globalen Variablen für weitere Verwendung zu speichern. Das Gleiche kann man auch mit den Variablen machen, die für die Normalisierung der Lotgröße verwendet werden — Begrenzungen des Servers für die maximale und minimale Größe sowie der Schritt bleiben während des Laufens des Expert Advisors unverändert, deshalb brauchen sie nicht jedes mal neu aufgerufen werden. Die Deklaration globaler Variablen und die Funktion der Initialisierung werden wie folgt aussehen:

int
  gi_Try_To_Trade = 4, // Anzahl der Versuche, eine Handelsorder zu senden
  gi_Connect_Wait = 2000 // Pause zwischen den Versuchen (in Millisekunden)
;
double
  gd_Stop_Level, // StopLevel aus den Einstellungen des Servers, umgerechnet in den Preis des Symbols
  gd_Lot_Step, gd_Lot_Min, gd_Lot_Max, // Begrenzung der Lotgröße aus den Einstellungen des Servers
  gd_Entry_Offset, // Einstieg: Abstand vom Extremum in Preisen des Symbols
  gd_Exit_Offset, // Austieg: Abstand vom Extremum in Preisen des Symbols
  gd_Trail_Trigger, gd_Trail_Step, gd_Trail_Distance // Trailing Parameter, umgerechnet in den Symbolpreis
;
MqlTick go_Tick; // Information über den allerletzten bekannten Tick



int OnInit() {
  // Umwandlung der Einstellungen aus Punkten in Preise:
  double d_One_Point_Rate = pow(10, _Digits);
  gd_Entry_Offset = Turtle_Soup_Entry_Offset / d_One_Point_Rate;
  gd_Exit_Offset = Turtle_Soup_Exit_Offset / d_One_Point_Rate;
  gd_Trail_Trigger = Trail_Trigger / d_One_Point_Rate;
  gd_Trail_Step = Trail_Step / d_One_Point_Rate;
  gd_Trail_Distance = Trail_Distance / d_One_Point_Rate;
  gd_Stop_Level = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) / d_One_Point_Rate;
  // Initialisierung der Lot-Begrenzungen:
  gd_Lot_Min = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
  gd_Lot_Max = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
  gd_Lot_Step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
  
  return(INIT_SUCCEEDED);
}

Es ist zu betonen, dass es ein Trailing Modul vom benötigten Typ (TrailingFixedPips.mqh) in der Standardbibliothek gibt. Es kann in den Code hinzugefügt werden, wie wir das mit der Klasse gemacht haben. Es entspricht aber nicht ganz den Besonderheiten dieses konkreten Expert Advisors, deshalb schreiben wir selbst einen Trailing Code und fügen ihn in den Körper des Expert Advisors als eine selbständige benutzerdefinierte Funktion hinzu:

bool fb_Trailing_Stop(    // Trailing SL der Position des aktuellen Symbols
  double d_Trail_Trigger,  // Distanz für das Aktivieren von Trailing (in Preisen des Symbols)
  double d_Trail_Step,    // SL Trailing Schritt (in Preisen des Symbols)
  double d_Trail_Distance  // minimale Distanz vom Preis bis zur SL Ebene (in Preisen des Symbols)
) {
  if(!PositionSelect(_Symbol)) return(false); // keine Position, kein Trailing 
  
  // als Basiswert für die Berechnung der neuen SL Ebene gilt der aktuelle Preiswert:
  double d_New_SL = PositionGetDouble(POSITION_PRICE_CURRENT);
  
  if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { // für eine Long-Position
    if(d_New_SL - PositionGetDouble(POSITION_PRICE_OPEN) < d_Trail_Trigger)
      return(false); // der Preis ist noch nicht weit genug, um Tailing zu aktivieren
      
    if(d_New_SL - PositionGetDouble(POSITION_SL) < d_Trail_Distance + d_Trail_Step)
      return(false); // die Änderung des Preises ist kleiner als der gesetzte Schritt für die SL Verschiebung
    
    d_New_SL -= d_Trail_Distance; // neuer SL Level
  } else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { // für eine Short-Position
    if(PositionGetDouble(POSITION_PRICE_OPEN) - d_New_SL < d_Trail_Trigger)
      return(false); // der Preis ist noch nicht weit genug, um Tailing zu aktivieren
    
    if(PositionGetDouble(POSITION_SL) > 0.0) if(PositionGetDouble(POSITION_SL) - d_New_SL < d_Trail_Distance + d_Trail_Step)
      return(false); // der Preis ist noch nicht weit genug, um Tailing zu aktivieren
    
    d_New_SL += d_Trail_Distance; // neuer SL Level
  } else return(false);
  
  // erlauben die Einstellungen des Servers die berechnete SL Ebene mit diesem Abstand vom aktuellen Preis zu platzieren?
  if(!fb_Is_Acceptable_Distance(d_New_SL, PositionGetDouble(POSITION_PRICE_CURRENT))) return(false);
  
  CTrade Trade;
  Trade.LogLevel(LOG_LEVEL_ERRORS);
  // SL verschieben
  Trade.PositionModify(_Symbol, d_New_SL, PositionGetDouble(POSITION_TP));
  
  return(true);
}



bool fb_Is_Acceptable_Distance(double d_Level_To_Check, double d_Current_Price) {
  return(
    fabs(d_Current_Price - d_Level_To_Check)
    >
    fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid)
  );
}

Die Überprüfung, ob SL an der berechneten Ebene platziert werden kann, befindet sich in der separaten Funktion fb_Is_Acceptable_Distance, um diese auch beim Validieren der Ebene für das Platzieren einer Pending Order und beim Setzen von StopLoss einer offenen Position zu verwenden.

Nun gehen wir zum Hauptarbeitsbereich im Code des Expert Advisors, der durch die OnTick Funktion aufgerufen wird (Event-Handler, der das Ereignis eines neu eingehendes Ticks verarbeitet). Laut den Regeln der Strategie soll man nicht nach neuen Signalen suchen, wenn es eine offene Position vorhanden ist, deswegen fangen wir mit der entsprechenden Überprüfung an. Wenn es eine Position gibt, hat der Expert Advisor zwei Varianten: entweder den ursprünglichen StopLoss für die neue Position zu berechnen und zu setzen, oder die Trailing Funktion zu aktivieren, die dann festlegt, ob die StopLoss Ebene verschoben werden muss, und die notwendige Operation durchführt. Der Aufruf der Trailing Funktion ist einfacher. Und was die Berechnung der StopLoss Ebene angeht, werden wir den vom Nutzer eingegebenen und aus Punkten in den Preis umgerechneten Abstand vom Extremum gd_Exit_Offset verwenden. Definieren wir das Extremum des Preises mithilfe der Standardfunktionen CopyHigh oder CopyLow. Die Validität der berechneten Ebenen muss mithilfe der Funktion fb_Is_Acceptable_Distance und des aktuellen Preiswertes aus der Struktur go_Tick überprüft werden. Diese Berechnungen und Überprüfungen werden für BuyStop und SellStop Orders im Code voneinander getrennt:

if(PositionSelect(_Symbol)) { // eine offene Position vorhanden
  if(PositionGetDouble(POSITION_SL) == 0.) { // neue Position
    double
      d_SL = WRONG_VALUE, // SL Level
      da_Price_Array[] // Hilfsarray
    ;
    
    // StopLoss Level berechnen:
    if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { // für eine Long-Position
      if(WRONG_VALUE == CopyLow(_Symbol, PERIOD_CURRENT, 0, 1 + (Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1), da_Price_Array)) {
        // Fehlerbehandlung der CopyLow Funktion
        if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: Fehler #%u", __FUNCTION__, _LastError);
        return;
      }
      d_SL = da_Price_Array[ArrayMinimum(da_Price_Array)] - gd_Exit_Offset;
      
      // ist der Abstand vom aktuellen Preis ausreichend?
      if(!fb_Is_Acceptable_Distance(d_SL, go_Tick.bid)) {
        if(Log_Level > LOG_LEVEL_NONE) PrintFormat("Die berechnete SL Ebene %s wurde durch die minimal erlaubte %s ersetzt", DoubleToString(d_SL, _Digits), DoubleToString(go_Tick.bid + fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid), _Digits));
        d_SL = go_Tick.bid - fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid);
      }
      
    } else { // für eine Short-Position
      if(WRONG_VALUE == CopyHigh(_Symbol, PERIOD_CURRENT, 0, 1 + (Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1), da_Price_Array)) {
        // Fehlerbehandlung der CopyHigh Funktion
        if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: Fehler #%u", __FUNCTION__, _LastError);
        return;
      }
      d_SL = da_Price_Array[ArrayMaximum(da_Price_Array)] + gd_Exit_Offset;
      
      // ist der Abstand vom aktuellen Preis ausreichend?
      if(!fb_Is_Acceptable_Distance(d_SL, go_Tick.ask)) {
        if(Log_Level > LOG_LEVEL_NONE) PrintFormat("Die berechnete SL Ebene %s wurde durch die minimal erlaubte %s ersetzt", DoubleToString(d_SL, _Digits), DoubleToString(go_Tick.ask - fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid), _Digits));
        d_SL = go_Tick.ask + fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid);
      }
    }
    
    CTrade Trade;
    Trade.LogLevel(LOG_LEVEL_ERRORS);
    // SL setzen
    Trade.PositionModify(_Symbol, d_SL, PositionGetDouble(POSITION_TP));
    return;
  }
  
  // Trailing
  fb_Trailing_Stop(gd_Trail_Trigger, gd_Trail_Step, gd_Trail_Distance);
  return;
}

Neben den bereits berechneten Parametern des Ticks müssen auch die Parameter des Kanals aktualisiert werden, welche zur Signalerkennung dienen. Aber die für diese Aktualisierung zuständige Funktion f_Set der Struktur go_Channel sollte erst nach dem Schließen eines Balkens aufgerufen werden, denn während der restlichen Zeit bleiben diese Parameter unverändert. Der neue Expert Advisor verfügt über eine weitere Aktion, die auch mit dem Anfang eines neuen Tages (Balkens) verbunden ist: das Löschen der gestrigen veralteten Pending Order. Programmieren wir diese zwei Aktionen:

int
  i_Order_Ticket = WRONG_VALUE, // Ticket einer Pending Order 
  i_Try = gi_Try_To_Trade, // Anzahl der Versuche, die Operation durchzuführen
  i_Pending_Type = -10 // Typ der vorhandenen Pending Order
;
static int si_Last_Tick_Bar_Num = 0; // Nummer des Balkens des vorherigen Ticks (0 = Anfang der Zeitrechnung nach MQL)

// Verarbeitung von Events, die mit dem Anfang eines neuen Tages (Balkens) verknüpft sind:
if(si_Last_Tick_Bar_Num < int(floor(go_Tick.time / PeriodSeconds()))) {
  // Hallo neuer Tag :)
  si_Last_Tick_Bar_Num = int(floor(go_Tick.time / PeriodSeconds()));
  
  // gibt es eine veraltete Pending Order?
  i_Pending_Type = fi_Get_Pending_Type(i_Order_Ticket);
  if(i_Pending_Type == ORDER_TYPE_SELL_STOP || i_Pending_Type == ORDER_TYPE_BUY_STOP) {
    // veraltete Order löschen:
    if(Log_Level > LOG_LEVEL_ERR) Print("Gestrige Pending Order löschen");
    
    CTrade o_Trade;
    o_Trade.LogLevel(LOG_LEVEL_ERRORS);
    while(i_Try-- > 0) { // Versuche, zu löschen
      if(o_Trade.OrderDelete(i_Order_Ticket)) { // erfolgreich
        i_Try = -10; // Flag einer erfolgreichen Operation
        break;
      }
      // Versuch fehlgeschlagen
      Sleep(gi_Connect_Wait); // Pause vor dem nächsten Versuch
    }
    
    if(i_Try == WRONG_VALUE) { // Löschen der Pending Order fehlgeschlagen
      if(Log_Level > LOG_LEVEL_NONE) Print("Beim Löschen der Pending Order ist ein Fehler aufgetreten");
      return; // warten auf den nächsten Tick
    }
  }
  
  // Aktualisierung der Parameter des Kanals:
  go_Channel.f_Set(Turtle_Soup_Period_Length, 1 + (Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1));
}

Die hier verwendete Funktion fi_Get_Pending_Type liefert den Typ einer Pending Order, und fügt ihr nach dem erhaltenen Verweis auf die i_Order_Ticket Variable die Ticketnummer hinzu. Der Ordertyp wird später für den Vergleich mit der auf diesem Tick aktuellen Richtung des Signals benötigt, und das Ticket wird dann verwendet, wenn die Order gelöscht werden muss. Wenn keine Pending Order vorhanden ist, sind beide Werte gleich WRONG_VALUE. Listing dieser Funktion:

int fi_Get_Pending_Type( // Ermitteln, ob es eine Pernding Order des aktuellen Symbols gibt
  int& i_Order_Ticket // Verweis auf das Ticket der gewählten Pending Order
) {
  int
    i_Order = OrdersTotal(), // Gesamtzahl der Order
    i_Order_Type = WRONG_VALUE // Variable für den Ordertyp
  ;
  i_Order_Ticket = WRONG_VALUE; // Wert des Tickets, der standardmäßig zurückgegeben wird
  
  if(i_Order < 1) return(i_Order_Ticket); // keine Orders
  
  while(i_Order-- > 0) { // über vorhandene Orders iterieren
    i_Order_Ticket = int(OrderGetTicket(i_Order)); // Ticket lesen
    if(i_Order_Ticket > 0)
      if(StringCompare(OrderGetString(ORDER_SYMBOL), _Symbol, false) == 0) {
        i_Order_Type = int(OrderGetInteger(ORDER_TYPE));
        // es werden nur Pending Orders benötigt:
        if(i_Order_Type == ORDER_TYPE_BUY_LIMIT || i_Order_Type == ORDER_TYPE_BUY_STOP || i_Order_Type == ORDER_TYPE_SELL_LIMIT || i_Order_Type == ORDER_TYPE_SELL_STOP)
           break; // eine Pending Order gefunden
      }
    i_Order_Ticket = WRONG_VALUE; // noch nicht gefunden
  }
  
  return(i_Order_Type);
}

Nun ist alles bereit zur Erkennung des Signalstatus. Wenn die Bedingungen der Handelsstrategie nicht erfüllt sind (der Signal hat dabei den Status ENTRY_NONE oder ENTRY_UNKNOWN), kann das Hauptprogramm auf diesem Tick beendet werden:

// Signalstatus erhalten:
ENUM_ENTRY_SIGNAL e_Signal = fe_Get_Entry_Signal(Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1, Turtle_Soup_Extremum_Offset);
if(e_Signal > 1) return; // kein Signal

Wenn es ein Signal gibt, vergleichen wir es mit der Richtung der vorhandenen Pending Order (wenn eine bereits platziert wurde):

// ermitteln wir den Typ der Pending Order und ihr Ticket, wenn dies noch nicht getan wurde:
if(i_Pending_Type == -10)
  i_Pending_Type = fi_Get_Pending_Type(i_Order_Ticket);

// wird eine neue Pending Order benötigt?
if(
  (e_Signal == ENTRY_SELL && i_Pending_Type == ORDER_TYPE_SELL_STOP)
  ||
  (e_Signal == ENTRY_BUY && i_Pending_Type == ORDER_TYPE_BUY_STOP)
) return; // es gibt bereits eine Pending Order in der Richtung des Signals

// muss die Pending Order gelöscht werden?
if(
  (e_Signal == ENTRY_SELL && i_Pending_Type == ORDER_TYPE_BUY_STOP)
  ||
  (e_Signal == ENTRY_BUY && i_Pending_Type == ORDER_TYPE_SELL_STOP)
) { // die Richtung der Pending Order stimmt nicht mit der Richtung des Signals überein
  if(Log_Level > LOG_LEVEL_ERR) Print("Die Richtung der Pending Order entspricht nicht der Richtung des Signals");
    
  i_Try = gi_Try_To_Trade;
  while(i_Try-- > 0) { // Versuche, zu löschen
    if(o_Trade.OrderDelete(i_Order_Ticket)) { // erfolgreich
      i_Try = -10; // Flag einer erfolgreichen Operation
      break;
    }
    // Versuch fehlgeschlagen
    Sleep(gi_Connect_Wait); // Pause vor dem nächsten Versuch
  }
  
  if(i_Try == WRONG_VALUE) { // Löschen der Pending Order fehlgeschlagen
    if(Log_Level > LOG_LEVEL_NONE) Print("Beim Löschen der Pending Order ist ein Fehler aufgetreten");
    return; // warten auf den nächsten Tick
  }
}

Nun gibt es keine Zweifel an der Notwendigkeit einer neuen Pending Order, berechnen wir ihre Parameter. Nach den Regeln der Strategie muss die Order mit einem Abstand von den Grenzen des Kanals (nach innen) platziert werden. StopLoss muss an der entgegengesetzten Seite der Grenze neben dem Extremum des heutigen oder zweitägigen Preises (je nach der gewählten Version der Strategie) platziert werden. Aber der StopLoss Level muss erst nach der Auslösung der Pending Order berechnet werden. Der Code dafür ist oben angeführt.


Die aktuellen Grenzen des Kanals lesen wir aus der Struktur go_Channel, den benutzerdefinierten und in die Preise des Symbols umgerechneten Abstand für den Einstieg beinhaltet die Variable gd_Entry_Offset. Der berechnete Level muss mithilfe der Funktion fb_Is_Acceptable_Distance und des aktuellen Preiswertes in der Struktur go_Tick validiert werden. Diese Berechnungen und Überprüfungen werden für BuyStop und SellStop Orders im Code voneinander getrennt:

double d_Entry_Level = WRONG_VALUE; // Level, an dem eine Pending Order platziert wird
if(e_Signal == ENTRY_BUY) { // für eine Buy Pending Orderу
  // überprüfen, ob es möglich ist, eine Pending Order zu platzieren:
  d_Entry_Level = go_Channel.d_Low + gd_Entry_Offset; // Level zum Platzieren der Order
  if(!fb_Is_Acceptable_Distance(d_Entry_Level, go_Tick.ask)) {
    // der Abstand vom aktuellen Preis ist nicht ausreichend 
    if(Log_Level > LOG_LEVEL_ERR)
      PrintFormat("BuyStop darf nicht an %s gesetzt werden. Bid: %s Ask: %s StopLevel: %s",
        DoubleToString(d_Entry_Level, _Digits),
        DoubleToString(go_Tick.bid, _Digits),
        DoubleToString(go_Tick.ask, _Digits),
        DoubleToString(gd_Stop_Level, _Digits)
      );
    
    return; // warten auf die Änderung des aktuellen Preises 
  }
} else {
  // überprüfen, ob es möglich ist, eine Pending Order zu platzieren:
  d_Entry_Level = go_Channel.d_High - gd_Entry_Offset; // Level zum Platzieren der Order
  if(!fb_Is_Acceptable_Distance(d_Entry_Level, go_Tick.bid)) {
    // der Abstand vom aktuellen Preis ist nicht ausreichend 
    if(Log_Level > LOG_LEVEL_ERR)
      PrintFormat("Platzieren von SellStop an %s nicht möglich. Bid: %s Ask: %s StopLevel: %s",
        DoubleToString(d_Entry_Level, _Digits),
        DoubleToString(go_Tick.bid, _Digits),
        DoubleToString(go_Tick.ask, _Digits),
        DoubleToString(gd_Stop_Level, _Digits)
      );
    
    return; // warten auf die Änderung des aktuellen Preises 
  }
}

Wenn der berechnete Level zum Platzieren der Pending Order erfolgreich überprüft wurde, kann die notwendige Order an den Server mithilfe der Klasse der Standardbibliothek gesendet werden:

// bringen wir den Lot mit den Anforderungen des Servers in Einklang:
double d_Volume = fd_Normalize_Lot(Trade_Volume);

// setzen wir eine Pending Order:
i_Try = gi_Try_To_Trade;

if(e_Signal == ENTRY_BUY) {
  while(i_Try-- > 0) { // Versuche, BuyStop zu setzen
    if(o_Trade.BuyStop(
      d_Volume,
      d_Entry_Level,
      _Symbol
    )) { // erfolgreich
      Alert("Buy Pending Order platziert!");
      i_Try = -10; // Flag einer erfolgreichen Operation
      break;
    }
    // fehlgeschlagen
    Sleep(gi_Connect_Wait); // Pause vor dem nächsten Versuch
  }
} else {
  while(i_Try-- > 0) { // Versuche, SellStop zu setzen
    if(o_Trade.SellStop(
      d_Volume,
      d_Entry_Level,
      _Symbol
    )) { // erfolgreich
      Alert("Sell Pending Order platziert!");
      i_Try = -10; // Flag einer erfolgreichen Operation
      break;
    }
    // fehlgeschlagen
    Sleep(gi_Connect_Wait); // Pause vor dem nächsten Versuch
  }
}

if(i_Try == WRONG_VALUE) // Setzen einer Pending Order fehlgeschlagen
  if(Log_Level > LOG_LEVEL_NONE) Print("Fehler beim Setzen der Pending Order");

Damit ist das Programmieren des Expert Advisors abgeschlossen, nach der Kompilierung analysieren wir ihn im Strategietester.

 

Testen der Strategie anhand historischer Daten


In ihrem Buch veranschaulichen L. Connors und L. Raschke die Strategie anhand Charts, die über 20 Jahre alt sind, deshalb war das Ziel des Testens, die Leistungsfähigkeit der Strategie anhand aktueller Daten zu überprüfen. Es wurden die von den Autoren angegebenen Parameter und D1-Zeitrahmen verwendet. Vor 20 Jahren waren fünfstellige Kurse nicht verbreitet, und das Testen wurde aber gerade anhand fünfstelliger Kurse des MetaQuotes Demoservers dürchgeführt, deswegen wurden die ursprünglichen 1 und 10 Punkte in 10 und 100 umgerechnet. In der Beschreibung der Strategie werden keine Trailing Parameter erwähnt, deswegen habe ich diejenigen verwendet, die am besten zum Tageszeitrahmen passen.

Die Testergebnisse der Strategie Turtle Soup auf USDJPY für die letzten fünf Jahre:

Turtle Soup, USDJPY, D1, 5 Jahre


Die Testergebnisse der Strategie Turtle Soup Plus One mit den gleichen Parametern und auf dem gleichen Abschnitt der Historie des gleichen Symbols:

Turtle Soup Plus One, USDJPY, D1, 5 Jahre


Testergebnisse Goldkurse für die letzten fünf Jahre. Die Strategie Turtle Soup:

Turtle Soup, XAUUSD, D1, 5 Jahre


Turtle Soup Plus One:

Turtle Soup Plus One, XAUUSD, D1, 5 Jahre

 


Die Testergebnisse anhand Ölpreise (crude oil) für die letzten vier Jahre. Die Strategie Turtle Soup:

Turtle Soup, OIL, D1, 4 Jahre


Turtle Soup Plus One:

Turtle Soup Plus One, OIL, D1, 4 Jahre


Die vollständigen Berichte zu allen Tests sind in angehängten Dateien zu finden.

Sie können selbst Schlussfolgerungen ziehen, aber ich muss noch einiges erläutern. L. Connors und L. Raschke warnen davor, den Regeln jeglicher im Buch beschriebenen Strategie blind zu folgen. Ihres Erachtens ist es erforderlich zu analysieren, wie sich der Preis den Grenzen des Kanals nähert und wie er sich nach dem Testen verhält. Leider gehen sie darauf nicht ausführlicher ein. Was die Optimierung betrifft, kann man versuchen, die Parameter auf andere Zeitrahmen abzustimmen, um bessere Symbole und Parameter auszuwählen.

Fazit

Wir haben die Regeln der ersten im Buch Street Smarts: High Probability Short-Term Trading Strategies beschriebenen Handelsstrategien Turtle Soup und Turtle Soup Plus One formuliert und programmiert. Der Expert Advisor und die Signalbibliothek beinhalten alle von L. Raschke und L. Connors beschriebenen Regeln. Allerdings sind einige wichtige Einzelheiten des Tradings der Autoren ausgelassen, welche nur flüchtig erwähnt wurden. Mindestens müssen Gaps und Grenzen der Handelszeiten berücksichtigt werden. Darüber hinaus scheint es logisch, den Handel auf einen Einstieg pro Tag oder einen erfolgreichen Einstieg pro Tag zu beschränken und eine Pending Order länger als bis zum Anfang des nächsten Tages zu halten. Das können Sie tun, wenn Sie den hier beschriebenen Expert Advisor vervollkommnen wollen.