MVC-Entwurfsmuster und seine mögliche Anwendung

Andrei Novichkov | 25 Mai, 2021

Einführung

Ich vermute, dass viele Entwickler eine Phase durchlaufen haben, in der das Projekt wächst, komplexer wird und neue Funktionen hinzukommen und so beginnt der Code mehr und mehr einer langen Spaghetti zu ähneln. Das Projekt ist noch nicht zu Ende, und es ist schon sehr schwierig, sich daran zu erinnern, an welchen Stellen diese oder jene Methode aufgerufen wird, warum dieser Aufruf genau hier steht und wie das alles funktioniert.

Mit der Zeit wird es selbst für den Codeautor immer schwieriger, den Code zu verstehen. Noch schlimmer ist es, wenn ein anderer Entwickler versucht, diesen Code zu verstehen. Die Aufgabe wird praktisch unlösbar, wenn der Codeautor zu diesem Zeitpunkt aus irgendeinem Grund nicht verfügbar ist. Ein unstrukturierter Code ist sehr schwer zu pflegen und zu modifizieren für jeden Code, der schwieriger ist als "Hello, world". Dies ist einer der Gründe für das Aufkommen von Entwurfsmustern. Sie bringen eine gewisse Struktur in das Projekt, machen es übersichtlicher und visuell verständlicher.


Das MVC-Muster und sein Zweck

Dieses Muster tauchte schon vor langer Zeit auf (1978), aber seine erste Beschreibung erschien erst viel später, nämlich im Jahr 1988. Seitdem hat sich das Muster weiterentwickelt und neue Ansätze hervorgebracht.

In diesem Artikel werden wir das "klassische MVC" betrachten, ohne jegliche Komplikationen oder zusätzliche Funktionalität. Die Idee ist, einen bestehenden Code in drei separate Komponenten aufzuteilen: Model, View (Darstellung) und Controller. Nach dem MVC-Muster können diese drei Komponenten unabhängig voneinander entwickelt und gewartet werden. Jede Komponente kann von einer separaten Gruppe von Entwicklern entwickelt werden, die sich um die Erstellung neuer Versionen und die Behebung von Fehlern kümmern. Dies kann natürlich die Verwaltung des Gesamtprojekts erheblich vereinfachen. Außerdem kann es anderen Personen helfen, den Code zu verstehen.

Werfen wir einen Blick auf die einzelnen Komponenten.

  1. View. View ist für die visuelle Darstellung von Informationen zuständig. In einem allgemeinen Fall sendet es Daten an den Benutzer. Es kann verschiedene Methoden geben, um dieselben Daten dem Benutzer zu präsentieren. Zum Beispiel können Daten gleichzeitig durch eine Tabelle, ein Diagramm oder ein Diagramm dargestellt werden. Mit anderen Worten: Eine MVC-basierte Anwendung kann mehrere Views enthalten. Views erhalten Daten vom Model, ohne zu wissen, was im Model passiert.
  2. Model. Das Model enthält Daten. Es verwaltet Verbindungen mit Datenbanken, sendet Anfragen und kommuniziert mit verschiedenen Ressourcen. Es verändert die Daten, prüft sie, speichert und löscht sie, wenn nötig. Model weiß nichts darüber, wie die Ansicht funktioniert und wie viele Ansichten existieren, aber es hat die notwendigen Schnittstellen, über die die Ansichten Daten anfordern können. Mehr können die Views nicht tun, d. h. sie können Model nicht zwingen, seinen Zustand zu ändern. Dieser Teil wird vom Controller übernommen. Intern kann Model aus mehreren anderen Models zusammengesetzt sein, die in einer Hierarchie angeordnet sind oder gleichberechtigt arbeiten. Das Model ist in dieser Hinsicht nicht eingeschränkt, abgesehen von der bereits erwähnten Einschränkung — das Model hält seine interne Struktur vor der View und dem Controller geheim.
  3. Controller. Controller implementiert die Kommunikation zwischen dem Nutzer und Model. Der Controller weiß nicht, was das Model mit den Daten macht, aber er kann dem Model mitteilen, dass es an der Zeit ist, den Inhalt zu aktualisieren. Im Allgemeinen arbeitet der Controller mit dem Model über dessen Schnittstelle, ohne zu versuchen zu verstehen, was in ihm vorgeht.

Die Beziehung zwischen den einzelnen Komponenten des MVC-Musters kann visuell wie folgt dargestellt werden:

Dennoch gibt es keine besonders strengen Regeln und Einschränkungen für den Einsatz von MVC. Der Entwickler sollte darauf achten, keine Model-Betriebslogik in den Controller einzubauen und sich nicht in View einzumischen. Der Controller selbst sollte schlanker gestaltet werden; man sollte ihn nicht überladen. Das MVC-Schema wird auch für andere Entwurfsmuster verwendet, wie z. B. Observer und Strategy.

Betrachten wir nun, wie das MVC-Schema in MQL verwendet werden kann und ob es notwendig ist, es zu verwenden.


Der einfachste Indikator aus der MVC-Perspektive

Lassen Sie uns einen einfachen Indikator erstellen, der mit den einfachsten Berechnungen eine Linie zeichnen kann. Der Indikator ist sehr klein und sein Code passt in eine Datei. So könnte er aussehen:

.......
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1
//--- plot Label1
#property indicator_label1  "Label1"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDarkSlateBlue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  2
//--- indicator buffers
double         lb[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   SetIndexBuffer(0, lb, INDICATOR_DATA);
   ArraySetAsSeries(lb, true);
   IndicatorSetString(INDICATOR_SHORTNAME, "Primitive1");
   IndicatorSetInteger(INDICATOR_DIGITS, _Digits);

   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   if(rates_total <= 4)
      return 0;

   ArraySetAsSeries(close, true);
   ArraySetAsSeries(open, true);

   int limit = rates_total - prev_calculated;

   if(limit == 0)
     {
     }
   else
      if(limit == 1)
        {

         lb[1] = (open[1] + close[1]) / 2;
         return(rates_total);

        }
      else
         if(limit > 1)
           {

            ArrayInitialize(lb, EMPTY_VALUE);

            limit = rates_total - 4;
            for(int i = limit; i >= 1 && !IsStopped(); i--)
              {
               lb[i] = (open[i] + close[i]) / 2;
              }
            return(rates_total);

           }

   lb[0] = (open[0] + close[0]) / 2;

   return(rates_total);
  }
//+------------------------------------------------------------------+

Der Indikator berechnet den Durchschnittswert von open[i] + close[i]. Der Quellcode ist im angehängten zip-Archiv MVC_primitive_1.zip enthalten.

Der Indikator ist sehr schlecht geschrieben, was erfahrenen Entwicklern leicht auffallen kann. Nehmen wir an, es besteht die Notwendigkeit, die Berechnungsmethode zu ändern: nur close[i] statt open[i] + close[i] zu verwenden. Dieser Indikator hat drei Stellen, an denen wir Änderungen vornehmen müssen. Was ist, wenn wir noch mehr Änderungen vornehmen oder die Berechnungen komplexer gestalten müssen? Offensichtlich ist es besser, Berechnungen in einer separaten Funktion zu implementieren. So können wir, wenn nötig, die entsprechenden logischen Korrekturen nur in dieser Funktion vornehmen.

So sehen der Handler und die Funktion jetzt aus:

double Prepare(const datetime &t[], const double &o[], const double &h[], const double &l[], const double &c[], int shift) {
   
   ArraySetAsSeries(c, true);
   ArraySetAsSeries(o, true);
   
   return (o[shift] + c[shift]) / 2;
}
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[]) {
                
   if(rates_total <= 4) return 0;
   
   int limit = rates_total - prev_calculated;
   
   if (limit == 0)        {
   } else if (limit == 1) {
   
      lb[1] = Prepare(time, open, high, low, close, 1);      
      return(rates_total);

   } else if (limit > 1)  {
   
      ArrayInitialize(lb, EMPTY_VALUE);
      
      limit = rates_total - 4;
      for(int i = limit; i >= 1 && !IsStopped(); i--) {
         lb[i] = Prepare(time, open, high, low, close, i);
      }
      return(rates_total);
      
   }
   lb[0] = Prepare(time, open, high, low, close, 0);

   return(rates_total);
}

Bitte beachten Sie, dass fast alle Zeitreihen an die neue Funktion übergeben werden. Warum wohl? Das ist nicht notwendig, da nur zwei Zeitreihen verwendet werden: open und close. Wir gehen aber davon aus, dass es in Zukunft viele Änderungen und Verbesserungen im Indikator geben kann, bei denen auch die restlichen Zeitreihen verwendet werden können. Eigentlich implementieren wir eine solide Basis für mögliche Versionen.

Betrachten wir nun den aktuellen Code aus dem Blickwinkel des MVC-Templates.

Versuchen wir, unseren Indikator auf der Grundlage der obigen Erklärung nachzubauen. Dazu implementieren wir den Code der Komponenten nicht nur in verschiedenen Dateien, sondern auch in verschiedenen Ordnern. Dies ist eine sinnvolle Lösung, da es mehrere Views geben kann, das Model andere Models enthalten kann und der Controller sehr komplex sein kann. So sieht die Hauptindikator-Datei jetzt aus:

//+------------------------------------------------------------------+
//|                                              MVC_primitive_2.mq5 |
//|                                Copyright 2021, Andrei Novichkov. |
//|                    https://www.mql5.com/en/users/andreifx60/news |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, Andrei Novichkov."
#property link      "https://www.mql5.com/en/users/andreifx60/news"

#property version   "1.00"

#property indicator_chart_window

#property indicator_buffers 1
#property indicator_plots   1

#include "View\MVC_View.mqh"
#include "Model\MVC_Model.mqh"


//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit() {

   return Initialize();
}

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[]) {
                
   if(rates_total <= 4) return 0;
   
   int limit = rates_total - prev_calculated;
   
   if (limit == 0)        {
   } else if (limit == 1) {
   
      lb[1] = Prepare(time, open, high, low, close, 1);      
      return(rates_total);

   } else if (limit > 1)  {
   
      ArrayInitialize(lb, EMPTY_VALUE);
      
      limit = rates_total - 4;
      for(int i = limit; i >= 1 && !IsStopped(); i--) {
         lb[i] = Prepare(time, open, high, low, close, i);
      }
      return(rates_total);
      
   }
   lb[0] = Prepare(time, open, high, low, close, 0);

   return(rates_total);
}
//+------------------------------------------------------------------+

