
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 16): Auswirkungen unterschiedlicher Kursverläufe auf die Testergebnisse
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.
//+------------------------------------------------------------------+ //| 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





- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.