English Русский 中文 Español 日本語 Português
Erstellen eines Expert Advisors mit separaten Modulen

Erstellen eines Expert Advisors mit separaten Modulen

MetaTrader 5Experten | 5 Dezember 2019, 16:45
1 305 0
Andrei Novichkov
Andrei Novichkov

Einführung

Bei der Entwicklung von Indikatoren, Expert Advisors und Skripten müssen Entwickler oft verschiedene Codeteile erstellen, die nicht direkt mit der Handelsstrategie zusammenhängen. Ein solcher Code kann sich beispielsweise auf die Ausführung des Expert Advisors beziehen: täglich, wöchentlich oder monatlich. Als Ergebnis werden wir ein unabhängiges Projekt erstellen, das über eine einfache Schnittstelle mit der Handelslogik und anderen Komponenten interagieren kann. Mit geringstem Aufwand kann ein solcher Zeitplan ohne gravierende Änderungen in individuell entwickelten Expert Advisors und Indikatoren weiterverwendet werden. In diesem Artikel werden wir versuchen, einen Systemansatz für die Erstellung von Expert Advisor mit Hilfe von Blöcken anzuwenden. Wir werden auch eine interessante Möglichkeit in Betracht ziehen, die sich aus unserer Arbeit ergeben wird. Der Artikel ist für Anfänger gedacht.

Was ist die übliche Praxis?

Lassen Sie uns versuchen zu verstehen, wie ein solcher Expert Advisor aussehen kann und welche Teile/Komponenten/Module er enthalten kann. Woher kommen solche Komponenten? Die Antwort ist einfach und klar — im Prozess der Anwendungsentwicklung muss der Programmierer verschiedene Komponenten erstellen, die oft ähnliche oder gleiche Funktionen haben.

Es ist offensichtlich, dass es nicht notwendig ist, jedes Mal von Grund auf das gleiche Feature, wie z.B. die Funktion eines Trailing-Stops, zu implementieren. Im Allgemeinen kann der Trailing Stop ähnliche Funktionen erfüllen und ähnliche Eingaben für verschiedene Expert Advisors machen. So kann der Programmierer einmalig den Funktionscode für eine Trailing erstellen und ihn dann mit minimalem Aufwand in die jeweiligen EAs einfügen. Das Gleiche gilt für viele andere Komponenten, darunter der Handelszeitplan, verschiedene Nachrichtenfilter und Module mit Handelsfunktionen, etc.

So haben wir eine Art Bausatz, auf dessen Grundlage wir einen Expert Advisor aus einzelnen Modulen/Blöcken zusammenstellen können. Module tauschen Informationen untereinander und mit dem "Kern" des Expert Advisors aus, wo sich die "Strategie" befindet und wo die Entscheidungen getroffen werden. Lassen Sie uns mögliche Beziehungen zwischen einzelnen Modulen darstellen:



Das daraus resultierende Schema ist eher verwirrend. Es zeigt zwar nur das Zusammenspiel von drei Modulen und zwei EA-Funktionen: OnStart und OnTick. In einem komplexeren Expert Advisor werden interne Verbindungen noch komplizierter. Ein solcher Expert Advisor ist schwer zu handhaben. Darüber hinaus würde es erhebliche Schwierigkeiten bereiten, wenn eines der Module ausgeschlossen oder ein zusätzliches hinzugefügt werden müsste. Außerdem wäre das anfängliche Debuggen und die Fehlersuche nicht einfach. Einer der Gründe für solche Schwierigkeiten hängt damit zusammen, dass die Verbindungen ohne einen geeigneten systematischen Ansatz entworfen werden. Verbieten Sie den Modulen, miteinander und mit den EA-Funktionen zu kommunizieren, wann immer es notwendig ist, und eine bestimmte Reihenfolge wird angezeigt:

  • Alle Module werden in OnInit erstellt.
  • Die EA-Logik ist in OnTick enthalten.
  • Die Module tauschen Informationen nur mit OnTick aus.
  • Gegebenenfalls werden Module in OnDeinit gelöscht.

Eine so einfache Lösung kann schnell einen positiven Effekt haben. Separate Module lassen sich leichter verbinden/trennen, debuggen und modifizieren. Die Logik in OnTick wird für Wartung und Verbesserung zugänglicher, wenn Verbindungen in einer Funktion implementiert werden, anstatt an verschiedenen Stellen im EA-Code hinzugefügt zu werden:


Diese kleine Designänderung sorgt für eine klarere EA-Struktur, die intuitiv wird. Die neue Struktur ähnelt dem Ergebnis der Anwendung des "Observer"-Musters, obwohl sich die Struktur selbst vom Muster unterscheidet. Mal sehen, wie wir das Design weiter verbessern können.

EA für Experimente

Wir brauchen einen einfachen Expert Advisor, um unsere Ideen zu überprüfen. Wir brauchen keinen sehr komplizierten EA, denn unser Ziel ist es jetzt nur noch, die Features zu demonstrieren. Der EA wird eine Verkaufsorder eröffnen, wenn die vorherige Kerze abwärts zeigt. Der Expert Advisor wird nach dem Baukastenprinzip konzipiert. Das erste Modul implementiert Handelsfunktionen:

class CTradeMod {
public:
   double dBaseLot;
   double dProfit;
   double dStop;
   long   lMagic;
   void   Sell();
   void   Buy();
};

void CTradeMod::Sell()
{
  CTrade Trade;
  Trade.SetExpertMagicNumber(lMagic);
  double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
  Trade.Sell(dBaseLot,NULL,0,ask + dStop,ask - dProfit);
} 

void CTradeMod::Buy()
{
  CTrade Trade;
  Trade.SetExpertMagicNumber(lMagic);
  double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
  Trade.Buy(dBaseLot,NULL,0,bid - dStop,bid + dProfit);
} 

Das Modul ist als Klasse mit offenen Feldern und Methoden implementiert. Wie im Moment benötigen wir keine implementierte Buy()-Methode im Modul, aber sie wird später benötigt. Der Wert der einzelnen Felder sollte klar sein; sie werden für das Volumen, die Handelsstufen und die Magicnummer verwendet. Wie man das Modul verwendet: Erstellen Sie es und rufen Sie die Methode Sell() auf, wenn ein Eingangssignal auftritt.

Ein weiteres Modul wird in den EA aufgenommen:

class CParam {
public:
   double dStopLevel;      
   double dFreezeLevel;   
    CParam() {
      new_day();      
    }//EA_PARAM()
    void new_day() {
      dStopLevel   = SymbolInfoInteger(Symbol(),SYMBOL_TRADE_STOPS_LEVEL) * Point();
      dFreezeLevel = SymbolInfoInteger(Symbol(),SYMBOL_TRADE_FREEZE_LEVEL) * Point();
    }//void new_day()
};

Lassen Sie uns dieses Modul näher betrachten. Dies ist ein Hilfsmodul, das verschiedene Parameter enthält, die von anderen Modulen und den EA-Funktionen verwendet werden. Sie sind vielleicht schon einen solchen Code begegnet:
...
input int MaxSpread = 100;
...
OnTick()
 {
   if(ask - bid > MaxSpread * Point() ) return;
....
 }

Offensichtlich ist dieses Fragment unwirksam. Wenn wir alle Eingabeparameter (und andere), die aktualisiert oder konvertiert werden müssen, in ein separates Modul einfügen (hier geht es um MaxSpread * Point()), halten wir den globalen Raum sauber und können ihren Zustand effizient kontrollieren, wie es bei den Werten stops_level und freeze_level im obigen CParam-Modul der Fall ist.

Wahrscheinlich wäre es besser, spezielle Getter, die die Werte abrufen, bereitzustellen, als die Modulfelder offen zu halten. Hier wird die obige Lösung zur Vereinfachung des Codes verwendet. Für ein echtes Projekt ist es besser, einen Getter zu verwenden.

Darüber hinaus könnten wir eine Ausnahme für das CParam-Modul machen und den Zugriff auf dieses Modul nicht nur durch die Funktion OnTick(), sondern auch durch alle anderen Module und Funktionen ermöglichen.

Hier sind die Eingabeparameter und die EA-Funktionen:

input double dlot   = 0.01;
input int    profit = 50;
input int    stop   = 50;
input long   Magic  = 123456;

CParam par;
CTradeMod trade;

int OnInit()
  {  
   trade.dBaseLot = dlot;
   trade.dProfit  = profit * _Point;
   trade.dStop    = stop   * _Point;
   trade.lMagic   = Magic;
   
   return (INIT_SUCCEEDED);
  }
  
void OnDeinit(const int reason)
  {
  }

void OnTick()
  {
   int total = PositionsTotal(); 
   ulong ticket, tsell = 0;
   ENUM_POSITION_TYPE type;
   double l, p;
   for (int i = total - 1; i >= 0; i--) {
      if ( (ticket = PositionGetTicket(i)) != 0) {
         if ( PositionGetString(POSITION_SYMBOL) == _Symbol) {
            if (PositionGetInteger(POSITION_MAGIC) == Magic) {
               type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
               l    = PositionGetDouble(POSITION_VOLUME);
               p    = PositionGetDouble(POSITION_PRICE_OPEN);
               switch(type) {
                  case POSITION_TYPE_SELL:
                     tsell = ticket;    
                     break;
               }//switch(type)
            }//if (PositionGetInteger(POSITION_MAGIC) == lmagic)
         }//if (PositionGetString(POSITION_SYMBOL) == symbol)
      }//if ( (ticket = PositionGetTicket(i)) != 0)
   }//for (int i = total - 1; i >= 0; i--)
   if (tsell == 0) 
      {
        double o = iOpen(NULL,PERIOD_CURRENT,1); 
        double c = iClose(NULL,PERIOD_CURRENT,1); 
        if (c < o) 
          {
            trade.Sell();
          }   
      }                         
  }

