English Русский 中文 Español 日本語 Português
preview
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 16): Auswirkungen unterschiedlicher Kursverläufe auf die Testergebnisse

Entwicklung eines Expertenberaters für mehrere Währungen (Teil 16): Auswirkungen unterschiedlicher Kursverläufe auf die Testergebnisse

MetaTrader 5Tester | 17 Februar 2025, 09:06
157 0
Yuriy Bykov
Yuriy Bykov

Einführung

Im vorherigen Artikel haben wir damit begonnen, den Multiwährungs-EA für den Handel auf einem realen Konto vorzubereiten. Als Teil des Vorbereitungsprozesses haben wir die Unterstützung für verschiedene Namen von Handelsinstrumenten, die automatische Beendigung des Handels, wenn Sie die Einstellungen von Handelsstrategien ändern möchten, und die korrekte Wiederaufnahme des EA nach einem Neustart aus verschiedenen Gründen hinzugefügt.

Die Vorbereitungsarbeiten sind damit noch nicht abgeschlossen. Wir haben einige weitere notwendige Schritte skizziert, auf die wir später zurückkommen werden. Betrachten wir nun einen so wichtigen Aspekt wie die Gewährleistung ähnlicher Ergebnisse bei verschiedenen Brokern. Es ist bekannt, dass die Notierungen für Handelsinstrumente bei verschiedenen Brokern nicht identisch sind. Daher wählen wir durch Tests und Optimierungen an einigen Angeboten die optimalen Parameter speziell für diese Angebote aus. Wir hoffen natürlich, dass, wenn wir mit dem Handel an anderen Kursen beginnen, die Unterschiede zu den für die Tests verwendeten Kursen gering sein werden und daher auch die Unterschiede in den Handelsergebnissen unbedeutend sein werden. 

Diese Frage ist jedoch zu wichtig, als dass sie nicht eingehend geprüft werden könnte. Schauen wir uns also an, wie sich unser EA verhält, wenn er mit Kursen von verschiedenen Brokern getestet wird.


Vergleich der Ergebnisse

Starten wir zunächst unseren EA mit Kursen vom MetaQuotes-Demo-Server. Der erste Start erfolgte mit aktiviertem Risikomanager. Mit Blick auf die Zukunft werden wir jedoch feststellen, dass der Risikomanager bei anderen Kursen den Handel deutlich früher als am Ende des Testzeitraums beendet hat, sodass wir ihn deaktivieren werden, um ein vollständiges Bild zu erhalten. Auf diese Weise können wir einen faireren Vergleich der Ergebnisse gewährleisten. Hier ist, was ich erreicht habe:


Abb. 1. Testergebnisse auf MetaQuotes-Demo-Server-Kursen ohne den Risikomanager

Verbinden wir nun das Terminal mit dem realen Server eines anderen Brokers und führen den EA-Test erneut mit denselben Parametern durch:

Abb. 2. Testergebnisse mit Kursen eines realen Servers eines anderen Brokers ohne den Risikomanager

Dies ist eine unerwartete Wendung der Ereignisse. Das Konto wurde in weniger als einem Jahr vollständig platt gemacht. Versuchen wir, die Gründe für dieses Verhalten zu verstehen, damit wir wissen, ob es möglich ist, die Situation irgendwie zu korrigieren.


Auf der Suche nach dem Grund

Wir speichern die Testerberichte für die abgeschlossenen Durchläufe als XML-Dateien, öffnen sie und suchen die Stelle, an der die Liste der abgeschlossenen Geschäfte beginnt. Wir ordnen die geöffneten Dateifenster so an, dass wir die oberen Teile der Geschäftslisten für beide Berichte gleichzeitig sehen können:

Abb. 3. Die obersten Teile der Liste der Geschäfte, die der EA beim Testen von Kursen von verschiedenen Servern durchführt

Schon aus den ersten Zeilen der Berichte geht hervor, dass die Positionen zu unterschiedlichen Zeiten eröffnet wurden. Wenn es also Unterschiede bei den Kursen für dieselben Zeitpunkte auf verschiedenen Servern gäbe, hätten diese höchstwahrscheinlich nicht so zerstörerische Auswirkungen wie unterschiedliche Öffnungszeiten.

Schauen wir uns an, wo die Momente der Positionseröffnung in unseren Strategien festgelegt werden. Werfen wir einen Blick auf die Datei, die die Klasse einer einzelnen Instanz der Handelsstrategie SimpleVolumesStrategy.mqh implementiert. Wenn wir uns den Code ansehen, finden wir die Methode SignalForOpen(), die das Open-Signal zurückgibt:

//+------------------------------------------------------------------+
//| Signal for opening pending orders                                |
//+------------------------------------------------------------------+
int CSimpleVolumesStrategy::SignalForOpen() {
// By default, there is no signal
   int signal = 0;

// Copy volume values from the indicator buffer to the receiving array
   int res = CopyBuffer(m_iVolumesHandle, 0, 0, m_signalPeriod, m_volumes);

// If the required amount of numbers have been copied
   if(res == m_signalPeriod) {
      // Calculate their average value
      double avrVolume = ArrayAverage(m_volumes);

      // If the current volume exceeds the specified level, then
      if(m_volumes[0] > avrVolume * (1 + m_signalDeviation + m_ordersTotal * m_signaAddlDeviation)) {
         // if the opening price of the candle is less than the current (closing) price, then 
         if(iOpen(m_symbol, m_timeframe, 0) < iClose(m_symbol, m_timeframe, 0)) {
            signal = 1; // buy signal
         } else {
            signal = -1; // otherwise, sell signal
         }
      }
   }

   return signal;
}

Wir sehen, dass das Eröffnungssignal durch die Tick-Volumenwerte für das aktuelle Handelsinstrument bestimmt wird. Die Preise (sowohl aktuelle als auch vergangene) nehmen nicht an der Bildung des Eröffnungssignals teil. Genauer gesagt, ist ihre Beteiligung erst dann gegeben, wenn feststeht, dass eine Position geöffnet werden muss, und sie beeinflusst nur die Richtung der Öffnung. Das Problem scheint also genau in den starken Unterschieden zwischen den von verschiedenen Servern empfangenen Tickvolumenwerten zu liegen.

Dies ist durchaus möglich, denn damit verschiedene Broker die Kerzen-Charts visuell angleichen können, reicht es aus, nur vier korrekte Ticks pro Minute anzugeben, um den Eröffnungs-, Schluss-, Höchst- und Tiefstkurs für die Kerze der kürzesten Periode M1 zu bilden. Die Anzahl der Zwischenticks, während derer sich der Kurs innerhalb der festgelegten Grenzen zwischen Tief und Hoch befand, ist nicht von Bedeutung. Dies bedeutet, dass die Broker frei entscheiden können, wie viele Ticks sie in der Historie speichern und wie sie innerhalb einer Kerze über die Zeit verteilt werden sollen. Es ist auch zu bedenken, dass selbst bei einem Broker die Server für Demokonten und für reale Konten nicht unbedingt das gleiche Bild zeigen.

