Universeller Expert Advisor: CUnIndicator und das Arbeiten mit Pending Orders (Teil 9)

Vasiliy Sokolov | 23 Oktober, 2017


Inhaltsverzeichnis

Einleitung

Die Quellcode-Bibliothek des Universellen Expert Advisors wird weiterentwickelt. Die meisten Methoden, die in die CStrategy Engine einprogrammiert sind, haben ihre Effektivität, Benutzerfreundlichkeit und Einfachheit in der Praxis bewiesen. Jedoch musste man im Laufe der Nutzung einige Aspekte der Arbeit des Experten revidieren.

Ein solcher Aspekt ist die Arbeit mit Indikatoren. Im dritten Artikel dieser Serie wurde dafür eine klassische objektorientierte Methode vorgeschlagen. Die Grundidee bestand darin, dass jeder Indikator eine objektorientierte Klasse mit eigenen Methoden für das Setzen und Erhalten dieser oder jener Eigenschaften darstellt. Jedoch ist eine eigene Wrapper-Klasse für jeden Indikator in der Praxis schwer zu implementieren. Hier wird eine neue objektorientierte Arbeitsweise für Indikatoren betrachtet, die keine Erstellung separater Modul-Klassen erfordert.

Die zweite Veränderung, die in diesem Artikel beschrieben wird, ist mit der Einführung einer kompletten Verwaltung von Pending Orders verbunden. Wenn man früher Pendig Orders unmittelbar im Code der gesamten Strategie verwalten musste, wird jetzt ein Teil solcher Funktionen der CStrategy Engine zugewiesen. Jetzt kann die Strategie-Klasse die Methoden SupportBuyPending und SupportSellPending neu definieren und beginnen, Pending Orders analog wie aktive Positionen zu verwalten.

Zugriff auf Indikatoren in früheren Versionen von CStrategy

Um die Problematik der Frage zu verstehen, wenden wir uns der Lösung für die Arbeit mit Indikatoren aus dem oben erwähnten dritten Artikel der Serie zu. Es wurde vorgeschlagen, mit allen Indikatoren über eine Wrapper-Klasse zu arbeiten. So wurde in einem Beispiel die Wrapper-Klasse CIndMovingAverage für die Arbeit mit dem iMA Indikator verwendet. Die Klasse CIndMovingAverage bestand aus den Methoden, die diese oder jene Eigenschaft des Indikators entweder setzte oder zurückgab, sowie aus der Methode Init, die die Systemfunktion iMA aufrief. Obwohl die Klasse des Indikators eine einfache Struktur hatte, hatte er einen umfangreichen Code, den der Nutzer selbst implementieren sollte. Führen wir den Quellcode dieser Klasse hier an, um das Arbeitspensum einzuschätzen:

//+------------------------------------------------------------------+
//|                                                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;         // Handle des Indikators
   ENUM_TIMEFRAMES   m_timeframe;         // Timeframe
   int               m_ma_period;         // Periode
   int               m_ma_shift;          // Verschiebung
   string            m_symbol;            // Symbol
   ENUM_MA_METHOD    m_ma_method;         // Methode des gleitenden Durchschnitts
   uint              m_applied_price;     // Der Handle des Indikators, zu welchen die Berechnung des gleitenden Durchschnitts durchzuführen ist,
                                          // 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);
  };
//+------------------------------------------------------------------+
//| Standardmäßiger Konstruktor                                      |
//+------------------------------------------------------------------+
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();
  }
//+------------------------------------------------------------------+
//| Initialisierung                                                  |
//+------------------------------------------------------------------+
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);
     }
  }
//+------------------------------------------------------------------+
//| Setzt die 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;
  }
//+------------------------------------------------------------------+
//| Legt 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 Symbol zurück, für welchen der Indikator berechnet wird |
//+------------------------------------------------------------------+
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;
  }

Sie werden wohl zustimmen, dass der Code sehr umfangreich ist. Und das ist nur ein Beispiel für einen nicht komplizierten Indikator. Dieser Ansatz wird auch dadurch kompliziert, dass es tausende verschiedene, sowohl standardmäßige als auch benutzerdefinierte Indikatoren für MetaTrader gibt. Jeder Indikator hat einen verhältnismäßig einzigartigen Satz von Eigenschaften und Parametern. Wenn man sich an den vorgeschlagenen Ansatz hält, muss man für jeden solchen Indikator einen eigenen Wrapper schreiben. 

In CStrategy kann man Indikatoren direkt aufrufen, ohne irgendwelche Klassen zu verwenden. In der Praxis rief ich deshalb oft eine konkrete Systemfunktion im Code einer Handelsstrategie direkt auf. Denn es ist viel einfacher, einen Indikator durch Standardmethoden aufzurufen, als Zeit für das Schreiben einer Klasse zu vergeuden.

Die IndicatorCreate Funktion — die Grundlage des universellen Interfaces

Eine Lösung wurde, wie es oft passiert, nach einer praktischen Erfahrung mit CStrategy gefunden. Es wurde offensichtlich, dass der Zugriffsmechanismus auf einen Indikator über die folgenden Eigenschaften verfügen muss:

  • Universalität. Der Zugriff auf jeden Indikator muss auf einem verallgemeinerten Zugriffsverfahren basieren, statt eine Vielzahl von Wrapper-Klassen zu verwenden.
  • Bequemlichkeit und Einfachheit. Der Zugriff auf die Werte eines Indikators muss einfach sein und nicht vom Typ des Indikators abhängen.

In der Praxis hat es sich erwiesen, dass den einfachsten und universellsten Zugriff normalerweise die Funktionen iCustom und IndicatorCreate ermöglichen.

Beide Funktionen erlauben, sowohl standardmäßige, als auch benutzerdefinierte Indikatoren zu erstellen. Die Funktion iCustom erfordert die Angabe der Parameter eines Indikators im Moment der Kompilation des Programms. Die Funktion IndicatorCreate ist anders. Als Parameter erhält sie den Array der Strukturen MqlParams. Gerade das Format der zweiten Funktion ermöglicht es, einen allgemeinen Zugriff auf einen beliebigen Indikator zu sichern. Dabei braucht man die Parameter des Indikators nicht im Voraus zu wissen, dadurch wird das Zugriffsverfahren wirklich universell.

Schauen wir uns ein konkretes Beispiel für das Arbeiten mit IndicatorCreate an. Erstellen wir mithilfe dieser Funktion den Handle des MovingAverage Indikators. Das ist der gleiche Indikator, den die Funktion iMa zurückgibt:

//+------------------------------------------------------------------+
//|                                                        Test2.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   // Erhalten des Handles des Indikators mithilfe von iMA
   int h_ima = iMA(Symbol(), Period(), 13, 0, MODE_SMA, PRICE_CLOSE); 
   // Erhalten des gleichen Handles des Indikators mithilfe von IndicatorCreate
   MqlParam ma_params_1[4];
   ma_params_1[0].type = TYPE_INT;
   ma_params_1[0].integer_value = 13;
   ma_params_1[1].type = TYPE_INT;
   ma_params_1[1].integer_value = 0;
   ma_params_1[2].type = TYPE_INT;
   ma_params_1[2].integer_value = MODE_SMA;
   ma_params_1[3].type = TYPE_INT;
   ma_params_1[3].integer_value = PRICE_CLOSE;
   int h_ma_c = IndicatorCreate(Symbol(), Period(), IND_MA, 4, ma_params_1);
   if(h_ima == h_ma_c)
      printf("Die Handles der Indikatoren sind gleich");
   else
      printf("Die Handles der Indikatoren sind nicht gleich");
}

Das angeführte Skript bekommt den Zugriff auf einen und denselben Indikator über zwei verschiedene Schnittstellen: iMA und IndicatorCreate. Beide Funktionen geben einen und denselben Handle zurück, was man sich leicht vergewissern kann. Beim Start des Indikators wird die Meldung "Die Handles der Indikatoren sind gleich" ausgegeben. Jedoch ist der Zugriff auf den Indikator über IndicatorCreate mit einer aufwendigen Konfiguration des MqlParams Arrays verbunden. Jedem Element von MqlParam müssen zwei Eigenschaften zugewiesen werden: der Typ der Variablen und deren Wert. Vor allem wegen dieser Umständlichkeit wird die Funktion IndicatorCreate selten verwendet. Aber gerade diese Schnittstelle ermöglicht den Zugriff auf jeden MQL-Infikator. Deshalb werden wir ihn verwenden.

CUnIndicator — der universelle Indikator von CStrategy

Dank der objektorientierten Programmierung können wir den größten Teil der Konfiguration der Elemente des Arrays MqlParams für den Nutzer ausblenden, und ihm eine bequeme Schnittstelle für die Installation eines beliebigen Parameters anbieten. Erstellen wir CUnIndicator — eine Wrapper-Klasse für die Funktion IndicatorCreate. Mithilfe des Wrappers kann man eine beliebige Anzahl der Parameter für den Indikator setzen. Dabei muss der Typ des Parameters nicht angegebenen werden. Dank Templates wird der übergebene Typ automatisch erkannt. Darüber hinaus wird unsere Klasse praktische Indexe in Form von eckigen Klammern '[]' haben, innerhalb deren man sowohl den Index eines Indikatorwertes, als auch die Zeit, wann dieser Wert erhalten werden muss, angeben kann. 

Die Arbeit mit CUnIndicator besteht aus folgenden Schritten.

  • Setzen der gewünschten Parameter mithilfe der Methode SetParameter
  • Erstellen des Indikators mithilfe der Methode Create
  • Setzen des gewünschten Puffers über SetBuffer (fakultativ)
  • Zugriff auf den Wert des Indikators nach einem zufälligen Index i über den Index []
  • Löschen des Indikators mihilfe der Methode IndicatorRealise (fakultativ).

Schreiben wir ein kleines Skript, das den Moving Average Indikator mithilfe von CUnIndicator erstellt:

//+------------------------------------------------------------------+
//|                                                        Test2.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Strategy\Indicators.mqh>
CUnIndicator UnMA;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   // Erhalten des Handles des Indikators mithilfe von iMA
   int h_ima = iMA(Symbol(), Period(), 13, 0, MODE_SMA, PRICE_CLOSE); 
   // Erhalten des Handles des Indikators mithilfe von CUnIndicator
   UnMA.SetParameter(13);
   UnMA.SetParameter(0);
   UnMA.SetParameter(MODE_SMA);
   int handle = UnMA.Create(Symbol(), Period(), "Examples\\Custom Moving Average");
}

Nun enthält die Variable 'handle' den Handle des erstellten Indikators, und das Objekt UnMA ermöglicht die Arbeit mit den Werten des Indikators. Zum Beispiel, um den Wert des Indikators auf der vorherigen Bar zu erhalten, reicht es, den folgenden Code zu schreiben:

double value = UnMA[1];

Schauen wir uns ein komplizierteres Beispiel an. Erstellen wir einen Indikator, der mehrere Puffer beinhaltet, z.B. den Standard MACD. Versehen wir jede Zeile mit ausführlichen Kommentaren:

//+------------------------------------------------------------------+
//|                                                        Test2.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Strategy\Indicators.mqh>
input int FastEMA = 12;
input int SlowEMA = 26;
input int SignalSMA = 9;

CUnIndicator UnMACD;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   UnMACD.SetParameter(FastEMA);                            // Festlegen des Wertes des schnellen gleitenden Durchschnitts
   UnMACD.SetParameter(SlowEMA);                            // Setzen des Wertes des langsamen gleitenden Durchschnitts
   UnMACD.SetParameter(SignalSMA);                          // Setzen der Periode der Signallinie
   int handle = UnMACD.Create(Symbol(), Period(),           // Erstellen des Indikators, Festlegen des Symbols und der Timeframe
                "Examples\\MACD", PRICE_CLOSE);
   UnMACD.SetBuffer(MAIN_LINE);                             // Setzen des standarmäßigen Puffers - MACD-Werte
   double macd = UnMACD[1];                                 // Erhalten des MACD-Wertes auf dem vorherigen Balken
   UnMACD.SetBuffer(SIGNAL_LINE);                           // Setzen der Signallinie als standardmäßigen Puffer
   double signal = UnMACD[1];                               // Erhalten des Wertes der Signallinie auf dem vorherigen Balken
   datetime time_span = TimeCurrent() - PeriodSeconds();    // Berechnen des Zeitpunktes der Öffnung des vorherigen Balkens
   double signal_by_time = UnMACD[time_span];               // Erhalten des Wertes der Signallinie für den Zeitpunkt
   printf("MACD: " + DoubleToString(macd, Digits()) +       // Ausgeben der Werte des MACD und der Signallinie auf dem vorherigen Balken
         "; Signal: " + DoubleToString(signal, Digits()));
   if(signal == signal_by_time)                             // Der Zugriff nach Zeit und nach Index wird gleich sein
      printf("Die Werte, die nach Index und nach Zeit erhalten wurden, sind gleich ");
}
//+------------------------------------------------------------------+

Der interessanteste Moment in diesem Beispiel ist die Umschaltung der internen Puffer des Indikators mit Hilfe der Methode SetBuffer. So werden sich die Werte, die UnMACD[1] zurückgibt, je nach aktuell gesetztem Puffer voneinander unterscheiden. Zuerst gibt UnMACD[1] die MACD-Werte auf dem vorherigen Balken zurück. Aber wenn man SIGNAL_LINE als Puffer standardmäßigt setzt, gibt UnMACD[1] den Wert der Signal-Linie zurück.

Der Zugriff auf die Werte des Indikators ist sowohl nach Index, als auch nach Zeit möglich. Im Beispiel wird die Zeit time_span berechnet, die gleich der Eröffnung des vorherigen Balkens ist. Wenn man diese Zeit statt des Index UnMACD angibt, wird der gleiche Wert wie bei UnMACD[1] zurückgegeben.

