Cross-Plattform Expert Advisor: Zeitfilter

Enrico Lambino | 28 August, 2017


Inhaltsverzeichnis

  1. Einführung
  2. Ziele
  3. Basisklasse
  4. Die Klassen und Typen der Zeitfilter
  5. Der Container für die Zeitfilter
  6. Subfilter (CTimeFilter)
  7. Beispiel
  8. Schlussfolgerung

Einführung

Das Filtern der Zeit wird verwendet, wenn eine besondere Zeitspanne definiert wurde, und ein Expert Advisor prüfen muss, ob ein bestimmter Zeitpunkt in diese Zeitspanne fällt. Bestimmte Funktionen können ein- oder ausgeschaltet werden, je nach dem, ob die Bedingungen erfüllt wurden oder nicht. Das ist sehr sinnvoll, wenn einige Funktionen eines Expert Advisors nicht zu jeder Zeit ausgeführt werden sollen (periodisch oder doch immer aber mit Ausnahmen). Unten sind ein paar Beispiele, die einen Zeitfilter verwenden:

  1. Vermeiden bestimmter Zeitspannen (z.B. Zeiten mit einer Seitwärtsbewegung oder hoher Volatilität)
  2. Bestimmen des 'Endes' einer Marktorder oder Position (Ausstieg aus dem Markt zum Endzeitpunkt)
  3. Schließen der Positionen zum Ende der Handelswoche.

Dies sind einige der weit verbreiteten Verwendungen durch die Händler, obwohl es noch andere Möglichkeiten gibt.

Ziele

  • Verstehen und Verwenden der gängigsten Methoden der Zeitfilterung
  • Einfaches Verwenden mehrerer Zeitfilter durch Expert Advisor
  • Kompatibilität mit MQL4 und MQL5

Basisklasse

Die Klasse CTime dient als Basisklasse für die anderen Objekte zur Zeitfilterung für unseren Expert Advisor. Die Definition der Klasse CTimeBase (die Basisklasse von CTime) zeigt der folgende Teil des Codes:

class CTimeBase : public CObject
  {
protected:
   bool              m_active;
   bool              m_reverse;
   CSymbolManager   *m_symbol_man;
   CEventAggregator *m_event_man;
   CObject          *m_container;
public:
                     CTimeBase(void);
                    ~CTimeBase(void);
   virtual int       Type(void) const {return CLASS_TYPE_TIME;}
   //--- Initialisierung
   virtual bool      Init(CSymbolManager*,CEventAggregator*);
   virtual CObject *GetContainer(void);
   virtual void      SetContainer(CObject*);
   virtual bool      Validate(void);
   //--- Ändern und Abfragen
   bool              Active(void) const;
   void              Active(const bool);
   bool              Reverse(void);
   void              Reverse(const bool);
   //--- Prüfen
   virtual bool      Evaluate(datetime)=0;
  };

Die Basisklasse hat zwei Variablen primitive Datentyps. m_active soll das Objekt aktivieren oder deaktivieren. m_reverse soll die Ausgabe des Klassenobjektes umkehren (Rückgabe von 'true' bei eigentlich 'false' und umgekehrt).

Die Klassen und Typen der Zeitfilter

Zeitfilter durch einen bestimmte Zeitspanne

Das ist die einfachste Methode eines Zeitfilters Für die Prüfung der Zeit mit dieser Methode braucht man nur die Zeitpunkte für Anfang und Ende. Wenn jetzt ein Zeitpunkt innerhalb dieser beiden liegt, wird 'true' zurückgegeben, andernfalls 'false'.

Diese Methode ist Teil von CTimeRange. Der folgende Code zeigt die Definition von CTimeRangeBase, der Basisklasse von CTimeRange:

class CTimeRangeBase : public CTime
  {
protected:
   datetime          m_begin;
   datetime          m_end;
public:
                     CTimeRangeBase(void);
                     CTimeRangeBase(datetime,datetime);
                    ~CTimeRangeBase(void);
   //--- Initialisierung       datetime,datetime
   virtual bool      Set(datetime,datetime);
   virtual bool      Validate(void);
   //--- Ändern und Abfragen
   datetime          Begin(void) const;
   void              Begin(const datetime);
   datetime          End(void) const;
   void              End(const datetime);
   //--- Verarbeitung
   virtual bool      Evaluate(datetime);
  };

Der Konstruktor der Klasse sollte die Variablen m_begin und m_end festlegen. Der übergebene Zeitpunkt wird in der Methode Evaluate() mit diesen beiden Zeitpunkten verglichen. Ist der übergebene Zeitpunkt nicht bestimmt oder Null, wird die aktuelle Serverzeit im Augenblick des Aufrufes verwendet:

bool CTimeRangeBase::Evaluate(datetime current=0)
  {
   if(!Active())
      return true;
   if(current==0)
      current=TimeCurrent();
   bool result=current>=m_begin && current<m_end;
   return Reverse()?!result:result;
  }

Zeitfilterung nach dem Tag der Woche

Das Filtern nach dem Wochentag ist einer der einfachsten und gebräuchlichsten Methoden der Zeitfilterung. Dieser Zeitfilter wird üblicherweise verwendet, um die Arbeit des Expert Advisors einzuschränken oder nur an bestimmten Tagen der Woche zu erlauben.

Nun, diese Klasse kann auf verschiedene Weise umgesetzt werden. Eine Methode wäre, eine nutzerdefinierte Funktion TimeDayOfWeek anzubieten, die es zwar für MQL4 aber nicht für MQL5 gibt. Ein anderer Weg wäre, die Zeit umzuwandeln, so dass sie zur Struktur MqlDateTime passt, um dann deren Parameter day_of_week zu überprüfen. Wir wählen das letztere Modell, und es ist zu empfehlen, da es erlaubt, alle verwendbaren Methoden der Klasse in der Basisklasse zu versammeln.

Dieser Weg wird von unserem Expert Advisor mit der Klasse CTimeDays verwendet. Der folgende Teil des Codes zeigt die Definition von CTimeDaysBase, der Basisklasse von CTimeDays:

class CTimeDaysBase : public CTime
  {
protected:
   long              m_day_flags;
public:
                     CTimeDaysBase(void);
                     CTimeDaysBase(const bool sun=false,const bool mon=true,const bool tue=true,const bool wed=true,
                                   const bool thu=true,const bool fri=true,const bool sat=false);
                    ~CTimeDaysBase(void);
   //--- Initialisierung                    
   virtual bool      Validate(void);
   virtual bool      Evaluate(datetime);
   virtual void      Set(const bool,const bool,const bool,const bool,const bool,const bool,const bool);
   //--- Ändern und Abfragen
   bool              Sunday(void) const;
   void              Sunday(const bool);
   bool              Monday(void) const;
   void              Monday(const bool);
   bool              Tuesday(void) const;
   void              Tuesday(const bool);
   bool              Wednesday(void) const;
   void              Wednesday(const bool);
   bool              Thursday(void) const;
   void              Thursday(const bool);
   bool              Friday(void) const;
   void              Friday(const bool);
   bool              Saturday(void) const;
   void              Saturday(const bool);
  };

Wie man sieht, hat die Klasse nur eine Klassenvariable vom Typ long. Damit wird der Rückgabewert dieser Klasse ermittelt (7 Tage einer Woche). Damit wird klar, dass wir hier bitweise rechnen, und wir eine eigene Enumeration deklarieren müssen, deren Werte die 7 Tage der Woche repräsentieren:

enum ENUM_TIME_DAY_FLAGS
  {
   TIME_DAY_FLAG_SUN=1<<0,
   TIME_DAY_FLAG_MON=1<<1,
   TIME_DAY_FLAG_TUE=1<<2,
   TIME_DAY_FLAG_WED=1<<3,
   TIME_DAY_FLAG_THU=1<<4,
   TIME_DAY_FLAG_FRI=1<<5,
   TIME_DAY_FLAG_SAT=1<<6
  };

Die Werte der Wochentage werden mit der folgenden Methode festgelegt. Der Einfachheit halber wird die Methode in dem Konstruktor der Klasse aufgerufen, um eine versehentliche Auswertung ohne festgelegte Werte durch eine Instanz der Klasse zu vermeiden.

void CTimeDaysBase::Set(const bool sun=false,const bool mon=true,const bool tue=true,const bool wed=true,
                        const bool thu=true,const bool fri=true,const bool sat=false)
  {
   Sunday(sun);
   Monday(mon);
   Tuesday(tue);
   Wednesday(wed);
   Thursday(thu);
   Friday(fri);
   Saturday(sat);
  }

Diese Werte können aber auch individuell bestimmt werden. Das ist nötig, wenn nur ein Wert verändert werden muss, und man nicht die Methode Set() verwenden will, die dann gleich alle 7 Tage bestimmt (welches in manchen Fällen zu Fehlern führen könnte). Der folgende Codeausschnitt zeigt die Methode Monday(), die für die Bestimmung des Wertes des zweiten Wochentages verwendet wird. Das Abfragen und Ändern der anderen Tage funktioniert in der gleichen Weise.

void CTimeDaysBase::Monday(const bool set)
  {
   if(set)
      m_day_flags|=TIME_DAY_FLAG_MON;
   else
      m_day_flags &=~TIME_DAY_FLAG_MON;
  }

Die Methode, mit der die Werte für die Wochentage individuell gesetzt wurden, wird auch für die Auswertung verwendet, das heißt, ob ein bestimmter Zeitpunkt auf einen Wochentag fällt oder nicht:

bool CTimeDaysBase::Evaluate(datetime current=0)
  {
   if(!Active())
      return true;
   bool result=false;
   MqlDateTime time;
   if(current==0)
      current=TimeCurrent();
   TimeToStruct(current,time);
   switch(time.day_of_week)
     {
      case 0: result=Sunday();      break;
      case 1: result=Monday();      break;
      case 2: result=Tuesday();     break;
      case 3: result=Wednesday();   break;
      case 4: result=Thursday();    break;
      case 5: result=Friday();      break;
      case 6: result=Saturday();    break;
     }
   return Reverse()?!result:result;
  }

Wie vorher bereits erwähnt, wird zunächst der Wert des Wochentages des Zeitpunktes ermittelt. Wurde kein Argument übergeben, verwendet die Methode die aktuelle Zeit. Es wird der Zeitpunkt in das Format MqlDateTime übertragen, um der Variablen day_of_week einen Wert zuzuweisen, damit eine Überprüfung mit dem Wert von (m_day_flags) stattfinden kann.

Diese Methode erfüllt oft die Anforderungen der Händler, wie "kein Handel am Freitag" oder "Sonntags", wenn an den Wochentagen der Broker zwar aktiv ist, dem Händler aber diese Tage nicht geeignet erscheinen.

Verwenden eines Timers

Ein andere Methode einer Zeitfilterung ist die Verwendung eines Timers. Ein Timer vergleicht die aktuelle Zeit mit einem bestimmten Zeitpunkt in der Vergangenheit. Ist die Zeitspanne während der Evaluation noch innerhalb der Ablaufzeit, wird ein 'true' zurückgegeben, andernfalls 'false'. Diese Methode ist Teil der Klasse CTimer. Der folgende Codeausschnitt zeigt die Definition von CTimerBase, der Basisklasse von CTimer:

class CTimerBase : public CTime
  {
protected:
   uint              m_years;
   uint              m_months;
   uint              m_days;
   uint              m_hours;
   uint              m_minutes;
   uint              m_seconds;
   int               m_total;
   int               m_elapsed;
   datetime          m_time_start;
public:
                     CTimerBase(const int);
                     CTimerBase(const uint,const uint,const uint,const uint,const uint,const uint);                     
                    ~CTimerBase(void);
   //--- Initialisierung
   virtual bool      Set(const uint,const uint,const uint,const uint,const uint,const uint);
   virtual bool      Validate(void);
   //--- Abfragen und Ändern
   uint              Year(void) const;
   void              Year(const uint);
   uint              Month(void) const;
   void              Month(const uint);
   uint              Days(void) const;
   void              Days(const uint);
   uint              Hours(void) const;
   void              Hours(const uint);
   uint              Minutes(void) const;
   void              Minutes(const uint);
   uint              Seconds(void) const;
   void              Seconds(const uint);
   bool              Total(void) const;
   datetime          TimeStart(void) const;
   void              TimeStart(const datetime);
   //--- Verarbeitung   
   virtual bool      Elapsed(void) const;
   virtual bool      Evaluate(datetime);
   virtual void      RecalculateTotal(void);
  };