Wenn dies wirklich der Fall ist, dann können wir dieses Hindernis leicht umgehen. Um eine solche Abhilfe zu schaffen, müssen wir jedoch zunächst sicherstellen, dass wir die Ursache für die beobachteten Diskrepanzen korrekt ermittelt haben, damit unsere Bemühungen nicht umsonst waren.


Der Weg ist vorgezeichnet

Um unsere Annahme zu überprüfen, benötigen wir die folgenden Instrumente:

  • Speichern der Handelsgeschichte. Fügen wir unserem EA die Möglichkeit hinzu, die Historie der Handelsgeschäfte (Eröffnung und Schluss der Positionen) am Ende des Testlaufs zu speichern. Die Speicherung kann entweder in einer Datei oder in einer Datenbank erfolgen. Da dieses Werkzeug vorerst nur als Hilfsmittel verwendet wird, ist es wahrscheinlich einfacher, es in einer Datei zu speichern. Wenn wir sie in Zukunft dauerhaft nutzen wollen, können wir sie um die Möglichkeit erweitern, den Verlauf in einer Datenbank zu speichern.

  • Wiederholen des Handels. Erstellen wir einen neuen EA, der keine Regeln für das Öffnen von Positionen enthält, sondern nur das Öffnen und Schließen von Positionen reproduziert, indem er sie aus der von einem anderen EA gespeicherten Historie liest. Da wir uns entschieden haben, die Historie vorerst in einer Datei zu speichern, wird dieser EA den Namen der Datei mit der Historie der Geschäfte als Eingabe akzeptieren, und dann die darin gespeicherten Geschäfte lesen und ausführen.

Nachdem wir diese Tools erstellt haben, starten wir zunächst unseren EA im Tester mit den Kursen vom MetaQuotes-Demo-Server und speichern den Handelsverlauf dieses Testdurchgangs in der Datei. Dies wird der erste Durchgang sein. Dann starten wir im Tester einen neuen Handelswiederholungs-EA mit Kursen von einem anderen Server unter Verwendung der gespeicherten Verlaufsdatei. Dies wird der zweite Durchgang sein. Wenn die Unterschiede in den zuvor erzielten Handelsergebnissen tatsächlich auf sehr unterschiedliche Tick-Volumen-Daten zurückzuführen sind und die Preise selbst annähernd gleich sind, dann sollten wir im zweiten Durchgang ähnliche Ergebnisse wie im ersten Durchgang erhalten.


Speichern der Handelshistorie

Es gibt verschiedene Möglichkeiten, das Speichern der Historie zu implementieren. Zum Beispiel können wir die Methode zur Klasse CVirtualAdvisor hinzufügen, die vom Ereignis OnTester() aufgerufen wird. Diese Methode zwingt uns dazu, eine bestehende Klasse zu erweitern und ihr Funktionen hinzuzufügen, auf die sie eigentlich verzichten kann. Also, lassen Sie uns eine separate Klasse CExpertHistory erstellen, um dieses spezielle Problem zu lösen. Da wir nicht mehrere Objekte dieser Klasse erstellen müssen, können wir sie statisch machen, d. h. sie enthält nur statische Eigenschaften und Methoden.

Es wird nur eine öffentliche Hauptmethode der Klasse geben — Export(). Die übrigen Methoden werden eine unterstützende Rolle spielen. Die Methode Export() erhält zwei Parameter: den Namen der Datei, in die der Verlauf geschrieben werden soll, und das Flag für die Verwendung des gemeinsamen Terminaldatenordners. Der Standard-Dateiname kann eine leere Zeichenfolge sein. In diesem Fall wird eine Hilfsmethode verwendet, um die Datei GetHistoryFileName() zu erzeugen. Mit dem Flag für das Schreiben in den gemeinsamen Ordner können wir wählen, wo die Verlaufsdatei gespeichert werden soll - im gemeinsamen Datenordner oder im lokalen Terminal-Datenordner. Standardmäßig wird der Wert des Flags für das Schreiben in den gemeinsamen Ordner gesetzt, da es bei der Ausführung im Testprogramm schwieriger ist, den lokalen Ordner des Testagenten zu öffnen als den gemeinsamen Ordner.

Als Klasseneigenschaften benötigen wir das Trennzeichen, das beim Öffnen einer CSV-Datei zum Schreiben angegeben wird, das Handle der geöffneten Datei selbst, damit es in Hilfsmethoden verwendet werden kann, und das Array der Spaltennamen der zu speichernden Daten.

//+------------------------------------------------------------------+
//| Export trade history to file                                     |
//+------------------------------------------------------------------+
class CExpertHistory {
private:
   static string     s_sep;            // Separator character
   static int        s_file;           // File handle for writing
   static string     s_columnNames[];  // Array of column names

   // Write deal history to file
   static void       WriteDealsHistory();

   // Write one row of deal history to file 
   static void       WriteDealsHistoryRow(const string &fields[]);

   // Get the first deal date
   static datetime   GetStartDate();

   // Form a file name
   static string     GetHistoryFileName();

public:
   // Export deal history
   static void       Export(
      string exportFileName = "",   // File name for export. If empty, the name is generated
      int commonFlag = FILE_COMMON  // Save the file in shared data folder
   );
};

// Static class variables
string CExpertHistory::s_sep = ",";
int    CExpertHistory::s_file;
string CExpertHistory::s_columnNames[] = {"DATE", "TICKET", "TYPE",
                                          "SYMBOL", "VOLUME", "ENTRY", "PRICE",
                                          "STOPLOSS", "TAKEPROFIT", "PROFIT",
                                          "COMMISSION", "FEE", "SWAP",
                                          "MAGIC", "COMMENT"
                                         };

In der Hauptmethode Export() wird eine Datei mit einem angegebenen oder generierten Namen erstellt und zum Schreiben geöffnet. Wenn die Datei erfolgreich geöffnet wurde, rufen wir die Methode zum Speichern des Geschäftsverlaufs auf und schließen die Datei.

//+------------------------------------------------------------------+
//| Export deal history                                              |
//+------------------------------------------------------------------+
void CExpertHistory::Export(string exportFileName = "", int commonFlag = FILE_COMMON) {
   // If the file name is not specified, then generate it
   if(exportFileName == "") {
      exportFileName = GetHistoryFileName();
   }

   // Open the file for writing in the desired data folder
   s_file = FileOpen(exportFileName, commonFlag | FILE_WRITE | FILE_CSV | FILE_ANSI, s_sep);

   // If the file is open,
   if(s_file > 0) {
      // Set the deal history
      WriteDealsHistory();

      // Close the file
      FileClose(s_file);
   } else {
      PrintFormat(__FUNCTION__" | ERROR: Can't open file [%s]. Last error: %d",  exportFileName, GetLastError());
   }
}