Der EA initialisiert Module in OnInit() und greift dann nur aus OnTick()auf sie zu. In OnTick() durchläuft der EA offene Positionen, um zu überprüfen, ob die gewünschte Position bereits geöffnet wurde. Wenn die Position noch nicht geöffnet wurde, öffnet der EA eine, sofern ein Signal anliegt.

Bitte beachten Sie, dass die Funktion OnDeinit(const int reason) derzeit leer ist. Module werden so angelegt, dass sie nicht explizit gelöscht werden müssen. Außerdem wurde das CParam noch nicht verwendet, da keine Prüfungen zur Positionsöffnung durchgeführt werden. Wenn solche Prüfungen durchgeführt werden, könnte das Modul CTradeMod Zugriff auf CParam benötigen, und der Entwickler müsste eine zuvor genannte Ausnahme machen und den Zugriff auf CParam erlauben. Dies ist in unserem Fall jedoch nicht erforderlich.

Lassen Sie uns das aber näher betrachten. Das Modul CTradeMod benötigt möglicherweise Daten von CParam, um den Stop-Loss zu überprüfen und Take-Profits sowie das Positionsvolumen zu ermitteln. Aber die gleiche Überprüfung kann auch an der Entscheidungsstelle durchgeführt werden: Wenn die Ebenen und das Volumen nicht den Anforderungen entsprechen, eröffnen Sie keine Position. Somit wird die Prüfung in OnTick() verschoben. Da in den Eingabeparametern Handelspreise- und Volumenwerte angegeben sind, kann die Prüfung wie in unserem Beispiel einmalig in OnInit() durchgeführt werden. Wenn die Prüfung nicht erfolgreich ist, sollte die Initialisierung des gesamten EA mit einem Fehler beendet werden. Somit können die Module CTradeMod und CParam unabhängig voneinander agieren. Dies ist für die meisten Expert Advisors relevant: Unabhängige Module arbeiten über OnTick() und wissen nichts voneinander. Diese Bedingung kann jedoch in einigen Fällen nicht beobachtet werden. Wir werden später auf sie zurückkommen.

Verbesserungen

Das erste zu behandelnde Problem ist die große Positionsschleife in OnTick(). Dieses Stück Code wird benötigt, wenn der Entwickler dies wünscht:

  1. Vermeiden Sie weitere Positionsöffnungen, wenn das Eingangssignal aktiv bleibt.
  2. Ermöglichen Sie es dem EA, bereits erteilte Aufträge nach einer Pause oder Betriebsunterbrechung zu erkennen.
  3. Ermöglichen Sie es dem EA, aktuelle Statistiken zu sammeln, wie beispielsweise das Gesamtvolumen der offenen Positionen oder den maximalen Drawdown.

Diese Schleife wird bei Expert Advisors, die Raster und Mittelungstechniken verwenden, noch größer sein. Darüber hinaus ist es erforderlich, wenn der EA virtuelle Handelspreise verwendet. Daher ist es besser, ein separates Modul auf Basis dieses Codes zu erstellen. Im einfachsten Fall erkennt das Modul Positionen mit der spezifischen magischen Zahl und informiert der EA über diese Positionen. In einem komplizierteren Fall könnte das Modul als Kernel implementiert werden, der einfachere Module enthält, wie z.B. Protokollierungs- oder Statistikmodule. In diesem Fall hat der EA eine baumartige Struktur mit OnInit() und OnTick() an der Basis des Baumes. So könnte ein solches Modul aussehen:

class CSnap {
public:
           void CSnap()  {
               m_lmagic = -1; 
               m_symbol = Symbol();               
           }
   virtual void  ~CSnap() {}   
           bool   CreateSnap();
           long   m_lmagic;
           string m_symbol;
};

Alle Felder sind wieder offen. Tatsächlich hat der Code zwei Felder: für die Magicnummer und für den Namen des Symbols, das der EA handelt. Bei Bedarf können in OnInit() Werte für diese Felder gesetzt werden. Der Hauptteil der Arbeit wird mit der Methode CreateSnap() durchgeführt:
bool CSnap::CreateSnap() {
   int total = PositionsTotal(); 
   ulong ticket;
   ENUM_POSITION_TYPE type;
   double l, p;
   for (int i = total - 1; i >= 0; i--) {
      if ( (ticket = PositionGetTicket(i)) != 0) {
         if (StringLen(m_symbol) == 0 || PositionGetString(POSITION_SYMBOL) == m_symbol) {
            if (m_lmagic < 0 || PositionGetInteger(POSITION_MAGIC) == m_lmagic) {
               type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
               l    = PositionGetDouble(POSITION_VOLUME);
               p    = PositionGetDouble(POSITION_PRICE_OPEN);
               switch(type) {
                  case POSITION_TYPE_BUY:
// ???????????????????????????????????????
                     break;
                  case POSITION_TYPE_SELL:
// ???????????????????????????????????????
                     break;
               }//switch(type)
            }//if (lmagic < 0 || PositionGetInteger(POSITION_MAGIC) == lmagic)
         }//if (StringLen(symbol) == 0 || PositionGetString(POSITION_SYMBOL) == symbol)
      }//if ( (ticket = PositionGetTicket(i)) != 0)
   }//for (int i = total - 1; i >= 0; i--)
   return true;
}