Ein paar Worte über die Eigenschaften des Indikators:

#property indicator_buffers 1
#property indicator_plots   1

Diese beiden Zeilen könnten in View (die Datei MVC_View.mqh) verschoben werden. Dies würde jedoch die Erzeugung einer Compiler-Warnung verursachen:

no indicator plot defined for indicator

Daher werden diese beiden Zeilen in der Hauptdatei belassen, die den Controller-Code enthält. Der Quellcode des Indikators befindet sich in dem angehängten zip-Archiv MVC_primitive_2.zip.

Achten Sie nun bitte auf die Kommunikation zwischen den einzelnen Komponenten des Musters. Derzeit gibt es keine Kommunikation. Wir verbinden einfach zwei Include-Dateien und alles funktioniert. Insbesondere enthält View einen Indikatorpuffer in Form einer globalen Variable und eine Funktion, in der die Initialisierung durchgeführt wird. Lassen Sie uns diesen Teil auf eine korrektere und sicherere Weise umschreiben. Fassen wir den Puffer, seine Initialisierung und den Zugriff auf ihn in einem Objekt zusammen. So entsteht ein bequemer und kompakter Code, der leicht zu debuggen und zu warten ist. Außerdem bietet dieser Ansatz alle Möglichkeiten für weitere Programmverbesserungen. er Entwickler kann einen Teil des Codes in eine Basisklasse oder eine Schnittstelle verschieben und so z. B. ein Array von Views erstellen. So könnte die neue View aussehen:

class CView 
  {
   public:
      void CView();
      void ResetBuffers();
      int  Initialize();
      void SetData(double value, int shift = 0);
      
   private:
      double _lb[];
      int    _Width;
      string _Name;
      string _Label;    
  
  };// class CView

void CView::CView() 
  {
      _Width = 2;
      _Name  = "Primitive" ;
      _Label = "Label1"; 
  }// void CView::CView()

void CView::ResetBuffers()
  {
   ArrayInitialize(_lb, EMPTY_VALUE);
  }

int CView::Initialize() 
  {
      SetIndexBuffer     (0,   _lb, INDICATOR_DATA);
      ArraySetAsSeries   (_lb, true);
   
      IndicatorSetString (INDICATOR_SHORTNAME, _Name);
      IndicatorSetInteger(INDICATOR_DIGITS,    _Digits);
   
      PlotIndexSetString (0, PLOT_LABEL,      _Label);
      PlotIndexSetInteger(0, PLOT_DRAW_TYPE,  DRAW_LINE);
      PlotIndexSetInteger(0, PLOT_LINE_COLOR, clrDarkSlateBlue);
      PlotIndexSetInteger(0, PLOT_LINE_STYLE, STYLE_SOLID);
      PlotIndexSetInteger(0, PLOT_LINE_WIDTH, _Width);   
      
      return(INIT_SUCCEEDED);   
  }

void CView::SetData(double value,int shift) 
  {   
   _lb[shift] = value;
  }

Achten Sie auf die letzte Methode SetData. Wir verbieten den unkontrollierten Zugriff auf den Indikatorpuffer und implementieren eine spezielle Methode für den Zugriff, die mit zusätzlichen Prüfungen versehen werden kann. Optional kann die Methode in der Basisklasse als virtuell deklariert werden. Auch in der Controller-Datei gibt es einige kleinere Änderungen, die hier aber nicht vorgestellt werden. Offensichtlich benötigen wir hier einen weiteren Konstruktor, in dem wir Pufferinitialisierungsparameter, wie Farbe, Stil und andere, übergeben können.

Auch die Aufrufe im Objekt der ersten View sehen nicht passend aus:

      IndicatorSetString (INDICATOR_SHORTNAME, _Name);
      IndicatorSetInteger(INDICATOR_DIGITS, _Digits);

In der Realität sollten sie aus der Klasse CView entfernt werden, wobei folgendes zu beachten ist:

Der Quellcode dieses Indikators befindet sich in dem angehängten Zip-Archiv MVC_primitive_3.zip.


Damit ist die Hauptindikator-Datei — die Datei mit dem Controller-Code — deutlich kürzer geworden. Der gesamte Code ist nun sicherer und für zukünftige Änderungen und Debugging bereit. Aber ist er nun für andere Entwickler übersichtlicher? Das ist eher zweifelhaft. In diesem Fall könnte es sinnvoller sein, den Indikatorcode in einer Datei zu belassen, die Controller, Model und View vereint. So wie es ganz am Anfang war.


Diese Sichtweise erscheint logisch. Sie ist aber nur für diesen speziellen Indikator anwendbar. Stellen Sie sich einen Indikator vor, der aus einem Dutzend Dateien besteht, ein grafisches Panel hat und Daten über das Web abfragt. In diesem Fall wäre das MVC-Modell sehr nützlich. MVC würde den Indikator für andere Personen leicht verständlich machen, eine bequeme Erkennung von Fehlern ermöglichen und die Grundlage für mögliche Logikmodifikationen und andere Änderungen bieten. Wollen Sie einen Spezialisten für einen bestimmten Entwicklungsteil gewinnen? Dies wird viel einfacher zu bewerkstelligen sein. Müssen wir ein weiteres Initialisierungsschema hinzufügen? Auch das ist machbar. Die Schlussfolgerung aus dem oben Gesagten ist ziemlich offensichtlich: Je komplexer das Projekt, desto nützlicher ist das MVC-Muster.