Innere Struktur von CUnIndicator

Es ist an der Zeit, die innere Struktur von CUnIndicator zu betrachten. Der Quellcode dieser Klasse sieht wie folgt aus:

//+------------------------------------------------------------------+
//|                                                   Indicators.mqh |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include "Message.mqh"
#include "Logs.mqh"
//+------------------------------------------------------------------+
//| Basisklasse des Indikators                                       |
//+------------------------------------------------------------------+
class CUnIndicator
{
private:
   MqlParam m_params[];
   int      m_params_count;
   int      m_current_buffer;
   int      m_handle;
   static   CLog*    Log;
   bool     m_invalid_handle;
   void     PushName(string name);
public:
            CUnIndicator(void);
   void     SetBuffer(int index);
   template <typename T>
   bool     SetParameter(T value);
   int      Create(string symbol, ENUM_TIMEFRAMES period, string name);
   int      Create(string symbol, ENUM_TIMEFRAMES period, string name, int app_price);
   int      Create(string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR ind_type);
   int      Create(string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR ind_type, int app_price);
   void     InitByHandle(int handle);
   void     IndicatorRelease(void);
   double   operator[](int index);
   double   operator[](datetime time);
   int      GetHandle(void);
};
CLog        *CUnIndicator::Log;
//+------------------------------------------------------------------+
//| Initialisierung ohne Angabe des Namens                           |
//+------------------------------------------------------------------+
CUnIndicator::CUnIndicator(void) : m_params_count(0),
                                   m_handle(INVALID_HANDLE),
                                   m_current_buffer(0),
                                   m_invalid_handle(false)
{
   Log = CLog::GetLog(); 
}

//+------------------------------------------------------------------+
//| Deinitialisierung des Indikators                                 |
//+------------------------------------------------------------------+
CUnIndicator::IndicatorRelease(void)
{
   if(m_handle != INVALID_HANDLE)
      IndicatorRelease(m_handle);
   ArrayResize(m_params, 1);
   m_params_count = 1;
   m_current_buffer = 0;
   m_handle = INVALID_HANDLE;
}

template <typename T>
bool CUnIndicator::SetParameter(T value)
{
   
   string type = typename(value);
   MqlParam param;
   if(type == "string")
   {
      param.type = TYPE_STRING;
      param.string_value = (string)value;
   }
   else if(type == "int")
   {
      param.type = TYPE_INT;
      param.integer_value = (long)value;
   }
   else if(type == "double")
   {
      param.type = TYPE_DOUBLE;
      param.double_value = (double)value;
   }
   else if(type == "bool")
   {
      param.type = TYPE_BOOL;
      param.integer_value = (int)value;
   }
   else if(type == "datetime")
   {
      param.type = TYPE_DATETIME;
      param.integer_value = (datetime)value;
   }
   else if(type == "color")
   {
      param.type = TYPE_COLOR;
      param.integer_value = (color)value;
   }
   else if(type == "ulong")
   {
      param.type = TYPE_ULONG;
      param.integer_value = (long)value;
   }
   else if(type == "uint")
   {
      param.type = TYPE_UINT;
      param.integer_value = (uint)value;
   }
   else
   {
      param.type = TYPE_INT;
      param.integer_value = (int)value;
   }
   m_params_count++;
   if(ArraySize(m_params) < m_params_count)
      ArrayResize(m_params, m_params_count);
   m_params[m_params_count-1].double_value = param.double_value;
   m_params[m_params_count-1].integer_value = param.integer_value;
   m_params[m_params_count-1].string_value = param.string_value;
   m_params[m_params_count-1].type = param.type;
   return true;
}
//+------------------------------------------------------------------+
//| Gibt den Handle des Indikators zurück                            |
//+------------------------------------------------------------------+
int CUnIndicator::GetHandle(void)
{
   return m_handle;
}
//+------------------------------------------------------------------+
//| Legt den aktuellen Puffer des Indikators fest                    |
//+------------------------------------------------------------------+
void CUnIndicator::SetBuffer(int index)
{
   m_current_buffer = index;
}
//+-------------------------------------------------------------------------+
//| Initialisiert den Indikator (erstellt seinen Handle). Gibt den Handle   |
//| des Indikators wenn erfolgreich zurück oder INVALID_HANDLE,             |
//| wenn der Indikator nicht erstellt werden konnte                         |
//+-------------------------------------------------------------------------+
int CUnIndicator::Create(string symbol, ENUM_TIMEFRAMES period, string name)
{
   PushName(name);
   m_handle = IndicatorCreate(symbol, period, IND_CUSTOM, m_params_count, m_params);
   if(m_handle == INVALID_HANDLE && m_invalid_handle == false)
   {
      string text = "CUnIndicator '" + m_params[0].string_value + "' was not created. Check it's params. Last error:" + (string)GetLastError();
      CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text);
      Log.AddMessage(msg);
      m_invalid_handle = true;
   }
   return m_handle;
}
//+-------------------------------------------------------------------------+
//| Initialisiert den Indikator (erstellt seinen Handle). Gibt den Handle   |
//| des Indikators wenn erfolgreich zurück oder INVALID_HANDLE,             |
//| wenn der Indikator nicht erstellt werden konnte                         |
//+-------------------------------------------------------------------------+
int CUnIndicator::Create(string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR ind_type)
{
   if(ind_type == IND_CUSTOM)
   {
      string text = "CUnIndicator '" + m_params[0].string_value + "' was not created. Indicator type can not be IND_CUSTOM";
      CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text);
      Log.AddMessage(msg);
      m_invalid_handle = true;
      return INVALID_HANDLE;
   }
   m_handle = IndicatorCreate(symbol, period, ind_type, m_params_count, m_params);
   if(m_handle == INVALID_HANDLE && m_invalid_handle == false)
   {
      string text = "CUnIndicator '" + m_params[0].string_value + "' was not created. Check it's params. Last error:" + (string)GetLastError();
      CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text);
      Log.AddMessage(msg);
      m_invalid_handle = true;
   }
   return m_handle;
}
//+-------------------------------------------------------------------------+
//| Initialisiert den Indikator (erstellt seinen Handle). Gibt den Handle   |
//| des Indikators wenn erfolgreich zurück oder INVALID_HANDLE              |
//| wenn der Indikator nicht erstellt werden konnte                         |
//+-------------------------------------------------------------------------+
int CUnIndicator::Create(string symbol,ENUM_TIMEFRAMES period,ENUM_INDICATOR ind_type,int app_price)
{
   SetParameter(app_price);
   return Create(symbol, period, ind_type);
}
//+-------------------------------------------------------------------------+
//| Platziert den Namen des Indikators am Null-Index des m_params[] Arrays  |
//+-------------------------------------------------------------------------+
void CUnIndicator::PushName(string name)
{
   int old_size = ArraySize(m_params);
   int size = ArrayResize(m_params, ArraySize(m_params) + 1);
   for(int i = 0; i < old_size; i++)
      m_params[i+1] = m_params[i];
   m_params[0].type = TYPE_STRING;
   m_params[0].string_value = name;
}
//+-------------------------------------------------------------------------+
//| Initialisiert den Indikator (erstellt seinen Handle). Gibt den Handle   |
//| des Indikators wenn erfolgreich zurück oder INVALID_HANDLE,             |
//| wenn der Indikator nicht erstellt werden konnte                         |
//+-------------------------------------------------------------------------+
int CUnIndicator::Create(string symbol, ENUM_TIMEFRAMES period, string name, int app_price)
{
   SetParameter(app_price);
   return Create(symbol, period, name);
}
//+-----------------------------------------------------------------------------+
//| Initialisiert die Klasse des Indikators basierend auf dem existierenden     |
//| Handle des Indikators                                                       |
//+-----------------------------------------------------------------------------+
void CUnIndicator::InitByHandle(int handle)
{
   this.IndicatorRelease();
   m_handle = handle;
}
//+------------------------------------------------------------------+
//| Gibt den Wert des MA mit dem Index 'index' zurück                |
//+------------------------------------------------------------------+
double CUnIndicator::operator[](int index)
{
   double values[];
   if(m_handle == INVALID_HANDLE)
      return EMPTY_VALUE;
   if(CopyBuffer(m_handle, m_current_buffer, index, 1, values) == 0)
   {
      string text = "Failed copy buffer of indicator. Last error: " + (string)GetLastError();
      CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text);
      Log.AddMessage(msg);
      return EMPTY_VALUE;
   }
   return values[0];
}
//+------------------------------------------------------------------+
//| Gibt den Wert des Indikators zum Zeitpunkt 'time' zurück         |
//+------------------------------------------------------------------+
double CUnIndicator::operator[](datetime time)
{
   double values[];
   if(m_handle == INVALID_HANDLE)
      return EMPTY_VALUE;
   
   if(CopyBuffer(m_handle, m_current_buffer, time, 1, values) == 0)
   {
      string text = "Failed copy buffer of indicator. Last error: " + (string)GetLastError();
      CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text);
      Log.AddMessage(msg);
      return EMPTY_VALUE;
   }
   return values[0];
}