Der Code ist einfach, aber er hat ein paar Probleme. Wie und wo soll das Modul die erhaltenen Informationen weitergeben? Was sollte in die Zeilen mit Fragezeichen im letzten Codefragment geschrieben werden? Das mag einfach erscheinen. Aufruf von CreateSnap() in OnTick(): Diese Methode führt die erforderlichen Aufgaben aus und speichert die Ergebnisse in den Klassenfeldern CSnap. Dann überprüft die Funktion die Felder und zieht Schlussfolgerungen.

Nun, diese Lösung kann im einfachsten Fall implementiert werden. Was aber würden wir tun, wenn wir eine getrennte Behandlung der einzelnen Positionsparameter benötigen, z.B. zur Berechnung eines gewichteten Durchschnittswertes? In diesem Fall ist ein universellerer Ansatz erforderlich, bei dem die Daten zur Weiterverarbeitung an ein nächstes Objekt weitergeleitet werden. Zu diesem Zweck muss in CSnap ein spezieller Pointer auf ein solches Objekt bereitgestellt werden:

CStrategy* m_strategy;
Die Methode in die Datei einen Wert einzutragen:
     bool SetStrategy(CStrategy* strategy) {
              if(CheckPointer(strategy) == POINTER_INVALID) return false;
                 m_strategy = strategy;
                 return true;
              }//bool SetStrategy(CStrategy* strategy)   

Der Objektname CStrategy wurde gewählt, da in diesem Objekt sowohl Eintrittsentscheidungen als auch andere Entscheidungen getroffen werden können. Daher kann das Objekt die Strategie des gesamten EAs definieren.

Nun sieht der 'Schalter' in der Methode CreateSnap() wie folgt aus:

               switch(type) {
                  case POSITION_TYPE_BUY:
                     m_strategy.OnBuyFind(ticket, p, l);
                     break;
                  case POSITION_TYPE_SELL:
                     m_strategy.OnSellFind(ticket, p, l);
                     break;
               }//switch(type

Bei Bedarf kann der Code leicht durch Schalter für Pending-Orders und entsprechende Methodenaufrufe ergänzt werden. Darüber hinaus kann die Methode leicht modifiziert werden, um die Erfassung weiterer Daten zu ermöglichen. CreateSnap() könnte als virtuelle Methode implementiert werden, um eine mögliche Vererbung von der Klasse CSnap zu ermöglichen. Dies ist in unserem Fall jedoch nicht notwendig und wir werden daher eine einfachere Code-Version verwenden.

Darüber hinaus kann ein Pointer auf ein solches Objekt (wir haben es als CStrategy* Pointer implementiert) nicht nur für das aktuelle Modul nützlich sein. Die potenzielle Verbindung eines Moduls mit der EA-Betriebslogik kann für jedes Modul erforderlich sein, das aktive Berechnungen durchführt. Daher sollten wir in der Basisklasse ein spezielles Feld und eine Initialisierungsmethode bereitstellen:

class CModule {
   public:
      CModule() {m_strategy = NULL;}
     ~CModule() {}
     virtual bool SetStrategy(CStrategy* strategy) {
                     if(CheckPointer(strategy) == POINTER_INVALID) return false;
                     m_strategy = strategy;
                     return true;
                  }//bool SetStrategy(CStrategy* strategy)   
   protected:   
      CStrategy* m_strategy;  
};


Weiterhin werden wir Module erstellen, die von CModule abgeleitet wurden. In einigen Fällen haben wir möglicherweise überflüssigen Code. Dieser Nachteil wird jedoch durch die Module kompensiert, in denen ein solcher Pointer wirklich benötigt wird. Wenn eines der Module einen solchen Pointer nicht benötigt, rufen Sie einfach nicht die Methode SetStrategy(...) in einem solchen Modul auf. Die Basismodulklasse kann auch für die Platzierung zusätzlicher Felder und Methoden nützlich sein, die uns noch unbekannt sind. Beispielsweise könnte die folgende Methode (die in unserem Fall nicht implementiert ist) nützlich sein:
public:
   const string GetName() const {return m_sModName;}
protected:
         string m_sModName;


Das Verfahren gibt den Modulnamen zurück, der für die Fehlersuche, das Debugging oder in Informationspanels verwendet werden kann.

Lassen Sie uns nun sehen, wie die wichtige Klasse CStrategy implementiert werden kann:

Die Strategie des Expert Advisors

Wie bereits erwähnt, muss es sich um ein Objekt handeln, das Eröffnungsentscheidungen trifft. Ein solches Objekt kann unter anderem auch Entscheidungen über Exits, Modifikationen und Teilschließungen treffen. Deshalb wird das Objekt so genannt. Es ist offensichtlich, dass es nicht als Modul implementiert werden kann: Es ist von grundlegender Bedeutung, dass das Entscheidungsobjekt in jedem Expert Advisor individuell ist. Andernfalls ist der resultierende EA identisch mit dem zuvor entwickelten. Daher können wir ein solches Objekt nicht einmal entwickeln und in alle EAs einfügen, wie es bei Modulen der Fall ist. Aber das ist nicht unser Ziel. Beginnen wir die Entwicklung der Basisklasse mit bereits bekannten Fakten:

class CStrategy  {
public:
   virtual  void    OnBuyFind  (ulong ticket, double price, double lot) = 0;
   virtual  void    OnSellFind (ulong ticket, double price, double lot) = 0;       
};// class CStrategy


So einfach ist das. Wir haben zwei Methoden zum Aufruf hinzugefügt, falls CreateSnap() benötigte Positionen erkennt. CStrategy ist als abstrakte Klasse implementiert, während die Methoden virtuell deklariert werden. Dies liegt daran, dass das Objekt für verschiedene Expert Advisor unterschiedlich sein wird. Somit kann die Basisklasse nur für die Vererbung verwendet werden, während ihre Methoden überschrieben werden.

Nun müssen wir die Datei CStrategy.mqh in CModule.mqh einbinden:

#include "CStrategy.mqh"


Danach kann das EA-Framework als abgeschlossen betrachtet werden und wir können mit weiteren Verbesserungen und Verbesserungen fortfahren.

Strategieverbesserungen des Expert Advisors

Mit Hilfe der virtuellen Methoden greift das Objekt CSnap auf das CStrategy zu. Es muss aber auch andere Methoden in CStrategy geben. Das Strategieobjekt muss entscheidungsfähig sein. Daher sollte sie bei Erkennung eines geeigneten Signals Eröffnungsempfehlungen geben und dann eine solche Eröffnung durchführen. Wir benötigen Methoden, die das Objekt CSnap zwingen, seine Methode CreateSnap() aufzurufen, etc. Lassen Sie uns einige der Methoden zur Klasse CStrategy hinzufügen:

   virtual  string  Name() const     = 0;
   virtual  void    CreateSnap()     = 0;  
   virtual  bool    MayAndEnter()    = 0;  
   virtual  bool    MayAndContinue() = 0;       
   virtual  void    MayAndClose()    = 0;


Dies ist eine sehr bedingte Liste, die für bestimmte EAs geändert oder erweitert werden kann. Was diese Methoden bewirken:

  • Name()                       —   gibt den Namen der Strategie zurück.
  • CreateSnap()              —   ruft die gleiche Methode des CSnap-Objekts auf.
  • ayAndEnter()             —   prüft, ob es ein Eingangssignal gibt und gibt an, ob es ein Signal gibt.
  • MayAndContinue()   —   prüft, ob ein Eingangssignal vorhanden ist und tritt erneut ein, wenn ein Signal vorhanden ist.
  • MayAndClose()         —   prüft, ob es ein Ausgangssignal gibt und schließt alle Positionen, wenn ein solches Signal vorhanden ist.

Natürlich ist diese Liste bei weitem nicht vollständig. Es fehlt eine sehr wichtige Methode, die Initialisierungsmethode der Strategie. Unser Ziel ist es, Pointer auf alle Module im CStrategy-Objekt zu implementieren, so dass OnTick() und andere Funktionen nur auf das CStrategy-Objekt zugreifen und nichts über die Existenz anderer Module wissen. Daher müssen dem CStrategy-Objekt Modulzeiger hinzugefügt werden. Wir können nicht einfach entsprechende offene Felder bereitstellen und in OnInit() initialisieren. Dies wird später erläutert.

Stattdessen werden wir eine Initialisierungsmethode verwenden

virtual  bool    Initialize(CInitializeStruct* pInit) = 0;
  mit dem Initialisierungsobjekt CInitializeStruct, das die erforderlichen Pointer enthält. Das Objekt wird in der CStrategy.mqh wie folgt deklariert:
class CInitializeStruct {};

Es handelt sich um eine leere Klasse, die zur Vererbung vorgesehen ist, ähnlich wie CStrategy. Wir haben die Vorbereitungsarbeiten abgeschlossen und können zu einem echten Expertenberater gehen.

Praktische Anwendung

Erstellen wir einen Demo-Expertenberater, der nach einer sehr einfachen Logik arbeitet: Wenn die vorherige Kerze abwärts zeigt, eröffnen wir eine Verkaufsposition mit festen Preisen für Take-Profit und Stop-Loss. Die nächste Position sollte erst geöffnet werden, wenn die vorherige geschlossen ist.

Wir haben fast fertige Module. Betrachten wir die von CStrategy abgeleitete Klasse:

class CRealStrat1 : public CStrategy   {
public:
   static   string  m_name;
                     CRealStrat1(){};
                    ~CRealStrat1(){};
   virtual  string  Name() const {return m_name;}
   virtual  bool    Initialize(CInitializeStruct* pInit) {
                        m_pparam = ((CInit1* )pInit).m_pparam;
                        m_psnap = ((CInit1* )pInit).m_psnap;
                        m_ptrade = ((CInit1* )pInit).m_ptrade;
                        m_psnap.SetStrategy(GetPointer(this));
                        return true;
                    }//Initialize(EA_InitializeStruct* pInit)
   virtual  void    CreateSnap() {
                        m_tb = 0;
                        m_psnap.CreateSnap();
                    }  
   virtual  bool    MayAndEnter();
   virtual  bool    MayAndContinue() {return false;}       
   virtual  void    MayAndClose()   {}
   virtual  bool    Stop()            {return false;}   
   virtual  void    OnBuyFind  (ulong ticket, double price, double lot) {}
   virtual  void    OnBuySFind (ulong ticket, double price, double lot) {}   
   virtual  void    OnBuyLFind (ulong ticket, double price, double lot) {}
   virtual  void    OnSellFind (ulong ticket, double price, double lot) {tb = ticket;}  
   virtual  void    OnSellSFind(ulong ticket, double price, double lot) {}   
   virtual  void    OnSellLFind(ulong ticket, double price, double lot) {}      
private:
   CParam*          m_pparam;
   CSnap*           m_psnap;  
   CTradeMod*       m_ptrade;   
   ulong            m_tb;            
};
static string CRealStrat1::m_name = "Real Strategy 1";

bool CRealStrat1::MayAndEnter() {
   if (tb != 0) return false;  
   double o = iOpen(NULL,PERIOD_CURRENT,1); 
   double c = iClose(NULL,PERIOD_CURRENT,1); 
   if (c < o) {
      m_ptrade.Sell();
      return true;
   }   
   return false;
} 

Der EA-Code ist einfach und bedarf keiner Erklärung. Betrachten wir nur einige der Teile. Die Methode CreateSnap() der Klasse CRealStrat1 setzt das Feld mit dem Ticket der bereits offenen Verkaufsposition zurück und ruft die Methode CreateSnap() des CSnap-Moduls auf. Das CSnap-Modul prüft offene Positionen. Wenn eine von dieser EA geöffnete Verkaufsposition gefunden wird, ruft das Modul die Methode OnSellFind(...) der Klasse CStrategy auf, deren Pointer im CSnap-Modul enthalten ist. Dadurch wird die Methode OnSellFind(...) der Klasse CRealStrat1 aufgerufen. Es ändert den Wert des Feldes m_tb erneut. Die Methode MayAndEnter() sieht, dass eine Position bereits geöffnet wurde und keine neue öffnen wird. Andere Methoden der Basisklasse CStrategy werden in unserem EA nicht verwendet, daher sind sie leer.

Ein weiterer interessanter Punkt betrifft die Methode Initialize(...). Diese Methode fügt der Klasse CRealStrat1 Pointer auf andere Module hinzu, die für separate Entscheidungen erforderlich sein können. Die Klasse CStrategy weiß nicht, welche Module für die Klasse CRealStrat1 benötigt werden können und verwendet daher eine leere Klasse CInitializeStruct. Wir werden die Klasse CInit1 in die Datei mit der Klasse CRealStrat1 aufnehmen (obwohl dies nicht notwendig ist). CInit1 wird von CInitializeStruct übernommen:

class CInit1: public CInitializeStruct {
public:
   bool Initialize(CParam* pparam, CSnap* psnap, CTradeMod* ptrade) {
   if (CheckPointer(pparam) == POINTER_INVALID || 
       CheckPointer(psnap)  == POINTER_INVALID) return false;   
      m_pparam = pparam; 
      m_psnap  = psnap;
      m_ptrade = ptrade;
       return true;
   }
   CParam* m_pparam;
   CSnap*  m_psnap;
   CTradeMod* m_ptrade;
};

Das Klassenobjekt kann in OnInit() angelegt und initialisiert und an die entsprechende Methode im Klassenobjekt CRealStrat1 übergeben werden. Somit haben wir eine komplexe Struktur, die aus einzelnen Objekten besteht. Aber wir können mit der Struktur über eine einfache Schnittstelle in OnTick() arbeiten.

Die Funktionen OnInit() und OnTick()

Hier ist OnInit() und eine mögliche Liste der globalen Objekte:

CParam     par;
CSnap      snap;
CTradeMod  trade;

CStrategy* pS1;

int OnInit()
  {  
   ...
   pS1 = new CRealStrat1();
   CInit1 ci;
   if (!ci.Initialize(GetPointer(par), GetPointer(snap), GetPointer(trade)) ) return (INIT_FAILED);
   pS1.Initialize(GetPointer(ci));      
   return (INIT_SUCCEEDED);
  }
  


  In OnInit() wird nur ein Objekt erzeugt: eine Instanz der Klasse CRealStrat1. Es wird mit einem Objekt der Klasse CInit1 initialisiert. Das Objekt wird dann in OnDeinit() wieder zerstört:
void OnDeinit(const int reason)
  {
      if (CheckPointer(pS1) != POINTER_INVALID) delete pS1;      
  }