Die Argumente des Konstruktors werden verwendet, um die Gesamtdauer oder die Ablaufzeit ab den Startzeitpunkt festzulegen, die in der Klassenvariablen m_total gespeichert wird. Der Einfachheit halber deklarieren wir Konstanten auf Basis der Anzahl von Sekunden der Zeitspanne, von einer Minute bis hinauf zu einem Jahr:

#define YEAR_SECONDS 31536000
#define MONTH_SECONDS 2419200
#define DAY_SECONDS 86400
#define HOUR_SECONDS 3600
#define MINUTE_SECONDS 60

Der Konstruktor der Klassen benötigt die Ablaufzeit des Timers von Sekunden bis zu Jahren:

CTimerBase::CTimerBase(const uint years,const uint months,const uint days,const uint hours,const uint minutes,const uint seconds) : m_years(0),
                                                                                                                                    m_months(0),
                                                                                                                                    m_days(0),
                                                                                                                                    m_hours(0),
                                                                                                                                    m_minutes(0),
                                                                                                                                    m_seconds(0),
                                                                                                                                    m_total(0),
                                                                                                                                    m_time_start(0)
  {
   Set(years,months,days,hours,minutes,seconds);
  }

Alternativ kann eine Instanz der Klasse mit den bevorzugten Werten für m_total als einziges Argument erstellt werden:

CTimerBase::CTimerBase(const int total_time) : m_years(0),
                                               m_months(0),
                                               m_days(0),
                                               m_hours(0),
                                               m_minutes(0),
                                               m_seconds(0),
                                               m_total(0),
                                               m_time_start(0)
  {
   m_total=total_time;
  }