Im Quellcode sieht man, dass die Methode SetParameter eine Template-Methode ist. Sie erhält das universelle Argument T als Inputparameter; je nach Typ des Arguments wird der Typ des Parameters ENUM_DATATYPE ausgewählt. Dieser Parameter wird für die Struktur MqlParam gesetzt. Multiple String-Prüfungen sind von der Geschwindigkeit her nicht optimal, sie beeinflussen aber nicht die Leistung, denn die Funktion SetParameter muss nur einmal aufgerufen werden — bei der Initialisierung des Expert Advisors.

Die Klasse hat zahlreiche Varianten der Methode mCreate. Aus diesem Grund kann man sowohl benutzerdefinierte Indikatoren (unter Angabe des String-Namens des Indikators), als auch Standard-Indikatoren erstellen, deren Typ über INDICATOR_TYPE gesetzt werden kann. Zum Beispiel kann man einen gleitenden Durchschnitt als benutzerdefinierten Indikator wie folgt erstellt:

UnMA.Create(Symbol(), Period(), "Examples\\Custom Moving Average");

Hier ist UnMA eine Instanz von CUnIndicator. Die Erstellung des gleichen Indikators in Form eines standardmäßigen Indikators geht etwas anders:

UnMA.Create(Symbol(), Period(), IND_MA);

Die Klasse CUnIndicator beinhaltet die Methode InitByHandle. Gehen wir darauf ausführlicher ein. Es ist bekannt, dass viele Indikatoren nicht nur anhand der Preise eines Symbols, sondern auch anhand der Daten eines anderen Indikators berechnet werden können. Deswegen kann man eine Kette von Indikatoren erstellen, die ihre Werte basierend auf den Ausgabewerten des vorhergehenden Indikators berechnen. Nehmen wir an, wir müssen die Werte des Stochastic anhand des gleitenden Durchschnitts berechnen. Dafür muss man zwei Indikatoren initialisieren: einen für die Berechnung des gleitenden Durchschnitts, den anderen — für die Berechnung des Stohastic. Man kann das auf folgende Weise machen:

//+------------------------------------------------------------------+
//|                                                        Test3.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Strategy\Indicators.mqh>
input int MaPeriod = 12;
input int StochK = 3;
input int StochD = 12;
input int StochSmoth = 3;

CUnIndicator UnSMA;
CUnIndicator UnStoch;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   // Parameter festlegen und den SMA Indikator erstellen
   UnSMA.SetParameter(12);
   UnSMA.SetParameter(0);
   UnSMA.SetParameter(MODE_SMA);
   UnSMA.SetParameter(PRICE_CLOSE);
   int sma_h = UnSMA.Create(Symbol(), Period(),
      "\\Examples\Custom Moving Average");
   // Parameter festlegen und den Stoch Indikator
   // basierend auf den SMA-Daten erstellen
   UnStoch.SetParameter(StochK);
   UnStoch.SetParameter(StochD);
   UnStoch.SetParameter(StochSmoth);
   UnStoch.InitByHandle(sma_h);
   double stoch_on_sma = UnStoch[1];
}
//+------------------------------------------------------------------+

Aus dem oben angeführten Code geht hervor, dass der Handle des Indikators, der durch die Methode Create erstellt wurde, wird für die Erstellung des Stochastic Indikators gespeichert und verwendet. Wenn ein benutzerdefinierter Indikator verwendet wird, muss die Datenquelle nicht angegebenen werden. Für Systemindikatoren muss jedoch die Datenquelle angegeben werden. Das kann man auf zwei Arten tun: durch die Methode SetParameter:

UnMA.SetParameter(PRICE_CLOSE);

oder durch eine überladene Version der Methode Create:

UnMA.Create(Symbol(), Period(), IND_MA, PRICE_CLOSE);

Später erstellen wir ein Demo-Handelssystem, das auf die Werte des Indikators über die Klasse CUnIndicator zugreift.

Verbesserte Arbeit mit Pending Orders

In einem vorhergehenden Artikel über CStrategy wurde die objektorientierte Klasse CPendingOrders beschrieben, die eine Pending Order im Rahmen der CStrategy darstellt. CPendingOrders ist eine Interface-Klasse. Sie beinhaltet keine inneren Mitglieder, außer dem Feld, in welchem das Orderticket gespeichert wird. Alle seine Methoden bekommen die entsprechenden Eigenschaften durch den Aufruf von drei grundlegenden Systemfunktionen — OrderGetInterer, OrderGetDouble und OrderGetString. Solche Organisation garantiert die Einheitlichkeit der Darstellung von Daten. Jeder Pending Order entspricht in MetaTrader 5 eine Instanz der CPendingOrders, deren Ticket dieser realen Order gleich ist. Wenn die Pending Order aus irgendwelchen Gründen (vom Experten oder vom Nutzer) gelöscht wird, so entfernt die CStrategy Engine die entsprechende Instanz der Klasse CPendingOrders aus der Liste der Pending Orders. Die Liste CPendingOrders wird als spezielle Klasse COrdersEnvironment gespeichert. In jeder Strategie gibt es eine einzigartige Instanz der COrdersEnvironment, die PendingOrders heißt. Die Strategie konnte auf dieses Objekt direkt zugreifen und die gewünschte Pending Order nach Index auswählen. 