OnTick() wird dadurch sehr einfach:
void OnTick()
  {
      if (IsNewCandle() ){
         pS1.CreateSnap();
         pS1.MayAndEnter();
         
      }    
  }


Überprüfen Sie die vorhandene Position beim Öffnen einer neuen Kerze und prüfen Sie dann, ob ein Eröffnungssignal vorhanden ist. Wenn ein Signal vorhanden ist und der EA noch keine Eröffnung durchgeführt hat, eröffnen Sie eine neue Position. Die Behandlung ist so einfach, dass bei Bedarf leicht zusätzlicher "globaler" Code hinzugefügt werden kann. Beispielsweise können Sie den EA anweisen, nicht sofort nach dem Start in einem Chart mit dem Trading zu beginnen, sondern auf die Bestätigung durch den Benutzer durch einen Tastenklick zu warten und so weiter.

Einige andere EA-Funktionen werden hier nicht beschrieben, sind aber im angehängten Zip-Archiv verfügbar.

So haben wir einen Expert Advisor entworfen, der aus einzelnen Steinen, den Modulen, besteht. Aber das ist nicht alles. Lassen Sie uns weitere interessante Möglichkeiten betrachten, die dieser Programmieransatz bietet.

Was kommt als Nächstes?

Das Erste, was als Nächstes umgesetzt werden kann, ist der dynamische Austausch von Modulen. Ein einfacher Zeitplan kann durch einen fortgeschritteneren ersetzt werden. In diesem Fall müssen wir den Zeitplan als Objekt ersetzen, anstatt Eigenschaften und Methoden zu dem bestehenden hinzuzufügen, was das Debuggen und die Wartung erschweren würde. Wir können Versionen für "Debug" und "Release" für einzelne Module bereitstellen, einen Manager zur Steuerung der Module erstellen.  

Aber es gibt eine noch interessantere Möglichkeit. Mit unserem Programmieransatz können wir eine dynamische EA-Strategieersetzung implementieren, die in unserem Fall als Ersatz für die Klasse CRealStrat1 implementiert ist. Das resultierende EA wird über zwei Kerne verfügen, die zwei Strategien implementieren, z.B. Trendhandel und Seitwärtsstrategien. Darüber hinaus kann eine dritte Strategie für den Handel während der asiatischen Sitzung hinzugefügt werden. Das bedeutet, dass wir mehrere Expert Advisors innerhalb eines EA implementieren und dynamisch wechseln können. Wie man es macht:

  1. Entwicken Sie eine Klasse mit einer Entscheidungslogik, die von der Klasse CStrategy abgeleitet ist (wie CRealStrat1).
  2. Entwickeln Sie eine Klasse, die diese neue Strategie initialisiert, abgeleitet von CInitializeStruct (CInit1).
  3. Binden Sie die resultierende Datei in das Projekt ein.
  4. Fügen Sie eine neue Variable den Eingabeparametern hinzu, die eine aktive Strategie beim Start festlegt.
  5. Implementierung von Strategieumschaltungen, d.h. eines Handelspanels.

Als Beispiel, lassen Sie uns eine weitere Strategie zu unserer Demo EA hinzufügen. Auch hier werden wir eine sehr einfache Strategie erarbeiten, um den Code nicht zu verkomplizieren. Die erste Strategie eröffnete eine Verkaufsposition. Diese hat eine ähnliche Logik: Wenn die vorherige Kerze aufwärts zeigt, eröffnen Sie eine Kaufposition mit festen Preisen Take-Profit und Stop-Loss. Die nächste Position sollte erst geöffnet werden, wenn die vorherige geschlossen ist. Der zweite Strategiecode ist dem ersten sehr ähnlich und befindet sich im angehängten Zip-Archiv. Die neue Strategieinitialisierungsklasse ist ebenfalls im Archiv enthalten.

Lassen Sie uns überlegen, welche Änderungen an der EA-Datei vorgenommen werden sollten, mit den Eingabeparameter und Funktionen enthält:

enum CSTRAT {
   strategy_1 = 1,
   strategy_2 = 2
};

input CSTRAT strt  = strategy_1;

CSTRAT str_curr = -1;

int OnInit()
  {  
   ...
   if (!SwitchStrategy(strt) ) return (INIT_FAILED);
   ...       
   return (INIT_SUCCEEDED);
  }
  
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
      if (id == CHARTEVENT_OBJECT_CLICK && StringCompare(...) == 0 ) {
         SwitchStrategy((CSTRAT)EDlg.GetStratID() );
      } 
  }  

bool SwitchStrategy(CSTRAT sr) {
   if (str_curr == sr) return true;
   CStrategy* s = NULL;
   switch(sr) {
      case strategy_1:
         {
            CInit1 ci;
            if (!ci.Initialize(GetPointer(par), GetPointer(snap), GetPointer(trade)) ) return false;
            s = new CRealStrat1();
            s.Initialize(GetPointer(ci));  
         }
         break;
      case strategy_2:
         {
            CInit2 ci;
            if (!ci.Initialize(GetPointer(par), GetPointer(snap), GetPointer(trade)) ) return false;
            s = new CRealStrat2();
            s.Initialize(GetPointer(ci));              
         }   
         break;
   }
   if (CheckPointer(pS1) != POINTER_INVALID) delete pS1;
   pS1 = s;    
   str_curr = sr;
   return true;
}