Gilt es nur für Indikatoren? Betrachten wir die Struktur von Expert Advisors im Hinblick auf die Möglichkeit der Verwendung des MVC-Musters.


MVC in Expert Advisors

Lassen Sie uns einen sehr einfachen Pseudo-Expert Advisor erstellen. Er soll eine Kaufposition eröffnen, wenn die vorherige Kerze gestiegen ist, und eine Verkaufsposition, wenn sie gefallen ist. Der Einfachheit halber wird der EA keine echten Positionen eröffnen, sondern nur den Ein- und Ausstieg simulieren. Es wird nur eine Position auf dem Markt existieren. In diesem Fall kann der Code des EAs (Ea_primitive.mq5 im angehängten zip-Archiv) wie folgt aussehen:

datetime dtNow;

int iBuy, iSell;

int OnInit() 
  {
   iBuy  = iSell = 0;
   
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason) 
  {

  }

void OnTick() 
  {
      if (IsNewCandle() ) 
        {
         double o = iOpen(NULL,PERIOD_CURRENT,1); 
         double c = iClose(NULL,PERIOD_CURRENT,1); 
         if (c < o) 
           { // Enter Sell
            if (GetSell() == 1) return;
            if (GetBuy()  == 1) CloseBuy();
            EnterSell();
           }
         else 
           {      // Enter Buy
            if (GetBuy()  == 1) return;
            if (GetSell() == 1) CloseSell();
            EnterBuy();
           }           
        }// if (IsNewCandle() )   
  }// void OnTick()

bool IsNewCandle() 
  {
   datetime d = iTime(NULL, PERIOD_CURRENT, 0);
   if (dtNow == -1 || dtNow != d) 
     {
      dtNow = d;
      return true;
     }  
   return false;
  }// bool IsNewCandle()

void CloseBuy()  {iBuy = 0;}

void CloseSell() {iSell = 0;}

void EnterBuy()  {iBuy = 1;}

void EnterSell() {iSell = 1;}

int GetBuy()     {return iBuy;}

int GetSell()    {return iSell;}

Bei der Betrachtung von Indikatoren haben wir bereits festgestellt, dass sich die OnInit, OnDeinit und andere Funktionen auf den Controller beziehen. Das Gleiche gilt für Expert Advisors. Was aber ist View zuzuordnen? Es stellt keine grafischen Objekte oder Charts dar. Wie Sie wissen, ist View für die Datenpräsentation für den Nutzer zuständig. Bei Expert Advisors ist die Datenpräsentation die Anzeige der offenen Positionen. Damit ist View alles, was mit den Positionen zu tun hat. Dazu gehören Orders, Trailing Stop, virtueller Stop-Loss und Take-Profit, gewichtete Durchschnittspreise, etc.

Dann wird das Modell die Logik der Entscheidung über die Positionseröffnung, die Losgrößenbestimmung, die Definition von Take-Profit und Stop-Loss beinhalten. Das Money-Management sollte ebenfalls im Modell implementiert werden. Dies beginnt, wie ein geschlossenes System auszusehen, das aus mehreren Teilmodellen besteht: Preisanalyse, Volumenberechnung, Überprüfung des Kontostandes (möglicherweise Überprüfung des Zustandes anderer Teilmodelle) und die daraus resultierende Entscheidung über den Markteintritt.

Ändern wir die Struktur der Pseudo-EA gemäß den obigen Überlegungen. Wir haben keine Losgrößenberechnung oder Arbeit mit dem Konto, also lassen Sie uns die Schritte durchführen, die wir können — Funktionen, die mit verschiedenen Komponenten verbunden sind, in ihre Unterordner verschieben und einige von ihnen bearbeiten. So wird sich der Pseudocode von OnTick ändern:

void OnTick() 
  {
      if (IsNewCandle() ) 
        {
         double o = iOpen(NULL,PERIOD_CURRENT,1); 
         double c = iClose(NULL,PERIOD_CURRENT,1); 
         if (MaySell(o, c) ) EnterSell();
         if (MayBuy(o, c)  ) EnterBuy();
        }// if (IsNewCandle() )   
  }// void OnTick()

Auch in diesem Abschnitt können wir sehen, dass der Code kürzer geworden ist. Ist er für einen Fremdentwickler übersichtlicher geworden? Auch hier gelten die Annahmen, die wir zuvor für Indikatoren besprochen haben:

- Je komplexer der EA ist, desto sinnvoller ist das MVC-Muster.

Der gesamte EA befindet sich in dem angehängten MVC_EA_primitive.zip Archiv. Lassen Sie uns nun versuchen, das MVC-Muster auf einen "echten" Code anzuwenden.

Dazu verwenden wir einen einfachen Expert Advisor, der nicht unbedingt ein funktionierender oder gut geschriebener sein muss. Im Gegenteil, der Expert Advisor sollte schlecht geschrieben sein — auf diese Weise können wir den Effekt der Anwendung des Musters bewerten.