In der Methode GetHistoryFileName() setzt sich der Dateiname aus mehreren Fragmenten zusammen. Zunächst fügen wir den EA-Namen und die Version an den Anfang des Namens an, wenn diese in der Konstante __VERSION__ angegeben ist. Zweitens fügen wir das Anfangs- und Enddatum des Geschäftsverlaufs hinzu. Wir bestimmen das Startdatum durch das Datum des ersten Geschäfts in der Geschichte, indem wir die Methode GetStartDate() aufrufen. Das Enddatum wird durch die aktuelle Uhrzeit bestimmt, da der Verlauf nach Abschluss des Testlaufs exportiert wird. Mit anderen Worten: Die aktuelle Zeit zum Zeitpunkt des Aufrufs der Methode zum Speichern der Historie ist genau die Endzeit des Tests. Drittens fügen wir dem Dateinamen die Werte einiger Passmerkmale hinzu: Anfangsbestand, Endbestand, Drawdown und Sharpe Ratio.

Wenn sich der Name als zu lang erweist, kürzen wir ihn auf eine akzeptable Länge und fügen die Erweiterung .history.csv hinzu.

//+------------------------------------------------------------------+
//| Form the file name                                               |
//+------------------------------------------------------------------+
string CExpertHistory::GetHistoryFileName() {
   // Take the EA name
   string fileName = MQLInfoString(MQL_PROGRAM_NAME);

   // If a version is specified, add it
#ifdef __VERSION__
   fileName += "." + __VERSION__;
#endif

   fileName += " ";

   // Add the history start and end date
   fileName += "[" + TimeToString(GetStartDate(), TIME_DATE);
   fileName += " - " + TimeToString(TimeCurrent(), TIME_DATE) + "]";

   fileName += " ";

   // Add some statistical characteristics
   fileName += "[" + DoubleToString(TesterStatistics(STAT_INITIAL_DEPOSIT), 0);
   fileName += ", " + DoubleToString(TesterStatistics(STAT_INITIAL_DEPOSIT) + TesterStatistics(STAT_PROFIT), 0);
   fileName += ", " + DoubleToString(TesterStatistics(STAT_EQUITY_DD_RELATIVE), 0);
   fileName += ", " + DoubleToString(TesterStatistics(STAT_SHARPE_RATIO), 2);
   fileName += "]";

   // If the name is too long, shorten it
   if(StringLen(fileName) > 255 - 13) {
      fileName = StringSubstr(fileName, 0, 255 - 13);
   }

   // Add extension
   fileName += ".history.csv";

   return fileName;
}

Bei der Methode, die Historie in eine Datei zu schreiben, wird zuerst die Kopfzeile geschrieben, d. h. die Zeile mit den Namen der Datenspalten. Dann wählen wir alle verfügbaren Historien aus und beginnen mit der Iteration durch alle Geschäfte. Wir rufen die Eigenschaften von jeden Handelsdeal ab. Wenn es sich um eine Operation zur Eröffnung oder zum Ausgleich eines Geschäfts handelt, bilden wir ein Array mit den Werten aller Geschäftseigenschaften und übergeben es an die Methode WriteDealsHistoryRow(), um ein einzelnes Geschäft zu schreiben.

//+------------------------------------------------------------------+
//| Write deal history to file                                       |
//+------------------------------------------------------------------+
void CExpertHistory::WriteDealsHistory() {
   // Write a header with column names
   WriteDealsHistoryRow(s_columnNames);

   // Variables for each deal properties
   uint     total;
   ulong    ticket = 0;
   long     entry;
   double   price;
   double   sl, tp;
   double   profit, commission, fee, swap;
   double   volume;
   datetime time;
   string   symbol;
   long     type, magic;
   string   comment;

   // Take the entire history
   HistorySelect(0, TimeCurrent());
   total = HistoryDealsTotal();

   // For all deals
   for(uint i = 0; i < total; i++) {
      // If the deal is successfully selected,
      if((ticket = HistoryDealGetTicket(i)) > 0) {
         // Get the values of its properties
         time  = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
         type  = HistoryDealGetInteger(ticket, DEAL_TYPE);
         symbol = HistoryDealGetString(ticket, DEAL_SYMBOL);
         volume = HistoryDealGetDouble(ticket, DEAL_VOLUME);
         entry = HistoryDealGetInteger(ticket, DEAL_ENTRY);
         price = HistoryDealGetDouble(ticket, DEAL_PRICE);
         sl = HistoryDealGetDouble(ticket, DEAL_SL);
         tp = HistoryDealGetDouble(ticket, DEAL_TP);
         profit = HistoryDealGetDouble(ticket, DEAL_PROFIT);
         commission = HistoryDealGetDouble(ticket, DEAL_COMMISSION);
         fee = HistoryDealGetDouble(ticket, DEAL_FEE);
         swap = HistoryDealGetDouble(ticket, DEAL_SWAP);
         magic = HistoryDealGetInteger(ticket, DEAL_MAGIC);
         comment = HistoryDealGetString(ticket, DEAL_COMMENT);

         if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL || type == DEAL_TYPE_BALANCE) {
            // Replace the separator characters in the comment with a space
            StringReplace(comment, s_sep, " ");

            // Form an array of values for writing one deal to the file string
            string fields[] = {TimeToString(time, TIME_DATE | TIME_MINUTES | TIME_SECONDS),
                               IntegerToString(ticket), IntegerToString(type), symbol, DoubleToString(volume), IntegerToString(entry),
                               DoubleToString(price, 5), DoubleToString(sl, 5), DoubleToString(tp, 5), DoubleToString(profit),
                               DoubleToString(commission), DoubleToString(fee), DoubleToString(swap), IntegerToString(magic), comment
                              };

            // Set the values of a single deal to the file
            WriteDealsHistoryRow(fields);
         }
      }
   }
}

In der Methode WriteDealsHistoryRow() werden einfach alle Werte aus dem übergebenen Array über das angegebene Trennzeichen zu einer Zeichenkette zusammengefasst und in die geöffnete CSV-Datei geschrieben. Für die Verbindung haben wir ein neues Makro JOIN verwendet, das zu unserer Makrosammlung in der Datei Macros.mqh hinzugefügt wurde.

//+------------------------------------------------------------------+
//| Write one row of deal history to the file                        |
//+------------------------------------------------------------------+
void CExpertHistory::WriteDealsHistoryRow(const string &fields[]) {
   // Row to be set
   string row = "";

   // Concatenate all array values into one row using a separator
   JOIN(fields, row, ",");

   // Write a row to the file
   FileWrite(s_file, row);
}

Wir speichern die Änderungen in der Datei ExpertHistory.mqh im aktuellen Ordner.

Jetzt müssen wir nur noch die Datei mit der EA-Datei verbinden und den Aufruf der Methode CExpertHistory::Export() zur Ereignisbehandlung von OnTester() hinzufügen:

...

#include "ExpertHistory.mqh"

...

//+------------------------------------------------------------------+
//| Test results                                                     |
//+------------------------------------------------------------------+
double OnTester(void) {
   CExpertHistory::Export();
   return expert.Tester();
}