Der Aufruf der Methode Evaluate() der Klasse gleich nach ihrer Instantiierung würde zu einem Vergleich der Methode von m_total mit dem UNIX-Anfangszeitpunkt des Typs 'datetime' führen. Daher muss vor dem ersten Aufruf von Evaluate() die gewünschte Startzeit bestimmt werden (es sei denn, die wäre der 1. Januar 1970 (Mitternacht UTC/GMT). Das Folgende zeigt die Methoden zum Abfragen und Ändern der Startzeit der Klassenvariablen m_time_start mit der überladenen Methode TimeStart():

datetime CTimerBase::TimeStart(void) const
  {
   return m_time_start;
  }

void CTimerBase::TimeStart(const datetime time_start)
  {
   m_time_start=time_start;
  }

Die Methode Evaluate() dieser Klasse ist ziemlich einfach: Sie berechnet die Differenz zwischen dem Startzeitpunkt und der als Argument übergebenen Zeit (meistens die aktuelle Zeit). Das ist die verstrichene Zeitspanne, und, wenn die das maximal Erlaubte (m_total) überschreitet, dann gibt die Methode 'false' zurück.

bool CTimerBase::Evaluate(datetime current=0)
  {
   if(!Active())
      return true;
   bool result=true;
   if(current==0)
      current= TimeCurrent();
   m_elapsed=(int)(current-m_time_start);
   if(m_elapsed>=m_total) result=false;
   return Reverse()?!result:result;
  }

Diese Methode des Zeitfilterns wird auf verschiedene Arten verwendet, wie zum Beispiel eine längste Dauer (Ablauf) für bestimmte Verhaltensweisen eines Expert Advisors oder dem Setzen der Ablaufzeit einer Marktorder oder Position (ähnlich dem binären Optionshandel). Dieser Filter ist in etwa der Gleiche wie OnTimer() (die sowohl in MQL4 wie in MQL5 existiert), da der aber nur einmal in einem Expert Advisor existieren kann, könnte CTimer als zusätzliche Timer von einem Expert Advisor benötigt werden.

Filtern mittel einer Tagesplan

Das Filtern mittels eines Tagesplans ist eine der beliebtesten der Händler. Der Filter verwendet einen 24-Stunden Zeitplan und der Expert Advisor wählt (über die Parameter) einen bestimmten Zeitplan, nach dem er seine Arbeit ausrichtet (normalerweise seine Reaktion auf die Handelssignale). Dieser Weg des Filterns wird realisiert durch die Klasse CTimeFilter. Der folgende Code zeigt die Definition von CTimeFilterBase, der Basisklasse von CTimeFilter.

class CTimeFilterBase : public CTime
  {
protected:
   MqlDateTime       m_filter_start;
   MqlDateTime       m_filter_end;
   CArrayObj         m_time_filters;
public:
                     CTimeFilterBase(void);
                     CTimeFilterBase(const int,const int,const int,const int,const int,const int,const int);
                    ~CTimeFilterBase(void);
   virtual bool      Init(CSymbolManager*,CEventAggregator*);
   virtual bool      Validate(void);
   virtual bool      Evaluate(datetime);
   virtual bool      Set(const int,const int,const int,const int,const int,const int,const int);
   virtual bool      AddFilter(CTimeFilterBase*);
  };

Die Klasse hat zwei Variablen des Typs MqlDateTime und eine des Typs CArrayObj. Die beiden Strukturen speichern die Zeitspanne innerhalb der 24-Stunden, während das Objekt Unterfilter speichern soll.

Durch den Konstruktor des Klassenobjektes erhalten wir die Start- und Endzeit in Stunden, Minuten und Sekunden. Die Werte werden in den Klassenvariablen m_filter_start und m_filter_end mit der Methode Set() der Klasse gespeichert. Der Parameter gmt wird für die Berechnung der GMT-Offset des Brokers verwendet.

bool CTimeFilterBase::Set(const int gmt,const int starthour,const int endhour,const int startminute=0,const int endminute=0,
                          const int startseconds=0,const int endseconds=0)
  {
   m_filter_start.hour=starthour+gmt;
   m_filter_start.min=startminute;
   m_filter_start.sec=startseconds;
   m_filter_end.hour=endhour+gmt;
   m_filter_end.min=endminute;
   m_filter_end.sec=endseconds;
   return true;
  }

Kommen wir jetzt zur Methode Evaluate() der Klasse. Bei der Initialisierung werden die Werte der beiden Parameter nur mit Stunde, Minute und Sekunde des 24-Stunden Tages gespeichert. Keine der anderen Werte, wie Jahr oder Monat, werden übernommen, um die Start- und Endzeit mit der angegebenen (oder aktuellen, wenn kein Argument übergeben wurde) Zeit zu vergleichen. Es gibt tatsächlich zwei Wege das zu tun:

  1. Umwandeln der übergebenen Zeit in Stunde, Minute und Sekunde, die dann mit den drei Variablen der Struktur verglichen werden.
  2. Aktualisieren der fehlenden Werte mittels der aktuellen Zeit, umwandeln der Zeit-Struktur in UNIX-Zeit (vom Typ datetime), und das wird dann mit der übergebenen Zeit vergleichen.

Es wurde der zweite Möglichkeit für die Methode Evaluate() gewählt, sie ist unten aufgeführt:

bool CTimeFilterBase::Evaluate(datetime current=0)
  {
   if(!Active())
      return true;
   bool result=true;
   MqlDateTime time;
   if(current==0)
      current=TimeCurrent();
   TimeToStruct(current,time);
   m_filter_start.year= time.year;
   m_filter_start.mon = time.mon;
   m_filter_start.day = time.day;
   m_filter_start.day_of_week = time.day_of_week;
   m_filter_start.day_of_year = time.day_of_year;
   m_filter_end.year= time.year;
   m_filter_end.mon = time.mon;
   m_filter_end.day = time.day;
   m_filter_end.day_of_week = time.day_of_week;
   m_filter_end.day_of_year = time.day_of_year;
   /*
     Andere Aufgaben hier
   */
  }

Der Vergleich mit der Endzeit ist ausschließend. Das heißt, wenn ein Expert Advisor nur zwischen 08:00 und 17:00 handeln soll, dann wird der er frühestens um genau 08:00, gleich mit dem Beginn der 08:00-Kerze, aber nur bis 17:00, also zuletzt um 16:59, handeln.

Da die Zeit-Struktur keine Zeit größer als diese Stunde kennt, muss das Fehlende aus der aktuellen Zeit ermittelt werden (oder aus der übergebenen Zeit). Es müssen allerdings einige Vorkehrungen gemacht werden, wenn Start- und Endzeit zwar kleiner als 24 Stunden sind, sie aber nicht in den selben Tag fallen. Im obigen Beispiel, 08:00 ist 8:00 Vormittags, während 17:00 5:00 Nachmittags ist. In diesem Fall liegen beide Zeitpunkte im selben Tag. Ändern wir das einmal, so dass die Startzeit 5:00 Nachmittags wird und die Endzeit 8:00 Vormittags. Jetzt ist die Stunde der Startzeit größer als die der Endzeit, und das heißt, die Zeitspanne reicht bis in den nächsten Tag. Und somit fallen Start- und Endzeit nicht in denselben Tag. Für diese Situation gibt es nun zwei Möglichkeiten:
  1. Die Startzeit fällt in den aktuellen Tag (oder den Tag der übergebenen Zeit), und die Endzeit ist am nächsten Tag.
  2. Die Startzeit ist vom Vortag (dem Tag vor der übergebenen Zeit) und die Endzeit vom aktuellen Tag (oder dem Tag der übergebenen Zeit).

Die nötigen Anpassungen hängen von der aktuellen oder übergebenen Zeit ab. Angenommen wir haben eine übergebene (oder aktuelle) Zeit von 5:01 Nachmittags (17:01). In diesem Fall fällt die Startzeit in den selben Tag wie die übergebenen Zeit. Jetzt sind wir sicher, dass die Endzeit in den nächsten Tag fällt. Andererseits bei einer übergebenen Zeit von 01:00 oder 01:00 in der Nacht, fällt die Endzeit auf den selben Tag wie die übergebene Zeit und die Startzeit in den Vortag. Daher wird die Berechnung von oben mit der Zeit-Struktur 'MqlDateTime' wie folgt angepasst:

  1. Fällt die Startzeit auf denselben Tag wie die übergebene Zeit, addieren wir 1 Tag zur Endzeit.
  2. Fällt die Endzeit auf denselben Tag wie die übergebene Zeit, subtrahieren wir 1 Tag von der Startzeit.

Das kommt allerdings nur zum Tragen, wenn Start- und Endzeit nicht in den gleichen Tag fallen, weil die Stunde der Startzeit größer ist als die der Endzeit für diesen Filter. Die Änderungen sind wie folgt in die Methode Evaluate() eingefügt:

if(m_filter_start.hour>=m_filter_end.hour)
  {
   if(time.hour>=m_filter_start.hour)
     {
      m_filter_end.day++;
      m_filter_end.day_of_week++;
      m_filter_end.day_of_year++;
     }
   else if(time.hour<=m_filter_end.hour)
     {
      m_filter_start.day--;
      m_filter_start.day_of_week--;
      m_filter_start.day_of_year--;
     }
  }

Der Rückgabevariable wird anfangs 'true' zugewiesen, so dass die aktuelle Prüfung davon abhängt, ob die übergebene Zeit zwischen Start- und Endzeit fällt oder nicht. Die Berechnung innerhalb der Methode Evaluate() stellt sicher, dass die Startzeit immer kleiner ist als die Endzeit. Sind Start- und Endzeit gleich (gleich in Bezug zu Stunde, Minute und Sekunde), gibt die Methode immer 'true' zurück. Wenn zum Beispiel Start und Endzeit gleich 05:00 sind, behandelt der Filter das, als ob beide Zeitpunkte nicht auf denselben Tag fallen, wodurch die Zeitspanne die gesamten 24 Stunden umfasst.

Der Container für die Zeitfilter

Ähnlich den anderen Klassenobjekten dieser Artikelserie, sollte der Zeitfilter auch einen Container haben, in dem die Pointer gespeichert werden. Das erlaubt die Auswertung durch einen Aufruf aller Methoden Evaluate() dieses Containers. Wenn alle Zeitfilter der Methoden Evaluate() ein 'true' zurückgeben (es gibt also kein Hindernis bezüglich der Zeitfilter), dann sollte auch dieser Container ein 'true' zurückgeben. Das ist in der Klasse CTimes implementiert. Der folgende Code zeigt die Definition der Klasse von CTimeBase, der Basisklasse von CTimes:

class CTimesBase : public CArrayObj
  {
protected:
   bool              m_active;
   int               m_selected;
   CEventAggregator *m_event_man;
   CObject          *m_container;
public:
                     CTimesBase(void);
                    ~CTimesBase(void);
   virtual int       Type(void) const {return CLASS_TYPE_TIMES;}
   //-- Initialisierung
   virtual bool      Init(CSymbolManager*,CEventAggregator*);
   virtual CObject *GetContainer(void);
   virtual void      SetContainer(CObject*);
   virtual bool      Validate(void) const;
   //--- activation and deactivation
   bool              Active(void) const;
   void              Active(const bool);
   int               Selected(void);
   //--- Prüfen
   virtual bool      Evaluate(datetime) const;
   //--- Wiederherstellung
   virtual bool      CreateElement(const int);
  };

Subfilter (CTimeFilter)

Die Methode Evaluate() der Zeitfilter verlangt, dass alle Mitglieder ein 'true' zurückgegeben haben, damit sie auch 'true' zurückgibt. Obwohl die meisten Filterobjekte nicht mehr als eine Instanz je Expert Advisor benötigen, ist CTimeFilter die Ausnahme davon, der übrigens der meist verwendete Filter überhaupt ist. Betrachten wir den folgenden Code:

CTimes times = new CTimes();
CTimeFilter time1 = new CTimeFilter(gmt,8,17);
times.Add(GetPointer(time1));

Angenommen der Zeitfilter wird zum Handeln verwendet (für Positionsöffnungen). In diesem Fall hat der Container nur einen Pointer in seinem dynamischen Array der Pointer. In dieser Situation, beim Aufruf der Methode Evaluate(), hängt das Ergebnis davon ab, ob die übergebenen Zeit in den Zeitraum von 08:00 und 17:00 fällt.

Betrachten wir jetzt den Fall, dass der Expert Advisor konfiguriert wurde, die Mittagszeit zu überspringen statt durchgehend von 08:00 Vormittags bis 05:00 Nachmittags zu handeln. Das heißt, er handelt Vormittags von 8:00 bis 12:00 und Nachmittags von 1:00 bis 5:00. Die Zeitspanne ist also nicht kontinuierlich, sondern in zwei Teile aufgeteilt. Ein Programmierer könnte versucht sein, den ursprünglichen Code zu ändern und zwei Instanzen von CTimeFilter zu verwenden, statt einem einzigen:

CTimes times = new CTimes();
CTimeFilter time1 = new CTimeFilter(gmt,8,12);
CTimeFilter time2 = new CTimeFilter(gmt,13,17);
times.Add(GetPointer(time1));
times.Add(GetPointer(time2));

Der obige Code würde aber nicht korrekt arbeiten. Er würde immer 'false' zurückgeben, da der Container der Zeitfilter verlangt, dass alle Primärfilter der Instanzen 'true' zurückgeben. Im obigen Beispiel würde, wenn einer 'true' zurück gibt, der andere immer 'false' zurückgeben und umgekehrt. Diese Situation verkompliziert sich weiter, wenn mehr als 2 Zeitfilter benötigt werden. In dieser Situation kann eine korrekte Auswertung nur dann erreicht werden, wenn ein Filter aktiv ist, die anderen deaktiviert wurden.

Die Lösung wäre, sicher zu stellen, dass der Container Zeitfilter maximal nur einen Pointer auf CTimeFilter speichert. Werden mehr als eine Instanz von CTimeFilter benötigt, sollten diese als Subfilter anderer Instanzen von CTimeFilter ergänzt werden. Die Pointer auf die Subfilter werden in der Klassenvariablen von CTimeFilter m_time_filters gespeichert, und die Pointers werden durch die Methode AddFilter() hinzugefügt. Der Code für die Auswertung der Subfilter ist Teil der Methode Evaluate() der Klasse und lautet wie folgt:

if(!result)
  {
   for(int i=0;i<m_time_filters.Total();i++)
     {
      CTimeFilter *filter=m_time_filters.At(i);
      if(filter.Evaluate(current))
      {
         return true;
      }   
     }
  }

Der Code wird nur ausgeführt, wenn der Hauptfilter 'false' zurückgibt,n d.h. es ist ein Mittel, um zu erkennen, ob es eine Ausnahme für das erste Ergebnis gibt. Wenn mindestens eine Subfilter ein 'true' zurückgibt, gibt auch der Hauptfilter ein 'true' zurück. Damit ändern wir den vorherigen Codeausschnitt wie folgt:

CTimes times = new CTimes();
CTimeFilter time1 = new CTimeFilter(gmt,8,12);
CTimeFilter time2 = new CTimeFilter(gmt,13,17);
CTimeFilter time0 = new CTimeFilter(gmt,0,0);
time0.Reverse();
time0.AddFilter(GetPointer(time1));
time0.AddFilter(GetPointer(time2));
times.Add(GetPointer(time0));
Durch diese Änderungen hat das Filterobjekt nur einen Pointer, nämlich time0, in seinem Array. time0, andererseits hat zwei Subfilter, time1 und time2, die ursprünglich im Container der Zeitfilter gespeichert waren. time0 hat die gleichen Parameter für Start- und Endzeit und würde also immer ein 'true' zurückgeben. Wir rufen die Methode Reverse() auf, damit Abfrage von time0 immer ein 'false' zurückgibt, um so gezwungener Maßen nach Ausnahmen für das erste Ergebnis zu suchen (über dessen Subfilter). In einer grafischen Darstellung schaut das so aus:


Graphicsche Darstellung des Hauptzeitfilters und der Subfilter

Ein Pointer auf time0 findet sich im Container der Zeitfilter. Gemäß der Darstellung von oben ist das die erste Auswertung. Das time0 nur ein 'false', werden die Subfilter geprüft. Zuerst wird die Zeit von 8:00 bis 12:00 geprüft. Falls das auch 'false' ist, wird die Zeit von 13:00 bis 17:00 geprüft. Wenn nun eine der beiden ein 'true' liefert, würde auch time0 ein 'true' zurückgeben (sonst ein 'false'). Daher würde letztendlich ein 'true' zurückgegeben werden, wenn die übergebene Zeit zwischen 8:00 und 12:00, oder 13:00 und 17:00 liegt. Es sind auch mehr als zwei Subfilter möglich und alles würde trotzdem den oben beschriebenen Regeln folgen. Subfiltern von Subfiltern werden meistens nicht benötigt, da der mehrere Zeitfilter während eines Tages durch nur zwei Ebenen realisiert werden können.

Beispiel

Als Beispiel ändern wir ein bisschen den Expert Advisorn des vorherigen Artikels. In diesen Expert Advisor werden wir alle in diesem Artikel beschriebenen Zeitfilter integrieren. Wir beginnen mit den Headerdateien für CTimesBase, da das für unseren Expert Advisor ausreicht, alle Klassen der Zeitfilter zu laden.

#include "MQLx\Base\OrderManager\OrderManagerBase.mqh"
#include "MQLx\Base\Signal\SignalsBase.mqh"
#include "MQLx\Base\Time\TimesBase.mqh" //ergänzen der integrierten Zeilen
#include <Indicators\Custom.mqh>

Dann deklarieren wir einen globalen Pointer auf den Container der Zeitfilter:

CTimes *time_filters;

In OnInit(), erstellen wir eine Instanz von CTimes und sichern dessen Pointer:

time_filters = new CTimes();

Für diesen Expert Advisor verwenden die Zeitfilter nur für das Öffnen von Positionen, nicht, sie zu schließen. Um das zu erreichen, soll der Expert Advisor, vor dem Öffnen einer Position, eine zusätzliche Prüfung durchführen, ob er tatsächlich einen neue Position zum gegebenen Zeitpunkt eröffnen darf:

if(signals.CheckOpenLong())
  {
   close_last();
   if (time_filters.Evaluate(TimeCurrent()))
   {
      //Print("Eröffnen eines Kauforder..");            
      money_manager.Selected(0);
      order_manager.TradeOpen(Symbol(),ORDER_TYPE_BUY,symbol_info.Ask());
   }
  }
else if(signals.CheckOpenShort())
  {
   close_last();
   if (time_filters.Evaluate(TimeCurrent()))
   {
      //Print("Eröffnen eines Verkaufsorder..");
      money_manager.Selected(1);
      order_manager.TradeOpen(Symbol(),ORDER_TYPE_SELL,symbol_info.Bid());
   }
  }

Wie der Code von oben zeigt, wird die letzte Position vor der Prüfung der Zeit geschlossen und nach ihr, wenn bestanden, die aktuelle Eröffnung einer neuen Position. Der Zeitfilter wird ergo nur auf die Eröffnung aber nicht auf das Schließen einer Position angewendet.

Für eine Zeitfilterung mittels einer Zeitspanne müssen wir die Parameter der Start- und der Endzeit eingeben. Beide Parameter sind vom Typ 'datetime'. Wir haben auch einen Parameter, der es dem Nutzer erlaubt, diese Funktion ein- oder auszuschalten:

input bool time_range_enabled = true;
input datetime time_range_start = 0;
input datetime time_range_end = 0;

Ein Standardwert von Null für beide Parameter führt dazu, dass sie den Anfangswert der UNIX-Zeit erhalten. Um zufällige Fehler durch die Standardwerte zu vermeiden, werden wir das zusätzlich prüfen, und so den Zeitfilter nur erstellen, wenn die Endzeit größer als Null und größer als die Startzeit ist. Der Code dafür kommt in die Funktion OnInit() des EAs:

if (time_range_enabled && time_range_end>0 && time_range_end>time_range_start)
 {
     CTimeRange *timerange = new CTimeRange(time_range_start,time_range_end);
     time_filters.Add(GetPointer(timerange));
 }    

Im Code oben findet sich auch die Speicherung der Pointer auf den Container der Zeitfilter.

Für den Zeitfilter der Handelstage, über geben wir 7 Parameter, jeder für einen Wochentag. Wieder haben wir einen Parameter, um diese Funktion an- oder abzuschalten:

input bool time_days_enabled = true;
input bool sunday_enabled = false;
input bool monday_enabled = true;
input bool tuesday_enabled = true;
input bool wednesday_enabled = true;
input bool thursday_enabled = true;
input bool friday_enabled = false;
input bool saturday_enabled = false;

In OnInit() wird auch eine neue Instanz des Zeitfilters erstellt, und diese wird auch im Container gespeichert, wenn diese Funktion angeschaltet wurde:

if (time_days_enabled)
 {
    CTimeDays *timedays = new CTimeDays(sunday_enabled,monday_enabled,tuesday_enabled,wednesday_enabled,thursday_enabled,friday_enabled,saturday_enabled);
    time_filters.Add(GetPointer(timedays));
 }

Der Timer benötigt hingegen nur einen Parameter, es ist die Gesamtzeit des Filter vor dem Ablauf und wieder die Funktion des An- und Abschaltens:

input bool timer_enabled= true;
input int timer_minutes = 10080;

Ähnlich den anderen, oben besprochenen Filtern erstellen wir eine neue Instanz von CTimer und speichern dessen Pointer im Container, wenn die Funktion dafür angeschaltet wurde:

if(timer_enabled)
  {
   CTimer *timer=new CTimer(timer_minutes*60);
   timer.TimeStart(TimeCurrent());
   time_filters.Add(GetPointer(timer));
  }

Der Zeitfilter innerhalb eines Tages ist komplexer, da wir die Möglichkeiten des EAs in folgenden Szenarien überprüfen wollen:

  1. Zeitfilterung, wenn Start- und Endzeit auf denselben Tag fallen
  2. Zeitfilterung, wenn Start- und Endzeit nicht auf denselben tag fallen
  3. Mehrere Instanzen von CTimeFilter

Die Szenarien #1 & #2 können mit den gleichen Set von Parametern demonstriert werden. Wenn die Startzeit kleiner ist als die Endzeit (Szenario #1), tauschen wir einfach die beiden Zeitpunkte und erhalte so Szenario #2. Für #3 benötigen wir jedoch zwei weitere Instanzen, vorzugsweise mit unterschiedlichen Werten, und daher brauchen wir mindestens zwei Sets von Start- und Endzeiten innerhalb der 24-Stunden Zeitspanne. Dafür deklarieren wir zuerst eine eigene Enumeration mit drei möglichen Einstellungen: Deaktiviert, Szenario #1/#2, und Szenario #3 :

enum ENUM_INTRADAY_SET 
  {
   INTRADAY_SET_NONE=0,
   INTRADAY_SET_1,
   INTRADAY_SET_2
  };

Dann deklarieren wir die Parameter wie folgt:

input ENUM_INTRADAY_SET time_intraday_set=INTRADAY_SET_1;
input int time_intraday_gmt=0;
// 1. Set
input int intraday1_hour_start=8;
input int intraday1_minute_start=0;
input int intraday1_hour_end=17;
input int intraday1_minute_end=0;
// 2. Set
input int intraday2_hour1_start=8;
input int intraday2_minute1_start=0;
input int intraday2_hour1_end=12;
input int intraday2_minute1_end=0;
// 3. Set
input int intraday2_hour2_start=13;
input int intraday2_minute2_start=0;
input int intraday2_hour2_end=17;
input int intraday2_minute2_end=0;
Für die Initialisierung der Zeitfilter verwenden wir die Anweisung 'switch'. Wenn time_intraday_set die Einstellung von INTRADAY_SET_1 zugewiesen wurde, dann initialisieren wir eine einzige Instanz von CTimeFilter mit dem ersten Set der Parameter. Wenn aber die Einstellung INTRADAY_SET_2 ist, erstellen wir zwei verschiedene Instanzen von CTimeFilter mit dem 2. und 3. Set der Parameter:
switch(time_intraday_set)
  {
   case INTRADAY_SET_1:
     {
      CTimeFilter *timefilter=new CTimeFilter(time_intraday_gmt,intraday1_hour_start,intraday1_hour_end,intraday1_minute_start,intraday1_minute_end);
      time_filters.Add(timefilter);
      break;
     }
   case INTRADAY_SET_2:
     {
      CTimeFilter *timefilter=new CTimeFilter(0,0,0);
      timefilter.Reverse(true);
      CTimeFilter *sub1 = new CTimeFilter(time_intraday_gmt,intraday2_hour1_start,intraday2_hour1_end,intraday2_minute1_start,intraday2_minute1_end);
      CTimeFilter *sub2 = new CTimeFilter(time_intraday_gmt,intraday2_hour2_start,intraday2_hour2_end,intraday2_minute2_start,intraday2_minute2_end);
      timefilter.AddFilter(sub1);
      timefilter.AddFilter(sub2);
      time_filters.Add(timefilter);
      break;
     }
   default: break;
  }

Nach dem Code für die Initialisierung der Klassen für die Zeitfilter, initialisieren wir den Container der Zeitfilter, CTimes. Zuerst fragen wir nach dem Pointer auf den Symbolmanager (wird in diesem Beispiel nicht benötigt, aber könnte bei einer Erweiterung der Zeitfilter wichtig werden), und dann prüfen wir die Einstellungen:

time_filters.Init(GetPointer(symbol_manager));
if(!time_filters.Validate())
  {
   Print("one or more time filters failed validation");
   return INIT_FAILED;
  }

Gehen wir jetzt weiter zu den Testergebnissen des Expert Advisors. Die Tests im Strategietester für den ganzen Monat Januar 2017 durchgeführt, aber der EA handelt nur in der ersten Woche.

Die Testergebnisse aus der Ausführung des Expert Advisors mit einer Zeitspanne als Zeitfilter findet sich am Ende des Artikels (tester_time_range.html). In diesem Test beginnt die Zeitspanne mit dem Beginn des Jahres 2017 und endet am ersten Freitag des Monats, 6. Januar 2017. Daher könne wir sagen, der Filter arbeitet wenn der Expert Advisor nach den Endzeitpunkt keine weiteren Positionen eröffnet. Ein Bildschirmfoto der letzten Position ist unten zu sehen:

Letzte Positionen der Zeitspanne

Die letzte Position des Tests wurde am 6. 1. um 15:00 eröffnet, das ist innerhalb der festgelegten Beschränkungen für den Expert Advisor. Bedenken Sie, dass die Position auch noch in der nächsten Woche offengeblieben ist, welches durch aus so geplant ist, da der Filter nur die Eröffnung von Positionen betrifft. Die gepunktete Vertikale zeigt die letzte Kerze der Woche.

Zur Filterung der Tage, wurde dessen Parameter mit einem 'true' aktiviert. Ebenso bleibt der Datumsfilter aktiviert, aber der Filter für Freitag is deaktiviert. Die letzte Position des vorherigen Tests zeigt, dass diese am 6. 1. (Freitag) eröffnet wurde. Daher, wenn wir sehen, dass speziell diese Position nicht mehr eröffnet wird, bestätigt das uns, dass dieser Zeitfilter arbeitet. Diese Testergebnisse (tester_time_days.html) sind auch am Ende des Artikels beigefügt. Ein Bildschirmfoto der letzten Position ist unten zu sehen:

Letzte Positionen der Zeitspanne


Wie das Bildschirmfoto zeigt, wurde die letzte Position am 5. 1. eröffnet, statt wie vorher am 6. Januar. Mit der neuen Konfiguration wird die vorletzte Position zur letzten in diesem Test. Der Ausstieg stimmt mit der zweiten Vertikalen überein, die auch der Einstig der letzten Position im vorherigen Test war.

Für den Timer, wie bereits beschrieben, verwenden wir einen alternativen Konstruktor von CTimer, der nur ein Argument verlangt. Deren Wert wird in der Klassenvariablen m_total in Form der die Anzahl der Sekunden gespeichert, so dass der Filter vor dem Ablauf ein 'true' zurückgibt. Da der Wert in Sekunden gespeichert wird, müssen wir den Eingabeparameter mit 60 multiplizieren, damit er als Sekundenanzahl gespeichert werden kann. 10080 ist der Standardwert in Minuten des Expert Advisors, welches eine Zeitspanne von 1 Woche darstellt. Daher, wenn wir den ersten Zeitfilter mit diesem kombinieren, sollte das Testergebnis dieses Filters identisch mit dem des ersten. Die Testergebnisse sind tatsächlich identisch, auch sie (tester_timer.html) sind am Ende des Artikels beigefügt.

Der letzte Zeitfilter, CTimeFilter, verfügt über drei verschiedene Fälle, die wir alle drei testen müssen. Da der EA seine Positionen so lange offen hält, bis er gezwungen ist, diese Position zu schließen, um eine neue zu öffnen, um dann auch die wieder für eine neue zu schließen, und so weiter. Eine Ausnahme entsteht nur, wenn einer oder mehrere Zeitfilter ein 'false' zurückgeben. Wenn daher eine Position während eines Tages ausgelassen wurde, wurde der EA durch einen Zeitfilter an der Eröffnung einer neuen Position gehindert. Das gesamte Testergebnis ohne Zeitfilter (tester_full.html) findet sich am Ende des Artikels.

Für das Szenario #1 des in diesem Artikel beschrieben Zeitfilters während eines Tages beträgt die Startzeit 08:00 und die Endzeit 17:00. Die aller erste Position des Tests ohne Filter wurde gleich zu Beginn des Tests eröffnet, während der ersten Stunde (00:00). Das aber ist außerhalb der erlaubten Zeitspanne des ersten Szenarios. Mit diesen Einstellungen sollte der EA diese Position nicht eröffnen, sondern erste die nächste, die in die vom Zeitfilter erlaubte Zeitspanne fällt. Das Testergebnis mit diesen Einstellungen (tester_time_hour1.html) findet sich am Ende dieses Artikels, und das Bildschirmfoto unten zeigt die erste Position:


Stundenfilter, Positionen 3

Wie erwartet hat der EA die Position zu Beginn des Tests ausgelassen. Er wartet, bis die Zeitspanne während des Tages erreicht wurde. Die gepunktete Vertikale zeigt den Anfang der erlaubten Zeitspanne, wo sich auch die Eröffnung der ersten Position (ohne Zeitfilter) befindet.

Für das zweite Szenario tauschen wir einfach Start- und Endzeit, so dass die Startzeit 17:00 beträgt und die Endzeit 08:00. Im vollständigen Test (ohne Filter) finden wir die erste Position, die nicht in die Zeitspanne fällt, am 3.1. um 10:00. 10:00 ist nach 08:00 und früher als 17:00, und so können wir sicher sein, diese Position liegt außerhalb der erlaubten Zeitspanne. Das Testergebnis unter Verwendung dieser Einstellungen (tester_time_hour2.html) findet sich am Ende des Artikels, und das Bildschirmfoto ist unten angezeigt:


Stundenfilter 2, Positionen

Wie man auf dem Bildschirmfoto sieht, schließt der EA die vorherige Position, aber er eröffnet keine neue. Die gepunktete Vertikale zeigt den Beginn der neuen Handelszeit. Der EA eröffnet die erste Position der neuen Handelszeit 3 Kerzen nach dem Beginn dieser Handelszeit.

Für das dritte Szenario konfigurieren wir den EA mit den Einstellungen des ersten Szenarios mit der Ausnahme der Mittagszeit. Es sollte daher keine Position während der 12:00 Kerze eröffnet werden und der EA sollte seine Handelstätigkeit mit dem Beginn der Kerze von 13:00 wieder aufnehmen. Im vollständigen Test (ohne Zeitfilter), sehen wir eine Position, die am 9. 1. um 12:00 eröffnet wurde. Da diese Position auf 12:00 fällt, sollte der EA diese Position mit den Einstellungen dieses Szenarios nicht eröffnen. Das Testergebnis mit diesen Einstellungen (tester_time_hour3.html) findet sich am Ende des Artikels, und das Bildschirmfoto ist unten angezeigt:


Stundenfilter 3, Positionen

Wie das Bildschirmfoto zeigt, wird die offenen Position während der 12:00-Kerze geschlossen, aber eine neue wird nicht eröffnet. Der EA macht wie erwartet eine einstündige Pause. Die einzige Position (Kaufen) wird um 16:00 eröffnet, 3 Stunden nach dem Beginn der Handelszeit des Nachmittags und eine Stunde vor deren Ende, denn das ist der früheste Zeitpunkt für den EA, eine Position nach der Umkehrung des Handelssignals zu eröffnen.

Schlussfolgerung

In diesem Artikel beschreiben wir die Implementierung verschiedener Methoden, die Zeit zu filtern für einen Cross-Plattform Expert Advisor. Der Artikel zeigt die Verwendung einer Reihe von Zeitfilter, so wie die Art, wie die Zeitfilter miteinander durch den Container der Filter kombiniert werden können, so dass diese Funktionen für einen Expert Advisor an- und ausgeschaltet werden können, je nach Einstellungen der Zeitfilter.