Universeller Expert Advisor: Benutzerstrategien und Hilfsklassen (Teil 3)

Vasiliy Sokolov | 5 Mai, 2016

Inhalt

 

Einleitung

In diesem Teil setzen wir unsere Diskussion über die Klasse CStrategy Trading-Engine fort. Hier ist ein kurzer Überblick über den Inhalt der vorherigen Teile. In dem ersten Artikel Universeller Expert Advisor: Handelsmodi von Strategien (Teil 1), haben wir im Detail die Handelsmodi besprochen, welche eine Konfiguration der Logik des Expert Advisors im Zusammenhang mit der Tageszeit und den Wochentagen erlaubt. In dem zweiten Artikel Universeller Expert Advisor: Das Event-Modell und ein Prototyp einer Handelsstrategie, haben wir das Event-Modell, basierend auf einer zentralisierten Event-Handling-Struktur analysiert, sowie den Hauptalgorithmus der Basisklasse CStrategy, die dem benutzerdefinierten Expert Advisor unterliegt.

In dem dritten Teil dieser Serie werden wir im Detail einige Beispiele von Expert-Advisors beschreiben, die auf der Klasse CStrategy-Trading-Engine und ein paar weiterer Algorithmen basieren. Ein besonderes Augenmerk wird auf das Protokollieren gerichtet. Trotz der rein unterstützende Rolle, ist die Protokollierung ein sehr wichtiges Element dieses komplexen Systems. Eine gute Protokollierung gibt Ihnen die Möglichkeit ganz schnell die Ursache von Problemen zu finden und sie auch zu verstehen. Die Protokollierung wird mit einer speziellen Programmierungstechnik programmiert, die sich Singleton pattern nennt. Die Information hierüber ist nicht nur für diejenigen interessant, die Handelsprozesse organisieren wollen, sondern auch für diejenigen, die Algorithmen für Nicht-Standardaufgaben entwickeln wollen.

Zudem beschreibt der Artikel die Algorithmen, mit denen Sie komfortabel auf Marktdaten und Indexe zugreifen können In der Tat ist der Datenzugriff über Indexe wie Close[1] und High[0] eine sehr häufig verwendete Eigenschaft von MetaTrader 4. Also warum sollte man es umgehen, wenn es genauso gut in MetaTrader 5 benutzt werden kann. Dieser Artikel beschreibt wie dieses gehandhabt wird und den Algorithmus, der die oben genannte Idee implementiert.

Um zum Abschluss zu kommen, möchte ich gerne die Wörter aus meinem vorherigen Artikel verwenden. Die CStrategy Trading Engine mit all ihren Algorithmen ist ein sehr komplexes Set. Aber wie schon erwähnt, ist es nicht notwendig alle Operationen und Funktionen dieser Klasse im Detail zu verstehen. Sie müssen nur das Grundprinzip und die Funktionalität der Trading Engine verstehen. Daher können Sie auch ruhig die Teile dieses Artikels überspringen, die Sie im Augenblick nicht verstehen. Dieses ist ein fundamentales Prinzip der objektorientierten Programmierung: Sie können ein komplexes System verwenden, ohne dabei die innere Struktur im Detail zu kennen.

 

Logs, CMessage und CLog Klassen, Singleton Pattern

Die Protokollierung ist eines der traditionellen Hilfsaufgaben. Normalerweise benutzen einfache Anwendungen die gängige Print oder printf Funktion, welche Fehlermeldungen in das Terminal-Fenster vom MetaTrader 5 schreibt:

...
double closes[];
if(CopyClose(Symbol(), Period(), 0, 100, closes) < 100)
   printf("Nicht genug Daten.");
...

Doch dieser einfache Ansatz ist nicht immer ausreichend, um zu verstehen, was in großen, komplexen Programmen geschieht, die hunderte von Zeilen Quellcode enthalten. Daher ist die beste Lösung für diese Aufgaben ein spezielles Erfassungsmodul zu entwickeln - die CLog Klasse.

Die naheliegendste Methode des Loggers ist AddMessage (). Wenn zum Beispiel Log ein Objekt von CLog ist, dann können wir die folgende Anweisung schreiben:

Log.AddMessage("Warnung! Die Anzahl der empfangenen Bars ist kleiner als die Anzahl die benötigt wird");

Aber für eine erfolgreiche Fehlersuche gibt uns diese Meldung noch viel zu wenig Informationen. Wie können Sie dieser Nachricht entnehmen wann sie erzeugt wurde? Welche Funktion hat diese Meldung hervorgerufen? Woher sollen sie wissen, welche wichtige Informationen in ihr enthalten ist? Um dieses zu vermeiden, müssen wir den Umfang der Nachricht erweitern. Zuzüglich zu dem Text sollte jede Nachricht noch folgende weitere Informationen besitzen:

  • Zeitpunkt der Erzeugung
  • Die Nachrichtenquelle
  • Der Typ der Nachricht (Information, Warnung, Fehlermeldung)

Zudem wären noch folgende Details hilfreich:

  • Die ID einer Systemfehlermeldung
  • Die ID der Fehlermeldung über ein Transaktionsproblem (Wenn eine Aktion für das Handeln durchgeführt wurde)
  • Die Zeit des Trade-Servers in dem Moment, wo die Nachricht erzeugt wurde.

Alle diese Informationen können einfach in einer speziellen CMessage Klasse zusammengefasst werden. Da unsere Nachricht eine Klasse ist, können Sie ganz einfach weitere Daten und Methoden hinzufügen. Hier ist der Header der Klasse:

//+----------------------------------------------------------------
//|                                                         Logs.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+----------------------------------------------------------------
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Object.mqh>
#include <Arrays\ArrayObj.mqh>

#define UNKNOW_SOURCE "unknown"     // Eine unbekannte Nachrichtenquelle
//+----------------------------------------------------------------
//| Der Typ der Nachricht                                                   |
//+----------------------------------------------------------------
enum ENUM_MESSAGE_TYPE
  {
   MESSAGE_INFO,                    // Informative Nachricht
   MESSAGE_WARNING,                 // Eine Warnung
   MESSAGE_ERROR                    // Eine Fehlermeldung
  };
//+----------------------------------------------------------------
//| Eine Nachricht wird der "logger" Klasse übergeben                            |
//+----------------------------------------------------------------
class CMessage : public CObject
  {
private:
   ENUM_MESSAGE_TYPE m_type;               // Nachrichtentyp
   string            m_source;             // Nachrichtenquelle
   string            m_text;               // Textteil der nachricht
   int               m_system_error_id;    // Erzeugt eine ID eines Systemfehlers
   int               m_retcode;            // Gibt den Fehlercode vom Handelsserver zurück
   datetime          m_server_time;        // Die Zeit des Handelsservers zu dem Zeitpunkt wo die Nachricht erzeugt wurde
   datetime          m_local_time;         // Die lokale Zeit in dem Moment wo die Nachricht erzeugt wurde
   void              Init(ENUM_MESSAGE_TYPE type,string source,string text);
public:
                     CMessage(void);
                     CMessage(ENUM_MESSAGE_TYPE type);
                     CMessage(ENUM_MESSAGE_TYPE type,string source,string text);
   void              Type(ENUM_MESSAGE_TYPE type);
   ENUM_MESSAGE_TYPE Type(void);
   void              Source(string source);
   string            Source(void);
   void              Text(string text);
   string            Text(void);
   datetime          TimeServer(void);
   datetime          TimeLocal();
   void              SystemErrorID(int error);
   int               SystemErrorID();
   void              Retcode(int retcode);
   int               Retcode(void);
   string            ToConsoleType(void);
   string            ToCSVType(void);
  };

Zunächst enthält der Header ENUM_MESSAGE_TYPE. Dieses definiert den Typ der Nachricht die erzeugt werden soll. Die Nachricht kann einen reinen Informationscharakter haben (MESSAGE_INFO), sie kann eine Warnung sein (MESSAGE_WARNING) oder über einen Fehler benachrichtigen (MESSAGE_ERROR).

Die Klasse verfügt über verschiedene Get und Set Methoden, welche es erlauben, die verschiedenen Eigenschaften einer Nachricht zu lesen oder zu setzen. Um eine Nachricht ganz einfach in einer Zeile erstellen zu können, bietet die Klasse CMessage einen entsprechenden überladenen Konstruktor, welcher direkt mit den notwendigen Parametern aufgerufen werden kann. Wenn wir also zum Beispiel eine Warnung in dem OnTick event erzeugen wollen, welche uns darüber informieren soll, dass zu wenige Daten geladen worden sind, kann dieses auf die folgende Art und Weise ausgeführt werden:

void OnTick(void)
  {
   double closes[];
   if(CopyClose(Symbol(),Period(),0,100,closes)<100)
      CMessage message=new CMessage(MESSAGE_WARNING,__FUNCTION__,"Nicht genügend Daten");
  }

Diese Nachricht enthält nun schon mehr Informationen als unsere Vorherige. Zu der Information selbst beinhaltet sie noch den Namen der Funktion die diese Nachricht erzeugt hat und den Typ der Nachricht. Zudem verfügt unsere Nachricht auch noch über Daten, die Sie bei der Erzeugung nicht angeben müssen. Zum Beispiel enthält das message Object die Information über den Zeitpunkt der Erzeugung der Meldung und falls es eine Fehlermeldung gibt auch den dazugehörigen Fehlercode.

Nun wird es Zeit, dass wir uns den CLog logger anschauen. Diese Klasse bietet einen Speicher für CMessage messages. Eine der interessantesten Funktionen ist das Senden von Push-Nachrichten zu mobilen Endgeräten unter der Verwendung der Funktion SendNotification. Dieses ist eine sehr nützliche Eigenschaft, wenn es ihnen zum Beispiel selbst nicht möglich ist, einen Expert Advisor rund um die Uhr zu überwachen. Somit können wir uns in einem Fehlerfall automatisch eine Nachricht zusenden lassen.

Die Besonderheit der Protokollierung ist, dass es ein einziges Verfahren für alle Teile des Programms sein muss. Es wäre sehr seltsam wenn jede Funktion oder Klasse ihre eigene Art der Protokollierung durchführen würde. Aus diesem Grund wird die CLog Klasse mit dem speziellen Singleton-Verfahren implementiert . Dieses stellt sicher, dass es nur eine Kopie von einem Objekt eines bestimmten Typs gibt. Wenn es zum Beispiel ein Programm mit zwei Pointern gibt, dann verweist jeder dieser Pointer auf das gleiche CLog Objekt. Ein Objekt wird durch private Methoden im Hintergrund der Klasse erzeugt und gelöscht.

Schauen wir uns den Titel der Klasse und die Methoden, die das Singleton-Verfahren implementieren, an:

//+----------------------------------------------------------------
//|                                                         Logs.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+----------------------------------------------------------------
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Object.mqh>
#include <Arrays\ArrayObj.mqh>
#include "Message.mqh"

//+----------------------------------------------------------------
//| Diese Klasse implementiert Protokollierung mit dem Singleton-Verfahren            |
//+----------------------------------------------------------------
class CLog
{
private:
   static CLog*      m_log;                     // Anzeiger zu dem globalen statischen Beispiel
   CArrayObj         m_messages;                // Die Liste der gespeicherten Nachrichten
   bool              m_terminal_enable;         // True, Wenn Sie eine Ausgabe der Nachrichten im Terminal-Fenster wünschen
   bool              m_push_enable;             // Wenn true, dann werden Push-Nachrichten gesendet
   ENUM_MESSAGE_TYPE m_push_priority;           // Enthält die angegebene Priorität der Nachrichtenanzeige im Terminalfenster
   ENUM_MESSAGE_TYPE m_terminal_priority;       // Enthält die angegebene Priorität für Push-Nachrichten auf mobile Geräte
   bool              m_recursive;               // Ein Flag, welcher auf den rekursiven Aufruf des Destruktors hinweist
   bool              SendPush(CMessage* msg);
   void              CheckMessage(CMessage* msg);
                     CLog(void);                // Privater Konstruktor
   string            GetName(void);
   void              DeleteOldLogs(int day_history = 30);
   void              DeleteOldLog(string file_name, int day_history);
                     ~CLog(void){;}
public:
   static CLog*      GetLog(void);              // Die Methode um ein statisches Objekt zu erhalten
   bool              AddMessage(CMessage* msg);
   void              Clear(void);
   bool              Save(string path);
   CMessage*         MessageAt(int index)const;
   int               Total(void);
   void              TerminalEnable(bool enable);
   bool              TerminalEnable(void);
   void              PushEnable(bool enable);
   bool              PushEnable(void);
   void              PushPriority(ENUM_MESSAGE_TYPE type);
   ENUM_MESSAGE_TYPE PushPriority(void);
   void              TerminalPriority(ENUM_MESSAGE_TYPE type);
   ENUM_MESSAGE_TYPE TerminalPriority(void);
   bool              SaveToFile(void);
   static bool       DeleteLog(void);
};
CLog* CLog::m_log;

Die CLog Klasse Speichert einen Zeiger zu dem statischen Objekt von sich selbst als ein "private member". Dieses mag seltsam aussehen, aber es macht durchaus Sinn. Der einzige Konstruktor ist privat und kann nicht aufgerufen werden. Anstelle des Konstruktors kann die GetLog Methode verwendet werden:

//+----------------------------------------------------------------
//| Gibt das logger Objekt zurück                                        |
//+----------------------------------------------------------------
static CLog* CLog::GetLog()
{
   if(CheckPointer(m_log) == POINTER_INVALID)
      m_log = new CLog();
   return m_log;
}

Es wird geprüft, ob der statische Pointer auf ein existierendes CLog-Objekt zeigt. Falls es so ist, dann wird eine Referenz zu diesem Objekt zurückgegeben. Andernfalls wird ein neues Objekt erzeugt und mit dem internen Pointer m_log in Verbindung gebracht. Das bedeutet, dass dieses Objekt nur einmal erzeugt wird. Bei jedem weiteren Aufruf der GetLog-Methode, wird ein vorher erzeugtes Objekt zurückgegeben.

Auch die Löschung dieses Objektes wird nur einmal durchgeführt. Dieses wird mit der DeleteLog Methode durchgeführt:

//+----------------------------------------------------------------
//| Entfernt das logger Objekt                                       |
//+----------------------------------------------------------------
bool CLog::DeleteLog(void)
{
   bool res = CheckPointer(m_log) != POINTER_INVALID;
   if(res)
      delete m_log;
   return res;
}

Wenn das m_log existiert, dann wird es gelöscht und true wird zurückgegeben.

Es mag so aussehen, dass das hier beschriebene System für die Protokollierung sehr komplex ist, aber die unterstützenden Eigenschaften sind wirklich beeindruckend. Sie können zum Beispiel Nachrichten über Prioritäten sortieren oder sie als Push Nachrichten zusenden. Aber letztendlich müssen Sie selbst entscheiden, ob sie dieses System verwenden wollen. Es ist in den separaten Modulen Message.mqh und Logs.mqh implementiert. Somit können Sie es separat von dem beschriebene Projekt verwenden oder auch innerhalb des Projektes.

 

Zugriff auf Kurse über Indexe von MetaTrader 4

Eine der wichtigsten Änderungen im MetaTrader 5, im Gegensatz zu seinem Vorgänger, ist ein Modell für den Zugriff auf Kurse und Indikator-Daten. Wenn Sie zum Beispiel in MetaTrader 4 den Schlusskurs der aktuellen Bar herausfinden wollen, können Sie das mit der folgenden Prozedur machen:

double close = Close[0];

Das bedeutet, dass Sie direkten Zugriff auf die Daten über eine Zeitserie haben. In Metatrader 5 wird ein wenig mehr Arbeit benötigt, um den Schlusskurs der aktuellen Bar herauszufinden:

  1. Definieren Sie zunächst einen Array, in welches die gewünschten Kurse hineinkopiert werden sollen.
  2. Kopieren Sie die gewünschten Kurse mit einer der Funktionen aus der Gruppe Copy* (Funktionen für den Zugriff auf Zeitserien und Indikator-Daten).
  3. Anschließend greifen Sie mit dem Index des Arrays auf die Daten zu.

In MetaTrader 5 sind somit folgende Aktionen notwendig, um den Kurs herauszufinden:

double closes[];
double close = 0.0;
if(CopyClose(Symbol(), Period(), 0, 1, closes))
   close = closes[0];
else
   printf("Fehler beim Kopieren des Schlusskurses.");

Dieser Datenzugriff ist ein wenig komplizierter als in MetaTrader 4. Aber diese Art des Datenzugriffs macht ihn universell: Die gleiche einheitliche Schnittstelle und Mechanismen werden für den Zugriff auf Daten von unterschiedlichen Symbolen und Indikatoren verwendet.