Zu diesem Zweck habe ich einen alten Entwurf des $OrdersInTheMorning EA aus dem Jahr 2013 gefunden. Seine Strategie war wie folgt:

Da der EA für MetaTrader 4 entwickelt wurde, musste ich ihn für MetaTrader 5 neu erstellen (was allerdings sehr nachlässig gemacht wurde). Hier sind die Hauptfunktionen des EA in ihrer ursprünglichen Form:

#property copyright "Copyright 2013, MetaQuotes Software Corp."
#property link      "http://www.metaquotes.net"

#include <Trade\Trade.mqh>
//+------------------------------------------------------------------+
//| script program start function                                    |
//+------------------------------------------------------------------+
input double delta = 200;
input double volumes = 0.03; 
input double sTopLossKoeff = 1;
input double tAkeProfitKoeff = 2; 
input int iTHour = 0; 
input bool bHLprocess = true;
input bool oNlyMondeyOrders = false; 
input string sTimeToCloseOrders = "22:00"; 
input string sTimeToOpenOrders  = "05:05"; 
input double iTimeIntervalForWork = 0.5;
input int iSlippage = 15; 
input int iTradeCount = 3; 
input int iTimeOut = 2000;

int dg;
bool bflag;

string smb[] = {"AUDJPY","CADJPY","EURJPY","NZDJPY","GBPJPY","CHFJPY"};

int init ()
{
   if ( (iTimeIntervalForWork < 0) || (iTimeIntervalForWork > 24) )
   {
      Alert ("... ",iTimeIntervalForWork);
   }
   return (0);
}

void OnTick()
{
   if ((oNlyMondeyOrders == true) && (DayOfWeek() != 1) ) 
   {
   }
   else
   {
         int count=ArraySize(smb);
         bool br = true;
         for (int i=0; i<count;i++)
         {
            if (!WeekOrderParam(smb[i], PERIOD_H4, delta*SymbolInfoDouble(smb[i],SYMBOL_POINT) ) )
               br = false;
         }
         if (!br)
            Alert("...");
         bflag = true; 
    }//end if if ((oNlyMondeyOrders == true) && (DayOfWeek() != 1) )  else...
    
   if ((oNlyMondeyOrders == true) && (DayOfWeek() != 5) ) 
   {
   }
   else
   {
         if (OrdersTotal() != 0)
            Alert ("...");      
   }//end if ((oNlyMondeyOrders == true) && (DayOfWeek() != 5) )  else...
}
  
  bool WeekOrderParam(string symbol,int tf, double dlt)
  {
   int j = -1;
   datetime mtime = 0;
   int k = 3;
   Alert(symbol);
   if (iTHour >= 0)
   {
      if (oNlyMondeyOrders == true)
      {
         for (int i = 0; i < k; i++)
         {
            mtime = iTime(symbol,0,i);
            if (TimeDayOfWeek(mtime) == 1)
            {
               if (TimeHour(mtime) == iTHour)
               {
                  j = i;
                  break;
               }
            }
         }
      }
      else
      {
         for (int i = 0; i < k; i++)
         {
            mtime = iTime(symbol,0,i);
            if (TimeHour(mtime) == iTHour)
            {
               j = i;
               break;
            }
         }   
      }
      if (j == -1) 
      {
         Print("tf?");
         return (false);
      }
   }//end if (iTHour >= 0)
   else 
      j = 0;
   Alert(j);
   double bsp,ssp;
   if (bHLprocess)
   {
      bsp = NormalizeDouble(iHigh(symbol,0,j) + dlt, dg); 
      ssp = NormalizeDouble(iLow(symbol,0,j) - dlt, dg); 
   }
   else
   {
      bsp = NormalizeDouble(MathMax(iOpen(symbol,0,j),iClose(symbol,0,j)) + dlt, dg); 
      ssp = NormalizeDouble(MathMin(iOpen(symbol,0,j),iClose(symbol,0,j)) - dlt, dg);  
   }
   double slsize = NormalizeDouble(sTopLossKoeff * (bsp - ssp), dg); 
   double tpb = NormalizeDouble(bsp + tAkeProfitKoeff*slsize, dg); 
   double tps = NormalizeDouble(ssp - tAkeProfitKoeff*slsize, dg);
   datetime expr = 0;
   return (mOrderSend(symbol,ORDER_TYPE_BUY_STOP,volumes,bsp,iSlippage,ssp,tpb,NULL,0,expr,CLR_NONE) && mOrderSend(symbol,ORDER_TYPE_SELL_STOP,volumes,ssp,iSlippage,bsp,tps,NULL,0,expr,CLR_NONE) );
  }
  
 int mOrderSend( string symbol, int cmd, double volume, double price, int slippage, double stoploss, double takeprofit, string comment = "", int magic=0, datetime expiration=0, color arrow_color=CLR_NONE) 
 {
   int ticket = -1;
      for (int i = 0; i < iTradeCount; i++)
      {
//         ticket=OrderSend(symbol,cmd,volume,price,slippage,stoploss,takeprofit,comment,magic,expiration,arrow_color);
         if(ticket<0)
            Print(symbol,": ",GetNameOP(cmd), GetLastError() ,iTimeOut);
         else
            break;
      }
   return (ticket);
 }  
 