Wenn die Strategie anstelle einer Marktorder eine Pending Order öffnen musste, sendete sie den entsprechenden Handelsauftrag über das Modul CTrade. In dieser Hinsicht unterschied sich das Platzieren einer Pending Order kaum vom Platzieren einer Marktorder. Aber weiter traten Unterschiede auf, die in CStrategy nicht berücksichtigt wurden. In CStrategy wird jede Marktposition eine nach der anderen einem Handler übergeben. So ist die Methode SupportBuy ein solcher Handler für Positionen vom Typ POSITION_TYPE_BUY, und die Methode SupportSell - der Handler für Positionen vom Typ POSITION_TYPE_SELL. Mit Pending Orders war alles ganz anders. Jede solche Order kam in die dem Expert Advisor verfügbare Sammlung PendingOrders, jedoch hatten solche Orders keinen eigenen Handler. Es wurde angenommen, dass Pending Orders noch irgendwo anders, zum Beispiel, in OnEvent, BuySupport/SellSupport oder sogar in BuyInit/SellInit verarbeitet werden müssen. Wenn es dabei keine offenen Positionen gab, so wurden BuySupport/SellSupport auch nicht aufgerufen, also konnten die Pending Orders nur in OnEvent verarbeitet werden. Die Verarbeitung in dieser Methode brach die allgemeine Reihenfolge der Aktionen. Ein Teil der Positionen wurde in der von CStrategy organisierten Reihenfolge verarbeitet, und der andere Teil (die Pending Orders) wurde wie früher in einem einzelnen OnEvent-Block verarbeitet.

In diesem Zusammenhang wurden der neuen Version von CStrategy zwei zusätzliche Methoden SupportPendingBuy und SupportPendingSell hinzugefügt:

class CStrategy
{
protected:
   ...   
   virtual void      SupportPendingBuy(const MarketEvent &event, CPendingOrder* order);
   virtual void      SupportPendingSell(const MarketEvent &event, CPendingOrder* order);
   ...
};

Ihre Signatur ist ähnlich den Methoden SupportBuy und SupportSell: als erstes Parameter wird das Ereignis MarketEvent übergeben, als zweites — der Pointer auf die aktuelle Order, ausgewählt von CStrategy. CStrategy wählt eine Order konsequent, indem alle Orders durchlaufen werden. Die Liste der Orders wird vom Ende zum Anfang in der Methode CallSupport durchlaufen:

//+------------------------------------------------------------------------------+
//| Ruft die Logik der Verwaltung von Positionen auf, sofern der Handelsstatus   |
//| nicht gleich TRADE_WAIT ist.                                                 |
//+------------------------------------------------------------------------------+
void CStrategy::CallSupport(const MarketEvent &event)
  {
   ...
   for(int i = PendingOrders.Total()-1; i >= 0; i--)
   {
      CPendingOrder* order = PendingOrders.GetOrder(i);
      if(order.ExpertMagic()!=m_expert_magic)continue;
      if(order.Direction() == POSITION_TYPE_BUY)
         SupportPendingBuy(event, order);
      else
         SupportPendingSell(event, order);
   }
}

Die Handler von Pending Orders werden auf die gleiche Weise wie die von Marktorders aufgerufen: für jede Pending-Kauforder wird die Methode SupportPendingBuy aufgerufen, und für jede Pending-Verkaufsorder - die Methode SupportPendingSell.

Der ganze Zyklus der Strategie, die mit Pending Orders arbeitet, ist länger als der komplette Zyklus der Strategie, die nur auf Marktorders basiert. Im zweiten Fall wird eine Reihenfolge aus zwei Handler für jede Richtung verwendet:

  1. Öffnen einer Long-Position in InitBuy / Öffnen einer Short-Position in InitSell;
  2. Verwaltung der Long-Position in SupportBuy / Verwaltung der Short-Position in SupportSell.

Bei der Verwendung der Strategie für Pending Orders muss man drei Handler für jede Richtung einbeziehen:

  1. Platzieren einer Long-Position in InitBuy / Platzieren einer Short-Position in InitSell;
  2. Verwaltung der Pending Long-Position in SupportPendingBuy, bis die Order ausgelöst oder gecancelt wird / Verwaltung der Pending Short-Position in SupportPendingSell, bis die Order ausgelöst oder gecancelt wird;
  3. Verwaltung der Long-Position in SupportBuy / Verwaltung der Short-Position in SupportSell.

In der Tat ist die Verwaltung von Pending Orders immer ein unentbehrlicher Bestandteil einer Handelsstrategie. Eine separate Verwaltung von Pending Orders und Markorders erleichtert wesentlich die Entwicklung solcher Strategien.

Die Strategie CImpulse 2.0 

Die beste Weise, sich mit den Änderungen vertraut zu machen, ist das uns bereits bekannte Beispiel der Handelsstrategie CImpulse neu zu schreiben, das im fünften Teil des Artikels beschrieben wurde. Die Grundidee der Strategie besteht darin, eine Pending Stop-Order mit einem Abstand vom gleitenden Durchschnitt auf jedem Balken zu platzieren. Dieser Abstand wird in Prozent ausgedrückt. Für den Kauf wird eine BuyStop platziert, deren Eröffnungslevel um N Prozent oberhalb des gleitenden Durchschnitts liegt. Für den Verkauf - umgekehrt: es wird eine SellStop Order platziert, deren Level um N Prozent unterhalb des gleitenden Durchschnitts liegt. Die Position wird dann geschlossen, wenn der Schlusskurs niedriger (für den Kauf) oder höher (für den Verkauf) als der gleitende Durchschnitt sein wird.

Im Großen und Ganzen ist das eine komplett neue Variante der gleichen Strategie, die früher vorgestellt wurde. Anhand dieser Strategie kann man die in CStrategy vorgenommenen Änderungen einschätzen und schauen, wie die neuen Möglichkeiten in die Praxis umgesetzt werden können. Aber zunächst einmal schauen wir uns den Code des Expert Advisors aus der letzten Version an. Wir führen hier den kompletten Code an, um beide Varianten der Syntax zu vergleichen:

//+------------------------------------------------------------------+
//|                                                      Impulse.mqh |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Strategy\Strategy.mqh>
#include <Strategy\Indicators\MovingAverage.mqh>