Wir speichern die Änderungen in der Datei SimpleVolumesExpert.mq5 im aktuellen Ordner.

Beginnen wir mit dem Testen des EA. Nach Abschluss des Tests erscheint im gemeinsamen Datenordner eine Datei mit folgendem Namen

SimpleVolumesExpert.1.19 [2021.01.01 - 2022.12.30] [10000, 34518, 1294, 3.75].history.csv

Der Name verrät, dass sich der Handelshistorie über zwei Jahre erstreckt (2021 und 2022), der Anfangskontostand 10.000 USD beträgt, während der Endstand 34.518 USD beträgt. Während des Testintervalls betrug der maximale relative Drawdown pro Aktie 1294 USD und das Sharpe Ratio 3,75 betrug. Wenn wir die resultierende Datei in Excel öffnen, sehen wir folgendes:

Abb. 4. Ergebnis des Entladens der Geschäftshistorie in eine CSV-Datei

Die Daten scheinen gültig zu sein. Gehen wir nun dazu über, einen EA zu entwickeln, der in der Lage ist, den Handel auf einem anderen Konto unter Verwendung der CSV-Datei zu reproduzieren.


Wiederholen des Handels

Beginnen wir mit der Implementierung des neuen EA, indem wir eine Handelsstrategie erstellen. Das Befolgen der Anweisungen anderer, wann und welche Positionen zu eröffnen sind, kann man auch als Handelsstrategie bezeichnen. Wenn die Quelle der Signale vertrauenswürdig ist, warum sollte man sie nicht nutzen. Erstellen wir daher eine neue Klasse CHistoryStrategy, die von CVirtualStrategy abgeleitet wird. Was die Methoden angeht, so müssen wir auf jeden Fall einen Konstruktor, eine Methode zur Behandlung von Ticks und eine Methode zur Konvertierung in eine Zeichenkette implementieren. Obwohl wir die letzte Methode nicht benötigen, ist ihr Vorhandensein aufgrund der Vererbung erforderlich, da diese Methode in der übergeordneten Klasse abstrakt ist.

Wir müssen der neuen Klasse nur die folgenden Eigenschaften hinzufügen:

  • m_symbols — Array mit den Symbolnamen (Handelsinstrumente);
  • m_history — zweidimensionales Array zum Auslesen der Datei mit der Handelshistorie (N Zeilen * 15 Spalten);
  • m_totalDeals — Anzahl der Handelsgeschäfte in der Vergangenheit;
  • m_currentDeal — Index des aktuellen Handelsgeschäfts;
  • m_symbolInfo — Objekt zum Abrufen von Daten über die Symboleigenschaften.
Die Anfangswerte für diese Eigenschaften werden im Konstruktor festgelegt.
//+------------------------------------------------------------------+
//| Trading strategy for reproducing the history of deals            |
//+------------------------------------------------------------------+
class CHistoryStrategy : public CVirtualStrategy {
protected:
   string            m_symbols[];            // Symbols (trading instruments)
   string            m_history[][15];        // Array of deal history (N rows * 15 columns)
   int               m_totalDeals;           // Number of deals in history
   int               m_currentDeal;          // Current deal index

   CSymbolInfo       m_symbolInfo;           // Object for getting information about the symbol properties

public:
                     CHistoryStrategy(string p_params);        // Constructor
   virtual void      Tick() override;        // OnTick event handler
   virtual string    operator~() override;   // Convert object to string
};

Der Strategiekonstruktor sollte ein Argument akzeptieren - die Initialisierungszeichenfolge. Diese Anforderung ergibt sich auch aus der Vererbung. Die Initialisierungszeichenfolge sollte alle erforderlichen Werte enthalten. Der Konstruktor liest sie aus der Zeichenkette und verwendet sie nach Bedarf. Zufälligerweise müssen wir bei dieser einfachen Strategie nur einen Wert in der Initialisierungszeichenfolge übergeben - den Namen der Verlaufsdatei. Alle weiteren Daten für die Strategie werden aus der Historiendatei entnommen. Dann kann der Konstruktor auf folgende Weise implementiert werden:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CHistoryStrategy::CHistoryStrategy(string p_params) {
   m_params = p_params;

// Read the file name from the parameters
   string fileName = ReadString(p_params);

// If the name is read, then
   if(IsValid()) {
      // Attempting to open a file in the data folder
      int f = FileOpen(fileName, FILE_READ | FILE_CSV | FILE_ANSI | FILE_SHARE_READ, ',');

      // If failed to open a file, then try to open the file from the shared folder
      if(f == INVALID_HANDLE) {
         f = FileOpen(fileName, FILE_COMMON | FILE_READ | FILE_CSV | FILE_ANSI | FILE_SHARE_READ, ',');
      }

      // If this does not work, report an error and exit
      if(f == INVALID_HANDLE) {
         SetInvalid(__FUNCTION__,
                    StringFormat("ERROR: Can't open file %s from common folder %s, error code: %d",
                                 fileName, TerminalInfoString(TERMINAL_COMMONDATA_PATH), GetLastError()));
         return;
      }

      // Read the file up to the header string (usually it comes first)
      while(!FileIsEnding(f)) {
         string s = FileReadString(f);
         // If we find a header string, read the names of all columns without saving them
         if(s == "DATE") {
            FORI(14, FileReadString(f));
            break;
         }
      }

      // Read the remaining rows until the end of the file
      while(!FileIsEnding(f)) {
         // If the array for storing the read history is filled, increase its size
         if(m_totalDeals == ArraySize(m_history)) {

            ArrayResize(m_history, ArraySize(m_history) + 10000, 100000);
         }

         // Read 15 values from the next file string into the array string
         FORI(15, m_history[m_totalDeals][i] = FileReadString(f));

         // If the deal symbol is not empty,
         if(m_history[m_totalDeals][SYMBOL] != "") {
            // Add it to the symbol array if there is no such symbol there yet
            ADD(m_symbols, m_history[m_totalDeals][SYMBOL]);
         }

         // Increase the counter of read deals
         m_totalDeals++;
      }

      // Close the file
      FileClose(f);

      PrintFormat(__FUNCTION__" | OK: Found %d rows in %s", m_totalDeals, fileName);

      // If there are read deals except for the very first one (account top-up), then
      if(m_totalDeals > 1) {
         // Set the exact size for the history array
         ArrayResize(m_history, m_totalDeals);

         // Current time
         datetime ct = TimeCurrent();

         PrintFormat(__FUNCTION__" |\n"
                     "Start time in tester:  %s\n"
                     "Start time in history: %s",
                     TimeToString(ct, TIME_DATE), m_history[0][DATE]);

         // If the test start date is greater than the history start date, then report an error
         if(StringToTime(m_history[0][DATE]) < ct) {
            SetInvalid(__FUNCTION__,
                       StringFormat("ERROR: For this history file [%s] set start date less than %s",
                                    fileName, m_history[0][DATE]));
         }
      }

      // Create virtual positions for each symbol
      CVirtualReceiver::Get(GetPointer(this), m_orders, ArraySize(m_symbols));

      // Register the event handler for a new bar on the minimum timeframe
      FOREACH(m_symbols, IsNewBar(m_symbols[i], PERIOD_M1));
   }
}