Aber diese Art des Zugriffs wird nicht tagtäglich gebraucht. Sehr oft benötigen wir lediglich den letzten Wert des aktuellen Symbols. Dieses kann der Eröffnungs- oder der Schlusskurs sein, oder auch der Hoch- oder Tiefkurs einer Bar. Auf jeden Fall wäre es zweckmäßig das Datenzugriffs-Modell aus MetaTrader 4 zu verwenden. Durch die objektorientierte Struktur in MetaTrader 5, wäre es aber auch möglich eine spezielle Klasse zu entwickeln, mit deren Hilfe man ebenfalls über einen Index auf die Handelsdaten zugreifen kann, so wie es im MetaTrader 4 der Fall ist. Um zum Beispiel auf diese Weise den Schlusskurs im MetaTrader 5 zu bekommen:

double close = Close[0];

Können wir den folgenden Wrapper für Schlusskurse schreiben:

//+----------------------------------------------------------------
//| Zugriff auf den Schlusskurs einer Bar.                        |
//+----------------------------------------------------------------
class CClose : public CSeries
  {
public:
   double operator[](int index)
     {
      double value[];
      if(CopyClose(m_symbol, m_timeframe, index, 1, value) == 0)return 0.0;
      return value[0];
     }
  };

Der selbe Programmcode müsste dann auch für die anderen Serien wie Volumen, Open, High und Low geschrieben werden. Natürlich kann diese Art des Datenzugriffs in einigen Fällen langsamer sein als die einmalige Kopie des benötigten Arrays mithilfe der Copy* System-Funktionen. Wie jedoch oben erwähnt, müssen wir oft nur auf das letzte Element zugreifen da alle vorherigen Elemente nur in sich aktualisierenden Fenstern von Interesse sind.

Dieses einfache Set von Klassen ist in der Datei Series.mqh zusammengefasst. Es stellt eine komfortable Schnittstelle für den Zugriff auf Kurse, wie es auch im MetaTrader 4 geschieht, zur Verfügung. Diese Schnittstelle wird auch in unserer Trading Engine verwendet.

Eine Besonderheit dieser Klassen ist ihre Plattformunabhängigkeit . Zum Beispiel kann ein Expert Advisor in MetaTrader 5 eine Methode dieser Klasse aufrufen wobei er "glaubt", dass er einen direkten Zugriff auf die Kurse hat. Diese Zugriffsmethode funktioniert auch in MetaTrader 4, aber anstelle des spezialisierten Wrappers wird hier direkt auf die Datenserien wie Open, High, Low oder Close zugegriffen.

 

Verwendung von objektorientierten Indikatoren

Fast jeder Indikator hat eine bestimmte Anzahl von Einstellungen für seine Konfiguration. Arbeiten mit Indikatoren in MetaTrader 5 ist ähnlich der Arbeit mit Kursen, mit dem einzigen Unterschied, dass die Notwendigkeit besteht, ein sogenanntes Indikator-Handle , d.h. einen speziellen Zeiger auf ein internes MetaTrader 5-Objekt, welches Berechnungswerte enthält, zu erstellen, bevor die Indikatordaten kopiert werden können . Die Parameter des Indikators werden zu dem Zeitpunkt der Erzeugung des Handles festgelegt. Wenn Sie die Indikatorparameter editieren wollen, müssen sie das alte Handle löschen und ein neues Handle mit aktualisierten Parametern erzeugen. Die Indikatorparameter sollten an einer externen Stelle gespeichert werden. Zum Beispiel in den Variablen des Expert Advisors.

Letztendlich werden die meisten Operationen mit Indikatoren über den Expert Advisor abgewickelt. Dieses ist nicht immer praktisch. Lassen Sie uns dieses an einem Beispiel veranschaulichen: Ein Expert Advisor handelt mit Hilfe der Signale von sich schneidenden gleitenden Durchschnitten. Aufgrund seiner einfachen Struktur hat der Indikator für die gleitenden Durchschnitte nur sechs Parameter, welche gesetzt werden müssen:

  1. Das Symbol für die Berechnung.
  2. Die Timeframe/Periode des Charts.
  3. Die Periode für die Mittelung
  4. Der Typ des gleitenden Durchschnitts (einfach, exponentiell, gewichtet, etc.)
  5. Die Verschiebung des Indikators
  6. Der verwendete Kurs-Typ(einer von den OHLC Kursen einer Bar oder ein Speicher eines anderen Indikators)

Wenn wir also ein Expert Advisor schreiben möchten, der mit dem Schneiden von zwei gleitenden Durchschnitten Transaktionen durchführt und eine vollständige Parameterliste der gleitenden Durchschnitte verwenden wollen, benötigen wir 12 Parameter. 6 Parameter für den schnellen gleitenden Durchschnitt und sechs weitere Parameter für den Langsamen. Außerdem müssen die Handles der Indikatoren neu initialisiert werden, sobald der Anwender die Timeframe oder das Symbol wechselt, auf dem der Expert Advisor gerade läuft.

Um den Expert Advisor von diesen Aufgaben befreien zu können, sollten wir objektorientierte Versionen der Indikatoren verwenden. Mit der Verwendung von objektorientierten Indikator-Klassen, ist es uns möglich folgende Konstrukte zu schreiben:

CMovingAverageExp MAExpert;     // Die Erzeugung eines Expert-Advisors, welcher mit dem Signal von zwei gleitenden Durchschnitten handelt.
//+----------------------------------------------------------------
//| Expert initialization Function                                   |
//+----------------------------------------------------------------
int OnInit()
  {
//--- Die Konfiguration des schnellen gleitenden Durchschnitts des Expert-Advisors
   MAExpert.FastMA.Symbol("EURUSD");
   MAExpert.FastMA.Symbol(PERIOD_M10);
   MAExpert.FastMA.Period(13);
   MAExpert.FastMA.AppliedPrice(PRICE_CLOSE);
   MAExpert.FastMA.MaShift(1);
//--- Die Konfiguration des langsamen gleitenden Durchschnitts des Expert-Advisors
   MAExpert.SlowMA.Symbol("EURUSD");
   MAExpert.SlowMA.Symbol(PERIOD_M15);
   MAExpert.SlowMA.Period(15);
   MAExpert.SlowMA.AppliedPrice(PRICE_CLOSE);
   MAExpert.SlowMA.MaShift(1);

   return(INIT_SUCCEEDED);
  }

Der Anwender braucht nun nur noch die Parameter zu setzen die von dem Expert Advisor verwendet werden. Der Expert Advisor wird nur diese Daten verwenden.

Es gibt noch einen weiteren großen Vorteil,der für die Verwendung von Indikator-Objekten spricht. Objektorientierte Indikatoren verstecken ihre Implementation. Das bedeutet, dass sie ihre Werte eigenständig durch den Aufruf der zugehörigen Handles berechnen. In den Fällen, wo mehrere Indikatoren berechnet werden müssen und eine hohe Geschwindigkeit benötigt wird, ist es ratsam, die Berechnung direkt in dem Expert-Advisor einzufügen. Dank der Eigenschaften von objektorientierter Programmierung, kann dieses ganz einfach geschehen ohne dass Programmcode neu geschrieben werden muss. Sie müssen lediglich die Indikatorwerte innerhalb der zugehörigen Klasse ohne die Verwendung von Handles berechnen.

Um dieses zu verdeutlichen ist hier der Programmcode von der CIndMovingAverage Klasse, welche auf dem iMA Indicator beruht:

//+----------------------------------------------------------------
//|                                                MovingAverage.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+----------------------------------------------------------------
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Strategy\Message.mqh>
#include <Strategy\Logs.mqh>
//+----------------------------------------------------------------
//| defines                                                          |
//+----------------------------------------------------------------
class CIndMovingAverage
  {
private:
   int               m_ma_handle;         // Indicator handle
   ENUM_TIMEFRAMES   m_timeframe;         // Timeframe
   int               m_ma_period;         // Period
   int               m_ma_shift;          // Shift
   string            m_symbol;            // Symbol
   ENUM_MA_METHOD    m_ma_method;         // Moving Average method
   uint              m_applied_price;     // Der Handle des Indikators, zu welchen Sie die Berechnung des gleitenden Durchschnitts durchführen,
                                          // Oder einer der Kurswerte von ENUM_APPLIED_PRICE
   CLog*             m_log;               // Logging
   void              Init(void);
public:
                     CIndMovingAverage(void);

/*Params*/
   void              Timeframe(ENUM_TIMEFRAMES timeframe);
   void              MaPeriod(int ma_period);
   void              MaShift(int ma_shift);
   void              MaMethod(ENUM_MA_METHOD method);
   void              AppliedPrice(int source);
   void              Symbol(string symbol);

   ENUM_TIMEFRAMES   Timeframe(void);
   int               MaPeriod(void);
   int               MaShift(void);
   ENUM_MA_METHOD    MaMethod(void);
   uint              AppliedPrice(void);
   string            Symbol(void);

/*Out values*/
   double            OutValue(int index);
  };