input double StopPercent = 0.20;
//+------------------------------------------------------------------+
//| Definiert die Aktion, die mit einer Pending Order                |
//| durchgeführt werden muss.                                        |
//+------------------------------------------------------------------+
enum ENUM_ORDER_TASK
{
   ORDER_TASK_DELETE,   // Löschen einer Pending Order
   ORDER_TASK_MODIFY    // Modifizieren einer Pending Order
};
//+------------------------------------------------------------------+
//| Die Strategie CImpulse                                           |
//+------------------------------------------------------------------+
class CImpulse : public CStrategy
{
private:
   double            m_percent;        // Der Prozentwert für den Level einer Pending Order
   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:
   double            GetPercent(void);
   void              SetPercent(double percent);
   CIndMovingAverage Moving;  // Das Arbeiten mit dem gleitenden Durchschnitt erfolgt durch eine Klasse, die extra dafür geschrieben wurde
};
//+-------------------------------------------------------------------------+
//| Das Arbeiten mit Pending BuyStop-Orders für das Öffnen einer Long-      |
//| Position                                                                |
//+-------------------------------------------------------------------------+
void CImpulse::InitBuy(const MarketEvent &event)
{
   if(!IsTrackEvents(event))return;                      
   if(positions.open_buy > 0) return;                    
   int buy_stop_total = 0;
   ENUM_ORDER_TASK task;
   double target = Ask() + Ask()*(m_percent/100.0);
   if(target < Moving.OutValue(0))                    // Der Trigger-Kurs muss oberhalb des gleitenden Durchschnitts liegen
      task = ORDER_TASK_DELETE;
   else
      task = ORDER_TASK_MODIFY;
   for(int i = PendingOrders.Total()-1; i >= 0; i--) // Die Pending Orders werden in InitBuy durchlaufen, was nicht besonders gut ist
   {
      CPendingOrder* Order = PendingOrders.GetOrder(i);
      if(Order == NULL || !Order.IsMain(ExpertSymbol(), ExpertMagic()))
         continue;
      if(Order.Type() == ORDER_TYPE_BUY_STOP)
      {
         if(task == ORDER_TASK_MODIFY) // Die Arbeit mit Pending Orders verläuft über ein Status-System
         {
            buy_stop_total++;
            Order.Modify(target);
         }
         else
            Order.Delete();
      }
   }
   if(buy_stop_total == 0 && task == ORDER_TASK_MODIFY)
      Trade.BuyStop(MM.GetLotFixed(), target, ExpertSymbol(), 0, 0, NULL);
}
//+--------------------------------------------------------------------------+
//| Das Arbeiten mit Pending SellStop-Orders für das Öffnen einer Short-     |
//| Position                                                                 |
//+--------------------------------------------------------------------------+
void CImpulse::InitSell(const MarketEvent &event)
{
   if(!IsTrackEvents(event))return;                      
   if(positions.open_sell > 0) return;                    
   int sell_stop_total = 0;
   ENUM_ORDER_TASK task;
   double target = Bid() - Bid()*(m_percent/100.0);
   if(target > Moving.OutValue(0))                    // Der Trigger-Kurs muss oberhalb des gleitenden Durchschnitts liegen
      task = ORDER_TASK_DELETE;
   else
      task = ORDER_TASK_MODIFY;
   for(int i = PendingOrders.Total()-1; i >= 0; i--)
   {
      CPendingOrder* Order = PendingOrders.GetOrder(i);
      if(Order == NULL || !Order.IsMain(ExpertSymbol(), ExpertMagic()))
         continue;
      if(Order.Type() == ORDER_TYPE_SELL_STOP)
      {
         if(task == ORDER_TASK_MODIFY)
         {
            sell_stop_total++;
            Order.Modify(target);
         }
         else
            Order.Delete();
      }
   }
   if(sell_stop_total == 0 && task == ORDER_TASK_MODIFY)
      Trade.SellStop(MM.GetLotFixed(), target, ExpertSymbol(), 0, 0, NULL);
}
//+------------------------------------------------------------------+
//| Verwaltung einer Long-Position nach Moving Average               |
//+------------------------------------------------------------------+
void CImpulse::SupportBuy(const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))return;
   if(Bid() < Moving.OutValue(0))
      pos.CloseAtMarket();
}
//+----------------------------------------------------------------------------+
//| Die Verwaltung einer Short-Position nach dem gleitenden Durchschnitt       |
//+----------------------------------------------------------------------------+
void CImpulse::SupportSell(const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))return;
   if(Ask() > Moving.OutValue(0))
      pos.CloseAtMarket();
}
//+--------------------------------------------------------------------------------------------------+
//| Filtert eingehende Events. Wenn das eingehende Ereignis                                          |
//| durch die Strategie nicht verarbeitet wird, wird false zurückgegeben, wenn es verarbeitet wird,  |
//| wird true zurückgegeben.                                                                         |
//+--------------------------------------------------------------------------------------------------+
bool CImpulse::IsTrackEvents(const MarketEvent &event)
  {
//Nur das Öffnen eines neuen Balkens auf dem aktuellen Symbol und der aktuellen Timeframe verarbeiten
   if(event.type != MARKET_EVENT_BAR_OPEN)return false;
   if(event.period != Timeframe())return false;
   if(event.symbol != ExpertSymbol())return false;
   return true;
  }
//+------------------------------------------------------------------+
//| Reagieren auf den Wechsel des Symbols                            |
//+------------------------------------------------------------------+
void CImpulse::OnSymbolChanged(string new_symbol)
  {
   Moving.Symbol(new_symbol);
  }
//+------------------------------------------------------------------+
//| Reagieren auf den Wechsel der Timeframe                          |
//+------------------------------------------------------------------+
void CImpulse::OnTimeframeChanged(ENUM_TIMEFRAMES new_tf)
  {
   Moving.Timeframe(new_tf);
  }
//+------------------------------------------------------------------+
//| Gibt das Prozent des Ausbruchslevels zurück                      |
//+------------------------------------------------------------------+  
double CImpulse::GetPercent(void)
{
   return m_percent;
}
//+------------------------------------------------------------------+
//| Setzt das Prozent des Ausbruchslevels                            |
//+------------------------------------------------------------------+  
void CImpulse::SetPercent(double percent)
{
   m_percent = percent;
}

Die kompliziertesten Momente bei der Umsetzung dieser Strategie sind in Gelb hervorgehoben.

Erstens, erfolgt die Arbeit mit dem Indikator über die früher geschriebene Klasse CIndMovingAverage. Wir haben bereits gesagt, dass dieser Ansatz irrational ist. Es gibt zu viele Indikatoren, um für jeden Indikator eine Klasse zu schreiben.

Zweitens, werden alle Pending Orders in den Blöcken BuyInit/SellInit durchlaufen. In solcher einfachen Strategie wie CImpulse ist es nicht kompliziert, aber bei einer komplizierteren Verwaltung von Orders kann dies zu Schwierigkeiten führen. Es ist besser, das Setzen von Pending Orders und deren Verwaltung in separate Methoden auszulagern, wie dies in der neuen Version von CStrategy getan wurde.