Im Konstruktor lesen wir den Dateinamen aus dem Initialisierungsstring und versuchen, ihn zu öffnen. Wenn die Datei erfolgreich aus einem lokalen oder gemeinsamen Datenordner geöffnet wurde, lesen wir ihren Inhalt und füllen das Array m_history damit. Beim Lesen füllen wir auch das Array m_symbols mit Symbolnamen: Sobald ein neuer Name auftaucht, fügen wir ihn sofort dem Array hinzu. Dies wird durch das Makro ADD() erreicht.

Auf dem Weg dorthin zählen wir die Anzahl der gelesenen Geschäftseinträge in der Eigenschaft m_totalDeals und verwenden sie als Index der ersten Dimension des Arrays m_history, in dem Informationen über das nächste Geschäft gespeichert werden sollen. Nachdem der gesamte Inhalt der Datei gelesen wurde, wird sie geschlossen.

Als Nächstes wird geprüft, ob das Startdatum des Tests größer ist als das Startdatum der Historie. Wir können eine solche Situation nicht zulassen, da es in diesem Fall nicht möglich sein wird, einige der Geschäfte vom Beginn der Geschichte an zu modellieren. Dies kann durchaus zu verzerrten Handelsergebnissen während des Tests führen. Daher erlauben wir dem Konstruktor nur dann, ein gültiges Objekt zu erstellen, wenn die Geschäftshistorie nicht vor dem Startdatum des Tests beginnt.

Der wichtigste Punkt im Konstruktor ist die Zuweisung virtueller Positionen, die sich strikt nach der Anzahl der verschiedenen Symbolnamen richtet, die in der Geschichte vorkommen. Da das Ziel der Strategie darin besteht, das erforderliche Volumen an offenen Positionen für jedes Symbol bereitzustellen, kann dies mit nur einer einzigen virtuellen Position pro Symbol erfolgen.

Die Methode für das Tick-Handling funktioniert nur mit dem Array der gelesenen Handelsgeschäfte. Da wir zu einem Zeitpunkt mehrere Symbole gleichzeitig öffnen/schließen können, richten wir eine Schleife ein, die alle Zeilen aus der Historie der Geschäfte abarbeitet, deren Zeitpunkt nicht größer ist als der aktuelle. Die verbleibenden Geschäftseinträge werden in den folgenden Ticks bearbeitet, wenn die aktuelle Zeit ansteigt und neue Geschäfte erscheinen, deren Zeit bereits abgelaufen ist.

Wenn mindestens ein Deal gefunden wird, der bearbeitet werden muss, wird zunächst sein Symbol und sein Index im Array m_symbols gesucht. Anhand dieses Indexes wird ermittelt, welche virtuelle Position aus dem Array m_orders für dieses Symbol zuständig ist. Wenn der Index aus irgendeinem Grund nicht gefunden wird (das sollte noch nicht passieren, wenn alles korrekt funktioniert), wird der Vorgang einfach übersprungen. Wir lassen auch Angebote aus, die den Saldo des Kontos widerspiegeln.

Jetzt beginnt der interessanteste Teil. Wir müssen uns um die Leseprozedur kümmern. Hier sind zwei Fälle möglich: Es gibt keine offene virtuelle Position für dieses Symbol, oder eine virtuelle Position ist offen.

Im ersten Fall ist alles ganz einfach: Wir eröffnen eine Position in Richtung des Handelsgeschäfts mit dem entsprechenden Volumen. Im zweiten Fall kann es erforderlich sein, das Volumen der aktuellen Position für ein bestimmtes Symbol entweder zu erhöhen oder zu verringern. Außerdem kann es notwendig sein, sie so weit zu reduzieren, dass sich die Richtung der offenen Position ändert.

Um die Berechnungen zu vereinfachen, gehen wir wie folgt vor:

  • Wir konvertieren das Volumen des neuen Handelsgeschäfts in das Format „signed“. Das heißt, wenn der Kurs in Richtung VERKAUF geht, dann wird das Volumen negativ.
  • Wir erhalten das Volumen des offenen Geschäfts für das gleiche Symbol wie im neuen Geschäft. Die Methode CVirtualOrder::Volume() gibt das Volumen sofort im Format „signed“ zurück.
  • Wir addieren das Volumen der bereits geöffneten Position zum Volumen der neuen Position und wir holen uns ein neues Volumen, das nach Berücksichtigung des neuen Handelsgeschäfts offen bleiben sollte. Dies Volumen wird ebenfalls im Format „signed“ erscheinen.
  • Wir schließen die offene, virtuelle Position.
  • Wenn das neue Volumen nicht gleich Null ist, öffnen wir eine neue virtuelle Position für das Symbol. Die Richtung wird durch das Vorzeichen des neuen Volumens bestimmt (positiv - KAUFEN, negativ - VERKAUFEN). Der Modulus des neuen Volumens wird als Volumen an die Methode zur Öffnung der virtuellen Position übergeben.

Nach dieser Prozedur erhöhen wir den Zähler der abgewickelten Handelsgeschäfte aus der Historie und fahren mit der nächsten Schleifeniteration fort. Wenn zu diesem Zeitpunkt keine Handelsgeschäfte mehr zu bearbeiten sind oder die Handelsgeschäfte in der Historie beendet sind, ist die Tickbearbeitung abgeschlossen.

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CHistoryStrategy::Tick() override {
//---
   while(m_currentDeal < m_totalDeals && StringToTime(m_history[m_currentDeal][DATE]) <= TimeCurrent()) {
      // Deal symbol
      string symbol = m_history[m_currentDeal][SYMBOL];
      
      // Find the index of the current deal symbol in the array of symbols
      int index;
      FIND(m_symbols, symbol, index);

      // If not found, then skip the current deal
      if(index == -1) {
         m_currentDeal++;
         continue;
      }
      
      // Deal type
      ENUM_DEAL_TYPE type = (ENUM_DEAL_TYPE) StringToInteger(m_history[m_currentDeal][TYPE]);

      // Current deal volume
      double volume = NormalizeDouble(StringToDouble(m_history[m_currentDeal][VOLUME]), 2);

      // If this is a top-up/withdrawal, skip the deal
      if(volume == 0) {
         m_currentDeal++;
         continue;
      }

      // Report information about the read deal
      PrintFormat(__FUNCTION__" | Process deal #%d: %s %.2f %s",
                  m_currentDeal, (type == DEAL_TYPE_BUY ? "BUY" : (type == DEAL_TYPE_SELL ? "SELL" : EnumToString(type))),
                  volume, symbol);

      // If this is a sell deal, then make the volume negative
      if(type == DEAL_TYPE_SELL) {
         volume *= -1;
      }

      // If the virtual position for the current deal symbol is open,
      if(m_orders[index].IsOpen()) {
         // Add its volume to the volume of the current trade
         volume += m_orders[index].Volume();
         
         // Close the virtual position
         m_orders[index].Close();
      }

      // If the volume for the current symbol is not 0,
      if(MathAbs(volume) > 0.00001) {
         // Open a virtual position of the required volume and direction
         m_orders[index].Open(symbol, (volume > 0 ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), MathAbs(volume));
      }

      // Increase the counter of handled deals
      m_currentDeal++;
   }
}