//+----------------------------------------------------------------
//| Default constructor.                                             |
//+----------------------------------------------------------------
CIndMovingAverage::CIndMovingAverage(void) : m_ma_handle(INVALID_HANDLE),
                                             m_timeframe(PERIOD_CURRENT),
                                             m_ma_period(12),
                                             m_ma_shift(0),
                                             m_ma_method(MODE_SMA),
                                             m_applied_price(PRICE_CLOSE)
  {
   m_log=CLog::GetLog();
  }
//+----------------------------------------------------------------
//| Initialization.                                                  |
//+----------------------------------------------------------------
CIndMovingAverage::Init(void)
  {
   if(m_ma_handle!=INVALID_HANDLE)
     {
      bool res=IndicatorRelease(m_ma_handle);
      if(!res)
        {
         string text="Realise iMA indicator failed. Error ID: "+(string)GetLastError();
         CMessage *msg=new CMessage(MESSAGE_WARNING,__FUNCTION__,text);
         m_log.AddMessage(msg);
        }
     }
   m_ma_handle=iMA(m_symbol,m_timeframe,m_ma_period,m_ma_shift,m_ma_method,m_applied_price);
   if(m_ma_handle==INVALID_HANDLE)
     {
      string params="(Period:"+(string)m_ma_period+", Shift: "+(string)m_ma_shift+
                    ", MA Method:"+EnumToString(m_ma_method)+")";
      string text="Create iMA indicator failed"+params+". Error ID: "+(string)GetLastError();
      CMessage *msg=new CMessage(MESSAGE_ERROR,__FUNCTION__,text);
      m_log.AddMessage(msg);
     }
  }