Wenn man sich den Code von CImpulse aufmerksam anschaut, sieht man, dass CImpulse einen Teil der Funktionalität übernimmt, die CStrategy bereitstellen soll. CStrategy muss ein Status-System für die Verwaltung von Pending Orders definieren, sie macht das aber nicht. Das System wird von CImpulse implementiert.

Schreiben wir den Code unter Berücksichtigung der neuen Möglichkeiten von CStrategy neu:

//+------------------------------------------------------------------+
//|                                                  Impulse 2.0.mqh |
//|           Copyright 2017, Vasiliy Sokolov, St-Petersburg, Russia |
//|                                https://www.mql5.com/de/users/c-4 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com/de/users/c-4"
#include "Strategy\Strategy.mqh"
#include "Strategy\Indicators.mqh"

input int PeriodMA = 12;
input double StopPercent = 0.05;

//+------------------------------------------------------------------+
//| Die Strategie CImpulse                                           |
//+------------------------------------------------------------------+
class CImpulse : public CStrategy
{
private:
   double            m_percent;        // Der Prozentwert für den Level einer Pending Order
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      SupportPendingBuy(const MarketEvent &event,CPendingOrder *order);
   virtual void      SupportPendingSell(const MarketEvent &event,CPendingOrder* order);
   virtual bool      OnInit(void);
public:
   double            GetPercent(void);
   void              SetPercent(double percent);
   CUnIndicator      UnMA;
};
//+------------------------------------------------------------------+
//| Initialisieren des gleitenden Durchschnitts                      |
//+------------------------------------------------------------------+
bool CImpulse::OnInit(void)
{
   UnMA.SetParameter(12);
   UnMA.SetParameter(0);
   UnMA.SetParameter(MODE_SMA);
   UnMA.SetParameter(PRICE_CLOSE);
   m_percent = StopPercent;
   if(UnMA.Create(Symbol(), Period(), IND_MA) != INVALID_HANDLE)
      return true;
   return false;
}
//+------------------------------------------------------------------+
//| Setzen von Pending BuyStop-Orders                                |
//+------------------------------------------------------------------+
void CImpulse::InitBuy(const MarketEvent &event)
{
   if(!IsTrackEvents(event))return;                                           // Erstellen von Pending Orders nur beim Öffnen eines neuen Balkens
   if(PositionsTotal(POSITION_TYPE_BUY, ExpertSymbol(), ExpertMagic()) > 0)   // Es darf keine offene Long-Position geben
      return;
   if(OrdersTotal(POSITION_TYPE_BUY, ExpertSymbol(), ExpertMagic()) > 0)      // Es darf keine Pending-Kauforder geben
      return;
   double target = WS.Ask() + WS.Ask()*(m_percent/100.0);                     // Berechnen des Levels der neuen Pending Order
   if(target < UnMA[0])                                                       // Der Trigger-Kurs der Order muss oberhalb des gleitenden Durchschnitts liegen
      return;
   Trade.BuyStop(MM.GetLotFixed(), target, ExpertSymbol(), 0, 0, NULL);       // Platzieren der neuen BuyStop-Order
}
//+-------------------------------------------------------------------------+
//| Das Arbeiten mit Pending BuyStop-Orders für das Öffnen einer Long-      |
//| Position                                                                |
//+-------------------------------------------------------------------------+
void CImpulse::SupportPendingBuy(const MarketEvent &event,CPendingOrder *order)
{
   if(!IsTrackEvents(event))return;
   double target = WS.Ask() + WS.Ask()*(m_percent/100.0);                     // Berechnen des Levels der neuen Pending Order
   if(UnMA[0] > target)                                                       // Wenn der neue Level unterhalb des aktuellen gleitenden Durchschnitt ist,
      order.Delete();                                                         // - löschen wir ihn
   else                                                                       // Andernfalls modifizieren wir ihn mit einem neuen Preis
      order.Modify(target);
}
//+--------------------------------------------------------------------------+
//| Das Arbeiten mit Pending SellStop-Orders für das Öffnen einer Short-     |
//| Position                                                                 |
//+--------------------------------------------------------------------------+
void CImpulse::SupportPendingSell(const MarketEvent &event,CPendingOrder* order)
{
   if(!IsTrackEvents(event))return;
   double target = WS.Ask() - WS.Ask()*(m_percent/100.0);                     // Berechnen des neuen Levels der Pending Order
   if(UnMA[0] < target)                                                       // Wenn der neue Level oberhalb des aktuelle Durchschnitts liegt,
      order.Delete();                                                         // - löschen wir ihn
   else                                                                       // Andernfalls modifizieren wir ihn mit einem neuen Preis
      order.Modify(target);
}
//+------------------------------------------------------------------+
//| Setzen von Pending SellStop-Orders                               |
//+------------------------------------------------------------------+
void CImpulse::InitSell(const MarketEvent &event)
{
   if(!IsTrackEvents(event))return;                                           // Erstellen von Pending Orders nur beim Öffnen eines neuen Balkens
   if(PositionsTotal(POSITION_TYPE_SELL, ExpertSymbol(), ExpertMagic()) > 0)  // Es darf keine offene Short-Position geben
      return;
   if(OrdersTotal(POSITION_TYPE_SELL, ExpertSymbol(), ExpertMagic()) > 0)     // Es darf keine offene Pending-Verkaufslevel geben
      return;
   double target = WS.Bid() - WS.Bid()*(m_percent/100.0);                     // Berechnen des neuen Levels der neuen Pending Order
   if(target > UnMA[0])                                                       // Der Trigger-Kurs muss unterhalb des gleitenden Durchschnitts liegen
      return;  
   Trade.SellStop(MM.GetLotFixed(), target, ExpertSymbol(), 0, 0, NULL);      // Platzieren der neuen BuyStop-Order
}
//+------------------------------------------------------------------+
//| Verwaltung einer Long-Position nach Moving Average               |
//+------------------------------------------------------------------+
void CImpulse::SupportBuy(const MarketEvent &event,CPosition *pos)
{
   int bar_open = WS.IndexByTime(pos.TimeOpen());
   if(!IsTrackEvents(event))return;
   ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
   {
      double target = WS.Bid() - WS.Bid()*(m_percent/100.0);
      if(target < UnMA[0])
         pos.StopLossValue(target);
      else
         pos.StopLossValue(0.0);
   }
   if(WS.Bid() < UnMA[0])
      pos.CloseAtMarket();
}
//+----------------------------------------------------------------------------+
//| Die Verwaltung einer Short-Position nach dem gleitenden Durchschnitt       |
//+----------------------------------------------------------------------------+
void CImpulse::SupportSell(const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))return;
   ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
   {
      double target = WS.Ask() + WS.Ask()*(m_percent/100.0);
      if(target > UnMA[0])
         pos.StopLossValue(target);
      else
         pos.StopLossValue(0.0);
   }
   if(WS.Ask() > UnMA[0])
      pos.CloseAtMarket();
}
//+------------------------------------------------------------------+
//| Gibt das Prozent des Ausbruchslevels zurück                      |
//+------------------------------------------------------------------+  
double CImpulse::GetPercent(void)
{
   return m_percent;
}
//+------------------------------------------------------------------+
//| Setzt das Prozent des Ausbruchslevels                            |
//+------------------------------------------------------------------+  
void CImpulse::SetPercent(double percent)
{
   m_percent = percent;
}

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   CImpulse* impulse = new CImpulse();
   impulse.ExpertMagic(140578);
   impulse.ExpertName("Impulse 2.0");
   impulse.Timeframe(Period());
   impulse.ExpertSymbol(Symbol());
   Manager.AddStrategy(impulse);
   return(INIT_SUCCEEDED);
   
}
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   Manager.OnTick();
}
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int    id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam)
{
   Manager.OnChartEvent(id, lparam, dparam, sparam);
   ChartRedraw(0);
}