Wir speichern den erhaltenen Code in der Datei HistoryStrategy.mqh des aktuellen Ordners.

Erstellen wir nun eine EA-Datei auf der Grundlage der bestehenden SimpleVolumesExpert.mq5. Um das gewünschte Ergebnis zu erhalten, müssen wir dem EA eine Eingabe hinzufügen, in der wir den Namen der Datei mit dem Verlauf angeben können.

input group "::: Testing the deal history"
input string historyFileName_    = "";    // File with history

Der Teil des Codes, der für das Laden der Strategieinitialisierungszeichenfolgen aus der Datenbank verantwortlich ist, wird nicht mehr benötigt und wird daher entfernt.

Wir müssen die Erstellung einer einzelnen Instanz der Strategie der Klasse CHistoryStrategy in der Initialisierungszeichenfolge festlegen. Die Strategie erhält den Dateinamen mit der Historie als Argument:

// Prepare the initialization string for an EA with a group of several strategies
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    class CVirtualStrategyGroup(\n"
                            "       [\n"
                            "        class CHistoryStrategy(\"%s\")\n"
                            "       ],%f\n"
                            "    ),\n"
                            "    class CVirtualRiskManager(\n"
                            "       %d,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%.2f,%d,%.2f,%.2f"
                            "    )\n"
                            "    ,%d,%s,%d\n"
                            ")",
                            historyFileName_, scale_,
                            rmIsActive_, rmStartBaseBalance_,
                            rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCloseDailyPart_,
                            rmCalcOverallLossLimit_, rmMaxOverallLossLimit_, rmCloseOverallPart_,
                            rmCalcOverallProfitLimit_, rmMaxOverallProfitLimit_, rmMaxOverallProfitDate_,
                            rmMaxRestoreTime_, rmLastVirtualProfitFactor_,
                            magic_, "HistoryReceiver", useOnlyNewBars_
                         );

Damit sind die Änderungen an der EA-Datei abgeschlossen. Wir speichern es als HistoryReceiverExpert.mq5 im aktuellen Ordner.

Jetzt haben wir einen funktionierenden EA, der die Historie der Handelsgeschäfte reproduzieren kann. In der Tat sind seine Möglichkeiten etwas breiter gefächert. Wir können leicht erkennen, wie die Handelsergebnisse aussehen werden, wenn das Volumen der eröffneten Positionen mit einer Erhöhung des Kontosaldos zunimmt, obwohl die Geschäfte in der Historie auf der Grundlage des Handels mit einem festen Saldo eingestellt sind. Wir können verschiedene Risikomanager-Parameter anwenden, um die Auswirkungen auf den Handel zu bewerten, obwohl die Geschäftshistorie mit anderen Risikomanager-Parametern eingestellt wurde (oder der Risikomanager sogar deaktiviert war). Nach dem Durchlaufen des Prüfers wird der Geschäftsverlauf automatisch in einer neuen Datei gespeichert.

Wenn wir aber all diese zusätzlichen Funktionen noch nicht benötigen, den Risikomanager nicht verwenden wollen und die vielen ungenutzten Eingaben, die damit verbunden sind, nicht mögen, dann können wir eine neue EA-Klasse erstellen, die keine zusätzlichen Funktionen hat. In dieser Klasse können wir auch das Speichern des Status und die Schnittstelle zum Zeichnen virtueller Positionen auf Diagrammen loswerden, sowie andere Dinge, die noch nicht viel gebraucht werden.

Die Implementierung einer solchen Klasse könnte etwa so aussehen:

//+------------------------------------------------------------------+
//| Trade history replay EA class                                    |
//+------------------------------------------------------------------+
class CVirtualHistoryAdvisor : public CAdvisor {
protected:
   CVirtualReceiver *m_receiver;       // Receiver object that brings positions to the market
   bool              m_useOnlyNewBar;  // Handle only new bar ticks
   datetime          m_fromDate;       // Test start time

public:
   CVirtualHistoryAdvisor(string p_param);   // Constructor
   ~CVirtualHistoryAdvisor();                // Destructor

   virtual void      Tick() override;        // OnTick event handler
   virtual double    Tester() override;      // OnTester event handler

   virtual string    operator~() override;   // Convert object to string
};


//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualHistoryAdvisor::CVirtualHistoryAdvisor(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the file name from the initialization string
   string fileName = ReadString(p_params);

// Read the work flag only at the bar opening
   m_useOnlyNewBar = (bool) ReadLong(p_params);

// If there are no read errors,
   if(IsValid()) {
      if(!MQLInfoInteger(MQL_TESTER)) {
         // Otherwise, set the object state to invalid
         SetInvalid(__FUNCTION__, "ERROR: This expert can run only in tester");
         return;
      }

      if(fileName == "") {
         // Otherwise, set the object state to invalid
         SetInvalid(__FUNCTION__, "ERROR: Set file name with deals history in ");
         return;
      }

      string strategyParams = StringFormat("class CHistoryStrategy(\"%s\")", fileName);

      CREATE(CHistoryStrategy, strategy, strategyParams);

      Add(strategy);

      // Initialize the receiver with the static receiver
      m_receiver = CVirtualReceiver::Instance(65677);

      // Save the work (test) start time
      m_fromDate = TimeCurrent();
   }
}

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualHistoryAdvisor::~CVirtualHistoryAdvisor() {
   if(!!m_receiver)     delete m_receiver;      // Remove the recipient
   DestroyNewBar();           // Remove the new bar tracking objects 
}


//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualHistoryAdvisor::Tick(void) {
// Define a new bar for all required symbols and timeframes
   bool isNewBar = UpdateNewBar();

// If there is no new bar anywhere, and we only work on new bars, then exit
   if(!isNewBar && m_useOnlyNewBar) {
      return;
   }

// Start handling in strategies
   CAdvisor::Tick();

// Receiver handles virtual positions
   m_receiver.Tick();

// Adjusting market volumes
   m_receiver.Correct();
}

//+------------------------------------------------------------------+
//| OnTester event handler                                           |
//+------------------------------------------------------------------+
double CVirtualHistoryAdvisor::Tester() {
// Maximum absolute drawdown
   double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD);

// Profit
   double profit = TesterStatistics(STAT_PROFIT);

// Fixed balance for trading from settings
   double fixedBalance = CMoney::FixedBalance();

// The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_
   double coeff = fixedBalance * 0.1 / MathMax(1, balanceDrawdown);