Es hat einen Initialisierungsblock, OnTick und Hilfsfunktionen. Die Funktionen werden im Controller belassen. Der veraltete Aufruf von init sollte korrigiert werden. Achten Sie nun auf OnTick. Innerhalb der Funktion gibt es einige Prüfungen und eine Schleife, in der die Hilfsfunktion WeekOrderParam aufgerufen wird. Diese Funktion bezieht sich auf Entscheidungen, die den Markteintritt und die Positionseröffnung betreffen. Dieser Ansatz ist absolut falsch. Wie Sie sehen können, ist die Funktion lang; sie hat mehrfach verschachtelte Bedingungen und Schleifen. Diese Funktion sollte mindestens in zwei Teile aufgeteilt werden. Die letzte Funktion mOrderSend ist recht gut — sie bezieht sich auf View, basierend auf den oben genannten Ideen. Neben den Änderungen in der EA-Struktur gemäß dem Muster muss auch der Code selbst korrigiert werden. Die Kommentare werden zusammen mit den entsprechenden Änderungen gegeben.

Beginnen wir damit, die Liste der Währungspaare in die Eingabeparameter zu verschieben. Entfernen Sie das Überflüssige aus dem OnInit. Wir erstellen die Datei EA_Init.mqh, die die Initialisierungsdetails enthalten wird und verbinden diese Datei mit der Hauptdatei. In dieser neuen Datei erstellen wir eine Klasse und führen darin alle Initialisierungen durch:

class CInit {
public:
   void CInit(){}
   void Initialize(string pair);
   string names[];
   double points[];     
   int iCount;
};

void CInit::Initialize(string pair) {
   
   iCount = StringSplit(pair, StringGetCharacter(",", 0), names);
   ArrayResize(points, iCount);
   for (int i = 0; i < iCount; i++) {
      points[i] = SymbolInfoDouble(names[i], SYMBOL_POINT);
   }
}

Der Code ist sehr einfach. Ich werde ein paar Punkte erklären:

Legen wir ein Objekt des erstellten Klassentyps in der Haupt-EA-Datei an und rufen seine Initialisierungsmethode in OnInit auf.

Fahren wir nun mit dem Model fort. Wir löschen alle Inhalte aus OnTick, erstellen den Ordner für Model und darin die Datei Model.mqh und die Klasse CModel in der neuen Datei. Die Klasse sollte zwei Methoden zur Überprüfung der Marktein- und -austrittsbedingungen enthalten. Außerdem müssen wir in dieser Klasse das Flag speichern, das anzeigt, dass Positionen geöffnet oder geschlossen wurden. Beachten Sie, dass ohne die Notwendigkeit, dieses Flag zu speichern, die Existenz der gesamten Klasse nicht notwendig wäre. Ein paar Funktionen würden ausreichen. Wenn wir unter realen Bedingungen handeln, müssten wir zusätzliche Prüfungen implementieren, wie z. B. Volumen, Fonds und andere. Sie alle sollten im Model implementiert werden. Jetzt sieht die Datei, die das Model enthält, wie folgt aus:

class CModel {
public:
         void CModel(): bFlag(false) {}
         bool TimeToOpen();
         bool TimeToClose();
private:
   bool bFlag;   
};

bool CModel::TimeToOpen() {

   if (bFlag) return false;

   MqlDateTime tm;
   TimeCurrent(tm);
   if (tm.day_of_week != 1) return false;
   if (tm.hour < iHourOpen) return false;
   
   bFlag = true;

   return true;   
}

bool CModel::TimeToClose() {

   if (!bFlag) return false;
   
   MqlDateTime tm;
   TimeCurrent(tm);
   if (tm.day_of_week != 5)  return false;
   if (tm.hour < iHourClose) return false;
   
   bFlag = false;

   return true;   
}

Wir legen wie im vorherigen Fall ein Objekt dieses Klassentyps in der Haupt-EA-Datei an und fügen dessen Methodenaufrufe in den OnInit ein.

Lassen Sie uns nun mit View fortfahren. Wir legen einen Ordner View und darin eine Datei View.mqh an. Diese Datei enthält Elemente zum Öffnen/Schließen von Orders und Positionen. Sie enthält auch Komponenten zur Verwaltung von virtuellen Levels, Trailing Stop und verschiedenen grafischen Objekten. In diesem Fall besteht das primäre Ziel darin, den Code klar und einfach zu gestalten. Als Option wollen wir versuchen, die View-Komponente ohne Verwendung von Klassen zu implementieren. Die View-Komponente wird drei Funktionen haben: eine für den Markteintritt, die zweite für das Schließen aller Positionen und die dritte für das Schließen von Orders. Jede der drei Funktionen verwendet ein Objekt vom Typ CTrade, das bei jeder Verwendung neu erstellt werden muss. Dies ist nicht optimal:

void Enter() {
   
   CTrade trade;
   
   trade.SetExpertMagicNumber(Magic);
   trade.SetMarginMode();
   trade.SetDeviationInPoints(iSlippage);      
   
   double dEnterBuy, dEnterSell;
   double dTpBuy,    dTpSell;
   double dSlBuy,    dSlSell;
   double dSlSize;
   
   for (int i = 0; i < init.iCount; i++) {
      dEnterBuy  = NormalizeDouble(iHigh(init.names[i],0,1) + delta * init.points[i], _Digits);  
      dEnterSell = NormalizeDouble(iLow(init.names[i],0,1)  - delta * init.points[i], _Digits);  
      dSlSell    = dEnterBuy; 
      dSlBuy     = dEnterSell;
      dSlSize    = (dEnterBuy - dEnterSell) * tAkeProfitKoeff;
      dTpBuy     = NormalizeDouble(dEnterBuy + dSlSize, _Digits);
      dTpSell    = NormalizeDouble(dEnterSell - dSlSize, _Digits);
      
      trade.SetTypeFillingBySymbol(init.names[i]);
      
      trade.BuyStop(volumes,  dEnterBuy,  init.names[i], dSlBuy,  dTpBuy);
      trade.SellStop(volumes, dEnterSell, init.names[i], dSlSell, dTpSell);
   }
}

void ClosePositions() {

   CTrade trade;
   
   for (int i = PositionsTotal() - 1; i >= 0; i--) {  
      trade.PositionClose(PositionGetTicket(i) );
   }   
}

void CloseOrder(string pair) {

   CTrade trade;
   
   ulong ticket;
   for (int i = OrdersTotal() - 1; i >= 0; i--) {
      ticket = OrderGetTicket(i);
      if (StringCompare(OrderGetString(ORDER_SYMBOL), pair) == 0) {
         trade.OrderDelete(ticket);
         break;
      }
   }
}

Ändern wir nun den Code, indem wir die Klasse CView erstellen. Wir verschieben die bereits erstellten Funktionen in die neue Klasse und erstellen eine weitere Komponenteninitialisierungsmethode für eine private Variable vom Typ CTrade. In anderen Fällen erstellen wir ein Objekt des erstellten Klassentyps in der Hauptdatei und fügen dessen Initialisierungsmethodenaufruf in OnInit ein.

Nun müssen wir das Entfernen der nicht getriggerten Aufträge implementieren. Dazu fügen wir OnTrade zum Controller hinzu. Wir prüfen in der Funktion die Änderung der Anzahl der Aufträge: Wenn sie sich geändert hat, dann löschen wir den entsprechenden nicht ausgelösten Auftrag. Dieser Handler ist der einzige knifflige Teil des EAs. Erstellen wir eine Methode in der CView-Klasse und rufen diese von OnTrade des Controllers auf. Dann sieht View so aus:

#include <Trade\Trade.mqh>

class CView {

public:
   void CView() {}
   void Initialize();  
   void Enter();
   void ClosePositions();
   void CloseAllOrder();
   void OnTrade();
private:
   void InitTicketArray() {
      ArrayInitialize(bTicket, 0);
      ArrayInitialize(sTicket, 0);
      iOrders = 0;
   }
   CTrade trade; 
   int    iOrders;  
   ulong  bTicket[], sTicket[];

};

void CView::OnTrade() {

   if (OrdersTotal() == iOrders) return;
   
   for (int i = 0; i < init.iCount; i++) {
      if (bTicket[i] != 0 && !OrderSelect(bTicket[i]) ) {
         bTicket[i] = 0; iOrders--;
         if (sTicket[i] != 0) {
            trade.OrderDelete(sTicket[i]);
            sTicket[i] = 0; iOrders--;
         }
         continue;
      }
      
      if (sTicket[i] != 0 && !OrderSelect(sTicket[i]) ) {
         sTicket[i] = 0; iOrders--;
         if (bTicket[i] != 0) {
            trade.OrderDelete(bTicket[i]);
            bTicket[i] = 0; iOrders--;
         }
      }      
   }
}

void CView::Initialize() {

   trade.SetExpertMagicNumber(Magic);
   trade.SetMarginMode();
   trade.SetDeviationInPoints(iSlippage);  
   
   ArrayResize(bTicket, init.iCount);
   ArrayResize(sTicket, init.iCount);
   
   InitTicketArray();
}

void CView::Enter() {
   
   double dEnterBuy, dEnterSell;
   double dTpBuy,    dTpSell;
   double dSlBuy,    dSlSell;
   double dSlSize;
   
   for (int i = 0; i < init.iCount; i++) {
      dEnterBuy  = NormalizeDouble(iHigh(init.names[i],0,1) + delta * init.points[i], _Digits);  
      dEnterSell = NormalizeDouble(iLow(init.names[i],0,1)  - delta * init.points[i], _Digits);  
      dSlSell    = dEnterBuy; 
      dSlBuy     = dEnterSell;
      dSlSize    = (dEnterBuy - dEnterSell) * tAkeProfitKoeff;
      dTpBuy     = NormalizeDouble(dEnterBuy + dSlSize, _Digits);
      dTpSell    = NormalizeDouble(dEnterSell - dSlSize, _Digits);
      
      trade.SetTypeFillingBySymbol(init.names[i]);
      
      trade.BuyStop(volumes,  dEnterBuy,  init.names[i], dSlBuy,  dTpBuy);
      bTicket[i] = trade.ResultOrder();
      
      trade.SellStop(volumes, dEnterSell, init.names[i], dSlSell, dTpSell);
      sTicket[i] = trade.ResultOrder();
      
      iOrders +=2;
   }
}

