Universeller Expert Advisor: Benutzerstrategien und Hilfsklassen (Teil 3)
Inhalt
- Einleitung
- Logs, CMessage und CLog Klassen, Singleton Pattern
- Zugriff auf Kurse über Indexe von MetaTrader 4
- Verwendung von objektorientierten Indikatoren
- Methoden, die von dem benutzerdefinierten Expert Advisor überschrieben werden
- Ein Beispiel wie der Expert Advisor mit zwei gleitenden Durchschnitten handelt
- Ein Beispiel für einen Expert Advisor der eine Ausbruchstrategie mit den Bollinger-Bands-Kanälen handelt
- Das Laden von benutzerdefinierten Strategien in die Trading-Engine
- Schlussfolgerung
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:
- Definieren Sie zunächst einen Array, in welches die gewünschten Kurse hineinkopiert werden sollen.
- Kopieren Sie die gewünschten Kurse mit einer der Funktionen aus der Gruppe Copy* (Funktionen für den Zugriff auf Zeitserien und Indikator-Daten).
- 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:
- Das Symbol für die Berechnung.
- Die Timeframe/Periode des Charts.
- Die Periode für die Mittelung
- Der Typ des gleitenden Durchschnitts (einfach, exponentiell, gewichtet, etc.)
- Die Verschiebung des Indikators
- 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:
- Ein Handels-Event ist das Öffnen einer neuen Bar.
- Es existiert keine andere Position in dieselbe Richtung.
- 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:
- Ein Handels-Event ist das Öffnen einer neuen Bar.
- 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.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/2170
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.