// Calculate the profit in annual terms
   long totalSeconds = TimeCurrent() - m_fromDate;
   double totalYears = totalSeconds / (365.0 * 24 * 3600);
   double fittedProfit = profit * coeff / totalYears;

// If it is not specified, then take the initial balance (although this will give a distorted result)
   if(fixedBalance < 1) {
      fixedBalance = TesterStatistics(STAT_INITIAL_DEPOSIT);
      balanceDrawdown = TesterStatistics(STAT_EQUITY_DDREL_PERCENT);
      coeff = 0.1 / balanceDrawdown;
      fittedProfit = fixedBalance * MathPow(1 + profit * coeff / fixedBalance, 1 / totalYears);
   }

   return fittedProfit;
}

//+------------------------------------------------------------------+
//| Convert an object to a string                                    |
//+------------------------------------------------------------------+
string CVirtualHistoryAdvisor::operator~() {
   return StringFormat("%s(%s)", typename(this), m_params);
}
//+------------------------------------------------------------------+

Der EA dieser Klasse akzeptiert nur zwei Parameter im Initialisierungsstring: den Namen der Datei der Handelshistorie und das Flag, nur bei der Eröffnung eines Minutenbalkens zu arbeiten. Wir speichern diesen Code in der Datei VirtualHistoryAdvisor.mqh im aktuellen Ordner.

Auch die EA-Datei, die diese Klasse verwendet, kann im Vergleich zur Vorgängerversion etwas gekürzt werden:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "::: Testing the deal history"
input string historyFileName_    = "";    // File with history
input group "::: Money management"
sinput double fixedBalance_      = 10000; // - Used deposit (0 - use all) in the account currency
input  double scale_             = 1.00;  // - Group scaling multiplier

input group "::: Other parameters"
input bool     useOnlyNewBars_   = true;  // - Work only at bar opening

datetime fromDate = TimeCurrent();        // Operation start time

CVirtualHistoryAdvisor     *expert;       // EA object

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Set parameters in the money management class
   CMoney::DepoPart(scale_);
   CMoney::FixedBalance(fixedBalance_);

// Prepare the initialization string for the deal history replay EA
   string expertParams = StringFormat(
                            "class CVirtualHistoryAdvisor(\"%s\",%f,%d)",
                            historyFileName_, useOnlyNewBars_
                         );

// Create an EA handling virtual positions
   expert = NEW(expertParams);

// If the EA is not created, then return an error
   if(!expert) return INIT_FAILED;

// Successful initialization
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   expert.Tick();
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   if(!!expert) delete expert;
}

//+------------------------------------------------------------------+
//| Test results                                                     |
//+------------------------------------------------------------------+
double OnTester(void) {
   return expert.Tester();
}
//+------------------------------------------------------------------+

Diesen Code speichern wir in der Datei SimpleHistoryReceiverExpert.mq5 im aktuellen Ordner.


Testergebnisse

Starten wir nun einen der erstellten EAs unter Angabe des korrekten Namens der Datei mit dem gespeicherten Geschäftsverlauf. Starten wir ihn zunächst auf demselben Preis-Server, von dem wir den Verlauf erhalten haben (MetaQuotes-Demo). Die erzielten Testergebnisse stimmen perfekt mit den ursprünglichen Ergebnissen überein! Ich muss zugeben, dass dies sogar ein etwas unerwartet gutes Ergebnis ist, das auf die korrekte Umsetzung des Plans hindeutet. 

Nun wollen wir sehen, was passiert, wenn wir den EA auf einem anderen Server ausführen:


Abb. 5. Ergebnisse der Reproduktion des Verlaufs von Geschäften auf den Kursen des realen Servers eines anderen Brokers

Das Saldendiagramm ist kaum von dem der ersten Handelsergebnisse auf MetaQuotes-Demo zu unterscheiden. Die numerischen Werte sind jedoch leicht unterschiedlich. Schauen wir uns zum Vergleich noch einmal die Originalwerte an:


Abb. 6. Erste Testergebnisse auf MetaQuotes-Demo-Server-Kursen

Wir sehen einen leichten Rückgang des gesamten und normalisierten durchschnittlichen Jahresgewinns und der Sharpe Ratio sowie einen leichten Anstieg des Drawdowns. Diese Ergebnisse sind jedoch nicht vergleichbar mit dem Verlust der gesamten Einlage, den wir anfänglich sahen, als wir den EA auf den Kursen eines echten Servers eines anderen Brokers laufen ließen. Dies ist sehr ermutigend und eröffnet eine neue Ebene von Aufgaben, die wir bei der Vorbereitung des EA für den realen Handel lösen müssen.


Schlussfolgerung

Es ist an der Zeit, einige vorläufige Schlussfolgerungen zu ziehen. Wir konnten zeigen, dass ein Wechsel des Kursservers für eine bestimmte Handelsstrategie sehr schwerwiegende Folgen haben kann. Aber nachdem wir die Gründe für dieses Verhalten verstanden hatten, konnten wir zeigen, dass die Handelsergebnisse wieder vergleichbar werden, wenn wir die Logik der Signale für die Positionseröffnung auf dem Server mit den ursprünglichen Kursen belassen und nur die Operationen zur Eröffnung und Schließung von Positionen an den neuen Server weitergeben.

Zu diesem Zweck haben wir zwei neue Tools entwickelt, die es uns ermöglichen, den Verlauf von Geschäften nach dem Durchlauf des Testers zu speichern und dann die Geschäfte auf der Grundlage des gespeicherten Verlaufs wiederzugeben. Diese Werkzeuge können jedoch nur im Prüfgerät verwendet werden. Im realen Handel sind sie bedeutungslos. Nun können wir mit der Umsetzung einer solchen Aufgabenteilung zwischen EAs auch im realen Handel beginnen, denn die Testergebnisse bestätigen die Gültigkeit eines solchen Ansatzes.

Wir müssen den EA in zwei getrennte EAs aufteilen. Der erste wird Entscheidungen über die Eröffnung von Positionen treffen und diese eröffnen, während er auf dem Kurs-Server arbeitet, der uns am bequemsten erscheint. Gleichzeitig muss er dafür sorgen, dass die Liste der offenen Positionen in einer Form übermittelt wird, die der zweite EA akzeptieren kann. Der zweite EA wird in einem anderen Terminal arbeiten, das gegebenenfalls mit einem anderen Preis-Server verbunden ist. Er hält das Volumen der offenen Positionen konstant aufrecht, das den vom ersten EA übermittelten Werten entspricht. Auf diese Weise kann die eingangs erwähnte Einschränkung umgangen werden.