//+----------------------------------------------------------------
//| Setzen der Timeframe.                                           |
//+----------------------------------------------------------------
void CIndMovingAverage::Timeframe(ENUM_TIMEFRAMES tf)
  {
   m_timeframe=tf;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+----------------------------------------------------------------
//| Gibt die aktuelle Timeframe zurück.                                   |
//+----------------------------------------------------------------
ENUM_TIMEFRAMES CIndMovingAverage::Timeframe(void)
  {
   return m_timeframe;
  }
//+----------------------------------------------------------------
//| Setzt die Mittelungsperiode des gleitenden Durchschnitts.                        |
//+----------------------------------------------------------------
void CIndMovingAverage::MaPeriod(int ma_period)
  {
   m_ma_period=ma_period;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+----------------------------------------------------------------
//| Gibt die aktuelle Mitteilungsperiode des gleitenden Durchschnitts zurück.      |
//+----------------------------------------------------------------
int CIndMovingAverage::MaPeriod(void)
  {
   return m_ma_period;
  }
//+----------------------------------------------------------------
//| Legt den Typ des gleitenden Durchschnitts fest.                                    |
//+----------------------------------------------------------------
void CIndMovingAverage::MaMethod(ENUM_MA_METHOD method)
  {
   m_ma_method=method;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+----------------------------------------------------------------
//| Gibt den Typ des gleitenden Durchschnitts zurück.                                 |
//+----------------------------------------------------------------
ENUM_MA_METHOD CIndMovingAverage::MaMethod(void)
  {
   return m_ma_method;
  }
//+----------------------------------------------------------------
//| Gibt die Verschiebung des gleitenden Durchschnitts zurück.                                |
//+----------------------------------------------------------------
int CIndMovingAverage::MaShift(void)
  {
   return m_ma_shift;
  }
//+----------------------------------------------------------------
//| Legt die Verschiebung des gleitenden Durchschnitts fest.                                   |
//+----------------------------------------------------------------
void CIndMovingAverage::MaShift(int ma_shift)
  {
   m_ma_shift=ma_shift;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+----------------------------------------------------------------
//| Legt den Typ des Preises fest, welcher für die Berechnung verwendet wird.              |
//+----------------------------------------------------------------
void CIndMovingAverage::AppliedPrice(int price)
  {
   m_applied_price = price;
   if(m_ma_handle != INVALID_HANDLE)
      Init();
  }
//+----------------------------------------------------------------
//| Gibt den Typ des Preises zurück, welcher für die Berechnung verwendet wird.           |
//+----------------------------------------------------------------
uint CIndMovingAverage::AppliedPrice(void)
  {
   return m_applied_price;
  }
//+----------------------------------------------------------------
//| Leegt das Symbol für die Berechnung des Indikators fest                   |
//+----------------------------------------------------------------
void CIndMovingAverage::Symbol(string symbol)
  {
   m_symbol=symbol;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+----------------------------------------------------------------
//| Gibt das verwendete Symbol für die Berechnung des Indikators zurück               |
//+----------------------------------------------------------------
string CIndMovingAverage::Symbol(void)
  {
   return m_symbol;
  }
//+----------------------------------------------------------------
//| Gibt den Wert des MA mit dem Index 'index' zurück                   |
//+----------------------------------------------------------------
double CIndMovingAverage::OutValue(int index)
  {
   if(m_ma_handle==INVALID_HANDLE)
      Init();
   double values[];
   if(CopyBuffer(m_ma_handle,0,index,1,values))
      return values[0];
   return EMPTY_VALUE;
  }

Diese Klasse ist sehr einfach. Ihre Hauptaufgabe ist es, den Indikator zur reinitialisieren, sobald einer seiner Parameter verändert wurde, sowie die Rückgabe des berechneten Wertes über den index. Ein Handle wird mit der Init-Methode reinitialisiert, und der gewünschte Wert wird mithilfe der OutValue-Funktion zurückgegeben. Methoden die einen Wert eines Indikators zurückgeben starten mit dem Out Prefix. Dieses erleichtert die Suche nach der benötigten Methode, da der MetaEditor bei der Eingabe sofort entsprechende Vorschläge anbietet.

Das komplette Paket der Trading-Engine beinhaltet eine Vielzahl von objektorientierten Indikatoren. Dieses hilft Ihnen dabei, die Arbeitsweise zu verstehen und auch die Entwicklung von neuen objektorientierten Versionen der klassischen Indikatoren zu beschleunigen. Der Abschnitt über die Entwicklung von benutzerdefinierten Expert-Advisors zeigt die Grundprinzipien auf, wie mit ihnen umgegangen werden muss.

 

Methoden, die von dem benutzerdefinierten Expert Advisor überschrieben werden

In dem ersten Artikel Universeller Expert Advisor: Handelsmodi von Strategien (Teil 1), haben wir uns im Detail die Trading Modis von Strategien und deren Hauptmethoden angeschaut, welche überschrieben werden müssen. Nun wird es Zeit sich ein reales Beispiel anzuschauen.

Jeder Expert Advisor, welcher unter der Verwendung der Klasse CStrategy Engine erzeugt worden ist, muss einige virtuelle Methoden überschreiben, die für die Eigenschaften und das Verhalten des Expert-Advisors verantwortlich sind. Wir zeigen jetzt alle Methoden, die überschrieben werden müssen, in einer Tabelle mit drei Spalten auf. Die erste Spalte beinhaltet den Namen der virtuellen Methode, die zweite Spalte zeigt das Event oder die Aktion, die verfolgt oder ausgeführt wird. Die dritte Spalte beinhaltet eine Beschreibung über den Gebrauch dieser Methode. Hier ist die Tabelle:

Virtuelle Methode Event/Aktion Verwendung
OnSymbolChanged Wird aufgerufen, wenn sich der Name des Symbols ändert Wenn Sie das Finanzinstrument, mit welchem sie gerade Handeln, ändern, dann sollte der Indikator des Expert Advisors reinitialisiert werden. Dieses Event erlaubt die Durchführung einer Reinitialisierung von Indikatoren des Expert Advisors.
OnTimeframeChanged Der Wechsel der aktuellen Timeframe. Wenn Sie die Timeframe, mit der sie gerade arbeiten ändern, dann sollten die Indikatoren des Expert-Advisors Reinitialisiert werden. Dieses Event erlaubt die Durchführung einer Reinitialisierung von Indikatoren des Expert Advisors.
ParseXmlParams Das Übersetzen (Parsen) von benutzerdefinierten Parametern einer Strategie, welche über eine XML-Datei geladen wurden. Die Strategie sollte XML Parameter, die zu der entsprechende Methode übergeben werden, erkennen und entsprechend die Einstellungen konfigurieren.
ExpertNameFull Gibt den vollständigen Namen des Expert-Advisors zurück. Der vollständige Name des Expert-Advisors beinhaltet den Namen der Strategie und in der Regel ein eindeutiges Set von Parametern für diese Strategie. Eine Instanz einer Strategie muss den vollständigen Namen unabhänging bestimmen. Dieser Name wird auch auf dem Panel und in der Dropdown-Agentenliste verwendet.
OnTradeTransaction Tritt im Fall eines Handels-Events auf Einige Strategien analysieren und verwenden Handels-Events. Dieses Element erlaubt die Weitergabe eines Handels-Events an den Expert Advisor für weitere Analysezwecke.
InitBuy Initiiert eine Kauf Operation Eine der grundlegenden Methoden, welche überschrieben werden müssen. In dieser Methode sollten Sie Kauf-Operationen durchführen, sofern passende Handelskonditionen vorliegen
InitSell Initiiert eine Verkauf-Operation Eine der grundlegenden Methoden, welche überschrieben werden müssen. In dieser Methode sollten Sie Verkauf-Operationen durchführen, sofern passende Handelskonditionen vorliegen
SupportBuy Verwaltet eine zuvor eröffnete Long-Position Wenn eine offene Long-Position verwaltet werden muss. Wenn Sie zum Beispiel Stop-Loss neu setzen oder die Position aufgrund eines Signals schließen müssen. Alle diese Schritte müssen in dieser Methode durchgeführt werden.
SupportSell Verwaltet eine zuvor eröffnete Short-Position Wenn eine offene Short-Position verwaltet werden muss. Wenn Sie zum Beispiel Stop-Loss neu setzen oder die Position aufgrund eines Signals schließen müssen. Alle diese Schritte müssen in dieser Methode durchgeführt werden.

 Tabelle 1. Virtuelle Methoden und deren Verwendung

Die wichtigsten Methoden, welche überschrieben werden müssen sind: InitBuy, InitSell, SupportBuy, und SupportSell. Sie sind in der Tabelle fett dargestellt. Wenn Sie es vergessen, diese Funktionen zu überschreiben, zum Beispiel bei InitBuy, dann wird die benutzerdefinierte Strategie nicht kaufen. Oder wenn Sie zum Beispiel vergessen eine der Verwaltungsmethoden zu überschreiben, dann könnte eine offene Position für immer offen bleiben. Also achten Sie bei der Erstellung eines Expert Advisor sorgfältig darauf, diese Methoden zu überschreiben.

Wenn Sie die Strategie der Trading-Engine aus einer XML Datei laden wollen und die Parameter aus der Datei für die Konfiguration der Strategie verwendet werden sollen, dann müssen sie auch die ParseXmlParams Methode überschreiben. In dieser Methode sollte eine Strategie die Parameter, die an Sie übergeben wurde, bestimmen können und auch wissen, wie Sie diese Parameter auf sich selbst anwendet. Wie mit XML Parametern umgegangen wird, wird in dem vierten Teil dieser Serie genauer beschrieben: Universeller Expert Advisor: Handeln in einer Gruppe und die Verwaltung eines Portfolios von Strategien (Teil 4)". Ein Beispiel für das Überschreiben von ParseXmlParams ist in dem Listing für Strategien, basierend auf den Bollinger Bands, enthalten.

 

Ein Beispiel wie der Expert Advisor mit zwei gleitenden Durchschnitten handelt

Nun ist es an der Zeit unseren ersten eigenen Expert Advisor unter Verwendung der Möglichkeiten der CStrategy-Klasse zu entwickeln. Um den Quellcode möglichst einfach und kompakt zu halten, werden wir die Protokollierungs-Funktion nicht verwenden. Lassen Sie uns kurz die Aktionen beschreiben, die in unserem Expert Advisor ausgeführt werden müssen:

  • Wenn die Timeframe oder das Symbol geändert wird, müssen die Einstellung des schnellen und des langsamen gleitenden Durchschnitts durch das Überschreiben der Methoden OnSymbolChanged und OnTimeframeChanged geändert werden.
  • Überschreiben der Methoden InitBuy, InitSell, SupportBuy und SupportSell. Definieren der Handels-Logik in diesen Methoden (Das Öffnen von Positionen und die Regeln für die Verwaltung).

Der Rest der Arbeit sollte durch die Trading Engine und die Indikatoren übernommen werden. Hier ist der Quellcode des Expert-Advisors:

//+----------------------------------------------------------------
//|                                                      Samples.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+----------------------------------------------------------------
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Strategy\Strategy.mqh>
#include <Strategy\Indicators\MovingAverage.mqh>
//+----------------------------------------------------------------
//| Ein Beispiel für die klassische Strategie, welche auf zwei gleitenden Durchschnitten basiert. |
//| Wenn der schnelle MA den langsamen MA von oben nach unten schneidet             |
//| Dann kaufen wir, wenn er von oben nach unten geschnitten wird, dann verkaufen wir.                              |
//+----------------------------------------------------------------
class CMovingAverage : public CStrategy
  {
private:
   bool              IsTrackEvents(const MarketEvent &event);
protected:
   virtual void      InitBuy(const MarketEvent &event);
   virtual void      InitSell(const MarketEvent &event);
   virtual void      SupportBuy(const MarketEvent &event,CPosition *pos);
   virtual void      SupportSell(const MarketEvent &event,CPosition *pos);
   virtual void      OnSymbolChanged(string new_symbol);
   virtual void      OnTimeframeChanged(ENUM_TIMEFRAMES new_tf);
public:
   CIndMovingAverage FastMA;        // Fast moving average
   CIndMovingAverage SlowMA;        // Slow moving average
                     CMovingAverage(void);
   virtual string    ExpertNameFull(void);
  };
//+----------------------------------------------------------------
//| Initialization.                                                  |
//+----------------------------------------------------------------
CMovingAverage::CMovingAverage(void)
  {
  }
//+----------------------------------------------------------------
//| Reaktionen auf den Wechsel des Symbols                                     |
//+----------------------------------------------------------------
void CMovingAverage::OnSymbolChanged(string new_symbol)
  {
   FastMA.Symbol(new_symbol);
   SlowMA.Symbol(new_symbol);
  }
//+----------------------------------------------------------------
//| Reaktion auf den Wechsel der Timeframe                               |
//+----------------------------------------------------------------
void CMovingAverage::OnTimeframeChanged(ENUM_TIMEFRAMES new_tf)
  {
   FastMA.Timeframe(new_tf);
   SlowMA.Timeframe(new_tf);
  }
//+----------------------------------------------------------------
//| Wir kaufen, wenn der schnelle MA oberhalb des Langsamen ist.                   |
//+----------------------------------------------------------------
void CMovingAverage::InitBuy(const MarketEvent &event)
  {
   if(!IsTrackEvents(event))return;                      // Nur das benötigte Event wird bearbeitet!
   if(positions.open_buy > 0) return;                    // Wenn es schon eine offene Position gibt, dann kaufen wir keine weitere!
   if(FastMA.OutValue(1) > SlowMA.OutValue(1))           // Wenn es keine offenen Positionen gibt, dann wird geprüft ob der schnelle MA oberhalb des Langsamen ist:
      Trade.Buy(MM.GetLotFixed(), ExpertSymbol(), "");   // Falls oberhalb, dann kaufen.
  }
//+----------------------------------------------------------------
//| Schließen der Long-Position, wenn der schnelle Ma unterhalb des            |
//| langsamen ist.                                                        |
//+----------------------------------------------------------------
void CMovingAverage::SupportBuy(const MarketEvent &event,CPosition *pos)
  {
   if(!IsTrackEvents(event))return;                      // Nur das benötigte Event wird bearbeitet!
   if(FastMA.OutValue(1) < SlowMA.OutValue(1))           // Wenn der schnelle MA unterhalb des langsamen ist -   
      pos.CloseAtMarket("Verkauf bei Schneidung");           // Schließe die Position.
  }
//+----------------------------------------------------------------
//| Wir kaufen, wenn der schnelle MA oberhalb des Langsamen ist.                   |
//+----------------------------------------------------------------
void CMovingAverage::InitSell(const MarketEvent &event)
  {
   if(!IsTrackEvents(event))return;                      // Nur das benötigte Event wird bearbeitet!
   if(positions.open_sell > 0) return;                   // Wenn es schon eine Short-Position gibt dann kaufen wir keine Weitere!
   if(FastMA.OutValue(1) < SlowMA.OutValue(1))           // Wenn es keine offenen Kauf-Positionen gibt, prüfen, ob der schnelle MA oberhalb des Langsamen ist:
      Trade.Sell(1.0, ExpertSymbol(), "");               // Wenn oberhalb, dann kaufen wir.
  }
//+----------------------------------------------------------------
//| Schließen der Short-Position wenn der schnelle MA oberhalb des           |
//| Langsamen ist.                                                    |
//+----------------------------------------------------------------
void CMovingAverage::SupportSell(const MarketEvent &event,CPosition *pos)
  {
   if(!IsTrackEvents(event))return;                      // Nur das benötigte Event wird bearbeitet!
   if(FastMA.OutValue(1) > SlowMA.OutValue(1))           // Wenn der schnelle MA oberhalb des langsamen ist -
      pos.CloseAtMarket("Exit by cross under");          // Schließe die Position.
  }
//+----------------------------------------------------------------
//| Filter an der eingehenden Events. Wenn das eingehende Event durch die Strategie nicht              |
//| berarbeitet wurde, dann wird false zurückgegeben; Wenn es bearbeitet wurde |
//| wird true zurückgeben.                                                    |
//+----------------------------------------------------------------
bool CMovingAverage::IsTrackEvents(const MarketEvent &event)
  {
//--- Wir handeln nur beim Öffnen einer neuen Bar auf dem aktuellen Symbol und der aktuellen Timeframe
   if(event.type != MARKET_EVENT_BAR_OPEN)return false;
   if(event.period != Timeframe())return false;
   if(event.symbol != ExpertSymbol())return false;
   return true;
  }

Der oben dargestellte Quellcode ist einfach zu verstehen. Allerdings müssen wir einige Punkte klären: Die CStrategy Engine ruft die Methoden InitBuy, InitSell, SupportBuy und SuportSell (Handels-Logik Methoden) beim Auftreten von jedem Events auf, wie zum Beispiel bei der Änderung der Markttiefe, wenn ein neuer Tick eingeht, oder wenn der Timer sich ändert. Daher werden diese Methoden typischerweise sehr häufig aufgerufen. Aber ein Expert Advisor verwendet ein sehr stark verkleinertes Set von Events. Dieser verwendet nur das Event über die Generierung einer neuen Bar. Daher können alle anderen auftretenden Events ignoriert werden. Die IsTrackEvents Methode wird hierfür verwendet. Sie überprüft, ob der übergebene Event getracked/bearbeitet wurde, und falls es so ist, wird true zurückgegeben andernfalls false.

Die Positions-Struktur wird als externe Variable verwendet. Sie enthält die Anzahl der Long und Short-Positionen, welche zu der aktuellen Strategie gehören. Die CStrategy Engine berechnet die Statistiken. Daher muss die Strategie nicht durch alle offenen Positionen laufen um diese zu zählen. Die Logik des Expert-Advisors für das Öffnen von Positionen, wird aktuell reduziert auf die Überprüfung der folgenden Konditionen:

  1. Ein Handels-Event ist das Öffnen einer neuen Bar.
  2. Es existiert keine andere Position in dieselbe Richtung.
  3. Der schnelle gleitende Durchschnitt ist oberhalb (für Kauf) oder unterhalb (für Verkauf) des langsamen gleitenden Durchschnitts.

Die Bedingungen für das Schließen einer Position sind einfacher:

  1. Ein Handels-Event ist das Öffnen einer neuen Bar.
  2. Der schnelle gleitende Durchschnitt ist unterhalb (um eine Long-Position zu schließen) oder oberhalb (um eine Short-Position zu schließen) des langsamen gleitenden Durchschnitts.

In diesem Fall ist es nicht notwendig auf eine offene Position hin zu prüfen, weil der Aufruf der SupportBuy und SupportSell-Methoden mit der aktuellen Position als Parameter durchgeführt wird, welches dem Expert Advisor anzeigt, dass diese Position existiert und ihm übergeben wird.

Wenn man die Definition der Methoden und Klassen außer Acht lässt, kann die aktuelle Logik des Expert-Advisors mit 18 Zeilen Programmcode dargestellt werden. Dabei ist die Hälfte dieser Zeilen (Konditionen für Sell) ein Spiegelbild der anderen Hälfte (Konditionen für Buy). Eine solche Vereinfachung der Logik ist nur möglich, wenn man externe Bibliotheken wie die CStrategy-Klasse verwendet.

 

Ein Beispiel für einen Expert Advisor der eine Ausbruchstrategie mit den Bollinger-Bands-Kanälen handelt

Wir fahren nun mit der Erstellung von Strategien unter Verwendung der CStrategy Trading Engine fort. Mit dem zweiten Beispiel werden wir eine Strategie erzeugen, die eine Ausbruchs-Strategie aus den Bollinger Bands darstellt. Wir kaufen, wenn der aktuelle Preis oberhalb der Bollinger Bands ist. Und umgekehrt verkaufen wir, wenn der aktuelle Kurs unterhalb der Bollinger Bands ist. Wir schließen Long- und Short-Positionen, sobald der Preis die Mittellinie des Indikators erreicht hat.

In diesem Fall verwenden wir das Standard iBands Indicator-Handle. Damit zeigen wir, dass unser Trading-Modell das direkte Arbeiten mit Indikator-Handles ermöglicht, d.h. die Erstellung von speziellen objektorientierten Indikator-Klassen ist nicht notwendig. Aber wir müssen in diesem Fall zwei wichtige Parameter des Indikators angeben — die Mittelungsperiode und die Standardabweichung — direkt in dem Expert Advisor. Hier ist der Quellcode der Strategie:

//+----------------------------------------------------------------
//|                                                ChannelSample.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+----------------------------------------------------------------
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Strategy\Strategy.mqh>
//+----------------------------------------------------------------
//| defines                                                          |
//+----------------------------------------------------------------
class CChannel : public CStrategy
  {
private:
   int               m_handle;   // Das Handle des Indikators, welchen dir benutzen
   int               m_period;   // Bollinger period
   double            m_std_dev;  // Wert der Standard-Abweichung
   bool              IsTrackEvents(const MarketEvent &event);
protected:
   virtual void      OnSymbolChanged(string new_symbol);
   virtual void      OnTimeframeChanged(ENUM_TIMEFRAMES new_tf);
   virtual void      InitBuy(const MarketEvent &event);
   virtual void      SupportBuy(const MarketEvent &event,CPosition *pos);
   virtual void      InitSell(const MarketEvent &event);
   virtual void      SupportSell(const MarketEvent &event,CPosition *pos);
   virtual bool      ParseXmlParams(CXmlElement *params);
   virtual string    ExpertNameFull(void);
public:
                     CChannel(void);
                    ~CChannel(void);
   int               PeriodBands(void);
   void              PeriodBands(int period);
   double            StdDev(void);
   void              StdDev(double std);
  };
//+----------------------------------------------------------------
//| Default constructor                                              |
//+----------------------------------------------------------------
CChannel::CChannel(void) : m_handle(INVALID_HANDLE)
  {
  }
//+----------------------------------------------------------------
//| Der Destruktor löst das verwendete Handle von dem Indikator           |
//+----------------------------------------------------------------
CChannel::~CChannel(void)
  {
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
  }
//+----------------------------------------------------------------
//| Reaktionen auf den Wechsel des Symbols                                     |
//+----------------------------------------------------------------
void CChannel::OnSymbolChanged(string new_symbol)
  {
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(new_symbol,Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+----------------------------------------------------------------
//| Reaktion auf den Wechsel der Timeframe                               |
//+----------------------------------------------------------------
void CChannel::OnTimeframeChanged(ENUM_TIMEFRAMES new_tf)
  {
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(ExpertSymbol(),Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+----------------------------------------------------------------
//| Gibt die Periode des Indikators zurück                                         |
//+----------------------------------------------------------------
int CChannel::PeriodBands(void)
  {
   return m_period;
  }
//+----------------------------------------------------------------
//|Legt die Periode des Indikators fest                                            |
//+----------------------------------------------------------------
void CChannel::PeriodBands(int period)
  {
   if(m_period == period)return;
   m_period=period;
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(ExpertSymbol(),Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+----------------------------------------------------------------
//| Legt den Wert für die Standardabweichung fest                                |
//+----------------------------------------------------------------
double CChannel::StdDev(void)
  {
   return m_std_dev;
  }
//+----------------------------------------------------------------
//| Legt den Wert für die Standardabweichung fest                                |
//+----------------------------------------------------------------
void CChannel::StdDev(double std)
  {
   if(m_std_dev == std)return;
   m_std_dev=std;
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(ExpertSymbol(),Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+----------------------------------------------------------------
//| Die Regeln für das Öffnen von Long-Positionen                                      |
//+----------------------------------------------------------------
void CChannel::InitBuy(const MarketEvent &event)
  {
   if(IsTrackEvents(event))return;                    // Die Logik wird nur aktiviert, wenn eine neue Bar geöffnet wurde
   if(positions.open_buy > 0)return;                  // Es wird nur eine Long-Position geöffnet
   double bands[];
   if(CopyBuffer(m_handle, UPPER_BAND, 1, 1, bands) == 0)return;
   if(Close[1]>bands[0])
      Trade.Buy(1.0,ExpertSymbol());
  }
//+----------------------------------------------------------------
//| Regeln für das Schließen von Long-Positionen                                      |
//+----------------------------------------------------------------
void CChannel::SupportBuy(const MarketEvent &event,CPosition *pos)
  {
   if(IsTrackEvents(event))return;                    // Die Logik wird nur aktiviert, wenn eine neue Bar geöffnet wurde
   double bands[];
   if(CopyBuffer(m_handle, BASE_LINE, 1, 1, bands) == 0)return;
   double b = bands[0];
   double s = Close[1];
   if(Close[1]<bands[0])
      pos.CloseAtMarket();
  }
//+----------------------------------------------------------------
//| Die Regeln für das Öffnen von Long-Positionen                                      |
//+----------------------------------------------------------------
void CChannel::InitSell(const MarketEvent &event)
  {
   if(IsTrackEvents(event))return;                    // Die Logik wird nur aktiviert, wenn eine neue Bar geöffnet wurde
   if(positions.open_sell > 0)return;                 // Es wird nur eine Short-Position geöffnet
   double bands[];
   if(CopyBuffer(m_handle, LOWER_BAND, 1, 1, bands) == 0)return;
   if(Close[1]<bands[0])
      Trade.Sell(1.0,ExpertSymbol());
  }
//+----------------------------------------------------------------
//| Regeln für das Schließen von Long-Positionen                                      |
//+----------------------------------------------------------------
void CChannel::SupportSell(const MarketEvent &event,CPosition *pos)
  {
   if(IsTrackEvents(event))return;     // Die Logik wird nur mit dem Öffnen einer neuen Bar aktiviert
   double bands[];
   if(CopyBuffer(m_handle, BASE_LINE, 1, 1, bands) == 0)return;
   double b = bands[0];
   double s = Close[1];
   if(Close[1]>bands[0])
      pos.CloseAtMarket();
  }
//+----------------------------------------------------------------
//| Filter an der eingehenden Events. Wenn das eingehende Event durch die Strategie nicht              |
//| berarbeitet wurde, dann wird false zurückgegeben; Wenn es bearbeitet wurde |
//| wird true zurückgeben.                                                    |
//+----------------------------------------------------------------
bool CChannel::IsTrackEvents(const MarketEvent &event)
  {
//--- Wir handeln nur beim Öffnen einer neuen Bar auf dem aktuellen Symbol und der aktuellen Timeframe
   if(event.type != MARKET_EVENT_BAR_OPEN)return false;
   if(event.period != Timeframe())return false;
   if(event.symbol != ExpertSymbol())return false;
   return true;
  }
//+----------------------------------------------------------------
//| Die Parameter der Strategie werden hier       |
//| in der von CStrategy überschriebenen Methode übersetzt (parsing)                            |
//+----------------------------------------------------------------
bool CChannel::ParseXmlParams(CXmlElement *params)
  {
   bool res=true;
   for(int i=0; i<params.GetChildCount(); i++)
     {
      CXmlElement *param=params.GetChild(i);
      string name=param.GetName();
      if(name=="Period")
         PeriodBands((int)param.GetText());
      else if(name=="StdDev")
         StdDev(StringToDouble(param.GetText()));
      else
         res=false;
     }
   return res;
  }
//+----------------------------------------------------------------
//| Der vollständige eindeutige Name des Expert-Advisors                       |
//+----------------------------------------------------------------
string CChannel::ExpertNameFull(void)
  {
   string name=ExpertName();
   name += "[" + ExpertSymbol();
   name += "-" + StringSubstr(EnumToString(Timeframe()), 7);
   name += "-" + (string)Period();
   name += "-" + DoubleToString(StdDev(), 1);
   name += "]";
   return name;
  }

Jetzt führt der Expert Advisor mehr Operationen aus. Der EA beinhaltet den Bollinger Mittelungsparameter und seinen Standardabweichungs-Wert. Zudem erzeugt er Indikator-Handles und zerstört sie in den zugehörigen Methoden. Dieses geschieht aufgrund der direkten Verwendung von Indikatoren ohne einen Wrapper. Der Rest des Programmcodes ist gleich dem des vorherigen Expert-Advisors. Er wartet darauf, dass der Schlusskurs einer Bar oberhalb (für Kauf) oder unterhalb (für Verkauf) des Bollinger Bands ist und öffnet eine neue Position.

Beachten Sie, dass der Expert Advisor einen direkten Zugriff auf die Bars über die speziellen Klassen der Zeitserien durchführt. Zum Beispiel wird diese Methode dafür verwendet um den letzten bekannten Schlusskurs mit dem oberen Bollinger Band in der Kauf-Sektion zu vergleichen (die InitBuy Methode):

double bands[];
if(CopyBuffer(m_handle, UPPER_BAND, 1, 1, bands) == 0)return;
if(Close[1] > bands[0])
   Trade.Buy(1.0, ExpertSymbol());

Zu den bekannten Methoden, besitzt diese Expert Advisor noch die überschriebenen Methoden ExpertNameFull und ParseXmlParams. Die Erste bestimmt den eindeutigen Namen des Expert Advisors, welcher auf dem Panel und als Expert Advisor-Name angezeigt wird. Die zweite Methode lädt die Einstellungen des Bollinger Indikators aus einer XML-Datei. Das Benutzer-Panel und das Abspeichern der Expert Advisor-Einstellungen in einer XML Datei wird in dem nächsten Artikel besprochen. Der Rest der Operationen des EAs ist gleich dem Vorherigen. Das ist das Ziel des vorgeschlagenen Konzepts: vollständige Vereinheitlichung von Expert Advisor-Entwicklungen.

 

Das Laden von benutzerdefinierten Strategien in die Trading-Engine

Sobald alle Strategien beschrieben worden sind, müssen wir die Instanzen erzeugen, sie mit den notwendigen Parametern initialisieren und sie der Trading Engine hinzufügen. Jede Strategie, die zu der Engine übergeben wird, sollte einige abrufbare Eigenschaften (Vollständige Eigenschaften) besitzen. Zu diesen Merkmalen sollten die folgenden Eigenschaften gehören:

  • Die eindeutige Kennzeichnung der Strategie (ihre magic number). Strategie-IDs müssen eindeutig sein, auch wenn sie als Instanzen derselben Klasse erstellt werden. Um eine eindeutige Nummer anzugeben, verwenden Sie die ExpertMagic() Set-Methode der Strategie.
  • Strategie Timeframe (oder die Periode). Selbst wenn eine Strategie auf mehreren Perioden in der gleichen Zeit läuft, müssen Sie noch die Timeframe angeben. Es könnte in diesem Fall zum Beispiel, die am meisten verwendete Timeframe sein Um die Periode anzugeben, verwenden Sie die Timeframe Set-Methode.
  • Strategy symbol (oder das aktuelle Finanzinstrument). Wenn eine Strategie mit mehreren Symbolen verwendet wird (eine Multi-Währungs-Strategie), müssen Sie dennoch das Arbeits-Symbol angeben. Dieses kann eines der Symbole sein, welches von der Strategie verwendet wird.
  • Strategy name. Zu den oben angegebenen Merkmalen muss jede Strategie auch noch ihren eigenen string Namen haben. Der Name des Expert-Advisors wird über die Methode ExpertName Set angegeben. Diese Eigenschaft wird gebraucht, da er für die automatische Erzeugung von Strategien aus der Datei Strategies.xml verwendet wird. Die selbe Eigenschaft wird verwendet, um die Strategie in dem Benutzer-Panel anzuzeigen, welches in dem 4. Artikel beschrieben wird.

Wenn auch nur eine dieser Merkmale nicht angegeben wird, dann verweigert die Engine das Laden des Algorithmus und gibt einen Warnhinweis zurück, in welchem der fehlende Parameter angegeben ist.

Die Trading Engine besteht aus zwei hauptsächlichen Teilen:

  • Einem externen Modul für das Verwalten von Strategien CStrategyList. Dieses Modul ist für die Verwaltung von Strategien verantwortlich und beinhaltet alle Algorithmen um diese zu kontrollieren. Wir werden dieses Modul in dem nächsten Teil dieser Serie besprechen.
  • Ein internes Modul mit Strategien CStrategy. Dieses Modul definiert die grundlegenden Funktionen der Strategie. Es wurde detailliert in diesem und in dem vorherigen Artikel: "Universeller Expert Advisor: Das Event-Modell und der Trading-Strategie Prototyp (Part 2)" beschrieben.

Jede Instanz von CStrategy muss in die CStrategyList geladen werden. Der Verwalter der Strategien erlaubt das Laden der Strategien auf zwei Arten:

  • Automatically Unter Verwendung der Strategies.xml Datei. Sie können also ein ganzes Set von Strategien und deren Parameter in dieser Datei beschreiben. Dann wird der Expert Advisor beim Start über den Strategie-Manager die gewünschten Instanzen der Strategien erzeugen, sie mit den zugehörigen Parametern initialisieren und zu der Liste hinzufügen. Diese Methode wird in dem nächsten Artikel genauer beschrieben.
  • Manually Durch das Hinzufügen der Beschreibung zu dem ausführenden Modul. In diesem Fall wird das zugehörige Strategie Objekt in der OnInit Sektion des Expert Advisor unter Verwendung eines Sets von Istruktionen erstellt. Anschließend wird es mit den benötigen Parametern initialisiert und zu der Liste CStrategyList hinzugefügt.

Hier ist die Beschreibung für den Prozess der manuellen Konfiguration. Wir erzeugen die Agent.mq5 Datei mit dem folgenden Inhalt:

//+----------------------------------------------------------------
//|                                                        Agent.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+----------------------------------------------------------------
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Strategy\StrategiesList.mqh>
#include <Strategy\Samples\ChannelSample.mqh>
#include <Strategy\Samples\MovingAverage.mqh>
CStrategyList Manager;
//+----------------------------------------------------------------
//| Expert initialization Function                                   |
//+----------------------------------------------------------------
int OnInit()
  {
//--- Configure and add to the list of strategies CMovingAverage
   CMovingAverage *ma=new CMovingAverage();
   ma.ExpertMagic(1215);
   ma.Timeframe(Period());
   ma.ExpertSymbol(Symbol());
   ma.ExpertName("Moving Average");
   ma.FastMA.MaPeriod(10);
   ma.SlowMA.MaPeriod(23);
   if(!Manager.AddStrategy(ma))
      delete ma;

//--- Configure and add to the list of strategies CChannel
   CChannel *channel=new CChannel();
   channel.ExpertMagic(1216);
   channel.Timeframe(Period());
   channel.ExpertSymbol(Symbol());
   channel.ExpertName("Bollinger Bands");
   channel.PeriodBands(50);
   channel.StdDev(2.0);
   if(!Manager.AddStrategy(channel))
      delete channel;

   return(INIT_SUCCEEDED);
  }
//+----------------------------------------------------------------
//| Expert deinitialization Funktion                                 |
//+----------------------------------------------------------------
void OnDeinit(const int reason)
  {
   EventKillTimer();
  }
//+----------------------------------------------------------------
//| Expert tick Funktion                                             |
//+----------------------------------------------------------------
void OnTick()
  {
   Manager.OnTick();
  }
//+----------------------------------------------------------------
//| Orderbuch Event-Funktion                                               |
//+----------------------------------------------------------------
void OnBookEvent(const string &symbol)
  {
   Manager.OnBookEvent(symbol);
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   Manager.OnChartEvent(id,lparam,dparam,sparam);
  }

Diesem Listing können wir entnehmen, dass die Konfiguration der Strategie in der Funktion OnInit durchgeführt wird. Wenn einer der benötigen Parameter nicht zu der Strategie hinzugefügt wird, dann wird der Strategie-Manager ein Hinzufügen zu der Liste ablehnen. In diesem Fall wird die AddStartegy Methode false zurückgeben und die erzeugte Instanz der Strategie muss gelöscht werden. Der Strategie-Manager erzeugt einen Warnhinweis, damit sie das Problem erkennen und verstehen können. Lassen Sie uns einen solchen Warnhinweis testen. Dafür ändern sie die Anweisung, die die Magic number setzt mit // in einen Kommentar:

//+----------------------------------------------------------------
//| Expert initialization Function                                   |
//+----------------------------------------------------------------
int OnInit()
  {
//--- Configure and add to the list of strategies CMovingAverage
   CMovingAverage *ma=new CMovingAverage();
 //ma.ExpertMagic(1215);
   ma.Timeframe(Period());
   ma.ExpertSymbol(Symbol());
   ma.ExpertName("Moving Average");
   ma.FastMA.MaPeriod(10);
   ma.SlowMA.MaPeriod(23);
   if(!Manager.AddStrategy(ma))
      delete ma;
   return(INIT_SUCCEEDED);
  }

Nach dem Start des ausführenden Moduls wird die folgende Nachricht in dem Terminal angezeigt:

2016.01.20 14:08:54.995 AgentWrong (FORTS-USDRUB,H1)    WARNING;CStrategyList::AddStrategy;The strategy should have a magic number. Adding strategy Moving Average is impossible;2016.01.20 14:09:01

Dieser Nachricht kann man deutlich entnehmen, dass die CStrategyList::AddStartegy Methode die Strategie nicht hinzufügen konnte, weil die Magic number fehlte.

Zu der Konfiguration von Strategien, enthält die Agent.mq5 Datei auch die Verwaltung von Handels-Events, die analysiert werden müssen. Diese Verarbeitung beinhaltet auch das Verfolgen von Events und die Weiterleitung an die entsprechenden Methoden der CStrategyList Klasse.

Sobald die ausführbare Datei erzeugt worden ist, kann sie kompiliert werden. Der Quellcode der analysierten Strategien ist erhältlich in dem Include\Strategy\Samples Verzeichnis der beigefügten .zip-Datei. Ein bereits kompilierter Expert Advisor ist für die direkte Verwendung enthalten und beinhaltet die Logik der beiden Handelsstrategien.

 

Schlussfolgerung

Wir haben die Beispiele für benutzerdefinierte Strategien und die Grundprinzipien der Klassen, die Zugriff auf Kurse und Indexe bereitstellen, analysiert. Außerdem haben wir über Klassen gesprochen, die eine Protokollierung implementieren und über Beispiele von objektorientierten Indikatoren. Das vorgeschlagene Konzept für die Konstruktion eines Expert Advisors macht die Beschreibung der Logik eines Handelssystems einfacher. Es müssen lediglich in einigen überschriebenen Methoden die Regeln definiert werden.

In dem vierten Teil der Serie "Universeller Expert Advisor: Handeln in einer Gruppe und die Verwaltung eines Portfolios von Strategien (Teil 4)" beschreiben wir den Algorithmus, mit welchem wir eine unbegrenzte Anzahl an Handelnslogiken zu einem einzigen ausführbaren Expert Advisor-Modul hinzufügen können (ex5). In dem vierten Teil werden wir zudem ein einfaches Benutzer-Panel betrachten, Mit welchem Sie während der Ausführung des Expert-Advisors Änderungen vornehmen können, wie zum Beispiel den Trading Mode oder den manuellen Kauf und Verkauf.