Die Funktion, um die Strategie umzuschalten, SwitchStrategy(....) und OnChartEvent(....) sind mit einem Handelspanel verbunden. Der Code wird nicht im Artikel besprochen, sondern befindet sich im angehängten Zip-Archiv. Auch ein dynamisches Strategiemanagement ist keine komplizierte Aufgabe. Erstellen Sie ein neues Objekt mit der Strategie, löschen Sie das vorherige und weisen Sie einen neuen Pointer der Variablen zu:

CStrategy* pS1;


Danach greift der EA auf die neue Strategie in OnTick() zu und verwendet damit die neue Betriebslogik. Die Hierarchie der Objekte und die Hauptabhängigkeiten sehen so aus:

Die Abbildung zeigt nicht das Handelspanel und die Verbindungen, die bei der Initialisierung als sekundär auftreten. In dieser Phase können wir unsere Aufgabe als erledigt betrachten: Der Expert Advisor ist betriebsbereit und weiter verbesserungsfähig. Mit dem verwendeten Block können kritische Verbesserungen und Modifikationen in relativ kurzer Zeit vorgenommen werden.

    Schlussfolgerung

    Wir haben einen Expert Advisor mit Elementen aus den Standarddesignmustern Beobachter und Fassade entworfen. Die vollständige Beschreibung dieser (und anderer) Muster ist im Buch "Design Patterns. Elements of Reusable Object-Oriented Software" von Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides. Ich empfehle Ihnen, dieses Buch zu lesen.

    Programme, die im diesem Artikel verwendet werden

     # Name
    Typ
     Beschreibung
    1
    Ea&Modules.zip Archive Ein Zip-Archiv mit den Dateien des Expert Advisors


    Übersetzt aus dem Russischen von MetaQuotes Ltd.
    Originalartikel: https://www.mql5.com/ru/articles/7318

    Beigefügte Dateien |
    EaxModules.zip (7.74 KB)
    Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XX): Erstellen und Speichern von Programmressourcen Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XX): Erstellen und Speichern von Programmressourcen
    Der Artikel beschäftigt sich mit dem Speichern von Daten im Quellcode des Programms und dem Erstellen von Audio- und Grafikdateien daraus. Bei der Entwicklung einer Anwendung benötigen wir oft Audio und Bilder. Die MQL-Sprache verfügt über mehrere Methoden zur Verwendung solcher Daten.
    Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XIX): Klassenbibliothek für Nachrichten Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XIX): Klassenbibliothek für Nachrichten
    In diesem Artikel werden wir die Klasse für die Darstellung von Textnachrichten besprechen. Derzeit haben wir eine ausreichende Anzahl verschiedener Textnachrichten. Es ist an der Zeit, die Methoden für die Speicherung, Anzeige und Übersetzung von russischen oder englischen Nachrichten in andere Sprachen neu zu organisieren. Außerdem wäre es gut, praktische Möglichkeiten einzuführen, um der Bibliothek neue Sprachen hinzuzufügen und schnell zwischen ihnen zu wechseln.
    Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XXI): Handelsklassen - Plattformübergreifendes Basis-Handelsobjekt Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XXI): Handelsklassen - Plattformübergreifendes Basis-Handelsobjekt
    In diesem Artikel werden wir mit der Entwicklung des neuen Bibliotheksbereichs beginnen - die Handelsklassen. Außerdem werden wir die Entwicklung eines einheitlichen Basisobjekts für den Handel auf den Plattformen MetaTrader 5 und MetaTrader 4 in Betracht ziehen. Wenn ein Auftrag an den Server gesendet wird, bedeutet ein solches Handelsobjekt, dass verifizierte und korrekte Parameter der Handelsanfrage an den Server übergeben werden.
    Entwicklung des Pivot Mean Oscillators: ein neuartiger Indikator für einen kumulativen gleitenden Durchschnitt Entwicklung des Pivot Mean Oscillators: ein neuartiger Indikator für einen kumulativen gleitenden Durchschnitt
    Dieser Artikel stellt den Pivot Mean Oscillator (PMO) vor, eine Implementierung des kumulativen Moving Average (CMA) als Handelsindikator für die MetaTrader-Plattformen. Insbesondere führen wir zunächst Pivot Mean (PM) als Normalisierungsindex für Zeitreihen ein, der den Bruchteil zwischen einem beliebigen Datenpunkt und dem CMA berechnet. Wir bilden dann den PMO als Differenz zwischen den gleitenden Durchschnitten, die auf zwei PM-Signale angewendet werden. Einige erste Experimente, die mit dem EURUSD-Symbol durchgeführt wurden, um die Wirksamkeit des vorgeschlagenen Indikators zu testen, werden ebenfalls besprochen, so dass genügend Raum für weitere Überlegungen und Verbesserungen bleibt.