Wir können sogar noch weiter gehen. Das erwähnte Arbeitslayout impliziert, dass beide Terminals an einem Computer arbeiten sollten. Aber das ist nicht notwendig. Die Terminals können auf verschiedenen Computern arbeiten. Die Hauptsache ist, dass der erste EA Informationen über Positionen über bestimmte Kanäle an den zweiten EA weitergeben kann. Es liegt auf der Hand, dass dies keine erfolgreiche Durchführung von Handelsstrategien ermöglicht, bei denen es sehr wichtig ist, den genauen Zeitpunkt und Preis der Positionseröffnung einzuhalten. Wir haben uns jedoch zunächst auf andere Strategien konzentriert, für die eine hohe Präzision der Eingaben nicht erforderlich ist. Daher sollten Verzögerungen bei den Kommunikationskanälen kein Hindernis bei der Gestaltung eines solchen Arbeitslayouts darstellen.

Aber wir wollen nicht zu weit vorpreschen. Wir werden unsere systematische Bewegung in die gewählte Richtung in den folgenden Artikeln fortsetzen.

Vielen Dank für Ihre Aufmerksamkeit! Bis bald! 


Inhalt des Archivs

#
 Name
Version  Beschreibung   Jüngste Änderungen
 MQL5/Experten/Artikel.15330
1 Advisor.mqh 1.04 EA-Basisklasse Part 10
2 Database.mqh 1.03 Klasse für den Umgang mit der Datenbank Teil 13
3 ExpertHistory.mqh 1.00. Klasse für den Export der Handelshistorie in eine Datei Part 16
4 Factorable.mqh 1.01 Basisklasse von Objekten, die aus einer Zeichenkette erstellt werden Part 10
5 HistoryReceiverExpert.mq5 1.00. EA für die Wiedergabe der Historie von Geschäften mit dem Risikomanager Part 16  
6 HistoryStrategy.mqh  1.00. Klasse der Handelsstrategie für die Wiederholung der Handelshistorie  Part 16
7 Interface.mqh 1.00. Basisklasse zur Visualisierung verschiedener Objekte Part 4
8 Macros.mqh 1.02 Nützliche Makros für Array-Operationen Part 16  
9 Money.mqh 1.01  Grundkurs Geldmanagement Part 12
10 NewBarEvent.mqh 1.00.  Klasse zur Definition eines neuen Balkens für ein bestimmtes Symbol  Part 8
11 Receiver.mqh 1.04  Basisklasse für die Umwandlung von offenen Volumina in Marktpositionen  Part 12
12 SimpleHistoryReceiverExpert.mq5 1.00. Vereinfachter EA für die Wiedergabe des Geschäftsverlaufs   Part 16
13 SimpleVolumesExpert.mq5 1.19 EA für den parallelen Betrieb von mehreren Gruppen von Modellstrategien. Die Parameter sollten aus der Optimierungsdatenbank geladen werden. Part 16
14 SimpleVolumesStrategy.mqh 1.09  Klasse der Handelsstrategie mit Tick-Volumen Part 15
15 Strategy.mqh 1.04  Handelsstrategie-Basisklasse Part 10
16 TesterHandler.mqh  1.02 Klasse zur Behandlung von Optimierungsereignissen  Teil 13 
17 VirtualAdvisor.mqh  1.06  Klasse des EA, der virtuelle Positionen (Aufträge) bearbeitet Part 15
18 VirtualChartOrder.mqh  1.00.  Grafische virtuelle Positionsklasse Part 4  
19 VirtualFactory.mqh 1.04  Objekt-Fabrik-Klasse  Part 16
20 VirtualHistoryAdvisor.mqh 1.00.  Die Klasse des EA zur Wiederholung des Handelsverlaufs  Part 16
21 VirtualInterface.mqh  1.00.  EA GUI-Klasse  Part 4  
22 VirtualOrder.mqh 1.04  Klasse der virtuellen Aufträge und Positionen  Part 8
23 VirtualReceiver.mqh 1.03  Klasse für die Umwandlung von offenen Volumina in Marktpositionen (Empfänger)  Part 12
24 VirtualRiskManager.mqh  1.02  Klasse Risikomanagement (Risikomanager)  Part 15
25 VirtualStrategy.mqh 1.05  Klasse einer Handelsstrategie mit virtuellen Positionen  Part 15
26 VirtualStrategyGroup.mqh  1.00.  Klasse der Handelsstrategien Gruppe(n) Part 11 
27 VirtualSymbolReceiver.mqh  1.00. Symbol-Empfängerklasse  Teil 3
MQL5/Dateien 
1 SimpleVolumesExpert.1.19 [2021.01.01 - 2022.12.30] [10000, 34518, 1294, 3.75].history.csv    Verlauf der SimpleVolumesExpert.mq5 EA-Geschäfte, die nach dem Export erhalten wurden. Es kann verwendet werden, um die Geschäfte im Tester mit SimpleHistoryReceiverExpert.mq5 oder HistoryReceiverExpert.mq5 EAs wiederzugeben  

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

Beigefügte Dateien |
MQL5.zip (163.78 KB)
Von der Grundstufe bis zur Mittelstufe: Variablen (II) Von der Grundstufe bis zur Mittelstufe: Variablen (II)
Heute werden wir uns ansehen, wie man mit statischen Variablen arbeitet. Diese Frage verwirrt oft viele Programmierer, sowohl Anfänger als auch solche mit einiger Erfahrung, denn es gibt mehrere Empfehlungen, die bei der Verwendung dieses Mechanismus beachtet werden müssen. Die hier vorgestellten Materialien sind ausschließlich für didaktische Zwecke bestimmt. Die Anwendung sollte unter keinen Umständen zu einem anderen Zweck als zum Erlernen und Beherrschen der vorgestellten Konzepte verwendet werden.
Überwachung des Handels mit Push-Benachrichtigungen — Beispiel für einen MetaTrader 5 Dienst Überwachung des Handels mit Push-Benachrichtigungen — Beispiel für einen MetaTrader 5 Dienst
In diesem Artikel befassen wir uns mit der Erstellung einer Service-App für das Senden von Benachrichtigungen über Handelsergebnisse an ein Smartphone. Wir werden lernen, wie man mit Listen von Objekten der Standardbibliothek umgeht, um eine Auswahl von Objekten nach erforderlichen Eigenschaften zu organisieren.
Künstlicher Bienenstock-Algorithmus (ABHA): Theorie und Methoden Künstlicher Bienenstock-Algorithmus (ABHA): Theorie und Methoden
In diesem Artikel geht es um den 2009 entwickelten Artificial Bee Hive Algorithm (ABHA). Der Algorithmus ist auf die Lösung kontinuierlicher Optimierungsprobleme ausgerichtet. Wir werden uns ansehen, wie ABHA sich vom Verhalten eines Bienenvolkes inspirieren lässt, in dem jede Biene eine einzigartige Aufgabe hat, die ihr hilft, Ressourcen effizienter zu finden.
Chaostheorie im Handel (Teil 2): Tiefer tauchen Chaostheorie im Handel (Teil 2): Tiefer tauchen
Wir setzen unsere Untersuchung der Chaostheorie auf den Finanzmärkten fort. Dieses Mal werde ich seine Anwendbarkeit auf die Analyse von Währungen und anderen Vermögenswerten untersuchen.