void CView::ClosePositions() {
   
   for (int i = PositionsTotal() - 1; i >= 0; i--) {  
      trade.PositionClose(PositionGetTicket(i) );
   }   
   
   InitTicketArray();   
}

void CView::CloseAllOrder() {
   
   for (int i = OrdersTotal() - 1; i >= 0; i--) {
      trade.OrderDelete(OrderGetTicket(i));
   }
}

Wie man sieht, wurde der gesamte ursprüngliche Code neu geschrieben. Ist er besser geworden? Zweifelsohne! Das Ergebnis der gesamten Arbeit befindet sich in dem angehängten Archiv EA_Real.zip. Jetzt sieht die Hauptdatei des Expert Advisors (Controller) wie folgt aus:

input string smb             = "AUDJPY, CADJPY, EURJPY, NZDJPY, GBPJPY, CHFJPY";
input double delta           = 200;
input double volumes         = 0.03; 
input double tAkeProfitKoeff = 2; 
input int    iHourOpen       = 5; 
input int    iHourClose      = 22;
input int    iSlippage       = 15; 
input int    Magic           = 12345;

#include "EA_Init.mqh"
#include "View\View.mqh"
#include "Model\Model.mqh"

CInit  init;
CModel model;
CView  view;
 

int OnInit()
{
   init.Initialize(smb);
   view.Initialize();
  
   return INIT_SUCCEEDED;
}

void OnTick() {
   if (model.TimeToOpen() ) {
      view.Enter();
      return;
   }
   if (model.TimeToClose() ) {
      view.CloseAllOrder();
      view.ClosePositions();
   }
}

void OnTrade() {
   view.OnTrade();
}


Wenn nun etwas geändert, hinzugefügt oder repariert werden muss, können wir einfach mit einer entsprechenden Komponente des Expert Advisors arbeiten. Der Speicherort der Komponente ist leicht zu bestimmen. Wir können den EA weiter entwickeln, neue Funktionen implementieren, Modelle hinzufügen und die Ansicht erweitern. Wir können sogar eine der Komponenten komplett ändern, ohne die anderen beiden zu beeinflussen.


In der betrachteten MVC-Anwendung gibt es einen Aspekt, der am Anfang des Artikels erwähnt wurde. Es geht um die Interaktion der Muster-Komponenten untereinander. Aus der Sicht des Nutzers gibt es kein Problem: Wir haben einen Controller, dem wir ein Dialogfeld und ein Handelsfeld hinzufügen können. Wir haben auch Eingabeparameter als Teil des Controllers. Aber wie sollen Model und View interagieren? In unserem Expert Advisor interagieren sie nicht direkt miteinander, sondern nur über den Controller in OnTick. Darüber hinaus kommuniziert View mit dem Controller auf ähnliche Weise — durch den "direkten" Aufruf von Methoden des Objekts CInint. In diesem Fall wird die Interaktion der Komponenten über ihre globalen Objekte organisiert. Das liegt daran, dass unser Expert Advisor sehr einfach ist und ich den Code nicht verkomplizieren wollte.

Trotz dieser Einfachheit hat View jedoch elf Aufrufe an den Controller. Wenn wir den EA weiterentwickeln, kann sich die Anzahl der Interaktionen um ein Vielfaches erhöhen, was den positiven Effekt des MVC-Musters zunichte macht. Dieses Problem kann durch den Verzicht auf globale Objekte und den Zugriff auf Komponenten und Methoden per Referenz gelöst werden. Ein Beispiel für diese Art der Interaktion ist die MFC und ihre Komponenten Document und View.

Aus Sicht der Muster ist die Interaktion zwischen den Muster-Komponenten in keiner Weise geregelt. Daher werden wir nicht näher auf dieses Thema eingehen.


Schlussfolgerung

Schauen wir nun abschließend, wie wir die Struktur des Indikators oder des Expert Advisors, auf den wir ursprünglich das MVC-Muster angewendet haben, weiterentwickeln könnten. Nehmen wir an, es gibt zwei weitere Models. Und einen weiteren View. Der Controller ist viel komplexer geworden. Was sollen wir tun, um das MVC-Muster beizubehalten? Die Lösung ist hier die Verwendung von separaten Modulen. Es ist ganz einfach. Wir haben drei Komponenten, von denen jede eine Methode zum Zugriff auf sie bereitstellt. Jede Komponente besteht aus separaten Modulen. Diese Methode wurde hier erwähnt. Im selben Artikel wurden Möglichkeiten der Interaktion und Verwaltung auf Modulebene besprochen.


Die Programme dieses Artikels:
 # Name
Typ
 Beschreibung
1 MVC_primitive_1.zip Archive
Die erste und schlimmste Indikatorvariante.
2
MVC_primitive_2.zip
Archive
Die zweite Indikatorvariante mit der Aufteilung in Komponenten.
3 MVC_primitive_3.zip Archive Die dritten Indikatorvariante mit Objekten.
4 EA_primitive.zip Archive
Pseudo-Expert Advisor
5 MVC_EA_primitive.zip Archive Pseudo-Expert Advisor basierend auf MVC.
 6 EA_Real.zip
 Archive Expert Advisor basierend auf MVC.