Auf dem Screenshot unten ist ein Fragment des Testens der Strategie CIpmulse 2.0 im Strategietester dargestellt. Man sieht die platzierten Pending Orders und die Arbeit mit ihnen:


Abb. 1. Die Arbeit mit Pending Orders während des Testens der Strategie Impulse 2.0

Obwohl die Logik gleich geblieben ist, unterscheiden sich der alte und der neue Code voneinander. Listen wir die Unterschiede zwischen der neuen und der alten Version von auf:

  • Der Zugriff auf das aktuelle Symbol erfolgt durch die Instanz CSymbol WS (work symbol). Dieser Klasse werden alle Eigenschaften des Symbols übergeben.
  • Die Daten des Expert Advisors werden in der Methode OnInit initialisiert. Als der fünfte Teil des Artikels verfasst wurde, gab diese Methode noch nicht.
  • Statt des Indikators CIndMovingAverage wird der universelle Indikator CUnIndicator verwendet.
  • Pending Orders werden in InitBuy/InitSell platziert; modifiziert und gelöscht werden diese in SupportPendingBuy/SupportPendingSell.
  • Die Iteration über Pending Orders wird nicht mehr verwendet. Diese Funktion wird der CStrategy übergeben.
  • Statt der Struktur position werden die Methoden PositionsTotal und OrdersTotal für die Berechnung der Anzahl aktueller Positionen und Pending Orders verwendet.

Das angeführte Quellcode beinhaltet sowohl den Code der Strategie, als auch die grundlegenden Funktionen des Expert Advisors. Dieses Beispiel ist als eine einzelne mq5-Datei angeführt. Dies ist damit verbunden, dass die Struktur des Projekts wesentlich geändert wurde. Darum geht es weiter.

Die neue Struktur des CStrategy Projekts

In den vorherigen Version war die CStrategy Trading-Engine gleichzeitig in mehreren MQL5-Unterordnern abgelegt. Die Engine selbst und deren Hilfsdateien wurden in MQL5\Include\Strategy gespeichert. Die Quellcodes für die Umsetzung des Handelspanels der Engine erschienen in MQL5\Include\Panel. Der Code des Expert Advisors konnte in MQL5\Include\Strategy\Samples untergebracht werden, und die mq5-Datei für den Start des Expert Advisors — in MQL5\Experts\Article08. Darüber hinaus wurden verschiedene Hilfskomponenten wie Dictionary.mqh oder XmlBase.mqh auch in verschiedenen Verzeichnissen des Include-Ordners abgelegt.

Offensichtlich sind die Verbindungen im Projekt sehr kompliziert geworden, und der Ort der Dateien und Verzeichnisse wird häufig dupliziert. Dies erschwert die potenzielle Arbeit mit der CStrategy Trading-Engine. Das kann Nutzer, vor allem Anfänger, durcheinanderbringen, und sie werden nicht verstehen, wie der Kompilierungsvorgang läuft. Aus diesem Grund werden ab der aktuellen Version der Trading Engine deren Dateien anders angeordnet.

Ab jetzt wird das ganze Projekt im Verzeichnis MQL5\Experts\UnExpert abgelegt. Da befindet sich der Ordner Strategy und die Dateien der Strategie mit der Erweiterung ".mq5". Nun stellt die Handelsstrategie eine einzige mq5-Datei dar, welche sowohl Standardfunktionen für die Verarbeitung von Ereignissen (solche wie OnInit und OnTick), als auch die Klasse der Strategie basierend auf CStrategy beinhaltet.

Alle Hilfsdateien sind ebenso in MQL5\Experts\UnExpert\Strategy zu finden. Dies betrifft auch die Dateien für das Arbeiten mit XML und Infrastruktur-Dateien, solche wie Dictionary.mqh. Um eine Instanz zu kompilieren, muss man eine Datei öffnen, zum Beispiel "MQL5\Experts\UnExpert\Impulse 2.0.mqh", und auf den "Kompilieren"-Button klicken.

In der Version, die in diesem Teil des Artikels als Beispiel angeführt wird, werden nur zwei Strategien, Impulse 1.0 und Impulse 2.0, verwendet. Das ist eine und dieselbe Strategie, die im alten und im neuen Stil von CStrategy geschrieben wurde. Dies wurde extra getan, damit man beide Ansätze vergleichen und die in diesem Artikel beschriebenen Unterschiede sehen könnte. Die anderen Beispiele für Strategien, die in den vorherigen Versionen von CStrategy vorhanden waren, sind in dieser Version nicht verfügbar. Das liegt daran, dass sie auf der alten Syntax basieren, und können deswegen nicht als Beispiel angeführt werden. Wahrscheinlich erscheinen diese in den nächsten Versionen, aber mit einer geänderten Syntax.

Fazit

Wir haben die neuen Komponenten der CStrategy betrachtet. Dazu gehört die Klasse CUnIndicator, die eine universelle Schnittstelle im OOP Stil für das Arbeiten mit System-Inidikatoren sowie mit beliebigen MQL5-Indikatoren, und ein System der Verwaltung von Pending Orders basierend auf den Methoden SupportPendingBuy und SupportPendingSell implementiert. Alle diese Elemente geben einen starken Synergieeffekt beim Schreiben eines Handelsexperten. Der Benutzer braucht sich keine Gedanken über Low-Level-Operationen zu machen. Fast die ganze Handelsumgebung ist für ihn über intuitiv klare und lakonische Klassen verfügbar, und die Handelslogik kann durch das Überschreiben der entsprechenden vorgegebenen Methoden definiert werden. 

Das Projekt selbst wird jetzt an einem Ort abgelegt, und seine Verbindungen beschränken sich auf das Verzeichnis MQL5\Experts\UnExpert. Es ist nicht mehr nötig, die Dateien in verschiedenen Ordnern im MQL5-Verzeichnis zu speichern. Diese Neuerung sollte Nutzer zum Wechsel zur CStrategy oder wenigstens zum Erlernen deren Möglichkeiten anregen.